@reticlehq/init 3.7.0 → 3.8.1
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-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-redirect.js +23 -8
- package/dist/diagnose/closing-hint.d.ts +5 -0
- package/dist/diagnose/closing-hint.js +18 -11
- package/dist/gather-plan-input.js +70 -18
- package/dist/host.d.ts +6 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.js +8 -2
- package/dist/no-package-json.js +62 -14
- package/dist/patch/next-patch.d.ts +19 -11
- package/dist/patch/next-patch.js +33 -6
- 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 +9 -30
- package/dist/patch/snippets.js +40 -103
- package/dist/patch/static-page.js +3 -5
- package/dist/patch/vite-config.js +11 -0
- package/dist/plan/connect-steps.d.ts +1 -0
- package/dist/plan/connect-steps.js +1 -0
- package/dist/plan/framework-adapter.d.ts +10 -0
- package/dist/plan/framework-adapter.js +120 -1
- package/dist/plan/notices.js +1 -1
- package/dist/plan/plan-framework.js +101 -32
- package/dist/plan/plan-types.d.ts +27 -0
- package/dist/plan/plan.d.ts +1 -10
- package/dist/plan/plan.js +128 -131
- package/dist/plan/stop-hook-step.d.ts +11 -0
- package/dist/plan/stop-hook-step.js +82 -6
- package/dist/project/agent-rules.d.ts +1 -1
- package/dist/project/agent-rules.js +8 -8
- package/dist/project/refresh-rules.js +2 -1
- 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 +4 -0
- package/dist/run.js +4 -1
- package/package.json +2 -2
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
|
|
@@ -18,11 +18,11 @@
|
|
|
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 =
|
|
25
|
+
export const STATIC_TOKEN_MODULE = LOCAL_TOKEN_MODULE;
|
|
26
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) -->';
|
|
@@ -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)) {
|
|
@@ -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,6 +18,7 @@ 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";
|
|
@@ -19,6 +19,7 @@ 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',
|
|
@@ -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';
|
|
@@ -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';
|
|
@@ -28,6 +28,20 @@ const STATIC_TOKEN_DETAIL = `write this machine’s pairing token to ${STATIC_TO
|
|
|
28
28
|
const STATIC_IGNORE_DETAIL = `add ${STATIC_TOKEN_MODULE} to ${STATIC_GITIGNORE_PATH} — it holds this machine’s pairing token`;
|
|
29
29
|
const STATIC_CONNECT_DETAIL = 'write the dev-only connect snippet (loopback hosts only)';
|
|
30
30
|
const STATIC_CONNECT_ALREADY_DETAIL = `${HTML_INDEX_PATH} already loads the Reticle SDK`;
|
|
31
|
+
/** The package whose presence makes a static page an Electron renderer. */
|
|
32
|
+
const ELECTRON_PACKAGE = 'electron';
|
|
33
|
+
/**
|
|
34
|
+
* An Electron app often serves its renderer from a local HTTP server, so the packaged app runs on
|
|
35
|
+
* `localhost` on every user's machine. The localhost-only guard then lets the snippet run in
|
|
36
|
+
* production, and a packager glob such as `**\/*.js` can ship the token module.
|
|
37
|
+
*/
|
|
38
|
+
const ELECTRON_STATIC_DETAIL = `This is an Electron app. Its packaged renderer can also run on localhost, so init did not ` +
|
|
39
|
+
`write the localhost-only snippet into ${HTML_INDEX_PATH} or the ${STATIC_TOKEN_MODULE} token ` +
|
|
40
|
+
'module. Connect only in development: for example, send `!app.isPackaged` from the main ' +
|
|
41
|
+
'process to the renderer, and call `reticle.connect()` only when it is true. Use ' +
|
|
42
|
+
`@reticlehq/electron for IPC and capture. If ${STATIC_TOKEN_MODULE} is in the project, keep ` +
|
|
43
|
+
`it out of the package: electron-builder \`files: ["!${STATIC_TOKEN_MODULE}"]\`, or Electron ` +
|
|
44
|
+
`Forge \`packagerConfig.ignore: [/${STATIC_TOKEN_MODULE.replaceAll('.', '\\.')}$/]\`.`;
|
|
31
45
|
/**
|
|
32
46
|
* Turn a conservative source patch into a step: applied when it patched, already when the wiring is
|
|
33
47
|
* there, and the hand-edit instructions when the file shape wasn't one we recognise.
|
|
@@ -60,15 +74,21 @@ export function patchStep(title, path, patch, applyDetail, manualDetail) {
|
|
|
60
74
|
*/
|
|
61
75
|
/** The env var withReticle sets from the discovered daemon, and the component must read. */
|
|
62
76
|
const NEXT_DAEMON_URL_ENV = 'NEXT_PUBLIC_RETICLE_URL';
|
|
77
|
+
const NEXT_SDK_VERSION_ENV = 'NEXT_PUBLIC_RETICLE_SDK_VERSION';
|
|
63
78
|
/**
|
|
64
79
|
* The two-line edit that unfreezes an existing install's port.
|
|
65
80
|
*
|
|
66
81
|
* Stated as the edit rather than as a problem, because the reader is an agent that can apply it in
|
|
67
82
|
* one write and is otherwise going to ask what to do.
|
|
68
83
|
*/
|
|
69
|
-
const
|
|
84
|
+
const NEXT_DEV_FILE_STALE_DETAIL_SOLO = 'predates daemon discovery, so it dials the port init saw when it ran. In this file add ' +
|
|
70
85
|
'`const url = process.env.NEXT_PUBLIC_RETICLE_URL;` and spread `...(url ? { url } : {})` into ' +
|
|
71
86
|
'reticle.connect(), after any url already there. Nothing else needs to change.';
|
|
87
|
+
const NEXT_DEV_FILE_STALE_DETAIL_DUAL = 'predates daemon discovery, so it dials the port init saw when it ran. In this file add ' +
|
|
88
|
+
'`const url = process.env.NEXT_PUBLIC_RETICLE_URL;` and spread `...(url ? { url } : {})` into ' +
|
|
89
|
+
'reticle.connect(), after any url already there.';
|
|
90
|
+
const NEXT_SDK_VERSION_DETAIL = 'lacks the SDK version. In this file add `const sdkVersion = process.env.NEXT_PUBLIC_RETICLE_SDK_VERSION;` ' +
|
|
91
|
+
'and spread `...(sdkVersion ? { sdkVersion } : {})` into the reticle.connect() options.';
|
|
72
92
|
export function nextSteps(input) {
|
|
73
93
|
const configFile = input.nextConfigFile ?? 'next.config.mjs';
|
|
74
94
|
const devPath = input.nextReticleDevPath ?? NEXT_RETICLE_DEV_PATH;
|
|
@@ -82,34 +102,52 @@ export function nextSteps(input) {
|
|
|
82
102
|
const devStale = true === input.nextReticleDevExists &&
|
|
83
103
|
'string' === typeof input.nextReticleDevSource &&
|
|
84
104
|
!input.nextReticleDevSource.includes(NEXT_DAEMON_URL_ENV);
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
105
|
+
const sdkVersionMissing = true === input.nextReticleDevExists &&
|
|
106
|
+
'string' === typeof input.nextReticleDevSource &&
|
|
107
|
+
!input.nextReticleDevSource.includes(NEXT_SDK_VERSION_ENV);
|
|
108
|
+
const baseDevStep = devStale
|
|
109
|
+
? {
|
|
110
|
+
title: StepTitle.RETICLE_DEV_COMPONENT,
|
|
111
|
+
target: devPath,
|
|
112
|
+
status: StepStatus.MANUAL,
|
|
113
|
+
detail: sdkVersionMissing
|
|
114
|
+
? `${NEXT_DEV_FILE_STALE_DETAIL_DUAL} Also ${NEXT_SDK_VERSION_DETAIL}`
|
|
115
|
+
: NEXT_DEV_FILE_STALE_DETAIL_SOLO,
|
|
116
|
+
}
|
|
117
|
+
: alreadyOrMovedPort({
|
|
118
|
+
title: StepTitle.RETICLE_DEV_COMPONENT,
|
|
119
|
+
target: devPath,
|
|
120
|
+
status: StepStatus.ALREADY,
|
|
121
|
+
detail: 'file exists',
|
|
122
|
+
}, input.nextReticleDevSource, input.options.port);
|
|
123
|
+
const devNoticeSteps = !devStale && sdkVersionMissing
|
|
124
|
+
? [
|
|
125
|
+
{
|
|
88
126
|
title: StepTitle.RETICLE_DEV_COMPONENT,
|
|
89
127
|
target: devPath,
|
|
90
|
-
status: StepStatus.
|
|
91
|
-
detail:
|
|
92
|
-
}
|
|
93
|
-
|
|
128
|
+
status: StepStatus.NOTICE,
|
|
129
|
+
detail: NEXT_SDK_VERSION_DETAIL,
|
|
130
|
+
},
|
|
131
|
+
]
|
|
132
|
+
: [];
|
|
133
|
+
const devFileSteps = input.nextReticleDevExists
|
|
134
|
+
? [baseDevStep, ...devNoticeSteps]
|
|
135
|
+
: [
|
|
136
|
+
{
|
|
94
137
|
title: StepTitle.RETICLE_DEV_COMPONENT,
|
|
95
138
|
target: devPath,
|
|
96
|
-
status: StepStatus.
|
|
97
|
-
detail: '
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
content: nextReticleDevFile(input.options.port, input.options.projectId, input.testids ?? [], input.storeHints ?? [],
|
|
107
|
-
// Next's dev module is not a sibling of `src/` — its imports are resolved from wherever
|
|
108
|
-
// the component actually lands (`app/`, or `src/app/` in a --src-dir app).
|
|
109
|
-
input.nextFoundStores ?? []),
|
|
139
|
+
status: StepStatus.APPLY,
|
|
140
|
+
detail: 'create dev-only connect component',
|
|
141
|
+
write: {
|
|
142
|
+
path: devPath,
|
|
143
|
+
content: nextReticleDevFile(input.options.port, input.options.projectId, input.testids ?? [], input.storeHints ?? [],
|
|
144
|
+
// Next's dev module is not a sibling of `src/` — its imports are resolved from wherever
|
|
145
|
+
// the component actually lands (`app/`, or `src/app/` in a --src-dir app).
|
|
146
|
+
input.nextFoundStores ?? []),
|
|
147
|
+
},
|
|
148
|
+
dependsOnInstall: true,
|
|
110
149
|
},
|
|
111
|
-
|
|
112
|
-
};
|
|
150
|
+
];
|
|
113
151
|
const configPatch = null === input.nextConfigSource || input.nextConfigSource === undefined
|
|
114
152
|
? { kind: PatchKind.MANUAL, reason: `no ${configFile} found` }
|
|
115
153
|
: patchNextConfig(input.nextConfigSource, true !== input.detection?.customReconciler);
|
|
@@ -141,11 +179,26 @@ export function nextSteps(input) {
|
|
|
141
179
|
},
|
|
142
180
|
]
|
|
143
181
|
: [];
|
|
182
|
+
// Each route-group root layout renders only its own routes, so one mount covers one group: the
|
|
183
|
+
// routes under the other root layouts render without ReticleDev, and saying nothing about them
|
|
184
|
+
// would read as an app that is fully wired.
|
|
185
|
+
const unmounted = input.nextUnmountedLayouts ?? [];
|
|
186
|
+
const otherRootLayouts = null !== layout && 0 < unmounted.length
|
|
187
|
+
? [
|
|
188
|
+
{
|
|
189
|
+
title: StepTitle.ROUTE_GROUPS_WITHOUT_RETICLE_DEV,
|
|
190
|
+
target: unmounted.join(', '),
|
|
191
|
+
status: StepStatus.NOTICE,
|
|
192
|
+
detail: `<ReticleDev /> is mounted in ${layout.path} only; ${unmounted.join(', ')} ${1 === unmounted.length ? 'is a root layout' : 'are root layouts'} of ${1 === unmounted.length ? 'its' : 'their'} own, so the routes under ${1 === unmounted.length ? 'it' : 'them'} render without it and Reticle sees nothing there. Mount it there too (dev-only) to cover them.`,
|
|
193
|
+
},
|
|
194
|
+
]
|
|
195
|
+
: [];
|
|
144
196
|
return [
|
|
145
|
-
|
|
197
|
+
...devFileSteps,
|
|
146
198
|
...nextTodo,
|
|
147
199
|
patchStep(StepTitle.NEXT_CONFIG, configFile, configPatch, 'wrap the export in withReticle (source mapping, dev-only)', nextConfigManual(configFile, true !== input.detection?.customReconciler)),
|
|
148
200
|
patchStep(StepTitle.MOUNT_RETICLE_DEV, layout?.path ?? NEXT_LAYOUT_PATH, layoutPatch, 'mount <ReticleDev /> in the root layout (dev-only)', NEXT_LAYOUT_MANUAL),
|
|
201
|
+
...otherRootLayouts,
|
|
149
202
|
];
|
|
150
203
|
}
|
|
151
204
|
/**
|
|
@@ -529,6 +582,16 @@ export function astroSteps(input) {
|
|
|
529
582
|
patchStep(StepTitle.ASTRO_ENV_DTS, ASTRO_ENV_DTS_PATH, envPatch, 'declare window.__RETICLE_TOKEN__ / __RETICLE_ROOT__ for astro check', manualWithLayout),
|
|
530
583
|
];
|
|
531
584
|
}
|
|
585
|
+
/** Write the gitignored module that holds this machine's pairing token, and its ignore line. */
|
|
586
|
+
function staticTokenSteps(input) {
|
|
587
|
+
return Object.entries(staticTokenFiles(input.pairingToken, (path) => input.htmlLocalSources?.[path] ?? null)).map(([path, content]) => ({
|
|
588
|
+
title: StepTitle.STATIC_PAIRING_TOKEN,
|
|
589
|
+
target: path,
|
|
590
|
+
status: StepStatus.APPLY,
|
|
591
|
+
detail: STATIC_GITIGNORE_PATH === path ? STATIC_IGNORE_DETAIL : STATIC_TOKEN_DETAIL,
|
|
592
|
+
write: { path, content },
|
|
593
|
+
}));
|
|
594
|
+
}
|
|
532
595
|
/**
|
|
533
596
|
* The plain-HTML path: write the static connect when the page exists, otherwise print the recipe.
|
|
534
597
|
*
|
|
@@ -537,17 +600,21 @@ export function astroSteps(input) {
|
|
|
537
600
|
* where the if/else chain this replaced used to hand it these instructions silently.
|
|
538
601
|
*/
|
|
539
602
|
export function htmlSteps(input) {
|
|
603
|
+
if (input.detection.dependencies?.[ELECTRON_PACKAGE] !== undefined) {
|
|
604
|
+
return [
|
|
605
|
+
{
|
|
606
|
+
title: StepTitle.CONNECT_SNIPPET,
|
|
607
|
+
target: HTML_INDEX_PATH,
|
|
608
|
+
status: StepStatus.MANUAL,
|
|
609
|
+
detail: ELECTRON_STATIC_DETAIL,
|
|
610
|
+
},
|
|
611
|
+
];
|
|
612
|
+
}
|
|
540
613
|
const index = input.htmlIndexSource ?? null;
|
|
541
614
|
if (null !== index) {
|
|
542
615
|
// The page is publishable; the pairing token belongs in its gitignored sibling module instead.
|
|
543
616
|
// Refresh that module even when the connect block itself is already in place.
|
|
544
|
-
const steps =
|
|
545
|
-
title: StepTitle.STATIC_PAIRING_TOKEN,
|
|
546
|
-
target: path,
|
|
547
|
-
status: StepStatus.APPLY,
|
|
548
|
-
detail: STATIC_GITIGNORE_PATH === path ? STATIC_IGNORE_DETAIL : STATIC_TOKEN_DETAIL,
|
|
549
|
-
write: { path, content },
|
|
550
|
-
}));
|
|
617
|
+
const steps = staticTokenSteps(input);
|
|
551
618
|
const written = withStaticSnippet(index, connectArg(input.options.port, input.options.projectId));
|
|
552
619
|
return [
|
|
553
620
|
...steps,
|
|
@@ -568,6 +635,8 @@ export function htmlSteps(input) {
|
|
|
568
635
|
];
|
|
569
636
|
}
|
|
570
637
|
return [
|
|
638
|
+
// The manual snippet imports the token from this module rather than printing it.
|
|
639
|
+
...staticTokenSteps(input),
|
|
571
640
|
{
|
|
572
641
|
title: StepTitle.CONNECT_SNIPPET,
|
|
573
642
|
target: HTML_INDEX_PATH,
|