@descent-vtt/spec-harness 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +106 -0
- package/README.md +15 -2
- package/dist/answers.js +2 -1
- package/dist/answers.js.map +1 -1
- package/dist/branch.js +12 -0
- package/dist/branch.js.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +36 -54
- package/dist/cli.js.map +1 -1
- package/dist/configure.d.ts +97 -9
- package/dist/configure.js +310 -14
- package/dist/configure.js.map +1 -1
- package/dist/context.js +2 -1
- package/dist/context.js.map +1 -1
- package/dist/fs.js +1 -0
- package/dist/fs.js.map +1 -1
- package/dist/git.js +2 -1
- package/dist/git.js.map +1 -1
- package/dist/hooks.js.map +1 -1
- package/dist/host.js.map +1 -1
- package/dist/literal.d.ts +18 -0
- package/dist/literal.js +21 -0
- package/dist/literal.js.map +1 -0
- package/dist/probe.js +1 -1
- package/dist/probe.js.map +1 -1
- package/dist/reader.js +7 -1
- package/dist/reader.js.map +1 -1
- package/dist/round.d.ts +28 -3
- package/dist/round.js +90 -9
- package/dist/round.js.map +1 -1
- package/dist/rulings.js +6 -0
- package/dist/rulings.js.map +1 -1
- package/dist/sandbox.js.map +1 -1
- package/dist/server.js +1 -1
- package/dist/server.js.map +1 -1
- package/dist/setup.d.ts +1 -1
- package/dist/setup.js +36 -14
- package/dist/setup.js.map +1 -1
- package/dist/siblings.js +2 -1
- package/dist/siblings.js.map +1 -1
- package/dist/signers.js +10 -1
- package/dist/signers.js.map +1 -1
- package/dist/usage.d.ts +98 -27
- package/dist/usage.js +67 -7
- package/dist/usage.js.map +1 -1
- package/dist/vendor/spec-core/jsonrpc/mcp.js.map +1 -1
- package/dist/vendor/spec-core/markdown/frontmatter.js.map +1 -1
- package/dist/vendor/spec-core/markdown/headings.js.map +1 -1
- package/dist/vendor/spec-core/markdown/links.js.map +1 -1
- package/dist/vendor/spec-core/markdown/scan.js.map +1 -1
- package/dist/vendor/spec-core/pattern/automaton.js.map +1 -1
- package/dist/vendor/spec-core/pattern/charset.js.map +1 -1
- package/dist/vendor/spec-core/pattern/regex.js.map +1 -1
- package/dist/vendor/spec-core/text/lines.js.map +1 -1
- package/dist/vendor/spec-core/text/width.js.map +1 -1
- package/dist/versions.js +3 -0
- package/dist/versions.js.map +1 -1
- package/dist/workspace.d.ts +3 -3
- package/dist/workspace.js +3 -1
- package/dist/workspace.js.map +1 -1
- package/package.json +3 -3
package/dist/siblings.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"siblings.js","sourceRoot":"","sources":["../src/siblings.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The edge where the harness meets spec-brief, spec-graph and spec-guard.\n *\n * Through their command lines and their versioned JSON, never by importing\n * their packages: each is released, pinned and dropped on its own, and none\n * becomes a dependency of this one (spec-core ADR-0005, ADR-0002 here).\n *\n * A sibling is found as the repository installed it -\n * `node_modules/@descent-vtt/<name>/bin/<name>.js`, run with this Node - or\n * as `.spec-harness.json` names it. One that is not there is `absent`, and\n * what needed it says so; nothing it would have checked is reported clean.\n * One installed below this release's minimum is `outdated`, and is reported\n * the same way rather than run (versions.ts).\n */\n\nimport { execFile } from 'node:child_process';\nimport { existsSync, readFileSync } from 'node:fs';\nimport { join } from 'node:path';\n\nimport { parseBriefList, SiblingOutputError } from './briefs.js';\nimport type { HarnessConfig, SiblingName } from './config.js';\nimport type { BriefRow } from './types.js';\nimport { checkVersion } from './versions.js';\n\nexport interface SiblingRun {\n readonly code: number;\n readonly stdout: string;\n readonly stderr: string;\n}\n\nexport type Sibling =\n /** `version` is `null` for a command `.spec-harness.json` names, whose version is not checked. */\n | { readonly kind: 'found'; readonly command: readonly string[]; readonly version: string | null }\n | { readonly kind: 'absent'; readonly reason: string }\n /** Installed, at a version below the minimum or one that cannot be read, which is `null`. */\n | { readonly kind: 'outdated'; readonly reason: string; readonly version: string | null };\n\n/** Where a sibling is, or why it cannot be run. */\nexport function locate(name: SiblingName, root: string, config: HarnessConfig): Sibling {\n // A command the configuration names is run as named: which package it runs,\n // and so which version, is the configuration's to say.\n const configured = config.tools[name];\n if (configured !== null) return { kind: 'found', command: configured, version: null };\n const directory = join(root, 'node_modules', '@descent-vtt', name);\n const bin = join(directory, 'bin', `${name}.js`);\n if (!existsSync(bin)) {\n return {\n kind: 'absent',\n reason: `${name} is not installed here: npm install --save-dev @descent-vtt/${name}, or name its command under \"tools\" in .spec-harness.json`,\n };\n }\n const check = checkVersion(name, declaredVersion(join(directory, 'package.json')));\n if (!check.ok) return { kind: 'outdated', reason: check.reason, version: check.version };\n return { kind: 'found', command: [process.execPath, bin], version: check.version };\n}\n\n/** The `version` a package.json declares, or `undefined` when there is no such file or it is not JSON. */\nexport function declaredVersion(file: string): unknown {\n // A manifest that holds null throws on the read, and any other value that\n // is not an object has no version, so the catch answers for both. Falling\n // out of it answers undefined as well, and a Buffer parses as its text:\n // the empty catch and the encoding are equivalent to their mutants.\n try {\n return (JSON.parse(readFileSync(file, 'utf8')) as { version?: unknown }).version;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Runs a sibling with arguments, in the repository root.\n *\n * Never through a shell: an argument is a path or a brief id, and a shell\n * would read it as syntax. `node` as a command's first word is this Node, so\n * `[\"node\", \"path/to/bin.js\"]` works on every host; a Windows `.cmd` shim such\n * as `npx` cannot be started without a shell, and the error says to name the\n * script instead.\n */\nexport function runSibling(command: readonly string[], args: readonly string[], root: string, timeoutMs = 600_000): Promise<SiblingRun> {\n const [first, ...rest] = command;\n const program = first === 'node' ? process.execPath : (first as string);\n return new Promise((resolve) => {\n execFile(\n program,\n [...rest, ...args],\n {\n cwd: root,\n maxBuffer: 256 * 1024 * 1024,\n encoding: 'utf8',\n // Keeps a console window from opening on Windows; the sweep runs on\n // Linux, where the mutant is equivalent.\n windowsHide: true,\n timeout: timeoutMs,\n shell: false,\n env: { ...process.env, NO_COLOR: '1', FORCE_COLOR: '0' },\n },\n (error, stdout, stderr) => {\n const failure = error as (Error & { code?: unknown }) | null;\n if (failure !== null && failure.code === 'ENOENT') {\n resolve({\n code: -1,\n stdout: '',\n stderr: `cannot start \"${first}\": name the tool's script, such as [\"node\", \"node_modules/@descent-vtt/<tool>/bin/<tool>.js\"]`,\n });\n return;\n }\n const code = failure === null ? 0 : typeof failure.code === 'number' ? failure.code : -1;\n resolve({ code, stdout, stderr });\n },\n );\n });\n}\n\nexport class SiblingError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'SiblingError';\n }\n}\n\nfunction parseJson(name: string, run: SiblingRun): unknown {\n try {\n return JSON.parse(run.stdout);\n } catch {\n const said = (run.stderr.trim() || run.stdout.trim()).split('\\n').slice(0, 3).join(' ');\n throw new SiblingError(`${name} exited ${run.code} without JSON${said ? `: ${said}` : ''}`);\n }\n}\n\nexport interface Siblings {\n readonly root: string;\n locate(name: SiblingName): Sibling;\n /** Every brief, live and archived. Exit 2 from spec-brief is an error here too. */\n briefs(): Promise<BriefRow[]>;\n /** A sibling's JSON output for a command line, with its exit code and what it said on stderr; `absent` says why it cannot be run, missing or outdated. */\n json(name: SiblingName, args: readonly string[]): Promise<{ readonly code: number; readonly document: unknown; readonly stderr: string } | { readonly absent: string }>;\n}\n\nexport function createSiblings(root: string, config: HarnessConfig): Siblings {\n const found = (name: SiblingName): readonly string[] => {\n const sibling = locate(name, root, config);\n if (sibling.kind !== 'found') throw new SiblingError(sibling.reason);\n return sibling.command;\n };\n return {\n root,\n locate: (name) => locate(name, root, config),\n async briefs() {\n const run = await runSibling(found('spec-brief'), ['list', '--archived', '--format', 'json', '--no-color'], root);\n if (run.code === 2) {\n throw new SiblingError(`spec-brief could not list the briefs: ${run.stderr.trim().split('\\n')[0] || 'exit 2'}`);\n }\n try {\n return parseBriefList(parseJson('spec-brief', run));\n } catch (error) {\n // parseJson throws a SiblingError, which this would copy, and\n // parseBriefList nothing but its own: the check is equivalent to its\n // mutant, and keeps any other error the stack trace it needs.\n if (error instanceof SiblingOutputError) throw new SiblingError(error.message);\n throw error;\n }\n },\n async json(name, args) {\n const sibling = locate(name, root, config);\n if (sibling.kind !== 'found') return { absent: sibling.reason };\n const run = await runSibling(sibling.command, args, root);\n return { code: run.code, document: parseJson(name, run), stderr: run.stderr };\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"siblings.js","sourceRoot":"","sources":["../src/siblings.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The edge where the harness meets spec-brief, spec-graph and spec-guard.\n *\n * Through their command lines and their versioned JSON, never by importing\n * their packages: each is released, pinned and dropped on its own, and none\n * becomes a dependency of this one (spec-core ADR-0005, ADR-0002 here).\n *\n * A sibling is found as the repository installed it -\n * `node_modules/@descent-vtt/<name>/bin/<name>.js`, run with this Node - or\n * as `.spec-harness.json` names it. One that is not there is `absent`, and\n * what needed it says so; nothing it would have checked is reported clean.\n * One installed below this release's minimum is `outdated`, and is reported\n * the same way rather than run (versions.ts).\n */\n\nimport { execFile } from 'node:child_process';\nimport { existsSync, readFileSync } from 'node:fs';\nimport { join } from 'node:path';\n\nimport { parseBriefList, SiblingOutputError } from './briefs.js';\nimport type { HarnessConfig, SiblingName } from './config.js';\nimport type { BriefRow } from './types.js';\nimport { checkVersion } from './versions.js';\n\nexport interface SiblingRun {\n readonly code: number;\n readonly stdout: string;\n readonly stderr: string;\n}\n\nexport type Sibling =\n /** `version` is `null` for a command `.spec-harness.json` names, whose version is not checked. */\n | { readonly kind: 'found'; readonly command: readonly string[]; readonly version: string | null }\n | { readonly kind: 'absent'; readonly reason: string }\n /** Installed, at a version below the minimum or one that cannot be read, which is `null`. */\n | { readonly kind: 'outdated'; readonly reason: string; readonly version: string | null };\n\n/** Where a sibling is, or why it cannot be run. */\nexport function locate(name: SiblingName, root: string, config: HarnessConfig): Sibling {\n // A command the configuration names is run as named: which package it runs,\n // and so which version, is the configuration's to say.\n const configured = config.tools[name];\n if (configured !== null) return { kind: 'found', command: configured, version: null };\n const directory = join(root, 'node_modules', '@descent-vtt', name);\n const bin = join(directory, 'bin', `${name}.js`);\n if (!existsSync(bin)) {\n return {\n kind: 'absent',\n reason: `${name} is not installed here: npm install --save-dev @descent-vtt/${name}, or name its command under \"tools\" in .spec-harness.json`,\n };\n }\n const check = checkVersion(name, declaredVersion(join(directory, 'package.json')));\n if (!check.ok) return { kind: 'outdated', reason: check.reason, version: check.version };\n return { kind: 'found', command: [process.execPath, bin], version: check.version };\n}\n\n/** The `version` a package.json declares, or `undefined` when there is no such file or it is not JSON. */\nexport function declaredVersion(file: string): unknown {\n // A manifest that holds null throws on the read, and any other value that\n // is not an object has no version, so the catch answers for both. Falling\n // out of it answers undefined as well, and a Buffer parses as its text:\n // the empty catch and the encoding are equivalent to their mutants.\n try {\n const manifest: { version?: unknown } = JSON.parse(readFileSync(file, 'utf8'));\n return manifest.version;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Runs a sibling with arguments, in the repository root.\n *\n * Never through a shell: an argument is a path or a brief id, and a shell\n * would read it as syntax. `node` as a command's first word is this Node, so\n * `[\"node\", \"path/to/bin.js\"]` works on every host; a Windows `.cmd` shim such\n * as `npx` cannot be started without a shell, and the error says to name the\n * script instead.\n */\nexport function runSibling(command: readonly string[], args: readonly string[], root: string, timeoutMs = 600_000): Promise<SiblingRun> {\n const [first, ...rest] = command;\n const program = first === 'node' ? process.execPath : (first as string);\n return new Promise((resolve) => {\n execFile(\n program,\n [...rest, ...args],\n {\n cwd: root,\n maxBuffer: 256 * 1024 * 1024,\n encoding: 'utf8',\n // Keeps a console window from opening on Windows; the sweep runs on\n // Linux, where the mutant is equivalent.\n windowsHide: true,\n timeout: timeoutMs,\n shell: false,\n env: { ...process.env, NO_COLOR: '1', FORCE_COLOR: '0' },\n },\n (error, stdout, stderr) => {\n const failure = error as (Error & { code?: unknown }) | null;\n if (failure !== null && failure.code === 'ENOENT') {\n resolve({\n code: -1,\n stdout: '',\n stderr: `cannot start \"${first}\": name the tool's script, such as [\"node\", \"node_modules/@descent-vtt/<tool>/bin/<tool>.js\"]`,\n });\n return;\n }\n const code = failure === null ? 0 : typeof failure.code === 'number' ? failure.code : -1;\n resolve({ code, stdout, stderr });\n },\n );\n });\n}\n\nexport class SiblingError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'SiblingError';\n }\n}\n\nfunction parseJson(name: string, run: SiblingRun): unknown {\n try {\n return JSON.parse(run.stdout);\n } catch {\n const said = (run.stderr.trim() || run.stdout.trim()).split('\\n').slice(0, 3).join(' ');\n throw new SiblingError(`${name} exited ${run.code} without JSON${said ? `: ${said}` : ''}`);\n }\n}\n\nexport interface Siblings {\n readonly root: string;\n locate(name: SiblingName): Sibling;\n /** Every brief, live and archived. Exit 2 from spec-brief is an error here too. */\n briefs(): Promise<BriefRow[]>;\n /** A sibling's JSON output for a command line, with its exit code and what it said on stderr; `absent` says why it cannot be run, missing or outdated. */\n json(name: SiblingName, args: readonly string[]): Promise<{ readonly code: number; readonly document: unknown; readonly stderr: string } | { readonly absent: string }>;\n}\n\nexport function createSiblings(root: string, config: HarnessConfig): Siblings {\n const found = (name: SiblingName): readonly string[] => {\n const sibling = locate(name, root, config);\n if (sibling.kind !== 'found') throw new SiblingError(sibling.reason);\n return sibling.command;\n };\n return {\n root,\n locate: (name) => locate(name, root, config),\n async briefs() {\n const run = await runSibling(found('spec-brief'), ['list', '--archived', '--format', 'json', '--no-color'], root);\n if (run.code === 2) {\n throw new SiblingError(`spec-brief could not list the briefs: ${run.stderr.trim().split('\\n')[0] || 'exit 2'}`);\n }\n try {\n return parseBriefList(parseJson('spec-brief', run));\n } catch (error) {\n // parseJson throws a SiblingError, which this would copy, and\n // parseBriefList nothing but its own: the check is equivalent to its\n // mutant, and keeps any other error the stack trace it needs.\n if (error instanceof SiblingOutputError) throw new SiblingError(error.message);\n throw error;\n }\n },\n async json(name, args) {\n const sibling = locate(name, root, config);\n if (sibling.kind !== 'found') return { absent: sibling.reason };\n const run = await runSibling(sibling.command, args, root);\n return { code: run.code, document: parseJson(name, run), stderr: run.stderr };\n },\n };\n}\n"]}
|
package/dist/signers.js
CHANGED
|
@@ -24,7 +24,11 @@ export const FIDO2_KEY_TYPES = ['sk-ssh-ed25519@openssh.com', 'sk-ecdsa-sha2-nis
|
|
|
24
24
|
* type OpenSSH adds later is read as one, as OpenSSH reads it.
|
|
25
25
|
*/
|
|
26
26
|
const KEY = /^AAAA[A-Za-z0-9+/]+={0,2}$/;
|
|
27
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* What OpenSSH's `strdelim` skips as whitespace between fields. Nothing is a
|
|
29
|
+
* match too, at the start of any text, so the anchor is equivalent to its
|
|
30
|
+
* mutant.
|
|
31
|
+
*/
|
|
28
32
|
const WHITESPACE = /^[ \t\r\n]*/;
|
|
29
33
|
/**
|
|
30
34
|
* The principals field, as OpenSSH's `strdelimw` reads it: up to the first
|
|
@@ -67,6 +71,9 @@ function optionsField(text) {
|
|
|
67
71
|
}
|
|
68
72
|
/** A key type or a key: up to the next space or tab. */
|
|
69
73
|
function word(text) {
|
|
74
|
+
// Each part of the pattern can match nothing, so it matches any text from
|
|
75
|
+
// its start, and its last part runs to the end: the match is never null,
|
|
76
|
+
// and either anchor is equivalent to its mutant.
|
|
70
77
|
const [, value, rest] = /^([^ \t]*)[ \t]*(.*)$/s.exec(text);
|
|
71
78
|
return { value: value, rest: rest };
|
|
72
79
|
}
|
|
@@ -87,6 +94,8 @@ function readOptions(text) {
|
|
|
87
94
|
let certAuthority = false;
|
|
88
95
|
let at = 0;
|
|
89
96
|
while (at < text.length) {
|
|
97
|
+
// Nothing is a match too, so there is one at the start of any text: the
|
|
98
|
+
// match is never null, and the anchor is equivalent to its mutant.
|
|
90
99
|
const name = /^[^,="]*/.exec(text.slice(at))[0];
|
|
91
100
|
const key = name.toLowerCase();
|
|
92
101
|
at += name.length;
|
package/dist/signers.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"signers.js","sourceRoot":"","sources":["../src/signers.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The allowed-signers file, read as OpenSSH reads it (ssh-keygen(1), ALLOWED\n * SIGNERS), to say which signers sign with a key that is not FIDO2.\n *\n * A line is `principals [options] keytype key [comment]`: principals a\n * comma-separated list, which may hold a double-quoted part; options a\n * comma-separated list with no space outside quotes, such as\n * `cert-authority`, `namespaces=\"git\"` or `valid-after=\"20260101\"`, each\n * value in double quotes. The quotes are read as OpenSSH's sshsig.c and\n * misc.c read them, and each function below names the one it follows. A\n * `cert-authority` line holds the key of a certificate authority, not a\n * person's: the keys it certifies are not in the file, so their type cannot\n * be read from it.\n *\n * Pure: doctor reads the file from the base and passes the text.\n */\n\n/** The key types of a FIDO2 security key: a signature needs a touch no process can supply (ADR-0006). */\nexport const FIDO2_KEY_TYPES: readonly string[] = ['sk-ssh-ed25519@openssh.com', 'sk-ecdsa-sha2-nistp256@openssh.com'];\n\nexport interface AllowedSigner {\n /** 1-based. */\n readonly line: number;\n readonly principals: readonly string[];\n /** Whether the key is a certificate authority's. */\n readonly certAuthority: boolean;\n /** The namespaces the key may sign in, or `null` for any. */\n readonly namespaces: readonly string[] | null;\n readonly validAfter: string | null;\n readonly validBefore: string | null;\n readonly keyType: string;\n readonly key: string;\n}\n\nexport interface SignersRead {\n readonly signers: readonly AllowedSigner[];\n /** Lines that are not a signer, with why. */\n readonly problems: readonly { readonly line: number; readonly message: string }[];\n}\n\n/**\n * A key as the file writes it: base64 of the key's wire form, which starts\n * with the four-byte length of its type's name, so with `AAAA`. OpenSSH tells\n * options from a key type by trying to read a key after it; a key type is\n * followed by a key, and options by a key type, which is not one. So a key\n * type OpenSSH adds later is read as one, as OpenSSH reads it.\n */\nconst KEY = /^AAAA[A-Za-z0-9+/]+={0,2}$/;\n\n/** What OpenSSH's `strdelim` skips as whitespace between fields. */\nconst WHITESPACE = /^[ \\t\\r\\n]*/;\n\n/**\n * The principals field, as OpenSSH's `strdelimw` reads it: up to the first\n * whitespace or double quote. A quote there, at the start or in the middle,\n * is taken out with the next one, and the field ends at that one whatever\n * follows it: `a@example.com,\"b@example.com\"` is\n * `a@example.com,b@example.com`, and `\"a\"b` is `a`, with `b` the next field.\n * `null` when the quote is never closed, which makes the line no signer.\n */\nfunction principalsField(text: string): { value: string; rest: string } | null {\n const at = text.search(/[ \\t\\r\\n\"]/);\n if (at === -1) return { value: text, rest: '' };\n if (text.charAt(at) !== '\"') return { value: text.slice(0, at), rest: text.slice(at).replace(WHITESPACE, '') };\n const close = text.indexOf('\"', at + 1);\n if (close === -1) return null;\n return { value: text.slice(0, at) + text.slice(at + 1, close), rest: text.slice(close + 1).replace(WHITESPACE, '') };\n}\n\n/**\n * The options field, as OpenSSH's `sshkey_advance_past_options` finds its\n * end: the first space or tab outside double quotes, where `\\\"` is a quote\n * inside them. `null` when a quote is never closed.\n */\nfunction optionsField(text: string): { value: string; rest: string } | null {\n // Reading one character past the end changes nothing - `charAt` gives the\n // empty string there - so `<=` for `<` is equivalent.\n let quoted = false;\n let i = 0;\n for (; i < text.length && (quoted || (text.charAt(i) !== ' ' && text.charAt(i) !== '\\t')); i += 1) {\n if (text.charAt(i) === '\\\\' && text.charAt(i + 1) === '\"') i += 1;\n else if (text.charAt(i) === '\"') quoted = !quoted;\n }\n if (quoted) return null;\n return { value: text.slice(0, i), rest: text.slice(i).replace(/^[ \\t]*/, '') };\n}\n\n/** A key type or a key: up to the next space or tab. */\nfunction word(text: string): { value: string; rest: string } {\n const [, value, rest] = /^([^ \\t]*)[ \\t]*(.*)$/s.exec(text) as RegExpExecArray;\n return { value: value as string, rest: rest as string };\n}\n\ninterface Options {\n readonly certAuthority: boolean;\n readonly namespaces: string | null;\n readonly validAfter: string | null;\n readonly validBefore: string | null;\n}\n\nconst NO_OPTIONS: Options = { certAuthority: false, namespaces: null, validAfter: null, validBefore: null };\n\n/** The options with a value, each of which OpenSSH takes once. */\nconst VALUED = ['namespaces', 'valid-after', 'valid-before'];\n\n/**\n * The options, as OpenSSH's `sshsigopt_parse` reads them: `cert-authority`\n * alone and `namespaces`, `valid-after` and `valid-before` with a value,\n * separated by commas, a name in any case and a value always in double\n * quotes, `\\\"` a quote inside one (its `opt_dequote`). Anything else - another\n * option, a value without its quotes or given twice, anything but a comma\n * after an option, a comma with nothing after it - makes the line no signer,\n * as it does for OpenSSH. The times are read as written.\n */\nfunction readOptions(text: string): Options | { refused: string } {\n const values = new Map<string, string>();\n let certAuthority = false;\n let at = 0;\n while (at < text.length) {\n const name = (/^[^,=\"]*/.exec(text.slice(at)) as RegExpExecArray)[0];\n const key = name.toLowerCase();\n at += name.length;\n const valued = text.charAt(at) === '=';\n if (key === 'cert-authority' && !valued) {\n certAuthority = true;\n } else if (VALUED.includes(key) && valued) {\n // `optionsField` has refused a quote never closed, so a value that\n // opens one closes it.\n const quoted = /^\"((?:\\\\\"|[^\"])*)\"/.exec(text.slice(at + 1));\n if (quoted === null) return { refused: `the value of ${name} is not in double quotes, as OpenSSH needs: write ${name}=\"...\"` };\n if (values.has(key)) return { refused: `it gives ${key} twice, which OpenSSH refuses` };\n values.set(key, (quoted[1] as string).replace(/\\\\\"/g, '\"'));\n at += 1 + quoted[0].length;\n } else {\n return { refused: 'OpenSSH reads only cert-authority, namespaces=\"...\", valid-after=\"...\" and valid-before=\"...\" as options' };\n }\n if (at === text.length) break;\n if (text.charAt(at) !== ',') return { refused: 'its options are not separated by commas' };\n at += 1;\n if (at === text.length) return { refused: 'its options end in a comma' };\n }\n return { certAuthority, namespaces: values.get('namespaces') ?? null, validAfter: values.get('valid-after') ?? null, validBefore: values.get('valid-before') ?? null };\n}\n\n/** Reads the file's text: every signer, and every line that is not one. */\nexport function readAllowedSigners(text: string): SignersRead {\n const signers: AllowedSigner[] = [];\n const problems: { line: number; message: string }[] = [];\n text.split(/\\r?\\n/).forEach((raw, index) => {\n const line = index + 1;\n const content = raw.trim();\n if (content === '' || content.startsWith('#')) return;\n const refuse = (message: string): void => {\n problems.push({ line, message });\n };\n const principals = principalsField(content);\n if (principals === null) return refuse('its principals open a double quote that is never closed');\n // OpenSSH reads a key where the key type would be, and options when it\n // cannot: a key type is followed by a key, and options are not.\n let type = word(principals.rest);\n let written: string | null = null;\n if (!KEY.test(word(type.rest).value)) {\n const field = optionsField(principals.rest);\n if (field === null) return refuse('its options open a double quote that is never closed');\n written = field.value;\n type = word(field.rest);\n }\n const key = word(type.rest).value;\n // With no key type, nothing follows it either, and no key is a key.\n if (!KEY.test(key)) return refuse('a signer is its principals, any options, a key type and a key');\n // OpenSSH reads the key before the options, and says first what is wrong with it.\n const options = written === null ? NO_OPTIONS : readOptions(written);\n if ('refused' in options) return refuse(options.refused);\n signers.push({\n line,\n // The field keeps no quote, so every comma parts two principals, as\n // OpenSSH's pattern lists read them.\n principals: principals.value\n .split(',')\n .map((principal) => principal.trim())\n .filter((principal) => principal !== ''),\n certAuthority: options.certAuthority,\n namespaces: options.namespaces === null ? null : options.namespaces.split(',').map((name) => name.trim()),\n validAfter: options.validAfter,\n validBefore: options.validBefore,\n keyType: type.value,\n key,\n });\n });\n return { signers, problems };\n}\n\n/**\n * The signers whose own key is not a FIDO2 security key's. A certificate\n * authority's line is left out: the keys it vouches for are not in the file.\n */\nexport function notFido2(signers: readonly AllowedSigner[]): AllowedSigner[] {\n return signers.filter((signer) => !signer.certAuthority && !FIDO2_KEY_TYPES.includes(signer.keyType));\n}\n"]}
|
|
1
|
+
{"version":3,"file":"signers.js","sourceRoot":"","sources":["../src/signers.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The allowed-signers file, read as OpenSSH reads it (ssh-keygen(1), ALLOWED\n * SIGNERS), to say which signers sign with a key that is not FIDO2.\n *\n * A line is `principals [options] keytype key [comment]`: principals a\n * comma-separated list, which may hold a double-quoted part; options a\n * comma-separated list with no space outside quotes, such as\n * `cert-authority`, `namespaces=\"git\"` or `valid-after=\"20260101\"`, each\n * value in double quotes. The quotes are read as OpenSSH's sshsig.c and\n * misc.c read them, and each function below names the one it follows. A\n * `cert-authority` line holds the key of a certificate authority, not a\n * person's: the keys it certifies are not in the file, so their type cannot\n * be read from it.\n *\n * Pure: doctor reads the file from the base and passes the text.\n */\n\n/** The key types of a FIDO2 security key: a signature needs a touch no process can supply (ADR-0006). */\nexport const FIDO2_KEY_TYPES: readonly string[] = ['sk-ssh-ed25519@openssh.com', 'sk-ecdsa-sha2-nistp256@openssh.com'];\n\nexport interface AllowedSigner {\n /** 1-based. */\n readonly line: number;\n readonly principals: readonly string[];\n /** Whether the key is a certificate authority's. */\n readonly certAuthority: boolean;\n /** The namespaces the key may sign in, or `null` for any. */\n readonly namespaces: readonly string[] | null;\n readonly validAfter: string | null;\n readonly validBefore: string | null;\n readonly keyType: string;\n readonly key: string;\n}\n\nexport interface SignersRead {\n readonly signers: readonly AllowedSigner[];\n /** Lines that are not a signer, with why. */\n readonly problems: readonly { readonly line: number; readonly message: string }[];\n}\n\n/**\n * A key as the file writes it: base64 of the key's wire form, which starts\n * with the four-byte length of its type's name, so with `AAAA`. OpenSSH tells\n * options from a key type by trying to read a key after it; a key type is\n * followed by a key, and options by a key type, which is not one. So a key\n * type OpenSSH adds later is read as one, as OpenSSH reads it.\n */\nconst KEY = /^AAAA[A-Za-z0-9+/]+={0,2}$/;\n\n/**\n * What OpenSSH's `strdelim` skips as whitespace between fields. Nothing is a\n * match too, at the start of any text, so the anchor is equivalent to its\n * mutant.\n */\nconst WHITESPACE = /^[ \\t\\r\\n]*/;\n\n/**\n * The principals field, as OpenSSH's `strdelimw` reads it: up to the first\n * whitespace or double quote. A quote there, at the start or in the middle,\n * is taken out with the next one, and the field ends at that one whatever\n * follows it: `a@example.com,\"b@example.com\"` is\n * `a@example.com,b@example.com`, and `\"a\"b` is `a`, with `b` the next field.\n * `null` when the quote is never closed, which makes the line no signer.\n */\nfunction principalsField(text: string): { value: string; rest: string } | null {\n const at = text.search(/[ \\t\\r\\n\"]/);\n if (at === -1) return { value: text, rest: '' };\n if (text.charAt(at) !== '\"') return { value: text.slice(0, at), rest: text.slice(at).replace(WHITESPACE, '') };\n const close = text.indexOf('\"', at + 1);\n if (close === -1) return null;\n return { value: text.slice(0, at) + text.slice(at + 1, close), rest: text.slice(close + 1).replace(WHITESPACE, '') };\n}\n\n/**\n * The options field, as OpenSSH's `sshkey_advance_past_options` finds its\n * end: the first space or tab outside double quotes, where `\\\"` is a quote\n * inside them. `null` when a quote is never closed.\n */\nfunction optionsField(text: string): { value: string; rest: string } | null {\n // Reading one character past the end changes nothing - `charAt` gives the\n // empty string there - so `<=` for `<` is equivalent.\n let quoted = false;\n let i = 0;\n for (; i < text.length && (quoted || (text.charAt(i) !== ' ' && text.charAt(i) !== '\\t')); i += 1) {\n if (text.charAt(i) === '\\\\' && text.charAt(i + 1) === '\"') i += 1;\n else if (text.charAt(i) === '\"') quoted = !quoted;\n }\n if (quoted) return null;\n return { value: text.slice(0, i), rest: text.slice(i).replace(/^[ \\t]*/, '') };\n}\n\n/** A key type or a key: up to the next space or tab. */\nfunction word(text: string): { value: string; rest: string } {\n // Each part of the pattern can match nothing, so it matches any text from\n // its start, and its last part runs to the end: the match is never null,\n // and either anchor is equivalent to its mutant.\n const [, value, rest] = /^([^ \\t]*)[ \\t]*(.*)$/s.exec(text)!;\n return { value: value as string, rest: rest as string };\n}\n\ninterface Options {\n readonly certAuthority: boolean;\n readonly namespaces: string | null;\n readonly validAfter: string | null;\n readonly validBefore: string | null;\n}\n\nconst NO_OPTIONS: Options = { certAuthority: false, namespaces: null, validAfter: null, validBefore: null };\n\n/** The options with a value, each of which OpenSSH takes once. */\nconst VALUED = ['namespaces', 'valid-after', 'valid-before'];\n\n/**\n * The options, as OpenSSH's `sshsigopt_parse` reads them: `cert-authority`\n * alone and `namespaces`, `valid-after` and `valid-before` with a value,\n * separated by commas, a name in any case and a value always in double\n * quotes, `\\\"` a quote inside one (its `opt_dequote`). Anything else - another\n * option, a value without its quotes or given twice, anything but a comma\n * after an option, a comma with nothing after it - makes the line no signer,\n * as it does for OpenSSH. The times are read as written.\n */\nfunction readOptions(text: string): Options | { refused: string } {\n const values = new Map<string, string>();\n let certAuthority = false;\n let at = 0;\n while (at < text.length) {\n // Nothing is a match too, so there is one at the start of any text: the\n // match is never null, and the anchor is equivalent to its mutant.\n const name = /^[^,=\"]*/.exec(text.slice(at))![0];\n const key = name.toLowerCase();\n at += name.length;\n const valued = text.charAt(at) === '=';\n if (key === 'cert-authority' && !valued) {\n certAuthority = true;\n } else if (VALUED.includes(key) && valued) {\n // `optionsField` has refused a quote never closed, so a value that\n // opens one closes it.\n const quoted = /^\"((?:\\\\\"|[^\"])*)\"/.exec(text.slice(at + 1));\n if (quoted === null) return { refused: `the value of ${name} is not in double quotes, as OpenSSH needs: write ${name}=\"...\"` };\n if (values.has(key)) return { refused: `it gives ${key} twice, which OpenSSH refuses` };\n values.set(key, (quoted[1] as string).replace(/\\\\\"/g, '\"'));\n at += 1 + quoted[0].length;\n } else {\n return { refused: 'OpenSSH reads only cert-authority, namespaces=\"...\", valid-after=\"...\" and valid-before=\"...\" as options' };\n }\n if (at === text.length) break;\n if (text.charAt(at) !== ',') return { refused: 'its options are not separated by commas' };\n at += 1;\n if (at === text.length) return { refused: 'its options end in a comma' };\n }\n return { certAuthority, namespaces: values.get('namespaces') ?? null, validAfter: values.get('valid-after') ?? null, validBefore: values.get('valid-before') ?? null };\n}\n\n/** Reads the file's text: every signer, and every line that is not one. */\nexport function readAllowedSigners(text: string): SignersRead {\n const signers: AllowedSigner[] = [];\n const problems: { line: number; message: string }[] = [];\n text.split(/\\r?\\n/).forEach((raw, index) => {\n const line = index + 1;\n const content = raw.trim();\n if (content === '' || content.startsWith('#')) return;\n const refuse = (message: string): void => {\n problems.push({ line, message });\n };\n const principals = principalsField(content);\n if (principals === null) return refuse('its principals open a double quote that is never closed');\n // OpenSSH reads a key where the key type would be, and options when it\n // cannot: a key type is followed by a key, and options are not.\n let type = word(principals.rest);\n let written: string | null = null;\n if (!KEY.test(word(type.rest).value)) {\n const field = optionsField(principals.rest);\n if (field === null) return refuse('its options open a double quote that is never closed');\n written = field.value;\n type = word(field.rest);\n }\n const key = word(type.rest).value;\n // With no key type, nothing follows it either, and no key is a key.\n if (!KEY.test(key)) return refuse('a signer is its principals, any options, a key type and a key');\n // OpenSSH reads the key before the options, and says first what is wrong with it.\n const options = written === null ? NO_OPTIONS : readOptions(written);\n if ('refused' in options) return refuse(options.refused);\n signers.push({\n line,\n // The field keeps no quote, so every comma parts two principals, as\n // OpenSSH's pattern lists read them.\n principals: principals.value\n .split(',')\n .map((principal) => principal.trim())\n .filter((principal) => principal !== ''),\n certAuthority: options.certAuthority,\n namespaces: options.namespaces === null ? null : options.namespaces.split(',').map((name) => name.trim()),\n validAfter: options.validAfter,\n validBefore: options.validBefore,\n keyType: type.value,\n key,\n });\n });\n return { signers, problems };\n}\n\n/**\n * The signers whose own key is not a FIDO2 security key's. A certificate\n * authority's line is left out: the keys it vouches for are not in the file.\n */\nexport function notFido2(signers: readonly AllowedSigner[]): AllowedSigner[] {\n return signers.filter((signer) => !signer.certAuthority && !FIDO2_KEY_TYPES.includes(signer.keyType));\n}\n"]}
|
package/dist/usage.d.ts
CHANGED
|
@@ -44,31 +44,31 @@ declare const VALUED: {
|
|
|
44
44
|
};
|
|
45
45
|
readonly base: {
|
|
46
46
|
readonly of: (line: CommandLine) => readonly string[];
|
|
47
|
-
readonly says:
|
|
47
|
+
readonly says: "names no commit; give it a branch or a commit, or leave it out for the configured base";
|
|
48
48
|
};
|
|
49
49
|
readonly root: {
|
|
50
50
|
readonly of: (line: CommandLine) => readonly string[];
|
|
51
|
-
readonly says:
|
|
51
|
+
readonly says: "names no directory; give it one, or leave it out to run in the current directory";
|
|
52
52
|
};
|
|
53
53
|
readonly path: {
|
|
54
54
|
readonly of: (line: CommandLine) => readonly string[];
|
|
55
|
-
readonly says:
|
|
55
|
+
readonly says: "names no file; give it the path of a file";
|
|
56
56
|
};
|
|
57
57
|
readonly reason: {
|
|
58
58
|
readonly of: (line: CommandLine) => readonly string[];
|
|
59
|
-
readonly says:
|
|
59
|
+
readonly says: "gives no reason; say why the round cannot be done without the file";
|
|
60
60
|
};
|
|
61
61
|
readonly option: {
|
|
62
62
|
readonly of: (line: CommandLine) => readonly string[];
|
|
63
|
-
readonly says:
|
|
63
|
+
readonly says: "names no choice; give it \"<label>: <cost>\", or leave it out";
|
|
64
64
|
};
|
|
65
65
|
readonly recommend: {
|
|
66
66
|
readonly of: (line: CommandLine) => readonly string[];
|
|
67
|
-
readonly says:
|
|
67
|
+
readonly says: "recommends nothing; say which option and why, or leave it out";
|
|
68
68
|
};
|
|
69
69
|
readonly show: {
|
|
70
70
|
readonly of: (line: CommandLine) => readonly string[];
|
|
71
|
-
readonly says:
|
|
71
|
+
readonly says: "names no escalation; give it an id, as spec-harness escalate --list prints them";
|
|
72
72
|
};
|
|
73
73
|
readonly id: {
|
|
74
74
|
readonly of: (line: CommandLine) => readonly string[];
|
|
@@ -96,9 +96,9 @@ declare const SHAPE: {
|
|
|
96
96
|
readonly takes: "one argument at most, a brief's id";
|
|
97
97
|
};
|
|
98
98
|
readonly argument: {
|
|
99
|
-
readonly role:
|
|
99
|
+
readonly role: "the brief";
|
|
100
100
|
readonly instead: "give it a brief's id, or leave it out for the one --brief, SPEC_BRIEF or the branch names";
|
|
101
|
-
readonly option:
|
|
101
|
+
readonly option: "brief";
|
|
102
102
|
};
|
|
103
103
|
readonly options: readonly ["brief", "base", "root", "format"];
|
|
104
104
|
};
|
|
@@ -106,28 +106,28 @@ declare const SHAPE: {
|
|
|
106
106
|
readonly options: readonly ["brief", "base", "root", "format", "strict"];
|
|
107
107
|
readonly limit: null;
|
|
108
108
|
readonly argument: {
|
|
109
|
-
readonly role:
|
|
110
|
-
readonly instead:
|
|
109
|
+
readonly role: "a path";
|
|
110
|
+
readonly instead: "give it the paths the round would write";
|
|
111
111
|
};
|
|
112
112
|
};
|
|
113
113
|
readonly hook: {
|
|
114
114
|
readonly options: readonly ["brief", "base", "root"];
|
|
115
115
|
readonly limit: {
|
|
116
116
|
readonly most: 1;
|
|
117
|
-
readonly takes:
|
|
117
|
+
readonly takes: "one argument, \"claude\" or \"git\"";
|
|
118
118
|
};
|
|
119
119
|
};
|
|
120
120
|
readonly escalate: {
|
|
121
121
|
readonly limit: {
|
|
122
122
|
readonly most: 0;
|
|
123
|
-
readonly takes:
|
|
123
|
+
readonly takes: "no argument";
|
|
124
124
|
};
|
|
125
125
|
readonly options: readonly ["brief", "root", "format", "path", "reason", "option", "recommend", "list", "show"];
|
|
126
126
|
readonly modes: readonly [{
|
|
127
|
-
readonly option:
|
|
127
|
+
readonly option: "list";
|
|
128
128
|
readonly beside: readonly ["root", "format"];
|
|
129
129
|
}, {
|
|
130
|
-
readonly option:
|
|
130
|
+
readonly option: "show";
|
|
131
131
|
readonly beside: readonly ["root", "format"];
|
|
132
132
|
}];
|
|
133
133
|
};
|
|
@@ -138,8 +138,8 @@ declare const SHAPE: {
|
|
|
138
138
|
readonly takes: "one argument, the escalation's id";
|
|
139
139
|
};
|
|
140
140
|
readonly argument: {
|
|
141
|
-
readonly role:
|
|
142
|
-
readonly instead:
|
|
141
|
+
readonly role: "the escalation";
|
|
142
|
+
readonly instead: "give it an id, as spec-harness escalate --list prints them";
|
|
143
143
|
};
|
|
144
144
|
};
|
|
145
145
|
readonly rulings: {
|
|
@@ -148,9 +148,9 @@ declare const SHAPE: {
|
|
|
148
148
|
readonly takes: "one argument at most, a brief's id";
|
|
149
149
|
};
|
|
150
150
|
readonly argument: {
|
|
151
|
-
readonly role:
|
|
151
|
+
readonly role: "the brief";
|
|
152
152
|
readonly instead: "give it a brief's id, or leave it out for the one --brief, SPEC_BRIEF or the branch names";
|
|
153
|
-
readonly option:
|
|
153
|
+
readonly option: "brief";
|
|
154
154
|
};
|
|
155
155
|
readonly options: readonly ["brief", "base", "root", "format"];
|
|
156
156
|
};
|
|
@@ -160,9 +160,9 @@ declare const SHAPE: {
|
|
|
160
160
|
readonly takes: "one argument at most, a brief's id";
|
|
161
161
|
};
|
|
162
162
|
readonly argument: {
|
|
163
|
-
readonly role:
|
|
163
|
+
readonly role: "the brief";
|
|
164
164
|
readonly instead: "give it a brief's id, or leave it out for the one --brief, SPEC_BRIEF or the branch names";
|
|
165
|
-
readonly option:
|
|
165
|
+
readonly option: "brief";
|
|
166
166
|
};
|
|
167
167
|
readonly options: readonly ["brief", "base", "root", "format", "strict"];
|
|
168
168
|
};
|
|
@@ -172,37 +172,37 @@ declare const SHAPE: {
|
|
|
172
172
|
readonly takes: "one argument at most, a brief's id";
|
|
173
173
|
};
|
|
174
174
|
readonly argument: {
|
|
175
|
-
readonly role:
|
|
175
|
+
readonly role: "the brief";
|
|
176
176
|
readonly instead: "give it a brief's id, or leave it out for the one --brief, SPEC_BRIEF or the branch names";
|
|
177
|
-
readonly option:
|
|
177
|
+
readonly option: "brief";
|
|
178
178
|
};
|
|
179
179
|
readonly options: readonly ["brief", "base", "root", "format", "id", "at"];
|
|
180
180
|
};
|
|
181
181
|
readonly premises: {
|
|
182
182
|
readonly limit: {
|
|
183
183
|
readonly most: 0;
|
|
184
|
-
readonly takes:
|
|
184
|
+
readonly takes: "no argument";
|
|
185
185
|
};
|
|
186
186
|
readonly options: readonly ["brief", "root", "format", "strict"];
|
|
187
187
|
};
|
|
188
188
|
readonly init: {
|
|
189
189
|
readonly limit: {
|
|
190
190
|
readonly most: 0;
|
|
191
|
-
readonly takes:
|
|
191
|
+
readonly takes: "no argument";
|
|
192
192
|
};
|
|
193
193
|
readonly options: readonly ["root", "format", "write", "git-hook"];
|
|
194
194
|
};
|
|
195
195
|
readonly mcp: {
|
|
196
196
|
readonly limit: {
|
|
197
197
|
readonly most: 0;
|
|
198
|
-
readonly takes:
|
|
198
|
+
readonly takes: "no argument";
|
|
199
199
|
};
|
|
200
200
|
readonly options: readonly ["root"];
|
|
201
201
|
};
|
|
202
202
|
readonly doctor: {
|
|
203
203
|
readonly limit: {
|
|
204
204
|
readonly most: 0;
|
|
205
|
-
readonly takes:
|
|
205
|
+
readonly takes: "no argument";
|
|
206
206
|
};
|
|
207
207
|
readonly options: readonly ["brief", "base", "root", "format", "strict"];
|
|
208
208
|
readonly reports: readonly ["brief", "base"];
|
|
@@ -222,6 +222,77 @@ export type Command = keyof typeof SHAPE;
|
|
|
222
222
|
export declare function isClaudeHook(command: string | undefined, argument: string | undefined): boolean;
|
|
223
223
|
/** The words for a `--root` that is a file, or is not there, wherever it is said. */
|
|
224
224
|
export declare function noDirectory(root: string): string;
|
|
225
|
+
/** What is wrong with the line of Claude Code's hook, and what mends it. */
|
|
226
|
+
export interface LineFault {
|
|
227
|
+
readonly wrong: string;
|
|
228
|
+
readonly mend: string;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* What Claude Code's hook does with its own line, as far as the line decides
|
|
232
|
+
* it: it guards, in the root the line gives it where it gives one and under
|
|
233
|
+
* the brief it names where it names one; or the guard is off, for a fault
|
|
234
|
+
* the hook says with exit 1 and holds no write for; or every write is held,
|
|
235
|
+
* exit 2, for a brief the line names that is not one (ADR-0005). The hook
|
|
236
|
+
* reads its line through the functions below, and `doctor` reads a line it
|
|
237
|
+
* finds in Claude Code's settings through the same ones, so what it says of
|
|
238
|
+
* a line is what the hook does with it.
|
|
239
|
+
*/
|
|
240
|
+
export type HookLine = Guards | GuardOff | WritesHeld;
|
|
241
|
+
export interface Guards {
|
|
242
|
+
readonly state: 'guards';
|
|
243
|
+
readonly root: string | undefined;
|
|
244
|
+
/**
|
|
245
|
+
* The brief the line names with `--brief`, as written. Whether it is a
|
|
246
|
+
* brief is spec-brief's to say, which the caller asks. Absent where the
|
|
247
|
+
* line names none.
|
|
248
|
+
*/
|
|
249
|
+
readonly brief?: string;
|
|
250
|
+
}
|
|
251
|
+
export interface GuardOff {
|
|
252
|
+
readonly state: 'off';
|
|
253
|
+
readonly fault: LineFault;
|
|
254
|
+
}
|
|
255
|
+
export interface WritesHeld {
|
|
256
|
+
readonly state: 'held';
|
|
257
|
+
/** What is said of the name, by what named the brief (workspace.ts). */
|
|
258
|
+
readonly wrong: string;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* A line under which the round has no brief to be checked against, since
|
|
262
|
+
* what names one names a brief spec-brief does not know, or one it has
|
|
263
|
+
* archived: the hook holds the write, exit 2, and every write after it until
|
|
264
|
+
* the name is mended (ADR-0004). `wrong` is what is said of the name.
|
|
265
|
+
*/
|
|
266
|
+
export declare function heldHookLine(wrong: string): WritesHeld;
|
|
267
|
+
/**
|
|
268
|
+
* A line the parser refused: an option no command has, an option left
|
|
269
|
+
* without its value, a `--format` or an `--at` that is none. The parser's
|
|
270
|
+
* words are kept, on one line: what node says of an option that may have
|
|
271
|
+
* lost its value is three.
|
|
272
|
+
*/
|
|
273
|
+
export declare function unreadHookLine(said: string): GuardOff;
|
|
274
|
+
/**
|
|
275
|
+
* A line that parsed, by the root it gives. An empty root is the directory
|
|
276
|
+
* the hook was started in: git starts there when it is handed no directory,
|
|
277
|
+
* so the hook has guarded there for an empty root in every release. A root
|
|
278
|
+
* that is a file, or is not there, is one git starts in neither of, which
|
|
279
|
+
* read as a session outside a work tree, exit 0 and no word, and the guard
|
|
280
|
+
* someone had configured was off.
|
|
281
|
+
*/
|
|
282
|
+
export declare function readHookLine(root: string | undefined, cwd: string, isDirectory: (path: string) => boolean): Guards | GuardOff;
|
|
283
|
+
/**
|
|
284
|
+
* What Claude Code's hook does with the words it is handed, or `null` for
|
|
285
|
+
* words that are not its line: read by the parser the command line reads
|
|
286
|
+
* every line with, and known by its command and the argument after it on a
|
|
287
|
+
* line that parsed and on one that did not, as the command line knows it
|
|
288
|
+
* (cli.ts). `parse` is that parser, which throws its refusal with the words
|
|
289
|
+
* it could still read.
|
|
290
|
+
*
|
|
291
|
+
* A line that guards says which brief it names, where it names one: the hook
|
|
292
|
+
* passes over a `--brief` that is empty, as every release has, and holds
|
|
293
|
+
* every write under one that names no brief, which only spec-brief can tell.
|
|
294
|
+
*/
|
|
295
|
+
export declare function claudeHookLine(words: readonly string[], parse: (argv: readonly string[]) => CommandLine, cwd: string, isDirectory: (path: string) => boolean): HookLine | null;
|
|
225
296
|
/**
|
|
226
297
|
* Why the command line is refused, or `null` when the command can run:
|
|
227
298
|
* an option it does not read, alone or beside another, an argument it does
|
package/dist/usage.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* `null`. The one thing only the disk knows, whether `--root` is a
|
|
14
14
|
* directory, is asked of the caller.
|
|
15
15
|
*/
|
|
16
|
+
import { literal } from './literal.js';
|
|
16
17
|
/** Whether a value names nothing: empty, or only space. */
|
|
17
18
|
export function isBlank(value) {
|
|
18
19
|
return value.trim() === '';
|
|
@@ -26,7 +27,7 @@ const one = (value) => (value === undefined ? [] : [value]);
|
|
|
26
27
|
* parser refuses any other, the empty one with them. Nor is `--note`, which
|
|
27
28
|
* `rule` refuses by name itself.
|
|
28
29
|
*/
|
|
29
|
-
const VALUED = {
|
|
30
|
+
const VALUED = literal({
|
|
30
31
|
brief: { of: (line) => one(line.brief), says: "names no brief; give it a brief's id, or leave it out for the one SPEC_BRIEF or the branch names" },
|
|
31
32
|
base: { of: (line) => one(line.base), says: 'names no commit; give it a branch or a commit, or leave it out for the configured base' },
|
|
32
33
|
root: { of: (line) => one(line.root), says: 'names no directory; give it one, or leave it out to run in the current directory' },
|
|
@@ -36,7 +37,7 @@ const VALUED = {
|
|
|
36
37
|
recommend: { of: (line) => one(line.recommend), says: 'recommends nothing; say which option and why, or leave it out' },
|
|
37
38
|
show: { of: (line) => one(line.show), says: 'names no escalation; give it an id, as spec-harness escalate --list prints them' },
|
|
38
39
|
id: { of: (line) => one(line.id), says: "names no probe; give it a probe's id, or leave it out to run every probe" },
|
|
39
|
-
};
|
|
40
|
+
});
|
|
40
41
|
/**
|
|
41
42
|
* The line for a value that names nothing. `subject` is the input as its
|
|
42
43
|
* caller knows it: `--brief` on a command line, `"brief"` in a tool call. The
|
|
@@ -45,11 +46,11 @@ const VALUED = {
|
|
|
45
46
|
export function emptyValue(subject, option, value) {
|
|
46
47
|
return `${subject} is ${JSON.stringify(value)}, which ${VALUED[option].says}`;
|
|
47
48
|
}
|
|
48
|
-
const NO_ARGUMENT = { limit: { most: 0, takes: 'no argument' } };
|
|
49
|
-
const ONE_BRIEF = {
|
|
49
|
+
const NO_ARGUMENT = literal({ limit: { most: 0, takes: 'no argument' } });
|
|
50
|
+
const ONE_BRIEF = literal({
|
|
50
51
|
limit: { most: 1, takes: "one argument at most, a brief's id" },
|
|
51
52
|
argument: { role: 'the brief', instead: "give it a brief's id, or leave it out for the one --brief, SPEC_BRIEF or the branch names", option: 'brief' },
|
|
52
|
-
};
|
|
53
|
+
});
|
|
53
54
|
/**
|
|
54
55
|
* Every command, with what it reads of a command line. An option is listed
|
|
55
56
|
* for a command only where the command's answer depends on it: `init`
|
|
@@ -57,7 +58,7 @@ const ONE_BRIEF = {
|
|
|
57
58
|
* neither takes `--base` or `--brief`, and taking them would tell someone
|
|
58
59
|
* the base or the brief had been set.
|
|
59
60
|
*/
|
|
60
|
-
const SHAPE = {
|
|
61
|
+
const SHAPE = literal({
|
|
61
62
|
context: { options: ['brief', 'base', 'root', 'format'], ...ONE_BRIEF },
|
|
62
63
|
guard: {
|
|
63
64
|
options: ['brief', 'base', 'root', 'format', 'strict'],
|
|
@@ -90,7 +91,7 @@ const SHAPE = {
|
|
|
90
91
|
// that acts on it stops: a brief or a base that names nothing is a line of
|
|
91
92
|
// doctor's report.
|
|
92
93
|
doctor: { options: ['brief', 'base', 'root', 'format', 'strict'], ...NO_ARGUMENT, reports: ['brief', 'base'] },
|
|
93
|
-
};
|
|
94
|
+
});
|
|
94
95
|
// A map, since a command is a word a person types and an object answers to
|
|
95
96
|
// more names than it was given: `constructor` has no shape.
|
|
96
97
|
const SHAPES = new Map(Object.entries(SHAPE));
|
|
@@ -117,6 +118,65 @@ export function isClaudeHook(command, argument) {
|
|
|
117
118
|
export function noDirectory(root) {
|
|
118
119
|
return `--root is "${root}", which is not a directory`;
|
|
119
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* A line under which the round has no brief to be checked against, since
|
|
123
|
+
* what names one names a brief spec-brief does not know, or one it has
|
|
124
|
+
* archived: the hook holds the write, exit 2, and every write after it until
|
|
125
|
+
* the name is mended (ADR-0004). `wrong` is what is said of the name.
|
|
126
|
+
*/
|
|
127
|
+
export function heldHookLine(wrong) {
|
|
128
|
+
return { state: 'held', wrong };
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* A line the parser refused: an option no command has, an option left
|
|
132
|
+
* without its value, a `--format` or an `--at` that is none. The parser's
|
|
133
|
+
* words are kept, on one line: what node says of an option that may have
|
|
134
|
+
* lost its value is three.
|
|
135
|
+
*/
|
|
136
|
+
export function unreadHookLine(said) {
|
|
137
|
+
return { state: 'off', fault: { wrong: `the hook's line cannot be read (${said.split('\n').join(' ')})`, mend: 'mend it in Claude Code\'s settings, where init writes "hook claude" and nothing after it' } };
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* A line that parsed, by the root it gives. An empty root is the directory
|
|
141
|
+
* the hook was started in: git starts there when it is handed no directory,
|
|
142
|
+
* so the hook has guarded there for an empty root in every release. A root
|
|
143
|
+
* that is a file, or is not there, is one git starts in neither of, which
|
|
144
|
+
* read as a session outside a work tree, exit 0 and no word, and the guard
|
|
145
|
+
* someone had configured was off.
|
|
146
|
+
*/
|
|
147
|
+
export function readHookLine(root, cwd, isDirectory) {
|
|
148
|
+
const given = root === '' ? cwd : root;
|
|
149
|
+
if (given !== undefined && !isDirectory(given)) {
|
|
150
|
+
return { state: 'off', fault: { wrong: noDirectory(given), mend: "give the hook's line a directory that exists, or leave --root out for the project Claude Code names" } };
|
|
151
|
+
}
|
|
152
|
+
return { state: 'guards', root: given };
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* What Claude Code's hook does with the words it is handed, or `null` for
|
|
156
|
+
* words that are not its line: read by the parser the command line reads
|
|
157
|
+
* every line with, and known by its command and the argument after it on a
|
|
158
|
+
* line that parsed and on one that did not, as the command line knows it
|
|
159
|
+
* (cli.ts). `parse` is that parser, which throws its refusal with the words
|
|
160
|
+
* it could still read.
|
|
161
|
+
*
|
|
162
|
+
* A line that guards says which brief it names, where it names one: the hook
|
|
163
|
+
* passes over a `--brief` that is empty, as every release has, and holds
|
|
164
|
+
* every write under one that names no brief, which only spec-brief can tell.
|
|
165
|
+
*/
|
|
166
|
+
export function claudeHookLine(words, parse, cwd, isDirectory) {
|
|
167
|
+
let line;
|
|
168
|
+
try {
|
|
169
|
+
line = parse(words);
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
const refused = error;
|
|
173
|
+
return isClaudeHook(refused.words[0], refused.words[1]) ? unreadHookLine(refused.message) : null;
|
|
174
|
+
}
|
|
175
|
+
if (!isClaudeHook(line.command, line.positionals[0]))
|
|
176
|
+
return null;
|
|
177
|
+
const read = readHookLine(line.root, cwd, isDirectory);
|
|
178
|
+
return read.state === 'guards' && line.brief !== undefined && !isBlank(line.brief) ? { ...read, brief: line.brief } : read;
|
|
179
|
+
}
|
|
120
180
|
/**
|
|
121
181
|
* Why the command line is refused, or `null` when the command can run:
|
|
122
182
|
* an option it does not read, alone or beside another, an argument it does
|