Claude Cursor Skill

skmtc-cli

Use the Skmtc CLI to scaffold projects, install or clone generators from JSR, configure schema sources and enrichments, and produce code artifacts from an OpenAPI v3 or GraphQL SDL schema. Teaches the workspace mental model (`<root>/.skmtc/<project>/`, client.json, bundle, manife

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download skmtc-skmtc-deno_docs_skills_skmtc-cli-e3abffc.zip · 19 KB
skmtc/skmtc 19 0 forks Apache-2.0 Updated 7d ago
Part of skmtc/skmtc — 12 skills

Install

skills CLI npx skills add https://github.com/skmtc/skmtc/tree/main/deno/docs/skills/skmtc-cli
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install skmtc-skmtc@llmmart
Git git clone https://github.com/skmtc/skmtc.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole skmtc/skmtc collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Skmtc CLI

The Skmtc CLI generates code from OpenAPI v3 or GraphQL SDL documents. It's a Deno binary that wraps a project workspace under <root>/.skmtc/, fetches generators from JSR, and runs them against a schema source pinned in each project.

This skill carries what the binary cannot tell you: the workspace mental model, the agent contract, and the decisions that need intent. Everything else — the command list, per-command flags, argument shapes — lives in the binary itself and is always current there; §3 shows how to pull it on demand. This skill guides using the CLI; for authoring generator packages see skmtc-generator, for diagnosing failures see debug-failing-generation.

1. Mental model

Concept Where it lives Notes
Skmtc root nearest ancestor dir containing .skmtc/ Created by skmtc init
Project <root>/.skmtc/<project>/ One schema + one set of generators
Project deps <root>/.skmtc/<project>/deno.json JSR imports of installed generators
Schema pin <root>/.skmtc/<project>/.settings/client.json source field — URL or path. Resolution: explicit schema arg → client.json#source → interactive prompt (TTY only; strict mode fails with a recipe error)
basePath client.json#settings.basePath Must match the consumer app's @ alias root. Both the on-disk root for generated files AND the alias root in the bundler's resolver. Generators produce @/<subdir>/... paths assuming this alignment. Absolute paths are rejected at init.
Bundle <root>/.skmtc/<project>/bundle.js Compiled worker entry. Regenerated by bundle/dev/clone/install.
Manifest <root>/.skmtc/<project>/.settings/manifest.json Per-run record of every file written and every (generator × item) outcome
Generator JSR package or local folder Local: <root>/.skmtc/<project>/<gen-name>/
Global state ~/.skmtc/ auth.json (the hub PAT stored by skmtc login), shadow project state, schema caches. Check this when local state alone doesn't explain a failure.

A "project" is not the consuming app — it's the generator configuration the consuming app pulls code from.

Generators are opinionated templates, not configurable libraries. Stock @skmtc/gen-* packages ship hardcoded defaults — export paths, identifier naming, peer imports, output shapes — and there are no config flags for any of them, deliberately. To change them, skmtc clone the generator into the project and edit its source; that is the customization seam, not a workaround. "Stock generator hardcodes X" is almost never a CLI bug. Enrichments supply the settings a generator's author declared it needs — its enrichments.ts schema is the whole contract a consumer can fill; cloning changes the shape.

Two engine facts that shape CLI expectations: generator order never affects output (coordination is a memoized cache, not a dependency graph — never sequence generators), and render does not run a formatter (unformatted output is by design; consumers format separately).

2. The agent contract

Every state-touching command supports three modes, picked automatically:

Mode When Behavior
Interactive TTY attached and no --json / --no-input Ink TUI; prompts for missing args
Strict text Non-TTY (CI / pipes / agents) OR --no-input Plain-text result on stdout; missing required args fail with a recipe error on stderr
Strict JSON --json (implies --no-input) Single JSON object on stdout; logs on stderr

For agents: add --json to every command. The CLI auto-degrades to non-interactive mode on any non-TTY stdin/stdout — no PTY wrappers needed. (Two exceptions surface in help: dev is long-running and has no --json; create has no --json yet.)

Exit codes are consistent across all commands: 0 success (including documented no-ops), 2 required input missing or invalid (recipe error on stderr), 1 anything else (registry unreachable, schema parse failure, fatal parseIssue, typecheck failure). For generate --json, an empty errors array is the success condition — not the exit code alone.

Recipe errors are the discovery mechanism. When a required argument is missing in strict mode, stderr carries the usage line, a worked example, and a Discover: line naming the command that lists the valid values (e.g. ls .skmtc/ for project names). Trust it: run the command, read the recipe, run the discovery, retry.

3. The command surface lives in the binary

Do not look for a command table in this skill — pull it live, where it is always current with the installed version:

skmtc --help        # every command, with real descriptions
skmtc <cmd> -h      # full flags for one command

The newer commands' help descriptions (status, eject, adopt, publish, push, pull) carry their full semantics — read them there rather than guessing from the names. One naming trap help cannot intercept: there is no skmtc deploy — stacks are published (skmtc publish) as immutable semver versions; deployments and the production alias belong to hub projects and are driven from the web app, not the CLI.

4. First steps in a workspace

skmtc agent-context --json    # enumerate projects, commands, state
skmtc doctor --json           # check for known frictions

These two give the full workspace picture without documentation lookups — agent-context is the snapshot, doctor the diagnostic, in that order. doctor's summary is the worst status across checks (error > warning > ok; exit 1 only on error), and every check carries its own id, status, message, and remediation hint — the output is self-describing. The check-id catalogue, if you need to reason about a specific check: reference.md §"Doctor check ids".

5. The bundle-freshness gotcha

Generation runs the compiled bundle.js, not generator source — a stale bundle silently shadows source edits:

1. `skmtc clone` triggers an automatic rebundle
2. If `worker.ts` and `deno.json#imports` disagree, strict-mode
   generate refuses with a freshness error
3. Remediation: `skmtc bundle <project>`, then re-run `generate`
4. `skmtc doctor --json` surfaces the mismatch as `project-bundle/<project>`

6. Configuration: client.json and filters

.skmtc/<project>/.settings/client.json — top level is { source?, settings }; settings carries basePath (required, relative, no ..), packages, enrichments, skip, include, generatedSuffix. Full annotated shape, every key: reference.md §6 — read it before editing the file.

packages (optional) routes output into monorepo packages. Each entry { rootPath, moduleName? } is a folder forward from basePath, which is then the common ancestor of every package, not a bundler alias. Inside a root, imports render @/ from that root; from outside, they render the root's moduleName. Nested roots are subpath exports (@app/sdk/models) that share the outer package's @. Config load rejects .., a repeated root and the workspace root; render fails on an outside import of a root with no moduleName. Task page: docs/using/how-to/generate-into-multiple-packages.md.

Enrichment misaddressing never errors. When a customization does not land, read manifest.enrichmentWarnings (printed after generate, and re-read by skmtc doctor as project-enrichments/<project>): a typo'd id, path, method, model name or leaf key is reported with the nearest match. Routing is the literal path plus lowercase method, never operationId, under a main variant key. A wrong-typed value fails only that item, recorded as error in the manifest.

settings.skip / settings.include accept a whole generator, a per-operation entry (path → method → variant[]), or a per-model entry (refName → variant[]); [] means every variant. Filters are where user intent is expressed — never a generator's isSupported. Semantics and precedence: reference.md §7.

Every command's --json envelope is one object discriminated by a type field; per-command shapes: reference.md §8 — read when parsing output, not before.

7. Task cards

End-to-end workflows (setup, adding generators, enrichments, CI, publishing, customizing a stock generator, …) live in task-cards.md — open the one card for the job in front of you. A single command doesn't need a card; -h covers it.

8. Boundary with other skills

This skill ends at the CLI surface. Hand off when:

  • The next step edits a .ts/.tsx file under <root>/.skmtc/<project>/<gen-name>/ → skmtc-generator
  • The user reports something broken and the cause isn't yet known → verify before proposing: manifest, parse issues, then a reproduction (debug-failing-generation, error codes)

Companion files

Loaded on demand with the Read tool, never eagerly:

File What it holds Read it when
reference.md §6 client.json shape, §7 filter semantics, §8 JSON envelopes, §11 operational principles, doctor check ids Editing settings, writing a filter, parsing --json, reasoning about a doctor check
task-cards.md Twelve end-to-end workflow cards Doing a multi-step job for the first time
Files (skmtc)
  • reference.md 19.5 KB
    # skmtc-cli — reference companion
    
    Pull-loaded detail for the `skmtc-cli` skill: the settings file's
    full shape, the filter semantics, the JSON envelopes agents parse,
    the user-facing operational principles, and the doctor check ids.
    Sections keep their historical numbers (§6/§7/§8/§11), which SKILL.md
    cites directly; they do not line up with SKILL.md's own numbering.
    
    Read this when you are editing `client.json`, writing a filter,
    parsing `--json` output, or checking a principle — not before.
    
    ## 6. The client.json shape
    
    ```jsonc
    {
      // Optional. URL or path to the OpenAPI / GraphQL schema.
      // When set, `skmtc generate <project>` doesn't need a schema arg.
      "source": "./openapi.json",
    
      "settings": {
        // The on-disk anchor for generated output. Required, relative,
        // no `..` segments. Single-package: the consumer app's bundler
        // `@` alias root — generators produce `@/<subdir>/...` paths
        // assuming this aligns with the bundler's alias config.
        // Multi-package (see `packages` below): a common ancestor of
        // every package — the monorepo root — not a bundler alias.
        "basePath": "mobile-app/src",
    
        // Per-generator and per-item user overrides. Routing
        // keys depend on factory:
        //   - OAS operation:  [path][method][variant]
        //   - GraphQL op:     [rootKind][fieldName][variant]
        //   - Model:          [refName][variant]
        //
        // The trailing `[variant]` level is `'main'` by default. Most
        // consumers write just one variant; a variants-aware generator
        // like gen-shadcn-form (operation) or a coercive zod variant
        // (model) can produce N artifacts per item by declaring extra
        // variant keys. `'main'` MUST be present whenever any variant
        // is declared — the engine throws at start otherwise.
        "enrichments": {
          "@skmtc/gen-shadcn-form": {
            "/contacts": {
              "post": {
                "main": {
                  "title": "Create Contact",
                  "submitLabel": "Save"
                }
              }
            },
            // Multi-variant operation example: one PATCH endpoint,
            // several section-edit forms with different field subsets.
            "/quotes/{id}": {
              "patch": {
                "main":     { "title": "Edit Quote" },
                "customer": { "title": "Customer details" },
                "location": { "title": "Location" }
              }
            }
          },
          // Multi-variant model example: same component schema produces
          // a strict and a coercive zod schema in adjacent files.
          "@scope/gen-zod-variants": {
            "Customer": {
              "main":     { "coerce": false },
              "coercive": { "coerce": true }
            }
          }
        },
    
        // Allow-list. Empty array = no filter. See §7.
        "include": [],
    
        // Deny-list. Applied after include. See §7.
        "skip": [],
    
        // Optional. Filename suffix the engine injects into every
        // projection export path before the extension (`CreateForm.tsx`
        // → `CreateForm.generated.tsx`). Defaults to ".generated"; set
        // "" to disable. Injection is idempotent, and the suffix marks a
        // file as engine-owned — it is the seam the eject/adopt flow
        // renames across. Usually leave it alone.
        "generatedSuffix": ".generated",
    
        // Optional. Multi-package output — route generated files into
        // separate packages of a monorepo. Each entry is
        // `{ rootPath, moduleName? }` with a FORWARD `rootPath` (relative
        // to basePath). A root is a folder, not a prefix. Rejected at
        // config load: a `..` segment, one root listed twice (in any
        // spelling), and the workspace root itself (`.`) — leave
        // `packages` unset to import everything through `@/` from
        // basePath. When `packages` is set, basePath is the monorepo root
        // and `@` is per-package: intra-package imports render `@/…`
        // (rooted at that package), cross-package imports render the
        // target's `moduleName`. Roots may nest: a nested root is a
        // subpath export of the package around it (`@app/sdk/models`),
        // sharing the outer package's `@`. See
        // `reference/settings/client-json-schema.md`.
        "packages": [
          { "rootPath": "packages/sdk/src", "moduleName": "@app/sdk" },
          { "rootPath": "packages/sdk/src/models", "moduleName": "@app/sdk/models" }
        ]
      }
    }
    ```
    
    To know what enrichment keys a generator accepts, **read its
    `gen-x/src/enrichments.ts`** — Valibot schema is canonical.
    
    ### Three enrichment scopes
    
    The `enrichments` namespace carries **per-subject** enrichments (the
    original, unchanged form) plus **two reserved `_`-prefixed scopes**.
    Three scopes in all, distinguished by key-depth:
    
    | Scope | Where the key sits | Reserved key | Lifetime |
    |---|---|---|---|
    | **subject** | `[id][subject][variant]` | — (customer subject names) | per item (model / operation) |
    | **generator** | `[id]._generator` | `_generator` | run-constant for that one generator |
    | **stack** | `._stack` | `_stack` | run-constant shared across every generator |
    
    `_stack` is a **top-level** key — a sibling of the generator-id keys.
    `_generator` lives **inside a generator's slot** — a sibling of the
    subject keys.
    
    **Reserved-key rule:** customer keys (generator ids at the top level,
    subject names inside a slot) **must not start with `_`**. The only
    reserved keys are `_stack` and `_generator`; any other `_`-prefixed key
    fails config validation at start.
    
    ```jsonc
    "enrichments": {
      // Stack scope — one leaf shared across every generator. Valid only
      // when every generator in the run declares `stack` in its umbrella.
      "_stack": { "apiTitle": "Billing API" },
    
      "@acme/gen-zod-strict": {
        // Generator scope — a run-constant for this generator only.
        "_generator": { "strict": true },
    
        // Subject scope — per-model, unchanged. `'main'` is the
        // default variant.
        "Pagination": { "main": { "coerce": true } }
      }
    }
    ```
    
    In `enrichments.ts` the same three scopes are the umbrella members
    `subject`, `generator` and `stack` — no underscore. A generator opts
    into a scope by declaring it; `v.undefined()` rejects any value at that
    key. Which scopes a generator declares is in its `enrichments.ts` —
    the stock generators declare `subject` only — so the `_generator` /
    `_stack` blocks above need cloned generators that declare them, and
    `_stack` needs every generator in the run to.
    
    Per-subject enrichments are otherwise unchanged: the routing keys and
    the mandatory `'main'` variant level (see the `client.json` shape above
    and §7) work exactly as before. A generator reads each scope by known
    key through typed helpers (`toStackEnrichment` /
    `toGeneratorEnrichment` / the per-subject path) — it never iterates the
    enrichments record itself.
    
    `packages` is optional; omit it for the common single-`basePath`
    project. With `packages` set, point `basePath` at a common ancestor
    of every package (the monorepo root) so each `rootPath` — and every
    generator's `toExportPath` — is a plain forward path. A `..` segment
    in `basePath` or any `rootPath` is rejected at config load: it means
    `basePath` is too deep, and hand-counting `../` segments is the
    silent-misplacement footgun the forward-path rule removes. Two more
    rejections at config load: a root listed twice (spellings such as
    `./packages/sdk/`, `@/packages/sdk` and `packages/sdk` are one root)
    and the workspace root itself as a package root — with no `packages`
    every import already goes through `@/` from `basePath`. Roots may
    nest; a nested root is a subpath export of the package around it,
    named by its own `moduleName` for importers outside the package and
    sharing the outer package's `@` for files inside it.
    
    ## 7. Skip and include filters
    
    Both `skip` and `include` accept three entry shapes:
    
    ```jsonc
    [
      // 1. Whole generator (string)
      "@skmtc/gen-zod",
    
      // 2. Per-operation (path → method → variant[])
      //    `[]` means "every variant of this method".
      //    `["customer", "main"]` means "only those variants".
      { "@skmtc/gen-shadcn-form": {
          "/customers": { "post": [] },
          "/quotes/{id}": { "patch": ["customer", "location"] }
      } },
    
      // 3. Per-model (refName → variant[])
      //    `[]` means "every variant of this refName".
      //    `["coercive", "main"]` means "only those variants".
      { "@scope/gen-zod-variants": {
          "Customer": [],
          "Order": ["coercive"]
      } }
    ]
    ```
    
    Order of evaluation in `GenerateContext.toArtifacts`:
    **`isSupported` (capability) → `include` (allow) → `skip` (deny).**
    
    - `include` is **per-generator**, not document-global. A generator
      with a per-operation `include` entry runs in allow-list mode (only
      the listed items); a generator absent from `include` is unaffected
      and runs default-on. There is no whole-generator exclusion.
    - `include === []` or undefined → no filter active; everything runs
    - A per-operation `include` entry → only matched items run for that
      generator; non-matching items emit `skipped`
    - A **bare-string** `include` entry (`"@skmtc/gen-zod"`) carries no
      per-operation filter and is a no-op — the generator runs default-on
      either way. Whole-generator opt-out is `skip`.
    - `include` + `skip` on the same item → **`skip` wins** (`final =
      include_set \ skip_set`) — `skip` is the deny-list, always decisive
    - Matching is exact — no wildcards, on path, method, OR variant name
    - The variant array is the third axis: `[]` matches every variant
      of the named method; a populated array matches only those variant
      names
    
    > **Migration note.** Earlier `@skmtc/core` treated a non-empty
    > `include` as document-global: every generator not mentioned was
    > silently excluded. `include` is now per-generator. If a project
    > relied on the global behaviour — listing a few generators in
    > `include` to switch the rest off — turn those off explicitly with
    > whole-generator `skip` entries instead.
    
    Use `include` for opt-in generators (forms, tables, page shells) where
    a blanket run would produce dozens of files the team doesn't want.
    Use a variant array under a method to narrow the allow/deny to
    specific variants of a multi-variant operation.
    
    ## 8. Common JSON output shapes
    
    Agents drive on these shapes. Discriminator field is usually `type`.
    
    ### `install`
    
    ```jsonc
    {
      "projectName": "my-api",
      "installed": ["@skmtc/gen-zod"],
      "bundle": { "type": "bundled", "projectName": "my-api", "bundlePath": "..." },
      "verifyWith": "cat .skmtc/my-api/deno.json"
    }
    ```
    
    The post-install rebundle runs for every project — remote-only and
    hybrid alike — so `bundle.type` is always `"bundled"`.
    
    ### `clone`
    
    ```jsonc
    {
      "projectName": "my-api",
      "cloned": [
        { "moduleName": "@skmtc/gen-typescript", "version": "0.0.55" }
      ],
      "bundle": { "type": "bundled", "projectName": "my-api", "bundlePath": "..." },
      "verifyWith": "ls .skmtc/my-api/"
    }
    ```
    
    `clone` includes a **pre-flight `@skmtc/core` peer-pin check** — if
    the project's pin doesn't match the CLI's major.minor, the command
    refuses with exit 2 before any state mutation. `--force` overrides
    (at the user's risk).
    
    ### `bundle`
    
    ```jsonc
    // Wrote bundle.js — the only outcome; every project (remote-only
    // included) builds a local bundle, since `generate` loads it:
    { "type": "bundled", "projectName": "my-api", "bundlePath": "..." }
    ```
    
    ### `generate`
    
    ```jsonc
    {
      "type": "generated",
      "projectName": "my-api",
      "basePath": "mobile-app/src",
      "manifestPath": ".skmtc/my-api/.settings/manifest.json",
      "stats": { "tokens": 201029, "lines": 1234, "files": 753, "totalTimeMs": 180 },
      "files": ["mobile-app/src/types/User.generated.ts", "..."],
      // errors: array of paths through manifest.results ending at 'error' leaves.
      // Shape: [traceId, spanId, "generate", generatorId, identifier]
      "errors": [
        ["trace-1778185255674", "span-1778185255674", "generate",
         "@skmtc/gen-zod", "BrokenModel"]
      ],
      "parseIssues": [
        { "protocol": "oas", "level": "warning", "type": "MISSING_OBJECT_TYPE",
          "location": "components:schemas:User", "message": "..." }
      ]
    }
    ```
    
    With `--typecheck`, gains a `typecheck` field (`{ type: "passed" | "failed" | "no-tsconfig" | "tsc-error" | "skipped", ... }`).
    A `failed` typecheck → exit 1; generated files stay on disk.
    
    **`--watch` and `--json` are mutually exclusive.** `--json` writes a
    single result and exits; `--watch` is a long-running stream. Passing
    both → exit 2 with a recipe error.
    
    ### `publish`
    
    ```jsonc
    // Success — a StackVersion was published. No deploymentId/shortId:
    // versions are addressed by semver.
    {
      "type": "published",
      "projectName": "my-api",
      "bundlePath": ".skmtc/my-api/server.js",
      "bundleBytes": 1228801,
      "bundleSha256": "e3b0c44298fc1c14...",
      "stack": { "account": "ada", "slug": "my-api" },
      "version": "3.0.1",
      "versionUrl": "https://skmtc.dev/ada/stacks/my-api/versions/3.0.1",
      "sourceFileCount": 28,
      "sourceTotalBytes": 96512
    }
    
    // Failure — `stage` pinpoints where:
    //   "version"  — no deno.json#version and no --version (fails before
    //                any network call)
    //   "identity" — GET /v1/user failed (usually a bad PAT)
    //   "bundle"   — local Deno bundling / source collection failed
    //   "publish"  — POST .../versions failed (commonly 409: that version
    //                is already published; versions are immutable — bump
    //                and re-publish)
    {
      "type": "failed",
      "projectName": "my-api",
      "reason": "version 3.0.1 is already published for ada/my-api — ...",
      "stage": "publish"
    }
    ```
    
    ## 11. Operational principles (user-facing subset)
    
    Selected from the full operational principles in `llms.md`. These are
    the ones most likely to override default LLM intuitions during CLI
    work:
    
    | Default intuition | Skmtc's stance |
    |---|---|
    | Add a config flag to customize a stock generator | Use `skmtc clone` and edit the source |
    | Run Prettier in the pipeline | Don't — produce valid TS; consumer formats separately |
    | Restart from scratch when something's off | Run `skmtc doctor --json` first; targeted fix beats nuke-and-pave |
    | Manually edit `bundle.js` or `worker.ts` | They're derived; run `skmtc bundle` to regenerate |
    | Mock the database in tests | Use real Supabase / real DB (project convention) |
    | Use `process.env.X` | Use `Deno.env.get('X')` — Deno codebase |
    | Use `skmtc deploy` to put a stack on the hub | The command is `skmtc publish` — `deploy` no longer exists. Stacks are published as immutable semver versions (`POST /v1/stacks/{account}/{stack}/versions`); there is no deploymentId/shortId/production alias in the CLI. Deployments and the `production` alias belong to hub *projects*, driven from the web app. |
    | After bumping to `@skmtc/core@0.5.0+`, treat the existing operation-level enrichment as still-valid | Wrap each `[id][path][method]` block in `{ "main": { … } }`. The variant level is now mandatory whenever an operation-level block exists — the engine throws at start with `"must include a 'main' variant"` if it's missing. See `concepts/variants.md`. |
    | Switch a generator between `install` and `clone` by editing only deno.json (or only the on-disk folder) | They're mutually exclusive states. A `jsr:` import in deno.json AND a `gen-X/` folder for the same name in the project root is a silent-failure footgun — deno's workspace resolver picks the local folder over the JSR pin, so the engine runs the vendored source even though the user thinks they're running the pinned version. See the *Imported vs cloned exclusivity* section below. |
    
    ### Imported vs cloned: mutually exclusive states
    
    For every `gen-*` generator referenced in a project's `deno.json#imports`,
    **exactly one** of the following must be true:
    
    | State | `deno.json#imports[…]` value | On-disk `gen-X/` folder | Source served by |
    |---|---|---|---|
    | Imported | `"jsr:@scope/gen-X@^1"` | **MUST NOT exist** | JSR (proxied via `/v1/generators/.../source`) |
    | Cloned | `"./gen-X/mod.ts"` | **MUST contain `mod.ts`** | hub R2 (uploaded with the release) |
    
    Mixed states are silently broken:
    
    - **`jsr:` import + folder both present**: deno's workspace resolver
      picks the local folder. The engine runs the vendored source. The
      pinned JSR version is ignored. No warning at runtime.
    - **`./path` import + no folder**: deno resolution fails at bundle
      time with a "module not found" — loud, easy to spot.
    - **Folder present + no import for it**: stale artefact from a
      previous `clone` that the user has since `install`-replaced
      without `rm -rf`'ing the directory. Harmless until the next
      `clone` of the same name re-uses the directory.
    
    **How states get out of sync:**
    
    - `skmtc clone @scope/gen-X` rewrites the import to `./gen-X/mod.ts`
      AND creates the folder. Then `skmtc install @scope/gen-X` rewrites
      the import back to `jsr:` — but doesn't remove the folder. The
      user is now in the silent-shadowing state.
    - Hand-edited `deno.json` divergent from disk reality.
    - Cloning into a previously-vendored directory without first
      cleaning it.
    
    **Detecting + correcting:**
    
    - `skmtc doctor` (when the consistency check lands) emits an `error`
      for each mismatch with the fix in the hint.
    - The hub validates at upload (`POST .../source` returns 422 with a
      precise reason if the uploaded `deno.json` contradicts the
      uploaded folder tree).
    - Manual remediation: pick the desired state, fix BOTH sides:
      - To use the pinned JSR version → import is `jsr:`, remove the
        folder.
      - To use cloned source → import is `./gen-X/mod.ts`, ensure the
        folder + `gen-X/mod.ts` exist.
    
    Full list in
    [`llms.md`, "Operational principles for proposing changes"](https://github.com/skmtc/skmtc/blob/main/deno/docs/llms.md#operational-principles-for-proposing-changes).
    
    ## Doctor check ids
    
    `skmtc doctor --json` output is self-describing (every check carries
    `id`, `status`, `message`, `hint`); this catalogue exists for
    reasoning about a specific check without running it.
    
    | Check id | What it inspects |
    |---|---|
    | `cli-version-current` | The running CLI vs the newest published `@skmtc/cli` — the only check that reaches the network (2s bound, `skipped` when unreachable, `--offline` skips it). Names Deno's 24h minimum-dependency-age window when the newest release is still inside it, since a reinstall without `--minimum-dependency-age=0` silently resolves an older one |
    | `install-lockfile` | `~/.deno/bin/.skmtc/deno.lock` — the installed CLI's version pin of `@skmtc/cli` and `@skmtc/core` |
    | `deno-version` | Running Deno is ≥ 2.4.0 — the floor for the esbuild-based `deno bundle` |
    | `hub-auth` | `~/.skmtc/auth.json` parses to `{ host, token }` — offline only; `skipped` when not logged in, `warning` + logout/login hint when malformed; never reports more than the token's last 4 chars |
    | `project-deno-json/<project>` | `deno.json` exists and parses |
    | `project-base-path/<project>` | `client.json#settings.basePath` present and relative |
    | `project-core-pin/<project>` | Project's `@skmtc/core` pin matches the CLI's major.minor |
    | `project-bundle/<project>` | `bundle.js` exists — every project (remote-only included) generates from it; warning with a `skmtc bundle` hint when missing |
    | `project-enrichments/<project>` | Last generate's `manifest.enrichmentWarnings` has no `warning`-level entries — dead enrichment config (typo'd generator id, path, method or model name) surfaces here between runs; `info` entries keep it `ok` |
    | `project-worker-pin/<project>` | If `worker.ts` exists, `@skmtc/worker` is pinned (the generated worker imports it); ok-noop before the first bundle |
    | `project-manifest/<project>` | `manifest.json` matches the current `@skmtc/core` schema |
    | `anchors-config/<project>` | `client.json#settings.anchors` shape; gen-maps are opt-in via `settings.anchors.enabled` |
    | `anchors-coverage/<project>` | Share of manifest files carrying an attribution sidecar; `warning` below threshold |
    | `anchors-staleness/<project>` | Sidecars on disk are current for the last run |
    
  • SKILL.md 10.9 KB
    ---
    name: skmtc-cli
    version: 0.5.1
    description: |
      Use the Skmtc CLI to scaffold projects, install or clone generators
      from JSR, configure schema sources and enrichments, and produce code
      artifacts from an OpenAPI v3 or GraphQL SDL schema. Teaches the
      workspace mental model (`<root>/.skmtc/<project>/`, client.json,
      bundle, manifest) and the agent contract (strict text / strict JSON
      modes, exit codes, recipe errors, `agent-context` + `doctor`); the
      command surface itself is discovered from the binary — `skmtc
      --help`, `skmtc <cmd> -h` — rather than carried in this skill.
    
      Use this skill when the user asks to "run skmtc", "generate code
      from an OpenAPI schema", "install a skmtc generator", "scaffold a
      skmtc project", "watch a skmtc project", "configure enrichments",
      "publish a stack", "deploy to skmtc-hub" (the command is `publish`;
      there is no `deploy`), "skmtc in CI", or invokes any CLI
      subcommand. For *authoring* a generator package (Projections,
      Snippets, transform functions), defer to `skmtc-generator`. When
      something is broken (no output, wrong output, error messages, stale
      bundle), verify before proposing a fix: read the manifest and the
      parse issues, and reproduce the failure first.
    allowed-tools:
      - Bash
      - Read
      - Glob
      - Grep
      - Write
      - Edit
    metadata:
      describes:
        '@skmtc/cli': '0.9'
    ---
    
    # Skmtc CLI
    
    The Skmtc CLI generates code from OpenAPI v3 or GraphQL SDL documents.
    It's a Deno binary that wraps a project workspace under
    `<root>/.skmtc/`, fetches generators from JSR, and runs them against a
    schema source pinned in each project.
    
    This skill carries what the binary cannot tell you: the workspace
    mental model, the agent contract, and the decisions that need intent.
    Everything else — the command list, per-command flags, argument
    shapes — lives in the binary itself and is always current there;
    §3 shows how to pull it on demand. This skill guides **using** the
    CLI; for authoring generator packages see `skmtc-generator`, for
    diagnosing failures see
    [debug-failing-generation](https://github.com/skmtc/skmtc/blob/main/deno/docs/using/how-to/debug-failing-generation.md).
    
    ## 1. Mental model
    
    | Concept | Where it lives | Notes |
    |---|---|---|
    | Skmtc root | nearest ancestor dir containing `.skmtc/` | Created by `skmtc init` |
    | Project | `<root>/.skmtc/<project>/` | One schema + one set of generators |
    | Project deps | `<root>/.skmtc/<project>/deno.json` | JSR imports of installed generators |
    | Schema pin | `<root>/.skmtc/<project>/.settings/client.json` | `source` field — URL or path. Resolution: explicit schema arg → `client.json#source` → interactive prompt (TTY only; strict mode fails with a recipe error) |
    | **basePath** | `client.json#settings.basePath` | **Must match the consumer app's `@` alias root.** Both the on-disk root for generated files AND the alias root in the bundler's resolver. Generators produce `@/<subdir>/...` paths assuming this alignment. Absolute paths are rejected at `init`. |
    | Bundle | `<root>/.skmtc/<project>/bundle.js` | Compiled worker entry. Regenerated by `bundle`/`dev`/`clone`/`install`. |
    | Manifest | `<root>/.skmtc/<project>/.settings/manifest.json` | Per-run record of every file written and every (generator × item) outcome |
    | Generator | JSR package or local folder | Local: `<root>/.skmtc/<project>/<gen-name>/` |
    | **Global state** | `~/.skmtc/` | `auth.json` (the hub PAT stored by `skmtc login`), shadow project state, schema caches. **Check this when local state alone doesn't explain a failure.** |
    
    A "project" is **not** the consuming app — it's the *generator
    configuration* the consuming app pulls code from.
    
    **Generators are opinionated templates, not configurable libraries.**
    Stock `@skmtc/gen-*` packages ship hardcoded defaults — export paths,
    identifier naming, peer imports, output shapes — and there are no
    config flags for any of them, deliberately. To change them,
    `skmtc clone` the generator into the project and edit its source;
    that is the customization seam, not a workaround. "Stock generator
    hardcodes X" is almost never a CLI bug. Enrichments supply the
    settings a generator's author declared it needs — its `enrichments.ts`
    schema is the whole contract a consumer can fill; cloning changes the
    shape.
    
    Two engine facts that shape CLI expectations: generator order never
    affects output (coordination is a memoized cache, not a dependency
    graph — never sequence generators), and render does not run a
    formatter (unformatted output is by design; consumers format
    separately).
    
    ## 2. The agent contract
    
    Every state-touching command supports three modes, picked
    automatically:
    
    | Mode | When | Behavior |
    |---|---|---|
    | **Interactive** | TTY attached and no `--json` / `--no-input` | Ink TUI; prompts for missing args |
    | **Strict text** | Non-TTY (CI / pipes / agents) OR `--no-input` | Plain-text result on stdout; missing required args fail with a recipe error on stderr |
    | **Strict JSON** | `--json` (implies `--no-input`) | Single JSON object on stdout; logs on stderr |
    
    **For agents: add `--json` to every command.** The CLI auto-degrades
    to non-interactive mode on any non-TTY stdin/stdout — no PTY wrappers
    needed. (Two exceptions surface in help: `dev` is long-running and
    has no `--json`; `create` has no `--json` yet.)
    
    Exit codes are consistent across all commands: `0` success (including
    documented no-ops), `2` required input missing or invalid (recipe
    error on stderr), `1` anything else (registry unreachable, schema
    parse failure, fatal parseIssue, typecheck failure). For `generate
    --json`, an empty `errors` array is the success condition — not the
    exit code alone.
    
    **Recipe errors are the discovery mechanism.** When a required
    argument is missing in strict mode, stderr carries the usage line, a
    worked example, and a `Discover:` line naming the command that lists
    the valid values (e.g. `ls .skmtc/` for project names). Trust it:
    run the command, read the recipe, run the discovery, retry.
    
    ## 3. The command surface lives in the binary
    
    Do not look for a command table in this skill — pull it live, where
    it is always current with the installed version:
    
    ```bash
    skmtc --help        # every command, with real descriptions
    skmtc <cmd> -h      # full flags for one command
    ```
    
    The newer commands' help descriptions (`status`, `eject`, `adopt`,
    `publish`, `push`, `pull`) carry their full semantics — read them
    there rather than guessing from the names. One naming trap help
    cannot intercept: there is **no `skmtc deploy`** — stacks are
    *published* (`skmtc publish`) as immutable semver versions;
    deployments and the `production` alias belong to hub projects and are
    driven from the web app, not the CLI.
    
    ## 4. First steps in a workspace
    
    ```bash
    skmtc agent-context --json    # enumerate projects, commands, state
    skmtc doctor --json           # check for known frictions
    ```
    
    These two give the full workspace picture without documentation
    lookups — `agent-context` is the snapshot, `doctor` the diagnostic,
    in that order. `doctor`'s `summary` is the worst status across checks
    (`error > warning > ok`; exit 1 only on `error`), and every check
    carries its own `id`, `status`, `message`, and remediation `hint` —
    the output is self-describing. The check-id catalogue, if you need to
    reason about a specific check: [`reference.md`](reference.md)
    §"Doctor check ids".
    
    ## 5. The bundle-freshness gotcha
    
    Generation runs the compiled `bundle.js`, not generator source — a
    stale bundle silently shadows source edits:
    
    ```
    1. `skmtc clone` triggers an automatic rebundle
    2. If `worker.ts` and `deno.json#imports` disagree, strict-mode
       generate refuses with a freshness error
    3. Remediation: `skmtc bundle <project>`, then re-run `generate`
    4. `skmtc doctor --json` surfaces the mismatch as `project-bundle/<project>`
    ```
    
    ## 6. Configuration: client.json and filters
    
    `.skmtc/<project>/.settings/client.json` — top level is
    `{ source?, settings }`; `settings` carries `basePath` (required,
    relative, no `..`), `packages`, `enrichments`, `skip`, `include`,
    `generatedSuffix`. Full annotated shape, every key:
    [`reference.md` §6](reference.md) — read it before editing the file.
    
    `packages` (optional) routes output into monorepo packages. Each
    entry `{ rootPath, moduleName? }` is a folder forward from `basePath`,
    which is then the common ancestor of every package, not a bundler
    alias. Inside a root, imports render `@/` from that root; from
    outside, they render the root's `moduleName`. Nested roots are subpath
    exports (`@app/sdk/models`) that share the outer package's `@`. Config
    load rejects `..`, a repeated root and the workspace root; render
    fails on an outside import of a root with no `moduleName`. Task page:
    `docs/using/how-to/generate-into-multiple-packages.md`.
    
    Enrichment misaddressing never errors. When a customization does not
    land, read `manifest.enrichmentWarnings` (printed after `generate`,
    and re-read by `skmtc doctor` as `project-enrichments/<project>`): a
    typo'd id, path, method, model name or leaf key is reported with the
    nearest match. Routing is the literal path plus lowercase method,
    never `operationId`, under a `main` variant key. A wrong-typed value
    fails only that item, recorded as `error` in the manifest.
    
    `settings.skip` / `settings.include` accept a whole generator, a
    per-operation entry (`path → method → variant[]`), or a per-model
    entry (`refName → variant[]`); `[]` means every variant. Filters are
    where **user intent** is expressed — never a generator's
    `isSupported`. Semantics and precedence:
    [`reference.md` §7](reference.md).
    
    Every command's `--json` envelope is one object discriminated by a
    `type` field; per-command shapes: [`reference.md` §8](reference.md) —
    read when parsing output, not before.
    
    ## 7. Task cards
    
    End-to-end workflows (setup, adding generators, enrichments, CI,
    publishing, customizing a stock generator, …) live in
    [`task-cards.md`](task-cards.md) — open the one card for the job in
    front of you. A single command doesn't need a card; `-h` covers it.
    
    ## 8. Boundary with other skills
    
    This skill ends at the CLI surface. Hand off when:
    
    - The next step edits a `.ts`/`.tsx` file under
      `<root>/.skmtc/<project>/<gen-name>/` → **skmtc-generator**
    - The user reports something broken and the cause isn't yet known →
      verify before proposing: manifest, parse issues, then a reproduction
      ([debug-failing-generation](https://github.com/skmtc/skmtc/blob/main/deno/docs/using/how-to/debug-failing-generation.md),
      [error codes](https://github.com/skmtc/skmtc/blob/main/deno/docs/reference/error-codes.md))
    
    ## Companion files
    
    Loaded on demand with the Read tool, never eagerly:
    
    | File | What it holds | Read it when |
    |---|---|---|
    | [`reference.md`](reference.md) | §6 client.json shape, §7 filter semantics, §8 JSON envelopes, §11 operational principles, doctor check ids | Editing settings, writing a filter, parsing `--json`, reasoning about a doctor check |
    | [`task-cards.md`](task-cards.md) | Twelve end-to-end workflow cards | Doing a multi-step job for the first time |
    
  • task-cards.md 16.9 KB
    # skmtc-cli — task cards
    
    Pull-loaded workflows for the `skmtc-cli` skill: end-to-end
    recipes for the common jobs, referenced from SKILL.md §7.
    
    Read the card for the job in front of you. The command surface lives
    in the binary (`skmtc --help`, `skmtc <cmd> -h`) — you do not need a
    card to run one command.
    
    ## Task cards
    
    ### Card: Setting up Skmtc in a project
    
    ```bash
    cd path/to/your-app                           # this becomes the Skmtc root
    skmtc init my-api ./src --json                # creates .skmtc/my-api/
    skmtc install @skmtc/gen-zod @skmtc/gen-typescript my-api --json
    # Edit .skmtc/my-api/.settings/client.json — set "source" to the schema URL/path
    skmtc generate my-api --json                  # one-shot generation
    ```
    
    The final `--json` run returns `{ files, stats, errors, parseIssues, ... }`.
    Inspect `errors` and `parseIssues` for any non-success outcomes.
    
    ### Card: Adding a generator to an existing project
    
    ```bash
    skmtc install @skmtc/gen-<name> <project> --json
    ```
    
    If `installed` is non-empty and `bundle.type === "bundled"` → ready
    to `generate`; the rebundle ran automatically (remote-only and
    hybrid projects alike).
    
    ### Card: Configuring enrichments
    
    1. Read the target generator's `gen-x/src/enrichments.ts` (in
       `skmtc-generators/` or via `deno info`) to learn the accepted
       *per-variant inner* shape. The variant axis is core-owned;
       generator schemas describe what goes inside a single variant.
    2. Edit `.skmtc/<project>/.settings/client.json` →
       `settings.enrichments[generatorId][...routingKeys][variant]`.
       Routing keys depend on the generator's factory:
       `[path][method][variant]` for OAS ops, `[refName][variant]` for
       models, `[rootKind][fieldName][variant]` for GraphQL ops. The
       variant level defaults to `'main'`; declare extra variants to
       get N artifacts per item from a variants-aware generator.
    3. Single-variant case (most common):
       ```jsonc
       { "@skmtc/gen-shadcn-form": { "/contacts": { "post":
         { "main": { "title": "Create Contact" } }
       } } }
       ```
    4. Multi-variant case (variants-aware generators only):
       ```jsonc
       { "@skmtc/gen-shadcn-form": { "/quotes/{id}": { "patch":
         {
           "main":     { "title": "Edit Quote" },
           "customer": { "title": "Customer section" }
         }
       } } }
       ```
    5. `skmtc generate <project>` — no rebundle needed; enrichments are
       runtime config.
    
    If you see `must include a 'main' variant` at engine start, you wrote
    non-`'main'` variant keys without `'main'`. Add it (often `"main": {}`
    is enough) or remove the other variants.
    
    ### Card: Pinning the schema source
    
    1. Edit `.skmtc/<project>/.settings/client.json` → add `"source"` at
       the top level.
    2. After this, `skmtc generate <project>` works without the schema
       positional arg.
    
    ### Card: Cleaning a project's generated output
    
    Use when stale output has accumulated and you want a guaranteed fresh
    tree, or before deleting a project. `clean` reads the manifest,
    deletes every file it recorded, prunes the directories those
    deletions emptied, and removes the manifest.
    
    ```bash
    skmtc clean <project> --dry-run --verbose   # preview: lists files + dirs, touches nothing
    skmtc clean <project> --json                # apply; returns { deleted, removedDirs, manifestRemoved, ... }
    ```
    
    Then, for a clean-slate regeneration:
    
    ```bash
    skmtc clean <project> --json && skmtc generate <project> --json
    ```
    
    Key facts:
    
    - **`clean` is the full delete; `generate`'s internal prune is
      incremental.** `generate` only deletes the files the *next* run
      won't rewrite (stale artifacts from a removed generator). `clean`
      deletes the *entire* manifest-recorded set. Both now also prune the
      directories they empty.
    - **Directory pruning is self-limiting and anchored.** It removes only
      dirs it emptied, stops at the first non-empty ancestor, and never
      removes `basePath` or a `packages[].rootPath`. If `basePath` is
      unset in `client.json`, dir pruning is skipped entirely.
    - **`clean` touches only generated output.** It never rebundles,
      contacts JSR, or edits `client.json` / `deno.json`. To uninstall a
      *generator*, use `remove`, not `clean`.
    - **No confirmation prompt** (no Ink variant). `--dry-run` is the
      safety valve; deletion is irreversible.
    - A project with no manifest (never generated, or already cleaned) →
      no-op, exit 0, `noManifest: true`.
    
    ### Card: Filtering operations (opt-in form generator pattern)
    
    ```jsonc
    // In client.json#settings:
    {
      "include": [
        {
          "@skmtc/gen-shadcn-form": {
            "/customers": { "post": [] },
            "/locations": { "post": [] }
          }
        }
      ]
    }
    ```
    
    This produces forms only for the listed (path, method) pairs. The
    empty variant array (`[]`) means "every variant of this method" —
    i.e. the standard "all" allow. To narrow to specific variants of a
    multi-variant operation, list them by name:
    
    ```jsonc
    "include": [{ "@skmtc/gen-shadcn-form":
      { "/quotes/{id}": { "patch": ["customer", "location"] } }
    }]
    ```
    
    Other operations route through other generators normally. Other
    generators not mentioned in `include` are unaffected — they continue
    producing their normal output.
    
    ### Card: Customizing a published generator
    
    ```bash
    skmtc clone <project> -g @skmtc/gen-<name> --json
    # Inspect: ls .skmtc/<project>/<gen-name>/src/
    # Edit src/base.ts (paths, identifiers) or src/<Main>.ts (output shape)
    skmtc dev <project>                           # rebundle + regenerate on save
    ```
    
    Hand off to `skmtc-generator` for the editing work.
    
    ### Card: Registering an agent-authored local generator (programmatic / sandbox use)
    
    Use this when generator **source already exists on disk** (authored
    programmatically, not scaffolded) and you want the CLI to run it —
    e.g. a sandbox handed "a folder of generator source + a schema".
    
    **`skmtc create` is agent-usable for Kotlin.** In a non-TTY session
    it runs headlessly from its command-line args, and
    `skmtc create <project> <name> model --lang kotlin` writes a WORKING
    baseline generator: a plain-signature `toKtValue` router with one
    module per scaffolded case (string/integer/number/boolean/array/ref/
    object), `protocol.ts` carrying the value-field contracts, decision
    cases throwing loudly (union, unknown, map-shaped object),
    `enrichments.ts`, and a real root `mod.ts` default export, plus the
    project `deno.json` registration — scaffold-then-customise is the
    preferred flow. The **TypeScript** templates (`--lang typescript`,
    the default) still scaffold stubs and leave the package root `mod.ts`
    empty — for TS, the direct-registration flow below remains the
    practical path.
    
    **The CLI discovers a local generator ONLY via the project
    `deno.json#imports`.** `toGeneratorIds()` = the import *keys* whose
    package name starts with `gen-`; `worker.ts` is generated by importing
    exactly those ids. A folder on disk not referenced from
    `deno.json#imports` is invisible.
    
    **`worker.ts` and `bundle.js` are the only derived artifacts** — never
    hand-write them. `bundle` regenerates `worker.ts` from
    `deno.json#imports`, then runs `deno bundle -o bundle.js worker.ts`.
    `deno.json` and `.settings/client.json` are *config*, not derived —
    hand-writing those is correct and expected.
    
    ```bash
    skmtc init lab <basePath> --json   # writes .skmtc/lab/deno.json ({}) + .settings/client.json
    ```
    
    Then, **by hand**, write under `.skmtc/lab/`:
    
    1. The generator folder `.skmtc/lab/<gen-dir>/`:
       ```
       <gen-dir>/
         deno.json     # { "name": "@<scope>/gen-<name>", "version": "0.0.1",
                       #   "exports": "./mod.ts", "imports": { ...cross-generator deps... } }
         mod.ts        # re-export the entry as DEFAULT:
                       #   export { xEntry as default } from './src/mod.ts'
         src/
           mod.ts      # toOasOperationEntry({ id, transform, ... }) /
                       #   toModelEntry({ id, transform }) — pure pipeline
                       #   config, NO `lang` field (core 0.8.0+)
           base.ts     # imports its projection-base veneer from the lang
                       #   package (e.g. toTsOasOperationProjectionBase from
                       #   @skmtc/lang-typescript; toKt*/toCs* for the Kotlin
                       #   / C# lang packages) — the import graph declares the
                       #   language
           *.ts
       ```
       - Package name **must** be `@<scope>/gen-<name>` — the `gen-`
         prefix is the discovery filter.
       - Root `mod.ts` **must have a default export that is the entry
         object** — the worker does `import g from '@scope/gen-x'` and
         reads `g.id`.
       - `src/mod.ts`'s entry `id` **must equal the package name** — it's
         the key `worker.ts` (`g.id`) and `client.json`
         enrichments/skip/include route on.
    
    2. Patch `.skmtc/lab/deno.json`:
       ```jsonc
       {
         "imports": {
           "@<scope>/gen-<name>": "./<gen-dir>/mod.ts",  // local generator
           "@skmtc/core":   "jsr:@skmtc/core@<pin>",      // peer deps the
           "@skmtc/worker": "jsr:@skmtc/worker@<pin>",    // generator src
           "@skmtc/lang-typescript": "jsr:@skmtc/lang-typescript@<pin>", // the lang package the base file imports
           "@std/path":     "jsr:@std/path@^1",           // imports by bare
           "tiny-invariant":"npm:tiny-invariant@^1.3.3"   // specifier
           // ...valibot, ts-pattern, etc. as the source needs
         },
         "workspace": ["./<gen-dir>"]
       }
       ```
       `init` writes an empty `{}`. `skmtc bundle` (and any command that
       rebundles — `clone`, `dev`) now adds the `@skmtc/core` and
       `@skmtc/worker` pins automatically, at the CLI's own versions, when
       it generates `worker.ts` — so you no longer hand-pin those two. You
       **do** still pin every *other* bare specifier the generator source
       imports (`@std/path`, `valibot`, `tiny-invariant`, …) — `bundle`
       only knows about the worker peer deps. Remote-only projects need
       no hand-pinning beyond the `jsr:` generator entries — published
       packages carry their own dependencies.
    
    3. Set the schema in `.skmtc/lab/.settings/client.json#source` (or
       pass it as the `generate` positional). `basePath` is set by `init`.
    
    ```bash
    skmtc bundle lab --json   # → { type: "bundled", bundlePath } — writes worker.ts AND bundle.js
    skmtc generate lab <schema> --json --typecheck
    ```
    
    `bundle` returns `type: "bundled"` for every project. To confirm the
    step-2 wiring took, check the generated `worker.ts` imports the
    local `@<scope>/gen-<name>` id — a missing entry means the import
    key isn't a `gen-*` entry in `deno.json#imports`.
    
    ### Card: Using Skmtc in CI
    
    ```bash
    # Setup (once per CI run) — the installer bootstraps Deno if needed;
    # SKMTC_VERSION pins the CLI so runs are reproducible:
    SKMTC_VERSION=<version> curl -fsSL https://skmtc.dev/install | sh
    # Build the project's bundle.js (required for every project unless a
    # fresh one is committed/cached):
    skmtc bundle <project>
    
    # Run:
    skmtc generate <project> --json --no-input --typecheck
    # Exit 0 on success, 1 on fatal parseIssue or typecheck failure.
    
    # Archive for forensics:
    cp <basePath>/../.skmtc/<project>/.settings/manifest.json ci-artifacts/
    ```
    
    `--unstable-worker-options` is required: `@skmtc/worker` constructs
    each per-project Worker with `new Worker(..., { deno: { permissions:
    {...} } })`. That uses Deno's `Worker.deno.permissions` API, which is
    gated behind this flag on current Deno releases. Without it the
    first `skmtc generate` exits at runtime with `Unstable API
    'Worker.deno.permissions'. The --unstable-worker-options flag must
    be provided.` The flag has to be passed at install time — `deno
    install` bakes the runtime flags into the installed CLI binary at
    `~/.deno/bin/skmtc`. If a previously-installed binary is missing the
    flag, reinstall with `-f` to overwrite it; adding the flag to
    invocations of the existing binary does not work.
    
    The install uses **scoped permissions, not `-A`**:
    `--allow-read --allow-write --allow-net --allow-env
    --allow-run=deno,sh --allow-sys=homedir`. skmtc reads/writes project
    files, fetches schemas + packages over the network (the schema
    `source` can be any URL, so `--allow-net` stays unscoped), reads a few
    env vars, spawns only `deno` (bundle) and `sh` (typecheck), and needs
    `homedir` to locate the workspace root. It uses no FFI and no remote
    imports, so those grants are dropped. Empirically validated against
    `doctor` / `generate` / `bundle`.
    
    ### Card: Publishing a stack version to skmtc-hub
    
    Use when the project should be shared on the hub as a stack package.
    Publishing creates an immutable semver version of the stack; a hub
    *project* later pins that version and runs it (deployments and the
    `production` alias are project concerns, driven from the web app).
    
    ```bash
    # 1. Set the version — `version` in .skmtc/<project>/deno.json,
    #    or pass --version. The CLI never invents or auto-bumps one.
    # 2a. One-off: log in once (paste a PAT from Settings → Access tokens;
    #     write:releases scope is enough), then publish with no token flags:
    skmtc login            # or: echo $PAT | skmtc login --with-token
    skmtc publish <project> --json
    # 2b. CI: pass the token explicitly (flag or env beats the stored login):
    skmtc publish <project> --token $SKMTC_HUB_TOKEN --json
    ```
    
    Key facts:
    
    - **Token resolution is `--token` → `$SKMTC_HUB_TOKEN` → the
      `skmtc login` store** (`~/.skmtc/auth.json`). When the token comes
      from the store, the store's `host` is also the default hub URL —
      a token minted against a local dev hub is never silently sent to
      production. Explicit `--origin` / `$SKMTC_ORIGIN` always win.
    - **The stack identity is the project `deno.json#name`** (`@account/slug`,
      the JSR-style package name) — the `@account` scope may be a user OR an
      **org**, so org-owned stacks are reachable: the PAT authenticates, the hub
      authorizes you as a `writer` on that account/stack. `name` is required —
      recipe error (stage `identity`) if missing or not a scoped `@account/slug`.
    - **The hub auto-creates the stack on first publish** ("git push
      creates the repo").
    - **Versions are immutable.** Re-publishing an existing semver →
      `409`, surfaced as `stage: "publish"` with an "already published"
      reason. Bump the version and re-run.
    - **Missing version fails fast** (`stage: "version"`, exit 1, before
      any network call) with the recipe: set `deno.json#version` or pass
      `--version`.
    - The upload is atomic: `version` + compiled `server.js` bundle + the
      source tree (filtered by built-in defaults + `.skmtcignore`) in one
      multipart request. The root `deno.json` must be in the upload — the
      hub reconciles the version's generator composition from it.
    - Read `version` / `versionUrl` from the JSON output. There is no
      `deploymentId` or `shortId` anymore.
    
    Full reference: [`reference/cli/publish.md`](https://github.com/skmtc/skmtc/blob/main/deno/docs/reference/cli/publish.md).
    
    ### Card: Pushing a project's config to skmtc-hub
    
    Use when local `client.json` edits (config + enrichments) should land
    on the project's hub project — the project-level counterpart to
    `publish`. `push` overwrites the hub project's config; it never creates
    a project (create it in the web app first).
    
    ```bash
    # Destination is the `project: "@account/slug"` field in client.json.
    skmtc login                                   # once; stores PAT + origin
    skmtc push <project> --json
    # First push to an org project — records the destination for next time:
    skmtc push <project> --project @acme-org/petstore-client --json
    ```
    
    Key facts:
    
    - **`<project>` is the LOCAL project** (`.skmtc/<project>/`) — the
      source. The **hub destination** is `client.json#project` (or
      `--project @account/slug`), decoupled from your identity like a git
      remote. The account may be an org; the hub slug can differ from the
      local dir name.
    - **Destination resolution:** `--project` → `client.json#project` →
      recipe error (no silent fallback to your handle). An explicit
      `--project` is written back into client.json (the `git push -u`
      ergonomic).
    - **Overwrites** the hub project's config. In a TTY it confirms first
      when config already exists (`--force` skips); in strict/`--json` it
      overwrites and reports `overwroteExistingConfig`.
    - **Never creates a project** — a `404` means "create it in the web
      app first". Authorization is checked against the destination account
      (org writers pass).
    - **`--base-files`** also pushes the app tree (package.json, components,
      css…) to `/preview/base-files`. Collected from the **app root**
      (`dirname(basePath)`) via the same `.skmtcignore` methodology as publish,
      minus `.skmtc/` and the manifest's generated files. Default push is
      config-only (config changes often, base files rarely).
    - Token + origin resolve exactly like `publish` (`--token` /
      `$SKMTC_HUB_TOKEN` / store; `--origin` / `$SKMTC_ORIGIN` / store host).
    
    Full reference: [`reference/cli/push.md`](https://github.com/skmtc/skmtc/blob/main/deno/docs/reference/cli/push.md).
    
    ### Card: When to hand off
    
    - "I want to edit this generator" → `skmtc-generator`
    - "Why is my generation failing / wrong / empty" → verify before
      proposing a fix: manifest first, then parse issues, then a
      reproduction —
      [debug-failing-generation](https://github.com/skmtc/skmtc/blob/main/deno/docs/using/how-to/debug-failing-generation.md)
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related