@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/patch/snippets.d.ts
CHANGED
|
@@ -35,19 +35,16 @@ export declare const CDN_SDK_URL: string;
|
|
|
35
35
|
* (so the app is identifiable across port changes). Empty string only when neither applies.
|
|
36
36
|
*/
|
|
37
37
|
export declare function connectArg(port: number | undefined, projectId?: string): string;
|
|
38
|
+
/** The gitignored module beside the page that holds this machine's pairing token. */
|
|
39
|
+
export declare const LOCAL_TOKEN_MODULE = "reticle.local.js";
|
|
38
40
|
/**
|
|
39
|
-
*
|
|
41
|
+
* A connect literal with `token` set to a JS expression, never to the token itself.
|
|
40
42
|
*
|
|
41
|
-
* The token
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* socket with AUTH_FAILED and no session ever appears: see Bridge's hello handler.
|
|
45
|
-
*
|
|
46
|
-
* An empty token is omitted rather than emitted as `token: ''`. A daemon that could not write to
|
|
47
|
-
* $HOME runs without auth and trusts loopback, and an empty string would fail the comparison against
|
|
48
|
-
* one that does hold a token.
|
|
43
|
+
* The token used to be a literal in every printed snippet. It then stayed in terminal history,
|
|
44
|
+
* in agent transcripts, and in every file the snippet was pasted into. The snippets now read it at
|
|
45
|
+
* run time: from the gitignored module, or, in Python, from the token file.
|
|
49
46
|
*/
|
|
50
|
-
export declare function
|
|
47
|
+
export declare function withTokenExpression(connectArgLiteral: string, tokenExpression: string): string;
|
|
51
48
|
export declare function usesReactKit(uiLibrary: UiLibrary, framework?: Framework): boolean;
|
|
52
49
|
/**
|
|
53
50
|
* Which SDK package the GENERATED code should import, and whether `install()` applies.
|
|
@@ -152,26 +149,8 @@ export declare const NEXT_LAYOUT_MANUAL = "Mount <ReticleDev /> in your root lay
|
|
|
152
149
|
* opposite sign, so the two share a builder rather than a copy.
|
|
153
150
|
*/
|
|
154
151
|
export declare function staticPageSnippet(connectArgLiteral: string): string;
|
|
155
|
-
/**
|
|
156
|
-
|
|
157
|
-
* it. Streamlit 1.63 added an explicit JavaScript boundary to `st.html`; use a classic script there
|
|
158
|
-
* so it can dynamically import the ESM SDK in the app document. A head marker makes reruns
|
|
159
|
-
* idempotent and is removed after an import failure so the next rerun can retry.
|
|
160
|
-
*/
|
|
161
|
-
export declare function streamlitPageSnippet(connectArgLiteral: string): string;
|
|
162
|
-
/**
|
|
163
|
-
* DEBUG-only middleware that injects Reticle into every Django-rendered page.
|
|
164
|
-
*
|
|
165
|
-
* Django serves templates, so the generic script tag has no single home: an app has many templates
|
|
166
|
-
* and a base template it may not have. Middleware has exactly one, runs for every page, and is the
|
|
167
|
-
* idiomatic Django answer — a field reporter arrived at this same shape by hand after `init` printed
|
|
168
|
-
* a script tag and exited 1, and had to write the middleware and `.reticle.json` themselves.
|
|
169
|
-
*
|
|
170
|
-
* Guarded on `settings.DEBUG` so it can never reach production, and on the response being HTML so it
|
|
171
|
-
* does not corrupt a JSON API response or a file download. Injected before `</body>` rather than in
|
|
172
|
-
* `<head>`, so the SDK sees a parsed document.
|
|
173
|
-
*/
|
|
174
|
-
export declare function djangoMiddlewareSnippet(connectArgLiteral: string): string;
|
|
152
|
+
/** The same snippet, with the token read from the gitignored module beside the page. */
|
|
153
|
+
export declare function staticPageSnippetWithLocalToken(connectArgLiteral: string): string;
|
|
175
154
|
export declare function htmlManual(port: number | undefined, projectId?: string, pairingToken?: string, uiLibrary?: UiLibrary): string;
|
|
176
155
|
export declare const NEXT_RETICLE_DEV_PATH = "app/reticle-dev.tsx";
|
|
177
156
|
export declare const SVELTEKIT_HOOKS_PATH = "src/hooks.client.ts";
|
|
@@ -182,7 +161,7 @@ export declare const SVELTEKIT_HOOKS_PATH = "src/hooks.client.ts";
|
|
|
182
161
|
* reporting all-green.
|
|
183
162
|
*/
|
|
184
163
|
export declare function unverifiedUiLibraryNote(library: string, installGated?: boolean): string;
|
|
185
|
-
export declare const SVELTEKIT_SETUP_GATED_NOTE = "The install gate scaffolds a SvelteKit app
|
|
164
|
+
export declare const SVELTEKIT_SETUP_GATED_NOTE = "The install gate scaffolds a SvelteKit app whenever the install path changes and requires this hook to connect a session, so the wiring is proven; driving flows on SvelteKit to a verdict is not. If the hook does not register a session, please open an issue.";
|
|
186
165
|
export declare const UNVERIFIED_TANSTACK_START_NOTE = "Reticle has no TanStack Start app and no CI gate for one, so this wiring is untested \u2014 it may work, but nothing proves it and nothing will tell us if it breaks. Gated today: Vite (React, Vue), Next.js, Nuxt, React Router, Remix, Astro, SvelteKit, Create React App and Angular. If the client effect does not register a session, please open an issue.";
|
|
187
166
|
/**
|
|
188
167
|
* Printed when init detects react-three-fiber. Reticle's model is DOM + store + network; a WebGL
|
package/dist/patch/snippets.js
CHANGED
|
@@ -46,24 +46,21 @@ export function connectArg(port, projectId) {
|
|
|
46
46
|
parts.push(`projectId: '${projectId}'`);
|
|
47
47
|
return parts.length > 0 ? `{ ${parts.join(', ')} }` : '';
|
|
48
48
|
}
|
|
49
|
+
/** The gitignored module beside the page that holds this machine's pairing token. */
|
|
50
|
+
export const LOCAL_TOKEN_MODULE = 'reticle.local.js';
|
|
51
|
+
/** The token as the printed and written snippets read it, from the module they import. */
|
|
52
|
+
const LOCAL_TOKEN_EXPR = 'local.token';
|
|
49
53
|
/**
|
|
50
|
-
*
|
|
54
|
+
* A connect literal with `token` set to a JS expression, never to the token itself.
|
|
51
55
|
*
|
|
52
|
-
* The token
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* socket with AUTH_FAILED and no session ever appears: see Bridge's hello handler.
|
|
56
|
-
*
|
|
57
|
-
* An empty token is omitted rather than emitted as `token: ''`. A daemon that could not write to
|
|
58
|
-
* $HOME runs without auth and trusts loopback, and an empty string would fail the comparison against
|
|
59
|
-
* one that does hold a token.
|
|
56
|
+
* The token used to be a literal in every printed snippet. It then stayed in terminal history,
|
|
57
|
+
* in agent transcripts, and in every file the snippet was pasted into. The snippets now read it at
|
|
58
|
+
* run time: from the gitignored module, or, in Python, from the token file.
|
|
60
59
|
*/
|
|
61
|
-
export function
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const inner = base.length > 0 ? base.slice(1, -1).trim() : '';
|
|
66
|
-
return `{ ${[inner, `token: '${pairingToken}'`].filter((p) => p.length > 0).join(', ')} }`;
|
|
60
|
+
export function withTokenExpression(connectArgLiteral, tokenExpression) {
|
|
61
|
+
return '' === connectArgLiteral
|
|
62
|
+
? `{ token: ${tokenExpression} }`
|
|
63
|
+
: `{ ...${connectArgLiteral}, token: ${tokenExpression} }`;
|
|
67
64
|
}
|
|
68
65
|
/**
|
|
69
66
|
* Does this codebase want the React kit, or the framework-neutral sensor? The ONE answer, read by
|
|
@@ -537,104 +534,44 @@ export function staticPageSnippet(connectArgLiteral) {
|
|
|
537
534
|
reticle.connect(${connectArgLiteral});
|
|
538
535
|
</script>`;
|
|
539
536
|
}
|
|
540
|
-
/**
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
st.html(
|
|
550
|
-
"""
|
|
551
|
-
<script>
|
|
552
|
-
(() => {
|
|
553
|
-
if (document.getElementById('reticle-streamlit-connect')) return;
|
|
554
|
-
const marker = document.createElement('meta');
|
|
555
|
-
marker.id = 'reticle-streamlit-connect';
|
|
556
|
-
document.head.appendChild(marker);
|
|
557
|
-
void import('${CDN_SDK_URL}')
|
|
558
|
-
.then(({ reticle }) => reticle.connect(${connectArgLiteral}))
|
|
559
|
-
.catch((error) => {
|
|
560
|
-
marker.remove();
|
|
561
|
-
throw error;
|
|
562
|
-
});
|
|
563
|
-
})();
|
|
564
|
-
</script>
|
|
565
|
-
""",
|
|
566
|
-
unsafe_allow_javascript=True,
|
|
567
|
-
)`;
|
|
568
|
-
}
|
|
569
|
-
/**
|
|
570
|
-
* DEBUG-only middleware that injects Reticle into every Django-rendered page.
|
|
571
|
-
*
|
|
572
|
-
* Django serves templates, so the generic script tag has no single home: an app has many templates
|
|
573
|
-
* and a base template it may not have. Middleware has exactly one, runs for every page, and is the
|
|
574
|
-
* idiomatic Django answer — a field reporter arrived at this same shape by hand after `init` printed
|
|
575
|
-
* a script tag and exited 1, and had to write the middleware and `.reticle.json` themselves.
|
|
576
|
-
*
|
|
577
|
-
* Guarded on `settings.DEBUG` so it can never reach production, and on the response being HTML so it
|
|
578
|
-
* does not corrupt a JSON API response or a file download. Injected before `</body>` rather than in
|
|
579
|
-
* `<head>`, so the SDK sees a parsed document.
|
|
580
|
-
*/
|
|
581
|
-
export function djangoMiddlewareSnippet(connectArgLiteral) {
|
|
582
|
-
return `# reticle_dev.py: add to MIDDLEWARE while DEBUG is on:
|
|
583
|
-
# MIDDLEWARE = [..., "reticle_dev.ReticleDevMiddleware"]
|
|
584
|
-
from django.conf import settings
|
|
585
|
-
|
|
586
|
-
_SNIPPET = """<script type="module">
|
|
587
|
-
import { reticle } from '${CDN_SDK_URL}';
|
|
588
|
-
reticle.connect(${connectArgLiteral});
|
|
589
|
-
</script>"""
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
class ReticleDevMiddleware:
|
|
593
|
-
"""Inject the Reticle SDK into HTML responses during development only."""
|
|
594
|
-
|
|
595
|
-
def __init__(self, get_response):
|
|
596
|
-
self.get_response = get_response
|
|
597
|
-
|
|
598
|
-
def __call__(self, request):
|
|
599
|
-
response = self.get_response(request)
|
|
600
|
-
if not settings.DEBUG:
|
|
601
|
-
return response
|
|
602
|
-
if "text/html" not in response.get("Content-Type", ""):
|
|
603
|
-
return response
|
|
604
|
-
# Streaming responses have no .content to rewrite; leave them alone.
|
|
605
|
-
if getattr(response, "streaming", False):
|
|
606
|
-
return response
|
|
607
|
-
body = response.content.decode(response.charset)
|
|
608
|
-
if "</body>" not in body:
|
|
609
|
-
return response
|
|
610
|
-
response.content = body.replace("</body>", _SNIPPET + "</body>", 1).encode(
|
|
611
|
-
response.charset
|
|
612
|
-
)
|
|
613
|
-
if response.has_header("Content-Length"):
|
|
614
|
-
response["Content-Length"] = str(len(response.content))
|
|
615
|
-
return response
|
|
616
|
-
`;
|
|
537
|
+
/** The same snippet, with the token read from the gitignored module beside the page. */
|
|
538
|
+
export function staticPageSnippetWithLocalToken(connectArgLiteral) {
|
|
539
|
+
return ` <script type="module">
|
|
540
|
+
Promise.all([
|
|
541
|
+
import('${CDN_SDK_URL}'),
|
|
542
|
+
import('./${LOCAL_TOKEN_MODULE}').catch(() => ({})),
|
|
543
|
+
]).then(([{ reticle }, local]) => reticle.connect(${withTokenExpression(connectArgLiteral, LOCAL_TOKEN_EXPR)}));
|
|
544
|
+
</script>`;
|
|
617
545
|
}
|
|
618
546
|
export function htmlManual(port, projectId, pairingToken,
|
|
619
547
|
// Which SDK the bundled form imports must be the one init INSTALLED. A page with no UI library
|
|
620
548
|
// now gets the sensor, and a snippet still naming `@reticlehq/react` would import a package that
|
|
621
549
|
// is not there.
|
|
622
550
|
uiLibrary = UiLibrary.UNKNOWN) {
|
|
623
|
-
const
|
|
551
|
+
const base = connectArg(port, projectId);
|
|
552
|
+
// The token is read from the gitignored module that `init` writes, never printed here.
|
|
553
|
+
const hasToken = pairingToken !== undefined && 0 < pairingToken.length;
|
|
554
|
+
const withToken = hasToken ? withTokenExpression(base, LOCAL_TOKEN_EXPR) : base;
|
|
624
555
|
const sdk = sdkImport(uiLibrary, Framework.HTML);
|
|
625
|
-
const
|
|
626
|
-
|
|
627
|
-
|
|
556
|
+
const install = sdk.usesInstall ? '\n install();' : '';
|
|
557
|
+
const names = sdk.usesInstall ? '{ reticle, install }' : '{ reticle }';
|
|
558
|
+
const bundledImport = hasToken
|
|
559
|
+
? `void Promise.all([
|
|
560
|
+
import('${sdk.specifier}'),
|
|
561
|
+
import('./${LOCAL_TOKEN_MODULE}').catch(() => ({})),
|
|
562
|
+
]).then(([${names}, local]) => {${install}
|
|
628
563
|
reticle.connect(${withToken});
|
|
629
564
|
});`
|
|
630
|
-
: `void import('${sdk.specifier}').then(({
|
|
565
|
+
: `void import('${sdk.specifier}').then((${names}) => {${install}
|
|
631
566
|
reticle.connect(${withToken});
|
|
632
567
|
});`;
|
|
633
|
-
const tokenNote =
|
|
634
|
-
? '
|
|
635
|
-
|
|
636
|
-
bridge REJECTS a connect without
|
|
637
|
-
per-machine
|
|
568
|
+
const tokenNote = hasToken
|
|
569
|
+
? `\n\n \`${LOCAL_TOKEN_MODULE}\` holds this machine's pairing token. Init wrote it at the project root and
|
|
570
|
+
added it to .gitignore. Change the import path so it points there from your entry module. Keep it:
|
|
571
|
+
the bridge REJECTS a connect without the token ("authentication failed") and no session appears.
|
|
572
|
+
It is per-machine, so do not commit it. A teammate runs \`reticle init\` for their own.`
|
|
573
|
+
: '';
|
|
574
|
+
const pageSnippet = hasToken ? staticPageSnippetWithLocalToken(base) : staticPageSnippet(base);
|
|
638
575
|
return `No Vite/Next plugin detected — wire the dev-only connect by hand. Pick the form for your setup:
|
|
639
576
|
|
|
640
577
|
• Bundled app (Create React App, webpack, Parcel, Vue/Svelte CLI, etc.) — add to your ENTRY module
|
|
@@ -648,7 +585,7 @@ uiLibrary = UiLibrary.UNKNOWN) {
|
|
|
648
585
|
paste this into the page, in a template you only serve in development. There is nothing to
|
|
649
586
|
install: no npm, no bundler, no package.json.
|
|
650
587
|
|
|
651
|
-
${
|
|
588
|
+
${pageSnippet}
|
|
652
589
|
|
|
653
590
|
Serving the app on something other than localhost (a hosts-file alias, a LAN IP, a container, a
|
|
654
591
|
tunnel)? You need TWO things, not one: \`allowNonLocalhost: true\` AND a pairing token, passed as
|
|
@@ -675,9 +612,9 @@ export function unverifiedUiLibraryNote(library, installGated = false) {
|
|
|
675
612
|
const identity = 'preact' === library
|
|
676
613
|
? 'React component identity — component names and stacks — comes from `@reticlehq/react`, which reaches Preact through `preact/compat`. That path is not covered by a CI gate here, so treat it as expected-to-work rather than proven.'
|
|
677
614
|
: 'What `@reticlehq/react` adds and you will NOT get is React component identity: component names and component stacks.';
|
|
678
|
-
return `Detected a ${library} app. Reticle's DOM, network, console and state tools work here. ${'vue' === library ? 'Source `file:line` does NOT come through: the build plugin stamps JSX and, separately, Svelte components, and a Vue single-file component is neither — measured, a Svelte counter reports `src/lib/Counter.svelte:5` and the same drive on Vue reports no source at all.' : 'Source `file:line` does too — the build plugin stamps it for this library (measured on preact and svelte).'} ${identity} ${installGated ? `The install gate scaffolds this ${library} stack from scratch
|
|
615
|
+
return `Detected a ${library} app. Reticle's DOM, network, console and state tools work here. ${'vue' === library ? 'Source `file:line` does NOT come through: the build plugin stamps JSX and, separately, Svelte components, and a Vue single-file component is neither — measured, a Svelte counter reports `src/lib/Counter.svelte:5` and the same drive on Vue reports no source at all.' : 'Source `file:line` does too — the build plugin stamps it for this library (measured on preact and svelte).'} ${identity} ${installGated ? `The install gate scaffolds this ${library} stack from scratch whenever the install path changes, so this SETUP is proven; no gate drives it to a verdict, so the drive is not.` : `No CI gate covers ${library}.`} Driven to a verdict in CI: Vite + React and Next.js; Astro is checked for a connected session. If something doesn't work, please open an issue.`;
|
|
679
616
|
}
|
|
680
|
-
export const SVELTEKIT_SETUP_GATED_NOTE = 'The install gate scaffolds a SvelteKit app
|
|
617
|
+
export const SVELTEKIT_SETUP_GATED_NOTE = 'The install gate scaffolds a SvelteKit app whenever the install path changes and requires this hook to connect a session, so the wiring is proven; driving flows on SvelteKit to a verdict is not. If the hook does not register a session, please open an issue.';
|
|
681
618
|
export const UNVERIFIED_TANSTACK_START_NOTE = 'Reticle has no TanStack Start app and no CI gate for one, so this wiring is untested — it may work, but nothing proves it and nothing will tell us if it breaks. Gated today: Vite (React, Vue), Next.js, Nuxt, React Router, Remix, Astro, SvelteKit, Create React App and Angular. If the client effect does not register a session, please open an issue.';
|
|
682
619
|
/**
|
|
683
620
|
* Printed when init detects react-three-fiber. Reticle's model is DOM + store + network; a WebGL
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
export declare const HTML_INDEX_PATH = "index.html";
|
|
23
23
|
/** The gitignored module beside the page that holds this machine's pairing token. */
|
|
24
24
|
export declare const STATIC_TOKEN_MODULE = "reticle.local.js";
|
|
25
|
+
export declare const STATIC_GITIGNORE_PATH = ".gitignore";
|
|
25
26
|
/** Opens the block `init` writes; a re-run looks for it. */
|
|
26
27
|
export declare const STATIC_SNIPPET_MARKER = "<!-- reticle:connect (dev only, written by reticle init) -->";
|
|
27
28
|
/** Whether this page already carries a Reticle connect. */
|
|
@@ -18,12 +18,12 @@
|
|
|
18
18
|
* gitignores, which the block imports and any static server serves. Absent — a teammate's
|
|
19
19
|
* checkout, a published copy — the connect goes without one, and the bridge says why it refused.
|
|
20
20
|
*/
|
|
21
|
-
import { CDN_SDK_URL } from './snippets.js';
|
|
21
|
+
import { CDN_SDK_URL, LOCAL_TOKEN_MODULE, withTokenExpression } from './snippets.js';
|
|
22
22
|
/** The page a static site serves at its root, and the one file `init` will write the snippet into. */
|
|
23
23
|
export const HTML_INDEX_PATH = 'index.html';
|
|
24
24
|
/** The gitignored module beside the page that holds this machine's pairing token. */
|
|
25
|
-
export const STATIC_TOKEN_MODULE =
|
|
26
|
-
const
|
|
25
|
+
export const STATIC_TOKEN_MODULE = LOCAL_TOKEN_MODULE;
|
|
26
|
+
export const STATIC_GITIGNORE_PATH = '.gitignore';
|
|
27
27
|
/** Opens the block `init` writes; a re-run looks for it. */
|
|
28
28
|
export const STATIC_SNIPPET_MARKER = '<!-- reticle:connect (dev only, written by reticle init) -->';
|
|
29
29
|
const STATIC_SNIPPET_END = '<!-- /reticle:connect -->';
|
|
@@ -42,9 +42,7 @@ export function hasStaticSnippet(html) {
|
|
|
42
42
|
}
|
|
43
43
|
function block(connectArgLiteral) {
|
|
44
44
|
const hosts = LOOPBACK_HOSTS.map((h) => `'${h}'`).join(', ');
|
|
45
|
-
const arg = ''
|
|
46
|
-
? '{ token: local.token }'
|
|
47
|
-
: `{ ...${connectArgLiteral}, token: local.token }`;
|
|
45
|
+
const arg = withTokenExpression(connectArgLiteral, 'local.token');
|
|
48
46
|
return ` ${STATIC_SNIPPET_MARKER}
|
|
49
47
|
<script type="module">
|
|
50
48
|
if ([${hosts}].includes(location.hostname)) {
|
|
@@ -71,12 +69,12 @@ export function staticTokenFiles(pairingToken, readFile) {
|
|
|
71
69
|
const module = staticTokenModule(pairingToken);
|
|
72
70
|
if (readFile(STATIC_TOKEN_MODULE) !== module)
|
|
73
71
|
files[STATIC_TOKEN_MODULE] = module;
|
|
74
|
-
const ignore = readFile(
|
|
72
|
+
const ignore = readFile(STATIC_GITIGNORE_PATH) ?? '';
|
|
75
73
|
const covered = ignore
|
|
76
74
|
.split('\n')
|
|
77
75
|
.some((l) => [STATIC_TOKEN_MODULE, `/${STATIC_TOKEN_MODULE}`].includes(l.trim()));
|
|
78
76
|
if (!covered) {
|
|
79
|
-
files[
|
|
77
|
+
files[STATIC_GITIGNORE_PATH] =
|
|
80
78
|
`${'' === ignore ? '' : ignore.replace(/\n*$/, '\n')}${STATIC_TOKEN_MODULE}\n`;
|
|
81
79
|
}
|
|
82
80
|
return files;
|
|
@@ -44,6 +44,12 @@ function reticlePluginCall(opts) {
|
|
|
44
44
|
}
|
|
45
45
|
/** Matches the start of a `plugins: [` array literal. */
|
|
46
46
|
const PLUGINS_ARRAY = /plugins\s*:\s*\[/;
|
|
47
|
+
/**
|
|
48
|
+
* A `plugins` key in any shape: `plugins: <expr>`, or the shorthand `{ plugins }`. When this holds
|
|
49
|
+
* and PLUGINS_ARRAY does not, the key is there but not as an array literal, so adding our own key
|
|
50
|
+
* would make a second one. The later key wins, and one of the two lists is lost.
|
|
51
|
+
*/
|
|
52
|
+
const PLUGINS_KEY = /\bplugins\s*[:,}]/;
|
|
47
53
|
/** Matches an ES import statement (used to place our import after the last one). */
|
|
48
54
|
const IMPORT_LINE = /^import\s.+from\s+['"][^'"]+['"];?\s*$/gm;
|
|
49
55
|
/**
|
|
@@ -59,6 +65,8 @@ const CONFIG_OBJECT = /(export\s+default\s+(?:defineConfig\s*\(\s*)?)\{/;
|
|
|
59
65
|
/** Alias kept so existing call sites read in Vite terms; the vocabulary is shared (see patch-kind). */
|
|
60
66
|
export const VitePatchKind = PatchKind;
|
|
61
67
|
const NO_PLUGINS_REASON = "couldn't find a `plugins: [...]` array to extend";
|
|
68
|
+
const PLUGINS_NOT_ARRAY_REASON = 'the `plugins` key is not an array literal, so init did not change it. Add `reticle()` to the ' +
|
|
69
|
+
'list that the key builds. Do not add a second `plugins` key: the later key replaces the first.';
|
|
62
70
|
function insertImport(source) {
|
|
63
71
|
const matches = [...source.matchAll(IMPORT_LINE)];
|
|
64
72
|
const last = matches[matches.length - 1];
|
|
@@ -114,6 +122,9 @@ export function patchViteConfig(source, port, captureBodies = false, inject = tr
|
|
|
114
122
|
if (PLUGINS_ARRAY.test(source)) {
|
|
115
123
|
return { kind: VitePatchKind.APPLY, code: insertImport(insertPlugin(source, call)) };
|
|
116
124
|
}
|
|
125
|
+
if (PLUGINS_KEY.test(source)) {
|
|
126
|
+
return { kind: VitePatchKind.MANUAL, reason: PLUGINS_NOT_ARRAY_REASON };
|
|
127
|
+
}
|
|
117
128
|
if (CONFIG_OBJECT.test(source)) {
|
|
118
129
|
return { kind: VitePatchKind.APPLY, code: insertImport(insertPluginsKey(source, call)) };
|
|
119
130
|
}
|
|
@@ -18,9 +18,11 @@ export declare const StepTitle: {
|
|
|
18
18
|
readonly RETICLE_DEV_COMPONENT: "ReticleDev component";
|
|
19
19
|
readonly NEXT_CONFIG: "Next config (withReticle)";
|
|
20
20
|
readonly MOUNT_RETICLE_DEV: "Mount ReticleDev";
|
|
21
|
+
readonly ROUTE_GROUPS_WITHOUT_RETICLE_DEV: "Route groups without ReticleDev";
|
|
21
22
|
readonly CONNECT_MODULE: "Reticle connect module";
|
|
22
23
|
readonly PAIRING_TOKEN: "Pairing token";
|
|
23
24
|
readonly PAIRING_TOKEN_PER_MACHINE: "Pairing token is per-machine";
|
|
25
|
+
readonly STATIC_PAIRING_TOKEN: "Pairing token (static page)";
|
|
24
26
|
readonly CONNECT_SNIPPET_CRA: "Connect snippet (CRA)";
|
|
25
27
|
readonly CONNECT_SNIPPET_NUXT: "Connect snippet (Nuxt)";
|
|
26
28
|
readonly NUXT_CONFIG: "Nuxt config (token + watcher)";
|
|
@@ -19,9 +19,11 @@ export const StepTitle = {
|
|
|
19
19
|
RETICLE_DEV_COMPONENT: 'ReticleDev component',
|
|
20
20
|
NEXT_CONFIG: 'Next config (withReticle)',
|
|
21
21
|
MOUNT_RETICLE_DEV: 'Mount ReticleDev',
|
|
22
|
+
ROUTE_GROUPS_WITHOUT_RETICLE_DEV: 'Route groups without ReticleDev',
|
|
22
23
|
CONNECT_MODULE: 'Reticle connect module',
|
|
23
24
|
PAIRING_TOKEN: 'Pairing token',
|
|
24
25
|
PAIRING_TOKEN_PER_MACHINE: 'Pairing token is per-machine',
|
|
26
|
+
STATIC_PAIRING_TOKEN: 'Pairing token (static page)',
|
|
25
27
|
CONNECT_SNIPPET_CRA: 'Connect snippet (CRA)',
|
|
26
28
|
CONNECT_SNIPPET_NUXT: 'Connect snippet (Nuxt)',
|
|
27
29
|
NUXT_CONFIG: 'Nuxt config (token + watcher)',
|
|
@@ -98,6 +100,10 @@ export const CONNECT_STEP_TITLES = new Set([
|
|
|
98
100
|
// not, and `gate:install` scaffolds no CRA app, so that change would ship with no coverage of the
|
|
99
101
|
// path it changes. It is worth doing, and worth doing with a scaffold behind it.
|
|
100
102
|
StepTitle.VITE_PLUGIN,
|
|
103
|
+
// A plain page imports its token from a gitignored sibling module. Unlike CRA's, this step never
|
|
104
|
+
// goes MANUAL by plan: it is ⚠ only when the write itself failed — no token module means the page
|
|
105
|
+
// is refused, and no ignore entry means the token is on disk without the protection we promised.
|
|
106
|
+
StepTitle.STATIC_PAIRING_TOKEN,
|
|
101
107
|
// electron-vite's connect IS the renderer plugin. Without it the SDK never injects, and the
|
|
102
108
|
// preload/capture steps are not enough to produce a session.
|
|
103
109
|
StepTitle.ELECTRON_VITE_PLUGIN,
|
|
@@ -65,4 +65,14 @@ export declare const FRAMEWORK_ADAPTERS: Record<Framework, FrameworkAdapter>;
|
|
|
65
65
|
* for the packages, the connect-step titles and the unverified-note flag in the same edit.
|
|
66
66
|
*/
|
|
67
67
|
export declare function frameworkSteps(input: PlanInput): Step[];
|
|
68
|
+
/**
|
|
69
|
+
* The dev-dependencies `reticle init` installs — kit (or sensor) first, build plugin next.
|
|
70
|
+
*
|
|
71
|
+
* `uiLibrary` matters because the framework does not always name the renderer. The rule below was
|
|
72
|
+
* already written for Nuxt, correctly, and applied only there: a Vue app on plain Vite is
|
|
73
|
+
* `Framework.VITE` and a SvelteKit app renders Svelte, and both were being handed the React kit
|
|
74
|
+
* right after `init` had detected the real UI library and said so on screen.
|
|
75
|
+
*/
|
|
76
|
+
export declare function frameworkPackages(framework: Framework, uiLibrary?: UiLibrary): readonly string[];
|
|
77
|
+
export declare function installStep(input: PlanInput): Step;
|
|
68
78
|
export {};
|
|
@@ -18,7 +18,10 @@
|
|
|
18
18
|
*
|
|
19
19
|
* `framework-adapter.test.ts` asserts the seam between the two halves.
|
|
20
20
|
*/
|
|
21
|
-
import { Framework, UiLibrary } from '../detect/detect.js';
|
|
21
|
+
import { Framework, UiLibrary, installCommand, installCommandParts } from '../detect/detect.js';
|
|
22
|
+
import { installFailureHint } from '../diagnose/install-hint.js';
|
|
23
|
+
import { installRetries } from '../diagnose/install-retries.js';
|
|
24
|
+
import { usesReactKit } from '../patch/snippets.js';
|
|
22
25
|
import { StepTitle } from './connect-steps.js';
|
|
23
26
|
import { astroSteps, craSteps, htmlSteps, nextSteps, nuxtSteps, reactRouterSteps, svelteKitSteps, tanstackStartSteps, } from './plan-framework.js';
|
|
24
27
|
import { VITE_PLUGIN_DETAIL, viteSteps } from './plan-vite.js';
|
|
@@ -223,10 +226,10 @@ export const FRAMEWORK_ADAPTERS = {
|
|
|
223
226
|
carriesOwnUnverifiedNote: false,
|
|
224
227
|
},
|
|
225
228
|
[Framework.HTML]: {
|
|
226
|
-
// No bundler plugin to install —
|
|
229
|
+
// No bundler plugin to install — write the static connect into index.html when it is present.
|
|
227
230
|
packages: (kit) => [kit],
|
|
228
231
|
steps: htmlSteps,
|
|
229
|
-
connectStepTitles: [StepTitle.CONNECT_SNIPPET],
|
|
232
|
+
connectStepTitles: [StepTitle.CONNECT_SNIPPET, StepTitle.STATIC_PAIRING_TOKEN],
|
|
230
233
|
carriesOwnUnverifiedNote: false,
|
|
231
234
|
},
|
|
232
235
|
};
|
|
@@ -241,3 +244,119 @@ export const FRAMEWORK_ADAPTERS = {
|
|
|
241
244
|
export function frameworkSteps(input) {
|
|
242
245
|
return FRAMEWORK_ADAPTERS[input.detection.framework].steps(input);
|
|
243
246
|
}
|
|
247
|
+
/**
|
|
248
|
+
* Pin the SDK to the CLI's own version.
|
|
249
|
+
*
|
|
250
|
+
* `pnpm add -D @reticlehq/react` once installed an older release in one project while npm and yarn
|
|
251
|
+
* took the current one in the next — a stale registry metadata cache, invisible to everyone. A
|
|
252
|
+
* version-skewed SDK talking to a newer daemon is the `-32000` failure path: the app connects, the
|
|
253
|
+
* protocol disagrees, and nothing on either side names a version. Asking for the CLI's exact version
|
|
254
|
+
* makes the cache irrelevant, and a skewed pair impossible to install by accident.
|
|
255
|
+
*/
|
|
256
|
+
function pinnedPackages(packages, version) {
|
|
257
|
+
if (version === undefined || 0 === version.length) {
|
|
258
|
+
return packages;
|
|
259
|
+
}
|
|
260
|
+
return packages.map((p) => `${p}@${version}`);
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* The dev-dependencies `reticle init` installs — kit (or sensor) first, build plugin next.
|
|
264
|
+
*
|
|
265
|
+
* `uiLibrary` matters because the framework does not always name the renderer. The rule below was
|
|
266
|
+
* already written for Nuxt, correctly, and applied only there: a Vue app on plain Vite is
|
|
267
|
+
* `Framework.VITE` and a SvelteKit app renders Svelte, and both were being handed the React kit
|
|
268
|
+
* right after `init` had detected the real UI library and said so on screen.
|
|
269
|
+
*/
|
|
270
|
+
export function frameworkPackages(framework, uiLibrary = UiLibrary.UNKNOWN) {
|
|
271
|
+
const kit = usesReactKit(uiLibrary, framework) ? RETICLE_REACT_KIT : RETICLE_BROWSER_SDK;
|
|
272
|
+
return FRAMEWORK_ADAPTERS[framework].packages(kit);
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* What to say when the install command fails.
|
|
276
|
+
*
|
|
277
|
+
* pnpm's `minimumReleaseAge` refuses any release younger than the configured window — a deliberate
|
|
278
|
+
* supply-chain policy, not a bug — with `ERR_PNPM_NO_MATURE_MATCHING_VERSION`. Unpinned it silently
|
|
279
|
+
* resolves to an OLDER version instead, which is how an app ends up running a stale SDK against a
|
|
280
|
+
* newer daemon: the connection succeeds, the protocol disagrees, and the failure surfaces as -32000
|
|
281
|
+
* with nothing naming a version. Pinning turns that into this loud failure, which is the better
|
|
282
|
+
* trade — but only if the message says what to do about it.
|
|
283
|
+
*/
|
|
284
|
+
/**
|
|
285
|
+
* Is this package already declared at the version we would pin?
|
|
286
|
+
*
|
|
287
|
+
* The caret is the point. `npm i -D pkg@3.2.0` WRITES `^3.2.0`, so on every later run the pinned
|
|
288
|
+
* `3.2.0` failed a string comparison against the range npm had just created, and init re-installed
|
|
289
|
+
* for ever. Only the two forms a package manager writes by itself are read - an exact version, and
|
|
290
|
+
* `^`/`~` over that same version.
|
|
291
|
+
*
|
|
292
|
+
* Everything else installs. A false "already" leaves somebody with no SDK and an install that
|
|
293
|
+
* claims to have run; a redundant install only costs time, so the unreadable cases go the safe way.
|
|
294
|
+
*/
|
|
295
|
+
function alreadyDeclared(declared, pinned) {
|
|
296
|
+
if (declared === undefined) {
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
if (pinned === undefined || 0 === pinned.length) {
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
return declared === pinned || `^${pinned}` === declared || `~${pinned}` === declared;
|
|
303
|
+
}
|
|
304
|
+
export function installStep(input) {
|
|
305
|
+
const pm = input.detection.packageManager;
|
|
306
|
+
// `Detection.packageManagerCommand` (see `preflight.ts`, #1149). Falling back to `pm` covers every
|
|
307
|
+
// caller that never went through preflight (tests, and any future one).
|
|
308
|
+
const pmCommand = input.detection.packageManagerCommand ?? pm;
|
|
309
|
+
const packages = pinnedPackages(frameworkPackages(input.detection.framework, input.detection.uiLibrary), input.options.sdkVersion);
|
|
310
|
+
const command = installCommand(pm, packages, pmCommand);
|
|
311
|
+
/*
|
|
312
|
+
* A re-run over an already-wired project does NO dependency work.
|
|
313
|
+
*
|
|
314
|
+
* Reported from the field: `init` on an instrumented npm-workspaces project ran a pnpm dependency
|
|
315
|
+
* migration, which moved the existing `node_modules` aside and broke the dev server. Whatever
|
|
316
|
+
* manager gets chosen, the redundant install is the step that touches the tree — so the fix that
|
|
317
|
+
* matters is not running one that nothing needs. `init` is documented as idempotent; this is the
|
|
318
|
+
* step that was not.
|
|
319
|
+
*/
|
|
320
|
+
const declared = input.detection.dependencies ?? {};
|
|
321
|
+
const version = input.options.sdkVersion;
|
|
322
|
+
const names = frameworkPackages(input.detection.framework, input.detection.uiLibrary);
|
|
323
|
+
if (names.length > 0 && names.every((n) => alreadyDeclared(declared[n], version))) {
|
|
324
|
+
return {
|
|
325
|
+
title: 'Install dependencies',
|
|
326
|
+
target: 'package.json',
|
|
327
|
+
status: StepStatus.ALREADY,
|
|
328
|
+
detail: `${names.join(', ')} already declared${version === undefined || 0 === version.length ? '' : ` at ${version}`}`,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
// Never install into a linked tree: the package manager replaces the link (a workspace's tree).
|
|
332
|
+
if (true === input.nodeModulesLinked) {
|
|
333
|
+
return {
|
|
334
|
+
title: 'Install dependencies',
|
|
335
|
+
target: 'package.json',
|
|
336
|
+
status: StepStatus.SKIP,
|
|
337
|
+
detail: `skipped: node_modules is a link to another tree, which an install here would replace; install them where it points: ${command}`,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
// The person said `--no-install`: a choice, not a step left undone, so not a ⚠.
|
|
341
|
+
if (!input.options.install) {
|
|
342
|
+
return {
|
|
343
|
+
title: 'Install dependencies',
|
|
344
|
+
target: 'package.json',
|
|
345
|
+
status: StepStatus.SKIP,
|
|
346
|
+
detail: `skipped (--no-install); install them yourself: ${command}`,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
const parts = installCommandParts(pm, packages, [], pmCommand);
|
|
350
|
+
return {
|
|
351
|
+
title: 'Install dependencies',
|
|
352
|
+
target: 'package.json',
|
|
353
|
+
status: StepStatus.APPLY,
|
|
354
|
+
detail: command,
|
|
355
|
+
exec: {
|
|
356
|
+
command: parts.command,
|
|
357
|
+
args: parts.args,
|
|
358
|
+
fallback: `${command}\n\n${installFailureHint(pm, pmCommand)}`,
|
|
359
|
+
},
|
|
360
|
+
retries: installRetries(pm, packages, frameworkPackages(input.detection.framework, input.detection.uiLibrary), input.options.sdkVersion, pmCommand),
|
|
361
|
+
};
|
|
362
|
+
}
|
package/dist/plan/notices.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* They live together because that shared test is a real seam.
|
|
10
10
|
*/
|
|
11
|
-
import { CONTAINERISED_TITLE, containerisedDevServerNote } from '../diagnose/containerised-dev-server.js';
|
|
11
|
+
import { CONTAINERISED_TITLE, containerisedDevServerNote, } from '../diagnose/containerised-dev-server.js';
|
|
12
12
|
import { mcpWindowsNote } from '../register/mcp.js';
|
|
13
13
|
import { unverifiedUiLibraryNote, WEBGL_CANVAS_LIMIT_NOTE } from '../patch/snippets.js';
|
|
14
14
|
import { FRAMEWORK_ADAPTERS, installGated } from './framework-adapter.js';
|
|
@@ -9,7 +9,7 @@ import { StepTitle } from './connect-steps.js';
|
|
|
9
9
|
import { patchStep } from './plan-framework.js';
|
|
10
10
|
import { alreadyOrMovedPort, retargetPort } from './port-steps.js';
|
|
11
11
|
import { StepStatus } from './plan-types.js';
|
|
12
|
-
const ANGULAR_SETUP_GATED_NOTE = 'The install gate scaffolds an Angular app (Angular 21, no SSR)
|
|
12
|
+
const ANGULAR_SETUP_GATED_NOTE = 'The install gate scaffolds an Angular app (Angular 21, no SSR) whenever the install path changes and requires a ' +
|
|
13
13
|
'session, and this wiring was proven by hand on Angular 22 with and without SSR; nothing yet ' +
|
|
14
14
|
'drives an Angular app to a verdict on each change. DOM, network, console and state ' +
|
|
15
15
|
'tools work here; component identity does not (that is the React adapter). If no session ' +
|
|
@@ -66,7 +66,7 @@ export declare function svelteKitSteps(input: PlanInput): Step[];
|
|
|
66
66
|
*/
|
|
67
67
|
export declare function astroSteps(input: PlanInput): Step[];
|
|
68
68
|
/**
|
|
69
|
-
* The plain-HTML path:
|
|
69
|
+
* The plain-HTML path: write the static connect when the page exists, otherwise print the recipe.
|
|
70
70
|
*
|
|
71
71
|
* A function of its own, and NOT a fallthrough. Every framework is sent here on purpose or not at
|
|
72
72
|
* all — a member of `Framework` with no adapter entry is a compile error in `FRAMEWORK_ADAPTERS`,
|