@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,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which command starts this project, and should we start it ourselves?
|
|
3
|
+
*
|
|
4
|
+
* `init` finishes by telling the human to start or restart their dev server, in a closing
|
|
5
|
+
* paragraph, in a terminal that is about to be cleared by the client restart it also asks for.
|
|
6
|
+
* That is the second unmarked hand-off in the install, after the capabilities file, and it lands
|
|
7
|
+
* on the same surface: prose nobody re-reads.
|
|
8
|
+
*
|
|
9
|
+
* Every fact needed to remove it is already in the project. `package.json` names the script; the
|
|
10
|
+
* framework tells us the conventional port; a probe tells us whether something already answers
|
|
11
|
+
* there. Nothing here needs a human.
|
|
12
|
+
*
|
|
13
|
+
* Kept as a PURE decision, separate from spawning, because the interesting part is the judgement —
|
|
14
|
+
* which script, and whether to touch anything at all — and that is what wants testing. Spawning a
|
|
15
|
+
* process is not.
|
|
16
|
+
*/
|
|
17
|
+
/** Script names a JS project uses to run itself, in the order we would pick them. */
|
|
18
|
+
export const DEV_SCRIPT_NAMES = ['dev', 'start', 'serve'];
|
|
19
|
+
const CANDIDATES = DEV_SCRIPT_NAMES;
|
|
20
|
+
export const DevScriptChoice = {
|
|
21
|
+
/** Something already answers on the port. Use it; never replace a server the user is running. */
|
|
22
|
+
ALREADY_SERVING: 'already-serving',
|
|
23
|
+
/** We know the command and nothing is listening. Start it. */
|
|
24
|
+
START: 'start',
|
|
25
|
+
/** No script we recognise. Say so rather than guess a command. */
|
|
26
|
+
NO_SCRIPT: 'no-script',
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Pick the script and decide whether to run it.
|
|
30
|
+
*
|
|
31
|
+
* `serving` is the probe result, passed in rather than measured here so the decision stays pure.
|
|
32
|
+
* When something is already answering we do not care which script exists: the rule is never to
|
|
33
|
+
* start a second server, because the one that is running may be the user's, with their state in it.
|
|
34
|
+
*/
|
|
35
|
+
export function planDevScript(scripts, packageManager, serving) {
|
|
36
|
+
if (serving)
|
|
37
|
+
return { choice: DevScriptChoice.ALREADY_SERVING };
|
|
38
|
+
const name = CANDIDATES.find((c) => 'string' === typeof scripts[c] && scripts[c] !== '');
|
|
39
|
+
if (name === undefined)
|
|
40
|
+
return { choice: DevScriptChoice.NO_SCRIPT };
|
|
41
|
+
// `npm` needs `run`; the others take the script name directly. Getting this wrong prints a
|
|
42
|
+
// command that fails, which is worse than printing nothing.
|
|
43
|
+
const command = 'npm' === packageManager ? `npm run ${name}` : `${packageManager} ${name}`;
|
|
44
|
+
return { choice: DevScriptChoice.START, script: name, command };
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The command to print for this project, straight from its `package.json`.
|
|
48
|
+
*
|
|
49
|
+
* Lives here rather than in `run.ts` because it is the same judgement as `planDevScript` with the
|
|
50
|
+
* parsing attached, and `run.ts` is at its cohesion limit. Returns undefined for anything it cannot
|
|
51
|
+
* read: this phrasing decides nothing, so it must never be able to break an install.
|
|
52
|
+
*/
|
|
53
|
+
export function devCommandFrom(pkg, packageManager) {
|
|
54
|
+
try {
|
|
55
|
+
// Takes the PARSED manifest. It used to take the raw string and parse it a fourth time — the
|
|
56
|
+
// caller now reads the file once, through a guard, so there is one place a malformed manifest
|
|
57
|
+
// can be noticed and it is not this one.
|
|
58
|
+
const scripts = 'object' === typeof pkg && null !== pkg
|
|
59
|
+
? (pkg.scripts ?? {})
|
|
60
|
+
: {};
|
|
61
|
+
return planDevScript(scripts, packageManager, false).command;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ports that belong to a DEV SERVER, not to Reticle's bridge. Next defaults to 3000, Vite to 5173,
|
|
3
|
+
* Astro to 4321, and so on.
|
|
4
|
+
*
|
|
5
|
+
* `.reticle.json`'s `port` is the bridge — the daemon ↔ SDK channel, default 4400 — and it has
|
|
6
|
+
* nothing to do with the port the app is served on. Setting it to the dev-server port makes the
|
|
7
|
+
* daemon try to bind the port the app already holds; what the user sees is either a bind failure or
|
|
8
|
+
* a daemon that never connects, neither of which mentions the actual mistake. The old setup skill
|
|
9
|
+
* asked "what port does your dev server run on?" a few lines before showing this field, which is how
|
|
10
|
+
* the confusion got manufactured in the first place.
|
|
11
|
+
*
|
|
12
|
+
* Lives in the scaffolder because `existing-config.ts` is what diagnoses the mistake in a config
|
|
13
|
+
* somebody already wrote, and this package may not import the daemon. `cli/cli-port.ts` re-exports
|
|
14
|
+
* all three for the runtime readers (the dev-server probe and the no-session diagnosis), so nothing
|
|
15
|
+
* on the server side changed.
|
|
16
|
+
*/
|
|
17
|
+
export declare const DEV_SERVER_PORTS: ReadonlySet<number>;
|
|
18
|
+
export declare function isLikelyDevServerPort(port: number): boolean;
|
|
19
|
+
/** The warning printed when a project pins the bridge to something that looks like its dev server. */
|
|
20
|
+
export declare function devServerPortWarning(port: number): string;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ports that belong to a DEV SERVER, not to Reticle's bridge. Next defaults to 3000, Vite to 5173,
|
|
3
|
+
* Astro to 4321, and so on.
|
|
4
|
+
*
|
|
5
|
+
* `.reticle.json`'s `port` is the bridge — the daemon ↔ SDK channel, default 4400 — and it has
|
|
6
|
+
* nothing to do with the port the app is served on. Setting it to the dev-server port makes the
|
|
7
|
+
* daemon try to bind the port the app already holds; what the user sees is either a bind failure or
|
|
8
|
+
* a daemon that never connects, neither of which mentions the actual mistake. The old setup skill
|
|
9
|
+
* asked "what port does your dev server run on?" a few lines before showing this field, which is how
|
|
10
|
+
* the confusion got manufactured in the first place.
|
|
11
|
+
*
|
|
12
|
+
* Lives in the scaffolder because `existing-config.ts` is what diagnoses the mistake in a config
|
|
13
|
+
* somebody already wrote, and this package may not import the daemon. `cli/cli-port.ts` re-exports
|
|
14
|
+
* all three for the runtime readers (the dev-server probe and the no-session diagnosis), so nothing
|
|
15
|
+
* on the server side changed.
|
|
16
|
+
*/
|
|
17
|
+
export const DEV_SERVER_PORTS = new Set([
|
|
18
|
+
3000,
|
|
19
|
+
3001,
|
|
20
|
+
4200,
|
|
21
|
+
4321,
|
|
22
|
+
5000,
|
|
23
|
+
5173,
|
|
24
|
+
5174,
|
|
25
|
+
8000,
|
|
26
|
+
8080,
|
|
27
|
+
8100,
|
|
28
|
+
9000,
|
|
29
|
+
// Added after the scan reported "nothing is listening" at people whose app was plainly running.
|
|
30
|
+
// A narrow list is not just unhelpful, it is CONFIDENTLY wrong — the message states absence as a
|
|
31
|
+
// fact — so the defaults of the frameworks agents actually meet belong in it.
|
|
32
|
+
1420, // Tauri
|
|
33
|
+
3100, // Next, second instance
|
|
34
|
+
4173, // vite preview
|
|
35
|
+
5175, // Vite, third instance
|
|
36
|
+
7860, // Gradio
|
|
37
|
+
8501, // Streamlit
|
|
38
|
+
]);
|
|
39
|
+
export function isLikelyDevServerPort(port) {
|
|
40
|
+
return DEV_SERVER_PORTS.has(port);
|
|
41
|
+
}
|
|
42
|
+
/** The warning printed when a project pins the bridge to something that looks like its dev server. */
|
|
43
|
+
export function devServerPortWarning(port) {
|
|
44
|
+
return `[reticle] .reticle.json sets "port": ${port}, which is a common DEV-SERVER port. That field is the Reticle BRIDGE port (default 4400), not the port your app is served on — they must be different, or the daemon fights your dev server for it. Remove "port" unless you are running several apps at once.`;
|
|
45
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conservative patchers for the Electron halves `init` can see whole: the preload IPC shim and
|
|
3
|
+
* the main-process screenshot helper.
|
|
4
|
+
*
|
|
5
|
+
* Preload is mechanical — prepend one line — so it is APPLY. Main is only APPLY when the file
|
|
6
|
+
* constructs exactly one BrowserWindow as `const <name> = new BrowserWindow(`; anything else
|
|
7
|
+
* (zero, several, a factory) is MANUAL carrying the doctor's existing fix text, not a guess.
|
|
8
|
+
*/
|
|
9
|
+
import { type SourcePatch } from './patch-kind.js';
|
|
10
|
+
export declare function patchElectronPreload(source: string, path: string): SourcePatch;
|
|
11
|
+
export declare function patchElectronMain(source: string, path?: string): SourcePatch;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conservative patchers for the Electron halves `init` can see whole: the preload IPC shim and
|
|
3
|
+
* the main-process screenshot helper.
|
|
4
|
+
*
|
|
5
|
+
* Preload is mechanical — prepend one line — so it is APPLY. Main is only APPLY when the file
|
|
6
|
+
* constructs exactly one BrowserWindow as `const <name> = new BrowserWindow(`; anything else
|
|
7
|
+
* (zero, several, a factory) is MANUAL carrying the doctor's existing fix text, not a guess.
|
|
8
|
+
*/
|
|
9
|
+
import { matchingParenEnd } from './brace-scan.js';
|
|
10
|
+
import { CAPTURE_REQUIRE, ELECTRON_CAPTURE_FIX, PRELOAD_REQUIRE } from './desktop-doctor.js';
|
|
11
|
+
import { PatchKind } from './patch-kind.js';
|
|
12
|
+
const ESM_PRELOAD_EXT = /\.(?:ts|mts|mjs)$/i;
|
|
13
|
+
const WINDOW_DECL = /const\s+(\w+)\s*=\s*new\s+BrowserWindow\s*\(/g;
|
|
14
|
+
const IMPORT_LINE = /^import\s.+from\s+['"][^'"]+['"];?\s*$/gm;
|
|
15
|
+
const REQUIRE_LINE = /^(?:const|let|var)\s+.+=\s+require\s*\(/gm;
|
|
16
|
+
function isEsmPath(path) {
|
|
17
|
+
return ESM_PRELOAD_EXT.test(path);
|
|
18
|
+
}
|
|
19
|
+
function preloadLine(path) {
|
|
20
|
+
return isEsmPath(path) ? `import '${PRELOAD_REQUIRE}'\n` : `require('${PRELOAD_REQUIRE}');\n`;
|
|
21
|
+
}
|
|
22
|
+
export function patchElectronPreload(source, path) {
|
|
23
|
+
if (source.includes(PRELOAD_REQUIRE)) {
|
|
24
|
+
return { kind: PatchKind.ALREADY };
|
|
25
|
+
}
|
|
26
|
+
return { kind: PatchKind.APPLY, code: `${preloadLine(path)}${source}` };
|
|
27
|
+
}
|
|
28
|
+
function insertMainImport(source, path) {
|
|
29
|
+
if (source.includes(CAPTURE_REQUIRE))
|
|
30
|
+
return source;
|
|
31
|
+
if (isEsmPath(path)) {
|
|
32
|
+
const line = `import { installReticleCapture } from '${CAPTURE_REQUIRE}'`;
|
|
33
|
+
const matches = [...source.matchAll(IMPORT_LINE)];
|
|
34
|
+
const last = matches[matches.length - 1];
|
|
35
|
+
if (last?.index === undefined)
|
|
36
|
+
return `${line}\n${source}`;
|
|
37
|
+
const end = last.index + last[0].length;
|
|
38
|
+
return `${source.slice(0, end)}\n${line}${source.slice(end)}`;
|
|
39
|
+
}
|
|
40
|
+
const line = `const { installReticleCapture } = require('${CAPTURE_REQUIRE}');`;
|
|
41
|
+
const matches = [...source.matchAll(REQUIRE_LINE)];
|
|
42
|
+
const last = matches[matches.length - 1];
|
|
43
|
+
if (last?.index === undefined)
|
|
44
|
+
return `${line}\n${source}`;
|
|
45
|
+
const endOfLine = source.indexOf('\n', last.index);
|
|
46
|
+
const end = endOfLine < 0 ? source.length : endOfLine;
|
|
47
|
+
return `${source.slice(0, end)}\n${line}${source.slice(end)}`;
|
|
48
|
+
}
|
|
49
|
+
export function patchElectronMain(source, path = 'electron/main.cjs') {
|
|
50
|
+
if (source.includes(CAPTURE_REQUIRE)) {
|
|
51
|
+
return { kind: PatchKind.ALREADY };
|
|
52
|
+
}
|
|
53
|
+
const matches = [...source.matchAll(WINDOW_DECL)];
|
|
54
|
+
if (matches.length !== 1 || matches[0]?.index === undefined || matches[0][1] === undefined) {
|
|
55
|
+
return { kind: PatchKind.MANUAL, reason: ELECTRON_CAPTURE_FIX };
|
|
56
|
+
}
|
|
57
|
+
const name = matches[0][1];
|
|
58
|
+
const openAt = source.indexOf('(', matches[0].index + matches[0][0].length - 1);
|
|
59
|
+
const closeAt = matchingParenEnd(source, openAt);
|
|
60
|
+
if (closeAt === undefined) {
|
|
61
|
+
return { kind: PatchKind.MANUAL, reason: ELECTRON_CAPTURE_FIX };
|
|
62
|
+
}
|
|
63
|
+
let insertAt = closeAt + 1;
|
|
64
|
+
if (';' === source[insertAt])
|
|
65
|
+
insertAt += 1;
|
|
66
|
+
const indentMatch = /^[ \t]*/.exec(source.slice(source.lastIndexOf('\n', matches[0].index) + 1));
|
|
67
|
+
const indent = indentMatch?.[0] ?? ' ';
|
|
68
|
+
const withCall = `${source.slice(0, insertAt)}\n${indent}installReticleCapture(${name})${source.slice(insertAt)}`;
|
|
69
|
+
return { kind: PatchKind.APPLY, code: insertMainImport(withCall, path) };
|
|
70
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conservative patcher for an electron-vite config: add `@reticlehq/vite-plugin` and drop
|
|
3
|
+
* `reticle({ desktop: true, ... })` into the RENDERER block's plugins array.
|
|
4
|
+
*
|
|
5
|
+
* A generic Vite patcher takes the first `plugins: [` in the file, which in a conventional
|
|
6
|
+
* electron-vite config is `main`'s. Wiring the SDK into the main process installs it where there
|
|
7
|
+
* is no document, and reports success — worse than today's honest refusal. This patcher finds the
|
|
8
|
+
* `renderer` block specifically and refuses anything it cannot see whole.
|
|
9
|
+
*/
|
|
10
|
+
import { type SourcePatch } from './patch-kind.js';
|
|
11
|
+
export declare const ELECTRON_VITE_IMPORT = "import { reticle } from '@reticlehq/vite-plugin';";
|
|
12
|
+
export declare function patchElectronViteConfig(source: string, port?: number): SourcePatch;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conservative patcher for an electron-vite config: add `@reticlehq/vite-plugin` and drop
|
|
3
|
+
* `reticle({ desktop: true, ... })` into the RENDERER block's plugins array.
|
|
4
|
+
*
|
|
5
|
+
* A generic Vite patcher takes the first `plugins: [` in the file, which in a conventional
|
|
6
|
+
* electron-vite config is `main`'s. Wiring the SDK into the main process installs it where there
|
|
7
|
+
* is no document, and reports success — worse than today's honest refusal. This patcher finds the
|
|
8
|
+
* `renderer` block specifically and refuses anything it cannot see whole.
|
|
9
|
+
*/
|
|
10
|
+
import { matchingBraceEnd } from './brace-scan.js';
|
|
11
|
+
import { PatchKind } from './patch-kind.js';
|
|
12
|
+
export const ELECTRON_VITE_IMPORT = "import { reticle } from '@reticlehq/vite-plugin';";
|
|
13
|
+
const RETICLE_MARKER = '@reticlehq/vite-plugin';
|
|
14
|
+
const RETICLE_CALL = /reticle\s*\(/;
|
|
15
|
+
const DESKTOP_FLAG = /desktop\s*:/;
|
|
16
|
+
const PLUGINS_ARRAY = /plugins\s*:\s*\[/;
|
|
17
|
+
const IMPORT_LINE = /^import\s.+from\s+['"][^'"]+['"];?\s*$/gm;
|
|
18
|
+
const RENDERER_KEY = /renderer\s*:\s*/;
|
|
19
|
+
const NO_RENDERER_REASON = "couldn't find a `renderer` key to extend";
|
|
20
|
+
const RENDERER_NOT_LITERAL_REASON = 'this config sets `renderer:` to something other than an object literal — merging into it is your call, not a text edit Reticle should make';
|
|
21
|
+
const WRONG_BLOCK_REASON = 'reticle() is in this file but not in the `renderer` block — the plugin belongs where there is a document, and leaving it in `main` or `preload` would report success for an app that cannot connect';
|
|
22
|
+
const MISSING_DESKTOP_REASON = 'reticle() is already in the `renderer` block but without `desktop: true` — a packaged renderer is a production build with no dev server, so the plugin is dropped from `vite build` and the shipped app has no connect()';
|
|
23
|
+
function reticlePluginCall(port) {
|
|
24
|
+
const options = [
|
|
25
|
+
'desktop: true',
|
|
26
|
+
...(port === undefined ? [] : [`port: ${String(port)}`]),
|
|
27
|
+
'captureNetworkBodies: true',
|
|
28
|
+
];
|
|
29
|
+
return `reticle({ ${options.join(', ')} })`;
|
|
30
|
+
}
|
|
31
|
+
function insertImport(source) {
|
|
32
|
+
if (source.includes(RETICLE_MARKER))
|
|
33
|
+
return source;
|
|
34
|
+
const matches = [...source.matchAll(IMPORT_LINE)];
|
|
35
|
+
const last = matches[matches.length - 1];
|
|
36
|
+
if (last?.index === undefined) {
|
|
37
|
+
return `${ELECTRON_VITE_IMPORT}\n${source}`;
|
|
38
|
+
}
|
|
39
|
+
const end = last.index + last[0].length;
|
|
40
|
+
return `${source.slice(0, end)}\n${ELECTRON_VITE_IMPORT}${source.slice(end)}`;
|
|
41
|
+
}
|
|
42
|
+
function rendererBounds(source) {
|
|
43
|
+
const key = RENDERER_KEY.exec(source);
|
|
44
|
+
if (key?.index === undefined)
|
|
45
|
+
return undefined;
|
|
46
|
+
const after = key.index + key[0].length;
|
|
47
|
+
if (source[after] !== '{')
|
|
48
|
+
return undefined;
|
|
49
|
+
const innerStart = after + 1;
|
|
50
|
+
const innerEnd = matchingBraceEnd(source, innerStart);
|
|
51
|
+
if (innerEnd === undefined)
|
|
52
|
+
return undefined;
|
|
53
|
+
return { keyAt: key.index, braceAt: after, innerStart, innerEnd };
|
|
54
|
+
}
|
|
55
|
+
function insertPluginInRenderer(source, bounds, port) {
|
|
56
|
+
const inner = source.slice(bounds.innerStart, bounds.innerEnd);
|
|
57
|
+
const match = PLUGINS_ARRAY.exec(inner);
|
|
58
|
+
if (match?.index === undefined)
|
|
59
|
+
return source;
|
|
60
|
+
const next = inner[match.index + match[0].length] ?? '';
|
|
61
|
+
const separator = '' === next || /\s/.test(next) ? '' : ' ';
|
|
62
|
+
const patched = `${inner.slice(0, match.index)}${match[0]}${reticlePluginCall(port)},${separator}${inner.slice(match.index + match[0].length)}`;
|
|
63
|
+
return `${source.slice(0, bounds.innerStart)}${patched}${source.slice(bounds.innerEnd)}`;
|
|
64
|
+
}
|
|
65
|
+
function insertPluginsKeyInRenderer(source, bounds, port) {
|
|
66
|
+
const rest = source.slice(bounds.braceAt + 1);
|
|
67
|
+
const multiline = /^\s*\n/.test(rest);
|
|
68
|
+
const indent = /^\s*\n(\s*)\S/.exec(rest)?.[1] ?? ' ';
|
|
69
|
+
const key = `plugins: [${reticlePluginCall(port)}],`;
|
|
70
|
+
const inserted = multiline ? `\n${indent}${key}` : ` ${key}`;
|
|
71
|
+
return `${source.slice(0, bounds.braceAt + 1)}${inserted}${source.slice(bounds.braceAt + 1)}`;
|
|
72
|
+
}
|
|
73
|
+
export function patchElectronViteConfig(source, port) {
|
|
74
|
+
const key = RENDERER_KEY.exec(source);
|
|
75
|
+
if (key?.index === undefined) {
|
|
76
|
+
return { kind: PatchKind.MANUAL, reason: NO_RENDERER_REASON };
|
|
77
|
+
}
|
|
78
|
+
const after = key.index + key[0].length;
|
|
79
|
+
if ('{' !== source[after]) {
|
|
80
|
+
return { kind: PatchKind.MANUAL, reason: RENDERER_NOT_LITERAL_REASON };
|
|
81
|
+
}
|
|
82
|
+
const bounds = rendererBounds(source);
|
|
83
|
+
if (bounds === undefined) {
|
|
84
|
+
return { kind: PatchKind.MANUAL, reason: NO_RENDERER_REASON };
|
|
85
|
+
}
|
|
86
|
+
const renderer = source.slice(bounds.innerStart, bounds.innerEnd);
|
|
87
|
+
if (RETICLE_CALL.test(renderer)) {
|
|
88
|
+
if (DESKTOP_FLAG.test(renderer)) {
|
|
89
|
+
return { kind: PatchKind.ALREADY };
|
|
90
|
+
}
|
|
91
|
+
return { kind: PatchKind.MANUAL, reason: MISSING_DESKTOP_REASON };
|
|
92
|
+
}
|
|
93
|
+
if (source.includes(RETICLE_MARKER) || RETICLE_CALL.test(source)) {
|
|
94
|
+
return { kind: PatchKind.MANUAL, reason: WRONG_BLOCK_REASON };
|
|
95
|
+
}
|
|
96
|
+
const withPlugin = PLUGINS_ARRAY.test(renderer)
|
|
97
|
+
? insertPluginInRenderer(source, bounds, port)
|
|
98
|
+
: insertPluginsKeyInRenderer(source, bounds, port);
|
|
99
|
+
return { kind: PatchKind.APPLY, code: insertImport(withPlugin) };
|
|
100
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** The project config file, named once. Lives here because this module is what reads it. */
|
|
2
|
+
export declare const RETICLE_CONFIG_FILE = ".reticle.json";
|
|
3
|
+
/**
|
|
4
|
+
* Reading a `.reticle.json` that is ALREADY on disk.
|
|
5
|
+
*
|
|
6
|
+
* Split out of plan.ts when it crossed the line cap. These two are one job -- looking at a config
|
|
7
|
+
* somebody else wrote and deciding what it tells us -- and neither knows anything about steps,
|
|
8
|
+
* plans or the shape of a report, which is what made them the cohesive piece to lift rather than
|
|
9
|
+
* the nearest thousand-line boundary.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* What is wrong with the config that is already there, if anything.
|
|
13
|
+
*
|
|
14
|
+
* Reported from the field (#317): a project carried `"port": 3000` — its own dev-server port, which
|
|
15
|
+
* is the confusion SKILL.md names as the top setup failure — and `init` printed `.reticle.json
|
|
16
|
+
* already exists` on every re-run without reading a single field of it, so the file could never be
|
|
17
|
+
* repaired by the command that wrote it. It stayed invisible because a daemon on `127.0.0.1:3000`
|
|
18
|
+
* and Vite on `[::1]:3000` split the port by address family and neither reported a conflict.
|
|
19
|
+
*
|
|
20
|
+
* Narrower than the issue asked for on purpose: `init`'s IO surface is synchronous, so this cannot
|
|
21
|
+
* probe what is LISTENING right now. `isLikelyDevServerPort` covers the framework defaults, which is
|
|
22
|
+
* where the mistake is actually made.
|
|
23
|
+
*/
|
|
24
|
+
export declare function existingConfigProblem(source: string | null | undefined): string | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* The project a `.reticle.json` claims, or undefined when it does not claim one we can read.
|
|
27
|
+
*
|
|
28
|
+
* Unreadable is deliberately NOT a conflict. A file we cannot parse tells us nothing about which
|
|
29
|
+
* app it points at, and refusing on it would block the redirect on any hand-edited config.
|
|
30
|
+
*/
|
|
31
|
+
export declare function projectIdOf(source: string | null | undefined): string | undefined;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { devServerPortWarning, isLikelyDevServerPort } from './dev-server-ports.js';
|
|
2
|
+
/** The project config file, named once. Lives here because this module is what reads it. */
|
|
3
|
+
export const RETICLE_CONFIG_FILE = '.reticle.json';
|
|
4
|
+
/**
|
|
5
|
+
* Reading a `.reticle.json` that is ALREADY on disk.
|
|
6
|
+
*
|
|
7
|
+
* Split out of plan.ts when it crossed the line cap. These two are one job -- looking at a config
|
|
8
|
+
* somebody else wrote and deciding what it tells us -- and neither knows anything about steps,
|
|
9
|
+
* plans or the shape of a report, which is what made them the cohesive piece to lift rather than
|
|
10
|
+
* the nearest thousand-line boundary.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* What is wrong with the config that is already there, if anything.
|
|
14
|
+
*
|
|
15
|
+
* Reported from the field (#317): a project carried `"port": 3000` — its own dev-server port, which
|
|
16
|
+
* is the confusion SKILL.md names as the top setup failure — and `init` printed `.reticle.json
|
|
17
|
+
* already exists` on every re-run without reading a single field of it, so the file could never be
|
|
18
|
+
* repaired by the command that wrote it. It stayed invisible because a daemon on `127.0.0.1:3000`
|
|
19
|
+
* and Vite on `[::1]:3000` split the port by address family and neither reported a conflict.
|
|
20
|
+
*
|
|
21
|
+
* Narrower than the issue asked for on purpose: `init`'s IO surface is synchronous, so this cannot
|
|
22
|
+
* probe what is LISTENING right now. `isLikelyDevServerPort` covers the framework defaults, which is
|
|
23
|
+
* where the mistake is actually made.
|
|
24
|
+
*/
|
|
25
|
+
export function existingConfigProblem(source) {
|
|
26
|
+
if (null === source || source === undefined)
|
|
27
|
+
return undefined;
|
|
28
|
+
let parsed;
|
|
29
|
+
try {
|
|
30
|
+
parsed = JSON.parse(source);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return (`${RETICLE_CONFIG_FILE} is present but is not valid JSON, so nothing reads it — neither the ` +
|
|
34
|
+
'daemon nor the SDK. Delete it and re-run `reticle init`.');
|
|
35
|
+
}
|
|
36
|
+
if ('object' !== typeof parsed || null === parsed)
|
|
37
|
+
return undefined;
|
|
38
|
+
const port = parsed['port'];
|
|
39
|
+
if ('number' !== typeof port || !isLikelyDevServerPort(port))
|
|
40
|
+
return undefined;
|
|
41
|
+
return `${devServerPortWarning(port)} Fix it by removing the "port" field from ${RETICLE_CONFIG_FILE}.`;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The project a `.reticle.json` claims, or undefined when it does not claim one we can read.
|
|
45
|
+
*
|
|
46
|
+
* Unreadable is deliberately NOT a conflict. A file we cannot parse tells us nothing about which
|
|
47
|
+
* app it points at, and refusing on it would block the redirect on any hand-edited config.
|
|
48
|
+
*/
|
|
49
|
+
export function projectIdOf(source) {
|
|
50
|
+
if (null === source || source === undefined)
|
|
51
|
+
return undefined;
|
|
52
|
+
let parsed;
|
|
53
|
+
try {
|
|
54
|
+
parsed = JSON.parse(source);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
if ('object' !== typeof parsed || null === parsed)
|
|
60
|
+
return undefined;
|
|
61
|
+
const id = parsed['projectId'];
|
|
62
|
+
return 'string' === typeof id && id.length > 0 ? id : undefined;
|
|
63
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format a file `init` is about to write, using the project's own Prettier when it is installed.
|
|
3
|
+
*
|
|
4
|
+
* Reported from the field (#684): a clean CRA install failed the project's own lint because
|
|
5
|
+
* `src/reticle-dev.ts` was emitted as one long line. The project already had Prettier; init never
|
|
6
|
+
* asked it. Best-effort: resolve Prettier from the app (not from ours — we do not ship it), apply
|
|
7
|
+
* its config, and on any failure return the original content so init never fails because of a
|
|
8
|
+
* formatter. Sync only — `applyEffects` is sync, and Prettier 3's async `format` cannot join here.
|
|
9
|
+
*/
|
|
10
|
+
/** True when this write is a connect/dev module lint will treat as project source. */
|
|
11
|
+
export declare function isGeneratedSourcePath(relPath: string): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Format `content` for `relPath` under `projectRoot`, or return it unchanged.
|
|
14
|
+
*
|
|
15
|
+
* Only the sync Prettier API is used. Prettier 3's async `format` is skipped (returns the original)
|
|
16
|
+
* rather than holding init open on a promise `applyEffects` cannot await.
|
|
17
|
+
*/
|
|
18
|
+
export declare function formatGeneratedSource(content: string, relPath: string, projectRoot: string): string;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format a file `init` is about to write, using the project's own Prettier when it is installed.
|
|
3
|
+
*
|
|
4
|
+
* Reported from the field (#684): a clean CRA install failed the project's own lint because
|
|
5
|
+
* `src/reticle-dev.ts` was emitted as one long line. The project already had Prettier; init never
|
|
6
|
+
* asked it. Best-effort: resolve Prettier from the app (not from ours — we do not ship it), apply
|
|
7
|
+
* its config, and on any failure return the original content so init never fails because of a
|
|
8
|
+
* formatter. Sync only — `applyEffects` is sync, and Prettier 3's async `format` cannot join here.
|
|
9
|
+
*/
|
|
10
|
+
import { createRequire } from 'node:module';
|
|
11
|
+
import { basename, join } from 'node:path';
|
|
12
|
+
/** Basenames of full-file generators that a project's lint will see. */
|
|
13
|
+
const GENERATED_SOURCE_NAMES = new Set([
|
|
14
|
+
'reticle-dev.ts',
|
|
15
|
+
'reticle-dev.tsx',
|
|
16
|
+
// CRA's JavaScript branch. Missing here, so the one project shape that HAS no TypeScript — and
|
|
17
|
+
// whose boilerplate lint is the strictest about line width — was the one never formatted.
|
|
18
|
+
'reticle-dev.js',
|
|
19
|
+
'hooks.client.ts',
|
|
20
|
+
'reticle.client.ts',
|
|
21
|
+
'entry.client.tsx',
|
|
22
|
+
]);
|
|
23
|
+
/** True when this write is a connect/dev module lint will treat as project source. */
|
|
24
|
+
export function isGeneratedSourcePath(relPath) {
|
|
25
|
+
return GENERATED_SOURCE_NAMES.has(basename(relPath));
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Format `content` for `relPath` under `projectRoot`, or return it unchanged.
|
|
29
|
+
*
|
|
30
|
+
* Only the sync Prettier API is used. Prettier 3's async `format` is skipped (returns the original)
|
|
31
|
+
* rather than holding init open on a promise `applyEffects` cannot await.
|
|
32
|
+
*/
|
|
33
|
+
export function formatGeneratedSource(content, relPath, projectRoot) {
|
|
34
|
+
if (!isGeneratedSourcePath(relPath))
|
|
35
|
+
return content;
|
|
36
|
+
try {
|
|
37
|
+
const require = createRequire(join(projectRoot, 'package.json'));
|
|
38
|
+
const prettier = require('prettier');
|
|
39
|
+
const filePath = join(projectRoot, relPath);
|
|
40
|
+
const resolved = prettier.resolveConfig.sync?.(filePath) ?? {};
|
|
41
|
+
const formatted = prettier.format(content, { ...resolved, filepath: filePath });
|
|
42
|
+
// Prettier 2 returns a string; Prettier 3 returns a Promise we cannot join from sync code.
|
|
43
|
+
return 'string' === typeof formatted ? formatted : content;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return content;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One record per framework, in one table the compiler refuses to leave a hole in.
|
|
3
|
+
*
|
|
4
|
+
* Adding a framework used to mean editing nine places — the enum, the detection globs, the
|
|
5
|
+
* detection chain, the package switch, an ad-hoc `SVELTEKIT || NUXT` pair test, the step switch,
|
|
6
|
+
* the step builder, the connect snippet, the connect-step title set — and only two of them went
|
|
7
|
+
* red when missed. The rest failed the way this whole area fails: `init` printed a clean plan and
|
|
8
|
+
* the app never dialled the daemon.
|
|
9
|
+
*
|
|
10
|
+
* Everything that is per-framework AND mechanical lives here. What is NOT here is deliberate:
|
|
11
|
+
*
|
|
12
|
+
* - Detection (`FRAMEWORK_SIGNALS` / `DETECTION_ORDER` in `detect.ts`). The step builders below
|
|
13
|
+
* import `detect.ts` transitively, so folding detection in would make the module that answers
|
|
14
|
+
* "what is this project" depend on the module that answers "how do we wire it". It is a
|
|
15
|
+
* `Record<Framework, …>` of its own there, so it is completed by the compiler just the same.
|
|
16
|
+
* - The snippet builders (`snippets.ts`). Each is a different recipe in a different language, and
|
|
17
|
+
* they are reached through the `steps` builder anyway.
|
|
18
|
+
*
|
|
19
|
+
* `framework-adapter.test.ts` asserts the seam between the two halves.
|
|
20
|
+
*/
|
|
21
|
+
import { Framework } from './detect.js';
|
|
22
|
+
import { StepTitle } from './connect-steps.js';
|
|
23
|
+
import type { PlanInput, Step } from './plan.js';
|
|
24
|
+
export declare const RETICLE_REACT_KIT = "@reticlehq/react";
|
|
25
|
+
/** The framework-neutral sensor, for stacks the React adapter has nothing to attach to. */
|
|
26
|
+
export declare const RETICLE_BROWSER_SDK = "@reticlehq/browser";
|
|
27
|
+
export declare const RETICLE_VITE_PLUGIN = "@reticlehq/vite-plugin";
|
|
28
|
+
export declare const RETICLE_NEXT_PLUGIN = "@reticlehq/next";
|
|
29
|
+
/** The Electron main/preload helper — what makes IPC and screenshots exist at all. */
|
|
30
|
+
export declare const RETICLE_ELECTRON = "@reticlehq/electron";
|
|
31
|
+
export interface FrameworkAdapter {
|
|
32
|
+
/**
|
|
33
|
+
* The dev dependencies `init` installs, given the kit the UI-library check chose
|
|
34
|
+
* (`@reticlehq/react` or the neutral sensor). A framework that knows its own renderer ignores the
|
|
35
|
+
* argument — Next is React by construction and Nuxt is Vue, so neither has a detection result
|
|
36
|
+
* worth honouring here.
|
|
37
|
+
*/
|
|
38
|
+
readonly packages: (kit: string) => readonly string[];
|
|
39
|
+
/** The per-framework half of the plan. */
|
|
40
|
+
readonly steps: (input: PlanInput) => Step[];
|
|
41
|
+
/**
|
|
42
|
+
* The steps WITHOUT which this framework's app never dials the daemon.
|
|
43
|
+
*
|
|
44
|
+
* `connect-steps.ts` decides whether a ⚠ makes `init` exit non-zero, and it does so from a
|
|
45
|
+
* hand-written set of titles. A framework whose connect step is missing from that set reports the
|
|
46
|
+
* ⚠ and exits 0 over an app that cannot connect; the test pins that no entry here is unknown to
|
|
47
|
+
* it, and that between them the entries account for the whole set.
|
|
48
|
+
*/
|
|
49
|
+
readonly connectStepTitles: readonly StepTitle[];
|
|
50
|
+
/**
|
|
51
|
+
* Whether this framework's own recipe already says it is unverified.
|
|
52
|
+
*
|
|
53
|
+
* Read by the generic "<library> is UNVERIFIED" notice, which must not argue with the more
|
|
54
|
+
* specific wording a recipe carries. This replaced a literal `SVELTEKIT || NUXT` test — a pair
|
|
55
|
+
* that had to be remembered, and is not the same question as "renders its own HTML" (Astro and
|
|
56
|
+
* React Router do that too and DO want the generic notice).
|
|
57
|
+
*/
|
|
58
|
+
readonly carriesOwnUnverifiedNote: boolean;
|
|
59
|
+
}
|
|
60
|
+
export declare const FRAMEWORK_ADAPTERS: Record<Framework, FrameworkAdapter>;
|