@ekanos/cli 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +132 -6
- package/dist/bin.js +14 -1
- package/dist/bin.js.map +1 -1
- package/dist/commands/dev.d.ts +4 -0
- package/dist/commands/dev.js +6 -20
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/init.d.ts +25 -0
- package/dist/commands/init.js +19 -8
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/publish.d.ts +59 -1
- package/dist/commands/publish.js +209 -25
- package/dist/commands/publish.js.map +1 -1
- package/dist/commands/sources.d.ts +17 -0
- package/dist/commands/sources.js +75 -0
- package/dist/commands/sources.js.map +1 -0
- package/dist/commands/status.js +9 -2
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/upgrade.d.ts +47 -0
- package/dist/commands/upgrade.js +445 -0
- package/dist/commands/upgrade.js.map +1 -0
- package/dist/commands/use.d.ts +21 -0
- package/dist/commands/use.js +62 -0
- package/dist/commands/use.js.map +1 -0
- package/dist/commands/validate.d.ts +5 -0
- package/dist/commands/validate.js +20 -6
- package/dist/commands/validate.js.map +1 -1
- package/dist/commands/whoami.d.ts +6 -0
- package/dist/commands/whoami.js +26 -1
- package/dist/commands/whoami.js.map +1 -1
- package/dist/context.d.ts +9 -0
- package/dist/context.js +9 -0
- package/dist/context.js.map +1 -1
- package/dist/delegate.d.ts +28 -0
- package/dist/delegate.js +136 -0
- package/dist/delegate.js.map +1 -0
- package/dist/harness-scaffold.d.ts +21 -6
- package/dist/harness-scaffold.js +8 -5
- package/dist/harness-scaffold.js.map +1 -1
- package/dist/index.d.ts +8 -0
- package/dist/index.js +81 -4
- package/dist/index.js.map +1 -1
- package/dist/package-manager.d.ts +10 -0
- package/dist/package-manager.js +32 -0
- package/dist/package-manager.js.map +1 -1
- package/dist/schema-skew.d.ts +77 -0
- package/dist/schema-skew.js +163 -0
- package/dist/schema-skew.js.map +1 -0
- package/dist/seats.d.ts +21 -0
- package/dist/seats.js +15 -0
- package/dist/seats.js.map +1 -0
- package/dist/sources-api.d.ts +44 -0
- package/dist/sources-api.js +69 -0
- package/dist/sources-api.js.map +1 -0
- package/dist/toolchain-api.d.ts +54 -0
- package/dist/toolchain-api.js +58 -0
- package/dist/toolchain-api.js.map +1 -0
- package/dist/toolchain-resolve.d.ts +26 -0
- package/dist/toolchain-resolve.js +100 -0
- package/dist/toolchain-resolve.js.map +1 -0
- package/dist/toolchain.d.ts +21 -0
- package/dist/toolchain.js +16 -0
- package/dist/toolchain.js.map +1 -0
- package/dist/update-notice.d.ts +32 -0
- package/dist/update-notice.js +180 -0
- package/dist/update-notice.js.map +1 -0
- package/dist/validate-findings.d.ts +12 -0
- package/dist/validate-findings.js +12 -0
- package/dist/validate-findings.js.map +1 -1
- package/package.json +1 -1
- package/templates/AGENTS.md.tmpl +81 -18
- package/templates/CLAUDE.md.tmpl +2 -1
- package/templates/claude-skill.md.tmpl +45 -13
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { requireSession, resolveAuthEnvironment } from '../auth/session.js';
|
|
2
|
+
import { forbiddenError, usageError } from '../errors.js';
|
|
3
|
+
import { loadProject } from '../project.js';
|
|
4
|
+
import { fetchSeatsWithOneRefresh } from '../seats.js';
|
|
5
|
+
import { stillUnauthorizedSourceSeats } from '../sources-api.js';
|
|
6
|
+
import { persistProjectFields } from './publish.js';
|
|
7
|
+
/**
|
|
8
|
+
* `use <source-slug>` — set this project's publish target, with the same
|
|
9
|
+
* seat verification `publish` runs: the slug must name a source the caller
|
|
10
|
+
* actually holds a seat on, so a project can never be pointed at a source by
|
|
11
|
+
* a typo alone.
|
|
12
|
+
*
|
|
13
|
+
* Persisted via the exact same mechanics `publish` uses after a successful
|
|
14
|
+
* submission (`persistProjectFields`), so `host` also sticks on first write
|
|
15
|
+
* and a later `publish` needs neither flag.
|
|
16
|
+
*/
|
|
17
|
+
export async function runUse(ctx, args) {
|
|
18
|
+
var _a;
|
|
19
|
+
if (!args.slug || args.slug.trim().length === 0) {
|
|
20
|
+
throw usageError('No source slug given.', 'Run "ekanos use <source-slug>" — see "ekanos sources" for the ' +
|
|
21
|
+
'slugs you hold a seat on.');
|
|
22
|
+
}
|
|
23
|
+
const slug = args.slug.trim();
|
|
24
|
+
// Requires a project up front: `use` sets ekanos.json's "source" field, so
|
|
25
|
+
// running it outside one is a precondition failure, not a network call.
|
|
26
|
+
const loaded = loadProject(args.dir);
|
|
27
|
+
const env = resolveAuthEnvironment(ctx, args.host, args.env, {
|
|
28
|
+
projectDir: args.dir,
|
|
29
|
+
});
|
|
30
|
+
const session = requireSession(env.store, env.host);
|
|
31
|
+
const result = await fetchSeatsWithOneRefresh(env, session);
|
|
32
|
+
if (result.status === 'unauthorized') {
|
|
33
|
+
stillUnauthorizedSourceSeats(env.host);
|
|
34
|
+
}
|
|
35
|
+
const seat = result.sources.find((s) => s.slug === slug);
|
|
36
|
+
if (!seat) {
|
|
37
|
+
throw forbiddenError(`This account does not hold a seat on source "${slug}" on ${env.host}.`, seatsHint(result.sources, env.host));
|
|
38
|
+
}
|
|
39
|
+
const persisted = persistProjectFields(loaded, seat.slug, env.host);
|
|
40
|
+
return ctx.succeed({
|
|
41
|
+
host: env.host,
|
|
42
|
+
source: {
|
|
43
|
+
id: seat.id,
|
|
44
|
+
slug: seat.slug,
|
|
45
|
+
name: seat.name,
|
|
46
|
+
roles: seat.roles,
|
|
47
|
+
},
|
|
48
|
+
sourcePersisted: persisted.source,
|
|
49
|
+
hostPersisted: persisted.host,
|
|
50
|
+
}, `use: ekanos.json now publishes to ${(_a = seat.name) !== null && _a !== void 0 ? _a : seat.slug} (${seat.slug}) on ${env.host}.`);
|
|
51
|
+
}
|
|
52
|
+
function seatsHint(sources, host) {
|
|
53
|
+
if (sources.length === 0) {
|
|
54
|
+
return (`You hold no seats on any source on ${host}. Ask a Fusion super-admin ` +
|
|
55
|
+
`to grant your account a seat on the intended source.`);
|
|
56
|
+
}
|
|
57
|
+
const list = sources
|
|
58
|
+
.map((s) => `${s.slug} (${s.roles.join(', ')})`)
|
|
59
|
+
.join(', ');
|
|
60
|
+
return `You hold a seat on: ${list}. Run "ekanos use <slug>" with one of these.`;
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=use.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use.js","sourceRoot":"","sources":["../../src/commands/use.ts"],"names":[],"mappings":"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","sourcesContent":["import { requireSession, resolveAuthEnvironment } from '../auth/session';\nimport type { CliContext } from '../context';\nimport { forbiddenError, usageError } from '../errors';\nimport type { ExitCode } from '../exit-codes';\nimport { loadProject } from '../project';\nimport { fetchSeatsWithOneRefresh } from '../seats';\nimport { type SourceSeat, stillUnauthorizedSourceSeats } from '../sources-api';\nimport { persistProjectFields } from './publish';\n\nexport interface UseArgs {\n host?: string;\n /** Project directory (contains ekanos.json). Defaults to cwd. */\n dir: string;\n /** The source slug to switch this project's publish target to. */\n slug?: string;\n env: Record<string, string | undefined>;\n}\n\n/**\n * `use <source-slug>` — set this project's publish target, with the same\n * seat verification `publish` runs: the slug must name a source the caller\n * actually holds a seat on, so a project can never be pointed at a source by\n * a typo alone.\n *\n * Persisted via the exact same mechanics `publish` uses after a successful\n * submission (`persistProjectFields`), so `host` also sticks on first write\n * and a later `publish` needs neither flag.\n */\nexport async function runUse(\n ctx: CliContext,\n args: UseArgs,\n): Promise<ExitCode> {\n if (!args.slug || args.slug.trim().length === 0) {\n throw usageError(\n 'No source slug given.',\n 'Run \"ekanos use <source-slug>\" — see \"ekanos sources\" for the ' +\n 'slugs you hold a seat on.',\n );\n }\n\n const slug = args.slug.trim();\n\n // Requires a project up front: `use` sets ekanos.json's \"source\" field, so\n // running it outside one is a precondition failure, not a network call.\n const loaded = loadProject(args.dir);\n\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 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 === slug);\n\n if (!seat) {\n throw forbiddenError(\n `This account does not hold a seat on source \"${slug}\" on ${env.host}.`,\n seatsHint(result.sources, env.host),\n );\n }\n\n const persisted = persistProjectFields(loaded, seat.slug, env.host);\n\n return ctx.succeed(\n {\n host: env.host,\n source: {\n id: seat.id,\n slug: seat.slug,\n name: seat.name,\n roles: seat.roles,\n },\n sourcePersisted: persisted.source,\n hostPersisted: persisted.host,\n },\n `use: ekanos.json now publishes to ${seat.name ?? seat.slug} (${seat.slug}) on ${env.host}.`,\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 a seat on the intended source.`\n );\n }\n\n const list = sources\n .map((s) => `${s.slug} (${s.roles.join(', ')})`)\n .join(', ');\n\n return `You hold a seat on: ${list}. Run \"ekanos use <slug>\" with one of these.`;\n}\n"]}
|
|
@@ -3,6 +3,7 @@ import type { ExitCode } from '../exit-codes.js';
|
|
|
3
3
|
export interface ValidateArgs {
|
|
4
4
|
/** Project directory (contains ekanos.json). Defaults to cwd. */
|
|
5
5
|
dir: string;
|
|
6
|
+
env?: Record<string, string | undefined>;
|
|
6
7
|
}
|
|
7
8
|
/**
|
|
8
9
|
* `validate` — parse the declaration with the REAL zod schemas from
|
|
@@ -13,5 +14,9 @@ export interface ValidateArgs {
|
|
|
13
14
|
*
|
|
14
15
|
* The findings pass itself lives in `validate-findings.ts`, shared with
|
|
15
16
|
* `publish` so the publish gate cannot drift from what validate checks.
|
|
17
|
+
*
|
|
18
|
+
* Runs the schema-skew gate FIRST, before anything else — see
|
|
19
|
+
* `schema-skew.ts` for why a newer project schema than the CLI bundles makes
|
|
20
|
+
* every finding below it unreliable.
|
|
16
21
|
*/
|
|
17
22
|
export declare function runValidate(ctx: CliContext, args: ValidateArgs): Promise<ExitCode>;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { validationError } from '../errors.js';
|
|
2
2
|
import { loadProject } from '../project.js';
|
|
3
|
+
import { assertSchemaNotSkewed } from '../schema-skew.js';
|
|
4
|
+
import { announceUpdate, checkForUpdate } from '../update-notice.js';
|
|
3
5
|
import { collectAllFindings } from '../validate-findings.js';
|
|
4
6
|
/**
|
|
5
7
|
* `validate` — parse the declaration with the REAL zod schemas from
|
|
@@ -10,22 +12,34 @@ import { collectAllFindings } from '../validate-findings.js';
|
|
|
10
12
|
*
|
|
11
13
|
* The findings pass itself lives in `validate-findings.ts`, shared with
|
|
12
14
|
* `publish` so the publish gate cannot drift from what validate checks.
|
|
15
|
+
*
|
|
16
|
+
* Runs the schema-skew gate FIRST, before anything else — see
|
|
17
|
+
* `schema-skew.ts` for why a newer project schema than the CLI bundles makes
|
|
18
|
+
* every finding below it unreliable.
|
|
13
19
|
*/
|
|
14
20
|
export async function runValidate(ctx, args) {
|
|
21
|
+
var _a;
|
|
15
22
|
const loaded = loadProject(args.dir);
|
|
23
|
+
const skew = assertSchemaNotSkewed(ctx, loaded.projectDir);
|
|
24
|
+
const notice = await checkForUpdate({ env: (_a = args.env) !== null && _a !== void 0 ? _a : process.env });
|
|
25
|
+
announceUpdate(ctx, notice);
|
|
16
26
|
const slugs = loaded.integrations.map((i) => i.slug).join(', ');
|
|
17
27
|
ctx.log(`Validating ${loaded.integrations.length} integration` +
|
|
18
28
|
`${loaded.integrations.length === 1 ? '' : 's'} (${slugs})…`);
|
|
19
29
|
const findings = await collectAllFindings(loaded);
|
|
20
30
|
const errorCount = findings.filter((f) => f.severity === 'error').length;
|
|
21
|
-
const data = {
|
|
22
|
-
slug: loaded.primary.slug,
|
|
23
|
-
integrations: loaded.integrations.map((i) => ({
|
|
31
|
+
const data = Object.assign(Object.assign({ slug: loaded.primary.slug, integrations: loaded.integrations.map((i) => ({
|
|
24
32
|
slug: i.slug,
|
|
25
33
|
entry: i.entry,
|
|
26
|
-
})),
|
|
27
|
-
|
|
28
|
-
|
|
34
|
+
})), findings }, (skew.status === 'project-older'
|
|
35
|
+
? {
|
|
36
|
+
schemaSkew: {
|
|
37
|
+
status: skew.status,
|
|
38
|
+
cliSchemaVersion: skew.cliSchemaVersion,
|
|
39
|
+
projectSchemaVersion: skew.projectSchemaVersion,
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
: {})), (notice ? { toolchain: notice } : {}));
|
|
29
43
|
if (errorCount > 0) {
|
|
30
44
|
return ctx.fail(validationError(`${errorCount} validation finding${errorCount === 1 ? '' : 's'}.`, 'Resolve the error-severity findings in data.findings, then re-run ' +
|
|
31
45
|
'"ekanos validate".'), data);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate.js","sourceRoot":"","sources":["../../src/commands/validate.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAE5C,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"validate.js","sourceRoot":"","sources":["../../src/commands/validate.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAE5C,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClE,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAQ1D;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,GAAe,EACf,IAAkB;;IAElB,MAAM,MAAM,GAAG,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAErC,MAAM,IAAI,GAAG,qBAAqB,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC;IAE3D,MAAM,MAAM,GAAG,MAAM,cAAc,CAAC,EAAE,GAAG,EAAE,MAAA,IAAI,CAAC,GAAG,mCAAI,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IACtE,cAAc,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IAE5B,MAAM,KAAK,GAAG,MAAM,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAChE,GAAG,CAAC,GAAG,CACL,cAAc,MAAM,CAAC,YAAY,CAAC,MAAM,cAAc;QACpD,GAAG,MAAM,CAAC,YAAY,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,KAAK,IAAI,CAC/D,CAAC;IAEF,MAAM,QAAQ,GAAG,MAAM,kBAAkB,CAAC,MAAM,CAAC,CAAC;IAElD,MAAM,UAAU,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,MAAM,CAAC;IACzE,MAAM,IAAI,iCACR,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,IAAI,EACzB,YAAY,EAAE,MAAM,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YAC5C,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,KAAK,EAAE,CAAC,CAAC,KAAK;SACf,CAAC,CAAC,EACH,QAAQ,IACL,CAAC,IAAI,CAAC,MAAM,KAAK,eAAe;QACjC,CAAC,CAAC;YACE,UAAU,EAAE;gBACV,MAAM,EAAE,IAAI,CAAC,MAAM;gBACnB,gBAAgB,EAAE,IAAI,CAAC,gBAAgB;gBACvC,oBAAoB,EAAE,IAAI,CAAC,oBAAoB;aAChD;SACF;QACH,CAAC,CAAC,EAAE,CAAC,GACJ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CACzC,CAAC;IAEF,IAAI,UAAU,GAAG,CAAC,EAAE,CAAC;QACnB,OAAO,GAAG,CAAC,IAAI,CACb,eAAe,CACb,GAAG,UAAU,sBAAsB,UAAU,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG,EACjE,oEAAoE;YAClE,oBAAoB,CACvB,EACD,IAAI,CACL,CAAC;IACJ,CAAC;IAED,OAAO,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,kCAAkC,KAAK,GAAG,CAAC,CAAC;AACvE,CAAC","sourcesContent":["import type { CliContext } from '../context';\nimport { validationError } from '../errors';\nimport type { ExitCode } from '../exit-codes';\nimport { loadProject } from '../project';\nimport { assertSchemaNotSkewed } from '../schema-skew';\nimport { announceUpdate, checkForUpdate } from '../update-notice';\nimport { collectAllFindings } from '../validate-findings';\n\nexport interface ValidateArgs {\n /** Project directory (contains ekanos.json). Defaults to cwd. */\n dir: string;\n env?: Record<string, string | undefined>;\n}\n\n/**\n * `validate` — parse the declaration with the REAL zod schemas from\n * `@ekanos/integration-schema` (never a reimplementation), gather structured\n * findings, and add project-level integrity checks (the vitest inline-SDK\n * check). Exit 0 with `findings: []` when clean; exit 3 when any error-severity\n * finding is present, with the findings carried in the envelope's `data`.\n *\n * The findings pass itself lives in `validate-findings.ts`, shared with\n * `publish` so the publish gate cannot drift from what validate checks.\n *\n * Runs the schema-skew gate FIRST, before anything else — see\n * `schema-skew.ts` for why a newer project schema than the CLI bundles makes\n * every finding below it unreliable.\n */\nexport async function runValidate(\n ctx: CliContext,\n args: ValidateArgs,\n): Promise<ExitCode> {\n const loaded = loadProject(args.dir);\n\n const skew = assertSchemaNotSkewed(ctx, loaded.projectDir);\n\n const notice = await checkForUpdate({ env: args.env ?? process.env });\n announceUpdate(ctx, notice);\n\n const slugs = loaded.integrations.map((i) => i.slug).join(', ');\n ctx.log(\n `Validating ${loaded.integrations.length} integration` +\n `${loaded.integrations.length === 1 ? '' : 's'} (${slugs})…`,\n );\n\n const findings = await collectAllFindings(loaded);\n\n const errorCount = findings.filter((f) => f.severity === 'error').length;\n const data = {\n slug: loaded.primary.slug,\n integrations: loaded.integrations.map((i) => ({\n slug: i.slug,\n entry: i.entry,\n })),\n findings,\n ...(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\n if (errorCount > 0) {\n return ctx.fail(\n validationError(\n `${errorCount} validation finding${errorCount === 1 ? '' : 's'}.`,\n 'Resolve the error-severity findings in data.findings, then re-run ' +\n '\"ekanos validate\".',\n ),\n data,\n );\n }\n\n return ctx.succeed(data, `validate: OK — no findings for ${slugs}.`);\n}\n"]}
|
|
@@ -16,5 +16,11 @@ export interface WhoamiArgs {
|
|
|
16
16
|
*
|
|
17
17
|
* Exit 4 (auth_required) when there is no session or it cannot be renewed —
|
|
18
18
|
* the code a script should branch on to decide whether to run `ekanos login`.
|
|
19
|
+
*
|
|
20
|
+
* `seats` is a best-effort add-on from the same source-visibility endpoint
|
|
21
|
+
* `ekanos sources` uses, compacted to slug+roles. Its failure — network, MFA,
|
|
22
|
+
* a deployment that predates the route — must never turn a working `whoami`
|
|
23
|
+
* into a failing one, so any error here degrades to `seats: null` rather than
|
|
24
|
+
* propagating.
|
|
19
25
|
*/
|
|
20
26
|
export declare function runWhoami(ctx: CliContext, args: WhoamiArgs): Promise<ExitCode>;
|
package/dist/commands/whoami.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { requireSession, resolveAuthEnvironment, resolveIdentity, } from '../auth/session.js';
|
|
2
|
+
import { fetchSeatsWithOneRefresh } from '../seats.js';
|
|
2
3
|
/**
|
|
3
4
|
* `whoami` — prove the stored session still authenticates against Fusion.
|
|
4
5
|
*
|
|
@@ -9,6 +10,12 @@ import { requireSession, resolveAuthEnvironment, resolveIdentity, } from '../aut
|
|
|
9
10
|
*
|
|
10
11
|
* Exit 4 (auth_required) when there is no session or it cannot be renewed —
|
|
11
12
|
* the code a script should branch on to decide whether to run `ekanos login`.
|
|
13
|
+
*
|
|
14
|
+
* `seats` is a best-effort add-on from the same source-visibility endpoint
|
|
15
|
+
* `ekanos sources` uses, compacted to slug+roles. Its failure — network, MFA,
|
|
16
|
+
* a deployment that predates the route — must never turn a working `whoami`
|
|
17
|
+
* into a failing one, so any error here degrades to `seats: null` rather than
|
|
18
|
+
* propagating.
|
|
12
19
|
*/
|
|
13
20
|
export async function runWhoami(ctx, args) {
|
|
14
21
|
var _a;
|
|
@@ -17,6 +24,7 @@ export async function runWhoami(ctx, args) {
|
|
|
17
24
|
});
|
|
18
25
|
const stored = requireSession(env.store, env.host);
|
|
19
26
|
const { identity, session } = await resolveIdentity(env, stored);
|
|
27
|
+
const seats = await fetchSeatsQuietly(env, session);
|
|
20
28
|
return ctx.succeed({
|
|
21
29
|
host: env.host,
|
|
22
30
|
user: {
|
|
@@ -26,6 +34,23 @@ export async function runWhoami(ctx, args) {
|
|
|
26
34
|
},
|
|
27
35
|
expiresAt: session.expiresAt,
|
|
28
36
|
credentialsPath: env.store.filePath,
|
|
29
|
-
|
|
37
|
+
seats,
|
|
38
|
+
}, `whoami: ${(_a = identity.email) !== null && _a !== void 0 ? _a : identity.id} on ${env.host}.` +
|
|
39
|
+
(seats
|
|
40
|
+
? seats.length > 0
|
|
41
|
+
? ` Seats: ${seats.map((s) => `${s.slug} [${s.roles.join(', ')}]`).join(', ')}.`
|
|
42
|
+
: ' No source seats.'
|
|
43
|
+
: ' (source seat lookup unavailable)'));
|
|
44
|
+
}
|
|
45
|
+
async function fetchSeatsQuietly(env, session) {
|
|
46
|
+
try {
|
|
47
|
+
const result = await fetchSeatsWithOneRefresh(env, session);
|
|
48
|
+
if (result.status !== 'ok')
|
|
49
|
+
return null;
|
|
50
|
+
return result.sources.map((s) => ({ slug: s.slug, roles: s.roles }));
|
|
51
|
+
}
|
|
52
|
+
catch (_a) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
30
55
|
}
|
|
31
56
|
//# sourceMappingURL=whoami.js.map
|
|
@@ -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;
|
|
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;AAGzB,OAAO,EAAE,wBAAwB,EAAE,MAAM,UAAU,CAAC;AASpD;;;;;;;;;;;;;;;;GAgBG;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,MAAM,KAAK,GAAG,MAAM,iBAAiB,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IAEpD,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;QACnC,KAAK;KACN,EACD,WAAW,MAAA,QAAQ,CAAC,KAAK,mCAAI,QAAQ,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,GAAG;QACxD,CAAC,KAAK;YACJ,CAAC,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC;gBAChB,CAAC,CAAC,WAAW,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG;gBAChF,CAAC,CAAC,mBAAmB;YACvB,CAAC,CAAC,mCAAmC,CAAC,CAC3C,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,iBAAiB,CAC9B,GAAmD,EACnD,OAAuD;IAEvD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,wBAAwB,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QAE5D,IAAI,MAAM,CAAC,MAAM,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC;QAExC,OAAO,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;IACvE,CAAC;IAAC,WAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC","sourcesContent":["import {\n requireSession,\n resolveAuthEnvironment,\n resolveIdentity,\n} from '../auth/session';\nimport type { CliContext } from '../context';\nimport type { ExitCode } from '../exit-codes';\nimport { fetchSeatsWithOneRefresh } from '../seats';\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 *\n * `seats` is a best-effort add-on from the same source-visibility endpoint\n * `ekanos sources` uses, compacted to slug+roles. Its failure — network, MFA,\n * a deployment that predates the route — must never turn a working `whoami`\n * into a failing one, so any error here degrades to `seats: null` rather than\n * propagating.\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 const seats = await fetchSeatsQuietly(env, session);\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 seats,\n },\n `whoami: ${identity.email ?? identity.id} on ${env.host}.` +\n (seats\n ? seats.length > 0\n ? ` Seats: ${seats.map((s) => `${s.slug} [${s.roles.join(', ')}]`).join(', ')}.`\n : ' No source seats.'\n : ' (source seat lookup unavailable)'),\n );\n}\n\nasync function fetchSeatsQuietly(\n env: Parameters<typeof fetchSeatsWithOneRefresh>[0],\n session: Parameters<typeof fetchSeatsWithOneRefresh>[1],\n): Promise<{ slug: string; roles: string[] }[] | null> {\n try {\n const result = await fetchSeatsWithOneRefresh(env, session);\n\n if (result.status !== 'ok') return null;\n\n return result.sources.map((s) => ({ slug: s.slug, roles: s.roles }));\n } catch {\n return null;\n }\n}\n"]}
|
package/dist/context.d.ts
CHANGED
|
@@ -20,6 +20,15 @@ export interface ContextInput {
|
|
|
20
20
|
/** Overrides process.env for CLAUDECODE detection (tests). */
|
|
21
21
|
env?: Record<string, string | undefined>;
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* `data` is deliberately `unknown` above — each verb owns its own shape — but
|
|
25
|
+
* one convention spans several of them: `validate`, `dev`, `publish` and
|
|
26
|
+
* `status` all add an OPTIONAL `toolchain` field to `data` (never to the
|
|
27
|
+
* envelope's top level) when the proactive update check in `update-notice.ts`
|
|
28
|
+
* finds this CLI is behind. See that file's `ToolchainNoticeData` for the
|
|
29
|
+
* shape, and `README.md`'s "Keeping the toolchain current" section for the
|
|
30
|
+
* full contract. It is absent, not `null`, when there is nothing to report.
|
|
31
|
+
*/
|
|
23
32
|
/**
|
|
24
33
|
* The agent-native output envelope, resolved ONCE at startup and threaded
|
|
25
34
|
* through every command.
|
package/dist/context.js
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import { EXIT_CODES } from './exit-codes.js';
|
|
2
|
+
/**
|
|
3
|
+
* `data` is deliberately `unknown` above — each verb owns its own shape — but
|
|
4
|
+
* one convention spans several of them: `validate`, `dev`, `publish` and
|
|
5
|
+
* `status` all add an OPTIONAL `toolchain` field to `data` (never to the
|
|
6
|
+
* envelope's top level) when the proactive update check in `update-notice.ts`
|
|
7
|
+
* finds this CLI is behind. See that file's `ToolchainNoticeData` for the
|
|
8
|
+
* shape, and `README.md`'s "Keeping the toolchain current" section for the
|
|
9
|
+
* full contract. It is absent, not `null`, when there is nothing to report.
|
|
10
|
+
*/
|
|
2
11
|
/**
|
|
3
12
|
* The agent-native output envelope, resolved ONCE at startup and threaded
|
|
4
13
|
* through every command.
|
package/dist/context.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"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","sourcesContent":["import { CliError } from './errors';\nimport { EXIT_CODES, type ExitCode } from './exit-codes';\n\n/**\n * A single writable sink. Real streams satisfy this; tests pass a capturing\n * buffer so the stdout-purity invariant can be asserted byte-for-byte.\n */\nexport interface WriteStream {\n write(chunk: string): void;\n isTTY?: boolean;\n}\n\nexport interface ContextInput {\n /**\n * The user's EXPLICIT preference, already parsed: `true` for `--json`,\n * `false` for `--no-json`, `undefined` when neither was passed. The\n * undefined case is what allows the mode to be inferred instead.\n */\n jsonFlag?: boolean;\n stdout?: WriteStream;\n stderr?: WriteStream;\n /** Overrides process.env for CLAUDECODE detection (tests). */\n env?: Record<string, string | undefined>;\n}\n\n/** The success envelope. */\ninterface OkEnvelope {\n ok: true;\n data: unknown;\n}\n\n/** The error envelope. `data` is optional — validate carries its findings here. */\ninterface ErrEnvelope {\n ok: false;\n data?: unknown;\n error: { code: string; message: string; hint: string };\n}\n\n/**\n * The agent-native output envelope, resolved ONCE at startup and threaded\n * through every command.\n *\n * The load-bearing invariant: in JSON mode, stdout receives EXACTLY ONE JSON\n * object for the whole process — nothing else, ever. Every progress line, warn,\n * and scrap of human prose goes to stderr. This is the single most common way\n * an agent's tool call breaks (a stray log line on stdout makes the JSON\n * unparseable), so the context enforces it structurally: `succeed`/`fail` are\n * the only writers of stdout, and a `settled` latch makes a second call throw\n * rather than emit a second object.\n */\nexport class CliContext {\n readonly jsonMode: boolean;\n /**\n * The environment the context resolved against — exposed so decoration\n * (the gradient banner) can honour `NO_COLOR` / `EKANOS_NO_BANNER` without\n * every call site threading `process.env` through by hand.\n */\n readonly env: Readonly<Record<string, string | undefined>>;\n private readonly stdout: WriteStream;\n private readonly stderr: WriteStream;\n private settled = false;\n\n constructor(input: ContextInput = {}) {\n const stdout = input.stdout ?? defaultStream(process.stdout);\n const stderr = input.stderr ?? defaultStream(process.stderr);\n const env = input.env ?? process.env;\n\n this.stdout = stdout;\n this.stderr = stderr;\n this.env = env;\n\n // Resolve JSON mode once. An EXPLICIT flag always wins: `--json` forces the\n // envelope on even at a terminal, `--no-json` forces the human form even\n // when piped. Only when the user expressed no preference is the mode\n // inferred — from a non-TTY stdout (piped into another program or an agent)\n // or the CLAUDECODE marker.\n const nonTty = stdout.isTTY !== true;\n this.jsonMode = input.jsonFlag ?? (nonTty || env.CLAUDECODE === '1');\n }\n\n /** Whether stderr is a real terminal — decoration (the banner) gates on this. */\n get stderrIsTTY(): boolean {\n return this.stderr.isTTY === true;\n }\n\n /** Human/progress output — ALWAYS stderr, so stdout stays pure in JSON mode. */\n log(message: string): void {\n this.stderr.write(`${message}\\n`);\n }\n\n /** A warning — stderr, no ANSI. */\n warn(message: string): void {\n this.stderr.write(`${message}\\n`);\n }\n\n /**\n * Emit the `claude-code-hint` protocol line to STDERR. Used on --help and on\n * an unknown command so an agent reading stderr is told what to do next\n * without the line ever touching stdout.\n */\n emitClaudeCodeHint(payload: Record<string, unknown>): void {\n this.stderr.write(`claude-code-hint: ${JSON.stringify(payload)}\\n`);\n }\n\n /**\n * Terminal success. Writes the single stdout object (JSON mode) or a concise\n * human line (human mode). Returns the OK exit code so the caller can end the\n * process with it. Idempotent-by-latch: a second settle throws.\n */\n succeed(data: unknown, humanSummary?: string): ExitCode {\n this.settle();\n if (this.jsonMode) {\n const envelope: OkEnvelope = { ok: true, data };\n this.stdout.write(`${JSON.stringify(envelope)}\\n`);\n } else {\n this.stdout.write(`${humanSummary ?? 'OK'}\\n`);\n }\n return EXIT_CODES.OK;\n }\n\n /**\n * Terminal failure. Writes the single stdout error object (JSON mode) or the\n * message + hint to stderr (human mode). Returns the error's exit code.\n *\n * `error.hint` is guaranteed non-empty by CliError's constructor, so the\n * envelope's \"every error carries a hint\" invariant holds for every path\n * that reaches here.\n */\n fail(error: CliError, data?: unknown): ExitCode {\n this.failWith({\n code: error.code,\n message: error.message,\n hint: error.hint,\n exitCode: error.exitCode,\n data,\n });\n return error.exitCode;\n }\n\n /**\n * The low-level failure emitter. Used directly by `test`, whose exit code is\n * a TRANSPARENT passthrough of the delegated runner's exit code (which is not\n * one of the CLI's own frozen numeric conditions) and whose error `code` is\n * an out-of-taxonomy string. Enforces the same non-empty-hint invariant as\n * CliError so no failure path can emit an empty hint.\n */\n failWith(params: {\n code: string;\n message: string;\n hint: string;\n exitCode: number;\n data?: unknown;\n }): number {\n this.settle();\n if (!params.hint || params.hint.trim().length === 0) {\n throw new Error(\n `failWith for \"${params.message}\" was given an empty hint. Every CLI ` +\n 'error must carry an imperative remediation.',\n );\n }\n if (this.jsonMode) {\n const envelope: ErrEnvelope = {\n ok: false,\n ...(params.data !== undefined ? { data: params.data } : {}),\n error: {\n code: params.code,\n message: params.message,\n hint: params.hint,\n },\n };\n this.stdout.write(`${JSON.stringify(envelope)}\\n`);\n } else {\n this.stderr.write(`Error: ${params.message}\\n`);\n this.stderr.write(`Hint: ${params.hint}\\n`);\n }\n return params.exitCode;\n }\n\n private settle(): void {\n if (this.settled) {\n throw new Error(\n 'CliContext settled twice — a command tried to emit a second ' +\n 'terminal result. In JSON mode this would corrupt stdout with two ' +\n 'objects. This is a bug in the command.',\n );\n }\n this.settled = true;\n }\n}\n\nfunction defaultStream(stream: NodeJS.WriteStream): WriteStream {\n return {\n write: (chunk: string) => {\n stream.write(chunk);\n },\n isTTY: stream.isTTY,\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"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","sourcesContent":["import { CliError } from './errors';\nimport { EXIT_CODES, type ExitCode } from './exit-codes';\n\n/**\n * A single writable sink. Real streams satisfy this; tests pass a capturing\n * buffer so the stdout-purity invariant can be asserted byte-for-byte.\n */\nexport interface WriteStream {\n write(chunk: string): void;\n isTTY?: boolean;\n}\n\nexport interface ContextInput {\n /**\n * The user's EXPLICIT preference, already parsed: `true` for `--json`,\n * `false` for `--no-json`, `undefined` when neither was passed. The\n * undefined case is what allows the mode to be inferred instead.\n */\n jsonFlag?: boolean;\n stdout?: WriteStream;\n stderr?: WriteStream;\n /** Overrides process.env for CLAUDECODE detection (tests). */\n env?: Record<string, string | undefined>;\n}\n\n/** The success envelope. */\ninterface OkEnvelope {\n ok: true;\n data: unknown;\n}\n\n/** The error envelope. `data` is optional — validate carries its findings here. */\ninterface ErrEnvelope {\n ok: false;\n data?: unknown;\n error: { code: string; message: string; hint: string };\n}\n\n/**\n * `data` is deliberately `unknown` above — each verb owns its own shape — but\n * one convention spans several of them: `validate`, `dev`, `publish` and\n * `status` all add an OPTIONAL `toolchain` field to `data` (never to the\n * envelope's top level) when the proactive update check in `update-notice.ts`\n * finds this CLI is behind. See that file's `ToolchainNoticeData` for the\n * shape, and `README.md`'s \"Keeping the toolchain current\" section for the\n * full contract. It is absent, not `null`, when there is nothing to report.\n */\n\n/**\n * The agent-native output envelope, resolved ONCE at startup and threaded\n * through every command.\n *\n * The load-bearing invariant: in JSON mode, stdout receives EXACTLY ONE JSON\n * object for the whole process — nothing else, ever. Every progress line, warn,\n * and scrap of human prose goes to stderr. This is the single most common way\n * an agent's tool call breaks (a stray log line on stdout makes the JSON\n * unparseable), so the context enforces it structurally: `succeed`/`fail` are\n * the only writers of stdout, and a `settled` latch makes a second call throw\n * rather than emit a second object.\n */\nexport class CliContext {\n readonly jsonMode: boolean;\n /**\n * The environment the context resolved against — exposed so decoration\n * (the gradient banner) can honour `NO_COLOR` / `EKANOS_NO_BANNER` without\n * every call site threading `process.env` through by hand.\n */\n readonly env: Readonly<Record<string, string | undefined>>;\n private readonly stdout: WriteStream;\n private readonly stderr: WriteStream;\n private settled = false;\n\n constructor(input: ContextInput = {}) {\n const stdout = input.stdout ?? defaultStream(process.stdout);\n const stderr = input.stderr ?? defaultStream(process.stderr);\n const env = input.env ?? process.env;\n\n this.stdout = stdout;\n this.stderr = stderr;\n this.env = env;\n\n // Resolve JSON mode once. An EXPLICIT flag always wins: `--json` forces the\n // envelope on even at a terminal, `--no-json` forces the human form even\n // when piped. Only when the user expressed no preference is the mode\n // inferred — from a non-TTY stdout (piped into another program or an agent)\n // or the CLAUDECODE marker.\n const nonTty = stdout.isTTY !== true;\n this.jsonMode = input.jsonFlag ?? (nonTty || env.CLAUDECODE === '1');\n }\n\n /** Whether stderr is a real terminal — decoration (the banner) gates on this. */\n get stderrIsTTY(): boolean {\n return this.stderr.isTTY === true;\n }\n\n /** Human/progress output — ALWAYS stderr, so stdout stays pure in JSON mode. */\n log(message: string): void {\n this.stderr.write(`${message}\\n`);\n }\n\n /** A warning — stderr, no ANSI. */\n warn(message: string): void {\n this.stderr.write(`${message}\\n`);\n }\n\n /**\n * Emit the `claude-code-hint` protocol line to STDERR. Used on --help and on\n * an unknown command so an agent reading stderr is told what to do next\n * without the line ever touching stdout.\n */\n emitClaudeCodeHint(payload: Record<string, unknown>): void {\n this.stderr.write(`claude-code-hint: ${JSON.stringify(payload)}\\n`);\n }\n\n /**\n * Terminal success. Writes the single stdout object (JSON mode) or a concise\n * human line (human mode). Returns the OK exit code so the caller can end the\n * process with it. Idempotent-by-latch: a second settle throws.\n */\n succeed(data: unknown, humanSummary?: string): ExitCode {\n this.settle();\n if (this.jsonMode) {\n const envelope: OkEnvelope = { ok: true, data };\n this.stdout.write(`${JSON.stringify(envelope)}\\n`);\n } else {\n this.stdout.write(`${humanSummary ?? 'OK'}\\n`);\n }\n return EXIT_CODES.OK;\n }\n\n /**\n * Terminal failure. Writes the single stdout error object (JSON mode) or the\n * message + hint to stderr (human mode). Returns the error's exit code.\n *\n * `error.hint` is guaranteed non-empty by CliError's constructor, so the\n * envelope's \"every error carries a hint\" invariant holds for every path\n * that reaches here.\n */\n fail(error: CliError, data?: unknown): ExitCode {\n this.failWith({\n code: error.code,\n message: error.message,\n hint: error.hint,\n exitCode: error.exitCode,\n data,\n });\n return error.exitCode;\n }\n\n /**\n * The low-level failure emitter. Used directly by `test`, whose exit code is\n * a TRANSPARENT passthrough of the delegated runner's exit code (which is not\n * one of the CLI's own frozen numeric conditions) and whose error `code` is\n * an out-of-taxonomy string. Enforces the same non-empty-hint invariant as\n * CliError so no failure path can emit an empty hint.\n */\n failWith(params: {\n code: string;\n message: string;\n hint: string;\n exitCode: number;\n data?: unknown;\n }): number {\n this.settle();\n if (!params.hint || params.hint.trim().length === 0) {\n throw new Error(\n `failWith for \"${params.message}\" was given an empty hint. Every CLI ` +\n 'error must carry an imperative remediation.',\n );\n }\n if (this.jsonMode) {\n const envelope: ErrEnvelope = {\n ok: false,\n ...(params.data !== undefined ? { data: params.data } : {}),\n error: {\n code: params.code,\n message: params.message,\n hint: params.hint,\n },\n };\n this.stdout.write(`${JSON.stringify(envelope)}\\n`);\n } else {\n this.stderr.write(`Error: ${params.message}\\n`);\n this.stderr.write(`Hint: ${params.hint}\\n`);\n }\n return params.exitCode;\n }\n\n private settle(): void {\n if (this.settled) {\n throw new Error(\n 'CliContext settled twice — a command tried to emit a second ' +\n 'terminal result. In JSON mode this would corrupt stdout with two ' +\n 'objects. This is a bug in the command.',\n );\n }\n this.settled = true;\n }\n}\n\nfunction defaultStream(stream: NodeJS.WriteStream): WriteStream {\n return {\n write: (chunk: string) => {\n stream.write(chunk);\n },\n isTTY: stream.isTTY,\n };\n}\n"]}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
export interface DelegationPlan {
|
|
3
|
+
shouldDelegate: boolean;
|
|
4
|
+
ownVersion: string;
|
|
5
|
+
localVersion?: string;
|
|
6
|
+
/** Absolute path to the local CLI's bin entry, when `shouldDelegate`. */
|
|
7
|
+
localBinPath?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Decide whether this invocation should delegate. Pure and side-effect-free
|
|
11
|
+
* (beyond the one file read of the local `@ekanos/cli` manifest, mirroring
|
|
12
|
+
* `readInstalledHarness`/`dev`'s own project-package resolution rather than
|
|
13
|
+
* writing a second one) so it is directly testable without spawning anything.
|
|
14
|
+
*/
|
|
15
|
+
export declare function planDelegation(argv: readonly string[], env: Record<string, string | undefined>, cwd: string): DelegationPlan;
|
|
16
|
+
/**
|
|
17
|
+
* The one-line stderr announcement — human mode only, and NEVER on stdout
|
|
18
|
+
* (a delegated `validate --json` must still emit exactly one JSON object on
|
|
19
|
+
* stdout, from the CHILD, with nothing of ours ahead of it).
|
|
20
|
+
*/
|
|
21
|
+
export declare function announceDelegation(plan: DelegationPlan, argv: readonly string[], env: Record<string, string | undefined>, stdoutIsTTY: boolean): void;
|
|
22
|
+
/**
|
|
23
|
+
* Spawn the local CLI's own bin entry with the SAME node executable,
|
|
24
|
+
* `stdio: 'inherit'` so the JSON envelope and banner gating are
|
|
25
|
+
* byte-identical to a direct run, and propagate its exit code exactly.
|
|
26
|
+
* `spawnFn` is injectable so tests never actually fork a process.
|
|
27
|
+
*/
|
|
28
|
+
export declare function delegate(plan: DelegationPlan, argv: readonly string[], env: NodeJS.ProcessEnv, spawnFn?: typeof spawn): Promise<number>;
|
package/dist/delegate.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { cliVersion } from './compatibility.js';
|
|
4
|
+
import { readInstalledPackageManifest } from './harness-scaffold.js';
|
|
5
|
+
/**
|
|
6
|
+
* Wrapper-style delegation to the project-local `@ekanos/cli` — the
|
|
7
|
+
* Gradle-wrapper / Yarn / `npx` pattern, applied to this CLI.
|
|
8
|
+
*
|
|
9
|
+
* A global install bundles ONE `@ekanos/integration-schema` version but
|
|
10
|
+
* routinely serves N projects on different `@ekanos/sdk` versions — it
|
|
11
|
+
* cannot be correct for all of them simultaneously (this is exactly the
|
|
12
|
+
* failure mode `schema-skew.ts` detects). Delegation dissolves the tension:
|
|
13
|
+
* `ekanos` stays on PATH for convenience, but the four verbs that actually
|
|
14
|
+
* read or generate project files always run through the project's OWN
|
|
15
|
+
* pinned copy when one is installed and differs from the invoked binary.
|
|
16
|
+
*
|
|
17
|
+
* Machine-level verbs (`init`, `login`, `logout`, `whoami`, `sources`,
|
|
18
|
+
* `use`, `status`, `upgrade`) are deliberately NEVER delegated: you never
|
|
19
|
+
* want an old local copy handling credentials, and `upgrade` must always be
|
|
20
|
+
* the newest logic doing the upgrading — delegating it could have an old CLI
|
|
21
|
+
* "upgrade" a project using its own out-of-date resolution.
|
|
22
|
+
*/
|
|
23
|
+
const DELEGATABLE_VERBS = new Set(['validate', 'publish', 'dev', 'test']);
|
|
24
|
+
/** The first bare (non-flag) token before a standalone `--` — mirrors `splitCommand` in `index.ts`. */
|
|
25
|
+
function firstCommand(argv) {
|
|
26
|
+
for (const token of argv) {
|
|
27
|
+
if (token === '--')
|
|
28
|
+
break;
|
|
29
|
+
if (!token.startsWith('-'))
|
|
30
|
+
return token;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
/** `--dir <path>` / `--dir=<path>`, before a standalone `--`. */
|
|
35
|
+
function scanDirFlag(argv) {
|
|
36
|
+
for (let i = 0; i < argv.length; i++) {
|
|
37
|
+
const token = argv[i];
|
|
38
|
+
if (token === '--')
|
|
39
|
+
break;
|
|
40
|
+
if (token === '--dir')
|
|
41
|
+
return argv[i + 1];
|
|
42
|
+
if (token.startsWith('--dir='))
|
|
43
|
+
return token.slice('--dir='.length);
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Decide whether this invocation should delegate. Pure and side-effect-free
|
|
49
|
+
* (beyond the one file read of the local `@ekanos/cli` manifest, mirroring
|
|
50
|
+
* `readInstalledHarness`/`dev`'s own project-package resolution rather than
|
|
51
|
+
* writing a second one) so it is directly testable without spawning anything.
|
|
52
|
+
*/
|
|
53
|
+
export function planDelegation(argv, env, cwd) {
|
|
54
|
+
var _a;
|
|
55
|
+
const ownVersion = cliVersion();
|
|
56
|
+
// The recursion guard, and the explicit escape hatch, both win outright.
|
|
57
|
+
if (env.EKANOS_DELEGATED === '1' || env.EKANOS_NO_DELEGATE === '1') {
|
|
58
|
+
return { shouldDelegate: false, ownVersion };
|
|
59
|
+
}
|
|
60
|
+
const command = firstCommand(argv);
|
|
61
|
+
if (!command || !DELEGATABLE_VERBS.has(command)) {
|
|
62
|
+
return { shouldDelegate: false, ownVersion };
|
|
63
|
+
}
|
|
64
|
+
const projectDir = path.resolve(cwd, (_a = scanDirFlag(argv)) !== null && _a !== void 0 ? _a : '.');
|
|
65
|
+
// Best-effort, like every other resolution this function does: a corrupt
|
|
66
|
+
// local manifest must fall through to "don't delegate", never throw. This
|
|
67
|
+
// runs in `bin.ts` BEFORE `run()`'s try/catch exists, so an uncaught throw
|
|
68
|
+
// here would bypass the JSON-envelope error machinery entirely and print a
|
|
69
|
+
// raw stderr string instead — a real regression in the "exactly one
|
|
70
|
+
// terminal emission" contract the rest of this package guarantees.
|
|
71
|
+
let local = null;
|
|
72
|
+
try {
|
|
73
|
+
local = readInstalledPackageManifest(projectDir, '@ekanos/cli');
|
|
74
|
+
}
|
|
75
|
+
catch (_b) {
|
|
76
|
+
local = null;
|
|
77
|
+
}
|
|
78
|
+
if (!local || local.version === ownVersion) {
|
|
79
|
+
return { shouldDelegate: false, ownVersion, localVersion: local === null || local === void 0 ? void 0 : local.version };
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
shouldDelegate: true,
|
|
83
|
+
ownVersion,
|
|
84
|
+
localVersion: local.version,
|
|
85
|
+
localBinPath: path.join(projectDir, 'node_modules', '@ekanos', 'cli', 'dist', 'bin.js'),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Same JSON-mode inference `CliContext` uses (`context.ts`) — duplicated
|
|
90
|
+
* deliberately rather than shared, because this runs BEFORE a `CliContext`
|
|
91
|
+
* exists (delegation is decided ahead of the whole `run()` machinery). Keep
|
|
92
|
+
* this in sync with `CliContext`'s constructor if that heuristic ever
|
|
93
|
+
* changes: an explicit `--json`/`--no-json` wins, otherwise a non-TTY stdout
|
|
94
|
+
* or `CLAUDECODE=1` means JSON mode.
|
|
95
|
+
*/
|
|
96
|
+
function looksLikeJsonMode(argv, env, stdoutIsTTY) {
|
|
97
|
+
for (const token of argv) {
|
|
98
|
+
if (token === '--')
|
|
99
|
+
break;
|
|
100
|
+
if (token === '--json')
|
|
101
|
+
return true;
|
|
102
|
+
if (token === '--no-json')
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
return !stdoutIsTTY || env.CLAUDECODE === '1';
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The one-line stderr announcement — human mode only, and NEVER on stdout
|
|
109
|
+
* (a delegated `validate --json` must still emit exactly one JSON object on
|
|
110
|
+
* stdout, from the CHILD, with nothing of ours ahead of it).
|
|
111
|
+
*/
|
|
112
|
+
export function announceDelegation(plan, argv, env, stdoutIsTTY) {
|
|
113
|
+
if (!plan.shouldDelegate)
|
|
114
|
+
return;
|
|
115
|
+
if (looksLikeJsonMode(argv, env, stdoutIsTTY))
|
|
116
|
+
return;
|
|
117
|
+
process.stderr.write(`ekanos: using this project's pinned CLI ${plan.localVersion} (global: ` +
|
|
118
|
+
`${plan.ownVersion}). Set EKANOS_NO_DELEGATE=1 to bypass.\n`);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Spawn the local CLI's own bin entry with the SAME node executable,
|
|
122
|
+
* `stdio: 'inherit'` so the JSON envelope and banner gating are
|
|
123
|
+
* byte-identical to a direct run, and propagate its exit code exactly.
|
|
124
|
+
* `spawnFn` is injectable so tests never actually fork a process.
|
|
125
|
+
*/
|
|
126
|
+
export function delegate(plan, argv, env, spawnFn = spawn) {
|
|
127
|
+
return new Promise((resolve) => {
|
|
128
|
+
const child = spawnFn(process.execPath, [plan.localBinPath, ...argv], {
|
|
129
|
+
stdio: 'inherit',
|
|
130
|
+
env: Object.assign(Object.assign({}, env), { EKANOS_DELEGATED: '1' }),
|
|
131
|
+
});
|
|
132
|
+
child.on('error', () => resolve(1));
|
|
133
|
+
child.on('exit', (code) => resolve(code !== null && code !== void 0 ? code : 1));
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=delegate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"delegate.js","sourceRoot":"","sources":["../src/delegate.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from 'node:child_process';\nimport * as path from 'node:path';\n\nimport { cliVersion } from './compatibility';\nimport { readInstalledPackageManifest } from './harness-scaffold';\n\n/**\n * Wrapper-style delegation to the project-local `@ekanos/cli` — the\n * Gradle-wrapper / Yarn / `npx` pattern, applied to this CLI.\n *\n * A global install bundles ONE `@ekanos/integration-schema` version but\n * routinely serves N projects on different `@ekanos/sdk` versions — it\n * cannot be correct for all of them simultaneously (this is exactly the\n * failure mode `schema-skew.ts` detects). Delegation dissolves the tension:\n * `ekanos` stays on PATH for convenience, but the four verbs that actually\n * read or generate project files always run through the project's OWN\n * pinned copy when one is installed and differs from the invoked binary.\n *\n * Machine-level verbs (`init`, `login`, `logout`, `whoami`, `sources`,\n * `use`, `status`, `upgrade`) are deliberately NEVER delegated: you never\n * want an old local copy handling credentials, and `upgrade` must always be\n * the newest logic doing the upgrading — delegating it could have an old CLI\n * \"upgrade\" a project using its own out-of-date resolution.\n */\n\nconst DELEGATABLE_VERBS = new Set(['validate', 'publish', 'dev', 'test']);\n\nexport interface DelegationPlan {\n shouldDelegate: boolean;\n ownVersion: string;\n localVersion?: string;\n /** Absolute path to the local CLI's bin entry, when `shouldDelegate`. */\n localBinPath?: string;\n}\n\n/** The first bare (non-flag) token before a standalone `--` — mirrors `splitCommand` in `index.ts`. */\nfunction firstCommand(argv: readonly string[]): string | null {\n for (const token of argv) {\n if (token === '--') break;\n if (!token.startsWith('-')) return token;\n }\n return null;\n}\n\n/** `--dir <path>` / `--dir=<path>`, before a standalone `--`. */\nfunction scanDirFlag(argv: readonly string[]): string | undefined {\n for (let i = 0; i < argv.length; i++) {\n const token = argv[i]!;\n if (token === '--') break;\n if (token === '--dir') return argv[i + 1];\n if (token.startsWith('--dir=')) return token.slice('--dir='.length);\n }\n return undefined;\n}\n\n/**\n * Decide whether this invocation should delegate. Pure and side-effect-free\n * (beyond the one file read of the local `@ekanos/cli` manifest, mirroring\n * `readInstalledHarness`/`dev`'s own project-package resolution rather than\n * writing a second one) so it is directly testable without spawning anything.\n */\nexport function planDelegation(\n argv: readonly string[],\n env: Record<string, string | undefined>,\n cwd: string,\n): DelegationPlan {\n const ownVersion = cliVersion();\n\n // The recursion guard, and the explicit escape hatch, both win outright.\n if (env.EKANOS_DELEGATED === '1' || env.EKANOS_NO_DELEGATE === '1') {\n return { shouldDelegate: false, ownVersion };\n }\n\n const command = firstCommand(argv);\n if (!command || !DELEGATABLE_VERBS.has(command)) {\n return { shouldDelegate: false, ownVersion };\n }\n\n const projectDir = path.resolve(cwd, scanDirFlag(argv) ?? '.');\n // Best-effort, like every other resolution this function does: a corrupt\n // local manifest must fall through to \"don't delegate\", never throw. This\n // runs in `bin.ts` BEFORE `run()`'s try/catch exists, so an uncaught throw\n // here would bypass the JSON-envelope error machinery entirely and print a\n // raw stderr string instead — a real regression in the \"exactly one\n // terminal emission\" contract the rest of this package guarantees.\n let local: ReturnType<typeof readInstalledPackageManifest> = null;\n try {\n local = readInstalledPackageManifest(projectDir, '@ekanos/cli');\n } catch {\n local = null;\n }\n\n if (!local || local.version === ownVersion) {\n return { shouldDelegate: false, ownVersion, localVersion: local?.version };\n }\n\n return {\n shouldDelegate: true,\n ownVersion,\n localVersion: local.version,\n localBinPath: path.join(\n projectDir,\n 'node_modules',\n '@ekanos',\n 'cli',\n 'dist',\n 'bin.js',\n ),\n };\n}\n\n/**\n * Same JSON-mode inference `CliContext` uses (`context.ts`) — duplicated\n * deliberately rather than shared, because this runs BEFORE a `CliContext`\n * exists (delegation is decided ahead of the whole `run()` machinery). Keep\n * this in sync with `CliContext`'s constructor if that heuristic ever\n * changes: an explicit `--json`/`--no-json` wins, otherwise a non-TTY stdout\n * or `CLAUDECODE=1` means JSON mode.\n */\nfunction looksLikeJsonMode(\n argv: readonly string[],\n env: Record<string, string | undefined>,\n stdoutIsTTY: boolean,\n): boolean {\n for (const token of argv) {\n if (token === '--') break;\n if (token === '--json') return true;\n if (token === '--no-json') return false;\n }\n return !stdoutIsTTY || env.CLAUDECODE === '1';\n}\n\n/**\n * The one-line stderr announcement — human mode only, and NEVER on stdout\n * (a delegated `validate --json` must still emit exactly one JSON object on\n * stdout, from the CHILD, with nothing of ours ahead of it).\n */\nexport function announceDelegation(\n plan: DelegationPlan,\n argv: readonly string[],\n env: Record<string, string | undefined>,\n stdoutIsTTY: boolean,\n): void {\n if (!plan.shouldDelegate) return;\n if (looksLikeJsonMode(argv, env, stdoutIsTTY)) return;\n\n process.stderr.write(\n `ekanos: using this project's pinned CLI ${plan.localVersion} (global: ` +\n `${plan.ownVersion}). Set EKANOS_NO_DELEGATE=1 to bypass.\\n`,\n );\n}\n\n/**\n * Spawn the local CLI's own bin entry with the SAME node executable,\n * `stdio: 'inherit'` so the JSON envelope and banner gating are\n * byte-identical to a direct run, and propagate its exit code exactly.\n * `spawnFn` is injectable so tests never actually fork a process.\n */\nexport function delegate(\n plan: DelegationPlan,\n argv: readonly string[],\n env: NodeJS.ProcessEnv,\n spawnFn: typeof spawn = spawn,\n): Promise<number> {\n return new Promise((resolve) => {\n const child = spawnFn(process.execPath, [plan.localBinPath!, ...argv], {\n stdio: 'inherit',\n env: { ...env, EKANOS_DELEGATED: '1' },\n });\n child.on('error', () => resolve(1));\n child.on('exit', (code) => resolve(code ?? 1));\n });\n}\n"]}
|
|
@@ -44,16 +44,31 @@ export interface WrittenFiles {
|
|
|
44
44
|
unchanged: string[];
|
|
45
45
|
}
|
|
46
46
|
/**
|
|
47
|
-
* Read
|
|
48
|
-
* `node_modules`. Resolved by path rather than `require.resolve`
|
|
49
|
-
*
|
|
50
|
-
*
|
|
47
|
+
* Read an installed package's `package.json` out of a project's TOP-LEVEL
|
|
48
|
+
* `node_modules`. Resolved by direct path join rather than `require.resolve`
|
|
49
|
+
* on purpose, for two independent reasons:
|
|
50
|
+
*
|
|
51
|
+
* - The CLI runs from its own install and must read the version of the copy
|
|
52
|
+
* in THE CALLER'S project, not one hoisted next to itself.
|
|
53
|
+
* - Several of the packages this reads (`@ekanos/integration-schema`
|
|
54
|
+
* included) declare an `exports` map with no `./package.json` subpath, so
|
|
55
|
+
* `require.resolve('<pkg>/package.json')` throws
|
|
56
|
+
* `ERR_PACKAGE_PATH_NOT_EXPORTED` under Node's ESM resolver. A plain file
|
|
57
|
+
* read is not subject to the export map at all.
|
|
58
|
+
*
|
|
59
|
+
* Shared by every "what does this project have installed?" check in the
|
|
60
|
+
* CLI — `readInstalledHarness` below, the schema-skew gate, and `upgrade`'s
|
|
61
|
+
* current-toolchain report — so there is exactly one implementation of "read
|
|
62
|
+
* a package's on-disk version", not a subtly different one per call site.
|
|
51
63
|
*/
|
|
52
|
-
export interface
|
|
64
|
+
export interface InstalledPackage {
|
|
53
65
|
version: string;
|
|
54
|
-
/** The raw package.json,
|
|
66
|
+
/** The raw package.json, for callers that need more than the version. */
|
|
55
67
|
manifest: unknown;
|
|
56
68
|
}
|
|
69
|
+
export declare function readInstalledPackageManifest(projectDir: string, packageName: string): InstalledPackage | null;
|
|
70
|
+
/** Read the installed `@ekanos/harness` version out of the partner's project. */
|
|
71
|
+
export type InstalledHarness = InstalledPackage;
|
|
57
72
|
export declare function readInstalledHarness(projectDir: string): InstalledHarness | null;
|
|
58
73
|
/** Just the installed version, for callers that need nothing else. */
|
|
59
74
|
export declare function readInstalledHarnessVersion(projectDir: string): string | null;
|
package/dist/harness-scaffold.js
CHANGED
|
@@ -88,12 +88,12 @@ export const REQUIRED_DEPENDENCIES = [
|
|
|
88
88
|
'tailwindcss',
|
|
89
89
|
'@tailwindcss/postcss',
|
|
90
90
|
];
|
|
91
|
-
export function
|
|
92
|
-
const
|
|
93
|
-
if (!fs.existsSync(
|
|
91
|
+
export function readInstalledPackageManifest(projectDir, packageName) {
|
|
92
|
+
const manifestPath = path.join(projectDir, 'node_modules', ...packageName.split('/'), 'package.json');
|
|
93
|
+
if (!fs.existsSync(manifestPath))
|
|
94
94
|
return null;
|
|
95
95
|
try {
|
|
96
|
-
const parsed = JSON.parse(fs.readFileSync(
|
|
96
|
+
const parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
97
97
|
const version = typeof parsed === 'object' && parsed !== null
|
|
98
98
|
? parsed.version
|
|
99
99
|
: undefined;
|
|
@@ -102,9 +102,12 @@ export function readInstalledHarness(projectDir) {
|
|
|
102
102
|
return { version, manifest: parsed };
|
|
103
103
|
}
|
|
104
104
|
catch (error) {
|
|
105
|
-
throw preconditionError(`${
|
|
105
|
+
throw preconditionError(`${packageName}'s package.json in node_modules is unreadable: ${error instanceof Error ? error.message : String(error)}`, `Reinstall dependencies in ${projectDir} so ${packageName} resolves.`);
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
|
+
export function readInstalledHarness(projectDir) {
|
|
109
|
+
return readInstalledPackageManifest(projectDir, HARNESS_PACKAGE);
|
|
110
|
+
}
|
|
108
111
|
/** Just the installed version, for callers that need nothing else. */
|
|
109
112
|
export function readInstalledHarnessVersion(projectDir) {
|
|
110
113
|
var _a, _b;
|