@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,138 @@
|
|
|
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 { VITE_PLUGIN_DETAIL, astroSteps, craSteps, htmlSteps, nextSteps, nuxtSteps, reactRouterSteps, svelteKitSteps, tanstackStartSteps, viteSteps, } from './plan-framework.js';
|
|
24
|
+
import { electronViteSteps } from './plan-electron-vite.js';
|
|
25
|
+
// An app dev installs exactly the audience-scoped browser-side dependencies — never the retired
|
|
26
|
+
// `@reticlehq/core` umbrella (which dragged the Node MCP server + ws into every app). The kit is the
|
|
27
|
+
// framework adapter (it re-exports the browser sensor), paired with that framework's dev-only build
|
|
28
|
+
// plugin for source mapping + connect injection.
|
|
29
|
+
export const RETICLE_REACT_KIT = '@reticlehq/react';
|
|
30
|
+
/** The framework-neutral sensor, for stacks the React adapter has nothing to attach to. */
|
|
31
|
+
export const RETICLE_BROWSER_SDK = '@reticlehq/browser';
|
|
32
|
+
export const RETICLE_VITE_PLUGIN = '@reticlehq/vite-plugin';
|
|
33
|
+
export const RETICLE_NEXT_PLUGIN = '@reticlehq/next';
|
|
34
|
+
/** The Electron main/preload helper — what makes IPC and screenshots exist at all. */
|
|
35
|
+
export const RETICLE_ELECTRON = '@reticlehq/electron';
|
|
36
|
+
export const FRAMEWORK_ADAPTERS = {
|
|
37
|
+
[Framework.NEXT]: {
|
|
38
|
+
// Next is React by construction, so the detection cannot disagree in a way worth honouring.
|
|
39
|
+
packages: () => [RETICLE_REACT_KIT, RETICLE_NEXT_PLUGIN],
|
|
40
|
+
steps: nextSteps,
|
|
41
|
+
connectStepTitles: [StepTitle.RETICLE_DEV_COMPONENT, StepTitle.MOUNT_RETICLE_DEV],
|
|
42
|
+
carriesOwnUnverifiedNote: false,
|
|
43
|
+
},
|
|
44
|
+
[Framework.NUXT]: {
|
|
45
|
+
// The framework-neutral sensor, NOT the React kit. Nuxt renders Vue, and installing a package
|
|
46
|
+
// named @reticlehq/react — with `react` in its peer dependencies — into a Vue codebase is the
|
|
47
|
+
// single thing most likely to make someone abandon the setup, whether or not it works.
|
|
48
|
+
packages: () => [RETICLE_BROWSER_SDK],
|
|
49
|
+
steps: nuxtSteps,
|
|
50
|
+
// BOTH halves. The config is the only thing in a Nuxt app that can inline the pairing token,
|
|
51
|
+
// and the bridge refuses a connect without one even on localhost — so a ⚠ on it is a guaranteed
|
|
52
|
+
// non-connection, not a caveat.
|
|
53
|
+
connectStepTitles: [StepTitle.CONNECT_SNIPPET_NUXT, StepTitle.NUXT_CONFIG],
|
|
54
|
+
// FALSE since `init` started writing the plugin itself. The Nuxt recipe carries an UNVERIFIED
|
|
55
|
+
// line and is now only the fallback for a config we could not patch — so on the path everybody
|
|
56
|
+
// takes, suppressing the generic note left a Vue app with no honesty line at all. The generic
|
|
57
|
+
// one is also the more accurate of the two now: the install gate scaffolds Nuxt from scratch on
|
|
58
|
+
// every change, so the SETUP is proven and only the drive is not, which is what it says.
|
|
59
|
+
carriesOwnUnverifiedNote: false,
|
|
60
|
+
},
|
|
61
|
+
[Framework.VITE]: {
|
|
62
|
+
// The build plugin stamps `data-reticle-source` regardless of UI library, so a Vue or Svelte
|
|
63
|
+
// app still gets source pointers — it is only component identity that needs the React kit.
|
|
64
|
+
packages: (kit) => [kit, RETICLE_VITE_PLUGIN],
|
|
65
|
+
steps: (input) => viteSteps(input),
|
|
66
|
+
connectStepTitles: [StepTitle.VITE_PLUGIN],
|
|
67
|
+
carriesOwnUnverifiedNote: false,
|
|
68
|
+
},
|
|
69
|
+
[Framework.REACT_ROUTER]: {
|
|
70
|
+
// React Router framework mode is a Vite app that renders React, so the kit and the plugin are
|
|
71
|
+
// both right for it too — only the connect INJECTION differs, and that is the plan's business.
|
|
72
|
+
packages: (kit) => [kit, RETICLE_VITE_PLUGIN],
|
|
73
|
+
// The Vite plugin too, for the reason SvelteKit gets it: React Router framework mode IS a Vite
|
|
74
|
+
// app, and the plugin is what stamps data-reticle-source. Without it the app connects and every
|
|
75
|
+
// verdict comes back with no file:line.
|
|
76
|
+
steps: (input) => [
|
|
77
|
+
...reactRouterSteps(input),
|
|
78
|
+
...viteSteps(input, VITE_PLUGIN_DETAIL.REACT_ROUTER),
|
|
79
|
+
],
|
|
80
|
+
connectStepTitles: [StepTitle.CONNECT_SNIPPET_REACT_ROUTER, StepTitle.VITE_PLUGIN],
|
|
81
|
+
carriesOwnUnverifiedNote: false,
|
|
82
|
+
},
|
|
83
|
+
[Framework.SVELTEKIT]: {
|
|
84
|
+
// SvelteKit builds on Vite; until a dedicated Svelte kit exists it uses the Vite build plugin.
|
|
85
|
+
packages: (kit) => [kit, RETICLE_VITE_PLUGIN],
|
|
86
|
+
// The Vite plugin as well as the client hook. `init` already INSTALLS @reticlehq/vite-plugin for
|
|
87
|
+
// SvelteKit and then never wired it into the config, so it sat in package.json doing nothing —
|
|
88
|
+
// which is why a SvelteKit app connected fine and every verdict had no file:line.
|
|
89
|
+
steps: (input) => [...svelteKitSteps(input), ...viteSteps(input, VITE_PLUGIN_DETAIL.SVELTEKIT)],
|
|
90
|
+
connectStepTitles: [StepTitle.CLIENT_HOOK, StepTitle.VITE_PLUGIN],
|
|
91
|
+
carriesOwnUnverifiedNote: true,
|
|
92
|
+
},
|
|
93
|
+
[Framework.ASTRO]: {
|
|
94
|
+
// Astro owns its own Vite instance and renders its own HTML, so there is no config for the
|
|
95
|
+
// plugin to attach to — the kit alone, connected from a page <script> (see astroManual).
|
|
96
|
+
packages: (kit) => [kit],
|
|
97
|
+
steps: astroSteps,
|
|
98
|
+
connectStepTitles: [StepTitle.CONNECT_SNIPPET_ASTRO],
|
|
99
|
+
carriesOwnUnverifiedNote: false,
|
|
100
|
+
},
|
|
101
|
+
[Framework.ELECTRON_VITE]: {
|
|
102
|
+
// Same kit + Vite plugin as a plain Vite app, plus the Electron main/preload helper. The plugin
|
|
103
|
+
// still stamps and injects; `@reticlehq/electron` is what makes IPC and screenshots exist.
|
|
104
|
+
packages: (kit) => [kit, RETICLE_VITE_PLUGIN, RETICLE_ELECTRON],
|
|
105
|
+
steps: electronViteSteps,
|
|
106
|
+
// electron-vite's connect IS the renderer plugin. The preload and capture steps add IPC and
|
|
107
|
+
// screenshots to a session; without the plugin there is no session to add them to.
|
|
108
|
+
connectStepTitles: [StepTitle.ELECTRON_VITE_PLUGIN],
|
|
109
|
+
carriesOwnUnverifiedNote: false,
|
|
110
|
+
},
|
|
111
|
+
[Framework.TANSTACK_START]: {
|
|
112
|
+
// Start IS a Vite app, so the plugin still stamps `data-reticle-source` — only the connect
|
|
113
|
+
// injection is inapplicable, and that is the plan's business (`inject: false`).
|
|
114
|
+
packages: (kit) => [kit, RETICLE_VITE_PLUGIN],
|
|
115
|
+
steps: (input) => [
|
|
116
|
+
...tanstackStartSteps(input),
|
|
117
|
+
...viteSteps(input, VITE_PLUGIN_DETAIL.TANSTACK_START, false),
|
|
118
|
+
],
|
|
119
|
+
connectStepTitles: [StepTitle.CONNECT_SNIPPET_TANSTACK_START, StepTitle.VITE_PLUGIN],
|
|
120
|
+
// The Start recipe carries its own UNVERIFIED line; a second generic notice would argue with it.
|
|
121
|
+
carriesOwnUnverifiedNote: true,
|
|
122
|
+
},
|
|
123
|
+
[Framework.CRA]: {
|
|
124
|
+
// react-scripts owns its webpack config and cannot be extended without ejecting, so there is no
|
|
125
|
+
// build plugin — the kit alone, imported from src/index.tsx (see cra.ts).
|
|
126
|
+
packages: () => [RETICLE_REACT_KIT],
|
|
127
|
+
steps: craSteps,
|
|
128
|
+
connectStepTitles: [StepTitle.CONNECT_MODULE, StepTitle.CONNECT_SNIPPET_CRA],
|
|
129
|
+
carriesOwnUnverifiedNote: false,
|
|
130
|
+
},
|
|
131
|
+
[Framework.HTML]: {
|
|
132
|
+
// No bundler plugin to install — just the kit; connect is wired by hand (see htmlManual).
|
|
133
|
+
packages: (kit) => [kit],
|
|
134
|
+
steps: htmlSteps,
|
|
135
|
+
connectStepTitles: [StepTitle.CONNECT_SNIPPET],
|
|
136
|
+
carriesOwnUnverifiedNote: false,
|
|
137
|
+
},
|
|
138
|
+
};
|
package/dist/host.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { InitOutcome } from '@reticlehq/core/telemetry';
|
|
2
|
+
/**
|
|
3
|
+
* Everything the scaffolder needs that only the daemon knows.
|
|
4
|
+
*
|
|
5
|
+
* `@reticlehq/init` writes files and runs a package manager; it never opens a socket, reads the
|
|
6
|
+
* bridge's state directory or emits an event. Those four capabilities used to arrive as imports
|
|
7
|
+
* from `@reticlehq/server`, which is exactly what made the scaffolder impossible to release on its
|
|
8
|
+
* own. They arrive as this object instead, carried on `InitIo` so every internal stage that already
|
|
9
|
+
* threads the IO gets it for free rather than growing a second parameter.
|
|
10
|
+
*
|
|
11
|
+
* Every member is REQUIRED. `reportOutcome` in particular: telemetry fails silently, and an optional
|
|
12
|
+
* reporter is a reporter somebody forgets to pass and nobody ever notices is missing. A caller with
|
|
13
|
+
* genuinely nothing to report says so out loud with `SILENT_HOST`.
|
|
14
|
+
*/
|
|
15
|
+
export interface InitHost {
|
|
16
|
+
/**
|
|
17
|
+
* Wrap one internal stage so it can be traced. Must call `fn` exactly once and return its value.
|
|
18
|
+
* The daemon passes `spanSync`; everything else passes the identity below.
|
|
19
|
+
*/
|
|
20
|
+
span<T>(name: string, fields: Record<string, unknown>, fn: () => T): T;
|
|
21
|
+
/** Report how the run went. Best-effort — setup must never fail because a metric did. */
|
|
22
|
+
reportOutcome(outcome: InitOutcome): void;
|
|
23
|
+
/**
|
|
24
|
+
* The daemon's pairing token, minted if nothing has written one yet.
|
|
25
|
+
*
|
|
26
|
+
* `init` inlines this into the CDN snippet, which has no build step — so an empty value there is a
|
|
27
|
+
* page that can never authenticate, and a token regenerated later makes the pasted literal stale.
|
|
28
|
+
* Minting belongs to the bridge, which owns the file; the scaffolder only asks for it.
|
|
29
|
+
*/
|
|
30
|
+
pairingToken(): string;
|
|
31
|
+
/** The install channel the environment declares, or undefined when nothing declared one. */
|
|
32
|
+
installSource(): string | undefined;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A host that does nothing, for callers that drive `init`'s file surface without running an install
|
|
36
|
+
* — `reticle update`'s agent-rules refresh, and the in-memory IO the unit tests build.
|
|
37
|
+
*
|
|
38
|
+
* Named rather than spelled inline at each call site so "this run reports nothing" is a decision
|
|
39
|
+
* somebody made once and can grep for, instead of four independently-forgotten reporters.
|
|
40
|
+
*/
|
|
41
|
+
export declare const SILENT_HOST: InitHost;
|
package/dist/host.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A host that does nothing, for callers that drive `init`'s file surface without running an install
|
|
3
|
+
* — `reticle update`'s agent-rules refresh, and the in-memory IO the unit tests build.
|
|
4
|
+
*
|
|
5
|
+
* Named rather than spelled inline at each call site so "this run reports nothing" is a decision
|
|
6
|
+
* somebody made once and can grep for, instead of four independently-forgotten reporters.
|
|
7
|
+
*/
|
|
8
|
+
export const SILENT_HOST = {
|
|
9
|
+
span(_name, _fields, fn) {
|
|
10
|
+
return fn();
|
|
11
|
+
},
|
|
12
|
+
reportOutcome() {
|
|
13
|
+
/* nothing to report to */
|
|
14
|
+
},
|
|
15
|
+
pairingToken() {
|
|
16
|
+
return '';
|
|
17
|
+
},
|
|
18
|
+
installSource() {
|
|
19
|
+
return undefined;
|
|
20
|
+
},
|
|
21
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The public surface of the Reticle scaffolder.
|
|
3
|
+
*
|
|
4
|
+
* `@reticlehq/server` is the only consumer today and reaches for a handful of things: the runner
|
|
5
|
+
* and its IO, the framework/package-manager detection its doctor and self-update reuse, the two
|
|
6
|
+
* diagnostics, and the leaf vocabulary (platform names, dev-server ports, the init-failure reasons)
|
|
7
|
+
* that both halves of the old single package shared. One entry point rather than a dozen subpath
|
|
8
|
+
* exports: the module layout inside here is not a contract, and pinning it as one is how a package
|
|
9
|
+
* ends up unable to move a file.
|
|
10
|
+
*/
|
|
11
|
+
export { runInit, resolveLockfiles, type InitIo, type InitOptions, type InitResult, } from './run.js';
|
|
12
|
+
export { buildNodeIo } from './node-io.js';
|
|
13
|
+
export { type InitHost, SILENT_HOST } from './host.js';
|
|
14
|
+
export { RETICLE_VERSION, RETICLE_NPM_PACKAGE } from './version.js';
|
|
15
|
+
export { InitFailure } from './init-failure.js';
|
|
16
|
+
export { NodePlatform } from './platform.js';
|
|
17
|
+
export { DEV_SERVER_PORTS, isLikelyDevServerPort, devServerPortWarning, } from './dev-server-ports.js';
|
|
18
|
+
export { configWithInstallSource } from './install-source-config.js';
|
|
19
|
+
export { projectIdOf, rememberProjectOnDisk, type RegistryIo } from './remember-project.js';
|
|
20
|
+
export { detectPackageManager, installCommandParts, parseMajor, PackageManager } from './detect.js';
|
|
21
|
+
export { findWorkspaceApps } from './workspace-apps.js';
|
|
22
|
+
export { refreshAgentRules } from './refresh-rules.js';
|
|
23
|
+
export { diagnoseDesktop, isDesktopProject } from './desktop-doctor.js';
|
|
24
|
+
export { diagnoseWebCsp } from './csp-doctor.js';
|
|
25
|
+
export { reticleConfigContent } from './snippets.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The public surface of the Reticle scaffolder.
|
|
3
|
+
*
|
|
4
|
+
* `@reticlehq/server` is the only consumer today and reaches for a handful of things: the runner
|
|
5
|
+
* and its IO, the framework/package-manager detection its doctor and self-update reuse, the two
|
|
6
|
+
* diagnostics, and the leaf vocabulary (platform names, dev-server ports, the init-failure reasons)
|
|
7
|
+
* that both halves of the old single package shared. One entry point rather than a dozen subpath
|
|
8
|
+
* exports: the module layout inside here is not a contract, and pinning it as one is how a package
|
|
9
|
+
* ends up unable to move a file.
|
|
10
|
+
*/
|
|
11
|
+
export { runInit, resolveLockfiles, } from './run.js';
|
|
12
|
+
export { buildNodeIo } from './node-io.js';
|
|
13
|
+
export { SILENT_HOST } from './host.js';
|
|
14
|
+
export { RETICLE_VERSION, RETICLE_NPM_PACKAGE } from './version.js';
|
|
15
|
+
export { InitFailure } from './init-failure.js';
|
|
16
|
+
export { NodePlatform } from './platform.js';
|
|
17
|
+
export { DEV_SERVER_PORTS, isLikelyDevServerPort, devServerPortWarning, } from './dev-server-ports.js';
|
|
18
|
+
export { configWithInstallSource } from './install-source-config.js';
|
|
19
|
+
export { projectIdOf, rememberProjectOnDisk } from './remember-project.js';
|
|
20
|
+
export { detectPackageManager, installCommandParts, parseMajor, PackageManager } from './detect.js';
|
|
21
|
+
export { findWorkspaceApps } from './workspace-apps.js';
|
|
22
|
+
export { refreshAgentRules } from './refresh-rules.js';
|
|
23
|
+
export { diagnoseDesktop, isDesktopProject } from './desktop-doctor.js';
|
|
24
|
+
export { diagnoseWebCsp } from './csp-doctor.js';
|
|
25
|
+
export { reticleConfigContent } from './snippets.js';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Classified `reticle init` failures — our own vocabulary, never a raw error or a path.
|
|
3
|
+
*
|
|
4
|
+
* Lives here rather than beside the telemetry client because `run.ts` is what classifies a failed
|
|
5
|
+
* run, and `@reticlehq/init` must not depend on the daemon. `@reticlehq/server`'s
|
|
6
|
+
* `telemetry/init-telemetry.ts` re-exports it, so every existing consumer is unchanged.
|
|
7
|
+
*/
|
|
8
|
+
export declare const InitFailure: {
|
|
9
|
+
/** Run outside a project root. The single most common first-run mistake. */
|
|
10
|
+
readonly NO_PACKAGE_JSON: "no_package_json";
|
|
11
|
+
/** The manifest exists and is not valid JSON — a trailing comma, usually. Never a stack trace. */
|
|
12
|
+
readonly MALFORMED_PACKAGE_JSON: "malformed_package_json";
|
|
13
|
+
/** The package manager failed: offline, a locked registry, a broken install. */
|
|
14
|
+
readonly DEPENDENCY_INSTALL: "dependency_install";
|
|
15
|
+
/** The `claude mcp add` step failed — the CLI is missing or refused. Reticle installs but is unreachable. */
|
|
16
|
+
readonly MCP_REGISTRATION: "mcp_registration";
|
|
17
|
+
readonly OTHER: "other";
|
|
18
|
+
};
|
|
19
|
+
export type InitFailure = (typeof InitFailure)[keyof typeof InitFailure];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Classified `reticle init` failures — our own vocabulary, never a raw error or a path.
|
|
3
|
+
*
|
|
4
|
+
* Lives here rather than beside the telemetry client because `run.ts` is what classifies a failed
|
|
5
|
+
* run, and `@reticlehq/init` must not depend on the daemon. `@reticlehq/server`'s
|
|
6
|
+
* `telemetry/init-telemetry.ts` re-exports it, so every existing consumer is unchanged.
|
|
7
|
+
*/
|
|
8
|
+
export const InitFailure = {
|
|
9
|
+
/** Run outside a project root. The single most common first-run mistake. */
|
|
10
|
+
NO_PACKAGE_JSON: 'no_package_json',
|
|
11
|
+
/** The manifest exists and is not valid JSON — a trailing comma, usually. Never a stack trace. */
|
|
12
|
+
MALFORMED_PACKAGE_JSON: 'malformed_package_json',
|
|
13
|
+
/** The package manager failed: offline, a locked registry, a broken install. */
|
|
14
|
+
DEPENDENCY_INSTALL: 'dependency_install',
|
|
15
|
+
/** The `claude mcp add` step failed — the CLI is missing or refused. Reticle installs but is unreachable. */
|
|
16
|
+
MCP_REGISTRATION: 'mcp_registration',
|
|
17
|
+
OTHER: 'other',
|
|
18
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An explicit "do not instrument this" in a file `init` would otherwise edit.
|
|
3
|
+
*
|
|
4
|
+
* `init` runs unattended, usually by an agent, in a repo it has just met. Reported from a monorepo
|
|
5
|
+
* audit: it added the plugin to an app whose `vite.config` carried a comment saying Reticle was
|
|
6
|
+
* deliberately excluded, and in the same run appended to `CLAUDE.md` — the file every agent in that
|
|
7
|
+
* repo reads first. The reporter reverted all of it.
|
|
8
|
+
*
|
|
9
|
+
* A tool that overrides an explicit "no" is one nobody lets near a monorepo twice, and a SILENT
|
|
10
|
+
* override is worse than a refusal because the file it edited is the file that said not to.
|
|
11
|
+
*
|
|
12
|
+
* One marker for every surface. A user should learn `@reticle-ignore` once, not once per file type.
|
|
13
|
+
*/
|
|
14
|
+
/** The marker, exported so no caller restates the string. */
|
|
15
|
+
export declare const OPT_OUT_MARKER = "@reticle-ignore";
|
|
16
|
+
/** True when `source` carries the opt-out marker. Absent or empty source is not an opt-out. */
|
|
17
|
+
export declare function hasOptOut(source: string | undefined): boolean;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An explicit "do not instrument this" in a file `init` would otherwise edit.
|
|
3
|
+
*
|
|
4
|
+
* `init` runs unattended, usually by an agent, in a repo it has just met. Reported from a monorepo
|
|
5
|
+
* audit: it added the plugin to an app whose `vite.config` carried a comment saying Reticle was
|
|
6
|
+
* deliberately excluded, and in the same run appended to `CLAUDE.md` — the file every agent in that
|
|
7
|
+
* repo reads first. The reporter reverted all of it.
|
|
8
|
+
*
|
|
9
|
+
* A tool that overrides an explicit "no" is one nobody lets near a monorepo twice, and a SILENT
|
|
10
|
+
* override is worse than a refusal because the file it edited is the file that said not to.
|
|
11
|
+
*
|
|
12
|
+
* One marker for every surface. A user should learn `@reticle-ignore` once, not once per file type.
|
|
13
|
+
*/
|
|
14
|
+
/** The marker, exported so no caller restates the string. */
|
|
15
|
+
export const OPT_OUT_MARKER = '@reticle-ignore';
|
|
16
|
+
/**
|
|
17
|
+
* Matched as a whole token so neighbouring text cannot trigger it.
|
|
18
|
+
*
|
|
19
|
+
* `@reticle-ignored-paths` in a doc link, or prose mentioning reticle and ignore in one sentence,
|
|
20
|
+
* must not read as an opt-out — a false positive here silently declines to instrument an app that
|
|
21
|
+
* wanted instrumenting, which is the same silent-wrong-outcome failure in the other direction.
|
|
22
|
+
*
|
|
23
|
+
* Case-insensitive: a marker people type by hand gets typed how they like, and refusing
|
|
24
|
+
* `@Reticle-Ignore` would be a rule that exists only to be tripped over.
|
|
25
|
+
*/
|
|
26
|
+
const OPT_OUT_TOKEN = new RegExp(`${OPT_OUT_MARKER}(?![\\w-])`, 'i');
|
|
27
|
+
/** True when `source` carries the opt-out marker. Absent or empty source is not an opt-out. */
|
|
28
|
+
export function hasOptOut(source) {
|
|
29
|
+
return source !== undefined && OPT_OUT_TOKEN.test(source);
|
|
30
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What to say when the dependency install fails.
|
|
3
|
+
*
|
|
4
|
+
* Split out of plan.ts, which was at the 1000-line backstop. A cohesive unit rather than an
|
|
5
|
+
* arbitrary cut: it is prose about one step's failure modes, and the only thing it needs is which
|
|
6
|
+
* package manager was used.
|
|
7
|
+
*/
|
|
8
|
+
import { PackageManager } from './detect.js';
|
|
9
|
+
export declare function installFailureHint(pm: PackageManager): string;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What to say when the dependency install fails.
|
|
3
|
+
*
|
|
4
|
+
* Split out of plan.ts, which was at the 1000-line backstop. A cohesive unit rather than an
|
|
5
|
+
* arbitrary cut: it is prose about one step's failure modes, and the only thing it needs is which
|
|
6
|
+
* package manager was used.
|
|
7
|
+
*/
|
|
8
|
+
import { PackageManager } from './detect.js';
|
|
9
|
+
/**
|
|
10
|
+
* Every package manager fetches, so every one of them can fail for a reason that is nothing to do
|
|
11
|
+
* with the project: offline, a proxy that blocks npmjs, a corporate mirror that is down.
|
|
12
|
+
*
|
|
13
|
+
* The hint used to name only version pinning and pnpm's maturity window. Both are real causes, and
|
|
14
|
+
* neither is that one — so a blocked registry sent the reader through their own dependency versions
|
|
15
|
+
* looking for a problem that was entirely about reachability.
|
|
16
|
+
*/
|
|
17
|
+
const REGISTRY_HINT = 'If it could not reach the registry at all (offline, a proxy, a mirror that is down), that is ' +
|
|
18
|
+
'about reachability and not about this project: check `npm config get registry` and whether this ' +
|
|
19
|
+
'machine can reach it.';
|
|
20
|
+
/**
|
|
21
|
+
* A checkout whose `node_modules` is symlinked into another checkout's `.pnpm` store — a git
|
|
22
|
+
* worktree, or an A/B harness running two copies of the same repo — makes pnpm refuse to add a
|
|
23
|
+
* package with ERR_PNPM_UNEXPECTED_VIRTUAL_STORE, because it will not silently repoint an
|
|
24
|
+
* existing virtual store. The original hint named only the maturity-window cause, so this one
|
|
25
|
+
* sent the reader through their dependency versions looking for a problem that was actually
|
|
26
|
+
* about where the store lives (#683).
|
|
27
|
+
*/
|
|
28
|
+
const VIRTUAL_STORE_HINT = "If pnpm reported ERR_PNPM_UNEXPECTED_VIRTUAL_STORE, this checkout's node_modules is " +
|
|
29
|
+
"symlinked into another checkout's pnpm store (a git worktree, or an A/B harness). Either run " +
|
|
30
|
+
'`pnpm install` once in that other checkout first, or point this one at its own store:\n' +
|
|
31
|
+
' pnpm add -D --config.virtual-store-dir=node_modules/.pnpm <packages>';
|
|
32
|
+
export function installFailureHint(pm) {
|
|
33
|
+
if (pm !== PackageManager.PNPM) {
|
|
34
|
+
return `If the version was refused, install the SDK yourself. ${REGISTRY_HINT}`;
|
|
35
|
+
}
|
|
36
|
+
return ('If pnpm reported ERR_PNPM_NO_MATURE_MATCHING_VERSION, its minimumReleaseAge setting is holding ' +
|
|
37
|
+
'this release back. Either wait out the window, or allow these packages explicitly:\n' +
|
|
38
|
+
' pnpm config set minimumReleaseAgeExclude "@reticlehq/*"\n' +
|
|
39
|
+
'Do NOT drop the version pin — unpinned, pnpm installs an older SDK against a newer daemon, and ' +
|
|
40
|
+
`that mismatch surfaces as a -32000 with nothing naming a version.\n${VIRTUAL_STORE_HINT}\n${REGISTRY_HINT}`);
|
|
41
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The weaker install attempts, in the order they are tried.
|
|
3
|
+
*
|
|
4
|
+
* Ordered by how much each one gives up, cheapest concession first, because the first that succeeds
|
|
5
|
+
* is the one that stands. Split out of plan.ts: the ladder is its own subject — two unrelated
|
|
6
|
+
* install failures, each with a different cost and a different sentence to say afterwards — and
|
|
7
|
+
* plan.ts is at its file cap.
|
|
8
|
+
*/
|
|
9
|
+
import { PackageManager } from './detect.js';
|
|
10
|
+
export interface InstallRetry {
|
|
11
|
+
command: string;
|
|
12
|
+
args: string[];
|
|
13
|
+
note: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Build the ladder for one package manager.
|
|
17
|
+
*
|
|
18
|
+
* `--legacy-peer-deps` is npm-only, and deliberately so: it is the only manager with the flag, and
|
|
19
|
+
* pnpm, yarn and bun would reject an unknown argument — turning one failed install into two, with
|
|
20
|
+
* the second failure blaming a flag the user never asked for.
|
|
21
|
+
*/
|
|
22
|
+
export declare function installRetries(pm: PackageManager, pinned: readonly string[], unpinned: readonly string[], sdkVersion: string | undefined): InstallRetry[];
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The weaker install attempts, in the order they are tried.
|
|
3
|
+
*
|
|
4
|
+
* Ordered by how much each one gives up, cheapest concession first, because the first that succeeds
|
|
5
|
+
* is the one that stands. Split out of plan.ts: the ladder is its own subject — two unrelated
|
|
6
|
+
* install failures, each with a different cost and a different sentence to say afterwards — and
|
|
7
|
+
* plan.ts is at its file cap.
|
|
8
|
+
*/
|
|
9
|
+
import { PackageManager, installCommandParts } from './detect.js';
|
|
10
|
+
const LEGACY_PEER_DEPS = '--legacy-peer-deps';
|
|
11
|
+
/**
|
|
12
|
+
* What the peer-conflict retry gives up, said out loud.
|
|
13
|
+
*
|
|
14
|
+
* npm's default peer resolution is a real check, and overriding it can leave a tree npm would not
|
|
15
|
+
* have built. That is still better than no install — with no install there is no wiring at all —
|
|
16
|
+
* but it is the user's tree and they get to know it was relaxed, and why we thought that was right.
|
|
17
|
+
*/
|
|
18
|
+
const LEGACY_PEER_NOTE = `the pinned install failed and succeeded with ${LEGACY_PEER_DEPS} — this project has a peer ` +
|
|
19
|
+
'dependency conflict npm will not resolve on its own. The Reticle version is still pinned; only ' +
|
|
20
|
+
'peer resolution was relaxed, and only for this install. If your project already builds with ' +
|
|
21
|
+
`${LEGACY_PEER_DEPS} everywhere else this is the same tree you always get; if it does not, the ` +
|
|
22
|
+
'conflict is worth looking at on its own terms.';
|
|
23
|
+
/**
|
|
24
|
+
* Said out loud when the exact-version install failed and the unpinned one worked.
|
|
25
|
+
*
|
|
26
|
+
* It used to assert a cause it cannot know. Every one of nine fixture apps got the same sentence —
|
|
27
|
+
* "the registry refused 2.5.0 (pnpm's minimumReleaseAge holds new releases back)" — when the actual
|
|
28
|
+
* cause on that run was that the version did not exist yet, and the remedy offered was a `pnpm
|
|
29
|
+
* config` command handed to a yarn 1 project that will never read it.
|
|
30
|
+
*
|
|
31
|
+
* This note is built at PLAN time, before anything runs, and `io.exec` returns a bare boolean, so
|
|
32
|
+
* the apply layer has no failure text to hand back either. The honest move is therefore to report
|
|
33
|
+
* the CONSEQUENCE (which is certain and is the part that bites) and offer the remedy that belongs to
|
|
34
|
+
* the manager actually in use — rather than name a cause that is one possibility among several.
|
|
35
|
+
*/
|
|
36
|
+
function unpinnedRetryNote(version, pm) {
|
|
37
|
+
const wanted = version === undefined ? 'the exact version' : version;
|
|
38
|
+
// Kept verbatim for pnpm, where minimumReleaseAge is a real and common cause with a real remedy.
|
|
39
|
+
const remedy = pm === PackageManager.PNPM
|
|
40
|
+
? ' One common cause on pnpm is minimumReleaseAge holding a new release back; if pnpm ' +
|
|
41
|
+
'reported ERR_PNPM_NO_MATURE_MATCHING_VERSION, either wait out the window or allow these ' +
|
|
42
|
+
'packages: pnpm config set minimumReleaseAgeExclude "@reticlehq/*"'
|
|
43
|
+
: '';
|
|
44
|
+
return (`the pinned install of ${wanted} failed, so the newest version the registry WOULD accept was ` +
|
|
45
|
+
`installed instead. That may not match the daemon — if the agent reports protocol errors, check ` +
|
|
46
|
+
`\`versionSkew\` in reticle_sessions.${remedy}`);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Build the ladder for one package manager.
|
|
50
|
+
*
|
|
51
|
+
* `--legacy-peer-deps` is npm-only, and deliberately so: it is the only manager with the flag, and
|
|
52
|
+
* pnpm, yarn and bun would reject an unknown argument — turning one failed install into two, with
|
|
53
|
+
* the second failure blaming a flag the user never asked for.
|
|
54
|
+
*/
|
|
55
|
+
export function installRetries(pm, pinned, unpinned, sdkVersion) {
|
|
56
|
+
return [
|
|
57
|
+
// Peers first: concedes only peer resolution and KEEPS the version pin.
|
|
58
|
+
...(pm === PackageManager.NPM
|
|
59
|
+
? [{ ...installCommandParts(pm, pinned, [LEGACY_PEER_DEPS]), note: LEGACY_PEER_NOTE }]
|
|
60
|
+
: []),
|
|
61
|
+
// Unpinned, last. pnpm resolves the newest MATURE version there, which is how a project with a
|
|
62
|
+
// release-age hold gets a working install instead of no install.
|
|
63
|
+
{ ...installCommandParts(pm, unpinned), note: unpinnedRetryNote(sdkVersion, pm) },
|
|
64
|
+
];
|
|
65
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The same `.reticle.json` with `installSource` added, or `undefined` when there is nothing to add.
|
|
3
|
+
*
|
|
4
|
+
* `installSource` is written only when `.reticle.json` is CREATED, and `init` reports that file as
|
|
5
|
+
* `already exists` on every re-run — so everyone who was already here when the marker shipped, and
|
|
6
|
+
* everyone who ran `init` once without one, keeps `unknown` forever with no way to fix it. That is
|
|
7
|
+
* most of the population the field exists to describe.
|
|
8
|
+
*
|
|
9
|
+
* An EXISTING value is never overwritten. The first channel is the one that brought the user in, and
|
|
10
|
+
* a later `init` through a different route must not take credit for an acquisition it did not make.
|
|
11
|
+
* Anything unparseable is left exactly as it is: a config is user data, and `init` rewriting a file
|
|
12
|
+
* it could not read would be a far worse failure than a missing dimension.
|
|
13
|
+
*
|
|
14
|
+
* Lives here because `init` is the only thing that writes this file. Deciding WHICH channel declared
|
|
15
|
+
* itself stays in the server's `telemetry/install-source.ts`, which re-exports this for its own
|
|
16
|
+
* tests; the value arrives as `PlanInput.installSource`.
|
|
17
|
+
*/
|
|
18
|
+
export declare function configWithInstallSource(source: string | null | undefined, declared: string | undefined): string | undefined;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The same `.reticle.json` with `installSource` added, or `undefined` when there is nothing to add.
|
|
3
|
+
*
|
|
4
|
+
* `installSource` is written only when `.reticle.json` is CREATED, and `init` reports that file as
|
|
5
|
+
* `already exists` on every re-run — so everyone who was already here when the marker shipped, and
|
|
6
|
+
* everyone who ran `init` once without one, keeps `unknown` forever with no way to fix it. That is
|
|
7
|
+
* most of the population the field exists to describe.
|
|
8
|
+
*
|
|
9
|
+
* An EXISTING value is never overwritten. The first channel is the one that brought the user in, and
|
|
10
|
+
* a later `init` through a different route must not take credit for an acquisition it did not make.
|
|
11
|
+
* Anything unparseable is left exactly as it is: a config is user data, and `init` rewriting a file
|
|
12
|
+
* it could not read would be a far worse failure than a missing dimension.
|
|
13
|
+
*
|
|
14
|
+
* Lives here because `init` is the only thing that writes this file. Deciding WHICH channel declared
|
|
15
|
+
* itself stays in the server's `telemetry/install-source.ts`, which re-exports this for its own
|
|
16
|
+
* tests; the value arrives as `PlanInput.installSource`.
|
|
17
|
+
*/
|
|
18
|
+
export function configWithInstallSource(source, declared) {
|
|
19
|
+
if (undefined === declared || null === source || undefined === source)
|
|
20
|
+
return undefined;
|
|
21
|
+
let parsed;
|
|
22
|
+
try {
|
|
23
|
+
parsed = JSON.parse(source);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
if (null === parsed || 'object' !== typeof parsed || Array.isArray(parsed))
|
|
29
|
+
return undefined;
|
|
30
|
+
const fields = parsed;
|
|
31
|
+
if (undefined !== fields['installSource'])
|
|
32
|
+
return undefined;
|
|
33
|
+
return `${JSON.stringify({ ...fields, installSource: declared }, null, 2)}\n`;
|
|
34
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { InitIo } from './run.js';
|
|
2
|
+
/**
|
|
3
|
+
* Resolve the lockfiles set used to pick the package manager. A lockfile in the project root wins;
|
|
4
|
+
* otherwise we walk UP the directory tree (monorepos keep the lockfile at the workspace root, not in
|
|
5
|
+
* each package) so `reticle init` in a sub-package suggests `pnpm add` instead of defaulting to `npm i`.
|
|
6
|
+
*
|
|
7
|
+
* The walk is skipped when the project has its own installed tree, because an INHERITED lockfile is
|
|
8
|
+
* weaker evidence than a `node_modules` sitting right there — the ancestor describes the workspace,
|
|
9
|
+
* the tree describes THIS package. Reported from the field: `init` in a `frontend/` app installed
|
|
10
|
+
* with npm emitted `pnpm add -D` off a repo-root `pnpm-lock.yaml`, pnpm was not on PATH, and the
|
|
11
|
+
* failed install took every downstream wiring step with it. A LOCAL lockfile still wins over the
|
|
12
|
+
* tree — it is a deliberate statement about this package, not an inheritance.
|
|
13
|
+
*/
|
|
14
|
+
export declare function resolveLockfiles(rootFiles: ReadonlySet<string>, cwd: string, io: Pick<InitIo, 'exists'>, nodeModulesMarkers?: ReadonlySet<string>): Set<string>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which lockfile decides the package manager `init` will tell the user to run.
|
|
3
|
+
*
|
|
4
|
+
* Split out of `run.ts` when that file crossed the 1000-line cap: this is one self-contained
|
|
5
|
+
* question with its own test block, and `run.ts` is the orchestration around it.
|
|
6
|
+
*/
|
|
7
|
+
import { dirname, join } from 'node:path';
|
|
8
|
+
import { namesAPackageManager } from './detect.js';
|
|
9
|
+
/** Lockfile basenames, in package-manager preference order (mirrors detect.ts). */
|
|
10
|
+
const LOCKFILE_NAMES = [
|
|
11
|
+
'pnpm-lock.yaml',
|
|
12
|
+
'yarn.lock',
|
|
13
|
+
'bun.lockb',
|
|
14
|
+
'bun.lock',
|
|
15
|
+
'package-lock.json',
|
|
16
|
+
];
|
|
17
|
+
/**
|
|
18
|
+
* Resolve the lockfiles set used to pick the package manager. A lockfile in the project root wins;
|
|
19
|
+
* otherwise we walk UP the directory tree (monorepos keep the lockfile at the workspace root, not in
|
|
20
|
+
* each package) so `reticle init` in a sub-package suggests `pnpm add` instead of defaulting to `npm i`.
|
|
21
|
+
*
|
|
22
|
+
* The walk is skipped when the project has its own installed tree, because an INHERITED lockfile is
|
|
23
|
+
* weaker evidence than a `node_modules` sitting right there — the ancestor describes the workspace,
|
|
24
|
+
* the tree describes THIS package. Reported from the field: `init` in a `frontend/` app installed
|
|
25
|
+
* with npm emitted `pnpm add -D` off a repo-root `pnpm-lock.yaml`, pnpm was not on PATH, and the
|
|
26
|
+
* failed install took every downstream wiring step with it. A LOCAL lockfile still wins over the
|
|
27
|
+
* tree — it is a deliberate statement about this package, not an inheritance.
|
|
28
|
+
*/
|
|
29
|
+
export function resolveLockfiles(rootFiles, cwd, io, nodeModulesMarkers = new Set()) {
|
|
30
|
+
const set = new Set(rootFiles);
|
|
31
|
+
if (LOCKFILE_NAMES.some((name) => set.has(name)))
|
|
32
|
+
return set; // local lockfile is authoritative
|
|
33
|
+
if (namesAPackageManager(nodeModulesMarkers))
|
|
34
|
+
return set;
|
|
35
|
+
let dir = cwd;
|
|
36
|
+
for (let depth = 0; depth < 50; depth++) {
|
|
37
|
+
for (const name of LOCKFILE_NAMES) {
|
|
38
|
+
if (io.exists(join(dir, name))) {
|
|
39
|
+
set.add(name);
|
|
40
|
+
return set;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const parent = dirname(dir);
|
|
44
|
+
if (parent === dir)
|
|
45
|
+
break; // filesystem root
|
|
46
|
+
dir = parent;
|
|
47
|
+
}
|
|
48
|
+
return set;
|
|
49
|
+
}
|