@ekanos/cli 0.1.0 → 0.1.2

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 +35 -4
  2. package/dist/auth/session.d.ts +25 -7
  3. package/dist/auth/session.js +88 -15
  4. package/dist/auth/session.js.map +1 -1
  5. package/dist/commands/init.d.ts +8 -1
  6. package/dist/commands/init.js +17 -3
  7. package/dist/commands/init.js.map +1 -1
  8. package/dist/commands/login.d.ts +2 -0
  9. package/dist/commands/login.js +7 -0
  10. package/dist/commands/login.js.map +1 -1
  11. package/dist/commands/logout.d.ts +2 -0
  12. package/dist/commands/logout.js +3 -1
  13. package/dist/commands/logout.js.map +1 -1
  14. package/dist/commands/publish.d.ts +26 -0
  15. package/dist/commands/publish.js +196 -0
  16. package/dist/commands/publish.js.map +1 -0
  17. package/dist/commands/status.d.ts +28 -0
  18. package/dist/commands/status.js +146 -0
  19. package/dist/commands/status.js.map +1 -0
  20. package/dist/commands/validate.d.ts +3 -0
  21. package/dist/commands/validate.js +6 -69
  22. package/dist/commands/validate.js.map +1 -1
  23. package/dist/commands/whoami.d.ts +2 -0
  24. package/dist/commands/whoami.js +3 -1
  25. package/dist/commands/whoami.js.map +1 -1
  26. package/dist/errors.d.ts +2 -0
  27. package/dist/errors.js +9 -0
  28. package/dist/errors.js.map +1 -1
  29. package/dist/index.js +41 -2
  30. package/dist/index.js.map +1 -1
  31. package/dist/pack.d.ts +14 -0
  32. package/dist/pack.js +157 -0
  33. package/dist/pack.js.map +1 -0
  34. package/dist/project.d.ts +20 -22
  35. package/dist/project.js +43 -1
  36. package/dist/project.js.map +1 -1
  37. package/dist/publish-api.d.ts +103 -0
  38. package/dist/publish-api.js +202 -0
  39. package/dist/publish-api.js.map +1 -0
  40. package/dist/validate-findings.d.ts +10 -0
  41. package/dist/validate-findings.js +78 -0
  42. package/dist/validate-findings.js.map +1 -0
  43. package/package.json +2 -2
  44. package/templates/AGENTS.md.tmpl +247 -0
  45. package/templates/CLAUDE.md.tmpl +6 -0
  46. package/templates/claude-skill.md.tmpl +53 -0
  47. package/templates/package.json.tmpl +1 -0
@@ -0,0 +1,196 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { refreshStoredSession, requireSession, resolveAuthEnvironment, } from '../auth/session.js';
4
+ import { gateFailedError, preconditionError, validationError } from '../errors.js';
5
+ import { packProject } from '../pack.js';
6
+ import { loadProject, serializeProject } from '../project.js';
7
+ import { MAX_MANIFEST_BYTES, stillUnauthorized, submitArchive, } from '../publish-api.js';
8
+ import { collectAllFindings } from '../validate-findings.js';
9
+ /**
10
+ * `publish` — pack the project and submit it to a Fusion deployment.
11
+ *
12
+ * The submission gate is the SAME findings pass `validate` runs (one
13
+ * implementation, `validate-findings.ts`): any error-severity finding refuses
14
+ * the publish with GATE_FAILED (exit 10) and the findings in `data`, so an
15
+ * agent can branch on the code and fix-then-retry without re-running validate.
16
+ *
17
+ * Authorization is server-side — the dev seat on the target source — and the
18
+ * stored session is refreshed once on a 401, mirroring `whoami`. On the first
19
+ * successful publish the chosen source slug is persisted into `ekanos.json`
20
+ * so later publishes need no `--source`.
21
+ */
22
+ export async function runPublish(ctx, args) {
23
+ const env = resolveAuthEnvironment(ctx, args.host, args.env, {
24
+ projectDir: args.dir,
25
+ });
26
+ const session = requireSession(env.store, env.host);
27
+ const loaded = loadProject(args.dir);
28
+ const source = resolveSourceSlug(args.source, loaded);
29
+ const version = readProjectVersion(loaded.projectDir);
30
+ const slug = loaded.primary.slug;
31
+ ctx.log(`Validating ${slug} before publishing…`);
32
+ const findings = await collectAllFindings(loaded);
33
+ const errorCount = findings.filter((f) => f.severity === 'error').length;
34
+ if (errorCount > 0) {
35
+ return ctx.fail(gateFailedError(`Refusing to publish: ${errorCount} validation finding` +
36
+ `${errorCount === 1 ? '' : 's'}.`, 'Resolve the error-severity findings in data.findings (they are ' +
37
+ 'exactly what "ekanos validate" reports), then re-run ' +
38
+ '"ekanos publish".'), { slug, version, findings });
39
+ }
40
+ ctx.log(`Packing ${slug}@${version}…`);
41
+ const packed = packProject(loaded.projectDir);
42
+ const manifest = buildManifest(loaded, packed.files);
43
+ ctx.log(`Submitting to ${env.host} (source "${source}")…`);
44
+ const receipt = await submitWithOneRefresh(env, session, {
45
+ source,
46
+ slug,
47
+ version,
48
+ manifest,
49
+ archive: packed.archive,
50
+ });
51
+ const persisted = persistProjectFields(loaded, source, env.host);
52
+ const persistedNote = [
53
+ ...(persisted.source ? [`"source": "${source}"`] : []),
54
+ ...(persisted.host ? [`"host": "${env.host}"`] : []),
55
+ ];
56
+ const overrideNote = persisted.hostOverride
57
+ ? ` NOTE: this publish went to ${env.host}, but ekanos.json keeps ` +
58
+ `"host": "${persisted.hostOverride}" — a one-off override never ` +
59
+ `replaces the saved host; edit ekanos.json to change it.`
60
+ : '';
61
+ return ctx.succeed({
62
+ host: env.host,
63
+ source,
64
+ slug: receipt.slug,
65
+ version: receipt.version,
66
+ submissionId: receipt.submissionId,
67
+ state: receipt.state,
68
+ archiveSha256: receipt.archiveSha256,
69
+ files: packed.files,
70
+ sourcePersisted: persisted.source,
71
+ hostPersisted: persisted.host,
72
+ hostOverride: persisted.hostOverride,
73
+ }, `publish: submitted ${receipt.slug}@${receipt.version} to ${env.host} ` +
74
+ `(source "${source}", state "${receipt.state}").` +
75
+ (persistedNote.length > 0
76
+ ? ` Saved ${persistedNote.join(' and ')} to ekanos.json — future ` +
77
+ `publishes won't need the flag${persistedNote.length === 1 ? '' : 's'}.`
78
+ : '') +
79
+ overrideNote);
80
+ }
81
+ async function submitWithOneRefresh(env, session, payload) {
82
+ const first = await submitArchive(env.host, session.accessToken, payload);
83
+ if (first.status === 'ok')
84
+ return first.receipt;
85
+ // The access token no longer authenticates — an ordinary event, not an
86
+ // error. One refresh, one retry; a second 401 is authoritative.
87
+ const refreshed = await refreshStoredSession(env, session);
88
+ const second = await submitArchive(env.host, refreshed.accessToken, payload);
89
+ if (second.status === 'ok')
90
+ return second.receipt;
91
+ return stillUnauthorized(env.host);
92
+ }
93
+ function resolveSourceSlug(flag, loaded) {
94
+ const source = flag !== null && flag !== void 0 ? flag : loaded.project.source;
95
+ if (!source || source.trim().length === 0) {
96
+ throw validationError('No Fusion source given for the submission.', 'Pass "--source <slug>" (your operator names the source), or set ' +
97
+ '"source": "<slug>" in ekanos.json. A successful publish saves it ' +
98
+ 'there for you.');
99
+ }
100
+ return source.trim();
101
+ }
102
+ /**
103
+ * Persist the chosen source AND host into ekanos.json after a SUCCESSFUL
104
+ * publish, so the next publish — and every other host-taking verb — needs no
105
+ * flag. Returns what was written. A write failure is reported but never fails
106
+ * the command — the submission already landed.
107
+ *
108
+ * `publish` is the only verb that writes ekanos.json: it is the one verb that
109
+ * proves the (host, source) pair actually works, and it always runs inside a
110
+ * project. `login` deliberately never writes — it is routinely run outside
111
+ * one, and the credential store's sole-host fallback covers it.
112
+ *
113
+ * `host` persists on FIRST WRITE ONLY. It decides where every future verb
114
+ * sends the bearer token and the source archive, so a one-off `--host`
115
+ * override (a staging test, say) must not silently become the project's
116
+ * sticky default; when the override differs from the stored host, the stored
117
+ * value is kept and the difference is called out in the result instead.
118
+ * Changing it is an explicit edit of ekanos.json. `source` keeps the simpler
119
+ * always-update semantics — it is scoped BY the host, and updating it is the
120
+ * documented way to repoint a project.
121
+ */
122
+ function persistProjectFields(loaded, source, host) {
123
+ const wants = {
124
+ source: loaded.project.source !== source,
125
+ host: loaded.project.host === undefined,
126
+ };
127
+ const hostOverride = loaded.project.host !== undefined && loaded.project.host !== host
128
+ ? loaded.project.host
129
+ : null;
130
+ if (!wants.source && !wants.host) {
131
+ return { source: false, host: false, hostOverride };
132
+ }
133
+ try {
134
+ fs.writeFileSync(loaded.configPath, serializeProject(Object.assign(Object.assign({}, loaded.project), { source, host: wants.host ? host : loaded.project.host })));
135
+ return Object.assign(Object.assign({}, wants), { hostOverride });
136
+ }
137
+ catch (_a) {
138
+ // Diagnostics-only: the publish succeeded; a read-only ekanos.json just
139
+ // means the next publish needs the flags again.
140
+ return { source: false, host: false, hostOverride };
141
+ }
142
+ }
143
+ /**
144
+ * How many packed paths the manifest embeds before switching to a truncated
145
+ * list plus a `filesTruncated` total. The manifest is metadata — the archive
146
+ * itself is the payload — and the server caps it at MAX_MANIFEST_BYTES, so a
147
+ * many-small-files project must not be able to blow the cap with its file
148
+ * list alone.
149
+ */
150
+ const MANIFEST_FILE_LIST_CAP = 200;
151
+ /**
152
+ * The submission manifest: the parsed ekanos.json, the integration entries,
153
+ * and (a bounded prefix of) the packed file list. Refuses to build one over
154
+ * the server's byte cap — a clear VALIDATION error here beats the server's
155
+ * generic 400 after a 4 MiB upload.
156
+ */
157
+ function buildManifest(loaded, files) {
158
+ const manifest = JSON.stringify(Object.assign({ project: loaded.project, integrations: loaded.integrations.map((i) => ({
159
+ slug: i.slug,
160
+ entry: i.entry,
161
+ })), files: files.slice(0, MANIFEST_FILE_LIST_CAP) }, (files.length > MANIFEST_FILE_LIST_CAP
162
+ ? { filesTruncated: files.length }
163
+ : {})));
164
+ const bytes = Buffer.byteLength(manifest, 'utf8');
165
+ if (bytes > MAX_MANIFEST_BYTES) {
166
+ throw validationError(`The submission manifest is ${bytes} bytes, over the ` +
167
+ `${MAX_MANIFEST_BYTES}-byte server cap.`, 'The manifest embeds your ekanos.json — trim oversized fields there ' +
168
+ '(sourceGlobs is the usual culprit), then re-run "ekanos publish".');
169
+ }
170
+ return manifest;
171
+ }
172
+ const SEMVER_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
173
+ /**
174
+ * The submitted version is the project's own `package.json#version` — one
175
+ * source of truth, the same field npm would publish.
176
+ */
177
+ function readProjectVersion(projectDir) {
178
+ const manifestPath = path.join(projectDir, 'package.json');
179
+ let parsed;
180
+ try {
181
+ parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
182
+ }
183
+ catch (error) {
184
+ throw preconditionError(`Could not read ${manifestPath}: ${error instanceof Error ? error.message : String(error)}`, `Ensure the project has a valid package.json — its "version" field is ` +
185
+ `what gets submitted.`);
186
+ }
187
+ const version = typeof parsed === 'object' && parsed !== null
188
+ ? parsed.version
189
+ : undefined;
190
+ if (typeof version !== 'string' || !SEMVER_PATTERN.test(version)) {
191
+ throw validationError(`package.json "version" is ${typeof version === 'string' ? `"${version}"` : 'missing'}, which is not a semver version.`, `Set "version" in ${manifestPath} to a semver string like "0.1.0", ` +
192
+ `then re-run "ekanos publish".`);
193
+ }
194
+ return version;
195
+ }
196
+ //# sourceMappingURL=publish.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"publish.js","sourceRoot":"","sources":["../../src/commands/publish.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 requireSession,\n resolveAuthEnvironment,\n} from '../auth/session';\nimport type { CliContext } from '../context';\nimport { gateFailedError, preconditionError, validationError } from '../errors';\nimport type { ExitCode } from '../exit-codes';\nimport { packProject } from '../pack';\nimport { type LoadedProject, loadProject, serializeProject } from '../project';\nimport {\n MAX_MANIFEST_BYTES,\n stillUnauthorized,\n submitArchive,\n} from '../publish-api';\nimport { collectAllFindings } from '../validate-findings';\n\nexport interface PublishArgs {\n host?: string;\n /** Fusion source slug. Falls back to ekanos.json's \"source\" field. */\n source?: string;\n /** Project directory (contains ekanos.json). Defaults to cwd. */\n dir: string;\n /** Accepted for non-interactive ergonomics; publish never prompts regardless. */\n yes: boolean;\n env: Record<string, string | undefined>;\n}\n\n/**\n * `publish` — pack the project and submit it to a Fusion deployment.\n *\n * The submission gate is the SAME findings pass `validate` runs (one\n * implementation, `validate-findings.ts`): any error-severity finding refuses\n * the publish with GATE_FAILED (exit 10) and the findings in `data`, so an\n * agent can branch on the code and fix-then-retry without re-running validate.\n *\n * Authorization is server-side — the dev seat on the target source — and the\n * stored session is refreshed once on a 401, mirroring `whoami`. On the first\n * successful publish the chosen source slug is persisted into `ekanos.json`\n * so later publishes need no `--source`.\n */\nexport async function runPublish(\n ctx: CliContext,\n args: PublishArgs,\n): Promise<ExitCode> {\n const env = resolveAuthEnvironment(ctx, args.host, args.env, {\n projectDir: args.dir,\n });\n const session = requireSession(env.store, env.host);\n\n const loaded = loadProject(args.dir);\n const source = resolveSourceSlug(args.source, loaded);\n const version = readProjectVersion(loaded.projectDir);\n const slug = loaded.primary.slug;\n\n ctx.log(`Validating ${slug} before publishing…`);\n\n const findings = await collectAllFindings(loaded);\n const errorCount = findings.filter((f) => f.severity === 'error').length;\n\n if (errorCount > 0) {\n return ctx.fail(\n gateFailedError(\n `Refusing to publish: ${errorCount} validation finding` +\n `${errorCount === 1 ? '' : 's'}.`,\n 'Resolve the error-severity findings in data.findings (they are ' +\n 'exactly what \"ekanos validate\" reports), then re-run ' +\n '\"ekanos publish\".',\n ),\n { slug, version, findings },\n );\n }\n\n ctx.log(`Packing ${slug}@${version}…`);\n\n const packed = packProject(loaded.projectDir);\n const manifest = buildManifest(loaded, packed.files);\n\n ctx.log(`Submitting to ${env.host} (source \"${source}\")…`);\n\n const receipt = await submitWithOneRefresh(env, session, {\n source,\n slug,\n version,\n manifest,\n archive: packed.archive,\n });\n\n const persisted = persistProjectFields(loaded, source, env.host);\n\n const persistedNote = [\n ...(persisted.source ? [`\"source\": \"${source}\"`] : []),\n ...(persisted.host ? [`\"host\": \"${env.host}\"`] : []),\n ];\n\n const overrideNote = persisted.hostOverride\n ? ` NOTE: this publish went to ${env.host}, but ekanos.json keeps ` +\n `\"host\": \"${persisted.hostOverride}\" — a one-off override never ` +\n `replaces the saved host; edit ekanos.json to change it.`\n : '';\n\n return ctx.succeed(\n {\n host: env.host,\n source,\n slug: receipt.slug,\n version: receipt.version,\n submissionId: receipt.submissionId,\n state: receipt.state,\n archiveSha256: receipt.archiveSha256,\n files: packed.files,\n sourcePersisted: persisted.source,\n hostPersisted: persisted.host,\n hostOverride: persisted.hostOverride,\n },\n `publish: submitted ${receipt.slug}@${receipt.version} to ${env.host} ` +\n `(source \"${source}\", state \"${receipt.state}\").` +\n (persistedNote.length > 0\n ? ` Saved ${persistedNote.join(' and ')} to ekanos.json — future ` +\n `publishes won't need the flag${persistedNote.length === 1 ? '' : 's'}.`\n : '') +\n overrideNote,\n );\n}\n\nasync function submitWithOneRefresh(\n env: ReturnType<typeof resolveAuthEnvironment>,\n session: StoredSession,\n payload: Parameters<typeof submitArchive>[2],\n) {\n const first = await submitArchive(env.host, session.accessToken, payload);\n\n if (first.status === 'ok') return first.receipt;\n\n // The access token no longer authenticates — an ordinary event, not an\n // error. One refresh, one retry; a second 401 is authoritative.\n const refreshed = await refreshStoredSession(env, session);\n const second = await submitArchive(env.host, refreshed.accessToken, payload);\n\n if (second.status === 'ok') return second.receipt;\n\n return stillUnauthorized(env.host);\n}\n\nfunction resolveSourceSlug(\n flag: string | undefined,\n loaded: LoadedProject,\n): string {\n const source = flag ?? loaded.project.source;\n\n if (!source || source.trim().length === 0) {\n throw validationError(\n 'No Fusion source given for the submission.',\n 'Pass \"--source <slug>\" (your operator names the source), or set ' +\n '\"source\": \"<slug>\" in ekanos.json. A successful publish saves it ' +\n 'there for you.',\n );\n }\n\n return source.trim();\n}\n\n/**\n * Persist the chosen source AND host into ekanos.json after a SUCCESSFUL\n * publish, so the next publish — and every other host-taking verb — needs no\n * flag. Returns what was written. A write failure is reported but never fails\n * the command — the submission already landed.\n *\n * `publish` is the only verb that writes ekanos.json: it is the one verb that\n * proves the (host, source) pair actually works, and it always runs inside a\n * project. `login` deliberately never writes — it is routinely run outside\n * one, and the credential store's sole-host fallback covers it.\n *\n * `host` persists on FIRST WRITE ONLY. It decides where every future verb\n * sends the bearer token and the source archive, so a one-off `--host`\n * override (a staging test, say) must not silently become the project's\n * sticky default; when the override differs from the stored host, the stored\n * value is kept and the difference is called out in the result instead.\n * Changing it is an explicit edit of ekanos.json. `source` keeps the simpler\n * always-update semantics — it is scoped BY the host, and updating it is the\n * documented way to repoint a project.\n */\nfunction persistProjectFields(\n loaded: LoadedProject,\n source: string,\n host: string,\n): { source: boolean; host: boolean; hostOverride: string | null } {\n const wants = {\n source: loaded.project.source !== source,\n host: loaded.project.host === undefined,\n };\n\n const hostOverride =\n loaded.project.host !== undefined && loaded.project.host !== host\n ? loaded.project.host\n : null;\n\n if (!wants.source && !wants.host) {\n return { source: false, host: false, hostOverride };\n }\n\n try {\n fs.writeFileSync(\n loaded.configPath,\n serializeProject({\n ...loaded.project,\n source,\n host: wants.host ? host : loaded.project.host,\n }),\n );\n\n return { ...wants, hostOverride };\n } catch {\n // Diagnostics-only: the publish succeeded; a read-only ekanos.json just\n // means the next publish needs the flags again.\n return { source: false, host: false, hostOverride };\n }\n}\n\n/**\n * How many packed paths the manifest embeds before switching to a truncated\n * list plus a `filesTruncated` total. The manifest is metadata — the archive\n * itself is the payload — and the server caps it at MAX_MANIFEST_BYTES, so a\n * many-small-files project must not be able to blow the cap with its file\n * list alone.\n */\nconst MANIFEST_FILE_LIST_CAP = 200;\n\n/**\n * The submission manifest: the parsed ekanos.json, the integration entries,\n * and (a bounded prefix of) the packed file list. Refuses to build one over\n * the server's byte cap — a clear VALIDATION error here beats the server's\n * generic 400 after a 4 MiB upload.\n */\nfunction buildManifest(loaded: LoadedProject, files: string[]): string {\n const manifest = JSON.stringify({\n project: loaded.project,\n integrations: loaded.integrations.map((i) => ({\n slug: i.slug,\n entry: i.entry,\n })),\n files: files.slice(0, MANIFEST_FILE_LIST_CAP),\n ...(files.length > MANIFEST_FILE_LIST_CAP\n ? { filesTruncated: files.length }\n : {}),\n });\n\n const bytes = Buffer.byteLength(manifest, 'utf8');\n\n if (bytes > MAX_MANIFEST_BYTES) {\n throw validationError(\n `The submission manifest is ${bytes} bytes, over the ` +\n `${MAX_MANIFEST_BYTES}-byte server cap.`,\n 'The manifest embeds your ekanos.json — trim oversized fields there ' +\n '(sourceGlobs is the usual culprit), then re-run \"ekanos publish\".',\n );\n }\n\n return manifest;\n}\n\nconst SEMVER_PATTERN =\n /^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$/;\n\n/**\n * The submitted version is the project's own `package.json#version` — one\n * source of truth, the same field npm would publish.\n */\nfunction readProjectVersion(projectDir: string): string {\n const manifestPath = path.join(projectDir, 'package.json');\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));\n } catch (error) {\n throw preconditionError(\n `Could not read ${manifestPath}: ${\n error instanceof Error ? error.message : String(error)\n }`,\n `Ensure the project has a valid package.json — its \"version\" field is ` +\n `what gets submitted.`,\n );\n }\n\n const version =\n typeof parsed === 'object' && parsed !== null\n ? (parsed as { version?: unknown }).version\n : undefined;\n\n if (typeof version !== 'string' || !SEMVER_PATTERN.test(version)) {\n throw validationError(\n `package.json \"version\" is ${\n typeof version === 'string' ? `\"${version}\"` : 'missing'\n }, which is not a semver version.`,\n `Set \"version\" in ${manifestPath} to a semver string like \"0.1.0\", ` +\n `then re-run \"ekanos publish\".`,\n );\n }\n\n return version;\n}\n"]}
@@ -0,0 +1,28 @@
1
+ import type { CliContext } from '../context.js';
2
+ import { type ExitCode } from '../exit-codes.js';
3
+ export interface StatusArgs {
4
+ host?: string;
5
+ /** Project directory (may contain ekanos.json). Defaults to cwd. */
6
+ dir: string;
7
+ env: Record<string, string | undefined>;
8
+ }
9
+ /**
10
+ * `status` — the read verb: where am I pointed, who am I, and what has this
11
+ * project submitted?
12
+ *
13
+ * Three sections, each degrading gracefully rather than failing:
14
+ *
15
+ * - **session** — whoami's server-side identity check, but an absent or dead
16
+ * session reports `authenticated: false` with a hint instead of exit 4. A
17
+ * partner asking "what is my state?" must get an answer, not an error whose
18
+ * answer it is.
19
+ * - **project** — the ekanos.json in `--dir` (default cwd), when there is
20
+ * one. No project is an ordinary state for `status`, never an error.
21
+ * - **submissions** — `GET /api/partner/submissions?source=…` for the
22
+ * project's resolved source, only when both a session and a source exist.
23
+ * Server errors here DO map through the taxonomy: an unreachable host or a
24
+ * missing source is a fault to surface, not a state to absorb.
25
+ *
26
+ * Exit 0 whenever the report itself could be assembled.
27
+ */
28
+ export declare function runStatus(ctx: CliContext, args: StatusArgs): Promise<ExitCode>;
@@ -0,0 +1,146 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { refreshStoredSession, resolveAuthEnvironment, resolveIdentity, } from '../auth/session.js';
4
+ import { CliError, authRequiredError } from '../errors.js';
5
+ import { ERROR_CODES } from '../exit-codes.js';
6
+ import { EKANOS_CONFIG_FILENAME, loadProject } from '../project.js';
7
+ import { listSubmissions } from '../publish-api.js';
8
+ /**
9
+ * `status` — the read verb: where am I pointed, who am I, and what has this
10
+ * project submitted?
11
+ *
12
+ * Three sections, each degrading gracefully rather than failing:
13
+ *
14
+ * - **session** — whoami's server-side identity check, but an absent or dead
15
+ * session reports `authenticated: false` with a hint instead of exit 4. A
16
+ * partner asking "what is my state?" must get an answer, not an error whose
17
+ * answer it is.
18
+ * - **project** — the ekanos.json in `--dir` (default cwd), when there is
19
+ * one. No project is an ordinary state for `status`, never an error.
20
+ * - **submissions** — `GET /api/partner/submissions?source=…` for the
21
+ * project's resolved source, only when both a session and a source exist.
22
+ * Server errors here DO map through the taxonomy: an unreachable host or a
23
+ * missing source is a fault to surface, not a state to absorb.
24
+ *
25
+ * Exit 0 whenever the report itself could be assembled.
26
+ */
27
+ export async function runStatus(ctx, args) {
28
+ var _a, _b;
29
+ const env = resolveAuthEnvironment(ctx, args.host, args.env, {
30
+ projectDir: args.dir,
31
+ });
32
+ const stored = env.store.read(env.host);
33
+ const session = stored
34
+ ? await probeSession(env, stored)
35
+ : {
36
+ authenticated: false,
37
+ hint: `Run "ekanos login --host ${env.host}" to sign in.`,
38
+ };
39
+ // probeSession may have refreshed the session — persisting a NEW access
40
+ // token and ROTATING the refresh token on disk. Re-read before using it:
41
+ // the pre-probe object now holds a dead access token and an already-spent
42
+ // refresh token, and presenting those would turn "logged in, token was
43
+ // merely stale" into a hard exit-4 — the one failure this command promises
44
+ // never to produce.
45
+ const live = stored ? ((_a = env.store.read(env.host)) !== null && _a !== void 0 ? _a : stored) : null;
46
+ const project = readProjectReport(args.dir);
47
+ let submissions = null;
48
+ if ((project === null || project === void 0 ? void 0 : project.source) && session.authenticated && live) {
49
+ submissions = await listWithOneRefresh(env, live, project.source);
50
+ }
51
+ else if ((project === null || project === void 0 ? void 0 : project.source) && !session.authenticated) {
52
+ ctx.log(`ekanos: not logged in to ${env.host} — skipping the submission list. ` +
53
+ ((_b = session.hint) !== null && _b !== void 0 ? _b : ''));
54
+ }
55
+ else if (project && !project.source) {
56
+ ctx.log('ekanos: this project has no "source" in ekanos.json yet — the first ' +
57
+ '"ekanos publish --source <slug>" saves one, and status will list ' +
58
+ 'submissions from then on.');
59
+ }
60
+ return ctx.succeed(Object.assign(Object.assign({ host: env.host }, session), { credentialsPath: env.store.filePath, project,
61
+ submissions }), humanSummary(env.host, session, project, submissions));
62
+ }
63
+ /**
64
+ * whoami's identity resolution, with AUTH_REQUIRED absorbed into
65
+ * `authenticated: false`. Anything else — network, precondition, MFA mapped
66
+ * to auth by `fetchIdentity` — still propagates: "the host was unreachable"
67
+ * must never be reported as "you are logged out".
68
+ */
69
+ async function probeSession(env, stored) {
70
+ try {
71
+ const { identity, session } = await resolveIdentity(env, stored);
72
+ return {
73
+ authenticated: true,
74
+ user: { id: identity.id, email: identity.email, name: identity.name },
75
+ expiresAt: session.expiresAt,
76
+ };
77
+ }
78
+ catch (error) {
79
+ if (error instanceof CliError && error.code === ERROR_CODES.AUTH_REQUIRED) {
80
+ return { authenticated: false, hint: error.hint };
81
+ }
82
+ throw error;
83
+ }
84
+ }
85
+ /**
86
+ * Lenient project read: no ekanos.json means "not in a project", which is an
87
+ * answer, not an error. An ekanos.json that EXISTS but does not load keeps
88
+ * `loadProject`'s strict behaviour — a broken project file is a fault the
89
+ * partner needs named, whatever verb they ran.
90
+ */
91
+ function readProjectReport(dir) {
92
+ var _a;
93
+ const projectDir = path.resolve(dir);
94
+ if (!fs.existsSync(path.join(projectDir, EKANOS_CONFIG_FILENAME))) {
95
+ return null;
96
+ }
97
+ const loaded = loadProject(projectDir);
98
+ return {
99
+ dir: loaded.projectDir,
100
+ slug: loaded.primary.slug,
101
+ source: (_a = loaded.project.source) !== null && _a !== void 0 ? _a : null,
102
+ };
103
+ }
104
+ /** Mirrors publish's one-refresh-one-retry on a stale access token. */
105
+ async function listWithOneRefresh(env, session, source) {
106
+ const first = await listSubmissions(env.host, session.accessToken, source);
107
+ if (first.status === 'ok')
108
+ return first.integrations;
109
+ const refreshed = await refreshStoredSession(env, session);
110
+ const second = await listSubmissions(env.host, refreshed.accessToken, source);
111
+ if (second.status === 'ok')
112
+ return second.integrations;
113
+ // Authenticated a moment ago, unauthorized twice here: report the list as
114
+ // unavailable rather than the session as dead — probeSession already proved
115
+ // it. This is unreachable in practice; belt and braces for a racing revoke.
116
+ throw authRequiredError(`The session for ${env.host} could not authorize the submission list.`, `Run "ekanos login --host ${env.host}" and retry.`);
117
+ }
118
+ function humanSummary(host, session, project, submissions) {
119
+ var _a, _b, _c;
120
+ const lines = [];
121
+ lines.push(session.authenticated
122
+ ? `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})` : ''}.`
123
+ : `status: not logged in to ${host}.`);
124
+ if (project) {
125
+ lines.push(` project: ${project.slug} in ${project.dir}` +
126
+ (project.source
127
+ ? ` (source "${project.source}")`
128
+ : ' (no source configured yet)'));
129
+ }
130
+ else {
131
+ lines.push(' project: none (no ekanos.json here).');
132
+ }
133
+ if (submissions) {
134
+ if (submissions.length === 0) {
135
+ lines.push(' submissions: none yet.');
136
+ }
137
+ for (const integration of submissions) {
138
+ for (const version of integration.versions) {
139
+ lines.push(` submissions: ${integration.slug}@${version.version} — ` +
140
+ `${version.state}${version.submittedAt ? ` (submitted ${version.submittedAt})` : ''}`);
141
+ }
142
+ }
143
+ }
144
+ return lines.join('\n');
145
+ }
146
+ //# sourceMappingURL=status.js.map
@@ -0,0 +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';\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 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 },\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"]}
@@ -10,5 +10,8 @@ export interface ValidateArgs {
10
10
  * findings, and add project-level integrity checks (the vitest inline-SDK
11
11
  * check). Exit 0 with `findings: []` when clean; exit 3 when any error-severity
12
12
  * finding is present, with the findings carried in the envelope's `data`.
13
+ *
14
+ * The findings pass itself lives in `validate-findings.ts`, shared with
15
+ * `publish` so the publish gate cannot drift from what validate checks.
13
16
  */
14
17
  export declare function runValidate(ctx: CliContext, args: ValidateArgs): Promise<ExitCode>;
@@ -1,55 +1,22 @@
1
- import { collectCollisionFindings, collectDefinitionFindings, } from '@ekanos/integration-schema';
2
- import { preconditionError, validationError } from '../errors.js';
3
- import { loadDefinition } from '../load-definition.js';
1
+ import { validationError } from '../errors.js';
4
2
  import { loadProject } from '../project.js';
5
- import { collectProjectFindings } from '../project-checks.js';
3
+ import { collectAllFindings } from '../validate-findings.js';
6
4
  /**
7
5
  * `validate` — parse the declaration with the REAL zod schemas from
8
6
  * `@ekanos/integration-schema` (never a reimplementation), gather structured
9
7
  * findings, and add project-level integrity checks (the vitest inline-SDK
10
8
  * check). Exit 0 with `findings: []` when clean; exit 3 when any error-severity
11
9
  * finding is present, with the findings carried in the envelope's `data`.
10
+ *
11
+ * The findings pass itself lives in `validate-findings.ts`, shared with
12
+ * `publish` so the publish gate cannot drift from what validate checks.
12
13
  */
13
14
  export async function runValidate(ctx, args) {
14
15
  const loaded = loadProject(args.dir);
15
16
  const slugs = loaded.integrations.map((i) => i.slug).join(', ');
16
17
  ctx.log(`Validating ${loaded.integrations.length} integration` +
17
18
  `${loaded.integrations.length === 1 ? '' : 's'} (${slugs})…`);
18
- const findings = [];
19
- const definitions = [];
20
- for (const integration of loaded.integrations) {
21
- const result = await loadDefinition(integration.entryPath, loaded.projectDir);
22
- if (result.ok) {
23
- findings.push(...collectDefinitionFindings(result.definition, {
24
- file: integration.entryPath,
25
- }));
26
- findings.push(...collectSlugAgreementFindings(integration, result.definition));
27
- definitions.push(result.definition);
28
- continue;
29
- }
30
- if (result.kind === 'rejected') {
31
- // The module loaded but the SDK/schema rejected the definition at import
32
- // time — surface it as a validation finding rather than a crash.
33
- findings.push({
34
- check: 'definition.load',
35
- severity: 'error',
36
- file: integration.entryPath,
37
- message: result.message,
38
- hint: 'Fix the integration definition so defineIntegration() accepts it, ' +
39
- 'then re-run validate.',
40
- });
41
- continue;
42
- }
43
- // A genuine module-load failure is a precondition, not a finding.
44
- throw preconditionError(`Could not load the integration definition for "${integration.slug}": ` +
45
- result.message, 'Ensure the entry module and its installed dependencies load under ' +
46
- 'Node, then re-run validate.');
47
- }
48
- // Cross-checks only mean something with more than one definition in hand —
49
- // which is exactly what this verb could not see before, since ekanos.json
50
- // held a single { slug, entry } and a second integration was invisible.
51
- findings.push(...collectCollisionFindings(definitions));
52
- findings.push(...collectProjectFindings(loaded.projectDir));
19
+ const findings = await collectAllFindings(loaded);
53
20
  const errorCount = findings.filter((f) => f.severity === 'error').length;
54
21
  const data = {
55
22
  slug: loaded.primary.slug,
@@ -65,34 +32,4 @@ export async function runValidate(ctx, args) {
65
32
  }
66
33
  return ctx.succeed(data, `validate: OK — no findings for ${slugs}.`);
67
34
  }
68
- /**
69
- * `ekanos.json` and the definition each carry a slug, and nothing compared
70
- * them: a project could declare `something-else` while the definition said
71
- * `repo-activity` and validate would report `{ ok: true, findings: [] }`.
72
- *
73
- * That is the identifier every surface addresses the integration by — the
74
- * harness route, the product slug, the widget id prefix, the MCP server — so
75
- * two sources of truth disagreeing is not a style question. It is an error,
76
- * not a warning, for the same reason a silent default is worse than a loud
77
- * one: the failure it causes shows up somewhere else entirely.
78
- */
79
- function collectSlugAgreementFindings(integration, definition) {
80
- const declared = definition.slug;
81
- if (typeof declared !== 'string' || declared === integration.slug)
82
- return [];
83
- return [
84
- {
85
- check: 'project.slug-agreement',
86
- severity: 'error',
87
- file: integration.entryPath,
88
- message: `ekanos.json declares slug "${integration.slug}" for this entry, but ` +
89
- `the definition says "${declared}". The slug addresses the ` +
90
- 'integration everywhere — its harness route, its product record, its ' +
91
- 'widget ids — so the two must agree.',
92
- hint: `Change one to match the other: either set "slug": "${declared}" in ` +
93
- `ekanos.json, or pass slug: '${integration.slug}' to ` +
94
- 'defineIntegration().',
95
- },
96
- ];
97
- }
98
35
  //# sourceMappingURL=validate.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"validate.js","sourceRoot":"","sources":["../../src/commands/validate.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n DefinitionCollisionInput,\n Finding,\n} from '@ekanos/integration-schema';\nimport {\n collectCollisionFindings,\n collectDefinitionFindings,\n} from '@ekanos/integration-schema';\n\nimport type { CliContext } from '../context';\nimport { preconditionError, validationError } from '../errors';\nimport type { ExitCode } from '../exit-codes';\nimport { loadDefinition } from '../load-definition';\nimport { loadProject } from '../project';\nimport { collectProjectFindings } from '../project-checks';\n\nexport interface ValidateArgs {\n /** Project directory (contains ekanos.json). Defaults to cwd. */\n dir: string;\n}\n\n/**\n * `validate` — parse the declaration with the REAL zod schemas from\n * `@ekanos/integration-schema` (never a reimplementation), gather structured\n * findings, and add project-level integrity checks (the vitest inline-SDK\n * check). Exit 0 with `findings: []` when clean; exit 3 when any error-severity\n * finding is present, with the findings carried in the envelope's `data`.\n */\nexport async function runValidate(\n ctx: CliContext,\n args: ValidateArgs,\n): Promise<ExitCode> {\n const loaded = loadProject(args.dir);\n const slugs = loaded.integrations.map((i) => i.slug).join(', ');\n ctx.log(\n `Validating ${loaded.integrations.length} integration` +\n `${loaded.integrations.length === 1 ? '' : 's'} (${slugs})…`,\n );\n\n const findings: Finding[] = [];\n const definitions: DefinitionCollisionInput[] = [];\n\n for (const integration of loaded.integrations) {\n const result = await loadDefinition(\n integration.entryPath,\n loaded.projectDir,\n );\n\n if (result.ok) {\n findings.push(\n ...collectDefinitionFindings(result.definition, {\n file: integration.entryPath,\n }),\n );\n findings.push(\n ...collectSlugAgreementFindings(\n integration,\n result.definition as { slug?: unknown },\n ),\n );\n definitions.push(result.definition as DefinitionCollisionInput);\n continue;\n }\n\n if (result.kind === 'rejected') {\n // The module loaded but the SDK/schema rejected the definition at import\n // time — surface it as a validation finding rather than a crash.\n findings.push({\n check: 'definition.load',\n severity: 'error',\n file: integration.entryPath,\n message: result.message,\n hint:\n 'Fix the integration definition so defineIntegration() accepts it, ' +\n 'then re-run validate.',\n });\n continue;\n }\n\n // A genuine module-load failure is a precondition, not a finding.\n throw preconditionError(\n `Could not load the integration definition for \"${integration.slug}\": ` +\n result.message,\n 'Ensure the entry module and its installed dependencies load under ' +\n 'Node, then re-run validate.',\n );\n }\n\n // Cross-checks only mean something with more than one definition in hand —\n // which is exactly what this verb could not see before, since ekanos.json\n // held a single { slug, entry } and a second integration was invisible.\n findings.push(...collectCollisionFindings(definitions));\n findings.push(...collectProjectFindings(loaded.projectDir));\n\n const errorCount = findings.filter((f) => f.severity === 'error').length;\n const data = {\n slug: loaded.primary.slug,\n integrations: loaded.integrations.map((i) => ({\n slug: i.slug,\n entry: i.entry,\n })),\n findings,\n };\n\n if (errorCount > 0) {\n return ctx.fail(\n validationError(\n `${errorCount} validation finding${errorCount === 1 ? '' : 's'}.`,\n 'Resolve the error-severity findings in data.findings, then re-run ' +\n '\"ekanos validate\".',\n ),\n data,\n );\n }\n\n return ctx.succeed(data, `validate: OK — no findings for ${slugs}.`);\n}\n\n/**\n * `ekanos.json` and the definition each carry a slug, and nothing compared\n * them: a project could declare `something-else` while the definition said\n * `repo-activity` and validate would report `{ ok: true, findings: [] }`.\n *\n * That is the identifier every surface addresses the integration by — the\n * harness route, the product slug, the widget id prefix, the MCP server — so\n * two sources of truth disagreeing is not a style question. It is an error,\n * not a warning, for the same reason a silent default is worse than a loud\n * one: the failure it causes shows up somewhere else entirely.\n */\nfunction collectSlugAgreementFindings(\n integration: { slug: string; entry: string; entryPath: string },\n definition: { slug?: unknown },\n): Finding[] {\n const declared = definition.slug;\n if (typeof declared !== 'string' || declared === integration.slug) return [];\n\n return [\n {\n check: 'project.slug-agreement',\n severity: 'error',\n file: integration.entryPath,\n message:\n `ekanos.json declares slug \"${integration.slug}\" for this entry, but ` +\n `the definition says \"${declared}\". The slug addresses the ` +\n 'integration everywhere — its harness route, its product record, its ' +\n 'widget ids — so the two must agree.',\n hint:\n `Change one to match the other: either set \"slug\": \"${declared}\" in ` +\n `ekanos.json, or pass slug: '${integration.slug}' to ` +\n 'defineIntegration().',\n },\n ];\n}\n"]}
1
+ {"version":3,"file":"validate.js","sourceRoot":"","sources":["../../src/commands/validate.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAE5C,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAO1D;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,GAAe,EACf,IAAkB;IAElB,MAAM,MAAM,GAAG,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACrC,MAAM,KAAK,GAAG,MAAM,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAChE,GAAG,CAAC,GAAG,CACL,cAAc,MAAM,CAAC,YAAY,CAAC,MAAM,cAAc;QACpD,GAAG,MAAM,CAAC,YAAY,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,KAAK,IAAI,CAC/D,CAAC;IAEF,MAAM,QAAQ,GAAG,MAAM,kBAAkB,CAAC,MAAM,CAAC,CAAC;IAElD,MAAM,UAAU,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,MAAM,CAAC;IACzE,MAAM,IAAI,GAAG;QACX,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,IAAI;QACzB,YAAY,EAAE,MAAM,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YAC5C,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,KAAK,EAAE,CAAC,CAAC,KAAK;SACf,CAAC,CAAC;QACH,QAAQ;KACT,CAAC;IAEF,IAAI,UAAU,GAAG,CAAC,EAAE,CAAC;QACnB,OAAO,GAAG,CAAC,IAAI,CACb,eAAe,CACb,GAAG,UAAU,sBAAsB,UAAU,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG,EACjE,oEAAoE;YAClE,oBAAoB,CACvB,EACD,IAAI,CACL,CAAC;IACJ,CAAC;IAED,OAAO,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,kCAAkC,KAAK,GAAG,CAAC,CAAC;AACvE,CAAC","sourcesContent":["import type { CliContext } from '../context';\nimport { validationError } from '../errors';\nimport type { ExitCode } from '../exit-codes';\nimport { loadProject } from '../project';\nimport { collectAllFindings } from '../validate-findings';\n\nexport interface ValidateArgs {\n /** Project directory (contains ekanos.json). Defaults to cwd. */\n dir: string;\n}\n\n/**\n * `validate` — parse the declaration with the REAL zod schemas from\n * `@ekanos/integration-schema` (never a reimplementation), gather structured\n * findings, and add project-level integrity checks (the vitest inline-SDK\n * check). Exit 0 with `findings: []` when clean; exit 3 when any error-severity\n * finding is present, with the findings carried in the envelope's `data`.\n *\n * The findings pass itself lives in `validate-findings.ts`, shared with\n * `publish` so the publish gate cannot drift from what validate checks.\n */\nexport async function runValidate(\n ctx: CliContext,\n args: ValidateArgs,\n): Promise<ExitCode> {\n const loaded = loadProject(args.dir);\n const slugs = loaded.integrations.map((i) => i.slug).join(', ');\n ctx.log(\n `Validating ${loaded.integrations.length} integration` +\n `${loaded.integrations.length === 1 ? '' : 's'} (${slugs})…`,\n );\n\n const findings = await collectAllFindings(loaded);\n\n const errorCount = findings.filter((f) => f.severity === 'error').length;\n const data = {\n slug: loaded.primary.slug,\n integrations: loaded.integrations.map((i) => ({\n slug: i.slug,\n entry: i.entry,\n })),\n findings,\n };\n\n if (errorCount > 0) {\n return ctx.fail(\n validationError(\n `${errorCount} validation finding${errorCount === 1 ? '' : 's'}.`,\n 'Resolve the error-severity findings in data.findings, then re-run ' +\n '\"ekanos validate\".',\n ),\n data,\n );\n }\n\n return ctx.succeed(data, `validate: OK — no findings for ${slugs}.`);\n}\n"]}
@@ -2,6 +2,8 @@ import type { CliContext } from '../context.js';
2
2
  import type { ExitCode } from '../exit-codes.js';
3
3
  export interface WhoamiArgs {
4
4
  host?: string;
5
+ /** Where a project's ekanos.json may sit, for its optional `host` field. */
6
+ cwd: string;
5
7
  env: Record<string, string | undefined>;
6
8
  }
7
9
  /**
@@ -12,7 +12,9 @@ import { requireSession, resolveAuthEnvironment, resolveIdentity, } from '../aut
12
12
  */
13
13
  export async function runWhoami(ctx, args) {
14
14
  var _a;
15
- const env = resolveAuthEnvironment(ctx, args.host, args.env);
15
+ const env = resolveAuthEnvironment(ctx, args.host, args.env, {
16
+ projectDir: args.cwd,
17
+ });
16
18
  const stored = requireSession(env.store, env.host);
17
19
  const { identity, session } = await resolveIdentity(env, stored);
18
20
  return ctx.succeed({
@@ -1 +1 @@
1
- {"version":3,"file":"whoami.js","sourceRoot":"","sources":["../../src/commands/whoami.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,sBAAsB,EACtB,eAAe,GAChB,MAAM,iBAAiB,CAAC;AASzB;;;;;;;;;;GAUG;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,CAAC,CAAC;IAC7D,MAAM,MAAM,GAAG,cAAc,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC;IAEnD,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,eAAe,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IAEjE,OAAO,GAAG,CAAC,OAAO,CAChB;QACE,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,IAAI,EAAE;YACJ,EAAE,EAAE,QAAQ,CAAC,EAAE;YACf,KAAK,EAAE,QAAQ,CAAC,KAAK;YACrB,IAAI,EAAE,QAAQ,CAAC,IAAI;SACpB;QACD,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,eAAe,EAAE,GAAG,CAAC,KAAK,CAAC,QAAQ;KACpC,EACD,WAAW,MAAA,QAAQ,CAAC,KAAK,mCAAI,QAAQ,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,GAAG,CAC3D,CAAC;AACJ,CAAC","sourcesContent":["import {\n requireSession,\n resolveAuthEnvironment,\n resolveIdentity,\n} from '../auth/session';\nimport type { CliContext } from '../context';\nimport type { ExitCode } from '../exit-codes';\n\nexport interface WhoamiArgs {\n host?: string;\n env: Record<string, string | undefined>;\n}\n\n/**\n * `whoami` — prove the stored session still authenticates against Fusion.\n *\n * The identity is resolved server-side by `/api/auth/cli-whoami`, which reads\n * the account through RLS as the caller. Printing the locally cached email\n * would be a cheaper lie: it would keep saying \"logged in\" after the account\n * was deleted or the token revoked.\n *\n * Exit 4 (auth_required) when there is no session or it cannot be renewed —\n * the code a script should branch on to decide whether to run `ekanos login`.\n */\nexport async function runWhoami(\n ctx: CliContext,\n args: WhoamiArgs,\n): Promise<ExitCode> {\n const env = resolveAuthEnvironment(ctx, args.host, args.env);\n const stored = requireSession(env.store, env.host);\n\n const { identity, session } = await resolveIdentity(env, stored);\n\n return ctx.succeed(\n {\n host: env.host,\n user: {\n id: identity.id,\n email: identity.email,\n name: identity.name,\n },\n expiresAt: session.expiresAt,\n credentialsPath: env.store.filePath,\n },\n `whoami: ${identity.email ?? identity.id} on ${env.host}.`,\n );\n}\n"]}
1
+ {"version":3,"file":"whoami.js","sourceRoot":"","sources":["../../src/commands/whoami.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,sBAAsB,EACtB,eAAe,GAChB,MAAM,iBAAiB,CAAC;AAWzB;;;;;;;;;;GAUG;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;IACH,MAAM,MAAM,GAAG,cAAc,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC;IAEnD,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,eAAe,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IAEjE,OAAO,GAAG,CAAC,OAAO,CAChB;QACE,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,IAAI,EAAE;YACJ,EAAE,EAAE,QAAQ,CAAC,EAAE;YACf,KAAK,EAAE,QAAQ,CAAC,KAAK;YACrB,IAAI,EAAE,QAAQ,CAAC,IAAI;SACpB;QACD,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,eAAe,EAAE,GAAG,CAAC,KAAK,CAAC,QAAQ;KACpC,EACD,WAAW,MAAA,QAAQ,CAAC,KAAK,mCAAI,QAAQ,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,GAAG,CAC3D,CAAC;AACJ,CAAC","sourcesContent":["import {\n requireSession,\n resolveAuthEnvironment,\n resolveIdentity,\n} from '../auth/session';\nimport type { CliContext } from '../context';\nimport type { ExitCode } from '../exit-codes';\n\nexport interface WhoamiArgs {\n host?: string;\n /** Where a project's ekanos.json may sit, for its optional `host` field. */\n cwd: string;\n env: Record<string, string | undefined>;\n}\n\n/**\n * `whoami` — prove the stored session still authenticates against Fusion.\n *\n * The identity is resolved server-side by `/api/auth/cli-whoami`, which reads\n * the account through RLS as the caller. Printing the locally cached email\n * would be a cheaper lie: it would keep saying \"logged in\" after the account\n * was deleted or the token revoked.\n *\n * Exit 4 (auth_required) when there is no session or it cannot be renewed —\n * the code a script should branch on to decide whether to run `ekanos login`.\n */\nexport async function runWhoami(\n ctx: CliContext,\n args: WhoamiArgs,\n): Promise<ExitCode> {\n const env = resolveAuthEnvironment(ctx, args.host, args.env, {\n projectDir: args.cwd,\n });\n const stored = requireSession(env.store, env.host);\n\n const { identity, session } = await resolveIdentity(env, stored);\n\n return ctx.succeed(\n {\n host: env.host,\n user: {\n id: identity.id,\n email: identity.email,\n name: identity.name,\n },\n expiresAt: session.expiresAt,\n credentialsPath: env.store.filePath,\n },\n `whoami: ${identity.email ?? identity.id} on ${env.host}.`,\n );\n}\n"]}
package/dist/errors.d.ts CHANGED
@@ -32,6 +32,8 @@ export declare function notFoundError(message: string, hint: string): CliError;
32
32
  export declare function authRequiredError(message: string, hint: string): CliError;
33
33
  /** Authenticated, but the server refused. Exit 5. */
34
34
  export declare function forbiddenError(message: string, hint: string): CliError;
35
+ /** A publish gate rejected the submission. Exit 10. */
36
+ export declare function gateFailedError(message: string, hint: string): CliError;
35
37
  /** A network operation failed (DNS, TLS, timeout, 5xx). Exit 8. */
36
38
  export declare function networkError(message: string, hint: string): CliError;
37
39
  /**