Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
d8a90a3
feat(sdk)!: generate into the destination itself, dropping --api-vers…
asadali214 Oct 30, 2026
bc51663
feat(sdk): record the first generation under git with --git-init
asadali214 Nov 2, 2026
82fd92d
feat(sdk): merge a regeneration into an SDK that keeps your edits
asadali214 Nov 2, 2026
7fcd38f
fix(sdk): quote the paths in the git commands a merge prints, for cmd…
Shield-Jaguar Nov 2, 2026
2e65e27
refactor: pluralise counts through one helper, and refresh the skill …
Shield-Jaguar Nov 2, 2026
b2af733
feat(sdk): ask how a regeneration's conflicts are resolved
Shield-Jaguar Nov 3, 2026
b2892d9
feat(sdk): settle or refuse what the last run left unresolved before …
asadali214 Nov 3, 2026
0eec5e8
feat(sdk): fetch the snapshot a clone lacks from its remote before re…
asadali214 Nov 3, 2026
48eaccf
feat(sdk): share the snapshots on every push through a pre-push hook
asadali214 Nov 3, 2026
f56d740
feat(sdk): publish an SDK that keeps your edits, uploading what git w…
asadali214 Nov 3, 2026
c64b480
docs(sdk): write up custom code, match the SDK flags to the docs, and…
asadali214 Nov 3, 2026
20948db
feat(sdk)!: drop --codegen-version from sdk generate and sdk publish
asadali214 Nov 3, 2026
b6afbc8
fix(sdk): make the hook executable without widening who may read it
asadali214 Nov 3, 2026
c2c2f1f
fix(sdk): name the previous ref in the restore command, not a commit id
asadali214 Nov 3, 2026
795b698
fix(sdk): write the generation record only when its content changed
asadali214 Nov 3, 2026
1c0a72d
fix(sdk): say the SDK directory could not be inspected for customizat…
asadali214 Nov 3, 2026
77388b0
fix(sdk): count every settled file in the summary, so it never reads …
asadali214 Nov 3, 2026
7d6b784
fix(sdk): warn and record afresh when git fails after the snapshot wa…
asadali214 Nov 3, 2026
09b030a
fix(sdk): hand the refs git lists on stdin back to the rest of the pr…
asadali214 Nov 3, 2026
a1adfcf
fix(sdk): read the ignore rules for an upload with any git found, old…
asadali214 Nov 3, 2026
c2ce9b5
docs(sdk): note the stdin hand-back and the old-git ignore rules in t…
asadali214 Nov 3, 2026
c127cec
style(sdk): wrap the generate command's import as prettier does
asadali214 Nov 3, 2026
c8acb68
refactor(sdk): read the repository root through module functions, not…
asadali214 Nov 3, 2026
fe40517
test(sdk): drop the CommitId import the restore prompt no longer needs
asadali214 Nov 3, 2026
a421176
refactor(sdk): read markers, diff records and the summary with less n…
asadali214 Nov 3, 2026
1a3bdef
fix(cli): run the help command the not-found hook offers
asadali214 Nov 3, 2026
37d73a1
style: settle the remaining Sonar findings
asadali214 Nov 3, 2026
133d651
Merge remote-tracking branch 'origin/dev' into asadali214/custom-code…
asadali214 Nov 3, 2026
fdeeb26
test(sdk): drop tests that repeat coverage at another layer
Shield-Jaguar Nov 4, 2026
4ed9d62
Merge branch 'dev' into asadali214/custom-code-redesign
asadali214 Nov 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions .ai/instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ Supporting: **Types** (`src/types/`) for value objects, context objects, and dom
- **Temp directories** — always use `withDirPath()` wrapper, never `tmp-promise` directly
- **`authKey` typed as `string | null = null`**, not `undefined`
- **Exit via `outro(result)`** — sets `process.exitCode`; never call `process.exit()` directly
- **`ActionResult` variants** — `success()`, `failed()`, `cancelled()`, `stopped()` (for long-running server commands); exit codes 0 / 1 / 130 respectively
- **`ActionResult` variants** — `success()`, `failed()`, `cancelled()`, `stopped()` (for long-running server commands), `actionRequired()` (the developer has something left to do, such as conflicts to resolve; yellow outro); exit codes 0 / 1 / 130 / 130 / 1 respectively
- **Constructor pattern**: `private readonly` properties, `public readonly execute = async (...) => { ... }`
- **`static cmdTxt`** on every Command using `format.cmd(...)` for example rendering
- **Commands use `export default class`** — oclif requires default export; actions, prompts, and services use named exports (`export class`)
Expand All @@ -102,7 +102,14 @@ Supporting: **Types** (`src/types/`) for value objects, context objects, and dom
- **Reuse infra** — network, file I/O, OS calls, sleep, time: use existing services, Node.js standard libraries and built-in JS APIs.
- **Readability is king** — simple names, simple domain models, composed with simple functions that turn one type into another instead of passing primitives around.
- **Validate once, up front** — no repeated runtime type checks; later code works with guaranteed types.
- **Telemetry** — After `outro(result)`, commands optionally track failures via `result.mapAll(() => {}, async () => { await new TelemetryService(configDir).trackEvent(new SomeFailedEvent(...), shell) }, () => {})`. Event classes extend `DomainEvent` (`src/types/events/`). Only the failure callback is populated; success/cancel are no-ops.
- **Async `Result`s are `Promise<Result<T, E>>`** — an `async` body that `await`s each step and returns early on `isErr()`. No `ResultAsync`, `safeTry`, `okAsync` or `errAsync` in new code: a signature never names `ResultAsync`, and a throwing promise becomes a `Result` through `try`/`catch` around the `await`.
- **No private instance methods** — a class has `private readonly` fields and public methods; a helper is a `private static` method or a module-level function.
- **No middlemen** — a method does something with each argument (reads it, transforms it, or combines it with the object's own state); no method only forwards to another call, and no layer re-exposes a service one to one.
- **Functional core, imperative shell** — a step reads every fact it needs first, decides as a pure function over that data, then does its I/O. `execute` is the script of the run and the only place that speaks; prompts word the data they are handed and decide nothing; a value with several outcomes (`ShareHook`, `SnapshotFetch`, `ActionResult`) offers `match` over named handlers, so a caller that leaves one out fails to compile; contexts answer data and never print.
- **Good code is not more functions** — a step's logic stays together where it reads as one thing; a function is split out only when it is used twice, when it separates a decision from I/O, or when its name says something the reader would otherwise work out. A one-line call does not get a wrapper.
- **One prompts class per action** — wording is repeated per class rather than shared through a prompt module.
- **Flag descriptions match the docs** — a flag's `description` (which the generated README carries) and its row in apimatic-docs' CLI Reference (`v4/src/content/apimatic-cli/cli-reference.mdx`) say the same words; change both, or flag the mismatch.
- **Telemetry** — After `outro(result)`, commands optionally track failures via `result.match({ success: () => {}, failed: async () => { await new TelemetryService(configDir).trackEvent(new SomeFailedEvent(...), shell) }, cancelled: () => {}, actionRequired: () => {} })`. Event classes extend `DomainEvent` (`src/types/events/`). Only the `failed` handler is populated; the others are no-ops.

## Branching

Expand Down
577 changes: 577 additions & 0 deletions .ai/plans/custom-code-phases.md

Large diffs are not rendered by default.

633 changes: 633 additions & 0 deletions .ai/plans/custom-code.md

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion .ai/skills/action.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ Actions live at `src/actions/` and are the single use-case orchestrators for eac

- **DO** use named export: `export class {PascalName}Action`.
- **DO** use `public readonly execute = async (...): Promise<ActionResult> =>` (arrow function property).
- **DO** make `execute` the script of the run, read top to bottom, and the only place that speaks: every question and message is a `this.prompts.*` call in it, every fact a `const`, and the action has no other method. A helper is a `private static` method or a module-level function, never a private instance method.
- **DO** `await` each `Result` a context or service answers with and return early on `isErr()` — never chain `andThen` or `asyncAndThen` on a promise, and never import `ResultAsync`.
- **DO** initialize prompts as a field: `private readonly prompts = new {Name}Prompts()`.
- **DO** initialize services as `private readonly` fields (inline with `new`) when the service takes no constructor args. If a service needs `configDir`, declare without initializer and assign in the constructor body after `this.configDir` is set.
- **DO** use the full constructor pattern when the action calls an API: `constructor(configDir: DirectoryPath, commandMetadata: CommandMetadata, authKey: string | null = null)`.
Expand Down Expand Up @@ -72,7 +74,7 @@ Actions live at `src/actions/` and are the single use-case orchestrators for eac
| Standard action (auth + services + withDirPath) | `src/actions/api/validate.ts` |
| Standard action (multiple params + overwrite guard) | `src/actions/api/transform.ts` |
| Standard action (Context + TempContext + service) | `src/actions/portal/generate.ts` |
| Standard action (SDK generation + version selection) | `src/actions/sdk/generate.ts` |
| Standard action (SDK generation + git-backed regeneration) | `src/actions/sdk/generate.ts` |
| Standard action (gate + services + withDirPath) | `src/actions/portal/generate.ts` |
| Minimal action (configDir only, no services) | `src/actions/auth/logout.ts` |
| Minimal action (configDir + commandMetadata, no auth) | `src/actions/auth/status.ts` |
Expand Down
3 changes: 2 additions & 1 deletion .ai/skills/command.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ These three layers are always created together and mirror each other in path str
- **DO** use typed path objects (`DirectoryPath`, `FilePath`, `FileName`, `UrlPath`) instead of raw strings.
- **DO** use `ActionResult.success(value?)`, `ActionResult.failed(message?)`, or `ActionResult.cancelled()` as return values.
- **DO** use neverthrow `Result<T, E>` for service returns — check with `.isErr()` / `.isOk()`.
- **DO** type an async `Result` as `Promise<Result<T, E>>` and `await` it before checking — never `ResultAsync`, `safeTry`, `okAsync` or `errAsync`.

### General — DON'T

Expand Down Expand Up @@ -128,7 +129,7 @@ These three layers are always created together and mirror each other in path str
| Action with Result error handling | `src/actions/api/validate.ts` |
| Action with simpler constructor (no authKey) | `src/actions/auth/status.ts` |
| Action that delegates to sub-actions | `src/actions/quickstart.ts` |
| Prompts with spinner + confirm + select | `src/prompts/sdk/generate.ts` |
| Prompts with spinner + confirm + noteWrapped | `src/prompts/sdk/generate.ts` |
| Prompts with error display methods | `src/prompts/portal/generate.ts` |
| Prompts with format helpers | `src/prompts/api/transform.ts` |
| Prompts with noteWrapped + getTree | `src/prompts/quickstart.ts` |
Expand Down
11 changes: 6 additions & 5 deletions .ai/skills/context.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@ Context objects live at `src/types/` and encapsulate path derivation, validation
- **DON'T** expose derived file names — methods like `getScriptFileName()` leak internal naming logic.
- **DON'T** add public properties for internal state — constructor parameters are `private readonly`, not exposed.
- **DON'T** use `console.log` or any prompt output — contexts are silent.
- **DON'T** use `Result<T, E>` for a plain file operation — return `boolean`, `undefined` or the value. A context returns a `Result` only when it does network I/O (rare) or reports a structured problem the prompts switch on, as `PortalSourceContext.resolve()` does with `PortalSourceProblem`.
- **DON'T** use `Result<T, E>` for a plain file operation — return `boolean`, `undefined` or the value. A context returns a `Result` only when it does network I/O (rare) or reports a structured problem the prompts switch on, as `PortalSourceContext.resolve()` does with `PortalSourceProblem`. An async one is `Promise<Result<T, E>>`, written with `await` and early returns — never `ResultAsync` or `safeTry`.
- **DON'T** use raw string paths — always wrap in `DirectoryPath`, `FilePath`, `FileName`.
- **DON'T** add methods that only use infrastructure services (`fileService`, `zipService`) without touching domain-specific private fields — these are stateless utilities, not context behavior. Every public method must use at least one constructor-derived private field (e.g., `sdkDirectory`, `language`). If a method takes all its inputs as parameters and never reads context state, it belongs in a service or a different context.
- **DON'T** embed the context's own domain subject in method names — if the class is `SdkContext`, a method named `cleanUpSdkDirectory()` is redundant. Prefer concise behavioral verbs: `cleanUp()`, `getChanges()`, `save()`.
- **DON'T** embed the context's own domain subject in method names — if the class is `PortalContext`, a method named `cleanUpPortalDirectory()` is redundant. Prefer concise behavioral verbs: `cleanUp()`, `getChanges()`, `save()`.
- **DON'T** keep a private instance method — a helper is a `private static` method or a module-level function, and every public method does something with each of its arguments.
- **DON'T** pass internal paths through callback parameters — `onSomething: (path: DirectoryPath) => void` is the same leakage as a public getter. If the caller needs the path, return it as the result of the operation (`Promise<DirectoryPath | undefined>` etc.).

### Variant-specific rules
Expand Down Expand Up @@ -83,8 +84,8 @@ Context objects live at `src/types/` and encapsulate path derivation, validation
| Input context (validate + file ops) | `src/types/spec-context.ts` | Good — zip detection internal |
| Temp context (zip + save stream) | `src/types/temp-context.ts` | Good — UUID naming internal |
| Temp context (download + resolve) | `src/types/resource-context.ts` | Good — URL/file decision internal |
| Composite (delegates to other contexts) | `src/types/project-context.ts` | Good — hands back `PortalSourceContext`, `PluginConfigContext` and `SdkContext`; knows the layout so no caller joins `src` |
| Output context (leaky — avoid pattern) | `src/types/sdk-context.ts` | Avoid — exposes `sdkLanguageDirectory`, has methods that only use infrastructure services without touching domain state |
| Composite (delegates to other contexts) | `src/types/project-context.ts` | Good — hands back `PortalSourceContext` and `PluginConfigContext`, and derives the SDK directory; knows the layout so no caller joins `src` |
| Directory under git (facts, plans, snapshots) | `src/types/git-context.ts` | Good — one directory, git and the disk; every method reads the directory, and answers values (`RepositoryState`, `MergePlan`, `ShareHook`) the prompts word |
| Input context (validate + typed result) | `src/types/portal-source-context.ts` | Good — reports problems, exposes no paths |

---
Expand All @@ -96,7 +97,7 @@ Use when creating a new Context class. Choose the variant that matches the domai
### What to determine

1. **Context name** — lowercase hyphenated (e.g., `portal`, `sdk`, `build`). File will be `{name}-context.ts`
2. **Class name** — PascalCase (e.g., `PortalContext`, `SdkContext`)
2. **Class name** — PascalCase (e.g., `PortalContext`, `GitContext`)
3. **Variant** — one of:
- `output` — save/exists pattern for command outputs (portals, SDKs, transformed files)
- `input` — validate/read pattern for user-provided input directories
Expand Down
4 changes: 2 additions & 2 deletions .ai/skills/event.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export class {PascalName}Event extends DomainEvent {
- **DO** place the file at `src/types/events/{kebab-name}.ts`.
- **DO** use `.js` extension in the import: `import { DomainEvent } from "./domain-event.js"`.
- **DO** fire events from the Command layer only — never from Actions or Services.
- **DO** fire success events inside the `mapAll` success callback (first arg); failure events in the failure callback (second arg).
- **DO** fire success events inside the `success` handler of `result.match(...)`; failure events in its `failed` handler.

### DON'T

Expand All @@ -92,7 +92,7 @@ export class {PascalName}Event extends DomainEvent {
- [ ] `message` and `commandName` are `private static readonly ... as const` (self-contained variant)
- [ ] Named export (not default)
- [ ] Import uses `.js` extension: `"./domain-event.js"`
- [ ] Fired from Command layer only, inside `result.mapAll(...)` callback
- [ ] Fired from Command layer only, inside a `result.match(...)` handler

---

Expand Down
3 changes: 2 additions & 1 deletion .ai/skills/prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Prompts live at `src/prompts/` and are the sole terminal UI layer for each comma

- **DON'T** use a constructor — no parameters, no fields.
- **DON'T** put business logic in Prompts — only UI rendering and user interaction. This is a wrapper around `@clack/prompts`, nothing more.
- **DON'T** decide anything from what a value means — a prompt handed a value with several outcomes (`ShareHook`, `SnapshotFetch`) calls its `match` with one handler per outcome and words each; the deciding was done where the value was made.
- **DON'T** use `console.log` — use `log.*` from `@clack/prompts`.
- **DON'T** skip the `isCancel()` guard on interactive prompts — omitting it causes a crash when the user presses Ctrl+C.
- **DON'T** return the raw `symbol` value from interactive prompts — always guard with `isCancel()` first.
Expand Down Expand Up @@ -65,7 +66,7 @@ Prompts live at `src/prompts/` and are the sole terminal UI layer for each comma
| Simple (log + spinner, no interactive prompts) | `src/prompts/auth/login.ts` |
| Simple (log-only display methods) | `src/prompts/auth/status.ts` |
| Standard (confirm + log + spinner) | `src/prompts/portal/generate.ts` |
| Standard (confirm + select + log + spinner) | `src/prompts/sdk/generate.ts` |
| Standard (confirm + log + spinner + noteWrapped) | `src/prompts/sdk/generate.ts` |
| Standard (log + spinner + structured display) | `src/prompts/api/validate.ts` |
| Delegation (select flow + welcome message) | `src/prompts/quickstart.ts` |
| Wizard (multi-step, noteWrapped, getTree) | `src/prompts/portal/quickstart.ts` |
Expand Down
2 changes: 2 additions & 0 deletions .ai/skills/service.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Services live at `src/infrastructure/services/` and are the only layer that make

- **DO** use named export: `export class {PascalName}Service`.
- **DO** return `Promise<Result<T, ServiceError>>` from all public methods — use `ok()` and `err()` from neverthrow.
- **DO** write the body as `async`/`await`: wrap the call in `try`/`catch`, `await` each step, and return `err(...)` early when a step `isErr()`.
- **DO** catch `ProblemDetailsError` first (for 400/403 with structured error messages), then fall back to `handleServiceError(error)`.
- **DO** close file streams in `finally` blocks when using `FileWrapper` or `getStream()`.
- **DO** resolve auth as: `getAuthInfo(configDir.toString())` → `createAuthorizationHeader()` → `apiClientFactory.createApiClient()`.
Expand All @@ -23,6 +24,7 @@ Services live at `src/infrastructure/services/` and are the only layer that make
### DON'T

- **DON'T** throw — always return `err(...)` wrapped in `ServiceError`.
- **DON'T** return or chain `ResultAsync` — no `ResultAsync`, `safeTry`, `okAsync` or `errAsync`; the return type is `Promise<Result<T, E>>` and a step is checked with `isErr()`, not composed with `andThen`.
- **DON'T** use `console.log` — services are silent; errors communicated via `Result`.
- **DON'T** use raw string paths — use `DirectoryPath`, `FilePath`, `UrlPath`, `FileName`.
- **DON'T** use `axios.create()` as a class field — create a fresh instance per call in `axiosInstance()` method.
Expand Down
6 changes: 3 additions & 3 deletions .ai/skills/value-object.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Value objects live at `src/types/file/` and wrap a single primitive (`string`, `
- **DO** have methods that produce derived values return rich types — a method extracting a leaf name returns `FileName`, not `string`.
- **DO** compose rich types through each other — `FilePath` holds `DirectoryPath` and `FileName` as objects; both are passed as objects in constructors and methods.
- **DO** use `static create()` returning `T | undefined` for construction from user input or external data that can fail validation.
- **DO** use `static fromGit()` returning `T | undefined` for a value git printed (an id, a path, a ref): it validates as `create()` does, and the service turns `undefined` into an `err`, never a throw.
- **DO** use `static createInput()` for the flag-value-to-rich-type pattern when a sensible fallback exists (e.g. `DirectoryPath.default`).
- **DO** use `static readonly` for well-known singleton instances (e.g., `DirectoryPath.default`).
- **DO** access the private field directly inside methods — write `this.directoryPath` not `this.toString()`.
Expand Down Expand Up @@ -75,7 +76,7 @@ Anything not in this table is a violation. In particular: domain methods, contex

### Context class exposure rule

Context classes (`PortalContext`, `SdkContext`, etc.) may expose a rich-type field via a public getter or method **only** when the caller needs it for **logging or prompting display** — never for path computation, I/O, or construction of other paths. Add an inline comment when you do:
Context classes (`PortalContext`, `GitContext`, etc.) may expose a rich-type field via a public getter or method **only** when the caller needs it for **logging or prompting display** — never for path computation, I/O, or construction of other paths. Add an inline comment when you do:

```ts
// Exposed for display in prompts — callers must not use this for path computation
Expand All @@ -93,7 +94,6 @@ These patterns exist in the current codebase and are flagged for awareness:
| File | Issue |
|---|---|
| `src/types/file/directoryPath.ts:31` | `leafName()` returns `string` — should return `FileName` |
| `src/types/sdk-context.ts` | `sdkLanguageDirectory` getter exposes `DirectoryPath` — needs justification comment if kept |


---
Expand Down Expand Up @@ -125,7 +125,7 @@ These patterns exist in the current codebase and are flagged for awareness:
| Rich predicate (`hasExtension`) and transform (`normalize`) returning rich type | `src/types/file/fileName.ts` |
| Composed value object — holds two rich types, `static create()` splits at boundary | `src/types/file/filePath.ts` |
| Correct boundary unwrapping — all `.toString()` calls at `fs.*` / `path.*` sites | `src/infrastructure/file-service.ts` |
| Context with flagged public getter (display use) | `src/types/sdk-context.ts` |
| Values git printed, through `static fromGit()` | `src/types/file/relativeFilePath.ts`, `src/types/git/commit-id.ts` |

---

Expand Down
Loading
Loading