create-kumiko-app 0.4.243 → 0.4.244

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.
@@ -2110,7 +2110,7 @@
2110
2110
  },
2111
2111
  {
2112
2112
  "name": "tags",
2113
- "description": "Generic, host-agnostic tagging for any entity. Owns two event-sourced entities — the per-tenant `tag` catalog (`read_tags`, with optional `color` and `scope`) and `tag-assignment` join rows keyed by (entityType, entityId) (`read_tag_assignments`) — so tagging adds NO column to the host entity and needs no relational pivot or JOIN. Provides write-handlers `create-tag`, `update-tag` (optimistic-locked rename/recolor/re-scope), `delete-tag` (cascades over assignments), `assign-tag` (idempotent), `remove-tag` (idempotent) and list queries for the catalog and the assignments. Read which tags an entity has, or which entities carry a tag, by listing `tag-assignment` filtered on `entityId` or `tagId` and composing in the read-layer. A tag with empty `scope` is global; a `scope` of an entityType restricts it to that type in the picker. Every path uses one access rule — adopt the host's model with createTagsFeature({ access: { openToAll: true } }) or pin roles with createTagsFeature({ roles }). Pass { toggleable: { default: false } } to make the whole feature tier-gatable via the tier-engine (no host hook).",
2113
+ "description": "Generic, host-agnostic tagging for any entity. Owns two event-sourced entities — the per-tenant `tag` catalog (`read_tags`, with optional `color` and `scope`) and `tag-assignment` join rows keyed by (entityType, entityId) (`read_tag_assignments`) — so tagging adds NO column to the host entity and needs no relational pivot or JOIN. Catalog screens are declarative (`entityList` + `entityEdit`) and use convention QNs `tag:{create,update,delete}`; TagManager/TagPicker keep `create-tag`/`update-tag`/`delete-tag`. Also: `assign-tag` (idempotent), `remove-tag` (idempotent) and list queries for the catalog and the assignments. Read which tags an entity has, or which entities carry a tag, by listing `tag-assignment` filtered on `entityId` or `tagId` and composing in the read-layer. A tag with empty `scope` is global; a `scope` of an entityType restricts it to that type in the picker. Every path uses one access rule — adopt the host's model with createTagsFeature({ access: { openToAll: true } }) or pin roles with createTagsFeature({ roles }). Pass { toggleable: { default: false } } to make the whole feature tier-gatable via the tier-engine (no host hook).",
2114
2114
  "toggleableDefault": null,
2115
2115
  "requires": [],
2116
2116
  "optionalRequires": [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-kumiko-app",
3
- "version": "0.4.243",
3
+ "version": "0.4.244",
4
4
  "description": "`bun create kumiko-app <name>` — scaffold a new Kumiko app with an interactive feature picker.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -27,9 +27,9 @@
27
27
  }
28
28
  },
29
29
  "dependencies": {
30
- "@cosmicdrift/kumiko-dev-server": "0.220.1",
31
- "@cosmicdrift/kumiko-framework": "0.220.1",
32
- "@cosmicdrift/kumiko-server-runtime": "0.220.1",
30
+ "@cosmicdrift/kumiko-dev-server": "0.221.0",
31
+ "@cosmicdrift/kumiko-framework": "0.221.0",
32
+ "@cosmicdrift/kumiko-server-runtime": "0.221.0",
33
33
  "@inquirer/core": "^10.0.0",
34
34
  "@inquirer/prompts": "^7.4.0"
35
35
  },
@@ -0,0 +1,32 @@
1
+ // Vendored manifest.json must match the source-of-truth in
2
+ // samples/apps/use-all-bundled/feature-manifest.json. The picker reads
3
+ // the vendored copy at runtime; a stale copy lets the picker show old
4
+ // features or miss new ones. Refresh by running `bun run gen:manifest`
5
+ // inside samples/apps/use-all-bundled (writes both copies at once).
6
+
7
+ import { describe, expect, test } from "bun:test";
8
+ import { readFileSync } from "node:fs";
9
+ import { dirname, resolve } from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+
12
+ const HERE = dirname(fileURLToPath(import.meta.url));
13
+ const VENDOR = resolve(HERE, "..", "..", "feature-manifest.json");
14
+ const SOURCE = resolve(
15
+ HERE,
16
+ "..",
17
+ "..",
18
+ "..",
19
+ "..",
20
+ "samples",
21
+ "apps",
22
+ "use-all-bundled",
23
+ "feature-manifest.json",
24
+ );
25
+
26
+ describe("vendored manifest", () => {
27
+ test("byte-identical to samples/apps/use-all-bundled/feature-manifest.json", () => {
28
+ const vendor = readFileSync(VENDOR, "utf-8");
29
+ const source = readFileSync(SOURCE, "utf-8");
30
+ expect(vendor).toBe(source);
31
+ });
32
+ });
@@ -6,9 +6,11 @@
6
6
  // feature satisfies — cli.test.ts's file-existence checks can't catch that,
7
7
  // only actually booting the resolved set through validateBoot can.
8
8
 
9
- import { describe, expect, test } from "bun:test";
9
+ import { afterAll, describe, expect, test } from "bun:test";
10
+ import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
11
+ import { join } from "node:path";
10
12
  import { frameworkCoreEnvSchema } from "@cosmicdrift/kumiko-dev-server";
11
- import { resolveKmsWiring } from "@cosmicdrift/kumiko-framework/crypto";
13
+ import { requireKmsWiring, resolveKmsWiring } from "@cosmicdrift/kumiko-framework/crypto";
12
14
  import {
13
15
  createRegistry,
14
16
  type FeatureDefinition,
@@ -21,6 +23,7 @@ import { runProdApp } from "@cosmicdrift/kumiko-server-runtime";
21
23
  import { composeFeatures } from "@cosmicdrift/kumiko-server-runtime/compose-features";
22
24
  import { resolveDeps } from "../dep-resolver";
23
25
  import { FEATURE_CONSTRUCTORS } from "../feature-constructors";
26
+ import { runCreate } from "../index";
24
27
  import { loadManifest } from "../manifest";
25
28
  import { buildChoices } from "../picker";
26
29
 
@@ -125,7 +128,19 @@ describe("--yes resolved set through runProdApp's PII boot gate (issue-2330 regr
125
128
  );
126
129
  });
127
130
 
128
- test("with resolveKmsWiring's fallback (what the generated bin/main.ts now does), boot succeeds", async () => {
131
+ test("requireKmsWiring aborts when NODE_ENV=production and the trio is empty", () => {
132
+ expect(() =>
133
+ requireKmsWiring(
134
+ {},
135
+ {
136
+ logPrefix: "[test]",
137
+ plaintextReason: "should-not-fallback",
138
+ },
139
+ ),
140
+ ).toThrow(/PLATFORM_KEK|SUBJECT_KEYS|BLIND_INDEX|trio|required/i);
141
+ });
142
+
143
+ test("with resolveKmsWiring's fallback (generated bin/main.ts non-prod path), boot succeeds", async () => {
129
144
  // Empty object, not process.env: a dev machine with the real trio set
130
145
  // would otherwise flip this into the ActiveKmsWiring branch and the test
131
146
  // would stop exercising the plaintext fallback it's meant to pin.
@@ -157,3 +172,69 @@ describe("--yes resolved set through runProdApp's PII boot gate (issue-2330 regr
157
172
  expect(handle).toBeDefined();
158
173
  });
159
174
  });
175
+
176
+ // The suite above rebuilds the wiring in-process and pins that the pieces
177
+ // are individually correct — it never imports the file scaffoldApp actually
178
+ // writes, so a typo in bin/main.ts's import specifier or a `...kmsWiring`
179
+ // spread landing in the wrong object literal would stay green. This drives
180
+ // the real generated bin/main.ts as a subprocess instead. Fixtures live
181
+ // under __tests__/.tmp-fixtures/ (gitignored, same trick as
182
+ // dev-server/src/codegen/__tests__/watch.test.ts) so Bun's node_modules
183
+ // walk-up from bin/main.ts still finds the repo's hoisted @cosmicdrift/*
184
+ // symlinks — a system tmpdir sits outside the workspace and can't resolve
185
+ // them (see walkthrough.integration.test.ts's comment on the same issue).
186
+ describe("generated bin/main.ts actually boots (issue-2330 regression, real subprocess)", () => {
187
+ const FIXTURE_ROOT = join(import.meta.dir, ".tmp-fixtures");
188
+ const createdDirs: string[] = [];
189
+
190
+ afterAll(() => {
191
+ for (const d of createdDirs) {
192
+ try {
193
+ rmSync(d, { recursive: true, force: true });
194
+ } catch {
195
+ // ignore — best-effort
196
+ }
197
+ }
198
+ });
199
+
200
+ test("KUMIKO_DRY_RUN_ENV=boot bun bin/main.ts exits 0 against the file scaffoldApp wrote", async () => {
201
+ mkdirSync(FIXTURE_ROOT, { recursive: true });
202
+ const cwd = mkdtempSync(join(FIXTURE_ROOT, "boot-"));
203
+ createdDirs.push(cwd);
204
+
205
+ const exitCode = await runCreate({ name: "boot-fixture", yes: true, cwd, log: () => {} });
206
+ expect(exitCode).toBe(0);
207
+
208
+ const appDir = join(cwd, "boot-fixture");
209
+ const proc = Bun.spawn({
210
+ cmd: ["bun", "bin/main.ts"],
211
+ cwd: appDir,
212
+ env: {
213
+ ...process.env,
214
+ KUMIKO_DRY_RUN_ENV: "boot",
215
+ JWT_SECRET: "a".repeat(32),
216
+ KUMIKO_SECRETS_MASTER_KEY_V1: Buffer.alloc(32, 7).toString("base64"),
217
+ DATABASE_URL: "postgres://dummy:dummy@127.0.0.1:1/dummy",
218
+ REDIS_URL: "redis://127.0.0.1:1",
219
+ // Pinned empty, not inherited from process.env: a dev machine with
220
+ // the real trio set would otherwise flip resolveKmsWiring into the
221
+ // ActiveKmsWiring branch (and its KMS health check) instead of the
222
+ // plaintext fallback this test exercises.
223
+ PLATFORM_KEK: "",
224
+ SUBJECT_KEYS_DATABASE_URL: "",
225
+ KUMIKO_BLIND_INDEX_KEY: "",
226
+ },
227
+ stdout: "pipe",
228
+ stderr: "pipe",
229
+ });
230
+ const [stdout, stderr, code] = await Promise.all([
231
+ new Response(proc.stdout).text(),
232
+ new Response(proc.stderr).text(),
233
+ proc.exited,
234
+ ]);
235
+
236
+ expect(code, `stdout:\n${stdout}\nstderr:\n${stderr}`).toBe(0);
237
+ expect(`${stdout}${stderr}`).not.toContain("BOOT ABORTED");
238
+ expect(stdout).toContain("boot validation OK");
239
+ }, 30_000);
240
+ });