@ibm/ixora 0.1.0
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/README.md +85 -0
- package/dist/index.js +1779 -0
- package/package.json +50 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1779 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
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
|
+
// 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
|
|
678
|
+
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
|
+
async function cmdVersion(opts) {
|
|
791
|
+
console.log(`ixora ${SCRIPT_VERSION}`);
|
|
792
|
+
if (!existsSync4(ENV_FILE)) return;
|
|
793
|
+
const version = envGet("IXORA_VERSION") || "latest";
|
|
794
|
+
const agentModel = envGet("IXORA_AGENT_MODEL") || "anthropic:claude-sonnet-4-6";
|
|
795
|
+
console.log(` images: ${version}`);
|
|
796
|
+
console.log(` model: ${agentModel}`);
|
|
797
|
+
if (existsSync4(COMPOSE_FILE)) {
|
|
798
|
+
try {
|
|
799
|
+
const composeCmd = await detectComposeCmd(opts?.runtime);
|
|
800
|
+
const output = await runComposeCapture(composeCmd, [
|
|
801
|
+
"images",
|
|
802
|
+
"--format",
|
|
803
|
+
"json"
|
|
804
|
+
]);
|
|
805
|
+
if (output.trim()) {
|
|
806
|
+
const images = parseComposeImages(output);
|
|
807
|
+
if (images.length > 0) {
|
|
808
|
+
console.log();
|
|
809
|
+
console.log(` ${chalk2.bold("Running containers:")}`);
|
|
810
|
+
for (const img of images) {
|
|
811
|
+
const tag = img.Tag || "unknown";
|
|
812
|
+
const id = img.ID ? dim(` (${img.ID.slice(0, 12)})`) : "";
|
|
813
|
+
const imageStr = tag === "latest" ? `${img.Repository || ""}:latest${id}` : `${img.Repository || ""}:${tag}`;
|
|
814
|
+
console.log(
|
|
815
|
+
` ${(img.Service || "").padEnd(22)} ${dim(imageStr)}`
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
} catch {
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
function parseComposeImages(output) {
|
|
825
|
+
const trimmed = output.trim();
|
|
826
|
+
if (!trimmed) return [];
|
|
827
|
+
try {
|
|
828
|
+
const parsed = JSON.parse(trimmed);
|
|
829
|
+
if (Array.isArray(parsed)) return parsed;
|
|
830
|
+
return [parsed];
|
|
831
|
+
} catch {
|
|
832
|
+
return trimmed.split("\n").filter((line) => line.trim()).map((line) => {
|
|
833
|
+
try {
|
|
834
|
+
return JSON.parse(line);
|
|
835
|
+
} catch {
|
|
836
|
+
return null;
|
|
837
|
+
}
|
|
838
|
+
}).filter(Boolean);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// src/commands/status.ts
|
|
843
|
+
import chalk3 from "chalk";
|
|
844
|
+
async function cmdStatus(opts) {
|
|
845
|
+
try {
|
|
846
|
+
requireComposeFile();
|
|
847
|
+
} catch (e) {
|
|
848
|
+
die(e.message);
|
|
849
|
+
}
|
|
850
|
+
let composeCmd;
|
|
851
|
+
try {
|
|
852
|
+
composeCmd = await detectComposeCmd(opts.runtime);
|
|
853
|
+
await verifyRuntimeRunning(composeCmd);
|
|
854
|
+
} catch (e) {
|
|
855
|
+
die(e.message);
|
|
856
|
+
}
|
|
857
|
+
detectPlatform();
|
|
858
|
+
const profile = envGet("IXORA_PROFILE") || "full";
|
|
859
|
+
const version = envGet("IXORA_VERSION") || "latest";
|
|
860
|
+
console.log();
|
|
861
|
+
console.log(` ${chalk3.bold("Profile:")} ${profile}`);
|
|
862
|
+
console.log(` ${chalk3.bold("Version:")} ${version}`);
|
|
863
|
+
console.log(` ${chalk3.bold("Config:")} ${IXORA_DIR}`);
|
|
864
|
+
console.log();
|
|
865
|
+
await runCompose(composeCmd, ["ps"]);
|
|
866
|
+
try {
|
|
867
|
+
const output = await runComposeCapture(composeCmd, [
|
|
868
|
+
"images",
|
|
869
|
+
"--format",
|
|
870
|
+
"json"
|
|
871
|
+
]);
|
|
872
|
+
if (output.trim()) {
|
|
873
|
+
const images = parseComposeImages2(output);
|
|
874
|
+
if (images.length > 0) {
|
|
875
|
+
console.log();
|
|
876
|
+
console.log(` ${chalk3.bold("Images:")}`);
|
|
877
|
+
for (const img of images) {
|
|
878
|
+
const tag = img.Tag || "unknown";
|
|
879
|
+
const id = img.ID ? ` (${img.ID.slice(0, 12)})` : "";
|
|
880
|
+
const tagDisplay = tag === "latest" ? `${tag}${dim(id)}` : tag;
|
|
881
|
+
console.log(
|
|
882
|
+
` ${dim(`${img.Repository || ""}:`)}${tagDisplay}`
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
console.log();
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
} catch {
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
function parseComposeImages2(output) {
|
|
892
|
+
const trimmed = output.trim();
|
|
893
|
+
if (!trimmed) return [];
|
|
894
|
+
try {
|
|
895
|
+
const parsed = JSON.parse(trimmed);
|
|
896
|
+
if (Array.isArray(parsed)) return parsed;
|
|
897
|
+
return [parsed];
|
|
898
|
+
} catch {
|
|
899
|
+
return trimmed.split("\n").filter((line) => line.trim()).map((line) => {
|
|
900
|
+
try {
|
|
901
|
+
return JSON.parse(line);
|
|
902
|
+
} catch {
|
|
903
|
+
return null;
|
|
904
|
+
}
|
|
905
|
+
}).filter(Boolean);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
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
|
+
// src/commands/start.ts
|
|
973
|
+
async function cmdStart(opts) {
|
|
974
|
+
try {
|
|
975
|
+
requireInstalled();
|
|
976
|
+
} catch (e) {
|
|
977
|
+
die(e.message);
|
|
978
|
+
}
|
|
979
|
+
let composeCmd;
|
|
980
|
+
try {
|
|
981
|
+
composeCmd = await detectComposeCmd(opts.runtime);
|
|
982
|
+
await verifyRuntimeRunning(composeCmd);
|
|
983
|
+
} catch (e) {
|
|
984
|
+
die(e.message);
|
|
985
|
+
}
|
|
986
|
+
detectPlatform();
|
|
987
|
+
if (opts.profile) {
|
|
988
|
+
if (!VALID_PROFILES.includes(opts.profile)) {
|
|
989
|
+
die(
|
|
990
|
+
`Invalid profile: ${opts.profile} (choose: ${VALID_PROFILES.join(", ")})`
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
info(`Setting profile: ${opts.profile}`);
|
|
994
|
+
updateEnvKey("IXORA_PROFILE", opts.profile);
|
|
995
|
+
}
|
|
996
|
+
writeComposeFile();
|
|
997
|
+
success("Wrote docker-compose.yml");
|
|
998
|
+
info("Starting ixora services...");
|
|
999
|
+
await runCompose(composeCmd, ["up", "-d"]);
|
|
1000
|
+
await waitForHealthy(composeCmd);
|
|
1001
|
+
const profile = envGet("IXORA_PROFILE") || "full";
|
|
1002
|
+
const total = totalSystemCount();
|
|
1003
|
+
console.log();
|
|
1004
|
+
success("ixora is running!");
|
|
1005
|
+
console.log(` ${bold("UI:")} http://localhost:3000`);
|
|
1006
|
+
console.log(` ${bold("API:")} http://localhost:8000`);
|
|
1007
|
+
console.log(` ${bold("Profile:")} ${profile}`);
|
|
1008
|
+
if (total > 1) {
|
|
1009
|
+
console.log(` ${bold("Systems:")} ${total}`);
|
|
1010
|
+
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
|
+
for (const sys of systems) {
|
|
1018
|
+
const idUpper = sys.id.toUpperCase().replace(/-/g, "_");
|
|
1019
|
+
const sysHost = envGet(`SYSTEM_${idUpper}_HOST`);
|
|
1020
|
+
console.log(` ${dim(`:${port} \u2192 ${sys.id} (${sysHost})`)}`);
|
|
1021
|
+
port++;
|
|
1022
|
+
}
|
|
1023
|
+
console.log(
|
|
1024
|
+
` ${dim("Note: UI connects to primary system (:8000) only. Use API ports for other systems.")}`
|
|
1025
|
+
);
|
|
1026
|
+
}
|
|
1027
|
+
console.log();
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// src/commands/stop.ts
|
|
1031
|
+
async function cmdStop(opts) {
|
|
1032
|
+
try {
|
|
1033
|
+
requireComposeFile();
|
|
1034
|
+
} catch (e) {
|
|
1035
|
+
die(e.message);
|
|
1036
|
+
}
|
|
1037
|
+
let composeCmd;
|
|
1038
|
+
try {
|
|
1039
|
+
composeCmd = await detectComposeCmd(opts.runtime);
|
|
1040
|
+
await verifyRuntimeRunning(composeCmd);
|
|
1041
|
+
} catch (e) {
|
|
1042
|
+
die(e.message);
|
|
1043
|
+
}
|
|
1044
|
+
detectPlatform();
|
|
1045
|
+
info("Stopping ixora services...");
|
|
1046
|
+
await runCompose(composeCmd, ["down"]);
|
|
1047
|
+
success("Services stopped");
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// src/commands/restart.ts
|
|
1051
|
+
async function cmdRestart(opts, service) {
|
|
1052
|
+
try {
|
|
1053
|
+
requireComposeFile();
|
|
1054
|
+
} catch (e) {
|
|
1055
|
+
die(e.message);
|
|
1056
|
+
}
|
|
1057
|
+
let composeCmd;
|
|
1058
|
+
try {
|
|
1059
|
+
composeCmd = await detectComposeCmd(opts.runtime);
|
|
1060
|
+
await verifyRuntimeRunning(composeCmd);
|
|
1061
|
+
} catch (e) {
|
|
1062
|
+
die(e.message);
|
|
1063
|
+
}
|
|
1064
|
+
detectPlatform();
|
|
1065
|
+
if (service) {
|
|
1066
|
+
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}`);
|
|
1076
|
+
} 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");
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// src/commands/logs.ts
|
|
1086
|
+
async function cmdLogs(opts, service) {
|
|
1087
|
+
try {
|
|
1088
|
+
requireComposeFile();
|
|
1089
|
+
} catch (e) {
|
|
1090
|
+
die(e.message);
|
|
1091
|
+
}
|
|
1092
|
+
let composeCmd;
|
|
1093
|
+
try {
|
|
1094
|
+
composeCmd = await detectComposeCmd(opts.runtime);
|
|
1095
|
+
await verifyRuntimeRunning(composeCmd);
|
|
1096
|
+
} catch (e) {
|
|
1097
|
+
die(e.message);
|
|
1098
|
+
}
|
|
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"]);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// src/commands/upgrade.ts
|
|
1109
|
+
async function cmdUpgrade(opts) {
|
|
1110
|
+
try {
|
|
1111
|
+
requireInstalled();
|
|
1112
|
+
} catch (e) {
|
|
1113
|
+
die(e.message);
|
|
1114
|
+
}
|
|
1115
|
+
let composeCmd;
|
|
1116
|
+
try {
|
|
1117
|
+
composeCmd = await detectComposeCmd(opts.runtime);
|
|
1118
|
+
await verifyRuntimeRunning(composeCmd);
|
|
1119
|
+
} catch (e) {
|
|
1120
|
+
die(e.message);
|
|
1121
|
+
}
|
|
1122
|
+
detectPlatform();
|
|
1123
|
+
const previousVersion = envGet("IXORA_VERSION") || "latest";
|
|
1124
|
+
info("Upgrading ixora...");
|
|
1125
|
+
writeComposeFile();
|
|
1126
|
+
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
|
+
if (opts.profile) {
|
|
1132
|
+
if (!VALID_PROFILES.includes(opts.profile)) {
|
|
1133
|
+
die(
|
|
1134
|
+
`Invalid profile: ${opts.profile} (choose: ${VALID_PROFILES.join(", ")})`
|
|
1135
|
+
);
|
|
1136
|
+
}
|
|
1137
|
+
info(`Setting profile: ${opts.profile}`);
|
|
1138
|
+
updateEnvKey("IXORA_PROFILE", opts.profile);
|
|
1139
|
+
}
|
|
1140
|
+
if (opts.pull !== false) {
|
|
1141
|
+
info("Pulling latest images...");
|
|
1142
|
+
await runCompose(composeCmd, ["pull"]);
|
|
1143
|
+
}
|
|
1144
|
+
info("Restarting services...");
|
|
1145
|
+
await runCompose(composeCmd, ["up", "-d"]);
|
|
1146
|
+
await waitForHealthy(composeCmd);
|
|
1147
|
+
const newVersion = envGet("IXORA_VERSION") || "latest";
|
|
1148
|
+
const profile = envGet("IXORA_PROFILE") || "full";
|
|
1149
|
+
console.log();
|
|
1150
|
+
success("Upgrade complete!");
|
|
1151
|
+
console.log(` ${bold("Version:")} ${newVersion}`);
|
|
1152
|
+
console.log(` ${bold("Profile:")} ${profile}`);
|
|
1153
|
+
if (opts.imageVersion && previousVersion !== opts.imageVersion) {
|
|
1154
|
+
console.log(` ${dim(`(was ${previousVersion})`)}`);
|
|
1155
|
+
}
|
|
1156
|
+
console.log();
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// src/commands/uninstall.ts
|
|
1160
|
+
import { existsSync as existsSync5, rmSync } from "fs";
|
|
1161
|
+
import { confirm } from "@inquirer/prompts";
|
|
1162
|
+
import chalk4 from "chalk";
|
|
1163
|
+
import { homedir as homedir2 } from "os";
|
|
1164
|
+
import { join as join2 } from "path";
|
|
1165
|
+
async function cmdUninstall(opts) {
|
|
1166
|
+
let composeCmd;
|
|
1167
|
+
try {
|
|
1168
|
+
composeCmd = await detectComposeCmd(opts.runtime);
|
|
1169
|
+
await verifyRuntimeRunning(composeCmd);
|
|
1170
|
+
} catch (e) {
|
|
1171
|
+
die(e.message);
|
|
1172
|
+
}
|
|
1173
|
+
detectPlatform();
|
|
1174
|
+
if (opts.purge) {
|
|
1175
|
+
console.log(
|
|
1176
|
+
chalk4.yellow(
|
|
1177
|
+
"This will remove all containers, images, volumes, and configuration."
|
|
1178
|
+
)
|
|
1179
|
+
);
|
|
1180
|
+
console.log(
|
|
1181
|
+
chalk4.yellow(
|
|
1182
|
+
"All agent data (sessions, memory) will be permanently deleted."
|
|
1183
|
+
)
|
|
1184
|
+
);
|
|
1185
|
+
} else {
|
|
1186
|
+
console.log(
|
|
1187
|
+
chalk4.yellow("This will stop containers and remove images.")
|
|
1188
|
+
);
|
|
1189
|
+
console.log(
|
|
1190
|
+
dim(
|
|
1191
|
+
`Configuration in ${IXORA_DIR} will be preserved. Run 'ixora start' to re-pull and restart.`
|
|
1192
|
+
)
|
|
1193
|
+
);
|
|
1194
|
+
}
|
|
1195
|
+
const confirmed = await confirm({
|
|
1196
|
+
message: "Continue?",
|
|
1197
|
+
default: false
|
|
1198
|
+
});
|
|
1199
|
+
if (!confirmed) {
|
|
1200
|
+
info("Cancelled");
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
if (existsSync5(COMPOSE_FILE)) {
|
|
1204
|
+
info("Stopping services and removing images...");
|
|
1205
|
+
try {
|
|
1206
|
+
if (opts.purge) {
|
|
1207
|
+
await runCompose(composeCmd, ["down", "--rmi", "all", "-v"]);
|
|
1208
|
+
} else {
|
|
1209
|
+
await runCompose(composeCmd, ["down", "--rmi", "all"]);
|
|
1210
|
+
}
|
|
1211
|
+
} catch {
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
if (opts.purge) {
|
|
1215
|
+
info(`Removing ${IXORA_DIR}...`);
|
|
1216
|
+
rmSync(IXORA_DIR, { recursive: true, force: true });
|
|
1217
|
+
}
|
|
1218
|
+
success("ixora has been uninstalled");
|
|
1219
|
+
if (!opts.purge) {
|
|
1220
|
+
console.log(` Configuration preserved in ${dim(IXORA_DIR)}`);
|
|
1221
|
+
console.log(` Run ${bold("ixora start")} to re-pull images and restart.`);
|
|
1222
|
+
console.log(
|
|
1223
|
+
` Run ${bold("ixora uninstall --purge")} to remove everything.`
|
|
1224
|
+
);
|
|
1225
|
+
}
|
|
1226
|
+
const binPath = join2(homedir2(), ".local", "bin", "ixora");
|
|
1227
|
+
if (existsSync5(binPath)) {
|
|
1228
|
+
console.log(
|
|
1229
|
+
` The ${bold("ixora")} command is still available at ${dim(binPath)}`
|
|
1230
|
+
);
|
|
1231
|
+
console.log(` To remove it: ${bold(`rm ${binPath}`)}`);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// src/commands/install.ts
|
|
1236
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1237
|
+
import {
|
|
1238
|
+
input,
|
|
1239
|
+
password,
|
|
1240
|
+
select
|
|
1241
|
+
} from "@inquirer/prompts";
|
|
1242
|
+
async function promptModelProvider() {
|
|
1243
|
+
const curAgentModel = envGet("IXORA_AGENT_MODEL");
|
|
1244
|
+
let defaultProvider = "anthropic";
|
|
1245
|
+
if (curAgentModel.startsWith("openai:")) defaultProvider = "openai";
|
|
1246
|
+
else if (curAgentModel.startsWith("google:")) defaultProvider = "google";
|
|
1247
|
+
else if (curAgentModel.startsWith("ollama:")) defaultProvider = "ollama";
|
|
1248
|
+
const provider = await select({
|
|
1249
|
+
message: "Select a model provider",
|
|
1250
|
+
choices: [
|
|
1251
|
+
{
|
|
1252
|
+
name: `Anthropic ${dim("Claude Sonnet 4.6 / Haiku 4.5 (recommended)")}`,
|
|
1253
|
+
value: "anthropic"
|
|
1254
|
+
},
|
|
1255
|
+
{
|
|
1256
|
+
name: `OpenAI ${dim("GPT-4o / GPT-4o-mini")}`,
|
|
1257
|
+
value: "openai"
|
|
1258
|
+
},
|
|
1259
|
+
{
|
|
1260
|
+
name: `Google ${dim("Gemini 2.5 Pro / Gemini 2.5 Flash")}`,
|
|
1261
|
+
value: "google"
|
|
1262
|
+
},
|
|
1263
|
+
{
|
|
1264
|
+
name: `Ollama ${dim("Local models via Ollama (no API key needed)")}`,
|
|
1265
|
+
value: "ollama"
|
|
1266
|
+
},
|
|
1267
|
+
{
|
|
1268
|
+
name: `Custom ${dim("Enter provider:model strings")}`,
|
|
1269
|
+
value: "custom"
|
|
1270
|
+
}
|
|
1271
|
+
],
|
|
1272
|
+
default: defaultProvider
|
|
1273
|
+
});
|
|
1274
|
+
const providerDef = PROVIDERS[provider];
|
|
1275
|
+
let agentModel = providerDef.agentModel;
|
|
1276
|
+
let teamModel = providerDef.teamModel;
|
|
1277
|
+
let apiKeyVar = providerDef.apiKeyVar;
|
|
1278
|
+
let apiKeyValue = "";
|
|
1279
|
+
let ollamaHost;
|
|
1280
|
+
if (provider === "ollama") {
|
|
1281
|
+
console.log();
|
|
1282
|
+
info("Ollama Setup");
|
|
1283
|
+
console.log();
|
|
1284
|
+
console.log(
|
|
1285
|
+
` ${dim("Ollama must be running on your machine and listening on all interfaces.")}`
|
|
1286
|
+
);
|
|
1287
|
+
console.log(
|
|
1288
|
+
` ${dim("Default URL works for macOS and Windows (Docker Desktop).")}`
|
|
1289
|
+
);
|
|
1290
|
+
console.log(
|
|
1291
|
+
` ${dim("Linux users: use your host IP (e.g., http://172.17.0.1:11434).")}`
|
|
1292
|
+
);
|
|
1293
|
+
console.log();
|
|
1294
|
+
const curOllamaHost = envGet("OLLAMA_HOST");
|
|
1295
|
+
ollamaHost = await input({
|
|
1296
|
+
message: "Ollama URL",
|
|
1297
|
+
default: curOllamaHost || "http://host.docker.internal:11434"
|
|
1298
|
+
});
|
|
1299
|
+
const curModel = curAgentModel.startsWith("ollama:") ? curAgentModel.slice(7) : "llama3.1";
|
|
1300
|
+
const modelName = await input({
|
|
1301
|
+
message: "Model name",
|
|
1302
|
+
default: curModel
|
|
1303
|
+
});
|
|
1304
|
+
agentModel = `ollama:${modelName}`;
|
|
1305
|
+
teamModel = `ollama:${modelName}`;
|
|
1306
|
+
try {
|
|
1307
|
+
let testUrl = ollamaHost;
|
|
1308
|
+
if (testUrl.includes("host.docker.internal")) {
|
|
1309
|
+
testUrl = testUrl.replace("host.docker.internal", "localhost");
|
|
1310
|
+
}
|
|
1311
|
+
const response = await fetch(`${testUrl}/api/tags`, {
|
|
1312
|
+
signal: AbortSignal.timeout(5e3)
|
|
1313
|
+
});
|
|
1314
|
+
if (response.ok) {
|
|
1315
|
+
success("Ollama is reachable");
|
|
1316
|
+
} else {
|
|
1317
|
+
warn(`Could not reach Ollama at ${testUrl}`);
|
|
1318
|
+
console.log(
|
|
1319
|
+
" Make sure Ollama is running and accessible from Docker containers."
|
|
1320
|
+
);
|
|
1321
|
+
}
|
|
1322
|
+
} catch {
|
|
1323
|
+
warn(`Could not reach Ollama at ${ollamaHost}`);
|
|
1324
|
+
console.log(
|
|
1325
|
+
" Make sure Ollama is running and accessible from Docker containers."
|
|
1326
|
+
);
|
|
1327
|
+
}
|
|
1328
|
+
} else if (provider === "custom") {
|
|
1329
|
+
const curAm = envGet("IXORA_AGENT_MODEL");
|
|
1330
|
+
const curTm = envGet("IXORA_TEAM_MODEL");
|
|
1331
|
+
agentModel = await input({
|
|
1332
|
+
message: "Agent model (provider:model)",
|
|
1333
|
+
default: curAm || "anthropic:claude-sonnet-4-6"
|
|
1334
|
+
});
|
|
1335
|
+
teamModel = await input({
|
|
1336
|
+
message: "Team model (provider:model)",
|
|
1337
|
+
default: curTm || "anthropic:claude-haiku-4-5"
|
|
1338
|
+
});
|
|
1339
|
+
apiKeyVar = await input({
|
|
1340
|
+
message: "API key env var name (e.g., ANTHROPIC_API_KEY)"
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
if (apiKeyVar) {
|
|
1344
|
+
const curKey = envGet(apiKeyVar);
|
|
1345
|
+
apiKeyValue = await password({
|
|
1346
|
+
message: apiKeyVar,
|
|
1347
|
+
validate: (value) => {
|
|
1348
|
+
if (!value && !curKey) return `${apiKeyVar} is required`;
|
|
1349
|
+
return true;
|
|
1350
|
+
}
|
|
1351
|
+
});
|
|
1352
|
+
if (!apiKeyValue && curKey) apiKeyValue = curKey;
|
|
1353
|
+
}
|
|
1354
|
+
success(`Provider: ${provider} (${agentModel})`);
|
|
1355
|
+
return { provider, agentModel, teamModel, apiKeyVar, apiKeyValue, ollamaHost };
|
|
1356
|
+
}
|
|
1357
|
+
async function promptIbmiConnection() {
|
|
1358
|
+
info("IBM i Connection");
|
|
1359
|
+
console.log();
|
|
1360
|
+
const curHost = envGet("DB2i_HOST");
|
|
1361
|
+
const curUser = envGet("DB2i_USER");
|
|
1362
|
+
const curPass = envGet("DB2i_PASS");
|
|
1363
|
+
const host = await input({
|
|
1364
|
+
message: "IBM i hostname",
|
|
1365
|
+
default: curHost || void 0,
|
|
1366
|
+
validate: (value) => value.trim() ? true : "IBM i hostname is required"
|
|
1367
|
+
});
|
|
1368
|
+
const user = await input({
|
|
1369
|
+
message: "IBM i username",
|
|
1370
|
+
default: curUser || void 0,
|
|
1371
|
+
validate: (value) => value.trim() ? true : "IBM i username is required"
|
|
1372
|
+
});
|
|
1373
|
+
const pass = await password({
|
|
1374
|
+
message: "IBM i password",
|
|
1375
|
+
validate: (value) => {
|
|
1376
|
+
if (!value && !curPass) return "IBM i password is required";
|
|
1377
|
+
return true;
|
|
1378
|
+
}
|
|
1379
|
+
});
|
|
1380
|
+
return { host: host.trim(), user: user.trim(), pass: pass || curPass };
|
|
1381
|
+
}
|
|
1382
|
+
async function promptProfile() {
|
|
1383
|
+
const curProfile = envGet("IXORA_PROFILE") || "full";
|
|
1384
|
+
const profile = await select({
|
|
1385
|
+
message: "Select an agent profile",
|
|
1386
|
+
choices: VALID_PROFILES.map((p) => ({
|
|
1387
|
+
name: `${PROFILES[p].name.padEnd(14)} ${dim(PROFILES[p].description)}`,
|
|
1388
|
+
value: p
|
|
1389
|
+
})),
|
|
1390
|
+
default: curProfile
|
|
1391
|
+
});
|
|
1392
|
+
success(`Profile: ${profile}`);
|
|
1393
|
+
return profile;
|
|
1394
|
+
}
|
|
1395
|
+
async function cmdInstall(opts) {
|
|
1396
|
+
info(`Installing ixora (v${SCRIPT_VERSION})`);
|
|
1397
|
+
console.log();
|
|
1398
|
+
let composeCmd;
|
|
1399
|
+
try {
|
|
1400
|
+
composeCmd = await detectComposeCmd(opts.runtime);
|
|
1401
|
+
await verifyRuntimeRunning(composeCmd);
|
|
1402
|
+
} catch (e) {
|
|
1403
|
+
die(e.message);
|
|
1404
|
+
}
|
|
1405
|
+
detectPlatform();
|
|
1406
|
+
info(`Using: ${composeCmd}`);
|
|
1407
|
+
console.log();
|
|
1408
|
+
if (existsSync6(IXORA_DIR)) {
|
|
1409
|
+
warn(`Existing installation found at ${IXORA_DIR}`);
|
|
1410
|
+
const action = await select({
|
|
1411
|
+
message: "What would you like to do?",
|
|
1412
|
+
choices: [
|
|
1413
|
+
{ name: "Reconfigure \u2014 re-run setup prompts (overwrites current config)", value: "reconfigure" },
|
|
1414
|
+
{ name: "Cancel \u2014 keep existing installation", value: "cancel" }
|
|
1415
|
+
],
|
|
1416
|
+
default: "reconfigure"
|
|
1417
|
+
});
|
|
1418
|
+
if (action === "cancel") {
|
|
1419
|
+
info("Cancelled");
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
info("Reconfiguring...");
|
|
1423
|
+
console.log();
|
|
1424
|
+
}
|
|
1425
|
+
const { agentModel, teamModel, apiKeyVar, apiKeyValue, ollamaHost } = await promptModelProvider();
|
|
1426
|
+
console.log();
|
|
1427
|
+
const { host, user, pass } = await promptIbmiConnection();
|
|
1428
|
+
console.log();
|
|
1429
|
+
const profile = opts.profile ? opts.profile : await promptProfile();
|
|
1430
|
+
console.log();
|
|
1431
|
+
const version = opts.imageVersion ?? envGet("IXORA_VERSION") ?? "latest";
|
|
1432
|
+
const envConfig = {
|
|
1433
|
+
agentModel,
|
|
1434
|
+
teamModel,
|
|
1435
|
+
apiKeyVar: apiKeyVar || void 0,
|
|
1436
|
+
apiKeyValue: apiKeyValue || void 0,
|
|
1437
|
+
ollamaHost,
|
|
1438
|
+
db2Host: host,
|
|
1439
|
+
db2User: user,
|
|
1440
|
+
db2Pass: pass,
|
|
1441
|
+
profile,
|
|
1442
|
+
version
|
|
1443
|
+
};
|
|
1444
|
+
writeEnvFile(envConfig);
|
|
1445
|
+
success("Wrote .env");
|
|
1446
|
+
writeComposeFile();
|
|
1447
|
+
success("Wrote docker-compose.yml");
|
|
1448
|
+
if (opts.pull !== false) {
|
|
1449
|
+
info("Pulling images...");
|
|
1450
|
+
await runCompose(composeCmd, ["pull"]);
|
|
1451
|
+
}
|
|
1452
|
+
info("Starting services...");
|
|
1453
|
+
await runCompose(composeCmd, ["up", "-d"]);
|
|
1454
|
+
await waitForHealthy(composeCmd);
|
|
1455
|
+
console.log();
|
|
1456
|
+
success("ixora is running!");
|
|
1457
|
+
console.log();
|
|
1458
|
+
console.log(` ${bold("UI:")} http://localhost:3000`);
|
|
1459
|
+
console.log(` ${bold("API:")} http://localhost:8000`);
|
|
1460
|
+
console.log(` ${bold("Profile:")} ${profile}`);
|
|
1461
|
+
console.log();
|
|
1462
|
+
console.log(
|
|
1463
|
+
` Manage with: ${bold("ixora start|stop|restart|status|upgrade|config|logs")}`
|
|
1464
|
+
);
|
|
1465
|
+
console.log(` Config dir: ${dim(IXORA_DIR)}`);
|
|
1466
|
+
console.log();
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
// 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";
|
|
1473
|
+
function cmdConfigShow() {
|
|
1474
|
+
if (!existsSync7(ENV_FILE)) {
|
|
1475
|
+
die("ixora is not installed. Run: ixora install");
|
|
1476
|
+
}
|
|
1477
|
+
console.log();
|
|
1478
|
+
console.log(` ${chalk5.bold("Configuration")} ${ENV_FILE}`);
|
|
1479
|
+
console.log();
|
|
1480
|
+
section("Model");
|
|
1481
|
+
const agentModel = envGet("IXORA_AGENT_MODEL") || "anthropic:claude-sonnet-4-6";
|
|
1482
|
+
const teamModel = envGet("IXORA_TEAM_MODEL") || "anthropic:claude-haiku-4-5";
|
|
1483
|
+
const anthKey = envGet("ANTHROPIC_API_KEY");
|
|
1484
|
+
const oaiKey = envGet("OPENAI_API_KEY");
|
|
1485
|
+
const googKey = envGet("GOOGLE_API_KEY");
|
|
1486
|
+
const ollamaHost = envGet("OLLAMA_HOST");
|
|
1487
|
+
console.log(` ${cyan("IXORA_AGENT_MODEL")} ${agentModel}`);
|
|
1488
|
+
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)}`);
|
|
1492
|
+
if (ollamaHost) console.log(` ${cyan("OLLAMA_HOST")} ${ollamaHost}`);
|
|
1493
|
+
console.log();
|
|
1494
|
+
section("IBM i Connection");
|
|
1495
|
+
const db2Host = envGet("DB2i_HOST");
|
|
1496
|
+
const db2User = envGet("DB2i_USER");
|
|
1497
|
+
const db2Pass = envGet("DB2i_PASS");
|
|
1498
|
+
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)")}`);
|
|
1501
|
+
console.log(` ${cyan("DB2i_PASS")} ${maskValue(db2Pass)}`);
|
|
1502
|
+
console.log(` ${cyan("DB2_PORT")} ${db2Port || "8076"}`);
|
|
1503
|
+
console.log();
|
|
1504
|
+
section("Deployment");
|
|
1505
|
+
const profile = envGet("IXORA_PROFILE") || "full";
|
|
1506
|
+
const version = envGet("IXORA_VERSION") || "latest";
|
|
1507
|
+
console.log(` ${cyan("IXORA_PROFILE")} ${profile}`);
|
|
1508
|
+
console.log(` ${cyan("IXORA_VERSION")} ${version}`);
|
|
1509
|
+
console.log();
|
|
1510
|
+
const knownKeys = /* @__PURE__ */ new Set([
|
|
1511
|
+
"ANTHROPIC_API_KEY",
|
|
1512
|
+
"OPENAI_API_KEY",
|
|
1513
|
+
"GOOGLE_API_KEY",
|
|
1514
|
+
"OLLAMA_HOST",
|
|
1515
|
+
"DB2i_HOST",
|
|
1516
|
+
"DB2i_USER",
|
|
1517
|
+
"DB2i_PASS",
|
|
1518
|
+
"DB2_PORT",
|
|
1519
|
+
"IXORA_PROFILE",
|
|
1520
|
+
"IXORA_VERSION",
|
|
1521
|
+
"IXORA_AGENT_MODEL",
|
|
1522
|
+
"IXORA_TEAM_MODEL"
|
|
1523
|
+
]);
|
|
1524
|
+
const content = readFileSync3(ENV_FILE, "utf-8");
|
|
1525
|
+
const extraLines = content.split("\n").filter((line) => {
|
|
1526
|
+
const trimmed = line.trim();
|
|
1527
|
+
if (!trimmed || trimmed.startsWith("#")) return false;
|
|
1528
|
+
const key = trimmed.split("=")[0];
|
|
1529
|
+
return !knownKeys.has(key);
|
|
1530
|
+
});
|
|
1531
|
+
if (extraLines.length > 0) {
|
|
1532
|
+
section("Other");
|
|
1533
|
+
for (const line of extraLines) {
|
|
1534
|
+
const [key, ...rest] = line.split("=");
|
|
1535
|
+
let val = rest.join("=").replace(/^['"]|['"]$/g, "");
|
|
1536
|
+
if (isSensitiveKey(key)) {
|
|
1537
|
+
console.log(` ${cyan(key)} ${maskValue(val)}`);
|
|
1538
|
+
} else {
|
|
1539
|
+
console.log(` ${cyan(key)} ${val}`);
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
console.log();
|
|
1543
|
+
}
|
|
1544
|
+
console.log(` ${dim("Edit with: ixora config edit")}`);
|
|
1545
|
+
console.log(` ${dim("Set a value: ixora config set KEY VALUE")}`);
|
|
1546
|
+
console.log();
|
|
1547
|
+
}
|
|
1548
|
+
function cmdConfigSet(key, value) {
|
|
1549
|
+
if (!existsSync7(ENV_FILE)) {
|
|
1550
|
+
die("ixora is not installed. Run: ixora install");
|
|
1551
|
+
}
|
|
1552
|
+
updateEnvKey(key, value);
|
|
1553
|
+
success(`Set ${key}`);
|
|
1554
|
+
console.log(` Restart to apply: ${bold("ixora restart")}`);
|
|
1555
|
+
}
|
|
1556
|
+
async function cmdConfigEdit() {
|
|
1557
|
+
if (!existsSync7(ENV_FILE)) {
|
|
1558
|
+
die("ixora is not installed. Run: ixora install");
|
|
1559
|
+
}
|
|
1560
|
+
const editor = process.env["EDITOR"] ?? process.env["VISUAL"] ?? "";
|
|
1561
|
+
let editorCmd = editor;
|
|
1562
|
+
if (!editorCmd) {
|
|
1563
|
+
for (const candidate of ["vim", "vi", "nano"]) {
|
|
1564
|
+
try {
|
|
1565
|
+
await execa4("which", [candidate]);
|
|
1566
|
+
editorCmd = candidate;
|
|
1567
|
+
break;
|
|
1568
|
+
} catch {
|
|
1569
|
+
continue;
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
if (!editorCmd) {
|
|
1574
|
+
die("No editor found. Set $EDITOR or install vim/nano.");
|
|
1575
|
+
}
|
|
1576
|
+
info(`Opening ${editorCmd}...`);
|
|
1577
|
+
await execa4(editorCmd, [ENV_FILE], { stdio: "inherit" });
|
|
1578
|
+
console.log();
|
|
1579
|
+
success("Config saved");
|
|
1580
|
+
console.log(` Restart to apply: ${bold("ixora restart")}`);
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
// src/commands/system.ts
|
|
1584
|
+
import { input as input2, password as password2, select as select2 } from "@inquirer/prompts";
|
|
1585
|
+
import chalk6 from "chalk";
|
|
1586
|
+
async function cmdSystemAdd() {
|
|
1587
|
+
info("Add an IBM i system");
|
|
1588
|
+
console.log();
|
|
1589
|
+
const id = await input2({
|
|
1590
|
+
message: "System ID (short name, e.g., dev, prod)",
|
|
1591
|
+
validate: (value) => {
|
|
1592
|
+
const cleaned = value.toLowerCase().replace(/[^a-z0-9-]/g, "");
|
|
1593
|
+
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`;
|
|
1598
|
+
return true;
|
|
1599
|
+
},
|
|
1600
|
+
transformer: (value) => value.toLowerCase().replace(/[^a-z0-9-]/g, "")
|
|
1601
|
+
});
|
|
1602
|
+
const cleanId = id.toLowerCase().replace(/[^a-z0-9-]/g, "");
|
|
1603
|
+
const name = await input2({
|
|
1604
|
+
message: "Display name",
|
|
1605
|
+
default: cleanId
|
|
1606
|
+
});
|
|
1607
|
+
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"
|
|
1614
|
+
});
|
|
1615
|
+
const user = await input2({
|
|
1616
|
+
message: "IBM i username",
|
|
1617
|
+
validate: (value) => value.trim() ? true : "Username is required"
|
|
1618
|
+
});
|
|
1619
|
+
const pass = await password2({
|
|
1620
|
+
message: "IBM i password",
|
|
1621
|
+
validate: (value) => value ? true : "Password is required"
|
|
1622
|
+
});
|
|
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"
|
|
1639
|
+
});
|
|
1640
|
+
const agents = AGENT_PRESETS[agentChoice];
|
|
1641
|
+
addSystem({
|
|
1642
|
+
id: cleanId,
|
|
1643
|
+
name,
|
|
1644
|
+
agents: [...agents],
|
|
1645
|
+
host: host.trim(),
|
|
1646
|
+
port: port.trim(),
|
|
1647
|
+
user: user.trim(),
|
|
1648
|
+
pass
|
|
1649
|
+
});
|
|
1650
|
+
console.log();
|
|
1651
|
+
success(`Added system '${cleanId}' (${host.trim()})`);
|
|
1652
|
+
console.log(` Credentials stored in ${dim(ENV_FILE)}`);
|
|
1653
|
+
console.log(` Systems: ${systemCount()}`);
|
|
1654
|
+
console.log(` Restart to apply: ${bold("ixora restart")}`);
|
|
1655
|
+
console.log();
|
|
1656
|
+
}
|
|
1657
|
+
function cmdSystemRemove(id) {
|
|
1658
|
+
try {
|
|
1659
|
+
removeSystem(id);
|
|
1660
|
+
} catch (e) {
|
|
1661
|
+
die(e.message);
|
|
1662
|
+
}
|
|
1663
|
+
success(`Removed system '${id}'`);
|
|
1664
|
+
console.log(` Systems: ${systemCount()}`);
|
|
1665
|
+
console.log(` Restart to apply: ${bold("ixora restart")}`);
|
|
1666
|
+
}
|
|
1667
|
+
function cmdSystemList() {
|
|
1668
|
+
const primaryHost = envGet("DB2i_HOST");
|
|
1669
|
+
console.log();
|
|
1670
|
+
console.log(` ${chalk6.bold("IBM i Systems")}`);
|
|
1671
|
+
console.log();
|
|
1672
|
+
if (primaryHost) {
|
|
1673
|
+
console.log(
|
|
1674
|
+
` ${cyan("*")} ${"default".padEnd(12)} ${primaryHost.padEnd(30)} ${dim("(primary \u2014 from install)")}`
|
|
1675
|
+
);
|
|
1676
|
+
}
|
|
1677
|
+
const systems = readSystems();
|
|
1678
|
+
for (const sys of systems) {
|
|
1679
|
+
const idUpper = sys.id.toUpperCase().replace(/-/g, "_");
|
|
1680
|
+
const sysHost = envGet(`SYSTEM_${idUpper}_HOST`) || dim("(no host)");
|
|
1681
|
+
const agentsStr = sys.agents.join(", ");
|
|
1682
|
+
console.log(
|
|
1683
|
+
` ${cyan(" ")} ${sys.id.padEnd(12)} ${String(sysHost).padEnd(30)} ${agentsStr}`
|
|
1684
|
+
);
|
|
1685
|
+
}
|
|
1686
|
+
if (!primaryHost && systems.length === 0) {
|
|
1687
|
+
console.log(` ${dim(`No systems configured. Run: ${bold("ixora install")}`)}`);
|
|
1688
|
+
}
|
|
1689
|
+
console.log();
|
|
1690
|
+
const total = systems.length + (primaryHost ? 1 : 0);
|
|
1691
|
+
if (total > 1) {
|
|
1692
|
+
console.log(
|
|
1693
|
+
` ${dim("Multi-system mode: each system runs on its own port (8000, 8001, ...)")}`
|
|
1694
|
+
);
|
|
1695
|
+
}
|
|
1696
|
+
console.log(
|
|
1697
|
+
` ${dim("Add: ixora system add | Remove: ixora system remove <id>")}`
|
|
1698
|
+
);
|
|
1699
|
+
console.log();
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
// src/cli.ts
|
|
1703
|
+
function createProgram() {
|
|
1704
|
+
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
|
+
"--profile <name>",
|
|
1706
|
+
"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
|
+
);
|
|
1711
|
+
program2.command("install").description("First-time setup (interactive)").action(async () => {
|
|
1712
|
+
const opts = program2.opts();
|
|
1713
|
+
await cmdInstall(opts);
|
|
1714
|
+
});
|
|
1715
|
+
program2.command("start").description("Start services").action(async () => {
|
|
1716
|
+
const opts = program2.opts();
|
|
1717
|
+
await cmdStart(opts);
|
|
1718
|
+
});
|
|
1719
|
+
program2.command("stop").description("Stop services").action(async () => {
|
|
1720
|
+
const opts = program2.opts();
|
|
1721
|
+
await cmdStop(opts);
|
|
1722
|
+
});
|
|
1723
|
+
program2.command("restart").argument("[service]", "Service to restart (omit for all)").description("Restart all services, or a specific service by name").action(async (service) => {
|
|
1724
|
+
const opts = program2.opts();
|
|
1725
|
+
await cmdRestart(opts, service);
|
|
1726
|
+
});
|
|
1727
|
+
program2.command("status").description("Show service status and deployed profile").action(async () => {
|
|
1728
|
+
const opts = program2.opts();
|
|
1729
|
+
await cmdStatus(opts);
|
|
1730
|
+
});
|
|
1731
|
+
program2.command("upgrade").description("Pull latest images and restart").action(async () => {
|
|
1732
|
+
const opts = program2.opts();
|
|
1733
|
+
await cmdUpgrade(opts);
|
|
1734
|
+
});
|
|
1735
|
+
program2.command("uninstall").description("Stop services and remove images").action(async () => {
|
|
1736
|
+
const opts = program2.opts();
|
|
1737
|
+
await cmdUninstall(opts);
|
|
1738
|
+
});
|
|
1739
|
+
program2.command("logs").argument("[service]", "Service to show logs for (omit for all)").description("Tail service logs").action(async (service) => {
|
|
1740
|
+
const opts = program2.opts();
|
|
1741
|
+
await cmdLogs(opts, service);
|
|
1742
|
+
});
|
|
1743
|
+
program2.command("version").description("Show CLI and image versions").action(async () => {
|
|
1744
|
+
const opts = program2.opts();
|
|
1745
|
+
await cmdVersion(opts);
|
|
1746
|
+
});
|
|
1747
|
+
const configCmd = program2.command("config").description("View and edit deployment configuration");
|
|
1748
|
+
configCmd.command("show", { isDefault: true }).description("Show current configuration").action(() => {
|
|
1749
|
+
cmdConfigShow();
|
|
1750
|
+
});
|
|
1751
|
+
configCmd.command("set").argument("<key>", "Configuration key").argument("<value>", "Configuration value").description("Set a configuration value").action((key, value) => {
|
|
1752
|
+
cmdConfigSet(key, value);
|
|
1753
|
+
});
|
|
1754
|
+
configCmd.command("edit").description("Open configuration in your editor").action(async () => {
|
|
1755
|
+
await cmdConfigEdit();
|
|
1756
|
+
});
|
|
1757
|
+
const systemCmd = program2.command("system").description("Manage IBM i systems (add, remove, list)");
|
|
1758
|
+
systemCmd.command("add").description("Add an IBM i system").action(async () => {
|
|
1759
|
+
await cmdSystemAdd();
|
|
1760
|
+
});
|
|
1761
|
+
systemCmd.command("remove").argument("<id>", "System ID to remove").description("Remove a system by ID").action((id) => {
|
|
1762
|
+
cmdSystemRemove(id);
|
|
1763
|
+
});
|
|
1764
|
+
systemCmd.command("list", { isDefault: true }).description("List configured systems").action(() => {
|
|
1765
|
+
cmdSystemList();
|
|
1766
|
+
});
|
|
1767
|
+
return program2;
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
// src/index.ts
|
|
1771
|
+
var program = createProgram();
|
|
1772
|
+
process.on("unhandledRejection", (err) => {
|
|
1773
|
+
error(err instanceof Error ? err.message : String(err));
|
|
1774
|
+
process.exit(1);
|
|
1775
|
+
});
|
|
1776
|
+
program.parseAsync().catch((err) => {
|
|
1777
|
+
error(err instanceof Error ? err.message : String(err));
|
|
1778
|
+
process.exit(1);
|
|
1779
|
+
});
|