@autopilot-harness/cli 0.1.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.
Files changed (93) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +55 -0
  3. package/assets/autopilot-harness-hook.mjs +193 -0
  4. package/assets/vendor/migrations/001_initial.sql +62 -0
  5. package/assets/vendor/migrations/002_pending_followup.sql +4 -0
  6. package/assets/vendor/migrations/003_reviewing_item.sql +1 -0
  7. package/assets/vendor/runtime.mjs +5524 -0
  8. package/dist/assets/autopilot-harness-hook.mjs +193 -0
  9. package/dist/assets/vendor/migrations/001_initial.sql +62 -0
  10. package/dist/assets/vendor/migrations/002_pending_followup.sql +4 -0
  11. package/dist/assets/vendor/migrations/003_reviewing_item.sql +1 -0
  12. package/dist/assets/vendor/runtime.mjs +5524 -0
  13. package/dist/bin.d.ts +3 -0
  14. package/dist/bin.d.ts.map +1 -0
  15. package/dist/bin.js +300 -0
  16. package/dist/bin.js.map +1 -0
  17. package/dist/index.d.ts +23 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +13 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/init/config-merge.d.ts +27 -0
  22. package/dist/init/config-merge.d.ts.map +1 -0
  23. package/dist/init/config-merge.js +115 -0
  24. package/dist/init/config-merge.js.map +1 -0
  25. package/dist/init/default-config.d.ts +17 -0
  26. package/dist/init/default-config.d.ts.map +1 -0
  27. package/dist/init/default-config.js +101 -0
  28. package/dist/init/default-config.js.map +1 -0
  29. package/dist/init/hooks-merge.d.ts +25 -0
  30. package/dist/init/hooks-merge.d.ts.map +1 -0
  31. package/dist/init/hooks-merge.js +134 -0
  32. package/dist/init/hooks-merge.js.map +1 -0
  33. package/dist/init/install.d.ts +31 -0
  34. package/dist/init/install.d.ts.map +1 -0
  35. package/dist/init/install.js +829 -0
  36. package/dist/init/install.js.map +1 -0
  37. package/dist/init/platforms.d.ts +60 -0
  38. package/dist/init/platforms.d.ts.map +1 -0
  39. package/dist/init/platforms.js +275 -0
  40. package/dist/init/platforms.js.map +1 -0
  41. package/dist/init/tui.d.ts +65 -0
  42. package/dist/init/tui.d.ts.map +1 -0
  43. package/dist/init/tui.js +549 -0
  44. package/dist/init/tui.js.map +1 -0
  45. package/dist/init/types.d.ts +75 -0
  46. package/dist/init/types.d.ts.map +1 -0
  47. package/dist/init/types.js +9 -0
  48. package/dist/init/types.js.map +1 -0
  49. package/dist/init/wizard-copy.d.ts +74 -0
  50. package/dist/init/wizard-copy.d.ts.map +1 -0
  51. package/dist/init/wizard-copy.js +173 -0
  52. package/dist/init/wizard-copy.js.map +1 -0
  53. package/dist/init/wizard-helpers.d.ts +103 -0
  54. package/dist/init/wizard-helpers.d.ts.map +1 -0
  55. package/dist/init/wizard-helpers.js +724 -0
  56. package/dist/init/wizard-helpers.js.map +1 -0
  57. package/dist/locale-set.d.ts +24 -0
  58. package/dist/locale-set.d.ts.map +1 -0
  59. package/dist/locale-set.js +286 -0
  60. package/dist/locale-set.js.map +1 -0
  61. package/dist/names.d.ts +5 -0
  62. package/dist/names.d.ts.map +1 -0
  63. package/dist/names.js +5 -0
  64. package/dist/names.js.map +1 -0
  65. package/dist/project-fs.d.ts +48 -0
  66. package/dist/project-fs.d.ts.map +1 -0
  67. package/dist/project-fs.js +210 -0
  68. package/dist/project-fs.js.map +1 -0
  69. package/dist/read-untrusted-file.d.ts +41 -0
  70. package/dist/read-untrusted-file.d.ts.map +1 -0
  71. package/dist/read-untrusted-file.js +362 -0
  72. package/dist/read-untrusted-file.js.map +1 -0
  73. package/dist/session.d.ts +38 -0
  74. package/dist/session.d.ts.map +1 -0
  75. package/dist/session.js +242 -0
  76. package/dist/session.js.map +1 -0
  77. package/dist/status-doctor.d.ts +22 -0
  78. package/dist/status-doctor.d.ts.map +1 -0
  79. package/dist/status-doctor.js +712 -0
  80. package/dist/status-doctor.js.map +1 -0
  81. package/dist/uninstall.d.ts +31 -0
  82. package/dist/uninstall.d.ts.map +1 -0
  83. package/dist/uninstall.js +333 -0
  84. package/dist/uninstall.js.map +1 -0
  85. package/dist/upgrade.d.ts +29 -0
  86. package/dist/upgrade.d.ts.map +1 -0
  87. package/dist/upgrade.js +290 -0
  88. package/dist/upgrade.js.map +1 -0
  89. package/dist/vendor-entry.d.ts +6 -0
  90. package/dist/vendor-entry.d.ts.map +1 -0
  91. package/dist/vendor-entry.js +16 -0
  92. package/dist/vendor-entry.js.map +1 -0
  93. package/package.json +54 -0
@@ -0,0 +1,724 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { execSync } from "node:child_process";
4
+ import { CLI_NAME, NPM_PACKAGE_NAME } from "../names.js";
5
+ import { formatBindingOptionLabel, INSTALLABLE_BINDINGS, MAX_PLATFORM_BINDINGS, mergePlatformBindings, mergedIncludesAllRequested, primaryBinding, sanitizePlatformId, } from "./platforms.js";
6
+ import { MAX_UNTRUSTED_TEXT_BYTES, readUntrustedUtf8File, writeFileReplaceSync, } from "../read-untrusted-file.js";
7
+ import { assertNotSymlink, assertParentDirInProject, assertRealpathInside, assertWrittenInsideProject, mkdirRealDirSync, resolveProjectRootOrThrow, } from "../project-fs.js";
8
+ export { assertNotSymlink, assertParentDirInProject, assertRealpathInside, assertRegularFileInsideProject, assertWrittenInsideProject, assertPairInsideOrUnlinkAll, assertPresentRealFile, mkdirRealDirSync, resolveProjectRootOrThrow, isRealRegularFile, isRealDirectory, resolveNofollowFlag, } from "../project-fs.js";
9
+ /** Cap for .gitignore / shell rc text when appending Autopilot lines. */
10
+ const MAX_APPEND_TEXT_BYTES = MAX_UNTRUSTED_TEXT_BYTES;
11
+ /**
12
+ * Write text via tmp+rename so a raced symlink is replaced, not followed
13
+ * (writeFileSync would create/write through the link target).
14
+ */
15
+ function writeTextFileReplace(filePath, contents, projectRoot) {
16
+ const dir = path.dirname(filePath);
17
+ // Match mkdirRealDirSync: if projectRoot is passed (incl. ""), validate —
18
+ // never treat blank as "no root" and skip bounds checks.
19
+ let root;
20
+ if (projectRoot !== undefined) {
21
+ root = resolveProjectRootOrThrow(projectRoot);
22
+ mkdirRealDirSync(dir, path.basename(dir) || dir, root);
23
+ // Re-check immediately before write (mkdir→write TOCTOU on parent symlink).
24
+ assertParentDirInProject(root, filePath, path.basename(dir) || dir);
25
+ }
26
+ else {
27
+ fs.mkdirSync(dir, { recursive: true });
28
+ }
29
+ writeFileReplaceSync(filePath, contents);
30
+ if (root !== undefined) {
31
+ assertWrittenInsideProject(root, filePath, path.basename(filePath) || filePath);
32
+ }
33
+ }
34
+ const PLANS_DIR_RE = /^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/;
35
+ /**
36
+ * Normalize + validate plans directory (relative, no traversal, YAML-safe).
37
+ */
38
+ export function normalizePlansDir(raw) {
39
+ const trimmed = (raw ?? "plans").trim().replace(/\/+$/, "");
40
+ const value = trimmed || "plans";
41
+ if (path.isAbsolute(value) || value.startsWith("~")) {
42
+ return { ok: false, error: "plansDir must be a relative path" };
43
+ }
44
+ if (value.includes("\0") ||
45
+ value.includes("\n") ||
46
+ value.includes("\r") ||
47
+ value.includes("\\")) {
48
+ return {
49
+ ok: false,
50
+ error: "plansDir contains invalid characters",
51
+ };
52
+ }
53
+ const parts = value.split("/");
54
+ if (parts.some((p) => p === "" || p === "." || p === "..")) {
55
+ return {
56
+ ok: false,
57
+ error: "plansDir must not contain . or .. segments",
58
+ };
59
+ }
60
+ if (!PLANS_DIR_RE.test(value)) {
61
+ return {
62
+ ok: false,
63
+ error: "plansDir may only contain letters, digits, ._- and / separators",
64
+ };
65
+ }
66
+ return { ok: true, value };
67
+ }
68
+ export function probeProject(projectRoot) {
69
+ if (typeof projectRoot !== "string" || projectRoot.trim() === "") {
70
+ return {
71
+ projectRoot: "",
72
+ hasGit: false,
73
+ branch: null,
74
+ alreadyInitialized: false,
75
+ };
76
+ }
77
+ const root = path.resolve(projectRoot.trim());
78
+ let hasGit = false;
79
+ let branch = null;
80
+ try {
81
+ execSync("git rev-parse --is-inside-work-tree", {
82
+ cwd: root,
83
+ stdio: ["ignore", "pipe", "ignore"],
84
+ timeout: 5_000,
85
+ });
86
+ hasGit = true;
87
+ }
88
+ catch {
89
+ hasGit = false;
90
+ }
91
+ if (hasGit) {
92
+ try {
93
+ branch =
94
+ execSync("git branch --show-current", {
95
+ cwd: root,
96
+ encoding: "utf8",
97
+ stdio: ["ignore", "pipe", "ignore"],
98
+ timeout: 5_000,
99
+ }).trim() || null;
100
+ }
101
+ catch {
102
+ // Detached / old git / empty repo: still a git work tree.
103
+ branch = null;
104
+ }
105
+ }
106
+ let alreadyInitialized = false;
107
+ try {
108
+ const cfg = path.join(root, ".autopilot", "config.yml");
109
+ const st = fs.lstatSync(cfg);
110
+ // Dangling/pointing symlinks are not a real init.
111
+ alreadyInitialized = st.isFile() && !st.isSymbolicLink();
112
+ }
113
+ catch {
114
+ alreadyInitialized = false;
115
+ }
116
+ return {
117
+ projectRoot: root,
118
+ hasGit,
119
+ branch,
120
+ alreadyInitialized,
121
+ };
122
+ }
123
+ function appendGitignoreLines(projectRoot, comment, lines) {
124
+ const root = resolveProjectRootOrThrow(projectRoot);
125
+ const gi = path.join(root, ".gitignore");
126
+ let body = "";
127
+ try {
128
+ // O_NOFOLLOW read — existsSync/lstat+readFileSync can race or miss dangling.
129
+ body = readUntrustedUtf8File(gi, MAX_APPEND_TEXT_BYTES, ".gitignore");
130
+ }
131
+ catch (err) {
132
+ const code = err?.code;
133
+ if (code === "ENOENT") {
134
+ body = "";
135
+ }
136
+ else {
137
+ throw err;
138
+ }
139
+ }
140
+ const existing = new Set(body
141
+ .split(/\r?\n/)
142
+ .map((l) => l.trim())
143
+ .filter(Boolean));
144
+ const toAdd = lines.filter((l) => !existing.has(l) && !existing.has(`/${l}`));
145
+ if (toAdd.length === 0)
146
+ return null;
147
+ if (body.length > 0 && !body.endsWith("\n"))
148
+ body += "\n";
149
+ const commentLine = `# ${comment}`;
150
+ // Prefer extending an existing section instead of duplicating the header
151
+ // (e.g. upgrade adds `.autopilot/state.db.bak*` to a prior runtime block).
152
+ if (existing.has(commentLine)) {
153
+ const rows = body.split(/\r?\n/);
154
+ let commentIdx = -1;
155
+ for (let i = 0; i < rows.length; i++) {
156
+ if (rows[i].trim() === commentLine)
157
+ commentIdx = i;
158
+ }
159
+ let insertAt = rows.length;
160
+ if (commentIdx >= 0) {
161
+ insertAt = commentIdx + 1;
162
+ while (insertAt < rows.length) {
163
+ const t = rows[insertAt].trim();
164
+ // End of section: blank line or next comment.
165
+ if (t === "" || t.startsWith("#"))
166
+ break;
167
+ insertAt++;
168
+ }
169
+ }
170
+ rows.splice(insertAt, 0, ...toAdd);
171
+ body = rows.join("\n");
172
+ if (!body.endsWith("\n"))
173
+ body += "\n";
174
+ }
175
+ else {
176
+ body += `\n${commentLine}\n${toAdd.map((l) => `${l}\n`).join("")}`;
177
+ }
178
+ assertNotSymlink(gi, ".gitignore");
179
+ writeTextFileReplace(gi, body, root);
180
+ return path.relative(root, gi);
181
+ }
182
+ /** Append plans dir to .gitignore once (dedupe). */
183
+ export function applyPlansGitignore(projectRoot, plansDir = "plans") {
184
+ const normalized = normalizePlansDir(plansDir);
185
+ if (!normalized.ok) {
186
+ throw new Error(normalized.error);
187
+ }
188
+ const entry = `${normalized.value}/`;
189
+ return appendGitignoreLines(projectRoot, "Autopilot plans (local only)", [
190
+ entry,
191
+ ]);
192
+ }
193
+ /** Always ignore runtime Autopilot artifacts (not config/hooks). */
194
+ export function applyAutopilotRuntimeGitignore(projectRoot) {
195
+ return appendGitignoreLines(projectRoot, "Autopilot runtime", [
196
+ ".autopilot/state.db",
197
+ ".autopilot/state.db-*",
198
+ ".autopilot/state.db.bak*",
199
+ ".autopilot/worktrees/",
200
+ ".autopilot/verify-last.json",
201
+ ".autopilot/logs/",
202
+ ]);
203
+ }
204
+ /** Map wizard answers → installInitYes options (+ post-install alias). */
205
+ export function answersToInstallOptions(answers) {
206
+ const raw = answers.platforms && answers.platforms.length > 0
207
+ ? answers.platforms
208
+ : [
209
+ {
210
+ id: answers.platform || "cursor",
211
+ surface: answers.surface || "ide",
212
+ },
213
+ ];
214
+ const platforms = mergePlatformBindings([], raw);
215
+ // Do not pre-truncate then hand a capped list to install (that would bypass
216
+ // installInitYes platformsExceedCap). Fail closed here instead.
217
+ if (!mergedIncludesAllRequested(platforms, raw)) {
218
+ throw new Error(`platforms list exceeds cap of ${MAX_PLATFORM_BINDINGS} unique entries; trim the list and retry`);
219
+ }
220
+ const primary = primaryBinding(platforms);
221
+ return {
222
+ projectRoot: answers.projectRoot,
223
+ platform: primary.id,
224
+ surface: primary.surface,
225
+ platforms,
226
+ mergePlatforms: Boolean(answers.mergePlatforms),
227
+ locale: answers.locale,
228
+ force: answers.force,
229
+ packageVersion: answers.packageVersion,
230
+ plansDir: answers.plansDir,
231
+ plansGit: answers.plansGit,
232
+ verifyEnabled: answers.verifyEnabled,
233
+ maxErrorsBeforePause: answers.maxErrorsBeforePause,
234
+ reviewScope: answers.reviewScope,
235
+ writeQuickstart: true,
236
+ };
237
+ }
238
+ /** POSIX single-quote a string for safe embedding in shell. */
239
+ export function shellSingleQuote(value) {
240
+ return `'${value.replace(/'/g, `'\"'\"'`)}'`;
241
+ }
242
+ /** Refuse path chars that break shell rc / single-line docs embedding. */
243
+ const SHELL_UNSAFE_PATH_CHARS = /[\0\n\r\u2028\u2029]/;
244
+ /**
245
+ * Absolute path of the running CLI entry (e.g. …/dist/bin.js), or null.
246
+ * Used so local checkouts write a working alias before the package is on npm.
247
+ */
248
+ export function tryResolveRunningCliScript() {
249
+ const argv1 = process.argv[1];
250
+ if (typeof argv1 !== "string" || argv1.trim() === "")
251
+ return null;
252
+ try {
253
+ const abs = fs.realpathSync(path.resolve(argv1));
254
+ if (!fs.statSync(abs).isFile())
255
+ return null;
256
+ if (SHELL_UNSAFE_PATH_CHARS.test(abs))
257
+ return null;
258
+ if (!isTrustedCliEntrypoint(abs))
259
+ return null;
260
+ return abs;
261
+ }
262
+ catch {
263
+ return null;
264
+ }
265
+ }
266
+ /**
267
+ * argv[1] is often some other .js under test runners / wrappers.
268
+ * Only accept known Autopilot CLI entry names (and bin.js under our package paths).
269
+ *
270
+ * Deliberately small allowlist: monorepo `packages/cli/.../bin.js` (no
271
+ * `node_modules/` in the path), npm `node_modules/<pkg>/.../bin.js`, and
272
+ * project `node_modules/.bin` shims. Global / version-manager shims fall
273
+ * through to `npx ${NPM_PACKAGE_NAME}`.
274
+ */
275
+ const escRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
276
+ // Matched against lowercased paths (see isTrustedCliEntrypoint).
277
+ const TRUSTED_MONOREPO_CLI_BIN = /(^|\/)packages\/cli\/(dist|src)\/bin\.js$/;
278
+ const TRUSTED_NPM_SCOPED_CLI_BIN = new RegExp(`(^|/)node_modules/${escRe(NPM_PACKAGE_NAME.toLowerCase())}/(dist|src)/bin\\.js$`);
279
+ const TRUSTED_NPM_LEGACY_CLI_BIN = new RegExp(`(^|/)node_modules/${escRe(CLI_NAME.toLowerCase())}/(dist|src)/bin\\.js$`);
280
+ export function isTrustedCliEntrypoint(absPath) {
281
+ if (typeof absPath !== "string" || !absPath.trim())
282
+ return false;
283
+ // Same refuse set as tryResolveRunningCliScript (shell rc / docs).
284
+ if (SHELL_UNSAFE_PATH_CHARS.test(absPath))
285
+ return false;
286
+ // Normalize before basename — on POSIX, path.basename ignores `\`, so a
287
+ // Windows-style path would otherwise never look like `bin.js`.
288
+ const norm = absPath.split(/[/\\]+/).filter(Boolean).join("/");
289
+ // Match path segments case-insensitively (Windows / macOS default volumes).
290
+ const normLower = norm.toLowerCase();
291
+ const base = path.posix.basename(normLower);
292
+ if (base === CLI_NAME ||
293
+ base === `${CLI_NAME}.js` ||
294
+ base === `${CLI_NAME}.mjs`) {
295
+ // pnpm/npm project shims only (not arbitrary …/bin/<name>).
296
+ return /(^|\/)node_modules\/\.bin\/[^/]+$/.test(normLower);
297
+ }
298
+ if (base !== "bin.js")
299
+ return false;
300
+ // Local monorepo checkout, or npm layout under node_modules/ (scoped / legacy).
301
+ // Package-name paths require a node_modules/<pkg>/ segment. Monorepo paths
302
+ // must not contain node_modules/ at all (blocks …/node_modules/**/packages/cli
303
+ // decoys that only share the packages/cli/.../bin.js suffix).
304
+ return ((TRUSTED_MONOREPO_CLI_BIN.test(normLower) &&
305
+ !/(^|\/)node_modules\//.test(normLower)) ||
306
+ TRUSTED_NPM_SCOPED_CLI_BIN.test(normLower) ||
307
+ TRUSTED_NPM_LEGACY_CLI_BIN.test(normLower));
308
+ }
309
+ /**
310
+ * Runnable CLI command for docs / cheat sheets.
311
+ * Prefers `node <this-bin>`; falls back to `npx` + {@link NPM_PACKAGE_NAME}.
312
+ */
313
+ export function resolveCliCommand() {
314
+ const script = tryResolveRunningCliScript();
315
+ if (script)
316
+ return `node ${shellSingleQuote(script)}`;
317
+ return `npx ${NPM_PACKAGE_NAME}`;
318
+ }
319
+ /**
320
+ * Shell rc snippet that defines `autopilot` as a function (not `alias=`).
321
+ * Alias RHS quoting breaks on paths containing `'`; a function body can embed
322
+ * the path via {@link shellSingleQuote} without source-time `$()` expansion.
323
+ */
324
+ export function autopilotShellAliasLine() {
325
+ const script = tryResolveRunningCliScript();
326
+ if (script) {
327
+ return `autopilot() { command node ${shellSingleQuote(script)} "$@"; }`;
328
+ }
329
+ return `autopilot() { command npx ${NPM_PACKAGE_NAME} "$@"; }`;
330
+ }
331
+ function shellRcDefinesAutopilot(body) {
332
+ // Line-anchored only — avoid false positives from comments / prose that
333
+ // mention `alias autopilot=` mid-line.
334
+ return (/(?:^|\n)\s*alias\s+autopilot=/.test(body) ||
335
+ /(?:^|\n)\s*autopilot\s*\(\)/.test(body) ||
336
+ /(?:^|\n)\s*function\s+autopilot\b/.test(body));
337
+ }
338
+ /** Append shell shortcut with dedupe. Returns path + whether a line was added. */
339
+ export function appendShellAlias(target) {
340
+ const home = process.env.HOME ?? process.env.USERPROFILE;
341
+ if (!home) {
342
+ throw new Error("HOME is not set; cannot write shell shortcut");
343
+ }
344
+ const file = target === "zshrc"
345
+ ? path.join(home, ".zshrc")
346
+ : path.join(home, ".bashrc");
347
+ let body = "";
348
+ try {
349
+ body = readUntrustedUtf8File(file, MAX_APPEND_TEXT_BYTES, path.basename(file));
350
+ }
351
+ catch (err) {
352
+ const code = err?.code;
353
+ if (code === "ENOENT") {
354
+ body = "";
355
+ }
356
+ else {
357
+ throw err;
358
+ }
359
+ }
360
+ if (shellRcDefinesAutopilot(body)) {
361
+ return { path: file, added: false };
362
+ }
363
+ if (body.length > 0 && !body.endsWith("\n"))
364
+ body += "\n";
365
+ body += `\n# Autopilot Harness\n${autopilotShellAliasLine()}\n`;
366
+ assertNotSymlink(file, path.basename(file));
367
+ writeTextFileReplace(file, body);
368
+ return { path: file, added: true };
369
+ }
370
+ /**
371
+ * Strip C0 controls / DEL and cap length so a hostile config.yml `platform`
372
+ * cannot inject control chars or megabyte strings into terminal tips.
373
+ * Allowlist first, then lowercase + length cap, so junk prefixes do not
374
+ * truncate away a real id (e.g. "***…***cursor" → "cursor").
375
+ */
376
+ // sanitizePlatformId lives in ./platforms.js (shared with config parsing).
377
+ /**
378
+ * Human label for an agent host id (init/upgrade tips).
379
+ * Init CLI copy is English; extend as new platforms ship.
380
+ */
381
+ export function formatHostDisplayName(platform) {
382
+ const id = sanitizePlatformId(platform);
383
+ switch (id) {
384
+ case "cursor":
385
+ return "Cursor";
386
+ case "claude-code":
387
+ return "Claude Code";
388
+ case "kimi-code":
389
+ return "Kimi Code";
390
+ default: {
391
+ const parts = id.split(/[-_]/).filter(Boolean);
392
+ if (parts.length === 0)
393
+ return "your agent host";
394
+ return parts
395
+ .map((p) => p.charAt(0).toUpperCase() + p.slice(1))
396
+ .join(" ");
397
+ }
398
+ }
399
+ }
400
+ /** Init/upgrade outro — always English (init UX language). */
401
+ export function formatPostInstallOutro(platformOrPlatforms) {
402
+ const ids = (typeof platformOrPlatforms === "string"
403
+ ? [platformOrPlatforms]
404
+ : [...platformOrPlatforms])
405
+ .map(sanitizePlatformId)
406
+ .filter(Boolean);
407
+ if (ids.length === 0) {
408
+ return "You're all set — try /autopilot-on in your agent host.";
409
+ }
410
+ if (ids.length === 1) {
411
+ return `You're all set — try /autopilot-on in ${formatHostDisplayName(ids[0])}.`;
412
+ }
413
+ const names = ids.map((id) => formatHostDisplayName(id)).join(", ");
414
+ return `You're all set — try /autopilot-on in ${names}.`;
415
+ }
416
+ /**
417
+ * Host-specific activation tips after hooks/skills install.
418
+ * Always English (init UX language). Install what you chose → tip for that host.
419
+ */
420
+ export function formatHostActivationTips(platformOrPlatforms) {
421
+ const ids = (typeof platformOrPlatforms === "string"
422
+ ? [platformOrPlatforms]
423
+ : [...platformOrPlatforms])
424
+ .map(sanitizePlatformId)
425
+ .filter(Boolean);
426
+ const tips = [];
427
+ const seen = new Set();
428
+ for (const id of ids) {
429
+ if (seen.has(id))
430
+ continue;
431
+ seen.add(id);
432
+ const host = formatHostDisplayName(id);
433
+ if (id === "cursor") {
434
+ tips.push(`If /autopilot-* skills or Autopilot hooks do not appear in ${host}: run Developer: Reload Window, or start a new Agent chat.`);
435
+ }
436
+ else {
437
+ tips.push(`If Autopilot skills or hooks do not appear in ${host}: reload or restart ${host}, then open a new agent session.`);
438
+ }
439
+ }
440
+ return tips;
441
+ }
442
+ /** Non-interactive init / upgrade footer lines (English). */
443
+ export function formatPostInstallFooter(platformOrPlatforms) {
444
+ return [
445
+ formatPostInstallOutro(platformOrPlatforms),
446
+ ...formatHostActivationTips(platformOrPlatforms),
447
+ ];
448
+ }
449
+ /** Installable host options for interactive multiselect (English). */
450
+ export function installableHostOptions() {
451
+ return INSTALLABLE_BINDINGS.map((b) => ({
452
+ value: `${b.id}:${b.surface}`,
453
+ label: formatBindingOptionLabel(b),
454
+ binding: { id: b.id, surface: b.surface },
455
+ }));
456
+ }
457
+ /** Plain (no markdown) host tips — cheat sheet / footer. */
458
+ function hostActivationPlainLines(locale, platformOrPlatforms) {
459
+ const ids = (typeof platformOrPlatforms === "string"
460
+ ? [platformOrPlatforms]
461
+ : [...platformOrPlatforms])
462
+ .map(sanitizePlatformId)
463
+ .filter(Boolean);
464
+ const lines = [];
465
+ const seen = new Set();
466
+ for (const id of ids.length > 0 ? ids : ["cursor"]) {
467
+ if (seen.has(id))
468
+ continue;
469
+ seen.add(id);
470
+ const host = formatHostDisplayName(id);
471
+ if (locale === "zh-CN") {
472
+ if (id === "cursor") {
473
+ lines.push(`在 ${host} 中试用 /autopilot-on。`, `若 skills / hooks 未出现:执行 Developer: Reload Window,或新开一条 Agent 对话。`);
474
+ }
475
+ else {
476
+ lines.push(`在 ${host} 中试用 /autopilot-on。`, `若 skills / hooks 未出现:重载或重启 ${host},再开新会话。`);
477
+ }
478
+ }
479
+ else if (id === "cursor") {
480
+ lines.push(`Try /autopilot-on in ${host}.`, `If skills or hooks are missing: Developer: Reload Window, or start a new Agent chat.`);
481
+ }
482
+ else {
483
+ lines.push(`Try /autopilot-on in ${host}.`, `If skills or hooks are missing: reload or restart ${host}, then open a new agent session.`);
484
+ }
485
+ }
486
+ return lines;
487
+ }
488
+ /** Markdown bullets for docs/autopilot/quickstart.md. */
489
+ function hostActivationDocLines(locale, platform) {
490
+ return hostActivationPlainLines(locale, platform).map((l) => l
491
+ .replace("/autopilot-on", "`/autopilot-on`")
492
+ .replace("Developer: Reload Window", "`Developer: Reload Window`"));
493
+ }
494
+ export function writeQuickstart(projectRoot, locale, plansDir = "plans", platform = "cursor") {
495
+ const root = resolveProjectRootOrThrow(projectRoot);
496
+ const normalized = normalizePlansDir(plansDir);
497
+ const plansLabel = normalized.ok ? normalized.value : "plans";
498
+ const docsDir = path.join(root, "docs");
499
+ const destDir = path.join(docsDir, "autopilot");
500
+ assertNotSymlink(docsDir, "docs/");
501
+ assertNotSymlink(destDir, "docs/autopilot/");
502
+ mkdirRealDirSync(destDir, "docs/autopilot/", root);
503
+ assertRealpathInside(root, destDir, "docs/autopilot/");
504
+ const dest = path.join(destDir, "quickstart.md");
505
+ try {
506
+ const st = fs.lstatSync(dest);
507
+ if (st.isSymbolicLink()) {
508
+ throw new Error("docs/autopilot/quickstart.md is a symlink; refusing to open");
509
+ }
510
+ if (st.isFile())
511
+ return null;
512
+ throw new Error("docs/autopilot/quickstart.md exists and is not a regular file");
513
+ }
514
+ catch (err) {
515
+ const code = err?.code;
516
+ if (code !== "ENOENT")
517
+ throw err;
518
+ }
519
+ const host = formatHostDisplayName(platform);
520
+ const afterInstall = hostActivationDocLines(locale, platform);
521
+ const body = locale === "zh-CN"
522
+ ? `# Autopilot 快速开始
523
+
524
+ 命令速查 + 每步产物。
525
+
526
+ ## 推荐流程(产物)
527
+
528
+ | 步骤 | 你做什么 | Autopilot 做什么 | 产物 |
529
+ |------|----------|------------------|------|
530
+ | **1. 规划** | \`/autopilot-on\`(可带需求描述);逐轮回答 grill | 写 \`${plansLabel}/<slug>/\`(可改文档),**不写产品代码** | \`brief.md\`、\`plan.md\`、\`checklist.md\` |
531
+ | **2. 执行** | \`/autopilot-run\`(或带 \`<slug>\`) | 一项一项:实现 → 自审修复 → 多角度确认 → 勾选推进 | 该项代码/文档;推进/完成时 dirty 则本地 commit(干净则跳过;确认轮不 commit;默认不自动 push) |
532
+ | **3. 完成** | — | 勾选最后一项;dirty 则本地 commit(干净则跳过;默认不自动 push);checklist 清空后停止 | 该轨结束 |
533
+
534
+ ## Planning
535
+
536
+ 推荐:在 ${host} 中使用 \`/autopilot-on\` 或 \`/autopilot-on <需求描述>\`
537
+
538
+ 也可:行首 \`Autopilot ON\` / \`开启自动驾驶\`
539
+
540
+ ## Executing
541
+
542
+ \`/autopilot-run\` 或 \`/autopilot-run <slug>\`
543
+
544
+ 也可:\`Autopilot RUN\` / \`开始执行\`
545
+
546
+ ## 暂停 / 恢复 / 改方案
547
+
548
+ - 暂停:\`/autopilot-off\` 或行首 \`Autopilot OFF\` / \`关闭自动驾驶\` — 本会话 paused;不推进 checklist,也不跑自审,直到 resume(phase 通常不变;\`done\` → \`idle\`)。
549
+ - 恢复:\`/autopilot-resume\` 或 \`/autopilot-resume <slug>\`(新聊天可认领旧轨);也可行首 \`Autopilot RESUME\` / \`继续执行\` — 清 pause,**保留**自审链进度;多轨执行中时用 \`<slug>\` 指定。认领后以**本聊天**为执行会话;勿在旧聊天继续跑同一轨。认领优先未 pause 的执行会话,也可回退到唯一一条**已 pause** 的执行轨(旧聊天已死时恢复)。
550
+ - 改方案:\`/autopilot-replan\` 或行首 \`Autopilot REPLAN\` / \`修改方案\` — 回到 planning,**重置**自审链;只改 \`plan.md\` 与未勾选项,勿静默删已完成 \`[x]\`;改完再 \`/autopilot-run\`。
551
+
552
+ ## 终端
553
+
554
+ **今天(尚未上公共 npm):** 用已构建的二进制(把路径换成你的 harness 克隆;\`cwd\` = 目标项目):
555
+
556
+ \`\`\`bash
557
+ node /path/to/autopilot-harness/packages/cli/dist/bin.js status
558
+ node /path/to/autopilot-harness/packages/cli/dist/bin.js doctor
559
+ node /path/to/autopilot-harness/packages/cli/dist/bin.js upgrade --dry-run
560
+ \`\`\`
561
+
562
+ **发布到 npm 之后:** \`npx ${NPM_PACKAGE_NAME} …\`(scoped 包名——不要用不存在的裸 \`npx ${CLI_NAME}\`)。未上架前用「今天」路径。
563
+
564
+ ## 安装后
565
+
566
+ ${afterInstall.map((l) => `- ${l}`).join("\n")}
567
+
568
+ ## 自审范围(\`review.scope\`)
569
+
570
+ 写在 \`.autopilot/config.yml\`:
571
+
572
+ | 取值 | 含义 |
573
+ |------|------|
574
+ | **\`executing_only\`**(默认) | 仅在 \`/autopilot-run\`(checklist 执行中)且改了产品代码后,才走修复 → 多角度确认 |
575
+ | **\`project\`** | **任意**产品代码编辑都会自审——**不需要**先 ON / RUN |
576
+
577
+ 产品代码排除命中 \`.autopilotignore\` 的路径,以及**未跟踪且被 \`.gitignore\` 忽略**的路径。暂停 / OFF 期间不跑自审链,需 resume。
578
+
579
+ 只开 \`/autopilot-on\` **不会**启动自审(规划只写方案/文档)。\`project\` 且**未在** checklist 执行中(含仍在 planning)时,确认链以 **自审完成** 结束(不勾选推进 checklist);在 RUN 执行中则仍按项推进/完成。若已有全局 Cursor 自审 hook,慎与 \`project\` 叠用(可能双重注入)。各宿主自带的 Plan 模式与 Autopilot 无关,目前未对接。
580
+
581
+ ## 排障速查
582
+
583
+ - skills / hooks 未出现:\`Developer: Reload Window\`,或新开 Agent 对话;再跑 \`doctor\`。
584
+ - 自审中途停住:确认 Autopilot stop 带 \`loop_limit: null\`(缺则 \`upgrade\`);Cursor 默认 stop 上限为 5。
585
+ - \`project\` 下双重 followup:关掉 \`~/.cursor\` 全局自审,或只用 Autopilot。
586
+ - 改了代码却不自审:检查 \`review.scope\`、是否 paused/OFF、路径是否被 \`.autopilotignore\` / 未跟踪+\`.gitignore\` 排除。
587
+
588
+ 方案与清单在 \`${plansLabel}/<slug>/\`(权威进度是 \`checklist.md\`)。
589
+ `
590
+ : `# Autopilot quickstart
591
+
592
+ Command cheat sheet + per-step artifacts.
593
+
594
+ ## Recommended flow (artifacts)
595
+
596
+ | Step | You do | Autopilot does | Artifacts |
597
+ |------|--------|----------------|-----------|
598
+ | **1. Plan** | \`/autopilot-on\` (optional description); reply to each grill round | Writes \`${plansLabel}/<slug>/\` (may edit docs); **no product code** | \`brief.md\`, \`plan.md\`, \`checklist.md\` |
599
+ | **2. Run** | \`/autopilot-run\` (or with \`<slug>\`) | One item at a time: implement → fix → multi-lens confirm → advance | Code/docs for that item; on advance/done, local commit if dirty (skip if clean; confirm rounds do not commit; no auto-push) |
600
+ | **3. Done** | — | Marks the last item; local commit if dirty (skip if clean; no auto-push); stops when the checklist is clear | Track complete |
601
+
602
+ ## Planning
603
+
604
+ Preferred: in ${host}, \`/autopilot-on\` or \`/autopilot-on <what to build>\`
605
+
606
+ Also: line-start \`Autopilot ON\`
607
+
608
+ ## Executing
609
+
610
+ \`/autopilot-run\` or \`/autopilot-run <slug>\`
611
+
612
+ Also: \`Autopilot RUN\`
613
+
614
+ ## Pause / resume / replan
615
+
616
+ - Pause: \`/autopilot-off\` or line-start \`Autopilot OFF\` — pauses this conversation; no checklist advance and no self-review until resume (phase usually unchanged; \`done\` → \`idle\`).
617
+ - Resume: \`/autopilot-resume\` or \`/autopilot-resume <slug>\` (new chat can claim a track); also line-start \`Autopilot RESUME\` — clears pause, **keeps** the review chain; use \`<slug>\` when several tracks are executing. After a claim, **this** chat owns the session; do not keep executing the same track in the old chat. Claim prefers an unpaused executing worker, and can fall back to a single **paused** executing session (dead-chat recovery).
618
+ - Replan: \`/autopilot-replan\` or line-start \`Autopilot REPLAN\` — returns to planning and **resets** the review chain; revise \`plan.md\` and unchecked items only (do not silently delete completed \`[x]\`); then \`/autopilot-run\` when ready.
619
+
620
+ ## Terminal
621
+
622
+ **Today (not on public npm yet):** use the built binary (replace the path with your harness clone; \`cwd\` = the app):
623
+
624
+ \`\`\`bash
625
+ node /path/to/autopilot-harness/packages/cli/dist/bin.js status
626
+ node /path/to/autopilot-harness/packages/cli/dist/bin.js doctor
627
+ node /path/to/autopilot-harness/packages/cli/dist/bin.js upgrade --dry-run
628
+ \`\`\`
629
+
630
+ **After npm publish:** \`npx ${NPM_PACKAGE_NAME} …\` (scoped name — not bare \`npx ${CLI_NAME}\`). Until then, use the **Today** path.
631
+
632
+ ## After install
633
+
634
+ ${afterInstall.map((l) => `- ${l}`).join("\n")}
635
+
636
+ ## Self-review scope (\`review.scope\`)
637
+
638
+ In \`.autopilot/config.yml\`:
639
+
640
+ | Value | Meaning |
641
+ |-------|---------|
642
+ | **\`executing_only\`** (default) | Fix → confirm only after \`/autopilot-run\` (checklist executing) + product-code edits |
643
+ | **\`project\`** | Fix → confirm on **any** product-code edit — **no** ON/RUN required |
644
+
645
+ Product-code paths exclude \`.autopilotignore\` hits and **untracked** \`.gitignore\` hits. Paused/OFF skips the chain until resume.
646
+
647
+ \`/autopilot-on\` by itself does **not** start self-review (planning writes plans/docs only). With \`project\` and **not** checklist-executing (including still planning), the chain ends at **review complete** (no checklist advance); during RUN it still advances/done as usual. Avoid stacking a global Cursor self-review hook with \`project\` (double injection). Host Plan modes are separate; Autopilot does not bridge them yet.
648
+
649
+ ## Troubleshooting
650
+
651
+ - Skills / hooks missing: \`Developer: Reload Window\`, or a new Agent chat; then run \`doctor\`.
652
+ - Review stops mid-chain: ensure Autopilot stop has \`loop_limit: null\` (run \`upgrade\` if missing); Cursor defaults stop hooks to 5.
653
+ - Double followups under \`project\`: disable \`~/.cursor\` global self-review, or use Autopilot alone.
654
+ - Edited code but no self-review: check \`review.scope\`, paused/OFF, and whether the path is excluded by \`.autopilotignore\` or untracked+\`.gitignore\`.
655
+
656
+ Artifacts live under \`${plansLabel}/<slug>/\` (progress authority is \`checklist.md\`).
657
+ `;
658
+ assertNotSymlink(dest, "docs/autopilot/quickstart.md");
659
+ writeTextFileReplace(dest, body, root);
660
+ return path.relative(root, dest);
661
+ }
662
+ export function formatCheatSheet(locale, cliCommand = resolveCliCommand(), plansDir = "plans", platformOrPlatforms = "cursor") {
663
+ const normalized = normalizePlansDir(plansDir);
664
+ const plansLabel = normalized.ok ? normalized.value : "plans";
665
+ const ids = (typeof platformOrPlatforms === "string"
666
+ ? [platformOrPlatforms]
667
+ : [...platformOrPlatforms])
668
+ .map(sanitizePlatformId)
669
+ .filter(Boolean);
670
+ const host = ids.length <= 1
671
+ ? formatHostDisplayName(ids[0] ?? "cursor")
672
+ : ids.map((id) => formatHostDisplayName(id)).join(" / ");
673
+ if (locale === "zh-CN") {
674
+ return [
675
+ "── 新开任务(Planning)──────────────────",
676
+ ` 推荐:在 ${host} 中 /autopilot-on`,
677
+ " /autopilot-on 我想做:<描述需求>",
678
+ " 也可:Autopilot ON",
679
+ "",
680
+ "── 开始执行 ─────────────────────────────",
681
+ " /autopilot-run",
682
+ " /autopilot-run <slug>",
683
+ "",
684
+ "── 暂停 / 恢复 / 改方案 ─────────────────",
685
+ " Autopilot OFF · RESUME · REPLAN",
686
+ "",
687
+ "── 终端 ─────────────────────────────────",
688
+ ` ${cliCommand} status`,
689
+ ` ${cliCommand} doctor`,
690
+ ` ${cliCommand} session list`,
691
+ ` ${cliCommand} locale set en`,
692
+ "",
693
+ "── 生效提示 ─────────────────────────────",
694
+ ...hostActivationPlainLines(locale, ids).map((l) => ` ${l}`),
695
+ "",
696
+ ` 详细:docs/autopilot/quickstart.md · ${plansLabel}/README.md`,
697
+ ];
698
+ }
699
+ return [
700
+ "── Planning ─────────────────────────────",
701
+ ` Preferred: in ${host}, /autopilot-on`,
702
+ " /autopilot-on <what to build>",
703
+ " Also: Autopilot ON",
704
+ "",
705
+ "── Executing ────────────────────────────",
706
+ " /autopilot-run",
707
+ " /autopilot-run <slug>",
708
+ "",
709
+ "── Pause / resume / replan ──────────────",
710
+ " Autopilot OFF · RESUME · REPLAN",
711
+ "",
712
+ "── Terminal ─────────────────────────────",
713
+ ` ${cliCommand} status`,
714
+ ` ${cliCommand} doctor`,
715
+ ` ${cliCommand} session list`,
716
+ ` ${cliCommand} locale set zh-CN`,
717
+ "",
718
+ "── After install ────────────────────────",
719
+ ...hostActivationPlainLines(locale, ids).map((l) => ` ${l}`),
720
+ "",
721
+ ` See: docs/autopilot/quickstart.md · ${plansLabel}/README.md`,
722
+ ];
723
+ }
724
+ //# sourceMappingURL=wizard-helpers.js.map