@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.
- package/dist/detect/detect.d.ts +9 -0
- package/dist/detect/detect.js +5 -1
- package/dist/detect/dev-server-ports.d.ts +2 -0
- package/dist/detect/dev-server-ports.js +20 -0
- package/dist/detect/prettier-config.d.ts +21 -0
- package/dist/detect/prettier-config.js +38 -0
- package/dist/detect/workspace-redirect.js +23 -8
- package/dist/diagnose/closing-hint.d.ts +5 -0
- package/dist/diagnose/closing-hint.js +18 -11
- package/dist/gather-plan-input.js +70 -18
- package/dist/host.d.ts +6 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.js +8 -2
- package/dist/no-package-json.js +62 -14
- package/dist/patch/next-patch.d.ts +19 -11
- package/dist/patch/next-patch.js +33 -6
- package/dist/patch/prettier-ignore.d.ts +34 -0
- package/dist/patch/prettier-ignore.js +49 -0
- package/dist/patch/python-snippets.d.ts +22 -0
- package/dist/patch/python-snippets.js +113 -0
- package/dist/patch/snippets.d.ts +9 -30
- package/dist/patch/snippets.js +40 -103
- package/dist/patch/static-page.js +3 -5
- package/dist/patch/vite-config.js +11 -0
- package/dist/plan/connect-steps.d.ts +1 -0
- package/dist/plan/connect-steps.js +1 -0
- package/dist/plan/framework-adapter.d.ts +10 -0
- package/dist/plan/framework-adapter.js +120 -1
- package/dist/plan/notices.js +1 -1
- package/dist/plan/plan-framework.js +101 -32
- package/dist/plan/plan-types.d.ts +27 -0
- package/dist/plan/plan.d.ts +1 -10
- package/dist/plan/plan.js +128 -131
- package/dist/plan/stop-hook-step.d.ts +11 -0
- package/dist/plan/stop-hook-step.js +82 -6
- package/dist/project/agent-rules.d.ts +1 -1
- package/dist/project/agent-rules.js +8 -8
- package/dist/project/refresh-rules.js +2 -1
- package/dist/register/prompt-hooks.d.ts +123 -0
- package/dist/register/prompt-hooks.js +449 -0
- package/dist/register/slash-command.d.ts +1 -1
- package/dist/register/slash-command.js +5 -4
- package/dist/run-types.d.ts +4 -0
- package/dist/run.js +4 -1
- package/package.json +2 -2
package/dist/detect/detect.d.ts
CHANGED
|
@@ -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;
|
package/dist/detect/detect.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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${
|
|
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${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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';
|