@starci/hfs 5.1.0 → 5.2.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 +20 -0
- package/README.md +10 -1
- package/db/postgres.mjs +9 -1
- package/package.json +1 -1
- package/runtime/knowledge/hfs/canon-pins.yaml +30 -9
- package/runtime/knowledge/hfs/fe-stack.yaml +7 -7
- package/runtime/knowledge/hfs/fork-images.yaml +7 -0
- package/runtime/knowledge/hfs/rules.yaml +55 -7
- package/runtime/knowledge/hfs/slots.yaml +75 -24
- package/runtime/modules/kernel/failure-codes.yaml +62 -0
- package/runtime/modules/schemas/application-stacks.schema.yaml +6 -0
- package/runtime/scripts/hfs/architecture/identity.mjs +18 -83
- package/runtime/scripts/hfs/architecture/transport-owner.d.mts +1 -2
- package/runtime/scripts/hfs/architecture/transport-owner.mjs +3 -6
- package/runtime/scripts/hfs/manifest-shape.d.mts +1 -0
- package/runtime/scripts/hfs/manifest-shape.mjs +3 -1
- package/runtime/scripts/hfs/rules/a15-findings.mjs +5 -0
- package/runtime/scripts/hfs/rules/stacks-local.d.mts +7 -0
- package/runtime/scripts/hfs/rules/stacks-local.mjs +71 -0
- package/runtime/scripts/hfs/rules/stacks-runtime.mjs +6 -1
- package/runtime/scripts/hfs/rules/stacks-service-images.d.mts +12 -0
- package/runtime/scripts/hfs/rules/stacks-service-images.mjs +120 -0
- package/runtime/scripts/hfs/secret-dev-init.d.mts +11 -0
- package/runtime/scripts/hfs/secret-dev-init.mjs +79 -0
- package/runtime/scripts/hfs/secret.mjs +10 -7
- package/runtime/scripts/hfs/slots.mjs +1 -1
- package/runtime/scripts/hfs/stack-local-config.d.mts +101 -0
- package/runtime/scripts/hfs/stack-local-config.mjs +320 -0
- package/runtime/scripts/hfs/stacks-gate.mjs +1 -1
- package/runtime/scripts/lib/swarm-stack.d.mts +2 -1
- package/runtime/scripts/lib/swarm-stack.mjs +2 -2
- package/scaffold/add-module.d.mts +16 -0
- package/scaffold/add-module.mjs +232 -0
- package/scaffold/add-queue-lite.mjs +49 -0
- package/scaffold/add.mjs +5 -0
- package/scaffold/app.mjs +7 -7
- package/scaffold/module-registry.d.mts +19 -0
- package/scaffold/module-registry.mjs +84 -0
- package/src/main.mjs +23 -2
- package/stack/cli.mjs +6 -1
- package/stack/context.d.mts +15 -0
- package/stack/context.mjs +1 -0
- package/stack/local-cli.d.mts +25 -0
- package/stack/local-cli.mjs +89 -0
- package/stack/local-config.d.mts +17 -0
- package/stack/local-config.mjs +37 -0
- package/stack/local-custody.d.mts +24 -0
- package/stack/local-custody.mjs +109 -0
- package/stack/local-doctor.d.mts +6 -0
- package/stack/local-doctor.mjs +49 -0
- package/stack/local-down.d.mts +8 -0
- package/stack/local-down.mjs +20 -0
- package/stack/local-host.d.mts +42 -0
- package/stack/local-host.mjs +163 -0
- package/stack/local-logs.d.mts +8 -0
- package/stack/local-logs.mjs +32 -0
- package/stack/local-ports.d.mts +7 -0
- package/stack/local-ports.mjs +39 -0
- package/stack/local-prepare.d.mts +22 -0
- package/stack/local-prepare.mjs +62 -0
- package/stack/local-run.d.mts +71 -0
- package/stack/local-run.mjs +200 -0
- package/stack/local-state.d.mts +35 -0
- package/stack/local-state.mjs +29 -0
- package/stack/local-status.d.mts +6 -0
- package/stack/local-status.mjs +36 -0
- package/stack/local-up.d.mts +11 -0
- package/stack/local-up.mjs +166 -0
- package/stack/local-verify.d.mts +6 -0
- package/stack/local-verify.mjs +21 -0
- package/sync/index.d.mts +13 -12
- package/sync/index.mjs +83 -28
- package/templates/app/module/jest.config.js.tpl +8 -0
- package/templates/app/module/src/index.ts.tpl +2 -0
- package/templates/app/module/src/manifest/declaration.spec.ts.tpl +11 -0
- package/templates/app/module/src/manifest/declaration.ts.tpl +13 -0
- package/templates/app/module/src/manifest/index.ts.tpl +1 -0
- package/templates/app/module/src/public/index.ts.tpl +2 -0
- package/templates/app/module/src/template/__name__.template.spec.ts.tpl +13 -0
- package/templates/app/module/src/template/__name__.template.ts.tpl +41 -0
- package/templates/app/module/src/template/index.ts.tpl +1 -0
- package/templates/app/module/src/workbench/components/__pascal__WorkbenchView.tsx.tpl +16 -0
- package/templates/app/module/src/workbench/index.ts.tpl +2 -0
- package/templates/app/package-scripts/package.json +1 -1
- package/templates/app/package-scripts-lite/package.json +1 -1
- package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +76 -70
- package/templates/app/skeleton-lite/.starcistacks/dev/README.md +23 -13
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/compose/compose.yaml +25 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +18 -26
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +1 -0
- package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +2 -1
- package/templates/app/skeleton-lite/db/migrations/__timestamp___baseline.sql +57 -14
- package/templates/be/auth-lite/src/features/api/auth/application/auth.contracts.ts +11 -0
- package/templates/be/auth-lite/src/features/api/auth/application/get-jwks.handler.ts +23 -0
- package/templates/be/auth-lite/src/features/api/auth/application/get-jwks.query.ts +12 -0
- package/templates/be/auth-lite/src/features/api/auth/application/google-begin.command.ts +11 -0
- package/templates/be/auth-lite/src/features/api/auth/application/google-begin.handler.ts +22 -0
- package/templates/be/auth-lite/src/features/api/auth/application/google-complete.command.ts +12 -0
- package/templates/be/auth-lite/src/features/api/auth/application/google-complete.handler.ts +23 -0
- package/templates/be/auth-lite/src/features/api/auth/application/login.command.ts +12 -0
- package/templates/be/auth-lite/src/features/api/auth/application/login.handler.ts +23 -0
- package/templates/be/auth-lite/src/features/api/auth/application/logout.command.ts +11 -0
- package/templates/be/auth-lite/src/features/api/auth/application/logout.handler.ts +22 -0
- package/templates/be/auth-lite/src/features/api/auth/application/refresh.command.ts +12 -0
- package/templates/be/auth-lite/src/features/api/auth/application/refresh.handler.ts +23 -0
- package/templates/be/auth-lite/src/features/api/auth/application/session.command.ts +12 -0
- package/templates/be/auth-lite/src/features/api/auth/application/session.handler.ts +23 -0
- package/templates/be/auth-lite/src/features/api/auth/auth.module-definition.ts +13 -0
- package/templates/be/auth-lite/src/features/api/auth/auth.module.ts +16 -0
- package/templates/be/auth-lite/src/features/api/auth/auth.options.ts +3 -0
- package/templates/be/auth-lite/src/features/api/auth/index.ts +2 -0
- package/templates/be/auth-lite/src/features/api/auth/transport/http/auth-http.module-definition.ts +13 -0
- package/templates/be/auth-lite/src/features/api/auth/transport/http/auth-http.module.ts +9 -0
- package/templates/be/auth-lite/src/features/api/auth/transport/http/auth-http.options.ts +3 -0
- package/templates/be/auth-lite/src/features/api/auth/transport/http/auth.controller.ts +90 -0
- package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/google-start.request.ts +17 -0
- package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/login.request.ts +16 -0
- package/templates/be/auth-lite/src/modules/domain/identity/account-directory.service.spec.ts +46 -0
- package/templates/be/auth-lite/src/modules/domain/identity/account-directory.service.ts +28 -0
- package/templates/be/auth-lite/src/modules/domain/identity/authenticator.service.spec.ts +145 -0
- package/templates/be/auth-lite/src/modules/domain/identity/authenticator.service.ts +95 -0
- package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/auth-identity.entity.ts +38 -0
- package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/auth-user.entity.ts +54 -0
- package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/identity-attempt.entity.ts +18 -0
- package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/identity-refresh-token.entity.ts +34 -0
- package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/identity-session.entity.ts +30 -0
- package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/index.ts +17 -0
- package/templates/be/auth-lite/src/modules/domain/identity/rate-counter.service.spec.ts +86 -0
- package/templates/be/auth-lite/src/modules/domain/identity/rate-counter.service.ts +45 -0
- package/templates/be/auth-lite/src/modules/domain/identity/session-store.service.spec.ts +97 -0
- package/templates/be/auth-lite/src/modules/domain/identity/session-store.service.ts +81 -0
- package/templates/be/auth-lite/src/modules/domain/identity/sign-in.service.spec.ts +78 -0
- package/templates/be/auth-lite/src/modules/domain/identity/sign-in.service.ts +87 -0
- package/templates/be/auth-lite/src/tests/fixtures/builders/identity.builder.ts +44 -0
- package/templates/be/skeleton-lite/apps/api/src/app.module.ts +6 -5
- package/templates/be/skeleton-lite/apps/api/src/compose/api.ts +3 -2
- package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +10 -5
- package/templates/be/skeleton-lite/src/modules/domain/identity/errors/identity.error.ts +34 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +22 -1
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +47 -2
- package/templates/be/skeleton-lite/src/modules/domain/identity/index.ts +2 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/messages/identity.messages.ts +24 -0
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/index.ts +3 -3
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.config.ts +3 -1
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.decorators.ts +8 -4
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.module-definition.ts +12 -0
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.module.ts +29 -0
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.options.ts +6 -31
- package/templates/be/skeleton-lite/src/modules/integrations/passport/errors/passport.error.ts +18 -9
- package/templates/be/skeleton-lite/src/modules/integrations/passport/index.ts +35 -11
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.config.spec.ts +44 -8
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.config.ts +40 -18
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.decorators.ts +18 -6
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.kit.spec.ts +73 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.kit.ts +26 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.options.ts +24 -53
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.registration.ts +31 -0
- package/templates/be/skeleton-lite/src/modules/platform/cqrs/index.ts +1 -1
- package/templates/be/skeleton-lite/src/modules/platform/database/entities.ts +2 -1
- 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 +11 -4
- package/templates/be/test-world/e2e/serves-once-migrated.e2e-spec.ts.tpl +9 -9
- package/templates/be/test-world/fixtures/migrations.sql.ts.tpl +4 -4
- package/templates/fe/auth-lite/apps/web/src/modules/auth/client.ts +26 -0
- package/templates/fe/common/site-shell.tsx.partial +3 -3
- package/templates/fe/skeleton/apps/__app__/src/components/providers/AppProviders/index.tsx +33 -10
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/index.ts +1 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useViewerId.ts +11 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/toast/toast.tsx +15 -12
- package/templates/fe/skeleton/apps/__app__/src/modules/viewer-cache/index.ts +5 -0
- package/templates/fe/skeleton/apps/__app__/src/redux/slices/session.ts +1 -1
- package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +1 -1
- package/templates/fe/skeleton/packages/__project__-ui/src/composites/NotFoundNotice/index.tsx +4 -7
- package/templates/fe/skeleton/packages/__project__-ui/src/frames/SiteShell/index.tsx +2 -3
- package/templates/fe/skeleton-lite/apps/web/src/components/composites/NotFoundNotice/index.tsx +4 -7
- package/templates/fe/skeleton-lite/apps/web/src/components/frames/SiteShell/index.tsx +1 -2
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/Caddyfile +0 -16
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/owner-actions.md +0 -11
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/recovery/restore-proof.md +0 -7
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/current.yaml +0 -6
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/images.lock.yaml +0 -8
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/previous.yaml +0 -6
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +0 -186
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.client.spec.ts +0 -22
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.client.ts +0 -25
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.cookie.spec.ts +0 -104
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.cookie.ts +0 -34
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.password.spec.ts +0 -28
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.password.ts +0 -11
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.tokens.spec.ts +0 -88
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.tokens.ts +0 -69
- package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/google.strategy.ts +0 -41
- package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/local.strategy.ts +0 -40
- package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/oauth-state.store.ts +0 -63
- package/templates/fe/skeleton/packages/__project__-ui/src/frames/SiteShell/classNames.ts +0 -4
- package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/auth-door.ts +0 -30
- package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +0 -50
- package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/token-store.ts +0 -31
- package/templates/fe/skeleton-lite/apps/web/src/components/frames/SiteShell/classNames.ts +0 -4
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// local-up.mjs - `starci app stack up --env <dev env> [--execute]`: the one command that brings the whole dev environment up (PROJECT-STACKS-6).
|
|
2
|
+
// Order: host prerequisites, dev custody, setup (install, codegen), compose services and their health, the environment's own database role,
|
|
3
|
+
// migrations with a receipt, seed, native app processes, readiness, origins. Idempotent: what runs is kept, what changed is converged.
|
|
4
|
+
// A dry run prints the plan. It never generates a replacement secret, never removes a volume and touches only its own compose project.
|
|
5
|
+
import crypto from 'node:crypto';
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { firstLine } from './context.mjs';
|
|
9
|
+
import { failure, refusal } from './errors.mjs';
|
|
10
|
+
import { missingKeys, missingStep, openCustody } from './local-custody.mjs';
|
|
11
|
+
import { hostRefusal, preflight, runSetup } from './local-prepare.mjs';
|
|
12
|
+
import { DEFAULT_TIMEOUT_SECONDS, makeDataDirectories, commandDigest, compose, composeValues, databaseUrl, isAlive, probe, runCommand, runEnvironment, serviceReady, startProcess, stateOf, waitFor } from './local-run.mjs';
|
|
13
|
+
import { readRecord, writeRecord } from './local-state.mjs';
|
|
14
|
+
const SAFE_PASSWORD = /^[A-Za-z0-9_-]{8,}$/u;
|
|
15
|
+
/** The digest of the migration folder (names and contents): what a migrate receipt records. */
|
|
16
|
+
export function migrationsDigest(repoRoot) {
|
|
17
|
+
const directory = path.join(repoRoot, 'db', 'migrations');
|
|
18
|
+
const hash = crypto.createHash('sha256');
|
|
19
|
+
let names = [];
|
|
20
|
+
try {
|
|
21
|
+
names = fs.readdirSync(directory).filter((name) => name.endsWith('.sql')).sort();
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return 'none';
|
|
25
|
+
}
|
|
26
|
+
for (const name of names)
|
|
27
|
+
hash.update(name).update(fs.readFileSync(path.join(directory, name)));
|
|
28
|
+
return hash.digest('hex').slice(0, 16);
|
|
29
|
+
}
|
|
30
|
+
/** Creates the environment's own login role and database if absent and sets its password (psql in the db container, SQL on stdin, never argv). */
|
|
31
|
+
async function provisionDatabase(ctx, local, custody) {
|
|
32
|
+
const database = local.database;
|
|
33
|
+
if (!database)
|
|
34
|
+
return 'database: none declared';
|
|
35
|
+
const password = custody.values.get(database.password.key) ?? '';
|
|
36
|
+
if (!SAFE_PASSWORD.test(password))
|
|
37
|
+
throw refusal('STACK_LOCAL_DB_PASSWORD', `${database.password.key} must be at least 8 characters of letters, digits, dash and underscore (the generated password kind is); the owner replaces it with secret unset and secret init`);
|
|
38
|
+
const sql = [
|
|
39
|
+
`DO $do$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${database.role}') THEN CREATE ROLE "${database.role}" LOGIN SUPERUSER PASSWORD '${password}'; ELSE ALTER ROLE "${database.role}" LOGIN SUPERUSER PASSWORD '${password}'; END IF; END $do$;`,
|
|
40
|
+
`SELECT 'CREATE DATABASE "${database.name}" OWNER "${database.role}"' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '${database.name}')\\gexec`,
|
|
41
|
+
'',
|
|
42
|
+
].join('\n');
|
|
43
|
+
const result = await compose(ctx, local, ['exec', '-T', database.service, 'psql', '-v', 'ON_ERROR_STOP=1', '-U', database.adminUser, '-d', 'postgres'], { values: composeValues(local, custody), input: sql });
|
|
44
|
+
if (result.status !== 0)
|
|
45
|
+
throw failure('STACK_LOCAL_DB_ROLE', `could not provision the role ${database.role}: ${(result.stderr.trim().split(/\r?\n/u)[0] ?? '').replaceAll(password, '[redacted]')}`);
|
|
46
|
+
return `database: role ${database.role} and database ${database.name} ready on port ${local.ports[database.port]}`;
|
|
47
|
+
}
|
|
48
|
+
/** Gives the app role the migrations created its own password for this environment (cluster roles are never shared: the db container is this environment's alone). */
|
|
49
|
+
async function appRolePassword(ctx, local, custody) {
|
|
50
|
+
const database = local.database;
|
|
51
|
+
if (!database?.appRole || !database.appPassword)
|
|
52
|
+
return '';
|
|
53
|
+
const password = custody.values.get(database.appPassword.key) ?? '';
|
|
54
|
+
if (!SAFE_PASSWORD.test(password))
|
|
55
|
+
throw refusal('STACK_LOCAL_DB_PASSWORD', `${database.appPassword.key} must be at least 8 characters of letters, digits, dash and underscore`);
|
|
56
|
+
const sql = `DO $do$ BEGIN IF EXISTS (SELECT FROM pg_roles WHERE rolname = '${database.appRole}') THEN ALTER ROLE "${database.appRole}" LOGIN PASSWORD '${password}'; ELSE RAISE EXCEPTION 'role ${database.appRole} does not exist: the migrations create it'; END IF; END $do$;\n`;
|
|
57
|
+
const result = await compose(ctx, local, ['exec', '-T', database.service, 'psql', '-v', 'ON_ERROR_STOP=1', '-U', database.adminUser, '-d', database.name], { values: composeValues(local, custody), input: sql });
|
|
58
|
+
if (result.status !== 0)
|
|
59
|
+
throw failure('STACK_LOCAL_DB_ROLE', `could not set the password of ${database.appRole}: ${(result.stderr.trim().split(/\r?\n/u)[0] ?? '').replaceAll(password, '[redacted]')}`);
|
|
60
|
+
return `, ${database.appRole} password set`;
|
|
61
|
+
}
|
|
62
|
+
/** Migrates through `starci app db migrate` in this process; the URL stays in the environment of that call. */
|
|
63
|
+
async function migrate(ctx, local, custody, deps) {
|
|
64
|
+
const database = local.database;
|
|
65
|
+
const url = databaseUrl(local, custody);
|
|
66
|
+
if (!database || url === null)
|
|
67
|
+
return 'migrate: no database declared';
|
|
68
|
+
const container = firstLine((await compose(ctx, local, ['ps', '-q', database.service])).stdout);
|
|
69
|
+
const env = { ...ctx.env, DATABASE_URL: url, STARCI_DB_CONTAINER: container };
|
|
70
|
+
const run = deps.migrate ?? (async (urlEnv, environment, cwd) => {
|
|
71
|
+
const { main } = await import('../src/main.mjs');
|
|
72
|
+
return main(['db', 'migrate', '--db-url-env', urlEnv, '--execute'], { cwd, env: environment, stdout: () => { }, stderr: () => { } });
|
|
73
|
+
});
|
|
74
|
+
const code = await run('DATABASE_URL', env, local.repoRoot);
|
|
75
|
+
if (code !== 0)
|
|
76
|
+
throw failure('STACK_LOCAL_MIGRATE', `starci app db migrate exited ${code}; run it with --db-url-env DATABASE_URL to read its report`);
|
|
77
|
+
const dir = stateOf(ctx, local);
|
|
78
|
+
const record = readRecord(dir);
|
|
79
|
+
record.receipts.migrate = { at: new Date(ctx.now()).toISOString(), digest: migrationsDigest(local.repoRoot) };
|
|
80
|
+
writeRecord(dir, record);
|
|
81
|
+
return `migrate: applied, receipt ${record.receipts.migrate.digest}${await appRolePassword(ctx, local, custody)}`;
|
|
82
|
+
}
|
|
83
|
+
/** Runs the seed commands once each (a receipt keyed by the command's digest); the seed owns its idempotence beyond that. */
|
|
84
|
+
async function seed(ctx, local, custody, after) {
|
|
85
|
+
const dir = stateOf(ctx, local);
|
|
86
|
+
const record = readRecord(dir);
|
|
87
|
+
const out = [];
|
|
88
|
+
for (const run of local.seed.filter((item) => item.after === after)) {
|
|
89
|
+
if (record.receipts.seed[run.name]?.digest === commandDigest(run)) {
|
|
90
|
+
out.push(`seed ${run.name}: already done`);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
await runCommand(ctx, local, run, runEnvironment(ctx, local, run, custody, 'owner'));
|
|
94
|
+
record.receipts.seed[run.name] = { at: new Date(ctx.now()).toISOString(), digest: commandDigest(run) };
|
|
95
|
+
writeRecord(dir, record);
|
|
96
|
+
out.push(`seed ${run.name}: done`);
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
/** Starts each native process that is not already running. */
|
|
101
|
+
function startProcesses(ctx, local, custody, deps) {
|
|
102
|
+
const dir = stateOf(ctx, local);
|
|
103
|
+
const record = readRecord(dir);
|
|
104
|
+
const out = [];
|
|
105
|
+
const alive = deps.alive ?? isAlive;
|
|
106
|
+
for (const run of local.processes) {
|
|
107
|
+
const known = record.processes[run.name];
|
|
108
|
+
if (known && alive(known.pid)) {
|
|
109
|
+
out.push(`process ${run.name}: running (pid ${known.pid})`);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const pid = startProcess(ctx, local, run, runEnvironment(ctx, local, run, custody, 'app'));
|
|
113
|
+
record.processes[run.name] = { pid, startedAt: new Date(ctx.now()).toISOString(), port: run.port === null ? null : (local.ports[run.port] ?? null) };
|
|
114
|
+
out.push(`process ${run.name}: started (pid ${pid})`);
|
|
115
|
+
}
|
|
116
|
+
record.ports = { ...local.ports };
|
|
117
|
+
writeRecord(dir, record);
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
/** The plan a dry run prints. */
|
|
121
|
+
function plan(local, missing) {
|
|
122
|
+
return [
|
|
123
|
+
`plan custody: ${missing.length ? `missing ${missing.join(', ')}: ${missingStep(local, missing)}` : 'open the sealed dev values into host-local files'}`,
|
|
124
|
+
...local.setup.map((run) => `plan setup ${run.name}: ${run.argv.join(' ')}`),
|
|
125
|
+
`plan services: ${local.services.map((service) => `${service.name} (wait ${service.wait})`).join(', ')}`,
|
|
126
|
+
...(local.database ? [`plan database: own role ${local.database.role}, database ${local.database.name}, then starci app db migrate`] : []),
|
|
127
|
+
...local.seed.map((run) => `plan seed ${run.name}: ${run.argv.join(' ')}`),
|
|
128
|
+
...local.processes.map((run) => `plan process ${run.name}: ${run.argv.join(' ')}`),
|
|
129
|
+
...local.ready.map((row) => `plan ready ${row.name}: ${row.url}`),
|
|
130
|
+
];
|
|
131
|
+
}
|
|
132
|
+
/** `up` for a dev environment. */
|
|
133
|
+
export async function upLocal(ctx, local, { execute = false, timeoutSeconds = DEFAULT_TIMEOUT_SECONDS } = {}, deps = {}) {
|
|
134
|
+
const host = await preflight(ctx, local, deps);
|
|
135
|
+
if (!host.ok)
|
|
136
|
+
return hostRefusal(host.lines);
|
|
137
|
+
const missing = missingKeys(local);
|
|
138
|
+
if (!execute)
|
|
139
|
+
return { ok: missing.length === 0, lines: [...host.lines, ...plan(local, missing), 'dry run: nothing was started; arm it with --execute'], data: { rows: host.rows } };
|
|
140
|
+
const lines = [...host.lines];
|
|
141
|
+
const custody = openCustody(ctx, local);
|
|
142
|
+
lines.push(`custody: ${custody.values.size} key file(s) in the host-local directory`);
|
|
143
|
+
lines.push(...await runSetup(ctx, local, custody));
|
|
144
|
+
makeDataDirectories(ctx, local);
|
|
145
|
+
const values = composeValues(local, custody);
|
|
146
|
+
const started = await compose(ctx, local, ['up', '-d', ...local.services.map((service) => service.name)], { values, heavy: true, label: 'compose up' });
|
|
147
|
+
if (started.status !== 0)
|
|
148
|
+
throw failure('STACK_LOCAL_COMPOSE', `docker compose up failed: ${(started.stderr.trim().split(/\r?\n/u)[0] ?? '').slice(0, 200)}`);
|
|
149
|
+
const deadline = ctx.now() + timeoutSeconds * 1000;
|
|
150
|
+
for (const service of local.services) {
|
|
151
|
+
const ready = await serviceReady(ctx, local, service.name, deadline, deps);
|
|
152
|
+
lines.push(`service ${ready.detail}`);
|
|
153
|
+
if (!ready.ok)
|
|
154
|
+
throw failure('STACK_LOCAL_SERVICE', `${service.name} did not become ready in ${timeoutSeconds}s (${ready.detail}); read it with: starci app stack logs --env ${local.env} --service ${service.name}`);
|
|
155
|
+
}
|
|
156
|
+
lines.push(await provisionDatabase(ctx, local, custody), await migrate(ctx, local, custody, deps), ...await seed(ctx, local, custody, 'migrate'), ...startProcesses(ctx, local, custody, deps));
|
|
157
|
+
for (const row of local.ready) {
|
|
158
|
+
const result = await waitFor(ctx, deadline, () => probe(ctx, local, row));
|
|
159
|
+
lines.push(`${result.ok ? 'ok ' : 'FAIL '}ready ${result.detail}`);
|
|
160
|
+
if (!result.ok)
|
|
161
|
+
throw failure('STACK_LOCAL_READY', `${row.name} is not ready: ${result.detail}; read a process log with: starci app stack logs --env ${local.env} --service ${local.processes.map((run) => run.name).join('|')}`);
|
|
162
|
+
}
|
|
163
|
+
lines.push(...await seed(ctx, local, custody, 'ready'));
|
|
164
|
+
lines.push(...local.origins.map((origin) => `origin ${origin.name}: ${origin.url.replaceAll(/\$\{port\.([a-z][a-z0-9-]*)\}/gu, (_whole, name) => String(local.ports[name]))}`));
|
|
165
|
+
return { ok: true, lines, data: { origins: local.origins } };
|
|
166
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { StackContext } from './context.mjs';
|
|
2
|
+
import type { ActionResult } from './remote.mjs';
|
|
3
|
+
import type { LocalConfig } from './local-config.mjs';
|
|
4
|
+
import type { LocalDeps } from './local-run.mjs';
|
|
5
|
+
/** `verify` for a dev environment. */
|
|
6
|
+
export declare function verifyLocal(ctx: StackContext, local: LocalConfig, _options?: Record<string, unknown>, deps?: LocalDeps): Promise<ActionResult>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { refusal } from './errors.mjs';
|
|
2
|
+
import { isAlive, probe, stateOf } from './local-run.mjs';
|
|
3
|
+
import { readRecord } from './local-state.mjs';
|
|
4
|
+
/** `verify` for a dev environment. */
|
|
5
|
+
export async function verifyLocal(ctx, local, _options = {}, deps = {}) {
|
|
6
|
+
if (local.verify.length === 0)
|
|
7
|
+
throw refusal('STACK_LOCAL_VERIFY_UNDECLARED', `environment ${local.env} declares no local.verify journey: a liveness probe alone is not verification`);
|
|
8
|
+
const record = readRecord(stateOf(ctx, local));
|
|
9
|
+
const alive = deps.alive ?? isAlive;
|
|
10
|
+
const down = local.processes.filter((run) => !(record.processes[run.name] && alive(record.processes[run.name].pid)));
|
|
11
|
+
if (down.length)
|
|
12
|
+
return { ok: false, lines: [`FAIL ${down.map((run) => run.name).join(', ')} not running: start the environment with starci app stack up --env ${local.env} --execute`] };
|
|
13
|
+
const lines = [];
|
|
14
|
+
let ok = true;
|
|
15
|
+
for (const row of local.verify) {
|
|
16
|
+
const result = await probe(ctx, local, row);
|
|
17
|
+
ok &&= result.ok;
|
|
18
|
+
lines.push(`${result.ok ? 'ok ' : 'FAIL '}${result.detail}`);
|
|
19
|
+
}
|
|
20
|
+
return { ok, lines, data: { checked: local.verify.length } };
|
|
21
|
+
}
|
package/sync/index.d.mts
CHANGED
|
@@ -64,6 +64,8 @@ export interface AppSourceFacts {
|
|
|
64
64
|
read: (file: string) => string;
|
|
65
65
|
workPresent: boolean;
|
|
66
66
|
hasImage: (file: string) => boolean;
|
|
67
|
+
modules?: readonly string[];
|
|
68
|
+
otherPackages?: readonly string[];
|
|
67
69
|
stack: any;
|
|
68
70
|
quality: any;
|
|
69
71
|
badges: string;
|
|
@@ -138,12 +140,6 @@ export declare function sonarScope(presets: Presets | null | undefined): {
|
|
|
138
140
|
tests: string[];
|
|
139
141
|
exclusions: string[];
|
|
140
142
|
};
|
|
141
|
-
/**
|
|
142
|
-
* The be source of the app at `root` the coverage components read: { files, read }, `files` be-relative posix paths of every `*.ts` file under
|
|
143
|
-
* be/apps and be/src (build output and dependencies skipped) and `read` their text. `workPresent` records whether the optional
|
|
144
|
-
* lite brand tree exists, so sync can preserve its absence. `hasImage` identifies authored optional Dockerfiles without reading
|
|
145
|
-
* their content. An app with no be folder has no source files.
|
|
146
|
-
*/
|
|
147
143
|
export declare function appSource(root: string, { parseYaml, notices }?: {
|
|
148
144
|
parseYaml?: ParseYaml | undefined;
|
|
149
145
|
notices?: string[] | undefined;
|
|
@@ -178,7 +174,7 @@ export declare function jestCoverageSource(coverage: {
|
|
|
178
174
|
* `docker:build:<app>` builds the image of one app (the exact command its Dockerfile header states, tag `<project>/<app>:dev`) and
|
|
179
175
|
* `docker:build` builds every image, one after the other, never pushing.
|
|
180
176
|
*/
|
|
181
|
-
export declare function appScripts(app: ResolvedApp, source: Partial<Pick<AppSourceFacts, 'hasImage'>> | undefined): string;
|
|
177
|
+
export declare function appScripts(app: ResolvedApp, source: Partial<Pick<AppSourceFacts, 'hasImage' | 'modules'>> | undefined): string;
|
|
182
178
|
/** The stylesheets of the fe side the CSS canon judges, relative to fe/: every app and every workspace package (slot fe.route, fe.package.*). */
|
|
183
179
|
export declare const STYLE_GLOB = "{apps,packages}/*/src/**/*.css";
|
|
184
180
|
/** The apps images.yml builds (one matrix entry per declared app): name and Dockerfile, be then fe, then a lite app's db image. */
|
|
@@ -192,14 +188,13 @@ export declare function imageFiles(app: ResolvedApp, source?: Partial<Pick<AppSo
|
|
|
192
188
|
path: string;
|
|
193
189
|
content: string;
|
|
194
190
|
}[];
|
|
195
|
-
/** Every value a template of `scope` can name, derived from hfs.json and the presets. */
|
|
196
191
|
export declare function variables(app: ResolvedApp, scope: string, presets: Presets | null | undefined, sonarKey: string | null | undefined, manifest?: SlotManifest, source?: Pick<AppSourceFacts, 'files' | 'read'> & Partial<AppSourceFacts>): Record<string, string>;
|
|
197
192
|
/**
|
|
198
|
-
* The
|
|
199
|
-
*
|
|
200
|
-
*
|
|
193
|
+
* The rendered targets of an app declaration. Without source facts the app counts the back-end source files its own targets
|
|
194
|
+
* render (the managed sign-in glue), as a check reading the disk after the targets are written does, so a render of a declaration
|
|
195
|
+
* alone is already in sync with itself.
|
|
201
196
|
*/
|
|
202
|
-
export declare function renderTargets(hfs: unknown, presets: Presets | null | undefined,
|
|
197
|
+
export declare function renderTargets(hfs: unknown, presets: Presets | null | undefined, options?: {
|
|
203
198
|
sonarKey?: string | null | undefined;
|
|
204
199
|
readTemplate?: (name: string) => string;
|
|
205
200
|
manifest?: SlotManifest;
|
|
@@ -212,6 +207,12 @@ export declare function writtenContent(target: RenderedTarget, existing: string
|
|
|
212
207
|
export declare function writeTargets(root: string, targets: readonly RenderedTarget[]): string[];
|
|
213
208
|
/** The hfs.json object of the app at `root` (validated), or a SyncError. */
|
|
214
209
|
export declare function loadHfs(root: string, manifest?: SlotManifest): Record<string, any>;
|
|
210
|
+
/**
|
|
211
|
+
* The source facts as they stand once the rendered back-end source files are on disk: a managed `.ts` target of the be side counts as a
|
|
212
|
+
* file of the app (and reads as its render), so the CI shard count and the coverage scope that count or read the source name the same
|
|
213
|
+
* files before and after the targets are written, and one sync leaves the app in sync.
|
|
214
|
+
*/
|
|
215
|
+
export declare function settleSource(source: AppSourceFacts, targets: readonly RenderedTarget[]): AppSourceFacts;
|
|
215
216
|
/** The rendered targets of the app at `root`: its hfs.json, the Sonar key the be side's stack declaration names, the presets it installs. */
|
|
216
217
|
export declare function renderRepo(root: string, { presets, parseYaml, notices, manifest }?: {
|
|
217
218
|
presets?: Presets | undefined;
|
package/sync/index.mjs
CHANGED
|
@@ -34,6 +34,7 @@ import { DB_DOCKERFILE, DB_IMAGE_APP, RETIRED_DB_DOCKERFILE } from '../db/image.
|
|
|
34
34
|
import { byCodeUnit } from '../report/order.mjs';
|
|
35
35
|
import { isRuntimeExampleApp } from '../runtime/scripts/lib/runtime-example.mjs';
|
|
36
36
|
import { ciWorkflowCommands } from './ci-commands.mjs';
|
|
37
|
+
import { MODULE_PREFIX, moduleNames, registryFiles } from '../scaffold/module-registry.mjs';
|
|
37
38
|
import { dependabotUpdates, hasDeclaredServices, readDeclaredQuality, readDeclaredBadges, sonarHostLine } from './quality-services.mjs';
|
|
38
39
|
import { qualityServices } from '../runtime/scripts/hfs/ci-declaration.mjs';
|
|
39
40
|
import { grammarLintFor } from '../runtime/scripts/lib/grammar-lint.mjs';
|
|
@@ -244,6 +245,16 @@ const SOURCE_SKIPPED = new Set(['node_modules', 'dist', 'coverage']);
|
|
|
244
245
|
* lite brand tree exists, so sync can preserve its absence. `hasImage` identifies authored optional Dockerfiles without reading
|
|
245
246
|
* their content. An app with no be folder has no source files.
|
|
246
247
|
*/
|
|
248
|
+
/** The folders of `packages/` that are neither a module package nor the registry. */
|
|
249
|
+
function otherPackages(root) {
|
|
250
|
+
try {
|
|
251
|
+
return fs.readdirSync(path.join(root, 'packages'), { withFileTypes: true })
|
|
252
|
+
.filter(entry => entry.isDirectory() && !entry.name.startsWith(MODULE_PREFIX) && entry.name !== 'modules').map(entry => entry.name).sort(byCodeUnit);
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
return [];
|
|
256
|
+
}
|
|
257
|
+
}
|
|
247
258
|
export function appSource(root, { parseYaml, notices } = {}) {
|
|
248
259
|
const files = [];
|
|
249
260
|
const walk = (dir) => {
|
|
@@ -267,6 +278,7 @@ export function appSource(root, { parseYaml, notices } = {}) {
|
|
|
267
278
|
for (const dir of ['apps', 'src'])
|
|
268
279
|
walk(dir);
|
|
269
280
|
return { runtimeExample: isRuntimeExampleApp(root), files: files.sort(byCodeUnit), read: (file) => fs.readFileSync(path.join(root, 'be', file), 'utf8'),
|
|
281
|
+
modules: moduleNames(root), otherPackages: otherPackages(root),
|
|
270
282
|
workPresent: fs.existsSync(path.join(root, '.starciwork')), hasImage: (file) => isFile(path.join(root, file), { followLinks: false }),
|
|
271
283
|
stack: readDeclaredStack(root, { parseYaml }), quality: readDeclaredQuality(root, { parseYaml }),
|
|
272
284
|
badges: readDeclaredBadges(root, { parseYaml, notices }),
|
|
@@ -321,6 +333,7 @@ export function appScripts(app, source) {
|
|
|
321
333
|
}),
|
|
322
334
|
...fe.map(entry => line(`start:${entry.name}`, `npm run start -w ${workspace(entry)}`)),
|
|
323
335
|
...(app.edition !== 'lite' && app.optionalSlots.includes('app.browser') ? [line('test:browser', 'playwright test -c browser/playwright.config.ts')] : []),
|
|
336
|
+
...(source?.modules?.length ? [line('build:modules', 'turbo run build --filter=./packages/*'), line('test:modules', 'turbo run test --filter=./packages/module-*')] : []),
|
|
324
337
|
line('docker:build', images.map(image => `npm run docker:build:${image.entry.name}`).join(' && ')),
|
|
325
338
|
...images.map(image => line(`docker:build:${image.entry.name}`, imageCommand(image))),
|
|
326
339
|
].join('\n ');
|
|
@@ -377,6 +390,12 @@ export function imageFiles(app, source) {
|
|
|
377
390
|
});
|
|
378
391
|
}
|
|
379
392
|
/** Every value a template of `scope` can name, derived from hfs.json and the presets. */
|
|
393
|
+
/** What a module package keeps out of the measured scope: its workbench (drawn with Grammar, a front end has no tests) and its specs. The logic of its template and manifest is measured. */
|
|
394
|
+
const moduleExclusions = (dirs) => dirs.flatMap(dir => [`${dir}/src/workbench/**`, `${dir}/src/**/index.ts`]);
|
|
395
|
+
/** The managed files of the registry package for the module packages the app has (empty when it has none). */
|
|
396
|
+
function registryTargets(app, source) {
|
|
397
|
+
return registryFiles(app.project, source?.modules ?? []).map((file) => ({ path: file.path, scope: APP_SCOPE, mode: 'file', content: file.content, hash: hashOf(file.content) }));
|
|
398
|
+
}
|
|
380
399
|
export function variables(app, scope, presets, sonarKey, manifest = loadSlotManifest(), source = { files: [], read: () => '' }) {
|
|
381
400
|
const stack = source.stack ?? { images: [], passEnv: [] };
|
|
382
401
|
const fe = app.sides.fe;
|
|
@@ -384,6 +403,10 @@ export function variables(app, scope, presets, sonarKey, manifest = loadSlotMani
|
|
|
384
403
|
const lite = app.edition === 'lite';
|
|
385
404
|
const providers = (app.sides.be.connections ?? []).map(connection => connection.provider);
|
|
386
405
|
const quality = source.quality ?? qualityServices({});
|
|
406
|
+
const modules = source.modules ?? [];
|
|
407
|
+
const moduleDirs = modules.map(name => `packages/${MODULE_PREFIX}${name}`);
|
|
408
|
+
// The module packages are built before anything imports them (typecheck, lint, the fe build and the be run all follow the registry).
|
|
409
|
+
const modulesFirst = modules.length ? 'npm run build:modules --silent && ' : '';
|
|
387
410
|
const grammarLint = scope === 'fe' ? grammarLintFor(fe.grammar) : null;
|
|
388
411
|
return {
|
|
389
412
|
header: HEADER(scope),
|
|
@@ -401,11 +424,12 @@ export function variables(app, scope, presets, sonarKey, manifest = loadSlotMani
|
|
|
401
424
|
// A lite app's declared Redis (sides.be.backing, ruling:R212/case-10) runs in development as one container of its pinned image.
|
|
402
425
|
backingScripts: lite && (app.sides.be.backing ?? []).includes('redis') ? redisDevText(app.project) : '',
|
|
403
426
|
// turbo builds every fe app workspace; ^build (turbo.json) builds the packages each app imports first, and caches both.
|
|
404
|
-
buildFe:
|
|
427
|
+
buildFe: `${modulesFirst}npm run codegen --silent && turbo run build --filter=./fe/apps/*`,
|
|
405
428
|
// The be side with its tsc; every fe workspace with its own typecheck script through turbo (the packages are built first: ^build).
|
|
406
|
-
typecheck:
|
|
429
|
+
typecheck: `${modulesFirst}npm run codegen --silent && tsc -p be/tsconfig.json && turbo run typecheck`,
|
|
407
430
|
// The typed lint reads the types the fe packages declare (their dist), so the packages are built first.
|
|
408
|
-
lintPrefix: packages ? 'npm run codegen --silent && turbo run build --filter=./fe/packages/* && ' : 'npm run codegen --silent && '
|
|
431
|
+
lintPrefix: `${modulesFirst}${packages ? 'npm run codegen --silent && turbo run build --filter=./fe/packages/* && ' : 'npm run codegen --silent && '}`,
|
|
432
|
+
moduleTests: modules.length ? ' && npm run test:modules' : '',
|
|
409
433
|
nodeMajor: String(NODE_MAJOR),
|
|
410
434
|
// The deploy server's age identity the restore workflow writes (stack/server-key.mjs owns the path).
|
|
411
435
|
serverAgeIdentity: SERVER_AGE_IDENTITY,
|
|
@@ -415,19 +439,19 @@ export function variables(app, scope, presets, sonarKey, manifest = loadSlotMani
|
|
|
415
439
|
sonarHostLine: sonarHostLine(quality.sonar),
|
|
416
440
|
sonarExclusions: sonarScope(presets).exclusions.join(','),
|
|
417
441
|
sonarTestInclusions: sonarScope(presets).tests.join(','),
|
|
418
|
-
sonarSources: ['be/apps', 'be/src', 'fe/apps', ...(packages ? ['fe/packages'] : [])].join(','),
|
|
442
|
+
sonarSources: ['be/apps', 'be/src', 'fe/apps', ...(packages ? ['fe/packages'] : []), ...moduleDirs.map(dir => `${dir}/src`)].join(','),
|
|
419
443
|
lcovReport: LCOV_REPORT,
|
|
420
444
|
cpdExclusions: SONAR_CPD_EXCLUSIONS.join(','),
|
|
421
445
|
...ciWorkflowCommands(source, LCOV_REPORT, source.qualityDeclared === false ? undefined : source.quality, { app, packages }),
|
|
422
446
|
// The coverage scope is the same in both editions: lite measures exactly what full measures (ruling:R212/case-1, amended by ruling:R212/case-4).
|
|
423
447
|
jestCoverage: scope === 'be' ? jestCoverageSource(jestCoverage(manifest, providers)) : '',
|
|
424
|
-
coverageExclusions: scope === APP_SCOPE ? sonarCoverageExclusions(manifest, providers).join(',') : '',
|
|
425
|
-
codecovExclusions: scope === APP_SCOPE ? sonarCoverageExclusions(manifest, providers).map(glob => ` - ${JSON.stringify(glob)}`).join('\n') : '',
|
|
426
|
-
codecovPaths: scope === APP_SCOPE ? codecovPaths(manifest, providers).map(glob => ` - ${JSON.stringify(glob)}`).join('\n') : '',
|
|
448
|
+
coverageExclusions: scope === APP_SCOPE ? [...sonarCoverageExclusions(manifest, providers), ...moduleExclusions(moduleDirs)].join(',') : '',
|
|
449
|
+
codecovExclusions: scope === APP_SCOPE ? [...sonarCoverageExclusions(manifest, providers), ...moduleExclusions(moduleDirs)].map(glob => ` - ${JSON.stringify(glob)}`).join('\n') : '',
|
|
450
|
+
codecovPaths: scope === APP_SCOPE ? [...codecovPaths(manifest, providers), ...moduleDirs.flatMap(dir => [`${dir}/src/manifest/**`, `${dir}/src/template/**`])].map(glob => ` - ${JSON.stringify(glob)}`).join('\n') : '',
|
|
427
451
|
codecovComponents: scope === APP_SCOPE ? componentsYaml(coverageComponents(manifest, { ...source, apps: app.sides.be.apps, providers })) : '',
|
|
428
452
|
codecovTarget: quality.codecov.target,
|
|
429
453
|
dependabotUpdates: dependabotUpdates(quality.dependabot),
|
|
430
|
-
tsconfigPaths: ['be/tsconfig.json', ...fe.apps.map(entry => `fe/apps/${entry.name}/tsconfig.json`), ...(packages ? ['fe/packages/*/tsconfig.json'] : [])].join(','),
|
|
454
|
+
tsconfigPaths: ['be/tsconfig.json', ...fe.apps.map(entry => `fe/apps/${entry.name}/tsconfig.json`), ...(packages ? ['fe/packages/*/tsconfig.json'] : []), ...moduleDirs.map(dir => `${dir}/tsconfig.json`)].join(','),
|
|
431
455
|
styleGlob: STYLE_GLOB,
|
|
432
456
|
grammarLintImport: grammarLint?.package ? `import { ${grammarLint.factory} } from "${grammarLint.package}"` : '',
|
|
433
457
|
grammarLintExport: grammarLint?.package ? `export default [...starciFeConfig({ hfs }), ...${grammarLint.factory}({ hfs })]` : 'export default starciFeConfig({ hfs })',
|
|
@@ -446,12 +470,24 @@ function parseScripts(text) {
|
|
|
446
470
|
return null;
|
|
447
471
|
}
|
|
448
472
|
}
|
|
473
|
+
/**
|
|
474
|
+
* The rendered targets of an app declaration. Without source facts the app counts the back-end source files its own targets
|
|
475
|
+
* render (the managed sign-in glue), as a check reading the disk after the targets are written does, so a render of a declaration
|
|
476
|
+
* alone is already in sync with itself.
|
|
477
|
+
*/
|
|
478
|
+
export function renderTargets(hfs, presets, options = {}) {
|
|
479
|
+
const first = renderOnce(hfs, presets, options);
|
|
480
|
+
if (options.source !== undefined)
|
|
481
|
+
return first;
|
|
482
|
+
const settled = settleSource({ files: [], read: () => '' }, first); // reason: the settled facts name only the rendered source files; every other fact stays absent as without source
|
|
483
|
+
return settled.files.length === 0 ? first : renderOnce(hfs, presets, { ...options, source: settled });
|
|
484
|
+
}
|
|
449
485
|
/**
|
|
450
486
|
* The managed content of every target of the app: [{ path, mode, content, hash, scope }], paths app-relative (a side's targets under
|
|
451
487
|
* its folder); a scripts target also carries `scripts`, the parsed block. `sonarKey` is the key the be side's stack declaration
|
|
452
488
|
* names; without one the key is the project.
|
|
453
489
|
*/
|
|
454
|
-
|
|
490
|
+
function renderOnce(hfs, presets, { sonarKey, readTemplate = readBundled, manifest = loadSlotManifest(), source } = {}) {
|
|
455
491
|
const app = validateHfs(hfs, manifest);
|
|
456
492
|
const hasTemplate = (name) => { try {
|
|
457
493
|
return readTemplate(name) !== undefined;
|
|
@@ -467,24 +503,24 @@ export function renderTargets(hfs, presets, { sonarKey, readTemplate = readBundl
|
|
|
467
503
|
declaredTargets.push({ path: 'README.md', mode: 'block', markdown: true, template: 'app/readme-badges/README.md' });
|
|
468
504
|
const targets = declaredTargets
|
|
469
505
|
.filter(target => !(app.edition === 'lite' && source?.workPresent === false && target.path === '.starciwork/.gitignore'));
|
|
470
|
-
return targets.map((target) => {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
506
|
+
return [...(scope === APP_SCOPE ? registryTargets(app, source) : []), ...targets.map((target) => {
|
|
507
|
+
const template = target.path === 'codecov.yml' && source?.quality?.codecov.enabled === false
|
|
508
|
+
? '{{header}}\n# Codecov is disabled in the manifest.\ncodecov:\n require_ci_to_pass: false\n'
|
|
509
|
+
: readTemplate(target.template);
|
|
510
|
+
const body = trailingNewline(render(template, vars, readTemplate));
|
|
511
|
+
const where = onSide(scope, target.path);
|
|
512
|
+
if (target.mode === 'scripts') {
|
|
513
|
+
const scripts = parseScripts(body);
|
|
514
|
+
if (!scripts)
|
|
515
|
+
throw new SyncError('HFS_SYNC_TEMPLATE_VARIABLE', `the rendered ${target.template} has no scripts object`);
|
|
516
|
+
const content = scriptsText(scripts);
|
|
517
|
+
return { path: where, scope, mode: 'scripts', content, hash: hashOf(content), scripts };
|
|
518
|
+
}
|
|
519
|
+
const markers = target.markdown ? { begin: '<!-- >>> starci app sync (managed block; do not edit) >>> -->', end: '<!-- <<< starci app sync <<< -->' } : { begin: BLOCK_BEGIN, end: BLOCK_END };
|
|
520
|
+
const spacing = target.markdown ? '\n' : '';
|
|
521
|
+
const managed = target.mode === 'block' ? `${markers.begin}\n${spacing}${trailingNewline(body)}${spacing}${markers.end}\n` : body;
|
|
522
|
+
return { path: where, scope, mode: target.mode, markdown: target.markdown, markers, content: managed, hash: hashOf(managed) };
|
|
523
|
+
})];
|
|
488
524
|
});
|
|
489
525
|
}
|
|
490
526
|
// The part of a file sync manages: all of it, all of it but the verb's section, the marked block, or the scripts block as text.
|
|
@@ -591,11 +627,30 @@ export function loadHfs(root, manifest = loadSlotManifest()) {
|
|
|
591
627
|
validateHfs(hfs, manifest);
|
|
592
628
|
return hfs;
|
|
593
629
|
}
|
|
630
|
+
/**
|
|
631
|
+
* The source facts as they stand once the rendered back-end source files are on disk: a managed `.ts` target of the be side counts as a
|
|
632
|
+
* file of the app (and reads as its render), so the CI shard count and the coverage scope that count or read the source name the same
|
|
633
|
+
* files before and after the targets are written, and one sync leaves the app in sync.
|
|
634
|
+
*/
|
|
635
|
+
export function settleSource(source, targets) {
|
|
636
|
+
const added = new Map();
|
|
637
|
+
for (const target of targets) {
|
|
638
|
+
const rel = target.scope === 'be' && target.path.startsWith('be/') ? target.path.slice('be/'.length) : '';
|
|
639
|
+
if (SOURCE_FILE.test(rel) && (rel.startsWith('apps/') || rel.startsWith('src/')) && !source.files.includes(rel))
|
|
640
|
+
added.set(rel, target.content);
|
|
641
|
+
}
|
|
642
|
+
if (added.size === 0)
|
|
643
|
+
return source;
|
|
644
|
+
return { ...source, files: [...source.files, ...added.keys()].sort(byCodeUnit), read: (file) => added.get(file) ?? source.read(file) };
|
|
645
|
+
}
|
|
594
646
|
/** The rendered targets of the app at `root`: its hfs.json, the Sonar key the be side's stack declaration names, the presets it installs. */
|
|
595
647
|
export async function renderRepo(root, { presets, parseYaml, notices, manifest = loadSlotManifest() } = {}) {
|
|
596
648
|
const hfs = loadHfs(root, manifest);
|
|
597
649
|
const sonarKey = await readDeclaredSonarKey(root, { parseYaml, fail: (message) => { throw new SyncError('HFS_SYNC_SONAR_KEY', message); } });
|
|
598
|
-
|
|
650
|
+
const installed = presets ?? await loadPresets(root);
|
|
651
|
+
const source = appSource(root, { parseYaml, notices });
|
|
652
|
+
const settled = settleSource(source, renderTargets(hfs, installed, { sonarKey, manifest, source }));
|
|
653
|
+
return { hfs, targets: renderTargets(hfs, installed, { sonarKey, manifest, source: settled }) };
|
|
599
654
|
}
|
|
600
655
|
/** The folders a lite app's sync retires (ruling lite-self-hosted-postgres-passport-minio): the generated Supabase row types nothing reads any more. */
|
|
601
656
|
const LITE_RETIRED_DIRS = Object.freeze(['supabase/types']);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Unit run of the {{pascal}} module package: the logic of its template and manifest is measured at 100 per file. */
|
|
2
|
+
module.exports = {
|
|
3
|
+
testEnvironment: "node",
|
|
4
|
+
transform: { "^.+\.tsx?$": ["ts-jest", { tsconfig: "tsconfig.json", isolatedModules: true }] },
|
|
5
|
+
testMatch: ["<rootDir>/src/**/*.spec.ts"],
|
|
6
|
+
collectCoverageFrom: ["src/manifest/**/*.ts", "src/template/**/*.ts", "!src/**/*.spec.ts", "!src/**/index.ts"],
|
|
7
|
+
coverageThreshold: { "./src/manifest/**/*.ts": { lines: 100, branches: 100, functions: 100, statements: 100 }, "./src/template/**/*.ts": { lines: 100, branches: 100, functions: 100, statements: 100 } },
|
|
8
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { {{upper}}_VIEW_IDS, {{upper}}_WORKBENCH } from "./declaration"
|
|
2
|
+
|
|
3
|
+
describe("the {{pascal}} workbench declaration", () => {
|
|
4
|
+
it("declares one view per id, in strip order", () => {
|
|
5
|
+
expect({{upper}}_WORKBENCH.views.map((view) => view.id)).toEqual([...{{upper}}_VIEW_IDS])
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
it("is keyed like its module", () => {
|
|
9
|
+
expect({{upper}}_WORKBENCH).toMatchObject({ key: "{{name}}", namespace: "{{name}}" })
|
|
10
|
+
})
|
|
11
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { WorkbenchDeclaration } from "@{{project}}/agent-sdk/workbench/contract"
|
|
2
|
+
|
|
3
|
+
/** The views of the {{pascal}} workbench in strip order (`?tab=`), each labelled from the module's namespace. */
|
|
4
|
+
export const {{upper}}_VIEW_IDS: readonly ["overview"] = ["overview"]
|
|
5
|
+
|
|
6
|
+
/** The {{pascal}} workbench: one overview every member of the workspace opens; grow its views, permissions, operations and exposed reads here. */
|
|
7
|
+
export const {{upper}}_WORKBENCH: WorkbenchDeclaration = {
|
|
8
|
+
key: "{{name}}",
|
|
9
|
+
namespace: "{{name}}",
|
|
10
|
+
views: [{ id: "overview", labelKey: "tab_overview" }],
|
|
11
|
+
permissions: [],
|
|
12
|
+
operations: [],
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { {{upper}}_VIEW_IDS, {{upper}}_WORKBENCH } from "./declaration"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { moduleListProblems } from "@{{project}}/agent-sdk/module"
|
|
2
|
+
import { {{upper}}_SERVER, {{upper}}_TEMPLATE } from "./{{name}}.template"
|
|
3
|
+
|
|
4
|
+
describe("{{upper}}_SERVER", () => {
|
|
5
|
+
it("is a module list the host may load", () => {
|
|
6
|
+
expect(moduleListProblems([{{upper}}_SERVER])).toEqual([])
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
it("serves its template under the key of the module", () => {
|
|
10
|
+
expect({{upper}}_SERVER.key).toBe({{upper}}_TEMPLATE.key)
|
|
11
|
+
expect({{upper}}_TEMPLATE).toMatchObject({ key: "{{name}}", workbench: "{{name}}" })
|
|
12
|
+
})
|
|
13
|
+
})
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ModuleServer, ModuleTemplate } from "@{{project}}/agent-sdk/module"
|
|
2
|
+
|
|
3
|
+
/** The {{pascal}} module's template: its copy, agent, setup gates, authority actions and accepted commands. */
|
|
4
|
+
export const {{upper}}_TEMPLATE: ModuleTemplate = {
|
|
5
|
+
key: "{{name}}",
|
|
6
|
+
order: 100,
|
|
7
|
+
name: { vi: "{{pascal}}", en: "{{pascal}}" },
|
|
8
|
+
shortName: { vi: "{{pascal}}", en: "{{pascal}}" },
|
|
9
|
+
description: { vi: "{{pascal}}", en: "{{pascal}}" },
|
|
10
|
+
points: { vi: [], en: [] },
|
|
11
|
+
purpose: "{{pascal}}",
|
|
12
|
+
icon: "module",
|
|
13
|
+
glyph: "M12 3l9 5v8l-9 5-9-5V8z",
|
|
14
|
+
mascot: "",
|
|
15
|
+
category: "operations",
|
|
16
|
+
status: "early",
|
|
17
|
+
audience: "customer",
|
|
18
|
+
includedWithWorkspace: false,
|
|
19
|
+
available: false,
|
|
20
|
+
knowledgeFolder: "{{name}}",
|
|
21
|
+
workbench: "{{name}}",
|
|
22
|
+
settingsExtras: null,
|
|
23
|
+
requires: { capabilities: [], connections: [] },
|
|
24
|
+
connectionProviders: [],
|
|
25
|
+
defaultOperatingMode: "assist",
|
|
26
|
+
multiInstance: false,
|
|
27
|
+
agent: {
|
|
28
|
+
label: { vi: "{{pascal}}", en: "{{pascal}}" },
|
|
29
|
+
handle: "{{name}}",
|
|
30
|
+
summary: { vi: "{{pascal}}", en: "{{pascal}}" },
|
|
31
|
+
capabilities: { vi: [], en: [] },
|
|
32
|
+
role: { vi: "{{pascal}}", en: "{{pascal}}" },
|
|
33
|
+
instructions: { vi: "{{pascal}}", en: "{{pascal}}" },
|
|
34
|
+
},
|
|
35
|
+
gates: [],
|
|
36
|
+
authorityActions: [],
|
|
37
|
+
accepts: [],
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The server half of the {{pascal}} module: its template and the words it adds to a graph. */
|
|
41
|
+
export const {{upper}}_SERVER: ModuleServer = { key: "{{name}}", template: {{upper}}_TEMPLATE }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { {{upper}}_SERVER, {{upper}}_TEMPLATE } from "./{{name}}.template"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ReactNode } from "react"
|
|
2
|
+
import { EmptyNotice, Page } from "@starci/grammar/common"
|
|
3
|
+
|
|
4
|
+
/** What the {{pascal}} workbench draws: its title and the words of its empty state. */
|
|
5
|
+
export interface {{pascal}}WorkbenchViewProps {
|
|
6
|
+
readonly title: string
|
|
7
|
+
readonly emptyTitle: string
|
|
8
|
+
readonly emptyBody: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Draws the {{pascal}} workbench with Grammar alone; it owns no class string, colour or visual of its own. */
|
|
12
|
+
export const {{pascal}}WorkbenchView = (props: {{pascal}}WorkbenchViewProps): ReactNode => (
|
|
13
|
+
<Page header={{ title: props.title }}>
|
|
14
|
+
<EmptyNotice message={props.emptyTitle} description={props.emptyBody} />
|
|
15
|
+
</Page>
|
|
16
|
+
)
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
{{appScripts}}
|
|
5
5
|
{{> app/package-scripts/core.json}}
|
|
6
6
|
"typecheck:tests": "tsc -p be/src/tests/tsconfig.json",
|
|
7
|
-
"test": "cd be && jest --selectProjects unit --coverage",
|
|
7
|
+
"test": "cd be && jest --selectProjects unit --coverage{{moduleTests}}",
|
|
8
8
|
"test:integration": "npm run typecheck:tests && cd be && jest --selectProjects integration",
|
|
9
9
|
"test:e2e": "npm run typecheck:tests && cd be && jest --selectProjects e2e",
|
|
10
10
|
"test:contract": "npm run typecheck:tests && cd be && jest --selectProjects contract",
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
{{appScripts}}
|
|
5
5
|
{{> app/package-scripts/core.json}}
|
|
6
6
|
"typecheck:tests": "tsc -p be/src/tests/tsconfig.json",
|
|
7
|
-
"test": "cd be && jest --selectProjects unit --coverage",
|
|
7
|
+
"test": "cd be && jest --selectProjects unit --coverage{{moduleTests}}",
|
|
8
8
|
"test:integration": "npm run typecheck:tests && cd be && jest --selectProjects integration",
|
|
9
9
|
{{> app/package-scripts/db.json}}
|
|
10
10
|
{{backingScripts}}
|