@haystackeditor/cli 0.25.1 → 0.27.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/README.md +39 -463
- package/dist/capture/app-config.js +30 -6
- package/dist/commands/capture-brief.js +45 -35
- package/dist/commands/capture-contract.js +4 -4
- package/dist/commands/crawl-report.js +196 -0
- package/dist/commands/db-profile-upload.js +3 -3
- package/dist/commands/feedback.js +66 -0
- package/dist/commands/init-telemetry.js +55 -9
- package/dist/commands/init.js +8 -3
- package/dist/commands/lockfile-pin.js +307 -0
- package/dist/commands/telemetry-token.js +3 -3
- package/dist/commands/tokens.js +4 -4
- package/dist/commands/verify-explore.js +3 -3
- package/dist/commands/verify-history.js +2 -2
- package/dist/commands/verify-onboarding.js +13 -16
- package/dist/commands/verify-precompute.js +3 -4
- package/dist/commands/verify.js +31 -118
- package/dist/index.js +55 -968
- package/dist/schema.js +4 -10
- package/dist/utils/haystack-api.js +8 -36
- package/package.json +1 -5
- package/schemas/feedback.v1.json +13 -0
- package/schemas/pre-verify.v2.json +240 -0
- package/schemas/verify-raw.v1.json +1132 -0
- package/schemas/verify.v2.json +655 -0
- package/dist/assets/hooks/agent-context/detect.ts +0 -316
- package/dist/assets/hooks/agent-context/format.ts +0 -100
- package/dist/assets/hooks/agent-context/index.ts +0 -41
- package/dist/assets/hooks/agent-context/parsers/claude.ts +0 -262
- package/dist/assets/hooks/agent-context/parsers/codex.ts +0 -416
- package/dist/assets/hooks/agent-context/parsers/gemini.ts +0 -155
- package/dist/assets/hooks/agent-context/parsers/opencode.ts +0 -174
- package/dist/assets/hooks/agent-context/tsconfig.json +0 -14
- package/dist/assets/hooks/agent-context/types.ts +0 -58
- package/dist/assets/hooks/llm-rules-template.md +0 -59
- package/dist/assets/hooks/package-lock.json +0 -598
- package/dist/assets/hooks/package.json +0 -12
- package/dist/assets/hooks/scripts/commit-msg.sh +0 -5
- package/dist/assets/hooks/scripts/post-commit.sh +0 -5
- package/dist/assets/hooks/scripts/pre-commit.sh +0 -175
- package/dist/assets/hooks/scripts/pre-push.sh +0 -25
- package/dist/assets/hooks/scripts/prepare-commit-msg.sh +0 -5
- package/dist/assets/hooks/truncation-checker/ast-analyzer.ts +0 -528
- package/dist/assets/hooks/truncation-checker/index.ts +0 -595
- package/dist/assets/hooks/truncation-checker/tsconfig.json +0 -13
- package/dist/assets/skills/map-cloud-verifier-universe/SKILL.md +0 -2051
- package/dist/assets/skills/map-cloud-verifier-universe/agents/openai.yaml +0 -4
- package/dist/assets/skills/map-cloud-verifier-universe/references/output-contract.md +0 -3411
- package/dist/assets/skills/map-your-system.md +0 -143
- package/dist/assets/skills/submit.md +0 -200
- package/dist/commands/ask.js +0 -20
- package/dist/commands/cloud-verifier-behaviors.js +0 -218
- package/dist/commands/cloud-verifier-data-store-census.js +0 -539
- package/dist/commands/cloud-verifier-data-store-drift.js +0 -158
- package/dist/commands/cloud-verifier-identity-census.js +0 -4060
- package/dist/commands/cloud-verifier-materialization.js +0 -704
- package/dist/commands/cloud-verifier-pascal-selector-census.js +0 -1382
- package/dist/commands/cloud-verifier-python-manifest-selector-census.js +0 -2015
- package/dist/commands/cloud-verifier-specialized-operational-census.js +0 -11432
- package/dist/commands/cloud-verifier-universe.js +0 -10178
- package/dist/commands/config.js +0 -549
- package/dist/commands/design-verify.js +0 -311
- package/dist/commands/dismiss.js +0 -159
- package/dist/commands/hooks.js +0 -226
- package/dist/commands/inbox.js +0 -137
- package/dist/commands/mcp.js +0 -201
- package/dist/commands/policy.js +0 -371
- package/dist/commands/pr-status.js +0 -207
- package/dist/commands/pr.js +0 -105
- package/dist/commands/prepare-universe-review.js +0 -1092
- package/dist/commands/production-source-deny-policy.js +0 -100
- package/dist/commands/request-review.js +0 -74
- package/dist/commands/review.js +0 -191
- package/dist/commands/rules.js +0 -98
- package/dist/commands/scaffold-provisional-universe.js +0 -806
- package/dist/commands/setup.js +0 -1170
- package/dist/commands/skills.js +0 -447
- package/dist/commands/status.js +0 -35
- package/dist/commands/submit.js +0 -745
- package/dist/commands/system-map.js +0 -228
- package/dist/commands/triage.js +0 -598
- package/dist/commands/webhooks.js +0 -241
- package/dist/states.js +0 -46
- package/dist/tools/detect.js +0 -832
- package/dist/triage/astra.js +0 -202
- package/dist/triage/prompts.js +0 -188
- package/dist/triage/runner.js +0 -200
- package/dist/triage/types.js +0 -7
- package/dist/types.js +0 -326
- package/dist/utils/action-output.js +0 -26
- package/dist/utils/analysis-api.js +0 -416
- package/dist/utils/config.js +0 -54
- package/dist/utils/design-verifier-api.js +0 -294
- package/dist/utils/design-verifier-history.js +0 -79
- package/dist/utils/design-verifier-result.js +0 -424
- package/dist/utils/github-api.js +0 -324
- package/dist/utils/pending-state.js +0 -86
- package/dist/utils/pr-ref.js +0 -56
- package/dist/utils/prompter.js +0 -328
- package/schemas/action.v1.json +0 -22
- package/schemas/ask.v1.json +0 -40
- package/schemas/inbox.v1.json +0 -27
- package/schemas/pr-status.v1.json +0 -61
- package/schemas/pr.v1.json +0 -97
- package/schemas/pr.v3.json +0 -45
- package/schemas/setup.v1.json +0 -75
- package/schemas/submit.v1.json +0 -90
- package/schemas/triage.v1.json +0 -103
- package/schemas/triage.v2.json +0 -64
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adding one dependency to an app's lockfile without moving anything else (rule 13: init must never change what a build
|
|
3
|
+
* resolves beyond what it adds). Measured on real lockfiles (Rallly's pnpm workspace, and scratch workspaces whose app
|
|
4
|
+
* locks older versions of packages the CLI also uses):
|
|
5
|
+
* - yarn 1, yarn 4 and bun add only the new descriptors and entries; nothing else moves.
|
|
6
|
+
* - npm hoists the new tree and replaces hoisted packages the app already locks (ws, yaml, zod moved to the CLI's
|
|
7
|
+
* versions); with `--install-strategy=nested` it places the new tree under the dependency and moves nothing.
|
|
8
|
+
* - pnpm re-resolves the dependencies of every package the new tree shares with the old one to the newest version in the
|
|
9
|
+
* graph (47 entries in Rallly, from @smithy/* to express), and no flag stops it. So init lets pnpm resolve the new
|
|
10
|
+
* dependency (`--lockfile-only`), keeps every entry the lockfile had, adds the entries only the new dependency
|
|
11
|
+
* reaches (mergePnpmLockfile), and installs exactly that (`pnpm install --frozen-lockfile`, which also proves it).
|
|
12
|
+
* lockfileDrift checks the result for every format init can read, so a package manager that moves anything else fails the
|
|
13
|
+
* change (and init restores every file).
|
|
14
|
+
*/
|
|
15
|
+
function unquote(text) {
|
|
16
|
+
if (text.startsWith('\'') && text.endsWith('\''))
|
|
17
|
+
return text.slice(1, -1).replace(/''/g, '\'');
|
|
18
|
+
if (text.startsWith('"') && text.endsWith('"'))
|
|
19
|
+
return JSON.parse(text);
|
|
20
|
+
return text;
|
|
21
|
+
}
|
|
22
|
+
/** An entry's key line (` 'name@1.0.0(peer@2)':`, ` name@1.0.0: {}`) as its name. */
|
|
23
|
+
function keyOf(line) {
|
|
24
|
+
return unquote(line.trim().replace(/: \{\}$/, '').replace(/:$/, ''));
|
|
25
|
+
}
|
|
26
|
+
const ENTRY_SECTIONS = new Set(['importers', 'packages', 'snapshots']);
|
|
27
|
+
/** pnpm-lock.yaml as its top-level sections, the importers, packages and snapshots split into entries kept as written. */
|
|
28
|
+
function parseLock(text) {
|
|
29
|
+
const sections = [];
|
|
30
|
+
for (const line of text.replace(/\n$/, '').split('\n')) {
|
|
31
|
+
if (/^\S/.test(line))
|
|
32
|
+
sections.push({ header: line, entries: null, raw: [] });
|
|
33
|
+
else if (sections.length === 0)
|
|
34
|
+
throw new Error('pnpm-lock.yaml does not start with a section');
|
|
35
|
+
else
|
|
36
|
+
sections[sections.length - 1].raw.push(line);
|
|
37
|
+
}
|
|
38
|
+
for (const section of sections) {
|
|
39
|
+
if (!ENTRY_SECTIONS.has(section.header.replace(/:$/, '')))
|
|
40
|
+
continue;
|
|
41
|
+
const entries = [];
|
|
42
|
+
for (const line of section.raw) {
|
|
43
|
+
if (/^ {2}\S/.test(line))
|
|
44
|
+
entries.push({ name: keyOf(line), lines: [line] });
|
|
45
|
+
else if (line.trim() !== '') {
|
|
46
|
+
if (entries.length === 0)
|
|
47
|
+
throw new Error(`pnpm-lock.yaml's ${section.header} has a line outside any entry`);
|
|
48
|
+
entries[entries.length - 1].lines.push(line);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
section.entries = entries;
|
|
52
|
+
}
|
|
53
|
+
return sections;
|
|
54
|
+
}
|
|
55
|
+
function formatLock(sections) {
|
|
56
|
+
const blocks = sections.map(section => {
|
|
57
|
+
if (section.entries === null)
|
|
58
|
+
return [section.header, ...section.raw].join('\n');
|
|
59
|
+
return [section.header, '', section.entries.map(entry => entry.lines.join('\n')).join('\n\n'), ''].join('\n');
|
|
60
|
+
});
|
|
61
|
+
return `${blocks.join('\n').replace(/\n+$/, '')}\n`;
|
|
62
|
+
}
|
|
63
|
+
function section(sections, name) {
|
|
64
|
+
const found = sections.find(item => item.header === `${name}:`);
|
|
65
|
+
if (!found?.entries)
|
|
66
|
+
throw new Error(`pnpm-lock.yaml has no ${name} section`);
|
|
67
|
+
return found.entries;
|
|
68
|
+
}
|
|
69
|
+
/** A snapshot's edges: `dependencies` (required) and `optionalDependencies`, as snapshot keys. */
|
|
70
|
+
function edges(entry) {
|
|
71
|
+
const out = [];
|
|
72
|
+
let field = null;
|
|
73
|
+
for (const line of entry.lines.slice(1)) {
|
|
74
|
+
const header = /^ {4}(\w+):$/.exec(line);
|
|
75
|
+
if (header) {
|
|
76
|
+
field = header[1];
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (/^ {4}\S/.test(line)) {
|
|
80
|
+
field = null;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const dependency = /^ {6}(.+?): (.+)$/.exec(line);
|
|
84
|
+
if (!dependency || (field !== 'dependencies' && field !== 'optionalDependencies'))
|
|
85
|
+
continue;
|
|
86
|
+
const name = unquote(dependency[1]);
|
|
87
|
+
const version = unquote(dependency[2]);
|
|
88
|
+
if (version.startsWith('link:'))
|
|
89
|
+
continue;
|
|
90
|
+
// An alias resolves to another package: its value is that package's own key.
|
|
91
|
+
const key = /^(@[^/@]+\/)?[^@(]+@/.test(version) && !/^\d/.test(version) ? version : `${name}@${version}`;
|
|
92
|
+
out.push({ key, optional: field === 'optionalDependencies' });
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
/** The snapshot key's package (`name@1.0.0(peer@2)` → `name@1.0.0`). */
|
|
97
|
+
function packageKey(key) {
|
|
98
|
+
const at = key.indexOf('@', 1);
|
|
99
|
+
const peers = key.indexOf('(', at);
|
|
100
|
+
return peers === -1 ? key : key.slice(0, peers);
|
|
101
|
+
}
|
|
102
|
+
/** An importer's dependencies by field (`dependencies:`, `devDependencies:`, …), each entry's lines as written. */
|
|
103
|
+
function importerFields(entry) {
|
|
104
|
+
const fields = [];
|
|
105
|
+
for (const line of entry.lines.slice(1)) {
|
|
106
|
+
const header = /^ {4}(\w+):$/.exec(line);
|
|
107
|
+
if (header) {
|
|
108
|
+
fields.push({ field: header[1], dependencies: [] });
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const current = fields[fields.length - 1];
|
|
112
|
+
if (!current)
|
|
113
|
+
throw new Error(`pnpm-lock.yaml's importer ${entry.name} has a line outside any field`);
|
|
114
|
+
if (/^ {6}\S/.test(line))
|
|
115
|
+
current.dependencies.push({ field: current.field, name: keyOf(line.slice(4)), lines: [line] });
|
|
116
|
+
else if (current.dependencies.length > 0)
|
|
117
|
+
current.dependencies[current.dependencies.length - 1].lines.push(line);
|
|
118
|
+
else
|
|
119
|
+
throw new Error(`pnpm-lock.yaml's importer ${entry.name} has an unexpected ${current.field} line`);
|
|
120
|
+
}
|
|
121
|
+
return fields;
|
|
122
|
+
}
|
|
123
|
+
const byName = (a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
124
|
+
function versionOf(dependency) {
|
|
125
|
+
const line = dependency.lines.find(item => /^ {8}version: /.test(item));
|
|
126
|
+
if (!line)
|
|
127
|
+
throw new Error(`pnpm-lock.yaml lists ${dependency.name} with no version`);
|
|
128
|
+
return unquote(line.trim().slice('version: '.length));
|
|
129
|
+
}
|
|
130
|
+
/** Every snapshot key reachable from `roots` (required edges first: `required` holds those reached without an optional edge). */
|
|
131
|
+
function reach(snapshots, roots) {
|
|
132
|
+
const all = new Set();
|
|
133
|
+
const required = new Set();
|
|
134
|
+
const queue = roots.map(root => ({ ...root }));
|
|
135
|
+
while (queue.length > 0) {
|
|
136
|
+
const { key, optional } = queue.shift();
|
|
137
|
+
if (optional ? all.has(key) : required.has(key))
|
|
138
|
+
continue;
|
|
139
|
+
const entry = snapshots.get(key);
|
|
140
|
+
if (!entry)
|
|
141
|
+
throw new Error(`pnpm-lock.yaml references ${key}, which it does not list`);
|
|
142
|
+
all.add(key);
|
|
143
|
+
if (!optional)
|
|
144
|
+
required.add(key);
|
|
145
|
+
for (const edge of edges(entry))
|
|
146
|
+
queue.push({ key: edge.key, optional: optional || edge.optional });
|
|
147
|
+
}
|
|
148
|
+
return { all, required };
|
|
149
|
+
}
|
|
150
|
+
function withOptional(entry, optional) {
|
|
151
|
+
const body = entry.lines.slice(1).filter(line => line !== ' optional: true');
|
|
152
|
+
const key = entry.lines[0].replace(/: \{\}$/, ':');
|
|
153
|
+
if (optional)
|
|
154
|
+
return { name: entry.name, lines: [key, ...body, ' optional: true'] };
|
|
155
|
+
return { name: entry.name, lines: body.length === 0 ? [`${key.replace(/:$/, '')}: {}`] : [key, ...body] };
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* The lockfile `before` plus exactly what pnpm's own resolution (`after`, from `pnpm add --lockfile-only`) adds for
|
|
159
|
+
* `dependency` in `importer`: its importer entry, the snapshots and packages only it reaches, and the optional flag
|
|
160
|
+
* removed from snapshots it now requires; an earlier version of it and what only that reached go. Everything else stays as
|
|
161
|
+
* `before` had it, so shared packages keep their resolutions (each one satisfies its own ranges already).
|
|
162
|
+
*/
|
|
163
|
+
export function mergePnpmLockfile(before, after, importer, dependency) {
|
|
164
|
+
const old = parseLock(before);
|
|
165
|
+
const resolved = parseLock(after);
|
|
166
|
+
if (formatLock(old) !== before)
|
|
167
|
+
throw new Error('init cannot rewrite this pnpm-lock.yaml exactly as pnpm wrote it');
|
|
168
|
+
for (const lock of [old, resolved]) {
|
|
169
|
+
if (lock[0]?.header !== 'lockfileVersion: \'9.0\'')
|
|
170
|
+
throw new Error(`init merges pnpm lockfile version 9.0, and this one is ${lock[0]?.header ?? 'empty'}`);
|
|
171
|
+
}
|
|
172
|
+
const header = (lock) => lock.filter(item => item.entries === null).map(item => [item.header, ...item.raw].join('\n')).join('\n');
|
|
173
|
+
if (header(old) !== header(resolved))
|
|
174
|
+
throw new Error('pnpm changed the lockfile\'s settings while adding the dependency');
|
|
175
|
+
const oldImporter = section(old, 'importers').find(entry => entry.name === importer);
|
|
176
|
+
const newImporter = section(resolved, 'importers').find(entry => entry.name === importer);
|
|
177
|
+
if (!oldImporter || !newImporter)
|
|
178
|
+
throw new Error(`pnpm-lock.yaml has no importer ${importer}`);
|
|
179
|
+
const added = importerFields(newImporter).flatMap(item => item.dependencies).find(item => item.name === dependency);
|
|
180
|
+
if (!added)
|
|
181
|
+
throw new Error(`pnpm did not add ${dependency} to ${importer}`);
|
|
182
|
+
const earlier = importerFields(oldImporter).flatMap(item => item.dependencies).find(item => item.name === dependency) ?? null;
|
|
183
|
+
// The importer: as before, with the dependency in the field pnpm put it in (fields in pnpm's order).
|
|
184
|
+
const oldFields = importerFields(oldImporter);
|
|
185
|
+
const order = importerFields(newImporter).map(item => item.field);
|
|
186
|
+
const fields = [...new Set([...order, ...oldFields.map(item => item.field)])].map(field => {
|
|
187
|
+
const kept = (oldFields.find(item => item.field === field)?.dependencies ?? []).filter(item => item.name !== dependency);
|
|
188
|
+
return { field, dependencies: field === added.field ? [...kept, added].sort(byName) : kept };
|
|
189
|
+
}).filter(item => item.dependencies.length > 0);
|
|
190
|
+
const importerEntry = { name: importer, lines: [oldImporter.lines[0].replace(/: \{\}$/, ':'), ...fields.flatMap(item => [` ${item.field}:`, ...item.dependencies.flatMap(dep => dep.lines)])] };
|
|
191
|
+
// The graph: before's snapshots, then pnpm's for keys before did not have.
|
|
192
|
+
const oldSnapshots = new Map(section(old, 'snapshots').map(entry => [entry.name, entry]));
|
|
193
|
+
const newSnapshots = new Map(section(resolved, 'snapshots').map(entry => [entry.name, entry]));
|
|
194
|
+
const merged = new Map(oldSnapshots);
|
|
195
|
+
const root = `${dependency}@${versionOf(added)}`;
|
|
196
|
+
// Walk the new dependency's tree through the merged graph, taking pnpm's entries only where before had none.
|
|
197
|
+
const queue = [root];
|
|
198
|
+
const seen = new Set();
|
|
199
|
+
while (queue.length > 0) {
|
|
200
|
+
const key = queue.shift();
|
|
201
|
+
if (seen.has(key))
|
|
202
|
+
continue;
|
|
203
|
+
seen.add(key);
|
|
204
|
+
if (!merged.has(key)) {
|
|
205
|
+
const entry = newSnapshots.get(key);
|
|
206
|
+
if (!entry)
|
|
207
|
+
throw new Error(`pnpm's resolution references ${key}, which it does not list`);
|
|
208
|
+
merged.set(key, entry);
|
|
209
|
+
}
|
|
210
|
+
for (const edge of edges(merged.get(key)))
|
|
211
|
+
queue.push(edge.key);
|
|
212
|
+
}
|
|
213
|
+
const fieldOf = { dependencies: false, devDependencies: false, optionalDependencies: true };
|
|
214
|
+
const ours = reach(merged, [{ key: root, optional: fieldOf[added.field] ?? false }]);
|
|
215
|
+
for (const key of ours.all) {
|
|
216
|
+
const entry = merged.get(key);
|
|
217
|
+
const optional = !ours.required.has(key) && (oldSnapshots.has(key) ? entry.lines.includes(' optional: true') : true);
|
|
218
|
+
merged.set(key, withOptional(entry, optional));
|
|
219
|
+
}
|
|
220
|
+
// An earlier version of the dependency: what only it reached goes, as pnpm would prune it.
|
|
221
|
+
if (earlier && `${dependency}@${versionOf(earlier)}` !== root) {
|
|
222
|
+
const importers = section(old, 'importers').map(entry => (entry.name === importer ? importerEntry : entry));
|
|
223
|
+
const roots = importers.flatMap(entry => importerFields(entry).flatMap(item => item.dependencies
|
|
224
|
+
.map(dep => ({ dep, version: versionOf(dep) })).filter(({ version }) => !version.startsWith('link:'))
|
|
225
|
+
.map(({ dep, version }) => ({ key: /^(@[^/@]+\/)?[^@(]+@/.test(version) && !/^\d/.test(version) ? version : `${dep.name}@${version}`,
|
|
226
|
+
optional: fieldOf[dep.field] ?? false }))));
|
|
227
|
+
const live = reach(merged, roots).all;
|
|
228
|
+
for (const key of reach(oldSnapshots, [{ key: `${dependency}@${versionOf(earlier)}`, optional: false }]).all)
|
|
229
|
+
if (!live.has(key))
|
|
230
|
+
merged.delete(key);
|
|
231
|
+
}
|
|
232
|
+
// Packages: before's, less those only the earlier version's snapshots used, plus pnpm's for the new ones.
|
|
233
|
+
const used = new Set([...merged.keys()].map(packageKey));
|
|
234
|
+
const dropped = new Set([...oldSnapshots.keys()].filter(key => !merged.has(key)).map(packageKey).filter(name => !used.has(name)));
|
|
235
|
+
const oldPackages = section(old, 'packages');
|
|
236
|
+
const listed = new Set(oldPackages.map(entry => entry.name));
|
|
237
|
+
const newPackages = new Map(section(resolved, 'packages').map(entry => [entry.name, entry]));
|
|
238
|
+
const packages = oldPackages.filter(entry => !dropped.has(entry.name));
|
|
239
|
+
for (const name of used) {
|
|
240
|
+
if (listed.has(name))
|
|
241
|
+
continue;
|
|
242
|
+
const entry = newPackages.get(name);
|
|
243
|
+
if (!entry)
|
|
244
|
+
throw new Error(`pnpm's resolution lists no package ${name}`);
|
|
245
|
+
packages.push(entry);
|
|
246
|
+
}
|
|
247
|
+
return formatLock(old.map(item => {
|
|
248
|
+
if (item.header === 'importers:')
|
|
249
|
+
return { ...item, entries: section(old, 'importers').map(entry => (entry.name === importer ? importerEntry : entry)) };
|
|
250
|
+
if (item.header === 'packages:')
|
|
251
|
+
return { ...item, entries: packages.sort(byName) };
|
|
252
|
+
if (item.header === 'snapshots:')
|
|
253
|
+
return { ...item, entries: [...merged.values()].sort(byName) };
|
|
254
|
+
return item;
|
|
255
|
+
}));
|
|
256
|
+
}
|
|
257
|
+
/** Entries as keys and their content, per format; the importer's own entry under IMPORTER. */
|
|
258
|
+
const IMPORTER = '\u0000importer';
|
|
259
|
+
function entriesOf(format, text, importer) {
|
|
260
|
+
const out = new Map();
|
|
261
|
+
if (format === 'pnpm') {
|
|
262
|
+
for (const item of parseLock(text)) {
|
|
263
|
+
if (item.entries === null) {
|
|
264
|
+
out.set(`header ${item.header}`, item.raw.join('\n'));
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
for (const entry of item.entries) {
|
|
268
|
+
const name = item.header === 'importers:' && entry.name === importer ? IMPORTER : `${item.header} ${entry.name}`;
|
|
269
|
+
// The optional flag is the dependency's to clear (it now requires the package): compared without it.
|
|
270
|
+
out.set(name, entry.lines.filter(line => line !== ' optional: true').join('\n').replace(/: \{\}$/, ':'));
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
else if (format === 'npm') {
|
|
275
|
+
const packages = JSON.parse(text).packages ?? {};
|
|
276
|
+
for (const [key, value] of Object.entries(packages))
|
|
277
|
+
out.set(key === (importer === '.' ? '' : importer) ? IMPORTER : key, JSON.stringify(value));
|
|
278
|
+
}
|
|
279
|
+
else if (format === 'yarn') {
|
|
280
|
+
for (const block of text.split(/\n\s*\n/)) {
|
|
281
|
+
const lines = block.split('\n').filter(line => line.trim() !== '' && !line.startsWith('#'));
|
|
282
|
+
if (lines.length === 0 || /^\s/.test(lines[0]))
|
|
283
|
+
continue;
|
|
284
|
+
for (const descriptor of lines[0].replace(/:$/, '').split(/,\s*/)) {
|
|
285
|
+
const name = unquote(descriptor);
|
|
286
|
+
out.set(/@workspace:/.test(name) && name.endsWith(`@workspace:${importer}`) ? IMPORTER : name, lines.slice(1).join('\n'));
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
const parsed = JSON.parse(text.replace(/,(\s*[}\]])/g, '$1'));
|
|
292
|
+
for (const [key, value] of Object.entries(parsed.workspaces ?? {}))
|
|
293
|
+
out.set(key === (importer === '.' ? '' : importer) ? IMPORTER : `workspace ${key}`, JSON.stringify(value));
|
|
294
|
+
for (const [key, value] of Object.entries(parsed.packages ?? {}))
|
|
295
|
+
out.set(`package ${key}`, JSON.stringify(value));
|
|
296
|
+
}
|
|
297
|
+
return out;
|
|
298
|
+
}
|
|
299
|
+
/** The entries both lockfiles list whose content adding `dependency` to `importer` changed, other than the importer's own
|
|
300
|
+
* and the dependency's own: what that must never touch. (An entry only an earlier version of the dependency used may go;
|
|
301
|
+
* a moved resolution always shows in the entries that reference it.) */
|
|
302
|
+
export function lockfileDrift(format, before, after, importer, dependency) {
|
|
303
|
+
const a = entriesOf(format, before, importer);
|
|
304
|
+
const b = entriesOf(format, after, importer);
|
|
305
|
+
return [...a.entries()].filter(([key, value]) => key !== IMPORTER && !key.includes(dependency) && b.has(key) && b.get(key) !== value)
|
|
306
|
+
.map(([key]) => key);
|
|
307
|
+
}
|
|
@@ -22,7 +22,7 @@ import { basename, dirname, join, resolve } from 'node:path';
|
|
|
22
22
|
import { createInterface } from 'node:readline/promises';
|
|
23
23
|
import chalk from 'chalk';
|
|
24
24
|
import { resolveAuthContext } from '../utils/auth.js';
|
|
25
|
-
import { classifyHttpError, HaystackApiError } from '../utils/haystack-api.js';
|
|
25
|
+
import { classifyHttpError, HaystackApiError, repoApiPath } from '../utils/haystack-api.js';
|
|
26
26
|
import { gatewayFetch } from './case-batch.js';
|
|
27
27
|
import { TELEMETRY_ENVIRONMENT, TELEMETRY_SERVER_STATUS_PATH, TELEMETRY_TOKEN_PATH, TELEMETRY_TOKEN_SHA256, TELEMETRY_TOKEN_VERSION, } from './capture-contract.js';
|
|
28
28
|
/** GitHub's owner and repository name alphabets. */
|
|
@@ -51,7 +51,7 @@ async function confirmed(question) {
|
|
|
51
51
|
}
|
|
52
52
|
/** GET the repository's server-telemetry status: its active token's hash, the endpoint, the newest flush. */
|
|
53
53
|
export async function readTelemetryServerStatus(repository, token) {
|
|
54
|
-
const path =
|
|
54
|
+
const path = repoApiPath(repository, TELEMETRY_SERVER_STATUS_PATH);
|
|
55
55
|
const response = await gatewayFetch(path, token);
|
|
56
56
|
if (!response.ok)
|
|
57
57
|
throw await classifyHttpError(response, `Haystack API ${TELEMETRY_SERVER_STATUS_PATH}`);
|
|
@@ -199,7 +199,7 @@ async function mintUnderLock(repository, file, authToken, options) {
|
|
|
199
199
|
let answer;
|
|
200
200
|
try {
|
|
201
201
|
const request = { repository, tokenSha256, rotate: options.rotate === true };
|
|
202
|
-
const response = await gatewayFetch(
|
|
202
|
+
const response = await gatewayFetch(repoApiPath(repository, TELEMETRY_TOKEN_PATH), authToken, {
|
|
203
203
|
method: 'POST', body: JSON.stringify(request),
|
|
204
204
|
});
|
|
205
205
|
if (!response.ok)
|
package/dist/commands/tokens.js
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
* - `haystack login --headless [--label NAME] [--scope repo:owner/name,...]`
|
|
6
6
|
* Opens the browser to mint a token via the auth-worker, then writes it
|
|
7
7
|
* locally so subsequent CLI calls auth via Bearer.
|
|
8
|
-
* - `haystack tokens
|
|
8
|
+
* - `haystack auth tokens`
|
|
9
9
|
* Lists tokens on the server side (the local file only stores ONE
|
|
10
10
|
* active token at a time; the server may have many tied to your user).
|
|
11
|
-
* - `haystack
|
|
11
|
+
* - `haystack auth revoke <id>`
|
|
12
12
|
* Revokes a server-side token. If the revoked token is the one
|
|
13
13
|
* currently on disk, the local file is wiped so the next CLI call
|
|
14
14
|
* doesn't keep using a dead token.
|
|
@@ -123,12 +123,12 @@ export async function headlessLoginCommand(opts) {
|
|
|
123
123
|
console.log(` ${chalk.dim('path:')} ${path}`);
|
|
124
124
|
console.log(` ${chalk.dim('id:')} ${result.id}`);
|
|
125
125
|
console.log(chalk.dim(' Subsequent `haystack` calls will use this token.'));
|
|
126
|
-
console.log(chalk.dim(' Revoke server-side with: haystack
|
|
126
|
+
console.log(chalk.dim(' Revoke server-side with: haystack auth revoke ' + result.id));
|
|
127
127
|
}
|
|
128
128
|
export async function headlessLogoutCommand() {
|
|
129
129
|
await clearHeadlessToken();
|
|
130
130
|
console.log(chalk.green('✓ Headless token removed from disk.'));
|
|
131
|
-
console.log(chalk.dim(' Server-side token is still active. Revoke it with `haystack
|
|
131
|
+
console.log(chalk.dim(' Server-side token is still active. Revoke it with `haystack auth revoke <id>`.'));
|
|
132
132
|
}
|
|
133
133
|
export async function listTokensCommand(opts) {
|
|
134
134
|
const token = await loadToken();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { resolveAuthContext } from '../utils/auth.js';
|
|
3
|
-
import { haystackJson } from '../utils/haystack-api.js';
|
|
3
|
+
import { haystackJson, repoApiPath } from '../utils/haystack-api.js';
|
|
4
4
|
const RUN_ID = /^cv_[0-9a-f]{48}$/;
|
|
5
5
|
const CASE_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
6
6
|
const ACTION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
@@ -27,8 +27,8 @@ async function context(runId, repository) {
|
|
|
27
27
|
const { token } = await resolveAuthContext({ owner, repo });
|
|
28
28
|
return {
|
|
29
29
|
token,
|
|
30
|
-
path: `/
|
|
31
|
-
query:
|
|
30
|
+
path: repoApiPath(repository, `/runs/${runId}/cases`),
|
|
31
|
+
query: '',
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
34
|
export async function verifyCasesCommand(runId, options) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resolveAuthContext } from '../utils/auth.js';
|
|
2
|
-
import { haystackJson, HaystackApiError } from '../utils/haystack-api.js';
|
|
2
|
+
import { haystackJson, HaystackApiError, repoApiPath } from '../utils/haystack-api.js';
|
|
3
3
|
const HEALTH = ['healthy', 'degraded', 'infrastructure-error', 'not-executed', 'unknown'];
|
|
4
4
|
const LIFECYCLES = ['complete', 'failed', 'cancelled', 'budget-stopped', 'account-action'];
|
|
5
5
|
const RUN_ID = /^cv_[0-9a-f]{48}$/;
|
|
@@ -139,7 +139,7 @@ export async function verifyHistoryCommand(repository, options) {
|
|
|
139
139
|
const auth = await resolveAuthContext({ preferredLogin: options.account, owner, repo: name });
|
|
140
140
|
let response;
|
|
141
141
|
try {
|
|
142
|
-
response = await haystackJson(
|
|
142
|
+
response = await haystackJson(repoApiPath(`${owner}/${name}`, `/runs?limit=${limit}`), auth.token, { method: 'GET' });
|
|
143
143
|
}
|
|
144
144
|
catch (error) {
|
|
145
145
|
if (error instanceof HaystackApiError && error.status === 404) {
|
|
@@ -10,17 +10,14 @@
|
|
|
10
10
|
import chalk from 'chalk';
|
|
11
11
|
import { withSchema } from '../schema.js';
|
|
12
12
|
import { resolveAuthContext } from '../utils/auth.js';
|
|
13
|
-
import { classifyHttpError, readWithRetries, SERVICE_SILENCE_LIMIT_MS, ServiceSilentError, transientReadFailure, } from '../utils/haystack-api.js';
|
|
13
|
+
import { classifyHttpError, readWithRetries, SERVICE_SILENCE_LIMIT_MS, ServiceSilentError, transientReadFailure, repoApiPath, } from '../utils/haystack-api.js';
|
|
14
14
|
import { trackOnboardingEvent } from '../utils/telemetry.js';
|
|
15
15
|
import { gatewayFetch } from './case-batch.js';
|
|
16
16
|
import { ONBOARDING_STAGES, ONBOARDING_VERSION, } from './onboarding-contract.js';
|
|
17
17
|
import { sameNotes } from './onboarding-notes.js';
|
|
18
18
|
import { deriveBaseSha, EXPLICIT_WALL_MS, resolveOriginRepository } from './verify-precompute.js';
|
|
19
19
|
import { findGitRoot } from '../utils/hooks.js';
|
|
20
|
-
const
|
|
21
|
-
const FACTS_PATH = `${ONBOARDING_PATH}/facts`;
|
|
22
|
-
const ANSWERS_PATH = `${ONBOARDING_PATH}/answers`;
|
|
23
|
-
const NOTES_PATH = `${ONBOARDING_PATH}/notes`;
|
|
20
|
+
const onboardingPath = (repository, rest = '') => repoApiPath(repository, `/onboarding${rest}`);
|
|
24
21
|
const ONBOARD_RUN_ID = /^ob_[0-9a-f]{48}$/;
|
|
25
22
|
const SHA256 = /^[0-9a-f]{64}$/;
|
|
26
23
|
const COMMIT = /^[0-9a-f]{40}$/;
|
|
@@ -155,11 +152,11 @@ async function body(response, what) {
|
|
|
155
152
|
export async function readOnboardingStatus(repository, baseCommit, token, onboardRunId = null) {
|
|
156
153
|
if (!COMMIT.test(baseCommit))
|
|
157
154
|
throw new Error(`Invalid base commit ${baseCommit}.`);
|
|
158
|
-
const path =
|
|
159
|
-
+ (onboardRunId === null ? '' : `&
|
|
155
|
+
const path = onboardingPath(repository, `?base=${baseCommit}`)
|
|
156
|
+
+ (onboardRunId === null ? '' : `&run=${encodeURIComponent(onboardRunId)}`);
|
|
160
157
|
const response = await gatewayFetch(path, token);
|
|
161
158
|
if (!response.ok)
|
|
162
|
-
throw await classifyHttpError(response, `Haystack API ${
|
|
159
|
+
throw await classifyHttpError(response, `Haystack API ${path}`);
|
|
163
160
|
return parseOnboardingStatus(await body(response, 'a status'), { repository, baseCommit });
|
|
164
161
|
}
|
|
165
162
|
/** Rule 12's start POST: onboard this base now, before any change exists. Idempotent: a
|
|
@@ -167,11 +164,11 @@ export async function readOnboardingStatus(repository, baseCommit, token, onboar
|
|
|
167
164
|
export async function startOnboarding(repository, baseCommit, token) {
|
|
168
165
|
if (!COMMIT.test(baseCommit))
|
|
169
166
|
throw new Error(`Invalid base commit ${baseCommit}.`);
|
|
170
|
-
const response = await gatewayFetch(
|
|
167
|
+
const response = await gatewayFetch(onboardingPath(repository), token, {
|
|
171
168
|
method: 'POST', body: JSON.stringify({ repository, baseCommit }),
|
|
172
169
|
});
|
|
173
170
|
if (!response.ok)
|
|
174
|
-
throw await classifyHttpError(response, `Haystack API ${
|
|
171
|
+
throw await classifyHttpError(response, `Haystack API ${onboardingPath(repository)}`);
|
|
175
172
|
return parseOnboardingStatus(await body(response, 'a status'), { repository, baseCommit });
|
|
176
173
|
}
|
|
177
174
|
/** How often a command reads onboarding status while it runs. The status route holds no read (a crawl's reads wait for a
|
|
@@ -357,9 +354,9 @@ function repositoryOf(value) {
|
|
|
357
354
|
return `${origin.owner}/${origin.repository}`;
|
|
358
355
|
}
|
|
359
356
|
export async function readFacts(repository, token) {
|
|
360
|
-
const response = await gatewayFetch(
|
|
357
|
+
const response = await gatewayFetch(onboardingPath(repository, '/facts'), token);
|
|
361
358
|
if (!response.ok)
|
|
362
|
-
throw await classifyHttpError(response, `Haystack API ${
|
|
359
|
+
throw await classifyHttpError(response, `Haystack API ${onboardingPath(repository, '/facts')}`);
|
|
363
360
|
return parseFactsView(await body(response, 'facts'), repository);
|
|
364
361
|
}
|
|
365
362
|
/** `haystack verify answer <question-id> <choice>`: files the answer to an open
|
|
@@ -379,7 +376,7 @@ export async function verifyAnswerCommand(questionId, choice, options) {
|
|
|
379
376
|
if (!question.choices.includes(choice)) {
|
|
380
377
|
throw new Error(`"${choice}" is not one of the question's choices: ${question.choices.map(item => JSON.stringify(item)).join(', ')}.`);
|
|
381
378
|
}
|
|
382
|
-
const response = await gatewayFetch(
|
|
379
|
+
const response = await gatewayFetch(onboardingPath(repository, '/answers'), auth.token, {
|
|
383
380
|
method: 'POST', body: JSON.stringify({ repository, expectedFactsDigest: facts.factsDigest, questionId, answer: choice }),
|
|
384
381
|
});
|
|
385
382
|
if (response.status === 409 && attempt < 3) {
|
|
@@ -387,7 +384,7 @@ export async function verifyAnswerCommand(questionId, choice, options) {
|
|
|
387
384
|
continue;
|
|
388
385
|
}
|
|
389
386
|
if (!response.ok)
|
|
390
|
-
throw await classifyHttpError(response, `Haystack API ${
|
|
387
|
+
throw await classifyHttpError(response, `Haystack API ${onboardingPath(repository, '/answers')}`);
|
|
391
388
|
const view = parseFactsView(await body(response, 'facts'), repository);
|
|
392
389
|
if (options.json) {
|
|
393
390
|
process.stdout.write(`${JSON.stringify(withSchema('verify-answer', { facts: view }), null, 2)}\n`);
|
|
@@ -407,7 +404,7 @@ export async function writeOnboardingNotes(repository, notes, token) {
|
|
|
407
404
|
if (sameNotes(facts.facts.notes, notes))
|
|
408
405
|
return 'unchanged';
|
|
409
406
|
const request = { repository, expectedFactsDigest: facts.factsDigest, notes };
|
|
410
|
-
const response = await gatewayFetch(
|
|
407
|
+
const response = await gatewayFetch(onboardingPath(repository, '/notes'), token, {
|
|
411
408
|
method: 'POST', body: JSON.stringify(request),
|
|
412
409
|
});
|
|
413
410
|
if (response.status === 409 && attempt < 3) {
|
|
@@ -415,7 +412,7 @@ export async function writeOnboardingNotes(repository, notes, token) {
|
|
|
415
412
|
continue;
|
|
416
413
|
}
|
|
417
414
|
if (!response.ok)
|
|
418
|
-
throw await classifyHttpError(response, `Haystack API ${
|
|
415
|
+
throw await classifyHttpError(response, `Haystack API ${onboardingPath(repository, '/notes')}`);
|
|
419
416
|
if (!sameNotes(parseFactsView(await body(response, 'facts'), repository).facts.notes, notes))
|
|
420
417
|
invalid('the notes it answered with');
|
|
421
418
|
return 'written';
|
|
@@ -8,7 +8,7 @@ import chalk from 'chalk';
|
|
|
8
8
|
import { NotLoggedInError, resolveAuthContext } from '../utils/auth.js';
|
|
9
9
|
import { findGitRoot, parseRemoteUrl, } from '../utils/git.js';
|
|
10
10
|
import { resolveVerifyBaseSha } from '../utils/verify-base.js';
|
|
11
|
-
import { HaystackApiError, haystackFetch, haystackJson, REPO_NOT_ENTITLED_CODE, SUBSCRIPTION_REQUIRED_CODE, } from '../utils/haystack-api.js';
|
|
11
|
+
import { HaystackApiError, haystackFetch, haystackJson, REPO_NOT_ENTITLED_CODE, SUBSCRIPTION_REQUIRED_CODE, repoApiPath, } from '../utils/haystack-api.js';
|
|
12
12
|
import { trackPrecomputeEvent } from '../utils/telemetry.js';
|
|
13
13
|
import { findSecretsInPatch, isSecretBearingPath } from '../utils/secret-paths.js';
|
|
14
14
|
import { startPrecomputeDelivery } from './precompute-delivery.js';
|
|
@@ -325,8 +325,7 @@ function precomputeResult(response) {
|
|
|
325
325
|
* service no longer has it). */
|
|
326
326
|
async function uploadSourcePatch(request, token, signal, force = false) {
|
|
327
327
|
const gzipped = Buffer.from(request.patchGzBase64, 'base64');
|
|
328
|
-
const path = `/
|
|
329
|
-
+ `?owner=${encodeURIComponent(request.owner)}&repository=${encodeURIComponent(request.repository)}`;
|
|
328
|
+
const path = repoApiPath(`${request.owner}/${request.repository}`, `/patches/${request.patchSha256}`);
|
|
330
329
|
const found = force ? null : await haystackFetch(path, token, { method: 'HEAD', signal }, [404]);
|
|
331
330
|
if (found === null || found.status === 404) {
|
|
332
331
|
await haystackFetch(path, token, { method: 'PUT', headers: { 'Content-Type': 'application/gzip' }, body: gzipped, signal });
|
|
@@ -339,7 +338,7 @@ async function submitCapture(request, token, signal) {
|
|
|
339
338
|
const { patchGzBase64: _inline, ...named } = request;
|
|
340
339
|
const submit = async (force) => {
|
|
341
340
|
const patchBytes = await uploadSourcePatch(request, token, signal, force);
|
|
342
|
-
return haystackJson('/
|
|
341
|
+
return haystackJson(repoApiPath(`${request.owner}/${request.repository}`, '/checks'), token, { method: 'POST',
|
|
343
342
|
headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...named, patchBytes }), signal });
|
|
344
343
|
};
|
|
345
344
|
try {
|