@cosmicdrift/kumiko-dev-server 0.373.0 → 0.377.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.
Files changed (34) hide show
  1. package/dist/compose-stacks.d.ts +3 -0
  2. package/dist/compose-stacks.js +11 -4
  3. package/dist/create-kumiko-server.d.ts +4 -2
  4. package/dist/create-kumiko-server.js +23 -25
  5. package/dist/managed-files/app-jobs.d.ts +4 -0
  6. package/dist/managed-files/app-jobs.js +68 -0
  7. package/dist/managed-files/check.d.ts +2 -0
  8. package/dist/managed-files/check.js +4 -2
  9. package/dist/managed-files/index.d.ts +2 -4
  10. package/dist/managed-files/index.js +2 -4
  11. package/dist/managed-files/registry.d.ts +7 -1
  12. package/dist/managed-files/registry.js +37 -5
  13. package/dist/managed-files/sync.js +10 -1
  14. package/dist/run-dev-app.d.ts +26 -10
  15. package/dist/run-dev-app.js +108 -37
  16. package/dist/scaffold-app-feature.js +1 -1
  17. package/dist/scaffold-app.d.ts +3 -0
  18. package/dist/scaffold-app.js +6 -7
  19. package/dist/scaffold-demo-tasks.js +1 -1
  20. package/dist/scaffold-deploy.d.ts +6 -0
  21. package/dist/scaffold-deploy.js +16 -1
  22. package/dist/testing.d.ts +17 -0
  23. package/dist/testing.js +46 -0
  24. package/package.json +19 -10
  25. package/src/changes.json +18 -0
  26. package/templates/managed/ci-public.yml.template +134 -0
  27. package/templates/managed/ci.yml.template +9 -6
  28. package/templates/managed/husky-pre-push.template +1 -7
  29. package/templates/managed/kumiko-schema.ts.template +16 -1
  30. package/templates/managed/release.yml.template +17 -1
  31. package/dist/managed-files/port-registry.d.ts +0 -17
  32. package/dist/managed-files/port-registry.js +0 -31
  33. package/dist/managed-files/static-app-config.d.ts +0 -16
  34. package/dist/managed-files/static-app-config.js +0 -99
@@ -0,0 +1,134 @@
1
+ # CI for {{appName}}: a public repo cannot call the private reusable workflows in
2
+ # cosmicdriftgamestudio/infra, so every job here is self-contained on GitHub-hosted
3
+ # runners. The rollout is manual: see release.yml. Jobs only this app needs: name
4
+ # them `app-<name>` in this file (they survive `kumiko sync-managed`).
5
+
6
+ name: CI
7
+
8
+ on:
9
+ push:
10
+ branches: [main]
11
+ pull_request:
12
+ branches: [main]
13
+
14
+ concurrency:
15
+ group: ci-${{ github.ref }}
16
+ cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
17
+
18
+ env:
19
+ HUSKY: "0"
20
+
21
+ jobs:
22
+ test:
23
+ if: github.event_name == 'pull_request'
24
+ runs-on: ubuntu-latest
25
+ timeout-minutes: 20
26
+ permissions:
27
+ contents: read
28
+ steps:
29
+ - uses: actions/checkout@v7
30
+ with:
31
+ fetch-depth: 0
32
+ - uses: oven-sh/setup-bun@v2
33
+ with:
34
+ bun-version: "1.4.0"
35
+ - run: bun install --frozen-lockfile
36
+ - run: bun run typecheck
37
+ - run: bun run lint
38
+ - run: bun kumiko check
39
+ - name: Schema + boot validate
40
+ env:
41
+ FILE_STORAGE_PROVIDER: "inmemory"
42
+ run: bun kumiko-schema validate
43
+ {{#uiGuards}}
44
+ - name: UI guards
45
+ run: bun kumiko-guards ui
46
+ {{/uiGuards}}
47
+ - run: bun run test
48
+
49
+ integration:
50
+ if: github.event_name == 'pull_request'
51
+ runs-on: ubuntu-latest
52
+ timeout-minutes: 30
53
+ permissions:
54
+ contents: read
55
+ services:
56
+ postgres:
57
+ image: postgres:18.4-alpine
58
+ env:
59
+ POSTGRES_USER: kumiko
60
+ POSTGRES_PASSWORD: kumiko
61
+ POSTGRES_DB: kumiko_dev
62
+ ports:
63
+ - 15432:5432
64
+ options: >-
65
+ --health-cmd "pg_isready -U kumiko -d kumiko_dev"
66
+ --health-interval 2s --health-timeout 3s --health-retries 30
67
+ redis:
68
+ image: redis:8-alpine
69
+ ports:
70
+ - 16379:6379
71
+ {{#meilisearch}}
72
+ meilisearch:
73
+ image: getmeili/meilisearch:v1.12
74
+ env:
75
+ MEILI_MASTER_KEY: kumiko-dev-key
76
+ MEILI_ENV: development
77
+ ports:
78
+ - 17700:7700
79
+ {{/meilisearch}}
80
+ steps:
81
+ - uses: actions/checkout@v7
82
+ - uses: oven-sh/setup-bun@v2
83
+ with:
84
+ bun-version: "1.4.0"
85
+ - run: bun install --frozen-lockfile
86
+ - name: Create test database
87
+ env:
88
+ PGPASSWORD: kumiko
89
+ run: psql -h localhost -p 15432 -U kumiko -d kumiko_dev -c "CREATE DATABASE kumiko_test;"
90
+ - name: Run integration tests
91
+ env:
92
+ NODE_ENV: test
93
+ KUMIKO_INSTANCE_ID: test-instance
94
+ DATABASE_URL: postgresql://kumiko:kumiko@localhost:15432/kumiko_dev
95
+ TEST_DATABASE_URL: postgresql://kumiko:kumiko@localhost:15432/kumiko_test
96
+ REDIS_URL: redis://localhost:16379
97
+ JWT_SECRET: testtesttesttesttesttesttesttest
98
+ run: bun run test:integration
99
+
100
+ # Build on main only; the rollout runs through the manual release workflow.
101
+ build:
102
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
103
+ runs-on: ubuntu-26.04-arm
104
+ timeout-minutes: 30
105
+ permissions:
106
+ contents: read
107
+ packages: write
108
+ steps:
109
+ - uses: actions/checkout@v7
110
+ - uses: docker/setup-buildx-action@v4
111
+ - uses: docker/login-action@v4
112
+ with:
113
+ registry: ghcr.io
114
+ username: ${{ github.actor }}
115
+ password: ${{ secrets.GITHUB_TOKEN }}
116
+ - id: meta
117
+ uses: docker/metadata-action@v6
118
+ with:
119
+ images: ghcr.io/cosmicdriftgamestudio/{{appName}}
120
+ tags: |
121
+ type=raw,value=latest
122
+ type=sha,prefix=sha-,format=short
123
+ - uses: docker/build-push-action@v7
124
+ with:
125
+ context: .
126
+ file: deploy/Dockerfile
127
+ push: true
128
+ tags: ${{ steps.meta.outputs.tags }}
129
+ labels: ${{ steps.meta.outputs.labels }}
130
+ cache-from: type=gha
131
+ cache-to: type=gha,mode=max
132
+ platforms: linux/arm64
133
+ build-args: |
134
+ BUILD_VERSION=${{ github.sha }}
@@ -1,6 +1,6 @@
1
1
  # CI for {{appName}}: a thin orchestrator over the reusable workflows in
2
- # cosmicdriftgamestudio/infra. Jobs only this app needs go into an unmanaged
3
- # workflow next to this one (e.g. ci-extra.yml).
2
+ # cosmicdriftgamestudio/infra. Jobs only this app needs: name them `app-<name>`
3
+ # in this file (they survive `kumiko sync-managed`) or use an unmanaged workflow.
4
4
 
5
5
  name: CI
6
6
 
@@ -19,16 +19,19 @@ env:
19
19
 
20
20
  jobs:
21
21
  test:
22
- uses: cosmicdriftgamestudio/infra/.github/workflows/_app-test.yml@main
22
+ uses: cosmicdriftgamestudio/infra/.github/workflows/_app-test.yml@{{infraRef}}
23
23
  with:
24
24
  ui-guards: {{uiGuards}}
25
25
  # The boot gate in kumiko-schema validate demands a provider for file
26
26
  # fields; CI only checks that one is configured.
27
27
  file-storage-provider: "inmemory"
28
+ {{#docStatusGuardOff}}
29
+ doc-status-guard: false
30
+ {{/docStatusGuardOff}}
28
31
  secrets: inherit
29
32
 
30
33
  integration:
31
- uses: cosmicdriftgamestudio/infra/.github/workflows/_app-integration.yml@main
34
+ uses: cosmicdriftgamestudio/infra/.github/workflows/_app-integration.yml@{{infraRef}}
32
35
  {{#minio}}
33
36
  with:
34
37
  minio: true
@@ -39,7 +42,7 @@ jobs:
39
42
  # otherwise never report and block the merge.
40
43
  e2e:
41
44
  if: contains(fromJSON('["pull_request","merge_group"]'), github.event_name)
42
- uses: cosmicdriftgamestudio/infra/.github/workflows/_app-e2e.yml@main
45
+ uses: cosmicdriftgamestudio/infra/.github/workflows/_app-e2e.yml@{{infraRef}}
43
46
  {{#e2eInputs}}
44
47
  with:
45
48
  {{/e2eInputs}}
@@ -58,7 +61,7 @@ jobs:
58
61
  permissions:
59
62
  contents: read
60
63
  packages: write
61
- uses: cosmicdriftgamestudio/infra/.github/workflows/_docker-build-push.yml@main
64
+ uses: cosmicdriftgamestudio/infra/.github/workflows/_docker-build-push.yml@{{infraRef}}
62
65
  with:
63
66
  image-name: {{appName}}
64
67
  dockerfile: deploy/Dockerfile
@@ -1,10 +1,4 @@
1
1
  #!/usr/bin/env sh
2
- # PRE_PUSH_SKIP is checked here too so the bypass works even without node_modules.
3
- if [ "${PRE_PUSH_SKIP:-0}" = "1" ]; then
4
- echo "[pre-push] PRE_PUSH_SKIP=1 — hook skipped"
5
- exit 0
6
- fi
7
-
8
2
  REPO_ROOT="$(git rev-parse --show-toplevel)"
9
3
  if [ -z "$REPO_ROOT" ]; then
10
4
  echo "[pre-push] FATAL: git rev-parse --show-toplevel failed — refusing push" >&2
@@ -33,7 +27,7 @@ done
33
27
 
34
28
  if [ -z "$BIN" ]; then
35
29
  echo "[pre-push] kumiko-pre-push not found in any node_modules/.bin above $REPO_ROOT." >&2
36
- echo "Run \`bun install\`, or set PRE_PUSH_SKIP=1 to bypass this hook." >&2
30
+ echo "Run \`bun install\`." >&2
37
31
  exit 1
38
32
  fi
39
33
 
@@ -5,9 +5,24 @@
5
5
  import { allAppFeatures } from "@cosmicdrift/kumiko-framework/config";
6
6
  import { collectTableMetas, type EntityTableMeta } from "@cosmicdrift/kumiko-framework/db";
7
7
  import type { FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
8
+ import { composeFeatures } from "@cosmicdrift/kumiko-server-runtime/compose-features";
8
9
  import appConfig from "../kumiko.config";
9
10
 
10
- export const FEATURES: readonly FeatureDefinition[] = allAppFeatures(appConfig);
11
+ const CONFIGURED_FEATURES = allAppFeatures(appConfig);
12
+
13
+ // Auth apps mount config/user/tenant/auth-email-password through composeFeatures at boot;
14
+ // without them here the generated schema would miss their tables. Composing an already
15
+ // composed list is a no-op (app-side duplicates of bundled names are dropped).
16
+ // kumiko.config.ts `includeBundled` wins; otherwise an auth-foundation or sessions feature marks an auth app.
17
+ const HAS_AUTH =
18
+ appConfig.includeBundled ??
19
+ CONFIGURED_FEATURES.some(
20
+ (feature) => feature.name === "auth-foundation" || feature.name === "sessions",
21
+ );
22
+
23
+ export const FEATURES: readonly FeatureDefinition[] = composeFeatures([...CONFIGURED_FEATURES], {
24
+ includeBundled: HAS_AUTH,
25
+ });
11
26
 
12
27
  // `kumiko check` validates the boot with the options kumiko.config.ts declares.
13
28
  export const VALIDATE_BOOT_OPTIONS = appConfig.validateBootOptions;
@@ -44,12 +44,28 @@ jobs:
44
44
  exit 1
45
45
  fi
46
46
 
47
+ {{#privateDeploy}}
47
48
  deploy:
48
49
  needs: verify
49
- uses: cosmicdriftgamestudio/infra/.github/workflows/_app-deploy.yml@main
50
+ uses: cosmicdriftgamestudio/infra/.github/workflows/_app-deploy.yml@{{infraRef}}
50
51
  with:
51
52
  namespace: {{namespace}}
52
53
  deployment: {{deployment}}
53
54
  image-name: {{appName}}
54
55
  git_sha: ${{ inputs.sha }}
55
56
  secrets: inherit
57
+ {{/privateDeploy}}
58
+ {{#publicRollout}}
59
+ # A public repo cannot call the private infra workflows; the rollout runs from infra.
60
+ rollout-hint:
61
+ needs: verify
62
+ runs-on: ubuntu-latest
63
+ timeout-minutes: 5
64
+ steps:
65
+ - name: Print rollout command
66
+ env:
67
+ SHA: ${{ inputs.sha }}
68
+ run: |
69
+ echo "SHA verified. Roll out from a machine with access to the infra repo:"
70
+ echo "gh workflow run public-app-rollout.yml --repo cosmicdriftgamestudio/infra -f app={{appName}} -f git_sha=$SHA"
71
+ {{/publicRollout}}
@@ -1,17 +0,0 @@
1
- import { type AppPortRole } from "@cosmicdrift/kumiko-framework/config";
2
- import { type StaticAppConfig } from "./static-app-config.ts";
3
- export type ScannedApp = {
4
- readonly app: string;
5
- readonly config: StaticAppConfig;
6
- };
7
- export type PortClaim = {
8
- readonly app: string;
9
- readonly role: AppPortRole;
10
- };
11
- export type PortCollision = {
12
- readonly port: number;
13
- readonly claims: readonly PortClaim[];
14
- };
15
- /** Direct children of `root` with a kumiko.config.ts; worktrees and dot-dirs are skipped. */
16
- export declare function scanWorkspaceApps(root: string): readonly ScannedApp[];
17
- export declare function findPortCollisions(apps: readonly ScannedApp[]): readonly PortCollision[];
@@ -1,31 +0,0 @@
1
- // Port registry across the apps of a workspace: every app declares its ports
2
- // in kumiko.config.ts, and two apps (or two roles of one app) must never claim
3
- // the same port, otherwise their local dev and E2E runs block each other.
4
- import { existsSync, readdirSync } from "node:fs";
5
- import { join } from "node:path";
6
- import { APP_CONFIG_FILE, listAppPorts, } from "@cosmicdrift/kumiko-framework/config";
7
- import { readStaticAppConfig } from "./static-app-config.js";
8
- /** Direct children of `root` with a kumiko.config.ts; worktrees and dot-dirs are skipped. */
9
- export function scanWorkspaceApps(root) {
10
- return readdirSync(root, { withFileTypes: true })
11
- .filter((entry) => entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules")
12
- .filter((entry) => existsSync(join(root, entry.name, APP_CONFIG_FILE)))
13
- .map((entry) => ({ app: entry.name, config: readStaticAppConfig(join(root, entry.name)) }))
14
- .sort((a, b) => a.app.localeCompare(b.app));
15
- }
16
- export function findPortCollisions(apps) {
17
- const claimsByPort = new Map();
18
- for (const { app, config } of apps) {
19
- if (config.kind !== "defined")
20
- continue;
21
- for (const { role, port } of listAppPorts(config.ports)) {
22
- const claims = claimsByPort.get(port) ?? [];
23
- claims.push({ app, role });
24
- claimsByPort.set(port, claims);
25
- }
26
- }
27
- return [...claimsByPort.entries()]
28
- .filter(([, claims]) => claims.length > 1)
29
- .map(([port, claims]) => ({ port, claims }))
30
- .sort((a, b) => a.port - b.port);
31
- }
@@ -1,16 +0,0 @@
1
- import { type AppPorts } from "@cosmicdrift/kumiko-framework/config";
2
- export type StaticAppConfig = {
3
- readonly kind: "missing";
4
- }
5
- /** A config without defineAppConfig: the app has not adopted the standard yet. */
6
- | {
7
- readonly kind: "legacy";
8
- } | {
9
- readonly kind: "invalid";
10
- readonly message: string;
11
- } | {
12
- readonly kind: "defined";
13
- readonly ports: AppPorts;
14
- };
15
- export declare function parseStaticAppConfig(source: string): StaticAppConfig;
16
- export declare function readStaticAppConfig(appRoot: string): StaticAppConfig;
@@ -1,99 +0,0 @@
1
- // Reads the ports out of kumiko.config.ts without importing it: importing
2
- // would execute every feature module of the app, and the port registry scans
3
- // sibling apps whose dependencies may not even be installed.
4
- import { existsSync, readFileSync } from "node:fs";
5
- import { join } from "node:path";
6
- import { APP_CONFIG_FILE, findAppPortProblems, } from "@cosmicdrift/kumiko-framework/config";
7
- import { Project, SyntaxKind } from "ts-morph";
8
- const INFRA_KEYS = [
9
- "postgres",
10
- "redis",
11
- "meilisearch",
12
- "s3",
13
- ];
14
- function isInfraKey(name) {
15
- return INFRA_KEYS.some((key) => key === name);
16
- }
17
- class StaticConfigError extends Error {
18
- }
19
- function propertyName(node) {
20
- if (node.isKind(SyntaxKind.PropertyAssignment))
21
- return node.getName().replace(/^["']|["']$/g, "");
22
- return undefined;
23
- }
24
- function objectProperty(object, name) {
25
- for (const property of object.getProperties()) {
26
- if (property.isKind(SyntaxKind.SpreadAssignment) ||
27
- property.isKind(SyntaxKind.ShorthandPropertyAssignment)) {
28
- throw new StaticConfigError(`${name}: spread and shorthand properties cannot be read statically`);
29
- }
30
- if (propertyName(property) === name && property.isKind(SyntaxKind.PropertyAssignment)) {
31
- return property.getInitializerOrThrow();
32
- }
33
- }
34
- return undefined;
35
- }
36
- function numericLiteral(node, path) {
37
- if (node === undefined)
38
- throw new StaticConfigError(`${path} is missing`);
39
- if (!node.isKind(SyntaxKind.NumericLiteral)) {
40
- throw new StaticConfigError(`${path} must be a number literal so tooling can read it, got "${node.getText()}"`);
41
- }
42
- return node.getLiteralValue();
43
- }
44
- function objectLiteral(node, path) {
45
- if (node === undefined)
46
- throw new StaticConfigError(`${path} is missing`);
47
- if (!node.isKind(SyntaxKind.ObjectLiteralExpression)) {
48
- throw new StaticConfigError(`${path} must be an object literal, got "${node.getText()}"`);
49
- }
50
- return node;
51
- }
52
- function readInfraPorts(node) {
53
- if (node === undefined)
54
- return undefined;
55
- const object = objectLiteral(node, "ports.infra");
56
- const infra = {};
57
- for (const property of object.getProperties()) {
58
- const name = propertyName(property);
59
- if (name === undefined || !isInfraKey(name)) {
60
- throw new StaticConfigError(`ports.infra: unknown entry "${property.getText()}", expected ${INFRA_KEYS.join(", ")}`);
61
- }
62
- infra[name] = numericLiteral(objectProperty(object, name), `ports.infra.${name}`);
63
- }
64
- return infra;
65
- }
66
- export function parseStaticAppConfig(source) {
67
- const project = new Project({ useInMemoryFileSystem: true, skipFileDependencyResolution: true });
68
- const file = project.createSourceFile(APP_CONFIG_FILE, source);
69
- const call = file
70
- .getDescendantsOfKind(SyntaxKind.CallExpression)
71
- .find((candidate) => candidate.getExpression().getText() === "defineAppConfig");
72
- if (call === undefined)
73
- return { kind: "legacy" };
74
- try {
75
- const config = objectLiteral(call.getArguments()[0], "defineAppConfig argument");
76
- const ports = objectLiteral(objectProperty(config, "ports"), "ports");
77
- const infra = readInfraPorts(objectProperty(ports, "infra"));
78
- const parsed = {
79
- dev: numericLiteral(objectProperty(ports, "dev"), "ports.dev"),
80
- e2e: numericLiteral(objectProperty(ports, "e2e"), "ports.e2e"),
81
- ...(infra === undefined ? {} : { infra }),
82
- };
83
- const problems = findAppPortProblems(parsed);
84
- if (problems.length > 0)
85
- return { kind: "invalid", message: problems.join("; ") };
86
- return { kind: "defined", ports: parsed };
87
- }
88
- catch (error) {
89
- if (error instanceof StaticConfigError)
90
- return { kind: "invalid", message: error.message };
91
- throw error;
92
- }
93
- }
94
- export function readStaticAppConfig(appRoot) {
95
- const path = join(appRoot, APP_CONFIG_FILE);
96
- if (!existsSync(path))
97
- return { kind: "missing" };
98
- return parseStaticAppConfig(readFileSync(path, "utf-8"));
99
- }