@kici-dev/orchestrator 0.1.12 → 0.1.14
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/cli/commands/agent-service/install.d.ts +7 -0
- package/dist/cli/commands/agent-service/logs.d.ts +8 -3
- package/dist/cli/commands/agent-service/restart.d.ts +5 -1
- package/dist/cli/commands/agent-service/start.d.ts +5 -1
- package/dist/cli/commands/agent-service/status.d.ts +5 -1
- package/dist/cli/commands/agent-service/stop.d.ts +5 -1
- package/dist/cli/commands/agent-service/uninstall.d.ts +6 -2
- package/dist/cli/commands/agent-service/upgrade.d.ts +13 -3
- package/dist/cli/commands/orchestrator-service/logs.d.ts +4 -0
- package/dist/cli/commands/orchestrator-service/restart.d.ts +5 -1
- package/dist/cli/commands/orchestrator-service/start.d.ts +5 -1
- package/dist/cli/commands/orchestrator-service/status.d.ts +4 -0
- package/dist/cli/commands/orchestrator-service/stop.d.ts +5 -1
- package/dist/cli/commands/orchestrator-service/uninstall.d.ts +6 -2
- package/dist/cli/commands/orchestrator-service/upgrade.d.ts +9 -0
- package/dist/cli/commands/shared/versioned-upgrade.d.ts +54 -2
- package/dist/cli/service/compose.d.ts +3 -2
- package/dist/cli/service/index.d.ts +7 -1
- package/dist/cli/service/instance/index-file.d.ts +30 -0
- package/dist/cli/service/instance/manifest.d.ts +30 -0
- package/dist/cli/service/instance/resolve.d.ts +59 -0
- package/dist/cli/service/instance/types.d.ts +55 -0
- package/dist/cli/service/launchd.d.ts +19 -1
- package/dist/cli/service/platform-detect.d.ts +29 -9
- package/dist/cli/service/systemd.d.ts +2 -1
- package/dist/cli/service/types.d.ts +28 -0
- package/dist/cli/service/windows.d.ts +2 -1
- package/dist/cli.js +1719 -1002
- package/dist/db/migrations/025_init_failure.d.ts +16 -0
- package/dist/db/types.d.ts +15 -0
- package/dist/diagnostics/checks/index.d.ts +2 -1
- package/dist/diagnostics/checks/scaler.d.ts +13 -0
- package/dist/diagnostics/types.d.ts +5 -2
- package/dist/metrics/prometheus.d.ts +32 -4
- package/dist/reporting/execution-tracker.d.ts +77 -6
- package/dist/scaler/failure-tracker.d.ts +46 -0
- package/dist/scaler/manager.d.ts +8 -0
- package/dist/server.js +570 -168
- package/dist/standalone.js +547 -156
- package/package.json +4 -4
- package/sbom.spdx.json +91 -36
package/dist/cli.js
CHANGED
|
@@ -25,8 +25,8 @@ import path from "node:path";
|
|
|
25
25
|
import archiver from "archiver";
|
|
26
26
|
import JSZip from "jszip";
|
|
27
27
|
import { fileURLToPath } from "node:url";
|
|
28
|
-
import { confirm, input, password, select } from "@inquirer/prompts";
|
|
29
28
|
import { pipeline } from "node:stream/promises";
|
|
29
|
+
import { confirm, input, password, select } from "@inquirer/prompts";
|
|
30
30
|
import "pg";
|
|
31
31
|
import { DASHBOARD_WRITE_OPERATIONS, DASHBOARD_WRITE_OPERATIONS_BY_NAME, DashboardWriteCategory, DashboardWriteSensitivity } from "@kici-dev/engine/protocol/dashboard-write-operations";
|
|
32
32
|
import { CLUSTER_NAME_FORMAT_MESSAGE } from "@kici-dev/engine/protocol/cluster-name";
|
|
@@ -1517,8 +1517,8 @@ var init_client = __esmMin((() => {
|
|
|
1517
1517
|
//#region src/db/migrations/001_initial.ts
|
|
1518
1518
|
init_client();
|
|
1519
1519
|
var _001_initial_exports = /* @__PURE__ */ __exportAll({
|
|
1520
|
-
down: () => down$
|
|
1521
|
-
up: () => up$
|
|
1520
|
+
down: () => down$24,
|
|
1521
|
+
up: () => up$24
|
|
1522
1522
|
});
|
|
1523
1523
|
/**
|
|
1524
1524
|
* Squashed initial migration -- creates the complete Orchestrator database schema.
|
|
@@ -2210,7 +2210,7 @@ const DDL_STATEMENTS = [
|
|
|
2210
2210
|
`ALTER TABLE ONLY public.held_runs
|
|
2211
2211
|
ADD CONSTRAINT held_runs_environment_id_fkey FOREIGN KEY (environment_id) REFERENCES public.environments(id);`
|
|
2212
2212
|
];
|
|
2213
|
-
async function up$
|
|
2213
|
+
async function up$24(db) {
|
|
2214
2214
|
for (const stmt of DDL_STATEMENTS) await sql.raw(stmt).execute(db);
|
|
2215
2215
|
await sql`
|
|
2216
2216
|
INSERT INTO cluster_meta (key, value)
|
|
@@ -2232,7 +2232,7 @@ async function up$23(db) {
|
|
|
2232
2232
|
* Rollback drops everything created above. Uses CASCADE on table drops to cut
|
|
2233
2233
|
* through the FK graph without relying on exact topological order.
|
|
2234
2234
|
*/
|
|
2235
|
-
async function down$
|
|
2235
|
+
async function down$24(db) {
|
|
2236
2236
|
for (const [trig, tbl] of [["source_secrets_change_trigger", "scoped_secrets"], ["sources_change_trigger", "sources"]]) await sql.raw(`DROP TRIGGER IF EXISTS ${trig} ON public.${tbl}`).execute(db);
|
|
2237
2237
|
for (const table of [
|
|
2238
2238
|
"workflow_registrations",
|
|
@@ -2279,8 +2279,8 @@ async function down$23(db) {
|
|
|
2279
2279
|
//#endregion
|
|
2280
2280
|
//#region src/db/migrations/002_config_versions_key_version.ts
|
|
2281
2281
|
var _002_config_versions_key_version_exports = /* @__PURE__ */ __exportAll({
|
|
2282
|
-
down: () => down$
|
|
2283
|
-
up: () => up$
|
|
2282
|
+
down: () => down$23,
|
|
2283
|
+
up: () => up$23
|
|
2284
2284
|
});
|
|
2285
2285
|
/**
|
|
2286
2286
|
* Add key_version column to config_versions so that sensitive-field encryption
|
|
@@ -2294,13 +2294,13 @@ var _002_config_versions_key_version_exports = /* @__PURE__ */ __exportAll({
|
|
|
2294
2294
|
* No index is needed: rotation does a full-table scan; reads are by
|
|
2295
2295
|
* `version` primary key and never filter on `key_version`.
|
|
2296
2296
|
*/
|
|
2297
|
-
async function up$
|
|
2297
|
+
async function up$23(db) {
|
|
2298
2298
|
await sql`
|
|
2299
2299
|
ALTER TABLE public.config_versions
|
|
2300
2300
|
ADD COLUMN key_version integer NOT NULL DEFAULT 1
|
|
2301
2301
|
`.execute(db);
|
|
2302
2302
|
}
|
|
2303
|
-
async function down$
|
|
2303
|
+
async function down$23(db) {
|
|
2304
2304
|
await sql`
|
|
2305
2305
|
ALTER TABLE public.config_versions
|
|
2306
2306
|
DROP COLUMN key_version
|
|
@@ -2309,8 +2309,8 @@ async function down$22(db) {
|
|
|
2309
2309
|
//#endregion
|
|
2310
2310
|
//#region src/db/migrations/003_access_log.ts
|
|
2311
2311
|
var _003_access_log_exports = /* @__PURE__ */ __exportAll({
|
|
2312
|
-
down: () => down$
|
|
2313
|
-
up: () => up$
|
|
2312
|
+
down: () => down$22,
|
|
2313
|
+
up: () => up$22
|
|
2314
2314
|
});
|
|
2315
2315
|
/**
|
|
2316
2316
|
* Access log: one row per read or orchestrator-admin mutation attributable
|
|
@@ -2334,7 +2334,7 @@ var _003_access_log_exports = /* @__PURE__ */ __exportAll({
|
|
|
2334
2334
|
* Retention is TTL-based via expires_at; packages/orchestrator/src/queue/
|
|
2335
2335
|
* cleanup.ts picks up the prune pass.
|
|
2336
2336
|
*/
|
|
2337
|
-
async function up$
|
|
2337
|
+
async function up$22(db) {
|
|
2338
2338
|
await sql`
|
|
2339
2339
|
CREATE TABLE public.access_log (
|
|
2340
2340
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
@@ -2372,28 +2372,28 @@ async function up$21(db) {
|
|
|
2372
2372
|
ON public.access_log (actor_type, actor_id, created_at DESC)
|
|
2373
2373
|
`.execute(db);
|
|
2374
2374
|
}
|
|
2375
|
-
async function down$
|
|
2375
|
+
async function down$22(db) {
|
|
2376
2376
|
await sql`DROP TABLE IF EXISTS public.access_log`.execute(db);
|
|
2377
2377
|
}
|
|
2378
2378
|
//#endregion
|
|
2379
2379
|
//#region src/db/migrations/004_rename_bundle_to_source.ts
|
|
2380
2380
|
var _004_rename_bundle_to_source_exports = /* @__PURE__ */ __exportAll({
|
|
2381
|
-
down: () => down$
|
|
2382
|
-
up: () => up$
|
|
2381
|
+
down: () => down$21,
|
|
2382
|
+
up: () => up$21
|
|
2383
2383
|
});
|
|
2384
|
-
async function up$
|
|
2384
|
+
async function up$21(db) {
|
|
2385
2385
|
await db.schema.alterTable("dispatch_queue").renameColumn("bundle_url", "source_tar_url").execute();
|
|
2386
2386
|
await db.schema.alterTable("dispatch_queue").renameColumn("bundle_hash", "source_tar_hash").execute();
|
|
2387
2387
|
}
|
|
2388
|
-
async function down$
|
|
2388
|
+
async function down$21(db) {
|
|
2389
2389
|
await db.schema.alterTable("dispatch_queue").renameColumn("source_tar_url", "bundle_url").execute();
|
|
2390
2390
|
await db.schema.alterTable("dispatch_queue").renameColumn("source_tar_hash", "bundle_hash").execute();
|
|
2391
2391
|
}
|
|
2392
2392
|
//#endregion
|
|
2393
2393
|
//#region src/db/migrations/005_cold_store_chunk_counter.ts
|
|
2394
2394
|
var _005_cold_store_chunk_counter_exports = /* @__PURE__ */ __exportAll({
|
|
2395
|
-
down: () => down$
|
|
2396
|
-
up: () => up$
|
|
2395
|
+
down: () => down$20,
|
|
2396
|
+
up: () => up$20
|
|
2397
2397
|
});
|
|
2398
2398
|
/**
|
|
2399
2399
|
* Cold-store chunk counter table.
|
|
@@ -2410,7 +2410,7 @@ var _005_cold_store_chunk_counter_exports = /* @__PURE__ */ __exportAll({
|
|
|
2410
2410
|
*
|
|
2411
2411
|
* sections 5 and 8.
|
|
2412
2412
|
*/
|
|
2413
|
-
async function up$
|
|
2413
|
+
async function up$20(db) {
|
|
2414
2414
|
await sql`
|
|
2415
2415
|
CREATE TABLE public.cold_store_chunk_counts (
|
|
2416
2416
|
db TEXT NOT NULL,
|
|
@@ -2428,14 +2428,14 @@ async function up$19(db) {
|
|
|
2428
2428
|
ON public.cold_store_chunk_counts (db, table_name)
|
|
2429
2429
|
`.execute(db);
|
|
2430
2430
|
}
|
|
2431
|
-
async function down$
|
|
2431
|
+
async function down$20(db) {
|
|
2432
2432
|
await sql`DROP TABLE IF EXISTS public.cold_store_chunk_counts`.execute(db);
|
|
2433
2433
|
}
|
|
2434
2434
|
//#endregion
|
|
2435
2435
|
//#region src/db/migrations/006_runs_jobs_steps_archived_at.ts
|
|
2436
2436
|
var _006_runs_jobs_steps_archived_at_exports = /* @__PURE__ */ __exportAll({
|
|
2437
|
-
down: () => down$
|
|
2438
|
-
up: () => up$
|
|
2437
|
+
down: () => down$19,
|
|
2438
|
+
up: () => up$19
|
|
2439
2439
|
});
|
|
2440
2440
|
/**
|
|
2441
2441
|
* `execution_runs` / `execution_jobs` / `execution_steps` cold-store
|
|
@@ -2471,7 +2471,7 @@ var _006_runs_jobs_steps_archived_at_exports = /* @__PURE__ */ __exportAll({
|
|
|
2471
2471
|
* - `idx_execution_jobs_routing_key_created (routing_key, created_at)`
|
|
2472
2472
|
* - `idx_execution_steps_routing_key_created (routing_key, created_at)`
|
|
2473
2473
|
*/
|
|
2474
|
-
async function up$
|
|
2474
|
+
async function up$19(db) {
|
|
2475
2475
|
await sql`
|
|
2476
2476
|
ALTER TABLE public.execution_runs
|
|
2477
2477
|
ADD COLUMN archived_at TIMESTAMPTZ NULL,
|
|
@@ -2516,7 +2516,7 @@ async function up$18(db) {
|
|
|
2516
2516
|
ON public.execution_steps (routing_key, created_at)
|
|
2517
2517
|
`.execute(db);
|
|
2518
2518
|
}
|
|
2519
|
-
async function down$
|
|
2519
|
+
async function down$19(db) {
|
|
2520
2520
|
await sql`DROP INDEX IF EXISTS public.idx_execution_steps_routing_key_created`.execute(db);
|
|
2521
2521
|
await sql`
|
|
2522
2522
|
ALTER TABLE public.execution_steps
|
|
@@ -2541,8 +2541,8 @@ async function down$18(db) {
|
|
|
2541
2541
|
//#endregion
|
|
2542
2542
|
//#region src/db/migrations/007_audit_logs_archived_at.ts
|
|
2543
2543
|
var _007_audit_logs_archived_at_exports = /* @__PURE__ */ __exportAll({
|
|
2544
|
-
down: () => down$
|
|
2545
|
-
up: () => up$
|
|
2544
|
+
down: () => down$18,
|
|
2545
|
+
up: () => up$18
|
|
2546
2546
|
});
|
|
2547
2547
|
/**
|
|
2548
2548
|
* `secret_audit_log` and `access_log` cold-store schema additions, plus
|
|
@@ -2582,7 +2582,7 @@ var _007_audit_logs_archived_at_exports = /* @__PURE__ */ __exportAll({
|
|
|
2582
2582
|
* `down()` would not have meaningful retention bounds. Acceptable for
|
|
2583
2583
|
* staging.
|
|
2584
2584
|
*/
|
|
2585
|
-
async function up$
|
|
2585
|
+
async function up$18(db) {
|
|
2586
2586
|
await sql`
|
|
2587
2587
|
ALTER TABLE public.secret_audit_log
|
|
2588
2588
|
ADD COLUMN archived_at TIMESTAMPTZ NULL,
|
|
@@ -2603,7 +2603,7 @@ async function up$17(db) {
|
|
|
2603
2603
|
DROP COLUMN IF EXISTS expires_at
|
|
2604
2604
|
`.execute(db);
|
|
2605
2605
|
}
|
|
2606
|
-
async function down$
|
|
2606
|
+
async function down$18(db) {
|
|
2607
2607
|
await sql`
|
|
2608
2608
|
ALTER TABLE public.access_log
|
|
2609
2609
|
ADD COLUMN expires_at TIMESTAMPTZ NOT NULL DEFAULT (now() + INTERVAL '90 days')
|
|
@@ -2631,8 +2631,8 @@ async function down$17(db) {
|
|
|
2631
2631
|
//#endregion
|
|
2632
2632
|
//#region src/db/migrations/008_event_log_archived_at.ts
|
|
2633
2633
|
var _008_event_log_archived_at_exports = /* @__PURE__ */ __exportAll({
|
|
2634
|
-
down: () => down$
|
|
2635
|
-
up: () => up$
|
|
2634
|
+
down: () => down$17,
|
|
2635
|
+
up: () => up$17
|
|
2636
2636
|
});
|
|
2637
2637
|
/**
|
|
2638
2638
|
* `event_log` cold-store schema additions plus removal of the
|
|
@@ -2670,7 +2670,7 @@ var _008_event_log_archived_at_exports = /* @__PURE__ */ __exportAll({
|
|
|
2670
2670
|
* — best effort; rows inserted between `up()` and a hypothetical
|
|
2671
2671
|
* `down()` would not have meaningful retention bounds.
|
|
2672
2672
|
*/
|
|
2673
|
-
async function up$
|
|
2673
|
+
async function up$17(db) {
|
|
2674
2674
|
await sql`
|
|
2675
2675
|
ALTER TABLE public.event_log
|
|
2676
2676
|
ADD COLUMN archived_at TIMESTAMPTZ NULL,
|
|
@@ -2686,7 +2686,7 @@ async function up$16(db) {
|
|
|
2686
2686
|
DROP COLUMN IF EXISTS expires_at
|
|
2687
2687
|
`.execute(db);
|
|
2688
2688
|
}
|
|
2689
|
-
async function down$
|
|
2689
|
+
async function down$17(db) {
|
|
2690
2690
|
await sql`
|
|
2691
2691
|
ALTER TABLE public.event_log
|
|
2692
2692
|
ADD COLUMN expires_at TIMESTAMPTZ NOT NULL DEFAULT (now() + INTERVAL '30 days')
|
|
@@ -2705,8 +2705,8 @@ async function down$16(db) {
|
|
|
2705
2705
|
//#endregion
|
|
2706
2706
|
//#region src/db/migrations/009_access_log_trigram.ts
|
|
2707
2707
|
var _009_access_log_trigram_exports = /* @__PURE__ */ __exportAll({
|
|
2708
|
-
down: () => down$
|
|
2709
|
-
up: () => up$
|
|
2708
|
+
down: () => down$16,
|
|
2709
|
+
up: () => up$16
|
|
2710
2710
|
});
|
|
2711
2711
|
/**
|
|
2712
2712
|
* Trigram (pg_trgm) index on access_log.error_message for the federated
|
|
@@ -2719,7 +2719,7 @@ var _009_access_log_trigram_exports = /* @__PURE__ */ __exportAll({
|
|
|
2719
2719
|
* EXISTS` are both safe to re-run. No CONCURRENTLY because Kysely runs
|
|
2720
2720
|
* migrations inside a transaction; the lock is brief on a sampled table.
|
|
2721
2721
|
*/
|
|
2722
|
-
async function up$
|
|
2722
|
+
async function up$16(db) {
|
|
2723
2723
|
await sql`CREATE EXTENSION IF NOT EXISTS pg_trgm`.execute(db);
|
|
2724
2724
|
await sql`
|
|
2725
2725
|
CREATE INDEX IF NOT EXISTS access_log_error_message_trgm_idx
|
|
@@ -2728,14 +2728,14 @@ async function up$15(db) {
|
|
|
2728
2728
|
WHERE error_message IS NOT NULL
|
|
2729
2729
|
`.execute(db);
|
|
2730
2730
|
}
|
|
2731
|
-
async function down$
|
|
2731
|
+
async function down$16(db) {
|
|
2732
2732
|
await sql`DROP INDEX IF EXISTS public.access_log_error_message_trgm_idx`.execute(db);
|
|
2733
2733
|
}
|
|
2734
2734
|
//#endregion
|
|
2735
2735
|
//#region src/db/migrations/010_cold_store_chunks.ts
|
|
2736
2736
|
var _010_cold_store_chunks_exports = /* @__PURE__ */ __exportAll({
|
|
2737
|
-
down: () => down$
|
|
2738
|
-
up: () => up$
|
|
2737
|
+
down: () => down$15,
|
|
2738
|
+
up: () => up$15
|
|
2739
2739
|
});
|
|
2740
2740
|
/**
|
|
2741
2741
|
* Cold-store chunk index — Phase 2 (cold-store purge).
|
|
@@ -2767,7 +2767,7 @@ var _010_cold_store_chunks_exports = /* @__PURE__ */ __exportAll({
|
|
|
2767
2767
|
* forever. Adapters that don't opt into per-bucket archival via
|
|
2768
2768
|
* `coldTtlDays` don't insert here either.
|
|
2769
2769
|
*/
|
|
2770
|
-
async function up$
|
|
2770
|
+
async function up$15(db) {
|
|
2771
2771
|
await sql`
|
|
2772
2772
|
CREATE TABLE public.cold_store_chunks (
|
|
2773
2773
|
db TEXT NOT NULL,
|
|
@@ -2794,14 +2794,14 @@ async function up$14(db) {
|
|
|
2794
2794
|
ON public.cold_store_chunks (db, table_name, tenant_id, archived_at DESC)
|
|
2795
2795
|
`.execute(db);
|
|
2796
2796
|
}
|
|
2797
|
-
async function down$
|
|
2797
|
+
async function down$15(db) {
|
|
2798
2798
|
await sql`DROP TABLE IF EXISTS public.cold_store_chunks`.execute(db);
|
|
2799
2799
|
}
|
|
2800
2800
|
//#endregion
|
|
2801
2801
|
//#region src/db/migrations/011_drop_source_secrets_notify.ts
|
|
2802
2802
|
var _011_drop_source_secrets_notify_exports = /* @__PURE__ */ __exportAll({
|
|
2803
|
-
down: () => down$
|
|
2804
|
-
up: () => up$
|
|
2803
|
+
down: () => down$14,
|
|
2804
|
+
up: () => up$14
|
|
2805
2805
|
});
|
|
2806
2806
|
/**
|
|
2807
2807
|
* Drop the `source_secrets_change_trigger` and the
|
|
@@ -2820,11 +2820,11 @@ var _011_drop_source_secrets_notify_exports = /* @__PURE__ */ __exportAll({
|
|
|
2820
2820
|
* in `001_initial.ts`. They wake up no consumer until the `WebhookSecretManager`
|
|
2821
2821
|
* is restored, so this migration is safe to roll back.
|
|
2822
2822
|
*/
|
|
2823
|
-
async function up$
|
|
2823
|
+
async function up$14(db) {
|
|
2824
2824
|
await sql`DROP TRIGGER IF EXISTS source_secrets_change_trigger ON public.scoped_secrets`.execute(db);
|
|
2825
2825
|
await sql`DROP FUNCTION IF EXISTS public.notify_source_secrets_change() CASCADE`.execute(db);
|
|
2826
2826
|
}
|
|
2827
|
-
async function down$
|
|
2827
|
+
async function down$14(db) {
|
|
2828
2828
|
await sql`
|
|
2829
2829
|
CREATE OR REPLACE FUNCTION public.notify_source_secrets_change() RETURNS trigger
|
|
2830
2830
|
LANGUAGE plpgsql
|
|
@@ -2863,8 +2863,8 @@ async function down$13(db) {
|
|
|
2863
2863
|
//#endregion
|
|
2864
2864
|
//#region src/db/migrations/012_peer_credentials_active_uniq.ts
|
|
2865
2865
|
var _012_peer_credentials_active_uniq_exports = /* @__PURE__ */ __exportAll({
|
|
2866
|
-
down: () => down$
|
|
2867
|
-
up: () => up$
|
|
2866
|
+
down: () => down$13,
|
|
2867
|
+
up: () => up$13
|
|
2868
2868
|
});
|
|
2869
2869
|
/**
|
|
2870
2870
|
* Add a partial unique index on `peer_credentials (instance_id) WHERE
|
|
@@ -2890,7 +2890,7 @@ var _012_peer_credentials_active_uniq_exports = /* @__PURE__ */ __exportAll({
|
|
|
2890
2890
|
* `down()` only drops the index; it does NOT undo the dedupe (there's no
|
|
2891
2891
|
* safe way to recreate revoked rows, and the dedupe is monotonic).
|
|
2892
2892
|
*/
|
|
2893
|
-
async function up$
|
|
2893
|
+
async function up$13(db) {
|
|
2894
2894
|
await sql`
|
|
2895
2895
|
UPDATE public.peer_credentials
|
|
2896
2896
|
SET revoked_at = NOW()
|
|
@@ -2908,14 +2908,14 @@ async function up$12(db) {
|
|
|
2908
2908
|
WHERE revoked_at IS NULL
|
|
2909
2909
|
`.execute(db);
|
|
2910
2910
|
}
|
|
2911
|
-
async function down$
|
|
2911
|
+
async function down$13(db) {
|
|
2912
2912
|
await sql`DROP INDEX IF EXISTS public.peer_credentials_active_uniq`.execute(db);
|
|
2913
2913
|
}
|
|
2914
2914
|
//#endregion
|
|
2915
2915
|
//#region src/db/migrations/013_execution_log_bytes.ts
|
|
2916
2916
|
var _013_execution_log_bytes_exports = /* @__PURE__ */ __exportAll({
|
|
2917
|
-
down: () => down$
|
|
2918
|
-
up: () => up$
|
|
2917
|
+
down: () => down$12,
|
|
2918
|
+
up: () => up$12
|
|
2919
2919
|
});
|
|
2920
2920
|
/**
|
|
2921
2921
|
* Add `log_bytes BIGINT NOT NULL DEFAULT 0` columns to `execution_runs` and
|
|
@@ -2934,7 +2934,7 @@ var _013_execution_log_bytes_exports = /* @__PURE__ */ __exportAll({
|
|
|
2934
2934
|
*
|
|
2935
2935
|
* Idempotent (`ADD COLUMN IF NOT EXISTS`).
|
|
2936
2936
|
*/
|
|
2937
|
-
async function up$
|
|
2937
|
+
async function up$12(db) {
|
|
2938
2938
|
await sql`
|
|
2939
2939
|
ALTER TABLE public.execution_runs
|
|
2940
2940
|
ADD COLUMN IF NOT EXISTS log_bytes BIGINT NOT NULL DEFAULT 0
|
|
@@ -2944,15 +2944,15 @@ async function up$11(db) {
|
|
|
2944
2944
|
ADD COLUMN IF NOT EXISTS log_bytes BIGINT NOT NULL DEFAULT 0
|
|
2945
2945
|
`.execute(db);
|
|
2946
2946
|
}
|
|
2947
|
-
async function down$
|
|
2947
|
+
async function down$12(db) {
|
|
2948
2948
|
await sql`ALTER TABLE public.execution_runs DROP COLUMN IF EXISTS log_bytes`.execute(db);
|
|
2949
2949
|
await sql`ALTER TABLE public.execution_jobs DROP COLUMN IF EXISTS log_bytes`.execute(db);
|
|
2950
2950
|
}
|
|
2951
2951
|
//#endregion
|
|
2952
2952
|
//#region src/db/migrations/014_kici_events_lease_retry.ts
|
|
2953
2953
|
var _014_kici_events_lease_retry_exports = /* @__PURE__ */ __exportAll({
|
|
2954
|
-
down: () => down$
|
|
2955
|
-
up: () => up$
|
|
2954
|
+
down: () => down$11,
|
|
2955
|
+
up: () => up$11
|
|
2956
2956
|
});
|
|
2957
2957
|
/**
|
|
2958
2958
|
* Add lease + retry + DLQ columns to `kici_events` so the EventRouter can
|
|
@@ -2986,7 +2986,7 @@ var _014_kici_events_lease_retry_exports = /* @__PURE__ */ __exportAll({
|
|
|
2986
2986
|
*
|
|
2987
2987
|
* Idempotent (`ADD COLUMN IF NOT EXISTS` + `CREATE INDEX IF NOT EXISTS`).
|
|
2988
2988
|
*/
|
|
2989
|
-
async function up$
|
|
2989
|
+
async function up$11(db) {
|
|
2990
2990
|
await sql`
|
|
2991
2991
|
ALTER TABLE public.kici_events
|
|
2992
2992
|
ADD COLUMN IF NOT EXISTS claimed_at TIMESTAMPTZ,
|
|
@@ -3017,7 +3017,7 @@ async function up$10(db) {
|
|
|
3017
3017
|
WHERE dlq_at IS NOT NULL
|
|
3018
3018
|
`.execute(db);
|
|
3019
3019
|
}
|
|
3020
|
-
async function down$
|
|
3020
|
+
async function down$11(db) {
|
|
3021
3021
|
await sql`DROP INDEX IF EXISTS public.idx_kici_events_dlq`.execute(db);
|
|
3022
3022
|
await sql`DROP INDEX IF EXISTS public.idx_kici_events_lease_expired`.execute(db);
|
|
3023
3023
|
await sql`DROP INDEX IF EXISTS public.idx_kici_events_retry_due`.execute(db);
|
|
@@ -3035,8 +3035,8 @@ async function down$10(db) {
|
|
|
3035
3035
|
//#endregion
|
|
3036
3036
|
//#region src/db/migrations/015_org_settings_customer_scoped.ts
|
|
3037
3037
|
var _015_org_settings_customer_scoped_exports = /* @__PURE__ */ __exportAll({
|
|
3038
|
-
down: () => down$
|
|
3039
|
-
up: () => up$
|
|
3038
|
+
down: () => down$10,
|
|
3039
|
+
up: () => up$10
|
|
3040
3040
|
});
|
|
3041
3041
|
/**
|
|
3042
3042
|
* Org-scope `org_settings` and qualify each glob entry by source.
|
|
@@ -3064,7 +3064,7 @@ var _015_org_settings_customer_scoped_exports = /* @__PURE__ */ __exportAll({
|
|
|
3064
3064
|
* Idempotent: a re-run on an already-migrated DB sees `customer_id` exists
|
|
3065
3065
|
* and the list columns are already jsonb, so it is a no-op.
|
|
3066
3066
|
*/
|
|
3067
|
-
async function up$
|
|
3067
|
+
async function up$10(db) {
|
|
3068
3068
|
if ((await sql`
|
|
3069
3069
|
SELECT EXISTS (
|
|
3070
3070
|
SELECT 1 FROM information_schema.columns
|
|
@@ -3189,7 +3189,7 @@ async function up$9(db) {
|
|
|
3189
3189
|
await sql`DROP TABLE _org_settings_merged`.execute(db);
|
|
3190
3190
|
await sql`DROP TABLE _org_settings_stage`.execute(db);
|
|
3191
3191
|
}
|
|
3192
|
-
async function down$
|
|
3192
|
+
async function down$10(db) {
|
|
3193
3193
|
if (!(await sql`
|
|
3194
3194
|
SELECT EXISTS (
|
|
3195
3195
|
SELECT 1 FROM information_schema.columns
|
|
@@ -3214,8 +3214,8 @@ async function down$9(db) {
|
|
|
3214
3214
|
//#endregion
|
|
3215
3215
|
//#region src/db/migrations/016_org_settings_allow_http_npm.ts
|
|
3216
3216
|
var _016_org_settings_allow_http_npm_exports = /* @__PURE__ */ __exportAll({
|
|
3217
|
-
down: () => down$
|
|
3218
|
-
up: () => up$
|
|
3217
|
+
down: () => down$9,
|
|
3218
|
+
up: () => up$9
|
|
3219
3219
|
});
|
|
3220
3220
|
/**
|
|
3221
3221
|
* Add `org_settings.allow_http_npm_registries boolean NOT NULL DEFAULT false`.
|
|
@@ -3228,7 +3228,7 @@ var _016_org_settings_allow_http_npm_exports = /* @__PURE__ */ __exportAll({
|
|
|
3228
3228
|
*
|
|
3229
3229
|
* Idempotent: a re-run on a DB that already has the column is a no-op.
|
|
3230
3230
|
*/
|
|
3231
|
-
async function up$
|
|
3231
|
+
async function up$9(db) {
|
|
3232
3232
|
if ((await sql`
|
|
3233
3233
|
SELECT EXISTS (
|
|
3234
3234
|
SELECT 1 FROM information_schema.columns
|
|
@@ -3242,7 +3242,7 @@ async function up$8(db) {
|
|
|
3242
3242
|
ADD COLUMN allow_http_npm_registries boolean NOT NULL DEFAULT false
|
|
3243
3243
|
`.execute(db);
|
|
3244
3244
|
}
|
|
3245
|
-
async function down$
|
|
3245
|
+
async function down$9(db) {
|
|
3246
3246
|
await sql`
|
|
3247
3247
|
ALTER TABLE public.org_settings DROP COLUMN IF EXISTS allow_http_npm_registries
|
|
3248
3248
|
`.execute(db);
|
|
@@ -3250,8 +3250,8 @@ async function down$8(db) {
|
|
|
3250
3250
|
//#endregion
|
|
3251
3251
|
//#region src/db/migrations/017_org_id_widen.ts
|
|
3252
3252
|
var _017_org_id_widen_exports = /* @__PURE__ */ __exportAll({
|
|
3253
|
-
down: () => down$
|
|
3254
|
-
up: () => up$
|
|
3253
|
+
down: () => down$8,
|
|
3254
|
+
up: () => up$8
|
|
3255
3255
|
});
|
|
3256
3256
|
/**
|
|
3257
3257
|
* Widen every orchestrator-side `org_id varchar(12)` column to
|
|
@@ -3291,17 +3291,17 @@ const ORG_ID_TABLES$1 = [
|
|
|
3291
3291
|
"held_runs",
|
|
3292
3292
|
"scoped_secrets"
|
|
3293
3293
|
];
|
|
3294
|
-
async function up$
|
|
3294
|
+
async function up$8(db) {
|
|
3295
3295
|
for (const table of ORG_ID_TABLES$1) await sql.raw(`ALTER TABLE public.${table} ALTER COLUMN org_id TYPE varchar(16)`).execute(db);
|
|
3296
3296
|
}
|
|
3297
|
-
async function down$
|
|
3297
|
+
async function down$8(db) {
|
|
3298
3298
|
for (const table of ORG_ID_TABLES$1) await sql.raw(`ALTER TABLE public.${table} ALTER COLUMN org_id TYPE varchar(12)`).execute(db);
|
|
3299
3299
|
}
|
|
3300
3300
|
//#endregion
|
|
3301
3301
|
//#region src/db/migrations/018_org_id_prefix_backfill.ts
|
|
3302
3302
|
var _018_org_id_prefix_backfill_exports = /* @__PURE__ */ __exportAll({
|
|
3303
|
-
down: () => down$
|
|
3304
|
-
up: () => up$
|
|
3303
|
+
down: () => down$7,
|
|
3304
|
+
up: () => up$7
|
|
3305
3305
|
});
|
|
3306
3306
|
/**
|
|
3307
3307
|
* Prefix every orchestrator-side tenant string with `org_` to align
|
|
@@ -3346,19 +3346,19 @@ const CUSTOMER_ID_TABLES = [
|
|
|
3346
3346
|
"workflow_registrations",
|
|
3347
3347
|
"org_settings"
|
|
3348
3348
|
];
|
|
3349
|
-
async function up$
|
|
3349
|
+
async function up$7(db) {
|
|
3350
3350
|
for (const table of ORG_ID_TABLES) await sql.raw(`UPDATE public.${table} SET org_id = 'org_' || org_id WHERE org_id <> 'kici-admin' AND org_id NOT LIKE 'org\\_%' ESCAPE '\\'`).execute(db);
|
|
3351
3351
|
for (const table of CUSTOMER_ID_TABLES) await sql.raw(`UPDATE public.${table} SET customer_id = 'org_' || customer_id WHERE customer_id <> 'kici-admin' AND customer_id NOT LIKE 'org\\_%' ESCAPE '\\'`).execute(db);
|
|
3352
3352
|
}
|
|
3353
|
-
async function down$
|
|
3353
|
+
async function down$7(db) {
|
|
3354
3354
|
for (const table of CUSTOMER_ID_TABLES) await sql.raw(`UPDATE public.${table} SET customer_id = substring(customer_id from 5) WHERE customer_id LIKE 'org\\_%' ESCAPE '\\'`).execute(db);
|
|
3355
3355
|
for (const table of ORG_ID_TABLES) await sql.raw(`UPDATE public.${table} SET org_id = substring(org_id from 5) WHERE org_id LIKE 'org\\_%' ESCAPE '\\'`).execute(db);
|
|
3356
3356
|
}
|
|
3357
3357
|
//#endregion
|
|
3358
3358
|
//#region src/db/migrations/019_generic_sources_change_notify.ts
|
|
3359
3359
|
var _019_generic_sources_change_notify_exports = /* @__PURE__ */ __exportAll({
|
|
3360
|
-
down: () => down$
|
|
3361
|
-
up: () => up$
|
|
3360
|
+
down: () => down$6,
|
|
3361
|
+
up: () => up$6
|
|
3362
3362
|
});
|
|
3363
3363
|
/**
|
|
3364
3364
|
* Add a Postgres trigger on `generic_webhook_sources` that emits
|
|
@@ -3381,7 +3381,7 @@ var _019_generic_sources_change_notify_exports = /* @__PURE__ */ __exportAll({
|
|
|
3381
3381
|
* (`notify_sources_change()` + `sources_change_trigger`, defined in
|
|
3382
3382
|
* `001_initial.ts`).
|
|
3383
3383
|
*/
|
|
3384
|
-
async function up$
|
|
3384
|
+
async function up$6(db) {
|
|
3385
3385
|
await sql`
|
|
3386
3386
|
CREATE FUNCTION public.notify_generic_sources_change() RETURNS trigger
|
|
3387
3387
|
LANGUAGE plpgsql
|
|
@@ -3402,15 +3402,15 @@ async function up$5(db) {
|
|
|
3402
3402
|
FOR EACH ROW EXECUTE FUNCTION public.notify_generic_sources_change()
|
|
3403
3403
|
`.execute(db);
|
|
3404
3404
|
}
|
|
3405
|
-
async function down$
|
|
3405
|
+
async function down$6(db) {
|
|
3406
3406
|
await sql`DROP TRIGGER IF EXISTS generic_sources_change_trigger ON public.generic_webhook_sources`.execute(db);
|
|
3407
3407
|
await sql`DROP FUNCTION IF EXISTS public.notify_generic_sources_change()`.execute(db);
|
|
3408
3408
|
}
|
|
3409
3409
|
//#endregion
|
|
3410
3410
|
//#region src/db/migrations/020_org_settings_dashboard_write_policy.ts
|
|
3411
3411
|
var _020_org_settings_dashboard_write_policy_exports = /* @__PURE__ */ __exportAll({
|
|
3412
|
-
down: () => down$
|
|
3413
|
-
up: () => up$
|
|
3412
|
+
down: () => down$5,
|
|
3413
|
+
up: () => up$5
|
|
3414
3414
|
});
|
|
3415
3415
|
/**
|
|
3416
3416
|
* Add `org_settings.dashboard_write_policy jsonb NOT NULL DEFAULT '{}'`.
|
|
@@ -3425,7 +3425,7 @@ var _020_org_settings_dashboard_write_policy_exports = /* @__PURE__ */ __exportA
|
|
|
3425
3425
|
*
|
|
3426
3426
|
* Idempotent: a re-run on a DB that already has the column is a no-op.
|
|
3427
3427
|
*/
|
|
3428
|
-
async function up$
|
|
3428
|
+
async function up$5(db) {
|
|
3429
3429
|
if ((await sql`
|
|
3430
3430
|
SELECT EXISTS (
|
|
3431
3431
|
SELECT 1 FROM information_schema.columns
|
|
@@ -3439,7 +3439,7 @@ async function up$4(db) {
|
|
|
3439
3439
|
ADD COLUMN dashboard_write_policy jsonb NOT NULL DEFAULT '{}'::jsonb
|
|
3440
3440
|
`.execute(db);
|
|
3441
3441
|
}
|
|
3442
|
-
async function down$
|
|
3442
|
+
async function down$5(db) {
|
|
3443
3443
|
await sql`
|
|
3444
3444
|
ALTER TABLE public.org_settings DROP COLUMN IF EXISTS dashboard_write_policy
|
|
3445
3445
|
`.execute(db);
|
|
@@ -3447,8 +3447,8 @@ async function down$4(db) {
|
|
|
3447
3447
|
//#endregion
|
|
3448
3448
|
//#region src/db/migrations/021_check_run_tracking.ts
|
|
3449
3449
|
var _021_check_run_tracking_exports = /* @__PURE__ */ __exportAll({
|
|
3450
|
-
down: () => down$
|
|
3451
|
-
up: () => up$
|
|
3450
|
+
down: () => down$4,
|
|
3451
|
+
up: () => up$4
|
|
3452
3452
|
});
|
|
3453
3453
|
/**
|
|
3454
3454
|
* Add `check_run_tracking` table for HA-safe check-run state persistence.
|
|
@@ -3472,7 +3472,7 @@ var _021_check_run_tracking_exports = /* @__PURE__ */ __exportAll({
|
|
|
3472
3472
|
*
|
|
3473
3473
|
* Idempotent: a re-run on a DB that already has the table is a no-op.
|
|
3474
3474
|
*/
|
|
3475
|
-
async function up$
|
|
3475
|
+
async function up$4(db) {
|
|
3476
3476
|
if ((await sql`
|
|
3477
3477
|
SELECT EXISTS (
|
|
3478
3478
|
SELECT 1 FROM information_schema.tables
|
|
@@ -3503,14 +3503,14 @@ async function up$3(db) {
|
|
|
3503
3503
|
WHERE run_id IS NOT NULL
|
|
3504
3504
|
`.execute(db);
|
|
3505
3505
|
}
|
|
3506
|
-
async function down$
|
|
3506
|
+
async function down$4(db) {
|
|
3507
3507
|
await sql`DROP TABLE IF EXISTS public.check_run_tracking`.execute(db);
|
|
3508
3508
|
}
|
|
3509
3509
|
//#endregion
|
|
3510
3510
|
//#region src/db/migrations/022_scaler_manager_state.ts
|
|
3511
3511
|
var _022_scaler_manager_state_exports = /* @__PURE__ */ __exportAll({
|
|
3512
|
-
down: () => down$
|
|
3513
|
-
up: () => up$
|
|
3512
|
+
down: () => down$3,
|
|
3513
|
+
up: () => up$3
|
|
3514
3514
|
});
|
|
3515
3515
|
/**
|
|
3516
3516
|
* Add three tables persisting `ScalerManager` per-coord state:
|
|
@@ -3537,7 +3537,7 @@ var _022_scaler_manager_state_exports = /* @__PURE__ */ __exportAll({
|
|
|
3537
3537
|
* Idempotent: a re-run on a DB that already has any of these tables
|
|
3538
3538
|
* leaves the existing one alone.
|
|
3539
3539
|
*/
|
|
3540
|
-
async function up$
|
|
3540
|
+
async function up$3(db) {
|
|
3541
3541
|
const tableExists = async (name) => {
|
|
3542
3542
|
return (await sql`
|
|
3543
3543
|
SELECT EXISTS (
|
|
@@ -3587,7 +3587,7 @@ async function up$2(db) {
|
|
|
3587
3587
|
`.execute(db);
|
|
3588
3588
|
}
|
|
3589
3589
|
}
|
|
3590
|
-
async function down$
|
|
3590
|
+
async function down$3(db) {
|
|
3591
3591
|
await sql`DROP TABLE IF EXISTS public.scaler_reservations`.execute(db);
|
|
3592
3592
|
await sql`DROP TABLE IF EXISTS public.scaler_agent_jobs`.execute(db);
|
|
3593
3593
|
await sql`DROP TABLE IF EXISTS public.scaler_spawning_agents`.execute(db);
|
|
@@ -3595,8 +3595,8 @@ async function down$2(db) {
|
|
|
3595
3595
|
//#endregion
|
|
3596
3596
|
//#region src/db/migrations/023_dispatch_queue_recovery_deadline.ts
|
|
3597
3597
|
var _023_dispatch_queue_recovery_deadline_exports = /* @__PURE__ */ __exportAll({
|
|
3598
|
-
down: () => down$
|
|
3599
|
-
up: () => up$
|
|
3598
|
+
down: () => down$2,
|
|
3599
|
+
up: () => up$2
|
|
3600
3600
|
});
|
|
3601
3601
|
/**
|
|
3602
3602
|
* Add `dispatch_queue.recovery_deadline TIMESTAMPTZ` and
|
|
@@ -3620,7 +3620,7 @@ var _023_dispatch_queue_recovery_deadline_exports = /* @__PURE__ */ __exportAll(
|
|
|
3620
3620
|
* Idempotent: re-running on a DB that already has either column is a
|
|
3621
3621
|
* no-op.
|
|
3622
3622
|
*/
|
|
3623
|
-
async function up$
|
|
3623
|
+
async function up$2(db) {
|
|
3624
3624
|
const colExists = async (name) => {
|
|
3625
3625
|
return (await sql`
|
|
3626
3626
|
SELECT EXISTS (
|
|
@@ -3645,7 +3645,7 @@ async function up$1(db) {
|
|
|
3645
3645
|
WHERE recovery_deadline IS NOT NULL
|
|
3646
3646
|
`.execute(db);
|
|
3647
3647
|
}
|
|
3648
|
-
async function down$
|
|
3648
|
+
async function down$2(db) {
|
|
3649
3649
|
await sql`DROP INDEX IF EXISTS public.idx_dispatch_queue_recovery_deadline`.execute(db);
|
|
3650
3650
|
await sql`
|
|
3651
3651
|
ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS recovery_agent_id
|
|
@@ -3657,8 +3657,8 @@ async function down$1(db) {
|
|
|
3657
3657
|
//#endregion
|
|
3658
3658
|
//#region src/db/migrations/024_dispatch_queue_provisioning_error.ts
|
|
3659
3659
|
var _024_dispatch_queue_provisioning_error_exports = /* @__PURE__ */ __exportAll({
|
|
3660
|
-
down: () => down,
|
|
3661
|
-
up: () => up
|
|
3660
|
+
down: () => down$1,
|
|
3661
|
+
up: () => up$1
|
|
3662
3662
|
});
|
|
3663
3663
|
/**
|
|
3664
3664
|
* Add `dispatch_queue.last_provisioning_error TEXT` recording the most
|
|
@@ -3674,7 +3674,7 @@ var _024_dispatch_queue_provisioning_error_exports = /* @__PURE__ */ __exportAll
|
|
|
3674
3674
|
*
|
|
3675
3675
|
* Idempotent: re-running on a DB that already has the column is a no-op.
|
|
3676
3676
|
*/
|
|
3677
|
-
async function up(db) {
|
|
3677
|
+
async function up$1(db) {
|
|
3678
3678
|
const colExists = async (name) => {
|
|
3679
3679
|
return (await sql`
|
|
3680
3680
|
SELECT EXISTS (
|
|
@@ -3690,12 +3690,58 @@ async function up(db) {
|
|
|
3690
3690
|
ADD COLUMN last_provisioning_error TEXT
|
|
3691
3691
|
`.execute(db);
|
|
3692
3692
|
}
|
|
3693
|
-
async function down(db) {
|
|
3693
|
+
async function down$1(db) {
|
|
3694
3694
|
await sql`
|
|
3695
3695
|
ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS last_provisioning_error
|
|
3696
3696
|
`.execute(db);
|
|
3697
3697
|
}
|
|
3698
3698
|
//#endregion
|
|
3699
|
+
//#region src/db/migrations/025_init_failure.ts
|
|
3700
|
+
var _025_init_failure_exports = /* @__PURE__ */ __exportAll({
|
|
3701
|
+
down: () => down,
|
|
3702
|
+
up: () => up
|
|
3703
|
+
});
|
|
3704
|
+
/**
|
|
3705
|
+
* Add `init_failure jsonb` columns to `execution_runs` and `execution_jobs`.
|
|
3706
|
+
*
|
|
3707
|
+
* Presence of this column on a row means the run/job never executed a step
|
|
3708
|
+
* because of an init-phase failure; absence (NULL) means a normal run.
|
|
3709
|
+
* Shape on the wire is `InitFailure` from `@kici-dev/engine`. The dashboard
|
|
3710
|
+
* reads this column directly so it can render the right banner without
|
|
3711
|
+
* round-tripping to the orchestrator (which may be offline).
|
|
3712
|
+
*
|
|
3713
|
+
* Idempotent: re-running on a DB that already has either column is a no-op
|
|
3714
|
+
* for that column.
|
|
3715
|
+
*/
|
|
3716
|
+
async function up(db) {
|
|
3717
|
+
const colExists = async (table, name) => {
|
|
3718
|
+
return (await sql`
|
|
3719
|
+
SELECT EXISTS (
|
|
3720
|
+
SELECT 1 FROM information_schema.columns
|
|
3721
|
+
WHERE table_schema = 'public'
|
|
3722
|
+
AND table_name = ${table}
|
|
3723
|
+
AND column_name = ${name}
|
|
3724
|
+
) AS exists
|
|
3725
|
+
`.execute(db)).rows[0]?.exists ?? false;
|
|
3726
|
+
};
|
|
3727
|
+
if (!await colExists("execution_runs", "init_failure")) await sql`
|
|
3728
|
+
ALTER TABLE public.execution_runs
|
|
3729
|
+
ADD COLUMN init_failure JSONB DEFAULT NULL
|
|
3730
|
+
`.execute(db);
|
|
3731
|
+
if (!await colExists("execution_jobs", "init_failure")) await sql`
|
|
3732
|
+
ALTER TABLE public.execution_jobs
|
|
3733
|
+
ADD COLUMN init_failure JSONB DEFAULT NULL
|
|
3734
|
+
`.execute(db);
|
|
3735
|
+
}
|
|
3736
|
+
async function down(db) {
|
|
3737
|
+
await sql`
|
|
3738
|
+
ALTER TABLE public.execution_jobs DROP COLUMN IF EXISTS init_failure
|
|
3739
|
+
`.execute(db);
|
|
3740
|
+
await sql`
|
|
3741
|
+
ALTER TABLE public.execution_runs DROP COLUMN IF EXISTS init_failure
|
|
3742
|
+
`.execute(db);
|
|
3743
|
+
}
|
|
3744
|
+
//#endregion
|
|
3699
3745
|
//#region src/db/migration-provider.ts
|
|
3700
3746
|
function createMigrationProvider() {
|
|
3701
3747
|
return { async getMigrations() {
|
|
@@ -3723,7 +3769,8 @@ function createMigrationProvider() {
|
|
|
3723
3769
|
"021_check_run_tracking": _021_check_run_tracking_exports,
|
|
3724
3770
|
"022_scaler_manager_state": _022_scaler_manager_state_exports,
|
|
3725
3771
|
"023_dispatch_queue_recovery_deadline": _023_dispatch_queue_recovery_deadline_exports,
|
|
3726
|
-
"024_dispatch_queue_provisioning_error": _024_dispatch_queue_provisioning_error_exports
|
|
3772
|
+
"024_dispatch_queue_provisioning_error": _024_dispatch_queue_provisioning_error_exports,
|
|
3773
|
+
"025_init_failure": _025_init_failure_exports
|
|
3727
3774
|
};
|
|
3728
3775
|
} };
|
|
3729
3776
|
}
|
|
@@ -3865,7 +3912,7 @@ function registerDbCommands(program, getClient) {
|
|
|
3865
3912
|
process.exit(1);
|
|
3866
3913
|
}
|
|
3867
3914
|
});
|
|
3868
|
-
db.command("ensure <name>").description("CREATE DATABASE IF NOT EXISTS (idempotent)").option("--database-url <url>", "Admin DB URL (else KICI_DATABASE_URL / DATABASE_URL)").option("--owner <role>", "DB owner role (default: URL user). Pass when the admin connection is privileged but the new DB should be owned by a separate non-privileged role.").option("--revoke-connect-public", "After ensure, REVOKE CONNECT ON DATABASE \"<name>\" FROM PUBLIC (recommended on shared clusters).").action(async (name, opts) => {
|
|
3915
|
+
db.command("ensure <name>").description("CREATE DATABASE IF NOT EXISTS (idempotent)").option("--database-url <url>", "Admin DB URL (else KICI_DATABASE_URL / DATABASE_URL)").option("--owner <role>", "DB owner role (default: URL user). Pass when the admin connection is privileged but the new DB should be owned by a separate non-privileged role.").option("--revoke-connect-public", "After ensure, REVOKE CONNECT ON DATABASE \"<name>\" FROM PUBLIC (recommended on shared clusters).").option("--grant-connect-role <role>", "After ensure (and any --revoke-connect-public), GRANT CONNECT ON DATABASE \"<name>\" TO \"<role>\". Repeatable.", (val, acc) => acc.concat([val]), []).action(async (name, opts) => {
|
|
3869
3916
|
try {
|
|
3870
3917
|
const baseUrl = resolveDatabaseUrl$1(opts.databaseUrl);
|
|
3871
3918
|
const url = new URL(baseUrl);
|
|
@@ -3874,9 +3921,10 @@ function registerDbCommands(program, getClient) {
|
|
|
3874
3921
|
logInvocation(`ensure ${name}`, targetUrl);
|
|
3875
3922
|
const outcome = await ensureDatabase(targetUrl, {
|
|
3876
3923
|
owner: opts.owner,
|
|
3877
|
-
revokeConnectFromPublic: !!opts.revokeConnectPublic
|
|
3924
|
+
revokeConnectFromPublic: !!opts.revokeConnectPublic,
|
|
3925
|
+
grantConnectToRoles: opts.grantConnectRole
|
|
3878
3926
|
});
|
|
3879
|
-
const suffix = (opts.owner ? ` (owner=${opts.owner})` : "") + (opts.revokeConnectPublic ? " [revoked CONNECT from PUBLIC]" : "");
|
|
3927
|
+
const suffix = (opts.owner ? ` (owner=${opts.owner})` : "") + (opts.revokeConnectPublic ? " [revoked CONNECT from PUBLIC]" : "") + (opts.grantConnectRole.length ? ` [granted CONNECT to: ${opts.grantConnectRole.join(", ")}]` : "");
|
|
3880
3928
|
console.log(`db ensure: ${name} — ${outcome}${suffix}`);
|
|
3881
3929
|
} catch (err) {
|
|
3882
3930
|
console.error(`Error: ${toErrorMessage(err)}`);
|
|
@@ -6092,7 +6140,8 @@ function isRoot() {
|
|
|
6092
6140
|
return typeof process.getuid === "function" && process.getuid() === 0;
|
|
6093
6141
|
}
|
|
6094
6142
|
/**
|
|
6095
|
-
*
|
|
6143
|
+
* Name-agnostic KiCI config root — the directory that contains every
|
|
6144
|
+
* per-instance config subdir for this privilege level.
|
|
6096
6145
|
*
|
|
6097
6146
|
* Paths follow platform conventions:
|
|
6098
6147
|
* - System Linux/macOS: /etc/kici/
|
|
@@ -6100,8 +6149,11 @@ function isRoot() {
|
|
|
6100
6149
|
* - User macOS: ~/Library/Application Support/kici/
|
|
6101
6150
|
* - System Windows: C:\ProgramData\kici\
|
|
6102
6151
|
* - User Windows: %LOCALAPPDATA%\kici\
|
|
6152
|
+
*
|
|
6153
|
+
* Used by the instance index (`<kiciRoot>/instances.json`) which lives
|
|
6154
|
+
* outside any per-instance subdir, and as the base for {@link getConfigDir}.
|
|
6103
6155
|
*/
|
|
6104
|
-
function
|
|
6156
|
+
function kiciConfigRoot(isUserLevel) {
|
|
6105
6157
|
const plat = os.platform();
|
|
6106
6158
|
if (plat === "win32") {
|
|
6107
6159
|
if (isUserLevel) return (process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local")) + "\\kici\\";
|
|
@@ -6112,20 +6164,40 @@ function getConfigDir(_serviceName, isUserLevel) {
|
|
|
6112
6164
|
return path.join(os.homedir(), ".config", "kici") + "/";
|
|
6113
6165
|
}
|
|
6114
6166
|
/**
|
|
6115
|
-
* Get the
|
|
6167
|
+
* Get the configuration directory for a specific KiCI service instance.
|
|
6116
6168
|
*
|
|
6117
|
-
*
|
|
6118
|
-
* -
|
|
6119
|
-
*
|
|
6120
|
-
*
|
|
6121
|
-
*
|
|
6169
|
+
* Returns `<kiciConfigRoot>/<serviceName>/`. The name-scoped subdir is the
|
|
6170
|
+
* folder-anchored home for everything belonging to one installed instance
|
|
6171
|
+
* (env file, generated unit references, future per-instance state).
|
|
6172
|
+
*
|
|
6173
|
+
* Examples:
|
|
6174
|
+
* - System Linux: /etc/kici/<name>/
|
|
6175
|
+
* - User Linux: ~/.config/kici/<name>/
|
|
6176
|
+
* - User macOS: ~/Library/Application Support/kici/<name>/
|
|
6177
|
+
* - System Windows: C:\ProgramData\kici\<name>\
|
|
6178
|
+
* - User Windows: %LOCALAPPDATA%\kici\<name>\
|
|
6179
|
+
*/
|
|
6180
|
+
function getConfigDir(serviceName, isUserLevel) {
|
|
6181
|
+
const root = kiciConfigRoot(isUserLevel);
|
|
6182
|
+
const sep = os.platform() === "win32" ? "\\" : "/";
|
|
6183
|
+
return root + serviceName + sep;
|
|
6184
|
+
}
|
|
6185
|
+
/**
|
|
6186
|
+
* Get the log directory for a specific KiCI service instance.
|
|
6187
|
+
*
|
|
6188
|
+
* The per-platform layout matches the existing matrix; the service name is
|
|
6189
|
+
* injected as the per-instance segment so each instance has its own log dir:
|
|
6190
|
+
* - System Linux/macOS: /var/log/kici/<name>/
|
|
6191
|
+
* - User Linux: ~/.local/share/kici/<name>/logs/
|
|
6192
|
+
* - User macOS: ~/Library/Logs/kici/<name>/
|
|
6193
|
+
* - Windows: C:\ProgramData\kici\<name>\logs\
|
|
6122
6194
|
*/
|
|
6123
|
-
function getLogDir(
|
|
6195
|
+
function getLogDir(serviceName, isUserLevel) {
|
|
6124
6196
|
const plat = os.platform();
|
|
6125
|
-
if (plat === "win32") return "C:\\ProgramData\\kici\\logs\\";
|
|
6126
|
-
if (!isUserLevel) return "/var/log/kici/";
|
|
6127
|
-
if (plat === "darwin") return path.join(os.homedir(), "Library", "Logs", "kici") + "/";
|
|
6128
|
-
return path.join(os.homedir(), ".local", "share", "kici", "logs") + "/";
|
|
6197
|
+
if (plat === "win32") return "C:\\ProgramData\\kici\\" + serviceName + "\\logs\\";
|
|
6198
|
+
if (!isUserLevel) return "/var/log/kici/" + serviceName + "/";
|
|
6199
|
+
if (plat === "darwin") return path.join(os.homedir(), "Library", "Logs", "kici", serviceName) + "/";
|
|
6200
|
+
return path.join(os.homedir(), ".local", "share", "kici", serviceName, "logs") + "/";
|
|
6129
6201
|
}
|
|
6130
6202
|
/**
|
|
6131
6203
|
* Get the cache directory for lazy dependency downloads.
|
|
@@ -6158,31 +6230,263 @@ function resolveUserLevel(opts) {
|
|
|
6158
6230
|
return !isRoot();
|
|
6159
6231
|
}
|
|
6160
6232
|
//#endregion
|
|
6161
|
-
//#region src/cli/service/
|
|
6233
|
+
//#region src/cli/service/instance/manifest.ts
|
|
6162
6234
|
/**
|
|
6163
|
-
*
|
|
6164
|
-
*
|
|
6165
|
-
*
|
|
6166
|
-
* journalctl, and loginctl commands. Supports both system-level
|
|
6167
|
-
* (/etc/systemd/system/) and user-level (~/.config/systemd/user/) services.
|
|
6235
|
+
* Instance manifest — the single source of truth for a folder-anchored
|
|
6236
|
+
* service install. Written by `install` into the deploy folder, read by
|
|
6237
|
+
* every lifecycle command to reconstruct the ServiceConfig.
|
|
6168
6238
|
*/
|
|
6169
|
-
|
|
6170
|
-
|
|
6171
|
-
|
|
6172
|
-
|
|
6239
|
+
const REQUIRED_FIELDS = [
|
|
6240
|
+
"component",
|
|
6241
|
+
"name",
|
|
6242
|
+
"platform",
|
|
6243
|
+
"isUserLevel",
|
|
6244
|
+
"envFilePath",
|
|
6245
|
+
"configDir",
|
|
6246
|
+
"logDir",
|
|
6247
|
+
"installBase",
|
|
6248
|
+
"createdAt",
|
|
6249
|
+
"kiciVersion"
|
|
6250
|
+
];
|
|
6251
|
+
/** Per-component manifest filename. */
|
|
6252
|
+
function manifestFilename(component) {
|
|
6253
|
+
return `.kici-${component}.json`;
|
|
6254
|
+
}
|
|
6255
|
+
/** Resolve the manifest path inside an instance directory. */
|
|
6256
|
+
function manifestPath(instanceDir, component) {
|
|
6257
|
+
return path.join(instanceDir, manifestFilename(component));
|
|
6258
|
+
}
|
|
6173
6259
|
/**
|
|
6174
|
-
*
|
|
6175
|
-
*
|
|
6176
|
-
|
|
6177
|
-
|
|
6178
|
-
|
|
6179
|
-
|
|
6260
|
+
* Read the manifest for `component` from `instanceDir`.
|
|
6261
|
+
* Returns null when the file does not exist; throws on parse or schema errors.
|
|
6262
|
+
*/
|
|
6263
|
+
function readManifest(instanceDir, component) {
|
|
6264
|
+
const file = manifestPath(instanceDir, component);
|
|
6265
|
+
if (!fs.existsSync(file)) return null;
|
|
6266
|
+
const raw = fs.readFileSync(file, "utf-8");
|
|
6267
|
+
let parsed;
|
|
6268
|
+
try {
|
|
6269
|
+
parsed = JSON.parse(raw);
|
|
6270
|
+
} catch (err) {
|
|
6271
|
+
throw new Error(`Malformed instance manifest at ${file}: ${err.message}`);
|
|
6272
|
+
}
|
|
6273
|
+
if (!parsed || typeof parsed !== "object") throw new Error(`Invalid instance manifest at ${file}: not an object`);
|
|
6274
|
+
for (const field of REQUIRED_FIELDS) if (!(field in parsed)) throw new Error(`Invalid instance manifest at ${file}: missing field "${field}"`);
|
|
6275
|
+
return parsed;
|
|
6276
|
+
}
|
|
6277
|
+
/**
|
|
6278
|
+
* Write the manifest for `manifest.component` into `instanceDir`.
|
|
6279
|
+
* Returns the full path written.
|
|
6280
|
+
*/
|
|
6281
|
+
function writeManifest(instanceDir, manifest) {
|
|
6282
|
+
fs.mkdirSync(instanceDir, { recursive: true });
|
|
6283
|
+
const file = manifestPath(instanceDir, manifest.component);
|
|
6284
|
+
fs.writeFileSync(file, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
|
|
6285
|
+
return file;
|
|
6286
|
+
}
|
|
6287
|
+
/**
|
|
6288
|
+
* Read the running kici-admin's version from the orchestrator package.json.
|
|
6180
6289
|
*
|
|
6181
|
-
*
|
|
6290
|
+
* `process.env.npm_package_version` is only populated under `npm run` and is
|
|
6291
|
+
* undefined when kici-admin runs as a globally-installed binary, which is the
|
|
6292
|
+
* actual install path. Reading from the package.json on disk is the only
|
|
6293
|
+
* reliable source.
|
|
6182
6294
|
*/
|
|
6183
|
-
function
|
|
6295
|
+
function readKiciVersion() {
|
|
6184
6296
|
try {
|
|
6185
|
-
const
|
|
6297
|
+
const pkgUrl = new URL("../../../../package.json", import.meta.url);
|
|
6298
|
+
const pkg = JSON.parse(fs.readFileSync(fileURLToPath(pkgUrl), "utf-8"));
|
|
6299
|
+
return typeof pkg.version === "string" ? pkg.version : "unknown";
|
|
6300
|
+
} catch {
|
|
6301
|
+
return "unknown";
|
|
6302
|
+
}
|
|
6303
|
+
}
|
|
6304
|
+
//#endregion
|
|
6305
|
+
//#region src/cli/service/instance/index-file.ts
|
|
6306
|
+
/**
|
|
6307
|
+
* Instance index — a reconciled CACHE of all installed instances on the host.
|
|
6308
|
+
*
|
|
6309
|
+
* Lives at <kiciRoot>/instances.json, where kiciRoot is the name-agnostic
|
|
6310
|
+
* config root (~/.config/kici/ for user-level, /etc/kici/ for system). The
|
|
6311
|
+
* index is convenience: discovery is authoritative against the init system
|
|
6312
|
+
* (see resolve.ts#listInstances which reconciles this file against scans).
|
|
6313
|
+
*
|
|
6314
|
+
* Corrupt index → warn and treat as empty; the next install/uninstall
|
|
6315
|
+
* rewrites it cleanly.
|
|
6316
|
+
*/
|
|
6317
|
+
const FILE = "instances.json";
|
|
6318
|
+
/** Resolve <kiciRoot>/instances.json. */
|
|
6319
|
+
function indexPath(kiciRoot) {
|
|
6320
|
+
return path.join(kiciRoot, FILE);
|
|
6321
|
+
}
|
|
6322
|
+
/** Read the index. Missing or corrupt → []. */
|
|
6323
|
+
function readIndex(kiciRoot) {
|
|
6324
|
+
const file = indexPath(kiciRoot);
|
|
6325
|
+
if (!fs.existsSync(file)) return [];
|
|
6326
|
+
try {
|
|
6327
|
+
const parsed = JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
6328
|
+
if (!Array.isArray(parsed)) {
|
|
6329
|
+
console.warn(`[kici] instance index at ${file} is invalid (not an array); ignoring`);
|
|
6330
|
+
return [];
|
|
6331
|
+
}
|
|
6332
|
+
return parsed;
|
|
6333
|
+
} catch (err) {
|
|
6334
|
+
console.warn(`[kici] instance index at ${file} is corrupt (${err.message}); ignoring`);
|
|
6335
|
+
return [];
|
|
6336
|
+
}
|
|
6337
|
+
}
|
|
6338
|
+
/** Overwrite the index. */
|
|
6339
|
+
function writeIndex(kiciRoot, entries) {
|
|
6340
|
+
fs.mkdirSync(kiciRoot, { recursive: true });
|
|
6341
|
+
fs.writeFileSync(indexPath(kiciRoot), JSON.stringify(entries, null, 2) + "\n", "utf-8");
|
|
6342
|
+
}
|
|
6343
|
+
/**
|
|
6344
|
+
* Append an entry. Idempotent for an exact (component,name,instanceDir) match.
|
|
6345
|
+
* Throws on a (component,name) collision with a different instanceDir — the
|
|
6346
|
+
* caller must use a different name or pass --force to overwrite.
|
|
6347
|
+
*/
|
|
6348
|
+
function appendIndexEntry(kiciRoot, entry) {
|
|
6349
|
+
const current = readIndex(kiciRoot);
|
|
6350
|
+
const existing = current.find((e) => e.component === entry.component && e.name === entry.name);
|
|
6351
|
+
if (existing) {
|
|
6352
|
+
if (existing.instanceDir === entry.instanceDir) return;
|
|
6353
|
+
throw new Error(`Already an ${entry.component} instance "${entry.name}" registered at ${existing.instanceDir}`);
|
|
6354
|
+
}
|
|
6355
|
+
writeIndex(kiciRoot, [...current, entry]);
|
|
6356
|
+
}
|
|
6357
|
+
/** Remove the matching entry (no-op when absent). */
|
|
6358
|
+
function removeIndexEntry(kiciRoot, key) {
|
|
6359
|
+
const current = readIndex(kiciRoot);
|
|
6360
|
+
const next = current.filter((e) => !(e.component === key.component && e.name === key.name));
|
|
6361
|
+
if (next.length !== current.length) writeIndex(kiciRoot, next);
|
|
6362
|
+
}
|
|
6363
|
+
//#endregion
|
|
6364
|
+
//#region src/cli/service/instance/resolve.ts
|
|
6365
|
+
/**
|
|
6366
|
+
* resolveInstance — the single entry point every lifecycle command uses to
|
|
6367
|
+
* decide which installed service it's operating on.
|
|
6368
|
+
*
|
|
6369
|
+
* Priority:
|
|
6370
|
+
* 1. --instance-dir <path> → read manifest at <path>
|
|
6371
|
+
* 2. --name <name> → match against listInstances() result
|
|
6372
|
+
* 3. CWD manifest → read ./.kici-<component>.json
|
|
6373
|
+
* 4. otherwise → refuse with a candidate list (throws)
|
|
6374
|
+
*
|
|
6375
|
+
* listInstances reconciles the on-disk index (cache) with the driver's
|
|
6376
|
+
* native scan (source of truth). The reconciled result is rewritten back to
|
|
6377
|
+
* the index to self-heal stale entries.
|
|
6378
|
+
*/
|
|
6379
|
+
/**
|
|
6380
|
+
* Reconcile <kiciRoot>/instances.json with the driver's native scan, then
|
|
6381
|
+
* rewrite the index dropping entries whose units no longer exist. Returns
|
|
6382
|
+
* the merged list filtered to the requested component + isUserLevel.
|
|
6383
|
+
*/
|
|
6384
|
+
async function listInstances(args) {
|
|
6385
|
+
const { component, isUserLevel, kiciRoot, manager } = args;
|
|
6386
|
+
const scanForComponent = (await manager.list(isUserLevel)).filter((s) => s.component === component);
|
|
6387
|
+
const scanNames = new Set(scanForComponent.map((s) => s.name));
|
|
6388
|
+
const index = readIndex(kiciRoot);
|
|
6389
|
+
const relevantIndex = index.filter((e) => e.component === component && e.isUserLevel === isUserLevel);
|
|
6390
|
+
const survivors = relevantIndex.filter((e) => scanNames.has(e.name));
|
|
6391
|
+
if (survivors.length !== relevantIndex.length) writeIndex(kiciRoot, [...index.filter((e) => !(e.component === component && e.isUserLevel === isUserLevel)), ...survivors]);
|
|
6392
|
+
const indexByName = new Map(survivors.map((e) => [e.name, e]));
|
|
6393
|
+
return scanForComponent.map((s) => {
|
|
6394
|
+
const idx = indexByName.get(s.name);
|
|
6395
|
+
return {
|
|
6396
|
+
...s,
|
|
6397
|
+
component,
|
|
6398
|
+
instanceDir: idx?.instanceDir,
|
|
6399
|
+
source: idx ? "index+scan" : "scan"
|
|
6400
|
+
};
|
|
6401
|
+
});
|
|
6402
|
+
}
|
|
6403
|
+
/**
|
|
6404
|
+
* Resolve the target instance for the current lifecycle invocation.
|
|
6405
|
+
* Throws with a refusal/candidate-listing error when ambiguous.
|
|
6406
|
+
*/
|
|
6407
|
+
async function resolveInstance(args) {
|
|
6408
|
+
const { component, opts, cwd, kiciRoot, manager, isUserLevel } = args;
|
|
6409
|
+
if (opts.instanceDir) {
|
|
6410
|
+
const dir = path.resolve(opts.instanceDir);
|
|
6411
|
+
const m = readManifest(dir, component);
|
|
6412
|
+
if (!m) throw new Error(`No ${component} manifest at ${manifestPath(dir, component)}. Did you install with --instance-dir ${dir}?`);
|
|
6413
|
+
return {
|
|
6414
|
+
manifest: m,
|
|
6415
|
+
manifestPath: manifestPath(dir, component),
|
|
6416
|
+
instanceDir: dir
|
|
6417
|
+
};
|
|
6418
|
+
}
|
|
6419
|
+
if (opts.name) {
|
|
6420
|
+
const candidates = await listInstances({
|
|
6421
|
+
component,
|
|
6422
|
+
isUserLevel,
|
|
6423
|
+
kiciRoot,
|
|
6424
|
+
manager
|
|
6425
|
+
});
|
|
6426
|
+
const match = candidates.find((c) => c.name === opts.name);
|
|
6427
|
+
if (!match) throw new Error(formatNameNotFound(component, opts.name, candidates));
|
|
6428
|
+
if (!match.instanceDir) throw new Error(`${component} instance "${opts.name}" exists in the init system but has no manifest. Pass --instance-dir <deploy folder> instead.`);
|
|
6429
|
+
const manifest = readManifest(match.instanceDir, component);
|
|
6430
|
+
if (!manifest) throw new Error(`Manifest for ${component} instance "${opts.name}" missing at ${manifestPath(match.instanceDir, component)}.`);
|
|
6431
|
+
return {
|
|
6432
|
+
manifest,
|
|
6433
|
+
manifestPath: manifestPath(match.instanceDir, component),
|
|
6434
|
+
instanceDir: match.instanceDir
|
|
6435
|
+
};
|
|
6436
|
+
}
|
|
6437
|
+
const cwdManifest = readManifest(cwd, component);
|
|
6438
|
+
if (cwdManifest) return {
|
|
6439
|
+
manifest: cwdManifest,
|
|
6440
|
+
manifestPath: manifestPath(cwd, component),
|
|
6441
|
+
instanceDir: path.resolve(cwd)
|
|
6442
|
+
};
|
|
6443
|
+
const candidates = await listInstances({
|
|
6444
|
+
component,
|
|
6445
|
+
isUserLevel,
|
|
6446
|
+
kiciRoot,
|
|
6447
|
+
manager
|
|
6448
|
+
});
|
|
6449
|
+
throw new Error(formatRefusal(component, candidates));
|
|
6450
|
+
}
|
|
6451
|
+
/**
|
|
6452
|
+
* Format the refusal message and candidate table.
|
|
6453
|
+
*
|
|
6454
|
+
* When candidates is empty, returns the "no instances installed" guidance.
|
|
6455
|
+
* When candidates exist, lists them with their instanceDir (or "(no manifest)").
|
|
6456
|
+
*/
|
|
6457
|
+
function formatRefusal(component, candidates) {
|
|
6458
|
+
if (candidates.length === 0) return `No ${component} instances installed on this host. Run \`kici-admin ${component} install --instance-dir <deploy folder>\` first.`;
|
|
6459
|
+
return `No instance specified and no manifest in CWD. Candidates on this host:\n${candidates.map((c) => ` - ${c.name} ${c.platform} ${c.instanceDir ?? "(no manifest)"}`).join("\n")}\nPass --instance-dir <path> or --name <name>, or cd into the deploy folder.`;
|
|
6460
|
+
}
|
|
6461
|
+
function formatNameNotFound(component, name, candidates) {
|
|
6462
|
+
return `${component} instance "${name}" not found. Installed:\n${candidates.length ? candidates.map((c) => ` - ${c.name} ${c.platform} ${c.instanceDir ?? "(no manifest)"}`).join("\n") : " (none)"}`;
|
|
6463
|
+
}
|
|
6464
|
+
//#endregion
|
|
6465
|
+
//#region src/cli/service/systemd.ts
|
|
6466
|
+
/**
|
|
6467
|
+
* systemd service manager implementation.
|
|
6468
|
+
*
|
|
6469
|
+
* Generates unit files and manages service lifecycle via systemctl,
|
|
6470
|
+
* journalctl, and loginctl commands. Supports both system-level
|
|
6471
|
+
* (/etc/systemd/system/) and user-level (~/.config/systemd/user/) services.
|
|
6472
|
+
*/
|
|
6473
|
+
var systemd_exports = /* @__PURE__ */ __exportAll({
|
|
6474
|
+
SystemdServiceManager: () => SystemdServiceManager,
|
|
6475
|
+
isLingerEnabled: () => isLingerEnabled
|
|
6476
|
+
});
|
|
6477
|
+
/**
|
|
6478
|
+
* Check whether linger is already enabled for a user.
|
|
6479
|
+
*
|
|
6480
|
+
* Uses `loginctl show-user <user>` which is readable by any user. Returns
|
|
6481
|
+
* true if the user's `Linger` property reads `yes`. Returns false on any
|
|
6482
|
+
* parse failure, missing binary, or unknown user — the caller is expected
|
|
6483
|
+
* to attempt `enable-linger` in that case.
|
|
6484
|
+
*
|
|
6485
|
+
* Exported (package-private) for testing.
|
|
6486
|
+
*/
|
|
6487
|
+
function isLingerEnabled(user) {
|
|
6488
|
+
try {
|
|
6489
|
+
const out = execFileSync("loginctl", [
|
|
6186
6490
|
"show-user",
|
|
6187
6491
|
user,
|
|
6188
6492
|
"--property=Linger"
|
|
@@ -6215,6 +6519,7 @@ var init_systemd = __esmMin((() => {
|
|
|
6215
6519
|
const lines = [];
|
|
6216
6520
|
lines.push("[Unit]");
|
|
6217
6521
|
lines.push(`Description=${config.description}`);
|
|
6522
|
+
if (config.component) lines.push(`X-KiCI-Component=${config.component}`);
|
|
6218
6523
|
lines.push("After=network.target postgresql.service");
|
|
6219
6524
|
lines.push("");
|
|
6220
6525
|
lines.push("[Service]");
|
|
@@ -6351,6 +6656,24 @@ var init_systemd = __esmMin((() => {
|
|
|
6351
6656
|
async isInstalled(config) {
|
|
6352
6657
|
return fs.existsSync(this.unitFilePath(config));
|
|
6353
6658
|
}
|
|
6659
|
+
async list(isUserLevel) {
|
|
6660
|
+
const unitDir = isUserLevel ? path.join(os.homedir(), ".config", "systemd", "user") : "/etc/systemd/system";
|
|
6661
|
+
if (!fs.existsSync(unitDir)) return [];
|
|
6662
|
+
const out = [];
|
|
6663
|
+
for (const entry of fs.readdirSync(unitDir)) {
|
|
6664
|
+
if (typeof entry !== "string") continue;
|
|
6665
|
+
if (!entry.startsWith("kici-") || !entry.endsWith(".service")) continue;
|
|
6666
|
+
const m = fs.readFileSync(path.join(unitDir, entry), "utf-8").match(/^X-KiCI-Component=(orchestrator|agent)\s*$/m);
|
|
6667
|
+
if (!m) continue;
|
|
6668
|
+
out.push({
|
|
6669
|
+
name: entry.replace(/\.service$/, ""),
|
|
6670
|
+
platform: "systemd",
|
|
6671
|
+
isUserLevel,
|
|
6672
|
+
component: m[1]
|
|
6673
|
+
});
|
|
6674
|
+
}
|
|
6675
|
+
return out;
|
|
6676
|
+
}
|
|
6354
6677
|
};
|
|
6355
6678
|
}));
|
|
6356
6679
|
//#endregion
|
|
@@ -6363,6 +6686,10 @@ var init_systemd = __esmMin((() => {
|
|
|
6363
6686
|
* user-level (~/Library/LaunchAgents/) agents.
|
|
6364
6687
|
*/
|
|
6365
6688
|
var launchd_exports = /* @__PURE__ */ __exportAll({ LaunchdServiceManager: () => LaunchdServiceManager });
|
|
6689
|
+
/** Async sleep used to pace launchd bootout/bootstrap reconciliation. */
|
|
6690
|
+
function sleep(ms) {
|
|
6691
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
6692
|
+
}
|
|
6366
6693
|
var LABEL_PREFIX, SYSTEM_LOG_DIR, LaunchdServiceManager;
|
|
6367
6694
|
var init_launchd = __esmMin((() => {
|
|
6368
6695
|
LABEL_PREFIX = "dev.kici";
|
|
@@ -6416,6 +6743,10 @@ var init_launchd = __esmMin((() => {
|
|
|
6416
6743
|
lines.push("<dict>");
|
|
6417
6744
|
lines.push(" <key>Label</key>");
|
|
6418
6745
|
lines.push(` <string>${this.escapeXml(label)}</string>`);
|
|
6746
|
+
if (config.component) {
|
|
6747
|
+
lines.push(" <key>KiCIComponent</key>");
|
|
6748
|
+
lines.push(` <string>${config.component}</string>`);
|
|
6749
|
+
}
|
|
6419
6750
|
lines.push(" <key>ProgramArguments</key>");
|
|
6420
6751
|
lines.push(" <array>");
|
|
6421
6752
|
lines.push(` <string>${this.escapeXml(config.executablePath)}</string>`);
|
|
@@ -6497,14 +6828,61 @@ var init_launchd = __esmMin((() => {
|
|
|
6497
6828
|
logDirectory
|
|
6498
6829
|
], { stdio: "inherit" });
|
|
6499
6830
|
fs.writeFileSync(plistFile, plistContent, "utf-8");
|
|
6500
|
-
if (this.isLoaded(config))
|
|
6501
|
-
|
|
6502
|
-
|
|
6503
|
-
|
|
6504
|
-
|
|
6505
|
-
|
|
6506
|
-
|
|
6507
|
-
|
|
6831
|
+
if (this.isLoaded(config)) {
|
|
6832
|
+
try {
|
|
6833
|
+
execFileSync("launchctl", ["bootout", this.domainTarget(config)], { stdio: "inherit" });
|
|
6834
|
+
} catch {}
|
|
6835
|
+
await this.waitUntilUnloaded(config);
|
|
6836
|
+
}
|
|
6837
|
+
await this.bootstrapWithRetry(config, plistFile);
|
|
6838
|
+
}
|
|
6839
|
+
/**
|
|
6840
|
+
* Poll until the service is no longer loaded in its target domain, or a
|
|
6841
|
+
* short deadline elapses. `launchctl bootout` is asynchronous — it returns
|
|
6842
|
+
* before launchd has finished releasing the service — so a bootstrap issued
|
|
6843
|
+
* immediately afterward races the teardown and fails with EIO. Waiting for
|
|
6844
|
+
* the unload to complete closes that race for the common case; the residual
|
|
6845
|
+
* window is covered by bootstrapWithRetry.
|
|
6846
|
+
*/
|
|
6847
|
+
async waitUntilUnloaded(config) {
|
|
6848
|
+
const deadline = Date.now() + 15e3;
|
|
6849
|
+
while (this.isLoaded(config)) {
|
|
6850
|
+
if (Date.now() >= deadline) return;
|
|
6851
|
+
await sleep(500);
|
|
6852
|
+
}
|
|
6853
|
+
}
|
|
6854
|
+
/**
|
|
6855
|
+
* Bootstrap into the target domain, retrying on the transient EIO
|
|
6856
|
+
* ("5: Input/output error") launchd returns when a just-removed service has
|
|
6857
|
+
* not finished tearing down. A genuine, non-transient failure (bad plist,
|
|
6858
|
+
* permission denied) is re-thrown on the first attempt. If a stale instance
|
|
6859
|
+
* reappears between attempts, it is booted out before the next try.
|
|
6860
|
+
*/
|
|
6861
|
+
async bootstrapWithRetry(config, plistFile) {
|
|
6862
|
+
const attempts = 5;
|
|
6863
|
+
for (let attempt = 1; attempt <= attempts; attempt++) try {
|
|
6864
|
+
execFileSync("launchctl", [
|
|
6865
|
+
"bootstrap",
|
|
6866
|
+
this.domain(config),
|
|
6867
|
+
plistFile
|
|
6868
|
+
], { stdio: [
|
|
6869
|
+
"inherit",
|
|
6870
|
+
"inherit",
|
|
6871
|
+
"pipe"
|
|
6872
|
+
] });
|
|
6873
|
+
return;
|
|
6874
|
+
} catch (err) {
|
|
6875
|
+
const e = err;
|
|
6876
|
+
const stderr = (e.stderr ?? "").toString();
|
|
6877
|
+
if (!(e.status === 5 || /input\/output error|resource busy/i.test(stderr)) || attempt === attempts) {
|
|
6878
|
+
if (stderr) process.stderr.write(stderr);
|
|
6879
|
+
throw err;
|
|
6880
|
+
}
|
|
6881
|
+
if (this.isLoaded(config)) try {
|
|
6882
|
+
execFileSync("launchctl", ["bootout", this.domainTarget(config)], { stdio: "inherit" });
|
|
6883
|
+
} catch {}
|
|
6884
|
+
await sleep(1e3 * attempt);
|
|
6885
|
+
}
|
|
6508
6886
|
}
|
|
6509
6887
|
async uninstall(config) {
|
|
6510
6888
|
const plistFile = this.plistPath(config);
|
|
@@ -6573,6 +6951,30 @@ var init_launchd = __esmMin((() => {
|
|
|
6573
6951
|
async isInstalled(config) {
|
|
6574
6952
|
return fs.existsSync(this.plistPath(config));
|
|
6575
6953
|
}
|
|
6954
|
+
async list(isUserLevel) {
|
|
6955
|
+
const baseDir = isUserLevel ? path.join(os.homedir(), "Library", "LaunchAgents") : "/Library/LaunchDaemons";
|
|
6956
|
+
if (!fs.existsSync(baseDir)) return [];
|
|
6957
|
+
const out = [];
|
|
6958
|
+
for (const entry of fs.readdirSync(baseDir)) {
|
|
6959
|
+
if (typeof entry !== "string") continue;
|
|
6960
|
+
if (!entry.endsWith(".plist")) continue;
|
|
6961
|
+
let content;
|
|
6962
|
+
try {
|
|
6963
|
+
content = fs.readFileSync(path.join(baseDir, entry), "utf-8");
|
|
6964
|
+
} catch {
|
|
6965
|
+
continue;
|
|
6966
|
+
}
|
|
6967
|
+
const match = content.match(/<key>KiCIComponent<\/key>\s*<string>(orchestrator|agent)<\/string>/);
|
|
6968
|
+
if (!match) continue;
|
|
6969
|
+
out.push({
|
|
6970
|
+
name: entry.replace(/\.plist$/, ""),
|
|
6971
|
+
platform: "launchd",
|
|
6972
|
+
isUserLevel,
|
|
6973
|
+
component: match[1]
|
|
6974
|
+
});
|
|
6975
|
+
}
|
|
6976
|
+
return out;
|
|
6977
|
+
}
|
|
6576
6978
|
};
|
|
6577
6979
|
}));
|
|
6578
6980
|
//#endregion
|
|
@@ -6870,6 +7272,8 @@ var init_windows = __esmMin((() => {
|
|
|
6870
7272
|
cmdParts.push("--", `"${config.executablePath}"`);
|
|
6871
7273
|
for (const arg of config.args ?? []) cmdParts.push(`"${arg}"`);
|
|
6872
7274
|
execSync(cmdParts.join(" "), { stdio: "pipe" });
|
|
7275
|
+
const descText = config.component ? `[KiCI:${config.component}] ${config.description}` : config.description;
|
|
7276
|
+
execSync(`sc.exe description ${config.name} "${descText.replace(/"/g, "\\\"")}"`, { stdio: "pipe" });
|
|
6873
7277
|
execSync(`sc.exe config ${config.name} start= auto`, { stdio: "pipe" });
|
|
6874
7278
|
const actions = config.restartPolicy.delays.map((d) => `restart/${d * 1e3}`).join("/");
|
|
6875
7279
|
const resetSeconds = config.restartPolicy.windowSeconds;
|
|
@@ -6960,6 +7364,37 @@ var init_windows = __esmMin((() => {
|
|
|
6960
7364
|
return false;
|
|
6961
7365
|
}
|
|
6962
7366
|
}
|
|
7367
|
+
async list(isUserLevel) {
|
|
7368
|
+
let raw;
|
|
7369
|
+
try {
|
|
7370
|
+
raw = execSync("powershell -Command \"Get-CimInstance Win32_Service -Filter \\\"Name LIKE 'kici-%'\\\" | Select-Object Name,Description | ConvertTo-Json\"", { stdio: "pipe" }).toString();
|
|
7371
|
+
} catch {
|
|
7372
|
+
return [];
|
|
7373
|
+
}
|
|
7374
|
+
if (!raw.trim()) return [];
|
|
7375
|
+
let parsed;
|
|
7376
|
+
try {
|
|
7377
|
+
parsed = JSON.parse(raw);
|
|
7378
|
+
} catch {
|
|
7379
|
+
return [];
|
|
7380
|
+
}
|
|
7381
|
+
const rows = Array.isArray(parsed) ? parsed : [parsed];
|
|
7382
|
+
const out = [];
|
|
7383
|
+
for (const row of rows) {
|
|
7384
|
+
if (!row || typeof row !== "object") continue;
|
|
7385
|
+
const r = row;
|
|
7386
|
+
const match = (typeof r.Description === "string" ? r.Description : "").match(/^\[KiCI:(orchestrator|agent)\]/);
|
|
7387
|
+
if (!match) continue;
|
|
7388
|
+
if (typeof r.Name !== "string") continue;
|
|
7389
|
+
out.push({
|
|
7390
|
+
name: r.Name,
|
|
7391
|
+
platform: "windows",
|
|
7392
|
+
isUserLevel,
|
|
7393
|
+
component: match[1]
|
|
7394
|
+
});
|
|
7395
|
+
}
|
|
7396
|
+
return out;
|
|
7397
|
+
}
|
|
6963
7398
|
};
|
|
6964
7399
|
}));
|
|
6965
7400
|
//#endregion
|
|
@@ -6982,11 +7417,11 @@ var compose_exports = /* @__PURE__ */ __exportAll({ ComposeServiceManager: () =>
|
|
|
6982
7417
|
function detectRuntime() {
|
|
6983
7418
|
try {
|
|
6984
7419
|
execSync("podman compose version", { stdio: "pipe" });
|
|
6985
|
-
return "podman
|
|
7420
|
+
return "podman";
|
|
6986
7421
|
} catch {}
|
|
6987
7422
|
try {
|
|
6988
7423
|
execSync("docker compose version", { stdio: "pipe" });
|
|
6989
|
-
return "docker
|
|
7424
|
+
return "docker";
|
|
6990
7425
|
} catch {}
|
|
6991
7426
|
throw new Error("No container runtime found. Install Docker or Podman with compose support.");
|
|
6992
7427
|
}
|
|
@@ -7029,7 +7464,7 @@ function getRestartMode(config) {
|
|
|
7029
7464
|
*/
|
|
7030
7465
|
function generateComposeYaml(config) {
|
|
7031
7466
|
const restartMode = getRestartMode(config);
|
|
7032
|
-
|
|
7467
|
+
const lines = [
|
|
7033
7468
|
"# Generated by KiCI service installer",
|
|
7034
7469
|
`# Service: ${config.displayName}`,
|
|
7035
7470
|
"",
|
|
@@ -7043,20 +7478,25 @@ function generateComposeYaml(config) {
|
|
|
7043
7478
|
" volumes:",
|
|
7044
7479
|
` - ${config.workingDirectory}:${config.workingDirectory}`,
|
|
7045
7480
|
" network_mode: host"
|
|
7046
|
-
]
|
|
7481
|
+
];
|
|
7482
|
+
if (config.component) {
|
|
7483
|
+
lines.push(" labels:");
|
|
7484
|
+
lines.push(` dev.kici.component: ${config.component}`);
|
|
7485
|
+
}
|
|
7486
|
+
return lines.join("\n") + "\n";
|
|
7047
7487
|
}
|
|
7048
7488
|
var ComposeServiceManager;
|
|
7049
7489
|
var init_compose = __esmMin((() => {
|
|
7050
7490
|
ComposeServiceManager = class {
|
|
7051
7491
|
runtime = null;
|
|
7052
|
-
/** Get the runtime
|
|
7492
|
+
/** Get the runtime binary (`podman` or `docker`), detecting it if not already done. */
|
|
7053
7493
|
getRuntime() {
|
|
7054
7494
|
if (!this.runtime) this.runtime = detectRuntime();
|
|
7055
7495
|
return this.runtime;
|
|
7056
7496
|
}
|
|
7057
7497
|
/** Run a compose command for the given service config. */
|
|
7058
7498
|
runCompose(config, args) {
|
|
7059
|
-
execSync(`${this.getRuntime()} -f "${getComposeFilePath(config)}" ${args}`, { stdio: "pipe" });
|
|
7499
|
+
execSync(`${this.getRuntime()} compose -f "${getComposeFilePath(config)}" ${args}`, { stdio: "pipe" });
|
|
7060
7500
|
}
|
|
7061
7501
|
async install(config) {
|
|
7062
7502
|
this.getRuntime();
|
|
@@ -7084,7 +7524,7 @@ var init_compose = __esmMin((() => {
|
|
|
7084
7524
|
}
|
|
7085
7525
|
async status(config) {
|
|
7086
7526
|
try {
|
|
7087
|
-
const output = execSync(`${this.getRuntime()} -f "${getComposeFilePath(config)}" ps --format json`, { stdio: "pipe" }).toString();
|
|
7527
|
+
const output = execSync(`${this.getRuntime()} compose -f "${getComposeFilePath(config)}" ps --format json`, { stdio: "pipe" }).toString();
|
|
7088
7528
|
const data = JSON.parse(output);
|
|
7089
7529
|
const container = Array.isArray(data) ? data[0] : data;
|
|
7090
7530
|
if (!container) return { state: "stopped" };
|
|
@@ -7108,6 +7548,44 @@ var init_compose = __esmMin((() => {
|
|
|
7108
7548
|
const composeFile = getComposeFilePath(config);
|
|
7109
7549
|
return fs.existsSync(composeFile);
|
|
7110
7550
|
}
|
|
7551
|
+
async list(isUserLevel) {
|
|
7552
|
+
let runtime;
|
|
7553
|
+
try {
|
|
7554
|
+
runtime = this.getRuntime();
|
|
7555
|
+
} catch {
|
|
7556
|
+
return [];
|
|
7557
|
+
}
|
|
7558
|
+
let raw;
|
|
7559
|
+
try {
|
|
7560
|
+
raw = execSync(`${runtime} ps -a --filter label=dev.kici.component --format '{{json .}}'`, { encoding: "utf-8" }).toString();
|
|
7561
|
+
} catch {
|
|
7562
|
+
return [];
|
|
7563
|
+
}
|
|
7564
|
+
const out = [];
|
|
7565
|
+
for (const line of raw.split("\n")) {
|
|
7566
|
+
const trimmed = line.trim();
|
|
7567
|
+
if (!trimmed) continue;
|
|
7568
|
+
let row;
|
|
7569
|
+
try {
|
|
7570
|
+
row = JSON.parse(trimmed);
|
|
7571
|
+
} catch {
|
|
7572
|
+
continue;
|
|
7573
|
+
}
|
|
7574
|
+
if (!row || typeof row !== "object") continue;
|
|
7575
|
+
const r = row;
|
|
7576
|
+
const m = (typeof r.Labels === "string" ? r.Labels : "").match(/dev\.kici\.component=(orchestrator|agent)/);
|
|
7577
|
+
if (!m) continue;
|
|
7578
|
+
const name = typeof r.Names === "string" ? r.Names : String(r.Names ?? "");
|
|
7579
|
+
if (!name) continue;
|
|
7580
|
+
out.push({
|
|
7581
|
+
name,
|
|
7582
|
+
platform: "compose",
|
|
7583
|
+
isUserLevel,
|
|
7584
|
+
component: m[1]
|
|
7585
|
+
});
|
|
7586
|
+
}
|
|
7587
|
+
return out;
|
|
7588
|
+
}
|
|
7111
7589
|
};
|
|
7112
7590
|
}));
|
|
7113
7591
|
//#endregion
|
|
@@ -7172,93 +7650,511 @@ function resolveServiceExecutable(opts) {
|
|
|
7172
7650
|
};
|
|
7173
7651
|
}
|
|
7174
7652
|
//#endregion
|
|
7175
|
-
//#region src/cli/
|
|
7653
|
+
//#region src/cli/commands/shared/versioned-upgrade.ts
|
|
7176
7654
|
/**
|
|
7177
|
-
* Shared
|
|
7655
|
+
* Shared versioned directory upgrade logic for kici-admin upgrade commands.
|
|
7178
7656
|
*
|
|
7179
|
-
*
|
|
7180
|
-
*
|
|
7657
|
+
* Implements the versioned directory layout:
|
|
7658
|
+
* - Extract new version alongside old versions
|
|
7659
|
+
* - Update symlink (Unix) or service registration (Windows) atomically
|
|
7660
|
+
* - Preserve old versions for rollback
|
|
7661
|
+
* - Optional cleanup of old versions
|
|
7181
7662
|
*/
|
|
7182
|
-
/**
|
|
7183
|
-
|
|
7184
|
-
|
|
7185
|
-
|
|
7186
|
-
|
|
7187
|
-
|
|
7188
|
-
|
|
7189
|
-
|
|
7190
|
-
|
|
7191
|
-
|
|
7192
|
-
|
|
7193
|
-
|
|
7194
|
-
|
|
7195
|
-
|
|
7196
|
-
|
|
7197
|
-
|
|
7198
|
-
|
|
7199
|
-
|
|
7200
|
-
|
|
7201
|
-
|
|
7202
|
-
|
|
7203
|
-
|
|
7204
|
-
|
|
7205
|
-
|
|
7206
|
-
|
|
7207
|
-
|
|
7208
|
-
return confirm({
|
|
7209
|
-
message,
|
|
7210
|
-
default: defaultValue
|
|
7663
|
+
/**
|
|
7664
|
+
* Resolve the upgrade target via the folder-anchored model and build the
|
|
7665
|
+
* ServiceConfig + installBase the rest of the upgrade flow needs.
|
|
7666
|
+
*
|
|
7667
|
+
* Priority chain (delegated to {@link resolveInstance}):
|
|
7668
|
+
* 1. `opts.instanceDir` — read manifest at that path.
|
|
7669
|
+
* 2. `opts.name` — match against listInstances() output.
|
|
7670
|
+
* 3. CWD manifest — read `./.kici-<component>.json`.
|
|
7671
|
+
* 4. otherwise — refuse with a candidate-list error.
|
|
7672
|
+
*
|
|
7673
|
+
* The returned `installBase` comes from the manifest, NEVER re-derived from
|
|
7674
|
+
* the service name. Instances installed with a non-default base must
|
|
7675
|
+
* continue to resolve to that base on upgrade.
|
|
7676
|
+
*/
|
|
7677
|
+
async function resolveUpgradeTarget(args) {
|
|
7678
|
+
const { component, opts, manager, isUserLevel, kiciRoot } = args;
|
|
7679
|
+
const resolved = await resolveInstance({
|
|
7680
|
+
component,
|
|
7681
|
+
opts: {
|
|
7682
|
+
instanceDir: opts.instanceDir,
|
|
7683
|
+
name: opts.name
|
|
7684
|
+
},
|
|
7685
|
+
cwd: process.cwd(),
|
|
7686
|
+
kiciRoot,
|
|
7687
|
+
manager,
|
|
7688
|
+
isUserLevel
|
|
7211
7689
|
});
|
|
7690
|
+
return {
|
|
7691
|
+
config: {
|
|
7692
|
+
name: resolved.manifest.name,
|
|
7693
|
+
displayName: `KiCI ${component}`,
|
|
7694
|
+
description: `KiCI ${component} service`,
|
|
7695
|
+
executablePath: "",
|
|
7696
|
+
envFilePath: resolved.manifest.envFilePath,
|
|
7697
|
+
workingDirectory: resolved.manifest.configDir,
|
|
7698
|
+
isUserLevel: resolved.manifest.isUserLevel,
|
|
7699
|
+
restartPolicy: {
|
|
7700
|
+
enabled: true,
|
|
7701
|
+
delays: [
|
|
7702
|
+
1,
|
|
7703
|
+
5,
|
|
7704
|
+
15,
|
|
7705
|
+
30
|
|
7706
|
+
],
|
|
7707
|
+
maxRetries: 5,
|
|
7708
|
+
windowSeconds: 300
|
|
7709
|
+
},
|
|
7710
|
+
component
|
|
7711
|
+
},
|
|
7712
|
+
installBase: resolved.manifest.installBase,
|
|
7713
|
+
resolvedInstance: resolved
|
|
7714
|
+
};
|
|
7212
7715
|
}
|
|
7213
|
-
/** Prompt for
|
|
7214
|
-
async function
|
|
7215
|
-
|
|
7216
|
-
|
|
7217
|
-
|
|
7218
|
-
validate: (value) => {
|
|
7219
|
-
const v = value.trim();
|
|
7220
|
-
if (!v.startsWith("http://") && !v.startsWith("https://") && !v.startsWith("wss://") && !v.startsWith("ws://")) return "Must start with http://, https://, ws://, or wss://";
|
|
7221
|
-
return true;
|
|
7222
|
-
}
|
|
7716
|
+
/** Prompt the user for confirmation (returns true if yes). */
|
|
7717
|
+
async function confirm$2(message) {
|
|
7718
|
+
const rl = createInterface({
|
|
7719
|
+
input: process.stdin,
|
|
7720
|
+
output: process.stdout
|
|
7223
7721
|
});
|
|
7224
|
-
|
|
7225
|
-
|
|
7226
|
-
|
|
7227
|
-
|
|
7228
|
-
|
|
7229
|
-
mask: "*",
|
|
7230
|
-
validate: (value) => {
|
|
7231
|
-
if (!value.trim()) return "This field is required";
|
|
7232
|
-
return true;
|
|
7233
|
-
}
|
|
7722
|
+
return new Promise((resolve) => {
|
|
7723
|
+
rl.question(`${message} [y/N] `, (answer) => {
|
|
7724
|
+
rl.close();
|
|
7725
|
+
resolve(answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes");
|
|
7726
|
+
});
|
|
7234
7727
|
});
|
|
7235
7728
|
}
|
|
7236
|
-
/**
|
|
7237
|
-
async function
|
|
7238
|
-
|
|
7239
|
-
|
|
7240
|
-
|
|
7241
|
-
|
|
7242
|
-
|
|
7729
|
+
/** Download a file from a URL to a local path. */
|
|
7730
|
+
async function downloadArchive(url, destPath) {
|
|
7731
|
+
console.log(`Downloading from ${url}...`);
|
|
7732
|
+
const res = await fetch(url);
|
|
7733
|
+
if (!res.ok || !res.body) throw new Error(`Download failed: ${res.status} ${res.statusText}`);
|
|
7734
|
+
const fileStream = createWriteStream(destPath);
|
|
7735
|
+
await pipeline(res.body, fileStream);
|
|
7736
|
+
console.log(`Downloaded to ${destPath}`);
|
|
7243
7737
|
}
|
|
7244
|
-
var init_prompts = __esmMin((() => {}));
|
|
7245
|
-
//#endregion
|
|
7246
|
-
//#region src/cli/wizard/orchestrator-wizard.ts
|
|
7247
7738
|
/**
|
|
7248
|
-
*
|
|
7739
|
+
* Install base for a KiCI component instance.
|
|
7249
7740
|
*
|
|
7250
|
-
*
|
|
7251
|
-
*
|
|
7252
|
-
*
|
|
7741
|
+
* Name-scoped so that two instances of the same component (e.g. an org's
|
|
7742
|
+
* dogfood orchestrator and an E2E test orchestrator) own independent
|
|
7743
|
+
* versioned trees and symlinks. Per-platform bases:
|
|
7744
|
+
* - systemd / compose: /opt/kici/<name>/
|
|
7745
|
+
* - launchd: /usr/local/kici/<name>/
|
|
7746
|
+
* - windows: C:\Program Files\KiCI\<name>\
|
|
7253
7747
|
*/
|
|
7254
|
-
|
|
7748
|
+
function getInstallBase(platform, name) {
|
|
7749
|
+
const sep = platform === "windows" ? "\\" : "/";
|
|
7750
|
+
switch (platform) {
|
|
7751
|
+
case "systemd":
|
|
7752
|
+
case "compose": return `/opt/kici/${name}${sep}`;
|
|
7753
|
+
case "launchd": return `/usr/local/kici/${name}${sep}`;
|
|
7754
|
+
case "windows": return `C:\\Program Files\\KiCI\\${name}${sep}`;
|
|
7755
|
+
}
|
|
7756
|
+
}
|
|
7757
|
+
/** Check if the platform is Windows. */
|
|
7758
|
+
function isWindows(platform) {
|
|
7759
|
+
return platform === "windows";
|
|
7760
|
+
}
|
|
7761
|
+
/** Get the launcher script name for a component. */
|
|
7762
|
+
function getLauncherName(component, platform) {
|
|
7763
|
+
const baseName = component === "orchestrator" ? "kici-orchestrator-standalone" : "kici-agent";
|
|
7764
|
+
return isWindows(platform) ? `${baseName}.cmd` : baseName;
|
|
7765
|
+
}
|
|
7255
7766
|
/**
|
|
7256
|
-
*
|
|
7257
|
-
*
|
|
7258
|
-
|
|
7259
|
-
|
|
7260
|
-
|
|
7261
|
-
|
|
7767
|
+
* Extract an archive (.tar.gz or .zip) to a destination directory.
|
|
7768
|
+
* Returns the name of the top-level directory inside the archive.
|
|
7769
|
+
*/
|
|
7770
|
+
function extractArchive(archivePath, destDir) {
|
|
7771
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
7772
|
+
if (archivePath.endsWith(".zip")) if (os.platform() === "win32") execSync(`powershell -Command "Expand-Archive -Path '${archivePath}' -DestinationPath '${destDir}' -Force"`, { stdio: "inherit" });
|
|
7773
|
+
else execSync(`unzip -o "${archivePath}" -d "${destDir}"`, { stdio: "inherit" });
|
|
7774
|
+
else execSync(`tar -xzf "${archivePath}" -C "${destDir}"`, { stdio: "inherit" });
|
|
7775
|
+
const dirs = fs.readdirSync(destDir).filter((e) => fs.statSync(path.join(destDir, e)).isDirectory());
|
|
7776
|
+
if (dirs.length === 0) throw new Error("Archive does not contain a directory");
|
|
7777
|
+
return dirs[0];
|
|
7778
|
+
}
|
|
7779
|
+
/**
|
|
7780
|
+
* List installed versions for a component by scanning the install base directory
|
|
7781
|
+
* for directories matching `{component}-{version}/`.
|
|
7782
|
+
*/
|
|
7783
|
+
function listInstalledVersions(installBase, component) {
|
|
7784
|
+
if (!fs.existsSync(installBase)) return [];
|
|
7785
|
+
const prefix = `${component}-`;
|
|
7786
|
+
return fs.readdirSync(installBase).filter((entry) => {
|
|
7787
|
+
if (!entry.startsWith(prefix)) return false;
|
|
7788
|
+
const fullPath = path.join(installBase, entry);
|
|
7789
|
+
return fs.statSync(fullPath).isDirectory();
|
|
7790
|
+
}).map((entry) => entry.slice(prefix.length)).sort();
|
|
7791
|
+
}
|
|
7792
|
+
/**
|
|
7793
|
+
* Read the current symlink target to determine the active version.
|
|
7794
|
+
* Returns null if no symlink exists or on Windows.
|
|
7795
|
+
*/
|
|
7796
|
+
function getCurrentVersion(installBase, component, platform) {
|
|
7797
|
+
if (isWindows(platform)) {
|
|
7798
|
+
const versionFile = path.join(installBase, `${component}-current-version.txt`);
|
|
7799
|
+
try {
|
|
7800
|
+
return fs.readFileSync(versionFile, "utf-8").trim();
|
|
7801
|
+
} catch {
|
|
7802
|
+
return null;
|
|
7803
|
+
}
|
|
7804
|
+
}
|
|
7805
|
+
const symlinkPath = path.join(installBase, component);
|
|
7806
|
+
try {
|
|
7807
|
+
const target = fs.readlinkSync(symlinkPath);
|
|
7808
|
+
const prefix = `${component}-`;
|
|
7809
|
+
if (target.startsWith(prefix)) return target.slice(prefix.length);
|
|
7810
|
+
const basename = path.basename(target);
|
|
7811
|
+
if (basename.startsWith(prefix)) return basename.slice(prefix.length);
|
|
7812
|
+
} catch {}
|
|
7813
|
+
return null;
|
|
7814
|
+
}
|
|
7815
|
+
/** Write the current version to a tracking file (used on Windows). */
|
|
7816
|
+
function writeCurrentVersion(installBase, component, version) {
|
|
7817
|
+
const versionFile = path.join(installBase, `${component}-current-version.txt`);
|
|
7818
|
+
fs.writeFileSync(versionFile, version, "utf-8");
|
|
7819
|
+
}
|
|
7820
|
+
/**
|
|
7821
|
+
* Update the symlink atomically on Unix.
|
|
7822
|
+
* Creates a temporary symlink then renames it over the existing one.
|
|
7823
|
+
*/
|
|
7824
|
+
function updateSymlinkAtomic(installBase, component, version) {
|
|
7825
|
+
const symlinkPath = path.join(installBase, component);
|
|
7826
|
+
const tmpLink = `${symlinkPath}.tmp.${Date.now()}`;
|
|
7827
|
+
const target = `${component}-${version}`;
|
|
7828
|
+
try {
|
|
7829
|
+
fs.symlinkSync(target, tmpLink);
|
|
7830
|
+
fs.renameSync(tmpLink, symlinkPath);
|
|
7831
|
+
} catch (err) {
|
|
7832
|
+
try {
|
|
7833
|
+
fs.unlinkSync(tmpLink);
|
|
7834
|
+
} catch {}
|
|
7835
|
+
throw err;
|
|
7836
|
+
}
|
|
7837
|
+
}
|
|
7838
|
+
/**
|
|
7839
|
+
* Perform a versioned directory upgrade for a KiCI component.
|
|
7840
|
+
*
|
|
7841
|
+
* Flow:
|
|
7842
|
+
* 1. Parse upgrade source (--from archive or --url)
|
|
7843
|
+
* 2. Determine install base by platform
|
|
7844
|
+
* 3. Extract new versioned directory
|
|
7845
|
+
* 4. Stop service
|
|
7846
|
+
* 5. Update symlink (Unix) or service registration (Windows)
|
|
7847
|
+
* 6. Start service
|
|
7848
|
+
*/
|
|
7849
|
+
async function performVersionedUpgrade(component, opts) {
|
|
7850
|
+
try {
|
|
7851
|
+
const platform = detectPlatform(opts.platform);
|
|
7852
|
+
const manager = await createServiceManager(platform);
|
|
7853
|
+
const userLevel = !isRoot();
|
|
7854
|
+
const kiciRoot = kiciConfigRoot(userLevel);
|
|
7855
|
+
const { config, installBase, resolvedInstance } = await resolveUpgradeTarget({
|
|
7856
|
+
component,
|
|
7857
|
+
opts: {
|
|
7858
|
+
instanceDir: opts.instanceDir,
|
|
7859
|
+
name: opts.name
|
|
7860
|
+
},
|
|
7861
|
+
manager,
|
|
7862
|
+
isUserLevel: userLevel,
|
|
7863
|
+
kiciRoot
|
|
7864
|
+
});
|
|
7865
|
+
if (!isWindows(platform) && !userLevel && !isRoot()) {
|
|
7866
|
+
console.error("Error: root privileges required to upgrade system-level services");
|
|
7867
|
+
process.exit(1);
|
|
7868
|
+
}
|
|
7869
|
+
if (opts.rollback) {
|
|
7870
|
+
await handleRollback(component, platform, installBase, config, manager, opts);
|
|
7871
|
+
return;
|
|
7872
|
+
}
|
|
7873
|
+
if (opts.cleanup) {
|
|
7874
|
+
await handleCleanup(component, platform, installBase);
|
|
7875
|
+
return;
|
|
7876
|
+
}
|
|
7877
|
+
if (!opts.from && !opts.url) {
|
|
7878
|
+
console.error("Error: provide --from <archive-path> or --url <url> for the upgrade package");
|
|
7879
|
+
process.exit(1);
|
|
7880
|
+
}
|
|
7881
|
+
if (!opts.version) {
|
|
7882
|
+
console.error("Error: --version is required to specify the target version");
|
|
7883
|
+
process.exit(1);
|
|
7884
|
+
}
|
|
7885
|
+
const version = opts.version;
|
|
7886
|
+
const versionedDirName = `${component}-${version}`;
|
|
7887
|
+
const versionedDirPath = path.join(installBase, versionedDirName);
|
|
7888
|
+
if (fs.existsSync(versionedDirPath)) if (opts.force) {
|
|
7889
|
+
console.log(`Removing existing directory ${versionedDirPath} (--force)`);
|
|
7890
|
+
fs.rmSync(versionedDirPath, {
|
|
7891
|
+
recursive: true,
|
|
7892
|
+
force: true
|
|
7893
|
+
});
|
|
7894
|
+
} else {
|
|
7895
|
+
console.error(`Error: version directory already exists: ${versionedDirPath}`);
|
|
7896
|
+
console.error("Use --force to overwrite.");
|
|
7897
|
+
process.exit(1);
|
|
7898
|
+
}
|
|
7899
|
+
if (!await manager.isInstalled(config)) {
|
|
7900
|
+
console.error(`Error: service "${config.name}" is not installed`);
|
|
7901
|
+
process.exit(1);
|
|
7902
|
+
}
|
|
7903
|
+
let archivePath;
|
|
7904
|
+
const tmpDir = path.join(os.tmpdir(), `kici-upgrade-${Date.now()}`);
|
|
7905
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
7906
|
+
if (opts.from) {
|
|
7907
|
+
archivePath = path.resolve(opts.from);
|
|
7908
|
+
if (!fs.existsSync(archivePath)) {
|
|
7909
|
+
console.error(`Error: archive not found at ${archivePath}`);
|
|
7910
|
+
process.exit(1);
|
|
7911
|
+
}
|
|
7912
|
+
} else {
|
|
7913
|
+
const ext = opts.url.endsWith(".zip") ? ".zip" : ".tar.gz";
|
|
7914
|
+
archivePath = path.join(tmpDir, `${component}-${version}${ext}`);
|
|
7915
|
+
await downloadArchive(opts.url, archivePath);
|
|
7916
|
+
}
|
|
7917
|
+
const currentVersion = getCurrentVersion(installBase, component, platform);
|
|
7918
|
+
if (!opts.yes) {
|
|
7919
|
+
console.log(`This will upgrade "${config.name}" to version ${version}:`);
|
|
7920
|
+
if (currentVersion) console.log(` Current version: ${currentVersion}`);
|
|
7921
|
+
console.log(` New version: ${version}`);
|
|
7922
|
+
console.log(` Install path: ${versionedDirPath}`);
|
|
7923
|
+
console.log(" The service will be stopped during upgrade.");
|
|
7924
|
+
console.log("");
|
|
7925
|
+
if (!await confirm$2("Proceed with upgrade?")) {
|
|
7926
|
+
console.log("Upgrade cancelled.");
|
|
7927
|
+
return;
|
|
7928
|
+
}
|
|
7929
|
+
}
|
|
7930
|
+
console.log("Extracting archive...");
|
|
7931
|
+
const extractDir = path.join(tmpDir, "extract");
|
|
7932
|
+
const extractedDirName = extractArchive(archivePath, extractDir);
|
|
7933
|
+
fs.mkdirSync(installBase, { recursive: true });
|
|
7934
|
+
const srcDir = path.join(extractDir, extractedDirName);
|
|
7935
|
+
if (isWindows(platform)) execSync(`xcopy "${srcDir}" "${versionedDirPath}" /E /I /Q /Y`, { stdio: "inherit" });
|
|
7936
|
+
else execSync(`cp -r "${srcDir}" "${versionedDirPath}"`, { stdio: "inherit" });
|
|
7937
|
+
console.log(`Extracted to ${versionedDirPath}`);
|
|
7938
|
+
console.log("Stopping service...");
|
|
7939
|
+
if ((await manager.status(config)).state === "running") {
|
|
7940
|
+
await manager.stop(config);
|
|
7941
|
+
console.log("Service stopped.");
|
|
7942
|
+
}
|
|
7943
|
+
if (isWindows(platform)) {
|
|
7944
|
+
const launcherPath = path.join(versionedDirPath, getLauncherName(component, platform));
|
|
7945
|
+
config.executablePath = launcherPath;
|
|
7946
|
+
await manager.uninstall(config);
|
|
7947
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
7948
|
+
await manager.install(config);
|
|
7949
|
+
writeCurrentVersion(installBase, component, version);
|
|
7950
|
+
console.log(`Service registration updated to ${launcherPath}`);
|
|
7951
|
+
} else {
|
|
7952
|
+
updateSymlinkAtomic(installBase, component, version);
|
|
7953
|
+
const symlinkPath = path.join(installBase, component);
|
|
7954
|
+
console.log(`Symlink updated: ${symlinkPath} -> ${versionedDirName}`);
|
|
7955
|
+
const launcherPath = path.join(symlinkPath, getLauncherName(component, platform));
|
|
7956
|
+
if (fs.existsSync(launcherPath)) fs.chmodSync(launcherPath, 493);
|
|
7957
|
+
config.executablePath = path.join(installBase, component, getLauncherName(component, platform));
|
|
7958
|
+
}
|
|
7959
|
+
console.log("Starting service...");
|
|
7960
|
+
await manager.start(config);
|
|
7961
|
+
console.log("Service started.");
|
|
7962
|
+
writeManifest(resolvedInstance.instanceDir, {
|
|
7963
|
+
...resolvedInstance.manifest,
|
|
7964
|
+
kiciVersion: version
|
|
7965
|
+
});
|
|
7966
|
+
fs.rmSync(tmpDir, {
|
|
7967
|
+
recursive: true,
|
|
7968
|
+
force: true
|
|
7969
|
+
});
|
|
7970
|
+
console.log("");
|
|
7971
|
+
if (currentVersion) {
|
|
7972
|
+
console.log(`Upgrade complete: ${currentVersion} -> ${version}`);
|
|
7973
|
+
console.log(`Previous version preserved at: ${path.join(installBase, `${component}-${currentVersion}`)}`);
|
|
7974
|
+
} else console.log(`Upgrade to ${version} complete.`);
|
|
7975
|
+
} catch (err) {
|
|
7976
|
+
console.error(`Error: ${toErrorMessage(err)}`);
|
|
7977
|
+
process.exit(1);
|
|
7978
|
+
}
|
|
7979
|
+
}
|
|
7980
|
+
/**
|
|
7981
|
+
* Handle --rollback: switch symlink to the previous version and restart.
|
|
7982
|
+
*/
|
|
7983
|
+
async function handleRollback(component, platform, installBase, config, manager, opts) {
|
|
7984
|
+
const versions = listInstalledVersions(installBase, component);
|
|
7985
|
+
if (versions.length < 2) {
|
|
7986
|
+
console.error("Error: no previous version available for rollback");
|
|
7987
|
+
if (versions.length === 1) console.error(`Only version installed: ${versions[0]}`);
|
|
7988
|
+
process.exit(1);
|
|
7989
|
+
}
|
|
7990
|
+
const currentVersion = getCurrentVersion(installBase, component, platform);
|
|
7991
|
+
if (!currentVersion) {
|
|
7992
|
+
console.error("Error: cannot determine current version (no symlink found)");
|
|
7993
|
+
console.log("Available versions:");
|
|
7994
|
+
for (const v of versions) console.log(` ${component}-${v}/`);
|
|
7995
|
+
process.exit(1);
|
|
7996
|
+
}
|
|
7997
|
+
const currentIdx = versions.indexOf(currentVersion);
|
|
7998
|
+
let previousVersion;
|
|
7999
|
+
if (currentIdx > 0) previousVersion = versions[currentIdx - 1];
|
|
8000
|
+
else if (versions.length >= 2) previousVersion = versions[1];
|
|
8001
|
+
else {
|
|
8002
|
+
console.error("Error: no alternative version available for rollback");
|
|
8003
|
+
process.exit(1);
|
|
8004
|
+
return;
|
|
8005
|
+
}
|
|
8006
|
+
if (!opts.yes) {
|
|
8007
|
+
console.log(`Rolling back "${config.name}":`);
|
|
8008
|
+
console.log(` Current version: ${currentVersion}`);
|
|
8009
|
+
console.log(` Rollback to: ${previousVersion}`);
|
|
8010
|
+
console.log("");
|
|
8011
|
+
if (!await confirm$2("Proceed with rollback?")) {
|
|
8012
|
+
console.log("Rollback cancelled.");
|
|
8013
|
+
return;
|
|
8014
|
+
}
|
|
8015
|
+
}
|
|
8016
|
+
console.log("Stopping service...");
|
|
8017
|
+
if ((await manager.status(config)).state === "running") {
|
|
8018
|
+
await manager.stop(config);
|
|
8019
|
+
console.log("Service stopped.");
|
|
8020
|
+
}
|
|
8021
|
+
if (isWindows(platform)) {
|
|
8022
|
+
const launcherPath = path.join(installBase, `${component}-${previousVersion}`, getLauncherName(component, platform));
|
|
8023
|
+
config.executablePath = launcherPath;
|
|
8024
|
+
await manager.uninstall(config);
|
|
8025
|
+
await manager.install(config);
|
|
8026
|
+
writeCurrentVersion(installBase, component, previousVersion);
|
|
8027
|
+
console.log(`Service registration updated to ${launcherPath}`);
|
|
8028
|
+
} else {
|
|
8029
|
+
updateSymlinkAtomic(installBase, component, previousVersion);
|
|
8030
|
+
console.log(`Symlink updated: ${path.join(installBase, component)} -> ${component}-${previousVersion}`);
|
|
8031
|
+
}
|
|
8032
|
+
console.log("Starting service...");
|
|
8033
|
+
await manager.start(config);
|
|
8034
|
+
console.log("Service started.");
|
|
8035
|
+
console.log("");
|
|
8036
|
+
console.log(`Rollback complete: ${currentVersion} -> ${previousVersion}`);
|
|
8037
|
+
}
|
|
8038
|
+
/**
|
|
8039
|
+
* Handle --cleanup: remove all versioned directories except the current
|
|
8040
|
+
* and previous versions.
|
|
8041
|
+
*/
|
|
8042
|
+
async function handleCleanup(component, platform, installBase) {
|
|
8043
|
+
const versions = listInstalledVersions(installBase, component);
|
|
8044
|
+
if (versions.length <= 2) {
|
|
8045
|
+
console.log("Nothing to clean up (2 or fewer versions installed).");
|
|
8046
|
+
return;
|
|
8047
|
+
}
|
|
8048
|
+
const currentVersion = getCurrentVersion(installBase, component, platform);
|
|
8049
|
+
const currentIdx = currentVersion ? versions.indexOf(currentVersion) : versions.length - 1;
|
|
8050
|
+
const previousIdx = currentIdx > 0 ? currentIdx - 1 : -1;
|
|
8051
|
+
const toRemove = versions.filter((_, i) => i !== currentIdx && i !== previousIdx);
|
|
8052
|
+
if (toRemove.length === 0) {
|
|
8053
|
+
console.log("Nothing to clean up.");
|
|
8054
|
+
return;
|
|
8055
|
+
}
|
|
8056
|
+
console.log("The following versions will be removed:");
|
|
8057
|
+
for (const v of toRemove) console.log(` ${component}-${v}/`);
|
|
8058
|
+
if (currentVersion) console.log(`\nKeeping: ${component}-${currentVersion}/ (current)`);
|
|
8059
|
+
if (previousIdx >= 0) console.log(`Keeping: ${component}-${versions[previousIdx]}/ (previous)`);
|
|
8060
|
+
for (const v of toRemove) {
|
|
8061
|
+
const dirPath = path.join(installBase, `${component}-${v}`);
|
|
8062
|
+
fs.rmSync(dirPath, {
|
|
8063
|
+
recursive: true,
|
|
8064
|
+
force: true
|
|
8065
|
+
});
|
|
8066
|
+
console.log(`Removed ${dirPath}`);
|
|
8067
|
+
}
|
|
8068
|
+
console.log(`\nCleanup complete. Removed ${toRemove.length} old version(s).`);
|
|
8069
|
+
}
|
|
8070
|
+
//#endregion
|
|
8071
|
+
//#region src/cli/wizard/prompts.ts
|
|
8072
|
+
/**
|
|
8073
|
+
* Shared prompt utilities for the setup wizards.
|
|
8074
|
+
*
|
|
8075
|
+
* Wraps @inquirer/prompts with consistent formatting and
|
|
8076
|
+
* validation for common input types (DB URLs, ports, etc.).
|
|
8077
|
+
*/
|
|
8078
|
+
/** Prompt for a PostgreSQL database URL with validation. */
|
|
8079
|
+
async function promptDbUrl() {
|
|
8080
|
+
return input({
|
|
8081
|
+
message: "PostgreSQL database URL:",
|
|
8082
|
+
validate: (value) => {
|
|
8083
|
+
const v = value.trim();
|
|
8084
|
+
if (!v.startsWith("postgresql://") && !v.startsWith("postgres://")) return "Must start with postgresql:// or postgres://";
|
|
8085
|
+
return true;
|
|
8086
|
+
}
|
|
8087
|
+
});
|
|
8088
|
+
}
|
|
8089
|
+
/** Prompt for a port number with validation. */
|
|
8090
|
+
async function promptPort(defaultPort) {
|
|
8091
|
+
const value = await input({
|
|
8092
|
+
message: "Port:",
|
|
8093
|
+
default: String(defaultPort),
|
|
8094
|
+
validate: (value) => {
|
|
8095
|
+
const n = parseInt(value.trim(), 10);
|
|
8096
|
+
if (isNaN(n) || n < 1 || n > 65535) return "Must be a number between 1 and 65535";
|
|
8097
|
+
return true;
|
|
8098
|
+
}
|
|
8099
|
+
});
|
|
8100
|
+
return parseInt(value.trim(), 10);
|
|
8101
|
+
}
|
|
8102
|
+
/** Prompt for a yes/no confirmation. */
|
|
8103
|
+
async function promptConfirm(message, defaultValue = true) {
|
|
8104
|
+
return confirm({
|
|
8105
|
+
message,
|
|
8106
|
+
default: defaultValue
|
|
8107
|
+
});
|
|
8108
|
+
}
|
|
8109
|
+
/** Prompt for a URL with http(s):// validation. */
|
|
8110
|
+
async function promptUrl(message, defaultValue) {
|
|
8111
|
+
return input({
|
|
8112
|
+
message,
|
|
8113
|
+
default: defaultValue,
|
|
8114
|
+
validate: (value) => {
|
|
8115
|
+
const v = value.trim();
|
|
8116
|
+
if (!v.startsWith("http://") && !v.startsWith("https://") && !v.startsWith("wss://") && !v.startsWith("ws://")) return "Must start with http://, https://, ws://, or wss://";
|
|
8117
|
+
return true;
|
|
8118
|
+
}
|
|
8119
|
+
});
|
|
8120
|
+
}
|
|
8121
|
+
/** Prompt for a secret/password (masked input). */
|
|
8122
|
+
async function promptSecret(message) {
|
|
8123
|
+
return password({
|
|
8124
|
+
message,
|
|
8125
|
+
mask: "*",
|
|
8126
|
+
validate: (value) => {
|
|
8127
|
+
if (!value.trim()) return "This field is required";
|
|
8128
|
+
return true;
|
|
8129
|
+
}
|
|
8130
|
+
});
|
|
8131
|
+
}
|
|
8132
|
+
/** Prompt for a selection from a list of options. */
|
|
8133
|
+
async function promptSelect(message, choices, defaultValue) {
|
|
8134
|
+
return select({
|
|
8135
|
+
message,
|
|
8136
|
+
choices,
|
|
8137
|
+
default: defaultValue
|
|
8138
|
+
});
|
|
8139
|
+
}
|
|
8140
|
+
var init_prompts = __esmMin((() => {}));
|
|
8141
|
+
//#endregion
|
|
8142
|
+
//#region src/cli/wizard/orchestrator-wizard.ts
|
|
8143
|
+
/**
|
|
8144
|
+
* Interactive wizard for orchestrator service setup.
|
|
8145
|
+
*
|
|
8146
|
+
* Walks the user through essential configuration (mode, DB URL,
|
|
8147
|
+
* port, secrets key) with sensible defaults. Returns a config
|
|
8148
|
+
* object that the install command uses to write the env file.
|
|
8149
|
+
*/
|
|
8150
|
+
var orchestrator_wizard_exports = /* @__PURE__ */ __exportAll({ runOrchestratorWizard: () => runOrchestratorWizard });
|
|
8151
|
+
/**
|
|
8152
|
+
* Run the interactive orchestrator setup wizard.
|
|
8153
|
+
*
|
|
8154
|
+
* Asks only essential questions per the user decision:
|
|
8155
|
+
* 1. Mode (platform/hybrid/independent)
|
|
8156
|
+
* 2. Database URL
|
|
8157
|
+
* 3. Port
|
|
7262
8158
|
* 4. Secrets encryption key
|
|
7263
8159
|
* 5. Bootstrap admin token (for kici-admin authentication)
|
|
7264
8160
|
* 6. Platform URL + token (if platform/hybrid mode)
|
|
@@ -7390,11 +8286,22 @@ function startDevPostgres(containerName) {
|
|
|
7390
8286
|
if (err instanceof Error && err.message.includes("already exists")) throw err;
|
|
7391
8287
|
}
|
|
7392
8288
|
console.log(`Starting dev PostgreSQL container "${containerName}" on port ${port}...`);
|
|
7393
|
-
|
|
8289
|
+
const tmpEnvFile = path.join(fs.mkdtempSync(path.join(os.tmpdir(), "kici-dev-pg-")), "postgres.env");
|
|
8290
|
+
try {
|
|
8291
|
+
fs.writeFileSync(tmpEnvFile, `POSTGRES_PASSWORD=${password}\n`, { mode: 384 });
|
|
8292
|
+
execSync(`${runtime} run -d --name ${containerName} -p ${port}:5432 --env-file ${tmpEnvFile} -e POSTGRES_DB=kici postgres:18-trixie`, { stdio: "inherit" });
|
|
8293
|
+
} finally {
|
|
8294
|
+
try {
|
|
8295
|
+
fs.rmSync(path.dirname(tmpEnvFile), {
|
|
8296
|
+
recursive: true,
|
|
8297
|
+
force: true
|
|
8298
|
+
});
|
|
8299
|
+
} catch {}
|
|
8300
|
+
}
|
|
7394
8301
|
return `postgresql://postgres:${password}@localhost:${port}/kici`;
|
|
7395
8302
|
}
|
|
7396
8303
|
function registerOrchestratorInstall(orchestrator) {
|
|
7397
|
-
orchestrator.command("install").description("Install the orchestrator as a system service").option("--platform <type>", "Service platform (systemd, launchd, windows, compose)").option("--env-file <path>", "Path to existing env/config file to use").option("--binary <path>", "Path to orchestrator binary (default: current executable)").option("--dev", "Dev mode: spin up PostgreSQL container on port 15432").option("--wizard", "Interactive wizard for guided setup").option("--name <name>", "Service name", "kici-orchestrator").option("--system", "Install as system-level service (requires root)").option("--user-level", "Install as user-level service (no root required)").option("--user <name>", "Run the service as the named user (system-level launchd only; sets UserName in plist so the daemon drops privileges)").action(async (opts) => {
|
|
8304
|
+
orchestrator.command("install").description("Install the orchestrator as a system service").option("--platform <type>", "Service platform (systemd, launchd, windows, compose)").option("--env-file <path>", "Path to existing env/config file to use").option("--binary <path>", "Path to orchestrator binary (default: current executable)").option("--dev", "Dev mode: spin up PostgreSQL container on port 15432").option("--wizard", "Interactive wizard for guided setup").option("--name <name>", "Service name", "kici-orchestrator").option("--system", "Install as system-level service (requires root)").option("--user-level", "Install as user-level service (no root required)").option("--user <name>", "Run the service as the named user (system-level launchd only; sets UserName in plist so the daemon drops privileges)").option("--instance-dir <path>", "Deploy folder; the instance manifest is written here (default: current working directory)").option("--force", "Overwrite an existing same-named foreign instance").action(async (opts) => {
|
|
7398
8305
|
try {
|
|
7399
8306
|
if (opts.wizard && opts.envFile) {
|
|
7400
8307
|
console.error("Error: Cannot use --wizard with --env-file");
|
|
@@ -7403,9 +8310,24 @@ function registerOrchestratorInstall(orchestrator) {
|
|
|
7403
8310
|
const platform = detectPlatform(opts.platform);
|
|
7404
8311
|
const userLevel = resolveUserLevel(opts);
|
|
7405
8312
|
const serviceName = opts.name;
|
|
8313
|
+
const instanceDir = path.resolve(opts.instanceDir ?? process.cwd());
|
|
8314
|
+
const kiciRoot = kiciConfigRoot(userLevel);
|
|
7406
8315
|
console.log(`Platform: ${platform}`);
|
|
7407
8316
|
console.log(`Privilege: ${userLevel ? "user" : "system"}`);
|
|
7408
8317
|
console.log(`Service name: ${serviceName}`);
|
|
8318
|
+
console.log(`Instance dir: ${instanceDir}`);
|
|
8319
|
+
const manager = await createServiceManager(platform);
|
|
8320
|
+
const existing = (await listInstances({
|
|
8321
|
+
component: "orchestrator",
|
|
8322
|
+
isUserLevel: userLevel,
|
|
8323
|
+
kiciRoot,
|
|
8324
|
+
manager
|
|
8325
|
+
})).find((c) => c.name === serviceName);
|
|
8326
|
+
if (existing && existing.instanceDir !== instanceDir && !opts.force) {
|
|
8327
|
+
const at = existing.instanceDir ?? "(no manifest)";
|
|
8328
|
+
console.error(`Error: an orchestrator instance "${serviceName}" is already installed at ${at}. Pass a different --name, a different --instance-dir, or --force to overwrite.`);
|
|
8329
|
+
process.exit(1);
|
|
8330
|
+
}
|
|
7409
8331
|
const configDir = getConfigDir(serviceName, userLevel);
|
|
7410
8332
|
const logDir = getLogDir(serviceName, userLevel);
|
|
7411
8333
|
fs.mkdirSync(configDir, { recursive: true });
|
|
@@ -7441,707 +8363,412 @@ function registerOrchestratorInstall(orchestrator) {
|
|
|
7441
8363
|
let envContent = `# KiCI orchestrator configuration\n# See docs for all available options\n`;
|
|
7442
8364
|
if (devDbUrl) envContent += `KICI_DATABASE_URL=${devDbUrl}\n`;
|
|
7443
8365
|
fs.writeFileSync(envFilePath, envContent, "utf-8");
|
|
7444
|
-
console.log(`Created env file at ${envFilePath}`);
|
|
7445
|
-
} else if (devDbUrl) {
|
|
7446
|
-
fs.appendFileSync(envFilePath, `\nKICI_DATABASE_URL=${devDbUrl}\n`);
|
|
7447
|
-
console.log(`Appended KICI_DATABASE_URL to ${envFilePath}`);
|
|
7448
|
-
}
|
|
7449
|
-
const entryScript = opts.binary ? void 0 : fileURLToPath(import.meta.resolve(`@kici-dev/orchestrator/${selectServerEntry(fs.readFileSync(envFilePath, "utf-8"))}`));
|
|
7450
|
-
const { executablePath, args } = resolveServiceExecutable({
|
|
7451
|
-
binary: opts.binary ? path.resolve(opts.binary) : void 0,
|
|
7452
|
-
nodePath: process.execPath,
|
|
7453
|
-
entryScript
|
|
7454
|
-
});
|
|
7455
|
-
if (userLevel) try {
|
|
7456
|
-
const envContent = fs.readFileSync(envFilePath, "utf-8");
|
|
7457
|
-
if (envContent.includes("firecracker") || envContent.includes("FIRECRACKER")) {
|
|
7458
|
-
console.warn("\nWARNING: Firecracker scaler requires root privileges.");
|
|
7459
|
-
console.warn("The service is being installed at user level. Firecracker will not work.");
|
|
7460
|
-
console.warn("Re-run as root (sudo) to install a system-level service.\n");
|
|
7461
|
-
}
|
|
7462
|
-
} catch {}
|
|
7463
|
-
const config = {
|
|
7464
|
-
name: serviceName,
|
|
7465
|
-
displayName: "KiCI Orchestrator",
|
|
7466
|
-
description: "KiCI CI/CD workflow orchestrator service",
|
|
7467
|
-
executablePath,
|
|
7468
|
-
args,
|
|
7469
|
-
nodeBinDir: path.dirname(process.execPath),
|
|
7470
|
-
envFilePath,
|
|
7471
|
-
workingDirectory: configDir,
|
|
7472
|
-
isUserLevel: userLevel,
|
|
7473
|
-
user: opts.user,
|
|
7474
|
-
restartPolicy: DEFAULT_RESTART_POLICY
|
|
7475
|
-
};
|
|
7476
|
-
await (await createServiceManager(platform)).install(config);
|
|
7477
|
-
console.log(`\nOrchestrator service "${serviceName}" installed successfully.`);
|
|
7478
|
-
console.log(` Config: ${envFilePath}`);
|
|
7479
|
-
console.log(` Logs: ${logDir}`);
|
|
7480
|
-
console.log(`\nNext steps:`);
|
|
7481
|
-
console.log(` 1. Edit ${envFilePath} with your configuration`);
|
|
7482
|
-
console.log(` 2. Run \`kici-admin orchestrator start\` to start the service`);
|
|
7483
|
-
} catch (err) {
|
|
7484
|
-
console.error(`Error: ${toErrorMessage(err)}`);
|
|
7485
|
-
process.exit(1);
|
|
7486
|
-
}
|
|
7487
|
-
});
|
|
7488
|
-
}
|
|
7489
|
-
//#endregion
|
|
7490
|
-
//#region src/cli/commands/orchestrator-service/uninstall.ts
|
|
7491
|
-
function registerOrchestratorUninstall(orchestrator) {
|
|
7492
|
-
orchestrator.command("uninstall").description("Remove the orchestrator service registration").option("--platform <type>", "Service platform (systemd, launchd, windows, compose)").option("--name <name>", "Service name", "kici-orchestrator").option("--system", "Operate against the system-level service (requires root)").option("--user-level", "Operate against the user-level service").action(async (opts) => {
|
|
7493
|
-
try {
|
|
7494
|
-
const platform = detectPlatform(opts.platform);
|
|
7495
|
-
const userLevel = resolveUserLevel(opts);
|
|
7496
|
-
const serviceName = opts.name;
|
|
7497
|
-
const configDir = getConfigDir(serviceName, userLevel);
|
|
7498
|
-
const logDir = getLogDir(serviceName, userLevel);
|
|
7499
|
-
const config = {
|
|
7500
|
-
name: serviceName,
|
|
7501
|
-
displayName: "KiCI Orchestrator",
|
|
7502
|
-
description: "KiCI CI/CD workflow orchestrator service",
|
|
7503
|
-
executablePath: "",
|
|
7504
|
-
envFilePath: `${configDir}${serviceName}.env`,
|
|
7505
|
-
workingDirectory: configDir,
|
|
7506
|
-
isUserLevel: userLevel,
|
|
7507
|
-
restartPolicy: DEFAULT_RESTART_POLICY
|
|
7508
|
-
};
|
|
7509
|
-
const manager = await createServiceManager(platform);
|
|
7510
|
-
if (!await manager.isInstalled(config)) {
|
|
7511
|
-
console.log(`Service "${serviceName}" is not installed.`);
|
|
7512
|
-
process.exit(0);
|
|
7513
|
-
}
|
|
7514
|
-
try {
|
|
7515
|
-
if ((await manager.status(config)).state === "running") {
|
|
7516
|
-
console.log(`Stopping service "${serviceName}"...`);
|
|
7517
|
-
await manager.stop(config);
|
|
7518
|
-
}
|
|
7519
|
-
} catch {}
|
|
7520
|
-
await manager.uninstall(config);
|
|
7521
|
-
console.log(`\nOrchestrator service "${serviceName}" uninstalled.`);
|
|
7522
|
-
console.log(`\nThe following files were preserved for manual cleanup:`);
|
|
7523
|
-
console.log(` Config: ${configDir}`);
|
|
7524
|
-
console.log(` Logs: ${logDir}`);
|
|
7525
|
-
console.log(` Database: check your DATABASE_URL in the env file`);
|
|
7526
|
-
} catch (err) {
|
|
7527
|
-
console.error(`Error: ${toErrorMessage(err)}`);
|
|
7528
|
-
process.exit(1);
|
|
7529
|
-
}
|
|
7530
|
-
});
|
|
7531
|
-
}
|
|
7532
|
-
//#endregion
|
|
7533
|
-
//#region src/cli/commands/orchestrator-service/start.ts
|
|
7534
|
-
function registerOrchestratorStart(orchestrator) {
|
|
7535
|
-
orchestrator.command("start").description("Start the orchestrator service").option("--platform <type>", "Service platform (systemd, launchd, windows, compose)").option("--name <name>", "Service name", "kici-orchestrator").option("--system", "Operate against the system-level service (requires root)").option("--user-level", "Operate against the user-level service").action(async (opts) => {
|
|
7536
|
-
try {
|
|
7537
|
-
const platform = detectPlatform(opts.platform);
|
|
7538
|
-
const userLevel = resolveUserLevel(opts);
|
|
7539
|
-
const serviceName = opts.name;
|
|
7540
|
-
const configDir = getConfigDir(serviceName, userLevel);
|
|
7541
|
-
const config = {
|
|
7542
|
-
name: serviceName,
|
|
7543
|
-
displayName: "KiCI Orchestrator",
|
|
7544
|
-
description: "KiCI CI/CD workflow orchestrator service",
|
|
7545
|
-
executablePath: "",
|
|
7546
|
-
envFilePath: path.join(configDir, `${serviceName}.env`),
|
|
7547
|
-
workingDirectory: configDir,
|
|
7548
|
-
isUserLevel: userLevel,
|
|
7549
|
-
restartPolicy: DEFAULT_RESTART_POLICY
|
|
7550
|
-
};
|
|
7551
|
-
const manager = await createServiceManager(platform);
|
|
7552
|
-
if (!await manager.isInstalled(config)) {
|
|
7553
|
-
console.error(`Error: service "${serviceName}" is not installed.`);
|
|
7554
|
-
console.error(`Run \`kici-admin orchestrator install\` first.`);
|
|
7555
|
-
process.exit(1);
|
|
7556
|
-
}
|
|
7557
|
-
await manager.start(config);
|
|
7558
|
-
console.log(`Orchestrator service "${serviceName}" started.`);
|
|
7559
|
-
} catch (err) {
|
|
7560
|
-
console.error(`Error: ${toErrorMessage(err)}`);
|
|
7561
|
-
process.exit(1);
|
|
7562
|
-
}
|
|
7563
|
-
});
|
|
7564
|
-
}
|
|
7565
|
-
//#endregion
|
|
7566
|
-
//#region src/cli/commands/orchestrator-service/stop.ts
|
|
7567
|
-
function registerOrchestratorStop(orchestrator) {
|
|
7568
|
-
orchestrator.command("stop").description("Stop the orchestrator service").option("--platform <type>", "Service platform (systemd, launchd, windows, compose)").option("--name <name>", "Service name", "kici-orchestrator").option("--system", "Operate against the system-level service (requires root)").option("--user-level", "Operate against the user-level service").action(async (opts) => {
|
|
7569
|
-
try {
|
|
7570
|
-
const platform = detectPlatform(opts.platform);
|
|
7571
|
-
const userLevel = resolveUserLevel(opts);
|
|
7572
|
-
const serviceName = opts.name;
|
|
7573
|
-
const configDir = getConfigDir(serviceName, userLevel);
|
|
8366
|
+
console.log(`Created env file at ${envFilePath}`);
|
|
8367
|
+
} else if (devDbUrl) {
|
|
8368
|
+
fs.appendFileSync(envFilePath, `\nKICI_DATABASE_URL=${devDbUrl}\n`);
|
|
8369
|
+
console.log(`Appended KICI_DATABASE_URL to ${envFilePath}`);
|
|
8370
|
+
}
|
|
8371
|
+
const entryScript = opts.binary ? void 0 : fileURLToPath(import.meta.resolve(`@kici-dev/orchestrator/${selectServerEntry(fs.readFileSync(envFilePath, "utf-8"))}`));
|
|
8372
|
+
const { executablePath, args } = resolveServiceExecutable({
|
|
8373
|
+
binary: opts.binary ? path.resolve(opts.binary) : void 0,
|
|
8374
|
+
nodePath: process.execPath,
|
|
8375
|
+
entryScript
|
|
8376
|
+
});
|
|
8377
|
+
if (userLevel) try {
|
|
8378
|
+
const envContent = fs.readFileSync(envFilePath, "utf-8");
|
|
8379
|
+
if (envContent.includes("firecracker") || envContent.includes("FIRECRACKER")) {
|
|
8380
|
+
console.warn("\nWARNING: Firecracker scaler requires root privileges.");
|
|
8381
|
+
console.warn("The service is being installed at user level. Firecracker will not work.");
|
|
8382
|
+
console.warn("Re-run as root (sudo) to install a system-level service.\n");
|
|
8383
|
+
}
|
|
8384
|
+
} catch {}
|
|
7574
8385
|
const config = {
|
|
7575
8386
|
name: serviceName,
|
|
7576
8387
|
displayName: "KiCI Orchestrator",
|
|
7577
8388
|
description: "KiCI CI/CD workflow orchestrator service",
|
|
7578
|
-
executablePath
|
|
7579
|
-
|
|
8389
|
+
executablePath,
|
|
8390
|
+
args,
|
|
8391
|
+
nodeBinDir: path.dirname(process.execPath),
|
|
8392
|
+
envFilePath,
|
|
7580
8393
|
workingDirectory: configDir,
|
|
7581
8394
|
isUserLevel: userLevel,
|
|
8395
|
+
user: opts.user,
|
|
8396
|
+
component: "orchestrator",
|
|
7582
8397
|
restartPolicy: DEFAULT_RESTART_POLICY
|
|
7583
8398
|
};
|
|
7584
|
-
await
|
|
7585
|
-
|
|
7586
|
-
|
|
7587
|
-
console.error(`Error: ${toErrorMessage(err)}`);
|
|
7588
|
-
process.exit(1);
|
|
7589
|
-
}
|
|
7590
|
-
});
|
|
7591
|
-
}
|
|
7592
|
-
//#endregion
|
|
7593
|
-
//#region src/cli/commands/orchestrator-service/restart.ts
|
|
7594
|
-
function registerOrchestratorRestart(orchestrator) {
|
|
7595
|
-
orchestrator.command("restart").description("Restart the orchestrator service").option("--platform <type>", "Service platform (systemd, launchd, windows, compose)").option("--name <name>", "Service name", "kici-orchestrator").option("--system", "Operate against the system-level service (requires root)").option("--user-level", "Operate against the user-level service").action(async (opts) => {
|
|
7596
|
-
try {
|
|
7597
|
-
const platform = detectPlatform(opts.platform);
|
|
7598
|
-
const userLevel = resolveUserLevel(opts);
|
|
7599
|
-
const serviceName = opts.name;
|
|
7600
|
-
const configDir = getConfigDir(serviceName, userLevel);
|
|
7601
|
-
const config = {
|
|
8399
|
+
await manager.install(config);
|
|
8400
|
+
const manifestFile = writeManifest(instanceDir, {
|
|
8401
|
+
component: "orchestrator",
|
|
7602
8402
|
name: serviceName,
|
|
7603
|
-
|
|
7604
|
-
description: "KiCI CI/CD workflow orchestrator service",
|
|
7605
|
-
executablePath: "",
|
|
7606
|
-
envFilePath: `${configDir}${serviceName}.env`,
|
|
7607
|
-
workingDirectory: configDir,
|
|
8403
|
+
platform,
|
|
7608
8404
|
isUserLevel: userLevel,
|
|
7609
|
-
|
|
7610
|
-
|
|
7611
|
-
|
|
7612
|
-
|
|
7613
|
-
|
|
7614
|
-
|
|
7615
|
-
|
|
7616
|
-
|
|
7617
|
-
|
|
7618
|
-
|
|
7619
|
-
|
|
7620
|
-
|
|
7621
|
-
|
|
7622
|
-
|
|
7623
|
-
|
|
7624
|
-
}
|
|
7625
|
-
|
|
7626
|
-
//#region src/cli/commands/orchestrator-service/status.ts
|
|
7627
|
-
/** Read the port from the env file in the config directory. */
|
|
7628
|
-
function readPortFromEnv$1(configDir, serviceName) {
|
|
7629
|
-
const envPath = path.join(configDir, `${serviceName}.env`);
|
|
7630
|
-
if (!fs.existsSync(envPath)) return 4e3;
|
|
7631
|
-
try {
|
|
7632
|
-
const content = fs.readFileSync(envPath, "utf-8");
|
|
7633
|
-
for (const line of content.split("\n")) {
|
|
7634
|
-
const trimmed = line.trim();
|
|
7635
|
-
if (trimmed.startsWith("#") || !trimmed.includes("=")) continue;
|
|
7636
|
-
const [key, ...rest] = trimmed.split("=");
|
|
7637
|
-
if (key?.trim() === "KICI_PORT") {
|
|
7638
|
-
const val = rest.join("=").trim().replace(/^["']|["']$/g, "");
|
|
7639
|
-
const parsed = parseInt(val, 10);
|
|
7640
|
-
if (!isNaN(parsed)) return parsed;
|
|
8405
|
+
envFilePath,
|
|
8406
|
+
configDir,
|
|
8407
|
+
logDir,
|
|
8408
|
+
installBase: getInstallBase(platform, serviceName),
|
|
8409
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8410
|
+
kiciVersion: readKiciVersion()
|
|
8411
|
+
});
|
|
8412
|
+
try {
|
|
8413
|
+
appendIndexEntry(kiciRoot, {
|
|
8414
|
+
component: "orchestrator",
|
|
8415
|
+
name: serviceName,
|
|
8416
|
+
platform,
|
|
8417
|
+
isUserLevel: userLevel,
|
|
8418
|
+
instanceDir
|
|
8419
|
+
});
|
|
8420
|
+
} catch (err) {
|
|
8421
|
+
console.warn(`Warning: instance index append failed: ${err.message}`);
|
|
7641
8422
|
}
|
|
7642
|
-
|
|
7643
|
-
|
|
7644
|
-
|
|
7645
|
-
}
|
|
7646
|
-
|
|
7647
|
-
|
|
7648
|
-
|
|
7649
|
-
const controller = new AbortController();
|
|
7650
|
-
const timeout = setTimeout(() => controller.abort(), 3e3);
|
|
7651
|
-
const res = await fetch(`http://localhost:${port}/health`, { signal: controller.signal });
|
|
7652
|
-
clearTimeout(timeout);
|
|
7653
|
-
if (!res.ok) return null;
|
|
7654
|
-
return await res.json();
|
|
7655
|
-
} catch {
|
|
7656
|
-
return null;
|
|
7657
|
-
}
|
|
7658
|
-
}
|
|
7659
|
-
/** Format the status output as a readable table. */
|
|
7660
|
-
function formatStatus$1(serviceStatus, health, serviceName) {
|
|
7661
|
-
const lines = [];
|
|
7662
|
-
lines.push(`Service: ${serviceName}`);
|
|
7663
|
-
lines.push(`State: ${serviceStatus.state}`);
|
|
7664
|
-
if (serviceStatus.pid) lines.push(`PID: ${serviceStatus.pid}`);
|
|
7665
|
-
if (serviceStatus.uptime != null) lines.push(`Uptime: ${formatUptime(serviceStatus.uptime)}`);
|
|
7666
|
-
if (serviceStatus.startedAt) lines.push(`Started: ${serviceStatus.startedAt}`);
|
|
7667
|
-
if (health) {
|
|
7668
|
-
lines.push("");
|
|
7669
|
-
lines.push("--- KiCI orchestrator ---");
|
|
7670
|
-
if (health.mode) lines.push(`Mode: ${health.mode}`);
|
|
7671
|
-
if (health.port) lines.push(`Port: ${health.port}`);
|
|
7672
|
-
if (health.database) lines.push(`Database: ${health.database}`);
|
|
7673
|
-
if (health.platformRelay) lines.push(`Platform relay: ${health.platformRelay}`);
|
|
7674
|
-
if (health.agents != null) lines.push(`Agents: ${health.agents}`);
|
|
7675
|
-
if (health.scaler) {
|
|
7676
|
-
const s = health.scaler;
|
|
7677
|
-
lines.push(`Scaler: ${s.type ?? "none"} (warm: ${s.warm ?? 0}, max: ${s.max ?? 0})`);
|
|
7678
|
-
}
|
|
7679
|
-
if (health.jobs) lines.push(`Jobs: ${health.jobs.pending ?? 0} pending, ${health.jobs.running ?? 0} running`);
|
|
7680
|
-
} else if (serviceStatus.state === "running") {
|
|
7681
|
-
lines.push("");
|
|
7682
|
-
lines.push("(Could not reach health API)");
|
|
7683
|
-
}
|
|
7684
|
-
return lines.join("\n");
|
|
7685
|
-
}
|
|
7686
|
-
/** Build JSON output combining service + health data. */
|
|
7687
|
-
function buildJsonOutput$1(serviceStatus, health, serviceName) {
|
|
7688
|
-
return {
|
|
7689
|
-
service: serviceName,
|
|
7690
|
-
...serviceStatus,
|
|
7691
|
-
health: health ?? void 0
|
|
7692
|
-
};
|
|
7693
|
-
}
|
|
7694
|
-
function registerStatusCommand(parent) {
|
|
7695
|
-
parent.command("status").description("Show orchestrator service status and health information").option("--platform <type>", "Service platform (systemd|launchd|windows|compose)").option("--name <name>", "Service name", "kici-orchestrator").option("--json", "Output as JSON").action(async (opts) => {
|
|
7696
|
-
try {
|
|
7697
|
-
const manager = await createServiceManager(detectPlatform(opts.platform));
|
|
7698
|
-
const userLevel = !isRoot();
|
|
7699
|
-
const configDir = getConfigDir(opts.name, userLevel);
|
|
7700
|
-
const config = {
|
|
7701
|
-
name: opts.name,
|
|
7702
|
-
displayName: "KiCI orchestrator",
|
|
7703
|
-
description: "KiCI orchestrator service",
|
|
7704
|
-
executablePath: "",
|
|
7705
|
-
envFilePath: path.join(configDir, `${opts.name}.env`),
|
|
7706
|
-
workingDirectory: configDir,
|
|
7707
|
-
isUserLevel: userLevel,
|
|
7708
|
-
restartPolicy: {
|
|
7709
|
-
enabled: true,
|
|
7710
|
-
delays: [
|
|
7711
|
-
1,
|
|
7712
|
-
5,
|
|
7713
|
-
15,
|
|
7714
|
-
30
|
|
7715
|
-
],
|
|
7716
|
-
maxRetries: 5,
|
|
7717
|
-
windowSeconds: 300
|
|
7718
|
-
}
|
|
7719
|
-
};
|
|
7720
|
-
const serviceStatus = await manager.status(config);
|
|
7721
|
-
let health = null;
|
|
7722
|
-
if (serviceStatus.state === "running") health = await queryHealth$1(readPortFromEnv$1(configDir, opts.name));
|
|
7723
|
-
if (opts.json) console.log(JSON.stringify(buildJsonOutput$1(serviceStatus, health, opts.name), null, 2));
|
|
7724
|
-
else console.log(formatStatus$1(serviceStatus, health, opts.name));
|
|
7725
|
-
} catch (err) {
|
|
7726
|
-
console.error(`Error: ${toErrorMessage(err)}`);
|
|
7727
|
-
process.exit(1);
|
|
7728
|
-
}
|
|
7729
|
-
});
|
|
7730
|
-
}
|
|
7731
|
-
//#endregion
|
|
7732
|
-
//#region src/cli/commands/orchestrator-service/logs.ts
|
|
7733
|
-
function registerLogsCommand(parent) {
|
|
7734
|
-
parent.command("logs").description("Tail and follow orchestrator service logs").option("--platform <type>", "Service platform (systemd|launchd|windows|compose)").option("--name <name>", "Service name", "kici-orchestrator").option("--since <duration>", "Show logs since duration (e.g. 1h, 30m)").option("--level <level>", "Filter by log level (error|warn|info)").option("--json", "Output as structured JSON").option("--no-follow", "Snapshot mode (do not tail)").action(async (opts) => {
|
|
7735
|
-
try {
|
|
7736
|
-
const manager = await createServiceManager(detectPlatform(opts.platform));
|
|
7737
|
-
const userLevel = !isRoot();
|
|
7738
|
-
const configDir = getConfigDir(opts.name, userLevel);
|
|
7739
|
-
const config = {
|
|
7740
|
-
name: opts.name,
|
|
7741
|
-
displayName: "KiCI orchestrator",
|
|
7742
|
-
description: "KiCI orchestrator service",
|
|
7743
|
-
executablePath: "",
|
|
7744
|
-
envFilePath: path.join(configDir, `${opts.name}.env`),
|
|
7745
|
-
workingDirectory: configDir,
|
|
7746
|
-
isUserLevel: userLevel,
|
|
7747
|
-
restartPolicy: {
|
|
7748
|
-
enabled: true,
|
|
7749
|
-
delays: [
|
|
7750
|
-
1,
|
|
7751
|
-
5,
|
|
7752
|
-
15,
|
|
7753
|
-
30
|
|
7754
|
-
],
|
|
7755
|
-
maxRetries: 5,
|
|
7756
|
-
windowSeconds: 300
|
|
7757
|
-
}
|
|
7758
|
-
};
|
|
7759
|
-
const logOptions = {
|
|
7760
|
-
since: opts.since,
|
|
7761
|
-
level: opts.level,
|
|
7762
|
-
json: opts.json,
|
|
7763
|
-
follow: opts.follow
|
|
7764
|
-
};
|
|
7765
|
-
await manager.logs(config, logOptions);
|
|
8423
|
+
console.log(`\nOrchestrator service "${serviceName}" installed successfully.`);
|
|
8424
|
+
console.log(` Config: ${envFilePath}`);
|
|
8425
|
+
console.log(` Logs: ${logDir}`);
|
|
8426
|
+
console.log(` Manifest: ${manifestFile}`);
|
|
8427
|
+
console.log(`\nNext steps:`);
|
|
8428
|
+
console.log(` 1. Edit ${envFilePath} with your configuration`);
|
|
8429
|
+
console.log(` 2. Run \`kici-admin orchestrator start\` to start the service`);
|
|
7766
8430
|
} catch (err) {
|
|
7767
8431
|
console.error(`Error: ${toErrorMessage(err)}`);
|
|
7768
8432
|
process.exit(1);
|
|
7769
8433
|
}
|
|
7770
|
-
});
|
|
7771
|
-
}
|
|
7772
|
-
//#endregion
|
|
7773
|
-
//#region src/cli/commands/shared/versioned-upgrade.ts
|
|
7774
|
-
/**
|
|
7775
|
-
* Shared versioned directory upgrade logic for kici-admin upgrade commands.
|
|
7776
|
-
*
|
|
7777
|
-
* Implements the versioned directory layout:
|
|
7778
|
-
* - Extract new version alongside old versions
|
|
7779
|
-
* - Update symlink (Unix) or service registration (Windows) atomically
|
|
7780
|
-
* - Preserve old versions for rollback
|
|
7781
|
-
* - Optional cleanup of old versions
|
|
7782
|
-
*/
|
|
7783
|
-
/** Prompt the user for confirmation (returns true if yes). */
|
|
7784
|
-
async function confirm$2(message) {
|
|
7785
|
-
const rl = createInterface({
|
|
7786
|
-
input: process.stdin,
|
|
7787
|
-
output: process.stdout
|
|
7788
|
-
});
|
|
7789
|
-
return new Promise((resolve) => {
|
|
7790
|
-
rl.question(`${message} [y/N] `, (answer) => {
|
|
7791
|
-
rl.close();
|
|
7792
|
-
resolve(answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes");
|
|
7793
|
-
});
|
|
7794
|
-
});
|
|
7795
|
-
}
|
|
7796
|
-
/** Download a file from a URL to a local path. */
|
|
7797
|
-
async function downloadArchive(url, destPath) {
|
|
7798
|
-
console.log(`Downloading from ${url}...`);
|
|
7799
|
-
const res = await fetch(url);
|
|
7800
|
-
if (!res.ok || !res.body) throw new Error(`Download failed: ${res.status} ${res.statusText}`);
|
|
7801
|
-
const fileStream = createWriteStream(destPath);
|
|
7802
|
-
await pipeline(res.body, fileStream);
|
|
7803
|
-
console.log(`Downloaded to ${destPath}`);
|
|
7804
|
-
}
|
|
7805
|
-
/**
|
|
7806
|
-
* Get the install base directory for the current platform.
|
|
7807
|
-
*
|
|
7808
|
-
* - Linux (systemd): /opt/kici/
|
|
7809
|
-
* - macOS (launchd): /usr/local/kici/
|
|
7810
|
-
* - Windows: C:\Program Files\KiCI\
|
|
7811
|
-
* - Compose: /opt/kici/ (default)
|
|
7812
|
-
*/
|
|
7813
|
-
function getInstallBase(platform) {
|
|
7814
|
-
switch (platform) {
|
|
7815
|
-
case "systemd":
|
|
7816
|
-
case "compose": return "/opt/kici/";
|
|
7817
|
-
case "launchd": return "/usr/local/kici/";
|
|
7818
|
-
case "windows": return "C:\\Program Files\\KiCI\\";
|
|
7819
|
-
}
|
|
7820
|
-
}
|
|
7821
|
-
/** Check if the platform is Windows. */
|
|
7822
|
-
function isWindows(platform) {
|
|
7823
|
-
return platform === "windows";
|
|
7824
|
-
}
|
|
7825
|
-
/** Get the launcher script name for a component. */
|
|
7826
|
-
function getLauncherName(component, platform) {
|
|
7827
|
-
const baseName = component === "orchestrator" ? "kici-orchestrator-standalone" : "kici-agent";
|
|
7828
|
-
return isWindows(platform) ? `${baseName}.cmd` : baseName;
|
|
7829
|
-
}
|
|
7830
|
-
/**
|
|
7831
|
-
* Extract an archive (.tar.gz or .zip) to a destination directory.
|
|
7832
|
-
* Returns the name of the top-level directory inside the archive.
|
|
7833
|
-
*/
|
|
7834
|
-
function extractArchive(archivePath, destDir) {
|
|
7835
|
-
fs.mkdirSync(destDir, { recursive: true });
|
|
7836
|
-
if (archivePath.endsWith(".zip")) if (os.platform() === "win32") execSync(`powershell -Command "Expand-Archive -Path '${archivePath}' -DestinationPath '${destDir}' -Force"`, { stdio: "inherit" });
|
|
7837
|
-
else execSync(`unzip -o "${archivePath}" -d "${destDir}"`, { stdio: "inherit" });
|
|
7838
|
-
else execSync(`tar -xzf "${archivePath}" -C "${destDir}"`, { stdio: "inherit" });
|
|
7839
|
-
const dirs = fs.readdirSync(destDir).filter((e) => fs.statSync(path.join(destDir, e)).isDirectory());
|
|
7840
|
-
if (dirs.length === 0) throw new Error("Archive does not contain a directory");
|
|
7841
|
-
return dirs[0];
|
|
7842
|
-
}
|
|
7843
|
-
/**
|
|
7844
|
-
* List installed versions for a component by scanning the install base directory
|
|
7845
|
-
* for directories matching `{component}-{version}/`.
|
|
7846
|
-
*/
|
|
7847
|
-
function listInstalledVersions(installBase, component) {
|
|
7848
|
-
if (!fs.existsSync(installBase)) return [];
|
|
7849
|
-
const prefix = `${component}-`;
|
|
7850
|
-
return fs.readdirSync(installBase).filter((entry) => {
|
|
7851
|
-
if (!entry.startsWith(prefix)) return false;
|
|
7852
|
-
const fullPath = path.join(installBase, entry);
|
|
7853
|
-
return fs.statSync(fullPath).isDirectory();
|
|
7854
|
-
}).map((entry) => entry.slice(prefix.length)).sort();
|
|
7855
|
-
}
|
|
7856
|
-
/**
|
|
7857
|
-
* Read the current symlink target to determine the active version.
|
|
7858
|
-
* Returns null if no symlink exists or on Windows.
|
|
7859
|
-
*/
|
|
7860
|
-
function getCurrentVersion(installBase, component, platform) {
|
|
7861
|
-
if (isWindows(platform)) {
|
|
7862
|
-
const versionFile = path.join(installBase, `${component}-current-version.txt`);
|
|
7863
|
-
try {
|
|
7864
|
-
return fs.readFileSync(versionFile, "utf-8").trim();
|
|
7865
|
-
} catch {
|
|
7866
|
-
return null;
|
|
7867
|
-
}
|
|
7868
|
-
}
|
|
7869
|
-
const symlinkPath = path.join(installBase, component);
|
|
7870
|
-
try {
|
|
7871
|
-
const target = fs.readlinkSync(symlinkPath);
|
|
7872
|
-
const prefix = `${component}-`;
|
|
7873
|
-
if (target.startsWith(prefix)) return target.slice(prefix.length);
|
|
7874
|
-
const basename = path.basename(target);
|
|
7875
|
-
if (basename.startsWith(prefix)) return basename.slice(prefix.length);
|
|
7876
|
-
} catch {}
|
|
7877
|
-
return null;
|
|
7878
|
-
}
|
|
7879
|
-
/** Write the current version to a tracking file (used on Windows). */
|
|
7880
|
-
function writeCurrentVersion(installBase, component, version) {
|
|
7881
|
-
const versionFile = path.join(installBase, `${component}-current-version.txt`);
|
|
7882
|
-
fs.writeFileSync(versionFile, version, "utf-8");
|
|
8434
|
+
});
|
|
7883
8435
|
}
|
|
7884
|
-
|
|
7885
|
-
|
|
7886
|
-
|
|
7887
|
-
|
|
7888
|
-
function updateSymlinkAtomic(installBase, component, version) {
|
|
7889
|
-
const symlinkPath = path.join(installBase, component);
|
|
7890
|
-
const tmpLink = `${symlinkPath}.tmp.${Date.now()}`;
|
|
7891
|
-
const target = `${component}-${version}`;
|
|
7892
|
-
try {
|
|
7893
|
-
fs.symlinkSync(target, tmpLink);
|
|
7894
|
-
fs.renameSync(tmpLink, symlinkPath);
|
|
7895
|
-
} catch (err) {
|
|
8436
|
+
//#endregion
|
|
8437
|
+
//#region src/cli/commands/orchestrator-service/uninstall.ts
|
|
8438
|
+
function registerOrchestratorUninstall(orchestrator) {
|
|
8439
|
+
orchestrator.command("uninstall").description("Remove the orchestrator service registration").option("--platform <type>", "Service platform (systemd, launchd, windows, compose)").option("--instance-dir <path>", "Deploy folder of the instance to uninstall").option("--name <name>", "Service name (no default — must resolve via flag/CWD)").option("--system", "Operate against the system-level service (requires root)").option("--user-level", "Operate against the user-level service").action(async (opts) => {
|
|
7896
8440
|
try {
|
|
7897
|
-
|
|
7898
|
-
|
|
7899
|
-
|
|
7900
|
-
|
|
7901
|
-
|
|
7902
|
-
|
|
7903
|
-
|
|
7904
|
-
|
|
7905
|
-
|
|
7906
|
-
|
|
7907
|
-
|
|
7908
|
-
|
|
7909
|
-
|
|
7910
|
-
|
|
7911
|
-
|
|
7912
|
-
|
|
7913
|
-
|
|
7914
|
-
|
|
7915
|
-
|
|
7916
|
-
|
|
7917
|
-
|
|
7918
|
-
|
|
7919
|
-
|
|
7920
|
-
|
|
7921
|
-
|
|
7922
|
-
|
|
7923
|
-
|
|
7924
|
-
|
|
7925
|
-
|
|
7926
|
-
|
|
7927
|
-
|
|
7928
|
-
|
|
7929
|
-
|
|
7930
|
-
|
|
7931
|
-
|
|
7932
|
-
5,
|
|
7933
|
-
15,
|
|
7934
|
-
30
|
|
7935
|
-
],
|
|
7936
|
-
maxRetries: 5,
|
|
7937
|
-
windowSeconds: 300
|
|
8441
|
+
const platform = detectPlatform(opts.platform);
|
|
8442
|
+
const userLevel = resolveUserLevel(opts);
|
|
8443
|
+
const manager = await createServiceManager(platform);
|
|
8444
|
+
const kiciRoot = kiciConfigRoot(userLevel);
|
|
8445
|
+
const resolved = await resolveInstance({
|
|
8446
|
+
component: "orchestrator",
|
|
8447
|
+
opts: {
|
|
8448
|
+
instanceDir: opts.instanceDir,
|
|
8449
|
+
name: opts.name
|
|
8450
|
+
},
|
|
8451
|
+
cwd: process.cwd(),
|
|
8452
|
+
kiciRoot,
|
|
8453
|
+
manager,
|
|
8454
|
+
isUserLevel: userLevel
|
|
8455
|
+
});
|
|
8456
|
+
const config = {
|
|
8457
|
+
name: resolved.manifest.name,
|
|
8458
|
+
displayName: "KiCI Orchestrator",
|
|
8459
|
+
description: "KiCI CI/CD workflow orchestrator service",
|
|
8460
|
+
executablePath: "",
|
|
8461
|
+
envFilePath: resolved.manifest.envFilePath,
|
|
8462
|
+
workingDirectory: resolved.manifest.configDir,
|
|
8463
|
+
isUserLevel: resolved.manifest.isUserLevel,
|
|
8464
|
+
restartPolicy: DEFAULT_RESTART_POLICY,
|
|
8465
|
+
component: "orchestrator"
|
|
8466
|
+
};
|
|
8467
|
+
if (!await manager.isInstalled(config)) console.log(`Service "${config.name}" is not installed.`);
|
|
8468
|
+
else {
|
|
8469
|
+
try {
|
|
8470
|
+
if ((await manager.status(config)).state === "running") {
|
|
8471
|
+
console.log(`Stopping service "${config.name}"...`);
|
|
8472
|
+
await manager.stop(config);
|
|
8473
|
+
}
|
|
8474
|
+
} catch {}
|
|
8475
|
+
await manager.uninstall(config);
|
|
7938
8476
|
}
|
|
7939
|
-
|
|
7940
|
-
|
|
7941
|
-
|
|
7942
|
-
|
|
7943
|
-
|
|
7944
|
-
|
|
7945
|
-
|
|
7946
|
-
|
|
7947
|
-
}
|
|
7948
|
-
if (opts.cleanup) {
|
|
7949
|
-
await handleCleanup(component, platform, installBase);
|
|
7950
|
-
return;
|
|
7951
|
-
}
|
|
7952
|
-
if (!opts.from && !opts.url) {
|
|
7953
|
-
console.error("Error: provide --from <archive-path> or --url <url> for the upgrade package");
|
|
7954
|
-
process.exit(1);
|
|
7955
|
-
}
|
|
7956
|
-
if (!opts.version) {
|
|
7957
|
-
console.error("Error: --version is required to specify the target version");
|
|
8477
|
+
removeIndexEntry(kiciRoot, {
|
|
8478
|
+
component: "orchestrator",
|
|
8479
|
+
name: config.name
|
|
8480
|
+
});
|
|
8481
|
+
console.log(`\nOrchestrator service "${config.name}" uninstalled.`);
|
|
8482
|
+
console.log(`Manifest preserved at ${resolved.manifestPath} — delete manually if no longer needed.`);
|
|
8483
|
+
} catch (err) {
|
|
8484
|
+
console.error(`Error: ${toErrorMessage(err)}`);
|
|
7958
8485
|
process.exit(1);
|
|
7959
8486
|
}
|
|
7960
|
-
|
|
7961
|
-
|
|
7962
|
-
|
|
7963
|
-
|
|
7964
|
-
|
|
7965
|
-
|
|
7966
|
-
|
|
7967
|
-
|
|
8487
|
+
});
|
|
8488
|
+
}
|
|
8489
|
+
//#endregion
|
|
8490
|
+
//#region src/cli/commands/orchestrator-service/start.ts
|
|
8491
|
+
function registerOrchestratorStart(orchestrator) {
|
|
8492
|
+
orchestrator.command("start").description("Start the orchestrator service").option("--platform <type>", "Service platform (systemd, launchd, windows, compose)").option("--instance-dir <path>", "Deploy folder of the instance to start").option("--name <name>", "Service name (no default — must resolve via flag/CWD)").option("--system", "Operate against the system-level service (requires root)").option("--user-level", "Operate against the user-level service").action(async (opts) => {
|
|
8493
|
+
try {
|
|
8494
|
+
const platform = detectPlatform(opts.platform);
|
|
8495
|
+
const userLevel = resolveUserLevel(opts);
|
|
8496
|
+
const manager = await createServiceManager(platform);
|
|
8497
|
+
const kiciRoot = kiciConfigRoot(userLevel);
|
|
8498
|
+
const resolved = await resolveInstance({
|
|
8499
|
+
component: "orchestrator",
|
|
8500
|
+
opts: {
|
|
8501
|
+
instanceDir: opts.instanceDir,
|
|
8502
|
+
name: opts.name
|
|
8503
|
+
},
|
|
8504
|
+
cwd: process.cwd(),
|
|
8505
|
+
kiciRoot,
|
|
8506
|
+
manager,
|
|
8507
|
+
isUserLevel: userLevel
|
|
7968
8508
|
});
|
|
7969
|
-
|
|
7970
|
-
|
|
7971
|
-
|
|
8509
|
+
const config = {
|
|
8510
|
+
name: resolved.manifest.name,
|
|
8511
|
+
displayName: "KiCI Orchestrator",
|
|
8512
|
+
description: "KiCI CI/CD workflow orchestrator service",
|
|
8513
|
+
executablePath: "",
|
|
8514
|
+
envFilePath: resolved.manifest.envFilePath,
|
|
8515
|
+
workingDirectory: resolved.manifest.configDir,
|
|
8516
|
+
isUserLevel: resolved.manifest.isUserLevel,
|
|
8517
|
+
restartPolicy: DEFAULT_RESTART_POLICY,
|
|
8518
|
+
component: "orchestrator"
|
|
8519
|
+
};
|
|
8520
|
+
if (!await manager.isInstalled(config)) {
|
|
8521
|
+
console.error(`Error: service "${config.name}" is not installed.`);
|
|
8522
|
+
console.error(`Run \`kici-admin orchestrator install\` first.`);
|
|
8523
|
+
process.exit(1);
|
|
8524
|
+
}
|
|
8525
|
+
await manager.start(config);
|
|
8526
|
+
console.log(`Orchestrator service "${config.name}" started.`);
|
|
8527
|
+
} catch (err) {
|
|
8528
|
+
console.error(`Error: ${toErrorMessage(err)}`);
|
|
7972
8529
|
process.exit(1);
|
|
7973
8530
|
}
|
|
7974
|
-
|
|
7975
|
-
|
|
8531
|
+
});
|
|
8532
|
+
}
|
|
8533
|
+
//#endregion
|
|
8534
|
+
//#region src/cli/commands/orchestrator-service/stop.ts
|
|
8535
|
+
function registerOrchestratorStop(orchestrator) {
|
|
8536
|
+
orchestrator.command("stop").description("Stop the orchestrator service").option("--platform <type>", "Service platform (systemd, launchd, windows, compose)").option("--instance-dir <path>", "Deploy folder of the instance to stop").option("--name <name>", "Service name (no default — must resolve via flag/CWD)").option("--system", "Operate against the system-level service (requires root)").option("--user-level", "Operate against the user-level service").action(async (opts) => {
|
|
8537
|
+
try {
|
|
8538
|
+
const platform = detectPlatform(opts.platform);
|
|
8539
|
+
const userLevel = resolveUserLevel(opts);
|
|
8540
|
+
const manager = await createServiceManager(platform);
|
|
8541
|
+
const kiciRoot = kiciConfigRoot(userLevel);
|
|
8542
|
+
const resolved = await resolveInstance({
|
|
8543
|
+
component: "orchestrator",
|
|
8544
|
+
opts: {
|
|
8545
|
+
instanceDir: opts.instanceDir,
|
|
8546
|
+
name: opts.name
|
|
8547
|
+
},
|
|
8548
|
+
cwd: process.cwd(),
|
|
8549
|
+
kiciRoot,
|
|
8550
|
+
manager,
|
|
8551
|
+
isUserLevel: userLevel
|
|
8552
|
+
});
|
|
8553
|
+
const config = {
|
|
8554
|
+
name: resolved.manifest.name,
|
|
8555
|
+
displayName: "KiCI Orchestrator",
|
|
8556
|
+
description: "KiCI CI/CD workflow orchestrator service",
|
|
8557
|
+
executablePath: "",
|
|
8558
|
+
envFilePath: resolved.manifest.envFilePath,
|
|
8559
|
+
workingDirectory: resolved.manifest.configDir,
|
|
8560
|
+
isUserLevel: resolved.manifest.isUserLevel,
|
|
8561
|
+
restartPolicy: DEFAULT_RESTART_POLICY,
|
|
8562
|
+
component: "orchestrator"
|
|
8563
|
+
};
|
|
8564
|
+
await manager.stop(config);
|
|
8565
|
+
console.log(`Orchestrator service "${config.name}" stopped.`);
|
|
8566
|
+
} catch (err) {
|
|
8567
|
+
console.error(`Error: ${toErrorMessage(err)}`);
|
|
7976
8568
|
process.exit(1);
|
|
7977
8569
|
}
|
|
7978
|
-
|
|
7979
|
-
|
|
7980
|
-
|
|
7981
|
-
|
|
7982
|
-
|
|
7983
|
-
|
|
7984
|
-
|
|
8570
|
+
});
|
|
8571
|
+
}
|
|
8572
|
+
//#endregion
|
|
8573
|
+
//#region src/cli/commands/orchestrator-service/restart.ts
|
|
8574
|
+
function registerOrchestratorRestart(orchestrator) {
|
|
8575
|
+
orchestrator.command("restart").description("Restart the orchestrator service").option("--platform <type>", "Service platform (systemd, launchd, windows, compose)").option("--instance-dir <path>", "Deploy folder of the instance to restart").option("--name <name>", "Service name (no default — must resolve via flag/CWD)").option("--system", "Operate against the system-level service (requires root)").option("--user-level", "Operate against the user-level service").action(async (opts) => {
|
|
8576
|
+
try {
|
|
8577
|
+
const platform = detectPlatform(opts.platform);
|
|
8578
|
+
const userLevel = resolveUserLevel(opts);
|
|
8579
|
+
const manager = await createServiceManager(platform);
|
|
8580
|
+
const kiciRoot = kiciConfigRoot(userLevel);
|
|
8581
|
+
const resolved = await resolveInstance({
|
|
8582
|
+
component: "orchestrator",
|
|
8583
|
+
opts: {
|
|
8584
|
+
instanceDir: opts.instanceDir,
|
|
8585
|
+
name: opts.name
|
|
8586
|
+
},
|
|
8587
|
+
cwd: process.cwd(),
|
|
8588
|
+
kiciRoot,
|
|
8589
|
+
manager,
|
|
8590
|
+
isUserLevel: userLevel
|
|
8591
|
+
});
|
|
8592
|
+
const config = {
|
|
8593
|
+
name: resolved.manifest.name,
|
|
8594
|
+
displayName: "KiCI Orchestrator",
|
|
8595
|
+
description: "KiCI CI/CD workflow orchestrator service",
|
|
8596
|
+
executablePath: "",
|
|
8597
|
+
envFilePath: resolved.manifest.envFilePath,
|
|
8598
|
+
workingDirectory: resolved.manifest.configDir,
|
|
8599
|
+
isUserLevel: resolved.manifest.isUserLevel,
|
|
8600
|
+
restartPolicy: DEFAULT_RESTART_POLICY,
|
|
8601
|
+
component: "orchestrator"
|
|
8602
|
+
};
|
|
8603
|
+
if (!await manager.isInstalled(config)) {
|
|
8604
|
+
console.error(`Error: service "${config.name}" is not installed.`);
|
|
8605
|
+
console.error(`Run \`kici-admin orchestrator install\` first.`);
|
|
7985
8606
|
process.exit(1);
|
|
7986
8607
|
}
|
|
7987
|
-
|
|
7988
|
-
|
|
7989
|
-
|
|
7990
|
-
|
|
8608
|
+
await manager.restart(config);
|
|
8609
|
+
console.log(`Orchestrator service "${config.name}" restarted.`);
|
|
8610
|
+
} catch (err) {
|
|
8611
|
+
console.error(`Error: ${toErrorMessage(err)}`);
|
|
8612
|
+
process.exit(1);
|
|
7991
8613
|
}
|
|
7992
|
-
|
|
7993
|
-
|
|
7994
|
-
|
|
7995
|
-
|
|
7996
|
-
|
|
7997
|
-
|
|
7998
|
-
|
|
7999
|
-
|
|
8000
|
-
|
|
8001
|
-
|
|
8002
|
-
|
|
8614
|
+
});
|
|
8615
|
+
}
|
|
8616
|
+
//#endregion
|
|
8617
|
+
//#region src/cli/commands/orchestrator-service/status.ts
|
|
8618
|
+
/** Read the port from the service's env file (path from the manifest). */
|
|
8619
|
+
function readPortFromEnvFile$1(envFilePath) {
|
|
8620
|
+
if (!fs.existsSync(envFilePath)) return 4e3;
|
|
8621
|
+
try {
|
|
8622
|
+
const content = fs.readFileSync(envFilePath, "utf-8");
|
|
8623
|
+
for (const line of content.split("\n")) {
|
|
8624
|
+
const trimmed = line.trim();
|
|
8625
|
+
if (trimmed.startsWith("#") || !trimmed.includes("=")) continue;
|
|
8626
|
+
const [key, ...rest] = trimmed.split("=");
|
|
8627
|
+
if (key?.trim() === "KICI_PORT") {
|
|
8628
|
+
const val = rest.join("=").trim().replace(/^["']|["']$/g, "");
|
|
8629
|
+
const parsed = parseInt(val, 10);
|
|
8630
|
+
if (!isNaN(parsed)) return parsed;
|
|
8003
8631
|
}
|
|
8004
8632
|
}
|
|
8005
|
-
|
|
8006
|
-
|
|
8007
|
-
|
|
8008
|
-
|
|
8009
|
-
|
|
8010
|
-
|
|
8011
|
-
|
|
8012
|
-
|
|
8013
|
-
|
|
8014
|
-
|
|
8015
|
-
|
|
8016
|
-
|
|
8017
|
-
|
|
8018
|
-
|
|
8019
|
-
const launcherPath = path.join(versionedDirPath, getLauncherName(component, platform));
|
|
8020
|
-
config.executablePath = launcherPath;
|
|
8021
|
-
await manager.uninstall(config);
|
|
8022
|
-
await new Promise((r) => setTimeout(r, 2e3));
|
|
8023
|
-
await manager.install(config);
|
|
8024
|
-
writeCurrentVersion(installBase, component, version);
|
|
8025
|
-
console.log(`Service registration updated to ${launcherPath}`);
|
|
8026
|
-
} else {
|
|
8027
|
-
updateSymlinkAtomic(installBase, component, version);
|
|
8028
|
-
const symlinkPath = path.join(installBase, component);
|
|
8029
|
-
console.log(`Symlink updated: ${symlinkPath} -> ${versionedDirName}`);
|
|
8030
|
-
const launcherPath = path.join(symlinkPath, getLauncherName(component, platform));
|
|
8031
|
-
if (fs.existsSync(launcherPath)) fs.chmodSync(launcherPath, 493);
|
|
8032
|
-
config.executablePath = path.join(installBase, component, getLauncherName(component, platform));
|
|
8033
|
-
}
|
|
8034
|
-
console.log("Starting service...");
|
|
8035
|
-
await manager.start(config);
|
|
8036
|
-
console.log("Service started.");
|
|
8037
|
-
fs.rmSync(tmpDir, {
|
|
8038
|
-
recursive: true,
|
|
8039
|
-
force: true
|
|
8040
|
-
});
|
|
8041
|
-
console.log("");
|
|
8042
|
-
if (currentVersion) {
|
|
8043
|
-
console.log(`Upgrade complete: ${currentVersion} -> ${version}`);
|
|
8044
|
-
console.log(`Previous version preserved at: ${path.join(installBase, `${component}-${currentVersion}`)}`);
|
|
8045
|
-
} else console.log(`Upgrade to ${version} complete.`);
|
|
8046
|
-
} catch (err) {
|
|
8047
|
-
console.error(`Error: ${toErrorMessage(err)}`);
|
|
8048
|
-
process.exit(1);
|
|
8633
|
+
} catch {}
|
|
8634
|
+
return 4e3;
|
|
8635
|
+
}
|
|
8636
|
+
/** Query orchestrator health API. */
|
|
8637
|
+
async function queryHealth$1(port) {
|
|
8638
|
+
try {
|
|
8639
|
+
const controller = new AbortController();
|
|
8640
|
+
const timeout = setTimeout(() => controller.abort(), 3e3);
|
|
8641
|
+
const res = await fetch(`http://localhost:${port}/health`, { signal: controller.signal });
|
|
8642
|
+
clearTimeout(timeout);
|
|
8643
|
+
if (!res.ok) return null;
|
|
8644
|
+
return await res.json();
|
|
8645
|
+
} catch {
|
|
8646
|
+
return null;
|
|
8049
8647
|
}
|
|
8050
8648
|
}
|
|
8051
|
-
/**
|
|
8052
|
-
|
|
8053
|
-
|
|
8054
|
-
|
|
8055
|
-
|
|
8056
|
-
if (
|
|
8057
|
-
|
|
8058
|
-
|
|
8059
|
-
|
|
8060
|
-
|
|
8061
|
-
|
|
8062
|
-
|
|
8063
|
-
|
|
8064
|
-
|
|
8065
|
-
|
|
8066
|
-
|
|
8067
|
-
|
|
8068
|
-
|
|
8069
|
-
|
|
8070
|
-
if (currentIdx > 0) previousVersion = versions[currentIdx - 1];
|
|
8071
|
-
else if (versions.length >= 2) previousVersion = versions[1];
|
|
8072
|
-
else {
|
|
8073
|
-
console.error("Error: no alternative version available for rollback");
|
|
8074
|
-
process.exit(1);
|
|
8075
|
-
return;
|
|
8076
|
-
}
|
|
8077
|
-
if (!opts.yes) {
|
|
8078
|
-
console.log(`Rolling back "${opts.name}":`);
|
|
8079
|
-
console.log(` Current version: ${currentVersion}`);
|
|
8080
|
-
console.log(` Rollback to: ${previousVersion}`);
|
|
8081
|
-
console.log("");
|
|
8082
|
-
if (!await confirm$2("Proceed with rollback?")) {
|
|
8083
|
-
console.log("Rollback cancelled.");
|
|
8084
|
-
return;
|
|
8649
|
+
/** Format the status output as a readable table. */
|
|
8650
|
+
function formatStatus$1(serviceStatus, health, serviceName) {
|
|
8651
|
+
const lines = [];
|
|
8652
|
+
lines.push(`Service: ${serviceName}`);
|
|
8653
|
+
lines.push(`State: ${serviceStatus.state}`);
|
|
8654
|
+
if (serviceStatus.pid) lines.push(`PID: ${serviceStatus.pid}`);
|
|
8655
|
+
if (serviceStatus.uptime != null) lines.push(`Uptime: ${formatUptime(serviceStatus.uptime)}`);
|
|
8656
|
+
if (serviceStatus.startedAt) lines.push(`Started: ${serviceStatus.startedAt}`);
|
|
8657
|
+
if (health) {
|
|
8658
|
+
lines.push("");
|
|
8659
|
+
lines.push("--- KiCI orchestrator ---");
|
|
8660
|
+
if (health.mode) lines.push(`Mode: ${health.mode}`);
|
|
8661
|
+
if (health.port) lines.push(`Port: ${health.port}`);
|
|
8662
|
+
if (health.database) lines.push(`Database: ${health.database}`);
|
|
8663
|
+
if (health.platformRelay) lines.push(`Platform relay: ${health.platformRelay}`);
|
|
8664
|
+
if (health.agents != null) lines.push(`Agents: ${health.agents}`);
|
|
8665
|
+
if (health.scaler) {
|
|
8666
|
+
const s = health.scaler;
|
|
8667
|
+
lines.push(`Scaler: ${s.type ?? "none"} (warm: ${s.warm ?? 0}, max: ${s.max ?? 0})`);
|
|
8085
8668
|
}
|
|
8669
|
+
if (health.jobs) lines.push(`Jobs: ${health.jobs.pending ?? 0} pending, ${health.jobs.running ?? 0} running`);
|
|
8670
|
+
} else if (serviceStatus.state === "running") {
|
|
8671
|
+
lines.push("");
|
|
8672
|
+
lines.push("(Could not reach health API)");
|
|
8086
8673
|
}
|
|
8087
|
-
|
|
8088
|
-
if ((await manager.status(config)).state === "running") {
|
|
8089
|
-
await manager.stop(config);
|
|
8090
|
-
console.log("Service stopped.");
|
|
8091
|
-
}
|
|
8092
|
-
if (isWindows(platform)) {
|
|
8093
|
-
const launcherPath = path.join(installBase, `${component}-${previousVersion}`, getLauncherName(component, platform));
|
|
8094
|
-
config.executablePath = launcherPath;
|
|
8095
|
-
await manager.uninstall(config);
|
|
8096
|
-
await manager.install(config);
|
|
8097
|
-
writeCurrentVersion(installBase, component, previousVersion);
|
|
8098
|
-
console.log(`Service registration updated to ${launcherPath}`);
|
|
8099
|
-
} else {
|
|
8100
|
-
updateSymlinkAtomic(installBase, component, previousVersion);
|
|
8101
|
-
console.log(`Symlink updated: ${path.join(installBase, component)} -> ${component}-${previousVersion}`);
|
|
8102
|
-
}
|
|
8103
|
-
console.log("Starting service...");
|
|
8104
|
-
await manager.start(config);
|
|
8105
|
-
console.log("Service started.");
|
|
8106
|
-
console.log("");
|
|
8107
|
-
console.log(`Rollback complete: ${currentVersion} -> ${previousVersion}`);
|
|
8674
|
+
return lines.join("\n");
|
|
8108
8675
|
}
|
|
8109
|
-
/**
|
|
8110
|
-
|
|
8111
|
-
|
|
8112
|
-
|
|
8113
|
-
|
|
8114
|
-
|
|
8115
|
-
|
|
8116
|
-
|
|
8117
|
-
|
|
8118
|
-
|
|
8119
|
-
|
|
8120
|
-
|
|
8121
|
-
|
|
8122
|
-
|
|
8123
|
-
|
|
8124
|
-
|
|
8125
|
-
|
|
8126
|
-
|
|
8127
|
-
|
|
8128
|
-
|
|
8129
|
-
|
|
8130
|
-
|
|
8131
|
-
|
|
8132
|
-
|
|
8133
|
-
|
|
8134
|
-
|
|
8135
|
-
|
|
8136
|
-
|
|
8137
|
-
|
|
8138
|
-
|
|
8139
|
-
|
|
8676
|
+
/** Build JSON output combining service + health data. */
|
|
8677
|
+
function buildJsonOutput$1(serviceStatus, health, serviceName) {
|
|
8678
|
+
return {
|
|
8679
|
+
service: serviceName,
|
|
8680
|
+
...serviceStatus,
|
|
8681
|
+
health: health ?? void 0
|
|
8682
|
+
};
|
|
8683
|
+
}
|
|
8684
|
+
function registerStatusCommand(parent) {
|
|
8685
|
+
parent.command("status").description("Show orchestrator service status and health information").option("--platform <type>", "Service platform (systemd|launchd|windows|compose)").option("--instance-dir <path>", "Deploy folder of the instance to inspect").option("--name <name>", "Service name (no default — must resolve via flag/CWD)").option("--system", "Operate against the system-level service (requires root)").option("--user-level", "Operate against the user-level service").option("--json", "Output as JSON").action(async (opts) => {
|
|
8686
|
+
try {
|
|
8687
|
+
const platform = detectPlatform(opts.platform);
|
|
8688
|
+
const userLevel = resolveUserLevel(opts);
|
|
8689
|
+
const manager = await createServiceManager(platform);
|
|
8690
|
+
const kiciRoot = kiciConfigRoot(userLevel);
|
|
8691
|
+
const resolved = await resolveInstance({
|
|
8692
|
+
component: "orchestrator",
|
|
8693
|
+
opts: {
|
|
8694
|
+
instanceDir: opts.instanceDir,
|
|
8695
|
+
name: opts.name
|
|
8696
|
+
},
|
|
8697
|
+
cwd: process.cwd(),
|
|
8698
|
+
kiciRoot,
|
|
8699
|
+
manager,
|
|
8700
|
+
isUserLevel: userLevel
|
|
8701
|
+
});
|
|
8702
|
+
const config = {
|
|
8703
|
+
name: resolved.manifest.name,
|
|
8704
|
+
displayName: "KiCI Orchestrator",
|
|
8705
|
+
description: "KiCI CI/CD workflow orchestrator service",
|
|
8706
|
+
executablePath: "",
|
|
8707
|
+
envFilePath: resolved.manifest.envFilePath,
|
|
8708
|
+
workingDirectory: resolved.manifest.configDir,
|
|
8709
|
+
isUserLevel: resolved.manifest.isUserLevel,
|
|
8710
|
+
restartPolicy: DEFAULT_RESTART_POLICY,
|
|
8711
|
+
component: "orchestrator"
|
|
8712
|
+
};
|
|
8713
|
+
const serviceStatus = await manager.status(config);
|
|
8714
|
+
let health = null;
|
|
8715
|
+
if (serviceStatus.state === "running") health = await queryHealth$1(readPortFromEnvFile$1(config.envFilePath));
|
|
8716
|
+
if (opts.json) console.log(JSON.stringify(buildJsonOutput$1(serviceStatus, health, config.name), null, 2));
|
|
8717
|
+
else console.log(formatStatus$1(serviceStatus, health, config.name));
|
|
8718
|
+
} catch (err) {
|
|
8719
|
+
console.error(`Error: ${toErrorMessage(err)}`);
|
|
8720
|
+
process.exit(1);
|
|
8721
|
+
}
|
|
8722
|
+
});
|
|
8723
|
+
}
|
|
8724
|
+
//#endregion
|
|
8725
|
+
//#region src/cli/commands/orchestrator-service/logs.ts
|
|
8726
|
+
function registerLogsCommand(parent) {
|
|
8727
|
+
parent.command("logs").description("Tail and follow orchestrator service logs").option("--platform <type>", "Service platform (systemd|launchd|windows|compose)").option("--instance-dir <path>", "Deploy folder of the instance whose logs to read").option("--name <name>", "Service name (no default — must resolve via flag/CWD)").option("--system", "Operate against the system-level service (requires root)").option("--user-level", "Operate against the user-level service").option("--since <duration>", "Show logs since duration (e.g. 1h, 30m)").option("--level <level>", "Filter by log level (error|warn|info)").option("--json", "Output as structured JSON").option("--no-follow", "Snapshot mode (do not tail)").action(async (opts) => {
|
|
8728
|
+
try {
|
|
8729
|
+
const platform = detectPlatform(opts.platform);
|
|
8730
|
+
const userLevel = resolveUserLevel(opts);
|
|
8731
|
+
const manager = await createServiceManager(platform);
|
|
8732
|
+
const kiciRoot = kiciConfigRoot(userLevel);
|
|
8733
|
+
const resolved = await resolveInstance({
|
|
8734
|
+
component: "orchestrator",
|
|
8735
|
+
opts: {
|
|
8736
|
+
instanceDir: opts.instanceDir,
|
|
8737
|
+
name: opts.name
|
|
8738
|
+
},
|
|
8739
|
+
cwd: process.cwd(),
|
|
8740
|
+
kiciRoot,
|
|
8741
|
+
manager,
|
|
8742
|
+
isUserLevel: userLevel
|
|
8743
|
+
});
|
|
8744
|
+
const config = {
|
|
8745
|
+
name: resolved.manifest.name,
|
|
8746
|
+
displayName: "KiCI Orchestrator",
|
|
8747
|
+
description: "KiCI CI/CD workflow orchestrator service",
|
|
8748
|
+
executablePath: "",
|
|
8749
|
+
envFilePath: resolved.manifest.envFilePath,
|
|
8750
|
+
workingDirectory: resolved.manifest.configDir,
|
|
8751
|
+
isUserLevel: resolved.manifest.isUserLevel,
|
|
8752
|
+
restartPolicy: DEFAULT_RESTART_POLICY,
|
|
8753
|
+
component: "orchestrator"
|
|
8754
|
+
};
|
|
8755
|
+
const logOptions = {
|
|
8756
|
+
since: opts.since,
|
|
8757
|
+
level: opts.level,
|
|
8758
|
+
json: opts.json,
|
|
8759
|
+
follow: opts.follow
|
|
8760
|
+
};
|
|
8761
|
+
await manager.logs(config, logOptions);
|
|
8762
|
+
} catch (err) {
|
|
8763
|
+
console.error(`Error: ${toErrorMessage(err)}`);
|
|
8764
|
+
process.exit(1);
|
|
8765
|
+
}
|
|
8766
|
+
});
|
|
8140
8767
|
}
|
|
8141
8768
|
//#endregion
|
|
8142
8769
|
//#region src/cli/commands/orchestrator-service/upgrade.ts
|
|
8143
8770
|
function registerUpgradeCommand(parent) {
|
|
8144
|
-
parent.command("upgrade").description("Upgrade orchestrator to a new version using versioned directory layout").option("--platform <type>", "Service platform (systemd|launchd|windows|compose)").option("--name <name>", "Service name
|
|
8771
|
+
parent.command("upgrade").description("Upgrade orchestrator to a new version using versioned directory layout").option("--platform <type>", "Service platform (systemd|launchd|windows|compose)").option("--instance-dir <path>", "Deploy folder of the instance to upgrade").option("--name <name>", "Service name (no default — must resolve via flag/CWD)").option("--from <path>", "Path to package archive (.tar.gz or .zip)").option("--url <url>", "URL to download package archive from").option("--version <version>", "Target version string (e.g., 0.3.0)").option("--yes", "Skip confirmation prompt").option("--force", "Overwrite existing versioned directory").option("--cleanup", "Remove old versions (keeps current and previous)").option("--rollback", "Roll back to the previous version").action(async (opts) => {
|
|
8145
8772
|
await performVersionedUpgrade("orchestrator", opts);
|
|
8146
8773
|
});
|
|
8147
8774
|
}
|
|
@@ -8199,18 +8826,33 @@ var init_agent_wizard = __esmMin((() => {
|
|
|
8199
8826
|
//#endregion
|
|
8200
8827
|
//#region src/cli/commands/agent-service/install.ts
|
|
8201
8828
|
function registerAgentInstall(agent) {
|
|
8202
|
-
agent.command("install").description("Install the agent as a system service").option("--platform <type>", "Service platform (systemd, launchd, windows, compose)").option("--env-file <path>", "Path to existing env/config file to use").option("--binary <path>", "Path to agent binary (default: current executable)").option("--name <name>", "Service name", "kici-agent").option("--orchestrator-url <url>", "URL of the orchestrator to connect to").option("--token <token>", "Agent authentication token").option("--labels <labels>", "Comma-separated agent labels for routing").option("--wizard", "Interactive wizard for guided setup").action(async (opts) => {
|
|
8829
|
+
agent.command("install").description("Install the agent as a system service").option("--platform <type>", "Service platform (systemd, launchd, windows, compose)").option("--env-file <path>", "Path to existing env/config file to use").option("--binary <path>", "Path to agent binary (default: current executable)").option("--name <name>", "Service name", "kici-agent").option("--orchestrator-url <url>", "URL of the orchestrator to connect to").option("--token <token>", "Agent authentication token").option("--labels <labels>", "Comma-separated agent labels for routing").option("--wizard", "Interactive wizard for guided setup").option("--system", "Install as system-level service (requires root)").option("--user-level", "Install as user-level service (no root required)").option("--instance-dir <path>", "Deploy folder; the instance manifest is written here (default: current working directory)").option("--force", "Overwrite an existing same-named foreign instance").action(async (opts) => {
|
|
8203
8830
|
try {
|
|
8204
8831
|
if (opts.wizard && opts.envFile) {
|
|
8205
8832
|
console.error("Error: Cannot use --wizard with --env-file");
|
|
8206
8833
|
process.exit(1);
|
|
8207
8834
|
}
|
|
8208
8835
|
const platform = detectPlatform(opts.platform);
|
|
8209
|
-
const userLevel =
|
|
8836
|
+
const userLevel = resolveUserLevel(opts);
|
|
8210
8837
|
const serviceName = opts.name;
|
|
8838
|
+
const instanceDir = path.resolve(opts.instanceDir ?? process.cwd());
|
|
8839
|
+
const kiciRoot = kiciConfigRoot(userLevel);
|
|
8211
8840
|
console.log(`Platform: ${platform}`);
|
|
8212
8841
|
console.log(`Privilege: ${userLevel ? "user" : "system"}`);
|
|
8213
8842
|
console.log(`Service name: ${serviceName}`);
|
|
8843
|
+
console.log(`Instance dir: ${instanceDir}`);
|
|
8844
|
+
const manager = await createServiceManager(platform);
|
|
8845
|
+
const existing = (await listInstances({
|
|
8846
|
+
component: "agent",
|
|
8847
|
+
isUserLevel: userLevel,
|
|
8848
|
+
kiciRoot,
|
|
8849
|
+
manager
|
|
8850
|
+
})).find((c) => c.name === serviceName);
|
|
8851
|
+
if (existing && existing.instanceDir !== instanceDir && !opts.force) {
|
|
8852
|
+
const at = existing.instanceDir ?? "(no manifest)";
|
|
8853
|
+
console.error(`Error: an agent instance "${serviceName}" is already installed at ${at}. Pass a different --name, a different --instance-dir, or --force to overwrite.`);
|
|
8854
|
+
process.exit(1);
|
|
8855
|
+
}
|
|
8214
8856
|
const configDir = getConfigDir(serviceName, userLevel);
|
|
8215
8857
|
const logDir = getLogDir(serviceName, userLevel);
|
|
8216
8858
|
fs.mkdirSync(configDir, { recursive: true });
|
|
@@ -8257,12 +8899,37 @@ function registerAgentInstall(agent) {
|
|
|
8257
8899
|
envFilePath,
|
|
8258
8900
|
workingDirectory: configDir,
|
|
8259
8901
|
isUserLevel: userLevel,
|
|
8902
|
+
component: "agent",
|
|
8260
8903
|
restartPolicy: DEFAULT_RESTART_POLICY
|
|
8261
8904
|
};
|
|
8262
|
-
await
|
|
8905
|
+
await manager.install(config);
|
|
8906
|
+
const manifestFile = writeManifest(instanceDir, {
|
|
8907
|
+
component: "agent",
|
|
8908
|
+
name: serviceName,
|
|
8909
|
+
platform,
|
|
8910
|
+
isUserLevel: userLevel,
|
|
8911
|
+
envFilePath,
|
|
8912
|
+
configDir,
|
|
8913
|
+
logDir,
|
|
8914
|
+
installBase: getInstallBase(platform, serviceName),
|
|
8915
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8916
|
+
kiciVersion: readKiciVersion()
|
|
8917
|
+
});
|
|
8918
|
+
try {
|
|
8919
|
+
appendIndexEntry(kiciRoot, {
|
|
8920
|
+
component: "agent",
|
|
8921
|
+
name: serviceName,
|
|
8922
|
+
platform,
|
|
8923
|
+
isUserLevel: userLevel,
|
|
8924
|
+
instanceDir
|
|
8925
|
+
});
|
|
8926
|
+
} catch (err) {
|
|
8927
|
+
console.warn(`Warning: instance index append failed: ${err.message}`);
|
|
8928
|
+
}
|
|
8263
8929
|
console.log(`\nAgent service "${serviceName}" installed successfully.`);
|
|
8264
|
-
console.log(` Config:
|
|
8265
|
-
console.log(` Logs:
|
|
8930
|
+
console.log(` Config: ${envFilePath}`);
|
|
8931
|
+
console.log(` Logs: ${logDir}`);
|
|
8932
|
+
console.log(` Manifest: ${manifestFile}`);
|
|
8266
8933
|
console.log(`\nNext steps:`);
|
|
8267
8934
|
console.log(` 1. Edit ${envFilePath} with your configuration`);
|
|
8268
8935
|
console.log(` 2. Run \`kici-admin agent start\` to start the service`);
|
|
@@ -8275,39 +8942,50 @@ function registerAgentInstall(agent) {
|
|
|
8275
8942
|
//#endregion
|
|
8276
8943
|
//#region src/cli/commands/agent-service/uninstall.ts
|
|
8277
8944
|
function registerAgentUninstall(agent) {
|
|
8278
|
-
agent.command("uninstall").description("Remove the agent service registration").option("--platform <type>", "Service platform (systemd, launchd, windows, compose)").option("--name <name>", "Service name", "
|
|
8945
|
+
agent.command("uninstall").description("Remove the agent service registration").option("--platform <type>", "Service platform (systemd, launchd, windows, compose)").option("--instance-dir <path>", "Deploy folder of the instance to uninstall").option("--name <name>", "Service name (no default — must resolve via flag/CWD)").option("--system", "Operate against the system-level service (requires root)").option("--user-level", "Operate against the user-level service").action(async (opts) => {
|
|
8279
8946
|
try {
|
|
8280
8947
|
const platform = detectPlatform(opts.platform);
|
|
8281
|
-
const userLevel =
|
|
8282
|
-
const
|
|
8283
|
-
const
|
|
8284
|
-
const
|
|
8948
|
+
const userLevel = resolveUserLevel(opts);
|
|
8949
|
+
const manager = await createServiceManager(platform);
|
|
8950
|
+
const kiciRoot = kiciConfigRoot(userLevel);
|
|
8951
|
+
const resolved = await resolveInstance({
|
|
8952
|
+
component: "agent",
|
|
8953
|
+
opts: {
|
|
8954
|
+
instanceDir: opts.instanceDir,
|
|
8955
|
+
name: opts.name
|
|
8956
|
+
},
|
|
8957
|
+
cwd: process.cwd(),
|
|
8958
|
+
kiciRoot,
|
|
8959
|
+
manager,
|
|
8960
|
+
isUserLevel: userLevel
|
|
8961
|
+
});
|
|
8285
8962
|
const config = {
|
|
8286
|
-
name:
|
|
8963
|
+
name: resolved.manifest.name,
|
|
8287
8964
|
displayName: "KiCI Agent",
|
|
8288
8965
|
description: "KiCI CI/CD workflow execution agent service",
|
|
8289
8966
|
executablePath: "",
|
|
8290
|
-
envFilePath:
|
|
8291
|
-
workingDirectory: configDir,
|
|
8292
|
-
isUserLevel:
|
|
8293
|
-
restartPolicy: DEFAULT_RESTART_POLICY
|
|
8967
|
+
envFilePath: resolved.manifest.envFilePath,
|
|
8968
|
+
workingDirectory: resolved.manifest.configDir,
|
|
8969
|
+
isUserLevel: resolved.manifest.isUserLevel,
|
|
8970
|
+
restartPolicy: DEFAULT_RESTART_POLICY,
|
|
8971
|
+
component: "agent"
|
|
8294
8972
|
};
|
|
8295
|
-
|
|
8296
|
-
|
|
8297
|
-
|
|
8298
|
-
|
|
8973
|
+
if (!await manager.isInstalled(config)) console.log(`Service "${config.name}" is not installed.`);
|
|
8974
|
+
else {
|
|
8975
|
+
try {
|
|
8976
|
+
if ((await manager.status(config)).state === "running") {
|
|
8977
|
+
console.log(`Stopping service "${config.name}"...`);
|
|
8978
|
+
await manager.stop(config);
|
|
8979
|
+
}
|
|
8980
|
+
} catch {}
|
|
8981
|
+
await manager.uninstall(config);
|
|
8299
8982
|
}
|
|
8300
|
-
|
|
8301
|
-
|
|
8302
|
-
|
|
8303
|
-
|
|
8304
|
-
|
|
8305
|
-
|
|
8306
|
-
await manager.uninstall(config);
|
|
8307
|
-
console.log(`\nAgent service "${serviceName}" uninstalled.`);
|
|
8308
|
-
console.log(`\nThe following files were preserved for manual cleanup:`);
|
|
8309
|
-
console.log(` Config: ${configDir}`);
|
|
8310
|
-
console.log(` Logs: ${logDir}`);
|
|
8983
|
+
removeIndexEntry(kiciRoot, {
|
|
8984
|
+
component: "agent",
|
|
8985
|
+
name: config.name
|
|
8986
|
+
});
|
|
8987
|
+
console.log(`\nAgent service "${config.name}" uninstalled.`);
|
|
8988
|
+
console.log(`Manifest preserved at ${resolved.manifestPath} — delete manually if no longer needed.`);
|
|
8311
8989
|
} catch (err) {
|
|
8312
8990
|
console.error(`Error: ${toErrorMessage(err)}`);
|
|
8313
8991
|
process.exit(1);
|
|
@@ -8317,30 +8995,41 @@ function registerAgentUninstall(agent) {
|
|
|
8317
8995
|
//#endregion
|
|
8318
8996
|
//#region src/cli/commands/agent-service/start.ts
|
|
8319
8997
|
function registerAgentStart(agent) {
|
|
8320
|
-
agent.command("start").description("Start the agent service").option("--platform <type>", "Service platform (systemd, launchd, windows, compose)").option("--name <name>", "Service name", "
|
|
8998
|
+
agent.command("start").description("Start the agent service").option("--platform <type>", "Service platform (systemd, launchd, windows, compose)").option("--instance-dir <path>", "Deploy folder of the instance to start").option("--name <name>", "Service name (no default — must resolve via flag/CWD)").option("--system", "Operate against the system-level service (requires root)").option("--user-level", "Operate against the user-level service").action(async (opts) => {
|
|
8321
8999
|
try {
|
|
8322
9000
|
const platform = detectPlatform(opts.platform);
|
|
8323
|
-
const userLevel =
|
|
8324
|
-
const
|
|
8325
|
-
const
|
|
9001
|
+
const userLevel = resolveUserLevel(opts);
|
|
9002
|
+
const manager = await createServiceManager(platform);
|
|
9003
|
+
const kiciRoot = kiciConfigRoot(userLevel);
|
|
9004
|
+
const resolved = await resolveInstance({
|
|
9005
|
+
component: "agent",
|
|
9006
|
+
opts: {
|
|
9007
|
+
instanceDir: opts.instanceDir,
|
|
9008
|
+
name: opts.name
|
|
9009
|
+
},
|
|
9010
|
+
cwd: process.cwd(),
|
|
9011
|
+
kiciRoot,
|
|
9012
|
+
manager,
|
|
9013
|
+
isUserLevel: userLevel
|
|
9014
|
+
});
|
|
8326
9015
|
const config = {
|
|
8327
|
-
name:
|
|
9016
|
+
name: resolved.manifest.name,
|
|
8328
9017
|
displayName: "KiCI Agent",
|
|
8329
9018
|
description: "KiCI CI/CD workflow execution agent service",
|
|
8330
9019
|
executablePath: "",
|
|
8331
|
-
envFilePath:
|
|
8332
|
-
workingDirectory: configDir,
|
|
8333
|
-
isUserLevel:
|
|
8334
|
-
restartPolicy: DEFAULT_RESTART_POLICY
|
|
9020
|
+
envFilePath: resolved.manifest.envFilePath,
|
|
9021
|
+
workingDirectory: resolved.manifest.configDir,
|
|
9022
|
+
isUserLevel: resolved.manifest.isUserLevel,
|
|
9023
|
+
restartPolicy: DEFAULT_RESTART_POLICY,
|
|
9024
|
+
component: "agent"
|
|
8335
9025
|
};
|
|
8336
|
-
const manager = await createServiceManager(platform);
|
|
8337
9026
|
if (!await manager.isInstalled(config)) {
|
|
8338
|
-
console.error(`Error: service "${
|
|
9027
|
+
console.error(`Error: service "${config.name}" is not installed.`);
|
|
8339
9028
|
console.error(`Run \`kici-admin agent install\` first.`);
|
|
8340
9029
|
process.exit(1);
|
|
8341
9030
|
}
|
|
8342
9031
|
await manager.start(config);
|
|
8343
|
-
console.log(`Agent service "${
|
|
9032
|
+
console.log(`Agent service "${config.name}" started.`);
|
|
8344
9033
|
} catch (err) {
|
|
8345
9034
|
console.error(`Error: ${toErrorMessage(err)}`);
|
|
8346
9035
|
process.exit(1);
|
|
@@ -8350,24 +9039,36 @@ function registerAgentStart(agent) {
|
|
|
8350
9039
|
//#endregion
|
|
8351
9040
|
//#region src/cli/commands/agent-service/stop.ts
|
|
8352
9041
|
function registerAgentStop(agent) {
|
|
8353
|
-
agent.command("stop").description("Stop the agent service").option("--platform <type>", "Service platform (systemd, launchd, windows, compose)").option("--name <name>", "Service name", "
|
|
9042
|
+
agent.command("stop").description("Stop the agent service").option("--platform <type>", "Service platform (systemd, launchd, windows, compose)").option("--instance-dir <path>", "Deploy folder of the instance to stop").option("--name <name>", "Service name (no default — must resolve via flag/CWD)").option("--system", "Operate against the system-level service (requires root)").option("--user-level", "Operate against the user-level service").action(async (opts) => {
|
|
8354
9043
|
try {
|
|
8355
9044
|
const platform = detectPlatform(opts.platform);
|
|
8356
|
-
const userLevel =
|
|
8357
|
-
const
|
|
8358
|
-
const
|
|
9045
|
+
const userLevel = resolveUserLevel(opts);
|
|
9046
|
+
const manager = await createServiceManager(platform);
|
|
9047
|
+
const kiciRoot = kiciConfigRoot(userLevel);
|
|
9048
|
+
const resolved = await resolveInstance({
|
|
9049
|
+
component: "agent",
|
|
9050
|
+
opts: {
|
|
9051
|
+
instanceDir: opts.instanceDir,
|
|
9052
|
+
name: opts.name
|
|
9053
|
+
},
|
|
9054
|
+
cwd: process.cwd(),
|
|
9055
|
+
kiciRoot,
|
|
9056
|
+
manager,
|
|
9057
|
+
isUserLevel: userLevel
|
|
9058
|
+
});
|
|
8359
9059
|
const config = {
|
|
8360
|
-
name:
|
|
9060
|
+
name: resolved.manifest.name,
|
|
8361
9061
|
displayName: "KiCI Agent",
|
|
8362
9062
|
description: "KiCI CI/CD workflow execution agent service",
|
|
8363
9063
|
executablePath: "",
|
|
8364
|
-
envFilePath:
|
|
8365
|
-
workingDirectory: configDir,
|
|
8366
|
-
isUserLevel:
|
|
8367
|
-
restartPolicy: DEFAULT_RESTART_POLICY
|
|
9064
|
+
envFilePath: resolved.manifest.envFilePath,
|
|
9065
|
+
workingDirectory: resolved.manifest.configDir,
|
|
9066
|
+
isUserLevel: resolved.manifest.isUserLevel,
|
|
9067
|
+
restartPolicy: DEFAULT_RESTART_POLICY,
|
|
9068
|
+
component: "agent"
|
|
8368
9069
|
};
|
|
8369
|
-
await
|
|
8370
|
-
console.log(`Agent service "${
|
|
9070
|
+
await manager.stop(config);
|
|
9071
|
+
console.log(`Agent service "${config.name}" stopped.`);
|
|
8371
9072
|
} catch (err) {
|
|
8372
9073
|
console.error(`Error: ${toErrorMessage(err)}`);
|
|
8373
9074
|
process.exit(1);
|
|
@@ -8377,30 +9078,41 @@ function registerAgentStop(agent) {
|
|
|
8377
9078
|
//#endregion
|
|
8378
9079
|
//#region src/cli/commands/agent-service/restart.ts
|
|
8379
9080
|
function registerAgentRestart(agent) {
|
|
8380
|
-
agent.command("restart").description("Restart the agent service").option("--platform <type>", "Service platform (systemd, launchd, windows, compose)").option("--name <name>", "Service name", "
|
|
9081
|
+
agent.command("restart").description("Restart the agent service").option("--platform <type>", "Service platform (systemd, launchd, windows, compose)").option("--instance-dir <path>", "Deploy folder of the instance to restart").option("--name <name>", "Service name (no default — must resolve via flag/CWD)").option("--system", "Operate against the system-level service (requires root)").option("--user-level", "Operate against the user-level service").action(async (opts) => {
|
|
8381
9082
|
try {
|
|
8382
9083
|
const platform = detectPlatform(opts.platform);
|
|
8383
|
-
const userLevel =
|
|
8384
|
-
const
|
|
8385
|
-
const
|
|
9084
|
+
const userLevel = resolveUserLevel(opts);
|
|
9085
|
+
const manager = await createServiceManager(platform);
|
|
9086
|
+
const kiciRoot = kiciConfigRoot(userLevel);
|
|
9087
|
+
const resolved = await resolveInstance({
|
|
9088
|
+
component: "agent",
|
|
9089
|
+
opts: {
|
|
9090
|
+
instanceDir: opts.instanceDir,
|
|
9091
|
+
name: opts.name
|
|
9092
|
+
},
|
|
9093
|
+
cwd: process.cwd(),
|
|
9094
|
+
kiciRoot,
|
|
9095
|
+
manager,
|
|
9096
|
+
isUserLevel: userLevel
|
|
9097
|
+
});
|
|
8386
9098
|
const config = {
|
|
8387
|
-
name:
|
|
9099
|
+
name: resolved.manifest.name,
|
|
8388
9100
|
displayName: "KiCI Agent",
|
|
8389
9101
|
description: "KiCI CI/CD workflow execution agent service",
|
|
8390
9102
|
executablePath: "",
|
|
8391
|
-
envFilePath:
|
|
8392
|
-
workingDirectory: configDir,
|
|
8393
|
-
isUserLevel:
|
|
8394
|
-
restartPolicy: DEFAULT_RESTART_POLICY
|
|
9103
|
+
envFilePath: resolved.manifest.envFilePath,
|
|
9104
|
+
workingDirectory: resolved.manifest.configDir,
|
|
9105
|
+
isUserLevel: resolved.manifest.isUserLevel,
|
|
9106
|
+
restartPolicy: DEFAULT_RESTART_POLICY,
|
|
9107
|
+
component: "agent"
|
|
8395
9108
|
};
|
|
8396
|
-
const manager = await createServiceManager(platform);
|
|
8397
9109
|
if (!await manager.isInstalled(config)) {
|
|
8398
|
-
console.error(`Error: service "${
|
|
9110
|
+
console.error(`Error: service "${config.name}" is not installed.`);
|
|
8399
9111
|
console.error(`Run \`kici-admin agent install\` first.`);
|
|
8400
9112
|
process.exit(1);
|
|
8401
9113
|
}
|
|
8402
9114
|
await manager.restart(config);
|
|
8403
|
-
console.log(`Agent service "${
|
|
9115
|
+
console.log(`Agent service "${config.name}" restarted.`);
|
|
8404
9116
|
} catch (err) {
|
|
8405
9117
|
console.error(`Error: ${toErrorMessage(err)}`);
|
|
8406
9118
|
process.exit(1);
|
|
@@ -8409,12 +9121,11 @@ function registerAgentRestart(agent) {
|
|
|
8409
9121
|
}
|
|
8410
9122
|
//#endregion
|
|
8411
9123
|
//#region src/cli/commands/agent-service/status.ts
|
|
8412
|
-
/** Read port from agent env file. */
|
|
8413
|
-
function
|
|
8414
|
-
|
|
8415
|
-
if (!fs.existsSync(envPath)) return 4001;
|
|
9124
|
+
/** Read the port from the agent's env file (path from the manifest). */
|
|
9125
|
+
function readPortFromEnvFile(envFilePath) {
|
|
9126
|
+
if (!fs.existsSync(envFilePath)) return 4001;
|
|
8416
9127
|
try {
|
|
8417
|
-
const content = fs.readFileSync(
|
|
9128
|
+
const content = fs.readFileSync(envFilePath, "utf-8");
|
|
8418
9129
|
for (const line of content.split("\n")) {
|
|
8419
9130
|
const trimmed = line.trim();
|
|
8420
9131
|
if (trimmed.startsWith("#") || !trimmed.includes("=")) continue;
|
|
@@ -8470,36 +9181,39 @@ function buildJsonOutput(serviceStatus, health, serviceName) {
|
|
|
8470
9181
|
};
|
|
8471
9182
|
}
|
|
8472
9183
|
function registerAgentStatusCommand(parent) {
|
|
8473
|
-
parent.command("status").description("Show agent service status and health information").option("--platform <type>", "Service platform (systemd|launchd|windows|compose)").option("--name <name>", "Service name", "
|
|
9184
|
+
parent.command("status").description("Show agent service status and health information").option("--platform <type>", "Service platform (systemd|launchd|windows|compose)").option("--instance-dir <path>", "Deploy folder of the instance to inspect").option("--name <name>", "Service name (no default — must resolve via flag/CWD)").option("--system", "Operate against the system-level service (requires root)").option("--user-level", "Operate against the user-level service").option("--json", "Output as JSON").action(async (opts) => {
|
|
8474
9185
|
try {
|
|
8475
|
-
const
|
|
8476
|
-
const userLevel =
|
|
8477
|
-
const
|
|
9186
|
+
const platform = detectPlatform(opts.platform);
|
|
9187
|
+
const userLevel = resolveUserLevel(opts);
|
|
9188
|
+
const manager = await createServiceManager(platform);
|
|
9189
|
+
const kiciRoot = kiciConfigRoot(userLevel);
|
|
9190
|
+
const resolved = await resolveInstance({
|
|
9191
|
+
component: "agent",
|
|
9192
|
+
opts: {
|
|
9193
|
+
instanceDir: opts.instanceDir,
|
|
9194
|
+
name: opts.name
|
|
9195
|
+
},
|
|
9196
|
+
cwd: process.cwd(),
|
|
9197
|
+
kiciRoot,
|
|
9198
|
+
manager,
|
|
9199
|
+
isUserLevel: userLevel
|
|
9200
|
+
});
|
|
8478
9201
|
const config = {
|
|
8479
|
-
name:
|
|
8480
|
-
displayName: "KiCI
|
|
8481
|
-
description: "KiCI agent service",
|
|
9202
|
+
name: resolved.manifest.name,
|
|
9203
|
+
displayName: "KiCI Agent",
|
|
9204
|
+
description: "KiCI CI/CD workflow execution agent service",
|
|
8482
9205
|
executablePath: "",
|
|
8483
|
-
envFilePath:
|
|
8484
|
-
workingDirectory: configDir,
|
|
8485
|
-
isUserLevel:
|
|
8486
|
-
restartPolicy:
|
|
8487
|
-
|
|
8488
|
-
delays: [
|
|
8489
|
-
1,
|
|
8490
|
-
5,
|
|
8491
|
-
15,
|
|
8492
|
-
30
|
|
8493
|
-
],
|
|
8494
|
-
maxRetries: 5,
|
|
8495
|
-
windowSeconds: 300
|
|
8496
|
-
}
|
|
9206
|
+
envFilePath: resolved.manifest.envFilePath,
|
|
9207
|
+
workingDirectory: resolved.manifest.configDir,
|
|
9208
|
+
isUserLevel: resolved.manifest.isUserLevel,
|
|
9209
|
+
restartPolicy: DEFAULT_RESTART_POLICY,
|
|
9210
|
+
component: "agent"
|
|
8497
9211
|
};
|
|
8498
9212
|
const serviceStatus = await manager.status(config);
|
|
8499
9213
|
let health = null;
|
|
8500
|
-
if (serviceStatus.state === "running") health = await queryHealth(
|
|
8501
|
-
if (opts.json) console.log(JSON.stringify(buildJsonOutput(serviceStatus, health,
|
|
8502
|
-
else console.log(formatStatus(serviceStatus, health,
|
|
9214
|
+
if (serviceStatus.state === "running") health = await queryHealth(readPortFromEnvFile(config.envFilePath));
|
|
9215
|
+
if (opts.json) console.log(JSON.stringify(buildJsonOutput(serviceStatus, health, config.name), null, 2));
|
|
9216
|
+
else console.log(formatStatus(serviceStatus, health, config.name));
|
|
8503
9217
|
} catch (err) {
|
|
8504
9218
|
console.error(`Error: ${toErrorMessage(err)}`);
|
|
8505
9219
|
process.exit(1);
|
|
@@ -8509,30 +9223,33 @@ function registerAgentStatusCommand(parent) {
|
|
|
8509
9223
|
//#endregion
|
|
8510
9224
|
//#region src/cli/commands/agent-service/logs.ts
|
|
8511
9225
|
function registerAgentLogsCommand(parent) {
|
|
8512
|
-
parent.command("logs").description("Tail and follow agent service logs").option("--platform <type>", "Service platform (systemd|launchd|windows|compose)").option("--name <name>", "Service name", "
|
|
9226
|
+
parent.command("logs").description("Tail and follow agent service logs").option("--platform <type>", "Service platform (systemd|launchd|windows|compose)").option("--instance-dir <path>", "Deploy folder of the instance whose logs to read").option("--name <name>", "Service name (no default — must resolve via flag/CWD)").option("--system", "Operate against the system-level service (requires root)").option("--user-level", "Operate against the user-level service").option("--since <duration>", "Show logs since duration (e.g. 1h, 30m)").option("--level <level>", "Filter by log level (error|warn|info)").option("--json", "Output as structured JSON").option("--no-follow", "Snapshot mode (do not tail)").action(async (opts) => {
|
|
8513
9227
|
try {
|
|
8514
|
-
const
|
|
8515
|
-
const userLevel =
|
|
8516
|
-
const
|
|
9228
|
+
const platform = detectPlatform(opts.platform);
|
|
9229
|
+
const userLevel = resolveUserLevel(opts);
|
|
9230
|
+
const manager = await createServiceManager(platform);
|
|
9231
|
+
const kiciRoot = kiciConfigRoot(userLevel);
|
|
9232
|
+
const resolved = await resolveInstance({
|
|
9233
|
+
component: "agent",
|
|
9234
|
+
opts: {
|
|
9235
|
+
instanceDir: opts.instanceDir,
|
|
9236
|
+
name: opts.name
|
|
9237
|
+
},
|
|
9238
|
+
cwd: process.cwd(),
|
|
9239
|
+
kiciRoot,
|
|
9240
|
+
manager,
|
|
9241
|
+
isUserLevel: userLevel
|
|
9242
|
+
});
|
|
8517
9243
|
const config = {
|
|
8518
|
-
name:
|
|
8519
|
-
displayName: "KiCI
|
|
8520
|
-
description: "KiCI agent service",
|
|
9244
|
+
name: resolved.manifest.name,
|
|
9245
|
+
displayName: "KiCI Agent",
|
|
9246
|
+
description: "KiCI CI/CD workflow execution agent service",
|
|
8521
9247
|
executablePath: "",
|
|
8522
|
-
envFilePath:
|
|
8523
|
-
workingDirectory: configDir,
|
|
8524
|
-
isUserLevel:
|
|
8525
|
-
restartPolicy:
|
|
8526
|
-
|
|
8527
|
-
delays: [
|
|
8528
|
-
1,
|
|
8529
|
-
5,
|
|
8530
|
-
15,
|
|
8531
|
-
30
|
|
8532
|
-
],
|
|
8533
|
-
maxRetries: 5,
|
|
8534
|
-
windowSeconds: 300
|
|
8535
|
-
}
|
|
9248
|
+
envFilePath: resolved.manifest.envFilePath,
|
|
9249
|
+
workingDirectory: resolved.manifest.configDir,
|
|
9250
|
+
isUserLevel: resolved.manifest.isUserLevel,
|
|
9251
|
+
restartPolicy: DEFAULT_RESTART_POLICY,
|
|
9252
|
+
component: "agent"
|
|
8536
9253
|
};
|
|
8537
9254
|
const logOptions = {
|
|
8538
9255
|
since: opts.since,
|
|
@@ -8550,7 +9267,7 @@ function registerAgentLogsCommand(parent) {
|
|
|
8550
9267
|
//#endregion
|
|
8551
9268
|
//#region src/cli/commands/agent-service/upgrade.ts
|
|
8552
9269
|
function registerAgentUpgradeCommand(parent) {
|
|
8553
|
-
parent.command("upgrade").description("Upgrade agent to a new version using versioned directory layout").option("--platform <type>", "Service platform (systemd|launchd|windows|compose)").option("--name <name>", "Service name
|
|
9270
|
+
parent.command("upgrade").description("Upgrade agent to a new version using versioned directory layout").option("--platform <type>", "Service platform (systemd|launchd|windows|compose)").option("--instance-dir <path>", "Deploy folder of the instance to upgrade").option("--name <name>", "Service name (no default — must resolve via flag/CWD)").option("--from <path>", "Path to package archive (.tar.gz or .zip)").option("--url <url>", "URL to download package archive from").option("--version <version>", "Target version string (e.g., 0.3.0)").option("--yes", "Skip confirmation prompt").option("--force", "Overwrite existing versioned directory").option("--cleanup", "Remove old versions (keeps current and previous)").option("--rollback", "Roll back to the previous version").action(async (opts) => {
|
|
8554
9271
|
await performVersionedUpgrade("agent", opts);
|
|
8555
9272
|
});
|
|
8556
9273
|
}
|