@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 +1 -1
- package/src/agent-brief.js +20 -11
- package/src/cli/command-parsers/project.js +0 -3
- package/src/cli/commands/import/adopt.js +2 -0
- package/src/cli/commands/import/check.js +4 -2
- package/src/cli/commands/import/diff.js +1 -0
- package/src/cli/commands/import/plan.js +2 -0
- package/src/cli/commands/import/refresh.js +2 -1
- package/src/cli/commands/import/status-history.js +5 -1
- package/src/cli/commands/import/workspace.js +2 -1
- package/src/cli/dispatcher.js +0 -5
- package/src/cli/help.js +0 -1
- package/src/cli/migration-guidance.js +3 -0
- package/src/generator/surfaces/databases/lifecycle-shared.js +2 -2
- package/src/new-project/project-files.js +2 -1
- package/src/workspace-paths.js +8 -1
- package/src/cli/commands/migrate.js +0 -153
package/package.json
CHANGED
package/src/agent-brief.js
CHANGED
|
@@ -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;
|
|
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
|
|
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,
|
package/src/cli/dispatcher.js
CHANGED
|
@@ -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/
|
|
213
|
-
printf '%s\\n' "$resolved/
|
|
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
|
|
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
|
package/src/workspace-paths.js
CHANGED
|
@@ -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
|
-
}
|