Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 11 additions & 0 deletions .github/workflows/portal_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,17 @@ jobs:
PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}\.playwright-browsers
run: pnpm test:visual

# The nav.json layouts that build. Each scenario generates its own copy
# of the suite's petstore build with this CLI and serves it on its
# worker's port (8090-8092), three at a time, so this needs neither the
# generate step above nor its server, and runs even if they failed.
- name: visual — nav.json
if: ${{ !cancelled() }}
working-directory: tests
env:
PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}\.playwright-browsers
run: pnpm test:nav:visual

# Functional runs against the suite's auth build, whose playground calls
# the API Tester (tests/api-tester/) on localhost:3000.
# Playwright starts the API Tester itself (webServer in playwright.config.ts),
Expand Down
Loading
Loading