@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.
Files changed (114) hide show
  1. package/LICENSE +105 -0
  2. package/NOTICE +5 -0
  3. package/README.md +29 -0
  4. package/dist/agent-rules.d.ts +116 -0
  5. package/dist/agent-rules.js +279 -0
  6. package/dist/app-choice.d.ts +29 -0
  7. package/dist/app-choice.js +39 -0
  8. package/dist/astro-host.d.ts +21 -0
  9. package/dist/astro-host.js +27 -0
  10. package/dist/astro-patch.d.ts +33 -0
  11. package/dist/astro-patch.js +103 -0
  12. package/dist/brace-scan.d.ts +24 -0
  13. package/dist/brace-scan.js +46 -0
  14. package/dist/capabilities.d.ts +40 -0
  15. package/dist/capabilities.js +167 -0
  16. package/dist/closing-hint.d.ts +35 -0
  17. package/dist/closing-hint.js +122 -0
  18. package/dist/codex-toml.d.ts +10 -0
  19. package/dist/codex-toml.js +144 -0
  20. package/dist/connect-steps.d.ts +55 -0
  21. package/dist/connect-steps.js +92 -0
  22. package/dist/cra.d.ts +82 -0
  23. package/dist/cra.js +183 -0
  24. package/dist/csp-check.d.ts +49 -0
  25. package/dist/csp-check.js +168 -0
  26. package/dist/csp-doctor.d.ts +32 -0
  27. package/dist/csp-doctor.js +62 -0
  28. package/dist/desktop-doctor.d.ts +46 -0
  29. package/dist/desktop-doctor.js +201 -0
  30. package/dist/detect.d.ts +225 -0
  31. package/dist/detect.js +334 -0
  32. package/dist/dev-script.d.ts +51 -0
  33. package/dist/dev-script.js +66 -0
  34. package/dist/dev-server-ports.d.ts +20 -0
  35. package/dist/dev-server-ports.js +45 -0
  36. package/dist/electron-patch.d.ts +11 -0
  37. package/dist/electron-patch.js +70 -0
  38. package/dist/electron-vite-patch.d.ts +12 -0
  39. package/dist/electron-vite-patch.js +100 -0
  40. package/dist/existing-config.d.ts +31 -0
  41. package/dist/existing-config.js +63 -0
  42. package/dist/format-generated.d.ts +18 -0
  43. package/dist/format-generated.js +48 -0
  44. package/dist/framework-adapter.d.ts +60 -0
  45. package/dist/framework-adapter.js +138 -0
  46. package/dist/host.d.ts +41 -0
  47. package/dist/host.js +21 -0
  48. package/dist/index.d.ts +25 -0
  49. package/dist/index.js +25 -0
  50. package/dist/init-failure.d.ts +19 -0
  51. package/dist/init-failure.js +18 -0
  52. package/dist/init-opt-out.d.ts +17 -0
  53. package/dist/init-opt-out.js +30 -0
  54. package/dist/install-hint.d.ts +9 -0
  55. package/dist/install-hint.js +41 -0
  56. package/dist/install-retries.d.ts +22 -0
  57. package/dist/install-retries.js +65 -0
  58. package/dist/install-source-config.d.ts +18 -0
  59. package/dist/install-source-config.js +34 -0
  60. package/dist/lockfiles.d.ts +14 -0
  61. package/dist/lockfiles.js +49 -0
  62. package/dist/mcp-clients.d.ts +94 -0
  63. package/dist/mcp-clients.js +327 -0
  64. package/dist/mcp-registered.d.ts +12 -0
  65. package/dist/mcp-registered.js +14 -0
  66. package/dist/mcp.d.ts +89 -0
  67. package/dist/mcp.js +129 -0
  68. package/dist/next-patch.d.ts +49 -0
  69. package/dist/next-patch.js +237 -0
  70. package/dist/node-io.d.ts +22 -0
  71. package/dist/node-io.js +150 -0
  72. package/dist/non-js-project.d.ts +44 -0
  73. package/dist/non-js-project.js +137 -0
  74. package/dist/nuxt-patch.d.ts +18 -0
  75. package/dist/nuxt-patch.js +33 -0
  76. package/dist/patch-kind.d.ts +20 -0
  77. package/dist/patch-kind.js +10 -0
  78. package/dist/plan-electron-vite.d.ts +16 -0
  79. package/dist/plan-electron-vite.js +72 -0
  80. package/dist/plan-framework.d.ts +122 -0
  81. package/dist/plan-framework.js +773 -0
  82. package/dist/plan.d.ts +312 -0
  83. package/dist/plan.js +650 -0
  84. package/dist/platform.d.ts +16 -0
  85. package/dist/platform.js +16 -0
  86. package/dist/preflight.d.ts +30 -0
  87. package/dist/preflight.js +52 -0
  88. package/dist/project-id.d.ts +12 -0
  89. package/dist/project-id.js +25 -0
  90. package/dist/refresh-rules.d.ts +40 -0
  91. package/dist/refresh-rules.js +51 -0
  92. package/dist/remember-project.d.ts +24 -0
  93. package/dist/remember-project.js +40 -0
  94. package/dist/run.d.ts +122 -0
  95. package/dist/run.js +762 -0
  96. package/dist/slash-command.d.ts +27 -0
  97. package/dist/slash-command.js +132 -0
  98. package/dist/snippets.d.ts +267 -0
  99. package/dist/snippets.js +898 -0
  100. package/dist/tanstack-start.d.ts +18 -0
  101. package/dist/tanstack-start.js +57 -0
  102. package/dist/version.d.ts +18 -0
  103. package/dist/version.js +23 -0
  104. package/dist/vite-config.d.ts +18 -0
  105. package/dist/vite-config.js +111 -0
  106. package/dist/vite-owning-config.d.ts +25 -0
  107. package/dist/vite-owning-config.js +219 -0
  108. package/dist/windows-quote.d.ts +38 -0
  109. package/dist/windows-quote.js +70 -0
  110. package/dist/workspace-apps.d.ts +40 -0
  111. package/dist/workspace-apps.js +188 -0
  112. package/dist/workspace-redirect.d.ts +22 -0
  113. package/dist/workspace-redirect.js +97 -0
  114. 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;
@@ -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;