@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.
Files changed (114) hide show
  1. package/LICENSE +105 -0
  2. package/NOTICE +5 -0
  3. package/README.md +29 -0
  4. package/dist/agent-rules.d.ts +116 -0
  5. package/dist/agent-rules.js +279 -0
  6. package/dist/app-choice.d.ts +29 -0
  7. package/dist/app-choice.js +39 -0
  8. package/dist/astro-host.d.ts +21 -0
  9. package/dist/astro-host.js +27 -0
  10. package/dist/astro-patch.d.ts +33 -0
  11. package/dist/astro-patch.js +103 -0
  12. package/dist/brace-scan.d.ts +24 -0
  13. package/dist/brace-scan.js +46 -0
  14. package/dist/capabilities.d.ts +40 -0
  15. package/dist/capabilities.js +167 -0
  16. package/dist/closing-hint.d.ts +35 -0
  17. package/dist/closing-hint.js +122 -0
  18. package/dist/codex-toml.d.ts +10 -0
  19. package/dist/codex-toml.js +144 -0
  20. package/dist/connect-steps.d.ts +55 -0
  21. package/dist/connect-steps.js +92 -0
  22. package/dist/cra.d.ts +82 -0
  23. package/dist/cra.js +183 -0
  24. package/dist/csp-check.d.ts +49 -0
  25. package/dist/csp-check.js +168 -0
  26. package/dist/csp-doctor.d.ts +32 -0
  27. package/dist/csp-doctor.js +62 -0
  28. package/dist/desktop-doctor.d.ts +46 -0
  29. package/dist/desktop-doctor.js +201 -0
  30. package/dist/detect.d.ts +225 -0
  31. package/dist/detect.js +334 -0
  32. package/dist/dev-script.d.ts +51 -0
  33. package/dist/dev-script.js +66 -0
  34. package/dist/dev-server-ports.d.ts +20 -0
  35. package/dist/dev-server-ports.js +45 -0
  36. package/dist/electron-patch.d.ts +11 -0
  37. package/dist/electron-patch.js +70 -0
  38. package/dist/electron-vite-patch.d.ts +12 -0
  39. package/dist/electron-vite-patch.js +100 -0
  40. package/dist/existing-config.d.ts +31 -0
  41. package/dist/existing-config.js +63 -0
  42. package/dist/format-generated.d.ts +18 -0
  43. package/dist/format-generated.js +48 -0
  44. package/dist/framework-adapter.d.ts +60 -0
  45. package/dist/framework-adapter.js +138 -0
  46. package/dist/host.d.ts +41 -0
  47. package/dist/host.js +21 -0
  48. package/dist/index.d.ts +25 -0
  49. package/dist/index.js +25 -0
  50. package/dist/init-failure.d.ts +19 -0
  51. package/dist/init-failure.js +18 -0
  52. package/dist/init-opt-out.d.ts +17 -0
  53. package/dist/init-opt-out.js +30 -0
  54. package/dist/install-hint.d.ts +9 -0
  55. package/dist/install-hint.js +41 -0
  56. package/dist/install-retries.d.ts +22 -0
  57. package/dist/install-retries.js +65 -0
  58. package/dist/install-source-config.d.ts +18 -0
  59. package/dist/install-source-config.js +34 -0
  60. package/dist/lockfiles.d.ts +14 -0
  61. package/dist/lockfiles.js +49 -0
  62. package/dist/mcp-clients.d.ts +94 -0
  63. package/dist/mcp-clients.js +327 -0
  64. package/dist/mcp-registered.d.ts +12 -0
  65. package/dist/mcp-registered.js +14 -0
  66. package/dist/mcp.d.ts +89 -0
  67. package/dist/mcp.js +129 -0
  68. package/dist/next-patch.d.ts +49 -0
  69. package/dist/next-patch.js +237 -0
  70. package/dist/node-io.d.ts +22 -0
  71. package/dist/node-io.js +150 -0
  72. package/dist/non-js-project.d.ts +44 -0
  73. package/dist/non-js-project.js +137 -0
  74. package/dist/nuxt-patch.d.ts +18 -0
  75. package/dist/nuxt-patch.js +33 -0
  76. package/dist/patch-kind.d.ts +20 -0
  77. package/dist/patch-kind.js +10 -0
  78. package/dist/plan-electron-vite.d.ts +16 -0
  79. package/dist/plan-electron-vite.js +72 -0
  80. package/dist/plan-framework.d.ts +122 -0
  81. package/dist/plan-framework.js +773 -0
  82. package/dist/plan.d.ts +312 -0
  83. package/dist/plan.js +650 -0
  84. package/dist/platform.d.ts +16 -0
  85. package/dist/platform.js +16 -0
  86. package/dist/preflight.d.ts +30 -0
  87. package/dist/preflight.js +52 -0
  88. package/dist/project-id.d.ts +12 -0
  89. package/dist/project-id.js +25 -0
  90. package/dist/refresh-rules.d.ts +40 -0
  91. package/dist/refresh-rules.js +51 -0
  92. package/dist/remember-project.d.ts +24 -0
  93. package/dist/remember-project.js +40 -0
  94. package/dist/run.d.ts +122 -0
  95. package/dist/run.js +762 -0
  96. package/dist/slash-command.d.ts +27 -0
  97. package/dist/slash-command.js +132 -0
  98. package/dist/snippets.d.ts +267 -0
  99. package/dist/snippets.js +898 -0
  100. package/dist/tanstack-start.d.ts +18 -0
  101. package/dist/tanstack-start.js +57 -0
  102. package/dist/version.d.ts +18 -0
  103. package/dist/version.js +23 -0
  104. package/dist/vite-config.d.ts +18 -0
  105. package/dist/vite-config.js +111 -0
  106. package/dist/vite-owning-config.d.ts +25 -0
  107. package/dist/vite-owning-config.js +219 -0
  108. package/dist/windows-quote.d.ts +38 -0
  109. package/dist/windows-quote.js +70 -0
  110. package/dist/workspace-apps.d.ts +40 -0
  111. package/dist/workspace-apps.js +188 -0
  112. package/dist/workspace-redirect.d.ts +22 -0
  113. package/dist/workspace-redirect.js +97 -0
  114. package/package.json +53 -0
@@ -0,0 +1,188 @@
1
+ import { DEV_SCRIPT_NAMES } from './dev-script.js';
2
+ /**
3
+ * The file names that say what a directory IS, shared by everything that has to ask.
4
+ *
5
+ * They lived in run.ts and are read from here now because `findWorkspaceApps` moved and needs them —
6
+ * one definition, imported in both directions of the question ("is this an app?" and "which app?").
7
+ */
8
+ export const PACKAGE_JSON = 'package.json';
9
+ const PNPM_WORKSPACE = 'pnpm-workspace.yaml';
10
+ export const VITE_CONFIG_CANDIDATES = [
11
+ 'vite.config.ts',
12
+ 'vite.config.js',
13
+ 'vite.config.mjs',
14
+ 'vite.config.mts',
15
+ ];
16
+ export const ELECTRON_VITE_CONFIG_CANDIDATES = [
17
+ 'electron.vite.config.ts',
18
+ 'electron.vite.config.js',
19
+ 'electron.vite.config.mjs',
20
+ 'electron.vite.config.mts',
21
+ ];
22
+ export const NEXT_CONFIG_CANDIDATES = [
23
+ 'next.config.mjs',
24
+ 'next.config.js',
25
+ 'next.config.ts',
26
+ 'next.config.cjs',
27
+ ];
28
+ /**
29
+ * Which directories to look in for the app, when `reticle init` runs at a monorepo root.
30
+ *
31
+ * This used to be the literal list `['apps', 'packages']`. Measured on a real repo with three Next
32
+ * apps at `web/`, `admin/` and `space/`, it found none — so the redirect never fired, init ran
33
+ * against the root, and it reported ✓ for writing `app/reticle-dev.tsx` into a directory Next never
34
+ * compiles. A ⚠ tells a human to act; a ✓ tells them it is handled.
35
+ *
36
+ * A workspace DECLARES its packages, so that declaration is used first and directory names are not
37
+ * guessed at all. Where nothing is declared, every top-level directory is a better candidate than two
38
+ * hardcoded ones — filtered by the usual non-source suspects, and still subject to the `looksLikeApp`
39
+ * check that follows, so a wrong guess here costs a `package.json` read.
40
+ */
41
+ /** Directories that are never a workspace package, whatever the layout. */
42
+ const NEVER_A_PACKAGE = new Set(['node_modules', 'dist', 'build', 'out', 'coverage', 'target']);
43
+ /** `packages/*` -> `packages`; `web` -> `web`. The base directory a glob searches. */
44
+ function globBase(pattern) {
45
+ const base = pattern
46
+ .split('/')[0]
47
+ ?.trim()
48
+ .replace(/^['"]|['"]$/g, '');
49
+ return base === undefined || '' === base || '.' === base || base.includes('*') ? undefined : base;
50
+ }
51
+ function fromPnpm(yaml) {
52
+ // A deliberately small reader rather than a YAML dependency: this file is a list of globs, and the
53
+ // only shape that matters is `- 'pattern'` under `packages:`. A malformed file yields nothing,
54
+ // which falls through to the top-level scan rather than failing the install.
55
+ const out = [];
56
+ let inPackages = false;
57
+ for (const line of yaml.split('\n')) {
58
+ if (/^packages\s*:/.test(line)) {
59
+ inPackages = true;
60
+ continue;
61
+ }
62
+ if (inPackages && /^\S/.test(line))
63
+ break;
64
+ const item = /^\s*-\s*(.+)$/.exec(line);
65
+ if (inPackages && item?.[1] !== undefined) {
66
+ const base = globBase(item[1]);
67
+ if (base !== undefined)
68
+ out.push(base);
69
+ }
70
+ }
71
+ return out;
72
+ }
73
+ function fromPkg(workspaces) {
74
+ const list = Array.isArray(workspaces)
75
+ ? workspaces
76
+ : 'object' === typeof workspaces && workspaces !== null
77
+ ? workspaces.packages
78
+ : undefined;
79
+ if (!Array.isArray(list))
80
+ return [];
81
+ return list
82
+ .filter((p) => 'string' === typeof p)
83
+ .map(globBase)
84
+ .filter((p) => p !== undefined);
85
+ }
86
+ export function workspaceParents(sources) {
87
+ const declared = [
88
+ ...(sources.pnpmWorkspace === undefined ? [] : fromPnpm(sources.pnpmWorkspace)),
89
+ ...fromPkg(sources.pkgWorkspaces),
90
+ ];
91
+ const candidates = declared.length > 0
92
+ ? declared
93
+ : (sources.topLevelDirs ?? []).filter((d) => !d.startsWith('.') && !NEVER_A_PACKAGE.has(d));
94
+ return [...new Set(candidates)];
95
+ }
96
+ /**
97
+ * Which directories under this workspace are runnable apps.
98
+ *
99
+ * Moved here from run.ts, which was over the 1000-line cap and had carried a note saying it was at
100
+ * its cohesion limit. `library-path-boundary.test.ts` had already written down that this function
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.
103
+ */
104
+ /** Deps that mark a directory as a runnable web app even when it has no bundler config file. */
105
+ export const APP_DEPS = ['next', 'vite', 'electron-vite'];
106
+ function hasDevScript(pkgRaw) {
107
+ try {
108
+ const parsed = JSON.parse(pkgRaw);
109
+ const scripts = 'object' === typeof parsed && parsed !== null
110
+ ? parsed.scripts
111
+ : undefined;
112
+ if ('object' !== typeof scripts || null === scripts)
113
+ return false;
114
+ const named = scripts;
115
+ return DEV_SCRIPT_NAMES.some((n) => 'string' === typeof named[n] && '' !== named[n]);
116
+ }
117
+ catch {
118
+ // A manifest that will not parse says nothing either way, and this is a filter, not a verdict.
119
+ return false;
120
+ }
121
+ }
122
+ function looksLikeApp(dir, io) {
123
+ const pkgRaw = io.readFile(`${dir}/${PACKAGE_JSON}`);
124
+ if (null === pkgRaw)
125
+ return false;
126
+ const configs = [
127
+ ...VITE_CONFIG_CANDIDATES,
128
+ ...ELECTRON_VITE_CONFIG_CANDIDATES,
129
+ ...NEXT_CONFIG_CANDIDATES,
130
+ ];
131
+ if (configs.some((c) => io.exists(`${dir}/${c}`)))
132
+ return true;
133
+ // `next.config` is optional in Next, so the dependency list is the other half of the signal.
134
+ if (APP_DEPS.some((d) => pkgRaw.includes(`"${d}"`)))
135
+ return true;
136
+ // And the honest third: an app somebody can SERVE. Asking only for a bundler missed every app
137
+ // built on anything else — Remix, Astro, a plain node server — and in a monorepo missing it means
138
+ // init never redirects, wires the ROOT, and reports ✓ for files nothing compiles.
139
+ //
140
+ // A monorepo root has no dev script by design, and a package that can only be BUILT is not the app
141
+ // somebody is working in, so this stays a filter rather than matching every directory.
142
+ return hasDevScript(pkgRaw);
143
+ }
144
+ /**
145
+ * App directories under a workspace root.
146
+ *
147
+ * Running `reticle init` at the repo root is what people actually do, and in a monorepo the app is a
148
+ * directory down — so init detected "no framework", printed a wall of manual HTML instructions, and
149
+ * would have installed the SDK into the ROOT package.json. It already walks UP for the lockfile, so
150
+ * it knows it is in a workspace; this is the matching walk DOWN.
151
+ */
152
+ export function findWorkspaceApps(io) {
153
+ const found = [];
154
+ // A workspace DECLARES its packages; `['apps','packages']` was a guess that missed a real repo
155
+ // with three Next apps at web/, admin/ and space/ — and missing them meant init ran against the
156
+ // root and reported ✓ for a file Next never compiles. See workspace-apps.
157
+ const pkgRaw = io.readFile(PACKAGE_JSON);
158
+ let pkgWorkspaces;
159
+ try {
160
+ const parsed = null === pkgRaw ? undefined : JSON.parse(pkgRaw);
161
+ pkgWorkspaces =
162
+ 'object' === typeof parsed && parsed !== null
163
+ ? parsed.workspaces
164
+ : undefined;
165
+ }
166
+ catch {
167
+ pkgWorkspaces = undefined;
168
+ }
169
+ const parents = workspaceParents({
170
+ ...(null === io.readFile(PNPM_WORKSPACE)
171
+ ? {}
172
+ : { pnpmWorkspace: io.readFile(PNPM_WORKSPACE) ?? '' }),
173
+ ...(pkgWorkspaces === undefined ? {} : { pkgWorkspaces }),
174
+ topLevelDirs: io.listDirs('.'),
175
+ });
176
+ // A declared parent can itself BE the app (`workspaces: ["web"]`), so check both the directory and
177
+ // its children rather than assuming one level of nesting.
178
+ for (const parent of parents) {
179
+ if (looksLikeApp(parent, io))
180
+ found.push(parent);
181
+ for (const name of io.listDirs(parent)) {
182
+ const dir = `${parent}/${name}`;
183
+ if (looksLikeApp(dir, io))
184
+ found.push(dir);
185
+ }
186
+ }
187
+ return [...new Set(found)];
188
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Workspace redirect: when `init` is run at a monorepo ROOT, wire the app rather than the root.
3
+ *
4
+ * Split out of `run.ts`, which sits one line under the 1000-line backstop — every init change that
5
+ * touches the orchestration now trips the cap before it can be reviewed. This is the part of that
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.
8
+ *
9
+ * `runInit` arrives as a parameter rather than an import: re-entering init is the whole point of a
10
+ * redirect, and taking it as an argument keeps this module a leaf instead of forming a cycle with
11
+ * the file it was cut from.
12
+ */
13
+ import type { InitIo, InitOptions, InitResult } from './run.js';
14
+ /** Re-enter `init`, scoped to one directory of the workspace. */
15
+ export type RunInit = (options: InitOptions, io: InitIo) => InitResult;
16
+ /**
17
+ * When the current directory is a workspace root with no app of its own, wire the app instead of the
18
+ * root. One candidate is wired silently (there is nothing to ask about); several are listed, because
19
+ * guessing which app someone meant is worse than one line of output.
20
+ * Returns null when there is nothing to redirect to — the caller then proceeds here as before.
21
+ */
22
+ export declare function redirectToWorkspaceApp(options: InitOptions, io: InitIo, pkg: unknown, runInit: RunInit): InitResult | null;
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Workspace redirect: when `init` is run at a monorepo ROOT, wire the app rather than the root.
3
+ *
4
+ * Split out of `run.ts`, which sits one line under the 1000-line backstop — every init change that
5
+ * touches the orchestration now trips the cap before it can be reviewed. This is the part of that
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.
8
+ *
9
+ * `runInit` arrives as a parameter rather than an import: re-entering init is the whole point of a
10
+ * redirect, and taking it as an argument keeps this module a leaf instead of forming a cycle with
11
+ * the file it was cut from.
12
+ */
13
+ import { join } from 'node:path';
14
+ import { detect, Framework } from './detect.js';
15
+ import { findWorkspaceApps, PACKAGE_JSON } from './workspace-apps.js';
16
+ import { chooseWorkspaceApp, withoutTrailingSlashes } from './app-choice.js';
17
+ const AMBIGUOUS_HEADER = 'Several apps found in this workspace. Re-run `reticle init` inside the one you want:';
18
+ /**
19
+ * When the current directory is a workspace root with no app of its own, wire the app instead of the
20
+ * root. One candidate is wired silently (there is nothing to ask about); several are listed, because
21
+ * guessing which app someone meant is worse than one line of output.
22
+ * Returns null when there is nothing to redirect to — the caller then proceeds here as before.
23
+ */
24
+ export function redirectToWorkspaceApp(options, io, pkg, runInit) {
25
+ if (true === options.redirected)
26
+ return null;
27
+ const rootFiles = new Set(io.rootFiles());
28
+ const here = detect({
29
+ pkg: 'object' === typeof pkg && pkg !== null ? pkg : {},
30
+ configFiles: rootFiles,
31
+ lockfiles: new Set(),
32
+ });
33
+ // `--app` is an INSTRUCTION, and it is read before the guess below. The guess answers "where is
34
+ // the app?" for somebody who did not say; when somebody said, there is nothing left to infer.
35
+ //
36
+ // It used to be read after, and the check underneath returns early for any directory that looks
37
+ // like an app — which a JS monorepo ROOT does, because shared tooling puts `vite` in its
38
+ // devDependencies. So on a real pnpm+turbo monorepo (measured on nuclear, a Tauri v2 app at
39
+ // product scale) `reticle init --app packages/player` silently ignored the flag, installed the
40
+ // SDK into the root's package.json, wrote `.reticle.json` and a whole `src/reticle-dev.ts` into a
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.
43
+ //
44
+ // Existence is the test, not membership of the discovered list: discovery scans conventional
45
+ // directories, and somebody who names a path knows their own layout better than the scan does.
46
+ const named = options.app === undefined || '' === options.app ? undefined : options.app;
47
+ if (named !== undefined) {
48
+ const wanted = withoutTrailingSlashes(named);
49
+ if (io.exists(`${wanted}/${PACKAGE_JSON}`))
50
+ return enterApp(options, io, wanted, 'Wiring', runInit);
51
+ io.print(`--app ${named} does not name a directory with a package.json in it.`);
52
+ return { ok: false, applied: 0, manual: 1 };
53
+ }
54
+ if (here.framework !== Framework.HTML)
55
+ return null; // this directory IS the app
56
+ const apps = findWorkspaceApps(io);
57
+ // An explicitly named app answers the ambiguity. Refusing to guess is right, but "re-run inside the
58
+ // one you want" is not something a script, a CI step, or an agent that cannot change directory can
59
+ // act on — so the refusal was a dead end for exactly the callers most likely to hit it.
60
+ const chosen = chooseWorkspaceApp(options.app, apps);
61
+ if (!chosen.ok) {
62
+ io.print(chosen.message);
63
+ return { ok: false, applied: 0, manual: 1 };
64
+ }
65
+ const target = chosen.app ?? (1 === apps.length ? apps[0] : undefined);
66
+ if (target === undefined) {
67
+ if (0 === apps.length)
68
+ return null; // not a workspace — fall through to the normal HTML plan
69
+ io.print(AMBIGUOUS_HEADER);
70
+ for (const a of apps)
71
+ io.print(` ${a}`);
72
+ io.print('');
73
+ io.print(`Or name one without changing directory: reticle init --app ${apps[0] ?? '<dir>'}`);
74
+ return { ok: false, applied: 0, manual: apps.length };
75
+ }
76
+ return enterApp(options, io, target, 'No app in this directory — wiring', runInit);
77
+ }
78
+ /**
79
+ * Re-enter `init` scoped to one directory of a workspace.
80
+ *
81
+ * One implementation for both routes in — the app somebody NAMED and the app discovery found when
82
+ * nobody did. They differ only in the sentence printed; scoping the io, moving the cwd and keeping
83
+ * the agent root has to be identical for both, and was worth having twice for exactly as long as it
84
+ * took to get one of them wrong.
85
+ */
86
+ function enterApp(options, io, target, lead, runInit) {
87
+ io.print(`${lead} ${target}.`);
88
+ io.print('');
89
+ return runInit({
90
+ ...options,
91
+ cwd: join(options.cwd, target),
92
+ redirected: true,
93
+ // Where the human is STANDING, kept across the redirect: their agent session runs here, so
94
+ // this is the only place a `/reticle` command file can be found by it.
95
+ agentRoot: options.agentRoot ?? options.cwd,
96
+ }, io.scoped(target));
97
+ }
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@reticlehq/init",
3
+ "version": "2.14.0",
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
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ },
13
+ "./package.json": "./package.json"
14
+ },
15
+ "dependencies": {
16
+ "@reticlehq/core": "2.14.0"
17
+ },
18
+ "license": "SEE LICENSE IN LICENSE",
19
+ "author": "Reticle contributors",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/reticlehq/reticle.git",
23
+ "directory": "packages/init"
24
+ },
25
+ "homepage": "https://github.com/reticlehq/reticle#readme",
26
+ "bugs": "https://github.com/reticlehq/reticle/issues",
27
+ "files": [
28
+ "dist",
29
+ "README.md",
30
+ "LICENSE",
31
+ "NOTICE"
32
+ ],
33
+ "sideEffects": false,
34
+ "keywords": [
35
+ "mcp",
36
+ "ai-agents",
37
+ "scaffolding",
38
+ "cli"
39
+ ],
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "engines": {
44
+ "node": ">=20.0.0"
45
+ },
46
+ "scripts": {
47
+ "build": "tsc -b",
48
+ "typecheck": "tsc -b",
49
+ "lint": "eslint src",
50
+ "test:unit": "vitest run src --passWithNoTests",
51
+ "test:coverage": "vitest run src --passWithNoTests --coverage.enabled --coverage.provider=v8 --coverage.reporter=text-summary --coverage.include='src/**/*.ts' --coverage.exclude='src/**/*.test.ts'"
52
+ }
53
+ }