@starci/hfs 4.9.9 → 4.10.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/CHANGELOG.md +33 -0
- package/README.md +20 -8
- package/db/backup-receipt.mjs +10 -47
- package/db/drift.mjs +289 -0
- package/db/dump.mjs +12 -11
- package/db/entity-list.mjs +9 -0
- package/db/migrate.mjs +84 -260
- package/db/new.mjs +46 -0
- package/db/post-cutover.mjs +13 -17
- package/db/postgres.mjs +80 -0
- package/emit/contracts.mjs +5 -9
- package/lint/run.mjs +12 -2
- package/package.json +5 -3
- package/release/gate.mjs +82 -31
- package/release/index.mjs +19 -8
- package/release/push.mjs +4 -4
- package/release/records.mjs +54 -0
- package/release/urgent.mjs +129 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +54 -24
- package/runtime/knowledge/hfs/design-truths.yaml +1 -1
- package/runtime/knowledge/hfs/fe-stack.yaml +4 -11
- package/runtime/knowledge/hfs/rules.yaml +250 -157
- package/runtime/knowledge/hfs/slots.yaml +199 -174
- package/runtime/knowledge/patterns/be/event-bus.yaml +3 -3
- package/runtime/knowledge/patterns/be/idempotency.yaml +3 -3
- package/runtime/knowledge/patterns/be/jobs.yaml +4 -4
- package/runtime/knowledge/patterns/be/projections.yaml +7 -5
- package/runtime/knowledge/patterns/be/queues.yaml +8 -8
- package/runtime/knowledge/patterns/be/reactors.yaml +2 -2
- package/runtime/knowledge/patterns/be/realtime.yaml +6 -6
- package/runtime/knowledge/patterns/be/saga.yaml +3 -3
- package/runtime/knowledge/patterns/be/webhooks.yaml +1 -1
- package/runtime/modules/kernel/command-policy.yaml +1 -1
- package/runtime/modules/kernel/failure-codes.yaml +86 -115
- package/runtime/modules/schemas/application-stacks.schema.yaml +5 -5
- package/runtime/scripts/hfs/app-wiring.mjs +23 -2
- package/runtime/scripts/hfs/architecture/canon-clones.mjs +1 -1
- package/runtime/scripts/hfs/architecture/{supabase-be.mjs → identity.mjs} +45 -15
- package/runtime/scripts/hfs/architecture/index.mjs +2 -3
- package/runtime/scripts/hfs/architecture/{supabase-ast.mjs → jwt-ast.mjs} +2 -103
- package/runtime/scripts/hfs/architecture/register-once.mjs +1 -1
- package/runtime/scripts/hfs/architecture/schema-owner.mjs +29 -4
- package/runtime/scripts/hfs/architecture/server-action.mjs +3 -4
- package/runtime/scripts/hfs/architecture/side-boundary.mjs +1 -1
- package/runtime/scripts/hfs/architecture/{supabase-keys.mjs → sql-keys.mjs} +1 -1
- package/runtime/scripts/hfs/architecture/sql-owner.mjs +25 -27
- package/runtime/scripts/hfs/architecture/transport-owner.mjs +7 -3
- package/runtime/scripts/hfs/check.mjs +8 -8
- package/runtime/scripts/hfs/coverage-scope.mjs +1 -1
- package/runtime/scripts/hfs/db-baseline-verb.mjs +48 -30
- package/runtime/scripts/hfs/db-plan-verb.mjs +17 -106
- package/runtime/scripts/hfs/db-restamp-verb.mjs +11 -10
- package/runtime/scripts/hfs/declaration-shape.mjs +15 -18
- package/runtime/scripts/hfs/manifest-shape.mjs +6 -5
- package/runtime/scripts/hfs/rule-params-shape.mjs +4 -1
- package/runtime/scripts/hfs/rules/a15-findings.mjs +1 -1
- package/runtime/scripts/hfs/rules/cli.mjs +1 -1
- package/runtime/scripts/hfs/rules/database-access.mjs +8 -74
- package/runtime/scripts/hfs/rules/database-baseline.mjs +3 -3
- package/runtime/scripts/hfs/rules/database-config.mjs +6 -214
- package/runtime/scripts/hfs/rules/database-constants.mjs +8 -10
- package/runtime/scripts/hfs/rules/database-migrations.mjs +3 -3
- package/runtime/scripts/hfs/rules/database-queues.mjs +4 -5
- package/runtime/scripts/hfs/rules/database-sql.mjs +1 -1
- package/runtime/scripts/hfs/rules/database.mjs +36 -39
- package/runtime/scripts/hfs/rules/edition.mjs +1 -1
- package/runtime/scripts/hfs/rules/{supabase-secrets.mjs → lite-secrets.mjs} +6 -6
- package/runtime/scripts/hfs/rules/script-hygiene.mjs +1 -10
- package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +5 -4
- package/runtime/scripts/hfs/rules/stacks-release.mjs +3 -2
- package/runtime/scripts/hfs/rules/stacks-runtime.mjs +2 -2
- package/runtime/scripts/hfs/rules/stacks.mjs +1 -1
- package/runtime/scripts/hfs/slot-app-view.mjs +1 -1
- package/runtime/scripts/hfs/slots.mjs +8 -2
- package/runtime/scripts/hfs/sql/pg-parse.mjs +2 -2
- package/runtime/scripts/lib/secret-patterns.mjs +2 -2
- package/runtime/scripts/lib/swarm-stack.mjs +7 -4
- package/scaffold/add-app.mjs +2 -10
- package/scaffold/add-cli-lite.mjs +6 -6
- package/scaffold/add-compose.mjs +1 -1
- package/scaffold/add-inbox-lite.mjs +4 -8
- package/scaffold/add-plan.mjs +1 -1
- package/scaffold/add-queue-lite.mjs +16 -24
- package/scaffold/add-table.mjs +34 -26
- package/scaffold/add.mjs +2 -2
- package/scaffold/app.mjs +19 -86
- package/scaffold/lite-exports.mjs +1 -1
- package/src/main.mjs +24 -22
- package/stack/cli.mjs +10 -2
- package/stack/deploy.mjs +21 -4
- package/stack/target-binding.mjs +0 -60
- package/stack/urgent-floor.mjs +147 -0
- package/sync/ci-commands.mjs +3 -1
- package/sync/index.mjs +32 -8
- package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +4 -1
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +1 -17
- package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +6 -8
- package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +4 -1
- package/templates/app/codegen-lite/scripts/codegen.mjs +2 -39
- package/templates/app/db-bootstrap/db/bootstrap.sql +139 -0
- package/templates/app/gitignore-lite +0 -3
- package/templates/app/package-scripts/db.json +9 -0
- package/templates/app/package-scripts/package.json +0 -1
- package/templates/app/package-scripts-lite/package.json +1 -1
- package/templates/app/skeleton/.starcistacks/dev/README.md +1 -1
- package/templates/app/skeleton/.starcistacks/production/README.md +1 -1
- package/templates/app/skeleton/README.md +1 -1
- package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +84 -19
- package/templates/app/skeleton-lite/.starcistacks/dev/README.md +5 -5
- package/templates/app/skeleton-lite/.starcistacks/dev/environment.json +1 -1
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/recovery/restore-proof.md +1 -1
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +69 -6
- package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +18 -7
- package/templates/app/skeleton-lite/.starcistacks/production/README.md +6 -6
- package/templates/app/skeleton-lite/.starcistacks/production/environment.json +1 -1
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/recovery/restore-proof.md +2 -2
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +69 -6
- package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +17 -9
- package/templates/app/skeleton-lite/README.md +11 -11
- package/templates/app/skeleton-lite/{supabase → db}/migrations/__timestamp___baseline.sql +21 -1
- package/templates/app/tool-config-lite/prettierignore +0 -1
- package/templates/be/inbox/database-client.spec.ts.tpl +9 -5
- package/templates/be/inbox/database-client.ts.tpl +13 -4
- package/templates/be/inbox/platform/client-operation-outcomes.sql.tpl +47 -0
- package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +3 -1
- package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +2 -2
- package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +6 -1
- package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +39 -7
- package/templates/be/inbox/platform/migration.sql.tpl +22 -0
- package/templates/be/inbox/service.spec.ts.tpl +2 -2
- package/templates/be/inbox/service.ts.tpl +1 -2
- package/templates/be/patterns/jobs/platform-lite/job-claim.service.spec.ts.tpl +156 -0
- package/templates/be/patterns/jobs/platform-lite/job-claim.service.ts.tpl +114 -0
- package/templates/be/patterns/jobs/platform-lite/jobs.sql.tpl +51 -0
- package/templates/be/patterns/queues/platform-lite/queue-outbox.service.spec.ts.tpl +62 -0
- package/templates/be/patterns/queues/platform-lite/queue-outbox.service.ts.tpl +20 -0
- package/templates/be/patterns/queues/platform-lite/queue-outbox.sql.tpl +35 -0
- package/templates/be/patterns/queues/platform-lite/queue-relay.service.spec.ts.tpl +4 -1
- package/templates/be/patterns/queues/platform-lite/queue-relay.service.ts.tpl +16 -5
- package/templates/be/patterns/realtime/channel.gateway.spec.ts.tpl +8 -8
- package/templates/be/patterns/realtime/channel.websocket.module.ts.tpl +3 -3
- package/templates/be/patterns/realtime/channel.websocket.options.ts.tpl +2 -2
- package/templates/be/skeleton/src/modules/domain/identity/identity.decorators.ts +18 -7
- package/templates/be/skeleton/src/modules/platform/composition/composition.decorators.ts +3 -0
- package/templates/be/skeleton/src/modules/platform/composition/index.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/errors/errors.filter.spec.ts +15 -1
- package/templates/be/skeleton/src/modules/platform/errors/errors.filter.ts +10 -9
- package/templates/be/skeleton/src/modules/platform/http-security/index.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/http-security/origin.guard.spec.ts +34 -18
- package/templates/be/skeleton/src/modules/platform/http-security/origin.guard.ts +29 -8
- package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.spec.ts +80 -4
- package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.ts +39 -11
- package/templates/be/skeleton-lite/apps/api/Dockerfile +0 -1
- package/templates/be/skeleton-lite/apps/api/src/api.env.ts +9 -5
- package/templates/be/skeleton-lite/apps/api/src/api.options.ts +7 -4
- package/templates/be/skeleton-lite/apps/api/src/app.module.ts +4 -4
- package/templates/be/skeleton-lite/apps/api/src/compose/domain.ts +3 -3
- package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +9 -13
- package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +4 -2
- package/templates/be/skeleton-lite/apps/cli/src/cli.env.ts +2 -2
- package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +3 -2
- package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.spec.ts +4 -4
- package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.ts +3 -3
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.contracts.ts +1 -1
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +23 -13
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.options.ts +3 -3
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/index.ts +4 -0
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.client.spec.ts +22 -0
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.client.ts +25 -0
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.config.ts +12 -0
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.decorators.ts +9 -0
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.options.ts +35 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/errors/passport.error.ts +22 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/index.ts +12 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.config.ts +23 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.cookie.ts +29 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.decorators.ts +22 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.options.ts +63 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.password.ts +11 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.tokens.spec.ts +32 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.tokens.ts +69 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/google.strategy.ts +37 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/local.strategy.ts +36 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/oauth-state.store.ts +59 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/database.functions.ts +24 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/database.module.ts +2 -1
- package/templates/be/skeleton-lite/src/modules/platform/database/database.options.ts +4 -2
- package/templates/be/skeleton-lite/src/modules/platform/database/database.sql.ts +19 -6
- package/templates/be/skeleton-lite/src/modules/platform/database/entities.ts +6 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +1 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/migration-runner.service.ts +1 -1
- package/templates/be/skeleton-lite/src/modules/platform/database/primary.config.ts +2 -2
- package/templates/be/skeleton-lite/src/modules/platform/database/seed-runner.service.ts +2 -2
- package/templates/be/skeleton-lite/src/modules/platform/http-security/index.ts +1 -1
- package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +18 -5
- package/templates/be/skeleton-lite/src/tests/fixtures/builders/cli.builder.ts +1 -1
- package/templates/be/table/entities.index.ts.tpl +4 -0
- package/templates/be/table/entity.ts.tpl +21 -0
- package/templates/be/table/index.ts.tpl +1 -0
- package/templates/be/table/module.ts.tpl +1 -1
- package/templates/be/table/service.api.ts.tpl +10 -11
- package/templates/be/table/service.ts.tpl +5 -6
- package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/auth-door.ts +30 -0
- package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +46 -14
- package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/token-store.ts +31 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/config/index.ts +2 -5
- package/db/local-migrate.mjs +0 -194
- package/db/reset.mjs +0 -125
- package/emit/db-types.mjs +0 -271
- package/runtime/scripts/api/supabase/database-dump.mjs +0 -5
- package/runtime/scripts/api/supabase/database-push.mjs +0 -5
- package/runtime/scripts/api/supabase/database-query.mjs +0 -5
- package/runtime/scripts/api/supabase/lib.mjs +0 -55
- package/runtime/scripts/api/supabase/migration-repair.mjs +0 -5
- package/runtime/scripts/hfs/architecture/supabase-tables.mjs +0 -50
- package/runtime/scripts/hfs/architecture/supabase.mjs +0 -226
- package/scaffold/supabase-ports.mjs +0 -26
- package/templates/app/package-scripts/supabase.json +0 -7
- package/templates/app/skeleton-lite/supabase/config.toml +0 -48
- package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +0 -5
- package/templates/be/inbox/platform/inbox/persistence/inbox.rows.ts.tpl +0 -33
- package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +0 -33
- package/templates/be/inbox/sql.ts.tpl +0 -16
- package/templates/be/patterns/jobs/platform-lite/persistence.jobs.sql.ts.tpl +0 -33
- package/templates/be/patterns/queues/platform-lite/persistence.queue.sql.ts.tpl +0 -12
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/errors/supabase.error.ts +0 -16
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/index.ts +0 -6
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.spec.ts +0 -27
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.ts +0 -24
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.config.ts +0 -8
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.decorators.ts +0 -17
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.jwks.ts +0 -32
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.options.ts +0 -24
- package/templates/be/table/rows.ts.tpl +0 -4
- package/templates/be/table/sql.api.ts.tpl +0 -7
- package/templates/be/table/sql.ts.tpl +0 -4
- package/templates/fe/auth/supabase-client.ts.tpl +0 -38
- package/templates/fe/skeleton/apps/__app__/src/app/auth/callback/route.ts +0 -11
- package/templates/fe/skeleton/apps/__app__/src/modules/auth/pkce-client.ts +0 -18
- package/templates/fe/skeleton/apps/__app__/src/modules/auth/server.ts +0 -19
- /package/templates/app/skeleton-lite/{supabase → db}/seed.sql +0 -0
package/src/main.mjs
CHANGED
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
// starci app ci secret set <NAME> [--env E | --dependabot] [--repo owner/name] reads the value from stdin only.
|
|
50
50
|
// starci app ci var set <NAME> [--env E] [--value v] | var sync [--execute] | verify [--json]
|
|
51
51
|
// declared GitHub CI custody; verify compares names, scopes and public values.
|
|
52
|
-
// starci app db baseline | restamp --map <restamp.yaml> | plan
|
|
52
|
+
// starci app db new <name> | baseline | restamp --map <restamp.yaml> | plan | migrate | post-cutover | dump
|
|
53
53
|
// preview the database cutover or compare its state with the repo; baseline and restamp mutate only with --apply.
|
|
54
54
|
// starci app add <api|webhook|realtime|saga|job|reactor|queue|projection> <name> [--event <event> --from <service> --service <Class>=<module>] [--owner <service> --failed <event> --done <event>] [--connection <name>] [--cwd <dir>]
|
|
55
55
|
// exactly that kind's file tree, generated FROM the files: tree of its pattern topic (knowledge/patterns/be) with the
|
|
@@ -79,7 +79,6 @@ import { main as syncMain } from '../sync/cli.mjs';
|
|
|
79
79
|
import { SyncError, bundledPresets } from '../sync/index.mjs';
|
|
80
80
|
import { managedFindings } from '../sync/managed.mjs';
|
|
81
81
|
import { emitContracts } from '../emit/contracts.mjs';
|
|
82
|
-
import { dbTypesEmitter } from '../emit/db-types.mjs';
|
|
83
82
|
import { ScaffoldError, newService, newSpec } from '../scaffold/service.mjs';
|
|
84
83
|
import { EditionRefusal } from '../scaffold/edition-gate.mjs';
|
|
85
84
|
import { addKind } from '../scaffold/add.mjs';
|
|
@@ -95,30 +94,33 @@ import { releaseMain } from '../release/index.mjs';
|
|
|
95
94
|
import { dbMigrateMain } from '../db/migrate.mjs';
|
|
96
95
|
import { dbPostCutoverMain } from '../db/post-cutover.mjs';
|
|
97
96
|
import { dbDumpMain } from '../db/dump.mjs';
|
|
98
|
-
import {
|
|
97
|
+
import { dbNewMain } from '../db/new.mjs';
|
|
98
|
+
import { dbDriftMain } from '../db/drift.mjs';
|
|
99
99
|
import { codemodMain } from '../codemod/button-intent.mjs';
|
|
100
100
|
|
|
101
101
|
export const VERBS = Object.freeze(['scaffold', 'add', 'lint', 'sync', 'check', 'upgrade', 'stack', 'release', 'explain', 'emit', 'new', 'secret', 'ci', 'db', 'hygiene', 'coverage', 'codemod']);
|
|
102
|
-
const DATABASE_VERBS = new Map([['baseline', dbBaselineMain], ['restamp', dbRestampMain], ['plan', dbPlanMain], ['migrate', dbMigrateMain], ['post-cutover', dbPostCutoverMain], ['dump', dbDumpMain], ['
|
|
102
|
+
const DATABASE_VERBS = new Map([['baseline', dbBaselineMain], ['restamp', dbRestampMain], ['plan', dbPlanMain], ['migrate', dbMigrateMain], ['post-cutover', dbPostCutoverMain], ['dump', dbDumpMain], ['new', dbNewMain], ['drift', dbDriftMain]]);
|
|
103
103
|
|
|
104
104
|
const USAGE = `starci app scaffold <name> [--into <dir>] [--edition full|lite] [--registry-visibility public|private] [--cwd <dir>]
|
|
105
105
|
starci app add <api|webhook|realtime|saga|job|reactor|queue|projection|cli|table|app|block|composite|branch|leaf|page|overlay|layout> <name> [options] [--cwd <dir>]
|
|
106
106
|
starci app lint [--cwd <dir>] [--changed <file>...] [--workspace <dir>] [--side be|fe] [--fix] [--format text|json] [--sonar <file>]
|
|
107
107
|
starci app codemod button-intent [--write] [--format json|markdown] [--report <file.json|file.md>] [--cwd <dir>]
|
|
108
108
|
starci app sync (--check | --write) [--cwd <dir>]
|
|
109
|
-
starci app check [--cwd <dir>] [--json] [--fast] [--base <ref>] [--
|
|
110
|
-
starci app db
|
|
111
|
-
starci app db
|
|
112
|
-
starci app db
|
|
113
|
-
starci app db
|
|
114
|
-
starci app db
|
|
115
|
-
starci app db migrate [--execute] [--
|
|
116
|
-
starci app db post-cutover [--execute] [--green-at <ISO> | --owner-confirms-old-app-gone] [--
|
|
117
|
-
starci app
|
|
118
|
-
starci app release
|
|
109
|
+
starci app check [--cwd <dir>] [--json] [--fast] [--base <ref>] [--wiring] [--coverage-from <lcov>]
|
|
110
|
+
starci app db new <kebab-name> [--json] [--cwd <dir>]
|
|
111
|
+
starci app db baseline [--stamp <version>] [--url-env <NAME>] [--apply] [--cwd <dir>]
|
|
112
|
+
starci app db restamp --map <restamp.yaml> [--url-env <NAME>] [--apply] [--cwd <dir>]
|
|
113
|
+
starci app db plan [--url-env <NAME>] [--json] [--cwd <dir>]
|
|
114
|
+
starci app db dump [--url-env <NAME>] --out <dir> [--schema-only] [--json] [--cwd <dir>]
|
|
115
|
+
starci app db migrate [--execute] [--url-env <NAME>] [--json] [--cwd <dir>]
|
|
116
|
+
starci app db post-cutover [--execute] [--green-at <ISO> | --owner-confirms-old-app-gone] [--url-env <NAME>] [--json] [--cwd <dir>]
|
|
117
|
+
starci app db drift [--url-env <NAME>] [--json] [--cwd <dir>]
|
|
118
|
+
starci app release gate [--execute] [--source <ref>] [--tag-prefix <prefix>] [--backfill <tag>] [--json] [--cwd <dir>]
|
|
119
|
+
starci app release push [--execute] [--remote <name>] [--urgent --reason <text>] [--json] [--cwd <dir>]
|
|
119
120
|
starci app upgrade [--plan] [--cwd <dir>]
|
|
120
121
|
starci app stack <up|down|status> [--stack <dir>] [--services <list>] [--k3d] [--force] [--json] [--cwd <dir>]
|
|
121
122
|
starci app stack <prepare|doctor|connect|disconnect|build-args|render|deploy|converge|verify|rollback|prune|backup|jobs> --env <env> [--execute] [--json] [--cwd <dir>]
|
|
123
|
+
starci app stack deploy --env <env> --source-sha <sha> --urgent [--db-url-env <NAME>] [--backup-out <dir>] [--execute] [--json] [--cwd <dir>]
|
|
122
124
|
starci app explain <path> [--cwd <dir>] [--json]
|
|
123
125
|
starci app emit [--cwd <dir>]
|
|
124
126
|
starci app new service <dir> <name> [--inject <binding>...] [--cwd <dir>]
|
|
@@ -144,7 +146,7 @@ const PER_CODE_LIMIT = 25;
|
|
|
144
146
|
const VALUE_FLAGS = new Set(['--base', '--inject', '--into', '--event', '--from', '--service', '--connection', '--owner', '--failed', '--done', '--coverage-from', '--registry-visibility', '--shapes', '--read', '--app', '--kind', '--blocks', '--tabs']);
|
|
145
147
|
/** Flags that may repeat: their values are collected in order. */
|
|
146
148
|
const LIST_FLAGS = new Set(['--inject']);
|
|
147
|
-
const BOOL_FLAGS = new Set(['--json', '--fast', '--fe', '--
|
|
149
|
+
const BOOL_FLAGS = new Set(['--json', '--fast', '--fe', '--wiring', '--form']);
|
|
148
150
|
|
|
149
151
|
function coverageArgument(arg, args, opts) {
|
|
150
152
|
const inline = arg.startsWith('--coverage-from=');
|
|
@@ -247,11 +249,11 @@ function printExplain(e, out) {
|
|
|
247
249
|
* The app pass: slots, managed files, formatter, contract snapshots and the architecture machine's check surface, root and sides.
|
|
248
250
|
* `only` limits the formatter to those (app-relative) files.
|
|
249
251
|
*/
|
|
250
|
-
async function runCheck({ repoRoot, harness, fast = false, base, only, presets, prettier
|
|
252
|
+
async function runCheck({ repoRoot, harness, fast = false, base, only, presets, prettier }) {
|
|
251
253
|
const tracked = trackedFiles(repoRoot);
|
|
252
254
|
// Managed files of the root and both sides, the .gitignore block and sonar against their render (R04, R05, R11, ...), and prettier
|
|
253
255
|
// through the app's own install (R19, never under --fast).
|
|
254
|
-
const extraFindings = [...await managedFindings({ repoRoot, tracked, presets }), ...await checkDatabase({ repoRoot, files: tracked, base
|
|
256
|
+
const extraFindings = [...await managedFindings({ repoRoot, tracked, presets }), ...await checkDatabase({ repoRoot, files: tracked, base }), ...(fast ? [] : await formatFindings({ repoRoot, files: only ?? tracked, prettier }))];
|
|
255
257
|
// R23 against the be side itself (full pass only): committed snapshots equal what `starci app emit` writes now.
|
|
256
258
|
let contracts = null;
|
|
257
259
|
let be = null;
|
|
@@ -342,7 +344,7 @@ async function installedStackMain(cwd) {
|
|
|
342
344
|
function databaseMain(argv, io) {
|
|
343
345
|
const [verb, ...rest] = argv;
|
|
344
346
|
const dispatch = DATABASE_VERBS.get(verb);
|
|
345
|
-
if (!dispatch) throw new Error('starci app db takes baseline, restamp, plan, migrate, post-cutover, dump or
|
|
347
|
+
if (!dispatch) throw new Error('starci app db takes new, baseline, restamp, plan, migrate, post-cutover, dump or drift');
|
|
346
348
|
return dispatch(rest, io);
|
|
347
349
|
}
|
|
348
350
|
|
|
@@ -354,7 +356,7 @@ function printCheckProofs(result, out) {
|
|
|
354
356
|
async function checkMain(opts, { cwd: repoRoot, harness, edition, stdout: out, presets, prettier, checkApp = runCheck, runLinter }) {
|
|
355
357
|
if (opts.positional.length) throw new Error('starci app check takes no path');
|
|
356
358
|
if (opts.base !== undefined && opts.fast !== true) throw new Error('--base names the ref --fast compares with; it needs --fast');
|
|
357
|
-
const checked = await checkEdition({ repoRoot, edition, normal: () => checkApp({ repoRoot, harness, fast: opts.fast === true, base: opts.base, presets, prettier
|
|
359
|
+
const checked = await checkEdition({ repoRoot, edition, normal: () => checkApp({ repoRoot, harness, fast: opts.fast === true, base: opts.base, presets, prettier }), presets, prettier });
|
|
358
360
|
const proof = withCheckProofs(checked, { repoRoot, edition, wiring: opts.wiring === true, coverageFrom: opts['coverage-from'] });
|
|
359
361
|
const changed = opts.fast === true ? changedSince(repoRoot, proof.fast?.base ?? opts.base).files : null;
|
|
360
362
|
const existing = changed ? changed.filter(file => fs.existsSync(path.join(repoRoot, file))) : [];
|
|
@@ -395,7 +397,7 @@ function newMain(opts, { cwd: repoRoot, stdout: out }) {
|
|
|
395
397
|
function addMain(opts, { cwd: repoRoot, stdout: out }) {
|
|
396
398
|
const [noun, name, ...extra] = opts.positional;
|
|
397
399
|
if (!noun || !name || extra.length) throw new Error('starci app add takes `<noun> <name>` and the options of the noun');
|
|
398
|
-
const { created, registered } = addKind({ repoRoot, noun, name, options: { event: opts.event, from: opts.from, service: opts.service, connection: opts.connection, owner: opts.owner, failed: opts.failed, done: opts.done, fe: opts.fe,
|
|
400
|
+
const { created, registered } = addKind({ repoRoot, noun, name, options: { event: opts.event, from: opts.from, service: opts.service, connection: opts.connection, owner: opts.owner, failed: opts.failed, done: opts.done, fe: opts.fe, shapes: opts.shapes, read: opts.read, app: opts.app, kind: opts.kind, form: opts.form, blocks: opts.blocks, tabs: opts.tabs } });
|
|
399
401
|
for (const file of created) out(`created ${file}\n`);
|
|
400
402
|
const kinds = registered.kinds.length ? `; kinds ${registered.kinds.join(', ')}` : '';
|
|
401
403
|
// A noun that registers nothing in hfs.json (a table, an app, a front-end unit) ends with what it wrote.
|
|
@@ -434,7 +436,7 @@ function declaredEdition(cwd) {
|
|
|
434
436
|
*/
|
|
435
437
|
async function stackAppMain(argv, { cwd, edition, stackMain, stdout: out, stderr: err, env, sops, stackIo }) {
|
|
436
438
|
if (STACK_ACTIONS.includes(argv[0])) return deployStackMain(argv, { ...stackIo, cwd, stdout: out, stderr: err, env, ...(sops ? { sops } : {}) });
|
|
437
|
-
if ((edition ?? declaredEdition(cwd)) === 'lite') throw new Error('starci app stack up|down|status manage the local test world of the full edition; a lite app starts its local
|
|
439
|
+
if ((edition ?? declaredEdition(cwd)) === 'lite') throw new Error('starci app stack up|down|status manage the local test world of the full edition; a lite app starts its local database with npm run db:start');
|
|
438
440
|
const run = stackMain ?? await installedStackMain(cwd);
|
|
439
441
|
return run(argv, { cwd, out, err });
|
|
440
442
|
}
|
|
@@ -457,7 +459,7 @@ function errorMessage(error) {
|
|
|
457
459
|
|
|
458
460
|
/**
|
|
459
461
|
* `harness` ({ version }) is the CLI that runs this package in-process: its version is the starci-harness an app is judged and
|
|
460
|
-
* scaffolded at (runtime/scripts/hfs/canon-pins.mjs). `run` injects the
|
|
462
|
+
* scaffolded at (runtime/scripts/hfs/canon-pins.mjs). `run` injects the database runner, `runLinter` the linter transport, `checkApp`
|
|
461
463
|
* the HFS check and `stackIo` Swarm tools; production defaults execute every check.
|
|
462
464
|
*/
|
|
463
465
|
export async function main(argv, {
|
package/stack/cli.mjs
CHANGED
|
@@ -14,8 +14,8 @@ const DOCKER_ACTIONS = new Set(['deploy', 'accept', 'converge', 'verify', 'rollb
|
|
|
14
14
|
|
|
15
15
|
export const ACTIONS = Object.freeze(['prepare', 'doctor', 'connect', 'disconnect', 'build-args', 'render', 'deploy', 'converge', 'verify', 'rollback', 'prune', 'backup', 'jobs', 'task', 'ingress', 'retire', 'build', 'accept']);
|
|
16
16
|
|
|
17
|
-
const BOOLEAN = new Set(['--execute', '--json', '--swarm-state', '--init-swarm', '--owner-approved', '--fix-live-restore', '--skip-public', '--skip-internal', '--allow-unpinned', '--push']);
|
|
18
|
-
const VALUE = new Set(['--env', '--image', '--source-sha', '--previous', '--out', '--release', '--since', '--timeout', '--retry', '--keep', '--app', '--ssh-cmd', '--extra-compose', '--project', '--target-binding']);
|
|
17
|
+
const BOOLEAN = new Set(['--execute', '--json', '--swarm-state', '--init-swarm', '--owner-approved', '--fix-live-restore', '--skip-public', '--skip-internal', '--allow-unpinned', '--push', '--urgent']);
|
|
18
|
+
const VALUE = new Set(['--env', '--image', '--source-sha', '--previous', '--out', '--release', '--since', '--timeout', '--retry', '--keep', '--app', '--ssh-cmd', '--extra-compose', '--project', '--target-binding', '--db-url-env', '--backup-out']);
|
|
19
19
|
const REPEAT = new Set(['--image', '--keep', '--extra-compose', '--project']);
|
|
20
20
|
|
|
21
21
|
/** Splits argv into { flags, positional }; an unknown flag or a flag without its value is a refusal. */
|
|
@@ -71,6 +71,7 @@ const OPTIONS = {
|
|
|
71
71
|
deploy: ({ flags, cwd }) => ({
|
|
72
72
|
images: parseImages(flags.image), sourceSha: flags['source-sha'], execute: flags.execute === true,
|
|
73
73
|
out: flags.out ? path.resolve(cwd, flags.out) : null, timeoutSeconds: number(flags, 'timeout'), retrySeconds: number(flags, 'retry'), skipPublic: flags['skip-public'] === true,
|
|
74
|
+
urgent: flags.urgent === true, dbUrlEnv: flags['db-url-env'], backupOut: flags['backup-out'] ? path.resolve(cwd, flags['backup-out']) : undefined,
|
|
74
75
|
}),
|
|
75
76
|
converge: ({ flags }) => ({ since: flags.since, timeoutSeconds: number(flags, 'timeout') }),
|
|
76
77
|
verify: ({ flags }) => ({ retrySeconds: number(flags, 'retry'), skipPublic: flags['skip-public'] === true, skipInternal: flags['skip-internal'] === true }),
|
|
@@ -125,6 +126,8 @@ export async function stackMain(argv, io = {}) {
|
|
|
125
126
|
if (binding && (!['deploy', 'verify'].includes(action) || flags['ssh-cmd'] || flags['skip-public'] || flags['skip-internal']))
|
|
126
127
|
throw refusal('STACK_USAGE', 'the bound native consumer supports only complete deploy or verify');
|
|
127
128
|
const config = bindStackTarget(ctx, loadDeployConfig({ repoRoot: ctx.cwd, env: flags.env ?? 'production' }), binding);
|
|
129
|
+
if (action !== 'deploy' && (flags.urgent || flags['db-url-env'] || flags['backup-out'])) throw refusal('STACK_USAGE', '--urgent, --db-url-env and --backup-out belong to deploy');
|
|
130
|
+
if (action === 'deploy' && !flags.urgent && (flags['db-url-env'] || flags['backup-out'])) throw refusal('STACK_USAGE', '--db-url-env and --backup-out choose the urgent floor\'s database and backup folder: add --urgent');
|
|
128
131
|
const options = OPTIONS[action]({ flags, positional, cwd: ctx.cwd });
|
|
129
132
|
const handler = await handlerOf(action, io.importer ?? ((file) => import(file)));
|
|
130
133
|
const sshCmd = flags['ssh-cmd'] ?? null;
|
|
@@ -132,6 +135,11 @@ export async function stackMain(argv, io = {}) {
|
|
|
132
135
|
const toolContext = nativeToolContext(ctx, binding);
|
|
133
136
|
const result = DOCKER_ACTIONS.has(action) ? await withDockerTransport(toolContext, config, { sshCmd }, call) : await call(toolContext);
|
|
134
137
|
print(ctx, action, flags, result);
|
|
138
|
+
// A floor step of an urgent deploy that failed is a refusal with its own code (urgent-floor.mjs), printed after the lines that led to it.
|
|
139
|
+
if (result.refused) {
|
|
140
|
+
ctx.stderr(`starci app stack: ${result.refused.code}: ${result.refused.message}\n`);
|
|
141
|
+
return EXIT.refused;
|
|
142
|
+
}
|
|
135
143
|
return result.ok ? EXIT.ok : EXIT.failed;
|
|
136
144
|
} catch (error) {
|
|
137
145
|
if (!(error instanceof StackError)) throw error;
|
package/stack/deploy.mjs
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// deploy: render, provision, store the release record, pre-pull, `docker stack deploy`, converge, post-deploy tasks (deploy.postDeploy, when declared), verify, accept, prune. A failure at or
|
|
3
3
|
// after the stack deploy redeploys the previous accepted release from its STORED bytes; a failure before it leaves the services untouched.
|
|
4
4
|
// Without `execute` a deploy renders locally and prints the plan: NO remote call at all. A rollback cannot undo data effects.
|
|
5
|
+
// An urgent deploy (`urgent`, owner ruling urgent-release 2026-10-29) runs the safety floor of urgent-floor.mjs first (fresh verified encrypted
|
|
6
|
+
// backup with a test decrypt, rollback target, migrate preview then execute) and refuses with the floor's own code when a step of it fails.
|
|
5
7
|
//
|
|
6
8
|
// `deps` are the collaborators, all factories or functions with the signature of their module, defaulting to lazy dynamic imports so this
|
|
7
9
|
// module never needs render.mjs / provision.mjs / remote.mjs at load time (the specs inject stubs):
|
|
@@ -21,6 +23,7 @@ import { postDeployStack } from './post-deploy.mjs';
|
|
|
21
23
|
import { pruneStack } from './prune.mjs';
|
|
22
24
|
import { parseYaml } from '../runtime/engine/yaml.mjs';
|
|
23
25
|
import { createReleaseStore } from './release-store.mjs';
|
|
26
|
+
import { urgentDeploy } from './urgent-floor.mjs';
|
|
24
27
|
import { verifyStack } from './verify.mjs';
|
|
25
28
|
|
|
26
29
|
/** Loads `file` (relative to this module). Split out so a spec can load a real sibling module through `lazy`. */
|
|
@@ -39,8 +42,11 @@ export const DEFAULT_DEPS = Object.freeze({
|
|
|
39
42
|
pruneStack,
|
|
40
43
|
releaseStore: createReleaseStore,
|
|
41
44
|
ssh: lazy('./remote.mjs', 'ownerSshRunner'),
|
|
45
|
+
floor: {},
|
|
42
46
|
});
|
|
43
47
|
|
|
48
|
+
const fileSha256 = (file) => createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
49
|
+
|
|
44
50
|
const IMAGE_LINE = /^[ \t]+image:[ \t]*(\S+)[ \t]*$/gmu;
|
|
45
51
|
const QUOTE = /^["']|["']$/gu;
|
|
46
52
|
const IMAGE_REF = /^[A-Za-z0-9][A-Za-z0-9./_:@-]*$/u;
|
|
@@ -212,7 +218,7 @@ async function deployExecute(ctx, config, options, d, out, cleanups) {
|
|
|
212
218
|
}
|
|
213
219
|
|
|
214
220
|
/**
|
|
215
|
-
* Deploys a release: `options` = { images, sourceSha, execute, out, timeoutSeconds, retrySeconds, skipPublic, sshCmd }.
|
|
221
|
+
* Deploys a release: `options` = { images, sourceSha, execute, out, timeoutSeconds, retrySeconds, skipPublic, sshCmd, urgent, dbUrlEnv, backupOut }.
|
|
216
222
|
* skipPublic (the first deploy into a shared ingress, whose public routes exist only after `ingress publish`): the verification checks only what runs
|
|
217
223
|
* inside the stack, and the release stays PENDING (stored and deployed, pointer not moved) until `acceptRelease` has verified it in full.
|
|
218
224
|
* Returns { ok, lines, data: { releaseId, previous, steps: [{ step, ok }], rolledBack } }.
|
|
@@ -224,15 +230,26 @@ export async function deployRelease(ctx, config, options = {}, deps = {}) {
|
|
|
224
230
|
const out = options.out ?? made;
|
|
225
231
|
const cleanups = [];
|
|
226
232
|
try {
|
|
227
|
-
|
|
228
|
-
const
|
|
229
|
-
|
|
233
|
+
// The local render and its inspection: a release that cannot render is refused before anything remote happens.
|
|
234
|
+
const render = async () => {
|
|
235
|
+
const rendered = await d.renderRelease(ctx, config, renderOptions(options, out, null));
|
|
236
|
+
return { rendered, releaseId: inspectRendered(out, config).releaseId };
|
|
237
|
+
};
|
|
238
|
+
const dryRun = async () => {
|
|
239
|
+
const { rendered, releaseId } = await render();
|
|
230
240
|
const post = config.postDeploy.length > 0 ? ` post-deploy tasks (${config.postDeploy.join(', ')}),` : '';
|
|
231
241
|
const plan = options.skipPublic
|
|
232
242
|
? `dry run: release ${releaseId} would be provisioned, stored, pre-pulled, deployed, converged,${post} and verified inside the stack (public hosts skipped); it would stay pending until ingress publish and stack accept`
|
|
233
243
|
: `dry run: release ${releaseId} would be provisioned, stored, pre-pulled, deployed, converged,${post} verified, accepted`;
|
|
234
244
|
return { ok: true, lines: [...(rendered.lines ?? []), plan], data: { releaseId, steps: [{ step: 'render', ok: true }], executed: false } };
|
|
245
|
+
};
|
|
246
|
+
if (options.urgent === true) {
|
|
247
|
+
return await urgentDeploy(ctx, config, options, {
|
|
248
|
+
render, dryRun, openStore: () => openStore(ctx, config, options, d, cleanups), deployExecute: () => deployExecute(ctx, config, options, d, out, cleanups), sha256: fileSha256,
|
|
249
|
+
}, d.floor);
|
|
235
250
|
}
|
|
251
|
+
if (options.execute !== true) return await dryRun();
|
|
252
|
+
await render();
|
|
236
253
|
return await deployExecute(ctx, config, options, d, out, cleanups);
|
|
237
254
|
} finally {
|
|
238
255
|
cleanups.forEach((close) => close());
|
package/stack/target-binding.mjs
CHANGED
|
@@ -6,7 +6,6 @@ import { refusal } from './errors.mjs';
|
|
|
6
6
|
const slug = value => typeof value === 'string' && /^[a-z][a-z0-9-]*$/.test(value);
|
|
7
7
|
const label = value => typeof value === 'string' && /^[A-Za-z0-9_][A-Za-z0-9_.-]*$/.test(value);
|
|
8
8
|
const host = value => typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9.-]*$/.test(value);
|
|
9
|
-
const port = value => Number.isInteger(value) && value > 0 && value <= 65535;
|
|
10
9
|
const denied = () => { throw refusal('STACKS_DEPLOY_CONFIG_INVALID', 'the native endpoint binding is invalid or differs from the selected owner target'); };
|
|
11
10
|
const exact = (value, fields) => value && typeof value === 'object' && !Array.isArray(value)
|
|
12
11
|
&& Object.keys(value).length === fields.length && fields.every(key => Object.hasOwn(value, key));
|
|
@@ -28,44 +27,11 @@ function ordinaryInput(root, file) {
|
|
|
28
27
|
/** A bound native owner opens only an ordinary envelope below its actual app, without ancestor links. */
|
|
29
28
|
export const assertBoundCustody = (root, file) => ordinaryInput(root, file);
|
|
30
29
|
|
|
31
|
-
/** Bound database commands require the app-local launcher; they cannot discover a global replacement. */
|
|
32
|
-
export function boundSupabaseLauncher(root) {
|
|
33
|
-
const launcher = path.join(root, 'node_modules', 'supabase', 'dist', 'supabase.js');
|
|
34
|
-
ordinaryInput(root, launcher);
|
|
35
|
-
return launcher;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
30
|
/** Native custody can use AGE; tools that do not decrypt custody must not inherit that identity. */
|
|
39
31
|
export function nativeToolEnvironment(env) {
|
|
40
32
|
return Object.fromEntries(Object.entries(env).filter(([key]) => !/^SOPS_/i.test(key)));
|
|
41
33
|
}
|
|
42
34
|
|
|
43
|
-
/** Bound database tools receive only their selected private password and native routing, without ambient libpq or provider credentials. */
|
|
44
|
-
export function nativeDatabaseEnvironment(env, binding, selected = {}, omit = []) {
|
|
45
|
-
const omitted = new Set(omit.filter(value => typeof value === 'string').map(value => value.toUpperCase()));
|
|
46
|
-
return { ...Object.fromEntries(Object.entries(nativeToolEnvironment(env)).filter(([key]) => !/^(?:PG[A-Z_]*|SUPABASE_)/i.test(key) && !omitted.has(key.toUpperCase()))),
|
|
47
|
-
'SUPABASE_ENV': binding.environment, ...selected };
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** Preserve a logical linked project while selecting a fixed native SQL endpoint without the CLI's linked Management finalizer. */
|
|
51
|
-
export function boundLinkedDatabaseUrl(binding, savedPooler = null) {
|
|
52
|
-
if (!binding || binding.mode !== 'linked' || !/^[a-z0-9]{20}$/.test(binding.projectRef)) denied();
|
|
53
|
-
if (savedPooler === null) return `postgresql://postgres@db.${binding.projectRef}.supabase.co:5432/postgres?sslmode=require`;
|
|
54
|
-
let url;
|
|
55
|
-
try {
|
|
56
|
-
if (typeof savedPooler !== 'string' || savedPooler.length > 8192) denied();
|
|
57
|
-
url = new URL(savedPooler.trim());
|
|
58
|
-
if (!['postgres:', 'postgresql:'].includes(url.protocol) || !/^[A-Za-z0-9.-]+\.pooler\.supabase\.com$/.test(url.hostname)
|
|
59
|
-
|| decodeURIComponent(url.username) !== `postgres.${binding.projectRef}` || url.pathname !== '/postgres'
|
|
60
|
-
|| url.port && !['5432', '6543'].includes(url.port) || url.hash
|
|
61
|
-
|| [...url.searchParams].some(([key, value]) => key !== 'sslmode' || !['require', 'verify-full'].includes(value))) denied();
|
|
62
|
-
} catch { denied(); }
|
|
63
|
-
url.password = '';
|
|
64
|
-
url.port = '5432';
|
|
65
|
-
if (!url.searchParams.has('sslmode')) url.searchParams.set('sslmode', 'require');
|
|
66
|
-
return url.toString();
|
|
67
|
-
}
|
|
68
|
-
|
|
69
35
|
/** Parse the internal public binding; ordinary commands without this flag retain their native behavior. */
|
|
70
36
|
export function parseTargetBinding(text, kind) {
|
|
71
37
|
if (text === undefined) return null;
|
|
@@ -75,16 +41,6 @@ export function parseTargetBinding(text, kind) {
|
|
|
75
41
|
if (kind === 'docker-swarm') {
|
|
76
42
|
if (!exact(binding, ['kind', 'environment', 'stack', 'host', 'user', 'port']) || !slug(binding.stack)
|
|
77
43
|
|| !host(binding.host) || !label(binding.user) || binding.port !== 22) denied();
|
|
78
|
-
} else if (kind === 'supabase-db') {
|
|
79
|
-
if (binding.mode === 'local') {
|
|
80
|
-
if (!exact(binding, ['kind', 'mode', 'environment', 'localProject', 'port']) || !label(binding.localProject) || !port(binding.port)) denied();
|
|
81
|
-
} else if (binding.mode === 'linked') {
|
|
82
|
-
if (!exact(binding, ['kind', 'mode', 'environment', 'projectRef']) || !/^[a-z0-9]{20}$/.test(binding.projectRef ?? '')) denied();
|
|
83
|
-
} else if (binding.mode === 'url-env') {
|
|
84
|
-
if (!exact(binding, ['kind', 'mode', 'environment', 'urlEnv', 'host', 'port', 'database', 'user'])
|
|
85
|
-
|| !/^[A-Z][A-Z0-9_]*$/.test(binding.urlEnv ?? '') || !host(binding.host) || !port(binding.port)
|
|
86
|
-
|| !label(binding.database) || !label(binding.user)) denied();
|
|
87
|
-
} else denied();
|
|
88
44
|
} else denied();
|
|
89
45
|
return Object.freeze(binding);
|
|
90
46
|
}
|
|
@@ -107,19 +63,3 @@ export function assertStackAccount(binding, account, knownHosts) {
|
|
|
107
63
|
if (!binding) return;
|
|
108
64
|
if (account !== `${binding.user}@${binding.host}` || binding.port !== 22 || !knownHosts.trim()) denied();
|
|
109
65
|
}
|
|
110
|
-
|
|
111
|
-
/** Compare the effective local Supabase identity after its existing config/environment resolver. */
|
|
112
|
-
export function assertLocalDatabase(binding, target) {
|
|
113
|
-
if (binding && (binding.mode !== 'local' || binding.localProject !== target.project || binding.port !== target.port)) denied();
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/** Refuse a selected URL unless its non-secret endpoint exactly matches the accepted target. */
|
|
117
|
-
export function assertUrlDatabase(binding, value) {
|
|
118
|
-
if (!binding) return;
|
|
119
|
-
let url;
|
|
120
|
-
try { url = new URL(value); } catch { denied(); }
|
|
121
|
-
if (binding.mode !== 'url-env' || !['postgres:', 'postgresql:'].includes(url.protocol)
|
|
122
|
-
|| url.hostname !== binding.host || Number(url.port || '5432') !== binding.port
|
|
123
|
-
|| decodeURIComponent(url.username) !== binding.user || decodeURIComponent(url.pathname.slice(1)) !== binding.database
|
|
124
|
-
|| url.search || url.hash || !url.password) denied();
|
|
125
|
-
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// urgent-floor.mjs - the safety floor of an urgent deploy (`starci app stack deploy --urgent`, owner ruling urgent-release 2026-10-29). An urgent
|
|
2
|
+
// release skips CI and the full gate, never this floor; each step that fails is a refusal with its own code, never a warning:
|
|
3
|
+
// record the commit carries a pushed URGENT record (release/urgent.mjs) STACK_URGENT_NO_RECORD, STACK_URGENT_NOT_PUSHED
|
|
4
|
+
// backup a fresh full database backup, encrypted to the app's age recipients (db dump) STACK_URGENT_BACKUP_FAILED
|
|
5
|
+
// verified its receipt names this run, its size, age header and sha256 match the archive STACK_URGENT_BACKUP_UNVERIFIED
|
|
6
|
+
// decrypt a test decrypt with the operator's age identity (SOPS_AGE_KEY_FILE) lists every part STACK_URGENT_DECRYPT_UNAVAILABLE, STACK_URGENT_DECRYPT_FAILED
|
|
7
|
+
// render the release renders locally (deploy.mjs inspectRendered) its own STACKS_/STACK_DEPLOY_ refusal
|
|
8
|
+
// rollback an accepted previous release exists, so a red deploy has somewhere to go back to STACK_URGENT_NO_ROLLBACK_TARGET
|
|
9
|
+
// migrate db migrate previews the plan, then executes it STACK_URGENT_MIGRATE_PREVIEW_RED, STACK_URGENT_MIGRATE_FAILED
|
|
10
|
+
// deploy the release with its readiness verify (deploy.verify); red rolls back by itself STACK_URGENT_RED_ROLLED_BACK, STACK_URGENT_ROLLBACK_FAILED,
|
|
11
|
+
// STACK_URGENT_DEPLOY_NOT_STARTED (red before the stack deploy)
|
|
12
|
+
// The public-host checks are never skipped (STACK_URGENT_SKIP_PUBLIC). A rollback cannot undo the migrations: the backup above is their way back.
|
|
13
|
+
// `deps` are the collaborators (backup, decrypt, migrate), each defaulting to the app's own db verbs; a spec injects stubs.
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import os from 'node:os';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { readDeclaration } from '../release/index.mjs';
|
|
18
|
+
import { releaseDir } from '../release/gate.mjs';
|
|
19
|
+
import { readUrgent } from '../release/records.mjs';
|
|
20
|
+
import { refusal } from './errors.mjs';
|
|
21
|
+
|
|
22
|
+
const AGE_HEADER = 'age-encryption.org/v1\n';
|
|
23
|
+
const SHA = /^[a-f0-9]{64}$/u;
|
|
24
|
+
|
|
25
|
+
/** The database a floor step reaches: the connection named by --db-url-env (default DATABASE_URL). */
|
|
26
|
+
const targetOf = (options) => (options.dbUrlEnv ? { urlEnv: options.dbUrlEnv } : {});
|
|
27
|
+
|
|
28
|
+
/** The backups of an urgent deploy live outside every repository: --backup-out, else ~/.starci-backups/<project>. */
|
|
29
|
+
const backupDir = (config, options) => options.backupOut ?? path.join(os.homedir(), '.starci-backups', config.project);
|
|
30
|
+
|
|
31
|
+
async function defaultBackup(ctx, { target, out }) {
|
|
32
|
+
const { dumpDatabase } = await import('../db/dump.mjs');
|
|
33
|
+
return dumpDatabase(ctx, { ...target, out }).data;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function defaultDecrypt(ctx, { archive, identity, parts }) {
|
|
37
|
+
const { backupTool } = await import('../db/backup-receipt.mjs');
|
|
38
|
+
const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'starci-decrypt-'));
|
|
39
|
+
try {
|
|
40
|
+
backupTool(ctx, 'age', ['-d', '-i', identity, '-o', path.join(scratch, 'backup.tar'), archive]);
|
|
41
|
+
const listed = String(backupTool(ctx, 'tar', ['-tf', 'backup.tar'], { cwd: scratch }).stdout ?? '').split(/\r?\n/u).map((line) => line.replace(/^\.\//u, '').trim());
|
|
42
|
+
const missing = parts.filter((part) => !listed.includes(part));
|
|
43
|
+
if (missing.length > 0) throw new Error(`the decrypted archive lacks ${missing.join(', ')}`);
|
|
44
|
+
return { parts: listed.filter(Boolean) };
|
|
45
|
+
} finally {
|
|
46
|
+
fs.rmSync(scratch, { recursive: true, force: true });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function defaultMigrate(ctx, { target, execute }) {
|
|
51
|
+
const { loadDatabase, migrateDatabase } = await import('../db/migrate.mjs');
|
|
52
|
+
// The urgent deploy's --execute is the arming of this one migration: the floor runs it, it never waits for a second switch.
|
|
53
|
+
const armed = Object.create(ctx, { env: { value: { ...ctx.env, STARCI_DB_ARMED: '1' } } });
|
|
54
|
+
return migrateDatabase(armed, loadDatabase(ctx.cwd), { ...target, execute });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const FLOOR_DEPS = Object.freeze({ backup: defaultBackup, decrypt: defaultDecrypt, migrate: defaultMigrate });
|
|
58
|
+
|
|
59
|
+
/** A refused floor step: the lines so far and the code; the CLI prints both and exits 2. */
|
|
60
|
+
const refused = (lines, code, message, data = {}) => ({ ok: false, refused: { code, message }, lines, data: { ...data, refused: code } });
|
|
61
|
+
|
|
62
|
+
/** Problems of the backup receipt against its archive: written by this run, a full backup, its size, age header and sha256 equal to the file's. */
|
|
63
|
+
function receiptProblems(receipt, archive, startedMs, sha256) {
|
|
64
|
+
const problems = [];
|
|
65
|
+
if (!receipt || typeof receipt !== 'object') return ['no receipt'];
|
|
66
|
+
if (receipt.schemaOnly !== false) problems.push('the backup is not a full (data) backup');
|
|
67
|
+
if (!Number.isFinite(Date.parse(receipt.at)) || Date.parse(receipt.at) < startedMs - 1000) problems.push('the receipt is not from this run');
|
|
68
|
+
if (!fs.existsSync(archive)) return [...problems, 'the archive is missing'];
|
|
69
|
+
const size = fs.statSync(archive).size;
|
|
70
|
+
const header = Buffer.alloc(AGE_HEADER.length);
|
|
71
|
+
const fd = fs.openSync(archive, 'r');
|
|
72
|
+
try { fs.readSync(fd, header, 0, header.length, 0); } finally { fs.closeSync(fd); }
|
|
73
|
+
if (header.toString('ascii') !== AGE_HEADER) problems.push('the archive has no age header');
|
|
74
|
+
if (size !== receipt.bytes) problems.push(`the archive is ${size} bytes, the receipt says ${receipt.bytes}`);
|
|
75
|
+
if (!SHA.test(String(receipt.sha256 ?? '')) || sha256(archive) !== receipt.sha256) problems.push('the archive sha256 differs from the receipt');
|
|
76
|
+
return problems;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** What the floor needs before anything runs: the pushed urgent record, a declared database, the identity of the test decrypt, the full public verify. */
|
|
80
|
+
function preconditions(ctx, config, options) {
|
|
81
|
+
if (options.skipPublic) throw refusal('STACK_URGENT_SKIP_PUBLIC', 'an urgent deploy verifies the public hosts and accepts the release: drop --skip-public');
|
|
82
|
+
const record = readUrgent(releaseDir(ctx), options.sourceSha);
|
|
83
|
+
if (!record) throw refusal('STACK_URGENT_NO_RECORD', `${options.sourceSha} has no urgent record: push it with starci app release push --urgent --reason "<text>" --execute first`);
|
|
84
|
+
if (record.pushed !== true) throw refusal('STACK_URGENT_NOT_PUSHED', `the urgent release ${record.tag} of ${options.sourceSha.slice(0, 10)} was never pushed`);
|
|
85
|
+
if (!readDeclaration(config.repoRoot ?? ctx.cwd).database) throw refusal('STACK_URGENT_NO_DATABASE', 'the floor backs up and migrates the database the `database:` block declares, and this app declares none');
|
|
86
|
+
const identity = ctx.env.SOPS_AGE_KEY_FILE;
|
|
87
|
+
if (!identity || !fs.existsSync(identity)) throw refusal('STACK_URGENT_DECRYPT_UNAVAILABLE', 'the test decrypt of the backup reads the age identity named by SOPS_AGE_KEY_FILE, which is not set or not a file');
|
|
88
|
+
return { record, identity };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The floor's steps before the deploy: backup, verify and test decrypt (before anything remote), the local render, the rollback target, migrate
|
|
93
|
+
* preview and execute. Returns null when all passed, else the refused result.
|
|
94
|
+
*/
|
|
95
|
+
async function guard(ctx, config, options, { f, run, identity, lines }) {
|
|
96
|
+
const target = targetOf(options);
|
|
97
|
+
const started = ctx.now();
|
|
98
|
+
let backup;
|
|
99
|
+
try { backup = await f.backup(ctx, { target, out: backupDir(config, options) }); } catch (error) {
|
|
100
|
+
return refused(lines, 'STACK_URGENT_BACKUP_FAILED', `the fresh database backup failed (${String(error?.code ?? error?.message ?? error)}); nothing was migrated or deployed`);
|
|
101
|
+
}
|
|
102
|
+
const archive = path.join(path.dirname(backup.receiptPath), backup.receipt?.file ?? '');
|
|
103
|
+
const problems = receiptProblems(backup.receipt, archive, started, run.sha256);
|
|
104
|
+
if (problems.length > 0) return refused(lines, 'STACK_URGENT_BACKUP_UNVERIFIED', `${problems.join('; ')}; nothing was migrated or deployed`);
|
|
105
|
+
lines.push(`floor: backup ${path.basename(archive)} (${backup.receipt.bytes} bytes, sha256 ${backup.receipt.sha256.slice(0, 12)}) receipt ${backup.receiptPath}`);
|
|
106
|
+
try { await f.decrypt(ctx, { archive, identity, parts: backup.receipt.parts }); } catch (error) {
|
|
107
|
+
return refused(lines, 'STACK_URGENT_DECRYPT_FAILED', `the test decrypt of the backup failed (${String(error?.code ?? error?.message ?? error)}); nothing was migrated or deployed`);
|
|
108
|
+
}
|
|
109
|
+
lines.push(`floor: test decrypt listed ${backup.receipt.parts.length} part(s)`);
|
|
110
|
+
const { releaseId } = await run.render();
|
|
111
|
+
lines.push(`floor: release ${releaseId} rendered`);
|
|
112
|
+
const store = await run.openStore();
|
|
113
|
+
if (!(await store.current())) return refused(lines, 'STACK_URGENT_NO_ROLLBACK_TARGET', 'no accepted previous release is stored on the manager, so a red urgent deploy could not roll back; nothing was migrated or deployed');
|
|
114
|
+
let preview;
|
|
115
|
+
try { preview = await f.migrate(ctx, { target, execute: false }); } catch (error) { preview = { ok: false, lines: [String(error?.message ?? error)] }; }
|
|
116
|
+
lines.push(...(preview.lines ?? []).map((line) => `floor: migrate preview: ${line}`));
|
|
117
|
+
if (!preview.ok) return refused(lines, 'STACK_URGENT_MIGRATE_PREVIEW_RED', 'the migration preview is red; nothing was migrated or deployed');
|
|
118
|
+
let applied;
|
|
119
|
+
try { applied = await f.migrate(ctx, { target, execute: true }); } catch (error) { applied = { ok: false, lines: [String(error?.message ?? error)] }; }
|
|
120
|
+
lines.push(...(applied.lines ?? []).map((line) => `floor: migrate: ${line}`));
|
|
121
|
+
if (!applied.ok) return refused(lines, 'STACK_URGENT_MIGRATE_FAILED', `the migration failed; nothing was deployed (restore from ${backup.receiptPath} if the database is half migrated)`);
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* An urgent deploy: the floor's preconditions, then (with execute) its guard steps, then `deployExecute` (the release, its readiness verify and the
|
|
127
|
+
* automatic rollback). `run` = { render(), openStore(), deployExecute(), dryRun(), sha256 }. Returns { ok, lines, data } or a refused result whose
|
|
128
|
+
* data names the urgent release.
|
|
129
|
+
*/
|
|
130
|
+
export async function urgentDeploy(ctx, config, options, run, deps = {}) {
|
|
131
|
+
const f = { ...FLOOR_DEPS, ...deps };
|
|
132
|
+
const { record, identity } = preconditions(ctx, config, options);
|
|
133
|
+
const lines = [`urgent ${record.tag} (${options.sourceSha.slice(0, 10)}) by ${record.role}: ${record.reason}`];
|
|
134
|
+
if (options.execute !== true) {
|
|
135
|
+
const plan = await run.dryRun();
|
|
136
|
+
return { ...plan, lines: [...lines, 'floor (never skipped): fresh encrypted database backup, receipt verify, test decrypt, rollback target, migrate preview then execute, deploy with readiness verify and automatic rollback', ...plan.lines] };
|
|
137
|
+
}
|
|
138
|
+
const stop = await guard(ctx, config, options, { f, run, identity, lines });
|
|
139
|
+
if (stop) return { ...stop, data: { ...stop.data, urgent: record.tag } };
|
|
140
|
+
const deployed = await run.deployExecute();
|
|
141
|
+
const all = [...lines, ...deployed.lines];
|
|
142
|
+
if (deployed.ok) return { ...deployed, lines: all, data: { ...deployed.data, urgent: record.tag } };
|
|
143
|
+
const data = { ...deployed.data, urgent: record.tag };
|
|
144
|
+
if (!(deployed.data?.steps ?? []).some((step) => step.step === 'deploy')) return refused(all, 'STACK_URGENT_DEPLOY_NOT_STARTED', 'the release failed before the stack deploy, so the services are untouched; the migrations stay (backup above)', data);
|
|
145
|
+
if (deployed.data?.rolledBack) return refused(all, 'STACK_URGENT_RED_ROLLED_BACK', `the release failed its deploy or readiness verify and was rolled back to ${deployed.data.previous}; the migrations stay (backup above)`, data);
|
|
146
|
+
return refused(all, 'STACK_URGENT_ROLLBACK_FAILED', 'the release is red and the automatic rollback did not converge: follow the stack runbook, Rollback', data);
|
|
147
|
+
}
|
package/sync/ci-commands.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import { parseYaml } from '../runtime/engine/yaml.mjs';
|
|
3
3
|
import { refusal } from '../stack/errors.mjs';
|
|
4
4
|
import { byCodeUnit } from '../report/order.mjs';
|
|
5
|
+
import { ENTITY_LIST } from '../db/entity-list.mjs';
|
|
5
6
|
|
|
6
7
|
// One command vocabulary for managed CI rendering and the runtime coverage gate.
|
|
7
8
|
const exampleCommand = (source, command) => source?.runtimeExample
|
|
@@ -195,7 +196,8 @@ export function ciWorkflowCommands(source, lcovReport = 'be/coverage/lcov.info',
|
|
|
195
196
|
ciLintSonar: exampleCommand(source, 'npm run lint -- --sonar reports/lint.sonar.json'),
|
|
196
197
|
// The coverage judgement alone over the merged report: the fast and lint jobs already ran every other rule of the app check.
|
|
197
198
|
ciCoverageProof: exampleCommand(source, 'npx starci app coverage verify ' + lcovReport),
|
|
198
|
-
|
|
199
|
+
// An app with an entity list also checks its entities against the schema its migrations build, in the build job (a runner with Docker).
|
|
200
|
+
ciDatabaseLint: exampleCommand(source, (source?.files ?? []).includes(ENTITY_LIST.slice('be/'.length)) ? 'npm run db:lint && npx starci app db drift' : 'npm run db:lint'),
|
|
199
201
|
auditCommand: `npm audit${audit.omitDev ? ' --omit=dev' : ''} --audit-level=${audit.level}`,
|
|
200
202
|
sonarTokenSecret: sonar?.token ?? 'SONAR_TOKEN',
|
|
201
203
|
sonarHostVar: sonar?.hostVar ?? 'SONAR_HOST_URL',
|
package/sync/index.mjs
CHANGED
|
@@ -157,6 +157,8 @@ function trailingNewline(text) {
|
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
/** The digest-pinned image of a lite app's declared Redis, in every environment that runs it (ruling:R212/case-10). */
|
|
160
|
+
/** The pinned image of a lite app's db service (contexts/stack/pins.yaml images): PostgreSQL with pg_cron, pg_net, pgmq and pgvector. */
|
|
161
|
+
export const DB_IMAGE = 'supabase/postgres:17.11.0.006@sha256:c141fd17ac6a76c4323e90b3aee8ff1a23d2e89e547c2b963cfadc309c079cc2';
|
|
160
162
|
export const REDIS_IMAGE = 'redis:7@sha256:c6eabf748fc7a61dbb5a705c78bcf3d6377b1127a97d0ce965c11c44ba46896f';
|
|
161
163
|
|
|
162
164
|
const redisDevText = project => render(readBundled('app/package-scripts/redis-dev.json'), { project, redisImage: REDIS_IMAGE });
|
|
@@ -286,7 +288,7 @@ export function appScripts(app, source) {
|
|
|
286
288
|
const watch = entry => `cd be && ts-node-dev --respawn -r tsconfig-paths/register apps/${entry.name}/src/main.ts`;
|
|
287
289
|
const built = entry => `node be/dist/apps/${entry.name}/src/main.js`;
|
|
288
290
|
const workspace = entry => feAppPackageName(app.project, entry.name);
|
|
289
|
-
// Both editions generate before they run (owner ruling 2026-10-13): the fe's GraphQL wire types, and under lite the
|
|
291
|
+
// Both editions generate before they run (owner ruling 2026-10-13): the fe's GraphQL wire types, and under lite the database types check.
|
|
290
292
|
const dev = entry => `npm run codegen --silent && turbo run dev --filter=${workspace(entry)}`;
|
|
291
293
|
const images = imageEntries(app, source);
|
|
292
294
|
const imageCommand = ({ side, entry }) => `docker build -f ${dockerfilePath(side, entry.name)} -t ${app.project}/${entry.name}:dev .`;
|
|
@@ -347,7 +349,7 @@ export function imageFiles(app, source) {
|
|
|
347
349
|
const side = app.sides.fe;
|
|
348
350
|
const inputs = [...(opensPackages(side) ? ['fe/packages'] : []), ...((side.reads ?? []).includes('be/contracts/') ? ['be/contracts'] : [])];
|
|
349
351
|
const copyInputs = inputs.map(folder => `COPY ${folder} ${folder}\n`).join('');
|
|
350
|
-
// The folders outside be/ that the back end declares it reads (the generated
|
|
352
|
+
// The folders outside be/ that the back end declares it reads (the generated database types of a lite back end, db/types/) are in every one of its images.
|
|
351
353
|
const beFolders = (app.sides.be.reads ?? []).map(folder => folder.replace(/\/$/, ''));
|
|
352
354
|
const copyBeInputs = beFolders.map(folder => `COPY ${folder} ${folder}\n`).join('');
|
|
353
355
|
const entries = imageEntries(app, source).map(({ side: which, entry: item }) => ({ side: which, item }));
|
|
@@ -373,8 +375,8 @@ export function variables(app, scope, presets, sonarKey, manifest = loadSlotMani
|
|
|
373
375
|
project: app.project,
|
|
374
376
|
appBinary: APP_BINARY,
|
|
375
377
|
appScripts: appScripts(app, source),
|
|
376
|
-
// A
|
|
377
|
-
|
|
378
|
+
// A lite app's own database runs in development as one container of the pinned db image (ruling:lite-self-hosted-postgres-passport-minio).
|
|
379
|
+
dbImage: DB_IMAGE,
|
|
378
380
|
// A lite app's declared Redis (sides.be.backing, ruling:R212/case-10) runs in development as one container of its pinned image.
|
|
379
381
|
backingScripts: lite && (app.sides.be.backing ?? []).includes('redis') ? redisDevText(app.project) : '',
|
|
380
382
|
// turbo builds every fe app workspace; ^build (turbo.json) builds the packages each app imports first, and caches both.
|
|
@@ -555,6 +557,24 @@ export async function renderRepo(root, { presets, parseYaml, notices, manifest =
|
|
|
555
557
|
return { hfs, targets: renderTargets(hfs, presets ?? await loadPresets(root), { sonarKey, manifest, source: appSource(root, { parseYaml, notices }) }) };
|
|
556
558
|
}
|
|
557
559
|
|
|
560
|
+
/** The folders a lite app's sync retires (ruling lite-self-hosted-postgres-passport-minio): the generated Supabase row types nothing reads any more. */
|
|
561
|
+
const LITE_RETIRED_DIRS = Object.freeze(['supabase/types']);
|
|
562
|
+
|
|
563
|
+
/** The app-relative files of the retired folders that still exist under `root` of a lite app; sync --write deletes them, --check reports them. */
|
|
564
|
+
export function retiredFiles(root, hfs) {
|
|
565
|
+
if (hfs?.edition !== 'lite') return [];
|
|
566
|
+
const found = [];
|
|
567
|
+
const walk = (rel) => {
|
|
568
|
+
const full = path.join(root, rel);
|
|
569
|
+
const stat = fs.lstatSync(full, { throwIfNoEntry: false });
|
|
570
|
+
if (!stat) return;
|
|
571
|
+
if (stat.isDirectory()) for (const name of fs.readdirSync(full).sort(byCodeUnit)) walk(`${rel}/${name}`);
|
|
572
|
+
else found.push(rel);
|
|
573
|
+
};
|
|
574
|
+
for (const dir of LITE_RETIRED_DIRS) walk(dir);
|
|
575
|
+
return found;
|
|
576
|
+
}
|
|
577
|
+
|
|
558
578
|
/** `starci app sync --check | --write [--cwd <dir>]` at the app root; returns 0 clean, 1 drift, 2 usage. */
|
|
559
579
|
function syncOptions(argv) {
|
|
560
580
|
const check = argv.includes('--check'), write = argv.includes('--write');
|
|
@@ -567,16 +587,17 @@ function syncOptions(argv) {
|
|
|
567
587
|
return { write, cwdAt };
|
|
568
588
|
}
|
|
569
589
|
|
|
570
|
-
function reportSyncCheck(root, targets, out) {
|
|
590
|
+
function reportSyncCheck(root, targets, out, retired = []) {
|
|
571
591
|
const results = checkTargets(root, targets);
|
|
572
592
|
const bad = results.filter(item => item.status !== 'ok');
|
|
593
|
+
for (const file of retired) out(`HFS_SYNC_DRIFT ${file}: retired, the lite edition generates no database types; run "npx starci app sync --write" to delete it`);
|
|
573
594
|
for (const result of bad) {
|
|
574
595
|
const found = result.actualHash ? `, found ${result.actualHash.slice(0, 12)}` : '';
|
|
575
596
|
const where = result.difference ? ` (line ${result.difference.line}: expected ${JSON.stringify(result.difference.expected)}, found ${JSON.stringify(result.difference.actual)})` : '';
|
|
576
597
|
out(`HFS_SYNC_DRIFT ${result.path}: ${result.status}, expected sha256 ${result.expectedHash.slice(0, 12)}${found}${where}`);
|
|
577
598
|
}
|
|
578
599
|
out(`starci app sync --check: ${results.length - bad.length} of ${results.length} in sync${bad.length ? '; run "npx starci app sync --write"' : ''}`);
|
|
579
|
-
return bad.length ? 1 : 0;
|
|
600
|
+
return bad.length || retired.length ? 1 : 0;
|
|
580
601
|
}
|
|
581
602
|
|
|
582
603
|
export async function runSync(argv, { cwd = process.cwd(), out = line => process.stdout.write(`${line}\n`), presets, parseYaml } = {}) {
|
|
@@ -589,15 +610,18 @@ export async function runSync(argv, { cwd = process.cwd(), out = line => process
|
|
|
589
610
|
const root = cwdAt >= 0 ? path.resolve(cwd, argv[cwdAt + 1]) : cwd;
|
|
590
611
|
try {
|
|
591
612
|
const notices = [];
|
|
592
|
-
const { targets } = await renderRepo(root, { presets, parseYaml, notices });
|
|
613
|
+
const { hfs, targets } = await renderRepo(root, { presets, parseYaml, notices });
|
|
614
|
+
const retired = retiredFiles(root, hfs);
|
|
593
615
|
if (targets.some(target => target.path === 'README.md')) for (const notice of new Set(notices)) out(notice);
|
|
594
616
|
if (write) {
|
|
595
617
|
const written = writeTargets(root, targets);
|
|
596
618
|
for (const file of written) out(`wrote ${file}`);
|
|
619
|
+
for (const file of retired) { fs.rmSync(path.join(root, file)); out(`removed ${file} (retired)`); }
|
|
620
|
+
for (const dir of LITE_RETIRED_DIRS) if (retired.length && fs.existsSync(path.join(root, dir)) && !fs.readdirSync(path.join(root, dir)).length) fs.rmdirSync(path.join(root, dir));
|
|
597
621
|
out(`starci app sync: ${written.length} written, ${targets.length - written.length} already in sync`);
|
|
598
622
|
return 0;
|
|
599
623
|
}
|
|
600
|
-
return reportSyncCheck(root, targets, out);
|
|
624
|
+
return reportSyncCheck(root, targets, out, retired);
|
|
601
625
|
} catch (error) {
|
|
602
626
|
if (!(error instanceof SyncError)) throw error;
|
|
603
627
|
out(error.message);
|