@topogram/cli 0.3.70 → 0.3.72

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topogram/cli",
3
- "version": "0.3.70",
3
+ "version": "0.3.72",
4
4
  "description": "Topogram CLI for checking Topogram workspaces and generating app bundles.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -28,7 +28,7 @@ import { DEFAULT_TOPO_FOLDER_NAME, resolveTopoRoot, resolveWorkspaceContext } fr
28
28
  * @typedef {{ path: string, ownership: string, rule: string }} AgentBriefOutputBoundary
29
29
  * @typedef {{ id: string, title: string, commands: string[], rule: string }} AgentBriefWorkflow
30
30
  * @typedef {{ id: string, kind: string, projection: string|null, generator: string|null, uses_api: string|null, uses_database: string|null }} AgentBriefRuntime
31
- * @typedef {{ path: string, source: string|null, tracks: string[], candidateCounts: Record<string, any>, ownership: string|null }} AgentBriefImport
31
+ * @typedef {{ path: string, workspaceRoot: string, source: string|null, tracks: string[], candidateCounts: Record<string, any>, ownership: string|null }} AgentBriefImport
32
32
  * @typedef {{ ok: true, payload: Record<string, any> } | { ok: false, kind: "topogram", validation: any } | { ok: false, kind: "project", validation: any, configPath: string }} AgentBriefResult
33
33
  */
34
34
 
@@ -137,6 +137,7 @@ function readImportSummary(projectRoot) {
137
137
  const record = JSON.parse(fs.readFileSync(importPath, "utf8"));
138
138
  return {
139
139
  path: TOPOGRAM_IMPORT_FILE,
140
+ workspaceRoot: resolveTopoRoot(projectRoot),
140
141
  source: typeof record?.source?.path === "string" ? record.source.path : null,
141
142
  tracks: Array.isArray(record?.import?.tracks) ? record.import.tracks.map(String) : [],
142
143
  candidateCounts: record?.import?.candidateCounts && typeof record.import.candidateCounts === "object"
@@ -147,6 +148,7 @@ function readImportSummary(projectRoot) {
147
148
  } catch (error) {
148
149
  return {
149
150
  path: TOPOGRAM_IMPORT_FILE,
151
+ workspaceRoot: resolveTopoRoot(projectRoot),
150
152
  source: null,
151
153
  tracks: [],
152
154
  candidateCounts: {},
@@ -215,13 +217,13 @@ function buildWorkflows(config, hasImportRecord) {
215
217
  id: "brownfield-import",
216
218
  title: "Brownfield import adoption loop",
217
219
  commands: [
218
- "topogram import check .",
219
- "topogram import plan .",
220
- "topogram import adopt --list .",
221
- "topogram import status .",
222
- "topogram import history . --verify"
220
+ "topogram import check . --json",
221
+ "topogram import plan . --json",
222
+ "topogram import adopt --list . --json",
223
+ "topogram import status . --json",
224
+ "topogram import history . --verify --json"
223
225
  ],
224
- rule: "Imported Topogram files are editable after adoption; source hashes record trusted import evidence."
226
+ rule: "Imported Topogram files are editable after adoption; JSON automation should read workspaceRoot for the project-owned workspace path."
225
227
  });
226
228
  }
227
229
  return workflows;
@@ -338,10 +340,11 @@ export function buildAgentBrief(inputPath, workspaceAst) {
338
340
  commandItem("npm run generate", "Write generated-owned runtime/app outputs after validation.", "write"),
339
341
  commandItem("npm run verify", "Run generated output verification.", "verify"),
340
342
  ...(importSummary ? [
341
- commandItem("topogram import check .", "Validate imported workspace provenance.", "import"),
342
- commandItem("topogram import plan .", "Review import adoption plan.", "import"),
343
- commandItem("topogram import adopt --list .", "List reviewable adoption selectors.", "import"),
344
- commandItem("topogram import history . --verify", "Verify import history evidence.", "import")
343
+ commandItem("topogram import check . --json", "Validate imported workspace provenance and read workspaceRoot.", "import"),
344
+ commandItem("topogram import plan . --json", "Review import adoption plan and workspaceRoot.", "import"),
345
+ commandItem("topogram import adopt --list . --json", "List reviewable adoption selectors.", "import"),
346
+ commandItem("topogram import status . --json", "Check import/adoption status.", "import"),
347
+ commandItem("topogram import history . --verify --json", "Verify import history evidence.", "import")
345
348
  ] : [])
346
349
  ];
347
350
 
@@ -461,6 +464,12 @@ export function formatAgentBrief(brief) {
461
464
  for (const workflow of brief.workflows || []) {
462
465
  lines.push(` - ${workflow.title}: ${workflow.rule}`);
463
466
  }
467
+ if (brief.import?.workspaceRoot) {
468
+ lines.push("");
469
+ lines.push("Import:");
470
+ lines.push(` - Workspace root: ${brief.import.workspaceRoot}`);
471
+ lines.push(" - JSON import commands expose workspaceRoot; prefer it over compatibility fields.");
472
+ }
464
473
  lines.push("");
465
474
  lines.push("Verification gates:");
466
475
  lines.push(" - npm run check");
@@ -43,8 +43,5 @@ export function parseProjectCommandArgs(args) {
43
43
  if (args[0] === "package" && args[1] === "update-cli") {
44
44
  return { packageCommand: "update-cli", inputPath: args.includes("--latest") ? "latest" : args[2] };
45
45
  }
46
- if (args[0] === "migrate" && args[1] === "workspace-folder") {
47
- return { migrateCommand: "workspace-folder", inputPath: commandPath(args, 2, ".") };
48
- }
49
46
  return null;
50
47
  }
@@ -50,6 +50,7 @@ export function buildImportAdoptionReceipt({ artifacts, selector, options, impor
50
50
  version: readInstalledCliPackageVersion()
51
51
  },
52
52
  projectRoot: artifacts.projectRoot,
53
+ workspaceRoot: artifacts.topogramRoot,
53
54
  topogramRoot: artifacts.topogramRoot,
54
55
  selector,
55
56
  mode: "write",
@@ -114,6 +115,7 @@ export function buildBrownfieldImportAdoptPayload(selector, inputPath, options =
114
115
  return {
115
116
  ok: true,
116
117
  projectRoot: artifacts.projectRoot,
118
+ workspaceRoot: artifacts.topogramRoot,
117
119
  topogramRoot: artifacts.topogramRoot,
118
120
  selector,
119
121
  dryRun: !options.write,
@@ -12,7 +12,8 @@ import { buildTopogramImportStatus } from "../../../import/provenance.js";
12
12
  import {
13
13
  checkSummaryPayload,
14
14
  combineProjectValidationResults,
15
- normalizeProjectRoot
15
+ normalizeProjectRoot,
16
+ normalizeTopogramPath
16
17
  } from "./paths.js";
17
18
 
18
19
  /**
@@ -39,7 +40,7 @@ export function buildTopogramCheckPayloadForPath(inputPath) {
39
40
 
40
41
  /**
41
42
  * @param {string} projectRoot
42
- * @returns {{ ok: boolean, projectRoot: string, import: ReturnType<typeof buildTopogramImportStatus>, topogram: ReturnType<typeof buildTopogramCheckPayloadForPath>, errors: any[] }}
43
+ * @returns {{ ok: boolean, projectRoot: string, workspaceRoot: string, import: ReturnType<typeof buildTopogramImportStatus>, topogram: ReturnType<typeof buildTopogramCheckPayloadForPath>, errors: any[] }}
43
44
  */
44
45
  export function buildBrownfieldImportCheckPayload(projectRoot) {
45
46
  const resolvedRoot = normalizeProjectRoot(projectRoot);
@@ -48,6 +49,7 @@ export function buildBrownfieldImportCheckPayload(projectRoot) {
48
49
  return {
49
50
  ok: importStatus.ok && topogramCheck.ok,
50
51
  projectRoot: resolvedRoot,
52
+ workspaceRoot: normalizeTopogramPath(resolvedRoot),
51
53
  import: importStatus,
52
54
  topogram: topogramCheck,
53
55
  errors: [
@@ -17,6 +17,7 @@ export function buildBrownfieldImportDiffPayload(inputPath, options = {}) {
17
17
  return {
18
18
  ok: true,
19
19
  projectRoot: analysis.projectRoot,
20
+ workspaceRoot: analysis.topogramRoot,
20
21
  topogramRoot: analysis.topogramRoot,
21
22
  sourcePath: analysis.sourcePath,
22
23
  provenancePath: analysis.provenancePath,
@@ -191,6 +191,7 @@ export function buildBrownfieldImportPlanPayload(inputPath) {
191
191
  return {
192
192
  ok: true,
193
193
  projectRoot: artifacts.projectRoot,
194
+ workspaceRoot: artifacts.topogramRoot,
194
195
  topogramRoot: artifacts.topogramRoot,
195
196
  artifacts: {
196
197
  adoptionPlan: artifacts.paths.adoptionPlanAgent,
@@ -254,6 +255,7 @@ export function buildBrownfieldImportAdoptListPayload(inputPath) {
254
255
  return {
255
256
  ok: true,
256
257
  projectRoot: plan.projectRoot,
258
+ workspaceRoot: plan.topogramRoot,
257
259
  topogramRoot: plan.topogramRoot,
258
260
  selectorCount: selectors.length,
259
261
  selectors,
@@ -354,7 +354,7 @@ export function buildBrownfieldImportRefreshAnalysis(inputPath, options = {}) {
354
354
  /**
355
355
  * @param {string} inputPath
356
356
  * @param {{ sourcePath?: string|null, dryRun?: boolean }} [options]
357
- * @returns {{ ok: boolean, dryRun: boolean, projectRoot: string, topogramRoot: string, sourcePath: string, provenancePath: string, previousImportStatus: string, currentImportStatus: string, tracks: string[], sourceFiles: number, sourceDiff: Record<string, any>, removedCandidateFiles: Record<string, number>, rawCandidateFiles: number, reconcileFiles: number, writtenFiles: string[], plannedFiles: string[], candidateCounts: Record<string, number>, candidateCountDeltas: Record<string, any>, adoptionPlanDeltas: Record<string, any>, receiptVerification: Record<string, any>, refreshMetadata: Record<string, any>|null, nextCommands: string[] }}
357
+ * @returns {{ ok: boolean, dryRun: boolean, projectRoot: string, workspaceRoot: string, topogramRoot: string, sourcePath: string, provenancePath: string, previousImportStatus: string, currentImportStatus: string, tracks: string[], sourceFiles: number, sourceDiff: Record<string, any>, removedCandidateFiles: Record<string, number>, rawCandidateFiles: number, reconcileFiles: number, writtenFiles: string[], plannedFiles: string[], candidateCounts: Record<string, number>, candidateCountDeltas: Record<string, any>, adoptionPlanDeltas: Record<string, any>, receiptVerification: Record<string, any>, refreshMetadata: Record<string, any>|null, nextCommands: string[] }}
358
358
  */
359
359
  export function buildBrownfieldImportRefreshPayload(inputPath, options = {}) {
360
360
  const analysis = buildBrownfieldImportRefreshAnalysis(inputPath, options);
@@ -405,6 +405,7 @@ export function buildBrownfieldImportRefreshPayload(inputPath, options = {}) {
405
405
  ok: dryRun || currentImportStatus === "clean",
406
406
  dryRun,
407
407
  projectRoot: analysis.projectRoot,
408
+ workspaceRoot: analysis.topogramRoot,
408
409
  topogramRoot: analysis.topogramRoot,
409
410
  sourcePath: analysis.sourcePath,
410
411
  provenancePath,
@@ -31,6 +31,7 @@ export function buildBrownfieldImportStatusPayload(inputPath) {
31
31
  return {
32
32
  ok: importCheck.ok,
33
33
  projectRoot: artifacts.projectRoot,
34
+ workspaceRoot: artifacts.topogramRoot,
34
35
  topogramRoot: artifacts.topogramRoot,
35
36
  import: importCheck.import,
36
37
  topogram: importCheck.topogram,
@@ -144,6 +145,7 @@ export function verifyImportAdoptionReceipts(projectRoot, receipts) {
144
145
  */
145
146
  export function buildBrownfieldImportHistoryPayload(inputPath, options = {}) {
146
147
  const projectRoot = normalizeProjectRoot(inputPath);
148
+ const workspaceRoot = normalizeTopogramPath(projectRoot);
147
149
  const historyPath = importAdoptionsPath(projectRoot);
148
150
  const receipts = readImportAdoptionReceipts(projectRoot);
149
151
  const forcedWrites = receipts.filter((receipt) => receipt.forced);
@@ -151,6 +153,7 @@ export function buildBrownfieldImportHistoryPayload(inputPath, options = {}) {
151
153
  return {
152
154
  ok: true,
153
155
  projectRoot,
156
+ workspaceRoot,
154
157
  path: historyPath,
155
158
  exists: fs.existsSync(historyPath),
156
159
  verified: Boolean(options.verify),
@@ -162,7 +165,8 @@ export function buildBrownfieldImportHistoryPayload(inputPath, options = {}) {
162
165
  lastSelector: receipts[receipts.length - 1]?.selector || null
163
166
  },
164
167
  verification,
165
- receipts
168
+ receipts,
169
+ entries: receipts
166
170
  };
167
171
  }
168
172
 
@@ -145,7 +145,7 @@ export function countFilesRecursive(rootPath) {
145
145
  * @param {string} sourcePath
146
146
  * @param {string} targetPath
147
147
  * @param {{ from?: string|null }} [options]
148
- * @returns {{ ok: boolean, sourcePath: string, targetPath: string, topogramRoot: string, projectConfigPath: string, provenancePath: string, tracks: string[], sourceFiles: number, rawCandidateFiles: number, reconcileFiles: number, writtenFiles: string[], candidateCounts: Record<string, number>, nextCommands: string[] }}
148
+ * @returns {{ ok: boolean, sourcePath: string, targetPath: string, workspaceRoot: string, topogramRoot: string, projectConfigPath: string, provenancePath: string, tracks: string[], sourceFiles: number, rawCandidateFiles: number, reconcileFiles: number, writtenFiles: string[], candidateCounts: Record<string, number>, nextCommands: string[] }}
149
149
  */
150
150
  export function buildBrownfieldImportWorkspacePayload(sourcePath, targetPath, options = {}) {
151
151
  const sourceRoot = path.resolve(sourcePath);
@@ -190,6 +190,7 @@ export function buildBrownfieldImportWorkspacePayload(sourcePath, targetPath, op
190
190
  ok: true,
191
191
  sourcePath: sourceRoot,
192
192
  targetPath: targetRoot,
193
+ workspaceRoot: topogramRoot,
193
194
  topogramRoot,
194
195
  projectConfigPath,
195
196
  provenancePath: provenance.path,
@@ -10,7 +10,6 @@ import { runGenerateAppCommand } from "./commands/generate.js";
10
10
  import { runGeneratorCommand } from "./commands/generator.js";
11
11
  import { runGeneratorPolicyCommand } from "./commands/generator-policy.js";
12
12
  import { runImportCommand } from "./commands/import-runner.js";
13
- import { runMigrateCommand } from "./commands/migrate.js";
14
13
  import { runNewProjectCommand } from "./commands/new.js";
15
14
  import { runPackageCommand } from "./commands/package.js";
16
15
  import { runParseCommand, runResolveCommand } from "./commands/inspect.js";
@@ -242,10 +241,6 @@ export async function runCliDispatch(context) {
242
241
  return runPackageCommand({ commandArgs, inputPath, json: emitJson });
243
242
  }
244
243
 
245
- if (commandArgs?.migrateCommand) {
246
- return runMigrateCommand(inputPath, { write: shouldWrite, json: emitJson });
247
- }
248
-
249
244
  if (commandArgs?.importCommand) {
250
245
  return runImportCommand({
251
246
  commandArgs,
package/src/cli/help.js CHANGED
@@ -28,7 +28,6 @@ export function printUsage(options = {}) {
28
28
  console.log(" or: topogram catalog doctor [--json] [--catalog <path-or-source>]");
29
29
  console.log(" or: topogram catalog check <path-or-url> [--json]");
30
30
  console.log(" or: topogram catalog copy <id> <target> [--version <version>] [--json] [--catalog <path-or-source>]");
31
- console.log(" or: topogram migrate workspace-folder [path] [--dry-run|--write] [--json]");
32
31
  console.log(" or: topogram package update-cli <version|--latest> [--json]");
33
32
  console.log(" or: topogram import <app-path> --out <target> [--from <track[,track]>] [--json]");
34
33
  console.log(" or: topogram import refresh [path] [--from <app-path>] [--dry-run] [--json]");
@@ -30,6 +30,9 @@ export function cliMigrationError(args) {
30
30
  if (args[0] === "component") {
31
31
  return "Command 'topogram component' was renamed to 'topogram widget'.";
32
32
  }
33
+ if (args[0] === "migrate") {
34
+ return "Command 'topogram migrate workspace-folder' was removed. Use topo/ workspaces or configure topogram.project.json workspace to a non-legacy relative path.";
35
+ }
33
36
  for (const [oldArg, newArg] of RENAMED_CLI_ARGS) {
34
37
  if (args.includes(oldArg)) {
35
38
  return `CLI flag '${oldArg}' was renamed to '${newArg}'.`;
@@ -209,8 +209,8 @@ discover_input_path() {
209
209
  )
210
210
  local candidate
211
211
  for candidate in "\${candidates[@]}"; do
212
- if resolved="$(resolve_path_candidate "$candidate" "$PWD")" && [[ -d "$resolved/topogram" ]]; then
213
- printf '%s\\n' "$resolved/topogram"
212
+ if resolved="$(resolve_path_candidate "$candidate" "$PWD")" && [[ -d "$resolved/topo" ]]; then
213
+ printf '%s\\n' "$resolved/topo"
214
214
  return
215
215
  fi
216
216
  done
@@ -335,7 +335,8 @@ npm run query:show -- widget-behavior
335
335
  ${hasImplementation ? "- This project has executable `implementation/` code. `topogram new` did not execute it. Do not refresh trust until the implementation has been reviewed.\n" : "- This template does not declare executable implementation code.\n"}
336
336
  ## Import And Adoption
337
337
 
338
- - If \`.topogram-import.json\` exists, run \`topogram import check .\`, \`topogram import plan .\`, \`topogram import adopt --list .\`, and \`topogram import history . --verify\`.
338
+ - If \`.topogram-import.json\` exists, agents should run \`topogram import check . --json\`, \`topogram import plan . --json\`, \`topogram import adopt --list . --json\`, \`topogram import status . --json\`, and \`topogram import history . --verify --json\`.
339
+ - Import JSON payloads expose \`workspaceRoot\`; prefer it as the canonical project-owned workspace path.
339
340
  - Imported Topogram files are project-owned after adoption; source hashes record trusted import evidence at the time of import.
340
341
 
341
342
  ## Verification Gates
@@ -4,10 +4,10 @@ import fs from "node:fs";
4
4
  import path from "node:path";
5
5
 
6
6
  export const DEFAULT_TOPO_FOLDER_NAME = "topo";
7
- export const LEGACY_TOPOGRAM_FOLDER_NAME = "topogram";
8
7
  export const DEFAULT_WORKSPACE_PATH = `./${DEFAULT_TOPO_FOLDER_NAME}`;
9
8
  export const PROJECT_CONFIG_FILE = "topogram.project.json";
10
9
 
10
+ const LEGACY_WORKSPACE_FOLDER_NAME = "topogram";
11
11
  const SIGNAL_SCAN_IGNORED_DIRS = new Set([
12
12
  ".git",
13
13
  ".next",
@@ -19,6 +19,7 @@ const SIGNAL_SCAN_IGNORED_DIRS = new Set([
19
19
  "coverage",
20
20
  "dist",
21
21
  "expected",
22
+ LEGACY_WORKSPACE_FOLDER_NAME,
22
23
  "node_modules",
23
24
  "tmp"
24
25
  ]);
@@ -128,6 +129,9 @@ export function normalizeWorkspaceConfigPath(workspacePath) {
128
129
  if (resolved === ".." || resolved.startsWith("../")) {
129
130
  throw new Error("topogram.project.json workspace must not escape the project root.");
130
131
  }
132
+ if (resolved === LEGACY_WORKSPACE_FOLDER_NAME || resolved.startsWith(`${LEGACY_WORKSPACE_FOLDER_NAME}/`)) {
133
+ throw new Error("topogram.project.json workspace must use ./topo or another non-legacy relative path.");
134
+ }
131
135
  return normalized;
132
136
  }
133
137
 
@@ -225,6 +229,9 @@ function signalWorkspaceCandidates(root) {
225
229
  */
226
230
  export function resolveWorkspaceContext(inputPath = ".") {
227
231
  const absolute = path.resolve(inputPath || ".");
232
+ if (isDirectory(absolute) && path.basename(absolute) === LEGACY_WORKSPACE_FOLDER_NAME && isWorkspaceSignalRoot(absolute)) {
233
+ throw new Error("Legacy workspace folders are not supported. Use topo/ or configure topogram.project.json workspace to a non-legacy relative path.");
234
+ }
228
235
  if (
229
236
  isDirectory(absolute) &&
230
237
  (
@@ -1,153 +0,0 @@
1
- // @ts-check
2
-
3
- import fs from "node:fs";
4
- import path from "node:path";
5
-
6
- import { stableStringify } from "../../format.js";
7
- import {
8
- DEFAULT_TOPO_FOLDER_NAME,
9
- DEFAULT_WORKSPACE_PATH,
10
- LEGACY_TOPOGRAM_FOLDER_NAME,
11
- PROJECT_CONFIG_FILE
12
- } from "../../workspace-paths.js";
13
-
14
- /**
15
- * @param {string|null|undefined} inputPath
16
- * @returns {string}
17
- */
18
- function projectRootForMigration(inputPath) {
19
- const absolute = path.resolve(inputPath || ".");
20
- const base = path.basename(absolute);
21
- if (base === DEFAULT_TOPO_FOLDER_NAME || base === LEGACY_TOPOGRAM_FOLDER_NAME) {
22
- return path.dirname(absolute);
23
- }
24
- return absolute;
25
- }
26
-
27
- /**
28
- * @param {string} projectRoot
29
- * @returns {string[]}
30
- */
31
- function caseCollisionEntries(projectRoot) {
32
- if (!fs.existsSync(projectRoot) || !fs.statSync(projectRoot).isDirectory()) {
33
- return [];
34
- }
35
- return fs.readdirSync(projectRoot)
36
- .filter((/** @type {string} */ entry) => entry.toLowerCase() === DEFAULT_TOPO_FOLDER_NAME && entry !== DEFAULT_TOPO_FOLDER_NAME);
37
- }
38
-
39
- /**
40
- * @param {string} projectRoot
41
- * @returns {{ write: boolean, path: string|null, before: any|null, after: any|null }}
42
- */
43
- function plannedProjectConfigUpdate(projectRoot) {
44
- const configPath = path.join(projectRoot, PROJECT_CONFIG_FILE);
45
- if (!fs.existsSync(configPath)) {
46
- return { write: false, path: null, before: null, after: null };
47
- }
48
- const before = JSON.parse(fs.readFileSync(configPath, "utf8"));
49
- const after = { ...before };
50
- const currentWorkspace = before.workspace;
51
- if (currentWorkspace == null || currentWorkspace === "./topogram" || currentWorkspace === "topogram") {
52
- after.workspace = DEFAULT_WORKSPACE_PATH;
53
- }
54
- return {
55
- write: JSON.stringify(before) !== JSON.stringify(after),
56
- path: configPath,
57
- before,
58
- after
59
- };
60
- }
61
-
62
- /**
63
- * @param {string|null|undefined} inputPath
64
- * @param {{ write?: boolean, json?: boolean }} [options]
65
- * @returns {number}
66
- */
67
- export function runMigrateCommand(inputPath, options = {}) {
68
- const projectRoot = projectRootForMigration(inputPath);
69
- const legacyPath = path.join(projectRoot, LEGACY_TOPOGRAM_FOLDER_NAME);
70
- const topoPath = path.join(projectRoot, DEFAULT_TOPO_FOLDER_NAME);
71
- const write = Boolean(options.write);
72
- /** @type {Array<Record<string, any>>} */
73
- const diagnostics = [];
74
- /** @type {Array<Record<string, any>>} */
75
- const actions = [];
76
-
77
- if (fs.existsSync(legacyPath) && fs.lstatSync(legacyPath).isSymbolicLink()) {
78
- diagnostics.push({ severity: "error", message: `Refusing to migrate symlinked ${LEGACY_TOPOGRAM_FOLDER_NAME}/ at ${legacyPath}.` });
79
- }
80
- const collisions = caseCollisionEntries(projectRoot);
81
- if (collisions.length > 0) {
82
- diagnostics.push({ severity: "error", message: `Refusing to migrate because case-conflicting topo path(s) exist: ${collisions.join(", ")}.` });
83
- }
84
- if (fs.existsSync(legacyPath) && fs.existsSync(topoPath)) {
85
- diagnostics.push({ severity: "error", message: `Refusing to migrate because both ${LEGACY_TOPOGRAM_FOLDER_NAME}/ and ${DEFAULT_TOPO_FOLDER_NAME}/ exist.` });
86
- }
87
- if (!fs.existsSync(legacyPath) && !fs.existsSync(topoPath)) {
88
- diagnostics.push({ severity: "error", message: `No ${LEGACY_TOPOGRAM_FOLDER_NAME}/ or ${DEFAULT_TOPO_FOLDER_NAME}/ workspace folder found at ${projectRoot}.` });
89
- }
90
- if (fs.existsSync(topoPath) && fs.statSync(topoPath).isDirectory() && fs.readdirSync(topoPath).length > 0 && fs.existsSync(legacyPath)) {
91
- diagnostics.push({ severity: "error", message: `Refusing to overwrite non-empty ${DEFAULT_TOPO_FOLDER_NAME}/ at ${topoPath}.` });
92
- }
93
-
94
- if (fs.existsSync(legacyPath) && diagnostics.length === 0) {
95
- actions.push({
96
- kind: "rename",
97
- from: legacyPath,
98
- to: topoPath
99
- });
100
- }
101
- const configUpdate = plannedProjectConfigUpdate(projectRoot);
102
- if (configUpdate.write) {
103
- actions.push({
104
- kind: "update_config",
105
- path: configUpdate.path,
106
- workspace: DEFAULT_WORKSPACE_PATH
107
- });
108
- }
109
-
110
- const ok = diagnostics.filter((diagnostic) => diagnostic.severity === "error").length === 0;
111
- if (ok && write) {
112
- for (const action of actions) {
113
- if (action.kind === "rename") {
114
- fs.renameSync(action.from, action.to);
115
- }
116
- if (action.kind === "update_config" && configUpdate.path && configUpdate.after) {
117
- fs.writeFileSync(configUpdate.path, `${JSON.stringify(configUpdate.after, null, 2)}\n`, "utf8");
118
- }
119
- }
120
- }
121
-
122
- const payload = {
123
- ok,
124
- dryRun: !write,
125
- projectRoot,
126
- legacyPath,
127
- topoPath,
128
- actions,
129
- diagnostics,
130
- errors: diagnostics.filter((diagnostic) => diagnostic.severity === "error").map((diagnostic) => diagnostic.message)
131
- };
132
- if (options.json) {
133
- console.log(stableStringify(payload));
134
- } else if (payload.ok) {
135
- console.log(write ? "Workspace folder migration complete." : "Workspace folder migration dry run.");
136
- if (actions.length === 0) {
137
- console.log("No changes needed.");
138
- }
139
- for (const action of actions) {
140
- if (action.kind === "rename") {
141
- console.log(`Rename: ${action.from} -> ${action.to}`);
142
- }
143
- if (action.kind === "update_config") {
144
- console.log(`Update ${action.path}: workspace ${DEFAULT_WORKSPACE_PATH}`);
145
- }
146
- }
147
- } else {
148
- for (const error of payload.errors) {
149
- console.error(error);
150
- }
151
- }
152
- return payload.ok ? 0 : 1;
153
- }