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/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) diff --git a/bin/cli/src/commands/build-command.test.ts b/bin/cli/src/commands/build-command.test.ts index b5786d7..7c7a2a3 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", @@ -132,8 +153,22 @@ 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 () => { + 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, + }; + + 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/build-command.ts b/bin/cli/src/commands/build-command.ts index ae4342b..19b69b0 100644 --- a/bin/cli/src/commands/build-command.ts +++ b/bin/cli/src/commands/build-command.ts @@ -8,7 +8,7 @@ import { getJamFiles, getServiceConfigs, loadServices, - SDK_CONFIGS, + resolveSdk, } from "@fluffylabs/jammin-sdk"; import { Command } from "commander"; @@ -25,7 +25,7 @@ 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; + 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/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", }; 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..8d97626 --- /dev/null +++ b/bin/cli/src/commands/test-command.test.ts @@ -0,0 +1,262 @@ +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", + }; + + 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 () => { + 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, + }; + + await 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`); + }); + }); +}); diff --git a/bin/cli/src/commands/test-command.ts b/bin/cli/src/commands/test-command.ts index dec1355..d4851d9 100644 --- a/bin/cli/src/commands/test-command.ts +++ b/bin/cli/src/commands/test-command.ts @@ -2,7 +2,7 @@ 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 { getServiceConfigs, resolveSdk } from "@fluffylabs/jammin-sdk"; import { Command } from "commander"; export class DockerError extends Error { @@ -18,7 +18,7 @@ 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; + 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/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 diff --git a/docs/src/service-examples.md b/docs/src/service-examples.md index fcbf3eb..0f164be 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; pin a versioned alias for reproducibility) diff --git a/packages/jammin-sdk/config/config-validator.test.ts b/packages/jammin-sdk/config/config-validator.test.ts index 0274c54..ffc842f 100644 --- a/packages/jammin-sdk/config/config-validator.test.ts +++ b/packages/jammin-sdk/config/config-validator.test.ts @@ -159,6 +159,79 @@ 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"); + }); + + 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(/supported SDK ids|aliases/); + }); + + test("Should reject misspelt canonical 'aslan' (no version)", () => { + const config = { + services: [ + { + path: "./services/example", + name: "example", + sdk: "aslan", + }, + ], + }; + + expect(() => validateBuildConfig(config)).toThrow(/supported SDK ids|aliases/); + }); + 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(", ")})`, ), }); 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..40536df --- /dev/null +++ b/packages/jammin-sdk/config/sdk-configs.test.ts @@ -0,0 +1,97 @@ +import { describe, expect, test } from "bun:test"; +import { resolveSdk, 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", () => { + 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(); + }); + + 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(); + }); + + 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", () => { + 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; + + // 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: "./svc", + name: "svc", + sdk: "as-lan", + }; + expect(cfg.sdk).toBe("as-lan"); + }); +}); diff --git a/packages/jammin-sdk/config/sdk-configs.ts b/packages/jammin-sdk/config/sdk-configs.ts index 2dcf238..a28c061 100644 --- a/packages/jammin-sdk/config/sdk-configs.ts +++ b/packages/jammin-sdk/config/sdk-configs.ts @@ -27,4 +27,44 @@ 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; + +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 (Object.hasOwn(SDK_CONFIGS, id)) { + return id as keyof typeof SDK_CONFIGS; + } + if (Object.hasOwn(SDK_ALIASES, id)) { + return SDK_ALIASES[id as keyof typeof SDK_ALIASES]; + } + 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]; +} 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 {