@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
@@ -0,0 +1,546 @@
1
+ import { spawn } from 'node:child_process';
2
+ import * as fs from 'node:fs';
3
+ import * as os from 'node:os';
4
+ import * as path from 'node:path';
5
+ import { resolveAuthEnvironment } from '../auth/session.js';
6
+ import { cliVersion } from '../compatibility.js';
7
+ import { networkError, preconditionError, validationError } from '../errors.js';
8
+ import { EXIT_CODES } from '../exit-codes.js';
9
+ import { readInstalledPackageManifest } from '../harness-scaffold.js';
10
+ import { detectPackageManager, installExactArgs, lockfileName, } from '../package-manager.js';
11
+ import { loadProject } from '../project.js';
12
+ import { classifyPath, writeFileNoFollow } from '../safe-fs.js';
13
+ import { renderTemplate, toDisplayName } from '../templates.js';
14
+ import { TOOLCHAIN_PACKAGES, } from '../toolchain.js';
15
+ import { resolveToolchainTarget } from '../toolchain-resolve.js';
16
+ import { collectAllFindings } from '../validate-findings.js';
17
+ import { AGENT_CONTEXT_FILES, DOCS_GROUNDING_FILES, copyBundledSkillTrees, } from './init.js';
18
+ import { promptYesNo } from './publish.js';
19
+ export async function runUpgrade(ctx, args, deps = {}) {
20
+ var _a, _b, _c, _d, _e, _f, _g;
21
+ const loaded = loadProject(args.dir);
22
+ const pm = detectPackageManager(loaded.projectDir);
23
+ // Captured BEFORE anything installs: the templates the project's CURRENT
24
+ // (pre-upgrade) local `@ekanos/cli` ships, which is what actually
25
+ // generated the agent-context files on disk — not necessarily this
26
+ // process's own bundled templates, since `upgrade` is a machine-level verb
27
+ // that never delegates and is routinely invoked via a DIFFERENT (global)
28
+ // install. See `regenerateAgentContextFiles`.
29
+ const oldAgentTemplates = captureAgentTemplates(loaded.projectDir);
30
+ const current = readCurrentToolchain(loaded.projectDir);
31
+ const session = resolveSessionQuietly(ctx, args);
32
+ const resolveTarget = (_a = deps.resolveTarget) !== null && _a !== void 0 ? _a : resolveToolchainTarget;
33
+ const resolution = await resolveTarget({
34
+ host: session === null || session === void 0 ? void 0 : session.host,
35
+ session: session === null || session === void 0 ? void 0 : session.session,
36
+ });
37
+ if (resolution.status === 'unavailable') {
38
+ throw networkError('Could not resolve a toolchain target from the Fusion host or the ' +
39
+ 'npm registry.', 'Check connectivity and retry. If this deployment has no ' +
40
+ '"/api/partner/toolchain" route yet, the npm registry is the ' +
41
+ 'fallback and a failure there means npm itself is unreachable.');
42
+ }
43
+ if (resolution.status === 'incoherent') {
44
+ throw preconditionError(`The resolved toolchain versions do not form an installable set: ` +
45
+ resolution.detail, 'Retry in a moment — this is almost always a multi-package publish ' +
46
+ 'still in flight, and it self-resolves once every package finishes ' +
47
+ 'publishing.');
48
+ }
49
+ const target = resolution.target;
50
+ const deltas = computeDeltas(current, target);
51
+ if (args.check) {
52
+ return ctx.succeed(Object.assign({ source: resolution.source, current,
53
+ target,
54
+ deltas }, (resolution.source === 'host'
55
+ ? { minimum: resolution.minimum, notes: resolution.notes }
56
+ : {})), checkSummary(resolution.source, deltas));
57
+ }
58
+ const changedKeys = Object.keys(deltas).filter((key) => deltas[key].changed);
59
+ if (changedKeys.length === 0) {
60
+ return ctx.succeed({ source: resolution.source, current, target, deltas }, `upgrade: already at the target toolchain (source: ${resolution.source}).`);
61
+ }
62
+ const isHuman = !ctx.jsonMode;
63
+ const stdinIsTTY = (_b = args.stdinIsTTY) !== null && _b !== void 0 ? _b : process.stdin.isTTY === true;
64
+ const canPrompt = isHuman && stdinIsTTY;
65
+ if (!args.yes) {
66
+ if (!canPrompt) {
67
+ throw validationError('Refusing to modify the project non-interactively without --yes.', 'Pass "ekanos upgrade --yes" to proceed, or "ekanos upgrade --check" ' +
68
+ 'to preview the target with no changes.');
69
+ }
70
+ ctx.log(planSummary(resolution.source, deltas));
71
+ const confirm = (_c = args.confirm) !== null && _c !== void 0 ? _c : promptYesNo;
72
+ const confirmed = await confirm('Upgrade now? [y/N]');
73
+ if (!confirmed) {
74
+ throw validationError('Upgrade was not confirmed.', 'Re-run "ekanos upgrade --yes" to proceed non-interactively, or ' +
75
+ '"ekanos upgrade --check" to preview with no changes. Nothing was ' +
76
+ 'modified.');
77
+ }
78
+ }
79
+ const packageJsonPath = path.join(loaded.projectDir, 'package.json');
80
+ const manifest = readManifest(packageJsonPath);
81
+ const specs = buildInstallSpecs(manifest, target, changedKeys);
82
+ if (specs.length === 0) {
83
+ return ctx.succeed({ source: resolution.source, current, target, deltas }, 'upgrade: none of the changed packages are direct dependencies of ' +
84
+ 'this project — nothing to install directly. Their versions follow ' +
85
+ 'from whichever direct dependency pins them.');
86
+ }
87
+ const snapshot = snapshotProject(loaded.projectDir, pm);
88
+ try {
89
+ const runInstall = (_d = deps.runInstall) !== null && _d !== void 0 ? _d : defaultRunInstall;
90
+ for (const group of [
91
+ specs.filter((s) => !s.dev),
92
+ specs.filter((s) => s.dev),
93
+ ]) {
94
+ if (group.length === 0)
95
+ continue;
96
+ const argv = installExactArgs(pm, group.map((s) => `${s.name}@${s.version}`), group[0].dev);
97
+ ctx.log(`Running "${pm.bin} ${argv.join(' ')}" in ${loaded.projectDir}…`);
98
+ const result = await runInstall(argv, pm.bin, loaded.projectDir);
99
+ if (result.code !== 0) {
100
+ return await failAndRollback(ctx, snapshot, pm, loaded.projectDir, runInstall, {
101
+ step: 'install',
102
+ exitCode: EXIT_CODES.PRECONDITION_FAILED,
103
+ message: `"${pm.bin} ${argv.join(' ')}" exited with code ${result.code}.`,
104
+ output: result.output,
105
+ });
106
+ }
107
+ }
108
+ if (!args.noVerify) {
109
+ const runValidateStep = (_e = deps.runValidateStep) !== null && _e !== void 0 ? _e : defaultRunValidateStep;
110
+ const validateResult = await runValidateStep(loaded.projectDir);
111
+ if (!validateResult.ok) {
112
+ return await failAndRollback(ctx, snapshot, pm, loaded.projectDir, runInstall, {
113
+ step: 'validate',
114
+ exitCode: EXIT_CODES.VALIDATION,
115
+ message: 'The upgraded toolchain fails "ekanos validate" against this ' +
116
+ "project's own integration definition.",
117
+ output: JSON.stringify((_f = validateResult.findings) !== null && _f !== void 0 ? _f : [], null, 2),
118
+ });
119
+ }
120
+ const runTestStep = (_g = deps.runTestStep) !== null && _g !== void 0 ? _g : defaultRunTestStep;
121
+ const testResult = await runTestStep(loaded.projectDir, pm);
122
+ if (testResult.code !== 0) {
123
+ return await failAndRollback(ctx, snapshot, pm, loaded.projectDir, runInstall, {
124
+ step: 'test',
125
+ exitCode: testResult.code,
126
+ message: `The project's test script exited with code ${testResult.code} on the upgraded toolchain.`,
127
+ output: testResult.output,
128
+ });
129
+ }
130
+ }
131
+ }
132
+ finally {
133
+ snapshot.dispose();
134
+ }
135
+ const newTemplatesDir = path.join(loaded.projectDir, 'node_modules', '@ekanos', 'cli', 'templates');
136
+ const regenerated = regenerateAgentContextFiles({
137
+ projectDir: loaded.projectDir,
138
+ slug: loaded.primary.slug,
139
+ oldTemplates: oldAgentTemplates,
140
+ newTemplatesDir,
141
+ });
142
+ // The regenerated AGENTS.md and skills may now REFERENCE artifacts this
143
+ // project predates (docs/api-notes.md, .mcp.json, bundled skill trees) —
144
+ // instructions pointing at files that don't exist are exactly the stale-docs
145
+ // failure the docs-grounding feature closes, so create what's missing.
146
+ // Create-if-missing ONLY: these files are partner/agent-authored after the
147
+ // scaffold and are never regenerated.
148
+ const scaffolded = scaffoldMissingDocsArtifacts({
149
+ projectDir: loaded.projectDir,
150
+ slug: loaded.primary.slug,
151
+ newTemplatesDir,
152
+ });
153
+ // `specs` only ever covers DIRECT dependencies (see `buildInstallSpecs`) —
154
+ // a changed key with no spec is transitive and was never actually
155
+ // installed here, so the report (and the human summary) must say so
156
+ // explicitly rather than implying every changed key was upgraded. This
157
+ // matters most for exactly the scenario this PR's own delegation feature
158
+ // targets: a globally-installed `ekanos` with no local `@ekanos/cli`
159
+ // devDependency, where `cli` changes in `deltas` but the running global
160
+ // binary itself is untouched by this command.
161
+ const installedKeys = specs.map((spec) => spec.key);
162
+ const skippedKeys = changedKeys.filter((key) => !installedKeys.includes(key));
163
+ return ctx.succeed({
164
+ source: resolution.source,
165
+ current,
166
+ target,
167
+ deltas,
168
+ installed: installedKeys,
169
+ skippedTransitive: skippedKeys,
170
+ regenerated,
171
+ scaffolded,
172
+ }, successSummary(installedKeys, skippedKeys, deltas, regenerated, scaffolded));
173
+ }
174
+ /**
175
+ * Best-effort host/session resolution — NEVER throws. `upgrade` must work for
176
+ * a partner who has never logged in at all (pure npm-registry flow), so the
177
+ * throwing `resolveAuthEnvironment`/`requireSession` machinery is unusable
178
+ * here; this mirrors the tolerant lookup `update-notice.ts` uses.
179
+ */
180
+ function resolveSessionQuietly(ctx, args) {
181
+ try {
182
+ const env = resolveAuthEnvironment(ctx, args.host, args.env, {
183
+ projectDir: args.dir,
184
+ });
185
+ const session = env.store.read(env.host);
186
+ return session ? { host: env.host, session } : null;
187
+ }
188
+ catch (_a) {
189
+ return null;
190
+ }
191
+ }
192
+ function readCurrentToolchain(projectDir) {
193
+ var _a;
194
+ const result = {};
195
+ for (const [key, pkg] of Object.entries(TOOLCHAIN_PACKAGES)) {
196
+ const installed = readInstalledPackageManifest(projectDir, pkg);
197
+ result[key] = (_a = installed === null || installed === void 0 ? void 0 : installed.version) !== null && _a !== void 0 ? _a : (key === 'cli' ? cliVersion() : null);
198
+ }
199
+ return result;
200
+ }
201
+ function computeDeltas(current, target) {
202
+ const keys = Object.keys(TOOLCHAIN_PACKAGES);
203
+ const result = {};
204
+ for (const key of keys) {
205
+ result[key] = {
206
+ current: current[key],
207
+ target: target[key],
208
+ changed: current[key] !== target[key],
209
+ };
210
+ }
211
+ return result;
212
+ }
213
+ /**
214
+ * Only packages the project depends on DIRECTLY are installed explicitly —
215
+ * `@ekanos/integration-schema` and often `@ekanos/harness` are transitive
216
+ * (pulled in by `@ekanos/sdk`), and forcing an exact transitive version would
217
+ * need a package-manager-specific override mechanism this CLI does not
218
+ * manage. Their versions follow from whichever direct dependency pins them,
219
+ * which is exactly what the coherent target guarantees is already correct.
220
+ */
221
+ function buildInstallSpecs(manifest, target, changedKeys) {
222
+ var _a, _b;
223
+ const deps = ((_a = manifest.dependencies) !== null && _a !== void 0 ? _a : {});
224
+ const devDeps = ((_b = manifest.devDependencies) !== null && _b !== void 0 ? _b : {});
225
+ const specs = [];
226
+ for (const key of changedKeys) {
227
+ const pkg = TOOLCHAIN_PACKAGES[key];
228
+ if (pkg in deps) {
229
+ specs.push({ key, name: pkg, version: target[key], dev: false });
230
+ }
231
+ else if (pkg in devDeps) {
232
+ specs.push({ key, name: pkg, version: target[key], dev: true });
233
+ }
234
+ }
235
+ return specs;
236
+ }
237
+ function readManifest(packageJsonPath) {
238
+ let parsed;
239
+ try {
240
+ parsed = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
241
+ }
242
+ catch (error) {
243
+ throw preconditionError(`Could not read ${packageJsonPath}: ${error instanceof Error ? error.message : String(error)}`, 'Ensure the project has a valid package.json, then re-run "ekanos upgrade".');
244
+ }
245
+ if (typeof parsed !== 'object' || parsed === null) {
246
+ throw preconditionError(`${packageJsonPath} does not contain a JSON object.`, 'Fix package.json, then re-run "ekanos upgrade".');
247
+ }
248
+ return parsed;
249
+ }
250
+ /**
251
+ * Copy `package.json` and the lockfile to a temp directory before anything
252
+ * installs. `restore()` copies them back (and removes a lockfile that did not
253
+ * exist before, if the failed install created one); `dispose()` cleans up the
254
+ * temp copies once the run is over, success or failure.
255
+ */
256
+ function snapshotProject(projectDir, pm) {
257
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ekanos-upgrade-'));
258
+ const packageJsonPath = path.join(projectDir, 'package.json');
259
+ // bun has shipped both a text (`bun.lock`) and a legacy binary
260
+ // (`bun.lockb`) lockfile; snapshot whichever is actually present.
261
+ const lockCandidates = pm.name === 'bun' ? ['bun.lock', 'bun.lockb'] : [lockfileName(pm)];
262
+ const lockName = lockCandidates.find((name) => fs.existsSync(path.join(projectDir, name)));
263
+ const lockfilePath = lockName
264
+ ? path.join(projectDir, lockName)
265
+ : path.join(projectDir, lockfileName(pm));
266
+ const lockfileExisted = lockName !== undefined;
267
+ fs.copyFileSync(packageJsonPath, path.join(tmpDir, 'package.json'));
268
+ if (lockfileExisted) {
269
+ fs.copyFileSync(lockfilePath, path.join(tmpDir, 'lockfile.snapshot'));
270
+ }
271
+ return {
272
+ restore: () => {
273
+ fs.copyFileSync(path.join(tmpDir, 'package.json'), packageJsonPath);
274
+ if (lockfileExisted) {
275
+ fs.copyFileSync(path.join(tmpDir, 'lockfile.snapshot'), lockfilePath);
276
+ }
277
+ else if (fs.existsSync(lockfilePath)) {
278
+ fs.rmSync(lockfilePath);
279
+ }
280
+ },
281
+ dispose: () => {
282
+ fs.rmSync(tmpDir, { recursive: true, force: true });
283
+ },
284
+ };
285
+ }
286
+ /**
287
+ * The one rollback path every failure funnels through: restore the snapshot,
288
+ * reinstall to put `node_modules` back in sync with the restored
289
+ * `package.json`/lockfile, and report exactly which step failed and what it
290
+ * said — with an explicit statement that the project was restored, so a
291
+ * partner (or an agent) never has to wonder whether they are looking at a
292
+ * half-upgraded tree.
293
+ */
294
+ async function failAndRollback(ctx, snapshot, pm, projectDir, runInstall, failure) {
295
+ ctx.warn(`ekanos: the "${failure.step}" step failed — rolling back package.json ` +
296
+ 'and the lockfile, then reinstalling to restore node_modules…');
297
+ snapshot.restore();
298
+ // The SAME injectable installer seam used for the forward install — a test
299
+ // that mocks it never shells out for the rollback path either.
300
+ const restore = await runInstall(['install'], pm.bin, projectDir);
301
+ return ctx.failWith({
302
+ code: `upgrade_${failure.step}_failed`,
303
+ message: failure.message,
304
+ hint: `The project HAS BEEN RESTORED to its pre-upgrade state (package.json, ` +
305
+ `lockfile and node_modules all rolled back${restore.code === 0
306
+ ? ''
307
+ : ' — though the restore install itself also failed with code ' +
308
+ `${restore.code}; run "${pm.bin} install" by hand to finish ` +
309
+ 'repairing node_modules'}). Read the output in data.output, fix the underlying issue, and ` +
310
+ 're-run "ekanos upgrade".',
311
+ exitCode: failure.exitCode,
312
+ data: { step: failure.step, output: failure.output },
313
+ });
314
+ }
315
+ async function defaultRunInstall(argv, bin, cwd) {
316
+ return new Promise((resolve, reject) => {
317
+ var _a, _b;
318
+ const child = spawn(bin, argv, { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
319
+ let output = '';
320
+ (_a = child.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (chunk) => {
321
+ output += chunk.toString();
322
+ });
323
+ (_b = child.stderr) === null || _b === void 0 ? void 0 : _b.on('data', (chunk) => {
324
+ output += chunk.toString();
325
+ });
326
+ child.on('error', reject);
327
+ child.on('close', (code) => resolve({ code: code !== null && code !== void 0 ? code : 1, output }));
328
+ });
329
+ }
330
+ async function defaultRunValidateStep(projectDir) {
331
+ const loaded = loadProject(projectDir);
332
+ const findings = await collectAllFindings(loaded);
333
+ const errorCount = findings.filter((f) => f.severity === 'error').length;
334
+ return { ok: errorCount === 0, findings };
335
+ }
336
+ async function defaultRunTestStep(projectDir, pm) {
337
+ return new Promise((resolve, reject) => {
338
+ var _a, _b;
339
+ const child = spawn(pm.bin, ['run', 'test'], {
340
+ cwd: projectDir,
341
+ stdio: ['ignore', 'pipe', 'pipe'],
342
+ });
343
+ let output = '';
344
+ (_a = child.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (chunk) => {
345
+ output += chunk.toString();
346
+ });
347
+ (_b = child.stderr) === null || _b === void 0 ? void 0 : _b.on('data', (chunk) => {
348
+ output += chunk.toString();
349
+ });
350
+ child.on('error', reject);
351
+ child.on('close', (code) => resolve({ code: code !== null && code !== void 0 ? code : 1, output }));
352
+ });
353
+ }
354
+ /**
355
+ * Read every `AGENT_CONTEXT_FILES` template source out of the project's
356
+ * CURRENT (pre-upgrade) local `@ekanos/cli` install, before anything installs
357
+ * over it. Missing entirely (the package isn't a resolvable local dependency)
358
+ * just means every file below is treated conservatively — see
359
+ * `regenerateAgentContextFiles`.
360
+ */
361
+ function captureAgentTemplates(projectDir) {
362
+ const templatesDir = path.join(projectDir, 'node_modules', '@ekanos', 'cli', 'templates');
363
+ const captured = new Map();
364
+ for (const { template } of AGENT_CONTEXT_FILES) {
365
+ const templatePath = path.join(templatesDir, template);
366
+ try {
367
+ captured.set(template, fs.readFileSync(templatePath, 'utf8'));
368
+ }
369
+ catch (_a) {
370
+ // Not resolvable — left out of the map, handled as "can't verify" below.
371
+ }
372
+ }
373
+ return captured;
374
+ }
375
+ /**
376
+ * Create the docs-grounding artifacts a pre-0.1.7 project is missing, from
377
+ * the NEWLY installed CLI's bundled templates: `docs/api-notes.md`,
378
+ * `.mcp.json`, and any bundled skill trees. Strictly create-if-missing —
379
+ * an existing file is the partner's (api-notes is agent-authored, .mcp.json
380
+ * may carry local servers) and is never touched, force or not. Writes go
381
+ * through safe-fs like init's own scaffold pass.
382
+ */
383
+ function scaffoldMissingDocsArtifacts(params) {
384
+ const vars = { SLUG: params.slug, DISPLAY_NAME: toDisplayName(params.slug) };
385
+ const created = [];
386
+ const needsAttention = [];
387
+ // This runs AFTER snapshot.dispose(): the toolchain upgrade has already
388
+ // succeeded and there is no rollback left to protect. A refused write here
389
+ // (a symlinked `docs/` or `.claude` ancestor — safe-fs throws rather than
390
+ // follow it) must therefore be REPORTED, never thrown — the same stance
391
+ // `regenerateAgentContextFiles` takes, for the same reason: a hard failure
392
+ // now would misreport a successful upgrade.
393
+ for (const { template, dest } of DOCS_GROUNDING_FILES) {
394
+ const destPath = path.join(params.projectDir, dest);
395
+ const kind = classifyPath(destPath);
396
+ // A real file or directory is the partner's — skipping it quietly IS the
397
+ // contract. A symlink (or fifo, socket, …) is not "already there": it is
398
+ // a path we refuse to write through, and silence would hide that — same
399
+ // reporting stance as regenerateAgentContextFiles.
400
+ if (kind === 'file' || kind === 'directory')
401
+ continue;
402
+ if (kind !== 'missing') {
403
+ needsAttention.push(dest);
404
+ continue;
405
+ }
406
+ const templatePath = path.join(params.newTemplatesDir, template);
407
+ if (!fs.existsSync(templatePath))
408
+ continue; // older target toolchain
409
+ try {
410
+ writeFileNoFollow(params.projectDir, destPath, renderTemplate(fs.readFileSync(templatePath, 'utf8'), vars));
411
+ created.push(dest);
412
+ }
413
+ catch (_a) {
414
+ needsAttention.push(dest);
415
+ }
416
+ }
417
+ try {
418
+ // 'collect': a refused file is recorded and the walk continues, so files
419
+ // that DID land — in this skill tree or an earlier one — stay in
420
+ // `created` instead of vanishing when a later write refuses.
421
+ const skills = copyBundledSkillTrees(params.projectDir, false, path.join(params.newTemplatesDir, 'skills'), 'collect');
422
+ created.push(...skills.created);
423
+ needsAttention.push(...skills.refused);
424
+ }
425
+ catch (_b) {
426
+ // Last resort (an unreadable templates dir, not a refused write — those
427
+ // are collected above).
428
+ needsAttention.push('.claude/skills');
429
+ }
430
+ return { created, needsAttention };
431
+ }
432
+ /**
433
+ * Regenerate `AGENTS.md`, `CLAUDE.md` and the Claude Code skill from the
434
+ * NEWLY installed CLI's bundled templates — using the exact byte-comparison
435
+ * discipline `harness-scaffold.ts` applies to the dev harness shell: a file
436
+ * whose current bytes match what the project's PRE-upgrade local CLI would
437
+ * have rendered (captured in `oldTemplates`, before install ran) is
438
+ * untouched by the partner and safe to regenerate; anything else has been
439
+ * edited and is left alone, reported so a human can merge it by hand. A
440
+ * stale AGENTS.md teaches an agent yesterday's contract — that is as harmful
441
+ * as a stale binary, which is why this runs on every successful upgrade
442
+ * rather than only on `init`.
443
+ */
444
+ function regenerateAgentContextFiles(params) {
445
+ const vars = { SLUG: params.slug, DISPLAY_NAME: toDisplayName(params.slug) };
446
+ const updated = [];
447
+ const unchanged = [];
448
+ const needsAttention = [];
449
+ for (const { template, dest } of AGENT_CONTEXT_FILES) {
450
+ const destPath = path.join(params.projectDir, dest);
451
+ const kind = classifyPath(destPath);
452
+ if (kind === 'missing')
453
+ continue; // never scaffolded — nothing to upgrade
454
+ if (kind !== 'file') {
455
+ // A symlink (or anything else odd) is not ours to write through — same
456
+ // stance as init's scaffold pass, but reported instead of thrown: the
457
+ // upgrade itself already succeeded, so surface it for a human.
458
+ needsAttention.push(dest);
459
+ continue;
460
+ }
461
+ const current = fs.readFileSync(destPath, 'utf8');
462
+ const oldSource = params.oldTemplates.get(template);
463
+ const oldRendered = oldSource === undefined ? null : renderTemplate(oldSource, vars);
464
+ if (oldRendered === null || current !== oldRendered) {
465
+ needsAttention.push(dest);
466
+ continue;
467
+ }
468
+ const newTemplatePath = path.join(params.newTemplatesDir, template);
469
+ if (!fs.existsSync(newTemplatePath))
470
+ continue;
471
+ const newRendered = renderTemplate(fs.readFileSync(newTemplatePath, 'utf8'), vars);
472
+ if (newRendered === current) {
473
+ unchanged.push(dest);
474
+ continue;
475
+ }
476
+ try {
477
+ writeFileNoFollow(params.projectDir, destPath, newRendered);
478
+ updated.push(dest);
479
+ }
480
+ catch (_a) {
481
+ // A symlinked ANCESTOR (.claude centralized across projects) passes the
482
+ // leaf classifyPath check above but is refused by the guarded write —
483
+ // and this runs post-rollback, so a throw here would misreport a
484
+ // successful toolchain upgrade as failed. Report it instead, like every
485
+ // other post-rollback write path in this file.
486
+ needsAttention.push(dest);
487
+ }
488
+ }
489
+ return { updated, unchanged, needsAttention };
490
+ }
491
+ function checkSummary(source, deltas) {
492
+ var _a;
493
+ const lines = [`upgrade --check: target resolved from ${source}.`];
494
+ for (const [key, delta] of Object.entries(deltas)) {
495
+ lines.push(` ${key}: ${(_a = delta.current) !== null && _a !== void 0 ? _a : '(not installed)'} → ${delta.target}` +
496
+ (delta.changed ? '' : ' (already current)'));
497
+ }
498
+ return lines.join('\n');
499
+ }
500
+ function planSummary(source, deltas) {
501
+ const changing = Object.entries(deltas)
502
+ .filter(([, d]) => d.changed)
503
+ .map(([key, d]) => { var _a; return `${key} ${(_a = d.current) !== null && _a !== void 0 ? _a : '(not installed)'} → ${d.target}`; });
504
+ return (`Upgrading (target resolved from ${source}):\n` +
505
+ changing.map((line) => ` ${line}`).join('\n'));
506
+ }
507
+ function successSummary(installedKeys, skippedKeys, deltas, regenerated, scaffolded) {
508
+ const installed = installedKeys
509
+ .map((key) => `${key} → ${deltas[key].target}`)
510
+ .join(', ');
511
+ const parts = [
512
+ installed.length > 0
513
+ ? `upgrade: OK — installed ${installed}.`
514
+ : 'upgrade: OK — nothing was a direct dependency of this project, so nothing was installed here.',
515
+ ];
516
+ if (skippedKeys.length > 0) {
517
+ // These changed in the resolved target but are transitive (or, for
518
+ // "cli", the invoked binary was global with no local devDependency) —
519
+ // never touched by the install step above.
520
+ const skipped = skippedKeys
521
+ .map((key) => `${key} → ${deltas[key].target}`)
522
+ .join(', ');
523
+ parts.push(`Not installed directly (transitive or not a local dependency): ${skipped}.`);
524
+ }
525
+ if (regenerated.updated.length > 0) {
526
+ parts.push(`regenerated ${regenerated.updated.join(', ')}.`);
527
+ }
528
+ if (scaffolded.created.length > 0) {
529
+ parts.push(`created ${scaffolded.created.join(', ')} (new in this toolchain — see ` +
530
+ `AGENTS.md's "API documentation" section).`);
531
+ }
532
+ if (scaffolded.needsAttention.length > 0) {
533
+ parts.push(`could not scaffold ${scaffolded.needsAttention.join(', ')} (a path ` +
534
+ 'component is a symlink or unwritable) — the toolchain upgrade itself ' +
535
+ 'succeeded. Fix the path, then run "ekanos init --slug <slug> --dir ." ' +
536
+ 'WITHOUT --force: init is per-file idempotent and creates only what ' +
537
+ 'is missing. Never use --force here — it overwrites every scaffolded ' +
538
+ 'file, including src/integration.ts.');
539
+ }
540
+ if (regenerated.needsAttention.length > 0) {
541
+ parts.push(`${regenerated.needsAttention.join(', ')} were locally modified — ` +
542
+ 'left untouched, merge the new template by hand.');
543
+ }
544
+ return parts.join(' ');
545
+ }
546
+ //# sourceMappingURL=upgrade.js.map