@reticlehq/init 2.14.0 → 3.2.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.d.ts → detect/detect.d.ts} +1 -1
- package/dist/{detect.js → detect/detect.js} +6 -2
- package/dist/{dev-server-ports.js → detect/dev-server-ports.js} +8 -0
- package/dist/{existing-config.d.ts → detect/existing-config.d.ts} +2 -4
- package/dist/{existing-config.js → detect/existing-config.js} +2 -4
- package/dist/{lockfiles.d.ts → detect/lockfiles.d.ts} +1 -1
- package/dist/{lockfiles.js → detect/lockfiles.js} +1 -2
- package/dist/{workspace-apps.d.ts → detect/workspace-apps.d.ts} +3 -5
- package/dist/{workspace-apps.js → detect/workspace-apps.js} +2 -4
- package/dist/{workspace-redirect.d.ts → detect/workspace-redirect.d.ts} +3 -5
- package/dist/{workspace-redirect.js → detect/workspace-redirect.js} +8 -12
- package/dist/{closing-hint.d.ts → diagnose/closing-hint.d.ts} +3 -4
- package/dist/{closing-hint.js → diagnose/closing-hint.js} +14 -19
- package/dist/diagnose/containerised-dev-server.d.ts +32 -0
- package/dist/diagnose/containerised-dev-server.js +56 -0
- package/dist/{csp-check.d.ts → diagnose/csp-check.d.ts} +2 -2
- package/dist/{csp-check.js → diagnose/csp-check.js} +2 -2
- package/dist/{desktop-doctor.js → diagnose/desktop-doctor.js} +12 -6
- package/dist/{init-failure.d.ts → diagnose/init-failure.d.ts} +1 -2
- package/dist/{init-failure.js → diagnose/init-failure.js} +1 -2
- package/dist/diagnose/install-hint.d.ts +8 -0
- package/dist/{install-hint.js → diagnose/install-hint.js} +3 -4
- package/dist/{install-retries.d.ts → diagnose/install-retries.d.ts} +3 -4
- package/dist/{install-retries.js → diagnose/install-retries.js} +6 -8
- package/dist/host.d.ts +14 -1
- package/dist/host.js +3 -0
- package/dist/index.d.ts +32 -12
- package/dist/index.js +36 -12
- package/dist/node-io.d.ts +12 -1
- package/dist/node-io.js +53 -25
- package/dist/{astro-patch.d.ts → patch/astro-patch.d.ts} +1 -1
- package/dist/{astro-patch.js → patch/astro-patch.js} +1 -1
- package/dist/{cra.js → patch/cra.js} +1 -1
- package/dist/{electron-patch.js → patch/electron-patch.js} +1 -1
- package/dist/{next-patch.d.ts → patch/next-patch.d.ts} +2 -2
- package/dist/{next-patch.js → patch/next-patch.js} +2 -2
- package/dist/{nuxt-patch.js → patch/nuxt-patch.js} +1 -1
- package/dist/{snippets.d.ts → patch/snippets.d.ts} +11 -21
- package/dist/{snippets.js → patch/snippets.js} +20 -29
- package/dist/{tanstack-start.d.ts → patch/tanstack-start.d.ts} +2 -3
- package/dist/{tanstack-start.js → patch/tanstack-start.js} +3 -4
- package/dist/{vite-config.js → patch/vite-config.js} +15 -13
- package/dist/{connect-steps.d.ts → plan/connect-steps.d.ts} +3 -3
- package/dist/{connect-steps.js → plan/connect-steps.js} +4 -4
- package/dist/{framework-adapter.d.ts → plan/framework-adapter.d.ts} +11 -2
- package/dist/{framework-adapter.js → plan/framework-adapter.js} +12 -1
- package/dist/plan/notices.d.ts +23 -0
- package/dist/plan/notices.js +94 -0
- package/dist/{plan-electron-vite.d.ts → plan/plan-electron-vite.d.ts} +3 -4
- package/dist/{plan-electron-vite.js → plan/plan-electron-vite.js} +7 -8
- package/dist/{plan-framework.d.ts → plan/plan-framework.d.ts} +4 -13
- package/dist/{plan-framework.js → plan/plan-framework.js} +14 -34
- package/dist/{plan.d.ts → plan/plan-types.d.ts} +14 -17
- package/dist/plan/plan-types.js +24 -0
- package/dist/plan/plan.d.ts +20 -0
- package/dist/{plan.js → plan/plan.js} +27 -88
- package/dist/{agent-rules.d.ts → project/agent-rules.d.ts} +2 -2
- package/dist/{agent-rules.js → project/agent-rules.js} +17 -21
- package/dist/project/install-source-config.d.ts +14 -0
- package/dist/{install-source-config.js → project/install-source-config.js} +3 -7
- package/dist/project/remember-project.js +74 -0
- package/dist/register/detect-clients.d.ts +23 -0
- package/dist/register/detect-clients.js +28 -0
- package/dist/{mcp-clients.js → register/mcp-clients.js} +4 -8
- package/dist/{mcp-registered.d.ts → register/mcp-registered.d.ts} +1 -4
- package/dist/{mcp-registered.js → register/mcp-registered.js} +1 -4
- package/dist/{mcp.d.ts → register/mcp.d.ts} +5 -8
- package/dist/{mcp.js → register/mcp.js} +6 -9
- package/dist/register/slash-command.d.ts +27 -0
- package/dist/{slash-command.js → register/slash-command.js} +10 -10
- package/dist/register/windows-quote.d.ts +17 -0
- package/dist/register/windows-quote.js +49 -0
- package/dist/run-types.d.ts +131 -0
- package/dist/run-types.js +8 -0
- package/dist/run.d.ts +3 -120
- package/dist/run.js +111 -56
- package/package.json +5 -5
- package/dist/install-hint.d.ts +0 -9
- package/dist/install-source-config.d.ts +0 -18
- package/dist/remember-project.js +0 -40
- package/dist/slash-command.d.ts +0 -27
- package/dist/windows-quote.d.ts +0 -38
- package/dist/windows-quote.js +0 -70
- /package/dist/{capabilities.d.ts → detect/capabilities.d.ts} +0 -0
- /package/dist/{capabilities.js → detect/capabilities.js} +0 -0
- /package/dist/{app-choice.d.ts → detect/declared/app-choice.d.ts} +0 -0
- /package/dist/{app-choice.js → detect/declared/app-choice.js} +0 -0
- /package/dist/{init-opt-out.d.ts → detect/declared/init-opt-out.d.ts} +0 -0
- /package/dist/{init-opt-out.js → detect/declared/init-opt-out.js} +0 -0
- /package/dist/{dev-script.d.ts → detect/dev-script.d.ts} +0 -0
- /package/dist/{dev-script.js → detect/dev-script.js} +0 -0
- /package/dist/{dev-server-ports.d.ts → detect/dev-server-ports.d.ts} +0 -0
- /package/dist/{non-js-project.d.ts → detect/non-js-project.d.ts} +0 -0
- /package/dist/{non-js-project.js → detect/non-js-project.js} +0 -0
- /package/dist/{platform.d.ts → detect/platform.d.ts} +0 -0
- /package/dist/{platform.js → detect/platform.js} +0 -0
- /package/dist/{csp-doctor.d.ts → diagnose/csp-doctor.d.ts} +0 -0
- /package/dist/{csp-doctor.js → diagnose/csp-doctor.js} +0 -0
- /package/dist/{desktop-doctor.d.ts → diagnose/desktop-doctor.d.ts} +0 -0
- /package/dist/{astro-host.d.ts → patch/astro-host.d.ts} +0 -0
- /package/dist/{astro-host.js → patch/astro-host.js} +0 -0
- /package/dist/{brace-scan.d.ts → patch/brace-scan.d.ts} +0 -0
- /package/dist/{brace-scan.js → patch/brace-scan.js} +0 -0
- /package/dist/{cra.d.ts → patch/cra.d.ts} +0 -0
- /package/dist/{electron-patch.d.ts → patch/electron-patch.d.ts} +0 -0
- /package/dist/{electron-vite-patch.d.ts → patch/electron-vite-patch.d.ts} +0 -0
- /package/dist/{electron-vite-patch.js → patch/electron-vite-patch.js} +0 -0
- /package/dist/{format-generated.d.ts → patch/format-generated.d.ts} +0 -0
- /package/dist/{format-generated.js → patch/format-generated.js} +0 -0
- /package/dist/{nuxt-patch.d.ts → patch/nuxt-patch.d.ts} +0 -0
- /package/dist/{patch-kind.d.ts → patch/patch-kind.d.ts} +0 -0
- /package/dist/{patch-kind.js → patch/patch-kind.js} +0 -0
- /package/dist/{vite-config.d.ts → patch/vite-config.d.ts} +0 -0
- /package/dist/{vite-owning-config.d.ts → patch/vite-owning-config.d.ts} +0 -0
- /package/dist/{vite-owning-config.js → patch/vite-owning-config.js} +0 -0
- /package/dist/{preflight.d.ts → plan/preflight.d.ts} +0 -0
- /package/dist/{preflight.js → plan/preflight.js} +0 -0
- /package/dist/{project-id.d.ts → project/project-id.d.ts} +0 -0
- /package/dist/{project-id.js → project/project-id.js} +0 -0
- /package/dist/{refresh-rules.d.ts → project/refresh-rules.d.ts} +0 -0
- /package/dist/{refresh-rules.js → project/refresh-rules.js} +0 -0
- /package/dist/{remember-project.d.ts → project/remember-project.d.ts} +0 -0
- /package/dist/{codex-toml.d.ts → register/codex-toml.d.ts} +0 -0
- /package/dist/{codex-toml.js → register/codex-toml.js} +0 -0
- /package/dist/{mcp-clients.d.ts → register/mcp-clients.d.ts} +0 -0
|
@@ -40,7 +40,7 @@ export declare const Framework: {
|
|
|
40
40
|
* TanStack Start SSRs `<html>` from `src/routes/__root.tsx` and never sends Vite's index.html, so
|
|
41
41
|
* the plugin's `transformIndexHtml` injection never fires. It used to fall through to
|
|
42
42
|
* `Framework.VITE`, where `init` wired the plugin, reported every step green ("also injects
|
|
43
|
-
* connect()"), and produced zero sessions — confirmed in the field as
|
|
43
|
+
* connect()"), and produced zero sessions — confirmed in the field as many minutes of "still
|
|
44
44
|
* verifying" against a daemon showing none (#773).
|
|
45
45
|
*
|
|
46
46
|
* Keyed on `@tanstack/react-start` or the older `@tanstack/start`, never on
|
|
@@ -40,7 +40,7 @@ export const Framework = {
|
|
|
40
40
|
* TanStack Start SSRs `<html>` from `src/routes/__root.tsx` and never sends Vite's index.html, so
|
|
41
41
|
* the plugin's `transformIndexHtml` injection never fires. It used to fall through to
|
|
42
42
|
* `Framework.VITE`, where `init` wired the plugin, reported every step green ("also injects
|
|
43
|
-
* connect()"), and produced zero sessions — confirmed in the field as
|
|
43
|
+
* connect()"), and produced zero sessions — confirmed in the field as many minutes of "still
|
|
44
44
|
* verifying" against a daemon showing none (#773).
|
|
45
45
|
*
|
|
46
46
|
* Keyed on `@tanstack/react-start` or the older `@tanstack/start`, never on
|
|
@@ -301,7 +301,11 @@ const INSTALL_ARGS = {
|
|
|
301
301
|
* hard failure, which is the opposite of the problem being fixed.
|
|
302
302
|
*/
|
|
303
303
|
const QUIET_INSTALL_ARGS = {
|
|
304
|
-
|
|
304
|
+
// `--no-update-notifier` joined the other two for the same reason and from the same measurement:
|
|
305
|
+
// a re-capture of a pristine Vite first run spent five of its lines on an `npm notice` block
|
|
306
|
+
// announcing a newer npm, its changelog url and the command to install it. Advice about npm,
|
|
307
|
+
// printed in the middle of somebody wiring up a different tool.
|
|
308
|
+
[PackageManager.NPM]: ['--no-audit', '--no-fund', '--no-update-notifier'],
|
|
305
309
|
};
|
|
306
310
|
/** Build a dev-dependency install command for one or more packages (e.g. the kit + its build plugin). */
|
|
307
311
|
export function installCommandParts(pm, pkgs,
|
|
@@ -35,6 +35,14 @@ export const DEV_SERVER_PORTS = new Set([
|
|
|
35
35
|
5175, // Vite, third instance
|
|
36
36
|
7860, // Gradio
|
|
37
37
|
8501, // Streamlit
|
|
38
|
+
// The same lesson again, from a run against Reticle's own bench-app: the scan reported "nothing is
|
|
39
|
+
// listening" while the app was serving on 4310. Absence is stated here as a fact, so a port left
|
|
40
|
+
// out is not a gap in the answer, it is a wrong answer delivered confidently.
|
|
41
|
+
1234, // Parcel
|
|
42
|
+
4000, // the common Node/GraphQL dev default
|
|
43
|
+
4310, // Reticle's own bench-app, the fixture its agents drive
|
|
44
|
+
6006, // Storybook
|
|
45
|
+
19006, // Expo web
|
|
38
46
|
]);
|
|
39
47
|
export function isLikelyDevServerPort(port) {
|
|
40
48
|
return DEV_SERVER_PORTS.has(port);
|
|
@@ -3,10 +3,8 @@ export declare const RETICLE_CONFIG_FILE = ".reticle.json";
|
|
|
3
3
|
/**
|
|
4
4
|
* Reading a `.reticle.json` that is ALREADY on disk.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* plans or the shape of a report, which is what made them the cohesive piece to lift rather than
|
|
9
|
-
* the nearest thousand-line boundary.
|
|
6
|
+
* These two are one job -- looking at a config somebody else wrote and deciding what it tells us --
|
|
7
|
+
* and neither knows anything about steps, plans or the shape of a report.
|
|
10
8
|
*/
|
|
11
9
|
/**
|
|
12
10
|
* What is wrong with the config that is already there, if anything.
|
|
@@ -4,10 +4,8 @@ export const RETICLE_CONFIG_FILE = '.reticle.json';
|
|
|
4
4
|
/**
|
|
5
5
|
* Reading a `.reticle.json` that is ALREADY on disk.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* plans or the shape of a report, which is what made them the cohesive piece to lift rather than
|
|
10
|
-
* the nearest thousand-line boundary.
|
|
7
|
+
* These two are one job -- looking at a config somebody else wrote and deciding what it tells us --
|
|
8
|
+
* and neither knows anything about steps, plans or the shape of a report.
|
|
11
9
|
*/
|
|
12
10
|
/**
|
|
13
11
|
* What is wrong with the config that is already there, if anything.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { InitIo } from '
|
|
1
|
+
import type { InitIo } from '../run-types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Resolve the lockfiles set used to pick the package manager. A lockfile in the project root wins;
|
|
4
4
|
* otherwise we walk UP the directory tree (monorepos keep the lockfile at the workspace root, not in
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Which lockfile decides the package manager `init` will tell the user to run.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* question with its own test block, and `run.ts` is the orchestration around it.
|
|
4
|
+
* One self-contained question with its own test block; `run.ts` is the orchestration around it.
|
|
6
5
|
*/
|
|
7
6
|
import { dirname, join } from 'node:path';
|
|
8
7
|
import { namesAPackageManager } from './detect.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { InitIo } from '
|
|
1
|
+
import type { InitIo } from '../run-types.js';
|
|
2
2
|
/**
|
|
3
3
|
* The file names that say what a directory IS, shared by everything that has to ask.
|
|
4
4
|
*
|
|
@@ -21,10 +21,8 @@ export declare function workspaceParents(sources: WorkspaceSources): string[];
|
|
|
21
21
|
/**
|
|
22
22
|
* Which directories under this workspace are runnable apps.
|
|
23
23
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* "squats in init/ for historical reasons" — it is a general-purpose question about a repository's
|
|
27
|
-
* shape, next to `workspaceParents`, which answers the other half of it.
|
|
24
|
+
* A general-purpose question about a repository's shape, so it lives next to `workspaceParents`,
|
|
25
|
+
* which answers the other half of it.
|
|
28
26
|
*/
|
|
29
27
|
/** Deps that mark a directory as a runnable web app even when it has no bundler config file. */
|
|
30
28
|
export declare const APP_DEPS: readonly ["next", "vite", "electron-vite"];
|
|
@@ -96,10 +96,8 @@ export function workspaceParents(sources) {
|
|
|
96
96
|
/**
|
|
97
97
|
* Which directories under this workspace are runnable apps.
|
|
98
98
|
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* "squats in init/ for historical reasons" — it is a general-purpose question about a repository's
|
|
102
|
-
* shape, next to `workspaceParents`, which answers the other half of it.
|
|
99
|
+
* A general-purpose question about a repository's shape, so it lives next to `workspaceParents`,
|
|
100
|
+
* which answers the other half of it.
|
|
103
101
|
*/
|
|
104
102
|
/** Deps that mark a directory as a runnable web app even when it has no bundler config file. */
|
|
105
103
|
export const APP_DEPS = ['next', 'vite', 'electron-vite'];
|
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Workspace redirect: when `init` is run at a monorepo ROOT, wire the app rather than the root.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* file that answers a single, separable question ("is the app somewhere else, and where?"), so it
|
|
7
|
-
* is the seam that costs nothing to move.
|
|
4
|
+
* Its own module because it answers a single, separable question: is the app somewhere else, and
|
|
5
|
+
* where?
|
|
8
6
|
*
|
|
9
7
|
* `runInit` arrives as a parameter rather than an import: re-entering init is the whole point of a
|
|
10
8
|
* redirect, and taking it as an argument keeps this module a leaf instead of forming a cycle with
|
|
11
9
|
* the file it was cut from.
|
|
12
10
|
*/
|
|
13
|
-
import type { InitIo, InitOptions, InitResult } from '
|
|
11
|
+
import type { InitIo, InitOptions, InitResult } from '../run-types.js';
|
|
14
12
|
/** Re-enter `init`, scoped to one directory of the workspace. */
|
|
15
13
|
export type RunInit = (options: InitOptions, io: InitIo) => InitResult;
|
|
16
14
|
/**
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Workspace redirect: when `init` is run at a monorepo ROOT, wire the app rather than the root.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* file that answers a single, separable question ("is the app somewhere else, and where?"), so it
|
|
7
|
-
* is the seam that costs nothing to move.
|
|
4
|
+
* Its own module because it answers a single, separable question: is the app somewhere else, and
|
|
5
|
+
* where?
|
|
8
6
|
*
|
|
9
7
|
* `runInit` arrives as a parameter rather than an import: re-entering init is the whole point of a
|
|
10
8
|
* redirect, and taking it as an argument keeps this module a leaf instead of forming a cycle with
|
|
@@ -13,7 +11,7 @@
|
|
|
13
11
|
import { join } from 'node:path';
|
|
14
12
|
import { detect, Framework } from './detect.js';
|
|
15
13
|
import { findWorkspaceApps, PACKAGE_JSON } from './workspace-apps.js';
|
|
16
|
-
import { chooseWorkspaceApp, withoutTrailingSlashes } from './app-choice.js';
|
|
14
|
+
import { chooseWorkspaceApp, withoutTrailingSlashes } from './declared/app-choice.js';
|
|
17
15
|
const AMBIGUOUS_HEADER = 'Several apps found in this workspace. Re-run `reticle init` inside the one you want:';
|
|
18
16
|
/**
|
|
19
17
|
* When the current directory is a workspace root with no app of its own, wire the app instead of the
|
|
@@ -33,13 +31,11 @@ export function redirectToWorkspaceApp(options, io, pkg, runInit) {
|
|
|
33
31
|
// `--app` is an INSTRUCTION, and it is read before the guess below. The guess answers "where is
|
|
34
32
|
// the app?" for somebody who did not say; when somebody said, there is nothing left to infer.
|
|
35
33
|
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
// repository root that has no `src/`, left `packages/player` untouched — and reported three ✓ and
|
|
42
|
-
// one ⚠. The one flag documented for this shape wired the wrong directory and said it worked.
|
|
34
|
+
// Read BEFORE the check underneath, which returns early for any directory that looks like an app —
|
|
35
|
+
// which a JS monorepo ROOT does, because shared tooling puts `vite` in its devDependencies. Read
|
|
36
|
+
// after, `reticle init --app packages/player` silently ignored the flag on a pnpm+turbo monorepo:
|
|
37
|
+
// it installed the SDK into the root's package.json, wrote `.reticle.json` and `src/reticle-dev.ts`
|
|
38
|
+
// into a repository root that has no `src/`, left the named app untouched, and reported success.
|
|
43
39
|
//
|
|
44
40
|
// Existence is the test, not membership of the discovered list: discovery scans conventional
|
|
45
41
|
// directories, and somebody who names a path knows their own layout better than the scan does.
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The last thing `init` prints, and the only part of it a user is asked to act on.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* this
|
|
6
|
-
* what to say afterwards. The two failure modes are different too — a wrong write breaks an
|
|
4
|
+
* Its own file because it is its own concern: everything else in `run.ts` decides what to write,
|
|
5
|
+
* and this decides what to say afterwards. The failure modes differ too — a wrong write breaks an
|
|
7
6
|
* install, a wrong sentence loses a user quietly.
|
|
8
7
|
*/
|
|
9
|
-
import { StepStatus } from '
|
|
8
|
+
import { StepStatus } from '../plan/plan.js';
|
|
10
9
|
/**
|
|
11
10
|
* The standing ask, printed at the end of every `init`.
|
|
12
11
|
*
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The last thing `init` prints, and the only part of it a user is asked to act on.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* this
|
|
6
|
-
* what to say afterwards. The two failure modes are different too — a wrong write breaks an
|
|
4
|
+
* Its own file because it is its own concern: everything else in `run.ts` decides what to write,
|
|
5
|
+
* and this decides what to say afterwards. The failure modes differ too — a wrong write breaks an
|
|
7
6
|
* install, a wrong sentence loses a user quietly.
|
|
8
7
|
*/
|
|
9
|
-
import { StepStatus } from '
|
|
10
|
-
import { wasMcpRegistered } from '
|
|
11
|
-
import { CLI } from '
|
|
8
|
+
import { StepStatus } from '../plan/plan.js';
|
|
9
|
+
import { wasMcpRegistered } from '../register/mcp-registered.js';
|
|
10
|
+
import { CLI } from '../project/agent-rules.js';
|
|
12
11
|
/**
|
|
13
12
|
* The standing ask, printed at the end of every `init`.
|
|
14
13
|
*
|
|
@@ -59,14 +58,13 @@ devCommand) {
|
|
|
59
58
|
// NAME THE COMMAND THAT PROVES IT, not one that merely asks.
|
|
60
59
|
//
|
|
61
60
|
// `init` writes files and stops; the install is not finished until an app carrying the SDK has
|
|
62
|
-
// actually dialled the daemon, and nothing here confirmed that
|
|
63
|
-
//
|
|
64
|
-
// weeks with nothing to drive — so this is the last instruction most of them read.
|
|
61
|
+
// actually dialled the daemon, and nothing here confirmed that — so this is the last instruction
|
|
62
|
+
// a user reads before the half that can still fail.
|
|
65
63
|
//
|
|
66
64
|
// "Ask your agent: List Reticle sessions" asks a question whose failure is a dead end. `reticle
|
|
67
|
-
// status` ANSWERS it:
|
|
68
|
-
// running
|
|
69
|
-
//
|
|
65
|
+
// status` ANSWERS it: it reports the session, or says why there is none — no app running, an app
|
|
66
|
+
// running that never dialled us, a tab that closed — with the fix for each. It works BEFORE the
|
|
67
|
+
// restart, so it goes before it.
|
|
70
68
|
//
|
|
71
69
|
// What comes after the restart, named as a JOB rather than as a capability.
|
|
72
70
|
//
|
|
@@ -89,13 +87,10 @@ devCommand) {
|
|
|
89
87
|
return `${dev}\n${demo}\n${prove}`;
|
|
90
88
|
// ALREADY: the tools are reachable RIGHT NOW, and this is the branch that used to lie.
|
|
91
89
|
//
|
|
92
|
-
// `wasMcpRegistered` is true for both APPLY and ALREADY, which is
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
// whatever its skill file said, and it describes the output in front of it, so it wins. The agent
|
|
97
|
-
// stops with the project wired and nothing driven, which is the exact failure the install path
|
|
98
|
-
// exists to prevent, caused by the install path itself.
|
|
90
|
+
// `wasMcpRegistered` is true for both APPLY and ALREADY, which is wrong here: the two cases have
|
|
91
|
+
// opposite answers to "must anybody restart". ALREADY used to take the APPLY text — "restart your
|
|
92
|
+
// agent, the tools only appear after that" — and an agent believes the output in front of it over
|
|
93
|
+
// whatever its skill file said, so it stops with the project wired and nothing driven.
|
|
99
94
|
if (mcpStatus === StepStatus.ALREADY) {
|
|
100
95
|
return (`${dev}\n` +
|
|
101
96
|
`${demo}\n` +
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The install shape Reticle assumes, stated once, for the projects that do not have it.
|
|
3
|
+
*
|
|
4
|
+
* Everything in `init` assumes the CLI and the dev server share a filesystem: the daemon writes
|
|
5
|
+
* `~/.reticle/pairing-token`, the build plugin reads it at config time and inlines it into the page,
|
|
6
|
+
* and the bridge checks that they match. When the dev server runs in a container that assumption is
|
|
7
|
+
* silently false — `$HOME` in there is the image's throwaway root, so the plugin finds no token,
|
|
8
|
+
* mints its own, and every page it serves is refused with `authentication failed`.
|
|
9
|
+
*
|
|
10
|
+
* Measured in the field: six minutes to diagnose, ending in a grep through the plugin's shipped
|
|
11
|
+
* `dist/` inside the container for an environment variable that appeared in no documentation. The
|
|
12
|
+
* daemon now says all this when it happens (see no-session-next-action). This says it BEFORE it
|
|
13
|
+
* happens, which is the difference between a note and a debugging session.
|
|
14
|
+
*
|
|
15
|
+
* It also names the rebuild. A containerised dev server usually installs its own `node_modules` into
|
|
16
|
+
* an anonymous volume, so `init` adding two dependencies is not picked up by restarting the
|
|
17
|
+
* container — it needs an image rebuild and a fresh volume. The same field install lost two and a
|
|
18
|
+
* half minutes discovering that through a failed build.
|
|
19
|
+
*
|
|
20
|
+
* DETECTION IS DELIBERATELY CHEAP AND OVER-EAGER. A Dockerfile or a compose file near the app is not
|
|
21
|
+
* proof the dev server runs in one — plenty of repos containerise only production. But this step
|
|
22
|
+
* asks for nothing and changes nothing, so a false positive costs one paragraph the reader skips,
|
|
23
|
+
* while a false negative costs what it cost in the field.
|
|
24
|
+
*/
|
|
25
|
+
/** Files whose presence means this repo builds a container image somewhere near the app. */
|
|
26
|
+
export declare const CONTAINER_MARKERS: readonly ["Dockerfile", "dockerfile", "docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml", ".devcontainer/devcontainer.json"];
|
|
27
|
+
export declare const CONTAINERISED_TITLE = "Containerised dev server";
|
|
28
|
+
/**
|
|
29
|
+
* The note itself. Both halves are things that go wrong at the same moment and look identical from
|
|
30
|
+
* the page — the SDK loads, the socket opens, no session appears — so they are told together.
|
|
31
|
+
*/
|
|
32
|
+
export declare function containerisedDevServerNote(marker: string): string;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The install shape Reticle assumes, stated once, for the projects that do not have it.
|
|
3
|
+
*
|
|
4
|
+
* Everything in `init` assumes the CLI and the dev server share a filesystem: the daemon writes
|
|
5
|
+
* `~/.reticle/pairing-token`, the build plugin reads it at config time and inlines it into the page,
|
|
6
|
+
* and the bridge checks that they match. When the dev server runs in a container that assumption is
|
|
7
|
+
* silently false — `$HOME` in there is the image's throwaway root, so the plugin finds no token,
|
|
8
|
+
* mints its own, and every page it serves is refused with `authentication failed`.
|
|
9
|
+
*
|
|
10
|
+
* Measured in the field: six minutes to diagnose, ending in a grep through the plugin's shipped
|
|
11
|
+
* `dist/` inside the container for an environment variable that appeared in no documentation. The
|
|
12
|
+
* daemon now says all this when it happens (see no-session-next-action). This says it BEFORE it
|
|
13
|
+
* happens, which is the difference between a note and a debugging session.
|
|
14
|
+
*
|
|
15
|
+
* It also names the rebuild. A containerised dev server usually installs its own `node_modules` into
|
|
16
|
+
* an anonymous volume, so `init` adding two dependencies is not picked up by restarting the
|
|
17
|
+
* container — it needs an image rebuild and a fresh volume. The same field install lost two and a
|
|
18
|
+
* half minutes discovering that through a failed build.
|
|
19
|
+
*
|
|
20
|
+
* DETECTION IS DELIBERATELY CHEAP AND OVER-EAGER. A Dockerfile or a compose file near the app is not
|
|
21
|
+
* proof the dev server runs in one — plenty of repos containerise only production. But this step
|
|
22
|
+
* asks for nothing and changes nothing, so a false positive costs one paragraph the reader skips,
|
|
23
|
+
* while a false negative costs what it cost in the field.
|
|
24
|
+
*/
|
|
25
|
+
import { ReticleEnv } from '@reticlehq/core';
|
|
26
|
+
/** Files whose presence means this repo builds a container image somewhere near the app. */
|
|
27
|
+
export const CONTAINER_MARKERS = [
|
|
28
|
+
'Dockerfile',
|
|
29
|
+
'dockerfile',
|
|
30
|
+
'docker-compose.yml',
|
|
31
|
+
'docker-compose.yaml',
|
|
32
|
+
'compose.yml',
|
|
33
|
+
'compose.yaml',
|
|
34
|
+
'.devcontainer/devcontainer.json',
|
|
35
|
+
];
|
|
36
|
+
export const CONTAINERISED_TITLE = 'Containerised dev server';
|
|
37
|
+
/**
|
|
38
|
+
* The note itself. Both halves are things that go wrong at the same moment and look identical from
|
|
39
|
+
* the page — the SDK loads, the socket opens, no session appears — so they are told together.
|
|
40
|
+
*/
|
|
41
|
+
export function containerisedDevServerNote(marker) {
|
|
42
|
+
return (`this project has a \`${marker}\`, so IF your dev server runs in a container two things about ` +
|
|
43
|
+
'this install are different, and both of them end in "no session" with nothing looking broken.\n' +
|
|
44
|
+
' 1. DEPENDENCIES. A container that installs its own node_modules will not see the two packages ' +
|
|
45
|
+
'added here by restarting — rebuild the image and renew the volume ' +
|
|
46
|
+
'(`docker compose build <service> && docker compose up -d --renew-anon-volumes <service>`).\n' +
|
|
47
|
+
' 2. THE PAIRING TOKEN. The daemon writes `~/.reticle/pairing-token` on the HOST; the build ' +
|
|
48
|
+
'plugin reads it from `$HOME` INSIDE the container, does not find it, and mints a different one — ' +
|
|
49
|
+
'which the bridge then refuses. Mount the host file read-only and point the plugin at it:\n' +
|
|
50
|
+
' environment:\n' +
|
|
51
|
+
` ${ReticleEnv.PAIRING_TOKEN_DIR}: /reticle-state\n` +
|
|
52
|
+
' volumes:\n' +
|
|
53
|
+
' - ~/.reticle/pairing-token:/reticle-state/pairing-token:ro\n' +
|
|
54
|
+
' The token is inlined when the dev server resolves its config, so restart it afterwards. ' +
|
|
55
|
+
'Run the Reticle daemon at least once first, or Docker creates a DIRECTORY at that host path.');
|
|
56
|
+
}
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* the dial URL was correct, and the app never connected because a strict `connect-src` excluded
|
|
6
6
|
* `ws://localhost:<port>`. The browser blocks the WebSocket and says so in its own console, which
|
|
7
7
|
* nothing on the Reticle side reads — so `status` and `doctor` reported perfect health at an app
|
|
8
|
-
* that could not connect at all
|
|
9
|
-
*
|
|
8
|
+
* that could not connect at all. A setup step that exits 0 while leaving the user broken is the
|
|
9
|
+
* failure mode this file exists to prevent.
|
|
10
10
|
*
|
|
11
11
|
* A TEXT SCAN, deliberately. `headers()` is a function; `init` cannot execute a user's Next config
|
|
12
12
|
* without importing their whole app, and the thing worth finding is the policy string a developer
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* the dial URL was correct, and the app never connected because a strict `connect-src` excluded
|
|
6
6
|
* `ws://localhost:<port>`. The browser blocks the WebSocket and says so in its own console, which
|
|
7
7
|
* nothing on the Reticle side reads — so `status` and `doctor` reported perfect health at an app
|
|
8
|
-
* that could not connect at all
|
|
9
|
-
*
|
|
8
|
+
* that could not connect at all. A setup step that exits 0 while leaving the user broken is the
|
|
9
|
+
* failure mode this file exists to prevent.
|
|
10
10
|
*
|
|
11
11
|
* A TEXT SCAN, deliberately. `headers()` is a function; `init` cannot execute a user's Next config
|
|
12
12
|
* without importing their whole app, and the thing worth finding is the policy string a developer
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RETICLE_IPC_GLOBAL } from '@reticlehq/core';
|
|
1
|
+
import { RETICLE_IPC_GLOBAL, realmOf, realmOfProject } from '@reticlehq/core';
|
|
2
2
|
/**
|
|
3
3
|
* Setup RCA for desktop apps.
|
|
4
4
|
*
|
|
@@ -177,13 +177,19 @@ function diagnoseElectron(read, main) {
|
|
|
177
177
|
}
|
|
178
178
|
/** Is this project a desktop shell at all? Used to print a positive result instead of silence. */
|
|
179
179
|
export function isDesktopProject(read) {
|
|
180
|
-
|
|
181
|
-
|
|
180
|
+
// Asked of the realm table rather than answered here. Which file or dependency marks a realm is a
|
|
181
|
+
// fact about that realm, and the copy of it that lived in this function was the second one -- so a
|
|
182
|
+
// realm added to the table but not to this line would have been reported as an ordinary web app,
|
|
183
|
+
// which is the answer least likely to look wrong.
|
|
184
|
+
const found = realmOfProject((path) => read(path) !== undefined, (name) => dependenciesOf(read)[name] !== undefined);
|
|
185
|
+
return found !== undefined && realmOf(found).isDesktopShell;
|
|
186
|
+
}
|
|
187
|
+
/** Every dependency a project declares, both kinds, in one record. */
|
|
188
|
+
function dependenciesOf(read) {
|
|
182
189
|
const pkg = parseJson(read('package.json'));
|
|
183
190
|
if (pkg === undefined)
|
|
184
|
-
return
|
|
185
|
-
|
|
186
|
-
return deps['electron'] !== undefined;
|
|
191
|
+
return {};
|
|
192
|
+
return { ...record(pkg['dependencies']), ...record(pkg['devDependencies']) };
|
|
187
193
|
}
|
|
188
194
|
/** Which desktop shell (if any) this project is, and what is missing from its Reticle wiring. */
|
|
189
195
|
export function diagnoseDesktop(read, port) {
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
* Classified `reticle init` failures — our own vocabulary, never a raw error or a path.
|
|
3
3
|
*
|
|
4
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.
|
|
6
|
-
* `telemetry/init-telemetry.ts` re-exports it, so every existing consumer is unchanged.
|
|
5
|
+
* run, and `@reticlehq/init` must not depend on the daemon.
|
|
7
6
|
*/
|
|
8
7
|
export declare const InitFailure: {
|
|
9
8
|
/** Run outside a project root. The single most common first-run mistake. */
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
* Classified `reticle init` failures — our own vocabulary, never a raw error or a path.
|
|
3
3
|
*
|
|
4
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.
|
|
6
|
-
* `telemetry/init-telemetry.ts` re-exports it, so every existing consumer is unchanged.
|
|
5
|
+
* run, and `@reticlehq/init` must not depend on the daemon.
|
|
7
6
|
*/
|
|
8
7
|
export const InitFailure = {
|
|
9
8
|
/** Run outside a project root. The single most common first-run mistake. */
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What to say when the dependency install fails.
|
|
3
|
+
*
|
|
4
|
+
* A cohesive unit on its own: prose about one step's failure modes, and the only thing it needs is
|
|
5
|
+
* which package manager was used.
|
|
6
|
+
*/
|
|
7
|
+
import { PackageManager } from '../detect/detect.js';
|
|
8
|
+
export declare function installFailureHint(pm: PackageManager): string;
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* What to say when the dependency install fails.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* package manager was used.
|
|
4
|
+
* A cohesive unit on its own: prose about one step's failure modes, and the only thing it needs is
|
|
5
|
+
* which package manager was used.
|
|
7
6
|
*/
|
|
8
|
-
import { PackageManager } from '
|
|
7
|
+
import { PackageManager } from '../detect/detect.js';
|
|
9
8
|
/**
|
|
10
9
|
* Every package manager fetches, so every one of them can fail for a reason that is nothing to do
|
|
11
10
|
* with the project: offline, a proxy that blocks npmjs, a corporate mirror that is down.
|
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
* The weaker install attempts, in the order they are tried.
|
|
3
3
|
*
|
|
4
4
|
* Ordered by how much each one gives up, cheapest concession first, because the first that succeeds
|
|
5
|
-
* is the one that stands.
|
|
6
|
-
*
|
|
7
|
-
* plan.ts is at its file cap.
|
|
5
|
+
* is the one that stands. The ladder is its own subject: two unrelated install failures, each with
|
|
6
|
+
* a different cost and a different sentence to say afterwards.
|
|
8
7
|
*/
|
|
9
|
-
import { PackageManager } from '
|
|
8
|
+
import { PackageManager } from '../detect/detect.js';
|
|
10
9
|
export interface InstallRetry {
|
|
11
10
|
command: string;
|
|
12
11
|
args: string[];
|
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
* The weaker install attempts, in the order they are tried.
|
|
3
3
|
*
|
|
4
4
|
* Ordered by how much each one gives up, cheapest concession first, because the first that succeeds
|
|
5
|
-
* is the one that stands.
|
|
6
|
-
*
|
|
7
|
-
* plan.ts is at its file cap.
|
|
5
|
+
* is the one that stands. The ladder is its own subject: two unrelated install failures, each with
|
|
6
|
+
* a different cost and a different sentence to say afterwards.
|
|
8
7
|
*/
|
|
9
|
-
import { PackageManager, installCommandParts } from '
|
|
8
|
+
import { PackageManager, installCommandParts } from '../detect/detect.js';
|
|
10
9
|
const LEGACY_PEER_DEPS = '--legacy-peer-deps';
|
|
11
10
|
/**
|
|
12
11
|
* What the peer-conflict retry gives up, said out loud.
|
|
@@ -23,10 +22,9 @@ const LEGACY_PEER_NOTE = `the pinned install failed and succeeded with ${LEGACY_
|
|
|
23
22
|
/**
|
|
24
23
|
* Said out loud when the exact-version install failed and the unpinned one worked.
|
|
25
24
|
*
|
|
26
|
-
* It used to assert a cause it cannot know
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* config` command handed to a yarn 1 project that will never read it.
|
|
25
|
+
* It used to assert a cause it cannot know: every fixture app got the same sentence, blaming pnpm's
|
|
26
|
+
* `minimumReleaseAge`, when the actual cause was that the version did not exist yet — and the remedy
|
|
27
|
+
* offered was a `pnpm config` command handed to a yarn 1 project that will never read it.
|
|
30
28
|
*
|
|
31
29
|
* This note is built at PLAN time, before anything runs, and `io.exec` returns a bare boolean, so
|
|
32
30
|
* the apply layer has no failure text to hand back either. The honest move is therefore to report
|
package/dist/host.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { InitOutcome } from '@reticlehq/core/telemetry';
|
|
1
|
+
import type { InitOutcome, OnboardingStep } from '@reticlehq/core/telemetry';
|
|
2
2
|
/**
|
|
3
3
|
* Everything the scaffolder needs that only the daemon knows.
|
|
4
4
|
*
|
|
@@ -20,6 +20,19 @@ export interface InitHost {
|
|
|
20
20
|
span<T>(name: string, fields: Record<string, unknown>, fn: () => T): T;
|
|
21
21
|
/** Report how the run went. Best-effort — setup must never fail because a metric did. */
|
|
22
22
|
reportOutcome(outcome: InitOutcome): void;
|
|
23
|
+
/**
|
|
24
|
+
* Report ONE step of the setup funnel, as it happens.
|
|
25
|
+
*
|
|
26
|
+
* `reportOutcome` fires once, at the end, and says whether the whole run succeeded. That cannot
|
|
27
|
+
* answer the question the funnel exists for — WHERE people stop — because a run that dies at the
|
|
28
|
+
* MCP registration and one that dies before it ever looked for a package.json are the same single
|
|
29
|
+
* row. A step reported as it happens is the difference between "init failed" and "init failed at
|
|
30
|
+
* this step, on this stack, after this long".
|
|
31
|
+
*
|
|
32
|
+
* REQUIRED for the same reason `reportOutcome` is: an optional reporter is one somebody forgets to
|
|
33
|
+
* pass and nobody notices is missing, because telemetry fails silently.
|
|
34
|
+
*/
|
|
35
|
+
reportStep(step: OnboardingStep): void;
|
|
23
36
|
/**
|
|
24
37
|
* The daemon's pairing token, minted if nothing has written one yet.
|
|
25
38
|
*
|
package/dist/host.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -9,17 +9,37 @@
|
|
|
9
9
|
* ends up unable to move a file.
|
|
10
10
|
*/
|
|
11
11
|
export { runInit, resolveLockfiles, type InitIo, type InitOptions, type InitResult, } from './run.js';
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Exported for the ONE other place in this repo that spawns a package manager: `reticle update`.
|
|
14
|
+
*
|
|
15
|
+
* It is a pure string rule with its own tests, and it was private while a second caller needed it
|
|
16
|
+
* -- which is exactly the shape that let `reticle setup mcp` reimplement the spawn as a bare
|
|
17
|
+
* `execFileSync` and break on Windows. Two copies of a quoting rule is the version of this bug that
|
|
18
|
+
* is hardest to see, so there is one.
|
|
19
|
+
*/
|
|
20
|
+
export { windowsShellArg } from './register/windows-quote.js';
|
|
21
|
+
export { buildNodeIo, probeCli } from './node-io.js';
|
|
13
22
|
export { type InitHost, SILENT_HOST } from './host.js';
|
|
14
23
|
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';
|
|
24
|
+
export { InitFailure } from './diagnose/init-failure.js';
|
|
25
|
+
export { NodePlatform } from './detect/platform.js';
|
|
26
|
+
export { DEV_SERVER_PORTS, isLikelyDevServerPort, devServerPortWarning, } from './detect/dev-server-ports.js';
|
|
27
|
+
export { configWithInstallSource } from './project/install-source-config.js';
|
|
28
|
+
export { projectIdOf, rememberProjectOnDisk, type RegistryIo } from './project/remember-project.js';
|
|
29
|
+
export { detectPackageManager, installCommandParts, parseMajor, PackageManager, } from './detect/detect.js';
|
|
30
|
+
export { findWorkspaceApps } from './detect/workspace-apps.js';
|
|
31
|
+
export { refreshAgentRules } from './project/refresh-rules.js';
|
|
32
|
+
export { diagnoseDesktop, isDesktopProject } from './diagnose/desktop-doctor.js';
|
|
33
|
+
export { diagnoseWebCsp } from './diagnose/csp-doctor.js';
|
|
34
|
+
export { reticleConfigContent } from './patch/snippets.js';
|
|
35
|
+
/**
|
|
36
|
+
* MCP registration, for a caller that has no project.
|
|
37
|
+
*
|
|
38
|
+
* `reticle setup mcp` registers the server for the USER across every agent on the machine, which is
|
|
39
|
+
* what the one-line installer runs before any project exists. It needs the same client table and
|
|
40
|
+
* the same detection `init` uses — a second copy would be a second answer to "which agents are
|
|
41
|
+
* here", and the two would disagree the first time a client was added to one of them.
|
|
42
|
+
*/
|
|
43
|
+
export { McpClient, ConfigScope, MCP_CLIENTS, ClientMergeStatus, clientSpec, mergeClientConfig, } from './register/mcp-clients.js';
|
|
44
|
+
export { claudeAddCommand, claudeAvailableProbe, claudeExistsProbe, } from './register/mcp.js';
|
|
45
|
+
export { detectMcpClients, type DetectedClient } from './register/detect-clients.js';
|