@topogram/cli 0.3.60 → 0.3.62

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.60",
3
+ "version": "0.3.62",
4
4
  "description": "Topogram CLI for checking Topogram workspaces and generating app bundles.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
package/src/cli.js CHANGED
@@ -153,6 +153,17 @@ const KNOWN_CLI_CONSUMER_WORKFLOWS = {
153
153
  "topogram-hello": "Topogram Package Verification",
154
154
  "topograms": "Catalog Verification"
155
155
  };
156
+ const KNOWN_CLI_CONSUMER_WORKFLOW_JOBS = {
157
+ "topograms": [
158
+ "Validate catalog",
159
+ "Smoke native starter",
160
+ "Smoke starter alias (hello-web)",
161
+ "Smoke starter alias (hello-api)",
162
+ "Smoke starter alias (hello-db)",
163
+ "Smoke starter alias (web-api)",
164
+ "Smoke starter alias (web-api-db)"
165
+ ]
166
+ };
156
167
  const PACKAGE_UPDATE_CLI_CHECK_SCRIPTS = [
157
168
  "cli:surface",
158
169
  "doctor",
@@ -199,7 +210,7 @@ function printUsage(options = {}) {
199
210
  console.log(" or: topogram widget check [path] [--projection <id>] [--widget <id>] [--json]");
200
211
  console.log(" or: topogram widget behavior [path] [--projection <id>] [--widget <id>] [--json]");
201
212
  console.log(" or: topogram generate [path] [--out <path>]");
202
- console.log(" or: topogram generate [path] --generate <target> [--json|--write --out-dir <path>]");
213
+ console.log(" or: topogram emit <target> [path] [--json|--write --out-dir <path>]");
203
214
  console.log(" or: topogram query list [--json]");
204
215
  console.log(" or: topogram query show <name> [--json]");
205
216
  console.log(" or: topogram trust template [path]");
@@ -259,6 +270,8 @@ function printUsage(options = {}) {
259
270
  console.log(" topogram query list");
260
271
  console.log(" topogram query show widget-behavior");
261
272
  console.log(" topogram query widget-behavior ./topogram --projection proj_web_surface --json");
273
+ console.log(" topogram emit ui-widget-contract --widget widget_data_grid --json");
274
+ console.log(" topogram emit widget-conformance-report ./topogram --projection proj_web_surface --json");
262
275
  console.log(" topogram generator list");
263
276
  console.log(" topogram generator show @topogram/generator-react-web");
264
277
  console.log(" topogram generator check ./generator-package");
@@ -327,7 +340,8 @@ function printUsage(options = {}) {
327
340
  console.log(" or: topogram template show <id> [--json] [--catalog <path-or-source>]");
328
341
  console.log(" or: topogram import app <path> [--from <track[,track]>] [--write]");
329
342
  console.log(" or: topogram validate <path>");
330
- console.log(" or: node ./src/cli.js <path> [--json] [--validate] [--resolve] [--generate <target>] [--workflow <name>] [--mode <id>] [--from <track[,track]>] [--adopt <selector>] [--refresh-adopted] [--shape <id>] [--capability <id>] [--widget <id>] [--projection <id>] [--entity <id>] [--journey <id>] [--surface <id>] [--task <id>] [--profile <id>] [--from-snapshot <path>] [--from-topogram <path>] [--write] [--out-dir <path>]");
343
+ console.log(" or: node ./src/cli.js <path> [--json] [--validate] [--resolve] [--workflow <name>] [--mode <id>] [--from <track[,track]>] [--adopt <selector>] [--refresh-adopted] [--shape <id>] [--capability <id>] [--widget <id>] [--projection <id>] [--entity <id>] [--journey <id>] [--surface <id>] [--task <id>] [--profile <id>] [--from-snapshot <path>] [--from-topogram <path>] [--write] [--out-dir <path>]");
344
+ console.log(" or: node ./src/cli.js emit <target> [path] [--json] [--write] [--out-dir <path>]");
331
345
  console.log(" or: node ./src/cli.js import app <path> [--from <track[,track]>] [--write]");
332
346
  console.log(" or: node ./src/cli.js import docs <path> [--write]");
333
347
  console.log(" or: node ./src/cli.js generate journeys <path> [--write]");
@@ -400,11 +414,23 @@ function printNewHelp() {
400
414
  function printGenerateHelp() {
401
415
  console.log("Usage: topogram generate [path] [--out <path>]");
402
416
  console.log(" or: topogram generate app [path] [--out <path>]");
403
- console.log(" or: topogram generate [path] --generate <target> [--json]");
404
- console.log(" or: topogram generate [path] --generate <target> --write [--out-dir <path>]");
405
417
  console.log("");
406
418
  console.log("Defaults: path is ./topogram and app generation writes ./app.");
407
- console.log("Explicit --generate targets print JSON by default and write files only with --write.");
419
+ console.log("Use `topogram emit <target>` for contracts, reports, snapshots, migration plans, and other artifacts.");
420
+ console.log("");
421
+ console.log("Examples:");
422
+ console.log(" topogram generate");
423
+ console.log(" topogram generate ./topogram --out ./app");
424
+ console.log(" topogram generate app ./topogram --out ./app");
425
+ }
426
+
427
+ function printEmitHelp() {
428
+ console.log("Usage: topogram emit <target> [path] [--json]");
429
+ console.log(" or: topogram emit <target> [path] --write [--out-dir <path>]");
430
+ console.log("");
431
+ console.log("Emits named contracts, reports, snapshots, migration plans, and other artifacts.");
432
+ console.log("");
433
+ console.log("Defaults: path is ./topogram. Emit prints to stdout unless --write is passed. --write writes ./artifacts unless --out-dir is supplied.");
408
434
  console.log("");
409
435
  console.log("Common artifact targets:");
410
436
  console.log(" ui-widget-contract");
@@ -422,13 +448,12 @@ function printGenerateHelp() {
422
448
  console.log(" --journey <id>");
423
449
  console.log("");
424
450
  console.log("Examples:");
425
- console.log(" topogram generate");
426
- console.log(" topogram generate ./topogram --out ./app");
427
- console.log(" topogram generate app ./topogram --out ./app");
428
- console.log(" topogram generate ./topogram --generate ui-widget-contract --widget widget_data_grid --json");
429
- console.log(" topogram generate ./topogram --generate widget-conformance-report --projection proj_web_surface --json");
430
- console.log(" topogram generate ./topogram --generate widget-behavior-report --projection proj_web_surface --json");
431
- console.log(" topogram generate ./topogram --generate ui-widget-contract --write --out-dir ./contracts");
451
+ console.log(" topogram emit ui-widget-contract --widget widget_data_grid --json");
452
+ console.log(" topogram emit widget-conformance-report ./topogram --projection proj_web_surface --json");
453
+ console.log(" topogram emit widget-behavior-report ./topogram --projection proj_web_surface --json");
454
+ console.log(" topogram emit db-schema-snapshot ./topogram --projection proj_db_postgres --json");
455
+ console.log(" topogram emit sql-migration ./topogram --projection proj_db_postgres --from-snapshot ./state/current.json");
456
+ console.log(" topogram emit ui-widget-contract --write --out-dir ./contracts");
432
457
  }
433
458
 
434
459
  function printWidgetHelp() {
@@ -858,6 +883,10 @@ function printCommandHelp(command) {
858
883
  printGenerateHelp();
859
884
  return true;
860
885
  }
886
+ if (command === "emit") {
887
+ printEmitHelp();
888
+ return true;
889
+ }
861
890
  if (command === "widget") {
862
891
  printWidgetHelp();
863
892
  return true;
@@ -3211,6 +3240,14 @@ function expectedConsumerWorkflowName(name) {
3211
3240
  return KNOWN_CLI_CONSUMER_WORKFLOWS[name] || null;
3212
3241
  }
3213
3242
 
3243
+ /**
3244
+ * @param {string} name
3245
+ * @returns {string[]}
3246
+ */
3247
+ function expectedConsumerWorkflowJobs(name) {
3248
+ return KNOWN_CLI_CONSUMER_WORKFLOW_JOBS[name] || [];
3249
+ }
3250
+
3214
3251
  /**
3215
3252
  * @param {string[]} args
3216
3253
  * @param {string} cwd
@@ -3351,16 +3388,18 @@ function waitForConsumerCi(consumer, options = {}) {
3351
3388
  /**
3352
3389
  * @param {{ name: string, root?: string|null, workflow?: string|null }} consumer
3353
3390
  * @param {{ strict?: boolean }} [options]
3354
- * @returns {{ checked: boolean, ok: boolean|null, expectedWorkflow: string|null, headSha: string|null, run: { databaseId?: number, workflowName?: string, status?: string, conclusion?: string, headSha?: string, url?: string }|null, diagnostics: Array<Record<string, any>> }}
3391
+ * @returns {{ checked: boolean, ok: boolean|null, expectedWorkflow: string|null, expectedJobs: string[], headSha: string|null, run: { databaseId?: number, workflowName?: string, status?: string, conclusion?: string, headSha?: string, url?: string, jobs?: Array<Record<string, any>> }|null, diagnostics: Array<Record<string, any>> }}
3355
3392
  */
3356
3393
  function inspectConsumerCi(consumer, options = {}) {
3357
3394
  const diagnostics = [];
3358
3395
  const expectedWorkflow = consumer.workflow || expectedConsumerWorkflowName(consumer.name);
3396
+ const expectedJobs = expectedConsumerWorkflowJobs(consumer.name);
3359
3397
  if (!consumer.root || !fs.existsSync(consumer.root)) {
3360
3398
  return {
3361
3399
  checked: false,
3362
3400
  ok: null,
3363
3401
  expectedWorkflow,
3402
+ expectedJobs,
3364
3403
  headSha: null,
3365
3404
  run: null,
3366
3405
  diagnostics: []
@@ -3388,6 +3427,7 @@ function inspectConsumerCi(consumer, options = {}) {
3388
3427
  checked: true,
3389
3428
  ok: false,
3390
3429
  expectedWorkflow,
3430
+ expectedJobs,
3391
3431
  headSha,
3392
3432
  run: null,
3393
3433
  diagnostics
@@ -3424,6 +3464,7 @@ function inspectConsumerCi(consumer, options = {}) {
3424
3464
  checked: true,
3425
3465
  ok: false,
3426
3466
  expectedWorkflow,
3467
+ expectedJobs,
3427
3468
  headSha,
3428
3469
  run: null,
3429
3470
  diagnostics
@@ -3454,6 +3495,7 @@ function inspectConsumerCi(consumer, options = {}) {
3454
3495
  checked: true,
3455
3496
  ok: false,
3456
3497
  expectedWorkflow,
3498
+ expectedJobs,
3457
3499
  headSha,
3458
3500
  run: null,
3459
3501
  diagnostics
@@ -3477,17 +3519,105 @@ function inspectConsumerCi(consumer, options = {}) {
3477
3519
  suggestedFix: "Wait for or fix the consumer verification workflow, then rerun release status."
3478
3520
  });
3479
3521
  }
3522
+ if (expectedJobs.length > 0 && run.databaseId) {
3523
+ const jobResult = inspectConsumerWorkflowJobs(consumer, run.databaseId, expectedJobs, options);
3524
+ if (jobResult.jobs) {
3525
+ run.jobs = jobResult.jobs;
3526
+ }
3527
+ diagnostics.push(...jobResult.diagnostics);
3528
+ } else if (expectedJobs.length > 0) {
3529
+ diagnostics.push({
3530
+ code: "release_consumer_ci_jobs_unavailable",
3531
+ severity: options.strict ? "error" : "warning",
3532
+ message: `${consumer.name} ${expectedWorkflow} run did not include a database id, so expected jobs could not be inspected.`,
3533
+ path: run.url || `attebury/${consumer.name}`,
3534
+ suggestedFix: "Rerun release status after GitHub exposes the workflow run id."
3535
+ });
3536
+ }
3537
+ const errorCount = diagnostics.filter((diagnostic) => diagnostic.severity === "error").length;
3480
3538
  return {
3481
3539
  checked: true,
3482
- ok: diagnostics.filter((diagnostic) => diagnostic.severity === "error").length === 0 &&
3540
+ ok: errorCount === 0 &&
3483
3541
  (!options.strict || (run.status === "completed" && run.conclusion === "success" && (!headSha || !run.headSha || run.headSha === headSha))),
3484
3542
  expectedWorkflow,
3543
+ expectedJobs,
3485
3544
  headSha,
3486
3545
  run,
3487
3546
  diagnostics
3488
3547
  };
3489
3548
  }
3490
3549
 
3550
+ /**
3551
+ * @param {{ name: string, root?: string|null }} consumer
3552
+ * @param {number|string} runId
3553
+ * @param {string[]} expectedJobs
3554
+ * @param {{ strict?: boolean }} [options]
3555
+ * @returns {{ jobs: Array<Record<string, any>>|null, diagnostics: Array<Record<string, any>> }}
3556
+ */
3557
+ function inspectConsumerWorkflowJobs(consumer, runId, expectedJobs, options = {}) {
3558
+ const diagnostics = [];
3559
+ const result = childProcess.spawnSync("gh", [
3560
+ "run",
3561
+ "view",
3562
+ String(runId),
3563
+ "--repo",
3564
+ `attebury/${consumer.name}`,
3565
+ "--json",
3566
+ "jobs"
3567
+ ], {
3568
+ cwd: consumer.root || process.cwd(),
3569
+ encoding: "utf8",
3570
+ env: { ...process.env, PATH: process.env.PATH || "" }
3571
+ });
3572
+ if (result.status !== 0) {
3573
+ diagnostics.push(commandDiagnostic({
3574
+ code: "release_consumer_ci_jobs_unavailable",
3575
+ severity: options.strict ? "error" : "warning",
3576
+ message: `Could not inspect expected jobs for ${consumer.name}.`,
3577
+ path: `attebury/${consumer.name}`,
3578
+ suggestedFix: "Check GitHub CLI auth/network access, then rerun release status.",
3579
+ result
3580
+ }));
3581
+ return { jobs: null, diagnostics };
3582
+ }
3583
+ let payload = {};
3584
+ try {
3585
+ payload = JSON.parse(String(result.stdout || "{}"));
3586
+ } catch (error) {
3587
+ diagnostics.push({
3588
+ code: "release_consumer_ci_jobs_unreadable",
3589
+ severity: options.strict ? "error" : "warning",
3590
+ message: `Could not parse ${consumer.name} workflow job status: ${messageFromError(error)}`,
3591
+ path: `attebury/${consumer.name}`,
3592
+ suggestedFix: "Rerun release status after GitHub CLI output is valid JSON."
3593
+ });
3594
+ }
3595
+ const jobs = Array.isArray(payload.jobs) ? payload.jobs : [];
3596
+ for (const expectedJob of expectedJobs) {
3597
+ const job = jobs.find((candidate) => candidate?.name === expectedJob);
3598
+ if (!job) {
3599
+ diagnostics.push({
3600
+ code: "release_consumer_ci_job_missing",
3601
+ severity: options.strict ? "error" : "warning",
3602
+ message: `${consumer.name} workflow is missing expected job '${expectedJob}'.`,
3603
+ path: `attebury/${consumer.name}`,
3604
+ suggestedFix: "Update the consumer workflow or the release-status expected job list, then rerun release status."
3605
+ });
3606
+ continue;
3607
+ }
3608
+ if (job.status !== "completed" || job.conclusion !== "success") {
3609
+ diagnostics.push({
3610
+ code: "release_consumer_ci_job_not_successful",
3611
+ severity: options.strict ? "error" : "warning",
3612
+ message: `${consumer.name} job '${expectedJob}' is ${job.status || "unknown"}/${job.conclusion || "unknown"}.`,
3613
+ path: job.url || `attebury/${consumer.name}`,
3614
+ suggestedFix: "Wait for or fix the expected workflow job, then rerun release status."
3615
+ });
3616
+ }
3617
+ }
3618
+ return { jobs, diagnostics };
3619
+ }
3620
+
3491
3621
  /**
3492
3622
  * @param {string} cwd
3493
3623
  * @returns {Array<{ name: string, root: string|null, path: string, version: string|null, found: boolean }>}
@@ -8482,12 +8612,18 @@ if (args[0] === "version" || args[0] === "--version") {
8482
8612
  } else if (args[0] === "generator") {
8483
8613
  printGeneratorHelp();
8484
8614
  process.exit(args[1] ? 1 : 0);
8615
+ } else if (args[0] === "emit") {
8616
+ if (!args[1] || args[1].startsWith("-")) {
8617
+ printEmitHelp();
8618
+ process.exit(1);
8619
+ }
8620
+ commandArgs = { generateTarget: args[1], inputPath: commandPath(2), emitArtifact: true };
8485
8621
  } else if (args[0] === "validate") {
8486
8622
  commandArgs = { validate: true, inputPath: args[1] };
8487
8623
  } else if (args[0] === "generate" && args[1] === "app") {
8488
8624
  commandArgs = { generateTarget: "app-bundle", write: true, inputPath: commandPath(2), defaultOutDir: "./app" };
8489
8625
  } else if (args[0] === "generate" && args.indexOf("--generate") >= 0) {
8490
- commandArgs = { inputPath: commandPath(1) };
8626
+ commandArgs = { inputPath: commandPath(1), deprecatedGenerateArtifact: true };
8491
8627
  } else if (args[0] === "generate" && args[1] !== "journeys") {
8492
8628
  commandArgs = { generateTarget: "app-bundle", write: true, inputPath: commandPath(1), defaultOutDir: "./app" };
8493
8629
  } else if (args[0] === "trust" && args[1] === "template") {
@@ -8727,8 +8863,13 @@ const shouldValidate = Boolean(commandArgs?.validate) || args.includes("--valida
8727
8863
  const shouldResolve = args.includes("--resolve");
8728
8864
  const generateIndex = args.indexOf("--generate");
8729
8865
  const generateTarget = commandArgs?.generateTarget || (generateIndex >= 0 ? args[generateIndex + 1] : null);
8866
+ if (commandArgs?.deprecatedGenerateArtifact && (!generateTarget || generateTarget.startsWith("-"))) {
8867
+ console.error("Missing required --generate <target>.");
8868
+ printUsage();
8869
+ process.exit(1);
8870
+ }
8730
8871
  if (RENAMED_GENERATE_TARGETS.has(generateTarget)) {
8731
- console.error(`Generator target '${generateTarget}' was renamed to '${RENAMED_GENERATE_TARGETS.get(generateTarget)}'.`);
8872
+ console.error(`Artifact target '${generateTarget}' was renamed to '${RENAMED_GENERATE_TARGETS.get(generateTarget)}'.`);
8732
8873
  process.exit(1);
8733
8874
  }
8734
8875
  const workflowIndex = args.indexOf("--workflow");
@@ -11461,6 +11602,9 @@ try {
11461
11602
  const ast = parsePath(inputPath);
11462
11603
 
11463
11604
  if (generateTarget) {
11605
+ if (commandArgs?.deprecatedGenerateArtifact) {
11606
+ console.error(`Deprecated: use \`topogram emit ${generateTarget} ${inputPath || "./topogram"}\` instead of \`topogram generate ${inputPath || "./topogram"} --generate ${generateTarget}\`.`);
11607
+ }
11464
11608
  const projectRoot = normalizeProjectRoot(inputPath);
11465
11609
  const explicitProjectConfig = loadProjectConfig(projectRoot) || loadProjectConfig(inputPath);
11466
11610
  const implementationOptionalTargets = new Set(["app-bundle-plan", "app-bundle", "environment-plan", "environment-bundle", "compile-check-plan", "compile-check-bundle"]);
@@ -352,17 +352,17 @@ infer_current_snapshot_from_live_tables() {
352
352
  }
353
353
 
354
354
  generate_desired_snapshot() {
355
- "$TOPOGRAM_BIN" "$INPUT_PATH" --generate db-schema-snapshot --projection "$PROJECTION_ID" > "$DESIRED_SNAPSHOT"
355
+ "$TOPOGRAM_BIN" emit db-schema-snapshot "$INPUT_PATH" --projection "$PROJECTION_ID" > "$DESIRED_SNAPSHOT"
356
356
  }
357
357
 
358
358
  generate_migration_plan() {
359
359
  local from_snapshot="$1"
360
- "$TOPOGRAM_BIN" "$INPUT_PATH" --generate db-migration-plan --projection "$PROJECTION_ID" --from-snapshot "$from_snapshot" > "$PLAN_JSON"
360
+ "$TOPOGRAM_BIN" emit db-migration-plan "$INPUT_PATH" --projection "$PROJECTION_ID" --from-snapshot "$from_snapshot" > "$PLAN_JSON"
361
361
  }
362
362
 
363
363
  generate_sql_migration() {
364
364
  local from_snapshot="$1"
365
- "$TOPOGRAM_BIN" "$INPUT_PATH" --generate sql-migration --projection "$PROJECTION_ID" --from-snapshot "$from_snapshot" > "$MIGRATION_SQL"
365
+ "$TOPOGRAM_BIN" emit sql-migration "$INPUT_PATH" --projection "$PROJECTION_ID" --from-snapshot "$from_snapshot" > "$MIGRATION_SQL"
366
366
  }
367
367
 
368
368
  ensure_supported_plan() {
@@ -22,5 +22,5 @@ Configure the API base URL and optional auth token via scheme environment variab
22
22
  From `engine/`:
23
23
 
24
24
  ```bash
25
- topogram ./topogram --generate swiftui-app --projection proj_ios_surface__swiftui --write --out-dir ./app/ios-swiftui
25
+ topogram emit swiftui-app ./topogram --projection proj_ios_surface__swiftui --write --out-dir ./app/ios-swiftui
26
26
  ```
@@ -2046,6 +2046,8 @@ Or run self-contained local runtime verification:
2046
2046
 
2047
2047
  Useful inspection:
2048
2048
  npm run check:json
2049
+ topogram emit ui-widget-contract ./topogram --json
2050
+ topogram emit widget-conformance-report ./topogram --json
2049
2051
  npm run doctor
2050
2052
  npm run source:status
2051
2053
  npm run source:status:remote
@@ -2123,6 +2125,7 @@ ${workflowCommands.join("\n")}
2123
2125
 
2124
2126
  Edit \`topogram/\` and \`topogram.project.json\`, then regenerate with \`npm run generate\`.
2125
2127
  Generated app code is written to \`app/\`.
2128
+ Use \`topogram emit <target>\` to inspect contracts, reports, snapshots, and other artifacts without regenerating the app.
2126
2129
  ${template.includesExecutableImplementation ? "\nThis template copied `implementation/` code. `topogram new` did not execute it; review `implementation/`, `topogram.template-policy.json`, and `.topogram-template-trust.json` before regenerating after edits.\n" : ""}
2127
2130
  `;
2128
2131
  fs.writeFileSync(path.join(projectRoot, "README.md"), readme, "utf8");