@hasna/machines 0.0.45 → 0.0.46
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/README.md +27 -4
- package/dist/agent/index.d.ts +0 -1
- package/dist/agent/index.js +249 -14
- package/dist/agent/runtime.d.ts +0 -1
- package/dist/cli/index.d.ts +0 -1
- package/dist/cli/index.js +1301 -210
- package/dist/cli-utils.d.ts +0 -1
- package/dist/commands/apps.d.ts +7 -5
- package/dist/commands/backup.d.ts +0 -1
- package/dist/commands/cert.d.ts +0 -1
- package/dist/commands/clipboard-daemon.d.ts +0 -1
- package/dist/commands/clipboard-server.d.ts +0 -1
- package/dist/commands/clipboard.d.ts +0 -1
- package/dist/commands/daemon.d.ts +0 -1
- package/dist/commands/diff.d.ts +0 -1
- package/dist/commands/dns.d.ts +0 -1
- package/dist/commands/doctor.d.ts +0 -1
- package/dist/commands/heal-daemon.d.ts +0 -1
- package/dist/commands/heal.d.ts +0 -1
- package/dist/commands/install-claude.d.ts +5 -3
- package/dist/commands/install-tailscale.d.ts +5 -3
- package/dist/commands/manifest.d.ts +0 -1
- package/dist/commands/mutation-approval.d.ts +54 -0
- package/dist/commands/notifications.d.ts +14 -2
- package/dist/commands/ports.d.ts +0 -1
- package/dist/commands/runtime.d.ts +15 -1
- package/dist/commands/screen.d.ts +4 -1
- package/dist/commands/self-test.d.ts +0 -1
- package/dist/commands/serve.d.ts +0 -1
- package/dist/commands/setup.d.ts +5 -3
- package/dist/commands/ssh.d.ts +8 -1
- package/dist/commands/status.d.ts +0 -1
- package/dist/commands/sync.d.ts +5 -3
- package/dist/commands/workspace.d.ts +0 -1
- package/dist/compatibility.d.ts +0 -1
- package/dist/consumer-schema.d.ts +0 -1
- package/dist/consumer.d.ts +0 -1
- package/dist/consumer.js +253 -12
- package/dist/cross-project-types.d.ts +0 -1
- package/dist/db.d.ts +0 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1091 -184
- package/dist/manifests.d.ts +0 -1
- package/dist/mcp/http.d.ts +26 -2
- package/dist/mcp/index.d.ts +0 -1
- package/dist/mcp/index.js +1004 -162
- package/dist/mcp/server.d.ts +5 -3
- package/dist/paths.d.ts +0 -1
- package/dist/pg-migrations.d.ts +0 -1
- package/dist/redaction.d.ts +0 -1
- package/dist/remote-storage.d.ts +0 -1
- package/dist/remote.d.ts +14 -5
- package/dist/storage-sync.d.ts +0 -1
- package/dist/storage.d.ts +0 -1
- package/dist/storage.js +18 -0
- package/dist/topology.d.ts +0 -1
- package/dist/types.d.ts +3 -1
- package/dist/version.d.ts +0 -1
- package/package.json +5 -3
- package/dist/agent/index.d.ts.map +0 -1
- package/dist/agent/runtime.d.ts.map +0 -1
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli-utils.d.ts.map +0 -1
- package/dist/commands/apps.d.ts.map +0 -1
- package/dist/commands/backup.d.ts.map +0 -1
- package/dist/commands/cert.d.ts.map +0 -1
- package/dist/commands/clipboard-daemon.d.ts.map +0 -1
- package/dist/commands/clipboard-server.d.ts.map +0 -1
- package/dist/commands/clipboard.d.ts.map +0 -1
- package/dist/commands/daemon.d.ts.map +0 -1
- package/dist/commands/diff.d.ts.map +0 -1
- package/dist/commands/dns.d.ts.map +0 -1
- package/dist/commands/doctor.d.ts.map +0 -1
- package/dist/commands/heal-daemon.d.ts.map +0 -1
- package/dist/commands/heal.d.ts.map +0 -1
- package/dist/commands/install-claude.d.ts.map +0 -1
- package/dist/commands/install-tailscale.d.ts.map +0 -1
- package/dist/commands/manifest.d.ts.map +0 -1
- package/dist/commands/notifications.d.ts.map +0 -1
- package/dist/commands/ports.d.ts.map +0 -1
- package/dist/commands/runtime.d.ts.map +0 -1
- package/dist/commands/screen.d.ts.map +0 -1
- package/dist/commands/self-test.d.ts.map +0 -1
- package/dist/commands/serve.d.ts.map +0 -1
- package/dist/commands/setup.d.ts.map +0 -1
- package/dist/commands/ssh.d.ts.map +0 -1
- package/dist/commands/status.d.ts.map +0 -1
- package/dist/commands/sync.d.ts.map +0 -1
- package/dist/commands/workspace.d.ts.map +0 -1
- package/dist/compatibility.d.ts.map +0 -1
- package/dist/consumer-schema.d.ts.map +0 -1
- package/dist/consumer.d.ts.map +0 -1
- package/dist/cross-project-types.d.ts.map +0 -1
- package/dist/db.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/manifests.d.ts.map +0 -1
- package/dist/mcp/http.d.ts.map +0 -1
- package/dist/mcp/index.d.ts.map +0 -1
- package/dist/mcp/server.d.ts.map +0 -1
- package/dist/paths.d.ts.map +0 -1
- package/dist/pg-migrations.d.ts.map +0 -1
- package/dist/redaction.d.ts.map +0 -1
- package/dist/remote-storage.d.ts.map +0 -1
- package/dist/remote.d.ts.map +0 -1
- package/dist/storage-sync.d.ts.map +0 -1
- package/dist/storage.d.ts.map +0 -1
- package/dist/topology.d.ts.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/version.d.ts.map +0 -1
package/dist/cli/index.js
CHANGED
|
@@ -2123,6 +2123,7 @@ class SqliteAdapter {
|
|
|
2123
2123
|
raw;
|
|
2124
2124
|
constructor(path) {
|
|
2125
2125
|
this.raw = new Database(path);
|
|
2126
|
+
this.raw.exec("PRAGMA busy_timeout = 5000");
|
|
2126
2127
|
}
|
|
2127
2128
|
close() {
|
|
2128
2129
|
this.raw.close();
|
|
@@ -2172,6 +2173,23 @@ function createTables(db) {
|
|
|
2172
2173
|
updated_at TEXT NOT NULL
|
|
2173
2174
|
)
|
|
2174
2175
|
`);
|
|
2176
|
+
db.exec(`
|
|
2177
|
+
CREATE TABLE IF NOT EXISTS mutation_approval_nonces (
|
|
2178
|
+
nonce_sha256 TEXT PRIMARY KEY,
|
|
2179
|
+
token_sha256 TEXT NOT NULL,
|
|
2180
|
+
surface TEXT NOT NULL,
|
|
2181
|
+
operation TEXT NOT NULL,
|
|
2182
|
+
caller_id TEXT NOT NULL,
|
|
2183
|
+
run_id TEXT NOT NULL,
|
|
2184
|
+
transport TEXT NOT NULL,
|
|
2185
|
+
expires_at INTEGER NOT NULL,
|
|
2186
|
+
used_at INTEGER NOT NULL
|
|
2187
|
+
)
|
|
2188
|
+
`);
|
|
2189
|
+
db.exec(`
|
|
2190
|
+
CREATE INDEX IF NOT EXISTS mutation_approval_nonces_expires_at_idx
|
|
2191
|
+
ON mutation_approval_nonces (expires_at)
|
|
2192
|
+
`);
|
|
2175
2193
|
}
|
|
2176
2194
|
function migrateAgentHeartbeats(db) {
|
|
2177
2195
|
const columns = db.query("PRAGMA table_info(agent_heartbeats)").all();
|
|
@@ -2689,8 +2707,15 @@ var {
|
|
|
2689
2707
|
} = import__.default;
|
|
2690
2708
|
|
|
2691
2709
|
// src/cli/index.ts
|
|
2692
|
-
import {
|
|
2710
|
+
import {
|
|
2711
|
+
EventsClient as EventsClient3,
|
|
2712
|
+
getEventsDataDir as getEventsDataDir2,
|
|
2713
|
+
sanitizeChannelForOutput,
|
|
2714
|
+
sanitizeChannelsForOutput as sanitizeChannelsForOutput2
|
|
2715
|
+
} from "@hasna/events";
|
|
2693
2716
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
2717
|
+
import { existsSync as existsSync13, readFileSync as readFileSync13, rmSync as rmSync3 } from "fs";
|
|
2718
|
+
import { join as join12, resolve as resolve4 } from "path";
|
|
2694
2719
|
|
|
2695
2720
|
// node_modules/chalk/source/vendor/ansi-styles/index.js
|
|
2696
2721
|
var ANSI_BACKGROUND_OFFSET = 10;
|
|
@@ -7521,10 +7546,271 @@ function manifestValidate() {
|
|
|
7521
7546
|
import { homedir as homedir2 } from "os";
|
|
7522
7547
|
init_db();
|
|
7523
7548
|
|
|
7549
|
+
// src/commands/mutation-approval.ts
|
|
7550
|
+
init_db();
|
|
7551
|
+
import { createHash, createHmac, randomUUID, timingSafeEqual } from "crypto";
|
|
7552
|
+
import { resolve as resolve2 } from "path";
|
|
7553
|
+
var MUTATION_APPROVAL_FLAG_ENV = "HASNA_MACHINES_ALLOW_MUTATIONS";
|
|
7554
|
+
var LEGACY_MUTATION_APPROVAL_FLAG_ENV = "HASNA_MACHINES_MUTATION_APPROVAL";
|
|
7555
|
+
var MUTATION_APPROVAL_TOKEN_ENV = "HASNA_MACHINES_MUTATION_TOKEN";
|
|
7556
|
+
var MUTATION_APPROVAL_CALLER_ENV = "HASNA_MACHINES_MUTATION_CALLER_ID";
|
|
7557
|
+
var MUTATION_APPROVAL_RUN_ENV = "HASNA_MACHINES_MUTATION_RUN_ID";
|
|
7558
|
+
var MUTATION_APPROVAL_REPLAY_PATH_ENV = "HASNA_MACHINES_MUTATION_REPLAY_PATH";
|
|
7559
|
+
var TOKEN_PREFIX = "machines-mut-v1";
|
|
7560
|
+
var DEFAULT_TOKEN_TTL_MS = 5 * 60 * 1000;
|
|
7561
|
+
var MAX_TOKEN_TTL_MS = 5 * 60 * 1000;
|
|
7562
|
+
var MAX_CLOCK_SKEW_MS = 30000;
|
|
7563
|
+
function isTruthy(value) {
|
|
7564
|
+
return value === "1" || value?.toLowerCase() === "true" || value?.toLowerCase() === "yes";
|
|
7565
|
+
}
|
|
7566
|
+
function nowMs(now) {
|
|
7567
|
+
if (typeof now === "number")
|
|
7568
|
+
return now;
|
|
7569
|
+
if (now instanceof Date)
|
|
7570
|
+
return now.getTime();
|
|
7571
|
+
return Date.now();
|
|
7572
|
+
}
|
|
7573
|
+
function signingSecret(env2, explicitSecret) {
|
|
7574
|
+
return explicitSecret?.trim() || env2[MUTATION_APPROVAL_TOKEN_ENV]?.trim();
|
|
7575
|
+
}
|
|
7576
|
+
function hmac(payload, secret) {
|
|
7577
|
+
return createHmac("sha256", secret).update(payload).digest("base64url");
|
|
7578
|
+
}
|
|
7579
|
+
function sha256Hex(payload) {
|
|
7580
|
+
return createHash("sha256").update(payload).digest("hex");
|
|
7581
|
+
}
|
|
7582
|
+
function replayDbPath(env2) {
|
|
7583
|
+
const configured = env2[MUTATION_APPROVAL_REPLAY_PATH_ENV]?.trim();
|
|
7584
|
+
return configured ? resolve2(configured) : undefined;
|
|
7585
|
+
}
|
|
7586
|
+
function replayNonceKey(claims) {
|
|
7587
|
+
return sha256Hex(JSON.stringify({ nonce: claims.nonce }));
|
|
7588
|
+
}
|
|
7589
|
+
function recordReplayNonce(env2, claims, tokenPayload, now) {
|
|
7590
|
+
const dbPath = replayDbPath(env2);
|
|
7591
|
+
if (!dbPath)
|
|
7592
|
+
return;
|
|
7593
|
+
if (!claims.nonce) {
|
|
7594
|
+
return { approved: false, reason: "approval_token nonce claim is required for replay protection." };
|
|
7595
|
+
}
|
|
7596
|
+
try {
|
|
7597
|
+
const db = getDb(dbPath);
|
|
7598
|
+
db.query("DELETE FROM mutation_approval_nonces WHERE expires_at <= ?").run(now);
|
|
7599
|
+
const result = db.query(`
|
|
7600
|
+
INSERT OR IGNORE INTO mutation_approval_nonces (
|
|
7601
|
+
nonce_sha256,
|
|
7602
|
+
token_sha256,
|
|
7603
|
+
surface,
|
|
7604
|
+
operation,
|
|
7605
|
+
caller_id,
|
|
7606
|
+
run_id,
|
|
7607
|
+
transport,
|
|
7608
|
+
expires_at,
|
|
7609
|
+
used_at
|
|
7610
|
+
)
|
|
7611
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
7612
|
+
`).run(replayNonceKey(claims), sha256Hex(tokenPayload), claims.surface, claims.operation, claims.callerId ?? "", claims.runId ?? "", claims.transport ?? "", claims.expiresAt, now);
|
|
7613
|
+
if (result.changes !== 1) {
|
|
7614
|
+
return { approved: false, reason: "approval_token nonce has already been used." };
|
|
7615
|
+
}
|
|
7616
|
+
return;
|
|
7617
|
+
} catch (error) {
|
|
7618
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7619
|
+
return { approved: false, reason: `approval_token replay store is unavailable: ${message}` };
|
|
7620
|
+
}
|
|
7621
|
+
}
|
|
7622
|
+
function safeEqual(left, right) {
|
|
7623
|
+
const leftBuffer = Buffer.from(left);
|
|
7624
|
+
const rightBuffer = Buffer.from(right);
|
|
7625
|
+
return leftBuffer.length === rightBuffer.length && timingSafeEqual(leftBuffer, rightBuffer);
|
|
7626
|
+
}
|
|
7627
|
+
function canonicalizeMutationArg(value, inArray = false) {
|
|
7628
|
+
if (value === undefined)
|
|
7629
|
+
return inArray ? null : undefined;
|
|
7630
|
+
if (value === null || typeof value === "boolean" || typeof value === "string")
|
|
7631
|
+
return value;
|
|
7632
|
+
if (typeof value === "number")
|
|
7633
|
+
return Number.isFinite(value) ? value : null;
|
|
7634
|
+
if (Array.isArray(value)) {
|
|
7635
|
+
return value.map((entry) => canonicalizeMutationArg(entry, true) ?? null);
|
|
7636
|
+
}
|
|
7637
|
+
if (value instanceof Date)
|
|
7638
|
+
return value.toISOString();
|
|
7639
|
+
if (typeof value === "object") {
|
|
7640
|
+
const result = {};
|
|
7641
|
+
for (const key of Object.keys(value).sort()) {
|
|
7642
|
+
if (key === "approval_token" || key === "approvalToken")
|
|
7643
|
+
continue;
|
|
7644
|
+
const canonicalValue = canonicalizeMutationArg(value[key]);
|
|
7645
|
+
if (canonicalValue !== undefined)
|
|
7646
|
+
result[key] = canonicalValue;
|
|
7647
|
+
}
|
|
7648
|
+
return result;
|
|
7649
|
+
}
|
|
7650
|
+
return inArray ? null : undefined;
|
|
7651
|
+
}
|
|
7652
|
+
function canonicalMutationArgs(value) {
|
|
7653
|
+
return JSON.stringify(canonicalizeMutationArg(value) ?? {});
|
|
7654
|
+
}
|
|
7655
|
+
function mutationArgsSha256(value) {
|
|
7656
|
+
return sha256Hex(canonicalMutationArgs(value));
|
|
7657
|
+
}
|
|
7658
|
+
function stripPlanRuntimeFields(value) {
|
|
7659
|
+
if (Array.isArray(value))
|
|
7660
|
+
return value.map(stripPlanRuntimeFields);
|
|
7661
|
+
if (value instanceof Date)
|
|
7662
|
+
return value;
|
|
7663
|
+
if (value && typeof value === "object") {
|
|
7664
|
+
const result = {};
|
|
7665
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
7666
|
+
if (key === "planDigest" || key === "plan_digest" || key === "mode" || key === "executed")
|
|
7667
|
+
continue;
|
|
7668
|
+
result[key] = stripPlanRuntimeFields(entry);
|
|
7669
|
+
}
|
|
7670
|
+
return result;
|
|
7671
|
+
}
|
|
7672
|
+
return value;
|
|
7673
|
+
}
|
|
7674
|
+
function mutationPlanDigest(plan) {
|
|
7675
|
+
return mutationArgsSha256(stripPlanRuntimeFields(plan));
|
|
7676
|
+
}
|
|
7677
|
+
function attachMutationPlanDigest(plan) {
|
|
7678
|
+
return {
|
|
7679
|
+
...plan,
|
|
7680
|
+
planDigest: mutationPlanDigest(plan)
|
|
7681
|
+
};
|
|
7682
|
+
}
|
|
7683
|
+
function assertMutationPlanDigest(plan, expectedPlanDigest) {
|
|
7684
|
+
if (expectedPlanDigest && mutationPlanDigest(plan) !== expectedPlanDigest) {
|
|
7685
|
+
throw new Error("Approved plan digest does not match the current execution plan.");
|
|
7686
|
+
}
|
|
7687
|
+
}
|
|
7688
|
+
function parseToken(token) {
|
|
7689
|
+
if (!token)
|
|
7690
|
+
return null;
|
|
7691
|
+
const parts = token.split(".");
|
|
7692
|
+
if (parts.length !== 3 || parts[0] !== TOKEN_PREFIX)
|
|
7693
|
+
return null;
|
|
7694
|
+
try {
|
|
7695
|
+
const claims = JSON.parse(Buffer.from(parts[1] ?? "", "base64url").toString("utf8"));
|
|
7696
|
+
return { payload: parts[1] ?? "", signature: parts[2] ?? "", claims };
|
|
7697
|
+
} catch {
|
|
7698
|
+
return null;
|
|
7699
|
+
}
|
|
7700
|
+
}
|
|
7701
|
+
function claimMatches(expected, actual) {
|
|
7702
|
+
if (expected === undefined)
|
|
7703
|
+
return actual === undefined;
|
|
7704
|
+
return actual === expected;
|
|
7705
|
+
}
|
|
7706
|
+
function verifyMutationApprovalToken(options) {
|
|
7707
|
+
const env2 = options.env ?? process.env;
|
|
7708
|
+
const secret = signingSecret(env2);
|
|
7709
|
+
if (!secret)
|
|
7710
|
+
return { approved: false, reason: `${MUTATION_APPROVAL_TOKEN_ENV} is not configured.` };
|
|
7711
|
+
const parsed = parseToken(options.approvalToken);
|
|
7712
|
+
if (!parsed)
|
|
7713
|
+
return { approved: false, reason: "approval_token is not a scoped mutation token." };
|
|
7714
|
+
if (!safeEqual(hmac(parsed.payload, secret), parsed.signature)) {
|
|
7715
|
+
return { approved: false, reason: "approval_token signature is invalid." };
|
|
7716
|
+
}
|
|
7717
|
+
const claims = parsed.claims;
|
|
7718
|
+
if (claims.version !== 1)
|
|
7719
|
+
return { approved: false, reason: "approval_token version is unsupported." };
|
|
7720
|
+
if (!claims.callerId || !claims.runId) {
|
|
7721
|
+
return { approved: false, reason: "approval_token must include caller and run claims." };
|
|
7722
|
+
}
|
|
7723
|
+
if (!claims.transport) {
|
|
7724
|
+
return { approved: false, reason: "approval_token must include a transport claim." };
|
|
7725
|
+
}
|
|
7726
|
+
if (!Number.isFinite(claims.expiresAt) || claims.expiresAt <= nowMs(options.now)) {
|
|
7727
|
+
return { approved: false, reason: "approval_token is expired." };
|
|
7728
|
+
}
|
|
7729
|
+
const now = nowMs(options.now);
|
|
7730
|
+
if (!Number.isFinite(claims.issuedAt) || claims.issuedAt > now + MAX_CLOCK_SKEW_MS) {
|
|
7731
|
+
return { approved: false, reason: "approval_token issue time is invalid." };
|
|
7732
|
+
}
|
|
7733
|
+
if (claims.expiresAt - claims.issuedAt > MAX_TOKEN_TTL_MS) {
|
|
7734
|
+
return { approved: false, reason: "approval_token TTL is too long." };
|
|
7735
|
+
}
|
|
7736
|
+
for (const key of ["surface", "operation", "machineId", "resourceId", "transport"]) {
|
|
7737
|
+
if (!claimMatches(options[key], claims[key])) {
|
|
7738
|
+
return { approved: false, reason: `approval_token ${key} claim does not match this mutation.` };
|
|
7739
|
+
}
|
|
7740
|
+
}
|
|
7741
|
+
for (const key of ["callerId", "runId"]) {
|
|
7742
|
+
if (options[key] !== undefined && options[key] !== claims[key]) {
|
|
7743
|
+
return { approved: false, reason: `approval_token ${key} claim does not match this mutation.` };
|
|
7744
|
+
}
|
|
7745
|
+
}
|
|
7746
|
+
const expectedArgsSha256 = options.argsSha256 || (options.args === undefined ? undefined : mutationArgsSha256(options.args));
|
|
7747
|
+
if (expectedArgsSha256 !== undefined && claims.args_sha256 !== expectedArgsSha256) {
|
|
7748
|
+
return { approved: false, reason: "approval_token args_sha256 claim does not match this mutation." };
|
|
7749
|
+
}
|
|
7750
|
+
const replayDecision = recordReplayNonce(env2, claims, parsed.payload, now);
|
|
7751
|
+
if (replayDecision)
|
|
7752
|
+
return replayDecision;
|
|
7753
|
+
return { approved: true, claims };
|
|
7754
|
+
}
|
|
7755
|
+
function isMutationApproved(options = {}) {
|
|
7756
|
+
const env2 = options.env ?? process.env;
|
|
7757
|
+
const surface = options.surface ?? "cli";
|
|
7758
|
+
if (surface === "mcp") {
|
|
7759
|
+
if (!options.operation)
|
|
7760
|
+
return false;
|
|
7761
|
+
return verifyMutationApprovalToken({
|
|
7762
|
+
surface,
|
|
7763
|
+
operation: options.operation,
|
|
7764
|
+
machineId: options.machineId,
|
|
7765
|
+
resourceId: options.resourceId,
|
|
7766
|
+
callerId: options.callerId,
|
|
7767
|
+
runId: options.runId,
|
|
7768
|
+
transport: options.transport ?? "mcp",
|
|
7769
|
+
args: options.args,
|
|
7770
|
+
argsSha256: options.argsSha256,
|
|
7771
|
+
approvalToken: options.approvalToken,
|
|
7772
|
+
env: env2,
|
|
7773
|
+
now: options.now
|
|
7774
|
+
}).approved;
|
|
7775
|
+
}
|
|
7776
|
+
if (options.approvalToken) {
|
|
7777
|
+
const decision = options.operation ? verifyMutationApprovalToken({
|
|
7778
|
+
surface,
|
|
7779
|
+
operation: options.operation,
|
|
7780
|
+
machineId: options.machineId,
|
|
7781
|
+
resourceId: options.resourceId,
|
|
7782
|
+
callerId: options.callerId,
|
|
7783
|
+
runId: options.runId,
|
|
7784
|
+
transport: options.transport ?? surface,
|
|
7785
|
+
args: options.args,
|
|
7786
|
+
argsSha256: options.argsSha256,
|
|
7787
|
+
approvalToken: options.approvalToken,
|
|
7788
|
+
env: env2,
|
|
7789
|
+
now: options.now
|
|
7790
|
+
}) : { approved: false };
|
|
7791
|
+
if (decision.approved)
|
|
7792
|
+
return true;
|
|
7793
|
+
if (env2[MUTATION_APPROVAL_TOKEN_ENV]?.trim())
|
|
7794
|
+
return false;
|
|
7795
|
+
}
|
|
7796
|
+
return isTruthy(env2[MUTATION_APPROVAL_FLAG_ENV]) || isTruthy(env2[LEGACY_MUTATION_APPROVAL_FLAG_ENV]);
|
|
7797
|
+
}
|
|
7798
|
+
function assertMutationApproved(options) {
|
|
7799
|
+
if (isMutationApproved(options)) {
|
|
7800
|
+
return;
|
|
7801
|
+
}
|
|
7802
|
+
const env2 = options.env ?? process.env;
|
|
7803
|
+
const tokenConfigured = Boolean(env2[MUTATION_APPROVAL_TOKEN_ENV]?.trim());
|
|
7804
|
+
const approvalHint = options.surface === "mcp" ? `pass a scoped approval_token signed with ${MUTATION_APPROVAL_TOKEN_ENV}` : tokenConfigured ? `pass a scoped approval_token signed with ${MUTATION_APPROVAL_TOKEN_ENV} or set ${MUTATION_APPROVAL_FLAG_ENV}=1 for a trusted local session` : `set ${MUTATION_APPROVAL_FLAG_ENV}=1 for a trusted local session or configure ${MUTATION_APPROVAL_TOKEN_ENV}`;
|
|
7805
|
+
throw new Error(`Fleet mutation blocked: ${options.surface}.${options.operation} requires operator approval; ${approvalHint}.`);
|
|
7806
|
+
}
|
|
7807
|
+
|
|
7524
7808
|
// src/remote.ts
|
|
7525
7809
|
init_db();
|
|
7526
7810
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
7527
|
-
import {
|
|
7811
|
+
import { existsSync as existsSync5, mkdtempSync, readFileSync as readFileSync3, rmSync } from "fs";
|
|
7812
|
+
import { hostname as hostname4, tmpdir } from "os";
|
|
7813
|
+
import { join as join3 } from "path";
|
|
7528
7814
|
|
|
7529
7815
|
// src/topology.ts
|
|
7530
7816
|
init_db();
|
|
@@ -8498,6 +8784,16 @@ function resolveMachineWorkspace(options) {
|
|
|
8498
8784
|
function shellQuote2(value) {
|
|
8499
8785
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
8500
8786
|
}
|
|
8787
|
+
function validateSshTarget(target) {
|
|
8788
|
+
const trimmed = target.trim();
|
|
8789
|
+
if (!trimmed || trimmed.startsWith("-") || /[\s"'`$\\;&|<>()[\]{}]/.test(trimmed)) {
|
|
8790
|
+
throw new Error(`Unsafe SSH target: ${target}`);
|
|
8791
|
+
}
|
|
8792
|
+
if (!/^(?:[A-Za-z0-9._%+-]+@)?[A-Za-z0-9._:-]+$/.test(trimmed)) {
|
|
8793
|
+
throw new Error(`Unsafe SSH target: ${target}`);
|
|
8794
|
+
}
|
|
8795
|
+
return trimmed;
|
|
8796
|
+
}
|
|
8501
8797
|
function resolveSshTarget(machineId, options = {}) {
|
|
8502
8798
|
const resolved = resolveMachineRoute(machineId, options);
|
|
8503
8799
|
if (!resolved.ok || !resolved.target) {
|
|
@@ -8508,15 +8804,25 @@ function resolveSshTarget(machineId, options = {}) {
|
|
|
8508
8804
|
}
|
|
8509
8805
|
return {
|
|
8510
8806
|
machineId: resolved.machine_id ?? machineId,
|
|
8511
|
-
target: resolved.command_target ?? resolved.target,
|
|
8807
|
+
target: validateSshTarget(resolved.command_target ?? resolved.target),
|
|
8512
8808
|
route: resolved.route,
|
|
8513
8809
|
confidence: resolved.confidence,
|
|
8514
8810
|
warnings: resolved.warnings
|
|
8515
8811
|
};
|
|
8516
8812
|
}
|
|
8517
8813
|
function buildSshCommand(machineId, remoteCommand, options = {}) {
|
|
8814
|
+
return buildSshCommandPlan(machineId, remoteCommand, options).shellCommand;
|
|
8815
|
+
}
|
|
8816
|
+
function buildSshCommandPlan(machineId, remoteCommand, options = {}) {
|
|
8518
8817
|
const resolved = resolveSshTarget(machineId, options);
|
|
8519
|
-
|
|
8818
|
+
const args = remoteCommand ? [resolved.target, remoteCommand] : [resolved.target];
|
|
8819
|
+
const shellCommand2 = `ssh ${args.map(shellQuote2).join(" ")}`;
|
|
8820
|
+
return {
|
|
8821
|
+
...resolved,
|
|
8822
|
+
command: "ssh",
|
|
8823
|
+
args,
|
|
8824
|
+
shellCommand: shellCommand2
|
|
8825
|
+
};
|
|
8520
8826
|
}
|
|
8521
8827
|
|
|
8522
8828
|
// src/remote.ts
|
|
@@ -8528,35 +8834,233 @@ function machineIsLocal(machineId, localMachineId) {
|
|
|
8528
8834
|
}
|
|
8529
8835
|
function resolveMachineCommand(machineId, command, localMachineId = getLocalMachineId()) {
|
|
8530
8836
|
if (machineIsLocal(machineId, localMachineId)) {
|
|
8531
|
-
return { source: "local", shellCommand: command };
|
|
8837
|
+
return { source: "local", command: "bash", args: ["-c", command], shellCommand: command, usesShell: true };
|
|
8532
8838
|
}
|
|
8533
8839
|
try {
|
|
8840
|
+
const plan = buildSshCommandPlan(machineId, command);
|
|
8534
8841
|
return {
|
|
8535
|
-
source:
|
|
8536
|
-
|
|
8842
|
+
source: plan.route,
|
|
8843
|
+
command: plan.command,
|
|
8844
|
+
args: plan.args,
|
|
8845
|
+
shellCommand: plan.shellCommand,
|
|
8846
|
+
usesShell: false
|
|
8537
8847
|
};
|
|
8538
8848
|
} catch (error) {
|
|
8539
8849
|
const message = String(error.message ?? error);
|
|
8540
8850
|
if (message.includes("Machine route not found") || message.includes("Machine not found in manifest")) {
|
|
8541
|
-
|
|
8851
|
+
const target = validateSshTarget(machineId);
|
|
8852
|
+
return {
|
|
8853
|
+
source: "ssh",
|
|
8854
|
+
command: "ssh",
|
|
8855
|
+
args: [target, command],
|
|
8856
|
+
shellCommand: `ssh ${shellQuote3(target)} ${shellQuote3(command)}`,
|
|
8857
|
+
usesShell: false
|
|
8858
|
+
};
|
|
8542
8859
|
}
|
|
8543
8860
|
throw error;
|
|
8544
8861
|
}
|
|
8545
8862
|
}
|
|
8546
|
-
function runMachineCommand(machineId, command) {
|
|
8863
|
+
function runMachineCommand(machineId, command, options = {}) {
|
|
8547
8864
|
const resolved = resolveMachineCommand(machineId, command);
|
|
8548
|
-
|
|
8865
|
+
if (options.timeoutMs && options.timeoutMs > 0 && process.platform !== "win32") {
|
|
8866
|
+
return runMachineCommandWithProcessGroupTimeout(machineId, resolved, options);
|
|
8867
|
+
}
|
|
8868
|
+
const result = spawnSync2(resolved.command, resolved.args, {
|
|
8549
8869
|
encoding: "utf8",
|
|
8550
|
-
env: process.env
|
|
8870
|
+
env: process.env,
|
|
8871
|
+
timeout: options.timeoutMs,
|
|
8872
|
+
killSignal: "SIGTERM"
|
|
8551
8873
|
});
|
|
8874
|
+
const timedOut = Boolean(result.error && "code" in result.error && result.error.code === "ETIMEDOUT");
|
|
8875
|
+
const timeoutMessage = timedOut ? `Command timed out after ${options.timeoutMs}ms.` : "";
|
|
8876
|
+
const stderr = [result.stderr || "", timeoutMessage].filter(Boolean).join(result.stderr ? `
|
|
8877
|
+
` : "");
|
|
8552
8878
|
return {
|
|
8553
8879
|
machineId,
|
|
8554
8880
|
source: resolved.source,
|
|
8555
8881
|
stdout: result.stdout || "",
|
|
8556
|
-
stderr
|
|
8557
|
-
exitCode: result.status ?? 1
|
|
8882
|
+
stderr,
|
|
8883
|
+
exitCode: timedOut ? 124 : result.status ?? 1,
|
|
8884
|
+
timedOut,
|
|
8885
|
+
signal: result.signal
|
|
8558
8886
|
};
|
|
8559
8887
|
}
|
|
8888
|
+
function runMachineCommandWithProcessGroupTimeout(machineId, resolved, options) {
|
|
8889
|
+
const timeoutMs = Math.max(1, options.timeoutMs ?? 1);
|
|
8890
|
+
const killGraceMs = Math.max(1, options.killGraceMs ?? 1000);
|
|
8891
|
+
const helperDir = mkdtempSync(join3(tmpdir(), "machines-timeout-helper-"));
|
|
8892
|
+
const pgidFile = join3(helperDir, "pgid");
|
|
8893
|
+
const helper = spawnSync2(process.execPath, ["--eval", PROCESS_GROUP_TIMEOUT_HELPER], {
|
|
8894
|
+
input: JSON.stringify({ command: resolved.command, args: resolved.args }),
|
|
8895
|
+
encoding: "utf8",
|
|
8896
|
+
env: {
|
|
8897
|
+
...process.env,
|
|
8898
|
+
HASNA_MACHINES_COMMAND_TIMEOUT_MS: String(timeoutMs),
|
|
8899
|
+
HASNA_MACHINES_COMMAND_KILL_GRACE_MS: String(killGraceMs),
|
|
8900
|
+
HASNA_MACHINES_COMMAND_PGID_FILE: pgidFile
|
|
8901
|
+
},
|
|
8902
|
+
timeout: timeoutMs + killGraceMs + 2000,
|
|
8903
|
+
killSignal: "SIGKILL",
|
|
8904
|
+
maxBuffer: 64 * 1024 * 1024
|
|
8905
|
+
});
|
|
8906
|
+
try {
|
|
8907
|
+
const parsed = parseHelperResult(helper.stdout);
|
|
8908
|
+
if (parsed) {
|
|
8909
|
+
return {
|
|
8910
|
+
machineId,
|
|
8911
|
+
source: resolved.source,
|
|
8912
|
+
stdout: parsed.stdout,
|
|
8913
|
+
stderr: parsed.stderr,
|
|
8914
|
+
exitCode: parsed.exitCode,
|
|
8915
|
+
timedOut: parsed.timedOut,
|
|
8916
|
+
signal: parsed.signal
|
|
8917
|
+
};
|
|
8918
|
+
}
|
|
8919
|
+
const helperTimedOut = Boolean(helper.error && "code" in helper.error && helper.error.code === "ETIMEDOUT");
|
|
8920
|
+
if (helperTimedOut)
|
|
8921
|
+
killPublishedProcessGroup(pgidFile);
|
|
8922
|
+
const timeoutMessage = helperTimedOut ? `Command timed out after ${timeoutMs}ms; timeout helper exceeded cleanup grace ${killGraceMs}ms.` : "";
|
|
8923
|
+
const stderr = [helper.stderr || "", timeoutMessage].filter(Boolean).join(helper.stderr ? `
|
|
8924
|
+
` : "");
|
|
8925
|
+
return {
|
|
8926
|
+
machineId,
|
|
8927
|
+
source: resolved.source,
|
|
8928
|
+
stdout: "",
|
|
8929
|
+
stderr,
|
|
8930
|
+
exitCode: helperTimedOut ? 124 : helper.status ?? 1,
|
|
8931
|
+
timedOut: helperTimedOut,
|
|
8932
|
+
signal: helper.signal
|
|
8933
|
+
};
|
|
8934
|
+
} finally {
|
|
8935
|
+
rmSync(helperDir, { recursive: true, force: true });
|
|
8936
|
+
}
|
|
8937
|
+
}
|
|
8938
|
+
function killPublishedProcessGroup(pgidFile) {
|
|
8939
|
+
if (!existsSync5(pgidFile))
|
|
8940
|
+
return;
|
|
8941
|
+
try {
|
|
8942
|
+
const pid = Number.parseInt(readFileSync3(pgidFile, "utf8").trim(), 10);
|
|
8943
|
+
if (!Number.isInteger(pid) || pid <= 1)
|
|
8944
|
+
return;
|
|
8945
|
+
process.kill(-pid, "SIGKILL");
|
|
8946
|
+
} catch {}
|
|
8947
|
+
}
|
|
8948
|
+
function parseHelperResult(stdout) {
|
|
8949
|
+
if (!stdout)
|
|
8950
|
+
return null;
|
|
8951
|
+
try {
|
|
8952
|
+
const parsed = JSON.parse(stdout);
|
|
8953
|
+
if (typeof parsed.stdout !== "string" || typeof parsed.stderr !== "string" || typeof parsed.exitCode !== "number")
|
|
8954
|
+
return null;
|
|
8955
|
+
return {
|
|
8956
|
+
machineId: "",
|
|
8957
|
+
source: "local",
|
|
8958
|
+
stdout: parsed.stdout,
|
|
8959
|
+
stderr: parsed.stderr,
|
|
8960
|
+
exitCode: parsed.exitCode,
|
|
8961
|
+
timedOut: parsed.timedOut === true,
|
|
8962
|
+
signal: typeof parsed.signal === "string" ? parsed.signal : null
|
|
8963
|
+
};
|
|
8964
|
+
} catch {
|
|
8965
|
+
return null;
|
|
8966
|
+
}
|
|
8967
|
+
}
|
|
8968
|
+
var PROCESS_GROUP_TIMEOUT_HELPER = `
|
|
8969
|
+
const { spawn } = require("node:child_process");
|
|
8970
|
+
const { readFileSync, writeFileSync } = require("node:fs");
|
|
8971
|
+
|
|
8972
|
+
const plan = JSON.parse(readFileSync(0, "utf8"));
|
|
8973
|
+
const command = String(plan.command || "");
|
|
8974
|
+
const args = Array.isArray(plan.args) ? plan.args.map(String) : [];
|
|
8975
|
+
const timeoutMs = Math.max(1, Number.parseInt(process.env.HASNA_MACHINES_COMMAND_TIMEOUT_MS || "1", 10));
|
|
8976
|
+
const killGraceMs = Math.max(1, Number.parseInt(process.env.HASNA_MACHINES_COMMAND_KILL_GRACE_MS || "1000", 10));
|
|
8977
|
+
const pgidFile = process.env.HASNA_MACHINES_COMMAND_PGID_FILE || "";
|
|
8978
|
+
let stdout = "";
|
|
8979
|
+
let stderr = "";
|
|
8980
|
+
let timedOut = false;
|
|
8981
|
+
let finished = false;
|
|
8982
|
+
let timeoutTimer;
|
|
8983
|
+
let killTimer;
|
|
8984
|
+
let sigkillSent = false;
|
|
8985
|
+
let pendingExit = null;
|
|
8986
|
+
|
|
8987
|
+
const child = spawn(command, args, {
|
|
8988
|
+
detached: true,
|
|
8989
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
8990
|
+
env: process.env,
|
|
8991
|
+
});
|
|
8992
|
+
|
|
8993
|
+
if (pgidFile && child.pid) {
|
|
8994
|
+
try {
|
|
8995
|
+
writeFileSync(pgidFile, String(child.pid), { mode: 0o600 });
|
|
8996
|
+
} catch {}
|
|
8997
|
+
}
|
|
8998
|
+
|
|
8999
|
+
function appendText(target, chunk) {
|
|
9000
|
+
return target + String(chunk);
|
|
9001
|
+
}
|
|
9002
|
+
|
|
9003
|
+
function killTarget(signal) {
|
|
9004
|
+
if (!child.pid) return;
|
|
9005
|
+
if (process.platform === "win32") {
|
|
9006
|
+
try {
|
|
9007
|
+
process.kill(child.pid, signal);
|
|
9008
|
+
} catch {}
|
|
9009
|
+
return;
|
|
9010
|
+
}
|
|
9011
|
+
try {
|
|
9012
|
+
process.kill(-child.pid, signal);
|
|
9013
|
+
} catch {}
|
|
9014
|
+
}
|
|
9015
|
+
|
|
9016
|
+
function finish(code, signal) {
|
|
9017
|
+
if (finished) return;
|
|
9018
|
+
if (timedOut && !sigkillSent) {
|
|
9019
|
+
pendingExit = { code, signal };
|
|
9020
|
+
return;
|
|
9021
|
+
}
|
|
9022
|
+
finished = true;
|
|
9023
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
9024
|
+
if (killTimer) clearTimeout(killTimer);
|
|
9025
|
+
if (timedOut) {
|
|
9026
|
+
stderr = [stderr, "Command timed out after " + timeoutMs + "ms."].filter(Boolean).join(stderr ? "\\n" : "");
|
|
9027
|
+
}
|
|
9028
|
+
const exitCode = timedOut ? 124 : code ?? 1;
|
|
9029
|
+
process.stdout.write(JSON.stringify({
|
|
9030
|
+
stdout,
|
|
9031
|
+
stderr,
|
|
9032
|
+
exitCode,
|
|
9033
|
+
timedOut,
|
|
9034
|
+
signal: signal ?? null,
|
|
9035
|
+
}), () => process.exit(exitCode));
|
|
9036
|
+
}
|
|
9037
|
+
|
|
9038
|
+
child.stdout.setEncoding("utf8");
|
|
9039
|
+
child.stderr.setEncoding("utf8");
|
|
9040
|
+
child.stdout.on("data", (chunk) => { stdout = appendText(stdout, chunk); });
|
|
9041
|
+
child.stderr.on("data", (chunk) => { stderr = appendText(stderr, chunk); });
|
|
9042
|
+
let childExit = { code: null, signal: null };
|
|
9043
|
+
child.on("error", (error) => {
|
|
9044
|
+
stderr = [stderr, error instanceof Error ? error.message : String(error)].filter(Boolean).join(stderr ? "\\n" : "");
|
|
9045
|
+
finish(1, null);
|
|
9046
|
+
});
|
|
9047
|
+
child.on("exit", (code, signal) => {
|
|
9048
|
+
childExit = { code, signal };
|
|
9049
|
+
});
|
|
9050
|
+
child.on("close", (code, signal) => {
|
|
9051
|
+
finish(code ?? childExit.code, signal ?? childExit.signal);
|
|
9052
|
+
});
|
|
9053
|
+
|
|
9054
|
+
timeoutTimer = setTimeout(() => {
|
|
9055
|
+
timedOut = true;
|
|
9056
|
+
killTarget("SIGTERM");
|
|
9057
|
+
killTimer = setTimeout(() => {
|
|
9058
|
+
sigkillSent = true;
|
|
9059
|
+
killTarget("SIGKILL");
|
|
9060
|
+
if (pendingExit) finish(pendingExit.code, pendingExit.signal);
|
|
9061
|
+
}, killGraceMs);
|
|
9062
|
+
}, timeoutMs);
|
|
9063
|
+
`;
|
|
8560
9064
|
function describeMachineCommandFailure(operation, result) {
|
|
8561
9065
|
const detail = (result.stderr || result.stdout || "").trim();
|
|
8562
9066
|
const suffix = detail ? `: ${detail}` : "";
|
|
@@ -8671,17 +9175,17 @@ function buildSetupPlan(machineId) {
|
|
|
8671
9175
|
workspacePath: `${homedir2()}/workspace`
|
|
8672
9176
|
};
|
|
8673
9177
|
const steps = [...buildBaseSteps(target), ...buildPackageSteps(target)];
|
|
8674
|
-
return {
|
|
9178
|
+
return attachMutationPlanDigest({
|
|
8675
9179
|
machineId: target.id,
|
|
8676
9180
|
mode: "plan",
|
|
8677
9181
|
steps,
|
|
8678
9182
|
executed: 0
|
|
8679
|
-
};
|
|
9183
|
+
});
|
|
8680
9184
|
}
|
|
8681
|
-
function
|
|
8682
|
-
|
|
9185
|
+
function runSetupPlan(plan, options = {}, runner = runMachineCommand) {
|
|
9186
|
+
assertMutationPlanDigest(plan, options.expectedPlanDigest);
|
|
8683
9187
|
if (!options.apply) {
|
|
8684
|
-
return plan;
|
|
9188
|
+
return attachMutationPlanDigest({ ...plan, mode: "plan", executed: 0 });
|
|
8685
9189
|
}
|
|
8686
9190
|
if (!options.yes) {
|
|
8687
9191
|
throw new Error("Setup execution requires --yes.");
|
|
@@ -8702,19 +9206,19 @@ function runSetup(machineId, options = {}, runner = runMachineCommand) {
|
|
|
8702
9206
|
}
|
|
8703
9207
|
executed += 1;
|
|
8704
9208
|
}
|
|
8705
|
-
const summary = {
|
|
9209
|
+
const summary = attachMutationPlanDigest({
|
|
8706
9210
|
machineId: plan.machineId,
|
|
8707
9211
|
mode: "apply",
|
|
8708
9212
|
steps: plan.steps,
|
|
8709
9213
|
executed
|
|
8710
|
-
};
|
|
9214
|
+
});
|
|
8711
9215
|
recordSetupRun(plan.machineId, "completed", summary);
|
|
8712
9216
|
return summary;
|
|
8713
9217
|
}
|
|
8714
9218
|
|
|
8715
9219
|
// src/commands/backup.ts
|
|
8716
9220
|
import { homedir as homedir3, hostname as hostname5 } from "os";
|
|
8717
|
-
import { join as
|
|
9221
|
+
import { join as join4 } from "path";
|
|
8718
9222
|
var MACHINES_BACKUP_BUCKET_ENV = "HASNA_MACHINES_S3_BUCKET";
|
|
8719
9223
|
var MACHINES_BACKUP_BUCKET_FALLBACK_ENV = "MACHINES_S3_BUCKET";
|
|
8720
9224
|
var MACHINES_BACKUP_PREFIX_ENV = "HASNA_MACHINES_S3_PREFIX";
|
|
@@ -8764,14 +9268,14 @@ function resolveBackupTarget(options = {}) {
|
|
|
8764
9268
|
function defaultBackupSources() {
|
|
8765
9269
|
const home = homedir3();
|
|
8766
9270
|
return [
|
|
8767
|
-
|
|
8768
|
-
|
|
8769
|
-
|
|
9271
|
+
join4(home, ".hasna"),
|
|
9272
|
+
join4(home, ".ssh"),
|
|
9273
|
+
join4(home, ".secrets")
|
|
8770
9274
|
];
|
|
8771
9275
|
}
|
|
8772
9276
|
function buildBackupPlan(bucket, prefix) {
|
|
8773
9277
|
const target = resolveBackupTarget({ bucket, prefix });
|
|
8774
|
-
const archivePath =
|
|
9278
|
+
const archivePath = join4(homedir3(), ".hasna", "machines", "backup.tgz");
|
|
8775
9279
|
const sources = defaultBackupSources();
|
|
8776
9280
|
const steps = [
|
|
8777
9281
|
{
|
|
@@ -8823,20 +9327,20 @@ function runBackup(bucket, prefix, options = {}) {
|
|
|
8823
9327
|
|
|
8824
9328
|
// src/commands/cert.ts
|
|
8825
9329
|
import { homedir as homedir4, platform as platform3 } from "os";
|
|
8826
|
-
import { join as
|
|
9330
|
+
import { join as join5 } from "path";
|
|
8827
9331
|
function quote3(value) {
|
|
8828
9332
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
8829
9333
|
}
|
|
8830
9334
|
function certDir() {
|
|
8831
|
-
return
|
|
9335
|
+
return join5(homedir4(), ".hasna", "machines", "certs");
|
|
8832
9336
|
}
|
|
8833
9337
|
function buildCertPlan(domains) {
|
|
8834
9338
|
if (domains.length === 0) {
|
|
8835
9339
|
throw new Error("At least one domain is required.");
|
|
8836
9340
|
}
|
|
8837
9341
|
const primary = domains[0];
|
|
8838
|
-
const certPath =
|
|
8839
|
-
const keyPath =
|
|
9342
|
+
const certPath = join5(certDir(), `${primary}.pem`);
|
|
9343
|
+
const keyPath = join5(certDir(), `${primary}-key.pem`);
|
|
8840
9344
|
const steps = [];
|
|
8841
9345
|
if (platform3() === "darwin") {
|
|
8842
9346
|
steps.push({
|
|
@@ -8901,16 +9405,16 @@ function runCertPlan(domains, options = {}) {
|
|
|
8901
9405
|
|
|
8902
9406
|
// src/commands/dns.ts
|
|
8903
9407
|
init_paths();
|
|
8904
|
-
import { existsSync as
|
|
8905
|
-
import { join as
|
|
9408
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
9409
|
+
import { join as join6 } from "path";
|
|
8906
9410
|
function getDnsPath() {
|
|
8907
|
-
return
|
|
9411
|
+
return join6(getDataDir(), "dns.json");
|
|
8908
9412
|
}
|
|
8909
9413
|
function readMappings() {
|
|
8910
9414
|
const path = getDnsPath();
|
|
8911
|
-
if (!
|
|
9415
|
+
if (!existsSync6(path))
|
|
8912
9416
|
return [];
|
|
8913
|
-
return JSON.parse(
|
|
9417
|
+
return JSON.parse(readFileSync4(path, "utf8"));
|
|
8914
9418
|
}
|
|
8915
9419
|
function writeMappings(mappings) {
|
|
8916
9420
|
const path = getDnsPath();
|
|
@@ -8937,10 +9441,10 @@ function renderDomainMapping(domain) {
|
|
|
8937
9441
|
hostsEntry: `${entry.targetHost} ${entry.domain}`,
|
|
8938
9442
|
caddySnippet: `${entry.domain} {
|
|
8939
9443
|
reverse_proxy 127.0.0.1:${entry.port}
|
|
8940
|
-
tls ${
|
|
9444
|
+
tls ${join6(getDataDir(), "certs", `${entry.domain}.pem`)} ${join6(getDataDir(), "certs", `${entry.domain}-key.pem`)}
|
|
8941
9445
|
}`,
|
|
8942
|
-
certPath:
|
|
8943
|
-
keyPath:
|
|
9446
|
+
certPath: join6(getDataDir(), "certs", `${entry.domain}.pem`),
|
|
9447
|
+
keyPath: join6(getDataDir(), "certs", `${entry.domain}-key.pem`)
|
|
8944
9448
|
};
|
|
8945
9449
|
}
|
|
8946
9450
|
|
|
@@ -9077,12 +9581,12 @@ function listApps(machineId) {
|
|
|
9077
9581
|
}
|
|
9078
9582
|
function buildAppsPlan(machineId) {
|
|
9079
9583
|
const machine = resolveMachine(machineId);
|
|
9080
|
-
return {
|
|
9584
|
+
return attachMutationPlanDigest({
|
|
9081
9585
|
machineId: machine.id,
|
|
9082
9586
|
mode: "plan",
|
|
9083
9587
|
steps: buildAppSteps(machine),
|
|
9084
9588
|
executed: 0
|
|
9085
|
-
};
|
|
9589
|
+
});
|
|
9086
9590
|
}
|
|
9087
9591
|
function getAppsStatus(machineId, runner = runMachineCommand) {
|
|
9088
9592
|
const machine = resolveMachine(machineId);
|
|
@@ -9105,10 +9609,10 @@ function diffApps(machineId, runner = runMachineCommand) {
|
|
|
9105
9609
|
installed: status.apps.filter((app) => app.installed).map((app) => app.name)
|
|
9106
9610
|
};
|
|
9107
9611
|
}
|
|
9108
|
-
function
|
|
9109
|
-
|
|
9612
|
+
function runAppsPlan(plan, options = {}, runner = runMachineCommand) {
|
|
9613
|
+
assertMutationPlanDigest(plan, options.expectedPlanDigest);
|
|
9110
9614
|
if (!options.apply)
|
|
9111
|
-
return plan;
|
|
9615
|
+
return attachMutationPlanDigest({ ...plan, mode: "plan", executed: 0 });
|
|
9112
9616
|
if (!options.yes) {
|
|
9113
9617
|
throw new Error("App installation requires --yes.");
|
|
9114
9618
|
}
|
|
@@ -9117,12 +9621,12 @@ function runAppsInstall(machineId, options = {}, runner = runMachineCommand) {
|
|
|
9117
9621
|
requireMachineCommandSuccess(`App install ${step.id}`, runner(plan.machineId, step.command));
|
|
9118
9622
|
executed += 1;
|
|
9119
9623
|
}
|
|
9120
|
-
return {
|
|
9624
|
+
return attachMutationPlanDigest({
|
|
9121
9625
|
machineId: plan.machineId,
|
|
9122
9626
|
mode: "apply",
|
|
9123
9627
|
steps: plan.steps,
|
|
9124
9628
|
executed
|
|
9125
|
-
};
|
|
9629
|
+
});
|
|
9126
9630
|
}
|
|
9127
9631
|
|
|
9128
9632
|
// src/commands/install-claude.ts
|
|
@@ -9184,12 +9688,12 @@ function parseProbe(tool, stdout) {
|
|
|
9184
9688
|
}
|
|
9185
9689
|
function buildClaudeInstallPlan(machineId, tools) {
|
|
9186
9690
|
const machine = resolveMachine2(machineId);
|
|
9187
|
-
return {
|
|
9691
|
+
return attachMutationPlanDigest({
|
|
9188
9692
|
machineId: machine.id,
|
|
9189
9693
|
mode: "plan",
|
|
9190
9694
|
steps: buildInstallSteps(machine, tools),
|
|
9191
9695
|
executed: 0
|
|
9192
|
-
};
|
|
9696
|
+
});
|
|
9193
9697
|
}
|
|
9194
9698
|
function getClaudeCliStatus(machineId, tools, runner = runMachineCommand) {
|
|
9195
9699
|
const machine = resolveMachine2(machineId);
|
|
@@ -9212,10 +9716,10 @@ function diffClaudeCli(machineId, tools, runner = runMachineCommand) {
|
|
|
9212
9716
|
installed: status.tools.filter((tool) => tool.installed).map((tool) => tool.tool)
|
|
9213
9717
|
};
|
|
9214
9718
|
}
|
|
9215
|
-
function
|
|
9216
|
-
|
|
9719
|
+
function runClaudeInstallPlan(plan, options = {}, runner = runMachineCommand) {
|
|
9720
|
+
assertMutationPlanDigest(plan, options.expectedPlanDigest);
|
|
9217
9721
|
if (!options.apply)
|
|
9218
|
-
return plan;
|
|
9722
|
+
return attachMutationPlanDigest({ ...plan, mode: "plan", executed: 0 });
|
|
9219
9723
|
if (!options.yes) {
|
|
9220
9724
|
throw new Error("Claude CLI installation requires --yes.");
|
|
9221
9725
|
}
|
|
@@ -9224,12 +9728,12 @@ function runClaudeInstall(machineId, tools, options = {}, runner = runMachineCom
|
|
|
9224
9728
|
requireMachineCommandSuccess(`AI CLI install ${step.id}`, runner(plan.machineId, step.command));
|
|
9225
9729
|
executed += 1;
|
|
9226
9730
|
}
|
|
9227
|
-
return {
|
|
9731
|
+
return attachMutationPlanDigest({
|
|
9228
9732
|
machineId: plan.machineId,
|
|
9229
9733
|
mode: "apply",
|
|
9230
9734
|
steps: plan.steps,
|
|
9231
9735
|
executed
|
|
9232
|
-
};
|
|
9736
|
+
});
|
|
9233
9737
|
}
|
|
9234
9738
|
|
|
9235
9739
|
// src/commands/install-tailscale.ts
|
|
@@ -9269,17 +9773,17 @@ function buildTailscaleInstallPlan(machineId) {
|
|
|
9269
9773
|
if (!machine) {
|
|
9270
9774
|
throw new Error(`Machine not found in manifest: ${machineId}`);
|
|
9271
9775
|
}
|
|
9272
|
-
return {
|
|
9776
|
+
return attachMutationPlanDigest({
|
|
9273
9777
|
machineId: machine.id,
|
|
9274
9778
|
mode: "plan",
|
|
9275
9779
|
steps: buildInstallSteps2(machine),
|
|
9276
9780
|
executed: 0
|
|
9277
|
-
};
|
|
9781
|
+
});
|
|
9278
9782
|
}
|
|
9279
|
-
function
|
|
9280
|
-
|
|
9783
|
+
function runTailscaleInstallPlan(plan, options = {}, runner = runMachineCommand) {
|
|
9784
|
+
assertMutationPlanDigest(plan, options.expectedPlanDigest);
|
|
9281
9785
|
if (!options.apply)
|
|
9282
|
-
return plan;
|
|
9786
|
+
return attachMutationPlanDigest({ ...plan, mode: "plan", executed: 0 });
|
|
9283
9787
|
if (!options.yes) {
|
|
9284
9788
|
throw new Error("Tailscale install requires --yes.");
|
|
9285
9789
|
}
|
|
@@ -9288,21 +9792,23 @@ function runTailscaleInstall(machineId, options = {}, runner = runMachineCommand
|
|
|
9288
9792
|
requireMachineCommandSuccess(`Tailscale install ${step.id}`, runner(plan.machineId, step.command));
|
|
9289
9793
|
executed += 1;
|
|
9290
9794
|
}
|
|
9291
|
-
return {
|
|
9795
|
+
return attachMutationPlanDigest({
|
|
9292
9796
|
machineId: plan.machineId,
|
|
9293
9797
|
mode: "apply",
|
|
9294
9798
|
steps: plan.steps,
|
|
9295
9799
|
executed
|
|
9296
|
-
};
|
|
9800
|
+
});
|
|
9297
9801
|
}
|
|
9298
9802
|
|
|
9299
9803
|
// src/commands/notifications.ts
|
|
9300
|
-
import { existsSync as
|
|
9804
|
+
import { accessSync, constants, existsSync as existsSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
9805
|
+
import { delimiter, isAbsolute, join as join7 } from "path";
|
|
9301
9806
|
init_paths();
|
|
9302
9807
|
var notificationChannelSchema = exports_external.object({
|
|
9303
9808
|
id: exports_external.string(),
|
|
9304
9809
|
type: exports_external.enum(["email", "webhook", "command"]),
|
|
9305
9810
|
target: exports_external.string(),
|
|
9811
|
+
commandArgs: exports_external.array(exports_external.string()).optional(),
|
|
9306
9812
|
events: exports_external.array(exports_external.string()),
|
|
9307
9813
|
enabled: exports_external.boolean()
|
|
9308
9814
|
});
|
|
@@ -9311,19 +9817,31 @@ var notificationConfigSchema = exports_external.object({
|
|
|
9311
9817
|
updatedAt: exports_external.string().optional(),
|
|
9312
9818
|
channels: exports_external.array(notificationChannelSchema)
|
|
9313
9819
|
});
|
|
9820
|
+
var trustedNotificationApproval = Symbol("trustedNotificationApproval");
|
|
9821
|
+
function createTrustedNotificationApproval() {
|
|
9822
|
+
return { [trustedNotificationApproval]: true };
|
|
9823
|
+
}
|
|
9824
|
+
function isTrustedNotificationApproval(approval) {
|
|
9825
|
+
return approval?.[trustedNotificationApproval] === true;
|
|
9826
|
+
}
|
|
9314
9827
|
function sortChannels(channels) {
|
|
9315
9828
|
return [...channels].sort((left, right) => left.id.localeCompare(right.id));
|
|
9316
9829
|
}
|
|
9317
|
-
function shellQuote5(value) {
|
|
9318
|
-
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
9319
|
-
}
|
|
9320
9830
|
function hasCommand2(binary) {
|
|
9321
|
-
|
|
9322
|
-
|
|
9323
|
-
|
|
9324
|
-
|
|
9325
|
-
|
|
9326
|
-
|
|
9831
|
+
return Boolean(resolveExecutable(binary));
|
|
9832
|
+
}
|
|
9833
|
+
function resolveExecutable(binary) {
|
|
9834
|
+
const trimmed = binary.trim();
|
|
9835
|
+
if (!trimmed)
|
|
9836
|
+
return null;
|
|
9837
|
+
const candidates = isAbsolute(trimmed) ? [trimmed] : (process.env.PATH ?? "").split(delimiter).filter(Boolean).map((dir) => join7(dir, trimmed));
|
|
9838
|
+
for (const candidate of candidates) {
|
|
9839
|
+
try {
|
|
9840
|
+
accessSync(candidate, constants.X_OK);
|
|
9841
|
+
return candidate;
|
|
9842
|
+
} catch {}
|
|
9843
|
+
}
|
|
9844
|
+
return null;
|
|
9327
9845
|
}
|
|
9328
9846
|
function buildNotificationPreview(channel, event, message) {
|
|
9329
9847
|
if (channel.type === "email") {
|
|
@@ -9332,7 +9850,8 @@ function buildNotificationPreview(channel, event, message) {
|
|
|
9332
9850
|
if (channel.type === "webhook") {
|
|
9333
9851
|
return `POST ${channel.target} with payload {"event":"${event}","message":"${message}"}`;
|
|
9334
9852
|
}
|
|
9335
|
-
|
|
9853
|
+
const args = channel.commandArgs?.length ? ` ${channel.commandArgs.join(" ")}` : "";
|
|
9854
|
+
return `${channel.target}${args} with HASNA_MACHINES_NOTIFICATION_* environment`;
|
|
9336
9855
|
}
|
|
9337
9856
|
async function dispatchEmail(channel, event, message) {
|
|
9338
9857
|
const subject = `[${event}] machines notification`;
|
|
@@ -9343,7 +9862,7 @@ Content-Type: text/plain; charset=utf-8
|
|
|
9343
9862
|
${message}
|
|
9344
9863
|
`;
|
|
9345
9864
|
if (hasCommand2("sendmail")) {
|
|
9346
|
-
const result = Bun.spawnSync(["
|
|
9865
|
+
const result = Bun.spawnSync(["sendmail", "-t"], {
|
|
9347
9866
|
stdin: new TextEncoder().encode(body),
|
|
9348
9867
|
stdout: "pipe",
|
|
9349
9868
|
stderr: "pipe",
|
|
@@ -9361,8 +9880,9 @@ ${message}
|
|
|
9361
9880
|
};
|
|
9362
9881
|
}
|
|
9363
9882
|
if (hasCommand2("mail")) {
|
|
9364
|
-
const
|
|
9365
|
-
|
|
9883
|
+
const result = Bun.spawnSync(["mail", "-s", subject, channel.target], {
|
|
9884
|
+
stdin: new TextEncoder().encode(`${message}
|
|
9885
|
+
`),
|
|
9366
9886
|
stdout: "pipe",
|
|
9367
9887
|
stderr: "pipe",
|
|
9368
9888
|
env: process.env
|
|
@@ -9405,8 +9925,20 @@ async function dispatchWebhook(channel, event, message) {
|
|
|
9405
9925
|
detail: `Webhook accepted with HTTP ${response.status}`
|
|
9406
9926
|
};
|
|
9407
9927
|
}
|
|
9408
|
-
async function dispatchCommand(channel, event, message) {
|
|
9409
|
-
|
|
9928
|
+
async function dispatchCommand(channel, event, message, options = {}) {
|
|
9929
|
+
if (!isTrustedNotificationApproval(options.trustedApproval)) {
|
|
9930
|
+
assertMutationApproved({
|
|
9931
|
+
surface: "notifications",
|
|
9932
|
+
operation: "dispatch_command",
|
|
9933
|
+
resourceId: channel.id,
|
|
9934
|
+
approvalToken: options.approvalToken
|
|
9935
|
+
});
|
|
9936
|
+
}
|
|
9937
|
+
const executable = resolveExecutable(channel.target);
|
|
9938
|
+
if (!executable) {
|
|
9939
|
+
throw new Error(`Command executable not found or not executable: ${channel.target}`);
|
|
9940
|
+
}
|
|
9941
|
+
const result = Bun.spawnSync([executable, ...channel.commandArgs ?? []], {
|
|
9410
9942
|
stdout: "pipe",
|
|
9411
9943
|
stderr: "pipe",
|
|
9412
9944
|
env: {
|
|
@@ -9428,7 +9960,7 @@ async function dispatchCommand(channel, event, message) {
|
|
|
9428
9960
|
detail: stdout || "Command completed successfully"
|
|
9429
9961
|
};
|
|
9430
9962
|
}
|
|
9431
|
-
async function dispatchChannel(channel, event, message) {
|
|
9963
|
+
async function dispatchChannel(channel, event, message, options = {}) {
|
|
9432
9964
|
if (!channel.enabled) {
|
|
9433
9965
|
return {
|
|
9434
9966
|
channelId: channel.id,
|
|
@@ -9444,7 +9976,7 @@ async function dispatchChannel(channel, event, message) {
|
|
|
9444
9976
|
if (channel.type === "webhook") {
|
|
9445
9977
|
return dispatchWebhook(channel, event, message);
|
|
9446
9978
|
}
|
|
9447
|
-
return dispatchCommand(channel, event, message);
|
|
9979
|
+
return dispatchCommand(channel, event, message, options);
|
|
9448
9980
|
}
|
|
9449
9981
|
function getDefaultNotificationConfig() {
|
|
9450
9982
|
return {
|
|
@@ -9454,10 +9986,10 @@ function getDefaultNotificationConfig() {
|
|
|
9454
9986
|
};
|
|
9455
9987
|
}
|
|
9456
9988
|
function readNotificationConfig(path = getNotificationsPath()) {
|
|
9457
|
-
if (!
|
|
9989
|
+
if (!existsSync7(path)) {
|
|
9458
9990
|
return getDefaultNotificationConfig();
|
|
9459
9991
|
}
|
|
9460
|
-
return notificationConfigSchema.parse(JSON.parse(
|
|
9992
|
+
return notificationConfigSchema.parse(JSON.parse(readFileSync5(path, "utf8")));
|
|
9461
9993
|
}
|
|
9462
9994
|
function writeNotificationConfig(config, path = getNotificationsPath()) {
|
|
9463
9995
|
ensureParentDir(path);
|
|
@@ -9473,11 +10005,20 @@ function writeNotificationConfig(config, path = getNotificationsPath()) {
|
|
|
9473
10005
|
function listNotificationChannels() {
|
|
9474
10006
|
return readNotificationConfig();
|
|
9475
10007
|
}
|
|
9476
|
-
function addNotificationChannel(channel) {
|
|
10008
|
+
function addNotificationChannel(channel, options = {}) {
|
|
10009
|
+
if (channel.type === "command" && !isTrustedNotificationApproval(options.trustedApproval)) {
|
|
10010
|
+
assertMutationApproved({
|
|
10011
|
+
surface: "notifications",
|
|
10012
|
+
operation: "add_command_channel",
|
|
10013
|
+
resourceId: channel.id,
|
|
10014
|
+
approvalToken: options.approvalToken
|
|
10015
|
+
});
|
|
10016
|
+
}
|
|
9477
10017
|
const config = readNotificationConfig();
|
|
9478
10018
|
const channels = config.channels.filter((entry) => entry.id !== channel.id);
|
|
9479
10019
|
channels.push({
|
|
9480
10020
|
...channel,
|
|
10021
|
+
commandArgs: channel.commandArgs?.map(String),
|
|
9481
10022
|
events: [...new Set(channel.events)]
|
|
9482
10023
|
});
|
|
9483
10024
|
return writeNotificationConfig({ ...config, channels });
|
|
@@ -9499,7 +10040,7 @@ async function dispatchNotificationEvent(event, message, options = {}) {
|
|
|
9499
10040
|
const deliveries = [];
|
|
9500
10041
|
for (const channel of channels) {
|
|
9501
10042
|
try {
|
|
9502
|
-
deliveries.push(await dispatchChannel(channel, event, message));
|
|
10043
|
+
deliveries.push(await dispatchChannel(channel, event, message, { approvalToken: options.approvalToken, trustedApproval: options.trustedApproval }));
|
|
9503
10044
|
} catch (error) {
|
|
9504
10045
|
deliveries.push({
|
|
9505
10046
|
channelId: channel.id,
|
|
@@ -9534,7 +10075,7 @@ async function testNotificationChannel(channelId, event = "manual.test", message
|
|
|
9534
10075
|
if (!options.yes) {
|
|
9535
10076
|
throw new Error("Notification test execution requires --yes.");
|
|
9536
10077
|
}
|
|
9537
|
-
const delivery = await dispatchChannel(channel, event, message);
|
|
10078
|
+
const delivery = await dispatchChannel(channel, event, message, { approvalToken: options.approvalToken, trustedApproval: options.trustedApproval });
|
|
9538
10079
|
return {
|
|
9539
10080
|
channelId,
|
|
9540
10081
|
mode: "apply",
|
|
@@ -9603,6 +10144,46 @@ function listPorts(machineId) {
|
|
|
9603
10144
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
9604
10145
|
import { setTimeout as sleep } from "timers/promises";
|
|
9605
10146
|
import { EventsClient } from "@hasna/events";
|
|
10147
|
+
function shellQuote5(value) {
|
|
10148
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
10149
|
+
}
|
|
10150
|
+
function buildTmuxPaneDiedHookPlan(options = {}) {
|
|
10151
|
+
const tmuxCommand = options.tmuxCommand ?? process.env["HASNA_MACHINES_TMUX_BIN"] ?? "tmux";
|
|
10152
|
+
const machinesCommand = options.machinesCommand ?? "machines";
|
|
10153
|
+
const deliver = options.deliver === true;
|
|
10154
|
+
const approvalToken = options.approvalToken?.trim();
|
|
10155
|
+
const trustedLocalMutation = approvalToken ? false : options.trustedLocalMutation === true;
|
|
10156
|
+
const emitArgs = [
|
|
10157
|
+
"events",
|
|
10158
|
+
"emit",
|
|
10159
|
+
"machines.tmux.pane_died",
|
|
10160
|
+
"--source",
|
|
10161
|
+
"machines",
|
|
10162
|
+
"--subject",
|
|
10163
|
+
"tmux:#{hook_pane}",
|
|
10164
|
+
"--severity",
|
|
10165
|
+
"warning",
|
|
10166
|
+
"--message",
|
|
10167
|
+
"tmux pane died: #{hook_pane}",
|
|
10168
|
+
"--data",
|
|
10169
|
+
'{"target":"#{hook_pane}","session":"#{session_name}","window":"#{window_index}"}'
|
|
10170
|
+
];
|
|
10171
|
+
if (!deliver)
|
|
10172
|
+
emitArgs.push("--no-deliver");
|
|
10173
|
+
if (approvalToken)
|
|
10174
|
+
emitArgs.push("--approval-token", approvalToken);
|
|
10175
|
+
const command = [machinesCommand, ...emitArgs].map(shellQuote5).join(" ");
|
|
10176
|
+
const runShell = trustedLocalMutation ? `HASNA_MACHINES_ALLOW_MUTATIONS=1 ${command}` : command;
|
|
10177
|
+
const args = ["set-hook", "-g", "pane-died", `run-shell ${shellQuote5(runShell)}`];
|
|
10178
|
+
return {
|
|
10179
|
+
tmuxCommand,
|
|
10180
|
+
args,
|
|
10181
|
+
shellCommand: [tmuxCommand, ...args].map(shellQuote5).join(" "),
|
|
10182
|
+
eventType: "machines.tmux.pane_died",
|
|
10183
|
+
deliver,
|
|
10184
|
+
trustedLocalMutation
|
|
10185
|
+
};
|
|
10186
|
+
}
|
|
9606
10187
|
function probeTmuxPane(target, tmuxCommand = process.env["HASNA_MACHINES_TMUX_BIN"] || "tmux") {
|
|
9607
10188
|
const checkedAt = new Date().toISOString();
|
|
9608
10189
|
const result = spawnSync4(tmuxCommand, ["display-message", "-p", "-t", target, "#{pane_id}"], {
|
|
@@ -9789,17 +10370,23 @@ function buildScreenEnableCommand(machineId, options = {}) {
|
|
|
9789
10370
|
}
|
|
9790
10371
|
const secretsCommand = options.secretsCommand || "secrets";
|
|
9791
10372
|
const remoteCommand = buildScreenEnableRemoteCommandFromStdin(credentials.user);
|
|
10373
|
+
const secretsCommandArgs = [secretsCommand, "get", credentials.passwordSecretKey];
|
|
10374
|
+
const sshPlan = buildSshCommandPlan(machineId, remoteCommand, options);
|
|
9792
10375
|
return {
|
|
9793
10376
|
machineId: credentials.machineId,
|
|
9794
10377
|
user: credentials.user,
|
|
9795
10378
|
passwordSecretKey: credentials.passwordSecretKey,
|
|
9796
10379
|
remoteCommand,
|
|
9797
|
-
|
|
10380
|
+
secretsCommand,
|
|
10381
|
+
secretsCommandArgs,
|
|
10382
|
+
sshCommand: sshPlan.command,
|
|
10383
|
+
sshCommandArgs: sshPlan.args,
|
|
10384
|
+
command: `${shellCommand2(secretsCommandArgs)} | ${sshPlan.shellCommand}`
|
|
9798
10385
|
};
|
|
9799
10386
|
}
|
|
9800
10387
|
|
|
9801
10388
|
// src/commands/sync.ts
|
|
9802
|
-
import { existsSync as
|
|
10389
|
+
import { existsSync as existsSync8, lstatSync, readFileSync as readFileSync6, symlinkSync, copyFileSync } from "fs";
|
|
9803
10390
|
import { homedir as homedir5 } from "os";
|
|
9804
10391
|
init_paths();
|
|
9805
10392
|
init_db();
|
|
@@ -9854,15 +10441,15 @@ function detectFileActions(machine) {
|
|
|
9854
10441
|
throw new Error(`Remote file sync planning is not supported for ${machine.id}; refusing to inspect or apply local paths as remote state.`);
|
|
9855
10442
|
}
|
|
9856
10443
|
return (machine.files || []).map((file, index) => {
|
|
9857
|
-
const sourceExists =
|
|
9858
|
-
const targetExists =
|
|
10444
|
+
const sourceExists = existsSync8(file.source);
|
|
10445
|
+
const targetExists = existsSync8(file.target);
|
|
9859
10446
|
let status = "missing";
|
|
9860
10447
|
if (sourceExists && targetExists) {
|
|
9861
10448
|
if (file.mode === "symlink") {
|
|
9862
10449
|
status = lstatSync(file.target).isSymbolicLink() ? "ok" : "drifted";
|
|
9863
10450
|
} else {
|
|
9864
|
-
const source =
|
|
9865
|
-
const target =
|
|
10451
|
+
const source = readFileSync6(file.source, "utf8");
|
|
10452
|
+
const target = readFileSync6(file.target, "utf8");
|
|
9866
10453
|
status = source === target ? "ok" : "drifted";
|
|
9867
10454
|
}
|
|
9868
10455
|
}
|
|
@@ -9892,12 +10479,12 @@ function buildSyncPlan(machineId, runner = runMachineCommand) {
|
|
|
9892
10479
|
...detectPackageActions(target, runner),
|
|
9893
10480
|
...detectFileActions(target)
|
|
9894
10481
|
];
|
|
9895
|
-
return {
|
|
10482
|
+
return attachMutationPlanDigest({
|
|
9896
10483
|
machineId: target.id,
|
|
9897
10484
|
mode: "plan",
|
|
9898
10485
|
actions,
|
|
9899
10486
|
executed: 0
|
|
9900
|
-
};
|
|
10487
|
+
});
|
|
9901
10488
|
}
|
|
9902
10489
|
function applyFileAction(command) {
|
|
9903
10490
|
const [verb, source, target] = command.split(" ");
|
|
@@ -9919,10 +10506,10 @@ function applyFileAction(command) {
|
|
|
9919
10506
|
symlinkSync(sourcePath, targetPath);
|
|
9920
10507
|
}
|
|
9921
10508
|
}
|
|
9922
|
-
function
|
|
9923
|
-
|
|
10509
|
+
function runSyncPlan(plan, options = {}, runner = runMachineCommand) {
|
|
10510
|
+
assertMutationPlanDigest(plan, options.expectedPlanDigest);
|
|
9924
10511
|
if (!options.apply) {
|
|
9925
|
-
return plan;
|
|
10512
|
+
return attachMutationPlanDigest({ ...plan, mode: "plan", executed: 0 });
|
|
9926
10513
|
}
|
|
9927
10514
|
if (!options.yes) {
|
|
9928
10515
|
throw new Error("Sync execution requires --yes.");
|
|
@@ -9949,12 +10536,12 @@ function runSync(machineId, options = {}, runner = runMachineCommand) {
|
|
|
9949
10536
|
}
|
|
9950
10537
|
executed += 1;
|
|
9951
10538
|
}
|
|
9952
|
-
const summary = {
|
|
10539
|
+
const summary = attachMutationPlanDigest({
|
|
9953
10540
|
machineId: plan.machineId,
|
|
9954
10541
|
mode: "apply",
|
|
9955
10542
|
actions: plan.actions,
|
|
9956
10543
|
executed
|
|
9957
|
-
};
|
|
10544
|
+
});
|
|
9958
10545
|
recordSyncRun(plan.machineId, "completed", summary);
|
|
9959
10546
|
return summary;
|
|
9960
10547
|
}
|
|
@@ -10589,8 +11176,8 @@ function runDoctor(machineId, options = {}) {
|
|
|
10589
11176
|
|
|
10590
11177
|
// src/commands/daemon.ts
|
|
10591
11178
|
import { execFileSync } from "child_process";
|
|
10592
|
-
import { chmodSync, existsSync as
|
|
10593
|
-
import { delimiter, dirname as dirname4 } from "path";
|
|
11179
|
+
import { chmodSync, existsSync as existsSync9, readFileSync as readFileSync7, statSync, mkdirSync as mkdirSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
11180
|
+
import { delimiter as delimiter2, dirname as dirname4 } from "path";
|
|
10594
11181
|
import { platform as osPlatform } from "os";
|
|
10595
11182
|
var DEFAULT_SERVICE_NAME = "machines-agent";
|
|
10596
11183
|
var DEFAULT_EXECUTABLE = "/usr/local/bin/machines-agent";
|
|
@@ -10998,13 +11585,13 @@ function bunRuntimeCandidates(executable) {
|
|
|
10998
11585
|
`${dirname4(executable)}/bun`,
|
|
10999
11586
|
process.env["BUN_INSTALL"] ? `${process.env["BUN_INSTALL"]}/bin/bun` : null,
|
|
11000
11587
|
process.env["HOME"] ? `${process.env["HOME"]}/.bun/bin/bun` : null,
|
|
11001
|
-
...(process.env["PATH"] ?? "").split(
|
|
11588
|
+
...(process.env["PATH"] ?? "").split(delimiter2).filter(Boolean).map((entry) => `${entry}/bun`)
|
|
11002
11589
|
].filter((value) => Boolean(value));
|
|
11003
11590
|
return [...new Set(candidates)];
|
|
11004
11591
|
}
|
|
11005
11592
|
function isBunShebangScript(executable) {
|
|
11006
11593
|
try {
|
|
11007
|
-
const content =
|
|
11594
|
+
const content = readFileSync7(executable, "utf8").slice(0, 256);
|
|
11008
11595
|
const firstLine2 = content.split(/\r?\n/, 1)[0] ?? "";
|
|
11009
11596
|
return /^#!.*\bbun\b/.test(firstLine2);
|
|
11010
11597
|
} catch {
|
|
@@ -11012,7 +11599,7 @@ function isBunShebangScript(executable) {
|
|
|
11012
11599
|
}
|
|
11013
11600
|
}
|
|
11014
11601
|
function isExecutableFile(path) {
|
|
11015
|
-
if (!
|
|
11602
|
+
if (!existsSync9(path))
|
|
11016
11603
|
return false;
|
|
11017
11604
|
try {
|
|
11018
11605
|
const stats = statSync(path);
|
|
@@ -11062,7 +11649,8 @@ function basename(path) {
|
|
|
11062
11649
|
init_db();
|
|
11063
11650
|
|
|
11064
11651
|
// src/commands/serve.ts
|
|
11065
|
-
import { EventsClient as EventsClient2, sanitizeChannelsForOutput } from "@hasna/events";
|
|
11652
|
+
import { EventsClient as EventsClient2, getEventsDataDir, sanitizeChannelsForOutput } from "@hasna/events";
|
|
11653
|
+
import { resolve as resolve3 } from "path";
|
|
11066
11654
|
|
|
11067
11655
|
// src/agent/runtime.ts
|
|
11068
11656
|
import { arch as arch3, hostname as hostname6, platform as platform4, release, uptime, version as osVersion } from "os";
|
|
@@ -11149,7 +11737,7 @@ function escapeHtml(value) {
|
|
|
11149
11737
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
11150
11738
|
}
|
|
11151
11739
|
function getServeInfo(options = {}) {
|
|
11152
|
-
const host = options.host || "
|
|
11740
|
+
const host = options.host || "127.0.0.1";
|
|
11153
11741
|
const port = options.port || 7676;
|
|
11154
11742
|
return {
|
|
11155
11743
|
host,
|
|
@@ -11352,6 +11940,59 @@ async function parseJsonBody(request) {
|
|
|
11352
11940
|
function jsonError(message, status = 400) {
|
|
11353
11941
|
return Response.json({ error: message }, { status });
|
|
11354
11942
|
}
|
|
11943
|
+
function dashboardResourceId(kind, ...parts) {
|
|
11944
|
+
const values = parts.map((part) => String(part ?? "*").trim()).filter(Boolean).join(":");
|
|
11945
|
+
return values ? `${kind}:${values}` : kind;
|
|
11946
|
+
}
|
|
11947
|
+
function eventStoreDir() {
|
|
11948
|
+
return resolve3(getEventsDataDir());
|
|
11949
|
+
}
|
|
11950
|
+
function eventStoreScope() {
|
|
11951
|
+
return { event_store_dir: eventStoreDir() };
|
|
11952
|
+
}
|
|
11953
|
+
function eventStoreResourceId(kind, ...parts) {
|
|
11954
|
+
return dashboardResourceId(kind, mutationArgsSha256(eventStoreScope()), ...parts);
|
|
11955
|
+
}
|
|
11956
|
+
function withEventStoreScope(args) {
|
|
11957
|
+
return { event_store_dir: eventStoreDir(), ...args };
|
|
11958
|
+
}
|
|
11959
|
+
function dashboardMutationCallerId() {
|
|
11960
|
+
return process.env[MUTATION_APPROVAL_CALLER_ENV]?.trim() || "dashboard";
|
|
11961
|
+
}
|
|
11962
|
+
function dashboardMutationRunId() {
|
|
11963
|
+
return process.env[MUTATION_APPROVAL_RUN_ENV]?.trim() || "dashboard";
|
|
11964
|
+
}
|
|
11965
|
+
function approvalTokenFromRequest(request, body) {
|
|
11966
|
+
const bodyToken = typeof body["approval_token"] === "string" ? body["approval_token"] : typeof body["approvalToken"] === "string" ? body["approvalToken"] : undefined;
|
|
11967
|
+
if (bodyToken?.trim())
|
|
11968
|
+
return bodyToken;
|
|
11969
|
+
const headerToken = request.headers.get("x-hasna-approval-token")?.trim();
|
|
11970
|
+
if (headerToken)
|
|
11971
|
+
return headerToken;
|
|
11972
|
+
const authorization = request.headers.get("authorization")?.trim();
|
|
11973
|
+
if (authorization?.toLowerCase().startsWith("bearer ")) {
|
|
11974
|
+
return authorization.slice("bearer ".length).trim();
|
|
11975
|
+
}
|
|
11976
|
+
return;
|
|
11977
|
+
}
|
|
11978
|
+
function requireDashboardMutation(operation, request, body, scope = {}) {
|
|
11979
|
+
const decision = verifyMutationApprovalToken({
|
|
11980
|
+
surface: "dashboard",
|
|
11981
|
+
operation,
|
|
11982
|
+
transport: "dashboard:http",
|
|
11983
|
+
callerId: dashboardMutationCallerId(),
|
|
11984
|
+
runId: dashboardMutationRunId(),
|
|
11985
|
+
resourceId: scope.resourceId,
|
|
11986
|
+
args: scope.args,
|
|
11987
|
+
approvalToken: approvalTokenFromRequest(request, body)
|
|
11988
|
+
});
|
|
11989
|
+
if (decision.approved)
|
|
11990
|
+
return;
|
|
11991
|
+
return jsonError(`Mutation approval denied: ${decision.reason ?? "approval_token is invalid."}`, 403);
|
|
11992
|
+
}
|
|
11993
|
+
function objectBodyValue(value) {
|
|
11994
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
11995
|
+
}
|
|
11355
11996
|
function privateOutputWarnings(requested, allowed) {
|
|
11356
11997
|
return requested && !allowed ? [PRIVATE_OUTPUT_DENIED_WARNING] : [];
|
|
11357
11998
|
}
|
|
@@ -11363,6 +12004,7 @@ function appendWarnings(payload, warnings) {
|
|
|
11363
12004
|
function startDashboardServer(options = {}) {
|
|
11364
12005
|
const info = getServeInfo(options);
|
|
11365
12006
|
const events = new EventsClient2;
|
|
12007
|
+
const trustedNotificationApproval2 = createTrustedNotificationApproval();
|
|
11366
12008
|
return Bun.serve({
|
|
11367
12009
|
hostname: info.host,
|
|
11368
12010
|
port: info.port,
|
|
@@ -11427,8 +12069,25 @@ function startDashboardServer(options = {}) {
|
|
|
11427
12069
|
const severity = typeof body["severity"] === "string" ? body["severity"] : undefined;
|
|
11428
12070
|
const message = typeof body["message"] === "string" ? body["message"] : undefined;
|
|
11429
12071
|
const dedupeKey = typeof body["dedupeKey"] === "string" ? body["dedupeKey"] : undefined;
|
|
11430
|
-
const data =
|
|
11431
|
-
const metadata =
|
|
12072
|
+
const data = objectBodyValue(body["data"]);
|
|
12073
|
+
const metadata = objectBodyValue(body["metadata"]);
|
|
12074
|
+
const denied = requireDashboardMutation("machines_events_emit", request, body, {
|
|
12075
|
+
resourceId: eventStoreResourceId("event", type, subject, dedupeKey),
|
|
12076
|
+
args: withEventStoreScope({
|
|
12077
|
+
event_type: type,
|
|
12078
|
+
source,
|
|
12079
|
+
subject,
|
|
12080
|
+
severity,
|
|
12081
|
+
message,
|
|
12082
|
+
data,
|
|
12083
|
+
metadata,
|
|
12084
|
+
dedupe_key: dedupeKey,
|
|
12085
|
+
deliver: true,
|
|
12086
|
+
dedupe: true
|
|
12087
|
+
})
|
|
12088
|
+
});
|
|
12089
|
+
if (denied)
|
|
12090
|
+
return denied;
|
|
11432
12091
|
return Response.json(await events.emit({
|
|
11433
12092
|
source,
|
|
11434
12093
|
type,
|
|
@@ -11471,8 +12130,20 @@ function startDashboardServer(options = {}) {
|
|
|
11471
12130
|
const message = typeof body["message"] === "string" ? body["message"] : undefined;
|
|
11472
12131
|
const apply = body["apply"] === true;
|
|
11473
12132
|
const yes = body["yes"] === true;
|
|
12133
|
+
const resolvedEvent = event ?? "manual.test";
|
|
12134
|
+
const resolvedMessage = message ?? "machines notification test";
|
|
12135
|
+
const denied = requireDashboardMutation("machines_notifications_test", request, body, {
|
|
12136
|
+
resourceId: dashboardResourceId("notification-test", channelId, resolvedEvent),
|
|
12137
|
+
args: { channel_id: channelId, event: resolvedEvent, message: resolvedMessage, apply, yes }
|
|
12138
|
+
});
|
|
12139
|
+
if (denied)
|
|
12140
|
+
return denied;
|
|
11474
12141
|
try {
|
|
11475
|
-
return Response.json(await testNotificationChannel(channelId,
|
|
12142
|
+
return Response.json(await testNotificationChannel(channelId, resolvedEvent, resolvedMessage, {
|
|
12143
|
+
apply,
|
|
12144
|
+
yes,
|
|
12145
|
+
trustedApproval: apply ? trustedNotificationApproval2 : undefined
|
|
12146
|
+
}));
|
|
11476
12147
|
} catch (error) {
|
|
11477
12148
|
return jsonError(error instanceof Error ? error.message : String(error));
|
|
11478
12149
|
}
|
|
@@ -11487,13 +12158,22 @@ function startDashboardServer(options = {}) {
|
|
|
11487
12158
|
return jsonError("channelId is required.");
|
|
11488
12159
|
}
|
|
11489
12160
|
const type = typeof body["type"] === "string" ? body["type"] : "events.test";
|
|
11490
|
-
const
|
|
12161
|
+
const subject = channelId;
|
|
12162
|
+
const message = typeof body["message"] === "string" ? body["message"] : "Hasna events test delivery";
|
|
12163
|
+
const data = objectBodyValue(body["data"]);
|
|
12164
|
+
const denied = requireDashboardMutation("machines_webhooks_test", request, body, {
|
|
12165
|
+
resourceId: eventStoreResourceId("webhook-test", channelId, type),
|
|
12166
|
+
args: withEventStoreScope({ channel_id: channelId, event_type: type, subject, message, data })
|
|
12167
|
+
});
|
|
12168
|
+
if (denied)
|
|
12169
|
+
return denied;
|
|
11491
12170
|
try {
|
|
11492
12171
|
return Response.json(await events.testChannel(channelId, {
|
|
11493
12172
|
source: "machines",
|
|
11494
12173
|
type,
|
|
11495
|
-
subject
|
|
11496
|
-
message
|
|
12174
|
+
subject,
|
|
12175
|
+
message,
|
|
12176
|
+
data
|
|
11497
12177
|
}));
|
|
11498
12178
|
} catch (error) {
|
|
11499
12179
|
return jsonError(error instanceof Error ? error.message : String(error));
|
|
@@ -11541,9 +12221,9 @@ function runSelfTest() {
|
|
|
11541
12221
|
|
|
11542
12222
|
// src/commands/clipboard.ts
|
|
11543
12223
|
init_paths();
|
|
11544
|
-
import { createHash } from "crypto";
|
|
11545
|
-
import { existsSync as
|
|
11546
|
-
import { join as
|
|
12224
|
+
import { createHash as createHash2 } from "crypto";
|
|
12225
|
+
import { existsSync as existsSync10, readFileSync as readFileSync8, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
|
|
12226
|
+
import { join as join8 } from "path";
|
|
11547
12227
|
var DEFAULT_CONFIG = {
|
|
11548
12228
|
version: 1,
|
|
11549
12229
|
enabled: true,
|
|
@@ -11561,7 +12241,7 @@ var DEFAULT_CONFIG = {
|
|
|
11561
12241
|
function resolveConfigPath(configPath) {
|
|
11562
12242
|
if (configPath)
|
|
11563
12243
|
return configPath;
|
|
11564
|
-
return
|
|
12244
|
+
return join8(getDataDir(), "clipboard-config.json");
|
|
11565
12245
|
}
|
|
11566
12246
|
function resolveHistoryPath(historyPath) {
|
|
11567
12247
|
if (historyPath)
|
|
@@ -11573,10 +12253,10 @@ function getDefaultConfig() {
|
|
|
11573
12253
|
}
|
|
11574
12254
|
function readConfig(configPath) {
|
|
11575
12255
|
const path = resolveConfigPath(configPath);
|
|
11576
|
-
if (!
|
|
12256
|
+
if (!existsSync10(path)) {
|
|
11577
12257
|
return getDefaultConfig();
|
|
11578
12258
|
}
|
|
11579
|
-
const parsed = JSON.parse(
|
|
12259
|
+
const parsed = JSON.parse(readFileSync8(path, "utf8"));
|
|
11580
12260
|
return { ...getDefaultConfig(), ...parsed };
|
|
11581
12261
|
}
|
|
11582
12262
|
function writeConfig(config, configPath) {
|
|
@@ -11587,11 +12267,11 @@ function writeConfig(config, configPath) {
|
|
|
11587
12267
|
}
|
|
11588
12268
|
function readHistory(historyPath) {
|
|
11589
12269
|
const path = resolveHistoryPath(historyPath);
|
|
11590
|
-
if (!
|
|
12270
|
+
if (!existsSync10(path)) {
|
|
11591
12271
|
return [];
|
|
11592
12272
|
}
|
|
11593
12273
|
try {
|
|
11594
|
-
return JSON.parse(
|
|
12274
|
+
return JSON.parse(readFileSync8(path, "utf8"));
|
|
11595
12275
|
} catch {
|
|
11596
12276
|
return [];
|
|
11597
12277
|
}
|
|
@@ -11603,7 +12283,7 @@ function writeHistory(entries, historyPath) {
|
|
|
11603
12283
|
`, "utf8");
|
|
11604
12284
|
}
|
|
11605
12285
|
function computeHash(content) {
|
|
11606
|
-
return
|
|
12286
|
+
return createHash2("sha256").update(content).digest("hex").slice(0, 16);
|
|
11607
12287
|
}
|
|
11608
12288
|
function shouldSkipContent(content, skipPatterns) {
|
|
11609
12289
|
const lower = content.toLowerCase();
|
|
@@ -11620,10 +12300,10 @@ function sanitizeClipboardForRead(content, maxSizeBytes, skipPatterns) {
|
|
|
11620
12300
|
}
|
|
11621
12301
|
function getOrCreateClipboardKey() {
|
|
11622
12302
|
const keyPath = getClipboardKeyPath();
|
|
11623
|
-
if (
|
|
11624
|
-
return
|
|
12303
|
+
if (existsSync10(keyPath)) {
|
|
12304
|
+
return readFileSync8(keyPath, "utf8").trim();
|
|
11625
12305
|
}
|
|
11626
|
-
const key =
|
|
12306
|
+
const key = createHash2("sha256").update(crypto.randomUUID()).digest("hex").slice(0, 32);
|
|
11627
12307
|
ensureParentDir(keyPath);
|
|
11628
12308
|
writeFileSync5(keyPath, `${key}
|
|
11629
12309
|
`, "utf8");
|
|
@@ -11660,8 +12340,8 @@ function addClipboardEntry(entry, historyPath) {
|
|
|
11660
12340
|
}
|
|
11661
12341
|
function clearClipboardHistory(historyPath) {
|
|
11662
12342
|
const path = resolveHistoryPath(historyPath);
|
|
11663
|
-
if (
|
|
11664
|
-
|
|
12343
|
+
if (existsSync10(path)) {
|
|
12344
|
+
rmSync2(path);
|
|
11665
12345
|
}
|
|
11666
12346
|
}
|
|
11667
12347
|
function getClipboardStatus(historyPath) {
|
|
@@ -11676,15 +12356,15 @@ function getClipboardStatus(historyPath) {
|
|
|
11676
12356
|
|
|
11677
12357
|
// src/commands/clipboard-daemon.ts
|
|
11678
12358
|
init_paths();
|
|
11679
|
-
import { readFileSync as
|
|
11680
|
-
import { join as
|
|
11681
|
-
import { createHash as
|
|
12359
|
+
import { readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "fs";
|
|
12360
|
+
import { join as join9 } from "path";
|
|
12361
|
+
import { createHash as createHash4 } from "crypto";
|
|
11682
12362
|
|
|
11683
12363
|
// src/commands/clipboard-server.ts
|
|
11684
12364
|
init_paths();
|
|
11685
12365
|
import { createServer } from "http";
|
|
11686
|
-
import { createHash as
|
|
11687
|
-
import { readFileSync as
|
|
12366
|
+
import { createHash as createHash3 } from "crypto";
|
|
12367
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
11688
12368
|
function readLocalClipboardSync() {
|
|
11689
12369
|
const platform5 = process.platform;
|
|
11690
12370
|
if (platform5 === "darwin") {
|
|
@@ -11730,7 +12410,7 @@ function hasCommand3(binary) {
|
|
|
11730
12410
|
function loadSharedSecret() {
|
|
11731
12411
|
const keyPath = getClipboardKeyPath();
|
|
11732
12412
|
try {
|
|
11733
|
-
return
|
|
12413
|
+
return readFileSync9(keyPath, "utf8").trim();
|
|
11734
12414
|
} catch {
|
|
11735
12415
|
return "";
|
|
11736
12416
|
}
|
|
@@ -11744,7 +12424,7 @@ function authenticate(request) {
|
|
|
11744
12424
|
const secret = loadSharedSecret();
|
|
11745
12425
|
if (!secret)
|
|
11746
12426
|
return false;
|
|
11747
|
-
return
|
|
12427
|
+
return createHash3("sha256").update(token).digest("hex") === createHash3("sha256").update(secret).digest("hex");
|
|
11748
12428
|
}
|
|
11749
12429
|
function jsonResponse(response, status, data) {
|
|
11750
12430
|
response.writeHead(status, { "content-type": "application/json" });
|
|
@@ -11784,7 +12464,7 @@ function startClipboardServer(options = {}) {
|
|
|
11784
12464
|
server,
|
|
11785
12465
|
port,
|
|
11786
12466
|
close: async () => {
|
|
11787
|
-
await new Promise((
|
|
12467
|
+
await new Promise((resolve4) => server.close(() => resolve4()));
|
|
11788
12468
|
}
|
|
11789
12469
|
};
|
|
11790
12470
|
}
|
|
@@ -11835,7 +12515,7 @@ function handleGetClipboard(response, config) {
|
|
|
11835
12515
|
}
|
|
11836
12516
|
|
|
11837
12517
|
// src/commands/clipboard-daemon.ts
|
|
11838
|
-
var DAEMON_PID_PATH =
|
|
12518
|
+
var DAEMON_PID_PATH = join9(getDataDir(), "clipboard-daemon.pid");
|
|
11839
12519
|
function readLocalClipboardSync2() {
|
|
11840
12520
|
const platform5 = process.platform;
|
|
11841
12521
|
if (platform5 === "darwin") {
|
|
@@ -11893,11 +12573,11 @@ function hasDisplayServer() {
|
|
|
11893
12573
|
return false;
|
|
11894
12574
|
}
|
|
11895
12575
|
function computeHash2(content) {
|
|
11896
|
-
return
|
|
12576
|
+
return createHash4("sha256").update(content).digest("hex").slice(0, 16);
|
|
11897
12577
|
}
|
|
11898
12578
|
function loadSharedSecret2() {
|
|
11899
12579
|
try {
|
|
11900
|
-
return
|
|
12580
|
+
return readFileSync10(getClipboardKeyPath(), "utf8").trim();
|
|
11901
12581
|
} catch {
|
|
11902
12582
|
return "";
|
|
11903
12583
|
}
|
|
@@ -11908,7 +12588,7 @@ function writePid(pid) {
|
|
|
11908
12588
|
}
|
|
11909
12589
|
function readPid() {
|
|
11910
12590
|
try {
|
|
11911
|
-
const pid = Number.parseInt(
|
|
12591
|
+
const pid = Number.parseInt(readFileSync10(DAEMON_PID_PATH, "utf8").trim());
|
|
11912
12592
|
return Number.isFinite(pid) ? pid : null;
|
|
11913
12593
|
} catch {
|
|
11914
12594
|
return null;
|
|
@@ -12009,8 +12689,8 @@ async function discoverPeers() {
|
|
|
12009
12689
|
|
|
12010
12690
|
// src/commands/heal.ts
|
|
12011
12691
|
init_paths();
|
|
12012
|
-
import { existsSync as
|
|
12013
|
-
import { join as
|
|
12692
|
+
import { existsSync as existsSync11, readFileSync as readFileSync11, writeFileSync as writeFileSync7 } from "fs";
|
|
12693
|
+
import { join as join10 } from "path";
|
|
12014
12694
|
var DEFAULT_THRESHOLDS = {
|
|
12015
12695
|
reconnect: 3,
|
|
12016
12696
|
nmRestart: 7,
|
|
@@ -12054,16 +12734,16 @@ function defaultHealState() {
|
|
|
12054
12734
|
};
|
|
12055
12735
|
}
|
|
12056
12736
|
function getHealConfigPath() {
|
|
12057
|
-
return process.env["HASNA_MACHINES_HEAL_CONFIG_PATH"] ||
|
|
12737
|
+
return process.env["HASNA_MACHINES_HEAL_CONFIG_PATH"] || join10(getDataDir(), "heal-config.json");
|
|
12058
12738
|
}
|
|
12059
12739
|
function getHealStatePath() {
|
|
12060
|
-
return process.env["HASNA_MACHINES_HEAL_STATE_PATH"] ||
|
|
12740
|
+
return process.env["HASNA_MACHINES_HEAL_STATE_PATH"] || join10(getDataDir(), "heal-state.json");
|
|
12061
12741
|
}
|
|
12062
12742
|
function readHealConfig(path) {
|
|
12063
12743
|
const p = path || getHealConfigPath();
|
|
12064
|
-
if (!
|
|
12744
|
+
if (!existsSync11(p))
|
|
12065
12745
|
return { ...DEFAULT_HEAL_CONFIG, thresholds: { ...DEFAULT_THRESHOLDS } };
|
|
12066
|
-
const parsed = JSON.parse(
|
|
12746
|
+
const parsed = JSON.parse(readFileSync11(p, "utf8"));
|
|
12067
12747
|
return {
|
|
12068
12748
|
...DEFAULT_HEAL_CONFIG,
|
|
12069
12749
|
...parsed,
|
|
@@ -12079,10 +12759,10 @@ function writeHealConfig(config, path) {
|
|
|
12079
12759
|
}
|
|
12080
12760
|
function readHealState(path) {
|
|
12081
12761
|
const p = path || getHealStatePath();
|
|
12082
|
-
if (!
|
|
12762
|
+
if (!existsSync11(p))
|
|
12083
12763
|
return defaultHealState();
|
|
12084
12764
|
try {
|
|
12085
|
-
return { ...defaultHealState(), ...JSON.parse(
|
|
12765
|
+
return { ...defaultHealState(), ...JSON.parse(readFileSync11(p, "utf8")) };
|
|
12086
12766
|
} catch {
|
|
12087
12767
|
return defaultHealState();
|
|
12088
12768
|
}
|
|
@@ -12209,7 +12889,7 @@ function sh(cmd, timeoutMs = 8000) {
|
|
|
12209
12889
|
}
|
|
12210
12890
|
function getCurrentBootId() {
|
|
12211
12891
|
try {
|
|
12212
|
-
return
|
|
12892
|
+
return readFileSync11("/proc/sys/kernel/random/boot_id", "utf8").trim();
|
|
12213
12893
|
} catch {
|
|
12214
12894
|
return "";
|
|
12215
12895
|
}
|
|
@@ -12295,9 +12975,9 @@ function executeAction(action, config) {
|
|
|
12295
12975
|
|
|
12296
12976
|
// src/commands/heal-daemon.ts
|
|
12297
12977
|
init_paths();
|
|
12298
|
-
import { existsSync as
|
|
12299
|
-
import { join as
|
|
12300
|
-
var DAEMON_PID_PATH2 =
|
|
12978
|
+
import { existsSync as existsSync12, readFileSync as readFileSync12, writeFileSync as writeFileSync8 } from "fs";
|
|
12979
|
+
import { join as join11 } from "path";
|
|
12980
|
+
var DAEMON_PID_PATH2 = join11(getDataDir(), "heal-daemon.pid");
|
|
12301
12981
|
var SERVICE_PATH = "/etc/systemd/system/machines-heal.service";
|
|
12302
12982
|
var SYSTEM_CONF = "/etc/systemd/system.conf";
|
|
12303
12983
|
function log(msg) {
|
|
@@ -12343,7 +13023,7 @@ function writePid2(pid) {
|
|
|
12343
13023
|
}
|
|
12344
13024
|
function readPid2() {
|
|
12345
13025
|
try {
|
|
12346
|
-
const pid = Number.parseInt(
|
|
13026
|
+
const pid = Number.parseInt(readFileSync12(DAEMON_PID_PATH2, "utf8").trim());
|
|
12347
13027
|
return Number.isFinite(pid) ? pid : null;
|
|
12348
13028
|
} catch {
|
|
12349
13029
|
return null;
|
|
@@ -12415,9 +13095,9 @@ function applyDeterminism(config) {
|
|
|
12415
13095
|
}
|
|
12416
13096
|
function enableHardwareWatchdog() {
|
|
12417
13097
|
const log2 = [];
|
|
12418
|
-
if (!
|
|
13098
|
+
if (!existsSync12(SYSTEM_CONF))
|
|
12419
13099
|
return ["/etc/systemd/system.conf not found; skipping hardware watchdog"];
|
|
12420
|
-
let conf =
|
|
13100
|
+
let conf = readFileSync12(SYSTEM_CONF, "utf8");
|
|
12421
13101
|
const set = (key, value) => {
|
|
12422
13102
|
const re = new RegExp(`^#?\\s*${key}=.*$`, "m");
|
|
12423
13103
|
if (re.test(conf))
|
|
@@ -12447,7 +13127,7 @@ function binPath() {
|
|
|
12447
13127
|
candidates.push(`${home}/.bun/bin/machines`);
|
|
12448
13128
|
candidates.push("/root/.bun/bin/machines", "/usr/local/bin/machines");
|
|
12449
13129
|
for (const c of candidates) {
|
|
12450
|
-
if (c &&
|
|
13130
|
+
if (c && existsSync12(c))
|
|
12451
13131
|
return c;
|
|
12452
13132
|
}
|
|
12453
13133
|
return "machines";
|
|
@@ -12483,7 +13163,7 @@ WantedBy=multi-user.target
|
|
|
12483
13163
|
function uninstallHealService() {
|
|
12484
13164
|
const log2 = [];
|
|
12485
13165
|
sh2("systemctl disable --now machines-heal.service 2>/dev/null || true");
|
|
12486
|
-
if (
|
|
13166
|
+
if (existsSync12(SERVICE_PATH)) {
|
|
12487
13167
|
sh2(`rm -f ${SERVICE_PATH}`);
|
|
12488
13168
|
sh2("systemctl daemon-reload");
|
|
12489
13169
|
log2.push(`removed ${SERVICE_PATH}`);
|
|
@@ -12494,7 +13174,7 @@ function uninstallHealService() {
|
|
|
12494
13174
|
}
|
|
12495
13175
|
function healServiceStatus() {
|
|
12496
13176
|
return {
|
|
12497
|
-
installed:
|
|
13177
|
+
installed: existsSync12(SERVICE_PATH),
|
|
12498
13178
|
active: sh2("systemctl is-active machines-heal.service").out === "active",
|
|
12499
13179
|
enabled: sh2("systemctl is-enabled machines-heal.service 2>/dev/null").out === "enabled"
|
|
12500
13180
|
};
|
|
@@ -12533,8 +13213,6 @@ ${items.map((item) => `- ${item}`).join(`
|
|
|
12533
13213
|
}
|
|
12534
13214
|
|
|
12535
13215
|
// src/cli/index.ts
|
|
12536
|
-
import { rmSync as rmSync2 } from "fs";
|
|
12537
|
-
import { readFileSync as readFileSync12 } from "fs";
|
|
12538
13216
|
var program2 = new Command;
|
|
12539
13217
|
function printJsonOrText(data, text, json = false) {
|
|
12540
13218
|
if (json || program2.opts().quiet) {
|
|
@@ -12802,23 +13480,307 @@ program2.name("machines").description("Machine fleet management CLI + MCP for de
|
|
|
12802
13480
|
var manifestCommand = program2.command("manifest").description("Manage the fleet manifest");
|
|
12803
13481
|
var appsCommand = program2.command("apps").description("Manage installed applications per machine");
|
|
12804
13482
|
var notificationsCommand = program2.command("notifications").description("Manage fleet alert delivery channels");
|
|
12805
|
-
var eventWebhooksCommand =
|
|
12806
|
-
|
|
12807
|
-
var webhookTestCommand = eventWebhooksCommand.commands.find((command2) => command2.name() === "test");
|
|
12808
|
-
var webhookOptions = webhookTestCommand?.options ?? [];
|
|
12809
|
-
var webhookMessageOption = webhookOptions.find((option) => option.long === "--message");
|
|
12810
|
-
if (webhookMessageOption) {
|
|
12811
|
-
webhookMessageOption.defaultValue = "Shared events test delivery";
|
|
12812
|
-
}
|
|
12813
|
-
var eventsCommand = registerEventCommands(program2, { source: "machines" });
|
|
12814
|
-
eventsCommand.description("Emit, list, and replay shared events");
|
|
13483
|
+
var eventWebhooksCommand = program2.command("webhooks").description("Manage shared event webhook subscriptions");
|
|
13484
|
+
var eventsCommand = program2.command("events").description("Emit, list, and replay shared events");
|
|
12815
13485
|
var runtimeCommand = program2.command("runtime").description("Watch runtime conditions and emit shared events");
|
|
12816
13486
|
var clipboardCommand = program2.command("clipboard").description("Real-time clipboard sync across fleet machines");
|
|
12817
13487
|
var installClaudeCommand = program2.command("install-claude").description("Install or inspect Claude, Codex, and Gemini CLIs");
|
|
12818
13488
|
var daemonCommand = program2.command("daemon").description("Install and inspect the machines-agent fleet daemon service");
|
|
13489
|
+
var trustedNotificationApproval2 = createTrustedNotificationApproval();
|
|
13490
|
+
function cliMachineId(machineId) {
|
|
13491
|
+
return machineId?.trim() || "local";
|
|
13492
|
+
}
|
|
13493
|
+
function cliResourceId(kind, ...parts) {
|
|
13494
|
+
const values = parts.map((part) => String(part ?? "*").trim()).filter(Boolean).join(":");
|
|
13495
|
+
return values ? `${kind}:${values}` : kind;
|
|
13496
|
+
}
|
|
13497
|
+
function cliMutationCallerId() {
|
|
13498
|
+
return process.env["HASNA_MACHINES_MUTATION_CALLER_ID"]?.trim() || "cli";
|
|
13499
|
+
}
|
|
13500
|
+
function cliMutationRunId() {
|
|
13501
|
+
return process.env["HASNA_MACHINES_MUTATION_RUN_ID"]?.trim() || "cli";
|
|
13502
|
+
}
|
|
13503
|
+
function requireCliMutation(operation, approvalToken, scope = {}) {
|
|
13504
|
+
assertMutationApproved({
|
|
13505
|
+
surface: "cli",
|
|
13506
|
+
operation,
|
|
13507
|
+
transport: "cli",
|
|
13508
|
+
callerId: cliMutationCallerId(),
|
|
13509
|
+
runId: cliMutationRunId(),
|
|
13510
|
+
machineId: scope.machineId === undefined ? undefined : cliMachineId(scope.machineId),
|
|
13511
|
+
resourceId: scope.resourceId === undefined || scope.resourceId === null ? undefined : scope.resourceId,
|
|
13512
|
+
args: scope.args,
|
|
13513
|
+
approvalToken
|
|
13514
|
+
});
|
|
13515
|
+
}
|
|
13516
|
+
function cliPlanApprovalArgs(args, plan) {
|
|
13517
|
+
return {
|
|
13518
|
+
...args,
|
|
13519
|
+
plan_digest: mutationPlanDigest(plan)
|
|
13520
|
+
};
|
|
13521
|
+
}
|
|
13522
|
+
function cliPlanResourceId(operation, machineId, plan) {
|
|
13523
|
+
return cliResourceId("plan", operation, machineId, mutationPlanDigest(plan));
|
|
13524
|
+
}
|
|
13525
|
+
function createEventsClient() {
|
|
13526
|
+
return new EventsClient3;
|
|
13527
|
+
}
|
|
13528
|
+
function eventStoreDir2() {
|
|
13529
|
+
return resolve4(getEventsDataDir2());
|
|
13530
|
+
}
|
|
13531
|
+
function eventStoreScope2() {
|
|
13532
|
+
return { event_store_dir: eventStoreDir2() };
|
|
13533
|
+
}
|
|
13534
|
+
function eventStoreResourceId2(kind, ...parts) {
|
|
13535
|
+
return cliResourceId(kind, mutationArgsSha256(eventStoreScope2()), ...parts);
|
|
13536
|
+
}
|
|
13537
|
+
function withEventStoreScope2(args) {
|
|
13538
|
+
return { event_store_dir: eventStoreDir2(), ...args };
|
|
13539
|
+
}
|
|
13540
|
+
function readJsonArrayFile(path) {
|
|
13541
|
+
if (!existsSync13(path))
|
|
13542
|
+
return [];
|
|
13543
|
+
const raw = readFileSync13(path, "utf8").trim();
|
|
13544
|
+
if (!raw)
|
|
13545
|
+
return [];
|
|
13546
|
+
const parsed = JSON.parse(raw);
|
|
13547
|
+
if (!Array.isArray(parsed))
|
|
13548
|
+
throw new Error(`Expected ${path} to contain a JSON array.`);
|
|
13549
|
+
return parsed;
|
|
13550
|
+
}
|
|
13551
|
+
function readEventChannelsWithoutInit() {
|
|
13552
|
+
return readJsonArrayFile(join12(eventStoreDir2(), "channels.json"));
|
|
13553
|
+
}
|
|
13554
|
+
function readEventsWithoutInit() {
|
|
13555
|
+
return readJsonArrayFile(join12(eventStoreDir2(), "events.json"));
|
|
13556
|
+
}
|
|
13557
|
+
function filterEventsForReplay(events, options) {
|
|
13558
|
+
return events.filter((event) => {
|
|
13559
|
+
if (options.id && event.id !== options.id)
|
|
13560
|
+
return false;
|
|
13561
|
+
if (options.source && event.source !== options.source)
|
|
13562
|
+
return false;
|
|
13563
|
+
if (options.type && event.type !== options.type)
|
|
13564
|
+
return false;
|
|
13565
|
+
return true;
|
|
13566
|
+
});
|
|
13567
|
+
}
|
|
13568
|
+
function collectOptionValues(value, previous = []) {
|
|
13569
|
+
previous.push(value);
|
|
13570
|
+
return previous;
|
|
13571
|
+
}
|
|
13572
|
+
function parseNumberOption(value) {
|
|
13573
|
+
const parsed = Number(value);
|
|
13574
|
+
if (!Number.isFinite(parsed))
|
|
13575
|
+
throw new Error(`Expected a finite number, got ${value}`);
|
|
13576
|
+
return parsed;
|
|
13577
|
+
}
|
|
13578
|
+
function parseJsonObjectOption(value, fallback) {
|
|
13579
|
+
if (value === undefined)
|
|
13580
|
+
return fallback;
|
|
13581
|
+
const parsed = JSON.parse(value);
|
|
13582
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
13583
|
+
throw new Error("Expected a JSON object.");
|
|
13584
|
+
}
|
|
13585
|
+
return parsed;
|
|
13586
|
+
}
|
|
13587
|
+
function parseHeaderOptions(values) {
|
|
13588
|
+
if (!values?.length)
|
|
13589
|
+
return;
|
|
13590
|
+
const headers = {};
|
|
13591
|
+
for (const value of values) {
|
|
13592
|
+
const separator = value.indexOf("=");
|
|
13593
|
+
if (separator === -1)
|
|
13594
|
+
throw new Error(`Invalid header, expected name=value: ${value}`);
|
|
13595
|
+
headers[value.slice(0, separator)] = value.slice(separator + 1);
|
|
13596
|
+
}
|
|
13597
|
+
return headers;
|
|
13598
|
+
}
|
|
13599
|
+
function buildEventFilter(options) {
|
|
13600
|
+
const filter = {};
|
|
13601
|
+
if (options.source)
|
|
13602
|
+
filter.source = options.source;
|
|
13603
|
+
if (options.type)
|
|
13604
|
+
filter.type = options.type;
|
|
13605
|
+
if (options.subject)
|
|
13606
|
+
filter.subject = options.subject;
|
|
13607
|
+
if (options.severity)
|
|
13608
|
+
filter.severity = options.severity;
|
|
13609
|
+
return Object.keys(filter).length > 0 ? [filter] : undefined;
|
|
13610
|
+
}
|
|
13611
|
+
function wantsCommandJson(options, command2) {
|
|
13612
|
+
return Boolean(options.json || command2.optsWithGlobals?.().json || command2.parent?.optsWithGlobals?.().json || program2.opts().quiet);
|
|
13613
|
+
}
|
|
13614
|
+
function printCommandResult(data, text, json) {
|
|
13615
|
+
if (json || program2.opts().quiet) {
|
|
13616
|
+
console.log(JSON.stringify(data, null, 2));
|
|
13617
|
+
return;
|
|
13618
|
+
}
|
|
13619
|
+
console.log(text);
|
|
13620
|
+
}
|
|
13621
|
+
function runtimeTmuxCommand() {
|
|
13622
|
+
return process.env["HASNA_MACHINES_TMUX_BIN"]?.trim() || "tmux";
|
|
13623
|
+
}
|
|
13624
|
+
function runtimeTmuxEventTypes(once) {
|
|
13625
|
+
return once ? ["machines.tmux.pane_missing"] : ["machines.tmux.pane_died"];
|
|
13626
|
+
}
|
|
13627
|
+
eventWebhooksCommand.command("add").description("Add or replace a webhook or command subscription").argument("<target>", "Webhook URL or command binary").requiredOption("--id <id>", "Subscription/channel identifier").option("--transport <kind>", "Transport kind: webhook or command", "webhook").option("--name <name>", "Display name").option("--type <pattern>", "Event type filter, e.g. todos.task.*").option("--source <pattern>", "Event source filter").option("--subject <pattern>", "Event subject filter").option("--severity <pattern>", "Event severity filter").option("--secret <secret>", "Webhook HMAC secret").option("--header <name=value...>", "Webhook header", collectOptionValues, []).option("--arg <arg...>", "Command argument", collectOptionValues, []).option("--timeout-ms <ms>", "Transport timeout in milliseconds", parseNumberOption).option("--retry-attempts <n>", "Maximum delivery attempts", parseNumberOption).option("--retry-backoff-ms <ms>", "Initial retry backoff in milliseconds", parseNumberOption).option("--redact <path...>", "Event field path to redact before delivery", collectOptionValues, []).option("--disabled", "Create channel disabled", false).option("--approval-token <token>", "Scoped mutation approval token").option("-j, --json", "Print JSON output", false).action(async (target, options, command2) => {
|
|
13628
|
+
const headers = parseHeaderOptions(options.header);
|
|
13629
|
+
const commandArgs = options.arg ?? [];
|
|
13630
|
+
const redactPaths = options.redact ?? [];
|
|
13631
|
+
const enabled = !options.disabled;
|
|
13632
|
+
const filter = buildEventFilter(options);
|
|
13633
|
+
const channel = {
|
|
13634
|
+
id: options.id,
|
|
13635
|
+
name: options.name,
|
|
13636
|
+
enabled,
|
|
13637
|
+
transport: options.transport,
|
|
13638
|
+
filters: filter,
|
|
13639
|
+
retry: options.retryAttempts || options.retryBackoffMs ? { maxAttempts: options.retryAttempts, backoffMs: options.retryBackoffMs } : undefined,
|
|
13640
|
+
redact: redactPaths.length > 0 ? { paths: redactPaths } : undefined
|
|
13641
|
+
};
|
|
13642
|
+
if (options.transport === "webhook") {
|
|
13643
|
+
channel.webhook = { url: target, secret: options.secret, headers, timeoutMs: options.timeoutMs };
|
|
13644
|
+
} else if (options.transport === "command") {
|
|
13645
|
+
channel.command = { command: target, args: commandArgs, timeoutMs: options.timeoutMs };
|
|
13646
|
+
} else {
|
|
13647
|
+
throw new Error(`Transport ${options.transport} is reserved for future use and cannot be added yet`);
|
|
13648
|
+
}
|
|
13649
|
+
requireCliMutation("machines_webhooks_add", options.approvalToken, {
|
|
13650
|
+
resourceId: eventStoreResourceId2("webhook", options.id),
|
|
13651
|
+
args: withEventStoreScope2({
|
|
13652
|
+
channel_id: options.id,
|
|
13653
|
+
target,
|
|
13654
|
+
transport: options.transport,
|
|
13655
|
+
name: options.name,
|
|
13656
|
+
event_type: options.type,
|
|
13657
|
+
source: options.source,
|
|
13658
|
+
subject: options.subject,
|
|
13659
|
+
severity: options.severity,
|
|
13660
|
+
secret: options.secret,
|
|
13661
|
+
headers,
|
|
13662
|
+
args: commandArgs,
|
|
13663
|
+
timeout_ms: options.timeoutMs,
|
|
13664
|
+
retry_attempts: options.retryAttempts,
|
|
13665
|
+
retry_backoff_ms: options.retryBackoffMs,
|
|
13666
|
+
redact: redactPaths,
|
|
13667
|
+
enabled
|
|
13668
|
+
})
|
|
13669
|
+
});
|
|
13670
|
+
const saved = await createEventsClient().addChannel(channel);
|
|
13671
|
+
printCommandResult(sanitizeChannelForOutput(saved), `Added ${saved.transport} channel ${saved.id}`, wantsCommandJson(options, command2));
|
|
13672
|
+
});
|
|
13673
|
+
eventWebhooksCommand.command("list").description("List configured subscriptions").option("-j, --json", "Print JSON output", false).action(async (options, command2) => {
|
|
13674
|
+
const channels = readEventChannelsWithoutInit();
|
|
13675
|
+
if (wantsCommandJson(options, command2)) {
|
|
13676
|
+
console.log(JSON.stringify(sanitizeChannelsForOutput2(channels), null, 2));
|
|
13677
|
+
return;
|
|
13678
|
+
}
|
|
13679
|
+
if (!channels.length) {
|
|
13680
|
+
console.log("No channels configured.");
|
|
13681
|
+
return;
|
|
13682
|
+
}
|
|
13683
|
+
for (const channel of channels) {
|
|
13684
|
+
console.log(`${channel.id} ${channel.enabled ? "enabled" : "disabled"} ${channel.transport} ${channel.webhook?.url ?? channel.command?.command ?? channel.transport}`);
|
|
13685
|
+
}
|
|
13686
|
+
});
|
|
13687
|
+
eventWebhooksCommand.command("remove").description("Remove a subscription").argument("<id>", "Subscription/channel identifier").option("--approval-token <token>", "Scoped mutation approval token").option("-j, --json", "Print JSON output", false).action(async (id, options, command2) => {
|
|
13688
|
+
requireCliMutation("machines_webhooks_remove", options.approvalToken, {
|
|
13689
|
+
resourceId: eventStoreResourceId2("webhook", id),
|
|
13690
|
+
args: withEventStoreScope2({ channel_id: id })
|
|
13691
|
+
});
|
|
13692
|
+
const removed = await createEventsClient().removeChannel(id);
|
|
13693
|
+
printCommandResult({ removed }, removed ? `Removed ${id}` : `Channel not found: ${id}`, wantsCommandJson(options, command2));
|
|
13694
|
+
});
|
|
13695
|
+
eventWebhooksCommand.command("test").description("Send a test event to one subscription").argument("<id>", "Subscription/channel identifier").option("--type <type>", "Event type", "events.test").option("--subject <subject>", "Event subject").option("--message <message>", "Event message", "Shared events test delivery").option("--data <json>", "Event data JSON object").option("--approval-token <token>", "Scoped mutation approval token").option("-j, --json", "Print JSON output", false).action(async (id, options, command2) => {
|
|
13696
|
+
const data = parseJsonObjectOption(options.data, { test: true });
|
|
13697
|
+
const subject = options.subject ?? id;
|
|
13698
|
+
requireCliMutation("machines_webhooks_test", options.approvalToken, {
|
|
13699
|
+
resourceId: eventStoreResourceId2("webhook-test", id, options.type),
|
|
13700
|
+
args: withEventStoreScope2({ channel_id: id, event_type: options.type, subject, message: options.message, data })
|
|
13701
|
+
});
|
|
13702
|
+
const result = await createEventsClient().testChannel(id, {
|
|
13703
|
+
source: "machines",
|
|
13704
|
+
type: options.type,
|
|
13705
|
+
subject,
|
|
13706
|
+
message: options.message,
|
|
13707
|
+
data
|
|
13708
|
+
});
|
|
13709
|
+
printCommandResult(result, `${result.status}: ${result.channelId}`, wantsCommandJson(options, command2));
|
|
13710
|
+
});
|
|
13711
|
+
eventsCommand.command("emit").description("Emit an event from this app").argument("<type>", "Event type").option("--source <source>", "Event source override").option("--subject <subject>", "Event subject").option("--severity <severity>", "Event severity", "info").option("--message <message>", "Event message").option("--dedupe-key <key>", "Dedupe key").option("--data <json>", "Event data JSON object").option("--metadata <json>", "Event metadata JSON object").option("--no-deliver", "Record without delivering").option("--no-dedupe", "Allow duplicate id/dedupeKey events").option("--approval-token <token>", "Scoped mutation approval token").option("-j, --json", "Print JSON output", false).action(async (type, options, command2) => {
|
|
13712
|
+
const source = options.source ?? "machines";
|
|
13713
|
+
const data = parseJsonObjectOption(options.data, {});
|
|
13714
|
+
const metadata = parseJsonObjectOption(options.metadata, {});
|
|
13715
|
+
requireCliMutation("machines_events_emit", options.approvalToken, {
|
|
13716
|
+
resourceId: eventStoreResourceId2("event", type, options.subject, options.dedupeKey),
|
|
13717
|
+
args: withEventStoreScope2({
|
|
13718
|
+
event_type: type,
|
|
13719
|
+
source,
|
|
13720
|
+
subject: options.subject,
|
|
13721
|
+
severity: options.severity,
|
|
13722
|
+
message: options.message,
|
|
13723
|
+
data,
|
|
13724
|
+
metadata,
|
|
13725
|
+
dedupe_key: options.dedupeKey,
|
|
13726
|
+
deliver: options.deliver,
|
|
13727
|
+
dedupe: options.dedupe
|
|
13728
|
+
})
|
|
13729
|
+
});
|
|
13730
|
+
const result = await createEventsClient().emit({
|
|
13731
|
+
source,
|
|
13732
|
+
type,
|
|
13733
|
+
subject: options.subject,
|
|
13734
|
+
severity: options.severity,
|
|
13735
|
+
message: options.message,
|
|
13736
|
+
dedupeKey: options.dedupeKey,
|
|
13737
|
+
data,
|
|
13738
|
+
metadata
|
|
13739
|
+
}, { deliver: options.deliver, dedupe: options.dedupe });
|
|
13740
|
+
printCommandResult(result, `${result.deduped ? "Deduped" : "Emitted"} ${result.event.id} to ${result.deliveries.length} channel(s)`, wantsCommandJson(options, command2));
|
|
13741
|
+
});
|
|
13742
|
+
eventsCommand.command("list").description("List recorded events").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--limit <n>", "Limit results", parseNumberOption).option("-j, --json", "Print JSON output", false).action(async (options, command2) => {
|
|
13743
|
+
let rows = readEventsWithoutInit();
|
|
13744
|
+
if (options.source)
|
|
13745
|
+
rows = rows.filter((event) => event.source === options.source);
|
|
13746
|
+
if (options.type)
|
|
13747
|
+
rows = rows.filter((event) => event.type === options.type);
|
|
13748
|
+
if (options.limit)
|
|
13749
|
+
rows = rows.slice(-options.limit);
|
|
13750
|
+
if (wantsCommandJson(options, command2)) {
|
|
13751
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
13752
|
+
return;
|
|
13753
|
+
}
|
|
13754
|
+
if (!rows.length) {
|
|
13755
|
+
console.log("No events recorded.");
|
|
13756
|
+
return;
|
|
13757
|
+
}
|
|
13758
|
+
for (const event of rows) {
|
|
13759
|
+
console.log(`${event.time} ${event.id} ${event.source} ${event.type} ${event.severity}`);
|
|
13760
|
+
}
|
|
13761
|
+
});
|
|
13762
|
+
eventsCommand.command("replay").description("Replay recorded events").option("--id <id>", "Replay one event id").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--dry-run", "Preview without delivery", false).option("--approval-token <token>", "Scoped mutation approval token").option("-j, --json", "Print JSON output", false).action(async (options, command2) => {
|
|
13763
|
+
if (options.dryRun !== true) {
|
|
13764
|
+
requireCliMutation("machines_events_replay", options.approvalToken, {
|
|
13765
|
+
resourceId: eventStoreResourceId2("event-replay", options.id, options.source, options.type),
|
|
13766
|
+
args: withEventStoreScope2({ event_id: options.id, source: options.source, event_type: options.type, dry_run: false })
|
|
13767
|
+
});
|
|
13768
|
+
}
|
|
13769
|
+
const result = options.dryRun === true ? { events: filterEventsForReplay(readEventsWithoutInit(), options), deliveries: [] } : await createEventsClient().replay({
|
|
13770
|
+
eventId: options.id,
|
|
13771
|
+
source: options.source,
|
|
13772
|
+
type: options.type,
|
|
13773
|
+
dryRun: options.dryRun
|
|
13774
|
+
});
|
|
13775
|
+
printCommandResult(result, `Replayed ${result.events.length} event(s), ${result.deliveries.length} delivery result(s)`, wantsCommandJson(options, command2));
|
|
13776
|
+
});
|
|
12819
13777
|
function addDaemonLifecycleCommand(action, description) {
|
|
12820
|
-
daemonCommand.command(action).description(description).option("--platform <platform>", "Service platform to plan for (macos, linux)").option("--mode <mode>", "Service mode (user, system)", "user").option("--service-name <name>", "Service name/label", "machines-agent").option("--executable <path>", "Absolute machines-agent executable path").option("--interval-ms <ms>", "Heartbeat interval in milliseconds").option("--storage-push", "Configure daemon to push heartbeat rows to storage", false).option("--doctor-summary", "Configure daemon to include lightweight doctor summaries", false).option("--private-metadata", "Opt in to private host/network metadata in heartbeat rows", false).option("--env <name...>", "Environment variable names to include as placeholders").option("--apply", "Write service files and run planned commands", false).option("--yes", "Confirm execution when using --apply", false).option("-j, --json", "Print JSON output", false).action((options) => {
|
|
12821
|
-
const
|
|
13778
|
+
daemonCommand.command(action).description(description).option("--platform <platform>", "Service platform to plan for (macos, linux)").option("--mode <mode>", "Service mode (user, system)", "user").option("--service-name <name>", "Service name/label", "machines-agent").option("--executable <path>", "Absolute machines-agent executable path").option("--interval-ms <ms>", "Heartbeat interval in milliseconds").option("--storage-push", "Configure daemon to push heartbeat rows to storage", false).option("--doctor-summary", "Configure daemon to include lightweight doctor summaries", false).option("--private-metadata", "Opt in to private host/network metadata in heartbeat rows", false).option("--env <name...>", "Environment variable names to include as placeholders").option("--apply", "Write service files and run planned commands", false).option("--yes", "Confirm execution when using --apply", false).option("--approval-token <token>", "Scoped mutation approval token").option("-j, --json", "Print JSON output", false).action((options) => {
|
|
13779
|
+
const planOptions = parseDaemonOptions(action, options);
|
|
13780
|
+
const plan = buildDaemonServicePlan(planOptions);
|
|
13781
|
+
if (options.apply) {
|
|
13782
|
+
requireCliMutation(`daemon_${action}`, options.approvalToken, { resourceId: cliResourceId("daemon", action, options.serviceName), args: planOptions });
|
|
13783
|
+
}
|
|
12822
13784
|
const result = runDaemonServicePlan(plan, { apply: options.apply, yes: options.yes });
|
|
12823
13785
|
if (options.json || options.apply) {
|
|
12824
13786
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -12832,7 +13794,8 @@ addDaemonLifecycleCommand("uninstall", "Plan or uninstall the machines-agent dae
|
|
|
12832
13794
|
addDaemonLifecycleCommand("restart", "Plan or restart the machines-agent daemon service");
|
|
12833
13795
|
addDaemonLifecycleCommand("status", "Plan a daemon service status command");
|
|
12834
13796
|
addDaemonLifecycleCommand("logs", "Plan a daemon service log command");
|
|
12835
|
-
manifestCommand.command("init").description("Create an empty fleet manifest").action(() => {
|
|
13797
|
+
manifestCommand.command("init").description("Create an empty fleet manifest").option("--approval-token <token>", "Scoped mutation approval token").action((options) => {
|
|
13798
|
+
requireCliMutation("manifest_init", options.approvalToken, { resourceId: "manifest:init", args: {} });
|
|
12836
13799
|
console.log(manifestInit());
|
|
12837
13800
|
});
|
|
12838
13801
|
manifestCommand.command("path").description("Print the manifest path").action(() => {
|
|
@@ -12844,7 +13807,8 @@ manifestCommand.command("list").description("Print the fleet manifest").action((
|
|
|
12844
13807
|
manifestCommand.command("validate").description("Validate the fleet manifest").action(() => {
|
|
12845
13808
|
console.log(JSON.stringify(manifestValidate(), null, 2));
|
|
12846
13809
|
});
|
|
12847
|
-
manifestCommand.command("bootstrap").description("Detect and upsert the current machine into the manifest").action(() => {
|
|
13810
|
+
manifestCommand.command("bootstrap").description("Detect and upsert the current machine into the manifest").option("--approval-token <token>", "Scoped mutation approval token").action((options) => {
|
|
13811
|
+
requireCliMutation("manifest_bootstrap", options.approvalToken, { resourceId: "manifest:bootstrap", args: {} });
|
|
12848
13812
|
console.log(JSON.stringify(manifestBootstrapCurrentMachine(), null, 2));
|
|
12849
13813
|
});
|
|
12850
13814
|
manifestCommand.command("get").description("Print a single machine from the manifest").argument("<id>", "Machine identifier").action((id) => {
|
|
@@ -12856,18 +13820,20 @@ manifestCommand.command("get").description("Print a single machine from the mani
|
|
|
12856
13820
|
}
|
|
12857
13821
|
console.log(JSON.stringify(machine, null, 2));
|
|
12858
13822
|
});
|
|
12859
|
-
manifestCommand.command("remove").description("Remove a machine from the manifest").argument("<id>", "Machine identifier").action((id) => {
|
|
13823
|
+
manifestCommand.command("remove").description("Remove a machine from the manifest").argument("<id>", "Machine identifier").option("--approval-token <token>", "Scoped mutation approval token").action((id, options) => {
|
|
13824
|
+
requireCliMutation("manifest_remove", options.approvalToken, { machineId: id, args: { machine_id: id } });
|
|
12860
13825
|
console.log(JSON.stringify(manifestRemove(id), null, 2));
|
|
12861
13826
|
});
|
|
12862
|
-
manifestCommand.command("add").description("Add or replace a machine in the fleet manifest").option("--id <id>", "Machine identifier").option("--platform <platform>", "linux | macos | windows").option("--workspace-path <path>", "Primary workspace path").option("--hostname <hostname>", "Machine hostname").option("--ssh-address <sshAddress>", "Machine SSH address").option("--tailscale-name <tailscaleName>", "Machine Tailscale DNS name").option("--connection <connection>", "local | ssh | tailscale").option("--bun-path <path>", "Bun executable directory").option("--tag <tag...>", "Machine tags").option("--package <name...>", "Desired packages").option("--app <spec...>", "Desired apps as name[:manager[:packageName]]").option("--file <spec...>", "File sync spec source:target[:copy|symlink]").option("--metadata <json>", "Machine metadata as JSON").option("--from-stdin", "Read the full MachineManifest JSON from stdin").action((options) => {
|
|
13827
|
+
manifestCommand.command("add").description("Add or replace a machine in the fleet manifest").option("--id <id>", "Machine identifier").option("--platform <platform>", "linux | macos | windows").option("--workspace-path <path>", "Primary workspace path").option("--hostname <hostname>", "Machine hostname").option("--ssh-address <sshAddress>", "Machine SSH address").option("--tailscale-name <tailscaleName>", "Machine Tailscale DNS name").option("--connection <connection>", "local | ssh | tailscale").option("--bun-path <path>", "Bun executable directory").option("--tag <tag...>", "Machine tags").option("--package <name...>", "Desired packages").option("--app <spec...>", "Desired apps as name[:manager[:packageName]]").option("--file <spec...>", "File sync spec source:target[:copy|symlink]").option("--metadata <json>", "Machine metadata as JSON").option("--from-stdin", "Read the full MachineManifest JSON from stdin").option("--approval-token <token>", "Scoped mutation approval token").action((options) => {
|
|
12863
13828
|
const fromStdin = Boolean(options["fromStdin"] || options["from-stdin"]);
|
|
12864
13829
|
if (fromStdin) {
|
|
12865
13830
|
if (process.stdin.isTTY) {
|
|
12866
13831
|
console.error("error: --from-stdin requires piped input");
|
|
12867
13832
|
process.exit(1);
|
|
12868
13833
|
}
|
|
12869
|
-
const input =
|
|
13834
|
+
const input = readFileSync13(0, "utf8");
|
|
12870
13835
|
const machine2 = JSON.parse(input);
|
|
13836
|
+
requireCliMutation("manifest_add", typeof options["approvalToken"] === "string" ? options["approvalToken"] : undefined, { machineId: machine2.id, args: machine2 });
|
|
12871
13837
|
console.log(JSON.stringify(manifestAdd(machine2), null, 2));
|
|
12872
13838
|
return;
|
|
12873
13839
|
}
|
|
@@ -12906,6 +13872,7 @@ manifestCommand.command("add").description("Add or replace a machine in the flee
|
|
|
12906
13872
|
apps,
|
|
12907
13873
|
files
|
|
12908
13874
|
};
|
|
13875
|
+
requireCliMutation("manifest_add", typeof options["approvalToken"] === "string" ? options["approvalToken"] : undefined, { machineId: machine.id, args: machine });
|
|
12909
13876
|
console.log(JSON.stringify(manifestAdd(machine), null, 2));
|
|
12910
13877
|
});
|
|
12911
13878
|
appsCommand.command("list").description("List manifest-managed apps for a machine").option("--machine <id>", "Machine identifier").option("-j, --json", "Print JSON output", false).action((options) => {
|
|
@@ -12924,16 +13891,47 @@ appsCommand.command("plan").description("Preview app install steps for a machine
|
|
|
12924
13891
|
const result = buildAppsPlan(options.machine);
|
|
12925
13892
|
console.log(JSON.stringify(result, null, 2));
|
|
12926
13893
|
});
|
|
12927
|
-
appsCommand.command("apply").description("Install manifest-managed apps for a machine").option("--machine <id>", "Machine identifier").option("--yes", "Confirm execution", false).action((options) => {
|
|
12928
|
-
const
|
|
13894
|
+
appsCommand.command("apply").description("Install manifest-managed apps for a machine").option("--machine <id>", "Machine identifier").option("--yes", "Confirm execution", false).option("--approval-token <token>", "Scoped mutation approval token").action((options) => {
|
|
13895
|
+
const resolvedMachineId = cliMachineId(options.machine);
|
|
13896
|
+
const plan = buildAppsPlan(options.machine);
|
|
13897
|
+
requireCliMutation("apps_apply", options.approvalToken, {
|
|
13898
|
+
machineId: resolvedMachineId,
|
|
13899
|
+
resourceId: cliPlanResourceId("apps_apply", resolvedMachineId, plan),
|
|
13900
|
+
args: cliPlanApprovalArgs({ machine_id: resolvedMachineId, yes: options.yes }, plan)
|
|
13901
|
+
});
|
|
13902
|
+
const result = runAppsPlan(plan, { apply: true, yes: options.yes });
|
|
12929
13903
|
console.log(JSON.stringify(result, null, 2));
|
|
12930
13904
|
});
|
|
12931
|
-
program2.command("setup").description("Prepare a machine from the fleet manifest").option("--machine <id>", "Machine identifier").option("--apply", "Execute provisioning commands instead of previewing the plan", false).option("--yes", "Confirm execution when using --apply", false).option("-j, --json", "Print JSON output", false).action((options) => {
|
|
12932
|
-
|
|
13905
|
+
program2.command("setup").description("Prepare a machine from the fleet manifest").option("--machine <id>", "Machine identifier").option("--apply", "Execute provisioning commands instead of previewing the plan", false).option("--yes", "Confirm execution when using --apply", false).option("--approval-token <token>", "Scoped mutation approval token").option("-j, --json", "Print JSON output", false).action((options) => {
|
|
13906
|
+
if (options.apply) {
|
|
13907
|
+
const resolvedMachineId = cliMachineId(options.machine);
|
|
13908
|
+
const plan = buildSetupPlan(options.machine);
|
|
13909
|
+
requireCliMutation("setup_apply", options.approvalToken, {
|
|
13910
|
+
machineId: resolvedMachineId,
|
|
13911
|
+
resourceId: cliPlanResourceId("setup_apply", resolvedMachineId, plan),
|
|
13912
|
+
args: cliPlanApprovalArgs({ machine_id: resolvedMachineId, yes: options.yes }, plan)
|
|
13913
|
+
});
|
|
13914
|
+
const result2 = runSetupPlan(plan, { apply: true, yes: options.yes });
|
|
13915
|
+
console.log(JSON.stringify(result2, null, 2));
|
|
13916
|
+
return;
|
|
13917
|
+
}
|
|
13918
|
+
const result = buildSetupPlan(options.machine);
|
|
12933
13919
|
console.log(JSON.stringify(result, null, 2));
|
|
12934
13920
|
});
|
|
12935
|
-
program2.command("sync").description("Reconcile a machine against the fleet manifest").option("--machine <id>", "Machine identifier").option("--apply", "Execute reconciliation commands instead of previewing the plan", false).option("--yes", "Confirm execution when using --apply", false).option("-j, --json", "Print JSON output", false).action((options) => {
|
|
12936
|
-
|
|
13921
|
+
program2.command("sync").description("Reconcile a machine against the fleet manifest").option("--machine <id>", "Machine identifier").option("--apply", "Execute reconciliation commands instead of previewing the plan", false).option("--yes", "Confirm execution when using --apply", false).option("--approval-token <token>", "Scoped mutation approval token").option("-j, --json", "Print JSON output", false).action((options) => {
|
|
13922
|
+
if (options.apply) {
|
|
13923
|
+
const resolvedMachineId = cliMachineId(options.machine);
|
|
13924
|
+
const plan = buildSyncPlan(options.machine);
|
|
13925
|
+
requireCliMutation("sync_apply", options.approvalToken, {
|
|
13926
|
+
machineId: resolvedMachineId,
|
|
13927
|
+
resourceId: cliPlanResourceId("sync_apply", resolvedMachineId, plan),
|
|
13928
|
+
args: cliPlanApprovalArgs({ machine_id: resolvedMachineId, yes: options.yes }, plan)
|
|
13929
|
+
});
|
|
13930
|
+
const result2 = runSyncPlan(plan, { apply: true, yes: options.yes });
|
|
13931
|
+
console.log(JSON.stringify(result2, null, 2));
|
|
13932
|
+
return;
|
|
13933
|
+
}
|
|
13934
|
+
const result = buildSyncPlan(options.machine);
|
|
12937
13935
|
console.log(JSON.stringify(result, null, 2));
|
|
12938
13936
|
});
|
|
12939
13937
|
program2.command("topology").description("Discover local, manifest, heartbeat, SSH, and Tailscale machine topology").option("--no-tailscale", "Skip tailscale status probing").option("--private-metadata", "Print private host/network route fields", false).option("-j, --json", "Print JSON output", false).action((options) => {
|
|
@@ -12999,7 +13997,19 @@ workspaceCommand.command("doctor").description("Diagnose repo and open-files wor
|
|
|
12999
13997
|
if (result.diagnostics.some((entry) => entry.severity === "fail") && !options.json)
|
|
13000
13998
|
process.exitCode = 1;
|
|
13001
13999
|
});
|
|
13002
|
-
workspaceCommand.command("repair").description("Preview or write explicit manifest path mappings for inferred workspace roots").requiredOption("--machine <id>", "Machine identifier").requiredOption("--project <id>", "Canonical project id").option("--repo <name>", "Repository name; defaults to project id").option("--open-files-repo <name>", "Open-files repository name", "open-files").option("--workspace-root <path>", "Override the machine workspace root for resolution").option("--project-root <path>", "Explicit project root to write").option("--open-files-root <path>", "Explicit open-files root to write").option("--apply", "Write the mappings into the manifest", false).option("--allow-untrusted", "Allow writing mappings for machines not marked trusted", false).option("--no-tailscale", "Skip tailscale status probing").option("-j, --json", "Print JSON output", false).action((options) => {
|
|
14000
|
+
workspaceCommand.command("repair").description("Preview or write explicit manifest path mappings for inferred workspace roots").requiredOption("--machine <id>", "Machine identifier").requiredOption("--project <id>", "Canonical project id").option("--repo <name>", "Repository name; defaults to project id").option("--open-files-repo <name>", "Open-files repository name", "open-files").option("--workspace-root <path>", "Override the machine workspace root for resolution").option("--project-root <path>", "Explicit project root to write").option("--open-files-root <path>", "Explicit open-files root to write").option("--apply", "Write the mappings into the manifest", false).option("--allow-untrusted", "Allow writing mappings for machines not marked trusted", false).option("--no-tailscale", "Skip tailscale status probing").option("--approval-token <token>", "Scoped mutation approval token").option("-j, --json", "Print JSON output", false).action((options) => {
|
|
14001
|
+
if (options.apply)
|
|
14002
|
+
requireCliMutation("workspace_repair", options.approvalToken, { machineId: options.machine, args: {
|
|
14003
|
+
machine: options.machine,
|
|
14004
|
+
project: options.project,
|
|
14005
|
+
repo: options.repo,
|
|
14006
|
+
openFilesRepo: options.openFilesRepo,
|
|
14007
|
+
workspaceRoot: options.workspaceRoot,
|
|
14008
|
+
projectRoot: options.projectRoot,
|
|
14009
|
+
openFilesRoot: options.openFilesRoot,
|
|
14010
|
+
allowUntrusted: options.allowUntrusted,
|
|
14011
|
+
tailscale: options.tailscale
|
|
14012
|
+
} });
|
|
13003
14013
|
const result = repairWorkspaceManifestMappings({
|
|
13004
14014
|
machineId: options.machine,
|
|
13005
14015
|
projectId: options.project,
|
|
@@ -13020,18 +14030,26 @@ program2.command("diff").description("Show manifest differences between two mach
|
|
|
13020
14030
|
const result = diffMachines(options.left, options.right);
|
|
13021
14031
|
console.log(JSON.stringify(result, null, 2));
|
|
13022
14032
|
});
|
|
13023
|
-
program2.command("backup").description("Create and optionally upload a machine backup archive").option("--bucket <name>", "S3 bucket name; defaults to HASNA_MACHINES_S3_BUCKET or MACHINES_S3_BUCKET").option("--prefix <prefix>", "S3 key prefix; defaults to HASNA_MACHINES_S3_PREFIX, MACHINES_S3_PREFIX, or machines").option("--apply", "Execute backup commands instead of previewing the plan", false).option("--yes", "Confirm execution when using --apply", false).option("-j, --json", "Print JSON output", false).action((options) => {
|
|
14033
|
+
program2.command("backup").description("Create and optionally upload a machine backup archive").option("--bucket <name>", "S3 bucket name; defaults to HASNA_MACHINES_S3_BUCKET or MACHINES_S3_BUCKET").option("--prefix <prefix>", "S3 key prefix; defaults to HASNA_MACHINES_S3_PREFIX, MACHINES_S3_PREFIX, or machines").option("--apply", "Execute backup commands instead of previewing the plan", false).option("--yes", "Confirm execution when using --apply", false).option("--approval-token <token>", "Scoped mutation approval token").option("-j, --json", "Print JSON output", false).action((options) => {
|
|
14034
|
+
if (options.apply) {
|
|
14035
|
+
const target = resolveBackupTarget({ bucket: options.bucket, prefix: options.prefix });
|
|
14036
|
+
requireCliMutation("backup_apply", options.approvalToken, { resourceId: cliResourceId("backup", target.bucket, target.prefix), args: { bucket: target.bucket, prefix: target.prefix, yes: options.yes } });
|
|
14037
|
+
}
|
|
13024
14038
|
const result = options.apply ? runBackup(options.bucket, options.prefix, { apply: true, yes: options.yes }) : buildBackupPlan(options.bucket, options.prefix);
|
|
13025
14039
|
console.log(JSON.stringify(result, null, 2));
|
|
13026
14040
|
});
|
|
13027
14041
|
var certCommand = program2.command("cert").description("Manage mkcert-based local SSL certificates");
|
|
13028
|
-
certCommand.command("issue").description("Plan or issue certificates for one or more domains").argument("<domains...>", "Domains to include in the certificate").option("--apply", "Execute certificate commands instead of previewing them", false).option("--yes", "Confirm execution when using --apply", false).option("-j, --json", "Print JSON output", false).action((domains, options) => {
|
|
14042
|
+
certCommand.command("issue").description("Plan or issue certificates for one or more domains").argument("<domains...>", "Domains to include in the certificate").option("--apply", "Execute certificate commands instead of previewing them", false).option("--yes", "Confirm execution when using --apply", false).option("--approval-token <token>", "Scoped mutation approval token").option("-j, --json", "Print JSON output", false).action((domains, options) => {
|
|
14043
|
+
if (options.apply)
|
|
14044
|
+
requireCliMutation("cert_apply", options.approvalToken, { resourceId: cliResourceId("cert", domains.join(",")), args: { domains, yes: options.yes } });
|
|
13029
14045
|
const result = options.apply ? runCertPlan(domains, { apply: true, yes: options.yes }) : buildCertPlan(domains);
|
|
13030
14046
|
console.log(JSON.stringify(result, null, 2));
|
|
13031
14047
|
});
|
|
13032
14048
|
var dnsCommand = program2.command("dns").description("Manage local domain mappings");
|
|
13033
|
-
dnsCommand.command("add").description("Add or replace a local domain mapping").requiredOption("--domain <domain>", "Domain name").requiredOption("--port <port>", "Target port").option("--target-host <host>", "Target host", "127.0.0.1").option("-j, --json", "Print JSON output", false).action((options) => {
|
|
13034
|
-
const
|
|
14049
|
+
dnsCommand.command("add").description("Add or replace a local domain mapping").requiredOption("--domain <domain>", "Domain name").requiredOption("--port <port>", "Target port").option("--target-host <host>", "Target host", "127.0.0.1").option("--approval-token <token>", "Scoped mutation approval token").option("-j, --json", "Print JSON output", false).action((options) => {
|
|
14050
|
+
const port = parseIntegerOption(options.port, "port", { min: 1, max: 65535 });
|
|
14051
|
+
requireCliMutation("dns_add", options.approvalToken, { resourceId: cliResourceId("dns", options.domain), args: { domain: options.domain, port, target_host: options.targetHost } });
|
|
14052
|
+
const result = addDomainMapping(options.domain, port, options.targetHost);
|
|
13035
14053
|
console.log(JSON.stringify(result, null, 2));
|
|
13036
14054
|
});
|
|
13037
14055
|
dnsCommand.command("list").description("List saved local domain mappings").option("-j, --json", "Print JSON output", false).action(() => {
|
|
@@ -13040,43 +14058,91 @@ dnsCommand.command("list").description("List saved local domain mappings").optio
|
|
|
13040
14058
|
dnsCommand.command("render").description("Render hosts/proxy configuration for a domain").argument("<domain>", "Domain name").option("-j, --json", "Print JSON output", false).action((domain) => {
|
|
13041
14059
|
console.log(JSON.stringify(renderDomainMapping(domain), null, 2));
|
|
13042
14060
|
});
|
|
13043
|
-
notificationsCommand.command("add").description("Add or replace a notification channel").requiredOption("--id <id>", "Channel identifier").requiredOption("--type <type>", "email | webhook | command").requiredOption("--target <target>", "Email, webhook URL, or
|
|
14061
|
+
notificationsCommand.command("add").description("Add or replace a notification channel").requiredOption("--id <id>", "Channel identifier").requiredOption("--type <type>", "email | webhook | command").requiredOption("--target <target>", "Email, webhook URL, or command executable").option("--arg <arg...>", "Command argument for command transports", collectOptionValues, []).option("--event <event...>", "Events routed to this channel", ["setup_failed", "sync_failed"]).option("--disabled", "Create the channel in disabled state", false).option("--approval-token <token>", "Operator mutation approval token for command transports").option("-j, --json", "Print JSON output", false).action((options) => {
|
|
14062
|
+
const enabled = !options.disabled;
|
|
14063
|
+
const events = [...new Set(options.event)];
|
|
14064
|
+
const commandArgs = options.arg ?? [];
|
|
14065
|
+
requireCliMutation("notifications_add", options.approvalToken, { resourceId: cliResourceId("notification", options.id), args: { id: options.id, type: options.type, target: options.target, args: commandArgs, event: events, enabled } });
|
|
13044
14066
|
const result = addNotificationChannel({
|
|
13045
14067
|
id: options.id,
|
|
13046
14068
|
type: options.type,
|
|
13047
14069
|
target: options.target,
|
|
13048
|
-
|
|
13049
|
-
|
|
13050
|
-
|
|
14070
|
+
commandArgs: options.type === "command" && commandArgs.length > 0 ? commandArgs : undefined,
|
|
14071
|
+
events,
|
|
14072
|
+
enabled
|
|
14073
|
+
}, { trustedApproval: trustedNotificationApproval2 });
|
|
13051
14074
|
printJsonOrText(result, renderNotificationConfigResult(result), options.json);
|
|
13052
14075
|
});
|
|
13053
14076
|
notificationsCommand.command("list").description("List configured notification channels").option("-j, --json", "Print JSON output", false).action((options) => {
|
|
13054
14077
|
const result = listNotificationChannels();
|
|
13055
14078
|
printJsonOrText(result, renderNotificationConfigResult(result), options.json);
|
|
13056
14079
|
});
|
|
13057
|
-
notificationsCommand.command("test").description("Preview or execute a notification test").requiredOption("--channel <id>", "Channel identifier").option("--event <name>", "Event name", "manual.test").option("--message <message>", "Test message", "machines notification test").option("--apply", "Execute the notification test instead of previewing it", false).option("--yes", "Confirm execution when using --apply", false).option("-j, --json", "Print JSON output", false).action(async (options) => {
|
|
14080
|
+
notificationsCommand.command("test").description("Preview or execute a notification test").requiredOption("--channel <id>", "Channel identifier").option("--event <name>", "Event name", "manual.test").option("--message <message>", "Test message", "machines notification test").option("--apply", "Execute the notification test instead of previewing it", false).option("--yes", "Confirm execution when using --apply", false).option("--approval-token <token>", "Operator mutation approval token for command transports").option("-j, --json", "Print JSON output", false).action(async (options) => {
|
|
14081
|
+
if (options.apply)
|
|
14082
|
+
requireCliMutation("notifications_test", options.approvalToken, { resourceId: cliResourceId("notification-test", options.channel, options.event), args: { channel: options.channel, event: options.event, message: options.message, yes: options.yes } });
|
|
13058
14083
|
const result = await testNotificationChannel(options.channel, options.event, options.message, {
|
|
13059
14084
|
apply: options.apply,
|
|
13060
|
-
yes: options.yes
|
|
14085
|
+
yes: options.yes,
|
|
14086
|
+
trustedApproval: options.apply === true ? trustedNotificationApproval2 : undefined
|
|
13061
14087
|
});
|
|
13062
14088
|
printJsonOrText(result, renderNotificationTestResult(result), options.json);
|
|
13063
14089
|
});
|
|
13064
|
-
notificationsCommand.command("dispatch").description("Dispatch an event to matching notification channels").requiredOption("--event <name>", "Event name").requiredOption("--message <message>", "Message body").option("--channel <id>", "Limit delivery to one channel").option("-j, --json", "Print JSON output", false).action(async (options) => {
|
|
13065
|
-
|
|
14090
|
+
notificationsCommand.command("dispatch").description("Dispatch an event to matching notification channels").requiredOption("--event <name>", "Event name").requiredOption("--message <message>", "Message body").option("--channel <id>", "Limit delivery to one channel").option("--approval-token <token>", "Operator mutation approval token for command transports").option("-j, --json", "Print JSON output", false).action(async (options) => {
|
|
14091
|
+
requireCliMutation("notifications_dispatch", options.approvalToken, { resourceId: cliResourceId("notification-dispatch", options.channel, options.event), args: { event: options.event, message: options.message, channel: options.channel } });
|
|
14092
|
+
const result = await dispatchNotificationEvent(options.event, options.message, { channelId: options.channel, trustedApproval: trustedNotificationApproval2 });
|
|
13066
14093
|
printJsonOrText(result, renderNotificationDispatchResult(result), options.json);
|
|
13067
14094
|
});
|
|
13068
|
-
notificationsCommand.command("remove").description("Remove a notification channel").argument("<id>", "Channel identifier").option("-j, --json", "Print JSON output", false).action((id, options) => {
|
|
14095
|
+
notificationsCommand.command("remove").description("Remove a notification channel").argument("<id>", "Channel identifier").option("--approval-token <token>", "Scoped mutation approval token").option("-j, --json", "Print JSON output", false).action((id, options) => {
|
|
14096
|
+
requireCliMutation("notifications_remove", options.approvalToken, { resourceId: cliResourceId("notification", id), args: { id } });
|
|
13069
14097
|
const result = removeNotificationChannel(id);
|
|
13070
14098
|
printJsonOrText(result, renderNotificationConfigResult(result), options.json);
|
|
13071
14099
|
});
|
|
13072
|
-
runtimeCommand.command("tmux-
|
|
14100
|
+
runtimeCommand.command("tmux-hook-plan").description("Print a tmux pane-died hook command that emits machines events").option("--machines-command <command>", "machines CLI executable", "machines").option("--tmux-command <command>", "tmux executable").option("--deliver", "Deliver webhooks from the hook instead of recording only", false).option("--approval-token <token>", "Scoped mutation token for the generated events emit command").option("--trusted-local-mutation", "Include process-local trusted mutation env when no approval token is supplied", false).option("-j, --json", "Print JSON output", false).action((options) => {
|
|
14101
|
+
if (!options.approvalToken && options.trustedLocalMutation !== true) {
|
|
14102
|
+
throw new Error("tmux-hook-plan requires --approval-token or explicit --trusted-local-mutation.");
|
|
14103
|
+
}
|
|
14104
|
+
const result = buildTmuxPaneDiedHookPlan({
|
|
14105
|
+
machinesCommand: options.machinesCommand,
|
|
14106
|
+
tmuxCommand: options.tmuxCommand,
|
|
14107
|
+
deliver: options.deliver,
|
|
14108
|
+
approvalToken: options.approvalToken,
|
|
14109
|
+
trustedLocalMutation: options.trustedLocalMutation
|
|
14110
|
+
});
|
|
14111
|
+
printJsonOrText(result, result.shellCommand, options.json);
|
|
14112
|
+
});
|
|
14113
|
+
runtimeCommand.command("tmux-watch").description("Watch a tmux pane and emit machines.tmux.pane_died if it disappears").argument("<target>", "tmux pane target, for example %1 or session:window.pane").option("--interval-ms <ms>", "Polling interval in milliseconds", "5000").option("--max-checks <n>", "Stop after N checks instead of watching forever").option("--once", "Probe once and emit machines.tmux.pane_missing when absent", false).option("--no-deliver", "Record the event without webhook delivery").option("--approval-token <token>", "Scoped mutation approval token for event delivery").option("-j, --json", "Print JSON output", false).action(async (target, options) => {
|
|
14114
|
+
const normalizedTarget = target.trim();
|
|
14115
|
+
if (!normalizedTarget)
|
|
14116
|
+
throw new Error("tmux pane target is required");
|
|
14117
|
+
const intervalMs = parseIntegerOption(options.intervalMs ?? "5000", "interval-ms", { min: 0 });
|
|
13073
14118
|
const maxChecks = options.once ? 1 : options.maxChecks ? parseIntegerOption(options.maxChecks, "max-checks", { min: 1 }) : undefined;
|
|
14119
|
+
const once = Boolean(options.once);
|
|
14120
|
+
const deliver = options.deliver !== false;
|
|
14121
|
+
const tmuxCommand = runtimeTmuxCommand();
|
|
14122
|
+
const eventTypes = runtimeTmuxEventTypes(once);
|
|
14123
|
+
const scopedIntervalMs = once ? undefined : intervalMs;
|
|
14124
|
+
if (deliver) {
|
|
14125
|
+
requireCliMutation("machines_runtime_tmux_watch_deliver", options.approvalToken, {
|
|
14126
|
+
resourceId: eventStoreResourceId2("runtime-tmux-watch", normalizedTarget, eventTypes.join(",")),
|
|
14127
|
+
args: withEventStoreScope2({
|
|
14128
|
+
target: normalizedTarget,
|
|
14129
|
+
event_types: eventTypes,
|
|
14130
|
+
interval_ms: scopedIntervalMs,
|
|
14131
|
+
max_checks: maxChecks,
|
|
14132
|
+
once,
|
|
14133
|
+
emit_initial_missing: once,
|
|
14134
|
+
deliver: true,
|
|
14135
|
+
tmux_command: tmuxCommand
|
|
14136
|
+
})
|
|
14137
|
+
});
|
|
14138
|
+
}
|
|
13074
14139
|
const result = await watchTmuxPane({
|
|
13075
|
-
target,
|
|
13076
|
-
intervalMs
|
|
14140
|
+
target: normalizedTarget,
|
|
14141
|
+
intervalMs,
|
|
13077
14142
|
maxChecks,
|
|
13078
|
-
emitInitialMissing:
|
|
13079
|
-
deliver
|
|
14143
|
+
emitInitialMissing: once,
|
|
14144
|
+
deliver,
|
|
14145
|
+
tmuxCommand,
|
|
13080
14146
|
onProbe: options.json ? undefined : (probe) => {
|
|
13081
14147
|
const status = probe.exists ? source_default.green("present") : source_default.yellow("missing");
|
|
13082
14148
|
console.error(`tmux ${probe.target}: ${status}${probe.paneId ? ` ${probe.paneId}` : ""}`);
|
|
@@ -13145,7 +14211,7 @@ clipboardCommand.command("clear-history").description("Clear clipboard sync hist
|
|
|
13145
14211
|
});
|
|
13146
14212
|
clipboardCommand.command("key").description("Show or rotate the shared secret key").option("--rotate", "Generate a new key", false).option("-j, --json", "Print JSON output", false).action((options) => {
|
|
13147
14213
|
if (options.rotate) {
|
|
13148
|
-
|
|
14214
|
+
rmSync3(getClipboardKeyPath(), { force: true });
|
|
13149
14215
|
}
|
|
13150
14216
|
const key = getOrCreateClipboardKey();
|
|
13151
14217
|
printJsonOrText({ key }, key, options.json);
|
|
@@ -13170,12 +14236,31 @@ installClaudeCommand.command("plan").description("Preview CLI install steps").op
|
|
|
13170
14236
|
const result = buildClaudeInstallPlan(options.machine, options.tool);
|
|
13171
14237
|
console.log(JSON.stringify(result, null, 2));
|
|
13172
14238
|
});
|
|
13173
|
-
installClaudeCommand.command("apply").description("Install or update the requested CLIs").option("--machine <id>", "Machine identifier").option("--tool <name...>", "CLI tools to install (claude, codex, gemini)").option("--yes", "Confirm execution when using apply", false).option("-j, --json", "Print JSON output", false).action((options) => {
|
|
13174
|
-
const
|
|
14239
|
+
installClaudeCommand.command("apply").description("Install or update the requested CLIs").option("--machine <id>", "Machine identifier").option("--tool <name...>", "CLI tools to install (claude, codex, gemini)").option("--yes", "Confirm execution when using apply", false).option("--approval-token <token>", "Scoped mutation approval token").option("-j, --json", "Print JSON output", false).action((options) => {
|
|
14240
|
+
const resolvedMachineId = cliMachineId(options.machine);
|
|
14241
|
+
const plan = buildClaudeInstallPlan(options.machine, options.tool);
|
|
14242
|
+
requireCliMutation("install_claude_apply", options.approvalToken, {
|
|
14243
|
+
machineId: resolvedMachineId,
|
|
14244
|
+
resourceId: cliPlanResourceId("install_claude_apply", resolvedMachineId, plan),
|
|
14245
|
+
args: cliPlanApprovalArgs({ machine_id: resolvedMachineId, tools: options.tool, yes: options.yes }, plan)
|
|
14246
|
+
});
|
|
14247
|
+
const result = runClaudeInstallPlan(plan, { apply: true, yes: options.yes });
|
|
13175
14248
|
console.log(JSON.stringify(result, null, 2));
|
|
13176
14249
|
});
|
|
13177
|
-
program2.command("install-tailscale").description("Install Tailscale on a machine").option("--machine <id>", "Machine identifier").option("--apply", "Execute installation commands instead of previewing the plan", false).option("--yes", "Confirm execution when using --apply", false).option("-j, --json", "Print JSON output", false).action((options) => {
|
|
13178
|
-
|
|
14250
|
+
program2.command("install-tailscale").description("Install Tailscale on a machine").option("--machine <id>", "Machine identifier").option("--apply", "Execute installation commands instead of previewing the plan", false).option("--yes", "Confirm execution when using --apply", false).option("--approval-token <token>", "Scoped mutation approval token").option("-j, --json", "Print JSON output", false).action((options) => {
|
|
14251
|
+
if (options.apply) {
|
|
14252
|
+
const resolvedMachineId = cliMachineId(options.machine);
|
|
14253
|
+
const plan = buildTailscaleInstallPlan(options.machine);
|
|
14254
|
+
requireCliMutation("install_tailscale_apply", options.approvalToken, {
|
|
14255
|
+
machineId: resolvedMachineId,
|
|
14256
|
+
resourceId: cliPlanResourceId("install_tailscale_apply", resolvedMachineId, plan),
|
|
14257
|
+
args: cliPlanApprovalArgs({ machine_id: resolvedMachineId, yes: options.yes }, plan)
|
|
14258
|
+
});
|
|
14259
|
+
const result2 = runTailscaleInstallPlan(plan, { apply: true, yes: options.yes });
|
|
14260
|
+
console.log(JSON.stringify(result2, null, 2));
|
|
14261
|
+
return;
|
|
14262
|
+
}
|
|
14263
|
+
const result = buildTailscaleInstallPlan(options.machine);
|
|
13179
14264
|
console.log(JSON.stringify(result, null, 2));
|
|
13180
14265
|
});
|
|
13181
14266
|
program2.command("route").description("Resolve the best route for a machine").requiredOption("--machine <id>", "Machine identifier").option("--no-tailscale", "Skip tailscale status probing").option("--private-metadata", "Print private route targets", false).option("--cmd <command>", "Remote command to run").option("-j, --json", "Print JSON output", false).action((options) => {
|
|
@@ -13327,28 +14412,34 @@ storageCommand.command("status").description("Show storage sync status").option(
|
|
|
13327
14412
|
["tables", status.tables.join(", ")]
|
|
13328
14413
|
]), options.json);
|
|
13329
14414
|
});
|
|
13330
|
-
storageCommand.command("push").description("Push local machine runtime data to storage PostgreSQL").option("--tables <tables>", "Comma-separated table names").option("-j, --json", "Print JSON output", false).action(async (options) => {
|
|
14415
|
+
storageCommand.command("push").description("Push local machine runtime data to storage PostgreSQL").option("--tables <tables>", "Comma-separated table names").option("--approval-token <token>", "Scoped mutation approval token").option("-j, --json", "Print JSON output", false).action(async (options) => {
|
|
13331
14416
|
try {
|
|
13332
|
-
const { parseStorageTables: parseStorageTables2, storagePush: storagePush2 } = await Promise.resolve().then(() => (init_storage(), exports_storage));
|
|
13333
|
-
const
|
|
14417
|
+
const { parseStorageTables: parseStorageTables2, resolveTables: resolveTables2, storagePush: storagePush2 } = await Promise.resolve().then(() => (init_storage(), exports_storage));
|
|
14418
|
+
const tables = resolveTables2(parseStorageTables2(options.tables));
|
|
14419
|
+
requireCliMutation("storage_push", options.approvalToken, { resourceId: cliResourceId("storage-push", tables.join(",")), args: { tables } });
|
|
14420
|
+
const results = await storagePush2({ tables });
|
|
13334
14421
|
printStorageResults(results, options.json);
|
|
13335
14422
|
} catch (error) {
|
|
13336
14423
|
printStorageError(error);
|
|
13337
14424
|
}
|
|
13338
14425
|
});
|
|
13339
|
-
storageCommand.command("pull").description("Pull machine runtime data from storage PostgreSQL to local SQLite").option("--tables <tables>", "Comma-separated table names").option("-j, --json", "Print JSON output", false).action(async (options) => {
|
|
14426
|
+
storageCommand.command("pull").description("Pull machine runtime data from storage PostgreSQL to local SQLite").option("--tables <tables>", "Comma-separated table names").option("--approval-token <token>", "Scoped mutation approval token").option("-j, --json", "Print JSON output", false).action(async (options) => {
|
|
13340
14427
|
try {
|
|
13341
|
-
const { parseStorageTables: parseStorageTables2, storagePull: storagePull2 } = await Promise.resolve().then(() => (init_storage(), exports_storage));
|
|
13342
|
-
const
|
|
14428
|
+
const { parseStorageTables: parseStorageTables2, resolveTables: resolveTables2, storagePull: storagePull2 } = await Promise.resolve().then(() => (init_storage(), exports_storage));
|
|
14429
|
+
const tables = resolveTables2(parseStorageTables2(options.tables));
|
|
14430
|
+
requireCliMutation("storage_pull", options.approvalToken, { resourceId: cliResourceId("storage-pull", tables.join(",")), args: { tables } });
|
|
14431
|
+
const results = await storagePull2({ tables });
|
|
13343
14432
|
printStorageResults(results, options.json);
|
|
13344
14433
|
} catch (error) {
|
|
13345
14434
|
printStorageError(error);
|
|
13346
14435
|
}
|
|
13347
14436
|
});
|
|
13348
|
-
storageCommand.command("sync").description("Bidirectional storage sync: pull then push").option("--tables <tables>", "Comma-separated table names").option("-j, --json", "Print JSON output", false).action(async (options) => {
|
|
14437
|
+
storageCommand.command("sync").description("Bidirectional storage sync: pull then push").option("--tables <tables>", "Comma-separated table names").option("--approval-token <token>", "Scoped mutation approval token").option("-j, --json", "Print JSON output", false).action(async (options) => {
|
|
13349
14438
|
try {
|
|
13350
|
-
const { parseStorageTables: parseStorageTables2, storageSync: storageSync2 } = await Promise.resolve().then(() => (init_storage(), exports_storage));
|
|
13351
|
-
const
|
|
14439
|
+
const { parseStorageTables: parseStorageTables2, resolveTables: resolveTables2, storageSync: storageSync2 } = await Promise.resolve().then(() => (init_storage(), exports_storage));
|
|
14440
|
+
const tables = resolveTables2(parseStorageTables2(options.tables));
|
|
14441
|
+
requireCliMutation("storage_sync", options.approvalToken, { resourceId: cliResourceId("storage-sync", tables.join(",")), args: { tables } });
|
|
14442
|
+
const result = await storageSync2({ tables });
|
|
13352
14443
|
if (options.json) {
|
|
13353
14444
|
console.log(JSON.stringify(result, null, 2));
|
|
13354
14445
|
return;
|
|
@@ -13373,7 +14464,7 @@ program2.command("self-test").description("Run local package smoke checks").opti
|
|
|
13373
14464
|
const result = runSelfTest();
|
|
13374
14465
|
printJsonOrText(result, renderSelfTestResult(result), options.json);
|
|
13375
14466
|
});
|
|
13376
|
-
program2.command("serve").description("Serve a local fleet dashboard and JSON API").option("--host <host>", "Host interface to bind", "
|
|
14467
|
+
program2.command("serve").description("Serve a local fleet dashboard and JSON API").option("--host <host>", "Host interface to bind", "127.0.0.1").option("--port <port>", "Port to bind", "7676").option("-j, --json", "Print serve config and exit", false).action((options) => {
|
|
13377
14468
|
const info = getServeInfo({ host: options.host, port: parseIntegerOption(options.port, "port", { min: 1, max: 65535 }) });
|
|
13378
14469
|
if (options.json) {
|
|
13379
14470
|
console.log(JSON.stringify(info, null, 2));
|