@ibm/ixora 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,441 @@
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-F7YJCNQP.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
+ DB2i_HOST: \${SYSTEM_${idUpper}_HOST}
164
+ DB2i_USER: \${SYSTEM_${idUpper}_USER}
165
+ DB2i_PASS: \${SYSTEM_${idUpper}_PASS}
166
+ DB2_PORT: \${SYSTEM_${idUpper}_PORT:-8076}
167
+ volumes:
168
+ - agentos-data:/data
169
+ - type: bind
170
+ source: \${HOME}/.ixora/user_tools
171
+ target: /data/user_tools
172
+ bind:
173
+ create_host_path: true
174
+ depends_on:
175
+ agentos-db:
176
+ condition: service_healthy
177
+ mcp-${sys.id}:
178
+ condition: service_healthy
179
+ healthcheck:
180
+ 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)\\""]
181
+ interval: 10s
182
+ timeout: 5s
183
+ retries: 6
184
+ start_period: 30s
185
+
186
+ `;
187
+ if (!firstApi) firstApi = `api-${sys.id}`;
188
+ apiPort++;
189
+ }
190
+ content += ` ui:
191
+ image: ghcr.io/ibmi-agi/ixora-ui:\${IXORA_VERSION:-latest}
192
+ restart: unless-stopped
193
+ ports:
194
+ - "3000:3000"
195
+ environment:
196
+ NEXT_PUBLIC_API_URL: http://localhost:8000
197
+ depends_on:
198
+ ${firstApi}:
199
+ condition: service_healthy
200
+
201
+ volumes:
202
+ pgdata:
203
+ agentos-data:
204
+ `;
205
+ return content;
206
+ }
207
+
208
+ // src/lib/ui.ts
209
+ import chalk from "chalk";
210
+ function info(message) {
211
+ console.log(`${chalk.blue("==>")} ${chalk.bold(message)}`);
212
+ }
213
+ function success(message) {
214
+ console.log(`${chalk.green("==>")} ${chalk.bold(message)}`);
215
+ }
216
+ function warn(message) {
217
+ console.log(`${chalk.yellow("Warning:")} ${message}`);
218
+ }
219
+ function error(message) {
220
+ console.error(`${chalk.red("Error:")} ${message}`);
221
+ }
222
+ function die(message) {
223
+ error(message);
224
+ process.exit(1);
225
+ }
226
+ function maskValue(value) {
227
+ if (!value) return chalk.dim("(not set)");
228
+ if (value.length <= 4) return "****";
229
+ return `${value.slice(0, 4)}****`;
230
+ }
231
+ function isSensitiveKey(key) {
232
+ const upper = key.toUpperCase();
233
+ return /KEY|TOKEN|PASS|SECRET|ENCRYPT/.test(upper);
234
+ }
235
+ function dim(message) {
236
+ return chalk.dim(message);
237
+ }
238
+ function bold(message) {
239
+ return chalk.bold(message);
240
+ }
241
+ function cyan(message) {
242
+ return chalk.cyan(message);
243
+ }
244
+ function section(title) {
245
+ console.log(
246
+ ` ${chalk.dim(`\u2500\u2500 ${title} ${"\u2500".repeat(Math.max(0, 49 - title.length))}`)}`
247
+ );
248
+ }
249
+
250
+ // src/lib/compose.ts
251
+ async function runCompose(composeCmd, args, options = {}) {
252
+ const [bin, subArgs] = getComposeParts(composeCmd);
253
+ const fullArgs = [
254
+ ...subArgs,
255
+ "-p",
256
+ "ixora",
257
+ "-f",
258
+ COMPOSE_FILE,
259
+ "--env-file",
260
+ ENV_FILE,
261
+ ...args
262
+ ];
263
+ try {
264
+ const result = await execa2(bin, fullArgs, {
265
+ stdio: "inherit",
266
+ ...options
267
+ });
268
+ return {
269
+ stdout: String(result.stdout ?? ""),
270
+ stderr: String(result.stderr ?? ""),
271
+ exitCode: result.exitCode ?? 0
272
+ };
273
+ } catch (err) {
274
+ const exitCode = err && typeof err === "object" && "exitCode" in err ? err.exitCode : 1;
275
+ error(`Command failed: ${composeCmd} ${args.join(" ")}`);
276
+ console.log(` Check ${bold("ixora logs")} for details.`);
277
+ process.exit(exitCode);
278
+ }
279
+ }
280
+ async function runComposeCapture(composeCmd, args) {
281
+ const [bin, subArgs] = getComposeParts(composeCmd);
282
+ const fullArgs = [
283
+ ...subArgs,
284
+ "-p",
285
+ "ixora",
286
+ "-f",
287
+ COMPOSE_FILE,
288
+ "--env-file",
289
+ ENV_FILE,
290
+ ...args
291
+ ];
292
+ const result = await execa2(bin, fullArgs, { stdio: "pipe" });
293
+ return result.stdout;
294
+ }
295
+ function writeComposeFile(envFile = ENV_FILE) {
296
+ mkdirSync(IXORA_DIR, { recursive: true });
297
+ const content = generateMultiCompose(envFile);
298
+ writeFileSync(COMPOSE_FILE, content, "utf-8");
299
+ }
300
+ function requireInstalled() {
301
+ if (!existsSync(ENV_FILE)) {
302
+ throw new Error("ixora is not installed. Run: ixora install");
303
+ }
304
+ }
305
+ function requireComposeFile() {
306
+ if (!existsSync(COMPOSE_FILE)) {
307
+ throw new Error("ixora is not installed. Run: ixora install");
308
+ }
309
+ }
310
+ function resolveService(input) {
311
+ return input.replace(/^ixora-/, "").replace(/-\d+$/, "");
312
+ }
313
+
314
+ // src/lib/health.ts
315
+ import { execa as execa3 } from "execa";
316
+ import ora from "ora";
317
+ async function waitForHealthy(composeCmd, timeout = HEALTH_TIMEOUT) {
318
+ const spinner = ora("Waiting for services to become healthy...").start();
319
+ const runtime = getRuntimeBin(composeCmd);
320
+ let apiContainer = "";
321
+ try {
322
+ const output = await runComposeCapture(composeCmd, [
323
+ "ps",
324
+ "--format",
325
+ "{{.Name}}"
326
+ ]);
327
+ const containers = output.split("\n").filter(Boolean);
328
+ apiContainer = containers.find((c) => c.includes("ixora-api-")) ?? "";
329
+ } catch {
330
+ }
331
+ if (!apiContainer) {
332
+ spinner.stop();
333
+ warn("Could not find API container \u2014 skipping health check");
334
+ return true;
335
+ }
336
+ spinner.text = `Waiting for services to become healthy (up to ${timeout}s)...`;
337
+ let elapsed = 0;
338
+ while (elapsed < timeout) {
339
+ try {
340
+ const stateResult = await execa3(runtime, [
341
+ "inspect",
342
+ "--format",
343
+ "{{.State.Status}}",
344
+ apiContainer
345
+ ]);
346
+ const state = stateResult.stdout.trim();
347
+ if (state === "exited" || state === "dead") {
348
+ spinner.fail("API container failed to start");
349
+ console.log(`
350
+ Run ${bold("ixora logs api")} to investigate.`);
351
+ return false;
352
+ }
353
+ const healthResult = await execa3(runtime, [
354
+ "inspect",
355
+ "--format",
356
+ "{{.State.Health.Status}}",
357
+ apiContainer
358
+ ]);
359
+ const health = healthResult.stdout.trim();
360
+ if (health === "healthy") {
361
+ spinner.succeed("Services are healthy");
362
+ return true;
363
+ }
364
+ } catch {
365
+ }
366
+ await new Promise((r) => setTimeout(r, 2e3));
367
+ elapsed += 2;
368
+ spinner.text = `Waiting for services to become healthy (${elapsed}s/${timeout}s)...`;
369
+ }
370
+ spinner.warn(
371
+ `Services did not become healthy within ${timeout}s \u2014 they may still be starting`
372
+ );
373
+ console.log(` Run ${bold("ixora logs api")} to investigate.`);
374
+ return false;
375
+ }
376
+
377
+ // src/commands/restart.ts
378
+ async function cmdRestart(opts, service) {
379
+ try {
380
+ requireInstalled();
381
+ } catch (e) {
382
+ die(e.message);
383
+ }
384
+ let composeCmd;
385
+ try {
386
+ composeCmd = await detectComposeCmd(opts.runtime);
387
+ await verifyRuntimeRunning(composeCmd);
388
+ } catch (e) {
389
+ die(e.message);
390
+ }
391
+ detectPlatform();
392
+ writeComposeFile();
393
+ if (service) {
394
+ const svc = resolveService(service);
395
+ info(`Restarting ${svc}...`);
396
+ await runCompose(composeCmd, [
397
+ "up",
398
+ "-d",
399
+ "--force-recreate",
400
+ "--no-deps",
401
+ svc
402
+ ]);
403
+ success(`Restarted ${svc}`);
404
+ } else {
405
+ info("Restarting all services...");
406
+ await runCompose(composeCmd, [
407
+ "up",
408
+ "-d",
409
+ "--force-recreate",
410
+ "--remove-orphans"
411
+ ]);
412
+ await waitForHealthy(composeCmd);
413
+ console.log();
414
+ success("All services restarted");
415
+ }
416
+ }
417
+
418
+ export {
419
+ detectComposeCmd,
420
+ verifyRuntimeRunning,
421
+ detectPlatform,
422
+ info,
423
+ success,
424
+ warn,
425
+ error,
426
+ die,
427
+ maskValue,
428
+ isSensitiveKey,
429
+ dim,
430
+ bold,
431
+ cyan,
432
+ section,
433
+ runCompose,
434
+ runComposeCapture,
435
+ writeComposeFile,
436
+ requireInstalled,
437
+ requireComposeFile,
438
+ resolveService,
439
+ waitForHealthy,
440
+ cmdRestart
441
+ };