@starci/hfs 4.2.17 → 4.3.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 +17 -0
- package/README.md +7 -0
- package/package.json +1 -1
- package/runtime/knowledge/hfs/canon-pins.yaml +4 -4
- package/runtime/knowledge/hfs/rules.yaml +41 -13
- package/runtime/knowledge/hfs/slots.yaml +99 -43
- package/runtime/knowledge/patterns/be/api.yaml +64 -1
- package/runtime/knowledge/patterns/be/cli.yaml +23 -5
- package/runtime/knowledge/patterns/be/webhooks.yaml +187 -32
- package/runtime/knowledge/patterns/fe/folder.yaml +3 -0
- package/runtime/modules/kernel/failure-codes.yaml +15 -4
- package/runtime/scripts/api/git/cat-file.mjs +5 -0
- package/runtime/scripts/api/sops/resolve-sops.mjs +7 -0
- package/runtime/scripts/api/sops/seal.mjs +1 -1
- package/runtime/scripts/hfs/architecture/backend-app-files.mjs +110 -0
- package/runtime/scripts/hfs/architecture/backend.mjs +20 -3
- package/runtime/scripts/hfs/architecture/background-unowned.mjs +157 -41
- package/runtime/scripts/hfs/architecture/canon-clones.mjs +240 -0
- package/runtime/scripts/hfs/architecture/canon-jsx-clones.mjs +143 -0
- package/runtime/scripts/hfs/architecture/clones.mjs +17 -8
- package/runtime/scripts/hfs/architecture/connection-map.mjs +14 -12
- package/runtime/scripts/hfs/architecture/context-map.mjs +25 -14
- package/runtime/scripts/hfs/architecture/context-owner.mjs +1 -2
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +14 -9
- package/runtime/scripts/hfs/architecture/frontend-block-render.mjs +104 -0
- package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +30 -11
- package/runtime/scripts/hfs/architecture/i18n-keys.mjs +16 -3
- package/runtime/scripts/hfs/architecture/machine-ast.mjs +17 -0
- package/runtime/scripts/hfs/architecture/module-per-transport.mjs +21 -12
- package/runtime/scripts/hfs/architecture/register-once.mjs +13 -5
- package/runtime/scripts/hfs/architecture/required-files.mjs +2 -1
- package/runtime/scripts/hfs/architecture/staged-consumers.mjs +37 -0
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +3 -6
- package/runtime/scripts/hfs/architecture/supabase-be.mjs +1 -6
- package/runtime/scripts/hfs/architecture/tiers.mjs +8 -1
- package/runtime/scripts/hfs/architecture/typescript.mjs +1 -8
- package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +1 -11
- package/runtime/scripts/hfs/check.mjs +18 -26
- package/runtime/scripts/hfs/manifest-shape.mjs +27 -2
- package/runtime/scripts/hfs/path-findings.mjs +32 -2
- package/runtime/scripts/hfs/rules/database-migrations.mjs +36 -35
- package/runtime/scripts/hfs/rules/database.mjs +2 -2
- package/runtime/scripts/hfs/rules/edition.mjs +5 -5
- package/runtime/scripts/hfs/rules/proof-commands.mjs +2 -0
- package/runtime/scripts/hfs/rules/secrets.mjs +13 -0
- package/runtime/scripts/hfs/rules/stacks-runtime.mjs +128 -0
- package/runtime/scripts/hfs/rules/stacks.mjs +13 -8
- package/runtime/scripts/hfs/secret-document.mjs +97 -0
- package/runtime/scripts/hfs/secret-errors.mjs +20 -0
- package/runtime/scripts/hfs/secret-files.mjs +124 -0
- package/runtime/scripts/hfs/secret-init.mjs +192 -0
- package/runtime/scripts/hfs/secret-output.mjs +100 -0
- package/runtime/scripts/hfs/secret-sops.mjs +98 -0
- package/runtime/scripts/hfs/secret-store.mjs +161 -0
- package/runtime/scripts/hfs/secret.mjs +135 -195
- package/runtime/scripts/hfs/slot-imports.mjs +3 -1
- package/runtime/scripts/hfs/slot-required.mjs +13 -0
- package/runtime/scripts/hfs/slot-semantic-problems.mjs +2 -1
- package/runtime/scripts/hfs/tree-findings.mjs +22 -0
- package/runtime/scripts/lib/roots.mjs +59 -0
- package/runtime/scripts/lib/swarm-stack.mjs +139 -0
- package/runtime/scripts/lib/ts-analysis.mjs +20 -0
- package/scaffold/add-cli-lite.mjs +10 -11
- package/scaffold/add-inbox-lite.mjs +15 -84
- package/scaffold/add-plan.mjs +183 -0
- package/scaffold/add-table.mjs +44 -5
- package/scaffold/add.mjs +33 -166
- package/scaffold/app.mjs +5 -5
- package/scaffold/image.mjs +2 -2
- package/src/main.mjs +12 -6
- package/sync/index.mjs +23 -16
- package/templates/app/gitignore +1 -0
- package/templates/app/gitignore-lite +1 -0
- package/templates/app/hooks/husky/pre-commit +7 -12
- package/templates/app/skeleton/.sops.yaml +1 -1
- package/templates/app/skeleton-lite/.sops.yaml +1 -1
- package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +16 -0
- package/templates/app/skeleton-lite/.starciwork/workspace.yaml +9 -0
- package/templates/app/skeleton-lite/README.md +4 -2
- package/templates/app/starciwork.gitignore-lite +6 -0
- package/templates/be/inbox/database-client.spec.ts.tpl +15 -20
- package/templates/be/inbox/database-client.ts.tpl +7 -22
- package/templates/be/inbox/service.spec.ts.tpl +16 -20
- package/templates/be/inbox/service.ts.tpl +6 -10
- package/templates/be/inbox/sql.ts.tpl +2 -2
- package/templates/be/inbox/webhook-door.spec.ts.tpl +3 -3
- package/templates/be/inbox/webhook-door.ts.tpl +4 -3
- package/templates/be/patterns/webhooks/webhook.door.spec.ts.tpl +3 -3
- package/templates/be/patterns/webhooks/webhook.door.ts.tpl +3 -3
- package/templates/be/skeleton/apps/__app__/src/__app__.env.ts +21 -0
- package/templates/be/skeleton/apps/__app__/src/__app__.options.ts +1 -1
- package/templates/be/skeleton/apps/__app__/src/app.module.ts +7 -39
- package/templates/be/skeleton/apps/__app__/src/compose/platform.ts +42 -0
- package/templates/be/skeleton/apps/__app__/src/main.ts +3 -9
- package/templates/be/skeleton/apps/cli/src/cli.env.ts +13 -0
- package/templates/be/skeleton/apps/cli/src/cli.options.ts +6 -8
- package/templates/be/skeleton/apps/cli/src/main.ts +3 -2
- package/templates/be/skeleton/src/modules/platform/config/env-source.config.ts +6 -6
- package/templates/be/skeleton-lite/apps/api/src/api.env.ts +25 -0
- package/templates/be/skeleton-lite/apps/api/src/api.options.ts +1 -1
- package/templates/be/skeleton-lite/apps/api/src/app.module.ts +6 -42
- package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +45 -0
- package/templates/be/skeleton-lite/apps/api/src/main.ts +4 -11
- package/templates/be/skeleton-lite/apps/cli/src/cli.env.ts +17 -0
- package/upgrade/file-state.mjs +68 -0
- package/upgrade/index.mjs +7 -56
- package/runtime/scripts/api/git/log.mjs +0 -5
- package/templates/app/skeleton-lite/.starciwork/shell/index.yaml +0 -55
|
@@ -21,18 +21,18 @@ export class Secret {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
* The only reader of the process environment. Typed readers name the missing or malformed key in the error
|
|
25
|
-
* `<KEY>_FILE`
|
|
24
|
+
* The only reader of the process environment. Typed readers name the missing or malformed key in the error.
|
|
25
|
+
* A declared `<KEY>_FILE` takes priority over `<KEY>` and supplies its exact file contents, so secrets can be mounted.
|
|
26
26
|
*/
|
|
27
27
|
export class EnvSource {
|
|
28
28
|
constructor(private readonly values: Readonly<Record<string, string | undefined>>) {}
|
|
29
29
|
|
|
30
|
-
/** Reads the process environment once
|
|
30
|
+
/** Reads the process environment once; a declared `<KEY>_FILE` must be readable and never falls back to `<KEY>`. */
|
|
31
31
|
static fromProcess(): EnvSource {
|
|
32
32
|
const values: Record<string, string | undefined> = { ...process.env }
|
|
33
|
-
for (const [key, path] of Object.entries(
|
|
34
|
-
if (key.endsWith(FILE_SUFFIX) && path)
|
|
35
|
-
values[key.slice(0, -FILE_SUFFIX.length)] = readFileSync(path, "utf8")
|
|
33
|
+
for (const [key, path] of Object.entries(values)) {
|
|
34
|
+
if (key.endsWith(FILE_SUFFIX) && path !== undefined)
|
|
35
|
+
values[key.slice(0, -FILE_SUFFIX.length)] = readFileSync(path, "utf8")
|
|
36
36
|
}
|
|
37
37
|
return new EnvSource(values)
|
|
38
38
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { EnvSource } from "@modules/platform/config"
|
|
2
|
+
import { parseServerConfig } from "@modules/platform/config"
|
|
3
|
+
import { parsePrimaryDatabaseConfig } from "@modules/platform/database"
|
|
4
|
+
import { parseHttpSecurityConfig } from "@modules/platform/http-security"
|
|
5
|
+
import { parseSupabaseConfig } from "@modules/integrations/supabase"
|
|
6
|
+
|
|
7
|
+
/** The API environment values required before composition. */
|
|
8
|
+
export interface ApiEnv {
|
|
9
|
+
/** The HTTP listener. */
|
|
10
|
+
readonly server: ReturnType<typeof parseServerConfig>
|
|
11
|
+
/** The origin allowlist and rate limits. */
|
|
12
|
+
readonly httpSecurity: ReturnType<typeof parseHttpSecurityConfig>
|
|
13
|
+
/** The least-privilege PostgreSQL connection to the Supabase project. */
|
|
14
|
+
readonly database: ReturnType<typeof parsePrimaryDatabaseConfig>
|
|
15
|
+
/** Supabase Auth, Storage and token-verification configuration. */
|
|
16
|
+
readonly supabase: ReturnType<typeof parseSupabaseConfig>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Reads the API environment; missing or invalid values fail boot, and secrets use `_FILE` first. */
|
|
20
|
+
export const readApiEnv = (env: EnvSource): ApiEnv => ({
|
|
21
|
+
server: parseServerConfig(env),
|
|
22
|
+
httpSecurity: parseHttpSecurityConfig(env),
|
|
23
|
+
database: parsePrimaryDatabaseConfig(env),
|
|
24
|
+
supabase: parseSupabaseConfig(env),
|
|
25
|
+
})
|
|
@@ -3,7 +3,7 @@ import type { ServerOptions } from "@modules/platform/config"
|
|
|
3
3
|
import type { DatabaseConnectionOptions } from "@modules/platform/database"
|
|
4
4
|
import type { HttpSecurityOptions } from "@modules/platform/http-security"
|
|
5
5
|
|
|
6
|
-
/** Everything the API needs from its environment, parsed
|
|
6
|
+
/** Everything the API needs from its environment, parsed by `api.env.ts` and handed to `AppModule.register`. */
|
|
7
7
|
export interface ApiOptions {
|
|
8
8
|
/** The HTTP listener. */
|
|
9
9
|
readonly server: ServerOptions
|
|
@@ -2,33 +2,21 @@ import { Module } from "@nestjs/common"
|
|
|
2
2
|
import type { DynamicModule } from "@nestjs/common"
|
|
3
3
|
import { APP_FILTER, APP_GUARD } from "@nestjs/core"
|
|
4
4
|
import { SystemHealthHttpModule } from "@features/api/system-health"
|
|
5
|
-
import { AuthGuard,
|
|
5
|
+
import { AuthGuard, IdentityModule } from "@modules/domain/identity"
|
|
6
6
|
import {
|
|
7
7
|
SUPABASE_ACCESS_TOKEN_VERIFIER,
|
|
8
8
|
SUPABASE_ADMIN_CLIENT,
|
|
9
|
-
SUPABASE_ERROR_KINDS,
|
|
10
9
|
createSupabaseAccessTokenVerifier,
|
|
11
10
|
createSupabaseAdminClient,
|
|
12
11
|
} from "@modules/integrations/supabase"
|
|
13
12
|
import { LivenessModule } from "@modules/domain/liveness"
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import { DATABASE_ERROR_KINDS, DatabaseModule } from "@modules/platform/database"
|
|
18
|
-
import { ERRORS_MESSAGES, ErrorsFilter, ErrorsModule } from "@modules/platform/errors"
|
|
19
|
-
import {
|
|
20
|
-
HTTP_SECURITY_ERROR_KINDS,
|
|
21
|
-
HTTP_SECURITY_MESSAGES,
|
|
22
|
-
HttpSecurityModule,
|
|
23
|
-
OriginGuard,
|
|
24
|
-
RateLimitGuard,
|
|
25
|
-
} from "@modules/platform/http-security"
|
|
26
|
-
import { I18nModule } from "@modules/platform/i18n"
|
|
27
|
-
import { LoggingModule } from "@modules/platform/logging"
|
|
13
|
+
import { ErrorsFilter } from "@modules/platform/errors"
|
|
14
|
+
import { OriginGuard, RateLimitGuard } from "@modules/platform/http-security"
|
|
15
|
+
import { platformModules } from "./compose/platform"
|
|
28
16
|
import type { ApiOptions } from "./api.options"
|
|
29
17
|
|
|
30
18
|
@Module({})
|
|
31
|
-
/** Composition root of the API:
|
|
19
|
+
/** Composition root of the API: it spreads the composition groups of `compose/`, and the three guards run in a fixed order. */
|
|
32
20
|
export class AppModule {
|
|
33
21
|
/** Builds the module from options parsed once at boot; nothing here reads the environment. */
|
|
34
22
|
static register(options: ApiOptions): DynamicModule {
|
|
@@ -36,31 +24,7 @@ export class AppModule {
|
|
|
36
24
|
return {
|
|
37
25
|
module: AppModule,
|
|
38
26
|
imports: [
|
|
39
|
-
|
|
40
|
-
LoggingModule.register({ isGlobal: true }),
|
|
41
|
-
I18nModule.register({
|
|
42
|
-
isGlobal: true,
|
|
43
|
-
bundles: [ERRORS_MESSAGES, HTTP_SECURITY_MESSAGES, IDENTITY_MESSAGES],
|
|
44
|
-
}),
|
|
45
|
-
ErrorsModule.register({
|
|
46
|
-
isGlobal: true,
|
|
47
|
-
kinds: [
|
|
48
|
-
CONFIG_ERROR_KINDS,
|
|
49
|
-
DATABASE_ERROR_KINDS,
|
|
50
|
-
HTTP_SECURITY_ERROR_KINDS,
|
|
51
|
-
IDENTITY_ERROR_KINDS,
|
|
52
|
-
SUPABASE_ERROR_KINDS,
|
|
53
|
-
],
|
|
54
|
-
}),
|
|
55
|
-
CqrsModule.register({ isGlobal: true }),
|
|
56
|
-
HttpSecurityModule.register({
|
|
57
|
-
isGlobal: true,
|
|
58
|
-
...options.httpSecurity,
|
|
59
|
-
}),
|
|
60
|
-
DatabaseModule.register({
|
|
61
|
-
isGlobal: true,
|
|
62
|
-
connections: [options.database],
|
|
63
|
-
}),
|
|
27
|
+
...platformModules(options),
|
|
64
28
|
IdentityModule.register({ isGlobal: true, verifyAccessToken }),
|
|
65
29
|
LivenessModule.register({ isGlobal: true }),
|
|
66
30
|
SystemHealthHttpModule,
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { DynamicModule } from "@nestjs/common"
|
|
2
|
+
import { IDENTITY_ERROR_KINDS, IDENTITY_MESSAGES } from "@modules/domain/identity"
|
|
3
|
+
import { SUPABASE_ERROR_KINDS } from "@modules/integrations/supabase"
|
|
4
|
+
import { ClockModule } from "@modules/platform/clock"
|
|
5
|
+
import { CONFIG_ERROR_KINDS } from "@modules/platform/config"
|
|
6
|
+
import { CqrsModule } from "@modules/platform/cqrs"
|
|
7
|
+
import { DATABASE_ERROR_KINDS, DatabaseModule } from "@modules/platform/database"
|
|
8
|
+
import { ERRORS_MESSAGES, ErrorsModule } from "@modules/platform/errors"
|
|
9
|
+
import { HTTP_SECURITY_ERROR_KINDS, HTTP_SECURITY_MESSAGES, HttpSecurityModule } from "@modules/platform/http-security"
|
|
10
|
+
import { I18nModule } from "@modules/platform/i18n"
|
|
11
|
+
import { LoggingModule } from "@modules/platform/logging"
|
|
12
|
+
import type { ApiOptions } from "../api.options"
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The platform area of the API: every platform capability, registered once and app-wide. A composition group holds
|
|
16
|
+
* one area's module list and nothing else (no logic, no environment read); `app.module.ts` spreads it into its
|
|
17
|
+
* imports.
|
|
18
|
+
*/
|
|
19
|
+
export const platformModules = (options: ApiOptions): Array<DynamicModule> => [
|
|
20
|
+
ClockModule.register({ isGlobal: true }),
|
|
21
|
+
LoggingModule.register({ isGlobal: true }),
|
|
22
|
+
I18nModule.register({
|
|
23
|
+
isGlobal: true,
|
|
24
|
+
bundles: [ERRORS_MESSAGES, HTTP_SECURITY_MESSAGES, IDENTITY_MESSAGES],
|
|
25
|
+
}),
|
|
26
|
+
ErrorsModule.register({
|
|
27
|
+
isGlobal: true,
|
|
28
|
+
kinds: [
|
|
29
|
+
CONFIG_ERROR_KINDS,
|
|
30
|
+
DATABASE_ERROR_KINDS,
|
|
31
|
+
HTTP_SECURITY_ERROR_KINDS,
|
|
32
|
+
IDENTITY_ERROR_KINDS,
|
|
33
|
+
SUPABASE_ERROR_KINDS,
|
|
34
|
+
],
|
|
35
|
+
}),
|
|
36
|
+
CqrsModule.register({ isGlobal: true }),
|
|
37
|
+
HttpSecurityModule.register({
|
|
38
|
+
isGlobal: true,
|
|
39
|
+
...options.httpSecurity,
|
|
40
|
+
}),
|
|
41
|
+
DatabaseModule.register({
|
|
42
|
+
isGlobal: true,
|
|
43
|
+
connections: [options.database],
|
|
44
|
+
}),
|
|
45
|
+
]
|
|
@@ -1,22 +1,15 @@
|
|
|
1
1
|
import { NestFactory } from "@nestjs/core"
|
|
2
2
|
import { SystemClockService } from "@modules/platform/clock"
|
|
3
|
-
import { EnvSource
|
|
4
|
-
import {
|
|
5
|
-
import { parseHttpSecurityConfig, RequestValidationService } from "@modules/platform/http-security"
|
|
3
|
+
import { EnvSource } from "@modules/platform/config"
|
|
4
|
+
import { RequestValidationService } from "@modules/platform/http-security"
|
|
6
5
|
import { createJsonLogger, LoggingLogEvent } from "@modules/platform/logging"
|
|
7
|
-
import {
|
|
6
|
+
import { readApiEnv } from "./api.env"
|
|
8
7
|
import { AppModule } from "./app.module"
|
|
9
|
-
import type { ApiOptions } from "./api.options"
|
|
10
8
|
|
|
11
9
|
/** Reads the environment once, composes the API from parsed options and serves until a shutdown signal. */
|
|
12
10
|
const bootstrap = async (): Promise<void> => {
|
|
13
11
|
const env = EnvSource.fromProcess()
|
|
14
|
-
const options
|
|
15
|
-
server: parseServerConfig(env),
|
|
16
|
-
httpSecurity: parseHttpSecurityConfig(env),
|
|
17
|
-
database: parsePrimaryDatabaseConfig(env),
|
|
18
|
-
supabase: parseSupabaseConfig(env),
|
|
19
|
-
}
|
|
12
|
+
const options = readApiEnv(env)
|
|
20
13
|
const app = await NestFactory.create(AppModule.register(options), { rawBody: true })
|
|
21
14
|
app.enableCors({ origin: [...options.httpSecurity.allowedOrigins] })
|
|
22
15
|
app.enableShutdownHooks()
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { EnvSource, Secret } from "@modules/platform/config"
|
|
2
|
+
import { parsePrimaryDatabaseConfig } from "@modules/platform/database"
|
|
3
|
+
|
|
4
|
+
/** The cli's primary database configuration before command composition. */
|
|
5
|
+
export interface CliEnv {
|
|
6
|
+
/** The logical connection name declared in `hfs.json`. */
|
|
7
|
+
readonly name: string
|
|
8
|
+
/** The Supabase PostgreSQL provider. */
|
|
9
|
+
readonly provider: "supabase"
|
|
10
|
+
/** The least-privilege database connection URL, read through the secret source. */
|
|
11
|
+
readonly url: Secret
|
|
12
|
+
/** The isolated PostgreSQL schema. */
|
|
13
|
+
readonly schema?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Reads the cli environment with no defaults; database secret readers prefer mounted files. */
|
|
17
|
+
export const readCliEnv = (env: EnvSource): CliEnv => parsePrimaryDatabaseConfig(env)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Authored-file preservation, snapshots and rollback for an additive edition upgrade.
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { dockerfilePath } from "../runtime/scripts/hfs/rules/docker.mjs";
|
|
5
|
+
|
|
6
|
+
/** App-owned additions are preserved; only declared regular optional front-end images receive that treatment. */
|
|
7
|
+
export function authoredAdditionReasons(declaration, source) {
|
|
8
|
+
const reasons = new Map([[
|
|
9
|
+
".starcistacks/application-stacks.yaml",
|
|
10
|
+
"The app already declares its stacks; the declaration stays as the app wrote it.",
|
|
11
|
+
]]);
|
|
12
|
+
for (const app of declaration.sides.fe.apps) {
|
|
13
|
+
const file = dockerfilePath("fe", app.name);
|
|
14
|
+
if (source.hasImage(file)) reasons.set(file,
|
|
15
|
+
"The lite app already authors this optional front-end image; its Dockerfile stays as the app wrote it.");
|
|
16
|
+
}
|
|
17
|
+
return reasons;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function remember(snapshot, target) {
|
|
21
|
+
if (snapshot.has(target)) return;
|
|
22
|
+
snapshot.set(
|
|
23
|
+
target,
|
|
24
|
+
fs.existsSync(target) && fs.statSync(target).isFile()
|
|
25
|
+
? fs.readFileSync(target)
|
|
26
|
+
: null,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function ensureParent(root, target, directories) {
|
|
31
|
+
const missing = [];
|
|
32
|
+
for (
|
|
33
|
+
let at = path.dirname(target);
|
|
34
|
+
at.startsWith(root) && at !== root && !fs.existsSync(at);
|
|
35
|
+
at = path.dirname(at)
|
|
36
|
+
)
|
|
37
|
+
missing.push(at);
|
|
38
|
+
for (const directory of missing.reverse()) {
|
|
39
|
+
fs.mkdirSync(directory);
|
|
40
|
+
directories.push(directory);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function writeFile(root, file, content, snapshot, directories) {
|
|
45
|
+
const target = path.join(root, ...file.split("/"));
|
|
46
|
+
remember(snapshot, target);
|
|
47
|
+
ensureParent(root, target, directories);
|
|
48
|
+
fs.writeFileSync(target, content);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function restore(snapshot, directories) {
|
|
52
|
+
for (const [target, content] of [...snapshot.entries()].reverse()) {
|
|
53
|
+
if (content === null) {
|
|
54
|
+
if (fs.existsSync(target) && fs.statSync(target).isFile())
|
|
55
|
+
fs.unlinkSync(target);
|
|
56
|
+
} else {
|
|
57
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
58
|
+
fs.writeFileSync(target, content);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
for (const directory of [...directories].reverse()) {
|
|
62
|
+
try {
|
|
63
|
+
fs.rmdirSync(directory);
|
|
64
|
+
} catch {
|
|
65
|
+
/* It is non-empty or already absent: preserve it. */
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
package/upgrade/index.mjs
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// restored before the refusal is returned.
|
|
7
7
|
import fs from "node:fs";
|
|
8
8
|
import path from "node:path";
|
|
9
|
+
import { authoredAdditionReasons, remember, ensureParent, writeFile, restore } from "./file-state.mjs";
|
|
9
10
|
import { parseYaml } from "../runtime/engine/yaml.mjs";
|
|
10
11
|
import {
|
|
11
12
|
loadSlotManifest,
|
|
@@ -229,9 +230,9 @@ function additions(root, full, hadCli, manifest) {
|
|
|
229
230
|
const variables = { project: full.project, app: "cli", appPascal: "Cli" };
|
|
230
231
|
const files = [
|
|
231
232
|
...templateFiles("app/upgrade-full", "", variables),
|
|
232
|
-
...(
|
|
233
|
-
|
|
234
|
-
|
|
233
|
+
...scaffoldWorkSeeds(full.project).filter((file) =>
|
|
234
|
+
!fs.existsSync(path.join(root, file.path))
|
|
235
|
+
&& (file.path === ".starciwork/workspace.yaml" || !fs.existsSync(path.join(root, ".starciwork", "index.yaml")))),
|
|
235
236
|
...(!hadCli
|
|
236
237
|
? [
|
|
237
238
|
...STANDARD_CLI_FILES.map((file) => ({
|
|
@@ -270,8 +271,6 @@ function sourceWithAdditions(root, additions) {
|
|
|
270
271
|
};
|
|
271
272
|
}
|
|
272
273
|
|
|
273
|
-
// Files the app authors itself: the upgrade never overwrites them and keeps an existing one (CONTEXT: declarations are preserved).
|
|
274
|
-
const APP_OWNED = new Set([".starcistacks/application-stacks.yaml"]);
|
|
275
274
|
const publicStep = ({ op, path: file, why }) => ({ op, path: file, why });
|
|
276
275
|
|
|
277
276
|
async function model({ root, to, presets, manifest }) {
|
|
@@ -286,6 +285,7 @@ async function model({ root, to, presets, manifest }) {
|
|
|
286
285
|
);
|
|
287
286
|
const full = fullEditionDeclaration(declaration, manifest);
|
|
288
287
|
const desired = additions(root, full, hadCli, manifest);
|
|
288
|
+
const authored = authoredAdditionReasons(declaration, appSource(root));
|
|
289
289
|
const pins = parseYaml(fs.readFileSync(PINS_FILE, "utf8")).pins;
|
|
290
290
|
const upgradedPackage = pinnedPackage(root, pins);
|
|
291
291
|
const resolvedPresets = presets ?? UPGRADE_PRESETS;
|
|
@@ -357,8 +357,8 @@ async function model({ root, to, presets, manifest }) {
|
|
|
357
357
|
for (const file of desired) {
|
|
358
358
|
const target = path.join(root, ...file.path.split("/"));
|
|
359
359
|
if (fs.existsSync(target)) {
|
|
360
|
-
if (
|
|
361
|
-
steps.push({ op: "keep", path: file.path, why:
|
|
360
|
+
if (authored.has(file.path)) {
|
|
361
|
+
steps.push({ op: "keep", path: file.path, why: authored.get(file.path), kind: "keep" });
|
|
362
362
|
continue;
|
|
363
363
|
}
|
|
364
364
|
if (!sameText(root, file.path, file.content))
|
|
@@ -400,55 +400,6 @@ async function model({ root, to, presets, manifest }) {
|
|
|
400
400
|
return { full, managed: managedWithoutPackage, steps, dependenciesChanged };
|
|
401
401
|
}
|
|
402
402
|
|
|
403
|
-
function remember(snapshot, target) {
|
|
404
|
-
if (snapshot.has(target)) return;
|
|
405
|
-
snapshot.set(
|
|
406
|
-
target,
|
|
407
|
-
fs.existsSync(target) && fs.statSync(target).isFile()
|
|
408
|
-
? fs.readFileSync(target)
|
|
409
|
-
: null,
|
|
410
|
-
);
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
function ensureParent(root, target, directories) {
|
|
414
|
-
const missing = [];
|
|
415
|
-
for (
|
|
416
|
-
let at = path.dirname(target);
|
|
417
|
-
at.startsWith(root) && at !== root && !fs.existsSync(at);
|
|
418
|
-
at = path.dirname(at)
|
|
419
|
-
)
|
|
420
|
-
missing.push(at);
|
|
421
|
-
for (const directory of missing.reverse()) {
|
|
422
|
-
fs.mkdirSync(directory);
|
|
423
|
-
directories.push(directory);
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
function writeFile(root, file, content, snapshot, directories) {
|
|
428
|
-
const target = path.join(root, ...file.split("/"));
|
|
429
|
-
remember(snapshot, target);
|
|
430
|
-
ensureParent(root, target, directories);
|
|
431
|
-
fs.writeFileSync(target, content);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
function restore(snapshot, directories) {
|
|
435
|
-
for (const [target, content] of [...snapshot.entries()].reverse()) {
|
|
436
|
-
if (content === null) {
|
|
437
|
-
if (fs.existsSync(target) && fs.statSync(target).isFile())
|
|
438
|
-
fs.unlinkSync(target);
|
|
439
|
-
} else {
|
|
440
|
-
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
441
|
-
fs.writeFileSync(target, content);
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
for (const directory of [...directories].reverse()) {
|
|
445
|
-
try {
|
|
446
|
-
fs.rmdirSync(directory);
|
|
447
|
-
} catch {
|
|
448
|
-
/* It is non-empty or already absent: preserve it. */
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
403
|
|
|
453
404
|
/**
|
|
454
405
|
* Plans or applies the lite -> full migration. Returns the ordered public plan (`{op,path,why}[]`) in both modes.
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
// log.mjs — `git log <args>`: the commit history in a caller-chosen format.
|
|
2
|
-
import { runGit } from './lib.mjs';
|
|
3
|
-
|
|
4
|
-
/** The spawn result {status, stdout, stderr, error} of `git log ...args`; options are runGit's (cwd or dir, config, timeout, maxBuffer, input, encoding, env, git). */
|
|
5
|
-
export const log = (args = [], options = {}) => runGit(['log', ...args], options);
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
schema: work/layout-tree@1
|
|
2
|
-
id: shell
|
|
3
|
-
kind: shell
|
|
4
|
-
state: todo
|
|
5
|
-
rev: 1
|
|
6
|
-
origin: planned
|
|
7
|
-
productLocale:
|
|
8
|
-
default: vi
|
|
9
|
-
fallback: vi
|
|
10
|
-
locales:
|
|
11
|
-
- vi
|
|
12
|
-
breakpoints:
|
|
13
|
-
- name: desktop
|
|
14
|
-
width: 1440
|
|
15
|
-
height: 900
|
|
16
|
-
- name: mobile
|
|
17
|
-
width: 390
|
|
18
|
-
height: 844
|
|
19
|
-
themes:
|
|
20
|
-
- light
|
|
21
|
-
apps:
|
|
22
|
-
- name: web
|
|
23
|
-
root: fe/apps/web
|
|
24
|
-
appDir: fe/apps/web/src/app
|
|
25
|
-
framework: next-app-router
|
|
26
|
-
localeParam: locale
|
|
27
|
-
nodes:
|
|
28
|
-
- id: /
|
|
29
|
-
parent: null
|
|
30
|
-
segment: /
|
|
31
|
-
segmentKind: root
|
|
32
|
-
url: /
|
|
33
|
-
origin: planned
|
|
34
|
-
- id: /[locale]
|
|
35
|
-
parent: /
|
|
36
|
-
segment: "[locale]"
|
|
37
|
-
segmentKind: dynamic
|
|
38
|
-
url: /[locale]
|
|
39
|
-
origin: planned
|
|
40
|
-
files:
|
|
41
|
-
layout:
|
|
42
|
-
path: fe/apps/web/src/app/[locale]/layout.tsx
|
|
43
|
-
page:
|
|
44
|
-
path: fe/apps/web/src/app/[locale]/page.tsx
|
|
45
|
-
loading:
|
|
46
|
-
path: fe/apps/web/src/app/[locale]/loading.tsx
|
|
47
|
-
error:
|
|
48
|
-
path: fe/apps/web/src/app/[locale]/error.tsx
|
|
49
|
-
not-found:
|
|
50
|
-
path: fe/apps/web/src/app/[locale]/not-found.tsx
|
|
51
|
-
layout:
|
|
52
|
-
component: AppLayout
|
|
53
|
-
chrome: visible
|
|
54
|
-
state: todo
|
|
55
|
-
rev: 1
|