diff --git a/.ai/plans/custom-code.md b/.ai/plans/custom-code.md new file mode 100644 index 000000000..b22741402 --- /dev/null +++ b/.ai/plans/custom-code.md @@ -0,0 +1,560 @@ +# Plan: keep your edits when regenerating an SDK + +Status: implemented on branch `asadali214/custom-code-injection`, cut from `dev` at +`4baa9485`, in nine phases reviewed one by one between 2026-10-20 and 2026-10-22 (section 8 +lists the commits); draft PR #445 into `dev`. Section 10 is the architecture, layer by layer; +section 11 lists what still needs a decision. The design was tested against Speakeasy's +generator first; section 7 records what that lab measured and what this plan does on the +same cases, and the APIMatic Customization Lab replays them on the real CLI. + +## 1. Goal and scope + +A developer edits a generated SDK, any file and any line, and regenerates it with the same +command; their edits survive, the generator's changes arrive, and the two are merged file by +file with git's three-way merge. Nothing to configure, no markers or regions to stay inside, +no commit required before regenerating. + +What changes for the developer: + +- **Git is required.** The CLI uses the git on `PATH`, 2.38 or newer, for the snapshots and + the merge. Without it `sdk generate` behaves exactly as before, writes no `.git` and no + `.apimatic/`, and warns: "git was not found, so your customizations to the generated SDK + will not persist across regenerations". +- **First run**: as before, a spec and a language; the SDK lands in `/sdk/`, + or in the `--destination` itself when one is named, with no language directory added below it. + If no git repository encloses that directory, the CLI runs `git init` in it and says so. +- **A clone**: `--destination` naming a clone of an SDK repository regenerates over the clone, + and merges when the clone holds a generation record. A destination holding the project's + `src` is refused, since the SDK directory is emptied before a write that does not merge. +- **Regenerate in place**: the same command. Generator changes are merged into edited files, + new files are added, files the generator stopped producing are removed when unedited. +- **Conflicts are explicit**: overlapping changes get git-style markers, the run exits 1 and + lists the files. Resolve, rerun. +- **Teams**: the first recorded run installs a `pre-push` hook, and from then on every push + of a branch also pushes the snapshot refs; a clone fetches them before merging. The CLI + never pushes (3.2). +- **No base, no merge**: an SDK directory with no generation record, or whose recorded + snapshot is missing, gets the "overwrite?" question as before; the run then records a + snapshot so the next one merges. + +Terms, also in `CONTEXT.md`: the **SDK directory** is the directory the CLI writes into (the +`--destination` as named, else `sdk/[/]`), input and output of every later run; the **repository** is the nearest git +repository enclosing it; a **pristine snapshot** is one generation's output exactly as the +server returned it, stored as a commit under a hidden ref; the **generation record** is +`/.apimatic/generation.json`, naming the snapshot the directory was last written from; +**custom code** is whatever differs between the SDK directory on disk and its snapshot. + +Out of scope: an ignore list for files the developer wants frozen, patch files, code regions, +portal downloads that include custom code (section 11), and any push by the CLI itself. + +## 2. Decisions + +| Ask | Decision | Why | +|---|---|---| +| Where the merge runs | In the CLI, against the working tree. The server is unchanged. | Nothing is uploaded or downloaded beyond the SDK zip, the merge sees uncommitted edits, and the design matches Speakeasy's, which the lab showed working. | +| When it merges | Always, whenever git is available, the SDK directory sits in a git repository and has a generation record. No switch in `apimatic.json`. | One rule to explain: "with git installed, your edits are kept". A switch would be read only after the first edit is already lost. | +| Git is a hard requirement | The system git, 2.38 or newer, found on `PATH` at the start of every run. Absent or older: today's flow, no `.git` or `.apimatic/` written, one warning. No bundled fallback engine. | Git's merge is the reference everyone else measures against: rename detection, modify/delete and add/add semantics, binary handling. Generators that embed a library write their own diff3 (Speakeasy, isomorphic-git); tools that accept git as a prerequisite shell out to it (JHipster, Copier, Cruft); none reimplements rename detection. 2.38 (October 2022) is the first with `merge-tree --write-tree`. | +| No repository | After the generation succeeds and before the first write, `git init -b main` in the SDK directory, reported afterwards. A non-empty directory without a repository is asked "overwrite?" first. | Without a snapshot of the first generation there is nothing to merge against later. The init is inside the developer's own destination directory. | +| Repository found but no record | Ask "overwrite?" as before (skipped by `--force`); then record a snapshot. | An SDK generated by an older CLI, copied in, or built from the portal has no base. | +| Record for another generation | A record for another stability (or generator) is "no base": a warning, the overwrite question, a fresh snapshot. | Merging a beta generation over a stable base would be a merge of everything; replacing is what the developer asked for. | +| Remote | Read only by the CLI, and only when a record names a commit the repository lacks. Writing is the developer's `git push`, through a `pre-push` hook the CLI installs (decided 2026-10-22, replacing the push inside the CLI). | A snapshot pushed by the CLI reached the remote before the branch that records it, and never when the push failed or the remote came later; the hook ties the snapshot to the push of the branch, so the two arrive together, every time. | +| Merge engine | `git merge-tree --write-tree` between two temporary commits (yours and the generation) that share the pristine snapshot as parent, run with the CLI's own `-c` settings. The CLI reads the result and writes it into the SDK directory. | No merge code to maintain; git supplies the three-way merge, rename detection and every conflict shape, without touching the working tree, the index or any branch. | +| Branches | None. Snapshots are commits under `refs/apimatic/…`, never checked out, never on a branch. | The CLI should not create or switch branches in someone's repository. The base for a run is the one the committed record names, so each branch carries its own. | +| Moves and renames | Git's rename detection, both ways, at its usual 50% similarity. | The lab's case 6 lost edits because Speakeasy relies on a header comment; git pairs by content. | +| Same path added by both | A conflict, unless the contents are identical. | Git semantics; Speakeasy overwrites the developer's file silently. | +| Generator drops an edited file | A conflict: the file stays, listed as "no longer generated". | Lab case 7: Speakeasy deletes it with no warning. | +| Developer deleted a file the generator changed | A conflict: the new version is written back and listed. Needs sign-off, section 10. | Git semantics. The alternative, remembering the deletion, is silent and recurs on docs files. | +| `--force` | Keeps its meaning: never ask. With a base it changes nothing, since nothing is asked. | CI runs already pass it. | +| Undo | Not offered by the CLI (decided 2026-10-23). The SDK directory as it was before the run is still kept under `refs/apimatic/previous/…`, overwritten each run, for a developer to restore by hand. | The first question can cancel before anything is written, so a way back is needed only after a run that could not ask; the ref covers that at the cost of one commit per run. | +| Conflicts in git's index | The conflicted paths are put into the repository's index at stages 1, 2 and 3. | `git status` says "both modified" and `git mergetool` opens them, as after a merge git ran itself. | +| One action | `GenerateAction` is the whole run: no `SdkPrepareAction`, no `SdkWriteAction` (decided 2026-10-22). | Each phase had one caller and could not run alone, and passing `SdkDestination` between actions through `ActionResult.getValue()` put data on a result that is meant to carry only an outcome. | +| Functional core, imperative shell | `execute` is 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, and the action has no other method. The prompts word the data they are handed and decide nothing, not even which of their own messages fits. The I/O is on the contexts (`SdkContext.destination`, `SdkRepositoryContext.regenerate` and `write`); the decisions are pure functions in `src/types/sdk/`: `destinationFor`, `replacesDirectory`, `resolving`, `favouring`, `MergePlan.taking`, `settledContent` (decided 2026-10-22). | The reader of `execute` sees what is asked and said, and when, without a hop; what a repository state or a conflict choice means is testable without git or a terminal; the contexts stay silent, so they can be composed. | +| Telemetry | None for now. `SdkMergeConflictedEvent`, `ConflictOutcome` and the result's payload are gone; `execute` returns a plain `ActionResult` (decided 2026-10-22). | An `ActionResult` carries no data. Conflict counts can come back later through a callback the command hands the action, as `sdk publish` does with `onPublishSdkError`. | +| Two contexts | `SdkContext` is the directory of this run; `SdkRepositoryContext` is that directory once git says a repository encloses it, and holds everything that needs the root. They stay two classes (decided 2026-10-22). | A first run's directory may not exist and may have no repository, so `repository()` can answer "none". One class would make every snapshot method begin by asking for a root that may not be there, or hold a mutable "opened" state; the second class makes "a repository was found" a fact the type carries, checked once. | + +## 3. What is saved, and where + +Two hidden refs per SDK directory plus one small file inside it. `` below is the SDK +directory relative to the repository root with forward slashes (`sdk/typescript`, or +`sdk/2.1.0/typescript` with `--api-version`); when the SDK directory is the repository root +the ref has no suffix. + +| Item | Holds | Written | Shared | +|---|---|---|---| +| `refs/apimatic/pristine/` | A chain of commits, one per generation. Each tree is that generation's output exactly as unzipped: no `.git`, no `.apimatic/`, no developer files. | After every successful write, with plumbing: the generated directory is added to a temporary index (`GIT_INDEX_FILE`, `--work-tree`), then `write-tree`, `commit-tree` and `update-ref`. Author "APIMatic CLI ", message `apimatic: pristine typescript v4 stable`. | Pushed to the remote after every recorded run; fetched by a run whose record names a commit the repository lacks (3.2). | +| `refs/apimatic/previous/` | One commit, no parent: the SDK directory as it was on disk the moment before the last run wrote to it (`.git` excluded). Its tree is rooted at the SDK directory, not at the repository root. | Before the merge writes anything. Overwritten each run. | No. Local safety net: `git --work-tree=sdk/typescript restore --source=refs/apimatic/previous/sdk/typescript --worktree -- .` puts the files back, record included (verified in the lab, case 15); files the run added stay and `git status` lists them. | +| `/.apimatic/generation.json` | `schemaVersion`, `language`, `codegenVersion`, `stability`, `pristine` (the commit the directory was written from), `generatedAt`, `cliVersion`. The files a run leaves in conflict are not in it: they are git's unmerged index entries, the only state that says when the developer has resolved them. | After the pristine commit exists, so the two never disagree; `.apimatic/.gitattributes` (`generation.json merge=binary linguist-generated=true`) first, each only when its content changed. | Yes: an ordinary tracked file, committed with the SDK. It is what makes a branch, a worktree or a clone merge against the right base. The attributes keep a pull where two clones both regenerated from writing markers into it (section 11). | + +### 3.1 Which snapshot is the base + +The commit named in the generation record on disk, never the ref's tip. The tip only says +where the chain continues. If the named commit is absent locally, the CLI fetches the ref from +the remote (3.2) and looks again; if it is still absent, it says so and why (no remote, the +remote lacks it, or the fetch failed and how), and offers to overwrite instead, which records a +fresh snapshot. The commit id and the ref are not shown: nothing in the message is for the +developer to act on beyond the question that follows. + +### 3.2 Sharing through a pre-push hook + +The snapshot reaches the remote at the same moment the record does: when the developer pushes +a branch. After every recorded run the CLI writes two things into git's own hooks directory +(`git rev-parse --git-common-dir`, so worktrees share it, whatever `core.hooksPath` says): the +script, `apimatic-pre-push` (`SHARE_HOOK_SCRIPT` in `src/types/sdk/share-hook.ts`, rewritten +only when its content changed), and a three-line marked block in `pre-push` that calls it +(`# >>> apimatic snapshots v2` … `# <<< apimatic snapshots`): a fresh hook when there is none, +right after the shebang of an existing shell hook (so an early `exit` below cannot skip it), +replacing an older block, and left alone when the current one is there. Nothing is printed: +the hook is part of keeping customizations, not something a run announces. The CLI never +pushes. + +The call is one line that finds the script through git, does nothing where the script is not +there (a clone that never ran the CLI) and never fails the push: +`f="$(git rev-parse --git-common-dir)/hooks/apimatic-pre-push"; [ -f "$f" ] && sh "$f" "$@" || true`. +That is what makes the other cases a one-step note rather than a warning with nothing to do. + +The script, for the remote git passes as `$1`: push `refs/apimatic/pristine` and every ref below +it (an SDK at the repository root has the ref itself, so `refs/apimatic/pristine/*` alone would +miss it) with `--no-verify` (so the push inside the hook does not re-enter the hook) and +`GIT_TERMINAL_PROMPT=0`; when that is rejected, fetch the remote's chains with +`+refs/apimatic/pristine*:refs/apimatic/remotes//pristine*` (the `*` may match nothing), +fast-forward or join each local chain with a two-parent commit (`apimatic: pristine chains +joined`), and push again. A second failure prints one line in the server's words; the script +always exits 0, and runs in its own `sh`, so a hook's `set -e` cannot be tripped by it. The +common push costs one extra push of small refs; the fetch happens only when another machine +pushed first. + +Tracking names, not the local chain, are what the fetch writes to: a plain +`refs/apimatic/pristine/*` refspec made a diverged chain reject the fetch and `git pull` skip +its merge, and a forced one would strand unpushed snapshots. + +When `core.hooksPath` is set, the CLI does not write the hook (husky regenerates `.husky/_` on +install, and the team's versioned `.husky/pre-push` is theirs). The run shows a next-steps note +naming the file that hook runs (`.husky/pre-push` for a hooks path ending in `_`, else +`/pre-push`) and the line to add; once that file mentions the script, the note +stops. The same note, without the commit step, when the existing `.git/hooks/pre-push` is not +a shell script. + +The CLI's only remote operation is the fetch inside `inspect`, when the record names a commit +the repository lacks: from the branch's remote (`branch..remote`) else `origin`, without +prompts, with a 30-second timeout, and joining the local chain when it had diverged. A +snapshot that is nowhere is reported as missing and the run asks before replacing. + +### 3.3 Rules + +- The ref name is the path, so an SDK directory nested inside another SDK directory is + refused before anything is uploaded (the refs would collide). +- `.git/` is never part of a snapshot. `.apimatic/` is in a previous snapshot (so an undo + restores the record too) but never in a pristine one, and the merge leaves it alone since + only the disk side has it. +- A snapshot of the disk honours the SDK's own `.gitignore` (so a `node_modules` or `dist` + never enters a tree), except that every path the pristine snapshot holds is added + regardless, so a generated file an ignore rule happens to match is still seen and never + mistaken for one the developer deleted. +- The developer's git configuration is bypassed: every git call runs with + `-c core.autocrlf=false`, `-c merge.conflictStyle=merge`, `-c merge.renormalize=true`, + raised rename limits, `gc.auto=0`, a temporary attributes file holding `* text=auto`, and + `LC_ALL=C`, so two machines produce the same trees and the same markers and git's messages + stay parseable. +- Snapshots are ordinary git objects: deduplicated, packed by the developer's own `git gc`, + invisible to `git log` and to a plain `git push`. +- `.apimatic/` never ships inside a published package: the three v4 generators' manifests are + whitelists that do not name it (verified 2026-10-20, section 10). + +## 4. One run of `apimatic sdk generate` + +### 4.1 Before this plan + +The destination was checked before anything was uploaded, and a non-empty one was emptied +before the fresh SDK was copied in. Nothing on disk survived a regeneration. + +### 4.2 With this plan + +```mermaid +flowchart TD + A([apimatic sdk generate -l typescript]) --> V{git 2.38+ on PATH?} + V -- no --> V1([Warn: customizations will not persist; today's flow, no .git, no .apimatic]) + V -- yes --> D{Generation record and its pristine commit found?} + D -- no, directory empty --> C1 + D -- no, directory not empty --> F{Overwrite? / --force} + F -- no --> X([Cancelled, nothing touched]) + F -- yes --> C1[Zip src/ and upload; poll; download the SDK zip into a temp directory] + C1 --> R1[git init in the SDK directory if no repository encloses it] + R1 --> E[Write the SDK; an overwrite empties the directory but keeps any existing .git] + D -- yes --> G{Markers left from the last run?} + G -- yes --> Y([Failed: resolve those files first]) + G -- no --> C2[Zip src/ and upload; poll; download the SDK zip into a temp directory] + C2 --> H[Save disk state to refs/apimatic/previous] + H --> I[git merge-tree of yours and the generation over the pristine base; write the result in place] + E --> J[Record pristine commit; write generation.json] + I --> J + J --> K{Conflicts?} + K -- no --> S([Success: summary of files]) + K -- yes --> T([Failed: list of conflicts and what to do]) +``` + +The run is one action, `GenerateAction.execute`, read top to bottom; every question and +message is in it (section 10). It validates as before, then `SdkContext.destination` says +how the SDK will be written, before the upload: + +1. `git --version`. Absent, or older than 2.38: warn that customizations will not persist; + `overwrite`, writing no `.git` and no `.apimatic/`. +2. `SdkContext.repository()` finds the enclosing repository (from the + nearest existing ancestor, since a first run's directory is not there yet), the nesting + check, the record, whether its commit is present, and which listed files still hold + markers. A nested SDK directory is refused here. +3. No repository, no record, a record for another generation, or a missing commit: `record`, + each with its message. Markers left: refused. Otherwise `merge` against the recorded base. +4. The overwrite question is asked for `overwrite` and `record` when the directory holds + files, never for `merge`, and never with `--force`. + +It asks the overwrite question, uploads, polls and downloads into a temp directory as +before, and puts the SDK in place the way the destination says: + +- `overwrite`: today's save into the directory; a `.git` in it stays. +- `record`: `git init -b main` in the SDK directory when nothing encloses it (after the + generation succeeded, so a failed one leaves no repository behind), the save, then the + pristine commit and the record. A failure to record is a warning: the SDK is on disk, and + the next run asks before replacing it. +- `merge`: `SdkRepositoryContext.regenerate` (the generated tree, the disk tree, + `merge-tree`) into a `Regeneration`; its conflicts settled into a `Settlement` (6.1); + `SdkRepositoryContext.write` (the previous ref, then the writes and deletions: only paths + whose content differs between the disk and the merged tree; the directory is never emptied); + the pristine commit and the record; the summary. With conflicts: the stages into the index, + the conflict block, and a failed result. + +## 5. The merge, file by file + +Three trees: base (the pristine snapshot), yours (the SDK directory on disk, snapshotted as +in 3.3) and new (the fresh generation). Yours and new are written as temporary commits whose +parent is the base, so `git merge-tree --write-tree yours new` finds the base itself and +returns a merged tree, the conflicted paths with their stages, and one informational line +per conflict naming its kind. The CLI does none of the deciding: + +| Base | Yours | New | Action | +|---|---|---|---| +| absent | absent | present | Write it (new generated file). | +| absent | present | present | Identical: nothing. Different: conflict add/add, markers around both versions. | +| present | = base | present | Write the new version (you never edited it). | +| present | edited | = base | Keep yours (the generator did not change it). | +| present | edited | = yours | Nothing. | +| present | edited | changed | Three-way line merge. Non-overlapping: write the merged file. Overlapping: conflict markers. | +| present | absent | = base | Stays deleted. | +| present | absent | changed | Conflict "deleted by you, changed by the generator": the new version is written back and listed. Section 10. | +| present | = base | absent | Delete it (the generator no longer produces it). | +| present | edited | absent | Conflict "no longer generated": your file stays and is listed. | +| present | absent | absent | Nothing. | +| absent | present | absent | Your own file: never touched. | + +Renames: git's detection on both sides at its default 50% similarity, with `merge.renameLimit` +raised. The generator renamed a file: your edits are merged into it at the new name and the +old name is removed. You moved a file: the generator's change to the old name is merged into +your file where it is now. A file both sides renamed differently is a rename conflict git +reports; the CLI lists it with git's message, and both candidates stay on disk. + +Content: trees are stored with `* text=auto` and merged with `merge.renormalize`, so a CRLF +file and its LF counterpart compare as equal and merge line by line; a file the merge changes +is written with LF, a file it leaves alone keeps its ending. Binary files are git's binary +files: unedited ones are replaced, edited ones are kept, and an edited one the generator also +changed is a conflict that keeps yours. + +## 6. Conflicts + +The merge finishes for every file; a conflict in one file does not stop the others. A line +conflict is written into the file, with git's labels (the temporary commit ids) replaced as +the file is written: + +``` +<<<<<<< Your changes + * Price in USD, VAT included. +======= + * Retail price in US dollars. +>>>>>>> Generated by APIMatic +``` + +The other kinds carry no markers: a file no longer generated stays as yours; a file you +deleted comes back in its new version; a binary keeps yours. All are listed by kind in a +block after the summary, "N files need your attention", with the steps: open each file, keep +what is right, remove the markers and stage the file (`git status` lists them as both +modified, `git mergetool` opens them, `git add` marks them resolved); rerun, nothing needs +committing first; the pre-run state is under `refs/apimatic/previous/…`. + +The result is failed, so the exit code is 1 and scripts and CI notice; the outro says the +conflicts need resolving, not that generation failed. Nothing is lost: every other file is +already updated, the pristine snapshot is recorded, the index holds the conflicted paths. + +The next run: while git still holds any file below the SDK directory as unmerged +(`GitService.unmergedStages`, `ls-files -u`), the run stops before generating and names them +(or asks, 6.1). That is git's own convention: the three stages stay in the index until +`git add` (or a mergetool, or an IDE's accept button) marks the file resolved, and `git commit` +refuses until then. The CLI reads nothing else: not the marker text, which a developer may +legitimately write and which an unstaged resolution still lacks, and not a list in the record, +which would carry a local, transient state in a committed file. Once resolved and staged, no +commit is needed. With the spec unchanged the rerun changes nothing, because new equals base. +With a changed spec it merges onto your resolution. + +### 6.1 The questions + +Conflicts are settled before anything is written. The plan is computed first (generated +tree, disk tree, `merge-tree`); when it has conflicts, a terminal is attached and `--force` +is absent, the action lists the conflicted files with their kind and asks +(`SdkGeneratePrompts.conflictChoice`, a `select`): + +- **I'll resolve them myself**: the run goes on as above, markers, exit 1, and the steps + (ending with "Or rerun and let the CLI resolve what is left, using either side."). The + `--force` and non-interactive default, which prints the list with the steps as before. +- **Resolve using generator changes**: the merge is run again with + `--strategy-option=theirs`, so every overlapping hunk goes the generator's way and your other + edits in the same file survive; a file one side deleted, or a binary one, is settled whole + by `MergePlan.taking('generated')` (the generator's version back, or the file removed). +- **Resolve using my changes**: the mirror image, with `--strategy-option=ours`: every + overlapping hunk keeps your lines and the generator's other changes in the same file still + apply; what is still in conflict after that is left as it is on disk, which is your side + (`MergePlan.taking('yours')`), so a file you deleted stays deleted and a customized file the + generator dropped stays. The next run sees your lines as your edits and keeps them. +- **Cancel this run**, or backing out of the question: nothing written, nothing recorded, no + previous ref saved; the result is cancelled. The previous ref is saved only after the + decision, so this answer leaves no trace. + +For either side the two merges become one `Settlement` (`favouring(side, …)`), which +remembers the files the first merge left in conflict so the run can name them, under +"Resolved using …", once they are written; it records a clean snapshot and succeeds. + +The next run, while git holds any of the SDK's files as unmerged, asks before generating +(`SdkGeneratePrompts.unresolvedChoice`); `--force` and a run without a terminal get the +refusal as before: + +- **I'll finish resolving them**, or backing out: nothing done, exit 1. Not a cancel: the + run cannot merge over unresolved files, so it stops while the developer works. +- **Resolve remaining conflicts using generator changes** and **… using my changes**: + `SdkRepositoryContext.settleUnresolved(side, paths)` reads the two sides git kept in the + index (`GitService.unmergedStages`, stage 2 yours, stage 3 the generator's) and settles each + file with the pure `settledContent`: markers still in it are settled in place for the side, + keeping every line around them (what you already resolved in that file included); without + markers, a file one side deleted stays or goes with the side asked for, a binary file takes + that side's version, and a text file whose markers are all gone was resolved by hand and + stays. Every file is then taken out of the unmerged state; no network needed. + +There is no undo and no cancel on this question (decided 2026-10-23): the first question +already offers the cancel, before anything is written. After either side, the directory is +inspected again, once; a file still unmerged after that is reported, never asked about +twice. The run then generates and merges from the settled state. + +Nothing is reported to telemetry (section 2). Whether a question can be asked is +`terminal.isInteractive()` (`process.stdin.isTTY`, `src/utils/terminal-utils.ts`), stubbed +in tests. + +CI: with `--force` nothing is asked; conflicts fail the job with the same list. A CI checkout +fetches the snapshot it needs by itself; its push back usually fails for lack of a write +token, which is a warning. Opening pull requests is not the CLI's job; an Action that commits +the marked files to a branch for review is a separate piece of work. + +## 7. Expected behaviour on the lab's cases + +The eleven Speakeasy runs from the lab notebook, plus three the lab did not cover. + +| # | Case | Speakeasy (measured) | This plan | +|---|---|---|---| +| 1 | Edit with nothing configured | lost unless the prompt is answered Yes | kept; there is nothing to configure | +| 2 | Edits and spec changes in different places | kept | kept | +| 3 | Same line changed by both | conflict markers, exit 1 | conflict markers, exit 1, pre-run state kept, stages in the index | +| 4 | Developer adds new files | kept | kept | +| 5 | Developer deletes a generated file | stays deleted | stays deleted while the generator leaves it alone; conflict once the generator changes it (section 10) | +| 6 | Developer moves a generated file | lost: rewritten from scratch | kept: matched by content, merged at the new path | +| 7 | Generator drops an edited file | lost: deleted silently | conflict: file kept and listed | +| 8 | `.genignore` frozen file | drifts from the spec | not offered; an ignore list is a possible follow-up | +| 9 | Patch files | replayed every run | not offered; edits in place cover the need | +| 10 | Code regions | kept inside markers, lost outside | not offered; every line is editable | +| 11 | SDK hooks registration file | written once | unchanged: hooks are generator features, merged like any file | +| 12 | Both add the same path with different content | developer's file overwritten | conflict add/add | +| 13 | Generator renames an edited file | lost | kept: edits follow the rename | +| 14 | Fresh clone, regenerate | fails unless the hidden refs were pushed | merges by itself: the generating run pushed the ref, the clone fetches it; a clear message with the commands and the overwrite question only when the push had failed | + +Every row of section 5 and this table is a test over real repositories in +`test/types/sdk-repository-context-merge.test.ts`, `test/actions/sdk/generate-destination.test.ts` +(row 14 with a bare remote) and `test/actions/sdk/generate-write.test.ts`, both driving +`GenerateAction` with a stubbed generation service (`test/helpers/sdk-generation.ts`). + +## 8. Phases, as delivered + +| Phase | Commit | What | +|---|---|---| +| 1 | `db3fe9f2`, reworked in `185e0663` | `GitService` (the system git through `execa`, every call with the 3.3 settings) and the pristine store (since the `SdkRepositoryContext`) with the value objects: refs, commit and tree ids, git version, generation record. | +| 2 | `185e0663` | The merge (since moved onto `SdkRepositoryContext`): the temporary commits, `merge-tree --write-tree -z`, the diffs; `src/application/sdk-merge/` (since folded into `GitService` and `src/types/sdk/`) keeps only what is pure: parsing merge-tree's output, the conflict kinds, marker relabelling, `MergePlan`. | +| 3 | `4d5b9ccf` | `SdkPrepareAction`, `SdkDestination`, `SdkWriteAction`, their prompts, the repository's `inspect`; `GenerateAction` back to validation and generation; `SdkContext.save` keeps `.git`. | +| 4, 5 | `87453917` | The missing-snapshot message with the push and fetch commands; `stageConflicts` through `update-index --index-info`; `SdkMergeConflictedEvent`. | +| 6 | this file | `CONTEXT.md` terms, the README section and requirement, the command description. | +| 7 | 8217ac56 | Sharing through the remote: `GitService.remoteOf`, `fetchRef`, `pushRef`, `isAncestor`; `SdkRepositoryContext.remote`, `shareSnapshot`, the fetch inside `inspect`, chains joined on divergence; the prompts; lab cases for a clone, a refused push, a remote attached after generating, and two diverged machines. | +| 8 | 7c181dfa | Sharing through a `pre-push` hook instead of a push in the CLI: the `apimatic-pre-push` script and the calling block (`ShareHook`), `GitService.hooksDirectory` and `hooksPathConfigured`, `SdkRepositoryContext.installShareHook`, the one-step note; `pushRef` and `shareSnapshot` removed; lab cases for `core.hooksPath` and an existing hook. | +| 9 | 07c03c13 | The conflict questions (6.1): `MergePlan.takingGenerated`, `ConflictChoice`/`UnresolvedChoice`/`ConflictOutcome`, `SdkRepositoryContext.restorePrevious` and `resolveForGenerator`, `GitService.checkoutTree`, `terminal.isInteractive`, the two `select` prompts and their follow-ups, the choice on the telemetry event; the README's custom-code section removed. | +| 10 | this file | Review follow-up: `SdkPrepareAction` and `SdkWriteAction` folded into `GenerateAction.execute`, their prompts into `SdkGeneratePrompts`; `SdkMergeConflictedEvent`, `ConflictOutcome`, `MergePlan.conflictsByKind` and the result payload removed; the two action tests now drive `GenerateAction` through `test/helpers/sdk-generation.ts`. | +| 11 | this file | Functional core, imperative shell: `SdkDestination` carrying its reason, `destinationFor` (`unresolved` included), `replacesDirectory`; `Regeneration` and `Settlement` (`resolving`, `favouringGenerator`); `SdkContext.destination`, `SdkRepositoryContext.regenerate` and `write`; `execute` the one place that speaks, with no other method on the action and no logic in the prompts. | +| 12 | this file | Review follow-up: the conflicts a run leaves are found from git's unmerged index entries (`GitService.unmergedPaths`, `ls-files -u` below the SDK directory), as git itself finds them, instead of a conflict list in the record plus a scan for marker text; `GenerationRecord` drops `conflicts` (older records still parse), `hasConflictMarkers` goes, `restorePrevious` takes the paths to settle, the second-run messages say "unresolved" and the first-run steps say to stage each file. | +| 13 | this file | Review follow-up: "Resolve using my changes" on both questions (`-X ours`, `MergePlan.taking(side)`, `favouring(side, …)`, `resolveMarkers(content, side)`), the first question's third answer renamed "Cancel this run", and the second question's undo removed with `restorePrevious`, `previousRestored` and `GitService.checkoutTree`; the second run settles files without markers from the index's stages (`GitService.unmergedStages`, `settledContent`), so a customized file the generator dropped is removed for the generator's side. | +| 14 | this file | Review follow-up: `.apimatic/.gitattributes` written beside the record (`RECORD_ATTRIBUTES`), so a pull where two clones both regenerated leaves the puller's record whole instead of writing markers into it; a test pulls between two real clones. | + +PR into `dev`, squash-merged, title `feat(sdk): keep your edits when regenerating an SDK`. + +## 9. Files + +| File | Role | +|---|---| +| `src/infrastructure/git-service.ts` | the system git behind every snapshot, the merge (its output read into stages and messages) and the index staging | +| `src/types/sdk-repository-context.ts` | the SDK directory with its repository: snapshots, the record, `inspect`, `regenerate` (the merge as a `Regeneration`), `write` (the previous ref, then the plan to disk), the staging | +| `src/types/sdk/{merge-plan,conflict-markers,unresolved-file}.ts` | pure: `MergePlan` built from git's merge and diffs, `taking(side)`, conflict kinds, the choices, markers and their resolution for either side, and how an unresolved file is settled from its stages | +| `src/types/sdk/{sdk-destination,settlement}.ts` | the functional core of the run: how it writes and why, what a settled merge is — data and pure functions, no I/O | +| `src/types/sdk/{snapshot-ref,generation-record,share-hook}.ts`, `src/types/git/{commit-id,tree-id,git-version,merge-tree}.ts`, `src/types/file/relativeFilePath.ts` | value objects, the hook script and block, git's output as types | +| `src/actions/sdk/generate.ts`, `src/prompts/sdk/generate.ts` | the run, its questions and its wording | +| `src/types/sdk-context.ts` | `directory()`, `destination(generation)` (what git and the repository say, as an `SdkDestination` or `unresolved`), `repository()`, `initRepository()`, save keeps `.git` | +| `src/utils/terminal-utils.ts` | whether a question can be asked at all | +| `src/commands/sdk/generate.ts` | flags, the git requirement in the description, intro/outro | +| `test/…` | mirrors each file above | + +## 10. Architecture + +Where each piece of the custom-code work lives, by layer, and what calls what. Arrows are +calls (or writes, where labelled); the dashed subgraph is outside the CLI. This is the code, +not the developer's flow (that is section 4.2). + +```mermaid +flowchart TB + subgraph CMD["Command — src/commands/sdk/generate.ts"] + C["SdkGenerate.run()
flags → GenerateAction.execute → outro(result)"] + end + subgraph ACT["Actions — src/actions/sdk/"] + G["GenerateAction.execute(project, sdkDir?, language, stability, force, apiVersion?, packageSettings?) → ActionResult
the only method; every prompt is called here
validate · SdkContext.destination; unresolved → unresolvedChoice, settleUnresolved(side), conflictsResolved, destination again once
gitUnavailable | recordNotForThisGeneration | snapshotMissing · overwriteSdk unless --force or !replacesDirectory
withDirPath: zip → generateSdk → save → unzip
overwrite → save, sdkGenerated · record → initRepository?, save, snapshotGenerated, recordGeneration, installShareHook, sdkGenerated
merge → regenerate → resolving | conflictChoice: regenerate favouring a side → favouring(side) | cancel (runCancelled)
write → recordGeneration, installShareHook → conflictsResolved?, sdkMerged; markers → stageConflicts, conflicts | conflictSteps"] + end + subgraph PR["Prompts — src/prompts/sdk/ (wording only)"] + GP["SdkGeneratePrompts (wording only, no branching)
gitUnavailable · overwriteSdk · recordNotForThisGeneration · snapshotMissing · nestedSdkDirectory
conflictsUnresolved · unresolvedChoice · resolveLater · conflictsResolved · conflictsNotSettled
sdkGenerated · repositoryInitialised · sdkMerged · conflictChoice · runCancelled
conflicts · conflictSteps · conflictsNotStaged · snapshotNotRecorded · customCodeFailed
shareHookNeedsOneStep · shareHookNotInstalled"] + end + subgraph TY["Types — src/types/"] + SC["SdkContext
directory · exists · destination(generation) → SdkDestination | Unresolved · repository() · initRepository · save · loadSdkInTempDirectory"] + RC["SdkRepositoryContext (sdk-repository-context.ts)
remote · inspect → SdkRepositoryState · regenerate → Regeneration{generated, merge} · write(base, plan)
snapshotGenerated · snapshotDisk · savePrevious · merge → MergeOutcome · apply · stageConflicts
recordPristine/recordGeneration · readRecord · installShareHook → ShareHookInstall · settleUnresolved(side, paths)"] + MP["MergePlan (sdk/merge-plan.ts)
from(inputs) · writes/deletes/conflicts/renames/files · taking(side)
ConflictKind · ConflictSide · ConflictChoice · UnresolvedChoice"] + CMK["conflict-markers.ts
relabelMarkers · resolveMarkers(content, side) · unresolved-file.ts: settledContent"] + GR["GenerationRecord (sdk/generation-record.ts)
generation.json: language, codegen, stability, pristine commit · describeApart"] + SR["SnapshotRef (sdk/snapshot-ref.ts)
refs/apimatic/pristine|previous/‹path› · refspec · sdkPath · collidesWith"] + SH["ShareHook (sdk/share-hook.ts)
SHARE_HOOK_SCRIPT · SHARE_HOOK_CALL · SHARE_HOOK_BLOCK · installInto · calls"] + SD["SdkDestination (sdk/sdk-destination.ts)
overwrite{found} | record{repository?, because} | merge{repository, base} · Unresolved{repository, paths}
destinationFor(repository, SdkRepositoryState) · replacesDirectory"] + ST["Settlement (sdk/settlement.ts)
{regeneration, plan, settled?: {side, paths}}
resolving(regeneration) · favouring(side, original, favoured)"] + GT["git/ · CommitId · TreeId · GitVersion · merge-tree (Change, StageEntry, MergeTreeOutput)"] + RF["file/ · RelativeFilePath · FilePath · DirectoryPath"] + end + subgraph INF["Infrastructure — src/infrastructure/"] + GS["GitService (git-service.ts)
version · init · treeOf · commitTree · updateRef · resolveRef · hasCommit · describe
mergeTree · diffTrees · listTree · readBlobs · stageConflicts (update-index) · unmergedStages (ls-files -u) · unstage
remoteOf · fetchRef (no prompts, 30 s) · hooksDirectory · hooksPathConfigured"] + FS["FileService"] + TU["utils/terminal-utils.ts · terminal.isInteractive"] + end + subgraph OUT["Outside the CLI (dashed)"] + HK[".git/hooks/apimatic-pre-push (sh) called from .git/hooks/pre-push or .husky/pre-push
push refs/apimatic/pristine and the refs below it · on rejection fetch into refs/apimatic/remotes/‹remote›/pristine…, join, push again"] + RM[(remote
refs/apimatic/pristine/‹path›)] + end + C --> G + G --> GP + G --> SC + SC --> RC + G --> RC + G --> SD + G --> ST + SC --> SD + ST --> MP + G --> TU + RC --> GS + RC --> FS + RC --> MP + RC --> CMK + RC --> GR + RC --> SR + RC --> SH + MP --> GT + GS --> GT + RC --> RF + RC -- writes script + block --> HK + HK -- git push --> RM + GS -- fetchRef --> RM + style OUT stroke-dasharray: 5 5 +``` + +What each layer owns: + +| Layer | Holds | Never does | +|---|---|---| +| Command | flags, intro/outro | git, files, questions, telemetry (none for now) | +| Actions | the imperative shell: one `execute` with the order of the steps and every question and message between them | wording, git commands, file I/O, data on the `ActionResult`, deciding what a state means, speaking from anywhere but `execute` | +| Prompts | words and `select`/`confirm` for data they are handed | decide anything, know git refs | +| Types | the functional core: what a state means (`destinationFor`), how a settled merge is written (`Settlement`); the SDK directory with its repository (`SdkRepositoryContext`), the plan, the record, refs, the hook text | print, call the API, ask | +| Infrastructure | `git` and the file system | know what a snapshot or a conflict means | +| Outside | the hook script in the developer's `.git/hooks`, the remote's refs | need the CLI to run | + +## 11. Needs a decision + +- **Deleted by you, changed by the generator.** Conflict (this plan) or remembered deletion + as Speakeasy does. The conflict recurs on docs files that change with every version bump; + the memory is silent. +- **`git init` without asking.** This plan tells the developer afterwards. The alternative is + a question on the first run, which CI would need `--force` to pass. +- **`SdkContext.directory()`.** The context skill marks a public directory accessor as the + pattern to avoid; this one falls under its exception (a layout context answering with a + derived directory for a prompt that names it and a service that works on it). The + alternative is a method handing back the store's findings, so no caller holds the path. +- **The record merges as binary.** Decided 2026-10-23 after testing with two clones: both + regenerated, so both changed `generatedAt`, and the second pull hit a text conflict in the + record. `.apimatic/.gitattributes` sets `merge=binary`: git writes no markers, leaves the + puller's record in place as valid JSON and lists it as both modified, so `git add` accepts + it; either record names a snapshot a run can merge from. Under `git pull --rebase` the + record left in place is the upstream one, equally valid. Weighed and set aside: `merge=union` + (no conflict, but invalid JSON), a custom "keep ours" driver (silent, but it needs a + `git config` line in every clone, and a clone without it falls back to markers), and + dropping the fields that change on every run (kept by decision). `linguist-generated` + collapses the record in GitHub's pull request diffs. +- **Unresolved conflicts are git's unmerged entries**, decided 2026-10-23 on review: the + CLI asks the index (`ls-files -u` below the SDK directory) and nothing else, so a file + counts as resolved exactly when `git add` has marked it so, as after any merge. A + developer who discards the index (`git reset`, `git stash`) with markers still in the + files has told git the files are resolved; the next merge then sees the marker lines as + their edit and flags them again, so nothing is lost silently. +- **Packages must exclude `.apimatic/`.** No change needed, checked 2026-10-20: all three v4 + manifests are whitelists (`package.json` `files`, the `.csproj`'s explicit `Pack="true"` + items, `[tool.poetry]` `packages`), and `npm pack`, `dotnet pack` and `python -m build` on + projects with a planted `.apimatic/generation.json` packed none of it. +- **Portal downloads.** `portal generate` ships the server's SDK zips, so a portal's download + never contains custom code. Zipping the merged SDK directory from disk is a follow-up. +- **Sharing by the CLI.** Decided 2026-10-21, reversing the earlier "no remote": the manual + push and fetch was the step developers would forget and the one a clone could not do. The + alternative weighed was a tracked `.apimatic/pristine/` copy (travels with every branch, but + doubles the working tree and every diff); the remote push keeps the tree clean and makes the + failure modes (no rights, no credential, offline) warnings with a manual fallback. +- **No `--offline` flag.** Decided 2026-10-21: the cases it guarded (a read-only remote, a + policy against tool pushes, a dead network) are per-repository, not per-run, and every one + of them is already a warning that does not fail the run. If an opt-out is ever needed, a git + config read by `remoteOf` fits better than a flag. +- **The hook, not the CLI, pushes.** Decided 2026-10-22, replacing the push inside the CLI + from the day before: a snapshot pushed by the CLI left the remote without it whenever the + push failed or the remote was attached later, and nothing but another `sdk generate` would + send it. Tying it to the developer's own push makes the snapshot and the record that names + it arrive together. Weighed and dropped: a fetch refspec in the remote's config (the hook and + the CLI fetch for themselves), writing into a `core.hooksPath` directory (husky wipes it), + a fetch hook (git has none; `post-merge` fires only on pulls that merge, and a clone has no + hooks yet). +- **Conflicts ask.** Decided 2026-10-22, reworded 2026-10-23 on review: the first run offers + resolve by hand (the `--force` and CI default, so a job never discards edits silently), + resolve using generator changes or using my changes (hunk by hunk, git's `-X theirs` and + `-X ours`; never the whole file or SDK, since dropping what merged cleanly is more than + asked), or cancel the run. The run after, while files are still unresolved, offers to + finish resolving or to resolve what remains for either side; undo and cancel are not on it. + No `--on-conflict` flag: `--force` keeps today's path and nothing else was asked for. +- **No `--zip` flag.** Decided 2026-10-21: `sdk generate --zip` wrote the archive the server + sent and nothing else, which custom code could never apply to, and nobody needs the CLI to + zip a directory. Removed outright rather than hidden and deprecated. The server still delivers + a zip, which the CLI unpacks into a temp directory; `portal generate --zip` is untouched. + +## 12. Relation to other work + +The codegen-v2 server-side plan (custom code as a git merge the server performs, with the +`.git` zipped up and down each run) is superseded by this one; the server's `/api/generate` +needs none of it, and the beta CLI never uploads a source tree. The v3 CLI flow +(`--track-changes`, `save-changes`, `merge-source-tree`) was removed from `dev` in #427 with +its `git-service.ts` and the isomorphic-git dependency; the file name returns here for the +system-git wrapper, and no library is added. Speakeasy's `persistentEdits` has the same shape +(snapshots as hidden refs in the developer's repository, per-file three-way merge on the +working tree, markers plus a failed exit on conflict), with the four gaps the lab found +closed: moves, generator renames, add/add, and dropped edited files. diff --git a/CONTEXT.md b/CONTEXT.md index 09ad52aa5..7ec1b2053 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -55,3 +55,21 @@ _Avoid_: Example name, variant **Unplaced sample**: A code sample whose endpoint no spec in the source directory declares. _Avoid_: Orphan sample, missing operation + +### SDK regeneration + +**SDK directory**: +The directory `sdk generate` writes an SDK into, and reads back on every later run: the `--destination` itself when one is named, else `/sdk/`, or `/sdk//`. +_Avoid_: Destination, output directory, language directory + +**Pristine snapshot**: +One generation's output exactly as the server returned it, stored as a commit under the hidden ref `refs/apimatic/pristine/` of the repository enclosing the SDK directory, never on a branch; the base of the next merge. +_Avoid_: Generated snapshot, baseline, original + +**Generation record**: +`/.apimatic/generation.json`, committed with the SDK, naming the pristine snapshot the directory was last written from. Written with `.apimatic/.gitattributes` beside it, so git merges it as binary and GitHub collapses it in pull requests. +_Avoid_: Lock file, manifest, metadata file + +**Custom code**: +Whatever differs between the SDK directory on disk and its pristine snapshot: the developer's edits, additions, deletions and moves, kept by merging the next generation over them. +_Avoid_: Customizations (in code), user changes, overrides diff --git a/README.md b/README.md index f04fca6e0..1dec8bb4f 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,10 @@ The official CLI for APIMatic. Node.js 24 or newer, for every command. npm only warns when your Node is older, so the install succeeds and the CLI then refuses to run, naming the version it found. +Git 2.38 or newer on your `PATH`, for `sdk generate` to keep your changes to a generated SDK +across regenerations. +Without it the command still generates, and warns that your customizations will not persist. + # Getting Started To get started with APIMatic's CLI using a step by step wizard, run the following command: @@ -28,7 +32,7 @@ $ npm install -g @apimatic/cli $ apimatic COMMAND running command... $ apimatic (--version) -@apimatic/cli/1.3.1 win32-x64 node-v24.19.0 +@apimatic/cli/1.5.0 win32-x64 node-v24.21.0 $ apimatic --help [COMMAND] USAGE $ apimatic COMMAND @@ -86,7 +90,7 @@ EXAMPLES apimatic api transform --format=raml --url="https://petstore.swagger.io/v2/swagger.json" --destination=./ ``` -_See code: [src/commands/api/transform.ts](https://github.com/apimatic/apimatic-cli/blob/v1.3.1/src/commands/api/transform.ts)_ +_See code: [src/commands/api/transform.ts](https://github.com/apimatic/apimatic-cli/blob/v1.5.0/src/commands/api/transform.ts)_ ## `apimatic api validate` @@ -119,7 +123,7 @@ EXAMPLES apimatic api validate --url="https://petstore.swagger.io/v2/swagger.json" ``` -_See code: [src/commands/api/validate.ts](https://github.com/apimatic/apimatic-cli/blob/v1.3.1/src/commands/api/validate.ts)_ +_See code: [src/commands/api/validate.ts](https://github.com/apimatic/apimatic-cli/blob/v1.5.0/src/commands/api/validate.ts)_ ## `apimatic auth login` @@ -143,7 +147,7 @@ EXAMPLES apimatic auth login --auth-key={api-key} ``` -_See code: [src/commands/auth/login.ts](https://github.com/apimatic/apimatic-cli/blob/v1.3.1/src/commands/auth/login.ts)_ +_See code: [src/commands/auth/login.ts](https://github.com/apimatic/apimatic-cli/blob/v1.5.0/src/commands/auth/login.ts)_ ## `apimatic auth logout` @@ -162,7 +166,7 @@ EXAMPLES apimatic auth logout ``` -_See code: [src/commands/auth/logout.ts](https://github.com/apimatic/apimatic-cli/blob/v1.3.1/src/commands/auth/logout.ts)_ +_See code: [src/commands/auth/logout.ts](https://github.com/apimatic/apimatic-cli/blob/v1.5.0/src/commands/auth/logout.ts)_ ## `apimatic auth status` @@ -179,7 +183,7 @@ EXAMPLES apimatic auth status ``` -_See code: [src/commands/auth/status.ts](https://github.com/apimatic/apimatic-cli/blob/v1.3.1/src/commands/auth/status.ts)_ +_See code: [src/commands/auth/status.ts](https://github.com/apimatic/apimatic-cli/blob/v1.5.0/src/commands/auth/status.ts)_ ## `apimatic autocomplete [SHELL]` @@ -251,8 +255,8 @@ DESCRIPTION Generate a context plugin for your SDKs. Generate a context plugin that teaches an AI coding assistant how to use your SDKs. Requires an input directory - containing a `src` directory with your API specification — `apimatic.json` is created if it is not there. It asks - for the plugin's name on the first run, and for its languages only when `apimatic.json` records none (`sdk publish` + containing a `src` directory with your API specification — `apimatic.json` is created if it is not there. It asks for + the plugin's name on the first run, and for its languages only when `apimatic.json` records none (`sdk publish` records them). Later runs read both from `apimatic.json` without asking. EXAMPLES @@ -261,7 +265,7 @@ EXAMPLES apimatic plugin generate --input="./" --destination="./plugin" ``` -_See code: [src/commands/plugin/generate.ts](https://github.com/apimatic/apimatic-cli/blob/v1.3.1/src/commands/plugin/generate.ts)_ +_See code: [src/commands/plugin/generate.ts](https://github.com/apimatic/apimatic-cli/blob/v1.5.0/src/commands/plugin/generate.ts)_ ## `apimatic plugin publish` @@ -288,7 +292,7 @@ EXAMPLES apimatic plugin publish --input="./" --destination="./plugin" ``` -_See code: [src/commands/plugin/publish.ts](https://github.com/apimatic/apimatic-cli/blob/v1.3.1/src/commands/plugin/publish.ts)_ +_See code: [src/commands/plugin/publish.ts](https://github.com/apimatic/apimatic-cli/blob/v1.5.0/src/commands/plugin/publish.ts)_ ## `apimatic portal generate` @@ -328,7 +332,7 @@ EXAMPLES apimatic portal generate --zip ``` -_See code: [src/commands/portal/generate.ts](https://github.com/apimatic/apimatic-cli/blob/v1.3.1/src/commands/portal/generate.ts)_ +_See code: [src/commands/portal/generate.ts](https://github.com/apimatic/apimatic-cli/blob/v1.5.0/src/commands/portal/generate.ts)_ ## `apimatic portal serve` @@ -364,7 +368,7 @@ EXAMPLES apimatic portal serve --input=./ --port=23513 --open ``` -_See code: [src/commands/portal/serve.ts](https://github.com/apimatic/apimatic-cli/blob/v1.3.1/src/commands/portal/serve.ts)_ +_See code: [src/commands/portal/serve.ts](https://github.com/apimatic/apimatic-cli/blob/v1.5.0/src/commands/portal/serve.ts)_ ## `apimatic publishing profile list` @@ -383,7 +387,7 @@ EXAMPLES apimatic publishing profile list ``` -_See code: [src/commands/publishing/profile/list.ts](https://github.com/apimatic/apimatic-cli/blob/v1.3.1/src/commands/publishing/profile/list.ts)_ +_See code: [src/commands/publishing/profile/list.ts](https://github.com/apimatic/apimatic-cli/blob/v1.5.0/src/commands/publishing/profile/list.ts)_ ## `apimatic quickstart` @@ -403,7 +407,7 @@ EXAMPLES apimatic quickstart ``` -_See code: [src/commands/quickstart.ts](https://github.com/apimatic/apimatic-cli/blob/v1.3.1/src/commands/quickstart.ts)_ +_See code: [src/commands/quickstart.ts](https://github.com/apimatic/apimatic-cli/blob/v1.5.0/src/commands/quickstart.ts)_ ## `apimatic sdk generate` @@ -412,11 +416,11 @@ Generate an SDK for your API ``` USAGE $ apimatic sdk generate -l csharp|java|php|python|ruby|typescript|go [-d ] [--api-version ] - [--zip] [--codegen-version v4] [--stability stable|beta] [-i ] [-f] [-k ] + [--codegen-version v4] [--stability stable|beta] [-i ] [-f] [-k ] FLAGS - -d, --destination= [default: /sdk/ | /sdk//] path where - the SDK will be generated + -d, --destination= [default: /sdk/ | /sdk//] directory the + SDK is generated into -f, --force overwrite changes without asking for user consent. -i, --input= [default: ./] path to the parent directory containing the 'src' directory, which includes API specifications and configuration files. @@ -428,23 +432,23 @@ FLAGS --stability=