@ibm/ixora 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,800 +1,60 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ bold,
4
+ cmdRestart,
5
+ cyan,
6
+ detectComposeCmd,
7
+ detectPlatform,
8
+ die,
9
+ dim,
10
+ error,
11
+ info,
12
+ isSensitiveKey,
13
+ maskValue,
14
+ requireComposeFile,
15
+ requireInstalled,
16
+ resolveService,
17
+ runCompose,
18
+ runComposeCapture,
19
+ section,
20
+ success,
21
+ verifyRuntimeRunning,
22
+ waitForHealthy,
23
+ warn,
24
+ writeComposeFile
25
+ } from "./chunk-UYIZNGRR.js";
26
+ import {
27
+ COMPOSE_FILE,
28
+ ENV_FILE,
29
+ IXORA_DIR,
30
+ PROFILES,
31
+ PROVIDERS,
32
+ SCRIPT_VERSION,
33
+ VALID_PROFILES,
34
+ addSystem,
35
+ envGet,
36
+ readSystems,
37
+ removeSystem,
38
+ systemCount,
39
+ systemIdExists,
40
+ updateEnvKey,
41
+ writeEnvFile
42
+ } from "./chunk-F7YJCNQP.js";
2
43
 
3
44
  // src/cli.ts
4
45
  import { Command } from "commander";
5
46
 
6
- // src/lib/constants.ts
7
- import { homedir } from "os";
8
- import { join } from "path";
9
- var SCRIPT_VERSION = "0.1.0";
10
- var HEALTH_TIMEOUT = 30;
11
- var IXORA_DIR = join(homedir(), ".ixora");
12
- var COMPOSE_FILE = join(IXORA_DIR, "docker-compose.yml");
13
- var SYSTEMS_CONFIG = join(IXORA_DIR, "ixora-systems.yaml");
14
- var ENV_FILE = join(IXORA_DIR, ".env");
15
- var PROFILES = {
16
- full: {
17
- name: "full",
18
- label: "Full",
19
- description: "All agents, teams, and workflows (3 agents, 2 teams, 1 workflow)"
20
- },
21
- "sql-services": {
22
- name: "sql-services",
23
- label: "SQL Services",
24
- description: "SQL Services agent for database queries and performance monitoring"
25
- },
26
- security: {
27
- name: "security",
28
- label: "Security",
29
- description: "Security agent, multi-system security team, and assessment workflow"
30
- },
31
- knowledge: {
32
- name: "knowledge",
33
- label: "Knowledge",
34
- description: "Knowledge agent only \u2014 documentation retrieval (lightest)"
35
- }
36
- };
37
- var VALID_PROFILES = Object.keys(PROFILES);
38
- var PROVIDERS = {
39
- anthropic: {
40
- name: "anthropic",
41
- label: "Anthropic",
42
- agentModel: "anthropic:claude-sonnet-4-6",
43
- teamModel: "anthropic:claude-haiku-4-5",
44
- apiKeyVar: "ANTHROPIC_API_KEY",
45
- description: "Claude Sonnet 4.6 / Haiku 4.5 (recommended)"
46
- },
47
- openai: {
48
- name: "openai",
49
- label: "OpenAI",
50
- agentModel: "openai:gpt-4o",
51
- teamModel: "openai:gpt-4o-mini",
52
- apiKeyVar: "OPENAI_API_KEY",
53
- description: "GPT-4o / GPT-4o-mini"
54
- },
55
- google: {
56
- name: "google",
57
- label: "Google",
58
- agentModel: "google:gemini-2.5-pro",
59
- teamModel: "google:gemini-2.5-flash",
60
- apiKeyVar: "GOOGLE_API_KEY",
61
- description: "Gemini 2.5 Pro / Gemini 2.5 Flash"
62
- },
63
- ollama: {
64
- name: "ollama",
65
- label: "Ollama",
66
- agentModel: "ollama:llama3.1",
67
- teamModel: "ollama:llama3.1",
68
- apiKeyVar: "",
69
- description: "Local models via Ollama (no API key needed)"
70
- },
71
- custom: {
72
- name: "custom",
73
- label: "Custom",
74
- agentModel: "",
75
- teamModel: "",
76
- apiKeyVar: "",
77
- description: "Enter provider:model strings"
78
- }
79
- };
80
- var ALL_AGENTS = [
81
- "ibmi-security-assistant",
82
- "ibmi-system-health",
83
- "ibmi-db-explorer",
84
- "ibmi-db-performance",
85
- "ibmi-work-management",
86
- "ibmi-system-config",
87
- "ibmi-sql-service-guide",
88
- "ibmi-knowledge-agent"
89
- ];
90
- var OPS_AGENTS = [
91
- "ibmi-system-health",
92
- "ibmi-db-explorer",
93
- "ibmi-db-performance",
94
- "ibmi-work-management",
95
- "ibmi-system-config",
96
- "ibmi-sql-service-guide"
97
- ];
98
- var AGENT_PRESETS = {
99
- all: [...ALL_AGENTS],
100
- "security-ops": ["ibmi-security-assistant", ...OPS_AGENTS],
101
- security: ["ibmi-security-assistant"],
102
- operations: [...OPS_AGENTS],
103
- knowledge: ["ibmi-knowledge-agent"]
104
- };
105
-
106
47
  // src/commands/version.ts
107
- import { existsSync as existsSync4 } from "fs";
108
- import chalk2 from "chalk";
109
-
110
- // src/lib/env.ts
111
- import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync } from "fs";
112
- import { dirname } from "path";
113
- function sqEscape(value) {
114
- return value.replace(/'/g, "'\\''");
115
- }
116
- function envGet(key, envFile = ENV_FILE) {
117
- if (!existsSync(envFile)) return "";
118
- const content = readFileSync(envFile, "utf-8");
119
- for (const line of content.split("\n")) {
120
- if (line.startsWith(`${key}=`)) {
121
- let val = line.slice(key.length + 1);
122
- if (val.startsWith("'") && val.endsWith("'") || val.startsWith('"') && val.endsWith('"')) {
123
- val = val.slice(1, -1);
124
- }
125
- return val;
126
- }
127
- }
128
- return "";
129
- }
130
- var KNOWN_KEYS = [
131
- "ANTHROPIC_API_KEY",
132
- "OPENAI_API_KEY",
133
- "GOOGLE_API_KEY",
134
- "OLLAMA_HOST",
135
- "DB2i_HOST",
136
- "DB2i_USER",
137
- "DB2i_PASS",
138
- "DB2_PORT",
139
- "IXORA_PROFILE",
140
- "IXORA_VERSION",
141
- "IXORA_AGENT_MODEL",
142
- "IXORA_TEAM_MODEL"
143
- ];
144
- function writeEnvFile(config, envFile = ENV_FILE) {
145
- mkdirSync(dirname(envFile), { recursive: true });
146
- let extra = "";
147
- if (existsSync(envFile)) {
148
- const existing = readFileSync(envFile, "utf-8");
149
- const extraLines = existing.split("\n").filter((line) => {
150
- const trimmed = line.trim();
151
- if (!trimmed || trimmed.startsWith("#")) return false;
152
- const lineKey = trimmed.split("=")[0];
153
- return !KNOWN_KEYS.includes(lineKey);
154
- });
155
- extra = extraLines.join("\n");
156
- }
157
- let content = `# Model provider
158
- IXORA_AGENT_MODEL='${sqEscape(config.agentModel)}'
159
- IXORA_TEAM_MODEL='${sqEscape(config.teamModel)}'
160
- `;
161
- if (config.apiKeyVar && config.apiKeyValue) {
162
- content += `${config.apiKeyVar}='${sqEscape(config.apiKeyValue)}'
163
- `;
164
- }
165
- if (config.ollamaHost) {
166
- content += `OLLAMA_HOST='${sqEscape(config.ollamaHost)}'
167
- `;
168
- }
169
- content += `
170
- # IBM i connection
171
- DB2i_HOST='${sqEscape(config.db2Host)}'
172
- DB2i_USER='${sqEscape(config.db2User)}'
173
- DB2i_PASS='${sqEscape(config.db2Pass)}'
174
-
175
- # Deployment
176
- IXORA_PROFILE='${sqEscape(config.profile)}'
177
- IXORA_VERSION='${sqEscape(config.version)}'
178
- `;
179
- if (extra) {
180
- content += `
181
- # Preserved user settings
182
- ${extra}
183
- `;
184
- }
185
- writeFileSync(envFile, content, "utf-8");
186
- chmodSync(envFile, 384);
187
- }
188
- function updateEnvKey(key, value, envFile = ENV_FILE) {
189
- const escaped = sqEscape(value);
190
- if (existsSync(envFile)) {
191
- const content = readFileSync(envFile, "utf-8");
192
- const lines = content.split("\n");
193
- let found = false;
194
- const updated = lines.map((line) => {
195
- if (line.startsWith(`${key}=`)) {
196
- found = true;
197
- return `${key}='${escaped}'`;
198
- }
199
- return line;
200
- });
201
- if (found) {
202
- writeFileSync(envFile, updated.join("\n"), "utf-8");
203
- } else {
204
- const existing = readFileSync(envFile, "utf-8");
205
- const suffix = existing.endsWith("\n") ? "" : "\n";
206
- writeFileSync(
207
- envFile,
208
- `${existing}${suffix}${key}='${escaped}'
209
- `,
210
- "utf-8"
211
- );
212
- }
213
- } else {
214
- mkdirSync(dirname(envFile), { recursive: true });
215
- writeFileSync(envFile, `${key}='${escaped}'
216
- `, "utf-8");
217
- }
218
- chmodSync(envFile, 384);
219
- }
220
-
221
- // src/lib/compose.ts
222
- import { execa as execa2 } from "execa";
223
- import { existsSync as existsSync3, mkdirSync as mkdirSync3 } from "fs";
224
- import { writeFileSync as writeFileSync3 } from "fs";
225
-
226
- // src/lib/platform.ts
227
- import { arch } from "os";
228
- import { execa } from "execa";
229
- async function detectComposeCmd(optRuntime) {
230
- if (optRuntime) {
231
- switch (optRuntime) {
232
- case "docker":
233
- return "docker compose";
234
- case "podman":
235
- return "podman compose";
236
- default:
237
- throw new Error(
238
- `Unknown runtime: ${optRuntime} (choose: docker, podman)`
239
- );
240
- }
241
- }
242
- try {
243
- await execa("docker", ["compose", "version"]);
244
- return "docker compose";
245
- } catch {
246
- }
247
- try {
248
- await execa("podman", ["compose", "version"]);
249
- return "podman compose";
250
- } catch {
251
- }
252
- try {
253
- await execa("docker-compose", ["version"]);
254
- return "docker-compose";
255
- } catch {
256
- }
257
- throw new Error(
258
- "Neither 'docker compose', 'podman compose', nor 'docker-compose' found.\nPlease install Docker or Podman first."
259
- );
260
- }
261
- async function verifyRuntimeRunning(composeCmd) {
262
- const runtime = composeCmd.startsWith("docker") ? "docker" : "podman";
263
- try {
264
- await execa(runtime, ["info"]);
265
- } catch {
266
- const name = runtime === "docker" ? "Docker Desktop" : "Podman";
267
- throw new Error(
268
- `${name} is not running. Please start it and try again.`
269
- );
270
- }
271
- }
272
- function detectPlatform() {
273
- const cpuArch = arch();
274
- if (cpuArch === "ppc64") {
275
- return {
276
- dbImage: process.env["IXORA_DB_IMAGE"] ?? `ghcr.io/ibmi-agi/ixora-db:${process.env["IXORA_VERSION"] ?? "latest"}`
277
- };
278
- }
279
- return {};
280
- }
281
- function getComposeParts(cmd) {
282
- if (cmd === "docker-compose") {
283
- return ["docker-compose", []];
284
- }
285
- const [bin, sub] = cmd.split(" ");
286
- return [bin, [sub]];
287
- }
288
- function getRuntimeBin(cmd) {
289
- return cmd.startsWith("docker") ? "docker" : "podman";
290
- }
291
-
292
- // src/lib/systems.ts
293
- import {
294
- readFileSync as readFileSync2,
295
- writeFileSync as writeFileSync2,
296
- existsSync as existsSync2,
297
- mkdirSync as mkdirSync2,
298
- chmodSync as chmodSync2
299
- } from "fs";
300
- import { dirname as dirname2 } from "path";
301
- function readSystems(configFile = SYSTEMS_CONFIG) {
302
- if (!existsSync2(configFile)) return [];
303
- const content = readFileSync2(configFile, "utf-8");
304
- const systems = [];
305
- let current = null;
306
- for (const line of content.split("\n")) {
307
- const idMatch = line.match(/^ {2}- id: (.+)$/);
308
- if (idMatch) {
309
- if (current?.id) {
310
- systems.push({
311
- id: current.id,
312
- name: current.name ?? current.id,
313
- agents: current.agents ?? []
314
- });
315
- }
316
- current = { id: idMatch[1] };
317
- continue;
318
- }
319
- if (!current) continue;
320
- const nameMatch = line.match(/name: *'?([^']*)'?/);
321
- if (nameMatch) {
322
- current.name = nameMatch[1];
323
- continue;
324
- }
325
- const agentsMatch = line.match(/agents: *\[([^\]]*)\]/);
326
- if (agentsMatch) {
327
- current.agents = agentsMatch[1].split(",").map((a) => a.trim()).filter(Boolean);
328
- }
329
- }
330
- if (current?.id) {
331
- systems.push({
332
- id: current.id,
333
- name: current.name ?? current.id,
334
- agents: current.agents ?? []
335
- });
336
- }
337
- return systems;
338
- }
339
- function systemCount(configFile = SYSTEMS_CONFIG) {
340
- return readSystems(configFile).length;
341
- }
342
- function systemIdExists(id, configFile = SYSTEMS_CONFIG) {
343
- return readSystems(configFile).some((s) => s.id === id);
344
- }
345
- function totalSystemCount(envFile = ENV_FILE, configFile = SYSTEMS_CONFIG) {
346
- const additional = systemCount(configFile);
347
- const primaryHost = envGet("DB2i_HOST", envFile);
348
- return primaryHost ? additional + 1 : additional;
349
- }
350
- function addSystem(system, envFile = ENV_FILE, configFile = SYSTEMS_CONFIG) {
351
- const idUpper = system.id.toUpperCase().replace(/-/g, "_");
352
- updateEnvKey(`SYSTEM_${idUpper}_HOST`, system.host, envFile);
353
- updateEnvKey(`SYSTEM_${idUpper}_PORT`, system.port, envFile);
354
- updateEnvKey(`SYSTEM_${idUpper}_USER`, system.user, envFile);
355
- updateEnvKey(`SYSTEM_${idUpper}_PASS`, system.pass, envFile);
356
- const escapedName = system.name.replace(/'/g, "'\\''");
357
- const agentsList = system.agents.join(", ");
358
- const entry = ` - id: ${system.id}
359
- name: '${escapedName}'
360
- agents: [${agentsList}]
361
- `;
362
- mkdirSync2(dirname2(configFile), { recursive: true });
363
- if (!existsSync2(configFile) || systemCount(configFile) === 0) {
364
- const content = `# yaml-language-server: $schema=
365
- # Ixora Systems Configuration
366
- # Manage with: ixora system add|remove|list
367
- # Credentials stored in .env (SYSTEM_<ID>_USER, SYSTEM_<ID>_PASS)
368
- systems:
369
- ${entry}`;
370
- writeFileSync2(configFile, content, "utf-8");
371
- } else {
372
- const existing = readFileSync2(configFile, "utf-8");
373
- writeFileSync2(configFile, `${existing}${entry}`, "utf-8");
374
- }
375
- chmodSync2(configFile, 384);
376
- }
377
- function removeSystem(id, envFile = ENV_FILE, configFile = SYSTEMS_CONFIG) {
378
- if (!existsSync2(configFile)) {
379
- throw new Error("No systems configured");
380
- }
381
- if (!systemIdExists(id, configFile)) {
382
- throw new Error(`System '${id}' not found`);
383
- }
384
- const content = readFileSync2(configFile, "utf-8");
385
- const lines = content.split("\n");
386
- const output = [];
387
- let skip = false;
388
- for (const line of lines) {
389
- if (line === ` - id: ${id}`) {
390
- skip = true;
391
- continue;
392
- }
393
- if (line.match(/^ {2}- id: /)) {
394
- skip = false;
395
- }
396
- if (!skip) {
397
- output.push(line);
398
- }
399
- }
400
- writeFileSync2(configFile, output.join("\n"), "utf-8");
401
- chmodSync2(configFile, 384);
402
- if (existsSync2(envFile)) {
403
- const idUpper = id.toUpperCase().replace(/-/g, "_");
404
- const envContent = readFileSync2(envFile, "utf-8");
405
- const filtered = envContent.split("\n").filter((line) => !line.startsWith(`SYSTEM_${idUpper}_`)).join("\n");
406
- writeFileSync2(envFile, filtered, "utf-8");
407
- chmodSync2(envFile, 384);
408
- }
409
- }
410
-
411
- // src/lib/templates/single-compose.ts
412
- function generateSingleCompose() {
413
- return `services:
414
- agentos-db:
415
- image: \${IXORA_DB_IMAGE:-agnohq/pgvector:18}
416
- restart: unless-stopped
417
- ports:
418
- - "\${DB_PORT:-5432}:5432"
419
- environment:
420
- POSTGRES_USER: \${DB_USER:-ai}
421
- POSTGRES_PASSWORD: \${DB_PASS:-ai}
422
- POSTGRES_DB: \${DB_DATABASE:-ai}
423
- volumes:
424
- - pgdata:/var/lib/postgresql
425
- healthcheck:
426
- test: ["CMD-SHELL", "pg_isready -U \${DB_USER:-ai}"]
427
- interval: 5s
428
- timeout: 5s
429
- retries: 5
430
-
431
- ibmi-mcp-server:
432
- image: ghcr.io/ibmi-agi/ixora-mcp-server:\${IXORA_VERSION:-latest}
433
- restart: unless-stopped
434
- ports:
435
- - "3010:3010"
436
- environment:
437
- DB2i_HOST: \${DB2i_HOST}
438
- DB2i_USER: \${DB2i_USER}
439
- DB2i_PASS: \${DB2i_PASS}
440
- DB2_PORT: \${DB2_PORT:-8076}
441
- MCP_TRANSPORT_TYPE: http
442
- MCP_SESSION_MODE: stateless
443
- YAML_AUTO_RELOAD: "true"
444
- TOOLS_YAML_PATH: /usr/src/app/tools
445
- YAML_ALLOW_DUPLICATE_SOURCES: "true"
446
- IBMI_ENABLE_EXECUTE_SQL: "true"
447
- IBMI_ENABLE_DEFAULT_TOOLS: "true"
448
- MCP_AUTH_MODE: "none"
449
- IBMI_HTTP_AUTH_ENABLED: "false"
450
- MCP_RATE_LIMIT_ENABLED: "true"
451
- MCP_RATE_LIMIT_MAX_REQUESTS: "5000"
452
- MCP_RATE_LIMIT_WINDOW_MS: "60000"
453
- MCP_RATE_LIMIT_SKIP_DEV: "true"
454
- MCP_POOL_QUERY_TIMEOUT_MS: "120000"
455
- healthcheck:
456
- test: ["CMD-SHELL", "node -e \\"fetch('http://localhost:3010/healthz').then(function(r){process.exit(r.ok?0:1)}).catch(function(){process.exit(1)})\\""]
457
- interval: 5s
458
- timeout: 5s
459
- retries: 5
460
- start_period: 3s
461
-
462
- api:
463
- image: ghcr.io/ibmi-agi/ixora-api:\${IXORA_VERSION:-latest}
464
- command: uvicorn app.main:app --host 0.0.0.0 --port 8000
465
- restart: unless-stopped
466
- ports:
467
- - "8000:8000"
468
- environment:
469
- ANTHROPIC_API_KEY: \${ANTHROPIC_API_KEY:-}
470
- OPENAI_API_KEY: \${OPENAI_API_KEY:-}
471
- GOOGLE_API_KEY: \${GOOGLE_API_KEY:-}
472
- OLLAMA_HOST: \${OLLAMA_HOST:-http://host.docker.internal:11434}
473
- IXORA_AGENT_MODEL: \${IXORA_AGENT_MODEL:-anthropic:claude-sonnet-4-6}
474
- IXORA_TEAM_MODEL: \${IXORA_TEAM_MODEL:-anthropic:claude-haiku-4-5}
475
- DB_HOST: agentos-db
476
- DB_PORT: "5432"
477
- DB_USER: \${DB_USER:-ai}
478
- DB_PASS: \${DB_PASS:-ai}
479
- DB_DATABASE: \${DB_DATABASE:-ai}
480
- MCP_URL: http://ibmi-mcp-server:3010/mcp
481
- IAASSIST_DEPLOYMENT_CONFIG: app/config/deployments/\${IXORA_PROFILE:-full}.yaml
482
- DATA_DIR: /data
483
- RUNTIME_ENV: docker
484
- WAIT_FOR_DB: "True"
485
- RAG_API_URL: \${RAG_API_URL:-}
486
- RAG_API_TIMEOUT: \${RAG_API_TIMEOUT:-120}
487
- AUTH_ENABLED: "false"
488
- MCP_AUTH_MODE: "none"
489
- CORS_ORIGINS: \${CORS_ORIGINS:-*}
490
- IXORA_ENABLE_BUILDER: "true"
491
- DB2i_HOST: \${DB2i_HOST}
492
- DB2i_USER: \${DB2i_USER}
493
- DB2i_PASS: \${DB2i_PASS}
494
- DB2_PORT: \${DB2_PORT:-8076}
495
- volumes:
496
- - agentos-data:/data
497
- - type: bind
498
- source: \${HOME}/.ixora/user_tools
499
- target: /data/user_tools
500
- bind:
501
- create_host_path: true
502
- depends_on:
503
- agentos-db:
504
- condition: service_healthy
505
- ibmi-mcp-server:
506
- condition: service_healthy
507
- healthcheck:
508
- test: ["CMD-SHELL", "python -c \\"import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=3).getcode()==200 else 1)\\""]
509
- interval: 10s
510
- timeout: 5s
511
- retries: 6
512
- start_period: 30s
513
-
514
- ui:
515
- image: ghcr.io/ibmi-agi/ixora-ui:\${IXORA_VERSION:-latest}
516
- restart: unless-stopped
517
- ports:
518
- - "3000:3000"
519
- environment:
520
- NEXT_PUBLIC_API_URL: http://localhost:8000
521
- depends_on:
522
- api:
523
- condition: service_healthy
524
-
525
- volumes:
526
- pgdata:
527
- agentos-data:
528
- `;
529
- }
530
-
531
- // src/lib/templates/multi-compose.ts
532
- function generateMultiCompose(envFile = ENV_FILE, configFile = SYSTEMS_CONFIG) {
533
- const version = envGet("IXORA_VERSION", envFile) || "latest";
534
- const dbImage = process.env["IXORA_DB_IMAGE"] ?? "agnohq/pgvector:18";
535
- let content = `# Auto-generated for multi-system deployment
536
- # Regenerated on every start. Edit ixora-systems.yaml instead.
537
- services:
538
- agentos-db:
539
- image: ${dbImage}
540
- restart: unless-stopped
541
- ports:
542
- - "\${DB_PORT:-5432}:5432"
543
- environment:
544
- POSTGRES_USER: \${DB_USER:-ai}
545
- POSTGRES_PASSWORD: \${DB_PASS:-ai}
546
- POSTGRES_DB: \${DB_DATABASE:-ai}
547
- volumes:
548
- - pgdata:/var/lib/postgresql
549
- healthcheck:
550
- test: ["CMD-SHELL", "pg_isready -U \${DB_USER:-ai}"]
551
- interval: 5s
552
- timeout: 5s
553
- retries: 5
554
-
555
- `;
556
- let apiPort = 8e3;
557
- let firstApi = "";
558
- const allSystems = [];
559
- const primaryHost = envGet("DB2i_HOST", envFile);
560
- if (primaryHost) {
561
- updateEnvKey("SYSTEM_DEFAULT_HOST", primaryHost, envFile);
562
- updateEnvKey(
563
- "SYSTEM_DEFAULT_PORT",
564
- envGet("DB2_PORT", envFile) || "8076",
565
- envFile
566
- );
567
- updateEnvKey("SYSTEM_DEFAULT_USER", envGet("DB2i_USER", envFile), envFile);
568
- updateEnvKey("SYSTEM_DEFAULT_PASS", envGet("DB2i_PASS", envFile), envFile);
569
- allSystems.push({ id: "default", name: primaryHost });
570
- }
571
- const additionalSystems = readSystems(configFile);
572
- for (const sys of additionalSystems) {
573
- allSystems.push({ id: sys.id, name: sys.name });
574
- }
575
- for (const sys of allSystems) {
576
- const idUpper = sys.id.toUpperCase().replace(/-/g, "_");
577
- content += ` mcp-${sys.id}:
578
- image: ghcr.io/ibmi-agi/ixora-mcp-server:\${IXORA_VERSION:-${version}}
579
- restart: unless-stopped
580
- environment:
581
- DB2i_HOST: \${SYSTEM_${idUpper}_HOST}
582
- DB2i_USER: \${SYSTEM_${idUpper}_USER}
583
- DB2i_PASS: \${SYSTEM_${idUpper}_PASS}
584
- DB2_PORT: \${SYSTEM_${idUpper}_PORT:-8076}
585
- MCP_TRANSPORT_TYPE: http
586
- MCP_SESSION_MODE: stateless
587
- YAML_AUTO_RELOAD: "true"
588
- TOOLS_YAML_PATH: /usr/src/app/tools
589
- YAML_ALLOW_DUPLICATE_SOURCES: "true"
590
- IBMI_ENABLE_EXECUTE_SQL: "true"
591
- IBMI_ENABLE_DEFAULT_TOOLS: "true"
592
- MCP_AUTH_MODE: "none"
593
- IBMI_HTTP_AUTH_ENABLED: "false"
594
- MCP_POOL_QUERY_TIMEOUT_MS: "120000"
595
- healthcheck:
596
- test: ["CMD-SHELL", "node -e \\"fetch('http://localhost:3010/healthz').then(function(r){process.exit(r.ok?0:1)}).catch(function(){process.exit(1)})\\""]
597
- interval: 5s
598
- timeout: 5s
599
- retries: 5
600
- start_period: 3s
601
-
602
- `;
603
- content += ` api-${sys.id}:
604
- image: ghcr.io/ibmi-agi/ixora-api:\${IXORA_VERSION:-${version}}
605
- command: uvicorn app.main:app --host 0.0.0.0 --port 8000
606
- restart: unless-stopped
607
- ports:
608
- - "${apiPort}:8000"
609
- environment:
610
- ANTHROPIC_API_KEY: \${ANTHROPIC_API_KEY:-}
611
- OPENAI_API_KEY: \${OPENAI_API_KEY:-}
612
- GOOGLE_API_KEY: \${GOOGLE_API_KEY:-}
613
- OLLAMA_HOST: \${OLLAMA_HOST:-http://host.docker.internal:11434}
614
- IXORA_AGENT_MODEL: \${IXORA_AGENT_MODEL:-anthropic:claude-sonnet-4-6}
615
- IXORA_TEAM_MODEL: \${IXORA_TEAM_MODEL:-anthropic:claude-haiku-4-5}
616
- DB_HOST: agentos-db
617
- DB_PORT: "5432"
618
- DB_USER: \${DB_USER:-ai}
619
- DB_PASS: \${DB_PASS:-ai}
620
- DB_DATABASE: \${DB_DATABASE:-ai}
621
- MCP_URL: http://mcp-${sys.id}:3010/mcp
622
- IXORA_SYSTEM_ID: ${sys.id}
623
- IXORA_SYSTEM_NAME: ${sys.name}
624
- IAASSIST_DEPLOYMENT_CONFIG: app/config/deployments/\${IXORA_PROFILE:-full}.yaml
625
- DATA_DIR: /data
626
- RUNTIME_ENV: docker
627
- WAIT_FOR_DB: "True"
628
- CORS_ORIGINS: \${CORS_ORIGINS:-*}
629
- AUTH_ENABLED: "false"
630
- MCP_AUTH_MODE: "none"
631
- IXORA_ENABLE_BUILDER: "true"
632
- DB2i_HOST: \${SYSTEM_${idUpper}_HOST}
633
- DB2i_USER: \${SYSTEM_${idUpper}_USER}
634
- DB2i_PASS: \${SYSTEM_${idUpper}_PASS}
635
- DB2_PORT: \${SYSTEM_${idUpper}_PORT:-8076}
636
- volumes:
637
- - agentos-data:/data
638
- - type: bind
639
- source: \${HOME}/.ixora/user_tools
640
- target: /data/user_tools
641
- bind:
642
- create_host_path: true
643
- depends_on:
644
- agentos-db:
645
- condition: service_healthy
646
- mcp-${sys.id}:
647
- condition: service_healthy
648
- healthcheck:
649
- test: ["CMD-SHELL", "python -c \\"import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=3).getcode()==200 else 1)\\""]
650
- interval: 10s
651
- timeout: 5s
652
- retries: 6
653
- start_period: 30s
654
-
655
- `;
656
- if (!firstApi) firstApi = `api-${sys.id}`;
657
- apiPort++;
658
- }
659
- content += ` ui:
660
- image: ghcr.io/ibmi-agi/ixora-ui:\${IXORA_VERSION:-${version}}
661
- restart: unless-stopped
662
- ports:
663
- - "3000:3000"
664
- environment:
665
- NEXT_PUBLIC_API_URL: http://localhost:8000
666
- depends_on:
667
- ${firstApi}:
668
- condition: service_healthy
669
-
670
- volumes:
671
- pgdata:
672
- agentos-data:
673
- `;
674
- return content;
675
- }
676
-
677
- // src/lib/ui.ts
48
+ import { existsSync } from "fs";
678
49
  import chalk from "chalk";
679
- function info(message) {
680
- console.log(`${chalk.blue("==>")} ${chalk.bold(message)}`);
681
- }
682
- function success(message) {
683
- console.log(`${chalk.green("==>")} ${chalk.bold(message)}`);
684
- }
685
- function warn(message) {
686
- console.log(`${chalk.yellow("Warning:")} ${message}`);
687
- }
688
- function error(message) {
689
- console.error(`${chalk.red("Error:")} ${message}`);
690
- }
691
- function die(message) {
692
- error(message);
693
- process.exit(1);
694
- }
695
- function maskValue(value) {
696
- if (!value) return chalk.dim("(not set)");
697
- if (value.length <= 4) return "****";
698
- return `${value.slice(0, 4)}****`;
699
- }
700
- function isSensitiveKey(key) {
701
- const upper = key.toUpperCase();
702
- return /KEY|TOKEN|PASS|SECRET|ENCRYPT/.test(upper);
703
- }
704
- function dim(message) {
705
- return chalk.dim(message);
706
- }
707
- function bold(message) {
708
- return chalk.bold(message);
709
- }
710
- function cyan(message) {
711
- return chalk.cyan(message);
712
- }
713
- function section(title) {
714
- console.log(
715
- ` ${chalk.dim(`\u2500\u2500 ${title} ${"\u2500".repeat(Math.max(0, 49 - title.length))}`)}`
716
- );
717
- }
718
-
719
- // src/lib/compose.ts
720
- async function runCompose(composeCmd, args, options = {}) {
721
- const [bin, subArgs] = getComposeParts(composeCmd);
722
- const fullArgs = [
723
- ...subArgs,
724
- "-p",
725
- "ixora",
726
- "-f",
727
- COMPOSE_FILE,
728
- "--env-file",
729
- ENV_FILE,
730
- ...args
731
- ];
732
- try {
733
- const result = await execa2(bin, fullArgs, {
734
- stdio: "inherit",
735
- ...options
736
- });
737
- return {
738
- stdout: String(result.stdout ?? ""),
739
- stderr: String(result.stderr ?? ""),
740
- exitCode: result.exitCode ?? 0
741
- };
742
- } catch (err) {
743
- const exitCode = err && typeof err === "object" && "exitCode" in err ? err.exitCode : 1;
744
- error(`Command failed: ${composeCmd} ${args.join(" ")}`);
745
- console.log(` Check ${bold("ixora logs")} for details.`);
746
- process.exit(exitCode);
747
- }
748
- }
749
- async function runComposeCapture(composeCmd, args) {
750
- const [bin, subArgs] = getComposeParts(composeCmd);
751
- const fullArgs = [
752
- ...subArgs,
753
- "-p",
754
- "ixora",
755
- "-f",
756
- COMPOSE_FILE,
757
- "--env-file",
758
- ENV_FILE,
759
- ...args
760
- ];
761
- const result = await execa2(bin, fullArgs, { stdio: "pipe" });
762
- return result.stdout;
763
- }
764
- function writeComposeFile(envFile = ENV_FILE) {
765
- mkdirSync3(IXORA_DIR, { recursive: true });
766
- const total = totalSystemCount(envFile);
767
- let content;
768
- if (total > 1) {
769
- content = generateMultiCompose(envFile);
770
- } else {
771
- content = generateSingleCompose();
772
- }
773
- writeFileSync3(COMPOSE_FILE, content, "utf-8");
774
- }
775
- function requireInstalled() {
776
- if (!existsSync3(ENV_FILE)) {
777
- throw new Error("ixora is not installed. Run: ixora install");
778
- }
779
- }
780
- function requireComposeFile() {
781
- if (!existsSync3(COMPOSE_FILE)) {
782
- throw new Error("ixora is not installed. Run: ixora install");
783
- }
784
- }
785
- function resolveService(input3) {
786
- return input3.replace(/^ixora-/, "").replace(/-\d+$/, "");
787
- }
788
-
789
- // src/commands/version.ts
790
50
  async function cmdVersion(opts) {
791
51
  console.log(`ixora ${SCRIPT_VERSION}`);
792
- if (!existsSync4(ENV_FILE)) return;
52
+ if (!existsSync(ENV_FILE)) return;
793
53
  const version = envGet("IXORA_VERSION") || "latest";
794
54
  const agentModel = envGet("IXORA_AGENT_MODEL") || "anthropic:claude-sonnet-4-6";
795
55
  console.log(` images: ${version}`);
796
56
  console.log(` model: ${agentModel}`);
797
- if (existsSync4(COMPOSE_FILE)) {
57
+ if (existsSync(COMPOSE_FILE)) {
798
58
  try {
799
59
  const composeCmd = await detectComposeCmd(opts?.runtime);
800
60
  const output = await runComposeCapture(composeCmd, [
@@ -806,11 +66,11 @@ async function cmdVersion(opts) {
806
66
  const images = parseComposeImages(output);
807
67
  if (images.length > 0) {
808
68
  console.log();
809
- console.log(` ${chalk2.bold("Running containers:")}`);
69
+ console.log(` ${chalk.bold("Running containers:")}`);
810
70
  for (const img of images) {
811
71
  const tag = img.Tag || "unknown";
812
72
  const id = img.ID ? dim(` (${img.ID.slice(0, 12)})`) : "";
813
- const imageStr = tag === "latest" ? `${img.Repository || ""}:latest${id}` : `${img.Repository || ""}:${tag}`;
73
+ const imageStr = `${img.Repository || ""}:${tag}${id}`;
814
74
  console.log(
815
75
  ` ${(img.Service || "").padEnd(22)} ${dim(imageStr)}`
816
76
  );
@@ -840,7 +100,7 @@ function parseComposeImages(output) {
840
100
  }
841
101
 
842
102
  // src/commands/status.ts
843
- import chalk3 from "chalk";
103
+ import chalk2 from "chalk";
844
104
  async function cmdStatus(opts) {
845
105
  try {
846
106
  requireComposeFile();
@@ -858,9 +118,9 @@ async function cmdStatus(opts) {
858
118
  const profile = envGet("IXORA_PROFILE") || "full";
859
119
  const version = envGet("IXORA_VERSION") || "latest";
860
120
  console.log();
861
- console.log(` ${chalk3.bold("Profile:")} ${profile}`);
862
- console.log(` ${chalk3.bold("Version:")} ${version}`);
863
- console.log(` ${chalk3.bold("Config:")} ${IXORA_DIR}`);
121
+ console.log(` ${chalk2.bold("Profile:")} ${profile}`);
122
+ console.log(` ${chalk2.bold("Version:")} ${version}`);
123
+ console.log(` ${chalk2.bold("Config:")} ${IXORA_DIR}`);
864
124
  console.log();
865
125
  await runCompose(composeCmd, ["ps"]);
866
126
  try {
@@ -873,14 +133,12 @@ async function cmdStatus(opts) {
873
133
  const images = parseComposeImages2(output);
874
134
  if (images.length > 0) {
875
135
  console.log();
876
- console.log(` ${chalk3.bold("Images:")}`);
136
+ console.log(` ${chalk2.bold("Images:")}`);
877
137
  for (const img of images) {
878
138
  const tag = img.Tag || "unknown";
879
139
  const id = img.ID ? ` (${img.ID.slice(0, 12)})` : "";
880
140
  const tagDisplay = tag === "latest" ? `${tag}${dim(id)}` : tag;
881
- console.log(
882
- ` ${dim(`${img.Repository || ""}:`)}${tagDisplay}`
883
- );
141
+ console.log(` ${dim(`${img.Repository || ""}:`)}${tagDisplay}`);
884
142
  }
885
143
  console.log();
886
144
  }
@@ -906,69 +164,6 @@ function parseComposeImages2(output) {
906
164
  }
907
165
  }
908
166
 
909
- // src/lib/health.ts
910
- import { execa as execa3 } from "execa";
911
- import ora from "ora";
912
- async function waitForHealthy(composeCmd, timeout = HEALTH_TIMEOUT) {
913
- const spinner = ora("Waiting for services to become healthy...").start();
914
- const runtime = getRuntimeBin(composeCmd);
915
- let apiContainer = "";
916
- try {
917
- const output = await runComposeCapture(composeCmd, [
918
- "ps",
919
- "--format",
920
- "{{.Name}}"
921
- ]);
922
- const containers = output.split("\n").filter(Boolean);
923
- apiContainer = containers.find((c) => c.includes("ixora-api-")) ?? "";
924
- } catch {
925
- }
926
- if (!apiContainer) {
927
- spinner.stop();
928
- warn("Could not find API container \u2014 skipping health check");
929
- return true;
930
- }
931
- spinner.text = `Waiting for services to become healthy (up to ${timeout}s)...`;
932
- let elapsed = 0;
933
- while (elapsed < timeout) {
934
- try {
935
- const stateResult = await execa3(runtime, [
936
- "inspect",
937
- "--format",
938
- "{{.State.Status}}",
939
- apiContainer
940
- ]);
941
- const state = stateResult.stdout.trim();
942
- if (state === "exited" || state === "dead") {
943
- spinner.fail("API container failed to start");
944
- console.log(`
945
- Run ${bold("ixora logs api")} to investigate.`);
946
- return false;
947
- }
948
- const healthResult = await execa3(runtime, [
949
- "inspect",
950
- "--format",
951
- "{{.State.Health.Status}}",
952
- apiContainer
953
- ]);
954
- const health = healthResult.stdout.trim();
955
- if (health === "healthy") {
956
- spinner.succeed("Services are healthy");
957
- return true;
958
- }
959
- } catch {
960
- }
961
- await new Promise((r) => setTimeout(r, 2e3));
962
- elapsed += 2;
963
- spinner.text = `Waiting for services to become healthy (${elapsed}s/${timeout}s)...`;
964
- }
965
- spinner.warn(
966
- `Services did not become healthy within ${timeout}s \u2014 they may still be starting`
967
- );
968
- console.log(` Run ${bold("ixora logs api")} to investigate.`);
969
- return false;
970
- }
971
-
972
167
  // src/commands/start.ts
973
168
  async function cmdStart(opts) {
974
169
  try {
@@ -996,24 +191,16 @@ async function cmdStart(opts) {
996
191
  writeComposeFile();
997
192
  success("Wrote docker-compose.yml");
998
193
  info("Starting ixora services...");
999
- await runCompose(composeCmd, ["up", "-d"]);
194
+ await runCompose(composeCmd, ["up", "-d", "--remove-orphans"]);
1000
195
  await waitForHealthy(composeCmd);
1001
- const profile = envGet("IXORA_PROFILE") || "full";
1002
- const total = totalSystemCount();
196
+ const systems = readSystems();
1003
197
  console.log();
1004
198
  success("ixora is running!");
1005
199
  console.log(` ${bold("UI:")} http://localhost:3000`);
1006
200
  console.log(` ${bold("API:")} http://localhost:8000`);
1007
- console.log(` ${bold("Profile:")} ${profile}`);
1008
- if (total > 1) {
1009
- console.log(` ${bold("Systems:")} ${total}`);
201
+ if (systems.length > 1) {
202
+ console.log(` ${bold("Systems:")} ${systems.length}`);
1010
203
  let port = 8e3;
1011
- const primaryHost = envGet("DB2i_HOST");
1012
- if (primaryHost) {
1013
- console.log(` ${dim(`:${port} \u2192 default (${primaryHost})`)}`);
1014
- port++;
1015
- }
1016
- const systems = readSystems();
1017
204
  for (const sys of systems) {
1018
205
  const idUpper = sys.id.toUpperCase().replace(/-/g, "_");
1019
206
  const sysHost = envGet(`SYSTEM_${idUpper}_HOST`);
@@ -1021,8 +208,10 @@ async function cmdStart(opts) {
1021
208
  port++;
1022
209
  }
1023
210
  console.log(
1024
- ` ${dim("Note: UI connects to primary system (:8000) only. Use API ports for other systems.")}`
211
+ ` ${dim("Note: UI connects to first system (:8000) only. Use API ports for other systems.")}`
1025
212
  );
213
+ } else if (systems.length === 1) {
214
+ console.log(` ${bold("Profile:")} ${systems[0].profile || "full"}`);
1026
215
  }
1027
216
  console.log();
1028
217
  }
@@ -1047,8 +236,8 @@ async function cmdStop(opts) {
1047
236
  success("Services stopped");
1048
237
  }
1049
238
 
1050
- // src/commands/restart.ts
1051
- async function cmdRestart(opts, service) {
239
+ // src/commands/logs.ts
240
+ async function cmdLogs(opts, service) {
1052
241
  try {
1053
242
  requireComposeFile();
1054
243
  } catch (e) {
@@ -1064,45 +253,47 @@ async function cmdRestart(opts, service) {
1064
253
  detectPlatform();
1065
254
  if (service) {
1066
255
  const svc = resolveService(service);
1067
- info(`Restarting ${svc}...`);
1068
- await runCompose(composeCmd, [
1069
- "up",
1070
- "-d",
1071
- "--force-recreate",
1072
- "--no-deps",
1073
- svc
1074
- ]);
1075
- success(`Restarted ${svc}`);
256
+ await runCompose(composeCmd, ["logs", "-f", svc]);
1076
257
  } else {
1077
- info("Restarting all services...");
1078
- await runCompose(composeCmd, ["up", "-d", "--force-recreate"]);
1079
- await waitForHealthy(composeCmd);
1080
- console.log();
1081
- success("All services restarted");
258
+ await runCompose(composeCmd, ["logs", "-f"]);
1082
259
  }
1083
260
  }
1084
261
 
1085
- // src/commands/logs.ts
1086
- async function cmdLogs(opts, service) {
1087
- try {
1088
- requireComposeFile();
1089
- } catch (e) {
1090
- die(e.message);
262
+ // src/commands/upgrade.ts
263
+ import { select } from "@inquirer/prompts";
264
+
265
+ // src/lib/registry.ts
266
+ var GHCR_TOKEN_URL = "https://ghcr.io/token";
267
+ var GHCR_TAGS_URL = "https://ghcr.io/v2";
268
+ var RELEASE_TAG = /^v\d+\.\d+\.\d+$/;
269
+ async function fetchImageTags(image) {
270
+ const tokenRes = await fetch(
271
+ `${GHCR_TOKEN_URL}?scope=repository:${image}:pull`
272
+ );
273
+ if (!tokenRes.ok) {
274
+ throw new Error(`Failed to get registry token: ${tokenRes.status}`);
1091
275
  }
1092
- let composeCmd;
1093
- try {
1094
- composeCmd = await detectComposeCmd(opts.runtime);
1095
- await verifyRuntimeRunning(composeCmd);
1096
- } catch (e) {
1097
- die(e.message);
276
+ const { token } = await tokenRes.json();
277
+ const tagsRes = await fetch(`${GHCR_TAGS_URL}/${image}/tags/list`, {
278
+ headers: { Authorization: `Bearer ${token}` }
279
+ });
280
+ if (!tagsRes.ok) {
281
+ throw new Error(`Failed to fetch tags: ${tagsRes.status}`);
1098
282
  }
1099
- detectPlatform();
1100
- if (service) {
1101
- const svc = resolveService(service);
1102
- await runCompose(composeCmd, ["logs", "-f", svc]);
1103
- } else {
1104
- await runCompose(composeCmd, ["logs", "-f"]);
283
+ const { tags } = await tagsRes.json();
284
+ return tags.filter((t) => RELEASE_TAG.test(t)).sort((a, b) => compareSemver(b, a));
285
+ }
286
+ function compareSemver(a, b) {
287
+ const pa = a.replace(/^v/, "").split(".").map(Number);
288
+ const pb = b.replace(/^v/, "").split(".").map(Number);
289
+ for (let i = 0; i < 3; i++) {
290
+ if (pa[i] !== pb[i]) return pa[i] - pb[i];
1105
291
  }
292
+ return 0;
293
+ }
294
+ function normalizeVersion(version) {
295
+ const v = version.trim();
296
+ return v.startsWith("v") ? v : `v${v}`;
1106
297
  }
1107
298
 
1108
299
  // src/commands/upgrade.ts
@@ -1121,13 +312,35 @@ async function cmdUpgrade(opts) {
1121
312
  }
1122
313
  detectPlatform();
1123
314
  const previousVersion = envGet("IXORA_VERSION") || "latest";
1124
- info("Upgrading ixora...");
315
+ let targetVersion;
316
+ const explicitVersion = opts.version || opts.imageVersion;
317
+ if (explicitVersion) {
318
+ targetVersion = normalizeVersion(explicitVersion);
319
+ } else {
320
+ let tags;
321
+ try {
322
+ tags = await fetchImageTags("ibmi-agi/ixora-api");
323
+ } catch {
324
+ warn("Could not fetch available versions from registry");
325
+ die("Specify a version: ixora upgrade <version>");
326
+ }
327
+ if (tags.length === 0) {
328
+ die("No release versions found in registry");
329
+ }
330
+ targetVersion = await select({
331
+ message: "Select version to upgrade to",
332
+ choices: tags.map((t) => ({
333
+ value: t,
334
+ name: t === previousVersion ? `${t} (current)` : t
335
+ }))
336
+ });
337
+ }
338
+ info(`Upgrading ixora: ${previousVersion} \u2192 ${targetVersion}`);
339
+ info("Stopping services...");
340
+ await runCompose(composeCmd, ["down", "--remove-orphans"]);
341
+ updateEnvKey("IXORA_VERSION", targetVersion);
1125
342
  writeComposeFile();
1126
343
  success("Wrote docker-compose.yml");
1127
- if (opts.imageVersion) {
1128
- info(`Pinning version: ${previousVersion} \u2192 ${opts.imageVersion}`);
1129
- updateEnvKey("IXORA_VERSION", opts.imageVersion);
1130
- }
1131
344
  if (opts.profile) {
1132
345
  if (!VALID_PROFILES.includes(opts.profile)) {
1133
346
  die(
@@ -1138,30 +351,29 @@ async function cmdUpgrade(opts) {
1138
351
  updateEnvKey("IXORA_PROFILE", opts.profile);
1139
352
  }
1140
353
  if (opts.pull !== false) {
1141
- info("Pulling latest images...");
354
+ info("Pulling images...");
1142
355
  await runCompose(composeCmd, ["pull"]);
1143
356
  }
1144
357
  info("Restarting services...");
1145
358
  await runCompose(composeCmd, ["up", "-d"]);
1146
359
  await waitForHealthy(composeCmd);
1147
- const newVersion = envGet("IXORA_VERSION") || "latest";
1148
360
  const profile = envGet("IXORA_PROFILE") || "full";
1149
361
  console.log();
1150
362
  success("Upgrade complete!");
1151
- console.log(` ${bold("Version:")} ${newVersion}`);
363
+ console.log(` ${bold("Version:")} ${targetVersion}`);
1152
364
  console.log(` ${bold("Profile:")} ${profile}`);
1153
- if (opts.imageVersion && previousVersion !== opts.imageVersion) {
365
+ if (previousVersion !== targetVersion) {
1154
366
  console.log(` ${dim(`(was ${previousVersion})`)}`);
1155
367
  }
1156
368
  console.log();
1157
369
  }
1158
370
 
1159
371
  // src/commands/uninstall.ts
1160
- import { existsSync as existsSync5, rmSync } from "fs";
372
+ import { existsSync as existsSync2, rmSync } from "fs";
1161
373
  import { confirm } from "@inquirer/prompts";
1162
- import chalk4 from "chalk";
1163
- import { homedir as homedir2 } from "os";
1164
- import { join as join2 } from "path";
374
+ import chalk3 from "chalk";
375
+ import { homedir } from "os";
376
+ import { join } from "path";
1165
377
  async function cmdUninstall(opts) {
1166
378
  let composeCmd;
1167
379
  try {
@@ -1173,19 +385,17 @@ async function cmdUninstall(opts) {
1173
385
  detectPlatform();
1174
386
  if (opts.purge) {
1175
387
  console.log(
1176
- chalk4.yellow(
388
+ chalk3.yellow(
1177
389
  "This will remove all containers, images, volumes, and configuration."
1178
390
  )
1179
391
  );
1180
392
  console.log(
1181
- chalk4.yellow(
393
+ chalk3.yellow(
1182
394
  "All agent data (sessions, memory) will be permanently deleted."
1183
395
  )
1184
396
  );
1185
397
  } else {
1186
- console.log(
1187
- chalk4.yellow("This will stop containers and remove images.")
1188
- );
398
+ console.log(chalk3.yellow("This will stop containers and remove images."));
1189
399
  console.log(
1190
400
  dim(
1191
401
  `Configuration in ${IXORA_DIR} will be preserved. Run 'ixora start' to re-pull and restart.`
@@ -1200,7 +410,7 @@ async function cmdUninstall(opts) {
1200
410
  info("Cancelled");
1201
411
  return;
1202
412
  }
1203
- if (existsSync5(COMPOSE_FILE)) {
413
+ if (existsSync2(COMPOSE_FILE)) {
1204
414
  info("Stopping services and removing images...");
1205
415
  try {
1206
416
  if (opts.purge) {
@@ -1223,8 +433,8 @@ async function cmdUninstall(opts) {
1223
433
  ` Run ${bold("ixora uninstall --purge")} to remove everything.`
1224
434
  );
1225
435
  }
1226
- const binPath = join2(homedir2(), ".local", "bin", "ixora");
1227
- if (existsSync5(binPath)) {
436
+ const binPath = join(homedir(), ".local", "bin", "ixora");
437
+ if (existsSync2(binPath)) {
1228
438
  console.log(
1229
439
  ` The ${bold("ixora")} command is still available at ${dim(binPath)}`
1230
440
  );
@@ -1233,19 +443,15 @@ async function cmdUninstall(opts) {
1233
443
  }
1234
444
 
1235
445
  // src/commands/install.ts
1236
- import { existsSync as existsSync6 } from "fs";
1237
- import {
1238
- input,
1239
- password,
1240
- select
1241
- } from "@inquirer/prompts";
446
+ import { existsSync as existsSync3 } from "fs";
447
+ import { input, password, select as select2 } from "@inquirer/prompts";
1242
448
  async function promptModelProvider() {
1243
449
  const curAgentModel = envGet("IXORA_AGENT_MODEL");
1244
450
  let defaultProvider = "anthropic";
1245
451
  if (curAgentModel.startsWith("openai:")) defaultProvider = "openai";
1246
452
  else if (curAgentModel.startsWith("google:")) defaultProvider = "google";
1247
453
  else if (curAgentModel.startsWith("ollama:")) defaultProvider = "ollama";
1248
- const provider = await select({
454
+ const provider = await select2({
1249
455
  message: "Select a model provider",
1250
456
  choices: [
1251
457
  {
@@ -1352,7 +558,14 @@ async function promptModelProvider() {
1352
558
  if (!apiKeyValue && curKey) apiKeyValue = curKey;
1353
559
  }
1354
560
  success(`Provider: ${provider} (${agentModel})`);
1355
- return { provider, agentModel, teamModel, apiKeyVar, apiKeyValue, ollamaHost };
561
+ return {
562
+ provider,
563
+ agentModel,
564
+ teamModel,
565
+ apiKeyVar,
566
+ apiKeyValue,
567
+ ollamaHost
568
+ };
1356
569
  }
1357
570
  async function promptIbmiConnection() {
1358
571
  info("IBM i Connection");
@@ -1360,28 +573,43 @@ async function promptIbmiConnection() {
1360
573
  const curHost = envGet("DB2i_HOST");
1361
574
  const curUser = envGet("DB2i_USER");
1362
575
  const curPass = envGet("DB2i_PASS");
576
+ const curPort = envGet("DB2_PORT");
1363
577
  const host = await input({
1364
- message: "IBM i hostname",
578
+ message: "IBM i hostname:",
1365
579
  default: curHost || void 0,
1366
580
  validate: (value) => value.trim() ? true : "IBM i hostname is required"
1367
581
  });
1368
582
  const user = await input({
1369
- message: "IBM i username",
583
+ message: "IBM i username:",
1370
584
  default: curUser || void 0,
1371
585
  validate: (value) => value.trim() ? true : "IBM i username is required"
1372
586
  });
1373
587
  const pass = await password({
1374
- message: "IBM i password",
588
+ message: "IBM i password:",
1375
589
  validate: (value) => {
1376
590
  if (!value && !curPass) return "IBM i password is required";
1377
591
  return true;
1378
592
  }
1379
593
  });
1380
- return { host: host.trim(), user: user.trim(), pass: pass || curPass };
594
+ const port = await input({
595
+ message: "IBM i port:",
596
+ default: curPort || "8076",
597
+ validate: (value) => {
598
+ const n = parseInt(value.trim(), 10);
599
+ if (isNaN(n) || n < 1 || n > 65535) return "Enter a valid port number";
600
+ return true;
601
+ }
602
+ });
603
+ return {
604
+ host: host.trim(),
605
+ user: user.trim(),
606
+ pass: pass || curPass,
607
+ port: port.trim()
608
+ };
1381
609
  }
1382
610
  async function promptProfile() {
1383
611
  const curProfile = envGet("IXORA_PROFILE") || "full";
1384
- const profile = await select({
612
+ const profile = await select2({
1385
613
  message: "Select an agent profile",
1386
614
  choices: VALID_PROFILES.map((p) => ({
1387
615
  name: `${PROFILES[p].name.padEnd(14)} ${dim(PROFILES[p].description)}`,
@@ -1405,12 +633,15 @@ async function cmdInstall(opts) {
1405
633
  detectPlatform();
1406
634
  info(`Using: ${composeCmd}`);
1407
635
  console.log();
1408
- if (existsSync6(IXORA_DIR)) {
636
+ if (existsSync3(IXORA_DIR)) {
1409
637
  warn(`Existing installation found at ${IXORA_DIR}`);
1410
- const action = await select({
638
+ const action = await select2({
1411
639
  message: "What would you like to do?",
1412
640
  choices: [
1413
- { name: "Reconfigure \u2014 re-run setup prompts (overwrites current config)", value: "reconfigure" },
641
+ {
642
+ name: "Reconfigure \u2014 re-run setup prompts (overwrites current config)",
643
+ value: "reconfigure"
644
+ },
1414
645
  { name: "Cancel \u2014 keep existing installation", value: "cancel" }
1415
646
  ],
1416
647
  default: "reconfigure"
@@ -1424,11 +655,38 @@ async function cmdInstall(opts) {
1424
655
  }
1425
656
  const { agentModel, teamModel, apiKeyVar, apiKeyValue, ollamaHost } = await promptModelProvider();
1426
657
  console.log();
1427
- const { host, user, pass } = await promptIbmiConnection();
658
+ const { host, user, pass, port } = await promptIbmiConnection();
659
+ const displayName = await input({
660
+ message: "Display name:",
661
+ default: host
662
+ });
1428
663
  console.log();
1429
664
  const profile = opts.profile ? opts.profile : await promptProfile();
1430
665
  console.log();
1431
- const version = opts.imageVersion ?? envGet("IXORA_VERSION") ?? "latest";
666
+ let version;
667
+ if (opts.imageVersion) {
668
+ version = normalizeVersion(opts.imageVersion);
669
+ } else {
670
+ let tags = [];
671
+ try {
672
+ tags = await fetchImageTags("ibmi-agi/ixora-api");
673
+ } catch {
674
+ warn("Could not fetch available versions from registry");
675
+ }
676
+ if (tags.length > 0) {
677
+ const curVersion = envGet("IXORA_VERSION") || void 0;
678
+ version = await select2({
679
+ message: "Select image version",
680
+ choices: tags.map((t) => ({
681
+ value: t,
682
+ name: t === curVersion ? `${t} (current)` : t
683
+ }))
684
+ });
685
+ } else {
686
+ version = envGet("IXORA_VERSION") || "latest";
687
+ }
688
+ }
689
+ console.log();
1432
690
  const envConfig = {
1433
691
  agentModel,
1434
692
  teamModel,
@@ -1438,11 +696,27 @@ async function cmdInstall(opts) {
1438
696
  db2Host: host,
1439
697
  db2User: user,
1440
698
  db2Pass: pass,
699
+ db2Port: port,
1441
700
  profile,
1442
701
  version
1443
702
  };
1444
703
  writeEnvFile(envConfig);
1445
704
  success("Wrote .env");
705
+ if (systemIdExists("default")) {
706
+ const { removeSystem: removeSystem2 } = await import("./systems-IM6IZCES.js");
707
+ removeSystem2("default");
708
+ }
709
+ addSystem({
710
+ id: "default",
711
+ name: displayName,
712
+ profile,
713
+ agents: [],
714
+ host,
715
+ port,
716
+ user,
717
+ pass
718
+ });
719
+ success("Wrote ixora-systems.yaml");
1446
720
  writeComposeFile();
1447
721
  success("Wrote docker-compose.yml");
1448
722
  if (opts.pull !== false) {
@@ -1450,7 +724,7 @@ async function cmdInstall(opts) {
1450
724
  await runCompose(composeCmd, ["pull"]);
1451
725
  }
1452
726
  info("Starting services...");
1453
- await runCompose(composeCmd, ["up", "-d"]);
727
+ await runCompose(composeCmd, ["up", "-d", "--remove-orphans"]);
1454
728
  await waitForHealthy(composeCmd);
1455
729
  console.log();
1456
730
  success("ixora is running!");
@@ -1467,15 +741,15 @@ async function cmdInstall(opts) {
1467
741
  }
1468
742
 
1469
743
  // src/commands/config.ts
1470
- import { existsSync as existsSync7, readFileSync as readFileSync3 } from "fs";
1471
- import { execa as execa4 } from "execa";
1472
- import chalk5 from "chalk";
744
+ import { existsSync as existsSync4, readFileSync } from "fs";
745
+ import { execa } from "execa";
746
+ import chalk4 from "chalk";
1473
747
  function cmdConfigShow() {
1474
- if (!existsSync7(ENV_FILE)) {
748
+ if (!existsSync4(ENV_FILE)) {
1475
749
  die("ixora is not installed. Run: ixora install");
1476
750
  }
1477
751
  console.log();
1478
- console.log(` ${chalk5.bold("Configuration")} ${ENV_FILE}`);
752
+ console.log(` ${chalk4.bold("Configuration")} ${ENV_FILE}`);
1479
753
  console.log();
1480
754
  section("Model");
1481
755
  const agentModel = envGet("IXORA_AGENT_MODEL") || "anthropic:claude-sonnet-4-6";
@@ -1486,9 +760,12 @@ function cmdConfigShow() {
1486
760
  const ollamaHost = envGet("OLLAMA_HOST");
1487
761
  console.log(` ${cyan("IXORA_AGENT_MODEL")} ${agentModel}`);
1488
762
  console.log(` ${cyan("IXORA_TEAM_MODEL")} ${teamModel}`);
1489
- if (anthKey) console.log(` ${cyan("ANTHROPIC_API_KEY")} ${maskValue(anthKey)}`);
1490
- if (oaiKey) console.log(` ${cyan("OPENAI_API_KEY")} ${maskValue(oaiKey)}`);
1491
- if (googKey) console.log(` ${cyan("GOOGLE_API_KEY")} ${maskValue(googKey)}`);
763
+ if (anthKey)
764
+ console.log(` ${cyan("ANTHROPIC_API_KEY")} ${maskValue(anthKey)}`);
765
+ if (oaiKey)
766
+ console.log(` ${cyan("OPENAI_API_KEY")} ${maskValue(oaiKey)}`);
767
+ if (googKey)
768
+ console.log(` ${cyan("GOOGLE_API_KEY")} ${maskValue(googKey)}`);
1492
769
  if (ollamaHost) console.log(` ${cyan("OLLAMA_HOST")} ${ollamaHost}`);
1493
770
  console.log();
1494
771
  section("IBM i Connection");
@@ -1496,8 +773,12 @@ function cmdConfigShow() {
1496
773
  const db2User = envGet("DB2i_USER");
1497
774
  const db2Pass = envGet("DB2i_PASS");
1498
775
  const db2Port = envGet("DB2_PORT");
1499
- console.log(` ${cyan("DB2i_HOST")} ${db2Host || dim("(not set)")}`);
1500
- console.log(` ${cyan("DB2i_USER")} ${db2User || dim("(not set)")}`);
776
+ console.log(
777
+ ` ${cyan("DB2i_HOST")} ${db2Host || dim("(not set)")}`
778
+ );
779
+ console.log(
780
+ ` ${cyan("DB2i_USER")} ${db2User || dim("(not set)")}`
781
+ );
1501
782
  console.log(` ${cyan("DB2i_PASS")} ${maskValue(db2Pass)}`);
1502
783
  console.log(` ${cyan("DB2_PORT")} ${db2Port || "8076"}`);
1503
784
  console.log();
@@ -1521,7 +802,7 @@ function cmdConfigShow() {
1521
802
  "IXORA_AGENT_MODEL",
1522
803
  "IXORA_TEAM_MODEL"
1523
804
  ]);
1524
- const content = readFileSync3(ENV_FILE, "utf-8");
805
+ const content = readFileSync(ENV_FILE, "utf-8");
1525
806
  const extraLines = content.split("\n").filter((line) => {
1526
807
  const trimmed = line.trim();
1527
808
  if (!trimmed || trimmed.startsWith("#")) return false;
@@ -1546,7 +827,7 @@ function cmdConfigShow() {
1546
827
  console.log();
1547
828
  }
1548
829
  function cmdConfigSet(key, value) {
1549
- if (!existsSync7(ENV_FILE)) {
830
+ if (!existsSync4(ENV_FILE)) {
1550
831
  die("ixora is not installed. Run: ixora install");
1551
832
  }
1552
833
  updateEnvKey(key, value);
@@ -1554,7 +835,7 @@ function cmdConfigSet(key, value) {
1554
835
  console.log(` Restart to apply: ${bold("ixora restart")}`);
1555
836
  }
1556
837
  async function cmdConfigEdit() {
1557
- if (!existsSync7(ENV_FILE)) {
838
+ if (!existsSync4(ENV_FILE)) {
1558
839
  die("ixora is not installed. Run: ixora install");
1559
840
  }
1560
841
  const editor = process.env["EDITOR"] ?? process.env["VISUAL"] ?? "";
@@ -1562,7 +843,7 @@ async function cmdConfigEdit() {
1562
843
  if (!editorCmd) {
1563
844
  for (const candidate of ["vim", "vi", "nano"]) {
1564
845
  try {
1565
- await execa4("which", [candidate]);
846
+ await execa("which", [candidate]);
1566
847
  editorCmd = candidate;
1567
848
  break;
1568
849
  } catch {
@@ -1574,15 +855,15 @@ async function cmdConfigEdit() {
1574
855
  die("No editor found. Set $EDITOR or install vim/nano.");
1575
856
  }
1576
857
  info(`Opening ${editorCmd}...`);
1577
- await execa4(editorCmd, [ENV_FILE], { stdio: "inherit" });
858
+ await execa(editorCmd, [ENV_FILE], { stdio: "inherit" });
1578
859
  console.log();
1579
860
  success("Config saved");
1580
861
  console.log(` Restart to apply: ${bold("ixora restart")}`);
1581
862
  }
1582
863
 
1583
864
  // src/commands/system.ts
1584
- import { input as input2, password as password2, select as select2 } from "@inquirer/prompts";
1585
- import chalk6 from "chalk";
865
+ import { input as input2, password as password2, select as select3, confirm as confirm3 } from "@inquirer/prompts";
866
+ import chalk5 from "chalk";
1586
867
  async function cmdSystemAdd() {
1587
868
  info("Add an IBM i system");
1588
869
  console.log();
@@ -1591,10 +872,7 @@ async function cmdSystemAdd() {
1591
872
  validate: (value) => {
1592
873
  const cleaned = value.toLowerCase().replace(/[^a-z0-9-]/g, "");
1593
874
  if (!cleaned) return "System ID must contain alphanumeric characters";
1594
- if (cleaned === "default")
1595
- return "System ID 'default' is reserved for the primary system";
1596
- if (systemIdExists(cleaned))
1597
- return `System '${cleaned}' already exists`;
875
+ if (systemIdExists(cleaned)) return `System '${cleaned}' already exists`;
1598
876
  return true;
1599
877
  },
1600
878
  transformer: (value) => value.toLowerCase().replace(/[^a-z0-9-]/g, "")
@@ -1605,43 +883,39 @@ async function cmdSystemAdd() {
1605
883
  default: cleanId
1606
884
  });
1607
885
  const host = await input2({
1608
- message: "IBM i hostname",
1609
- validate: (value) => value.trim() ? true : "Hostname is required"
1610
- });
1611
- const port = await input2({
1612
- message: "IBM i Mapepire port",
1613
- default: "8076"
886
+ message: "IBM i hostname:",
887
+ validate: (value) => value.trim() ? true : "IBM i hostname is required"
1614
888
  });
1615
889
  const user = await input2({
1616
- message: "IBM i username",
1617
- validate: (value) => value.trim() ? true : "Username is required"
890
+ message: "IBM i username:",
891
+ validate: (value) => value.trim() ? true : "IBM i username is required"
1618
892
  });
1619
893
  const pass = await password2({
1620
- message: "IBM i password",
894
+ message: "IBM i password:",
1621
895
  validate: (value) => value ? true : "Password is required"
1622
896
  });
1623
- const agentChoice = await select2({
1624
- message: "Select agents for this system",
1625
- choices: [
1626
- {
1627
- name: "All agents (security, operations, knowledge)",
1628
- value: "all"
1629
- },
1630
- { name: "Security + Operations", value: "security-ops" },
1631
- { name: "Security only", value: "security" },
1632
- {
1633
- name: "Operations only (health, database, work mgmt, config)",
1634
- value: "operations"
1635
- },
1636
- { name: "Knowledge only", value: "knowledge" }
1637
- ],
1638
- default: "all"
897
+ const port = await input2({
898
+ message: "IBM i port:",
899
+ default: "8076",
900
+ validate: (value) => {
901
+ const n = parseInt(value.trim(), 10);
902
+ if (isNaN(n) || n < 1 || n > 65535) return "Enter a valid port number";
903
+ return true;
904
+ }
905
+ });
906
+ const profile = await select3({
907
+ message: "Select an agent profile",
908
+ choices: VALID_PROFILES.map((p) => ({
909
+ name: `${PROFILES[p].name.padEnd(14)} ${dim(PROFILES[p].description)}`,
910
+ value: p
911
+ })),
912
+ default: "full"
1639
913
  });
1640
- const agents = AGENT_PRESETS[agentChoice];
1641
914
  addSystem({
1642
915
  id: cleanId,
1643
916
  name,
1644
- agents: [...agents],
917
+ profile,
918
+ agents: [],
1645
919
  host: host.trim(),
1646
920
  port: port.trim(),
1647
921
  user: user.trim(),
@@ -1651,8 +925,18 @@ async function cmdSystemAdd() {
1651
925
  success(`Added system '${cleanId}' (${host.trim()})`);
1652
926
  console.log(` Credentials stored in ${dim(ENV_FILE)}`);
1653
927
  console.log(` Systems: ${systemCount()}`);
1654
- console.log(` Restart to apply: ${bold("ixora restart")}`);
1655
928
  console.log();
929
+ const shouldRestart = await confirm3({
930
+ message: "Restart services now to apply?",
931
+ default: true
932
+ });
933
+ if (shouldRestart) {
934
+ const { cmdRestart: cmdRestart2 } = await import("./restart-FTYGBLP7.js");
935
+ await cmdRestart2({});
936
+ } else {
937
+ console.log(` Restart to apply: ${bold("ixora restart")}`);
938
+ console.log();
939
+ }
1656
940
  }
1657
941
  function cmdSystemRemove(id) {
1658
942
  try {
@@ -1664,31 +948,98 @@ function cmdSystemRemove(id) {
1664
948
  console.log(` Systems: ${systemCount()}`);
1665
949
  console.log(` Restart to apply: ${bold("ixora restart")}`);
1666
950
  }
951
+ function validateSystemId(id) {
952
+ if (!systemIdExists(id)) die(`System '${id}' not found`);
953
+ }
954
+ function systemServices(id) {
955
+ return [`mcp-${id}`, `api-${id}`];
956
+ }
957
+ async function cmdSystemStart(id) {
958
+ try {
959
+ requireInstalled();
960
+ } catch (e) {
961
+ die(e.message);
962
+ }
963
+ validateSystemId(id);
964
+ let composeCmd;
965
+ try {
966
+ composeCmd = await detectComposeCmd();
967
+ await verifyRuntimeRunning(composeCmd);
968
+ } catch (e) {
969
+ die(e.message);
970
+ }
971
+ detectPlatform();
972
+ writeComposeFile();
973
+ const services = systemServices(id);
974
+ info(`Starting system '${id}' (${services.join(", ")})...`);
975
+ await runCompose(composeCmd, ["up", "-d", ...services]);
976
+ await waitForHealthy(composeCmd);
977
+ success(`System '${id}' started`);
978
+ }
979
+ async function cmdSystemStop(id) {
980
+ try {
981
+ requireInstalled();
982
+ } catch (e) {
983
+ die(e.message);
984
+ }
985
+ validateSystemId(id);
986
+ let composeCmd;
987
+ try {
988
+ composeCmd = await detectComposeCmd();
989
+ await verifyRuntimeRunning(composeCmd);
990
+ } catch (e) {
991
+ die(e.message);
992
+ }
993
+ detectPlatform();
994
+ writeComposeFile();
995
+ const services = systemServices(id);
996
+ info(`Stopping system '${id}' (${services.join(", ")})...`);
997
+ await runCompose(composeCmd, ["stop", ...services]);
998
+ success(`System '${id}' stopped`);
999
+ }
1000
+ async function cmdSystemRestart(id) {
1001
+ try {
1002
+ requireInstalled();
1003
+ } catch (e) {
1004
+ die(e.message);
1005
+ }
1006
+ validateSystemId(id);
1007
+ let composeCmd;
1008
+ try {
1009
+ composeCmd = await detectComposeCmd();
1010
+ await verifyRuntimeRunning(composeCmd);
1011
+ } catch (e) {
1012
+ die(e.message);
1013
+ }
1014
+ detectPlatform();
1015
+ writeComposeFile();
1016
+ const services = systemServices(id);
1017
+ info(`Restarting system '${id}' (${services.join(", ")})...`);
1018
+ await runCompose(composeCmd, ["up", "-d", "--force-recreate", ...services]);
1019
+ await waitForHealthy(composeCmd);
1020
+ success(`System '${id}' restarted`);
1021
+ }
1667
1022
  function cmdSystemList() {
1668
- const primaryHost = envGet("DB2i_HOST");
1023
+ const systems = readSystems();
1669
1024
  console.log();
1670
- console.log(` ${chalk6.bold("IBM i Systems")}`);
1025
+ console.log(` ${chalk5.bold("IBM i Systems")}`);
1671
1026
  console.log();
1672
- if (primaryHost) {
1027
+ if (systems.length === 0) {
1673
1028
  console.log(
1674
- ` ${cyan("*")} ${"default".padEnd(12)} ${primaryHost.padEnd(30)} ${dim("(primary \u2014 from install)")}`
1029
+ ` ${dim(`No systems configured. Run: ${bold("ixora install")}`)}`
1675
1030
  );
1676
1031
  }
1677
- const systems = readSystems();
1678
1032
  for (const sys of systems) {
1679
1033
  const idUpper = sys.id.toUpperCase().replace(/-/g, "_");
1680
1034
  const sysHost = envGet(`SYSTEM_${idUpper}_HOST`) || dim("(no host)");
1681
- const agentsStr = sys.agents.join(", ");
1035
+ const marker = sys.id === "default" ? cyan("*") : cyan(" ");
1036
+ const profile = sys.profile || "full";
1682
1037
  console.log(
1683
- ` ${cyan(" ")} ${sys.id.padEnd(12)} ${String(sysHost).padEnd(30)} ${agentsStr}`
1038
+ ` ${marker} ${sys.id.padEnd(12)} ${String(sysHost).padEnd(30)} ${dim(profile)}`
1684
1039
  );
1685
1040
  }
1686
- if (!primaryHost && systems.length === 0) {
1687
- console.log(` ${dim(`No systems configured. Run: ${bold("ixora install")}`)}`);
1688
- }
1689
1041
  console.log();
1690
- const total = systems.length + (primaryHost ? 1 : 0);
1691
- if (total > 1) {
1042
+ if (systems.length > 1) {
1692
1043
  console.log(
1693
1044
  ` ${dim("Multi-system mode: each system runs on its own port (8000, 8001, ...)")}`
1694
1045
  );
@@ -1704,10 +1055,7 @@ function createProgram() {
1704
1055
  const program2 = new Command().name("ixora").description("Manage ixora AI agent deployments on IBM i").version(SCRIPT_VERSION, "-V, --cli-version", "Show CLI version number").option(
1705
1056
  "--profile <name>",
1706
1057
  "Agent profile (full|sql-services|security|knowledge)"
1707
- ).option("--image-version <tag>", "Pin image version (e.g., v1.2.0)").option("--no-pull", "Skip pulling images").option("--purge", "Remove volumes too (with uninstall)").option(
1708
- "--runtime <name>",
1709
- "Force container runtime (docker or podman)"
1710
- );
1058
+ ).option("--image-version <tag>", "Pin image version (e.g., v1.2.0)").option("--no-pull", "Skip pulling images").option("--purge", "Remove volumes too (with uninstall)").option("--runtime <name>", "Force container runtime (docker or podman)");
1711
1059
  program2.command("install").description("First-time setup (interactive)").action(async () => {
1712
1060
  const opts = program2.opts();
1713
1061
  await cmdInstall(opts);
@@ -1728,9 +1076,9 @@ function createProgram() {
1728
1076
  const opts = program2.opts();
1729
1077
  await cmdStatus(opts);
1730
1078
  });
1731
- program2.command("upgrade").description("Pull latest images and restart").action(async () => {
1079
+ program2.command("upgrade").description("Pull latest images and restart").argument("[version]", "Target version (e.g., 0.0.11 or v0.0.11)").action(async (version) => {
1732
1080
  const opts = program2.opts();
1733
- await cmdUpgrade(opts);
1081
+ await cmdUpgrade({ ...opts, version });
1734
1082
  });
1735
1083
  program2.command("uninstall").description("Stop services and remove images").action(async () => {
1736
1084
  const opts = program2.opts();
@@ -1764,6 +1112,15 @@ function createProgram() {
1764
1112
  systemCmd.command("list", { isDefault: true }).description("List configured systems").action(() => {
1765
1113
  cmdSystemList();
1766
1114
  });
1115
+ systemCmd.command("start").argument("<id>", "System ID (from ixora system)").description("Start a specific system's services").action(async (id) => {
1116
+ await cmdSystemStart(id);
1117
+ });
1118
+ systemCmd.command("stop").argument("<id>", "System ID (from ixora system)").description("Stop a specific system's services").action(async (id) => {
1119
+ await cmdSystemStop(id);
1120
+ });
1121
+ systemCmd.command("restart").argument("<id>", "System ID (from ixora system)").description("Restart a specific system's services").action(async (id) => {
1122
+ await cmdSystemRestart(id);
1123
+ });
1767
1124
  return program2;
1768
1125
  }
1769
1126