@ekanos/cli 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/README.md +81 -2
  2. package/dist/bin.js +14 -1
  3. package/dist/bin.js.map +1 -1
  4. package/dist/commands/dev.d.ts +4 -0
  5. package/dist/commands/dev.js +6 -20
  6. package/dist/commands/dev.js.map +1 -1
  7. package/dist/commands/init.d.ts +61 -0
  8. package/dist/commands/init.js +145 -18
  9. package/dist/commands/init.js.map +1 -1
  10. package/dist/commands/publish.d.ts +13 -0
  11. package/dist/commands/publish.js +38 -15
  12. package/dist/commands/publish.js.map +1 -1
  13. package/dist/commands/status.js +9 -2
  14. package/dist/commands/status.js.map +1 -1
  15. package/dist/commands/upgrade.d.ts +47 -0
  16. package/dist/commands/upgrade.js +546 -0
  17. package/dist/commands/upgrade.js.map +1 -0
  18. package/dist/commands/validate.d.ts +5 -0
  19. package/dist/commands/validate.js +20 -6
  20. package/dist/commands/validate.js.map +1 -1
  21. package/dist/context.d.ts +9 -0
  22. package/dist/context.js +9 -0
  23. package/dist/context.js.map +1 -1
  24. package/dist/copy-tree.d.ts +48 -0
  25. package/dist/copy-tree.js +55 -0
  26. package/dist/copy-tree.js.map +1 -0
  27. package/dist/delegate.d.ts +28 -0
  28. package/dist/delegate.js +136 -0
  29. package/dist/delegate.js.map +1 -0
  30. package/dist/harness-scaffold.d.ts +21 -6
  31. package/dist/harness-scaffold.js +8 -5
  32. package/dist/harness-scaffold.js.map +1 -1
  33. package/dist/index.js +40 -3
  34. package/dist/index.js.map +1 -1
  35. package/dist/package-manager.d.ts +10 -0
  36. package/dist/package-manager.js +32 -0
  37. package/dist/package-manager.js.map +1 -1
  38. package/dist/parse-argv.d.ts +7 -1
  39. package/dist/parse-argv.js +10 -2
  40. package/dist/parse-argv.js.map +1 -1
  41. package/dist/project-checks.d.ts +9 -0
  42. package/dist/project-checks.js +29 -0
  43. package/dist/project-checks.js.map +1 -1
  44. package/dist/project.d.ts +23 -0
  45. package/dist/project.js +66 -1
  46. package/dist/project.js.map +1 -1
  47. package/dist/publish-api.d.ts +1 -1
  48. package/dist/publish-api.js +2 -2
  49. package/dist/publish-api.js.map +1 -1
  50. package/dist/safe-fs.d.ts +1 -1
  51. package/dist/safe-fs.js.map +1 -1
  52. package/dist/schema-skew.d.ts +77 -0
  53. package/dist/schema-skew.js +163 -0
  54. package/dist/schema-skew.js.map +1 -0
  55. package/dist/sources-api.d.ts +1 -1
  56. package/dist/sources-api.js.map +1 -1
  57. package/dist/toolchain-api.d.ts +54 -0
  58. package/dist/toolchain-api.js +58 -0
  59. package/dist/toolchain-api.js.map +1 -0
  60. package/dist/toolchain-resolve.d.ts +26 -0
  61. package/dist/toolchain-resolve.js +100 -0
  62. package/dist/toolchain-resolve.js.map +1 -0
  63. package/dist/toolchain.d.ts +21 -0
  64. package/dist/toolchain.js +16 -0
  65. package/dist/toolchain.js.map +1 -0
  66. package/dist/update-notice.d.ts +32 -0
  67. package/dist/update-notice.js +180 -0
  68. package/dist/update-notice.js.map +1 -0
  69. package/dist/validate-findings.d.ts +12 -0
  70. package/dist/validate-findings.js +14 -1
  71. package/dist/validate-findings.js.map +1 -1
  72. package/package.json +1 -1
  73. package/templates/AGENTS.md.tmpl +81 -0
  74. package/templates/CLAUDE.md.tmpl +2 -1
  75. package/templates/claude-skill.md.tmpl +20 -2
  76. package/templates/docs/api-notes.md.tmpl +21 -0
  77. package/templates/mcp.json.tmpl +8 -0
  78. package/templates/skills/build-integration/SKILL.md +178 -0
  79. package/templates/skills/build-integration/references/api-docs-research.md +93 -0
  80. package/templates/skills/build-integration/references/api-wiring.md +94 -0
  81. package/templates/skills/build-integration/references/widget-design.md +132 -0
  82. package/templates/skills/polish-widgets/SKILL.md +139 -0
  83. package/templates/skills/polish-widgets/references/patterns.md +269 -0
package/README.md CHANGED
@@ -23,9 +23,25 @@ npx @ekanos/cli dev
23
23
  | `logout` | Revoke the session at Auth0 and delete the local credentials. |
24
24
  | `whoami` | Print the identity the stored session authenticates as, plus a compact `seats` list (slug + roles) from the same endpoint `sources` uses — degrades to `seats: null` rather than failing if that lookup is unavailable. |
25
25
  | `status` | The read verb: login state for the resolved host, this project's `ekanos.json`, and its submissions on the host (`GET /api/partner/submissions`). Never fails just because you are logged out — it reports `authenticated: false` instead. |
26
- | `sources` | List every Fusion source you hold a developer or admin seat on (`GET /api/partner/sources`), with a marker on this project's current publish target. |
26
+ | `sources` | List every Fusion source you hold a developer or source-admin seat on (`GET /api/partner/sources`), with a marker on this project's current publish target. |
27
27
  | `use` | `ekanos use <source-slug>` — set this project's publish target to a source you hold a seat on. Refuses (`forbidden`, with your actual seats in the hint) for a slug you don't hold a seat on, so a typo can't silently repoint a project. |
28
28
  | `publish` | Validate, pack and submit the integration to a Fusion host — refusing (`forbidden`) a target you hold no seat on, and confirming an ambiguous multi-seat target before submitting. See [Publish target verification](#publish-target-verification). |
29
+ | `upgrade` | Move this toolchain to a coherent target version, with verify-and-rollback. `--check` reports current vs. target and changes nothing; a bare `upgrade` installs, verifies (`validate` + the test script), and rolls back automatically on any failure. See [Keeping the toolchain current](#keeping-the-toolchain-current). |
30
+
31
+ ### Installing: global is fine
32
+
33
+ ```bash
34
+ npm i -g @ekanos/cli # convenient — the `ekanos` binary is always on PATH
35
+ ```
36
+
37
+ Inside a project, `validate`/`dev`/`publish`/`test` automatically delegate to
38
+ **that project's own** pinned `@ekanos/cli` devDependency whenever it differs
39
+ from the globally invoked binary — the same wrapper pattern as Gradle's
40
+ `./gradlew` or `npx`. A partner with several integrations on different
41
+ `@ekanos/sdk` versions gets the right toolchain per project without juggling
42
+ installs by hand. Machine-level verbs (`init`, `login`, `logout`, `whoami`,
43
+ `sources`, `use`, `status`, `upgrade`) never delegate — they always run on the
44
+ binary you invoked. Set `EKANOS_NO_DELEGATE=1` to opt out for one invocation.
29
45
 
30
46
  ## Global flags
31
47
 
@@ -54,6 +70,13 @@ and `publish` itself in JSON/non-TTY mode or with an explicit `--source`, never
54
70
  prompts. `init` requires `--slug` and fails with a usage error naming the flag
55
71
  rather than asking for it.
56
72
 
73
+ `validate`, `dev`, `publish` and `status` additionally carry an OPTIONAL
74
+ `data.toolchain` field — `{ status: 'behind' | 'unsupported', current, target,
75
+ upgradeCommand: 'ekanos upgrade' }` — whenever the background update check
76
+ (see [Keeping the toolchain current](#keeping-the-toolchain-current)) finds
77
+ this CLI is not current. Absent when current, unresolvable, or disabled via
78
+ `EKANOS_NO_UPDATE_CHECK`.
79
+
57
80
  ### Exit codes (frozen public contract)
58
81
 
59
82
  `0` ok · `1` internal · `2` usage · `3` validation-findings · `4` auth-required ·
@@ -236,6 +259,62 @@ returns** rather than blocking on a dev server that never exits — the envelope
236
259
  `data.server` carries `{ started: false, command, url, port }` so the caller can
237
260
  start it itself. Pass `--start` to override.
238
261
 
262
+ ## Keeping the toolchain current
263
+
264
+ Three independent defenses against a stale toolchain, all landing together
265
+ because they're the same underlying problem seen from three angles:
266
+
267
+ ### The schema-skew gate
268
+
269
+ `validate` and `publish` bundle an EXACT `@ekanos/integration-schema` version.
270
+ If the PROJECT resolves a NEWER one (checked against both a direct install and
271
+ whatever `@ekanos/sdk` itself declares a dependency on), both verbs fail fast
272
+ with `precondition_failed` (exit 9) naming both versions — **before** loading
273
+ the definition at all. This is not pedantry: in that state a field the newer
274
+ schema added parses here as an `unrecognized_keys` zod issue, and the usual
275
+ hint for that ("host-assigned fields are never partner-authorable — remove
276
+ them") tells you to delete code that is completely correct. A real partner hit
277
+ exactly this. Failing loudly beats answering falsely. The opposite skew — the
278
+ project's schema is OLDER — only warns; the CLI's own validation is still
279
+ correct, you just can't use fields it doesn't know about yet.
280
+
281
+ ### `ekanos upgrade`
282
+
283
+ Resolves a COHERENT target version set — `GET /api/partner/toolchain` on your
284
+ logged-in host when available, else the npm registry, with an explicit
285
+ coherence check (the `@ekanos/sdk`/`@ekanos/ui`/`@ekanos/integration-schema`
286
+ trio must share one version, and the sdk release at that version must pin
287
+ exactly that schema) so a mid-publish registry state is refused rather than
288
+ turned into an uninstallable tree.
289
+
290
+ - `ekanos upgrade --check` — read-only, reports current vs. target, changes
291
+ nothing. Safe to run anytime, including from CI or an agent.
292
+ - `ekanos upgrade` — snapshots `package.json` and the lockfile, installs the
293
+ target versions, then VERIFIES with `ekanos validate` and the project's own
294
+ test script. **Any failure at any step rolls everything back** — package.json,
295
+ lockfile and `node_modules` all restored — and reports exactly which step
296
+ failed with its output. `--yes` skips the interactive confirm (required
297
+ outside a real TTY); `--no-verify` skips the post-install verification.
298
+ - On success, the agent-context files (`AGENTS.md`, `CLAUDE.md`, the Claude
299
+ Code skill) are regenerated from the newly installed CLI's templates —
300
+ byte-compared against what the PRE-upgrade CLI would have rendered, so a
301
+ file you haven't touched is refreshed and one you've edited is left alone
302
+ and reported as needing manual attention. A stale AGENTS.md teaches an agent
303
+ yesterday's contract, which is as harmful as a stale binary.
304
+
305
+ ### The proactive update notice
306
+
307
+ `validate`, `dev`, `publish` and `status` all check (in the background, with a
308
+ 24h cache and a 1.5s timeout — never blocking, never changing a verb's exit
309
+ code) whether a newer `@ekanos/cli` exists. Human mode gets one stderr line;
310
+ JSON mode never prints prose to stdout, but the envelope's `data.toolchain`
311
+ carries `{ status: 'behind' | 'unsupported', current, target, upgradeCommand
312
+ }` when you're not current, and the same object rides the `claude-code-hint`
313
+ stderr line so an agent can act on it without parsing prose. `unsupported`
314
+ (below the host's declared minimum) makes `publish` fail fast — the host would
315
+ reject the submission anyway. Set `EKANOS_NO_UPDATE_CHECK=1` to disable
316
+ entirely.
317
+
239
318
  ## `ekanos init` on an existing project
240
319
 
241
320
  `npm init -y` first is fine. `init` **merges** into a `package.json` you
@@ -375,7 +454,7 @@ ekanos publish # verified before anything is packed or sent
375
454
  ```
376
455
 
377
456
  `ekanos sources` reads `GET /api/partner/sources` and lists every source you
378
- hold a `dev` or `admin` seat on, with a `*` marking the slug currently saved in
457
+ hold a `developer` or `source_admin` seat on, with a `*` marking the slug currently saved in
379
458
  this project's `ekanos.json`. `ekanos use <slug>` persists that field, but only
380
459
  after confirming the slug names a source you actually hold a seat on — a typo
381
460
  or a stale slug is refused (`forbidden`) with your real seat list in the hint,
package/dist/bin.js CHANGED
@@ -1,11 +1,24 @@
1
1
  #!/usr/bin/env node
2
+ import { announceDelegation, delegate, planDelegation } from './delegate.js';
2
3
  import { run } from './index.js';
3
4
  /**
4
5
  * The bin entry — the ONLY place that calls process.exit, so `run()` stays a
5
6
  * pure, testable function that returns an exit code. Argv is sliced to drop
6
7
  * the node executable and this script path.
8
+ *
9
+ * Before dispatching, check whether this invocation should delegate to the
10
+ * current project's OWN pinned `@ekanos/cli` — see `delegate.ts` for why.
7
11
  */
8
- run(process.argv.slice(2))
12
+ async function main() {
13
+ const argv = process.argv.slice(2);
14
+ const plan = planDelegation(argv, process.env, process.cwd());
15
+ if (plan.shouldDelegate) {
16
+ announceDelegation(plan, argv, process.env, process.stdout.isTTY === true);
17
+ return delegate(plan, argv, process.env);
18
+ }
19
+ return run(argv);
20
+ }
21
+ main()
9
22
  .then((code) => {
10
23
  process.exitCode = code;
11
24
  })
package/dist/bin.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"bin.js","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,GAAG,EAAE,MAAM,SAAS,CAAC;AAE9B;;;;GAIG;AACH,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;KACvB,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE;IACb,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;AAC1B,CAAC,CAAC;KACD,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;;IACxB,2EAA2E;IAC3E,oEAAoE;IACpE,uCAAuC;IACvC,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,iCACE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,MAAA,KAAK,CAAC,KAAK,mCAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CACxE,IAAI,CACL,CAAC;IACF,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;AACvB,CAAC,CAAC,CAAC","sourcesContent":["#!/usr/bin/env node\nimport { run } from './index';\n\n/**\n * The bin entry — the ONLY place that calls process.exit, so `run()` stays a\n * pure, testable function that returns an exit code. Argv is sliced to drop\n * the node executable and this script path.\n */\nrun(process.argv.slice(2))\n .then((code) => {\n process.exitCode = code;\n })\n .catch((error: unknown) => {\n // run() catches its own errors and emits the envelope; reaching here means\n // the envelope machinery itself threw. Fail loudly on stderr (never\n // stdout) with the internal exit code.\n process.stderr.write(\n `ekanos: fatal internal error: ${\n error instanceof Error ? (error.stack ?? error.message) : String(error)\n }\\n`,\n );\n process.exitCode = 1;\n });\n"]}
1
+ {"version":3,"file":"bin.js","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,kBAAkB,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC1E,OAAO,EAAE,GAAG,EAAE,MAAM,SAAS,CAAC;AAE9B;;;;;;;GAOG;AACH,KAAK,UAAU,IAAI;IACjB,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACnC,MAAM,IAAI,GAAG,cAAc,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IAE9D,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;QACxB,kBAAkB,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,MAAM,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC;QAC3E,OAAO,QAAQ,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC;IAC3C,CAAC;IAED,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC;AACnB,CAAC;AAED,IAAI,EAAE;KACH,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE;IACb,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;AAC1B,CAAC,CAAC;KACD,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;;IACxB,2EAA2E;IAC3E,oEAAoE;IACpE,uCAAuC;IACvC,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,iCACE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,MAAA,KAAK,CAAC,KAAK,mCAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CACxE,IAAI,CACL,CAAC;IACF,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;AACvB,CAAC,CAAC,CAAC","sourcesContent":["#!/usr/bin/env node\nimport { announceDelegation, delegate, planDelegation } from './delegate';\nimport { run } from './index';\n\n/**\n * The bin entry — the ONLY place that calls process.exit, so `run()` stays a\n * pure, testable function that returns an exit code. Argv is sliced to drop\n * the node executable and this script path.\n *\n * Before dispatching, check whether this invocation should delegate to the\n * current project's OWN pinned `@ekanos/cli` — see `delegate.ts` for why.\n */\nasync function main(): Promise<number> {\n const argv = process.argv.slice(2);\n const plan = planDelegation(argv, process.env, process.cwd());\n\n if (plan.shouldDelegate) {\n announceDelegation(plan, argv, process.env, process.stdout.isTTY === true);\n return delegate(plan, argv, process.env);\n }\n\n return run(argv);\n}\n\nmain()\n .then((code) => {\n process.exitCode = code;\n })\n .catch((error: unknown) => {\n // run() catches its own errors and emits the envelope; reaching here means\n // the envelope machinery itself threw. Fail loudly on stderr (never\n // stdout) with the internal exit code.\n process.stderr.write(\n `ekanos: fatal internal error: ${\n error instanceof Error ? (error.stack ?? error.message) : String(error)\n }\\n`,\n );\n process.exitCode = 1;\n });\n"]}
@@ -7,6 +7,9 @@ export interface DevArgs {
7
7
  /**
8
8
  * Interface to bind. Defaults to loopback — see `resolveHost`. Anything
9
9
  * else is an explicit, warned-about decision to expose the harness.
10
+ *
11
+ * NOTE: unrelated to the Fusion HOST the update notice checks against —
12
+ * this is the local bind address for `next dev`, never a Fusion origin.
10
13
  */
11
14
  host?: string;
12
15
  /** Rewrite every generated file even when its bytes already match. */
@@ -17,6 +20,7 @@ export interface DevArgs {
17
20
  * a process that never exits.
18
21
  */
19
22
  start?: boolean;
23
+ env?: Record<string, string | undefined>;
20
24
  }
21
25
  /**
22
26
  * `dev` — scaffold the local harness shell into the partner's project, then
@@ -5,6 +5,7 @@ import { preconditionError, usageError } from '../errors.js';
5
5
  import { HARNESS_DIR, HARNESS_PACKAGE, assertShellDirIsOurs, bootstrapHarnessConfig, ensureGitignoreEntry, missingDependencies, readInstalledHarness, scaffoldShell, } from '../harness-scaffold.js';
6
6
  import { detectPackageManager, execArgs, formatCommand, installCommand, } from '../package-manager.js';
7
7
  import { loadProject } from '../project.js';
8
+ import { announceUpdate, checkForUpdate } from '../update-notice.js';
8
9
  const DEFAULT_PORT = 3100;
9
10
  /**
10
11
  * Loopback, NOT Next's default.
@@ -36,12 +37,14 @@ const LOOPBACK_HOSTS = new Set(['127.0.0.1', '::1', 'localhost']);
36
37
  * and `.gitignore` never grows a duplicate line.
37
38
  */
38
39
  export async function runDev(ctx, args) {
39
- var _a, _b;
40
+ var _a, _b, _c;
40
41
  const loaded = loadProject(args.dir);
41
42
  const port = parsePort(args.port);
42
43
  const host = parseHost(args.host);
43
44
  const exposed = !LOOPBACK_HOSTS.has(host);
44
45
  const pm = detectPackageManager(loaded.projectDir);
46
+ const notice = await checkForUpdate({ env: (_a = args.env) !== null && _a !== void 0 ? _a : process.env });
47
+ announceUpdate(ctx, notice);
45
48
  // Fail before writing anything: the shell declares no dependencies of its
46
49
  // own, so a missing one would otherwise surface as an opaque Next
47
50
  // resolution error minutes later.
@@ -98,24 +101,7 @@ export async function runDev(ctx, args) {
98
101
  const serverArgs = ['dev', '--port', String(port), '--hostname', host];
99
102
  const command = formatCommand(pm.bin, execArgs(pm, 'next', serverArgs));
100
103
  const url = `http://${displayHost(host)}:${port}`;
101
- const scaffold = {
102
- slug: loaded.primary.slug,
103
- integrations: loaded.integrations.map((i) => i.slug),
104
- projectDir: loaded.projectDir,
105
- harnessDir: shell.harnessDir,
106
- harnessVersion,
107
- previousVersion: shell.previousVersion,
108
- previousMarker: shell.previousMarker,
109
- stale: shell.stale,
110
- staleReasons: shell.staleReasons,
111
- sourceGlobs: (_a = loaded.project.sourceGlobs) !== null && _a !== void 0 ? _a : [],
112
- created: shell.created,
113
- updated: shell.updated,
114
- unchanged: shell.unchanged,
115
- configBootstrapped: config.bootstrapped,
116
- configPath: config.configPath,
117
- gitignoreUpdated,
118
- };
104
+ const scaffold = Object.assign({ slug: loaded.primary.slug, integrations: loaded.integrations.map((i) => i.slug), projectDir: loaded.projectDir, harnessDir: shell.harnessDir, harnessVersion, previousVersion: shell.previousVersion, previousMarker: shell.previousMarker, stale: shell.stale, staleReasons: shell.staleReasons, sourceGlobs: (_b = loaded.project.sourceGlobs) !== null && _b !== void 0 ? _b : [], created: shell.created, updated: shell.updated, unchanged: shell.unchanged, configBootstrapped: config.bootstrapped, configPath: config.configPath, gitignoreUpdated }, (notice ? { toolchain: notice } : {}));
119
105
  if (exposed) {
120
106
  ctx.warn(`WARNING: --host ${host} exposes the harness beyond this machine. It ` +
121
107
  'has no authentication, and it renders your fixtures and — in live ' +
@@ -126,7 +112,7 @@ export async function runDev(ctx, args) {
126
112
  // exits would hang its tool call forever and it would never see the
127
113
  // envelope, so JSON mode scaffolds and reports the command instead of
128
114
  // running it. `--start` overrides; `--no-start` opts out in a terminal.
129
- const shouldStart = (_b = args.start) !== null && _b !== void 0 ? _b : !ctx.jsonMode;
115
+ const shouldStart = (_c = args.start) !== null && _c !== void 0 ? _c : !ctx.jsonMode;
130
116
  if (!shouldStart) {
131
117
  return ctx.succeed(Object.assign(Object.assign({}, scaffold), { server: { started: false, command, url, port, host, exposed } }), `dev: harness ready in ${shell.harnessDir}. Start it with: ${command}`);
132
118
  }
@@ -1 +1 @@
1
- {"version":3,"file":"dev.js","sourceRoot":"","sources":["../../src/commands/dev.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from 'node:child_process';\nimport * as path from 'node:path';\n\nimport { assertHarnessCompatible } from '../compatibility';\nimport type { CliContext } from '../context';\nimport { preconditionError, usageError } from '../errors';\nimport {\n HARNESS_DIR,\n HARNESS_PACKAGE,\n assertShellDirIsOurs,\n bootstrapHarnessConfig,\n ensureGitignoreEntry,\n missingDependencies,\n readInstalledHarness,\n scaffoldShell,\n} from '../harness-scaffold';\nimport {\n detectPackageManager,\n execArgs,\n formatCommand,\n installCommand,\n} from '../package-manager';\nimport { loadProject } from '../project';\n\nexport interface DevArgs {\n /** Project directory (contains ekanos.json). Defaults to cwd. */\n dir: string;\n /** Port for the dev server. */\n port?: string;\n /**\n * Interface to bind. Defaults to loopback — see `resolveHost`. Anything\n * else is an explicit, warned-about decision to expose the harness.\n */\n host?: string;\n /** Rewrite every generated file even when its bytes already match. */\n force: boolean;\n /**\n * Explicit `--start` / `--no-start`. Undefined means \"decide from the\n * output mode\": a JSON caller is an agent or a pipe and must not be handed\n * a process that never exits.\n */\n start?: boolean;\n}\n\nconst DEFAULT_PORT = 3100;\n\n/**\n * Loopback, NOT Next's default.\n *\n * `next dev` binds 0.0.0.0 unless told otherwise, so the harness would be\n * reachable by anyone on the same network — a coffee-shop Wi-Fi, a shared\n * office VLAN — while we printed \"http://localhost:3100\" and implied\n * otherwise. It renders a partner's integration with their fixtures and, in\n * live mode, whatever their vendor API returns; there is no auth in front of\n * any of it. Binding loopback by default costs a partner nothing and makes\n * exposure a thing they ask for.\n */\nconst DEFAULT_HOST = '127.0.0.1';\n\n/** Addresses that are only reachable from this machine. */\nconst LOOPBACK_HOSTS = new Set(['127.0.0.1', '::1', 'localhost']);\n\n/**\n * `dev` — scaffold the local harness shell into the partner's project, then\n * run it with their own Next.\n *\n * The shape of this verb follows from one rule: we must be able to ship a\n * harness bugfix as an `@ekanos/harness` version bump that a partner picks up\n * with `pnpm up`, with zero edits to any file they own. So everything under\n * `.ekanos/` is generated, gitignored and rewritten freely, `harness.config.ts`\n * at the project root is written once and never touched again, and all actual\n * harness behaviour lives in the installed package.\n *\n * Two idempotency properties matter and are both tested: a second run produces\n * byte-identical files and rewrites nothing (so Next's watcher stays quiet),\n * and `.gitignore` never grows a duplicate line.\n */\nexport async function runDev(ctx: CliContext, args: DevArgs): Promise<number> {\n const loaded = loadProject(args.dir);\n const port = parsePort(args.port);\n const host = parseHost(args.host);\n const exposed = !LOOPBACK_HOSTS.has(host);\n const pm = detectPackageManager(loaded.projectDir);\n\n // Fail before writing anything: the shell declares no dependencies of its\n // own, so a missing one would otherwise surface as an opaque Next\n // resolution error minutes later.\n const missing = missingDependencies(loaded.projectDir);\n if (missing.length > 0) {\n throw preconditionError(\n `${missing.length} package${missing.length === 1 ? '' : 's'} the ` +\n `harness needs ${missing.length === 1 ? 'is' : 'are'} not installed ` +\n `in ${loaded.projectDir}: ${missing.join(', ')}.`,\n `Run \"${installCommand(pm, missing)}\" in ${loaded.projectDir}, then ` +\n 're-run \"ekanos dev\".',\n );\n }\n\n const harness = readInstalledHarness(loaded.projectDir);\n const harnessVersion = harness?.version;\n if (!harnessVersion) {\n throw preconditionError(\n `${HARNESS_PACKAGE} is installed but declares no version.`,\n `Reinstall it with \"${installCommand(pm, [HARNESS_PACKAGE])}\" in ` +\n `${loaded.projectDir}, then re-run \"ekanos dev\".`,\n );\n }\n\n // The templates ship with the CLI and the code they import ships with the\n // harness, so a partner can upgrade one without the other. Name that\n // mismatch here rather than letting it surface later as a module-resolution\n // error inside a shell we cheerfully certified as current.\n assertHarnessCompatible({\n harnessVersion,\n harnessManifest: harness?.manifest,\n installHint: (spec) => installCommand(pm, [spec]),\n });\n\n // Validate the shell target BEFORE writing anything at all. Bootstrapping\n // harness.config.ts first and only then discovering that .ekanos/harness is\n // a file or someone else's tree would leave the project half-scaffolded.\n assertShellDirIsOurs(path.join(loaded.projectDir, HARNESS_DIR), args.force);\n\n const config = bootstrapHarnessConfig(loaded.projectDir, loaded.integrations);\n const shell = scaffoldShell({\n projectDir: loaded.projectDir,\n slug: loaded.primary.slug,\n harnessVersion,\n force: args.force,\n sourceGlobs: loaded.project.sourceGlobs,\n });\n const gitignoreUpdated = ensureGitignoreEntry(loaded.projectDir);\n\n if (shell.stale) {\n ctx.log(\n `Regenerating ${HARNESS_DIR} — ${shell.staleReasons.join('; ')}. ` +\n `Nothing you own changed; ${config.configPath} is untouched.`,\n );\n }\n\n if (config.bootstrapped) {\n ctx.log(` created harness.config.ts (yours — edit it, we never will)`);\n }\n for (const file of shell.created) ctx.log(` created ${HARNESS_DIR}/${file}`);\n for (const file of shell.updated) ctx.log(` updated ${HARNESS_DIR}/${file}`);\n if (gitignoreUpdated) ctx.log(' updated .gitignore (.ekanos/)');\n\n // `--hostname` is passed ALWAYS, never left to Next's default, so the URL\n // we print is the URL that is actually served.\n const serverArgs = ['dev', '--port', String(port), '--hostname', host];\n const command = formatCommand(pm.bin, execArgs(pm, 'next', serverArgs));\n const url = `http://${displayHost(host)}:${port}`;\n\n const scaffold = {\n slug: loaded.primary.slug,\n integrations: loaded.integrations.map((i) => i.slug),\n projectDir: loaded.projectDir,\n harnessDir: shell.harnessDir,\n harnessVersion,\n previousVersion: shell.previousVersion,\n previousMarker: shell.previousMarker,\n stale: shell.stale,\n staleReasons: shell.staleReasons,\n sourceGlobs: loaded.project.sourceGlobs ?? [],\n created: shell.created,\n updated: shell.updated,\n unchanged: shell.unchanged,\n configBootstrapped: config.bootstrapped,\n configPath: config.configPath,\n gitignoreUpdated,\n };\n\n if (exposed) {\n ctx.warn(\n `WARNING: --host ${host} exposes the harness beyond this machine. It ` +\n 'has no authentication, and it renders your fixtures and — in live ' +\n 'mode — real responses from your vendor API. Anyone who can reach ' +\n `${url} can read all of it. Omit --host to bind ${DEFAULT_HOST}.`,\n );\n }\n\n // A JSON caller is an agent or a pipe. Handing it a dev server that never\n // exits would hang its tool call forever and it would never see the\n // envelope, so JSON mode scaffolds and reports the command instead of\n // running it. `--start` overrides; `--no-start` opts out in a terminal.\n const shouldStart = args.start ?? !ctx.jsonMode;\n if (!shouldStart) {\n return ctx.succeed(\n {\n ...scaffold,\n server: { started: false, command, url, port, host, exposed },\n },\n `dev: harness ready in ${shell.harnessDir}. Start it with: ${command}`,\n );\n }\n\n ctx.log(`Starting the harness on ${url} (${command})…`);\n const exitCode = await spawnServer({\n ctx,\n bin: pm.bin,\n argv: execArgs(pm, 'next', serverArgs),\n cwd: shell.harnessDir,\n });\n\n const data = {\n ...scaffold,\n server: { started: true, command, url, port, host, exposed, exitCode },\n };\n\n if (exitCode === 0) {\n return ctx.succeed(data, `dev: harness stopped cleanly.`);\n }\n\n return ctx.failWith({\n code: 'dev_server_failed',\n message: `\"${command}\" exited with code ${exitCode}.`,\n hint:\n 'Read the Next output on stderr. If it cannot resolve @ekanos/harness ' +\n `or next, reinstall dependencies in ${loaded.projectDir}; if a route ` +\n `fails to compile, delete ${shell.harnessDir} and re-run \"ekanos dev\".`,\n // Transparent passthrough of Next's own exit code.\n exitCode,\n data,\n });\n}\n\nfunction spawnServer(params: {\n ctx: CliContext;\n bin: string;\n argv: string[];\n cwd: string;\n}): Promise<number> {\n return new Promise<number>((resolve, reject) => {\n const child = spawn(params.bin, params.argv, {\n cwd: params.cwd,\n // stdin inherited so Next's own key handling works; stdout/stderr piped\n // so we can route them and keep OUR stdout reserved for the envelope.\n stdio: ['inherit', 'pipe', 'pipe'],\n });\n\n child.stdout?.on('data', (chunk: Buffer) => {\n (params.ctx.jsonMode ? process.stderr : process.stdout).write(chunk);\n });\n child.stderr?.on('data', (chunk: Buffer) => {\n process.stderr.write(chunk);\n });\n\n child.on('error', (error) => reject(error));\n child.on('close', (code) => resolve(code ?? 1));\n });\n}\n\n/**\n * The hostname to show a human. `127.0.0.1` is spelled `localhost` because\n * that is what people type; a wildcard bind is spelled `localhost` too, but\n * only after `exposed` has already been warned about — printing `0.0.0.0` as\n * a URL is not useful.\n */\nfunction displayHost(host: string): string {\n if (host === '127.0.0.1' || host === '0.0.0.0' || host === '::') {\n return 'localhost';\n }\n if (host === '::1') return '[::1]';\n return host;\n}\n\nfunction parseHost(raw: string | undefined): string {\n if (raw === undefined) return DEFAULT_HOST;\n\n const host = raw.trim();\n if (host.length === 0) {\n throw usageError(\n 'Invalid --host \"\".',\n `Pass an interface to bind, e.g. \"--host 0.0.0.0\" to expose the ` +\n `harness on your network, or omit it to bind ${DEFAULT_HOST}.`,\n );\n }\n return host;\n}\n\nfunction parsePort(raw: string | undefined): number {\n if (raw === undefined) return DEFAULT_PORT;\n\n const port = Number(raw);\n if (!Number.isInteger(port) || port < 1 || port > 65535) {\n throw usageError(\n `Invalid --port \"${raw}\".`,\n 'Pass an integer between 1 and 65535, e.g. \"--port 3100\".',\n );\n }\n return port;\n}\n"]}
1
+ {"version":3,"file":"dev.js","sourceRoot":"","sources":["../../src/commands/dev.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from 'node:child_process';\nimport * as path from 'node:path';\n\nimport { assertHarnessCompatible } from '../compatibility';\nimport type { CliContext } from '../context';\nimport { preconditionError, usageError } from '../errors';\nimport {\n HARNESS_DIR,\n HARNESS_PACKAGE,\n assertShellDirIsOurs,\n bootstrapHarnessConfig,\n ensureGitignoreEntry,\n missingDependencies,\n readInstalledHarness,\n scaffoldShell,\n} from '../harness-scaffold';\nimport {\n detectPackageManager,\n execArgs,\n formatCommand,\n installCommand,\n} from '../package-manager';\nimport { loadProject } from '../project';\nimport { announceUpdate, checkForUpdate } from '../update-notice';\n\nexport interface DevArgs {\n /** Project directory (contains ekanos.json). Defaults to cwd. */\n dir: string;\n /** Port for the dev server. */\n port?: string;\n /**\n * Interface to bind. Defaults to loopback — see `resolveHost`. Anything\n * else is an explicit, warned-about decision to expose the harness.\n *\n * NOTE: unrelated to the Fusion HOST the update notice checks against —\n * this is the local bind address for `next dev`, never a Fusion origin.\n */\n host?: string;\n /** Rewrite every generated file even when its bytes already match. */\n force: boolean;\n /**\n * Explicit `--start` / `--no-start`. Undefined means \"decide from the\n * output mode\": a JSON caller is an agent or a pipe and must not be handed\n * a process that never exits.\n */\n start?: boolean;\n env?: Record<string, string | undefined>;\n}\n\nconst DEFAULT_PORT = 3100;\n\n/**\n * Loopback, NOT Next's default.\n *\n * `next dev` binds 0.0.0.0 unless told otherwise, so the harness would be\n * reachable by anyone on the same network — a coffee-shop Wi-Fi, a shared\n * office VLAN — while we printed \"http://localhost:3100\" and implied\n * otherwise. It renders a partner's integration with their fixtures and, in\n * live mode, whatever their vendor API returns; there is no auth in front of\n * any of it. Binding loopback by default costs a partner nothing and makes\n * exposure a thing they ask for.\n */\nconst DEFAULT_HOST = '127.0.0.1';\n\n/** Addresses that are only reachable from this machine. */\nconst LOOPBACK_HOSTS = new Set(['127.0.0.1', '::1', 'localhost']);\n\n/**\n * `dev` — scaffold the local harness shell into the partner's project, then\n * run it with their own Next.\n *\n * The shape of this verb follows from one rule: we must be able to ship a\n * harness bugfix as an `@ekanos/harness` version bump that a partner picks up\n * with `pnpm up`, with zero edits to any file they own. So everything under\n * `.ekanos/` is generated, gitignored and rewritten freely, `harness.config.ts`\n * at the project root is written once and never touched again, and all actual\n * harness behaviour lives in the installed package.\n *\n * Two idempotency properties matter and are both tested: a second run produces\n * byte-identical files and rewrites nothing (so Next's watcher stays quiet),\n * and `.gitignore` never grows a duplicate line.\n */\nexport async function runDev(ctx: CliContext, args: DevArgs): Promise<number> {\n const loaded = loadProject(args.dir);\n const port = parsePort(args.port);\n const host = parseHost(args.host);\n const exposed = !LOOPBACK_HOSTS.has(host);\n const pm = detectPackageManager(loaded.projectDir);\n\n const notice = await checkForUpdate({ env: args.env ?? process.env });\n announceUpdate(ctx, notice);\n\n // Fail before writing anything: the shell declares no dependencies of its\n // own, so a missing one would otherwise surface as an opaque Next\n // resolution error minutes later.\n const missing = missingDependencies(loaded.projectDir);\n if (missing.length > 0) {\n throw preconditionError(\n `${missing.length} package${missing.length === 1 ? '' : 's'} the ` +\n `harness needs ${missing.length === 1 ? 'is' : 'are'} not installed ` +\n `in ${loaded.projectDir}: ${missing.join(', ')}.`,\n `Run \"${installCommand(pm, missing)}\" in ${loaded.projectDir}, then ` +\n 're-run \"ekanos dev\".',\n );\n }\n\n const harness = readInstalledHarness(loaded.projectDir);\n const harnessVersion = harness?.version;\n if (!harnessVersion) {\n throw preconditionError(\n `${HARNESS_PACKAGE} is installed but declares no version.`,\n `Reinstall it with \"${installCommand(pm, [HARNESS_PACKAGE])}\" in ` +\n `${loaded.projectDir}, then re-run \"ekanos dev\".`,\n );\n }\n\n // The templates ship with the CLI and the code they import ships with the\n // harness, so a partner can upgrade one without the other. Name that\n // mismatch here rather than letting it surface later as a module-resolution\n // error inside a shell we cheerfully certified as current.\n assertHarnessCompatible({\n harnessVersion,\n harnessManifest: harness?.manifest,\n installHint: (spec) => installCommand(pm, [spec]),\n });\n\n // Validate the shell target BEFORE writing anything at all. Bootstrapping\n // harness.config.ts first and only then discovering that .ekanos/harness is\n // a file or someone else's tree would leave the project half-scaffolded.\n assertShellDirIsOurs(path.join(loaded.projectDir, HARNESS_DIR), args.force);\n\n const config = bootstrapHarnessConfig(loaded.projectDir, loaded.integrations);\n const shell = scaffoldShell({\n projectDir: loaded.projectDir,\n slug: loaded.primary.slug,\n harnessVersion,\n force: args.force,\n sourceGlobs: loaded.project.sourceGlobs,\n });\n const gitignoreUpdated = ensureGitignoreEntry(loaded.projectDir);\n\n if (shell.stale) {\n ctx.log(\n `Regenerating ${HARNESS_DIR} — ${shell.staleReasons.join('; ')}. ` +\n `Nothing you own changed; ${config.configPath} is untouched.`,\n );\n }\n\n if (config.bootstrapped) {\n ctx.log(` created harness.config.ts (yours — edit it, we never will)`);\n }\n for (const file of shell.created) ctx.log(` created ${HARNESS_DIR}/${file}`);\n for (const file of shell.updated) ctx.log(` updated ${HARNESS_DIR}/${file}`);\n if (gitignoreUpdated) ctx.log(' updated .gitignore (.ekanos/)');\n\n // `--hostname` is passed ALWAYS, never left to Next's default, so the URL\n // we print is the URL that is actually served.\n const serverArgs = ['dev', '--port', String(port), '--hostname', host];\n const command = formatCommand(pm.bin, execArgs(pm, 'next', serverArgs));\n const url = `http://${displayHost(host)}:${port}`;\n\n const scaffold = {\n slug: loaded.primary.slug,\n integrations: loaded.integrations.map((i) => i.slug),\n projectDir: loaded.projectDir,\n harnessDir: shell.harnessDir,\n harnessVersion,\n previousVersion: shell.previousVersion,\n previousMarker: shell.previousMarker,\n stale: shell.stale,\n staleReasons: shell.staleReasons,\n sourceGlobs: loaded.project.sourceGlobs ?? [],\n created: shell.created,\n updated: shell.updated,\n unchanged: shell.unchanged,\n configBootstrapped: config.bootstrapped,\n configPath: config.configPath,\n gitignoreUpdated,\n ...(notice ? { toolchain: notice } : {}),\n };\n\n if (exposed) {\n ctx.warn(\n `WARNING: --host ${host} exposes the harness beyond this machine. It ` +\n 'has no authentication, and it renders your fixtures and — in live ' +\n 'mode — real responses from your vendor API. Anyone who can reach ' +\n `${url} can read all of it. Omit --host to bind ${DEFAULT_HOST}.`,\n );\n }\n\n // A JSON caller is an agent or a pipe. Handing it a dev server that never\n // exits would hang its tool call forever and it would never see the\n // envelope, so JSON mode scaffolds and reports the command instead of\n // running it. `--start` overrides; `--no-start` opts out in a terminal.\n const shouldStart = args.start ?? !ctx.jsonMode;\n if (!shouldStart) {\n return ctx.succeed(\n {\n ...scaffold,\n server: { started: false, command, url, port, host, exposed },\n },\n `dev: harness ready in ${shell.harnessDir}. Start it with: ${command}`,\n );\n }\n\n ctx.log(`Starting the harness on ${url} (${command})…`);\n const exitCode = await spawnServer({\n ctx,\n bin: pm.bin,\n argv: execArgs(pm, 'next', serverArgs),\n cwd: shell.harnessDir,\n });\n\n const data = {\n ...scaffold,\n server: { started: true, command, url, port, host, exposed, exitCode },\n };\n\n if (exitCode === 0) {\n return ctx.succeed(data, `dev: harness stopped cleanly.`);\n }\n\n return ctx.failWith({\n code: 'dev_server_failed',\n message: `\"${command}\" exited with code ${exitCode}.`,\n hint:\n 'Read the Next output on stderr. If it cannot resolve @ekanos/harness ' +\n `or next, reinstall dependencies in ${loaded.projectDir}; if a route ` +\n `fails to compile, delete ${shell.harnessDir} and re-run \"ekanos dev\".`,\n // Transparent passthrough of Next's own exit code.\n exitCode,\n data,\n });\n}\n\nfunction spawnServer(params: {\n ctx: CliContext;\n bin: string;\n argv: string[];\n cwd: string;\n}): Promise<number> {\n return new Promise<number>((resolve, reject) => {\n const child = spawn(params.bin, params.argv, {\n cwd: params.cwd,\n // stdin inherited so Next's own key handling works; stdout/stderr piped\n // so we can route them and keep OUR stdout reserved for the envelope.\n stdio: ['inherit', 'pipe', 'pipe'],\n });\n\n child.stdout?.on('data', (chunk: Buffer) => {\n (params.ctx.jsonMode ? process.stderr : process.stdout).write(chunk);\n });\n child.stderr?.on('data', (chunk: Buffer) => {\n process.stderr.write(chunk);\n });\n\n child.on('error', (error) => reject(error));\n child.on('close', (code) => resolve(code ?? 1));\n });\n}\n\n/**\n * The hostname to show a human. `127.0.0.1` is spelled `localhost` because\n * that is what people type; a wildcard bind is spelled `localhost` too, but\n * only after `exposed` has already been warned about — printing `0.0.0.0` as\n * a URL is not useful.\n */\nfunction displayHost(host: string): string {\n if (host === '127.0.0.1' || host === '0.0.0.0' || host === '::') {\n return 'localhost';\n }\n if (host === '::1') return '[::1]';\n return host;\n}\n\nfunction parseHost(raw: string | undefined): string {\n if (raw === undefined) return DEFAULT_HOST;\n\n const host = raw.trim();\n if (host.length === 0) {\n throw usageError(\n 'Invalid --host \"\".',\n `Pass an interface to bind, e.g. \"--host 0.0.0.0\" to expose the ` +\n `harness on your network, or omit it to bind ${DEFAULT_HOST}.`,\n );\n }\n return host;\n}\n\nfunction parsePort(raw: string | undefined): number {\n if (raw === undefined) return DEFAULT_PORT;\n\n const port = Number(raw);\n if (!Number.isInteger(port) || port < 1 || port > 65535) {\n throw usageError(\n `Invalid --port \"${raw}\".`,\n 'Pass an integer between 1 and 65535, e.g. \"--port 3100\".',\n );\n }\n return port;\n}\n"]}
@@ -25,7 +25,53 @@ export interface InitArgs {
25
25
  force: boolean;
26
26
  /** Accepted for non-interactive ergonomics; init never prompts regardless. */
27
27
  yes: boolean;
28
+ /**
29
+ * Repeatable `--docs` entries — http(s) URLs or project-relative paths to
30
+ * the target API's documentation, written into the scaffolded ekanos.json
31
+ * as `docs`. Absent flag means the field is simply omitted; init never
32
+ * prompts for it.
33
+ */
34
+ docs?: string[];
28
35
  }
36
+ /**
37
+ * The files init writes: template basename → destination path.
38
+ *
39
+ * `package.json` is handled separately (see `writeManifest`) because it is the
40
+ * one file a partner plausibly already has — `npm init -y` before `ekanos
41
+ * init` is the ordinary way to start — and skipping it silently costs them
42
+ * every dependency and script.
43
+ */
44
+ /**
45
+ * The agent-context files. The SDK's day-1 users are coding agents, so the
46
+ * scaffold ships the authoring contract alongside the code: AGENTS.md is the
47
+ * contract, CLAUDE.md points at it, and the Claude Code skill teaches the
48
+ * iterate-validate-publish loop. Version-matched to the CLI because they live
49
+ * in its templates.
50
+ *
51
+ * Exported so `ekanos upgrade` can regenerate exactly this set from the
52
+ * NEWLY installed CLI's templates after a version bump — a stale AGENTS.md
53
+ * teaches an agent yesterday's contract, which is as harmful as a stale
54
+ * binary. One list, so `init` and `upgrade` can never disagree on what "the
55
+ * agent-context files" even are.
56
+ */
57
+ export declare const AGENT_CONTEXT_FILES: {
58
+ template: string;
59
+ dest: string;
60
+ }[];
61
+ /**
62
+ * The docs-grounding artifacts, exported for the same reason as
63
+ * `AGENT_CONTEXT_FILES`: `upgrade` must create these when they are missing
64
+ * (an older project upgrading into a toolchain whose AGENTS.md and skills
65
+ * now reference them), and one list keeps `init` and `upgrade` from ever
66
+ * disagreeing on what they are. Unlike the agent-context files these are
67
+ * NEVER regenerated — `docs/api-notes.md` is agent/partner-authored after
68
+ * the scaffold and `.mcp.json` may carry local additions — only created
69
+ * when absent.
70
+ */
71
+ export declare const DOCS_GROUNDING_FILES: {
72
+ template: string;
73
+ dest: string;
74
+ }[];
29
75
  /**
30
76
  * `init` — instantiate a project from the bundled template: ekanos.json, a
31
77
  * working minimal defineIntegration, a test, a vitest config (with the
@@ -35,3 +81,18 @@ export interface InitArgs {
35
81
  * with a usage error naming the flag.
36
82
  */
37
83
  export declare function runInit(ctx: CliContext, args: InitArgs): ExitCode;
84
+ /**
85
+ * Copy every bundled skill tree (`templates/skills/<name>/`) into the
86
+ * scaffolded project at `.claude/skills/<name>/`, verbatim — see
87
+ * `copyTreeVerbatim` for why these are bytes, never rendered templates. The
88
+ * walk is generic: a skill directory added to the published templates ships
89
+ * with no code change here. A CLI build with no `templates/skills/` at all
90
+ * (today's) scaffolds nothing and reports nothing.
91
+ *
92
+ * `skillsRoot` is injectable for tests; real callers take the default.
93
+ */
94
+ export declare function copyBundledSkillTrees(targetDir: string, force: boolean, skillsRoot?: string, onRefusal?: 'throw' | 'collect'): {
95
+ created: string[];
96
+ skipped: SkippedFile[];
97
+ refused: string[];
98
+ };
@@ -1,9 +1,12 @@
1
1
  import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
3
  import { printBanner } from '../banner.js';
4
- import { usageError } from '../errors.js';
4
+ import { copyTreeVerbatim } from '../copy-tree.js';
5
+ import { invalidStateError, usageError } from '../errors.js';
5
6
  import { mergeManifest, serializeManifest, } from '../merge-manifest.js';
6
- import { readTemplate, toDisplayName } from '../templates.js';
7
+ import { docEntryProblem, serializeProject, } from '../project.js';
8
+ import { classifyPath, readFileNoFollow, writeFileNoFollow } from '../safe-fs.js';
9
+ import { readTemplate, templatesDir, toDisplayName } from '../templates.js';
7
10
  const SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
8
11
  /**
9
12
  * The files init writes: template basename → destination path.
@@ -13,6 +16,42 @@ const SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
13
16
  * init` is the ordinary way to start — and skipping it silently costs them
14
17
  * every dependency and script.
15
18
  */
19
+ /**
20
+ * The agent-context files. The SDK's day-1 users are coding agents, so the
21
+ * scaffold ships the authoring contract alongside the code: AGENTS.md is the
22
+ * contract, CLAUDE.md points at it, and the Claude Code skill teaches the
23
+ * iterate-validate-publish loop. Version-matched to the CLI because they live
24
+ * in its templates.
25
+ *
26
+ * Exported so `ekanos upgrade` can regenerate exactly this set from the
27
+ * NEWLY installed CLI's templates after a version bump — a stale AGENTS.md
28
+ * teaches an agent yesterday's contract, which is as harmful as a stale
29
+ * binary. One list, so `init` and `upgrade` can never disagree on what "the
30
+ * agent-context files" even are.
31
+ */
32
+ export const AGENT_CONTEXT_FILES = [
33
+ { template: 'AGENTS.md.tmpl', dest: 'AGENTS.md' },
34
+ { template: 'CLAUDE.md.tmpl', dest: 'CLAUDE.md' },
35
+ { template: 'claude-skill.md.tmpl', dest: '.claude/skills/ekanos/SKILL.md' },
36
+ ];
37
+ /**
38
+ * The docs-grounding artifacts, exported for the same reason as
39
+ * `AGENT_CONTEXT_FILES`: `upgrade` must create these when they are missing
40
+ * (an older project upgrading into a toolchain whose AGENTS.md and skills
41
+ * now reference them), and one list keeps `init` and `upgrade` from ever
42
+ * disagreeing on what they are. Unlike the agent-context files these are
43
+ * NEVER regenerated — `docs/api-notes.md` is agent/partner-authored after
44
+ * the scaffold and `.mcp.json` may carry local additions — only created
45
+ * when absent.
46
+ */
47
+ export const DOCS_GROUNDING_FILES = [
48
+ // The agent-authored snapshot of the target API — see the template itself
49
+ // and AGENTS.md's "API documentation" section for the convention.
50
+ { template: 'docs/api-notes.md.tmpl', dest: 'docs/api-notes.md' },
51
+ // Per-project MCP config so Claude Code picks up Context7 (the docs
52
+ // fallback in the resolution ladder) without any global setup.
53
+ { template: 'mcp.json.tmpl', dest: '.mcp.json' },
54
+ ];
16
55
  const FILES = [
17
56
  { template: 'ekanos.json.tmpl', dest: 'ekanos.json' },
18
57
  { template: 'gitignore.tmpl', dest: '.gitignore' },
@@ -25,14 +64,8 @@ const FILES = [
25
64
  template: 'src/integration.test.ts.tmpl',
26
65
  dest: 'src/integration.test.ts',
27
66
  },
28
- // The agent-context files. The SDK's day-1 users are coding agents, so the
29
- // scaffold ships the authoring contract alongside the code: AGENTS.md is
30
- // the contract, CLAUDE.md points at it, and the Claude Code skill teaches
31
- // the iterate-validate-publish loop. Version-matched to the CLI because
32
- // they live in its templates.
33
- { template: 'AGENTS.md.tmpl', dest: 'AGENTS.md' },
34
- { template: 'CLAUDE.md.tmpl', dest: 'CLAUDE.md' },
35
- { template: 'claude-skill.md.tmpl', dest: '.claude/skills/ekanos/SKILL.md' },
67
+ ...DOCS_GROUNDING_FILES,
68
+ ...AGENT_CONTEXT_FILES,
36
69
  ];
37
70
  /**
38
71
  * `init` — instantiate a project from the bundled template: ekanos.json, a
@@ -43,6 +76,7 @@ const FILES = [
43
76
  * with a usage error naming the flag.
44
77
  */
45
78
  export function runInit(ctx, args) {
79
+ var _a;
46
80
  const slug = args.slug;
47
81
  if (!slug) {
48
82
  throw usageError('init requires a slug.', 'Pass it explicitly: "ekanos init --slug <your-integration> --yes". ' +
@@ -52,6 +86,18 @@ export function runInit(ctx, args) {
52
86
  throw usageError(`Invalid slug "${slug}".`, 'Use kebab-case: lowercase [a-z0-9] segments joined by single hyphens, ' +
53
87
  'e.g. "acme-crm".');
54
88
  }
89
+ // Same rule the ekanos.json schema enforces, applied at the flag so a bad
90
+ // entry fails HERE (usage, exit 2) instead of scaffolding a project whose
91
+ // very first `ekanos validate` cannot even load its config.
92
+ const docs = (_a = args.docs) !== null && _a !== void 0 ? _a : [];
93
+ for (const entry of docs) {
94
+ const problem = docEntryProblem(entry);
95
+ if (problem) {
96
+ throw usageError(`Invalid --docs value: ${problem}`, 'Pass --docs an http(s) URL or a project-relative path, e.g. ' +
97
+ '"--docs https://developer.example.com/openapi.json" or ' +
98
+ '"--docs docs/api.md". Repeat the flag for multiple entries.');
99
+ }
100
+ }
55
101
  // Default to `./<slug>`, not the cwd: scaffolding six files into whatever
56
102
  // directory the partner happened to be sitting in is how a real project got
57
103
  // polluted. `--dir .` remains the explicit way to scaffold in place.
@@ -61,9 +107,34 @@ export function runInit(ctx, args) {
61
107
  const vars = { SLUG: slug, DISPLAY_NAME: toDisplayName(slug) };
62
108
  const created = [];
63
109
  const skipped = [];
110
+ // The project root is the guard for every write below: creating it raw is
111
+ // fine (it is the boundary, not inside it), but everything under it goes
112
+ // through safe-fs so a planted symlink refuses loudly instead of teleporting
113
+ // a write outside the project. See safe-fs.ts for the threat model. The one
114
+ // case the guard cannot see is the boundary ITSELF being a symlink — the
115
+ // containment math is lexical against `targetDir`, so a planted link there
116
+ // would relocate every "contained" write wholesale. Refuse it. (A symlinked
117
+ // ANCESTOR — macOS's /tmp, a linked home — stays fine: only the leaf is
118
+ // checked.)
119
+ let targetStats = null;
120
+ try {
121
+ targetStats = fs.lstatSync(targetDir);
122
+ }
123
+ catch (_b) {
124
+ targetStats = null; // does not exist yet — ours to create
125
+ }
126
+ if (targetStats === null || targetStats === void 0 ? void 0 : targetStats.isSymbolicLink()) {
127
+ throw invalidStateError(`The target directory ${targetDir} is a symlink, so the scaffold's ` +
128
+ 'write-containment guarantees cannot hold there.', 'Scaffold into the real directory (pass its resolved path to --dir), ' +
129
+ 'or remove the symlink first.');
130
+ }
131
+ fs.mkdirSync(targetDir, { recursive: true });
64
132
  for (const { template, dest } of FILES) {
65
133
  const destPath = path.join(targetDir, dest);
66
- if (fs.existsSync(destPath) && !args.force) {
134
+ const kind = classifyPath(destPath);
135
+ // A symlink is never "already exists": skipping would keep the plant, and
136
+ // writing through it is the attack — writeFileNoFollow refuses it.
137
+ if (kind !== 'missing' && kind !== 'symlink' && !args.force) {
67
138
  skipped.push({
68
139
  file: dest,
69
140
  reason: 'it already exists',
@@ -72,10 +143,14 @@ export function runInit(ctx, args) {
72
143
  });
73
144
  continue;
74
145
  }
75
- fs.mkdirSync(path.dirname(destPath), { recursive: true });
76
- fs.writeFileSync(destPath, readTemplate(template, vars));
146
+ writeFileNoFollow(targetDir, destPath, dest === 'ekanos.json'
147
+ ? renderEkanosConfig(vars, docs)
148
+ : readTemplate(template, vars));
77
149
  created.push(dest);
78
150
  }
151
+ const skills = copyBundledSkillTrees(targetDir, args.force);
152
+ created.push(...skills.created);
153
+ skipped.push(...skills.skipped);
79
154
  const manifest = writeManifest(targetDir, vars, args.force);
80
155
  if (manifest.kind === 'created')
81
156
  created.push('package.json');
@@ -117,6 +192,56 @@ export function runInit(ctx, args) {
117
192
  printBanner(ctx, 'full');
118
193
  return ctx.succeed(data, summary);
119
194
  }
195
+ /**
196
+ * The scaffolded ekanos.json: the template, plus the collected `--docs`
197
+ * entries when there are any. Parse-then-serialize (rather than string
198
+ * surgery) so the written file is exactly what `loadProject` will read back.
199
+ */
200
+ function renderEkanosConfig(vars, docs) {
201
+ const rendered = readTemplate('ekanos.json.tmpl', vars);
202
+ if (docs.length === 0)
203
+ return rendered;
204
+ const project = JSON.parse(rendered);
205
+ return serializeProject(Object.assign(Object.assign({}, project), { docs }));
206
+ }
207
+ /**
208
+ * Copy every bundled skill tree (`templates/skills/<name>/`) into the
209
+ * scaffolded project at `.claude/skills/<name>/`, verbatim — see
210
+ * `copyTreeVerbatim` for why these are bytes, never rendered templates. The
211
+ * walk is generic: a skill directory added to the published templates ships
212
+ * with no code change here. A CLI build with no `templates/skills/` at all
213
+ * (today's) scaffolds nothing and reports nothing.
214
+ *
215
+ * `skillsRoot` is injectable for tests; real callers take the default.
216
+ */
217
+ export function copyBundledSkillTrees(targetDir, force, skillsRoot = path.join(templatesDir(), 'skills'), onRefusal = 'throw') {
218
+ if (!fs.existsSync(skillsRoot))
219
+ return { created: [], skipped: [], refused: [] };
220
+ const created = [];
221
+ const skipped = [];
222
+ const refused = [];
223
+ const skillDirs = fs
224
+ .readdirSync(skillsRoot, { withFileTypes: true })
225
+ .filter((entry) => entry.isDirectory())
226
+ .map((entry) => entry.name)
227
+ .sort();
228
+ for (const name of skillDirs) {
229
+ const destBase = path.join('.claude', 'skills', name);
230
+ const result = copyTreeVerbatim(path.join(skillsRoot, name), path.join(targetDir, destBase), { force, root: targetDir, onRefusal });
231
+ created.push(...result.created.map((rel) => path.join(destBase, rel)));
232
+ refused.push(...result.refused.map((rel) => path.join(destBase, rel)));
233
+ skipped.push(...result.skipped.map((rel) => {
234
+ const file = path.join(destBase, rel);
235
+ return {
236
+ file,
237
+ reason: 'it already exists',
238
+ hint: `Compare it against the bundled skill and merge anything you ` +
239
+ `are missing, or re-run with --force to overwrite ${file}.`,
240
+ };
241
+ }));
242
+ }
243
+ return { created, skipped, refused };
244
+ }
120
245
  /**
121
246
  * Write or MERGE package.json.
122
247
  *
@@ -129,15 +254,17 @@ export function runInit(ctx, args) {
129
254
  function writeManifest(targetDir, vars, force) {
130
255
  const destPath = path.join(targetDir, 'package.json');
131
256
  const rendered = readTemplate('package.json.tmpl', vars);
132
- if (!fs.existsSync(destPath) || force) {
133
- fs.mkdirSync(path.dirname(destPath), { recursive: true });
134
- fs.writeFileSync(destPath, rendered);
257
+ // readFileNoFollow refuses a symlinked package.json outright, so the write
258
+ // below can never follow one either; null means genuinely absent.
259
+ const existingRaw = readFileNoFollow(targetDir, destPath);
260
+ if (existingRaw === null || force) {
261
+ writeFileNoFollow(targetDir, destPath, rendered);
135
262
  return { kind: 'created', added: [], kept: [], notes: [] };
136
263
  }
137
264
  const template = JSON.parse(rendered);
138
265
  let existing;
139
266
  try {
140
- existing = JSON.parse(fs.readFileSync(destPath, 'utf8'));
267
+ existing = JSON.parse(existingRaw);
141
268
  }
142
269
  catch (error) {
143
270
  // Unparseable: do not guess, and do not clobber. Name the fix.
@@ -164,7 +291,7 @@ function writeManifest(targetDir, vars, force) {
164
291
  notes: result.notes,
165
292
  };
166
293
  }
167
- fs.writeFileSync(destPath, serializeManifest(result.manifest));
294
+ writeFileNoFollow(targetDir, destPath, serializeManifest(result.manifest));
168
295
  return {
169
296
  kind: 'merged',
170
297
  added: result.added,