@norskvideo/ctl-dev-kit 0.1.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.
@@ -0,0 +1,55 @@
1
+ # Shared copy-only product-backend image for the "bundle" products
2
+ # (probe / funke-pegasus / norsk-commentary / norsk-playout). Single-sourced in
3
+ # @norskvideo/ctl-dev-kit so the four images cannot drift; the per-product facts
4
+ # (tag, container port, optional native addon / assets) come in as build args and
5
+ # staged directories from the shared build-bundle-image.sh driver.
6
+ #
7
+ # Studio is NOT built from this file — it ships a `bun build --compile` binary
8
+ # with a different (STUDIO_ASSET_ROOT) layout; see products/studio/deployment.
9
+ #
10
+ # Copy-only: the backend bundle, frontend, dashboards, components, and the
11
+ # optional native addon + product-template assets are all built/collected on the
12
+ # host by the driver and COPYd in. Nothing is installed or compiled here (the old
13
+ # per-product images ran `bun install` inside an oven/bun:alpine builder and
14
+ # shipped the whole node_modules; the bundle inlines only what it reaches).
15
+ #
16
+ # The base is single-sourced in build/common.sh (PRODUCT_BASE_IMAGE) and passed
17
+ # as BASE_IMAGE, so every product image shares one pinned distroless base (glibc
18
+ # + libgcc/libstdc++, no shell). The runtime bun is imported Docker-natively from
19
+ # the upstream slim image (BUN_IMAGE = oven/bun:<version>-slim): buildkit rejects
20
+ # a variable in `COPY --from=${BUN_IMAGE}`, so it's aliased to a named stage from
21
+ # the global-scope ARG and the binary COPYd out; that stage only donates a
22
+ # prebuilt binary, so the final image stays copy-only.
23
+ #
24
+ # Split-level asset layout (the subtle part): the backend is a single bundled
25
+ # file at backend/dist/index.js, so at runtime import.meta.dir is that bundle's
26
+ # dir for ALL of the collapsed modules. server.ts's `../../frontend/dist` lands
27
+ # beside the app dir, but lib/{components,dashboards,assets}.ts (originally under
28
+ # backend/src/lib, one level deeper) kept `../../../{components/lib,dashboards,
29
+ # assets}`, which resolve ONE level above the app dir. So dashboards +
30
+ # components/lib + assets go at /usr/src while backend/frontend go under
31
+ # /usr/src/app. A flat /usr/src/app layout silently drops them from the generated
32
+ # product-template tars.
33
+ ARG BASE_IMAGE
34
+ ARG BUN_IMAGE
35
+ FROM ${BUN_IMAGE} AS bun
36
+
37
+ FROM ${BASE_IMAGE}
38
+ # The container port docker-runner maps the host port to (probe/funke 4321,
39
+ # commentary/playout 4322). Passed by the driver from the wrapper's PRODUCT_PORT.
40
+ ARG PRODUCT_PORT=4321
41
+ COPY --from=bun /usr/local/bin/bun /usr/local/bin/bun
42
+ WORKDIR /usr/src/app
43
+ COPY backend/dist ./backend/dist
44
+ # Optional native addon(s), beside the bundle where <product>-identity.ts
45
+ # createRequires them. The driver stages an empty native/ for products that ship
46
+ # no signer (funke/commentary/playout), so this COPY is then a harmless no-op.
47
+ COPY native/ ./backend/dist/
48
+ COPY frontend/dist ./frontend/dist
49
+ COPY dashboards /usr/src/dashboards
50
+ COPY components/lib /usr/src/components/lib
51
+ # Optional product-template assets (funke's slate/black PNGs). Empty otherwise.
52
+ COPY assets /usr/src/assets
53
+ ENV PORT=${PRODUCT_PORT}
54
+ EXPOSE ${PRODUCT_PORT}
55
+ CMD ["/usr/local/bin/bun", "run", "backend/dist/index.js"]
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env bash
2
+ # Shared driver for the copy-only "bundle" product images
3
+ # (probe / funke-pegasus / norsk-commentary / norsk-playout). Single-sourced in
4
+ # @norskvideo/ctl-dev-kit; each product's deployment/build-image.sh is a thin
5
+ # wrapper that sets the per-product facts and sources this file. Studio does NOT
6
+ # use this driver (it ships a --compile binary; see products/studio/deployment).
7
+ #
8
+ # The wrapper MUST set before sourcing:
9
+ # PRODUCT_DIR absolute path to the product workspace (backend/, frontend/,
10
+ # dashboards/, components/, package.json)
11
+ # IMAGE_TAG the image tag to build (the wrapper sets the product default)
12
+ # PRODUCT_PORT the container port docker-runner maps to (probe/funke 4321,
13
+ # commentary/playout 4322)
14
+ # Optional:
15
+ # PRODUCT_BUILD_SCRIPT workspace script that builds the host artifacts
16
+ # (default: build:no-lint)
17
+ # BUN_VERSION override the pinned bun (default from packageManager)
18
+ #
19
+ # Everything else is auto-detected off PRODUCT_DIR, so a product opts in just by
20
+ # having the directory/script — no per-product flag to keep in sync:
21
+ # native/ native addon(s) (probe's signer) -> beside bundle
22
+ # assets/ product-template assets (funke's PNGs)
23
+ # scripts/runtime-images.ts -> emits the norsk-ctl.runtime-images label
24
+ set -euo pipefail
25
+
26
+ : "${PRODUCT_DIR:?build-bundle-image.sh: PRODUCT_DIR must be set by the wrapper}"
27
+ : "${IMAGE_TAG:?build-bundle-image.sh: IMAGE_TAG must be set by the wrapper}"
28
+ : "${PRODUCT_PORT:?build-bundle-image.sh: PRODUCT_PORT must be set by the wrapper}"
29
+ build_script="${PRODUCT_BUILD_SCRIPT:-build:no-lint}"
30
+
31
+ driver_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
32
+ # shellcheck source=./common.sh
33
+ source "${driver_dir}/common.sh"
34
+
35
+ # Pin bun once from the product repo's packageManager so downstream helpers don't
36
+ # each re-walk the tree.
37
+ export BUN_VERSION="${BUN_VERSION:-$(product_bun_version "${PRODUCT_DIR}")}"
38
+
39
+ echo "==> building on host (${build_script})"
40
+ bun run --cwd "${PRODUCT_DIR}" "${build_script}"
41
+
42
+ stage="$(mktemp -d)"
43
+ trap 'rm -rf "${stage}"' EXIT
44
+
45
+ echo "==> staging artifacts"
46
+ mkdir -p "${stage}/backend" "${stage}/frontend" "${stage}/components" "${stage}/native" "${stage}/assets"
47
+ cp -R "${PRODUCT_DIR}/backend/dist" "${stage}/backend/dist"
48
+ cp -R "${PRODUCT_DIR}/frontend/dist" "${stage}/frontend/dist"
49
+ cp -R "${PRODUCT_DIR}/dashboards" "${stage}/dashboards"
50
+ cp -R "${PRODUCT_DIR}/components/lib" "${stage}/components/lib"
51
+
52
+ # Native addon(s): staged beside the bundle. Strip the build host's RUNPATH (nix
53
+ # store / repo-local paths) so the shipped .node carries no build-host paths; its
54
+ # NEEDED libs resolve via the base image's default search path. Best-effort:
55
+ # only if patchelf is available.
56
+ if [ -d "${PRODUCT_DIR}/native" ]; then
57
+ find "${PRODUCT_DIR}/native" -maxdepth 1 -name '*.node' -exec cp {} "${stage}/native/" \;
58
+ if command -v patchelf >/dev/null 2>&1; then
59
+ find "${stage}/native" -name '*.node' -exec patchelf --remove-rpath {} \;
60
+ fi
61
+ fi
62
+
63
+ # Product-template assets (optional): copy contents into the always-staged dir.
64
+ if [ -d "${PRODUCT_DIR}/assets" ]; then
65
+ cp -R "${PRODUCT_DIR}/assets/." "${stage}/assets/"
66
+ fi
67
+
68
+ # Runtime images the default templates launch, stamped on as a label so a
69
+ # golden-image bake can warm them with `docker inspect` + `docker pull` alone —
70
+ # no daemon, no license. Derived from the rendered compose (scripts/runtime-images.ts),
71
+ # never hardcoded. Products without that script simply don't carry the label.
72
+ label_args=()
73
+ if [ -f "${PRODUCT_DIR}/scripts/runtime-images.ts" ]; then
74
+ echo "==> resolving runtime images for the norsk-ctl.runtime-images label"
75
+ runtime_images="$(bun "${PRODUCT_DIR}/scripts/runtime-images.ts")"
76
+ label_args+=(--label "norsk-ctl.runtime-images=${runtime_images}")
77
+ fi
78
+
79
+ echo "==> docker build ${IMAGE_TAG}"
80
+ docker build \
81
+ --build-arg BASE_IMAGE="${PRODUCT_BASE_IMAGE}" \
82
+ --build-arg BUN_IMAGE="$(product_bun_slim_image)" \
83
+ --build-arg PRODUCT_PORT="${PRODUCT_PORT}" \
84
+ "${label_args[@]}" \
85
+ -f "${driver_dir}/Dockerfile.bundle" -t "${IMAGE_TAG}" "${stage}"
86
+
87
+ echo "==> built ${IMAGE_TAG}"
@@ -0,0 +1,77 @@
1
+ # Shared deployment constants + helpers for the product container images.
2
+ # Single-sourced in @norskvideo/ctl-dev-kit and sourced by the product build
3
+ # scripts (the bundle driver build-bundle-image.sh, and studio's bespoke
4
+ # build-image.sh). This is the one place the pinned base + bun live, so no two
5
+ # product images can drift.
6
+ #
7
+ # Location-independent: this file makes NO assumption about where in a repo it
8
+ # sits (it used to live at products/deployment/common.sh and derive the repo
9
+ # root from its own path). The same file works vendored/symlinked in the
10
+ # monorepo and installed into a split-out product repo's node_modules — callers
11
+ # pass the product directory in, they don't rely on this file's location.
12
+ #
13
+ # Base choice — gcr.io/distroless/cc-debian12: glibc plus libgcc/libstdc++ (bun
14
+ # and probe's native rust addon link these) and nothing else — no shell, no
15
+ # package manager. studio's compiled binary would fit the smaller
16
+ # distroless/base-debian12, but we take probe's minimum (cc) for every product so
17
+ # a host running several products pulls one shared base layer. Pinned by digest
18
+ # because distroless publishes only moving tags (no immutable version tags). No
19
+ # shell is fine: images run via exec-form CMD and health is checked over HTTP.
20
+ PRODUCT_BASE_IMAGE="gcr.io/distroless/cc-debian12@sha256:7ee09f36862efbdbf70422db263e411c2618409ca46faa555bd5b636155307df"
21
+
22
+ # The pinned bun version. Override with BUN_VERSION; otherwise walk up from the
23
+ # given directory (default: PWD) to the nearest package.json declaring
24
+ # "packageManager": "bun@X" — the workspace root in the monorepo, the repo root
25
+ # in a split-out product. Prints X.
26
+ product_bun_version() {
27
+ if [ -n "${BUN_VERSION:-}" ]; then
28
+ printf '%s' "${BUN_VERSION}"
29
+ return 0
30
+ fi
31
+ local dir version
32
+ dir="${1:-$PWD}"
33
+ while [ "${dir}" != "/" ]; do
34
+ if [ -f "${dir}/package.json" ]; then
35
+ version="$(sed -n 's/.*"packageManager": *"bun@\([^"]*\)".*/\1/p' "${dir}/package.json")"
36
+ if [ -n "${version}" ]; then
37
+ printf '%s' "${version}"
38
+ return 0
39
+ fi
40
+ fi
41
+ dir="$(dirname "${dir}")"
42
+ done
43
+ echo "could not determine bun version (no packageManager in any ancestor package.json)" >&2
44
+ return 1
45
+ }
46
+
47
+ # Prints the upstream oven/bun slim image ref for the pinned version. The bundle
48
+ # products import their runtime bun from this image Docker-natively (COPY --from),
49
+ # so they never touch the host cache; only studio's host-side --compile needs the
50
+ # extracted binary below.
51
+ product_bun_slim_image() {
52
+ local version
53
+ version="$(product_bun_version)" || return 1
54
+ printf 'oven/bun:%s-slim' "${version}"
55
+ }
56
+
57
+ # Prints the path to an upstream (glibc, FHS-loader) bun of the pinned version,
58
+ # extracted from oven/bun:<version>-slim and cached. The nix-devshell bun is
59
+ # patched to a /nix/store loader: it won't run in the base image, and baked into
60
+ # a `--compile` binary it makes that binary fail to exec in a stock container.
61
+ # So the bun that ships (bundle products) or does the compile (studio) must be
62
+ # this one. Progress goes to stderr so `$(product_upstream_bun)` captures only
63
+ # the path.
64
+ product_upstream_bun() {
65
+ local version cache_dir bun_path cid
66
+ version="$(product_bun_version)" || return 1
67
+ cache_dir="${XDG_CACHE_HOME:-${HOME}/.cache}/norsk-ctl"
68
+ bun_path="${cache_dir}/bun-${version}"
69
+ if [ ! -x "${bun_path}" ]; then
70
+ echo "==> fetching upstream bun ${version} from oven/bun:${version}-slim" >&2
71
+ mkdir -p "${cache_dir}"
72
+ cid="$(docker create "oven/bun:${version}-slim")"
73
+ docker cp "${cid}:/usr/local/bin/bun" "${bun_path}"
74
+ docker rm "${cid}" >/dev/null
75
+ fi
76
+ printf '%s' "${bun_path}"
77
+ }
@@ -0,0 +1,49 @@
1
+ <!-- BEGIN ctl-shared-conventions v1 -->
2
+ <!--
3
+ This block is the SHARED conventions core, single-sourced in
4
+ @norskvideo/ctl-dev-kit (conventions/CLAUDE.core.md). It is copied verbatim
5
+ into each product repo's CLAUDE.md between these fenced markers, and a
6
+ drift-check gate (Workstream I) fails CI if a copy diverges from the dev-kit's
7
+ version. Do NOT hand-edit a copy — edit the dev-kit source and re-sync. The
8
+ per-product tail (this product's build, iterate loop, gotchas) lives OUTSIDE
9
+ the markers and is yours to own.
10
+
11
+ It is copied (not `@import`ed) on purpose: CLAUDE.md's value is being read
12
+ verbatim by a human on GitHub with no tooling, and an @import shows a human a
13
+ dead path.
14
+ -->
15
+
16
+ ## Conventions (shared across all Norsk ctl product repos — do not edit in place)
17
+
18
+ - **RED -> GREEN -> REFACTOR. No fix before its failing test.** Write the test,
19
+ run it, watch it fail against the old code, THEN write the fix. This applies
20
+ _most_ when the fix feels too obvious to bother — that's precisely when the
21
+ skip happens. If code somehow got written first: stash it, prove the test RED,
22
+ pop, prove GREEN.
23
+ - **No `Co-Authored-By` tags on commits.** Imperative subject. Short body, only
24
+ if the why isn't obvious from the diff. Each commit logically scoped — don't
25
+ dump a phase into one commit.
26
+ - **No emojis** in code, commits, or docs unless explicitly asked.
27
+ - **Minimal comments.** Names explain _what_; comments explain _why_ —
28
+ non-obvious constraints, surprising invariants, workarounds for upstream bugs.
29
+ Don't restate the code.
30
+ - **Result over throws** for predictable failures. `Result<T, E>` + `ok()`/
31
+ `err()`/`tryResult()` from `@norskvideo/ctl-foundation`. Throws are reserved
32
+ for genuine exceptions.
33
+ - **ESLint / biome rules are non-negotiable** — adhere, never ignore. Keep the
34
+ whole project clean (`biome check .` exits 0), not just your own diff.
35
+ - **Test temp dirs that get bind-mounted into a container must come from the
36
+ repo-local temp helper**, never `os.tmpdir()`/`/tmp`. On OrbStack, `/tmp` bind
37
+ mounts don't forward host->guest inotify events, so file watchers never fire —
38
+ a failure that looks exactly like a code bug. Unit tests that don't bind-mount
39
+ may keep using `os.tmpdir()`.
40
+ - **Don't pipe test runs to `tail`** — you lose the failure context. Write output
41
+ to a temp file, then tail _that_ file for the results.
42
+ - **Evolve the ctl<->product contract additively.** An older ctl must launch a
43
+ newer product release: never repurpose or remove a required launch-interface
44
+ field. When a product genuinely cannot launch on an older ctl it declares a
45
+ named capability in its manifest's `requires: [...]`; ctl refuses only if it
46
+ lacks that capability. Reach for a capability as rarely as possible — it is the
47
+ escape hatch, not the default.
48
+
49
+ <!-- END ctl-shared-conventions v1 -->
@@ -0,0 +1,45 @@
1
+ # Local-dev overrides
2
+
3
+ A product repo consumes ctl and the Norsk runtime as published packages/images.
4
+ To develop a product against **un-published** ctl code or a **locally-built**
5
+ media server, there are two independent override layers. Each is opt-in and
6
+ leaves the default (registry/pinned) path untouched when unset.
7
+
8
+ ## 1. The npm layer — local `@norskvideo/ctl-*` checkouts
9
+
10
+ The product depends on `@norskvideo/ctl-{sdk,foundation,product-template-schema,
11
+ oas-to-ts,test-harness,commands,dev-kit}` at a caret range. To point one (or all)
12
+ at a local checkout of the ctl monorepo instead of the registry, use
13
+ `dev-link.sh`:
14
+
15
+ ```sh
16
+ # link every @norskvideo/ctl-* dep to a sibling monorepo checkout
17
+ node_modules/@norskvideo/ctl-dev-kit/local-dev/dev-link.sh ../norsk-ctl
18
+
19
+ # undo — restore the registry versions
20
+ node_modules/@norskvideo/ctl-dev-kit/local-dev/dev-link.sh --unlink
21
+ ```
22
+
23
+ It `bun link`s the packages the monorepo exposes and re-installs; `--unlink`
24
+ drops the overrides and reinstalls from the lockfile. Nothing is committed —
25
+ the links live only in your local `node_modules`.
26
+
27
+ ## 2. The image layer — local media / studio image
28
+
29
+ Every product's `shared/src/version.ts` already reads the runtime image tag from
30
+ the environment, so this layer needs no script — just export the hook before you
31
+ launch:
32
+
33
+ - `NORSK_MEDIA_TAG` — override the Norsk media-server image tag.
34
+ - `NORSK_STUDIO_TAG` — override the Norsk Studio image tag.
35
+
36
+ Since Workstream C unified the tag convention, both are **bare tags** across every
37
+ product (no full-ref/bare-tag divergence), so the same exported value means the
38
+ same thing everywhere:
39
+
40
+ ```sh
41
+ export NORSK_STUDIO_TAG=my-local-studio:dev
42
+ bun run --cwd . iterate # or the product's launch/iterate entry
43
+ ```
44
+
45
+ Leave the hooks unset to launch the pinned tags the product declares.
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env bash
2
+ # Point a product repo's @norskvideo/ctl-* dependencies at a local checkout of
3
+ # the ctl monorepo (or restore the registry versions). See local-dev/README.md.
4
+ #
5
+ # dev-link.sh <path-to-ctl-monorepo> # link ctl-* packages to that checkout
6
+ # dev-link.sh --unlink # drop the overrides, reinstall pinned
7
+ #
8
+ # Nothing is committed — the links live only in this repo's node_modules.
9
+ set -euo pipefail
10
+
11
+ PACKAGES=(sdk foundation product-template-schema oas-to-ts test-harness commands dev-kit)
12
+ product_root="$(cd "$(dirname "${BASH_SOURCE[0]}")" && git -C . rev-parse --show-toplevel 2>/dev/null || pwd)"
13
+
14
+ if [ "${1:-}" = "--unlink" ]; then
15
+ for pkg in "${PACKAGES[@]}"; do
16
+ bun unlink "@norskvideo/ctl-${pkg}" 2>/dev/null || true
17
+ done
18
+ echo "==> reinstalling pinned versions"
19
+ bun install --cwd "${product_root}"
20
+ echo "==> unlinked; registry versions restored"
21
+ exit 0
22
+ fi
23
+
24
+ mono="${1:?usage: dev-link.sh <path-to-ctl-monorepo> | --unlink}"
25
+ mono="$(cd "${mono}" && pwd)"
26
+ [ -d "${mono}/packages" ] || { echo "not a ctl monorepo checkout: ${mono}" >&2; exit 1; }
27
+
28
+ for pkg in "${PACKAGES[@]}"; do
29
+ dir="${mono}/packages/${pkg}"
30
+ [ -d "${dir}" ] || { echo " skip @norskvideo/ctl-${pkg} (not in checkout)"; continue; }
31
+ ( cd "${dir}" && bun link )
32
+ bun link "@norskvideo/ctl-${pkg}" --cwd "${product_root}"
33
+ echo " linked @norskvideo/ctl-${pkg} -> ${dir}"
34
+ done
35
+ echo "==> linked against ${mono}; run dev-link.sh --unlink to restore"
package/package.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "@norskvideo/ctl-dev-kit",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "exports": {
6
+ "./package.json": "./package.json"
7
+ },
8
+ "publishConfig": {
9
+ "access": "public"
10
+ }
11
+ }