@reticlehq/init 3.6.0 → 3.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/detect/detect.d.ts +9 -0
- package/dist/detect/detect.js +5 -1
- package/dist/detect/dev-script.d.ts +17 -0
- package/dist/detect/dev-script.js +128 -0
- package/dist/detect/dev-server-ports.d.ts +2 -0
- package/dist/detect/dev-server-ports.js +20 -0
- package/dist/detect/prettier-config.d.ts +21 -0
- package/dist/detect/prettier-config.js +38 -0
- package/dist/detect/workspace-apps.d.ts +11 -0
- package/dist/detect/workspace-apps.js +63 -0
- package/dist/detect/workspace-redirect.js +69 -10
- package/dist/diagnose/closing-hint.d.ts +5 -0
- package/dist/diagnose/closing-hint.js +18 -11
- package/dist/diagnose/init-failure.d.ts +2 -0
- package/dist/diagnose/init-failure.js +2 -0
- package/dist/gather-plan-input.d.ts +11 -0
- package/dist/gather-plan-input.js +468 -0
- package/dist/host.d.ts +6 -0
- package/dist/index.d.ts +11 -4
- package/dist/index.js +14 -4
- package/dist/no-package-json.js +62 -14
- package/dist/node-io.d.ts +1 -1
- package/dist/node-io.js +11 -2
- package/dist/patch/next-patch.d.ts +45 -9
- package/dist/patch/next-patch.js +93 -0
- package/dist/patch/nuxt-snippets.js +1 -1
- package/dist/patch/prettier-ignore.d.ts +34 -0
- package/dist/patch/prettier-ignore.js +49 -0
- package/dist/patch/python-snippets.d.ts +22 -0
- package/dist/patch/python-snippets.js +113 -0
- package/dist/patch/snippets.d.ts +10 -31
- package/dist/patch/snippets.js +42 -105
- package/dist/patch/static-page.d.ts +1 -0
- package/dist/patch/static-page.js +6 -8
- package/dist/patch/vite-config.js +11 -0
- package/dist/plan/connect-steps.d.ts +2 -0
- package/dist/plan/connect-steps.js +6 -0
- package/dist/plan/framework-adapter.d.ts +10 -0
- package/dist/plan/framework-adapter.js +122 -3
- package/dist/plan/notices.js +1 -1
- package/dist/plan/plan-angular.js +1 -1
- package/dist/plan/plan-framework.d.ts +1 -1
- package/dist/plan/plan-framework.js +140 -37
- package/dist/plan/plan-types.d.ts +50 -2
- package/dist/plan/plan-vite.js +15 -0
- package/dist/plan/plan.d.ts +1 -10
- package/dist/plan/plan.js +181 -126
- package/dist/plan/stop-hook-step.d.ts +30 -0
- package/dist/plan/stop-hook-step.js +154 -5
- package/dist/project/agent-rules.d.ts +1 -1
- package/dist/project/agent-rules.js +22 -17
- package/dist/project/refresh-rules.js +2 -1
- package/dist/register/codex-toml.d.ts +12 -0
- package/dist/register/codex-toml.js +44 -0
- package/dist/register/mcp.d.ts +15 -2
- package/dist/register/mcp.js +16 -0
- package/dist/register/prompt-hooks.d.ts +123 -0
- package/dist/register/prompt-hooks.js +449 -0
- package/dist/register/slash-command.d.ts +1 -1
- package/dist/register/slash-command.js +5 -4
- package/dist/run-types.d.ts +10 -0
- package/dist/run.d.ts +1 -1
- package/dist/run.js +15 -390
- package/package.json +2 -2
package/dist/run.js
CHANGED
|
@@ -4,177 +4,25 @@
|
|
|
4
4
|
* access goes through `InitIo` so the orchestration is unit-testable with an in-memory IO.
|
|
5
5
|
*/
|
|
6
6
|
import { OnboardingPhase, OnboardingStepStatus } from '@reticlehq/core/telemetry';
|
|
7
|
-
import { detectMcpClients } from './register/detect-clients.js';
|
|
8
|
-
import { dirname, join } from 'node:path';
|
|
9
|
-
import { CONTAINER_MARKERS, runsDevServer } from './diagnose/containerised-dev-server.js';
|
|
10
7
|
import { explainInstallFailure } from './diagnose/install-retries.js';
|
|
11
|
-
import { CSP_FILES } from './diagnose/csp-doctor.js';
|
|
12
8
|
import { preflight } from './plan/preflight.js';
|
|
13
|
-
import { devCommandFrom
|
|
14
|
-
import { restartHint, FEEDBACK_HINT } from './diagnose/closing-hint.js';
|
|
9
|
+
import { devCommandFrom } from './detect/dev-script.js';
|
|
10
|
+
import { restartHint, COMMUNITY_HINT, FEEDBACK_HINT } from './diagnose/closing-hint.js';
|
|
15
11
|
import { projectIdOf, rememberProjectOnDisk } from './project/remember-project.js';
|
|
16
|
-
import { ANGULAR_WORKSPACE_FILE, detect, FORGE_RENDERER_CONFIGS, Framework, } from './detect/detect.js';
|
|
17
|
-
import { ANGULAR_PROXY_PATH, angularEntry } from './patch/angular.js';
|
|
18
|
-
import { HTML_INDEX_PATH } from './patch/static-page.js';
|
|
19
12
|
import { initWithoutPackageJson } from './no-package-json.js';
|
|
20
13
|
import { sdkPackagesDeclared, sdkPackagesPresent } from './sdk-packages.js';
|
|
21
14
|
import { wasMcpRegistered } from './register/mcp-registered.js';
|
|
22
|
-
import {
|
|
23
|
-
import { ELECTRON_VITE_CONFIG_CANDIDATES, NEXT_CONFIG_CANDIDATES, PACKAGE_JSON, VITE_CONFIG_CANDIDATES, } from './detect/workspace-apps.js';
|
|
15
|
+
import { PACKAGE_JSON } from './detect/workspace-apps.js';
|
|
24
16
|
import { redirectToWorkspaceApp } from './detect/workspace-redirect.js';
|
|
25
|
-
import { enclosingWorkspaceRoot } from './detect/workspace-apps.js';
|
|
17
|
+
import { appsAt, enclosingWorkspaceRoot } from './detect/workspace-apps.js';
|
|
26
18
|
import { isConnectStep } from './plan/connect-steps.js';
|
|
27
|
-
import { CURSOR_RULE_PATH, RETICLE_MD_PATH } from './project/agent-rules.js';
|
|
28
|
-
import { CLAUDE_SETTINGS_PATH } from './plan/stop-hook-step.js';
|
|
29
|
-
import { CRA_ENV_PATH } from './patch/cra.js';
|
|
30
|
-
import { NEXT_LAYOUT_CANDIDATES, NEXT_PAGES_APP_CANDIDATES } from './patch/next-patch.js';
|
|
31
19
|
import { formatGeneratedSource } from './patch/format-generated.js';
|
|
32
|
-
import { tanstackStartConnectPath } from './patch/tanstack-start.js';
|
|
33
|
-
import { installedViteMajor } from './patch/vite-owning-config.js';
|
|
34
|
-
/** CRA's bundled entry, in the order create-react-app itself generates them. */
|
|
35
|
-
const CRA_ENTRY_CANDIDATES = ['src/index.tsx', 'src/index.jsx', 'src/index.ts', 'src/index.js'];
|
|
36
|
-
/** Preview snippets must not mint a real machine credential or imply this token is usable. */
|
|
37
|
-
const DRY_RUN_PAIRING_TOKEN = 'DRY_RUN_TOKEN_NOT_FOR_USE';
|
|
38
|
-
function craEntryOf(io) {
|
|
39
|
-
for (const path of CRA_ENTRY_CANDIDATES) {
|
|
40
|
-
const source = io.readFile(path);
|
|
41
|
-
if (source !== null)
|
|
42
|
-
return { path, source };
|
|
43
|
-
}
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
20
|
import { DEPS_TARGET, RETICLE_CONFIG_FILE, MCP_TARGET, buildPlan, StepStatus, } from './plan/plan.js';
|
|
47
|
-
import { claudeAvailableProbe, claudeHasReticle } from './register/mcp.js';
|
|
48
|
-
import { reticleDevLocation } from './patch/next-patch.js';
|
|
49
|
-
import { scanTestids, storeHints, scanStores } from './detect/capabilities.js';
|
|
50
|
-
import { CLAUDE_PROJECT_CONFIG, CURSOR_PROJECT_MARKER } from './register/mcp-clients.js';
|
|
51
|
-
import { deriveProjectId, packageName } from './project/project-id.js';
|
|
52
|
-
import { VITE_DEV_MODULE_PATH, ELECTRON_VITE_DEV_MODULE_PATH, nuxtPluginPath, } from './patch/snippets.js';
|
|
53
|
-
import { NUXT_CONFIG_CANDIDATES } from './patch/nuxt-patch.js';
|
|
54
|
-
import { CLAUDE_COMMAND_PATH, CURSOR_COMMAND_PATH } from './register/slash-command.js';
|
|
55
|
-
import { RETICLE_VERSION } from './version.js';
|
|
56
21
|
import { InitFailure } from './diagnose/init-failure.js';
|
|
57
22
|
import { resolveLockfiles } from './detect/lockfiles.js';
|
|
23
|
+
import { agentRootOf, gatherPlanInput } from './gather-plan-input.js';
|
|
58
24
|
// Re-exported: it moved to its own module, and every existing importer says `run.js`.
|
|
59
25
|
export { resolveLockfiles };
|
|
60
|
-
const SVELTEKIT_HOOKS = 'src/hooks.client.ts';
|
|
61
|
-
const REACT_ROUTER_ENTRY = 'app/entry.client.tsx';
|
|
62
|
-
const TANSTACK_START_ROOT_CANDIDATES = [
|
|
63
|
-
'src/routes/__root.tsx',
|
|
64
|
-
'src/routes/__root.jsx',
|
|
65
|
-
'app/routes/__root.tsx',
|
|
66
|
-
'app/routes/__root.jsx',
|
|
67
|
-
];
|
|
68
|
-
const SOURCE_FILE = /\.(tsx|jsx|ts|js|svelte|vue|astro)$/;
|
|
69
|
-
/** Files read for the testid scan. A capabilities block is a hint; reading a whole repo for it is not. */
|
|
70
|
-
const MAX_SCANNED_FILES = 200;
|
|
71
|
-
/** Directories that never hold the app's own source, and are expensive or misleading to read. */
|
|
72
|
-
const NOT_SOURCE_DIRS = new Set([
|
|
73
|
-
'node_modules',
|
|
74
|
-
'dist',
|
|
75
|
-
'build',
|
|
76
|
-
'out',
|
|
77
|
-
'coverage',
|
|
78
|
-
'target',
|
|
79
|
-
'public',
|
|
80
|
-
'static',
|
|
81
|
-
'.next',
|
|
82
|
-
'.svelte-kit',
|
|
83
|
-
'.nuxt',
|
|
84
|
-
]);
|
|
85
|
-
/** How far below the app root to look. Deep enough for `modules/users/UserList.tsx`, not a repo crawl. */
|
|
86
|
-
const MAX_SCAN_DEPTH = 5;
|
|
87
|
-
/**
|
|
88
|
-
* Read a bounded set of the app's source files, for the `data-testid` scan.
|
|
89
|
-
*
|
|
90
|
-
* This used to walk the fixed list `src, src/components, src/pages, app, components`. Reported from
|
|
91
|
-
* the field (#318) by a repo whose app is at `src/admin`: the scan read directories that were not
|
|
92
|
-
* the app's and correctly reported finding nothing in them, so `init` said "no data-testid values
|
|
93
|
-
* yet" about an app with several — which makes an agent go and write the ones already there. That
|
|
94
|
-
* list had already grown once for a `frontend/` app, and a third report of the same shape is what
|
|
95
|
-
* says the answer is not another name in a list. So: walk the app root, bounded by depth and by a
|
|
96
|
-
* file count.
|
|
97
|
-
*/
|
|
98
|
-
function readSourceFiles(io) {
|
|
99
|
-
const out = [];
|
|
100
|
-
const read = (path) => {
|
|
101
|
-
const content = io.readFile(path);
|
|
102
|
-
if (content !== null)
|
|
103
|
-
out.push({ path, source: content });
|
|
104
|
-
};
|
|
105
|
-
for (const name of io.rootFiles()) {
|
|
106
|
-
if (out.length >= MAX_SCANNED_FILES)
|
|
107
|
-
return out;
|
|
108
|
-
if (SOURCE_FILE.test(name))
|
|
109
|
-
read(name);
|
|
110
|
-
}
|
|
111
|
-
const walk = (dir, depth) => {
|
|
112
|
-
for (const name of io.listFiles(dir)) {
|
|
113
|
-
if (out.length >= MAX_SCANNED_FILES)
|
|
114
|
-
return;
|
|
115
|
-
if (SOURCE_FILE.test(name))
|
|
116
|
-
read(`${dir}/${name}`);
|
|
117
|
-
}
|
|
118
|
-
if (depth >= MAX_SCAN_DEPTH)
|
|
119
|
-
return;
|
|
120
|
-
for (const sub of io.listDirs(dir)) {
|
|
121
|
-
if (out.length >= MAX_SCANNED_FILES)
|
|
122
|
-
return;
|
|
123
|
-
if (sub.startsWith('.') || NOT_SOURCE_DIRS.has(sub))
|
|
124
|
-
continue;
|
|
125
|
-
walk(`${dir}/${sub}`, depth + 1);
|
|
126
|
-
}
|
|
127
|
-
};
|
|
128
|
-
for (const dir of io.listDirs('.')) {
|
|
129
|
-
if (out.length >= MAX_SCANNED_FILES)
|
|
130
|
-
return out;
|
|
131
|
-
if (dir.startsWith('.') || NOT_SOURCE_DIRS.has(dir))
|
|
132
|
-
continue;
|
|
133
|
-
walk(dir, 1);
|
|
134
|
-
}
|
|
135
|
-
return out;
|
|
136
|
-
}
|
|
137
|
-
/** Direct dependency names, for naming the state libraries an app actually has. */
|
|
138
|
-
function dependencyNames(pkg) {
|
|
139
|
-
const p = (pkg ?? {});
|
|
140
|
-
return new Set([
|
|
141
|
-
...Object.keys(p['dependencies'] ?? {}),
|
|
142
|
-
...Object.keys(p['devDependencies'] ?? {}),
|
|
143
|
-
]);
|
|
144
|
-
}
|
|
145
|
-
/** electron-vite and the unbundled Electron layouts this repo already ships. */
|
|
146
|
-
const ELECTRON_MAIN_SOURCES = [
|
|
147
|
-
'src/main/index.ts',
|
|
148
|
-
'src/main/index.js',
|
|
149
|
-
'src/main/index.mts',
|
|
150
|
-
'src/main/index.mjs',
|
|
151
|
-
'electron/main.cjs',
|
|
152
|
-
'electron/main.js',
|
|
153
|
-
'electron/main.ts',
|
|
154
|
-
'src/main.ts',
|
|
155
|
-
'src/main.js',
|
|
156
|
-
];
|
|
157
|
-
const ELECTRON_PRELOAD_SOURCES = [
|
|
158
|
-
'src/preload/index.ts',
|
|
159
|
-
'src/preload/index.js',
|
|
160
|
-
'src/preload/index.mts',
|
|
161
|
-
'src/preload/index.mjs',
|
|
162
|
-
'electron/preload.cjs',
|
|
163
|
-
'electron/preload.js',
|
|
164
|
-
'electron/preload.ts',
|
|
165
|
-
'src/preload.ts',
|
|
166
|
-
'src/preload.js',
|
|
167
|
-
];
|
|
168
|
-
const ASTRO_CONFIG_CANDIDATES = [
|
|
169
|
-
'astro.config.mjs',
|
|
170
|
-
'astro.config.js',
|
|
171
|
-
'astro.config.ts',
|
|
172
|
-
'astro.config.cjs',
|
|
173
|
-
];
|
|
174
|
-
/** Where a conventional Astro app keeps the layout every page wraps itself in. */
|
|
175
|
-
const ASTRO_LAYOUTS_DIR = 'src/layouts';
|
|
176
|
-
/** Also searched: an app with no layouts directory renders the document straight from a page. */
|
|
177
|
-
const ASTRO_PAGES_DIR = 'src/pages';
|
|
178
26
|
/**
|
|
179
27
|
* `⚠` means WORK LEFT TO DO and nothing else — it is what an agent (and the release gate) counts to
|
|
180
28
|
* decide whether the install finished. A notice gets its own mark so "steps remaining" can reach zero
|
|
@@ -204,234 +52,6 @@ const STATUS_SYMBOL = {
|
|
|
204
52
|
[StepStatus.SKIP]: '–',
|
|
205
53
|
[StepStatus.NOTICE]: 'ℹ',
|
|
206
54
|
};
|
|
207
|
-
function firstPresent(files, candidates) {
|
|
208
|
-
for (const c of candidates)
|
|
209
|
-
if (files.has(c))
|
|
210
|
-
return c;
|
|
211
|
-
return null;
|
|
212
|
-
}
|
|
213
|
-
function firstReadable(io, candidates) {
|
|
214
|
-
for (const path of candidates) {
|
|
215
|
-
const source = io.readFile(path);
|
|
216
|
-
if (source !== null)
|
|
217
|
-
return { path, source };
|
|
218
|
-
}
|
|
219
|
-
return null;
|
|
220
|
-
}
|
|
221
|
-
/**
|
|
222
|
-
* The directory the human's agent runs in, when it is not the app's directory.
|
|
223
|
-
*
|
|
224
|
-
* `undefined` for a single-package repo, which keeps every agent-file path project-relative exactly
|
|
225
|
-
* as it was — the two roots are the same there, which is why writing them beside the app went
|
|
226
|
-
* unnoticed until a repo with its app at `src/admin` reported it.
|
|
227
|
-
*/
|
|
228
|
-
function agentRootOf(options) {
|
|
229
|
-
const root = options.agentRoot;
|
|
230
|
-
return root === undefined || root === options.cwd ? undefined : root;
|
|
231
|
-
}
|
|
232
|
-
/** What the Angular steps read: the workspace file, the entry it names, and our proxy module. */
|
|
233
|
-
function angularInputs(io) {
|
|
234
|
-
const workspace = io.readFile(ANGULAR_WORKSPACE_FILE);
|
|
235
|
-
const entryPath = angularEntry(workspace);
|
|
236
|
-
const entry = io.readFile(entryPath);
|
|
237
|
-
return {
|
|
238
|
-
angularWorkspace: null === workspace ? null : { path: ANGULAR_WORKSPACE_FILE, source: workspace },
|
|
239
|
-
angularEntry: null === entry ? null : { path: entryPath, source: entry },
|
|
240
|
-
angularProxySource: io.readFile(ANGULAR_PROXY_PATH),
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
function gatherPlanInput(options, io, pkg) {
|
|
244
|
-
// Stable identity derived from the app's package.json name + root, so it survives port changes.
|
|
245
|
-
const projectId = deriveProjectId(packageName(pkg), options.cwd);
|
|
246
|
-
const rootFiles = new Set(io.rootFiles());
|
|
247
|
-
const nodeModulesMarkers = new Set(io.listFiles('node_modules'));
|
|
248
|
-
const detectInput = {
|
|
249
|
-
pkg: 'object' === typeof pkg && pkg !== null ? pkg : {},
|
|
250
|
-
configFiles: rootFiles,
|
|
251
|
-
// Walk up for the lockfile so a monorepo sub-package picks the workspace's package manager —
|
|
252
|
-
// unless this package's own installed tree already answers it, which outranks an inherited one.
|
|
253
|
-
lockfiles: resolveLockfiles(rootFiles, options.cwd, io, nodeModulesMarkers),
|
|
254
|
-
// An already-installed tree names its own manager, which matters when no lockfile is committed.
|
|
255
|
-
nodeModulesMarkers,
|
|
256
|
-
};
|
|
257
|
-
const detection = detect(detectInput);
|
|
258
|
-
// Forge has no `vite.config.*`; its renderer config IS the Vite config the plugin belongs in, and
|
|
259
|
-
// handing it over under that name is what lets the ordinary Vite patcher do the work.
|
|
260
|
-
const vitePath = firstPresent(rootFiles, Framework.ELECTRON_FORGE === detection.framework
|
|
261
|
-
? FORGE_RENDERER_CONFIGS
|
|
262
|
-
: VITE_CONFIG_CANDIDATES);
|
|
263
|
-
const viteSource = null === vitePath ? null : io.readFile(vitePath);
|
|
264
|
-
const viteConfig = vitePath !== null && viteSource !== null ? { path: vitePath, source: viteSource } : null;
|
|
265
|
-
const electronVitePath = firstPresent(rootFiles, ELECTRON_VITE_CONFIG_CANDIDATES);
|
|
266
|
-
const electronViteSource = null === electronVitePath ? null : io.readFile(electronVitePath);
|
|
267
|
-
const electronViteConfig = electronVitePath !== null && electronViteSource !== null
|
|
268
|
-
? { path: electronVitePath, source: electronViteSource }
|
|
269
|
-
: null;
|
|
270
|
-
const electronMain = firstReadable(io, ELECTRON_MAIN_SOURCES);
|
|
271
|
-
const electronPreload = firstReadable(io, ELECTRON_PRELOAD_SOURCES);
|
|
272
|
-
// Global MCP registration targets each agent that's present: Claude via its CLI, Cursor via its
|
|
273
|
-
// global config file. Only probe when the MCP step is in play.
|
|
274
|
-
const availableProbe = claudeAvailableProbe();
|
|
275
|
-
const claudeCli = options.mcp ? io.probe(availableProbe.command, availableProbe.args) : false;
|
|
276
|
-
// Read from Claude's config, never `claude mcp get`: that launches the server — see claudeHasReticle.
|
|
277
|
-
const mcpExists = claudeCli ? claudeHasReticle(io, io.cwd()) : false;
|
|
278
|
-
// Every MCP client this machine shows evidence of. Conservative and one-directional: we write
|
|
279
|
-
// into a config a client ALREADY has, and never create ~/.gemini or ~/.codeium for somebody who
|
|
280
|
-
// does not use them.
|
|
281
|
-
const detectedClients = options.mcp ? detectMcpClients(io) : [];
|
|
282
|
-
const astroPath = firstPresent(rootFiles, ASTRO_CONFIG_CANDIDATES);
|
|
283
|
-
const astroSource = null === astroPath ? null : io.readFile(astroPath);
|
|
284
|
-
// Which file owns the DOCUMENT, not how many files sit in a directory. The old rule ("exactly one
|
|
285
|
-
// .astro in src/layouts") fired on neither real Astro app: one has no layouts directory and
|
|
286
|
-
// renders from src/pages/index.astro, the other has three files there of which two are partials.
|
|
287
|
-
// `</body>` is the discriminator — see astro-host.
|
|
288
|
-
const astroCandidates = [ASTRO_LAYOUTS_DIR, ASTRO_PAGES_DIR].flatMap((dir) => io
|
|
289
|
-
.listFiles(dir)
|
|
290
|
-
.filter((f) => f.endsWith('.astro'))
|
|
291
|
-
.map((f) => ({ path: `${dir}/${f}`, source: io.readFile(`${dir}/${f}`) }))
|
|
292
|
-
.filter((c) => c.source !== null));
|
|
293
|
-
const astroHost = pickAstroHost(astroCandidates);
|
|
294
|
-
const layoutRelPath = astroHost?.path ?? null;
|
|
295
|
-
const astroLayoutSource = astroHost?.source ?? null;
|
|
296
|
-
const nuxtConfigPath = firstPresent(rootFiles, NUXT_CONFIG_CANDIDATES);
|
|
297
|
-
const nuxtConfigSource = null === nuxtConfigPath ? null : io.readFile(nuxtConfigPath);
|
|
298
|
-
// Nuxt 4's default srcDir is `app/`, Nuxt 3's is the root — and a plugin written into the
|
|
299
|
-
// directory Nuxt does not scan is never registered, silently.
|
|
300
|
-
const nuxtHasAppDir = io.listDirs('.').includes('app');
|
|
301
|
-
const nextConfigFile = firstPresent(rootFiles, NEXT_CONFIG_CANDIDATES);
|
|
302
|
-
// App Router first; a Pages Router app has no layout, and its mount point is pages/_app.
|
|
303
|
-
const layoutPath = NEXT_LAYOUT_CANDIDATES.find((p) => io.exists(p)) ??
|
|
304
|
-
NEXT_PAGES_APP_CANDIDATES.find((p) => io.exists(p)) ??
|
|
305
|
-
null;
|
|
306
|
-
const layoutSource = null === layoutPath ? null : io.readFile(layoutPath);
|
|
307
|
-
// Where the component goes depends on WHICH router mounts it: `pages/` routes on presence, so a
|
|
308
|
-
// component there becomes a broken route; `app/` routes on filename, so a sibling is inert.
|
|
309
|
-
const devLocation = reticleDevLocation(layoutPath ?? 'app/layout.tsx', detection.typescript);
|
|
310
|
-
// Read once: both the testid scan and the store scan want the same bounded set of files.
|
|
311
|
-
const sourceFiles = readSourceFiles(io);
|
|
312
|
-
const agentRoot = agentRootOf(options);
|
|
313
|
-
const agentFile = (relPath) => agentRoot === undefined ? relPath : join(agentRoot, relPath);
|
|
314
|
-
const cspSources = {};
|
|
315
|
-
for (const file of CSP_FILES) {
|
|
316
|
-
const source = io.readFile(file);
|
|
317
|
-
if (null !== source)
|
|
318
|
-
cspSources[file] = source;
|
|
319
|
-
}
|
|
320
|
-
return {
|
|
321
|
-
detection,
|
|
322
|
-
captureBodies: options.captureBodies,
|
|
323
|
-
hooks: options.hooks,
|
|
324
|
-
cspSources,
|
|
325
|
-
claudeCli,
|
|
326
|
-
mcpExists,
|
|
327
|
-
insideClaudeCode: io.host.insideClaudeCode(),
|
|
328
|
-
claudeProjectConfig: options.mcp ? io.readFile(agentFile(CLAUDE_PROJECT_CONFIG)) : undefined,
|
|
329
|
-
platform: process.platform,
|
|
330
|
-
detectedClients,
|
|
331
|
-
cursorProjectPresent: io.exists(CURSOR_PROJECT_MARKER),
|
|
332
|
-
// Looked for beside the app AND one level up, because the app is routinely a subdirectory of the
|
|
333
|
-
// repo that containerises it — `frontend/` under a root `docker-compose.yml` is the shape this
|
|
334
|
-
// came from. Only the first match is reported; the note is the same whichever file found it.
|
|
335
|
-
...(() => {
|
|
336
|
-
// Only a file that RUNS the dev server: a production-only image is the common case, and
|
|
337
|
-
// React Router's template ships one. See runsDevServer.
|
|
338
|
-
const devScript = devScriptBody(pkg);
|
|
339
|
-
const marker = CONTAINER_MARKERS.find((name) => [name, join('..', name)].some((path) => {
|
|
340
|
-
const content = io.readFile(path);
|
|
341
|
-
return null !== content && runsDevServer(name, content, devScript);
|
|
342
|
-
}));
|
|
343
|
-
return marker === undefined ? {} : { containerMarker: marker };
|
|
344
|
-
})(),
|
|
345
|
-
viteConfig,
|
|
346
|
-
electronViteConfig,
|
|
347
|
-
electronPreload,
|
|
348
|
-
electronMain,
|
|
349
|
-
astroConfig: astroPath !== null && astroSource !== null ? { path: astroPath, source: astroSource } : null,
|
|
350
|
-
astroLayout: layoutRelPath !== null && astroLayoutSource !== null
|
|
351
|
-
? { path: layoutRelPath, source: astroLayoutSource }
|
|
352
|
-
: null,
|
|
353
|
-
astroEnvDts: io.readFile('src/env.d.ts'),
|
|
354
|
-
astroReticleDev: io.readFile('src/components/ReticleDev.ts'),
|
|
355
|
-
nextConfigFile,
|
|
356
|
-
nextConfigSource: null === nextConfigFile ? null : io.readFile(nextConfigFile),
|
|
357
|
-
nextLayout: layoutPath !== null && layoutSource !== null
|
|
358
|
-
? { path: layoutPath, source: layoutSource }
|
|
359
|
-
: null,
|
|
360
|
-
// Capabilities: scanned, never asked for. Bounded — a hint for the agent, not a repo index.
|
|
361
|
-
testids: scanTestids(sourceFiles.map((f) => f.source)),
|
|
362
|
-
storeHints: storeHints(dependencyNames(pkg), sourceFiles),
|
|
363
|
-
foundStores: scanStores(sourceFiles, dependencyNames(pkg)),
|
|
364
|
-
nextFoundStores: scanStores(sourceFiles, dependencyNames(pkg), dirname(devLocation.path)),
|
|
365
|
-
viteDevModuleExists: io.exists(detection.framework === Framework.ELECTRON_VITE
|
|
366
|
-
? ELECTRON_VITE_DEV_MODULE_PATH
|
|
367
|
-
: VITE_DEV_MODULE_PATH),
|
|
368
|
-
nextReticleDevPath: devLocation.path,
|
|
369
|
-
nextReticleDevImport: devLocation.importSpecifier,
|
|
370
|
-
nextReticleDevExists: io.exists(devLocation.path),
|
|
371
|
-
nextReticleDevSource: io.readFile(devLocation.path),
|
|
372
|
-
svelteKitHooksExists: io.exists(SVELTEKIT_HOOKS),
|
|
373
|
-
svelteKitHooksSource: io.readFile(SVELTEKIT_HOOKS),
|
|
374
|
-
reactRouterEntryExists: io.exists(REACT_ROUTER_ENTRY),
|
|
375
|
-
// The SOURCE, so an entry the app already owns is ADDED TO rather than replaced — it is an
|
|
376
|
-
// override of React Router's default, and everything in it is load-bearing.
|
|
377
|
-
reactRouterEntrySource: io.readFile(REACT_ROUTER_ENTRY),
|
|
378
|
-
nuxtConfig: nuxtConfigPath !== null && nuxtConfigSource !== null
|
|
379
|
-
? { path: nuxtConfigPath, source: nuxtConfigSource }
|
|
380
|
-
: null,
|
|
381
|
-
nuxtHasAppDir,
|
|
382
|
-
viteMajor: installedViteMajor(io, detectInput.pkg),
|
|
383
|
-
nuxtPluginExists: io.exists(nuxtPluginPath(nuxtHasAppDir)),
|
|
384
|
-
nuxtPluginSource: io.readFile(nuxtPluginPath(nuxtHasAppDir)),
|
|
385
|
-
...(() => {
|
|
386
|
-
const root = TANSTACK_START_ROOT_CANDIDATES.find((file) => io.exists(file));
|
|
387
|
-
return root === undefined
|
|
388
|
-
? {}
|
|
389
|
-
: {
|
|
390
|
-
tanstackStartRoot: root,
|
|
391
|
-
tanstackStartRootSource: io.readFile(root),
|
|
392
|
-
tanstackStartConnectExists: io.exists(tanstackStartConnectPath(root)),
|
|
393
|
-
tanstackStartConnectSource: io.readFile(tanstackStartConnectPath(root)),
|
|
394
|
-
};
|
|
395
|
-
})(),
|
|
396
|
-
...(Framework.ANGULAR === detection.framework ? angularInputs(io) : {}),
|
|
397
|
-
...(Framework.HTML === detection.framework
|
|
398
|
-
? { htmlIndexSource: io.readFile(HTML_INDEX_PATH) }
|
|
399
|
-
: {}),
|
|
400
|
-
craEntry: craEntryOf(io),
|
|
401
|
-
craEnv: io.readFile(CRA_ENV_PATH),
|
|
402
|
-
pairingToken: options.dryRun ? DRY_RUN_PAIRING_TOKEN : io.host.pairingToken(),
|
|
403
|
-
installSource: io.host.installSource(),
|
|
404
|
-
reticleConfigExists: io.exists(RETICLE_CONFIG_FILE),
|
|
405
|
-
// The CONTENT, so a config that exists can be checked rather than trusted — a `"port"` set to
|
|
406
|
-
// the app's own dev-server port used to survive every re-run of `init`.
|
|
407
|
-
reticleConfigSource: io.readFile(RETICLE_CONFIG_FILE),
|
|
408
|
-
// Read the agent instruction files so the rule merge stays idempotent across re-runs — from the
|
|
409
|
-
// agent's own root, or the merge would idempotently check a file it is not going to write.
|
|
410
|
-
claudeMdContent: io.readFile(agentFile('CLAUDE.md')),
|
|
411
|
-
agentsMdContent: io.readFile(agentFile('AGENTS.md')),
|
|
412
|
-
reticleMdContent: io.readFile(agentFile(RETICLE_MD_PATH)),
|
|
413
|
-
cursorRuleContent: io.readFile(agentFile(CURSOR_RULE_PATH)),
|
|
414
|
-
claudeCommandContent: io.readFile(agentFile(CLAUDE_COMMAND_PATH)),
|
|
415
|
-
cursorCommandContent: io.readFile(agentFile(CURSOR_COMMAND_PATH)),
|
|
416
|
-
// Read only to MERGE into: the settings file is the user's, and other tools write in it too.
|
|
417
|
-
claudeSettingsContent: true === options.hooks ? io.readFile(agentFile(CLAUDE_SETTINGS_PATH)) : undefined,
|
|
418
|
-
...(agentRoot === undefined
|
|
419
|
-
? {}
|
|
420
|
-
: {
|
|
421
|
-
agentFileRoot: agentRoot,
|
|
422
|
-
// The config the AGENT's cwd already has, if any — see agentRootConfigStep.
|
|
423
|
-
agentRootConfigSource: io.readFile(agentFile(RETICLE_CONFIG_FILE)),
|
|
424
|
-
}),
|
|
425
|
-
options: {
|
|
426
|
-
port: options.port,
|
|
427
|
-
mcp: options.mcp,
|
|
428
|
-
install: options.install,
|
|
429
|
-
projectId,
|
|
430
|
-
// The SDK must match the CLI asking for it — see pinnedPackages.
|
|
431
|
-
sdkVersion: RETICLE_VERSION,
|
|
432
|
-
},
|
|
433
|
-
};
|
|
434
|
-
}
|
|
435
55
|
/** Where workspace tooling conventionally puts packages. */
|
|
436
56
|
/** pnpm's workspace declaration, read when present — it is authoritative about where packages live. */
|
|
437
57
|
const SKIPPED_DETAIL = 'skipped — the dependency install above failed, and wiring the app to a package that is not ' +
|
|
@@ -678,7 +298,9 @@ function withWorkspaceRoot(options, io) {
|
|
|
678
298
|
if (options.agentRoot !== undefined || true === options.redirected)
|
|
679
299
|
return options;
|
|
680
300
|
const root = enclosingWorkspaceRoot(options.cwd, io);
|
|
681
|
-
|
|
301
|
+
if (root === undefined)
|
|
302
|
+
return options;
|
|
303
|
+
return { ...options, agentRoot: root, severalApps: 1 < appsAt(root, io).length };
|
|
682
304
|
}
|
|
683
305
|
export function runInit(options, io) {
|
|
684
306
|
const result = runInitSteps(withWorkspaceRoot(options, io), io);
|
|
@@ -695,6 +317,9 @@ export function runInit(options, io) {
|
|
|
695
317
|
if (true !== options.redirected && true !== options.continuesToRuntime) {
|
|
696
318
|
io.print('');
|
|
697
319
|
io.print(FEEDBACK_HINT);
|
|
320
|
+
if (result.ok && true !== options.dryRun) {
|
|
321
|
+
io.print(COMMUNITY_HINT);
|
|
322
|
+
}
|
|
698
323
|
}
|
|
699
324
|
return result;
|
|
700
325
|
}
|
|
@@ -743,11 +368,11 @@ function runInitSteps(options, io) {
|
|
|
743
368
|
// `--app frontend`, the flag that exists for exactly this, was never read because the bail came
|
|
744
369
|
// first. Reported twice by the same user, who tried the documented workaround and hit the same
|
|
745
370
|
// wall. Discovery already handles the case (it scans top-level directories, not just declared
|
|
746
|
-
// workspaces); it was simply unreachable.
|
|
747
|
-
//
|
|
748
|
-
// `'{}'` because the redirect only needs the manifest to ask "is THIS directory the app", and a
|
|
749
|
-
// directory with no package.json is definitively not.
|
|
371
|
+
// workspaces); it was simply unreachable. `'{}'`: a directory with no manifest is not the app.
|
|
750
372
|
const redirectedEarly = redirectToWorkspaceApp(options, io, pkgRaw ?? {}, runInit);
|
|
373
|
+
// Listing the apps below still counts as "no app here", in the same series as before.
|
|
374
|
+
if (true === redirectedEarly?.listedAppsBelow && !options.dryRun)
|
|
375
|
+
io.host.reportOutcome({ ok: false, reason: InitFailure.NO_PACKAGE_JSON });
|
|
751
376
|
if (redirectedEarly !== null)
|
|
752
377
|
return redirectedEarly;
|
|
753
378
|
if (null === pkgRaw)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reticlehq/init",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.8.0",
|
|
4
4
|
"description": "Reticle project scaffolder — detects the framework, writes the config, patches the build and registers the MCP server. The build-time half of `reticle init`.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"./package.json": "./package.json"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@reticlehq/core": "3.
|
|
16
|
+
"@reticlehq/core": "3.8.0"
|
|
17
17
|
},
|
|
18
18
|
"license": "FSL-1.1-ALv2",
|
|
19
19
|
"author": "Reticle contributors",
|