@hasna/machines 0.0.44 → 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 +1316 -213
- 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 +1108 -189
- 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 +1021 -167
- 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 { 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";
|
|
@@ -10980,19 +11567,31 @@ WantedBy=${options.mode === "system" ? "multi-user.target" : "default.target"}
|
|
|
10980
11567
|
`;
|
|
10981
11568
|
}
|
|
10982
11569
|
function daemonProgramArguments(options) {
|
|
10983
|
-
const bunRuntime =
|
|
11570
|
+
const bunRuntime = bunRuntimeForExecutable(options.executable);
|
|
10984
11571
|
const base = bunRuntime ? [bunRuntime, options.executable] : [options.executable];
|
|
10985
11572
|
return [...base, "--interval-ms", String(options.intervalMs)];
|
|
10986
11573
|
}
|
|
10987
|
-
function
|
|
11574
|
+
function bunRuntimeForExecutable(executable) {
|
|
10988
11575
|
if (!isBunShebangScript(executable))
|
|
10989
11576
|
return null;
|
|
10990
|
-
const candidate
|
|
10991
|
-
|
|
11577
|
+
for (const candidate of bunRuntimeCandidates(executable)) {
|
|
11578
|
+
if (isExecutableFile(candidate))
|
|
11579
|
+
return candidate;
|
|
11580
|
+
}
|
|
11581
|
+
return null;
|
|
11582
|
+
}
|
|
11583
|
+
function bunRuntimeCandidates(executable) {
|
|
11584
|
+
const candidates = [
|
|
11585
|
+
`${dirname4(executable)}/bun`,
|
|
11586
|
+
process.env["BUN_INSTALL"] ? `${process.env["BUN_INSTALL"]}/bin/bun` : null,
|
|
11587
|
+
process.env["HOME"] ? `${process.env["HOME"]}/.bun/bin/bun` : null,
|
|
11588
|
+
...(process.env["PATH"] ?? "").split(delimiter2).filter(Boolean).map((entry) => `${entry}/bun`)
|
|
11589
|
+
].filter((value) => Boolean(value));
|
|
11590
|
+
return [...new Set(candidates)];
|
|
10992
11591
|
}
|
|
10993
11592
|
function isBunShebangScript(executable) {
|
|
10994
11593
|
try {
|
|
10995
|
-
const content =
|
|
11594
|
+
const content = readFileSync7(executable, "utf8").slice(0, 256);
|
|
10996
11595
|
const firstLine2 = content.split(/\r?\n/, 1)[0] ?? "";
|
|
10997
11596
|
return /^#!.*\bbun\b/.test(firstLine2);
|
|
10998
11597
|
} catch {
|
|
@@ -11000,7 +11599,7 @@ function isBunShebangScript(executable) {
|
|
|
11000
11599
|
}
|
|
11001
11600
|
}
|
|
11002
11601
|
function isExecutableFile(path) {
|
|
11003
|
-
if (!
|
|
11602
|
+
if (!existsSync9(path))
|
|
11004
11603
|
return false;
|
|
11005
11604
|
try {
|
|
11006
11605
|
const stats = statSync(path);
|
|
@@ -11050,7 +11649,8 @@ function basename(path) {
|
|
|
11050
11649
|
init_db();
|
|
11051
11650
|
|
|
11052
11651
|
// src/commands/serve.ts
|
|
11053
|
-
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";
|
|
11054
11654
|
|
|
11055
11655
|
// src/agent/runtime.ts
|
|
11056
11656
|
import { arch as arch3, hostname as hostname6, platform as platform4, release, uptime, version as osVersion } from "os";
|
|
@@ -11137,7 +11737,7 @@ function escapeHtml(value) {
|
|
|
11137
11737
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
11138
11738
|
}
|
|
11139
11739
|
function getServeInfo(options = {}) {
|
|
11140
|
-
const host = options.host || "
|
|
11740
|
+
const host = options.host || "127.0.0.1";
|
|
11141
11741
|
const port = options.port || 7676;
|
|
11142
11742
|
return {
|
|
11143
11743
|
host,
|
|
@@ -11340,6 +11940,59 @@ async function parseJsonBody(request) {
|
|
|
11340
11940
|
function jsonError(message, status = 400) {
|
|
11341
11941
|
return Response.json({ error: message }, { status });
|
|
11342
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
|
+
}
|
|
11343
11996
|
function privateOutputWarnings(requested, allowed) {
|
|
11344
11997
|
return requested && !allowed ? [PRIVATE_OUTPUT_DENIED_WARNING] : [];
|
|
11345
11998
|
}
|
|
@@ -11351,6 +12004,7 @@ function appendWarnings(payload, warnings) {
|
|
|
11351
12004
|
function startDashboardServer(options = {}) {
|
|
11352
12005
|
const info = getServeInfo(options);
|
|
11353
12006
|
const events = new EventsClient2;
|
|
12007
|
+
const trustedNotificationApproval2 = createTrustedNotificationApproval();
|
|
11354
12008
|
return Bun.serve({
|
|
11355
12009
|
hostname: info.host,
|
|
11356
12010
|
port: info.port,
|
|
@@ -11415,8 +12069,25 @@ function startDashboardServer(options = {}) {
|
|
|
11415
12069
|
const severity = typeof body["severity"] === "string" ? body["severity"] : undefined;
|
|
11416
12070
|
const message = typeof body["message"] === "string" ? body["message"] : undefined;
|
|
11417
12071
|
const dedupeKey = typeof body["dedupeKey"] === "string" ? body["dedupeKey"] : undefined;
|
|
11418
|
-
const data =
|
|
11419
|
-
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;
|
|
11420
12091
|
return Response.json(await events.emit({
|
|
11421
12092
|
source,
|
|
11422
12093
|
type,
|
|
@@ -11459,8 +12130,20 @@ function startDashboardServer(options = {}) {
|
|
|
11459
12130
|
const message = typeof body["message"] === "string" ? body["message"] : undefined;
|
|
11460
12131
|
const apply = body["apply"] === true;
|
|
11461
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;
|
|
11462
12141
|
try {
|
|
11463
|
-
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
|
+
}));
|
|
11464
12147
|
} catch (error) {
|
|
11465
12148
|
return jsonError(error instanceof Error ? error.message : String(error));
|
|
11466
12149
|
}
|
|
@@ -11475,13 +12158,22 @@ function startDashboardServer(options = {}) {
|
|
|
11475
12158
|
return jsonError("channelId is required.");
|
|
11476
12159
|
}
|
|
11477
12160
|
const type = typeof body["type"] === "string" ? body["type"] : "events.test";
|
|
11478
|
-
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;
|
|
11479
12170
|
try {
|
|
11480
12171
|
return Response.json(await events.testChannel(channelId, {
|
|
11481
12172
|
source: "machines",
|
|
11482
12173
|
type,
|
|
11483
|
-
subject
|
|
11484
|
-
message
|
|
12174
|
+
subject,
|
|
12175
|
+
message,
|
|
12176
|
+
data
|
|
11485
12177
|
}));
|
|
11486
12178
|
} catch (error) {
|
|
11487
12179
|
return jsonError(error instanceof Error ? error.message : String(error));
|
|
@@ -11529,9 +12221,9 @@ function runSelfTest() {
|
|
|
11529
12221
|
|
|
11530
12222
|
// src/commands/clipboard.ts
|
|
11531
12223
|
init_paths();
|
|
11532
|
-
import { createHash } from "crypto";
|
|
11533
|
-
import { existsSync as
|
|
11534
|
-
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";
|
|
11535
12227
|
var DEFAULT_CONFIG = {
|
|
11536
12228
|
version: 1,
|
|
11537
12229
|
enabled: true,
|
|
@@ -11549,7 +12241,7 @@ var DEFAULT_CONFIG = {
|
|
|
11549
12241
|
function resolveConfigPath(configPath) {
|
|
11550
12242
|
if (configPath)
|
|
11551
12243
|
return configPath;
|
|
11552
|
-
return
|
|
12244
|
+
return join8(getDataDir(), "clipboard-config.json");
|
|
11553
12245
|
}
|
|
11554
12246
|
function resolveHistoryPath(historyPath) {
|
|
11555
12247
|
if (historyPath)
|
|
@@ -11561,10 +12253,10 @@ function getDefaultConfig() {
|
|
|
11561
12253
|
}
|
|
11562
12254
|
function readConfig(configPath) {
|
|
11563
12255
|
const path = resolveConfigPath(configPath);
|
|
11564
|
-
if (!
|
|
12256
|
+
if (!existsSync10(path)) {
|
|
11565
12257
|
return getDefaultConfig();
|
|
11566
12258
|
}
|
|
11567
|
-
const parsed = JSON.parse(
|
|
12259
|
+
const parsed = JSON.parse(readFileSync8(path, "utf8"));
|
|
11568
12260
|
return { ...getDefaultConfig(), ...parsed };
|
|
11569
12261
|
}
|
|
11570
12262
|
function writeConfig(config, configPath) {
|
|
@@ -11575,11 +12267,11 @@ function writeConfig(config, configPath) {
|
|
|
11575
12267
|
}
|
|
11576
12268
|
function readHistory(historyPath) {
|
|
11577
12269
|
const path = resolveHistoryPath(historyPath);
|
|
11578
|
-
if (!
|
|
12270
|
+
if (!existsSync10(path)) {
|
|
11579
12271
|
return [];
|
|
11580
12272
|
}
|
|
11581
12273
|
try {
|
|
11582
|
-
return JSON.parse(
|
|
12274
|
+
return JSON.parse(readFileSync8(path, "utf8"));
|
|
11583
12275
|
} catch {
|
|
11584
12276
|
return [];
|
|
11585
12277
|
}
|
|
@@ -11591,7 +12283,7 @@ function writeHistory(entries, historyPath) {
|
|
|
11591
12283
|
`, "utf8");
|
|
11592
12284
|
}
|
|
11593
12285
|
function computeHash(content) {
|
|
11594
|
-
return
|
|
12286
|
+
return createHash2("sha256").update(content).digest("hex").slice(0, 16);
|
|
11595
12287
|
}
|
|
11596
12288
|
function shouldSkipContent(content, skipPatterns) {
|
|
11597
12289
|
const lower = content.toLowerCase();
|
|
@@ -11608,10 +12300,10 @@ function sanitizeClipboardForRead(content, maxSizeBytes, skipPatterns) {
|
|
|
11608
12300
|
}
|
|
11609
12301
|
function getOrCreateClipboardKey() {
|
|
11610
12302
|
const keyPath = getClipboardKeyPath();
|
|
11611
|
-
if (
|
|
11612
|
-
return
|
|
12303
|
+
if (existsSync10(keyPath)) {
|
|
12304
|
+
return readFileSync8(keyPath, "utf8").trim();
|
|
11613
12305
|
}
|
|
11614
|
-
const key =
|
|
12306
|
+
const key = createHash2("sha256").update(crypto.randomUUID()).digest("hex").slice(0, 32);
|
|
11615
12307
|
ensureParentDir(keyPath);
|
|
11616
12308
|
writeFileSync5(keyPath, `${key}
|
|
11617
12309
|
`, "utf8");
|
|
@@ -11648,8 +12340,8 @@ function addClipboardEntry(entry, historyPath) {
|
|
|
11648
12340
|
}
|
|
11649
12341
|
function clearClipboardHistory(historyPath) {
|
|
11650
12342
|
const path = resolveHistoryPath(historyPath);
|
|
11651
|
-
if (
|
|
11652
|
-
|
|
12343
|
+
if (existsSync10(path)) {
|
|
12344
|
+
rmSync2(path);
|
|
11653
12345
|
}
|
|
11654
12346
|
}
|
|
11655
12347
|
function getClipboardStatus(historyPath) {
|
|
@@ -11664,15 +12356,15 @@ function getClipboardStatus(historyPath) {
|
|
|
11664
12356
|
|
|
11665
12357
|
// src/commands/clipboard-daemon.ts
|
|
11666
12358
|
init_paths();
|
|
11667
|
-
import { readFileSync as
|
|
11668
|
-
import { join as
|
|
11669
|
-
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";
|
|
11670
12362
|
|
|
11671
12363
|
// src/commands/clipboard-server.ts
|
|
11672
12364
|
init_paths();
|
|
11673
12365
|
import { createServer } from "http";
|
|
11674
|
-
import { createHash as
|
|
11675
|
-
import { readFileSync as
|
|
12366
|
+
import { createHash as createHash3 } from "crypto";
|
|
12367
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
11676
12368
|
function readLocalClipboardSync() {
|
|
11677
12369
|
const platform5 = process.platform;
|
|
11678
12370
|
if (platform5 === "darwin") {
|
|
@@ -11718,7 +12410,7 @@ function hasCommand3(binary) {
|
|
|
11718
12410
|
function loadSharedSecret() {
|
|
11719
12411
|
const keyPath = getClipboardKeyPath();
|
|
11720
12412
|
try {
|
|
11721
|
-
return
|
|
12413
|
+
return readFileSync9(keyPath, "utf8").trim();
|
|
11722
12414
|
} catch {
|
|
11723
12415
|
return "";
|
|
11724
12416
|
}
|
|
@@ -11732,7 +12424,7 @@ function authenticate(request) {
|
|
|
11732
12424
|
const secret = loadSharedSecret();
|
|
11733
12425
|
if (!secret)
|
|
11734
12426
|
return false;
|
|
11735
|
-
return
|
|
12427
|
+
return createHash3("sha256").update(token).digest("hex") === createHash3("sha256").update(secret).digest("hex");
|
|
11736
12428
|
}
|
|
11737
12429
|
function jsonResponse(response, status, data) {
|
|
11738
12430
|
response.writeHead(status, { "content-type": "application/json" });
|
|
@@ -11772,7 +12464,7 @@ function startClipboardServer(options = {}) {
|
|
|
11772
12464
|
server,
|
|
11773
12465
|
port,
|
|
11774
12466
|
close: async () => {
|
|
11775
|
-
await new Promise((
|
|
12467
|
+
await new Promise((resolve4) => server.close(() => resolve4()));
|
|
11776
12468
|
}
|
|
11777
12469
|
};
|
|
11778
12470
|
}
|
|
@@ -11823,7 +12515,7 @@ function handleGetClipboard(response, config) {
|
|
|
11823
12515
|
}
|
|
11824
12516
|
|
|
11825
12517
|
// src/commands/clipboard-daemon.ts
|
|
11826
|
-
var DAEMON_PID_PATH =
|
|
12518
|
+
var DAEMON_PID_PATH = join9(getDataDir(), "clipboard-daemon.pid");
|
|
11827
12519
|
function readLocalClipboardSync2() {
|
|
11828
12520
|
const platform5 = process.platform;
|
|
11829
12521
|
if (platform5 === "darwin") {
|
|
@@ -11881,11 +12573,11 @@ function hasDisplayServer() {
|
|
|
11881
12573
|
return false;
|
|
11882
12574
|
}
|
|
11883
12575
|
function computeHash2(content) {
|
|
11884
|
-
return
|
|
12576
|
+
return createHash4("sha256").update(content).digest("hex").slice(0, 16);
|
|
11885
12577
|
}
|
|
11886
12578
|
function loadSharedSecret2() {
|
|
11887
12579
|
try {
|
|
11888
|
-
return
|
|
12580
|
+
return readFileSync10(getClipboardKeyPath(), "utf8").trim();
|
|
11889
12581
|
} catch {
|
|
11890
12582
|
return "";
|
|
11891
12583
|
}
|
|
@@ -11896,7 +12588,7 @@ function writePid(pid) {
|
|
|
11896
12588
|
}
|
|
11897
12589
|
function readPid() {
|
|
11898
12590
|
try {
|
|
11899
|
-
const pid = Number.parseInt(
|
|
12591
|
+
const pid = Number.parseInt(readFileSync10(DAEMON_PID_PATH, "utf8").trim());
|
|
11900
12592
|
return Number.isFinite(pid) ? pid : null;
|
|
11901
12593
|
} catch {
|
|
11902
12594
|
return null;
|
|
@@ -11997,8 +12689,8 @@ async function discoverPeers() {
|
|
|
11997
12689
|
|
|
11998
12690
|
// src/commands/heal.ts
|
|
11999
12691
|
init_paths();
|
|
12000
|
-
import { existsSync as
|
|
12001
|
-
import { join as
|
|
12692
|
+
import { existsSync as existsSync11, readFileSync as readFileSync11, writeFileSync as writeFileSync7 } from "fs";
|
|
12693
|
+
import { join as join10 } from "path";
|
|
12002
12694
|
var DEFAULT_THRESHOLDS = {
|
|
12003
12695
|
reconnect: 3,
|
|
12004
12696
|
nmRestart: 7,
|
|
@@ -12042,16 +12734,16 @@ function defaultHealState() {
|
|
|
12042
12734
|
};
|
|
12043
12735
|
}
|
|
12044
12736
|
function getHealConfigPath() {
|
|
12045
|
-
return process.env["HASNA_MACHINES_HEAL_CONFIG_PATH"] ||
|
|
12737
|
+
return process.env["HASNA_MACHINES_HEAL_CONFIG_PATH"] || join10(getDataDir(), "heal-config.json");
|
|
12046
12738
|
}
|
|
12047
12739
|
function getHealStatePath() {
|
|
12048
|
-
return process.env["HASNA_MACHINES_HEAL_STATE_PATH"] ||
|
|
12740
|
+
return process.env["HASNA_MACHINES_HEAL_STATE_PATH"] || join10(getDataDir(), "heal-state.json");
|
|
12049
12741
|
}
|
|
12050
12742
|
function readHealConfig(path) {
|
|
12051
12743
|
const p = path || getHealConfigPath();
|
|
12052
|
-
if (!
|
|
12744
|
+
if (!existsSync11(p))
|
|
12053
12745
|
return { ...DEFAULT_HEAL_CONFIG, thresholds: { ...DEFAULT_THRESHOLDS } };
|
|
12054
|
-
const parsed = JSON.parse(
|
|
12746
|
+
const parsed = JSON.parse(readFileSync11(p, "utf8"));
|
|
12055
12747
|
return {
|
|
12056
12748
|
...DEFAULT_HEAL_CONFIG,
|
|
12057
12749
|
...parsed,
|
|
@@ -12067,10 +12759,10 @@ function writeHealConfig(config, path) {
|
|
|
12067
12759
|
}
|
|
12068
12760
|
function readHealState(path) {
|
|
12069
12761
|
const p = path || getHealStatePath();
|
|
12070
|
-
if (!
|
|
12762
|
+
if (!existsSync11(p))
|
|
12071
12763
|
return defaultHealState();
|
|
12072
12764
|
try {
|
|
12073
|
-
return { ...defaultHealState(), ...JSON.parse(
|
|
12765
|
+
return { ...defaultHealState(), ...JSON.parse(readFileSync11(p, "utf8")) };
|
|
12074
12766
|
} catch {
|
|
12075
12767
|
return defaultHealState();
|
|
12076
12768
|
}
|
|
@@ -12197,7 +12889,7 @@ function sh(cmd, timeoutMs = 8000) {
|
|
|
12197
12889
|
}
|
|
12198
12890
|
function getCurrentBootId() {
|
|
12199
12891
|
try {
|
|
12200
|
-
return
|
|
12892
|
+
return readFileSync11("/proc/sys/kernel/random/boot_id", "utf8").trim();
|
|
12201
12893
|
} catch {
|
|
12202
12894
|
return "";
|
|
12203
12895
|
}
|
|
@@ -12283,9 +12975,9 @@ function executeAction(action, config) {
|
|
|
12283
12975
|
|
|
12284
12976
|
// src/commands/heal-daemon.ts
|
|
12285
12977
|
init_paths();
|
|
12286
|
-
import { existsSync as
|
|
12287
|
-
import { join as
|
|
12288
|
-
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");
|
|
12289
12981
|
var SERVICE_PATH = "/etc/systemd/system/machines-heal.service";
|
|
12290
12982
|
var SYSTEM_CONF = "/etc/systemd/system.conf";
|
|
12291
12983
|
function log(msg) {
|
|
@@ -12331,7 +13023,7 @@ function writePid2(pid) {
|
|
|
12331
13023
|
}
|
|
12332
13024
|
function readPid2() {
|
|
12333
13025
|
try {
|
|
12334
|
-
const pid = Number.parseInt(
|
|
13026
|
+
const pid = Number.parseInt(readFileSync12(DAEMON_PID_PATH2, "utf8").trim());
|
|
12335
13027
|
return Number.isFinite(pid) ? pid : null;
|
|
12336
13028
|
} catch {
|
|
12337
13029
|
return null;
|
|
@@ -12403,9 +13095,9 @@ function applyDeterminism(config) {
|
|
|
12403
13095
|
}
|
|
12404
13096
|
function enableHardwareWatchdog() {
|
|
12405
13097
|
const log2 = [];
|
|
12406
|
-
if (!
|
|
13098
|
+
if (!existsSync12(SYSTEM_CONF))
|
|
12407
13099
|
return ["/etc/systemd/system.conf not found; skipping hardware watchdog"];
|
|
12408
|
-
let conf =
|
|
13100
|
+
let conf = readFileSync12(SYSTEM_CONF, "utf8");
|
|
12409
13101
|
const set = (key, value) => {
|
|
12410
13102
|
const re = new RegExp(`^#?\\s*${key}=.*$`, "m");
|
|
12411
13103
|
if (re.test(conf))
|
|
@@ -12435,7 +13127,7 @@ function binPath() {
|
|
|
12435
13127
|
candidates.push(`${home}/.bun/bin/machines`);
|
|
12436
13128
|
candidates.push("/root/.bun/bin/machines", "/usr/local/bin/machines");
|
|
12437
13129
|
for (const c of candidates) {
|
|
12438
|
-
if (c &&
|
|
13130
|
+
if (c && existsSync12(c))
|
|
12439
13131
|
return c;
|
|
12440
13132
|
}
|
|
12441
13133
|
return "machines";
|
|
@@ -12471,7 +13163,7 @@ WantedBy=multi-user.target
|
|
|
12471
13163
|
function uninstallHealService() {
|
|
12472
13164
|
const log2 = [];
|
|
12473
13165
|
sh2("systemctl disable --now machines-heal.service 2>/dev/null || true");
|
|
12474
|
-
if (
|
|
13166
|
+
if (existsSync12(SERVICE_PATH)) {
|
|
12475
13167
|
sh2(`rm -f ${SERVICE_PATH}`);
|
|
12476
13168
|
sh2("systemctl daemon-reload");
|
|
12477
13169
|
log2.push(`removed ${SERVICE_PATH}`);
|
|
@@ -12482,7 +13174,7 @@ function uninstallHealService() {
|
|
|
12482
13174
|
}
|
|
12483
13175
|
function healServiceStatus() {
|
|
12484
13176
|
return {
|
|
12485
|
-
installed:
|
|
13177
|
+
installed: existsSync12(SERVICE_PATH),
|
|
12486
13178
|
active: sh2("systemctl is-active machines-heal.service").out === "active",
|
|
12487
13179
|
enabled: sh2("systemctl is-enabled machines-heal.service 2>/dev/null").out === "enabled"
|
|
12488
13180
|
};
|
|
@@ -12521,8 +13213,6 @@ ${items.map((item) => `- ${item}`).join(`
|
|
|
12521
13213
|
}
|
|
12522
13214
|
|
|
12523
13215
|
// src/cli/index.ts
|
|
12524
|
-
import { rmSync as rmSync2 } from "fs";
|
|
12525
|
-
import { readFileSync as readFileSync12 } from "fs";
|
|
12526
13216
|
var program2 = new Command;
|
|
12527
13217
|
function printJsonOrText(data, text, json = false) {
|
|
12528
13218
|
if (json || program2.opts().quiet) {
|
|
@@ -12790,23 +13480,307 @@ program2.name("machines").description("Machine fleet management CLI + MCP for de
|
|
|
12790
13480
|
var manifestCommand = program2.command("manifest").description("Manage the fleet manifest");
|
|
12791
13481
|
var appsCommand = program2.command("apps").description("Manage installed applications per machine");
|
|
12792
13482
|
var notificationsCommand = program2.command("notifications").description("Manage fleet alert delivery channels");
|
|
12793
|
-
var eventWebhooksCommand =
|
|
12794
|
-
|
|
12795
|
-
var webhookTestCommand = eventWebhooksCommand.commands.find((command2) => command2.name() === "test");
|
|
12796
|
-
var webhookOptions = webhookTestCommand?.options ?? [];
|
|
12797
|
-
var webhookMessageOption = webhookOptions.find((option) => option.long === "--message");
|
|
12798
|
-
if (webhookMessageOption) {
|
|
12799
|
-
webhookMessageOption.defaultValue = "Shared events test delivery";
|
|
12800
|
-
}
|
|
12801
|
-
var eventsCommand = registerEventCommands(program2, { source: "machines" });
|
|
12802
|
-
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");
|
|
12803
13485
|
var runtimeCommand = program2.command("runtime").description("Watch runtime conditions and emit shared events");
|
|
12804
13486
|
var clipboardCommand = program2.command("clipboard").description("Real-time clipboard sync across fleet machines");
|
|
12805
13487
|
var installClaudeCommand = program2.command("install-claude").description("Install or inspect Claude, Codex, and Gemini CLIs");
|
|
12806
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
|
+
});
|
|
12807
13777
|
function addDaemonLifecycleCommand(action, description) {
|
|
12808
|
-
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) => {
|
|
12809
|
-
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
|
+
}
|
|
12810
13784
|
const result = runDaemonServicePlan(plan, { apply: options.apply, yes: options.yes });
|
|
12811
13785
|
if (options.json || options.apply) {
|
|
12812
13786
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -12820,7 +13794,8 @@ addDaemonLifecycleCommand("uninstall", "Plan or uninstall the machines-agent dae
|
|
|
12820
13794
|
addDaemonLifecycleCommand("restart", "Plan or restart the machines-agent daemon service");
|
|
12821
13795
|
addDaemonLifecycleCommand("status", "Plan a daemon service status command");
|
|
12822
13796
|
addDaemonLifecycleCommand("logs", "Plan a daemon service log command");
|
|
12823
|
-
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: {} });
|
|
12824
13799
|
console.log(manifestInit());
|
|
12825
13800
|
});
|
|
12826
13801
|
manifestCommand.command("path").description("Print the manifest path").action(() => {
|
|
@@ -12832,7 +13807,8 @@ manifestCommand.command("list").description("Print the fleet manifest").action((
|
|
|
12832
13807
|
manifestCommand.command("validate").description("Validate the fleet manifest").action(() => {
|
|
12833
13808
|
console.log(JSON.stringify(manifestValidate(), null, 2));
|
|
12834
13809
|
});
|
|
12835
|
-
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: {} });
|
|
12836
13812
|
console.log(JSON.stringify(manifestBootstrapCurrentMachine(), null, 2));
|
|
12837
13813
|
});
|
|
12838
13814
|
manifestCommand.command("get").description("Print a single machine from the manifest").argument("<id>", "Machine identifier").action((id) => {
|
|
@@ -12844,18 +13820,20 @@ manifestCommand.command("get").description("Print a single machine from the mani
|
|
|
12844
13820
|
}
|
|
12845
13821
|
console.log(JSON.stringify(machine, null, 2));
|
|
12846
13822
|
});
|
|
12847
|
-
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 } });
|
|
12848
13825
|
console.log(JSON.stringify(manifestRemove(id), null, 2));
|
|
12849
13826
|
});
|
|
12850
|
-
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) => {
|
|
12851
13828
|
const fromStdin = Boolean(options["fromStdin"] || options["from-stdin"]);
|
|
12852
13829
|
if (fromStdin) {
|
|
12853
13830
|
if (process.stdin.isTTY) {
|
|
12854
13831
|
console.error("error: --from-stdin requires piped input");
|
|
12855
13832
|
process.exit(1);
|
|
12856
13833
|
}
|
|
12857
|
-
const input =
|
|
13834
|
+
const input = readFileSync13(0, "utf8");
|
|
12858
13835
|
const machine2 = JSON.parse(input);
|
|
13836
|
+
requireCliMutation("manifest_add", typeof options["approvalToken"] === "string" ? options["approvalToken"] : undefined, { machineId: machine2.id, args: machine2 });
|
|
12859
13837
|
console.log(JSON.stringify(manifestAdd(machine2), null, 2));
|
|
12860
13838
|
return;
|
|
12861
13839
|
}
|
|
@@ -12894,6 +13872,7 @@ manifestCommand.command("add").description("Add or replace a machine in the flee
|
|
|
12894
13872
|
apps,
|
|
12895
13873
|
files
|
|
12896
13874
|
};
|
|
13875
|
+
requireCliMutation("manifest_add", typeof options["approvalToken"] === "string" ? options["approvalToken"] : undefined, { machineId: machine.id, args: machine });
|
|
12897
13876
|
console.log(JSON.stringify(manifestAdd(machine), null, 2));
|
|
12898
13877
|
});
|
|
12899
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) => {
|
|
@@ -12912,16 +13891,47 @@ appsCommand.command("plan").description("Preview app install steps for a machine
|
|
|
12912
13891
|
const result = buildAppsPlan(options.machine);
|
|
12913
13892
|
console.log(JSON.stringify(result, null, 2));
|
|
12914
13893
|
});
|
|
12915
|
-
appsCommand.command("apply").description("Install manifest-managed apps for a machine").option("--machine <id>", "Machine identifier").option("--yes", "Confirm execution", false).action((options) => {
|
|
12916
|
-
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 });
|
|
12917
13903
|
console.log(JSON.stringify(result, null, 2));
|
|
12918
13904
|
});
|
|
12919
|
-
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) => {
|
|
12920
|
-
|
|
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);
|
|
12921
13919
|
console.log(JSON.stringify(result, null, 2));
|
|
12922
13920
|
});
|
|
12923
|
-
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) => {
|
|
12924
|
-
|
|
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);
|
|
12925
13935
|
console.log(JSON.stringify(result, null, 2));
|
|
12926
13936
|
});
|
|
12927
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) => {
|
|
@@ -12987,7 +13997,19 @@ workspaceCommand.command("doctor").description("Diagnose repo and open-files wor
|
|
|
12987
13997
|
if (result.diagnostics.some((entry) => entry.severity === "fail") && !options.json)
|
|
12988
13998
|
process.exitCode = 1;
|
|
12989
13999
|
});
|
|
12990
|
-
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
|
+
} });
|
|
12991
14013
|
const result = repairWorkspaceManifestMappings({
|
|
12992
14014
|
machineId: options.machine,
|
|
12993
14015
|
projectId: options.project,
|
|
@@ -13008,18 +14030,26 @@ program2.command("diff").description("Show manifest differences between two mach
|
|
|
13008
14030
|
const result = diffMachines(options.left, options.right);
|
|
13009
14031
|
console.log(JSON.stringify(result, null, 2));
|
|
13010
14032
|
});
|
|
13011
|
-
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
|
+
}
|
|
13012
14038
|
const result = options.apply ? runBackup(options.bucket, options.prefix, { apply: true, yes: options.yes }) : buildBackupPlan(options.bucket, options.prefix);
|
|
13013
14039
|
console.log(JSON.stringify(result, null, 2));
|
|
13014
14040
|
});
|
|
13015
14041
|
var certCommand = program2.command("cert").description("Manage mkcert-based local SSL certificates");
|
|
13016
|
-
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 } });
|
|
13017
14045
|
const result = options.apply ? runCertPlan(domains, { apply: true, yes: options.yes }) : buildCertPlan(domains);
|
|
13018
14046
|
console.log(JSON.stringify(result, null, 2));
|
|
13019
14047
|
});
|
|
13020
14048
|
var dnsCommand = program2.command("dns").description("Manage local domain mappings");
|
|
13021
|
-
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) => {
|
|
13022
|
-
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);
|
|
13023
14053
|
console.log(JSON.stringify(result, null, 2));
|
|
13024
14054
|
});
|
|
13025
14055
|
dnsCommand.command("list").description("List saved local domain mappings").option("-j, --json", "Print JSON output", false).action(() => {
|
|
@@ -13028,43 +14058,91 @@ dnsCommand.command("list").description("List saved local domain mappings").optio
|
|
|
13028
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) => {
|
|
13029
14059
|
console.log(JSON.stringify(renderDomainMapping(domain), null, 2));
|
|
13030
14060
|
});
|
|
13031
|
-
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 } });
|
|
13032
14066
|
const result = addNotificationChannel({
|
|
13033
14067
|
id: options.id,
|
|
13034
14068
|
type: options.type,
|
|
13035
14069
|
target: options.target,
|
|
13036
|
-
|
|
13037
|
-
|
|
13038
|
-
|
|
14070
|
+
commandArgs: options.type === "command" && commandArgs.length > 0 ? commandArgs : undefined,
|
|
14071
|
+
events,
|
|
14072
|
+
enabled
|
|
14073
|
+
}, { trustedApproval: trustedNotificationApproval2 });
|
|
13039
14074
|
printJsonOrText(result, renderNotificationConfigResult(result), options.json);
|
|
13040
14075
|
});
|
|
13041
14076
|
notificationsCommand.command("list").description("List configured notification channels").option("-j, --json", "Print JSON output", false).action((options) => {
|
|
13042
14077
|
const result = listNotificationChannels();
|
|
13043
14078
|
printJsonOrText(result, renderNotificationConfigResult(result), options.json);
|
|
13044
14079
|
});
|
|
13045
|
-
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 } });
|
|
13046
14083
|
const result = await testNotificationChannel(options.channel, options.event, options.message, {
|
|
13047
14084
|
apply: options.apply,
|
|
13048
|
-
yes: options.yes
|
|
14085
|
+
yes: options.yes,
|
|
14086
|
+
trustedApproval: options.apply === true ? trustedNotificationApproval2 : undefined
|
|
13049
14087
|
});
|
|
13050
14088
|
printJsonOrText(result, renderNotificationTestResult(result), options.json);
|
|
13051
14089
|
});
|
|
13052
|
-
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) => {
|
|
13053
|
-
|
|
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 });
|
|
13054
14093
|
printJsonOrText(result, renderNotificationDispatchResult(result), options.json);
|
|
13055
14094
|
});
|
|
13056
|
-
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 } });
|
|
13057
14097
|
const result = removeNotificationChannel(id);
|
|
13058
14098
|
printJsonOrText(result, renderNotificationConfigResult(result), options.json);
|
|
13059
14099
|
});
|
|
13060
|
-
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 });
|
|
13061
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
|
+
}
|
|
13062
14139
|
const result = await watchTmuxPane({
|
|
13063
|
-
target,
|
|
13064
|
-
intervalMs
|
|
14140
|
+
target: normalizedTarget,
|
|
14141
|
+
intervalMs,
|
|
13065
14142
|
maxChecks,
|
|
13066
|
-
emitInitialMissing:
|
|
13067
|
-
deliver
|
|
14143
|
+
emitInitialMissing: once,
|
|
14144
|
+
deliver,
|
|
14145
|
+
tmuxCommand,
|
|
13068
14146
|
onProbe: options.json ? undefined : (probe) => {
|
|
13069
14147
|
const status = probe.exists ? source_default.green("present") : source_default.yellow("missing");
|
|
13070
14148
|
console.error(`tmux ${probe.target}: ${status}${probe.paneId ? ` ${probe.paneId}` : ""}`);
|
|
@@ -13133,7 +14211,7 @@ clipboardCommand.command("clear-history").description("Clear clipboard sync hist
|
|
|
13133
14211
|
});
|
|
13134
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) => {
|
|
13135
14213
|
if (options.rotate) {
|
|
13136
|
-
|
|
14214
|
+
rmSync3(getClipboardKeyPath(), { force: true });
|
|
13137
14215
|
}
|
|
13138
14216
|
const key = getOrCreateClipboardKey();
|
|
13139
14217
|
printJsonOrText({ key }, key, options.json);
|
|
@@ -13158,12 +14236,31 @@ installClaudeCommand.command("plan").description("Preview CLI install steps").op
|
|
|
13158
14236
|
const result = buildClaudeInstallPlan(options.machine, options.tool);
|
|
13159
14237
|
console.log(JSON.stringify(result, null, 2));
|
|
13160
14238
|
});
|
|
13161
|
-
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) => {
|
|
13162
|
-
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 });
|
|
13163
14248
|
console.log(JSON.stringify(result, null, 2));
|
|
13164
14249
|
});
|
|
13165
|
-
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) => {
|
|
13166
|
-
|
|
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);
|
|
13167
14264
|
console.log(JSON.stringify(result, null, 2));
|
|
13168
14265
|
});
|
|
13169
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) => {
|
|
@@ -13315,28 +14412,34 @@ storageCommand.command("status").description("Show storage sync status").option(
|
|
|
13315
14412
|
["tables", status.tables.join(", ")]
|
|
13316
14413
|
]), options.json);
|
|
13317
14414
|
});
|
|
13318
|
-
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) => {
|
|
13319
14416
|
try {
|
|
13320
|
-
const { parseStorageTables: parseStorageTables2, storagePush: storagePush2 } = await Promise.resolve().then(() => (init_storage(), exports_storage));
|
|
13321
|
-
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 });
|
|
13322
14421
|
printStorageResults(results, options.json);
|
|
13323
14422
|
} catch (error) {
|
|
13324
14423
|
printStorageError(error);
|
|
13325
14424
|
}
|
|
13326
14425
|
});
|
|
13327
|
-
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) => {
|
|
13328
14427
|
try {
|
|
13329
|
-
const { parseStorageTables: parseStorageTables2, storagePull: storagePull2 } = await Promise.resolve().then(() => (init_storage(), exports_storage));
|
|
13330
|
-
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 });
|
|
13331
14432
|
printStorageResults(results, options.json);
|
|
13332
14433
|
} catch (error) {
|
|
13333
14434
|
printStorageError(error);
|
|
13334
14435
|
}
|
|
13335
14436
|
});
|
|
13336
|
-
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) => {
|
|
13337
14438
|
try {
|
|
13338
|
-
const { parseStorageTables: parseStorageTables2, storageSync: storageSync2 } = await Promise.resolve().then(() => (init_storage(), exports_storage));
|
|
13339
|
-
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 });
|
|
13340
14443
|
if (options.json) {
|
|
13341
14444
|
console.log(JSON.stringify(result, null, 2));
|
|
13342
14445
|
return;
|
|
@@ -13361,7 +14464,7 @@ program2.command("self-test").description("Run local package smoke checks").opti
|
|
|
13361
14464
|
const result = runSelfTest();
|
|
13362
14465
|
printJsonOrText(result, renderSelfTestResult(result), options.json);
|
|
13363
14466
|
});
|
|
13364
|
-
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) => {
|
|
13365
14468
|
const info = getServeInfo({ host: options.host, port: parseIntegerOption(options.port, "port", { min: 1, max: 65535 }) });
|
|
13366
14469
|
if (options.json) {
|
|
13367
14470
|
console.log(JSON.stringify(info, null, 2));
|