@topogram/cli 0.3.74 → 0.3.76
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 +57 -6
- package/src/agent-brief.js +18 -3
- package/src/cli/commands/check.js +15 -1
- package/src/cli/commands/import/workspace.js +1 -0
- package/src/generator/adapters.js +1 -1
- package/src/generator/runtime/app-bundle.js +3 -2
- package/src/generator/runtime/environment/index.js +15 -6
- package/src/generator/runtime/shared/index.js +1 -0
- package/src/generator/surfaces/databases/lifecycle-shared.js +175 -13
- package/src/import/core/runner/candidates.js +2 -1
- package/src/import/core/runner/reports.js +5 -2
- package/src/import/core/runner/tracks.js +1 -1
- package/src/import/extractors/cli/generic.js +107 -40
- package/src/import/extractors/db/drizzle.js +35 -4
- package/src/import/extractors/db/maintained-seams.js +208 -0
- package/src/import/extractors/db/prisma.js +3 -2
- package/src/import/extractors/db/sql.js +3 -1
- package/src/project-config/index.js +97 -0
- package/src/project-config.js +1 -0
- package/src/workflows/reconcile/candidate-model.js +8 -2
- package/src/workflows/reconcile/workflow.js +1 -0
|
@@ -18,6 +18,17 @@ import { DEFAULT_WORKSPACE_PATH, normalizeWorkspaceConfigPath } from "../workspa
|
|
|
18
18
|
* @property {string} [package]
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {Object} RuntimeMigrationStrategy
|
|
23
|
+
* @property {"generated"|"maintained"} ownership
|
|
24
|
+
* @property {"sql"|"prisma"|"drizzle"} tool
|
|
25
|
+
* @property {"never"|"script"} apply
|
|
26
|
+
* @property {string} [statePath]
|
|
27
|
+
* @property {string} [snapshotPath]
|
|
28
|
+
* @property {string} [schemaPath]
|
|
29
|
+
* @property {string} [migrationsPath]
|
|
30
|
+
*/
|
|
31
|
+
|
|
21
32
|
/**
|
|
22
33
|
* @typedef {Object} RuntimeTopologyRuntime
|
|
23
34
|
* @property {string} id
|
|
@@ -28,6 +39,7 @@ import { DEFAULT_WORKSPACE_PATH, normalizeWorkspaceConfigPath } from "../workspa
|
|
|
28
39
|
* @property {string} [uses_api]
|
|
29
40
|
* @property {string} [uses_database]
|
|
30
41
|
* @property {Record<string, string>} [env]
|
|
42
|
+
* @property {RuntimeMigrationStrategy} [migration]
|
|
31
43
|
*/
|
|
32
44
|
|
|
33
45
|
/**
|
|
@@ -58,6 +70,10 @@ const PROJECT_CONFIG_FILE = "topogram.project.json";
|
|
|
58
70
|
const LEGACY_IMPLEMENTATION_FILE = "topogram.implementation.json";
|
|
59
71
|
const GENERATED_OUTPUT_SENTINEL = ".topogram-generated.json";
|
|
60
72
|
const IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
73
|
+
const MIGRATION_OWNERSHIPS = new Set(["generated", "maintained"]);
|
|
74
|
+
const MIGRATION_TOOLS = new Set(["sql", "prisma", "drizzle"]);
|
|
75
|
+
const MIGRATION_APPLY_MODES = new Set(["never", "script"]);
|
|
76
|
+
const MIGRATION_PATH_FIELDS = ["statePath", "snapshotPath", "schemaPath", "migrationsPath"];
|
|
61
77
|
|
|
62
78
|
/**
|
|
63
79
|
* @param {string|null|undefined} root
|
|
@@ -249,6 +265,21 @@ function validateWorkspaceConfig(errors, config) {
|
|
|
249
265
|
}
|
|
250
266
|
}
|
|
251
267
|
|
|
268
|
+
/**
|
|
269
|
+
* @param {string} value
|
|
270
|
+
* @returns {boolean}
|
|
271
|
+
*/
|
|
272
|
+
function isProjectRelativePath(value) {
|
|
273
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
if (path.isAbsolute(value)) {
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
const normalized = path.posix.normalize(value.replace(/\\/g, "/"));
|
|
280
|
+
return normalized !== ".." && !normalized.startsWith("../");
|
|
281
|
+
}
|
|
282
|
+
|
|
252
283
|
/**
|
|
253
284
|
* @param {string} root
|
|
254
285
|
* @returns {ProjectConfigInfo|null}
|
|
@@ -388,9 +419,75 @@ function validateRuntimeShape(errors, runtime, seenIds) {
|
|
|
388
419
|
if (runtime.port != null && (!Number.isInteger(runtime.port) || runtime.port <= 0 || runtime.port > 65535)) {
|
|
389
420
|
pushError(errors, `${runtimeLabel(runtime)} port must be an integer from 1 to 65535`);
|
|
390
421
|
}
|
|
422
|
+
validateRuntimeMigrationStrategy(errors, runtime);
|
|
391
423
|
return true;
|
|
392
424
|
}
|
|
393
425
|
|
|
426
|
+
/**
|
|
427
|
+
* @param {ValidationError[]} errors
|
|
428
|
+
* @param {any} runtime
|
|
429
|
+
* @returns {void}
|
|
430
|
+
*/
|
|
431
|
+
function validateRuntimeMigrationStrategy(errors, runtime) {
|
|
432
|
+
if (runtime.migration == null) {
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
if (runtime.kind !== "database") {
|
|
436
|
+
pushError(errors, `${runtimeLabel(runtime)} migration is only supported on database runtimes`);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
const migration = runtime.migration;
|
|
440
|
+
if (!migration || typeof migration !== "object" || Array.isArray(migration)) {
|
|
441
|
+
pushError(errors, `${runtimeLabel(runtime)} migration must be an object`);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
if (!MIGRATION_OWNERSHIPS.has(migration.ownership)) {
|
|
445
|
+
pushError(errors, `${runtimeLabel(runtime)} migration.ownership must be generated or maintained`);
|
|
446
|
+
}
|
|
447
|
+
if (!MIGRATION_TOOLS.has(migration.tool)) {
|
|
448
|
+
pushError(errors, `${runtimeLabel(runtime)} migration.tool must be sql, prisma, or drizzle`);
|
|
449
|
+
}
|
|
450
|
+
if (!MIGRATION_APPLY_MODES.has(migration.apply)) {
|
|
451
|
+
pushError(errors, `${runtimeLabel(runtime)} migration.apply must be never or script`);
|
|
452
|
+
}
|
|
453
|
+
if (migration.ownership === "maintained" && migration.apply !== "never") {
|
|
454
|
+
pushError(errors, `${runtimeLabel(runtime)} maintained migration.apply must be never`);
|
|
455
|
+
}
|
|
456
|
+
if (migration.ownership === "generated" && migration.apply !== "script") {
|
|
457
|
+
pushError(errors, `${runtimeLabel(runtime)} generated migration.apply must be script`);
|
|
458
|
+
}
|
|
459
|
+
for (const field of MIGRATION_PATH_FIELDS) {
|
|
460
|
+
if (migration[field] != null && !isProjectRelativePath(migration[field])) {
|
|
461
|
+
pushError(errors, `${runtimeLabel(runtime)} migration.${field} must be a non-empty relative path that stays inside the project root`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
if (migration.ownership === "generated" && typeof migration.statePath !== "string") {
|
|
465
|
+
pushError(errors, `${runtimeLabel(runtime)} generated migration requires statePath`);
|
|
466
|
+
}
|
|
467
|
+
if (migration.ownership === "maintained" && typeof migration.snapshotPath !== "string") {
|
|
468
|
+
pushError(errors, `${runtimeLabel(runtime)} maintained migration requires snapshotPath`);
|
|
469
|
+
}
|
|
470
|
+
if (migration.ownership === "maintained" && migration.tool === "prisma") {
|
|
471
|
+
if (typeof migration.schemaPath !== "string") {
|
|
472
|
+
pushError(errors, `${runtimeLabel(runtime)} maintained prisma migration requires schemaPath`);
|
|
473
|
+
}
|
|
474
|
+
if (typeof migration.migrationsPath !== "string") {
|
|
475
|
+
pushError(errors, `${runtimeLabel(runtime)} maintained prisma migration requires migrationsPath`);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
if (migration.ownership === "maintained" && migration.tool === "drizzle") {
|
|
479
|
+
if (typeof migration.schemaPath !== "string") {
|
|
480
|
+
pushError(errors, `${runtimeLabel(runtime)} maintained drizzle migration requires schemaPath`);
|
|
481
|
+
}
|
|
482
|
+
if (typeof migration.migrationsPath !== "string") {
|
|
483
|
+
pushError(errors, `${runtimeLabel(runtime)} maintained drizzle migration requires migrationsPath`);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
if (migration.ownership === "maintained" && migration.tool === "sql" && typeof migration.migrationsPath !== "string") {
|
|
487
|
+
pushError(errors, `${runtimeLabel(runtime)} maintained sql migration requires migrationsPath`);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
394
491
|
/**
|
|
395
492
|
* @param {ValidationError[]} errors
|
|
396
493
|
* @param {RuntimeTopologyRuntime} runtime
|
package/src/project-config.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @typedef {import("./project-config/index.js").GeneratorBinding} GeneratorBinding
|
|
5
|
+
* @typedef {import("./project-config/index.js").RuntimeMigrationStrategy} RuntimeMigrationStrategy
|
|
5
6
|
* @typedef {import("./project-config/index.js").RuntimeTopologyRuntime} RuntimeTopologyRuntime
|
|
6
7
|
* @typedef {import("./project-config/index.js").ProjectConfig} ProjectConfig
|
|
7
8
|
* @typedef {import("./project-config/index.js").ProjectConfigInfo} ProjectConfigInfo
|
|
@@ -81,7 +81,7 @@ export function bestContextBundleForCandidate(bundles, candidate) {
|
|
|
81
81
|
|
|
82
82
|
/** @param {ResolvedGraph} graph @param {ImportArtifacts} appImport @param {any} topogramRoot @returns {any} */
|
|
83
83
|
export function buildCandidateModelBundles(graph, appImport, topogramRoot) {
|
|
84
|
-
const dbCandidates = appImport.candidates.db || { entities: [], enums: [] };
|
|
84
|
+
const dbCandidates = appImport.candidates.db || { entities: [], enums: [], maintained_seams: [] };
|
|
85
85
|
const apiCandidates = appImport.candidates.api || { capabilities: [] };
|
|
86
86
|
const uiCandidates = appImport.candidates.ui || { screens: [], routes: [], actions: [], widgets: [], shapes: [] };
|
|
87
87
|
const uiWidgetCandidates = uiCandidates.widgets || uiCandidates.components || [];
|
|
@@ -151,6 +151,10 @@ export function buildCandidateModelBundles(graph, appImport, topogramRoot) {
|
|
|
151
151
|
bundles.delete(`enum_${enumId}`);
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
|
+
for (const seam of dbCandidates.maintained_seams || []) {
|
|
155
|
+
const bundle = getOrCreateCandidateBundle(bundles, "database", "Database");
|
|
156
|
+
bundle.maintainedSeams = [...(bundle.maintainedSeams || []), seam];
|
|
157
|
+
}
|
|
154
158
|
for (const entry of apiCandidates.capabilities || []) {
|
|
155
159
|
const matchedCapability = graph ? matchImportedApiCapability(entry, topogramApiCapabilities) : null;
|
|
156
160
|
if (matchedCapability) {
|
|
@@ -333,7 +337,8 @@ export function buildCandidateModelBundles(graph, appImport, topogramRoot) {
|
|
|
333
337
|
bundle.workflows.length > 0 ||
|
|
334
338
|
bundle.verifications.length > 0 ||
|
|
335
339
|
bundle.workflowStates.length > 0 ||
|
|
336
|
-
bundle.workflowTransitions.length > 0
|
|
340
|
+
bundle.workflowTransitions.length > 0 ||
|
|
341
|
+
(bundle.maintainedSeams || []).length > 0
|
|
337
342
|
)
|
|
338
343
|
.map((/** @type {any} */ bundle) => {
|
|
339
344
|
const sortedBundle = {
|
|
@@ -353,6 +358,7 @@ export function buildCandidateModelBundles(graph, appImport, topogramRoot) {
|
|
|
353
358
|
verifications: bundle.verifications.sort((/** @type {any} */ a, /** @type {any} */ b) => a.id_hint.localeCompare(b.id_hint)),
|
|
354
359
|
workflowStates: bundle.workflowStates.sort((/** @type {any} */ a, /** @type {any} */ b) => a.id_hint.localeCompare(b.id_hint)),
|
|
355
360
|
workflowTransitions: bundle.workflowTransitions.sort((/** @type {any} */ a, /** @type {any} */ b) => a.id_hint.localeCompare(b.id_hint)),
|
|
361
|
+
maintainedSeams: (bundle.maintainedSeams || []).sort((/** @type {any} */ a, /** @type {any} */ b) => a.id_hint.localeCompare(b.id_hint)),
|
|
356
362
|
docs: bundle.docs.sort((/** @type {any} */ a, /** @type {any} */ b) => a.id.localeCompare(b.id))
|
|
357
363
|
};
|
|
358
364
|
const mergeHints = buildBundleMergeHints(sortedBundle, canonicalEntityIds);
|
|
@@ -91,6 +91,7 @@ export function reconcileWorkflow(inputPath, options = {}) {
|
|
|
91
91
|
type: "reconcile_adoption_plan",
|
|
92
92
|
workspace: paths.topogramRoot,
|
|
93
93
|
approved_review_groups: [...new Set(existingPlan?.approved_review_groups || [])],
|
|
94
|
+
imported_maintained_db_seams: appImport.candidates?.db?.maintained_seams || [],
|
|
94
95
|
items: mergedPlanItems,
|
|
95
96
|
projection_review_groups: buildProjectionReviewGroups(mergedPlanItems),
|
|
96
97
|
ui_review_groups: buildUiReviewGroups(mergedPlanItems),
|