@ekanos/cli 0.1.10 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/banner.d.ts +5 -1
- package/dist/banner.js +7 -2
- package/dist/banner.js.map +1 -1
- package/dist/commands/init.d.ts +13 -1
- package/dist/commands/init.js +70 -13
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/login.d.ts +13 -0
- package/dist/commands/login.js +41 -10
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/publish.js +55 -3
- package/dist/commands/publish.js.map +1 -1
- package/dist/commands/status.d.ts +68 -2
- package/dist/commands/status.js +312 -43
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/test.d.ts +27 -5
- package/dist/commands/test.js +85 -9
- package/dist/commands/test.js.map +1 -1
- package/dist/commands/validate.js +115 -3
- package/dist/commands/validate.js.map +1 -1
- package/dist/context.d.ts +37 -1
- package/dist/context.js +27 -1
- package/dist/context.js.map +1 -1
- package/dist/did-you-mean.d.ts +36 -0
- package/dist/did-you-mean.js +192 -0
- package/dist/did-you-mean.js.map +1 -0
- package/dist/findings-renderer.d.ts +59 -0
- package/dist/findings-renderer.js +271 -0
- package/dist/findings-renderer.js.map +1 -0
- package/dist/index.d.ts +11 -4
- package/dist/index.js +23 -5
- package/dist/index.js.map +1 -1
- package/dist/project-checks.d.ts +30 -0
- package/dist/project-checks.js +162 -0
- package/dist/project-checks.js.map +1 -1
- package/dist/publish-api.d.ts +13 -0
- package/dist/publish-api.js.map +1 -1
- package/dist/source-locator.d.ts +67 -0
- package/dist/source-locator.js +284 -0
- package/dist/source-locator.js.map +1 -0
- package/dist/test-report.d.ts +32 -0
- package/dist/test-report.js +255 -0
- package/dist/test-report.js.map +1 -0
- package/dist/ui.d.ts +75 -0
- package/dist/ui.js +132 -0
- package/dist/ui.js.map +1 -0
- package/dist/validate-findings.d.ts +22 -0
- package/dist/validate-findings.js +57 -1
- package/dist/validate-findings.js.map +1 -1
- package/dist/wizard.d.ts +80 -0
- package/dist/wizard.js +74 -0
- package/dist/wizard.js.map +1 -0
- package/package.json +4 -2
- package/templates/package.json.tmpl +3 -1
- package/templates/scripts/setup-hooks.mjs.tmpl +45 -0
- package/templates/tsconfig.json.tmpl +2 -0
package/dist/commands/publish.js
CHANGED
|
@@ -2,6 +2,7 @@ import * as fs from 'node:fs';
|
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import { refreshStoredSession, requireSession, resolveAuthEnvironment, } from '../auth/session.js';
|
|
4
4
|
import { forbiddenError, gateFailedError, preconditionError, validationError, } from '../errors.js';
|
|
5
|
+
import { renderFindings } from '../findings-renderer.js';
|
|
5
6
|
import { packProject } from '../pack.js';
|
|
6
7
|
import { loadProject, serializeProject } from '../project.js';
|
|
7
8
|
import { MAX_MANIFEST_BYTES, stillUnauthorized, submitArchive, } from '../publish-api.js';
|
|
@@ -10,6 +11,31 @@ import { fetchSeatsWithOneRefresh } from '../seats.js';
|
|
|
10
11
|
import { stillUnauthorizedSourceSeats } from '../sources-api.js';
|
|
11
12
|
import { announceUpdate, checkForUpdate } from '../update-notice.js';
|
|
12
13
|
import { collectAllFindings } from '../validate-findings.js';
|
|
14
|
+
/**
|
|
15
|
+
* Checks that are WARN in plain `ekanos validate` but must refuse a
|
|
16
|
+
* `publish` outright: a project that will fail the gate is not allowed to
|
|
17
|
+
* upload and find that out from the gate instead of from itself.
|
|
18
|
+
* `project.host-compiler-rules` (see `project-checks.ts`) is WARN-severity
|
|
19
|
+
* everywhere else on purpose — it reads a partner's local tsconfig, not the
|
|
20
|
+
* gate's own verdict, so it must not block plain validate on a false
|
|
21
|
+
* positive — but at the publish gate, "this WILL fail the gate" is exactly
|
|
22
|
+
* the thing a submission preflight exists to refuse. A list, not a second
|
|
23
|
+
* check, so the underlying rule and its message/hint stay defined in exactly
|
|
24
|
+
* one place.
|
|
25
|
+
*/
|
|
26
|
+
const PUBLISH_ERROR_ESCALATIONS = [
|
|
27
|
+
'project.host-compiler-rules',
|
|
28
|
+
];
|
|
29
|
+
/**
|
|
30
|
+
* Escalate the checks in `PUBLISH_ERROR_ESCALATIONS` from `warn` to `error`
|
|
31
|
+
* for THIS publish run only — `validate`'s own output (and the underlying
|
|
32
|
+
* finding objects elsewhere) is never touched.
|
|
33
|
+
*/
|
|
34
|
+
function escalateForPublish(findings) {
|
|
35
|
+
return findings.map((finding) => finding.severity === 'warn' &&
|
|
36
|
+
PUBLISH_ERROR_ESCALATIONS.includes(finding.check)
|
|
37
|
+
? Object.assign(Object.assign({}, finding), { severity: 'error' }) : finding);
|
|
38
|
+
}
|
|
13
39
|
/**
|
|
14
40
|
* `publish` — pack the project and submit it to a Fusion deployment.
|
|
15
41
|
*
|
|
@@ -54,13 +80,32 @@ export async function runPublish(ctx, args) {
|
|
|
54
80
|
const version = readProjectVersion(loaded.projectDir);
|
|
55
81
|
const slug = loaded.primary.slug;
|
|
56
82
|
ctx.log(`Validating ${slug} before publishing…`);
|
|
57
|
-
const findings = await collectAllFindings(loaded);
|
|
83
|
+
const findings = escalateForPublish(await collectAllFindings(loaded));
|
|
58
84
|
const errorCount = findings.filter((f) => f.severity === 'error').length;
|
|
59
85
|
if (errorCount > 0) {
|
|
60
|
-
|
|
86
|
+
// Same rule as `validate`: render the shared findings block to stderr
|
|
87
|
+
// BEFORE the envelope settles, human mode only. JSON mode carries the
|
|
88
|
+
// identical findings in `data.findings` and must stay byte-identical.
|
|
89
|
+
// Closing order is findings block -> short summary (via `ctx.fail`'s
|
|
90
|
+
// `findingsSummary` option) -> next action.
|
|
91
|
+
if (!ctx.jsonMode) {
|
|
92
|
+
ctx.log(ctx.ui.err(`${ctx.ui.glyphs.err} Refusing to publish: ${errorCount} finding` +
|
|
93
|
+
`${errorCount === 1 ? '' : 's'}.`));
|
|
94
|
+
renderFindings(ctx, findings, { cwd: loaded.projectDir });
|
|
95
|
+
}
|
|
96
|
+
const exitCode = ctx.fail(gateFailedError(`Refusing to publish: ${errorCount} validation finding` +
|
|
61
97
|
`${errorCount === 1 ? '' : 's'}.`, 'Resolve the error-severity findings in data.findings (they are ' +
|
|
62
98
|
'exactly what "ekanos validate" reports), then re-run ' +
|
|
63
|
-
'"ekanos publish".'), { slug, version, findings }
|
|
99
|
+
'"ekanos publish".'), { slug, version, findings }, !ctx.jsonMode
|
|
100
|
+
? { findingsSummary: { command: 'ekanos publish', errorCount } }
|
|
101
|
+
: undefined);
|
|
102
|
+
if (!ctx.jsonMode) {
|
|
103
|
+
ctx.log(ctx.ui.nextAction('ekanos validate', 'fix the error-severity findings above, then re-run "ekanos publish"'));
|
|
104
|
+
}
|
|
105
|
+
return exitCode;
|
|
106
|
+
}
|
|
107
|
+
if (!ctx.jsonMode) {
|
|
108
|
+
ctx.log(ctx.ui.ok(`${ctx.ui.glyphs.ok} validated ${slug} — no error-severity findings.`));
|
|
64
109
|
}
|
|
65
110
|
// The 100%-certainty mechanism: confirm the resolved source is one the
|
|
66
111
|
// caller actually holds a seat on — and, when it was resolved IMPLICITLY
|
|
@@ -80,6 +125,9 @@ export async function runPublish(ctx, args) {
|
|
|
80
125
|
ctx.log(`Packing ${slug}@${version}…`);
|
|
81
126
|
const packed = packProject(loaded.projectDir);
|
|
82
127
|
const manifest = buildManifest(loaded, packed.files);
|
|
128
|
+
if (!ctx.jsonMode) {
|
|
129
|
+
ctx.log(ctx.ui.ok(`${ctx.ui.glyphs.ok} packed ${slug}@${version} (${packed.files.length} files).`));
|
|
130
|
+
}
|
|
83
131
|
ctx.log(`Submitting to ${env.host} (source "${source}")…`);
|
|
84
132
|
const receipt = await submitWithOneRefresh(env, verified.session, {
|
|
85
133
|
source,
|
|
@@ -88,6 +136,10 @@ export async function runPublish(ctx, args) {
|
|
|
88
136
|
manifest,
|
|
89
137
|
archive: packed.archive,
|
|
90
138
|
});
|
|
139
|
+
if (!ctx.jsonMode) {
|
|
140
|
+
ctx.log(ctx.ui.ok(`${ctx.ui.glyphs.ok} submitted ${receipt.slug}@${receipt.version} ` +
|
|
141
|
+
`(state "${receipt.state}").`));
|
|
142
|
+
}
|
|
91
143
|
const persisted = persistProjectFields(loaded, source, env.host);
|
|
92
144
|
const persistedNote = [
|
|
93
145
|
...(persisted.source ? [`"source": "${source}"`] : []),
|
|
@@ -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 type AuthEnvironment,\n refreshStoredSession,\n requireSession,\n resolveAuthEnvironment,\n} from '../auth/session';\nimport type { CliContext } from '../context';\nimport {\n forbiddenError,\n gateFailedError,\n preconditionError,\n validationError,\n} 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 { assertSchemaNotSkewed } from '../schema-skew';\nimport { fetchSeatsWithOneRefresh } from '../seats';\nimport { type SourceSeat, stillUnauthorizedSourceSeats } from '../sources-api';\nimport { announceUpdate, checkForUpdate } from '../update-notice';\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 /**\n * Skips the interactive confirm when the source resolved IMPLICITLY (from\n * ekanos.json) and the caller holds more than one developer seat. Has no\n * effect when `--source` was passed this invocation (explicit is already\n * consent) or when the caller holds exactly one seat (nothing to\n * disambiguate).\n */\n yes: boolean;\n env: Record<string, string | undefined>;\n /**\n * Whether stdin is a real interactive terminal — the SECOND condition (with\n * human/non-JSON mode) required before publish will ever prompt. Defaults\n * to `process.stdin.isTTY === true`. Injectable so tests can exercise the\n * confirm branch without a real TTY.\n */\n stdinIsTTY?: boolean;\n /**\n * Injectable y/N prompt, for tests. Defaults to a real readline prompt over\n * stdin/stderr. Never invoked unless BOTH human mode and a real stdin TTY\n * are in play — see `canPrompt` in `verifyPublishTarget`.\n */\n confirm?: (question: string) => Promise<boolean>;\n}\n\n/** Where the resolved source slug came from — governs the confirmation rules below. */\ninterface SourceResolution {\n source: string;\n /** True iff `--source` was passed THIS invocation. */\n explicit: boolean;\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\n // Fail fast, before anything else: a project whose schema is newer than\n // this CLI bundles cannot be trusted to validate correctly, so publishing\n // it — the one verb whose findings gate is meant to be authoritative —\n // must refuse before packing or submitting anything. See `schema-skew.ts`.\n const skew = assertSchemaNotSkewed(ctx, loaded.projectDir);\n\n const notice = await checkForUpdate({\n env: args.env,\n host: env.host,\n session,\n });\n announceUpdate(ctx, notice);\n\n // The host would reject a build from a CLI it has declared unsupported\n // anyway (see the wire contract in `toolchain-api.ts`) — refuse here with a\n // hint naming the fix, rather than let the submission round-trip fail with\n // whatever generic error the host happens to answer with.\n if (notice?.status === 'unsupported') {\n throw preconditionError(\n `@ekanos/cli ${notice.current.cli} is below ${env.host}'s minimum ` +\n `supported version (needs at least ${notice.target.cli}).`,\n 'Run \"ekanos upgrade\" before publishing — the host will refuse this ' +\n 'submission from an unsupported CLI regardless.',\n );\n }\n\n const resolution = await resolveSourceSlug(env, session, 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 // The 100%-certainty mechanism: confirm the resolved source is one the\n // caller actually holds a seat on — and, when it was resolved IMPLICITLY\n // from ekanos.json with more than one seat in play, that a human (or an\n // explicit --yes) actually intends this target — BEFORE anything is packed\n // or sent over the wire.\n const verified = await verifyPublishTarget(ctx, env, resolution.session, {\n source: resolution.source,\n explicit: resolution.explicit,\n slug,\n version,\n yes: args.yes,\n stdinIsTTY: args.stdinIsTTY ?? process.stdin.isTTY === true,\n confirm: args.confirm ?? promptYesNo,\n });\n\n const source = verified.sourceSeat.slug;\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, verified.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 id: verified.sourceSeat.id,\n slug: verified.sourceSeat.slug,\n name: verified.sourceSeat.name,\n },\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 ...(skew.status === 'project-older'\n ? {\n schemaSkew: {\n status: skew.status,\n cliSchemaVersion: skew.cliSchemaVersion,\n projectSchemaVersion: skew.projectSchemaVersion,\n },\n }\n : {}),\n ...(notice ? { toolchain: notice } : {}),\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\n/**\n * The publish-target verification gate.\n *\n * FORBIDDEN (exit 5) whenever the resolved source is not among the caller's\n * seats — regardless of whether it came from `--source` or ekanos.json.\n * Beyond that, the confirmation rules branch on how the source was resolved:\n *\n * - `--source` THIS invocation: explicit is consent. Proceed.\n * - ekanos.json, exactly one seat held (which must be the match, or the\n * FORBIDDEN check above would have already fired): proceed, but say so —\n * there is nothing to disambiguate, but the target should never be silent.\n * - ekanos.json, multiple seats held: the ambiguous case this mechanism\n * exists for. Only when BOTH human (non-JSON) mode AND stdin is a real\n * TTY does it print the target and require an explicit y/N confirm\n * (`--yes` skips it) — see `canPrompt` below for why stdin matters, not\n * just stdout. Every other combination (JSON mode, or human mode with a\n * closed/non-interactive stdin) NEVER prompts — it proceeds only with\n * `--yes`, otherwise VALIDATION (exit 3) with the seat list, naming\n * `--source`, `--yes` and `ekanos use`.\n */\nasync function verifyPublishTarget(\n ctx: CliContext,\n env: AuthEnvironment,\n session: StoredSession,\n args: SourceResolution & {\n slug: string;\n version: string;\n yes: boolean;\n /** Real interactivity signal for stdin — see `canPrompt` below. */\n stdinIsTTY: boolean;\n confirm: (question: string) => Promise<boolean>;\n },\n): Promise<{ sourceSeat: SourceSeat; session: StoredSession }> {\n const result = await fetchSeatsWithOneRefresh(env, session);\n\n if (result.status === 'unauthorized') {\n stillUnauthorizedSourceSeats(env.host);\n }\n\n const seat = result.sources.find((s) => s.slug === args.source);\n\n if (!seat) {\n throw forbiddenError(\n `This account does not hold a seat on source \"${args.source}\" on ${env.host}.`,\n seatsHint(result.sources, env.host),\n );\n }\n\n if (args.explicit) {\n return { sourceSeat: seat, session: result.session };\n }\n\n if (result.sources.length === 1) {\n ctx.log(\n `Publishing to ${seat.name ?? seat.slug} (${seat.slug}) — your only ` +\n `developer seat.`,\n );\n return { sourceSeat: seat, session: result.session };\n }\n\n // Implicit source, multiple seats: the ambiguous case.\n //\n // `ctx.jsonMode` alone is NOT enough to decide whether an interactive\n // confirm is safe: `--no-json` forces human mode even when stdout is\n // piped, and neither `--no-json` nor a bare human run says anything about\n // stdin. Prompting `readline`'s `.question()` against a closed/non-TTY\n // stdin (a cron job, a wrapper script piping stdout but not attached to a\n // terminal) never resolves — the process hangs instead of exiting with a\n // clean code. So an actual prompt requires BOTH: human mode AND a real\n // stdin TTY. Anything else — JSON mode, or human mode with non-interactive\n // stdin — falls through to the same non-prompting `--yes`-or-refuse rule.\n const canPrompt = !ctx.jsonMode && args.stdinIsTTY;\n\n if (canPrompt) {\n ctx.log(\n `Publishing ${args.slug}@${args.version} to SOURCE ${seat.name ?? seat.slug} ` +\n `(${seat.slug}) on ${env.host}`,\n );\n\n if (!args.yes) {\n const confirmed = await args.confirm('Proceed? [y/N]');\n\n if (!confirmed) {\n throw validationError(\n `Publish to \"${seat.slug}\" was not confirmed.`,\n multiSeatHint(result.sources),\n );\n }\n }\n\n return { sourceSeat: seat, session: result.session };\n }\n\n if (args.yes) {\n return { sourceSeat: seat, session: result.session };\n }\n\n throw validationError(\n `Ambiguous publish target: this account holds ${result.sources.length} ` +\n `developer seats and the source was resolved from ekanos.json, not ` +\n `an explicit flag.`,\n multiSeatHint(result.sources),\n );\n}\n\nfunction seatsHint(sources: SourceSeat[], host: string): string {\n if (sources.length === 0) {\n return (\n `You hold no seats on any source on ${host}. Ask a Fusion super-admin ` +\n `to grant your account the developer seat on the intended source.`\n );\n }\n\n return (\n `You hold a seat on: ${describeSeats(sources)}. Pass \"--source <slug>\" ` +\n `naming one of these, or run \"ekanos use <slug>\" to set the project's ` +\n `default.`\n );\n}\n\nfunction multiSeatHint(sources: SourceSeat[]): string {\n return (\n `You hold seats on: ${describeSeats(sources)}. Pass \"--source <slug>\" ` +\n `to name the target explicitly, add \"--yes\" to confirm the target ` +\n `already in ekanos.json non-interactively, or run \"ekanos use <slug>\" ` +\n `to change the project's default.`\n );\n}\n\nfunction describeSeats(sources: SourceSeat[]): string {\n return sources.map((s) => `${s.slug} (${s.roles.join(', ')})`).join(', ');\n}\n\n/**\n * The real, non-test y/N prompt. Reads a line from stdin and writes the\n * question to stderr, so it never touches stdout — the one stream the JSON\n * envelope invariant depends on. Only reachable when `verifyPublishTarget`'s\n * `canPrompt` is true — human mode AND a real stdin TTY — so this never\n * blocks on a closed or non-interactive stdin.\n */\n/**\n * Exported so `upgrade` can reuse the exact same y/N prompt rather than\n * writing a second one — see `canPrompt` above for the stdin-TTY gating this\n * is only ever invoked behind.\n */\nexport async function promptYesNo(question: string): Promise<boolean> {\n const readline = await import('node:readline/promises');\n const rl = readline.createInterface({\n input: process.stdin,\n output: process.stderr,\n });\n\n try {\n const answer = await rl.question(`${question} `);\n return /^y(es)?$/i.test(answer.trim());\n } finally {\n rl.close();\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\n/**\n * Resolve the source slug (flag → ekanos.json) and whether it was given\n * EXPLICITLY this invocation — the fact `verifyPublishTarget` uses to decide\n * whether a confirmation is even in play. `flag !== undefined` (rather than\n * \"non-empty after trim\") is deliberate: an explicit `--source` that is\n * effectively empty is still a caller who typed the flag, not one who\n * fell through to ekanos.json.\n */\nasync function resolveSourceSlug(\n env: AuthEnvironment,\n session: StoredSession,\n flag: string | undefined,\n loaded: LoadedProject,\n): Promise<SourceResolution & { session: StoredSession }> {\n const explicit = flag !== undefined;\n const raw = flag ?? loaded.project.source;\n\n if (!raw || raw.trim().length === 0) {\n return noSourceError(env, session);\n }\n\n return { source: raw.trim(), explicit, session };\n}\n\n/**\n * The no-source validation error, enriched with the caller's seat list when\n * it can be fetched — best-effort, since a session is guaranteed to exist\n * here (publish already called `requireSession`) but the fetch itself can\n * still fail (network, MFA, a down deployment). A failure to enrich must\n * never mask or replace the primary \"no source given\" error.\n */\nasync function noSourceError(\n env: AuthEnvironment,\n session: StoredSession,\n): Promise<never> {\n let seatsNote = '';\n\n try {\n const result = await fetchSeatsWithOneRefresh(env, session);\n\n if (result.status === 'ok') {\n seatsNote =\n result.sources.length > 0\n ? ` You hold a seat on: ${describeSeats(result.sources)}.`\n : ' You hold no seats on any source on this host yet.';\n }\n } catch {\n // Best-effort — the primary error below stands on its own.\n }\n\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.${seatsNote}`,\n );\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 */\nexport function 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 */\nexport function 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 type { Finding } from '@ekanos/integration-schema';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport type { StoredSession } from '../auth/credential-store';\nimport {\n type AuthEnvironment,\n refreshStoredSession,\n requireSession,\n resolveAuthEnvironment,\n} from '../auth/session';\nimport type { CliContext } from '../context';\nimport {\n forbiddenError,\n gateFailedError,\n preconditionError,\n validationError,\n} from '../errors';\nimport type { ExitCode } from '../exit-codes';\nimport { renderFindings } from '../findings-renderer';\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 { assertSchemaNotSkewed } from '../schema-skew';\nimport { fetchSeatsWithOneRefresh } from '../seats';\nimport { type SourceSeat, stillUnauthorizedSourceSeats } from '../sources-api';\nimport { announceUpdate, checkForUpdate } from '../update-notice';\nimport { collectAllFindings } from '../validate-findings';\n\n/**\n * Checks that are WARN in plain `ekanos validate` but must refuse a\n * `publish` outright: a project that will fail the gate is not allowed to\n * upload and find that out from the gate instead of from itself.\n * `project.host-compiler-rules` (see `project-checks.ts`) is WARN-severity\n * everywhere else on purpose — it reads a partner's local tsconfig, not the\n * gate's own verdict, so it must not block plain validate on a false\n * positive — but at the publish gate, \"this WILL fail the gate\" is exactly\n * the thing a submission preflight exists to refuse. A list, not a second\n * check, so the underlying rule and its message/hint stay defined in exactly\n * one place.\n */\nconst PUBLISH_ERROR_ESCALATIONS: readonly string[] = [\n 'project.host-compiler-rules',\n];\n\n/**\n * Escalate the checks in `PUBLISH_ERROR_ESCALATIONS` from `warn` to `error`\n * for THIS publish run only — `validate`'s own output (and the underlying\n * finding objects elsewhere) is never touched.\n */\nfunction escalateForPublish(findings: Finding[]): Finding[] {\n return findings.map((finding) =>\n finding.severity === 'warn' &&\n PUBLISH_ERROR_ESCALATIONS.includes(finding.check)\n ? { ...finding, severity: 'error' }\n : finding,\n );\n}\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 /**\n * Skips the interactive confirm when the source resolved IMPLICITLY (from\n * ekanos.json) and the caller holds more than one developer seat. Has no\n * effect when `--source` was passed this invocation (explicit is already\n * consent) or when the caller holds exactly one seat (nothing to\n * disambiguate).\n */\n yes: boolean;\n env: Record<string, string | undefined>;\n /**\n * Whether stdin is a real interactive terminal — the SECOND condition (with\n * human/non-JSON mode) required before publish will ever prompt. Defaults\n * to `process.stdin.isTTY === true`. Injectable so tests can exercise the\n * confirm branch without a real TTY.\n */\n stdinIsTTY?: boolean;\n /**\n * Injectable y/N prompt, for tests. Defaults to a real readline prompt over\n * stdin/stderr. Never invoked unless BOTH human mode and a real stdin TTY\n * are in play — see `canPrompt` in `verifyPublishTarget`.\n */\n confirm?: (question: string) => Promise<boolean>;\n}\n\n/** Where the resolved source slug came from — governs the confirmation rules below. */\ninterface SourceResolution {\n source: string;\n /** True iff `--source` was passed THIS invocation. */\n explicit: boolean;\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\n // Fail fast, before anything else: a project whose schema is newer than\n // this CLI bundles cannot be trusted to validate correctly, so publishing\n // it — the one verb whose findings gate is meant to be authoritative —\n // must refuse before packing or submitting anything. See `schema-skew.ts`.\n const skew = assertSchemaNotSkewed(ctx, loaded.projectDir);\n\n const notice = await checkForUpdate({\n env: args.env,\n host: env.host,\n session,\n });\n announceUpdate(ctx, notice);\n\n // The host would reject a build from a CLI it has declared unsupported\n // anyway (see the wire contract in `toolchain-api.ts`) — refuse here with a\n // hint naming the fix, rather than let the submission round-trip fail with\n // whatever generic error the host happens to answer with.\n if (notice?.status === 'unsupported') {\n throw preconditionError(\n `@ekanos/cli ${notice.current.cli} is below ${env.host}'s minimum ` +\n `supported version (needs at least ${notice.target.cli}).`,\n 'Run \"ekanos upgrade\" before publishing — the host will refuse this ' +\n 'submission from an unsupported CLI regardless.',\n );\n }\n\n const resolution = await resolveSourceSlug(env, session, 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 = escalateForPublish(await collectAllFindings(loaded));\n const errorCount = findings.filter((f) => f.severity === 'error').length;\n\n if (errorCount > 0) {\n // Same rule as `validate`: render the shared findings block to stderr\n // BEFORE the envelope settles, human mode only. JSON mode carries the\n // identical findings in `data.findings` and must stay byte-identical.\n // Closing order is findings block -> short summary (via `ctx.fail`'s\n // `findingsSummary` option) -> next action.\n if (!ctx.jsonMode) {\n ctx.log(\n ctx.ui.err(\n `${ctx.ui.glyphs.err} Refusing to publish: ${errorCount} finding` +\n `${errorCount === 1 ? '' : 's'}.`,\n ),\n );\n renderFindings(ctx, findings, { cwd: loaded.projectDir });\n }\n\n const exitCode = 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 !ctx.jsonMode\n ? { findingsSummary: { command: 'ekanos publish', errorCount } }\n : undefined,\n );\n\n if (!ctx.jsonMode) {\n ctx.log(\n ctx.ui.nextAction(\n 'ekanos validate',\n 'fix the error-severity findings above, then re-run \"ekanos publish\"',\n ),\n );\n }\n\n return exitCode;\n }\n\n if (!ctx.jsonMode) {\n ctx.log(\n ctx.ui.ok(\n `${ctx.ui.glyphs.ok} validated ${slug} — no error-severity findings.`,\n ),\n );\n }\n\n // The 100%-certainty mechanism: confirm the resolved source is one the\n // caller actually holds a seat on — and, when it was resolved IMPLICITLY\n // from ekanos.json with more than one seat in play, that a human (or an\n // explicit --yes) actually intends this target — BEFORE anything is packed\n // or sent over the wire.\n const verified = await verifyPublishTarget(ctx, env, resolution.session, {\n source: resolution.source,\n explicit: resolution.explicit,\n slug,\n version,\n yes: args.yes,\n stdinIsTTY: args.stdinIsTTY ?? process.stdin.isTTY === true,\n confirm: args.confirm ?? promptYesNo,\n });\n\n const source = verified.sourceSeat.slug;\n\n ctx.log(`Packing ${slug}@${version}…`);\n\n const packed = packProject(loaded.projectDir);\n const manifest = buildManifest(loaded, packed.files);\n\n if (!ctx.jsonMode) {\n ctx.log(\n ctx.ui.ok(\n `${ctx.ui.glyphs.ok} packed ${slug}@${version} (${packed.files.length} files).`,\n ),\n );\n }\n\n ctx.log(`Submitting to ${env.host} (source \"${source}\")…`);\n\n const receipt = await submitWithOneRefresh(env, verified.session, {\n source,\n slug,\n version,\n manifest,\n archive: packed.archive,\n });\n\n if (!ctx.jsonMode) {\n ctx.log(\n ctx.ui.ok(\n `${ctx.ui.glyphs.ok} submitted ${receipt.slug}@${receipt.version} ` +\n `(state \"${receipt.state}\").`,\n ),\n );\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 id: verified.sourceSeat.id,\n slug: verified.sourceSeat.slug,\n name: verified.sourceSeat.name,\n },\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 ...(skew.status === 'project-older'\n ? {\n schemaSkew: {\n status: skew.status,\n cliSchemaVersion: skew.cliSchemaVersion,\n projectSchemaVersion: skew.projectSchemaVersion,\n },\n }\n : {}),\n ...(notice ? { toolchain: notice } : {}),\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\n/**\n * The publish-target verification gate.\n *\n * FORBIDDEN (exit 5) whenever the resolved source is not among the caller's\n * seats — regardless of whether it came from `--source` or ekanos.json.\n * Beyond that, the confirmation rules branch on how the source was resolved:\n *\n * - `--source` THIS invocation: explicit is consent. Proceed.\n * - ekanos.json, exactly one seat held (which must be the match, or the\n * FORBIDDEN check above would have already fired): proceed, but say so —\n * there is nothing to disambiguate, but the target should never be silent.\n * - ekanos.json, multiple seats held: the ambiguous case this mechanism\n * exists for. Only when BOTH human (non-JSON) mode AND stdin is a real\n * TTY does it print the target and require an explicit y/N confirm\n * (`--yes` skips it) — see `canPrompt` below for why stdin matters, not\n * just stdout. Every other combination (JSON mode, or human mode with a\n * closed/non-interactive stdin) NEVER prompts — it proceeds only with\n * `--yes`, otherwise VALIDATION (exit 3) with the seat list, naming\n * `--source`, `--yes` and `ekanos use`.\n */\nasync function verifyPublishTarget(\n ctx: CliContext,\n env: AuthEnvironment,\n session: StoredSession,\n args: SourceResolution & {\n slug: string;\n version: string;\n yes: boolean;\n /** Real interactivity signal for stdin — see `canPrompt` below. */\n stdinIsTTY: boolean;\n confirm: (question: string) => Promise<boolean>;\n },\n): Promise<{ sourceSeat: SourceSeat; session: StoredSession }> {\n const result = await fetchSeatsWithOneRefresh(env, session);\n\n if (result.status === 'unauthorized') {\n stillUnauthorizedSourceSeats(env.host);\n }\n\n const seat = result.sources.find((s) => s.slug === args.source);\n\n if (!seat) {\n throw forbiddenError(\n `This account does not hold a seat on source \"${args.source}\" on ${env.host}.`,\n seatsHint(result.sources, env.host),\n );\n }\n\n if (args.explicit) {\n return { sourceSeat: seat, session: result.session };\n }\n\n if (result.sources.length === 1) {\n ctx.log(\n `Publishing to ${seat.name ?? seat.slug} (${seat.slug}) — your only ` +\n `developer seat.`,\n );\n return { sourceSeat: seat, session: result.session };\n }\n\n // Implicit source, multiple seats: the ambiguous case.\n //\n // `ctx.jsonMode` alone is NOT enough to decide whether an interactive\n // confirm is safe: `--no-json` forces human mode even when stdout is\n // piped, and neither `--no-json` nor a bare human run says anything about\n // stdin. Prompting `readline`'s `.question()` against a closed/non-TTY\n // stdin (a cron job, a wrapper script piping stdout but not attached to a\n // terminal) never resolves — the process hangs instead of exiting with a\n // clean code. So an actual prompt requires BOTH: human mode AND a real\n // stdin TTY. Anything else — JSON mode, or human mode with non-interactive\n // stdin — falls through to the same non-prompting `--yes`-or-refuse rule.\n const canPrompt = !ctx.jsonMode && args.stdinIsTTY;\n\n if (canPrompt) {\n ctx.log(\n `Publishing ${args.slug}@${args.version} to SOURCE ${seat.name ?? seat.slug} ` +\n `(${seat.slug}) on ${env.host}`,\n );\n\n if (!args.yes) {\n const confirmed = await args.confirm('Proceed? [y/N]');\n\n if (!confirmed) {\n throw validationError(\n `Publish to \"${seat.slug}\" was not confirmed.`,\n multiSeatHint(result.sources),\n );\n }\n }\n\n return { sourceSeat: seat, session: result.session };\n }\n\n if (args.yes) {\n return { sourceSeat: seat, session: result.session };\n }\n\n throw validationError(\n `Ambiguous publish target: this account holds ${result.sources.length} ` +\n `developer seats and the source was resolved from ekanos.json, not ` +\n `an explicit flag.`,\n multiSeatHint(result.sources),\n );\n}\n\nfunction seatsHint(sources: SourceSeat[], host: string): string {\n if (sources.length === 0) {\n return (\n `You hold no seats on any source on ${host}. Ask a Fusion super-admin ` +\n `to grant your account the developer seat on the intended source.`\n );\n }\n\n return (\n `You hold a seat on: ${describeSeats(sources)}. Pass \"--source <slug>\" ` +\n `naming one of these, or run \"ekanos use <slug>\" to set the project's ` +\n `default.`\n );\n}\n\nfunction multiSeatHint(sources: SourceSeat[]): string {\n return (\n `You hold seats on: ${describeSeats(sources)}. Pass \"--source <slug>\" ` +\n `to name the target explicitly, add \"--yes\" to confirm the target ` +\n `already in ekanos.json non-interactively, or run \"ekanos use <slug>\" ` +\n `to change the project's default.`\n );\n}\n\nfunction describeSeats(sources: SourceSeat[]): string {\n return sources.map((s) => `${s.slug} (${s.roles.join(', ')})`).join(', ');\n}\n\n/**\n * The real, non-test y/N prompt. Reads a line from stdin and writes the\n * question to stderr, so it never touches stdout — the one stream the JSON\n * envelope invariant depends on. Only reachable when `verifyPublishTarget`'s\n * `canPrompt` is true — human mode AND a real stdin TTY — so this never\n * blocks on a closed or non-interactive stdin.\n */\n/**\n * Exported so `upgrade` can reuse the exact same y/N prompt rather than\n * writing a second one — see `canPrompt` above for the stdin-TTY gating this\n * is only ever invoked behind.\n */\nexport async function promptYesNo(question: string): Promise<boolean> {\n const readline = await import('node:readline/promises');\n const rl = readline.createInterface({\n input: process.stdin,\n output: process.stderr,\n });\n\n try {\n const answer = await rl.question(`${question} `);\n return /^y(es)?$/i.test(answer.trim());\n } finally {\n rl.close();\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\n/**\n * Resolve the source slug (flag → ekanos.json) and whether it was given\n * EXPLICITLY this invocation — the fact `verifyPublishTarget` uses to decide\n * whether a confirmation is even in play. `flag !== undefined` (rather than\n * \"non-empty after trim\") is deliberate: an explicit `--source` that is\n * effectively empty is still a caller who typed the flag, not one who\n * fell through to ekanos.json.\n */\nasync function resolveSourceSlug(\n env: AuthEnvironment,\n session: StoredSession,\n flag: string | undefined,\n loaded: LoadedProject,\n): Promise<SourceResolution & { session: StoredSession }> {\n const explicit = flag !== undefined;\n const raw = flag ?? loaded.project.source;\n\n if (!raw || raw.trim().length === 0) {\n return noSourceError(env, session);\n }\n\n return { source: raw.trim(), explicit, session };\n}\n\n/**\n * The no-source validation error, enriched with the caller's seat list when\n * it can be fetched — best-effort, since a session is guaranteed to exist\n * here (publish already called `requireSession`) but the fetch itself can\n * still fail (network, MFA, a down deployment). A failure to enrich must\n * never mask or replace the primary \"no source given\" error.\n */\nasync function noSourceError(\n env: AuthEnvironment,\n session: StoredSession,\n): Promise<never> {\n let seatsNote = '';\n\n try {\n const result = await fetchSeatsWithOneRefresh(env, session);\n\n if (result.status === 'ok') {\n seatsNote =\n result.sources.length > 0\n ? ` You hold a seat on: ${describeSeats(result.sources)}.`\n : ' You hold no seats on any source on this host yet.';\n }\n } catch {\n // Best-effort — the primary error below stands on its own.\n }\n\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.${seatsNote}`,\n );\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 */\nexport function 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 */\nexport function 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,5 +1,7 @@
|
|
|
1
1
|
import type { CliContext } from '../context.js';
|
|
2
2
|
import { type ExitCode } from '../exit-codes.js';
|
|
3
|
+
import { type SubmissionListing, type SubmissionVersion } from '../publish-api.js';
|
|
4
|
+
import type { Ui } from '../ui.js';
|
|
3
5
|
export interface StatusArgs {
|
|
4
6
|
host?: string;
|
|
5
7
|
/** Project directory (may contain ekanos.json). Defaults to cwd. */
|
|
@@ -7,6 +9,14 @@ export interface StatusArgs {
|
|
|
7
9
|
env: Record<string, string | undefined>;
|
|
8
10
|
/** Render each version's full finding list, not just error/warn counts. */
|
|
9
11
|
findings?: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Expand "OTHER INTEGRATIONS IN SOURCE …" from one summary line per
|
|
14
|
+
* integration into the same VERSION/STATE/AGE/FINDINGS table shape THIS
|
|
15
|
+
* PROJECT's section uses. Human mode only — the JSON envelope's
|
|
16
|
+
* `submissions` already carries every integration's full version history
|
|
17
|
+
* regardless of this flag.
|
|
18
|
+
*/
|
|
19
|
+
all?: boolean;
|
|
10
20
|
/**
|
|
11
21
|
* Block until this project's current (slug, version) submission reaches a
|
|
12
22
|
* terminal-for-waiting state (gated, dev, rejected), polling every 30s.
|
|
@@ -15,6 +25,8 @@ export interface StatusArgs {
|
|
|
15
25
|
watch?: boolean;
|
|
16
26
|
/** Injectable for tests — see `auth/device-flow.ts`'s identical pattern. */
|
|
17
27
|
sleep?: (ms: number) => Promise<void>;
|
|
28
|
+
/** Injectable "now" for deterministic AGE rendering in tests. Defaults to `new Date()`. */
|
|
29
|
+
now?: Date;
|
|
18
30
|
}
|
|
19
31
|
/**
|
|
20
32
|
* `status` — the read verb: where am I pointed, who am I, and what has this
|
|
@@ -36,8 +48,12 @@ export interface StatusArgs {
|
|
|
36
48
|
* Exit 0 whenever the report itself could be assembled — EXCEPT under
|
|
37
49
|
* `--watch` (see below), whose exit code carries the gate's verdict instead.
|
|
38
50
|
*
|
|
39
|
-
*
|
|
40
|
-
* server-side
|
|
51
|
+
* The submission listing always requests the full `?detail=findings` shape
|
|
52
|
+
* server-side (needed so THIS PROJECT's latest-error auto-inline rendering
|
|
53
|
+
* — see `humanSummary` — has findings to inspect without a second
|
|
54
|
+
* round-trip). `--findings` no longer changes what's FETCHED; it changes
|
|
55
|
+
* what's RENDERED inline in human mode: every shown version's findings,
|
|
56
|
+
* not just the latest's error-severity ones.
|
|
41
57
|
*
|
|
42
58
|
* `--watch` turns `status` into a blocking wait: it requires a project with
|
|
43
59
|
* a resolved source and a live session (both are PRECONDITIONs here, not
|
|
@@ -50,5 +66,55 @@ export interface StatusArgs {
|
|
|
50
66
|
* branches on it identically whether the gate ran inline (future) or via
|
|
51
67
|
* this poll (today). `--json --watch` still emits exactly ONE envelope, at
|
|
52
68
|
* completion — the polling itself only ever writes to stderr via `ctx.log`.
|
|
69
|
+
*
|
|
70
|
+
* Human-mode rendering is PROJECT-SCOPED: the server lists every
|
|
71
|
+
* submission for the resolved source (an org-level bucket shared by every
|
|
72
|
+
* project publishing there — `listSubmissions` has no server-side slug
|
|
73
|
+
* filter, see `publish-api.ts`'s wire contract), so "THIS PROJECT" groups
|
|
74
|
+
* the rows matching this project's own slug and "OTHER INTEGRATIONS IN
|
|
75
|
+
* SOURCE …" collapses everything else to one summary line per integration
|
|
76
|
+
* (or, with `--all`, the same table shape).
|
|
77
|
+
* The `--json` envelope stays additive-compatible: `submissions` is still
|
|
78
|
+
* the full, unfiltered list, with `data.project.scopedSubmissions` and
|
|
79
|
+
* `data.otherIntegrations` layered on top for a consumer that wants the
|
|
80
|
+
* pre-grouped view without re-deriving it.
|
|
53
81
|
*/
|
|
54
82
|
export declare function runStatus(ctx: CliContext, args: StatusArgs): Promise<ExitCode>;
|
|
83
|
+
/**
|
|
84
|
+
* Newest-first by `gatedAt ?? submittedAt`. Ties (including two versions
|
|
85
|
+
* with no timestamp at all) keep their original, server-given order —
|
|
86
|
+
* `Array#sort` is stable, and a stable no-op is the right answer when there
|
|
87
|
+
* is nothing to compare.
|
|
88
|
+
*/
|
|
89
|
+
export declare function sortVersionsNewestFirst(versions: readonly SubmissionVersion[]): SubmissionVersion[];
|
|
90
|
+
export declare function findingsCellParts(ui: Ui, version: SubmissionVersion): {
|
|
91
|
+
text: string;
|
|
92
|
+
paint: (text: string) => string;
|
|
93
|
+
};
|
|
94
|
+
export declare function stateColorFor(ui: Ui, state: string): (text: string) => string;
|
|
95
|
+
/**
|
|
96
|
+
* A VERSION/STATE/AGE/FINDINGS table, built by hand rather than via
|
|
97
|
+
* `ui.table`: STATE and FINDINGS need per-cell color, and `ui.table` sizes
|
|
98
|
+
* columns from the raw cell strings it's handed — a colored cell's ANSI
|
|
99
|
+
* escapes would inflate that width and misalign the column. `ui.ts`'s own
|
|
100
|
+
* doc comment on `padCell` names this exact tradeoff ("ignoring color codes
|
|
101
|
+
* in length math is the CALLER's job — pad before coloring, never after"),
|
|
102
|
+
* so this pads against the PLAIN text first and colors the already-padded
|
|
103
|
+
* string — the same rule `ui.table` documents, just applied here instead of
|
|
104
|
+
* inside it.
|
|
105
|
+
*/
|
|
106
|
+
export declare function renderVersionsTable(ui: Ui, versions: readonly SubmissionVersion[], now: Date): string;
|
|
107
|
+
export interface OtherIntegrationSummary {
|
|
108
|
+
slug: string;
|
|
109
|
+
name: string | null;
|
|
110
|
+
versionsCount: number;
|
|
111
|
+
latestVersion: string | null;
|
|
112
|
+
latestState: string | null;
|
|
113
|
+
}
|
|
114
|
+
export declare function summarizeIntegration(integration: SubmissionListing): OtherIntegrationSummary;
|
|
115
|
+
/**
|
|
116
|
+
* The closing `ui.nextAction` line, chosen from the LATEST version of THIS
|
|
117
|
+
* project — `null` (no closing line at all) whenever nothing is pending:
|
|
118
|
+
* no submission yet, or the latest is already `dev`/`prod`/`revoked`.
|
|
119
|
+
*/
|
|
120
|
+
export declare function buildNextAction(ui: Ui, latest: SubmissionVersion | null): string | null;
|