@reticlehq/init 3.7.0 → 3.8.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 (45) hide show
  1. package/dist/detect/detect.d.ts +9 -0
  2. package/dist/detect/detect.js +5 -1
  3. package/dist/detect/dev-server-ports.d.ts +2 -0
  4. package/dist/detect/dev-server-ports.js +20 -0
  5. package/dist/detect/prettier-config.d.ts +21 -0
  6. package/dist/detect/prettier-config.js +38 -0
  7. package/dist/detect/workspace-redirect.js +23 -8
  8. package/dist/diagnose/closing-hint.d.ts +5 -0
  9. package/dist/diagnose/closing-hint.js +18 -11
  10. package/dist/gather-plan-input.js +70 -18
  11. package/dist/host.d.ts +6 -0
  12. package/dist/index.d.ts +8 -2
  13. package/dist/index.js +8 -2
  14. package/dist/no-package-json.js +62 -14
  15. package/dist/patch/next-patch.d.ts +19 -11
  16. package/dist/patch/next-patch.js +33 -6
  17. package/dist/patch/prettier-ignore.d.ts +34 -0
  18. package/dist/patch/prettier-ignore.js +49 -0
  19. package/dist/patch/python-snippets.d.ts +22 -0
  20. package/dist/patch/python-snippets.js +113 -0
  21. package/dist/patch/snippets.d.ts +9 -30
  22. package/dist/patch/snippets.js +40 -103
  23. package/dist/patch/static-page.js +3 -5
  24. package/dist/patch/vite-config.js +11 -0
  25. package/dist/plan/connect-steps.d.ts +1 -0
  26. package/dist/plan/connect-steps.js +1 -0
  27. package/dist/plan/framework-adapter.d.ts +10 -0
  28. package/dist/plan/framework-adapter.js +120 -1
  29. package/dist/plan/notices.js +1 -1
  30. package/dist/plan/plan-framework.js +101 -32
  31. package/dist/plan/plan-types.d.ts +27 -0
  32. package/dist/plan/plan.d.ts +1 -10
  33. package/dist/plan/plan.js +128 -131
  34. package/dist/plan/stop-hook-step.d.ts +11 -0
  35. package/dist/plan/stop-hook-step.js +82 -6
  36. package/dist/project/agent-rules.d.ts +1 -1
  37. package/dist/project/agent-rules.js +8 -8
  38. package/dist/project/refresh-rules.js +2 -1
  39. package/dist/register/prompt-hooks.d.ts +123 -0
  40. package/dist/register/prompt-hooks.js +449 -0
  41. package/dist/register/slash-command.d.ts +1 -1
  42. package/dist/register/slash-command.js +5 -4
  43. package/dist/run-types.d.ts +4 -0
  44. package/dist/run.js +4 -1
  45. package/package.json +2 -2
@@ -122,6 +122,13 @@ export interface Detection {
122
122
  * require-hook down with it and the dev server never starts.
123
123
  */
124
124
  typescript: boolean;
125
+ /**
126
+ * Whether the project uses Prettier (config file or package.json "prettier" key).
127
+ * Used by `init` to decide whether to add `.reticle/` to `.prettierignore` (#1560).
128
+ *
129
+ * Optional so existing test fixtures keep compiling without naming it; `detect` always sets it.
130
+ */
131
+ usesPrettier?: boolean | undefined;
125
132
  reactMajor: number | undefined;
126
133
  /**
127
134
  * `react-scripts`' major, when the project has it. `undefined` on every other stack.
@@ -251,6 +258,8 @@ export declare function parseMajor(range: string | undefined): number | undefine
251
258
  */
252
259
  export declare function namesAPackageManager(markers: ReadonlySet<string>): boolean;
253
260
  export declare function detectPackageManager(lockfiles: ReadonlySet<string>, nodeModulesMarkers: ReadonlySet<string>, pkg?: unknown): PackageManager;
261
+ /** The file that makes a project TypeScript to its tooling, Next and Vite included. */
262
+ export declare const TSCONFIG_FILE = "tsconfig.json";
254
263
  export declare function detect(input: DetectInput): Detection;
255
264
  interface InstallCommand {
256
265
  command: string;
@@ -2,6 +2,7 @@
2
2
  * Pure framework + toolchain detection for `reticle init`. No filesystem access — callers pass in
3
3
  * the parsed package.json and the set of config/lock filenames present in the project root.
4
4
  */
5
+ import { hasPrettierConfig } from './prettier-config.js';
5
6
  export const Framework = {
6
7
  NEXT: 'next',
7
8
  /**
@@ -353,7 +354,9 @@ function detectUiLibrary(pkg) {
353
354
  return UiLibrary.SVELTE;
354
355
  return UiLibrary.UNKNOWN;
355
356
  }
356
- const TS_CONFIGS = ['tsconfig.json'];
357
+ /** The file that makes a project TypeScript to its tooling, Next and Vite included. */
358
+ export const TSCONFIG_FILE = 'tsconfig.json';
359
+ const TS_CONFIGS = [TSCONFIG_FILE];
357
360
  export function detect(input) {
358
361
  const reactMajor = parseMajor(depVersion(input.pkg, 'react'));
359
362
  return {
@@ -362,6 +365,7 @@ export function detect(input) {
362
365
  uiLibrary: detectUiLibrary(input.pkg),
363
366
  typescript: hasAnyConfig(input.configFiles, TS_CONFIGS) ||
364
367
  depVersion(input.pkg, 'typescript') !== undefined,
368
+ usesPrettier: hasPrettierConfig(input.configFiles, input.pkg),
365
369
  reactMajor,
366
370
  needsSourceMapping: reactMajor !== undefined && reactMajor >= 19,
367
371
  customReconciler: CUSTOM_RECONCILER_DEPS.some((name) => depVersion(input.pkg, name) !== undefined),
@@ -15,6 +15,8 @@
15
15
  * on the server side changed.
16
16
  */
17
17
  export declare const DEV_SERVER_PORTS: ReadonlySet<number>;
18
+ /** The framework's default dev-server port, or undefined for one that has none of its own. */
19
+ export declare function frameworkDevPort(framework: string): number | undefined;
18
20
  export declare function isLikelyDevServerPort(port: number): boolean;
19
21
  /** The warning printed when a project pins the bridge to something that looks like its dev server. */
20
22
  export declare function devServerPortWarning(port: number): string;
@@ -1,3 +1,4 @@
1
+ import { Framework } from './detect.js';
1
2
  /**
2
3
  * Ports that belong to a DEV SERVER, not to Reticle's bridge. Next defaults to 3000, Vite to 5173,
3
4
  * Astro to 4321, and so on.
@@ -44,6 +45,25 @@ export const DEV_SERVER_PORTS = new Set([
44
45
  6006, // Storybook
45
46
  19006, // Expo web
46
47
  ]);
48
+ /**
49
+ * The port each framework's dev server takes when nothing configures another. Every value is in
50
+ * {@link DEV_SERVER_PORTS} as well. `reticle setup` reads it to know which port a dev script will
51
+ * take before starting one, so a server already on half of that port is found first (#1377).
52
+ */
53
+ const FRAMEWORK_DEV_PORTS = {
54
+ [Framework.VITE]: 5173,
55
+ [Framework.SVELTEKIT]: 5173,
56
+ [Framework.REACT_ROUTER]: 5173,
57
+ [Framework.NEXT]: 3000,
58
+ [Framework.NUXT]: 3000,
59
+ [Framework.CRA]: 3000,
60
+ [Framework.ASTRO]: 4321,
61
+ [Framework.ANGULAR]: 4200,
62
+ };
63
+ /** The framework's default dev-server port, or undefined for one that has none of its own. */
64
+ export function frameworkDevPort(framework) {
65
+ return FRAMEWORK_DEV_PORTS[framework];
66
+ }
47
67
  export function isLikelyDevServerPort(port) {
48
68
  return DEV_SERVER_PORTS.has(port);
49
69
  }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Detecting whether a project uses Prettier.
3
+ *
4
+ * Lives in `detect/` (not `patch/`) because `patch/` already imports from `detect/` —
5
+ * the reverse import would create a `detect <-> patch` cycle that
6
+ * `directory-reach.test.ts` forbids. Detection belongs here anyway.
7
+ */
8
+ interface PackageJsonLike {
9
+ dependencies?: Record<string, string>;
10
+ devDependencies?: Record<string, string>;
11
+ peerDependencies?: Record<string, string>;
12
+ prettier?: unknown;
13
+ }
14
+ /** Filenames that indicate the project uses Prettier. */
15
+ export declare const PRETTIER_CONFIG_FILES: Set<string>;
16
+ /**
17
+ * True when the project uses Prettier: a Prettier config file exists at the root,
18
+ * package.json declares a "prettier" key, or prettier is in (dev)dependencies.
19
+ */
20
+ export declare function hasPrettierConfig(rootFiles: ReadonlySet<string>, pkg: PackageJsonLike): boolean;
21
+ export {};
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Detecting whether a project uses Prettier.
3
+ *
4
+ * Lives in `detect/` (not `patch/`) because `patch/` already imports from `detect/` —
5
+ * the reverse import would create a `detect <-> patch` cycle that
6
+ * `directory-reach.test.ts` forbids. Detection belongs here anyway.
7
+ */
8
+ /** Filenames that indicate the project uses Prettier. */
9
+ export const PRETTIER_CONFIG_FILES = new Set([
10
+ '.prettierrc',
11
+ '.prettierrc.json',
12
+ '.prettierrc.yml',
13
+ '.prettierrc.yaml',
14
+ '.prettierrc.json5',
15
+ '.prettierrc.js',
16
+ '.prettierrc.cjs',
17
+ '.prettierrc.mjs',
18
+ '.prettierrc.ts',
19
+ 'prettier.config.js',
20
+ 'prettier.config.cjs',
21
+ 'prettier.config.mjs',
22
+ 'prettier.config.ts',
23
+ ]);
24
+ /**
25
+ * True when the project uses Prettier: a Prettier config file exists at the root,
26
+ * package.json declares a "prettier" key, or prettier is in (dev)dependencies.
27
+ */
28
+ export function hasPrettierConfig(rootFiles, pkg) {
29
+ for (const name of rootFiles) {
30
+ if (PRETTIER_CONFIG_FILES.has(name))
31
+ return true;
32
+ }
33
+ if (pkg.prettier !== undefined)
34
+ return true;
35
+ return (pkg.dependencies?.prettier !== undefined ||
36
+ pkg.devDependencies?.prettier !== undefined ||
37
+ pkg.peerDependencies?.prettier !== undefined);
38
+ }
@@ -12,6 +12,7 @@ import { join } from 'node:path';
12
12
  import { detect, Framework } from './detect.js';
13
13
  import { detectNonJsEcosystem, noPackageJsonMessage } from './non-js-project.js';
14
14
  import { findAppsBelow, findWorkspaceApps, PACKAGE_JSON, workspacePackageDirs, } from './workspace-apps.js';
15
+ import { devScriptBody } from './dev-script.js';
15
16
  import { projectIdOf, RETICLE_CONFIG_FILE } from './existing-config.js';
16
17
  import { chooseWorkspaceApp, withoutTrailingSlashes } from './declared/app-choice.js';
17
18
  const ROOT_CONFIG_LEAD = `This root's ${RETICLE_CONFIG_FILE} names`;
@@ -39,14 +40,17 @@ function inHome(io) {
39
40
  return trim(io.cwd()) === trim(io.homeDir());
40
41
  }
41
42
  /** Print the candidates as commands to paste, and stop: the choice is the person's. */
42
- function listApps(io, header, apps) {
43
- io.print(header);
43
+ function printAppCommands(io, apps) {
44
44
  for (const a of apps) {
45
45
  const displayed = displayedWorkspaceAppCommand(a, process.platform);
46
46
  if (displayed.label !== undefined)
47
47
  io.print(` ${displayed.label}`);
48
48
  io.print(` ${displayed.command}`);
49
49
  }
50
+ }
51
+ function listApps(io, header, apps) {
52
+ io.print(header);
53
+ printAppCommands(io, apps);
50
54
  return { ok: false, applied: 0, manual: apps.length, listedAppsBelow: true };
51
55
  }
52
56
  const WORKSPACE_APP_COMMAND = 'reticle init --app';
@@ -126,6 +130,22 @@ export function redirectToWorkspaceApp(options, io, pkg, runInit) {
126
130
  const pointed = appNamedByRootConfig(io);
127
131
  if (pointed !== undefined)
128
132
  return enterApp(options, io, pointed, ROOT_CONFIG_LEAD, runInit);
133
+ // A plain page with a manifest that has a dev script is the app too: it renders through its own
134
+ // index.html, and the dev script that serves it lives in its package.json. A manifest that holds
135
+ // only tooling is not enough: the real app may sit in a nested folder, so discovery still runs.
136
+ //
137
+ // Discovery preferred a nested copy - a `backup/` folder holding another app - over the folder the
138
+ // person is standing in, and `init --dry-run` named the copy as the target. A root pointer stored
139
+ // by an earlier `--app` run is read first, above. Past that, the nested app is something to LIST,
140
+ // never to choose; the root is wired by the normal plan after this null.
141
+ if (io.exists(STATIC_PAGE) && io.exists(PACKAGE_JSON) && devScriptBody(pkg) !== undefined) {
142
+ const nested = findWorkspaceApps(io);
143
+ if (0 < nested.length) {
144
+ io.print(WIRE_ANOTHER);
145
+ printAppCommands(io, nested);
146
+ }
147
+ return null;
148
+ }
129
149
  const apps = findWorkspaceApps(io);
130
150
  // An explicitly named app answers the ambiguity. Refusing to guess is right, but "re-run inside the
131
151
  // one you want" is not something a script, a CI step, or an agent that cannot change directory can
@@ -159,12 +179,7 @@ export function redirectToWorkspaceApp(options, io, pkg, runInit) {
159
179
  const rest = apps.filter((a) => !wired.includes(a));
160
180
  if (0 < wired.length && 0 < rest.length)
161
181
  io.print(WIRE_ANOTHER);
162
- for (const a of 0 === wired.length ? apps : rest) {
163
- const displayed = displayedWorkspaceAppCommand(a, process.platform);
164
- if (displayed.label !== undefined)
165
- io.print(` ${displayed.label}`);
166
- io.print(` ${displayed.command}`);
167
- }
182
+ printAppCommands(io, 0 === wired.length ? apps : rest);
168
183
  return 0 === wired.length
169
184
  ? { ok: false, applied: 0, manual: apps.length }
170
185
  : { ok: true, applied: 0, manual: 0 };
@@ -16,6 +16,11 @@ import { StepStatus } from '../plan/plan.js';
16
16
  * the workspace redirect is kept from asking twice.
17
17
  */
18
18
  export declare const FEEDBACK_HINT: string;
19
+ /**
20
+ * The last line of a successful `init`, after the ask: where the user can tell the maintainers what
21
+ * they build. Only on success: on a failure, the ask above is the thing to do.
22
+ */
23
+ export declare const COMMUNITY_HINT = "Tell us what you are building: https://discord.gg/BwAbzv9ZRz";
19
24
  /**
20
25
  * The last two lines a user reads, and the order matters.
21
26
  *
@@ -5,6 +5,7 @@
5
5
  * and this decides what to say afterwards. The failure modes differ too — a wrong write breaks an
6
6
  * install, a wrong sentence loses a user quietly.
7
7
  */
8
+ import { FEEDBACK_KIND_CHOICES } from '@reticlehq/core';
8
9
  import { StepStatus } from '../plan/plan.js';
9
10
  import { wasMcpRegistered } from '../register/mcp-registered.js';
10
11
  import { CLI } from '../project/agent-rules.js';
@@ -19,12 +20,15 @@ import { CLI } from '../project/agent-rules.js';
19
20
  */
20
21
  export const FEEDBACK_HINT = 'Anything wrong, missing, or awkward — in this setup or in Reticle itself? Tell us; it is the ' +
21
22
  'one thing that decides what gets fixed:\n' +
22
- ` ${CLI} feedback --agent --kind <bug|gap|ambiguity|feature_request|improvement> "what happened" (agents)\n` +
23
+ ` ${CLI} feedback --agent --kind ${FEEDBACK_KIND_CHOICES} "what happened" (agents)\n` +
23
24
  ` ${CLI} feedback "what worked, what didn't" (humans)`;
25
+ /**
26
+ * The last line of a successful `init`, after the ask: where the user can tell the maintainers what
27
+ * they build. Only on success: on a failure, the ask above is the thing to do.
28
+ */
29
+ export const COMMUNITY_HINT = 'Tell us what you are building: https://discord.gg/BwAbzv9ZRz';
24
30
  /** User-facing closing copy for `restartHint` — one named home per sentence block. */
25
31
  const RestartHintCopy = {
26
- DEMO: 'Then drive one real flow of your app end to end and report the verdict — that is the install ' +
27
- "proving itself, and it is the agent's job, not a separate command.",
28
32
  PROVE: 'Then run `npx @reticlehq/server status` — it confirms the app connected, or says exactly why ' +
29
33
  'it has not.',
30
34
  ALREADY_REGISTERED_ALL: 'The Reticle MCP server was already registered on this machine, so there is no restart to do ' +
@@ -33,9 +37,12 @@ const RestartHintCopy = {
33
37
  'do and the tools are available there now. They are NOT available in the clients whose ' +
34
38
  'step above still needs a hand edit (',
35
39
  ALREADY_REGISTERED_CLAUDE_TAIL: ') until that is done.',
36
- CLOSING_ALREADY_NO_MANUAL: 'Once it shows a session, drive a flow — that is the install finished. Carry straight on; ' +
40
+ CLOSING_NO_MCP: 'Once it shows a session, drive one flow and report the verdict — that is the install finished.',
41
+ CLOSING_ALREADY_NO_MANUAL: 'Once it shows a session, drive one flow and report the verdict — that is the install ' +
42
+ 'finished. Carry straight on; ' +
37
43
  'nothing here is waiting on a human.',
38
- CLOSING_ALREADY_WITH_MANUAL: 'Once it shows a session, drive a flow where the tools are already available — that is the ' +
44
+ CLOSING_ALREADY_WITH_MANUAL: 'Once it shows a session, drive one flow and report the verdict where the tools are already ' +
45
+ 'available — that is the ' +
39
46
  'install finished on that client. The hand-edit step above still needs a human for the other ' +
40
47
  'clients named there.',
41
48
  };
@@ -100,13 +107,14 @@ manualClients = []) {
100
107
  //
101
108
  // So the close hands the agent the sequence instead. It is deliberately one drive of one real
102
109
  // flow ending in a verdict, because an install that ends in "the tools are available" is an
103
- // install that ends in nothing.
104
- const demo = RestartHintCopy.DEMO;
110
+ // install that ends in nothing. It is said ONCE, last: after the status check and after the client
111
+ // restart, because that is the order in which a reader can do it.
105
112
  const prove = RestartHintCopy.PROVE;
106
113
  // Nothing registered this run (`--no-mcp`, or a client that needs a manual edit): a restart picks
107
114
  // up nothing, so advice about one would be advice about something we did not do.
108
- if (!wasMcpRegistered(mcpStatus))
109
- return `${dev}\n${demo}\n${prove}`;
115
+ if (!wasMcpRegistered(mcpStatus)) {
116
+ return `${dev}\n${prove}\n${RestartHintCopy.CLOSING_NO_MCP}`;
117
+ }
110
118
  // ALREADY: the tools are reachable RIGHT NOW, and this is the branch that used to lie.
111
119
  //
112
120
  // `wasMcpRegistered` is true for both APPLY and ALREADY, which is wrong here: the two cases have
@@ -125,10 +133,9 @@ manualClients = []) {
125
133
  const closing = 0 === manualClients.length
126
134
  ? RestartHintCopy.CLOSING_ALREADY_NO_MANUAL
127
135
  : RestartHintCopy.CLOSING_ALREADY_WITH_MANUAL;
128
- return `${dev}\n${demo}\n${availability}\n${prove}\n${closing}`;
136
+ return `${dev}\n${availability}\n${prove}\n${closing}`;
129
137
  }
130
138
  return (`${dev}\n` +
131
- `${demo}\n` +
132
139
  'Then restart your agent so it picks up the new MCP server. On Claude Code and Codex, do NOT ' +
133
140
  'ask a human to do it: `npx @reticlehq/server init --relaunch` prints the exact resume command ' +
134
141
  'for THIS conversation — run that command and the restart continues the work on the other ' +
@@ -10,20 +10,22 @@ import { dirname, join } from 'node:path';
10
10
  import { CONTAINER_MARKERS, runsDevServer } from './diagnose/containerised-dev-server.js';
11
11
  import { CSP_FILES } from './diagnose/csp-doctor.js';
12
12
  import { devScriptBody, discoverPlainViteConfig } from './detect/dev-script.js';
13
- import { ANGULAR_WORKSPACE_FILE, detect, FORGE_RENDERER_CONFIGS, Framework, } from './detect/detect.js';
13
+ import { ANGULAR_WORKSPACE_FILE, detect, FORGE_RENDERER_CONFIGS, Framework, TSCONFIG_FILE, } from './detect/detect.js';
14
14
  import { ANGULAR_PROXY_PATH, angularEntry } from './patch/angular.js';
15
15
  import { HTML_INDEX_PATH, STATIC_GITIGNORE_PATH, STATIC_TOKEN_MODULE, } from './patch/static-page.js';
16
16
  import { pickAstroHost } from './patch/astro-host.js';
17
17
  import { ELECTRON_VITE_CONFIG_CANDIDATES, NEXT_CONFIG_CANDIDATES, VITE_CONFIG_CANDIDATES, } from './detect/workspace-apps.js';
18
18
  import { CURSOR_RULE_PATH, RETICLE_MD_PATH } from './project/agent-rules.js';
19
19
  import { CLAUDE_SETTINGS_PATH } from './plan/stop-hook-step.js';
20
+ import { PromptHookAgent, promptHookTargets, } from './register/prompt-hooks.js';
20
21
  import { CRA_ENV_PATH } from './patch/cra.js';
21
22
  import { findNextMountFile } from './patch/next-patch.js';
22
23
  import { tanstackStartConnectPath } from './patch/tanstack-start.js';
23
24
  import { installedViteMajor } from './patch/vite-owning-config.js';
25
+ import { PRETTIERIGNORE_PATH } from './patch/prettier-ignore.js';
24
26
  import { RETICLE_CONFIG_FILE } from './plan/plan.js';
25
27
  import { claudeAvailableProbe, claudeHasReticle, codexAvailableProbe } from './register/mcp.js';
26
- import { reticleDevLocation } from './patch/next-patch.js';
28
+ import { nextUsesTypeScript, reticleDevLocation } from './patch/next-patch.js';
27
29
  import { scanTestids, storeHints, scanStores } from './detect/capabilities.js';
28
30
  import { CLAUDE_PROJECT_CONFIG, CURSOR_PROJECT_MARKER, McpClient } from './register/mcp-clients.js';
29
31
  import { deriveProjectId, packageName } from './project/project-id.js';
@@ -32,6 +34,7 @@ import { NUXT_CONFIG_CANDIDATES } from './patch/nuxt-patch.js';
32
34
  import { CLAUDE_COMMAND_PATH, CURSOR_COMMAND_PATH } from './register/slash-command.js';
33
35
  import { RETICLE_VERSION } from './version.js';
34
36
  import { resolveLockfiles } from './detect/lockfiles.js';
37
+ import { projectIdOf } from './detect/existing-config.js';
35
38
  /** CRA's bundled entry, in the order create-react-app itself generates them. */
36
39
  const CRA_ENTRY_CANDIDATES = ['src/index.tsx', 'src/index.jsx', 'src/index.ts', 'src/index.js'];
37
40
  /** Preview snippets must not mint a real machine credential or imply this token is usable. */
@@ -39,8 +42,9 @@ const DRY_RUN_PAIRING_TOKEN = 'DRY_RUN_TOKEN_NOT_FOR_USE';
39
42
  function craEntryOf(io) {
40
43
  for (const path of CRA_ENTRY_CANDIDATES) {
41
44
  const source = io.readFile(path);
42
- if (source !== null)
45
+ if (source !== null) {
43
46
  return { path, source };
47
+ }
44
48
  }
45
49
  return null;
46
50
  }
@@ -82,41 +86,58 @@ const MAX_SCAN_DEPTH = 5;
82
86
  * says the answer is not another name in a list. So: walk the app root, bounded by depth and by a
83
87
  * file count.
84
88
  */
89
+ /** The files that quote `id` as a whole string literal (see `copiedIdSources`). */
90
+ function quoting(files, id) {
91
+ if (id === undefined) {
92
+ return [];
93
+ }
94
+ return files.filter((f) => ['"', "'", '`'].some((q) => f.source.includes(`${q}${id}${q}`)));
95
+ }
85
96
  function readSourceFiles(io) {
86
97
  const out = [];
87
98
  const read = (path) => {
88
99
  const content = io.readFile(path);
89
- if (content !== null)
100
+ if (content !== null) {
90
101
  out.push({ path, source: content });
102
+ }
91
103
  };
92
104
  for (const name of io.rootFiles()) {
93
- if (out.length >= MAX_SCANNED_FILES)
105
+ if (out.length >= MAX_SCANNED_FILES) {
94
106
  return out;
95
- if (SOURCE_FILE.test(name))
107
+ }
108
+ if (SOURCE_FILE.test(name)) {
96
109
  read(name);
110
+ }
97
111
  }
98
112
  const walk = (dir, depth) => {
99
113
  for (const name of io.listFiles(dir)) {
100
- if (out.length >= MAX_SCANNED_FILES)
114
+ if (out.length >= MAX_SCANNED_FILES) {
101
115
  return;
102
- if (SOURCE_FILE.test(name))
116
+ }
117
+ if (SOURCE_FILE.test(name)) {
103
118
  read(`${dir}/${name}`);
119
+ }
104
120
  }
105
- if (depth >= MAX_SCAN_DEPTH)
121
+ if (depth >= MAX_SCAN_DEPTH) {
106
122
  return;
123
+ }
107
124
  for (const sub of io.listDirs(dir)) {
108
- if (out.length >= MAX_SCANNED_FILES)
125
+ if (out.length >= MAX_SCANNED_FILES) {
109
126
  return;
110
- if (sub.startsWith('.') || NOT_SOURCE_DIRS.has(sub))
127
+ }
128
+ if (sub.startsWith('.') || NOT_SOURCE_DIRS.has(sub)) {
111
129
  continue;
130
+ }
112
131
  walk(`${dir}/${sub}`, depth + 1);
113
132
  }
114
133
  };
115
134
  for (const dir of io.listDirs('.')) {
116
- if (out.length >= MAX_SCANNED_FILES)
135
+ if (out.length >= MAX_SCANNED_FILES) {
117
136
  return out;
118
- if (dir.startsWith('.') || NOT_SOURCE_DIRS.has(dir))
137
+ }
138
+ if (dir.startsWith('.') || NOT_SOURCE_DIRS.has(dir)) {
119
139
  continue;
140
+ }
120
141
  walk(dir, 1);
121
142
  }
122
143
  return out;
@@ -163,16 +184,19 @@ const ASTRO_LAYOUTS_DIR = 'src/layouts';
163
184
  /** Also searched: an app with no layouts directory renders the document straight from a page. */
164
185
  const ASTRO_PAGES_DIR = 'src/pages';
165
186
  function firstPresent(files, candidates) {
166
- for (const c of candidates)
167
- if (files.has(c))
187
+ for (const c of candidates) {
188
+ if (files.has(c)) {
168
189
  return c;
190
+ }
191
+ }
169
192
  return null;
170
193
  }
171
194
  function firstReadable(io, candidates) {
172
195
  for (const path of candidates) {
173
196
  const source = io.readFile(path);
174
- if (source !== null)
197
+ if (source !== null) {
175
198
  return { path, source };
199
+ }
176
200
  }
177
201
  return null;
178
202
  }
@@ -277,16 +301,20 @@ export function gatherPlanInput(options, io, pkg) {
277
301
  const layoutSource = null === layoutPath ? null : io.readFile(layoutPath);
278
302
  // Where the component goes depends on WHICH router mounts it: `pages/` routes on presence, so a
279
303
  // component there becomes a broken route; `app/` routes on filename, so a sibling is inert.
280
- const devLocation = reticleDevLocation(layoutPath ?? 'app/layout.tsx', detection.typescript);
304
+ // And which LANGUAGE it is written in follows Next's rule, not `detection.typescript`: a
305
+ // `typescript` devDependency in a jsconfig app is not a TypeScript project to Next (#1527).
306
+ const devLocation = reticleDevLocation(layoutPath ?? 'app/layout.tsx', nextUsesTypeScript(layoutPath, rootFiles.has(TSCONFIG_FILE)));
281
307
  // Read once: both the testid scan and the store scan want the same bounded set of files.
282
308
  const sourceFiles = readSourceFiles(io);
309
+ const copiedId = projectIdOf(io.readFile(RETICLE_CONFIG_FILE));
283
310
  const agentRoot = agentRootOf(options);
284
311
  const agentFile = (relPath) => agentRoot === undefined ? relPath : join(agentRoot, relPath);
285
312
  const cspSources = {};
286
313
  for (const file of CSP_FILES) {
287
314
  const source = io.readFile(file);
288
- if (null !== source)
315
+ if (null !== source) {
289
316
  cspSources[file] = source;
317
+ }
290
318
  }
291
319
  return {
292
320
  detection,
@@ -294,6 +322,7 @@ export function gatherPlanInput(options, io, pkg) {
294
322
  captureBodies: options.captureBodies,
295
323
  hooks: options.hooks,
296
324
  cspSources,
325
+ prettierIgnoreSource: io.readFile(PRETTIERIGNORE_PATH),
297
326
  claudeCli,
298
327
  mcpExists,
299
328
  insideClaudeCode: io.host.insideClaudeCode(),
@@ -333,6 +362,7 @@ export function gatherPlanInput(options, io, pkg) {
333
362
  : null,
334
363
  // Capabilities: scanned, never asked for. Bounded — a hint for the agent, not a repo index.
335
364
  nextLayoutCandidates: nextMount.candidates,
365
+ nextUnmountedLayouts: nextMount.unmounted,
336
366
  testids: scanTestids(sourceFiles.map((f) => f.source)),
337
367
  storeHints: storeHints(dependencyNames(pkg), sourceFiles),
338
368
  foundStores: scanStores(sourceFiles, dependencyNames(pkg)),
@@ -386,6 +416,7 @@ export function gatherPlanInput(options, io, pkg) {
386
416
  // The CONTENT, so a config that exists can be checked rather than trusted — a `"port"` set to
387
417
  // the app's own dev-server port used to survive every re-run of `init`.
388
418
  reticleConfigSource: io.readFile(RETICLE_CONFIG_FILE),
419
+ ...(true === options.newId ? { copiedIdSources: quoting(sourceFiles, copiedId) } : {}),
389
420
  // Read the agent instruction files so the rule merge stays idempotent across re-runs — from the
390
421
  // agent's own root, or the merge would idempotently check a file it is not going to write.
391
422
  claudeMdContent: io.readFile(agentFile('CLAUDE.md')),
@@ -396,6 +427,11 @@ export function gatherPlanInput(options, io, pkg) {
396
427
  cursorCommandContent: io.readFile(agentFile(CURSOR_COMMAND_PATH)),
397
428
  // Read only to MERGE into: the settings file is the user's, and other tools write in it too.
398
429
  claudeSettingsContent: io.readFile(agentFile(CLAUDE_SETTINGS_PATH)),
430
+ ...promptHookInput(io, options, claudeCli, agentFile),
431
+ ...(() => {
432
+ const bin = io.host.hookBin?.();
433
+ return bin === undefined ? {} : { hookBin: bin };
434
+ })(),
399
435
  ...(agentRoot === undefined
400
436
  ? {}
401
437
  : {
@@ -409,8 +445,24 @@ export function gatherPlanInput(options, io, pkg) {
409
445
  mcp: options.mcp,
410
446
  install: options.install,
411
447
  projectId,
448
+ ...(true === options.newId ? { newId: true } : {}),
412
449
  // The SDK must match the CLI asking for it — see pinnedPackages.
413
450
  sdkVersion: RETICLE_VERSION,
414
451
  },
415
452
  };
416
453
  }
454
+ /**
455
+ * Which agents get the prompt-capture hook, and what their configs hold now. Rides with the agent
456
+ * wiring, so `--no-mcp` skips it as it skips the rule files. Claude Code's entry is merged with the
457
+ * other hooks in `.claude/settings.json`, so it is a flag here and not a target.
458
+ */
459
+ function promptHookInput(io, options, claudeCli, agentFile) {
460
+ if (!options.mcp || false === options.promptCapture) {
461
+ return { claudePromptHook: false, promptHooks: [] };
462
+ }
463
+ const targets = promptHookTargets(io, agentFile('.'), io.homeDir(), claudeCli, join);
464
+ return {
465
+ claudePromptHook: targets.some((t) => PromptHookAgent.CLAUDE_CODE === t.spec.id),
466
+ promptHooks: targets.filter((t) => PromptHookAgent.CLAUDE_CODE !== t.spec.id),
467
+ };
468
+ }
package/dist/host.d.ts CHANGED
@@ -49,6 +49,12 @@ export interface InitHost {
49
49
  * which the scaffolder does not read for itself.
50
50
  */
51
51
  insideClaudeCode(): boolean;
52
+ /**
53
+ * The installed CLI's bin, for hook commands that run it with node and skip npx's start. Under
54
+ * the home folder it is written `$HOME/...`, so no user name lands in a committed file. Undefined
55
+ * where a direct command is not wanted (Windows shells), and the hooks use npx alone.
56
+ */
57
+ hookBin?(): string | undefined;
52
58
  }
53
59
  /**
54
60
  * A host that does nothing, for callers that drive `init`'s file surface without running an install
package/dist/index.d.ts CHANGED
@@ -20,12 +20,12 @@ export { runInit, resolveLockfiles, type InitIo, type InitOptions, type InitResu
20
20
  export { windowsShellArg } from './register/windows-quote.js';
21
21
  export { buildNodeIo, probeCli } from './node-io.js';
22
22
  export { terminalWidth, wrapForTerminal } from './diagnose/terminal-wrap.js';
23
- export { FEEDBACK_HINT } from './diagnose/closing-hint.js';
23
+ export { COMMUNITY_HINT, FEEDBACK_HINT } from './diagnose/closing-hint.js';
24
24
  export { type InitHost, SILENT_HOST } from './host.js';
25
25
  export { RETICLE_VERSION, RETICLE_NPM_PACKAGE } from './version.js';
26
26
  export { InitFailure } from './diagnose/init-failure.js';
27
27
  export { NodePlatform } from './detect/platform.js';
28
- export { DEV_SERVER_PORTS, isLikelyDevServerPort, devServerPortWarning, } from './detect/dev-server-ports.js';
28
+ export { DEV_SERVER_PORTS, isLikelyDevServerPort, devServerPortWarning, frameworkDevPort, } from './detect/dev-server-ports.js';
29
29
  export { configWithInstallSource } from './project/install-source-config.js';
30
30
  export { projectIdOf, rememberProjectOnDisk, type RegistryIo } from './project/remember-project.js';
31
31
  export { detectPackageManager, Framework, installCommandParts, parseMajor, PackageManager, } from './detect/detect.js';
@@ -50,3 +50,9 @@ export { McpClient, ConfigScope, MCP_CLIENTS, ClientMergeStatus, clientSpec, mer
50
50
  export { claudeAddCommand, claudeAvailableProbe, claudeHasReticle, codexAddCommand, codexAvailableProbe, } from './register/mcp.js';
51
51
  export { codexNamesOurServer } from './register/codex-toml.js';
52
52
  export { detectMcpClients, type DetectedClient } from './register/detect-clients.js';
53
+ /**
54
+ * The prompt-capture hook, for `reticle hook install|uninstall|status` and `reticle doctor`: the
55
+ * same agent table and the same merge `init` uses, so the two cannot disagree about an agent.
56
+ */
57
+ export { AGENT_FLAG, MCP_FALLBACK_AGENTS, PROMPT_HOOK_AGENTS, PromptHookAgent, PromptHookStatus, installPromptHook, promptHookCommand, promptHookInstalled, promptHookNpxOnly, promptHookTargets, uninstallPromptHook, type PromptHookSpec, type PromptHookTarget, } from './register/prompt-hooks.js';
58
+ export { PROMPT_CAPTURE_NOTICE } from './plan/stop-hook-step.js';
package/dist/index.js CHANGED
@@ -20,12 +20,12 @@ export { runInit, resolveLockfiles, } from './run.js';
20
20
  export { windowsShellArg } from './register/windows-quote.js';
21
21
  export { buildNodeIo, probeCli } from './node-io.js';
22
22
  export { terminalWidth, wrapForTerminal } from './diagnose/terminal-wrap.js';
23
- export { FEEDBACK_HINT } from './diagnose/closing-hint.js';
23
+ export { COMMUNITY_HINT, FEEDBACK_HINT } from './diagnose/closing-hint.js';
24
24
  export { SILENT_HOST } from './host.js';
25
25
  export { RETICLE_VERSION, RETICLE_NPM_PACKAGE } from './version.js';
26
26
  export { InitFailure } from './diagnose/init-failure.js';
27
27
  export { NodePlatform } from './detect/platform.js';
28
- export { DEV_SERVER_PORTS, isLikelyDevServerPort, devServerPortWarning, } from './detect/dev-server-ports.js';
28
+ export { DEV_SERVER_PORTS, isLikelyDevServerPort, devServerPortWarning, frameworkDevPort, } from './detect/dev-server-ports.js';
29
29
  export { configWithInstallSource } from './project/install-source-config.js';
30
30
  export { projectIdOf, rememberProjectOnDisk } from './project/remember-project.js';
31
31
  export { detectPackageManager, Framework, installCommandParts, parseMajor, PackageManager, } from './detect/detect.js';
@@ -57,3 +57,9 @@ claudeAvailableProbe, claudeHasReticle,
57
57
  codexAddCommand, codexAvailableProbe, } from './register/mcp.js';
58
58
  export { codexNamesOurServer } from './register/codex-toml.js';
59
59
  export { detectMcpClients } from './register/detect-clients.js';
60
+ /**
61
+ * The prompt-capture hook, for `reticle hook install|uninstall|status` and `reticle doctor`: the
62
+ * same agent table and the same merge `init` uses, so the two cannot disagree about an agent.
63
+ */
64
+ export { AGENT_FLAG, MCP_FALLBACK_AGENTS, PROMPT_HOOK_AGENTS, PromptHookAgent, PromptHookStatus, installPromptHook, promptHookCommand, promptHookInstalled, promptHookNpxOnly, promptHookTargets, uninstallPromptHook, } from './register/prompt-hooks.js';
65
+ export { PROMPT_CAPTURE_NOTICE } from './plan/stop-hook-step.js';