Skip to content

Commit b21bfb1

Browse files
committed
feat(cli): wire ai-agents through bootstrap and diagnostics
1 parent 5c0a02c commit b21bfb1

16 files changed

Lines changed: 137 additions & 37 deletions

src/commands/register-bootstrap-command.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ export function registerBootstrapCommand(program: Command): void {
1212
.command('bootstrap')
1313
.description('Run the idempotent bootstrap only')
1414
.option('--project-dir <path>', 'Explicit lab asset root instead of the packaged install')
15+
.option('--with-ai-agents', 'Include the optional AI agents bootstrap (n8n owner reconciliation)')
1516
.option('--with-ai-llm, --with-ai', 'Include the optional AI LLM bootstrap (Ollama models)')
1617
.option('--skip-gitea', 'Skip the Gitea admin reconciliation step')
18+
.option('--skip-n8n', 'Skip the n8n owner reconciliation step')
1719
.option('--skip-ollama', 'Skip the Ollama model reconciliation step')
1820
.action(async (options: BootstrapCommandOptions) => {
1921
const normalizedOptions = normalizeAiAliasOptions(options);

src/commands/register-doctor-command.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export function registerDoctorCommand(program: Command): void {
1313
.description('Check host requirements and optionally run smoke tests')
1414
.option('--project-dir <path>', 'Explicit lab asset root instead of the packaged install')
1515
.option('--with-ai-llm, --with-ai', 'Include the optional AI LLM layer checks')
16+
.option('--with-ai-agents', 'Include the optional AI agents layer checks')
1617
.option('--with-ai-image, --with-image', 'Include the optional AI image layer checks')
1718
.option('--with-ai-video', 'Include the optional AI video layer checks')
1819
.option('--with-workbench', 'Validate the optional workbench Compose layer')

src/commands/register-save-images-command.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export function registerSaveImagesCommand(program: Command): void {
1414
.option('--project-dir <path>', 'Explicit lab asset root instead of the packaged install')
1515
.option('--output <path>', 'Output archive path (defaults under ./backups/images)')
1616
.option('--with-ai-llm, --with-ai', 'Include the optional AI LLM layer images')
17+
.option('--with-ai-agents', 'Include the optional AI agents layer images')
1718
.option('--with-ai-image, --with-image', 'Include the optional AI image layer images')
1819
.option('--with-ai-video', 'Include the optional AI video layer images')
1920
.option('--with-workbench', 'Include the optional workbench layer images')

src/commands/register-save-volumes-command.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export function registerSaveVolumesCommand(program: Command): void {
1414
.option('--project-dir <path>', 'Explicit lab asset root instead of the packaged install')
1515
.option('--output <path>', 'Output archive path (defaults under ./backups/volumes)')
1616
.option('--with-ai-llm, --with-ai', 'Include the optional AI LLM layer volumes')
17+
.option('--with-ai-agents', 'Include the optional AI agents layer volumes')
1718
.option('--with-ai-image, --with-image', 'Include the optional AI image layer volumes')
1819
.option('--with-ai-video', 'Include the optional AI video layer volumes')
1920
.option('--with-workbench', 'Include the optional workbench layer volumes')

src/commands/register-up-command.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export function registerUpCommand(program: Command): void {
1414
.option('--project-dir <path>', 'Explicit lab asset root instead of the packaged install')
1515
.option('--build', 'Rebuild images before starting the stack')
1616
.option('--with-ai-llm, --with-ai', 'Include the optional AI LLM layer (Open WebUI and Ollama)')
17+
.option('--with-ai-agents', 'Include the optional AI agents layer (n8n and external runners)')
1718
.option(
1819
'--with-ai-image, --with-image',
1920
'Include the optional AI image layer (InvokeAI and its paired runtime)'

src/config/lab-env.schema.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,13 @@ export const bootstrapEnvSchema = labEnvSchema.extend({
6161
GITEA_GID: requiredEnvValue,
6262
GITEA_ROOT_USERNAME: requiredEnvValue,
6363
GITEA_ROOT_PASSWORD: requiredEnvValue,
64-
GITEA_ROOT_EMAIL: requiredEnvValue,
64+
GITEA_ROOT_EMAIL: requiredEnvValue
65+
});
66+
67+
/**
68+
* Schema for workflows that require AI agents bootstrap env values.
69+
*/
70+
export const aiAgentsBootstrapEnvSchema = labEnvSchema.extend({
6571
N8N_URL: requiredEnvValue,
6672
N8N_ROOT_FIRST_NAME: requiredEnvValue,
6773
N8N_ROOT_LAST_NAME: requiredEnvValue,
@@ -74,7 +80,13 @@ export const bootstrapEnvSchema = labEnvSchema.extend({
7480
*/
7581
export const smokeEnvSchema = labEnvSchema.extend({
7682
LAB_URL: requiredEnvValue,
77-
GITEA_URL: requiredEnvValue,
83+
GITEA_URL: requiredEnvValue
84+
});
85+
86+
/**
87+
* Schema for AI agents smoke checks.
88+
*/
89+
export const aiAgentsSmokeEnvSchema = labEnvSchema.extend({
7890
N8N_URL: requiredEnvValue,
7991
N8N_ROOT_EMAIL: requiredEnvValue,
8092
N8N_ROOT_PASSWORD: requiredEnvValue

src/services/bootstrap.service.ts

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@ import { Listr } from 'listr2';
22
import pWaitFor from 'p-wait-for';
33
import { createComposeCommandArgs, type ComposeLayerSelection } from '../lib/compose.js';
44
import type { BootstrapCommandOptions } from '../types/cli.types.js';
5-
import type { BootstrapEnv, ProjectContext } from '../types/project.types.js';
5+
import type { ProjectContext } from '../types/project.types.js';
66
import { ensureGiteaAdmin } from './gitea-admin.service.js';
77
import { ensureN8nOwner } from './n8n-owner.service.js';
88
import { printCommandHeader } from '../ui/banner.js';
99
import { formatTaskTitle, printInfo, printSuccess } from '../ui/logger.js';
1010
import { runCommand } from '../utils/process.js';
11-
import { parseAiLlmBootstrapEnv, parseBootstrapEnv } from './project.service.js';
11+
import {
12+
parseAiAgentsBootstrapEnv,
13+
parseAiLlmBootstrapEnv,
14+
parseBootstrapEnv
15+
} from './project.service.js';
1216

1317
const VERBOSE_TASK_RENDERER = 'verbose' as const;
1418

@@ -21,7 +25,7 @@ export async function runBootstrapCommand(
2125
): Promise<void> {
2226
printCommandHeader({
2327
title: 'Bootstrap Atlas Lab',
24-
summary: 'Reconcile core runtime state and optional AI LLM models',
28+
summary: 'Reconcile core runtime state plus optional AI agents and AI LLM bootstrap',
2529
projectRoot: context.projectRoot,
2630
workingDirectory: context.workingDirectory
2731
});
@@ -42,6 +46,7 @@ export function createBootstrapTasks(
4246
options: BootstrapCommandOptions
4347
) {
4448
const env = parseBootstrapEnv(context.env);
49+
const aiAgentsEnv = options.withAiAgents ? parseAiAgentsBootstrapEnv(context.env) : undefined;
4550
const aiLlmEnv = options.withAiLlm ? parseAiLlmBootstrapEnv(context.env) : undefined;
4651

4752
const tasks = [];
@@ -57,15 +62,17 @@ export function createBootstrapTasks(
5762
});
5863
}
5964

60-
tasks.push({
61-
title: formatTaskTitle('bootstrap', 'Align n8n owner account'),
62-
task: async () => {
63-
await waitForService(context, 'n8n');
64-
await waitForService(context, 'gateway');
65-
const result = await ensureN8nOwner(context, env);
66-
printInfo(`n8n owner account ${result}.`, 'bootstrap');
67-
}
68-
});
65+
if (options.withAiAgents && !options.skipN8n && aiAgentsEnv) {
66+
tasks.push({
67+
title: formatTaskTitle('bootstrap', 'Align n8n owner account'),
68+
task: async () => {
69+
await waitForService(context, 'n8n', 180, { includeAiAgents: true });
70+
await waitForService(context, 'gateway-ai-agents', 180, { includeAiAgents: true });
71+
const result = await ensureN8nOwner(context, aiAgentsEnv);
72+
printInfo(`n8n owner account ${result}.`, 'bootstrap');
73+
}
74+
});
75+
}
6976

7077
if (options.withAiLlm && !options.skipOllama && aiLlmEnv) {
7178
tasks.push({

src/services/compose-project.service.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,13 @@ export async function getRunningComposePublishedPorts(
5454
*/
5555
export async function listConfiguredComposeImages(
5656
context: ProjectContext,
57-
options: Pick<SaveImagesCommandOptions, 'withAiLlm' | 'withAiImage' | 'withAiVideo' | 'withWorkbench'>
57+
options: Pick<SaveImagesCommandOptions, 'withAiLlm' | 'withAiAgents' | 'withAiImage' | 'withAiVideo' | 'withWorkbench'>
5858
): Promise<string[]> {
5959
const result = await runCommand(
6060
'docker',
6161
createComposeCommandArgs(context, ['config', '--images'], {
6262
includeAiLlm: Boolean(options.withAiLlm),
63+
includeAiAgents: Boolean(options.withAiAgents),
6364
includeAiImage: Boolean(options.withAiImage),
6465
includeAiVideo: Boolean(options.withAiVideo),
6566
includeWorkbench: Boolean(options.withWorkbench)
@@ -81,6 +82,7 @@ export async function listConfiguredDockerVolumes(
8182
context: ProjectContext,
8283
options: Pick<GlobalCliOptions, never> & {
8384
withAiLlm?: boolean;
85+
withAiAgents?: boolean;
8486
withAiImage?: boolean;
8587
withAiVideo?: boolean;
8688
withWorkbench?: boolean;
@@ -90,6 +92,7 @@ export async function listConfiguredDockerVolumes(
9092
'docker',
9193
createComposeCommandArgs(context, ['config', '--volumes'], {
9294
includeAiLlm: Boolean(options.withAiLlm),
95+
includeAiAgents: Boolean(options.withAiAgents),
9396
includeAiImage: Boolean(options.withAiImage),
9497
includeAiVideo: Boolean(options.withAiVideo),
9598
includeWorkbench: Boolean(options.withWorkbench)

src/services/doctor.service.ts

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { createComposeCommandArgs } from '../lib/compose.js';
66
import type { DoctorCommandOptions } from '../types/cli.types.js';
77
import type { HostCheckResult, SmokeCheckDefinition } from '../types/doctor.types.js';
88
import type {
9+
AiAgentsSmokeEnv,
910
AiImageSmokeEnv,
1011
AiVideoSmokeEnv,
1112
AiLlmSmokeEnv,
@@ -21,6 +22,7 @@ import { readGatewayCertificate } from './gateway-certificate.service.js';
2122
import { checkNvidiaGpuRuntime } from './gpu-preflight.service.js';
2223
import { canLoginToN8n } from './n8n-owner.service.js';
2324
import {
25+
parseAiAgentsSmokeEnv,
2426
parseAiImageSmokeEnv,
2527
parseAiVideoSmokeEnv,
2628
parseAiLlmSmokeEnv,
@@ -69,11 +71,12 @@ export async function runDoctorCommand(
6971

7072
if (options.smoke) {
7173
const env = parseSmokeEnv(context.env);
74+
const aiAgentsEnv = options.withAiAgents ? parseAiAgentsSmokeEnv(context.env) : undefined;
7275
const aiLlmEnv = options.withAiLlm ? parseAiLlmSmokeEnv(context.env) : undefined;
7376
const aiImageEnv = options.withAiImage ? parseAiImageSmokeEnv(context.env) : undefined;
7477
const aiVideoEnv = options.withAiVideo ? parseAiVideoSmokeEnv(context.env) : undefined;
7578
const gatewayCertificate = await readGatewayCertificate(context, 'smoke');
76-
for (const smokeCheck of buildSmokeChecks(env, aiLlmEnv, aiImageEnv, aiVideoEnv)) {
79+
for (const smokeCheck of buildSmokeChecks(env, aiAgentsEnv, aiLlmEnv, aiImageEnv, aiVideoEnv)) {
7780
tasks.push(createCheckTask(results, 'smoke', smokeCheck.name, () => smokeCheck.run(gatewayCertificate)));
7881
}
7982
}
@@ -193,12 +196,13 @@ function npmCheckCommand(): [string, string[], string] {
193196
*/
194197
async function checkComposeConfiguration(
195198
context: ProjectContext,
196-
options: Pick<DoctorCommandOptions, 'withAiLlm' | 'withAiImage' | 'withAiVideo' | 'withWorkbench'>
199+
options: Pick<DoctorCommandOptions, 'withAiLlm' | 'withAiAgents' | 'withAiImage' | 'withAiVideo' | 'withWorkbench'>
197200
): Promise<HostCheckResult> {
198201
const result = await runCommand(
199202
'docker',
200203
createComposeCommandArgs(context, ['config', '-q'], {
201204
includeAiLlm: Boolean(options.withAiLlm),
205+
includeAiAgents: Boolean(options.withAiAgents),
202206
includeAiImage: Boolean(options.withAiImage),
203207
includeAiVideo: Boolean(options.withAiVideo),
204208
includeWorkbench: Boolean(options.withWorkbench)
@@ -239,6 +243,7 @@ function checkRequiredFile(projectRoot: string, relativePath: string): HostCheck
239243
*/
240244
function buildSmokeChecks(
241245
env: SmokeEnv,
246+
aiAgentsEnv?: AiAgentsSmokeEnv,
242247
aiLlmEnv?: AiLlmSmokeEnv,
243248
aiImageEnv?: AiImageSmokeEnv,
244249
aiVideoEnv?: AiVideoSmokeEnv
@@ -252,23 +257,26 @@ function buildSmokeChecks(
252257
name: 'Smoke Gitea',
253258
run: (caCertificate) =>
254259
runStatusCheck('Smoke Gitea', new URL('/api/healthz', env.GITEA_URL).toString(), caCertificate)
255-
},
256-
{
260+
}
261+
];
262+
263+
if (!aiAgentsEnv && !aiLlmEnv && !aiImageEnv && !aiVideoEnv) {
264+
return checks;
265+
}
266+
267+
if (aiAgentsEnv) {
268+
checks.push({
257269
name: 'Smoke n8n',
258270
run: async (caCertificate) => {
259-
const ok = await canLoginToN8n(env, caCertificate);
271+
const ok = await canLoginToN8n(aiAgentsEnv, caCertificate);
260272

261273
return {
262274
name: 'Smoke n8n',
263275
ok,
264276
detail: ok ? 'Owner login verified' : 'Could not authenticate with the configured owner account'
265277
};
266278
}
267-
}
268-
];
269-
270-
if (!aiLlmEnv && !aiImageEnv && !aiVideoEnv) {
271-
return checks;
279+
});
272280
}
273281

274282
if (aiImageEnv) {

src/services/host-preflight.service.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ import { runCommand } from '../utils/process.js';
1212

1313
const CORE_PORT_ENV_KEYS = [
1414
'LAB_HTTPS_PORT',
15-
'GITEA_HTTPS_PORT',
15+
'GITEA_HTTPS_PORT'
16+
] as const;
17+
18+
const AI_AGENTS_PORT_ENV_KEYS = [
1619
'N8N_HTTPS_PORT'
1720
] as const;
1821

@@ -46,6 +49,7 @@ export async function assertPublishedPortsAvailable(
4649
context: ProjectContext,
4750
options: {
4851
includeAiLlm: boolean;
52+
includeAiAgents: boolean;
4953
includeAiImage: boolean;
5054
includeAiVideo: boolean;
5155
includeWorkbench: boolean;
@@ -55,6 +59,7 @@ export async function assertPublishedPortsAvailable(
5559
context.env,
5660
options.includeWorkbench,
5761
options.includeAiLlm,
62+
options.includeAiAgents,
5863
options.includeAiImage,
5964
options.includeAiVideo
6065
);
@@ -82,12 +87,14 @@ function getConfiguredHostPorts(
8287
env: LabEnv,
8388
includeWorkbench: boolean,
8489
includeAiLlm: boolean,
90+
includeAiAgents: boolean,
8591
includeAiImage: boolean,
8692
includeAiVideo: boolean
8793
): HostPortDefinition[] {
8894
const envKeys = [
8995
...CORE_PORT_ENV_KEYS,
9096
...(includeAiLlm ? [...AI_LLM_PORT_ENV_KEYS] : []),
97+
...(includeAiAgents ? [...AI_AGENTS_PORT_ENV_KEYS] : []),
9198
...(includeAiImage ? [...AI_IMAGE_PORT_ENV_KEYS] : []),
9299
...(includeAiVideo ? [...AI_VIDEO_PORT_ENV_KEYS] : []),
93100
...(includeWorkbench ? [...WORKBENCH_PORT_ENV_KEYS] : [])

0 commit comments

Comments
 (0)