@hydranium/cli 1.0.0-next.10

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 (267) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +240 -0
  3. package/heap-analysis/analyze-heap.mjs +798 -0
  4. package/heap-analysis/heap/core/aggregate.mjs +152 -0
  5. package/heap-analysis/heap/core/bucket-label.mjs +23 -0
  6. package/heap-analysis/heap/core/cutpoints.mjs +51 -0
  7. package/heap-analysis/heap/core/diff.mjs +56 -0
  8. package/heap-analysis/heap/core/format.mjs +47 -0
  9. package/heap-analysis/heap/core/holders.mjs +80 -0
  10. package/heap-analysis/heap/core/load.mjs +46 -0
  11. package/heap-analysis/heap/core/report.mjs +68 -0
  12. package/heap-analysis/heap/core/strings.mjs +80 -0
  13. package/heap-analysis/heap/langium/concepts.mjs +230 -0
  14. package/heap-analysis/heap/langium/glsp.classifier.mjs +80 -0
  15. package/heap-analysis/heap/langium/langium.classifier.mjs +203 -0
  16. package/heap-analysis/heap/renderer/renderer.mjs +122 -0
  17. package/lib/cli-version.d.ts +21 -0
  18. package/lib/cli-version.d.ts.map +1 -0
  19. package/lib/cli-version.js +30 -0
  20. package/lib/cli-version.js.map +1 -0
  21. package/lib/cli.d.ts +11 -0
  22. package/lib/cli.d.ts.map +1 -0
  23. package/lib/cli.js +135 -0
  24. package/lib/cli.js.map +1 -0
  25. package/lib/commands/analyze-heap-args.d.ts +46 -0
  26. package/lib/commands/analyze-heap-args.d.ts.map +1 -0
  27. package/lib/commands/analyze-heap-args.js +174 -0
  28. package/lib/commands/analyze-heap-args.js.map +1 -0
  29. package/lib/commands/analyze-heap.d.ts +37 -0
  30. package/lib/commands/analyze-heap.d.ts.map +1 -0
  31. package/lib/commands/analyze-heap.js +63 -0
  32. package/lib/commands/analyze-heap.js.map +1 -0
  33. package/lib/commands/ast-ground-truth-args.d.ts +23 -0
  34. package/lib/commands/ast-ground-truth-args.d.ts.map +1 -0
  35. package/lib/commands/ast-ground-truth-args.js +47 -0
  36. package/lib/commands/ast-ground-truth-args.js.map +1 -0
  37. package/lib/commands/ast-ground-truth-driver.d.ts +10 -0
  38. package/lib/commands/ast-ground-truth-driver.d.ts.map +1 -0
  39. package/lib/commands/ast-ground-truth-driver.js +72 -0
  40. package/lib/commands/ast-ground-truth-driver.js.map +1 -0
  41. package/lib/commands/ast-ground-truth.d.ts +31 -0
  42. package/lib/commands/ast-ground-truth.d.ts.map +1 -0
  43. package/lib/commands/ast-ground-truth.js +36 -0
  44. package/lib/commands/ast-ground-truth.js.map +1 -0
  45. package/lib/commands/generate-transfer-model-args.d.ts +38 -0
  46. package/lib/commands/generate-transfer-model-args.d.ts.map +1 -0
  47. package/lib/commands/generate-transfer-model-args.js +183 -0
  48. package/lib/commands/generate-transfer-model-args.js.map +1 -0
  49. package/lib/commands/generate-transfer-model-config.d.ts +32 -0
  50. package/lib/commands/generate-transfer-model-config.d.ts.map +1 -0
  51. package/lib/commands/generate-transfer-model-config.js +122 -0
  52. package/lib/commands/generate-transfer-model-config.js.map +1 -0
  53. package/lib/commands/generate-transfer-model.d.ts +110 -0
  54. package/lib/commands/generate-transfer-model.d.ts.map +1 -0
  55. package/lib/commands/generate-transfer-model.js +587 -0
  56. package/lib/commands/generate-transfer-model.js.map +1 -0
  57. package/lib/commands/harness-args.d.ts +198 -0
  58. package/lib/commands/harness-args.d.ts.map +1 -0
  59. package/lib/commands/harness-args.js +339 -0
  60. package/lib/commands/harness-args.js.map +1 -0
  61. package/lib/commands/headless-harness.d.ts +97 -0
  62. package/lib/commands/headless-harness.d.ts.map +1 -0
  63. package/lib/commands/headless-harness.js +121 -0
  64. package/lib/commands/headless-harness.js.map +1 -0
  65. package/lib/commands/init-args.d.ts +81 -0
  66. package/lib/commands/init-args.d.ts.map +1 -0
  67. package/lib/commands/init-args.js +250 -0
  68. package/lib/commands/init-args.js.map +1 -0
  69. package/lib/commands/init-prompt.d.ts +66 -0
  70. package/lib/commands/init-prompt.d.ts.map +1 -0
  71. package/lib/commands/init-prompt.js +71 -0
  72. package/lib/commands/init-prompt.js.map +1 -0
  73. package/lib/commands/init-templates.d.ts +42 -0
  74. package/lib/commands/init-templates.d.ts.map +1 -0
  75. package/lib/commands/init-templates.js +1902 -0
  76. package/lib/commands/init-templates.js.map +1 -0
  77. package/lib/commands/init-wizard.d.ts +65 -0
  78. package/lib/commands/init-wizard.d.ts.map +1 -0
  79. package/lib/commands/init-wizard.js +302 -0
  80. package/lib/commands/init-wizard.js.map +1 -0
  81. package/lib/commands/init-workspace.d.ts +168 -0
  82. package/lib/commands/init-workspace.d.ts.map +1 -0
  83. package/lib/commands/init-workspace.js +372 -0
  84. package/lib/commands/init-workspace.js.map +1 -0
  85. package/lib/commands/init.d.ts +273 -0
  86. package/lib/commands/init.d.ts.map +1 -0
  87. package/lib/commands/init.js +303 -0
  88. package/lib/commands/init.js.map +1 -0
  89. package/lib/commands/lint-grammar-args.d.ts +23 -0
  90. package/lib/commands/lint-grammar-args.d.ts.map +1 -0
  91. package/lib/commands/lint-grammar-args.js +63 -0
  92. package/lib/commands/lint-grammar-args.js.map +1 -0
  93. package/lib/commands/lint-grammar-driver.d.ts +10 -0
  94. package/lib/commands/lint-grammar-driver.d.ts.map +1 -0
  95. package/lib/commands/lint-grammar-driver.js +72 -0
  96. package/lib/commands/lint-grammar-driver.js.map +1 -0
  97. package/lib/commands/lint-grammar-report.d.ts +24 -0
  98. package/lib/commands/lint-grammar-report.d.ts.map +1 -0
  99. package/lib/commands/lint-grammar-report.js +65 -0
  100. package/lib/commands/lint-grammar-report.js.map +1 -0
  101. package/lib/commands/lint-grammar.d.ts +33 -0
  102. package/lib/commands/lint-grammar.d.ts.map +1 -0
  103. package/lib/commands/lint-grammar.js +42 -0
  104. package/lib/commands/lint-grammar.js.map +1 -0
  105. package/lib/commands/measure-memory-args.d.ts +23 -0
  106. package/lib/commands/measure-memory-args.d.ts.map +1 -0
  107. package/lib/commands/measure-memory-args.js +106 -0
  108. package/lib/commands/measure-memory-args.js.map +1 -0
  109. package/lib/commands/measure-memory-driver.d.ts +10 -0
  110. package/lib/commands/measure-memory-driver.d.ts.map +1 -0
  111. package/lib/commands/measure-memory-driver.js +110 -0
  112. package/lib/commands/measure-memory-driver.js.map +1 -0
  113. package/lib/commands/measure-memory.d.ts +68 -0
  114. package/lib/commands/measure-memory.d.ts.map +1 -0
  115. package/lib/commands/measure-memory.js +100 -0
  116. package/lib/commands/measure-memory.js.map +1 -0
  117. package/lib/commands/model-docs-args.d.ts +23 -0
  118. package/lib/commands/model-docs-args.d.ts.map +1 -0
  119. package/lib/commands/model-docs-args.js +53 -0
  120. package/lib/commands/model-docs-args.js.map +1 -0
  121. package/lib/commands/model-docs-driver.d.ts +10 -0
  122. package/lib/commands/model-docs-driver.d.ts.map +1 -0
  123. package/lib/commands/model-docs-driver.js +61 -0
  124. package/lib/commands/model-docs-driver.js.map +1 -0
  125. package/lib/commands/model-docs-report.d.ts +18 -0
  126. package/lib/commands/model-docs-report.d.ts.map +1 -0
  127. package/lib/commands/model-docs-report.js +123 -0
  128. package/lib/commands/model-docs-report.js.map +1 -0
  129. package/lib/commands/model-docs.d.ts +30 -0
  130. package/lib/commands/model-docs.d.ts.map +1 -0
  131. package/lib/commands/model-docs.js +37 -0
  132. package/lib/commands/model-docs.js.map +1 -0
  133. package/lib/commands/projects-args.d.ts +25 -0
  134. package/lib/commands/projects-args.d.ts.map +1 -0
  135. package/lib/commands/projects-args.js +49 -0
  136. package/lib/commands/projects-args.js.map +1 -0
  137. package/lib/commands/projects.d.ts +45 -0
  138. package/lib/commands/projects.d.ts.map +1 -0
  139. package/lib/commands/projects.js +39 -0
  140. package/lib/commands/projects.js.map +1 -0
  141. package/lib/commands/query-args.d.ts +25 -0
  142. package/lib/commands/query-args.d.ts.map +1 -0
  143. package/lib/commands/query-args.js +46 -0
  144. package/lib/commands/query-args.js.map +1 -0
  145. package/lib/commands/query.d.ts +32 -0
  146. package/lib/commands/query.d.ts.map +1 -0
  147. package/lib/commands/query.js +29 -0
  148. package/lib/commands/query.js.map +1 -0
  149. package/lib/commands/reflect-args.d.ts +23 -0
  150. package/lib/commands/reflect-args.d.ts.map +1 -0
  151. package/lib/commands/reflect-args.js +59 -0
  152. package/lib/commands/reflect-args.js.map +1 -0
  153. package/lib/commands/reflect-driver.d.ts +10 -0
  154. package/lib/commands/reflect-driver.d.ts.map +1 -0
  155. package/lib/commands/reflect-driver.js +66 -0
  156. package/lib/commands/reflect-driver.js.map +1 -0
  157. package/lib/commands/reflect-report.d.ts +20 -0
  158. package/lib/commands/reflect-report.d.ts.map +1 -0
  159. package/lib/commands/reflect-report.js +127 -0
  160. package/lib/commands/reflect-report.js.map +1 -0
  161. package/lib/commands/reflect.d.ts +30 -0
  162. package/lib/commands/reflect.d.ts.map +1 -0
  163. package/lib/commands/reflect.js +38 -0
  164. package/lib/commands/reflect.js.map +1 -0
  165. package/lib/commands/save-args.d.ts +26 -0
  166. package/lib/commands/save-args.d.ts.map +1 -0
  167. package/lib/commands/save-args.js +59 -0
  168. package/lib/commands/save-args.js.map +1 -0
  169. package/lib/commands/save.d.ts +38 -0
  170. package/lib/commands/save.d.ts.map +1 -0
  171. package/lib/commands/save.js +48 -0
  172. package/lib/commands/save.js.map +1 -0
  173. package/lib/commands/validate-args.d.ts +23 -0
  174. package/lib/commands/validate-args.d.ts.map +1 -0
  175. package/lib/commands/validate-args.js +60 -0
  176. package/lib/commands/validate-args.js.map +1 -0
  177. package/lib/commands/validate-driver.d.ts +10 -0
  178. package/lib/commands/validate-driver.d.ts.map +1 -0
  179. package/lib/commands/validate-driver.js +72 -0
  180. package/lib/commands/validate-driver.js.map +1 -0
  181. package/lib/commands/validate-report.d.ts +30 -0
  182. package/lib/commands/validate-report.d.ts.map +1 -0
  183. package/lib/commands/validate-report.js +87 -0
  184. package/lib/commands/validate-report.js.map +1 -0
  185. package/lib/commands/validate.d.ts +34 -0
  186. package/lib/commands/validate.d.ts.map +1 -0
  187. package/lib/commands/validate.js +41 -0
  188. package/lib/commands/validate.js.map +1 -0
  189. package/lib/commands/watch-args.d.ts +26 -0
  190. package/lib/commands/watch-args.d.ts.map +1 -0
  191. package/lib/commands/watch-args.js +59 -0
  192. package/lib/commands/watch-args.js.map +1 -0
  193. package/lib/commands/watch.d.ts +75 -0
  194. package/lib/commands/watch.d.ts.map +1 -0
  195. package/lib/commands/watch.js +112 -0
  196. package/lib/commands/watch.js.map +1 -0
  197. package/lib/index.d.ts +16 -0
  198. package/lib/index.d.ts.map +1 -0
  199. package/lib/index.js +16 -0
  200. package/lib/index.js.map +1 -0
  201. package/lib/log-level.d.ts +39 -0
  202. package/lib/log-level.d.ts.map +1 -0
  203. package/lib/log-level.js +47 -0
  204. package/lib/log-level.js.map +1 -0
  205. package/lib/package-name.d.ts +33 -0
  206. package/lib/package-name.d.ts.map +1 -0
  207. package/lib/package-name.js +37 -0
  208. package/lib/package-name.js.map +1 -0
  209. package/lib/spawn-data-server.d.ts +102 -0
  210. package/lib/spawn-data-server.d.ts.map +1 -0
  211. package/lib/spawn-data-server.js +154 -0
  212. package/lib/spawn-data-server.js.map +1 -0
  213. package/lib/testing/echo-server.d.ts +11 -0
  214. package/lib/testing/echo-server.d.ts.map +1 -0
  215. package/lib/testing/echo-server.js +79 -0
  216. package/lib/testing/echo-server.js.map +1 -0
  217. package/package.json +105 -0
  218. package/src/cli-version.ts +32 -0
  219. package/src/cli.ts +139 -0
  220. package/src/commands/analyze-heap-args.ts +184 -0
  221. package/src/commands/analyze-heap.ts +76 -0
  222. package/src/commands/ast-ground-truth-args.ts +63 -0
  223. package/src/commands/ast-ground-truth-driver.ts +81 -0
  224. package/src/commands/ast-ground-truth.ts +49 -0
  225. package/src/commands/generate-transfer-model-args.ts +203 -0
  226. package/src/commands/generate-transfer-model-config.ts +128 -0
  227. package/src/commands/generate-transfer-model.ts +716 -0
  228. package/src/commands/harness-args.ts +429 -0
  229. package/src/commands/headless-harness.ts +165 -0
  230. package/src/commands/init-args.ts +273 -0
  231. package/src/commands/init-prompt.ts +149 -0
  232. package/src/commands/init-templates.ts +2037 -0
  233. package/src/commands/init-wizard.ts +378 -0
  234. package/src/commands/init-workspace.ts +442 -0
  235. package/src/commands/init.ts +554 -0
  236. package/src/commands/lint-grammar-args.ts +79 -0
  237. package/src/commands/lint-grammar-driver.ts +83 -0
  238. package/src/commands/lint-grammar-report.ts +74 -0
  239. package/src/commands/lint-grammar.ts +57 -0
  240. package/src/commands/measure-memory-args.ts +123 -0
  241. package/src/commands/measure-memory-driver.ts +128 -0
  242. package/src/commands/measure-memory.ts +143 -0
  243. package/src/commands/model-docs-args.ts +69 -0
  244. package/src/commands/model-docs-driver.ts +68 -0
  245. package/src/commands/model-docs-report.ts +133 -0
  246. package/src/commands/model-docs.ts +48 -0
  247. package/src/commands/projects-args.ts +62 -0
  248. package/src/commands/projects.ts +76 -0
  249. package/src/commands/query-args.ts +61 -0
  250. package/src/commands/query.ts +58 -0
  251. package/src/commands/reflect-args.ts +75 -0
  252. package/src/commands/reflect-driver.ts +76 -0
  253. package/src/commands/reflect-report.ts +138 -0
  254. package/src/commands/reflect.ts +51 -0
  255. package/src/commands/save-args.ts +74 -0
  256. package/src/commands/save.ts +84 -0
  257. package/src/commands/validate-args.ts +76 -0
  258. package/src/commands/validate-driver.ts +84 -0
  259. package/src/commands/validate-report.ts +95 -0
  260. package/src/commands/validate.ts +58 -0
  261. package/src/commands/watch-args.ts +74 -0
  262. package/src/commands/watch.ts +185 -0
  263. package/src/index.ts +16 -0
  264. package/src/log-level.ts +49 -0
  265. package/src/package-name.ts +39 -0
  266. package/src/spawn-data-server.ts +247 -0
  267. package/src/testing/echo-server.ts +93 -0
@@ -0,0 +1,442 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2026 CrossBreeze, EclipseSource and others.
3
+ *
4
+ * This program and the accompanying materials are made available under the
5
+ * terms of the MIT License which is available in the project root.
6
+ *
7
+ * SPDX-License-Identifier: MIT
8
+ ********************************************************************************/
9
+
10
+ /**
11
+ * Workspace detection for `init --monorepo`.
12
+ *
13
+ * Split from `init.ts` because the rules here are the only part of `init` that
14
+ * reads files it does not write, and because the reliable/unreliable line runs
15
+ * straight through them: the root and the base tsconfig are *detected*, the
16
+ * package scope is *guessed from siblings* and the `private` flag is not
17
+ * derivable at all. Keeping them in one module makes that distinction reviewable
18
+ * rather than scattered across the scaffolder.
19
+ *
20
+ * Everything is expressed over {@link WorkspaceProbe} rather than `node:fs`, so
21
+ * the rules are testable against an in-memory tree instead of a temp directory.
22
+ */
23
+
24
+ import * as fs from 'node:fs';
25
+ import * as path from 'node:path';
26
+
27
+ /** A JSON value as `JSON.parse` produces it. */
28
+ export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
29
+
30
+ /** True for a JSON object — the shape `package.json` and `tsconfig.json` parse to. */
31
+ function isJsonObject(value: JsonValue | undefined): value is { [key: string]: JsonValue } {
32
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
33
+ }
34
+
35
+ /** The filesystem reads detection needs, injectable so the rules are unit-testable. */
36
+ export interface WorkspaceProbe {
37
+ /** Parse a JSON file, or `undefined` when it is missing or unparseable. */
38
+ readonly readJson: (absolutePath: string) => JsonValue | undefined;
39
+ /** Names of the subdirectories of `absolutePath`; empty when it is not a directory. */
40
+ readonly listDirectories: (absolutePath: string) => readonly string[];
41
+ /** Names of the files directly in `absolutePath`; empty when it is not a directory. */
42
+ readonly listFiles: (absolutePath: string) => readonly string[];
43
+ /**
44
+ * Raw contents of a file, or `undefined` when it is missing or unreadable.
45
+ *
46
+ * Separate from {@link readJson} because the two configs read through it —
47
+ * prettier's and eslint's — are routinely JavaScript modules, which no JSON
48
+ * parser can reach. Required rather than optional so a test double cannot
49
+ * omit it and silently degrade every detection built on it to "not found".
50
+ */
51
+ readonly readText: (absolutePath: string) => string | undefined;
52
+ }
53
+
54
+ /**
55
+ * Strip the two things `tsc` accepts in a config that `JSON.parse` rejects:
56
+ * comments, and a trailing comma before a closing brace or bracket.
57
+ *
58
+ * Written as a scanner rather than a regex because both constructs are legal
59
+ * INSIDE a string — `"https://example.com"` holds a `//`, and a description can
60
+ * end in a comma — so a pattern that ignores string state corrupts the very
61
+ * configs it is meant to rescue. Without this, a base config with a single
62
+ * comment reads as "no compilerOptions here" and the scaffold silently emits no
63
+ * `extends` at all, which is the worst failure shape available: quietly wrong
64
+ * rather than loudly broken.
65
+ */
66
+ export function stripJsonComments(text: string): string {
67
+ const characters = [...text];
68
+ const output: string[] = [];
69
+ const commaIndices: number[] = [];
70
+ let inString = false;
71
+ let escaped = false;
72
+ for (let index = 0; index < characters.length; index += 1) {
73
+ const character = characters[index];
74
+ const next = characters[index + 1];
75
+ if (inString) {
76
+ output.push(character);
77
+ if (escaped) {
78
+ escaped = false;
79
+ } else if (character === '\\') {
80
+ escaped = true;
81
+ } else if (character === '"') {
82
+ inString = false;
83
+ }
84
+ continue;
85
+ }
86
+ if (character === '"') {
87
+ inString = true;
88
+ output.push(character);
89
+ continue;
90
+ }
91
+ if (character === '/' && next === '/') {
92
+ while (index < characters.length && characters[index] !== '\n') {
93
+ index += 1;
94
+ }
95
+ output.push('\n');
96
+ continue;
97
+ }
98
+ if (character === '/' && next === '*') {
99
+ index += 2;
100
+ while (index < characters.length && !(characters[index] === '*' && characters[index + 1] === '/')) {
101
+ index += 1;
102
+ }
103
+ index += 1;
104
+ continue;
105
+ }
106
+ if (character === ',') {
107
+ commaIndices.push(output.length);
108
+ }
109
+ output.push(character);
110
+ }
111
+ // A comma is trailing only if the next thing that is not whitespace closes
112
+ // the object or array. Resolved after the scan, so the lookahead sees text
113
+ // with the comments already gone.
114
+ for (const commaIndex of commaIndices) {
115
+ let lookahead = commaIndex + 1;
116
+ while (lookahead < output.length && /\s/.test(output[lookahead])) {
117
+ lookahead += 1;
118
+ }
119
+ if (output[lookahead] === '}' || output[lookahead] === ']') {
120
+ output[commaIndex] = '';
121
+ }
122
+ }
123
+ return output.join('');
124
+ }
125
+
126
+ /**
127
+ * The real-filesystem probe.
128
+ *
129
+ * Every read is best-effort: detection runs over directories the user may not
130
+ * own, so an unreadable or malformed file degrades to "not found" rather than
131
+ * aborting a scaffold.
132
+ */
133
+ export function createNodeWorkspaceProbe(): WorkspaceProbe {
134
+ const listEntries = (absolutePath: string, wantDirectory: boolean): string[] => {
135
+ try {
136
+ return fs
137
+ .readdirSync(absolutePath, { withFileTypes: true })
138
+ .filter(entry => entry.isDirectory() === wantDirectory)
139
+ .map(entry => entry.name);
140
+ } catch {
141
+ return [];
142
+ }
143
+ };
144
+ return {
145
+ readJson: absolutePath => {
146
+ try {
147
+ return JSON.parse(stripJsonComments(fs.readFileSync(absolutePath, 'utf-8'))) as JsonValue;
148
+ } catch {
149
+ return undefined;
150
+ }
151
+ },
152
+ listDirectories: absolutePath => listEntries(absolutePath, true),
153
+ listFiles: absolutePath => listEntries(absolutePath, false),
154
+ readText: absolutePath => {
155
+ try {
156
+ return fs.readFileSync(absolutePath, 'utf-8');
157
+ } catch {
158
+ return undefined;
159
+ }
160
+ }
161
+ };
162
+ }
163
+
164
+ /** What {@link detectWorkspace} found around a target directory. */
165
+ export interface WorkspaceDetection {
166
+ /** Absolute path of the directory whose `package.json` declares `workspaces`. */
167
+ readonly rootDir: string;
168
+ /** The root's `workspaces` globs, verbatim. */
169
+ readonly workspaces: readonly string[];
170
+ /** POSIX path of the target relative to {@link rootDir}, e.g. `packages/foo`. */
171
+ readonly targetPath: string;
172
+ /**
173
+ * The `workspaces` glob already covering {@link targetPath}, if any.
174
+ *
175
+ * Absent means the root manifest needs a new entry — which is why this is
176
+ * computed per target rather than assumed: a `packages/*` glob covers a new
177
+ * package for free, while a repo that lists its examples one by one does not.
178
+ */
179
+ readonly coveredBy?: string;
180
+ /** The npm scope the sibling packages agree on (`@acme`), when they agree. */
181
+ readonly scope?: string;
182
+ /**
183
+ * Relative specifier of the tsconfig carrying `compilerOptions`, e.g.
184
+ * `../../tsconfig.base.json`. Absent when the root has none.
185
+ */
186
+ readonly baseTsconfig?: string;
187
+ /** That tsconfig's `compilerOptions`, so the emitted one can drop what it inherits. */
188
+ readonly baseCompilerOptions?: Readonly<Record<string, JsonValue>>;
189
+ /** The root prettier config's `printWidth`, so emitted sources wrap where the repo wraps. */
190
+ readonly printWidth?: number;
191
+ /** Filename of the root eslint config, when there is one. Absent means the repo does not lint. */
192
+ readonly eslintConfig?: string;
193
+ }
194
+
195
+ /**
196
+ * The nearest ancestor of `targetDir` whose `package.json` declares
197
+ * `workspaces`.
198
+ *
199
+ * Nearest rather than outermost: nested workspaces are legal, and the inner one
200
+ * is the manifest that would actually claim the new package.
201
+ */
202
+ export function findWorkspaceRoot(targetDir: string, probe: WorkspaceProbe): string | undefined {
203
+ let current = path.resolve(targetDir);
204
+ for (;;) {
205
+ const manifest = probe.readJson(path.join(current, 'package.json'));
206
+ if (isJsonObject(manifest) && Array.isArray(manifest.workspaces)) {
207
+ return current;
208
+ }
209
+ const parent = path.dirname(current);
210
+ if (parent === current) {
211
+ return undefined;
212
+ }
213
+ current = parent;
214
+ }
215
+ }
216
+
217
+ /** The `workspaces` globs of a detected root, as strings. */
218
+ function readWorkspaces(rootDir: string, probe: WorkspaceProbe): string[] {
219
+ const manifest = probe.readJson(path.join(rootDir, 'package.json'));
220
+ if (!isJsonObject(manifest) || !Array.isArray(manifest.workspaces)) {
221
+ return [];
222
+ }
223
+ return manifest.workspaces.filter((entry): entry is string => typeof entry === 'string');
224
+ }
225
+
226
+ /** Match one path segment against a glob segment, where `*` stands for "any run of non-separator". */
227
+ function segmentMatches(glob: string, segment: string): boolean {
228
+ const source = glob.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^/]*');
229
+ return new RegExp(`^${source}$`).test(segment);
230
+ }
231
+
232
+ /**
233
+ * True when a `workspaces` glob covers a repo-relative path.
234
+ *
235
+ * `**` is treated as covering any run of segments. Deliberately no glob
236
+ * dependency: npm's own set is small, and a wrong answer here only costs a
237
+ * printed hint that turns out to be unnecessary.
238
+ */
239
+ export function workspaceGlobCovers(glob: string, relativePath: string): boolean {
240
+ const globSegments = glob.split('/').filter(segment => segment.length > 0);
241
+ const pathSegments = relativePath.split('/').filter(segment => segment.length > 0);
242
+ const matches = (globIndex: number, pathIndex: number): boolean => {
243
+ if (globIndex === globSegments.length) {
244
+ return pathIndex === pathSegments.length;
245
+ }
246
+ if (globSegments[globIndex] === '**') {
247
+ for (let skip = pathIndex; skip <= pathSegments.length; skip += 1) {
248
+ if (matches(globIndex + 1, skip)) {
249
+ return true;
250
+ }
251
+ }
252
+ return false;
253
+ }
254
+ return (
255
+ pathIndex < pathSegments.length &&
256
+ segmentMatches(globSegments[globIndex], pathSegments[pathIndex]) &&
257
+ matches(globIndex + 1, pathIndex + 1)
258
+ );
259
+ };
260
+ return matches(0, 0);
261
+ }
262
+
263
+ /**
264
+ * Expand a `workspaces` glob to the directories it currently names.
265
+ *
266
+ * `**` is not expanded — it returns nothing rather than walking an unbounded
267
+ * tree, because the only caller is scope inference, which a partial sample
268
+ * answers just as well.
269
+ */
270
+ function expandWorkspaceGlob(rootDir: string, glob: string, probe: WorkspaceProbe): string[] {
271
+ const segments = glob.split('/').filter(segment => segment.length > 0);
272
+ let directories = [rootDir];
273
+ for (const segment of segments) {
274
+ if (segment === '**') {
275
+ return [];
276
+ }
277
+ if (!segment.includes('*')) {
278
+ directories = directories.map(directory => path.join(directory, segment));
279
+ continue;
280
+ }
281
+ directories = directories.flatMap(directory =>
282
+ probe
283
+ .listDirectories(directory)
284
+ .filter(name => segmentMatches(segment, name))
285
+ .map(name => path.join(directory, name))
286
+ );
287
+ }
288
+ return directories;
289
+ }
290
+
291
+ /**
292
+ * The npm scope the existing workspace members agree on.
293
+ *
294
+ * Guessed from siblings and NOT from the root manifest, which in a framework
295
+ * repo is routinely named for the repo rather than the scope it publishes under
296
+ * — so reading it would confidently produce the wrong answer. Requires a
297
+ * unanimous scope: a split verdict is exactly the case where a prompt should
298
+ * ask rather than a heuristic should pick.
299
+ */
300
+ export function inferPackageScope(rootDir: string, workspaces: readonly string[], probe: WorkspaceProbe): string | undefined {
301
+ const scopes = new Set<string>();
302
+ for (const glob of workspaces) {
303
+ for (const directory of expandWorkspaceGlob(rootDir, glob, probe)) {
304
+ const manifest = probe.readJson(path.join(directory, 'package.json'));
305
+ if (!isJsonObject(manifest) || typeof manifest.name !== 'string') {
306
+ continue;
307
+ }
308
+ const scope = manifest.name.startsWith('@') ? manifest.name.split('/')[0] : '';
309
+ scopes.add(scope);
310
+ }
311
+ }
312
+ scopes.delete('');
313
+ return scopes.size === 1 ? [...scopes][0] : undefined;
314
+ }
315
+
316
+ /**
317
+ * The root tsconfig that actually carries `compilerOptions`.
318
+ *
319
+ * `tsconfig.json` at a monorepo root is very often a *solution* file — `files:
320
+ * []` plus `references` — so extending it inherits nothing and silently drops
321
+ * every compiler setting the package meant to pick up. Hence the search is by
322
+ * content, preferring the conventional `tsconfig.base.json` name only as a
323
+ * tie-break, never as the test.
324
+ */
325
+ export function findBaseTsconfig(
326
+ rootDir: string,
327
+ probe: WorkspaceProbe
328
+ ): { readonly file: string; readonly compilerOptions: Readonly<Record<string, JsonValue>> } | undefined {
329
+ const present = probe.listFiles(rootDir).filter(name => /^tsconfig(\..+)?\.json$/.test(name));
330
+ const preferred = ['tsconfig.base.json', 'tsconfig.json'];
331
+ const candidates = [...preferred.filter(name => present.includes(name)), ...present.filter(name => !preferred.includes(name)).sort()];
332
+ for (const file of candidates) {
333
+ const parsed = probe.readJson(path.join(rootDir, file));
334
+ if (isJsonObject(parsed) && isJsonObject(parsed.compilerOptions) && Object.keys(parsed.compilerOptions).length > 0) {
335
+ return { file, compilerOptions: parsed.compilerOptions };
336
+ }
337
+ }
338
+ return undefined;
339
+ }
340
+
341
+ /**
342
+ * The repo's prettier `printWidth`, so the emitted sources wrap where the repo
343
+ * wraps instead of at the scaffold's own default.
344
+ *
345
+ * **Why this is worth detecting at all.** There is no width that is stable for
346
+ * an unknown repo — 80 (prettier's own default), 100, 120 and 140 each produce
347
+ * different wrapping — so emitted code that "passes a formatter check as-is" is
348
+ * only ever true relative to a config. Matching the one we can see is the
349
+ * closest thing available.
350
+ *
351
+ * **A regex over the file, deliberately, and only for the module forms.** The
352
+ * JSON forms are parsed properly; `.prettierrc.js` and `prettier.config.mjs` are
353
+ * executable modules that no scaffold should evaluate, and skipping them would
354
+ * miss the configs most repos actually use. The pattern takes the FIRST
355
+ * `printWidth`, which is the top-level one in every config laid out
356
+ * conventionally — an `overrides` entry appearing before it would win, and that
357
+ * is the known limit.
358
+ *
359
+ * Getting it wrong costs exactly what getting it absent costs today: the
360
+ * adopter's formatter rewraps on first commit. So a heuristic that is usually
361
+ * right strictly improves on no detection, and can break nothing.
362
+ */
363
+ export function findPrettierPrintWidth(rootDir: string, probe: WorkspaceProbe): number | undefined {
364
+ const manifest = probe.readJson(path.join(rootDir, 'package.json'));
365
+ if (isJsonObject(manifest) && isJsonObject(manifest.prettier) && typeof manifest.prettier.printWidth === 'number') {
366
+ return manifest.prettier.printWidth;
367
+ }
368
+ const present = probe.listFiles(rootDir);
369
+ for (const file of present.filter(name => name === '.prettierrc' || name === '.prettierrc.json')) {
370
+ const parsed = probe.readJson(path.join(rootDir, file));
371
+ if (isJsonObject(parsed) && typeof parsed.printWidth === 'number') {
372
+ return parsed.printWidth;
373
+ }
374
+ }
375
+ for (const file of present.filter(name => /^(\.prettierrc\.[cm]?[jt]s|prettier\.config\.[cm]?[jt]s)$/.test(name))) {
376
+ const width = /\bprintWidth\s*:\s*(\d+)/.exec(probe.readText(path.join(rootDir, file)) ?? '');
377
+ if (width) {
378
+ return Number(width[1]);
379
+ }
380
+ }
381
+ return undefined;
382
+ }
383
+
384
+ /**
385
+ * The root eslint config's filename, when the repo has one.
386
+ *
387
+ * Read to decide whether the scaffold emits a `lint` script. The failure it
388
+ * prevents is silent in the direction that matters: a task runner runs a script
389
+ * only where one is declared, so a package without `lint` is SKIPPED rather than
390
+ * reported, and the absence reads as a clean lint. Emitting the script into a
391
+ * repo whose lint invocation differs costs one visible line to edit.
392
+ *
393
+ * Both config generations, because detecting a repo that has not migrated off
394
+ * `.eslintrc` is free and the question asked here — does this repo lint? — is
395
+ * the same either way.
396
+ */
397
+ export function findEslintConfig(rootDir: string, probe: WorkspaceProbe): string | undefined {
398
+ return probe.listFiles(rootDir).find(name => /^eslint\.config\.[cm]?[jt]s$/.test(name) || /^\.eslintrc(\..+)?$/.test(name));
399
+ }
400
+
401
+ /** POSIX-style relative path, which is what both `extends` and `--prefix` want on every platform. */
402
+ function posixRelative(from: string, to: string): string {
403
+ return path.relative(from, to).split(path.sep).join('/');
404
+ }
405
+
406
+ /**
407
+ * Detect the workspace `targetDir` would join. `undefined` when no ancestor
408
+ * declares `workspaces`, which is the standalone case and not an error here —
409
+ * `runInit` decides whether that is fatal.
410
+ *
411
+ * The target is resolved against the process's working directory, so a relative
412
+ * one is relative to where the command was RUN, not to anything the scaffolder
413
+ * picks. That matters because the two are routinely different directories.
414
+ */
415
+ export function detectWorkspace(targetDir: string, probe: WorkspaceProbe): WorkspaceDetection | undefined {
416
+ const absoluteTarget = path.resolve(targetDir);
417
+ const rootDir = findWorkspaceRoot(absoluteTarget, probe);
418
+ if (rootDir === undefined) {
419
+ return undefined;
420
+ }
421
+ const workspaces = readWorkspaces(rootDir, probe);
422
+ const targetPath = posixRelative(rootDir, absoluteTarget);
423
+ // The target IS the workspace root — `init .` at the top of a monorepo, or a
424
+ // target that resolved to the cwd because it was empty. There is no member to
425
+ // place, so reporting a placement here describes a scaffold that cannot
426
+ // happen and names a directory the user never asked for.
427
+ if (targetPath === '') {
428
+ return undefined;
429
+ }
430
+ const base = findBaseTsconfig(rootDir, probe);
431
+ return {
432
+ rootDir,
433
+ workspaces,
434
+ targetPath,
435
+ coveredBy: workspaces.find(glob => workspaceGlobCovers(glob, targetPath)),
436
+ scope: inferPackageScope(rootDir, workspaces, probe),
437
+ baseTsconfig: base && posixRelative(absoluteTarget, path.join(rootDir, base.file)),
438
+ baseCompilerOptions: base?.compilerOptions,
439
+ printWidth: findPrettierPrintWidth(rootDir, probe),
440
+ eslintConfig: findEslintConfig(rootDir, probe)
441
+ };
442
+ }