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