@cosmicdrift/kumiko-dev-server 0.369.0 → 0.370.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/dist/build.d.ts +1 -1
- package/dist/cli.d.ts +3 -3
- package/dist/compose-stacks.d.ts +57 -3
- package/dist/compose-stacks.js +88 -9
- package/dist/host-dispatch-adapter.d.ts +7 -0
- package/dist/host-dispatch-adapter.js +31 -0
- package/dist/index.d.ts +21 -20
- package/dist/index.js +2 -1
- package/dist/init-deploy-cli.js +6 -0
- package/dist/managed-files/app-scripts.d.ts +13 -0
- package/dist/managed-files/app-scripts.js +40 -0
- package/dist/managed-files/check.d.ts +34 -0
- package/dist/managed-files/check.js +109 -0
- package/dist/managed-files/index.d.ts +8 -0
- package/dist/managed-files/index.js +8 -0
- package/dist/managed-files/jsonc.d.ts +1 -0
- package/dist/managed-files/jsonc.js +12 -0
- package/dist/managed-files/line-diff.d.ts +2 -0
- package/dist/managed-files/line-diff.js +60 -0
- package/dist/managed-files/marker.d.ts +17 -0
- package/dist/managed-files/marker.js +49 -0
- package/dist/managed-files/port-registry.d.ts +17 -0
- package/dist/managed-files/port-registry.js +31 -0
- package/dist/managed-files/registry.d.ts +51 -0
- package/dist/managed-files/registry.js +115 -0
- package/dist/managed-files/static-app-config.d.ts +16 -0
- package/dist/managed-files/static-app-config.js +99 -0
- package/dist/managed-files/sync.d.ts +19 -0
- package/dist/managed-files/sync.js +101 -0
- package/dist/run-dev-app.d.ts +1 -1
- package/dist/run-dev-app.js +7 -1
- package/dist/scaffold-app.d.ts +3 -3
- package/dist/scaffold-app.js +125 -199
- package/dist/scaffold-deploy.d.ts +9 -0
- package/dist/scaffold-deploy.js +34 -6
- package/dist/scaffold-feature.js +1 -0
- package/package.json +18 -9
- package/src/changes.json +19 -0
- package/templates/managed/cache-cleanup.yml.template +30 -0
- package/templates/managed/ci.yml.template +65 -0
- package/templates/managed/husky-pre-push.template +36 -0
- package/templates/managed/kumiko-schema.ts.template +12 -0
- package/templates/managed/release.yml.template +55 -0
package/dist/scaffold-app.js
CHANGED
|
@@ -1,22 +1,31 @@
|
|
|
1
1
|
// scaffoldApp — generate a runnable Kumiko app workspace from a name.
|
|
2
2
|
//
|
|
3
3
|
// Used by `kumiko new app <name>`. Produces the minimal app shape that
|
|
4
|
-
// `KUMIKO_DRY_RUN_ENV=boot bun bin/main.ts`
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
4
|
+
// `KUMIKO_DRY_RUN_ENV=boot bun bin/main.ts` and `kumiko check` run green
|
|
5
|
+
// against. Every framework-owned file (deploy, workflows, bunfigs, presets,
|
|
6
|
+
// kumiko/schema.ts) comes from the managed-files registry, the same renders
|
|
7
|
+
// `kumiko check managed` compares against.
|
|
8
8
|
//
|
|
9
9
|
// Generated files are emitted already formatted to Biome's output (import
|
|
10
10
|
// order, 100-col wrapping, trailing commas) so a fresh app passes `biome check`.
|
|
11
11
|
// Static files (package.json, tsconfig, .env, README) stay text-based.
|
|
12
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
13
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
14
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
15
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
return path;
|
|
19
|
+
};
|
|
12
20
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
13
|
-
import { dirname, join,
|
|
21
|
+
import { dirname, join, resolve } from "node:path";
|
|
22
|
+
import { APP_CONFIG_FILE } from "@cosmicdrift/kumiko-framework/config";
|
|
14
23
|
import { collectTableMetas, generateMigration, writeSnapshotJson, } from "@cosmicdrift/kumiko-framework/db";
|
|
15
24
|
import { composeFeatures } from "@cosmicdrift/kumiko-server-runtime/compose-features";
|
|
16
25
|
import { isKebabSegment } from "./kebab.js";
|
|
26
|
+
import { PREPARE_SCRIPT, resolveManagedAppFacts, syncManagedFiles } from "./managed-files/index.js";
|
|
17
27
|
import { renderAgentsMd, renderClaudeMd, renderMountedFeatureList } from "./scaffold-agent-docs.js";
|
|
18
28
|
import { createDemoTasksFeature, renderDemoSeedFile, renderDemoTasksFeatureFile, renderDemoTasksI18n, renderDemoTasksIndex, renderDemoTasksWebIndex, } from "./scaffold-demo-tasks.js";
|
|
19
|
-
import { scaffoldDeploy } from "./scaffold-deploy.js";
|
|
20
29
|
// Exact pin (no caret): the generated files are formatted to this Biome's output,
|
|
21
30
|
// and a scaffold-lint test keeps it equal to the monorepo's own Biome version.
|
|
22
31
|
export const SCAFFOLD_BIOME_VERSION = "2.5.7";
|
|
@@ -44,20 +53,10 @@ export async function scaffoldApp(options) {
|
|
|
44
53
|
const files = [];
|
|
45
54
|
write(join(destination, "package.json"), renderPackageJson(options.name, version, testSetup));
|
|
46
55
|
files.push("package.json");
|
|
47
|
-
write(join(destination, "tsconfig.json"), renderTsconfig());
|
|
48
|
-
files.push("tsconfig.json");
|
|
49
56
|
write(join(destination, "kumiko.json"), renderKumikoManifest());
|
|
50
57
|
files.push("kumiko.json");
|
|
51
|
-
write(join(destination, "biome.json"), renderBiomeJson());
|
|
52
|
-
files.push("biome.json");
|
|
53
58
|
write(join(destination, ".gitignore"), SCAFFOLD_GITIGNORE);
|
|
54
59
|
files.push(".gitignore");
|
|
55
|
-
if (testSetup === undefined) {
|
|
56
|
-
write(join(destination, "bunfig.toml"), renderBunfigToml());
|
|
57
|
-
files.push("bunfig.toml");
|
|
58
|
-
write(join(destination, "bunfig.ci.toml"), renderBunfigCiToml());
|
|
59
|
-
files.push("bunfig.ci.toml");
|
|
60
|
-
}
|
|
61
60
|
write(join(destination, "src", "run-config.ts"), renderRunConfig(options.features));
|
|
62
61
|
files.push("src/run-config.ts");
|
|
63
62
|
mkdirSync(join(destination, "src", "features", "tasks", "web"), { recursive: true });
|
|
@@ -71,12 +70,14 @@ export async function scaffoldApp(options) {
|
|
|
71
70
|
files.push("src/features/tasks/web/index.ts");
|
|
72
71
|
write(join(destination, "src", "seed.ts"), renderDemoSeedFile());
|
|
73
72
|
files.push("src/seed.ts");
|
|
74
|
-
write(join(destination,
|
|
75
|
-
files.push(
|
|
73
|
+
write(join(destination, APP_CONFIG_FILE), renderAppConfig());
|
|
74
|
+
files.push(APP_CONFIG_FILE);
|
|
76
75
|
write(join(destination, "bin", "main.ts"), renderMain(options.name));
|
|
77
76
|
files.push("bin/main.ts");
|
|
78
|
-
write(join(destination, "bin", "
|
|
79
|
-
files.push("bin/
|
|
77
|
+
write(join(destination, "bin", "server.ts"), renderServer(options.name));
|
|
78
|
+
files.push("bin/server.ts");
|
|
79
|
+
write(join(destination, "bin", "env-schema.ts"), renderEnvSchema(options.name));
|
|
80
|
+
files.push("bin/env-schema.ts");
|
|
80
81
|
write(join(destination, "bin", "kumiko.ts"), renderBinKumiko());
|
|
81
82
|
files.push("bin/kumiko.ts");
|
|
82
83
|
write(join(destination, "src", "client.tsx"), renderClient(options.name));
|
|
@@ -89,12 +90,6 @@ export async function scaffoldApp(options) {
|
|
|
89
90
|
files.push("docker-compose.yml");
|
|
90
91
|
await writeInitMigration(destination, options.features);
|
|
91
92
|
files.push("kumiko/migrations/0001_init.sql", "kumiko/migrations/.snapshot.json");
|
|
92
|
-
const deploy = scaffoldDeploy({ appName: options.name, destination });
|
|
93
|
-
for (const f of deploy.files) {
|
|
94
|
-
if (f.written) {
|
|
95
|
-
files.push(relative(destination, f.path));
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
93
|
if (testSetup !== undefined) {
|
|
99
94
|
for (const [rel, content] of Object.entries(testSetup.files)) {
|
|
100
95
|
mkdirSync(dirname(join(destination, rel)), { recursive: true });
|
|
@@ -102,6 +97,15 @@ export async function scaffoldApp(options) {
|
|
|
102
97
|
files.push(rel);
|
|
103
98
|
}
|
|
104
99
|
}
|
|
100
|
+
const managedFacts = resolveManagedAppFacts({
|
|
101
|
+
appRoot: destination,
|
|
102
|
+
appName: options.name,
|
|
103
|
+
uiRoots: SCAFFOLD_UI_ROOTS,
|
|
104
|
+
});
|
|
105
|
+
for (const action of syncManagedFiles(managedFacts, { write: true, adopt: false })) {
|
|
106
|
+
if (action.action === "create")
|
|
107
|
+
files.push(action.path);
|
|
108
|
+
}
|
|
105
109
|
write(join(destination, "README.md"), `${renderReadme(options.name, options.features)}${testSetup === undefined ? "" : `\n${testSetup.rulesMarkdown}`}`);
|
|
106
110
|
files.push("README.md");
|
|
107
111
|
write(join(destination, "AGENTS.md"), renderAgentsMd({
|
|
@@ -120,15 +124,17 @@ function write(path, content) {
|
|
|
120
124
|
}
|
|
121
125
|
function appScripts(testSetup) {
|
|
122
126
|
return {
|
|
123
|
-
dev: "bun --watch bin/
|
|
127
|
+
dev: "bun --watch bin/server.ts",
|
|
124
128
|
build: "bun kumiko-build",
|
|
125
129
|
start: "bun run bin/main.ts",
|
|
126
130
|
boot: "KUMIKO_DRY_RUN_ENV=boot bun bin/main.ts",
|
|
127
131
|
typecheck: "tsc --noEmit",
|
|
128
132
|
lint: "biome check .",
|
|
129
|
-
|
|
133
|
+
check: "kumiko check",
|
|
134
|
+
...(testSetup?.scripts ?? { test: "bun --config=bunfig.toml test --dots" }),
|
|
130
135
|
"schema:apply": "bun kumiko-schema apply",
|
|
131
136
|
"schema:generate": "bun kumiko-schema generate",
|
|
137
|
+
prepare: PREPARE_SCRIPT,
|
|
132
138
|
};
|
|
133
139
|
}
|
|
134
140
|
function renderPackageJson(name, version, testSetup) {
|
|
@@ -150,6 +156,7 @@ function renderPackageJson(name, version, testSetup) {
|
|
|
150
156
|
},
|
|
151
157
|
devDependencies: sortedByName({
|
|
152
158
|
"@biomejs/biome": SCAFFOLD_BIOME_VERSION,
|
|
159
|
+
"@cosmicdrift/kumiko-cli": version,
|
|
153
160
|
"@tailwindcss/cli": "^4.3.0",
|
|
154
161
|
"@types/react": "^19.2.0",
|
|
155
162
|
"@types/react-dom": "^19.2.0",
|
|
@@ -163,114 +170,6 @@ function renderPackageJson(name, version, testSetup) {
|
|
|
163
170
|
function sortedByName(record) {
|
|
164
171
|
return Object.fromEntries(Object.entries(record).sort(([a], [b]) => (a < b ? -1 : 1)));
|
|
165
172
|
}
|
|
166
|
-
function renderTsconfig() {
|
|
167
|
-
return `${JSON.stringify({
|
|
168
|
-
compilerOptions: {
|
|
169
|
-
strict: true,
|
|
170
|
-
noUncheckedIndexedAccess: true,
|
|
171
|
-
forceConsistentCasingInFileNames: true,
|
|
172
|
-
verbatimModuleSyntax: true,
|
|
173
|
-
target: "ESNext",
|
|
174
|
-
module: "ESNext",
|
|
175
|
-
moduleResolution: "bundler",
|
|
176
|
-
esModuleInterop: true,
|
|
177
|
-
skipLibCheck: true,
|
|
178
|
-
lib: ["ESNext", "DOM"],
|
|
179
|
-
types: ["bun-types"],
|
|
180
|
-
jsx: "react-jsx",
|
|
181
|
-
noEmit: true,
|
|
182
|
-
},
|
|
183
|
-
include: ["bin", "src", "kumiko", "e2e", "playwright.config.ts"],
|
|
184
|
-
}, null, 2)}\n`;
|
|
185
|
-
}
|
|
186
|
-
function renderBiomeJson() {
|
|
187
|
-
return `${JSON.stringify({
|
|
188
|
-
$schema: `https://biomejs.dev/schemas/${SCAFFOLD_BIOME_VERSION}/schema.json`,
|
|
189
|
-
vcs: {
|
|
190
|
-
enabled: true,
|
|
191
|
-
clientKind: "git",
|
|
192
|
-
useIgnoreFile: true,
|
|
193
|
-
defaultBranch: "main",
|
|
194
|
-
},
|
|
195
|
-
files: {
|
|
196
|
-
includes: [
|
|
197
|
-
"src/**",
|
|
198
|
-
"bin/**",
|
|
199
|
-
"kumiko/**",
|
|
200
|
-
"e2e/**",
|
|
201
|
-
"playwright.config.ts",
|
|
202
|
-
"!**/dist",
|
|
203
|
-
"!kumiko/migrations",
|
|
204
|
-
],
|
|
205
|
-
},
|
|
206
|
-
formatter: {
|
|
207
|
-
enabled: true,
|
|
208
|
-
indentStyle: "space",
|
|
209
|
-
indentWidth: 2,
|
|
210
|
-
lineWidth: 100,
|
|
211
|
-
lineEnding: "lf",
|
|
212
|
-
},
|
|
213
|
-
css: {
|
|
214
|
-
parser: { cssModules: false, tailwindDirectives: true },
|
|
215
|
-
},
|
|
216
|
-
javascript: {
|
|
217
|
-
formatter: {
|
|
218
|
-
quoteStyle: "double",
|
|
219
|
-
jsxQuoteStyle: "double",
|
|
220
|
-
semicolons: "always",
|
|
221
|
-
trailingCommas: "all",
|
|
222
|
-
arrowParentheses: "always",
|
|
223
|
-
},
|
|
224
|
-
},
|
|
225
|
-
json: { formatter: { indentWidth: 2, lineWidth: 80 } },
|
|
226
|
-
linter: {
|
|
227
|
-
enabled: true,
|
|
228
|
-
rules: {
|
|
229
|
-
preset: "recommended",
|
|
230
|
-
correctness: { noUnusedVariables: "error", noUnusedImports: "error" },
|
|
231
|
-
suspicious: { noExplicitAny: "error", noDebugger: "error", noConsole: "warn" },
|
|
232
|
-
complexity: { useLiteralKeys: "off" },
|
|
233
|
-
style: { useConst: "error" },
|
|
234
|
-
nursery: { noFloatingPromises: "error" },
|
|
235
|
-
},
|
|
236
|
-
},
|
|
237
|
-
overrides: [
|
|
238
|
-
{
|
|
239
|
-
includes: ["**/*.test.ts", "**/*.spec.ts", "**/*.integration.ts", "**/*.e2e.ts"],
|
|
240
|
-
linter: {
|
|
241
|
-
rules: {
|
|
242
|
-
suspicious: { noConsole: "off" },
|
|
243
|
-
style: { noNonNullAssertion: "off" },
|
|
244
|
-
},
|
|
245
|
-
},
|
|
246
|
-
},
|
|
247
|
-
],
|
|
248
|
-
}, null, 2)}\n`;
|
|
249
|
-
}
|
|
250
|
-
function renderBunfigToml() {
|
|
251
|
-
return `[install]
|
|
252
|
-
linker = "hoisted"
|
|
253
|
-
|
|
254
|
-
[test]
|
|
255
|
-
pathIgnorePatterns = [
|
|
256
|
-
"**/e2e/**",
|
|
257
|
-
"**/*.spec.ts",
|
|
258
|
-
"**/dist/**",
|
|
259
|
-
]
|
|
260
|
-
`;
|
|
261
|
-
}
|
|
262
|
-
function renderBunfigCiToml() {
|
|
263
|
-
return `[install]
|
|
264
|
-
linker = "hoisted"
|
|
265
|
-
|
|
266
|
-
[test]
|
|
267
|
-
pathIgnorePatterns = [
|
|
268
|
-
"**/e2e/**",
|
|
269
|
-
"**/*.spec.ts",
|
|
270
|
-
"**/dist/**",
|
|
271
|
-
]
|
|
272
|
-
`;
|
|
273
|
-
}
|
|
274
173
|
const FOUNDATION_FEATURES = [
|
|
275
174
|
{
|
|
276
175
|
name: "secrets",
|
|
@@ -389,45 +288,51 @@ ${passwordAndExtras}
|
|
|
389
288
|
},
|
|
390
289
|
},`;
|
|
391
290
|
}
|
|
392
|
-
|
|
393
|
-
|
|
291
|
+
// Ports match runDevApp's default and kumiko-testing's SCAFFOLD_E2E_PORT.
|
|
292
|
+
const SCAFFOLD_DEV_PORT = 4173;
|
|
293
|
+
const SCAFFOLD_E2E_PORT = 4174;
|
|
294
|
+
function renderAppConfig() {
|
|
394
295
|
const imports = renderSortedImports([
|
|
395
|
-
{ specifier: "@cosmicdrift/kumiko-
|
|
396
|
-
{ specifier: "@cosmicdrift/kumiko-server-runtime", names: ["composeFeatures", "runProdApp"] },
|
|
397
|
-
{ specifier: "@cosmicdrift/kumiko-framework/engine", names: ["TenantId"], typeOnly: true },
|
|
398
|
-
{ specifier: "@cosmicdrift/kumiko-framework/env", names: ["composeEnvSchema"] },
|
|
296
|
+
{ specifier: "@cosmicdrift/kumiko-framework/config", names: ["defineAppConfig"] },
|
|
399
297
|
{
|
|
400
|
-
specifier: "@cosmicdrift/kumiko-
|
|
401
|
-
names: ["
|
|
298
|
+
specifier: "@cosmicdrift/kumiko-server-runtime/compose-features",
|
|
299
|
+
names: ["composeFeatures"],
|
|
402
300
|
},
|
|
403
|
-
{ specifier: "
|
|
404
|
-
{ specifier: "../src/run-config", names: ["APP_FEATURES", "HAS_AUTH"] },
|
|
301
|
+
{ specifier: "./src/run-config", names: ["APP_FEATURES", "HAS_AUTH"] },
|
|
405
302
|
]);
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
'displayName: "Admin",',
|
|
410
|
-
]);
|
|
411
|
-
// The envSchema must cover the SAME features runProdApp mounts at boot:
|
|
412
|
-
// `auth: { admin: … }` makes runProdApp auto-mix config/user/tenant/
|
|
413
|
-
// auth-email-password, so the JWT_SECRET (min-32) declaration has to be part
|
|
414
|
-
// of the boot-gate here too. Subject-key KMS is optional locally (plaintext +
|
|
415
|
-
// loud warning) but required under NODE_ENV=production (fw#2317 / #2339).
|
|
416
|
-
return `// Production-bootstrap. KUMIKO_DRY_RUN_ENV=boot exits after
|
|
417
|
-
// composeFeatures + validateBoot + createRegistry without DB/Redis-connect
|
|
418
|
-
// (see @cosmicdrift/kumiko-server-runtime runProdApp). The real dev boot
|
|
419
|
-
// runs via \`bunx kumiko dev\` (in-repo dev-tool) with a Docker stack — DX-1.0
|
|
420
|
-
// only covers the boot-mode path; \`kumiko dev\` lands in a later DX phase.
|
|
303
|
+
return `// The one place the tooling reads: features for schema, migrations and agent
|
|
304
|
+
// lint, client entries, and the ports this app claims (see \`kumiko ports\`).
|
|
305
|
+
// features is the composed list, so the bundled auth features count too.
|
|
421
306
|
|
|
422
307
|
${imports}
|
|
423
308
|
|
|
424
|
-
|
|
309
|
+
export default defineAppConfig({
|
|
310
|
+
features: composeFeatures([...APP_FEATURES], { includeBundled: HAS_AUTH }),
|
|
311
|
+
client: { entries: ["src/client.tsx"] },
|
|
312
|
+
ports: { dev: ${SCAFFOLD_DEV_PORT}, e2e: ${SCAFFOLD_E2E_PORT} },
|
|
313
|
+
});
|
|
314
|
+
`;
|
|
315
|
+
}
|
|
316
|
+
function renderEnvSchema(appName) {
|
|
317
|
+
const imports = renderSortedImports([
|
|
318
|
+
{ specifier: "@cosmicdrift/kumiko-dev-server/env-schema", names: ["frameworkCoreEnvSchema"] },
|
|
319
|
+
{ specifier: "@cosmicdrift/kumiko-framework/config", names: ["allAppFeatures"] },
|
|
320
|
+
{ specifier: "@cosmicdrift/kumiko-framework/env", names: ["composeEnvSchema"] },
|
|
321
|
+
{ specifier: "zod", names: ["z"] },
|
|
322
|
+
]);
|
|
323
|
+
// The schema must cover the SAME features runProdApp mounts at boot:
|
|
324
|
+
// `auth: { admin: … }` makes runProdApp auto-mix config/user/tenant/
|
|
325
|
+
// auth-email-password, so their JWT_SECRET (min-32) declaration is part of the
|
|
326
|
+
// boot gate. kumiko.config.ts already holds that composed list.
|
|
327
|
+
return `// Env contract of the production processes (bin/main.ts, bin/kumiko.ts).
|
|
328
|
+
// Importing it must not read process.env; validation happens in runProdApp.
|
|
425
329
|
|
|
426
|
-
|
|
330
|
+
${imports}
|
|
331
|
+
import appConfig from "../kumiko.config";
|
|
427
332
|
|
|
428
|
-
const envSchema = composeEnvSchema({
|
|
333
|
+
export const envSchema = composeEnvSchema({
|
|
429
334
|
core: frameworkCoreEnvSchema,
|
|
430
|
-
features:
|
|
335
|
+
features: allAppFeatures(appConfig),
|
|
431
336
|
extend: z.object({
|
|
432
337
|
DEMO_ADMIN_PASSWORD: z
|
|
433
338
|
.string()
|
|
@@ -438,6 +343,34 @@ const envSchema = composeEnvSchema({
|
|
|
438
343
|
.meta({ kumiko: { pulumi: { secret: true } } }),
|
|
439
344
|
}),
|
|
440
345
|
});
|
|
346
|
+
`;
|
|
347
|
+
}
|
|
348
|
+
function renderMain(appName) {
|
|
349
|
+
const tenantId = deriveTenantId(appName);
|
|
350
|
+
const imports = renderSortedImports([
|
|
351
|
+
{ specifier: "@cosmicdrift/kumiko-server-runtime", names: ["runProdApp"] },
|
|
352
|
+
{ specifier: "@cosmicdrift/kumiko-framework/engine", names: ["TenantId"], typeOnly: true },
|
|
353
|
+
{
|
|
354
|
+
specifier: "@cosmicdrift/kumiko-framework/crypto",
|
|
355
|
+
names: ["requireKmsWiring", "resolveKmsWiring"],
|
|
356
|
+
},
|
|
357
|
+
{ specifier: "../src/run-config", names: ["APP_FEATURES"] },
|
|
358
|
+
{ specifier: "./env-schema", names: ["envSchema"] },
|
|
359
|
+
]);
|
|
360
|
+
const authBlock = renderAdminAuthBlock(appName, [
|
|
361
|
+
"// runProdApp validates envSchema before seeding: a missing or short value aborts the boot.",
|
|
362
|
+
'password: process.env["DEMO_ADMIN_PASSWORD"] ?? "",',
|
|
363
|
+
'displayName: "Admin",',
|
|
364
|
+
]);
|
|
365
|
+
// Subject-key KMS is optional locally (plaintext + loud warning) but required
|
|
366
|
+
// under NODE_ENV=production (fw#2317 / #2339).
|
|
367
|
+
return `// Production-bootstrap. KUMIKO_DRY_RUN_ENV=boot exits after
|
|
368
|
+
// composeFeatures + validateBoot + createRegistry without DB/Redis-connect
|
|
369
|
+
// (see @cosmicdrift/kumiko-server-runtime runProdApp).
|
|
370
|
+
|
|
371
|
+
${imports}
|
|
372
|
+
|
|
373
|
+
const DEFAULT_TENANT_ID = "${tenantId}" as TenantId;
|
|
441
374
|
|
|
442
375
|
const kmsOpts = {
|
|
443
376
|
logPrefix: "[${appName}]",
|
|
@@ -459,7 +392,7 @@ ${authBlock}
|
|
|
459
392
|
});
|
|
460
393
|
`;
|
|
461
394
|
}
|
|
462
|
-
function
|
|
395
|
+
function renderServer(appName) {
|
|
463
396
|
const tenantId = deriveTenantId(appName);
|
|
464
397
|
const imports = renderSortedImports([
|
|
465
398
|
{ specifier: "@cosmicdrift/kumiko-dev-server", names: ["runDevApp"] },
|
|
@@ -474,7 +407,7 @@ function renderDev(appName) {
|
|
|
474
407
|
'displayName: "Admin",',
|
|
475
408
|
"emailVerified: true,",
|
|
476
409
|
]);
|
|
477
|
-
return `// Dev bootstrap. \`bun --watch bin/
|
|
410
|
+
return `// Dev bootstrap. \`bun --watch bin/server.ts\` (see package.json scripts.dev)
|
|
478
411
|
// starts a full-featured dev server with auto-reload on code changes.
|
|
479
412
|
// setupTestStack creates missing entity tables automatically — a new
|
|
480
413
|
// r.entity(...) in a feature becomes a CREATE TABLE on the next reboot,
|
|
@@ -662,7 +595,9 @@ bun run boot
|
|
|
662
595
|
\`\`\`
|
|
663
596
|
|
|
664
597
|
Runs \`KUMIKO_DRY_RUN_ENV=boot bun bin/main.ts\` — validates feature composition
|
|
665
|
-
+ env schema, exits 0 without touching DB/Redis. Useful in CI. It
|
|
598
|
+
+ env schema, exits 0 without touching DB/Redis. Useful in CI. It prints every
|
|
599
|
+
variable with its feature and status, never the value. An invalid value exits 1;
|
|
600
|
+
a missing one exits 0 without validating the wiring, so set
|
|
666
601
|
\`DEMO_ADMIN_PASSWORD\` (min 12 chars, \`openssl rand -hex 24\`) in \`.env\`.
|
|
667
602
|
|
|
668
603
|
Without \`PLATFORM_KEK\` / \`SUBJECT_KEYS_DATABASE_URL\` / \`KUMIKO_BLIND_INDEX_KEY\`
|
|
@@ -692,13 +627,23 @@ bun run schema:generate <name>
|
|
|
692
627
|
\`deploy/Dockerfile\` + \`deploy/migrate-step.sh\` are scaffolded for container
|
|
693
628
|
deploys. Build context = app repo root; migrations ship in \`kumiko/migrations/\`.
|
|
694
629
|
|
|
630
|
+
## Managed files
|
|
631
|
+
|
|
632
|
+
Files whose first line reads \`kumiko-managed\` (deploy/, .github/workflows/,
|
|
633
|
+
.husky/pre-push, the bunfigs, kumiko/schema.ts) belong to the framework.
|
|
634
|
+
\`bun run check\` reports drift against the installed version;
|
|
635
|
+
\`bunx kumiko sync-managed --write\` restores them. To keep a local change, add
|
|
636
|
+
\`# kumiko-managed: local <reason>\` as line 2.
|
|
637
|
+
|
|
695
638
|
## Architecture
|
|
696
639
|
|
|
697
640
|
- \`src/run-config.ts\` — single source of truth: which features your app mounts (\`APP_FEATURES\`, \`HAS_AUTH\`).
|
|
698
641
|
- \`src/features/tasks/\` — demo feature (entity + handlers + screens + nav).
|
|
699
642
|
- \`src/seed.ts\` — dev seed for demo tasks (\`bun dev\` only).
|
|
700
|
-
- \`kumiko
|
|
701
|
-
- \`
|
|
643
|
+
- \`kumiko.config.ts\` — the composed feature list, client entries and ports the tooling reads.
|
|
644
|
+
- \`kumiko/schema.ts\` — generated (\`kumiko-managed\`): \`ENTITY_METAS\` from kumiko.config.ts for \`kumiko schema\`.
|
|
645
|
+
- \`bin/server.ts\` — dev-server entry (\`bun dev\`).
|
|
646
|
+
- \`bin/env-schema.ts\` — env contract of the production processes.
|
|
702
647
|
- \`bin/main.ts\` — production-bootstrap (\`bun run start\`).
|
|
703
648
|
- \`bin/kumiko.ts\` — schema + consumer-ops CLI bundled into \`dist-server/kumiko.js\`.
|
|
704
649
|
- \`docker-compose.yml\` — local Postgres + Redis for \`bun dev\`.
|
|
@@ -714,33 +659,14 @@ function renderStylesCss() {
|
|
|
714
659
|
"",
|
|
715
660
|
].join("\n");
|
|
716
661
|
}
|
|
717
|
-
function renderKumikoSchema() {
|
|
718
|
-
return [
|
|
719
|
-
"// Live ENTITY_METAS source for `kumiko schema generate|apply|status`.",
|
|
720
|
-
"//",
|
|
721
|
-
"// Computes table-metas from the SAME composeFeatures(APP_FEATURES) the",
|
|
722
|
-
"// runtime sees (runProdApp/runDevApp) — migration and runtime cannot drift.",
|
|
723
|
-
"",
|
|
724
|
-
'import { collectTableMetas, type EntityTableMeta } from "@cosmicdrift/kumiko-framework/db";',
|
|
725
|
-
'import type { FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";',
|
|
726
|
-
'import { composeFeatures } from "@cosmicdrift/kumiko-server-runtime/compose-features";',
|
|
727
|
-
'import { APP_FEATURES, HAS_AUTH } from "../src/run-config";',
|
|
728
|
-
"",
|
|
729
|
-
"export const FEATURES: readonly FeatureDefinition[] = composeFeatures([...APP_FEATURES], {",
|
|
730
|
-
" includeBundled: HAS_AUTH,",
|
|
731
|
-
"});",
|
|
732
|
-
"",
|
|
733
|
-
"export const ENTITY_METAS: readonly EntityTableMeta[] = collectTableMetas(FEATURES);",
|
|
734
|
-
"",
|
|
735
|
-
].join("\n");
|
|
736
|
-
}
|
|
737
662
|
// `kumiko check` fails a kind "app" without uiRoots, so a fresh app must declare them.
|
|
663
|
+
const SCAFFOLD_UI_ROOTS = ["src/features/*/web"];
|
|
738
664
|
function renderKumikoManifest() {
|
|
739
665
|
return `${JSON.stringify({
|
|
740
666
|
kind: "app",
|
|
741
667
|
sourceRoots: ["src", "bin"],
|
|
742
668
|
testGlobs: ["src/**/*.{test,integration}.{ts,tsx}"],
|
|
743
|
-
uiRoots:
|
|
669
|
+
uiRoots: SCAFFOLD_UI_ROOTS,
|
|
744
670
|
excludes: ["**/node_modules/**", "**/dist/**"],
|
|
745
671
|
}, null, 2)}\n`;
|
|
746
672
|
}
|
|
@@ -753,20 +679,20 @@ function renderBinKumiko() {
|
|
|
753
679
|
"// consumer status|restart <name>` to recover a dead event consumer without",
|
|
754
680
|
"// ad-hoc SQL. kumiko-build bundles this file to dist-server/kumiko.js.",
|
|
755
681
|
"",
|
|
756
|
-
'import {
|
|
682
|
+
'import { allAppFeatures } from "@cosmicdrift/kumiko-framework/config";',
|
|
757
683
|
'import { runConsumerCli } from "@cosmicdrift/kumiko-framework/consumer-cli";',
|
|
758
|
-
'import {
|
|
684
|
+
'import { kmsSlotsOf } from "@cosmicdrift/kumiko-framework/env";',
|
|
759
685
|
'import { runSchemaCli } from "@cosmicdrift/kumiko-framework/schema-cli";',
|
|
760
|
-
'import
|
|
761
|
-
'import {
|
|
686
|
+
'import appConfig from "../kumiko.config";',
|
|
687
|
+
'import { envSchema } from "./env-schema";',
|
|
762
688
|
"",
|
|
763
689
|
"const [, , cmd, ...rest] = Bun.argv;",
|
|
764
690
|
"// biome-ignore lint/suspicious/noConsole: CLI output is the feature.",
|
|
765
691
|
"const out = { log: (l: string) => console.log(l), err: (l: string) => console.error(l) };",
|
|
766
692
|
"",
|
|
767
693
|
'if (cmd === "schema") {',
|
|
768
|
-
" const features =
|
|
769
|
-
" const kmsSlots = kmsSlotsOf(
|
|
694
|
+
" const features = allAppFeatures(appConfig);",
|
|
695
|
+
" const kmsSlots = kmsSlotsOf(envSchema.schema);",
|
|
770
696
|
" process.exit(",
|
|
771
697
|
" await runSchemaCli(rest, process.env.INIT_CWD ?? process.cwd(), out, { features, kmsSlots }),",
|
|
772
698
|
" );",
|
|
@@ -778,7 +704,7 @@ function renderBinKumiko() {
|
|
|
778
704
|
"",
|
|
779
705
|
"// biome-ignore lint/suspicious/noConsole: CLI output is the feature.",
|
|
780
706
|
"console.error(",
|
|
781
|
-
` \`\\n Unknown: kumiko \${cmd ?? ""} — only 'kumiko schema
|
|
707
|
+
` \`\\n Unknown: kumiko \${cmd ?? ""} — the standalone bundle only runs 'kumiko schema' and 'kumiko consumer'.\\n\`,`,
|
|
782
708
|
");",
|
|
783
709
|
"process.exit(1);",
|
|
784
710
|
"",
|
|
@@ -789,7 +715,7 @@ async function instantiateScaffoldFeatures(features) {
|
|
|
789
715
|
const effective = base.filter((f) => !COMPOSE_AUTO_MOUNTED_NAMES.has(f.name));
|
|
790
716
|
const instances = [];
|
|
791
717
|
for (const entry of effective) {
|
|
792
|
-
const mod = (await import(entry.importPath));
|
|
718
|
+
const mod = (await import(__rewriteRelativeImportExtension(entry.importPath)));
|
|
793
719
|
const exp = mod[entry.exportName];
|
|
794
720
|
if (exp === undefined) {
|
|
795
721
|
throw new Error(`scaffoldApp: ${entry.importPath} missing export ${entry.exportName} for ${entry.callExpression}`);
|
|
@@ -12,6 +12,9 @@ export type RenderDeployFilesOptions = {
|
|
|
12
12
|
* `destination`. Lets the caller scaffold into one dir while detecting
|
|
13
13
|
* optional surfaces in another (rare — mostly destination = sourceDir). */
|
|
14
14
|
readonly sourceDir?: string;
|
|
15
|
+
/** Registry configs to treat as present although they may not exist yet:
|
|
16
|
+
* the managed-files sync writes bunfig.toml in the same pass as the Dockerfile. */
|
|
17
|
+
readonly ensuredRegistryConfigFiles?: readonly RegistryConfigFile[];
|
|
15
18
|
};
|
|
16
19
|
export type ScaffoldDeployOptions = RenderDeployFilesOptions & {
|
|
17
20
|
/** Overwrite existing files instead of skipping them. */
|
|
@@ -85,5 +88,11 @@ export declare function scaffoldDeploy(options: ScaffoldDeployOptions): Scaffold
|
|
|
85
88
|
/** Read-only: compares the rendered deploy files against what's on disk
|
|
86
89
|
* without writing anything. */
|
|
87
90
|
export declare function checkDeployDrift(options: RenderDeployFilesOptions): CheckDeployDriftResult;
|
|
91
|
+
export type KumikoReleaseTarget = {
|
|
92
|
+
readonly namespace: string;
|
|
93
|
+
readonly deployment: string;
|
|
94
|
+
};
|
|
95
|
+
/** Release target from `package.json#kumiko.deploy`; throws on an invalid deploy config. */
|
|
96
|
+
export declare function readReleaseTarget(sourceDir: string, appName: string): KumikoReleaseTarget;
|
|
88
97
|
export declare function render(source: string, subs: Readonly<Record<string, string>>, flags: Readonly<Record<string, boolean>>): string;
|
|
89
98
|
export {};
|
package/dist/scaffold-deploy.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
14
14
|
import { dirname, join } from "node:path";
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
|
+
import { parseJsonOrThrow } from "@cosmicdrift/kumiko-framework/utils";
|
|
16
17
|
import * as z from "zod";
|
|
17
18
|
import { isKebabSegment } from "./kebab.js";
|
|
18
19
|
const REGISTRY_CONFIG_FILES = ["bunfig.toml", ".npmrc"];
|
|
@@ -46,7 +47,7 @@ export function renderDeployFiles(options) {
|
|
|
46
47
|
// a `seeds/` directory (e.g. studio) crash in Docker-build with
|
|
47
48
|
// `failed to compute cache key: "/app/seeds": not found`.
|
|
48
49
|
const sourceDir = options.sourceDir ?? destinationRoot;
|
|
49
|
-
const detected = detectOptionalSurfaces(sourceDir, options.appName);
|
|
50
|
+
const detected = detectOptionalSurfaces(sourceDir, options.appName, options.ensuredRegistryConfigFiles ?? []);
|
|
50
51
|
if (detected.hasPrivateGhPackages && detected.registryConfigFiles.length === 0) {
|
|
51
52
|
// The Dockerfile only forwards the auth build-arg; without a registry
|
|
52
53
|
// config scope entry bun resolves @cosmicdriftgamestudio/* against npmjs
|
|
@@ -143,10 +144,17 @@ const LOCAL_DEP_SPEC_RE = /^(file:|workspace:|link:)/;
|
|
|
143
144
|
// full Postgres identifier grammar (no quoting support) since they are
|
|
144
145
|
// interpolated into a shell string, not passed through a driver's escaping.
|
|
145
146
|
const DB_IDENTIFIER_RE = /^[a-zA-Z0-9_][a-zA-Z0-9_-]{0,62}$/;
|
|
147
|
+
// Kubernetes object names (RFC 1123 label); they are interpolated into the
|
|
148
|
+
// generated release workflow, so nothing outside this charset may pass.
|
|
149
|
+
const K8S_NAME_RE = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
146
150
|
const kumikoDeployConfigSchema = z
|
|
147
151
|
.object({
|
|
148
152
|
dbUser: z.string().regex(DB_IDENTIFIER_RE).optional(),
|
|
149
153
|
dbName: z.string().regex(DB_IDENTIFIER_RE).optional(),
|
|
154
|
+
/** Namespace of the prod deployment; default = appName. */
|
|
155
|
+
namespace: z.string().regex(K8S_NAME_RE).optional(),
|
|
156
|
+
/** Name of the prod Deployment; default = appName. */
|
|
157
|
+
deployment: z.string().regex(K8S_NAME_RE).optional(),
|
|
150
158
|
// "directory" is the only behaviour left; the key stays valid so existing
|
|
151
159
|
// package.json files keep scaffolding.
|
|
152
160
|
stackNetwork: z
|
|
@@ -176,6 +184,25 @@ function parseDeployConfig(raw) {
|
|
|
176
184
|
: ["kumiko", "deploy", ...issue.path.map(String)].join(".");
|
|
177
185
|
throw new Error(`scaffoldDeploy: invalid package.json#${location} — ${issue?.message ?? "validation failed"}`);
|
|
178
186
|
}
|
|
187
|
+
/** Release target from `package.json#kumiko.deploy`; throws on an invalid deploy config. */
|
|
188
|
+
export function readReleaseTarget(sourceDir, appName) {
|
|
189
|
+
const pkgJsonPath = join(sourceDir, "package.json");
|
|
190
|
+
const raw = existsSync(pkgJsonPath)
|
|
191
|
+
? parseJsonOrThrow(readFileSync(pkgJsonPath, "utf-8"), pkgJsonPath)
|
|
192
|
+
: undefined;
|
|
193
|
+
const config = parseDeployConfig(extractDeployConfigRaw(raw));
|
|
194
|
+
const namespace = config.namespace ?? appName;
|
|
195
|
+
const deployment = config.deployment ?? appName;
|
|
196
|
+
for (const [field, value] of [
|
|
197
|
+
["namespace", namespace],
|
|
198
|
+
["deployment", deployment],
|
|
199
|
+
]) {
|
|
200
|
+
if (!K8S_NAME_RE.test(value)) {
|
|
201
|
+
throw new Error(`scaffoldDeploy: "${value}" cannot serve as the release ${field} (must match ${K8S_NAME_RE}); set package.json#kumiko.deploy.${field}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return { namespace, deployment };
|
|
205
|
+
}
|
|
179
206
|
function resolveDbIdentifier(configured, appName, field, role) {
|
|
180
207
|
const effective = configured ?? appName;
|
|
181
208
|
// The default (appName) already passed isKebabSegment's charset check but
|
|
@@ -197,7 +224,7 @@ function resolveDeployConfig(deployConfigRaw, appName) {
|
|
|
197
224
|
dbName: resolveDbIdentifier(config.dbName, appName, "dbName", "DB name"),
|
|
198
225
|
};
|
|
199
226
|
}
|
|
200
|
-
function detectOptionalSurfaces(sourceDir, appName) {
|
|
227
|
+
function detectOptionalSurfaces(sourceDir, appName, ensuredRegistryConfigFiles) {
|
|
201
228
|
const hasSeeds = existsSync(join(sourceDir, "seeds"));
|
|
202
229
|
let hasPrivateGhPackages = false;
|
|
203
230
|
let installFromFullTree = false;
|
|
@@ -237,7 +264,7 @@ function detectOptionalSurfaces(sourceDir, appName) {
|
|
|
237
264
|
console.warn(message);
|
|
238
265
|
}
|
|
239
266
|
}
|
|
240
|
-
const registryConfigFiles = REGISTRY_CONFIG_FILES.filter((f) => existsSync(join(sourceDir, f)));
|
|
267
|
+
const registryConfigFiles = REGISTRY_CONFIG_FILES.filter((f) => ensuredRegistryConfigFiles.includes(f) || existsSync(join(sourceDir, f)));
|
|
241
268
|
return {
|
|
242
269
|
hasSeeds,
|
|
243
270
|
hasPrivateGhPackages,
|
|
@@ -247,10 +274,11 @@ function detectOptionalSurfaces(sourceDir, appName) {
|
|
|
247
274
|
};
|
|
248
275
|
}
|
|
249
276
|
// Unconsumed-mustache guard. After step 1+2 the only legitimate `{{`
|
|
250
|
-
// survivors are Docker/Go template tags (`{{.Name}}`, leading `.`)
|
|
277
|
+
// survivors are Docker/Go template tags (`{{.Name}}`, leading `.`) and GitHub
|
|
278
|
+
// Actions expressions (`${{ github.sha }}`, leading `$`). An
|
|
251
279
|
// orphan close-tag (`{{/foo}}` without an opener) matches neither step and
|
|
252
280
|
// would otherwise leak into the rendered file as a silent failure.
|
|
253
|
-
const ORPHAN_MUSTACHE_RE =
|
|
281
|
+
const ORPHAN_MUSTACHE_RE = /(?<!\$)\{\{(?!\.)[^}]*\}\}/;
|
|
254
282
|
export function render(source, subs, flags) {
|
|
255
283
|
// Step 1: handle mustache-style block conditionals `{{#flag}}...{{/flag}}`
|
|
256
284
|
// (multiline-aware via [\s\S]). When flag is truthy → keep inner content;
|
|
@@ -267,7 +295,7 @@ export function render(source, subs, flags) {
|
|
|
267
295
|
// narrow: lowercase-leading identifier followed by alphanumerics. That
|
|
268
296
|
// excludes Docker/Go template syntax like `{{.Name}}` (leading `.`)
|
|
269
297
|
// which appears verbatim in the migrate-step.sh shell snippet.
|
|
270
|
-
result = result.replace(
|
|
298
|
+
result = result.replace(/(?<!\$)\{\{([a-z][a-zA-Z0-9]*)\}\}/g, (full, key) => {
|
|
271
299
|
const value = subs[key];
|
|
272
300
|
if (value === undefined) {
|
|
273
301
|
throw new Error(`scaffoldDeploy.render: unknown placeholder "${full}"`);
|
package/dist/scaffold-feature.js
CHANGED