@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.
- package/package.json +1 -1
- package/src/adoption/plan/index.js +21 -8
- package/src/adoption/reporting.js +1 -1
- package/src/agent-brief.js +7 -21
- package/src/agent-ops/query-builders/change-risk/review-packets.js +2 -2
- package/src/agent-ops/query-builders/common.js +2 -2
- package/src/agent-ops/query-builders/multi-agent.js +1 -1
- package/src/agent-ops/query-builders/workflow-presets-core.js +3 -2
- package/src/archive/jsonl.js +2 -2
- package/src/archive/resolver-bridge.js +1 -1
- package/src/archive/unarchive.js +2 -1
- package/src/catalog/copy.js +11 -6
- package/src/catalog/provenance.js +2 -1
- package/src/cli/command-parsers/project.js +3 -0
- package/src/cli/command-parsers/shared.js +1 -1
- package/src/cli/commands/agent.js +2 -2
- package/src/cli/commands/check.js +3 -3
- package/src/cli/commands/doctor.js +2 -9
- package/src/cli/commands/generator-policy/runner.js +1 -1
- package/src/cli/commands/import/help.js +2 -2
- package/src/cli/commands/import/paths.js +3 -11
- package/src/cli/commands/import/plan.js +9 -1
- package/src/cli/commands/import/refresh.js +7 -6
- package/src/cli/commands/import/workspace.js +8 -5
- package/src/cli/commands/migrate.js +153 -0
- package/src/cli/commands/query/definitions.js +10 -10
- package/src/cli/commands/query/workspace.js +2 -6
- package/src/cli/commands/source.js +3 -12
- package/src/cli/commands/template/check.js +6 -5
- package/src/cli/commands/template-runner.js +6 -6
- package/src/cli/commands/trust.js +1 -1
- package/src/cli/commands/workflow.js +6 -1
- package/src/cli/dispatcher.js +6 -1
- package/src/cli/help.js +15 -14
- package/src/cli/migration-guidance.js +1 -1
- package/src/cli/output-safety.js +2 -1
- package/src/cli/path-normalization.js +3 -13
- package/src/generator/context/domain-page.js +1 -1
- package/src/generator/context/shared/maintained-boundary.js +2 -2
- package/src/generator/context/shared/metrics.js +2 -2
- package/src/generator/context/task-mode.js +2 -2
- package/src/generator/sdlc/doc-page.js +1 -1
- package/src/generator/surfaces/databases/lifecycle-shared.js +1 -1
- package/src/generator/surfaces/native/swiftui-templates/README.generated.md +1 -1
- package/src/import/core/context.js +5 -7
- package/src/import/core/runner/candidates.js +123 -3
- package/src/import/core/runner/reports.js +4 -3
- package/src/import/core/runner/ui-drafts.js +58 -2
- package/src/new-project/constants.js +1 -1
- package/src/new-project/create.js +9 -2
- package/src/new-project/project-files.js +16 -13
- package/src/new-project/template-resolution.js +6 -4
- package/src/new-project/template-snapshots.js +38 -8
- package/src/new-project/template-updates.js +1 -1
- package/src/project-config/index.js +27 -0
- package/src/sdlc/adopt.js +6 -5
- package/src/sdlc/paths.js +3 -5
- package/src/sdlc/scaffold.js +2 -1
- package/src/workflows/reconcile/adoption-plan/build.js +7 -3
- package/src/workflows/reconcile/adoption-plan/outputs.js +12 -2
- package/src/workflows/reconcile/adoption-plan/paths.js +1 -1
- package/src/workflows/reconcile/candidate-model.js +18 -2
- package/src/workflows/reconcile/impacts/adoption-plan.js +6 -2
- package/src/workflows/reconcile/impacts/indexes.js +5 -1
- package/src/workflows/reconcile/renderers.js +41 -6
- package/src/workflows/shared.js +5 -11
- 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
|
+
}
|