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