@reticlehq/server 2.3.0 → 2.4.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/bridge.js +43 -3
- package/dist/bridge.test-harness.d.ts +5 -0
- package/dist/bridge.test-harness.js +12 -0
- package/dist/cli-parse.d.ts +14 -2
- package/dist/cli-parse.js +40 -12
- package/dist/cli-port.d.ts +3 -0
- package/dist/cli-port.js +21 -0
- package/dist/cli.js +9 -2
- package/dist/flows/decision.d.ts +0 -6
- package/dist/flows/decision.js +47 -6
- package/dist/flows/flow-tools.js +14 -6
- package/dist/init/capabilities.d.ts +17 -0
- package/dist/init/capabilities.js +51 -0
- package/dist/init/detect.d.ts +25 -0
- package/dist/init/detect.js +65 -3
- package/dist/init/next-patch.d.ts +41 -0
- package/dist/init/next-patch.js +106 -0
- package/dist/init/node-io.js +25 -0
- package/dist/init/patch-kind.d.ts +20 -0
- package/dist/init/patch-kind.js +10 -0
- package/dist/init/plan-framework.d.ts +35 -0
- package/dist/init/plan-framework.js +217 -0
- package/dist/init/plan.d.ts +51 -3
- package/dist/init/plan.js +125 -125
- package/dist/init/run.d.ts +17 -0
- package/dist/init/run.js +207 -16
- package/dist/init/slash-command.d.ts +18 -0
- package/dist/init/slash-command.js +52 -0
- package/dist/init/snippets.d.ts +44 -3
- package/dist/init/snippets.js +152 -6
- package/dist/init/vite-config.d.ts +4 -10
- package/dist/init/vite-config.js +15 -7
- package/dist/input/driven-redaction.d.ts +15 -0
- package/dist/input/driven-redaction.js +76 -0
- package/dist/input/network-detail.d.ts +9 -3
- package/dist/input/network-detail.js +35 -15
- package/dist/mcp-proxy.d.ts +35 -3
- package/dist/mcp-proxy.js +174 -27
- package/dist/session/session-manager.js +6 -0
- package/dist/session/session.d.ts +6 -0
- package/dist/session/session.js +7 -0
- package/dist/tools/read-tools.js +4 -0
- package/package.json +2 -2
package/dist/init/detect.js
CHANGED
|
@@ -6,6 +6,7 @@ export const Framework = {
|
|
|
6
6
|
NEXT: 'next',
|
|
7
7
|
VITE: 'vite',
|
|
8
8
|
SVELTEKIT: 'sveltekit',
|
|
9
|
+
ASTRO: 'astro',
|
|
9
10
|
HTML: 'html',
|
|
10
11
|
};
|
|
11
12
|
export const PackageManager = {
|
|
@@ -14,9 +15,22 @@ export const PackageManager = {
|
|
|
14
15
|
BUN: 'bun',
|
|
15
16
|
NPM: 'npm',
|
|
16
17
|
};
|
|
18
|
+
/**
|
|
19
|
+
* The UI library the app renders with. Detection used to key on "vite is in package.json" and stop
|
|
20
|
+
* there, so a Vue or Preact app got `@reticlehq/react` installed and an all-green report with no
|
|
21
|
+
* mention that the React adapter has nothing to attach to.
|
|
22
|
+
*/
|
|
23
|
+
export const UiLibrary = {
|
|
24
|
+
REACT: 'react',
|
|
25
|
+
PREACT: 'preact',
|
|
26
|
+
VUE: 'vue',
|
|
27
|
+
SVELTE: 'svelte',
|
|
28
|
+
UNKNOWN: 'unknown',
|
|
29
|
+
};
|
|
17
30
|
const NEXT_CONFIGS = ['next.config.js', 'next.config.mjs', 'next.config.ts', 'next.config.cjs'];
|
|
18
31
|
const VITE_CONFIGS = ['vite.config.js', 'vite.config.ts', 'vite.config.mjs', 'vite.config.mts'];
|
|
19
32
|
const SVELTE_CONFIGS = ['svelte.config.js', 'svelte.config.ts', 'svelte.config.mjs'];
|
|
33
|
+
const ASTRO_CONFIGS = ['astro.config.mjs', 'astro.config.js', 'astro.config.ts', 'astro.config.cjs'];
|
|
20
34
|
function depVersion(pkg, name) {
|
|
21
35
|
return pkg.dependencies?.[name] ?? pkg.devDependencies?.[name] ?? pkg.peerDependencies?.[name];
|
|
22
36
|
}
|
|
@@ -33,14 +47,36 @@ export function parseMajor(range) {
|
|
|
33
47
|
const major = parseInt(match[1], 10);
|
|
34
48
|
return isNaN(major) ? undefined : major;
|
|
35
49
|
}
|
|
36
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Markers each package manager leaves INSIDE `node_modules`. An installed tree is the strongest
|
|
52
|
+
* evidence there is — stronger than a lockfile, which may simply not be committed.
|
|
53
|
+
*
|
|
54
|
+
* Without this, a pnpm-installed project with no committed lockfile was read as npm, and `npm i -D`
|
|
55
|
+
* then died on pnpm's symlink layout with `Cannot read properties of null (reading 'matches')`. Worse,
|
|
56
|
+
* it left the package present in `node_modules` but absent from `package.json`, so every later run
|
|
57
|
+
* reported the same failure — a setup that could not be retried into working.
|
|
58
|
+
*/
|
|
59
|
+
const NODE_MODULES_MARKERS = [
|
|
60
|
+
['.modules.yaml', PackageManager.PNPM],
|
|
61
|
+
['.yarn-state.yml', PackageManager.YARN],
|
|
62
|
+
['.package-lock.json', PackageManager.NPM],
|
|
63
|
+
];
|
|
64
|
+
/** Marker basenames present inside the project's `node_modules`, if it has one. */
|
|
65
|
+
export function packageManagerFromNodeModules(markers) {
|
|
66
|
+
for (const [name, pm] of NODE_MODULES_MARKERS)
|
|
67
|
+
if (markers.has(name))
|
|
68
|
+
return pm;
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
function detectPackageManager(lockfiles, nodeModulesMarkers) {
|
|
37
72
|
if (lockfiles.has('pnpm-lock.yaml'))
|
|
38
73
|
return PackageManager.PNPM;
|
|
39
74
|
if (lockfiles.has('yarn.lock'))
|
|
40
75
|
return PackageManager.YARN;
|
|
41
76
|
if (lockfiles.has('bun.lockb') || lockfiles.has('bun.lock'))
|
|
42
77
|
return PackageManager.BUN;
|
|
43
|
-
|
|
78
|
+
// No lockfile is not the same as "npm". An already-installed tree says which manager built it.
|
|
79
|
+
return packageManagerFromNodeModules(nodeModulesMarkers) ?? PackageManager.NPM;
|
|
44
80
|
}
|
|
45
81
|
function detectFramework(input) {
|
|
46
82
|
const { pkg, configFiles } = input;
|
|
@@ -52,18 +88,44 @@ function detectFramework(input) {
|
|
|
52
88
|
if (depVersion(pkg, '@sveltejs/kit') !== undefined || hasAnyConfig(configFiles, SVELTE_CONFIGS)) {
|
|
53
89
|
return Framework.SVELTEKIT;
|
|
54
90
|
}
|
|
91
|
+
// Astro is Vite-based but SSRs its own HTML, so the plugin's index.html injection never fires and
|
|
92
|
+
// `vite` is not a direct dependency — it used to fall all the way through to HTML and be handed
|
|
93
|
+
// connect instructions for a bundler it does not have. Check BEFORE the generic Vite branch.
|
|
94
|
+
if (depVersion(pkg, 'astro') !== undefined || hasAnyConfig(configFiles, ASTRO_CONFIGS)) {
|
|
95
|
+
return Framework.ASTRO;
|
|
96
|
+
}
|
|
55
97
|
if (depVersion(pkg, 'vite') !== undefined || hasAnyConfig(configFiles, VITE_CONFIGS)) {
|
|
56
98
|
return Framework.VITE;
|
|
57
99
|
}
|
|
58
100
|
return Framework.HTML;
|
|
59
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* React first: a Preact app using preact/compat aliases React, and Next/Remix apps list both. The
|
|
104
|
+
* order is the precedence — whichever the app actually renders through is the one it depends on
|
|
105
|
+
* directly.
|
|
106
|
+
*/
|
|
107
|
+
function detectUiLibrary(pkg) {
|
|
108
|
+
if (depVersion(pkg, 'react') !== undefined)
|
|
109
|
+
return UiLibrary.REACT;
|
|
110
|
+
if (depVersion(pkg, 'preact') !== undefined)
|
|
111
|
+
return UiLibrary.PREACT;
|
|
112
|
+
if (depVersion(pkg, 'vue') !== undefined)
|
|
113
|
+
return UiLibrary.VUE;
|
|
114
|
+
if (depVersion(pkg, 'svelte') !== undefined)
|
|
115
|
+
return UiLibrary.SVELTE;
|
|
116
|
+
return UiLibrary.UNKNOWN;
|
|
117
|
+
}
|
|
118
|
+
const TS_CONFIGS = ['tsconfig.json'];
|
|
60
119
|
export function detect(input) {
|
|
61
120
|
const reactMajor = parseMajor(depVersion(input.pkg, 'react'));
|
|
62
121
|
return {
|
|
63
122
|
framework: detectFramework(input),
|
|
123
|
+
uiLibrary: detectUiLibrary(input.pkg),
|
|
124
|
+
typescript: hasAnyConfig(input.configFiles, TS_CONFIGS) ||
|
|
125
|
+
depVersion(input.pkg, 'typescript') !== undefined,
|
|
64
126
|
reactMajor,
|
|
65
127
|
needsSourceMapping: reactMajor !== undefined && reactMajor >= 19,
|
|
66
|
-
packageManager: detectPackageManager(input.lockfiles),
|
|
128
|
+
packageManager: detectPackageManager(input.lockfiles, input.nodeModulesMarkers ?? new Set()),
|
|
67
129
|
};
|
|
68
130
|
}
|
|
69
131
|
const INSTALL_ARGS = {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, conservative patchers for the two files a Next app needed edited BY HAND: `next.config.*`
|
|
3
|
+
* (wrap the export in `withReticle`) and `app/layout.tsx` (mount `<ReticleDev />`). Same contract as
|
|
4
|
+
* the Vite patcher — recognise the obvious shape, bail to `manual` on anything ambiguous, never
|
|
5
|
+
* half-edit. Leaving these manual is why Next connected 0% of the time: both edits are silent when
|
|
6
|
+
* skipped, and one of them is JSX.
|
|
7
|
+
*/
|
|
8
|
+
import { type SourcePatch } from './patch-kind.js';
|
|
9
|
+
export declare const NEXT_CONFIG_IMPORT = "import { withReticle } from '@reticlehq/next';";
|
|
10
|
+
export declare const NEXT_CONFIG_REQUIRE = "const { withReticle } = require('@reticlehq/next');";
|
|
11
|
+
export declare const RETICLE_DEV_IMPORT: string;
|
|
12
|
+
/** Where the generated connect component goes, and how the mount file should import it. */
|
|
13
|
+
export interface ReticleDevLocation {
|
|
14
|
+
/** Project-relative path to write. */
|
|
15
|
+
path: string;
|
|
16
|
+
/** What the mount file (`app/layout.*` or `pages/_app.*`) should import. */
|
|
17
|
+
importSpecifier: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Decide where the connect component lives, from the file that will mount it.
|
|
21
|
+
*
|
|
22
|
+
* Two things this gets wrong if hardcoded, both of which broke real apps:
|
|
23
|
+
*
|
|
24
|
+
* - **Pages Router routes on presence.** EVERY file under `pages/` is a route, so a component
|
|
25
|
+
* dropped there has no default export: `/reticle-dev` 500s and `next build` fails. It has to go
|
|
26
|
+
* somewhere that is not a route directory. App Router routes on FILENAME (`page`/`layout`/
|
|
27
|
+
* `route`), so an extra file beside the layout is inert and can stay there.
|
|
28
|
+
* - **The extension is not cosmetic.** A `.tsx` file in a JavaScript project makes Next auto-install
|
|
29
|
+
* TypeScript on the next `next dev` — and on Next 13 that takes its require-hook down with it, so
|
|
30
|
+
* the dev server never starts. The install then looks like it broke the app, because it did.
|
|
31
|
+
*/
|
|
32
|
+
export declare function reticleDevLocation(mountPath: string, typescript: boolean): ReticleDevLocation;
|
|
33
|
+
export declare function patchNextConfig(source: string): SourcePatch;
|
|
34
|
+
/**
|
|
35
|
+
* Pages Router has no root layout to mount into — `app/layout.tsx` does not exist — so the connect
|
|
36
|
+
* rides in `pages/_app` instead, wrapped in a fragment alongside the page. Without this, `init`
|
|
37
|
+
* wrote `app/reticle-dev.tsx` into a directory the app does not have and nothing ever connected,
|
|
38
|
+
* with no error to say so.
|
|
39
|
+
*/
|
|
40
|
+
export declare function patchPagesApp(source: string, importSpecifier?: string): SourcePatch;
|
|
41
|
+
export declare function patchRootLayout(source: string): SourcePatch;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, conservative patchers for the two files a Next app needed edited BY HAND: `next.config.*`
|
|
3
|
+
* (wrap the export in `withReticle`) and `app/layout.tsx` (mount `<ReticleDev />`). Same contract as
|
|
4
|
+
* the Vite patcher — recognise the obvious shape, bail to `manual` on anything ambiguous, never
|
|
5
|
+
* half-edit. Leaving these manual is why Next connected 0% of the time: both edits are silent when
|
|
6
|
+
* skipped, and one of them is JSX.
|
|
7
|
+
*/
|
|
8
|
+
import { PatchKind } from './patch-kind.js';
|
|
9
|
+
const RETICLE_NEXT_PACKAGE = '@reticlehq/next';
|
|
10
|
+
export const NEXT_CONFIG_IMPORT = `import { withReticle } from '${RETICLE_NEXT_PACKAGE}';`;
|
|
11
|
+
export const NEXT_CONFIG_REQUIRE = `const { withReticle } = require('${RETICLE_NEXT_PACKAGE}');`;
|
|
12
|
+
const RETICLE_DEV_COMPONENT = 'ReticleDev';
|
|
13
|
+
const RETICLE_DEV_BASENAME = 'reticle-dev';
|
|
14
|
+
/** Default import specifier: the component sits beside the file importing it (App Router). */
|
|
15
|
+
const RETICLE_DEV_SIBLING = `./${RETICLE_DEV_BASENAME}`;
|
|
16
|
+
const reticleDevImport = (specifier) => `import { ${RETICLE_DEV_COMPONENT} } from '${specifier}';`;
|
|
17
|
+
export const RETICLE_DEV_IMPORT = reticleDevImport(RETICLE_DEV_SIBLING);
|
|
18
|
+
/**
|
|
19
|
+
* Decide where the connect component lives, from the file that will mount it.
|
|
20
|
+
*
|
|
21
|
+
* Two things this gets wrong if hardcoded, both of which broke real apps:
|
|
22
|
+
*
|
|
23
|
+
* - **Pages Router routes on presence.** EVERY file under `pages/` is a route, so a component
|
|
24
|
+
* dropped there has no default export: `/reticle-dev` 500s and `next build` fails. It has to go
|
|
25
|
+
* somewhere that is not a route directory. App Router routes on FILENAME (`page`/`layout`/
|
|
26
|
+
* `route`), so an extra file beside the layout is inert and can stay there.
|
|
27
|
+
* - **The extension is not cosmetic.** A `.tsx` file in a JavaScript project makes Next auto-install
|
|
28
|
+
* TypeScript on the next `next dev` — and on Next 13 that takes its require-hook down with it, so
|
|
29
|
+
* the dev server never starts. The install then looks like it broke the app, because it did.
|
|
30
|
+
*/
|
|
31
|
+
export function reticleDevLocation(mountPath, typescript) {
|
|
32
|
+
const ext = typescript ? '.tsx' : '.jsx';
|
|
33
|
+
const slash = mountPath.lastIndexOf('/');
|
|
34
|
+
const dir = slash === -1 ? '' : mountPath.slice(0, slash);
|
|
35
|
+
const isPagesRouter = /(^|\/)pages$/.test(dir);
|
|
36
|
+
if (!isPagesRouter) {
|
|
37
|
+
return {
|
|
38
|
+
path: `${dir === '' ? '' : `${dir}/`}${RETICLE_DEV_BASENAME}${ext}`,
|
|
39
|
+
importSpecifier: RETICLE_DEV_SIBLING,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
// Siblings of `pages/`, so `src/pages/_app.js` → `src/components/…` and `pages/_app.js` → `components/…`.
|
|
43
|
+
const parent = dir.slice(0, Math.max(0, dir.length - 'pages'.length));
|
|
44
|
+
return {
|
|
45
|
+
path: `${parent}components/${RETICLE_DEV_BASENAME}${ext}`,
|
|
46
|
+
importSpecifier: `../components/${RETICLE_DEV_BASENAME}`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** The dev-guarded mount. Production strips it — `process.env.NODE_ENV` is inlined at build time. */
|
|
50
|
+
const RETICLE_DEV_MOUNT = `{process.env.NODE_ENV === 'development' ? <${RETICLE_DEV_COMPONENT} /> : null}`;
|
|
51
|
+
/** `export default <expr>;` — the ESM shape every `create-next-app` config uses. */
|
|
52
|
+
const ESM_DEFAULT_EXPORT = /export\s+default\s+([\s\S]+?);?\s*$/;
|
|
53
|
+
/** `module.exports = <expr>;` — the CJS shape older configs use. */
|
|
54
|
+
const CJS_EXPORT = /module\.exports\s*=\s*([\s\S]+?);?\s*$/;
|
|
55
|
+
/** The opening `<body ...>` tag, whose first child is where the mount goes. */
|
|
56
|
+
const BODY_OPEN_TAG = /<body(\s[^>]*)?>/g;
|
|
57
|
+
const NO_EXPORT_REASON = "couldn't find an `export default` or `module.exports` to wrap";
|
|
58
|
+
const NO_BODY_REASON = "couldn't find a single <body> tag to mount <ReticleDev /> inside";
|
|
59
|
+
export function patchNextConfig(source) {
|
|
60
|
+
if (source.includes(RETICLE_NEXT_PACKAGE))
|
|
61
|
+
return { kind: PatchKind.ALREADY };
|
|
62
|
+
const esm = ESM_DEFAULT_EXPORT.exec(source);
|
|
63
|
+
if (esm?.[1] !== undefined) {
|
|
64
|
+
const wrapped = source.replace(ESM_DEFAULT_EXPORT, `export default withReticle(${esm[1]});\n`);
|
|
65
|
+
return { kind: PatchKind.APPLY, code: `${NEXT_CONFIG_IMPORT}\n${wrapped}` };
|
|
66
|
+
}
|
|
67
|
+
const cjs = CJS_EXPORT.exec(source);
|
|
68
|
+
if (cjs?.[1] !== undefined) {
|
|
69
|
+
const wrapped = source.replace(CJS_EXPORT, `module.exports = withReticle(${cjs[1]});\n`);
|
|
70
|
+
return { kind: PatchKind.APPLY, code: `${NEXT_CONFIG_REQUIRE}\n${wrapped}` };
|
|
71
|
+
}
|
|
72
|
+
return { kind: PatchKind.MANUAL, reason: NO_EXPORT_REASON };
|
|
73
|
+
}
|
|
74
|
+
/** `<Component {...pageProps} />` — the one element every `pages/_app` renders. */
|
|
75
|
+
const PAGES_APP_COMPONENT = /<Component\b[^>]*\/>/g;
|
|
76
|
+
const NO_PAGES_COMPONENT_REASON = "couldn't find a single <Component {...pageProps} /> to wrap in pages/_app";
|
|
77
|
+
/**
|
|
78
|
+
* Pages Router has no root layout to mount into — `app/layout.tsx` does not exist — so the connect
|
|
79
|
+
* rides in `pages/_app` instead, wrapped in a fragment alongside the page. Without this, `init`
|
|
80
|
+
* wrote `app/reticle-dev.tsx` into a directory the app does not have and nothing ever connected,
|
|
81
|
+
* with no error to say so.
|
|
82
|
+
*/
|
|
83
|
+
export function patchPagesApp(source, importSpecifier = RETICLE_DEV_SIBLING) {
|
|
84
|
+
if (source.includes(RETICLE_DEV_COMPONENT))
|
|
85
|
+
return { kind: PatchKind.ALREADY };
|
|
86
|
+
const matches = [...source.matchAll(PAGES_APP_COMPONENT)];
|
|
87
|
+
const match = matches.length === 1 ? matches[0] : undefined;
|
|
88
|
+
if (match?.index === undefined)
|
|
89
|
+
return { kind: PatchKind.MANUAL, reason: NO_PAGES_COMPONENT_REASON };
|
|
90
|
+
const wrapped = `<>${RETICLE_DEV_MOUNT}${match[0]}</>`;
|
|
91
|
+
const code = source.slice(0, match.index) + wrapped + source.slice(match.index + match[0].length);
|
|
92
|
+
return { kind: PatchKind.APPLY, code: `${reticleDevImport(importSpecifier)}\n${code}` };
|
|
93
|
+
}
|
|
94
|
+
export function patchRootLayout(source) {
|
|
95
|
+
if (source.includes(RETICLE_DEV_COMPONENT))
|
|
96
|
+
return { kind: PatchKind.ALREADY };
|
|
97
|
+
const tags = [...source.matchAll(BODY_OPEN_TAG)];
|
|
98
|
+
// Exactly one <body> or we cannot tell which one actually renders — and mounting into the wrong
|
|
99
|
+
// one is the same silent no-connect as not mounting at all.
|
|
100
|
+
const tag = tags.length === 1 ? tags[0] : undefined;
|
|
101
|
+
if (tag?.index === undefined)
|
|
102
|
+
return { kind: PatchKind.MANUAL, reason: NO_BODY_REASON };
|
|
103
|
+
const insertAt = tag.index + tag[0].length;
|
|
104
|
+
const mounted = `${source.slice(0, insertAt)}${RETICLE_DEV_MOUNT}${source.slice(insertAt)}`;
|
|
105
|
+
return { kind: PatchKind.APPLY, code: `${RETICLE_DEV_IMPORT}\n${mounted}` };
|
|
106
|
+
}
|
package/dist/init/node-io.js
CHANGED
|
@@ -38,6 +38,31 @@ export function buildNodeIo(cwd) {
|
|
|
38
38
|
}
|
|
39
39
|
});
|
|
40
40
|
},
|
|
41
|
+
listDirs(rel) {
|
|
42
|
+
const path = abs(rel);
|
|
43
|
+
if (!existsSync(path))
|
|
44
|
+
return [];
|
|
45
|
+
try {
|
|
46
|
+
return readdirSync(path).filter((name) => statSync(join(path, name)).isDirectory());
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
listFiles(rel) {
|
|
53
|
+
const path = abs(rel);
|
|
54
|
+
if (!existsSync(path))
|
|
55
|
+
return [];
|
|
56
|
+
try {
|
|
57
|
+
return readdirSync(path).filter((name) => !statSync(join(path, name)).isDirectory());
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
scoped(rel) {
|
|
64
|
+
return buildNodeIo(abs(rel));
|
|
65
|
+
},
|
|
41
66
|
exec(command, args) {
|
|
42
67
|
// `shell: true` lets package-manager shims (pnpm.cmd, etc.) resolve on Windows; inherit
|
|
43
68
|
// stdio so the install's own progress is visible to the user.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The outcome vocabulary shared by every conservative source patcher in `init` (Vite config, Next
|
|
3
|
+
* config, Next root layout). One enum so a new patcher cannot invent a fourth status the reporter
|
|
4
|
+
* has no symbol for.
|
|
5
|
+
*/
|
|
6
|
+
export declare const PatchKind: {
|
|
7
|
+
readonly APPLY: "apply";
|
|
8
|
+
readonly ALREADY: "already";
|
|
9
|
+
readonly MANUAL: "manual";
|
|
10
|
+
};
|
|
11
|
+
export type PatchKind = (typeof PatchKind)[keyof typeof PatchKind];
|
|
12
|
+
export type SourcePatch = {
|
|
13
|
+
kind: typeof PatchKind.APPLY;
|
|
14
|
+
code: string;
|
|
15
|
+
} | {
|
|
16
|
+
kind: typeof PatchKind.ALREADY;
|
|
17
|
+
} | {
|
|
18
|
+
kind: typeof PatchKind.MANUAL;
|
|
19
|
+
reason: string;
|
|
20
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The outcome vocabulary shared by every conservative source patcher in `init` (Vite config, Next
|
|
3
|
+
* config, Next root layout). One enum so a new patcher cannot invent a fourth status the reporter
|
|
4
|
+
* has no symbol for.
|
|
5
|
+
*/
|
|
6
|
+
export const PatchKind = {
|
|
7
|
+
APPLY: 'apply',
|
|
8
|
+
ALREADY: 'already',
|
|
9
|
+
MANUAL: 'manual',
|
|
10
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How each framework gets wired: the Vite plugin, the three Next files, the SvelteKit client hook.
|
|
3
|
+
* Split out of `plan.ts` — that file is the plan's SHAPE (statuses, ordering, the agent/MCP steps),
|
|
4
|
+
* this one is the per-framework detail, and they grow for different reasons.
|
|
5
|
+
*/
|
|
6
|
+
import { type PlanInput, type Step } from './plan.js';
|
|
7
|
+
/** What adding `reticle()` to a Vite config buys, which differs by framework. */
|
|
8
|
+
export declare const VITE_PLUGIN_DETAIL: {
|
|
9
|
+
/** A plain Vite app gets both halves from the plugin. */
|
|
10
|
+
readonly VITE: "add reticle() to plugins (also injects connect())";
|
|
11
|
+
/**
|
|
12
|
+
* SvelteKit renders through app.html, so the plugin's HTML injection never fires and connect()
|
|
13
|
+
* comes from the client hook instead. The plugin is still required: it is what stamps
|
|
14
|
+
* data-reticle-source into .svelte components, and without it every verdict on a SvelteKit app
|
|
15
|
+
* comes back with no file:line at all.
|
|
16
|
+
*/
|
|
17
|
+
readonly SVELTEKIT: "add reticle() to plugins (stamps data-reticle-source in .svelte components)";
|
|
18
|
+
};
|
|
19
|
+
export declare const CAPABILITIES_TITLE = "Capabilities + store";
|
|
20
|
+
export declare function viteSteps(input: PlanInput, detail?: string): Step[];
|
|
21
|
+
/**
|
|
22
|
+
* Next used to be the ONLY stack with hand edits left over — and both of them fail silently when
|
|
23
|
+
* skipped, so a Next user's app booted, connected to nothing, and said nothing about why. Both are
|
|
24
|
+
* now patched by the same conservative rules the Vite config gets.
|
|
25
|
+
*/
|
|
26
|
+
export declare function nextSteps(input: PlanInput): Step[];
|
|
27
|
+
/**
|
|
28
|
+
* SvelteKit is WIRED but not SUPPORTED, and the plan says so out loud.
|
|
29
|
+
*
|
|
30
|
+
* There is no SvelteKit app in `apps/` and no CI gate for one, so nothing proves this hook still
|
|
31
|
+
* registers a session — every other framework init offers (React, Next, Remix, Astro) has both. The
|
|
32
|
+
* wiring is real and may well work; what is missing is anything that would tell us when it stops.
|
|
33
|
+
* Silently emitting it reads as a support claim, which is the thing this project exists to not do.
|
|
34
|
+
*/
|
|
35
|
+
export declare function svelteKitSteps(input: PlanInput): Step[];
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How each framework gets wired: the Vite plugin, the three Next files, the SvelteKit client hook.
|
|
3
|
+
* Split out of `plan.ts` — that file is the plan's SHAPE (statuses, ordering, the agent/MCP steps),
|
|
4
|
+
* this one is the per-framework detail, and they grow for different reasons.
|
|
5
|
+
*/
|
|
6
|
+
import { patchViteConfig, VitePatchKind } from './vite-config.js';
|
|
7
|
+
import { patchNextConfig, patchRootLayout, patchPagesApp } from './next-patch.js';
|
|
8
|
+
import { PatchKind } from './patch-kind.js';
|
|
9
|
+
import { viteManual, NEXT_LAYOUT_MANUAL, NEXT_LAYOUT_PATH, viteDevModuleFile, VITE_DEV_MODULE_PATH, nextReticleDevFile, NEXT_RETICLE_DEV_PATH, nextConfigManual, svelteKitHooksFile, SVELTEKIT_HOOKS_PATH, UNVERIFIED_FRAMEWORK_NOTE, } from './snippets.js';
|
|
10
|
+
import { StepStatus } from './plan.js';
|
|
11
|
+
/** What adding `reticle()` to a Vite config buys, which differs by framework. */
|
|
12
|
+
export const VITE_PLUGIN_DETAIL = {
|
|
13
|
+
/** A plain Vite app gets both halves from the plugin. */
|
|
14
|
+
VITE: 'add reticle() to plugins (also injects connect())',
|
|
15
|
+
/**
|
|
16
|
+
* SvelteKit renders through app.html, so the plugin's HTML injection never fires and connect()
|
|
17
|
+
* comes from the client hook instead. The plugin is still required: it is what stamps
|
|
18
|
+
* data-reticle-source into .svelte components, and without it every verdict on a SvelteKit app
|
|
19
|
+
* comes back with no file:line at all.
|
|
20
|
+
*/
|
|
21
|
+
SVELTEKIT: 'add reticle() to plugins (stamps data-reticle-source in .svelte components)',
|
|
22
|
+
};
|
|
23
|
+
export const CAPABILITIES_TITLE = 'Capabilities + store';
|
|
24
|
+
/**
|
|
25
|
+
* The dev module carrying `registerCapabilities` / `registerStore`.
|
|
26
|
+
*
|
|
27
|
+
* Without it every app came up `hasCapabilities: false` with a `reticle_state` holding nothing but
|
|
28
|
+
* `__reticle_renders` — the state-truth read was unavailable on every app out of the box. Written
|
|
29
|
+
* only when absent, because it is the one generated file a user is expected to EDIT.
|
|
30
|
+
*/
|
|
31
|
+
function capabilitiesStep(input) {
|
|
32
|
+
if (input.viteDevModuleExists === true) {
|
|
33
|
+
return [
|
|
34
|
+
{
|
|
35
|
+
title: CAPABILITIES_TITLE,
|
|
36
|
+
target: VITE_DEV_MODULE_PATH,
|
|
37
|
+
status: StepStatus.ALREADY,
|
|
38
|
+
detail: 'file exists — left alone, it is yours to edit',
|
|
39
|
+
},
|
|
40
|
+
];
|
|
41
|
+
}
|
|
42
|
+
const testids = input.testids ?? [];
|
|
43
|
+
const stores = input.storeHints ?? [];
|
|
44
|
+
const found = testids.length > 0 ? `${String(testids.length)} data-testid values` : 'no data-testid values yet';
|
|
45
|
+
return [
|
|
46
|
+
{
|
|
47
|
+
title: CAPABILITIES_TITLE,
|
|
48
|
+
target: VITE_DEV_MODULE_PATH,
|
|
49
|
+
status: StepStatus.APPLY,
|
|
50
|
+
detail: `${found}; ${stores.length > 0 ? `store: uncomment the ${String(stores.length)} suggested line(s)` : 'no state library detected'}`,
|
|
51
|
+
write: { path: VITE_DEV_MODULE_PATH, content: viteDevModuleFile(testids, stores) },
|
|
52
|
+
dependsOnInstall: true,
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
}
|
|
56
|
+
export function viteSteps(input, detail = VITE_PLUGIN_DETAIL.VITE) {
|
|
57
|
+
// Capabilities are independent of whether the config needed patching. Attaching them to the APPLY
|
|
58
|
+
// branch meant a re-run on an already-wired app silently never created the module.
|
|
59
|
+
return [...viteConfigSteps(input, detail), ...capabilitiesStep(input)];
|
|
60
|
+
}
|
|
61
|
+
function viteConfigSteps(input, detail) {
|
|
62
|
+
const cfg = input.viteConfig;
|
|
63
|
+
const port = input.options.port;
|
|
64
|
+
if (cfg === null) {
|
|
65
|
+
return [
|
|
66
|
+
{
|
|
67
|
+
title: 'Vite plugin',
|
|
68
|
+
target: 'vite.config',
|
|
69
|
+
status: StepStatus.MANUAL,
|
|
70
|
+
detail: viteManual(port),
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
}
|
|
74
|
+
const patch = patchViteConfig(cfg.source, port);
|
|
75
|
+
if (patch.kind === VitePatchKind.ALREADY) {
|
|
76
|
+
return [
|
|
77
|
+
{
|
|
78
|
+
title: 'Vite plugin',
|
|
79
|
+
target: cfg.path,
|
|
80
|
+
status: StepStatus.ALREADY,
|
|
81
|
+
detail: 'reticle() already in plugins',
|
|
82
|
+
},
|
|
83
|
+
];
|
|
84
|
+
}
|
|
85
|
+
if (patch.kind === VitePatchKind.MANUAL) {
|
|
86
|
+
return [
|
|
87
|
+
{
|
|
88
|
+
title: 'Vite plugin',
|
|
89
|
+
target: cfg.path,
|
|
90
|
+
status: StepStatus.MANUAL,
|
|
91
|
+
detail: `${patch.reason}\n\n${viteManual(port)}`,
|
|
92
|
+
},
|
|
93
|
+
];
|
|
94
|
+
}
|
|
95
|
+
return [
|
|
96
|
+
{
|
|
97
|
+
title: 'Vite plugin',
|
|
98
|
+
target: cfg.path,
|
|
99
|
+
status: StepStatus.APPLY,
|
|
100
|
+
detail,
|
|
101
|
+
write: { path: cfg.path, content: patch.code },
|
|
102
|
+
dependsOnInstall: true,
|
|
103
|
+
},
|
|
104
|
+
];
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Turn a conservative source patch into a step: applied when it patched, already when the wiring is
|
|
108
|
+
* there, and the hand-edit instructions when the file shape wasn't one we recognise.
|
|
109
|
+
*/
|
|
110
|
+
function patchStep(title, path, patch, applyDetail, manualDetail) {
|
|
111
|
+
if (patch.kind === PatchKind.ALREADY) {
|
|
112
|
+
return { title, target: path, status: StepStatus.ALREADY, detail: 'already wired' };
|
|
113
|
+
}
|
|
114
|
+
if (patch.kind === PatchKind.MANUAL) {
|
|
115
|
+
return {
|
|
116
|
+
title,
|
|
117
|
+
target: path,
|
|
118
|
+
status: StepStatus.MANUAL,
|
|
119
|
+
detail: `${patch.reason}\n\n${manualDetail}`,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
title,
|
|
124
|
+
target: path,
|
|
125
|
+
status: StepStatus.APPLY,
|
|
126
|
+
detail: applyDetail,
|
|
127
|
+
write: { path, content: patch.code },
|
|
128
|
+
dependsOnInstall: true,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Next used to be the ONLY stack with hand edits left over — and both of them fail silently when
|
|
133
|
+
* skipped, so a Next user's app booted, connected to nothing, and said nothing about why. Both are
|
|
134
|
+
* now patched by the same conservative rules the Vite config gets.
|
|
135
|
+
*/
|
|
136
|
+
export function nextSteps(input) {
|
|
137
|
+
const configFile = input.nextConfigFile ?? 'next.config.mjs';
|
|
138
|
+
const devPath = input.nextReticleDevPath ?? NEXT_RETICLE_DEV_PATH;
|
|
139
|
+
const devFile = input.nextReticleDevExists
|
|
140
|
+
? {
|
|
141
|
+
title: 'ReticleDev component',
|
|
142
|
+
target: devPath,
|
|
143
|
+
status: StepStatus.ALREADY,
|
|
144
|
+
detail: 'file exists',
|
|
145
|
+
}
|
|
146
|
+
: {
|
|
147
|
+
title: 'ReticleDev component',
|
|
148
|
+
target: devPath,
|
|
149
|
+
status: StepStatus.APPLY,
|
|
150
|
+
detail: 'create dev-only connect component',
|
|
151
|
+
write: {
|
|
152
|
+
path: devPath,
|
|
153
|
+
content: nextReticleDevFile(input.options.port, input.options.projectId, input.testids ?? [], input.storeHints ?? []),
|
|
154
|
+
},
|
|
155
|
+
dependsOnInstall: true,
|
|
156
|
+
};
|
|
157
|
+
const configPatch = input.nextConfigSource === null || input.nextConfigSource === undefined
|
|
158
|
+
? { kind: PatchKind.MANUAL, reason: `no ${configFile} found` }
|
|
159
|
+
: patchNextConfig(input.nextConfigSource);
|
|
160
|
+
const layout = input.nextLayout ?? null;
|
|
161
|
+
// Pages Router mounts through pages/_app, App Router through the root layout — different edits,
|
|
162
|
+
// and picking by path is what stops a Pages app being handed the layout patch that cannot apply.
|
|
163
|
+
const isPagesRouter = layout !== null && /(^|\/)pages\/_app\.[jt]sx?$/.test(layout.path);
|
|
164
|
+
const layoutPatch = layout === null
|
|
165
|
+
? { kind: PatchKind.MANUAL, reason: 'no root layout (app/layout.tsx) or pages/_app found' }
|
|
166
|
+
: isPagesRouter
|
|
167
|
+
? patchPagesApp(layout.source, input.nextReticleDevImport)
|
|
168
|
+
: patchRootLayout(layout.source);
|
|
169
|
+
return [
|
|
170
|
+
devFile,
|
|
171
|
+
patchStep('Next config (withReticle)', configFile, configPatch, 'wrap the export in withReticle (source mapping, dev-only)', nextConfigManual(configFile)),
|
|
172
|
+
patchStep('Mount ReticleDev', layout?.path ?? NEXT_LAYOUT_PATH, layoutPatch, 'mount <ReticleDev /> in the root layout (dev-only)', NEXT_LAYOUT_MANUAL),
|
|
173
|
+
];
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* SvelteKit is WIRED but not SUPPORTED, and the plan says so out loud.
|
|
177
|
+
*
|
|
178
|
+
* There is no SvelteKit app in `apps/` and no CI gate for one, so nothing proves this hook still
|
|
179
|
+
* registers a session — every other framework init offers (React, Next, Remix, Astro) has both. The
|
|
180
|
+
* wiring is real and may well work; what is missing is anything that would tell us when it stops.
|
|
181
|
+
* Silently emitting it reads as a support claim, which is the thing this project exists to not do.
|
|
182
|
+
*/
|
|
183
|
+
export function svelteKitSteps(input) {
|
|
184
|
+
const unverified = {
|
|
185
|
+
title: 'SvelteKit is UNVERIFIED',
|
|
186
|
+
target: SVELTEKIT_HOOKS_PATH,
|
|
187
|
+
status: StepStatus.NOTICE,
|
|
188
|
+
detail: UNVERIFIED_FRAMEWORK_NOTE,
|
|
189
|
+
};
|
|
190
|
+
// SvelteKit can't use the Vite-plugin injection (it renders via app.html) — wire a client hook
|
|
191
|
+
// that SvelteKit runs on startup, which is the path that can register a session at all.
|
|
192
|
+
if (input.svelteKitHooksExists === true) {
|
|
193
|
+
return [
|
|
194
|
+
unverified,
|
|
195
|
+
{
|
|
196
|
+
title: 'Reticle client hook',
|
|
197
|
+
target: SVELTEKIT_HOOKS_PATH,
|
|
198
|
+
status: StepStatus.ALREADY,
|
|
199
|
+
detail: 'file exists',
|
|
200
|
+
},
|
|
201
|
+
];
|
|
202
|
+
}
|
|
203
|
+
return [
|
|
204
|
+
unverified,
|
|
205
|
+
{
|
|
206
|
+
title: 'Reticle client hook',
|
|
207
|
+
target: SVELTEKIT_HOOKS_PATH,
|
|
208
|
+
status: StepStatus.APPLY,
|
|
209
|
+
detail: 'create dev-only client connect (SvelteKit renders via app.html)',
|
|
210
|
+
write: {
|
|
211
|
+
path: SVELTEKIT_HOOKS_PATH,
|
|
212
|
+
content: svelteKitHooksFile(input.options.port, input.options.projectId),
|
|
213
|
+
},
|
|
214
|
+
dependsOnInstall: true,
|
|
215
|
+
},
|
|
216
|
+
];
|
|
217
|
+
}
|