@cosmicdrift/kumiko-dev-server 0.329.0 → 0.330.2
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/README.md +17 -0
- package/bin/kumiko-build.ts +3 -2
- package/bin/kumiko-dev.ts +1 -1
- package/bin/kumiko-init-deploy.ts +1 -1
- package/bin/kumiko-schema-check.ts +4 -1
- package/dist/build-server-bundle.d.ts +31 -0
- package/dist/build-server-bundle.js +282 -0
- package/dist/build.d.ts +2 -0
- package/dist/build.js +5 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +9 -0
- package/dist/codegen/index.d.ts +4 -0
- package/dist/codegen/index.js +4 -0
- package/dist/codegen/render.d.ts +55 -0
- package/dist/codegen/render.js +273 -0
- package/dist/codegen/run-codegen.d.ts +20 -0
- package/dist/codegen/run-codegen.js +219 -0
- package/dist/codegen/scan-events.d.ts +76 -0
- package/dist/codegen/scan-events.js +447 -0
- package/dist/codegen/watch.d.ts +33 -0
- package/dist/codegen/watch.js +112 -0
- package/dist/compose-stacks.d.ts +60 -0
- package/dist/compose-stacks.js +127 -0
- package/dist/crash-tracker.d.ts +17 -0
- package/dist/crash-tracker.js +37 -0
- package/dist/create-kumiko-server.d.ts +195 -0
- package/dist/create-kumiko-server.js +1040 -0
- package/dist/env-schema.d.ts +15 -0
- package/dist/env-schema.js +60 -0
- package/dist/few-shot-corpus.d.ts +72 -0
- package/dist/few-shot-corpus.js +262 -0
- package/dist/framework-version.d.ts +1 -0
- package/dist/framework-version.js +31 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +33 -0
- package/dist/init-deploy-cli.d.ts +10 -0
- package/dist/init-deploy-cli.js +111 -0
- package/dist/kebab.d.ts +1 -0
- package/{src/kebab.ts → dist/kebab.js} +2 -3
- package/dist/run-dev-app.d.ts +174 -0
- package/dist/run-dev-app.js +403 -0
- package/dist/scaffold-app-feature.d.ts +16 -0
- package/dist/scaffold-app-feature.js +240 -0
- package/dist/scaffold-app.d.ts +45 -0
- package/dist/scaffold-app.js +852 -0
- package/dist/scaffold-demo-tasks.d.ts +8 -0
- package/{src/scaffold-demo-tasks.ts → dist/scaffold-demo-tasks.js} +69 -98
- package/dist/scaffold-deploy.d.ts +92 -0
- package/dist/scaffold-deploy.js +254 -0
- package/dist/scaffold-feature.d.ts +26 -0
- package/dist/scaffold-feature.js +163 -0
- package/dist/schema-apply.d.ts +11 -0
- package/dist/schema-apply.js +95 -0
- package/dist/schema-check-core.d.ts +2 -0
- package/{src/schema-check-core.ts → dist/schema-check-core.js} +8 -10
- package/dist/schema-tables-auth-mode.d.ts +2 -0
- package/{src/schema-tables-auth-mode.ts → dist/schema-tables-auth-mode.js} +2 -10
- package/dist/schema-tables-minimal.d.ts +2 -0
- package/{src/schema-tables-minimal.ts → dist/schema-tables-minimal.js} +2 -11
- package/dist/setup-test-stack-from-features.d.ts +11 -0
- package/dist/setup-test-stack-from-features.js +42 -0
- package/dist/welcome-banner.d.ts +15 -0
- package/dist/welcome-banner.js +27 -0
- package/package.json +71 -24
- package/src/changes.json +6 -0
- package/src/__tests__/build-prod-bundle.integration.test.ts +0 -282
- package/src/__tests__/build-server-bundle.test.ts +0 -228
- package/src/__tests__/classify-change.test.ts +0 -87
- package/src/__tests__/compose-stacks.test.ts +0 -273
- package/src/__tests__/config-resolver-default.integration.test.ts +0 -165
- package/src/__tests__/crash-tracker.test.ts +0 -89
- package/src/__tests__/create-kumiko-server-errors.test.ts +0 -54
- package/src/__tests__/create-kumiko-server-options.test.ts +0 -33
- package/src/__tests__/create-kumiko-server.integration.test.ts +0 -1007
- package/src/__tests__/discover-format.test.ts +0 -32
- package/src/__tests__/env-schema.integration.test.ts +0 -248
- package/src/__tests__/env-schema.test.ts +0 -132
- package/src/__tests__/few-shot-corpus.test.ts +0 -316
- package/src/__tests__/framework-version.test.ts +0 -45
- package/src/__tests__/init-deploy-cli.test.ts +0 -113
- package/src/__tests__/kumiko-schema-check.test.ts +0 -56
- package/src/__tests__/merge-extra-context.test.ts +0 -67
- package/src/__tests__/resolve-stylesheet.test.ts +0 -132
- package/src/__tests__/run-dev-app-boot-validation.test.ts +0 -38
- package/src/__tests__/run-dev-app.integration.test.ts +0 -290
- package/src/__tests__/runtime-classification.test.ts +0 -26
- package/src/__tests__/saas-identity-wire.integration.test.ts +0 -427
- package/src/__tests__/scaffold-app-feature.test.ts +0 -216
- package/src/__tests__/scaffold-app.test.ts +0 -630
- package/src/__tests__/scaffold-demo-tasks.test.ts +0 -47
- package/src/__tests__/scaffold-deploy.test.ts +0 -464
- package/src/__tests__/scaffold-dev-cycle.integration.test.ts +0 -109
- package/src/__tests__/scaffold-feature.test.ts +0 -143
- package/src/__tests__/schema-apply.integration.test.ts +0 -191
- package/src/__tests__/setup-test-stack-from-features.integration.test.ts +0 -30
- package/src/__tests__/setup-test-stack-job-run-logger.integration.test.ts +0 -72
- package/src/__tests__/walkthrough.integration.test.ts +0 -140
- package/src/__tests__/welcome-banner.test.ts +0 -42
- package/src/build-server-bundle.ts +0 -346
- package/src/build.ts +0 -28
- package/src/codegen/__tests__/render-codegen.test.ts +0 -100
- package/src/codegen/__tests__/run-codegen.test.ts +0 -665
- package/src/codegen/__tests__/strict-mode-diagnostics.test.ts +0 -347
- package/src/codegen/__tests__/watch.test.ts +0 -222
- package/src/codegen/index.ts +0 -19
- package/src/codegen/render.ts +0 -305
- package/src/codegen/run-codegen.ts +0 -278
- package/src/codegen/scan-events.ts +0 -565
- package/src/codegen/watch.ts +0 -140
- package/src/compose-stacks.ts +0 -193
- package/src/crash-tracker.ts +0 -56
- package/src/create-kumiko-server.ts +0 -1397
- package/src/env-schema.ts +0 -72
- package/src/few-shot-corpus.ts +0 -370
- package/src/framework-version.ts +0 -39
- package/src/index.ts +0 -105
- package/src/init-deploy-cli.ts +0 -148
- package/src/run-dev-app.ts +0 -694
- package/src/scaffold-app-feature.ts +0 -283
- package/src/scaffold-app.ts +0 -1033
- package/src/scaffold-deploy.ts +0 -393
- package/src/scaffold-feature.ts +0 -226
- package/src/schema-apply.ts +0 -123
- package/src/setup-test-stack-from-features.ts +0 -80
- package/src/welcome-banner.ts +0 -43
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
/** Canonical demo feature — keep in sync with `renderDemoTasksFeatureFile()`. */
|
|
3
|
+
export declare function createDemoTasksFeature(): FeatureDefinition;
|
|
4
|
+
export declare function renderDemoTasksFeatureFile(): string;
|
|
5
|
+
export declare function renderDemoTasksI18n(): string;
|
|
6
|
+
export declare function renderDemoTasksWebIndex(): string;
|
|
7
|
+
export declare function renderDemoTasksIndex(): string;
|
|
8
|
+
export declare function renderDemoSeedFile(): string;
|
|
@@ -1,102 +1,77 @@
|
|
|
1
1
|
// Demo `tasks` feature + seed for scaffolded apps — wasp-like starter UX.
|
|
2
2
|
// `createDemoTasksFeature()` feeds init-migration generation; the render*
|
|
3
3
|
// functions emit the same shape into the user's repo.
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
createBooleanField,
|
|
7
|
-
createEntity,
|
|
8
|
-
createNumberField,
|
|
9
|
-
createTextField,
|
|
10
|
-
defineEntityCreateHandler,
|
|
11
|
-
defineEntityDeleteHandler,
|
|
12
|
-
defineEntityDetailHandler,
|
|
13
|
-
defineEntityListHandler,
|
|
14
|
-
defineEntityUpdateHandler,
|
|
15
|
-
defineFeature,
|
|
16
|
-
type FeatureDefinition,
|
|
17
|
-
} from "@cosmicdrift/kumiko-framework/engine";
|
|
18
|
-
import type {
|
|
19
|
-
EntityEditScreenDefinition,
|
|
20
|
-
EntityListScreenDefinition,
|
|
21
|
-
} from "@cosmicdrift/kumiko-framework/ui-types";
|
|
22
|
-
|
|
4
|
+
import { createBooleanField, createEntity, createNumberField, createTextField, defineEntityCreateHandler, defineEntityDeleteHandler, defineEntityDetailHandler, defineEntityListHandler, defineEntityUpdateHandler, defineFeature, } from "@cosmicdrift/kumiko-framework/engine";
|
|
23
5
|
const taskEntity = createEntity({
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
6
|
+
fields: {
|
|
7
|
+
title: createTextField({
|
|
8
|
+
required: true,
|
|
9
|
+
sortable: true,
|
|
10
|
+
personal: false,
|
|
11
|
+
reason: "is_business_data",
|
|
12
|
+
}),
|
|
13
|
+
status: createTextField({ sortable: true, personal: false, reason: "is_business_data" }),
|
|
14
|
+
priority: createNumberField(),
|
|
15
|
+
isUrgent: createBooleanField({ default: false }),
|
|
16
|
+
},
|
|
35
17
|
});
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
rowActions: [{ kind: "navigate", id: "edit", label: "Edit", screen: "task-edit" }],
|
|
18
|
+
const listScreen = {
|
|
19
|
+
id: "task-list",
|
|
20
|
+
type: "entityList",
|
|
21
|
+
entity: "task",
|
|
22
|
+
columns: ["title", "status", "isUrgent", "priority"],
|
|
23
|
+
defaultSort: { field: "title", dir: "asc" },
|
|
24
|
+
rowActions: [{ kind: "navigate", id: "edit", label: "Edit", screen: "task-edit" }],
|
|
44
25
|
};
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
},
|
|
26
|
+
const editScreen = {
|
|
27
|
+
id: "task-edit",
|
|
28
|
+
type: "entityEdit",
|
|
29
|
+
entity: "task",
|
|
30
|
+
layout: {
|
|
31
|
+
sections: [{ title: "Task", fields: ["title", "status", "priority", "isUrgent"] }],
|
|
32
|
+
},
|
|
53
33
|
};
|
|
54
|
-
|
|
55
34
|
const open = {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
35
|
+
access: {
|
|
36
|
+
openToAll: {
|
|
37
|
+
reason: "scaffolded demo handler: every signed-in user of the demo tenant manages their tasks",
|
|
38
|
+
},
|
|
60
39
|
},
|
|
61
|
-
|
|
62
|
-
} as const;
|
|
63
|
-
|
|
40
|
+
};
|
|
64
41
|
const TASKS_I18N = {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
|
|
42
|
+
"screen:task-list.title": { de: "Aufgaben", en: "Tasks" },
|
|
43
|
+
"screen:task-edit.title": { de: "Aufgabe", en: "Task" },
|
|
44
|
+
"tasks:nav.tasks": { de: "Aufgaben", en: "Tasks" },
|
|
45
|
+
"tasks:nav.taskNew": { de: "Neue Aufgabe", en: "New task" },
|
|
46
|
+
"tasks:entity:task:field:title": { de: "Titel", en: "Title" },
|
|
47
|
+
"tasks:entity:task:field:status": { de: "Status", en: "Status" },
|
|
48
|
+
"tasks:entity:task:field:priority": { de: "Priorität", en: "Priority" },
|
|
49
|
+
"tasks:entity:task:field:isUrgent": { de: "Dringend", en: "Urgent" },
|
|
50
|
+
};
|
|
75
51
|
/** Canonical demo feature — keep in sync with `renderDemoTasksFeatureFile()`. */
|
|
76
|
-
export function createDemoTasksFeature()
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
52
|
+
export function createDemoTasksFeature() {
|
|
53
|
+
return defineFeature("tasks", (r) => {
|
|
54
|
+
r.translations({ keys: TASKS_I18N });
|
|
55
|
+
r.entity("task", taskEntity);
|
|
56
|
+
r.writeHandler(defineEntityCreateHandler("task", taskEntity, open));
|
|
57
|
+
r.writeHandler(defineEntityUpdateHandler("task", taskEntity, open));
|
|
58
|
+
r.writeHandler(defineEntityDeleteHandler("task", taskEntity, open));
|
|
59
|
+
r.queryHandler(defineEntityListHandler("task", taskEntity, open));
|
|
60
|
+
r.queryHandler(defineEntityDetailHandler("task", taskEntity, open));
|
|
61
|
+
r.screen(listScreen);
|
|
62
|
+
r.screen(editScreen);
|
|
63
|
+
r.nav({ id: "tasks", label: "tasks:nav.tasks", order: 10, screen: "tasks:screen:task-list" });
|
|
64
|
+
r.nav({
|
|
65
|
+
id: "task-new",
|
|
66
|
+
label: "tasks:nav.taskNew",
|
|
67
|
+
parent: "tasks:nav:tasks",
|
|
68
|
+
screen: "tasks:screen:task-edit",
|
|
69
|
+
order: 10,
|
|
70
|
+
});
|
|
94
71
|
});
|
|
95
|
-
});
|
|
96
72
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
return `// Demo tasks feature — scaffolded by \`kumiko new app\`. Edit or replace.
|
|
73
|
+
export function renderDemoTasksFeatureFile() {
|
|
74
|
+
return `// Demo tasks feature — scaffolded by \`kumiko new app\`. Edit or replace.
|
|
100
75
|
// Entity + CRUD handlers + list/edit screens + sidebar nav.
|
|
101
76
|
|
|
102
77
|
import {
|
|
@@ -178,9 +153,8 @@ export const tasksFeature = defineFeature("tasks", (r) => {
|
|
|
178
153
|
});
|
|
179
154
|
`;
|
|
180
155
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
return `// Server-side translation keys for the demo tasks feature.
|
|
156
|
+
export function renderDemoTasksI18n() {
|
|
157
|
+
return `// Server-side translation keys for the demo tasks feature.
|
|
184
158
|
|
|
185
159
|
import type { TranslationsByLocale } from "@cosmicdrift/kumiko-renderer";
|
|
186
160
|
|
|
@@ -219,9 +193,8 @@ export const tasksTranslations: TranslationsByLocale = {
|
|
|
219
193
|
};
|
|
220
194
|
`;
|
|
221
195
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
return `import type { ClientFeatureDefinition } from "@cosmicdrift/kumiko-renderer-web";
|
|
196
|
+
export function renderDemoTasksWebIndex() {
|
|
197
|
+
return `import type { ClientFeatureDefinition } from "@cosmicdrift/kumiko-renderer-web";
|
|
225
198
|
import { tasksTranslations } from "../i18n";
|
|
226
199
|
|
|
227
200
|
export const tasksClient: ClientFeatureDefinition = {
|
|
@@ -230,14 +203,12 @@ export const tasksClient: ClientFeatureDefinition = {
|
|
|
230
203
|
};
|
|
231
204
|
`;
|
|
232
205
|
}
|
|
233
|
-
|
|
234
|
-
export
|
|
235
|
-
return `export { tasksFeature } from "./feature";
|
|
206
|
+
export function renderDemoTasksIndex() {
|
|
207
|
+
return `export { tasksFeature } from "./feature";
|
|
236
208
|
`;
|
|
237
209
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
return `// Demo seed — a few tasks so \`bun dev\` shows a non-empty list.
|
|
210
|
+
export function renderDemoSeedFile() {
|
|
211
|
+
return `// Demo seed — a few tasks so \`bun dev\` shows a non-empty list.
|
|
241
212
|
|
|
242
213
|
import type { SeedFn } from "@cosmicdrift/kumiko-dev-server";
|
|
243
214
|
import { TestUsers } from "@cosmicdrift/kumiko-framework/stack";
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
export type RenderDeployFilesOptions = {
|
|
2
|
+
/** App name, kebab-case (e.g. "publicstatus", "kumiko-studio"). */
|
|
3
|
+
readonly appName: string;
|
|
4
|
+
/** Container port the app listens on. Default 3000. */
|
|
5
|
+
readonly port?: number;
|
|
6
|
+
/** GitHub org for the published image-tag. Default "cosmicdriftgamestudio". */
|
|
7
|
+
readonly githubOrg?: string;
|
|
8
|
+
/** Destination directory (absolute or relative to cwd). The `deploy/`
|
|
9
|
+
* subdir is created inside this. Default: process.cwd(). */
|
|
10
|
+
readonly destination?: string;
|
|
11
|
+
/** Source-tree root for optional-dir detection (seeds/, …). Defaults to
|
|
12
|
+
* `destination`. Lets the caller scaffold into one dir while detecting
|
|
13
|
+
* optional surfaces in another (rare — mostly destination = sourceDir). */
|
|
14
|
+
readonly sourceDir?: string;
|
|
15
|
+
};
|
|
16
|
+
export type ScaffoldDeployOptions = RenderDeployFilesOptions & {
|
|
17
|
+
/** Overwrite existing files instead of skipping them. */
|
|
18
|
+
readonly force?: boolean;
|
|
19
|
+
};
|
|
20
|
+
declare const REGISTRY_CONFIG_FILES: readonly ["bunfig.toml", ".npmrc"];
|
|
21
|
+
export type RegistryConfigFile = (typeof REGISTRY_CONFIG_FILES)[number];
|
|
22
|
+
/** Detected optional-dirs/layout traits in the app's source-tree. Drives
|
|
23
|
+
* which COPY blocks the Dockerfile-template emits. */
|
|
24
|
+
export type ScaffoldDeployDetected = {
|
|
25
|
+
/** ES-Operations seed-migrations (`seeds/`). Required for apps that
|
|
26
|
+
* use the es-ops feature. */
|
|
27
|
+
readonly hasSeeds: boolean;
|
|
28
|
+
/** Private @cosmicdriftgamestudio/* GH-Packages → Dockerfile needs to
|
|
29
|
+
* pass NPM_AUTH_TOKEN as build-arg + re-export it as $GITHUB_TOKEN inside
|
|
30
|
+
* the build-stage (bunfig.toml/.npmrc read $GITHUB_TOKEN). */
|
|
31
|
+
readonly hasPrivateGhPackages: boolean;
|
|
32
|
+
/** A `workspaces` root, or a `file:`/`workspace:`/`link:` dependency spec
|
|
33
|
+
* — bun needs the whole build context on disk to resolve the lockfile,
|
|
34
|
+
* so the Dockerfile does `COPY . .` before `bun install` instead of
|
|
35
|
+
* copying manifests first for layer-cache. */
|
|
36
|
+
readonly installFromFullTree: boolean;
|
|
37
|
+
/** Which of bunfig.toml/.npmrc exist at the source root — copied ahead of
|
|
38
|
+
* `bun install` in the manifests-first install variant. */
|
|
39
|
+
readonly registryConfigFiles: readonly RegistryConfigFile[];
|
|
40
|
+
/** DB user for migrate-step.sh's DATABASE_URL — from
|
|
41
|
+
* `package.json#kumiko.deploy.dbUser`, default = appName. The db name
|
|
42
|
+
* always stays appName regardless of this value. */
|
|
43
|
+
readonly dbUser: string;
|
|
44
|
+
/** How migrate-step.sh locates the compose `_stack` network — from
|
|
45
|
+
* `package.json#kumiko.deploy.stackNetwork`. "discover" (default):
|
|
46
|
+
* today's `docker network ls` heuristic. "directory": the exact name
|
|
47
|
+
* `$(basename "$PWD")_stack`, checked via `docker network inspect`. */
|
|
48
|
+
readonly stackNetwork: "discover" | "directory";
|
|
49
|
+
};
|
|
50
|
+
export type ScaffoldedFile = {
|
|
51
|
+
readonly path: string;
|
|
52
|
+
readonly written: boolean;
|
|
53
|
+
readonly reason?: "exists" | "force";
|
|
54
|
+
};
|
|
55
|
+
export type ScaffoldDeployResult = {
|
|
56
|
+
readonly destination: string;
|
|
57
|
+
readonly files: readonly ScaffoldedFile[];
|
|
58
|
+
/** What scaffoldDeploy detected in the source-tree and used to gate
|
|
59
|
+
* conditional Dockerfile blocks. Surfaced so the CLI can report it
|
|
60
|
+
* ("hasSeeds=true → /app/seeds COPY emitted"). */
|
|
61
|
+
readonly detected: ScaffoldDeployDetected;
|
|
62
|
+
};
|
|
63
|
+
export type RenderedDeployFile = {
|
|
64
|
+
/** Template-relative output filename (e.g. "Dockerfile"). */
|
|
65
|
+
readonly output: string;
|
|
66
|
+
/** Absolute path this would be written to. */
|
|
67
|
+
readonly path: string;
|
|
68
|
+
readonly content: string;
|
|
69
|
+
};
|
|
70
|
+
export type RenderDeployFilesResult = {
|
|
71
|
+
readonly deployDir: string;
|
|
72
|
+
readonly files: readonly RenderedDeployFile[];
|
|
73
|
+
readonly detected: ScaffoldDeployDetected;
|
|
74
|
+
};
|
|
75
|
+
export type DeployDriftEntry = {
|
|
76
|
+
readonly path: string;
|
|
77
|
+
readonly reason: "missing" | "differs";
|
|
78
|
+
};
|
|
79
|
+
export type CheckDeployDriftResult = {
|
|
80
|
+
readonly drifted: readonly DeployDriftEntry[];
|
|
81
|
+
readonly detected: ScaffoldDeployDetected;
|
|
82
|
+
};
|
|
83
|
+
/** Pure: computes the rendered deploy files for `options` without touching
|
|
84
|
+
* disk beyond reading the app's source-tree (package.json, seeds/,
|
|
85
|
+
* registry-config files) and the shipped templates. */
|
|
86
|
+
export declare function renderDeployFiles(options: RenderDeployFilesOptions): RenderDeployFilesResult;
|
|
87
|
+
export declare function scaffoldDeploy(options: ScaffoldDeployOptions): ScaffoldDeployResult;
|
|
88
|
+
/** Read-only: compares the rendered deploy files against what's on disk
|
|
89
|
+
* without writing anything. */
|
|
90
|
+
export declare function checkDeployDrift(options: RenderDeployFilesOptions): CheckDeployDriftResult;
|
|
91
|
+
export declare function render(source: string, subs: Readonly<Record<string, string>>, flags: Readonly<Record<string, boolean>>): string;
|
|
92
|
+
export {};
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
// `kumiko init-deploy` scaffolding helper.
|
|
2
|
+
//
|
|
3
|
+
// Generates `deploy/Dockerfile`, `deploy/Dockerfile.dockerignore`, and
|
|
4
|
+
// `deploy/migrate-step.sh` in the target app from canonical templates
|
|
5
|
+
// shipped with @cosmicdrift/kumiko-dev-server. Substitutes `{{appName}}`,
|
|
6
|
+
// `{{port}}`, `{{githubOrg}}`, `{{installManifests}}` placeholders and the
|
|
7
|
+
// `hasSeeds`/`hasPrivateGhPackages`/`installFromFullTree`/
|
|
8
|
+
// `installFromManifests` block flags detected from the app's source-tree.
|
|
9
|
+
// `renderDeployFiles` is the pure computation (no writes); `scaffoldDeploy`
|
|
10
|
+
// writes it to disk (refuses to overwrite existing files unless
|
|
11
|
+
// `force: true`); `checkDeployDrift` compares it against what's on disk
|
|
12
|
+
// without writing.
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { dirname, join } from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
import * as z from "zod";
|
|
17
|
+
import { isKebabSegment } from "./kebab.js";
|
|
18
|
+
const REGISTRY_CONFIG_FILES = ["bunfig.toml", ".npmrc"];
|
|
19
|
+
const TEMPLATE_FILES = [
|
|
20
|
+
{ template: "Dockerfile.template", output: "Dockerfile" },
|
|
21
|
+
{
|
|
22
|
+
template: "Dockerfile.dockerignore.template",
|
|
23
|
+
output: "Dockerfile.dockerignore",
|
|
24
|
+
},
|
|
25
|
+
{ template: "migrate-step.sh.template", output: "migrate-step.sh" },
|
|
26
|
+
];
|
|
27
|
+
function templatesDir() {
|
|
28
|
+
return join(dirname(fileURLToPath(import.meta.url)), "..", "templates", "deploy");
|
|
29
|
+
}
|
|
30
|
+
/** Pure: computes the rendered deploy files for `options` without touching
|
|
31
|
+
* disk beyond reading the app's source-tree (package.json, seeds/,
|
|
32
|
+
* registry-config files) and the shipped templates. */
|
|
33
|
+
export function renderDeployFiles(options) {
|
|
34
|
+
if (!isKebabSegment(options.appName)) {
|
|
35
|
+
throw new Error(`renderDeployFiles: appName must be kebab-case (a-z, 0-9, -); got "${options.appName}"`);
|
|
36
|
+
}
|
|
37
|
+
const port = options.port ?? 3000;
|
|
38
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
39
|
+
throw new Error(`renderDeployFiles: port must be 1..65535, got ${port}`);
|
|
40
|
+
}
|
|
41
|
+
const githubOrg = options.githubOrg ?? "cosmicdriftgamestudio";
|
|
42
|
+
const destinationRoot = options.destination ?? process.cwd();
|
|
43
|
+
const deployDir = join(destinationRoot, "deploy");
|
|
44
|
+
// Detect optional surfaces in the source-tree so the Dockerfile only
|
|
45
|
+
// emits COPYs for dirs that actually exist. Without this, apps without
|
|
46
|
+
// a `seeds/` directory (e.g. studio) crash in Docker-build with
|
|
47
|
+
// `failed to compute cache key: "/app/seeds": not found`.
|
|
48
|
+
const sourceDir = options.sourceDir ?? destinationRoot;
|
|
49
|
+
const detected = detectOptionalSurfaces(sourceDir, options.appName);
|
|
50
|
+
const installManifests = ["package.json", "bun.lock", ...detected.registryConfigFiles].join(" ");
|
|
51
|
+
const subs = {
|
|
52
|
+
appName: options.appName,
|
|
53
|
+
port: String(port),
|
|
54
|
+
githubOrg,
|
|
55
|
+
installManifests,
|
|
56
|
+
dbUser: detected.dbUser,
|
|
57
|
+
};
|
|
58
|
+
const flags = {
|
|
59
|
+
hasSeeds: detected.hasSeeds,
|
|
60
|
+
hasPrivateGhPackages: detected.hasPrivateGhPackages,
|
|
61
|
+
installFromFullTree: detected.installFromFullTree,
|
|
62
|
+
installFromManifests: !detected.installFromFullTree,
|
|
63
|
+
stackNetworkDiscover: detected.stackNetwork === "discover",
|
|
64
|
+
stackNetworkDirectory: detected.stackNetwork === "directory",
|
|
65
|
+
customDbUser: detected.dbUser !== options.appName,
|
|
66
|
+
};
|
|
67
|
+
const dir = templatesDir();
|
|
68
|
+
const files = TEMPLATE_FILES.map(({ template, output }) => ({
|
|
69
|
+
output,
|
|
70
|
+
path: join(deployDir, output),
|
|
71
|
+
content: render(readFileSync(join(dir, template), "utf-8"), subs, flags),
|
|
72
|
+
}));
|
|
73
|
+
return { deployDir, files, detected };
|
|
74
|
+
}
|
|
75
|
+
export function scaffoldDeploy(options) {
|
|
76
|
+
const { deployDir, files: rendered, detected } = renderDeployFiles(options);
|
|
77
|
+
mkdirSync(deployDir, { recursive: true });
|
|
78
|
+
const files = [];
|
|
79
|
+
for (const { path, content } of rendered) {
|
|
80
|
+
const preExisted = existsSync(path);
|
|
81
|
+
if (preExisted && !options.force) {
|
|
82
|
+
files.push({ path, written: false, reason: "exists" });
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
writeFileSync(path, content);
|
|
86
|
+
// `reason: "force"` only when we actually clobbered a pre-existing
|
|
87
|
+
// file — distinct from a clean first-time write. The existsSync above
|
|
88
|
+
// is captured BEFORE the write so the flag reflects pre-state.
|
|
89
|
+
files.push({
|
|
90
|
+
path,
|
|
91
|
+
written: true,
|
|
92
|
+
...(preExisted && options.force ? { reason: "force" } : {}),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return { destination: deployDir, files, detected };
|
|
96
|
+
}
|
|
97
|
+
/** Read-only: compares the rendered deploy files against what's on disk
|
|
98
|
+
* without writing anything. */
|
|
99
|
+
export function checkDeployDrift(options) {
|
|
100
|
+
const { files: rendered, detected } = renderDeployFiles(options);
|
|
101
|
+
const drifted = [];
|
|
102
|
+
for (const { path, content } of rendered) {
|
|
103
|
+
if (!existsSync(path)) {
|
|
104
|
+
drifted.push({ path, reason: "missing" });
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (readFileSync(path, "utf-8") !== content) {
|
|
108
|
+
drifted.push({ path, reason: "differs" });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return { drifted, detected };
|
|
112
|
+
}
|
|
113
|
+
const packageJsonSchema = z.object({
|
|
114
|
+
dependencies: z.record(z.string(), z.string()).optional(),
|
|
115
|
+
devDependencies: z.record(z.string(), z.string()).optional(),
|
|
116
|
+
workspaces: z
|
|
117
|
+
.union([z.array(z.string()), z.object({ packages: z.array(z.string()).optional() })])
|
|
118
|
+
.optional(),
|
|
119
|
+
});
|
|
120
|
+
function isRecord(value) {
|
|
121
|
+
return typeof value === "object" && value !== null;
|
|
122
|
+
}
|
|
123
|
+
/** Reads `raw.kumiko.deploy` without going through `packageJsonSchema` — an
|
|
124
|
+
* unrelated shape problem elsewhere in package.json (e.g. a malformed
|
|
125
|
+
* `dependencies`) must not silently drop a valid deploy config; only
|
|
126
|
+
* actually-malformed JSON (JSON.parse throwing) should fall back. */
|
|
127
|
+
function extractDeployConfigRaw(raw) {
|
|
128
|
+
if (!isRecord(raw))
|
|
129
|
+
return undefined;
|
|
130
|
+
const kumiko = raw["kumiko"];
|
|
131
|
+
if (!isRecord(kumiko))
|
|
132
|
+
return undefined;
|
|
133
|
+
return kumiko["deploy"];
|
|
134
|
+
}
|
|
135
|
+
const LOCAL_DEP_SPEC_RE = /^(file:|workspace:|link:)/;
|
|
136
|
+
// The user part of DATABASE_URL — deliberately stricter than a full Postgres
|
|
137
|
+
// role-name grammar (no quoting support) since it is interpolated into a
|
|
138
|
+
// shell string, not passed through a driver's escaping.
|
|
139
|
+
const DB_USER_RE = /^[a-zA-Z0-9_][a-zA-Z0-9_-]{0,62}$/;
|
|
140
|
+
const kumikoDeployConfigSchema = z
|
|
141
|
+
.object({
|
|
142
|
+
dbUser: z.string().regex(DB_USER_RE).optional(),
|
|
143
|
+
stackNetwork: z.enum(["discover", "directory"]).optional(),
|
|
144
|
+
})
|
|
145
|
+
.strict();
|
|
146
|
+
/** `package.json#kumiko.deploy` must be well-formed — silently falling back
|
|
147
|
+
* to defaults on an invalid `dbUser`/`stackNetwork` would render a migrate
|
|
148
|
+
* step that talks to the wrong DB user or the wrong stack network, breaking
|
|
149
|
+
* prod migrations without anyone noticing. Unlike detectOptionalSurfaces'
|
|
150
|
+
* malformed-JSON fallback, this throws (fail loud) and names the field. */
|
|
151
|
+
function parseDeployConfig(raw) {
|
|
152
|
+
if (raw === undefined)
|
|
153
|
+
return {};
|
|
154
|
+
const result = kumikoDeployConfigSchema.safeParse(raw);
|
|
155
|
+
if (result.success)
|
|
156
|
+
return result.data;
|
|
157
|
+
const issue = result.error.issues[0];
|
|
158
|
+
const location = issue === undefined
|
|
159
|
+
? "kumiko.deploy"
|
|
160
|
+
: issue.code === "unrecognized_keys"
|
|
161
|
+
? ["kumiko", "deploy", ...issue.keys].join(".")
|
|
162
|
+
: ["kumiko", "deploy", ...issue.path.map(String)].join(".");
|
|
163
|
+
throw new Error(`scaffoldDeploy: invalid package.json#${location} — ${issue?.message ?? "validation failed"}`);
|
|
164
|
+
}
|
|
165
|
+
function resolveDeployConfig(deployConfigRaw, appName) {
|
|
166
|
+
const config = parseDeployConfig(deployConfigRaw);
|
|
167
|
+
const dbUser = config.dbUser ?? appName;
|
|
168
|
+
// The default (appName) already passed isKebabSegment's charset check but
|
|
169
|
+
// not DB_USER_RE's length cap — validate the effective value so an
|
|
170
|
+
// over-long appName fails loud here instead of producing a DB user
|
|
171
|
+
// Postgres itself would reject at migrate-time.
|
|
172
|
+
if (!DB_USER_RE.test(dbUser)) {
|
|
173
|
+
throw new Error(`scaffoldDeploy: invalid package.json#kumiko.deploy.dbUser — effective value "${dbUser}" (defaulted from appName) does not match ${DB_USER_RE}`);
|
|
174
|
+
}
|
|
175
|
+
return { dbUser, stackNetwork: config.stackNetwork ?? "discover" };
|
|
176
|
+
}
|
|
177
|
+
function detectOptionalSurfaces(sourceDir, appName) {
|
|
178
|
+
const hasSeeds = existsSync(join(sourceDir, "seeds"));
|
|
179
|
+
let hasPrivateGhPackages = false;
|
|
180
|
+
let installFromFullTree = false;
|
|
181
|
+
let deployConfigRaw;
|
|
182
|
+
const pkgJsonPath = join(sourceDir, "package.json");
|
|
183
|
+
if (existsSync(pkgJsonPath)) {
|
|
184
|
+
try {
|
|
185
|
+
const raw = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
|
|
186
|
+
// Read ahead of packageJsonSchema.parse: an unrelated shape problem in
|
|
187
|
+
// `dependencies`/`workspaces` must not silently discard a valid deploy
|
|
188
|
+
// config along with it (that's caught by the outer catch below).
|
|
189
|
+
deployConfigRaw = extractDeployConfigRaw(raw);
|
|
190
|
+
const pkg = packageJsonSchema.parse(raw);
|
|
191
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
192
|
+
hasPrivateGhPackages = Object.keys(allDeps).some((d) => d.startsWith("@cosmicdriftgamestudio/"));
|
|
193
|
+
installFromFullTree =
|
|
194
|
+
pkg.workspaces !== undefined ||
|
|
195
|
+
Object.values(allDeps).some((v) => LOCAL_DEP_SPEC_RE.test(v));
|
|
196
|
+
}
|
|
197
|
+
catch (err) {
|
|
198
|
+
// malformed JSON (or unexpected dependencies/workspaces shape) —
|
|
199
|
+
// assume no private packages and a plain manifests-first install;
|
|
200
|
+
// app-author can override via Dockerfile. Warn so a silent
|
|
201
|
+
// mis-detection (later YN0041 on yarn install, or a broken lockfile
|
|
202
|
+
// resolve) is traceable to the scaffold step. If JSON.parse itself
|
|
203
|
+
// failed, deployConfigRaw also stays undefined → deploy params fall
|
|
204
|
+
// back to defaults (an invalid deploy config must THROW, but
|
|
205
|
+
// unreadable JSON can't distinguish "no deploy config" from "invalid
|
|
206
|
+
// deploy config" — it already surfaces via this warning).
|
|
207
|
+
// biome-ignore lint/suspicious/noConsole: scaffold visibility for skipped private-package detection
|
|
208
|
+
console.warn(`scaffoldDeploy: package.json at ${pkgJsonPath} is not valid JSON — private-GH-packages/install-layout detection skipped (${err instanceof Error ? err.message : String(err)})`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const registryConfigFiles = REGISTRY_CONFIG_FILES.filter((f) => existsSync(join(sourceDir, f)));
|
|
212
|
+
return {
|
|
213
|
+
hasSeeds,
|
|
214
|
+
hasPrivateGhPackages,
|
|
215
|
+
installFromFullTree,
|
|
216
|
+
registryConfigFiles,
|
|
217
|
+
...resolveDeployConfig(deployConfigRaw, appName),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
// Unconsumed-mustache guard. After step 1+2 the only legitimate `{{`
|
|
221
|
+
// survivors are Docker/Go template tags (`{{.Name}}`, leading `.`). An
|
|
222
|
+
// orphan close-tag (`{{/foo}}` without an opener) matches neither step and
|
|
223
|
+
// would otherwise leak into the rendered file as a silent failure.
|
|
224
|
+
const ORPHAN_MUSTACHE_RE = /\{\{(?!\.)[^}]*\}\}/;
|
|
225
|
+
export function render(source, subs, flags) {
|
|
226
|
+
// Step 1: handle mustache-style block conditionals `{{#flag}}...{{/flag}}`
|
|
227
|
+
// (multiline-aware via [\s\S]). When flag is truthy → keep inner content;
|
|
228
|
+
// when falsy → strip the entire block (including the surrounding line so
|
|
229
|
+
// we don't leave blank lines in the rendered Dockerfile).
|
|
230
|
+
let result = source.replace(/^[ \t]*\{\{#([a-z][a-zA-Z0-9]*)\}\}\n([\s\S]*?)\n[ \t]*\{\{\/\1\}\}[ \t]*\n?/gm, (_full, key, inner) => {
|
|
231
|
+
const flag = flags[key];
|
|
232
|
+
if (flag === undefined) {
|
|
233
|
+
throw new Error(`scaffoldDeploy.render: unknown block-flag "{{#${key}}}"`);
|
|
234
|
+
}
|
|
235
|
+
return flag ? `${inner}\n` : "";
|
|
236
|
+
});
|
|
237
|
+
// Step 2: handle plain `{{key}}` substitutions. Pattern is intentionally
|
|
238
|
+
// narrow: lowercase-leading identifier followed by alphanumerics. That
|
|
239
|
+
// excludes Docker/Go template syntax like `{{.Name}}` (leading `.`)
|
|
240
|
+
// which appears verbatim in the migrate-step.sh shell snippet.
|
|
241
|
+
result = result.replace(/\{\{([a-z][a-zA-Z0-9]*)\}\}/g, (full, key) => {
|
|
242
|
+
const value = subs[key];
|
|
243
|
+
if (value === undefined) {
|
|
244
|
+
throw new Error(`scaffoldDeploy.render: unknown placeholder "${full}"`);
|
|
245
|
+
}
|
|
246
|
+
return value;
|
|
247
|
+
});
|
|
248
|
+
const orphan = ORPHAN_MUSTACHE_RE.exec(result);
|
|
249
|
+
if (orphan) {
|
|
250
|
+
throw new Error(`scaffoldDeploy.render: unconsumed mustache tag "${orphan[0]}" — ` +
|
|
251
|
+
`likely an orphan close-tag (e.g. a stray "{{/flag}}") in the template.`);
|
|
252
|
+
}
|
|
253
|
+
return result;
|
|
254
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type ScaffoldFeatureOptions = {
|
|
2
|
+
/** camelCase feature name. Must be a valid JS identifier. */
|
|
3
|
+
readonly name: string;
|
|
4
|
+
/**
|
|
5
|
+
* Absolute or repo-relative path where the feature workspace gets
|
|
6
|
+
* created. Defaults to `samples/recipes/<kebab-name>/` under the
|
|
7
|
+
* resolved repo root.
|
|
8
|
+
*/
|
|
9
|
+
readonly destination?: string;
|
|
10
|
+
/** Repo root used to resolve the default destination. Defaults to cwd. */
|
|
11
|
+
readonly repoRoot?: string;
|
|
12
|
+
};
|
|
13
|
+
export type ScaffoldFeatureResult = {
|
|
14
|
+
readonly destination: string;
|
|
15
|
+
readonly featureFile: string;
|
|
16
|
+
readonly packageJsonFile: string;
|
|
17
|
+
readonly tsconfigFile: string;
|
|
18
|
+
readonly featureName: string;
|
|
19
|
+
readonly packageName: string;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Generate a starter feature workspace at `destination`. Throws when
|
|
23
|
+
* the destination already exists — refuses to overwrite. The caller is
|
|
24
|
+
* expected to run `bun install` afterwards to wire the workspace.
|
|
25
|
+
*/
|
|
26
|
+
export declare function scaffoldFeature(options: ScaffoldFeatureOptions): ScaffoldFeatureResult;
|