@substrat-run/cli 0.44.0 → 0.46.0
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/dist/bump.d.ts +120 -0
- package/dist/bump.d.ts.map +1 -0
- package/dist/bump.js +780 -0
- package/dist/bump.js.map +1 -0
- package/dist/changeset.d.ts +129 -0
- package/dist/changeset.d.ts.map +1 -0
- package/dist/changeset.js +336 -0
- package/dist/changeset.js.map +1 -0
- package/dist/cli.js +193 -41
- package/dist/cli.js.map +1 -1
- package/dist/concept.d.ts +19 -0
- package/dist/concept.d.ts.map +1 -0
- package/dist/concept.js +25 -0
- package/dist/concept.js.map +1 -0
- package/dist/data-loss-check.d.ts +70 -0
- package/dist/data-loss-check.d.ts.map +1 -0
- package/dist/data-loss-check.js +260 -0
- package/dist/data-loss-check.js.map +1 -0
- package/dist/data-loss-replay-child.d.ts +27 -0
- package/dist/data-loss-replay-child.d.ts.map +1 -0
- package/dist/data-loss-replay-child.js +71 -0
- package/dist/data-loss-replay-child.js.map +1 -0
- package/dist/promote.d.ts +16 -1
- package/dist/promote.d.ts.map +1 -1
- package/dist/promote.js +87 -11
- package/dist/promote.js.map +1 -1
- package/dist/push.d.ts +32 -6
- package/dist/push.d.ts.map +1 -1
- package/dist/push.js +94 -32
- package/dist/push.js.map +1 -1
- package/dist/scope.d.ts +23 -2
- package/dist/scope.d.ts.map +1 -1
- package/dist/scope.js +63 -7
- package/dist/scope.js.map +1 -1
- package/dist/store-driver.d.ts +59 -0
- package/dist/store-driver.d.ts.map +1 -0
- package/dist/{store-kind.js → store-driver.js} +58 -22
- package/dist/store-driver.js.map +1 -0
- package/package.json +4 -4
- package/dist/store-kind.d.ts +0 -46
- package/dist/store-kind.d.ts.map +0 -1
- package/dist/store-kind.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -20,24 +20,27 @@
|
|
|
20
20
|
* (in the console) still gates serving.
|
|
21
21
|
*/
|
|
22
22
|
import { bindConnectionScope, listConnectionBindings } from './connections.js';
|
|
23
|
-
import { join } from 'node:path';
|
|
23
|
+
import { join, resolve as resolvePath } from 'node:path';
|
|
24
24
|
import { createInterface } from 'node:readline';
|
|
25
25
|
import { loadConfig, saveConfig, resolveAuth } from './config.js';
|
|
26
26
|
import { browserLogin } from './login.js';
|
|
27
27
|
import { push, readVerticalMeta, nextVersion, previewVersion, pinTenant, assertLayerRules, assertSchedulesAreSwept, checkPermissionSurface, deployConfigIfAny, formatDeclaredModel, formatPermissionSurface, readDeclaredModel, } from './push.js';
|
|
28
28
|
import { printVersions } from './versions.js';
|
|
29
29
|
import { checkOutboundDeclared } from './outbound-scan.js';
|
|
30
|
-
import { exportBreakLines, promote } from './promote.js';
|
|
30
|
+
import { exportBreakLines, parseReviewToken, promote } from './promote.js';
|
|
31
31
|
import { setListing, requestPublish } from './listing.js';
|
|
32
|
-
import { isReleaseSha, packedFilesOf,
|
|
32
|
+
import { isReleaseSha, packedFilesOf, packStoreDriverDir, rebuildStoreDriver, rollBackStoreDriver, sourceCommitOf, rollOutStoreDriver, storeDriverRebuildLines, storeDriverStatus, storeDriverStatusLines, uploadStoreDriverRelease } from './store-driver.js';
|
|
33
33
|
import { fetchWhoami } from './whoami.js';
|
|
34
34
|
import { planeFor } from './plane.js';
|
|
35
|
+
import { dataLossCheckRequired, formatDataLossCheck } from './data-loss-check.js';
|
|
35
36
|
import { cliVersion, warnIfDistStale } from './version.js';
|
|
36
37
|
import { pullScope, restoreScope, resolveTenantId, adoptScopeServing, adoptVerticalServing, provisionScope, rebindScopeVertical, bindScopeVersion, scopeStatus } from './scope.js';
|
|
37
38
|
import { printInstalls } from './installs.js';
|
|
38
39
|
import { createPreview, deletePreview, listPreviews, formatPreviewLogin, formatPreviews, parseTtlHours } from './preview.js';
|
|
39
40
|
import { writeCiWorkflow, detectDefaultBranch, nextStepsMessage } from './init.js';
|
|
40
41
|
import { writeModelView } from './model.js';
|
|
42
|
+
import { CannotRun, changesetCommand } from './changeset.js';
|
|
43
|
+
import { syncConceptFile } from './concept.js';
|
|
41
44
|
import { listVerticalHostnames, bindSurfaceHostname, bindScopeHostname, unbindHostname, verifyHostname, formatHostnames, formatDnsRecords, } from './hostnames.js';
|
|
42
45
|
const argv = process.argv.slice(2);
|
|
43
46
|
/** `--name value` → value, else undefined. */
|
|
@@ -107,12 +110,25 @@ Usage:
|
|
|
107
110
|
defineScopeSweeperDO are refused — they
|
|
108
111
|
would never fire; --allow-unswept-schedules
|
|
109
112
|
pushes them anyway
|
|
113
|
+
Authored migrations are replayed against a
|
|
114
|
+
populated in-memory scope through the
|
|
115
|
+
vertical's own kernel's data-loss fence;
|
|
116
|
+
one that would lose undeclared data is
|
|
117
|
+
refused (declare it: moves / discards).
|
|
118
|
+
A check skipped (no node:sqlite, Node
|
|
119
|
+
< 22.13) or incomplete (a migration that
|
|
120
|
+
cannot run on made-up rows, or runs past
|
|
121
|
+
30 s) is a warning;
|
|
122
|
+
--require-data-loss-check (or
|
|
123
|
+
SUBSTRAT_REQUIRE_DATA_LOSS_CHECK=1/true/yes)
|
|
124
|
+
makes it a refusal
|
|
110
125
|
A host the code names as a literal that
|
|
111
126
|
package.json substrat.outbound does not
|
|
112
127
|
declare is warned about with the line to add
|
|
113
128
|
(best effort); --strict makes it an error
|
|
114
129
|
substrat push [dir] --check [--json] run the push's LOCAL gate and stop: the layer
|
|
115
|
-
rules, the
|
|
130
|
+
rules, the data-loss migration replay, the
|
|
131
|
+
entity model this tree would ship
|
|
116
132
|
(or a note that it would ship none), then the
|
|
117
133
|
declared permission surface — resolve
|
|
118
134
|
"substrat": { "permissions" }, import it,
|
|
@@ -124,13 +140,19 @@ Usage:
|
|
|
124
140
|
the registry as data instead of prose
|
|
125
141
|
substrat promote <slug> --version <versionId>
|
|
126
142
|
[--ack-permissions] [--ack-migrations] [--ack-export-break] (prod is the only channel)
|
|
143
|
+
[--review <token>] the review the acks answer, as the refusal printed it
|
|
127
144
|
substrat publish <slug> request listing on the public marketplace (staff reviews)
|
|
128
|
-
substrat store-
|
|
129
|
-
store a release of a scope-store
|
|
130
|
-
substrat store-
|
|
145
|
+
substrat store-driver upload --driver <driver> --dir <dir> --entry <driver_x/…/module.py> [--commit <sha>]
|
|
146
|
+
store a release of a scope-store driver (staff, signed in)
|
|
147
|
+
substrat store-driver rollout --driver <driver> --sha256 <sha256>
|
|
131
148
|
canary a stored, pinned release, then roll it out
|
|
132
|
-
substrat store-
|
|
133
|
-
substrat store-
|
|
149
|
+
substrat store-driver rollback --driver <driver> return to the previous good release
|
|
150
|
+
substrat store-driver status --driver <driver> a driver's current release and rollouts
|
|
151
|
+
substrat store-driver rebuild --driver <driver> [--deploy-finished]
|
|
152
|
+
rebuild every script of the driver now, at
|
|
153
|
+
the release it runs (after a key is revoked);
|
|
154
|
+
--deploy-finished attests no control-plane
|
|
155
|
+
deploy is rolling out
|
|
134
156
|
substrat unpublish <slug> remove from the public marketplace (staff)
|
|
135
157
|
substrat versions <slug> list a vertical's versions + channels
|
|
136
158
|
substrat installs <slug> list this workspace's installs of a vertical
|
|
@@ -147,6 +169,7 @@ Usage:
|
|
|
147
169
|
Requires its existing scoped landing grant.
|
|
148
170
|
Planima: --organization-id <id|null>, --currency,
|
|
149
171
|
--horizon-years, --from-year and --to-year.
|
|
172
|
+
Microsoft 365: --folder <channel folder's drive path>.
|
|
150
173
|
substrat connection bindings <id> read the connection's configured landing targets
|
|
151
174
|
substrat scope restore <scopeId> --file <backup>
|
|
152
175
|
load a backup into an existing hosted scope,
|
|
@@ -180,8 +203,10 @@ Usage:
|
|
|
180
203
|
lineage's serving script, data carried;
|
|
181
204
|
the old copy is deleted once the move is
|
|
182
205
|
confirmed, so snapshot first to keep a way
|
|
183
|
-
back (staff
|
|
184
|
-
|
|
206
|
+
back (staff, or a workspace for its own
|
|
207
|
+
scope; --ack-migrations with the
|
|
208
|
+
--review token its refusal prints, to
|
|
209
|
+
cross a differing migration surface;
|
|
185
210
|
--abandon-data for a source script that
|
|
186
211
|
predates /internal/export — directory-only
|
|
187
212
|
flip, then re-provision the scope)
|
|
@@ -206,7 +231,8 @@ Usage:
|
|
|
206
231
|
re-forks from prod. --ttl is the GC backstop
|
|
207
232
|
(default 72h; 'none' pins the preview until deleted).
|
|
208
233
|
Takes push's own overrides too: --skip-lint,
|
|
209
|
-
--allow-unserved-ui
|
|
234
|
+
--allow-unserved-ui, --allow-unswept-schedules
|
|
235
|
+
and --require-data-loss-check
|
|
210
236
|
substrat preview delete --tag <tag> [--slug <s>] reap a preview (idempotent)
|
|
211
237
|
substrat preview ls [--slug <s>] list a vertical's active previews
|
|
212
238
|
substrat model view [dir|model.json] render the entity model as a self-contained
|
|
@@ -215,6 +241,20 @@ Usage:
|
|
|
215
241
|
open it in a browser. Reads the checked-in
|
|
216
242
|
model.json, so it needs no login and no push.
|
|
217
243
|
Writes a temp file unless --out places it
|
|
244
|
+
substrat changeset [dir] [--base <ref>] write a .changeset file whose bump is COMPUTED
|
|
245
|
+
[--summary <text>] from the diff of model.json, openapi.json and
|
|
246
|
+
[--check] [--json] PERMISSIONS.md against the merge-base: removed
|
|
247
|
+
or narrowed → major, added → minor, docs → patch;
|
|
248
|
+
permission changes are flagged for review.
|
|
249
|
+
--check refuses a changeset declaring less than
|
|
250
|
+
the computed bump (exit 1); --json prints the
|
|
251
|
+
classification. --base defaults to the
|
|
252
|
+
changesets baseBranch on origin
|
|
253
|
+
substrat concept sync [dir] [--check] rewrite the generated regions of spec/concept.md —
|
|
254
|
+
roles and permissions, the entity diagram, one state
|
|
255
|
+
diagram per lifecycle — from model.json and
|
|
256
|
+
PERMISSIONS.md. Idempotent; --check writes nothing
|
|
257
|
+
and exits 1 when the file is stale
|
|
218
258
|
substrat init --ci github [dir] write .github/workflows/substrat-deploy.yml —
|
|
219
259
|
[--branch <b>] [--path <packageDir>] [--release trunk|changesets]
|
|
220
260
|
[--out <path>] [--force]
|
|
@@ -245,7 +285,9 @@ so '--promote prod' is a complete deploy. Once a vertical is LISTED on the marke
|
|
|
245
285
|
pushes land PENDING and prod promotion + admission are a staff decision again. A promotion
|
|
246
286
|
that changes the permission or migration surface is refused until the change is
|
|
247
287
|
acknowledged (--ack-permissions / --ack-migrations) — the refusal prints the permission
|
|
248
|
-
diff and each new migration's SQL; read them first.
|
|
288
|
+
diff and each new migration's SQL; read them first. It also prints a --review token: the
|
|
289
|
+
acknowledgement answers that review, and is refused if prod moved since it was read (a
|
|
290
|
+
migration acknowledgement is refused without one). For a non-prod environment, run the version against a copy of the data with 'substrat preview
|
|
249
291
|
create' — dev/staging channels were retired (#509).
|
|
250
292
|
|
|
251
293
|
Auth resolves: explicit --token/SUBSTRAT_SERVICE_TOKEN → stored browser session →
|
|
@@ -373,7 +415,10 @@ async function cmdPush() {
|
|
|
373
415
|
// rules above have already run, which makes `substrat push --check` the whole local gate.
|
|
374
416
|
// Every failure throws (main() prints it and exits non-zero) — that is the gate.
|
|
375
417
|
if (argv.includes('--check')) {
|
|
376
|
-
const surface = await checkPermissionSurface(dir
|
|
418
|
+
const surface = await checkPermissionSurface(dir, {
|
|
419
|
+
require: dataLossCheckRequired(argv),
|
|
420
|
+
log: jsonCheck ? console.error : console.log,
|
|
421
|
+
});
|
|
377
422
|
// The schedule check (#1646) the push would make — `--check` is the whole local gate, so
|
|
378
423
|
// a vertical whose schedules nothing would run is refused here too. A tree with no deploy
|
|
379
424
|
// config has no bindings to judge; the push itself names that refusal.
|
|
@@ -448,6 +493,8 @@ async function cmdPush() {
|
|
|
448
493
|
// does not scan the same tree twice.
|
|
449
494
|
skipLint: argv.includes('--skip-lint'),
|
|
450
495
|
linted,
|
|
496
|
+
// A data-loss replay (#2321) that was skipped or incomplete is a loud warning; this makes it a refusal.
|
|
497
|
+
requireDataLossCheck: dataLossCheckRequired(argv),
|
|
451
498
|
envSpec: meta.envSpec,
|
|
452
499
|
ownerGrants: meta.ownerGrants,
|
|
453
500
|
entitlements: meta.entitlements,
|
|
@@ -463,6 +510,7 @@ async function cmdPush() {
|
|
|
463
510
|
controlPlaneUrl, authHeader: header,
|
|
464
511
|
});
|
|
465
512
|
console.log(`✓ pushed ${v.verticalSlug ?? slug}. version ${v.id} (${version}) is ${v.admission}; deploymentRef=${v.deploymentRef}`);
|
|
513
|
+
console.log(` ${formatDataLossCheck(v.dataLoss)}`);
|
|
466
514
|
// Advisory, same spirit as the permission-surface gate: a bound surface the new
|
|
467
515
|
// version stopped declaring keeps serving, but probably not what its users expect.
|
|
468
516
|
for (const w of v.warnings ?? [])
|
|
@@ -520,7 +568,14 @@ async function cmdPromote() {
|
|
|
520
568
|
const channel = flag('channel') ?? 'prod';
|
|
521
569
|
const version = flag('version');
|
|
522
570
|
if (!slug || slug.startsWith('--') || !version) {
|
|
523
|
-
console.error('usage: substrat promote <slug> --version <versionId> [--ack-permissions] [--ack-migrations] [--ack-export-break]');
|
|
571
|
+
console.error('usage: substrat promote <slug> --version <versionId> [--ack-permissions] [--ack-migrations] [--ack-export-break] [--review <token>]');
|
|
572
|
+
process.exit(1);
|
|
573
|
+
}
|
|
574
|
+
// #2333: the review the acknowledgements answer, as a refusal printed it.
|
|
575
|
+
const reviewFlag = flag('review');
|
|
576
|
+
const review = reviewFlag === undefined ? undefined : parseReviewToken(reviewFlag);
|
|
577
|
+
if (review === null) {
|
|
578
|
+
console.error('--review takes the token a refused promote printed (<base>:<digest>)');
|
|
524
579
|
process.exit(1);
|
|
525
580
|
}
|
|
526
581
|
const { controlPlaneUrl, header, as } = resolveAuth({ cp: flag('cp'), token: flag('token'), tenant: flag('tenant') });
|
|
@@ -530,8 +585,11 @@ async function cmdPromote() {
|
|
|
530
585
|
...(argv.includes('--ack-permissions') ? { permissionChange: true } : {}),
|
|
531
586
|
...(argv.includes('--ack-migrations') ? { migrationChange: true } : {}),
|
|
532
587
|
...(argv.includes('--ack-export-break') ? { exportBreak: true } : {}),
|
|
588
|
+
...(review ? { review } : {}),
|
|
533
589
|
}
|
|
534
|
-
:
|
|
590
|
+
: review
|
|
591
|
+
? { review }
|
|
592
|
+
: undefined;
|
|
535
593
|
const ch = await promote({ controlPlaneUrl, header, slug, channel, versionId: version, acknowledge });
|
|
536
594
|
console.log(`✓ ${slug} → ${ch.channel} now points at ${ch.versionId}`);
|
|
537
595
|
if (ch.exportBreaks) {
|
|
@@ -563,55 +621,73 @@ function reportStoreBackfill(result) {
|
|
|
563
621
|
' scope with `substrat scope provision <scopeId>`; `substrat scope status` shows the gap.');
|
|
564
622
|
}
|
|
565
623
|
}
|
|
566
|
-
const
|
|
567
|
-
'usage: substrat store-
|
|
568
|
-
' substrat store-
|
|
569
|
-
' substrat store-
|
|
570
|
-
' substrat store-
|
|
624
|
+
const STORE_DRIVER_USAGE = [
|
|
625
|
+
'usage: substrat store-driver upload --driver <driver> --dir <dir> --entry <driver_x/…/module.py> [--commit <sha>]',
|
|
626
|
+
' substrat store-driver rollout --driver <driver> --sha256 <sha256>',
|
|
627
|
+
' substrat store-driver rollback --driver <driver>',
|
|
628
|
+
' substrat store-driver status --driver <driver>',
|
|
629
|
+
' substrat store-driver rebuild --driver <driver> [--deploy-finished]',
|
|
571
630
|
];
|
|
572
|
-
async function
|
|
631
|
+
async function cmdStoreDriver() {
|
|
573
632
|
const sub = argv[1];
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
633
|
+
if (argv.includes('--kind')) {
|
|
634
|
+
console.error('--kind was renamed --driver');
|
|
635
|
+
process.exit(1);
|
|
636
|
+
}
|
|
637
|
+
const driver = flag('driver');
|
|
638
|
+
if (!['upload', 'rollout', 'rollback', 'status', 'rebuild'].includes(sub ?? '') || !driver) {
|
|
639
|
+
for (const line of STORE_DRIVER_USAGE)
|
|
577
640
|
console.error(line);
|
|
578
641
|
process.exit(1);
|
|
579
642
|
}
|
|
580
643
|
const sha256 = flag('sha256');
|
|
581
644
|
if (sub === 'rollout' && !isReleaseSha(sha256)) {
|
|
582
|
-
console.error('a rollout names the release by --sha256 <64 lowercase hex>, as `store-
|
|
583
|
-
for (const line of
|
|
645
|
+
console.error('a rollout names the release by --sha256 <64 lowercase hex>, as `store-driver upload` printed it');
|
|
646
|
+
for (const line of STORE_DRIVER_USAGE)
|
|
584
647
|
console.error(line);
|
|
585
648
|
process.exit(1);
|
|
586
649
|
}
|
|
587
650
|
const { controlPlaneUrl, header, as } = resolveAuth({ cp: flag('cp'), token: flag('token'), tenant: flag('tenant') });
|
|
588
651
|
console.log(`authenticating with ${as}`);
|
|
589
652
|
console.log(`control plane ${controlPlaneUrl}`);
|
|
590
|
-
const plane = { controlPlaneUrl, header,
|
|
653
|
+
const plane = { controlPlaneUrl, header, driver };
|
|
591
654
|
if (sub === 'status') {
|
|
592
|
-
const view = await
|
|
593
|
-
for (const line of
|
|
655
|
+
const view = await storeDriverStatus(plane);
|
|
656
|
+
for (const line of storeDriverStatusLines(driver, view))
|
|
657
|
+
console.log(line);
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
if (sub === 'rebuild') {
|
|
661
|
+
const { pass, rebuild } = await rebuildStoreDriver({ ...plane, deployFinished: argv.includes('--deploy-finished') });
|
|
662
|
+
console.log(`✓ ${driver} rebuild: ${pass.outcome}, ${pass.walked.length} script(s) walked, ${pass.uploaded.length} uploaded this run`);
|
|
663
|
+
for (const line of storeDriverRebuildLines(rebuild))
|
|
594
664
|
console.log(line);
|
|
665
|
+
if (pass.outcome === 'paused')
|
|
666
|
+
console.log(' scripts remain: run the same command again to continue');
|
|
667
|
+
if (pass.outcome === 'held')
|
|
668
|
+
console.log(' a rollout or rollback holds the driver: run it again once that ends');
|
|
669
|
+
if (pass.outcome === 'busy')
|
|
670
|
+
console.log(' another pass is rebuilding the driver: run `store-driver status` to follow it');
|
|
595
671
|
return;
|
|
596
672
|
}
|
|
597
673
|
if (sub === 'upload') {
|
|
598
674
|
const dir = flag('dir');
|
|
599
675
|
const entry = flag('entry');
|
|
600
676
|
if (!dir || !entry) {
|
|
601
|
-
for (const line of
|
|
677
|
+
for (const line of STORE_DRIVER_USAGE)
|
|
602
678
|
console.error(line);
|
|
603
679
|
process.exit(1);
|
|
604
680
|
}
|
|
605
|
-
const body =
|
|
606
|
-
const { release, pinned } = await
|
|
607
|
-
console.log(`✓ stored ${
|
|
608
|
-
console.log(pinned ? ' pinned: roll it out with `substrat store-
|
|
681
|
+
const body = packStoreDriverDir(dir, entry, flag('commit') ?? sourceCommitOf(dir, packedFilesOf(dir)));
|
|
682
|
+
const { release, pinned } = await uploadStoreDriverRelease({ ...plane, body });
|
|
683
|
+
console.log(`✓ stored ${driver} ${release.sha256} (commit ${release.sourceCommit})`);
|
|
684
|
+
console.log(pinned ? ' pinned: roll it out with `substrat store-driver rollout`' : ' not pinned yet: it runs once a reviewed change pins this sha256 and commit');
|
|
609
685
|
return;
|
|
610
686
|
}
|
|
611
687
|
const result = sub === 'rollback'
|
|
612
|
-
? await
|
|
613
|
-
: await
|
|
614
|
-
console.log(`✓ ${
|
|
688
|
+
? await rollBackStoreDriver(plane)
|
|
689
|
+
: await rollOutStoreDriver({ ...plane, sha256: sha256 });
|
|
690
|
+
console.log(`✓ ${driver} ${result.rollout.action} to ${result.rollout.sha256}: ${result.rollout.state} (${result.moved.length} vertical(s) this run)`);
|
|
615
691
|
if (!result.complete)
|
|
616
692
|
console.log(' verticals remain: run the same command again to continue');
|
|
617
693
|
}
|
|
@@ -641,7 +717,7 @@ async function cmdConnection() {
|
|
|
641
717
|
const sub = argv[1];
|
|
642
718
|
const connectionId = argv[2];
|
|
643
719
|
if (!connectionId || connectionId.startsWith('--') || !['bind', 'bindings'].includes(sub ?? '')) {
|
|
644
|
-
throw new Error('usage: substrat connection bind <id> --scope <id> --operation <module/operation> --permission <key> [--organization-id <id|null>] [--currency SEK] [--horizon-years 50] [--from-year <year> --to-year <year>] [--tenant <id-or-slug>]\n substrat connection bindings <id> [--tenant <id-or-slug>]');
|
|
720
|
+
throw new Error('usage: substrat connection bind <id> --scope <id> --operation <module/operation> --permission <key> [--organization-id <id|null>] [--currency SEK] [--horizon-years 50] [--from-year <year> --to-year <year>] [--folder <drive path>] [--tenant <id-or-slug>]\n substrat connection bindings <id> [--tenant <id-or-slug>]');
|
|
645
721
|
}
|
|
646
722
|
const { controlPlaneUrl, header } = resolveAuth({ cp: flag('cp'), token: flag('token'), tenant: flag('tenant') });
|
|
647
723
|
const tenantId = await resolveTenantId(controlPlaneUrl, header, flag('tenant') ?? process.env.SUBSTRAT_TENANT ?? loadConfig().defaultTenant);
|
|
@@ -672,6 +748,7 @@ async function cmdConnection() {
|
|
|
672
748
|
...(flag('currency') === undefined ? {} : { currency: flag('currency') }),
|
|
673
749
|
...(flag('horizon-years') === undefined ? {} : { horizonYears: numberFlag('horizon-years') }),
|
|
674
750
|
...(fromYear === undefined || toYear === undefined ? {} : { window: { fromYear, toYear } }),
|
|
751
|
+
...(flag('folder') === undefined ? {} : { folder: flag('folder') }),
|
|
675
752
|
};
|
|
676
753
|
console.log(JSON.stringify(await bindConnectionScope({ ...opts, scopeId, input }), null, 2));
|
|
677
754
|
}
|
|
@@ -686,7 +763,7 @@ async function cmdScope() {
|
|
|
686
763
|
' substrat scope adopt-serving --vertical <slug> [--ack-export-break]\n' +
|
|
687
764
|
' substrat scope bind <scopeId> --version <versionId> [--snapshot] [--ack-export-break] [--tenant <id-or-slug>]\n' +
|
|
688
765
|
' substrat scope domain <scopeId> --domain <fqdn> [--surface app] [--canonical] [--tenant <id-or-slug>]\n' +
|
|
689
|
-
' substrat scope rebind <scopeId> --to <vertical> [--ack-migrations] [--abandon-data] [--ack-export-break] [--tenant <id-or-slug>]';
|
|
766
|
+
' substrat scope rebind <scopeId> --to <vertical> [--ack-migrations] [--review <token>] [--abandon-data] [--ack-export-break] [--tenant <id-or-slug>]';
|
|
690
767
|
const known = sub === 'pull' || sub === 'restore' || sub === 'status' || sub === 'provision' ||
|
|
691
768
|
sub === 'adopt-serving' || sub === 'bind' || sub === 'domain' || sub === 'rebind';
|
|
692
769
|
// adopt-serving --vertical takes no positional scopeId; every other form requires one.
|
|
@@ -761,11 +838,19 @@ async function cmdScope() {
|
|
|
761
838
|
console.error(usage);
|
|
762
839
|
process.exit(1);
|
|
763
840
|
}
|
|
841
|
+
// #2347: the review the migration acknowledgement answers, as a refusal printed it.
|
|
842
|
+
const reviewFlag = flag('review');
|
|
843
|
+
const review = reviewFlag === undefined ? undefined : parseReviewToken(reviewFlag);
|
|
844
|
+
if (review === null) {
|
|
845
|
+
console.error('--review takes the token a refused rebind printed (<base>:<digest>)');
|
|
846
|
+
process.exit(1);
|
|
847
|
+
}
|
|
764
848
|
await rebindScopeVertical({
|
|
765
849
|
controlPlaneUrl, header, tenantId, scopeId: scope,
|
|
766
850
|
vertical: to, ackMigrations: argv.includes('--ack-migrations'),
|
|
767
851
|
abandonData: argv.includes('--abandon-data'),
|
|
768
852
|
ackExportBreak: argv.includes('--ack-export-break'),
|
|
853
|
+
...(review ? { review } : {}),
|
|
769
854
|
});
|
|
770
855
|
return;
|
|
771
856
|
}
|
|
@@ -943,6 +1028,8 @@ async function cmdPreview() {
|
|
|
943
1028
|
allowUnservedUi: argv.includes('--allow-unserved-ui'),
|
|
944
1029
|
// …and the same schedule check (#1646): a preview's schedules need a sweeper too.
|
|
945
1030
|
allowUnsweptSchedules: argv.includes('--allow-unswept-schedules'),
|
|
1031
|
+
// …and the same data-loss replay (#2321), which a preview's migrations meet too.
|
|
1032
|
+
requireDataLossCheck: dataLossCheckRequired(argv),
|
|
946
1033
|
linted,
|
|
947
1034
|
envSpec: meta.envSpec,
|
|
948
1035
|
ownerGrants: meta.ownerGrants,
|
|
@@ -1078,6 +1165,62 @@ async function cmdModel() {
|
|
|
1078
1165
|
console.log(`✓ ${entities} ${entities === 1 ? 'entity' : 'entities'} rendered — open it in a browser:`);
|
|
1079
1166
|
console.log(file);
|
|
1080
1167
|
}
|
|
1168
|
+
/**
|
|
1169
|
+
* `substrat changeset [dir] [--base <ref>] [--summary <text>] [--check] [--json]` — the bump,
|
|
1170
|
+
* computed from the artifacts of record rather than chosen (#2364). No control plane and no
|
|
1171
|
+
* auth: it reads the checked-in artifacts at the merge-base and in the working tree.
|
|
1172
|
+
*/
|
|
1173
|
+
function cmdChangeset() {
|
|
1174
|
+
const usage = 'usage: substrat changeset [dir] [--base <ref>] [--summary <text>] [--check] [--json]';
|
|
1175
|
+
for (const f of ['base', 'summary']) {
|
|
1176
|
+
const v = flag(f);
|
|
1177
|
+
if (argv.includes(`--${f}`) && (!v || v.startsWith('--'))) {
|
|
1178
|
+
console.error(`${usage}\n\n--${f} needs a value.`);
|
|
1179
|
+
process.exit(1);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
const booleans = ['check', 'json'];
|
|
1183
|
+
const dir = resolvePath(positional(0, booleans) ?? '.');
|
|
1184
|
+
try {
|
|
1185
|
+
const r = changesetCommand(dir, {
|
|
1186
|
+
...(flag('base') ? { base: flag('base') } : {}),
|
|
1187
|
+
...(flag('summary') ? { summary: flag('summary') } : {}),
|
|
1188
|
+
check: argv.includes('--check'),
|
|
1189
|
+
json: argv.includes('--json'),
|
|
1190
|
+
});
|
|
1191
|
+
for (const l of r.out)
|
|
1192
|
+
console.log(l);
|
|
1193
|
+
for (const l of r.err)
|
|
1194
|
+
console.error(l);
|
|
1195
|
+
if (r.code !== 0)
|
|
1196
|
+
process.exit(r.code);
|
|
1197
|
+
}
|
|
1198
|
+
catch (err) {
|
|
1199
|
+
if (err instanceof CannotRun) {
|
|
1200
|
+
console.error(`error: ${err.message}`);
|
|
1201
|
+
process.exit(2);
|
|
1202
|
+
}
|
|
1203
|
+
throw err;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
/**
|
|
1207
|
+
* `substrat concept sync [dir] [--check]` — keep `spec/concept.md`'s generated regions true.
|
|
1208
|
+
* Offline like `model view`: it reads the checked-in `model.json` and `PERMISSIONS.md`.
|
|
1209
|
+
*/
|
|
1210
|
+
async function cmdConcept() {
|
|
1211
|
+
if (argv[1] !== 'sync') {
|
|
1212
|
+
console.error('usage: substrat concept sync [dir] [--check]' +
|
|
1213
|
+
(argv[1] && !argv[1].startsWith('--') ? `\n\n'concept ${argv[1]}' is not a subcommand — 'sync' is the only one.` : ''));
|
|
1214
|
+
process.exit(1);
|
|
1215
|
+
}
|
|
1216
|
+
const check = argv.includes('--check');
|
|
1217
|
+
const { file, changed } = syncConceptFile(positional(1, ['check']) ?? '.', { check });
|
|
1218
|
+
if (check && changed) {
|
|
1219
|
+
console.error(`✗ ${file} is stale — run \`substrat concept sync\``);
|
|
1220
|
+
process.exit(1);
|
|
1221
|
+
}
|
|
1222
|
+
console.log(changed ? `✓ rewrote the generated sections of ${file}` : `✓ ${file} is up to date`);
|
|
1223
|
+
}
|
|
1081
1224
|
async function cmdWhoami() {
|
|
1082
1225
|
const { controlPlaneUrl, header } = resolveAuth({ cp: flag('cp'), token: flag('token'), tenant: flag('tenant') });
|
|
1083
1226
|
const { user, tenants } = await fetchWhoami(controlPlaneUrl, header);
|
|
@@ -1109,8 +1252,13 @@ async function main() {
|
|
|
1109
1252
|
return cmdPromote();
|
|
1110
1253
|
case 'publish':
|
|
1111
1254
|
return cmdPublish();
|
|
1255
|
+
case 'store-driver':
|
|
1256
|
+
return cmdStoreDriver();
|
|
1112
1257
|
case 'store-kind':
|
|
1113
|
-
|
|
1258
|
+
// Renamed with the concept (K-53): a store's code is its driver. Nothing to alias, since no
|
|
1259
|
+
// driver was ever registered for the old command to have acted on.
|
|
1260
|
+
console.error('`substrat store-kind` was renamed `substrat store-driver`, and its --kind flag --driver');
|
|
1261
|
+
process.exit(1);
|
|
1114
1262
|
case 'unpublish':
|
|
1115
1263
|
return cmdUnpublish();
|
|
1116
1264
|
case 'whoami':
|
|
@@ -1128,6 +1276,10 @@ async function main() {
|
|
|
1128
1276
|
return cmdInit();
|
|
1129
1277
|
case 'model':
|
|
1130
1278
|
return cmdModel();
|
|
1279
|
+
case 'changeset':
|
|
1280
|
+
return cmdChangeset();
|
|
1281
|
+
case 'concept':
|
|
1282
|
+
return cmdConcept();
|
|
1131
1283
|
case 'version':
|
|
1132
1284
|
case '--version':
|
|
1133
1285
|
case '-v':
|