@reticlehq/init 2.14.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/LICENSE +105 -0
- package/NOTICE +5 -0
- package/README.md +29 -0
- package/dist/agent-rules.d.ts +116 -0
- package/dist/agent-rules.js +279 -0
- package/dist/app-choice.d.ts +29 -0
- package/dist/app-choice.js +39 -0
- package/dist/astro-host.d.ts +21 -0
- package/dist/astro-host.js +27 -0
- package/dist/astro-patch.d.ts +33 -0
- package/dist/astro-patch.js +103 -0
- package/dist/brace-scan.d.ts +24 -0
- package/dist/brace-scan.js +46 -0
- package/dist/capabilities.d.ts +40 -0
- package/dist/capabilities.js +167 -0
- package/dist/closing-hint.d.ts +35 -0
- package/dist/closing-hint.js +122 -0
- package/dist/codex-toml.d.ts +10 -0
- package/dist/codex-toml.js +144 -0
- package/dist/connect-steps.d.ts +55 -0
- package/dist/connect-steps.js +92 -0
- package/dist/cra.d.ts +82 -0
- package/dist/cra.js +183 -0
- package/dist/csp-check.d.ts +49 -0
- package/dist/csp-check.js +168 -0
- package/dist/csp-doctor.d.ts +32 -0
- package/dist/csp-doctor.js +62 -0
- package/dist/desktop-doctor.d.ts +46 -0
- package/dist/desktop-doctor.js +201 -0
- package/dist/detect.d.ts +225 -0
- package/dist/detect.js +334 -0
- package/dist/dev-script.d.ts +51 -0
- package/dist/dev-script.js +66 -0
- package/dist/dev-server-ports.d.ts +20 -0
- package/dist/dev-server-ports.js +45 -0
- package/dist/electron-patch.d.ts +11 -0
- package/dist/electron-patch.js +70 -0
- package/dist/electron-vite-patch.d.ts +12 -0
- package/dist/electron-vite-patch.js +100 -0
- package/dist/existing-config.d.ts +31 -0
- package/dist/existing-config.js +63 -0
- package/dist/format-generated.d.ts +18 -0
- package/dist/format-generated.js +48 -0
- package/dist/framework-adapter.d.ts +60 -0
- package/dist/framework-adapter.js +138 -0
- package/dist/host.d.ts +41 -0
- package/dist/host.js +21 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +25 -0
- package/dist/init-failure.d.ts +19 -0
- package/dist/init-failure.js +18 -0
- package/dist/init-opt-out.d.ts +17 -0
- package/dist/init-opt-out.js +30 -0
- package/dist/install-hint.d.ts +9 -0
- package/dist/install-hint.js +41 -0
- package/dist/install-retries.d.ts +22 -0
- package/dist/install-retries.js +65 -0
- package/dist/install-source-config.d.ts +18 -0
- package/dist/install-source-config.js +34 -0
- package/dist/lockfiles.d.ts +14 -0
- package/dist/lockfiles.js +49 -0
- package/dist/mcp-clients.d.ts +94 -0
- package/dist/mcp-clients.js +327 -0
- package/dist/mcp-registered.d.ts +12 -0
- package/dist/mcp-registered.js +14 -0
- package/dist/mcp.d.ts +89 -0
- package/dist/mcp.js +129 -0
- package/dist/next-patch.d.ts +49 -0
- package/dist/next-patch.js +237 -0
- package/dist/node-io.d.ts +22 -0
- package/dist/node-io.js +150 -0
- package/dist/non-js-project.d.ts +44 -0
- package/dist/non-js-project.js +137 -0
- package/dist/nuxt-patch.d.ts +18 -0
- package/dist/nuxt-patch.js +33 -0
- package/dist/patch-kind.d.ts +20 -0
- package/dist/patch-kind.js +10 -0
- package/dist/plan-electron-vite.d.ts +16 -0
- package/dist/plan-electron-vite.js +72 -0
- package/dist/plan-framework.d.ts +122 -0
- package/dist/plan-framework.js +773 -0
- package/dist/plan.d.ts +312 -0
- package/dist/plan.js +650 -0
- package/dist/platform.d.ts +16 -0
- package/dist/platform.js +16 -0
- package/dist/preflight.d.ts +30 -0
- package/dist/preflight.js +52 -0
- package/dist/project-id.d.ts +12 -0
- package/dist/project-id.js +25 -0
- package/dist/refresh-rules.d.ts +40 -0
- package/dist/refresh-rules.js +51 -0
- package/dist/remember-project.d.ts +24 -0
- package/dist/remember-project.js +40 -0
- package/dist/run.d.ts +122 -0
- package/dist/run.js +762 -0
- package/dist/slash-command.d.ts +27 -0
- package/dist/slash-command.js +132 -0
- package/dist/snippets.d.ts +267 -0
- package/dist/snippets.js +898 -0
- package/dist/tanstack-start.d.ts +18 -0
- package/dist/tanstack-start.js +57 -0
- package/dist/version.d.ts +18 -0
- package/dist/version.js +23 -0
- package/dist/vite-config.d.ts +18 -0
- package/dist/vite-config.js +111 -0
- package/dist/vite-owning-config.d.ts +25 -0
- package/dist/vite-owning-config.js +219 -0
- package/dist/windows-quote.d.ts +38 -0
- package/dist/windows-quote.js +70 -0
- package/dist/workspace-apps.d.ts +40 -0
- package/dist/workspace-apps.js +188 -0
- package/dist/workspace-redirect.d.ts +22 -0
- package/dist/workspace-redirect.js +97 -0
- package/package.json +53 -0
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
/** Where the generated connect component goes, and how the mount file should import it. */
|
|
10
|
+
interface ReticleDevLocation {
|
|
11
|
+
/** Project-relative path to write. */
|
|
12
|
+
path: string;
|
|
13
|
+
/** What the mount file (`app/layout.*` or `pages/_app.*`) should import. */
|
|
14
|
+
importSpecifier: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Decide where the connect component lives, from the file that will mount it.
|
|
18
|
+
*
|
|
19
|
+
* Two things this gets wrong if hardcoded, both of which broke real apps:
|
|
20
|
+
*
|
|
21
|
+
* - **Pages Router routes on presence.** EVERY file under `pages/` is a route, so a component
|
|
22
|
+
* dropped there has no default export: `/reticle-dev` 500s and `next build` fails. It has to go
|
|
23
|
+
* somewhere that is not a route directory. App Router routes on FILENAME (`page`/`layout`/
|
|
24
|
+
* `route`), so an extra file beside the layout is inert and can stay there.
|
|
25
|
+
* - **The extension is not cosmetic.** A `.tsx` file in a JavaScript project makes Next auto-install
|
|
26
|
+
* TypeScript on the next `next dev` — and on Next 13 that takes its require-hook down with it, so
|
|
27
|
+
* the dev server never starts. The install then looks like it broke the app, because it did.
|
|
28
|
+
*/
|
|
29
|
+
export declare function reticleDevLocation(mountPath: string, typescript: boolean): ReticleDevLocation;
|
|
30
|
+
export declare function patchNextConfig(source: string, sourceMapping?: boolean): SourcePatch;
|
|
31
|
+
/**
|
|
32
|
+
* Pages Router has no root layout to mount into — `app/layout.tsx` does not exist — so the connect
|
|
33
|
+
* rides in `pages/_app` instead, wrapped in a fragment alongside the page. Without this, `init`
|
|
34
|
+
* wrote `app/reticle-dev.tsx` into a directory the app does not have and nothing ever connected,
|
|
35
|
+
* with no error to say so.
|
|
36
|
+
*/
|
|
37
|
+
export declare function patchPagesApp(source: string, importSpecifier?: string): SourcePatch;
|
|
38
|
+
export declare function patchRootLayout(source: string): SourcePatch;
|
|
39
|
+
/**
|
|
40
|
+
* Root-layout candidates, App Router only. `--src-dir` apps keep theirs under `src/app`, and the
|
|
41
|
+
* ReticleDev component has to land NEXT TO the layout or the relative import it generates is dead.
|
|
42
|
+
*/
|
|
43
|
+
export declare const NEXT_LAYOUT_CANDIDATES: string[];
|
|
44
|
+
/**
|
|
45
|
+
* Pages Router mount points, checked only when there is no App Router layout. A Pages app has no
|
|
46
|
+
* `app/` directory at all, so writing the component there produced a file nothing imported.
|
|
47
|
+
*/
|
|
48
|
+
export declare const NEXT_PAGES_APP_CANDIDATES: string[];
|
|
49
|
+
export {};
|
|
@@ -0,0 +1,237 @@
|
|
|
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
|
+
const NEXT_CONFIG_IMPORT = `import { withReticle } from '${RETICLE_NEXT_PACKAGE}';`;
|
|
11
|
+
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
|
+
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 = -1 === slash ? '' : 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
|
+
/**
|
|
52
|
+
* `export default ` / `module.exports = ` at the START of a line — the head of the assignment, not
|
|
53
|
+
* the whole statement. Where the exported expression ENDS is decided by `expressionEnd` below.
|
|
54
|
+
*
|
|
55
|
+
* These used to be `/…([\s\S]+?);?\s*$/`, which reads as "the expression, lazily". It is not: with
|
|
56
|
+
* no `m` flag the `$` is end-of-FILE and the trailing `;?\s*` is satisfiable only there, so the
|
|
57
|
+
* capture always ran to the last non-blank character of the file. Every config whose export was not
|
|
58
|
+
* the final statement got everything after it swallowed into the wrap. On a config exporting
|
|
59
|
+
* conditionally that produced an unbalanced paren — a syntax error, so `next dev` exited 1 while
|
|
60
|
+
* `init` reported the step as ✓ and the only symptom was "dev server never served".
|
|
61
|
+
*/
|
|
62
|
+
const ESM_DEFAULT_HEAD = /^[ \t]*export[ \t]+default[ \t]+/gm;
|
|
63
|
+
const CJS_EXPORT_HEAD = /^[ \t]*module\.exports[ \t]*=[ \t]*/gm;
|
|
64
|
+
/** The opening `<body ...>` tag, whose first child is where the mount goes. */
|
|
65
|
+
const BODY_OPEN_TAG = /<body(\s[^>]*)?>/g;
|
|
66
|
+
const NO_EXPORT_REASON = "couldn't find an `export default` or `module.exports` to wrap";
|
|
67
|
+
const NO_BODY_REASON = "couldn't find a single <body> tag to mount <ReticleDev /> inside";
|
|
68
|
+
/** Bracket pairs that keep an expression open. A `;` at depth 0 is what ends it. */
|
|
69
|
+
const OPENERS = '([{';
|
|
70
|
+
const CLOSERS = ')]}';
|
|
71
|
+
const LINE_COMMENT = '//';
|
|
72
|
+
const BLOCK_COMMENT_OPEN = '/*';
|
|
73
|
+
const BLOCK_COMMENT_CLOSE = '*/';
|
|
74
|
+
const QUOTES = '\'"`';
|
|
75
|
+
/**
|
|
76
|
+
* Index just past the exported expression that starts at `from` — the `;` that closes it, or end of
|
|
77
|
+
* file. Tracks bracket depth so a multi-line object or a nested call ends in the right place, and
|
|
78
|
+
* skips strings and comments so a `;` or a brace inside one cannot end it early.
|
|
79
|
+
*
|
|
80
|
+
* Deliberately a scanner and not a parser: `next.config.*` is arbitrary JS and this file's contract
|
|
81
|
+
* is to recognise the obvious shape or bail. It only has to be right about where ONE expression
|
|
82
|
+
* stops, and wrong is caught by the caller bailing to manual, never by a half-edit.
|
|
83
|
+
*/
|
|
84
|
+
function expressionEnd(source, from) {
|
|
85
|
+
let depth = 0;
|
|
86
|
+
for (let i = from; i < source.length; i++) {
|
|
87
|
+
const ch = source[i] ?? '';
|
|
88
|
+
const pair = source.slice(i, i + 2);
|
|
89
|
+
if (pair === LINE_COMMENT) {
|
|
90
|
+
const nl = source.indexOf('\n', i);
|
|
91
|
+
if (-1 === nl)
|
|
92
|
+
return source.length;
|
|
93
|
+
i = nl;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (pair === BLOCK_COMMENT_OPEN) {
|
|
97
|
+
const close = source.indexOf(BLOCK_COMMENT_CLOSE, i + 2);
|
|
98
|
+
i = -1 === close ? source.length : close + 1;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (QUOTES.includes(ch)) {
|
|
102
|
+
for (i++; i < source.length; i++) {
|
|
103
|
+
if ('\\' === source[i])
|
|
104
|
+
i++;
|
|
105
|
+
else if (source[i] === ch)
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (OPENERS.includes(ch))
|
|
111
|
+
depth++;
|
|
112
|
+
else if (CLOSERS.includes(ch)) {
|
|
113
|
+
// A closer at depth 0 belongs to an enclosing block, so the expression ended before it.
|
|
114
|
+
if (0 === depth)
|
|
115
|
+
return i;
|
|
116
|
+
depth--;
|
|
117
|
+
}
|
|
118
|
+
else if (';' === ch && 0 === depth)
|
|
119
|
+
return i;
|
|
120
|
+
}
|
|
121
|
+
return source.length;
|
|
122
|
+
}
|
|
123
|
+
/** Every top-level export assignment in the file, in source order. */
|
|
124
|
+
function exportSites(source, head) {
|
|
125
|
+
const sites = [];
|
|
126
|
+
for (const match of source.matchAll(head)) {
|
|
127
|
+
if (match.index === undefined)
|
|
128
|
+
continue;
|
|
129
|
+
const start = match.index + match[0].length;
|
|
130
|
+
sites.push({ start, end: expressionEnd(source, start) });
|
|
131
|
+
}
|
|
132
|
+
return sites;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Wrap every listed expression in `withReticle(...)`, leaving every other byte of the file alone.
|
|
136
|
+
*
|
|
137
|
+
* All of them, not just the first: a config can export CONDITIONALLY — Sentry-wrapped in one branch,
|
|
138
|
+
* plain in the other — and which branch runs is an environment variable's business, not ours.
|
|
139
|
+
* Wrapping each assignment is correct whichever one executes, and it is what turns that shape from a
|
|
140
|
+
* manual step (an app that boots and never connects) into a working install.
|
|
141
|
+
*
|
|
142
|
+
* Applied back-to-front so each splice leaves the earlier offsets valid.
|
|
143
|
+
*/
|
|
144
|
+
function wrapAll(source, sites, secondArg) {
|
|
145
|
+
let out = source;
|
|
146
|
+
for (const site of [...sites].sort((a, b) => b.start - a.start)) {
|
|
147
|
+
out = `${out.slice(0, site.start)}withReticle(${out.slice(site.start, site.end)}${secondArg})${out.slice(site.end)}`;
|
|
148
|
+
}
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* The second argument to `withReticle`, or nothing at all.
|
|
153
|
+
*
|
|
154
|
+
* A Next app has no plugin call to carry `sourceMapping: false`, so the only place to say it is the
|
|
155
|
+
* config wrap itself. Omitted entirely when the stamp stays on, because an install that appends
|
|
156
|
+
* `, {}` to every config it touches is a diff against the user's file for no reason.
|
|
157
|
+
*/
|
|
158
|
+
function withReticleOptions(sourceMapping) {
|
|
159
|
+
return sourceMapping ? '' : ', { sourceMapping: false }';
|
|
160
|
+
}
|
|
161
|
+
export function patchNextConfig(source, sourceMapping = true) {
|
|
162
|
+
if (source.includes(RETICLE_NEXT_PACKAGE))
|
|
163
|
+
return { kind: PatchKind.ALREADY };
|
|
164
|
+
const options = withReticleOptions(sourceMapping);
|
|
165
|
+
const esm = exportSites(source, ESM_DEFAULT_HEAD);
|
|
166
|
+
if (esm.length > 0) {
|
|
167
|
+
return {
|
|
168
|
+
kind: PatchKind.APPLY,
|
|
169
|
+
code: `${NEXT_CONFIG_IMPORT}\n${wrapAll(source, esm, options)}`,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
const cjs = exportSites(source, CJS_EXPORT_HEAD);
|
|
173
|
+
if (cjs.length > 0) {
|
|
174
|
+
return {
|
|
175
|
+
kind: PatchKind.APPLY,
|
|
176
|
+
code: `${NEXT_CONFIG_REQUIRE}\n${wrapAll(source, cjs, options)}`,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
return { kind: PatchKind.MANUAL, reason: NO_EXPORT_REASON };
|
|
180
|
+
}
|
|
181
|
+
/** `<Component {...pageProps} />` — the one element every `pages/_app` renders. */
|
|
182
|
+
const PAGES_APP_COMPONENT = /<Component\b[^>]*\/>/g;
|
|
183
|
+
const NO_PAGES_COMPONENT_REASON = "couldn't find a single <Component {...pageProps} /> to wrap in pages/_app";
|
|
184
|
+
/**
|
|
185
|
+
* Pages Router has no root layout to mount into — `app/layout.tsx` does not exist — so the connect
|
|
186
|
+
* rides in `pages/_app` instead, wrapped in a fragment alongside the page. Without this, `init`
|
|
187
|
+
* wrote `app/reticle-dev.tsx` into a directory the app does not have and nothing ever connected,
|
|
188
|
+
* with no error to say so.
|
|
189
|
+
*/
|
|
190
|
+
export function patchPagesApp(source, importSpecifier = RETICLE_DEV_SIBLING) {
|
|
191
|
+
if (source.includes(RETICLE_DEV_COMPONENT))
|
|
192
|
+
return { kind: PatchKind.ALREADY };
|
|
193
|
+
const matches = [...source.matchAll(PAGES_APP_COMPONENT)];
|
|
194
|
+
const match = 1 === matches.length ? matches[0] : undefined;
|
|
195
|
+
if (match?.index === undefined)
|
|
196
|
+
return { kind: PatchKind.MANUAL, reason: NO_PAGES_COMPONENT_REASON };
|
|
197
|
+
const wrapped = `<>${RETICLE_DEV_MOUNT}${match[0]}</>`;
|
|
198
|
+
const code = source.slice(0, match.index) + wrapped + source.slice(match.index + match[0].length);
|
|
199
|
+
return { kind: PatchKind.APPLY, code: `${reticleDevImport(importSpecifier)}\n${code}` };
|
|
200
|
+
}
|
|
201
|
+
export function patchRootLayout(source) {
|
|
202
|
+
if (source.includes(RETICLE_DEV_COMPONENT))
|
|
203
|
+
return { kind: PatchKind.ALREADY };
|
|
204
|
+
const tags = [...source.matchAll(BODY_OPEN_TAG)];
|
|
205
|
+
// Exactly one <body> or we cannot tell which one actually renders — and mounting into the wrong
|
|
206
|
+
// one is the same silent no-connect as not mounting at all.
|
|
207
|
+
const tag = 1 === tags.length ? tags[0] : undefined;
|
|
208
|
+
if (tag?.index === undefined)
|
|
209
|
+
return { kind: PatchKind.MANUAL, reason: NO_BODY_REASON };
|
|
210
|
+
const insertAt = tag.index + tag[0].length;
|
|
211
|
+
const mounted = `${source.slice(0, insertAt)}${RETICLE_DEV_MOUNT}${source.slice(insertAt)}`;
|
|
212
|
+
return { kind: PatchKind.APPLY, code: `${RETICLE_DEV_IMPORT}\n${mounted}` };
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Root-layout candidates, App Router only. `--src-dir` apps keep theirs under `src/app`, and the
|
|
216
|
+
* ReticleDev component has to land NEXT TO the layout or the relative import it generates is dead.
|
|
217
|
+
*/
|
|
218
|
+
export const NEXT_LAYOUT_CANDIDATES = [
|
|
219
|
+
'app/layout.tsx',
|
|
220
|
+
'app/layout.jsx',
|
|
221
|
+
'app/layout.js',
|
|
222
|
+
'src/app/layout.tsx',
|
|
223
|
+
'src/app/layout.jsx',
|
|
224
|
+
'src/app/layout.js',
|
|
225
|
+
];
|
|
226
|
+
/**
|
|
227
|
+
* Pages Router mount points, checked only when there is no App Router layout. A Pages app has no
|
|
228
|
+
* `app/` directory at all, so writing the component there produced a file nothing imported.
|
|
229
|
+
*/
|
|
230
|
+
export const NEXT_PAGES_APP_CANDIDATES = [
|
|
231
|
+
'pages/_app.tsx',
|
|
232
|
+
'pages/_app.jsx',
|
|
233
|
+
'pages/_app.js',
|
|
234
|
+
'src/pages/_app.tsx',
|
|
235
|
+
'src/pages/_app.jsx',
|
|
236
|
+
'src/pages/_app.js',
|
|
237
|
+
];
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The real (Node filesystem) implementation of `InitIo`. Kept separate from the pure runner so
|
|
3
|
+
* `runInit` stays testable with an in-memory IO. Prints to stdout — `init` is a one-shot CLI
|
|
4
|
+
* command, not the MCP stdio transport.
|
|
5
|
+
*/
|
|
6
|
+
import type { InitIo } from './run.js';
|
|
7
|
+
import type { InitHost } from './host.js';
|
|
8
|
+
/**
|
|
9
|
+
* Every program `init` is ever allowed to run.
|
|
10
|
+
*
|
|
11
|
+
* The set is closed and tiny — the four package managers, `npx`, and the Claude CLI — because those
|
|
12
|
+
* are the only things the plan can ask for. It is a named constant for the same reason every other
|
|
13
|
+
* wire string here is one, and it is CHECKED because of `shellOpt`: on Windows these spawn through
|
|
14
|
+
* a shell, and a shell turns the command name into something the shell parses rather than a file it
|
|
15
|
+
* executes. Arguments are quoted by `shellSafe`; the command name was the half nothing covered.
|
|
16
|
+
*
|
|
17
|
+
* A command outside this set is a programming error — the plan built something no branch of this
|
|
18
|
+
* package can produce — so it throws rather than returning `false`, which would read to the caller
|
|
19
|
+
* as "the command ran and failed" and hide the bug in a retry.
|
|
20
|
+
*/
|
|
21
|
+
export declare const RUNNABLE_COMMANDS: readonly string[];
|
|
22
|
+
export declare function buildNodeIo(cwd: string, host: InitHost): InitIo;
|
package/dist/node-io.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The real (Node filesystem) implementation of `InitIo`. Kept separate from the pure runner so
|
|
3
|
+
* `runInit` stays testable with an in-memory IO. Prints to stdout — `init` is a one-shot CLI
|
|
4
|
+
* command, not the MCP stdio transport.
|
|
5
|
+
*/
|
|
6
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync, accessSync, constants, } from 'node:fs';
|
|
7
|
+
import { NodePlatform } from './platform.js';
|
|
8
|
+
import { join, dirname, isAbsolute } from 'node:path';
|
|
9
|
+
import { homedir } from 'node:os';
|
|
10
|
+
import { spawnSync } from 'node:child_process';
|
|
11
|
+
import { windowsShellArg } from './windows-quote.js';
|
|
12
|
+
/**
|
|
13
|
+
* `shell: true` ONLY where it earns its keep.
|
|
14
|
+
*
|
|
15
|
+
* It exists so package-manager shims (`pnpm.cmd`, `npx.cmd`) resolve on Windows. On POSIX it buys
|
|
16
|
+
* nothing and costs correctness: under a shell, arguments are re-parsed, so a path containing a
|
|
17
|
+
* space — `/Users/ada/My Projects/app` — silently becomes two arguments and registration fails with
|
|
18
|
+
* no error anyone can read.
|
|
19
|
+
*/
|
|
20
|
+
function shellOpt() {
|
|
21
|
+
return NodePlatform.WINDOWS === process.platform ? { shell: true } : {};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Under a shell, quote what the shell would otherwise split or interpret. Windows only, where
|
|
25
|
+
* `shell: true` is required (see shellOpt). The rule itself lives in windows-quote.ts as a pure
|
|
26
|
+
* function so it can be tested on every platform — keeping it inside this branch is why it was
|
|
27
|
+
* wrong for as long as it was.
|
|
28
|
+
*/
|
|
29
|
+
function shellSafe(args) {
|
|
30
|
+
if (process.platform !== NodePlatform.WINDOWS)
|
|
31
|
+
return [...args];
|
|
32
|
+
return args.map(windowsShellArg);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Every program `init` is ever allowed to run.
|
|
36
|
+
*
|
|
37
|
+
* The set is closed and tiny — the four package managers, `npx`, and the Claude CLI — because those
|
|
38
|
+
* are the only things the plan can ask for. It is a named constant for the same reason every other
|
|
39
|
+
* wire string here is one, and it is CHECKED because of `shellOpt`: on Windows these spawn through
|
|
40
|
+
* a shell, and a shell turns the command name into something the shell parses rather than a file it
|
|
41
|
+
* executes. Arguments are quoted by `shellSafe`; the command name was the half nothing covered.
|
|
42
|
+
*
|
|
43
|
+
* A command outside this set is a programming error — the plan built something no branch of this
|
|
44
|
+
* package can produce — so it throws rather than returning `false`, which would read to the caller
|
|
45
|
+
* as "the command ran and failed" and hide the bug in a retry.
|
|
46
|
+
*/
|
|
47
|
+
export const RUNNABLE_COMMANDS = ['pnpm', 'yarn', 'bun', 'npm', 'npx', 'claude'];
|
|
48
|
+
/** The command, proven to be one of `RUNNABLE_COMMANDS` — never the caller's string. */
|
|
49
|
+
function runnable(command) {
|
|
50
|
+
const found = RUNNABLE_COMMANDS.find((allowed) => allowed === command);
|
|
51
|
+
if (undefined === found) {
|
|
52
|
+
throw new Error(`init refused to run ${JSON.stringify(command)}: not one of ${RUNNABLE_COMMANDS.join(', ')}.`);
|
|
53
|
+
}
|
|
54
|
+
return found;
|
|
55
|
+
}
|
|
56
|
+
export function buildNodeIo(cwd, host) {
|
|
57
|
+
// Project-relative by default; absolute paths (e.g. ~/.cursor/mcp.json) pass through unchanged.
|
|
58
|
+
const abs = (rel) => (isAbsolute(rel) ? rel : join(cwd, rel));
|
|
59
|
+
return {
|
|
60
|
+
readFile(rel) {
|
|
61
|
+
const path = abs(rel);
|
|
62
|
+
if (!existsSync(path))
|
|
63
|
+
return null;
|
|
64
|
+
return readFileSync(path, 'utf8');
|
|
65
|
+
},
|
|
66
|
+
writeFile(rel, content) {
|
|
67
|
+
const path = abs(rel);
|
|
68
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
69
|
+
writeFileSync(path, content, 'utf8');
|
|
70
|
+
},
|
|
71
|
+
exists(rel) {
|
|
72
|
+
return existsSync(abs(rel));
|
|
73
|
+
},
|
|
74
|
+
homeDir() {
|
|
75
|
+
return homedir();
|
|
76
|
+
},
|
|
77
|
+
cwd() {
|
|
78
|
+
return cwd;
|
|
79
|
+
},
|
|
80
|
+
rootFiles() {
|
|
81
|
+
return readdirSync(cwd).filter((name) => {
|
|
82
|
+
try {
|
|
83
|
+
return statSync(join(cwd, name)).isFile();
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
},
|
|
90
|
+
listDirs(rel) {
|
|
91
|
+
const path = abs(rel);
|
|
92
|
+
if (!existsSync(path))
|
|
93
|
+
return [];
|
|
94
|
+
try {
|
|
95
|
+
return readdirSync(path).filter((name) => statSync(join(path, name)).isDirectory());
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
listFiles(rel) {
|
|
102
|
+
const path = abs(rel);
|
|
103
|
+
if (!existsSync(path))
|
|
104
|
+
return [];
|
|
105
|
+
try {
|
|
106
|
+
return readdirSync(path).filter((name) => !statSync(join(path, name)).isDirectory());
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
scoped(rel) {
|
|
113
|
+
// The host travels with the re-rooted IO: a monorepo redirect re-enters `runInit` through
|
|
114
|
+
// this, and an untraced, unreported inner run is the half of init that actually did the work.
|
|
115
|
+
return buildNodeIo(abs(rel), host);
|
|
116
|
+
},
|
|
117
|
+
exec(command, args) {
|
|
118
|
+
// Inherit stdio so the install's own progress is visible to the user.
|
|
119
|
+
const result = spawnSync(runnable(command), shellSafe(args), {
|
|
120
|
+
cwd,
|
|
121
|
+
stdio: 'inherit',
|
|
122
|
+
...shellOpt(),
|
|
123
|
+
});
|
|
124
|
+
return 0 === result.status;
|
|
125
|
+
},
|
|
126
|
+
/** One access check, rather than discovering it as an EACCES stack four phases later. */
|
|
127
|
+
canWrite() {
|
|
128
|
+
try {
|
|
129
|
+
accessSync(cwd, constants.W_OK);
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
probe(command, args) {
|
|
137
|
+
// Quiet yes/no check (CLI availability, existing registration). Never throws.
|
|
138
|
+
const result = spawnSync(runnable(command), shellSafe(args), {
|
|
139
|
+
cwd,
|
|
140
|
+
stdio: 'ignore',
|
|
141
|
+
...shellOpt(),
|
|
142
|
+
});
|
|
143
|
+
return 0 === result.status;
|
|
144
|
+
},
|
|
145
|
+
print(line) {
|
|
146
|
+
process.stdout.write(`${line}\n`);
|
|
147
|
+
},
|
|
148
|
+
host,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telling a Python (or Ruby, or Go, or PHP) developer why Reticle cannot wire their app.
|
|
3
|
+
*
|
|
4
|
+
* `reticle init` answered "No package.json found here, and no app directory beneath it either",
|
|
5
|
+
* which reads as *you are in the wrong directory* — so an agent on a Streamlit app went looking for
|
|
6
|
+
* a directory that does not exist, then went looking for a standalone browser bundle to inject by
|
|
7
|
+
* hand, and only worked out the real answer several steps later.
|
|
8
|
+
*
|
|
9
|
+
* The real answer is not about directories at all: Reticle's SDK has to be imported by the app's own
|
|
10
|
+
* JS build, and a server-rendered Python app has no JS build to import it from. That is a property
|
|
11
|
+
* of the project, it is knowable from one look at the filesystem, and saying it plainly costs the
|
|
12
|
+
* reader nothing while the directory message costs them the whole investigation.
|
|
13
|
+
*
|
|
14
|
+
* Deliberately a hint, not a refusal: a Django or Rails app with a `frontend/` package.json is a
|
|
15
|
+
* perfectly ordinary Reticle install, and this only ever speaks when there is no package.json to be
|
|
16
|
+
* found anywhere.
|
|
17
|
+
*/
|
|
18
|
+
/** Whether the no-package project is specifically a Streamlit app. */
|
|
19
|
+
export declare function detectStreamlitProject(readFile: (file: string) => string | null, rootFiles: readonly string[]): boolean;
|
|
20
|
+
/** The Streamlit-specific explanation printed immediately before its generated helper. */
|
|
21
|
+
export declare function streamlitSetupMessage(): string;
|
|
22
|
+
/**
|
|
23
|
+
* Whether the no-package project is specifically a Django app.
|
|
24
|
+
*
|
|
25
|
+
* `manage.py` is the marker every Django project has and nothing else does, and it is already in
|
|
26
|
+
* ECOSYSTEM_MARKERS as `Python (Django)` — this asks the narrower question so the setup message can
|
|
27
|
+
* be Django's rather than Python's in general.
|
|
28
|
+
*
|
|
29
|
+
* Confirmed by `settings.py` alongside it where possible, because a bare `manage.py` in a repo root
|
|
30
|
+
* that also holds the real app deeper down should not claim more than it knows.
|
|
31
|
+
*/
|
|
32
|
+
export declare function detectDjangoProject(exists: (file: string) => boolean, rootDirectories?: readonly string[]): boolean;
|
|
33
|
+
/** The Django-specific explanation printed immediately before its generated middleware. */
|
|
34
|
+
export declare function djangoSetupMessage(): string;
|
|
35
|
+
/** The ecosystem this directory looks like, or undefined when nothing says. */
|
|
36
|
+
export declare function detectNonJsEcosystem(exists: (file: string) => boolean): string | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* What `init` should say when there is no package.json.
|
|
39
|
+
*
|
|
40
|
+
* Two genuinely different situations behind one old sentence: a JS developer standing in the wrong
|
|
41
|
+
* directory, and a developer whose project is not JavaScript at all. The first needs a path; the
|
|
42
|
+
* second needs to know that no path exists and why.
|
|
43
|
+
*/
|
|
44
|
+
export declare function noPackageJsonMessage(exists: (file: string) => boolean): string;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telling a Python (or Ruby, or Go, or PHP) developer why Reticle cannot wire their app.
|
|
3
|
+
*
|
|
4
|
+
* `reticle init` answered "No package.json found here, and no app directory beneath it either",
|
|
5
|
+
* which reads as *you are in the wrong directory* — so an agent on a Streamlit app went looking for
|
|
6
|
+
* a directory that does not exist, then went looking for a standalone browser bundle to inject by
|
|
7
|
+
* hand, and only worked out the real answer several steps later.
|
|
8
|
+
*
|
|
9
|
+
* The real answer is not about directories at all: Reticle's SDK has to be imported by the app's own
|
|
10
|
+
* JS build, and a server-rendered Python app has no JS build to import it from. That is a property
|
|
11
|
+
* of the project, it is knowable from one look at the filesystem, and saying it plainly costs the
|
|
12
|
+
* reader nothing while the directory message costs them the whole investigation.
|
|
13
|
+
*
|
|
14
|
+
* Deliberately a hint, not a refusal: a Django or Rails app with a `frontend/` package.json is a
|
|
15
|
+
* perfectly ordinary Reticle install, and this only ever speaks when there is no package.json to be
|
|
16
|
+
* found anywhere.
|
|
17
|
+
*/
|
|
18
|
+
/** Marker files that identify an ecosystem, and what to call it. */
|
|
19
|
+
const ECOSYSTEM_MARKERS = [
|
|
20
|
+
['requirements.txt', 'Python'],
|
|
21
|
+
['pyproject.toml', 'Python'],
|
|
22
|
+
['Pipfile', 'Python'],
|
|
23
|
+
['setup.py', 'Python'],
|
|
24
|
+
['manage.py', 'Python (Django)'],
|
|
25
|
+
['Gemfile', 'Ruby'],
|
|
26
|
+
['go.mod', 'Go'],
|
|
27
|
+
['composer.json', 'PHP'],
|
|
28
|
+
['Cargo.toml', 'Rust'],
|
|
29
|
+
['pom.xml', 'Java'],
|
|
30
|
+
['build.gradle', 'Java'],
|
|
31
|
+
['mix.exs', 'Elixir'],
|
|
32
|
+
// Not like the others. Every ecosystem above can grow a JS front end that Reticle instruments
|
|
33
|
+
// perfectly well, so their message ends with a lead. Flutter web paints into a canvas, and there
|
|
34
|
+
// is no DOM behind it at any path, so the honest answer is a named refusal.
|
|
35
|
+
['pubspec.yaml', 'Flutter'],
|
|
36
|
+
];
|
|
37
|
+
/** The one ecosystem where no directory, front end or flag could ever help. */
|
|
38
|
+
const FLUTTER = 'Flutter';
|
|
39
|
+
const STREAMLIT_DEPENDENCY_FILES = ['requirements.txt', 'pyproject.toml', 'Pipfile'];
|
|
40
|
+
const STREAMLIT_DEPENDENCY = /(?:^|[\n,"'])\s*streamlit(?:\[[^\]\n]*\])?\s*(?:[<>=!~;]|$)/im;
|
|
41
|
+
const STREAMLIT_IMPORT = /(?:^|\n)\s*(?:import\s+streamlit(?:\s+as\s+\w+)?|from\s+streamlit(?:\.|\s+import))/m;
|
|
42
|
+
/** Whether the no-package project is specifically a Streamlit app. */
|
|
43
|
+
export function detectStreamlitProject(readFile, rootFiles) {
|
|
44
|
+
for (const file of STREAMLIT_DEPENDENCY_FILES) {
|
|
45
|
+
const source = readFile(file);
|
|
46
|
+
if (null !== source && STREAMLIT_DEPENDENCY.test(source))
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
for (const file of rootFiles.filter((name) => name.endsWith('.py'))) {
|
|
50
|
+
const source = readFile(file);
|
|
51
|
+
if (null !== source && STREAMLIT_IMPORT.test(source))
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
/** The Streamlit-specific explanation printed immediately before its generated helper. */
|
|
57
|
+
export function streamlitSetupMessage() {
|
|
58
|
+
return ('This is a Streamlit project. Streamlit owns the top-level HTML document, so there is no ' +
|
|
59
|
+
'served template for the generic script tag, and scripts passed to `st.markdown` do not run. ' +
|
|
60
|
+
'Use Streamlit 1.63 or newer and add the `st.html` helper below during development. It runs ' +
|
|
61
|
+
'Reticle in the app document and guards against duplicate connections across reruns.');
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Whether the no-package project is specifically a Django app.
|
|
65
|
+
*
|
|
66
|
+
* `manage.py` is the marker every Django project has and nothing else does, and it is already in
|
|
67
|
+
* ECOSYSTEM_MARKERS as `Python (Django)` — this asks the narrower question so the setup message can
|
|
68
|
+
* be Django's rather than Python's in general.
|
|
69
|
+
*
|
|
70
|
+
* Confirmed by `settings.py` alongside it where possible, because a bare `manage.py` in a repo root
|
|
71
|
+
* that also holds the real app deeper down should not claim more than it knows.
|
|
72
|
+
*/
|
|
73
|
+
export function detectDjangoProject(exists, rootDirectories = []) {
|
|
74
|
+
if (!exists('manage.py'))
|
|
75
|
+
return false;
|
|
76
|
+
if (exists('settings.py'))
|
|
77
|
+
return true;
|
|
78
|
+
return rootDirectories.some((dir) => exists(`${dir}/settings.py`));
|
|
79
|
+
}
|
|
80
|
+
/** The Django-specific explanation printed immediately before its generated middleware. */
|
|
81
|
+
export function djangoSetupMessage() {
|
|
82
|
+
return ('This is a Django project. Django renders templates server-side, so there is no JS build to ' +
|
|
83
|
+
'import the SDK from and no single template to paste a script tag into — an app has many, and ' +
|
|
84
|
+
'may not have a base one. The middleware below is the Django-shaped answer: it runs for every ' +
|
|
85
|
+
'page, injects Reticle into HTML responses only, and is guarded on `settings.DEBUG` so it can ' +
|
|
86
|
+
'never reach production. Add it to MIDDLEWARE, start your server as usual, and the page will ' +
|
|
87
|
+
'dial the daemon on load. ' +
|
|
88
|
+
'If this project ALSO has a JS front end with its own package.json, running `reticle init` ' +
|
|
89
|
+
'there instead gives you source mapping and framework state as well: point at it with ' +
|
|
90
|
+
'`--app <dir>`.');
|
|
91
|
+
}
|
|
92
|
+
/** The ecosystem this directory looks like, or undefined when nothing says. */
|
|
93
|
+
export function detectNonJsEcosystem(exists) {
|
|
94
|
+
for (const [marker, name] of ECOSYSTEM_MARKERS) {
|
|
95
|
+
if (exists(marker))
|
|
96
|
+
return name;
|
|
97
|
+
}
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* What `init` should say when there is no package.json.
|
|
102
|
+
*
|
|
103
|
+
* Two genuinely different situations behind one old sentence: a JS developer standing in the wrong
|
|
104
|
+
* directory, and a developer whose project is not JavaScript at all. The first needs a path; the
|
|
105
|
+
* second needs to know that no path exists and why.
|
|
106
|
+
*/
|
|
107
|
+
export function noPackageJsonMessage(exists) {
|
|
108
|
+
const ecosystem = detectNonJsEcosystem(exists);
|
|
109
|
+
if (ecosystem === undefined) {
|
|
110
|
+
return ('No package.json found here, and no app directory beneath it either. Run `reticle init` from ' +
|
|
111
|
+
"your app's directory, or from a repo root that contains it.");
|
|
112
|
+
}
|
|
113
|
+
if (ecosystem === FLUTTER) {
|
|
114
|
+
// An agent on a Flutter workspace asked us to "support Flutter, or clearly identify it as
|
|
115
|
+
// unsupported during init". The second is the truthful one, and saying it by name in one line
|
|
116
|
+
// is the difference between an agent that stops and an agent that spends an hour looking for
|
|
117
|
+
// the package.json we told it was missing.
|
|
118
|
+
return ('This is a Flutter project. Reticle cannot instrument it, and no directory or flag will ' +
|
|
119
|
+
'change that: Reticle reads the DOM of a running page, and Flutter web paints its entire UI ' +
|
|
120
|
+
'into a single canvas element with no DOM behind it. Nothing here is missing or misconfigured. ' +
|
|
121
|
+
'If this repo also contains an ordinary web app with its own package.json, run `reticle init` ' +
|
|
122
|
+
'inside that directory instead.');
|
|
123
|
+
}
|
|
124
|
+
return (`This looks like a ${ecosystem} project, and there is no package.json here or in any app ` +
|
|
125
|
+
'directory beneath it. That is not a blocker: Reticle needs its SDK loaded by the page, not ' +
|
|
126
|
+
'built by npm, and a page with no build step can load it from a URL. ' +
|
|
127
|
+
// The old message ended at "there is no directory you could run this from that would change
|
|
128
|
+
// that", which read as a refusal and was acted on as one — agents went looking for a bundler,
|
|
129
|
+
// or for a standalone build to inject by hand, or gave up and used a different tool. The
|
|
130
|
+
// capability was there the whole time; only the sentence was missing. Proven end to end on a
|
|
131
|
+
// page served by `python3 -m http.server` before this line was written.
|
|
132
|
+
'Add the script-tag snippet below to a template you only serve in development, then reload the ' +
|
|
133
|
+
'page. ' +
|
|
134
|
+
'If this project ALSO has a JS front end (a `frontend/`, `client/` or `assets/` directory with ' +
|
|
135
|
+
'its own package.json), running `reticle init` there instead gives you source mapping and ' +
|
|
136
|
+
'framework state as well: point at it with `--app <dir>`.');
|
|
137
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wiring Reticle into a Nuxt app: the config gets the token `define`, the plugin gets written.
|
|
3
|
+
*
|
|
4
|
+
* Nuxt owns its own Vite instance and renders its own HTML, so there is no `vite.config` to patch
|
|
5
|
+
* and no `index.html` to inject into — the same position Astro is in, and it takes the same config
|
|
6
|
+
* patch (see `vite-owning-config.ts`). The half Nuxt does differently is the connect itself: Nuxt's
|
|
7
|
+
* own idiom is a `.client` plugin it auto-registers, not a `<script>` in a layout.
|
|
8
|
+
*
|
|
9
|
+
* The config patch is not optional and not cosmetic. The bridge requires a pairing token even on
|
|
10
|
+
* localhost, nothing in a browser can read the file it lives in, and Nuxt loads no plugin of ours
|
|
11
|
+
* that could inject it — so a plugin written beside an unpatched config is an app that dials the
|
|
12
|
+
* daemon and is refused, with one line in a console nobody had open. That is why the two halves are
|
|
13
|
+
* planned atomically.
|
|
14
|
+
*/
|
|
15
|
+
import type { SourcePatch } from './patch-kind.js';
|
|
16
|
+
/** The config filenames Nuxt itself accepts, in the order `init` should prefer them. */
|
|
17
|
+
export declare const NUXT_CONFIG_CANDIDATES: string[];
|
|
18
|
+
export declare function patchNuxtConfig(source: string): SourcePatch;
|