@norskvideo/ctl-dev-kit 0.1.52 → 0.1.53

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.
@@ -2,7 +2,8 @@
2
2
  // shared conventions have diverged from this dev-kit's canonical source. Each
3
3
  // gated file uses the loosest mechanism that still pins what is shared:
4
4
  // - byte-verbatim: CLAUDE.md fenced core, flake.nix, biome.json,
5
- // tsconfig.base.json, the .gitignore core block
5
+ // tsconfig.base.json, the .gitignore core block, scripts/demo (only a
6
+ // product with tests/demo.spec.ts is asked for it)
6
7
  // - masked per-repo line: checks.yml + upgrade-latest.yml (`product:` key)
7
8
  // - base + sanctioned extension block: dprint.json (repo-specific excludes)
8
9
  // - structural: root tsconfig.json (shape-dependent includes stay free),
@@ -16,7 +17,7 @@
16
17
  // dev-kit forces a re-sync. Canonical bytes ship alongside this script
17
18
  // (conventions/* + build/*), resolved package-relative so they work both
18
19
  // workspace-symlinked and installed from the published tarball.
19
- import { existsSync, readFileSync } from "node:fs";
20
+ import { existsSync, readFileSync, statSync } from "node:fs";
20
21
  import { join } from "node:path";
21
22
  import { parseManifestSeed } from "@norskvideo/ctl-sdk/manifest-seed";
22
23
 
@@ -245,6 +246,56 @@ export interface CanonicalBytes {
245
246
  gitignoreCore: string;
246
247
  buildImage: string;
247
248
  smoke: string;
249
+ /** conventions/demo.sh — the scripts/demo shim a product with tests/demo.spec.ts carries. */
250
+ demoShim: string;
251
+ }
252
+
253
+ // 05-demo s6: a product that has a demo (tests/demo.spec.ts) exposes it the
254
+ // shared way — scripts/demo, the verbatim executable shim that re-execs under
255
+ // the nix dev shell and delegates to `bun run demo`, and the package.json
256
+ // "demo" script that resolves to the harness's ctl-demo bin. A product without
257
+ // a demo is asked for neither; a shim left behind is still a copy and stays
258
+ // verbatim.
259
+ const DEMO_SPEC = "tests/demo.spec.ts";
260
+ const DEMO_SHIM = "scripts/demo";
261
+ const DEMO_SCRIPT = "ctl-demo";
262
+
263
+ export function demoProblems(repoRoot: string, canonicalShim: string): string[] {
264
+ const problems: string[] = [];
265
+ const hasSpec = existsSync(join(repoRoot, DEMO_SPEC));
266
+ const shimPath = join(repoRoot, DEMO_SHIM);
267
+ if (existsSync(shimPath)) {
268
+ const actual = readFileSync(shimPath, "utf8");
269
+ if (actual !== canonicalShim) {
270
+ problems.push(
271
+ `${DEMO_SHIM} has drifted from @norskvideo/ctl-dev-kit conventions/demo.sh (${firstDiffLine(actual, canonicalShim)}). ${RESYNC}`,
272
+ );
273
+ }
274
+ if ((statSync(shimPath).mode & 0o111) === 0) {
275
+ problems.push(`${DEMO_SHIM} is not executable (chmod +x) — it is the entry a developer runs.`);
276
+ }
277
+ } else if (hasSpec) {
278
+ problems.push(
279
+ `${DEMO_SHIM} not found but ${DEMO_SPEC} exists. A product with a demo carries the shim verbatim from @norskvideo/ctl-dev-kit conventions/demo.sh (sync-drift writes it). ${RESYNC}`,
280
+ );
281
+ }
282
+ if (hasSpec) {
283
+ const pkgPath = join(repoRoot, "package.json");
284
+ let scripts: Record<string, string> | undefined;
285
+ try {
286
+ scripts = existsSync(pkgPath)
287
+ ? (JSON.parse(readFileSync(pkgPath, "utf8")) as { scripts?: Record<string, string> }).scripts
288
+ : undefined;
289
+ } catch (e) {
290
+ problems.push(`package.json is not parseable JSON (${e instanceof Error ? e.message : String(e)}).`);
291
+ }
292
+ if (scripts?.demo !== DEMO_SCRIPT) {
293
+ problems.push(
294
+ `package.json must carry "demo": "${DEMO_SCRIPT}" when ${DEMO_SPEC} exists — ${DEMO_SHIM} delegates to \`bun run demo\`.`,
295
+ );
296
+ }
297
+ }
298
+ return problems;
248
299
  }
249
300
 
250
301
  // A file that must be a verbatim, byte-for-byte copy of its dev-kit canonical.
@@ -435,6 +486,8 @@ export function checkDrift(repoRoot: string, canonical: CanonicalBytes): DriftRe
435
486
  }
436
487
  }
437
488
 
489
+ for (const p of demoProblems(repoRoot, canonical.demoShim)) push(p);
490
+
438
491
  return { ok: problems.length === 0, problems };
439
492
  }
440
493
 
@@ -454,6 +507,7 @@ if (import.meta.main) {
454
507
  gitignoreCore: readFileSync(join(import.meta.dir, "gitignore.core"), "utf8"),
455
508
  buildImage: readFileSync(join(import.meta.dir, "build-image.yml"), "utf8"),
456
509
  smoke: readFileSync(join(import.meta.dir, "smoke.yml"), "utf8"),
510
+ demoShim: readFileSync(join(import.meta.dir, "demo.sh"), "utf8"),
457
511
  };
458
512
  const report = checkDrift(repoRoot, canonical);
459
513
  if (report.ok) {
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env bash
2
+ # scripts/demo — this product's demo: tests/demo.spec.ts run by the
3
+ # @norskvideo/ctl-test-harness demo driver (`ctl-demo`).
4
+ #
5
+ # scripts/demo up from source on a private daemon; Ctrl-C tears down
6
+ # scripts/demo up --mode image --daemon reuse the built image into YOUR live daemon
7
+ # scripts/demo check headless; what CI runs
8
+ # scripts/demo spec | down | ui the plan / teardown / a multiplexer layout
9
+ #
10
+ # The driver's private daemon is the released norsk-ctl the repo's flake pins,
11
+ # so this re-execs itself under `nix develop .#dev` when that binary is not on
12
+ # PATH, then delegates to the package's `demo` script (`bun run demo`).
13
+ #
14
+ # Copied verbatim from @norskvideo/ctl-dev-kit conventions/demo.sh and
15
+ # drift-gated: edit the dev-kit source and re-sync, never this copy.
16
+ set -euo pipefail
17
+
18
+ repo="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
19
+ cd "$repo"
20
+
21
+ if ! command -v norsk-ctl >/dev/null 2>&1; then
22
+ if [ -n "${IN_NIX_SHELL:-}" ]; then
23
+ echo "scripts/demo: norsk-ctl is not on PATH in this nix shell — the .#build shell carries no ctl; use \`nix develop .#dev\`" >&2
24
+ exit 1
25
+ fi
26
+ exec nix develop "$repo#dev" --command "$repo/scripts/demo" "$@"
27
+ fi
28
+
29
+ exec bun run demo -- "$@"
@@ -18,8 +18,8 @@
18
18
  // cannot mechanically converge (e.g. a workflow with no product key to preserve)
19
19
  // is left for check-drift to flag, and a convention that genuinely breaks the
20
20
  // product surfaces as a red lint/typecheck/test — not as a silent bad copy.
21
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
22
- import { join } from "node:path";
21
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
22
+ import { dirname, join } from "node:path";
23
23
  import {
24
24
  BEGIN,
25
25
  type CanonicalBytes,
@@ -195,6 +195,19 @@ function syncDprint(repoRoot: string, canonical: string, r: SyncReport): void {
195
195
  writeIfChanged(path, out, "dprint.json", r.written);
196
196
  }
197
197
 
198
+ // scripts/demo (05-demo s6): verbatim + executable for a repo that has a demo
199
+ // (tests/demo.spec.ts) or already carries the shim; a repo without a demo is
200
+ // not given one. The package.json "demo" script is structural and stays the
201
+ // product's to add — the gate names it.
202
+ function syncDemoShim(repoRoot: string, canonical: string, r: SyncReport): void {
203
+ const rel = "scripts/demo";
204
+ const path = join(repoRoot, rel);
205
+ if (!existsSync(join(repoRoot, "tests/demo.spec.ts")) && !existsSync(path)) return;
206
+ mkdirSync(dirname(path), { recursive: true });
207
+ writeIfChanged(path, canonical, rel, r.written);
208
+ chmodSync(path, 0o755);
209
+ }
210
+
198
211
  export function syncDrift(repoRoot: string, canonical: CanonicalBytes): SyncReport {
199
212
  const r: SyncReport = { written: [], skipped: [] };
200
213
 
@@ -218,6 +231,7 @@ export function syncDrift(repoRoot: string, canonical: CanonicalBytes): SyncRepo
218
231
  syncClaude(repoRoot, canonical.core, r);
219
232
  syncGitignore(repoRoot, canonical.gitignoreCore, r);
220
233
  syncDprint(repoRoot, canonical.dprint, r);
234
+ syncDemoShim(repoRoot, canonical.demoShim, r);
221
235
 
222
236
  return r;
223
237
  }
@@ -239,6 +253,7 @@ if (import.meta.main) {
239
253
  gitignoreCore: readFileSync(join(dir, "gitignore.core"), "utf8"),
240
254
  buildImage: readFileSync(join(dir, "build-image.yml"), "utf8"),
241
255
  smoke: readFileSync(join(dir, "smoke.yml"), "utf8"),
256
+ demoShim: readFileSync(join(dir, "demo.sh"), "utf8"),
242
257
  };
243
258
  const report = syncDrift(repoRoot, canonical);
244
259
  if (report.written.length === 0) {
@@ -32,10 +32,18 @@ pinned in \`flake.nix\` and put on PATH by the nix shell:
32
32
  \`norsk-ctl product add --dev-url http://localhost:${ctx.devPort}\` registers the
33
33
  running backend as a dev product.
34
34
 
35
- **Outer loop (real image):** \`bun run build:image\` builds the container via the
36
- shared dev-kit bundle driver (\`deployment/build-image.sh\`, tag \`${ctx.name}\`,
37
- container port 4321), then re-add it into the daemon
38
- \`deployment/iterate.sh\` scripts the remove/re-add dance.
35
+ **Demo:** \`bun run demo -- up\` runs \`tests/demo.spec.ts\` (the
36
+ @norskvideo/ctl-test-harness demo driver) from source on a private daemon —
37
+ dev backend, registration, launch, the camera1 sample on the ingest, ready
38
+ gates, URLs; Ctrl-C tears down. \`check\` is the headless form CI runs,
39
+ \`spec\` prints the plan. \`scripts/demo\` is the same entry re-exec'd under
40
+ \`nix develop .#dev\` when the pinned ctl is not on PATH.
41
+
42
+ **Outer loop (real image):** \`bun run iterate\` — \`bun run build:image\` (the
43
+ shared dev-kit bundle driver via \`deployment/build-image.sh\`, tag \`${ctx.name}\`,
44
+ container port 4321), then \`bun run demo -- up --mode image --daemon reuse\`:
45
+ the driver deletes the instance, removes the template + product, re-adds the
46
+ fresh image and checks the pins before relaunching.
39
47
 
40
48
  ## First run after generation
41
49
 
@@ -61,7 +69,8 @@ The daemon launches instances from a **stored** product-template snapshot
61
69
  fetched once at \`product add\`, and refuses to overwrite one by name.
62
70
  Rebuilding the image does NOT reach launched instances or new launches until
63
71
  you refresh the stored template (\`norsk-ctl template refresh <name>\`, or the
64
- full delete/remove/re-add dance in \`deployment/iterate.sh\`). Verify the
72
+ delete/remove/re-add dance \`bun run iterate\` drives through the demo
73
+ driver). Verify the
65
74
  refreshed \`~/.norsk-ctl/product-templates/<name>/compose.yml\` carries the pins
66
75
  \`manifest.seed.json\` declares.
67
76
 
@@ -126,10 +135,14 @@ function rootPackageJson(ctx: ShapeContext): string {
126
135
  dev: "bun run --cwd backend dev",
127
136
  start: "bun run --cwd backend start",
128
137
  iterate: "bash deployment/iterate.sh",
138
+ demo: "ctl-demo",
129
139
  },
130
140
  devDependencies: {
131
141
  "@biomejs/biome": "2.5.5",
132
142
  "@norskvideo/ctl-dev-kit": "^0.1.10",
143
+ // The demo driver (`ctl-demo`, tests/demo.spec.ts) — hoisted here so the
144
+ // root `demo` script resolves the bin.
145
+ "@norskvideo/ctl-test-harness": "^0.1.23",
133
146
  "@types/bun": "latest",
134
147
  "@types/node": "^22.10.0",
135
148
  typescript: "^5.7.2",
@@ -1174,7 +1187,7 @@ function testsPackageJson(ctx: ShapeContext): string {
1174
1187
  // directly, and bun links a member's deps into its OWN node_modules —
1175
1188
  // the backend's copy is not reachable from here.
1176
1189
  "@norskvideo/ctl-product-template-schema": "^0.1.0",
1177
- "@norskvideo/ctl-test-harness": "^0.1.3",
1190
+ "@norskvideo/ctl-test-harness": "^0.1.23",
1178
1191
  "@types/bun": "latest",
1179
1192
  "@types/node": "^22.10.0",
1180
1193
  typescript: "^5.7.2",
@@ -1325,42 +1338,98 @@ describe("${ctx.name} image: container surface", () => {
1325
1338
 
1326
1339
  function iterateSh(ctx: ShapeContext): string {
1327
1340
  return `#!/usr/bin/env bash
1328
- # Iterate loop: rebuild the image and push it through the daemon — the
1329
- # stored-template trap (see CLAUDE.md) means NONE of this happens implicitly.
1330
- # Modeled on funke-pegasus's deployment/iterate.sh (the fleet reference); grow
1331
- # the TODOs into real probes as the product takes shape.
1341
+ #
1342
+ # Rebuild ${ctx.name} and reload it into the LIVE norsk-ctl daemon:
1343
+ # build image -> \`bun run demo -- up --mode image --daemon reuse\`.
1344
+ #
1345
+ # The driver (tests/demo.spec.ts via @norskvideo/ctl-test-harness/demo) does
1346
+ # the stored-template dance CLAUDE.md describes: deletes the instance, removes
1347
+ # the template + product, adds the fresh image again, checks the studio pin
1348
+ # against manifest.seed.json, pumps the source, waits for the ready gates,
1349
+ # prints the URLs and holds. Ctrl-C (or \`bun run demo -- down\`) removes the
1350
+ # instance; your daemon and the registration stay.
1351
+ #
1352
+ # Needs the daemon already running (\`norsk-ctl serve\`), \`norsk-ctl\` on PATH
1353
+ # (\`nix develop .#dev\` provides it) and NORSK_LICENSE_FILE.
1354
+ #
1355
+ # Usage (from the repo root):
1356
+ # bun run iterate # full loop
1357
+ # SKIP_BUILD=1 bun run iterate # reuse the existing image
1358
+ # IMAGE_TAG=... bun run iterate # build + register under another tag
1332
1359
  set -euo pipefail
1333
1360
 
1334
- product="${ctx.name}"
1335
- template="default"
1336
- image="\${IMAGE_TAG:-${ctx.name}:dev}"
1361
+ here="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
1362
+ repo="$(cd "$here/.." && pwd)"
1337
1363
 
1338
- echo "==> build image \${image}"
1339
- bun run build:image
1364
+ # The licence names the product by image ref, so the build lands on that ref
1365
+ # directly (build-image.sh honours IMAGE_TAG).
1366
+ export IMAGE_TAG="\${IMAGE_TAG:-${ctx.name}:dev}"
1340
1367
 
1341
- # Refresh the stored template in place if the daemon supports it; otherwise
1342
- # fall back to the full remove/re-add dance.
1343
- if norsk-ctl template refresh "\${template}" 2>/dev/null; then
1344
- echo "==> stored template refreshed"
1368
+ if [ -n "\${SKIP_BUILD:-}" ]; then
1369
+ echo "=== build (skipped, SKIP_BUILD set) ==="
1345
1370
  else
1346
- echo "==> template refresh unavailable — full remove/re-add"
1347
- # TODO: delete instances first: norsk-ctl instance delete <id> --purge
1348
- norsk-ctl template remove "\${template}" || true
1349
- norsk-ctl product remove "\${product}" || true
1350
- norsk-ctl product add --image "\${image}" \${NORSK_LICENSE_FILE:+--license-file "\${NORSK_LICENSE_FILE}"}
1371
+ echo "=== build image $IMAGE_TAG ==="
1372
+ bun run --cwd "$repo" build:image
1351
1373
  fi
1352
1374
 
1353
- # Verify the refresh landed — never assume (a stale stored template debugs
1354
- # like a code bug).
1355
- echo "==> stored template pins:"
1356
- grep "image:" "\${HOME}/.norsk-ctl/product-templates/\${template}/compose.yml"
1375
+ cd "$repo"
1376
+ exec bun run demo -- up --mode image --daemon reuse
1377
+ `;
1378
+ }
1357
1379
 
1358
- # TODO: launch an instance, pump a source, and assert output — funke's
1359
- # iterate.sh pumps SRT and asserts playable HLS; do the equivalent here.
1360
- echo "==> done (launch + probe steps are TODO)"
1380
+ function demoSpecTs(ctx: ShapeContext): string {
1381
+ return `// The ${ctx.name} demo (fleet review 05-demo): the manifest's default
1382
+ // template with the daemon's camera1 sample pumped into the SRT ingest. Run by
1383
+ // the @norskvideo/ctl-test-harness demo driver:
1384
+ //
1385
+ // bun run demo -- up from source on a private daemon; Ctrl-C tears down
1386
+ // bun run demo -- up --mode image --daemon reuse the built image into YOUR live daemon (bun run iterate)
1387
+ // bun run demo -- check headless, torn down on exit; what CI runs
1388
+ //
1389
+ // The driver owns the daemon, the ingest port (INGEST_PORT, read back at
1390
+ // launch), the pump and the teardown. Grow the ready gates as the product
1391
+ // takes shape: the one below only says Studio's runtime API answers.
1392
+ import { defineDemo } from "@norskvideo/ctl-test-harness/demo";
1393
+
1394
+ export default defineDemo({
1395
+ product: "${ctx.name}",
1396
+ dev: { command: ["bun", "run", "dev"], readyPath: "/healthz" },
1397
+ // The licence names the product by image ref; build-image.sh honours IMAGE_TAG.
1398
+ image: process.env.IMAGE_TAG ?? "${ctx.name}:dev",
1399
+ template: { name: "default" },
1400
+ launch: { hardware: "none" },
1401
+ sources: [{ name: "input", asset: { preset: "camera1" }, ingest: { param: "INGEST_PORT" }, streamId: "input" }],
1402
+ ready: [{ http: { studio: "/live/api/components" }, status: 200, timeoutMs: 120_000 }],
1403
+ open: [
1404
+ { name: "Studio", url: { proxy: "/instance/{id}/studio/" } },
1405
+ { name: "Studio (host port, no auth)", url: { studio: "/" } },
1406
+ ],
1407
+ });
1361
1408
  `;
1362
1409
  }
1363
1410
 
1411
+ const DEMO_SPEC_TEST_TS = `// The demo spec (tests/demo.spec.ts) is validated by defineDemo at import;
1412
+ // this adds, without Docker, the product-side agreement: the template it
1413
+ // names is one the manifest publishes and its source dials the parameter the
1414
+ // template declares — the class of rot the demo driver exists to stop.
1415
+ import { describe, expect, test } from "bun:test";
1416
+ import { buildManifest } from "../../shared/src/manifest.ts";
1417
+ import demo from "../demo.spec.ts";
1418
+
1419
+ describe("tests/demo.spec.ts", () => {
1420
+ test("names a template the manifest publishes as a default", () => {
1421
+ const published = buildManifest().defaultProductTemplates.map((t) => t.name);
1422
+ if (!demo.template || !("name" in demo.template)) throw new Error("the demo launches a published template");
1423
+ expect(published).toContain(demo.template.name);
1424
+ });
1425
+
1426
+ test("its source dials INGEST_PORT, the parameter the srt-listener template declares", () => {
1427
+ const [input] = demo.sources ?? [];
1428
+ expect(input && "param" in input.ingest ? input.ingest.param : "").toBe("INGEST_PORT");
1429
+ });
1430
+ });
1431
+ `;
1432
+
1364
1433
  const DOCKERIGNORE = `node_modules
1365
1434
  **/node_modules
1366
1435
  dist
@@ -1393,6 +1462,9 @@ export const backendTurnkey: ShapeModule = {
1393
1462
  { path: ".dockerignore", content: DOCKERIGNORE },
1394
1463
  { path: "examples/default/input.json", content: exampleInputJson(ctx) },
1395
1464
  { path: "deployment/iterate.sh", content: iterateSh(ctx), executable: true },
1465
+ { path: "scripts/demo", content: ctx.canon.demoShim, executable: true },
1466
+ { path: "tests/demo.spec.ts", content: demoSpecTs(ctx) },
1467
+ { path: "tests/unit/demo-spec.test.ts", content: DEMO_SPEC_TEST_TS },
1396
1468
  { path: "shared/package.json", content: sharedPackageJson(ctx) },
1397
1469
  { path: "shared/tsconfig.json", content: WORKSPACE_TSCONFIG },
1398
1470
  { path: "shared/src/index.ts", content: SHARED_INDEX_TS },
@@ -26,6 +26,8 @@ export interface Canon {
26
26
  buildImage: string;
27
27
  gitignoreCore: string;
28
28
  invariantsTemplate: string;
29
+ /** conventions/demo.sh, emitted as scripts/demo. */
30
+ demoShim: string;
29
31
  }
30
32
 
31
33
  export function loadCanon(): Canon {
@@ -43,6 +45,7 @@ export function loadCanon(): Canon {
43
45
  buildImage: readFileSync(join(conventionsDir, "build-image.yml"), "utf8"),
44
46
  gitignoreCore: readFileSync(join(conventionsDir, "gitignore.core"), "utf8"),
45
47
  invariantsTemplate: readFileSync(join(import.meta.dir, "..", "testing", "INVARIANTS.template.md"), "utf8"),
48
+ demoShim: readFileSync(join(conventionsDir, "demo.sh"), "utf8"),
46
49
  };
47
50
  }
48
51
 
@@ -30,7 +30,12 @@ bump `ctlVersion` + the four platform hashes in `flake.nix` (or run
30
30
 
31
31
  **Inner loop (fast, no image build)** — run the backend from source and register
32
32
  it as a `kind: dev` product, so edits to the backend / workflow / components land
33
- without building a container:
33
+ without building a container. `bun run demo -- up` does the whole thing on a
34
+ private daemon — `tests/demo.spec.ts` run by the test-harness demo driver: dev
35
+ backend, registration, launch, the sample sources, the ready gates, the URLs;
36
+ Ctrl-C tears down (`spec` prints the plan, `down` tears down from another
37
+ shell). `scripts/demo` is the same entry, re-exec'd under `nix develop .#dev`
38
+ when the pinned ctl is not on PATH. By hand, against your own daemon:
34
39
 
35
40
  ```sh
36
41
  bun install
@@ -40,15 +45,19 @@ norsk-ctl instance launch-template <id> --template <t> --hardware none
40
45
  ```
41
46
 
42
47
  **Outer loop (real container)** — rebuild the product image and reload it into
43
- the live daemon, with pin + segfault guards. This is `deployment/iterate.sh`:
48
+ the live daemon. This is `bun run iterate` (`deployment/iterate.sh`):
49
+ `build:image`, then `bun run demo -- up --mode image --daemon reuse`, where the
50
+ driver deletes the instance, removes the template + product, re-adds the fresh
51
+ image, checks the pins against `manifest.seed.json` and relaunches:
44
52
 
45
53
  ```sh
46
- NORSK_LICENSE_FILE=/path/to/license.json bun run iterate # needs a running daemon + GPU
47
- HARDWARE=none NORSK_LICENSE_FILE=... bun run iterate # no GPU reservation
54
+ NORSK_LICENSE_FILE=/path/to/license.json bun run iterate # needs a running daemon (+ GPU where the spec says nvidia)
55
+ HARDWARE=none NORSK_LICENSE_FILE=... bun run iterate # no GPU reservation (funke)
48
56
  ```
49
57
 
50
- `test:integration` uses the same daemon path but against the **released** ctl
51
- (via `NORSK_CTL_BINARY`), exactly as CI does — see `.github/workflows/integration.yml`.
58
+ `bun run demo -- check` is the headless form CI runs after `test:integration`,
59
+ both against the **released** ctl (via `NORSK_CTL_BINARY`), exactly as a
60
+ customer would — see `.github/workflows/integration.yml`.
52
61
 
53
62
  ---
54
63
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@norskvideo/ctl-dev-kit",
3
- "version": "0.1.52",
3
+ "version": "0.1.53",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./package.json": "./package.json",