@orkestrel/scaffold 0.0.77 → 0.0.78
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/skills/orkestrel-dispatch/scripts/bench.js +204 -0
- package/dist/agents/skills/orkestrel-dispatch/scripts/brief.js +102 -0
- package/dist/agents/skills/orkestrel-dispatch/scripts/cite.js +95 -0
- package/dist/agents/skills/orkestrel-dispatch/scripts/helpers.js +207 -0
- package/dist/agents/skills/orkestrel-dispatch/scripts/launch.js +108 -0
- package/dist/agents/skills/orkestrel-dispatch/scripts/login.js +114 -0
- package/dist/agents/skills/orkestrel-dispatch/scripts/result.js +108 -0
- package/dist/agents/skills/orkestrel-dispatch/scripts/sweep.js +156 -0
- package/dist/agents/skills/orkestrel-harden/scripts/discovery.js +196 -0
- package/dist/agents/skills/orkestrel-publish/scripts/compare.js +206 -0
- package/dist/agents/skills/orkestrel-publish/scripts/pins.js +93 -0
- package/dist/agents/skills/orkestrel-publish/scripts/wave.js +458 -0
- package/dist/agents/skills/orkestrel-publish/scripts/window.js +188 -0
- package/dist/agents/skills/orkestrel-scout/scripts/map.js +300 -0
- package/dist/agents/templates/brief.md +55 -0
- package/dist/bin/main.js +4 -2
- package/dist/bin/main.js.map +1 -1
- package/dist/host/AGENTS.md +77 -135
- package/dist/host/agents/orchestration.md +147 -998
- package/dist/host/agents/skills/enterprise-bootstrap/SKILL.md +2 -2
- package/dist/host/agents/skills/enterprise-bootstrap/references/inspection.md +1 -1
- package/dist/host/agents/skills/{orkestrel-align-packages → orkestrel-align}/SKILL.md +6 -13
- package/dist/host/agents/skills/{orkestrel-align-packages → orkestrel-align}/agents/openai.yaml +1 -1
- package/dist/host/agents/skills/{orkestrel-align-packages → orkestrel-align}/references/fleet.md +5 -7
- package/dist/host/agents/skills/{orkestrel-build-application → orkestrel-build}/SKILL.md +11 -22
- package/dist/host/agents/skills/{orkestrel-build-application → orkestrel-build}/agents/openai.yaml +1 -1
- package/dist/host/agents/skills/orkestrel-debrief/SKILL.md +8 -16
- package/dist/host/agents/skills/orkestrel-debrief/references/instruction-audit.md +3 -3
- package/dist/host/agents/skills/orkestrel-debrief/references/retention.md +13 -13
- package/dist/host/agents/skills/orkestrel-dispatch/SKILL.md +61 -0
- package/dist/host/agents/skills/orkestrel-dispatch/agents/openai.yaml +4 -0
- package/dist/host/agents/skills/orkestrel-dispatch/references/bench.md +25 -0
- package/dist/host/agents/skills/orkestrel-dispatch/references/launch.md +32 -0
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/bench.ts +259 -0
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/brief.ts +110 -0
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/cite.ts +115 -0
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/helpers.ts +239 -0
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/launch.ts +124 -0
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/login.ts +123 -0
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/result.ts +129 -0
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/sweep.ts +157 -0
- package/dist/host/agents/skills/orkestrel-falsify/SKILL.md +42 -193
- package/dist/host/agents/skills/orkestrel-falsify/references/brief.md +38 -108
- package/dist/host/agents/skills/orkestrel-falsify/references/reconcile.md +35 -134
- package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/SKILL.md +10 -14
- package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/agents/openai.yaml +1 -1
- package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/references/hardening.md +3 -4
- package/dist/host/agents/skills/orkestrel-harden/scripts/discovery.ts +228 -0
- package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/SKILL.md +15 -23
- package/dist/host/agents/skills/orkestrel-journey/agents/openai.yaml +4 -0
- package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/references/captures.md +1 -1
- package/dist/host/agents/skills/{orkestrel-polish-surface → orkestrel-polish}/SKILL.md +25 -33
- package/dist/host/agents/skills/{orkestrel-polish-surface → orkestrel-polish}/agents/openai.yaml +1 -1
- package/dist/host/agents/skills/{orkestrel-polish-surface → orkestrel-polish}/references/capture-harness.md +3 -3
- package/dist/host/agents/skills/orkestrel-publish/SKILL.md +33 -20
- package/dist/host/agents/skills/orkestrel-publish/references/release.md +39 -0
- package/dist/host/agents/skills/orkestrel-publish/references/wave.md +22 -21
- package/dist/host/agents/skills/orkestrel-publish/references/window.md +27 -14
- package/dist/host/agents/skills/orkestrel-publish/scripts/compare.ts +220 -0
- package/dist/host/agents/skills/orkestrel-publish/scripts/pins.ts +114 -0
- package/dist/host/agents/skills/orkestrel-publish/scripts/wave.ts +629 -0
- package/dist/host/agents/skills/orkestrel-publish/scripts/window.ts +242 -0
- package/dist/host/agents/skills/orkestrel-scout/SKILL.md +28 -0
- package/dist/host/agents/skills/orkestrel-scout/agents/openai.yaml +4 -0
- package/dist/host/agents/skills/orkestrel-scout/scripts/map.ts +352 -0
- package/dist/host/agents/templates/brief.md +21 -142
- package/dist/host/agents/transports/claude-cli.md +21 -0
- package/dist/host/agents/transports/codex.md +38 -159
- package/dist/host/agents/transports/cursor.md +16 -65
- package/dist/host/claude/AGENTS.md +38 -0
- package/dist/host/claude/agents/analyst.md +14 -53
- package/dist/host/claude/agents/astra.md +26 -0
- package/dist/host/claude/agents/builder.md +14 -30
- package/dist/host/claude/agents/checker.md +13 -57
- package/dist/host/claude/agents/distiller.md +11 -26
- package/dist/host/claude/agents/grok.md +12 -35
- package/dist/host/claude/agents/opus.md +14 -30
- package/dist/host/claude/agents/planner.md +10 -44
- package/dist/host/claude/agents/researcher.md +11 -30
- package/dist/host/claude/agents/reviewer.md +11 -95
- package/dist/host/claude/agents/scout.md +9 -23
- package/dist/host/claude/agents/verifier.md +15 -33
- package/dist/host/claude/rules/documentation.md +8 -2
- package/dist/host/claude/rules/portability.md +7 -1
- package/dist/host/claude/rules/quality.md +36 -96
- package/dist/host/claude/rules/styles.md +3 -0
- package/dist/host/claude/rules/tests.md +6 -3
- package/dist/host/claude/rules/workspace.md +19 -15
- package/dist/host/claude/rules/writing.md +57 -108
- package/dist/host/claude/settings.json +5 -3
- package/dist/host/claude/skills/enterprise-bootstrap/SKILL.md +1 -1
- package/dist/host/claude/skills/{orkestrel-align-packages → orkestrel-align}/SKILL.md +2 -2
- package/dist/host/claude/skills/{orkestrel-build-application → orkestrel-build}/SKILL.md +2 -2
- package/dist/host/claude/skills/orkestrel-dispatch/SKILL.md +11 -0
- package/dist/host/claude/skills/orkestrel-falsify/SKILL.md +2 -1
- package/dist/host/claude/skills/{orkestrel-harden-package → orkestrel-harden}/SKILL.md +2 -2
- package/dist/host/claude/skills/{orkestrel-prove-journey → orkestrel-journey}/SKILL.md +2 -2
- package/dist/host/claude/skills/orkestrel-polish/SKILL.md +12 -0
- package/dist/host/claude/skills/orkestrel-scout/SKILL.md +11 -0
- package/dist/host/codex/agents/analyst.toml +14 -31
- package/dist/host/codex/agents/astra.toml +25 -0
- package/dist/host/codex/agents/builder.toml +13 -20
- package/dist/host/codex/agents/checker.toml +13 -27
- package/dist/host/codex/agents/distiller.toml +9 -22
- package/dist/host/codex/agents/grok.toml +11 -30
- package/dist/host/codex/agents/opus.toml +14 -22
- package/dist/host/codex/agents/orkestrel.toml +1 -1
- package/dist/host/codex/agents/planner.toml +11 -28
- package/dist/host/codex/agents/researcher.toml +10 -22
- package/dist/host/codex/agents/reviewer.toml +11 -27
- package/dist/host/codex/agents/scout.toml +11 -17
- package/dist/host/codex/agents/verifier.toml +16 -12
- package/dist/host/codex/config.toml +18 -21
- package/dist/host/cursor/mcp.json +0 -4
- package/dist/host/cursor/rules/orchestration.mdc +12 -20
- package/dist/host/dotfiles/mcp.json +0 -4
- package/dist/host/dotfiles/oxlintrc.json +7 -0
- package/dist/host/guides/probe.md +9 -9
- package/dist/host/guides/scaffold.md +117 -71
- package/dist/host/guides/test.md +1 -1
- package/dist/host/manifest.json +321 -184
- package/dist/host/scripts/codex.sh +0 -0
- package/dist/host/scripts/cursor.sh +0 -0
- package/dist/host/scripts/deps.sh +0 -0
- package/dist/host/scripts/ollama.sh +0 -0
- package/dist/host/tests/config.test.ts +68 -46
- package/dist/host/tests/policy.test.ts +1 -5
- package/dist/host/tests/setupPolicy.ts +179 -4
- package/dist/src/core/index.cjs +255 -84
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +94 -29
- package/dist/src/core/index.d.ts +94 -29
- package/dist/src/core/index.js +253 -85
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +55 -9
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +29 -4
- package/dist/src/server/index.d.ts +29 -4
- package/dist/src/server/index.js +56 -11
- package/dist/src/server/index.js.map +1 -1
- package/package.json +15 -11
- package/dist/host/CLAUDE.md +0 -61
- package/dist/host/agents/skills/orkestrel-prove-journey/agents/openai.yaml +0 -4
- package/dist/host/agents/transports/claude.md +0 -49
- package/dist/host/claude/agents/application.md +0 -36
- package/dist/host/claude/agents/sol.md +0 -61
- package/dist/host/claude/skills/orkestrel-polish-surface/SKILL.md +0 -12
- package/dist/host/codex/agents/application.toml +0 -25
- package/dist/host/codex/agents/sol.toml +0 -19
- /package/dist/host/agents/skills/{orkestrel-align-packages → orkestrel-align}/references/integration.md +0 -0
- /package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/references/centralization.md +0 -0
- /package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/references/contract.md +0 -0
- /package/dist/host/agents/skills/{orkestrel-harden-package → orkestrel-harden}/references/research.md +0 -0
- /package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/references/decide.md +0 -0
- /package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/references/layer.md +0 -0
- /package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/references/statechart.md +0 -0
- /package/dist/host/agents/skills/{orkestrel-prove-journey → orkestrel-journey}/references/styles.md +0 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// Census of test discovery: which projects the gates reach, what each collects, and where a suite
|
|
2
|
+
// skips. Run from the checkout root:
|
|
3
|
+
// node .agents/skills/orkestrel-harden/scripts/discovery.ts [--config vite.config.ts] [--projects a,b] [--json]
|
|
4
|
+
// The script follows every root script chain in package.json (a script no other script invokes) to
|
|
5
|
+
// the `--project` names its Vitest scripts gate and the test files its `node` scripts run, runs
|
|
6
|
+
// `vitest list --json` once through the local vitest entry to read what each project collects, and
|
|
7
|
+
// reads every collected file for `.skip(`, `.todo(`, `.skipIf(`, `.runIf(`, `retry:`, and
|
|
8
|
+
// `timeout:`. It flags a collected project no root chain reaches, a named project that collects
|
|
9
|
+
// nothing, and a test file under tests/ that no project collects and no root script runs directly.
|
|
10
|
+
// A project with no test file and no gate is outside the census. Exit 0 with no flag, 3 with one, 2 when Vitest cannot list, 64 on
|
|
11
|
+
// usage.
|
|
12
|
+
import { spawnSync } from 'node:child_process';
|
|
13
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
14
|
+
import { relative, resolve } from 'node:path';
|
|
15
|
+
import { listFiles, readMissingFlags, readOption, } from "../../orkestrel-dispatch/scripts/helpers.js";
|
|
16
|
+
const VITEST = 'node_modules/vitest/vitest.mjs';
|
|
17
|
+
const MARKERS = ['.skip(', '.todo(', '.skipIf(', '.runIf(', 'retry:', 'timeout:'];
|
|
18
|
+
function readScripts() {
|
|
19
|
+
const parsed = JSON.parse(readFileSync('package.json', 'utf8'));
|
|
20
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
21
|
+
return {};
|
|
22
|
+
const scripts = Object.fromEntries(Object.entries(parsed)).scripts;
|
|
23
|
+
if (typeof scripts !== 'object' || scripts === null)
|
|
24
|
+
return {};
|
|
25
|
+
const record = {};
|
|
26
|
+
for (const [key, value] of Object.entries(scripts))
|
|
27
|
+
if (typeof value === 'string')
|
|
28
|
+
record[key] = value;
|
|
29
|
+
return record;
|
|
30
|
+
}
|
|
31
|
+
function listInvocations(text) {
|
|
32
|
+
return [...text.matchAll(/npm run ([A-Za-z0-9:_-]+)/gu)].flatMap((match) => match[1] === undefined ? [] : [match[1]]);
|
|
33
|
+
}
|
|
34
|
+
function readGates(scripts) {
|
|
35
|
+
const invoked = new Set(Object.values(scripts).flatMap(listInvocations));
|
|
36
|
+
const projects = new Map();
|
|
37
|
+
const files = new Map();
|
|
38
|
+
for (const root of Object.keys(scripts).filter((name) => !invoked.has(name))) {
|
|
39
|
+
const visited = new Set();
|
|
40
|
+
const pending = [root];
|
|
41
|
+
while (pending.length > 0) {
|
|
42
|
+
const name = pending.pop();
|
|
43
|
+
if (name === undefined || visited.has(name))
|
|
44
|
+
continue;
|
|
45
|
+
visited.add(name);
|
|
46
|
+
const text = scripts[name];
|
|
47
|
+
if (text === undefined)
|
|
48
|
+
continue;
|
|
49
|
+
pending.push(...listInvocations(text));
|
|
50
|
+
const gate = root === name ? name : `${root} > ${name}`;
|
|
51
|
+
if (/\bvitest\b/u.test(text)) {
|
|
52
|
+
for (const match of text.matchAll(/--project[= ]([A-Za-z0-9:_-]+)/gu)) {
|
|
53
|
+
if (match[1] !== undefined && !projects.has(match[1]))
|
|
54
|
+
projects.set(match[1], gate);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
for (const match of text.matchAll(/(?:^|\s)((?:tests|src|app)\/[A-Za-z0-9_./-]+\.test\.[cm]?ts)\b/gu)) {
|
|
58
|
+
if (match[1] !== undefined && !files.has(match[1]))
|
|
59
|
+
files.set(match[1], gate);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return { projects, files };
|
|
64
|
+
}
|
|
65
|
+
function listCollected(config) {
|
|
66
|
+
const result = spawnSync(process.execPath, [VITEST, 'list', '--config', config, '--json'], {
|
|
67
|
+
encoding: 'utf8',
|
|
68
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
69
|
+
windowsHide: true,
|
|
70
|
+
});
|
|
71
|
+
if (result.status !== 0)
|
|
72
|
+
return undefined;
|
|
73
|
+
const start = result.stdout.indexOf('[');
|
|
74
|
+
if (start === -1)
|
|
75
|
+
return undefined;
|
|
76
|
+
const parsed = JSON.parse(result.stdout.slice(start));
|
|
77
|
+
if (!Array.isArray(parsed))
|
|
78
|
+
return undefined;
|
|
79
|
+
const collected = [];
|
|
80
|
+
for (const entry of parsed) {
|
|
81
|
+
if (typeof entry !== 'object' || entry === null)
|
|
82
|
+
continue;
|
|
83
|
+
const record = Object.fromEntries(Object.entries(entry));
|
|
84
|
+
if (typeof record.file === 'string' && typeof record.projectName === 'string') {
|
|
85
|
+
collected.push({
|
|
86
|
+
file: relative(process.cwd(), record.file).split('\\').join('/'),
|
|
87
|
+
projectName: record.projectName,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return collected;
|
|
92
|
+
}
|
|
93
|
+
function listTestFiles(directory) {
|
|
94
|
+
if (!existsSync(directory))
|
|
95
|
+
return [];
|
|
96
|
+
return listFiles(directory)
|
|
97
|
+
.map((file) => file.path)
|
|
98
|
+
.filter((path) => /\.test\.[cm]?ts$/u.test(path));
|
|
99
|
+
}
|
|
100
|
+
function countMarkers(file) {
|
|
101
|
+
const text = readFileSync(file, 'utf8');
|
|
102
|
+
const counts = {};
|
|
103
|
+
for (const marker of MARKERS) {
|
|
104
|
+
const count = text.split(marker).length - 1;
|
|
105
|
+
if (count > 0)
|
|
106
|
+
counts[marker] = count;
|
|
107
|
+
}
|
|
108
|
+
return counts;
|
|
109
|
+
}
|
|
110
|
+
function main(argv) {
|
|
111
|
+
const missing = readMissingFlags(argv, ['--config', '--projects']);
|
|
112
|
+
if (missing.length > 0) {
|
|
113
|
+
console.error(`discovery: ${missing.join(', ')} given with no value`);
|
|
114
|
+
return 64;
|
|
115
|
+
}
|
|
116
|
+
const config = readOption(argv, '--config') ?? 'vite.config.ts';
|
|
117
|
+
if (!existsSync(config) || !existsSync('package.json')) {
|
|
118
|
+
console.error('usage: discovery.ts [--config vite.config.ts] [--projects a,b] [--json]');
|
|
119
|
+
return 64;
|
|
120
|
+
}
|
|
121
|
+
if (!existsSync(VITEST)) {
|
|
122
|
+
console.error(`discovery: ${VITEST} is missing; run npm ci first`);
|
|
123
|
+
return 2;
|
|
124
|
+
}
|
|
125
|
+
const gates = readGates(readScripts());
|
|
126
|
+
const collected = listCollected(config);
|
|
127
|
+
if (collected === undefined) {
|
|
128
|
+
console.error('discovery: vitest list failed; run it bare to read the diagnostic');
|
|
129
|
+
return 2;
|
|
130
|
+
}
|
|
131
|
+
const named = (readOption(argv, '--projects') ?? '').split(',').filter((name) => name !== '');
|
|
132
|
+
const names = new Set([
|
|
133
|
+
...gates.projects.keys(),
|
|
134
|
+
...collected.map((entry) => entry.projectName),
|
|
135
|
+
...named,
|
|
136
|
+
]);
|
|
137
|
+
const projects = [...names].sort().map((name) => {
|
|
138
|
+
const mine = collected.filter((entry) => entry.projectName === name);
|
|
139
|
+
const fileGate = mine
|
|
140
|
+
.map((entry) => gates.files.get(entry.file))
|
|
141
|
+
.find((gate) => gate !== undefined);
|
|
142
|
+
return {
|
|
143
|
+
name,
|
|
144
|
+
gate: gates.projects.get(name) ?? fileGate,
|
|
145
|
+
files: new Set(mine.map((entry) => entry.file)).size,
|
|
146
|
+
tests: mine.length,
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
const collectedFiles = new Set(collected.map((entry) => entry.file));
|
|
150
|
+
const undiscovered = listTestFiles('tests')
|
|
151
|
+
.filter((file) => !collectedFiles.has(file) && !gates.files.has(file))
|
|
152
|
+
.sort();
|
|
153
|
+
const ungated = projects
|
|
154
|
+
.filter((project) => project.gate === undefined && project.tests > 0)
|
|
155
|
+
.map((project) => project.name);
|
|
156
|
+
// A project no chain reaches is a workbench (the `probe` project collects `tmp/probes/**`), so an
|
|
157
|
+
// empty one is reported, never flagged.
|
|
158
|
+
const empty = projects
|
|
159
|
+
.filter((project) => project.tests === 0 && project.gate?.includes(' > ') === true)
|
|
160
|
+
.map((project) => project.name);
|
|
161
|
+
const workbenches = projects
|
|
162
|
+
.filter((project) => project.tests === 0 && project.gate?.includes(' > ') !== true)
|
|
163
|
+
.map((project) => project.name);
|
|
164
|
+
const census = [...collectedFiles].sort().map((file) => ({
|
|
165
|
+
file,
|
|
166
|
+
projects: [
|
|
167
|
+
...new Set(collected.filter((entry) => entry.file === file).map((entry) => entry.projectName)),
|
|
168
|
+
],
|
|
169
|
+
markers: countMarkers(resolve(file)),
|
|
170
|
+
}));
|
|
171
|
+
const flagged = undiscovered.length + ungated.length + empty.length > 0;
|
|
172
|
+
if (argv.includes('--json')) {
|
|
173
|
+
console.log(JSON.stringify({ config, projects, ungated, empty, workbenches, undiscovered, census }));
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
for (const project of projects) {
|
|
177
|
+
console.log(`discovery: ${project.name.padEnd(14)} ${String(project.files).padStart(3)} file(s) ${String(project.tests).padStart(5)} test(s) gate=${project.gate ?? 'none'}`);
|
|
178
|
+
}
|
|
179
|
+
for (const name of ungated)
|
|
180
|
+
console.log(`discovery: ${name} collects tests but no root script chain reaches it`);
|
|
181
|
+
for (const name of empty)
|
|
182
|
+
console.log(`discovery: ${name} collects nothing`);
|
|
183
|
+
for (const name of workbenches)
|
|
184
|
+
console.log(`discovery: ${name} is a workbench no chain runs; it collects nothing`);
|
|
185
|
+
for (const file of undiscovered)
|
|
186
|
+
console.log(`discovery: ${file} is collected by no project`);
|
|
187
|
+
for (const entry of census) {
|
|
188
|
+
const marks = Object.entries(entry.markers);
|
|
189
|
+
if (marks.length > 0) {
|
|
190
|
+
console.log(`discovery: ${entry.file} ${marks.map(([marker, count]) => `${marker}×${count}`).join(' ')}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return flagged ? 3 : 0;
|
|
195
|
+
}
|
|
196
|
+
process.exitCode = main(process.argv.slice(2));
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// Compare the rebuilt dist/ against the published tarball on material content. Run from the
|
|
2
|
+
// publishing package's root after its build:
|
|
3
|
+
// node .agents/skills/orkestrel-publish/scripts/compare.ts [--package NAME] [--version V] [--tarball PATH] [--dist dist] [--json]
|
|
4
|
+
// The package defaults to the manifest's name and the version to what the registry serves; --tarball
|
|
5
|
+
// compares against a local .tgz instead of fetching. Sourcemaps are excluded, a text file differs
|
|
6
|
+
// only when it differs with all whitespace removed, and a binary file differs on any byte. The
|
|
7
|
+
// summary lists the paths added, removed, and changed. Exit 0 when nothing material differs, 3 when
|
|
8
|
+
// something does, 2 when the tarball cannot be read, 64 on usage.
|
|
9
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
10
|
+
import { relative } from 'node:path';
|
|
11
|
+
import { gunzipSync } from 'node:zlib';
|
|
12
|
+
import { listFiles, readMissingFlags, readOption, runNpm, } from "../../orkestrel-dispatch/scripts/helpers.js";
|
|
13
|
+
const BLOCK = 512;
|
|
14
|
+
const PACKAGE_PREFIX = 'package/';
|
|
15
|
+
function readManifest() {
|
|
16
|
+
if (!existsSync('package.json'))
|
|
17
|
+
return { name: undefined, version: undefined };
|
|
18
|
+
let parsed;
|
|
19
|
+
try {
|
|
20
|
+
parsed = JSON.parse(readFileSync('package.json', 'utf8'));
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return { name: undefined, version: undefined };
|
|
24
|
+
}
|
|
25
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
26
|
+
return { name: undefined, version: undefined };
|
|
27
|
+
const record = Object.fromEntries(Object.entries(parsed));
|
|
28
|
+
return {
|
|
29
|
+
name: typeof record.name === 'string' ? record.name : undefined,
|
|
30
|
+
version: typeof record.version === 'string' ? record.version : undefined,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function readField(text) {
|
|
34
|
+
const stripped = text.replace(/^"|"$/gu, '');
|
|
35
|
+
return stripped === '' ? undefined : stripped;
|
|
36
|
+
}
|
|
37
|
+
function readHeaderText(header, start, length) {
|
|
38
|
+
const slice = header.subarray(start, start + length);
|
|
39
|
+
const end = slice.indexOf(0);
|
|
40
|
+
return slice.subarray(0, end === -1 ? slice.length : end).toString('utf8');
|
|
41
|
+
}
|
|
42
|
+
function readPaxPath(data) {
|
|
43
|
+
let offset = 0;
|
|
44
|
+
while (offset < data.length) {
|
|
45
|
+
const space = data.indexOf(0x20, offset);
|
|
46
|
+
if (space === -1)
|
|
47
|
+
return undefined;
|
|
48
|
+
const length = Number(data.subarray(offset, space).toString('utf8'));
|
|
49
|
+
if (!Number.isFinite(length) || length <= 0)
|
|
50
|
+
return undefined;
|
|
51
|
+
const record = data.subarray(space + 1, offset + length - 1).toString('utf8');
|
|
52
|
+
if (record.startsWith('path='))
|
|
53
|
+
return record.slice('path='.length);
|
|
54
|
+
offset += length;
|
|
55
|
+
}
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
function readTar(archive) {
|
|
59
|
+
const files = new Map();
|
|
60
|
+
let offset = 0;
|
|
61
|
+
let overrideName;
|
|
62
|
+
while (offset + BLOCK <= archive.length) {
|
|
63
|
+
const header = archive.subarray(offset, offset + BLOCK);
|
|
64
|
+
if (header.every((byte) => byte === 0))
|
|
65
|
+
break;
|
|
66
|
+
const size = Number.parseInt(readHeaderText(header, 124, 12).trim() || '0', 8);
|
|
67
|
+
const type = String.fromCharCode(header[156] ?? 0);
|
|
68
|
+
const magic = readHeaderText(header, 257, 6);
|
|
69
|
+
const prefix = magic.startsWith('ustar') ? readHeaderText(header, 345, 155) : '';
|
|
70
|
+
const shortName = readHeaderText(header, 0, 100);
|
|
71
|
+
const name = overrideName ?? (prefix === '' ? shortName : `${prefix}/${shortName}`);
|
|
72
|
+
const data = archive.subarray(offset + BLOCK, offset + BLOCK + size);
|
|
73
|
+
overrideName = undefined;
|
|
74
|
+
if (type === 'x')
|
|
75
|
+
overrideName = readPaxPath(data);
|
|
76
|
+
else if (type === 'L')
|
|
77
|
+
overrideName = readHeaderText(data, 0, data.length);
|
|
78
|
+
else if (type === '0' || type === '\0')
|
|
79
|
+
files.set(name, Buffer.from(data));
|
|
80
|
+
offset += BLOCK + Math.ceil(size / BLOCK) * BLOCK;
|
|
81
|
+
}
|
|
82
|
+
return files;
|
|
83
|
+
}
|
|
84
|
+
// A dist tree holds nothing a walk skips, so every emitted file counts.
|
|
85
|
+
const NOTHING_SKIPPED = new Set();
|
|
86
|
+
function readNpmValue(args) {
|
|
87
|
+
const result = runNpm(args);
|
|
88
|
+
return result.status === 0 ? readField(result.stdout.trim()) : undefined;
|
|
89
|
+
}
|
|
90
|
+
function isMaterial(path) {
|
|
91
|
+
return !path.endsWith('.map');
|
|
92
|
+
}
|
|
93
|
+
function differs(left, right) {
|
|
94
|
+
if (left.includes(0) || right.includes(0))
|
|
95
|
+
return !left.equals(right);
|
|
96
|
+
return left.toString('utf8').replace(/\s+/gu, '') !== right.toString('utf8').replace(/\s+/gu, '');
|
|
97
|
+
}
|
|
98
|
+
async function main(argv) {
|
|
99
|
+
const missing = readMissingFlags(argv, ['--package', '--version', '--tarball', '--dist']);
|
|
100
|
+
if (missing.length > 0) {
|
|
101
|
+
console.error(`compare: ${missing.join(', ')} given with no value`);
|
|
102
|
+
return 64;
|
|
103
|
+
}
|
|
104
|
+
const manifest = readManifest();
|
|
105
|
+
const name = readOption(argv, '--package') ?? manifest.name;
|
|
106
|
+
const dist = readOption(argv, '--dist') ?? 'dist';
|
|
107
|
+
const tarball = readOption(argv, '--tarball');
|
|
108
|
+
if (name === undefined ||
|
|
109
|
+
!existsSync(dist) ||
|
|
110
|
+
(argv.includes('--tarball') && tarball === undefined)) {
|
|
111
|
+
console.error('usage: compare.ts [--package NAME] [--version V] [--tarball PATH] [--dist dist] [--json]');
|
|
112
|
+
return 64;
|
|
113
|
+
}
|
|
114
|
+
let version = readOption(argv, '--version');
|
|
115
|
+
let archive;
|
|
116
|
+
if (tarball !== undefined) {
|
|
117
|
+
if (!existsSync(tarball)) {
|
|
118
|
+
console.error(`compare: ${tarball} does not exist`);
|
|
119
|
+
return 2;
|
|
120
|
+
}
|
|
121
|
+
archive = readFileSync(tarball);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
version ??= readNpmValue(['view', name, 'version', '--json']);
|
|
125
|
+
const url = version === undefined
|
|
126
|
+
? undefined
|
|
127
|
+
: readNpmValue(['view', `${name}@${version}`, 'dist.tarball', '--json']);
|
|
128
|
+
if (version === undefined || url === undefined) {
|
|
129
|
+
console.error(`compare: the registry served no tarball for ${name}${version === undefined ? '' : `@${version}`}`);
|
|
130
|
+
return 2;
|
|
131
|
+
}
|
|
132
|
+
const fetched = await fetchTarball(url);
|
|
133
|
+
if (fetched === undefined) {
|
|
134
|
+
console.error(`compare: fetching ${url} failed`);
|
|
135
|
+
return 2;
|
|
136
|
+
}
|
|
137
|
+
archive = fetched;
|
|
138
|
+
}
|
|
139
|
+
const published = new Map();
|
|
140
|
+
let entries;
|
|
141
|
+
try {
|
|
142
|
+
entries = readTar(gunzipSync(archive));
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
console.error(`compare: the tarball could not be read: ${error instanceof Error ? error.message : String(error)}`);
|
|
146
|
+
return 2;
|
|
147
|
+
}
|
|
148
|
+
for (const [path, content] of entries) {
|
|
149
|
+
const prefixed = `${PACKAGE_PREFIX}${dist}/`;
|
|
150
|
+
if (path.startsWith(prefixed) && isMaterial(path))
|
|
151
|
+
published.set(path.slice(prefixed.length), content);
|
|
152
|
+
}
|
|
153
|
+
const local = new Map();
|
|
154
|
+
for (const { path } of listFiles(dist, NOTHING_SKIPPED)) {
|
|
155
|
+
const key = relative(dist, path).split('\\').join('/');
|
|
156
|
+
if (isMaterial(key))
|
|
157
|
+
local.set(key, readFileSync(path));
|
|
158
|
+
}
|
|
159
|
+
const added = [...local.keys()].filter((key) => !published.has(key)).sort();
|
|
160
|
+
const removed = [...published.keys()].filter((key) => !local.has(key)).sort();
|
|
161
|
+
const changed = [...local.keys()]
|
|
162
|
+
.filter((key) => {
|
|
163
|
+
const other = published.get(key);
|
|
164
|
+
const mine = local.get(key);
|
|
165
|
+
return other !== undefined && mine !== undefined && differs(mine, other);
|
|
166
|
+
})
|
|
167
|
+
.sort();
|
|
168
|
+
const material = added.length + removed.length + changed.length > 0;
|
|
169
|
+
const summary = {
|
|
170
|
+
package: name,
|
|
171
|
+
version,
|
|
172
|
+
tarball,
|
|
173
|
+
dist,
|
|
174
|
+
compared: published.size,
|
|
175
|
+
added,
|
|
176
|
+
removed,
|
|
177
|
+
changed,
|
|
178
|
+
material,
|
|
179
|
+
};
|
|
180
|
+
if (argv.includes('--json'))
|
|
181
|
+
console.log(JSON.stringify(summary));
|
|
182
|
+
else {
|
|
183
|
+
console.log(`compare: ${name}${version === undefined ? '' : `@${version}`} — ${published.size} published file(s), ${added.length} added, ${removed.length} removed, ${changed.length} changed`);
|
|
184
|
+
for (const path of added)
|
|
185
|
+
console.log(`compare: added ${path}`);
|
|
186
|
+
for (const path of removed)
|
|
187
|
+
console.log(`compare: removed ${path}`);
|
|
188
|
+
for (const path of changed)
|
|
189
|
+
console.log(`compare: changed ${path}`);
|
|
190
|
+
}
|
|
191
|
+
return material ? 3 : 0;
|
|
192
|
+
}
|
|
193
|
+
async function fetchTarball(url) {
|
|
194
|
+
try {
|
|
195
|
+
const response = await fetch(url);
|
|
196
|
+
if (!response.ok)
|
|
197
|
+
return undefined;
|
|
198
|
+
return Buffer.from(await response.arrayBuffer());
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
main(process.argv.slice(2)).then((code) => {
|
|
205
|
+
process.exitCode = code;
|
|
206
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Sweep source and tests for a prior version or range literal after a bump or a re-pin. Run from
|
|
2
|
+
// the publishing package's root:
|
|
3
|
+
// node .agents/skills/orkestrel-publish/scripts/pins.ts --version 0.0.76 [--range ^0.0.16 ...] [--paths src,tests] [--json]
|
|
4
|
+
// `--range` repeats. A version hit is the literal bounded by characters other than digits and dots,
|
|
5
|
+
// so `~0.0.76` and `"0.0.76"` both hit and `0.0.760` does not. A range that is one comparator over
|
|
6
|
+
// one version (`^8.3.0`, `~8.3.0`, `>=8.3.0`, `v8.3.0`) hits its bare version in any form, bounded the
|
|
7
|
+
// same way, so `--range ^8.3.0` hits `~8.3.0` and `serves 8.3.0` and misses `^8.3.1`; any other
|
|
8
|
+
// range (`>=1.2.0 <2.0.0`, `8.x`) hits its literal bounded the same way and not preceded by another
|
|
9
|
+
// range operator. Each matching line prints once as path:line: text, naming the first requested
|
|
10
|
+
// literal that matched in the order --version then each --range, so the operator rules on it.
|
|
11
|
+
// Exit 0 with no hits, 3 with hits, 64 on usage.
|
|
12
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
13
|
+
import { listFiles, readMissingFlags, readOptions, } from "../../orkestrel-dispatch/scripts/helpers.js";
|
|
14
|
+
const BINARY = /\.(png|jpg|jpeg|gif|webp|ico|woff2?|ttf|otf|eot|zip|tgz|gz|pdf|wasm)$/u;
|
|
15
|
+
const COMPARATOR = /^(?:\^|~|>=|>|<=|<|=)?v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?)$/u;
|
|
16
|
+
function escapePattern(literal) {
|
|
17
|
+
return literal.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
|
|
18
|
+
}
|
|
19
|
+
function buildVersionPattern(version) {
|
|
20
|
+
return new RegExp(`(?<![0-9.])${escapePattern(version)}(?![0-9.])`, 'u');
|
|
21
|
+
}
|
|
22
|
+
function buildVersionLiteral(literal) {
|
|
23
|
+
return { literal, pattern: buildVersionPattern(literal) };
|
|
24
|
+
}
|
|
25
|
+
function buildRangeLiteral(literal) {
|
|
26
|
+
const version = COMPARATOR.exec(literal)?.[1];
|
|
27
|
+
if (version !== undefined)
|
|
28
|
+
return { literal, pattern: buildVersionPattern(version) };
|
|
29
|
+
return {
|
|
30
|
+
literal,
|
|
31
|
+
pattern: new RegExp(`(?<![0-9.^~<>=])${escapePattern(literal)}(?![0-9.])`, 'u'),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function sweepFile(path, literals) {
|
|
35
|
+
const content = readFileSync(path);
|
|
36
|
+
if (content.includes(0))
|
|
37
|
+
return [];
|
|
38
|
+
const hits = [];
|
|
39
|
+
content
|
|
40
|
+
.toString('utf8')
|
|
41
|
+
.split(/\r\n|\n/)
|
|
42
|
+
.forEach((text, index) => {
|
|
43
|
+
const match = literals.find((entry) => entry.pattern.test(text));
|
|
44
|
+
if (match !== undefined) {
|
|
45
|
+
hits.push({ path, line: index + 1, literal: match.literal, text: text.trim() });
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
return hits;
|
|
49
|
+
}
|
|
50
|
+
function main(argv) {
|
|
51
|
+
const missing = readMissingFlags(argv, ['--version', '--range', '--paths']);
|
|
52
|
+
if (missing.length > 0) {
|
|
53
|
+
console.error(`pins: ${missing.join(', ')} given with no value`);
|
|
54
|
+
return 64;
|
|
55
|
+
}
|
|
56
|
+
const version = readOptions(argv, '--version')[0];
|
|
57
|
+
const ranges = readOptions(argv, '--range');
|
|
58
|
+
const paths = (readOptions(argv, '--paths')[0] ?? 'src,tests').split(',');
|
|
59
|
+
if (version === undefined && ranges.length === 0) {
|
|
60
|
+
console.error('usage: pins.ts --version PRIOR [--range PRIOR_RANGE ...] [--paths src,tests] [--json]');
|
|
61
|
+
return 64;
|
|
62
|
+
}
|
|
63
|
+
const literals = [
|
|
64
|
+
...(version === undefined ? [] : [buildVersionLiteral(version)]),
|
|
65
|
+
...ranges.map(buildRangeLiteral),
|
|
66
|
+
];
|
|
67
|
+
const present = paths.filter((path) => {
|
|
68
|
+
try {
|
|
69
|
+
return statSync(path).isDirectory();
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
const skipped = paths.filter((path) => !present.includes(path));
|
|
76
|
+
for (const path of skipped)
|
|
77
|
+
console.error(`pins: ${path} is not a directory; skipped`);
|
|
78
|
+
const hits = present
|
|
79
|
+
.flatMap((directory) => listFiles(directory).map((file) => file.path))
|
|
80
|
+
.filter((file) => !BINARY.test(file))
|
|
81
|
+
.flatMap((file) => sweepFile(file, literals));
|
|
82
|
+
const names = literals.map((entry) => entry.literal);
|
|
83
|
+
if (argv.includes('--json')) {
|
|
84
|
+
console.log(JSON.stringify({ literals: names, paths: present, skipped, hits }));
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
for (const hit of hits)
|
|
88
|
+
console.log(`${hit.path}:${hit.line}: ${hit.text}`);
|
|
89
|
+
console.log(`pins: ${hits.length} hit(s) for ${names.join(', ')} under ${present.join(', ')}`);
|
|
90
|
+
}
|
|
91
|
+
return hits.length === 0 ? 0 : 3;
|
|
92
|
+
}
|
|
93
|
+
process.exitCode = main(process.argv.slice(2));
|