@cosmicdrift/kumiko-cli 0.388.0 → 0.390.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-cli",
3
- "version": "0.388.0",
3
+ "version": "0.390.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,19 @@
36
36
  }
37
37
  },
38
38
  "dependencies": {
39
- "@cosmicdrift/kumiko-dev-server": "0.388.0",
40
- "@cosmicdrift/kumiko-guards": "0.388.0",
41
- "@cosmicdrift/kumiko-server-runtime": "0.388.0",
42
- "@cosmicdrift/kumiko-testing": "0.388.0",
39
+ "@cosmicdrift/kumiko-dev-server": "0.390.0",
40
+ "@cosmicdrift/kumiko-feature-ast": "0.390.0",
41
+ "@cosmicdrift/kumiko-guards": "0.390.0",
42
+ "@cosmicdrift/kumiko-server-runtime": "0.390.0",
43
+ "@cosmicdrift/kumiko-testing": "0.390.0",
43
44
  "marked": "^18.0.3",
44
45
  "zod": "^4.4.3"
45
46
  },
46
47
  "peerDependencies": {
47
48
  "sharp": "^0.35.5",
48
- "@cosmicdrift/kumiko-bundled-features": "0.388.0",
49
- "@cosmicdrift/kumiko-framework": "0.388.0",
50
- "@cosmicdrift/kumiko-renderer": "0.388.0"
49
+ "@cosmicdrift/kumiko-bundled-features": "0.390.0",
50
+ "@cosmicdrift/kumiko-framework": "0.390.0",
51
+ "@cosmicdrift/kumiko-renderer": "0.390.0"
51
52
  },
52
53
  "peerDependenciesMeta": {
53
54
  "sharp": {
@@ -64,9 +65,9 @@
64
65
  }
65
66
  },
66
67
  "devDependencies": {
67
- "@cosmicdrift/kumiko-bundled-features": "0.388.0",
68
- "@cosmicdrift/kumiko-framework": "0.388.0",
69
- "@cosmicdrift/kumiko-renderer": "0.388.0",
68
+ "@cosmicdrift/kumiko-bundled-features": "0.390.0",
69
+ "@cosmicdrift/kumiko-framework": "0.390.0",
70
+ "@cosmicdrift/kumiko-renderer": "0.390.0",
70
71
  "sharp": "^0.35.5"
71
72
  },
72
73
  "publishConfig": {
package/src/changes.json CHANGED
@@ -1,4 +1,16 @@
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
+ },
2
14
  {
3
15
  "version": "0.388.0",
4
16
  "type": "fix",
@@ -39,13 +39,13 @@ async function runCodemod(ctx: CliCommandContext): Promise<number> {
39
39
 
40
40
  const { resolveCodemodScript } = await import("@cosmicdrift/kumiko-framework/upgrade-cli");
41
41
  const scriptField = `scripts/codemod/${name}.ts`;
42
- // The project's own install wins; the framework next to this CLI is the fallback.
42
+ // The project's own install wins; the feature-ast package next to this CLI is the fallback.
43
43
  const script =
44
44
  resolveCodemodScript(ctx.cwd, scriptField) ??
45
45
  resolveCodemodScript(import.meta.dir, scriptField);
46
46
  if (script === null) {
47
47
  ctx.out.err(
48
- " Codemod script not found. Install @cosmicdrift/kumiko-framework in this project.",
48
+ " Codemod script not found. Install @cosmicdrift/kumiko-feature-ast in this project.",
49
49
  );
50
50
  return 1;
51
51
  }
@@ -62,30 +62,44 @@ export const consumerCommand: CliCommand = {
62
62
  SEARCH_CONSUMER_NAME,
63
63
  ...registry.getAllMultiStreamProjections().keys(),
64
64
  ];
65
+ const { runInSystemRowLevelSecurityScope } = await import(
66
+ "@cosmicdrift/kumiko-framework/internal/row-level-security"
67
+ );
65
68
  const { db, close } = createDbConnection({ url: databaseUrl });
66
69
 
67
70
  try {
68
71
  await createEventConsumerStateTable(db);
69
- switch (sub) {
70
- case "list":
71
- return await listConsumers(ctx, db, registeredConsumerNames, listConsumersWithState);
72
- case "status":
73
- return await showConsumerStatus(ctx, db, arg, registeredConsumerNames, getConsumerState);
74
- case "restart":
75
- return await transitionOne(ctx, "restart", arg, "restarted", (name) =>
76
- restartConsumer(db, name),
77
- );
78
- case "disable":
79
- return await transitionOne(ctx, "disable", arg, "disabled", (name) =>
80
- disableConsumer(db, name),
81
- );
82
- case "enable":
83
- return await transitionOne(ctx, "enable", arg, "enabled", (name) =>
84
- enableConsumer(db, name),
85
- );
86
- case "skip":
87
- return await skipOne(ctx, db, arg, skipPoisonEvent);
88
- }
72
+ return await runInSystemRowLevelSecurityScope(
73
+ "operator consumer tool skips poison events, which reads the events of every tenant",
74
+ async () => {
75
+ switch (sub) {
76
+ case "list":
77
+ return await listConsumers(ctx, db, registeredConsumerNames, listConsumersWithState);
78
+ case "status":
79
+ return await showConsumerStatus(
80
+ ctx,
81
+ db,
82
+ arg,
83
+ registeredConsumerNames,
84
+ getConsumerState,
85
+ );
86
+ case "restart":
87
+ return await transitionOne(ctx, "restart", arg, "restarted", (name) =>
88
+ restartConsumer(db, name),
89
+ );
90
+ case "disable":
91
+ return await transitionOne(ctx, "disable", arg, "disabled", (name) =>
92
+ disableConsumer(db, name),
93
+ );
94
+ case "enable":
95
+ return await transitionOne(ctx, "enable", arg, "enabled", (name) =>
96
+ enableConsumer(db, name),
97
+ );
98
+ case "skip":
99
+ return await skipOne(ctx, db, arg, skipPoisonEvent);
100
+ }
101
+ },
102
+ );
89
103
  } catch (e) {
90
104
  ctx.out.err("");
91
105
  ctx.out.err(` ✗ ${e instanceof Error ? e.message : String(e)}`);
@@ -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 protectedDirs = [
94
- ctx.cwd,
95
- rootDir,
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 (protectedDirs.some((dir) => containsPath(outDir, dir))) {
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 contains the working directory, --root or a source repo, and is deleted before the build.`,
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;
@@ -223,11 +223,18 @@ export async function runBackfillPii(ctx: CliCommandContext): Promise<number> {
223
223
  crypto.configurePiiSubjectKms(wiring.kms);
224
224
  crypto.configureBlindIndexKey(wiring.blindIndexKey);
225
225
  const registry = createRegistry(config.features);
226
+ const { runInSystemRowLevelSecurityScope } = await import(
227
+ "@cosmicdrift/kumiko-framework/internal/row-level-security"
228
+ );
226
229
  const { db, close } = createDbConnection({ url: databaseUrl, maxConnections: 4 });
227
230
  try {
228
- return args.mode === "rebuild"
229
- ? await rebuildPiiProjections(ctx, db, registry, crypto)
230
- : await backfillPii(ctx, db, registry, args);
231
+ return await runInSystemRowLevelSecurityScope(
232
+ "operator PII backfill rewrites events and projections of every tenant",
233
+ () =>
234
+ args.mode === "rebuild"
235
+ ? rebuildPiiProjections(ctx, db, registry, crypto)
236
+ : backfillPii(ctx, db, registry, args),
237
+ );
231
238
  } finally {
232
239
  await close();
233
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,31 +60,39 @@ export const waitlistMigrateOp = {
30
60
  const from = flagValue(ctx.argv, "--from");
31
61
  if (!isLegacyWaitlistSource(from)) return fail(ctx, USAGE);
32
62
 
33
- const configPath = join(ctx.cwd, "kumiko.config.ts");
34
- if (!(await Bun.file(configPath).exists())) {
35
- return fail(ctx, `kumiko.config.ts not found at: ${configPath}`);
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 { runInSystemRowLevelSecurityScope } = await import(
72
+ "@cosmicdrift/kumiko-framework/internal/row-level-security"
73
+ );
74
+ const { db, close } = createDbConnection({ url: checked.databaseUrl });
75
+ let wiring: Awaited<ReturnType<typeof wireKmsFromEnv>> | undefined;
51
76
  try {
52
- const report = await migrateLegacyWaitlist(db, {
53
- from,
54
- dryRun,
55
- sourceTable: flagValue(ctx.argv, "--source-table") ?? DEFAULT_LEGACY_SOURCE_TABLE[from],
56
- defaultLocale: flagValue(ctx.argv, "--locale") ?? DEFAULT_IMPORT_LOCALE,
77
+ // Without the KMS the executor would write name/email/message as plaintext into the
78
+ // event log, where crypto-shredding cannot reach them.
79
+ wiring = await wireKmsFromEnv({
80
+ logPrefix: "[kumiko ops waitlist-migrate]",
81
+ log: (line) => ctx.out.log(line),
82
+ features: checked.config.features,
83
+ allowPlaintextPii: flagValue(ctx.argv, "--allow-plaintext-pii"),
84
+ consequence: "The import would store personal data as plaintext in the event log.",
57
85
  });
86
+ const report = await runInSystemRowLevelSecurityScope(
87
+ "operator waitlist migration imports signups into the platform tenant and archives legacy streams",
88
+ () =>
89
+ migrateLegacyWaitlist(db, {
90
+ from,
91
+ dryRun,
92
+ sourceTable: flagValue(ctx.argv, "--source-table") ?? DEFAULT_LEGACY_SOURCE_TABLE[from],
93
+ defaultLocale: flagValue(ctx.argv, "--locale") ?? DEFAULT_IMPORT_LOCALE,
94
+ }),
95
+ );
58
96
  ctx.out.log("");
59
97
  ctx.out.log(
60
98
  ` ${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}`,
@@ -64,6 +102,7 @@ export const waitlistMigrateOp = {
64
102
  } catch (e) {
65
103
  return fail(ctx, `✗ ${e instanceof Error ? e.message : String(e)}`);
66
104
  } finally {
105
+ await wiring?.close();
67
106
  await close();
68
107
  }
69
108
  },
@@ -67,6 +67,9 @@ export const projectCommand: CliCommand = {
67
67
  } = await import("@cosmicdrift/kumiko-framework/pipeline");
68
68
 
69
69
  const registry = createRegistry(config.features);
70
+ const { runInSystemRowLevelSecurityScope } = await import(
71
+ "@cosmicdrift/kumiko-framework/internal/row-level-security"
72
+ );
70
73
  const { db, close } = createDbConnection({ url: databaseUrl });
71
74
  let wiring: Awaited<ReturnType<typeof wireKmsFromEnv>> | undefined;
72
75
 
@@ -80,14 +83,19 @@ export const projectCommand: CliCommand = {
80
83
  });
81
84
  }
82
85
  await createProjectionStateTable(db);
83
- switch (sub) {
84
- case "list":
85
- return await listProjections(ctx, db, registry, listProjectionsWithState);
86
- case "status":
87
- return await showProjectionStatus(ctx, db, arg, registry, getProjectionState);
88
- case "rebuild":
89
- return await rebuildOne(ctx, db, arg, registry, rebuildProjection);
90
- }
86
+ return await runInSystemRowLevelSecurityScope(
87
+ "operator projection tool reads and rebuilds projections of every tenant",
88
+ async () => {
89
+ switch (sub) {
90
+ case "list":
91
+ return await listProjections(ctx, db, registry, listProjectionsWithState);
92
+ case "status":
93
+ return await showProjectionStatus(ctx, db, arg, registry, getProjectionState);
94
+ case "rebuild":
95
+ return await rebuildOne(ctx, db, arg, registry, rebuildProjection);
96
+ }
97
+ },
98
+ );
91
99
  } catch (e) {
92
100
  ctx.out.err("");
93
101
  ctx.out.err(` ✗ ${e instanceof Error ? e.message : String(e)}`);
@@ -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
- try {
35
- const manifest = Bun.resolveSync(`${FRAMEWORK_PACKAGE}/package.json`, repoDir);
36
- return stringField(readJson(manifest), "version");
37
- } catch {
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 { existsSync, readFileSync } from "node:fs";
2
- import { dirname, join } from "node:path";
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 = packageDirectory(entry.package, sources.repoDir);
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("");