@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.
Files changed (47) hide show
  1. package/README.md +2 -2
  2. package/dist/commands/init.d.ts +36 -0
  3. package/dist/commands/init.js +126 -10
  4. package/dist/commands/init.js.map +1 -1
  5. package/dist/commands/publish.d.ts +5 -0
  6. package/dist/commands/publish.js +1 -1
  7. package/dist/commands/publish.js.map +1 -1
  8. package/dist/commands/status.d.ts +27 -1
  9. package/dist/commands/status.js +145 -12
  10. package/dist/commands/status.js.map +1 -1
  11. package/dist/commands/upgrade.js +108 -7
  12. package/dist/commands/upgrade.js.map +1 -1
  13. package/dist/copy-tree.d.ts +48 -0
  14. package/dist/copy-tree.js +55 -0
  15. package/dist/copy-tree.js.map +1 -0
  16. package/dist/index.d.ts +6 -0
  17. package/dist/index.js +12 -1
  18. package/dist/index.js.map +1 -1
  19. package/dist/parse-argv.d.ts +7 -1
  20. package/dist/parse-argv.js +10 -2
  21. package/dist/parse-argv.js.map +1 -1
  22. package/dist/project-checks.d.ts +9 -0
  23. package/dist/project-checks.js +29 -0
  24. package/dist/project-checks.js.map +1 -1
  25. package/dist/project.d.ts +23 -0
  26. package/dist/project.js +66 -1
  27. package/dist/project.js.map +1 -1
  28. package/dist/publish-api.d.ts +48 -4
  29. package/dist/publish-api.js +32 -13
  30. package/dist/publish-api.js.map +1 -1
  31. package/dist/safe-fs.d.ts +1 -1
  32. package/dist/safe-fs.js.map +1 -1
  33. package/dist/sources-api.d.ts +1 -1
  34. package/dist/sources-api.js.map +1 -1
  35. package/dist/validate-findings.js +2 -1
  36. package/dist/validate-findings.js.map +1 -1
  37. package/package.json +1 -1
  38. package/templates/AGENTS.md.tmpl +57 -0
  39. package/templates/claude-skill.md.tmpl +7 -0
  40. package/templates/docs/api-notes.md.tmpl +21 -0
  41. package/templates/mcp.json.tmpl +8 -0
  42. package/templates/skills/build-integration/SKILL.md +178 -0
  43. package/templates/skills/build-integration/references/api-docs-research.md +93 -0
  44. package/templates/skills/build-integration/references/api-wiring.md +94 -0
  45. package/templates/skills/build-integration/references/widget-design.md +132 -0
  46. package/templates/skills/polish-widgets/SKILL.md +139 -0
  47. package/templates/skills/polish-widgets/references/patterns.md +269 -0
@@ -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":"AAAA,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAGlC,OAAO,EACL,oBAAoB,EACpB,sBAAsB,EACtB,eAAe,GAChB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EACL,QAAQ,EACR,iBAAiB,EACjB,eAAe,EACf,iBAAiB,GAClB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,WAAW,EAAiB,MAAM,eAAe,CAAC;AAC3D,OAAO,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACjE,OAAO,EAGL,eAAe,GAChB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClE,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAE/C,qEAAqE;AACrE,MAAM,sBAAsB,GAAG,KAAM,CAAC;AAEtC;;;;GAIG;AACH,MAAM,qBAAqB,GAAG,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC;AAgCpE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,GAAe,EACf,IAAgB;;IAEhB,MAAM,GAAG,GAAG,sBAAsB,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,EAAE;QAC3D,UAAU,EAAE,IAAI,CAAC,GAAG;KACrB,CAAC,CAAC;IAEH,MAAM,MAAM,GAAG,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAExC,MAAM,MAAM,GAAG,MAAM,cAAc,CAAC;QAClC,GAAG,EAAE,IAAI,CAAC,GAAG;QACb,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,OAAO,EAAE,MAAM,aAAN,MAAM,cAAN,MAAM,GAAI,SAAS;KAC7B,CAAC,CAAC;IACH,cAAc,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IAE5B,MAAM,OAAO,GAAG,MAAM;QACpB,CAAC,CAAC,MAAM,YAAY,CAAC,GAAG,EAAE,MAAM,CAAC;QACjC,CAAC,CAAC;YACE,aAAa,EAAE,KAAK;YACpB,IAAI,EAAE,4BAA4B,GAAG,CAAC,IAAI,eAAe;SAC1D,CAAC;IAEN,wEAAwE;IACxE,yEAAyE;IACzE,0EAA0E;IAC1E,uEAAuE;IACvE,2EAA2E;IAC3E,oBAAoB;IACpB,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,MAAA,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,mCAAI,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAElE,MAAM,OAAO,GAAG,iBAAiB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAE5C,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC;IAEpE,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;QACf,IAAI,CAAC,OAAO,CAAC,aAAa,IAAI,CAAC,IAAI,EAAE,CAAC;YACpC,MAAM,iBAAiB,CACrB,mDAAmD,GAAG,CAAC,IAAI,GAAG,EAC9D,4BAA4B,GAAG,CAAC,IAAI,cAAc,CACnD,CAAC;QACJ,CAAC;QAED,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,MAAM,iBAAiB,CACrB,2BAA2B,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,EACpD,qEAAqE,CACtE,CAAC;QACJ,CAAC;QAED,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;YACpB,MAAM,iBAAiB,CACrB,kDAAkD,EAClD,8DAA8D;gBAC5D,8CAA8C,CACjD,CAAC;QACJ,CAAC;QAED,OAAO,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;IAC3C,CAAC;IAED,IAAI,WAAW,GAA+B,IAAI,CAAC;IAEnD,IAAI,CAAA,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,MAAM,KAAI,OAAO,CAAC,aAAa,IAAI,IAAI,EAAE,CAAC;QACrD,WAAW,GAAG,MAAM,kBAAkB,CAAC,GAAG,EAAE,IAAI,EAAE,OAAO,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5E,CAAC;SAAM,IAAI,CAAA,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,MAAM,KAAI,CAAC,OAAO,CAAC,aAAa,EAAE,CAAC;QACrD,GAAG,CAAC,GAAG,CACL,4BAA4B,GAAG,CAAC,IAAI,mCAAmC;YACrE,CAAC,MAAA,OAAO,CAAC,IAAI,mCAAI,EAAE,CAAC,CACvB,CAAC;IACJ,CAAC;SAAM,IAAI,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;QACtC,GAAG,CAAC,GAAG,CACL,sEAAsE;YACpE,mEAAmE;YACnE,2BAA2B,CAC9B,CAAC;IACJ,CAAC;IAED,OAAO,GAAG,CAAC,OAAO,6CAEd,IAAI,EAAE,GAAG,CAAC,IAAI,IACX,OAAO,KACV,eAAe,EAAE,GAAG,CAAC,KAAK,CAAC,QAAQ,EACnC,OAAO;QACP,WAAW,KACR,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,GAE1C,YAAY,CACV,GAAG,CAAC,IAAI,EACR,OAAO,EACP,OAAO,EACP,WAAW,EACX,MAAA,IAAI,CAAC,QAAQ,mCAAI,KAAK,CACvB,CACF,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,KAAK,UAAU,QAAQ,CACrB,GAAe,EACf,GAA8C,EAC9C,OAAsB,EACtB,IAAgB;;IAEhB,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAC9B,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,uEAAuE;QACvE,gDAAgD;QAChD,MAAM,iBAAiB,CACrB,kDAAkD,EAClD,qEAAqE,CACtE,CAAC;IACJ,CAAC;IAED,MAAM,aAAa,GAAG,kBAAkB,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACtD,MAAM,KAAK,GAAG,MAAA,IAAI,CAAC,KAAK,mCAAI,YAAY,CAAC;IAEzC,GAAG,CAAC,GAAG,CACL,oBAAoB,OAAO,CAAC,IAAI,IAAI,aAAa,OAAO,GAAG,CAAC,IAAI,GAAG;QACjE,kBAAkB,sBAAsB,GAAG,IAAI,KAAK,CACvD,CAAC;IAEF,SAAS,CAAC;QACR,qEAAqE;QACrE,wEAAwE;QACxE,sEAAsE;QACtE,wEAAwE;QACxE,uEAAuE;QACvE,oEAAoE;QACpE,oEAAoE;QACpE,iEAAiE;QACjE,sEAAsE;QACtE,gEAAgE;QAChE,MAAM,OAAO,GAAG,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAEzC,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,MAAM,iBAAiB,CACrB,mBAAmB,GAAG,CAAC,IAAI,qCAAqC;gBAC9D,6BAA6B,EAC/B,4BAA4B,GAAG,CAAC,IAAI,cAAc,CACnD,CAAC;QACJ,CAAC;QAED,MAAM,YAAY,GAAG,MAAM,kBAAkB,CAC3C,GAAG,EACH,OAAO,EACP,MAAM,EACN,UAAU,CACX,CAAC;QACF,MAAM,OAAO,GAAG,WAAW,CAAC,YAAY,EAAE,OAAO,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC;QAEvE,IAAI,OAAO,IAAI,qBAAqB,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YACxD,OAAO,kBAAkB,CAAC,GAAG,EAAE,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QACxD,CAAC;QAED,GAAG,CAAC,GAAG,CACL,WAAW,OAAO,CAAC,IAAI,IAAI,aAAa,MAAM;YAC5C,GAAG,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,iBAAiB,aAAa,CAC9D,CAAC;QAEF,MAAM,KAAK,CAAC,sBAAsB,CAAC,CAAC;IACtC,CAAC;AACH,CAAC;AAED,SAAS,WAAW,CAClB,YAAiC,EACjC,IAAY,EACZ,OAAe;;IAEf,MAAM,WAAW,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;IAC9D,OAAO,MAAA,WAAW,aAAX,WAAW,uBAAX,WAAW,CAAE,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,KAAK,OAAO,CAAC,mCAAI,IAAI,CAAC;AAC1E,CAAC;AAED,SAAS,kBAAkB,CACzB,GAAe,EACf,IAAY,EACZ,OAA0B;;IAE1B,MAAM,IAAI,GAAG;QACX,IAAI;QACJ,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,UAAU,EAAE,OAAO,CAAC,UAAU;QAC9B,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,QAAQ,EAAE,MAAA,OAAO,CAAC,QAAQ,mCAAI,EAAE;QAChC,MAAM,EAAE,MAAA,OAAO,CAAC,MAAM,mCAAI,EAAE;KAC7B,CAAC;IAEF,IAAI,OAAO,CAAC,KAAK,KAAK,UAAU,EAAE,CAAC;QACjC,OAAO,GAAG,CAAC,IAAI,CACb,eAAe,CACb,GAAG,IAAI,IAAI,OAAO,CAAC,OAAO,4BAA4B;YACpD,IAAI,MAAA,IAAI,CAAC,UAAU,mCAAI,CAAC,SAAS,IAAI,CAAC,UAAU,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,IAAI,EACvE,iEAAiE;YAC/D,uCAAuC,CAC1C,EACD,IAAI,CACL,CAAC;IACJ,CAAC;IAED,OAAO,GAAG,CAAC,OAAO,CAChB,IAAI,EACJ,mBAAmB,IAAI,IAAI,OAAO,CAAC,OAAO,YAAY,OAAO,CAAC,KAAK,GAAG;QACpE,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,SAAS,WAAW,IAAI,CAAC,SAAS,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,CAC/F,CAAC;AACJ,CAAC;AAED,SAAS,YAAY,CAAC,EAAU;IAC9B,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;IAC1B,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;GAKG;AACH,KAAK,UAAU,YAAY,CACzB,GAA8C,EAC9C,MAAqB;IAErB,IAAI,CAAC;QACH,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,eAAe,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QAEjE,OAAO;YACL,aAAa,EAAE,IAAI;YACnB,IAAI,EAAE,EAAE,EAAE,EAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,KAAK,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,EAAE;YACrE,SAAS,EAAE,OAAO,CAAC,SAAS;SAC7B,CAAC;IACJ,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,KAAK,YAAY,QAAQ,IAAI,KAAK,CAAC,IAAI,KAAK,WAAW,CAAC,aAAa,EAAE,CAAC;YAC1E,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC;QACpD,CAAC;QAED,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,SAAS,iBAAiB,CAAC,GAAW;;IACpC,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAErC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,sBAAsB,CAAC,CAAC,EAAE,CAAC;QAClE,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,MAAM,GAAG,WAAW,CAAC,UAAU,CAAC,CAAC;IAEvC,OAAO;QACL,GAAG,EAAE,MAAM,CAAC,UAAU;QACtB,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,IAAI;QACzB,MAAM,EAAE,MAAA,MAAM,CAAC,OAAO,CAAC,MAAM,mCAAI,IAAI;KACtC,CAAC;AACJ,CAAC;AAED,uEAAuE;AACvE,KAAK,UAAU,kBAAkB,CAC/B,GAA8C,EAC9C,OAAsB,EACtB,MAAc,EACd,MAAmB;IAEnB,MAAM,KAAK,GAAG,MAAM,eAAe,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,WAAW,EAAE,MAAM,EAAE;QACzE,MAAM;KACP,CAAC,CAAC;IAEH,IAAI,KAAK,CAAC,MAAM,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC,YAAY,CAAC;IAErD,MAAM,SAAS,GAAG,MAAM,oBAAoB,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IAC3D,MAAM,MAAM,GAAG,MAAM,eAAe,CAClC,GAAG,CAAC,IAAI,EACR,SAAS,CAAC,WAAW,EACrB,MAAM,EACN;QACE,MAAM;KACP,CACF,CAAC;IAEF,IAAI,MAAM,CAAC,MAAM,KAAK,IAAI;QAAE,OAAO,MAAM,CAAC,YAAY,CAAC;IAEvD,0EAA0E;IAC1E,4EAA4E;IAC5E,4EAA4E;IAC5E,MAAM,iBAAiB,CACrB,mBAAmB,GAAG,CAAC,IAAI,2CAA2C,EACtE,4BAA4B,GAAG,CAAC,IAAI,cAAc,CACnD,CAAC;AACJ,CAAC;AAED,SAAS,YAAY,CACnB,IAAY,EACZ,OAAsB,EACtB,OAA6B,EAC7B,WAAuC,EACvC,YAAqB;;IAErB,MAAM,KAAK,GAAa,EAAE,CAAC;IAE3B,KAAK,CAAC,IAAI,CACR,OAAO,CAAC,aAAa;QACnB,CAAC,CAAC,wBAAwB,IAAI,OAC1B,MAAA,MAAA,OAAO,CAAC,IAAI,0CAAE,KAAK,mCAAI,MAAA,OAAO,CAAC,IAAI,0CAAE,EACvC,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,qBAAqB,OAAO,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG;QAC1E,CAAC,CAAC,4BAA4B,IAAI,GAAG,CACxC,CAAC;IAEF,IAAI,OAAO,EAAE,CAAC;QACZ,KAAK,CAAC,IAAI,CACR,cAAc,OAAO,CAAC,IAAI,OAAO,OAAO,CAAC,GAAG,EAAE;YAC5C,CAAC,OAAO,CAAC,MAAM;gBACb,CAAC,CAAC,aAAa,OAAO,CAAC,MAAM,IAAI;gBACjC,CAAC,CAAC,6BAA6B,CAAC,CACrC,CAAC;IACJ,CAAC;SAAM,CAAC;QACN,KAAK,CAAC,IAAI,CAAC,wCAAwC,CAAC,CAAC;IACvD,CAAC;IAED,IAAI,WAAW,EAAE,CAAC;QAChB,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC7B,KAAK,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAC;QACzC,CAAC;QAED,KAAK,MAAM,WAAW,IAAI,WAAW,EAAE,CAAC;YACtC,KAAK,MAAM,OAAO,IAAI,WAAW,CAAC,QAAQ,EAAE,CAAC;gBAC3C,KAAK,CAAC,IAAI,CACR,kBAAkB,WAAW,CAAC,IAAI,IAAI,OAAO,CAAC,OAAO,KAAK;oBACxD,GAAG,OAAO,CAAC,KAAK,GACd,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,eAAe,OAAO,CAAC,WAAW,GAAG,CAAC,CAAC,CAAC,EAChE,GAAG,gBAAgB,CAAC,OAAO,CAAC,EAAE,CACjC,CAAC;gBAEF,IAAI,YAAY,EAAE,CAAC;oBACjB,KAAK,MAAM,OAAO,IAAI,MAAA,OAAO,CAAC,QAAQ,mCAAI,EAAE,EAAE,CAAC;wBAC7C,KAAK,CAAC,IAAI,CACR,QAAQ,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,KAAK,KAAK,OAAO,CAAC,OAAO,GAAG;4BAC/D,KAAK,OAAO,CAAC,IAAI,EAAE,CACtB,CAAC;oBACJ,CAAC;gBACH,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED,iFAAiF;AACjF,SAAS,gBAAgB,CAAC,OAA0B;;IAClD,IAAI,OAAO,CAAC,UAAU,KAAK,IAAI,IAAI,OAAO,CAAC,SAAS,KAAK,IAAI;QAAE,OAAO,EAAE,CAAC;IAEzE,MAAM,UAAU,GAAG,MAAA,OAAO,CAAC,UAAU,mCAAI,CAAC,CAAC;IAC3C,MAAM,SAAS,GAAG,MAAA,OAAO,CAAC,SAAS,mCAAI,CAAC,CAAC;IAEzC,OAAO,CACL,KAAK,UAAU,SAAS,UAAU,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,IAAI;QACvD,GAAG,SAAS,WAAW,SAAS,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG,CACrD,CAAC;AACJ,CAAC","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"]}
@@ -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: path.join(loaded.projectDir, 'node_modules', '@ekanos', 'cli', 'templates'),
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
- }, successSummary(installedKeys, skippedKeys, deltas, regenerated));
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
- if (!fs.existsSync(destPath))
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
- fs.writeFileSync(destPath, newRendered);
398
- updated.push(dest);
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.');