@cosmicdrift/kumiko-cli 0.387.0 → 0.389.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/package.json +11 -11
- package/src/changes.json +18 -0
- package/src/commands/agent.ts +6 -3
- package/src/commands/check-managed.ts +20 -2
- package/src/commands/check-scripts.ts +9 -1
- package/src/commands/handbuch.ts +34 -6
- package/src/commands/kms-wiring.ts +3 -1
- package/src/commands/load-app-config.ts +3 -6
- package/src/commands/ops/backfill-pii.ts +11 -9
- package/src/commands/ops/waitlist-migrate.ts +48 -16
- package/src/commands/sync-managed.ts +9 -5
- package/src/handbuch/buildinfo.ts +5 -6
- package/src/handbuch/uistrings.ts +4 -20
- package/src/index.ts +1 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.389.0",
|
|
4
4
|
"description": "CLI for Kumiko apps. `bunx @cosmicdrift/kumiko-cli new app <name>` scaffolds a runnable workspace; `add feature <name>` mounts a feature; `check` runs boot validation plus the guard suites with the step list derived from kumiko.json; `agent lint`, `project` and `consumer` operate on an existing app; `handbuch build` renders the HTML handbuch.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -36,18 +36,18 @@
|
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@cosmicdrift/kumiko-dev-server": "0.
|
|
40
|
-
"@cosmicdrift/kumiko-guards": "0.
|
|
41
|
-
"@cosmicdrift/kumiko-server-runtime": "0.
|
|
42
|
-
"@cosmicdrift/kumiko-testing": "0.
|
|
39
|
+
"@cosmicdrift/kumiko-dev-server": "0.389.0",
|
|
40
|
+
"@cosmicdrift/kumiko-guards": "0.389.0",
|
|
41
|
+
"@cosmicdrift/kumiko-server-runtime": "0.389.0",
|
|
42
|
+
"@cosmicdrift/kumiko-testing": "0.389.0",
|
|
43
43
|
"marked": "^18.0.3",
|
|
44
44
|
"zod": "^4.4.3"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"sharp": "^0.35.5",
|
|
48
|
-
"@cosmicdrift/kumiko-bundled-features": "0.
|
|
49
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
50
|
-
"@cosmicdrift/kumiko-renderer": "0.
|
|
48
|
+
"@cosmicdrift/kumiko-bundled-features": "0.389.0",
|
|
49
|
+
"@cosmicdrift/kumiko-framework": "0.389.0",
|
|
50
|
+
"@cosmicdrift/kumiko-renderer": "0.389.0"
|
|
51
51
|
},
|
|
52
52
|
"peerDependenciesMeta": {
|
|
53
53
|
"sharp": {
|
|
@@ -64,9 +64,9 @@
|
|
|
64
64
|
}
|
|
65
65
|
},
|
|
66
66
|
"devDependencies": {
|
|
67
|
-
"@cosmicdrift/kumiko-bundled-features": "0.
|
|
68
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
69
|
-
"@cosmicdrift/kumiko-renderer": "0.
|
|
67
|
+
"@cosmicdrift/kumiko-bundled-features": "0.389.0",
|
|
68
|
+
"@cosmicdrift/kumiko-framework": "0.389.0",
|
|
69
|
+
"@cosmicdrift/kumiko-renderer": "0.389.0",
|
|
70
70
|
"sharp": "^0.35.5"
|
|
71
71
|
},
|
|
72
72
|
"publishConfig": {
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.389.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "handbuch build no longer wipes its own inputs",
|
|
6
|
+
"detail": "The out directory is deleted before the build, so it is now rejected when it sits inside the configured repo, a flow source repo or an input directory, not only when it contains one."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.389.0",
|
|
10
|
+
"type": "fix",
|
|
11
|
+
"title": "waitlist-migrate encrypts imported personal data or aborts",
|
|
12
|
+
"detail": "waitlist-migrate wires the PII KMS and blind-index key from the environment before importing. Without them it aborts with a clear message; --allow-plaintext-pii <reason> opts out for apps without subject encryption."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"version": "0.388.0",
|
|
16
|
+
"type": "fix",
|
|
17
|
+
"title": "check, sync-managed and backfill-pii handle failure and subdirectories correctly",
|
|
18
|
+
"detail": "kumiko check only warns about unresolvable managed files or package.json scripts until kumiko.config.ts calls defineAppConfig. sync-managed resolves the repo root like check instead of using cwd. ops backfill-pii closes the KMS wiring when registry creation fails. The agent command reuses allAppFeatures from the framework config."
|
|
19
|
+
},
|
|
2
20
|
{
|
|
3
21
|
"version": "0.379.0",
|
|
4
22
|
"type": "improvement",
|
package/src/commands/agent.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
-
import {
|
|
2
|
+
import { allAppFeatures } from "@cosmicdrift/kumiko-framework/config";
|
|
3
|
+
import { loadAppConfig } from "./load-app-config";
|
|
3
4
|
import type { CliCommand } from "./types";
|
|
4
5
|
|
|
5
6
|
const LINT_SUBCOMMAND = "lint";
|
|
@@ -26,7 +27,9 @@ export const agentCommand: CliCommand = {
|
|
|
26
27
|
ctx.out.err(" // kumiko.config.ts");
|
|
27
28
|
ctx.out.err(' import { defineAppConfig } from "@cosmicdrift/kumiko-framework/config";');
|
|
28
29
|
ctx.out.err(' import { myFeature } from "./src/features/my-feature";');
|
|
29
|
-
ctx.out.err(
|
|
30
|
+
ctx.out.err(
|
|
31
|
+
' export default defineAppConfig({ features: [myFeature], client: { entries: ["src/client.tsx"] }, ports: { dev: 4173, e2e: 4174 } });',
|
|
32
|
+
);
|
|
30
33
|
ctx.out.err("");
|
|
31
34
|
return 1;
|
|
32
35
|
}
|
|
@@ -37,7 +40,7 @@ export const agentCommand: CliCommand = {
|
|
|
37
40
|
"@cosmicdrift/kumiko-bundled-features/agent-tools"
|
|
38
41
|
);
|
|
39
42
|
|
|
40
|
-
const features =
|
|
43
|
+
const features = allAppFeatures(config);
|
|
41
44
|
const gaps = findAgentDocGaps(features);
|
|
42
45
|
const translationGaps = findHandlerTranslationGaps(features);
|
|
43
46
|
|
|
@@ -35,8 +35,26 @@ export async function runManagedCheckStep(
|
|
|
35
35
|
uiRoots: readonly string[],
|
|
36
36
|
): Promise<number> {
|
|
37
37
|
const managed: ManagedFilesModule = await import("@cosmicdrift/kumiko-dev-server/managed-files");
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
let result: ReturnType<ManagedFilesModule["checkManagedFiles"]>;
|
|
39
|
+
try {
|
|
40
|
+
result = managed.checkManagedFiles(
|
|
41
|
+
managed.resolveManagedAppFacts({ appRoot: repoRoot, uiRoots }),
|
|
42
|
+
);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
// Before defineAppConfig is adopted the managed step only warns, also when the facts cannot be resolved.
|
|
45
|
+
if (managed.readStaticAppConfig(repoRoot).kind === "defined") throw error;
|
|
46
|
+
out.log(` ! ${error instanceof Error ? error.message : String(error)}`);
|
|
47
|
+
out.log(` ${NOT_ADOPTED_NOTE}`);
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
50
|
+
return reportManagedCheck(out, managed, result);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function reportManagedCheck(
|
|
54
|
+
out: Output,
|
|
55
|
+
managed: ManagedFilesModule,
|
|
56
|
+
result: ReturnType<ManagedFilesModule["checkManagedFiles"]>,
|
|
57
|
+
): number {
|
|
40
58
|
const drift = result.files.filter(managed.isManagedDrift);
|
|
41
59
|
const local = result.files.filter((report) => report.state === "local");
|
|
42
60
|
const okCount = result.files.length - drift.length - local.length;
|
|
@@ -28,8 +28,16 @@ export async function runScriptsCheckStep(
|
|
|
28
28
|
uiRoots: readonly string[],
|
|
29
29
|
): Promise<number> {
|
|
30
30
|
const managed: ManagedFilesModule = await import("@cosmicdrift/kumiko-dev-server/managed-files");
|
|
31
|
-
const report = managed.checkAppScripts(readScripts(repoRoot), { hasUi: uiRoots.length > 0 });
|
|
32
31
|
const adopted = managed.readStaticAppConfig(repoRoot).kind === "defined";
|
|
32
|
+
let scripts: Record<string, string>;
|
|
33
|
+
try {
|
|
34
|
+
scripts = readScripts(repoRoot);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if (adopted) throw error;
|
|
37
|
+
out.log(` ! ${error instanceof Error ? error.message : String(error)}`);
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
const report = managed.checkAppScripts(scripts, { hasUi: uiRoots.length > 0 });
|
|
33
41
|
if (report.knownExtras.length > 0)
|
|
34
42
|
out.log(` extras (standard): ${report.knownExtras.join(", ")}`);
|
|
35
43
|
if (report.appExtras.length > 0) out.log(` extras (app): ${report.appExtras.join(", ")}`);
|
package/src/commands/handbuch.ts
CHANGED
|
@@ -46,6 +46,33 @@ function containsPath(parentDir: string, candidate: string): boolean {
|
|
|
46
46
|
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
type OutDirContext = {
|
|
50
|
+
readonly outDir: string;
|
|
51
|
+
readonly rootDir: string;
|
|
52
|
+
readonly cwd: string;
|
|
53
|
+
readonly sourceRepoDirs: readonly string[];
|
|
54
|
+
readonly inputDirs: readonly string[];
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
function wipesBuildInputs({
|
|
58
|
+
outDir,
|
|
59
|
+
rootDir,
|
|
60
|
+
cwd,
|
|
61
|
+
sourceRepoDirs,
|
|
62
|
+
inputDirs,
|
|
63
|
+
}: OutDirContext): boolean {
|
|
64
|
+
const containsOutDir = (dir: string) => containsPath(dir, outDir);
|
|
65
|
+
const wipesProtectedDir = [cwd, rootDir, ...sourceRepoDirs, ...inputDirs].some((dir) =>
|
|
66
|
+
containsPath(outDir, dir),
|
|
67
|
+
);
|
|
68
|
+
// The default out dir lives under --root, which a "." repo legitimately is.
|
|
69
|
+
const outIsDefaultLocation = containsPath(join(rootDir, ".handbuch"), outDir);
|
|
70
|
+
const lieInsideSource =
|
|
71
|
+
inputDirs.some(containsOutDir) ||
|
|
72
|
+
(!outIsDefaultLocation && sourceRepoDirs.some(containsOutDir));
|
|
73
|
+
return wipesProtectedDir || lieInsideSource;
|
|
74
|
+
}
|
|
75
|
+
|
|
49
76
|
async function runBuild(ctx: CliCommandContext, args: ReadonlyArray<string>): Promise<number> {
|
|
50
77
|
let parsed: ReturnType<
|
|
51
78
|
typeof parseArgs<{ options: typeof BUILD_OPTIONS; allowPositionals: true }>
|
|
@@ -90,20 +117,21 @@ async function runBuild(ctx: CliCommandContext, args: ReadonlyArray<string>): Pr
|
|
|
90
117
|
// buildHandbuch wipes outDir recursively, so a mistyped --out must never be
|
|
91
118
|
// (or contain) a directory the build reads from or the user works in.
|
|
92
119
|
const outDir = resolve(ctx.cwd, values.out ?? join(rootDir, ".handbuch", appSlug(configPath)));
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
120
|
+
const optionalDir = (value: string | undefined) => (value ? resolve(ctx.cwd, value) : undefined);
|
|
121
|
+
const inputDirs = [values.texts, values.shots, values["web-texts"], values["web-shots"]].flatMap(
|
|
122
|
+
(value) => optionalDir(value) ?? [],
|
|
123
|
+
);
|
|
124
|
+
const sourceRepoDirs = [
|
|
96
125
|
resolve(rootDir, config.repo),
|
|
97
126
|
...(config.flowSources ?? []).map((source) => resolve(rootDir, source.repo)),
|
|
98
127
|
];
|
|
99
|
-
if (
|
|
128
|
+
if (wipesBuildInputs({ outDir, rootDir, cwd: ctx.cwd, sourceRepoDirs, inputDirs })) {
|
|
100
129
|
return usageError(
|
|
101
130
|
ctx,
|
|
102
|
-
`Refusing to build into ${outDir}: it is or
|
|
131
|
+
`Refusing to build into ${outDir}: it is, contains or lies inside the working directory, --root, a source repo or an input directory, and is deleted before the build.`,
|
|
103
132
|
);
|
|
104
133
|
}
|
|
105
134
|
|
|
106
|
-
const optionalDir = (value: string | undefined) => (value ? resolve(ctx.cwd, value) : undefined);
|
|
107
135
|
try {
|
|
108
136
|
const result = await buildHandbuch(config, {
|
|
109
137
|
rootDir,
|
|
@@ -17,6 +17,8 @@ export async function wireKmsFromEnv(options: {
|
|
|
17
17
|
readonly log: (line: string) => void;
|
|
18
18
|
readonly features: readonly FeatureDefinition[];
|
|
19
19
|
readonly allowPlaintextPii?: string | undefined;
|
|
20
|
+
/** What goes wrong without the wiring; completes the abort message. */
|
|
21
|
+
readonly consequence?: string;
|
|
20
22
|
}): Promise<KmsWiring> {
|
|
21
23
|
const crypto = await import("@cosmicdrift/kumiko-framework/crypto");
|
|
22
24
|
const { assertPiiBootInvariants } = await import(
|
|
@@ -46,7 +48,7 @@ export async function wireKmsFromEnv(options: {
|
|
|
46
48
|
await wiring.close();
|
|
47
49
|
const reason = e instanceof Error ? e.message : String(e);
|
|
48
50
|
throw new Error(
|
|
49
|
-
`${options.logPrefix} ${reason} The rebuild would null the blind-index columns. Provide PLATFORM_KEK, SUBJECT_KEYS_DATABASE_URL and KUMIKO_BLIND_INDEX_KEY, or pass --allow-plaintext-pii <reason>.`,
|
|
51
|
+
`${options.logPrefix} ${reason} ${options.consequence ?? "The rebuild would null the blind-index columns."} Provide PLATFORM_KEK, SUBJECT_KEYS_DATABASE_URL and KUMIKO_BLIND_INDEX_KEY, or pass --allow-plaintext-pii <reason>.`,
|
|
50
52
|
);
|
|
51
53
|
}
|
|
52
54
|
return wiring;
|
|
@@ -20,11 +20,6 @@ function isAppConfig(value: unknown): value is AppConfig {
|
|
|
20
20
|
);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
/** Everything prod mounts: lint and docs checks must see the prod-only features too. */
|
|
24
|
-
export function allConfiguredFeatures(config: AppConfig): readonly FeatureDefinition[] {
|
|
25
|
-
return [...config.features, ...(config.prodFeatures ?? [])];
|
|
26
|
-
}
|
|
27
|
-
|
|
28
23
|
// kumiko.config.ts is user-authored: a missing default export or `features`
|
|
29
24
|
// array must produce a readable message, not a TypeError further down.
|
|
30
25
|
export async function loadAppConfig(
|
|
@@ -38,7 +33,9 @@ export async function loadAppConfig(
|
|
|
38
33
|
ctx.out.err("");
|
|
39
34
|
ctx.out.err(` ${configPath} must default-export an object with a "features" array:`);
|
|
40
35
|
ctx.out.err(' import { defineAppConfig } from "@cosmicdrift/kumiko-framework/config";');
|
|
41
|
-
ctx.out.err(
|
|
36
|
+
ctx.out.err(
|
|
37
|
+
' export default defineAppConfig({ features: [myFeature], client: { entries: ["src/client.tsx"] }, ports: { dev: 4173, e2e: 4174 } });',
|
|
38
|
+
);
|
|
42
39
|
ctx.out.err("");
|
|
43
40
|
return null;
|
|
44
41
|
}
|
|
@@ -219,20 +219,22 @@ export async function runBackfillPii(ctx: CliCommandContext): Promise<number> {
|
|
|
219
219
|
ctx.out.err(errorMessage(e));
|
|
220
220
|
return 1;
|
|
221
221
|
}
|
|
222
|
-
crypto.configurePiiSubjectKms(wiring.kms);
|
|
223
|
-
crypto.configureBlindIndexKey(wiring.blindIndexKey);
|
|
224
|
-
|
|
225
|
-
const registry = createRegistry(config.features);
|
|
226
|
-
const { db, close } = createDbConnection({ url: databaseUrl, maxConnections: 4 });
|
|
227
222
|
try {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
223
|
+
crypto.configurePiiSubjectKms(wiring.kms);
|
|
224
|
+
crypto.configureBlindIndexKey(wiring.blindIndexKey);
|
|
225
|
+
const registry = createRegistry(config.features);
|
|
226
|
+
const { db, close } = createDbConnection({ url: databaseUrl, maxConnections: 4 });
|
|
227
|
+
try {
|
|
228
|
+
return args.mode === "rebuild"
|
|
229
|
+
? await rebuildPiiProjections(ctx, db, registry, crypto)
|
|
230
|
+
: await backfillPii(ctx, db, registry, args);
|
|
231
|
+
} finally {
|
|
232
|
+
await close();
|
|
233
|
+
}
|
|
231
234
|
} catch (e) {
|
|
232
235
|
ctx.out.err(`${LOG_PREFIX} ${errorMessage(e)}`);
|
|
233
236
|
return 1;
|
|
234
237
|
} finally {
|
|
235
|
-
await close();
|
|
236
238
|
// The KMS pool keeps the process alive otherwise.
|
|
237
239
|
await wiring.close();
|
|
238
240
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
+
import { wireKmsFromEnv } from "../kms-wiring";
|
|
2
3
|
import { loadAppConfig } from "../load-app-config";
|
|
3
4
|
import type { CliCommandContext } from "../types";
|
|
4
5
|
|
|
5
6
|
const USAGE =
|
|
6
|
-
"Usage: kumiko ops waitlist-migrate --from <offlot|publicstatus> [--dry-run] [--source-table <name>] [--locale <tag>]";
|
|
7
|
+
"Usage: kumiko ops waitlist-migrate --from <offlot|publicstatus> [--dry-run] [--source-table <name>] [--locale <tag>] [--allow-plaintext-pii <reason>]";
|
|
7
8
|
|
|
8
9
|
function flagValue(argv: readonly string[], flag: string): string | undefined {
|
|
9
10
|
const index = argv.indexOf(flag);
|
|
@@ -17,6 +18,35 @@ function fail(ctx: CliCommandContext, ...lines: readonly string[]): number {
|
|
|
17
18
|
return 1;
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
type AppConfig = NonNullable<Awaited<ReturnType<typeof loadAppConfig>>>;
|
|
22
|
+
type Preflight =
|
|
23
|
+
| { readonly ok: true; readonly databaseUrl: string; readonly config: AppConfig }
|
|
24
|
+
| { readonly ok: false; readonly exitCode: number };
|
|
25
|
+
|
|
26
|
+
async function preflight(ctx: CliCommandContext): Promise<Preflight> {
|
|
27
|
+
const configPath = join(ctx.cwd, "kumiko.config.ts");
|
|
28
|
+
if (!(await Bun.file(configPath).exists())) {
|
|
29
|
+
return { ok: false, exitCode: fail(ctx, `kumiko.config.ts not found at: ${configPath}`) };
|
|
30
|
+
}
|
|
31
|
+
const databaseUrl = process.env["DATABASE_URL"];
|
|
32
|
+
if (!databaseUrl) return { ok: false, exitCode: fail(ctx, "DATABASE_URL not set.") };
|
|
33
|
+
const config = await loadAppConfig(ctx, configPath);
|
|
34
|
+
if (config === null) return { ok: false, exitCode: 1 };
|
|
35
|
+
if (!config.features.some((feature) => feature.name === "waitlist")) {
|
|
36
|
+
const message =
|
|
37
|
+
"The app config does not mount the framework `waitlist` feature; add createWaitlistFeature() first.";
|
|
38
|
+
return { ok: false, exitCode: fail(ctx, message) };
|
|
39
|
+
}
|
|
40
|
+
return { ok: true, databaseUrl, config };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function plaintextPiiReasonIsMissing(ctx: CliCommandContext): boolean {
|
|
44
|
+
return (
|
|
45
|
+
ctx.argv.includes("--allow-plaintext-pii") &&
|
|
46
|
+
!flagValue(ctx.argv, "--allow-plaintext-pii")?.trim()
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
20
50
|
export const waitlistMigrateOp = {
|
|
21
51
|
id: "waitlist-migrate",
|
|
22
52
|
description: "Copy a legacy app waitlist into the framework waitlist entity (idempotent)",
|
|
@@ -30,25 +60,26 @@ export const waitlistMigrateOp = {
|
|
|
30
60
|
const from = flagValue(ctx.argv, "--from");
|
|
31
61
|
if (!isLegacyWaitlistSource(from)) return fail(ctx, USAGE);
|
|
32
62
|
|
|
33
|
-
const
|
|
34
|
-
if (!
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const databaseUrl = process.env["DATABASE_URL"];
|
|
38
|
-
if (!databaseUrl) return fail(ctx, "DATABASE_URL not set.");
|
|
39
|
-
const config = await loadAppConfig(ctx, configPath);
|
|
40
|
-
if (config === null) return 1;
|
|
41
|
-
if (!config.features.some((feature) => feature.name === "waitlist")) {
|
|
42
|
-
return fail(
|
|
43
|
-
ctx,
|
|
44
|
-
"The app config does not mount the framework `waitlist` feature; add createWaitlistFeature() first.",
|
|
45
|
-
);
|
|
63
|
+
const checked = await preflight(ctx);
|
|
64
|
+
if (!checked.ok) return checked.exitCode;
|
|
65
|
+
if (plaintextPiiReasonIsMissing(ctx)) {
|
|
66
|
+
return fail(ctx, "--allow-plaintext-pii requires a non-empty reason");
|
|
46
67
|
}
|
|
47
68
|
|
|
48
69
|
const dryRun = ctx.argv.includes("--dry-run");
|
|
49
70
|
const { createDbConnection } = await import("@cosmicdrift/kumiko-framework/db");
|
|
50
|
-
const { db, close } = createDbConnection({ url: databaseUrl });
|
|
71
|
+
const { db, close } = createDbConnection({ url: checked.databaseUrl });
|
|
72
|
+
let wiring: Awaited<ReturnType<typeof wireKmsFromEnv>> | undefined;
|
|
51
73
|
try {
|
|
74
|
+
// Without the KMS the executor would write name/email/message as plaintext into the
|
|
75
|
+
// event log, where crypto-shredding cannot reach them.
|
|
76
|
+
wiring = await wireKmsFromEnv({
|
|
77
|
+
logPrefix: "[kumiko ops waitlist-migrate]",
|
|
78
|
+
log: (line) => ctx.out.log(line),
|
|
79
|
+
features: checked.config.features,
|
|
80
|
+
allowPlaintextPii: flagValue(ctx.argv, "--allow-plaintext-pii"),
|
|
81
|
+
consequence: "The import would store personal data as plaintext in the event log.",
|
|
82
|
+
});
|
|
52
83
|
const report = await migrateLegacyWaitlist(db, {
|
|
53
84
|
from,
|
|
54
85
|
dryRun,
|
|
@@ -57,13 +88,14 @@ export const waitlistMigrateOp = {
|
|
|
57
88
|
});
|
|
58
89
|
ctx.out.log("");
|
|
59
90
|
ctx.out.log(
|
|
60
|
-
` ${dryRun ? "[dry-run] " : ""}waitlist-migrate --from ${from}: read ${report.read}, ${dryRun ? "would create" : "created"} ${report.created}, already present ${report.skipped}, unreadable ${report.unreadable}, legacy streams archived ${report.archivedStreams}`,
|
|
91
|
+
` ${dryRun ? "[dry-run] " : ""}waitlist-migrate --from ${from}: read ${report.read}, ${dryRun ? "would create" : "created"} ${report.created}, already present ${report.skipped}, unreadable ${report.unreadable}, legacy streams ${dryRun ? "would archive" : "archived"} ${report.archivedStreams}`,
|
|
61
92
|
);
|
|
62
93
|
ctx.out.log("");
|
|
63
94
|
return report.unreadable > 0 ? 1 : 0;
|
|
64
95
|
} catch (e) {
|
|
65
96
|
return fail(ctx, `✗ ${e instanceof Error ? e.message : String(e)}`);
|
|
66
97
|
} finally {
|
|
98
|
+
await wiring?.close();
|
|
67
99
|
await close();
|
|
68
100
|
}
|
|
69
101
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { REPO_MANIFEST_FILE } from "../repo-manifest";
|
|
2
2
|
import type { CliCommand, CliCommandContext } from "./types";
|
|
3
3
|
|
|
4
4
|
const SYNC_FLAGS = ["--write", "--adopt"] as const;
|
|
@@ -11,9 +11,13 @@ export async function runSyncManaged(ctx: CliCommandContext): Promise<number> {
|
|
|
11
11
|
);
|
|
12
12
|
return 1;
|
|
13
13
|
}
|
|
14
|
-
const {
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
const { findLocalRepo } = await import("@cosmicdrift/kumiko-guards");
|
|
15
|
+
const repo = findLocalRepo(ctx.cwd);
|
|
16
|
+
if (repo === undefined) {
|
|
17
|
+
ctx.out.err(` No Kumiko repo above ${ctx.cwd}. Add a ${REPO_MANIFEST_FILE} at the repo root.`);
|
|
18
|
+
return 1;
|
|
19
|
+
}
|
|
20
|
+
const { manifest } = repo;
|
|
17
21
|
if (manifest.kind !== "app") {
|
|
18
22
|
ctx.out.err(
|
|
19
23
|
` sync-managed needs kind "app" in ${REPO_MANIFEST_FILE}, got "${manifest.kind}".`,
|
|
@@ -23,7 +27,7 @@ export async function runSyncManaged(ctx: CliCommandContext): Promise<number> {
|
|
|
23
27
|
const write = ctx.argv.includes("--write");
|
|
24
28
|
const managed = await import("@cosmicdrift/kumiko-dev-server/managed-files");
|
|
25
29
|
const facts = managed.resolveManagedAppFacts({
|
|
26
|
-
appRoot:
|
|
30
|
+
appRoot: repo.absPath,
|
|
27
31
|
uiRoots: manifest.uiRoots ?? [],
|
|
28
32
|
});
|
|
29
33
|
for (const error of managed.listManagedErrors(facts)) ctx.out.err(` ✗ ${error}`);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { basename, join } from "node:path";
|
|
4
|
+
import { findInstalledPackageDir } from "@cosmicdrift/kumiko-server-runtime/resolve-installed-package";
|
|
4
5
|
|
|
5
6
|
const FRAMEWORK_PACKAGE = "@cosmicdrift/kumiko-framework";
|
|
6
7
|
|
|
@@ -31,12 +32,10 @@ function stringField(record: Record<string, unknown> | undefined, key: string):
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
function resolvedFrameworkVersion(repoDir: string): string | undefined {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return undefined;
|
|
39
|
-
}
|
|
35
|
+
const packageDir = findInstalledPackageDir(FRAMEWORK_PACKAGE, repoDir);
|
|
36
|
+
return packageDir === undefined
|
|
37
|
+
? undefined
|
|
38
|
+
: stringField(readJson(join(packageDir, "package.json")), "version");
|
|
40
39
|
}
|
|
41
40
|
|
|
42
41
|
function gitOutput(repoDir: string, args: string[]): string | undefined {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { findInstalledPackageDir } from "@cosmicdrift/kumiko-server-runtime/resolve-installed-package";
|
|
3
4
|
|
|
4
5
|
export type UiStringSources = {
|
|
5
6
|
repoDir: string;
|
|
@@ -183,23 +184,6 @@ export function findMissingUiTerms(
|
|
|
183
184
|
);
|
|
184
185
|
}
|
|
185
186
|
|
|
186
|
-
function packageDirectory(packageName: string, fromDir: string): string | undefined {
|
|
187
|
-
try {
|
|
188
|
-
// Bare-name resolution fails for packages without a "." export (bundled-features).
|
|
189
|
-
let current = dirname(Bun.resolveSync(`${packageName}/package.json`, fromDir));
|
|
190
|
-
while (current !== dirname(current)) {
|
|
191
|
-
const manifest = join(current, "package.json");
|
|
192
|
-
if (existsSync(manifest) && JSON.parse(readFileSync(manifest, "utf8")).name === packageName) {
|
|
193
|
-
return current;
|
|
194
|
-
}
|
|
195
|
-
current = dirname(current);
|
|
196
|
-
}
|
|
197
|
-
} catch {
|
|
198
|
-
return undefined;
|
|
199
|
-
}
|
|
200
|
-
return undefined;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
187
|
function scanFiles(cwd: string, patterns: readonly string[]): string[] {
|
|
204
188
|
const files = new Set<string>();
|
|
205
189
|
for (const pattern of patterns) {
|
|
@@ -273,7 +257,7 @@ export function collectUiStrings(sources: UiStringSources): UiStringMap {
|
|
|
273
257
|
};
|
|
274
258
|
absorb(scanFiles(sources.repoDir, sources.globs));
|
|
275
259
|
for (const entry of sources.packages) {
|
|
276
|
-
const directory =
|
|
260
|
+
const directory = findInstalledPackageDir(entry.package, sources.repoDir);
|
|
277
261
|
if (directory) absorb(scanFiles(directory, entry.files), entry.locale);
|
|
278
262
|
}
|
|
279
263
|
return merged;
|
package/src/index.ts
CHANGED
|
@@ -9,11 +9,7 @@
|
|
|
9
9
|
// `consumer`). Workspace-only maintainer commands (ci:guards, dev, build, …)
|
|
10
10
|
// stay in the framework repo's bin/kumiko.ts.
|
|
11
11
|
|
|
12
|
-
import {
|
|
13
|
-
resolveFrameworkVersion,
|
|
14
|
-
scaffoldApp,
|
|
15
|
-
scaffoldAppFeature,
|
|
16
|
-
} from "@cosmicdrift/kumiko-dev-server";
|
|
12
|
+
import { scaffoldApp, scaffoldAppFeature } from "@cosmicdrift/kumiko-dev-server";
|
|
17
13
|
import { renderTestSetup } from "@cosmicdrift/kumiko-testing/scaffold";
|
|
18
14
|
import type { CliCommand } from "./commands";
|
|
19
15
|
import { APP_COMMANDS, findAppCommand } from "./commands";
|
|
@@ -93,12 +89,10 @@ async function runNew(args: readonly string[], out: Output, cwd: string): Promis
|
|
|
93
89
|
return 1;
|
|
94
90
|
}
|
|
95
91
|
try {
|
|
96
|
-
const frameworkVersion = resolveFrameworkVersion();
|
|
97
92
|
const result = await scaffoldApp({
|
|
98
93
|
name,
|
|
99
94
|
destination: `${cwd}/${name}`,
|
|
100
95
|
testSetup: renderTestSetup,
|
|
101
|
-
...(frameworkVersion !== undefined && { frameworkVersion }),
|
|
102
96
|
});
|
|
103
97
|
out.log(`✓ Scaffolded ${result.appName} → ${result.destination}`);
|
|
104
98
|
out.log("");
|