@starci/hfs 4.7.47 → 4.9.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/CHANGELOG.md +18 -0
- package/README.md +4 -9
- package/emit/operation-schemas.mjs +3 -0
- package/lint/bound-sys.cjs +2 -2
- package/package.json +11 -5
- package/runtime/engine/admission.mjs +1 -1
- package/runtime/engine/config.mjs +2 -2
- package/runtime/engine/db/machine.mjs +2 -3
- package/runtime/engine/invalid-config.mjs +1 -1
- package/runtime/knowledge/grammars/{starci → core}/playbook.yaml +3 -3
- package/runtime/knowledge/grammars/index.yaml +14 -23
- package/runtime/knowledge/grammars/offset-pop/playbook.yaml +2 -2
- package/runtime/knowledge/hfs/canon-pins.yaml +23 -23
- package/runtime/knowledge/hfs/fe-stack.yaml +1 -1
- package/runtime/knowledge/hfs/rules.yaml +83 -60
- package/runtime/modules/kernel/failure-codes.yaml +23 -13
- package/runtime/scripts/guards/git-policy.mjs +2 -3
- package/runtime/scripts/hfs/rule-catalog.mjs +2 -2
- package/runtime/scripts/hfs/rules/pipeline.mjs +5 -5
- package/runtime/scripts/lib/example-refs.mjs +10 -3
- package/runtime/scripts/lib/grammar-lint.mjs +16 -3
- package/runtime/scripts/lib/package-at.mjs +5 -6
- package/runtime/scripts/lib/roots.mjs +2 -2
- package/runtime/scripts/work/draw/draw-dna-findings.mjs +23 -12
- package/runtime/scripts/work/draw/draw-dna.mjs +14 -8
- package/scaffold/app.mjs +6 -6
- package/src/main.mjs +3 -3
- package/sync/hygiene.mjs +1 -1
- package/sync/index.mjs +15 -2
- package/sync/managed.mjs +3 -2
- package/templates/app/codegen-graphql.mjs.partial +4 -4
- package/templates/app/quality-config/sonar-project.properties +4 -2
- package/runtime/engine/canonical-json.mjs +0 -14
- package/runtime/engine/db/ledger-open.mjs +0 -38
- package/runtime/engine/db/ledger-paths.mjs +0 -87
- package/runtime/engine/db/ledger.mjs +0 -1068
- package/runtime/engine/db/provision-authority.mjs +0 -76
- package/runtime/engine/db/provision-operations.mjs +0 -146
- package/runtime/packages/grammar/scripts/build-stamp.mjs +0 -98
- package/runtime/scripts/agent/codex-app-server.mjs +0 -79
- package/runtime/scripts/agent/codex-mirror-source.mjs +0 -34
- package/runtime/scripts/agent/host-agents.mjs +0 -80
- package/runtime/scripts/agent/launch-trust-policy.mjs +0 -53
- package/runtime/scripts/agent/trust-launch.mjs +0 -191
- package/runtime/scripts/agent/trust.mjs +0 -523
- package/runtime/scripts/api/fs/temp-path.mjs +0 -8
- package/runtime/scripts/api/git/diff-names.mjs +0 -8
- package/runtime/scripts/api/git/head-time.mjs +0 -10
- package/runtime/scripts/api/git/index-lock.mjs +0 -78
- package/runtime/scripts/api/git/log.mjs +0 -5
- package/runtime/scripts/api/git/merge-tree.mjs +0 -5
- package/runtime/scripts/api/git/rev-list.mjs +0 -5
- package/runtime/scripts/api/git/rev-parse.mjs +0 -13
- package/runtime/scripts/api/git/status-query.mjs +0 -5
- package/runtime/scripts/api/git/worktree-list-query.mjs +0 -5
- package/runtime/scripts/api/node/spawn-node.mjs +0 -6
- package/runtime/scripts/api/npm/dist-tag.mjs +0 -8
- package/runtime/scripts/api/npm/lib.mjs +0 -32
- package/runtime/scripts/api/npm/pack-dry-run.mjs +0 -9
- package/runtime/scripts/api/npm/run-npm.mjs +0 -6
- package/runtime/scripts/api/npm/view.mjs +0 -30
- package/runtime/scripts/api/npm/whoami.mjs +0 -8
- package/runtime/scripts/api/process/hide-child-windows.mjs +0 -31
- package/runtime/scripts/api/process/kill-tree.mjs +0 -22
- package/runtime/scripts/api/process/lib.mjs +0 -125
- package/runtime/scripts/api/process/probe-guard-command.mjs +0 -74
- package/runtime/scripts/api/process/process-list.mjs +0 -28
- package/runtime/scripts/api/process/system-tool.mjs +0 -37
- package/runtime/scripts/gates/canon-digest.mjs +0 -81
- package/runtime/scripts/gates/canon-pins.mjs +0 -119
- package/runtime/scripts/gates/gate-input.mjs +0 -98
- package/runtime/scripts/gates/gate.mjs +0 -636
- package/runtime/scripts/gates/grammar-dist.mjs +0 -206
- package/runtime/scripts/gates/output.mjs +0 -40
- package/runtime/scripts/gates/runtime-package-selection.mjs +0 -27
- package/runtime/scripts/gates/test-world-run.mjs +0 -252
- package/runtime/scripts/gates/type-impact.mjs +0 -277
- package/runtime/scripts/gates/type-program.mjs +0 -113
- package/runtime/scripts/guards/assigned-command.mjs +0 -12
- package/runtime/scripts/guards/call-rights.mjs +0 -47
- package/runtime/scripts/guards/command-guard.mjs +0 -468
- package/runtime/scripts/guards/command-policy.mjs +0 -342
- package/runtime/scripts/guards/critic-reach.mjs +0 -82
- package/runtime/scripts/guards/deps-guard.mjs +0 -150
- package/runtime/scripts/guards/env-dump-verdict.mjs +0 -89
- package/runtime/scripts/guards/guards-root.mjs +0 -27
- package/runtime/scripts/guards/hook-install.mjs +0 -343
- package/runtime/scripts/guards/hook-verdict.mjs +0 -12
- package/runtime/scripts/guards/install-verdict.mjs +0 -41
- package/runtime/scripts/guards/kernel-seat.mjs +0 -63
- package/runtime/scripts/guards/launch-verdict.mjs +0 -74
- package/runtime/scripts/guards/op-starci.mjs +0 -16
- package/runtime/scripts/guards/op-write-scope.mjs +0 -27
- package/runtime/scripts/guards/orca-self-lifecycle.mjs +0 -39
- package/runtime/scripts/guards/process-kill-verdict.mjs +0 -106
- package/runtime/scripts/guards/protected-zone.mjs +0 -56
- package/runtime/scripts/guards/refusals.mjs +0 -27
- package/runtime/scripts/guards/rights.mjs +0 -268
- package/runtime/scripts/guards/seat-tools.mjs +0 -47
- package/runtime/scripts/guards/shell-commands.mjs +0 -328
- package/runtime/scripts/guards/supervisor-seat.mjs +0 -80
- package/runtime/scripts/guards/verdict-process.mjs +0 -81
- package/runtime/scripts/lib/app-data.mjs +0 -11
- package/runtime/scripts/lib/canon-binding-text.mjs +0 -129
- package/runtime/scripts/lib/check-format.mjs +0 -7
- package/runtime/scripts/lib/failure-code-reads.mjs +0 -100
- package/runtime/scripts/lib/failure-code-template-errors.mjs +0 -161
- package/runtime/scripts/lib/git-dir.mjs +0 -120
- package/runtime/scripts/lib/guard-command.mjs +0 -24
- package/runtime/scripts/lib/interrupt-cleanup.mjs +0 -38
- package/runtime/scripts/lib/json-formats.mjs +0 -34
- package/runtime/scripts/lib/managed-json.mjs +0 -51
- package/runtime/scripts/lib/menu-parts.mjs +0 -11
- package/runtime/scripts/lib/op-gate.mjs +0 -15
- package/runtime/scripts/lib/provision-control-failure.mjs +0 -32
- package/runtime/scripts/lib/release-version.mjs +0 -134
- package/runtime/scripts/lib/runtime-location.mjs +0 -21
- package/runtime/scripts/lib/runtime-package-inventory.mjs +0 -21
- package/runtime/scripts/lib/runtime-package-manifests.mjs +0 -84
- package/runtime/scripts/lib/runtime-publication-tags.mjs +0 -56
- package/runtime/scripts/lib/seat-env.mjs +0 -43
- package/runtime/scripts/lib/sonar-new-code.mjs +0 -58
- package/runtime/scripts/lib/source-package-imports.mjs +0 -111
- package/runtime/scripts/lib/tar-files.mjs +0 -39
- package/runtime/scripts/lib/uat-run-folder.mjs +0 -118
- package/runtime/scripts/lib/worktree-record.mjs +0 -14
- package/runtime/scripts/machine/ask-receipts.mjs +0 -113
- package/runtime/scripts/machine/evidence-store.mjs +0 -223
- package/runtime/scripts/machine/home.mjs +0 -216
- package/runtime/scripts/machine/host-lock.mjs +0 -184
- package/runtime/scripts/machine/lock-recovery.mjs +0 -55
- package/runtime/scripts/machine/roles-contract.mjs +0 -90
- package/runtime/scripts/machine/runtime-change.mjs +0 -20
- package/runtime/scripts/machine/workflow-tree.mjs +0 -122
- package/runtime/scripts/reconciler/checklist-items.mjs +0 -7
- package/runtime/scripts/reconciler/guard-row.mjs +0 -25
- package/runtime/scripts/work/brand/brand-colour.mjs +0 -125
- package/runtime/scripts/work/brand/brand-tokens.mjs +0 -167
- package/runtime/scripts/work/brand/brand-value-source.mjs +0 -106
- package/runtime/scripts/work/brand/brand.mjs +0 -892
- package/runtime/scripts/work/draw/draw-layer.mjs +0 -306
- package/runtime/scripts/work/draw/draw-rationale-page.mjs +0 -237
- package/runtime/scripts/work/draw/draw-rationale-ref.mjs +0 -14
- package/runtime/scripts/work/draw/draw-rationale.mjs +0 -353
- package/runtime/scripts/work/draw/draw-source.mjs +0 -412
- package/runtime/scripts/work/draw/draw-taste.mjs +0 -278
- package/runtime/scripts/work/draw/draw-typecheck.mjs +0 -72
- package/runtime/scripts/work/draw/rationale-findings.mjs +0 -205
- package/runtime/scripts/work/draw-grammar.mjs +0 -175
- package/runtime/scripts/work/draw-render-page.mjs +0 -188
- package/runtime/scripts/work/draw-render.mjs +0 -626
- package/runtime/scripts/work/png.mjs +0 -237
- package/runtime/scripts/work/render-tools.mjs +0 -64
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
// rev-list.mjs — `git rev-list <args>`: the commits reachable from one set of refs and not another.
|
|
2
|
-
import { runGit } from './lib.mjs';
|
|
3
|
-
|
|
4
|
-
/** The spawn result {status, stdout, stderr, error} of `git rev-list ...args`; options are runGit's (cwd or dir, config, timeout, maxBuffer, input, encoding, env, git). */
|
|
5
|
-
export const revList = (args = [], options = {}) => runGit(['rev-list', ...args], options);
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
// rev-parse.mjs — the commit-resolution read used by runtime Git consumers.
|
|
2
|
-
import { gitRunner } from './lib.mjs';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Resolve a Git revision to a commit in `cwd`, peeling tags with `^{commit}`.
|
|
6
|
-
* Returns the trimmed commit ID only when Git succeeds with output; unresolved
|
|
7
|
-
* or non-commit revisions, failed reads and empty output return null.
|
|
8
|
-
* `git` may supply a caller-owned runner instead of the native read.
|
|
9
|
-
*/
|
|
10
|
-
export const revParse = (cwd, ref, { git = null } = {}) => {
|
|
11
|
-
const r = gitRunner(git)(['rev-parse', '--verify', '--quiet', `${ref}^{commit}`], { cwd });
|
|
12
|
-
return r.ok && r.stdout ? r.stdout : null;
|
|
13
|
-
};
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
// status-query.mjs — `git status <args>`: what a checkout changed (--porcelain, untracked-files mode, pathspecs).
|
|
2
|
-
import { runGit } from './lib.mjs';
|
|
3
|
-
|
|
4
|
-
/** The spawn result {status, stdout, stderr, error} of `git status ...args`; options are runGit's (cwd or dir, config, timeout, maxBuffer, input, encoding, env, git). */
|
|
5
|
-
export const statusQuery = (args = [], options = {}) => runGit(['status', ...args], options);
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
// worktree-list-query.mjs — `git worktree list <args>`: the registered worktrees of a repository (--porcelain for records).
|
|
2
|
-
import { runGit } from './lib.mjs';
|
|
3
|
-
|
|
4
|
-
/** The spawn result {status, stdout, stderr, error} of `git worktree list ...args`; options are runGit's (cwd or dir, config, timeout, maxBuffer, input, encoding, env, git). */
|
|
5
|
-
export const worktreeListQuery = (args = [], options = {}) => runGit(['worktree', 'list', ...args], options);
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
// spawn-node.mjs — `node <args>` with this process's node binary, started and not waited for: a long-running runtime
|
|
2
|
-
// process (a server, an engine, a detached helper). Returns the ChildProcess.
|
|
3
|
-
import { nodeStart } from './lib.mjs';
|
|
4
|
-
|
|
5
|
-
/** Starts `node <args>`; options (cwd, env, stdio, detached) pass through. */
|
|
6
|
-
export const spawnNode = (args, options = {}) => nodeStart(args, options);
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
// The release owner's npm dist-tag mutation edge; reads remain owned by view.mjs.
|
|
2
|
-
import { npmSpawn } from './lib.mjs';
|
|
3
|
-
|
|
4
|
-
/** Set one declared tag on an already confirmed package version; report incomplete effects as failure. */
|
|
5
|
-
export function distTag(name, version, tag, { cwd, run = npmSpawn } = {}) {
|
|
6
|
-
const result = run(['dist-tag', 'add', `${name}@${version}`, tag], { cwd, timeout: 120_000 });
|
|
7
|
-
return { ok: !result.error && !result.signal && result.status === 0, status: result.status ?? null };
|
|
8
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
// scripts/api/npm/lib.mjs — the runner of npm and npx: the npm-cli.js / npx-cli.js that ship with this node install, run by
|
|
2
|
-
// that node, or the npm / npx executable beside the node binary (no .cmd shim, no shell, no PATH lookup). Utf8 text, a hidden window. The call
|
|
3
|
-
// files beside it (ci.mjs, pack-dry-run.mjs, run-npm.mjs, run-npx.mjs) each name one use; nothing outside scripts/api/npm
|
|
4
|
-
// imports this runner.
|
|
5
|
-
import fs from 'node:fs';
|
|
6
|
-
import path from 'node:path';
|
|
7
|
-
import { spawnSync } from 'node:child_process';
|
|
8
|
-
import { withTempEnv } from '../../../engine/temp-root.mjs';
|
|
9
|
-
import { withoutTestRunner } from '../../lib/env.mjs';
|
|
10
|
-
|
|
11
|
-
/** The `<tool>-cli.js` (npm or npx) of this node install (Windows: beside node; elsewhere: <prefix>/lib/node_modules) run by node, else the `<tool>` executable beside the node binary: an absolute spawn target fixed by the node install, never a PATH lookup; null when the node has neither. */
|
|
12
|
-
const bundledCli = (tool = 'npm', platform = process.platform, execPath = process.execPath, exists = fs.existsSync) => {
|
|
13
|
-
const dir = path.dirname(execPath);
|
|
14
|
-
const cli = path.join(dir, ...(platform === 'win32' ? [] : ['..', 'lib']), 'node_modules', 'npm', 'bin', `${tool}-cli.js`);
|
|
15
|
-
if (exists(cli)) return { file: execPath, args: [cli] };
|
|
16
|
-
const sibling = path.join(dir, tool);
|
|
17
|
-
return platform !== 'win32' && exists(sibling) ? { file: sibling, args: [] } : null;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
const toolSpawn = (tool, args, options) => {
|
|
21
|
-
// npm runs a package's test script, which is node --test: the child never carries an enclosing spec run's mark (scripts/lib/env.mjs withoutTestRunner).
|
|
22
|
-
const spawn = withTempEnv({ encoding: 'utf8', windowsHide: true, maxBuffer: 64 * 1024 * 1024, ...options, env: withoutTestRunner(options.env ?? process.env) });
|
|
23
|
-
const run = bundledCli(tool);
|
|
24
|
-
return run ? spawnSync(run.file, [...run.args, ...args], spawn)
|
|
25
|
-
: { status: null, stdout: '', stderr: '', signal: null, error: Object.assign(new Error(`${tool} was not found beside this node binary`), { code: 'ENOENT' }) };
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
/** `npm <args>`; options pass through last (cwd, timeout, env, maxBuffer, stdio). */
|
|
29
|
-
export const npmSpawn = (args, options = {}) => toolSpawn('npm', args, options);
|
|
30
|
-
|
|
31
|
-
/** `npx <args>`; options pass through last (cwd, timeout, env, maxBuffer, stdio). */
|
|
32
|
-
export const npxSpawn = (args, options = {}) => toolSpawn('npx', args, options);
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
// pack-dry-run.mjs — `npm pack --dry-run --json --ignore-scripts` in a package directory: the file list npm would
|
|
2
|
-
// publish (scripts/gates/canon-digest.mjs packedFiles shapes it).
|
|
3
|
-
import { npmSpawn } from './lib.mjs';
|
|
4
|
-
|
|
5
|
-
/** {status, stdout, stderr, error} of the dry run. npm runs through the runner's absolute node + npm-cli.js target: no shell, no PATH lookup. */
|
|
6
|
-
export function packDryRun(directory, { spawn = npmSpawn } = {}) {
|
|
7
|
-
const r = spawn(['pack', '--dry-run', '--json', '--ignore-scripts'], { cwd: directory });
|
|
8
|
-
return { status: r.status, stdout: r.stdout, stderr: r.stderr, error: r.error ?? null };
|
|
9
|
-
}
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
// run-npm.mjs — `npm <args>` waited for: a package script (`run <script>`, `test`) or an install (`ci`, `install`) whose
|
|
2
|
-
// status and output the caller reads. Returns the spawnSync result {status, stdout, stderr, error, signal} (utf8 text).
|
|
3
|
-
import { npmSpawn } from './lib.mjs';
|
|
4
|
-
|
|
5
|
-
/** Runs `npm <args>`; options (cwd, env, timeout, maxBuffer, stdio) pass through. */
|
|
6
|
-
export const runNpm = (args, options = {}) => npmSpawn(args, options);
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
// view.mjs - `npm view`: what the npm registry says about one package version. The release gate reads it to know whether a
|
|
2
|
-
// version is published and which integrity the registry holds; it never writes.
|
|
3
|
-
import { npmSpawn } from './lib.mjs';
|
|
4
|
-
|
|
5
|
-
/** The JSON `npm view <spec> <field> --json` printed, or null when it printed none. */
|
|
6
|
-
const parsed = (text) => { try { return JSON.parse(String(text ?? '').trim() || 'null'); } catch { return null; } };
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* {state: 'present' | 'absent' | 'unreachable', integrity?, latest?, versions?, distTags?, detail?} of `name@version`. A package the registry does not
|
|
10
|
-
* know (E404) or a version it does not list is `absent` (with the registry's latest when it has one); any other failure
|
|
11
|
-
* (network, auth, a broken npm) is `unreachable`, never a guess. `run` is the spawn seam of a spec.
|
|
12
|
-
*/
|
|
13
|
-
export function view(name, version, { cwd, run = npmSpawn } = {}) {
|
|
14
|
-
const listed = run(['view', name, 'versions', 'dist-tags', '--json'], { cwd, timeout: 120_000 });
|
|
15
|
-
const err = `${listed.stderr ?? ''}${listed.error?.message ?? ''}`;
|
|
16
|
-
if (listed.error || listed.signal || listed.status === null) return { state: 'unreachable', detail: err.trim().slice(0, 200) || 'npm did not run' };
|
|
17
|
-
if (listed.status !== 0) {
|
|
18
|
-
return /\bE404\b|404 Not Found|code E404/.test(err) ? { state: 'absent', latest: '-', versions: [], distTags: {} } : { state: 'unreachable', detail: err.trim().split(/\r?\n/)[0].slice(0, 200) };
|
|
19
|
-
}
|
|
20
|
-
const doc = parsed(listed.stdout);
|
|
21
|
-
const versions = [doc?.versions].flat();
|
|
22
|
-
const distTags = doc?.['dist-tags'];
|
|
23
|
-
if (!versions.length || versions.some((entry) => typeof entry !== 'string') || !distTags || Array.isArray(distTags) || typeof distTags !== 'object' || Object.values(distTags).some((entry) => typeof entry !== 'string'))
|
|
24
|
-
return { state: 'unreachable', detail: 'the registry returned an unreadable version or dist-tag inventory' };
|
|
25
|
-
const inventory = { versions, distTags, latest: distTags.latest ?? '-' };
|
|
26
|
-
if (!versions.includes(version)) return { state: 'absent', ...inventory };
|
|
27
|
-
const dist = run(['view', `${name}@${version}`, 'dist.integrity', '--json'], { cwd, timeout: 120_000 });
|
|
28
|
-
const integrity = !dist.error && !dist.signal && dist.status === 0 ? parsed(dist.stdout) : null;
|
|
29
|
-
return typeof integrity === 'string' ? { state: 'present', integrity, ...inventory } : { state: 'unreachable', detail: 'the registry lists the version but gave no integrity' };
|
|
30
|
-
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
// whoami.mjs - `npm whoami`: the npm account logged in on this host (release-publish.mjs refuses a publish from another one).
|
|
2
|
-
import { npmSpawn } from './lib.mjs';
|
|
3
|
-
|
|
4
|
-
/** The npm account logged in on this host, or null. `run` is the spawn seam of a spec. */
|
|
5
|
-
export function whoami({ cwd, run = npmSpawn } = {}) {
|
|
6
|
-
const r = run(['whoami'], { cwd, timeout: 60_000 });
|
|
7
|
-
return !r.error && !r.signal && r.status === 0 ? String(r.stdout ?? '').trim() || null : null;
|
|
8
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
// hide-child-windows.mjs — the child-process defaults for background Windows entry points.
|
|
2
|
-
import cp from 'node:child_process';
|
|
3
|
-
import { syncBuiltinESMExports } from 'node:module';
|
|
4
|
-
import { withWindowsHide } from './lib.mjs';
|
|
5
|
-
|
|
6
|
-
const MARK = Symbol.for('starci.hideChildWindows');
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Install process-wide hidden-window defaults for Windows child-process calls.
|
|
10
|
-
* Explicit `windowsHide` options survive, and builtin ESM exports are synced
|
|
11
|
-
* so their named imports use the wrappers. Importing this module installs the
|
|
12
|
-
* patch; detached entry points load it before spawning to avoid visible consoles.
|
|
13
|
-
* Returns true on the first Windows installation, false elsewhere or once patched.
|
|
14
|
-
*/
|
|
15
|
-
export function hideChildWindows({ platform = process.platform } = {}) {
|
|
16
|
-
if (platform !== 'win32' || cp[MARK]) return false;
|
|
17
|
-
for (const [name, hasArgv] of [['spawn', true], ['spawnSync', true], ['execFile', true], ['execFileSync', true], ['exec', false], ['execSync', false]]) {
|
|
18
|
-
const original = cp[name];
|
|
19
|
-
const wrapped = function (...list) { return original.apply(this, withWindowsHide(list, hasArgv)); };
|
|
20
|
-
for (const key of Reflect.ownKeys(original)) {
|
|
21
|
-
if (key === 'length' || key === 'name' || key === 'prototype') continue;
|
|
22
|
-
try { Object.defineProperty(wrapped, key, Object.getOwnPropertyDescriptor(original, key)); } catch { /* non-configurable */ }
|
|
23
|
-
}
|
|
24
|
-
cp[name] = wrapped;
|
|
25
|
-
}
|
|
26
|
-
cp[MARK] = true;
|
|
27
|
-
syncBuiltinESMExports();
|
|
28
|
-
return true;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
hideChildWindows();
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
// kill-tree.mjs — the host API for a forced Windows process-tree stop.
|
|
2
|
-
import { spawnSync } from 'node:child_process';
|
|
3
|
-
|
|
4
|
-
const KILL_TREE_COMMAND = 'taskkill.exe';
|
|
5
|
-
const killTreeArgs = (pid) => ['/F', '/T', '/PID', String(pid)];
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Force-stop a caller-owned PID and its descendants with Windows taskkill.
|
|
9
|
-
* The caller proves ownership and validates the PID; this API checks neither.
|
|
10
|
-
* Non-Windows calls stop nothing and return `{ok:false,status:null,output}`.
|
|
11
|
-
* Returns `{ok,status,output}` with bounded diagnostic text; spawn failures become
|
|
12
|
-
* failure results. `ok` reflects taskkill's exit status without rechecking liveness.
|
|
13
|
-
*/
|
|
14
|
-
export function killTree(pid, { timeoutMs = 60_000, run = spawnSync, platform = process.platform } = {}) {
|
|
15
|
-
if (platform !== 'win32') return { ok: false, status: null, output: 'not windows' };
|
|
16
|
-
try {
|
|
17
|
-
const r = run(KILL_TREE_COMMAND, killTreeArgs(pid), { encoding: 'utf8', windowsHide: true, timeout: timeoutMs });
|
|
18
|
-
return { ok: r?.status === 0, status: r?.status ?? null, output: String(r?.stdout || r?.stderr || r?.error?.message || '').trim().slice(0, 300) };
|
|
19
|
-
} catch (error) {
|
|
20
|
-
return { ok: false, status: null, output: String(error?.message ?? error).slice(0, 300) };
|
|
21
|
-
}
|
|
22
|
-
}
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
// scripts/api/process/lib.mjs — what the process call files beside it share: the spawn options every child of the
|
|
2
|
-
// runtime carries. The call files (process-list.mjs, kill-tree.mjs, run-shell.mjs, spawn-detached.mjs, set-priority.mjs,
|
|
3
|
-
// hide-child-windows.mjs, ...) each name one use of the host's process table or a child process.
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* `list` with windowsHide: true added to its options argument. `hasArgv`:
|
|
7
|
-
* the call's second argument may be an argv array (spawn, execFile) rather
|
|
8
|
-
* than options (exec). An explicit windowsHide (true or false) is kept.
|
|
9
|
-
*/
|
|
10
|
-
export function withWindowsHide(list, hasArgv) {
|
|
11
|
-
const out = [...list];
|
|
12
|
-
let slot = 1;
|
|
13
|
-
if (hasArgv && (Array.isArray(out[1]) || (out[1] == null && out.length > 2))) slot = 2;
|
|
14
|
-
const options = out[slot];
|
|
15
|
-
if (options == null) out.splice(slot, out.length > slot ? 1 : 0, { windowsHide: true });
|
|
16
|
-
else if (typeof options === 'function') out.splice(slot, 0, { windowsHide: true });
|
|
17
|
-
else if (typeof options === 'object' && !('windowsHide' in options)) out[slot] = { ...options, windowsHide: true };
|
|
18
|
-
return out;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// ---- the host process table (process-list.mjs, process-list-async.mjs) ------------------------------------------
|
|
22
|
-
|
|
23
|
-
const psQuote = (s) => String(s).replaceAll("'", "''");
|
|
24
|
-
|
|
25
|
-
/** The PowerShell script of one process-table read: Win32_Process rows (optionally filtered), with CPU when asked. */
|
|
26
|
-
export function processListScript({ where = null, cmdMax = 4000, cpu = false } = {}) {
|
|
27
|
-
const filter = where ? ` -Filter '${psQuote(where)}'` : '';
|
|
28
|
-
return [
|
|
29
|
-
...(cpu ? ['$perf = @{}', 'Get-CimInstance Win32_PerfFormattedData_PerfProc_Process | ForEach-Object { if ($_.IDProcess -gt 0) { $perf[[int]$_.IDProcess] = [double]$_.PercentProcessorTime } }'] : []),
|
|
30
|
-
`Get-CimInstance Win32_Process${filter} | ForEach-Object { $c = [string]$_.CommandLine; [pscustomobject]@{ pid = [int]$_.ProcessId; ppid = [int]$_.ParentProcessId; name = [string]$_.Name; exe = [string]$_.ExecutablePath;`,
|
|
31
|
-
` cmd = $c.Substring(0, [Math]::Min(${Number(cmdMax) || 4000}, $c.Length)); ws = [int64]$_.WorkingSetSize;`,
|
|
32
|
-
` created = $(if ($_.CreationDate) { ([DateTimeOffset]$_.CreationDate).ToUnixTimeMilliseconds() } else { 0 })${cpu ? '; cpu = $perf[[int]$_.ProcessId]' : ''} } } | ConvertTo-Json -Compress`,
|
|
33
|
-
].join('\n');
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** The rows of the PowerShell read's JSON (one object or an array). */
|
|
37
|
-
export const processRowsOfJson = (text) => {
|
|
38
|
-
const t = String(text ?? '').trim();
|
|
39
|
-
const v = t ? JSON.parse(t) : [];
|
|
40
|
-
return (Array.isArray(v) ? v : [v]).map((p) => ({ ...p, pid: Number(p.pid), ppid: Number(p.ppid), cmd: String(p.cmd ?? '') }));
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
/** The rows of `ps -eo pid=,ppid=,comm=,args=` (exe/created/ws are null there). */
|
|
44
|
-
const PS_ROW_PARTS = [String.raw`^\s*`, String.raw`(\d+)`, String.raw`\s+`, String.raw`(\d+)`, String.raw`\s+`, String.raw`(\S+)`, String.raw`\s+`, '(.*)', '$'];
|
|
45
|
-
const PS_ROW = new RegExp(PS_ROW_PARTS.join(''));
|
|
46
|
-
export const processRowsOfPs = (text, cmdMax = 4000) => String(text ?? '').split(/\r?\n/).map((line) => PS_ROW.exec(line)).filter(Boolean)
|
|
47
|
-
.map((m) => ({ pid: Number(m[1]), ppid: Number(m[2]), name: m[3].split('/').pop(), exe: null, cmd: m[4].slice(0, cmdMax), created: null, ws: null }));
|
|
48
|
-
|
|
49
|
-
// ---- the environment of other processes (process-env.mjs) -------------------------------------------------------
|
|
50
|
-
|
|
51
|
-
// Windows keeps a process's environment block in its PEB: PEB.ProcessParameters (+0x20) -> Environment (+0x80) and
|
|
52
|
-
// EnvironmentSize (+0x3F0), x64 only. A same-user process opens with PROCESS_QUERY_INFORMATION | PROCESS_VM_READ (0x0410).
|
|
53
|
-
export const PROCESS_ENV_NATIVE = String.raw`
|
|
54
|
-
using System;
|
|
55
|
-
using System.Collections.Generic;
|
|
56
|
-
using System.Runtime.InteropServices;
|
|
57
|
-
using System.Text;
|
|
58
|
-
public static class StarciProcessEnv {
|
|
59
|
-
[StructLayout(LayoutKind.Sequential)] struct PBI { public IntPtr Reserved1; public IntPtr PebBaseAddress; public IntPtr Reserved2a; public IntPtr Reserved2b; public IntPtr UniqueProcessId; public IntPtr Reserved3; }
|
|
60
|
-
[DllImport("ntdll.dll")] static extern int NtQueryInformationProcess(IntPtr h, int cls, ref PBI pbi, int len, out int ret);
|
|
61
|
-
[DllImport("kernel32.dll")] static extern bool ReadProcessMemory(IntPtr h, IntPtr addr, byte[] buf, IntPtr size, out IntPtr read);
|
|
62
|
-
[DllImport("kernel32.dll")] static extern IntPtr OpenProcess(int access, bool inherit, int pid);
|
|
63
|
-
[DllImport("kernel32.dll")] static extern bool CloseHandle(IntPtr h);
|
|
64
|
-
[DllImport("kernel32.dll")] static extern bool IsWow64Process(IntPtr h, out bool wow);
|
|
65
|
-
static byte[] Read(IntPtr h, IntPtr addr, int size) {
|
|
66
|
-
var buf = new byte[size]; IntPtr n;
|
|
67
|
-
if (!ReadProcessMemory(h, addr, buf, (IntPtr)size, out n) || (int)n != size) return null;
|
|
68
|
-
return buf;
|
|
69
|
-
}
|
|
70
|
-
public static Dictionary<string, string> Env(int pid) {
|
|
71
|
-
var h = OpenProcess(0x0410, false, pid);
|
|
72
|
-
if (h == IntPtr.Zero) return null;
|
|
73
|
-
try { return EnvHandle(h); } finally { CloseHandle(h); }
|
|
74
|
-
}
|
|
75
|
-
public static Dictionary<string, string> EnvHandle(IntPtr h) {
|
|
76
|
-
bool wow; if (IsWow64Process(h, out wow) && wow) return null;
|
|
77
|
-
var pbi = new PBI(); int ret;
|
|
78
|
-
if (NtQueryInformationProcess(h, 0, ref pbi, Marshal.SizeOf(pbi), out ret) != 0) return null;
|
|
79
|
-
var peb = Read(h, pbi.PebBaseAddress + 0x20, 8); if (peb == null) return null;
|
|
80
|
-
var parameters = (IntPtr)BitConverter.ToInt64(peb, 0);
|
|
81
|
-
var envPtr = Read(h, parameters + 0x80, 8); if (envPtr == null) return null;
|
|
82
|
-
var sizeBuf = Read(h, parameters + 0x3F0, 8); if (sizeBuf == null) return null;
|
|
83
|
-
long size = BitConverter.ToInt64(sizeBuf, 0);
|
|
84
|
-
if (size <= 0 || size > 4 * 1024 * 1024) return null;
|
|
85
|
-
var block = Read(h, (IntPtr)BitConverter.ToInt64(envPtr, 0), (int)size); if (block == null) return null;
|
|
86
|
-
var d = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
87
|
-
foreach (var entry in Encoding.Unicode.GetString(block).Split('\0')) {
|
|
88
|
-
if (entry.Length == 0) continue;
|
|
89
|
-
int i = entry.IndexOf('=', 1); if (i < 0) continue;
|
|
90
|
-
d[entry.Substring(0, i)] = entry.Substring(i + 1);
|
|
91
|
-
}
|
|
92
|
-
return d;
|
|
93
|
-
}
|
|
94
|
-
}`;
|
|
95
|
-
|
|
96
|
-
/** The PowerShell script reading `names` from the environment of `pids` (every process when null): JSON [{pid, readable, values}]. */
|
|
97
|
-
export function processEnvScript({ names, pids = null }) {
|
|
98
|
-
const wanted = JSON.stringify([...names].map(String));
|
|
99
|
-
const ids = pids ? `@(${[...pids].map(Number).filter(Number.isInteger).join(',')})` : '(Get-Process).Id';
|
|
100
|
-
return [
|
|
101
|
-
`Add-Type -TypeDefinition @'${PROCESS_ENV_NATIVE}\n'@ -Language CSharp`,
|
|
102
|
-
`$names = '${psQuote(wanted)}' | ConvertFrom-Json`,
|
|
103
|
-
`$rows = foreach ($id in ${ids}) { $e = [StarciProcessEnv]::Env([int]$id); $v = [ordered]@{}; foreach ($n in $names) { $v[$n] = if ($null -ne $e -and $e.ContainsKey($n)) { $e[$n] } else { $null } }; [pscustomobject]@{ pid = [int]$id; readable = ($null -ne $e); values = $v } }`,
|
|
104
|
-
'@($rows) | ConvertTo-Json -Compress -Depth 4',
|
|
105
|
-
].join('\n');
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/** The rows of that script's JSON. */
|
|
109
|
-
export const processEnvRowsOfJson = (text) => {
|
|
110
|
-
const t = String(text ?? '').trim();
|
|
111
|
-
const v = t ? JSON.parse(t) : [];
|
|
112
|
-
return (Array.isArray(v) ? v : [v]).map((r) => ({ pid: Number(r.pid), readable: r.readable === true, values: r.values ?? {} }));
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
/** The spawnSync-shaped result of a system tool the host does not carry (status null + error), for a caller that returns a spawn result. `refusal`: a systemTool refusal. */
|
|
116
|
-
export const toolUnavailableResult = (refusal) => ({ status: null, stdout: '', stderr: '', error: Object.assign(new Error(`${refusal.tool} is not in its fixed system location on ${refusal.platform}`), { code: refusal.code }) });
|
|
117
|
-
|
|
118
|
-
/** Promise<string | null>: stdout of one system tool run without blocking the calling thread (hidden window, 1 MiB), or null when the tool is absent, failed or timed out. `tool`: systemTool; `exec`: execFile. */
|
|
119
|
-
export const execSystemTool = (name, args, timeout, { exec, tool }) => new Promise((resolve) => {
|
|
120
|
-
try {
|
|
121
|
-
const found = tool(name);
|
|
122
|
-
if (!found.ok) { resolve(null); return; }
|
|
123
|
-
exec(found.path, args, { timeout, windowsHide: true, maxBuffer: 1 << 20 }, (error, stdout) => resolve(error ? null : String(stdout)));
|
|
124
|
-
} catch { resolve(null); }
|
|
125
|
-
});
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
// probe-guard-command.mjs — run a registered guard hook command the way an agent host runs it, and say whether it resolves.
|
|
2
|
-
//
|
|
3
|
-
// The host runs a hook command through a shell and treats a failure to start it as non-blocking, so an unresolvable
|
|
4
|
-
// guard command leaves every guarded command unguarded without any error the seat acts on. The probe runs the exact
|
|
5
|
-
// command text, with the empty request `{}` the guard allows, once through each shell the host family uses: bash (Git Bash
|
|
6
|
-
// on Windows, the shell Claude Code runs hooks in) and cmd on Windows, sh on POSIX. The environment is the caller's without
|
|
7
|
-
// the seat identity and without the per-user launcher directory on PATH, so only the command's own spelling can resolve.
|
|
8
|
-
// A shell the machine does not carry is skipped with its reason; the probe is ok when every shell that ran exited 0.
|
|
9
|
-
import fs from 'node:fs';
|
|
10
|
-
import os from 'node:os';
|
|
11
|
-
import path from 'node:path';
|
|
12
|
-
import { readEnv } from '../../lib/env.mjs';
|
|
13
|
-
import { withoutSeatEnv } from '../../lib/seat-env.mjs';
|
|
14
|
-
import { resolveRealTool } from './resolve-real-tool.mjs';
|
|
15
|
-
import { runProgram } from './run-program.mjs';
|
|
16
|
-
import { systemTool } from './system-tool.mjs';
|
|
17
|
-
|
|
18
|
-
const PROBE_REQUEST = '{}';
|
|
19
|
-
const PROBE_TIMEOUT_MS = 30_000;
|
|
20
|
-
const DETAIL_CHARS = 200;
|
|
21
|
-
|
|
22
|
-
/** `env` without the per-user launcher directory on its PATH entry. */
|
|
23
|
-
function withoutLauncherDir(env, { home, platform }) {
|
|
24
|
-
const key = Object.keys(env).find((name) => name.toLowerCase() === 'path');
|
|
25
|
-
if (!key) return env;
|
|
26
|
-
const comparable = (entry) => (platform === 'win32' ? path.resolve(entry).toLowerCase() : path.resolve(entry));
|
|
27
|
-
const launcher = comparable(path.join(home, '.starci', 'bin'));
|
|
28
|
-
const kept = String(env[key]).split(platform === 'win32' ? ';' : ':').filter((entry) => entry && comparable(entry.replace(/^"(.*)"$/, '$1')) !== launcher);
|
|
29
|
-
return { ...env, [key]: kept.join(platform === 'win32' ? ';' : ':') };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** The Git Bash a Windows seat runs hooks in: Claude Code's own override, else the `bin/bash.exe` of the Git installation `git` resolves to; null when none. */
|
|
33
|
-
function gitBashOf({ env, home, platform, exists }) {
|
|
34
|
-
const named = readEnv('CLAUDE_CODE_GIT_BASH_PATH', env);
|
|
35
|
-
if (named) return exists(named) ? named : null;
|
|
36
|
-
const git = resolveRealTool('git', { env, home, platform });
|
|
37
|
-
if (!git) return null;
|
|
38
|
-
const root = path.dirname(path.dirname(git));
|
|
39
|
-
return [root, path.dirname(root)].map((dir) => path.join(dir, 'bin', 'bash.exe')).find((file) => exists(file)) ?? null;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** The shells this host family runs hooks in: [{shell, file (null when absent), argv(command), verbatim}]. */
|
|
43
|
-
function shellsOf({ env, home, platform, exists }) {
|
|
44
|
-
const toolPath = (name) => { const found = systemTool(name, { platform, env, exists }); return found.ok ? found.path : null; };
|
|
45
|
-
if (platform !== 'win32') return [{ shell: 'sh', file: toolPath('sh'), argv: (command) => ['-c', command], verbatim: false }];
|
|
46
|
-
return [
|
|
47
|
-
{ shell: 'cmd', file: toolPath('cmd'), argv: (command) => ['/d', '/s', '/c', `"${command}"`], verbatim: true },
|
|
48
|
-
{ shell: 'bash', file: gitBashOf({ env, home, platform, exists }), argv: (command) => ['-c', command], verbatim: false },
|
|
49
|
-
];
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const detailOf = (result) => {
|
|
53
|
-
const text = String(result.stderr || result.stdout || result.error?.message || '').trim().replace(/\s+/g, ' ');
|
|
54
|
-
return text.slice(0, DETAIL_CHARS);
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Run `command` through every shell of the platform. Returns {ok, reason, shells: [{shell, ok, skipped?, status?, detail?}]};
|
|
59
|
-
* `reason` is null when ok, else one line naming each failing shell with its exit status and output.
|
|
60
|
-
* Seams: `exists` (file probe), `run` (the spawn).
|
|
61
|
-
*/
|
|
62
|
-
export function probeGuardCommand({ command, home = os.homedir(), platform = process.platform, env = process.env, exists = fs.existsSync, run = runProgram } = {}) {
|
|
63
|
-
const probeEnv = withoutLauncherDir(withoutSeatEnv(env), { home, platform });
|
|
64
|
-
const shells = shellsOf({ env, home, platform, exists }).map((spec) => {
|
|
65
|
-
if (!spec.file) return { shell: spec.shell, ok: true, skipped: `${spec.shell} is not installed on this machine` };
|
|
66
|
-
const result = run(spec.file, spec.argv(command), { input: PROBE_REQUEST, env: probeEnv, timeout: PROBE_TIMEOUT_MS, ...(spec.verbatim ? { windowsVerbatimArguments: true } : {}) });
|
|
67
|
-
return { shell: spec.shell, ok: result.status === 0, status: result.status, detail: detailOf(result) };
|
|
68
|
-
});
|
|
69
|
-
const ran = shells.filter((entry) => !entry.skipped);
|
|
70
|
-
const failed = ran.filter((entry) => !entry.ok);
|
|
71
|
-
const ok = ran.length > 0 && failed.length === 0;
|
|
72
|
-
const reason = ok ? null : (failed.map((entry) => `${entry.shell} exit ${entry.status}: ${entry.detail}`).join('; ') || 'no shell on this machine can run the guard command');
|
|
73
|
-
return { ok, reason, shells };
|
|
74
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
// process-list.mjs — the synchronous host process-table read.
|
|
2
|
-
import { spawnSync } from 'node:child_process';
|
|
3
|
-
import { processListScript, processRowsOfJson, processRowsOfPs } from './lib.mjs';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Synchronously read the host process table for caller-owned filtering.
|
|
7
|
-
* `where` is a Windows WQL filter; `cpu` requests Windows per-core CPU values.
|
|
8
|
-
* Other platforms use ps, with `exe`, `created` and `ws` left null. `cmdMax`
|
|
9
|
-
* truncates command lines before the optional `match` RegExp filters rows.
|
|
10
|
-
* Returns process rows, including an empty list, or null on query or parsing
|
|
11
|
-
* failure. A caller must treat null as an unreadable table rather than no processes.
|
|
12
|
-
*/
|
|
13
|
-
export function processList({ where = null, match = null, cmdMax = 4000, cpu = false, run = spawnSync, platform = process.platform, timeoutMs = 120_000 } = {}) {
|
|
14
|
-
try {
|
|
15
|
-
let rows;
|
|
16
|
-
if (platform === 'win32') {
|
|
17
|
-
const r = run('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', processListScript({ where, cmdMax, cpu })],
|
|
18
|
-
{ encoding: 'utf8', windowsHide: true, timeout: timeoutMs, maxBuffer: 256 * 1024 * 1024 });
|
|
19
|
-
if (!r || r.error || r.status !== 0) return null;
|
|
20
|
-
rows = processRowsOfJson(r.stdout);
|
|
21
|
-
} else {
|
|
22
|
-
const r = run('ps', ['-eo', 'pid=,ppid=,comm=,args='], { encoding: 'utf8', timeout: Math.min(timeoutMs, 30_000) });
|
|
23
|
-
if (!r || r.error || r.status !== 0) return null;
|
|
24
|
-
rows = processRowsOfPs(r.stdout, cmdMax);
|
|
25
|
-
}
|
|
26
|
-
return match ? rows.filter((p) => match.test(p.cmd)) : rows;
|
|
27
|
-
} catch { return null; }
|
|
28
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
// system-tool.mjs — the absolute path of one OS system tool, taken from FIXED directories no ordinary user can write:
|
|
2
|
-
// Windows <SystemRoot>\System32 (and the NVIDIA driver's own folders), POSIX /usr/bin, /bin, /usr/sbin, /sbin. A tool is
|
|
3
|
-
// never looked up through PATH, so a writable directory early on PATH cannot stand in for it. The process call files
|
|
4
|
-
// spawn the returned path; a tool the host does not carry is a typed refusal, which each caller turns into its own
|
|
5
|
-
// degraded answer (null, "unknown"), never a crash.
|
|
6
|
-
import fs from 'node:fs';
|
|
7
|
-
import path from 'node:path';
|
|
8
|
-
import { readEnv } from '../../lib/env.mjs';
|
|
9
|
-
|
|
10
|
-
const DEFAULT_SYSTEM_ROOT = ['C:', 'Windows'].join(path.win32.sep);
|
|
11
|
-
const POSIX_DIRS = ['/usr/bin', '/bin', '/usr/sbin', '/sbin'];
|
|
12
|
-
// Windows: path segments under <SystemRoot>\System32 (nvidia-smi also under <drive>\Program Files\NVIDIA Corporation\NVSMI, its legacy home).
|
|
13
|
-
const WINDOWS_TOOLS = {
|
|
14
|
-
cmd: [['cmd.exe']], powershell: [['WindowsPowerShell', 'v1.0', 'powershell.exe']], schtasks: [['schtasks.exe']], netstat: [['NETSTAT.EXE']], tasklist: [['tasklist.exe']],
|
|
15
|
-
'nvidia-smi': [['nvidia-smi.exe'], ['..', '..', 'Program Files', 'NVIDIA Corporation', 'NVSMI', 'nvidia-smi.exe']],
|
|
16
|
-
};
|
|
17
|
-
const POSIX_TOOLS = { sh: 'sh', lsof: 'lsof', ps: 'ps', 'nvidia-smi': 'nvidia-smi' };
|
|
18
|
-
|
|
19
|
-
const refusal = (tool, platform) => ({ ok: false, code: 'SYSTEM_TOOL_UNAVAILABLE', tool, platform });
|
|
20
|
-
|
|
21
|
-
/** {ok: true, path} (absolute, existing) or the typed refusal {ok: false, code: 'SYSTEM_TOOL_UNAVAILABLE', tool, platform}. `exists` is the file probe. */
|
|
22
|
-
export function systemTool(tool, { platform = process.platform, env = process.env, exists = (file) => fs.existsSync(file) } = {}) {
|
|
23
|
-
if (platform === 'win32') {
|
|
24
|
-
const root = readEnv('SystemRoot', env) ?? readEnv('SYSTEMROOT', env) ?? readEnv('windir', env) ?? readEnv('WINDIR', env) ?? DEFAULT_SYSTEM_ROOT;
|
|
25
|
-
const system32 = path.win32.join(root, 'System32');
|
|
26
|
-
for (const rel of Object.hasOwn(WINDOWS_TOOLS, tool) ? WINDOWS_TOOLS[tool] : []) {
|
|
27
|
-
const file = path.win32.join(system32, ...rel);
|
|
28
|
-
if (exists(file)) return { ok: true, path: file };
|
|
29
|
-
}
|
|
30
|
-
return refusal(tool, platform);
|
|
31
|
-
}
|
|
32
|
-
for (const dir of Object.hasOwn(POSIX_TOOLS, tool) ? POSIX_DIRS : []) {
|
|
33
|
-
const file = path.posix.join(dir, POSIX_TOOLS[tool]);
|
|
34
|
-
if (exists(file)) return { ok: true, path: file };
|
|
35
|
-
}
|
|
36
|
-
return refusal(tool, platform);
|
|
37
|
-
}
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
// canon-digest.mjs — the one content digest of a lint canon package (modules/models/code-patterns.yaml `canon.contentDigest`).
|
|
2
|
-
// The policy names the algorithm, the include/exclude globs and the framing; the only framing is
|
|
3
|
-
// `sorted-posix-relative-path-null-raw-bytes-null`: every selected file, sorted by its posix relative path (ICU 'en' collation),
|
|
4
|
-
// contributes its path as UTF-8, one NUL byte, its raw bytes, one NUL byte. An unknown algorithm or framing, a malformed
|
|
5
|
-
// policy and a symlink in the file set are refused with a typed code, never hashed around.
|
|
6
|
-
//
|
|
7
|
-
// The file set is either the canon's PUBLISHED set (`packedFiles`: the npm pack list of its source directory, what the
|
|
8
|
-
// registry tarball carries) or an installed copy on disk (`installedFiles`: every regular file under the package root,
|
|
9
|
-
// node_modules excluded). Both feed `canonContentDigest`, so the runtime check and the product gate hash one way.
|
|
10
|
-
import crypto from 'node:crypto';
|
|
11
|
-
import fs from 'node:fs';
|
|
12
|
-
import path from 'node:path';
|
|
13
|
-
import { packDryRun } from '../api/npm/pack-dry-run.mjs';
|
|
14
|
-
import { braceVariants, globExpression } from '../lib/glob.mjs';
|
|
15
|
-
import { posixPath } from '../lib/path-key.mjs';
|
|
16
|
-
import { stringList } from '../lib/list.mjs';
|
|
17
|
-
|
|
18
|
-
const CANON_DIGEST_ALGORITHMS = Object.freeze(['sha256']);
|
|
19
|
-
const CANON_DIGEST_FRAMINGS = Object.freeze(['sorted-posix-relative-path-null-raw-bytes-null']);
|
|
20
|
-
|
|
21
|
-
// The framing's order is the ICU collation of the 'en' locale (what String#localeCompare gives on a default Node host), fixed
|
|
22
|
-
// here so the order never follows the host's locale; the bound profile values are computed in it.
|
|
23
|
-
const ORDER = new Intl.Collator('en', { usage: 'sort', sensitivity: 'variant' });
|
|
24
|
-
const fail = (code, message) => Object.assign(new Error(message), { code });
|
|
25
|
-
const matchAny = (file, globs) => globs.flatMap(braceVariants).some((pattern) => globExpression(pattern).test(file));
|
|
26
|
-
const globList = (value) => stringList(value, { blank: false });
|
|
27
|
-
|
|
28
|
-
/** The policy itself, or a typed refusal: CANON_DIGEST_ALGORITHM_UNKNOWN, CANON_DIGEST_FRAMING_UNKNOWN, CANON_DIGEST_POLICY_INVALID. */
|
|
29
|
-
function assertDigestPolicy(policy) {
|
|
30
|
-
if (!policy || typeof policy !== 'object' || Array.isArray(policy)) throw fail('CANON_DIGEST_POLICY_INVALID', 'The canon content digest policy is not an object.');
|
|
31
|
-
if (!CANON_DIGEST_ALGORITHMS.includes(policy.algorithm)) throw fail('CANON_DIGEST_ALGORITHM_UNKNOWN', `The canon digest algorithm ${JSON.stringify(policy.algorithm)} is not one of ${CANON_DIGEST_ALGORITHMS.join(', ')}.`);
|
|
32
|
-
if (!CANON_DIGEST_FRAMINGS.includes(policy.framing)) throw fail('CANON_DIGEST_FRAMING_UNKNOWN', `The canon digest framing ${JSON.stringify(policy.framing)} is not one of ${CANON_DIGEST_FRAMINGS.join(', ')}.`);
|
|
33
|
-
if (!globList(policy.include) || !policy.include.length || !globList(policy.exclude)) throw fail('CANON_DIGEST_POLICY_INVALID', 'The canon digest policy needs a non-empty include glob list and an exclude glob list.');
|
|
34
|
-
return policy;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Whether posix relative `file` is selected by the policy's include and exclude globs. */
|
|
38
|
-
const selectedByPolicy = (file, policy) => matchAny(file, policy.include) && !matchAny(file, policy.exclude);
|
|
39
|
-
|
|
40
|
-
/** The PUBLISHED file list of the package whose source is `directory`: `npm pack --dry-run --json --ignore-scripts`. */
|
|
41
|
-
export function packedFiles(directory) {
|
|
42
|
-
const run = packDryRun(directory);
|
|
43
|
-
let listed;
|
|
44
|
-
try { listed = JSON.parse(run.stdout)?.[0]?.files; } catch { listed = null; }
|
|
45
|
-
if (run.status !== 0 || !Array.isArray(listed)) throw fail('CANON_PACK_UNAVAILABLE', `npm pack --dry-run could not list ${directory}: ${String(run.stderr ?? run.error?.message ?? '').trim().split(/\r?\n/).at(-1) ?? ''}`);
|
|
46
|
-
return listed.map((entry) => posixPath(entry.path));
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** Every regular file of an installed package at `root` (node_modules skipped); a symlink is refused (CANON_PACKAGE_SYMLINK). */
|
|
50
|
-
export function installedFiles(root) {
|
|
51
|
-
const files = [];
|
|
52
|
-
const visit = (directory) => {
|
|
53
|
-
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
54
|
-
if (entry.name === 'node_modules') continue;
|
|
55
|
-
const absolute = path.join(directory, entry.name), relative = posixPath(path.relative(root, absolute)), stat = fs.lstatSync(absolute);
|
|
56
|
-
if (stat.isSymbolicLink()) throw fail('CANON_PACKAGE_SYMLINK', `The canon package holds the symlink ${relative}; a canon is hashed from regular files only.`);
|
|
57
|
-
if (stat.isDirectory()) visit(absolute);
|
|
58
|
-
else if (stat.isFile()) files.push(relative);
|
|
59
|
-
}
|
|
60
|
-
};
|
|
61
|
-
visit(root);
|
|
62
|
-
return files;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** {value, files} of the files of `root` the policy selects out of `files` (posix paths relative to `root`). */
|
|
66
|
-
export function canonContentDigest(root, policy, files) {
|
|
67
|
-
assertDigestPolicy(policy);
|
|
68
|
-
const selected = [...new Set(files.map(posixPath))].filter((file) => selectedByPolicy(file, policy)).sort(ORDER.compare);
|
|
69
|
-
const hash = crypto.createHash(policy.algorithm), nul = Buffer.from([0]);
|
|
70
|
-
for (const file of selected) {
|
|
71
|
-
const absolute = path.join(root, file), segments = file.split('/');
|
|
72
|
-
let stat;
|
|
73
|
-
for (let depth = 1; depth <= segments.length; depth += 1) {
|
|
74
|
-
try { stat = fs.lstatSync(path.join(root, ...segments.slice(0, depth))); } catch { throw fail('CANON_PACKAGE_UNREADABLE', `The canon file ${file} is missing.`); }
|
|
75
|
-
if (stat.isSymbolicLink()) throw fail('CANON_PACKAGE_SYMLINK', `The canon file ${file} is reached through the symlink ${segments.slice(0, depth).join('/')}; a canon is hashed from regular files only.`);
|
|
76
|
-
}
|
|
77
|
-
if (!stat.isFile()) throw fail('CANON_PACKAGE_UNREADABLE', `The canon file ${file} is not a regular file.`);
|
|
78
|
-
hash.update(Buffer.from(file, 'utf8')); hash.update(nul); hash.update(fs.readFileSync(absolute)); hash.update(nul);
|
|
79
|
-
}
|
|
80
|
-
return { value: hash.digest('hex'), files: selected.length };
|
|
81
|
-
}
|