@topogram/cli 0.3.65 → 0.3.67

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 (67) hide show
  1. package/package.json +1 -1
  2. package/src/adoption/plan/index.js +21 -8
  3. package/src/adoption/reporting.js +1 -1
  4. package/src/agent-brief.js +7 -21
  5. package/src/agent-ops/query-builders/change-risk/review-packets.js +2 -2
  6. package/src/agent-ops/query-builders/common.js +2 -2
  7. package/src/agent-ops/query-builders/multi-agent.js +1 -1
  8. package/src/agent-ops/query-builders/workflow-presets-core.js +3 -2
  9. package/src/archive/jsonl.js +2 -2
  10. package/src/archive/resolver-bridge.js +1 -1
  11. package/src/archive/unarchive.js +2 -1
  12. package/src/catalog/copy.js +11 -6
  13. package/src/catalog/provenance.js +2 -1
  14. package/src/cli/command-parsers/project.js +3 -0
  15. package/src/cli/command-parsers/shared.js +1 -1
  16. package/src/cli/commands/agent.js +2 -2
  17. package/src/cli/commands/check.js +3 -3
  18. package/src/cli/commands/doctor.js +2 -9
  19. package/src/cli/commands/generator-policy/runner.js +1 -1
  20. package/src/cli/commands/import/help.js +2 -2
  21. package/src/cli/commands/import/paths.js +3 -11
  22. package/src/cli/commands/import/plan.js +9 -1
  23. package/src/cli/commands/import/refresh.js +7 -6
  24. package/src/cli/commands/import/workspace.js +8 -5
  25. package/src/cli/commands/migrate.js +153 -0
  26. package/src/cli/commands/query/definitions.js +10 -10
  27. package/src/cli/commands/query/workspace.js +2 -6
  28. package/src/cli/commands/source.js +3 -12
  29. package/src/cli/commands/template/check.js +6 -5
  30. package/src/cli/commands/template-runner.js +6 -6
  31. package/src/cli/commands/trust.js +1 -1
  32. package/src/cli/commands/workflow.js +6 -1
  33. package/src/cli/dispatcher.js +6 -1
  34. package/src/cli/help.js +15 -14
  35. package/src/cli/migration-guidance.js +1 -1
  36. package/src/cli/output-safety.js +2 -1
  37. package/src/cli/path-normalization.js +3 -13
  38. package/src/generator/context/domain-page.js +1 -1
  39. package/src/generator/context/shared/maintained-boundary.js +2 -2
  40. package/src/generator/context/shared/metrics.js +2 -2
  41. package/src/generator/context/task-mode.js +2 -2
  42. package/src/generator/sdlc/doc-page.js +1 -1
  43. package/src/generator/surfaces/databases/lifecycle-shared.js +1 -1
  44. package/src/generator/surfaces/native/swiftui-templates/README.generated.md +1 -1
  45. package/src/import/core/context.js +5 -7
  46. package/src/import/core/runner/candidates.js +123 -3
  47. package/src/import/core/runner/reports.js +4 -3
  48. package/src/import/core/runner/ui-drafts.js +58 -2
  49. package/src/new-project/constants.js +1 -1
  50. package/src/new-project/create.js +9 -2
  51. package/src/new-project/project-files.js +16 -13
  52. package/src/new-project/template-resolution.js +6 -4
  53. package/src/new-project/template-snapshots.js +38 -8
  54. package/src/new-project/template-updates.js +1 -1
  55. package/src/project-config/index.js +27 -0
  56. package/src/sdlc/adopt.js +6 -5
  57. package/src/sdlc/paths.js +3 -5
  58. package/src/sdlc/scaffold.js +2 -1
  59. package/src/workflows/reconcile/adoption-plan/build.js +7 -3
  60. package/src/workflows/reconcile/adoption-plan/outputs.js +12 -2
  61. package/src/workflows/reconcile/adoption-plan/paths.js +1 -1
  62. package/src/workflows/reconcile/candidate-model.js +18 -2
  63. package/src/workflows/reconcile/impacts/adoption-plan.js +6 -2
  64. package/src/workflows/reconcile/impacts/indexes.js +5 -1
  65. package/src/workflows/reconcile/renderers.js +41 -6
  66. package/src/workflows/shared.js +5 -11
  67. package/src/workspace-paths.js +328 -0
@@ -0,0 +1,328 @@
1
+ // @ts-check
2
+
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+
6
+ export const DEFAULT_TOPO_FOLDER_NAME = "topo";
7
+ export const LEGACY_TOPOGRAM_FOLDER_NAME = "topogram";
8
+ export const DEFAULT_WORKSPACE_PATH = `./${DEFAULT_TOPO_FOLDER_NAME}`;
9
+ export const PROJECT_CONFIG_FILE = "topogram.project.json";
10
+
11
+ const SIGNAL_SCAN_IGNORED_DIRS = new Set([
12
+ ".git",
13
+ ".next",
14
+ ".tmp",
15
+ ".turbo",
16
+ ".yarn",
17
+ "app",
18
+ "build",
19
+ "coverage",
20
+ "dist",
21
+ "expected",
22
+ "node_modules",
23
+ "tmp"
24
+ ]);
25
+ const WORKSPACE_SIGNAL_DIRS = new Set([
26
+ "_archive",
27
+ "acceptance_criteria",
28
+ "actors",
29
+ "bugs",
30
+ "capabilities",
31
+ "decisions",
32
+ "domains",
33
+ "entities",
34
+ "enums",
35
+ "operations",
36
+ "pitches",
37
+ "projections",
38
+ "requirements",
39
+ "rules",
40
+ "shapes",
41
+ "tasks",
42
+ "terms",
43
+ "verifications",
44
+ "widgets",
45
+ "workflows"
46
+ ]);
47
+
48
+ /**
49
+ * @typedef {Object} WorkspaceResolution
50
+ * @property {string} inputRoot
51
+ * @property {string} topoRoot
52
+ * @property {string} projectRoot
53
+ * @property {string|null} configPath
54
+ * @property {boolean} fromConfig
55
+ * @property {boolean} fromSignal
56
+ * @property {boolean} bootstrappedTopoRoot
57
+ */
58
+
59
+ /**
60
+ * @param {string} candidatePath
61
+ * @returns {boolean}
62
+ */
63
+ function isDirectory(candidatePath) {
64
+ try {
65
+ return fs.existsSync(candidatePath) && fs.statSync(candidatePath).isDirectory();
66
+ } catch {
67
+ return false;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * @param {string} filePath
73
+ * @returns {any}
74
+ */
75
+ function readJson(filePath) {
76
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
77
+ }
78
+
79
+ /**
80
+ * @param {string} startPath
81
+ * @returns {string}
82
+ */
83
+ function searchStartDirectory(startPath) {
84
+ const absolute = path.resolve(startPath || ".");
85
+ if (fs.existsSync(absolute) && fs.statSync(absolute).isFile()) {
86
+ return path.dirname(absolute);
87
+ }
88
+ if (!fs.existsSync(absolute) && path.basename(absolute) === DEFAULT_TOPO_FOLDER_NAME) {
89
+ return path.dirname(absolute);
90
+ }
91
+ return absolute;
92
+ }
93
+
94
+ /**
95
+ * @param {string} startPath
96
+ * @returns {{ config: any, configPath: string, configDir: string }|null}
97
+ */
98
+ export function findProjectRoot(startPath) {
99
+ let current = searchStartDirectory(startPath);
100
+ while (current && current !== path.dirname(current)) {
101
+ const candidate = path.join(current, PROJECT_CONFIG_FILE);
102
+ if (fs.existsSync(candidate)) {
103
+ return {
104
+ config: readJson(candidate),
105
+ configPath: candidate,
106
+ configDir: current
107
+ };
108
+ }
109
+ current = path.dirname(current);
110
+ }
111
+ return null;
112
+ }
113
+
114
+ /**
115
+ * @param {string} workspacePath
116
+ * @returns {string}
117
+ */
118
+ export function normalizeWorkspaceConfigPath(workspacePath) {
119
+ const value = String(workspacePath || "").trim();
120
+ if (!value) {
121
+ throw new Error("topogram.project.json workspace must be a non-empty relative path.");
122
+ }
123
+ if (path.isAbsolute(value)) {
124
+ throw new Error("topogram.project.json workspace must be relative to the project root.");
125
+ }
126
+ const normalized = value.replace(/\\/g, "/");
127
+ const resolved = path.posix.normalize(normalized);
128
+ if (resolved === ".." || resolved.startsWith("../")) {
129
+ throw new Error("topogram.project.json workspace must not escape the project root.");
130
+ }
131
+ return normalized;
132
+ }
133
+
134
+ /**
135
+ * @param {any} config
136
+ * @param {string} configDir
137
+ * @returns {string}
138
+ */
139
+ export function resolveProjectWorkspace(config, configDir) {
140
+ if (config && Object.prototype.hasOwnProperty.call(config, "workspaces")) {
141
+ throw new Error("topogram.project.json workspaces[] is not supported yet; use single workspace instead.");
142
+ }
143
+ const configured = config?.workspace == null ? DEFAULT_WORKSPACE_PATH : config.workspace;
144
+ const normalized = normalizeWorkspaceConfigPath(configured);
145
+ return path.resolve(configDir, normalized);
146
+ }
147
+
148
+ /**
149
+ * @param {string} root
150
+ * @param {number} [maxDepth]
151
+ * @returns {boolean}
152
+ */
153
+ export function workspaceHasTgFiles(root, maxDepth = 3) {
154
+ if (!isDirectory(root)) {
155
+ return false;
156
+ }
157
+ const walk = (/** @type {string} */ current, /** @type {number} */ depth) => {
158
+ if (depth > maxDepth) {
159
+ return false;
160
+ }
161
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
162
+ if (SIGNAL_SCAN_IGNORED_DIRS.has(entry.name)) {
163
+ continue;
164
+ }
165
+ const child = path.join(current, entry.name);
166
+ if (entry.isFile() && entry.name.endsWith(".tg")) {
167
+ return true;
168
+ }
169
+ if (entry.isDirectory() && walk(child, depth + 1)) {
170
+ return true;
171
+ }
172
+ }
173
+ return false;
174
+ };
175
+ return walk(root, 0);
176
+ }
177
+
178
+ /**
179
+ * @param {string} root
180
+ * @returns {boolean}
181
+ */
182
+ function isWorkspaceSignalRoot(root) {
183
+ if (!isDirectory(root)) {
184
+ return false;
185
+ }
186
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
187
+ if (entry.isFile() && entry.name.endsWith(".tg")) {
188
+ return true;
189
+ }
190
+ if (entry.isDirectory() && WORKSPACE_SIGNAL_DIRS.has(entry.name) && workspaceHasTgFiles(path.join(root, entry.name), 2)) {
191
+ return true;
192
+ }
193
+ }
194
+ return false;
195
+ }
196
+
197
+ /**
198
+ * @param {string} root
199
+ * @returns {string[]}
200
+ */
201
+ function signalWorkspaceCandidates(root) {
202
+ if (!isDirectory(root)) {
203
+ return [];
204
+ }
205
+ /** @type {string[]} */
206
+ const candidates = [];
207
+ if (isWorkspaceSignalRoot(root)) {
208
+ candidates.push(root);
209
+ }
210
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
211
+ if (!entry.isDirectory() || SIGNAL_SCAN_IGNORED_DIRS.has(entry.name)) {
212
+ continue;
213
+ }
214
+ const child = path.join(root, entry.name);
215
+ if (isWorkspaceSignalRoot(child)) {
216
+ candidates.push(child);
217
+ }
218
+ }
219
+ return [...new Set(candidates.map((candidate) => path.resolve(candidate)))].sort();
220
+ }
221
+
222
+ /**
223
+ * @param {string} inputPath
224
+ * @returns {WorkspaceResolution}
225
+ */
226
+ export function resolveWorkspaceContext(inputPath = ".") {
227
+ const absolute = path.resolve(inputPath || ".");
228
+ if (
229
+ isDirectory(absolute) &&
230
+ (
231
+ path.basename(absolute) === DEFAULT_TOPO_FOLDER_NAME ||
232
+ (isWorkspaceSignalRoot(absolute) && !isDirectory(path.join(absolute, DEFAULT_TOPO_FOLDER_NAME)))
233
+ )
234
+ ) {
235
+ return {
236
+ inputRoot: absolute,
237
+ topoRoot: absolute,
238
+ projectRoot: path.basename(absolute) === DEFAULT_TOPO_FOLDER_NAME ? path.dirname(absolute) : absolute,
239
+ configPath: null,
240
+ fromConfig: false,
241
+ fromSignal: false,
242
+ bootstrappedTopoRoot: false
243
+ };
244
+ }
245
+
246
+ const configInfo = findProjectRoot(absolute);
247
+ if (configInfo) {
248
+ const topoRoot = resolveProjectWorkspace(configInfo.config, configInfo.configDir);
249
+ return {
250
+ inputRoot: absolute,
251
+ topoRoot,
252
+ projectRoot: configInfo.configDir,
253
+ configPath: configInfo.configPath,
254
+ fromConfig: true,
255
+ fromSignal: false,
256
+ bootstrappedTopoRoot: !fs.existsSync(topoRoot)
257
+ };
258
+ }
259
+
260
+ const searchBase = !fs.existsSync(absolute) && path.basename(absolute) === DEFAULT_TOPO_FOLDER_NAME
261
+ ? path.dirname(absolute)
262
+ : absolute;
263
+ const defaultCandidate = path.join(searchBase, DEFAULT_TOPO_FOLDER_NAME);
264
+ if (isDirectory(defaultCandidate)) {
265
+ return {
266
+ inputRoot: absolute,
267
+ topoRoot: defaultCandidate,
268
+ projectRoot: searchBase,
269
+ configPath: null,
270
+ fromConfig: false,
271
+ fromSignal: false,
272
+ bootstrappedTopoRoot: false
273
+ };
274
+ }
275
+
276
+ const signalCandidates = signalWorkspaceCandidates(searchBase);
277
+ if (signalCandidates.length === 1) {
278
+ const topoRoot = signalCandidates[0];
279
+ return {
280
+ inputRoot: absolute,
281
+ topoRoot,
282
+ projectRoot: topoRoot,
283
+ configPath: null,
284
+ fromConfig: false,
285
+ fromSignal: true,
286
+ bootstrappedTopoRoot: false
287
+ };
288
+ }
289
+ if (signalCandidates.length > 1) {
290
+ throw new Error(
291
+ `Multiple Topogram workspace candidates found. Pass one explicitly: ${signalCandidates.join(", ")}`
292
+ );
293
+ }
294
+
295
+ return {
296
+ inputRoot: absolute,
297
+ topoRoot: defaultCandidate,
298
+ projectRoot: searchBase,
299
+ configPath: null,
300
+ fromConfig: false,
301
+ fromSignal: false,
302
+ bootstrappedTopoRoot: true
303
+ };
304
+ }
305
+
306
+ /**
307
+ * @param {string} inputPath
308
+ * @returns {string}
309
+ */
310
+ export function resolveTopoRoot(inputPath = ".") {
311
+ return resolveWorkspaceContext(inputPath).topoRoot;
312
+ }
313
+
314
+ /**
315
+ * @param {string} packageRoot
316
+ * @returns {{ root: string, legacy: boolean }}
317
+ */
318
+ export function resolvePackageWorkspace(packageRoot) {
319
+ const topoRoot = path.join(packageRoot, DEFAULT_TOPO_FOLDER_NAME);
320
+ if (isDirectory(topoRoot)) {
321
+ return { root: topoRoot, legacy: false };
322
+ }
323
+ const legacyRoot = path.join(packageRoot, LEGACY_TOPOGRAM_FOLDER_NAME);
324
+ if (isDirectory(legacyRoot)) {
325
+ return { root: legacyRoot, legacy: true };
326
+ }
327
+ throw new Error(`Package is missing ${DEFAULT_TOPO_FOLDER_NAME}/.`);
328
+ }