diff --git a/packages/cli-workflow/src/commands/init/template.ts b/packages/cli-workflow/src/commands/init/template.ts index 81ec9a6..d788343 100644 --- a/packages/cli-workflow/src/commands/init/template.ts +++ b/packages/cli-workflow/src/commands/init/template.ts @@ -13,19 +13,7 @@ import { templateTsconfigJson, } from "./templates.js"; import type { CmdInitTemplateSuccess } from "./types.js"; - -function validateWorkspaceSegment(name: string): Result { - if (name.length === 0) { - return err("workspace name must not be empty"); - } - if (name === "." || name === "..") { - return err("invalid workspace name"); - } - if (name.includes("/") || name.includes("\\")) { - return err("workspace name must not contain path separators"); - } - return ok(undefined); -} +import { validateWorkspaceSegment } from "./validate.js"; function hasTemplatesWorkspaceGlob(workspaces: unknown): boolean { return Array.isArray(workspaces) && workspaces.includes("templates/*"); diff --git a/packages/cli-workflow/src/commands/init/validate.ts b/packages/cli-workflow/src/commands/init/validate.ts new file mode 100644 index 0000000..e39ad19 --- /dev/null +++ b/packages/cli-workflow/src/commands/init/validate.ts @@ -0,0 +1,15 @@ +import { err, ok, type Result } from "@uncaged/workflow"; + +/** Validates a single path segment for workspace / template names (no separators, not `.` / `..`). */ +export function validateWorkspaceSegment(name: string): Result { + if (name.length === 0) { + return err("workspace name must not be empty"); + } + if (name === "." || name === "..") { + return err("invalid workspace name"); + } + if (name.includes("/") || name.includes("\\")) { + return err("workspace name must not contain path separators"); + } + return ok(undefined); +} diff --git a/packages/cli-workflow/src/commands/init/workspace.ts b/packages/cli-workflow/src/commands/init/workspace.ts index 8b6d0e1..26dba34 100644 --- a/packages/cli-workflow/src/commands/init/workspace.ts +++ b/packages/cli-workflow/src/commands/init/workspace.ts @@ -5,19 +5,7 @@ import { err, ok, type Result } from "@uncaged/workflow"; import { pathExists } from "../../fs-utils.js"; import type { CmdInitWorkspaceSuccess } from "./types.js"; - -function validateWorkspaceSegment(name: string): Result { - if (name.length === 0) { - return err("workspace name must not be empty"); - } - if (name === "." || name === "..") { - return err("invalid workspace name"); - } - if (name.includes("/") || name.includes("\\")) { - return err("workspace name must not contain path separators"); - } - return ok(undefined); -} +import { validateWorkspaceSegment } from "./validate.js"; function rootPackageJson(workspaceName: string): string { return `${JSON.stringify( diff --git a/packages/workflow/src/config/index.ts b/packages/workflow/src/config/index.ts index ecd62e1..029a409 100644 --- a/packages/workflow/src/config/index.ts +++ b/packages/workflow/src/config/index.ts @@ -1,2 +1,3 @@ export { resolveModel } from "./resolve-model.js"; +export { splitProviderModelRef } from "./split-provider-model-ref.js"; export type { ProviderConfig, ResolvedModel } from "./types.js"; diff --git a/packages/workflow/src/config/resolve-model.ts b/packages/workflow/src/config/resolve-model.ts index 0941352..e766b44 100644 --- a/packages/workflow/src/config/resolve-model.ts +++ b/packages/workflow/src/config/resolve-model.ts @@ -1,22 +1,8 @@ import type { WorkflowConfig } from "../registry/index.js"; import { err, ok, type Result } from "../util/index.js"; +import { splitProviderModelRef } from "./split-provider-model-ref.js"; import type { ResolvedModel } from "./types.js"; -function splitProviderModelRef( - ref: string, -): Result<{ providerName: string; modelName: string }, string> { - const idx = ref.indexOf("/"); - if (idx <= 0 || idx === ref.length - 1) { - return err(`invalid model reference "${ref}": expected providerName/modelName`); - } - const providerName = ref.slice(0, idx); - const modelName = ref.slice(idx + 1); - if (providerName === "" || modelName === "") { - return err(`invalid model reference "${ref}": expected providerName/modelName`); - } - return ok({ providerName, modelName }); -} - /** Resolves scene → provider endpoint + model using {@link WorkflowConfig.providers} and {@link WorkflowConfig.models}. */ export function resolveModel(config: WorkflowConfig, scene: string): Result { const models = config.models; diff --git a/packages/workflow/src/config/split-provider-model-ref.ts b/packages/workflow/src/config/split-provider-model-ref.ts new file mode 100644 index 0000000..0002920 --- /dev/null +++ b/packages/workflow/src/config/split-provider-model-ref.ts @@ -0,0 +1,17 @@ +import { err, ok, type Result } from "../util/index.js"; + +/** Parses `providerName/modelName` references used in {@link WorkflowConfig.models}. */ +export function splitProviderModelRef( + ref: string, +): Result<{ providerName: string; modelName: string }, string> { + const idx = ref.indexOf("/"); + if (idx <= 0 || idx === ref.length - 1) { + return err(`invalid model reference "${ref}": expected providerName/modelName`); + } + const providerName = ref.slice(0, idx); + const modelName = ref.slice(idx + 1); + if (providerName === "" || modelName === "") { + return err(`invalid model reference "${ref}": expected providerName/modelName`); + } + return ok({ providerName, modelName }); +} diff --git a/packages/workflow/src/registry/registry-normalize.ts b/packages/workflow/src/registry/registry-normalize.ts index 12b844a..3927941 100644 --- a/packages/workflow/src/registry/registry-normalize.ts +++ b/packages/workflow/src/registry/registry-normalize.ts @@ -1,5 +1,5 @@ -import type { ProviderConfig } from "../config/index.js"; -import { err, ok, type Result } from "../util/index.js"; +import { type ProviderConfig, splitProviderModelRef } from "../config/index.js"; +import { createLogger, err, ok, type Result } from "../util/index.js"; import type { WorkflowConfig, WorkflowHistoryEntry, @@ -7,6 +7,8 @@ import type { WorkflowRegistryFile, } from "./types.js"; +const registryNormalizeLog = createLogger({ sink: { kind: "stderr" } }); + function resolveRegistryApiKey(raw: string, ctx: string): Result { if (raw.startsWith("env:")) { const name = raw.slice("env:".length); @@ -22,22 +24,6 @@ function resolveRegistryApiKey(raw: string, ctx: string): Result return ok(raw); } -function parseModelProviderRef( - ref: string, - ctx: string, -): Result<{ providerName: string; modelName: string }, Error> { - const idx = ref.indexOf("/"); - if (idx <= 0 || idx === ref.length - 1) { - return err(new Error(`${ctx}: expected providerName/modelName, got "${ref}"`)); - } - const providerName = ref.slice(0, idx); - const modelName = ref.slice(idx + 1); - if (providerName === "" || modelName === "") { - return err(new Error(`${ctx}: expected providerName/modelName, got "${ref}"`)); - } - return ok({ providerName, modelName }); -} - function normalizeProviderEntry(name: string, entryRaw: unknown): Result { if (name === "") { return err(new Error("config.providers must not contain an empty provider name")); @@ -96,9 +82,9 @@ function normalizeModels( return err(new Error(`config.models.${scene} must be a non-empty string (provider/model)`)); } const ctx = `config.models.${scene}`; - const parsed = parseModelProviderRef(refRaw, ctx); + const parsed = splitProviderModelRef(refRaw); if (!parsed.ok) { - return parsed; + return err(new Error(`${ctx}: ${parsed.error}`)); } if (!providerKeys.has(parsed.value.providerName)) { return err( @@ -109,6 +95,12 @@ function normalizeModels( } models[scene] = refRaw; } + if (!Object.hasOwn(models, "default")) { + registryNormalizeLog( + "Z2KP9NWQ", + 'registry config: models mapping has no "default" key; scenes without explicit model mappings may fail at resolveModel', + ); + } return ok(models); }