@ibm/ixora 0.1.1 → 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,801 +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.1";
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
- DB2_PORT='${sqEscape(config.db2Port)}'
175
-
176
- # Deployment
177
- IXORA_PROFILE='${sqEscape(config.profile)}'
178
- IXORA_VERSION='${sqEscape(config.version)}'
179
- `;
180
- if (extra) {
181
- content += `
182
- # Preserved user settings
183
- ${extra}
184
- `;
185
- }
186
- writeFileSync(envFile, content, "utf-8");
187
- chmodSync(envFile, 384);
188
- }
189
- function updateEnvKey(key, value, envFile = ENV_FILE) {
190
- const escaped = sqEscape(value);
191
- if (existsSync(envFile)) {
192
- const content = readFileSync(envFile, "utf-8");
193
- const lines = content.split("\n");
194
- let found = false;
195
- const updated = lines.map((line) => {
196
- if (line.startsWith(`${key}=`)) {
197
- found = true;
198
- return `${key}='${escaped}'`;
199
- }
200
- return line;
201
- });
202
- if (found) {
203
- writeFileSync(envFile, updated.join("\n"), "utf-8");
204
- } else {
205
- const existing = readFileSync(envFile, "utf-8");
206
- const suffix = existing.endsWith("\n") ? "" : "\n";
207
- writeFileSync(
208
- envFile,
209
- `${existing}${suffix}${key}='${escaped}'
210
- `,
211
- "utf-8"
212
- );
213
- }
214
- } else {
215
- mkdirSync(dirname(envFile), { recursive: true });
216
- writeFileSync(envFile, `${key}='${escaped}'
217
- `, "utf-8");
218
- }
219
- chmodSync(envFile, 384);
220
- }
221
-
222
- // src/lib/compose.ts
223
- import { execa as execa2 } from "execa";
224
- import { existsSync as existsSync3, mkdirSync as mkdirSync3 } from "fs";
225
- import { writeFileSync as writeFileSync3 } from "fs";
226
-
227
- // src/lib/platform.ts
228
- import { arch } from "os";
229
- import { execa } from "execa";
230
- async function detectComposeCmd(optRuntime) {
231
- if (optRuntime) {
232
- switch (optRuntime) {
233
- case "docker":
234
- return "docker compose";
235
- case "podman":
236
- return "podman compose";
237
- default:
238
- throw new Error(
239
- `Unknown runtime: ${optRuntime} (choose: docker, podman)`
240
- );
241
- }
242
- }
243
- try {
244
- await execa("docker", ["compose", "version"]);
245
- return "docker compose";
246
- } catch {
247
- }
248
- try {
249
- await execa("podman", ["compose", "version"]);
250
- return "podman compose";
251
- } catch {
252
- }
253
- try {
254
- await execa("docker-compose", ["version"]);
255
- return "docker-compose";
256
- } catch {
257
- }
258
- throw new Error(
259
- "Neither 'docker compose', 'podman compose', nor 'docker-compose' found.\nPlease install Docker or Podman first."
260
- );
261
- }
262
- async function verifyRuntimeRunning(composeCmd) {
263
- const runtime = composeCmd.startsWith("docker") ? "docker" : "podman";
264
- try {
265
- await execa(runtime, ["info"]);
266
- } catch {
267
- const name = runtime === "docker" ? "Docker Desktop" : "Podman";
268
- throw new Error(
269
- `${name} is not running. Please start it and try again.`
270
- );
271
- }
272
- }
273
- function detectPlatform() {
274
- const cpuArch = arch();
275
- if (cpuArch === "ppc64") {
276
- return {
277
- dbImage: process.env["IXORA_DB_IMAGE"] ?? `ghcr.io/ibmi-agi/ixora-db:${process.env["IXORA_VERSION"] ?? "latest"}`
278
- };
279
- }
280
- return {};
281
- }
282
- function getComposeParts(cmd) {
283
- if (cmd === "docker-compose") {
284
- return ["docker-compose", []];
285
- }
286
- const [bin, sub] = cmd.split(" ");
287
- return [bin, [sub]];
288
- }
289
- function getRuntimeBin(cmd) {
290
- return cmd.startsWith("docker") ? "docker" : "podman";
291
- }
292
-
293
- // src/lib/systems.ts
294
- import {
295
- readFileSync as readFileSync2,
296
- writeFileSync as writeFileSync2,
297
- existsSync as existsSync2,
298
- mkdirSync as mkdirSync2,
299
- chmodSync as chmodSync2
300
- } from "fs";
301
- import { dirname as dirname2 } from "path";
302
- function readSystems(configFile = SYSTEMS_CONFIG) {
303
- if (!existsSync2(configFile)) return [];
304
- const content = readFileSync2(configFile, "utf-8");
305
- const systems = [];
306
- let current = null;
307
- for (const line of content.split("\n")) {
308
- const idMatch = line.match(/^ {2}- id: (.+)$/);
309
- if (idMatch) {
310
- if (current?.id) {
311
- systems.push({
312
- id: current.id,
313
- name: current.name ?? current.id,
314
- agents: current.agents ?? []
315
- });
316
- }
317
- current = { id: idMatch[1] };
318
- continue;
319
- }
320
- if (!current) continue;
321
- const nameMatch = line.match(/name: *'?([^']*)'?/);
322
- if (nameMatch) {
323
- current.name = nameMatch[1];
324
- continue;
325
- }
326
- const agentsMatch = line.match(/agents: *\[([^\]]*)\]/);
327
- if (agentsMatch) {
328
- current.agents = agentsMatch[1].split(",").map((a) => a.trim()).filter(Boolean);
329
- }
330
- }
331
- if (current?.id) {
332
- systems.push({
333
- id: current.id,
334
- name: current.name ?? current.id,
335
- agents: current.agents ?? []
336
- });
337
- }
338
- return systems;
339
- }
340
- function systemCount(configFile = SYSTEMS_CONFIG) {
341
- return readSystems(configFile).length;
342
- }
343
- function systemIdExists(id, configFile = SYSTEMS_CONFIG) {
344
- return readSystems(configFile).some((s) => s.id === id);
345
- }
346
- function totalSystemCount(envFile = ENV_FILE, configFile = SYSTEMS_CONFIG) {
347
- const additional = systemCount(configFile);
348
- const primaryHost = envGet("DB2i_HOST", envFile);
349
- return primaryHost ? additional + 1 : additional;
350
- }
351
- function addSystem(system, envFile = ENV_FILE, configFile = SYSTEMS_CONFIG) {
352
- const idUpper = system.id.toUpperCase().replace(/-/g, "_");
353
- updateEnvKey(`SYSTEM_${idUpper}_HOST`, system.host, envFile);
354
- updateEnvKey(`SYSTEM_${idUpper}_PORT`, system.port, envFile);
355
- updateEnvKey(`SYSTEM_${idUpper}_USER`, system.user, envFile);
356
- updateEnvKey(`SYSTEM_${idUpper}_PASS`, system.pass, envFile);
357
- const escapedName = system.name.replace(/'/g, "'\\''");
358
- const agentsList = system.agents.join(", ");
359
- const entry = ` - id: ${system.id}
360
- name: '${escapedName}'
361
- agents: [${agentsList}]
362
- `;
363
- mkdirSync2(dirname2(configFile), { recursive: true });
364
- if (!existsSync2(configFile) || systemCount(configFile) === 0) {
365
- const content = `# yaml-language-server: $schema=
366
- # Ixora Systems Configuration
367
- # Manage with: ixora system add|remove|list
368
- # Credentials stored in .env (SYSTEM_<ID>_USER, SYSTEM_<ID>_PASS)
369
- systems:
370
- ${entry}`;
371
- writeFileSync2(configFile, content, "utf-8");
372
- } else {
373
- const existing = readFileSync2(configFile, "utf-8");
374
- writeFileSync2(configFile, `${existing}${entry}`, "utf-8");
375
- }
376
- chmodSync2(configFile, 384);
377
- }
378
- function removeSystem(id, envFile = ENV_FILE, configFile = SYSTEMS_CONFIG) {
379
- if (!existsSync2(configFile)) {
380
- throw new Error("No systems configured");
381
- }
382
- if (!systemIdExists(id, configFile)) {
383
- throw new Error(`System '${id}' not found`);
384
- }
385
- const content = readFileSync2(configFile, "utf-8");
386
- const lines = content.split("\n");
387
- const output = [];
388
- let skip = false;
389
- for (const line of lines) {
390
- if (line === ` - id: ${id}`) {
391
- skip = true;
392
- continue;
393
- }
394
- if (line.match(/^ {2}- id: /)) {
395
- skip = false;
396
- }
397
- if (!skip) {
398
- output.push(line);
399
- }
400
- }
401
- writeFileSync2(configFile, output.join("\n"), "utf-8");
402
- chmodSync2(configFile, 384);
403
- if (existsSync2(envFile)) {
404
- const idUpper = id.toUpperCase().replace(/-/g, "_");
405
- const envContent = readFileSync2(envFile, "utf-8");
406
- const filtered = envContent.split("\n").filter((line) => !line.startsWith(`SYSTEM_${idUpper}_`)).join("\n");
407
- writeFileSync2(envFile, filtered, "utf-8");
408
- chmodSync2(envFile, 384);
409
- }
410
- }
411
-
412
- // src/lib/templates/single-compose.ts
413
- function generateSingleCompose() {
414
- return `services:
415
- agentos-db:
416
- image: \${IXORA_DB_IMAGE:-agnohq/pgvector:18}
417
- restart: unless-stopped
418
- ports:
419
- - "\${DB_PORT:-5432}:5432"
420
- environment:
421
- POSTGRES_USER: \${DB_USER:-ai}
422
- POSTGRES_PASSWORD: \${DB_PASS:-ai}
423
- POSTGRES_DB: \${DB_DATABASE:-ai}
424
- volumes:
425
- - pgdata:/var/lib/postgresql
426
- healthcheck:
427
- test: ["CMD-SHELL", "pg_isready -U \${DB_USER:-ai}"]
428
- interval: 5s
429
- timeout: 5s
430
- retries: 5
431
-
432
- ibmi-mcp-server:
433
- image: ghcr.io/ibmi-agi/ixora-mcp-server:\${IXORA_VERSION:-latest}
434
- restart: unless-stopped
435
- ports:
436
- - "3010:3010"
437
- environment:
438
- DB2i_HOST: \${DB2i_HOST}
439
- DB2i_USER: \${DB2i_USER}
440
- DB2i_PASS: \${DB2i_PASS}
441
- DB2_PORT: \${DB2_PORT:-8076}
442
- MCP_TRANSPORT_TYPE: http
443
- MCP_SESSION_MODE: stateless
444
- YAML_AUTO_RELOAD: "true"
445
- TOOLS_YAML_PATH: /usr/src/app/tools
446
- YAML_ALLOW_DUPLICATE_SOURCES: "true"
447
- IBMI_ENABLE_EXECUTE_SQL: "true"
448
- IBMI_ENABLE_DEFAULT_TOOLS: "true"
449
- MCP_AUTH_MODE: "none"
450
- IBMI_HTTP_AUTH_ENABLED: "false"
451
- MCP_RATE_LIMIT_ENABLED: "true"
452
- MCP_RATE_LIMIT_MAX_REQUESTS: "5000"
453
- MCP_RATE_LIMIT_WINDOW_MS: "60000"
454
- MCP_RATE_LIMIT_SKIP_DEV: "true"
455
- MCP_POOL_QUERY_TIMEOUT_MS: "120000"
456
- healthcheck:
457
- test: ["CMD-SHELL", "node -e \\"fetch('http://localhost:3010/healthz').then(function(r){process.exit(r.ok?0:1)}).catch(function(){process.exit(1)})\\""]
458
- interval: 5s
459
- timeout: 5s
460
- retries: 5
461
- start_period: 3s
462
-
463
- api:
464
- image: ghcr.io/ibmi-agi/ixora-api:\${IXORA_VERSION:-latest}
465
- command: uvicorn app.main:app --host 0.0.0.0 --port 8000
466
- restart: unless-stopped
467
- ports:
468
- - "8000:8000"
469
- environment:
470
- ANTHROPIC_API_KEY: \${ANTHROPIC_API_KEY:-}
471
- OPENAI_API_KEY: \${OPENAI_API_KEY:-}
472
- GOOGLE_API_KEY: \${GOOGLE_API_KEY:-}
473
- OLLAMA_HOST: \${OLLAMA_HOST:-http://host.docker.internal:11434}
474
- IXORA_AGENT_MODEL: \${IXORA_AGENT_MODEL:-anthropic:claude-sonnet-4-6}
475
- IXORA_TEAM_MODEL: \${IXORA_TEAM_MODEL:-anthropic:claude-haiku-4-5}
476
- DB_HOST: agentos-db
477
- DB_PORT: "5432"
478
- DB_USER: \${DB_USER:-ai}
479
- DB_PASS: \${DB_PASS:-ai}
480
- DB_DATABASE: \${DB_DATABASE:-ai}
481
- MCP_URL: http://ibmi-mcp-server:3010/mcp
482
- IAASSIST_DEPLOYMENT_CONFIG: app/config/deployments/\${IXORA_PROFILE:-full}.yaml
483
- DATA_DIR: /data
484
- RUNTIME_ENV: docker
485
- WAIT_FOR_DB: "True"
486
- RAG_API_URL: \${RAG_API_URL:-}
487
- RAG_API_TIMEOUT: \${RAG_API_TIMEOUT:-120}
488
- AUTH_ENABLED: "false"
489
- MCP_AUTH_MODE: "none"
490
- CORS_ORIGINS: \${CORS_ORIGINS:-*}
491
- IXORA_ENABLE_BUILDER: "true"
492
- DB2i_HOST: \${DB2i_HOST}
493
- DB2i_USER: \${DB2i_USER}
494
- DB2i_PASS: \${DB2i_PASS}
495
- DB2_PORT: \${DB2_PORT:-8076}
496
- volumes:
497
- - agentos-data:/data
498
- - type: bind
499
- source: \${HOME}/.ixora/user_tools
500
- target: /data/user_tools
501
- bind:
502
- create_host_path: true
503
- depends_on:
504
- agentos-db:
505
- condition: service_healthy
506
- ibmi-mcp-server:
507
- condition: service_healthy
508
- healthcheck:
509
- 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)\\""]
510
- interval: 10s
511
- timeout: 5s
512
- retries: 6
513
- start_period: 30s
514
-
515
- ui:
516
- image: ghcr.io/ibmi-agi/ixora-ui:\${IXORA_VERSION:-latest}
517
- restart: unless-stopped
518
- ports:
519
- - "3000:3000"
520
- environment:
521
- NEXT_PUBLIC_API_URL: http://localhost:8000
522
- depends_on:
523
- api:
524
- condition: service_healthy
525
-
526
- volumes:
527
- pgdata:
528
- agentos-data:
529
- `;
530
- }
531
-
532
- // src/lib/templates/multi-compose.ts
533
- function generateMultiCompose(envFile = ENV_FILE, configFile = SYSTEMS_CONFIG) {
534
- const version = envGet("IXORA_VERSION", envFile) || "latest";
535
- const dbImage = process.env["IXORA_DB_IMAGE"] ?? "agnohq/pgvector:18";
536
- let content = `# Auto-generated for multi-system deployment
537
- # Regenerated on every start. Edit ixora-systems.yaml instead.
538
- services:
539
- agentos-db:
540
- image: ${dbImage}
541
- restart: unless-stopped
542
- ports:
543
- - "\${DB_PORT:-5432}:5432"
544
- environment:
545
- POSTGRES_USER: \${DB_USER:-ai}
546
- POSTGRES_PASSWORD: \${DB_PASS:-ai}
547
- POSTGRES_DB: \${DB_DATABASE:-ai}
548
- volumes:
549
- - pgdata:/var/lib/postgresql
550
- healthcheck:
551
- test: ["CMD-SHELL", "pg_isready -U \${DB_USER:-ai}"]
552
- interval: 5s
553
- timeout: 5s
554
- retries: 5
555
-
556
- `;
557
- let apiPort = 8e3;
558
- let firstApi = "";
559
- const allSystems = [];
560
- const primaryHost = envGet("DB2i_HOST", envFile);
561
- if (primaryHost) {
562
- updateEnvKey("SYSTEM_DEFAULT_HOST", primaryHost, envFile);
563
- updateEnvKey(
564
- "SYSTEM_DEFAULT_PORT",
565
- envGet("DB2_PORT", envFile) || "8076",
566
- envFile
567
- );
568
- updateEnvKey("SYSTEM_DEFAULT_USER", envGet("DB2i_USER", envFile), envFile);
569
- updateEnvKey("SYSTEM_DEFAULT_PASS", envGet("DB2i_PASS", envFile), envFile);
570
- allSystems.push({ id: "default", name: primaryHost });
571
- }
572
- const additionalSystems = readSystems(configFile);
573
- for (const sys of additionalSystems) {
574
- allSystems.push({ id: sys.id, name: sys.name });
575
- }
576
- for (const sys of allSystems) {
577
- const idUpper = sys.id.toUpperCase().replace(/-/g, "_");
578
- content += ` mcp-${sys.id}:
579
- image: ghcr.io/ibmi-agi/ixora-mcp-server:\${IXORA_VERSION:-${version}}
580
- restart: unless-stopped
581
- environment:
582
- DB2i_HOST: \${SYSTEM_${idUpper}_HOST}
583
- DB2i_USER: \${SYSTEM_${idUpper}_USER}
584
- DB2i_PASS: \${SYSTEM_${idUpper}_PASS}
585
- DB2_PORT: \${SYSTEM_${idUpper}_PORT:-8076}
586
- MCP_TRANSPORT_TYPE: http
587
- MCP_SESSION_MODE: stateless
588
- YAML_AUTO_RELOAD: "true"
589
- TOOLS_YAML_PATH: /usr/src/app/tools
590
- YAML_ALLOW_DUPLICATE_SOURCES: "true"
591
- IBMI_ENABLE_EXECUTE_SQL: "true"
592
- IBMI_ENABLE_DEFAULT_TOOLS: "true"
593
- MCP_AUTH_MODE: "none"
594
- IBMI_HTTP_AUTH_ENABLED: "false"
595
- MCP_POOL_QUERY_TIMEOUT_MS: "120000"
596
- healthcheck:
597
- test: ["CMD-SHELL", "node -e \\"fetch('http://localhost:3010/healthz').then(function(r){process.exit(r.ok?0:1)}).catch(function(){process.exit(1)})\\""]
598
- interval: 5s
599
- timeout: 5s
600
- retries: 5
601
- start_period: 3s
602
-
603
- `;
604
- content += ` api-${sys.id}:
605
- image: ghcr.io/ibmi-agi/ixora-api:\${IXORA_VERSION:-${version}}
606
- command: uvicorn app.main:app --host 0.0.0.0 --port 8000
607
- restart: unless-stopped
608
- ports:
609
- - "${apiPort}:8000"
610
- environment:
611
- ANTHROPIC_API_KEY: \${ANTHROPIC_API_KEY:-}
612
- OPENAI_API_KEY: \${OPENAI_API_KEY:-}
613
- GOOGLE_API_KEY: \${GOOGLE_API_KEY:-}
614
- OLLAMA_HOST: \${OLLAMA_HOST:-http://host.docker.internal:11434}
615
- IXORA_AGENT_MODEL: \${IXORA_AGENT_MODEL:-anthropic:claude-sonnet-4-6}
616
- IXORA_TEAM_MODEL: \${IXORA_TEAM_MODEL:-anthropic:claude-haiku-4-5}
617
- DB_HOST: agentos-db
618
- DB_PORT: "5432"
619
- DB_USER: \${DB_USER:-ai}
620
- DB_PASS: \${DB_PASS:-ai}
621
- DB_DATABASE: \${DB_DATABASE:-ai}
622
- MCP_URL: http://mcp-${sys.id}:3010/mcp
623
- IXORA_SYSTEM_ID: ${sys.id}
624
- IXORA_SYSTEM_NAME: ${sys.name}
625
- IAASSIST_DEPLOYMENT_CONFIG: app/config/deployments/\${IXORA_PROFILE:-full}.yaml
626
- DATA_DIR: /data
627
- RUNTIME_ENV: docker
628
- WAIT_FOR_DB: "True"
629
- CORS_ORIGINS: \${CORS_ORIGINS:-*}
630
- AUTH_ENABLED: "false"
631
- MCP_AUTH_MODE: "none"
632
- IXORA_ENABLE_BUILDER: "true"
633
- DB2i_HOST: \${SYSTEM_${idUpper}_HOST}
634
- DB2i_USER: \${SYSTEM_${idUpper}_USER}
635
- DB2i_PASS: \${SYSTEM_${idUpper}_PASS}
636
- DB2_PORT: \${SYSTEM_${idUpper}_PORT:-8076}
637
- volumes:
638
- - agentos-data:/data
639
- - type: bind
640
- source: \${HOME}/.ixora/user_tools
641
- target: /data/user_tools
642
- bind:
643
- create_host_path: true
644
- depends_on:
645
- agentos-db:
646
- condition: service_healthy
647
- mcp-${sys.id}:
648
- condition: service_healthy
649
- healthcheck:
650
- 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)\\""]
651
- interval: 10s
652
- timeout: 5s
653
- retries: 6
654
- start_period: 30s
655
-
656
- `;
657
- if (!firstApi) firstApi = `api-${sys.id}`;
658
- apiPort++;
659
- }
660
- content += ` ui:
661
- image: ghcr.io/ibmi-agi/ixora-ui:\${IXORA_VERSION:-${version}}
662
- restart: unless-stopped
663
- ports:
664
- - "3000:3000"
665
- environment:
666
- NEXT_PUBLIC_API_URL: http://localhost:8000
667
- depends_on:
668
- ${firstApi}:
669
- condition: service_healthy
670
-
671
- volumes:
672
- pgdata:
673
- agentos-data:
674
- `;
675
- return content;
676
- }
677
-
678
- // src/lib/ui.ts
48
+ import { existsSync } from "fs";
679
49
  import chalk from "chalk";
680
- function info(message) {
681
- console.log(`${chalk.blue("==>")} ${chalk.bold(message)}`);
682
- }
683
- function success(message) {
684
- console.log(`${chalk.green("==>")} ${chalk.bold(message)}`);
685
- }
686
- function warn(message) {
687
- console.log(`${chalk.yellow("Warning:")} ${message}`);
688
- }
689
- function error(message) {
690
- console.error(`${chalk.red("Error:")} ${message}`);
691
- }
692
- function die(message) {
693
- error(message);
694
- process.exit(1);
695
- }
696
- function maskValue(value) {
697
- if (!value) return chalk.dim("(not set)");
698
- if (value.length <= 4) return "****";
699
- return `${value.slice(0, 4)}****`;
700
- }
701
- function isSensitiveKey(key) {
702
- const upper = key.toUpperCase();
703
- return /KEY|TOKEN|PASS|SECRET|ENCRYPT/.test(upper);
704
- }
705
- function dim(message) {
706
- return chalk.dim(message);
707
- }
708
- function bold(message) {
709
- return chalk.bold(message);
710
- }
711
- function cyan(message) {
712
- return chalk.cyan(message);
713
- }
714
- function section(title) {
715
- console.log(
716
- ` ${chalk.dim(`\u2500\u2500 ${title} ${"\u2500".repeat(Math.max(0, 49 - title.length))}`)}`
717
- );
718
- }
719
-
720
- // src/lib/compose.ts
721
- async function runCompose(composeCmd, args, options = {}) {
722
- const [bin, subArgs] = getComposeParts(composeCmd);
723
- const fullArgs = [
724
- ...subArgs,
725
- "-p",
726
- "ixora",
727
- "-f",
728
- COMPOSE_FILE,
729
- "--env-file",
730
- ENV_FILE,
731
- ...args
732
- ];
733
- try {
734
- const result = await execa2(bin, fullArgs, {
735
- stdio: "inherit",
736
- ...options
737
- });
738
- return {
739
- stdout: String(result.stdout ?? ""),
740
- stderr: String(result.stderr ?? ""),
741
- exitCode: result.exitCode ?? 0
742
- };
743
- } catch (err) {
744
- const exitCode = err && typeof err === "object" && "exitCode" in err ? err.exitCode : 1;
745
- error(`Command failed: ${composeCmd} ${args.join(" ")}`);
746
- console.log(` Check ${bold("ixora logs")} for details.`);
747
- process.exit(exitCode);
748
- }
749
- }
750
- async function runComposeCapture(composeCmd, args) {
751
- const [bin, subArgs] = getComposeParts(composeCmd);
752
- const fullArgs = [
753
- ...subArgs,
754
- "-p",
755
- "ixora",
756
- "-f",
757
- COMPOSE_FILE,
758
- "--env-file",
759
- ENV_FILE,
760
- ...args
761
- ];
762
- const result = await execa2(bin, fullArgs, { stdio: "pipe" });
763
- return result.stdout;
764
- }
765
- function writeComposeFile(envFile = ENV_FILE) {
766
- mkdirSync3(IXORA_DIR, { recursive: true });
767
- const total = totalSystemCount(envFile);
768
- let content;
769
- if (total > 1) {
770
- content = generateMultiCompose(envFile);
771
- } else {
772
- content = generateSingleCompose();
773
- }
774
- writeFileSync3(COMPOSE_FILE, content, "utf-8");
775
- }
776
- function requireInstalled() {
777
- if (!existsSync3(ENV_FILE)) {
778
- throw new Error("ixora is not installed. Run: ixora install");
779
- }
780
- }
781
- function requireComposeFile() {
782
- if (!existsSync3(COMPOSE_FILE)) {
783
- throw new Error("ixora is not installed. Run: ixora install");
784
- }
785
- }
786
- function resolveService(input3) {
787
- return input3.replace(/^ixora-/, "").replace(/-\d+$/, "");
788
- }
789
-
790
- // src/commands/version.ts
791
50
  async function cmdVersion(opts) {
792
51
  console.log(`ixora ${SCRIPT_VERSION}`);
793
- if (!existsSync4(ENV_FILE)) return;
52
+ if (!existsSync(ENV_FILE)) return;
794
53
  const version = envGet("IXORA_VERSION") || "latest";
795
54
  const agentModel = envGet("IXORA_AGENT_MODEL") || "anthropic:claude-sonnet-4-6";
796
55
  console.log(` images: ${version}`);
797
56
  console.log(` model: ${agentModel}`);
798
- if (existsSync4(COMPOSE_FILE)) {
57
+ if (existsSync(COMPOSE_FILE)) {
799
58
  try {
800
59
  const composeCmd = await detectComposeCmd(opts?.runtime);
801
60
  const output = await runComposeCapture(composeCmd, [
@@ -807,7 +66,7 @@ async function cmdVersion(opts) {
807
66
  const images = parseComposeImages(output);
808
67
  if (images.length > 0) {
809
68
  console.log();
810
- console.log(` ${chalk2.bold("Running containers:")}`);
69
+ console.log(` ${chalk.bold("Running containers:")}`);
811
70
  for (const img of images) {
812
71
  const tag = img.Tag || "unknown";
813
72
  const id = img.ID ? dim(` (${img.ID.slice(0, 12)})`) : "";
@@ -841,7 +100,7 @@ function parseComposeImages(output) {
841
100
  }
842
101
 
843
102
  // src/commands/status.ts
844
- import chalk3 from "chalk";
103
+ import chalk2 from "chalk";
845
104
  async function cmdStatus(opts) {
846
105
  try {
847
106
  requireComposeFile();
@@ -859,9 +118,9 @@ async function cmdStatus(opts) {
859
118
  const profile = envGet("IXORA_PROFILE") || "full";
860
119
  const version = envGet("IXORA_VERSION") || "latest";
861
120
  console.log();
862
- console.log(` ${chalk3.bold("Profile:")} ${profile}`);
863
- console.log(` ${chalk3.bold("Version:")} ${version}`);
864
- 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}`);
865
124
  console.log();
866
125
  await runCompose(composeCmd, ["ps"]);
867
126
  try {
@@ -874,14 +133,12 @@ async function cmdStatus(opts) {
874
133
  const images = parseComposeImages2(output);
875
134
  if (images.length > 0) {
876
135
  console.log();
877
- console.log(` ${chalk3.bold("Images:")}`);
136
+ console.log(` ${chalk2.bold("Images:")}`);
878
137
  for (const img of images) {
879
138
  const tag = img.Tag || "unknown";
880
139
  const id = img.ID ? ` (${img.ID.slice(0, 12)})` : "";
881
140
  const tagDisplay = tag === "latest" ? `${tag}${dim(id)}` : tag;
882
- console.log(
883
- ` ${dim(`${img.Repository || ""}:`)}${tagDisplay}`
884
- );
141
+ console.log(` ${dim(`${img.Repository || ""}:`)}${tagDisplay}`);
885
142
  }
886
143
  console.log();
887
144
  }
@@ -907,69 +164,6 @@ function parseComposeImages2(output) {
907
164
  }
908
165
  }
909
166
 
910
- // src/lib/health.ts
911
- import { execa as execa3 } from "execa";
912
- import ora from "ora";
913
- async function waitForHealthy(composeCmd, timeout = HEALTH_TIMEOUT) {
914
- const spinner = ora("Waiting for services to become healthy...").start();
915
- const runtime = getRuntimeBin(composeCmd);
916
- let apiContainer = "";
917
- try {
918
- const output = await runComposeCapture(composeCmd, [
919
- "ps",
920
- "--format",
921
- "{{.Name}}"
922
- ]);
923
- const containers = output.split("\n").filter(Boolean);
924
- apiContainer = containers.find((c) => c.includes("ixora-api-")) ?? "";
925
- } catch {
926
- }
927
- if (!apiContainer) {
928
- spinner.stop();
929
- warn("Could not find API container \u2014 skipping health check");
930
- return true;
931
- }
932
- spinner.text = `Waiting for services to become healthy (up to ${timeout}s)...`;
933
- let elapsed = 0;
934
- while (elapsed < timeout) {
935
- try {
936
- const stateResult = await execa3(runtime, [
937
- "inspect",
938
- "--format",
939
- "{{.State.Status}}",
940
- apiContainer
941
- ]);
942
- const state = stateResult.stdout.trim();
943
- if (state === "exited" || state === "dead") {
944
- spinner.fail("API container failed to start");
945
- console.log(`
946
- Run ${bold("ixora logs api")} to investigate.`);
947
- return false;
948
- }
949
- const healthResult = await execa3(runtime, [
950
- "inspect",
951
- "--format",
952
- "{{.State.Health.Status}}",
953
- apiContainer
954
- ]);
955
- const health = healthResult.stdout.trim();
956
- if (health === "healthy") {
957
- spinner.succeed("Services are healthy");
958
- return true;
959
- }
960
- } catch {
961
- }
962
- await new Promise((r) => setTimeout(r, 2e3));
963
- elapsed += 2;
964
- spinner.text = `Waiting for services to become healthy (${elapsed}s/${timeout}s)...`;
965
- }
966
- spinner.warn(
967
- `Services did not become healthy within ${timeout}s \u2014 they may still be starting`
968
- );
969
- console.log(` Run ${bold("ixora logs api")} to investigate.`);
970
- return false;
971
- }
972
-
973
167
  // src/commands/start.ts
974
168
  async function cmdStart(opts) {
975
169
  try {
@@ -997,24 +191,16 @@ async function cmdStart(opts) {
997
191
  writeComposeFile();
998
192
  success("Wrote docker-compose.yml");
999
193
  info("Starting ixora services...");
1000
- await runCompose(composeCmd, ["up", "-d"]);
194
+ await runCompose(composeCmd, ["up", "-d", "--remove-orphans"]);
1001
195
  await waitForHealthy(composeCmd);
1002
- const profile = envGet("IXORA_PROFILE") || "full";
1003
- const total = totalSystemCount();
196
+ const systems = readSystems();
1004
197
  console.log();
1005
198
  success("ixora is running!");
1006
199
  console.log(` ${bold("UI:")} http://localhost:3000`);
1007
200
  console.log(` ${bold("API:")} http://localhost:8000`);
1008
- console.log(` ${bold("Profile:")} ${profile}`);
1009
- if (total > 1) {
1010
- console.log(` ${bold("Systems:")} ${total}`);
201
+ if (systems.length > 1) {
202
+ console.log(` ${bold("Systems:")} ${systems.length}`);
1011
203
  let port = 8e3;
1012
- const primaryHost = envGet("DB2i_HOST");
1013
- if (primaryHost) {
1014
- console.log(` ${dim(`:${port} \u2192 default (${primaryHost})`)}`);
1015
- port++;
1016
- }
1017
- const systems = readSystems();
1018
204
  for (const sys of systems) {
1019
205
  const idUpper = sys.id.toUpperCase().replace(/-/g, "_");
1020
206
  const sysHost = envGet(`SYSTEM_${idUpper}_HOST`);
@@ -1022,8 +208,10 @@ async function cmdStart(opts) {
1022
208
  port++;
1023
209
  }
1024
210
  console.log(
1025
- ` ${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.")}`
1026
212
  );
213
+ } else if (systems.length === 1) {
214
+ console.log(` ${bold("Profile:")} ${systems[0].profile || "full"}`);
1027
215
  }
1028
216
  console.log();
1029
217
  }
@@ -1048,41 +236,6 @@ async function cmdStop(opts) {
1048
236
  success("Services stopped");
1049
237
  }
1050
238
 
1051
- // src/commands/restart.ts
1052
- async function cmdRestart(opts, service) {
1053
- try {
1054
- requireComposeFile();
1055
- } catch (e) {
1056
- die(e.message);
1057
- }
1058
- let composeCmd;
1059
- try {
1060
- composeCmd = await detectComposeCmd(opts.runtime);
1061
- await verifyRuntimeRunning(composeCmd);
1062
- } catch (e) {
1063
- die(e.message);
1064
- }
1065
- detectPlatform();
1066
- if (service) {
1067
- const svc = resolveService(service);
1068
- info(`Restarting ${svc}...`);
1069
- await runCompose(composeCmd, [
1070
- "up",
1071
- "-d",
1072
- "--force-recreate",
1073
- "--no-deps",
1074
- svc
1075
- ]);
1076
- success(`Restarted ${svc}`);
1077
- } else {
1078
- info("Restarting all services...");
1079
- await runCompose(composeCmd, ["up", "-d", "--force-recreate"]);
1080
- await waitForHealthy(composeCmd);
1081
- console.log();
1082
- success("All services restarted");
1083
- }
1084
- }
1085
-
1086
239
  // src/commands/logs.ts
1087
240
  async function cmdLogs(opts, service) {
1088
241
  try {
@@ -1216,11 +369,11 @@ async function cmdUpgrade(opts) {
1216
369
  }
1217
370
 
1218
371
  // src/commands/uninstall.ts
1219
- import { existsSync as existsSync5, rmSync } from "fs";
372
+ import { existsSync as existsSync2, rmSync } from "fs";
1220
373
  import { confirm } from "@inquirer/prompts";
1221
- import chalk4 from "chalk";
1222
- import { homedir as homedir2 } from "os";
1223
- import { join as join2 } from "path";
374
+ import chalk3 from "chalk";
375
+ import { homedir } from "os";
376
+ import { join } from "path";
1224
377
  async function cmdUninstall(opts) {
1225
378
  let composeCmd;
1226
379
  try {
@@ -1232,19 +385,17 @@ async function cmdUninstall(opts) {
1232
385
  detectPlatform();
1233
386
  if (opts.purge) {
1234
387
  console.log(
1235
- chalk4.yellow(
388
+ chalk3.yellow(
1236
389
  "This will remove all containers, images, volumes, and configuration."
1237
390
  )
1238
391
  );
1239
392
  console.log(
1240
- chalk4.yellow(
393
+ chalk3.yellow(
1241
394
  "All agent data (sessions, memory) will be permanently deleted."
1242
395
  )
1243
396
  );
1244
397
  } else {
1245
- console.log(
1246
- chalk4.yellow("This will stop containers and remove images.")
1247
- );
398
+ console.log(chalk3.yellow("This will stop containers and remove images."));
1248
399
  console.log(
1249
400
  dim(
1250
401
  `Configuration in ${IXORA_DIR} will be preserved. Run 'ixora start' to re-pull and restart.`
@@ -1259,7 +410,7 @@ async function cmdUninstall(opts) {
1259
410
  info("Cancelled");
1260
411
  return;
1261
412
  }
1262
- if (existsSync5(COMPOSE_FILE)) {
413
+ if (existsSync2(COMPOSE_FILE)) {
1263
414
  info("Stopping services and removing images...");
1264
415
  try {
1265
416
  if (opts.purge) {
@@ -1282,8 +433,8 @@ async function cmdUninstall(opts) {
1282
433
  ` Run ${bold("ixora uninstall --purge")} to remove everything.`
1283
434
  );
1284
435
  }
1285
- const binPath = join2(homedir2(), ".local", "bin", "ixora");
1286
- if (existsSync5(binPath)) {
436
+ const binPath = join(homedir(), ".local", "bin", "ixora");
437
+ if (existsSync2(binPath)) {
1287
438
  console.log(
1288
439
  ` The ${bold("ixora")} command is still available at ${dim(binPath)}`
1289
440
  );
@@ -1292,12 +443,8 @@ async function cmdUninstall(opts) {
1292
443
  }
1293
444
 
1294
445
  // src/commands/install.ts
1295
- import { existsSync as existsSync6 } from "fs";
1296
- import {
1297
- input,
1298
- password,
1299
- select as select2
1300
- } from "@inquirer/prompts";
446
+ import { existsSync as existsSync3 } from "fs";
447
+ import { input, password, select as select2 } from "@inquirer/prompts";
1301
448
  async function promptModelProvider() {
1302
449
  const curAgentModel = envGet("IXORA_AGENT_MODEL");
1303
450
  let defaultProvider = "anthropic";
@@ -1411,7 +558,14 @@ async function promptModelProvider() {
1411
558
  if (!apiKeyValue && curKey) apiKeyValue = curKey;
1412
559
  }
1413
560
  success(`Provider: ${provider} (${agentModel})`);
1414
- return { provider, agentModel, teamModel, apiKeyVar, apiKeyValue, ollamaHost };
561
+ return {
562
+ provider,
563
+ agentModel,
564
+ teamModel,
565
+ apiKeyVar,
566
+ apiKeyValue,
567
+ ollamaHost
568
+ };
1415
569
  }
1416
570
  async function promptIbmiConnection() {
1417
571
  info("IBM i Connection");
@@ -1446,7 +600,12 @@ async function promptIbmiConnection() {
1446
600
  return true;
1447
601
  }
1448
602
  });
1449
- return { host: host.trim(), user: user.trim(), pass: pass || curPass, port: port.trim() };
603
+ return {
604
+ host: host.trim(),
605
+ user: user.trim(),
606
+ pass: pass || curPass,
607
+ port: port.trim()
608
+ };
1450
609
  }
1451
610
  async function promptProfile() {
1452
611
  const curProfile = envGet("IXORA_PROFILE") || "full";
@@ -1474,12 +633,15 @@ async function cmdInstall(opts) {
1474
633
  detectPlatform();
1475
634
  info(`Using: ${composeCmd}`);
1476
635
  console.log();
1477
- if (existsSync6(IXORA_DIR)) {
636
+ if (existsSync3(IXORA_DIR)) {
1478
637
  warn(`Existing installation found at ${IXORA_DIR}`);
1479
638
  const action = await select2({
1480
639
  message: "What would you like to do?",
1481
640
  choices: [
1482
- { 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
+ },
1483
645
  { name: "Cancel \u2014 keep existing installation", value: "cancel" }
1484
646
  ],
1485
647
  default: "reconfigure"
@@ -1494,6 +656,10 @@ async function cmdInstall(opts) {
1494
656
  const { agentModel, teamModel, apiKeyVar, apiKeyValue, ollamaHost } = await promptModelProvider();
1495
657
  console.log();
1496
658
  const { host, user, pass, port } = await promptIbmiConnection();
659
+ const displayName = await input({
660
+ message: "Display name:",
661
+ default: host
662
+ });
1497
663
  console.log();
1498
664
  const profile = opts.profile ? opts.profile : await promptProfile();
1499
665
  console.log();
@@ -1536,6 +702,21 @@ async function cmdInstall(opts) {
1536
702
  };
1537
703
  writeEnvFile(envConfig);
1538
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");
1539
720
  writeComposeFile();
1540
721
  success("Wrote docker-compose.yml");
1541
722
  if (opts.pull !== false) {
@@ -1543,7 +724,7 @@ async function cmdInstall(opts) {
1543
724
  await runCompose(composeCmd, ["pull"]);
1544
725
  }
1545
726
  info("Starting services...");
1546
- await runCompose(composeCmd, ["up", "-d"]);
727
+ await runCompose(composeCmd, ["up", "-d", "--remove-orphans"]);
1547
728
  await waitForHealthy(composeCmd);
1548
729
  console.log();
1549
730
  success("ixora is running!");
@@ -1560,15 +741,15 @@ async function cmdInstall(opts) {
1560
741
  }
1561
742
 
1562
743
  // src/commands/config.ts
1563
- import { existsSync as existsSync7, readFileSync as readFileSync3 } from "fs";
1564
- import { execa as execa4 } from "execa";
1565
- import chalk5 from "chalk";
744
+ import { existsSync as existsSync4, readFileSync } from "fs";
745
+ import { execa } from "execa";
746
+ import chalk4 from "chalk";
1566
747
  function cmdConfigShow() {
1567
- if (!existsSync7(ENV_FILE)) {
748
+ if (!existsSync4(ENV_FILE)) {
1568
749
  die("ixora is not installed. Run: ixora install");
1569
750
  }
1570
751
  console.log();
1571
- console.log(` ${chalk5.bold("Configuration")} ${ENV_FILE}`);
752
+ console.log(` ${chalk4.bold("Configuration")} ${ENV_FILE}`);
1572
753
  console.log();
1573
754
  section("Model");
1574
755
  const agentModel = envGet("IXORA_AGENT_MODEL") || "anthropic:claude-sonnet-4-6";
@@ -1579,9 +760,12 @@ function cmdConfigShow() {
1579
760
  const ollamaHost = envGet("OLLAMA_HOST");
1580
761
  console.log(` ${cyan("IXORA_AGENT_MODEL")} ${agentModel}`);
1581
762
  console.log(` ${cyan("IXORA_TEAM_MODEL")} ${teamModel}`);
1582
- if (anthKey) console.log(` ${cyan("ANTHROPIC_API_KEY")} ${maskValue(anthKey)}`);
1583
- if (oaiKey) console.log(` ${cyan("OPENAI_API_KEY")} ${maskValue(oaiKey)}`);
1584
- 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)}`);
1585
769
  if (ollamaHost) console.log(` ${cyan("OLLAMA_HOST")} ${ollamaHost}`);
1586
770
  console.log();
1587
771
  section("IBM i Connection");
@@ -1589,8 +773,12 @@ function cmdConfigShow() {
1589
773
  const db2User = envGet("DB2i_USER");
1590
774
  const db2Pass = envGet("DB2i_PASS");
1591
775
  const db2Port = envGet("DB2_PORT");
1592
- console.log(` ${cyan("DB2i_HOST")} ${db2Host || dim("(not set)")}`);
1593
- 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
+ );
1594
782
  console.log(` ${cyan("DB2i_PASS")} ${maskValue(db2Pass)}`);
1595
783
  console.log(` ${cyan("DB2_PORT")} ${db2Port || "8076"}`);
1596
784
  console.log();
@@ -1614,7 +802,7 @@ function cmdConfigShow() {
1614
802
  "IXORA_AGENT_MODEL",
1615
803
  "IXORA_TEAM_MODEL"
1616
804
  ]);
1617
- const content = readFileSync3(ENV_FILE, "utf-8");
805
+ const content = readFileSync(ENV_FILE, "utf-8");
1618
806
  const extraLines = content.split("\n").filter((line) => {
1619
807
  const trimmed = line.trim();
1620
808
  if (!trimmed || trimmed.startsWith("#")) return false;
@@ -1639,7 +827,7 @@ function cmdConfigShow() {
1639
827
  console.log();
1640
828
  }
1641
829
  function cmdConfigSet(key, value) {
1642
- if (!existsSync7(ENV_FILE)) {
830
+ if (!existsSync4(ENV_FILE)) {
1643
831
  die("ixora is not installed. Run: ixora install");
1644
832
  }
1645
833
  updateEnvKey(key, value);
@@ -1647,7 +835,7 @@ function cmdConfigSet(key, value) {
1647
835
  console.log(` Restart to apply: ${bold("ixora restart")}`);
1648
836
  }
1649
837
  async function cmdConfigEdit() {
1650
- if (!existsSync7(ENV_FILE)) {
838
+ if (!existsSync4(ENV_FILE)) {
1651
839
  die("ixora is not installed. Run: ixora install");
1652
840
  }
1653
841
  const editor = process.env["EDITOR"] ?? process.env["VISUAL"] ?? "";
@@ -1655,7 +843,7 @@ async function cmdConfigEdit() {
1655
843
  if (!editorCmd) {
1656
844
  for (const candidate of ["vim", "vi", "nano"]) {
1657
845
  try {
1658
- await execa4("which", [candidate]);
846
+ await execa("which", [candidate]);
1659
847
  editorCmd = candidate;
1660
848
  break;
1661
849
  } catch {
@@ -1667,15 +855,15 @@ async function cmdConfigEdit() {
1667
855
  die("No editor found. Set $EDITOR or install vim/nano.");
1668
856
  }
1669
857
  info(`Opening ${editorCmd}...`);
1670
- await execa4(editorCmd, [ENV_FILE], { stdio: "inherit" });
858
+ await execa(editorCmd, [ENV_FILE], { stdio: "inherit" });
1671
859
  console.log();
1672
860
  success("Config saved");
1673
861
  console.log(` Restart to apply: ${bold("ixora restart")}`);
1674
862
  }
1675
863
 
1676
864
  // src/commands/system.ts
1677
- import { input as input2, password as password2, select as select3 } from "@inquirer/prompts";
1678
- 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";
1679
867
  async function cmdSystemAdd() {
1680
868
  info("Add an IBM i system");
1681
869
  console.log();
@@ -1684,10 +872,7 @@ async function cmdSystemAdd() {
1684
872
  validate: (value) => {
1685
873
  const cleaned = value.toLowerCase().replace(/[^a-z0-9-]/g, "");
1686
874
  if (!cleaned) return "System ID must contain alphanumeric characters";
1687
- if (cleaned === "default")
1688
- return "System ID 'default' is reserved for the primary system";
1689
- if (systemIdExists(cleaned))
1690
- return `System '${cleaned}' already exists`;
875
+ if (systemIdExists(cleaned)) return `System '${cleaned}' already exists`;
1691
876
  return true;
1692
877
  },
1693
878
  transformer: (value) => value.toLowerCase().replace(/[^a-z0-9-]/g, "")
@@ -1698,43 +883,39 @@ async function cmdSystemAdd() {
1698
883
  default: cleanId
1699
884
  });
1700
885
  const host = await input2({
1701
- message: "IBM i hostname",
1702
- validate: (value) => value.trim() ? true : "Hostname is required"
1703
- });
1704
- const port = await input2({
1705
- message: "IBM i Mapepire port",
1706
- default: "8076"
886
+ message: "IBM i hostname:",
887
+ validate: (value) => value.trim() ? true : "IBM i hostname is required"
1707
888
  });
1708
889
  const user = await input2({
1709
- message: "IBM i username",
1710
- validate: (value) => value.trim() ? true : "Username is required"
890
+ message: "IBM i username:",
891
+ validate: (value) => value.trim() ? true : "IBM i username is required"
1711
892
  });
1712
893
  const pass = await password2({
1713
- message: "IBM i password",
894
+ message: "IBM i password:",
1714
895
  validate: (value) => value ? true : "Password is required"
1715
896
  });
1716
- const agentChoice = await select3({
1717
- message: "Select agents for this system",
1718
- choices: [
1719
- {
1720
- name: "All agents (security, operations, knowledge)",
1721
- value: "all"
1722
- },
1723
- { name: "Security + Operations", value: "security-ops" },
1724
- { name: "Security only", value: "security" },
1725
- {
1726
- name: "Operations only (health, database, work mgmt, config)",
1727
- value: "operations"
1728
- },
1729
- { name: "Knowledge only", value: "knowledge" }
1730
- ],
1731
- 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"
1732
913
  });
1733
- const agents = AGENT_PRESETS[agentChoice];
1734
914
  addSystem({
1735
915
  id: cleanId,
1736
916
  name,
1737
- agents: [...agents],
917
+ profile,
918
+ agents: [],
1738
919
  host: host.trim(),
1739
920
  port: port.trim(),
1740
921
  user: user.trim(),
@@ -1744,8 +925,18 @@ async function cmdSystemAdd() {
1744
925
  success(`Added system '${cleanId}' (${host.trim()})`);
1745
926
  console.log(` Credentials stored in ${dim(ENV_FILE)}`);
1746
927
  console.log(` Systems: ${systemCount()}`);
1747
- console.log(` Restart to apply: ${bold("ixora restart")}`);
1748
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
+ }
1749
940
  }
1750
941
  function cmdSystemRemove(id) {
1751
942
  try {
@@ -1757,31 +948,98 @@ function cmdSystemRemove(id) {
1757
948
  console.log(` Systems: ${systemCount()}`);
1758
949
  console.log(` Restart to apply: ${bold("ixora restart")}`);
1759
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
+ }
1760
1022
  function cmdSystemList() {
1761
- const primaryHost = envGet("DB2i_HOST");
1023
+ const systems = readSystems();
1762
1024
  console.log();
1763
- console.log(` ${chalk6.bold("IBM i Systems")}`);
1025
+ console.log(` ${chalk5.bold("IBM i Systems")}`);
1764
1026
  console.log();
1765
- if (primaryHost) {
1027
+ if (systems.length === 0) {
1766
1028
  console.log(
1767
- ` ${cyan("*")} ${"default".padEnd(12)} ${primaryHost.padEnd(30)} ${dim("(primary \u2014 from install)")}`
1029
+ ` ${dim(`No systems configured. Run: ${bold("ixora install")}`)}`
1768
1030
  );
1769
1031
  }
1770
- const systems = readSystems();
1771
1032
  for (const sys of systems) {
1772
1033
  const idUpper = sys.id.toUpperCase().replace(/-/g, "_");
1773
1034
  const sysHost = envGet(`SYSTEM_${idUpper}_HOST`) || dim("(no host)");
1774
- const agentsStr = sys.agents.join(", ");
1035
+ const marker = sys.id === "default" ? cyan("*") : cyan(" ");
1036
+ const profile = sys.profile || "full";
1775
1037
  console.log(
1776
- ` ${cyan(" ")} ${sys.id.padEnd(12)} ${String(sysHost).padEnd(30)} ${agentsStr}`
1038
+ ` ${marker} ${sys.id.padEnd(12)} ${String(sysHost).padEnd(30)} ${dim(profile)}`
1777
1039
  );
1778
1040
  }
1779
- if (!primaryHost && systems.length === 0) {
1780
- console.log(` ${dim(`No systems configured. Run: ${bold("ixora install")}`)}`);
1781
- }
1782
1041
  console.log();
1783
- const total = systems.length + (primaryHost ? 1 : 0);
1784
- if (total > 1) {
1042
+ if (systems.length > 1) {
1785
1043
  console.log(
1786
1044
  ` ${dim("Multi-system mode: each system runs on its own port (8000, 8001, ...)")}`
1787
1045
  );
@@ -1797,10 +1055,7 @@ function createProgram() {
1797
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(
1798
1056
  "--profile <name>",
1799
1057
  "Agent profile (full|sql-services|security|knowledge)"
1800
- ).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(
1801
- "--runtime <name>",
1802
- "Force container runtime (docker or podman)"
1803
- );
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)");
1804
1059
  program2.command("install").description("First-time setup (interactive)").action(async () => {
1805
1060
  const opts = program2.opts();
1806
1061
  await cmdInstall(opts);
@@ -1857,6 +1112,15 @@ function createProgram() {
1857
1112
  systemCmd.command("list", { isDefault: true }).description("List configured systems").action(() => {
1858
1113
  cmdSystemList();
1859
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
+ });
1860
1124
  return program2;
1861
1125
  }
1862
1126