@ekanos/cli 0.1.6 → 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.
- package/README.md +2 -2
- package/dist/commands/init.d.ts +36 -0
- package/dist/commands/init.js +126 -10
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/upgrade.js +108 -7
- package/dist/commands/upgrade.js.map +1 -1
- package/dist/copy-tree.d.ts +48 -0
- package/dist/copy-tree.js +55 -0
- package/dist/copy-tree.js.map +1 -0
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/parse-argv.d.ts +7 -1
- package/dist/parse-argv.js +10 -2
- package/dist/parse-argv.js.map +1 -1
- package/dist/project-checks.d.ts +9 -0
- package/dist/project-checks.js +29 -0
- package/dist/project-checks.js.map +1 -1
- package/dist/project.d.ts +23 -0
- package/dist/project.js +66 -1
- package/dist/project.js.map +1 -1
- package/dist/publish-api.d.ts +1 -1
- package/dist/publish-api.js +2 -2
- package/dist/publish-api.js.map +1 -1
- package/dist/safe-fs.d.ts +1 -1
- package/dist/safe-fs.js.map +1 -1
- package/dist/sources-api.d.ts +1 -1
- package/dist/sources-api.js.map +1 -1
- package/dist/validate-findings.js +2 -1
- package/dist/validate-findings.js.map +1 -1
- package/package.json +1 -1
- package/templates/AGENTS.md.tmpl +57 -0
- package/templates/claude-skill.md.tmpl +7 -0
- package/templates/docs/api-notes.md.tmpl +21 -0
- package/templates/mcp.json.tmpl +8 -0
- package/templates/skills/build-integration/SKILL.md +178 -0
- package/templates/skills/build-integration/references/api-docs-research.md +93 -0
- package/templates/skills/build-integration/references/api-wiring.md +94 -0
- package/templates/skills/build-integration/references/widget-design.md +132 -0
- package/templates/skills/polish-widgets/SKILL.md +139 -0
- package/templates/skills/polish-widgets/references/patterns.md +269 -0
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ 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
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). |
|
|
@@ -454,7 +454,7 @@ ekanos publish # verified before anything is packed or sent
|
|
|
454
454
|
```
|
|
455
455
|
|
|
456
456
|
`ekanos sources` reads `GET /api/partner/sources` and lists every source you
|
|
457
|
-
hold a `
|
|
457
|
+
hold a `developer` or `source_admin` seat on, with a `*` marking the slug currently saved in
|
|
458
458
|
this project's `ekanos.json`. `ekanos use <slug>` persists that field, but only
|
|
459
459
|
after confirming the slug names a source you actually hold a seat on — a typo
|
|
460
460
|
or a stale slug is refused (`forbidden`) with your real seat list in the hint,
|
package/dist/commands/init.d.ts
CHANGED
|
@@ -25,6 +25,13 @@ 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
|
}
|
|
29
36
|
/**
|
|
30
37
|
* The files init writes: template basename → destination path.
|
|
@@ -51,6 +58,20 @@ export declare const AGENT_CONTEXT_FILES: {
|
|
|
51
58
|
template: string;
|
|
52
59
|
dest: string;
|
|
53
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
|
+
}[];
|
|
54
75
|
/**
|
|
55
76
|
* `init` — instantiate a project from the bundled template: ekanos.json, a
|
|
56
77
|
* working minimal defineIntegration, a test, a vitest config (with the
|
|
@@ -60,3 +81,18 @@ export declare const AGENT_CONTEXT_FILES: {
|
|
|
60
81
|
* with a usage error naming the flag.
|
|
61
82
|
*/
|
|
62
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
|
+
};
|
package/dist/commands/init.js
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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.
|
|
@@ -31,6 +34,24 @@ export const AGENT_CONTEXT_FILES = [
|
|
|
31
34
|
{ template: 'CLAUDE.md.tmpl', dest: 'CLAUDE.md' },
|
|
32
35
|
{ template: 'claude-skill.md.tmpl', dest: '.claude/skills/ekanos/SKILL.md' },
|
|
33
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
|
+
];
|
|
34
55
|
const FILES = [
|
|
35
56
|
{ template: 'ekanos.json.tmpl', dest: 'ekanos.json' },
|
|
36
57
|
{ template: 'gitignore.tmpl', dest: '.gitignore' },
|
|
@@ -43,6 +64,7 @@ const FILES = [
|
|
|
43
64
|
template: 'src/integration.test.ts.tmpl',
|
|
44
65
|
dest: 'src/integration.test.ts',
|
|
45
66
|
},
|
|
67
|
+
...DOCS_GROUNDING_FILES,
|
|
46
68
|
...AGENT_CONTEXT_FILES,
|
|
47
69
|
];
|
|
48
70
|
/**
|
|
@@ -54,6 +76,7 @@ const FILES = [
|
|
|
54
76
|
* with a usage error naming the flag.
|
|
55
77
|
*/
|
|
56
78
|
export function runInit(ctx, args) {
|
|
79
|
+
var _a;
|
|
57
80
|
const slug = args.slug;
|
|
58
81
|
if (!slug) {
|
|
59
82
|
throw usageError('init requires a slug.', 'Pass it explicitly: "ekanos init --slug <your-integration> --yes". ' +
|
|
@@ -63,6 +86,18 @@ export function runInit(ctx, args) {
|
|
|
63
86
|
throw usageError(`Invalid slug "${slug}".`, 'Use kebab-case: lowercase [a-z0-9] segments joined by single hyphens, ' +
|
|
64
87
|
'e.g. "acme-crm".');
|
|
65
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
|
+
}
|
|
66
101
|
// Default to `./<slug>`, not the cwd: scaffolding six files into whatever
|
|
67
102
|
// directory the partner happened to be sitting in is how a real project got
|
|
68
103
|
// polluted. `--dir .` remains the explicit way to scaffold in place.
|
|
@@ -72,9 +107,34 @@ export function runInit(ctx, args) {
|
|
|
72
107
|
const vars = { SLUG: slug, DISPLAY_NAME: toDisplayName(slug) };
|
|
73
108
|
const created = [];
|
|
74
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 });
|
|
75
132
|
for (const { template, dest } of FILES) {
|
|
76
133
|
const destPath = path.join(targetDir, dest);
|
|
77
|
-
|
|
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) {
|
|
78
138
|
skipped.push({
|
|
79
139
|
file: dest,
|
|
80
140
|
reason: 'it already exists',
|
|
@@ -83,10 +143,14 @@ export function runInit(ctx, args) {
|
|
|
83
143
|
});
|
|
84
144
|
continue;
|
|
85
145
|
}
|
|
86
|
-
|
|
87
|
-
|
|
146
|
+
writeFileNoFollow(targetDir, destPath, dest === 'ekanos.json'
|
|
147
|
+
? renderEkanosConfig(vars, docs)
|
|
148
|
+
: readTemplate(template, vars));
|
|
88
149
|
created.push(dest);
|
|
89
150
|
}
|
|
151
|
+
const skills = copyBundledSkillTrees(targetDir, args.force);
|
|
152
|
+
created.push(...skills.created);
|
|
153
|
+
skipped.push(...skills.skipped);
|
|
90
154
|
const manifest = writeManifest(targetDir, vars, args.force);
|
|
91
155
|
if (manifest.kind === 'created')
|
|
92
156
|
created.push('package.json');
|
|
@@ -128,6 +192,56 @@ export function runInit(ctx, args) {
|
|
|
128
192
|
printBanner(ctx, 'full');
|
|
129
193
|
return ctx.succeed(data, summary);
|
|
130
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
|
+
}
|
|
131
245
|
/**
|
|
132
246
|
* Write or MERGE package.json.
|
|
133
247
|
*
|
|
@@ -140,15 +254,17 @@ export function runInit(ctx, args) {
|
|
|
140
254
|
function writeManifest(targetDir, vars, force) {
|
|
141
255
|
const destPath = path.join(targetDir, 'package.json');
|
|
142
256
|
const rendered = readTemplate('package.json.tmpl', vars);
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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);
|
|
146
262
|
return { kind: 'created', added: [], kept: [], notes: [] };
|
|
147
263
|
}
|
|
148
264
|
const template = JSON.parse(rendered);
|
|
149
265
|
let existing;
|
|
150
266
|
try {
|
|
151
|
-
existing = JSON.parse(
|
|
267
|
+
existing = JSON.parse(existingRaw);
|
|
152
268
|
}
|
|
153
269
|
catch (error) {
|
|
154
270
|
// Unparseable: do not guess, and do not clobber. Name the fix.
|
|
@@ -175,7 +291,7 @@ function writeManifest(targetDir, vars, force) {
|
|
|
175
291
|
notes: result.notes,
|
|
176
292
|
};
|
|
177
293
|
}
|
|
178
|
-
|
|
294
|
+
writeFileNoFollow(targetDir, destPath, serializeManifest(result.manifest));
|
|
179
295
|
return {
|
|
180
296
|
kind: 'merged',
|
|
181
297
|
added: result.added,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.js","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { printBanner } from '../banner';\nimport type { CliContext } from '../context';\nimport { usageError } from '../errors';\nimport type { ExitCode } from '../exit-codes';\nimport {\n type Manifest,\n type ManifestNote,\n mergeManifest,\n serializeManifest,\n} from '../merge-manifest';\nimport { readTemplate, toDisplayName } from '../templates';\n\n/**\n * A file init did not write, and what the partner should do about it.\n *\n * Every ERROR path in this CLI carries an imperative hint; a skip did not, and\n * that asymmetry was the bug — `skipped package.json (already exists)` hid the\n * loss of eleven dependencies and the project's module type.\n */\nexport interface SkippedFile {\n file: string;\n reason: string;\n hint: string;\n}\n\nexport interface InitArgs {\n slug?: string;\n /**\n * Explicit target directory. When omitted, init scaffolds into `./<slug>`\n * under `cwd` — never into `cwd` itself. A real partner ran `init` in a\n * populated directory and got six files sprayed into it; `--dir .` is the\n * explicit opt-in for that.\n */\n dir?: string;\n cwd: string;\n force: boolean;\n /** Accepted for non-interactive ergonomics; init never prompts regardless. */\n yes: boolean;\n}\n\nconst SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;\n\n/**\n * The files init writes: template basename → destination path.\n *\n * `package.json` is handled separately (see `writeManifest`) because it is the\n * one file a partner plausibly already has — `npm init -y` before `ekanos\n * init` is the ordinary way to start — and skipping it silently costs them\n * every dependency and script.\n */\n/**\n * The agent-context files. The SDK's day-1 users are coding agents, so the\n * scaffold ships the authoring contract alongside the code: AGENTS.md is the\n * contract, CLAUDE.md points at it, and the Claude Code skill teaches the\n * iterate-validate-publish loop. Version-matched to the CLI because they live\n * in its templates.\n *\n * Exported so `ekanos upgrade` can regenerate exactly this set from the\n * NEWLY installed CLI's templates after a version bump — a stale AGENTS.md\n * teaches an agent yesterday's contract, which is as harmful as a stale\n * binary. One list, so `init` and `upgrade` can never disagree on what \"the\n * agent-context files\" even are.\n */\nexport const AGENT_CONTEXT_FILES: { template: string; dest: string }[] = [\n { template: 'AGENTS.md.tmpl', dest: 'AGENTS.md' },\n { template: 'CLAUDE.md.tmpl', dest: 'CLAUDE.md' },\n { template: 'claude-skill.md.tmpl', dest: '.claude/skills/ekanos/SKILL.md' },\n];\n\nconst FILES: { template: string; dest: string }[] = [\n { template: 'ekanos.json.tmpl', dest: 'ekanos.json' },\n { template: 'gitignore.tmpl', dest: '.gitignore' },\n // Without this, the `typecheck` script below prints tsc's help and exits 1 —\n // and, worse, no type error in the scaffold ever surfaces.\n { template: 'tsconfig.json.tmpl', dest: 'tsconfig.json' },\n { template: 'vitest.config.ts.tmpl', dest: 'vitest.config.ts' },\n { template: 'src/integration.ts.tmpl', dest: 'src/integration.ts' },\n {\n template: 'src/integration.test.ts.tmpl',\n dest: 'src/integration.test.ts',\n },\n ...AGENT_CONTEXT_FILES,\n];\n\n/**\n * `init` — instantiate a project from the bundled template: ekanos.json, a\n * working minimal defineIntegration, a test, a vitest config (with the\n * required SDK inline block), a .gitignore, and a package.json with real\n * published deps. Idempotent: an existing file is left untouched (reported as\n * skipped) unless --force is passed. Never prompts — a missing --slug fails\n * with a usage error naming the flag.\n */\nexport function runInit(ctx: CliContext, args: InitArgs): ExitCode {\n const slug = args.slug;\n if (!slug) {\n throw usageError(\n 'init requires a slug.',\n 'Pass it explicitly: \"ekanos init --slug <your-integration> --yes\". ' +\n 'The CLI never prompts, so the slug must be on the command line.',\n );\n }\n if (!SLUG_PATTERN.test(slug)) {\n throw usageError(\n `Invalid slug \"${slug}\".`,\n 'Use kebab-case: lowercase [a-z0-9] segments joined by single hyphens, ' +\n 'e.g. \"acme-crm\".',\n );\n }\n\n // Default to `./<slug>`, not the cwd: scaffolding six files into whatever\n // directory the partner happened to be sitting in is how a real project got\n // polluted. `--dir .` remains the explicit way to scaffold in place.\n const targetDir =\n args.dir !== undefined\n ? path.resolve(args.cwd, args.dir)\n : path.resolve(args.cwd, slug);\n const vars = { SLUG: slug, DISPLAY_NAME: toDisplayName(slug) };\n\n const created: string[] = [];\n const skipped: SkippedFile[] = [];\n\n for (const { template, dest } of FILES) {\n const destPath = path.join(targetDir, dest);\n if (fs.existsSync(destPath) && !args.force) {\n skipped.push({\n file: dest,\n reason: 'it already exists',\n hint:\n `Compare it against the scaffold and merge anything you are ` +\n `missing, or re-run with --force to overwrite ${dest}.`,\n });\n continue;\n }\n\n fs.mkdirSync(path.dirname(destPath), { recursive: true });\n fs.writeFileSync(destPath, readTemplate(template, vars));\n created.push(dest);\n }\n\n const manifest = writeManifest(targetDir, vars, args.force);\n if (manifest.kind === 'created') created.push('package.json');\n\n for (const file of created) ctx.log(` created ${file}`);\n if (manifest.kind === 'merged') {\n ctx.log(\n ` merged package.json (added ${manifest.added.length} field${\n manifest.added.length === 1 ? '' : 's'\n })`,\n );\n }\n if (manifest.kind === 'unchanged') {\n ctx.log(' package.json already had everything the scaffold adds');\n }\n for (const skip of skipped) {\n ctx.log(` skipped ${skip.file} — ${skip.reason}. ${skip.hint}`);\n }\n for (const note of manifest.notes) {\n ctx.warn(` ${note.message} ${note.hint}`);\n }\n\n const data = {\n slug,\n dir: targetDir,\n created,\n skipped,\n manifest: {\n action: manifest.kind,\n added: manifest.added,\n kept: manifest.kept,\n notes: manifest.notes,\n },\n };\n\n const summary = summarize({\n slug,\n dir: targetDir,\n created,\n skipped,\n manifestAction: manifest.kind,\n manifestAdded: manifest.added.length,\n noteCount: manifest.notes.length,\n });\n\n printBanner(ctx, 'full');\n\n return ctx.succeed(data, summary);\n}\n\ntype ManifestOutcome = {\n kind: 'created' | 'merged' | 'unchanged';\n added: string[];\n kept: string[];\n notes: ManifestNote[];\n};\n\n/**\n * Write or MERGE package.json.\n *\n * A partner who ran `npm init -y` first had a manifest with none of the\n * scaffold's dependencies and `\"type\": \"commonjs\"` under ESM sources, and the\n * old behaviour reported that as one word: \"skipped\". Merging is additive — a\n * version or script they already set is theirs and is never touched — so the\n * ordinary `npm init -y` path now lands somewhere that installs and runs.\n */\nfunction writeManifest(\n targetDir: string,\n vars: Readonly<Record<string, string>>,\n force: boolean,\n): ManifestOutcome {\n const destPath = path.join(targetDir, 'package.json');\n const rendered = readTemplate('package.json.tmpl', vars);\n\n if (!fs.existsSync(destPath) || force) {\n fs.mkdirSync(path.dirname(destPath), { recursive: true });\n fs.writeFileSync(destPath, rendered);\n return { kind: 'created', added: [], kept: [], notes: [] };\n }\n\n const template = JSON.parse(rendered) as Manifest;\n\n let existing: Manifest;\n try {\n existing = JSON.parse(fs.readFileSync(destPath, 'utf8')) as Manifest;\n } catch (error) {\n // Unparseable: do not guess, and do not clobber. Name the fix.\n return {\n kind: 'unchanged',\n added: [],\n kept: [],\n notes: [\n {\n field: '(root)',\n message: `package.json is not valid JSON: ${\n error instanceof Error ? error.message : String(error)\n }`,\n hint:\n 'Fix the JSON syntax, then re-run \"ekanos init\" to have the ' +\n \"scaffold's dependencies and scripts merged in.\",\n },\n ],\n };\n }\n\n const result = mergeManifest(existing, template);\n if (!result.changed) {\n return {\n kind: 'unchanged',\n added: [],\n kept: result.kept,\n notes: result.notes,\n };\n }\n\n fs.writeFileSync(destPath, serializeManifest(result.manifest));\n return {\n kind: 'merged',\n added: result.added,\n kept: result.kept,\n notes: result.notes,\n };\n}\n\nfunction summarize(params: {\n slug: string;\n dir: string;\n created: string[];\n skipped: SkippedFile[];\n manifestAction: ManifestOutcome['kind'];\n manifestAdded: number;\n noteCount: number;\n}): string {\n const parts: string[] = [];\n if (params.created.length > 0) {\n parts.push(\n `${params.created.length} file${params.created.length === 1 ? '' : 's'} written`,\n );\n }\n if (params.manifestAction === 'merged') {\n parts.push(`${params.manifestAdded} package.json field(s) merged`);\n }\n if (params.skipped.length > 0) {\n parts.push(`${params.skipped.length} skipped`);\n }\n if (params.noteCount > 0) {\n parts.push(`${params.noteCount} thing(s) need your attention`);\n }\n\n if (parts.length === 0) {\n return `init: nothing to do — \"${params.slug}\" is already scaffolded in ${params.dir}.`;\n }\n return `init: scaffolded \"${params.slug}\" into ${params.dir} (${parts.join(', ')}).`;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"init.js","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { printBanner } from '../banner';\nimport type { CliContext } from '../context';\nimport { copyTreeVerbatim } from '../copy-tree';\nimport { invalidStateError, usageError } from '../errors';\nimport type { ExitCode } from '../exit-codes';\nimport {\n type Manifest,\n type ManifestNote,\n mergeManifest,\n serializeManifest,\n} from '../merge-manifest';\nimport {\n type EkanosProject,\n docEntryProblem,\n serializeProject,\n} from '../project';\nimport { classifyPath, readFileNoFollow, writeFileNoFollow } from '../safe-fs';\nimport { readTemplate, templatesDir, toDisplayName } from '../templates';\n\n/**\n * A file init did not write, and what the partner should do about it.\n *\n * Every ERROR path in this CLI carries an imperative hint; a skip did not, and\n * that asymmetry was the bug — `skipped package.json (already exists)` hid the\n * loss of eleven dependencies and the project's module type.\n */\nexport interface SkippedFile {\n file: string;\n reason: string;\n hint: string;\n}\n\nexport interface InitArgs {\n slug?: string;\n /**\n * Explicit target directory. When omitted, init scaffolds into `./<slug>`\n * under `cwd` — never into `cwd` itself. A real partner ran `init` in a\n * populated directory and got six files sprayed into it; `--dir .` is the\n * explicit opt-in for that.\n */\n dir?: string;\n cwd: string;\n force: boolean;\n /** Accepted for non-interactive ergonomics; init never prompts regardless. */\n yes: boolean;\n /**\n * Repeatable `--docs` entries — http(s) URLs or project-relative paths to\n * the target API's documentation, written into the scaffolded ekanos.json\n * as `docs`. Absent flag means the field is simply omitted; init never\n * prompts for it.\n */\n docs?: string[];\n}\n\nconst SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;\n\n/**\n * The files init writes: template basename → destination path.\n *\n * `package.json` is handled separately (see `writeManifest`) because it is the\n * one file a partner plausibly already has — `npm init -y` before `ekanos\n * init` is the ordinary way to start — and skipping it silently costs them\n * every dependency and script.\n */\n/**\n * The agent-context files. The SDK's day-1 users are coding agents, so the\n * scaffold ships the authoring contract alongside the code: AGENTS.md is the\n * contract, CLAUDE.md points at it, and the Claude Code skill teaches the\n * iterate-validate-publish loop. Version-matched to the CLI because they live\n * in its templates.\n *\n * Exported so `ekanos upgrade` can regenerate exactly this set from the\n * NEWLY installed CLI's templates after a version bump — a stale AGENTS.md\n * teaches an agent yesterday's contract, which is as harmful as a stale\n * binary. One list, so `init` and `upgrade` can never disagree on what \"the\n * agent-context files\" even are.\n */\nexport const AGENT_CONTEXT_FILES: { template: string; dest: string }[] = [\n { template: 'AGENTS.md.tmpl', dest: 'AGENTS.md' },\n { template: 'CLAUDE.md.tmpl', dest: 'CLAUDE.md' },\n { template: 'claude-skill.md.tmpl', dest: '.claude/skills/ekanos/SKILL.md' },\n];\n\n/**\n * The docs-grounding artifacts, exported for the same reason as\n * `AGENT_CONTEXT_FILES`: `upgrade` must create these when they are missing\n * (an older project upgrading into a toolchain whose AGENTS.md and skills\n * now reference them), and one list keeps `init` and `upgrade` from ever\n * disagreeing on what they are. Unlike the agent-context files these are\n * NEVER regenerated — `docs/api-notes.md` is agent/partner-authored after\n * the scaffold and `.mcp.json` may carry local additions — only created\n * when absent.\n */\nexport const DOCS_GROUNDING_FILES: { template: string; dest: string }[] = [\n // The agent-authored snapshot of the target API — see the template itself\n // and AGENTS.md's \"API documentation\" section for the convention.\n { template: 'docs/api-notes.md.tmpl', dest: 'docs/api-notes.md' },\n // Per-project MCP config so Claude Code picks up Context7 (the docs\n // fallback in the resolution ladder) without any global setup.\n { template: 'mcp.json.tmpl', dest: '.mcp.json' },\n];\n\nconst FILES: { template: string; dest: string }[] = [\n { template: 'ekanos.json.tmpl', dest: 'ekanos.json' },\n { template: 'gitignore.tmpl', dest: '.gitignore' },\n // Without this, the `typecheck` script below prints tsc's help and exits 1 —\n // and, worse, no type error in the scaffold ever surfaces.\n { template: 'tsconfig.json.tmpl', dest: 'tsconfig.json' },\n { template: 'vitest.config.ts.tmpl', dest: 'vitest.config.ts' },\n { template: 'src/integration.ts.tmpl', dest: 'src/integration.ts' },\n {\n template: 'src/integration.test.ts.tmpl',\n dest: 'src/integration.test.ts',\n },\n ...DOCS_GROUNDING_FILES,\n ...AGENT_CONTEXT_FILES,\n];\n\n/**\n * `init` — instantiate a project from the bundled template: ekanos.json, a\n * working minimal defineIntegration, a test, a vitest config (with the\n * required SDK inline block), a .gitignore, and a package.json with real\n * published deps. Idempotent: an existing file is left untouched (reported as\n * skipped) unless --force is passed. Never prompts — a missing --slug fails\n * with a usage error naming the flag.\n */\nexport function runInit(ctx: CliContext, args: InitArgs): ExitCode {\n const slug = args.slug;\n if (!slug) {\n throw usageError(\n 'init requires a slug.',\n 'Pass it explicitly: \"ekanos init --slug <your-integration> --yes\". ' +\n 'The CLI never prompts, so the slug must be on the command line.',\n );\n }\n if (!SLUG_PATTERN.test(slug)) {\n throw usageError(\n `Invalid slug \"${slug}\".`,\n 'Use kebab-case: lowercase [a-z0-9] segments joined by single hyphens, ' +\n 'e.g. \"acme-crm\".',\n );\n }\n\n // Same rule the ekanos.json schema enforces, applied at the flag so a bad\n // entry fails HERE (usage, exit 2) instead of scaffolding a project whose\n // very first `ekanos validate` cannot even load its config.\n const docs = args.docs ?? [];\n for (const entry of docs) {\n const problem = docEntryProblem(entry);\n if (problem) {\n throw usageError(\n `Invalid --docs value: ${problem}`,\n 'Pass --docs an http(s) URL or a project-relative path, e.g. ' +\n '\"--docs https://developer.example.com/openapi.json\" or ' +\n '\"--docs docs/api.md\". Repeat the flag for multiple entries.',\n );\n }\n }\n\n // Default to `./<slug>`, not the cwd: scaffolding six files into whatever\n // directory the partner happened to be sitting in is how a real project got\n // polluted. `--dir .` remains the explicit way to scaffold in place.\n const targetDir =\n args.dir !== undefined\n ? path.resolve(args.cwd, args.dir)\n : path.resolve(args.cwd, slug);\n const vars = { SLUG: slug, DISPLAY_NAME: toDisplayName(slug) };\n\n const created: string[] = [];\n const skipped: SkippedFile[] = [];\n\n // The project root is the guard for every write below: creating it raw is\n // fine (it is the boundary, not inside it), but everything under it goes\n // through safe-fs so a planted symlink refuses loudly instead of teleporting\n // a write outside the project. See safe-fs.ts for the threat model. The one\n // case the guard cannot see is the boundary ITSELF being a symlink — the\n // containment math is lexical against `targetDir`, so a planted link there\n // would relocate every \"contained\" write wholesale. Refuse it. (A symlinked\n // ANCESTOR — macOS's /tmp, a linked home — stays fine: only the leaf is\n // checked.)\n let targetStats: fs.Stats | null = null;\n try {\n targetStats = fs.lstatSync(targetDir);\n } catch {\n targetStats = null; // does not exist yet — ours to create\n }\n if (targetStats?.isSymbolicLink()) {\n throw invalidStateError(\n `The target directory ${targetDir} is a symlink, so the scaffold's ` +\n 'write-containment guarantees cannot hold there.',\n 'Scaffold into the real directory (pass its resolved path to --dir), ' +\n 'or remove the symlink first.',\n );\n }\n fs.mkdirSync(targetDir, { recursive: true });\n\n for (const { template, dest } of FILES) {\n const destPath = path.join(targetDir, dest);\n const kind = classifyPath(destPath);\n // A symlink is never \"already exists\": skipping would keep the plant, and\n // writing through it is the attack — writeFileNoFollow refuses it.\n if (kind !== 'missing' && kind !== 'symlink' && !args.force) {\n skipped.push({\n file: dest,\n reason: 'it already exists',\n hint:\n `Compare it against the scaffold and merge anything you are ` +\n `missing, or re-run with --force to overwrite ${dest}.`,\n });\n continue;\n }\n\n writeFileNoFollow(\n targetDir,\n destPath,\n dest === 'ekanos.json'\n ? renderEkanosConfig(vars, docs)\n : readTemplate(template, vars),\n );\n created.push(dest);\n }\n\n const skills = copyBundledSkillTrees(targetDir, args.force);\n created.push(...skills.created);\n skipped.push(...skills.skipped);\n\n const manifest = writeManifest(targetDir, vars, args.force);\n if (manifest.kind === 'created') created.push('package.json');\n\n for (const file of created) ctx.log(` created ${file}`);\n if (manifest.kind === 'merged') {\n ctx.log(\n ` merged package.json (added ${manifest.added.length} field${\n manifest.added.length === 1 ? '' : 's'\n })`,\n );\n }\n if (manifest.kind === 'unchanged') {\n ctx.log(' package.json already had everything the scaffold adds');\n }\n for (const skip of skipped) {\n ctx.log(` skipped ${skip.file} — ${skip.reason}. ${skip.hint}`);\n }\n for (const note of manifest.notes) {\n ctx.warn(` ${note.message} ${note.hint}`);\n }\n\n const data = {\n slug,\n dir: targetDir,\n created,\n skipped,\n manifest: {\n action: manifest.kind,\n added: manifest.added,\n kept: manifest.kept,\n notes: manifest.notes,\n },\n };\n\n const summary = summarize({\n slug,\n dir: targetDir,\n created,\n skipped,\n manifestAction: manifest.kind,\n manifestAdded: manifest.added.length,\n noteCount: manifest.notes.length,\n });\n\n printBanner(ctx, 'full');\n\n return ctx.succeed(data, summary);\n}\n\n/**\n * The scaffolded ekanos.json: the template, plus the collected `--docs`\n * entries when there are any. Parse-then-serialize (rather than string\n * surgery) so the written file is exactly what `loadProject` will read back.\n */\nfunction renderEkanosConfig(\n vars: Readonly<Record<string, string>>,\n docs: string[],\n): string {\n const rendered = readTemplate('ekanos.json.tmpl', vars);\n if (docs.length === 0) return rendered;\n const project = JSON.parse(rendered) as EkanosProject;\n return serializeProject({ ...project, docs });\n}\n\n/**\n * Copy every bundled skill tree (`templates/skills/<name>/`) into the\n * scaffolded project at `.claude/skills/<name>/`, verbatim — see\n * `copyTreeVerbatim` for why these are bytes, never rendered templates. The\n * walk is generic: a skill directory added to the published templates ships\n * with no code change here. A CLI build with no `templates/skills/` at all\n * (today's) scaffolds nothing and reports nothing.\n *\n * `skillsRoot` is injectable for tests; real callers take the default.\n */\nexport function copyBundledSkillTrees(\n targetDir: string,\n force: boolean,\n skillsRoot = path.join(templatesDir(), 'skills'),\n onRefusal: 'throw' | 'collect' = 'throw',\n): { created: string[]; skipped: SkippedFile[]; refused: string[] } {\n if (!fs.existsSync(skillsRoot))\n return { created: [], skipped: [], refused: [] };\n\n const created: string[] = [];\n const skipped: SkippedFile[] = [];\n const refused: string[] = [];\n\n const skillDirs = fs\n .readdirSync(skillsRoot, { withFileTypes: true })\n .filter((entry) => entry.isDirectory())\n .map((entry) => entry.name)\n .sort();\n\n for (const name of skillDirs) {\n const destBase = path.join('.claude', 'skills', name);\n const result = copyTreeVerbatim(\n path.join(skillsRoot, name),\n path.join(targetDir, destBase),\n { force, root: targetDir, onRefusal },\n );\n created.push(...result.created.map((rel) => path.join(destBase, rel)));\n refused.push(...result.refused.map((rel) => path.join(destBase, rel)));\n skipped.push(\n ...result.skipped.map((rel) => {\n const file = path.join(destBase, rel);\n return {\n file,\n reason: 'it already exists',\n hint:\n `Compare it against the bundled skill and merge anything you ` +\n `are missing, or re-run with --force to overwrite ${file}.`,\n };\n }),\n );\n }\n\n return { created, skipped, refused };\n}\n\ntype ManifestOutcome = {\n kind: 'created' | 'merged' | 'unchanged';\n added: string[];\n kept: string[];\n notes: ManifestNote[];\n};\n\n/**\n * Write or MERGE package.json.\n *\n * A partner who ran `npm init -y` first had a manifest with none of the\n * scaffold's dependencies and `\"type\": \"commonjs\"` under ESM sources, and the\n * old behaviour reported that as one word: \"skipped\". Merging is additive — a\n * version or script they already set is theirs and is never touched — so the\n * ordinary `npm init -y` path now lands somewhere that installs and runs.\n */\nfunction writeManifest(\n targetDir: string,\n vars: Readonly<Record<string, string>>,\n force: boolean,\n): ManifestOutcome {\n const destPath = path.join(targetDir, 'package.json');\n const rendered = readTemplate('package.json.tmpl', vars);\n\n // readFileNoFollow refuses a symlinked package.json outright, so the write\n // below can never follow one either; null means genuinely absent.\n const existingRaw = readFileNoFollow(targetDir, destPath);\n\n if (existingRaw === null || force) {\n writeFileNoFollow(targetDir, destPath, rendered);\n return { kind: 'created', added: [], kept: [], notes: [] };\n }\n\n const template = JSON.parse(rendered) as Manifest;\n\n let existing: Manifest;\n try {\n existing = JSON.parse(existingRaw) as Manifest;\n } catch (error) {\n // Unparseable: do not guess, and do not clobber. Name the fix.\n return {\n kind: 'unchanged',\n added: [],\n kept: [],\n notes: [\n {\n field: '(root)',\n message: `package.json is not valid JSON: ${\n error instanceof Error ? error.message : String(error)\n }`,\n hint:\n 'Fix the JSON syntax, then re-run \"ekanos init\" to have the ' +\n \"scaffold's dependencies and scripts merged in.\",\n },\n ],\n };\n }\n\n const result = mergeManifest(existing, template);\n if (!result.changed) {\n return {\n kind: 'unchanged',\n added: [],\n kept: result.kept,\n notes: result.notes,\n };\n }\n\n writeFileNoFollow(targetDir, destPath, serializeManifest(result.manifest));\n return {\n kind: 'merged',\n added: result.added,\n kept: result.kept,\n notes: result.notes,\n };\n}\n\nfunction summarize(params: {\n slug: string;\n dir: string;\n created: string[];\n skipped: SkippedFile[];\n manifestAction: ManifestOutcome['kind'];\n manifestAdded: number;\n noteCount: number;\n}): string {\n const parts: string[] = [];\n if (params.created.length > 0) {\n parts.push(\n `${params.created.length} file${params.created.length === 1 ? '' : 's'} written`,\n );\n }\n if (params.manifestAction === 'merged') {\n parts.push(`${params.manifestAdded} package.json field(s) merged`);\n }\n if (params.skipped.length > 0) {\n parts.push(`${params.skipped.length} skipped`);\n }\n if (params.noteCount > 0) {\n parts.push(`${params.noteCount} thing(s) need your attention`);\n }\n\n if (parts.length === 0) {\n return `init: nothing to do — \"${params.slug}\" is already scaffolded in ${params.dir}.`;\n }\n return `init: scaffolded \"${params.slug}\" into ${params.dir} (${parts.join(', ')}).`;\n}\n"]}
|
package/dist/commands/upgrade.js
CHANGED
|
@@ -9,11 +9,12 @@ import { EXIT_CODES } from '../exit-codes.js';
|
|
|
9
9
|
import { readInstalledPackageManifest } from '../harness-scaffold.js';
|
|
10
10
|
import { detectPackageManager, installExactArgs, lockfileName, } from '../package-manager.js';
|
|
11
11
|
import { loadProject } from '../project.js';
|
|
12
|
+
import { classifyPath, writeFileNoFollow } from '../safe-fs.js';
|
|
12
13
|
import { renderTemplate, toDisplayName } from '../templates.js';
|
|
13
14
|
import { TOOLCHAIN_PACKAGES, } from '../toolchain.js';
|
|
14
15
|
import { resolveToolchainTarget } from '../toolchain-resolve.js';
|
|
15
16
|
import { collectAllFindings } from '../validate-findings.js';
|
|
16
|
-
import { AGENT_CONTEXT_FILES } from './init.js';
|
|
17
|
+
import { AGENT_CONTEXT_FILES, DOCS_GROUNDING_FILES, copyBundledSkillTrees, } from './init.js';
|
|
17
18
|
import { promptYesNo } from './publish.js';
|
|
18
19
|
export async function runUpgrade(ctx, args, deps = {}) {
|
|
19
20
|
var _a, _b, _c, _d, _e, _f, _g;
|
|
@@ -131,11 +132,23 @@ export async function runUpgrade(ctx, args, deps = {}) {
|
|
|
131
132
|
finally {
|
|
132
133
|
snapshot.dispose();
|
|
133
134
|
}
|
|
135
|
+
const newTemplatesDir = path.join(loaded.projectDir, 'node_modules', '@ekanos', 'cli', 'templates');
|
|
134
136
|
const regenerated = regenerateAgentContextFiles({
|
|
135
137
|
projectDir: loaded.projectDir,
|
|
136
138
|
slug: loaded.primary.slug,
|
|
137
139
|
oldTemplates: oldAgentTemplates,
|
|
138
|
-
newTemplatesDir
|
|
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,
|
|
139
152
|
});
|
|
140
153
|
// `specs` only ever covers DIRECT dependencies (see `buildInstallSpecs`) —
|
|
141
154
|
// a changed key with no spec is transitive and was never actually
|
|
@@ -155,7 +168,8 @@ export async function runUpgrade(ctx, args, deps = {}) {
|
|
|
155
168
|
installed: installedKeys,
|
|
156
169
|
skippedTransitive: skippedKeys,
|
|
157
170
|
regenerated,
|
|
158
|
-
|
|
171
|
+
scaffolded,
|
|
172
|
+
}, successSummary(installedKeys, skippedKeys, deltas, regenerated, scaffolded));
|
|
159
173
|
}
|
|
160
174
|
/**
|
|
161
175
|
* Best-effort host/session resolution — NEVER throws. `upgrade` must work for
|
|
@@ -358,6 +372,63 @@ function captureAgentTemplates(projectDir) {
|
|
|
358
372
|
}
|
|
359
373
|
return captured;
|
|
360
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
|
+
}
|
|
361
432
|
/**
|
|
362
433
|
* Regenerate `AGENTS.md`, `CLAUDE.md` and the Claude Code skill from the
|
|
363
434
|
* NEWLY installed CLI's bundled templates — using the exact byte-comparison
|
|
@@ -377,8 +448,16 @@ function regenerateAgentContextFiles(params) {
|
|
|
377
448
|
const needsAttention = [];
|
|
378
449
|
for (const { template, dest } of AGENT_CONTEXT_FILES) {
|
|
379
450
|
const destPath = path.join(params.projectDir, dest);
|
|
380
|
-
|
|
451
|
+
const kind = classifyPath(destPath);
|
|
452
|
+
if (kind === 'missing')
|
|
381
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
|
+
}
|
|
382
461
|
const current = fs.readFileSync(destPath, 'utf8');
|
|
383
462
|
const oldSource = params.oldTemplates.get(template);
|
|
384
463
|
const oldRendered = oldSource === undefined ? null : renderTemplate(oldSource, vars);
|
|
@@ -394,8 +473,18 @@ function regenerateAgentContextFiles(params) {
|
|
|
394
473
|
unchanged.push(dest);
|
|
395
474
|
continue;
|
|
396
475
|
}
|
|
397
|
-
|
|
398
|
-
|
|
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
|
+
}
|
|
399
488
|
}
|
|
400
489
|
return { updated, unchanged, needsAttention };
|
|
401
490
|
}
|
|
@@ -415,7 +504,7 @@ function planSummary(source, deltas) {
|
|
|
415
504
|
return (`Upgrading (target resolved from ${source}):\n` +
|
|
416
505
|
changing.map((line) => ` ${line}`).join('\n'));
|
|
417
506
|
}
|
|
418
|
-
function successSummary(installedKeys, skippedKeys, deltas, regenerated) {
|
|
507
|
+
function successSummary(installedKeys, skippedKeys, deltas, regenerated, scaffolded) {
|
|
419
508
|
const installed = installedKeys
|
|
420
509
|
.map((key) => `${key} → ${deltas[key].target}`)
|
|
421
510
|
.join(', ');
|
|
@@ -436,6 +525,18 @@ function successSummary(installedKeys, skippedKeys, deltas, regenerated) {
|
|
|
436
525
|
if (regenerated.updated.length > 0) {
|
|
437
526
|
parts.push(`regenerated ${regenerated.updated.join(', ')}.`);
|
|
438
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
|
+
}
|
|
439
540
|
if (regenerated.needsAttention.length > 0) {
|
|
440
541
|
parts.push(`${regenerated.needsAttention.join(', ')} were locally modified — ` +
|
|
441
542
|
'left untouched, merge the new template by hand.');
|