From acff036bb323928b38d60cb459f5cd6061df2b25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Tue, 28 Apr 2026 15:22:47 +0200 Subject: [PATCH 01/21] Add design spec for as-lan SDK support Documents the plan to register as-lan as a built-in SDK in SDK_CONFIGS, with bare and versioned alias support so users no longer need to inline image/build/test in their service config. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../2026-04-28-aslan-framework-design.md | 190 ++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-28-aslan-framework-design.md diff --git a/docs/superpowers/specs/2026-04-28-aslan-framework-design.md b/docs/superpowers/specs/2026-04-28-aslan-framework-design.md new file mode 100644 index 0000000..e882ce8 --- /dev/null +++ b/docs/superpowers/specs/2026-04-28-aslan-framework-design.md @@ -0,0 +1,190 @@ +# Add as-lan as a built-in SDK + +## Goal + +Make as-lan a first-class supported framework so a service author writes +`sdk: as-lan` (or `sdk: as-lan-0.0.4`, or `sdk: aslan-0.0.4`) in +`jammin.build.yml` and jammin resolves the docker image plus build/test +commands automatically — no inline image/build/test fields required. + +`jammin create my-app --template aslan` scaffolds from +`jammin-create/jammin-create-aslan`. + +## Non-goals + +- Rebuilding the `jammin-as-lan` docker image. That work happens in the + `tomusdrw/as-lan` repo and produces the `0.0.4` tag this spec pins to. + Without it the SDK entry's `npm run build` / `npm test` commands would + fall back to running `npm install` on every build, which we explicitly + do not want. +- Generalised alias resolution (regex/prefix transforms across the SDK + family). The alias map is explicit. +- Versioned aliases for as-lan releases that haven't been pinned yet + (e.g. `as-lan-0.0.3` — only versions present in `SDK_CONFIGS` get an + alias entry). + +## Dependency + +This change must NOT merge until `ghcr.io/tomusdrw/jammin-as-lan:0.0.4` +exists with deps baked in (or otherwise resolved such that `npm run +build` works against a mounted source dir without a per-build install). + +## Design + +### Canonical SDK key + +`aslan-0.0.4`. Keeps the existing kebab-case-plus-version convention +shared with `jamc3-1.1.2`, `ajanta-0.1.0`, `jam-sdk-0.1.26`. Underlying +image: `ghcr.io/tomusdrw/jammin-as-lan:0.0.4`. Build command: +`npm run build`. Test command: `npm test`. + +### Aliases + +A new exported map next to `SDK_CONFIGS`: + +```ts +export const SDK_ALIASES = { + "as-lan": "aslan-0.0.4", + "as-lan-0.0.4": "aslan-0.0.4", +} as const satisfies Record; +``` + +- `as-lan` (bare) — points to the current default as-lan version. + Convenient shorthand; will silently follow version bumps when the + default changes. +- `as-lan-0.0.4` — versioned re-spelling (matches the framework's + canonical name with the dash). Pinned, will not move. + +When a future `aslan-0.0.5` is added, `SDK_ALIASES` gains a +`"as-lan-0.0.5": "aslan-0.0.5"` entry and the bare `"as-lan"` alias +target is rebumped to `aslan-0.0.5`. + +### Resolver helper + +```ts +export function resolveSdkId(id: string): keyof typeof SDK_CONFIGS | undefined { + if (id in SDK_CONFIGS) { + return id as keyof typeof SDK_CONFIGS; + } + if (id in SDK_ALIASES) { + return SDK_ALIASES[id as keyof typeof SDK_ALIASES]; + } + return undefined; +} +``` + +Single source of truth for "string SDK identifier → canonical +`SDK_CONFIGS` key". Lives in `packages/jammin-sdk/config/sdk-configs.ts` +alongside the data it operates on. + +### Type widening + +`ServiceConfig.sdk` becomes: + +```ts +sdk: keyof typeof SDK_CONFIGS | keyof typeof SDK_ALIASES | SdkConfig; +``` + +So the YAML config type is honest about which strings are accepted. + +### Validation + +`config-validator.ts` Zod schema for `sdk` field accepts canonical keys +**and** alias keys. Resolution happens at consumption time, not +validation time — the parsed config keeps whatever string the user +wrote (useful for error messages and round-tripping). Validator error +message lists both sets: + +> Expected a valid custom SDK configuration or one of the supported SDK +> ids (jam-sdk-0.1.26, jambrains-1cfc41c, jade-0.0.15-pre.1, +> ajanta-0.1.0, jamc3-1.1.2, aslan-0.0.4) or aliases (as-lan, +> as-lan-0.0.4) + +### Build/test command consumption + +`bin/cli/src/commands/build-command.ts` line 28 currently: + +```ts +const sdk = typeof service.sdk === "string" ? SDK_CONFIGS[service.sdk] : service.sdk; +``` + +Becomes: + +```ts +let sdk: SdkConfig; +if (typeof service.sdk === "string") { + const canonicalId = resolveSdkId(service.sdk); + if (!canonicalId) { + throw new Error(`Unknown SDK id: ${service.sdk}`); + } + sdk = SDK_CONFIGS[canonicalId]; +} else { + sdk = service.sdk; +} +``` + +Same change in `test-command.ts` line 21. The thrown error is a +defensive belt — the validator should already have rejected unknown +ids, but a stale config could in principle bypass it. + +### Create-command template + +`bin/cli/src/commands/create-command.ts`: + +- Add `aslan` to the `Template` type union +- Add `aslan: "jammin-create/jammin-create-aslan"` to `TARGETS` + +Pattern is identical to commit `b995067` (C3 SDK). + +## Files touched + +- `packages/jammin-sdk/config/sdk-configs.ts` — new entry, alias map, resolver helper +- `packages/jammin-sdk/config/types/config.ts` — widen `ServiceConfig.sdk` +- `packages/jammin-sdk/config/config-validator.ts` — accept alias keys, update error message +- `bin/cli/src/commands/build-command.ts` — use `resolveSdkId` before `SDK_CONFIGS` lookup +- `bin/cli/src/commands/test-command.ts` — same as above +- `bin/cli/src/commands/create-command.ts` — register `aslan` template + +## Tests + +Additions only, no new files except where noted. + +`packages/jammin-sdk/config/config-validator.test.ts`: +- Accepts canonical `aslan-0.0.4` +- Accepts alias `as-lan` +- Accepts alias `as-lan-0.0.4` +- Rejects unknown alias `as-lan-0.0.3` +- Rejects misspelt canonical `aslan` (no version) + +`packages/jammin-sdk/config/sdk-configs.test.ts` (new file, small): +- `resolveSdkId("aslan-0.0.4")` returns `"aslan-0.0.4"` +- `resolveSdkId("as-lan")` returns `"aslan-0.0.4"` +- `resolveSdkId("as-lan-0.0.4")` returns `"aslan-0.0.4"` +- `resolveSdkId("nonsense")` returns `undefined` + +`bin/cli/src/commands/build-command.test.ts`: +- Existing `jambrains-1cfc41c` / `jade-0.0.15-pre.1` cases stay +- Add a case asserting that `service.sdk === "as-lan"` produces a + docker invocation containing the `aslan-0.0.4` image and build command + +## Docs + +`docs/src/service-examples.md`: +- New "as-lan" section under "Using Docker images" mirroring the JAM + SDK / JamBrains / Jade entries (image, build invocation, brief test + invocation) +- Brief mention of accepted SDK names: canonical, dashed-versioned, and + bare alias + +`docs/src/SUMMARY.md` — no change (editing existing page only). + +## Validation matrix + +| Input | Validator | Resolves to | +|------------------------|-----------|----------------| +| `aslan-0.0.4` | accept | `aslan-0.0.4` | +| `as-lan` | accept | `aslan-0.0.4` | +| `as-lan-0.0.4` | accept | `aslan-0.0.4` | +| `as-lan-0.0.3` | reject | n/a | +| `aslan` (no version) | reject | n/a | +| custom `SdkConfig` obj | accept | the inline obj | From 97809b8f21aab26188adfaa701729b16e2548872 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Thu, 7 May 2026 23:03:19 +0200 Subject: [PATCH 02/21] Pin as-lan SDK spec to published 0.0.4 image Replaces the merge-blocker dependency note: ghcr.io/tomusdrw/jammin-as-lan:0.0.4 is already published and uses an entrypoint symlink so lean npm run build / npm test work without per-build npm install. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../2026-04-28-aslan-framework-design.md | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/docs/superpowers/specs/2026-04-28-aslan-framework-design.md b/docs/superpowers/specs/2026-04-28-aslan-framework-design.md index e882ce8..9bb2ac0 100644 --- a/docs/superpowers/specs/2026-04-28-aslan-framework-design.md +++ b/docs/superpowers/specs/2026-04-28-aslan-framework-design.md @@ -12,22 +12,25 @@ commands automatically — no inline image/build/test fields required. ## Non-goals -- Rebuilding the `jammin-as-lan` docker image. That work happens in the - `tomusdrw/as-lan` repo and produces the `0.0.4` tag this spec pins to. - Without it the SDK entry's `npm run build` / `npm test` commands would - fall back to running `npm install` on every build, which we explicitly - do not want. - Generalised alias resolution (regex/prefix transforms across the SDK family). The alias map is explicit. - Versioned aliases for as-lan releases that haven't been pinned yet (e.g. `as-lan-0.0.3` — only versions present in `SDK_CONFIGS` get an alias entry). - -## Dependency - -This change must NOT merge until `ghcr.io/tomusdrw/jammin-as-lan:0.0.4` -exists with deps baked in (or otherwise resolved such that `npm run -build` works against a mounted source dir without a per-build install). +- Migrating the image to `ghcr.io/fluffylabs/jammin-as-lan` (the + namespace used by other built-in SDKs). The README documents that + target, but only `ghcr.io/tomusdrw/jammin-as-lan` is published today. + This spec pins to `tomusdrw` to match what works; a future change can + flip the namespace once the image is mirrored. + +## Image behaviour + +`ghcr.io/tomusdrw/jammin-as-lan:0.0.4` is published and ships with +Node.js, `wasm-pvm`, and the as-lan/ecalli/AssemblyScript packages +pre-installed globally. Its entrypoint symlinks the global install into +`/app/node_modules` when none exists in the mounted volume, so +`npm run build` and `npm test` work against the user's source without a +per-build `npm install`. ## Design From 380ff60b415bfe4be131b8533888145a5ee79f30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Thu, 7 May 2026 23:17:20 +0200 Subject: [PATCH 03/21] Add implementation plan for as-lan framework support Bite-sized TDD task breakdown covering SDK_CONFIGS entry, alias map, resolver helper, validator updates, build/test command consumption, create-command template registration, and docs. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../plans/2026-04-28-aslan-framework.md | 767 ++++++++++++++++++ 1 file changed, 767 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-28-aslan-framework.md diff --git a/docs/superpowers/plans/2026-04-28-aslan-framework.md b/docs/superpowers/plans/2026-04-28-aslan-framework.md new file mode 100644 index 0000000..72f2361 --- /dev/null +++ b/docs/superpowers/plans/2026-04-28-aslan-framework.md @@ -0,0 +1,767 @@ +# as-lan Framework Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Register as-lan as a built-in SDK in jammin so users can write `sdk: as-lan` (or `sdk: as-lan-0.0.4` / `sdk: aslan-0.0.4`) in `jammin.build.yml` instead of inlining the docker image and build/test commands. + +**Architecture:** Add a single canonical entry `aslan-0.0.4` to the existing `SDK_CONFIGS` map. Add a separate `SDK_ALIASES` map and a `resolveSdkId` helper next to it. Build-command and test-command call the helper before looking up `SDK_CONFIGS`. The Zod validator accepts both canonical keys and alias keys. A new `aslan` entry is added to the create-command template registry. + +**Tech Stack:** TypeScript, Bun (runtime + test runner), Zod (validation), Biome (lint/format), Commander (CLI). + +**Spec:** `docs/superpowers/specs/2026-04-28-aslan-framework-design.md` + +--- + +## File Structure + +| File | Status | Responsibility | +|---|---|---| +| `packages/jammin-sdk/config/sdk-configs.ts` | modify | New `aslan-0.0.4` entry, new `SDK_ALIASES` map, new `resolveSdkId` helper | +| `packages/jammin-sdk/config/sdk-configs.test.ts` | create | `resolveSdkId` unit tests | +| `packages/jammin-sdk/config/types/config.ts` | modify | Widen `ServiceConfig.sdk` to include alias keys | +| `packages/jammin-sdk/config/config-validator.ts` | modify | Accept alias keys in Zod schema; refresh error message | +| `packages/jammin-sdk/config/config-validator.test.ts` | modify | Add tests for alias acceptance / rejection | +| `bin/cli/src/commands/build-command.ts` | modify | Resolve string SDK ids via `resolveSdkId` before `SDK_CONFIGS` lookup | +| `bin/cli/src/commands/build-command.test.ts` | modify | Add a docker-command test using the `as-lan` alias | +| `bin/cli/src/commands/test-command.ts` | modify | Same resolver swap as build-command | +| `bin/cli/src/commands/create-command.ts` | modify | Register `aslan` template | +| `docs/src/service-examples.md` | modify | New "as-lan" docker section | + +--- + +## Task 1: Register `aslan-0.0.4` in `SDK_CONFIGS` + +**Files:** +- Modify: `packages/jammin-sdk/config/sdk-configs.ts` + +The validator's Zod enum is built from `Object.keys(SDK_CONFIGS)`, so adding the entry alone makes `sdk: aslan-0.0.4` accepted by the validator. We assert that with a focused validator test before changing any code. + +- [ ] **Step 1: Add a failing validator test for the canonical `aslan-0.0.4` key** + +Append to `packages/jammin-sdk/config/config-validator.test.ts`, inside the existing `describe("Validate Build Config", ...)` block (after the "Should reject inline custom SDK with empty strings" test, before the `Deployment Config Validation` describe block): + +```typescript +test("Should accept canonical aslan-0.0.4 SDK", () => { + const config = { + services: [ + { + path: "./services/example", + name: "example", + sdk: "aslan-0.0.4", + }, + ], + }; + + const result = validateBuildConfig(config); + expect(result.services[0]?.sdk).toBe("aslan-0.0.4"); +}); +``` + +- [ ] **Step 2: Run the new test, expect it to fail** + +```bash +bun test packages/jammin-sdk/config/config-validator.test.ts -t "Should accept canonical aslan-0.0.4 SDK" +``` + +Expected: FAIL — Zod rejects `aslan-0.0.4` because it's not in the enum yet. + +- [ ] **Step 3: Add the `aslan-0.0.4` entry to `SDK_CONFIGS`** + +In `packages/jammin-sdk/config/sdk-configs.ts`, append a new entry after the `jamc3-1.1.2` entry (before the closing `}`): + +```typescript +import type { SdkConfig } from "./types/config.js"; + +export const SDK_CONFIGS = { + "jam-sdk-0.1.26": { + image: "ghcr.io/fluffylabs/jammin-jam-sdk:0.1.26", + build: "jam-pvm-build -m service", + test: "cargo test", + }, + "jambrains-1cfc41c": { + image: + "ghcr.io/jambrains/service-sdk:latest@sha256:1cfc41c23f5c348aaee5f5c70aaa24f10c26baf903de4b4f6774e2032820ba87", + build: "single-file main.c", + test: "true", + }, + "jade-0.0.15-pre.1": { + image: "ghcr.io/fluffylabs/jammin-jade:0.0.15-pre.1", + build: "build", + test: "test", + }, + "ajanta-0.1.0": { + image: "ghcr.io/fluffylabs/jammin-ajanta:0.1.0", + build: "ajanta build main.py -o service.jam", + test: "true", + }, + "jamc3-1.1.2": { + image: "ghcr.io/dreverr/jamc3:1.1.2", + build: "main.c3 -o service.jam", + test: "bun test", + }, + "aslan-0.0.4": { + image: "ghcr.io/tomusdrw/jammin-as-lan:0.0.4", + build: "npm run build", + test: "npm test", + }, +} as const satisfies Record; +``` + +- [ ] **Step 4: Run the validator tests, expect all to pass** + +```bash +bun test packages/jammin-sdk/config/config-validator.test.ts +``` + +Expected: PASS for the new test plus all previously-passing tests. + +- [ ] **Step 5: Commit** + +```bash +git add packages/jammin-sdk/config/sdk-configs.ts packages/jammin-sdk/config/config-validator.test.ts +git commit -m "Add aslan-0.0.4 entry to SDK_CONFIGS" +``` + +--- + +## Task 2: Add `SDK_ALIASES` map and `resolveSdkId` helper + +**Files:** +- Modify: `packages/jammin-sdk/config/sdk-configs.ts` +- Create: `packages/jammin-sdk/config/sdk-configs.test.ts` + +The helper resolves any accepted string SDK identifier (canonical key or alias) to a canonical `SDK_CONFIGS` key, returning `undefined` for unknown strings. It does NOT handle the `SdkConfig` object form — that's a structurally-different value handled by the caller. + +- [ ] **Step 1: Write failing tests for `SDK_ALIASES` shape and `resolveSdkId` behaviour** + +Create `packages/jammin-sdk/config/sdk-configs.test.ts`: + +```typescript +import { describe, expect, test } from "bun:test"; +import { resolveSdkId, SDK_ALIASES, SDK_CONFIGS } from "./sdk-configs.js"; + +describe("SDK_ALIASES", () => { + test("Every alias target points to a canonical SDK_CONFIGS key", () => { + for (const target of Object.values(SDK_ALIASES)) { + expect(SDK_CONFIGS).toHaveProperty(target); + } + }); + + test("Bare 'as-lan' alias resolves to aslan-0.0.4", () => { + expect(SDK_ALIASES["as-lan"]).toBe("aslan-0.0.4"); + }); + + test("Versioned 'as-lan-0.0.4' alias resolves to aslan-0.0.4", () => { + expect(SDK_ALIASES["as-lan-0.0.4"]).toBe("aslan-0.0.4"); + }); +}); + +describe("resolveSdkId", () => { + test("Returns the input when it is already a canonical SDK_CONFIGS key", () => { + expect(resolveSdkId("aslan-0.0.4")).toBe("aslan-0.0.4"); + expect(resolveSdkId("jam-sdk-0.1.26")).toBe("jam-sdk-0.1.26"); + }); + + test("Resolves the bare 'as-lan' alias to aslan-0.0.4", () => { + expect(resolveSdkId("as-lan")).toBe("aslan-0.0.4"); + }); + + test("Resolves the versioned 'as-lan-0.0.4' alias to aslan-0.0.4", () => { + expect(resolveSdkId("as-lan-0.0.4")).toBe("aslan-0.0.4"); + }); + + test("Returns undefined for unknown identifiers", () => { + expect(resolveSdkId("nonsense")).toBeUndefined(); + expect(resolveSdkId("as-lan-0.0.3")).toBeUndefined(); + expect(resolveSdkId("aslan")).toBeUndefined(); + }); +}); +``` + +- [ ] **Step 2: Run the new tests, expect them to fail** + +```bash +bun test packages/jammin-sdk/config/sdk-configs.test.ts +``` + +Expected: FAIL — `SDK_ALIASES` and `resolveSdkId` aren't exported yet. + +- [ ] **Step 3: Add `SDK_ALIASES` and `resolveSdkId` to `sdk-configs.ts`** + +Append to `packages/jammin-sdk/config/sdk-configs.ts` after the `SDK_CONFIGS` declaration: + +```typescript +export const SDK_ALIASES = { + "as-lan": "aslan-0.0.4", + "as-lan-0.0.4": "aslan-0.0.4", +} as const satisfies Record; + +/** + * Resolve a string SDK identifier (canonical key or alias) to a canonical + * SDK_CONFIGS key. Returns undefined for unknown identifiers. + */ +export function resolveSdkId(id: string): keyof typeof SDK_CONFIGS | undefined { + if (id in SDK_CONFIGS) { + return id as keyof typeof SDK_CONFIGS; + } + if (id in SDK_ALIASES) { + return SDK_ALIASES[id as keyof typeof SDK_ALIASES]; + } + return undefined; +} +``` + +- [ ] **Step 4: Run the new tests, expect them to pass** + +```bash +bun test packages/jammin-sdk/config/sdk-configs.test.ts +``` + +Expected: PASS for all four `resolveSdkId` tests and the three `SDK_ALIASES` tests. + +- [ ] **Step 5: Commit** + +```bash +git add packages/jammin-sdk/config/sdk-configs.ts packages/jammin-sdk/config/sdk-configs.test.ts +git commit -m "Add SDK_ALIASES map and resolveSdkId helper" +``` + +--- + +## Task 3: Widen `ServiceConfig.sdk` type to include alias keys + +**Files:** +- Modify: `packages/jammin-sdk/config/types/config.ts` + +This is a type-only change. It lets TypeScript users construct `ServiceConfig` objects with alias strings (e.g. in tests, or in callers building config objects in code). Runtime behaviour is unchanged. + +- [ ] **Step 1: Add a failing type-level assertion test** + +Two edits to `packages/jammin-sdk/config/sdk-configs.test.ts`: + +(a) Add this import line at the top of the file, alongside the existing imports: + +```typescript +import type { ServiceConfig } from "./types/config.js"; +``` + +(b) Append a new `describe` block after the existing `describe("resolveSdkId", ...)` block: + +```typescript +describe("ServiceConfig.sdk type accepts alias strings", () => { + test("Compiles when sdk is an alias key", () => { + const cfg: ServiceConfig = { + path: "./services/example", + name: "example", + sdk: "as-lan", + }; + expect(cfg.sdk).toBe("as-lan"); + }); + + test("Compiles when sdk is a versioned alias key", () => { + const cfg: ServiceConfig = { + path: "./services/example", + name: "example", + sdk: "as-lan-0.0.4", + }; + expect(cfg.sdk).toBe("as-lan-0.0.4"); + }); +}); +``` + +- [ ] **Step 2: Run the new test file, expect type errors** + +```bash +bun test packages/jammin-sdk/config/sdk-configs.test.ts +``` + +Expected: FAIL with TypeScript errors like `Type '"as-lan"' is not assignable to type ...`. + +- [ ] **Step 3: Widen `ServiceConfig.sdk`** + +Edit `packages/jammin-sdk/config/types/config.ts`. Change the import line to include `SDK_ALIASES`: + +```typescript +import type { SDK_ALIASES, SDK_CONFIGS } from "../sdk-configs.js"; +``` + +And change the `sdk` field on the `ServiceConfig` interface from: + +```typescript +sdk: keyof typeof SDK_CONFIGS | SdkConfig; +``` + +to: + +```typescript +sdk: keyof typeof SDK_CONFIGS | keyof typeof SDK_ALIASES | SdkConfig; +``` + +- [ ] **Step 4: Run the file's tests, expect pass** + +```bash +bun test packages/jammin-sdk/config/sdk-configs.test.ts +``` + +Expected: PASS for all tests, no type errors. + +- [ ] **Step 5: Run the project-wide type check + lint to catch any consumer that broke** + +```bash +bun run qa +``` + +Expected: PASS. (If a consumer was narrowing the old type, fix locally — none expected based on the spec, but check.) + +- [ ] **Step 6: Commit** + +```bash +git add packages/jammin-sdk/config/types/config.ts packages/jammin-sdk/config/sdk-configs.test.ts +git commit -m "Widen ServiceConfig.sdk type to accept alias keys" +``` + +--- + +## Task 4: Validator accepts alias keys + +**Files:** +- Modify: `packages/jammin-sdk/config/config-validator.ts` +- Modify: `packages/jammin-sdk/config/config-validator.test.ts` + +The Zod schema needs to accept `as-lan` and `as-lan-0.0.4` in addition to the canonical keys. The error message lists both sets so users can debug typos. + +- [ ] **Step 1: Add failing alias-acceptance tests** + +Append to `packages/jammin-sdk/config/config-validator.test.ts`, inside the existing `describe("Validate Build Config", ...)` block, near the `aslan-0.0.4` test added in Task 1: + +```typescript +test("Should accept bare 'as-lan' alias as SDK", () => { + const config = { + services: [ + { + path: "./services/example", + name: "example", + sdk: "as-lan", + }, + ], + }; + + const result = validateBuildConfig(config); + expect(result.services[0]?.sdk).toBe("as-lan"); +}); + +test("Should accept versioned 'as-lan-0.0.4' alias as SDK", () => { + const config = { + services: [ + { + path: "./services/example", + name: "example", + sdk: "as-lan-0.0.4", + }, + ], + }; + + const result = validateBuildConfig(config); + expect(result.services[0]?.sdk).toBe("as-lan-0.0.4"); +}); + +test("Should reject unknown 'as-lan-0.0.3' alias", () => { + const config = { + services: [ + { + path: "./services/example", + name: "example", + sdk: "as-lan-0.0.3", + }, + ], + }; + + expect(() => validateBuildConfig(config)).toThrow(); +}); + +test("Should reject misspelt canonical 'aslan' (no version)", () => { + const config = { + services: [ + { + path: "./services/example", + name: "example", + sdk: "aslan", + }, + ], + }; + + expect(() => validateBuildConfig(config)).toThrow(); +}); +``` + +- [ ] **Step 2: Run the new tests, expect the alias-acceptance ones to fail** + +```bash +bun test packages/jammin-sdk/config/config-validator.test.ts -t "alias" +``` + +Expected: FAIL on the two acceptance tests (validator currently rejects alias strings); PASS on the two rejection tests (validator already rejects unknown strings). + +- [ ] **Step 3: Update the validator to accept aliases** + +Edit `packages/jammin-sdk/config/config-validator.ts`. Change the import line at the top: + +```typescript +import { SDK_ALIASES, SDK_CONFIGS } from "./sdk-configs.js"; +``` + +Replace the `ServiceConfigSchema` `sdk` field's union (currently around lines 32-35): + +```typescript +sdk: z.union( + [z.enum(Object.keys(SDK_CONFIGS) as (keyof typeof SDK_CONFIGS)[]), SdkConfigSchema], + `Expected a valid custom SDK configuration or one of the supported SDK ids (${Object.keys(SDK_CONFIGS).join(", ")})`, +), +``` + +with: + +```typescript +sdk: z.union( + [ + z.enum([ + ...(Object.keys(SDK_CONFIGS) as (keyof typeof SDK_CONFIGS)[]), + ...(Object.keys(SDK_ALIASES) as (keyof typeof SDK_ALIASES)[]), + ]), + SdkConfigSchema, + ], + `Expected a valid custom SDK configuration or one of the supported SDK ids (${Object.keys(SDK_CONFIGS).join(", ")}) or aliases (${Object.keys(SDK_ALIASES).join(", ")})`, +), +``` + +- [ ] **Step 4: Run the validator tests, expect all to pass** + +```bash +bun test packages/jammin-sdk/config/config-validator.test.ts +``` + +Expected: PASS for all tests, including the four new ones from this task. + +- [ ] **Step 5: Commit** + +```bash +git add packages/jammin-sdk/config/config-validator.ts packages/jammin-sdk/config/config-validator.test.ts +git commit -m "Accept SDK aliases in build config validator" +``` + +--- + +## Task 5: Build command resolves alias before docker invocation + +**Files:** +- Modify: `bin/cli/src/commands/build-command.ts` +- Modify: `bin/cli/src/commands/build-command.test.ts` + +`callDockerBuild` currently does `SDK_CONFIGS[service.sdk]` directly when `service.sdk` is a string. With aliases, that lookup returns `undefined` for `as-lan` because the alias key isn't in `SDK_CONFIGS`. We route string SDK ids through `resolveSdkId` first. + +- [ ] **Step 1: Add a failing test for the alias path through `callDockerBuild`** + +Append to `bin/cli/src/commands/build-command.test.ts`, inside the `describe("buildService - Docker command generation", ...)` block (after the existing "should generate correct Docker command for predefined SDK (jade)" test): + +```typescript +test("should generate correct Docker command for as-lan alias", async () => { + const service: ServiceConfig = { + name: "as-lan-service", + path: "./services/example", + sdk: "as-lan", + }; + + await callDockerBuild(service, "/test/project"); + + expect(mockSpawn).toHaveBeenCalledTimes(1); + const spawnCall = mockSpawn.mock.calls[0]; + if (!spawnCall) { + throw new Error("spawnCall is undefined"); + } + const dockerCommand = spawnCall[0][2] as string; + + expect(dockerCommand).toContain(SDK_CONFIGS["aslan-0.0.4"].image); + expect(dockerCommand).toContain(SDK_CONFIGS["aslan-0.0.4"].build); + expect(dockerCommand).toContain(`${resolve("/test/project", "./services/example")}:/app`); +}); +``` + +- [ ] **Step 2: Run the new test, expect failure** + +```bash +bun test bin/cli/src/commands/build-command.test.ts -t "as-lan alias" +``` + +Expected: FAIL — `SDK_CONFIGS["as-lan"]` is `undefined`, so the docker command won't include the expected image/build strings (likely throws on `sdk.build.split` because `sdk` is `undefined`). + +- [ ] **Step 3: Use `resolveSdkId` in `callDockerBuild`** + +Edit `bin/cli/src/commands/build-command.ts`. Update the import block (around lines 4-13) to add `resolveSdkId` and `type SdkConfig`: + +```typescript +import type { ServiceConfig, SdkConfig } from "@fluffylabs/jammin-sdk"; +import { + copyJamToDist, + generateTestConfigInProjectDir, + getJamFiles, + getServiceConfigs, + loadServices, + resolveSdkId, + SDK_CONFIGS, +} from "@fluffylabs/jammin-sdk"; +``` + +Replace line 28 in `callDockerBuild`: + +```typescript +const sdk = typeof service.sdk === "string" ? SDK_CONFIGS[service.sdk] : service.sdk; +``` + +with: + +```typescript +let sdk: SdkConfig; +if (typeof service.sdk === "string") { + const canonicalId = resolveSdkId(service.sdk); + if (!canonicalId) { + throw new Error(`Unknown SDK id: '${service.sdk}'`); + } + sdk = SDK_CONFIGS[canonicalId]; +} else { + sdk = service.sdk; +} +``` + +- [ ] **Step 4: Run the build-command tests, expect all to pass** + +```bash +bun test bin/cli/src/commands/build-command.test.ts +``` + +Expected: PASS for the new alias test plus all previously-passing tests. + +- [ ] **Step 5: Commit** + +```bash +git add bin/cli/src/commands/build-command.ts bin/cli/src/commands/build-command.test.ts +git commit -m "Resolve SDK aliases in build command" +``` + +--- + +## Task 6: Test command resolves alias before docker invocation + +**Files:** +- Modify: `bin/cli/src/commands/test-command.ts` + +Same change as Task 5, applied to `testService`. No new test file is added; Task 2 already covers `resolveSdkId` in isolation, and Task 5's test exercises the same helper through a docker invocation. + +- [ ] **Step 1: Use `resolveSdkId` in `testService`** + +Edit `bin/cli/src/commands/test-command.ts`. Update the import block (lines 4-5): + +```typescript +import type { ServiceConfig, SdkConfig } from "@fluffylabs/jammin-sdk"; +import { getServiceConfigs, resolveSdkId, SDK_CONFIGS } from "@fluffylabs/jammin-sdk"; +``` + +Replace line 21 in `testService`: + +```typescript +const sdk = typeof service.sdk === "string" ? SDK_CONFIGS[service.sdk] : service.sdk; +``` + +with: + +```typescript +let sdk: SdkConfig; +if (typeof service.sdk === "string") { + const canonicalId = resolveSdkId(service.sdk); + if (!canonicalId) { + throw new Error(`Unknown SDK id: '${service.sdk}'`); + } + sdk = SDK_CONFIGS[canonicalId]; +} else { + sdk = service.sdk; +} +``` + +- [ ] **Step 2: Run the full CLI test suite to confirm nothing regressed** + +```bash +bun test bin/cli/ +``` + +Expected: PASS for all tests. + +- [ ] **Step 3: Commit** + +```bash +git add bin/cli/src/commands/test-command.ts +git commit -m "Resolve SDK aliases in test command" +``` + +--- + +## Task 7: Register `aslan` template in create-command + +**Files:** +- Modify: `bin/cli/src/commands/create-command.ts` + +Pattern is identical to commit `b995067` (C3 SDK addition). + +- [ ] **Step 1: Add `aslan` to the `Template` union and `TARGETS` map** + +Edit `bin/cli/src/commands/create-command.ts`. Replace lines 5-14: + +```typescript +type Template = "jam-sdk" | "jade" | "jambrains" | "ajanta" | "jamc3" | "undecided"; + +const TARGETS: Record = { + "jam-sdk": "jammin-create/jammin-create-jam-sdk", + jade: "jammin-create/jammin-create-jade", + jambrains: "jammin-create/jammin-create-jambrains", + ajanta: "jammin-create/jammin-create-ajanta", + jamc3: "jammin-create/jammin-create-jamc3", + undecided: "jammin-create/jammin-create-undecided", +}; +``` + +with: + +```typescript +type Template = "jam-sdk" | "jade" | "jambrains" | "ajanta" | "jamc3" | "aslan" | "undecided"; + +const TARGETS: Record = { + "jam-sdk": "jammin-create/jammin-create-jam-sdk", + jade: "jammin-create/jammin-create-jade", + jambrains: "jammin-create/jammin-create-jambrains", + ajanta: "jammin-create/jammin-create-ajanta", + jamc3: "jammin-create/jammin-create-jamc3", + aslan: "jammin-create/jammin-create-aslan", + undecided: "jammin-create/jammin-create-undecided", +}; +``` + +- [ ] **Step 2: Run the create-command tests, expect pass** + +```bash +bun test bin/cli/src/commands/create-command.test.ts +``` + +Expected: PASS for all existing tests (the change is additive; existing tests don't enumerate templates). + +- [ ] **Step 3: Smoke-test the help output to confirm `aslan` is offered** + +```bash +bun run cli create --help +``` + +Expected: Output lists `aslan` as one of the `--template` choices. + +- [ ] **Step 4: Commit** + +```bash +git add bin/cli/src/commands/create-command.ts +git commit -m "Register aslan template in create-command" +``` + +--- + +## Task 8: Document the as-lan SDK in service-examples.md + +**Files:** +- Modify: `docs/src/service-examples.md` + +The existing page has sections for JAM SDK, JamBrains, and Jade. Add an "as-lan" section that mirrors that style. + +- [ ] **Step 1: Add the as-lan section** + +Append the following literal markdown to `docs/src/service-examples.md` after the existing Jade section. Mirror the existing section format (heading hierarchy `###` for SDK name, `####` for "Unit tests"). Use the Write/Edit tool — these are real fenced code blocks, not nested-string escapes. + +Section content (semantic outline; render to markdown using the same shape as the existing JAM SDK / JamBrains / Jade sections in this same file): + +1. `### as-lan` heading. +2. Short paragraph: "The as-lan docker image ships with Node.js, `wasm-pvm`, and the AssemblyScript toolchain pre-installed. Pull it:" +3. Console-fenced block: `$ docker pull ghcr.io/tomusdrw/jammin-as-lan:0.0.4` +4. Short paragraph: "Then `cd` into the example code directory and build:" +5. Console-fenced block, two lines: + - `$ cd jammin-create-aslan/services/example` + - `$ docker run --rm -v $(pwd):/app ghcr.io/tomusdrw/jammin-as-lan:0.0.4 npm run build` +6. Short paragraph: "The image's entrypoint symlinks the global toolchain into `/app/node_modules` if no `node_modules` already exists in the mounted directory." +7. `#### Unit tests` subheading. +8. Console-fenced block: `$ docker run --rm -v $(pwd):/app ghcr.io/tomusdrw/jammin-as-lan:0.0.4 npm test` +9. `#### SDK names accepted in jammin.build.yml` subheading. +10. Short paragraph: "Any of the following resolve to the same image and commands:" +11. Bulleted list (three items): + - `` `aslan-0.0.4` `` (canonical key in `SDK_CONFIGS`) + - `` `as-lan-0.0.4` `` (versioned alias matching the framework's spelling) + - `` `as-lan` `` (bare alias — follows the current default version) + +- [ ] **Step 2: Verify the markdown renders cleanly** + +```bash +ls docs/src/service-examples.md && head -120 docs/src/service-examples.md +``` + +Expected: file ends with the new as-lan section, fenced blocks are well-formed. + +- [ ] **Step 3: Commit** + +```bash +git add docs/src/service-examples.md +git commit -m "Document as-lan SDK in service examples" +``` + +--- + +## Task 9: Final QA + smoke verification + +**Files:** none modified + +- [ ] **Step 1: Run the project's full QA gate** + +```bash +bun run qa +``` + +Expected: PASS — Biome formatting/lint is clean, type checks pass. + +- [ ] **Step 2: Run the full test suite** + +```bash +bun test +``` + +Expected: PASS for all tests, including the new ones added in Tasks 1, 2, 3, 4, 5. + +- [ ] **Step 3: Smoke test that an `as-lan` config validates end-to-end** + +```bash +bun -e 'import("./packages/jammin-sdk/config/config-validator.js").then(m => { console.log(JSON.stringify(m.validateBuildConfig({ services: [{ path: "./svc", name: "svc", sdk: "as-lan" }] }), null, 2)); })' +``` + +Expected: prints a JSON object with `services[0].sdk === "as-lan"`, no thrown error. + +- [ ] **Step 4: Confirm git log is clean** + +```bash +git log --oneline origin/main..HEAD +``` + +Expected: 8 commits, one per implementation task plus the two existing spec commits already on the branch. + +--- + +## Self-review checklist (run before opening PR) + +- [ ] Spec section "Canonical SDK key" → covered by Task 1 +- [ ] Spec section "Aliases" → covered by Task 2 +- [ ] Spec section "Resolver helper" → covered by Task 2 +- [ ] Spec section "Type widening" → covered by Task 3 +- [ ] Spec section "Validation" → covered by Task 4 +- [ ] Spec section "Build/test command consumption" → covered by Tasks 5 and 6 +- [ ] Spec section "Create-command template" → covered by Task 7 +- [ ] Spec section "Tests" → all listed test cases present in Tasks 1, 2, 4, 5 +- [ ] Spec section "Docs" → covered by Task 8 +- [ ] Validation matrix in spec → exercised by tests in Tasks 1, 2, 4 From fbc40e98e136211cd785d3a9af66f60e120ef392 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Thu, 7 May 2026 23:21:31 +0200 Subject: [PATCH 04/21] Add aslan-0.0.4 entry to SDK_CONFIGS --- .../jammin-sdk/config/config-validator.test.ts | 15 +++++++++++++++ packages/jammin-sdk/config/sdk-configs.ts | 5 +++++ 2 files changed, 20 insertions(+) diff --git a/packages/jammin-sdk/config/config-validator.test.ts b/packages/jammin-sdk/config/config-validator.test.ts index 0274c54..019c8bf 100644 --- a/packages/jammin-sdk/config/config-validator.test.ts +++ b/packages/jammin-sdk/config/config-validator.test.ts @@ -159,6 +159,21 @@ describe("Validate Build Config", () => { expect(() => validateBuildConfig(config)).toThrow("SDK image is required"); }); + test("Should accept canonical aslan-0.0.4 SDK", () => { + const config = { + services: [ + { + path: "./services/example", + name: "example", + sdk: "aslan-0.0.4", + }, + ], + }; + + const result = validateBuildConfig(config); + expect(result.services[0]?.sdk).toBe("aslan-0.0.4"); + }); + describe("Deployment Config Validation", () => { test("Should parse valid deployment config with spawn and services", () => { const config = { diff --git a/packages/jammin-sdk/config/sdk-configs.ts b/packages/jammin-sdk/config/sdk-configs.ts index 2dcf238..7152bc4 100644 --- a/packages/jammin-sdk/config/sdk-configs.ts +++ b/packages/jammin-sdk/config/sdk-configs.ts @@ -27,4 +27,9 @@ export const SDK_CONFIGS = { build: "main.c3 -o service.jam", test: "bun test", }, + "aslan-0.0.4": { + image: "ghcr.io/tomusdrw/jammin-as-lan:0.0.4", + build: "npm run build", + test: "npm test", + }, } as const satisfies Record; From a3f07b3184bcf4d4cd1b313535596e7684c84e47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Thu, 7 May 2026 23:25:10 +0200 Subject: [PATCH 05/21] Add SDK_ALIASES map and resolveSdkId helper --- .../jammin-sdk/config/sdk-configs.test.ts | 39 +++++++++++++++++++ packages/jammin-sdk/config/sdk-configs.ts | 19 +++++++++ 2 files changed, 58 insertions(+) create mode 100644 packages/jammin-sdk/config/sdk-configs.test.ts diff --git a/packages/jammin-sdk/config/sdk-configs.test.ts b/packages/jammin-sdk/config/sdk-configs.test.ts new file mode 100644 index 0000000..33743fe --- /dev/null +++ b/packages/jammin-sdk/config/sdk-configs.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, test } from "bun:test"; +import { resolveSdkId, SDK_ALIASES, SDK_CONFIGS } from "./sdk-configs.js"; + +describe("SDK_ALIASES", () => { + test("Every alias target points to a canonical SDK_CONFIGS key", () => { + for (const target of Object.values(SDK_ALIASES)) { + expect(Object.hasOwn(SDK_CONFIGS, target)).toBe(true); + } + }); + + test("Bare 'as-lan' alias resolves to aslan-0.0.4", () => { + expect(SDK_ALIASES["as-lan"]).toBe("aslan-0.0.4"); + }); + + test("Versioned 'as-lan-0.0.4' alias resolves to aslan-0.0.4", () => { + expect(SDK_ALIASES["as-lan-0.0.4"]).toBe("aslan-0.0.4"); + }); +}); + +describe("resolveSdkId", () => { + test("Returns the input when it is already a canonical SDK_CONFIGS key", () => { + expect(resolveSdkId("aslan-0.0.4")).toBe("aslan-0.0.4"); + expect(resolveSdkId("jam-sdk-0.1.26")).toBe("jam-sdk-0.1.26"); + }); + + test("Resolves the bare 'as-lan' alias to aslan-0.0.4", () => { + expect(resolveSdkId("as-lan")).toBe("aslan-0.0.4"); + }); + + test("Resolves the versioned 'as-lan-0.0.4' alias to aslan-0.0.4", () => { + expect(resolveSdkId("as-lan-0.0.4")).toBe("aslan-0.0.4"); + }); + + test("Returns undefined for unknown identifiers", () => { + expect(resolveSdkId("nonsense")).toBeUndefined(); + expect(resolveSdkId("as-lan-0.0.3")).toBeUndefined(); + expect(resolveSdkId("aslan")).toBeUndefined(); + }); +}); diff --git a/packages/jammin-sdk/config/sdk-configs.ts b/packages/jammin-sdk/config/sdk-configs.ts index 7152bc4..6663e3f 100644 --- a/packages/jammin-sdk/config/sdk-configs.ts +++ b/packages/jammin-sdk/config/sdk-configs.ts @@ -33,3 +33,22 @@ export const SDK_CONFIGS = { test: "npm test", }, } as const satisfies Record; + +export const SDK_ALIASES = { + "as-lan": "aslan-0.0.4", + "as-lan-0.0.4": "aslan-0.0.4", +} as const satisfies Record; + +/** + * Resolve a string SDK identifier (canonical key or alias) to a canonical + * SDK_CONFIGS key. Returns undefined for unknown identifiers. + */ +export function resolveSdkId(id: string): keyof typeof SDK_CONFIGS | undefined { + if (id in SDK_CONFIGS) { + return id as keyof typeof SDK_CONFIGS; + } + if (id in SDK_ALIASES) { + return SDK_ALIASES[id as keyof typeof SDK_ALIASES]; + } + return undefined; +} From 4eda534d5b10995384c4ed29e5b96b232a59f908 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Thu, 7 May 2026 23:28:01 +0200 Subject: [PATCH 06/21] Fix resolveSdkId to ignore prototype properties --- packages/jammin-sdk/config/sdk-configs.test.ts | 10 ++++++++++ packages/jammin-sdk/config/sdk-configs.ts | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/jammin-sdk/config/sdk-configs.test.ts b/packages/jammin-sdk/config/sdk-configs.test.ts index 33743fe..8cd5ec6 100644 --- a/packages/jammin-sdk/config/sdk-configs.test.ts +++ b/packages/jammin-sdk/config/sdk-configs.test.ts @@ -36,4 +36,14 @@ describe("resolveSdkId", () => { expect(resolveSdkId("as-lan-0.0.3")).toBeUndefined(); expect(resolveSdkId("aslan")).toBeUndefined(); }); + + test("Returns undefined for prototype properties (toString, constructor, etc.)", () => { + expect(resolveSdkId("toString")).toBeUndefined(); + expect(resolveSdkId("constructor")).toBeUndefined(); + expect(resolveSdkId("hasOwnProperty")).toBeUndefined(); + }); + + test("Returns undefined for empty string", () => { + expect(resolveSdkId("")).toBeUndefined(); + }); }); diff --git a/packages/jammin-sdk/config/sdk-configs.ts b/packages/jammin-sdk/config/sdk-configs.ts index 6663e3f..f936838 100644 --- a/packages/jammin-sdk/config/sdk-configs.ts +++ b/packages/jammin-sdk/config/sdk-configs.ts @@ -44,10 +44,10 @@ export const SDK_ALIASES = { * SDK_CONFIGS key. Returns undefined for unknown identifiers. */ export function resolveSdkId(id: string): keyof typeof SDK_CONFIGS | undefined { - if (id in SDK_CONFIGS) { + if (Object.hasOwn(SDK_CONFIGS, id)) { return id as keyof typeof SDK_CONFIGS; } - if (id in SDK_ALIASES) { + if (Object.hasOwn(SDK_ALIASES, id)) { return SDK_ALIASES[id as keyof typeof SDK_ALIASES]; } return undefined; From 05128a39a0ced07847e5618122fd9afbd6029ef5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Thu, 7 May 2026 23:30:00 +0200 Subject: [PATCH 07/21] Widen ServiceConfig.sdk type to accept alias keys --- .../jammin-sdk/config/sdk-configs.test.ts | 21 +++++++++++++++++++ packages/jammin-sdk/config/types/config.ts | 4 ++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/packages/jammin-sdk/config/sdk-configs.test.ts b/packages/jammin-sdk/config/sdk-configs.test.ts index 8cd5ec6..9599f88 100644 --- a/packages/jammin-sdk/config/sdk-configs.test.ts +++ b/packages/jammin-sdk/config/sdk-configs.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "bun:test"; import { resolveSdkId, SDK_ALIASES, SDK_CONFIGS } from "./sdk-configs.js"; +import type { ServiceConfig } from "./types/config.js"; describe("SDK_ALIASES", () => { test("Every alias target points to a canonical SDK_CONFIGS key", () => { @@ -47,3 +48,23 @@ describe("resolveSdkId", () => { expect(resolveSdkId("")).toBeUndefined(); }); }); + +describe("ServiceConfig.sdk type accepts alias strings", () => { + test("Compiles when sdk is an alias key", () => { + const cfg: ServiceConfig = { + path: "./services/example", + name: "example", + sdk: "as-lan", + }; + expect(cfg.sdk).toBe("as-lan"); + }); + + test("Compiles when sdk is a versioned alias key", () => { + const cfg: ServiceConfig = { + path: "./services/example", + name: "example", + sdk: "as-lan-0.0.4", + }; + expect(cfg.sdk).toBe("as-lan-0.0.4"); + }); +}); diff --git a/packages/jammin-sdk/config/types/config.ts b/packages/jammin-sdk/config/types/config.ts index 1e37423..d43f819 100644 --- a/packages/jammin-sdk/config/types/config.ts +++ b/packages/jammin-sdk/config/types/config.ts @@ -1,6 +1,6 @@ // Core configuration types matching YAML schema -import type { SDK_CONFIGS } from "../sdk-configs.js"; +import type { SDK_ALIASES, SDK_CONFIGS } from "../sdk-configs.js"; // jammin.build.yml types @@ -15,7 +15,7 @@ export interface ServiceConfig { /** Service identifier */ name: string; /** SDK name (built-in) or custom sdk */ - sdk: keyof typeof SDK_CONFIGS | SdkConfig; + sdk: keyof typeof SDK_CONFIGS | keyof typeof SDK_ALIASES | SdkConfig; } export interface SdkConfig { From 4376a7d4184b50b0e3161fc444de4e5773742d71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Thu, 7 May 2026 23:33:36 +0200 Subject: [PATCH 08/21] Replace runtime type tautologies with compile-time assertions Co-Authored-By: Claude Sonnet 4.6 --- .../jammin-sdk/config/sdk-configs.test.ts | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/jammin-sdk/config/sdk-configs.test.ts b/packages/jammin-sdk/config/sdk-configs.test.ts index 9599f88..7c97be1 100644 --- a/packages/jammin-sdk/config/sdk-configs.test.ts +++ b/packages/jammin-sdk/config/sdk-configs.test.ts @@ -50,21 +50,19 @@ describe("resolveSdkId", () => { }); describe("ServiceConfig.sdk type accepts alias strings", () => { - test("Compiles when sdk is an alias key", () => { + type Assert = T; + + // Compile-time assertions: these fail tsc if the union narrows. + type _AliasAccepted = Assert<"as-lan" extends ServiceConfig["sdk"] ? true : false>; + type _VersionedAliasAccepted = Assert<"as-lan-0.0.4" extends ServiceConfig["sdk"] ? true : false>; + type _CanonicalAccepted = Assert<"aslan-0.0.4" extends ServiceConfig["sdk"] ? true : false>; + + test("Constructs a ServiceConfig literal with an alias key", () => { const cfg: ServiceConfig = { - path: "./services/example", - name: "example", + path: "./svc", + name: "svc", sdk: "as-lan", }; expect(cfg.sdk).toBe("as-lan"); }); - - test("Compiles when sdk is a versioned alias key", () => { - const cfg: ServiceConfig = { - path: "./services/example", - name: "example", - sdk: "as-lan-0.0.4", - }; - expect(cfg.sdk).toBe("as-lan-0.0.4"); - }); }); From c693614af4ece18d0877b1ed89792bed7e3161ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Thu, 7 May 2026 23:34:48 +0200 Subject: [PATCH 09/21] Accept SDK aliases in build config validator --- .../config/config-validator.test.ts | 58 +++++++++++++++++++ .../jammin-sdk/config/config-validator.ts | 12 +++- 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/packages/jammin-sdk/config/config-validator.test.ts b/packages/jammin-sdk/config/config-validator.test.ts index 019c8bf..d58a09a 100644 --- a/packages/jammin-sdk/config/config-validator.test.ts +++ b/packages/jammin-sdk/config/config-validator.test.ts @@ -174,6 +174,64 @@ describe("Validate Build Config", () => { expect(result.services[0]?.sdk).toBe("aslan-0.0.4"); }); + test("Should accept bare 'as-lan' alias as SDK", () => { + const config = { + services: [ + { + path: "./services/example", + name: "example", + sdk: "as-lan", + }, + ], + }; + + const result = validateBuildConfig(config); + expect(result.services[0]?.sdk).toBe("as-lan"); + }); + + test("Should accept versioned 'as-lan-0.0.4' alias as SDK", () => { + const config = { + services: [ + { + path: "./services/example", + name: "example", + sdk: "as-lan-0.0.4", + }, + ], + }; + + const result = validateBuildConfig(config); + expect(result.services[0]?.sdk).toBe("as-lan-0.0.4"); + }); + + test("Should reject unknown 'as-lan-0.0.3' alias", () => { + const config = { + services: [ + { + path: "./services/example", + name: "example", + sdk: "as-lan-0.0.3", + }, + ], + }; + + expect(() => validateBuildConfig(config)).toThrow(); + }); + + test("Should reject misspelt canonical 'aslan' (no version)", () => { + const config = { + services: [ + { + path: "./services/example", + name: "example", + sdk: "aslan", + }, + ], + }; + + expect(() => validateBuildConfig(config)).toThrow(); + }); + describe("Deployment Config Validation", () => { test("Should parse valid deployment config with spawn and services", () => { const config = { diff --git a/packages/jammin-sdk/config/config-validator.ts b/packages/jammin-sdk/config/config-validator.ts index 56badf8..92b5ba4 100644 --- a/packages/jammin-sdk/config/config-validator.ts +++ b/packages/jammin-sdk/config/config-validator.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { SDK_CONFIGS } from "./sdk-configs.js"; +import { SDK_ALIASES, SDK_CONFIGS } from "./sdk-configs.js"; // Zod schemas for runtime validation of YAML configs @@ -30,8 +30,14 @@ const ServiceConfigSchema = z.object({ .min(1, "Service name is required") .regex(/^[a-zA-Z0-9_-]+$/, "Service name must contain only letters, numbers, hyphens, and underscores"), sdk: z.union( - [z.enum(Object.keys(SDK_CONFIGS) as (keyof typeof SDK_CONFIGS)[]), SdkConfigSchema], - `Expected a valid custom SDK configuration or one of the supported SDK ids (${Object.keys(SDK_CONFIGS).join(", ")})`, + [ + z.enum([ + ...(Object.keys(SDK_CONFIGS) as (keyof typeof SDK_CONFIGS)[]), + ...(Object.keys(SDK_ALIASES) as (keyof typeof SDK_ALIASES)[]), + ]), + SdkConfigSchema, + ], + `Expected a valid custom SDK configuration or one of the supported SDK ids (${Object.keys(SDK_CONFIGS).join(", ")}) or aliases (${Object.keys(SDK_ALIASES).join(", ")})`, ), }); From f277f0fb6535c78120bf2ecf35e2d5a722951bb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Thu, 7 May 2026 23:37:44 +0200 Subject: [PATCH 10/21] Tighten rejection-test assertions to match validator error message --- packages/jammin-sdk/config/config-validator.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/jammin-sdk/config/config-validator.test.ts b/packages/jammin-sdk/config/config-validator.test.ts index d58a09a..ffc842f 100644 --- a/packages/jammin-sdk/config/config-validator.test.ts +++ b/packages/jammin-sdk/config/config-validator.test.ts @@ -215,7 +215,7 @@ describe("Validate Build Config", () => { ], }; - expect(() => validateBuildConfig(config)).toThrow(); + expect(() => validateBuildConfig(config)).toThrow(/supported SDK ids|aliases/); }); test("Should reject misspelt canonical 'aslan' (no version)", () => { @@ -229,7 +229,7 @@ describe("Validate Build Config", () => { ], }; - expect(() => validateBuildConfig(config)).toThrow(); + expect(() => validateBuildConfig(config)).toThrow(/supported SDK ids|aliases/); }); describe("Deployment Config Validation", () => { From 1852cc420eb38515edebc1d8fe2fa7cdfa40f902 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Thu, 7 May 2026 23:39:07 +0200 Subject: [PATCH 11/21] Resolve SDK aliases in build command --- bin/cli/src/commands/build-command.test.ts | 21 +++++++++++++++++++++ bin/cli/src/commands/build-command.ts | 14 ++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/bin/cli/src/commands/build-command.test.ts b/bin/cli/src/commands/build-command.test.ts index b5786d7..947b864 100644 --- a/bin/cli/src/commands/build-command.test.ts +++ b/bin/cli/src/commands/build-command.test.ts @@ -78,6 +78,27 @@ describe("build-command", () => { expect(dockerCommand).toContain(`${resolve("/test/project", "./jade")}:/app`); }); + test("should generate correct Docker command for as-lan alias", async () => { + const service: ServiceConfig = { + name: "as-lan-service", + path: "./services/example", + sdk: "as-lan", + }; + + await callDockerBuild(service, "/test/project"); + + expect(mockSpawn).toHaveBeenCalledTimes(1); + const spawnCall = mockSpawn.mock.calls[0]; + if (!spawnCall) { + throw new Error("spawnCall is undefined"); + } + const dockerCommand = spawnCall[0][2] as string; + + expect(dockerCommand).toContain(SDK_CONFIGS["aslan-0.0.4"].image); + expect(dockerCommand).toContain(SDK_CONFIGS["aslan-0.0.4"].build); + expect(dockerCommand).toContain(`${resolve("/test/project", "./services/example")}:/app`); + }); + test("should generate correct Docker command for custom SDK config", async () => { const customSdk: SdkConfig = { image: "custom-image:latest", diff --git a/bin/cli/src/commands/build-command.ts b/bin/cli/src/commands/build-command.ts index ae4342b..c990319 100644 --- a/bin/cli/src/commands/build-command.ts +++ b/bin/cli/src/commands/build-command.ts @@ -1,13 +1,14 @@ import { mkdir } from "node:fs/promises"; import { join, relative, resolve } from "node:path"; import * as p from "@clack/prompts"; -import type { ServiceConfig } from "@fluffylabs/jammin-sdk"; +import type { ServiceConfig, SdkConfig } from "@fluffylabs/jammin-sdk"; import { copyJamToDist, generateTestConfigInProjectDir, getJamFiles, getServiceConfigs, loadServices, + resolveSdkId, SDK_CONFIGS, } from "@fluffylabs/jammin-sdk"; import { Command } from "commander"; @@ -25,7 +26,16 @@ export class DockerError extends Error { } export async function callDockerBuild(service: ServiceConfig, projectRoot: string): Promise { - const sdk = typeof service.sdk === "string" ? SDK_CONFIGS[service.sdk] : service.sdk; + let sdk: SdkConfig; + if (typeof service.sdk === "string") { + const canonicalId = resolveSdkId(service.sdk); + if (!canonicalId) { + throw new Error(`Unknown SDK id: '${service.sdk}'`); + } + sdk = SDK_CONFIGS[canonicalId]; + } else { + sdk = service.sdk; + } const servicePath = resolve(projectRoot, service.path); const dockerArgs = ["run", "--rm", "-v", `${servicePath}:/app`, sdk.image, ...sdk.build.split(" ")]; From fdac82d13af19f61a212e217e72fc514e5aff9e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Thu, 7 May 2026 23:42:14 +0200 Subject: [PATCH 12/21] Add negative test for unknown SDK id throw Co-Authored-By: Claude Sonnet 4.6 --- bin/cli/src/commands/build-command.test.ts | 11 +++++++++++ bin/cli/src/commands/build-command.ts | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/bin/cli/src/commands/build-command.test.ts b/bin/cli/src/commands/build-command.test.ts index 947b864..1434ad5 100644 --- a/bin/cli/src/commands/build-command.test.ts +++ b/bin/cli/src/commands/build-command.test.ts @@ -157,6 +157,17 @@ describe("build-command", () => { expect(callDockerBuild(service, "/test/project")).rejects.toThrow("Build failed for service 'failing-service'"); }); + test("should throw with descriptive message when SDK id is unknown", async () => { + const service: ServiceConfig = { + name: "broken-service", + path: "./broken", + // biome-ignore lint/suspicious/noExplicitAny: simulating a stale config that bypassed validation + sdk: "definitely-not-a-real-sdk" as any, + }; + + expect(callDockerBuild(service, "/test/project")).rejects.toThrow("Unknown SDK id: 'definitely-not-a-real-sdk'"); + }); + test("should return build output on success", async () => { const expectedOutput = "build successful output"; const mockSuccessSpawn = mock(() => { diff --git a/bin/cli/src/commands/build-command.ts b/bin/cli/src/commands/build-command.ts index c990319..0a03cdf 100644 --- a/bin/cli/src/commands/build-command.ts +++ b/bin/cli/src/commands/build-command.ts @@ -1,7 +1,7 @@ import { mkdir } from "node:fs/promises"; import { join, relative, resolve } from "node:path"; import * as p from "@clack/prompts"; -import type { ServiceConfig, SdkConfig } from "@fluffylabs/jammin-sdk"; +import type { SdkConfig, ServiceConfig } from "@fluffylabs/jammin-sdk"; import { copyJamToDist, generateTestConfigInProjectDir, From 3b3e932e81d66666c1a4c1ff4d9cfa77a58ef87a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Thu, 7 May 2026 23:43:30 +0200 Subject: [PATCH 13/21] Resolve SDK aliases in test command --- bin/cli/src/commands/test-command.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/bin/cli/src/commands/test-command.ts b/bin/cli/src/commands/test-command.ts index dec1355..c528cbe 100644 --- a/bin/cli/src/commands/test-command.ts +++ b/bin/cli/src/commands/test-command.ts @@ -1,8 +1,8 @@ import { mkdir } from "node:fs/promises"; import { join, relative, resolve } from "node:path"; import * as p from "@clack/prompts"; -import type { ServiceConfig } from "@fluffylabs/jammin-sdk"; -import { getServiceConfigs, SDK_CONFIGS } from "@fluffylabs/jammin-sdk"; +import type { SdkConfig, ServiceConfig } from "@fluffylabs/jammin-sdk"; +import { getServiceConfigs, resolveSdkId, SDK_CONFIGS } from "@fluffylabs/jammin-sdk"; import { Command } from "commander"; export class DockerError extends Error { @@ -18,7 +18,16 @@ export class DockerError extends Error { * Test a single service using Docker */ export async function testService(service: ServiceConfig, projectRoot: string): Promise { - const sdk = typeof service.sdk === "string" ? SDK_CONFIGS[service.sdk] : service.sdk; + let sdk: SdkConfig; + if (typeof service.sdk === "string") { + const canonicalId = resolveSdkId(service.sdk); + if (!canonicalId) { + throw new Error(`Unknown SDK id: '${service.sdk}'`); + } + sdk = SDK_CONFIGS[canonicalId]; + } else { + sdk = service.sdk; + } const servicePath = resolve(projectRoot, service.path); const dockerArgs = ["run", "--rm", "-v", `${servicePath}:/app`, sdk.image, ...sdk.test.split(" ")]; From 45658523f7dc2ebdabc15cf9334850405bf221bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Thu, 7 May 2026 23:47:43 +0200 Subject: [PATCH 14/21] Extract resolveSdk helper to deduplicate SDK lookup Co-Authored-By: Claude Sonnet 4.6 --- bin/cli/src/commands/build-command.ts | 16 +++---------- bin/cli/src/commands/test-command.ts | 15 +++--------- .../jammin-sdk/config/sdk-configs.test.ts | 24 ++++++++++++++++++- packages/jammin-sdk/config/sdk-configs.ts | 16 +++++++++++++ 4 files changed, 45 insertions(+), 26 deletions(-) diff --git a/bin/cli/src/commands/build-command.ts b/bin/cli/src/commands/build-command.ts index 0a03cdf..19b69b0 100644 --- a/bin/cli/src/commands/build-command.ts +++ b/bin/cli/src/commands/build-command.ts @@ -1,15 +1,14 @@ import { mkdir } from "node:fs/promises"; import { join, relative, resolve } from "node:path"; import * as p from "@clack/prompts"; -import type { SdkConfig, ServiceConfig } from "@fluffylabs/jammin-sdk"; +import type { ServiceConfig } from "@fluffylabs/jammin-sdk"; import { copyJamToDist, generateTestConfigInProjectDir, getJamFiles, getServiceConfigs, loadServices, - resolveSdkId, - SDK_CONFIGS, + resolveSdk, } from "@fluffylabs/jammin-sdk"; import { Command } from "commander"; @@ -26,16 +25,7 @@ export class DockerError extends Error { } export async function callDockerBuild(service: ServiceConfig, projectRoot: string): Promise { - let sdk: SdkConfig; - if (typeof service.sdk === "string") { - const canonicalId = resolveSdkId(service.sdk); - if (!canonicalId) { - throw new Error(`Unknown SDK id: '${service.sdk}'`); - } - sdk = SDK_CONFIGS[canonicalId]; - } else { - sdk = service.sdk; - } + const sdk = resolveSdk(service.sdk); const servicePath = resolve(projectRoot, service.path); const dockerArgs = ["run", "--rm", "-v", `${servicePath}:/app`, sdk.image, ...sdk.build.split(" ")]; diff --git a/bin/cli/src/commands/test-command.ts b/bin/cli/src/commands/test-command.ts index c528cbe..d4851d9 100644 --- a/bin/cli/src/commands/test-command.ts +++ b/bin/cli/src/commands/test-command.ts @@ -1,8 +1,8 @@ import { mkdir } from "node:fs/promises"; import { join, relative, resolve } from "node:path"; import * as p from "@clack/prompts"; -import type { SdkConfig, ServiceConfig } from "@fluffylabs/jammin-sdk"; -import { getServiceConfigs, resolveSdkId, SDK_CONFIGS } from "@fluffylabs/jammin-sdk"; +import type { ServiceConfig } from "@fluffylabs/jammin-sdk"; +import { getServiceConfigs, resolveSdk } from "@fluffylabs/jammin-sdk"; import { Command } from "commander"; export class DockerError extends Error { @@ -18,16 +18,7 @@ export class DockerError extends Error { * Test a single service using Docker */ export async function testService(service: ServiceConfig, projectRoot: string): Promise { - let sdk: SdkConfig; - if (typeof service.sdk === "string") { - const canonicalId = resolveSdkId(service.sdk); - if (!canonicalId) { - throw new Error(`Unknown SDK id: '${service.sdk}'`); - } - sdk = SDK_CONFIGS[canonicalId]; - } else { - sdk = service.sdk; - } + const sdk = resolveSdk(service.sdk); const servicePath = resolve(projectRoot, service.path); const dockerArgs = ["run", "--rm", "-v", `${servicePath}:/app`, sdk.image, ...sdk.test.split(" ")]; diff --git a/packages/jammin-sdk/config/sdk-configs.test.ts b/packages/jammin-sdk/config/sdk-configs.test.ts index 7c97be1..dbe205a 100644 --- a/packages/jammin-sdk/config/sdk-configs.test.ts +++ b/packages/jammin-sdk/config/sdk-configs.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test"; -import { resolveSdkId, SDK_ALIASES, SDK_CONFIGS } from "./sdk-configs.js"; +import { resolveSdk, resolveSdkId, SDK_ALIASES, SDK_CONFIGS } from "./sdk-configs.js"; import type { ServiceConfig } from "./types/config.js"; describe("SDK_ALIASES", () => { @@ -49,6 +49,28 @@ describe("resolveSdkId", () => { }); }); +describe("resolveSdk", () => { + test("Returns SDK_CONFIGS entry for a canonical key", () => { + const result = resolveSdk("aslan-0.0.4"); + expect(result).toBe(SDK_CONFIGS["aslan-0.0.4"]); + }); + + test("Returns SDK_CONFIGS entry for an alias key", () => { + const result = resolveSdk("as-lan"); + expect(result).toBe(SDK_CONFIGS["aslan-0.0.4"]); + }); + + test("Returns the inline SdkConfig object unchanged", () => { + const inline = { image: "custom:1", build: "make", test: "make test" }; + const result = resolveSdk(inline); + expect(result).toBe(inline); + }); + + test("Throws with a descriptive message for unknown string ids", () => { + expect(() => resolveSdk("nonsense")).toThrow("Unknown SDK id: 'nonsense'"); + }); +}); + describe("ServiceConfig.sdk type accepts alias strings", () => { type Assert = T; diff --git a/packages/jammin-sdk/config/sdk-configs.ts b/packages/jammin-sdk/config/sdk-configs.ts index f936838..a28c061 100644 --- a/packages/jammin-sdk/config/sdk-configs.ts +++ b/packages/jammin-sdk/config/sdk-configs.ts @@ -52,3 +52,19 @@ export function resolveSdkId(id: string): keyof typeof SDK_CONFIGS | undefined { } return undefined; } + +/** + * Resolve a service's `sdk` field to a concrete SdkConfig. Accepts canonical + * keys, alias keys, or an inline SdkConfig object. Throws for unknown string + * identifiers. + */ +export function resolveSdk(sdk: string | SdkConfig): SdkConfig { + if (typeof sdk !== "string") { + return sdk; + } + const canonicalId = resolveSdkId(sdk); + if (!canonicalId) { + throw new Error(`Unknown SDK id: '${sdk}'`); + } + return SDK_CONFIGS[canonicalId]; +} From 94f872d5fe317fa73bc563ee2affd7a5c1eb5db4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Thu, 7 May 2026 23:49:58 +0200 Subject: [PATCH 15/21] Register aslan template in create-command --- bin/cli/src/commands/create-command.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bin/cli/src/commands/create-command.ts b/bin/cli/src/commands/create-command.ts index 7685394..619c9a9 100644 --- a/bin/cli/src/commands/create-command.ts +++ b/bin/cli/src/commands/create-command.ts @@ -2,7 +2,7 @@ import * as p from "@clack/prompts"; import { fetchRepo, updatePackageJson } from "@fluffylabs/jammin-sdk"; import { Command, InvalidArgumentError } from "commander"; -type Template = "jam-sdk" | "jade" | "jambrains" | "ajanta" | "jamc3" | "undecided"; +type Template = "jam-sdk" | "jade" | "jambrains" | "ajanta" | "jamc3" | "aslan" | "undecided"; const TARGETS: Record = { "jam-sdk": "jammin-create/jammin-create-jam-sdk", @@ -10,6 +10,7 @@ const TARGETS: Record = { jambrains: "jammin-create/jammin-create-jambrains", ajanta: "jammin-create/jammin-create-ajanta", jamc3: "jammin-create/jammin-create-jamc3", + aslan: "jammin-create/jammin-create-aslan", undecided: "jammin-create/jammin-create-undecided", }; From 9ed06bc767987218804633b9b3daa8459a77d3a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Thu, 7 May 2026 23:51:08 +0200 Subject: [PATCH 16/21] Document as-lan SDK in service examples --- docs/src/service-examples.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/docs/src/service-examples.md b/docs/src/service-examples.md index fcbf3eb..15e9cf4 100644 --- a/docs/src/service-examples.md +++ b/docs/src/service-examples.md @@ -80,3 +80,34 @@ To run unit tests: ```console $ docker run --rm -v $(pwd):/app jade test ``` + +### as-lan + +The as-lan docker image ships with Node.js, `wasm-pvm`, and the AssemblyScript toolchain pre-installed. Pull it: + +```console +$ docker pull ghcr.io/tomusdrw/jammin-as-lan:0.0.4 +``` + +Then `cd` into the example code directory and build: + +```console +$ cd jammin-create-aslan/services/example +$ docker run --rm -v $(pwd):/app ghcr.io/tomusdrw/jammin-as-lan:0.0.4 npm run build +``` + +The image's entrypoint symlinks the global toolchain into `/app/node_modules` if no `node_modules` already exists in the mounted directory. + +#### Unit tests + +```console +$ docker run --rm -v $(pwd):/app ghcr.io/tomusdrw/jammin-as-lan:0.0.4 npm test +``` + +#### SDK names accepted in jammin.build.yml + +Any of the following resolve to the same image and commands: + +- `aslan-0.0.4` (canonical key in `SDK_CONFIGS`) +- `as-lan-0.0.4` (versioned alias matching the framework's spelling) +- `as-lan` (bare alias — follows the current default version) From 260b97e03aa1fbfe51ee217301f4839c2795a5f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Fri, 8 May 2026 09:48:19 +0200 Subject: [PATCH 17/21] Lock in validator/resolver consistency invariant + doc nuance --- docs/src/service-examples.md | 2 +- packages/jammin-sdk/config/sdk-configs.test.ts | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/src/service-examples.md b/docs/src/service-examples.md index 15e9cf4..0f164be 100644 --- a/docs/src/service-examples.md +++ b/docs/src/service-examples.md @@ -110,4 +110,4 @@ Any of the following resolve to the same image and commands: - `aslan-0.0.4` (canonical key in `SDK_CONFIGS`) - `as-lan-0.0.4` (versioned alias matching the framework's spelling) -- `as-lan` (bare alias — follows the current default version) +- `as-lan` (bare alias — follows the current default version; pin a versioned alias for reproducibility) diff --git a/packages/jammin-sdk/config/sdk-configs.test.ts b/packages/jammin-sdk/config/sdk-configs.test.ts index dbe205a..40536df 100644 --- a/packages/jammin-sdk/config/sdk-configs.test.ts +++ b/packages/jammin-sdk/config/sdk-configs.test.ts @@ -47,6 +47,13 @@ describe("resolveSdkId", () => { test("Returns undefined for empty string", () => { expect(resolveSdkId("")).toBeUndefined(); }); + + test("Every accepted SDK identifier (canonical or alias) is resolvable", () => { + const allAcceptedIds = [...Object.keys(SDK_CONFIGS), ...Object.keys(SDK_ALIASES)]; + for (const id of allAcceptedIds) { + expect(resolveSdkId(id)).toBeDefined(); + } + }); }); describe("resolveSdk", () => { From 56959c3244ca69d63e08d813c25479643386155f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Fri, 8 May 2026 09:51:45 +0200 Subject: [PATCH 18/21] List ajanta, jamc3, and aslan templates in getting-started The interactive create-command flow already offers these three templates, but the docs only listed jam-sdk, jade, and jambrains. Bring the docs in sync with the actual template registry. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/src/getting-started.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/src/getting-started.md b/docs/src/getting-started.md index 10ddf9e..fde0c26 100644 --- a/docs/src/getting-started.md +++ b/docs/src/getting-started.md @@ -45,6 +45,9 @@ The interactive wizard will ask you: - `jam-sdk` - JAM SDK template for building JAM services - `jade` - JADE SDK template - `jambrains` - JamBrains SDK template + - `ajanta` - Ajanta (Python) SDK template + - `jamc3` - JAMC3 (C3) SDK template + - `aslan` - as-lan (AssemblyScript) SDK template - `undecided` - Starter template for exploring options with all of the above ### Command-line mode From 3a770b50379d084cb901bead180223c4840a7e46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Fri, 8 May 2026 09:55:01 +0200 Subject: [PATCH 19/21] Add test-command unit tests covering docker invocation Co-Authored-By: Claude Sonnet 4.6 --- bin/cli/src/commands/test-command.test.ts | 261 ++++++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100644 bin/cli/src/commands/test-command.test.ts diff --git a/bin/cli/src/commands/test-command.test.ts b/bin/cli/src/commands/test-command.test.ts new file mode 100644 index 0000000..537675c --- /dev/null +++ b/bin/cli/src/commands/test-command.test.ts @@ -0,0 +1,261 @@ +import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test"; +import { resolve } from "node:path"; +import { SDK_CONFIGS, type SdkConfig, type ServiceConfig } from "@fluffylabs/jammin-sdk"; +import { testService } from "./test-command"; + +describe("test-command", () => { + describe("testService - Docker command generation", () => { + let originalSpawn: typeof Bun.spawn; + let mockSpawn: ReturnType; + + beforeEach(() => { + originalSpawn = Bun.spawn; + mockSpawn = mock(() => { + return { + stdout: new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode("test output")); + controller.close(); + }, + }), + stderr: new ReadableStream({ + start(controller) { + controller.close(); + }, + }), + exited: Promise.resolve(0), + }; + }); + // biome-ignore lint/suspicious/noExplicitAny: Need to mock Bun.spawn for testing + (Bun as any).spawn = mockSpawn; + }); + + afterEach(() => { + Bun.spawn = originalSpawn; + }); + + test("should generate correct Docker command for predefined SDK (jambrains)", async () => { + const service: ServiceConfig = { + name: "test-service", + path: "./services/test", + sdk: "jambrains-1cfc41c", + }; + + await testService(service, "/test/project"); + + expect(mockSpawn).toHaveBeenCalledTimes(1); + const spawnCall = mockSpawn.mock.calls[0]; + if (!spawnCall) { + throw new Error("spawnCall is undefined"); + } + expect(spawnCall[0]).toEqual(["sh", "-c", expect.stringContaining("docker")]); + + const dockerCommand = spawnCall[0][2] as string; + expect(dockerCommand).toContain("docker run --rm -v"); + expect(dockerCommand).toContain(`${resolve("/test/project", "./services/test")}:/app`); + expect(dockerCommand).toContain(SDK_CONFIGS["jambrains-1cfc41c"].image); + expect(dockerCommand).toContain(SDK_CONFIGS["jambrains-1cfc41c"].test); + }); + + test("should generate correct Docker command for predefined SDK (jade)", async () => { + const service: ServiceConfig = { + name: "jade-service", + path: "./jade", + sdk: "jade-0.0.15-pre.1", + }; + + await testService(service, "/test/project"); + + expect(mockSpawn).toHaveBeenCalledTimes(1); + const spawnCall = mockSpawn.mock.calls[0]; + if (!spawnCall) { + throw new Error("spawnCall is undefined"); + } + const dockerCommand = spawnCall[0][2] as string; + + expect(dockerCommand).toContain(SDK_CONFIGS["jade-0.0.15-pre.1"].image); + expect(dockerCommand).toContain(SDK_CONFIGS["jade-0.0.15-pre.1"].test); + expect(dockerCommand).toContain(`${resolve("/test/project", "./jade")}:/app`); + }); + + test("should generate correct Docker command for as-lan alias", async () => { + const service: ServiceConfig = { + name: "as-lan-service", + path: "./services/example", + sdk: "as-lan", + }; + + await testService(service, "/test/project"); + + expect(mockSpawn).toHaveBeenCalledTimes(1); + const spawnCall = mockSpawn.mock.calls[0]; + if (!spawnCall) { + throw new Error("spawnCall is undefined"); + } + const dockerCommand = spawnCall[0][2] as string; + + expect(dockerCommand).toContain(SDK_CONFIGS["aslan-0.0.4"].image); + expect(dockerCommand).toContain(SDK_CONFIGS["aslan-0.0.4"].test); + expect(dockerCommand).toContain(`${resolve("/test/project", "./services/example")}:/app`); + }); + + test("should generate correct Docker command for custom SDK config", async () => { + const customSdk: SdkConfig = { + image: "custom-image:latest", + build: "custom build command with args", + test: "custom test command", + }; + + const service: ServiceConfig = { + name: "custom-service", + path: "./custom", + sdk: customSdk, + }; + + await testService(service, "/test/project"); + + expect(mockSpawn).toHaveBeenCalledTimes(1); + const spawnCall = mockSpawn.mock.calls[0]; + if (!spawnCall) { + throw new Error("spawnCall is undefined"); + } + const dockerCommand = spawnCall[0][2] as string; + + expect(dockerCommand).toContain("custom-image:latest"); + expect(dockerCommand).toContain("custom test command"); + expect(dockerCommand).toContain(`${resolve("/test/project", "./custom")}:/app`); + }); + + test("should handle test failure with non-zero exit code", async () => { + const mockFailedSpawn = mock(() => { + return { + stdout: new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode("test error output")); + controller.close(); + }, + }), + stderr: new ReadableStream({ + start(controller) { + controller.close(); + }, + }), + exited: Promise.resolve(1), + }; + }); + + // biome-ignore lint/suspicious/noExplicitAny: Need to mock Bun.spawn for testing + (Bun as any).spawn = mockFailedSpawn; + + const service: ServiceConfig = { + name: "failing-service", + path: "./fail", + sdk: "jambrains-1cfc41c", + }; + + expect(testService(service, "/test/project")).rejects.toThrow(); + expect(testService(service, "/test/project")).rejects.toThrow("Tests failed for service 'failing-service'"); + }); + + test("should throw with descriptive message when SDK id is unknown", async () => { + const service: ServiceConfig = { + name: "broken-service", + path: "./broken", + // biome-ignore lint/suspicious/noExplicitAny: simulating a stale config that bypassed validation + sdk: "definitely-not-a-real-sdk" as any, + }; + + expect(testService(service, "/test/project")).rejects.toThrow("Unknown SDK id: 'definitely-not-a-real-sdk'"); + }); + + test("should return test output on success", async () => { + const expectedOutput = "test successful output"; + const mockSuccessSpawn = mock(() => { + return { + stdout: new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode(expectedOutput)); + controller.close(); + }, + }), + stderr: new ReadableStream({ + start(controller) { + controller.close(); + }, + }), + exited: Promise.resolve(0), + }; + }); + + // biome-ignore lint/suspicious/noExplicitAny: Need to mock Bun.spawn for testing + (Bun as any).spawn = mockSuccessSpawn; + + const service: ServiceConfig = { + name: "success-service", + path: "./success", + sdk: "jambrains-1cfc41c", + }; + + const output = await testService(service, "/test/project"); + expect(output).toBe(expectedOutput); + }); + }); + + describe("testService - service path resolution", () => { + let originalSpawn: typeof Bun.spawn; + + beforeEach(() => { + originalSpawn = Bun.spawn; + }); + + afterEach(() => { + Bun.spawn = originalSpawn; + }); + + test("should resolve relative service paths correctly", async () => { + const mockSpawn = mock(() => { + return { + stdout: new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode("test output")); + controller.close(); + }, + }), + stderr: new ReadableStream({ + start(controller) { + controller.close(); + }, + }), + exited: Promise.resolve(0), + }; + }); + + // biome-ignore lint/suspicious/noExplicitAny: Need to mock Bun.spawn for testing + (Bun as any).spawn = mockSpawn; + + const service: ServiceConfig = { + name: "test-service", + path: "./services/test", + sdk: "jambrains-1cfc41c", + }; + + const projectRoot = "/absolute/project/root"; + await testService(service, projectRoot); + + expect(mockSpawn).toHaveBeenCalledTimes(1); + expect(mockSpawn.mock.calls.length).toBeGreaterThan(0); + const spawnCall = mockSpawn.mock.calls[0]; + if (!spawnCall || spawnCall.length === 0) { + throw new Error("spawnCall is undefined or empty"); + } + const spawnArgs = (spawnCall as unknown[])[0] as string[]; + if (!spawnArgs || spawnArgs.length < 3) { + throw new Error("spawnArgs is invalid"); + } + const dockerCommand = spawnArgs[2] as string; + const expectedPath = resolve(projectRoot, service.path); + + expect(dockerCommand).toContain(`${expectedPath}:/app`); + }); + }); +}); From 1b0187a490082edbc30dd427bd73268aa86a58b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Fri, 8 May 2026 09:55:24 +0200 Subject: [PATCH 20/21] Document SDK rebuild step for local test runs Tests in bin/cli/ import @fluffylabs/jammin-sdk via package name, which resolves to packages/jammin-sdk/dist/. After SDK source edits, bun run build must run before bun test locally. CI already chains build before test so this only matters during local development. Co-Authored-By: Claude Opus 4.7 (1M context) --- AGENTS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 0bf4e13..935f8bb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -62,6 +62,8 @@ bun test bin/cli/src/commands/create-command.test.ts # Run specific test file bun test --watch # Run tests in watch mode ``` +> **Heads-up for SDK changes.** The `@fluffylabs/jammin-sdk` package is published from `packages/jammin-sdk/dist/`, and CLI tests import it via the package name. After editing files under `packages/jammin-sdk/`, run `bun run build` before `bun test` so test imports see the new symbols. CI runs `bun run build` before `bun test` automatically; the manual step is only needed locally. + ## Code Style & Conventions ### Linting & Formatting (Biome) From 0b06504018ab5d689501fe05c838b7ab00d73764 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Fri, 8 May 2026 13:27:15 +0200 Subject: [PATCH 21/21] Address PR review: untrack internal plan docs, await rejects assertions - Remove docs/superpowers/ planning artifacts from source control and add the directory to .gitignore. The mdBook user docs under docs/src/ stay tracked. - Await `.rejects.toThrow(...)` calls in build-command.test.ts and test-command.test.ts so async rejections are actually asserted (per CodeRabbit feedback). Combine the duplicate failing-exit-code calls into a single awaited assertion checking the specific error message. Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 3 + bin/cli/src/commands/build-command.test.ts | 9 +- bin/cli/src/commands/test-command.test.ts | 7 +- .../plans/2026-04-28-aslan-framework.md | 767 ------------------ .../2026-04-28-aslan-framework-design.md | 193 ----- 5 files changed, 13 insertions(+), 966 deletions(-) delete mode 100644 docs/superpowers/plans/2026-04-28-aslan-framework.md delete mode 100644 docs/superpowers/specs/2026-04-28-aslan-framework-design.md diff --git a/.gitignore b/.gitignore index 268d771..9e80ac8 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,6 @@ bun.lockb # e2e test temporary directory .test + +# Internal planning artifacts (specs/plans for in-progress work) +docs/superpowers/ diff --git a/bin/cli/src/commands/build-command.test.ts b/bin/cli/src/commands/build-command.test.ts index 1434ad5..7c7a2a3 100644 --- a/bin/cli/src/commands/build-command.test.ts +++ b/bin/cli/src/commands/build-command.test.ts @@ -153,8 +153,9 @@ describe("build-command", () => { sdk: "jambrains-1cfc41c", }; - expect(callDockerBuild(service, "/test/project")).rejects.toThrow(); - expect(callDockerBuild(service, "/test/project")).rejects.toThrow("Build failed for service 'failing-service'"); + await expect(callDockerBuild(service, "/test/project")).rejects.toThrow( + "Build failed for service 'failing-service'", + ); }); test("should throw with descriptive message when SDK id is unknown", async () => { @@ -165,7 +166,9 @@ describe("build-command", () => { sdk: "definitely-not-a-real-sdk" as any, }; - expect(callDockerBuild(service, "/test/project")).rejects.toThrow("Unknown SDK id: 'definitely-not-a-real-sdk'"); + await expect(callDockerBuild(service, "/test/project")).rejects.toThrow( + "Unknown SDK id: 'definitely-not-a-real-sdk'", + ); }); test("should return build output on success", async () => { diff --git a/bin/cli/src/commands/test-command.test.ts b/bin/cli/src/commands/test-command.test.ts index 537675c..8d97626 100644 --- a/bin/cli/src/commands/test-command.test.ts +++ b/bin/cli/src/commands/test-command.test.ts @@ -153,8 +153,7 @@ describe("test-command", () => { sdk: "jambrains-1cfc41c", }; - expect(testService(service, "/test/project")).rejects.toThrow(); - expect(testService(service, "/test/project")).rejects.toThrow("Tests failed for service 'failing-service'"); + await expect(testService(service, "/test/project")).rejects.toThrow("Tests failed for service 'failing-service'"); }); test("should throw with descriptive message when SDK id is unknown", async () => { @@ -165,7 +164,9 @@ describe("test-command", () => { sdk: "definitely-not-a-real-sdk" as any, }; - expect(testService(service, "/test/project")).rejects.toThrow("Unknown SDK id: 'definitely-not-a-real-sdk'"); + await expect(testService(service, "/test/project")).rejects.toThrow( + "Unknown SDK id: 'definitely-not-a-real-sdk'", + ); }); test("should return test output on success", async () => { diff --git a/docs/superpowers/plans/2026-04-28-aslan-framework.md b/docs/superpowers/plans/2026-04-28-aslan-framework.md deleted file mode 100644 index 72f2361..0000000 --- a/docs/superpowers/plans/2026-04-28-aslan-framework.md +++ /dev/null @@ -1,767 +0,0 @@ -# as-lan Framework Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Register as-lan as a built-in SDK in jammin so users can write `sdk: as-lan` (or `sdk: as-lan-0.0.4` / `sdk: aslan-0.0.4`) in `jammin.build.yml` instead of inlining the docker image and build/test commands. - -**Architecture:** Add a single canonical entry `aslan-0.0.4` to the existing `SDK_CONFIGS` map. Add a separate `SDK_ALIASES` map and a `resolveSdkId` helper next to it. Build-command and test-command call the helper before looking up `SDK_CONFIGS`. The Zod validator accepts both canonical keys and alias keys. A new `aslan` entry is added to the create-command template registry. - -**Tech Stack:** TypeScript, Bun (runtime + test runner), Zod (validation), Biome (lint/format), Commander (CLI). - -**Spec:** `docs/superpowers/specs/2026-04-28-aslan-framework-design.md` - ---- - -## File Structure - -| File | Status | Responsibility | -|---|---|---| -| `packages/jammin-sdk/config/sdk-configs.ts` | modify | New `aslan-0.0.4` entry, new `SDK_ALIASES` map, new `resolveSdkId` helper | -| `packages/jammin-sdk/config/sdk-configs.test.ts` | create | `resolveSdkId` unit tests | -| `packages/jammin-sdk/config/types/config.ts` | modify | Widen `ServiceConfig.sdk` to include alias keys | -| `packages/jammin-sdk/config/config-validator.ts` | modify | Accept alias keys in Zod schema; refresh error message | -| `packages/jammin-sdk/config/config-validator.test.ts` | modify | Add tests for alias acceptance / rejection | -| `bin/cli/src/commands/build-command.ts` | modify | Resolve string SDK ids via `resolveSdkId` before `SDK_CONFIGS` lookup | -| `bin/cli/src/commands/build-command.test.ts` | modify | Add a docker-command test using the `as-lan` alias | -| `bin/cli/src/commands/test-command.ts` | modify | Same resolver swap as build-command | -| `bin/cli/src/commands/create-command.ts` | modify | Register `aslan` template | -| `docs/src/service-examples.md` | modify | New "as-lan" docker section | - ---- - -## Task 1: Register `aslan-0.0.4` in `SDK_CONFIGS` - -**Files:** -- Modify: `packages/jammin-sdk/config/sdk-configs.ts` - -The validator's Zod enum is built from `Object.keys(SDK_CONFIGS)`, so adding the entry alone makes `sdk: aslan-0.0.4` accepted by the validator. We assert that with a focused validator test before changing any code. - -- [ ] **Step 1: Add a failing validator test for the canonical `aslan-0.0.4` key** - -Append to `packages/jammin-sdk/config/config-validator.test.ts`, inside the existing `describe("Validate Build Config", ...)` block (after the "Should reject inline custom SDK with empty strings" test, before the `Deployment Config Validation` describe block): - -```typescript -test("Should accept canonical aslan-0.0.4 SDK", () => { - const config = { - services: [ - { - path: "./services/example", - name: "example", - sdk: "aslan-0.0.4", - }, - ], - }; - - const result = validateBuildConfig(config); - expect(result.services[0]?.sdk).toBe("aslan-0.0.4"); -}); -``` - -- [ ] **Step 2: Run the new test, expect it to fail** - -```bash -bun test packages/jammin-sdk/config/config-validator.test.ts -t "Should accept canonical aslan-0.0.4 SDK" -``` - -Expected: FAIL — Zod rejects `aslan-0.0.4` because it's not in the enum yet. - -- [ ] **Step 3: Add the `aslan-0.0.4` entry to `SDK_CONFIGS`** - -In `packages/jammin-sdk/config/sdk-configs.ts`, append a new entry after the `jamc3-1.1.2` entry (before the closing `}`): - -```typescript -import type { SdkConfig } from "./types/config.js"; - -export const SDK_CONFIGS = { - "jam-sdk-0.1.26": { - image: "ghcr.io/fluffylabs/jammin-jam-sdk:0.1.26", - build: "jam-pvm-build -m service", - test: "cargo test", - }, - "jambrains-1cfc41c": { - image: - "ghcr.io/jambrains/service-sdk:latest@sha256:1cfc41c23f5c348aaee5f5c70aaa24f10c26baf903de4b4f6774e2032820ba87", - build: "single-file main.c", - test: "true", - }, - "jade-0.0.15-pre.1": { - image: "ghcr.io/fluffylabs/jammin-jade:0.0.15-pre.1", - build: "build", - test: "test", - }, - "ajanta-0.1.0": { - image: "ghcr.io/fluffylabs/jammin-ajanta:0.1.0", - build: "ajanta build main.py -o service.jam", - test: "true", - }, - "jamc3-1.1.2": { - image: "ghcr.io/dreverr/jamc3:1.1.2", - build: "main.c3 -o service.jam", - test: "bun test", - }, - "aslan-0.0.4": { - image: "ghcr.io/tomusdrw/jammin-as-lan:0.0.4", - build: "npm run build", - test: "npm test", - }, -} as const satisfies Record; -``` - -- [ ] **Step 4: Run the validator tests, expect all to pass** - -```bash -bun test packages/jammin-sdk/config/config-validator.test.ts -``` - -Expected: PASS for the new test plus all previously-passing tests. - -- [ ] **Step 5: Commit** - -```bash -git add packages/jammin-sdk/config/sdk-configs.ts packages/jammin-sdk/config/config-validator.test.ts -git commit -m "Add aslan-0.0.4 entry to SDK_CONFIGS" -``` - ---- - -## Task 2: Add `SDK_ALIASES` map and `resolveSdkId` helper - -**Files:** -- Modify: `packages/jammin-sdk/config/sdk-configs.ts` -- Create: `packages/jammin-sdk/config/sdk-configs.test.ts` - -The helper resolves any accepted string SDK identifier (canonical key or alias) to a canonical `SDK_CONFIGS` key, returning `undefined` for unknown strings. It does NOT handle the `SdkConfig` object form — that's a structurally-different value handled by the caller. - -- [ ] **Step 1: Write failing tests for `SDK_ALIASES` shape and `resolveSdkId` behaviour** - -Create `packages/jammin-sdk/config/sdk-configs.test.ts`: - -```typescript -import { describe, expect, test } from "bun:test"; -import { resolveSdkId, SDK_ALIASES, SDK_CONFIGS } from "./sdk-configs.js"; - -describe("SDK_ALIASES", () => { - test("Every alias target points to a canonical SDK_CONFIGS key", () => { - for (const target of Object.values(SDK_ALIASES)) { - expect(SDK_CONFIGS).toHaveProperty(target); - } - }); - - test("Bare 'as-lan' alias resolves to aslan-0.0.4", () => { - expect(SDK_ALIASES["as-lan"]).toBe("aslan-0.0.4"); - }); - - test("Versioned 'as-lan-0.0.4' alias resolves to aslan-0.0.4", () => { - expect(SDK_ALIASES["as-lan-0.0.4"]).toBe("aslan-0.0.4"); - }); -}); - -describe("resolveSdkId", () => { - test("Returns the input when it is already a canonical SDK_CONFIGS key", () => { - expect(resolveSdkId("aslan-0.0.4")).toBe("aslan-0.0.4"); - expect(resolveSdkId("jam-sdk-0.1.26")).toBe("jam-sdk-0.1.26"); - }); - - test("Resolves the bare 'as-lan' alias to aslan-0.0.4", () => { - expect(resolveSdkId("as-lan")).toBe("aslan-0.0.4"); - }); - - test("Resolves the versioned 'as-lan-0.0.4' alias to aslan-0.0.4", () => { - expect(resolveSdkId("as-lan-0.0.4")).toBe("aslan-0.0.4"); - }); - - test("Returns undefined for unknown identifiers", () => { - expect(resolveSdkId("nonsense")).toBeUndefined(); - expect(resolveSdkId("as-lan-0.0.3")).toBeUndefined(); - expect(resolveSdkId("aslan")).toBeUndefined(); - }); -}); -``` - -- [ ] **Step 2: Run the new tests, expect them to fail** - -```bash -bun test packages/jammin-sdk/config/sdk-configs.test.ts -``` - -Expected: FAIL — `SDK_ALIASES` and `resolveSdkId` aren't exported yet. - -- [ ] **Step 3: Add `SDK_ALIASES` and `resolveSdkId` to `sdk-configs.ts`** - -Append to `packages/jammin-sdk/config/sdk-configs.ts` after the `SDK_CONFIGS` declaration: - -```typescript -export const SDK_ALIASES = { - "as-lan": "aslan-0.0.4", - "as-lan-0.0.4": "aslan-0.0.4", -} as const satisfies Record; - -/** - * Resolve a string SDK identifier (canonical key or alias) to a canonical - * SDK_CONFIGS key. Returns undefined for unknown identifiers. - */ -export function resolveSdkId(id: string): keyof typeof SDK_CONFIGS | undefined { - if (id in SDK_CONFIGS) { - return id as keyof typeof SDK_CONFIGS; - } - if (id in SDK_ALIASES) { - return SDK_ALIASES[id as keyof typeof SDK_ALIASES]; - } - return undefined; -} -``` - -- [ ] **Step 4: Run the new tests, expect them to pass** - -```bash -bun test packages/jammin-sdk/config/sdk-configs.test.ts -``` - -Expected: PASS for all four `resolveSdkId` tests and the three `SDK_ALIASES` tests. - -- [ ] **Step 5: Commit** - -```bash -git add packages/jammin-sdk/config/sdk-configs.ts packages/jammin-sdk/config/sdk-configs.test.ts -git commit -m "Add SDK_ALIASES map and resolveSdkId helper" -``` - ---- - -## Task 3: Widen `ServiceConfig.sdk` type to include alias keys - -**Files:** -- Modify: `packages/jammin-sdk/config/types/config.ts` - -This is a type-only change. It lets TypeScript users construct `ServiceConfig` objects with alias strings (e.g. in tests, or in callers building config objects in code). Runtime behaviour is unchanged. - -- [ ] **Step 1: Add a failing type-level assertion test** - -Two edits to `packages/jammin-sdk/config/sdk-configs.test.ts`: - -(a) Add this import line at the top of the file, alongside the existing imports: - -```typescript -import type { ServiceConfig } from "./types/config.js"; -``` - -(b) Append a new `describe` block after the existing `describe("resolveSdkId", ...)` block: - -```typescript -describe("ServiceConfig.sdk type accepts alias strings", () => { - test("Compiles when sdk is an alias key", () => { - const cfg: ServiceConfig = { - path: "./services/example", - name: "example", - sdk: "as-lan", - }; - expect(cfg.sdk).toBe("as-lan"); - }); - - test("Compiles when sdk is a versioned alias key", () => { - const cfg: ServiceConfig = { - path: "./services/example", - name: "example", - sdk: "as-lan-0.0.4", - }; - expect(cfg.sdk).toBe("as-lan-0.0.4"); - }); -}); -``` - -- [ ] **Step 2: Run the new test file, expect type errors** - -```bash -bun test packages/jammin-sdk/config/sdk-configs.test.ts -``` - -Expected: FAIL with TypeScript errors like `Type '"as-lan"' is not assignable to type ...`. - -- [ ] **Step 3: Widen `ServiceConfig.sdk`** - -Edit `packages/jammin-sdk/config/types/config.ts`. Change the import line to include `SDK_ALIASES`: - -```typescript -import type { SDK_ALIASES, SDK_CONFIGS } from "../sdk-configs.js"; -``` - -And change the `sdk` field on the `ServiceConfig` interface from: - -```typescript -sdk: keyof typeof SDK_CONFIGS | SdkConfig; -``` - -to: - -```typescript -sdk: keyof typeof SDK_CONFIGS | keyof typeof SDK_ALIASES | SdkConfig; -``` - -- [ ] **Step 4: Run the file's tests, expect pass** - -```bash -bun test packages/jammin-sdk/config/sdk-configs.test.ts -``` - -Expected: PASS for all tests, no type errors. - -- [ ] **Step 5: Run the project-wide type check + lint to catch any consumer that broke** - -```bash -bun run qa -``` - -Expected: PASS. (If a consumer was narrowing the old type, fix locally — none expected based on the spec, but check.) - -- [ ] **Step 6: Commit** - -```bash -git add packages/jammin-sdk/config/types/config.ts packages/jammin-sdk/config/sdk-configs.test.ts -git commit -m "Widen ServiceConfig.sdk type to accept alias keys" -``` - ---- - -## Task 4: Validator accepts alias keys - -**Files:** -- Modify: `packages/jammin-sdk/config/config-validator.ts` -- Modify: `packages/jammin-sdk/config/config-validator.test.ts` - -The Zod schema needs to accept `as-lan` and `as-lan-0.0.4` in addition to the canonical keys. The error message lists both sets so users can debug typos. - -- [ ] **Step 1: Add failing alias-acceptance tests** - -Append to `packages/jammin-sdk/config/config-validator.test.ts`, inside the existing `describe("Validate Build Config", ...)` block, near the `aslan-0.0.4` test added in Task 1: - -```typescript -test("Should accept bare 'as-lan' alias as SDK", () => { - const config = { - services: [ - { - path: "./services/example", - name: "example", - sdk: "as-lan", - }, - ], - }; - - const result = validateBuildConfig(config); - expect(result.services[0]?.sdk).toBe("as-lan"); -}); - -test("Should accept versioned 'as-lan-0.0.4' alias as SDK", () => { - const config = { - services: [ - { - path: "./services/example", - name: "example", - sdk: "as-lan-0.0.4", - }, - ], - }; - - const result = validateBuildConfig(config); - expect(result.services[0]?.sdk).toBe("as-lan-0.0.4"); -}); - -test("Should reject unknown 'as-lan-0.0.3' alias", () => { - const config = { - services: [ - { - path: "./services/example", - name: "example", - sdk: "as-lan-0.0.3", - }, - ], - }; - - expect(() => validateBuildConfig(config)).toThrow(); -}); - -test("Should reject misspelt canonical 'aslan' (no version)", () => { - const config = { - services: [ - { - path: "./services/example", - name: "example", - sdk: "aslan", - }, - ], - }; - - expect(() => validateBuildConfig(config)).toThrow(); -}); -``` - -- [ ] **Step 2: Run the new tests, expect the alias-acceptance ones to fail** - -```bash -bun test packages/jammin-sdk/config/config-validator.test.ts -t "alias" -``` - -Expected: FAIL on the two acceptance tests (validator currently rejects alias strings); PASS on the two rejection tests (validator already rejects unknown strings). - -- [ ] **Step 3: Update the validator to accept aliases** - -Edit `packages/jammin-sdk/config/config-validator.ts`. Change the import line at the top: - -```typescript -import { SDK_ALIASES, SDK_CONFIGS } from "./sdk-configs.js"; -``` - -Replace the `ServiceConfigSchema` `sdk` field's union (currently around lines 32-35): - -```typescript -sdk: z.union( - [z.enum(Object.keys(SDK_CONFIGS) as (keyof typeof SDK_CONFIGS)[]), SdkConfigSchema], - `Expected a valid custom SDK configuration or one of the supported SDK ids (${Object.keys(SDK_CONFIGS).join(", ")})`, -), -``` - -with: - -```typescript -sdk: z.union( - [ - z.enum([ - ...(Object.keys(SDK_CONFIGS) as (keyof typeof SDK_CONFIGS)[]), - ...(Object.keys(SDK_ALIASES) as (keyof typeof SDK_ALIASES)[]), - ]), - SdkConfigSchema, - ], - `Expected a valid custom SDK configuration or one of the supported SDK ids (${Object.keys(SDK_CONFIGS).join(", ")}) or aliases (${Object.keys(SDK_ALIASES).join(", ")})`, -), -``` - -- [ ] **Step 4: Run the validator tests, expect all to pass** - -```bash -bun test packages/jammin-sdk/config/config-validator.test.ts -``` - -Expected: PASS for all tests, including the four new ones from this task. - -- [ ] **Step 5: Commit** - -```bash -git add packages/jammin-sdk/config/config-validator.ts packages/jammin-sdk/config/config-validator.test.ts -git commit -m "Accept SDK aliases in build config validator" -``` - ---- - -## Task 5: Build command resolves alias before docker invocation - -**Files:** -- Modify: `bin/cli/src/commands/build-command.ts` -- Modify: `bin/cli/src/commands/build-command.test.ts` - -`callDockerBuild` currently does `SDK_CONFIGS[service.sdk]` directly when `service.sdk` is a string. With aliases, that lookup returns `undefined` for `as-lan` because the alias key isn't in `SDK_CONFIGS`. We route string SDK ids through `resolveSdkId` first. - -- [ ] **Step 1: Add a failing test for the alias path through `callDockerBuild`** - -Append to `bin/cli/src/commands/build-command.test.ts`, inside the `describe("buildService - Docker command generation", ...)` block (after the existing "should generate correct Docker command for predefined SDK (jade)" test): - -```typescript -test("should generate correct Docker command for as-lan alias", async () => { - const service: ServiceConfig = { - name: "as-lan-service", - path: "./services/example", - sdk: "as-lan", - }; - - await callDockerBuild(service, "/test/project"); - - expect(mockSpawn).toHaveBeenCalledTimes(1); - const spawnCall = mockSpawn.mock.calls[0]; - if (!spawnCall) { - throw new Error("spawnCall is undefined"); - } - const dockerCommand = spawnCall[0][2] as string; - - expect(dockerCommand).toContain(SDK_CONFIGS["aslan-0.0.4"].image); - expect(dockerCommand).toContain(SDK_CONFIGS["aslan-0.0.4"].build); - expect(dockerCommand).toContain(`${resolve("/test/project", "./services/example")}:/app`); -}); -``` - -- [ ] **Step 2: Run the new test, expect failure** - -```bash -bun test bin/cli/src/commands/build-command.test.ts -t "as-lan alias" -``` - -Expected: FAIL — `SDK_CONFIGS["as-lan"]` is `undefined`, so the docker command won't include the expected image/build strings (likely throws on `sdk.build.split` because `sdk` is `undefined`). - -- [ ] **Step 3: Use `resolveSdkId` in `callDockerBuild`** - -Edit `bin/cli/src/commands/build-command.ts`. Update the import block (around lines 4-13) to add `resolveSdkId` and `type SdkConfig`: - -```typescript -import type { ServiceConfig, SdkConfig } from "@fluffylabs/jammin-sdk"; -import { - copyJamToDist, - generateTestConfigInProjectDir, - getJamFiles, - getServiceConfigs, - loadServices, - resolveSdkId, - SDK_CONFIGS, -} from "@fluffylabs/jammin-sdk"; -``` - -Replace line 28 in `callDockerBuild`: - -```typescript -const sdk = typeof service.sdk === "string" ? SDK_CONFIGS[service.sdk] : service.sdk; -``` - -with: - -```typescript -let sdk: SdkConfig; -if (typeof service.sdk === "string") { - const canonicalId = resolveSdkId(service.sdk); - if (!canonicalId) { - throw new Error(`Unknown SDK id: '${service.sdk}'`); - } - sdk = SDK_CONFIGS[canonicalId]; -} else { - sdk = service.sdk; -} -``` - -- [ ] **Step 4: Run the build-command tests, expect all to pass** - -```bash -bun test bin/cli/src/commands/build-command.test.ts -``` - -Expected: PASS for the new alias test plus all previously-passing tests. - -- [ ] **Step 5: Commit** - -```bash -git add bin/cli/src/commands/build-command.ts bin/cli/src/commands/build-command.test.ts -git commit -m "Resolve SDK aliases in build command" -``` - ---- - -## Task 6: Test command resolves alias before docker invocation - -**Files:** -- Modify: `bin/cli/src/commands/test-command.ts` - -Same change as Task 5, applied to `testService`. No new test file is added; Task 2 already covers `resolveSdkId` in isolation, and Task 5's test exercises the same helper through a docker invocation. - -- [ ] **Step 1: Use `resolveSdkId` in `testService`** - -Edit `bin/cli/src/commands/test-command.ts`. Update the import block (lines 4-5): - -```typescript -import type { ServiceConfig, SdkConfig } from "@fluffylabs/jammin-sdk"; -import { getServiceConfigs, resolveSdkId, SDK_CONFIGS } from "@fluffylabs/jammin-sdk"; -``` - -Replace line 21 in `testService`: - -```typescript -const sdk = typeof service.sdk === "string" ? SDK_CONFIGS[service.sdk] : service.sdk; -``` - -with: - -```typescript -let sdk: SdkConfig; -if (typeof service.sdk === "string") { - const canonicalId = resolveSdkId(service.sdk); - if (!canonicalId) { - throw new Error(`Unknown SDK id: '${service.sdk}'`); - } - sdk = SDK_CONFIGS[canonicalId]; -} else { - sdk = service.sdk; -} -``` - -- [ ] **Step 2: Run the full CLI test suite to confirm nothing regressed** - -```bash -bun test bin/cli/ -``` - -Expected: PASS for all tests. - -- [ ] **Step 3: Commit** - -```bash -git add bin/cli/src/commands/test-command.ts -git commit -m "Resolve SDK aliases in test command" -``` - ---- - -## Task 7: Register `aslan` template in create-command - -**Files:** -- Modify: `bin/cli/src/commands/create-command.ts` - -Pattern is identical to commit `b995067` (C3 SDK addition). - -- [ ] **Step 1: Add `aslan` to the `Template` union and `TARGETS` map** - -Edit `bin/cli/src/commands/create-command.ts`. Replace lines 5-14: - -```typescript -type Template = "jam-sdk" | "jade" | "jambrains" | "ajanta" | "jamc3" | "undecided"; - -const TARGETS: Record = { - "jam-sdk": "jammin-create/jammin-create-jam-sdk", - jade: "jammin-create/jammin-create-jade", - jambrains: "jammin-create/jammin-create-jambrains", - ajanta: "jammin-create/jammin-create-ajanta", - jamc3: "jammin-create/jammin-create-jamc3", - undecided: "jammin-create/jammin-create-undecided", -}; -``` - -with: - -```typescript -type Template = "jam-sdk" | "jade" | "jambrains" | "ajanta" | "jamc3" | "aslan" | "undecided"; - -const TARGETS: Record = { - "jam-sdk": "jammin-create/jammin-create-jam-sdk", - jade: "jammin-create/jammin-create-jade", - jambrains: "jammin-create/jammin-create-jambrains", - ajanta: "jammin-create/jammin-create-ajanta", - jamc3: "jammin-create/jammin-create-jamc3", - aslan: "jammin-create/jammin-create-aslan", - undecided: "jammin-create/jammin-create-undecided", -}; -``` - -- [ ] **Step 2: Run the create-command tests, expect pass** - -```bash -bun test bin/cli/src/commands/create-command.test.ts -``` - -Expected: PASS for all existing tests (the change is additive; existing tests don't enumerate templates). - -- [ ] **Step 3: Smoke-test the help output to confirm `aslan` is offered** - -```bash -bun run cli create --help -``` - -Expected: Output lists `aslan` as one of the `--template` choices. - -- [ ] **Step 4: Commit** - -```bash -git add bin/cli/src/commands/create-command.ts -git commit -m "Register aslan template in create-command" -``` - ---- - -## Task 8: Document the as-lan SDK in service-examples.md - -**Files:** -- Modify: `docs/src/service-examples.md` - -The existing page has sections for JAM SDK, JamBrains, and Jade. Add an "as-lan" section that mirrors that style. - -- [ ] **Step 1: Add the as-lan section** - -Append the following literal markdown to `docs/src/service-examples.md` after the existing Jade section. Mirror the existing section format (heading hierarchy `###` for SDK name, `####` for "Unit tests"). Use the Write/Edit tool — these are real fenced code blocks, not nested-string escapes. - -Section content (semantic outline; render to markdown using the same shape as the existing JAM SDK / JamBrains / Jade sections in this same file): - -1. `### as-lan` heading. -2. Short paragraph: "The as-lan docker image ships with Node.js, `wasm-pvm`, and the AssemblyScript toolchain pre-installed. Pull it:" -3. Console-fenced block: `$ docker pull ghcr.io/tomusdrw/jammin-as-lan:0.0.4` -4. Short paragraph: "Then `cd` into the example code directory and build:" -5. Console-fenced block, two lines: - - `$ cd jammin-create-aslan/services/example` - - `$ docker run --rm -v $(pwd):/app ghcr.io/tomusdrw/jammin-as-lan:0.0.4 npm run build` -6. Short paragraph: "The image's entrypoint symlinks the global toolchain into `/app/node_modules` if no `node_modules` already exists in the mounted directory." -7. `#### Unit tests` subheading. -8. Console-fenced block: `$ docker run --rm -v $(pwd):/app ghcr.io/tomusdrw/jammin-as-lan:0.0.4 npm test` -9. `#### SDK names accepted in jammin.build.yml` subheading. -10. Short paragraph: "Any of the following resolve to the same image and commands:" -11. Bulleted list (three items): - - `` `aslan-0.0.4` `` (canonical key in `SDK_CONFIGS`) - - `` `as-lan-0.0.4` `` (versioned alias matching the framework's spelling) - - `` `as-lan` `` (bare alias — follows the current default version) - -- [ ] **Step 2: Verify the markdown renders cleanly** - -```bash -ls docs/src/service-examples.md && head -120 docs/src/service-examples.md -``` - -Expected: file ends with the new as-lan section, fenced blocks are well-formed. - -- [ ] **Step 3: Commit** - -```bash -git add docs/src/service-examples.md -git commit -m "Document as-lan SDK in service examples" -``` - ---- - -## Task 9: Final QA + smoke verification - -**Files:** none modified - -- [ ] **Step 1: Run the project's full QA gate** - -```bash -bun run qa -``` - -Expected: PASS — Biome formatting/lint is clean, type checks pass. - -- [ ] **Step 2: Run the full test suite** - -```bash -bun test -``` - -Expected: PASS for all tests, including the new ones added in Tasks 1, 2, 3, 4, 5. - -- [ ] **Step 3: Smoke test that an `as-lan` config validates end-to-end** - -```bash -bun -e 'import("./packages/jammin-sdk/config/config-validator.js").then(m => { console.log(JSON.stringify(m.validateBuildConfig({ services: [{ path: "./svc", name: "svc", sdk: "as-lan" }] }), null, 2)); })' -``` - -Expected: prints a JSON object with `services[0].sdk === "as-lan"`, no thrown error. - -- [ ] **Step 4: Confirm git log is clean** - -```bash -git log --oneline origin/main..HEAD -``` - -Expected: 8 commits, one per implementation task plus the two existing spec commits already on the branch. - ---- - -## Self-review checklist (run before opening PR) - -- [ ] Spec section "Canonical SDK key" → covered by Task 1 -- [ ] Spec section "Aliases" → covered by Task 2 -- [ ] Spec section "Resolver helper" → covered by Task 2 -- [ ] Spec section "Type widening" → covered by Task 3 -- [ ] Spec section "Validation" → covered by Task 4 -- [ ] Spec section "Build/test command consumption" → covered by Tasks 5 and 6 -- [ ] Spec section "Create-command template" → covered by Task 7 -- [ ] Spec section "Tests" → all listed test cases present in Tasks 1, 2, 4, 5 -- [ ] Spec section "Docs" → covered by Task 8 -- [ ] Validation matrix in spec → exercised by tests in Tasks 1, 2, 4 diff --git a/docs/superpowers/specs/2026-04-28-aslan-framework-design.md b/docs/superpowers/specs/2026-04-28-aslan-framework-design.md deleted file mode 100644 index 9bb2ac0..0000000 --- a/docs/superpowers/specs/2026-04-28-aslan-framework-design.md +++ /dev/null @@ -1,193 +0,0 @@ -# Add as-lan as a built-in SDK - -## Goal - -Make as-lan a first-class supported framework so a service author writes -`sdk: as-lan` (or `sdk: as-lan-0.0.4`, or `sdk: aslan-0.0.4`) in -`jammin.build.yml` and jammin resolves the docker image plus build/test -commands automatically — no inline image/build/test fields required. - -`jammin create my-app --template aslan` scaffolds from -`jammin-create/jammin-create-aslan`. - -## Non-goals - -- Generalised alias resolution (regex/prefix transforms across the SDK - family). The alias map is explicit. -- Versioned aliases for as-lan releases that haven't been pinned yet - (e.g. `as-lan-0.0.3` — only versions present in `SDK_CONFIGS` get an - alias entry). -- Migrating the image to `ghcr.io/fluffylabs/jammin-as-lan` (the - namespace used by other built-in SDKs). The README documents that - target, but only `ghcr.io/tomusdrw/jammin-as-lan` is published today. - This spec pins to `tomusdrw` to match what works; a future change can - flip the namespace once the image is mirrored. - -## Image behaviour - -`ghcr.io/tomusdrw/jammin-as-lan:0.0.4` is published and ships with -Node.js, `wasm-pvm`, and the as-lan/ecalli/AssemblyScript packages -pre-installed globally. Its entrypoint symlinks the global install into -`/app/node_modules` when none exists in the mounted volume, so -`npm run build` and `npm test` work against the user's source without a -per-build `npm install`. - -## Design - -### Canonical SDK key - -`aslan-0.0.4`. Keeps the existing kebab-case-plus-version convention -shared with `jamc3-1.1.2`, `ajanta-0.1.0`, `jam-sdk-0.1.26`. Underlying -image: `ghcr.io/tomusdrw/jammin-as-lan:0.0.4`. Build command: -`npm run build`. Test command: `npm test`. - -### Aliases - -A new exported map next to `SDK_CONFIGS`: - -```ts -export const SDK_ALIASES = { - "as-lan": "aslan-0.0.4", - "as-lan-0.0.4": "aslan-0.0.4", -} as const satisfies Record; -``` - -- `as-lan` (bare) — points to the current default as-lan version. - Convenient shorthand; will silently follow version bumps when the - default changes. -- `as-lan-0.0.4` — versioned re-spelling (matches the framework's - canonical name with the dash). Pinned, will not move. - -When a future `aslan-0.0.5` is added, `SDK_ALIASES` gains a -`"as-lan-0.0.5": "aslan-0.0.5"` entry and the bare `"as-lan"` alias -target is rebumped to `aslan-0.0.5`. - -### Resolver helper - -```ts -export function resolveSdkId(id: string): keyof typeof SDK_CONFIGS | undefined { - if (id in SDK_CONFIGS) { - return id as keyof typeof SDK_CONFIGS; - } - if (id in SDK_ALIASES) { - return SDK_ALIASES[id as keyof typeof SDK_ALIASES]; - } - return undefined; -} -``` - -Single source of truth for "string SDK identifier → canonical -`SDK_CONFIGS` key". Lives in `packages/jammin-sdk/config/sdk-configs.ts` -alongside the data it operates on. - -### Type widening - -`ServiceConfig.sdk` becomes: - -```ts -sdk: keyof typeof SDK_CONFIGS | keyof typeof SDK_ALIASES | SdkConfig; -``` - -So the YAML config type is honest about which strings are accepted. - -### Validation - -`config-validator.ts` Zod schema for `sdk` field accepts canonical keys -**and** alias keys. Resolution happens at consumption time, not -validation time — the parsed config keeps whatever string the user -wrote (useful for error messages and round-tripping). Validator error -message lists both sets: - -> Expected a valid custom SDK configuration or one of the supported SDK -> ids (jam-sdk-0.1.26, jambrains-1cfc41c, jade-0.0.15-pre.1, -> ajanta-0.1.0, jamc3-1.1.2, aslan-0.0.4) or aliases (as-lan, -> as-lan-0.0.4) - -### Build/test command consumption - -`bin/cli/src/commands/build-command.ts` line 28 currently: - -```ts -const sdk = typeof service.sdk === "string" ? SDK_CONFIGS[service.sdk] : service.sdk; -``` - -Becomes: - -```ts -let sdk: SdkConfig; -if (typeof service.sdk === "string") { - const canonicalId = resolveSdkId(service.sdk); - if (!canonicalId) { - throw new Error(`Unknown SDK id: ${service.sdk}`); - } - sdk = SDK_CONFIGS[canonicalId]; -} else { - sdk = service.sdk; -} -``` - -Same change in `test-command.ts` line 21. The thrown error is a -defensive belt — the validator should already have rejected unknown -ids, but a stale config could in principle bypass it. - -### Create-command template - -`bin/cli/src/commands/create-command.ts`: - -- Add `aslan` to the `Template` type union -- Add `aslan: "jammin-create/jammin-create-aslan"` to `TARGETS` - -Pattern is identical to commit `b995067` (C3 SDK). - -## Files touched - -- `packages/jammin-sdk/config/sdk-configs.ts` — new entry, alias map, resolver helper -- `packages/jammin-sdk/config/types/config.ts` — widen `ServiceConfig.sdk` -- `packages/jammin-sdk/config/config-validator.ts` — accept alias keys, update error message -- `bin/cli/src/commands/build-command.ts` — use `resolveSdkId` before `SDK_CONFIGS` lookup -- `bin/cli/src/commands/test-command.ts` — same as above -- `bin/cli/src/commands/create-command.ts` — register `aslan` template - -## Tests - -Additions only, no new files except where noted. - -`packages/jammin-sdk/config/config-validator.test.ts`: -- Accepts canonical `aslan-0.0.4` -- Accepts alias `as-lan` -- Accepts alias `as-lan-0.0.4` -- Rejects unknown alias `as-lan-0.0.3` -- Rejects misspelt canonical `aslan` (no version) - -`packages/jammin-sdk/config/sdk-configs.test.ts` (new file, small): -- `resolveSdkId("aslan-0.0.4")` returns `"aslan-0.0.4"` -- `resolveSdkId("as-lan")` returns `"aslan-0.0.4"` -- `resolveSdkId("as-lan-0.0.4")` returns `"aslan-0.0.4"` -- `resolveSdkId("nonsense")` returns `undefined` - -`bin/cli/src/commands/build-command.test.ts`: -- Existing `jambrains-1cfc41c` / `jade-0.0.15-pre.1` cases stay -- Add a case asserting that `service.sdk === "as-lan"` produces a - docker invocation containing the `aslan-0.0.4` image and build command - -## Docs - -`docs/src/service-examples.md`: -- New "as-lan" section under "Using Docker images" mirroring the JAM - SDK / JamBrains / Jade entries (image, build invocation, brief test - invocation) -- Brief mention of accepted SDK names: canonical, dashed-versioned, and - bare alias - -`docs/src/SUMMARY.md` — no change (editing existing page only). - -## Validation matrix - -| Input | Validator | Resolves to | -|------------------------|-----------|----------------| -| `aslan-0.0.4` | accept | `aslan-0.0.4` | -| `as-lan` | accept | `aslan-0.0.4` | -| `as-lan-0.0.4` | accept | `aslan-0.0.4` | -| `as-lan-0.0.3` | reject | n/a | -| `aslan` (no version) | reject | n/a | -| custom `SdkConfig` obj | accept | the inline obj |