@ekanos/cli 0.3.4 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/dev.js +11 -3
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/test.js +9 -2
- package/dist/commands/test.js.map +1 -1
- package/dist/pnpm-builds.d.ts +15 -0
- package/dist/pnpm-builds.js +48 -0
- package/dist/pnpm-builds.js.map +1 -0
- package/package.json +2 -2
- package/templates/AGENTS.md.tmpl +63 -0
- package/templates/package.json.tmpl +4 -4
- package/templates/pnpm-workspace.yaml.tmpl +12 -5
package/dist/commands/dev.js
CHANGED
|
@@ -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:
|
|
162
|
-
|
|
163
|
-
|
|
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));
|
package/dist/commands/dev.js.map
CHANGED
|
@@ -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"]}
|
package/dist/commands/test.js
CHANGED
|
@@ -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:
|
|
134
|
-
|
|
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.
|
|
3
|
+
"version": "0.3.5",
|
|
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.
|
|
36
|
+
"@ekanos/integration-schema": "0.5.0",
|
|
37
37
|
"esbuild": "0.28.1",
|
|
38
38
|
"picocolors": "1.1.1"
|
|
39
39
|
},
|
package/templates/AGENTS.md.tmpl
CHANGED
|
@@ -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;
|
|
@@ -411,6 +463,17 @@ Two different fetches, two different guarantees:
|
|
|
411
463
|
`ctx.fetch`, just in the browser" — that is the one thing this section
|
|
412
464
|
means to rule out.
|
|
413
465
|
|
|
466
|
+
**Node built-ins are not available to integration code.** The gate bundles
|
|
467
|
+
your single `entry` for the browser as well as for the server, and the entry
|
|
468
|
+
reaches every handler, so `import 'node:crypto'` (or any other built-in)
|
|
469
|
+
anywhere in the graph fails the build — even in code that only ever runs
|
|
470
|
+
server-side. For HMACs, hashes, random ids and JWT signing use **WebCrypto**
|
|
471
|
+
(`globalThis.crypto.subtle`, `crypto.randomUUID()`), which is the same API in
|
|
472
|
+
Node 20+ and every browser; it needs no import and no dependency. A
|
|
473
|
+
passphrase-encrypted private key cannot be imported by WebCrypto — have the
|
|
474
|
+
admin decrypt it once (`openssl pkcs8 -nocrypt`) and store the plain PKCS#8
|
|
475
|
+
key.
|
|
476
|
+
|
|
414
477
|
In short: widgets get real network in Live API mode, gated by convenience in
|
|
415
478
|
the harness and by nothing in production; handlers get fixtures in fixtures
|
|
416
479
|
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.
|
|
15
|
-
"@ekanos/ui": "^0.
|
|
14
|
+
"@ekanos/sdk": "^0.5.0",
|
|
15
|
+
"@ekanos/ui": "^0.5.0"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
|
-
"@ekanos/cli": "^0.3.
|
|
19
|
-
"@ekanos/harness": "^0.4.
|
|
18
|
+
"@ekanos/cli": "^0.3.5",
|
|
19
|
+
"@ekanos/harness": "^0.4.1",
|
|
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
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
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
|