@reticlehq/init 3.3.0 → 3.5.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/capabilities.d.ts +4 -1
- package/dist/detect/capabilities.js +45 -9
- package/dist/detect/detect.d.ts +45 -2
- package/dist/detect/detect.js +76 -5
- package/dist/detect/dev-script.d.ts +26 -2
- package/dist/detect/dev-script.js +80 -7
- package/dist/detect/non-js-project.d.ts +1 -1
- package/dist/detect/non-js-project.js +6 -1
- package/dist/detect/workspace-apps.d.ts +21 -0
- package/dist/detect/workspace-apps.js +111 -12
- package/dist/detect/workspace-redirect.js +28 -2
- package/dist/diagnose/containerised-dev-server.d.ts +12 -4
- package/dist/diagnose/containerised-dev-server.js +36 -4
- package/dist/diagnose/csp-check.d.ts +12 -6
- package/dist/diagnose/csp-check.js +61 -13
- package/dist/diagnose/csp-doctor.d.ts +13 -1
- package/dist/diagnose/csp-doctor.js +2 -2
- package/dist/diagnose/csp-step.d.ts +27 -0
- package/dist/diagnose/csp-step.js +86 -0
- package/dist/diagnose/desktop-doctor.js +8 -2
- package/dist/diagnose/init-failure.d.ts +7 -0
- package/dist/diagnose/init-failure.js +7 -0
- package/dist/diagnose/install-hint.d.ts +9 -1
- package/dist/diagnose/install-hint.js +17 -7
- package/dist/diagnose/install-retries.d.ts +16 -1
- package/dist/diagnose/install-retries.js +50 -3
- package/dist/diagnose/terminal-wrap.d.ts +18 -0
- package/dist/diagnose/terminal-wrap.js +63 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +6 -2
- package/dist/no-package-json.d.ts +10 -0
- package/dist/no-package-json.js +131 -0
- package/dist/node-io.d.ts +12 -6
- package/dist/node-io.js +33 -11
- package/dist/patch/angular.d.ts +68 -0
- package/dist/patch/angular.js +216 -0
- package/dist/patch/astro-patch.d.ts +1 -1
- package/dist/patch/astro-patch.js +2 -2
- package/dist/patch/cra.d.ts +0 -2
- package/dist/patch/cra.js +0 -2
- package/dist/patch/electron-patch.d.ts +10 -1
- package/dist/patch/electron-patch.js +79 -27
- package/dist/patch/electron-vite-patch.js +1 -1
- package/dist/patch/nuxt-patch.d.ts +1 -1
- package/dist/patch/nuxt-patch.js +2 -2
- package/dist/patch/nuxt-snippets.js +6 -6
- package/dist/patch/remix.d.ts +24 -0
- package/dist/patch/remix.js +59 -0
- package/dist/patch/snippets.d.ts +57 -9
- package/dist/patch/snippets.js +119 -28
- package/dist/patch/static-page.d.ts +39 -0
- package/dist/patch/static-page.js +102 -0
- package/dist/patch/tanstack-start.d.ts +36 -8
- package/dist/patch/tanstack-start.js +118 -29
- package/dist/patch/vite-config.d.ts +5 -0
- package/dist/patch/vite-config.js +8 -0
- package/dist/patch/vite-owning-config.d.ts +34 -0
- package/dist/patch/vite-owning-config.js +63 -5
- package/dist/plan/connect-steps.d.ts +7 -1
- package/dist/plan/connect-steps.js +18 -1
- package/dist/plan/framework-adapter.d.ts +3 -1
- package/dist/plan/framework-adapter.js +99 -6
- package/dist/plan/notices.js +4 -3
- package/dist/plan/plan-angular.d.ts +6 -0
- package/dist/plan/plan-angular.js +89 -0
- package/dist/plan/plan-electron-vite.d.ts +6 -3
- package/dist/plan/plan-electron-vite.js +39 -8
- package/dist/plan/plan-framework.d.ts +8 -46
- package/dist/plan/plan-framework.js +81 -312
- package/dist/plan/plan-types.d.ts +29 -0
- package/dist/plan/plan-vite.d.ts +67 -0
- package/dist/plan/plan-vite.js +256 -0
- package/dist/plan/plan.js +38 -34
- package/dist/plan/port-steps.d.ts +45 -0
- package/dist/plan/port-steps.js +119 -0
- package/dist/plan/preflight.d.ts +17 -3
- package/dist/plan/preflight.js +53 -23
- package/dist/register/mcp-clients.js +5 -3
- package/dist/register/mcp.d.ts +18 -5
- package/dist/register/mcp.js +48 -8
- package/dist/run-types.d.ts +8 -0
- package/dist/run.js +118 -80
- package/package.json +3 -4
- package/NOTICE +0 -5
package/dist/plan/preflight.js
CHANGED
|
@@ -11,8 +11,38 @@
|
|
|
11
11
|
* npm ships with node, so refusing for its absence would refuse on a machine that is fine.
|
|
12
12
|
*/
|
|
13
13
|
const ALWAYS_PRESENT = 'npm';
|
|
14
|
+
/** The one program allowed to run a package manager it cannot find on PATH itself. */
|
|
15
|
+
const COREPACK = 'corepack';
|
|
14
16
|
/**
|
|
15
|
-
* The
|
|
17
|
+
* The prefix this run can actually invoke `packageManager` through, or undefined when neither the
|
|
18
|
+
* bare binary nor corepack can.
|
|
19
|
+
*
|
|
20
|
+
* Reported from the field (#1149): a Windows machine with a corepack-managed pnpm — no `pnpm` on
|
|
21
|
+
* PATH, only `corepack`, which resolves it from the project's `packageManager` field — failed the
|
|
22
|
+
* bare `pnpm --version` probe and refused, even though `corepack pnpm --version` succeeds and every
|
|
23
|
+
* later step could have run through it. `corepack <pm>` is not a fallback guess: corepack shipped
|
|
24
|
+
* with node and does exactly what the bare binary would have, so a machine that can only reach it
|
|
25
|
+
* that way is not missing the tool, only the shim. Every other place in this codebase that needs to
|
|
26
|
+
* talk about a corepack-only machine (`Detection.packageManagerCommand` and its readers) points back
|
|
27
|
+
* here rather than re-explaining it.
|
|
28
|
+
*/
|
|
29
|
+
function resolvedPmCommand(io, packageManager) {
|
|
30
|
+
if (ALWAYS_PRESENT === packageManager)
|
|
31
|
+
return packageManager;
|
|
32
|
+
if (io.probe(packageManager, ['--version']))
|
|
33
|
+
return packageManager;
|
|
34
|
+
if (io.probe(COREPACK, [packageManager, '--version']))
|
|
35
|
+
return `${COREPACK} ${packageManager}`;
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The one place this run decides whether — and how — it can invoke the package manager.
|
|
40
|
+
*
|
|
41
|
+
* Resolves the invocation ONCE: earlier, `preflightRefusal` probed the machine to decide whether to
|
|
42
|
+
* refuse, and callers that got past it probed AGAIN, separately, to learn what the resolved command
|
|
43
|
+
* actually was — two subprocesses (the bare binary, then corepack) doing the same test twice on every
|
|
44
|
+
* ordinary run. This is the single call site: it returns either the command every later step should
|
|
45
|
+
* invoke, or the refusal to print, never both and never neither.
|
|
16
46
|
*
|
|
17
47
|
* `packageManager` is the one init RESOLVED, never a raw lockfile check. An inherited
|
|
18
48
|
* `pnpm-lock.yaml` at a monorepo root does not mean the app in `frontend/` uses pnpm — that app's own
|
|
@@ -20,33 +50,33 @@ const ALWAYS_PRESENT = 'npm';
|
|
|
20
50
|
* here from `exists('pnpm-lock.yaml')` refused an npm app sitting under a pnpm monorepo on a machine
|
|
21
51
|
* with no pnpm, which the install gate proves must succeed.
|
|
22
52
|
*/
|
|
23
|
-
export function
|
|
53
|
+
export function preflight(io, packageManager, options = {}) {
|
|
24
54
|
// First: on a read-only checkout nothing else matters, and one access check is cheaper and
|
|
25
55
|
// quieter than spawning a subprocess to discover the same thing.
|
|
26
56
|
if (!io.canWrite()) {
|
|
27
|
-
return
|
|
28
|
-
|
|
57
|
+
return {
|
|
58
|
+
refusal: `${io.cwd()} is not writable, and init has to write into it (.reticle.json, the build config, ` +
|
|
59
|
+
'a capabilities file). Fix the permissions, or run init from a checkout you own.',
|
|
60
|
+
};
|
|
29
61
|
}
|
|
62
|
+
const resolved = resolvedPmCommand(io, packageManager);
|
|
63
|
+
if (resolved !== undefined)
|
|
64
|
+
return { command: resolved };
|
|
30
65
|
// What the project resolves to says nothing about what the machine HAS, and a project committed to
|
|
31
66
|
// pnpm on an npm-only box is an ordinary Monday.
|
|
32
67
|
//
|
|
33
|
-
// `alreadyServed` is `--url
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return (`this project uses ${packageManager} (its lockfile says so) and ${packageManager} is not ` +
|
|
48
|
-
`installed on this machine. Install it (npm i -g ${packageManager}, or corepack enable), or ` +
|
|
49
|
-
'pass --url with the address the app already serves.');
|
|
50
|
-
}
|
|
51
|
-
return undefined;
|
|
68
|
+
// `alreadyServed` is `--url`. This guard exists for the DEV SERVER — to stop `spawn pnpm ENOENT`
|
|
69
|
+
// surfacing inside "the dev server exited" — and `--url` says the app is already up, so init starts
|
|
70
|
+
// nothing and the failure it protects against cannot happen. The dependency install can still fail
|
|
71
|
+
// without the package manager (one step reporting ⚠, a better outcome than writing nothing), so a
|
|
72
|
+
// command is still returned here — the bare name, since neither probe above found one — rather than
|
|
73
|
+
// refusing a run that `--url` already made safe.
|
|
74
|
+
if (true === options.alreadyServed)
|
|
75
|
+
return { command: packageManager };
|
|
76
|
+
return {
|
|
77
|
+
refusal: `this project uses ${packageManager} (its lockfile says so) and ${packageManager} is not ` +
|
|
78
|
+
`installed on this machine — and corepack ${packageManager} could not run it either. Install ` +
|
|
79
|
+
`it (npm i -g ${packageManager}), or enable corepack for it (corepack enable — on Windows this ` +
|
|
80
|
+
'needs an elevated shell), or pass --url with the address the app already serves.',
|
|
81
|
+
};
|
|
52
82
|
}
|
|
@@ -162,9 +162,8 @@ export const MCP_CLIENTS = [
|
|
|
162
162
|
* `~/.gemini/config/mcp_config.json`, and one file serves the 2.0 IDE, the CLI and the SDK. Same
|
|
163
163
|
* `command`/`args` object Cursor, Windsurf and Gemini CLI already use.
|
|
164
164
|
*
|
|
165
|
-
* It shares the `~/.gemini` tree with Gemini CLI
|
|
166
|
-
*
|
|
167
|
-
* so having one installed cannot make `init` write a config for the other.
|
|
165
|
+
* It shares the `~/.gemini` tree with Gemini CLI. Gemini detection requires its settings file:
|
|
166
|
+
* checking only the parent directory also matches an Antigravity-only installation.
|
|
168
167
|
*
|
|
169
168
|
* NOT verified against a running install. The path is documented, the merge is the shared JSON
|
|
170
169
|
* path that every other client here uses, and an unparseable config still degrades to a printed
|
|
@@ -329,6 +328,9 @@ export function clientSnippet(spec) {
|
|
|
329
328
|
export function clientMarkerRelPath(spec) {
|
|
330
329
|
if (spec.scope === ConfigScope.CLI)
|
|
331
330
|
return '';
|
|
331
|
+
// Antigravity also creates ~/.gemini. The shared parent is not evidence of Gemini CLI.
|
|
332
|
+
if (spec.id === McpClient.GEMINI)
|
|
333
|
+
return spec.relPath;
|
|
332
334
|
const parts = spec.relPath.split('/');
|
|
333
335
|
return parts.length > 1 ? parts.slice(0, -1).join('/') : spec.relPath;
|
|
334
336
|
}
|
package/dist/register/mcp.d.ts
CHANGED
|
@@ -66,11 +66,24 @@ interface ClaudeAddCommand {
|
|
|
66
66
|
}
|
|
67
67
|
/** `claude mcp add reticle -s user -- npx @reticlehq/server mcp` — registers globally for all projects (portless). */
|
|
68
68
|
export declare function claudeAddCommand(): ClaudeAddCommand;
|
|
69
|
-
/**
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Is a `reticle` server already registered with Claude Code, in any scope this project sees?
|
|
71
|
+
*
|
|
72
|
+
* READ FROM CLAUDE'S OWN CONFIG, never asked of the CLI. `claude mcp get reticle` health-checks the
|
|
73
|
+
* entry by LAUNCHING it — `npx @reticlehq/server mcp` in the project — which starts a daemon on
|
|
74
|
+
* whatever `.reticle.json` says before `init` has rewritten it. A re-run moving the port brought the
|
|
75
|
+
* old-port daemon back about two seconds after it was stopped. `claude mcp list` health-checks
|
|
76
|
+
* every server the same way.
|
|
77
|
+
*
|
|
78
|
+
* The three scopes `claude mcp add` writes: user (`mcpServers` at the top of the state file), local
|
|
79
|
+
* (`projects[<dir>].mcpServers`, keyed by absolute path) and project (`<dir>/.mcp.json`). Without a
|
|
80
|
+
* project directory only user scope is looked at. An unreadable file answers "not registered",
|
|
81
|
+
* which costs at worst a `claude mcp add` that reports the entry already exists.
|
|
82
|
+
*/
|
|
83
|
+
export declare function claudeHasReticle(io: {
|
|
84
|
+
readFile(path: string): string | null;
|
|
85
|
+
homeDir(): string;
|
|
86
|
+
}, projectDir: string | undefined, configDir?: string | undefined): boolean;
|
|
74
87
|
/** Probe args for whether the `claude` CLI is installed at all. */
|
|
75
88
|
export declare function claudeAvailableProbe(): {
|
|
76
89
|
command: string;
|
package/dist/register/mcp.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* project's `.reticle.json` in its CWD (see cli-port.ts). The port belongs to the project, not the
|
|
12
12
|
* global agent config.
|
|
13
13
|
*/
|
|
14
|
+
import { join } from 'node:path';
|
|
14
15
|
import { RETICLE_NPM_PACKAGE } from '../version.js';
|
|
15
16
|
// The name is a wire identity and lives in the contract; re-exported here so the registration writers
|
|
16
17
|
// below (and their tests) keep reading it from the module that USES it.
|
|
@@ -87,14 +88,53 @@ export function claudeAddCommand() {
|
|
|
87
88
|
const args = [MCP_SUBCOMMAND, 'add', MCP_SERVER_NAME, '-s', 'user', '--', ...tail];
|
|
88
89
|
return { command: CLAUDE_CLI, args, display: `${CLAUDE_CLI} ${args.join(' ')}` };
|
|
89
90
|
}
|
|
90
|
-
/**
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
return
|
|
91
|
+
/** Where Claude Code keeps its state: `$CLAUDE_CONFIG_DIR/.claude.json`, else `~/.claude.json`. */
|
|
92
|
+
const CLAUDE_STATE_FILE = '.claude.json';
|
|
93
|
+
/** `mcpServers` holds a `reticle` entry. */
|
|
94
|
+
function namesReticle(scope) {
|
|
95
|
+
if ('object' !== typeof scope || null === scope)
|
|
96
|
+
return false;
|
|
97
|
+
const servers = scope.mcpServers;
|
|
98
|
+
return 'object' === typeof servers && null !== servers && MCP_SERVER_NAME in servers;
|
|
99
|
+
}
|
|
100
|
+
function parsed(source) {
|
|
101
|
+
if (null === source)
|
|
102
|
+
return undefined;
|
|
103
|
+
try {
|
|
104
|
+
return JSON.parse(source);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Is a `reticle` server already registered with Claude Code, in any scope this project sees?
|
|
112
|
+
*
|
|
113
|
+
* READ FROM CLAUDE'S OWN CONFIG, never asked of the CLI. `claude mcp get reticle` health-checks the
|
|
114
|
+
* entry by LAUNCHING it — `npx @reticlehq/server mcp` in the project — which starts a daemon on
|
|
115
|
+
* whatever `.reticle.json` says before `init` has rewritten it. A re-run moving the port brought the
|
|
116
|
+
* old-port daemon back about two seconds after it was stopped. `claude mcp list` health-checks
|
|
117
|
+
* every server the same way.
|
|
118
|
+
*
|
|
119
|
+
* The three scopes `claude mcp add` writes: user (`mcpServers` at the top of the state file), local
|
|
120
|
+
* (`projects[<dir>].mcpServers`, keyed by absolute path) and project (`<dir>/.mcp.json`). Without a
|
|
121
|
+
* project directory only user scope is looked at. An unreadable file answers "not registered",
|
|
122
|
+
* which costs at worst a `claude mcp add` that reports the entry already exists.
|
|
123
|
+
*/
|
|
124
|
+
export function claudeHasReticle(io, projectDir, configDir = process.env['CLAUDE_CONFIG_DIR']) {
|
|
125
|
+
const state = parsed(io.readFile(join(configDir ?? io.homeDir(), CLAUDE_STATE_FILE)));
|
|
126
|
+
if (namesReticle(state))
|
|
127
|
+
return true;
|
|
128
|
+
if (projectDir === undefined)
|
|
129
|
+
return false;
|
|
130
|
+
const projects = 'object' === typeof state && null !== state
|
|
131
|
+
? state.projects
|
|
132
|
+
: undefined;
|
|
133
|
+
if ('object' === typeof projects && null !== projects) {
|
|
134
|
+
if (namesReticle(projects[projectDir]))
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
return namesReticle(parsed(io.readFile(join(projectDir, CLAUDE_PROJECT_CONFIG))));
|
|
98
138
|
}
|
|
99
139
|
/** Probe args for whether the `claude` CLI is installed at all. */
|
|
100
140
|
export function claudeAvailableProbe() {
|
package/dist/run-types.d.ts
CHANGED
|
@@ -78,6 +78,14 @@ export interface InitIo {
|
|
|
78
78
|
scoped(relPath: string): InitIo;
|
|
79
79
|
/** Runs a subprocess to completion (inherits stdio); returns true on exit code 0. */
|
|
80
80
|
exec(command: string, args: readonly string[]): boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Runs a subprocess quietly and returns its combined output — for a failure whose text names its
|
|
83
|
+
* cause (see explainInstallFailure). Optional: without it the generic failure text stands.
|
|
84
|
+
*/
|
|
85
|
+
capture?(command: string, args: readonly string[]): {
|
|
86
|
+
ok: boolean;
|
|
87
|
+
output: string;
|
|
88
|
+
};
|
|
81
89
|
/** Runs a subprocess quietly (no stdio) for a yes/no check; returns true on exit code 0. */
|
|
82
90
|
probe(command: string, args: readonly string[]): boolean;
|
|
83
91
|
/** Can this process write into the project root — see preflight.ts. */
|
package/dist/run.js
CHANGED
|
@@ -6,26 +6,34 @@
|
|
|
6
6
|
import { OnboardingPhase, OnboardingStepStatus } from '@reticlehq/core/telemetry';
|
|
7
7
|
import { detectMcpClients } from './register/detect-clients.js';
|
|
8
8
|
import { dirname, join } from 'node:path';
|
|
9
|
-
import { CONTAINER_MARKERS } from './diagnose/containerised-dev-server.js';
|
|
9
|
+
import { CONTAINER_MARKERS, runsDevServer } from './diagnose/containerised-dev-server.js';
|
|
10
|
+
import { explainInstallFailure } from './diagnose/install-retries.js';
|
|
10
11
|
import { CSP_FILES } from './diagnose/csp-doctor.js';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import { devCommandFrom } from './detect/dev-script.js';
|
|
12
|
+
import { preflight } from './plan/preflight.js';
|
|
13
|
+
import { devCommandFrom, devScriptBody } from './detect/dev-script.js';
|
|
14
14
|
import { restartHint, FEEDBACK_HINT } from './diagnose/closing-hint.js';
|
|
15
15
|
import { projectIdOf, rememberProjectOnDisk } from './project/remember-project.js';
|
|
16
|
-
import { detect, Framework, UiLibrary } from './detect/detect.js';
|
|
16
|
+
import { ANGULAR_WORKSPACE_FILE, detect, FORGE_RENDERER_CONFIGS, Framework, UiLibrary, } from './detect/detect.js';
|
|
17
|
+
import { ANGULAR_PROXY_PATH, angularEntry } from './patch/angular.js';
|
|
18
|
+
import { HTML_INDEX_PATH } from './patch/static-page.js';
|
|
19
|
+
import { initWithoutPackageJson } from './no-package-json.js';
|
|
17
20
|
import { wasMcpRegistered } from './register/mcp-registered.js';
|
|
18
21
|
import { pickAstroHost } from './patch/astro-host.js';
|
|
19
22
|
import { ELECTRON_VITE_CONFIG_CANDIDATES, NEXT_CONFIG_CANDIDATES, PACKAGE_JSON, VITE_CONFIG_CANDIDATES, } from './detect/workspace-apps.js';
|
|
20
23
|
import { redirectToWorkspaceApp } from './detect/workspace-redirect.js';
|
|
24
|
+
import { enclosingWorkspaceRoot } from './detect/workspace-apps.js';
|
|
21
25
|
import { isConnectStep } from './plan/connect-steps.js';
|
|
22
26
|
import { CURSOR_RULE_PATH, RETICLE_MD_PATH } from './project/agent-rules.js';
|
|
23
27
|
import { CLAUDE_SETTINGS_PATH } from './plan/stop-hook-step.js';
|
|
24
28
|
import { CRA_ENV_PATH } from './patch/cra.js';
|
|
25
29
|
import { NEXT_LAYOUT_CANDIDATES, NEXT_PAGES_APP_CANDIDATES } from './patch/next-patch.js';
|
|
26
30
|
import { formatGeneratedSource } from './patch/format-generated.js';
|
|
31
|
+
import { tanstackStartConnectPath } from './patch/tanstack-start.js';
|
|
32
|
+
import { installedViteMajor } from './patch/vite-owning-config.js';
|
|
27
33
|
/** CRA's bundled entry, in the order create-react-app itself generates them. */
|
|
28
34
|
const CRA_ENTRY_CANDIDATES = ['src/index.tsx', 'src/index.jsx', 'src/index.ts', 'src/index.js'];
|
|
35
|
+
/** Preview snippets must not mint a real machine credential or imply this token is usable. */
|
|
36
|
+
const DRY_RUN_PAIRING_TOKEN = 'DRY_RUN_TOKEN_NOT_FOR_USE';
|
|
29
37
|
function craEntryOf(io) {
|
|
30
38
|
for (const path of CRA_ENTRY_CANDIDATES) {
|
|
31
39
|
const source = io.readFile(path);
|
|
@@ -35,12 +43,12 @@ function craEntryOf(io) {
|
|
|
35
43
|
return null;
|
|
36
44
|
}
|
|
37
45
|
import { DEPS_TARGET, RETICLE_CONFIG_FILE, frameworkPackages, MCP_TARGET, buildPlan, StepStatus, } from './plan/plan.js';
|
|
38
|
-
import { claudeAvailableProbe,
|
|
46
|
+
import { claudeAvailableProbe, claudeHasReticle } from './register/mcp.js';
|
|
39
47
|
import { reticleDevLocation } from './patch/next-patch.js';
|
|
40
48
|
import { scanTestids, storeHints, scanStores } from './detect/capabilities.js';
|
|
41
49
|
import { CLAUDE_PROJECT_CONFIG, CURSOR_PROJECT_MARKER } from './register/mcp-clients.js';
|
|
42
50
|
import { deriveProjectId, packageName } from './project/project-id.js';
|
|
43
|
-
import { VITE_DEV_MODULE_PATH, ELECTRON_VITE_DEV_MODULE_PATH,
|
|
51
|
+
import { VITE_DEV_MODULE_PATH, ELECTRON_VITE_DEV_MODULE_PATH, nuxtPluginPath, } from './patch/snippets.js';
|
|
44
52
|
import { NUXT_CONFIG_CANDIDATES } from './patch/nuxt-patch.js';
|
|
45
53
|
import { CLAUDE_COMMAND_PATH, CURSOR_COMMAND_PATH } from './register/slash-command.js';
|
|
46
54
|
import { RETICLE_VERSION } from './version.js';
|
|
@@ -219,6 +227,17 @@ function agentRootOf(options) {
|
|
|
219
227
|
const root = options.agentRoot;
|
|
220
228
|
return root === undefined || root === options.cwd ? undefined : root;
|
|
221
229
|
}
|
|
230
|
+
/** What the Angular steps read: the workspace file, the entry it names, and our proxy module. */
|
|
231
|
+
function angularInputs(io) {
|
|
232
|
+
const workspace = io.readFile(ANGULAR_WORKSPACE_FILE);
|
|
233
|
+
const entryPath = angularEntry(workspace);
|
|
234
|
+
const entry = io.readFile(entryPath);
|
|
235
|
+
return {
|
|
236
|
+
angularWorkspace: null === workspace ? null : { path: ANGULAR_WORKSPACE_FILE, source: workspace },
|
|
237
|
+
angularEntry: null === entry ? null : { path: entryPath, source: entry },
|
|
238
|
+
angularProxySource: io.readFile(ANGULAR_PROXY_PATH),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
222
241
|
function gatherPlanInput(options, io, pkg) {
|
|
223
242
|
// Stable identity derived from the app's package.json name + root, so it survives port changes.
|
|
224
243
|
const projectId = deriveProjectId(packageName(pkg), options.cwd);
|
|
@@ -234,7 +253,11 @@ function gatherPlanInput(options, io, pkg) {
|
|
|
234
253
|
nodeModulesMarkers,
|
|
235
254
|
};
|
|
236
255
|
const detection = detect(detectInput);
|
|
237
|
-
|
|
256
|
+
// Forge has no `vite.config.*`; its renderer config IS the Vite config the plugin belongs in, and
|
|
257
|
+
// handing it over under that name is what lets the ordinary Vite patcher do the work.
|
|
258
|
+
const vitePath = firstPresent(rootFiles, Framework.ELECTRON_FORGE === detection.framework
|
|
259
|
+
? FORGE_RENDERER_CONFIGS
|
|
260
|
+
: VITE_CONFIG_CANDIDATES);
|
|
238
261
|
const viteSource = null === vitePath ? null : io.readFile(vitePath);
|
|
239
262
|
const viteConfig = vitePath !== null && viteSource !== null ? { path: vitePath, source: viteSource } : null;
|
|
240
263
|
const electronVitePath = firstPresent(rootFiles, ELECTRON_VITE_CONFIG_CANDIDATES);
|
|
@@ -248,8 +271,8 @@ function gatherPlanInput(options, io, pkg) {
|
|
|
248
271
|
// global config file. Only probe when the MCP step is in play.
|
|
249
272
|
const availableProbe = claudeAvailableProbe();
|
|
250
273
|
const claudeCli = options.mcp ? io.probe(availableProbe.command, availableProbe.args) : false;
|
|
251
|
-
|
|
252
|
-
const mcpExists = claudeCli ? io
|
|
274
|
+
// Read from Claude's config, never `claude mcp get`: that launches the server — see claudeHasReticle.
|
|
275
|
+
const mcpExists = claudeCli ? claudeHasReticle(io, io.cwd()) : false;
|
|
253
276
|
// Every MCP client this machine shows evidence of. Conservative and one-directional: we write
|
|
254
277
|
// into a config a client ALREADY has, and never create ~/.gemini or ~/.codeium for somebody who
|
|
255
278
|
// does not use them.
|
|
@@ -308,7 +331,13 @@ function gatherPlanInput(options, io, pkg) {
|
|
|
308
331
|
// repo that containerises it — `frontend/` under a root `docker-compose.yml` is the shape this
|
|
309
332
|
// came from. Only the first match is reported; the note is the same whichever file found it.
|
|
310
333
|
...(() => {
|
|
311
|
-
|
|
334
|
+
// Only a file that RUNS the dev server: a production-only image is the common case, and
|
|
335
|
+
// React Router's template ships one. See runsDevServer.
|
|
336
|
+
const devScript = devScriptBody(pkg);
|
|
337
|
+
const marker = CONTAINER_MARKERS.find((name) => [name, join('..', name)].some((path) => {
|
|
338
|
+
const content = io.readFile(path);
|
|
339
|
+
return null !== content && runsDevServer(name, content, devScript);
|
|
340
|
+
}));
|
|
312
341
|
return marker === undefined ? {} : { containerMarker: marker };
|
|
313
342
|
})(),
|
|
314
343
|
viteConfig,
|
|
@@ -328,7 +357,7 @@ function gatherPlanInput(options, io, pkg) {
|
|
|
328
357
|
: null,
|
|
329
358
|
// Capabilities: scanned, never asked for. Bounded — a hint for the agent, not a repo index.
|
|
330
359
|
testids: scanTestids(sourceFiles.map((f) => f.source)),
|
|
331
|
-
storeHints: storeHints(dependencyNames(pkg)),
|
|
360
|
+
storeHints: storeHints(dependencyNames(pkg), sourceFiles),
|
|
332
361
|
foundStores: scanStores(sourceFiles, dependencyNames(pkg)),
|
|
333
362
|
nextFoundStores: scanStores(sourceFiles, dependencyNames(pkg), dirname(devLocation.path)),
|
|
334
363
|
viteDevModuleExists: io.exists(detection.framework === Framework.ELECTRON_VITE
|
|
@@ -339,6 +368,7 @@ function gatherPlanInput(options, io, pkg) {
|
|
|
339
368
|
nextReticleDevExists: io.exists(devLocation.path),
|
|
340
369
|
nextReticleDevSource: io.readFile(devLocation.path),
|
|
341
370
|
svelteKitHooksExists: io.exists(SVELTEKIT_HOOKS),
|
|
371
|
+
svelteKitHooksSource: io.readFile(SVELTEKIT_HOOKS),
|
|
342
372
|
reactRouterEntryExists: io.exists(REACT_ROUTER_ENTRY),
|
|
343
373
|
// The SOURCE, so an entry the app already owns is ADDED TO rather than replaced — it is an
|
|
344
374
|
// override of React Router's default, and everything in it is load-bearing.
|
|
@@ -347,11 +377,27 @@ function gatherPlanInput(options, io, pkg) {
|
|
|
347
377
|
? { path: nuxtConfigPath, source: nuxtConfigSource }
|
|
348
378
|
: null,
|
|
349
379
|
nuxtHasAppDir,
|
|
380
|
+
viteMajor: installedViteMajor(io, detectInput.pkg),
|
|
350
381
|
nuxtPluginExists: io.exists(nuxtPluginPath(nuxtHasAppDir)),
|
|
351
|
-
|
|
382
|
+
nuxtPluginSource: io.readFile(nuxtPluginPath(nuxtHasAppDir)),
|
|
383
|
+
...(() => {
|
|
384
|
+
const root = TANSTACK_START_ROOT_CANDIDATES.find((file) => io.exists(file));
|
|
385
|
+
return root === undefined
|
|
386
|
+
? {}
|
|
387
|
+
: {
|
|
388
|
+
tanstackStartRoot: root,
|
|
389
|
+
tanstackStartRootSource: io.readFile(root),
|
|
390
|
+
tanstackStartConnectExists: io.exists(tanstackStartConnectPath(root)),
|
|
391
|
+
tanstackStartConnectSource: io.readFile(tanstackStartConnectPath(root)),
|
|
392
|
+
};
|
|
393
|
+
})(),
|
|
394
|
+
...(Framework.ANGULAR === detection.framework ? angularInputs(io) : {}),
|
|
395
|
+
...(Framework.HTML === detection.framework
|
|
396
|
+
? { htmlIndexSource: io.readFile(HTML_INDEX_PATH) }
|
|
397
|
+
: {}),
|
|
352
398
|
craEntry: craEntryOf(io),
|
|
353
399
|
craEnv: io.readFile(CRA_ENV_PATH),
|
|
354
|
-
pairingToken: io.host.pairingToken(),
|
|
400
|
+
pairingToken: options.dryRun ? DRY_RUN_PAIRING_TOKEN : io.host.pairingToken(),
|
|
355
401
|
installSource: io.host.installSource(),
|
|
356
402
|
reticleConfigExists: io.exists(RETICLE_CONFIG_FILE),
|
|
357
403
|
// The CONTENT, so a config that exists can be checked rather than trusted — a `"port"` set to
|
|
@@ -388,7 +434,7 @@ function gatherPlanInput(options, io, pkg) {
|
|
|
388
434
|
/** pnpm's workspace declaration, read when present — it is authoritative about where packages live. */
|
|
389
435
|
const SKIPPED_DETAIL = 'skipped — the dependency install above failed, and wiring the app to a package that is not ' +
|
|
390
436
|
'installed stops it booting. Run that install, then re-run `reticle init`.';
|
|
391
|
-
function report(plan, dryRun, failed, skipped, degraded, io, projectDir, agentRoot,
|
|
437
|
+
function report(plan, dryRun, failed, skipped, degraded, why, io, projectDir, agentRoot,
|
|
392
438
|
/** The project's own dev command, so the closing line names what a human would type. */
|
|
393
439
|
devCommand,
|
|
394
440
|
/**
|
|
@@ -412,7 +458,7 @@ continuesToRuntime = false) {
|
|
|
412
458
|
* On the REAL run only. In a dry run the reader has already found the flag.
|
|
413
459
|
*/
|
|
414
460
|
if (!dryRun) {
|
|
415
|
-
io.print(' --dry-run
|
|
461
|
+
io.print(' --dry-run previews · --app <dir> picks the app · --no-mcp skips agent registration');
|
|
416
462
|
}
|
|
417
463
|
// Every path below is printed RELATIVE, and until now nothing said what to. Reported from the
|
|
418
464
|
// field as "[✓] Reticle config → .reticle.json" followed by the file not being there: the app was
|
|
@@ -447,7 +493,7 @@ continuesToRuntime = false) {
|
|
|
447
493
|
const detail = skipped.has(s.target)
|
|
448
494
|
? SKIPPED_DETAIL
|
|
449
495
|
: downgraded && s.exec !== undefined
|
|
450
|
-
? `step failed — run manually: ${s.exec.fallback}`
|
|
496
|
+
? (why.get(s.target) ?? `step failed — run manually: ${s.exec.fallback}`)
|
|
451
497
|
: s.detail;
|
|
452
498
|
io.print(` [${STATUS_SYMBOL[status]}] ${s.title} → ${s.target}`);
|
|
453
499
|
if (status === StepStatus.APPLY)
|
|
@@ -520,7 +566,8 @@ function sdkPackagesPresent(framework, uiLibrary, io) {
|
|
|
520
566
|
return (packages.length > 0 &&
|
|
521
567
|
packages.every((p) => io.exists(`${NODE_MODULES_DIR}/${p}/${PACKAGE_JSON}`)));
|
|
522
568
|
}
|
|
523
|
-
function applyEffects(plan, io) {
|
|
569
|
+
function applyEffects(plan, io, packageManager) {
|
|
570
|
+
const why = new Map();
|
|
524
571
|
const failed = new Set();
|
|
525
572
|
const skipped = new Set();
|
|
526
573
|
const degraded = new Map();
|
|
@@ -594,8 +641,12 @@ function applyEffects(plan, io) {
|
|
|
594
641
|
failed.add(s.target);
|
|
595
642
|
// A failed install only blocks the wiring when the packages are genuinely ABSENT. See
|
|
596
643
|
// sdkPackagesPresent: the guard protects "the import resolves", not "our subprocess exited 0".
|
|
597
|
-
if (s.target === DEPS_TARGET)
|
|
644
|
+
if (s.target === DEPS_TARGET) {
|
|
598
645
|
installFailed = true;
|
|
646
|
+
const explained = explainInstallFailure(io, packageManager, exec, s.detail);
|
|
647
|
+
if (explained !== undefined)
|
|
648
|
+
why.set(s.target, explained);
|
|
649
|
+
}
|
|
599
650
|
}
|
|
600
651
|
}
|
|
601
652
|
// Where this project lives, remembered for a daemon that will be started somewhere else.
|
|
@@ -609,7 +660,7 @@ function applyEffects(plan, io) {
|
|
|
609
660
|
if (projectId !== undefined) {
|
|
610
661
|
rememberProjectOnDisk(io, projectId, io.cwd(), Date.now());
|
|
611
662
|
}
|
|
612
|
-
return { failed, skipped, degraded };
|
|
663
|
+
return { failed, skipped, degraded, why };
|
|
613
664
|
}
|
|
614
665
|
/**
|
|
615
666
|
* Which STEP failed, from the step targets — not from an error string, which would carry paths.
|
|
@@ -626,8 +677,20 @@ function classifyInitFailure(failed) {
|
|
|
626
677
|
return InitFailure.MCP_REGISTRATION;
|
|
627
678
|
return InitFailure.OTHER;
|
|
628
679
|
}
|
|
680
|
+
/**
|
|
681
|
+
* Where the agent stands when `init` runs inside a workspace package: the workspace root.
|
|
682
|
+
*
|
|
683
|
+
* The same answer `--app` from the root already gives (see enterApp), so both routes into one app
|
|
684
|
+
* leave the root in one state — a pointer config and the agent files an agent opened there reads.
|
|
685
|
+
*/
|
|
686
|
+
function withWorkspaceRoot(options, io) {
|
|
687
|
+
if (options.agentRoot !== undefined || true === options.redirected)
|
|
688
|
+
return options;
|
|
689
|
+
const root = enclosingWorkspaceRoot(options.cwd, io);
|
|
690
|
+
return root === undefined ? options : { ...options, agentRoot: root };
|
|
691
|
+
}
|
|
629
692
|
export function runInit(options, io) {
|
|
630
|
-
const result = runInitSteps(options, io);
|
|
693
|
+
const result = runInitSteps(withWorkspaceRoot(options, io), io);
|
|
631
694
|
// The ask goes here, not in report(): report() is only the success-shaped path, and the exits that
|
|
632
695
|
// matter most are the ones that never reach it — no package.json, an ambiguous workspace — where
|
|
633
696
|
// setup died before anything ran and the person holding the report has the least to go on. This
|
|
@@ -635,7 +698,10 @@ export function runInit(options, io) {
|
|
|
635
698
|
//
|
|
636
699
|
// `redirected` is what keeps it to ONE print: wiring an app in a monorepo re-enters runInit for the
|
|
637
700
|
// chosen directory, and the inner call must not ask again.
|
|
638
|
-
|
|
701
|
+
//
|
|
702
|
+
// And not when the runtime follows: it asks once it has finished, as the last thing printed,
|
|
703
|
+
// rather than here in the middle, before the dev server has even started.
|
|
704
|
+
if (true !== options.redirected && true !== options.continuesToRuntime) {
|
|
639
705
|
io.print('');
|
|
640
706
|
io.print(FEEDBACK_HINT);
|
|
641
707
|
}
|
|
@@ -673,7 +739,8 @@ function runInitSteps(options, io) {
|
|
|
673
739
|
io.print(`${PACKAGE_JSON} is not valid JSON (${manifest.error}). Fix it and re-run — init reads the ` +
|
|
674
740
|
'framework, the dev script and the package manager from it, and will not guess at any of ' +
|
|
675
741
|
'them from a file it cannot read.');
|
|
676
|
-
|
|
742
|
+
if (!options.dryRun)
|
|
743
|
+
io.host.reportOutcome({ ok: false, reason: InitFailure.MALFORMED_PACKAGE_JSON });
|
|
677
744
|
return { ok: false, applied: 0, manual: 0 };
|
|
678
745
|
}
|
|
679
746
|
const pkgRaw = manifest.pkg;
|
|
@@ -692,52 +759,8 @@ function runInitSteps(options, io) {
|
|
|
692
759
|
const redirectedEarly = redirectToWorkspaceApp(options, io, pkgRaw ?? {}, runInit);
|
|
693
760
|
if (redirectedEarly !== null)
|
|
694
761
|
return redirectedEarly;
|
|
695
|
-
if (null === pkgRaw)
|
|
696
|
-
|
|
697
|
-
// Asked only when Streamlit already said no, so the two can never both claim the page.
|
|
698
|
-
const django = !streamlit &&
|
|
699
|
-
detectDjangoProject((file) => io.exists(join(options.cwd, file)), io.rootFiles());
|
|
700
|
-
io.print(
|
|
701
|
-
// Two genuinely different situations used to share one sentence: a JS developer in the wrong
|
|
702
|
-
// directory, and a project that is not JavaScript at all. The second reads the old wording as
|
|
703
|
-
// a path problem and goes looking for a directory that cannot exist — reported from a
|
|
704
|
-
// Streamlit app, where the search continued into hunting for a browser bundle to inject by
|
|
705
|
-
// hand before the real answer surfaced.
|
|
706
|
-
streamlit
|
|
707
|
-
? streamlitSetupMessage()
|
|
708
|
-
: django
|
|
709
|
-
? djangoSetupMessage()
|
|
710
|
-
: noPackageJsonMessage((file) => io.exists(join(options.cwd, file))));
|
|
711
|
-
// The message says "add the snippet below". Print the snippet, or the message is the same
|
|
712
|
-
// broken promise in the other direction. `connectArg` carries the port; there is no projectId
|
|
713
|
-
// to bake, because a projectId is derived from the package.json that does not exist here.
|
|
714
|
-
// Scope the project on disk before printing anything, so the daemon can serve this page.
|
|
715
|
-
//
|
|
716
|
-
// This path used to print and exit, leaving no `.reticle.json` at all — and the reporter who
|
|
717
|
-
// asked for the Django path had to hand-write one alongside the middleware. Without it the
|
|
718
|
-
// daemon has no config in its own directory, refuses the page's dial, and every downstream
|
|
719
|
-
// symptom points somewhere other than the cause (see #685). The projectId derivation already
|
|
720
|
-
// handles the no-package case: it falls back to the root folder name, fingerprinted by the
|
|
721
|
-
// absolute path so two checkouts stay distinct.
|
|
722
|
-
//
|
|
723
|
-
// Written only when absent. A config a user or an earlier run already placed here is theirs.
|
|
724
|
-
const nonJsProjectId = deriveProjectId(undefined, io.cwd());
|
|
725
|
-
if (!io.exists(RETICLE_CONFIG_FILE)) {
|
|
726
|
-
io.writeFile(RETICLE_CONFIG_FILE, reticleConfigContent(Framework.HTML, options.port, nonJsProjectId, io.host.installSource()));
|
|
727
|
-
rememberProjectOnDisk(io, nonJsProjectId, io.cwd(), Date.now());
|
|
728
|
-
io.print(`Wrote ${RETICLE_CONFIG_FILE} (project "${nonJsProjectId}").`);
|
|
729
|
-
}
|
|
730
|
-
const connect = connectArgWithToken(options.port, nonJsProjectId, io.host.pairingToken());
|
|
731
|
-
io.print(streamlit
|
|
732
|
-
? streamlitPageSnippet(connect)
|
|
733
|
-
: django
|
|
734
|
-
? djangoMiddlewareSnippet(connect)
|
|
735
|
-
: staticPageSnippet(connect));
|
|
736
|
-
// The onboarding funnel had NO instrumentation, so a setup that died here was indistinguishable
|
|
737
|
-
// from someone who never ran the command — the two failure modes with the most different fixes.
|
|
738
|
-
io.host.reportOutcome({ ok: false, reason: InitFailure.NO_PACKAGE_JSON });
|
|
739
|
-
return { ok: false, applied: 0, manual: 0 };
|
|
740
|
-
}
|
|
762
|
+
if (null === pkgRaw)
|
|
763
|
+
return initWithoutPackageJson(options, io);
|
|
741
764
|
// Init is the flow a user experiences the wait of personally, and the fixture gate measures it at
|
|
742
765
|
// 1–6s per app with no explanation of the spread. These three spans split that number into detect
|
|
743
766
|
// (filesystem probing), plan (pure), and apply (writes + package-manager and CLI subprocesses).
|
|
@@ -747,23 +770,34 @@ function runInitSteps(options, io) {
|
|
|
747
770
|
// the app in frontend/ uses pnpm, and checking the file refused a scaffold the install gate proves
|
|
748
771
|
// must succeed. Both conditions make every later phase fail, and each arrives far from its cause
|
|
749
772
|
// when it is not checked here. See preflight.ts.
|
|
750
|
-
const
|
|
773
|
+
const preflightIo = {
|
|
751
774
|
cwd: () => io.cwd(),
|
|
752
775
|
canWrite: () => io.canWrite(),
|
|
753
776
|
probe: (command, args) => io.probe(command, args),
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
|
|
777
|
+
};
|
|
778
|
+
const resolved = preflight(preflightIo, planInput.detection.packageManager, {
|
|
779
|
+
alreadyServed: options.url !== undefined && '' !== options.url,
|
|
780
|
+
});
|
|
781
|
+
if (resolved.refusal !== undefined) {
|
|
782
|
+
io.print(resolved.refusal);
|
|
783
|
+
if (!options.dryRun)
|
|
784
|
+
io.host.reportOutcome({ ok: false, reason: InitFailure.PREFLIGHT });
|
|
757
785
|
return { ok: false, applied: 0, manual: 1 };
|
|
758
786
|
}
|
|
787
|
+
// The prefix that can actually run it on THIS machine — `pnpm`, or `corepack pnpm` where preflight
|
|
788
|
+
// just proved only corepack can. Threaded onto detection (never recomputed by `detect`, which is
|
|
789
|
+
// pure and never probes) so the install step, its retries and the dev command all spawn and print
|
|
790
|
+
// the same thing preflight just confirmed works, instead of a bare binary it already ruled out.
|
|
791
|
+
const packageManagerCommand = resolved.command;
|
|
792
|
+
planInput.detection = { ...planInput.detection, packageManagerCommand };
|
|
759
793
|
const plan = io.host.span('init.plan', {}, () => buildPlan(planInput));
|
|
760
794
|
const effects = options.dryRun
|
|
761
|
-
? { failed: new Set(), skipped: new Set(), degraded: new Map() }
|
|
762
|
-
: io.host.span('init.apply', { steps: plan.steps.length }, () => applyEffects(plan, io));
|
|
763
|
-
const { failed, skipped, degraded } = effects;
|
|
795
|
+
? { failed: new Set(), skipped: new Set(), degraded: new Map(), why: new Map() }
|
|
796
|
+
: io.host.span('init.apply', { steps: plan.steps.length }, () => applyEffects(plan, io, planInput.detection.packageManager));
|
|
797
|
+
const { failed, skipped, degraded, why } = effects;
|
|
764
798
|
// The project's own dev command, so the closing line names what a human would actually type.
|
|
765
|
-
const devCommand = devCommandFrom(pkgRaw,
|
|
766
|
-
const result = report(plan, options.dryRun, failed, skipped, degraded, io, options.cwd, agentRootOf(options), devCommand, true === options.continuesToRuntime);
|
|
799
|
+
const devCommand = devCommandFrom(pkgRaw, packageManagerCommand);
|
|
800
|
+
const result = report(plan, options.dryRun, failed, skipped, degraded, why, io, options.cwd, agentRootOf(options), devCommand, true === options.continuesToRuntime);
|
|
767
801
|
// A dry run is a preview, not an outcome — reporting it would inflate both success and failure.
|
|
768
802
|
if (options.dryRun)
|
|
769
803
|
return result;
|
|
@@ -831,8 +865,12 @@ function runInitSteps(options, io) {
|
|
|
831
865
|
stack: plan.framework,
|
|
832
866
|
...(result.ok ? {} : { reason: classifyInitFailure(failed) }),
|
|
833
867
|
});
|
|
868
|
+
// A failed dependency install skipped every wiring step, so there is no instrumented app to boot:
|
|
869
|
+
// without a context the runtime stops at this report, rather than starting the dev server and
|
|
870
|
+
// diagnosing an SDK that was never installed as a dev server that needs restarting.
|
|
871
|
+
const handover = failed.has(DEPS_TARGET) ? {} : { context };
|
|
834
872
|
if (true === options.deferOutcome)
|
|
835
|
-
return { ...result,
|
|
873
|
+
return { ...result, ...handover, outcome };
|
|
836
874
|
io.host.reportOutcome(outcome);
|
|
837
|
-
return { ...result,
|
|
875
|
+
return { ...result, ...handover };
|
|
838
876
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reticlehq/init",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.0",
|
|
4
4
|
"description": "Reticle project scaffolder — detects the framework, writes the config, patches the build and registers the MCP server. The build-time half of `reticle init`.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"./package.json": "./package.json"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@reticlehq/core": "3.
|
|
16
|
+
"@reticlehq/core": "3.5.0"
|
|
17
17
|
},
|
|
18
18
|
"license": "FSL-1.1-ALv2",
|
|
19
19
|
"author": "Reticle contributors",
|
|
@@ -27,8 +27,7 @@
|
|
|
27
27
|
"files": [
|
|
28
28
|
"dist",
|
|
29
29
|
"README.md",
|
|
30
|
-
"LICENSE"
|
|
31
|
-
"NOTICE"
|
|
30
|
+
"LICENSE"
|
|
32
31
|
],
|
|
33
32
|
"sideEffects": false,
|
|
34
33
|
"keywords": [
|