@ekanos/cli 0.1.6 → 0.1.8
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/publish.d.ts +5 -0
- package/dist/commands/publish.js +1 -1
- package/dist/commands/publish.js.map +1 -1
- package/dist/commands/status.d.ts +27 -1
- package/dist/commands/status.js +145 -12
- package/dist/commands/status.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.d.ts +6 -0
- package/dist/index.js +12 -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 +48 -4
- package/dist/publish-api.js +32 -13
- 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/dist/commands/status.js
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import { refreshStoredSession, resolveAuthEnvironment, resolveIdentity, } from '../auth/session.js';
|
|
4
|
-
import { CliError, authRequiredError } from '../errors.js';
|
|
4
|
+
import { CliError, authRequiredError, gateFailedError, preconditionError, } from '../errors.js';
|
|
5
5
|
import { ERROR_CODES } from '../exit-codes.js';
|
|
6
6
|
import { EKANOS_CONFIG_FILENAME, loadProject } from '../project.js';
|
|
7
|
-
import { listSubmissions } from '../publish-api.js';
|
|
7
|
+
import { listSubmissions, } from '../publish-api.js';
|
|
8
8
|
import { announceUpdate, checkForUpdate } from '../update-notice.js';
|
|
9
|
+
import { readProjectVersion } from './publish.js';
|
|
10
|
+
/** `--watch`'s poll interval — matches the mission brief exactly. */
|
|
11
|
+
const WATCH_POLL_INTERVAL_MS = 30000;
|
|
12
|
+
/**
|
|
13
|
+
* The states `--watch` stops on. `submitted` is the only non-terminal state
|
|
14
|
+
* a version can be in when watching starts (an already-`gated`/`dev`/
|
|
15
|
+
* `rejected` version resolves on the very first check, no polling needed).
|
|
16
|
+
*/
|
|
17
|
+
const WATCH_TERMINAL_STATES = new Set(['gated', 'dev', 'rejected']);
|
|
9
18
|
/**
|
|
10
19
|
* `status` — the read verb: where am I pointed, who am I, and what has this
|
|
11
20
|
* project submitted?
|
|
@@ -23,10 +32,26 @@ import { announceUpdate, checkForUpdate } from '../update-notice.js';
|
|
|
23
32
|
* Server errors here DO map through the taxonomy: an unreachable host or a
|
|
24
33
|
* missing source is a fault to surface, not a state to absorb.
|
|
25
34
|
*
|
|
26
|
-
* Exit 0 whenever the report itself could be assembled
|
|
35
|
+
* Exit 0 whenever the report itself could be assembled — EXCEPT under
|
|
36
|
+
* `--watch` (see below), whose exit code carries the gate's verdict instead.
|
|
37
|
+
*
|
|
38
|
+
* `--findings` requests each version's full finding list (`?detail=findings`
|
|
39
|
+
* server-side) instead of just error/warn counts.
|
|
40
|
+
*
|
|
41
|
+
* `--watch` turns `status` into a blocking wait: it requires a project with
|
|
42
|
+
* a resolved source and a live session (both are PRECONDITIONs here, not
|
|
43
|
+
* degraded-to-false states — there is nothing to watch without them), then
|
|
44
|
+
* polls `GET /api/partner/submissions?detail=findings` every 30s for THIS
|
|
45
|
+
* project's current (slug, package.json version) submission until it reaches
|
|
46
|
+
* a terminal-for-waiting state: `gated`/`dev` exit 0, `rejected` exits 10
|
|
47
|
+
* (GATE_FAILED) with the findings carried in `data` — the same shape
|
|
48
|
+
* `publish`'s own gate refusal uses, so an agent's fix-then-retry loop
|
|
49
|
+
* branches on it identically whether the gate ran inline (future) or via
|
|
50
|
+
* this poll (today). `--json --watch` still emits exactly ONE envelope, at
|
|
51
|
+
* completion — the polling itself only ever writes to stderr via `ctx.log`.
|
|
27
52
|
*/
|
|
28
53
|
export async function runStatus(ctx, args) {
|
|
29
|
-
var _a, _b;
|
|
54
|
+
var _a, _b, _c;
|
|
30
55
|
const env = resolveAuthEnvironment(ctx, args.host, args.env, {
|
|
31
56
|
projectDir: args.dir,
|
|
32
57
|
});
|
|
@@ -51,9 +76,23 @@ export async function runStatus(ctx, args) {
|
|
|
51
76
|
// never to produce.
|
|
52
77
|
const live = stored ? ((_a = env.store.read(env.host)) !== null && _a !== void 0 ? _a : stored) : null;
|
|
53
78
|
const project = readProjectReport(args.dir);
|
|
79
|
+
const detail = args.findings || args.watch ? 'findings' : undefined;
|
|
80
|
+
if (args.watch) {
|
|
81
|
+
if (!session.authenticated || !live) {
|
|
82
|
+
throw authRequiredError(`"ekanos status --watch" needs a live session on ${env.host}.`, `Run "ekanos login --host ${env.host}" and retry.`);
|
|
83
|
+
}
|
|
84
|
+
if (!project) {
|
|
85
|
+
throw preconditionError(`No ekanos.json found in ${path.resolve(args.dir)}.`, 'Run "ekanos publish" at least once before watching its gate result.');
|
|
86
|
+
}
|
|
87
|
+
if (!project.source) {
|
|
88
|
+
throw preconditionError('This project has no "source" in ekanos.json yet.', 'Run "ekanos publish" at least once (it saves the source you ' +
|
|
89
|
+
'publish to) before watching its gate result.');
|
|
90
|
+
}
|
|
91
|
+
return runWatch(ctx, env, project, args);
|
|
92
|
+
}
|
|
54
93
|
let submissions = null;
|
|
55
94
|
if ((project === null || project === void 0 ? void 0 : project.source) && session.authenticated && live) {
|
|
56
|
-
submissions = await listWithOneRefresh(env, live, project.source);
|
|
95
|
+
submissions = await listWithOneRefresh(env, live, project.source, detail);
|
|
57
96
|
}
|
|
58
97
|
else if ((project === null || project === void 0 ? void 0 : project.source) && !session.authenticated) {
|
|
59
98
|
ctx.log(`ekanos: not logged in to ${env.host} — skipping the submission list. ` +
|
|
@@ -65,7 +104,81 @@ export async function runStatus(ctx, args) {
|
|
|
65
104
|
'submissions from then on.');
|
|
66
105
|
}
|
|
67
106
|
return ctx.succeed(Object.assign(Object.assign(Object.assign({ host: env.host }, session), { credentialsPath: env.store.filePath, project,
|
|
68
|
-
submissions }), (notice ? { toolchain: notice } : {})), humanSummary(env.host, session, project, submissions));
|
|
107
|
+
submissions }), (notice ? { toolchain: notice } : {})), humanSummary(env.host, session, project, submissions, (_c = args.findings) !== null && _c !== void 0 ? _c : false));
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The `--watch` poll loop. Targets THIS project's current submission —
|
|
111
|
+
* `project.slug` + the version presently in `package.json` (the same value
|
|
112
|
+
* `publish` would submit right now) — not "whatever's most recent," so
|
|
113
|
+
* watching after a publish can never resolve against a stale prior version.
|
|
114
|
+
*/
|
|
115
|
+
async function runWatch(ctx, env, project, args) {
|
|
116
|
+
var _a;
|
|
117
|
+
const source = project.source;
|
|
118
|
+
if (!source) {
|
|
119
|
+
// Unreachable — the caller already checked — but keeps this function's
|
|
120
|
+
// own type honest without a non-null assertion.
|
|
121
|
+
throw preconditionError('This project has no "source" in ekanos.json yet.', 'Run "ekanos publish" at least once before watching its gate result.');
|
|
122
|
+
}
|
|
123
|
+
const targetVersion = readProjectVersion(project.dir);
|
|
124
|
+
const sleep = (_a = args.sleep) !== null && _a !== void 0 ? _a : defaultSleep;
|
|
125
|
+
ctx.log(`ekanos: watching ${project.slug}@${targetVersion} on ${env.host} ` +
|
|
126
|
+
`(polling every ${WATCH_POLL_INTERVAL_MS / 1000}s)…`);
|
|
127
|
+
for (;;) {
|
|
128
|
+
// Re-read the session from the credential store on EVERY iteration —
|
|
129
|
+
// never carry one over in a closure. Auth0 rotates the refresh token on
|
|
130
|
+
// every use (see `refreshStoredSession`'s docstring), so a token this
|
|
131
|
+
// loop already consumed is dead by the next iteration; the store is the
|
|
132
|
+
// persisted source of truth for whichever refresh most recently landed
|
|
133
|
+
// — this loop's own (via `listWithOneRefresh` below, which persists
|
|
134
|
+
// through `refreshStoredSession`), or a concurrent one from another
|
|
135
|
+
// process (another terminal running login/publish/status --watch
|
|
136
|
+
// against the same host). Threading the refreshed session back out of
|
|
137
|
+
// `listWithOneRefresh` instead would only cover the first case.
|
|
138
|
+
const current = env.store.read(env.host);
|
|
139
|
+
if (!current) {
|
|
140
|
+
throw authRequiredError(`The session for ${env.host} is gone — "ekanos status --watch" ` +
|
|
141
|
+
'cannot continue without it.', `Run "ekanos login --host ${env.host}" and retry.`);
|
|
142
|
+
}
|
|
143
|
+
const integrations = await listWithOneRefresh(env, current, source, 'findings');
|
|
144
|
+
const version = findVersion(integrations, project.slug, targetVersion);
|
|
145
|
+
if (version && WATCH_TERMINAL_STATES.has(version.state)) {
|
|
146
|
+
return reportWatchOutcome(ctx, project.slug, version);
|
|
147
|
+
}
|
|
148
|
+
ctx.log(`ekanos: ${project.slug}@${targetVersion} is ` +
|
|
149
|
+
`${version ? version.state : 'not visible yet'} — waiting…`);
|
|
150
|
+
await sleep(WATCH_POLL_INTERVAL_MS);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function findVersion(integrations, slug, version) {
|
|
154
|
+
var _a;
|
|
155
|
+
const integration = integrations.find((i) => i.slug === slug);
|
|
156
|
+
return (_a = integration === null || integration === void 0 ? void 0 : integration.versions.find((v) => v.version === version)) !== null && _a !== void 0 ? _a : null;
|
|
157
|
+
}
|
|
158
|
+
function reportWatchOutcome(ctx, slug, version) {
|
|
159
|
+
var _a, _b, _c;
|
|
160
|
+
const data = {
|
|
161
|
+
slug,
|
|
162
|
+
version: version.version,
|
|
163
|
+
state: version.state,
|
|
164
|
+
gatedAt: version.gatedAt,
|
|
165
|
+
errorCount: version.errorCount,
|
|
166
|
+
warnCount: version.warnCount,
|
|
167
|
+
findings: (_a = version.findings) !== null && _a !== void 0 ? _a : [],
|
|
168
|
+
stages: (_b = version.stages) !== null && _b !== void 0 ? _b : [],
|
|
169
|
+
};
|
|
170
|
+
if (version.state === 'rejected') {
|
|
171
|
+
return ctx.fail(gateFailedError(`${slug}@${version.version} was rejected by the gate ` +
|
|
172
|
+
`(${(_c = data.errorCount) !== null && _c !== void 0 ? _c : 0} error${data.errorCount === 1 ? '' : 's'}).`, 'Resolve the error-severity findings in data.findings, bump the ' +
|
|
173
|
+
'version, and re-run "ekanos publish".'), data);
|
|
174
|
+
}
|
|
175
|
+
return ctx.succeed(data, `status --watch: ${slug}@${version.version} is now "${version.state}"` +
|
|
176
|
+
`${data.warnCount ? ` (${data.warnCount} warning${data.warnCount === 1 ? '' : 's'})` : ''}.`);
|
|
177
|
+
}
|
|
178
|
+
function defaultSleep(ms) {
|
|
179
|
+
return new Promise((resolve) => {
|
|
180
|
+
setTimeout(resolve, ms);
|
|
181
|
+
});
|
|
69
182
|
}
|
|
70
183
|
/**
|
|
71
184
|
* whoami's identity resolution, with AUTH_REQUIRED absorbed into
|
|
@@ -109,12 +222,16 @@ function readProjectReport(dir) {
|
|
|
109
222
|
};
|
|
110
223
|
}
|
|
111
224
|
/** Mirrors publish's one-refresh-one-retry on a stale access token. */
|
|
112
|
-
async function listWithOneRefresh(env, session, source) {
|
|
113
|
-
const first = await listSubmissions(env.host, session.accessToken, source
|
|
225
|
+
async function listWithOneRefresh(env, session, source, detail) {
|
|
226
|
+
const first = await listSubmissions(env.host, session.accessToken, source, {
|
|
227
|
+
detail,
|
|
228
|
+
});
|
|
114
229
|
if (first.status === 'ok')
|
|
115
230
|
return first.integrations;
|
|
116
231
|
const refreshed = await refreshStoredSession(env, session);
|
|
117
|
-
const second = await listSubmissions(env.host, refreshed.accessToken, source
|
|
232
|
+
const second = await listSubmissions(env.host, refreshed.accessToken, source, {
|
|
233
|
+
detail,
|
|
234
|
+
});
|
|
118
235
|
if (second.status === 'ok')
|
|
119
236
|
return second.integrations;
|
|
120
237
|
// Authenticated a moment ago, unauthorized twice here: report the list as
|
|
@@ -122,8 +239,8 @@ async function listWithOneRefresh(env, session, source) {
|
|
|
122
239
|
// it. This is unreachable in practice; belt and braces for a racing revoke.
|
|
123
240
|
throw authRequiredError(`The session for ${env.host} could not authorize the submission list.`, `Run "ekanos login --host ${env.host}" and retry.`);
|
|
124
241
|
}
|
|
125
|
-
function humanSummary(host, session, project, submissions) {
|
|
126
|
-
var _a, _b, _c;
|
|
242
|
+
function humanSummary(host, session, project, submissions, showFindings) {
|
|
243
|
+
var _a, _b, _c, _d;
|
|
127
244
|
const lines = [];
|
|
128
245
|
lines.push(session.authenticated
|
|
129
246
|
? `status: logged in to ${host} as ${(_b = (_a = session.user) === null || _a === void 0 ? void 0 : _a.email) !== null && _b !== void 0 ? _b : (_c = session.user) === null || _c === void 0 ? void 0 : _c.id}${session.expiresAt ? ` (session expires ${session.expiresAt})` : ''}.`
|
|
@@ -144,10 +261,26 @@ function humanSummary(host, session, project, submissions) {
|
|
|
144
261
|
for (const integration of submissions) {
|
|
145
262
|
for (const version of integration.versions) {
|
|
146
263
|
lines.push(` submissions: ${integration.slug}@${version.version} — ` +
|
|
147
|
-
`${version.state}${version.submittedAt ? ` (submitted ${version.submittedAt})` : ''}`);
|
|
264
|
+
`${version.state}${version.submittedAt ? ` (submitted ${version.submittedAt})` : ''}${gateCountsSuffix(version)}`);
|
|
265
|
+
if (showFindings) {
|
|
266
|
+
for (const finding of (_d = version.findings) !== null && _d !== void 0 ? _d : []) {
|
|
267
|
+
lines.push(` [${finding.severity}] ${finding.check}: ${finding.message} ` +
|
|
268
|
+
`— ${finding.hint}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
148
271
|
}
|
|
149
272
|
}
|
|
150
273
|
}
|
|
151
274
|
return lines.join('\n');
|
|
152
275
|
}
|
|
276
|
+
/** ` (N errors, M warnings)`, omitted entirely until a gate has actually run. */
|
|
277
|
+
function gateCountsSuffix(version) {
|
|
278
|
+
var _a, _b;
|
|
279
|
+
if (version.errorCount === null && version.warnCount === null)
|
|
280
|
+
return '';
|
|
281
|
+
const errorCount = (_a = version.errorCount) !== null && _a !== void 0 ? _a : 0;
|
|
282
|
+
const warnCount = (_b = version.warnCount) !== null && _b !== void 0 ? _b : 0;
|
|
283
|
+
return (` (${errorCount} error${errorCount === 1 ? '' : 's'}, ` +
|
|
284
|
+
`${warnCount} warning${warnCount === 1 ? '' : 's'})`);
|
|
285
|
+
}
|
|
153
286
|
//# sourceMappingURL=status.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"status.js","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport type { StoredSession } from '../auth/credential-store';\nimport {\n refreshStoredSession,\n resolveAuthEnvironment,\n resolveIdentity,\n} from '../auth/session';\nimport type { CliContext } from '../context';\nimport { CliError, authRequiredError } from '../errors';\nimport { ERROR_CODES, type ExitCode } from '../exit-codes';\nimport { EKANOS_CONFIG_FILENAME, loadProject } from '../project';\nimport { type SubmissionListing, listSubmissions } from '../publish-api';\nimport { announceUpdate, checkForUpdate } from '../update-notice';\n\nexport interface StatusArgs {\n host?: string;\n /** Project directory (may contain ekanos.json). Defaults to cwd. */\n dir: string;\n env: Record<string, string | undefined>;\n}\n\ninterface SessionReport {\n authenticated: boolean;\n user?: { id: string; email: string | null; name: string | null };\n expiresAt?: string;\n hint?: string;\n}\n\ninterface ProjectReport {\n dir: string;\n slug: string;\n source: string | null;\n}\n\n/**\n * `status` — the read verb: where am I pointed, who am I, and what has this\n * project submitted?\n *\n * Three sections, each degrading gracefully rather than failing:\n *\n * - **session** — whoami's server-side identity check, but an absent or dead\n * session reports `authenticated: false` with a hint instead of exit 4. A\n * partner asking \"what is my state?\" must get an answer, not an error whose\n * answer it is.\n * - **project** — the ekanos.json in `--dir` (default cwd), when there is\n * one. No project is an ordinary state for `status`, never an error.\n * - **submissions** — `GET /api/partner/submissions?source=…` for the\n * project's resolved source, only when both a session and a source exist.\n * Server errors here DO map through the taxonomy: an unreachable host or a\n * missing source is a fault to surface, not a state to absorb.\n *\n * Exit 0 whenever the report itself could be assembled.\n */\nexport async function runStatus(\n ctx: CliContext,\n args: StatusArgs,\n): Promise<ExitCode> {\n const env = resolveAuthEnvironment(ctx, args.host, args.env, {\n projectDir: args.dir,\n });\n\n const stored = env.store.read(env.host);\n\n const notice = await checkForUpdate({\n env: args.env,\n host: env.host,\n session: stored ?? undefined,\n });\n announceUpdate(ctx, notice);\n\n const session = stored\n ? await probeSession(env, stored)\n : {\n authenticated: false,\n hint: `Run \"ekanos login --host ${env.host}\" to sign in.`,\n };\n\n // probeSession may have refreshed the session — persisting a NEW access\n // token and ROTATING the refresh token on disk. Re-read before using it:\n // the pre-probe object now holds a dead access token and an already-spent\n // refresh token, and presenting those would turn \"logged in, token was\n // merely stale\" into a hard exit-4 — the one failure this command promises\n // never to produce.\n const live = stored ? (env.store.read(env.host) ?? stored) : null;\n\n const project = readProjectReport(args.dir);\n\n let submissions: SubmissionListing[] | null = null;\n\n if (project?.source && session.authenticated && live) {\n submissions = await listWithOneRefresh(env, live, project.source);\n } else if (project?.source && !session.authenticated) {\n ctx.log(\n `ekanos: not logged in to ${env.host} — skipping the submission list. ` +\n (session.hint ?? ''),\n );\n } else if (project && !project.source) {\n ctx.log(\n 'ekanos: this project has no \"source\" in ekanos.json yet — the first ' +\n '\"ekanos publish --source <slug>\" saves one, and status will list ' +\n 'submissions from then on.',\n );\n }\n\n return ctx.succeed(\n {\n host: env.host,\n ...session,\n credentialsPath: env.store.filePath,\n project,\n submissions,\n ...(notice ? { toolchain: notice } : {}),\n },\n humanSummary(env.host, session, project, submissions),\n );\n}\n\n/**\n * whoami's identity resolution, with AUTH_REQUIRED absorbed into\n * `authenticated: false`. Anything else — network, precondition, MFA mapped\n * to auth by `fetchIdentity` — still propagates: \"the host was unreachable\"\n * must never be reported as \"you are logged out\".\n */\nasync function probeSession(\n env: ReturnType<typeof resolveAuthEnvironment>,\n stored: StoredSession,\n): Promise<SessionReport> {\n try {\n const { identity, session } = await resolveIdentity(env, stored);\n\n return {\n authenticated: true,\n user: { id: identity.id, email: identity.email, name: identity.name },\n expiresAt: session.expiresAt,\n };\n } catch (error) {\n if (error instanceof CliError && error.code === ERROR_CODES.AUTH_REQUIRED) {\n return { authenticated: false, hint: error.hint };\n }\n\n throw error;\n }\n}\n\n/**\n * Lenient project read: no ekanos.json means \"not in a project\", which is an\n * answer, not an error. An ekanos.json that EXISTS but does not load keeps\n * `loadProject`'s strict behaviour — a broken project file is a fault the\n * partner needs named, whatever verb they ran.\n */\nfunction readProjectReport(dir: string): ProjectReport | null {\n const projectDir = path.resolve(dir);\n\n if (!fs.existsSync(path.join(projectDir, EKANOS_CONFIG_FILENAME))) {\n return null;\n }\n\n const loaded = loadProject(projectDir);\n\n return {\n dir: loaded.projectDir,\n slug: loaded.primary.slug,\n source: loaded.project.source ?? null,\n };\n}\n\n/** Mirrors publish's one-refresh-one-retry on a stale access token. */\nasync function listWithOneRefresh(\n env: ReturnType<typeof resolveAuthEnvironment>,\n session: StoredSession,\n source: string,\n): Promise<SubmissionListing[]> {\n const first = await listSubmissions(env.host, session.accessToken, source);\n\n if (first.status === 'ok') return first.integrations;\n\n const refreshed = await refreshStoredSession(env, session);\n const second = await listSubmissions(env.host, refreshed.accessToken, source);\n\n if (second.status === 'ok') return second.integrations;\n\n // Authenticated a moment ago, unauthorized twice here: report the list as\n // unavailable rather than the session as dead — probeSession already proved\n // it. This is unreachable in practice; belt and braces for a racing revoke.\n throw authRequiredError(\n `The session for ${env.host} could not authorize the submission list.`,\n `Run \"ekanos login --host ${env.host}\" and retry.`,\n );\n}\n\nfunction humanSummary(\n host: string,\n session: SessionReport,\n project: ProjectReport | null,\n submissions: SubmissionListing[] | null,\n): string {\n const lines: string[] = [];\n\n lines.push(\n session.authenticated\n ? `status: logged in to ${host} as ${\n session.user?.email ?? session.user?.id\n }${session.expiresAt ? ` (session expires ${session.expiresAt})` : ''}.`\n : `status: not logged in to ${host}.`,\n );\n\n if (project) {\n lines.push(\n ` project: ${project.slug} in ${project.dir}` +\n (project.source\n ? ` (source \"${project.source}\")`\n : ' (no source configured yet)'),\n );\n } else {\n lines.push(' project: none (no ekanos.json here).');\n }\n\n if (submissions) {\n if (submissions.length === 0) {\n lines.push(' submissions: none yet.');\n }\n\n for (const integration of submissions) {\n for (const version of integration.versions) {\n lines.push(\n ` submissions: ${integration.slug}@${version.version} — ` +\n `${version.state}${\n version.submittedAt ? ` (submitted ${version.submittedAt})` : ''\n }`,\n );\n }\n }\n }\n\n return lines.join('\\n');\n}\n"]}
|
|
1
|
+
{"version":3,"file":"status.js","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport type { StoredSession } from '../auth/credential-store';\nimport {\n refreshStoredSession,\n resolveAuthEnvironment,\n resolveIdentity,\n} from '../auth/session';\nimport type { CliContext } from '../context';\nimport {\n CliError,\n authRequiredError,\n gateFailedError,\n preconditionError,\n} from '../errors';\nimport { ERROR_CODES, type ExitCode } from '../exit-codes';\nimport { EKANOS_CONFIG_FILENAME, loadProject } from '../project';\nimport {\n type SubmissionListing,\n type SubmissionVersion,\n listSubmissions,\n} from '../publish-api';\nimport { announceUpdate, checkForUpdate } from '../update-notice';\nimport { readProjectVersion } from './publish';\n\n/** `--watch`'s poll interval — matches the mission brief exactly. */\nconst WATCH_POLL_INTERVAL_MS = 30_000;\n\n/**\n * The states `--watch` stops on. `submitted` is the only non-terminal state\n * a version can be in when watching starts (an already-`gated`/`dev`/\n * `rejected` version resolves on the very first check, no polling needed).\n */\nconst WATCH_TERMINAL_STATES = new Set(['gated', 'dev', 'rejected']);\n\nexport interface StatusArgs {\n host?: string;\n /** Project directory (may contain ekanos.json). Defaults to cwd. */\n dir: string;\n env: Record<string, string | undefined>;\n /** Render each version's full finding list, not just error/warn counts. */\n findings?: boolean;\n /**\n * Block until this project's current (slug, version) submission reaches a\n * terminal-for-waiting state (gated, dev, rejected), polling every 30s.\n * Changes the command's exit-code contract — see `runStatus`'s docstring.\n */\n watch?: boolean;\n /** Injectable for tests — see `auth/device-flow.ts`'s identical pattern. */\n sleep?: (ms: number) => Promise<void>;\n}\n\ninterface SessionReport {\n authenticated: boolean;\n user?: { id: string; email: string | null; name: string | null };\n expiresAt?: string;\n hint?: string;\n}\n\ninterface ProjectReport {\n dir: string;\n slug: string;\n source: string | null;\n}\n\n/**\n * `status` — the read verb: where am I pointed, who am I, and what has this\n * project submitted?\n *\n * Three sections, each degrading gracefully rather than failing:\n *\n * - **session** — whoami's server-side identity check, but an absent or dead\n * session reports `authenticated: false` with a hint instead of exit 4. A\n * partner asking \"what is my state?\" must get an answer, not an error whose\n * answer it is.\n * - **project** — the ekanos.json in `--dir` (default cwd), when there is\n * one. No project is an ordinary state for `status`, never an error.\n * - **submissions** — `GET /api/partner/submissions?source=…` for the\n * project's resolved source, only when both a session and a source exist.\n * Server errors here DO map through the taxonomy: an unreachable host or a\n * missing source is a fault to surface, not a state to absorb.\n *\n * Exit 0 whenever the report itself could be assembled — EXCEPT under\n * `--watch` (see below), whose exit code carries the gate's verdict instead.\n *\n * `--findings` requests each version's full finding list (`?detail=findings`\n * server-side) instead of just error/warn counts.\n *\n * `--watch` turns `status` into a blocking wait: it requires a project with\n * a resolved source and a live session (both are PRECONDITIONs here, not\n * degraded-to-false states — there is nothing to watch without them), then\n * polls `GET /api/partner/submissions?detail=findings` every 30s for THIS\n * project's current (slug, package.json version) submission until it reaches\n * a terminal-for-waiting state: `gated`/`dev` exit 0, `rejected` exits 10\n * (GATE_FAILED) with the findings carried in `data` — the same shape\n * `publish`'s own gate refusal uses, so an agent's fix-then-retry loop\n * branches on it identically whether the gate ran inline (future) or via\n * this poll (today). `--json --watch` still emits exactly ONE envelope, at\n * completion — the polling itself only ever writes to stderr via `ctx.log`.\n */\nexport async function runStatus(\n ctx: CliContext,\n args: StatusArgs,\n): Promise<ExitCode> {\n const env = resolveAuthEnvironment(ctx, args.host, args.env, {\n projectDir: args.dir,\n });\n\n const stored = env.store.read(env.host);\n\n const notice = await checkForUpdate({\n env: args.env,\n host: env.host,\n session: stored ?? undefined,\n });\n announceUpdate(ctx, notice);\n\n const session = stored\n ? await probeSession(env, stored)\n : {\n authenticated: false,\n hint: `Run \"ekanos login --host ${env.host}\" to sign in.`,\n };\n\n // probeSession may have refreshed the session — persisting a NEW access\n // token and ROTATING the refresh token on disk. Re-read before using it:\n // the pre-probe object now holds a dead access token and an already-spent\n // refresh token, and presenting those would turn \"logged in, token was\n // merely stale\" into a hard exit-4 — the one failure this command promises\n // never to produce.\n const live = stored ? (env.store.read(env.host) ?? stored) : null;\n\n const project = readProjectReport(args.dir);\n\n const detail = args.findings || args.watch ? 'findings' : undefined;\n\n if (args.watch) {\n if (!session.authenticated || !live) {\n throw authRequiredError(\n `\"ekanos status --watch\" needs a live session on ${env.host}.`,\n `Run \"ekanos login --host ${env.host}\" and retry.`,\n );\n }\n\n if (!project) {\n throw preconditionError(\n `No ekanos.json found in ${path.resolve(args.dir)}.`,\n 'Run \"ekanos publish\" at least once before watching its gate result.',\n );\n }\n\n if (!project.source) {\n throw preconditionError(\n 'This project has no \"source\" in ekanos.json yet.',\n 'Run \"ekanos publish\" at least once (it saves the source you ' +\n 'publish to) before watching its gate result.',\n );\n }\n\n return runWatch(ctx, env, project, args);\n }\n\n let submissions: SubmissionListing[] | null = null;\n\n if (project?.source && session.authenticated && live) {\n submissions = await listWithOneRefresh(env, live, project.source, detail);\n } else if (project?.source && !session.authenticated) {\n ctx.log(\n `ekanos: not logged in to ${env.host} — skipping the submission list. ` +\n (session.hint ?? ''),\n );\n } else if (project && !project.source) {\n ctx.log(\n 'ekanos: this project has no \"source\" in ekanos.json yet — the first ' +\n '\"ekanos publish --source <slug>\" saves one, and status will list ' +\n 'submissions from then on.',\n );\n }\n\n return ctx.succeed(\n {\n host: env.host,\n ...session,\n credentialsPath: env.store.filePath,\n project,\n submissions,\n ...(notice ? { toolchain: notice } : {}),\n },\n humanSummary(\n env.host,\n session,\n project,\n submissions,\n args.findings ?? false,\n ),\n );\n}\n\n/**\n * The `--watch` poll loop. Targets THIS project's current submission —\n * `project.slug` + the version presently in `package.json` (the same value\n * `publish` would submit right now) — not \"whatever's most recent,\" so\n * watching after a publish can never resolve against a stale prior version.\n */\nasync function runWatch(\n ctx: CliContext,\n env: ReturnType<typeof resolveAuthEnvironment>,\n project: ProjectReport,\n args: StatusArgs,\n): Promise<ExitCode> {\n const source = project.source;\n if (!source) {\n // Unreachable — the caller already checked — but keeps this function's\n // own type honest without a non-null assertion.\n throw preconditionError(\n 'This project has no \"source\" in ekanos.json yet.',\n 'Run \"ekanos publish\" at least once before watching its gate result.',\n );\n }\n\n const targetVersion = readProjectVersion(project.dir);\n const sleep = args.sleep ?? defaultSleep;\n\n ctx.log(\n `ekanos: watching ${project.slug}@${targetVersion} on ${env.host} ` +\n `(polling every ${WATCH_POLL_INTERVAL_MS / 1000}s)…`,\n );\n\n for (;;) {\n // Re-read the session from the credential store on EVERY iteration —\n // never carry one over in a closure. Auth0 rotates the refresh token on\n // every use (see `refreshStoredSession`'s docstring), so a token this\n // loop already consumed is dead by the next iteration; the store is the\n // persisted source of truth for whichever refresh most recently landed\n // — this loop's own (via `listWithOneRefresh` below, which persists\n // through `refreshStoredSession`), or a concurrent one from another\n // process (another terminal running login/publish/status --watch\n // against the same host). Threading the refreshed session back out of\n // `listWithOneRefresh` instead would only cover the first case.\n const current = env.store.read(env.host);\n\n if (!current) {\n throw authRequiredError(\n `The session for ${env.host} is gone — \"ekanos status --watch\" ` +\n 'cannot continue without it.',\n `Run \"ekanos login --host ${env.host}\" and retry.`,\n );\n }\n\n const integrations = await listWithOneRefresh(\n env,\n current,\n source,\n 'findings',\n );\n const version = findVersion(integrations, project.slug, targetVersion);\n\n if (version && WATCH_TERMINAL_STATES.has(version.state)) {\n return reportWatchOutcome(ctx, project.slug, version);\n }\n\n ctx.log(\n `ekanos: ${project.slug}@${targetVersion} is ` +\n `${version ? version.state : 'not visible yet'} — waiting…`,\n );\n\n await sleep(WATCH_POLL_INTERVAL_MS);\n }\n}\n\nfunction findVersion(\n integrations: SubmissionListing[],\n slug: string,\n version: string,\n): SubmissionVersion | null {\n const integration = integrations.find((i) => i.slug === slug);\n return integration?.versions.find((v) => v.version === version) ?? null;\n}\n\nfunction reportWatchOutcome(\n ctx: CliContext,\n slug: string,\n version: SubmissionVersion,\n): ExitCode {\n const data = {\n slug,\n version: version.version,\n state: version.state,\n gatedAt: version.gatedAt,\n errorCount: version.errorCount,\n warnCount: version.warnCount,\n findings: version.findings ?? [],\n stages: version.stages ?? [],\n };\n\n if (version.state === 'rejected') {\n return ctx.fail(\n gateFailedError(\n `${slug}@${version.version} was rejected by the gate ` +\n `(${data.errorCount ?? 0} error${data.errorCount === 1 ? '' : 's'}).`,\n 'Resolve the error-severity findings in data.findings, bump the ' +\n 'version, and re-run \"ekanos publish\".',\n ),\n data,\n );\n }\n\n return ctx.succeed(\n data,\n `status --watch: ${slug}@${version.version} is now \"${version.state}\"` +\n `${data.warnCount ? ` (${data.warnCount} warning${data.warnCount === 1 ? '' : 's'})` : ''}.`,\n );\n}\n\nfunction defaultSleep(ms: number): Promise<void> {\n return new Promise((resolve) => {\n setTimeout(resolve, ms);\n });\n}\n\n/**\n * whoami's identity resolution, with AUTH_REQUIRED absorbed into\n * `authenticated: false`. Anything else — network, precondition, MFA mapped\n * to auth by `fetchIdentity` — still propagates: \"the host was unreachable\"\n * must never be reported as \"you are logged out\".\n */\nasync function probeSession(\n env: ReturnType<typeof resolveAuthEnvironment>,\n stored: StoredSession,\n): Promise<SessionReport> {\n try {\n const { identity, session } = await resolveIdentity(env, stored);\n\n return {\n authenticated: true,\n user: { id: identity.id, email: identity.email, name: identity.name },\n expiresAt: session.expiresAt,\n };\n } catch (error) {\n if (error instanceof CliError && error.code === ERROR_CODES.AUTH_REQUIRED) {\n return { authenticated: false, hint: error.hint };\n }\n\n throw error;\n }\n}\n\n/**\n * Lenient project read: no ekanos.json means \"not in a project\", which is an\n * answer, not an error. An ekanos.json that EXISTS but does not load keeps\n * `loadProject`'s strict behaviour — a broken project file is a fault the\n * partner needs named, whatever verb they ran.\n */\nfunction readProjectReport(dir: string): ProjectReport | null {\n const projectDir = path.resolve(dir);\n\n if (!fs.existsSync(path.join(projectDir, EKANOS_CONFIG_FILENAME))) {\n return null;\n }\n\n const loaded = loadProject(projectDir);\n\n return {\n dir: loaded.projectDir,\n slug: loaded.primary.slug,\n source: loaded.project.source ?? null,\n };\n}\n\n/** Mirrors publish's one-refresh-one-retry on a stale access token. */\nasync function listWithOneRefresh(\n env: ReturnType<typeof resolveAuthEnvironment>,\n session: StoredSession,\n source: string,\n detail?: 'findings',\n): Promise<SubmissionListing[]> {\n const first = await listSubmissions(env.host, session.accessToken, source, {\n detail,\n });\n\n if (first.status === 'ok') return first.integrations;\n\n const refreshed = await refreshStoredSession(env, session);\n const second = await listSubmissions(\n env.host,\n refreshed.accessToken,\n source,\n {\n detail,\n },\n );\n\n if (second.status === 'ok') return second.integrations;\n\n // Authenticated a moment ago, unauthorized twice here: report the list as\n // unavailable rather than the session as dead — probeSession already proved\n // it. This is unreachable in practice; belt and braces for a racing revoke.\n throw authRequiredError(\n `The session for ${env.host} could not authorize the submission list.`,\n `Run \"ekanos login --host ${env.host}\" and retry.`,\n );\n}\n\nfunction humanSummary(\n host: string,\n session: SessionReport,\n project: ProjectReport | null,\n submissions: SubmissionListing[] | null,\n showFindings: boolean,\n): string {\n const lines: string[] = [];\n\n lines.push(\n session.authenticated\n ? `status: logged in to ${host} as ${\n session.user?.email ?? session.user?.id\n }${session.expiresAt ? ` (session expires ${session.expiresAt})` : ''}.`\n : `status: not logged in to ${host}.`,\n );\n\n if (project) {\n lines.push(\n ` project: ${project.slug} in ${project.dir}` +\n (project.source\n ? ` (source \"${project.source}\")`\n : ' (no source configured yet)'),\n );\n } else {\n lines.push(' project: none (no ekanos.json here).');\n }\n\n if (submissions) {\n if (submissions.length === 0) {\n lines.push(' submissions: none yet.');\n }\n\n for (const integration of submissions) {\n for (const version of integration.versions) {\n lines.push(\n ` submissions: ${integration.slug}@${version.version} — ` +\n `${version.state}${\n version.submittedAt ? ` (submitted ${version.submittedAt})` : ''\n }${gateCountsSuffix(version)}`,\n );\n\n if (showFindings) {\n for (const finding of version.findings ?? []) {\n lines.push(\n ` [${finding.severity}] ${finding.check}: ${finding.message} ` +\n `— ${finding.hint}`,\n );\n }\n }\n }\n }\n }\n\n return lines.join('\\n');\n}\n\n/** ` (N errors, M warnings)`, omitted entirely until a gate has actually run. */\nfunction gateCountsSuffix(version: SubmissionVersion): string {\n if (version.errorCount === null && version.warnCount === null) return '';\n\n const errorCount = version.errorCount ?? 0;\n const warnCount = version.warnCount ?? 0;\n\n return (\n ` (${errorCount} error${errorCount === 1 ? '' : 's'}, ` +\n `${warnCount} warning${warnCount === 1 ? '' : 's'})`\n );\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.');
|