@haystackeditor/cli 0.25.0 → 0.26.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 -455
- package/dist/capture/adapters/django.js +7 -0
- package/dist/capture/adapters/index.js +10 -0
- package/dist/capture/adapters/next-build.js +113 -0
- package/dist/capture/adapters/next.js +272 -105
- package/dist/capture/adapters/nuxt.js +7 -0
- package/dist/capture/adapters/rails.js +7 -0
- package/dist/capture/adapters/react-router.js +7 -0
- package/dist/capture/adapters/sveltekit.js +7 -0
- package/dist/capture/adapters/vite.js +14 -0
- package/dist/capture/app-config.js +27 -2
- package/dist/capture/csp.js +7 -6
- package/dist/capture/url-rewrites.js +232 -0
- package/dist/capture-step.js +3 -0
- package/dist/commands/capture-brief.js +41 -32
- package/dist/commands/capture-manifest.js +25 -17
- package/dist/commands/feedback.js +66 -0
- package/dist/commands/init-capture.js +44 -27
- package/dist/commands/init-telemetry.js +77 -14
- package/dist/commands/init.js +19 -4
- package/dist/commands/lockfile-pin.js +307 -0
- package/dist/commands/verify-onboarding.js +16 -0
- package/dist/commands/verify.js +70 -15
- package/dist/index.js +29 -923
- package/dist/schema.js +4 -10
- package/dist/utils/haystack-api.js +0 -36
- package/package.json +1 -5
- package/schemas/feedback.v1.json +13 -0
- package/schemas/init.v1.json +1 -1
- 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/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/utils/action-output.js +0 -26
- package/dist/utils/analysis-api.js +0 -416
- 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
|
@@ -31,8 +31,9 @@ import { findCsp } from '../capture/csp.js';
|
|
|
31
31
|
import { applyEdits, keyName, loadBabel, parseJsonLike } from '../capture/js-ast.js';
|
|
32
32
|
import { buildManifest, exactRoutes, manifestDocument, MANIFEST_DIR } from '../capture/manifest.js';
|
|
33
33
|
import { appContext } from '../capture/project.js';
|
|
34
|
+
import { parseUrlRewrites, URL_REWRITES_QUESTION } from '../capture/url-rewrites.js';
|
|
34
35
|
import { loadCaptureScript } from '../capture/script-release.js';
|
|
35
|
-
import {
|
|
36
|
+
import { STATIC_SUBDIR } from '../capture/tag.js';
|
|
36
37
|
import { CAPTURE_ORIGIN, CAPTURE_VERSION } from './capture-contract.js';
|
|
37
38
|
import { HaystackApiError } from '../utils/haystack-api.js';
|
|
38
39
|
import { lineDiff } from './init.js';
|
|
@@ -50,9 +51,10 @@ const COMPOSABLE_SCRIPT = /^[A-Za-z0-9_./=:@\s&-]+$/;
|
|
|
50
51
|
const STAGED_DIR = `.haystack/${STATIC_SUBDIR}`;
|
|
51
52
|
/** The answers in effect: the flags, else the ones init recorded in .haystack/capture.json (rule 8b: the user's answers,
|
|
52
53
|
* never inferred, so a rerun that only updates the script needs none). */
|
|
53
|
-
export function captureAnswers(appDir, origins, consent) {
|
|
54
|
+
export function captureAnswers(appDir, origins, consent, urlRewrites) {
|
|
54
55
|
const recorded = readCaptureConfig(appDir);
|
|
55
|
-
return { origins: origins.length > 0 ? origins : recorded?.origins ?? [], consent: consent ?? recorded?.consent
|
|
56
|
+
return { origins: origins.length > 0 ? origins : recorded?.origins ?? [], consent: consent ?? recorded?.consent,
|
|
57
|
+
urlRewrites: urlRewrites ?? recorded?.urlRewrites ?? undefined };
|
|
56
58
|
}
|
|
57
59
|
function readText(path) {
|
|
58
60
|
try {
|
|
@@ -126,6 +128,9 @@ function scriptFiles(dir) {
|
|
|
126
128
|
return [];
|
|
127
129
|
}
|
|
128
130
|
}
|
|
131
|
+
function formatUrlRewrites(rewrites) {
|
|
132
|
+
return rewrites.kind === 'none' ? 'none' : `hidden-segment:${rewrites.name}`;
|
|
133
|
+
}
|
|
129
134
|
function manifestCommand(appPath) {
|
|
130
135
|
return `haystack capture manifest --app ${appPath}`;
|
|
131
136
|
}
|
|
@@ -144,6 +149,7 @@ export async function planCapture(input) {
|
|
|
144
149
|
if (input.answers.consent === undefined)
|
|
145
150
|
throw new Error('planCapture needs the consent answer.');
|
|
146
151
|
const consent = parseConsentChoice(input.answers.consent);
|
|
152
|
+
const urlRewrites = input.answers.urlRewrites === undefined ? null : formatUrlRewrites(parseUrlRewrites(input.answers.urlRewrites));
|
|
147
153
|
const plan = { changes: [], packageText: null, needsCli: false, registration: null, notices: [], part: part([]) };
|
|
148
154
|
if (input.registration && 'failed' in input.registration) {
|
|
149
155
|
plan.part = part([`Run \`haystack init\` again: registering the app for capture failed (${input.registration.failed}).`]);
|
|
@@ -182,18 +188,36 @@ export async function planCapture(input) {
|
|
|
182
188
|
key: tags.key,
|
|
183
189
|
repositoryId: identity?.repositoryId ?? ID_PLACEHOLDER,
|
|
184
190
|
applicationId: identity?.applicationId ?? ID_PLACEHOLDER,
|
|
185
|
-
adapter
|
|
191
|
+
// The adapter init recognized, whatever it left manual: the manifest command uses it once the cause is fixed.
|
|
192
|
+
adapter: detected ? detected.id : null,
|
|
186
193
|
app: ctx.appPath,
|
|
187
194
|
origins,
|
|
188
195
|
consent: formatConsentChoice(consent),
|
|
189
196
|
// Where a build step publishes (none for a static site, whose manifest init writes and the repository keeps).
|
|
190
|
-
publish: detected && !detected.
|
|
191
|
-
|
|
197
|
+
publish: detected && !isManual(detected.publishDirs) && !isManual(detected.build) && detected.build.kind !== 'init-writes'
|
|
198
|
+
? detected.publishDirs : [],
|
|
199
|
+
urlRewrites,
|
|
192
200
|
};
|
|
193
201
|
planner.text(captureConfigFile(appDir), formatCaptureConfig(config), 'The app\'s capture identity, committed for the build step that publishes its route manifest (CAPTURE-V1 rule 8b).');
|
|
194
202
|
const consentStep = manualConsentStep(consent);
|
|
195
|
-
|
|
196
|
-
|
|
203
|
+
if (!detected) {
|
|
204
|
+
plan.part = { status: 'unsupported', why: `init recognized no web framework at ${ctx.appPath} (it looked for package.json, Gemfile,`
|
|
205
|
+
+ ' manage.py and index.html there), and the tag records nothing without the route manifest only a recognized framework\'s routes'
|
|
206
|
+
+ ' give; run `haystack init --app <dir>` with the app\'s directory' };
|
|
207
|
+
plan.changes = planner.changes;
|
|
208
|
+
return plan;
|
|
209
|
+
}
|
|
210
|
+
// A manifest that cannot be made exactly (a base path, a config init cannot read) leaves nothing worth placing: a tag
|
|
211
|
+
// without a manifest records nothing. Two steps that work: remove the cause, run init again (the recorded answers carry
|
|
212
|
+
// over, and the manifest command checks the cause on every build).
|
|
213
|
+
if (detected.manifestBlocked) {
|
|
214
|
+
steps.push(`Remove what stops ${detected.label}'s route manifest: ${detected.manifestBlocked} (init looked at ${detected.looked.join(', ')}).`);
|
|
215
|
+
steps.push('Then run `haystack init` again: it places the tag, the files and the build step that publishes the manifest itself.');
|
|
216
|
+
plan.changes = planner.changes;
|
|
217
|
+
plan.part = part(steps);
|
|
218
|
+
return plan;
|
|
219
|
+
}
|
|
220
|
+
const staticDir = typeof detected.staticDir === 'string' ? detected.staticDir : null;
|
|
197
221
|
// The files: in the app's static output, or staged for a manual step.
|
|
198
222
|
const filesDir = staticDir === null ? join(appDir, STAGED_DIR) : join(appDir, staticDir, STATIC_SUBDIR);
|
|
199
223
|
const wanted = new Set([script.file, ...(consentFile ? [consentFile.file] : [])]);
|
|
@@ -205,24 +229,8 @@ export async function planCapture(input) {
|
|
|
205
229
|
if (!wanted.has(old))
|
|
206
230
|
planner.remove(join(filesDir, old), 'An earlier capture file, replaced by this version.');
|
|
207
231
|
const servedAt = `/${STATIC_SUBDIR}/`;
|
|
208
|
-
if (!automatic) {
|
|
209
|
-
const why = detected ? `${detected.label}: ${detected.blocked}` : `init recognized no framework at ${ctx.appPath}`;
|
|
210
|
-
const looked = detected?.looked.join(', ') ?? `${ctx.appPath} (package.json, Gemfile, manage.py, index.html)`;
|
|
211
|
-
steps.push(`Add the tag yourself (${why}; init looked at ${looked}):\n${(detected ? detected.manualTag(tags) : ['Inside <head> of every page:', ...htmlTags(tags)]).map(line => ` ${line}`).join('\n')}`);
|
|
212
|
-
steps.push(`Serve the files init staged in ${posix.join(ctx.appPath, STAGED_DIR)}/ at ${servedAt} on each production origin.`);
|
|
213
|
-
steps.push(`Publish the route manifest (CAPTURE-V1 rule 4) after each build at /${MANIFEST_DIR}/current.json and`
|
|
214
|
-
+ ` /${MANIFEST_DIR}/<release>.json on each production origin; until one is published the tag records nothing.`
|
|
215
|
-
+ ` \`${manifestCommand(ctx.appPath)}\` generates it only for an app init sets up itself: once the reason above is gone,`
|
|
216
|
-
+ ' run `haystack init` again and it adds that build step.');
|
|
217
|
-
steps.push(`If production sends a Content-Security-Policy, it needs: ${cspFallbackLine()}.`);
|
|
218
|
-
if (consentStep)
|
|
219
|
-
steps.push(consentStep);
|
|
220
|
-
plan.changes = planner.changes;
|
|
221
|
-
plan.part = part(steps);
|
|
222
|
-
return plan;
|
|
223
|
-
}
|
|
224
232
|
// The tag.
|
|
225
|
-
const integration = await detected.integrate(tags);
|
|
233
|
+
const integration = detected.blocked ? { manual: detected.blocked } : await detected.integrate(tags);
|
|
226
234
|
if (isManual(integration)) {
|
|
227
235
|
steps.push(`Add the tag yourself (${integration.manual}; init looked at ${detected.looked.join(', ')}):\n${detected.manualTag(tags).map(line => ` ${line}`).join('\n')}`);
|
|
228
236
|
}
|
|
@@ -234,8 +242,17 @@ export async function planCapture(input) {
|
|
|
234
242
|
steps.push(`Serve ${posix.join(ctx.appPath, STAGED_DIR)} at ${servedAt}: ${detected.staticDir.manual}.`);
|
|
235
243
|
// The build step and where the manifest goes.
|
|
236
244
|
await planBuild(plan, planner, steps, ctx, detected, config);
|
|
237
|
-
|
|
238
|
-
|
|
245
|
+
if (detected.publishNote)
|
|
246
|
+
plan.notices.push(`Capture: ${detected.publishNote}`);
|
|
247
|
+
// A middleware or proxy that may rewrite page URLs: the routes are exact only with the user's answer.
|
|
248
|
+
if (detected.rewriting && urlRewrites === null)
|
|
249
|
+
steps.push(`Answer one question about URL rewrites: ${detected.rewriting}. ${URL_REWRITES_QUESTION}`);
|
|
250
|
+
// Routes now, so a router init cannot read is a step today rather than a manifest missing after the next build; an
|
|
251
|
+
// adapter whose routes come from the build reads them there.
|
|
252
|
+
if (detected.fromBuild) {
|
|
253
|
+
plan.notices.push(`Capture: the routes come from each production build (${detected.label}), read after it by the build step.`);
|
|
254
|
+
}
|
|
255
|
+
else if (!isManual(detected.build) && detected.build.kind !== 'rake') {
|
|
239
256
|
const routes = exactRoutes(await detected.routes({}));
|
|
240
257
|
if (isManual(routes))
|
|
241
258
|
steps.push(`Make the routes readable or publish the manifest yourself: ${routes.manual}.`);
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
* - the report (8e). InitTelemetryReport, plus the person's steps beside it: the ingest token (init never mints, prints,
|
|
21
21
|
* stores or reads one), production's environment, the start command, the deploy and the off switches (13c).
|
|
22
22
|
*/
|
|
23
|
-
import { spawnSync } from 'node:child_process';
|
|
24
|
-
import { existsSync, lstatSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
23
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
24
|
+
import { existsSync, lstatSync, readFileSync, realpathSync, statSync, writeFileSync } from 'node:fs';
|
|
25
25
|
import { createRequire } from 'node:module';
|
|
26
26
|
import { dirname, extname, isAbsolute, join, posix, relative, resolve, sep } from 'node:path';
|
|
27
27
|
import { fileURLToPath } from 'node:url';
|
|
@@ -31,8 +31,10 @@ import { gatewayFetch } from './case-batch.js';
|
|
|
31
31
|
import { CAPTURE_EVIDENCE_FRESH_HOURS, CAPTURE_STATUS_PATH, } from './capture-contract.js';
|
|
32
32
|
import { captureAnswers, planCapture } from './init-capture.js';
|
|
33
33
|
import { discoverApps } from '../capture/adapters/index.js';
|
|
34
|
+
import { CAPTURE_CONFIG_PATH, recordedApps } from '../capture/app-config.js';
|
|
34
35
|
import { loadBabel } from '../capture/js-ast.js';
|
|
35
36
|
import { lineDiff } from './init.js';
|
|
37
|
+
import { lockfileDrift, mergePnpmLockfile } from './lockfile-pin.js';
|
|
36
38
|
import { readTelemetryServerStatus } from './telemetry-token.js';
|
|
37
39
|
import { readFacts } from './verify-onboarding.js';
|
|
38
40
|
/** Lane D1: with the user's yes, init checks browser capture's registration with Haystack around the writes. */
|
|
@@ -214,15 +216,20 @@ function planDependency(gitRoot, appDir, pkg, after, need) {
|
|
|
214
216
|
const spec = `${CLI_PACKAGE}@${version}`;
|
|
215
217
|
const dev = field === 'devDependencies';
|
|
216
218
|
const workspaceRoot = isFile(join(appDir, 'pnpm-workspace.yaml')) ? ['--workspace-root'] : [];
|
|
219
|
+
// Each package manager's add, as it changes no lockfile entry but the CLI's own (lockfile-pin.ts): npm with its tree
|
|
220
|
+
// nested under the CLI (hoisting would replace packages the app already locks), pnpm through the merge.
|
|
217
221
|
const argv = manager === null ? null : {
|
|
218
222
|
pnpm: ['pnpm', 'add', '--save-exact', dev ? '--save-dev' : '--save-prod', ...workspaceRoot, spec],
|
|
219
|
-
npm: ['npm', 'install', '--save-exact', dev ? '--save-dev' : '--save-prod', spec],
|
|
223
|
+
npm: ['npm', 'install', '--save-exact', dev ? '--save-dev' : '--save-prod', '--install-strategy=nested', spec],
|
|
220
224
|
yarn: ['yarn', 'add', '--exact', ...(dev ? ['--dev'] : []), spec],
|
|
221
225
|
bun: ['bun', 'add', '--exact', ...(dev ? ['--dev'] : []), spec],
|
|
222
226
|
}[manager.name];
|
|
223
227
|
const action = promoting ? `Moved from devDependencies to dependencies at ${version}` : `Pinned to ${version}`;
|
|
228
|
+
const lockfileNote = manager?.name === 'pnpm'
|
|
229
|
+
? `, keeping every other entry of pnpm's lockfile as it was (pnpm's own add re-resolves packages the CLI shares with the app; init adds only the CLI's entries, then \`pnpm install --frozen-lockfile\`)`
|
|
230
|
+
: `, which adds only the CLI's entries to ${manager?.name}'s lockfile`;
|
|
224
231
|
const reason = argv
|
|
225
|
-
? `${why} ${action} with \`${argv.join(' ')}\` in ${inside(gitRoot, appDir)}
|
|
232
|
+
? `${why} ${action} with \`${argv.join(' ')}\` in ${inside(gitRoot, appDir)}${lockfileNote}.`
|
|
226
233
|
: `${why} ${action}: a newer release never reaches your build unless you change it.`;
|
|
227
234
|
// The package manager's own word is not taken for it: the manifest must say what the integration needs.
|
|
228
235
|
const check = () => {
|
|
@@ -235,7 +242,39 @@ function planDependency(gitRoot, appDir, pkg, after, need) {
|
|
|
235
242
|
return null;
|
|
236
243
|
};
|
|
237
244
|
const touches = manager === null ? [] : LOCKFILES.map(([lockfile]) => join(manager.root, lockfile));
|
|
238
|
-
|
|
245
|
+
const exec = manager === null || argv === null ? undefined : () => pin(manager, appDir, argv);
|
|
246
|
+
return { kind: 'change', change: change(gitRoot, pkg.path, reason, after, content, argv ? { argv, cwd: appDir, touches, check, exec } : undefined) };
|
|
247
|
+
}
|
|
248
|
+
/** The lockfile a package manager writes, in a format lockfile-pin.ts reads (bun's binary lockb aside). */
|
|
249
|
+
function textLockfile(manager) {
|
|
250
|
+
const name = { pnpm: 'pnpm-lock.yaml', npm: 'package-lock.json', yarn: 'yarn.lock', bun: 'bun.lock' }[manager.name];
|
|
251
|
+
const path = join(manager.root, name);
|
|
252
|
+
return isFile(path) ? { path, format: manager.name } : null;
|
|
253
|
+
}
|
|
254
|
+
/** Runs the add, so that the lockfile gains the CLI's entries and nothing else moves (rule 13); throws (init then restores
|
|
255
|
+
* every file) when the package manager moved anything else. */
|
|
256
|
+
function pin(manager, appDir, argv) {
|
|
257
|
+
const run = (command) => { execFileSync(command[0], command.slice(1), { cwd: appDir, stdio: ['ignore', 2, 2] }); };
|
|
258
|
+
const lockfile = textLockfile(manager);
|
|
259
|
+
const before = lockfile ? readFileSync(lockfile.path, 'utf8') : null;
|
|
260
|
+
const importer = posix.normalize(relative(manager.root, appDir).split(sep).join('/')) || '.';
|
|
261
|
+
if (manager.name === 'pnpm' && lockfile && before !== null) {
|
|
262
|
+
// pnpm resolves the CLI, init keeps the rest as it was, and the install takes exactly that lockfile (proving it).
|
|
263
|
+
run([...argv, '--lockfile-only']);
|
|
264
|
+
writeFileSync(lockfile.path, mergePnpmLockfile(before, readFileSync(lockfile.path, 'utf8'), importer, CLI_PACKAGE));
|
|
265
|
+
run(['pnpm', 'install', '--frozen-lockfile']);
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
run(argv);
|
|
269
|
+
}
|
|
270
|
+
if (!lockfile || before === null)
|
|
271
|
+
return;
|
|
272
|
+
const moved = lockfileDrift(lockfile.format, before, readFileSync(lockfile.path, 'utf8'), importer, CLI_PACKAGE);
|
|
273
|
+
if (moved.length > 0) {
|
|
274
|
+
throw new Error(`\`${argv.join(' ')}\` also changed ${moved.length} lockfile ${moved.length === 1 ? 'entry' : 'entries'} the CLI does not own`
|
|
275
|
+
+ ` (${moved.slice(0, 5).join(', ')}${moved.length > 5 ? ', …' : ''}), so init put every file back. Add ${CLI_PACKAGE} at its exact version`
|
|
276
|
+
+ ` to ${inside(manager.root, appDir) || '.'} yourself, keeping the lockfile's other entries, then run \`haystack init\` again`);
|
|
277
|
+
}
|
|
239
278
|
}
|
|
240
279
|
function isNode(value) {
|
|
241
280
|
return !!value && typeof value === 'object' && typeof value.type === 'string' && typeof value.start === 'number';
|
|
@@ -819,13 +858,18 @@ export async function planTelemetry(gitRoot, repository, options) {
|
|
|
819
858
|
// One rule for which directories are apps (capture/adapters/index.ts): a web framework an adapter claims, or a package
|
|
820
859
|
// with a start script.
|
|
821
860
|
await loadBabel();
|
|
822
|
-
|
|
861
|
+
// The user's earlier answer first: the one app a .haystack/capture.json records (with its origins and consent, which
|
|
862
|
+
// captureAnswers reads from it). Several records are as ambiguous as several apps.
|
|
863
|
+
const discovered = await discoverApps(gitRoot);
|
|
864
|
+
const recorded = recordedApps(gitRoot, discovered);
|
|
865
|
+
const apps = recorded.length > 0 ? recorded : discovered;
|
|
823
866
|
if (apps.length !== 1) {
|
|
824
867
|
// Rule 8(a): init never picks among several apps, and never guesses one where it found none.
|
|
825
868
|
const ask = apps.length === 0
|
|
826
869
|
? 'init found no app here (no web framework it recognizes and no package with a start script). If the repository has one that serves production, run '
|
|
827
870
|
+ '`haystack init` again with `--app <its directory>`.'
|
|
828
|
-
: `Ask your user which of these apps serves production: ${apps.join(', ')}
|
|
871
|
+
: `Ask your user which of these apps serves production: ${apps.join(', ')}${recorded.length > 0 ? ` (each has a ${CAPTURE_CONFIG_PATH})` : ''};`
|
|
872
|
+
+ ' then run `haystack init` again with `--app <that directory>`.';
|
|
829
873
|
const questions = [ask, ...captureQuestions(options.origins, options.consent)];
|
|
830
874
|
// A notice too, so a preview (which reports no telemetry yet) already says what to ask.
|
|
831
875
|
notices.push(`Telemetry waits for your user's answers: ${questions.join(' ')}`);
|
|
@@ -834,6 +878,8 @@ export async function planTelemetry(gitRoot, repository, options) {
|
|
|
834
878
|
captureRegistration: null };
|
|
835
879
|
}
|
|
836
880
|
appDir = realpathSync(join(gitRoot, apps[0]));
|
|
881
|
+
if (recorded.length > 0)
|
|
882
|
+
notices.push(`Telemetry: the app is ${apps[0]}, as its ${CAPTURE_CONFIG_PATH} records (\`--app\` picks another).`);
|
|
837
883
|
}
|
|
838
884
|
const app = inside(gitRoot, appDir);
|
|
839
885
|
const changes = [];
|
|
@@ -841,7 +887,7 @@ export async function planTelemetry(gitRoot, repository, options) {
|
|
|
841
887
|
const manifest = join(appDir, 'package.json');
|
|
842
888
|
// CONNECT(lane D1): browser capture (init-capture.ts) plans with the answers in effect (flags, else the ones init recorded
|
|
843
889
|
// for this app); a missing answer is a question, never a guess.
|
|
844
|
-
const answers = captureAnswers(appDir, options.origins, options.consent);
|
|
890
|
+
const answers = captureAnswers(appDir, options.origins, options.consent, options.urlRewrites);
|
|
845
891
|
const questions = captureQuestions(answers.origins, answers.consent);
|
|
846
892
|
const capture = questions.length > 0 ? null
|
|
847
893
|
: await planCapture({ gitRoot, appDir, repository, answers, registration: options.registration });
|
|
@@ -940,19 +986,36 @@ function dbProfile(repository, onboarding, notes) {
|
|
|
940
986
|
+ '`haystack db profile upload --help` says how a profile names the one it describes.'] : []),
|
|
941
987
|
] };
|
|
942
988
|
}
|
|
989
|
+
function serverPartOf(plan) {
|
|
990
|
+
return plan.server.status === 'unsupported' ? { status: 'unsupported', why: plan.server.why }
|
|
991
|
+
: plan.server.status === 'manual' ? { status: 'manual', steps: plan.server.steps, received: null } : { status: 'installed', received: null };
|
|
992
|
+
}
|
|
993
|
+
const WAITING_STEP = 'Telemetry waits for answers only your user can give: ask what the `steps` of each `manual` part say, then run `haystack init` '
|
|
994
|
+
+ 'again with them.';
|
|
995
|
+
/** Rule 8(e) before anything is changed (a preview without --yes, or no login yet): the report the apply gives, with
|
|
996
|
+
* `received` null (checking it needs Haystack), so the coding agent can show its user what telemetry will do; with the steps that need
|
|
997
|
+
* nothing from Haystack (the token and environment steps follow with --yes). */
|
|
998
|
+
export function previewTelemetry(plan, repository, notes) {
|
|
999
|
+
const steps = [];
|
|
1000
|
+
if (plan.app === null || plan.capture.status === 'manual')
|
|
1001
|
+
steps.push(WAITING_STEP);
|
|
1002
|
+
if (plan.app !== null && plan.server.status !== 'unsupported') {
|
|
1003
|
+
steps.push(`With --yes, init then says how production gets its telemetry token (\`haystack telemetry token --repo ${repository}\`, run by your`
|
|
1004
|
+
+ ' user in their own terminal) and which variables production\'s server environment needs.');
|
|
1005
|
+
}
|
|
1006
|
+
const profile = dbProfile(repository, null, notes);
|
|
1007
|
+
return { telemetry: { capture: plan.capture, server: serverPartOf(plan), dbProfileCommand: profile?.command ?? null }, steps };
|
|
1008
|
+
}
|
|
943
1009
|
/** Rule 8(e): the report, and what the person does for telemetry to arrive (the token, production's environment, the start,
|
|
944
1010
|
* the deploy) with the off switches (rule 13c). `onboarding` and `notes` feed the data profile. */
|
|
945
1011
|
export async function telemetryReport(plan, context) {
|
|
946
1012
|
const { repository, token } = context;
|
|
947
1013
|
const steps = [];
|
|
948
|
-
const serverPart = plan
|
|
949
|
-
: plan.server.status === 'manual' ? { status: 'manual', steps: plan.server.steps, received: null } : { status: 'installed', received: null };
|
|
1014
|
+
const serverPart = serverPartOf(plan);
|
|
950
1015
|
let server = serverPart;
|
|
951
1016
|
// Answers only the user can give come first: until then the rest of telemetry waits (rule 8: init never guesses).
|
|
952
|
-
if (plan.app === null || plan.capture.status === 'manual')
|
|
953
|
-
steps.push(
|
|
954
|
-
+ 'again with them.');
|
|
955
|
-
}
|
|
1017
|
+
if (plan.app === null || plan.capture.status === 'manual')
|
|
1018
|
+
steps.push(WAITING_STEP);
|
|
956
1019
|
if (plan.app !== null && plan.server.status !== 'unsupported') {
|
|
957
1020
|
const status = await readTelemetryServerStatus(repository, token);
|
|
958
1021
|
server = withReceived(serverPart, status.newestFlush === null ? null : received(status.newestFlush.storedAt, status.newestFlush.buildId));
|
package/dist/commands/init.js
CHANGED
|
@@ -54,9 +54,11 @@ export const AGENT_NOTE = [
|
|
|
54
54
|
'- Fix each bug it reports that your change caused, then run `haystack verify` again.',
|
|
55
55
|
'- If it says onboarding needs an answer, ask the user that question and file their choice with',
|
|
56
56
|
' `haystack verify answer <question-id> <choice>`.',
|
|
57
|
-
'- Read its short report. `--json` prints the
|
|
57
|
+
'- Read its short report. `--json` prints the same report as data; `--raw` prints the crawl\'s whole record (large), for scripts;',
|
|
58
58
|
' `haystack verify --help` explains the output and exit codes.',
|
|
59
59
|
'- If `haystack` is not installed, run each command as `npx -y @haystackeditor/cli@latest <command>`.',
|
|
60
|
+
'- If Haystack fails, confuses you or gets in your way, say so in a sentence with',
|
|
61
|
+
' `haystack feedback "<what happened>"` and keep going: the Haystack team sees it at once.',
|
|
60
62
|
NOTE_END,
|
|
61
63
|
].join('\n');
|
|
62
64
|
const EXIT_CODES = { planned: 2, 'login-required': 4, 'app-required': 5 };
|
|
@@ -310,7 +312,10 @@ function applyChange(gitRoot, planned) {
|
|
|
310
312
|
if (planned.run) {
|
|
311
313
|
const before = existsSync(planned.target) ? readFileSync(planned.target, 'utf8') : '';
|
|
312
314
|
// Its output goes to stderr: in --json mode stdout carries only the one document.
|
|
313
|
-
|
|
315
|
+
if (planned.run.exec)
|
|
316
|
+
planned.run.exec();
|
|
317
|
+
else
|
|
318
|
+
execFileSync(planned.run.argv[0], planned.run.argv.slice(1), { cwd: planned.run.cwd, stdio: ['ignore', 2, 2] });
|
|
314
319
|
const failed = planned.run.check?.() ?? null;
|
|
315
320
|
if (failed !== null)
|
|
316
321
|
throw new Error(`\`${planned.run.argv.join(' ')}\` ran, but ${failed}.`);
|
|
@@ -359,18 +364,26 @@ export async function initCommand(options) {
|
|
|
359
364
|
const plan = planInit(gitRoot);
|
|
360
365
|
// Telemetry is part of onboarding (CAPTURE-V1 rule 8): planned on every run; a missing answer stops only it.
|
|
361
366
|
const telemetryModule = await import('./init-telemetry.js');
|
|
362
|
-
const telemetryOptions = { app: options.app, origins: options.origin ?? [], consent: options.consent };
|
|
367
|
+
const telemetryOptions = { app: options.app, origins: options.origin ?? [], consent: options.consent, urlRewrites: options.urlRewrites };
|
|
363
368
|
let telemetryPlan = await telemetryModule.planTelemetry(gitRoot, repository, telemetryOptions);
|
|
364
369
|
const telemetryFrom = plan.changes.length;
|
|
365
370
|
plan.changes.push(...telemetryPlan.changes);
|
|
366
371
|
plan.notices.push(...telemetryPlan.notices);
|
|
367
372
|
let notesWritten = null;
|
|
368
373
|
let telemetry = null;
|
|
374
|
+
// Before the apply (a preview, or no login), the report is the preview: what telemetry will do, `received` unchecked (null).
|
|
375
|
+
const report = () => telemetry ?? telemetryModule.previewTelemetry(telemetryPlan, repository, notes);
|
|
376
|
+
const showPreview = () => {
|
|
377
|
+
const preview = telemetryModule.previewTelemetry(telemetryPlan, repository, notes);
|
|
378
|
+
for (const line of telemetryModule.formatTelemetry(preview.telemetry, preview.steps))
|
|
379
|
+
say(line);
|
|
380
|
+
say('');
|
|
381
|
+
};
|
|
369
382
|
const finish = (status, onboarding, exitCode) => {
|
|
370
383
|
if (options.json) {
|
|
371
384
|
const changes = plan.changes.map(({ path, action, reason, diff }) => ({ path, action, reason, diff }));
|
|
372
385
|
process.stdout.write(`${JSON.stringify(withSchema('init', { status, repository, changes, notices: plan.notices, notes: notesWritten,
|
|
373
|
-
onboarding, telemetry: telemetry
|
|
386
|
+
onboarding, telemetry: report().telemetry, telemetrySteps: report().steps }), null, 2)}\n`);
|
|
374
387
|
}
|
|
375
388
|
process.exitCode = exitCode;
|
|
376
389
|
};
|
|
@@ -397,6 +410,7 @@ export async function initCommand(options) {
|
|
|
397
410
|
catch (error) {
|
|
398
411
|
if (!(error instanceof NotLoggedInError))
|
|
399
412
|
throw error;
|
|
413
|
+
showPreview();
|
|
400
414
|
say(chalk.yellow('Not logged in, so nothing was changed. Run `haystack login`, then `haystack init` again.'));
|
|
401
415
|
finish('login-required', null, EXIT_CODES['login-required']);
|
|
402
416
|
return;
|
|
@@ -404,6 +418,7 @@ export async function initCommand(options) {
|
|
|
404
418
|
if (plan.changes.length > 0 && !options.yes) {
|
|
405
419
|
const interactive = !options.json && process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
406
420
|
if (!interactive || !await confirmed()) {
|
|
421
|
+
showPreview();
|
|
407
422
|
say('Nothing was changed. Run `haystack init --yes` to make these changes.');
|
|
408
423
|
finish('planned', null, EXIT_CODES.planned);
|
|
409
424
|
return;
|