@ekanos/cli 0.3.4 → 0.3.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.
@@ -4,6 +4,7 @@ import { assertHarnessCompatible } from '../compatibility.js';
4
4
  import { preconditionError, usageError } from '../errors.js';
5
5
  import { HARNESS_DIR, HARNESS_PACKAGE, assertShellDirIsOurs, bootstrapHarnessConfig, ensureGitignoreEntry, missingDependencies, readInstalledHarness, scaffoldShell, } from '../harness-scaffold.js';
6
6
  import { detectPackageManager, execArgs, formatCommand, installCommand, } from '../package-manager.js';
7
+ import { createOutputTail, findIgnoredBuilds, ignoredBuildsHint, } from '../pnpm-builds.js';
7
8
  import { loadProject } from '../project.js';
8
9
  import { displayPath } from '../ui.js';
9
10
  import { announceUpdate, checkForUpdate } from '../update-notice.js';
@@ -144,8 +145,10 @@ export async function runDev(ctx, args) {
144
145
  }));
145
146
  }
146
147
  ctx.log(ctx.ui.dim(`Starting the harness on ${url} (${command})…`));
148
+ const tail = createOutputTail();
147
149
  const exitCode = await spawnServer({
148
150
  ctx,
151
+ tail,
149
152
  bin: pm.bin,
150
153
  argv: execArgs(pm, 'next', serverArgs),
151
154
  cwd: shell.harnessDir,
@@ -155,12 +158,15 @@ export async function runDev(ctx, args) {
155
158
  if (exitCode === 0) {
156
159
  return ctx.succeed(data, ctx.ui.result({ title: 'Harness stopped', rows: [['URL', url]] }));
157
160
  }
161
+ const ignoredBuilds = findIgnoredBuilds(tail.text());
158
162
  return ctx.failWith({
159
163
  code: 'dev_server_failed',
160
164
  message: `"${command}" exited with code ${exitCode}.`,
161
- hint: 'Read the Next output on stderr. If it cannot resolve @ekanos/harness ' +
162
- `or next, reinstall dependencies in ${loaded.projectDir}; if a route ` +
163
- `fails to compile, delete ${shell.harnessDir} and re-run "ekanos dev".`,
165
+ hint: ignoredBuilds.length > 0
166
+ ? ignoredBuildsHint(ignoredBuilds)
167
+ : 'Read the Next output on stderr. If it cannot resolve @ekanos/harness ' +
168
+ `or next, reinstall dependencies in ${loaded.projectDir}; if a route ` +
169
+ `fails to compile, delete ${shell.harnessDir} and re-run "ekanos dev".`,
164
170
  // Transparent passthrough of Next's own exit code.
165
171
  exitCode,
166
172
  data,
@@ -177,9 +183,11 @@ function spawnServer(params) {
177
183
  stdio: ['inherit', 'pipe', 'pipe'],
178
184
  });
179
185
  (_a = child.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (chunk) => {
186
+ params.tail.push(chunk);
180
187
  (params.ctx.jsonMode ? process.stderr : process.stdout).write(chunk);
181
188
  });
182
189
  (_b = child.stderr) === null || _b === void 0 ? void 0 : _b.on('data', (chunk) => {
190
+ params.tail.push(chunk);
183
191
  process.stderr.write(chunk);
184
192
  });
185
193
  child.on('error', (error) => reject(error));
@@ -1 +1 @@
1
- {"version":3,"file":"dev.js","sourceRoot":"","sources":["../../src/commands/dev.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from 'node:child_process';\nimport * as path from 'node:path';\n\nimport { assertHarnessCompatible } from '../compatibility';\nimport type { CliContext } from '../context';\nimport { preconditionError, usageError } from '../errors';\nimport {\n HARNESS_DIR,\n HARNESS_PACKAGE,\n assertShellDirIsOurs,\n bootstrapHarnessConfig,\n ensureGitignoreEntry,\n missingDependencies,\n readInstalledHarness,\n scaffoldShell,\n} from '../harness-scaffold';\nimport {\n detectPackageManager,\n execArgs,\n formatCommand,\n installCommand,\n} from '../package-manager';\nimport { loadProject } from '../project';\nimport { displayPath } from '../ui';\nimport { announceUpdate, checkForUpdate } from '../update-notice';\n\nexport interface DevArgs {\n /** Project directory (contains ekanos.json). Defaults to cwd. */\n dir: string;\n /** Port for the dev server. */\n port?: string;\n /**\n * Interface to bind. Defaults to loopback — see `resolveHost`. Anything\n * else is an explicit, warned-about decision to expose the harness.\n *\n * NOTE: unrelated to the Ekanos HOST the update notice checks against —\n * this is the local bind address for `next dev`, never an Ekanos origin.\n */\n host?: string;\n /** Rewrite every generated file even when its bytes already match. */\n force: boolean;\n /**\n * Explicit `--start` / `--no-start`. Undefined means \"decide from the\n * output mode\": a JSON caller is an agent or a pipe and must not be handed\n * a process that never exits.\n */\n start?: boolean;\n env?: Record<string, string | undefined>;\n}\n\nconst DEFAULT_PORT = 3100;\n\n/**\n * Loopback, NOT Next's default.\n *\n * `next dev` binds 0.0.0.0 unless told otherwise, so the harness would be\n * reachable by anyone on the same network — a coffee-shop Wi-Fi, a shared\n * office VLAN — while we printed \"http://localhost:3100\" and implied\n * otherwise. It renders a partner's integration with their fixtures and, in\n * live mode, whatever their vendor API returns; there is no auth in front of\n * any of it. Binding loopback by default costs a partner nothing and makes\n * exposure a thing they ask for.\n */\nconst DEFAULT_HOST = '127.0.0.1';\n\n/** Addresses that are only reachable from this machine. */\nconst LOOPBACK_HOSTS = new Set(['127.0.0.1', '::1', 'localhost']);\n\n/**\n * Set for the dev server when `--host` binds beyond loopback. The shell's\n * API routes (`@ekanos/harness/api`) refuse non-loopback `Host` headers\n * unless this is `1`: they hold the partner's local OAuth credentials and\n * run handlers on request, so exposing them has to be a decision — this one,\n * made after the warning below — rather than a side effect of the bind.\n */\nconst EXPOSED_ENV = 'EKANOS_HARNESS_EXPOSED';\n\n/** Where the harness generates, and reads, local dev credentials. */\nconst LOCAL_SECRETS_PATH = path.join('.ekanos', 'local-secrets.json');\n\n/**\n * `dev` — scaffold the local harness shell into the partner's project, then\n * run it with their own Next.\n *\n * The shape of this verb follows from one rule: we must be able to ship a\n * harness bugfix as an `@ekanos/harness` version bump that a partner picks up\n * with `pnpm up`, with zero edits to any file they own. So everything under\n * `.ekanos/` is generated, gitignored and rewritten freely, `harness.config.ts`\n * at the project root is written once and never touched again, and all actual\n * harness behaviour lives in the installed package.\n *\n * Two idempotency properties matter and are both tested: a second run produces\n * byte-identical files and rewrites nothing (so Next's watcher stays quiet),\n * and `.gitignore` never grows a duplicate line.\n */\nexport async function runDev(ctx: CliContext, args: DevArgs): Promise<number> {\n const loaded = loadProject(args.dir);\n const port = parsePort(args.port);\n const host = parseHost(args.host);\n const exposed = !LOOPBACK_HOSTS.has(host);\n const pm = detectPackageManager(loaded.projectDir);\n\n const notice = await checkForUpdate({ env: args.env ?? process.env });\n announceUpdate(ctx, notice);\n\n // Fail before writing anything: the shell declares no dependencies of its\n // own, so a missing one would otherwise surface as an opaque Next\n // resolution error minutes later.\n const missing = missingDependencies(loaded.projectDir);\n if (missing.length > 0) {\n throw preconditionError(\n `${missing.length} package${missing.length === 1 ? '' : 's'} the ` +\n `harness needs ${missing.length === 1 ? 'is' : 'are'} not installed ` +\n `in ${loaded.projectDir}: ${missing.join(', ')}.`,\n `Run \"${installCommand(pm, missing)}\" in ${loaded.projectDir}, then ` +\n 're-run \"ekanos dev\".',\n );\n }\n\n const harness = readInstalledHarness(loaded.projectDir);\n const harnessVersion = harness?.version;\n if (!harnessVersion) {\n throw preconditionError(\n `${HARNESS_PACKAGE} is installed but declares no version.`,\n `Reinstall it with \"${installCommand(pm, [HARNESS_PACKAGE])}\" in ` +\n `${loaded.projectDir}, then re-run \"ekanos dev\".`,\n );\n }\n\n // The templates ship with the CLI and the code they import ships with the\n // harness, so a partner can upgrade one without the other. Name that\n // mismatch here rather than letting it surface later as a module-resolution\n // error inside a shell we cheerfully certified as current.\n assertHarnessCompatible({\n harnessVersion,\n harnessManifest: harness?.manifest,\n installHint: (spec) => installCommand(pm, [spec]),\n });\n\n // Validate the shell target BEFORE writing anything at all. Bootstrapping\n // harness.config.ts first and only then discovering that .ekanos/harness is\n // a file or someone else's tree would leave the project half-scaffolded.\n assertShellDirIsOurs(path.join(loaded.projectDir, HARNESS_DIR), args.force);\n\n const config = bootstrapHarnessConfig(loaded.projectDir, loaded.integrations);\n const shell = scaffoldShell({\n projectDir: loaded.projectDir,\n slug: loaded.primary.slug,\n harnessVersion,\n force: args.force,\n sourceGlobs: loaded.project.sourceGlobs,\n });\n const gitignoreUpdated = ensureGitignoreEntry(loaded.projectDir);\n\n if (shell.stale) {\n ctx.log(\n `Regenerating ${HARNESS_DIR} — ${shell.staleReasons.join('; ')}. ` +\n `Nothing you own changed; ${config.configPath} is untouched.`,\n );\n }\n\n if (config.bootstrapped) {\n ctx.log(` created harness.config.ts (yours — edit it, we never will)`);\n }\n for (const file of shell.created) ctx.log(` created ${HARNESS_DIR}/${file}`);\n for (const file of shell.updated) ctx.log(` updated ${HARNESS_DIR}/${file}`);\n if (gitignoreUpdated) ctx.log(' updated .gitignore (.ekanos/)');\n ctx.log(\n ` live-mode OAuth: the harness generates ${LOCAL_SECRETS_PATH} on first ` +\n 'use — fill it in and follow \"Local OAuth\" on the Activation surface.',\n );\n\n // `--hostname` is passed ALWAYS, never left to Next's default, so the URL\n // we print is the URL that is actually served.\n const serverArgs = ['dev', '--port', String(port), '--hostname', host];\n const serverEnv: Record<string, string> = exposed\n ? { [EXPOSED_ENV]: '1' }\n : {};\n const command =\n (exposed ? `${EXPOSED_ENV}=1 ` : '') +\n formatCommand(pm.bin, execArgs(pm, 'next', serverArgs));\n const url = `http://${displayHost(host)}:${port}`;\n\n const scaffold = {\n slug: loaded.primary.slug,\n integrations: loaded.integrations.map((i) => i.slug),\n projectDir: loaded.projectDir,\n harnessDir: shell.harnessDir,\n harnessVersion,\n previousVersion: shell.previousVersion,\n previousMarker: shell.previousMarker,\n stale: shell.stale,\n staleReasons: shell.staleReasons,\n sourceGlobs: loaded.project.sourceGlobs ?? [],\n created: shell.created,\n updated: shell.updated,\n unchanged: shell.unchanged,\n configBootstrapped: config.bootstrapped,\n configPath: config.configPath,\n gitignoreUpdated,\n ...(notice ? { toolchain: notice } : {}),\n };\n\n if (exposed) {\n ctx.warn(\n `WARNING: --host ${host} exposes the harness beyond this machine. It ` +\n 'has no authentication, and it renders your fixtures and — in live ' +\n 'mode — real responses from your vendor API. Its API routes hold the ' +\n `client secret from ${LOCAL_SECRETS_PATH}, the tokens onTokens stored, ` +\n 'and run your handlers on request; they refuse non-loopback callers ' +\n `unless ${EXPOSED_ENV}=1, which this sets. Anyone who can reach ` +\n `${url} can read and run all of it. Omit --host to bind ${DEFAULT_HOST}.`,\n );\n }\n\n // A JSON caller is an agent or a pipe. Handing it a dev server that never\n // exits would hang its tool call forever and it would never see the\n // envelope, so JSON mode scaffolds and reports the command instead of\n // running it. `--start` overrides; `--no-start` opts out in a terminal.\n const shouldStart = args.start ?? !ctx.jsonMode;\n if (!shouldStart) {\n return ctx.succeed(\n {\n ...scaffold,\n server: { started: false, command, url, port, host, exposed },\n },\n ctx.ui.result({\n title: 'Harness ready',\n rows: [\n ['Directory', displayPath(shell.harnessDir, process.cwd())],\n ['URL', url],\n ['Start', command],\n ],\n }),\n );\n }\n\n ctx.log(ctx.ui.dim(`Starting the harness on ${url} (${command})…`));\n const exitCode = await spawnServer({\n ctx,\n bin: pm.bin,\n argv: execArgs(pm, 'next', serverArgs),\n cwd: shell.harnessDir,\n env: serverEnv,\n });\n\n const data = {\n ...scaffold,\n server: { started: true, command, url, port, host, exposed, exitCode },\n };\n\n if (exitCode === 0) {\n return ctx.succeed(\n data,\n ctx.ui.result({ title: 'Harness stopped', rows: [['URL', url]] }),\n );\n }\n\n return ctx.failWith({\n code: 'dev_server_failed',\n message: `\"${command}\" exited with code ${exitCode}.`,\n hint:\n 'Read the Next output on stderr. If it cannot resolve @ekanos/harness ' +\n `or next, reinstall dependencies in ${loaded.projectDir}; if a route ` +\n `fails to compile, delete ${shell.harnessDir} and re-run \"ekanos dev\".`,\n // Transparent passthrough of Next's own exit code.\n exitCode,\n data,\n });\n}\n\nfunction spawnServer(params: {\n ctx: CliContext;\n bin: string;\n argv: string[];\n cwd: string;\n env: Record<string, string>;\n}): Promise<number> {\n return new Promise<number>((resolve, reject) => {\n const child = spawn(params.bin, params.argv, {\n cwd: params.cwd,\n env: { ...process.env, ...params.env },\n // stdin inherited so Next's own key handling works; stdout/stderr piped\n // so we can route them and keep OUR stdout reserved for the envelope.\n stdio: ['inherit', 'pipe', 'pipe'],\n });\n\n child.stdout?.on('data', (chunk: Buffer) => {\n (params.ctx.jsonMode ? process.stderr : process.stdout).write(chunk);\n });\n child.stderr?.on('data', (chunk: Buffer) => {\n process.stderr.write(chunk);\n });\n\n child.on('error', (error) => reject(error));\n child.on('close', (code) => resolve(code ?? 1));\n });\n}\n\n/**\n * The hostname to show a human. `127.0.0.1` is spelled `localhost` because\n * that is what people type; a wildcard bind is spelled `localhost` too, but\n * only after `exposed` has already been warned about — printing `0.0.0.0` as\n * a URL is not useful.\n */\nfunction displayHost(host: string): string {\n if (host === '127.0.0.1' || host === '0.0.0.0' || host === '::') {\n return 'localhost';\n }\n if (host === '::1') return '[::1]';\n return host;\n}\n\nfunction parseHost(raw: string | undefined): string {\n if (raw === undefined) return DEFAULT_HOST;\n\n const host = raw.trim();\n if (host.length === 0) {\n throw usageError(\n 'Invalid --host \"\".',\n `Pass an interface to bind, e.g. \"--host 0.0.0.0\" to expose the ` +\n `harness on your network, or omit it to bind ${DEFAULT_HOST}.`,\n );\n }\n return host;\n}\n\nfunction parsePort(raw: string | undefined): number {\n if (raw === undefined) return DEFAULT_PORT;\n\n const port = Number(raw);\n if (!Number.isInteger(port) || port < 1 || port > 65535) {\n throw usageError(\n `Invalid --port \"${raw}\".`,\n 'Pass an integer between 1 and 65535, e.g. \"--port 3100\".',\n );\n }\n return port;\n}\n"]}
1
+ {"version":3,"file":"dev.js","sourceRoot":"","sources":["../../src/commands/dev.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from 'node:child_process';\nimport * as path from 'node:path';\n\nimport { assertHarnessCompatible } from '../compatibility';\nimport type { CliContext } from '../context';\nimport { preconditionError, usageError } from '../errors';\nimport {\n HARNESS_DIR,\n HARNESS_PACKAGE,\n assertShellDirIsOurs,\n bootstrapHarnessConfig,\n ensureGitignoreEntry,\n missingDependencies,\n readInstalledHarness,\n scaffoldShell,\n} from '../harness-scaffold';\nimport {\n detectPackageManager,\n execArgs,\n formatCommand,\n installCommand,\n} from '../package-manager';\nimport {\n createOutputTail,\n findIgnoredBuilds,\n ignoredBuildsHint,\n} from '../pnpm-builds';\nimport { loadProject } from '../project';\nimport { displayPath } from '../ui';\nimport { announceUpdate, checkForUpdate } from '../update-notice';\n\nexport interface DevArgs {\n /** Project directory (contains ekanos.json). Defaults to cwd. */\n dir: string;\n /** Port for the dev server. */\n port?: string;\n /**\n * Interface to bind. Defaults to loopback — see `resolveHost`. Anything\n * else is an explicit, warned-about decision to expose the harness.\n *\n * NOTE: unrelated to the Ekanos HOST the update notice checks against —\n * this is the local bind address for `next dev`, never an Ekanos origin.\n */\n host?: string;\n /** Rewrite every generated file even when its bytes already match. */\n force: boolean;\n /**\n * Explicit `--start` / `--no-start`. Undefined means \"decide from the\n * output mode\": a JSON caller is an agent or a pipe and must not be handed\n * a process that never exits.\n */\n start?: boolean;\n env?: Record<string, string | undefined>;\n}\n\nconst DEFAULT_PORT = 3100;\n\n/**\n * Loopback, NOT Next's default.\n *\n * `next dev` binds 0.0.0.0 unless told otherwise, so the harness would be\n * reachable by anyone on the same network — a coffee-shop Wi-Fi, a shared\n * office VLAN — while we printed \"http://localhost:3100\" and implied\n * otherwise. It renders a partner's integration with their fixtures and, in\n * live mode, whatever their vendor API returns; there is no auth in front of\n * any of it. Binding loopback by default costs a partner nothing and makes\n * exposure a thing they ask for.\n */\nconst DEFAULT_HOST = '127.0.0.1';\n\n/** Addresses that are only reachable from this machine. */\nconst LOOPBACK_HOSTS = new Set(['127.0.0.1', '::1', 'localhost']);\n\n/**\n * Set for the dev server when `--host` binds beyond loopback. The shell's\n * API routes (`@ekanos/harness/api`) refuse non-loopback `Host` headers\n * unless this is `1`: they hold the partner's local OAuth credentials and\n * run handlers on request, so exposing them has to be a decision — this one,\n * made after the warning below — rather than a side effect of the bind.\n */\nconst EXPOSED_ENV = 'EKANOS_HARNESS_EXPOSED';\n\n/** Where the harness generates, and reads, local dev credentials. */\nconst LOCAL_SECRETS_PATH = path.join('.ekanos', 'local-secrets.json');\n\n/**\n * `dev` — scaffold the local harness shell into the partner's project, then\n * run it with their own Next.\n *\n * The shape of this verb follows from one rule: we must be able to ship a\n * harness bugfix as an `@ekanos/harness` version bump that a partner picks up\n * with `pnpm up`, with zero edits to any file they own. So everything under\n * `.ekanos/` is generated, gitignored and rewritten freely, `harness.config.ts`\n * at the project root is written once and never touched again, and all actual\n * harness behaviour lives in the installed package.\n *\n * Two idempotency properties matter and are both tested: a second run produces\n * byte-identical files and rewrites nothing (so Next's watcher stays quiet),\n * and `.gitignore` never grows a duplicate line.\n */\nexport async function runDev(ctx: CliContext, args: DevArgs): Promise<number> {\n const loaded = loadProject(args.dir);\n const port = parsePort(args.port);\n const host = parseHost(args.host);\n const exposed = !LOOPBACK_HOSTS.has(host);\n const pm = detectPackageManager(loaded.projectDir);\n\n const notice = await checkForUpdate({ env: args.env ?? process.env });\n announceUpdate(ctx, notice);\n\n // Fail before writing anything: the shell declares no dependencies of its\n // own, so a missing one would otherwise surface as an opaque Next\n // resolution error minutes later.\n const missing = missingDependencies(loaded.projectDir);\n if (missing.length > 0) {\n throw preconditionError(\n `${missing.length} package${missing.length === 1 ? '' : 's'} the ` +\n `harness needs ${missing.length === 1 ? 'is' : 'are'} not installed ` +\n `in ${loaded.projectDir}: ${missing.join(', ')}.`,\n `Run \"${installCommand(pm, missing)}\" in ${loaded.projectDir}, then ` +\n 're-run \"ekanos dev\".',\n );\n }\n\n const harness = readInstalledHarness(loaded.projectDir);\n const harnessVersion = harness?.version;\n if (!harnessVersion) {\n throw preconditionError(\n `${HARNESS_PACKAGE} is installed but declares no version.`,\n `Reinstall it with \"${installCommand(pm, [HARNESS_PACKAGE])}\" in ` +\n `${loaded.projectDir}, then re-run \"ekanos dev\".`,\n );\n }\n\n // The templates ship with the CLI and the code they import ships with the\n // harness, so a partner can upgrade one without the other. Name that\n // mismatch here rather than letting it surface later as a module-resolution\n // error inside a shell we cheerfully certified as current.\n assertHarnessCompatible({\n harnessVersion,\n harnessManifest: harness?.manifest,\n installHint: (spec) => installCommand(pm, [spec]),\n });\n\n // Validate the shell target BEFORE writing anything at all. Bootstrapping\n // harness.config.ts first and only then discovering that .ekanos/harness is\n // a file or someone else's tree would leave the project half-scaffolded.\n assertShellDirIsOurs(path.join(loaded.projectDir, HARNESS_DIR), args.force);\n\n const config = bootstrapHarnessConfig(loaded.projectDir, loaded.integrations);\n const shell = scaffoldShell({\n projectDir: loaded.projectDir,\n slug: loaded.primary.slug,\n harnessVersion,\n force: args.force,\n sourceGlobs: loaded.project.sourceGlobs,\n });\n const gitignoreUpdated = ensureGitignoreEntry(loaded.projectDir);\n\n if (shell.stale) {\n ctx.log(\n `Regenerating ${HARNESS_DIR} — ${shell.staleReasons.join('; ')}. ` +\n `Nothing you own changed; ${config.configPath} is untouched.`,\n );\n }\n\n if (config.bootstrapped) {\n ctx.log(` created harness.config.ts (yours — edit it, we never will)`);\n }\n for (const file of shell.created) ctx.log(` created ${HARNESS_DIR}/${file}`);\n for (const file of shell.updated) ctx.log(` updated ${HARNESS_DIR}/${file}`);\n if (gitignoreUpdated) ctx.log(' updated .gitignore (.ekanos/)');\n ctx.log(\n ` live-mode OAuth: the harness generates ${LOCAL_SECRETS_PATH} on first ` +\n 'use — fill it in and follow \"Local OAuth\" on the Activation surface.',\n );\n\n // `--hostname` is passed ALWAYS, never left to Next's default, so the URL\n // we print is the URL that is actually served.\n const serverArgs = ['dev', '--port', String(port), '--hostname', host];\n const serverEnv: Record<string, string> = exposed\n ? { [EXPOSED_ENV]: '1' }\n : {};\n const command =\n (exposed ? `${EXPOSED_ENV}=1 ` : '') +\n formatCommand(pm.bin, execArgs(pm, 'next', serverArgs));\n const url = `http://${displayHost(host)}:${port}`;\n\n const scaffold = {\n slug: loaded.primary.slug,\n integrations: loaded.integrations.map((i) => i.slug),\n projectDir: loaded.projectDir,\n harnessDir: shell.harnessDir,\n harnessVersion,\n previousVersion: shell.previousVersion,\n previousMarker: shell.previousMarker,\n stale: shell.stale,\n staleReasons: shell.staleReasons,\n sourceGlobs: loaded.project.sourceGlobs ?? [],\n created: shell.created,\n updated: shell.updated,\n unchanged: shell.unchanged,\n configBootstrapped: config.bootstrapped,\n configPath: config.configPath,\n gitignoreUpdated,\n ...(notice ? { toolchain: notice } : {}),\n };\n\n if (exposed) {\n ctx.warn(\n `WARNING: --host ${host} exposes the harness beyond this machine. It ` +\n 'has no authentication, and it renders your fixtures and — in live ' +\n 'mode — real responses from your vendor API. Its API routes hold the ' +\n `client secret from ${LOCAL_SECRETS_PATH}, the tokens onTokens stored, ` +\n 'and run your handlers on request; they refuse non-loopback callers ' +\n `unless ${EXPOSED_ENV}=1, which this sets. Anyone who can reach ` +\n `${url} can read and run all of it. Omit --host to bind ${DEFAULT_HOST}.`,\n );\n }\n\n // A JSON caller is an agent or a pipe. Handing it a dev server that never\n // exits would hang its tool call forever and it would never see the\n // envelope, so JSON mode scaffolds and reports the command instead of\n // running it. `--start` overrides; `--no-start` opts out in a terminal.\n const shouldStart = args.start ?? !ctx.jsonMode;\n if (!shouldStart) {\n return ctx.succeed(\n {\n ...scaffold,\n server: { started: false, command, url, port, host, exposed },\n },\n ctx.ui.result({\n title: 'Harness ready',\n rows: [\n ['Directory', displayPath(shell.harnessDir, process.cwd())],\n ['URL', url],\n ['Start', command],\n ],\n }),\n );\n }\n\n ctx.log(ctx.ui.dim(`Starting the harness on ${url} (${command})…`));\n const tail = createOutputTail();\n const exitCode = await spawnServer({\n ctx,\n tail,\n bin: pm.bin,\n argv: execArgs(pm, 'next', serverArgs),\n cwd: shell.harnessDir,\n env: serverEnv,\n });\n\n const data = {\n ...scaffold,\n server: { started: true, command, url, port, host, exposed, exitCode },\n };\n\n if (exitCode === 0) {\n return ctx.succeed(\n data,\n ctx.ui.result({ title: 'Harness stopped', rows: [['URL', url]] }),\n );\n }\n\n const ignoredBuilds = findIgnoredBuilds(tail.text());\n\n return ctx.failWith({\n code: 'dev_server_failed',\n message: `\"${command}\" exited with code ${exitCode}.`,\n hint:\n ignoredBuilds.length > 0\n ? ignoredBuildsHint(ignoredBuilds)\n : 'Read the Next output on stderr. If it cannot resolve @ekanos/harness ' +\n `or next, reinstall dependencies in ${loaded.projectDir}; if a route ` +\n `fails to compile, delete ${shell.harnessDir} and re-run \"ekanos dev\".`,\n // Transparent passthrough of Next's own exit code.\n exitCode,\n data,\n });\n}\n\nfunction spawnServer(params: {\n ctx: CliContext;\n tail: ReturnType<typeof createOutputTail>;\n bin: string;\n argv: string[];\n cwd: string;\n env: Record<string, string>;\n}): Promise<number> {\n return new Promise<number>((resolve, reject) => {\n const child = spawn(params.bin, params.argv, {\n cwd: params.cwd,\n env: { ...process.env, ...params.env },\n // stdin inherited so Next's own key handling works; stdout/stderr piped\n // so we can route them and keep OUR stdout reserved for the envelope.\n stdio: ['inherit', 'pipe', 'pipe'],\n });\n\n child.stdout?.on('data', (chunk: Buffer) => {\n params.tail.push(chunk);\n (params.ctx.jsonMode ? process.stderr : process.stdout).write(chunk);\n });\n child.stderr?.on('data', (chunk: Buffer) => {\n params.tail.push(chunk);\n process.stderr.write(chunk);\n });\n\n child.on('error', (error) => reject(error));\n child.on('close', (code) => resolve(code ?? 1));\n });\n}\n\n/**\n * The hostname to show a human. `127.0.0.1` is spelled `localhost` because\n * that is what people type; a wildcard bind is spelled `localhost` too, but\n * only after `exposed` has already been warned about — printing `0.0.0.0` as\n * a URL is not useful.\n */\nfunction displayHost(host: string): string {\n if (host === '127.0.0.1' || host === '0.0.0.0' || host === '::') {\n return 'localhost';\n }\n if (host === '::1') return '[::1]';\n return host;\n}\n\nfunction parseHost(raw: string | undefined): string {\n if (raw === undefined) return DEFAULT_HOST;\n\n const host = raw.trim();\n if (host.length === 0) {\n throw usageError(\n 'Invalid --host \"\".',\n `Pass an interface to bind, e.g. \"--host 0.0.0.0\" to expose the ` +\n `harness on your network, or omit it to bind ${DEFAULT_HOST}.`,\n );\n }\n return host;\n}\n\nfunction parsePort(raw: string | undefined): number {\n if (raw === undefined) return DEFAULT_PORT;\n\n const port = Number(raw);\n if (!Number.isInteger(port) || port < 1 || port > 65535) {\n throw usageError(\n `Invalid --port \"${raw}\".`,\n 'Pass an integer between 1 and 65535, e.g. \"--port 3100\".',\n );\n }\n return port;\n}\n"]}
@@ -2,6 +2,7 @@ import { spawn } from 'node:child_process';
2
2
  import * as fs from 'node:fs';
3
3
  import { renderFindings } from '../findings-renderer.js';
4
4
  import { detectPackageManager } from '../package-manager.js';
5
+ import { createOutputTail, findIgnoredBuilds, ignoredBuildsHint, } from '../pnpm-builds.js';
5
6
  import { loadProject } from '../project.js';
6
7
  import { newReportPath, parseTestReport, projectTestScriptUsesVitest, reporterArgs, } from '../test-report.js';
7
8
  import { displayPath, formatDuration, pluralize } from '../ui.js';
@@ -47,6 +48,7 @@ export async function runTest(ctx, args) {
47
48
  const command = `${pm.bin} ${baseRunnerArgs.join(' ')}`;
48
49
  const where = displayPath(loaded.projectDir, process.cwd());
49
50
  ctx.log(ctx.ui.dim(`Running "${command}"${where === '.' ? '' : ` in ${where}`}…`));
51
+ const tail = createOutputTail();
50
52
  const started = Date.now();
51
53
  const exitCode = await new Promise((resolve, reject) => {
52
54
  var _a, _b;
@@ -59,9 +61,11 @@ export async function runTest(ctx, args) {
59
61
  // In JSON mode every byte of runner output goes to stderr; in human mode we
60
62
  // keep the child's stream separation.
61
63
  (_a = child.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (chunk) => {
64
+ tail.push(chunk);
62
65
  (ctx.jsonMode ? process.stderr : process.stdout).write(chunk);
63
66
  });
64
67
  (_b = child.stderr) === null || _b === void 0 ? void 0 : _b.on('data', (chunk) => {
68
+ tail.push(chunk);
65
69
  process.stderr.write(chunk);
66
70
  });
67
71
  child.on('error', (error) => reject(error));
@@ -127,11 +131,14 @@ export async function runTest(ctx, args) {
127
131
  ctx.log(ctx.ui.nextAction('ekanos test', 'fix the above, then re-run'));
128
132
  }
129
133
  }
134
+ const ignoredBuilds = findIgnoredBuilds(tail.text());
130
135
  return ctx.failWith({
131
136
  code: 'test_failed',
132
137
  message: `"${command}" exited with code ${exitCode}.`,
133
- hint: 'Read the runner output on stderr and fix the failing tests, then ' +
134
- 're-run "ekanos test".',
138
+ hint: ignoredBuilds.length > 0
139
+ ? ignoredBuildsHint(ignoredBuilds)
140
+ : 'Read the runner output on stderr and fix the failing tests, then ' +
141
+ 're-run "ekanos test".',
135
142
  // Transparent passthrough of the runner's own exit code.
136
143
  exitCode,
137
144
  data,
@@ -1 +1 @@
1
- {"version":3,"file":"test.js","sourceRoot":"","sources":["../../src/commands/test.ts"],"names":[],"mappings":"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","sourcesContent":["import { type spawn as nodeSpawn, spawn } from 'node:child_process';\nimport * as fs from 'node:fs';\n\nimport type { CliContext } from '../context';\nimport { type RenderableFinding, renderFindings } from '../findings-renderer';\nimport { detectPackageManager } from '../package-manager';\nimport { loadProject } from '../project';\nimport {\n newReportPath,\n parseTestReport,\n projectTestScriptUsesVitest,\n reporterArgs,\n} from '../test-report';\nimport { displayPath, formatDuration, pluralize } from '../ui';\n\nexport interface TestArgs {\n dir: string;\n /** argv after `--`, forwarded verbatim to the runner. */\n passthrough: string[];\n /**\n * Escape hatch: skips reporter injection and the curated failures\n * appendix entirely, reproducing exactly the pre-appendix raw-passthrough\n * behavior. Also the sanctioned bypass when the JSON-reporter detection\n * (below) ever guesses wrong for a project.\n */\n raw?: boolean;\n /** Injectable so tests never fork a real process; defaults to the real `node:child_process` spawn. */\n spawnFn?: typeof nodeSpawn;\n}\n\n/**\n * `test` — delegate to the partner's own test script; do NOT reimplement or\n * parse the runner's output ourselves beyond the OPTIONAL structured\n * appendix below. We detect the package manager from the lockfile, spawn\n * `<pm> run test` with any post-`--` args appended, stream the child's\n * output to OUR stderr (keeping stdout pure for the single JSON envelope),\n * and report `{ command, exitCode, durationMs }`.\n *\n * When the project's OWN `test` script names vitest (and `--raw` wasn't\n * passed), we ALSO ask vitest for a `--reporter=json` report alongside its\n * normal terminal output — mirroring how the server-side gate grades a\n * submission's test run, so local triage has the same per-test intelligence\n * as CI. On failure, that report drives a curated \"FAILURES\" appendix\n * (`../findings-renderer.ts`) rendered AFTER the child's own live stream\n * ends; on success, it drives a one-line pass count. Reading/parsing the\n * report is entirely best-effort (`test-report.ts`): anything wrong with it\n * silently drops the appendix, never fails the command.\n *\n * The CLI's exit code is always a transparent passthrough of the runner's\n * exit code, with or without the appendix.\n */\nexport async function runTest(\n ctx: CliContext,\n args: TestArgs,\n): Promise<number> {\n const loaded = loadProject(args.dir);\n const pm = detectPackageManager(loaded.projectDir);\n const spawnFn = args.spawnFn ?? spawn;\n\n const useReporter =\n args.raw !== true && projectTestScriptUsesVitest(loaded.projectDir);\n const reportPath = useReporter ? newReportPath() : null;\n\n const baseRunnerArgs = ['run', 'test', ...args.passthrough];\n // A bare `--` before the injected reporter flags — npm treats anything\n // after `run <script>` as ITS OWN CLI config unless told otherwise (it\n // warns and silently drops `--reporter`/`--outputFile` rather than\n // forwarding them to vitest); pnpm/yarn/bun already forward unrecognized\n // args either way, so the extra separator is a harmless no-op for them.\n // This only protects OUR OWN appended flags — a caller's own passthrough\n // args keep today's exact (unseparated) behavior.\n const runnerArgs = reportPath\n ? [...baseRunnerArgs, '--', ...reporterArgs(reportPath)]\n : baseRunnerArgs;\n const command = `${pm.bin} ${baseRunnerArgs.join(' ')}`;\n const where = displayPath(loaded.projectDir, process.cwd());\n ctx.log(\n ctx.ui.dim(`Running \"${command}\"${where === '.' ? '' : ` in ${where}`}…`),\n );\n\n const started = Date.now();\n const exitCode = await new Promise<number>((resolve, reject) => {\n const child = spawnFn(pm.bin, runnerArgs, {\n cwd: loaded.projectDir,\n // stdin inherited; the child's stdout/stderr are piped so we can route\n // them to OUR stderr and keep stdout reserved for the JSON envelope.\n stdio: ['inherit', 'pipe', 'pipe'],\n });\n\n // In JSON mode every byte of runner output goes to stderr; in human mode we\n // keep the child's stream separation.\n child.stdout?.on('data', (chunk: Buffer) => {\n (ctx.jsonMode ? process.stderr : process.stdout).write(chunk);\n });\n child.stderr?.on('data', (chunk: Buffer) => {\n process.stderr.write(chunk);\n });\n\n child.on('error', (error) => reject(error));\n child.on('close', (code) => resolve(code ?? 1));\n });\n\n const durationMs = Date.now() - started;\n\n const report = reportPath\n ? parseTestReport(reportPath, loaded.projectDir)\n : null;\n if (reportPath) {\n try {\n fs.rmSync(reportPath, { force: true });\n } catch {\n // Best-effort cleanup only — a leftover temp file is not worth failing\n // (or even flagging) the command over.\n }\n }\n\n const data: {\n command: string;\n exitCode: number;\n durationMs: number;\n summary?: { passed: number; failed: number; files: number };\n failures?: RenderableFinding[];\n } = { command, exitCode, durationMs };\n if (report?.summary) data.summary = report.summary;\n\n if (exitCode === 0) {\n const summary = report?.summary;\n const humanLine = ctx.ui.result({\n title: summary\n ? `${pluralize(summary.passed, 'test')} passed`\n : 'Tests passed',\n rows: [\n ...(summary && summary.files > 0\n ? [['Files', String(summary.files)] as const]\n : []),\n ['Command', command],\n ['Time', formatDuration(durationMs)],\n ],\n });\n return ctx.succeed(data, humanLine);\n }\n\n const hasAppendix = report !== null && report.findings.length > 0;\n if (hasAppendix) data.failures = report.findings;\n\n if (!ctx.jsonMode && args.raw !== true) {\n if (hasAppendix && report) {\n ctx.log('');\n ctx.log(ctx.ui.section('FAILURES'));\n renderFindings(ctx, report.findings, { cwd: loaded.projectDir });\n\n if (report.failingFiles.length === 1) {\n ctx.log(\n ctx.ui.nextAction(\n `ekanos test -- run ${report.failingFiles[0]}`,\n 'fix, then re-run just this file',\n ),\n );\n } else {\n ctx.log(\n ctx.ui.dim(\n `Failing files:\\n${report.failingFiles\n .map((file) => ` ${file}`)\n .join('\\n')}`,\n ),\n );\n ctx.log(ctx.ui.nextAction('ekanos test', 'fix the above, then re-run'));\n }\n } else if (useReporter) {\n // Reporter was injected but the report never parsed (vitest crashed\n // before writing it, or wrote something unrecognizable) — say so\n // quietly and fall back to a generic re-run rather than staying silent\n // or failing over a decoration.\n ctx.log('');\n ctx.log(\n ctx.ui.dim(\n '(Structured failure details were unavailable for this run — see the output above.)',\n ),\n );\n ctx.log(ctx.ui.nextAction('ekanos test', 'fix the above, then re-run'));\n }\n }\n\n return ctx.failWith({\n code: 'test_failed',\n message: `\"${command}\" exited with code ${exitCode}.`,\n hint:\n 'Read the runner output on stderr and fix the failing tests, then ' +\n 're-run \"ekanos test\".',\n // Transparent passthrough of the runner's own exit code.\n exitCode,\n data,\n });\n}\n"]}
1
+ {"version":3,"file":"test.js","sourceRoot":"","sources":["../../src/commands/test.ts"],"names":[],"mappings":"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","sourcesContent":["import { type spawn as nodeSpawn, spawn } from 'node:child_process';\nimport * as fs from 'node:fs';\n\nimport type { CliContext } from '../context';\nimport { type RenderableFinding, renderFindings } from '../findings-renderer';\nimport { detectPackageManager } from '../package-manager';\nimport {\n createOutputTail,\n findIgnoredBuilds,\n ignoredBuildsHint,\n} from '../pnpm-builds';\nimport { loadProject } from '../project';\nimport {\n newReportPath,\n parseTestReport,\n projectTestScriptUsesVitest,\n reporterArgs,\n} from '../test-report';\nimport { displayPath, formatDuration, pluralize } from '../ui';\n\nexport interface TestArgs {\n dir: string;\n /** argv after `--`, forwarded verbatim to the runner. */\n passthrough: string[];\n /**\n * Escape hatch: skips reporter injection and the curated failures\n * appendix entirely, reproducing exactly the pre-appendix raw-passthrough\n * behavior. Also the sanctioned bypass when the JSON-reporter detection\n * (below) ever guesses wrong for a project.\n */\n raw?: boolean;\n /** Injectable so tests never fork a real process; defaults to the real `node:child_process` spawn. */\n spawnFn?: typeof nodeSpawn;\n}\n\n/**\n * `test` — delegate to the partner's own test script; do NOT reimplement or\n * parse the runner's output ourselves beyond the OPTIONAL structured\n * appendix below. We detect the package manager from the lockfile, spawn\n * `<pm> run test` with any post-`--` args appended, stream the child's\n * output to OUR stderr (keeping stdout pure for the single JSON envelope),\n * and report `{ command, exitCode, durationMs }`.\n *\n * When the project's OWN `test` script names vitest (and `--raw` wasn't\n * passed), we ALSO ask vitest for a `--reporter=json` report alongside its\n * normal terminal output — mirroring how the server-side gate grades a\n * submission's test run, so local triage has the same per-test intelligence\n * as CI. On failure, that report drives a curated \"FAILURES\" appendix\n * (`../findings-renderer.ts`) rendered AFTER the child's own live stream\n * ends; on success, it drives a one-line pass count. Reading/parsing the\n * report is entirely best-effort (`test-report.ts`): anything wrong with it\n * silently drops the appendix, never fails the command.\n *\n * The CLI's exit code is always a transparent passthrough of the runner's\n * exit code, with or without the appendix.\n */\nexport async function runTest(\n ctx: CliContext,\n args: TestArgs,\n): Promise<number> {\n const loaded = loadProject(args.dir);\n const pm = detectPackageManager(loaded.projectDir);\n const spawnFn = args.spawnFn ?? spawn;\n\n const useReporter =\n args.raw !== true && projectTestScriptUsesVitest(loaded.projectDir);\n const reportPath = useReporter ? newReportPath() : null;\n\n const baseRunnerArgs = ['run', 'test', ...args.passthrough];\n // A bare `--` before the injected reporter flags — npm treats anything\n // after `run <script>` as ITS OWN CLI config unless told otherwise (it\n // warns and silently drops `--reporter`/`--outputFile` rather than\n // forwarding them to vitest); pnpm/yarn/bun already forward unrecognized\n // args either way, so the extra separator is a harmless no-op for them.\n // This only protects OUR OWN appended flags — a caller's own passthrough\n // args keep today's exact (unseparated) behavior.\n const runnerArgs = reportPath\n ? [...baseRunnerArgs, '--', ...reporterArgs(reportPath)]\n : baseRunnerArgs;\n const command = `${pm.bin} ${baseRunnerArgs.join(' ')}`;\n const where = displayPath(loaded.projectDir, process.cwd());\n ctx.log(\n ctx.ui.dim(`Running \"${command}\"${where === '.' ? '' : ` in ${where}`}…`),\n );\n\n const tail = createOutputTail();\n const started = Date.now();\n const exitCode = await new Promise<number>((resolve, reject) => {\n const child = spawnFn(pm.bin, runnerArgs, {\n cwd: loaded.projectDir,\n // stdin inherited; the child's stdout/stderr are piped so we can route\n // them to OUR stderr and keep stdout reserved for the JSON envelope.\n stdio: ['inherit', 'pipe', 'pipe'],\n });\n\n // In JSON mode every byte of runner output goes to stderr; in human mode we\n // keep the child's stream separation.\n child.stdout?.on('data', (chunk: Buffer) => {\n tail.push(chunk);\n (ctx.jsonMode ? process.stderr : process.stdout).write(chunk);\n });\n child.stderr?.on('data', (chunk: Buffer) => {\n tail.push(chunk);\n process.stderr.write(chunk);\n });\n\n child.on('error', (error) => reject(error));\n child.on('close', (code) => resolve(code ?? 1));\n });\n\n const durationMs = Date.now() - started;\n\n const report = reportPath\n ? parseTestReport(reportPath, loaded.projectDir)\n : null;\n if (reportPath) {\n try {\n fs.rmSync(reportPath, { force: true });\n } catch {\n // Best-effort cleanup only — a leftover temp file is not worth failing\n // (or even flagging) the command over.\n }\n }\n\n const data: {\n command: string;\n exitCode: number;\n durationMs: number;\n summary?: { passed: number; failed: number; files: number };\n failures?: RenderableFinding[];\n } = { command, exitCode, durationMs };\n if (report?.summary) data.summary = report.summary;\n\n if (exitCode === 0) {\n const summary = report?.summary;\n const humanLine = ctx.ui.result({\n title: summary\n ? `${pluralize(summary.passed, 'test')} passed`\n : 'Tests passed',\n rows: [\n ...(summary && summary.files > 0\n ? [['Files', String(summary.files)] as const]\n : []),\n ['Command', command],\n ['Time', formatDuration(durationMs)],\n ],\n });\n return ctx.succeed(data, humanLine);\n }\n\n const hasAppendix = report !== null && report.findings.length > 0;\n if (hasAppendix) data.failures = report.findings;\n\n if (!ctx.jsonMode && args.raw !== true) {\n if (hasAppendix && report) {\n ctx.log('');\n ctx.log(ctx.ui.section('FAILURES'));\n renderFindings(ctx, report.findings, { cwd: loaded.projectDir });\n\n if (report.failingFiles.length === 1) {\n ctx.log(\n ctx.ui.nextAction(\n `ekanos test -- run ${report.failingFiles[0]}`,\n 'fix, then re-run just this file',\n ),\n );\n } else {\n ctx.log(\n ctx.ui.dim(\n `Failing files:\\n${report.failingFiles\n .map((file) => ` ${file}`)\n .join('\\n')}`,\n ),\n );\n ctx.log(ctx.ui.nextAction('ekanos test', 'fix the above, then re-run'));\n }\n } else if (useReporter) {\n // Reporter was injected but the report never parsed (vitest crashed\n // before writing it, or wrote something unrecognizable) — say so\n // quietly and fall back to a generic re-run rather than staying silent\n // or failing over a decoration.\n ctx.log('');\n ctx.log(\n ctx.ui.dim(\n '(Structured failure details were unavailable for this run — see the output above.)',\n ),\n );\n ctx.log(ctx.ui.nextAction('ekanos test', 'fix the above, then re-run'));\n }\n }\n\n const ignoredBuilds = findIgnoredBuilds(tail.text());\n\n return ctx.failWith({\n code: 'test_failed',\n message: `\"${command}\" exited with code ${exitCode}.`,\n hint:\n ignoredBuilds.length > 0\n ? ignoredBuildsHint(ignoredBuilds)\n : 'Read the runner output on stderr and fix the failing tests, then ' +\n 're-run \"ekanos test\".',\n // Transparent passthrough of the runner's own exit code.\n exitCode,\n data,\n });\n}\n"]}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * pnpm 10+ aborts with `ERR_PNPM_IGNORED_BUILDS` when a dependency has an
3
+ * install script nobody has approved or declined. The partner-facing fix is
4
+ * always the same edit, so name it instead of leaving them with an opaque
5
+ * "exited with code 1".
6
+ */
7
+ /** Package names (versions stripped) from pnpm's ignored-build error, or `[]`. */
8
+ export declare function findIgnoredBuilds(output: string): string[];
9
+ /** One-sentence remedy naming the file, key and exact packages. */
10
+ export declare function ignoredBuildsHint(packages: readonly string[]): string;
11
+ /** Keeps only the tail of streamed output so matching stays cheap. */
12
+ export declare function createOutputTail(limit?: number): {
13
+ push: (chunk: Buffer | string) => void;
14
+ text: () => string;
15
+ };
@@ -0,0 +1,48 @@
1
+ /**
2
+ * pnpm 10+ aborts with `ERR_PNPM_IGNORED_BUILDS` when a dependency has an
3
+ * install script nobody has approved or declined. The partner-facing fix is
4
+ * always the same edit, so name it instead of leaving them with an opaque
5
+ * "exited with code 1".
6
+ */
7
+ const IGNORED_BUILDS_MARKER = 'ERR_PNPM_IGNORED_BUILDS';
8
+ const IGNORED_LIST = /Ignored build scripts:\s*([^\n]+)/;
9
+ /** Package names (versions stripped) from pnpm's ignored-build error, or `[]`. */
10
+ export function findIgnoredBuilds(output) {
11
+ var _a;
12
+ if (!output.includes(IGNORED_BUILDS_MARKER))
13
+ return [];
14
+ const list = (_a = IGNORED_LIST.exec(output)) === null || _a === void 0 ? void 0 : _a[1];
15
+ if (!list)
16
+ return [];
17
+ const names = list
18
+ .replace(/\.\s*$/, '')
19
+ .split(/,\s*/)
20
+ .map((entry) => entry.trim().replace(/(?<=.)@[^@/\s]+$/, ''))
21
+ .filter((name) => name.length > 0);
22
+ return [...new Set(names)];
23
+ }
24
+ /** One-sentence remedy naming the file, key and exact packages. */
25
+ export function ignoredBuildsHint(packages) {
26
+ const subject = packages.length > 0
27
+ ? `pnpm ignored build scripts for ${packages.join(', ')}`
28
+ : 'pnpm ignored dependency build scripts';
29
+ const entries = packages.length > 0
30
+ ? packages.map((name) => `'${name}': true`).join(', ')
31
+ : '<package>: true';
32
+ return (`${subject} (ERR_PNPM_IGNORED_BUILDS). Open pnpm-workspace.yaml, set ` +
33
+ `each of them under "allowBuilds" to true or false (e.g. ${entries}), ` +
34
+ 'then run "pnpm install" again.');
35
+ }
36
+ /** Keeps only the tail of streamed output so matching stays cheap. */
37
+ export function createOutputTail(limit = 16384) {
38
+ let buffer = '';
39
+ return {
40
+ push: (chunk) => {
41
+ buffer += chunk.toString();
42
+ if (buffer.length > limit)
43
+ buffer = buffer.slice(-limit);
44
+ },
45
+ text: () => buffer,
46
+ };
47
+ }
48
+ //# sourceMappingURL=pnpm-builds.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pnpm-builds.js","sourceRoot":"","sources":["../src/pnpm-builds.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,qBAAqB,GAAG,yBAAyB,CAAC;AACxD,MAAM,YAAY,GAAG,mCAAmC,CAAC;AAEzD,kFAAkF;AAClF,MAAM,UAAU,iBAAiB,CAAC,MAAc;;IAC9C,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,qBAAqB,CAAC;QAAE,OAAO,EAAE,CAAC;IAEvD,MAAM,IAAI,GAAG,MAAA,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,0CAAG,CAAC,CAAC,CAAC;IAC5C,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,CAAC;IAErB,MAAM,KAAK,GAAG,IAAI;SACf,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC;SACrB,KAAK,CAAC,MAAM,CAAC;SACb,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,kBAAkB,EAAE,EAAE,CAAC,CAAC;SAC5D,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAErC,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC;AAC7B,CAAC;AAED,mEAAmE;AACnE,MAAM,UAAU,iBAAiB,CAAC,QAA2B;IAC3D,MAAM,OAAO,GACX,QAAQ,CAAC,MAAM,GAAG,CAAC;QACjB,CAAC,CAAC,kCAAkC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;QACzD,CAAC,CAAC,uCAAuC,CAAC;IAC9C,MAAM,OAAO,GACX,QAAQ,CAAC,MAAM,GAAG,CAAC;QACjB,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,IAAI,SAAS,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;QACtD,CAAC,CAAC,iBAAiB,CAAC;IAExB,OAAO,CACL,GAAG,OAAO,4DAA4D;QACtE,2DAA2D,OAAO,KAAK;QACvE,gCAAgC,CACjC,CAAC;AACJ,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,gBAAgB,CAAC,KAAK,GAAG,KAAM;IAI7C,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,OAAO;QACL,IAAI,EAAE,CAAC,KAAK,EAAE,EAAE;YACd,MAAM,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;YAC3B,IAAI,MAAM,CAAC,MAAM,GAAG,KAAK;gBAAE,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC;QAC3D,CAAC;QACD,IAAI,EAAE,GAAG,EAAE,CAAC,MAAM;KACnB,CAAC;AACJ,CAAC","sourcesContent":["/**\n * pnpm 10+ aborts with `ERR_PNPM_IGNORED_BUILDS` when a dependency has an\n * install script nobody has approved or declined. The partner-facing fix is\n * always the same edit, so name it instead of leaving them with an opaque\n * \"exited with code 1\".\n */\n\nconst IGNORED_BUILDS_MARKER = 'ERR_PNPM_IGNORED_BUILDS';\nconst IGNORED_LIST = /Ignored build scripts:\\s*([^\\n]+)/;\n\n/** Package names (versions stripped) from pnpm's ignored-build error, or `[]`. */\nexport function findIgnoredBuilds(output: string): string[] {\n if (!output.includes(IGNORED_BUILDS_MARKER)) return [];\n\n const list = IGNORED_LIST.exec(output)?.[1];\n if (!list) return [];\n\n const names = list\n .replace(/\\.\\s*$/, '')\n .split(/,\\s*/)\n .map((entry) => entry.trim().replace(/(?<=.)@[^@/\\s]+$/, ''))\n .filter((name) => name.length > 0);\n\n return [...new Set(names)];\n}\n\n/** One-sentence remedy naming the file, key and exact packages. */\nexport function ignoredBuildsHint(packages: readonly string[]): string {\n const subject =\n packages.length > 0\n ? `pnpm ignored build scripts for ${packages.join(', ')}`\n : 'pnpm ignored dependency build scripts';\n const entries =\n packages.length > 0\n ? packages.map((name) => `'${name}': true`).join(', ')\n : '<package>: true';\n\n return (\n `${subject} (ERR_PNPM_IGNORED_BUILDS). Open pnpm-workspace.yaml, set ` +\n `each of them under \"allowBuilds\" to true or false (e.g. ${entries}), ` +\n 'then run \"pnpm install\" again.'\n );\n}\n\n/** Keeps only the tail of streamed output so matching stays cheap. */\nexport function createOutputTail(limit = 16_384): {\n push: (chunk: Buffer | string) => void;\n text: () => string;\n} {\n let buffer = '';\n return {\n push: (chunk) => {\n buffer += chunk.toString();\n if (buffer.length > limit) buffer = buffer.slice(-limit);\n },\n text: () => buffer,\n };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ekanos/cli",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "description": "The Ekanos partner toolchain CLI: scaffold, validate, and test an Ekanos integration against the published @ekanos packages. Agent-native — every verb speaks JSON with a stable exit-code taxonomy.",
5
5
  "keywords": [
6
6
  "cli",
@@ -33,7 +33,7 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "@clack/prompts": "1.7.0",
36
- "@ekanos/integration-schema": "0.4.0",
36
+ "@ekanos/integration-schema": "0.6.0",
37
37
  "esbuild": "0.28.1",
38
38
  "picocolors": "1.1.1"
39
39
  },
@@ -129,6 +129,15 @@ missing key and for an entry past its `expiresAt`). Evolve a schema with a
129
129
  versioned key or a `z.union` — a stored value that no longer parses throws on
130
130
  read.
131
131
 
132
+ **Limits, enforced by the host and by `createMockContext()` alike** (exported
133
+ as `PARTNER_STORAGE_LIMITS` from `@ekanos/integration-schema`): one stored
134
+ value serializes to at most **64 KB**, one secret value is at most 64 KB, an
135
+ integration may write at most **32 distinct secret names**, and a key segment
136
+ is at most 100 characters. A cache of vendor records hits the 64 KB cap
137
+ sooner than intuition says — roughly 120–150 modest records — so budget the
138
+ snapshot (trim the oldest rows until `JSON.stringify` fits, and say so with a
139
+ `trimmed` flag) rather than discovering the cap on the host.
140
+
132
141
  ### Secrets and the activation contract
133
142
 
134
143
  There is **no `secrets` block** in the definition. Secrets are *named*, not
@@ -165,6 +174,15 @@ your slug at discovery.
165
174
  Declared with kebab-case ids. The transport validates `payloadSchema` before
166
175
  your handler runs and owns signature verification (`signature: { header,
167
176
  secretName, scheme? }` or `'none'`) — never verify a signature in the handler.
177
+
178
+ **If you must declare `'none'`** (the vendor's scheme is not yet
179
+ expressible), the delivery is authenticated only by the per-account endpoint
180
+ token in its URL. Treat the payload as a HINT: read the ids out of it, then
181
+ re-fetch the item from the vendor with `ctx.fetch` before writing anything
182
+ to storage a widget will render — never copy names, URLs or text from an
183
+ unverified body into a `clientReadable` key. Confirm removals the same way
184
+ (a 404 or a "trashed" status from the vendor), so a stale retry cannot
185
+ resurrect or delete the wrong row.
168
186
  Omit `scheme` for a plain HMAC-SHA256 body digest; declare one for keyed or
169
187
  timestamped headers (Stripe/Calendly: `{ format: 'keyed', digestKey: 'v1',
170
188
  timestampKey: 't', signedPayload: '{timestamp}.{body}' }`) or SHA-1
@@ -248,6 +266,23 @@ logic — export the handler once and reference it from both declarations
248
266
  (`run: (ctx, args) => createRecord.handler(ctx, CreateRecordInput.parse(args))`)
249
267
  if you also want the assistant able to call it.
250
268
 
269
+ **Reading through an action (credential must stay server-side).** The
270
+ schedule → `clientReadable` storage pattern is for data that is safe to hand
271
+ to the browser. When the credential cannot leave the server, a widget reads
272
+ through an action instead: declare a read-only action (e.g. `get-overview`)
273
+ and wrap it in `useQuery`, whose `queryFn` calls
274
+ `useIntegrationAction(...).mutateAsync`. Declare ONE shared overview action
275
+ and have several widgets call it with the same query key, so they share a
276
+ single fetch rather than one request each:
277
+
278
+ ```tsx
279
+ const overview = useIntegrationAction<void, Overview>('__SLUG__', 'get-overview', { accountId });
280
+ const { data } = useQuery({
281
+ queryKey: ['__SLUG__', 'overview', accountId],
282
+ queryFn: () => overview.mutateAsync(),
283
+ });
284
+ ```
285
+
251
286
  ### `onActivate`
252
287
 
253
288
  Runs once after an activation is first persisted, and again after
@@ -298,6 +333,23 @@ harness and in production. A widget reads declared `clientReadable`
298
333
  account-scope storage back with `fetchIntegrationStorage()` from
299
334
  `@ekanos/sdk/hooks`.
300
335
 
336
+ Two more hooks from `@ekanos/sdk/hooks` that widgets use together:
337
+
338
+ - **Cross-widget events.** Publish with
339
+ `useWidgetEventBus().publish({ subtype, sourceWidget, sourceProduct, payload })`
340
+ and subscribe with
341
+ `useWidgetEvents({ subtypes: ['deal-selected'], sourceProducts: ['__SLUG__'] })`,
342
+ which returns `{ latestEvent, events, clear }`. `sourceProduct` is your slug;
343
+ `subtype` is your own kebab-case name. Both are no-ops outside a dashboard
344
+ (the admin preview, the harness tile view), so calling them never crashes.
345
+ - **Assistant hand-off.** `useAskAssistant()` returns a function that takes a
346
+ `WidgetAskContext` OBJECT, not a string:
347
+ `ask({ widgetId, title, snapshot, suggestedPrompt, suggestedPrompts? })`.
348
+ `snapshot` is small JSON the assistant sees as context; `suggestedPrompt`
349
+ is pre-filled for the user to send; `suggestedPrompts` are extra
350
+ `{ label, prompt }` chips. Put ids in prompts, not free text copied from
351
+ vendor data.
352
+
301
353
  For a widget that WRITES — a form's Save button, not a read — reach for
302
354
  `useIntegrationAction()` (see "Actions" above), not `useAskAssistant()`.
303
355
  `useAskAssistant()` sends a conversational question to the assistant dock;
@@ -306,10 +358,11 @@ your handler with the values the user actually typed.
306
358
 
307
359
  Build widget UI from `@ekanos/ui` — the published slice of the host design
308
360
  system. Available modules (import individually, e.g. `@ekanos/ui/button`):
309
- `alert`, `badge`, `button`, `card`, `dialog`, `dropdown-menu`, `form`, `icon`,
310
- `if`, `input`, `label`, `select`, `separator`, `skeleton`, `spinner`,
311
- `switch`, `textarea`, `tooltip`, `trans`, `utils` (the `cn()` helper),
312
- `ai-prompt-input`, plus the stylesheets `styles.css` / `tokens.css` /
361
+ `alert`, `badge`, `button`, `card`, `confirm-dialog`, `dialog`,
362
+ `dropdown-menu`, `empty-note`, `form`, `icon`, `if`, `input`, `label`,
363
+ `search-field`, `select`, `sheet`, `separator`, `skeleton`, `spinner`,
364
+ `switch`, `table`, `tabs`, `textarea`, `tooltip`, `trans`, `utils` (the
365
+ `cn()` helper), `ai-prompt-input`, plus the stylesheets `styles.css` / `tokens.css` /
313
366
  `theme.css` / `base.css`. Icons are Font Awesome glyph names via
314
367
  `@ekanos/ui/icon`. This generated shell loads Font Awesome **Free**
315
368
  (`@fortawesome/fontawesome-free`) — a much smaller set than the Pro kit Ekanos
@@ -375,6 +428,13 @@ who answers `/api/integrations/<slug>/…`:
375
428
  declared scopes, copy the keys) derived from the declaration plus the
376
429
  entry's `oauth.setup` (console link, provider-specific notes — WRITE these
377
430
  for every OAuth integration), then a form that saves the keys server-side.
431
+ Check the provider's docs for how its token endpoint wants client
432
+ authentication: the default, HTTP Basic, is wrong for some providers
433
+ (LinkedIn accepts only the secret in the form body). Set
434
+ `oauth.provider.tokenAuthMethod: 'client_secret_post'` for those; omit it
435
+ for Basic (`'client_secret_basic'`). A `<slug>_oauth_error=provider_error`
436
+ means the provider itself sent the user back with an error other than
437
+ `access_denied`; read the harness log for its `error_description`.
378
438
  Then Connect, and `onTokens` runs on the server. The connection is
379
439
  memory-only — a dev-server restart means one more Connect. **Never take a
380
440
  user's client id, secret or token through chat or write one into a file;
@@ -411,6 +471,17 @@ Two different fetches, two different guarantees:
411
471
  `ctx.fetch`, just in the browser" — that is the one thing this section
412
472
  means to rule out.
413
473
 
474
+ **Node built-ins are not available to integration code.** The gate bundles
475
+ your single `entry` for the browser as well as for the server, and the entry
476
+ reaches every handler, so `import 'node:crypto'` (or any other built-in)
477
+ anywhere in the graph fails the build — even in code that only ever runs
478
+ server-side. For HMACs, hashes, random ids and JWT signing use **WebCrypto**
479
+ (`globalThis.crypto.subtle`, `crypto.randomUUID()`), which is the same API in
480
+ Node 20+ and every browser; it needs no import and no dependency. A
481
+ passphrase-encrypted private key cannot be imported by WebCrypto — have the
482
+ admin decrypt it once (`openssl pkcs8 -nocrypt`) and store the plain PKCS#8
483
+ key.
484
+
414
485
  In short: widgets get real network in Live API mode, gated by convenience in
415
486
  the harness and by nothing in production; handlers get fixtures in fixtures
416
487
  mode and the real, enforced thing in live mode — locally on the dev server,
@@ -11,12 +11,12 @@
11
11
  "prepare": "node scripts/setup-hooks.mjs"
12
12
  },
13
13
  "dependencies": {
14
- "@ekanos/sdk": "^0.4.0",
15
- "@ekanos/ui": "^0.4.0"
14
+ "@ekanos/sdk": "^0.6.0",
15
+ "@ekanos/ui": "^0.6.0"
16
16
  },
17
17
  "devDependencies": {
18
- "@ekanos/cli": "^0.3.4",
19
- "@ekanos/harness": "^0.4.0",
18
+ "@ekanos/cli": "^0.3.6",
19
+ "@ekanos/harness": "^0.4.3",
20
20
  "@hookform/resolvers": "^5.2.2",
21
21
  "@tailwindcss/postcss": "^4.3.3",
22
22
  "@tanstack/react-query": "^5.101.4",
@@ -1,8 +1,15 @@
1
1
  # pnpm 10+ refuses to run dependency build scripts it has not been told to
2
- # allow, and `vitest` pulls in `esbuild`, whose install script compiles its
3
- # binary. Without this file the first `pnpm install` writes a placeholder
4
- # value here instead, and every later `ekanos dev` / `ekanos test` fails
5
- # with ERR_PNPM_IGNORED_BUILDS until a human edits it. npm and yarn ignore
6
- # this file.
2
+ # allow. Any dependency with an install script that is not answered here gets
3
+ # a placeholder value written by the first `pnpm install`, and every later `ekanos dev` / `ekanos test` then fails
4
+ # with ERR_PNPM_IGNORED_BUILDS until a human edits it. Every build script in
5
+ # the scaffolded dependency tree is answered below. npm and yarn ignore this
6
+ # file.
7
7
  allowBuilds:
8
+ # vitest -> esbuild: the install script puts the platform binary in place.
8
9
  esbuild: true
10
+ # next-intl -> @parcel/watcher: only used for optional message-file
11
+ # watching, which falls back to a plain watcher when the native one is absent.
12
+ '@parcel/watcher': false
13
+ # next-intl -> @swc/core: Next compiles with its own bundled SWC, and the
14
+ # platform binary arrives as an optional dependency, not via this script.
15
+ '@swc/core': false
@@ -33,7 +33,11 @@ live: { default: 'live' },
33
33
  Find `consoleUrl` and the notes in the provider's docs during Phase 2
34
34
  (research) — the "create an app" / "register an application" page. Write
35
35
  what the CONSOLE calls things, not what the declaration calls them; the
36
- harness already prints the declared secret names beside each field. The
36
+ harness already prints the declared secret names beside each field. Also
37
+ read the provider's docs for how its token endpoint authenticates the client:
38
+ the default is HTTP Basic (`'client_secret_basic'`), but a provider that wants
39
+ `client_id` and `client_secret` in the form body (LinkedIn) needs
40
+ `provider.tokenAuthMethod: 'client_secret_post'` in the declaration. The
37
41
  harness derives the rest — provider host, redirect URI, scopes, PKCE — from
38
42
  `defineIntegration({ oauth })`, so do not repeat those in `notes`.
39
43
 
@@ -67,9 +71,13 @@ opens with a **Local OAuth** panel:
67
71
  connected" is expected — say so instead of debugging.
68
72
  - `<slug>_oauth_error=<code>` on the return URL is the transport's
69
73
  vocabulary: `not_configured` = a key is still missing (check the panel's
70
- badges), `denied` = the user cancelled at the provider, `exchange_failed` =
71
- the provider rejected the exchange (wrong redirect URI or secret, or the
72
- token host missing from `egress`), `save_failed` = your `onTokens` threw
74
+ badges), `denied` = the user cancelled at the provider, `provider_error` =
75
+ the provider sent the user back with an error other than `access_denied`
76
+ (the dev-server log has its `error` and `error_description`),
77
+ `exchange_failed` = the provider rejected the exchange (wrong redirect URI
78
+ or secret, the wrong `tokenAuthMethod`, or the token host missing from
79
+ `egress`; the log line carries the provider's redacted response body),
80
+ `save_failed` = your `onTokens` threw
73
81
  (read the dev-server log). Fix the cause; do not retry blindly.
74
82
  - Widgets read the connection through `useOAuthWidgetGate` / the storage
75
83
  route. If they show "Connect" after a successful loop, check `onActivate`