@ekanos/cli 0.1.1 → 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.
@@ -4,7 +4,7 @@ import { refreshStoredSession, requireSession, resolveAuthEnvironment, } from '.
4
4
  import { gateFailedError, preconditionError, validationError } from '../errors.js';
5
5
  import { packProject } from '../pack.js';
6
6
  import { loadProject, serializeProject } from '../project.js';
7
- import { stillUnauthorized, submitArchive } from '../publish-api.js';
7
+ import { MAX_MANIFEST_BYTES, stillUnauthorized, submitArchive, } from '../publish-api.js';
8
8
  import { collectAllFindings } from '../validate-findings.js';
9
9
  /**
10
10
  * `publish` — pack the project and submit it to a Fusion deployment.
@@ -20,7 +20,9 @@ import { collectAllFindings } from '../validate-findings.js';
20
20
  * so later publishes need no `--source`.
21
21
  */
22
22
  export async function runPublish(ctx, args) {
23
- const env = resolveAuthEnvironment(ctx, args.host, args.env);
23
+ const env = resolveAuthEnvironment(ctx, args.host, args.env, {
24
+ projectDir: args.dir,
25
+ });
24
26
  const session = requireSession(env.store, env.host);
25
27
  const loaded = loadProject(args.dir);
26
28
  const source = resolveSourceSlug(args.source, loaded);
@@ -37,14 +39,7 @@ export async function runPublish(ctx, args) {
37
39
  }
38
40
  ctx.log(`Packing ${slug}@${version}…`);
39
41
  const packed = packProject(loaded.projectDir);
40
- const manifest = JSON.stringify({
41
- project: loaded.project,
42
- integrations: loaded.integrations.map((i) => ({
43
- slug: i.slug,
44
- entry: i.entry,
45
- })),
46
- files: packed.files,
47
- });
42
+ const manifest = buildManifest(loaded, packed.files);
48
43
  ctx.log(`Submitting to ${env.host} (source "${source}")…`);
49
44
  const receipt = await submitWithOneRefresh(env, session, {
50
45
  source,
@@ -53,7 +48,16 @@ export async function runPublish(ctx, args) {
53
48
  manifest,
54
49
  archive: packed.archive,
55
50
  });
56
- const persistedSource = persistSourceSlug(loaded, source);
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
+ : '';
57
61
  return ctx.succeed({
58
62
  host: env.host,
59
63
  source,
@@ -63,13 +67,16 @@ export async function runPublish(ctx, args) {
63
67
  state: receipt.state,
64
68
  archiveSha256: receipt.archiveSha256,
65
69
  files: packed.files,
66
- sourcePersisted: persistedSource,
70
+ sourcePersisted: persisted.source,
71
+ hostPersisted: persisted.host,
72
+ hostOverride: persisted.hostOverride,
67
73
  }, `publish: submitted ${receipt.slug}@${receipt.version} to ${env.host} ` +
68
74
  `(source "${source}", state "${receipt.state}").` +
69
- (persistedSource
70
- ? ` Saved "source": "${source}" to ekanos.json — future publishes ` +
71
- `won't need --source.`
72
- : ''));
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);
73
80
  }
74
81
  async function submitWithOneRefresh(env, session, payload) {
75
82
  const first = await submitArchive(env.host, session.accessToken, payload);
@@ -93,23 +100,74 @@ function resolveSourceSlug(flag, loaded) {
93
100
  return source.trim();
94
101
  }
95
102
  /**
96
- * Persist the chosen source into ekanos.json after a SUCCESSFUL publish, so
97
- * the next publish needs no flag. Returns whether anything was written. A
98
- * write failure is reported but never fails the command — the submission
99
- * already landed.
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.
100
121
  */
101
- function persistSourceSlug(loaded, source) {
102
- if (loaded.project.source === source)
103
- return false;
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
+ }
104
133
  try {
105
- fs.writeFileSync(loaded.configPath, serializeProject(Object.assign(Object.assign({}, loaded.project), { source })));
106
- return true;
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 });
107
136
  }
108
137
  catch (_a) {
109
138
  // Diagnostics-only: the publish succeeded; a read-only ekanos.json just
110
- // means the next publish needs --source again.
111
- return false;
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".');
112
169
  }
170
+ return manifest;
113
171
  }
114
172
  const SEMVER_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
115
173
  /**
@@ -1 +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 { stillUnauthorized, submitArchive } 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 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\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: packed.files,\n });\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 persistedSource = persistSourceSlug(loaded, source);\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: persistedSource,\n },\n `publish: submitted ${receipt.slug}@${receipt.version} to ${env.host} ` +\n `(source \"${source}\", state \"${receipt.state}\").` +\n (persistedSource\n ? ` Saved \"source\": \"${source}\" to ekanos.json — future publishes ` +\n `won't need --source.`\n : ''),\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 into ekanos.json after a SUCCESSFUL publish, so\n * the next publish needs no flag. Returns whether anything was written. A\n * write failure is reported but never fails the command — the submission\n * already landed.\n */\nfunction persistSourceSlug(loaded: LoadedProject, source: string): boolean {\n if (loaded.project.source === source) return false;\n\n try {\n fs.writeFileSync(\n loaded.configPath,\n serializeProject({ ...loaded.project, source }),\n );\n\n return true;\n } catch {\n // Diagnostics-only: the publish succeeded; a read-only ekanos.json just\n // means the next publish needs --source again.\n return false;\n }\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"]}
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"]}
@@ -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/index.js CHANGED
@@ -4,6 +4,7 @@ import { runInit } from './commands/init.js';
4
4
  import { runLogin } from './commands/login.js';
5
5
  import { runLogout } from './commands/logout.js';
6
6
  import { runPublish } from './commands/publish.js';
7
+ import { runStatus } from './commands/status.js';
7
8
  import { runTest } from './commands/test.js';
8
9
  import { runValidate } from './commands/validate.js';
9
10
  import { runWhoami } from './commands/whoami.js';
@@ -49,6 +50,10 @@ const COMMAND_SPECS = {
49
50
  whoami: {
50
51
  host: { type: 'string' },
51
52
  },
53
+ status: {
54
+ host: { type: 'string' },
55
+ dir: { type: 'string' },
56
+ },
52
57
  publish: {
53
58
  host: { type: 'string' },
54
59
  source: { type: 'string' },
@@ -61,15 +66,23 @@ const USAGE = [
61
66
  'Usage: ekanos <command> [options]',
62
67
  '',
63
68
  'Commands:',
64
- ' init Scaffold a new integration project (--slug <slug> --yes)',
69
+ ' init Scaffold a new integration project into ./<slug> (--slug <slug> --yes)',
65
70
  ' validate Validate ekanos.json + the integration definition (--json)',
66
71
  ' dev Scaffold .ekanos/harness and run it with your own Next',
67
72
  ' test Run the project test script via its package manager (--json)',
68
73
  ' login Authenticate against a Fusion host in a browser (--host <url>)',
69
74
  ' logout Revoke and delete the stored session (--host <url> | --all)',
70
75
  ' whoami Print the identity the stored session authenticates as',
76
+ " status Show login state and this project's submissions on the host",
71
77
  ' publish Validate, pack and submit the integration to a Fusion host',
72
78
  '',
79
+ 'Host resolution (logout, whoami, status, publish):',
80
+ ' --host, else EKANOS_HOST, else the "host" field in ekanos.json, else the',
81
+ ' sole stored login. "ekanos publish" saves host and source to ekanos.json.',
82
+ ' "ekanos login" never reads ekanos.json for a host (--host, else',
83
+ ' EKANOS_HOST, else the sole stored login) — it is the verb that creates',
84
+ ' credentials, so a committed file must never choose where they go.',
85
+ '',
73
86
  'Global options:',
74
87
  ' --json Emit a single JSON envelope on stdout (auto-on when piped);',
75
88
  ' --no-json forces the human-readable form',
@@ -138,7 +151,8 @@ export async function run(argv, options = {}) {
138
151
  case 'init':
139
152
  return runInit(ctx, {
140
153
  slug: parsed.flags.slug,
141
- dir,
154
+ dir: parsed.flags.dir,
155
+ cwd,
142
156
  force: parsed.flags.force === true,
143
157
  yes: parsed.flags.yes === true,
144
158
  });
@@ -158,17 +172,26 @@ export async function run(argv, options = {}) {
158
172
  return await runLogin(ctx, {
159
173
  host: parsed.flags.host,
160
174
  force: parsed.flags.force === true,
175
+ cwd,
161
176
  env,
162
177
  });
163
178
  case 'logout':
164
179
  return await runLogout(ctx, {
165
180
  host: parsed.flags.host,
166
181
  all: parsed.flags.all === true,
182
+ cwd,
167
183
  env,
168
184
  });
169
185
  case 'whoami':
170
186
  return await runWhoami(ctx, {
171
187
  host: parsed.flags.host,
188
+ cwd,
189
+ env,
190
+ });
191
+ case 'status':
192
+ return await runStatus(ctx, {
193
+ host: parsed.flags.host,
194
+ dir,
172
195
  env,
173
196
  });
174
197
  case 'publish':