@automagik/omni 2.260910.16 → 2.260910.17
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/commands/events.d.ts +1 -1
- package/dist/commands/events.d.ts.map +1 -1
- package/dist/commands/sources.d.ts +70 -0
- package/dist/commands/sources.d.ts.map +1 -0
- package/dist/commands/webhooks.d.ts +1 -1
- package/dist/commands/webhooks.d.ts.map +1 -1
- package/dist/index.js +2429 -2032
- package/dist/server/index.js +77 -91
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -137198,7 +137198,7 @@ import { fileURLToPath } from "url";
|
|
|
137198
137198
|
// package.json
|
|
137199
137199
|
var package_default = {
|
|
137200
137200
|
name: "@automagik/omni",
|
|
137201
|
-
version: "2.260910.
|
|
137201
|
+
version: "2.260910.17",
|
|
137202
137202
|
repository: {
|
|
137203
137203
|
type: "git",
|
|
137204
137204
|
url: "git+https://github.com/automagik-dev/omni.git",
|
|
@@ -150668,2282 +150668,2673 @@ function createSlackCommand() {
|
|
|
150668
150668
|
return cmd;
|
|
150669
150669
|
}
|
|
150670
150670
|
|
|
150671
|
-
// src/commands/
|
|
150672
|
-
import {
|
|
150671
|
+
// src/commands/sources.ts
|
|
150672
|
+
import { randomBytes as randomBytes4 } from "crypto";
|
|
150673
|
+
init_config();
|
|
150673
150674
|
init_output();
|
|
150674
|
-
|
|
150675
|
-
|
|
150676
|
-
|
|
150677
|
-
|
|
150678
|
-
|
|
150679
|
-
|
|
150680
|
-
|
|
150681
|
-
|
|
150682
|
-
|
|
150683
|
-
|
|
150684
|
-
|
|
150685
|
-
|
|
150686
|
-
|
|
150687
|
-
|
|
150688
|
-
|
|
150689
|
-
|
|
150690
|
-
|
|
150691
|
-
|
|
150692
|
-
}
|
|
150693
|
-
|
|
150694
|
-
|
|
150695
|
-
|
|
150696
|
-
|
|
150697
|
-
|
|
150698
|
-
|
|
150699
|
-
|
|
150700
|
-
|
|
150701
|
-
|
|
150702
|
-
|
|
150703
|
-
|
|
150704
|
-
|
|
150705
|
-
|
|
150706
|
-
|
|
150707
|
-
|
|
150708
|
-
|
|
150709
|
-
|
|
150710
|
-
|
|
150711
|
-
|
|
150712
|
-
|
|
150713
|
-
|
|
150714
|
-
|
|
150715
|
-
|
|
150716
|
-
|
|
150717
|
-
|
|
150718
|
-
});
|
|
150719
|
-
audioBuffer = result.audio;
|
|
150720
|
-
mimeType = result.mimeType;
|
|
150721
|
-
durationMs = result.durationMs;
|
|
150722
|
-
providerName = result.provider;
|
|
150723
|
-
} catch (err2) {
|
|
150724
|
-
const message2 = err2 instanceof Error ? err2.message : "Unknown error";
|
|
150725
|
-
return error(`TTS synthesis failed: ${message2}`);
|
|
150726
|
-
}
|
|
150727
|
-
if (options3.output) {
|
|
150728
|
-
try {
|
|
150729
|
-
await writeFile2(options3.output, audioBuffer);
|
|
150730
|
-
return success("Audio saved", {
|
|
150731
|
-
path: options3.output,
|
|
150732
|
-
provider: providerName,
|
|
150733
|
-
mimeType,
|
|
150734
|
-
durationMs,
|
|
150735
|
-
sizeKb: Math.round(audioBuffer.length / 1024 * 100) / 100
|
|
150736
|
-
});
|
|
150737
|
-
} catch (err2) {
|
|
150738
|
-
const message2 = err2 instanceof Error ? err2.message : "Unknown error";
|
|
150739
|
-
return error(`Failed to write ${options3.output}: ${message2}`);
|
|
150675
|
+
// src/commands/sources/github/custom.github.issues.json
|
|
150676
|
+
var custom_github_issues_default = {
|
|
150677
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
150678
|
+
title: "custom.github.issues",
|
|
150679
|
+
description: "GitHub issues webhook delivery (X-GitHub-Event: issues). Minimal-but-real contract: the fields consumers rely on are required; everything else GitHub sends passes through (additionalProperties: true). Evolution rule: revisions must be additive-optional (issue #959).",
|
|
150680
|
+
type: "object",
|
|
150681
|
+
additionalProperties: true,
|
|
150682
|
+
required: ["action", "issue", "repository", "sender"],
|
|
150683
|
+
properties: {
|
|
150684
|
+
action: {
|
|
150685
|
+
type: "string",
|
|
150686
|
+
description: "What happened: opened, closed, edited, labeled, ..."
|
|
150687
|
+
},
|
|
150688
|
+
issue: {
|
|
150689
|
+
type: "object",
|
|
150690
|
+
additionalProperties: true,
|
|
150691
|
+
required: ["number"],
|
|
150692
|
+
properties: {
|
|
150693
|
+
number: { type: "integer", description: "Issue number" },
|
|
150694
|
+
title: { type: "string" },
|
|
150695
|
+
state: { type: "string" },
|
|
150696
|
+
html_url: { type: "string" }
|
|
150697
|
+
}
|
|
150698
|
+
},
|
|
150699
|
+
repository: {
|
|
150700
|
+
type: "object",
|
|
150701
|
+
additionalProperties: true,
|
|
150702
|
+
required: ["full_name"],
|
|
150703
|
+
properties: {
|
|
150704
|
+
full_name: {
|
|
150705
|
+
type: "string",
|
|
150706
|
+
description: "owner/repo, e.g. automagik-dev/omni"
|
|
150707
|
+
}
|
|
150708
|
+
}
|
|
150709
|
+
},
|
|
150710
|
+
sender: {
|
|
150711
|
+
type: "object",
|
|
150712
|
+
additionalProperties: true,
|
|
150713
|
+
required: ["login"],
|
|
150714
|
+
properties: {
|
|
150715
|
+
login: {
|
|
150716
|
+
type: "string",
|
|
150717
|
+
description: "GitHub username that triggered the delivery"
|
|
150718
|
+
}
|
|
150740
150719
|
}
|
|
150741
150720
|
}
|
|
150742
|
-
|
|
150743
|
-
|
|
150744
|
-
|
|
150745
|
-
|
|
150746
|
-
|
|
150747
|
-
|
|
150721
|
+
}
|
|
150722
|
+
};
|
|
150723
|
+
// src/commands/sources/github/custom.github.pull_request.json
|
|
150724
|
+
var custom_github_pull_request_default = {
|
|
150725
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
150726
|
+
title: "custom.github.pull_request",
|
|
150727
|
+
description: "GitHub pull_request webhook delivery (X-GitHub-Event: pull_request). Minimal-but-real contract: the fields consumers rely on are required; everything else GitHub sends passes through (additionalProperties: true). Evolution rule: revisions must be additive-optional (issue #959).",
|
|
150728
|
+
type: "object",
|
|
150729
|
+
additionalProperties: true,
|
|
150730
|
+
required: ["action", "number", "repository", "sender"],
|
|
150731
|
+
properties: {
|
|
150732
|
+
action: {
|
|
150733
|
+
type: "string",
|
|
150734
|
+
description: "What happened: opened, closed, synchronize, reopened, ..."
|
|
150735
|
+
},
|
|
150736
|
+
number: {
|
|
150737
|
+
type: "integer",
|
|
150738
|
+
description: "Pull request number"
|
|
150739
|
+
},
|
|
150740
|
+
pull_request: {
|
|
150741
|
+
type: "object",
|
|
150742
|
+
additionalProperties: true,
|
|
150743
|
+
properties: {
|
|
150744
|
+
title: { type: "string" },
|
|
150745
|
+
state: { type: "string" },
|
|
150746
|
+
merged: { type: "boolean" },
|
|
150747
|
+
html_url: { type: "string" }
|
|
150748
|
+
}
|
|
150749
|
+
},
|
|
150750
|
+
repository: {
|
|
150751
|
+
type: "object",
|
|
150752
|
+
additionalProperties: true,
|
|
150753
|
+
required: ["full_name"],
|
|
150754
|
+
properties: {
|
|
150755
|
+
full_name: {
|
|
150756
|
+
type: "string",
|
|
150757
|
+
description: "owner/repo, e.g. automagik-dev/omni"
|
|
150758
|
+
}
|
|
150759
|
+
}
|
|
150760
|
+
},
|
|
150761
|
+
sender: {
|
|
150762
|
+
type: "object",
|
|
150763
|
+
additionalProperties: true,
|
|
150764
|
+
required: ["login"],
|
|
150765
|
+
properties: {
|
|
150766
|
+
login: {
|
|
150767
|
+
type: "string",
|
|
150768
|
+
description: "GitHub username that triggered the delivery"
|
|
150769
|
+
}
|
|
150770
|
+
}
|
|
150748
150771
|
}
|
|
150749
|
-
|
|
150750
|
-
|
|
150772
|
+
}
|
|
150773
|
+
};
|
|
150774
|
+
// src/commands/sources/github/custom.github.push.json
|
|
150775
|
+
var custom_github_push_default = {
|
|
150776
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
150777
|
+
title: "custom.github.push",
|
|
150778
|
+
description: "GitHub push webhook delivery (X-GitHub-Event: push). Minimal-but-real contract: the fields consumers rely on are required; everything else GitHub sends passes through (additionalProperties: true). Evolution rule: revisions must be additive-optional (issue #959).",
|
|
150779
|
+
type: "object",
|
|
150780
|
+
additionalProperties: true,
|
|
150781
|
+
required: ["ref", "repository", "sender"],
|
|
150782
|
+
properties: {
|
|
150783
|
+
ref: {
|
|
150784
|
+
type: "string",
|
|
150785
|
+
description: "Full git ref that was pushed, e.g. refs/heads/main"
|
|
150786
|
+
},
|
|
150787
|
+
before: {
|
|
150788
|
+
type: "string",
|
|
150789
|
+
description: "SHA of the most recent commit on ref before the push"
|
|
150790
|
+
},
|
|
150791
|
+
after: {
|
|
150792
|
+
type: "string",
|
|
150793
|
+
description: "SHA of the most recent commit on ref after the push"
|
|
150794
|
+
},
|
|
150795
|
+
commits: {
|
|
150796
|
+
type: "array",
|
|
150797
|
+
description: "Commits in the push (GitHub caps this at 2048 entries)",
|
|
150798
|
+
items: {
|
|
150799
|
+
type: "object",
|
|
150800
|
+
additionalProperties: true,
|
|
150801
|
+
properties: {
|
|
150802
|
+
id: { type: "string" },
|
|
150803
|
+
message: { type: "string" },
|
|
150804
|
+
url: { type: "string" }
|
|
150805
|
+
}
|
|
150806
|
+
}
|
|
150807
|
+
},
|
|
150808
|
+
repository: {
|
|
150809
|
+
type: "object",
|
|
150810
|
+
additionalProperties: true,
|
|
150811
|
+
required: ["full_name"],
|
|
150812
|
+
properties: {
|
|
150813
|
+
full_name: {
|
|
150814
|
+
type: "string",
|
|
150815
|
+
description: "owner/repo, e.g. automagik-dev/omni"
|
|
150816
|
+
}
|
|
150817
|
+
}
|
|
150818
|
+
},
|
|
150819
|
+
sender: {
|
|
150820
|
+
type: "object",
|
|
150821
|
+
additionalProperties: true,
|
|
150822
|
+
required: ["login"],
|
|
150823
|
+
properties: {
|
|
150824
|
+
login: {
|
|
150825
|
+
type: "string",
|
|
150826
|
+
description: "GitHub username that triggered the delivery"
|
|
150827
|
+
}
|
|
150828
|
+
}
|
|
150751
150829
|
}
|
|
150752
|
-
|
|
150753
|
-
|
|
150754
|
-
|
|
150755
|
-
|
|
150756
|
-
|
|
150757
|
-
|
|
150758
|
-
|
|
150759
|
-
|
|
150760
|
-
|
|
150761
|
-
|
|
150762
|
-
|
|
150763
|
-
|
|
150764
|
-
|
|
150765
|
-
|
|
150766
|
-
|
|
150767
|
-
|
|
150768
|
-
|
|
150769
|
-
|
|
150830
|
+
}
|
|
150831
|
+
};
|
|
150832
|
+
// src/commands/sources/github/custom.github.release.json
|
|
150833
|
+
var custom_github_release_default = {
|
|
150834
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
150835
|
+
title: "custom.github.release",
|
|
150836
|
+
description: "GitHub release webhook delivery (X-GitHub-Event: release). Minimal-but-real contract: the fields consumers rely on are required; everything else GitHub sends passes through (additionalProperties: true). Evolution rule: revisions must be additive-optional (issue #959).",
|
|
150837
|
+
type: "object",
|
|
150838
|
+
additionalProperties: true,
|
|
150839
|
+
required: ["action", "release", "repository", "sender"],
|
|
150840
|
+
properties: {
|
|
150841
|
+
action: {
|
|
150842
|
+
type: "string",
|
|
150843
|
+
description: "What happened: published, created, edited, released, ..."
|
|
150844
|
+
},
|
|
150845
|
+
release: {
|
|
150846
|
+
type: "object",
|
|
150847
|
+
additionalProperties: true,
|
|
150848
|
+
required: ["tag_name"],
|
|
150849
|
+
properties: {
|
|
150850
|
+
tag_name: { type: "string", description: "Git tag of the release, e.g. v2.260907.4" },
|
|
150851
|
+
name: { type: "string" },
|
|
150852
|
+
draft: { type: "boolean" },
|
|
150853
|
+
prerelease: { type: "boolean" },
|
|
150854
|
+
html_url: { type: "string" }
|
|
150855
|
+
}
|
|
150856
|
+
},
|
|
150857
|
+
repository: {
|
|
150858
|
+
type: "object",
|
|
150859
|
+
additionalProperties: true,
|
|
150860
|
+
required: ["full_name"],
|
|
150861
|
+
properties: {
|
|
150862
|
+
full_name: {
|
|
150863
|
+
type: "string",
|
|
150864
|
+
description: "owner/repo, e.g. automagik-dev/omni"
|
|
150865
|
+
}
|
|
150866
|
+
}
|
|
150867
|
+
},
|
|
150868
|
+
sender: {
|
|
150869
|
+
type: "object",
|
|
150870
|
+
additionalProperties: true,
|
|
150871
|
+
required: ["login"],
|
|
150872
|
+
properties: {
|
|
150873
|
+
login: {
|
|
150874
|
+
type: "string",
|
|
150875
|
+
description: "GitHub username that triggered the delivery"
|
|
150876
|
+
}
|
|
150877
|
+
}
|
|
150770
150878
|
}
|
|
150771
|
-
}
|
|
150772
|
-
}
|
|
150773
|
-
function pickFilename(mimeType, provider) {
|
|
150774
|
-
const lower = mimeType.toLowerCase();
|
|
150775
|
-
if (lower.includes("ogg") || lower.includes("opus"))
|
|
150776
|
-
return `${provider}-speak.ogg`;
|
|
150777
|
-
if (lower.includes("wav"))
|
|
150778
|
-
return `${provider}-speak.wav`;
|
|
150779
|
-
if (lower.includes("mp3") || lower.includes("mpeg"))
|
|
150780
|
-
return `${provider}-speak.mp3`;
|
|
150781
|
-
if (lower.includes("flac"))
|
|
150782
|
-
return `${provider}-speak.flac`;
|
|
150783
|
-
if (lower.includes("aac") || lower.includes("m4a"))
|
|
150784
|
-
return `${provider}-speak.m4a`;
|
|
150785
|
-
return `${provider}-speak.bin`;
|
|
150786
|
-
}
|
|
150879
|
+
}
|
|
150880
|
+
};
|
|
150787
150881
|
|
|
150788
|
-
// src/commands/
|
|
150789
|
-
import {
|
|
150790
|
-
|
|
150791
|
-
import { join as join17 } from "path";
|
|
150792
|
-
init_config();
|
|
150793
|
-
init_embedded_canonical_migration();
|
|
150882
|
+
// src/commands/webhooks.ts
|
|
150883
|
+
import { readFileSync as readFileSync18 } from "fs";
|
|
150884
|
+
init_zod();
|
|
150794
150885
|
init_output();
|
|
150795
|
-
var
|
|
150796
|
-
|
|
150797
|
-
|
|
150798
|
-
|
|
150799
|
-
|
|
150800
|
-
|
|
150801
|
-
pm2NotFoundError();
|
|
150802
|
-
}
|
|
150803
|
-
const bundlePath = getServerBundlePath();
|
|
150804
|
-
if (!existsSync18(bundlePath)) {
|
|
150805
|
-
bundleNotFoundError(bundlePath);
|
|
150886
|
+
var SIGNATURE_ALGORITHMS = ["hmac-sha256", "hmac-sha1", "token-match"];
|
|
150887
|
+
var signatureSecretSchema = exports_external.string().min(8, "at least 8 characters").max(512, "at most 512 characters");
|
|
150888
|
+
var SECRET_SOURCE_FLAGS = "--signature-secret, --signature-secret-env, --signature-secret-stdin";
|
|
150889
|
+
async function readSecretFromStdin() {
|
|
150890
|
+
if (process.stdin.isTTY) {
|
|
150891
|
+
throw new Error('--signature-secret-stdin requires piped stdin (e.g. `printf %s "$SECRET" | omni webhooks ...`)');
|
|
150806
150892
|
}
|
|
150807
|
-
const
|
|
150808
|
-
const
|
|
150809
|
-
|
|
150810
|
-
const major = readDataDirMajor(EMBEDDED_PGSERVE_DATA_DIR);
|
|
150811
|
-
error(`Legacy embedded pgserve detected \u2014 this omni build does not run an embedded Postgres.
|
|
150812
|
-
Embedded data dir: ${EMBEDDED_PGSERVE_DATA_DIR}${major ? ` (PostgreSQL ${major})` : ""}
|
|
150813
|
-
Your data is intact. Migrate to canonical pgserve first:
|
|
150814
|
-
omni doctor --fix
|
|
150815
|
-
(idempotent \u2014 preserves data; do not \`omni start\` until it completes).`, undefined, 1);
|
|
150893
|
+
const chunks = [];
|
|
150894
|
+
for await (const chunk of process.stdin) {
|
|
150895
|
+
chunks.push(Buffer.from(chunk));
|
|
150816
150896
|
}
|
|
150817
|
-
|
|
150818
|
-
|
|
150819
|
-
|
|
150820
|
-
const
|
|
150821
|
-
|
|
150822
|
-
|
|
150823
|
-
|
|
150824
|
-
|
|
150825
|
-
|
|
150826
|
-
interpreter: "bash"
|
|
150827
|
-
});
|
|
150828
|
-
const apiCode = await runPm2(apiArgs, env2);
|
|
150829
|
-
if (apiCode !== 0) {
|
|
150830
|
-
error(`Failed to start ${PM2_PROCESSES.api} (pm2 exit code ${apiCode})`, undefined, 1);
|
|
150897
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
150898
|
+
}
|
|
150899
|
+
async function resolveSignatureSecret(options3, readStdin = readSecretFromStdin) {
|
|
150900
|
+
const sources = [
|
|
150901
|
+
options3.signatureSecret !== undefined,
|
|
150902
|
+
options3.signatureSecretEnv !== undefined,
|
|
150903
|
+
options3.signatureSecretStdin === true
|
|
150904
|
+
].filter(Boolean).length;
|
|
150905
|
+
if (sources === 0)
|
|
150831
150906
|
return;
|
|
150907
|
+
if (sources > 1) {
|
|
150908
|
+
throw new Error(`Use only one of ${SECRET_SOURCE_FLAGS}`);
|
|
150832
150909
|
}
|
|
150833
|
-
|
|
150834
|
-
|
|
150835
|
-
|
|
150836
|
-
const
|
|
150837
|
-
|
|
150838
|
-
|
|
150839
|
-
|
|
150840
|
-
|
|
150841
|
-
|
|
150842
|
-
|
|
150843
|
-
});
|
|
150844
|
-
const natsCode = await runPm2(natsArgs);
|
|
150845
|
-
if (natsCode !== 0) {
|
|
150846
|
-
warn(`${PM2_PROCESSES.nats} failed to start \u2014 run 'omni install' to download NATS first`);
|
|
150910
|
+
let raw2;
|
|
150911
|
+
let origin;
|
|
150912
|
+
if (options3.signatureSecretEnv !== undefined) {
|
|
150913
|
+
const name = options3.signatureSecretEnv;
|
|
150914
|
+
if (!name)
|
|
150915
|
+
throw new Error("--signature-secret-env requires a variable name");
|
|
150916
|
+
raw2 = process.env[name];
|
|
150917
|
+
origin = `environment variable ${name}`;
|
|
150918
|
+
if (raw2 === undefined || raw2 === "") {
|
|
150919
|
+
throw new Error(`${origin} is not set or empty`);
|
|
150847
150920
|
}
|
|
150921
|
+
} else if (options3.signatureSecretStdin) {
|
|
150922
|
+
raw2 = (await readStdin()).replace(/\r?\n$/, "");
|
|
150923
|
+
origin = "stdin";
|
|
150924
|
+
if (!raw2)
|
|
150925
|
+
throw new Error("no secret received on stdin");
|
|
150848
150926
|
} else {
|
|
150849
|
-
|
|
150927
|
+
raw2 = options3.signatureSecret;
|
|
150928
|
+
origin = "--signature-secret";
|
|
150850
150929
|
}
|
|
150851
|
-
const
|
|
150852
|
-
|
|
150853
|
-
|
|
150854
|
-
|
|
150855
|
-
success(`Server is healthy at ${healthUrl}`);
|
|
150856
|
-
} else {
|
|
150857
|
-
warn(`Health check did not pass within ${START_HEALTH_TIMEOUT_MS / 1000}s \u2014 server may still be starting`);
|
|
150930
|
+
const parsed = signatureSecretSchema.safeParse(raw2);
|
|
150931
|
+
if (!parsed.success) {
|
|
150932
|
+
const reason = parsed.error.issues.map((issue) => issue.message).join("; ");
|
|
150933
|
+
throw new Error(`Invalid signature secret from ${origin}: ${reason}`);
|
|
150858
150934
|
}
|
|
150935
|
+
return parsed.data;
|
|
150859
150936
|
}
|
|
150860
|
-
function
|
|
150861
|
-
|
|
150862
|
-
|
|
150863
|
-
|
|
150864
|
-
|
|
150865
|
-
|
|
150866
|
-
init_output();
|
|
150867
|
-
function formatUptime(ms) {
|
|
150868
|
-
if (ms === undefined || ms === 0)
|
|
150869
|
-
return "-";
|
|
150870
|
-
const seconds = Math.floor(ms / 1000);
|
|
150871
|
-
if (seconds < 60)
|
|
150872
|
-
return `${seconds}s`;
|
|
150873
|
-
const minutes = Math.floor(seconds / 60);
|
|
150874
|
-
if (minutes < 60)
|
|
150875
|
-
return `${minutes}m`;
|
|
150876
|
-
const hours = Math.floor(minutes / 60);
|
|
150877
|
-
if (hours < 24)
|
|
150878
|
-
return `${hours}h`;
|
|
150879
|
-
const days = Math.floor(hours / 24);
|
|
150880
|
-
return `${days}d`;
|
|
150881
|
-
}
|
|
150882
|
-
function formatMemory(bytes) {
|
|
150883
|
-
if (bytes === undefined || bytes === 0)
|
|
150884
|
-
return "-";
|
|
150885
|
-
return `${Math.round(bytes / 1024 / 1024)}MB`;
|
|
150886
|
-
}
|
|
150887
|
-
async function getProcessStatus() {
|
|
150888
|
-
if (!await isPm2Available()) {
|
|
150889
|
-
return null;
|
|
150937
|
+
function buildSignatureConfig(options3) {
|
|
150938
|
+
const { signatureAlgorithm, signatureHeader, signaturePrefix } = options3;
|
|
150939
|
+
if (!signatureAlgorithm && !signatureHeader && !signaturePrefix)
|
|
150940
|
+
return;
|
|
150941
|
+
if (!signatureAlgorithm || !signatureHeader) {
|
|
150942
|
+
error("--signature-algorithm and --signature-header must be provided together");
|
|
150890
150943
|
}
|
|
150891
|
-
const
|
|
150892
|
-
if (
|
|
150893
|
-
|
|
150944
|
+
const algorithm = SIGNATURE_ALGORITHMS.find((a2) => a2 === signatureAlgorithm);
|
|
150945
|
+
if (!algorithm) {
|
|
150946
|
+
error(`Invalid --signature-algorithm; expected one of: ${SIGNATURE_ALGORITHMS.join(", ")}`);
|
|
150894
150947
|
}
|
|
150895
|
-
|
|
150896
|
-
|
|
150897
|
-
processes = JSON.parse(stdout);
|
|
150898
|
-
} catch {
|
|
150899
|
-
return null;
|
|
150948
|
+
if (algorithm === "token-match" && signaturePrefix) {
|
|
150949
|
+
error("--signature-prefix is not applicable to token-match (the header carries the secret verbatim)");
|
|
150900
150950
|
}
|
|
150901
|
-
|
|
150902
|
-
return omniProcesses.map((p11) => ({
|
|
150903
|
-
service: p11.name ?? "-",
|
|
150904
|
-
pid: p11.pid !== undefined ? String(p11.pid) : "-",
|
|
150905
|
-
status: p11.pm2_env?.status ?? "-",
|
|
150906
|
-
uptime: p11.pm2_env?.pm_uptime ? formatUptime(Date.now() - p11.pm2_env.pm_uptime) : "-",
|
|
150907
|
-
cpu: p11.monit?.cpu !== undefined ? `${p11.monit.cpu}%` : "-",
|
|
150908
|
-
memory: formatMemory(p11.monit?.memory)
|
|
150909
|
-
}));
|
|
150951
|
+
return { algorithm, header: signatureHeader, prefix: signaturePrefix };
|
|
150910
150952
|
}
|
|
150911
|
-
|
|
150912
|
-
|
|
150913
|
-
|
|
150914
|
-
|
|
150915
|
-
|
|
150916
|
-
|
|
150917
|
-
|
|
150918
|
-
|
|
150919
|
-
|
|
150920
|
-
|
|
150921
|
-
|
|
150922
|
-
|
|
150923
|
-
|
|
150953
|
+
var eventTypeMappingSchema = exports_external.discriminatedUnion("source", [
|
|
150954
|
+
exports_external.object({
|
|
150955
|
+
source: exports_external.literal("header"),
|
|
150956
|
+
header: exports_external.string().min(1, "header must be 1-200 characters").max(200, "header must be 1-200 characters")
|
|
150957
|
+
}),
|
|
150958
|
+
exports_external.object({
|
|
150959
|
+
source: exports_external.literal("body"),
|
|
150960
|
+
path: exports_external.string().min(1, "path must be 1-200 characters").max(200, "path must be 1-200 characters")
|
|
150961
|
+
})
|
|
150962
|
+
]);
|
|
150963
|
+
var EVENT_TYPE_MAPPING_HINT = 'expected {"source":"header","header":"X-GitHub-Event"} or {"source":"body","path":"event"}';
|
|
150964
|
+
function resolveEventTypeMapping(raw2) {
|
|
150965
|
+
if (raw2 === undefined)
|
|
150966
|
+
return;
|
|
150967
|
+
let text3 = raw2;
|
|
150968
|
+
if (raw2.startsWith("@")) {
|
|
150969
|
+
const path = raw2.slice(1);
|
|
150970
|
+
try {
|
|
150971
|
+
text3 = readFileSync18(path, "utf-8");
|
|
150972
|
+
} catch (err2) {
|
|
150973
|
+
const reason = err2 instanceof Error ? err2.message : "Unknown error";
|
|
150974
|
+
throw new Error(`Cannot read --event-type-mapping file '${path}': ${reason}`);
|
|
150924
150975
|
}
|
|
150925
|
-
return response.headers.get(SERVER_VERSION_HEADER);
|
|
150926
|
-
} catch (err2) {
|
|
150927
|
-
statusInfo.apiStatus = "unreachable";
|
|
150928
|
-
statusInfo.apiError = err2 instanceof Error ? err2.message : "Unknown error";
|
|
150929
|
-
return null;
|
|
150930
150976
|
}
|
|
150931
|
-
|
|
150932
|
-
async function validateAuthKey(statusInfo, client) {
|
|
150977
|
+
let parsed;
|
|
150933
150978
|
try {
|
|
150934
|
-
|
|
150935
|
-
statusInfo.keyValid = auth.valid;
|
|
150936
|
-
statusInfo.keyName = auth.keyName;
|
|
150937
|
-
statusInfo.keyPrefix = auth.keyPrefix;
|
|
150938
|
-
statusInfo.scopes = auth.scopes;
|
|
150939
|
-
Object.assign(statusInfo, credentialStatusFields(auth));
|
|
150940
|
-
Object.assign(statusInfo, serverPostureFields(auth));
|
|
150979
|
+
parsed = JSON.parse(text3);
|
|
150941
150980
|
} catch {
|
|
150942
|
-
|
|
150981
|
+
throw new Error(`Invalid JSON for --event-type-mapping; ${EVENT_TYPE_MAPPING_HINT}`);
|
|
150982
|
+
}
|
|
150983
|
+
const result = eventTypeMappingSchema.safeParse(parsed);
|
|
150984
|
+
if (!result.success) {
|
|
150985
|
+
const reason = result.error.issues.map((issue) => issue.path.length > 0 ? `${issue.path.join(".")}: ${issue.message}` : issue.message).join("; ");
|
|
150986
|
+
throw new Error(`Invalid --event-type-mapping: ${reason}; ${EVENT_TYPE_MAPPING_HINT}`);
|
|
150943
150987
|
}
|
|
150988
|
+
return result.data;
|
|
150944
150989
|
}
|
|
150945
|
-
async function
|
|
150946
|
-
const
|
|
150947
|
-
|
|
150948
|
-
|
|
150949
|
-
|
|
150950
|
-
|
|
150951
|
-
|
|
150952
|
-
|
|
150953
|
-
|
|
150954
|
-
|
|
150955
|
-
|
|
150956
|
-
|
|
150957
|
-
if (isAuthenticated) {
|
|
150958
|
-
const client = getOptionalClient();
|
|
150959
|
-
if (client) {
|
|
150960
|
-
await validateAuthKey(statusInfo, client);
|
|
150961
|
-
}
|
|
150990
|
+
async function buildUpdateSourcePatch(options3) {
|
|
150991
|
+
const updates = buildLifecycleUpdates(options3);
|
|
150992
|
+
if (options3.name)
|
|
150993
|
+
updates.name = options3.name;
|
|
150994
|
+
if (options3.description)
|
|
150995
|
+
updates.description = options3.description;
|
|
150996
|
+
if (options3.idempotencyKeyTemplate)
|
|
150997
|
+
updates.idempotencyKeyTemplate = options3.idempotencyKeyTemplate;
|
|
150998
|
+
if (options3.strictSchemas !== undefined)
|
|
150999
|
+
updates.strictSchemas = options3.strictSchemas;
|
|
151000
|
+
if (options3.clearEventTypeMapping && options3.eventTypeMapping !== undefined) {
|
|
151001
|
+
throw new Error("Use only one of --event-type-mapping and --clear-event-type-mapping");
|
|
150962
151002
|
}
|
|
150963
|
-
|
|
150964
|
-
|
|
150965
|
-
|
|
151003
|
+
if (options3.clearEventTypeMapping)
|
|
151004
|
+
updates.eventTypeMapping = null;
|
|
151005
|
+
const eventTypeMapping = resolveEventTypeMapping(options3.eventTypeMapping);
|
|
151006
|
+
if (eventTypeMapping !== undefined)
|
|
151007
|
+
updates.eventTypeMapping = eventTypeMapping;
|
|
151008
|
+
if (options3.enable)
|
|
151009
|
+
updates.enabled = true;
|
|
151010
|
+
if (options3.disable)
|
|
151011
|
+
updates.enabled = false;
|
|
151012
|
+
if (options3.clearSignature) {
|
|
151013
|
+
updates.signatureConfig = null;
|
|
151014
|
+
return updates;
|
|
150966
151015
|
}
|
|
150967
|
-
|
|
151016
|
+
const signatureConfig = buildSignatureConfig(options3);
|
|
151017
|
+
if (signatureConfig)
|
|
151018
|
+
updates.signatureConfig = signatureConfig;
|
|
151019
|
+
const signatureSecret = await resolveSignatureSecret(options3);
|
|
151020
|
+
if (signatureSecret)
|
|
151021
|
+
updates.signatureSecret = signatureSecret;
|
|
151022
|
+
return updates;
|
|
150968
151023
|
}
|
|
150969
|
-
function
|
|
150970
|
-
if (
|
|
150971
|
-
|
|
151024
|
+
function assertPairedSignatureOnCreate(signatureConfig, signatureSecret) {
|
|
151025
|
+
if (signatureConfig === undefined === (signatureSecret === undefined))
|
|
151026
|
+
return;
|
|
151027
|
+
if (signatureConfig === undefined) {
|
|
151028
|
+
throw new Error(`a signature secret (${SECRET_SOURCE_FLAGS}) requires --signature-algorithm and --signature-header`);
|
|
150972
151029
|
}
|
|
150973
|
-
|
|
150974
|
-
|
|
150975
|
-
|
|
150976
|
-
|
|
150977
|
-
|
|
150978
|
-
|
|
151030
|
+
throw new Error(`--signature-algorithm and --signature-header require a signature secret (${SECRET_SOURCE_FLAGS})`);
|
|
151031
|
+
}
|
|
151032
|
+
var WINDOW_SEMANTICS = ["future_only", "includes_in_progress"];
|
|
151033
|
+
var MUTATION_POLICIES = ["same_id", "new_id"];
|
|
151034
|
+
function parseExpectedInterval(raw2) {
|
|
151035
|
+
if (raw2 === undefined)
|
|
151036
|
+
return;
|
|
151037
|
+
const seconds = Number(raw2);
|
|
151038
|
+
if (!Number.isInteger(seconds) || seconds < 1) {
|
|
151039
|
+
error("--expected-interval must be a positive integer number of seconds");
|
|
150979
151040
|
}
|
|
151041
|
+
return seconds;
|
|
150980
151042
|
}
|
|
150981
|
-
function
|
|
150982
|
-
|
|
150983
|
-
|
|
150984
|
-
|
|
150985
|
-
|
|
150986
|
-
|
|
150987
|
-
|
|
150988
|
-
|
|
150989
|
-
return status;
|
|
151043
|
+
function parseWindowSemantics(raw2) {
|
|
151044
|
+
if (raw2 === undefined)
|
|
151045
|
+
return;
|
|
151046
|
+
const value = WINDOW_SEMANTICS.find((v4) => v4 === raw2);
|
|
151047
|
+
if (!value) {
|
|
151048
|
+
error(`Invalid --window-semantics; expected one of: ${WINDOW_SEMANTICS.join(", ")}`);
|
|
151049
|
+
}
|
|
151050
|
+
return value;
|
|
150990
151051
|
}
|
|
150991
|
-
|
|
150992
|
-
|
|
150993
|
-
|
|
150994
|
-
|
|
150995
|
-
|
|
150996
|
-
|
|
150997
|
-
pm2NotFoundError();
|
|
150998
|
-
}
|
|
150999
|
-
info("Stopping omni services...");
|
|
151000
|
-
for (const name of Object.values(PM2_PROCESSES)) {
|
|
151001
|
-
await runPm2(["delete", name]);
|
|
151052
|
+
function parseMutationPolicy(raw2) {
|
|
151053
|
+
if (raw2 === undefined)
|
|
151054
|
+
return;
|
|
151055
|
+
const value = MUTATION_POLICIES.find((v4) => v4 === raw2);
|
|
151056
|
+
if (!value) {
|
|
151057
|
+
error(`Invalid --mutation-policy; expected one of: ${MUTATION_POLICIES.join(", ")}`);
|
|
151002
151058
|
}
|
|
151003
|
-
|
|
151004
|
-
|
|
151059
|
+
return value;
|
|
151060
|
+
}
|
|
151061
|
+
function buildLifecycleUpdates(options3) {
|
|
151062
|
+
if (options3.clearCadence && options3.expectedInterval !== undefined) {
|
|
151063
|
+
error("Use only one of --expected-interval and --clear-cadence");
|
|
151005
151064
|
}
|
|
151006
|
-
|
|
151065
|
+
const updates = {};
|
|
151066
|
+
if (options3.clearCadence)
|
|
151067
|
+
updates.expectedIntervalSeconds = null;
|
|
151068
|
+
const expectedIntervalSeconds = parseExpectedInterval(options3.expectedInterval);
|
|
151069
|
+
if (expectedIntervalSeconds !== undefined)
|
|
151070
|
+
updates.expectedIntervalSeconds = expectedIntervalSeconds;
|
|
151071
|
+
const windowSemantics = parseWindowSemantics(options3.windowSemantics);
|
|
151072
|
+
if (windowSemantics !== undefined)
|
|
151073
|
+
updates.windowSemantics = windowSemantics;
|
|
151074
|
+
const mutationPolicy = parseMutationPolicy(options3.mutationPolicy);
|
|
151075
|
+
if (mutationPolicy !== undefined)
|
|
151076
|
+
updates.mutationPolicy = mutationPolicy;
|
|
151077
|
+
return updates;
|
|
151007
151078
|
}
|
|
151008
|
-
function
|
|
151009
|
-
|
|
151079
|
+
function createWebhooksCommand() {
|
|
151080
|
+
const webhooks = new Command("webhooks").description("Manage webhook sources");
|
|
151081
|
+
webhooks.command("list").description("List webhook sources").option("--enabled", "Show only enabled sources").option("--disabled", "Show only disabled sources").action(async (options3) => {
|
|
151082
|
+
const client = getClient();
|
|
151083
|
+
try {
|
|
151084
|
+
let enabledFilter;
|
|
151085
|
+
if (options3.enabled)
|
|
151086
|
+
enabledFilter = true;
|
|
151087
|
+
if (options3.disabled)
|
|
151088
|
+
enabledFilter = false;
|
|
151089
|
+
const result = await client.webhooks.listSources({
|
|
151090
|
+
enabled: enabledFilter
|
|
151091
|
+
});
|
|
151092
|
+
const items = result.map((w) => ({
|
|
151093
|
+
id: w.id,
|
|
151094
|
+
name: w.name,
|
|
151095
|
+
enabled: w.enabled ? "yes" : "no",
|
|
151096
|
+
health: w.livenessStatus ?? "-",
|
|
151097
|
+
cadence: w.expectedIntervalSeconds != null ? `${w.expectedIntervalSeconds}s` : "-",
|
|
151098
|
+
createdAt: w.createdAt
|
|
151099
|
+
}));
|
|
151100
|
+
list(items, { emptyMessage: "No webhook sources found." });
|
|
151101
|
+
} catch (err2) {
|
|
151102
|
+
const message2 = err2 instanceof Error ? err2.message : "Unknown error";
|
|
151103
|
+
error(`Failed to list webhook sources: ${message2}`);
|
|
151104
|
+
}
|
|
151105
|
+
});
|
|
151106
|
+
webhooks.command("get <id>").description("Get webhook source details").action(async (id) => {
|
|
151107
|
+
const resolvedId = await resolveWebhookId(id);
|
|
151108
|
+
const client = getClient();
|
|
151109
|
+
try {
|
|
151110
|
+
const source = await client.webhooks.getSource(resolvedId);
|
|
151111
|
+
data(source);
|
|
151112
|
+
} catch (err2) {
|
|
151113
|
+
const message2 = err2 instanceof Error ? err2.message : "Unknown error";
|
|
151114
|
+
error(`Failed to get webhook source: ${message2}`);
|
|
151115
|
+
}
|
|
151116
|
+
});
|
|
151117
|
+
webhooks.command("create").description("Create a webhook source").requiredOption("--name <name>", "Webhook source name").option("--description <desc>", "Description").option("--disabled", "Create in disabled state").option("--headers <json>", `Expected headers as JSON (e.g., '{"X-Secret": true}')`).option("--signature-algorithm <alg>", "Signature verification: hmac-sha256, hmac-sha1, or token-match").option("--signature-header <name>", "Header carrying the signature/token (e.g., X-Hub-Signature-256)").option("--signature-prefix <prefix>", "Prefix before the hex digest (e.g., 'sha256=')").option("--signature-secret <secret>", "Shared secret for signature verification (write-only). Visible in shell history and process lists \u2014 prefer --signature-secret-env or --signature-secret-stdin").option("--signature-secret-env <VAR>", "Read the shared secret from environment variable VAR").option("--signature-secret-stdin", "Read the shared secret from stdin (trailing newline stripped)").option("--idempotency-key-template <template>", "Delivery-identity key template for redelivery dedup (#958). Placeholders: {source}, {sha256(body)}, " + "{headers.<name>}, {payload.<dot.path>}. Defaults to '{source}:{sha256(body)}'").option("--strict-schemas", "Refuse deliveries whose event type has no enabled registered schema (dead-lettered as " + "schema_not_registered, #1000). Recommended for new sources. Defaults to off").option("--event-type-mapping <json>", "Semantic event-type extraction (#959): inline JSON or @path/to/file.json, e.g. " + `'{"source":"header","header":"X-GitHub-Event"}' or '{"source":"body","path":"event"}'. ` + "A mapped source emits custom.{source}.{event} instead of the collapsed custom.webhook.{source}").option("--expected-interval <seconds>", "Declared cadence: >=1 event or heartbeat per N seconds. Arms liveness supervision (#961)").option("--window-semantics <value>", `Declared window semantics: ${WINDOW_SEMANTICS.join(" or ")}`).option("--mutation-policy <value>", `Declared mutation re-emit policy: ${MUTATION_POLICIES.join(" or ")}`).action(async (options3) => {
|
|
151118
|
+
const client = getClient();
|
|
151119
|
+
try {
|
|
151120
|
+
let expectedHeaders;
|
|
151121
|
+
if (options3.headers) {
|
|
151122
|
+
try {
|
|
151123
|
+
expectedHeaders = JSON.parse(options3.headers);
|
|
151124
|
+
} catch {
|
|
151125
|
+
error("Invalid JSON for --headers");
|
|
151126
|
+
}
|
|
151127
|
+
}
|
|
151128
|
+
const signatureConfig = buildSignatureConfig(options3);
|
|
151129
|
+
const signatureSecret = await resolveSignatureSecret(options3);
|
|
151130
|
+
assertPairedSignatureOnCreate(signatureConfig, signatureSecret);
|
|
151131
|
+
const source = await client.webhooks.createSource({
|
|
151132
|
+
name: options3.name,
|
|
151133
|
+
description: options3.description,
|
|
151134
|
+
enabled: !options3.disabled,
|
|
151135
|
+
expectedHeaders,
|
|
151136
|
+
signatureConfig,
|
|
151137
|
+
signatureSecret,
|
|
151138
|
+
idempotencyKeyTemplate: options3.idempotencyKeyTemplate,
|
|
151139
|
+
strictSchemas: options3.strictSchemas,
|
|
151140
|
+
eventTypeMapping: resolveEventTypeMapping(options3.eventTypeMapping),
|
|
151141
|
+
expectedIntervalSeconds: parseExpectedInterval(options3.expectedInterval),
|
|
151142
|
+
windowSemantics: parseWindowSemantics(options3.windowSemantics),
|
|
151143
|
+
mutationPolicy: parseMutationPolicy(options3.mutationPolicy)
|
|
151144
|
+
});
|
|
151145
|
+
const details = {
|
|
151146
|
+
id: source.id,
|
|
151147
|
+
name: source.name,
|
|
151148
|
+
url: `POST /api/v2/webhooks/${source.name}`
|
|
151149
|
+
};
|
|
151150
|
+
if (signatureConfig) {
|
|
151151
|
+
details.publicUrl = `POST /api/v2/webhooks/ingress/${source.name}`;
|
|
151152
|
+
}
|
|
151153
|
+
success(`Webhook source created: ${source.id}`, details);
|
|
151154
|
+
} catch (err2) {
|
|
151155
|
+
const message2 = err2 instanceof Error ? err2.message : "Unknown error";
|
|
151156
|
+
error(`Failed to create webhook source: ${message2}`);
|
|
151157
|
+
}
|
|
151158
|
+
});
|
|
151159
|
+
webhooks.command("update <id>").description("Update a webhook source").option("--name <name>", "New name").option("--description <desc>", "New description").option("--enable", "Enable the webhook").option("--disable", "Disable the webhook").option("--signature-algorithm <alg>", "Signature verification: hmac-sha256, hmac-sha1, or token-match").option("--signature-header <name>", "Header carrying the signature/token (e.g., X-Hub-Signature-256)").option("--signature-prefix <prefix>", "Prefix before the hex digest (e.g., 'sha256=')").option("--signature-secret <secret>", "Shared secret for signature verification (write-only). Visible in shell history and process lists \u2014 prefer --signature-secret-env or --signature-secret-stdin").option("--signature-secret-env <VAR>", "Read the shared secret from environment variable VAR").option("--signature-secret-stdin", "Read the shared secret from stdin (trailing newline stripped)").option("--clear-signature", "Remove the signature config and stored secret").option("--idempotency-key-template <template>", "Delivery-identity key template for redelivery dedup (#958). Placeholders: {source}, {sha256(body)}, " + "{headers.<name>}, {payload.<dot.path>}").option("--strict-schemas", "Refuse deliveries whose event type has no enabled registered schema (dead-lettered as " + "schema_not_registered, #1000)").option("--no-strict-schemas", "Return the source to opt-in pass-through for unregistered event types").option("--event-type-mapping <json>", "Semantic event-type extraction (#959): inline JSON or @path/to/file.json, e.g. " + `'{"source":"header","header":"X-GitHub-Event"}' or '{"source":"body","path":"event"}'`).option("--clear-event-type-mapping", "Remove the mapping (deliveries fall back to the collapsed custom.webhook.{source} type)").option("--expected-interval <seconds>", "Declared cadence: >=1 event or heartbeat per N seconds. (Re)arms liveness supervision (#961)").option("--clear-cadence", "Remove the declared cadence (disarms liveness supervision)").option("--window-semantics <value>", `Declared window semantics: ${WINDOW_SEMANTICS.join(" or ")}`).option("--mutation-policy <value>", `Declared mutation re-emit policy: ${MUTATION_POLICIES.join(" or ")}`).action(async (id, options3) => {
|
|
151160
|
+
const resolvedId = await resolveWebhookId(id);
|
|
151161
|
+
const client = getClient();
|
|
151162
|
+
try {
|
|
151163
|
+
const updates = await buildUpdateSourcePatch(options3);
|
|
151164
|
+
const source = await client.webhooks.updateSource(resolvedId, updates);
|
|
151165
|
+
success(`Webhook source updated: ${source.id}`, {
|
|
151166
|
+
id: source.id,
|
|
151167
|
+
name: source.name,
|
|
151168
|
+
enabled: source.enabled
|
|
151169
|
+
});
|
|
151170
|
+
} catch (err2) {
|
|
151171
|
+
const message2 = err2 instanceof Error ? err2.message : "Unknown error";
|
|
151172
|
+
error(`Failed to update webhook source: ${message2}`);
|
|
151173
|
+
}
|
|
151174
|
+
});
|
|
151175
|
+
webhooks.command("delete <id>").description("Delete a webhook source").action(async (id) => {
|
|
151176
|
+
const resolvedId = await resolveWebhookId(id);
|
|
151177
|
+
const client = getClient();
|
|
151178
|
+
try {
|
|
151179
|
+
await client.webhooks.deleteSource(resolvedId);
|
|
151180
|
+
success(`Webhook source deleted: ${resolvedId}`);
|
|
151181
|
+
} catch (err2) {
|
|
151182
|
+
const message2 = err2 instanceof Error ? err2.message : "Unknown error";
|
|
151183
|
+
error(`Failed to delete webhook source: ${message2}`);
|
|
151184
|
+
}
|
|
151185
|
+
});
|
|
151186
|
+
webhooks.command("trigger").description("Trigger a custom event").requiredOption("--type <type>", "Event type").requiredOption("--payload <json>", "Event payload as JSON").option("--instance <id>", "Instance ID").option("--correlation-id <id>", "Correlation ID").option("--causation-id <event-id>", "Parent event ID (keeps the causality tree for mid-flow emissions, #1072)").action(async (options3) => {
|
|
151187
|
+
const client = getClient();
|
|
151188
|
+
try {
|
|
151189
|
+
let payload;
|
|
151190
|
+
try {
|
|
151191
|
+
payload = JSON.parse(options3.payload);
|
|
151192
|
+
} catch {
|
|
151193
|
+
error("Invalid JSON for --payload");
|
|
151194
|
+
return;
|
|
151195
|
+
}
|
|
151196
|
+
const result = await client.webhooks.trigger({
|
|
151197
|
+
eventType: options3.type,
|
|
151198
|
+
payload,
|
|
151199
|
+
instanceId: options3.instance,
|
|
151200
|
+
correlationId: options3.correlationId,
|
|
151201
|
+
causationId: options3.causationId
|
|
151202
|
+
});
|
|
151203
|
+
success(`Event triggered: ${result.eventId}`, {
|
|
151204
|
+
eventId: result.eventId,
|
|
151205
|
+
eventType: result.eventType
|
|
151206
|
+
});
|
|
151207
|
+
} catch (err2) {
|
|
151208
|
+
const message2 = err2 instanceof Error ? err2.message : "Unknown error";
|
|
151209
|
+
error(`Failed to trigger event: ${message2}`);
|
|
151210
|
+
}
|
|
151211
|
+
});
|
|
151212
|
+
webhooks.command("heartbeat <source>").description('Record a connector heartbeat ("ran, zero events found") \u2014 resets the liveness window (#961)').action(async (source) => {
|
|
151213
|
+
const client = getClient();
|
|
151214
|
+
try {
|
|
151215
|
+
const result = await client.webhooks.heartbeat(source);
|
|
151216
|
+
success(`Heartbeat recorded for '${result.source}'`, {
|
|
151217
|
+
heartbeatAt: result.heartbeatAt,
|
|
151218
|
+
livenessStatus: result.livenessStatus ?? "unsupervised",
|
|
151219
|
+
expectedIntervalSeconds: result.expectedIntervalSeconds
|
|
151220
|
+
});
|
|
151221
|
+
} catch (err2) {
|
|
151222
|
+
const message2 = err2 instanceof Error ? err2.message : "Unknown error";
|
|
151223
|
+
error(`Failed to record heartbeat: ${message2}`);
|
|
151224
|
+
}
|
|
151225
|
+
});
|
|
151226
|
+
return webhooks;
|
|
151010
151227
|
}
|
|
151011
151228
|
|
|
151012
|
-
// src/commands/
|
|
151013
|
-
|
|
151014
|
-
|
|
151015
|
-
|
|
151016
|
-
|
|
151017
|
-
|
|
151018
|
-
|
|
151019
|
-
|
|
151020
|
-
|
|
151021
|
-
|
|
151022
|
-
|
|
151023
|
-
|
|
151229
|
+
// src/commands/sources.ts
|
|
151230
|
+
var github = {
|
|
151231
|
+
name: "github",
|
|
151232
|
+
description: "GitHub repo webhooks (RFC #925 Phase 3 recipe)",
|
|
151233
|
+
signatureConfig: { algorithm: "hmac-sha256", header: "X-Hub-Signature-256", prefix: "sha256=" },
|
|
151234
|
+
idempotencyKeyTemplate: "github:{headers.x-github-delivery}",
|
|
151235
|
+
eventTypeMapping: { source: "header", header: "X-GitHub-Event" },
|
|
151236
|
+
expectedIntervalSeconds: 86400,
|
|
151237
|
+
schemas: { push: custom_github_push_default, pull_request: custom_github_pull_request_default, issues: custom_github_issues_default, release: custom_github_release_default },
|
|
151238
|
+
async ensureProviderWebhook(api, repo, hook) {
|
|
151239
|
+
if (!/^[\w.-]+\/[\w.-]+$/.test(repo))
|
|
151240
|
+
throw new Error(`--repo must be OWNER/NAME, got '${repo}'`);
|
|
151241
|
+
const body = {
|
|
151242
|
+
active: true,
|
|
151243
|
+
events: hook.events,
|
|
151244
|
+
config: { url: hook.url, content_type: "json", secret: hook.secret, insecure_ssl: "0" }
|
|
151245
|
+
};
|
|
151246
|
+
const existing = await api("GET", `/repos/${repo}/hooks`);
|
|
151247
|
+
const match = existing.find((h10) => h10.config?.url === hook.url);
|
|
151248
|
+
if (match) {
|
|
151249
|
+
await api("PATCH", `/repos/${repo}/hooks/${match.id}`, body);
|
|
151250
|
+
return String(match.id);
|
|
151251
|
+
}
|
|
151252
|
+
const created = await api("POST", `/repos/${repo}/hooks`, { name: "web", ...body });
|
|
151253
|
+
return String(created.id);
|
|
151024
151254
|
}
|
|
151025
|
-
|
|
151026
|
-
}
|
|
151027
|
-
|
|
151028
|
-
|
|
151029
|
-
|
|
151255
|
+
};
|
|
151256
|
+
var PRESETS = { github };
|
|
151257
|
+
var GITHUB_API = "https://api.github.com";
|
|
151258
|
+
async function ghAvailable() {
|
|
151259
|
+
try {
|
|
151260
|
+
return await Bun.spawn({ cmd: ["gh", "auth", "status"], stdout: "ignore", stderr: "ignore" }).exited === 0;
|
|
151261
|
+
} catch {
|
|
151262
|
+
return false;
|
|
151030
151263
|
}
|
|
151031
|
-
return value;
|
|
151032
151264
|
}
|
|
151033
|
-
function
|
|
151034
|
-
|
|
151035
|
-
|
|
151265
|
+
async function githubApiViaGh(method, path, body) {
|
|
151266
|
+
const cmd = ["gh", "api", "--method", method, path];
|
|
151267
|
+
if (body !== undefined)
|
|
151268
|
+
cmd.push("--input", "-");
|
|
151269
|
+
const proc = Bun.spawn({ cmd, stdin: body === undefined ? "ignore" : "pipe", stdout: "pipe", stderr: "pipe" });
|
|
151270
|
+
if (body !== undefined && proc.stdin) {
|
|
151271
|
+
proc.stdin.write(JSON.stringify(body));
|
|
151272
|
+
proc.stdin.end();
|
|
151036
151273
|
}
|
|
151037
|
-
|
|
151274
|
+
const [stdout, stderr, code] = await Promise.all([
|
|
151275
|
+
new Response(proc.stdout).text(),
|
|
151276
|
+
new Response(proc.stderr).text(),
|
|
151277
|
+
proc.exited
|
|
151278
|
+
]);
|
|
151279
|
+
if (code !== 0)
|
|
151280
|
+
throw new Error(`gh api ${method} ${path} failed: ${stderr.trim() || stdout.trim()}`);
|
|
151281
|
+
return stdout.trim() ? JSON.parse(stdout) : undefined;
|
|
151038
151282
|
}
|
|
151039
|
-
function
|
|
151040
|
-
|
|
151041
|
-
|
|
151042
|
-
|
|
151043
|
-
|
|
151044
|
-
|
|
151283
|
+
function githubApiViaToken(token) {
|
|
151284
|
+
return async (method, path, body) => {
|
|
151285
|
+
const resp = await fetch(`${GITHUB_API}${path}`, {
|
|
151286
|
+
method,
|
|
151287
|
+
headers: {
|
|
151288
|
+
authorization: `Bearer ${token}`,
|
|
151289
|
+
accept: "application/vnd.github+json",
|
|
151290
|
+
"content-type": "application/json"
|
|
151291
|
+
},
|
|
151292
|
+
body: body === undefined ? undefined : JSON.stringify(body)
|
|
151293
|
+
});
|
|
151294
|
+
if (!resp.ok)
|
|
151295
|
+
throw new Error(`GitHub API ${method} ${path} returned ${resp.status}: ${await resp.text()}`);
|
|
151296
|
+
return resp.status === 204 ? undefined : await resp.json();
|
|
151297
|
+
};
|
|
151045
151298
|
}
|
|
151046
|
-
function
|
|
151047
|
-
|
|
151048
|
-
|
|
151049
|
-
|
|
151050
|
-
|
|
151051
|
-
|
|
151052
|
-
|
|
151299
|
+
async function resolveGithubApi() {
|
|
151300
|
+
if (await ghAvailable())
|
|
151301
|
+
return githubApiViaGh;
|
|
151302
|
+
const token = process.env.GITHUB_TOKEN;
|
|
151303
|
+
if (token)
|
|
151304
|
+
return githubApiViaToken(token);
|
|
151305
|
+
throw new Error("no GitHub credentials: log in with `gh auth login` or set GITHUB_TOKEN (or pass --no-provider-webhook)");
|
|
151306
|
+
}
|
|
151307
|
+
|
|
151308
|
+
class StepFailure extends Error {
|
|
151309
|
+
step;
|
|
151310
|
+
completed;
|
|
151311
|
+
constructor(step, completed, cause) {
|
|
151312
|
+
super(cause instanceof Error ? cause.message : String(cause));
|
|
151313
|
+
this.step = step;
|
|
151314
|
+
this.completed = completed;
|
|
151053
151315
|
}
|
|
151054
|
-
|
|
151055
|
-
|
|
151316
|
+
}
|
|
151317
|
+
var STEPS = ["source", "idempotency", "event-type-mapping", "schemas", "provider-webhook"];
|
|
151318
|
+
async function addSource(input) {
|
|
151319
|
+
const { preset, client } = input;
|
|
151320
|
+
const unknown2 = input.events.filter((e10) => !(e10 in preset.schemas));
|
|
151321
|
+
if (unknown2.length > 0) {
|
|
151322
|
+
throw new Error(`unknown ${preset.name} events: ${unknown2.join(", ")} (known: ${Object.keys(preset.schemas).join(", ")})`);
|
|
151056
151323
|
}
|
|
151057
|
-
const
|
|
151058
|
-
|
|
151059
|
-
|
|
151060
|
-
|
|
151324
|
+
const webhookUrl = `${input.publicUrl.replace(/\/+$/, "")}/api/v2/webhooks/ingress/${preset.name}`;
|
|
151325
|
+
const completed = [];
|
|
151326
|
+
const run = async (step, fn) => {
|
|
151327
|
+
try {
|
|
151328
|
+
const value = await fn();
|
|
151329
|
+
completed.push(step);
|
|
151330
|
+
return value;
|
|
151331
|
+
} catch (err2) {
|
|
151332
|
+
throw new StepFailure(step, completed, err2);
|
|
151061
151333
|
}
|
|
151334
|
+
};
|
|
151335
|
+
const source = await run(STEPS[0], async () => {
|
|
151336
|
+
const existing = (await client.listSources()).find((s2) => s2.name === preset.name);
|
|
151337
|
+
const body = {
|
|
151338
|
+
description: preset.description,
|
|
151339
|
+
signatureConfig: preset.signatureConfig,
|
|
151340
|
+
signatureSecret: input.secret,
|
|
151341
|
+
expectedIntervalSeconds: preset.expectedIntervalSeconds
|
|
151342
|
+
};
|
|
151343
|
+
if (existing)
|
|
151344
|
+
return { id: (await client.updateSource(existing.id, body)).id, created: false };
|
|
151345
|
+
return { id: (await client.createSource({ name: preset.name, enabled: true, ...body })).id, created: true };
|
|
151346
|
+
});
|
|
151347
|
+
await run(STEPS[1], () => client.updateSource(source.id, { idempotencyKeyTemplate: preset.idempotencyKeyTemplate }));
|
|
151348
|
+
await run(STEPS[2], () => client.updateSource(source.id, { eventTypeMapping: preset.eventTypeMapping }));
|
|
151349
|
+
await run(STEPS[3], async () => {
|
|
151350
|
+
for (const event of input.events) {
|
|
151351
|
+
const eventType = `custom.${preset.name}.${event}`;
|
|
151352
|
+
await input.registerSchema(eventType, preset.schemas[event], `${preset.name} ${event} deliveries`);
|
|
151353
|
+
}
|
|
151354
|
+
});
|
|
151355
|
+
let providerHookId;
|
|
151356
|
+
if (input.providerWebhook) {
|
|
151357
|
+
providerHookId = await run(STEPS[4], async () => preset.ensureProviderWebhook(await input.providerApi(), input.target, {
|
|
151358
|
+
url: webhookUrl,
|
|
151359
|
+
secret: input.secret,
|
|
151360
|
+
events: input.events
|
|
151361
|
+
}));
|
|
151062
151362
|
}
|
|
151063
|
-
return
|
|
151363
|
+
return { sourceId: source.id, created: source.created, webhookUrl, providerHookId, completed };
|
|
151064
151364
|
}
|
|
151065
|
-
function
|
|
151066
|
-
|
|
151067
|
-
|
|
151068
|
-
|
|
151069
|
-
|
|
151070
|
-
|
|
151071
|
-
|
|
151072
|
-
|
|
151073
|
-
|
|
151074
|
-
|
|
151075
|
-
|
|
151076
|
-
|
|
151077
|
-
|
|
151078
|
-
|
|
151079
|
-
|
|
151080
|
-
|
|
151081
|
-
|
|
151082
|
-
|
|
151083
|
-
|
|
151084
|
-
|
|
151085
|
-
|
|
151086
|
-
|
|
151087
|
-
|
|
151088
|
-
|
|
151089
|
-
|
|
151090
|
-
|
|
151091
|
-
|
|
151365
|
+
function createSourcesCommand() {
|
|
151366
|
+
const sources = new Command("sources").description("One-command onboarding of external webhook sources");
|
|
151367
|
+
sources.command("add <provider>").description(`Create or update a provider source end to end (presets: ${Object.keys(PRESETS).join(", ")})`).requiredOption("--repo <owner/name>", "Provider-side target (GitHub: OWNER/NAME)").option("--events <list>", "Comma-separated provider events (default: every bundled schema)").option("--public-url <url>", "Omni URL reachable from the provider (default: configured apiUrl)").option("--secret-env <VAR>", "Reuse the webhook secret from environment variable VAR instead of generating one").option("--no-provider-webhook", "Skip creating the webhook on the provider side").action(async (provider, options3) => {
|
|
151368
|
+
const preset = PRESETS[provider];
|
|
151369
|
+
if (!preset)
|
|
151370
|
+
error(`Unknown provider '${provider}'. Presets: ${Object.keys(PRESETS).join(", ")}`);
|
|
151371
|
+
const client = getClient();
|
|
151372
|
+
try {
|
|
151373
|
+
const secret = await resolveSignatureSecret({ signatureSecretEnv: options3.secretEnv }) ?? randomBytes4(32).toString("hex");
|
|
151374
|
+
const result = await addSource({
|
|
151375
|
+
preset,
|
|
151376
|
+
target: options3.repo,
|
|
151377
|
+
events: options3.events ? options3.events.split(",").map((e10) => e10.trim()) : Object.keys(preset.schemas),
|
|
151378
|
+
secret,
|
|
151379
|
+
publicUrl: options3.publicUrl ?? loadConfig().apiUrl ?? "http://localhost:8882",
|
|
151380
|
+
providerWebhook: options3.providerWebhook,
|
|
151381
|
+
client: client.webhooks,
|
|
151382
|
+
registerSchema: (eventType, schema3, description) => schemaApiRequest("", { method: "POST", body: JSON.stringify({ eventType, schema: schema3, description }) }),
|
|
151383
|
+
providerApi: resolveGithubApi
|
|
151384
|
+
});
|
|
151385
|
+
success(`Source ${result.created ? "created" : "updated"}: ${result.sourceId}`, {
|
|
151386
|
+
id: result.sourceId,
|
|
151387
|
+
name: preset.name,
|
|
151388
|
+
secret,
|
|
151389
|
+
webhookUrl: result.webhookUrl,
|
|
151390
|
+
providerHookId: result.providerHookId ?? "(skipped)",
|
|
151391
|
+
steps: result.completed.join(", ")
|
|
151392
|
+
});
|
|
151393
|
+
warn("The secret is shown once; store it now (re-run with --secret-env VAR to reuse it).");
|
|
151394
|
+
} catch (err2) {
|
|
151395
|
+
if (err2 instanceof StepFailure) {
|
|
151396
|
+
const remaining = STEPS.filter((s2) => !err2.completed.includes(s2));
|
|
151397
|
+
error(`Step '${err2.step}' failed: ${err2.message}`, {
|
|
151398
|
+
completed: err2.completed,
|
|
151399
|
+
remaining,
|
|
151400
|
+
resume: "re-run the same command; completed steps are updated in place, not duplicated"
|
|
151401
|
+
});
|
|
151402
|
+
}
|
|
151403
|
+
error(`Failed to add source: ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
151092
151404
|
}
|
|
151093
|
-
}
|
|
151094
|
-
renderPlatformError(err2, action);
|
|
151095
|
-
}
|
|
151096
|
-
function formatTenantRow(tenant) {
|
|
151097
|
-
return {
|
|
151098
|
-
id: tenant.id,
|
|
151099
|
-
slug: tenant.slug,
|
|
151100
|
-
name: tenant.displayName,
|
|
151101
|
-
status: tenant.status,
|
|
151102
|
-
keyTtl: tenant.maxKeyTtlSeconds,
|
|
151103
|
-
keyRate: tenant.maxKeyRateLimit,
|
|
151104
|
-
keyBudget: tenant.maxKeyBudget,
|
|
151105
|
-
created: new Date(tenant.createdAt).toLocaleDateString()
|
|
151106
|
-
};
|
|
151107
|
-
}
|
|
151108
|
-
function formatMembershipRow(membership) {
|
|
151109
|
-
return {
|
|
151110
|
-
id: membership.id,
|
|
151111
|
-
principal: membership.principalId,
|
|
151112
|
-
role: membership.role,
|
|
151113
|
-
status: membership.status,
|
|
151114
|
-
invitedBy: membership.invitedByPrincipalId ?? "-",
|
|
151115
|
-
created: new Date(membership.createdAt).toLocaleDateString()
|
|
151116
|
-
};
|
|
151117
|
-
}
|
|
151118
|
-
async function handleList5(client, options3) {
|
|
151119
|
-
const reason = requireReason(options3.reason, "list tenants");
|
|
151120
|
-
const result = await client.platform.tenants.list(reason);
|
|
151121
|
-
list(result.items.map(formatTenantRow), {
|
|
151122
|
-
emptyMessage: "No tenants found.",
|
|
151123
|
-
rawData: result.items
|
|
151124
|
-
});
|
|
151125
|
-
}
|
|
151126
|
-
async function handleGet3(client, id, options3) {
|
|
151127
|
-
const reason = requireReason(options3.reason, "read a tenant");
|
|
151128
|
-
const tenant = await client.platform.tenants.get(id, reason);
|
|
151129
|
-
data(tenant);
|
|
151130
|
-
}
|
|
151131
|
-
async function handleCreate4(client, options3) {
|
|
151132
|
-
const reason = requireReason(options3.reason, "create a tenant");
|
|
151133
|
-
const tenant = await client.platform.tenants.create({
|
|
151134
|
-
slug: options3.slug,
|
|
151135
|
-
displayName: options3.name,
|
|
151136
|
-
maxKeyTtlSeconds: options3.maxKeyTtl,
|
|
151137
|
-
maxKeyRateLimit: options3.maxKeyRate,
|
|
151138
|
-
maxKeyBudget: options3.maxKeyBudget,
|
|
151139
|
-
reason
|
|
151140
|
-
});
|
|
151141
|
-
success(`Tenant created: ${tenant.slug} (${tenant.id})`);
|
|
151142
|
-
data(tenant);
|
|
151143
|
-
}
|
|
151144
|
-
async function handleSuspend(client, id, options3) {
|
|
151145
|
-
const reason = requireReason(options3.reason, "suspend a tenant");
|
|
151146
|
-
const tenant = await client.platform.tenants.suspend(id, reason);
|
|
151147
|
-
success(`Tenant suspended: ${tenant.slug} (${tenant.id})`);
|
|
151148
|
-
info(`Reason: ${reason}`);
|
|
151149
|
-
}
|
|
151150
|
-
async function handleArchive(client, id, options3) {
|
|
151151
|
-
const reason = requireReason(options3.reason, "archive a tenant");
|
|
151152
|
-
const tenant = await client.platform.tenants.archive(id, reason);
|
|
151153
|
-
success(`Tenant archived: ${tenant.slug} (${tenant.id})`);
|
|
151154
|
-
warn("Archived is terminal \u2014 there is no un-archive and no hard delete.");
|
|
151155
|
-
info(`Reason: ${reason}`);
|
|
151156
|
-
}
|
|
151157
|
-
async function handleMembershipsList(client, tenantId, options3) {
|
|
151158
|
-
const reason = requireReason(options3.reason, "list memberships");
|
|
151159
|
-
const result = await client.platform.tenants.memberships.list(tenantId, reason);
|
|
151160
|
-
list(result.items.map(formatMembershipRow), {
|
|
151161
|
-
emptyMessage: "No memberships found.",
|
|
151162
|
-
rawData: result.items
|
|
151163
|
-
});
|
|
151164
|
-
}
|
|
151165
|
-
async function handleMembershipsAdd(client, tenantId, options3) {
|
|
151166
|
-
const reason = requireReason(options3.reason, "attach a membership");
|
|
151167
|
-
const role = parseRole(options3.role);
|
|
151168
|
-
const membership = await client.platform.tenants.memberships.attach(tenantId, {
|
|
151169
|
-
principalId: options3.principal,
|
|
151170
|
-
role,
|
|
151171
|
-
reason
|
|
151172
151405
|
});
|
|
151173
|
-
|
|
151174
|
-
}
|
|
151175
|
-
async function handleMembershipsDisable(client, tenantId, membershipId, options3) {
|
|
151176
|
-
const reason = requireReason(options3.reason, "disable a membership");
|
|
151177
|
-
const membership = await client.platform.tenants.memberships.disable(tenantId, membershipId, reason);
|
|
151178
|
-
success(`Membership disabled: ${membership.id}`);
|
|
151179
|
-
info(`Reason: ${reason}`);
|
|
151180
|
-
}
|
|
151181
|
-
async function handleMembershipsStatus(client, tenantId, membershipId, options3) {
|
|
151182
|
-
const reason = requireReason(options3.reason, "set a membership status");
|
|
151183
|
-
const status = parseStatus(options3.status);
|
|
151184
|
-
const membership = await client.platform.tenants.memberships.setStatus(tenantId, membershipId, status, reason);
|
|
151185
|
-
success(`Membership ${membership.id} status set to ${membership.status}`);
|
|
151406
|
+
return sources;
|
|
151186
151407
|
}
|
|
151187
|
-
|
|
151188
|
-
|
|
151189
|
-
|
|
151190
|
-
|
|
151191
|
-
|
|
151408
|
+
|
|
151409
|
+
// src/commands/speak.ts
|
|
151410
|
+
import { writeFile as writeFile2 } from "fs/promises";
|
|
151411
|
+
init_output();
|
|
151412
|
+
var ALLOWED_FORMATS2 = ["mp3", "ogg", "opus", "wav", "pcm", "flac", "aac"];
|
|
151413
|
+
function parseFormat2(value) {
|
|
151414
|
+
if (!value)
|
|
151415
|
+
return;
|
|
151416
|
+
const normalized = value.toLowerCase();
|
|
151417
|
+
if (!ALLOWED_FORMATS2.includes(normalized)) {
|
|
151418
|
+
error(`Invalid --format "${value}". Allowed: ${ALLOWED_FORMATS2.join(", ")}`);
|
|
151419
|
+
}
|
|
151420
|
+
return normalized;
|
|
151192
151421
|
}
|
|
151193
|
-
|
|
151194
|
-
|
|
151195
|
-
const role = parseRole(options3.role);
|
|
151196
|
-
const scopes = parseScopes(options3.scopes);
|
|
151197
|
-
const constraints = options3.constraints === undefined ? undefined : parseConstraints(options3.constraints);
|
|
151198
|
-
const body = {
|
|
151199
|
-
principalId: options3.principal,
|
|
151200
|
-
membershipId: options3.membership,
|
|
151201
|
-
role,
|
|
151202
|
-
name: options3.name,
|
|
151203
|
-
scopes,
|
|
151204
|
-
expiresAt: options3.expires,
|
|
151205
|
-
rateLimit: options3.rateLimit,
|
|
151206
|
-
budget: options3.budget,
|
|
151207
|
-
...constraints ? { resourceConstraints: constraints } : {},
|
|
151208
|
-
reason
|
|
151209
|
-
};
|
|
151210
|
-
const result = await client.platform.tenants.keys.issueRoot(tenantId, body);
|
|
151211
|
-
if (getCurrentFormat() === "json") {
|
|
151212
|
-
warn("plainTextKey is shown once and can never be retrieved again \u2014 store it now.");
|
|
151213
|
-
data(result);
|
|
151422
|
+
function parseSpeed(value) {
|
|
151423
|
+
if (value === undefined)
|
|
151214
151424
|
return;
|
|
151425
|
+
const n2 = Number.parseFloat(value);
|
|
151426
|
+
if (Number.isNaN(n2) || n2 < 0.5 || n2 > 2) {
|
|
151427
|
+
error(`Invalid --speed "${value}". Must be a number between 0.5 and 2.0.`);
|
|
151215
151428
|
}
|
|
151216
|
-
|
|
151217
|
-
success(`Root key issued: ${issued.name} (${issued.id})`);
|
|
151218
|
-
warn("The key below is shown ONCE and can never be retrieved again \u2014 store it now.");
|
|
151219
|
-
raw(`
|
|
151220
|
-
${plainTextKey}
|
|
151221
|
-
`);
|
|
151222
|
-
data(issued);
|
|
151429
|
+
return n2;
|
|
151223
151430
|
}
|
|
151224
|
-
|
|
151225
|
-
|
|
151226
|
-
const tenants2 = new Command("tenants").description("Platform tenant control plane (requires a PLATFORM-class credential and OMNI_MULTITENANCY_ENABLED=true)");
|
|
151227
|
-
tenants2.command("list").description("List tenants, newest first (the read itself is audited)").option("--reason <text>", REASON_HELP).action(async (options3) => {
|
|
151431
|
+
function createSpeakCommand() {
|
|
151432
|
+
return new Command("speak").description("Synthesize text to speech and send as a voice note (or save with --output)").argument("<text>", "Text to convert to speech").option("--provider <name>", "TTS provider (gemini, openai, elevenlabs). Default: server config.").option("--model <model>", "TTS model override (provider-specific)").option("--voice <name>", "Voice identifier (e.g. Kore for Gemini, JBFqn... for ElevenLabs)").option("--style <prompt>", 'Style prompt prepended to text (Gemini only, e.g. "Say cheerfully")').option("--instructions <text>", "Provider-native speaking instructions").option("--tone <text>", "Tone guidance").option("--accent <text>", "Accent guidance").option("--pace <text>", "Pace guidance").option("--emotion <text>", "Emotion guidance").option("--voice-note-profile <name>", "Voice-note profile/preset name").option("--language <code>", "BCP-47 language code (e.g. en-US, pt-BR)").option("--speed <factor>", "Speaking speed multiplier 0.5-2.0 (provider-dependent)").option("--format <fmt>", `Audio format: ${ALLOWED_FORMATS2.join(", ")} (default: ogg)`).option("--output <path>", "Save audio to file instead of sending").option("--instance <id>", "Override instance (default: from context)").option("--chat <id>", "Override chat (default: from context)").action(async (text3, options3) => {
|
|
151228
151433
|
const client = getClient();
|
|
151434
|
+
const format = parseFormat2(options3.format);
|
|
151435
|
+
const speed = parseSpeed(options3.speed);
|
|
151436
|
+
let audioBuffer;
|
|
151437
|
+
let mimeType;
|
|
151438
|
+
let durationMs;
|
|
151439
|
+
let providerName;
|
|
151229
151440
|
try {
|
|
151230
|
-
await
|
|
151441
|
+
const result = await client.media.tts({
|
|
151442
|
+
text: text3,
|
|
151443
|
+
provider: options3.provider,
|
|
151444
|
+
voice: options3.voice,
|
|
151445
|
+
language: options3.language,
|
|
151446
|
+
speed,
|
|
151447
|
+
format,
|
|
151448
|
+
style: options3.style,
|
|
151449
|
+
model: options3.model,
|
|
151450
|
+
instructions: options3.instructions,
|
|
151451
|
+
tone: options3.tone,
|
|
151452
|
+
accent: options3.accent,
|
|
151453
|
+
pace: options3.pace,
|
|
151454
|
+
emotion: options3.emotion,
|
|
151455
|
+
voiceNoteProfile: options3.voiceNoteProfile
|
|
151456
|
+
});
|
|
151457
|
+
audioBuffer = result.audio;
|
|
151458
|
+
mimeType = result.mimeType;
|
|
151459
|
+
durationMs = result.durationMs;
|
|
151460
|
+
providerName = result.provider;
|
|
151231
151461
|
} catch (err2) {
|
|
151232
|
-
|
|
151462
|
+
const message2 = err2 instanceof Error ? err2.message : "Unknown error";
|
|
151463
|
+
return error(`TTS synthesis failed: ${message2}`);
|
|
151233
151464
|
}
|
|
151234
|
-
|
|
151235
|
-
|
|
151236
|
-
|
|
151237
|
-
|
|
151238
|
-
|
|
151239
|
-
|
|
151240
|
-
|
|
151465
|
+
if (options3.output) {
|
|
151466
|
+
try {
|
|
151467
|
+
await writeFile2(options3.output, audioBuffer);
|
|
151468
|
+
return success("Audio saved", {
|
|
151469
|
+
path: options3.output,
|
|
151470
|
+
provider: providerName,
|
|
151471
|
+
mimeType,
|
|
151472
|
+
durationMs,
|
|
151473
|
+
sizeKb: Math.round(audioBuffer.length / 1024 * 100) / 100
|
|
151474
|
+
});
|
|
151475
|
+
} catch (err2) {
|
|
151476
|
+
const message2 = err2 instanceof Error ? err2.message : "Unknown error";
|
|
151477
|
+
return error(`Failed to write ${options3.output}: ${message2}`);
|
|
151478
|
+
}
|
|
151241
151479
|
}
|
|
151242
|
-
|
|
151243
|
-
|
|
151244
|
-
|
|
151245
|
-
|
|
151246
|
-
|
|
151247
|
-
|
|
151248
|
-
renderPlatformError(err2, "create the tenant");
|
|
151480
|
+
const ctx = await resolveContext({
|
|
151481
|
+
instance: options3.instance,
|
|
151482
|
+
chat: options3.chat
|
|
151483
|
+
});
|
|
151484
|
+
if (!ctx.instanceId) {
|
|
151485
|
+
return error("No instance in context. Set OMNI_INSTANCE, use --instance, run: omni use <instance>, or pass --output to save.");
|
|
151249
151486
|
}
|
|
151250
|
-
|
|
151251
|
-
|
|
151252
|
-
const client = getClient();
|
|
151253
|
-
try {
|
|
151254
|
-
await handleSuspend(client, id, options3);
|
|
151255
|
-
} catch (err2) {
|
|
151256
|
-
renderPlatformError(err2, "suspend the tenant");
|
|
151487
|
+
if (!ctx.chatId) {
|
|
151488
|
+
return error("No chat in context. Set OMNI_CHAT, use --chat, run: omni open <contact>, or pass --output to save.");
|
|
151257
151489
|
}
|
|
151258
|
-
});
|
|
151259
|
-
tenants2.command("archive <id>").description("Archive a tenant (TERMINAL \u2014 no un-archive, no hard delete)").option("--reason <text>", REASON_HELP).action(async (id, options3) => {
|
|
151260
|
-
const client = getClient();
|
|
151261
151490
|
try {
|
|
151262
|
-
await
|
|
151263
|
-
|
|
151264
|
-
|
|
151265
|
-
|
|
151266
|
-
|
|
151267
|
-
|
|
151268
|
-
|
|
151269
|
-
|
|
151270
|
-
|
|
151271
|
-
|
|
151272
|
-
|
|
151273
|
-
|
|
151274
|
-
|
|
151275
|
-
|
|
151276
|
-
memberships.command("add <tenant-id>").description("Grant a principal a role in the tenant").requiredOption("--principal <uuid>", "Principal to grant access to").requiredOption("--role <role>", `Tenant role: ${TENANT_ROLES.join(" | ")}`).option("--reason <text>", REASON_HELP).action(async (tenantId, options3) => {
|
|
151277
|
-
const client = getClient();
|
|
151278
|
-
try {
|
|
151279
|
-
await handleMembershipsAdd(client, tenantId, options3);
|
|
151280
|
-
} catch (err2) {
|
|
151281
|
-
renderPlatformError(err2, "attach the membership");
|
|
151282
|
-
}
|
|
151283
|
-
});
|
|
151284
|
-
memberships.command("disable <tenant-id> <membership-id>").description("Disable a membership (memberships are never hard-deleted)").option("--reason <text>", REASON_HELP).action(async (tenantId, membershipId, options3) => {
|
|
151285
|
-
const client = getClient();
|
|
151286
|
-
try {
|
|
151287
|
-
await handleMembershipsDisable(client, tenantId, membershipId, options3);
|
|
151288
|
-
} catch (err2) {
|
|
151289
|
-
renderPlatformError(err2, "disable the membership");
|
|
151290
|
-
}
|
|
151291
|
-
});
|
|
151292
|
-
memberships.command("status <tenant-id> <membership-id>").description("Activate or disable an existing membership").requiredOption("--status <status>", `New status: ${MEMBERSHIP_STATUSES.join(" | ")}`).option("--reason <text>", REASON_HELP).action(async (tenantId, membershipId, options3) => {
|
|
151293
|
-
const client = getClient();
|
|
151294
|
-
try {
|
|
151295
|
-
await handleMembershipsStatus(client, tenantId, membershipId, options3);
|
|
151296
|
-
} catch (err2) {
|
|
151297
|
-
renderPlatformError(err2, "set the membership status");
|
|
151298
|
-
}
|
|
151299
|
-
});
|
|
151300
|
-
memberships.command("role <tenant-id> <membership-id>").description("Change the role an existing membership acts under").requiredOption("--role <role>", `New role: ${TENANT_ROLES.join(" | ")}`).option("--reason <text>", REASON_HELP).action(async (tenantId, membershipId, options3) => {
|
|
151301
|
-
const client = getClient();
|
|
151302
|
-
try {
|
|
151303
|
-
await handleMembershipsRole(client, tenantId, membershipId, options3);
|
|
151304
|
-
} catch (err2) {
|
|
151305
|
-
renderPlatformError(err2, "set the membership role");
|
|
151306
|
-
}
|
|
151307
|
-
});
|
|
151308
|
-
tenants2.addCommand(memberships);
|
|
151309
|
-
const keys = new Command("keys").description("Manage tenant credentials");
|
|
151310
|
-
keys.command("issue-root <tenant-id>").description("Issue the initial tenant ROOT key (the plaintext is shown ONCE and never again)").requiredOption("--principal <uuid>", "Principal the key acts as (must hold the membership)").requiredOption("--membership <uuid>", "Active membership binding the principal to the tenant").requiredOption("--role <role>", `Tenant role: ${TENANT_ROLES.join(" | ")}`).requiredOption("--name <name>", "Human-readable key name").requiredOption("--scopes <list>", "Comma-separated explicit tenant scopes (platform/wildcard scopes are refused)").requiredOption("--expires <iso>", "Expiry (ISO 8601); must be in the future and within the tenant TTL ceiling").requiredOption("--rate-limit <n>", "Rate limit; capped by the tenant policy ceiling", Number.parseInt).requiredOption("--budget <n>", "Budget; capped by the tenant policy ceiling", Number.parseInt).option("--constraints <json>", `Optional resource allowlists as JSON, e.g. '{"instanceAllowlist":["<id>"]}'`).option("--reason <text>", REASON_HELP).action(async (tenantId, options3) => {
|
|
151311
|
-
const client = getClient();
|
|
151312
|
-
try {
|
|
151313
|
-
await handleKeysIssueRoot(client, tenantId, options3);
|
|
151491
|
+
const result = await client.messages.sendMedia({
|
|
151492
|
+
instanceId: ctx.instanceId,
|
|
151493
|
+
to: ctx.chatId,
|
|
151494
|
+
type: "audio",
|
|
151495
|
+
base64: audioBuffer.toString("base64"),
|
|
151496
|
+
filename: pickFilename(mimeType, providerName),
|
|
151497
|
+
voiceNote: true
|
|
151498
|
+
});
|
|
151499
|
+
success("Voice note sent", {
|
|
151500
|
+
provider: providerName,
|
|
151501
|
+
durationMs,
|
|
151502
|
+
sizeKb: Math.round(audioBuffer.length / 1024 * 100) / 100,
|
|
151503
|
+
...result
|
|
151504
|
+
});
|
|
151314
151505
|
} catch (err2) {
|
|
151315
|
-
|
|
151506
|
+
const message2 = err2 instanceof Error ? err2.message : "Unknown error";
|
|
151507
|
+
error(`Failed to send voice note: ${message2}`);
|
|
151316
151508
|
}
|
|
151317
151509
|
});
|
|
151318
|
-
|
|
151319
|
-
|
|
151510
|
+
}
|
|
151511
|
+
function pickFilename(mimeType, provider) {
|
|
151512
|
+
const lower = mimeType.toLowerCase();
|
|
151513
|
+
if (lower.includes("ogg") || lower.includes("opus"))
|
|
151514
|
+
return `${provider}-speak.ogg`;
|
|
151515
|
+
if (lower.includes("wav"))
|
|
151516
|
+
return `${provider}-speak.wav`;
|
|
151517
|
+
if (lower.includes("mp3") || lower.includes("mpeg"))
|
|
151518
|
+
return `${provider}-speak.mp3`;
|
|
151519
|
+
if (lower.includes("flac"))
|
|
151520
|
+
return `${provider}-speak.flac`;
|
|
151521
|
+
if (lower.includes("aac") || lower.includes("m4a"))
|
|
151522
|
+
return `${provider}-speak.m4a`;
|
|
151523
|
+
return `${provider}-speak.bin`;
|
|
151320
151524
|
}
|
|
151321
151525
|
|
|
151322
|
-
// src/commands/
|
|
151323
|
-
import {
|
|
151526
|
+
// src/commands/start.ts
|
|
151527
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync11 } from "fs";
|
|
151528
|
+
import { homedir as homedir12 } from "os";
|
|
151529
|
+
import { join as join17 } from "path";
|
|
151324
151530
|
init_config();
|
|
151531
|
+
init_embedded_canonical_migration();
|
|
151325
151532
|
init_output();
|
|
151326
|
-
|
|
151327
|
-
|
|
151328
|
-
|
|
151329
|
-
function trustEndpoint(path) {
|
|
151330
|
-
if (!hasAuth()) {
|
|
151331
|
-
exitNotAuthenticated();
|
|
151332
|
-
}
|
|
151333
|
-
return `${targetBaseUrl()}/api/v2/trust${path}`;
|
|
151533
|
+
var START_HEALTH_TIMEOUT_MS = 1e4;
|
|
151534
|
+
function legacyEmbeddedNeedsMigration(useCanonicalPgserve, embeddedDataPresent = existsSync18(join17(EMBEDDED_PGSERVE_DATA_DIR, "PG_VERSION"))) {
|
|
151535
|
+
return useCanonicalPgserve !== true && embeddedDataPresent;
|
|
151334
151536
|
}
|
|
151335
|
-
function
|
|
151336
|
-
|
|
151337
|
-
|
|
151338
|
-
if (config2.apiKey) {
|
|
151339
|
-
headers.Authorization = `Bearer ${config2.apiKey}`;
|
|
151537
|
+
async function runStart() {
|
|
151538
|
+
if (!await isPm2Available()) {
|
|
151539
|
+
pm2NotFoundError();
|
|
151340
151540
|
}
|
|
151341
|
-
|
|
151342
|
-
|
|
151343
|
-
|
|
151344
|
-
function getSigningContextOnce() {
|
|
151345
|
-
if (_cachedSigningContext === undefined) {
|
|
151346
|
-
_cachedSigningContext = loadSigningContextForServer(targetBaseUrl());
|
|
151541
|
+
const bundlePath = getServerBundlePath();
|
|
151542
|
+
if (!existsSync18(bundlePath)) {
|
|
151543
|
+
bundleNotFoundError(bundlePath);
|
|
151347
151544
|
}
|
|
151348
|
-
|
|
151349
|
-
|
|
151350
|
-
|
|
151351
|
-
|
|
151352
|
-
|
|
151353
|
-
|
|
151354
|
-
|
|
151355
|
-
|
|
151356
|
-
|
|
151357
|
-
const sigHeaders = ctx.signRequest(method, reqPath, bodyString);
|
|
151358
|
-
Object.assign(headers, sigHeaders);
|
|
151545
|
+
const serverConfig = loadServerConfig();
|
|
151546
|
+
const apiPort = serverConfig.port;
|
|
151547
|
+
if (legacyEmbeddedNeedsMigration(serverConfig.useCanonicalPgserve)) {
|
|
151548
|
+
const major = readDataDirMajor(EMBEDDED_PGSERVE_DATA_DIR);
|
|
151549
|
+
error(`Legacy embedded pgserve detected \u2014 this omni build does not run an embedded Postgres.
|
|
151550
|
+
Embedded data dir: ${EMBEDDED_PGSERVE_DATA_DIR}${major ? ` (PostgreSQL ${major})` : ""}
|
|
151551
|
+
Your data is intact. Migrate to canonical pgserve first:
|
|
151552
|
+
omni doctor --fix
|
|
151553
|
+
(idempotent \u2014 preserves data; do not \`omni start\` until it completes).`, undefined, 1);
|
|
151359
151554
|
}
|
|
151360
|
-
|
|
151361
|
-
|
|
151362
|
-
|
|
151363
|
-
|
|
151555
|
+
mkdirSync11(getPm2LogDir(), { recursive: true });
|
|
151556
|
+
info(`Starting ${PM2_PROCESSES.api} (port ${apiPort})...`);
|
|
151557
|
+
const cliConfig = loadLocalRuntimeConfig();
|
|
151558
|
+
const env2 = buildRuntimeEnv(serverConfig, cliConfig);
|
|
151559
|
+
const launcherPath = getServerLauncherPath();
|
|
151560
|
+
const apiArgs = buildPm2StartArgs({
|
|
151561
|
+
kind: "api",
|
|
151562
|
+
script: launcherPath,
|
|
151563
|
+
name: PM2_PROCESSES.api,
|
|
151564
|
+
interpreter: "bash"
|
|
151364
151565
|
});
|
|
151365
|
-
|
|
151366
|
-
|
|
151367
|
-
|
|
151566
|
+
const apiCode = await runPm2(apiArgs, env2);
|
|
151567
|
+
if (apiCode !== 0) {
|
|
151568
|
+
error(`Failed to start ${PM2_PROCESSES.api} (pm2 exit code ${apiCode})`, undefined, 1);
|
|
151569
|
+
return;
|
|
151368
151570
|
}
|
|
151369
|
-
|
|
151370
|
-
|
|
151371
|
-
|
|
151372
|
-
|
|
151373
|
-
|
|
151374
|
-
|
|
151375
|
-
|
|
151376
|
-
|
|
151377
|
-
|
|
151378
|
-
|
|
151379
|
-
};
|
|
151380
|
-
}
|
|
151381
|
-
async function handleList6() {
|
|
151382
|
-
try {
|
|
151383
|
-
const { items } = await callApi("GET", "/hosts");
|
|
151384
|
-
list(items.map(formatHostRow), {
|
|
151385
|
-
emptyMessage: "No genie hosts registered. Run `genie omni handshake` from a genie installation to register one.",
|
|
151386
|
-
rawData: items
|
|
151571
|
+
const natsPath = join17(homedir12(), ".omni", "nats-server");
|
|
151572
|
+
if (existsSync18(natsPath)) {
|
|
151573
|
+
info(`Starting ${PM2_PROCESSES.nats}...`);
|
|
151574
|
+
const natsDataDir = join17(serverConfig.dataDir, "nats");
|
|
151575
|
+
mkdirSync11(natsDataDir, { recursive: true });
|
|
151576
|
+
const natsArgs = buildPm2StartArgs({
|
|
151577
|
+
kind: "nats",
|
|
151578
|
+
script: natsPath,
|
|
151579
|
+
name: PM2_PROCESSES.nats,
|
|
151580
|
+
scriptArgs: ["-js", "-sd", natsDataDir]
|
|
151387
151581
|
});
|
|
151388
|
-
|
|
151389
|
-
|
|
151582
|
+
const natsCode = await runPm2(natsArgs);
|
|
151583
|
+
if (natsCode !== 0) {
|
|
151584
|
+
warn(`${PM2_PROCESSES.nats} failed to start \u2014 run 'omni install' to download NATS first`);
|
|
151585
|
+
}
|
|
151586
|
+
} else {
|
|
151587
|
+
warn(`NATS binary not found at ${natsPath} \u2014 skipping. Run 'omni install' to set it up.`);
|
|
151390
151588
|
}
|
|
151391
|
-
|
|
151392
|
-
|
|
151393
|
-
|
|
151394
|
-
|
|
151395
|
-
|
|
151396
|
-
}
|
|
151397
|
-
|
|
151589
|
+
const healthUrl = getHealthCheckUrl(apiPort);
|
|
151590
|
+
info(`Waiting for health check at ${healthUrl}...`);
|
|
151591
|
+
const healthy = await waitForHealth(apiPort, START_HEALTH_TIMEOUT_MS);
|
|
151592
|
+
if (healthy) {
|
|
151593
|
+
success(`Server is healthy at ${healthUrl}`);
|
|
151594
|
+
} else {
|
|
151595
|
+
warn(`Health check did not pass within ${START_HEALTH_TIMEOUT_MS / 1000}s \u2014 server may still be starting`);
|
|
151398
151596
|
}
|
|
151399
151597
|
}
|
|
151400
|
-
|
|
151401
|
-
|
|
151402
|
-
|
|
151403
|
-
|
|
151598
|
+
function createStartCommand() {
|
|
151599
|
+
return new Command("start").description("Start API and NATS via PM2").action(runStart);
|
|
151600
|
+
}
|
|
151601
|
+
|
|
151602
|
+
// src/commands/status.ts
|
|
151603
|
+
init_config();
|
|
151604
|
+
init_output();
|
|
151605
|
+
function formatUptime(ms) {
|
|
151606
|
+
if (ms === undefined || ms === 0)
|
|
151607
|
+
return "-";
|
|
151608
|
+
const seconds = Math.floor(ms / 1000);
|
|
151609
|
+
if (seconds < 60)
|
|
151610
|
+
return `${seconds}s`;
|
|
151611
|
+
const minutes = Math.floor(seconds / 60);
|
|
151612
|
+
if (minutes < 60)
|
|
151613
|
+
return `${minutes}m`;
|
|
151614
|
+
const hours = Math.floor(minutes / 60);
|
|
151615
|
+
if (hours < 24)
|
|
151616
|
+
return `${hours}h`;
|
|
151617
|
+
const days = Math.floor(hours / 24);
|
|
151618
|
+
return `${days}d`;
|
|
151619
|
+
}
|
|
151620
|
+
function formatMemory(bytes) {
|
|
151621
|
+
if (bytes === undefined || bytes === 0)
|
|
151622
|
+
return "-";
|
|
151623
|
+
return `${Math.round(bytes / 1024 / 1024)}MB`;
|
|
151624
|
+
}
|
|
151625
|
+
async function getProcessStatus() {
|
|
151626
|
+
if (!await isPm2Available()) {
|
|
151627
|
+
return null;
|
|
151404
151628
|
}
|
|
151405
|
-
|
|
151406
|
-
|
|
151407
|
-
|
|
151408
|
-
data(data2);
|
|
151409
|
-
} catch (err2) {
|
|
151410
|
-
error(`Failed to update genie host: ${err2 instanceof Error ? err2.message : "Unknown error"}`);
|
|
151629
|
+
const { code, stdout } = await capturePm2("jlist");
|
|
151630
|
+
if (code !== 0) {
|
|
151631
|
+
return null;
|
|
151411
151632
|
}
|
|
151412
|
-
|
|
151413
|
-
async function registerPubkey(pubkey, hostname) {
|
|
151633
|
+
let processes = [];
|
|
151414
151634
|
try {
|
|
151415
|
-
|
|
151416
|
-
|
|
151417
|
-
|
|
151418
|
-
|
|
151419
|
-
|
|
151420
|
-
|
|
151421
|
-
|
|
151635
|
+
processes = JSON.parse(stdout);
|
|
151636
|
+
} catch {
|
|
151637
|
+
return null;
|
|
151638
|
+
}
|
|
151639
|
+
const omniProcesses = processes.filter((p11) => typeof p11.name === "string" && p11.name.startsWith("omni-"));
|
|
151640
|
+
return omniProcesses.map((p11) => ({
|
|
151641
|
+
service: p11.name ?? "-",
|
|
151642
|
+
pid: p11.pid !== undefined ? String(p11.pid) : "-",
|
|
151643
|
+
status: p11.pm2_env?.status ?? "-",
|
|
151644
|
+
uptime: p11.pm2_env?.pm_uptime ? formatUptime(Date.now() - p11.pm2_env.pm_uptime) : "-",
|
|
151645
|
+
cpu: p11.monit?.cpu !== undefined ? `${p11.monit.cpu}%` : "-",
|
|
151646
|
+
memory: formatMemory(p11.monit?.memory)
|
|
151647
|
+
}));
|
|
151648
|
+
}
|
|
151649
|
+
async function checkApiHealth(statusInfo, apiUrl) {
|
|
151650
|
+
try {
|
|
151651
|
+
const response = await fetch(`${apiUrl.replace(/\/$/, "")}/api/v2/health`, {
|
|
151652
|
+
headers: {
|
|
151653
|
+
"Accept-Encoding": "identity",
|
|
151654
|
+
[CLI_VERSION_HEADER]: VERSION
|
|
151422
151655
|
}
|
|
151423
151656
|
});
|
|
151424
|
-
|
|
151657
|
+
const health = await response.json();
|
|
151658
|
+
statusInfo.apiStatus = health.status ?? "unknown";
|
|
151659
|
+
statusInfo.apiVersion = health.version;
|
|
151660
|
+
if (health.checks) {
|
|
151661
|
+
statusInfo.checks = health.checks;
|
|
151662
|
+
}
|
|
151663
|
+
return response.headers.get(SERVER_VERSION_HEADER);
|
|
151425
151664
|
} catch (err2) {
|
|
151426
|
-
|
|
151665
|
+
statusInfo.apiStatus = "unreachable";
|
|
151666
|
+
statusInfo.apiError = err2 instanceof Error ? err2.message : "Unknown error";
|
|
151667
|
+
return null;
|
|
151427
151668
|
}
|
|
151428
151669
|
}
|
|
151429
|
-
async function
|
|
151430
|
-
|
|
151431
|
-
|
|
151432
|
-
|
|
151433
|
-
|
|
151434
|
-
|
|
151435
|
-
|
|
151436
|
-
|
|
151437
|
-
|
|
151438
|
-
|
|
151439
|
-
|
|
151440
|
-
}
|
|
151441
|
-
const registered2 = await registerPubkey(existing.pubkey, options3.hostname ?? existing.hostname);
|
|
151442
|
-
const meta2 = {
|
|
151443
|
-
...existing,
|
|
151444
|
-
pubkey: registered2.pubkey,
|
|
151445
|
-
boundServers: [...bindings, { url: targetUrl, hostId: registered2.id }]
|
|
151446
|
-
};
|
|
151447
|
-
writeHostMetadata(meta2);
|
|
151448
|
-
success(`Bound existing host key to server '${serverName}' (${targetUrl}) \u2014 no rotation.`);
|
|
151449
|
-
data({
|
|
151450
|
-
hostId: registered2.id,
|
|
151451
|
-
hostname: meta2.hostname,
|
|
151452
|
-
pubkey: meta2.pubkey,
|
|
151453
|
-
boundServers: meta2.boundServers,
|
|
151454
|
-
rotated: false
|
|
151455
|
-
});
|
|
151456
|
-
return;
|
|
151670
|
+
async function validateAuthKey(statusInfo, client) {
|
|
151671
|
+
try {
|
|
151672
|
+
const auth = await client.auth.validate();
|
|
151673
|
+
statusInfo.keyValid = auth.valid;
|
|
151674
|
+
statusInfo.keyName = auth.keyName;
|
|
151675
|
+
statusInfo.keyPrefix = auth.keyPrefix;
|
|
151676
|
+
statusInfo.scopes = auth.scopes;
|
|
151677
|
+
Object.assign(statusInfo, credentialStatusFields(auth));
|
|
151678
|
+
Object.assign(statusInfo, serverPostureFields(auth));
|
|
151679
|
+
} catch {
|
|
151680
|
+
statusInfo.keyValid = false;
|
|
151457
151681
|
}
|
|
151458
|
-
|
|
151459
|
-
|
|
151460
|
-
const
|
|
151461
|
-
const
|
|
151462
|
-
|
|
151463
|
-
|
|
151464
|
-
|
|
151465
|
-
|
|
151466
|
-
|
|
151682
|
+
}
|
|
151683
|
+
async function collectStatusInfo() {
|
|
151684
|
+
const config2 = loadConfig();
|
|
151685
|
+
const isAuthenticated = hasAuth();
|
|
151686
|
+
const apiUrl = config2.apiUrl ?? "http://localhost:8882";
|
|
151687
|
+
const statusInfo = {
|
|
151688
|
+
configDir: getConfigDir(),
|
|
151689
|
+
apiUrl,
|
|
151690
|
+
authenticated: isAuthenticated,
|
|
151691
|
+
defaultInstance: config2.defaultInstance ?? "-",
|
|
151692
|
+
format: config2.format ?? "auto (TTY detection)"
|
|
151467
151693
|
};
|
|
151468
|
-
|
|
151469
|
-
|
|
151470
|
-
|
|
151471
|
-
|
|
151472
|
-
|
|
151473
|
-
|
|
151474
|
-
|
|
151475
|
-
|
|
151476
|
-
|
|
151477
|
-
|
|
151694
|
+
const serverVersion = await checkApiHealth(statusInfo, apiUrl);
|
|
151695
|
+
if (isAuthenticated) {
|
|
151696
|
+
const client = getOptionalClient();
|
|
151697
|
+
if (client) {
|
|
151698
|
+
await validateAuthKey(statusInfo, client);
|
|
151699
|
+
}
|
|
151700
|
+
}
|
|
151701
|
+
const processes = await getProcessStatus();
|
|
151702
|
+
if (processes !== null) {
|
|
151703
|
+
statusInfo.processes = processes;
|
|
151704
|
+
}
|
|
151705
|
+
return { statusInfo, serverVersion, processes };
|
|
151478
151706
|
}
|
|
151479
|
-
|
|
151480
|
-
|
|
151481
|
-
|
|
151482
|
-
|
|
151483
|
-
|
|
151484
|
-
|
|
151485
|
-
|
|
151707
|
+
function renderHumanExtras(serverVersion, processes) {
|
|
151708
|
+
if (serverVersion) {
|
|
151709
|
+
raw(formatStatusVersionHint(VERSION, serverVersion));
|
|
151710
|
+
}
|
|
151711
|
+
if (processes !== null && processes.length > 0) {
|
|
151712
|
+
header("Services");
|
|
151713
|
+
list(processes);
|
|
151714
|
+
} else if (processes !== null) {
|
|
151715
|
+
header("Services");
|
|
151716
|
+
dim("No omni services running. Start with: omni start");
|
|
151486
151717
|
}
|
|
151487
151718
|
}
|
|
151488
|
-
function
|
|
151489
|
-
const
|
|
151490
|
-
|
|
151491
|
-
|
|
151492
|
-
|
|
151493
|
-
|
|
151494
|
-
|
|
151495
|
-
|
|
151496
|
-
|
|
151497
|
-
trust.command("update <id>").description("Replace a genie host scopes (comma-separated)").requiredOption("--scope <list>", 'Comma-separated scope list, e.g. "agents:write,providers:write"').action(handleUpdate3);
|
|
151498
|
-
trust.command("revoke <id>").description("Revoke a genie host (irreversible \u2014 re-register with a fresh keypair to restore trust)").action(handleRevoke2);
|
|
151499
|
-
return trust;
|
|
151719
|
+
function createStatusCommand() {
|
|
151720
|
+
const status = new Command("status").description("Show API health and connection info").action(async () => {
|
|
151721
|
+
const { statusInfo, serverVersion, processes } = await collectStatusInfo();
|
|
151722
|
+
data(statusInfo);
|
|
151723
|
+
if (getCurrentFormat() === "human") {
|
|
151724
|
+
renderHumanExtras(serverVersion, processes);
|
|
151725
|
+
}
|
|
151726
|
+
});
|
|
151727
|
+
return status;
|
|
151500
151728
|
}
|
|
151501
151729
|
|
|
151502
|
-
// src/commands/
|
|
151730
|
+
// src/commands/stop.ts
|
|
151503
151731
|
init_output();
|
|
151504
|
-
|
|
151505
|
-
|
|
151506
|
-
|
|
151507
|
-
|
|
151508
|
-
|
|
151509
|
-
|
|
151510
|
-
|
|
151511
|
-
|
|
151512
|
-
|
|
151513
|
-
|
|
151514
|
-
|
|
151515
|
-
|
|
151516
|
-
|
|
151517
|
-
|
|
151518
|
-
|
|
151519
|
-
|
|
151520
|
-
} catch (err2) {
|
|
151521
|
-
const message2 = err2 instanceof Error ? err2.message : "Unknown error";
|
|
151522
|
-
error(`Failed to list voices: ${message2}`);
|
|
151523
|
-
}
|
|
151524
|
-
});
|
|
151525
|
-
return tts;
|
|
151732
|
+
var LEGACY_PROCESSES = ["omni-v2-api", "omni-v2-nats"];
|
|
151733
|
+
async function runStop() {
|
|
151734
|
+
if (!await isPm2Available()) {
|
|
151735
|
+
pm2NotFoundError();
|
|
151736
|
+
}
|
|
151737
|
+
info("Stopping omni services...");
|
|
151738
|
+
for (const name of Object.values(PM2_PROCESSES)) {
|
|
151739
|
+
await runPm2(["delete", name]);
|
|
151740
|
+
}
|
|
151741
|
+
for (const name of LEGACY_PROCESSES) {
|
|
151742
|
+
await runPm2(["delete", name]);
|
|
151743
|
+
}
|
|
151744
|
+
success("Omni services stopped");
|
|
151745
|
+
}
|
|
151746
|
+
function createStopCommand() {
|
|
151747
|
+
return new Command("stop").description("Stop all omni PM2 processes").action(runStop);
|
|
151526
151748
|
}
|
|
151527
151749
|
|
|
151528
|
-
// src/commands/
|
|
151750
|
+
// src/commands/tenants.ts
|
|
151529
151751
|
init_output();
|
|
151530
|
-
|
|
151531
|
-
|
|
151532
|
-
|
|
151533
|
-
|
|
151534
|
-
|
|
151535
|
-
|
|
151536
|
-
|
|
151537
|
-
|
|
151538
|
-
|
|
151752
|
+
var TENANT_ROLES = ["tenant-owner", "tenant-admin", "tenant-operator", "tenant-viewer"];
|
|
151753
|
+
var MEMBERSHIP_STATUSES = ["active", "disabled"];
|
|
151754
|
+
var REASON_REQUIRED_HINT = "Every platform control-plane call is audited; " + 'provide a justification of 3-500 characters, e.g. --reason "onboarding request OPS-1421".';
|
|
151755
|
+
var CONTROL_PLANE_UNAVAILABLE = "the platform control plane is not available on this server. " + "Either multitenancy is disabled (start the API with OMNI_MULTITENANCY_ENABLED=true) " + "or the server predates the tenant control plane \u2014 upgrade it.";
|
|
151756
|
+
var CREDENTIAL_REJECTED_401 = "the server rejected the credential (401). " + "The platform control plane accepts only PLATFORM-class credentials \u2014 legacy admin/god keys and " + "tenant data-plane keys are denied by design. Authenticate with a platform key: " + "omni auth login --api-key <platform-key>.";
|
|
151757
|
+
var CREDENTIAL_FORBIDDEN_403 = "the credential authenticated but is not allowed (403). " + "A PLATFORM-class credential with the matching platform:* scope is required \u2014 tenant-class " + "credentials can never reach the control plane, and platform keys act only within their scopes.";
|
|
151758
|
+
function requireReason(reason, action) {
|
|
151759
|
+
const trimmed = reason?.trim() ?? "";
|
|
151760
|
+
if (trimmed.length < 3 || trimmed.length > 500) {
|
|
151761
|
+
error(`--reason is required to ${action}. ${REASON_REQUIRED_HINT}`, undefined, 1);
|
|
151762
|
+
}
|
|
151763
|
+
return trimmed;
|
|
151539
151764
|
}
|
|
151540
|
-
function
|
|
151541
|
-
|
|
151542
|
-
|
|
151543
|
-
const client = getClient();
|
|
151544
|
-
const result = await client.turns.list({
|
|
151545
|
-
status: opts.status,
|
|
151546
|
-
instanceId: opts.instance,
|
|
151547
|
-
chatId: opts.chat,
|
|
151548
|
-
agentId: opts.agent,
|
|
151549
|
-
limit: Number(opts.limit),
|
|
151550
|
-
offset: Number(opts.offset)
|
|
151551
|
-
});
|
|
151552
|
-
const rows = result.items.map((t) => ({
|
|
151553
|
-
id: t.id.slice(0, 8),
|
|
151554
|
-
status: t.status,
|
|
151555
|
-
chat: t.chatId.length > 20 ? `${t.chatId.slice(0, 20)}\u2026` : t.chatId,
|
|
151556
|
-
agent: t.agentId.slice(0, 8),
|
|
151557
|
-
duration: formatDuration2(t.startedAt, t.closedAt),
|
|
151558
|
-
nudges: t.nudgeCount,
|
|
151559
|
-
msgs: t.messagesSent
|
|
151560
|
-
}));
|
|
151561
|
-
list(rows, {
|
|
151562
|
-
emptyMessage: "No turns found.",
|
|
151563
|
-
rawData: result.items
|
|
151564
|
-
});
|
|
151565
|
-
if (getCurrentFormat() !== "json") {
|
|
151566
|
-
dim(`Total: ${result.total} | Showing ${result.offset + 1}\u2013${result.offset + result.items.length}`);
|
|
151567
|
-
}
|
|
151568
|
-
});
|
|
151569
|
-
cmd.command("get").description("Get a single turn by ID").argument("<id>", "Turn ID").action(async (id) => {
|
|
151570
|
-
const client = getClient();
|
|
151571
|
-
const turn = await client.turns.get(id);
|
|
151572
|
-
data(turn);
|
|
151573
|
-
});
|
|
151574
|
-
cmd.command("close").description("Admin force-close a turn").argument("<id>", "Turn ID to close").option("--reason <text>", "Close reason").action(async (id, opts) => {
|
|
151575
|
-
const client = getClient();
|
|
151576
|
-
const result = await client.turns.forceClose(id, opts.reason);
|
|
151577
|
-
success(`Turn ${result.turnId} closed`, result);
|
|
151578
|
-
});
|
|
151579
|
-
cmd.command("close-all").description("Bulk close all open turns (requires --confirm)").option("--confirm", "Confirm bulk close").option("--reason <text>", "Close reason").action(async (opts) => {
|
|
151580
|
-
if (!opts.confirm) {
|
|
151581
|
-
error("Bulk close requires --confirm flag to proceed.");
|
|
151582
|
-
}
|
|
151583
|
-
const client = getClient();
|
|
151584
|
-
const result = await client.turns.bulkClose(opts.reason);
|
|
151585
|
-
success(result.message, result);
|
|
151586
|
-
});
|
|
151587
|
-
cmd.command("stats").description("Show aggregate turn metrics").action(async () => {
|
|
151588
|
-
const client = getClient();
|
|
151589
|
-
const stats = await client.turns.stats();
|
|
151590
|
-
data({
|
|
151591
|
-
openCount: stats.openCount,
|
|
151592
|
-
totalCount: stats.totalCount,
|
|
151593
|
-
avgDurationMs: stats.avgDurationMs,
|
|
151594
|
-
timeoutRate: `${(stats.timeoutRate * 100).toFixed(1)}%`
|
|
151595
|
-
});
|
|
151596
|
-
});
|
|
151597
|
-
return cmd;
|
|
151598
|
-
}
|
|
151599
|
-
|
|
151600
|
-
// src/commands/update.ts
|
|
151601
|
-
import { existsSync as existsSync20 } from "fs";
|
|
151602
|
-
import { join as join19 } from "path";
|
|
151603
|
-
import { createInterface as createInterface3 } from "readline";
|
|
151604
|
-
init_source();
|
|
151605
|
-
init_config();
|
|
151606
|
-
|
|
151607
|
-
// src/sidecar-cleanup.ts
|
|
151608
|
-
var SIDECAR_BASENAME = "nats-reply-sidecar.mjs";
|
|
151609
|
-
var SIDECAR_NAME_HINT = "nats-reply-sidecar";
|
|
151610
|
-
function pm2EntryToMatch(entry) {
|
|
151611
|
-
if (!entry || typeof entry !== "object")
|
|
151612
|
-
return null;
|
|
151613
|
-
const name = typeof entry.name === "string" ? entry.name : "";
|
|
151614
|
-
const scriptPath = typeof entry.pm2_env?.pm_exec_path === "string" ? entry.pm2_env.pm_exec_path : "";
|
|
151615
|
-
const pmId = typeof entry.pm_id === "number" ? entry.pm_id : null;
|
|
151616
|
-
const nameMatch = name.includes(SIDECAR_NAME_HINT);
|
|
151617
|
-
const pathMatch = scriptPath.endsWith(`/${SIDECAR_BASENAME}`) || scriptPath.endsWith(SIDECAR_BASENAME);
|
|
151618
|
-
if (!nameMatch && !pathMatch)
|
|
151619
|
-
return null;
|
|
151620
|
-
return {
|
|
151621
|
-
name,
|
|
151622
|
-
pmId,
|
|
151623
|
-
scriptPath,
|
|
151624
|
-
matchedBy: nameMatch ? "name" : "script-path"
|
|
151625
|
-
};
|
|
151626
|
-
}
|
|
151627
|
-
function parsePm2SidecarMatches(rawJson) {
|
|
151628
|
-
const trimmed = rawJson.trim();
|
|
151629
|
-
if (trimmed.length === 0 || trimmed === "[]")
|
|
151630
|
-
return [];
|
|
151631
|
-
let parsed;
|
|
151632
|
-
try {
|
|
151633
|
-
parsed = JSON.parse(trimmed);
|
|
151634
|
-
} catch {
|
|
151635
|
-
return [];
|
|
151636
|
-
}
|
|
151637
|
-
if (!Array.isArray(parsed))
|
|
151638
|
-
return [];
|
|
151639
|
-
const matches = [];
|
|
151640
|
-
for (const entry of parsed) {
|
|
151641
|
-
const match = pm2EntryToMatch(entry);
|
|
151642
|
-
if (match !== null)
|
|
151643
|
-
matches.push(match);
|
|
151765
|
+
function parseRole(value) {
|
|
151766
|
+
if (!TENANT_ROLES.includes(value)) {
|
|
151767
|
+
error(`Invalid --role "${value}". Valid roles: ${TENANT_ROLES.join(", ")}`, undefined, 1);
|
|
151644
151768
|
}
|
|
151645
|
-
return
|
|
151769
|
+
return value;
|
|
151646
151770
|
}
|
|
151647
|
-
function
|
|
151648
|
-
|
|
151649
|
-
`);
|
|
151650
|
-
const matches = [];
|
|
151651
|
-
for (const line3 of lines) {
|
|
151652
|
-
const trimmed = line3.trim();
|
|
151653
|
-
if (trimmed.length === 0)
|
|
151654
|
-
continue;
|
|
151655
|
-
const firstSpace = trimmed.indexOf(" ");
|
|
151656
|
-
if (firstSpace === -1)
|
|
151657
|
-
continue;
|
|
151658
|
-
const pidStr = trimmed.slice(0, firstSpace);
|
|
151659
|
-
const command = trimmed.slice(firstSpace + 1);
|
|
151660
|
-
const pid = Number.parseInt(pidStr, 10);
|
|
151661
|
-
if (!Number.isFinite(pid) || pid <= 0)
|
|
151662
|
-
continue;
|
|
151663
|
-
if (!command.includes(SIDECAR_BASENAME))
|
|
151664
|
-
continue;
|
|
151665
|
-
matches.push({ pid, command });
|
|
151771
|
+
function parseStatus(value) {
|
|
151772
|
+
if (!MEMBERSHIP_STATUSES.includes(value)) {
|
|
151773
|
+
error(`Invalid --status "${value}". Valid statuses: ${MEMBERSHIP_STATUSES.join(", ")}`, undefined, 1);
|
|
151666
151774
|
}
|
|
151667
|
-
return
|
|
151775
|
+
return value;
|
|
151668
151776
|
}
|
|
151669
|
-
|
|
151670
|
-
|
|
151671
|
-
|
|
151672
|
-
|
|
151673
|
-
stdout: "pipe",
|
|
151674
|
-
stderr: "pipe"
|
|
151675
|
-
});
|
|
151676
|
-
const [stdout, exitCode] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
|
151677
|
-
if (exitCode !== 0)
|
|
151678
|
-
return [];
|
|
151679
|
-
return parsePm2SidecarMatches(stdout);
|
|
151680
|
-
} catch {
|
|
151681
|
-
return [];
|
|
151777
|
+
function parseScopes(value) {
|
|
151778
|
+
const scopes = value.split(",").map((s2) => s2.trim()).filter((s2) => s2.length > 0);
|
|
151779
|
+
if (scopes.length === 0) {
|
|
151780
|
+
error('--scopes must list at least one explicit tenant scope (comma-separated), e.g. --scopes "messages:send,chats:read".', undefined, 1);
|
|
151682
151781
|
}
|
|
151782
|
+
return scopes;
|
|
151683
151783
|
}
|
|
151684
|
-
|
|
151784
|
+
function parseConstraints(value) {
|
|
151785
|
+
const invalid = () => error(`Invalid --constraints "${value}". Expected a JSON object of string arrays, e.g. '{"instanceAllowlist":["<instance-id>"]}'.`, undefined, 1);
|
|
151786
|
+
let parsed;
|
|
151685
151787
|
try {
|
|
151686
|
-
|
|
151687
|
-
cmd: ["pgrep", "-fa", SIDECAR_BASENAME],
|
|
151688
|
-
stdout: "pipe",
|
|
151689
|
-
stderr: "pipe"
|
|
151690
|
-
});
|
|
151691
|
-
const [stdout, exitCode] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
|
151692
|
-
if (exitCode !== 0 && exitCode !== 1)
|
|
151693
|
-
return [];
|
|
151694
|
-
const matches = parseRawSidecarMatches(stdout);
|
|
151695
|
-
const ownPid = process.pid;
|
|
151696
|
-
return matches.filter((m10) => m10.pid !== ownPid);
|
|
151788
|
+
parsed = JSON.parse(value);
|
|
151697
151789
|
} catch {
|
|
151698
|
-
|
|
151790
|
+
invalid();
|
|
151699
151791
|
}
|
|
151700
|
-
|
|
151701
|
-
|
|
151702
|
-
const target = match.name.length > 0 ? match.name : match.pmId !== null ? String(match.pmId) : null;
|
|
151703
|
-
if (target === null)
|
|
151704
|
-
return false;
|
|
151705
|
-
for (const verb of ["stop", "delete"]) {
|
|
151706
|
-
const proc = Bun.spawn({
|
|
151707
|
-
cmd: ["pm2", verb, target],
|
|
151708
|
-
stdout: "pipe",
|
|
151709
|
-
stderr: "pipe"
|
|
151710
|
-
});
|
|
151711
|
-
const exitCode = await proc.exited;
|
|
151712
|
-
if (exitCode !== 0)
|
|
151713
|
-
return false;
|
|
151792
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
151793
|
+
invalid();
|
|
151714
151794
|
}
|
|
151715
|
-
|
|
151716
|
-
|
|
151717
|
-
|
|
151718
|
-
|
|
151719
|
-
|
|
151720
|
-
return true;
|
|
151721
|
-
} catch {
|
|
151722
|
-
return false;
|
|
151795
|
+
const record2 = parsed;
|
|
151796
|
+
for (const entry of Object.values(record2)) {
|
|
151797
|
+
if (!Array.isArray(entry) || entry.some((item) => typeof item !== "string")) {
|
|
151798
|
+
invalid();
|
|
151799
|
+
}
|
|
151723
151800
|
}
|
|
151801
|
+
return record2;
|
|
151724
151802
|
}
|
|
151725
|
-
|
|
151726
|
-
|
|
151727
|
-
|
|
151728
|
-
|
|
151729
|
-
|
|
151730
|
-
|
|
151731
|
-
|
|
151732
|
-
|
|
151733
|
-
|
|
151734
|
-
|
|
151803
|
+
function renderPlatformError(err2, action) {
|
|
151804
|
+
if (err2 instanceof OmniApiError) {
|
|
151805
|
+
if (err2.status === 404 && err2.message.startsWith("Endpoint not found")) {
|
|
151806
|
+
error(`Failed to ${action}: ${CONTROL_PLANE_UNAVAILABLE}`, undefined, 1);
|
|
151807
|
+
}
|
|
151808
|
+
if (err2.status === 401) {
|
|
151809
|
+
error(`Failed to ${action}: ${CREDENTIAL_REJECTED_401}`, undefined, 1);
|
|
151810
|
+
}
|
|
151811
|
+
if (err2.status === 403) {
|
|
151812
|
+
error(`Failed to ${action}: ${CREDENTIAL_FORBIDDEN_403}`, undefined, 1);
|
|
151735
151813
|
}
|
|
151736
151814
|
}
|
|
151737
|
-
const
|
|
151738
|
-
|
|
151739
|
-
|
|
151740
|
-
|
|
151741
|
-
|
|
151742
|
-
|
|
151743
|
-
|
|
151744
|
-
|
|
151815
|
+
const message2 = err2 instanceof Error ? err2.message : "Unknown error";
|
|
151816
|
+
error(`Failed to ${action}: ${message2}`);
|
|
151817
|
+
}
|
|
151818
|
+
var TENANT_NOT_ACTIVE_409 = "the tenant is not active (409). Root keys can be issued only while the tenant is active \u2014 " + "a suspended or archived tenant refuses new credentials.";
|
|
151819
|
+
var ROOT_KEY_EXCEEDS_POLICY_403 = "the request exceeds the tenant policy ceilings (403). Keep --expires within the tenant " + "max-key-ttl and --rate-limit/--budget at or below the tenant ceilings (see: omni tenants get <id>).";
|
|
151820
|
+
function renderRootKeyError(err2, action) {
|
|
151821
|
+
if (err2 instanceof OmniApiError) {
|
|
151822
|
+
if (err2.status === 409) {
|
|
151823
|
+
error(`Failed to ${action}: ${TENANT_NOT_ACTIVE_409}`, undefined, 1);
|
|
151824
|
+
}
|
|
151825
|
+
if (err2.status === 400) {
|
|
151826
|
+
error(`Failed to ${action}: ${err2.message}`, undefined, 1);
|
|
151827
|
+
}
|
|
151828
|
+
if (err2.status === 403 && err2.message.includes("exceeds tenant policy")) {
|
|
151829
|
+
error(`Failed to ${action}: ${ROOT_KEY_EXCEEDS_POLICY_403}`, undefined, 1);
|
|
151745
151830
|
}
|
|
151746
151831
|
}
|
|
151832
|
+
renderPlatformError(err2, action);
|
|
151833
|
+
}
|
|
151834
|
+
function formatTenantRow(tenant) {
|
|
151747
151835
|
return {
|
|
151748
|
-
|
|
151749
|
-
|
|
151750
|
-
|
|
151751
|
-
|
|
151752
|
-
|
|
151753
|
-
|
|
151836
|
+
id: tenant.id,
|
|
151837
|
+
slug: tenant.slug,
|
|
151838
|
+
name: tenant.displayName,
|
|
151839
|
+
status: tenant.status,
|
|
151840
|
+
keyTtl: tenant.maxKeyTtlSeconds,
|
|
151841
|
+
keyRate: tenant.maxKeyRateLimit,
|
|
151842
|
+
keyBudget: tenant.maxKeyBudget,
|
|
151843
|
+
created: new Date(tenant.createdAt).toLocaleDateString()
|
|
151754
151844
|
};
|
|
151755
151845
|
}
|
|
151756
|
-
function
|
|
151757
|
-
const totalDetected = result.pm2Detected.length + result.rawDetected.length;
|
|
151758
|
-
if (totalDetected === 0)
|
|
151759
|
-
return "";
|
|
151760
|
-
const lines = [];
|
|
151761
|
-
lines.push(`Found ${totalDetected} legacy nats-reply-sidecar process(es) \u2014 stopping to prevent duplicate replies.`);
|
|
151762
|
-
for (const match of result.pm2Stopped) {
|
|
151763
|
-
const label = match.name || `pm_id=${match.pmId}`;
|
|
151764
|
-
lines.push(` \u2713 pm2: stopped and deleted "${label}" (matched by ${match.matchedBy})`);
|
|
151765
|
-
}
|
|
151766
|
-
for (const match of result.pm2Failed) {
|
|
151767
|
-
const label = match.name || `pm_id=${match.pmId}`;
|
|
151768
|
-
lines.push(` \u2717 pm2: failed to stop "${label}" \u2014 run \`pm2 stop ${label} && pm2 delete ${label}\` manually`);
|
|
151769
|
-
}
|
|
151770
|
-
for (const match of result.rawKilled) {
|
|
151771
|
-
lines.push(` \u2713 raw: SIGTERM sent to pid ${match.pid}`);
|
|
151772
|
-
}
|
|
151773
|
-
for (const match of result.rawFailed) {
|
|
151774
|
-
lines.push(` \u2717 raw: failed to signal pid ${match.pid} \u2014 kill it manually with \`kill ${match.pid}\``);
|
|
151775
|
-
}
|
|
151776
|
-
if (result.pm2Failed.length > 0 || result.rawFailed.length > 0) {
|
|
151777
|
-
lines.push("See docs/migration/nats-genie-sidecar-decommission.md for the manual cleanup runbook.");
|
|
151778
|
-
}
|
|
151779
|
-
return lines.join(`
|
|
151780
|
-
`);
|
|
151781
|
-
}
|
|
151782
|
-
|
|
151783
|
-
// src/legacy-cleanup.ts
|
|
151784
|
-
function createNatsReplySidecarArtifact() {
|
|
151785
|
-
let lastResult = null;
|
|
151846
|
+
function formatMembershipRow(membership) {
|
|
151786
151847
|
return {
|
|
151787
|
-
|
|
151788
|
-
|
|
151789
|
-
|
|
151790
|
-
|
|
151791
|
-
|
|
151792
|
-
|
|
151793
|
-
lastResult = result;
|
|
151794
|
-
const removed = [];
|
|
151795
|
-
const warnings = [];
|
|
151796
|
-
for (const m10 of result.pm2Stopped) {
|
|
151797
|
-
removed.push(`pm2:${m10.name.length > 0 ? m10.name : `pm_id=${m10.pmId}`}`);
|
|
151798
|
-
}
|
|
151799
|
-
for (const m10 of result.rawKilled) {
|
|
151800
|
-
removed.push(`pid:${m10.pid}`);
|
|
151801
|
-
}
|
|
151802
|
-
for (const m10 of result.pm2Failed) {
|
|
151803
|
-
warnings.push(`pm2-failed:${m10.name.length > 0 ? m10.name : `pm_id=${m10.pmId}`}`);
|
|
151804
|
-
}
|
|
151805
|
-
for (const m10 of result.rawFailed) {
|
|
151806
|
-
warnings.push(`raw-failed:pid=${m10.pid}`);
|
|
151807
|
-
}
|
|
151808
|
-
return { removed, warnings };
|
|
151809
|
-
},
|
|
151810
|
-
summary() {
|
|
151811
|
-
return lastResult === null ? "" : formatCleanupSummary(lastResult);
|
|
151812
|
-
}
|
|
151848
|
+
id: membership.id,
|
|
151849
|
+
principal: membership.principalId,
|
|
151850
|
+
role: membership.role,
|
|
151851
|
+
status: membership.status,
|
|
151852
|
+
invitedBy: membership.invitedByPrincipalId ?? "-",
|
|
151853
|
+
created: new Date(membership.createdAt).toLocaleDateString()
|
|
151813
151854
|
};
|
|
151814
151855
|
}
|
|
151815
|
-
|
|
151816
|
-
|
|
151817
|
-
|
|
151856
|
+
async function handleList5(client, options3) {
|
|
151857
|
+
const reason = requireReason(options3.reason, "list tenants");
|
|
151858
|
+
const result = await client.platform.tenants.list(reason);
|
|
151859
|
+
list(result.items.map(formatTenantRow), {
|
|
151860
|
+
emptyMessage: "No tenants found.",
|
|
151861
|
+
rawData: result.items
|
|
151862
|
+
});
|
|
151818
151863
|
}
|
|
151819
|
-
function
|
|
151820
|
-
const
|
|
151821
|
-
|
|
151822
|
-
|
|
151823
|
-
state: "errored",
|
|
151824
|
-
removed: [],
|
|
151825
|
-
warnings: [`${phase}-threw:${message2}`],
|
|
151826
|
-
summary: "",
|
|
151827
|
-
error: message2
|
|
151828
|
-
};
|
|
151864
|
+
async function handleGet3(client, id, options3) {
|
|
151865
|
+
const reason = requireReason(options3.reason, "read a tenant");
|
|
151866
|
+
const tenant = await client.platform.tenants.get(id, reason);
|
|
151867
|
+
data(tenant);
|
|
151829
151868
|
}
|
|
151830
|
-
async function
|
|
151831
|
-
|
|
151832
|
-
|
|
151833
|
-
|
|
151834
|
-
|
|
151835
|
-
|
|
151836
|
-
|
|
151837
|
-
|
|
151838
|
-
|
|
151839
|
-
|
|
151840
|
-
|
|
151841
|
-
|
|
151842
|
-
warnings: [],
|
|
151843
|
-
summary: ""
|
|
151844
|
-
};
|
|
151845
|
-
}
|
|
151846
|
-
try {
|
|
151847
|
-
const { removed, warnings } = await artifact.cleanup();
|
|
151848
|
-
return {
|
|
151849
|
-
name: artifact.name,
|
|
151850
|
-
state: "ran",
|
|
151851
|
-
removed,
|
|
151852
|
-
warnings,
|
|
151853
|
-
summary: artifact.summary()
|
|
151854
|
-
};
|
|
151855
|
-
} catch (error2) {
|
|
151856
|
-
return erroredOutcome(artifact.name, "cleanup", error2);
|
|
151857
|
-
}
|
|
151869
|
+
async function handleCreate4(client, options3) {
|
|
151870
|
+
const reason = requireReason(options3.reason, "create a tenant");
|
|
151871
|
+
const tenant = await client.platform.tenants.create({
|
|
151872
|
+
slug: options3.slug,
|
|
151873
|
+
displayName: options3.name,
|
|
151874
|
+
maxKeyTtlSeconds: options3.maxKeyTtl,
|
|
151875
|
+
maxKeyRateLimit: options3.maxKeyRate,
|
|
151876
|
+
maxKeyBudget: options3.maxKeyBudget,
|
|
151877
|
+
reason
|
|
151878
|
+
});
|
|
151879
|
+
success(`Tenant created: ${tenant.slug} (${tenant.id})`);
|
|
151880
|
+
data(tenant);
|
|
151858
151881
|
}
|
|
151859
|
-
async function
|
|
151860
|
-
const
|
|
151861
|
-
const
|
|
151862
|
-
|
|
151863
|
-
|
|
151864
|
-
skipped.push(artifact.name);
|
|
151865
|
-
outcomes.push({
|
|
151866
|
-
name: artifact.name,
|
|
151867
|
-
state: "skipped",
|
|
151868
|
-
removed: [],
|
|
151869
|
-
warnings: [],
|
|
151870
|
-
summary: ""
|
|
151871
|
-
});
|
|
151872
|
-
continue;
|
|
151873
|
-
}
|
|
151874
|
-
outcomes.push(await runArtifact(artifact));
|
|
151875
|
-
}
|
|
151876
|
-
const succeeded = outcomes.every((o2) => o2.state !== "errored" && (o2.state !== "ran" || o2.warnings.length === 0));
|
|
151877
|
-
return { outcomes, succeeded, skipped };
|
|
151882
|
+
async function handleSuspend(client, id, options3) {
|
|
151883
|
+
const reason = requireReason(options3.reason, "suspend a tenant");
|
|
151884
|
+
const tenant = await client.platform.tenants.suspend(id, reason);
|
|
151885
|
+
success(`Tenant suspended: ${tenant.slug} (${tenant.id})`);
|
|
151886
|
+
info(`Reason: ${reason}`);
|
|
151878
151887
|
}
|
|
151879
|
-
|
|
151880
|
-
|
|
151881
|
-
|
|
151882
|
-
|
|
151883
|
-
|
|
151884
|
-
|
|
151885
|
-
import { homedir as homedir13 } from "os";
|
|
151886
|
-
import { join as join18 } from "path";
|
|
151887
|
-
var UPDATE_DIAGNOSTICS_SCHEMA_VERSION = 1;
|
|
151888
|
-
function createDiagnostics(args) {
|
|
151889
|
-
const startedAt = new Date().toISOString();
|
|
151890
|
-
return {
|
|
151891
|
-
schemaVersion: UPDATE_DIAGNOSTICS_SCHEMA_VERSION,
|
|
151892
|
-
startedAt,
|
|
151893
|
-
finishedAt: startedAt,
|
|
151894
|
-
cli: { runningVersion: args.runningVersion, channel: args.channel },
|
|
151895
|
-
registry: { latestVersion: null },
|
|
151896
|
-
preflight: { ran: false, blocked: false },
|
|
151897
|
-
install: { attempted: false, succeeded: null, targetVersion: null },
|
|
151898
|
-
restart: { attempted: false, succeeded: null, services: [] },
|
|
151899
|
-
verify: null,
|
|
151900
|
-
cleanups: null,
|
|
151901
|
-
maintenance: null,
|
|
151902
|
-
parallelNpmGlobal: null,
|
|
151903
|
-
recentLogSignals: [],
|
|
151904
|
-
exitCode: 0
|
|
151905
|
-
};
|
|
151888
|
+
async function handleArchive(client, id, options3) {
|
|
151889
|
+
const reason = requireReason(options3.reason, "archive a tenant");
|
|
151890
|
+
const tenant = await client.platform.tenants.archive(id, reason);
|
|
151891
|
+
success(`Tenant archived: ${tenant.slug} (${tenant.id})`);
|
|
151892
|
+
warn("Archived is terminal \u2014 there is no un-archive and no hard delete.");
|
|
151893
|
+
info(`Reason: ${reason}`);
|
|
151906
151894
|
}
|
|
151907
|
-
function
|
|
151908
|
-
const
|
|
151909
|
-
|
|
151895
|
+
async function handleMembershipsList(client, tenantId, options3) {
|
|
151896
|
+
const reason = requireReason(options3.reason, "list memberships");
|
|
151897
|
+
const result = await client.platform.tenants.memberships.list(tenantId, reason);
|
|
151898
|
+
list(result.items.map(formatMembershipRow), {
|
|
151899
|
+
emptyMessage: "No memberships found.",
|
|
151900
|
+
rawData: result.items
|
|
151901
|
+
});
|
|
151910
151902
|
}
|
|
151911
|
-
function
|
|
151912
|
-
const
|
|
151913
|
-
|
|
151903
|
+
async function handleMembershipsAdd(client, tenantId, options3) {
|
|
151904
|
+
const reason = requireReason(options3.reason, "attach a membership");
|
|
151905
|
+
const role = parseRole(options3.role);
|
|
151906
|
+
const membership = await client.platform.tenants.memberships.attach(tenantId, {
|
|
151907
|
+
principalId: options3.principal,
|
|
151908
|
+
role,
|
|
151909
|
+
reason
|
|
151910
|
+
});
|
|
151911
|
+
success(`Membership attached: ${membership.principalId} as ${membership.role} (${membership.id})`);
|
|
151914
151912
|
}
|
|
151915
|
-
function
|
|
151916
|
-
|
|
151917
|
-
|
|
151918
|
-
|
|
151919
|
-
|
|
151920
|
-
|
|
151921
|
-
|
|
151922
|
-
|
|
151923
|
-
|
|
151924
|
-
|
|
151925
|
-
|
|
151913
|
+
async function handleMembershipsDisable(client, tenantId, membershipId, options3) {
|
|
151914
|
+
const reason = requireReason(options3.reason, "disable a membership");
|
|
151915
|
+
const membership = await client.platform.tenants.memberships.disable(tenantId, membershipId, reason);
|
|
151916
|
+
success(`Membership disabled: ${membership.id}`);
|
|
151917
|
+
info(`Reason: ${reason}`);
|
|
151918
|
+
}
|
|
151919
|
+
async function handleMembershipsStatus(client, tenantId, membershipId, options3) {
|
|
151920
|
+
const reason = requireReason(options3.reason, "set a membership status");
|
|
151921
|
+
const status = parseStatus(options3.status);
|
|
151922
|
+
const membership = await client.platform.tenants.memberships.setStatus(tenantId, membershipId, status, reason);
|
|
151923
|
+
success(`Membership ${membership.id} status set to ${membership.status}`);
|
|
151924
|
+
}
|
|
151925
|
+
async function handleMembershipsRole(client, tenantId, membershipId, options3) {
|
|
151926
|
+
const reason = requireReason(options3.reason, "set a membership role");
|
|
151927
|
+
const role = parseRole(options3.role);
|
|
151928
|
+
const membership = await client.platform.tenants.memberships.setRole(tenantId, membershipId, role, reason);
|
|
151929
|
+
success(`Membership ${membership.id} role set to ${membership.role}`);
|
|
151930
|
+
}
|
|
151931
|
+
async function handleKeysIssueRoot(client, tenantId, options3) {
|
|
151932
|
+
const reason = requireReason(options3.reason, "issue a root key");
|
|
151933
|
+
const role = parseRole(options3.role);
|
|
151934
|
+
const scopes = parseScopes(options3.scopes);
|
|
151935
|
+
const constraints = options3.constraints === undefined ? undefined : parseConstraints(options3.constraints);
|
|
151936
|
+
const body = {
|
|
151937
|
+
principalId: options3.principal,
|
|
151938
|
+
membershipId: options3.membership,
|
|
151939
|
+
role,
|
|
151940
|
+
name: options3.name,
|
|
151941
|
+
scopes,
|
|
151942
|
+
expiresAt: options3.expires,
|
|
151943
|
+
rateLimit: options3.rateLimit,
|
|
151944
|
+
budget: options3.budget,
|
|
151945
|
+
...constraints ? { resourceConstraints: constraints } : {},
|
|
151946
|
+
reason
|
|
151947
|
+
};
|
|
151948
|
+
const result = await client.platform.tenants.keys.issueRoot(tenantId, body);
|
|
151949
|
+
if (getCurrentFormat() === "json") {
|
|
151950
|
+
warn("plainTextKey is shown once and can never be retrieved again \u2014 store it now.");
|
|
151951
|
+
data(result);
|
|
151952
|
+
return;
|
|
151926
151953
|
}
|
|
151954
|
+
const { plainTextKey, ...issued } = result;
|
|
151955
|
+
success(`Root key issued: ${issued.name} (${issued.id})`);
|
|
151956
|
+
warn("The key below is shown ONCE and can never be retrieved again \u2014 store it now.");
|
|
151957
|
+
raw(`
|
|
151958
|
+
${plainTextKey}
|
|
151959
|
+
`);
|
|
151960
|
+
data(issued);
|
|
151927
151961
|
}
|
|
151928
|
-
|
|
151929
|
-
|
|
151930
|
-
|
|
151931
|
-
|
|
151932
|
-
const
|
|
151933
|
-
|
|
151934
|
-
|
|
151962
|
+
var REASON_HELP = "Audited justification (required, 3-500 chars)";
|
|
151963
|
+
function createTenantsCommand() {
|
|
151964
|
+
const tenants2 = new Command("tenants").description("Platform tenant control plane (requires a PLATFORM-class credential and OMNI_MULTITENANCY_ENABLED=true)");
|
|
151965
|
+
tenants2.command("list").description("List tenants, newest first (the read itself is audited)").option("--reason <text>", REASON_HELP).action(async (options3) => {
|
|
151966
|
+
const client = getClient();
|
|
151967
|
+
try {
|
|
151968
|
+
await handleList5(client, options3);
|
|
151969
|
+
} catch (err2) {
|
|
151970
|
+
renderPlatformError(err2, "list tenants");
|
|
151935
151971
|
}
|
|
151936
|
-
|
|
151937
|
-
|
|
151938
|
-
|
|
151972
|
+
});
|
|
151973
|
+
tenants2.command("get <id>").description("Get one tenant by id").option("--reason <text>", REASON_HELP).action(async (id, options3) => {
|
|
151974
|
+
const client = getClient();
|
|
151975
|
+
try {
|
|
151976
|
+
await handleGet3(client, id, options3);
|
|
151977
|
+
} catch (err2) {
|
|
151978
|
+
renderPlatformError(err2, "get the tenant");
|
|
151939
151979
|
}
|
|
151940
|
-
}
|
|
151941
|
-
|
|
151942
|
-
|
|
151943
|
-
|
|
151944
|
-
|
|
151945
|
-
|
|
151946
|
-
|
|
151947
|
-
state.recentLogSignals = collectRecentLogSignals();
|
|
151948
|
-
}
|
|
151949
|
-
const dir = getDiagnosticsDir();
|
|
151950
|
-
const path = getDiagnosticsPath(state.startedAt);
|
|
151951
|
-
try {
|
|
151952
|
-
if (!existsSync19(dir)) {
|
|
151953
|
-
mkdirSync12(dir, { recursive: true, mode: 448 });
|
|
151980
|
+
});
|
|
151981
|
+
tenants2.command("create").description("Create a tenant with its mandatory credential ceilings").requiredOption("--slug <slug>", "Immutable lowercase DNS-like slug (e.g. acme)").requiredOption("--name <name>", "Human-readable tenant name").requiredOption("--max-key-ttl <seconds>", "Ceiling for credential TTL, in seconds", Number.parseInt).requiredOption("--max-key-rate <n>", "Ceiling for credential rate limit", Number.parseInt).requiredOption("--max-key-budget <n>", "Ceiling for credential budget", Number.parseInt).option("--reason <text>", REASON_HELP).action(async (options3) => {
|
|
151982
|
+
const client = getClient();
|
|
151983
|
+
try {
|
|
151984
|
+
await handleCreate4(client, options3);
|
|
151985
|
+
} catch (err2) {
|
|
151986
|
+
renderPlatformError(err2, "create the tenant");
|
|
151954
151987
|
}
|
|
151955
|
-
|
|
151956
|
-
|
|
151957
|
-
|
|
151958
|
-
|
|
151959
|
-
|
|
151960
|
-
|
|
151988
|
+
});
|
|
151989
|
+
tenants2.command("suspend <id>").description("Suspend a tenant (bumps its revocation epoch, invalidating tenant credentials)").option("--reason <text>", REASON_HELP).action(async (id, options3) => {
|
|
151990
|
+
const client = getClient();
|
|
151991
|
+
try {
|
|
151992
|
+
await handleSuspend(client, id, options3);
|
|
151993
|
+
} catch (err2) {
|
|
151994
|
+
renderPlatformError(err2, "suspend the tenant");
|
|
151995
|
+
}
|
|
151996
|
+
});
|
|
151997
|
+
tenants2.command("archive <id>").description("Archive a tenant (TERMINAL \u2014 no un-archive, no hard delete)").option("--reason <text>", REASON_HELP).action(async (id, options3) => {
|
|
151998
|
+
const client = getClient();
|
|
151999
|
+
try {
|
|
152000
|
+
await handleArchive(client, id, options3);
|
|
152001
|
+
} catch (err2) {
|
|
152002
|
+
renderPlatformError(err2, "archive the tenant");
|
|
152003
|
+
}
|
|
152004
|
+
});
|
|
152005
|
+
const memberships = new Command("memberships").description("Manage tenant memberships");
|
|
152006
|
+
memberships.command("list <tenant-id>").description("List the memberships of one tenant (the read itself is audited)").option("--reason <text>", REASON_HELP).action(async (tenantId, options3) => {
|
|
152007
|
+
const client = getClient();
|
|
152008
|
+
try {
|
|
152009
|
+
await handleMembershipsList(client, tenantId, options3);
|
|
152010
|
+
} catch (err2) {
|
|
152011
|
+
renderPlatformError(err2, "list memberships");
|
|
152012
|
+
}
|
|
152013
|
+
});
|
|
152014
|
+
memberships.command("add <tenant-id>").description("Grant a principal a role in the tenant").requiredOption("--principal <uuid>", "Principal to grant access to").requiredOption("--role <role>", `Tenant role: ${TENANT_ROLES.join(" | ")}`).option("--reason <text>", REASON_HELP).action(async (tenantId, options3) => {
|
|
152015
|
+
const client = getClient();
|
|
152016
|
+
try {
|
|
152017
|
+
await handleMembershipsAdd(client, tenantId, options3);
|
|
152018
|
+
} catch (err2) {
|
|
152019
|
+
renderPlatformError(err2, "attach the membership");
|
|
152020
|
+
}
|
|
152021
|
+
});
|
|
152022
|
+
memberships.command("disable <tenant-id> <membership-id>").description("Disable a membership (memberships are never hard-deleted)").option("--reason <text>", REASON_HELP).action(async (tenantId, membershipId, options3) => {
|
|
152023
|
+
const client = getClient();
|
|
152024
|
+
try {
|
|
152025
|
+
await handleMembershipsDisable(client, tenantId, membershipId, options3);
|
|
152026
|
+
} catch (err2) {
|
|
152027
|
+
renderPlatformError(err2, "disable the membership");
|
|
152028
|
+
}
|
|
152029
|
+
});
|
|
152030
|
+
memberships.command("status <tenant-id> <membership-id>").description("Activate or disable an existing membership").requiredOption("--status <status>", `New status: ${MEMBERSHIP_STATUSES.join(" | ")}`).option("--reason <text>", REASON_HELP).action(async (tenantId, membershipId, options3) => {
|
|
152031
|
+
const client = getClient();
|
|
152032
|
+
try {
|
|
152033
|
+
await handleMembershipsStatus(client, tenantId, membershipId, options3);
|
|
152034
|
+
} catch (err2) {
|
|
152035
|
+
renderPlatformError(err2, "set the membership status");
|
|
152036
|
+
}
|
|
152037
|
+
});
|
|
152038
|
+
memberships.command("role <tenant-id> <membership-id>").description("Change the role an existing membership acts under").requiredOption("--role <role>", `New role: ${TENANT_ROLES.join(" | ")}`).option("--reason <text>", REASON_HELP).action(async (tenantId, membershipId, options3) => {
|
|
152039
|
+
const client = getClient();
|
|
152040
|
+
try {
|
|
152041
|
+
await handleMembershipsRole(client, tenantId, membershipId, options3);
|
|
152042
|
+
} catch (err2) {
|
|
152043
|
+
renderPlatformError(err2, "set the membership role");
|
|
152044
|
+
}
|
|
152045
|
+
});
|
|
152046
|
+
tenants2.addCommand(memberships);
|
|
152047
|
+
const keys = new Command("keys").description("Manage tenant credentials");
|
|
152048
|
+
keys.command("issue-root <tenant-id>").description("Issue the initial tenant ROOT key (the plaintext is shown ONCE and never again)").requiredOption("--principal <uuid>", "Principal the key acts as (must hold the membership)").requiredOption("--membership <uuid>", "Active membership binding the principal to the tenant").requiredOption("--role <role>", `Tenant role: ${TENANT_ROLES.join(" | ")}`).requiredOption("--name <name>", "Human-readable key name").requiredOption("--scopes <list>", "Comma-separated explicit tenant scopes (platform/wildcard scopes are refused)").requiredOption("--expires <iso>", "Expiry (ISO 8601); must be in the future and within the tenant TTL ceiling").requiredOption("--rate-limit <n>", "Rate limit; capped by the tenant policy ceiling", Number.parseInt).requiredOption("--budget <n>", "Budget; capped by the tenant policy ceiling", Number.parseInt).option("--constraints <json>", `Optional resource allowlists as JSON, e.g. '{"instanceAllowlist":["<id>"]}'`).option("--reason <text>", REASON_HELP).action(async (tenantId, options3) => {
|
|
152049
|
+
const client = getClient();
|
|
152050
|
+
try {
|
|
152051
|
+
await handleKeysIssueRoot(client, tenantId, options3);
|
|
152052
|
+
} catch (err2) {
|
|
152053
|
+
renderRootKeyError(err2, "issue the root key");
|
|
152054
|
+
}
|
|
152055
|
+
});
|
|
152056
|
+
tenants2.addCommand(keys);
|
|
152057
|
+
return tenants2;
|
|
151961
152058
|
}
|
|
151962
152059
|
|
|
151963
|
-
// src/commands/
|
|
151964
|
-
|
|
151965
|
-
|
|
151966
|
-
|
|
151967
|
-
function
|
|
151968
|
-
|
|
151969
|
-
return "next";
|
|
151970
|
-
if (options3.stable)
|
|
151971
|
-
return "latest";
|
|
151972
|
-
const saved = (config2 ?? loadConfig()).updateChannel;
|
|
151973
|
-
if (saved === "latest" || saved === "next")
|
|
151974
|
-
return saved;
|
|
151975
|
-
return "latest";
|
|
152060
|
+
// src/commands/trust.ts
|
|
152061
|
+
import { hostname as osHostname } from "os";
|
|
152062
|
+
init_config();
|
|
152063
|
+
init_output();
|
|
152064
|
+
function targetBaseUrl() {
|
|
152065
|
+
return normalizeServerUrl(loadConfig().apiUrl ?? "http://localhost:8882");
|
|
151976
152066
|
}
|
|
151977
|
-
function
|
|
151978
|
-
|
|
151979
|
-
|
|
151980
|
-
|
|
151981
|
-
|
|
151982
|
-
} catch {}
|
|
152067
|
+
function trustEndpoint(path) {
|
|
152068
|
+
if (!hasAuth()) {
|
|
152069
|
+
exitNotAuthenticated();
|
|
152070
|
+
}
|
|
152071
|
+
return `${targetBaseUrl()}/api/v2/trust${path}`;
|
|
151983
152072
|
}
|
|
151984
|
-
|
|
151985
|
-
|
|
151986
|
-
|
|
151987
|
-
|
|
151988
|
-
|
|
151989
|
-
stderr: "pipe"
|
|
151990
|
-
});
|
|
151991
|
-
const [stdout, exitCode] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
|
151992
|
-
if (exitCode !== 0) {
|
|
151993
|
-
return null;
|
|
151994
|
-
}
|
|
151995
|
-
const text3 = stdout.trim();
|
|
151996
|
-
return text3.length > 0 ? text3 : null;
|
|
151997
|
-
} catch {
|
|
151998
|
-
return null;
|
|
152073
|
+
function authHeaders() {
|
|
152074
|
+
const config2 = loadConfig();
|
|
152075
|
+
const headers = { "Content-Type": "application/json" };
|
|
152076
|
+
if (config2.apiKey) {
|
|
152077
|
+
headers.Authorization = `Bearer ${config2.apiKey}`;
|
|
151999
152078
|
}
|
|
152079
|
+
return headers;
|
|
152000
152080
|
}
|
|
152001
|
-
|
|
152002
|
-
|
|
152003
|
-
|
|
152004
|
-
|
|
152005
|
-
stdout: "pipe",
|
|
152006
|
-
stderr: "pipe"
|
|
152007
|
-
});
|
|
152008
|
-
if (result.exitCode !== 0) {
|
|
152009
|
-
return [];
|
|
152010
|
-
}
|
|
152011
|
-
const raw2 = new TextDecoder().decode(result.stdout).trim();
|
|
152012
|
-
if (!raw2 || raw2 === "[]")
|
|
152013
|
-
return [];
|
|
152014
|
-
const pm2Names = new Set(Object.values(PM2_PROCESSES));
|
|
152015
|
-
const list2 = JSON.parse(raw2);
|
|
152016
|
-
const running = new Set;
|
|
152017
|
-
for (const proc of list2) {
|
|
152018
|
-
const name = proc.name;
|
|
152019
|
-
if (name && pm2Names.has(name) && proc.pm2_env?.status === "online") {
|
|
152020
|
-
running.add(name);
|
|
152021
|
-
}
|
|
152022
|
-
}
|
|
152023
|
-
return [...running];
|
|
152024
|
-
} catch {
|
|
152025
|
-
return [];
|
|
152081
|
+
var _cachedSigningContext;
|
|
152082
|
+
function getSigningContextOnce() {
|
|
152083
|
+
if (_cachedSigningContext === undefined) {
|
|
152084
|
+
_cachedSigningContext = loadSigningContextForServer(targetBaseUrl());
|
|
152026
152085
|
}
|
|
152086
|
+
return _cachedSigningContext;
|
|
152027
152087
|
}
|
|
152028
|
-
async function
|
|
152029
|
-
const
|
|
152030
|
-
|
|
152031
|
-
|
|
152032
|
-
|
|
152033
|
-
|
|
152034
|
-
|
|
152088
|
+
async function callApi(method, path, body) {
|
|
152089
|
+
const url = trustEndpoint(path);
|
|
152090
|
+
const bodyString = body === undefined ? "" : JSON.stringify(body);
|
|
152091
|
+
const headers = authHeaders();
|
|
152092
|
+
const ctx = getSigningContextOnce();
|
|
152093
|
+
if (ctx && !path.startsWith("/handshake")) {
|
|
152094
|
+
const reqPath = new URL(url).pathname + new URL(url).search;
|
|
152095
|
+
const sigHeaders = ctx.signRequest(method, reqPath, bodyString);
|
|
152096
|
+
Object.assign(headers, sigHeaders);
|
|
152097
|
+
}
|
|
152098
|
+
const res = await fetch(url, {
|
|
152099
|
+
method,
|
|
152100
|
+
headers,
|
|
152101
|
+
body: body === undefined ? undefined : bodyString
|
|
152035
152102
|
});
|
|
152036
|
-
|
|
152037
|
-
|
|
152103
|
+
if (!res.ok) {
|
|
152104
|
+
const text3 = await res.text().catch(() => "");
|
|
152105
|
+
throw new Error(`HTTP ${res.status} ${res.statusText}${text3 ? `: ${text3}` : ""}`);
|
|
152106
|
+
}
|
|
152107
|
+
return await res.json();
|
|
152038
152108
|
}
|
|
152039
|
-
|
|
152040
|
-
|
|
152041
|
-
|
|
152042
|
-
|
|
152043
|
-
|
|
152044
|
-
|
|
152045
|
-
|
|
152046
|
-
|
|
152047
|
-
|
|
152048
|
-
|
|
152049
|
-
|
|
152109
|
+
function formatHostRow(host) {
|
|
152110
|
+
return {
|
|
152111
|
+
id: host.id.slice(0, 8),
|
|
152112
|
+
hostname: host.hostname,
|
|
152113
|
+
scopes: host.scopes.join(", ") || "(none)",
|
|
152114
|
+
pubkeyPrefix: `${host.pubkey.slice(0, 12)}\u2026`,
|
|
152115
|
+
lastSeen: host.lastSeenAt ? new Date(host.lastSeenAt).toISOString().replace("T", " ").slice(0, 16) : "never",
|
|
152116
|
+
status: host.revokedAt ? "revoked" : "active"
|
|
152117
|
+
};
|
|
152118
|
+
}
|
|
152119
|
+
async function handleList6() {
|
|
152120
|
+
try {
|
|
152121
|
+
const { items } = await callApi("GET", "/hosts");
|
|
152122
|
+
list(items.map(formatHostRow), {
|
|
152123
|
+
emptyMessage: "No genie hosts registered. Run `genie omni handshake` from a genie installation to register one.",
|
|
152124
|
+
rawData: items
|
|
152050
152125
|
});
|
|
152051
|
-
|
|
152052
|
-
|
|
152053
|
-
allSucceeded = false;
|
|
152054
|
-
}
|
|
152126
|
+
} catch (err2) {
|
|
152127
|
+
error(`Failed to list genie hosts: ${err2 instanceof Error ? err2.message : "Unknown error"}`);
|
|
152055
152128
|
}
|
|
152056
|
-
return allSucceeded;
|
|
152057
152129
|
}
|
|
152058
|
-
function
|
|
152059
|
-
|
|
152130
|
+
async function handleGet4(id) {
|
|
152131
|
+
try {
|
|
152132
|
+
const { data: data2 } = await callApi("GET", `/hosts/${encodeURIComponent(id)}`);
|
|
152133
|
+
data(data2);
|
|
152134
|
+
} catch (err2) {
|
|
152135
|
+
error(`Failed to get genie host: ${err2 instanceof Error ? err2.message : "Unknown error"}`);
|
|
152136
|
+
}
|
|
152060
152137
|
}
|
|
152061
|
-
function
|
|
152062
|
-
|
|
152063
|
-
|
|
152138
|
+
async function handleUpdate3(id, options3) {
|
|
152139
|
+
const scopes = options3.scope.split(",").map((s2) => s2.trim()).filter(Boolean);
|
|
152140
|
+
if (scopes.length === 0) {
|
|
152141
|
+
error("--scope must contain at least one scope (use `omni trust revoke <id>` to deny everything).");
|
|
152064
152142
|
}
|
|
152065
|
-
const cliVersion = normalizeVersion(args.latest);
|
|
152066
|
-
if (args.healthBody === null) {
|
|
152067
|
-
return { kind: "health-unreachable", apiPort: args.apiPort };
|
|
152068
|
-
}
|
|
152069
|
-
const serverVersion = args.healthBody.version ? normalizeVersion(args.healthBody.version) : null;
|
|
152070
|
-
if (!serverVersion || serverVersion !== cliVersion) {
|
|
152071
|
-
return { kind: "version-mismatch", cliVersion, serverVersion };
|
|
152072
|
-
}
|
|
152073
|
-
if (!args.keyValid) {
|
|
152074
|
-
return { kind: "auth-invalid" };
|
|
152075
|
-
}
|
|
152076
|
-
return { kind: "ok", cliVersion, serverVersion };
|
|
152077
|
-
}
|
|
152078
|
-
function updateErrorVersionMismatch(cli, server) {
|
|
152079
|
-
return `Server version mismatch: cli=v${cli} server=v${server ?? "unknown"}. Run: omni doctor`;
|
|
152080
|
-
}
|
|
152081
|
-
var UPDATE_ERROR_AUTH_INVALID = "Auth key invalid after restart. Run: omni doctor --fix";
|
|
152082
|
-
async function fetchHealthBody(apiPort, timeoutMs) {
|
|
152083
|
-
const url = getHealthCheckUrl(apiPort);
|
|
152084
|
-
const deadline = Date.now() + timeoutMs;
|
|
152085
|
-
while (Date.now() < deadline) {
|
|
152086
|
-
try {
|
|
152087
|
-
const resp = await fetch(url, {
|
|
152088
|
-
signal: AbortSignal.timeout(1500),
|
|
152089
|
-
headers: { "Accept-Encoding": "identity" }
|
|
152090
|
-
});
|
|
152091
|
-
if (resp.ok) {
|
|
152092
|
-
const body = await resp.json();
|
|
152093
|
-
return body;
|
|
152094
|
-
}
|
|
152095
|
-
} catch {}
|
|
152096
|
-
await Bun.sleep(VERIFY_POLL_INTERVAL_MS);
|
|
152097
|
-
}
|
|
152098
|
-
return null;
|
|
152099
|
-
}
|
|
152100
|
-
async function validateStoredKey(apiPort) {
|
|
152101
|
-
const cliConfig = loadLocalRuntimeConfig();
|
|
152102
|
-
if (!cliConfig.apiKey) {
|
|
152103
|
-
return false;
|
|
152104
|
-
}
|
|
152105
|
-
const baseUrl = cliConfig.apiUrl ?? `http://localhost:${apiPort}`;
|
|
152106
|
-
try {
|
|
152107
|
-
const client = createOmniClient({ baseUrl, apiKey: cliConfig.apiKey, cliVersion: VERSION });
|
|
152108
|
-
const result = await client.auth.validate();
|
|
152109
|
-
return result.valid === true;
|
|
152110
|
-
} catch {
|
|
152111
|
-
return false;
|
|
152112
|
-
}
|
|
152113
|
-
}
|
|
152114
|
-
function printVerifyBanner(latest) {
|
|
152115
|
-
console.log(`${source_default.green("\u2713")} CLI: v${latest}`);
|
|
152116
|
-
console.log(`${source_default.green("\u2713")} Server: v${latest} (healthy)`);
|
|
152117
|
-
console.log(`${source_default.green("\u2713")} Auth: key valid`);
|
|
152118
|
-
}
|
|
152119
|
-
function printVerifySkippedBanner(latest) {
|
|
152120
|
-
console.log(`${source_default.green("\u2713")} CLI: v${latest}`);
|
|
152121
|
-
console.log(`${source_default.yellow("-")} Server: v${latest} (skipped)`);
|
|
152122
|
-
console.log(`${source_default.yellow("-")} Auth: skipped`);
|
|
152123
|
-
}
|
|
152124
|
-
function detectParallelNpmGlobalInstall(deps) {
|
|
152125
|
-
const npmRootFn = deps?.npmRoot ?? defaultNpmRoot;
|
|
152126
|
-
const existsFn = deps?.exists ?? existsSync20;
|
|
152127
|
-
let root;
|
|
152128
152143
|
try {
|
|
152129
|
-
|
|
152130
|
-
|
|
152131
|
-
|
|
152132
|
-
}
|
|
152133
|
-
|
|
152134
|
-
return { detected: false, skipped: "npm-not-on-path" };
|
|
152135
|
-
}
|
|
152136
|
-
const candidate = join19(root, "@automagik", "omni");
|
|
152137
|
-
if (existsFn(candidate)) {
|
|
152138
|
-
return { detected: true, path: candidate };
|
|
152144
|
+
const { data: data2 } = await callApi("PATCH", `/hosts/${encodeURIComponent(id)}`, { scopes });
|
|
152145
|
+
success(`Updated genie host ${data2.id} scopes: ${data2.scopes.join(", ")}`);
|
|
152146
|
+
data(data2);
|
|
152147
|
+
} catch (err2) {
|
|
152148
|
+
error(`Failed to update genie host: ${err2 instanceof Error ? err2.message : "Unknown error"}`);
|
|
152139
152149
|
}
|
|
152140
|
-
return { detected: false };
|
|
152141
152150
|
}
|
|
152142
|
-
function
|
|
152151
|
+
async function registerPubkey(pubkey, hostname) {
|
|
152143
152152
|
try {
|
|
152144
|
-
const
|
|
152145
|
-
|
|
152146
|
-
|
|
152147
|
-
|
|
152148
|
-
|
|
152153
|
+
const res = await callApi("POST", "/handshake", {
|
|
152154
|
+
pubkey,
|
|
152155
|
+
hostname,
|
|
152156
|
+
capabilities: {
|
|
152157
|
+
client: "omni-cli",
|
|
152158
|
+
platform: process.platform,
|
|
152159
|
+
nodeVersion: process.version
|
|
152160
|
+
}
|
|
152149
152161
|
});
|
|
152150
|
-
|
|
152151
|
-
|
|
152152
|
-
|
|
152153
|
-
return text3.length > 0 ? text3 : null;
|
|
152154
|
-
} catch {
|
|
152155
|
-
return null;
|
|
152162
|
+
return res.data;
|
|
152163
|
+
} catch (err2) {
|
|
152164
|
+
return error(`Handshake failed: ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
152156
152165
|
}
|
|
152157
152166
|
}
|
|
152158
|
-
async function
|
|
152159
|
-
const
|
|
152160
|
-
const
|
|
152161
|
-
|
|
152162
|
-
|
|
152163
|
-
|
|
152164
|
-
|
|
152167
|
+
async function handleHandshake(options3) {
|
|
152168
|
+
const targetUrl = targetBaseUrl();
|
|
152169
|
+
const serverName = getTargetServerName();
|
|
152170
|
+
const existing = loadHostMetadata();
|
|
152171
|
+
if (existing && !options3.rotate) {
|
|
152172
|
+
const bindings = boundServerBindings(existing);
|
|
152173
|
+
const bound = boundServerUrls(existing);
|
|
152174
|
+
const alreadyBound = bindingForServer(existing, targetUrl);
|
|
152175
|
+
if (alreadyBound) {
|
|
152176
|
+
success(`Already handshook as host ${alreadyBound.hostId} for server '${serverName}' (${targetUrl}). Bound servers: ${bound.join(", ")} (each with its own host id). Requests to any other server entry are sent UNSIGNED (bearer only) \u2014 run \`omni trust handshake --server <name>\` there to bind it. Pass --rotate to issue a new keypair.`);
|
|
152177
|
+
return;
|
|
152165
152178
|
}
|
|
152179
|
+
const registered2 = await registerPubkey(existing.pubkey, options3.hostname ?? existing.hostname);
|
|
152180
|
+
const meta2 = {
|
|
152181
|
+
...existing,
|
|
152182
|
+
pubkey: registered2.pubkey,
|
|
152183
|
+
boundServers: [...bindings, { url: targetUrl, hostId: registered2.id }]
|
|
152184
|
+
};
|
|
152185
|
+
writeHostMetadata(meta2);
|
|
152186
|
+
success(`Bound existing host key to server '${serverName}' (${targetUrl}) \u2014 no rotation.`);
|
|
152187
|
+
data({
|
|
152188
|
+
hostId: registered2.id,
|
|
152189
|
+
hostname: meta2.hostname,
|
|
152190
|
+
pubkey: meta2.pubkey,
|
|
152191
|
+
boundServers: meta2.boundServers,
|
|
152192
|
+
rotated: false
|
|
152193
|
+
});
|
|
152194
|
+
return;
|
|
152166
152195
|
}
|
|
152167
|
-
|
|
152168
|
-
|
|
152169
|
-
|
|
152170
|
-
|
|
152171
|
-
|
|
152172
|
-
|
|
152173
|
-
|
|
152174
|
-
|
|
152175
|
-
|
|
152176
|
-
|
|
152177
|
-
|
|
152178
|
-
}
|
|
152179
|
-
|
|
152180
|
-
|
|
152181
|
-
|
|
152182
|
-
|
|
152183
|
-
|
|
152196
|
+
const { pubkeyB64Url } = generateAndStoreKeypair();
|
|
152197
|
+
const hostname = options3.hostname ?? osHostname();
|
|
152198
|
+
const registered = await registerPubkey(pubkeyB64Url, hostname);
|
|
152199
|
+
const meta = {
|
|
152200
|
+
hostId: registered.id,
|
|
152201
|
+
pubkey: registered.pubkey,
|
|
152202
|
+
hostname: registered.hostname,
|
|
152203
|
+
registeredAt: new Date().toISOString(),
|
|
152204
|
+
boundServers: [{ url: targetUrl, hostId: registered.id }]
|
|
152205
|
+
};
|
|
152206
|
+
writeHostMetadata(meta);
|
|
152207
|
+
success(`Operator-host registered: ${meta.hostId} (server: ${serverName})`);
|
|
152208
|
+
data({
|
|
152209
|
+
hostId: meta.hostId,
|
|
152210
|
+
hostname: meta.hostname,
|
|
152211
|
+
pubkey: meta.pubkey,
|
|
152212
|
+
boundServers: meta.boundServers,
|
|
152213
|
+
keysDir: "~/.omni/keys/",
|
|
152214
|
+
nextStep: `Calls to '${serverName}' will now be signed automatically; other server entries stay bearer-only until you run \`omni trust handshake --server <name>\` against them. Run \`omni trust list\` to verify (lastSeenAt should advance after subsequent calls).`
|
|
152215
|
+
});
|
|
152184
152216
|
}
|
|
152185
|
-
async function
|
|
152186
|
-
if (args.skipReason !== null) {
|
|
152187
|
-
return { outcome: "skipped", durationMs: 0, skipReason: args.skipReason };
|
|
152188
|
-
}
|
|
152189
|
-
const impl = args.runDoctorImpl ?? runDoctor;
|
|
152190
|
-
const startedAt = Date.now();
|
|
152217
|
+
async function handleRevoke2(id) {
|
|
152191
152218
|
try {
|
|
152192
|
-
const
|
|
152193
|
-
|
|
152194
|
-
|
|
152195
|
-
durationMs: Date.now() - startedAt,
|
|
152196
|
-
doctorReport
|
|
152197
|
-
};
|
|
152219
|
+
const { data: data2 } = await callApi("DELETE", `/hosts/${encodeURIComponent(id)}`);
|
|
152220
|
+
success(`Revoked genie host ${data2.id} (${data2.hostname}). Tombstone kept for audit.`);
|
|
152221
|
+
data(data2);
|
|
152198
152222
|
} catch (err2) {
|
|
152199
|
-
|
|
152200
|
-
outcome: "failed",
|
|
152201
|
-
durationMs: Date.now() - startedAt,
|
|
152202
|
-
error: err2 instanceof Error ? err2.message : String(err2)
|
|
152203
|
-
};
|
|
152223
|
+
error(`Failed to revoke genie host: ${err2 instanceof Error ? err2.message : "Unknown error"}`);
|
|
152204
152224
|
}
|
|
152205
152225
|
}
|
|
152206
|
-
|
|
152207
|
-
const
|
|
152208
|
-
|
|
152209
|
-
|
|
152210
|
-
|
|
152211
|
-
|
|
152212
|
-
|
|
152213
|
-
|
|
152214
|
-
|
|
152215
|
-
|
|
152216
|
-
|
|
152217
|
-
|
|
152218
|
-
|
|
152219
|
-
|
|
152220
|
-
|
|
152221
|
-
|
|
152222
|
-
|
|
152223
|
-
|
|
152224
|
-
|
|
152225
|
-
|
|
152226
|
-
|
|
152227
|
-
|
|
152228
|
-
|
|
152226
|
+
function createTrustCommand() {
|
|
152227
|
+
const trust = new Command("trust").description("Manage genie host fingerprint registrations");
|
|
152228
|
+
trust.command("handshake").description("Register THIS omni CLI as a host with the targeted server (mints ed25519 keypair in ~/.omni/keys/ on first run; later servers reuse it).").addHelpText("after", `
|
|
152229
|
+
Signing is per-server: only servers you have handshaken against recognize this
|
|
152230
|
+
keypair, so calls to other entries stay bearer-only. Bind another one with:
|
|
152231
|
+
omni trust handshake --server <name>
|
|
152232
|
+
`).option("--rotate", "Issue a new keypair even if one already exists (unbinds every previously bound server)").option("--hostname <name>", "Override the hostname reported to omni (defaults to os.hostname())").action(handleHandshake);
|
|
152233
|
+
trust.command("list").description("List active (non-revoked) genie hosts").action(handleList6);
|
|
152234
|
+
trust.command("get <id>").description("Show details for one genie host (active or revoked)").action(handleGet4);
|
|
152235
|
+
trust.command("update <id>").description("Replace a genie host scopes (comma-separated)").requiredOption("--scope <list>", 'Comma-separated scope list, e.g. "agents:write,providers:write"').action(handleUpdate3);
|
|
152236
|
+
trust.command("revoke <id>").description("Revoke a genie host (irreversible \u2014 re-register with a fresh keypair to restore trust)").action(handleRevoke2);
|
|
152237
|
+
return trust;
|
|
152238
|
+
}
|
|
152239
|
+
|
|
152240
|
+
// src/commands/tts.ts
|
|
152241
|
+
init_output();
|
|
152242
|
+
function createTtsCommand() {
|
|
152243
|
+
const tts = new Command("tts").description("Text-to-speech operations");
|
|
152244
|
+
tts.command("voices").description("List available TTS voices").action(async () => {
|
|
152245
|
+
const client = getClient();
|
|
152246
|
+
try {
|
|
152247
|
+
const voices = await client.messages.listVoices();
|
|
152248
|
+
if (voices.length === 0) {
|
|
152249
|
+
info("No TTS voices available. Check ElevenLabs configuration.");
|
|
152250
|
+
return;
|
|
152251
|
+
}
|
|
152252
|
+
const items = voices.map((v4) => ({
|
|
152253
|
+
voiceId: v4.voiceId,
|
|
152254
|
+
name: v4.name,
|
|
152255
|
+
category: v4.category ?? "-"
|
|
152256
|
+
}));
|
|
152257
|
+
list(items, { emptyMessage: "No voices found." });
|
|
152258
|
+
} catch (err2) {
|
|
152259
|
+
const message2 = err2 instanceof Error ? err2.message : "Unknown error";
|
|
152260
|
+
error(`Failed to list voices: ${message2}`);
|
|
152229
152261
|
}
|
|
152230
|
-
|
|
152231
|
-
|
|
152232
|
-
return;
|
|
152233
|
-
}
|
|
152234
|
-
const verifySpinner = ora("Verifying server version...").start();
|
|
152235
|
-
const healthBody = await fetchHealthBody(apiPort, UPDATE_HEALTH_TIMEOUT_MS);
|
|
152236
|
-
verifySpinner.stop();
|
|
152237
|
-
const keyValid = healthBody !== null ? await validateStoredKey(apiPort) : false;
|
|
152238
|
-
const result = decideVerify({ latest, apiPort, healthBody, keyValid });
|
|
152239
|
-
diagnostics.verify = result;
|
|
152240
|
-
switch (result.kind) {
|
|
152241
|
-
case "ok": {
|
|
152242
|
-
printVerifyBanner(result.cliVersion);
|
|
152243
|
-
if (cleanupReport !== null && !cleanupReport.succeeded) {
|
|
152244
|
-
warn("One or more legacy artifacts could not be cleaned up automatically. " + "See messages above and docs/migration/nats-genie-sidecar-decommission.md.");
|
|
152245
|
-
}
|
|
152246
|
-
const skipReason = resolveMaintenanceSkipReason({
|
|
152247
|
-
verifyOk: true,
|
|
152248
|
-
skipMaintenance: options3.skipMaintenance,
|
|
152249
|
-
env: process.env
|
|
152250
|
-
});
|
|
152251
|
-
const maintenance = await runPostUpdateMaintenance({ skipReason });
|
|
152252
|
-
diagnostics.maintenance = maintenance;
|
|
152253
|
-
if (maintenance.outcome === "completed" && maintenance.doctorReport) {
|
|
152254
|
-
info(formatMaintenanceSummary(maintenance.doctorReport));
|
|
152255
|
-
} else if (maintenance.outcome === "failed") {
|
|
152256
|
-
warn(`Maintenance: skipped (runDoctor failed: ${maintenance.error ?? "unknown error"})`);
|
|
152257
|
-
}
|
|
152258
|
-
return;
|
|
152259
|
-
}
|
|
152260
|
-
case "health-unreachable":
|
|
152261
|
-
warn(`omni CLI updated to v${latest}, but health check failed on port ${result.apiPort}. Run \`omni status\`.`);
|
|
152262
|
-
finalize(1);
|
|
152263
|
-
break;
|
|
152264
|
-
case "version-mismatch":
|
|
152265
|
-
console.error(`${source_default.red("\u2717")} ${updateErrorVersionMismatch(result.cliVersion, result.serverVersion)}`);
|
|
152266
|
-
finalize(1);
|
|
152267
|
-
break;
|
|
152268
|
-
case "auth-invalid":
|
|
152269
|
-
console.error(`${source_default.red("\u2717")} ${UPDATE_ERROR_AUTH_INVALID}`);
|
|
152270
|
-
finalize(1);
|
|
152271
|
-
break;
|
|
152272
|
-
}
|
|
152262
|
+
});
|
|
152263
|
+
return tts;
|
|
152273
152264
|
}
|
|
152274
|
-
|
|
152275
|
-
|
|
152276
|
-
|
|
152277
|
-
|
|
152265
|
+
|
|
152266
|
+
// src/commands/turns.ts
|
|
152267
|
+
init_output();
|
|
152268
|
+
function formatDuration2(startedAt, closedAt) {
|
|
152269
|
+
const start = new Date(startedAt).getTime();
|
|
152270
|
+
const end = closedAt ? new Date(closedAt).getTime() : Date.now();
|
|
152271
|
+
const ms = end - start;
|
|
152272
|
+
if (ms < 1000)
|
|
152273
|
+
return `${ms}ms`;
|
|
152274
|
+
if (ms < 60000)
|
|
152275
|
+
return `${Math.round(ms / 1000)}s`;
|
|
152276
|
+
return `${Math.round(ms / 60000)}m`;
|
|
152277
|
+
}
|
|
152278
|
+
function createTurnsCommand() {
|
|
152279
|
+
const cmd = new Command("turns").description("Admin turn management");
|
|
152280
|
+
cmd.command("list").description("List turns with optional filters").option("--status <status>", "Filter by status (open, done, timeout)").option("--instance <id>", "Filter by instance ID").option("--chat <id>", "Filter by chat ID").option("--agent <id>", "Filter by agent ID").option("--limit <n>", "Max results (default 50)", "50").option("--offset <n>", "Offset for pagination (default 0)", "0").action(async (opts) => {
|
|
152281
|
+
const client = getClient();
|
|
152282
|
+
const result = await client.turns.list({
|
|
152283
|
+
status: opts.status,
|
|
152284
|
+
instanceId: opts.instance,
|
|
152285
|
+
chatId: opts.chat,
|
|
152286
|
+
agentId: opts.agent,
|
|
152287
|
+
limit: Number(opts.limit),
|
|
152288
|
+
offset: Number(opts.offset)
|
|
152289
|
+
});
|
|
152290
|
+
const rows = result.items.map((t) => ({
|
|
152291
|
+
id: t.id.slice(0, 8),
|
|
152292
|
+
status: t.status,
|
|
152293
|
+
chat: t.chatId.length > 20 ? `${t.chatId.slice(0, 20)}\u2026` : t.chatId,
|
|
152294
|
+
agent: t.agentId.slice(0, 8),
|
|
152295
|
+
duration: formatDuration2(t.startedAt, t.closedAt),
|
|
152296
|
+
nudges: t.nudgeCount,
|
|
152297
|
+
msgs: t.messagesSent
|
|
152298
|
+
}));
|
|
152299
|
+
list(rows, {
|
|
152300
|
+
emptyMessage: "No turns found.",
|
|
152301
|
+
rawData: result.items
|
|
152302
|
+
});
|
|
152303
|
+
if (getCurrentFormat() !== "json") {
|
|
152304
|
+
dim(`Total: ${result.total} | Showing ${result.offset + 1}\u2013${result.offset + result.items.length}`);
|
|
152305
|
+
}
|
|
152278
152306
|
});
|
|
152279
|
-
|
|
152280
|
-
|
|
152281
|
-
|
|
152282
|
-
|
|
152283
|
-
|
|
152307
|
+
cmd.command("get").description("Get a single turn by ID").argument("<id>", "Turn ID").action(async (id) => {
|
|
152308
|
+
const client = getClient();
|
|
152309
|
+
const turn = await client.turns.get(id);
|
|
152310
|
+
data(turn);
|
|
152311
|
+
});
|
|
152312
|
+
cmd.command("close").description("Admin force-close a turn").argument("<id>", "Turn ID to close").option("--reason <text>", "Close reason").action(async (id, opts) => {
|
|
152313
|
+
const client = getClient();
|
|
152314
|
+
const result = await client.turns.forceClose(id, opts.reason);
|
|
152315
|
+
success(`Turn ${result.turnId} closed`, result);
|
|
152316
|
+
});
|
|
152317
|
+
cmd.command("close-all").description("Bulk close all open turns (requires --confirm)").option("--confirm", "Confirm bulk close").option("--reason <text>", "Close reason").action(async (opts) => {
|
|
152318
|
+
if (!opts.confirm) {
|
|
152319
|
+
error("Bulk close requires --confirm flag to proceed.");
|
|
152320
|
+
}
|
|
152321
|
+
const client = getClient();
|
|
152322
|
+
const result = await client.turns.bulkClose(opts.reason);
|
|
152323
|
+
success(result.message, result);
|
|
152324
|
+
});
|
|
152325
|
+
cmd.command("stats").description("Show aggregate turn metrics").action(async () => {
|
|
152326
|
+
const client = getClient();
|
|
152327
|
+
const stats = await client.turns.stats();
|
|
152328
|
+
data({
|
|
152329
|
+
openCount: stats.openCount,
|
|
152330
|
+
totalCount: stats.totalCount,
|
|
152331
|
+
avgDurationMs: stats.avgDurationMs,
|
|
152332
|
+
timeoutRate: `${(stats.timeoutRate * 100).toFixed(1)}%`
|
|
152284
152333
|
});
|
|
152285
152334
|
});
|
|
152335
|
+
return cmd;
|
|
152286
152336
|
}
|
|
152287
|
-
|
|
152288
|
-
|
|
152289
|
-
|
|
152290
|
-
|
|
152337
|
+
|
|
152338
|
+
// src/commands/update.ts
|
|
152339
|
+
import { existsSync as existsSync20 } from "fs";
|
|
152340
|
+
import { join as join19 } from "path";
|
|
152341
|
+
import { createInterface as createInterface3 } from "readline";
|
|
152342
|
+
init_source();
|
|
152343
|
+
init_config();
|
|
152344
|
+
|
|
152345
|
+
// src/sidecar-cleanup.ts
|
|
152346
|
+
var SIDECAR_BASENAME = "nats-reply-sidecar.mjs";
|
|
152347
|
+
var SIDECAR_NAME_HINT = "nats-reply-sidecar";
|
|
152348
|
+
function pm2EntryToMatch(entry) {
|
|
152349
|
+
if (!entry || typeof entry !== "object")
|
|
152350
|
+
return null;
|
|
152351
|
+
const name = typeof entry.name === "string" ? entry.name : "";
|
|
152352
|
+
const scriptPath = typeof entry.pm2_env?.pm_exec_path === "string" ? entry.pm2_env.pm_exec_path : "";
|
|
152353
|
+
const pmId = typeof entry.pm_id === "number" ? entry.pm_id : null;
|
|
152354
|
+
const nameMatch = name.includes(SIDECAR_NAME_HINT);
|
|
152355
|
+
const pathMatch = scriptPath.endsWith(`/${SIDECAR_BASENAME}`) || scriptPath.endsWith(SIDECAR_BASENAME);
|
|
152356
|
+
if (!nameMatch && !pathMatch)
|
|
152357
|
+
return null;
|
|
152358
|
+
return {
|
|
152359
|
+
name,
|
|
152360
|
+
pmId,
|
|
152361
|
+
scriptPath,
|
|
152362
|
+
matchedBy: nameMatch ? "name" : "script-path"
|
|
152363
|
+
};
|
|
152364
|
+
}
|
|
152365
|
+
function parsePm2SidecarMatches(rawJson) {
|
|
152366
|
+
const trimmed = rawJson.trim();
|
|
152367
|
+
if (trimmed.length === 0 || trimmed === "[]")
|
|
152368
|
+
return [];
|
|
152369
|
+
let parsed;
|
|
152370
|
+
try {
|
|
152371
|
+
parsed = JSON.parse(trimmed);
|
|
152372
|
+
} catch {
|
|
152373
|
+
return [];
|
|
152291
152374
|
}
|
|
152292
|
-
|
|
152293
|
-
|
|
152294
|
-
const
|
|
152295
|
-
|
|
152296
|
-
|
|
152297
|
-
|
|
152375
|
+
if (!Array.isArray(parsed))
|
|
152376
|
+
return [];
|
|
152377
|
+
const matches = [];
|
|
152378
|
+
for (const entry of parsed) {
|
|
152379
|
+
const match = pm2EntryToMatch(entry);
|
|
152380
|
+
if (match !== null)
|
|
152381
|
+
matches.push(match);
|
|
152382
|
+
}
|
|
152383
|
+
return matches;
|
|
152384
|
+
}
|
|
152385
|
+
function parseRawSidecarMatches(rawPgrep) {
|
|
152386
|
+
const lines = rawPgrep.split(`
|
|
152387
|
+
`);
|
|
152388
|
+
const matches = [];
|
|
152389
|
+
for (const line3 of lines) {
|
|
152390
|
+
const trimmed = line3.trim();
|
|
152391
|
+
if (trimmed.length === 0)
|
|
152392
|
+
continue;
|
|
152393
|
+
const firstSpace = trimmed.indexOf(" ");
|
|
152394
|
+
if (firstSpace === -1)
|
|
152395
|
+
continue;
|
|
152396
|
+
const pidStr = trimmed.slice(0, firstSpace);
|
|
152397
|
+
const command = trimmed.slice(firstSpace + 1);
|
|
152398
|
+
const pid = Number.parseInt(pidStr, 10);
|
|
152399
|
+
if (!Number.isFinite(pid) || pid <= 0)
|
|
152400
|
+
continue;
|
|
152401
|
+
if (!command.includes(SIDECAR_BASENAME))
|
|
152402
|
+
continue;
|
|
152403
|
+
matches.push({ pid, command });
|
|
152404
|
+
}
|
|
152405
|
+
return matches;
|
|
152406
|
+
}
|
|
152407
|
+
async function detectPm2Sidecars() {
|
|
152408
|
+
try {
|
|
152409
|
+
const proc = Bun.spawn({
|
|
152410
|
+
cmd: ["pm2", "jlist"],
|
|
152411
|
+
stdout: "pipe",
|
|
152412
|
+
stderr: "pipe"
|
|
152413
|
+
});
|
|
152414
|
+
const [stdout, exitCode] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
|
152415
|
+
if (exitCode !== 0)
|
|
152416
|
+
return [];
|
|
152417
|
+
return parsePm2SidecarMatches(stdout);
|
|
152418
|
+
} catch {
|
|
152419
|
+
return [];
|
|
152420
|
+
}
|
|
152421
|
+
}
|
|
152422
|
+
async function detectRawSidecars() {
|
|
152423
|
+
try {
|
|
152424
|
+
const proc = Bun.spawn({
|
|
152425
|
+
cmd: ["pgrep", "-fa", SIDECAR_BASENAME],
|
|
152426
|
+
stdout: "pipe",
|
|
152427
|
+
stderr: "pipe"
|
|
152428
|
+
});
|
|
152429
|
+
const [stdout, exitCode] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
|
152430
|
+
if (exitCode !== 0 && exitCode !== 1)
|
|
152431
|
+
return [];
|
|
152432
|
+
const matches = parseRawSidecarMatches(stdout);
|
|
152433
|
+
const ownPid = process.pid;
|
|
152434
|
+
return matches.filter((m10) => m10.pid !== ownPid);
|
|
152435
|
+
} catch {
|
|
152436
|
+
return [];
|
|
152437
|
+
}
|
|
152438
|
+
}
|
|
152439
|
+
async function stopPm2Sidecar(match) {
|
|
152440
|
+
const target = match.name.length > 0 ? match.name : match.pmId !== null ? String(match.pmId) : null;
|
|
152441
|
+
if (target === null)
|
|
152442
|
+
return false;
|
|
152443
|
+
for (const verb of ["stop", "delete"]) {
|
|
152444
|
+
const proc = Bun.spawn({
|
|
152445
|
+
cmd: ["pm2", verb, target],
|
|
152446
|
+
stdout: "pipe",
|
|
152447
|
+
stderr: "pipe"
|
|
152448
|
+
});
|
|
152449
|
+
const exitCode = await proc.exited;
|
|
152450
|
+
if (exitCode !== 0)
|
|
152451
|
+
return false;
|
|
152452
|
+
}
|
|
152453
|
+
return true;
|
|
152454
|
+
}
|
|
152455
|
+
async function killRawSidecar(match) {
|
|
152456
|
+
try {
|
|
152457
|
+
process.kill(match.pid, "SIGTERM");
|
|
152458
|
+
return true;
|
|
152459
|
+
} catch {
|
|
152460
|
+
return false;
|
|
152461
|
+
}
|
|
152462
|
+
}
|
|
152463
|
+
async function cleanupSidecars() {
|
|
152464
|
+
const [pm2Detected, rawDetected] = await Promise.all([detectPm2Sidecars(), detectRawSidecars()]);
|
|
152465
|
+
const pm2Stopped = [];
|
|
152466
|
+
const pm2Failed = [];
|
|
152467
|
+
for (const match of pm2Detected) {
|
|
152468
|
+
const ok3 = await stopPm2Sidecar(match);
|
|
152469
|
+
if (ok3) {
|
|
152470
|
+
pm2Stopped.push(match);
|
|
152471
|
+
} else {
|
|
152472
|
+
pm2Failed.push(match);
|
|
152298
152473
|
}
|
|
152299
|
-
|
|
152474
|
+
}
|
|
152475
|
+
const rawKilled = [];
|
|
152476
|
+
const rawFailed = [];
|
|
152477
|
+
for (const match of rawDetected) {
|
|
152478
|
+
const ok3 = await killRawSidecar(match);
|
|
152479
|
+
if (ok3) {
|
|
152480
|
+
rawKilled.push(match);
|
|
152481
|
+
} else {
|
|
152482
|
+
rawFailed.push(match);
|
|
152483
|
+
}
|
|
152484
|
+
}
|
|
152485
|
+
return {
|
|
152486
|
+
pm2Detected,
|
|
152487
|
+
rawDetected,
|
|
152488
|
+
pm2Stopped,
|
|
152489
|
+
pm2Failed,
|
|
152490
|
+
rawKilled,
|
|
152491
|
+
rawFailed
|
|
152492
|
+
};
|
|
152493
|
+
}
|
|
152494
|
+
function formatCleanupSummary(result) {
|
|
152495
|
+
const totalDetected = result.pm2Detected.length + result.rawDetected.length;
|
|
152496
|
+
if (totalDetected === 0)
|
|
152497
|
+
return "";
|
|
152498
|
+
const lines = [];
|
|
152499
|
+
lines.push(`Found ${totalDetected} legacy nats-reply-sidecar process(es) \u2014 stopping to prevent duplicate replies.`);
|
|
152500
|
+
for (const match of result.pm2Stopped) {
|
|
152501
|
+
const label = match.name || `pm_id=${match.pmId}`;
|
|
152502
|
+
lines.push(` \u2713 pm2: stopped and deleted "${label}" (matched by ${match.matchedBy})`);
|
|
152503
|
+
}
|
|
152504
|
+
for (const match of result.pm2Failed) {
|
|
152505
|
+
const label = match.name || `pm_id=${match.pmId}`;
|
|
152506
|
+
lines.push(` \u2717 pm2: failed to stop "${label}" \u2014 run \`pm2 stop ${label} && pm2 delete ${label}\` manually`);
|
|
152507
|
+
}
|
|
152508
|
+
for (const match of result.rawKilled) {
|
|
152509
|
+
lines.push(` \u2713 raw: SIGTERM sent to pid ${match.pid}`);
|
|
152510
|
+
}
|
|
152511
|
+
for (const match of result.rawFailed) {
|
|
152512
|
+
lines.push(` \u2717 raw: failed to signal pid ${match.pid} \u2014 kill it manually with \`kill ${match.pid}\``);
|
|
152513
|
+
}
|
|
152514
|
+
if (result.pm2Failed.length > 0 || result.rawFailed.length > 0) {
|
|
152515
|
+
lines.push("See docs/migration/nats-genie-sidecar-decommission.md for the manual cleanup runbook.");
|
|
152516
|
+
}
|
|
152517
|
+
return lines.join(`
|
|
152518
|
+
`);
|
|
152519
|
+
}
|
|
152520
|
+
|
|
152521
|
+
// src/legacy-cleanup.ts
|
|
152522
|
+
function createNatsReplySidecarArtifact() {
|
|
152523
|
+
let lastResult = null;
|
|
152524
|
+
return {
|
|
152525
|
+
name: "nats-reply-sidecar",
|
|
152526
|
+
async detect() {
|
|
152527
|
+
return true;
|
|
152528
|
+
},
|
|
152529
|
+
async cleanup() {
|
|
152530
|
+
const result = await cleanupSidecars();
|
|
152531
|
+
lastResult = result;
|
|
152532
|
+
const removed = [];
|
|
152533
|
+
const warnings = [];
|
|
152534
|
+
for (const m10 of result.pm2Stopped) {
|
|
152535
|
+
removed.push(`pm2:${m10.name.length > 0 ? m10.name : `pm_id=${m10.pmId}`}`);
|
|
152536
|
+
}
|
|
152537
|
+
for (const m10 of result.rawKilled) {
|
|
152538
|
+
removed.push(`pid:${m10.pid}`);
|
|
152539
|
+
}
|
|
152540
|
+
for (const m10 of result.pm2Failed) {
|
|
152541
|
+
warnings.push(`pm2-failed:${m10.name.length > 0 ? m10.name : `pm_id=${m10.pmId}`}`);
|
|
152542
|
+
}
|
|
152543
|
+
for (const m10 of result.rawFailed) {
|
|
152544
|
+
warnings.push(`raw-failed:pid=${m10.pid}`);
|
|
152545
|
+
}
|
|
152546
|
+
return { removed, warnings };
|
|
152547
|
+
},
|
|
152548
|
+
summary() {
|
|
152549
|
+
return lastResult === null ? "" : formatCleanupSummary(lastResult);
|
|
152550
|
+
}
|
|
152551
|
+
};
|
|
152552
|
+
}
|
|
152553
|
+
var REGISTRY = [createNatsReplySidecarArtifact()];
|
|
152554
|
+
function errorMessage2(error2) {
|
|
152555
|
+
return error2 instanceof Error ? error2.message : String(error2);
|
|
152556
|
+
}
|
|
152557
|
+
function erroredOutcome(name, phase, error2) {
|
|
152558
|
+
const message2 = errorMessage2(error2);
|
|
152559
|
+
return {
|
|
152560
|
+
name,
|
|
152561
|
+
state: "errored",
|
|
152562
|
+
removed: [],
|
|
152563
|
+
warnings: [`${phase}-threw:${message2}`],
|
|
152564
|
+
summary: "",
|
|
152565
|
+
error: message2
|
|
152300
152566
|
};
|
|
152301
|
-
|
|
152302
|
-
|
|
152303
|
-
|
|
152304
|
-
|
|
152567
|
+
}
|
|
152568
|
+
async function runArtifact(artifact) {
|
|
152569
|
+
let detected;
|
|
152570
|
+
try {
|
|
152571
|
+
detected = await artifact.detect();
|
|
152572
|
+
} catch (error2) {
|
|
152573
|
+
return erroredOutcome(artifact.name, "detect", error2);
|
|
152305
152574
|
}
|
|
152306
|
-
|
|
152307
|
-
|
|
152308
|
-
|
|
152309
|
-
|
|
152310
|
-
|
|
152311
|
-
|
|
152312
|
-
|
|
152313
|
-
|
|
152575
|
+
if (!detected) {
|
|
152576
|
+
return {
|
|
152577
|
+
name: artifact.name,
|
|
152578
|
+
state: "not-detected",
|
|
152579
|
+
removed: [],
|
|
152580
|
+
warnings: [],
|
|
152581
|
+
summary: ""
|
|
152582
|
+
};
|
|
152314
152583
|
}
|
|
152315
|
-
|
|
152316
|
-
|
|
152317
|
-
return
|
|
152584
|
+
try {
|
|
152585
|
+
const { removed, warnings } = await artifact.cleanup();
|
|
152586
|
+
return {
|
|
152587
|
+
name: artifact.name,
|
|
152588
|
+
state: "ran",
|
|
152589
|
+
removed,
|
|
152590
|
+
warnings,
|
|
152591
|
+
summary: artifact.summary()
|
|
152592
|
+
};
|
|
152593
|
+
} catch (error2) {
|
|
152594
|
+
return erroredOutcome(artifact.name, "cleanup", error2);
|
|
152318
152595
|
}
|
|
152319
|
-
|
|
152320
|
-
|
|
152321
|
-
|
|
152322
|
-
|
|
152323
|
-
|
|
152324
|
-
|
|
152596
|
+
}
|
|
152597
|
+
async function cleanupLegacyArtifacts(skipList) {
|
|
152598
|
+
const outcomes = [];
|
|
152599
|
+
const skipped = [];
|
|
152600
|
+
for (const artifact of REGISTRY) {
|
|
152601
|
+
if (skipList.has(artifact.name)) {
|
|
152602
|
+
skipped.push(artifact.name);
|
|
152603
|
+
outcomes.push({
|
|
152604
|
+
name: artifact.name,
|
|
152605
|
+
state: "skipped",
|
|
152606
|
+
removed: [],
|
|
152607
|
+
warnings: [],
|
|
152608
|
+
summary: ""
|
|
152609
|
+
});
|
|
152610
|
+
continue;
|
|
152325
152611
|
}
|
|
152612
|
+
outcomes.push(await runArtifact(artifact));
|
|
152326
152613
|
}
|
|
152327
|
-
const
|
|
152328
|
-
|
|
152329
|
-
diagnostics.install.attempted = true;
|
|
152330
|
-
diagnostics.install.targetVersion = latest;
|
|
152331
|
-
const installed = await installLatest(channel2);
|
|
152332
|
-
installSpinner.stop();
|
|
152333
|
-
diagnostics.install.succeeded = installed;
|
|
152334
|
-
if (!installed) {
|
|
152335
|
-
warn(`Installation failed. Your current version (v${currentClean}) is still intact.`);
|
|
152336
|
-
return finalize(1);
|
|
152337
|
-
}
|
|
152338
|
-
const sidecarCleanupExplicit = options3.sidecarCleanup === false;
|
|
152339
|
-
if (sidecarCleanupExplicit) {
|
|
152340
|
-
info("--no-sidecar-cleanup (deprecated alias for --no-legacy-cleanup)");
|
|
152341
|
-
}
|
|
152342
|
-
const legacyCleanupEnabled = options3.legacyCleanup !== false && options3.sidecarCleanup !== false;
|
|
152343
|
-
const skipList = parseSkipCleanupList(options3.skipCleanup);
|
|
152344
|
-
if (servicesToRestart.length > 0) {
|
|
152345
|
-
await restartServicesAndVerify(servicesToRestart, latest, {
|
|
152346
|
-
legacyCleanup: legacyCleanupEnabled,
|
|
152347
|
-
skipList,
|
|
152348
|
-
skipMaintenance: options3.skipMaintenance === true,
|
|
152349
|
-
skipVerify: options3.verify === false
|
|
152350
|
-
}, diagnostics, finalize);
|
|
152351
|
-
return finalize(0);
|
|
152352
|
-
}
|
|
152353
|
-
diagnostics.verify = decideVerify({ skipReason: "no-restart" });
|
|
152354
|
-
success(`omni updated to v${latest}`);
|
|
152355
|
-
return finalize(0);
|
|
152356
|
-
}
|
|
152357
|
-
function parseSkipCleanupList(value) {
|
|
152358
|
-
if (!value)
|
|
152359
|
-
return new Set;
|
|
152360
|
-
const names = value.split(",").map((s2) => s2.trim()).filter((s2) => s2.length > 0);
|
|
152361
|
-
return new Set(names);
|
|
152614
|
+
const succeeded = outcomes.every((o2) => o2.state !== "errored" && (o2.state !== "ran" || o2.warnings.length === 0));
|
|
152615
|
+
return { outcomes, succeeded, skipped };
|
|
152362
152616
|
}
|
|
152363
|
-
function createUpdateCommand() {
|
|
152364
|
-
return new Command("update").description(`Update ${PACKAGE_NAME} to the latest version (restart only services already running)`).option("-y, --yes", "Skip confirmation prompts (non-interactive)").option("--no-restart", "Update CLI only; skip service restarts and verification").option("--no-verify", "Restart services but skip the post-restart probe (use when a release ships a broken /api/v2/health and operators need to roll forward)").option("--no-legacy-cleanup", "Skip every registered legacy-artifact cleanup (e.g. nats-reply-sidecar)").option("--no-sidecar-cleanup", "Deprecated alias for --no-legacy-cleanup").option("--skip-cleanup <names>", 'Comma-separated list of legacy-cleanup registry entries to skip (e.g. "nats-reply-sidecar")').option("--next", "Switch to dev builds (npm @next tag) and persist as default").option("--stable", "Switch to stable releases (npm @latest tag) and persist as default").option("--skip-maintenance", `Skip the post-update maintenance hook (read-only \`omni doctor\` sweep). Also honored via the ${OMNI_UPDATE_SKIP_MAINTENANCE_ENV} env var.`).addHelpText("after", `
|
|
152365
|
-
Channels:
|
|
152366
|
-
- stable (default) \u2014 tracks the npm @latest tag, bumped from main branch releases.
|
|
152367
|
-
- next (dev builds) \u2014 tracks the npm @next tag, bumped on every CI-green dev merge.
|
|
152368
152617
|
|
|
152369
|
-
|
|
152370
|
-
|
|
152371
|
-
'omni update' calls stay on the selected track. Check or change manually:
|
|
152372
|
-
omni config get updateChannel
|
|
152373
|
-
omni config set updateChannel next
|
|
152374
|
-
omni config set updateChannel latest
|
|
152618
|
+
// src/commands/update.ts
|
|
152619
|
+
init_output();
|
|
152375
152620
|
|
|
152376
|
-
|
|
152377
|
-
|
|
152378
|
-
|
|
152379
|
-
|
|
152380
|
-
|
|
152381
|
-
|
|
152382
|
-
|
|
152383
|
-
|
|
152384
|
-
|
|
152385
|
-
|
|
152386
|
-
|
|
152387
|
-
|
|
152388
|
-
|
|
152389
|
-
|
|
152390
|
-
|
|
152391
|
-
|
|
152392
|
-
|
|
152393
|
-
|
|
152394
|
-
|
|
152395
|
-
|
|
152396
|
-
|
|
152397
|
-
|
|
152398
|
-
|
|
152399
|
-
- Use --no-restart to skip restart + verification entirely. --no-restart
|
|
152400
|
-
also skips legacy-artifact cleanup; manage those services manually.
|
|
152401
|
-
- Use --no-verify to restart services but skip the post-restart probe.
|
|
152402
|
-
The banner shows the new CLI version on both lines but tags Server /
|
|
152403
|
-
Auth as "(skipped)"; maintenance is auto-skipped because verify never
|
|
152404
|
-
confirmed health.
|
|
152405
|
-
- Detects parallel npm-global installs of ${PACKAGE_NAME} (omni installs
|
|
152406
|
-
via bun-global; an npm-global install hides stale binaries on PATH and
|
|
152407
|
-
confuses 'which omni'). When found, prints a one-line warning naming
|
|
152408
|
-
the offending path; recommended remediation: npm uninstall -g ${PACKAGE_NAME}.
|
|
152409
|
-
- Every invocation writes a diagnostics record to
|
|
152410
|
-
~/.omni/logs/update-diagnostics-<iso>.json (schemaVersion: 1). Captures
|
|
152411
|
-
install attempt, registry probe, restart outcome, verify result, cleanup
|
|
152412
|
-
registry result, maintenance hook, and a tail of pm2 log signals. Set
|
|
152413
|
-
OMNI_UPDATE_DIAGNOSTICS_VERBOSE=1 to print the path on completion. A
|
|
152414
|
-
failed write never changes the update exit code.
|
|
152415
|
-
- On a successful restart + verify, runs a post-update maintenance hook:
|
|
152416
|
-
a read-only \`omni doctor\` sweep that prints a one-line summary
|
|
152417
|
-
("Maintenance: <ok> ok, <warn> warn, <fail> fail"). The probe never
|
|
152418
|
-
mutates pm2 / config / DB state (\`omni doctor --fix\` remains the
|
|
152419
|
-
explicit operator action for repair). A failing maintenance call is
|
|
152420
|
-
non-fatal \u2014 exit code stays 0 with a banner warning. Skip with
|
|
152421
|
-
--skip-maintenance or ${OMNI_UPDATE_SKIP_MAINTENANCE_ENV}=1.
|
|
152422
|
-
- Verify runtime health after update with: omni status
|
|
152423
|
-
`).action(runUpdate);
|
|
152621
|
+
// src/update-diagnostics.ts
|
|
152622
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync12, readFileSync as readFileSync19, writeFileSync as writeFileSync11 } from "fs";
|
|
152623
|
+
import { homedir as homedir13 } from "os";
|
|
152624
|
+
import { join as join18 } from "path";
|
|
152625
|
+
var UPDATE_DIAGNOSTICS_SCHEMA_VERSION = 1;
|
|
152626
|
+
function createDiagnostics(args) {
|
|
152627
|
+
const startedAt = new Date().toISOString();
|
|
152628
|
+
return {
|
|
152629
|
+
schemaVersion: UPDATE_DIAGNOSTICS_SCHEMA_VERSION,
|
|
152630
|
+
startedAt,
|
|
152631
|
+
finishedAt: startedAt,
|
|
152632
|
+
cli: { runningVersion: args.runningVersion, channel: args.channel },
|
|
152633
|
+
registry: { latestVersion: null },
|
|
152634
|
+
preflight: { ran: false, blocked: false },
|
|
152635
|
+
install: { attempted: false, succeeded: null, targetVersion: null },
|
|
152636
|
+
restart: { attempted: false, succeeded: null, services: [] },
|
|
152637
|
+
verify: null,
|
|
152638
|
+
cleanups: null,
|
|
152639
|
+
maintenance: null,
|
|
152640
|
+
parallelNpmGlobal: null,
|
|
152641
|
+
recentLogSignals: [],
|
|
152642
|
+
exitCode: 0
|
|
152643
|
+
};
|
|
152424
152644
|
}
|
|
152425
|
-
|
|
152426
|
-
|
|
152427
|
-
|
|
152428
|
-
|
|
152429
|
-
|
|
152430
|
-
|
|
152431
|
-
|
|
152432
|
-
|
|
152433
|
-
|
|
152434
|
-
|
|
152435
|
-
|
|
152436
|
-
|
|
152645
|
+
function getDiagnosticsDir() {
|
|
152646
|
+
const base = process.env.OMNI_CONFIG_DIR ?? join18(homedir13(), ".omni");
|
|
152647
|
+
return join18(base, "logs");
|
|
152648
|
+
}
|
|
152649
|
+
function getDiagnosticsPath(startedAt) {
|
|
152650
|
+
const safe = startedAt.replace(/:/g, "-");
|
|
152651
|
+
return join18(getDiagnosticsDir(), `update-diagnostics-${safe}.json`);
|
|
152652
|
+
}
|
|
152653
|
+
function tailFileLines(path, maxLines) {
|
|
152654
|
+
if (!existsSync19(path))
|
|
152655
|
+
return [];
|
|
152656
|
+
try {
|
|
152657
|
+
const raw2 = readFileSync19(path, "utf8");
|
|
152658
|
+
const lines = raw2.split(/\r?\n/);
|
|
152659
|
+
if (lines.length > 0 && lines[lines.length - 1] === "")
|
|
152660
|
+
lines.pop();
|
|
152661
|
+
return lines.slice(-maxLines);
|
|
152662
|
+
} catch {
|
|
152663
|
+
return [];
|
|
152664
|
+
}
|
|
152665
|
+
}
|
|
152666
|
+
function collectRecentLogSignals(maxLinesPerStream = 30, logPathsFor = getPm2LogPaths) {
|
|
152667
|
+
const signals2 = [];
|
|
152668
|
+
for (const name of Object.values(PM2_PROCESSES)) {
|
|
152669
|
+
const { out, error: error2 } = logPathsFor(name);
|
|
152670
|
+
const outLines = tailFileLines(out, maxLinesPerStream);
|
|
152671
|
+
if (outLines.length > 0) {
|
|
152672
|
+
signals2.push({ source: name, stream: "out", lines: outLines });
|
|
152437
152673
|
}
|
|
152438
|
-
|
|
152439
|
-
|
|
152440
|
-
|
|
152441
|
-
} catch {
|
|
152442
|
-
success(`Active instance: ${instanceId.slice(0, 8)}\u2026`);
|
|
152674
|
+
const errLines = tailFileLines(error2, maxLinesPerStream);
|
|
152675
|
+
if (errLines.length > 0) {
|
|
152676
|
+
signals2.push({ source: name, stream: "error", lines: errLines });
|
|
152443
152677
|
}
|
|
152444
|
-
}
|
|
152678
|
+
}
|
|
152679
|
+
return signals2;
|
|
152680
|
+
}
|
|
152681
|
+
function writeDiagnostics(state, exitCode) {
|
|
152682
|
+
state.exitCode = exitCode;
|
|
152683
|
+
state.finishedAt = new Date().toISOString();
|
|
152684
|
+
if (state.recentLogSignals.length === 0) {
|
|
152685
|
+
state.recentLogSignals = collectRecentLogSignals();
|
|
152686
|
+
}
|
|
152687
|
+
const dir = getDiagnosticsDir();
|
|
152688
|
+
const path = getDiagnosticsPath(state.startedAt);
|
|
152689
|
+
try {
|
|
152690
|
+
if (!existsSync19(dir)) {
|
|
152691
|
+
mkdirSync12(dir, { recursive: true, mode: 448 });
|
|
152692
|
+
}
|
|
152693
|
+
writeFileSync11(path, `${JSON.stringify(state, null, 2)}
|
|
152694
|
+
`, { mode: 384 });
|
|
152695
|
+
return path;
|
|
152696
|
+
} catch {
|
|
152697
|
+
return null;
|
|
152698
|
+
}
|
|
152445
152699
|
}
|
|
152446
152700
|
|
|
152447
|
-
// src/commands/
|
|
152448
|
-
|
|
152449
|
-
|
|
152450
|
-
|
|
152451
|
-
|
|
152452
|
-
|
|
152701
|
+
// src/commands/update.ts
|
|
152702
|
+
var PACKAGE_NAME = "@automagik/omni";
|
|
152703
|
+
var UPDATE_HEALTH_TIMEOUT_MS = 1e4;
|
|
152704
|
+
var VERIFY_POLL_INTERVAL_MS = 500;
|
|
152705
|
+
function resolveChannel(options3, config2) {
|
|
152706
|
+
if (options3.next)
|
|
152707
|
+
return "next";
|
|
152708
|
+
if (options3.stable)
|
|
152709
|
+
return "latest";
|
|
152710
|
+
const saved = (config2 ?? loadConfig()).updateChannel;
|
|
152711
|
+
if (saved === "latest" || saved === "next")
|
|
152712
|
+
return saved;
|
|
152713
|
+
return "latest";
|
|
152453
152714
|
}
|
|
152454
|
-
function
|
|
152455
|
-
|
|
152456
|
-
|
|
152715
|
+
function persistChannel(channel2) {
|
|
152716
|
+
try {
|
|
152717
|
+
const config2 = loadConfig();
|
|
152718
|
+
config2.updateChannel = channel2;
|
|
152719
|
+
saveConfig(config2);
|
|
152720
|
+
} catch {}
|
|
152457
152721
|
}
|
|
152458
|
-
function
|
|
152459
|
-
|
|
152460
|
-
|
|
152461
|
-
|
|
152462
|
-
|
|
152463
|
-
|
|
152464
|
-
|
|
152465
|
-
|
|
152466
|
-
|
|
152467
|
-
|
|
152468
|
-
guildId: opts.guild
|
|
152469
|
-
})
|
|
152470
|
-
});
|
|
152471
|
-
const body = await res.json();
|
|
152472
|
-
if (!res.ok) {
|
|
152473
|
-
error("Failed to join", body);
|
|
152474
|
-
process.exit(1);
|
|
152475
|
-
}
|
|
152476
|
-
success("Joined voice channel");
|
|
152477
|
-
data(body);
|
|
152478
|
-
} catch (err2) {
|
|
152479
|
-
error("Failed to join voice channel", err2);
|
|
152480
|
-
process.exit(1);
|
|
152722
|
+
async function fetchLatestVersion(channel2) {
|
|
152723
|
+
try {
|
|
152724
|
+
const proc = Bun.spawn({
|
|
152725
|
+
cmd: ["bunx", "npm", "view", `${PACKAGE_NAME}@${channel2}`, "version"],
|
|
152726
|
+
stdout: "pipe",
|
|
152727
|
+
stderr: "pipe"
|
|
152728
|
+
});
|
|
152729
|
+
const [stdout, exitCode] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
|
152730
|
+
if (exitCode !== 0) {
|
|
152731
|
+
return null;
|
|
152481
152732
|
}
|
|
152482
|
-
|
|
152483
|
-
|
|
152484
|
-
|
|
152485
|
-
|
|
152486
|
-
|
|
152487
|
-
|
|
152488
|
-
|
|
152489
|
-
|
|
152490
|
-
|
|
152491
|
-
|
|
152492
|
-
|
|
152493
|
-
|
|
152494
|
-
|
|
152495
|
-
|
|
152496
|
-
|
|
152497
|
-
process.exit(1);
|
|
152733
|
+
const text3 = stdout.trim();
|
|
152734
|
+
return text3.length > 0 ? text3 : null;
|
|
152735
|
+
} catch {
|
|
152736
|
+
return null;
|
|
152737
|
+
}
|
|
152738
|
+
}
|
|
152739
|
+
function getRunningPm2Services() {
|
|
152740
|
+
try {
|
|
152741
|
+
const result = Bun.spawnSync({
|
|
152742
|
+
cmd: ["pm2", "jlist"],
|
|
152743
|
+
stdout: "pipe",
|
|
152744
|
+
stderr: "pipe"
|
|
152745
|
+
});
|
|
152746
|
+
if (result.exitCode !== 0) {
|
|
152747
|
+
return [];
|
|
152498
152748
|
}
|
|
152499
|
-
|
|
152500
|
-
|
|
152501
|
-
|
|
152502
|
-
|
|
152503
|
-
|
|
152504
|
-
|
|
152505
|
-
|
|
152506
|
-
|
|
152507
|
-
|
|
152749
|
+
const raw2 = new TextDecoder().decode(result.stdout).trim();
|
|
152750
|
+
if (!raw2 || raw2 === "[]")
|
|
152751
|
+
return [];
|
|
152752
|
+
const pm2Names = new Set(Object.values(PM2_PROCESSES));
|
|
152753
|
+
const list2 = JSON.parse(raw2);
|
|
152754
|
+
const running = new Set;
|
|
152755
|
+
for (const proc of list2) {
|
|
152756
|
+
const name = proc.name;
|
|
152757
|
+
if (name && pm2Names.has(name) && proc.pm2_env?.status === "online") {
|
|
152758
|
+
running.add(name);
|
|
152508
152759
|
}
|
|
152509
|
-
} catch (err2) {
|
|
152510
|
-
error("Failed to list sessions", err2);
|
|
152511
|
-
process.exit(1);
|
|
152512
152760
|
}
|
|
152761
|
+
return [...running];
|
|
152762
|
+
} catch {
|
|
152763
|
+
return [];
|
|
152764
|
+
}
|
|
152765
|
+
}
|
|
152766
|
+
async function installLatest(channel2) {
|
|
152767
|
+
const proc = Bun.spawn({
|
|
152768
|
+
cmd: ["bun", "add", "-g", "--force", "--no-cache", `${PACKAGE_NAME}@${channel2}`],
|
|
152769
|
+
stdin: "inherit",
|
|
152770
|
+
stdout: "inherit",
|
|
152771
|
+
stderr: "inherit",
|
|
152772
|
+
env: process.env
|
|
152513
152773
|
});
|
|
152514
|
-
|
|
152515
|
-
|
|
152516
|
-
|
|
152517
|
-
|
|
152518
|
-
|
|
152519
|
-
|
|
152520
|
-
|
|
152521
|
-
|
|
152522
|
-
|
|
152523
|
-
|
|
152524
|
-
|
|
152525
|
-
|
|
152526
|
-
|
|
152527
|
-
|
|
152528
|
-
|
|
152529
|
-
|
|
152530
|
-
|
|
152531
|
-
|
|
152532
|
-
if (opts.save) {
|
|
152533
|
-
const { mkdirSync: mkdirSync13 } = await import("fs");
|
|
152534
|
-
saveDir = opts.save;
|
|
152535
|
-
mkdirSync13(saveDir, { recursive: true });
|
|
152536
|
-
info(`Saving audio to: ${saveDir}`);
|
|
152774
|
+
const exitCode = await proc.exited;
|
|
152775
|
+
return exitCode === 0;
|
|
152776
|
+
}
|
|
152777
|
+
async function restartPm2Services(processNames) {
|
|
152778
|
+
const serverConfig = loadServerConfig();
|
|
152779
|
+
const cliConfig = loadLocalRuntimeConfig();
|
|
152780
|
+
const runtimeEnv = buildRuntimeEnv(serverConfig, cliConfig);
|
|
152781
|
+
let allSucceeded = true;
|
|
152782
|
+
for (const name of processNames) {
|
|
152783
|
+
const proc = Bun.spawn({
|
|
152784
|
+
cmd: ["pm2", "restart", name],
|
|
152785
|
+
stdout: "pipe",
|
|
152786
|
+
stderr: "pipe",
|
|
152787
|
+
env: { ...process.env, ...runtimeEnv }
|
|
152788
|
+
});
|
|
152789
|
+
const exitCode = await proc.exited;
|
|
152790
|
+
if (exitCode !== 0) {
|
|
152791
|
+
allSucceeded = false;
|
|
152537
152792
|
}
|
|
152538
|
-
|
|
152539
|
-
|
|
152540
|
-
|
|
152541
|
-
|
|
152542
|
-
|
|
152543
|
-
|
|
152544
|
-
|
|
152545
|
-
|
|
152546
|
-
};
|
|
152547
|
-
|
|
152548
|
-
|
|
152549
|
-
|
|
152550
|
-
|
|
152551
|
-
|
|
152552
|
-
|
|
152553
|
-
|
|
152554
|
-
|
|
152555
|
-
|
|
152556
|
-
|
|
152557
|
-
|
|
152558
|
-
|
|
152559
|
-
|
|
152560
|
-
|
|
152561
|
-
|
|
152562
|
-
|
|
152563
|
-
|
|
152564
|
-
|
|
152565
|
-
|
|
152566
|
-
|
|
152567
|
-
|
|
152568
|
-
|
|
152569
|
-
|
|
152570
|
-
|
|
152571
|
-
|
|
152572
|
-
|
|
152573
|
-
|
|
152574
|
-
|
|
152575
|
-
|
|
152576
|
-
|
|
152577
|
-
${lines.join(`
|
|
152578
|
-
`)}`);
|
|
152579
|
-
}
|
|
152580
|
-
};
|
|
152581
|
-
ws.onerror = () => {
|
|
152582
|
-
error("WebSocket error");
|
|
152583
|
-
};
|
|
152584
|
-
ws.onclose = (ev2) => {
|
|
152585
|
-
info(`Disconnected: code=${ev2.code} ${ev2.reason}`);
|
|
152586
|
-
const elapsed = ((Date.now() - startTime) / 1000).toFixed(0);
|
|
152587
|
-
info(`Session lasted ${elapsed}s, ${totalFrames} total frames`);
|
|
152588
|
-
for (const [uid2, st] of stats) {
|
|
152589
|
-
info(` ${uid2}: ${st.frames} frames, ${(st.bytes / 1024).toFixed(1)}KB`);
|
|
152793
|
+
}
|
|
152794
|
+
return allSucceeded;
|
|
152795
|
+
}
|
|
152796
|
+
function normalizeVersion(version2) {
|
|
152797
|
+
return version2.split("+")[0] ?? version2;
|
|
152798
|
+
}
|
|
152799
|
+
function decideVerify(args) {
|
|
152800
|
+
if (args.skipReason !== undefined) {
|
|
152801
|
+
return { kind: "skipped", reason: args.skipReason };
|
|
152802
|
+
}
|
|
152803
|
+
const cliVersion = normalizeVersion(args.latest);
|
|
152804
|
+
if (args.healthBody === null) {
|
|
152805
|
+
return { kind: "health-unreachable", apiPort: args.apiPort };
|
|
152806
|
+
}
|
|
152807
|
+
const serverVersion = args.healthBody.version ? normalizeVersion(args.healthBody.version) : null;
|
|
152808
|
+
if (!serverVersion || serverVersion !== cliVersion) {
|
|
152809
|
+
return { kind: "version-mismatch", cliVersion, serverVersion };
|
|
152810
|
+
}
|
|
152811
|
+
if (!args.keyValid) {
|
|
152812
|
+
return { kind: "auth-invalid" };
|
|
152813
|
+
}
|
|
152814
|
+
return { kind: "ok", cliVersion, serverVersion };
|
|
152815
|
+
}
|
|
152816
|
+
function updateErrorVersionMismatch(cli, server) {
|
|
152817
|
+
return `Server version mismatch: cli=v${cli} server=v${server ?? "unknown"}. Run: omni doctor`;
|
|
152818
|
+
}
|
|
152819
|
+
var UPDATE_ERROR_AUTH_INVALID = "Auth key invalid after restart. Run: omni doctor --fix";
|
|
152820
|
+
async function fetchHealthBody(apiPort, timeoutMs) {
|
|
152821
|
+
const url = getHealthCheckUrl(apiPort);
|
|
152822
|
+
const deadline = Date.now() + timeoutMs;
|
|
152823
|
+
while (Date.now() < deadline) {
|
|
152824
|
+
try {
|
|
152825
|
+
const resp = await fetch(url, {
|
|
152826
|
+
signal: AbortSignal.timeout(1500),
|
|
152827
|
+
headers: { "Accept-Encoding": "identity" }
|
|
152828
|
+
});
|
|
152829
|
+
if (resp.ok) {
|
|
152830
|
+
const body = await resp.json();
|
|
152831
|
+
return body;
|
|
152590
152832
|
}
|
|
152591
|
-
|
|
152592
|
-
|
|
152593
|
-
|
|
152594
|
-
|
|
152595
|
-
ws.close();
|
|
152596
|
-
});
|
|
152597
|
-
await new Promise(() => {});
|
|
152598
|
-
});
|
|
152599
|
-
return voice;
|
|
152833
|
+
} catch {}
|
|
152834
|
+
await Bun.sleep(VERIFY_POLL_INTERVAL_MS);
|
|
152835
|
+
}
|
|
152836
|
+
return null;
|
|
152600
152837
|
}
|
|
152601
|
-
|
|
152602
|
-
|
|
152603
|
-
|
|
152604
|
-
|
|
152605
|
-
init_output();
|
|
152606
|
-
var SIGNATURE_ALGORITHMS = ["hmac-sha256", "hmac-sha1", "token-match"];
|
|
152607
|
-
var signatureSecretSchema = exports_external.string().min(8, "at least 8 characters").max(512, "at most 512 characters");
|
|
152608
|
-
var SECRET_SOURCE_FLAGS = "--signature-secret, --signature-secret-env, --signature-secret-stdin";
|
|
152609
|
-
async function readSecretFromStdin() {
|
|
152610
|
-
if (process.stdin.isTTY) {
|
|
152611
|
-
throw new Error('--signature-secret-stdin requires piped stdin (e.g. `printf %s "$SECRET" | omni webhooks ...`)');
|
|
152838
|
+
async function validateStoredKey(apiPort) {
|
|
152839
|
+
const cliConfig = loadLocalRuntimeConfig();
|
|
152840
|
+
if (!cliConfig.apiKey) {
|
|
152841
|
+
return false;
|
|
152612
152842
|
}
|
|
152613
|
-
const
|
|
152614
|
-
|
|
152615
|
-
|
|
152843
|
+
const baseUrl = cliConfig.apiUrl ?? `http://localhost:${apiPort}`;
|
|
152844
|
+
try {
|
|
152845
|
+
const client = createOmniClient({ baseUrl, apiKey: cliConfig.apiKey, cliVersion: VERSION });
|
|
152846
|
+
const result = await client.auth.validate();
|
|
152847
|
+
return result.valid === true;
|
|
152848
|
+
} catch {
|
|
152849
|
+
return false;
|
|
152616
152850
|
}
|
|
152617
|
-
return Buffer.concat(chunks).toString("utf-8");
|
|
152618
152851
|
}
|
|
152619
|
-
|
|
152620
|
-
|
|
152621
|
-
|
|
152622
|
-
|
|
152623
|
-
|
|
152624
|
-
|
|
152625
|
-
|
|
152626
|
-
|
|
152627
|
-
|
|
152628
|
-
|
|
152852
|
+
function printVerifyBanner(latest) {
|
|
152853
|
+
console.log(`${source_default.green("\u2713")} CLI: v${latest}`);
|
|
152854
|
+
console.log(`${source_default.green("\u2713")} Server: v${latest} (healthy)`);
|
|
152855
|
+
console.log(`${source_default.green("\u2713")} Auth: key valid`);
|
|
152856
|
+
}
|
|
152857
|
+
function printVerifySkippedBanner(latest) {
|
|
152858
|
+
console.log(`${source_default.green("\u2713")} CLI: v${latest}`);
|
|
152859
|
+
console.log(`${source_default.yellow("-")} Server: v${latest} (skipped)`);
|
|
152860
|
+
console.log(`${source_default.yellow("-")} Auth: skipped`);
|
|
152861
|
+
}
|
|
152862
|
+
function detectParallelNpmGlobalInstall(deps) {
|
|
152863
|
+
const npmRootFn = deps?.npmRoot ?? defaultNpmRoot;
|
|
152864
|
+
const existsFn = deps?.exists ?? existsSync20;
|
|
152865
|
+
let root;
|
|
152866
|
+
try {
|
|
152867
|
+
root = npmRootFn();
|
|
152868
|
+
} catch {
|
|
152869
|
+
return { detected: false, skipped: "npm-root-failed" };
|
|
152629
152870
|
}
|
|
152630
|
-
|
|
152631
|
-
|
|
152632
|
-
if (options3.signatureSecretEnv !== undefined) {
|
|
152633
|
-
const name = options3.signatureSecretEnv;
|
|
152634
|
-
if (!name)
|
|
152635
|
-
throw new Error("--signature-secret-env requires a variable name");
|
|
152636
|
-
raw2 = process.env[name];
|
|
152637
|
-
origin = `environment variable ${name}`;
|
|
152638
|
-
if (raw2 === undefined || raw2 === "") {
|
|
152639
|
-
throw new Error(`${origin} is not set or empty`);
|
|
152640
|
-
}
|
|
152641
|
-
} else if (options3.signatureSecretStdin) {
|
|
152642
|
-
raw2 = (await readStdin()).replace(/\r?\n$/, "");
|
|
152643
|
-
origin = "stdin";
|
|
152644
|
-
if (!raw2)
|
|
152645
|
-
throw new Error("no secret received on stdin");
|
|
152646
|
-
} else {
|
|
152647
|
-
raw2 = options3.signatureSecret;
|
|
152648
|
-
origin = "--signature-secret";
|
|
152871
|
+
if (root === null || root.length === 0) {
|
|
152872
|
+
return { detected: false, skipped: "npm-not-on-path" };
|
|
152649
152873
|
}
|
|
152650
|
-
const
|
|
152651
|
-
if (
|
|
152652
|
-
|
|
152653
|
-
throw new Error(`Invalid signature secret from ${origin}: ${reason}`);
|
|
152874
|
+
const candidate = join19(root, "@automagik", "omni");
|
|
152875
|
+
if (existsFn(candidate)) {
|
|
152876
|
+
return { detected: true, path: candidate };
|
|
152654
152877
|
}
|
|
152655
|
-
return
|
|
152878
|
+
return { detected: false };
|
|
152656
152879
|
}
|
|
152657
|
-
function
|
|
152658
|
-
|
|
152659
|
-
|
|
152660
|
-
|
|
152661
|
-
|
|
152662
|
-
|
|
152880
|
+
function defaultNpmRoot() {
|
|
152881
|
+
try {
|
|
152882
|
+
const result = Bun.spawnSync({
|
|
152883
|
+
cmd: ["npm", "root", "-g"],
|
|
152884
|
+
stdout: "pipe",
|
|
152885
|
+
stderr: "pipe",
|
|
152886
|
+
timeout: 5000
|
|
152887
|
+
});
|
|
152888
|
+
if (result.exitCode !== 0)
|
|
152889
|
+
return null;
|
|
152890
|
+
const text3 = new TextDecoder().decode(result.stdout).trim();
|
|
152891
|
+
return text3.length > 0 ? text3 : null;
|
|
152892
|
+
} catch {
|
|
152893
|
+
return null;
|
|
152663
152894
|
}
|
|
152664
|
-
|
|
152665
|
-
|
|
152666
|
-
|
|
152895
|
+
}
|
|
152896
|
+
async function runLegacyCleanup(skipList) {
|
|
152897
|
+
const cleanupSpinner = ora("Checking for legacy artifacts to clean up...").start();
|
|
152898
|
+
const report = await cleanupLegacyArtifacts(skipList);
|
|
152899
|
+
cleanupSpinner.stop();
|
|
152900
|
+
for (const outcome of report.outcomes) {
|
|
152901
|
+
if (outcome.state === "ran" && outcome.summary.length > 0) {
|
|
152902
|
+
console.log(outcome.summary);
|
|
152903
|
+
}
|
|
152667
152904
|
}
|
|
152668
|
-
|
|
152669
|
-
|
|
152905
|
+
return report;
|
|
152906
|
+
}
|
|
152907
|
+
var OMNI_UPDATE_SKIP_MAINTENANCE_ENV = "OMNI_UPDATE_SKIP_MAINTENANCE";
|
|
152908
|
+
function resolveMaintenanceSkipReason(args) {
|
|
152909
|
+
if (!args.verifyOk)
|
|
152910
|
+
return "verify-failed";
|
|
152911
|
+
if (args.skipMaintenance === true)
|
|
152912
|
+
return "cli-flag";
|
|
152913
|
+
const raw2 = args.env[OMNI_UPDATE_SKIP_MAINTENANCE_ENV];
|
|
152914
|
+
if (raw2 !== undefined && raw2 !== "" && raw2 !== "0" && raw2.toLowerCase() !== "false") {
|
|
152915
|
+
return "env";
|
|
152670
152916
|
}
|
|
152671
|
-
return
|
|
152917
|
+
return null;
|
|
152672
152918
|
}
|
|
152673
|
-
|
|
152674
|
-
|
|
152675
|
-
|
|
152676
|
-
|
|
152677
|
-
|
|
152678
|
-
|
|
152679
|
-
|
|
152680
|
-
path: exports_external.string().min(1, "path must be 1-200 characters").max(200, "path must be 1-200 characters")
|
|
152681
|
-
})
|
|
152682
|
-
]);
|
|
152683
|
-
var EVENT_TYPE_MAPPING_HINT = 'expected {"source":"header","header":"X-GitHub-Event"} or {"source":"body","path":"event"}';
|
|
152684
|
-
function resolveEventTypeMapping(raw2) {
|
|
152685
|
-
if (raw2 === undefined)
|
|
152686
|
-
return;
|
|
152687
|
-
let text3 = raw2;
|
|
152688
|
-
if (raw2.startsWith("@")) {
|
|
152689
|
-
const path = raw2.slice(1);
|
|
152690
|
-
try {
|
|
152691
|
-
text3 = readFileSync19(path, "utf-8");
|
|
152692
|
-
} catch (err2) {
|
|
152693
|
-
const reason = err2 instanceof Error ? err2.message : "Unknown error";
|
|
152694
|
-
throw new Error(`Cannot read --event-type-mapping file '${path}': ${reason}`);
|
|
152695
|
-
}
|
|
152919
|
+
function formatMaintenanceSummary(report) {
|
|
152920
|
+
const { ok: ok3, warn: warn2, fail } = report.summary;
|
|
152921
|
+
return `Maintenance: ${ok3} ok, ${warn2} warn, ${fail} fail`;
|
|
152922
|
+
}
|
|
152923
|
+
async function runPostUpdateMaintenance(args) {
|
|
152924
|
+
if (args.skipReason !== null) {
|
|
152925
|
+
return { outcome: "skipped", durationMs: 0, skipReason: args.skipReason };
|
|
152696
152926
|
}
|
|
152697
|
-
|
|
152927
|
+
const impl = args.runDoctorImpl ?? runDoctor;
|
|
152928
|
+
const startedAt = Date.now();
|
|
152698
152929
|
try {
|
|
152699
|
-
|
|
152700
|
-
|
|
152701
|
-
|
|
152930
|
+
const doctorReport = await impl({ json: true, dryRun: true });
|
|
152931
|
+
return {
|
|
152932
|
+
outcome: "completed",
|
|
152933
|
+
durationMs: Date.now() - startedAt,
|
|
152934
|
+
doctorReport
|
|
152935
|
+
};
|
|
152936
|
+
} catch (err2) {
|
|
152937
|
+
return {
|
|
152938
|
+
outcome: "failed",
|
|
152939
|
+
durationMs: Date.now() - startedAt,
|
|
152940
|
+
error: err2 instanceof Error ? err2.message : String(err2)
|
|
152941
|
+
};
|
|
152702
152942
|
}
|
|
152703
|
-
|
|
152704
|
-
|
|
152705
|
-
|
|
152706
|
-
|
|
152943
|
+
}
|
|
152944
|
+
async function restartServicesAndVerify(servicesToRestart, latest, options3, diagnostics, finalize) {
|
|
152945
|
+
const apiPort = loadServerConfig().port;
|
|
152946
|
+
diagnostics.restart.attempted = true;
|
|
152947
|
+
diagnostics.restart.services = [...servicesToRestart];
|
|
152948
|
+
const restartSpinner = ora("Restarting services...").start();
|
|
152949
|
+
const restartSucceeded = await restartPm2Services(servicesToRestart);
|
|
152950
|
+
restartSpinner.stop();
|
|
152951
|
+
diagnostics.restart.succeeded = restartSucceeded;
|
|
152952
|
+
if (!restartSucceeded) {
|
|
152953
|
+
warn(`omni CLI updated to v${latest}, but one or more service restarts failed. Run \`omni status\`.`);
|
|
152954
|
+
finalize(1);
|
|
152955
|
+
}
|
|
152956
|
+
let cleanupReport = null;
|
|
152957
|
+
if (options3.legacyCleanup) {
|
|
152958
|
+
cleanupReport = await runLegacyCleanup(options3.skipList);
|
|
152959
|
+
diagnostics.cleanups = cleanupReport;
|
|
152960
|
+
}
|
|
152961
|
+
if (options3.skipVerify) {
|
|
152962
|
+
const result2 = decideVerify({ skipReason: "no-verify-flag" });
|
|
152963
|
+
diagnostics.verify = result2;
|
|
152964
|
+
printVerifySkippedBanner(latest);
|
|
152965
|
+
if (cleanupReport !== null && !cleanupReport.succeeded) {
|
|
152966
|
+
warn("One or more legacy artifacts could not be cleaned up automatically. " + "See messages above and docs/migration/nats-genie-sidecar-decommission.md.");
|
|
152967
|
+
}
|
|
152968
|
+
const maintenance = await runPostUpdateMaintenance({ skipReason: "verify-failed" });
|
|
152969
|
+
diagnostics.maintenance = maintenance;
|
|
152970
|
+
return;
|
|
152971
|
+
}
|
|
152972
|
+
const verifySpinner = ora("Verifying server version...").start();
|
|
152973
|
+
const healthBody = await fetchHealthBody(apiPort, UPDATE_HEALTH_TIMEOUT_MS);
|
|
152974
|
+
verifySpinner.stop();
|
|
152975
|
+
const keyValid = healthBody !== null ? await validateStoredKey(apiPort) : false;
|
|
152976
|
+
const result = decideVerify({ latest, apiPort, healthBody, keyValid });
|
|
152977
|
+
diagnostics.verify = result;
|
|
152978
|
+
switch (result.kind) {
|
|
152979
|
+
case "ok": {
|
|
152980
|
+
printVerifyBanner(result.cliVersion);
|
|
152981
|
+
if (cleanupReport !== null && !cleanupReport.succeeded) {
|
|
152982
|
+
warn("One or more legacy artifacts could not be cleaned up automatically. " + "See messages above and docs/migration/nats-genie-sidecar-decommission.md.");
|
|
152983
|
+
}
|
|
152984
|
+
const skipReason = resolveMaintenanceSkipReason({
|
|
152985
|
+
verifyOk: true,
|
|
152986
|
+
skipMaintenance: options3.skipMaintenance,
|
|
152987
|
+
env: process.env
|
|
152988
|
+
});
|
|
152989
|
+
const maintenance = await runPostUpdateMaintenance({ skipReason });
|
|
152990
|
+
diagnostics.maintenance = maintenance;
|
|
152991
|
+
if (maintenance.outcome === "completed" && maintenance.doctorReport) {
|
|
152992
|
+
info(formatMaintenanceSummary(maintenance.doctorReport));
|
|
152993
|
+
} else if (maintenance.outcome === "failed") {
|
|
152994
|
+
warn(`Maintenance: skipped (runDoctor failed: ${maintenance.error ?? "unknown error"})`);
|
|
152995
|
+
}
|
|
152996
|
+
return;
|
|
152997
|
+
}
|
|
152998
|
+
case "health-unreachable":
|
|
152999
|
+
warn(`omni CLI updated to v${latest}, but health check failed on port ${result.apiPort}. Run \`omni status\`.`);
|
|
153000
|
+
finalize(1);
|
|
153001
|
+
break;
|
|
153002
|
+
case "version-mismatch":
|
|
153003
|
+
console.error(`${source_default.red("\u2717")} ${updateErrorVersionMismatch(result.cliVersion, result.serverVersion)}`);
|
|
153004
|
+
finalize(1);
|
|
153005
|
+
break;
|
|
153006
|
+
case "auth-invalid":
|
|
153007
|
+
console.error(`${source_default.red("\u2717")} ${UPDATE_ERROR_AUTH_INVALID}`);
|
|
153008
|
+
finalize(1);
|
|
153009
|
+
break;
|
|
152707
153010
|
}
|
|
152708
|
-
return result.data;
|
|
152709
153011
|
}
|
|
152710
|
-
async function
|
|
152711
|
-
const
|
|
152712
|
-
|
|
152713
|
-
|
|
152714
|
-
|
|
152715
|
-
|
|
152716
|
-
|
|
152717
|
-
|
|
152718
|
-
|
|
152719
|
-
|
|
152720
|
-
|
|
152721
|
-
|
|
153012
|
+
async function promptConfirm(question) {
|
|
153013
|
+
const rl = createInterface3({
|
|
153014
|
+
input: process.stdin,
|
|
153015
|
+
output: process.stdout
|
|
153016
|
+
});
|
|
153017
|
+
return new Promise((resolve4) => {
|
|
153018
|
+
rl.question(question, (answer) => {
|
|
153019
|
+
rl.close();
|
|
153020
|
+
const trimmed = answer.trim().toLowerCase();
|
|
153021
|
+
resolve4(trimmed === "" || trimmed === "y" || trimmed === "yes");
|
|
153022
|
+
});
|
|
153023
|
+
});
|
|
153024
|
+
}
|
|
153025
|
+
async function runUpdate(options3) {
|
|
153026
|
+
const channel2 = resolveChannel(options3);
|
|
153027
|
+
if (options3.next || options3.stable) {
|
|
153028
|
+
persistChannel(channel2);
|
|
152722
153029
|
}
|
|
152723
|
-
|
|
152724
|
-
|
|
152725
|
-
const
|
|
152726
|
-
|
|
152727
|
-
|
|
152728
|
-
|
|
152729
|
-
|
|
152730
|
-
|
|
152731
|
-
|
|
152732
|
-
|
|
152733
|
-
|
|
152734
|
-
|
|
153030
|
+
const currentClean = VERSION.split("+")[0];
|
|
153031
|
+
const diagnostics = createDiagnostics({ runningVersion: currentClean, channel: channel2 });
|
|
153032
|
+
const finalize = (exitCode) => {
|
|
153033
|
+
const path = writeDiagnostics(diagnostics, exitCode);
|
|
153034
|
+
if (path !== null && process.env.OMNI_UPDATE_DIAGNOSTICS_VERBOSE === "1") {
|
|
153035
|
+
info(`Diagnostics written: ${path}`);
|
|
153036
|
+
}
|
|
153037
|
+
process.exit(exitCode);
|
|
153038
|
+
};
|
|
153039
|
+
const parallelInstall = detectParallelNpmGlobalInstall();
|
|
153040
|
+
diagnostics.parallelNpmGlobal = parallelInstall;
|
|
153041
|
+
if (parallelInstall.detected && parallelInstall.path) {
|
|
153042
|
+
warn(`Parallel npm-global install of ${PACKAGE_NAME} detected at ${parallelInstall.path}. This may shadow the bun-installed binary on PATH. Recommended: npm uninstall -g ${PACKAGE_NAME}`);
|
|
152735
153043
|
}
|
|
152736
|
-
|
|
152737
|
-
|
|
152738
|
-
|
|
152739
|
-
|
|
152740
|
-
|
|
152741
|
-
|
|
152742
|
-
|
|
152743
|
-
|
|
152744
|
-
function assertPairedSignatureOnCreate(signatureConfig, signatureSecret) {
|
|
152745
|
-
if (signatureConfig === undefined === (signatureSecret === undefined))
|
|
152746
|
-
return;
|
|
152747
|
-
if (signatureConfig === undefined) {
|
|
152748
|
-
throw new Error(`a signature secret (${SECRET_SOURCE_FLAGS}) requires --signature-algorithm and --signature-header`);
|
|
153044
|
+
info(`Channel: ${channel2}${channel2 === "next" ? " (dev builds)" : " (stable)"}`);
|
|
153045
|
+
const versionSpinner = ora(`Checking ${channel2} version of ${PACKAGE_NAME}...`).start();
|
|
153046
|
+
const latest = await fetchLatestVersion(channel2);
|
|
153047
|
+
versionSpinner.stop();
|
|
153048
|
+
diagnostics.registry.latestVersion = latest;
|
|
153049
|
+
if (latest === null) {
|
|
153050
|
+
warn("Could not reach npm registry. Check your network connection and try again.");
|
|
153051
|
+
return finalize(1);
|
|
152749
153052
|
}
|
|
152750
|
-
|
|
152751
|
-
}
|
|
152752
|
-
|
|
152753
|
-
var MUTATION_POLICIES = ["same_id", "new_id"];
|
|
152754
|
-
function parseExpectedInterval(raw2) {
|
|
152755
|
-
if (raw2 === undefined)
|
|
152756
|
-
return;
|
|
152757
|
-
const seconds = Number(raw2);
|
|
152758
|
-
if (!Number.isInteger(seconds) || seconds < 1) {
|
|
152759
|
-
error("--expected-interval must be a positive integer number of seconds");
|
|
153053
|
+
if (currentClean === latest) {
|
|
153054
|
+
success(`Already up to date (v${latest}, channel: ${channel2})`);
|
|
153055
|
+
return finalize(0);
|
|
152760
153056
|
}
|
|
152761
|
-
|
|
152762
|
-
|
|
152763
|
-
|
|
152764
|
-
|
|
152765
|
-
|
|
152766
|
-
|
|
152767
|
-
|
|
152768
|
-
error(`Invalid --window-semantics; expected one of: ${WINDOW_SEMANTICS.join(", ")}`);
|
|
153057
|
+
info(`Update available: v${currentClean} \u2192 v${latest} (${channel2})`);
|
|
153058
|
+
if (!options3.yes) {
|
|
153059
|
+
const confirmed = await promptConfirm(`Update from v${currentClean} to v${latest}? [Y/n] `);
|
|
153060
|
+
if (!confirmed) {
|
|
153061
|
+
info("Update cancelled.");
|
|
153062
|
+
return finalize(0);
|
|
153063
|
+
}
|
|
152769
153064
|
}
|
|
152770
|
-
|
|
152771
|
-
}
|
|
152772
|
-
|
|
152773
|
-
|
|
152774
|
-
|
|
152775
|
-
|
|
152776
|
-
|
|
152777
|
-
|
|
153065
|
+
const servicesToRestart = options3.restart !== false ? getRunningPm2Services() : [];
|
|
153066
|
+
const installSpinner = ora(`Updating ${PACKAGE_NAME}@${channel2}...`).start();
|
|
153067
|
+
diagnostics.install.attempted = true;
|
|
153068
|
+
diagnostics.install.targetVersion = latest;
|
|
153069
|
+
const installed = await installLatest(channel2);
|
|
153070
|
+
installSpinner.stop();
|
|
153071
|
+
diagnostics.install.succeeded = installed;
|
|
153072
|
+
if (!installed) {
|
|
153073
|
+
warn(`Installation failed. Your current version (v${currentClean}) is still intact.`);
|
|
153074
|
+
return finalize(1);
|
|
152778
153075
|
}
|
|
152779
|
-
|
|
152780
|
-
|
|
152781
|
-
|
|
152782
|
-
if (options3.clearCadence && options3.expectedInterval !== undefined) {
|
|
152783
|
-
error("Use only one of --expected-interval and --clear-cadence");
|
|
153076
|
+
const sidecarCleanupExplicit = options3.sidecarCleanup === false;
|
|
153077
|
+
if (sidecarCleanupExplicit) {
|
|
153078
|
+
info("--no-sidecar-cleanup (deprecated alias for --no-legacy-cleanup)");
|
|
152784
153079
|
}
|
|
152785
|
-
const
|
|
152786
|
-
|
|
152787
|
-
|
|
152788
|
-
|
|
152789
|
-
|
|
152790
|
-
|
|
152791
|
-
|
|
152792
|
-
|
|
152793
|
-
|
|
152794
|
-
|
|
152795
|
-
|
|
152796
|
-
|
|
152797
|
-
|
|
153080
|
+
const legacyCleanupEnabled = options3.legacyCleanup !== false && options3.sidecarCleanup !== false;
|
|
153081
|
+
const skipList = parseSkipCleanupList(options3.skipCleanup);
|
|
153082
|
+
if (servicesToRestart.length > 0) {
|
|
153083
|
+
await restartServicesAndVerify(servicesToRestart, latest, {
|
|
153084
|
+
legacyCleanup: legacyCleanupEnabled,
|
|
153085
|
+
skipList,
|
|
153086
|
+
skipMaintenance: options3.skipMaintenance === true,
|
|
153087
|
+
skipVerify: options3.verify === false
|
|
153088
|
+
}, diagnostics, finalize);
|
|
153089
|
+
return finalize(0);
|
|
153090
|
+
}
|
|
153091
|
+
diagnostics.verify = decideVerify({ skipReason: "no-restart" });
|
|
153092
|
+
success(`omni updated to v${latest}`);
|
|
153093
|
+
return finalize(0);
|
|
152798
153094
|
}
|
|
152799
|
-
function
|
|
152800
|
-
|
|
152801
|
-
|
|
152802
|
-
|
|
152803
|
-
|
|
152804
|
-
|
|
152805
|
-
|
|
152806
|
-
|
|
152807
|
-
|
|
152808
|
-
|
|
152809
|
-
|
|
152810
|
-
|
|
152811
|
-
|
|
152812
|
-
|
|
152813
|
-
|
|
152814
|
-
|
|
152815
|
-
|
|
152816
|
-
|
|
152817
|
-
|
|
152818
|
-
|
|
152819
|
-
|
|
152820
|
-
|
|
152821
|
-
|
|
152822
|
-
|
|
152823
|
-
|
|
152824
|
-
|
|
152825
|
-
|
|
152826
|
-
|
|
152827
|
-
|
|
153095
|
+
function parseSkipCleanupList(value) {
|
|
153096
|
+
if (!value)
|
|
153097
|
+
return new Set;
|
|
153098
|
+
const names = value.split(",").map((s2) => s2.trim()).filter((s2) => s2.length > 0);
|
|
153099
|
+
return new Set(names);
|
|
153100
|
+
}
|
|
153101
|
+
function createUpdateCommand() {
|
|
153102
|
+
return new Command("update").description(`Update ${PACKAGE_NAME} to the latest version (restart only services already running)`).option("-y, --yes", "Skip confirmation prompts (non-interactive)").option("--no-restart", "Update CLI only; skip service restarts and verification").option("--no-verify", "Restart services but skip the post-restart probe (use when a release ships a broken /api/v2/health and operators need to roll forward)").option("--no-legacy-cleanup", "Skip every registered legacy-artifact cleanup (e.g. nats-reply-sidecar)").option("--no-sidecar-cleanup", "Deprecated alias for --no-legacy-cleanup").option("--skip-cleanup <names>", 'Comma-separated list of legacy-cleanup registry entries to skip (e.g. "nats-reply-sidecar")').option("--next", "Switch to dev builds (npm @next tag) and persist as default").option("--stable", "Switch to stable releases (npm @latest tag) and persist as default").option("--skip-maintenance", `Skip the post-update maintenance hook (read-only \`omni doctor\` sweep). Also honored via the ${OMNI_UPDATE_SKIP_MAINTENANCE_ENV} env var.`).addHelpText("after", `
|
|
153103
|
+
Channels:
|
|
153104
|
+
- stable (default) \u2014 tracks the npm @latest tag, bumped from main branch releases.
|
|
153105
|
+
- next (dev builds) \u2014 tracks the npm @next tag, bumped on every CI-green dev merge.
|
|
153106
|
+
|
|
153107
|
+
Use --next to switch to dev builds; --stable to switch back. The choice is
|
|
153108
|
+
persisted to ~/.omni/config.json under 'updateChannel' so subsequent
|
|
153109
|
+
'omni update' calls stay on the selected track. Check or change manually:
|
|
153110
|
+
omni config get updateChannel
|
|
153111
|
+
omni config set updateChannel next
|
|
153112
|
+
omni config set updateChannel latest
|
|
153113
|
+
|
|
153114
|
+
Behavior:
|
|
153115
|
+
- Installs the latest CLI package first.
|
|
153116
|
+
- Restarts tracked Omni services only when they were online before the update.
|
|
153117
|
+
- When that restart path runs, update performs 3-step verification:
|
|
153118
|
+
1. Server version matches the new CLI version (via /api/v2/health).
|
|
153119
|
+
2. Stored CLI API key still validates against the server.
|
|
153120
|
+
3. On success, prints:
|
|
153121
|
+
\u2713 CLI: v<latest>
|
|
153122
|
+
\u2713 Server: v<latest> (healthy)
|
|
153123
|
+
\u2713 Auth: key valid
|
|
153124
|
+
- On mismatch, exits non-zero with:
|
|
153125
|
+
"Server version mismatch: cli=v<X> server=v<Y>. Run: omni doctor"
|
|
153126
|
+
- On auth failure, exits non-zero with:
|
|
153127
|
+
"Auth key invalid after restart. Run: omni doctor --fix"
|
|
153128
|
+
- After a successful restart, runs every registered legacy-artifact cleanup.
|
|
153129
|
+
The day-one entry is nats-reply-sidecar: it scans for any legacy
|
|
153130
|
+
nats-reply-sidecar.mjs process (PM2-managed or raw) and stops it. The
|
|
153131
|
+
sidecar was an external workaround for bugs that were fixed in #362;
|
|
153132
|
+
leaving it running causes every agent reply to be delivered twice.
|
|
153133
|
+
Skip every cleanup with --no-legacy-cleanup, or skip a single registry
|
|
153134
|
+
entry with --skip-cleanup=<name1,name2>. The deprecated alias
|
|
153135
|
+
--no-sidecar-cleanup still works and behaves identically. Manual runbook:
|
|
153136
|
+
docs/migration/nats-genie-sidecar-decommission.md
|
|
153137
|
+
- Use --no-restart to skip restart + verification entirely. --no-restart
|
|
153138
|
+
also skips legacy-artifact cleanup; manage those services manually.
|
|
153139
|
+
- Use --no-verify to restart services but skip the post-restart probe.
|
|
153140
|
+
The banner shows the new CLI version on both lines but tags Server /
|
|
153141
|
+
Auth as "(skipped)"; maintenance is auto-skipped because verify never
|
|
153142
|
+
confirmed health.
|
|
153143
|
+
- Detects parallel npm-global installs of ${PACKAGE_NAME} (omni installs
|
|
153144
|
+
via bun-global; an npm-global install hides stale binaries on PATH and
|
|
153145
|
+
confuses 'which omni'). When found, prints a one-line warning naming
|
|
153146
|
+
the offending path; recommended remediation: npm uninstall -g ${PACKAGE_NAME}.
|
|
153147
|
+
- Every invocation writes a diagnostics record to
|
|
153148
|
+
~/.omni/logs/update-diagnostics-<iso>.json (schemaVersion: 1). Captures
|
|
153149
|
+
install attempt, registry probe, restart outcome, verify result, cleanup
|
|
153150
|
+
registry result, maintenance hook, and a tail of pm2 log signals. Set
|
|
153151
|
+
OMNI_UPDATE_DIAGNOSTICS_VERBOSE=1 to print the path on completion. A
|
|
153152
|
+
failed write never changes the update exit code.
|
|
153153
|
+
- On a successful restart + verify, runs a post-update maintenance hook:
|
|
153154
|
+
a read-only \`omni doctor\` sweep that prints a one-line summary
|
|
153155
|
+
("Maintenance: <ok> ok, <warn> warn, <fail> fail"). The probe never
|
|
153156
|
+
mutates pm2 / config / DB state (\`omni doctor --fix\` remains the
|
|
153157
|
+
explicit operator action for repair). A failing maintenance call is
|
|
153158
|
+
non-fatal \u2014 exit code stays 0 with a banner warning. Skip with
|
|
153159
|
+
--skip-maintenance or ${OMNI_UPDATE_SKIP_MAINTENANCE_ENV}=1.
|
|
153160
|
+
- Verify runtime health after update with: omni status
|
|
153161
|
+
`).action(runUpdate);
|
|
153162
|
+
}
|
|
153163
|
+
|
|
153164
|
+
// src/commands/use.ts
|
|
153165
|
+
init_output();
|
|
153166
|
+
function createUseCommand() {
|
|
153167
|
+
return new Command("use").description("Set active instance for verb commands").argument("<instance>", "Instance name, ID, or prefix").action(async (instance3) => {
|
|
152828
153168
|
const client = getClient();
|
|
153169
|
+
const instanceId = await resolveInstanceId(instance3);
|
|
152829
153170
|
try {
|
|
152830
|
-
|
|
152831
|
-
data(source);
|
|
153171
|
+
await client.context.use(instanceId);
|
|
152832
153172
|
} catch (err2) {
|
|
152833
153173
|
const message2 = err2 instanceof Error ? err2.message : "Unknown error";
|
|
152834
|
-
error(`Failed to
|
|
153174
|
+
return error(`Failed to set active instance: ${message2}`);
|
|
152835
153175
|
}
|
|
152836
|
-
});
|
|
152837
|
-
webhooks.command("create").description("Create a webhook source").requiredOption("--name <name>", "Webhook source name").option("--description <desc>", "Description").option("--disabled", "Create in disabled state").option("--headers <json>", `Expected headers as JSON (e.g., '{"X-Secret": true}')`).option("--signature-algorithm <alg>", "Signature verification: hmac-sha256, hmac-sha1, or token-match").option("--signature-header <name>", "Header carrying the signature/token (e.g., X-Hub-Signature-256)").option("--signature-prefix <prefix>", "Prefix before the hex digest (e.g., 'sha256=')").option("--signature-secret <secret>", "Shared secret for signature verification (write-only). Visible in shell history and process lists \u2014 prefer --signature-secret-env or --signature-secret-stdin").option("--signature-secret-env <VAR>", "Read the shared secret from environment variable VAR").option("--signature-secret-stdin", "Read the shared secret from stdin (trailing newline stripped)").option("--idempotency-key-template <template>", "Delivery-identity key template for redelivery dedup (#958). Placeholders: {source}, {sha256(body)}, " + "{headers.<name>}, {payload.<dot.path>}. Defaults to '{source}:{sha256(body)}'").option("--strict-schemas", "Refuse deliveries whose event type has no enabled registered schema (dead-lettered as " + "schema_not_registered, #1000). Recommended for new sources. Defaults to off").option("--event-type-mapping <json>", "Semantic event-type extraction (#959): inline JSON or @path/to/file.json, e.g. " + `'{"source":"header","header":"X-GitHub-Event"}' or '{"source":"body","path":"event"}'. ` + "A mapped source emits custom.{source}.{event} instead of the collapsed custom.webhook.{source}").option("--expected-interval <seconds>", "Declared cadence: >=1 event or heartbeat per N seconds. Arms liveness supervision (#961)").option("--window-semantics <value>", `Declared window semantics: ${WINDOW_SEMANTICS.join(" or ")}`).option("--mutation-policy <value>", `Declared mutation re-emit policy: ${MUTATION_POLICIES.join(" or ")}`).action(async (options3) => {
|
|
152838
|
-
const client = getClient();
|
|
152839
153176
|
try {
|
|
152840
|
-
|
|
152841
|
-
|
|
152842
|
-
|
|
152843
|
-
|
|
152844
|
-
} catch {
|
|
152845
|
-
error("Invalid JSON for --headers");
|
|
152846
|
-
}
|
|
152847
|
-
}
|
|
152848
|
-
const signatureConfig = buildSignatureConfig(options3);
|
|
152849
|
-
const signatureSecret = await resolveSignatureSecret(options3);
|
|
152850
|
-
assertPairedSignatureOnCreate(signatureConfig, signatureSecret);
|
|
152851
|
-
const source = await client.webhooks.createSource({
|
|
152852
|
-
name: options3.name,
|
|
152853
|
-
description: options3.description,
|
|
152854
|
-
enabled: !options3.disabled,
|
|
152855
|
-
expectedHeaders,
|
|
152856
|
-
signatureConfig,
|
|
152857
|
-
signatureSecret,
|
|
152858
|
-
idempotencyKeyTemplate: options3.idempotencyKeyTemplate,
|
|
152859
|
-
strictSchemas: options3.strictSchemas,
|
|
152860
|
-
eventTypeMapping: resolveEventTypeMapping(options3.eventTypeMapping),
|
|
152861
|
-
expectedIntervalSeconds: parseExpectedInterval(options3.expectedInterval),
|
|
152862
|
-
windowSemantics: parseWindowSemantics(options3.windowSemantics),
|
|
152863
|
-
mutationPolicy: parseMutationPolicy(options3.mutationPolicy)
|
|
152864
|
-
});
|
|
152865
|
-
const details = {
|
|
152866
|
-
id: source.id,
|
|
152867
|
-
name: source.name,
|
|
152868
|
-
url: `POST /api/v2/webhooks/${source.name}`
|
|
152869
|
-
};
|
|
152870
|
-
if (signatureConfig) {
|
|
152871
|
-
details.publicUrl = `POST /api/v2/webhooks/ingress/${source.name}`;
|
|
152872
|
-
}
|
|
152873
|
-
success(`Webhook source created: ${source.id}`, details);
|
|
152874
|
-
} catch (err2) {
|
|
152875
|
-
const message2 = err2 instanceof Error ? err2.message : "Unknown error";
|
|
152876
|
-
error(`Failed to create webhook source: ${message2}`);
|
|
153177
|
+
const inst = await client.instances.get(instanceId);
|
|
153178
|
+
success(`Active instance: ${inst.name} (${instanceId.slice(0, 8)}\u2026)`);
|
|
153179
|
+
} catch {
|
|
153180
|
+
success(`Active instance: ${instanceId.slice(0, 8)}\u2026`);
|
|
152877
153181
|
}
|
|
152878
153182
|
});
|
|
152879
|
-
|
|
152880
|
-
|
|
152881
|
-
|
|
153183
|
+
}
|
|
153184
|
+
|
|
153185
|
+
// src/commands/voice.ts
|
|
153186
|
+
init_config();
|
|
153187
|
+
init_output();
|
|
153188
|
+
function apiHeaders() {
|
|
153189
|
+
const config2 = loadConfig();
|
|
153190
|
+
return { "x-api-key": config2.apiKey ?? "", "Content-Type": "application/json" };
|
|
153191
|
+
}
|
|
153192
|
+
function apiUrl(path) {
|
|
153193
|
+
const config2 = loadConfig();
|
|
153194
|
+
return `${config2.apiUrl ?? "http://localhost:8882"}/api/v2${path}`;
|
|
153195
|
+
}
|
|
153196
|
+
function createVoiceCommand() {
|
|
153197
|
+
const voice = new Command("voice").description("Voice channel operations");
|
|
153198
|
+
voice.command("join").description("Join a Discord voice channel").requiredOption("--instance <id>", "Instance ID or name").requiredOption("--channel <channelId>", "Discord voice channel ID").requiredOption("--guild <guildId>", "Discord guild ID").action(async (opts) => {
|
|
152882
153199
|
try {
|
|
152883
|
-
const
|
|
152884
|
-
|
|
152885
|
-
|
|
152886
|
-
|
|
152887
|
-
|
|
152888
|
-
|
|
153200
|
+
const res = await fetch(apiUrl("/voice/join"), {
|
|
153201
|
+
method: "POST",
|
|
153202
|
+
headers: apiHeaders(),
|
|
153203
|
+
body: JSON.stringify({
|
|
153204
|
+
instanceId: opts.instance,
|
|
153205
|
+
channelId: opts.channel,
|
|
153206
|
+
guildId: opts.guild
|
|
153207
|
+
})
|
|
152889
153208
|
});
|
|
153209
|
+
const body = await res.json();
|
|
153210
|
+
if (!res.ok) {
|
|
153211
|
+
error("Failed to join", body);
|
|
153212
|
+
process.exit(1);
|
|
153213
|
+
}
|
|
153214
|
+
success("Joined voice channel");
|
|
153215
|
+
data(body);
|
|
152890
153216
|
} catch (err2) {
|
|
152891
|
-
|
|
152892
|
-
|
|
153217
|
+
error("Failed to join voice channel", err2);
|
|
153218
|
+
process.exit(1);
|
|
152893
153219
|
}
|
|
152894
153220
|
});
|
|
152895
|
-
|
|
152896
|
-
const resolvedId = await resolveWebhookId(id);
|
|
152897
|
-
const client = getClient();
|
|
153221
|
+
voice.command("leave").description("Leave a voice session").requiredOption("--session <sessionId>", "Voice session ID").action(async (opts) => {
|
|
152898
153222
|
try {
|
|
152899
|
-
await
|
|
152900
|
-
|
|
153223
|
+
const res = await fetch(apiUrl("/voice/leave"), {
|
|
153224
|
+
method: "POST",
|
|
153225
|
+
headers: apiHeaders(),
|
|
153226
|
+
body: JSON.stringify({ sessionId: opts.session })
|
|
153227
|
+
});
|
|
153228
|
+
if (!res.ok) {
|
|
153229
|
+
error("Failed to leave", await res.json());
|
|
153230
|
+
process.exit(1);
|
|
153231
|
+
}
|
|
153232
|
+
success("Left voice session");
|
|
152901
153233
|
} catch (err2) {
|
|
152902
|
-
|
|
152903
|
-
|
|
153234
|
+
error("Failed to leave voice session", err2);
|
|
153235
|
+
process.exit(1);
|
|
152904
153236
|
}
|
|
152905
153237
|
});
|
|
152906
|
-
|
|
152907
|
-
const client = getClient();
|
|
153238
|
+
voice.command("sessions").description("List active voice sessions").action(async () => {
|
|
152908
153239
|
try {
|
|
152909
|
-
|
|
152910
|
-
|
|
152911
|
-
|
|
152912
|
-
|
|
152913
|
-
|
|
152914
|
-
|
|
153240
|
+
const res = await fetch(apiUrl("/voice/sessions"), { headers: apiHeaders() });
|
|
153241
|
+
const body = await res.json();
|
|
153242
|
+
if (body.items.length === 0) {
|
|
153243
|
+
info("No active voice sessions");
|
|
153244
|
+
} else {
|
|
153245
|
+
data(body.items);
|
|
152915
153246
|
}
|
|
152916
|
-
const result = await client.webhooks.trigger({
|
|
152917
|
-
eventType: options3.type,
|
|
152918
|
-
payload,
|
|
152919
|
-
instanceId: options3.instance,
|
|
152920
|
-
correlationId: options3.correlationId,
|
|
152921
|
-
causationId: options3.causationId
|
|
152922
|
-
});
|
|
152923
|
-
success(`Event triggered: ${result.eventId}`, {
|
|
152924
|
-
eventId: result.eventId,
|
|
152925
|
-
eventType: result.eventType
|
|
152926
|
-
});
|
|
152927
153247
|
} catch (err2) {
|
|
152928
|
-
|
|
152929
|
-
|
|
153248
|
+
error("Failed to list sessions", err2);
|
|
153249
|
+
process.exit(1);
|
|
152930
153250
|
}
|
|
152931
153251
|
});
|
|
152932
|
-
|
|
152933
|
-
const
|
|
152934
|
-
|
|
152935
|
-
|
|
152936
|
-
|
|
152937
|
-
|
|
152938
|
-
|
|
152939
|
-
|
|
152940
|
-
|
|
152941
|
-
|
|
152942
|
-
|
|
152943
|
-
|
|
153252
|
+
voice.command("stream <sessionId>").description("Stream voice audio via WebSocket").option("--format <format>", "Audio format: opus or pcm", "opus").option("--user <userId>", "Filter to specific user").option("--save <dir>", "Save audio frames to directory").option("--events-only", "Show only control events, no audio stats").action(async (sessionId, opts) => {
|
|
153253
|
+
const config2 = loadConfig();
|
|
153254
|
+
if (!config2.apiKey || !config2.apiUrl) {
|
|
153255
|
+
error("Not authenticated. Run: omni auth login");
|
|
153256
|
+
process.exit(1);
|
|
153257
|
+
}
|
|
153258
|
+
const wsUrl = config2.apiUrl.replace(/^http/, "ws");
|
|
153259
|
+
let url = `${wsUrl}/api/v2/voice/stream/${sessionId}?api_key=${config2.apiKey}&format=${opts.format}`;
|
|
153260
|
+
if (opts.user)
|
|
153261
|
+
url += `&user=${opts.user}`;
|
|
153262
|
+
info(`Connecting to voice stream: ${sessionId}`);
|
|
153263
|
+
info(`Format: ${opts.format} | User filter: ${opts.user || "all"}`);
|
|
153264
|
+
const ws = new WebSocket(url);
|
|
153265
|
+
ws.binaryType = "arraybuffer";
|
|
153266
|
+
const stats = new Map;
|
|
153267
|
+
let totalFrames = 0;
|
|
153268
|
+
const startTime = Date.now();
|
|
153269
|
+
let saveDir = "";
|
|
153270
|
+
if (opts.save) {
|
|
153271
|
+
const { mkdirSync: mkdirSync13 } = await import("fs");
|
|
153272
|
+
saveDir = opts.save;
|
|
153273
|
+
mkdirSync13(saveDir, { recursive: true });
|
|
153274
|
+
info(`Saving audio to: ${saveDir}`);
|
|
152944
153275
|
}
|
|
153276
|
+
ws.onopen = () => {
|
|
153277
|
+
success("Connected to voice stream");
|
|
153278
|
+
};
|
|
153279
|
+
const handleControl = (data2) => {
|
|
153280
|
+
const msg = JSON.parse(data2);
|
|
153281
|
+
const ts = new Date().toLocaleTimeString();
|
|
153282
|
+
const label = msg.userId || msg.sessionId || "";
|
|
153283
|
+
info(`[${ts}] ${String(msg.type)}: ${String(label)}`);
|
|
153284
|
+
};
|
|
153285
|
+
ws.onmessage = async (ev2) => {
|
|
153286
|
+
if (typeof ev2.data === "string") {
|
|
153287
|
+
handleControl(ev2.data);
|
|
153288
|
+
return;
|
|
153289
|
+
}
|
|
153290
|
+
const buf = Buffer.from(ev2.data);
|
|
153291
|
+
const userIdLen = buf[0] ?? 0;
|
|
153292
|
+
const userId = buf.subarray(1, 1 + userIdLen).toString("utf8");
|
|
153293
|
+
const audio = buf.subarray(1 + userIdLen);
|
|
153294
|
+
let s2 = stats.get(userId);
|
|
153295
|
+
if (!s2) {
|
|
153296
|
+
s2 = { frames: 0, bytes: 0 };
|
|
153297
|
+
stats.set(userId, s2);
|
|
153298
|
+
const ts = new Date().toLocaleTimeString();
|
|
153299
|
+
info(`[${ts}] New speaker: ${userId}`);
|
|
153300
|
+
}
|
|
153301
|
+
s2.frames++;
|
|
153302
|
+
s2.bytes += audio.length;
|
|
153303
|
+
totalFrames++;
|
|
153304
|
+
if (saveDir) {
|
|
153305
|
+
const { appendFileSync: appendFileSync2 } = await import("fs");
|
|
153306
|
+
appendFileSync2(`${saveDir}/${userId}.opus`, audio);
|
|
153307
|
+
}
|
|
153308
|
+
if (!opts.eventsOnly && totalFrames % 500 === 0) {
|
|
153309
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(0);
|
|
153310
|
+
const lines = [];
|
|
153311
|
+
for (const [uid2, st] of stats) {
|
|
153312
|
+
lines.push(` ${uid2}: ${st.frames} frames, ${(st.bytes / 1024).toFixed(1)}KB`);
|
|
153313
|
+
}
|
|
153314
|
+
info(`[${elapsed}s] ${totalFrames} frames total
|
|
153315
|
+
${lines.join(`
|
|
153316
|
+
`)}`);
|
|
153317
|
+
}
|
|
153318
|
+
};
|
|
153319
|
+
ws.onerror = () => {
|
|
153320
|
+
error("WebSocket error");
|
|
153321
|
+
};
|
|
153322
|
+
ws.onclose = (ev2) => {
|
|
153323
|
+
info(`Disconnected: code=${ev2.code} ${ev2.reason}`);
|
|
153324
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(0);
|
|
153325
|
+
info(`Session lasted ${elapsed}s, ${totalFrames} total frames`);
|
|
153326
|
+
for (const [uid2, st] of stats) {
|
|
153327
|
+
info(` ${uid2}: ${st.frames} frames, ${(st.bytes / 1024).toFixed(1)}KB`);
|
|
153328
|
+
}
|
|
153329
|
+
process.exit(0);
|
|
153330
|
+
};
|
|
153331
|
+
process.on("SIGINT", () => {
|
|
153332
|
+
info("Closing stream...");
|
|
153333
|
+
ws.close();
|
|
153334
|
+
});
|
|
153335
|
+
await new Promise(() => {});
|
|
152945
153336
|
});
|
|
152946
|
-
return
|
|
153337
|
+
return voice;
|
|
152947
153338
|
}
|
|
152948
153339
|
|
|
152949
153340
|
// src/commands/where.ts
|
|
@@ -153438,6 +153829,12 @@ var COMMANDS = [
|
|
|
153438
153829
|
helpGroup: "Management",
|
|
153439
153830
|
helpDescription: "Webhook management"
|
|
153440
153831
|
},
|
|
153832
|
+
{
|
|
153833
|
+
create: createSourcesCommand,
|
|
153834
|
+
category: "advanced",
|
|
153835
|
+
helpGroup: "Management",
|
|
153836
|
+
helpDescription: "One-command external source onboarding (github, ...)"
|
|
153837
|
+
},
|
|
153441
153838
|
{
|
|
153442
153839
|
create: createTurnsCommand,
|
|
153443
153840
|
category: "advanced",
|