@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
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
COMPOSE_FILE,
|
|
4
|
+
ENV_FILE,
|
|
5
|
+
HEALTH_TIMEOUT,
|
|
6
|
+
IXORA_DIR,
|
|
7
|
+
SYSTEMS_CONFIG,
|
|
8
|
+
readSystems
|
|
9
|
+
} from "./chunk-VCQRSQ4F.js";
|
|
10
|
+
|
|
11
|
+
// src/lib/compose.ts
|
|
12
|
+
import { execa as execa2 } from "execa";
|
|
13
|
+
import { existsSync, mkdirSync } from "fs";
|
|
14
|
+
import { writeFileSync } from "fs";
|
|
15
|
+
|
|
16
|
+
// src/lib/platform.ts
|
|
17
|
+
import { arch } from "os";
|
|
18
|
+
import { execa } from "execa";
|
|
19
|
+
async function detectComposeCmd(optRuntime) {
|
|
20
|
+
if (optRuntime) {
|
|
21
|
+
switch (optRuntime) {
|
|
22
|
+
case "docker":
|
|
23
|
+
return "docker compose";
|
|
24
|
+
case "podman":
|
|
25
|
+
return "podman compose";
|
|
26
|
+
default:
|
|
27
|
+
throw new Error(
|
|
28
|
+
`Unknown runtime: ${optRuntime} (choose: docker, podman)`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
await execa("docker", ["compose", "version"]);
|
|
34
|
+
return "docker compose";
|
|
35
|
+
} catch {
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
await execa("podman", ["compose", "version"]);
|
|
39
|
+
return "podman compose";
|
|
40
|
+
} catch {
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
await execa("docker-compose", ["version"]);
|
|
44
|
+
return "docker-compose";
|
|
45
|
+
} catch {
|
|
46
|
+
}
|
|
47
|
+
throw new Error(
|
|
48
|
+
"Neither 'docker compose', 'podman compose', nor 'docker-compose' found.\nPlease install Docker or Podman first."
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
async function verifyRuntimeRunning(composeCmd) {
|
|
52
|
+
const runtime = composeCmd.startsWith("docker") ? "docker" : "podman";
|
|
53
|
+
try {
|
|
54
|
+
await execa(runtime, ["info"]);
|
|
55
|
+
} catch {
|
|
56
|
+
const name = runtime === "docker" ? "Docker Desktop" : "Podman";
|
|
57
|
+
throw new Error(`${name} is not running. Please start it and try again.`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function detectPlatform() {
|
|
61
|
+
const cpuArch = arch();
|
|
62
|
+
if (cpuArch === "ppc64") {
|
|
63
|
+
return {
|
|
64
|
+
dbImage: process.env["IXORA_DB_IMAGE"] ?? `ghcr.io/ibmi-agi/ixora-db:${process.env["IXORA_VERSION"] ?? "latest"}`
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return {};
|
|
68
|
+
}
|
|
69
|
+
function getComposeParts(cmd) {
|
|
70
|
+
if (cmd === "docker-compose") {
|
|
71
|
+
return ["docker-compose", []];
|
|
72
|
+
}
|
|
73
|
+
const [bin, sub] = cmd.split(" ");
|
|
74
|
+
return [bin, [sub]];
|
|
75
|
+
}
|
|
76
|
+
function getRuntimeBin(cmd) {
|
|
77
|
+
return cmd.startsWith("docker") ? "docker" : "podman";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// src/lib/templates/multi-compose.ts
|
|
81
|
+
function generateMultiCompose(envFile = ENV_FILE, configFile = SYSTEMS_CONFIG) {
|
|
82
|
+
const systems = readSystems(configFile);
|
|
83
|
+
let content = `# Auto-generated compose file
|
|
84
|
+
# Regenerated on every start. Edit ixora-systems.yaml instead.
|
|
85
|
+
services:
|
|
86
|
+
agentos-db:
|
|
87
|
+
image: \${IXORA_DB_IMAGE:-agnohq/pgvector:18}
|
|
88
|
+
restart: unless-stopped
|
|
89
|
+
ports:
|
|
90
|
+
- "\${DB_PORT:-5432}:5432"
|
|
91
|
+
environment:
|
|
92
|
+
POSTGRES_USER: \${DB_USER:-ai}
|
|
93
|
+
POSTGRES_PASSWORD: \${DB_PASS:-ai}
|
|
94
|
+
POSTGRES_DB: \${DB_DATABASE:-ai}
|
|
95
|
+
volumes:
|
|
96
|
+
- pgdata:/var/lib/postgresql
|
|
97
|
+
healthcheck:
|
|
98
|
+
test: ["CMD-SHELL", "pg_isready -U \${DB_USER:-ai}"]
|
|
99
|
+
interval: 5s
|
|
100
|
+
timeout: 5s
|
|
101
|
+
retries: 5
|
|
102
|
+
|
|
103
|
+
`;
|
|
104
|
+
let apiPort = 8e3;
|
|
105
|
+
let firstApi = "";
|
|
106
|
+
for (const sys of systems) {
|
|
107
|
+
const idUpper = sys.id.toUpperCase().replace(/-/g, "_");
|
|
108
|
+
content += ` mcp-${sys.id}:
|
|
109
|
+
image: ghcr.io/ibmi-agi/ixora-mcp-server:\${IXORA_VERSION:-latest}
|
|
110
|
+
restart: unless-stopped
|
|
111
|
+
environment:
|
|
112
|
+
DB2i_HOST: \${SYSTEM_${idUpper}_HOST}
|
|
113
|
+
DB2i_USER: \${SYSTEM_${idUpper}_USER}
|
|
114
|
+
DB2i_PASS: \${SYSTEM_${idUpper}_PASS}
|
|
115
|
+
DB2_PORT: \${SYSTEM_${idUpper}_PORT:-8076}
|
|
116
|
+
MCP_TRANSPORT_TYPE: http
|
|
117
|
+
MCP_SESSION_MODE: stateless
|
|
118
|
+
YAML_AUTO_RELOAD: "true"
|
|
119
|
+
TOOLS_YAML_PATH: /usr/src/app/tools
|
|
120
|
+
YAML_ALLOW_DUPLICATE_SOURCES: "true"
|
|
121
|
+
IBMI_ENABLE_EXECUTE_SQL: "true"
|
|
122
|
+
IBMI_ENABLE_DEFAULT_TOOLS: "true"
|
|
123
|
+
MCP_AUTH_MODE: "none"
|
|
124
|
+
IBMI_HTTP_AUTH_ENABLED: "false"
|
|
125
|
+
MCP_POOL_QUERY_TIMEOUT_MS: "120000"
|
|
126
|
+
healthcheck:
|
|
127
|
+
test: ["CMD-SHELL", "node -e \\"fetch('http://localhost:3010/healthz').then(function(r){process.exit(r.ok?0:1)}).catch(function(){process.exit(1)})\\""]
|
|
128
|
+
interval: 5s
|
|
129
|
+
timeout: 5s
|
|
130
|
+
retries: 5
|
|
131
|
+
start_period: 3s
|
|
132
|
+
|
|
133
|
+
`;
|
|
134
|
+
content += ` api-${sys.id}:
|
|
135
|
+
image: ghcr.io/ibmi-agi/ixora-api:\${IXORA_VERSION:-latest}
|
|
136
|
+
command: uvicorn app.main:app --host 0.0.0.0 --port 8000
|
|
137
|
+
restart: unless-stopped
|
|
138
|
+
ports:
|
|
139
|
+
- "${apiPort}:8000"
|
|
140
|
+
environment:
|
|
141
|
+
ANTHROPIC_API_KEY: \${ANTHROPIC_API_KEY:-}
|
|
142
|
+
OPENAI_API_KEY: \${OPENAI_API_KEY:-}
|
|
143
|
+
GOOGLE_API_KEY: \${GOOGLE_API_KEY:-}
|
|
144
|
+
OLLAMA_HOST: \${OLLAMA_HOST:-http://host.docker.internal:11434}
|
|
145
|
+
IXORA_AGENT_MODEL: \${IXORA_AGENT_MODEL:-anthropic:claude-sonnet-4-6}
|
|
146
|
+
IXORA_TEAM_MODEL: \${IXORA_TEAM_MODEL:-anthropic:claude-haiku-4-5}
|
|
147
|
+
DB_HOST: agentos-db
|
|
148
|
+
DB_PORT: "5432"
|
|
149
|
+
DB_USER: \${DB_USER:-ai}
|
|
150
|
+
DB_PASS: \${DB_PASS:-ai}
|
|
151
|
+
DB_DATABASE: \${DB_DATABASE:-ai}
|
|
152
|
+
MCP_URL: http://mcp-${sys.id}:3010/mcp
|
|
153
|
+
IXORA_SYSTEM_ID: ${sys.id}
|
|
154
|
+
IXORA_SYSTEM_NAME: ${sys.name}
|
|
155
|
+
IAASSIST_DEPLOYMENT_CONFIG: app/config/deployments/${sys.profile || "full"}.yaml
|
|
156
|
+
DATA_DIR: /data
|
|
157
|
+
RUNTIME_ENV: docker
|
|
158
|
+
WAIT_FOR_DB: "True"
|
|
159
|
+
CORS_ORIGINS: \${CORS_ORIGINS:-*}
|
|
160
|
+
AUTH_ENABLED: "false"
|
|
161
|
+
MCP_AUTH_MODE: "none"
|
|
162
|
+
IXORA_ENABLE_BUILDER: "true"
|
|
163
|
+
A2A_INTERFACE: \${A2A_INTERFACE:-false}
|
|
164
|
+
DB2i_HOST: \${SYSTEM_${idUpper}_HOST}
|
|
165
|
+
DB2i_USER: \${SYSTEM_${idUpper}_USER}
|
|
166
|
+
DB2i_PASS: \${SYSTEM_${idUpper}_PASS}
|
|
167
|
+
DB2_PORT: \${SYSTEM_${idUpper}_PORT:-8076}
|
|
168
|
+
volumes:
|
|
169
|
+
- agentos-data:/data
|
|
170
|
+
- type: bind
|
|
171
|
+
source: \${HOME}/.ixora/user_tools
|
|
172
|
+
target: /data/user_tools
|
|
173
|
+
bind:
|
|
174
|
+
create_host_path: true
|
|
175
|
+
depends_on:
|
|
176
|
+
agentos-db:
|
|
177
|
+
condition: service_healthy
|
|
178
|
+
mcp-${sys.id}:
|
|
179
|
+
condition: service_healthy
|
|
180
|
+
healthcheck:
|
|
181
|
+
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)\\""]
|
|
182
|
+
interval: 10s
|
|
183
|
+
timeout: 5s
|
|
184
|
+
retries: 6
|
|
185
|
+
start_period: 30s
|
|
186
|
+
|
|
187
|
+
`;
|
|
188
|
+
if (!firstApi) firstApi = `api-${sys.id}`;
|
|
189
|
+
apiPort++;
|
|
190
|
+
}
|
|
191
|
+
content += ` ui:
|
|
192
|
+
image: ghcr.io/ibmi-agi/ixora-ui:\${IXORA_VERSION:-latest}
|
|
193
|
+
restart: unless-stopped
|
|
194
|
+
ports:
|
|
195
|
+
- "3000:3000"
|
|
196
|
+
environment:
|
|
197
|
+
NEXT_PUBLIC_API_URL: http://localhost:8000
|
|
198
|
+
depends_on:
|
|
199
|
+
${firstApi}:
|
|
200
|
+
condition: service_healthy
|
|
201
|
+
|
|
202
|
+
volumes:
|
|
203
|
+
pgdata:
|
|
204
|
+
agentos-data:
|
|
205
|
+
`;
|
|
206
|
+
return content;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// src/lib/ui.ts
|
|
210
|
+
import chalk from "chalk";
|
|
211
|
+
function info(message) {
|
|
212
|
+
console.log(`${chalk.blue("==>")} ${chalk.bold(message)}`);
|
|
213
|
+
}
|
|
214
|
+
function success(message) {
|
|
215
|
+
console.log(`${chalk.green("==>")} ${chalk.bold(message)}`);
|
|
216
|
+
}
|
|
217
|
+
function warn(message) {
|
|
218
|
+
console.log(`${chalk.yellow("Warning:")} ${message}`);
|
|
219
|
+
}
|
|
220
|
+
function error(message) {
|
|
221
|
+
console.error(`${chalk.red("Error:")} ${message}`);
|
|
222
|
+
}
|
|
223
|
+
function die(message) {
|
|
224
|
+
error(message);
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}
|
|
227
|
+
function maskValue(value) {
|
|
228
|
+
if (!value) return chalk.dim("(not set)");
|
|
229
|
+
if (value.length <= 4) return "****";
|
|
230
|
+
return `${value.slice(0, 4)}****`;
|
|
231
|
+
}
|
|
232
|
+
function isSensitiveKey(key) {
|
|
233
|
+
const upper = key.toUpperCase();
|
|
234
|
+
return /KEY|TOKEN|PASS|SECRET|ENCRYPT/.test(upper);
|
|
235
|
+
}
|
|
236
|
+
function dim(message) {
|
|
237
|
+
return chalk.dim(message);
|
|
238
|
+
}
|
|
239
|
+
function bold(message) {
|
|
240
|
+
return chalk.bold(message);
|
|
241
|
+
}
|
|
242
|
+
function cyan(message) {
|
|
243
|
+
return chalk.cyan(message);
|
|
244
|
+
}
|
|
245
|
+
function section(title) {
|
|
246
|
+
console.log(
|
|
247
|
+
` ${chalk.dim(`\u2500\u2500 ${title} ${"\u2500".repeat(Math.max(0, 49 - title.length))}`)}`
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// src/lib/compose.ts
|
|
252
|
+
async function runCompose(composeCmd, args, options = {}) {
|
|
253
|
+
const [bin, subArgs] = getComposeParts(composeCmd);
|
|
254
|
+
const fullArgs = [
|
|
255
|
+
...subArgs,
|
|
256
|
+
"-p",
|
|
257
|
+
"ixora",
|
|
258
|
+
"-f",
|
|
259
|
+
COMPOSE_FILE,
|
|
260
|
+
"--env-file",
|
|
261
|
+
ENV_FILE,
|
|
262
|
+
...args
|
|
263
|
+
];
|
|
264
|
+
try {
|
|
265
|
+
const result = await execa2(bin, fullArgs, {
|
|
266
|
+
stdio: "inherit",
|
|
267
|
+
...options
|
|
268
|
+
});
|
|
269
|
+
return {
|
|
270
|
+
stdout: String(result.stdout ?? ""),
|
|
271
|
+
stderr: String(result.stderr ?? ""),
|
|
272
|
+
exitCode: result.exitCode ?? 0
|
|
273
|
+
};
|
|
274
|
+
} catch (err) {
|
|
275
|
+
const exitCode = err && typeof err === "object" && "exitCode" in err ? err.exitCode : 1;
|
|
276
|
+
error(`Command failed: ${composeCmd} ${args.join(" ")}`);
|
|
277
|
+
console.log(` Check ${bold("ixora logs")} for details.`);
|
|
278
|
+
process.exit(exitCode);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
async function runComposeCapture(composeCmd, args) {
|
|
282
|
+
const [bin, subArgs] = getComposeParts(composeCmd);
|
|
283
|
+
const fullArgs = [
|
|
284
|
+
...subArgs,
|
|
285
|
+
"-p",
|
|
286
|
+
"ixora",
|
|
287
|
+
"-f",
|
|
288
|
+
COMPOSE_FILE,
|
|
289
|
+
"--env-file",
|
|
290
|
+
ENV_FILE,
|
|
291
|
+
...args
|
|
292
|
+
];
|
|
293
|
+
const result = await execa2(bin, fullArgs, { stdio: "pipe" });
|
|
294
|
+
return result.stdout;
|
|
295
|
+
}
|
|
296
|
+
function writeComposeFile(envFile = ENV_FILE) {
|
|
297
|
+
mkdirSync(IXORA_DIR, { recursive: true });
|
|
298
|
+
const content = generateMultiCompose(envFile);
|
|
299
|
+
writeFileSync(COMPOSE_FILE, content, "utf-8");
|
|
300
|
+
}
|
|
301
|
+
function requireInstalled() {
|
|
302
|
+
if (!existsSync(ENV_FILE)) {
|
|
303
|
+
throw new Error("ixora is not installed. Run: ixora install");
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
function requireComposeFile() {
|
|
307
|
+
if (!existsSync(COMPOSE_FILE)) {
|
|
308
|
+
throw new Error("ixora is not installed. Run: ixora install");
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
function resolveService(input) {
|
|
312
|
+
return input.replace(/^ixora-/, "").replace(/-\d+$/, "");
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// src/lib/health.ts
|
|
316
|
+
import { execa as execa3 } from "execa";
|
|
317
|
+
import ora from "ora";
|
|
318
|
+
async function waitForHealthy(composeCmd, timeout = HEALTH_TIMEOUT) {
|
|
319
|
+
const spinner = ora("Waiting for services to become healthy...").start();
|
|
320
|
+
const runtime = getRuntimeBin(composeCmd);
|
|
321
|
+
let apiContainer = "";
|
|
322
|
+
try {
|
|
323
|
+
const output = await runComposeCapture(composeCmd, [
|
|
324
|
+
"ps",
|
|
325
|
+
"--format",
|
|
326
|
+
"{{.Name}}"
|
|
327
|
+
]);
|
|
328
|
+
const containers = output.split("\n").filter(Boolean);
|
|
329
|
+
apiContainer = containers.find((c) => c.includes("ixora-api-")) ?? "";
|
|
330
|
+
} catch {
|
|
331
|
+
}
|
|
332
|
+
if (!apiContainer) {
|
|
333
|
+
spinner.stop();
|
|
334
|
+
warn("Could not find API container \u2014 skipping health check");
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
spinner.text = `Waiting for services to become healthy (up to ${timeout}s)...`;
|
|
338
|
+
let elapsed = 0;
|
|
339
|
+
while (elapsed < timeout) {
|
|
340
|
+
try {
|
|
341
|
+
const stateResult = await execa3(runtime, [
|
|
342
|
+
"inspect",
|
|
343
|
+
"--format",
|
|
344
|
+
"{{.State.Status}}",
|
|
345
|
+
apiContainer
|
|
346
|
+
]);
|
|
347
|
+
const state = stateResult.stdout.trim();
|
|
348
|
+
if (state === "exited" || state === "dead") {
|
|
349
|
+
spinner.fail("API container failed to start");
|
|
350
|
+
console.log(`
|
|
351
|
+
Run ${bold("ixora logs api")} to investigate.`);
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
const healthResult = await execa3(runtime, [
|
|
355
|
+
"inspect",
|
|
356
|
+
"--format",
|
|
357
|
+
"{{.State.Health.Status}}",
|
|
358
|
+
apiContainer
|
|
359
|
+
]);
|
|
360
|
+
const health = healthResult.stdout.trim();
|
|
361
|
+
if (health === "healthy") {
|
|
362
|
+
spinner.succeed("Services are healthy");
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
} catch {
|
|
366
|
+
}
|
|
367
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
368
|
+
elapsed += 2;
|
|
369
|
+
spinner.text = `Waiting for services to become healthy (${elapsed}s/${timeout}s)...`;
|
|
370
|
+
}
|
|
371
|
+
spinner.warn(
|
|
372
|
+
`Services did not become healthy within ${timeout}s \u2014 they may still be starting`
|
|
373
|
+
);
|
|
374
|
+
console.log(` Run ${bold("ixora logs api")} to investigate.`);
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// src/commands/restart.ts
|
|
379
|
+
async function cmdRestart(opts, service) {
|
|
380
|
+
try {
|
|
381
|
+
requireInstalled();
|
|
382
|
+
} catch (e) {
|
|
383
|
+
die(e.message);
|
|
384
|
+
}
|
|
385
|
+
let composeCmd;
|
|
386
|
+
try {
|
|
387
|
+
composeCmd = await detectComposeCmd(opts.runtime);
|
|
388
|
+
await verifyRuntimeRunning(composeCmd);
|
|
389
|
+
} catch (e) {
|
|
390
|
+
die(e.message);
|
|
391
|
+
}
|
|
392
|
+
detectPlatform();
|
|
393
|
+
writeComposeFile();
|
|
394
|
+
if (service) {
|
|
395
|
+
const svc = resolveService(service);
|
|
396
|
+
info(`Restarting ${svc}...`);
|
|
397
|
+
await runCompose(composeCmd, [
|
|
398
|
+
"up",
|
|
399
|
+
"-d",
|
|
400
|
+
"--force-recreate",
|
|
401
|
+
"--no-deps",
|
|
402
|
+
svc
|
|
403
|
+
]);
|
|
404
|
+
success(`Restarted ${svc}`);
|
|
405
|
+
} else {
|
|
406
|
+
info("Restarting all services...");
|
|
407
|
+
await runCompose(composeCmd, [
|
|
408
|
+
"up",
|
|
409
|
+
"-d",
|
|
410
|
+
"--force-recreate",
|
|
411
|
+
"--remove-orphans"
|
|
412
|
+
]);
|
|
413
|
+
await waitForHealthy(composeCmd);
|
|
414
|
+
console.log();
|
|
415
|
+
success("All services restarted");
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export {
|
|
420
|
+
detectComposeCmd,
|
|
421
|
+
verifyRuntimeRunning,
|
|
422
|
+
detectPlatform,
|
|
423
|
+
info,
|
|
424
|
+
success,
|
|
425
|
+
warn,
|
|
426
|
+
error,
|
|
427
|
+
die,
|
|
428
|
+
maskValue,
|
|
429
|
+
isSensitiveKey,
|
|
430
|
+
dim,
|
|
431
|
+
bold,
|
|
432
|
+
cyan,
|
|
433
|
+
section,
|
|
434
|
+
runCompose,
|
|
435
|
+
runComposeCapture,
|
|
436
|
+
writeComposeFile,
|
|
437
|
+
requireInstalled,
|
|
438
|
+
requireComposeFile,
|
|
439
|
+
resolveService,
|
|
440
|
+
waitForHealthy,
|
|
441
|
+
cmdRestart
|
|
442
|
+
};
|