(workflowPath: string | undefined, args: string[])
| 256 | // defaults. Used on the non-interactive path (no prompts). |
| 257 | export function configFromPartial(p: Partial<DeployConfig>, outputDirDefault: string): DeployConfig { |
| 258 | return { |
| 259 | llmKeys: p.llmKeys ?? {}, |
| 260 | outputDir: p.outputDir ?? outputDirDefault, |
| 261 | force: p.force ?? false, |
| 262 | logLevel: p.logLevel ?? "info", |
| 263 | hardware: p.hardware ?? {}, |
| 264 | mqtt: p.mqtt ?? {}, |
| 265 | llmModels: p.llmModels ?? {}, |
| 266 | mlModels: p.mlModels ?? {}, |
| 267 | cameras: p.cameras ?? {}, |
| 268 | webSearch: p.webSearch, |
| 269 | }; |
| 270 | } |
| 271 | |
| 272 | // Read and validate the --component folders once, up front, so a bad component.json |
| 273 | // fails before any prompting (in either mode). Identical folders are deduped |
| 274 | // (passing one twice is accidental); two distinct folders declaring the same name |
| 275 | // collide — surfaced here rather than late at spec assembly. Each component is |
| 276 | // paired with its folder for the later <name>.env step. |
| 277 | async function loadFlagComponents(dirs: string[]): Promise<LoadedComponent[]> { |
| 278 | const uniqueDirs = [...new Set(dirs.map((d) => path.resolve(process.cwd(), d)))]; |
| 279 | const entries = await Promise.all( |
| 280 | uniqueDirs.map(async (dir) => ({ source: path.join(dir, "component.json"), data: await readComponentJson(dir), dir })), |
| 281 | ); |
| 282 | parseDeployComponents(entries); // validates all at once; throws on any gap |
| 283 | const seen = new Set<string>(); |
| 284 | const loaded: LoadedComponent[] = []; |
| 285 | for (const e of entries) { |
| 286 | const component = e.data as DeployComponent; |
| 287 | if (seen.has(component.name)) { |
| 288 | throw new Error(`duplicate component name "${component.name}" (two --component folders declare it)`); |
| 289 | } |
| 290 | seen.add(component.name); |
| 291 | loaded.push({ component, dir: e.dir }); |
| 292 | } |
| 293 | return loaded; |
| 294 | } |
| 295 | |
| 296 | // Turn each component's <name>.env.example into <name>.env text. The non-interactive |
| 297 | // path's env step (stubs empty values); the interactive path resolves env inline as |
| 298 | // each component is added. |
| 299 | async function resolveComponentsEnv(loaded: LoadedComponent[]): Promise<Record<string, string>> { |
| 300 | const env: Record<string, string> = {}; |
| 301 | for (const { component, dir } of loaded) { |
| 302 | const text = await resolveComponentEnv(dir, component.name, { interactive: false }); |
| 303 | if (text !== null) env[component.name] = text; |
| 304 | } |
| 305 | return env; |
| 306 | } |
| 307 | |
| 308 | // --------------------------------------------------------------------------- |
| 309 | // Entry point — wired into cli/index.ts |
| 310 | // --------------------------------------------------------------------------- |
| 311 | |
| 312 | export async function deployCommand(workflowPath: string | undefined, args: string[]): Promise<void> { |
| 313 | // --help or missing positional → print USAGE |
| 314 | if (!workflowPath || workflowPath === "--help" || workflowPath === "-h") { |
| 315 | process.stdout.write(USAGE); |
no test coverage detected