@kici-dev/orchestrator 0.1.11 → 0.1.13
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 +21 -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 +1670 -1029
- package/dist/metrics/prometheus.d.ts +32 -4
- package/dist/server.js +19 -19
- package/dist/standalone.js +19 -19
- package/package.json +4 -4
- package/sbom.spdx.json +36 -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";
|
|
@@ -6092,7 +6092,8 @@ function isRoot() {
|
|
|
6092
6092
|
return typeof process.getuid === "function" && process.getuid() === 0;
|
|
6093
6093
|
}
|
|
6094
6094
|
/**
|
|
6095
|
-
*
|
|
6095
|
+
* Name-agnostic KiCI config root — the directory that contains every
|
|
6096
|
+
* per-instance config subdir for this privilege level.
|
|
6096
6097
|
*
|
|
6097
6098
|
* Paths follow platform conventions:
|
|
6098
6099
|
* - System Linux/macOS: /etc/kici/
|
|
@@ -6100,8 +6101,11 @@ function isRoot() {
|
|
|
6100
6101
|
* - User macOS: ~/Library/Application Support/kici/
|
|
6101
6102
|
* - System Windows: C:\ProgramData\kici\
|
|
6102
6103
|
* - User Windows: %LOCALAPPDATA%\kici\
|
|
6104
|
+
*
|
|
6105
|
+
* Used by the instance index (`<kiciRoot>/instances.json`) which lives
|
|
6106
|
+
* outside any per-instance subdir, and as the base for {@link getConfigDir}.
|
|
6103
6107
|
*/
|
|
6104
|
-
function
|
|
6108
|
+
function kiciConfigRoot(isUserLevel) {
|
|
6105
6109
|
const plat = os.platform();
|
|
6106
6110
|
if (plat === "win32") {
|
|
6107
6111
|
if (isUserLevel) return (process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local")) + "\\kici\\";
|
|
@@ -6112,20 +6116,40 @@ function getConfigDir(_serviceName, isUserLevel) {
|
|
|
6112
6116
|
return path.join(os.homedir(), ".config", "kici") + "/";
|
|
6113
6117
|
}
|
|
6114
6118
|
/**
|
|
6115
|
-
* Get the
|
|
6119
|
+
* Get the configuration directory for a specific KiCI service instance.
|
|
6116
6120
|
*
|
|
6117
|
-
*
|
|
6118
|
-
* -
|
|
6119
|
-
*
|
|
6120
|
-
*
|
|
6121
|
-
*
|
|
6121
|
+
* Returns `<kiciConfigRoot>/<serviceName>/`. The name-scoped subdir is the
|
|
6122
|
+
* folder-anchored home for everything belonging to one installed instance
|
|
6123
|
+
* (env file, generated unit references, future per-instance state).
|
|
6124
|
+
*
|
|
6125
|
+
* Examples:
|
|
6126
|
+
* - System Linux: /etc/kici/<name>/
|
|
6127
|
+
* - User Linux: ~/.config/kici/<name>/
|
|
6128
|
+
* - User macOS: ~/Library/Application Support/kici/<name>/
|
|
6129
|
+
* - System Windows: C:\ProgramData\kici\<name>\
|
|
6130
|
+
* - User Windows: %LOCALAPPDATA%\kici\<name>\
|
|
6131
|
+
*/
|
|
6132
|
+
function getConfigDir(serviceName, isUserLevel) {
|
|
6133
|
+
const root = kiciConfigRoot(isUserLevel);
|
|
6134
|
+
const sep = os.platform() === "win32" ? "\\" : "/";
|
|
6135
|
+
return root + serviceName + sep;
|
|
6136
|
+
}
|
|
6137
|
+
/**
|
|
6138
|
+
* Get the log directory for a specific KiCI service instance.
|
|
6139
|
+
*
|
|
6140
|
+
* The per-platform layout matches the existing matrix; the service name is
|
|
6141
|
+
* injected as the per-instance segment so each instance has its own log dir:
|
|
6142
|
+
* - System Linux/macOS: /var/log/kici/<name>/
|
|
6143
|
+
* - User Linux: ~/.local/share/kici/<name>/logs/
|
|
6144
|
+
* - User macOS: ~/Library/Logs/kici/<name>/
|
|
6145
|
+
* - Windows: C:\ProgramData\kici\<name>\logs\
|
|
6122
6146
|
*/
|
|
6123
|
-
function getLogDir(
|
|
6147
|
+
function getLogDir(serviceName, isUserLevel) {
|
|
6124
6148
|
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") + "/";
|
|
6149
|
+
if (plat === "win32") return "C:\\ProgramData\\kici\\" + serviceName + "\\logs\\";
|
|
6150
|
+
if (!isUserLevel) return "/var/log/kici/" + serviceName + "/";
|
|
6151
|
+
if (plat === "darwin") return path.join(os.homedir(), "Library", "Logs", "kici", serviceName) + "/";
|
|
6152
|
+
return path.join(os.homedir(), ".local", "share", "kici", serviceName, "logs") + "/";
|
|
6129
6153
|
}
|
|
6130
6154
|
/**
|
|
6131
6155
|
* Get the cache directory for lazy dependency downloads.
|
|
@@ -6158,6 +6182,221 @@ function resolveUserLevel(opts) {
|
|
|
6158
6182
|
return !isRoot();
|
|
6159
6183
|
}
|
|
6160
6184
|
//#endregion
|
|
6185
|
+
//#region src/cli/service/instance/manifest.ts
|
|
6186
|
+
/**
|
|
6187
|
+
* Instance manifest — the single source of truth for a folder-anchored
|
|
6188
|
+
* service install. Written by `install` into the deploy folder, read by
|
|
6189
|
+
* every lifecycle command to reconstruct the ServiceConfig.
|
|
6190
|
+
*/
|
|
6191
|
+
const REQUIRED_FIELDS = [
|
|
6192
|
+
"component",
|
|
6193
|
+
"name",
|
|
6194
|
+
"platform",
|
|
6195
|
+
"isUserLevel",
|
|
6196
|
+
"envFilePath",
|
|
6197
|
+
"configDir",
|
|
6198
|
+
"logDir",
|
|
6199
|
+
"installBase",
|
|
6200
|
+
"createdAt",
|
|
6201
|
+
"kiciVersion"
|
|
6202
|
+
];
|
|
6203
|
+
/** Per-component manifest filename. */
|
|
6204
|
+
function manifestFilename(component) {
|
|
6205
|
+
return `.kici-${component}.json`;
|
|
6206
|
+
}
|
|
6207
|
+
/** Resolve the manifest path inside an instance directory. */
|
|
6208
|
+
function manifestPath(instanceDir, component) {
|
|
6209
|
+
return path.join(instanceDir, manifestFilename(component));
|
|
6210
|
+
}
|
|
6211
|
+
/**
|
|
6212
|
+
* Read the manifest for `component` from `instanceDir`.
|
|
6213
|
+
* Returns null when the file does not exist; throws on parse or schema errors.
|
|
6214
|
+
*/
|
|
6215
|
+
function readManifest(instanceDir, component) {
|
|
6216
|
+
const file = manifestPath(instanceDir, component);
|
|
6217
|
+
if (!fs.existsSync(file)) return null;
|
|
6218
|
+
const raw = fs.readFileSync(file, "utf-8");
|
|
6219
|
+
let parsed;
|
|
6220
|
+
try {
|
|
6221
|
+
parsed = JSON.parse(raw);
|
|
6222
|
+
} catch (err) {
|
|
6223
|
+
throw new Error(`Malformed instance manifest at ${file}: ${err.message}`);
|
|
6224
|
+
}
|
|
6225
|
+
if (!parsed || typeof parsed !== "object") throw new Error(`Invalid instance manifest at ${file}: not an object`);
|
|
6226
|
+
for (const field of REQUIRED_FIELDS) if (!(field in parsed)) throw new Error(`Invalid instance manifest at ${file}: missing field "${field}"`);
|
|
6227
|
+
return parsed;
|
|
6228
|
+
}
|
|
6229
|
+
/**
|
|
6230
|
+
* Write the manifest for `manifest.component` into `instanceDir`.
|
|
6231
|
+
* Returns the full path written.
|
|
6232
|
+
*/
|
|
6233
|
+
function writeManifest(instanceDir, manifest) {
|
|
6234
|
+
fs.mkdirSync(instanceDir, { recursive: true });
|
|
6235
|
+
const file = manifestPath(instanceDir, manifest.component);
|
|
6236
|
+
fs.writeFileSync(file, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
|
|
6237
|
+
return file;
|
|
6238
|
+
}
|
|
6239
|
+
//#endregion
|
|
6240
|
+
//#region src/cli/service/instance/index-file.ts
|
|
6241
|
+
/**
|
|
6242
|
+
* Instance index — a reconciled CACHE of all installed instances on the host.
|
|
6243
|
+
*
|
|
6244
|
+
* Lives at <kiciRoot>/instances.json, where kiciRoot is the name-agnostic
|
|
6245
|
+
* config root (~/.config/kici/ for user-level, /etc/kici/ for system). The
|
|
6246
|
+
* index is convenience: discovery is authoritative against the init system
|
|
6247
|
+
* (see resolve.ts#listInstances which reconciles this file against scans).
|
|
6248
|
+
*
|
|
6249
|
+
* Corrupt index → warn and treat as empty; the next install/uninstall
|
|
6250
|
+
* rewrites it cleanly.
|
|
6251
|
+
*/
|
|
6252
|
+
const FILE = "instances.json";
|
|
6253
|
+
/** Resolve <kiciRoot>/instances.json. */
|
|
6254
|
+
function indexPath(kiciRoot) {
|
|
6255
|
+
return path.join(kiciRoot, FILE);
|
|
6256
|
+
}
|
|
6257
|
+
/** Read the index. Missing or corrupt → []. */
|
|
6258
|
+
function readIndex(kiciRoot) {
|
|
6259
|
+
const file = indexPath(kiciRoot);
|
|
6260
|
+
if (!fs.existsSync(file)) return [];
|
|
6261
|
+
try {
|
|
6262
|
+
const parsed = JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
6263
|
+
if (!Array.isArray(parsed)) {
|
|
6264
|
+
console.warn(`[kici] instance index at ${file} is invalid (not an array); ignoring`);
|
|
6265
|
+
return [];
|
|
6266
|
+
}
|
|
6267
|
+
return parsed;
|
|
6268
|
+
} catch (err) {
|
|
6269
|
+
console.warn(`[kici] instance index at ${file} is corrupt (${err.message}); ignoring`);
|
|
6270
|
+
return [];
|
|
6271
|
+
}
|
|
6272
|
+
}
|
|
6273
|
+
/** Overwrite the index. */
|
|
6274
|
+
function writeIndex(kiciRoot, entries) {
|
|
6275
|
+
fs.mkdirSync(kiciRoot, { recursive: true });
|
|
6276
|
+
fs.writeFileSync(indexPath(kiciRoot), JSON.stringify(entries, null, 2) + "\n", "utf-8");
|
|
6277
|
+
}
|
|
6278
|
+
/**
|
|
6279
|
+
* Append an entry. Idempotent for an exact (component,name,instanceDir) match.
|
|
6280
|
+
* Throws on a (component,name) collision with a different instanceDir — the
|
|
6281
|
+
* caller must use a different name or pass --force to overwrite.
|
|
6282
|
+
*/
|
|
6283
|
+
function appendIndexEntry(kiciRoot, entry) {
|
|
6284
|
+
const current = readIndex(kiciRoot);
|
|
6285
|
+
const existing = current.find((e) => e.component === entry.component && e.name === entry.name);
|
|
6286
|
+
if (existing) {
|
|
6287
|
+
if (existing.instanceDir === entry.instanceDir) return;
|
|
6288
|
+
throw new Error(`Already an ${entry.component} instance "${entry.name}" registered at ${existing.instanceDir}`);
|
|
6289
|
+
}
|
|
6290
|
+
writeIndex(kiciRoot, [...current, entry]);
|
|
6291
|
+
}
|
|
6292
|
+
/** Remove the matching entry (no-op when absent). */
|
|
6293
|
+
function removeIndexEntry(kiciRoot, key) {
|
|
6294
|
+
const current = readIndex(kiciRoot);
|
|
6295
|
+
const next = current.filter((e) => !(e.component === key.component && e.name === key.name));
|
|
6296
|
+
if (next.length !== current.length) writeIndex(kiciRoot, next);
|
|
6297
|
+
}
|
|
6298
|
+
//#endregion
|
|
6299
|
+
//#region src/cli/service/instance/resolve.ts
|
|
6300
|
+
/**
|
|
6301
|
+
* resolveInstance — the single entry point every lifecycle command uses to
|
|
6302
|
+
* decide which installed service it's operating on.
|
|
6303
|
+
*
|
|
6304
|
+
* Priority:
|
|
6305
|
+
* 1. --instance-dir <path> → read manifest at <path>
|
|
6306
|
+
* 2. --name <name> → match against listInstances() result
|
|
6307
|
+
* 3. CWD manifest → read ./.kici-<component>.json
|
|
6308
|
+
* 4. otherwise → refuse with a candidate list (throws)
|
|
6309
|
+
*
|
|
6310
|
+
* listInstances reconciles the on-disk index (cache) with the driver's
|
|
6311
|
+
* native scan (source of truth). The reconciled result is rewritten back to
|
|
6312
|
+
* the index to self-heal stale entries.
|
|
6313
|
+
*/
|
|
6314
|
+
/**
|
|
6315
|
+
* Reconcile <kiciRoot>/instances.json with the driver's native scan, then
|
|
6316
|
+
* rewrite the index dropping entries whose units no longer exist. Returns
|
|
6317
|
+
* the merged list filtered to the requested component + isUserLevel.
|
|
6318
|
+
*/
|
|
6319
|
+
async function listInstances(args) {
|
|
6320
|
+
const { component, isUserLevel, kiciRoot, manager } = args;
|
|
6321
|
+
const scanForComponent = (await manager.list(isUserLevel)).filter((s) => s.component === component);
|
|
6322
|
+
const scanNames = new Set(scanForComponent.map((s) => s.name));
|
|
6323
|
+
const index = readIndex(kiciRoot);
|
|
6324
|
+
const relevantIndex = index.filter((e) => e.component === component && e.isUserLevel === isUserLevel);
|
|
6325
|
+
const survivors = relevantIndex.filter((e) => scanNames.has(e.name));
|
|
6326
|
+
if (survivors.length !== relevantIndex.length) writeIndex(kiciRoot, [...index.filter((e) => !(e.component === component && e.isUserLevel === isUserLevel)), ...survivors]);
|
|
6327
|
+
const indexByName = new Map(survivors.map((e) => [e.name, e]));
|
|
6328
|
+
return scanForComponent.map((s) => {
|
|
6329
|
+
const idx = indexByName.get(s.name);
|
|
6330
|
+
return {
|
|
6331
|
+
...s,
|
|
6332
|
+
component,
|
|
6333
|
+
instanceDir: idx?.instanceDir,
|
|
6334
|
+
source: idx ? "index+scan" : "scan"
|
|
6335
|
+
};
|
|
6336
|
+
});
|
|
6337
|
+
}
|
|
6338
|
+
/**
|
|
6339
|
+
* Resolve the target instance for the current lifecycle invocation.
|
|
6340
|
+
* Throws with a refusal/candidate-listing error when ambiguous.
|
|
6341
|
+
*/
|
|
6342
|
+
async function resolveInstance(args) {
|
|
6343
|
+
const { component, opts, cwd, kiciRoot, manager, isUserLevel } = args;
|
|
6344
|
+
if (opts.instanceDir) {
|
|
6345
|
+
const dir = path.resolve(opts.instanceDir);
|
|
6346
|
+
const m = readManifest(dir, component);
|
|
6347
|
+
if (!m) throw new Error(`No ${component} manifest at ${manifestPath(dir, component)}. Did you install with --instance-dir ${dir}?`);
|
|
6348
|
+
return {
|
|
6349
|
+
manifest: m,
|
|
6350
|
+
manifestPath: manifestPath(dir, component),
|
|
6351
|
+
instanceDir: dir
|
|
6352
|
+
};
|
|
6353
|
+
}
|
|
6354
|
+
if (opts.name) {
|
|
6355
|
+
const candidates = await listInstances({
|
|
6356
|
+
component,
|
|
6357
|
+
isUserLevel,
|
|
6358
|
+
kiciRoot,
|
|
6359
|
+
manager
|
|
6360
|
+
});
|
|
6361
|
+
const match = candidates.find((c) => c.name === opts.name);
|
|
6362
|
+
if (!match) throw new Error(formatNameNotFound(component, opts.name, candidates));
|
|
6363
|
+
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.`);
|
|
6364
|
+
const manifest = readManifest(match.instanceDir, component);
|
|
6365
|
+
if (!manifest) throw new Error(`Manifest for ${component} instance "${opts.name}" missing at ${manifestPath(match.instanceDir, component)}.`);
|
|
6366
|
+
return {
|
|
6367
|
+
manifest,
|
|
6368
|
+
manifestPath: manifestPath(match.instanceDir, component),
|
|
6369
|
+
instanceDir: match.instanceDir
|
|
6370
|
+
};
|
|
6371
|
+
}
|
|
6372
|
+
const cwdManifest = readManifest(cwd, component);
|
|
6373
|
+
if (cwdManifest) return {
|
|
6374
|
+
manifest: cwdManifest,
|
|
6375
|
+
manifestPath: manifestPath(cwd, component),
|
|
6376
|
+
instanceDir: path.resolve(cwd)
|
|
6377
|
+
};
|
|
6378
|
+
const candidates = await listInstances({
|
|
6379
|
+
component,
|
|
6380
|
+
isUserLevel,
|
|
6381
|
+
kiciRoot,
|
|
6382
|
+
manager
|
|
6383
|
+
});
|
|
6384
|
+
throw new Error(formatRefusal(component, candidates));
|
|
6385
|
+
}
|
|
6386
|
+
/**
|
|
6387
|
+
* Format the refusal message and candidate table.
|
|
6388
|
+
*
|
|
6389
|
+
* When candidates is empty, returns the "no instances installed" guidance.
|
|
6390
|
+
* When candidates exist, lists them with their instanceDir (or "(no manifest)").
|
|
6391
|
+
*/
|
|
6392
|
+
function formatRefusal(component, candidates) {
|
|
6393
|
+
if (candidates.length === 0) return `No ${component} instances installed on this host. Run \`kici-admin ${component} install --instance-dir <deploy folder>\` first.`;
|
|
6394
|
+
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.`;
|
|
6395
|
+
}
|
|
6396
|
+
function formatNameNotFound(component, name, candidates) {
|
|
6397
|
+
return `${component} instance "${name}" not found. Installed:\n${candidates.length ? candidates.map((c) => ` - ${c.name} ${c.platform} ${c.instanceDir ?? "(no manifest)"}`).join("\n") : " (none)"}`;
|
|
6398
|
+
}
|
|
6399
|
+
//#endregion
|
|
6161
6400
|
//#region src/cli/service/systemd.ts
|
|
6162
6401
|
/**
|
|
6163
6402
|
* systemd service manager implementation.
|
|
@@ -6215,6 +6454,7 @@ var init_systemd = __esmMin((() => {
|
|
|
6215
6454
|
const lines = [];
|
|
6216
6455
|
lines.push("[Unit]");
|
|
6217
6456
|
lines.push(`Description=${config.description}`);
|
|
6457
|
+
if (config.component) lines.push(`X-KiCI-Component=${config.component}`);
|
|
6218
6458
|
lines.push("After=network.target postgresql.service");
|
|
6219
6459
|
lines.push("");
|
|
6220
6460
|
lines.push("[Service]");
|
|
@@ -6351,6 +6591,24 @@ var init_systemd = __esmMin((() => {
|
|
|
6351
6591
|
async isInstalled(config) {
|
|
6352
6592
|
return fs.existsSync(this.unitFilePath(config));
|
|
6353
6593
|
}
|
|
6594
|
+
async list(isUserLevel) {
|
|
6595
|
+
const unitDir = isUserLevel ? path.join(os.homedir(), ".config", "systemd", "user") : "/etc/systemd/system";
|
|
6596
|
+
if (!fs.existsSync(unitDir)) return [];
|
|
6597
|
+
const out = [];
|
|
6598
|
+
for (const entry of fs.readdirSync(unitDir)) {
|
|
6599
|
+
if (typeof entry !== "string") continue;
|
|
6600
|
+
if (!entry.startsWith("kici-") || !entry.endsWith(".service")) continue;
|
|
6601
|
+
const m = fs.readFileSync(path.join(unitDir, entry), "utf-8").match(/^X-KiCI-Component=(orchestrator|agent)\s*$/m);
|
|
6602
|
+
if (!m) continue;
|
|
6603
|
+
out.push({
|
|
6604
|
+
name: entry.replace(/\.service$/, ""),
|
|
6605
|
+
platform: "systemd",
|
|
6606
|
+
isUserLevel,
|
|
6607
|
+
component: m[1]
|
|
6608
|
+
});
|
|
6609
|
+
}
|
|
6610
|
+
return out;
|
|
6611
|
+
}
|
|
6354
6612
|
};
|
|
6355
6613
|
}));
|
|
6356
6614
|
//#endregion
|
|
@@ -6363,6 +6621,10 @@ var init_systemd = __esmMin((() => {
|
|
|
6363
6621
|
* user-level (~/Library/LaunchAgents/) agents.
|
|
6364
6622
|
*/
|
|
6365
6623
|
var launchd_exports = /* @__PURE__ */ __exportAll({ LaunchdServiceManager: () => LaunchdServiceManager });
|
|
6624
|
+
/** Async sleep used to pace launchd bootout/bootstrap reconciliation. */
|
|
6625
|
+
function sleep(ms) {
|
|
6626
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
6627
|
+
}
|
|
6366
6628
|
var LABEL_PREFIX, SYSTEM_LOG_DIR, LaunchdServiceManager;
|
|
6367
6629
|
var init_launchd = __esmMin((() => {
|
|
6368
6630
|
LABEL_PREFIX = "dev.kici";
|
|
@@ -6416,6 +6678,10 @@ var init_launchd = __esmMin((() => {
|
|
|
6416
6678
|
lines.push("<dict>");
|
|
6417
6679
|
lines.push(" <key>Label</key>");
|
|
6418
6680
|
lines.push(` <string>${this.escapeXml(label)}</string>`);
|
|
6681
|
+
if (config.component) {
|
|
6682
|
+
lines.push(" <key>KiCIComponent</key>");
|
|
6683
|
+
lines.push(` <string>${config.component}</string>`);
|
|
6684
|
+
}
|
|
6419
6685
|
lines.push(" <key>ProgramArguments</key>");
|
|
6420
6686
|
lines.push(" <array>");
|
|
6421
6687
|
lines.push(` <string>${this.escapeXml(config.executablePath)}</string>`);
|
|
@@ -6497,14 +6763,61 @@ var init_launchd = __esmMin((() => {
|
|
|
6497
6763
|
logDirectory
|
|
6498
6764
|
], { stdio: "inherit" });
|
|
6499
6765
|
fs.writeFileSync(plistFile, plistContent, "utf-8");
|
|
6500
|
-
if (this.isLoaded(config))
|
|
6501
|
-
|
|
6502
|
-
|
|
6503
|
-
|
|
6504
|
-
|
|
6505
|
-
|
|
6506
|
-
|
|
6507
|
-
|
|
6766
|
+
if (this.isLoaded(config)) {
|
|
6767
|
+
try {
|
|
6768
|
+
execFileSync("launchctl", ["bootout", this.domainTarget(config)], { stdio: "inherit" });
|
|
6769
|
+
} catch {}
|
|
6770
|
+
await this.waitUntilUnloaded(config);
|
|
6771
|
+
}
|
|
6772
|
+
await this.bootstrapWithRetry(config, plistFile);
|
|
6773
|
+
}
|
|
6774
|
+
/**
|
|
6775
|
+
* Poll until the service is no longer loaded in its target domain, or a
|
|
6776
|
+
* short deadline elapses. `launchctl bootout` is asynchronous — it returns
|
|
6777
|
+
* before launchd has finished releasing the service — so a bootstrap issued
|
|
6778
|
+
* immediately afterward races the teardown and fails with EIO. Waiting for
|
|
6779
|
+
* the unload to complete closes that race for the common case; the residual
|
|
6780
|
+
* window is covered by bootstrapWithRetry.
|
|
6781
|
+
*/
|
|
6782
|
+
async waitUntilUnloaded(config) {
|
|
6783
|
+
const deadline = Date.now() + 15e3;
|
|
6784
|
+
while (this.isLoaded(config)) {
|
|
6785
|
+
if (Date.now() >= deadline) return;
|
|
6786
|
+
await sleep(500);
|
|
6787
|
+
}
|
|
6788
|
+
}
|
|
6789
|
+
/**
|
|
6790
|
+
* Bootstrap into the target domain, retrying on the transient EIO
|
|
6791
|
+
* ("5: Input/output error") launchd returns when a just-removed service has
|
|
6792
|
+
* not finished tearing down. A genuine, non-transient failure (bad plist,
|
|
6793
|
+
* permission denied) is re-thrown on the first attempt. If a stale instance
|
|
6794
|
+
* reappears between attempts, it is booted out before the next try.
|
|
6795
|
+
*/
|
|
6796
|
+
async bootstrapWithRetry(config, plistFile) {
|
|
6797
|
+
const attempts = 5;
|
|
6798
|
+
for (let attempt = 1; attempt <= attempts; attempt++) try {
|
|
6799
|
+
execFileSync("launchctl", [
|
|
6800
|
+
"bootstrap",
|
|
6801
|
+
this.domain(config),
|
|
6802
|
+
plistFile
|
|
6803
|
+
], { stdio: [
|
|
6804
|
+
"inherit",
|
|
6805
|
+
"inherit",
|
|
6806
|
+
"pipe"
|
|
6807
|
+
] });
|
|
6808
|
+
return;
|
|
6809
|
+
} catch (err) {
|
|
6810
|
+
const e = err;
|
|
6811
|
+
const stderr = (e.stderr ?? "").toString();
|
|
6812
|
+
if (!(e.status === 5 || /input\/output error|resource busy/i.test(stderr)) || attempt === attempts) {
|
|
6813
|
+
if (stderr) process.stderr.write(stderr);
|
|
6814
|
+
throw err;
|
|
6815
|
+
}
|
|
6816
|
+
if (this.isLoaded(config)) try {
|
|
6817
|
+
execFileSync("launchctl", ["bootout", this.domainTarget(config)], { stdio: "inherit" });
|
|
6818
|
+
} catch {}
|
|
6819
|
+
await sleep(1e3 * attempt);
|
|
6820
|
+
}
|
|
6508
6821
|
}
|
|
6509
6822
|
async uninstall(config) {
|
|
6510
6823
|
const plistFile = this.plistPath(config);
|
|
@@ -6573,6 +6886,30 @@ var init_launchd = __esmMin((() => {
|
|
|
6573
6886
|
async isInstalled(config) {
|
|
6574
6887
|
return fs.existsSync(this.plistPath(config));
|
|
6575
6888
|
}
|
|
6889
|
+
async list(isUserLevel) {
|
|
6890
|
+
const baseDir = isUserLevel ? path.join(os.homedir(), "Library", "LaunchAgents") : "/Library/LaunchDaemons";
|
|
6891
|
+
if (!fs.existsSync(baseDir)) return [];
|
|
6892
|
+
const out = [];
|
|
6893
|
+
for (const entry of fs.readdirSync(baseDir)) {
|
|
6894
|
+
if (typeof entry !== "string") continue;
|
|
6895
|
+
if (!entry.endsWith(".plist")) continue;
|
|
6896
|
+
let content;
|
|
6897
|
+
try {
|
|
6898
|
+
content = fs.readFileSync(path.join(baseDir, entry), "utf-8");
|
|
6899
|
+
} catch {
|
|
6900
|
+
continue;
|
|
6901
|
+
}
|
|
6902
|
+
const match = content.match(/<key>KiCIComponent<\/key>\s*<string>(orchestrator|agent)<\/string>/);
|
|
6903
|
+
if (!match) continue;
|
|
6904
|
+
out.push({
|
|
6905
|
+
name: entry.replace(/\.plist$/, ""),
|
|
6906
|
+
platform: "launchd",
|
|
6907
|
+
isUserLevel,
|
|
6908
|
+
component: match[1]
|
|
6909
|
+
});
|
|
6910
|
+
}
|
|
6911
|
+
return out;
|
|
6912
|
+
}
|
|
6576
6913
|
};
|
|
6577
6914
|
}));
|
|
6578
6915
|
//#endregion
|
|
@@ -6870,6 +7207,8 @@ var init_windows = __esmMin((() => {
|
|
|
6870
7207
|
cmdParts.push("--", `"${config.executablePath}"`);
|
|
6871
7208
|
for (const arg of config.args ?? []) cmdParts.push(`"${arg}"`);
|
|
6872
7209
|
execSync(cmdParts.join(" "), { stdio: "pipe" });
|
|
7210
|
+
const descText = config.component ? `[KiCI:${config.component}] ${config.description}` : config.description;
|
|
7211
|
+
execSync(`sc.exe description ${config.name} "${descText.replace(/"/g, "\\\"")}"`, { stdio: "pipe" });
|
|
6873
7212
|
execSync(`sc.exe config ${config.name} start= auto`, { stdio: "pipe" });
|
|
6874
7213
|
const actions = config.restartPolicy.delays.map((d) => `restart/${d * 1e3}`).join("/");
|
|
6875
7214
|
const resetSeconds = config.restartPolicy.windowSeconds;
|
|
@@ -6960,6 +7299,37 @@ var init_windows = __esmMin((() => {
|
|
|
6960
7299
|
return false;
|
|
6961
7300
|
}
|
|
6962
7301
|
}
|
|
7302
|
+
async list(isUserLevel) {
|
|
7303
|
+
let raw;
|
|
7304
|
+
try {
|
|
7305
|
+
raw = execSync("powershell -Command \"Get-CimInstance Win32_Service -Filter \\\"Name LIKE 'kici-%'\\\" | Select-Object Name,Description | ConvertTo-Json\"", { stdio: "pipe" }).toString();
|
|
7306
|
+
} catch {
|
|
7307
|
+
return [];
|
|
7308
|
+
}
|
|
7309
|
+
if (!raw.trim()) return [];
|
|
7310
|
+
let parsed;
|
|
7311
|
+
try {
|
|
7312
|
+
parsed = JSON.parse(raw);
|
|
7313
|
+
} catch {
|
|
7314
|
+
return [];
|
|
7315
|
+
}
|
|
7316
|
+
const rows = Array.isArray(parsed) ? parsed : [parsed];
|
|
7317
|
+
const out = [];
|
|
7318
|
+
for (const row of rows) {
|
|
7319
|
+
if (!row || typeof row !== "object") continue;
|
|
7320
|
+
const r = row;
|
|
7321
|
+
const match = (typeof r.Description === "string" ? r.Description : "").match(/^\[KiCI:(orchestrator|agent)\]/);
|
|
7322
|
+
if (!match) continue;
|
|
7323
|
+
if (typeof r.Name !== "string") continue;
|
|
7324
|
+
out.push({
|
|
7325
|
+
name: r.Name,
|
|
7326
|
+
platform: "windows",
|
|
7327
|
+
isUserLevel,
|
|
7328
|
+
component: match[1]
|
|
7329
|
+
});
|
|
7330
|
+
}
|
|
7331
|
+
return out;
|
|
7332
|
+
}
|
|
6963
7333
|
};
|
|
6964
7334
|
}));
|
|
6965
7335
|
//#endregion
|
|
@@ -6982,11 +7352,11 @@ var compose_exports = /* @__PURE__ */ __exportAll({ ComposeServiceManager: () =>
|
|
|
6982
7352
|
function detectRuntime() {
|
|
6983
7353
|
try {
|
|
6984
7354
|
execSync("podman compose version", { stdio: "pipe" });
|
|
6985
|
-
return "podman
|
|
7355
|
+
return "podman";
|
|
6986
7356
|
} catch {}
|
|
6987
7357
|
try {
|
|
6988
7358
|
execSync("docker compose version", { stdio: "pipe" });
|
|
6989
|
-
return "docker
|
|
7359
|
+
return "docker";
|
|
6990
7360
|
} catch {}
|
|
6991
7361
|
throw new Error("No container runtime found. Install Docker or Podman with compose support.");
|
|
6992
7362
|
}
|
|
@@ -7029,7 +7399,7 @@ function getRestartMode(config) {
|
|
|
7029
7399
|
*/
|
|
7030
7400
|
function generateComposeYaml(config) {
|
|
7031
7401
|
const restartMode = getRestartMode(config);
|
|
7032
|
-
|
|
7402
|
+
const lines = [
|
|
7033
7403
|
"# Generated by KiCI service installer",
|
|
7034
7404
|
`# Service: ${config.displayName}`,
|
|
7035
7405
|
"",
|
|
@@ -7043,20 +7413,25 @@ function generateComposeYaml(config) {
|
|
|
7043
7413
|
" volumes:",
|
|
7044
7414
|
` - ${config.workingDirectory}:${config.workingDirectory}`,
|
|
7045
7415
|
" network_mode: host"
|
|
7046
|
-
]
|
|
7416
|
+
];
|
|
7417
|
+
if (config.component) {
|
|
7418
|
+
lines.push(" labels:");
|
|
7419
|
+
lines.push(` dev.kici.component: ${config.component}`);
|
|
7420
|
+
}
|
|
7421
|
+
return lines.join("\n") + "\n";
|
|
7047
7422
|
}
|
|
7048
7423
|
var ComposeServiceManager;
|
|
7049
7424
|
var init_compose = __esmMin((() => {
|
|
7050
7425
|
ComposeServiceManager = class {
|
|
7051
7426
|
runtime = null;
|
|
7052
|
-
/** Get the runtime
|
|
7427
|
+
/** Get the runtime binary (`podman` or `docker`), detecting it if not already done. */
|
|
7053
7428
|
getRuntime() {
|
|
7054
7429
|
if (!this.runtime) this.runtime = detectRuntime();
|
|
7055
7430
|
return this.runtime;
|
|
7056
7431
|
}
|
|
7057
7432
|
/** Run a compose command for the given service config. */
|
|
7058
7433
|
runCompose(config, args) {
|
|
7059
|
-
execSync(`${this.getRuntime()} -f "${getComposeFilePath(config)}" ${args}`, { stdio: "pipe" });
|
|
7434
|
+
execSync(`${this.getRuntime()} compose -f "${getComposeFilePath(config)}" ${args}`, { stdio: "pipe" });
|
|
7060
7435
|
}
|
|
7061
7436
|
async install(config) {
|
|
7062
7437
|
this.getRuntime();
|
|
@@ -7084,7 +7459,7 @@ var init_compose = __esmMin((() => {
|
|
|
7084
7459
|
}
|
|
7085
7460
|
async status(config) {
|
|
7086
7461
|
try {
|
|
7087
|
-
const output = execSync(`${this.getRuntime()} -f "${getComposeFilePath(config)}" ps --format json`, { stdio: "pipe" }).toString();
|
|
7462
|
+
const output = execSync(`${this.getRuntime()} compose -f "${getComposeFilePath(config)}" ps --format json`, { stdio: "pipe" }).toString();
|
|
7088
7463
|
const data = JSON.parse(output);
|
|
7089
7464
|
const container = Array.isArray(data) ? data[0] : data;
|
|
7090
7465
|
if (!container) return { state: "stopped" };
|
|
@@ -7108,6 +7483,44 @@ var init_compose = __esmMin((() => {
|
|
|
7108
7483
|
const composeFile = getComposeFilePath(config);
|
|
7109
7484
|
return fs.existsSync(composeFile);
|
|
7110
7485
|
}
|
|
7486
|
+
async list(isUserLevel) {
|
|
7487
|
+
let runtime;
|
|
7488
|
+
try {
|
|
7489
|
+
runtime = this.getRuntime();
|
|
7490
|
+
} catch {
|
|
7491
|
+
return [];
|
|
7492
|
+
}
|
|
7493
|
+
let raw;
|
|
7494
|
+
try {
|
|
7495
|
+
raw = execSync(`${runtime} ps -a --filter label=dev.kici.component --format '{{json .}}'`, { encoding: "utf-8" }).toString();
|
|
7496
|
+
} catch {
|
|
7497
|
+
return [];
|
|
7498
|
+
}
|
|
7499
|
+
const out = [];
|
|
7500
|
+
for (const line of raw.split("\n")) {
|
|
7501
|
+
const trimmed = line.trim();
|
|
7502
|
+
if (!trimmed) continue;
|
|
7503
|
+
let row;
|
|
7504
|
+
try {
|
|
7505
|
+
row = JSON.parse(trimmed);
|
|
7506
|
+
} catch {
|
|
7507
|
+
continue;
|
|
7508
|
+
}
|
|
7509
|
+
if (!row || typeof row !== "object") continue;
|
|
7510
|
+
const r = row;
|
|
7511
|
+
const m = (typeof r.Labels === "string" ? r.Labels : "").match(/dev\.kici\.component=(orchestrator|agent)/);
|
|
7512
|
+
if (!m) continue;
|
|
7513
|
+
const name = typeof r.Names === "string" ? r.Names : String(r.Names ?? "");
|
|
7514
|
+
if (!name) continue;
|
|
7515
|
+
out.push({
|
|
7516
|
+
name,
|
|
7517
|
+
platform: "compose",
|
|
7518
|
+
isUserLevel,
|
|
7519
|
+
component: m[1]
|
|
7520
|
+
});
|
|
7521
|
+
}
|
|
7522
|
+
return out;
|
|
7523
|
+
}
|
|
7111
7524
|
};
|
|
7112
7525
|
}));
|
|
7113
7526
|
//#endregion
|
|
@@ -7172,976 +7585,1114 @@ function resolveServiceExecutable(opts) {
|
|
|
7172
7585
|
};
|
|
7173
7586
|
}
|
|
7174
7587
|
//#endregion
|
|
7175
|
-
//#region src/cli/
|
|
7588
|
+
//#region src/cli/commands/shared/versioned-upgrade.ts
|
|
7176
7589
|
/**
|
|
7177
|
-
* Shared
|
|
7590
|
+
* Shared versioned directory upgrade logic for kici-admin upgrade commands.
|
|
7178
7591
|
*
|
|
7179
|
-
*
|
|
7180
|
-
*
|
|
7592
|
+
* Implements the versioned directory layout:
|
|
7593
|
+
* - Extract new version alongside old versions
|
|
7594
|
+
* - Update symlink (Unix) or service registration (Windows) atomically
|
|
7595
|
+
* - Preserve old versions for rollback
|
|
7596
|
+
* - Optional cleanup of old versions
|
|
7181
7597
|
*/
|
|
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
|
|
7598
|
+
/**
|
|
7599
|
+
* Resolve the upgrade target via the folder-anchored model and build the
|
|
7600
|
+
* ServiceConfig + installBase the rest of the upgrade flow needs.
|
|
7601
|
+
*
|
|
7602
|
+
* Priority chain (delegated to {@link resolveInstance}):
|
|
7603
|
+
* 1. `opts.instanceDir` — read manifest at that path.
|
|
7604
|
+
* 2. `opts.name` — match against listInstances() output.
|
|
7605
|
+
* 3. CWD manifest — read `./.kici-<component>.json`.
|
|
7606
|
+
* 4. otherwise — refuse with a candidate-list error.
|
|
7607
|
+
*
|
|
7608
|
+
* The returned `installBase` comes from the manifest, NEVER re-derived from
|
|
7609
|
+
* the service name. Instances installed with a non-default base must
|
|
7610
|
+
* continue to resolve to that base on upgrade.
|
|
7611
|
+
*/
|
|
7612
|
+
async function resolveUpgradeTarget(args) {
|
|
7613
|
+
const { component, opts, manager, isUserLevel, kiciRoot } = args;
|
|
7614
|
+
const resolved = await resolveInstance({
|
|
7615
|
+
component,
|
|
7616
|
+
opts: {
|
|
7617
|
+
instanceDir: opts.instanceDir,
|
|
7618
|
+
name: opts.name
|
|
7619
|
+
},
|
|
7620
|
+
cwd: process.cwd(),
|
|
7621
|
+
kiciRoot,
|
|
7622
|
+
manager,
|
|
7623
|
+
isUserLevel
|
|
7211
7624
|
});
|
|
7625
|
+
return {
|
|
7626
|
+
config: {
|
|
7627
|
+
name: resolved.manifest.name,
|
|
7628
|
+
displayName: `KiCI ${component}`,
|
|
7629
|
+
description: `KiCI ${component} service`,
|
|
7630
|
+
executablePath: "",
|
|
7631
|
+
envFilePath: resolved.manifest.envFilePath,
|
|
7632
|
+
workingDirectory: resolved.manifest.configDir,
|
|
7633
|
+
isUserLevel: resolved.manifest.isUserLevel,
|
|
7634
|
+
restartPolicy: {
|
|
7635
|
+
enabled: true,
|
|
7636
|
+
delays: [
|
|
7637
|
+
1,
|
|
7638
|
+
5,
|
|
7639
|
+
15,
|
|
7640
|
+
30
|
|
7641
|
+
],
|
|
7642
|
+
maxRetries: 5,
|
|
7643
|
+
windowSeconds: 300
|
|
7644
|
+
},
|
|
7645
|
+
component
|
|
7646
|
+
},
|
|
7647
|
+
installBase: resolved.manifest.installBase,
|
|
7648
|
+
resolvedInstance: resolved
|
|
7649
|
+
};
|
|
7212
7650
|
}
|
|
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
|
-
}
|
|
7651
|
+
/** Prompt the user for confirmation (returns true if yes). */
|
|
7652
|
+
async function confirm$2(message) {
|
|
7653
|
+
const rl = createInterface({
|
|
7654
|
+
input: process.stdin,
|
|
7655
|
+
output: process.stdout
|
|
7223
7656
|
});
|
|
7224
|
-
|
|
7225
|
-
|
|
7226
|
-
|
|
7227
|
-
|
|
7228
|
-
|
|
7229
|
-
mask: "*",
|
|
7230
|
-
validate: (value) => {
|
|
7231
|
-
if (!value.trim()) return "This field is required";
|
|
7232
|
-
return true;
|
|
7233
|
-
}
|
|
7657
|
+
return new Promise((resolve) => {
|
|
7658
|
+
rl.question(`${message} [y/N] `, (answer) => {
|
|
7659
|
+
rl.close();
|
|
7660
|
+
resolve(answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes");
|
|
7661
|
+
});
|
|
7234
7662
|
});
|
|
7235
7663
|
}
|
|
7236
|
-
/**
|
|
7237
|
-
async function
|
|
7238
|
-
|
|
7239
|
-
|
|
7240
|
-
|
|
7241
|
-
|
|
7242
|
-
|
|
7664
|
+
/** Download a file from a URL to a local path. */
|
|
7665
|
+
async function downloadArchive(url, destPath) {
|
|
7666
|
+
console.log(`Downloading from ${url}...`);
|
|
7667
|
+
const res = await fetch(url);
|
|
7668
|
+
if (!res.ok || !res.body) throw new Error(`Download failed: ${res.status} ${res.statusText}`);
|
|
7669
|
+
const fileStream = createWriteStream(destPath);
|
|
7670
|
+
await pipeline(res.body, fileStream);
|
|
7671
|
+
console.log(`Downloaded to ${destPath}`);
|
|
7243
7672
|
}
|
|
7244
|
-
var init_prompts = __esmMin((() => {}));
|
|
7245
|
-
//#endregion
|
|
7246
|
-
//#region src/cli/wizard/orchestrator-wizard.ts
|
|
7247
7673
|
/**
|
|
7248
|
-
*
|
|
7674
|
+
* Install base for a KiCI component instance.
|
|
7249
7675
|
*
|
|
7250
|
-
*
|
|
7251
|
-
*
|
|
7252
|
-
*
|
|
7676
|
+
* Name-scoped so that two instances of the same component (e.g. an org's
|
|
7677
|
+
* dogfood orchestrator and an E2E test orchestrator) own independent
|
|
7678
|
+
* versioned trees and symlinks. Per-platform bases:
|
|
7679
|
+
* - systemd / compose: /opt/kici/<name>/
|
|
7680
|
+
* - launchd: /usr/local/kici/<name>/
|
|
7681
|
+
* - windows: C:\Program Files\KiCI\<name>\
|
|
7253
7682
|
*/
|
|
7254
|
-
|
|
7683
|
+
function getInstallBase(platform, name) {
|
|
7684
|
+
const sep = platform === "windows" ? "\\" : "/";
|
|
7685
|
+
switch (platform) {
|
|
7686
|
+
case "systemd":
|
|
7687
|
+
case "compose": return `/opt/kici/${name}${sep}`;
|
|
7688
|
+
case "launchd": return `/usr/local/kici/${name}${sep}`;
|
|
7689
|
+
case "windows": return `C:\\Program Files\\KiCI\\${name}${sep}`;
|
|
7690
|
+
}
|
|
7691
|
+
}
|
|
7692
|
+
/** Check if the platform is Windows. */
|
|
7693
|
+
function isWindows(platform) {
|
|
7694
|
+
return platform === "windows";
|
|
7695
|
+
}
|
|
7696
|
+
/** Get the launcher script name for a component. */
|
|
7697
|
+
function getLauncherName(component, platform) {
|
|
7698
|
+
const baseName = component === "orchestrator" ? "kici-orchestrator-standalone" : "kici-agent";
|
|
7699
|
+
return isWindows(platform) ? `${baseName}.cmd` : baseName;
|
|
7700
|
+
}
|
|
7255
7701
|
/**
|
|
7256
|
-
*
|
|
7257
|
-
*
|
|
7258
|
-
* Asks only essential questions per the user decision:
|
|
7259
|
-
* 1. Mode (platform/hybrid/independent)
|
|
7260
|
-
* 2. Database URL
|
|
7261
|
-
* 3. Port
|
|
7262
|
-
* 4. Secrets encryption key
|
|
7263
|
-
* 5. Bootstrap admin token (for kici-admin authentication)
|
|
7264
|
-
* 6. Platform URL + token (if platform/hybrid mode)
|
|
7265
|
-
* 7. Webhook secret (if hybrid/independent mode)
|
|
7702
|
+
* Extract an archive (.tar.gz or .zip) to a destination directory.
|
|
7703
|
+
* Returns the name of the top-level directory inside the archive.
|
|
7266
7704
|
*/
|
|
7267
|
-
|
|
7268
|
-
|
|
7269
|
-
|
|
7270
|
-
|
|
7271
|
-
|
|
7272
|
-
const
|
|
7273
|
-
|
|
7274
|
-
|
|
7275
|
-
value: "platform",
|
|
7276
|
-
description: "Connect to KiCI Platform for webhook routing"
|
|
7277
|
-
},
|
|
7278
|
-
{
|
|
7279
|
-
name: "Hybrid",
|
|
7280
|
-
value: "hybrid",
|
|
7281
|
-
description: "Platform relay + direct webhooks"
|
|
7282
|
-
},
|
|
7283
|
-
{
|
|
7284
|
-
name: "Independent",
|
|
7285
|
-
value: "independent",
|
|
7286
|
-
description: "Self-hosted, no Platform dependency"
|
|
7287
|
-
}
|
|
7288
|
-
], "platform");
|
|
7289
|
-
console.log("");
|
|
7290
|
-
const databaseUrl = await promptDbUrl();
|
|
7291
|
-
const port = await promptPort(4e3);
|
|
7292
|
-
console.log("");
|
|
7293
|
-
const generatedKey = randomBytes(32).toString("hex");
|
|
7294
|
-
console.log(`Generated secrets key: ${generatedKey}`);
|
|
7295
|
-
const useGenerated = await promptConfirm("Use this key?");
|
|
7296
|
-
let secretsKey;
|
|
7297
|
-
if (useGenerated) secretsKey = generatedKey;
|
|
7298
|
-
else secretsKey = await promptSecret("Enter custom secrets encryption key (64 hex chars):");
|
|
7299
|
-
console.log("");
|
|
7300
|
-
const generatedAdminToken = randomBytes(32).toString("hex");
|
|
7301
|
-
console.log(`Generated bootstrap admin token: ${generatedAdminToken}`);
|
|
7302
|
-
const useGeneratedAdminToken = await promptConfirm("Use this token?");
|
|
7303
|
-
let bootstrapAdminToken;
|
|
7304
|
-
if (useGeneratedAdminToken) bootstrapAdminToken = generatedAdminToken;
|
|
7305
|
-
else bootstrapAdminToken = await promptSecret("Enter custom bootstrap admin token:");
|
|
7306
|
-
let platformUrl;
|
|
7307
|
-
let platformToken;
|
|
7308
|
-
if (mode === "platform" || mode === "hybrid") {
|
|
7309
|
-
console.log("");
|
|
7310
|
-
platformUrl = await promptUrl("Platform relay URL:", "wss://platform.kici.dev");
|
|
7311
|
-
platformToken = await promptSecret("Platform authentication token:");
|
|
7312
|
-
}
|
|
7313
|
-
let source;
|
|
7314
|
-
console.log("");
|
|
7315
|
-
if (await promptConfirm("Add a GitHub App source?", false)) {
|
|
7316
|
-
const sourceName = await input({ message: "Source name (e.g. main-org):" });
|
|
7317
|
-
const appId = await input({
|
|
7318
|
-
message: "GitHub App ID:",
|
|
7319
|
-
validate: (v) => /^\d+$/.test(v.trim()) ? true : "Must be a numeric App ID"
|
|
7320
|
-
});
|
|
7321
|
-
const privateKeyPath = await input({
|
|
7322
|
-
message: "Path to private key file (.pem):",
|
|
7323
|
-
validate: (v) => v.trim() ? true : "Path is required"
|
|
7324
|
-
});
|
|
7325
|
-
const sourceWebhookSecret = await promptSecret("Webhook secret:");
|
|
7326
|
-
let privateKey;
|
|
7327
|
-
try {
|
|
7328
|
-
privateKey = (await readFile(privateKeyPath.trim(), "utf-8")).trim();
|
|
7329
|
-
} catch (err) {
|
|
7330
|
-
throw new Error(`Failed to read private key from ${privateKeyPath}: ${err}`);
|
|
7331
|
-
}
|
|
7332
|
-
source = {
|
|
7333
|
-
provider: "github",
|
|
7334
|
-
name: sourceName.trim(),
|
|
7335
|
-
appId: appId.trim(),
|
|
7336
|
-
privateKey,
|
|
7337
|
-
webhookSecret: sourceWebhookSecret
|
|
7338
|
-
};
|
|
7339
|
-
}
|
|
7340
|
-
console.log("");
|
|
7341
|
-
console.log("Configuration complete. Summary:");
|
|
7342
|
-
console.log(` Mode: ${mode}`);
|
|
7343
|
-
console.log(` Database: ${databaseUrl.replace(/:[^@]*@/, ":***@")}`);
|
|
7344
|
-
console.log(` Port: ${port}`);
|
|
7345
|
-
console.log(` Admin token: ${bootstrapAdminToken.slice(0, 8)}…`);
|
|
7346
|
-
console.log(` Platform: ${platformUrl ?? "N/A"}`);
|
|
7347
|
-
if (source) console.log(` Source: ${source.provider}:${source.appId} (${source.name})`);
|
|
7348
|
-
console.log("");
|
|
7349
|
-
return {
|
|
7350
|
-
mode,
|
|
7351
|
-
databaseUrl,
|
|
7352
|
-
port,
|
|
7353
|
-
secretsKey,
|
|
7354
|
-
bootstrapAdminToken,
|
|
7355
|
-
platformUrl,
|
|
7356
|
-
platformToken,
|
|
7357
|
-
source
|
|
7358
|
-
};
|
|
7705
|
+
function extractArchive(archivePath, destDir) {
|
|
7706
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
7707
|
+
if (archivePath.endsWith(".zip")) if (os.platform() === "win32") execSync(`powershell -Command "Expand-Archive -Path '${archivePath}' -DestinationPath '${destDir}' -Force"`, { stdio: "inherit" });
|
|
7708
|
+
else execSync(`unzip -o "${archivePath}" -d "${destDir}"`, { stdio: "inherit" });
|
|
7709
|
+
else execSync(`tar -xzf "${archivePath}" -C "${destDir}"`, { stdio: "inherit" });
|
|
7710
|
+
const dirs = fs.readdirSync(destDir).filter((e) => fs.statSync(path.join(destDir, e)).isDirectory());
|
|
7711
|
+
if (dirs.length === 0) throw new Error("Archive does not contain a directory");
|
|
7712
|
+
return dirs[0];
|
|
7359
7713
|
}
|
|
7360
|
-
var init_orchestrator_wizard = __esmMin((() => {
|
|
7361
|
-
init_prompts();
|
|
7362
|
-
}));
|
|
7363
|
-
//#endregion
|
|
7364
|
-
//#region src/cli/commands/orchestrator-service/install.ts
|
|
7365
7714
|
/**
|
|
7366
|
-
*
|
|
7367
|
-
*
|
|
7715
|
+
* List installed versions for a component by scanning the install base directory
|
|
7716
|
+
* for directories matching `{component}-{version}/`.
|
|
7368
7717
|
*/
|
|
7369
|
-
function
|
|
7370
|
-
|
|
7371
|
-
const
|
|
7372
|
-
|
|
7373
|
-
|
|
7374
|
-
|
|
7375
|
-
|
|
7376
|
-
|
|
7718
|
+
function listInstalledVersions(installBase, component) {
|
|
7719
|
+
if (!fs.existsSync(installBase)) return [];
|
|
7720
|
+
const prefix = `${component}-`;
|
|
7721
|
+
return fs.readdirSync(installBase).filter((entry) => {
|
|
7722
|
+
if (!entry.startsWith(prefix)) return false;
|
|
7723
|
+
const fullPath = path.join(installBase, entry);
|
|
7724
|
+
return fs.statSync(fullPath).isDirectory();
|
|
7725
|
+
}).map((entry) => entry.slice(prefix.length)).sort();
|
|
7726
|
+
}
|
|
7727
|
+
/**
|
|
7728
|
+
* Read the current symlink target to determine the active version.
|
|
7729
|
+
* Returns null if no symlink exists or on Windows.
|
|
7730
|
+
*/
|
|
7731
|
+
function getCurrentVersion(installBase, component, platform) {
|
|
7732
|
+
if (isWindows(platform)) {
|
|
7733
|
+
const versionFile = path.join(installBase, `${component}-current-version.txt`);
|
|
7377
7734
|
try {
|
|
7378
|
-
|
|
7735
|
+
return fs.readFileSync(versionFile, "utf-8").trim();
|
|
7379
7736
|
} catch {
|
|
7380
|
-
|
|
7737
|
+
return null;
|
|
7381
7738
|
}
|
|
7382
7739
|
}
|
|
7740
|
+
const symlinkPath = path.join(installBase, component);
|
|
7383
7741
|
try {
|
|
7384
|
-
|
|
7385
|
-
|
|
7386
|
-
|
|
7387
|
-
|
|
7388
|
-
|
|
7742
|
+
const target = fs.readlinkSync(symlinkPath);
|
|
7743
|
+
const prefix = `${component}-`;
|
|
7744
|
+
if (target.startsWith(prefix)) return target.slice(prefix.length);
|
|
7745
|
+
const basename = path.basename(target);
|
|
7746
|
+
if (basename.startsWith(prefix)) return basename.slice(prefix.length);
|
|
7747
|
+
} catch {}
|
|
7748
|
+
return null;
|
|
7749
|
+
}
|
|
7750
|
+
/** Write the current version to a tracking file (used on Windows). */
|
|
7751
|
+
function writeCurrentVersion(installBase, component, version) {
|
|
7752
|
+
const versionFile = path.join(installBase, `${component}-current-version.txt`);
|
|
7753
|
+
fs.writeFileSync(versionFile, version, "utf-8");
|
|
7754
|
+
}
|
|
7755
|
+
/**
|
|
7756
|
+
* Update the symlink atomically on Unix.
|
|
7757
|
+
* Creates a temporary symlink then renames it over the existing one.
|
|
7758
|
+
*/
|
|
7759
|
+
function updateSymlinkAtomic(installBase, component, version) {
|
|
7760
|
+
const symlinkPath = path.join(installBase, component);
|
|
7761
|
+
const tmpLink = `${symlinkPath}.tmp.${Date.now()}`;
|
|
7762
|
+
const target = `${component}-${version}`;
|
|
7763
|
+
try {
|
|
7764
|
+
fs.symlinkSync(target, tmpLink);
|
|
7765
|
+
fs.renameSync(tmpLink, symlinkPath);
|
|
7389
7766
|
} catch (err) {
|
|
7390
|
-
|
|
7767
|
+
try {
|
|
7768
|
+
fs.unlinkSync(tmpLink);
|
|
7769
|
+
} catch {}
|
|
7770
|
+
throw err;
|
|
7391
7771
|
}
|
|
7392
|
-
console.log(`Starting dev PostgreSQL container "${containerName}" on port ${port}...`);
|
|
7393
|
-
execSync(`${runtime} run -d --name ${containerName} -p ${port}:5432 -e POSTGRES_PASSWORD=${password} -e POSTGRES_DB=kici postgres:18-trixie`, { stdio: "inherit" });
|
|
7394
|
-
return `postgresql://postgres:${password}@localhost:${port}/kici`;
|
|
7395
7772
|
}
|
|
7396
|
-
|
|
7397
|
-
|
|
7398
|
-
|
|
7399
|
-
|
|
7400
|
-
|
|
7773
|
+
/**
|
|
7774
|
+
* Perform a versioned directory upgrade for a KiCI component.
|
|
7775
|
+
*
|
|
7776
|
+
* Flow:
|
|
7777
|
+
* 1. Parse upgrade source (--from archive or --url)
|
|
7778
|
+
* 2. Determine install base by platform
|
|
7779
|
+
* 3. Extract new versioned directory
|
|
7780
|
+
* 4. Stop service
|
|
7781
|
+
* 5. Update symlink (Unix) or service registration (Windows)
|
|
7782
|
+
* 6. Start service
|
|
7783
|
+
*/
|
|
7784
|
+
async function performVersionedUpgrade(component, opts) {
|
|
7785
|
+
try {
|
|
7786
|
+
const platform = detectPlatform(opts.platform);
|
|
7787
|
+
const manager = await createServiceManager(platform);
|
|
7788
|
+
const userLevel = !isRoot();
|
|
7789
|
+
const kiciRoot = kiciConfigRoot(userLevel);
|
|
7790
|
+
const { config, installBase, resolvedInstance } = await resolveUpgradeTarget({
|
|
7791
|
+
component,
|
|
7792
|
+
opts: {
|
|
7793
|
+
instanceDir: opts.instanceDir,
|
|
7794
|
+
name: opts.name
|
|
7795
|
+
},
|
|
7796
|
+
manager,
|
|
7797
|
+
isUserLevel: userLevel,
|
|
7798
|
+
kiciRoot
|
|
7799
|
+
});
|
|
7800
|
+
if (!isWindows(platform) && !userLevel && !isRoot()) {
|
|
7801
|
+
console.error("Error: root privileges required to upgrade system-level services");
|
|
7802
|
+
process.exit(1);
|
|
7803
|
+
}
|
|
7804
|
+
if (opts.rollback) {
|
|
7805
|
+
await handleRollback(component, platform, installBase, config, manager, opts);
|
|
7806
|
+
return;
|
|
7807
|
+
}
|
|
7808
|
+
if (opts.cleanup) {
|
|
7809
|
+
await handleCleanup(component, platform, installBase);
|
|
7810
|
+
return;
|
|
7811
|
+
}
|
|
7812
|
+
if (!opts.from && !opts.url) {
|
|
7813
|
+
console.error("Error: provide --from <archive-path> or --url <url> for the upgrade package");
|
|
7814
|
+
process.exit(1);
|
|
7815
|
+
}
|
|
7816
|
+
if (!opts.version) {
|
|
7817
|
+
console.error("Error: --version is required to specify the target version");
|
|
7818
|
+
process.exit(1);
|
|
7819
|
+
}
|
|
7820
|
+
const version = opts.version;
|
|
7821
|
+
const versionedDirName = `${component}-${version}`;
|
|
7822
|
+
const versionedDirPath = path.join(installBase, versionedDirName);
|
|
7823
|
+
if (fs.existsSync(versionedDirPath)) if (opts.force) {
|
|
7824
|
+
console.log(`Removing existing directory ${versionedDirPath} (--force)`);
|
|
7825
|
+
fs.rmSync(versionedDirPath, {
|
|
7826
|
+
recursive: true,
|
|
7827
|
+
force: true
|
|
7828
|
+
});
|
|
7829
|
+
} else {
|
|
7830
|
+
console.error(`Error: version directory already exists: ${versionedDirPath}`);
|
|
7831
|
+
console.error("Use --force to overwrite.");
|
|
7832
|
+
process.exit(1);
|
|
7833
|
+
}
|
|
7834
|
+
if (!await manager.isInstalled(config)) {
|
|
7835
|
+
console.error(`Error: service "${config.name}" is not installed`);
|
|
7836
|
+
process.exit(1);
|
|
7837
|
+
}
|
|
7838
|
+
let archivePath;
|
|
7839
|
+
const tmpDir = path.join(os.tmpdir(), `kici-upgrade-${Date.now()}`);
|
|
7840
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
7841
|
+
if (opts.from) {
|
|
7842
|
+
archivePath = path.resolve(opts.from);
|
|
7843
|
+
if (!fs.existsSync(archivePath)) {
|
|
7844
|
+
console.error(`Error: archive not found at ${archivePath}`);
|
|
7401
7845
|
process.exit(1);
|
|
7402
7846
|
}
|
|
7403
|
-
|
|
7404
|
-
const
|
|
7405
|
-
|
|
7406
|
-
|
|
7407
|
-
|
|
7408
|
-
|
|
7409
|
-
|
|
7410
|
-
|
|
7411
|
-
|
|
7412
|
-
|
|
7413
|
-
|
|
7414
|
-
|
|
7415
|
-
|
|
7416
|
-
|
|
7417
|
-
console.log(
|
|
7418
|
-
|
|
7419
|
-
if (opts.wizard) {
|
|
7420
|
-
const { runOrchestratorWizard } = await Promise.resolve().then(() => (init_orchestrator_wizard(), orchestrator_wizard_exports));
|
|
7421
|
-
const wizardConfig = await runOrchestratorWizard();
|
|
7422
|
-
let envContent = "# KiCI orchestrator configuration (generated by setup wizard)\n";
|
|
7423
|
-
envContent += `KICI_MODE=${wizardConfig.mode}\n`;
|
|
7424
|
-
envContent += `KICI_DATABASE_URL=${wizardConfig.databaseUrl}\n`;
|
|
7425
|
-
envContent += `KICI_PORT=${wizardConfig.port}\n`;
|
|
7426
|
-
envContent += `KICI_SECRET_KEY=${wizardConfig.secretsKey}\n`;
|
|
7427
|
-
envContent += `KICI_BOOTSTRAP_ADMIN_TOKEN=${wizardConfig.bootstrapAdminToken}\n`;
|
|
7428
|
-
if (wizardConfig.platformUrl) envContent += `KICI_PLATFORM_URL=${wizardConfig.platformUrl}\n`;
|
|
7429
|
-
if (wizardConfig.platformToken) envContent += `KICI_PLATFORM_TOKEN=${wizardConfig.platformToken}\n`;
|
|
7430
|
-
fs.writeFileSync(envFilePath, envContent, "utf-8");
|
|
7431
|
-
console.log(`Wrote wizard configuration to ${envFilePath}`);
|
|
7432
|
-
} else if (opts.envFile) {
|
|
7433
|
-
const source = path.resolve(opts.envFile);
|
|
7434
|
-
if (!fs.existsSync(source)) {
|
|
7435
|
-
console.error(`Error: env file not found: ${source}`);
|
|
7436
|
-
process.exit(1);
|
|
7437
|
-
}
|
|
7438
|
-
fs.copyFileSync(source, envFilePath);
|
|
7439
|
-
console.log(`Copied env file to ${envFilePath}`);
|
|
7440
|
-
} else if (!fs.existsSync(envFilePath)) {
|
|
7441
|
-
let envContent = `# KiCI orchestrator configuration\n# See docs for all available options\n`;
|
|
7442
|
-
if (devDbUrl) envContent += `KICI_DATABASE_URL=${devDbUrl}\n`;
|
|
7443
|
-
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}`);
|
|
7847
|
+
} else {
|
|
7848
|
+
const ext = opts.url.endsWith(".zip") ? ".zip" : ".tar.gz";
|
|
7849
|
+
archivePath = path.join(tmpDir, `${component}-${version}${ext}`);
|
|
7850
|
+
await downloadArchive(opts.url, archivePath);
|
|
7851
|
+
}
|
|
7852
|
+
const currentVersion = getCurrentVersion(installBase, component, platform);
|
|
7853
|
+
if (!opts.yes) {
|
|
7854
|
+
console.log(`This will upgrade "${config.name}" to version ${version}:`);
|
|
7855
|
+
if (currentVersion) console.log(` Current version: ${currentVersion}`);
|
|
7856
|
+
console.log(` New version: ${version}`);
|
|
7857
|
+
console.log(` Install path: ${versionedDirPath}`);
|
|
7858
|
+
console.log(" The service will be stopped during upgrade.");
|
|
7859
|
+
console.log("");
|
|
7860
|
+
if (!await confirm$2("Proceed with upgrade?")) {
|
|
7861
|
+
console.log("Upgrade cancelled.");
|
|
7862
|
+
return;
|
|
7448
7863
|
}
|
|
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
7864
|
}
|
|
7487
|
-
|
|
7865
|
+
console.log("Extracting archive...");
|
|
7866
|
+
const extractDir = path.join(tmpDir, "extract");
|
|
7867
|
+
const extractedDirName = extractArchive(archivePath, extractDir);
|
|
7868
|
+
fs.mkdirSync(installBase, { recursive: true });
|
|
7869
|
+
const srcDir = path.join(extractDir, extractedDirName);
|
|
7870
|
+
if (isWindows(platform)) execSync(`xcopy "${srcDir}" "${versionedDirPath}" /E /I /Q /Y`, { stdio: "inherit" });
|
|
7871
|
+
else execSync(`cp -r "${srcDir}" "${versionedDirPath}"`, { stdio: "inherit" });
|
|
7872
|
+
console.log(`Extracted to ${versionedDirPath}`);
|
|
7873
|
+
console.log("Stopping service...");
|
|
7874
|
+
if ((await manager.status(config)).state === "running") {
|
|
7875
|
+
await manager.stop(config);
|
|
7876
|
+
console.log("Service stopped.");
|
|
7877
|
+
}
|
|
7878
|
+
if (isWindows(platform)) {
|
|
7879
|
+
const launcherPath = path.join(versionedDirPath, getLauncherName(component, platform));
|
|
7880
|
+
config.executablePath = launcherPath;
|
|
7881
|
+
await manager.uninstall(config);
|
|
7882
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
7883
|
+
await manager.install(config);
|
|
7884
|
+
writeCurrentVersion(installBase, component, version);
|
|
7885
|
+
console.log(`Service registration updated to ${launcherPath}`);
|
|
7886
|
+
} else {
|
|
7887
|
+
updateSymlinkAtomic(installBase, component, version);
|
|
7888
|
+
const symlinkPath = path.join(installBase, component);
|
|
7889
|
+
console.log(`Symlink updated: ${symlinkPath} -> ${versionedDirName}`);
|
|
7890
|
+
const launcherPath = path.join(symlinkPath, getLauncherName(component, platform));
|
|
7891
|
+
if (fs.existsSync(launcherPath)) fs.chmodSync(launcherPath, 493);
|
|
7892
|
+
config.executablePath = path.join(installBase, component, getLauncherName(component, platform));
|
|
7893
|
+
}
|
|
7894
|
+
console.log("Starting service...");
|
|
7895
|
+
await manager.start(config);
|
|
7896
|
+
console.log("Service started.");
|
|
7897
|
+
writeManifest(resolvedInstance.instanceDir, {
|
|
7898
|
+
...resolvedInstance.manifest,
|
|
7899
|
+
kiciVersion: version
|
|
7900
|
+
});
|
|
7901
|
+
fs.rmSync(tmpDir, {
|
|
7902
|
+
recursive: true,
|
|
7903
|
+
force: true
|
|
7904
|
+
});
|
|
7905
|
+
console.log("");
|
|
7906
|
+
if (currentVersion) {
|
|
7907
|
+
console.log(`Upgrade complete: ${currentVersion} -> ${version}`);
|
|
7908
|
+
console.log(`Previous version preserved at: ${path.join(installBase, `${component}-${currentVersion}`)}`);
|
|
7909
|
+
} else console.log(`Upgrade to ${version} complete.`);
|
|
7910
|
+
} catch (err) {
|
|
7911
|
+
console.error(`Error: ${toErrorMessage(err)}`);
|
|
7912
|
+
process.exit(1);
|
|
7913
|
+
}
|
|
7914
|
+
}
|
|
7915
|
+
/**
|
|
7916
|
+
* Handle --rollback: switch symlink to the previous version and restart.
|
|
7917
|
+
*/
|
|
7918
|
+
async function handleRollback(component, platform, installBase, config, manager, opts) {
|
|
7919
|
+
const versions = listInstalledVersions(installBase, component);
|
|
7920
|
+
if (versions.length < 2) {
|
|
7921
|
+
console.error("Error: no previous version available for rollback");
|
|
7922
|
+
if (versions.length === 1) console.error(`Only version installed: ${versions[0]}`);
|
|
7923
|
+
process.exit(1);
|
|
7924
|
+
}
|
|
7925
|
+
const currentVersion = getCurrentVersion(installBase, component, platform);
|
|
7926
|
+
if (!currentVersion) {
|
|
7927
|
+
console.error("Error: cannot determine current version (no symlink found)");
|
|
7928
|
+
console.log("Available versions:");
|
|
7929
|
+
for (const v of versions) console.log(` ${component}-${v}/`);
|
|
7930
|
+
process.exit(1);
|
|
7931
|
+
}
|
|
7932
|
+
const currentIdx = versions.indexOf(currentVersion);
|
|
7933
|
+
let previousVersion;
|
|
7934
|
+
if (currentIdx > 0) previousVersion = versions[currentIdx - 1];
|
|
7935
|
+
else if (versions.length >= 2) previousVersion = versions[1];
|
|
7936
|
+
else {
|
|
7937
|
+
console.error("Error: no alternative version available for rollback");
|
|
7938
|
+
process.exit(1);
|
|
7939
|
+
return;
|
|
7940
|
+
}
|
|
7941
|
+
if (!opts.yes) {
|
|
7942
|
+
console.log(`Rolling back "${config.name}":`);
|
|
7943
|
+
console.log(` Current version: ${currentVersion}`);
|
|
7944
|
+
console.log(` Rollback to: ${previousVersion}`);
|
|
7945
|
+
console.log("");
|
|
7946
|
+
if (!await confirm$2("Proceed with rollback?")) {
|
|
7947
|
+
console.log("Rollback cancelled.");
|
|
7948
|
+
return;
|
|
7949
|
+
}
|
|
7950
|
+
}
|
|
7951
|
+
console.log("Stopping service...");
|
|
7952
|
+
if ((await manager.status(config)).state === "running") {
|
|
7953
|
+
await manager.stop(config);
|
|
7954
|
+
console.log("Service stopped.");
|
|
7955
|
+
}
|
|
7956
|
+
if (isWindows(platform)) {
|
|
7957
|
+
const launcherPath = path.join(installBase, `${component}-${previousVersion}`, getLauncherName(component, platform));
|
|
7958
|
+
config.executablePath = launcherPath;
|
|
7959
|
+
await manager.uninstall(config);
|
|
7960
|
+
await manager.install(config);
|
|
7961
|
+
writeCurrentVersion(installBase, component, previousVersion);
|
|
7962
|
+
console.log(`Service registration updated to ${launcherPath}`);
|
|
7963
|
+
} else {
|
|
7964
|
+
updateSymlinkAtomic(installBase, component, previousVersion);
|
|
7965
|
+
console.log(`Symlink updated: ${path.join(installBase, component)} -> ${component}-${previousVersion}`);
|
|
7966
|
+
}
|
|
7967
|
+
console.log("Starting service...");
|
|
7968
|
+
await manager.start(config);
|
|
7969
|
+
console.log("Service started.");
|
|
7970
|
+
console.log("");
|
|
7971
|
+
console.log(`Rollback complete: ${currentVersion} -> ${previousVersion}`);
|
|
7972
|
+
}
|
|
7973
|
+
/**
|
|
7974
|
+
* Handle --cleanup: remove all versioned directories except the current
|
|
7975
|
+
* and previous versions.
|
|
7976
|
+
*/
|
|
7977
|
+
async function handleCleanup(component, platform, installBase) {
|
|
7978
|
+
const versions = listInstalledVersions(installBase, component);
|
|
7979
|
+
if (versions.length <= 2) {
|
|
7980
|
+
console.log("Nothing to clean up (2 or fewer versions installed).");
|
|
7981
|
+
return;
|
|
7982
|
+
}
|
|
7983
|
+
const currentVersion = getCurrentVersion(installBase, component, platform);
|
|
7984
|
+
const currentIdx = currentVersion ? versions.indexOf(currentVersion) : versions.length - 1;
|
|
7985
|
+
const previousIdx = currentIdx > 0 ? currentIdx - 1 : -1;
|
|
7986
|
+
const toRemove = versions.filter((_, i) => i !== currentIdx && i !== previousIdx);
|
|
7987
|
+
if (toRemove.length === 0) {
|
|
7988
|
+
console.log("Nothing to clean up.");
|
|
7989
|
+
return;
|
|
7990
|
+
}
|
|
7991
|
+
console.log("The following versions will be removed:");
|
|
7992
|
+
for (const v of toRemove) console.log(` ${component}-${v}/`);
|
|
7993
|
+
if (currentVersion) console.log(`\nKeeping: ${component}-${currentVersion}/ (current)`);
|
|
7994
|
+
if (previousIdx >= 0) console.log(`Keeping: ${component}-${versions[previousIdx]}/ (previous)`);
|
|
7995
|
+
for (const v of toRemove) {
|
|
7996
|
+
const dirPath = path.join(installBase, `${component}-${v}`);
|
|
7997
|
+
fs.rmSync(dirPath, {
|
|
7998
|
+
recursive: true,
|
|
7999
|
+
force: true
|
|
8000
|
+
});
|
|
8001
|
+
console.log(`Removed ${dirPath}`);
|
|
8002
|
+
}
|
|
8003
|
+
console.log(`\nCleanup complete. Removed ${toRemove.length} old version(s).`);
|
|
7488
8004
|
}
|
|
7489
8005
|
//#endregion
|
|
7490
|
-
//#region src/cli/
|
|
7491
|
-
|
|
7492
|
-
|
|
7493
|
-
|
|
7494
|
-
|
|
7495
|
-
|
|
7496
|
-
|
|
7497
|
-
|
|
7498
|
-
|
|
7499
|
-
|
|
7500
|
-
|
|
7501
|
-
|
|
7502
|
-
|
|
7503
|
-
|
|
7504
|
-
|
|
7505
|
-
|
|
7506
|
-
|
|
7507
|
-
|
|
7508
|
-
|
|
7509
|
-
|
|
7510
|
-
|
|
7511
|
-
|
|
7512
|
-
|
|
7513
|
-
|
|
7514
|
-
|
|
7515
|
-
|
|
7516
|
-
|
|
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);
|
|
8006
|
+
//#region src/cli/wizard/prompts.ts
|
|
8007
|
+
/**
|
|
8008
|
+
* Shared prompt utilities for the setup wizards.
|
|
8009
|
+
*
|
|
8010
|
+
* Wraps @inquirer/prompts with consistent formatting and
|
|
8011
|
+
* validation for common input types (DB URLs, ports, etc.).
|
|
8012
|
+
*/
|
|
8013
|
+
/** Prompt for a PostgreSQL database URL with validation. */
|
|
8014
|
+
async function promptDbUrl() {
|
|
8015
|
+
return input({
|
|
8016
|
+
message: "PostgreSQL database URL:",
|
|
8017
|
+
validate: (value) => {
|
|
8018
|
+
const v = value.trim();
|
|
8019
|
+
if (!v.startsWith("postgresql://") && !v.startsWith("postgres://")) return "Must start with postgresql:// or postgres://";
|
|
8020
|
+
return true;
|
|
8021
|
+
}
|
|
8022
|
+
});
|
|
8023
|
+
}
|
|
8024
|
+
/** Prompt for a port number with validation. */
|
|
8025
|
+
async function promptPort(defaultPort) {
|
|
8026
|
+
const value = await input({
|
|
8027
|
+
message: "Port:",
|
|
8028
|
+
default: String(defaultPort),
|
|
8029
|
+
validate: (value) => {
|
|
8030
|
+
const n = parseInt(value.trim(), 10);
|
|
8031
|
+
if (isNaN(n) || n < 1 || n > 65535) return "Must be a number between 1 and 65535";
|
|
8032
|
+
return true;
|
|
7529
8033
|
}
|
|
7530
8034
|
});
|
|
8035
|
+
return parseInt(value.trim(), 10);
|
|
7531
8036
|
}
|
|
7532
|
-
|
|
7533
|
-
|
|
7534
|
-
|
|
7535
|
-
|
|
7536
|
-
|
|
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
|
-
}
|
|
8037
|
+
/** Prompt for a yes/no confirmation. */
|
|
8038
|
+
async function promptConfirm(message, defaultValue = true) {
|
|
8039
|
+
return confirm({
|
|
8040
|
+
message,
|
|
8041
|
+
default: defaultValue
|
|
7563
8042
|
});
|
|
7564
8043
|
}
|
|
7565
|
-
|
|
7566
|
-
|
|
7567
|
-
|
|
7568
|
-
|
|
7569
|
-
|
|
7570
|
-
|
|
7571
|
-
const
|
|
7572
|
-
|
|
7573
|
-
|
|
7574
|
-
const config = {
|
|
7575
|
-
name: serviceName,
|
|
7576
|
-
displayName: "KiCI Orchestrator",
|
|
7577
|
-
description: "KiCI CI/CD workflow orchestrator service",
|
|
7578
|
-
executablePath: "",
|
|
7579
|
-
envFilePath: `${configDir}${serviceName}.env`,
|
|
7580
|
-
workingDirectory: configDir,
|
|
7581
|
-
isUserLevel: userLevel,
|
|
7582
|
-
restartPolicy: DEFAULT_RESTART_POLICY
|
|
7583
|
-
};
|
|
7584
|
-
await (await createServiceManager(platform)).stop(config);
|
|
7585
|
-
console.log(`Orchestrator service "${serviceName}" stopped.`);
|
|
7586
|
-
} catch (err) {
|
|
7587
|
-
console.error(`Error: ${toErrorMessage(err)}`);
|
|
7588
|
-
process.exit(1);
|
|
8044
|
+
/** Prompt for a URL with http(s):// validation. */
|
|
8045
|
+
async function promptUrl(message, defaultValue) {
|
|
8046
|
+
return input({
|
|
8047
|
+
message,
|
|
8048
|
+
default: defaultValue,
|
|
8049
|
+
validate: (value) => {
|
|
8050
|
+
const v = value.trim();
|
|
8051
|
+
if (!v.startsWith("http://") && !v.startsWith("https://") && !v.startsWith("wss://") && !v.startsWith("ws://")) return "Must start with http://, https://, ws://, or wss://";
|
|
8052
|
+
return true;
|
|
7589
8053
|
}
|
|
7590
8054
|
});
|
|
7591
8055
|
}
|
|
7592
|
-
|
|
7593
|
-
|
|
7594
|
-
|
|
7595
|
-
|
|
7596
|
-
|
|
7597
|
-
|
|
7598
|
-
|
|
7599
|
-
|
|
7600
|
-
const configDir = getConfigDir(serviceName, userLevel);
|
|
7601
|
-
const config = {
|
|
7602
|
-
name: serviceName,
|
|
7603
|
-
displayName: "KiCI Orchestrator",
|
|
7604
|
-
description: "KiCI CI/CD workflow orchestrator service",
|
|
7605
|
-
executablePath: "",
|
|
7606
|
-
envFilePath: `${configDir}${serviceName}.env`,
|
|
7607
|
-
workingDirectory: configDir,
|
|
7608
|
-
isUserLevel: userLevel,
|
|
7609
|
-
restartPolicy: DEFAULT_RESTART_POLICY
|
|
7610
|
-
};
|
|
7611
|
-
const manager = await createServiceManager(platform);
|
|
7612
|
-
if (!await manager.isInstalled(config)) {
|
|
7613
|
-
console.error(`Error: service "${serviceName}" is not installed.`);
|
|
7614
|
-
console.error(`Run \`kici-admin orchestrator install\` first.`);
|
|
7615
|
-
process.exit(1);
|
|
7616
|
-
}
|
|
7617
|
-
await manager.restart(config);
|
|
7618
|
-
console.log(`Orchestrator service "${serviceName}" restarted.`);
|
|
7619
|
-
} catch (err) {
|
|
7620
|
-
console.error(`Error: ${toErrorMessage(err)}`);
|
|
7621
|
-
process.exit(1);
|
|
8056
|
+
/** Prompt for a secret/password (masked input). */
|
|
8057
|
+
async function promptSecret(message) {
|
|
8058
|
+
return password({
|
|
8059
|
+
message,
|
|
8060
|
+
mask: "*",
|
|
8061
|
+
validate: (value) => {
|
|
8062
|
+
if (!value.trim()) return "This field is required";
|
|
8063
|
+
return true;
|
|
7622
8064
|
}
|
|
7623
8065
|
});
|
|
7624
8066
|
}
|
|
8067
|
+
/** Prompt for a selection from a list of options. */
|
|
8068
|
+
async function promptSelect(message, choices, defaultValue) {
|
|
8069
|
+
return select({
|
|
8070
|
+
message,
|
|
8071
|
+
choices,
|
|
8072
|
+
default: defaultValue
|
|
8073
|
+
});
|
|
8074
|
+
}
|
|
8075
|
+
var init_prompts = __esmMin((() => {}));
|
|
7625
8076
|
//#endregion
|
|
7626
|
-
//#region src/cli/
|
|
7627
|
-
/**
|
|
7628
|
-
|
|
7629
|
-
|
|
7630
|
-
|
|
7631
|
-
|
|
7632
|
-
|
|
7633
|
-
|
|
7634
|
-
|
|
7635
|
-
|
|
7636
|
-
|
|
7637
|
-
|
|
7638
|
-
|
|
7639
|
-
|
|
7640
|
-
|
|
7641
|
-
|
|
8077
|
+
//#region src/cli/wizard/orchestrator-wizard.ts
|
|
8078
|
+
/**
|
|
8079
|
+
* Interactive wizard for orchestrator service setup.
|
|
8080
|
+
*
|
|
8081
|
+
* Walks the user through essential configuration (mode, DB URL,
|
|
8082
|
+
* port, secrets key) with sensible defaults. Returns a config
|
|
8083
|
+
* object that the install command uses to write the env file.
|
|
8084
|
+
*/
|
|
8085
|
+
var orchestrator_wizard_exports = /* @__PURE__ */ __exportAll({ runOrchestratorWizard: () => runOrchestratorWizard });
|
|
8086
|
+
/**
|
|
8087
|
+
* Run the interactive orchestrator setup wizard.
|
|
8088
|
+
*
|
|
8089
|
+
* Asks only essential questions per the user decision:
|
|
8090
|
+
* 1. Mode (platform/hybrid/independent)
|
|
8091
|
+
* 2. Database URL
|
|
8092
|
+
* 3. Port
|
|
8093
|
+
* 4. Secrets encryption key
|
|
8094
|
+
* 5. Bootstrap admin token (for kici-admin authentication)
|
|
8095
|
+
* 6. Platform URL + token (if platform/hybrid mode)
|
|
8096
|
+
* 7. Webhook secret (if hybrid/independent mode)
|
|
8097
|
+
*/
|
|
8098
|
+
async function runOrchestratorWizard() {
|
|
8099
|
+
console.log("");
|
|
8100
|
+
console.log("KiCI orchestrator setup wizard");
|
|
8101
|
+
console.log("==============================");
|
|
8102
|
+
console.log("");
|
|
8103
|
+
const mode = await promptSelect("Operating mode:", [
|
|
8104
|
+
{
|
|
8105
|
+
name: "Platform relay (recommended)",
|
|
8106
|
+
value: "platform",
|
|
8107
|
+
description: "Connect to KiCI Platform for webhook routing"
|
|
8108
|
+
},
|
|
8109
|
+
{
|
|
8110
|
+
name: "Hybrid",
|
|
8111
|
+
value: "hybrid",
|
|
8112
|
+
description: "Platform relay + direct webhooks"
|
|
8113
|
+
},
|
|
8114
|
+
{
|
|
8115
|
+
name: "Independent",
|
|
8116
|
+
value: "independent",
|
|
8117
|
+
description: "Self-hosted, no Platform dependency"
|
|
7642
8118
|
}
|
|
7643
|
-
|
|
7644
|
-
|
|
7645
|
-
|
|
7646
|
-
|
|
7647
|
-
|
|
7648
|
-
|
|
7649
|
-
|
|
7650
|
-
|
|
7651
|
-
|
|
7652
|
-
|
|
7653
|
-
|
|
7654
|
-
|
|
7655
|
-
|
|
7656
|
-
|
|
8119
|
+
], "platform");
|
|
8120
|
+
console.log("");
|
|
8121
|
+
const databaseUrl = await promptDbUrl();
|
|
8122
|
+
const port = await promptPort(4e3);
|
|
8123
|
+
console.log("");
|
|
8124
|
+
const generatedKey = randomBytes(32).toString("hex");
|
|
8125
|
+
console.log(`Generated secrets key: ${generatedKey}`);
|
|
8126
|
+
const useGenerated = await promptConfirm("Use this key?");
|
|
8127
|
+
let secretsKey;
|
|
8128
|
+
if (useGenerated) secretsKey = generatedKey;
|
|
8129
|
+
else secretsKey = await promptSecret("Enter custom secrets encryption key (64 hex chars):");
|
|
8130
|
+
console.log("");
|
|
8131
|
+
const generatedAdminToken = randomBytes(32).toString("hex");
|
|
8132
|
+
console.log(`Generated bootstrap admin token: ${generatedAdminToken}`);
|
|
8133
|
+
const useGeneratedAdminToken = await promptConfirm("Use this token?");
|
|
8134
|
+
let bootstrapAdminToken;
|
|
8135
|
+
if (useGeneratedAdminToken) bootstrapAdminToken = generatedAdminToken;
|
|
8136
|
+
else bootstrapAdminToken = await promptSecret("Enter custom bootstrap admin token:");
|
|
8137
|
+
let platformUrl;
|
|
8138
|
+
let platformToken;
|
|
8139
|
+
if (mode === "platform" || mode === "hybrid") {
|
|
8140
|
+
console.log("");
|
|
8141
|
+
platformUrl = await promptUrl("Platform relay URL:", "wss://platform.kici.dev");
|
|
8142
|
+
platformToken = await promptSecret("Platform authentication token:");
|
|
7657
8143
|
}
|
|
7658
|
-
|
|
7659
|
-
|
|
7660
|
-
|
|
7661
|
-
|
|
7662
|
-
|
|
7663
|
-
|
|
7664
|
-
|
|
7665
|
-
|
|
7666
|
-
|
|
7667
|
-
|
|
7668
|
-
|
|
7669
|
-
|
|
7670
|
-
|
|
7671
|
-
|
|
7672
|
-
|
|
7673
|
-
|
|
7674
|
-
|
|
7675
|
-
|
|
7676
|
-
const s = health.scaler;
|
|
7677
|
-
lines.push(`Scaler: ${s.type ?? "none"} (warm: ${s.warm ?? 0}, max: ${s.max ?? 0})`);
|
|
8144
|
+
let source;
|
|
8145
|
+
console.log("");
|
|
8146
|
+
if (await promptConfirm("Add a GitHub App source?", false)) {
|
|
8147
|
+
const sourceName = await input({ message: "Source name (e.g. main-org):" });
|
|
8148
|
+
const appId = await input({
|
|
8149
|
+
message: "GitHub App ID:",
|
|
8150
|
+
validate: (v) => /^\d+$/.test(v.trim()) ? true : "Must be a numeric App ID"
|
|
8151
|
+
});
|
|
8152
|
+
const privateKeyPath = await input({
|
|
8153
|
+
message: "Path to private key file (.pem):",
|
|
8154
|
+
validate: (v) => v.trim() ? true : "Path is required"
|
|
8155
|
+
});
|
|
8156
|
+
const sourceWebhookSecret = await promptSecret("Webhook secret:");
|
|
8157
|
+
let privateKey;
|
|
8158
|
+
try {
|
|
8159
|
+
privateKey = (await readFile(privateKeyPath.trim(), "utf-8")).trim();
|
|
8160
|
+
} catch (err) {
|
|
8161
|
+
throw new Error(`Failed to read private key from ${privateKeyPath}: ${err}`);
|
|
7678
8162
|
}
|
|
7679
|
-
|
|
7680
|
-
|
|
7681
|
-
|
|
7682
|
-
|
|
8163
|
+
source = {
|
|
8164
|
+
provider: "github",
|
|
8165
|
+
name: sourceName.trim(),
|
|
8166
|
+
appId: appId.trim(),
|
|
8167
|
+
privateKey,
|
|
8168
|
+
webhookSecret: sourceWebhookSecret
|
|
8169
|
+
};
|
|
7683
8170
|
}
|
|
7684
|
-
|
|
7685
|
-
|
|
7686
|
-
|
|
7687
|
-
|
|
8171
|
+
console.log("");
|
|
8172
|
+
console.log("Configuration complete. Summary:");
|
|
8173
|
+
console.log(` Mode: ${mode}`);
|
|
8174
|
+
console.log(` Database: ${databaseUrl.replace(/:[^@]*@/, ":***@")}`);
|
|
8175
|
+
console.log(` Port: ${port}`);
|
|
8176
|
+
console.log(` Admin token: ${bootstrapAdminToken.slice(0, 8)}…`);
|
|
8177
|
+
console.log(` Platform: ${platformUrl ?? "N/A"}`);
|
|
8178
|
+
if (source) console.log(` Source: ${source.provider}:${source.appId} (${source.name})`);
|
|
8179
|
+
console.log("");
|
|
7688
8180
|
return {
|
|
7689
|
-
|
|
7690
|
-
|
|
7691
|
-
|
|
8181
|
+
mode,
|
|
8182
|
+
databaseUrl,
|
|
8183
|
+
port,
|
|
8184
|
+
secretsKey,
|
|
8185
|
+
bootstrapAdminToken,
|
|
8186
|
+
platformUrl,
|
|
8187
|
+
platformToken,
|
|
8188
|
+
source
|
|
7692
8189
|
};
|
|
7693
8190
|
}
|
|
7694
|
-
|
|
7695
|
-
|
|
8191
|
+
var init_orchestrator_wizard = __esmMin((() => {
|
|
8192
|
+
init_prompts();
|
|
8193
|
+
}));
|
|
8194
|
+
//#endregion
|
|
8195
|
+
//#region src/cli/commands/orchestrator-service/install.ts
|
|
8196
|
+
/**
|
|
8197
|
+
* Spin up a dev PostgreSQL container using Docker or Podman.
|
|
8198
|
+
* Returns the DATABASE_URL for the container.
|
|
8199
|
+
*/
|
|
8200
|
+
function startDevPostgres(containerName) {
|
|
8201
|
+
const password = crypto.randomBytes(16).toString("hex");
|
|
8202
|
+
const port = 15432;
|
|
8203
|
+
let runtime = "podman";
|
|
8204
|
+
try {
|
|
8205
|
+
execSync("podman --version", { stdio: "ignore" });
|
|
8206
|
+
} catch {
|
|
8207
|
+
runtime = "docker";
|
|
7696
8208
|
try {
|
|
7697
|
-
|
|
7698
|
-
|
|
7699
|
-
|
|
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);
|
|
8209
|
+
execSync("docker --version", { stdio: "ignore" });
|
|
8210
|
+
} catch {
|
|
8211
|
+
throw new Error("Neither podman nor docker found. Install one to use --dev mode.");
|
|
7728
8212
|
}
|
|
7729
|
-
}
|
|
8213
|
+
}
|
|
8214
|
+
try {
|
|
8215
|
+
if (execSync(`${runtime} ps -a --filter name=${containerName} --format "{{.Names}}"`, { encoding: "utf-8" }).trim()) {
|
|
8216
|
+
console.log(`Dev PostgreSQL container "${containerName}" already exists.`);
|
|
8217
|
+
console.log(`Remove it with: ${runtime} rm -f ${containerName}`);
|
|
8218
|
+
throw new Error(`Container "${containerName}" already exists`);
|
|
8219
|
+
}
|
|
8220
|
+
} catch (err) {
|
|
8221
|
+
if (err instanceof Error && err.message.includes("already exists")) throw err;
|
|
8222
|
+
}
|
|
8223
|
+
console.log(`Starting dev PostgreSQL container "${containerName}" on port ${port}...`);
|
|
8224
|
+
execSync(`${runtime} run -d --name ${containerName} -p ${port}:5432 -e POSTGRES_PASSWORD=${password} -e POSTGRES_DB=kici postgres:18-trixie`, { stdio: "inherit" });
|
|
8225
|
+
return `postgresql://postgres:${password}@localhost:${port}/kici`;
|
|
7730
8226
|
}
|
|
7731
|
-
|
|
7732
|
-
|
|
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) => {
|
|
8227
|
+
function registerOrchestratorInstall(orchestrator) {
|
|
8228
|
+
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) => {
|
|
7735
8229
|
try {
|
|
7736
|
-
|
|
7737
|
-
|
|
7738
|
-
|
|
8230
|
+
if (opts.wizard && opts.envFile) {
|
|
8231
|
+
console.error("Error: Cannot use --wizard with --env-file");
|
|
8232
|
+
process.exit(1);
|
|
8233
|
+
}
|
|
8234
|
+
const platform = detectPlatform(opts.platform);
|
|
8235
|
+
const userLevel = resolveUserLevel(opts);
|
|
8236
|
+
const serviceName = opts.name;
|
|
8237
|
+
const instanceDir = path.resolve(opts.instanceDir ?? process.cwd());
|
|
8238
|
+
const kiciRoot = kiciConfigRoot(userLevel);
|
|
8239
|
+
console.log(`Platform: ${platform}`);
|
|
8240
|
+
console.log(`Privilege: ${userLevel ? "user" : "system"}`);
|
|
8241
|
+
console.log(`Service name: ${serviceName}`);
|
|
8242
|
+
console.log(`Instance dir: ${instanceDir}`);
|
|
8243
|
+
const manager = await createServiceManager(platform);
|
|
8244
|
+
const existing = (await listInstances({
|
|
8245
|
+
component: "orchestrator",
|
|
8246
|
+
isUserLevel: userLevel,
|
|
8247
|
+
kiciRoot,
|
|
8248
|
+
manager
|
|
8249
|
+
})).find((c) => c.name === serviceName);
|
|
8250
|
+
if (existing && existing.instanceDir !== instanceDir && !opts.force) {
|
|
8251
|
+
const at = existing.instanceDir ?? "(no manifest)";
|
|
8252
|
+
console.error(`Error: an orchestrator instance "${serviceName}" is already installed at ${at}. Pass a different --name, a different --instance-dir, or --force to overwrite.`);
|
|
8253
|
+
process.exit(1);
|
|
8254
|
+
}
|
|
8255
|
+
const configDir = getConfigDir(serviceName, userLevel);
|
|
8256
|
+
const logDir = getLogDir(serviceName, userLevel);
|
|
8257
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
8258
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
8259
|
+
const envFilePath = path.join(configDir, `${serviceName}.env`);
|
|
8260
|
+
let devDbUrl;
|
|
8261
|
+
if (opts.dev) {
|
|
8262
|
+
devDbUrl = startDevPostgres(`${serviceName}-dev-pg`);
|
|
8263
|
+
console.log(`Dev PostgreSQL URL: ${devDbUrl}`);
|
|
8264
|
+
}
|
|
8265
|
+
if (opts.wizard) {
|
|
8266
|
+
const { runOrchestratorWizard } = await Promise.resolve().then(() => (init_orchestrator_wizard(), orchestrator_wizard_exports));
|
|
8267
|
+
const wizardConfig = await runOrchestratorWizard();
|
|
8268
|
+
let envContent = "# KiCI orchestrator configuration (generated by setup wizard)\n";
|
|
8269
|
+
envContent += `KICI_MODE=${wizardConfig.mode}\n`;
|
|
8270
|
+
envContent += `KICI_DATABASE_URL=${wizardConfig.databaseUrl}\n`;
|
|
8271
|
+
envContent += `KICI_PORT=${wizardConfig.port}\n`;
|
|
8272
|
+
envContent += `KICI_SECRET_KEY=${wizardConfig.secretsKey}\n`;
|
|
8273
|
+
envContent += `KICI_BOOTSTRAP_ADMIN_TOKEN=${wizardConfig.bootstrapAdminToken}\n`;
|
|
8274
|
+
if (wizardConfig.platformUrl) envContent += `KICI_PLATFORM_URL=${wizardConfig.platformUrl}\n`;
|
|
8275
|
+
if (wizardConfig.platformToken) envContent += `KICI_PLATFORM_TOKEN=${wizardConfig.platformToken}\n`;
|
|
8276
|
+
fs.writeFileSync(envFilePath, envContent, "utf-8");
|
|
8277
|
+
console.log(`Wrote wizard configuration to ${envFilePath}`);
|
|
8278
|
+
} else if (opts.envFile) {
|
|
8279
|
+
const source = path.resolve(opts.envFile);
|
|
8280
|
+
if (!fs.existsSync(source)) {
|
|
8281
|
+
console.error(`Error: env file not found: ${source}`);
|
|
8282
|
+
process.exit(1);
|
|
8283
|
+
}
|
|
8284
|
+
fs.copyFileSync(source, envFilePath);
|
|
8285
|
+
console.log(`Copied env file to ${envFilePath}`);
|
|
8286
|
+
} else if (!fs.existsSync(envFilePath)) {
|
|
8287
|
+
let envContent = `# KiCI orchestrator configuration\n# See docs for all available options\n`;
|
|
8288
|
+
if (devDbUrl) envContent += `KICI_DATABASE_URL=${devDbUrl}\n`;
|
|
8289
|
+
fs.writeFileSync(envFilePath, envContent, "utf-8");
|
|
8290
|
+
console.log(`Created env file at ${envFilePath}`);
|
|
8291
|
+
} else if (devDbUrl) {
|
|
8292
|
+
fs.appendFileSync(envFilePath, `\nKICI_DATABASE_URL=${devDbUrl}\n`);
|
|
8293
|
+
console.log(`Appended KICI_DATABASE_URL to ${envFilePath}`);
|
|
8294
|
+
}
|
|
8295
|
+
const entryScript = opts.binary ? void 0 : fileURLToPath(import.meta.resolve(`@kici-dev/orchestrator/${selectServerEntry(fs.readFileSync(envFilePath, "utf-8"))}`));
|
|
8296
|
+
const { executablePath, args } = resolveServiceExecutable({
|
|
8297
|
+
binary: opts.binary ? path.resolve(opts.binary) : void 0,
|
|
8298
|
+
nodePath: process.execPath,
|
|
8299
|
+
entryScript
|
|
8300
|
+
});
|
|
8301
|
+
if (userLevel) try {
|
|
8302
|
+
const envContent = fs.readFileSync(envFilePath, "utf-8");
|
|
8303
|
+
if (envContent.includes("firecracker") || envContent.includes("FIRECRACKER")) {
|
|
8304
|
+
console.warn("\nWARNING: Firecracker scaler requires root privileges.");
|
|
8305
|
+
console.warn("The service is being installed at user level. Firecracker will not work.");
|
|
8306
|
+
console.warn("Re-run as root (sudo) to install a system-level service.\n");
|
|
8307
|
+
}
|
|
8308
|
+
} catch {}
|
|
7739
8309
|
const config = {
|
|
7740
|
-
name:
|
|
7741
|
-
displayName: "KiCI
|
|
7742
|
-
description: "KiCI orchestrator service",
|
|
7743
|
-
executablePath
|
|
7744
|
-
|
|
8310
|
+
name: serviceName,
|
|
8311
|
+
displayName: "KiCI Orchestrator",
|
|
8312
|
+
description: "KiCI CI/CD workflow orchestrator service",
|
|
8313
|
+
executablePath,
|
|
8314
|
+
args,
|
|
8315
|
+
nodeBinDir: path.dirname(process.execPath),
|
|
8316
|
+
envFilePath,
|
|
7745
8317
|
workingDirectory: configDir,
|
|
7746
8318
|
isUserLevel: userLevel,
|
|
7747
|
-
|
|
7748
|
-
|
|
7749
|
-
|
|
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
|
|
8319
|
+
user: opts.user,
|
|
8320
|
+
component: "orchestrator",
|
|
8321
|
+
restartPolicy: DEFAULT_RESTART_POLICY
|
|
7764
8322
|
};
|
|
7765
|
-
await manager.
|
|
8323
|
+
await manager.install(config);
|
|
8324
|
+
const manifestFile = writeManifest(instanceDir, {
|
|
8325
|
+
component: "orchestrator",
|
|
8326
|
+
name: serviceName,
|
|
8327
|
+
platform,
|
|
8328
|
+
isUserLevel: userLevel,
|
|
8329
|
+
envFilePath,
|
|
8330
|
+
configDir,
|
|
8331
|
+
logDir,
|
|
8332
|
+
installBase: getInstallBase(platform, serviceName),
|
|
8333
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8334
|
+
kiciVersion: process.env.npm_package_version ?? "unknown"
|
|
8335
|
+
});
|
|
8336
|
+
try {
|
|
8337
|
+
appendIndexEntry(kiciRoot, {
|
|
8338
|
+
component: "orchestrator",
|
|
8339
|
+
name: serviceName,
|
|
8340
|
+
platform,
|
|
8341
|
+
isUserLevel: userLevel,
|
|
8342
|
+
instanceDir
|
|
8343
|
+
});
|
|
8344
|
+
} catch (err) {
|
|
8345
|
+
console.warn(`Warning: instance index append failed: ${err.message}`);
|
|
8346
|
+
}
|
|
8347
|
+
console.log(`\nOrchestrator service "${serviceName}" installed successfully.`);
|
|
8348
|
+
console.log(` Config: ${envFilePath}`);
|
|
8349
|
+
console.log(` Logs: ${logDir}`);
|
|
8350
|
+
console.log(` Manifest: ${manifestFile}`);
|
|
8351
|
+
console.log(`\nNext steps:`);
|
|
8352
|
+
console.log(` 1. Edit ${envFilePath} with your configuration`);
|
|
8353
|
+
console.log(` 2. Run \`kici-admin orchestrator start\` to start the service`);
|
|
7766
8354
|
} catch (err) {
|
|
7767
8355
|
console.error(`Error: ${toErrorMessage(err)}`);
|
|
7768
8356
|
process.exit(1);
|
|
7769
8357
|
}
|
|
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");
|
|
8358
|
+
});
|
|
7883
8359
|
}
|
|
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) {
|
|
8360
|
+
//#endregion
|
|
8361
|
+
//#region src/cli/commands/orchestrator-service/uninstall.ts
|
|
8362
|
+
function registerOrchestratorUninstall(orchestrator) {
|
|
8363
|
+
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
8364
|
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
|
|
8365
|
+
const platform = detectPlatform(opts.platform);
|
|
8366
|
+
const userLevel = resolveUserLevel(opts);
|
|
8367
|
+
const manager = await createServiceManager(platform);
|
|
8368
|
+
const kiciRoot = kiciConfigRoot(userLevel);
|
|
8369
|
+
const resolved = await resolveInstance({
|
|
8370
|
+
component: "orchestrator",
|
|
8371
|
+
opts: {
|
|
8372
|
+
instanceDir: opts.instanceDir,
|
|
8373
|
+
name: opts.name
|
|
8374
|
+
},
|
|
8375
|
+
cwd: process.cwd(),
|
|
8376
|
+
kiciRoot,
|
|
8377
|
+
manager,
|
|
8378
|
+
isUserLevel: userLevel
|
|
8379
|
+
});
|
|
8380
|
+
const config = {
|
|
8381
|
+
name: resolved.manifest.name,
|
|
8382
|
+
displayName: "KiCI Orchestrator",
|
|
8383
|
+
description: "KiCI CI/CD workflow orchestrator service",
|
|
8384
|
+
executablePath: "",
|
|
8385
|
+
envFilePath: resolved.manifest.envFilePath,
|
|
8386
|
+
workingDirectory: resolved.manifest.configDir,
|
|
8387
|
+
isUserLevel: resolved.manifest.isUserLevel,
|
|
8388
|
+
restartPolicy: DEFAULT_RESTART_POLICY,
|
|
8389
|
+
component: "orchestrator"
|
|
8390
|
+
};
|
|
8391
|
+
if (!await manager.isInstalled(config)) console.log(`Service "${config.name}" is not installed.`);
|
|
8392
|
+
else {
|
|
8393
|
+
try {
|
|
8394
|
+
if ((await manager.status(config)).state === "running") {
|
|
8395
|
+
console.log(`Stopping service "${config.name}"...`);
|
|
8396
|
+
await manager.stop(config);
|
|
8397
|
+
}
|
|
8398
|
+
} catch {}
|
|
8399
|
+
await manager.uninstall(config);
|
|
7938
8400
|
}
|
|
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");
|
|
8401
|
+
removeIndexEntry(kiciRoot, {
|
|
8402
|
+
component: "orchestrator",
|
|
8403
|
+
name: config.name
|
|
8404
|
+
});
|
|
8405
|
+
console.log(`\nOrchestrator service "${config.name}" uninstalled.`);
|
|
8406
|
+
console.log(`Manifest preserved at ${resolved.manifestPath} — delete manually if no longer needed.`);
|
|
8407
|
+
} catch (err) {
|
|
8408
|
+
console.error(`Error: ${toErrorMessage(err)}`);
|
|
7958
8409
|
process.exit(1);
|
|
7959
8410
|
}
|
|
7960
|
-
|
|
7961
|
-
|
|
7962
|
-
|
|
7963
|
-
|
|
7964
|
-
|
|
7965
|
-
|
|
7966
|
-
|
|
7967
|
-
|
|
8411
|
+
});
|
|
8412
|
+
}
|
|
8413
|
+
//#endregion
|
|
8414
|
+
//#region src/cli/commands/orchestrator-service/start.ts
|
|
8415
|
+
function registerOrchestratorStart(orchestrator) {
|
|
8416
|
+
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) => {
|
|
8417
|
+
try {
|
|
8418
|
+
const platform = detectPlatform(opts.platform);
|
|
8419
|
+
const userLevel = resolveUserLevel(opts);
|
|
8420
|
+
const manager = await createServiceManager(platform);
|
|
8421
|
+
const kiciRoot = kiciConfigRoot(userLevel);
|
|
8422
|
+
const resolved = await resolveInstance({
|
|
8423
|
+
component: "orchestrator",
|
|
8424
|
+
opts: {
|
|
8425
|
+
instanceDir: opts.instanceDir,
|
|
8426
|
+
name: opts.name
|
|
8427
|
+
},
|
|
8428
|
+
cwd: process.cwd(),
|
|
8429
|
+
kiciRoot,
|
|
8430
|
+
manager,
|
|
8431
|
+
isUserLevel: userLevel
|
|
7968
8432
|
});
|
|
7969
|
-
|
|
7970
|
-
|
|
7971
|
-
|
|
8433
|
+
const config = {
|
|
8434
|
+
name: resolved.manifest.name,
|
|
8435
|
+
displayName: "KiCI Orchestrator",
|
|
8436
|
+
description: "KiCI CI/CD workflow orchestrator service",
|
|
8437
|
+
executablePath: "",
|
|
8438
|
+
envFilePath: resolved.manifest.envFilePath,
|
|
8439
|
+
workingDirectory: resolved.manifest.configDir,
|
|
8440
|
+
isUserLevel: resolved.manifest.isUserLevel,
|
|
8441
|
+
restartPolicy: DEFAULT_RESTART_POLICY,
|
|
8442
|
+
component: "orchestrator"
|
|
8443
|
+
};
|
|
8444
|
+
if (!await manager.isInstalled(config)) {
|
|
8445
|
+
console.error(`Error: service "${config.name}" is not installed.`);
|
|
8446
|
+
console.error(`Run \`kici-admin orchestrator install\` first.`);
|
|
8447
|
+
process.exit(1);
|
|
8448
|
+
}
|
|
8449
|
+
await manager.start(config);
|
|
8450
|
+
console.log(`Orchestrator service "${config.name}" started.`);
|
|
8451
|
+
} catch (err) {
|
|
8452
|
+
console.error(`Error: ${toErrorMessage(err)}`);
|
|
7972
8453
|
process.exit(1);
|
|
7973
8454
|
}
|
|
7974
|
-
|
|
7975
|
-
|
|
8455
|
+
});
|
|
8456
|
+
}
|
|
8457
|
+
//#endregion
|
|
8458
|
+
//#region src/cli/commands/orchestrator-service/stop.ts
|
|
8459
|
+
function registerOrchestratorStop(orchestrator) {
|
|
8460
|
+
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) => {
|
|
8461
|
+
try {
|
|
8462
|
+
const platform = detectPlatform(opts.platform);
|
|
8463
|
+
const userLevel = resolveUserLevel(opts);
|
|
8464
|
+
const manager = await createServiceManager(platform);
|
|
8465
|
+
const kiciRoot = kiciConfigRoot(userLevel);
|
|
8466
|
+
const resolved = await resolveInstance({
|
|
8467
|
+
component: "orchestrator",
|
|
8468
|
+
opts: {
|
|
8469
|
+
instanceDir: opts.instanceDir,
|
|
8470
|
+
name: opts.name
|
|
8471
|
+
},
|
|
8472
|
+
cwd: process.cwd(),
|
|
8473
|
+
kiciRoot,
|
|
8474
|
+
manager,
|
|
8475
|
+
isUserLevel: userLevel
|
|
8476
|
+
});
|
|
8477
|
+
const config = {
|
|
8478
|
+
name: resolved.manifest.name,
|
|
8479
|
+
displayName: "KiCI Orchestrator",
|
|
8480
|
+
description: "KiCI CI/CD workflow orchestrator service",
|
|
8481
|
+
executablePath: "",
|
|
8482
|
+
envFilePath: resolved.manifest.envFilePath,
|
|
8483
|
+
workingDirectory: resolved.manifest.configDir,
|
|
8484
|
+
isUserLevel: resolved.manifest.isUserLevel,
|
|
8485
|
+
restartPolicy: DEFAULT_RESTART_POLICY,
|
|
8486
|
+
component: "orchestrator"
|
|
8487
|
+
};
|
|
8488
|
+
await manager.stop(config);
|
|
8489
|
+
console.log(`Orchestrator service "${config.name}" stopped.`);
|
|
8490
|
+
} catch (err) {
|
|
8491
|
+
console.error(`Error: ${toErrorMessage(err)}`);
|
|
7976
8492
|
process.exit(1);
|
|
7977
8493
|
}
|
|
7978
|
-
|
|
7979
|
-
|
|
7980
|
-
|
|
7981
|
-
|
|
7982
|
-
|
|
7983
|
-
|
|
7984
|
-
|
|
8494
|
+
});
|
|
8495
|
+
}
|
|
8496
|
+
//#endregion
|
|
8497
|
+
//#region src/cli/commands/orchestrator-service/restart.ts
|
|
8498
|
+
function registerOrchestratorRestart(orchestrator) {
|
|
8499
|
+
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) => {
|
|
8500
|
+
try {
|
|
8501
|
+
const platform = detectPlatform(opts.platform);
|
|
8502
|
+
const userLevel = resolveUserLevel(opts);
|
|
8503
|
+
const manager = await createServiceManager(platform);
|
|
8504
|
+
const kiciRoot = kiciConfigRoot(userLevel);
|
|
8505
|
+
const resolved = await resolveInstance({
|
|
8506
|
+
component: "orchestrator",
|
|
8507
|
+
opts: {
|
|
8508
|
+
instanceDir: opts.instanceDir,
|
|
8509
|
+
name: opts.name
|
|
8510
|
+
},
|
|
8511
|
+
cwd: process.cwd(),
|
|
8512
|
+
kiciRoot,
|
|
8513
|
+
manager,
|
|
8514
|
+
isUserLevel: userLevel
|
|
8515
|
+
});
|
|
8516
|
+
const config = {
|
|
8517
|
+
name: resolved.manifest.name,
|
|
8518
|
+
displayName: "KiCI Orchestrator",
|
|
8519
|
+
description: "KiCI CI/CD workflow orchestrator service",
|
|
8520
|
+
executablePath: "",
|
|
8521
|
+
envFilePath: resolved.manifest.envFilePath,
|
|
8522
|
+
workingDirectory: resolved.manifest.configDir,
|
|
8523
|
+
isUserLevel: resolved.manifest.isUserLevel,
|
|
8524
|
+
restartPolicy: DEFAULT_RESTART_POLICY,
|
|
8525
|
+
component: "orchestrator"
|
|
8526
|
+
};
|
|
8527
|
+
if (!await manager.isInstalled(config)) {
|
|
8528
|
+
console.error(`Error: service "${config.name}" is not installed.`);
|
|
8529
|
+
console.error(`Run \`kici-admin orchestrator install\` first.`);
|
|
7985
8530
|
process.exit(1);
|
|
7986
8531
|
}
|
|
7987
|
-
|
|
7988
|
-
|
|
7989
|
-
|
|
7990
|
-
|
|
8532
|
+
await manager.restart(config);
|
|
8533
|
+
console.log(`Orchestrator service "${config.name}" restarted.`);
|
|
8534
|
+
} catch (err) {
|
|
8535
|
+
console.error(`Error: ${toErrorMessage(err)}`);
|
|
8536
|
+
process.exit(1);
|
|
7991
8537
|
}
|
|
7992
|
-
|
|
7993
|
-
|
|
7994
|
-
|
|
7995
|
-
|
|
7996
|
-
|
|
7997
|
-
|
|
7998
|
-
|
|
7999
|
-
|
|
8000
|
-
|
|
8001
|
-
|
|
8002
|
-
|
|
8538
|
+
});
|
|
8539
|
+
}
|
|
8540
|
+
//#endregion
|
|
8541
|
+
//#region src/cli/commands/orchestrator-service/status.ts
|
|
8542
|
+
/** Read the port from the service's env file (path from the manifest). */
|
|
8543
|
+
function readPortFromEnvFile$1(envFilePath) {
|
|
8544
|
+
if (!fs.existsSync(envFilePath)) return 4e3;
|
|
8545
|
+
try {
|
|
8546
|
+
const content = fs.readFileSync(envFilePath, "utf-8");
|
|
8547
|
+
for (const line of content.split("\n")) {
|
|
8548
|
+
const trimmed = line.trim();
|
|
8549
|
+
if (trimmed.startsWith("#") || !trimmed.includes("=")) continue;
|
|
8550
|
+
const [key, ...rest] = trimmed.split("=");
|
|
8551
|
+
if (key?.trim() === "KICI_PORT") {
|
|
8552
|
+
const val = rest.join("=").trim().replace(/^["']|["']$/g, "");
|
|
8553
|
+
const parsed = parseInt(val, 10);
|
|
8554
|
+
if (!isNaN(parsed)) return parsed;
|
|
8003
8555
|
}
|
|
8004
8556
|
}
|
|
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);
|
|
8557
|
+
} catch {}
|
|
8558
|
+
return 4e3;
|
|
8559
|
+
}
|
|
8560
|
+
/** Query orchestrator health API. */
|
|
8561
|
+
async function queryHealth$1(port) {
|
|
8562
|
+
try {
|
|
8563
|
+
const controller = new AbortController();
|
|
8564
|
+
const timeout = setTimeout(() => controller.abort(), 3e3);
|
|
8565
|
+
const res = await fetch(`http://localhost:${port}/health`, { signal: controller.signal });
|
|
8566
|
+
clearTimeout(timeout);
|
|
8567
|
+
if (!res.ok) return null;
|
|
8568
|
+
return await res.json();
|
|
8569
|
+
} catch {
|
|
8570
|
+
return null;
|
|
8049
8571
|
}
|
|
8050
8572
|
}
|
|
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;
|
|
8573
|
+
/** Format the status output as a readable table. */
|
|
8574
|
+
function formatStatus$1(serviceStatus, health, serviceName) {
|
|
8575
|
+
const lines = [];
|
|
8576
|
+
lines.push(`Service: ${serviceName}`);
|
|
8577
|
+
lines.push(`State: ${serviceStatus.state}`);
|
|
8578
|
+
if (serviceStatus.pid) lines.push(`PID: ${serviceStatus.pid}`);
|
|
8579
|
+
if (serviceStatus.uptime != null) lines.push(`Uptime: ${formatUptime(serviceStatus.uptime)}`);
|
|
8580
|
+
if (serviceStatus.startedAt) lines.push(`Started: ${serviceStatus.startedAt}`);
|
|
8581
|
+
if (health) {
|
|
8582
|
+
lines.push("");
|
|
8583
|
+
lines.push("--- KiCI orchestrator ---");
|
|
8584
|
+
if (health.mode) lines.push(`Mode: ${health.mode}`);
|
|
8585
|
+
if (health.port) lines.push(`Port: ${health.port}`);
|
|
8586
|
+
if (health.database) lines.push(`Database: ${health.database}`);
|
|
8587
|
+
if (health.platformRelay) lines.push(`Platform relay: ${health.platformRelay}`);
|
|
8588
|
+
if (health.agents != null) lines.push(`Agents: ${health.agents}`);
|
|
8589
|
+
if (health.scaler) {
|
|
8590
|
+
const s = health.scaler;
|
|
8591
|
+
lines.push(`Scaler: ${s.type ?? "none"} (warm: ${s.warm ?? 0}, max: ${s.max ?? 0})`);
|
|
8085
8592
|
}
|
|
8593
|
+
if (health.jobs) lines.push(`Jobs: ${health.jobs.pending ?? 0} pending, ${health.jobs.running ?? 0} running`);
|
|
8594
|
+
} else if (serviceStatus.state === "running") {
|
|
8595
|
+
lines.push("");
|
|
8596
|
+
lines.push("(Could not reach health API)");
|
|
8086
8597
|
}
|
|
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}`);
|
|
8598
|
+
return lines.join("\n");
|
|
8108
8599
|
}
|
|
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
|
-
|
|
8600
|
+
/** Build JSON output combining service + health data. */
|
|
8601
|
+
function buildJsonOutput$1(serviceStatus, health, serviceName) {
|
|
8602
|
+
return {
|
|
8603
|
+
service: serviceName,
|
|
8604
|
+
...serviceStatus,
|
|
8605
|
+
health: health ?? void 0
|
|
8606
|
+
};
|
|
8607
|
+
}
|
|
8608
|
+
function registerStatusCommand(parent) {
|
|
8609
|
+
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) => {
|
|
8610
|
+
try {
|
|
8611
|
+
const platform = detectPlatform(opts.platform);
|
|
8612
|
+
const userLevel = resolveUserLevel(opts);
|
|
8613
|
+
const manager = await createServiceManager(platform);
|
|
8614
|
+
const kiciRoot = kiciConfigRoot(userLevel);
|
|
8615
|
+
const resolved = await resolveInstance({
|
|
8616
|
+
component: "orchestrator",
|
|
8617
|
+
opts: {
|
|
8618
|
+
instanceDir: opts.instanceDir,
|
|
8619
|
+
name: opts.name
|
|
8620
|
+
},
|
|
8621
|
+
cwd: process.cwd(),
|
|
8622
|
+
kiciRoot,
|
|
8623
|
+
manager,
|
|
8624
|
+
isUserLevel: userLevel
|
|
8625
|
+
});
|
|
8626
|
+
const config = {
|
|
8627
|
+
name: resolved.manifest.name,
|
|
8628
|
+
displayName: "KiCI Orchestrator",
|
|
8629
|
+
description: "KiCI CI/CD workflow orchestrator service",
|
|
8630
|
+
executablePath: "",
|
|
8631
|
+
envFilePath: resolved.manifest.envFilePath,
|
|
8632
|
+
workingDirectory: resolved.manifest.configDir,
|
|
8633
|
+
isUserLevel: resolved.manifest.isUserLevel,
|
|
8634
|
+
restartPolicy: DEFAULT_RESTART_POLICY,
|
|
8635
|
+
component: "orchestrator"
|
|
8636
|
+
};
|
|
8637
|
+
const serviceStatus = await manager.status(config);
|
|
8638
|
+
let health = null;
|
|
8639
|
+
if (serviceStatus.state === "running") health = await queryHealth$1(readPortFromEnvFile$1(config.envFilePath));
|
|
8640
|
+
if (opts.json) console.log(JSON.stringify(buildJsonOutput$1(serviceStatus, health, config.name), null, 2));
|
|
8641
|
+
else console.log(formatStatus$1(serviceStatus, health, config.name));
|
|
8642
|
+
} catch (err) {
|
|
8643
|
+
console.error(`Error: ${toErrorMessage(err)}`);
|
|
8644
|
+
process.exit(1);
|
|
8645
|
+
}
|
|
8646
|
+
});
|
|
8647
|
+
}
|
|
8648
|
+
//#endregion
|
|
8649
|
+
//#region src/cli/commands/orchestrator-service/logs.ts
|
|
8650
|
+
function registerLogsCommand(parent) {
|
|
8651
|
+
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) => {
|
|
8652
|
+
try {
|
|
8653
|
+
const platform = detectPlatform(opts.platform);
|
|
8654
|
+
const userLevel = resolveUserLevel(opts);
|
|
8655
|
+
const manager = await createServiceManager(platform);
|
|
8656
|
+
const kiciRoot = kiciConfigRoot(userLevel);
|
|
8657
|
+
const resolved = await resolveInstance({
|
|
8658
|
+
component: "orchestrator",
|
|
8659
|
+
opts: {
|
|
8660
|
+
instanceDir: opts.instanceDir,
|
|
8661
|
+
name: opts.name
|
|
8662
|
+
},
|
|
8663
|
+
cwd: process.cwd(),
|
|
8664
|
+
kiciRoot,
|
|
8665
|
+
manager,
|
|
8666
|
+
isUserLevel: userLevel
|
|
8667
|
+
});
|
|
8668
|
+
const config = {
|
|
8669
|
+
name: resolved.manifest.name,
|
|
8670
|
+
displayName: "KiCI Orchestrator",
|
|
8671
|
+
description: "KiCI CI/CD workflow orchestrator service",
|
|
8672
|
+
executablePath: "",
|
|
8673
|
+
envFilePath: resolved.manifest.envFilePath,
|
|
8674
|
+
workingDirectory: resolved.manifest.configDir,
|
|
8675
|
+
isUserLevel: resolved.manifest.isUserLevel,
|
|
8676
|
+
restartPolicy: DEFAULT_RESTART_POLICY,
|
|
8677
|
+
component: "orchestrator"
|
|
8678
|
+
};
|
|
8679
|
+
const logOptions = {
|
|
8680
|
+
since: opts.since,
|
|
8681
|
+
level: opts.level,
|
|
8682
|
+
json: opts.json,
|
|
8683
|
+
follow: opts.follow
|
|
8684
|
+
};
|
|
8685
|
+
await manager.logs(config, logOptions);
|
|
8686
|
+
} catch (err) {
|
|
8687
|
+
console.error(`Error: ${toErrorMessage(err)}`);
|
|
8688
|
+
process.exit(1);
|
|
8689
|
+
}
|
|
8690
|
+
});
|
|
8140
8691
|
}
|
|
8141
8692
|
//#endregion
|
|
8142
8693
|
//#region src/cli/commands/orchestrator-service/upgrade.ts
|
|
8143
8694
|
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
|
|
8695
|
+
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
8696
|
await performVersionedUpgrade("orchestrator", opts);
|
|
8146
8697
|
});
|
|
8147
8698
|
}
|
|
@@ -8199,18 +8750,33 @@ var init_agent_wizard = __esmMin((() => {
|
|
|
8199
8750
|
//#endregion
|
|
8200
8751
|
//#region src/cli/commands/agent-service/install.ts
|
|
8201
8752
|
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) => {
|
|
8753
|
+
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
8754
|
try {
|
|
8204
8755
|
if (opts.wizard && opts.envFile) {
|
|
8205
8756
|
console.error("Error: Cannot use --wizard with --env-file");
|
|
8206
8757
|
process.exit(1);
|
|
8207
8758
|
}
|
|
8208
8759
|
const platform = detectPlatform(opts.platform);
|
|
8209
|
-
const userLevel =
|
|
8760
|
+
const userLevel = resolveUserLevel(opts);
|
|
8210
8761
|
const serviceName = opts.name;
|
|
8762
|
+
const instanceDir = path.resolve(opts.instanceDir ?? process.cwd());
|
|
8763
|
+
const kiciRoot = kiciConfigRoot(userLevel);
|
|
8211
8764
|
console.log(`Platform: ${platform}`);
|
|
8212
8765
|
console.log(`Privilege: ${userLevel ? "user" : "system"}`);
|
|
8213
8766
|
console.log(`Service name: ${serviceName}`);
|
|
8767
|
+
console.log(`Instance dir: ${instanceDir}`);
|
|
8768
|
+
const manager = await createServiceManager(platform);
|
|
8769
|
+
const existing = (await listInstances({
|
|
8770
|
+
component: "agent",
|
|
8771
|
+
isUserLevel: userLevel,
|
|
8772
|
+
kiciRoot,
|
|
8773
|
+
manager
|
|
8774
|
+
})).find((c) => c.name === serviceName);
|
|
8775
|
+
if (existing && existing.instanceDir !== instanceDir && !opts.force) {
|
|
8776
|
+
const at = existing.instanceDir ?? "(no manifest)";
|
|
8777
|
+
console.error(`Error: an agent instance "${serviceName}" is already installed at ${at}. Pass a different --name, a different --instance-dir, or --force to overwrite.`);
|
|
8778
|
+
process.exit(1);
|
|
8779
|
+
}
|
|
8214
8780
|
const configDir = getConfigDir(serviceName, userLevel);
|
|
8215
8781
|
const logDir = getLogDir(serviceName, userLevel);
|
|
8216
8782
|
fs.mkdirSync(configDir, { recursive: true });
|
|
@@ -8257,12 +8823,37 @@ function registerAgentInstall(agent) {
|
|
|
8257
8823
|
envFilePath,
|
|
8258
8824
|
workingDirectory: configDir,
|
|
8259
8825
|
isUserLevel: userLevel,
|
|
8826
|
+
component: "agent",
|
|
8260
8827
|
restartPolicy: DEFAULT_RESTART_POLICY
|
|
8261
8828
|
};
|
|
8262
|
-
await
|
|
8829
|
+
await manager.install(config);
|
|
8830
|
+
const manifestFile = writeManifest(instanceDir, {
|
|
8831
|
+
component: "agent",
|
|
8832
|
+
name: serviceName,
|
|
8833
|
+
platform,
|
|
8834
|
+
isUserLevel: userLevel,
|
|
8835
|
+
envFilePath,
|
|
8836
|
+
configDir,
|
|
8837
|
+
logDir,
|
|
8838
|
+
installBase: getInstallBase(platform, serviceName),
|
|
8839
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8840
|
+
kiciVersion: process.env.npm_package_version ?? "unknown"
|
|
8841
|
+
});
|
|
8842
|
+
try {
|
|
8843
|
+
appendIndexEntry(kiciRoot, {
|
|
8844
|
+
component: "agent",
|
|
8845
|
+
name: serviceName,
|
|
8846
|
+
platform,
|
|
8847
|
+
isUserLevel: userLevel,
|
|
8848
|
+
instanceDir
|
|
8849
|
+
});
|
|
8850
|
+
} catch (err) {
|
|
8851
|
+
console.warn(`Warning: instance index append failed: ${err.message}`);
|
|
8852
|
+
}
|
|
8263
8853
|
console.log(`\nAgent service "${serviceName}" installed successfully.`);
|
|
8264
|
-
console.log(` Config:
|
|
8265
|
-
console.log(` Logs:
|
|
8854
|
+
console.log(` Config: ${envFilePath}`);
|
|
8855
|
+
console.log(` Logs: ${logDir}`);
|
|
8856
|
+
console.log(` Manifest: ${manifestFile}`);
|
|
8266
8857
|
console.log(`\nNext steps:`);
|
|
8267
8858
|
console.log(` 1. Edit ${envFilePath} with your configuration`);
|
|
8268
8859
|
console.log(` 2. Run \`kici-admin agent start\` to start the service`);
|
|
@@ -8275,39 +8866,50 @@ function registerAgentInstall(agent) {
|
|
|
8275
8866
|
//#endregion
|
|
8276
8867
|
//#region src/cli/commands/agent-service/uninstall.ts
|
|
8277
8868
|
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", "
|
|
8869
|
+
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
8870
|
try {
|
|
8280
8871
|
const platform = detectPlatform(opts.platform);
|
|
8281
|
-
const userLevel =
|
|
8282
|
-
const
|
|
8283
|
-
const
|
|
8284
|
-
const
|
|
8872
|
+
const userLevel = resolveUserLevel(opts);
|
|
8873
|
+
const manager = await createServiceManager(platform);
|
|
8874
|
+
const kiciRoot = kiciConfigRoot(userLevel);
|
|
8875
|
+
const resolved = await resolveInstance({
|
|
8876
|
+
component: "agent",
|
|
8877
|
+
opts: {
|
|
8878
|
+
instanceDir: opts.instanceDir,
|
|
8879
|
+
name: opts.name
|
|
8880
|
+
},
|
|
8881
|
+
cwd: process.cwd(),
|
|
8882
|
+
kiciRoot,
|
|
8883
|
+
manager,
|
|
8884
|
+
isUserLevel: userLevel
|
|
8885
|
+
});
|
|
8285
8886
|
const config = {
|
|
8286
|
-
name:
|
|
8887
|
+
name: resolved.manifest.name,
|
|
8287
8888
|
displayName: "KiCI Agent",
|
|
8288
8889
|
description: "KiCI CI/CD workflow execution agent service",
|
|
8289
8890
|
executablePath: "",
|
|
8290
|
-
envFilePath:
|
|
8291
|
-
workingDirectory: configDir,
|
|
8292
|
-
isUserLevel:
|
|
8293
|
-
restartPolicy: DEFAULT_RESTART_POLICY
|
|
8891
|
+
envFilePath: resolved.manifest.envFilePath,
|
|
8892
|
+
workingDirectory: resolved.manifest.configDir,
|
|
8893
|
+
isUserLevel: resolved.manifest.isUserLevel,
|
|
8894
|
+
restartPolicy: DEFAULT_RESTART_POLICY,
|
|
8895
|
+
component: "agent"
|
|
8294
8896
|
};
|
|
8295
|
-
|
|
8296
|
-
|
|
8297
|
-
|
|
8298
|
-
|
|
8897
|
+
if (!await manager.isInstalled(config)) console.log(`Service "${config.name}" is not installed.`);
|
|
8898
|
+
else {
|
|
8899
|
+
try {
|
|
8900
|
+
if ((await manager.status(config)).state === "running") {
|
|
8901
|
+
console.log(`Stopping service "${config.name}"...`);
|
|
8902
|
+
await manager.stop(config);
|
|
8903
|
+
}
|
|
8904
|
+
} catch {}
|
|
8905
|
+
await manager.uninstall(config);
|
|
8299
8906
|
}
|
|
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}`);
|
|
8907
|
+
removeIndexEntry(kiciRoot, {
|
|
8908
|
+
component: "agent",
|
|
8909
|
+
name: config.name
|
|
8910
|
+
});
|
|
8911
|
+
console.log(`\nAgent service "${config.name}" uninstalled.`);
|
|
8912
|
+
console.log(`Manifest preserved at ${resolved.manifestPath} — delete manually if no longer needed.`);
|
|
8311
8913
|
} catch (err) {
|
|
8312
8914
|
console.error(`Error: ${toErrorMessage(err)}`);
|
|
8313
8915
|
process.exit(1);
|
|
@@ -8317,30 +8919,41 @@ function registerAgentUninstall(agent) {
|
|
|
8317
8919
|
//#endregion
|
|
8318
8920
|
//#region src/cli/commands/agent-service/start.ts
|
|
8319
8921
|
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", "
|
|
8922
|
+
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
8923
|
try {
|
|
8322
8924
|
const platform = detectPlatform(opts.platform);
|
|
8323
|
-
const userLevel =
|
|
8324
|
-
const
|
|
8325
|
-
const
|
|
8925
|
+
const userLevel = resolveUserLevel(opts);
|
|
8926
|
+
const manager = await createServiceManager(platform);
|
|
8927
|
+
const kiciRoot = kiciConfigRoot(userLevel);
|
|
8928
|
+
const resolved = await resolveInstance({
|
|
8929
|
+
component: "agent",
|
|
8930
|
+
opts: {
|
|
8931
|
+
instanceDir: opts.instanceDir,
|
|
8932
|
+
name: opts.name
|
|
8933
|
+
},
|
|
8934
|
+
cwd: process.cwd(),
|
|
8935
|
+
kiciRoot,
|
|
8936
|
+
manager,
|
|
8937
|
+
isUserLevel: userLevel
|
|
8938
|
+
});
|
|
8326
8939
|
const config = {
|
|
8327
|
-
name:
|
|
8940
|
+
name: resolved.manifest.name,
|
|
8328
8941
|
displayName: "KiCI Agent",
|
|
8329
8942
|
description: "KiCI CI/CD workflow execution agent service",
|
|
8330
8943
|
executablePath: "",
|
|
8331
|
-
envFilePath:
|
|
8332
|
-
workingDirectory: configDir,
|
|
8333
|
-
isUserLevel:
|
|
8334
|
-
restartPolicy: DEFAULT_RESTART_POLICY
|
|
8944
|
+
envFilePath: resolved.manifest.envFilePath,
|
|
8945
|
+
workingDirectory: resolved.manifest.configDir,
|
|
8946
|
+
isUserLevel: resolved.manifest.isUserLevel,
|
|
8947
|
+
restartPolicy: DEFAULT_RESTART_POLICY,
|
|
8948
|
+
component: "agent"
|
|
8335
8949
|
};
|
|
8336
|
-
const manager = await createServiceManager(platform);
|
|
8337
8950
|
if (!await manager.isInstalled(config)) {
|
|
8338
|
-
console.error(`Error: service "${
|
|
8951
|
+
console.error(`Error: service "${config.name}" is not installed.`);
|
|
8339
8952
|
console.error(`Run \`kici-admin agent install\` first.`);
|
|
8340
8953
|
process.exit(1);
|
|
8341
8954
|
}
|
|
8342
8955
|
await manager.start(config);
|
|
8343
|
-
console.log(`Agent service "${
|
|
8956
|
+
console.log(`Agent service "${config.name}" started.`);
|
|
8344
8957
|
} catch (err) {
|
|
8345
8958
|
console.error(`Error: ${toErrorMessage(err)}`);
|
|
8346
8959
|
process.exit(1);
|
|
@@ -8350,24 +8963,36 @@ function registerAgentStart(agent) {
|
|
|
8350
8963
|
//#endregion
|
|
8351
8964
|
//#region src/cli/commands/agent-service/stop.ts
|
|
8352
8965
|
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", "
|
|
8966
|
+
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
8967
|
try {
|
|
8355
8968
|
const platform = detectPlatform(opts.platform);
|
|
8356
|
-
const userLevel =
|
|
8357
|
-
const
|
|
8358
|
-
const
|
|
8969
|
+
const userLevel = resolveUserLevel(opts);
|
|
8970
|
+
const manager = await createServiceManager(platform);
|
|
8971
|
+
const kiciRoot = kiciConfigRoot(userLevel);
|
|
8972
|
+
const resolved = await resolveInstance({
|
|
8973
|
+
component: "agent",
|
|
8974
|
+
opts: {
|
|
8975
|
+
instanceDir: opts.instanceDir,
|
|
8976
|
+
name: opts.name
|
|
8977
|
+
},
|
|
8978
|
+
cwd: process.cwd(),
|
|
8979
|
+
kiciRoot,
|
|
8980
|
+
manager,
|
|
8981
|
+
isUserLevel: userLevel
|
|
8982
|
+
});
|
|
8359
8983
|
const config = {
|
|
8360
|
-
name:
|
|
8984
|
+
name: resolved.manifest.name,
|
|
8361
8985
|
displayName: "KiCI Agent",
|
|
8362
8986
|
description: "KiCI CI/CD workflow execution agent service",
|
|
8363
8987
|
executablePath: "",
|
|
8364
|
-
envFilePath:
|
|
8365
|
-
workingDirectory: configDir,
|
|
8366
|
-
isUserLevel:
|
|
8367
|
-
restartPolicy: DEFAULT_RESTART_POLICY
|
|
8988
|
+
envFilePath: resolved.manifest.envFilePath,
|
|
8989
|
+
workingDirectory: resolved.manifest.configDir,
|
|
8990
|
+
isUserLevel: resolved.manifest.isUserLevel,
|
|
8991
|
+
restartPolicy: DEFAULT_RESTART_POLICY,
|
|
8992
|
+
component: "agent"
|
|
8368
8993
|
};
|
|
8369
|
-
await
|
|
8370
|
-
console.log(`Agent service "${
|
|
8994
|
+
await manager.stop(config);
|
|
8995
|
+
console.log(`Agent service "${config.name}" stopped.`);
|
|
8371
8996
|
} catch (err) {
|
|
8372
8997
|
console.error(`Error: ${toErrorMessage(err)}`);
|
|
8373
8998
|
process.exit(1);
|
|
@@ -8377,30 +9002,41 @@ function registerAgentStop(agent) {
|
|
|
8377
9002
|
//#endregion
|
|
8378
9003
|
//#region src/cli/commands/agent-service/restart.ts
|
|
8379
9004
|
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", "
|
|
9005
|
+
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
9006
|
try {
|
|
8382
9007
|
const platform = detectPlatform(opts.platform);
|
|
8383
|
-
const userLevel =
|
|
8384
|
-
const
|
|
8385
|
-
const
|
|
9008
|
+
const userLevel = resolveUserLevel(opts);
|
|
9009
|
+
const manager = await createServiceManager(platform);
|
|
9010
|
+
const kiciRoot = kiciConfigRoot(userLevel);
|
|
9011
|
+
const resolved = await resolveInstance({
|
|
9012
|
+
component: "agent",
|
|
9013
|
+
opts: {
|
|
9014
|
+
instanceDir: opts.instanceDir,
|
|
9015
|
+
name: opts.name
|
|
9016
|
+
},
|
|
9017
|
+
cwd: process.cwd(),
|
|
9018
|
+
kiciRoot,
|
|
9019
|
+
manager,
|
|
9020
|
+
isUserLevel: userLevel
|
|
9021
|
+
});
|
|
8386
9022
|
const config = {
|
|
8387
|
-
name:
|
|
9023
|
+
name: resolved.manifest.name,
|
|
8388
9024
|
displayName: "KiCI Agent",
|
|
8389
9025
|
description: "KiCI CI/CD workflow execution agent service",
|
|
8390
9026
|
executablePath: "",
|
|
8391
|
-
envFilePath:
|
|
8392
|
-
workingDirectory: configDir,
|
|
8393
|
-
isUserLevel:
|
|
8394
|
-
restartPolicy: DEFAULT_RESTART_POLICY
|
|
9027
|
+
envFilePath: resolved.manifest.envFilePath,
|
|
9028
|
+
workingDirectory: resolved.manifest.configDir,
|
|
9029
|
+
isUserLevel: resolved.manifest.isUserLevel,
|
|
9030
|
+
restartPolicy: DEFAULT_RESTART_POLICY,
|
|
9031
|
+
component: "agent"
|
|
8395
9032
|
};
|
|
8396
|
-
const manager = await createServiceManager(platform);
|
|
8397
9033
|
if (!await manager.isInstalled(config)) {
|
|
8398
|
-
console.error(`Error: service "${
|
|
9034
|
+
console.error(`Error: service "${config.name}" is not installed.`);
|
|
8399
9035
|
console.error(`Run \`kici-admin agent install\` first.`);
|
|
8400
9036
|
process.exit(1);
|
|
8401
9037
|
}
|
|
8402
9038
|
await manager.restart(config);
|
|
8403
|
-
console.log(`Agent service "${
|
|
9039
|
+
console.log(`Agent service "${config.name}" restarted.`);
|
|
8404
9040
|
} catch (err) {
|
|
8405
9041
|
console.error(`Error: ${toErrorMessage(err)}`);
|
|
8406
9042
|
process.exit(1);
|
|
@@ -8409,12 +9045,11 @@ function registerAgentRestart(agent) {
|
|
|
8409
9045
|
}
|
|
8410
9046
|
//#endregion
|
|
8411
9047
|
//#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;
|
|
9048
|
+
/** Read the port from the agent's env file (path from the manifest). */
|
|
9049
|
+
function readPortFromEnvFile(envFilePath) {
|
|
9050
|
+
if (!fs.existsSync(envFilePath)) return 4001;
|
|
8416
9051
|
try {
|
|
8417
|
-
const content = fs.readFileSync(
|
|
9052
|
+
const content = fs.readFileSync(envFilePath, "utf-8");
|
|
8418
9053
|
for (const line of content.split("\n")) {
|
|
8419
9054
|
const trimmed = line.trim();
|
|
8420
9055
|
if (trimmed.startsWith("#") || !trimmed.includes("=")) continue;
|
|
@@ -8470,36 +9105,39 @@ function buildJsonOutput(serviceStatus, health, serviceName) {
|
|
|
8470
9105
|
};
|
|
8471
9106
|
}
|
|
8472
9107
|
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", "
|
|
9108
|
+
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
9109
|
try {
|
|
8475
|
-
const
|
|
8476
|
-
const userLevel =
|
|
8477
|
-
const
|
|
9110
|
+
const platform = detectPlatform(opts.platform);
|
|
9111
|
+
const userLevel = resolveUserLevel(opts);
|
|
9112
|
+
const manager = await createServiceManager(platform);
|
|
9113
|
+
const kiciRoot = kiciConfigRoot(userLevel);
|
|
9114
|
+
const resolved = await resolveInstance({
|
|
9115
|
+
component: "agent",
|
|
9116
|
+
opts: {
|
|
9117
|
+
instanceDir: opts.instanceDir,
|
|
9118
|
+
name: opts.name
|
|
9119
|
+
},
|
|
9120
|
+
cwd: process.cwd(),
|
|
9121
|
+
kiciRoot,
|
|
9122
|
+
manager,
|
|
9123
|
+
isUserLevel: userLevel
|
|
9124
|
+
});
|
|
8478
9125
|
const config = {
|
|
8479
|
-
name:
|
|
8480
|
-
displayName: "KiCI
|
|
8481
|
-
description: "KiCI agent service",
|
|
9126
|
+
name: resolved.manifest.name,
|
|
9127
|
+
displayName: "KiCI Agent",
|
|
9128
|
+
description: "KiCI CI/CD workflow execution agent service",
|
|
8482
9129
|
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
|
-
}
|
|
9130
|
+
envFilePath: resolved.manifest.envFilePath,
|
|
9131
|
+
workingDirectory: resolved.manifest.configDir,
|
|
9132
|
+
isUserLevel: resolved.manifest.isUserLevel,
|
|
9133
|
+
restartPolicy: DEFAULT_RESTART_POLICY,
|
|
9134
|
+
component: "agent"
|
|
8497
9135
|
};
|
|
8498
9136
|
const serviceStatus = await manager.status(config);
|
|
8499
9137
|
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,
|
|
9138
|
+
if (serviceStatus.state === "running") health = await queryHealth(readPortFromEnvFile(config.envFilePath));
|
|
9139
|
+
if (opts.json) console.log(JSON.stringify(buildJsonOutput(serviceStatus, health, config.name), null, 2));
|
|
9140
|
+
else console.log(formatStatus(serviceStatus, health, config.name));
|
|
8503
9141
|
} catch (err) {
|
|
8504
9142
|
console.error(`Error: ${toErrorMessage(err)}`);
|
|
8505
9143
|
process.exit(1);
|
|
@@ -8509,30 +9147,33 @@ function registerAgentStatusCommand(parent) {
|
|
|
8509
9147
|
//#endregion
|
|
8510
9148
|
//#region src/cli/commands/agent-service/logs.ts
|
|
8511
9149
|
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", "
|
|
9150
|
+
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
9151
|
try {
|
|
8514
|
-
const
|
|
8515
|
-
const userLevel =
|
|
8516
|
-
const
|
|
9152
|
+
const platform = detectPlatform(opts.platform);
|
|
9153
|
+
const userLevel = resolveUserLevel(opts);
|
|
9154
|
+
const manager = await createServiceManager(platform);
|
|
9155
|
+
const kiciRoot = kiciConfigRoot(userLevel);
|
|
9156
|
+
const resolved = await resolveInstance({
|
|
9157
|
+
component: "agent",
|
|
9158
|
+
opts: {
|
|
9159
|
+
instanceDir: opts.instanceDir,
|
|
9160
|
+
name: opts.name
|
|
9161
|
+
},
|
|
9162
|
+
cwd: process.cwd(),
|
|
9163
|
+
kiciRoot,
|
|
9164
|
+
manager,
|
|
9165
|
+
isUserLevel: userLevel
|
|
9166
|
+
});
|
|
8517
9167
|
const config = {
|
|
8518
|
-
name:
|
|
8519
|
-
displayName: "KiCI
|
|
8520
|
-
description: "KiCI agent service",
|
|
9168
|
+
name: resolved.manifest.name,
|
|
9169
|
+
displayName: "KiCI Agent",
|
|
9170
|
+
description: "KiCI CI/CD workflow execution agent service",
|
|
8521
9171
|
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
|
-
}
|
|
9172
|
+
envFilePath: resolved.manifest.envFilePath,
|
|
9173
|
+
workingDirectory: resolved.manifest.configDir,
|
|
9174
|
+
isUserLevel: resolved.manifest.isUserLevel,
|
|
9175
|
+
restartPolicy: DEFAULT_RESTART_POLICY,
|
|
9176
|
+
component: "agent"
|
|
8536
9177
|
};
|
|
8537
9178
|
const logOptions = {
|
|
8538
9179
|
since: opts.since,
|
|
@@ -8550,7 +9191,7 @@ function registerAgentLogsCommand(parent) {
|
|
|
8550
9191
|
//#endregion
|
|
8551
9192
|
//#region src/cli/commands/agent-service/upgrade.ts
|
|
8552
9193
|
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
|
|
9194
|
+
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
9195
|
await performVersionedUpgrade("agent", opts);
|
|
8555
9196
|
});
|
|
8556
9197
|
}
|