@hasna/machines 0.0.29 → 0.0.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +1036 -1038
- package/dist/commands/apps.d.ts +4 -3
- package/dist/commands/apps.d.ts.map +1 -1
- package/dist/commands/install-claude.d.ts +4 -3
- package/dist/commands/install-claude.d.ts.map +1 -1
- package/dist/commands/install-tailscale.d.ts +2 -1
- package/dist/commands/install-tailscale.d.ts.map +1 -1
- package/dist/commands/setup.d.ts +2 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/sync.d.ts +3 -2
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/consumer.js +11 -0
- package/dist/index.js +63 -67
- package/dist/mcp/index.js +63 -67
- package/dist/remote.d.ts +3 -0
- package/dist/remote.d.ts.map +1 -1
- package/package.json +3 -3
package/dist/cli/index.js
CHANGED
|
@@ -7912,996 +7912,610 @@ function manifestValidate() {
|
|
|
7912
7912
|
// src/commands/setup.ts
|
|
7913
7913
|
import { homedir as homedir3 } from "os";
|
|
7914
7914
|
init_db();
|
|
7915
|
-
|
|
7916
|
-
|
|
7915
|
+
|
|
7916
|
+
// src/remote.ts
|
|
7917
|
+
init_db();
|
|
7918
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
7919
|
+
import { hostname as hostname4 } from "os";
|
|
7920
|
+
|
|
7921
|
+
// src/topology.ts
|
|
7922
|
+
init_db();
|
|
7923
|
+
import { existsSync as existsSync5 } from "fs";
|
|
7924
|
+
import { arch as arch2, hostname as hostname3, platform as platform2, userInfo as userInfo2 } from "os";
|
|
7925
|
+
import { spawnSync } from "child_process";
|
|
7926
|
+
init_paths();
|
|
7927
|
+
var MACHINES_CONSUMER_CONTRACT_VERSION = 1;
|
|
7928
|
+
var MACHINES_PACKAGE_NAME = "@hasna/machines";
|
|
7929
|
+
var DEFAULT_MACHINE_RESOLVER_TTL_MS = 24 * 60 * 60 * 1000;
|
|
7930
|
+
var MACHINES_CONSUMER_CAPABILITIES = {
|
|
7931
|
+
topology: true,
|
|
7932
|
+
compatibility: true,
|
|
7933
|
+
route_resolution: true,
|
|
7934
|
+
cli_json_fallback: true,
|
|
7935
|
+
workspace_path_mapping: true,
|
|
7936
|
+
workspace_diagnostics: true,
|
|
7937
|
+
schema_artifacts: true,
|
|
7938
|
+
cacheability_metadata: true,
|
|
7939
|
+
resolver_snapshots: true,
|
|
7940
|
+
field_capability_descriptors: true
|
|
7941
|
+
};
|
|
7942
|
+
function getMachinesConsumerCapabilities() {
|
|
7943
|
+
return { ...MACHINES_CONSUMER_CAPABILITIES };
|
|
7917
7944
|
}
|
|
7918
|
-
function
|
|
7919
|
-
const
|
|
7920
|
-
|
|
7921
|
-
|
|
7922
|
-
|
|
7923
|
-
|
|
7924
|
-
|
|
7925
|
-
|
|
7926
|
-
|
|
7927
|
-
id: "bun",
|
|
7928
|
-
title: "Install Bun if missing",
|
|
7929
|
-
command: "command -v bun >/dev/null 2>&1 || curl -fsSL https://bun.sh/install | bash",
|
|
7930
|
-
manager: "shell"
|
|
7931
|
-
}
|
|
7932
|
-
];
|
|
7933
|
-
if (machine.platform === "linux") {
|
|
7934
|
-
steps.push({
|
|
7935
|
-
id: "apt-base",
|
|
7936
|
-
title: "Install core Linux tooling",
|
|
7937
|
-
command: "sudo apt-get update && sudo apt-get install -y git curl unzip build-essential",
|
|
7938
|
-
manager: "apt",
|
|
7939
|
-
privileged: true
|
|
7940
|
-
});
|
|
7941
|
-
} else if (machine.platform === "macos") {
|
|
7942
|
-
steps.push({
|
|
7943
|
-
id: "brew-base",
|
|
7944
|
-
title: "Install Homebrew if missing",
|
|
7945
|
-
command: 'command -v brew >/dev/null 2>&1 || /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"',
|
|
7946
|
-
manager: "brew"
|
|
7947
|
-
});
|
|
7948
|
-
steps.push({
|
|
7949
|
-
id: "brew-core",
|
|
7950
|
-
title: "Install core macOS tooling",
|
|
7951
|
-
command: "brew install git coreutils",
|
|
7952
|
-
manager: "brew"
|
|
7953
|
-
});
|
|
7954
|
-
}
|
|
7955
|
-
return steps;
|
|
7945
|
+
function normalizePlatform2(value = platform2()) {
|
|
7946
|
+
const normalized = value.toLowerCase();
|
|
7947
|
+
if (normalized === "darwin" || normalized === "macos")
|
|
7948
|
+
return "macos";
|
|
7949
|
+
if (normalized === "win32" || normalized === "windows")
|
|
7950
|
+
return "windows";
|
|
7951
|
+
if (normalized === "linux")
|
|
7952
|
+
return "linux";
|
|
7953
|
+
return value;
|
|
7956
7954
|
}
|
|
7957
|
-
function
|
|
7958
|
-
|
|
7959
|
-
|
|
7960
|
-
|
|
7961
|
-
if (manager === "bun") {
|
|
7962
|
-
command = `bun install -g ${quote(pkg.name)}`;
|
|
7963
|
-
} else if (manager === "brew") {
|
|
7964
|
-
command = `brew install ${quote(pkg.name)}`;
|
|
7965
|
-
} else if (manager === "apt") {
|
|
7966
|
-
command = `sudo apt-get install -y ${quote(pkg.name)}`;
|
|
7967
|
-
}
|
|
7968
|
-
return {
|
|
7969
|
-
id: `package-${index + 1}`,
|
|
7970
|
-
title: `Install package ${pkg.name}`,
|
|
7971
|
-
command,
|
|
7972
|
-
manager,
|
|
7973
|
-
privileged: manager === "apt"
|
|
7974
|
-
};
|
|
7955
|
+
function defaultRunner(command) {
|
|
7956
|
+
const result = spawnSync("bash", ["-c", command], {
|
|
7957
|
+
encoding: "utf8",
|
|
7958
|
+
env: process.env
|
|
7975
7959
|
});
|
|
7976
|
-
}
|
|
7977
|
-
function buildSetupPlan(machineId) {
|
|
7978
|
-
const manifest = readManifest();
|
|
7979
|
-
const currentMachineId = getLocalMachineId();
|
|
7980
|
-
const selected = machineId ? manifest.machines.find((machine) => machine.id === machineId) : manifest.machines.find((machine) => machine.id === currentMachineId);
|
|
7981
|
-
const target = selected || {
|
|
7982
|
-
id: currentMachineId,
|
|
7983
|
-
platform: "linux",
|
|
7984
|
-
workspacePath: `${homedir3()}/workspace`
|
|
7985
|
-
};
|
|
7986
|
-
const steps = [...buildBaseSteps(target), ...buildPackageSteps(target)];
|
|
7987
7960
|
return {
|
|
7988
|
-
|
|
7989
|
-
|
|
7990
|
-
|
|
7991
|
-
executed: 0
|
|
7961
|
+
stdout: result.stdout || "",
|
|
7962
|
+
stderr: result.stderr || "",
|
|
7963
|
+
exitCode: result.status ?? 1
|
|
7992
7964
|
};
|
|
7993
7965
|
}
|
|
7994
|
-
function
|
|
7995
|
-
|
|
7996
|
-
|
|
7997
|
-
|
|
7966
|
+
function hasCommand(command, runner) {
|
|
7967
|
+
return runner(`command -v ${command} >/dev/null 2>&1`).exitCode === 0;
|
|
7968
|
+
}
|
|
7969
|
+
function parseTailscaleStatus(raw) {
|
|
7970
|
+
try {
|
|
7971
|
+
const parsed = JSON.parse(raw);
|
|
7972
|
+
if (!parsed || typeof parsed !== "object")
|
|
7973
|
+
return null;
|
|
7974
|
+
return parsed;
|
|
7975
|
+
} catch {
|
|
7976
|
+
return null;
|
|
7998
7977
|
}
|
|
7999
|
-
|
|
8000
|
-
|
|
7978
|
+
}
|
|
7979
|
+
function loadTailscalePeers(runner, warnings) {
|
|
7980
|
+
const peers = new Map;
|
|
7981
|
+
if (!hasCommand("tailscale", runner)) {
|
|
7982
|
+
warnings.push("tailscale_not_available");
|
|
7983
|
+
return peers;
|
|
8001
7984
|
}
|
|
8002
|
-
|
|
8003
|
-
|
|
8004
|
-
|
|
8005
|
-
|
|
8006
|
-
stderr: "pipe",
|
|
8007
|
-
env: process.env
|
|
8008
|
-
});
|
|
8009
|
-
if (result.exitCode !== 0) {
|
|
8010
|
-
recordSetupRun(plan.machineId, "failed", {
|
|
8011
|
-
executed,
|
|
8012
|
-
failedStep: step,
|
|
8013
|
-
stderr: result.stderr.toString()
|
|
8014
|
-
});
|
|
8015
|
-
throw new Error(`Setup step failed (${step.id}): ${result.stderr.toString().trim()}`);
|
|
8016
|
-
}
|
|
8017
|
-
executed += 1;
|
|
7985
|
+
const result = runner("tailscale status --json");
|
|
7986
|
+
if (result.exitCode !== 0) {
|
|
7987
|
+
warnings.push(`tailscale_status_failed:${result.stderr.trim() || result.exitCode}`);
|
|
7988
|
+
return peers;
|
|
8018
7989
|
}
|
|
8019
|
-
const
|
|
8020
|
-
|
|
8021
|
-
|
|
8022
|
-
|
|
8023
|
-
|
|
7990
|
+
const status = parseTailscaleStatus(result.stdout);
|
|
7991
|
+
if (!status) {
|
|
7992
|
+
warnings.push("tailscale_status_invalid_json");
|
|
7993
|
+
return peers;
|
|
7994
|
+
}
|
|
7995
|
+
const addPeer = (peer) => {
|
|
7996
|
+
if (!peer)
|
|
7997
|
+
return;
|
|
7998
|
+
const id = peer.HostName || peer.DNSName?.split(".")[0];
|
|
7999
|
+
if (id)
|
|
8000
|
+
peers.set(id, peer);
|
|
8024
8001
|
};
|
|
8025
|
-
|
|
8026
|
-
|
|
8002
|
+
addPeer(status.Self);
|
|
8003
|
+
for (const peer of Object.values(status.Peer ?? {}))
|
|
8004
|
+
addPeer(peer);
|
|
8005
|
+
return peers;
|
|
8027
8006
|
}
|
|
8028
|
-
|
|
8029
|
-
|
|
8030
|
-
|
|
8031
|
-
|
|
8032
|
-
|
|
8033
|
-
|
|
8034
|
-
|
|
8035
|
-
|
|
8036
|
-
var DEFAULT_BACKUP_PREFIX = "machines";
|
|
8037
|
-
function quote2(value) {
|
|
8038
|
-
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
8007
|
+
function machineKeys(machine) {
|
|
8008
|
+
return [
|
|
8009
|
+
machine.id,
|
|
8010
|
+
machine.hostname,
|
|
8011
|
+
machine.tailscaleName?.split(".")[0],
|
|
8012
|
+
machine.tailscaleName,
|
|
8013
|
+
machine.sshAddress?.split("@").pop()
|
|
8014
|
+
].filter((value) => Boolean(value));
|
|
8039
8015
|
}
|
|
8040
|
-
function
|
|
8041
|
-
|
|
8042
|
-
|
|
8016
|
+
function findTailscalePeer(machine, machineId, peers) {
|
|
8017
|
+
if (machine) {
|
|
8018
|
+
for (const key of machineKeys(machine)) {
|
|
8019
|
+
const peer = peers.get(key) ?? peers.get(key.replace(/\.$/, ""));
|
|
8020
|
+
if (peer)
|
|
8021
|
+
return peer;
|
|
8022
|
+
}
|
|
8023
|
+
}
|
|
8024
|
+
return peers.get(machineId) ?? null;
|
|
8043
8025
|
}
|
|
8044
|
-
function
|
|
8045
|
-
const
|
|
8046
|
-
|
|
8047
|
-
return { bucket: primary, bucketSource: MACHINES_BACKUP_BUCKET_ENV };
|
|
8048
|
-
const fallback = readEnv(MACHINES_BACKUP_BUCKET_FALLBACK_ENV);
|
|
8049
|
-
if (fallback)
|
|
8050
|
-
return { bucket: fallback, bucketSource: MACHINES_BACKUP_BUCKET_FALLBACK_ENV };
|
|
8051
|
-
return null;
|
|
8026
|
+
function envReachableHosts() {
|
|
8027
|
+
const raw = process.env["HASNA_MACHINES_REACHABLE_HOSTS"];
|
|
8028
|
+
return new Set((raw || "").split(",").map((value) => value.trim()).filter(Boolean));
|
|
8052
8029
|
}
|
|
8053
|
-
function
|
|
8054
|
-
const
|
|
8055
|
-
if (
|
|
8056
|
-
return
|
|
8057
|
-
|
|
8058
|
-
if (fallback)
|
|
8059
|
-
return { prefix: fallback, prefixSource: MACHINES_BACKUP_PREFIX_FALLBACK_ENV };
|
|
8060
|
-
return null;
|
|
8030
|
+
function manifestHostReachable(target) {
|
|
8031
|
+
const overrides = envReachableHosts();
|
|
8032
|
+
if (overrides.size === 0)
|
|
8033
|
+
return null;
|
|
8034
|
+
return overrides.has(target);
|
|
8061
8035
|
}
|
|
8062
|
-
function
|
|
8063
|
-
const
|
|
8064
|
-
|
|
8065
|
-
|
|
8066
|
-
if (!bucket) {
|
|
8067
|
-
throw new Error(`Missing S3 backup bucket. Pass --bucket or set ${MACHINES_BACKUP_BUCKET_ENV} or ${MACHINES_BACKUP_BUCKET_FALLBACK_ENV}.`);
|
|
8036
|
+
function routeHints(input) {
|
|
8037
|
+
const hints = [];
|
|
8038
|
+
if (input.machineId === input.localMachineId) {
|
|
8039
|
+
hints.push({ kind: "local", target: "localhost", reachable: true });
|
|
8068
8040
|
}
|
|
8069
|
-
|
|
8070
|
-
|
|
8071
|
-
return {
|
|
8072
|
-
bucket,
|
|
8073
|
-
prefix: explicitPrefix || envPrefix?.prefix || DEFAULT_BACKUP_PREFIX,
|
|
8074
|
-
bucketSource: explicitBucket ? "argument" : envBucket.bucketSource,
|
|
8075
|
-
prefixSource: explicitPrefix ? "argument" : envPrefix?.prefixSource || "default"
|
|
8076
|
-
};
|
|
8077
|
-
}
|
|
8078
|
-
function defaultBackupSources() {
|
|
8079
|
-
const home = homedir4();
|
|
8080
|
-
return [
|
|
8081
|
-
join4(home, ".hasna"),
|
|
8082
|
-
join4(home, ".ssh"),
|
|
8083
|
-
join4(home, ".secrets")
|
|
8084
|
-
];
|
|
8085
|
-
}
|
|
8086
|
-
function buildBackupPlan(bucket, prefix) {
|
|
8087
|
-
const target = resolveBackupTarget({ bucket, prefix });
|
|
8088
|
-
const archivePath = join4(homedir4(), ".hasna", "machines", "backup.tgz");
|
|
8089
|
-
const sources = defaultBackupSources();
|
|
8090
|
-
const steps = [
|
|
8091
|
-
{
|
|
8092
|
-
id: "backup-archive",
|
|
8093
|
-
title: "Create compressed machine backup archive",
|
|
8094
|
-
command: `tar -czf ${quote2(archivePath)} ${sources.map((source) => quote2(source)).join(" ")}`,
|
|
8095
|
-
manager: "shell"
|
|
8096
|
-
},
|
|
8097
|
-
{
|
|
8098
|
-
id: "backup-upload",
|
|
8099
|
-
title: "Upload archive to S3",
|
|
8100
|
-
command: `aws s3 cp ${quote2(archivePath)} ${quote2(`s3://${target.bucket}/${target.prefix}/${hostname3()}-backup.tgz`)}`,
|
|
8101
|
-
manager: "custom"
|
|
8102
|
-
}
|
|
8103
|
-
];
|
|
8104
|
-
return {
|
|
8105
|
-
machineId: process.env["HASNA_MACHINES_MACHINE_ID"] || "local",
|
|
8106
|
-
mode: "plan",
|
|
8107
|
-
steps,
|
|
8108
|
-
executed: 0
|
|
8109
|
-
};
|
|
8110
|
-
}
|
|
8111
|
-
function runBackup(bucket, prefix, options = {}) {
|
|
8112
|
-
const plan = buildBackupPlan(bucket, prefix);
|
|
8113
|
-
if (!options.apply)
|
|
8114
|
-
return plan;
|
|
8115
|
-
if (!options.yes) {
|
|
8116
|
-
throw new Error("Backup execution requires --yes.");
|
|
8041
|
+
if (input.manifest?.sshAddress) {
|
|
8042
|
+
hints.push({ kind: "ssh", target: input.manifest.sshAddress, reachable: manifestHostReachable(input.manifest.sshAddress) });
|
|
8117
8043
|
}
|
|
8118
|
-
|
|
8119
|
-
|
|
8120
|
-
const result = Bun.spawnSync(["bash", "-lc", step.command], {
|
|
8121
|
-
stdout: "pipe",
|
|
8122
|
-
stderr: "pipe",
|
|
8123
|
-
env: process.env
|
|
8124
|
-
});
|
|
8125
|
-
if (result.exitCode !== 0) {
|
|
8126
|
-
throw new Error(`Backup step failed (${step.id}): ${result.stderr.toString().trim()}`);
|
|
8127
|
-
}
|
|
8128
|
-
executed += 1;
|
|
8044
|
+
if (input.manifest?.hostname) {
|
|
8045
|
+
hints.push({ kind: "lan", target: input.manifest.hostname, reachable: manifestHostReachable(input.manifest.hostname) });
|
|
8129
8046
|
}
|
|
8130
|
-
|
|
8131
|
-
|
|
8132
|
-
|
|
8133
|
-
|
|
8134
|
-
|
|
8135
|
-
};
|
|
8047
|
+
const tailscaleTarget = input.manifest?.tailscaleName ?? input.peer?.DNSName ?? input.peer?.TailscaleIPs?.[0];
|
|
8048
|
+
if (tailscaleTarget) {
|
|
8049
|
+
hints.push({ kind: "tailscale", target: tailscaleTarget.replace(/\.$/, ""), reachable: input.peer?.Online ?? null });
|
|
8050
|
+
}
|
|
8051
|
+
return hints;
|
|
8136
8052
|
}
|
|
8137
|
-
|
|
8138
|
-
|
|
8139
|
-
|
|
8140
|
-
|
|
8141
|
-
|
|
8142
|
-
|
|
8053
|
+
function routeRank(hint) {
|
|
8054
|
+
if (hint.kind === "local")
|
|
8055
|
+
return 0;
|
|
8056
|
+
if (hint.reachable === true && hint.kind === "ssh")
|
|
8057
|
+
return 1;
|
|
8058
|
+
if (hint.reachable === true && hint.kind === "lan")
|
|
8059
|
+
return 2;
|
|
8060
|
+
if (hint.reachable === true && hint.kind === "tailscale")
|
|
8061
|
+
return 3;
|
|
8062
|
+
if (hint.reachable === false)
|
|
8063
|
+
return 8;
|
|
8064
|
+
if (hint.kind === "ssh")
|
|
8065
|
+
return 4;
|
|
8066
|
+
if (hint.kind === "lan")
|
|
8067
|
+
return 5;
|
|
8068
|
+
if (hint.kind === "tailscale")
|
|
8069
|
+
return 6;
|
|
8070
|
+
return 9;
|
|
8143
8071
|
}
|
|
8144
|
-
function
|
|
8145
|
-
return
|
|
8072
|
+
function selectRouteHint(hints) {
|
|
8073
|
+
return [...hints].sort((left, right) => routeRank(left) - routeRank(right))[0] ?? null;
|
|
8146
8074
|
}
|
|
8147
|
-
function
|
|
8148
|
-
|
|
8149
|
-
|
|
8150
|
-
|
|
8151
|
-
|
|
8152
|
-
|
|
8153
|
-
|
|
8154
|
-
|
|
8155
|
-
if (platform2() === "darwin") {
|
|
8156
|
-
steps.push({
|
|
8157
|
-
id: "mkcert-install-macos",
|
|
8158
|
-
title: "Install mkcert on macOS",
|
|
8159
|
-
command: "brew install mkcert nss",
|
|
8160
|
-
manager: "brew"
|
|
8161
|
-
});
|
|
8162
|
-
} else {
|
|
8163
|
-
steps.push({
|
|
8164
|
-
id: "mkcert-install-linux",
|
|
8165
|
-
title: "Install mkcert on Linux",
|
|
8166
|
-
command: "sudo apt-get update && sudo apt-get install -y mkcert libnss3-tools",
|
|
8167
|
-
manager: "apt",
|
|
8168
|
-
privileged: true
|
|
8169
|
-
});
|
|
8170
|
-
}
|
|
8171
|
-
steps.push({
|
|
8172
|
-
id: "mkcert-local-ca",
|
|
8173
|
-
title: "Install local mkcert CA",
|
|
8174
|
-
command: "mkcert -install",
|
|
8175
|
-
manager: "custom"
|
|
8176
|
-
}, {
|
|
8177
|
-
id: "mkcert-issue",
|
|
8178
|
-
title: `Issue certificate for ${domains.join(", ")}`,
|
|
8179
|
-
command: `mkdir -p ${quote3(certDir())} && mkcert -cert-file ${quote3(certPath)} -key-file ${quote3(keyPath)} ${domains.map((domain) => quote3(domain)).join(" ")}`,
|
|
8180
|
-
manager: "custom"
|
|
8075
|
+
function buildEntry(input) {
|
|
8076
|
+
const manifest = input.manifest;
|
|
8077
|
+
const peer = input.peer;
|
|
8078
|
+
const hints = routeHints({
|
|
8079
|
+
machineId: input.machineId,
|
|
8080
|
+
localMachineId: input.localMachineId,
|
|
8081
|
+
manifest,
|
|
8082
|
+
peer
|
|
8181
8083
|
});
|
|
8084
|
+
const selectedRoute = selectRouteHint(hints);
|
|
8085
|
+
const route = selectedRoute?.kind === "ssh" ? "ssh" : selectedRoute?.kind ?? "unknown";
|
|
8182
8086
|
return {
|
|
8183
|
-
|
|
8184
|
-
|
|
8185
|
-
|
|
8186
|
-
|
|
8087
|
+
machine_id: input.machineId,
|
|
8088
|
+
hostname: manifest?.hostname ?? peer?.HostName ?? null,
|
|
8089
|
+
platform: manifest?.platform ?? (peer?.OS ? normalizePlatform2(peer.OS) : null),
|
|
8090
|
+
os: peer?.OS ?? null,
|
|
8091
|
+
user: typeof manifest?.metadata?.user === "string" ? manifest.metadata.user : null,
|
|
8092
|
+
workspace_path: manifest?.workspacePath ?? null,
|
|
8093
|
+
manifest_declared: Boolean(manifest),
|
|
8094
|
+
heartbeat_status: input.heartbeat?.status ?? "unknown",
|
|
8095
|
+
last_heartbeat_at: input.heartbeat?.updated_at ?? null,
|
|
8096
|
+
tailscale: {
|
|
8097
|
+
dns_name: manifest?.tailscaleName ?? peer?.DNSName?.replace(/\.$/, "") ?? null,
|
|
8098
|
+
ips: peer?.TailscaleIPs ?? [],
|
|
8099
|
+
online: peer?.Online ?? null,
|
|
8100
|
+
active: peer?.Active ?? null,
|
|
8101
|
+
last_seen: peer?.LastSeen ?? null
|
|
8102
|
+
},
|
|
8103
|
+
ssh: {
|
|
8104
|
+
address: manifest?.sshAddress ?? null,
|
|
8105
|
+
route,
|
|
8106
|
+
command_target: selectedRoute?.target ?? null
|
|
8107
|
+
},
|
|
8108
|
+
route_hints: hints,
|
|
8109
|
+
tags: manifest?.tags ?? [],
|
|
8110
|
+
metadata: manifest?.metadata ?? {}
|
|
8187
8111
|
};
|
|
8188
8112
|
}
|
|
8189
|
-
function
|
|
8190
|
-
const
|
|
8191
|
-
|
|
8192
|
-
|
|
8193
|
-
|
|
8194
|
-
|
|
8195
|
-
|
|
8196
|
-
|
|
8197
|
-
|
|
8198
|
-
|
|
8199
|
-
|
|
8200
|
-
|
|
8201
|
-
|
|
8113
|
+
function discoverMachineTopology(options = {}) {
|
|
8114
|
+
const now2 = options.now ?? new Date;
|
|
8115
|
+
const runner = options.runner ?? defaultRunner;
|
|
8116
|
+
const warnings = [];
|
|
8117
|
+
const manifest = readManifest();
|
|
8118
|
+
const heartbeats = listHeartbeats();
|
|
8119
|
+
const heartbeatByMachine = new Map(heartbeats.map((heartbeat) => [heartbeat.machine_id, heartbeat]));
|
|
8120
|
+
const localMachineId = getLocalMachineId();
|
|
8121
|
+
const peers = options.includeTailscale === false ? new Map : loadTailscalePeers(runner, warnings);
|
|
8122
|
+
const machineIds = new Set([
|
|
8123
|
+
localMachineId,
|
|
8124
|
+
...manifest.machines.map((machine) => machine.id),
|
|
8125
|
+
...heartbeats.map((heartbeat) => heartbeat.machine_id),
|
|
8126
|
+
...peers.keys()
|
|
8127
|
+
]);
|
|
8128
|
+
const manifestById = new Map(manifest.machines.map((machine) => [machine.id, machine]));
|
|
8129
|
+
const machines = [...machineIds].sort().map((machineId) => {
|
|
8130
|
+
const manifestMachine = manifestById.get(machineId);
|
|
8131
|
+
return buildEntry({
|
|
8132
|
+
machineId,
|
|
8133
|
+
localMachineId,
|
|
8134
|
+
manifest: manifestMachine,
|
|
8135
|
+
peer: findTailscalePeer(manifestMachine ?? null, machineId, peers),
|
|
8136
|
+
heartbeat: heartbeatByMachine.get(machineId)
|
|
8202
8137
|
});
|
|
8203
|
-
|
|
8204
|
-
throw new Error(`Certificate step failed (${step.id}): ${result.stderr.toString().trim()}`);
|
|
8205
|
-
}
|
|
8206
|
-
executed += 1;
|
|
8207
|
-
}
|
|
8138
|
+
});
|
|
8208
8139
|
return {
|
|
8209
|
-
|
|
8210
|
-
|
|
8211
|
-
|
|
8212
|
-
|
|
8140
|
+
schema_version: MACHINES_CONSUMER_CONTRACT_VERSION,
|
|
8141
|
+
package: {
|
|
8142
|
+
name: MACHINES_PACKAGE_NAME,
|
|
8143
|
+
version: getPackageVersion()
|
|
8144
|
+
},
|
|
8145
|
+
capabilities: getMachinesConsumerCapabilities(),
|
|
8146
|
+
generated_at: now2.toISOString(),
|
|
8147
|
+
local_machine_id: localMachineId,
|
|
8148
|
+
local_hostname: hostname3(),
|
|
8149
|
+
current_platform: normalizePlatform2(),
|
|
8150
|
+
manifest_path_known: existsSync5(getManifestPath()),
|
|
8151
|
+
machines,
|
|
8152
|
+
warnings
|
|
8213
8153
|
};
|
|
8214
8154
|
}
|
|
8215
|
-
|
|
8216
|
-
|
|
8217
|
-
init_paths();
|
|
8218
|
-
import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
8219
|
-
import { join as join6 } from "path";
|
|
8220
|
-
function getDnsPath() {
|
|
8221
|
-
return join6(getDataDir(), "dns.json");
|
|
8222
|
-
}
|
|
8223
|
-
function readMappings() {
|
|
8224
|
-
const path = getDnsPath();
|
|
8225
|
-
if (!existsSync5(path))
|
|
8226
|
-
return [];
|
|
8227
|
-
return JSON.parse(readFileSync3(path, "utf8"));
|
|
8228
|
-
}
|
|
8229
|
-
function writeMappings(mappings) {
|
|
8230
|
-
const path = getDnsPath();
|
|
8231
|
-
ensureParentDir(path);
|
|
8232
|
-
writeFileSync2(path, `${JSON.stringify(mappings, null, 2)}
|
|
8233
|
-
`, "utf8");
|
|
8234
|
-
return path;
|
|
8235
|
-
}
|
|
8236
|
-
function addDomainMapping(domain, port, targetHost = "127.0.0.1") {
|
|
8237
|
-
const mappings = readMappings().filter((entry) => entry.domain !== domain);
|
|
8238
|
-
mappings.push({ domain, port, targetHost });
|
|
8239
|
-
writeMappings(mappings);
|
|
8240
|
-
return mappings.sort((left, right) => left.domain.localeCompare(right.domain));
|
|
8155
|
+
function normalizeMachineAlias(value) {
|
|
8156
|
+
return value.trim().replace(/\.$/, "").toLowerCase();
|
|
8241
8157
|
}
|
|
8242
|
-
function
|
|
8243
|
-
|
|
8158
|
+
function routeTargetMatches(machine, requested) {
|
|
8159
|
+
const normalized = normalizeMachineAlias(requested);
|
|
8160
|
+
const values = [
|
|
8161
|
+
machine.ssh.address,
|
|
8162
|
+
machine.ssh.command_target,
|
|
8163
|
+
machine.tailscale.dns_name,
|
|
8164
|
+
machine.tailscale.dns_name?.split(".")[0],
|
|
8165
|
+
...machine.tailscale.ips,
|
|
8166
|
+
...machine.route_hints.map((hint) => hint.target),
|
|
8167
|
+
...machine.route_hints.map((hint) => hint.target.split("@").pop() ?? hint.target)
|
|
8168
|
+
].filter((value) => Boolean(value));
|
|
8169
|
+
return values.some((value) => normalizeMachineAlias(value) === normalized);
|
|
8244
8170
|
}
|
|
8245
|
-
function
|
|
8246
|
-
const
|
|
8247
|
-
if (
|
|
8248
|
-
|
|
8171
|
+
function findRouteMachine(topology, requestedMachineId) {
|
|
8172
|
+
const requested = normalizeMachineAlias(requestedMachineId);
|
|
8173
|
+
if (requested === "local" || requested === "localhost" || requested === normalizeMachineAlias(hostname3()) || requested === normalizeMachineAlias(topology.local_machine_id)) {
|
|
8174
|
+
return {
|
|
8175
|
+
machine: topology.machines.find((machine) => machine.machine_id === topology.local_machine_id) ?? null,
|
|
8176
|
+
matchedBy: "local_alias"
|
|
8177
|
+
};
|
|
8249
8178
|
}
|
|
8250
|
-
|
|
8251
|
-
|
|
8252
|
-
|
|
8253
|
-
|
|
8254
|
-
|
|
8255
|
-
}
|
|
8256
|
-
|
|
8257
|
-
|
|
8258
|
-
|
|
8179
|
+
const machineIdMatch = topology.machines.find((machine) => normalizeMachineAlias(machine.machine_id) === requested);
|
|
8180
|
+
if (machineIdMatch)
|
|
8181
|
+
return { machine: machineIdMatch, matchedBy: "machine_id" };
|
|
8182
|
+
const hostnameMatch = topology.machines.find((machine) => machine.hostname && normalizeMachineAlias(machine.hostname) === requested);
|
|
8183
|
+
if (hostnameMatch)
|
|
8184
|
+
return { machine: hostnameMatch, matchedBy: "hostname" };
|
|
8185
|
+
const tailscaleMatch = topology.machines.find((machine) => {
|
|
8186
|
+
if (!machine.tailscale.dns_name)
|
|
8187
|
+
return false;
|
|
8188
|
+
const dns = normalizeMachineAlias(machine.tailscale.dns_name);
|
|
8189
|
+
return dns === requested || dns.split(".")[0] === requested;
|
|
8190
|
+
});
|
|
8191
|
+
if (tailscaleMatch)
|
|
8192
|
+
return { machine: tailscaleMatch, matchedBy: "tailscale" };
|
|
8193
|
+
const routeMatch = topology.machines.find((machine) => routeTargetMatches(machine, requestedMachineId));
|
|
8194
|
+
if (routeMatch)
|
|
8195
|
+
return { machine: routeMatch, matchedBy: "route_target" };
|
|
8196
|
+
return { machine: null, matchedBy: null };
|
|
8259
8197
|
}
|
|
8260
|
-
|
|
8261
|
-
|
|
8262
|
-
|
|
8263
|
-
|
|
8198
|
+
function routeConfidence(input) {
|
|
8199
|
+
if (input.matchedBy === "local_alias")
|
|
8200
|
+
return "exact";
|
|
8201
|
+
if (input.hint?.kind === "local")
|
|
8202
|
+
return "exact";
|
|
8203
|
+
if (input.hint?.reachable === true)
|
|
8204
|
+
return "high";
|
|
8205
|
+
if (input.machine.manifest_declared && (input.hint?.kind === "ssh" || input.hint?.kind === "lan"))
|
|
8206
|
+
return "medium";
|
|
8207
|
+
if (input.hint)
|
|
8208
|
+
return "low";
|
|
8209
|
+
return "none";
|
|
8264
8210
|
}
|
|
8265
|
-
function
|
|
8266
|
-
return (
|
|
8211
|
+
function addMilliseconds(date, milliseconds) {
|
|
8212
|
+
return new Date(date.getTime() + milliseconds).toISOString();
|
|
8267
8213
|
}
|
|
8268
|
-
function
|
|
8269
|
-
|
|
8270
|
-
|
|
8271
|
-
|
|
8272
|
-
|
|
8273
|
-
|
|
8274
|
-
|
|
8275
|
-
|
|
8214
|
+
function routeAuthority(input) {
|
|
8215
|
+
if (!input.machine)
|
|
8216
|
+
return "unresolved";
|
|
8217
|
+
if (input.matchedBy === "fallback")
|
|
8218
|
+
return "fallback";
|
|
8219
|
+
if (input.selectedHint?.kind === "local")
|
|
8220
|
+
return "live_topology";
|
|
8221
|
+
if (input.selectedHint?.kind === "tailscale" || input.machine.tailscale.online !== null)
|
|
8222
|
+
return "live_topology";
|
|
8223
|
+
if (input.machine.manifest_declared)
|
|
8224
|
+
return "manifest";
|
|
8225
|
+
return "open-machines";
|
|
8226
|
+
}
|
|
8227
|
+
function workspaceAuthority(paths) {
|
|
8228
|
+
const sources = [paths.workspace_root.source, paths.project_root.source, paths.open_files_root.source];
|
|
8229
|
+
if (sources.some((source) => source === "argument"))
|
|
8230
|
+
return "argument";
|
|
8231
|
+
if (sources.some((source) => source === "manifest_metadata"))
|
|
8232
|
+
return "manifest_metadata";
|
|
8233
|
+
if (sources.some((source) => source === "manifest"))
|
|
8234
|
+
return "manifest";
|
|
8235
|
+
if (sources.some((source) => source === "inferred"))
|
|
8236
|
+
return "inferred";
|
|
8237
|
+
if (sources.every((source) => source === "unresolved"))
|
|
8238
|
+
return "unresolved";
|
|
8239
|
+
return "open-machines";
|
|
8240
|
+
}
|
|
8241
|
+
function cacheability(input) {
|
|
8242
|
+
const ttlMs = input.ttlMs === undefined ? DEFAULT_MACHINE_RESOLVER_TTL_MS : input.ttlMs;
|
|
8243
|
+
const expiresAt = typeof ttlMs === "number" && ttlMs > 0 ? addMilliseconds(input.observedAt, ttlMs) : null;
|
|
8244
|
+
const stale = expiresAt ? input.now.getTime() > new Date(expiresAt).getTime() : false;
|
|
8245
|
+
const confidenceCacheable = input.confidence !== "none" && input.confidence !== "low";
|
|
8246
|
+
const cacheable = input.ok && confidenceCacheable && !stale && input.authority !== "unresolved";
|
|
8247
|
+
const reasons = [...input.reasons];
|
|
8248
|
+
if (!input.ok)
|
|
8249
|
+
reasons.push("resolver_not_ok");
|
|
8250
|
+
if (!confidenceCacheable)
|
|
8251
|
+
reasons.push(`low_confidence:${input.confidence}`);
|
|
8252
|
+
if (stale)
|
|
8253
|
+
reasons.push("stale");
|
|
8254
|
+
if (input.authority === "unresolved")
|
|
8255
|
+
reasons.push("unresolved_authority");
|
|
8256
|
+
return {
|
|
8257
|
+
observed_at: input.observedAt.toISOString(),
|
|
8258
|
+
verified_at: input.ok ? input.now.toISOString() : null,
|
|
8259
|
+
expires_at: expiresAt,
|
|
8260
|
+
ttl_ms: typeof ttlMs === "number" && ttlMs > 0 ? ttlMs : null,
|
|
8261
|
+
source_authority: input.authority,
|
|
8262
|
+
confidence: input.confidence,
|
|
8263
|
+
cacheable,
|
|
8264
|
+
stale,
|
|
8265
|
+
reasons: [...new Set(reasons)].sort()
|
|
8266
|
+
};
|
|
8267
|
+
}
|
|
8268
|
+
function resolveMachineRoute(machineId, options = {}) {
|
|
8269
|
+
const now2 = options.now ?? new Date;
|
|
8270
|
+
const topology = options.topology ?? discoverMachineTopology(options);
|
|
8271
|
+
const warnings = [...topology.warnings];
|
|
8272
|
+
const { machine, matchedBy } = findRouteMachine(topology, machineId);
|
|
8273
|
+
const generatedAt = now2.toISOString();
|
|
8274
|
+
if (!machine) {
|
|
8275
|
+
warnings.push(`machine_not_found:${machineId}`);
|
|
8276
|
+
return {
|
|
8277
|
+
schema_version: MACHINES_CONSUMER_CONTRACT_VERSION,
|
|
8278
|
+
package: { name: MACHINES_PACKAGE_NAME, version: getPackageVersion() },
|
|
8279
|
+
ok: false,
|
|
8280
|
+
machine_id: null,
|
|
8281
|
+
requested_machine_id: machineId,
|
|
8282
|
+
generated_at: generatedAt,
|
|
8283
|
+
route: "unknown",
|
|
8284
|
+
source: "unknown",
|
|
8285
|
+
target: null,
|
|
8286
|
+
command_target: null,
|
|
8287
|
+
confidence: "none",
|
|
8288
|
+
local: false,
|
|
8289
|
+
evidence: {
|
|
8290
|
+
topology: true,
|
|
8291
|
+
matched_by: null,
|
|
8292
|
+
manifest_declared: null,
|
|
8293
|
+
heartbeat_status: null,
|
|
8294
|
+
tailscale_online: null,
|
|
8295
|
+
selected_hint: null
|
|
8296
|
+
},
|
|
8297
|
+
cacheability: cacheability({
|
|
8298
|
+
ok: false,
|
|
8299
|
+
observedAt: now2,
|
|
8300
|
+
now: now2,
|
|
8301
|
+
ttlMs: options.resolverTtlMs,
|
|
8302
|
+
authority: "unresolved",
|
|
8303
|
+
confidence: "none",
|
|
8304
|
+
reasons: [`machine_not_found:${machineId}`]
|
|
8305
|
+
}),
|
|
8306
|
+
warnings
|
|
8307
|
+
};
|
|
8276
8308
|
}
|
|
8277
|
-
const
|
|
8278
|
-
|
|
8279
|
-
|
|
8280
|
-
|
|
8281
|
-
|
|
8282
|
-
].filter(Boolean);
|
|
8283
|
-
const leftPackages = new Set(packageNames(left));
|
|
8284
|
-
const rightPackages = new Set(packageNames(right));
|
|
8285
|
-
const leftFiles = new Set(fileTargets(left));
|
|
8286
|
-
const rightFiles = new Set(fileTargets(right));
|
|
8309
|
+
const selectedHint = selectRouteHint(machine.route_hints);
|
|
8310
|
+
const route = selectedHint?.kind ?? machine.ssh.route ?? "unknown";
|
|
8311
|
+
const local = route === "local" || machine.machine_id === topology.local_machine_id;
|
|
8312
|
+
const confidence = routeConfidence({ machine, hint: selectedHint, matchedBy });
|
|
8313
|
+
const ok = Boolean(selectedHint?.target);
|
|
8287
8314
|
return {
|
|
8288
|
-
|
|
8289
|
-
|
|
8290
|
-
|
|
8291
|
-
|
|
8292
|
-
|
|
8293
|
-
|
|
8315
|
+
schema_version: MACHINES_CONSUMER_CONTRACT_VERSION,
|
|
8316
|
+
package: topology.package,
|
|
8317
|
+
ok,
|
|
8318
|
+
machine_id: machine.machine_id,
|
|
8319
|
+
requested_machine_id: machineId,
|
|
8320
|
+
generated_at: generatedAt,
|
|
8321
|
+
route,
|
|
8322
|
+
source: route,
|
|
8323
|
+
target: selectedHint?.target ?? null,
|
|
8324
|
+
command_target: selectedHint?.target ?? null,
|
|
8325
|
+
confidence,
|
|
8326
|
+
local,
|
|
8327
|
+
evidence: {
|
|
8328
|
+
topology: true,
|
|
8329
|
+
matched_by: matchedBy,
|
|
8330
|
+
manifest_declared: machine.manifest_declared,
|
|
8331
|
+
heartbeat_status: machine.heartbeat_status,
|
|
8332
|
+
tailscale_online: machine.tailscale.online,
|
|
8333
|
+
selected_hint: selectedHint
|
|
8294
8334
|
},
|
|
8295
|
-
|
|
8296
|
-
|
|
8297
|
-
|
|
8298
|
-
|
|
8335
|
+
cacheability: cacheability({
|
|
8336
|
+
ok,
|
|
8337
|
+
observedAt: now2,
|
|
8338
|
+
now: now2,
|
|
8339
|
+
ttlMs: options.resolverTtlMs,
|
|
8340
|
+
authority: routeAuthority({ machine, selectedHint, matchedBy }),
|
|
8341
|
+
confidence,
|
|
8342
|
+
reasons: selectedHint ? [] : ["route_target_unresolved"]
|
|
8343
|
+
}),
|
|
8344
|
+
warnings
|
|
8299
8345
|
};
|
|
8300
8346
|
}
|
|
8301
|
-
|
|
8302
|
-
|
|
8303
|
-
init_db();
|
|
8304
|
-
import { spawnSync as spawnSync2 } from "child_process";
|
|
8305
|
-
import { hostname as hostname5 } from "os";
|
|
8306
|
-
|
|
8307
|
-
// src/topology.ts
|
|
8308
|
-
init_db();
|
|
8309
|
-
import { existsSync as existsSync6 } from "fs";
|
|
8310
|
-
import { arch as arch2, hostname as hostname4, platform as platform3, userInfo as userInfo2 } from "os";
|
|
8311
|
-
import { spawnSync } from "child_process";
|
|
8312
|
-
init_paths();
|
|
8313
|
-
var MACHINES_CONSUMER_CONTRACT_VERSION = 1;
|
|
8314
|
-
var MACHINES_PACKAGE_NAME = "@hasna/machines";
|
|
8315
|
-
var DEFAULT_MACHINE_RESOLVER_TTL_MS = 24 * 60 * 60 * 1000;
|
|
8316
|
-
var MACHINES_CONSUMER_CAPABILITIES = {
|
|
8317
|
-
topology: true,
|
|
8318
|
-
compatibility: true,
|
|
8319
|
-
route_resolution: true,
|
|
8320
|
-
cli_json_fallback: true,
|
|
8321
|
-
workspace_path_mapping: true,
|
|
8322
|
-
workspace_diagnostics: true,
|
|
8323
|
-
schema_artifacts: true,
|
|
8324
|
-
cacheability_metadata: true,
|
|
8325
|
-
resolver_snapshots: true,
|
|
8326
|
-
field_capability_descriptors: true
|
|
8327
|
-
};
|
|
8328
|
-
function getMachinesConsumerCapabilities() {
|
|
8329
|
-
return { ...MACHINES_CONSUMER_CAPABILITIES };
|
|
8347
|
+
function isRecord(value) {
|
|
8348
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
8330
8349
|
}
|
|
8331
|
-
function
|
|
8332
|
-
const
|
|
8333
|
-
|
|
8334
|
-
|
|
8335
|
-
|
|
8336
|
-
|
|
8337
|
-
|
|
8338
|
-
return "linux";
|
|
8339
|
-
return value;
|
|
8350
|
+
function metadataString(metadata, keys) {
|
|
8351
|
+
for (const key of keys) {
|
|
8352
|
+
const value = metadata[key];
|
|
8353
|
+
if (typeof value === "string" && value.trim())
|
|
8354
|
+
return value.trim();
|
|
8355
|
+
}
|
|
8356
|
+
return null;
|
|
8340
8357
|
}
|
|
8341
|
-
function
|
|
8342
|
-
const
|
|
8343
|
-
|
|
8344
|
-
|
|
8345
|
-
|
|
8346
|
-
return {
|
|
8347
|
-
stdout: result.stdout || "",
|
|
8348
|
-
stderr: result.stderr || "",
|
|
8349
|
-
exitCode: result.status ?? 1
|
|
8350
|
-
};
|
|
8351
|
-
}
|
|
8352
|
-
function hasCommand(command, runner) {
|
|
8353
|
-
return runner(`command -v ${command} >/dev/null 2>&1`).exitCode === 0;
|
|
8354
|
-
}
|
|
8355
|
-
function parseTailscaleStatus(raw) {
|
|
8356
|
-
try {
|
|
8357
|
-
const parsed = JSON.parse(raw);
|
|
8358
|
-
if (!parsed || typeof parsed !== "object")
|
|
8359
|
-
return null;
|
|
8360
|
-
return parsed;
|
|
8361
|
-
} catch {
|
|
8362
|
-
return null;
|
|
8358
|
+
function metadataBoolean(metadata, keys) {
|
|
8359
|
+
for (const key of keys) {
|
|
8360
|
+
const value = metadata[key];
|
|
8361
|
+
if (typeof value === "boolean")
|
|
8362
|
+
return value;
|
|
8363
8363
|
}
|
|
8364
|
+
return null;
|
|
8364
8365
|
}
|
|
8365
|
-
function
|
|
8366
|
-
const
|
|
8367
|
-
|
|
8368
|
-
|
|
8369
|
-
|
|
8370
|
-
}
|
|
8371
|
-
const result = runner("tailscale status --json");
|
|
8372
|
-
if (result.exitCode !== 0) {
|
|
8373
|
-
warnings.push(`tailscale_status_failed:${result.stderr.trim() || result.exitCode}`);
|
|
8374
|
-
return peers;
|
|
8375
|
-
}
|
|
8376
|
-
const status = parseTailscaleStatus(result.stdout);
|
|
8377
|
-
if (!status) {
|
|
8378
|
-
warnings.push("tailscale_status_invalid_json");
|
|
8379
|
-
return peers;
|
|
8366
|
+
function metadataStringArray(metadata, keys) {
|
|
8367
|
+
for (const key of keys) {
|
|
8368
|
+
const value = metadata[key];
|
|
8369
|
+
if (Array.isArray(value))
|
|
8370
|
+
return value.filter((entry) => typeof entry === "string");
|
|
8380
8371
|
}
|
|
8381
|
-
|
|
8382
|
-
if (!peer)
|
|
8383
|
-
return;
|
|
8384
|
-
const id = peer.HostName || peer.DNSName?.split(".")[0];
|
|
8385
|
-
if (id)
|
|
8386
|
-
peers.set(id, peer);
|
|
8387
|
-
};
|
|
8388
|
-
addPeer(status.Self);
|
|
8389
|
-
for (const peer of Object.values(status.Peer ?? {}))
|
|
8390
|
-
addPeer(peer);
|
|
8391
|
-
return peers;
|
|
8392
|
-
}
|
|
8393
|
-
function machineKeys(machine) {
|
|
8394
|
-
return [
|
|
8395
|
-
machine.id,
|
|
8396
|
-
machine.hostname,
|
|
8397
|
-
machine.tailscaleName?.split(".")[0],
|
|
8398
|
-
machine.tailscaleName,
|
|
8399
|
-
machine.sshAddress?.split("@").pop()
|
|
8400
|
-
].filter((value) => Boolean(value));
|
|
8372
|
+
return [];
|
|
8401
8373
|
}
|
|
8402
|
-
function
|
|
8403
|
-
|
|
8404
|
-
|
|
8405
|
-
|
|
8406
|
-
|
|
8407
|
-
|
|
8374
|
+
function readMappedPath(input) {
|
|
8375
|
+
for (const containerName of input.containers) {
|
|
8376
|
+
const container = input.metadata[containerName];
|
|
8377
|
+
if (!isRecord(container))
|
|
8378
|
+
continue;
|
|
8379
|
+
for (const key of input.keys) {
|
|
8380
|
+
const value = container[key];
|
|
8381
|
+
if (typeof value === "string" && value.trim())
|
|
8382
|
+
return value.trim();
|
|
8383
|
+
if (isRecord(value)) {
|
|
8384
|
+
const path = metadataString(value, ["path", "root", "workspacePath", "workspace_path"]);
|
|
8385
|
+
if (path)
|
|
8386
|
+
return path;
|
|
8387
|
+
}
|
|
8408
8388
|
}
|
|
8409
8389
|
}
|
|
8410
|
-
return
|
|
8390
|
+
return null;
|
|
8411
8391
|
}
|
|
8412
|
-
function
|
|
8413
|
-
|
|
8414
|
-
return new Set((raw || "").split(",").map((value) => value.trim()).filter(Boolean));
|
|
8392
|
+
function trimTrailingSlash(value) {
|
|
8393
|
+
return value.replace(/\/+$/, "");
|
|
8415
8394
|
}
|
|
8416
|
-
function
|
|
8417
|
-
|
|
8418
|
-
if (overrides.size === 0)
|
|
8419
|
-
return null;
|
|
8420
|
-
return overrides.has(target);
|
|
8395
|
+
function joinPath(left, right) {
|
|
8396
|
+
return `${trimTrailingSlash(left)}/${right.replace(/^\/+/, "")}`;
|
|
8421
8397
|
}
|
|
8422
|
-
function
|
|
8423
|
-
|
|
8424
|
-
|
|
8425
|
-
|
|
8426
|
-
}
|
|
8427
|
-
|
|
8428
|
-
|
|
8429
|
-
|
|
8430
|
-
if (input.manifest?.hostname) {
|
|
8431
|
-
hints.push({ kind: "lan", target: input.manifest.hostname, reachable: manifestHostReachable(input.manifest.hostname) });
|
|
8432
|
-
}
|
|
8433
|
-
const tailscaleTarget = input.manifest?.tailscaleName ?? input.peer?.DNSName ?? input.peer?.TailscaleIPs?.[0];
|
|
8434
|
-
if (tailscaleTarget) {
|
|
8435
|
-
hints.push({ kind: "tailscale", target: tailscaleTarget.replace(/\.$/, ""), reachable: input.peer?.Online ?? null });
|
|
8398
|
+
function inferRepoRoot(workspaceRoot, repoName) {
|
|
8399
|
+
if (!workspaceRoot || !repoName)
|
|
8400
|
+
return null;
|
|
8401
|
+
const root = trimTrailingSlash(workspaceRoot);
|
|
8402
|
+
if (root.endsWith(`/${repoName}`) || root === repoName)
|
|
8403
|
+
return root;
|
|
8404
|
+
if (root.endsWith("/workspace") || root.endsWith("/Workspace")) {
|
|
8405
|
+
return joinPath(root, `hasna/opensource/${repoName}`);
|
|
8436
8406
|
}
|
|
8437
|
-
return
|
|
8407
|
+
return joinPath(root, repoName);
|
|
8438
8408
|
}
|
|
8439
|
-
function
|
|
8440
|
-
|
|
8441
|
-
return 0;
|
|
8442
|
-
if (hint.reachable === true && hint.kind === "ssh")
|
|
8443
|
-
return 1;
|
|
8444
|
-
if (hint.reachable === true && hint.kind === "lan")
|
|
8445
|
-
return 2;
|
|
8446
|
-
if (hint.reachable === true && hint.kind === "tailscale")
|
|
8447
|
-
return 3;
|
|
8448
|
-
if (hint.reachable === false)
|
|
8449
|
-
return 8;
|
|
8450
|
-
if (hint.kind === "ssh")
|
|
8451
|
-
return 4;
|
|
8452
|
-
if (hint.kind === "lan")
|
|
8453
|
-
return 5;
|
|
8454
|
-
if (hint.kind === "tailscale")
|
|
8455
|
-
return 6;
|
|
8456
|
-
return 9;
|
|
8409
|
+
function shellQuote(value) {
|
|
8410
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
8457
8411
|
}
|
|
8458
|
-
function
|
|
8459
|
-
return
|
|
8412
|
+
function shellCommand(command) {
|
|
8413
|
+
return command.map(shellQuote).join(" ");
|
|
8460
8414
|
}
|
|
8461
|
-
function
|
|
8462
|
-
|
|
8463
|
-
|
|
8464
|
-
|
|
8465
|
-
|
|
8466
|
-
|
|
8467
|
-
manifest,
|
|
8468
|
-
peer
|
|
8469
|
-
});
|
|
8470
|
-
const selectedRoute = selectRouteHint(hints);
|
|
8471
|
-
const route = selectedRoute?.kind === "ssh" ? "ssh" : selectedRoute?.kind ?? "unknown";
|
|
8472
|
-
return {
|
|
8473
|
-
machine_id: input.machineId,
|
|
8474
|
-
hostname: manifest?.hostname ?? peer?.HostName ?? null,
|
|
8475
|
-
platform: manifest?.platform ?? (peer?.OS ? normalizePlatform2(peer.OS) : null),
|
|
8476
|
-
os: peer?.OS ?? null,
|
|
8477
|
-
user: typeof manifest?.metadata?.user === "string" ? manifest.metadata.user : null,
|
|
8478
|
-
workspace_path: manifest?.workspacePath ?? null,
|
|
8479
|
-
manifest_declared: Boolean(manifest),
|
|
8480
|
-
heartbeat_status: input.heartbeat?.status ?? "unknown",
|
|
8481
|
-
last_heartbeat_at: input.heartbeat?.updated_at ?? null,
|
|
8482
|
-
tailscale: {
|
|
8483
|
-
dns_name: manifest?.tailscaleName ?? peer?.DNSName?.replace(/\.$/, "") ?? null,
|
|
8484
|
-
ips: peer?.TailscaleIPs ?? [],
|
|
8485
|
-
online: peer?.Online ?? null,
|
|
8486
|
-
active: peer?.Active ?? null,
|
|
8487
|
-
last_seen: peer?.LastSeen ?? null
|
|
8488
|
-
},
|
|
8489
|
-
ssh: {
|
|
8490
|
-
address: manifest?.sshAddress ?? null,
|
|
8491
|
-
route,
|
|
8492
|
-
command_target: selectedRoute?.target ?? null
|
|
8493
|
-
},
|
|
8494
|
-
route_hints: hints,
|
|
8495
|
-
tags: manifest?.tags ?? [],
|
|
8496
|
-
metadata: manifest?.metadata ?? {}
|
|
8497
|
-
};
|
|
8415
|
+
function canCheckPathForMachine(machine, localMachineId) {
|
|
8416
|
+
if (!machine)
|
|
8417
|
+
return false;
|
|
8418
|
+
if (machine.machine_id === localMachineId)
|
|
8419
|
+
return true;
|
|
8420
|
+
return machine.route_hints.some((hint) => hint.kind === "local");
|
|
8498
8421
|
}
|
|
8499
|
-
function
|
|
8500
|
-
|
|
8501
|
-
|
|
8502
|
-
|
|
8503
|
-
|
|
8504
|
-
|
|
8505
|
-
const
|
|
8506
|
-
|
|
8507
|
-
|
|
8508
|
-
|
|
8509
|
-
|
|
8510
|
-
|
|
8511
|
-
|
|
8512
|
-
|
|
8513
|
-
]
|
|
8514
|
-
|
|
8515
|
-
|
|
8516
|
-
|
|
8517
|
-
|
|
8518
|
-
|
|
8519
|
-
|
|
8520
|
-
manifest: manifestMachine,
|
|
8521
|
-
peer: findTailscalePeer(manifestMachine ?? null, machineId, peers),
|
|
8522
|
-
heartbeat: heartbeatByMachine.get(machineId)
|
|
8523
|
-
});
|
|
8524
|
-
});
|
|
8422
|
+
function checkedPathExists(path, check) {
|
|
8423
|
+
if (!path || !check)
|
|
8424
|
+
return null;
|
|
8425
|
+
return existsSync5(path);
|
|
8426
|
+
}
|
|
8427
|
+
function repairHint(input) {
|
|
8428
|
+
const command = [
|
|
8429
|
+
"machines",
|
|
8430
|
+
"workspace",
|
|
8431
|
+
"repair",
|
|
8432
|
+
"--machine",
|
|
8433
|
+
input.machineId,
|
|
8434
|
+
"--project",
|
|
8435
|
+
input.projectId
|
|
8436
|
+
];
|
|
8437
|
+
if (input.repoName)
|
|
8438
|
+
command.push("--repo", input.repoName);
|
|
8439
|
+
if (input.openFilesRepoName)
|
|
8440
|
+
command.push("--open-files-repo", input.openFilesRepoName);
|
|
8441
|
+
command.push("--json");
|
|
8442
|
+
const applyCommand = [...command.slice(0, -1), "--apply", "--json"];
|
|
8525
8443
|
return {
|
|
8526
|
-
|
|
8527
|
-
|
|
8528
|
-
|
|
8529
|
-
|
|
8530
|
-
|
|
8531
|
-
|
|
8532
|
-
generated_at: now2.toISOString(),
|
|
8533
|
-
local_machine_id: localMachineId,
|
|
8534
|
-
local_hostname: hostname4(),
|
|
8535
|
-
current_platform: normalizePlatform2(),
|
|
8536
|
-
manifest_path_known: existsSync6(getManifestPath()),
|
|
8537
|
-
machines,
|
|
8538
|
-
warnings
|
|
8444
|
+
id: `repair:${input.machineId}:${input.projectId}`,
|
|
8445
|
+
reason: input.reason,
|
|
8446
|
+
command,
|
|
8447
|
+
shell_command: shellCommand(command),
|
|
8448
|
+
apply_command: applyCommand,
|
|
8449
|
+
apply_shell_command: shellCommand(applyCommand)
|
|
8539
8450
|
};
|
|
8540
8451
|
}
|
|
8541
|
-
function
|
|
8542
|
-
|
|
8543
|
-
}
|
|
8544
|
-
function routeTargetMatches(machine, requested) {
|
|
8545
|
-
const normalized = normalizeMachineAlias(requested);
|
|
8546
|
-
const values = [
|
|
8547
|
-
machine.ssh.address,
|
|
8548
|
-
machine.ssh.command_target,
|
|
8549
|
-
machine.tailscale.dns_name,
|
|
8550
|
-
machine.tailscale.dns_name?.split(".")[0],
|
|
8551
|
-
...machine.tailscale.ips,
|
|
8552
|
-
...machine.route_hints.map((hint) => hint.target),
|
|
8553
|
-
...machine.route_hints.map((hint) => hint.target.split("@").pop() ?? hint.target)
|
|
8554
|
-
].filter((value) => Boolean(value));
|
|
8555
|
-
return values.some((value) => normalizeMachineAlias(value) === normalized);
|
|
8556
|
-
}
|
|
8557
|
-
function findRouteMachine(topology, requestedMachineId) {
|
|
8558
|
-
const requested = normalizeMachineAlias(requestedMachineId);
|
|
8559
|
-
if (requested === "local" || requested === "localhost" || requested === normalizeMachineAlias(hostname4()) || requested === normalizeMachineAlias(topology.local_machine_id)) {
|
|
8452
|
+
function pathDiagnostic(input) {
|
|
8453
|
+
if (!input.path.path) {
|
|
8560
8454
|
return {
|
|
8561
|
-
|
|
8562
|
-
|
|
8455
|
+
id: input.id,
|
|
8456
|
+
status: "missing",
|
|
8457
|
+
severity: input.required ? "fail" : "warn",
|
|
8458
|
+
message: `${input.label} is unresolved.`,
|
|
8459
|
+
path: null,
|
|
8460
|
+
source: input.path.source,
|
|
8461
|
+
path_exists: null
|
|
8563
8462
|
};
|
|
8564
8463
|
}
|
|
8565
|
-
|
|
8566
|
-
if (machineIdMatch)
|
|
8567
|
-
return { machine: machineIdMatch, matchedBy: "machine_id" };
|
|
8568
|
-
const hostnameMatch = topology.machines.find((machine) => machine.hostname && normalizeMachineAlias(machine.hostname) === requested);
|
|
8569
|
-
if (hostnameMatch)
|
|
8570
|
-
return { machine: hostnameMatch, matchedBy: "hostname" };
|
|
8571
|
-
const tailscaleMatch = topology.machines.find((machine) => {
|
|
8572
|
-
if (!machine.tailscale.dns_name)
|
|
8573
|
-
return false;
|
|
8574
|
-
const dns = normalizeMachineAlias(machine.tailscale.dns_name);
|
|
8575
|
-
return dns === requested || dns.split(".")[0] === requested;
|
|
8576
|
-
});
|
|
8577
|
-
if (tailscaleMatch)
|
|
8578
|
-
return { machine: tailscaleMatch, matchedBy: "tailscale" };
|
|
8579
|
-
const routeMatch = topology.machines.find((machine) => routeTargetMatches(machine, requestedMachineId));
|
|
8580
|
-
if (routeMatch)
|
|
8581
|
-
return { machine: routeMatch, matchedBy: "route_target" };
|
|
8582
|
-
return { machine: null, matchedBy: null };
|
|
8583
|
-
}
|
|
8584
|
-
function routeConfidence(input) {
|
|
8585
|
-
if (input.matchedBy === "local_alias")
|
|
8586
|
-
return "exact";
|
|
8587
|
-
if (input.hint?.kind === "local")
|
|
8588
|
-
return "exact";
|
|
8589
|
-
if (input.hint?.reachable === true)
|
|
8590
|
-
return "high";
|
|
8591
|
-
if (input.machine.manifest_declared && (input.hint?.kind === "ssh" || input.hint?.kind === "lan"))
|
|
8592
|
-
return "medium";
|
|
8593
|
-
if (input.hint)
|
|
8594
|
-
return "low";
|
|
8595
|
-
return "none";
|
|
8596
|
-
}
|
|
8597
|
-
function addMilliseconds(date, milliseconds) {
|
|
8598
|
-
return new Date(date.getTime() + milliseconds).toISOString();
|
|
8599
|
-
}
|
|
8600
|
-
function routeAuthority(input) {
|
|
8601
|
-
if (!input.machine)
|
|
8602
|
-
return "unresolved";
|
|
8603
|
-
if (input.matchedBy === "fallback")
|
|
8604
|
-
return "fallback";
|
|
8605
|
-
if (input.selectedHint?.kind === "local")
|
|
8606
|
-
return "live_topology";
|
|
8607
|
-
if (input.selectedHint?.kind === "tailscale" || input.machine.tailscale.online !== null)
|
|
8608
|
-
return "live_topology";
|
|
8609
|
-
if (input.machine.manifest_declared)
|
|
8610
|
-
return "manifest";
|
|
8611
|
-
return "open-machines";
|
|
8612
|
-
}
|
|
8613
|
-
function workspaceAuthority(paths) {
|
|
8614
|
-
const sources = [paths.workspace_root.source, paths.project_root.source, paths.open_files_root.source];
|
|
8615
|
-
if (sources.some((source) => source === "argument"))
|
|
8616
|
-
return "argument";
|
|
8617
|
-
if (sources.some((source) => source === "manifest_metadata"))
|
|
8618
|
-
return "manifest_metadata";
|
|
8619
|
-
if (sources.some((source) => source === "manifest"))
|
|
8620
|
-
return "manifest";
|
|
8621
|
-
if (sources.some((source) => source === "inferred"))
|
|
8622
|
-
return "inferred";
|
|
8623
|
-
if (sources.every((source) => source === "unresolved"))
|
|
8624
|
-
return "unresolved";
|
|
8625
|
-
return "open-machines";
|
|
8626
|
-
}
|
|
8627
|
-
function cacheability(input) {
|
|
8628
|
-
const ttlMs = input.ttlMs === undefined ? DEFAULT_MACHINE_RESOLVER_TTL_MS : input.ttlMs;
|
|
8629
|
-
const expiresAt = typeof ttlMs === "number" && ttlMs > 0 ? addMilliseconds(input.observedAt, ttlMs) : null;
|
|
8630
|
-
const stale = expiresAt ? input.now.getTime() > new Date(expiresAt).getTime() : false;
|
|
8631
|
-
const confidenceCacheable = input.confidence !== "none" && input.confidence !== "low";
|
|
8632
|
-
const cacheable = input.ok && confidenceCacheable && !stale && input.authority !== "unresolved";
|
|
8633
|
-
const reasons = [...input.reasons];
|
|
8634
|
-
if (!input.ok)
|
|
8635
|
-
reasons.push("resolver_not_ok");
|
|
8636
|
-
if (!confidenceCacheable)
|
|
8637
|
-
reasons.push(`low_confidence:${input.confidence}`);
|
|
8638
|
-
if (stale)
|
|
8639
|
-
reasons.push("stale");
|
|
8640
|
-
if (input.authority === "unresolved")
|
|
8641
|
-
reasons.push("unresolved_authority");
|
|
8642
|
-
return {
|
|
8643
|
-
observed_at: input.observedAt.toISOString(),
|
|
8644
|
-
verified_at: input.ok ? input.now.toISOString() : null,
|
|
8645
|
-
expires_at: expiresAt,
|
|
8646
|
-
ttl_ms: typeof ttlMs === "number" && ttlMs > 0 ? ttlMs : null,
|
|
8647
|
-
source_authority: input.authority,
|
|
8648
|
-
confidence: input.confidence,
|
|
8649
|
-
cacheable,
|
|
8650
|
-
stale,
|
|
8651
|
-
reasons: [...new Set(reasons)].sort()
|
|
8652
|
-
};
|
|
8653
|
-
}
|
|
8654
|
-
function resolveMachineRoute(machineId, options = {}) {
|
|
8655
|
-
const now2 = options.now ?? new Date;
|
|
8656
|
-
const topology = options.topology ?? discoverMachineTopology(options);
|
|
8657
|
-
const warnings = [...topology.warnings];
|
|
8658
|
-
const { machine, matchedBy } = findRouteMachine(topology, machineId);
|
|
8659
|
-
const generatedAt = now2.toISOString();
|
|
8660
|
-
if (!machine) {
|
|
8661
|
-
warnings.push(`machine_not_found:${machineId}`);
|
|
8464
|
+
if (input.pathExists === false) {
|
|
8662
8465
|
return {
|
|
8663
|
-
|
|
8664
|
-
|
|
8665
|
-
|
|
8666
|
-
|
|
8667
|
-
|
|
8668
|
-
|
|
8669
|
-
|
|
8670
|
-
|
|
8671
|
-
|
|
8672
|
-
|
|
8673
|
-
|
|
8674
|
-
|
|
8675
|
-
|
|
8676
|
-
|
|
8677
|
-
|
|
8678
|
-
|
|
8679
|
-
|
|
8680
|
-
|
|
8681
|
-
selected_hint: null
|
|
8682
|
-
},
|
|
8683
|
-
cacheability: cacheability({
|
|
8684
|
-
ok: false,
|
|
8685
|
-
observedAt: now2,
|
|
8686
|
-
now: now2,
|
|
8687
|
-
ttlMs: options.resolverTtlMs,
|
|
8688
|
-
authority: "unresolved",
|
|
8689
|
-
confidence: "none",
|
|
8690
|
-
reasons: [`machine_not_found:${machineId}`]
|
|
8691
|
-
}),
|
|
8692
|
-
warnings
|
|
8466
|
+
id: input.id,
|
|
8467
|
+
status: "stale",
|
|
8468
|
+
severity: "fail",
|
|
8469
|
+
message: `${input.label} points to a path that does not exist on this machine.`,
|
|
8470
|
+
path: input.path.path,
|
|
8471
|
+
source: input.path.source,
|
|
8472
|
+
path_exists: false
|
|
8473
|
+
};
|
|
8474
|
+
}
|
|
8475
|
+
if (input.path.source === "inferred") {
|
|
8476
|
+
return {
|
|
8477
|
+
id: input.id,
|
|
8478
|
+
status: "inferred",
|
|
8479
|
+
severity: "warn",
|
|
8480
|
+
message: `${input.label} was inferred from the workspace root; write an explicit manifest mapping for repeatable downstream sync.`,
|
|
8481
|
+
path: input.path.path,
|
|
8482
|
+
source: input.path.source,
|
|
8483
|
+
path_exists: input.pathExists
|
|
8693
8484
|
};
|
|
8694
8485
|
}
|
|
8695
|
-
const selectedHint = selectRouteHint(machine.route_hints);
|
|
8696
|
-
const route = selectedHint?.kind ?? machine.ssh.route ?? "unknown";
|
|
8697
|
-
const local = route === "local" || machine.machine_id === topology.local_machine_id;
|
|
8698
|
-
const confidence = routeConfidence({ machine, hint: selectedHint, matchedBy });
|
|
8699
|
-
const ok = Boolean(selectedHint?.target);
|
|
8700
8486
|
return {
|
|
8701
|
-
|
|
8702
|
-
|
|
8703
|
-
ok,
|
|
8704
|
-
|
|
8705
|
-
|
|
8706
|
-
|
|
8707
|
-
|
|
8708
|
-
source: route,
|
|
8709
|
-
target: selectedHint?.target ?? null,
|
|
8710
|
-
command_target: selectedHint?.target ?? null,
|
|
8711
|
-
confidence,
|
|
8712
|
-
local,
|
|
8713
|
-
evidence: {
|
|
8714
|
-
topology: true,
|
|
8715
|
-
matched_by: matchedBy,
|
|
8716
|
-
manifest_declared: machine.manifest_declared,
|
|
8717
|
-
heartbeat_status: machine.heartbeat_status,
|
|
8718
|
-
tailscale_online: machine.tailscale.online,
|
|
8719
|
-
selected_hint: selectedHint
|
|
8720
|
-
},
|
|
8721
|
-
cacheability: cacheability({
|
|
8722
|
-
ok,
|
|
8723
|
-
observedAt: now2,
|
|
8724
|
-
now: now2,
|
|
8725
|
-
ttlMs: options.resolverTtlMs,
|
|
8726
|
-
authority: routeAuthority({ machine, selectedHint, matchedBy }),
|
|
8727
|
-
confidence,
|
|
8728
|
-
reasons: selectedHint ? [] : ["route_target_unresolved"]
|
|
8729
|
-
}),
|
|
8730
|
-
warnings
|
|
8487
|
+
id: input.id,
|
|
8488
|
+
status: "ok",
|
|
8489
|
+
severity: "ok",
|
|
8490
|
+
message: `${input.label} is explicit enough for downstream sync.`,
|
|
8491
|
+
path: input.path.path,
|
|
8492
|
+
source: input.path.source,
|
|
8493
|
+
path_exists: input.pathExists
|
|
8731
8494
|
};
|
|
8732
8495
|
}
|
|
8733
|
-
function
|
|
8734
|
-
|
|
8735
|
-
|
|
8736
|
-
|
|
8737
|
-
|
|
8738
|
-
|
|
8739
|
-
|
|
8740
|
-
|
|
8741
|
-
|
|
8742
|
-
|
|
8743
|
-
|
|
8744
|
-
|
|
8745
|
-
|
|
8746
|
-
|
|
8747
|
-
|
|
8748
|
-
|
|
8749
|
-
|
|
8750
|
-
|
|
8751
|
-
|
|
8752
|
-
|
|
8753
|
-
|
|
8754
|
-
|
|
8755
|
-
|
|
8756
|
-
return value.filter((entry) => typeof entry === "string");
|
|
8757
|
-
}
|
|
8758
|
-
return [];
|
|
8759
|
-
}
|
|
8760
|
-
function readMappedPath(input) {
|
|
8761
|
-
for (const containerName of input.containers) {
|
|
8762
|
-
const container = input.metadata[containerName];
|
|
8763
|
-
if (!isRecord(container))
|
|
8764
|
-
continue;
|
|
8765
|
-
for (const key of input.keys) {
|
|
8766
|
-
const value = container[key];
|
|
8767
|
-
if (typeof value === "string" && value.trim())
|
|
8768
|
-
return value.trim();
|
|
8769
|
-
if (isRecord(value)) {
|
|
8770
|
-
const path = metadataString(value, ["path", "root", "workspacePath", "workspace_path"]);
|
|
8771
|
-
if (path)
|
|
8772
|
-
return path;
|
|
8773
|
-
}
|
|
8774
|
-
}
|
|
8775
|
-
}
|
|
8776
|
-
return null;
|
|
8777
|
-
}
|
|
8778
|
-
function trimTrailingSlash(value) {
|
|
8779
|
-
return value.replace(/\/+$/, "");
|
|
8780
|
-
}
|
|
8781
|
-
function joinPath(left, right) {
|
|
8782
|
-
return `${trimTrailingSlash(left)}/${right.replace(/^\/+/, "")}`;
|
|
8783
|
-
}
|
|
8784
|
-
function inferRepoRoot(workspaceRoot, repoName) {
|
|
8785
|
-
if (!workspaceRoot || !repoName)
|
|
8786
|
-
return null;
|
|
8787
|
-
const root = trimTrailingSlash(workspaceRoot);
|
|
8788
|
-
if (root.endsWith(`/${repoName}`) || root === repoName)
|
|
8789
|
-
return root;
|
|
8790
|
-
if (root.endsWith("/workspace") || root.endsWith("/Workspace")) {
|
|
8791
|
-
return joinPath(root, `hasna/opensource/${repoName}`);
|
|
8792
|
-
}
|
|
8793
|
-
return joinPath(root, repoName);
|
|
8794
|
-
}
|
|
8795
|
-
function shellQuote(value) {
|
|
8796
|
-
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
8797
|
-
}
|
|
8798
|
-
function shellCommand(command) {
|
|
8799
|
-
return command.map(shellQuote).join(" ");
|
|
8800
|
-
}
|
|
8801
|
-
function canCheckPathForMachine(machine, localMachineId) {
|
|
8802
|
-
if (!machine)
|
|
8803
|
-
return false;
|
|
8804
|
-
if (machine.machine_id === localMachineId)
|
|
8805
|
-
return true;
|
|
8806
|
-
return machine.route_hints.some((hint) => hint.kind === "local");
|
|
8807
|
-
}
|
|
8808
|
-
function checkedPathExists(path, check) {
|
|
8809
|
-
if (!path || !check)
|
|
8810
|
-
return null;
|
|
8811
|
-
return existsSync6(path);
|
|
8812
|
-
}
|
|
8813
|
-
function repairHint(input) {
|
|
8814
|
-
const command = [
|
|
8815
|
-
"machines",
|
|
8816
|
-
"workspace",
|
|
8817
|
-
"repair",
|
|
8818
|
-
"--machine",
|
|
8819
|
-
input.machineId,
|
|
8820
|
-
"--project",
|
|
8821
|
-
input.projectId
|
|
8822
|
-
];
|
|
8823
|
-
if (input.repoName)
|
|
8824
|
-
command.push("--repo", input.repoName);
|
|
8825
|
-
if (input.openFilesRepoName)
|
|
8826
|
-
command.push("--open-files-repo", input.openFilesRepoName);
|
|
8827
|
-
command.push("--json");
|
|
8828
|
-
const applyCommand = [...command.slice(0, -1), "--apply", "--json"];
|
|
8829
|
-
return {
|
|
8830
|
-
id: `repair:${input.machineId}:${input.projectId}`,
|
|
8831
|
-
reason: input.reason,
|
|
8832
|
-
command,
|
|
8833
|
-
shell_command: shellCommand(command),
|
|
8834
|
-
apply_command: applyCommand,
|
|
8835
|
-
apply_shell_command: shellCommand(applyCommand)
|
|
8836
|
-
};
|
|
8837
|
-
}
|
|
8838
|
-
function pathDiagnostic(input) {
|
|
8839
|
-
if (!input.path.path) {
|
|
8840
|
-
return {
|
|
8841
|
-
id: input.id,
|
|
8842
|
-
status: "missing",
|
|
8843
|
-
severity: input.required ? "fail" : "warn",
|
|
8844
|
-
message: `${input.label} is unresolved.`,
|
|
8845
|
-
path: null,
|
|
8846
|
-
source: input.path.source,
|
|
8847
|
-
path_exists: null
|
|
8848
|
-
};
|
|
8849
|
-
}
|
|
8850
|
-
if (input.pathExists === false) {
|
|
8851
|
-
return {
|
|
8852
|
-
id: input.id,
|
|
8853
|
-
status: "stale",
|
|
8854
|
-
severity: "fail",
|
|
8855
|
-
message: `${input.label} points to a path that does not exist on this machine.`,
|
|
8856
|
-
path: input.path.path,
|
|
8857
|
-
source: input.path.source,
|
|
8858
|
-
path_exists: false
|
|
8859
|
-
};
|
|
8860
|
-
}
|
|
8861
|
-
if (input.path.source === "inferred") {
|
|
8862
|
-
return {
|
|
8863
|
-
id: input.id,
|
|
8864
|
-
status: "inferred",
|
|
8865
|
-
severity: "warn",
|
|
8866
|
-
message: `${input.label} was inferred from the workspace root; write an explicit manifest mapping for repeatable downstream sync.`,
|
|
8867
|
-
path: input.path.path,
|
|
8868
|
-
source: input.path.source,
|
|
8869
|
-
path_exists: input.pathExists
|
|
8870
|
-
};
|
|
8871
|
-
}
|
|
8872
|
-
return {
|
|
8873
|
-
id: input.id,
|
|
8874
|
-
status: "ok",
|
|
8875
|
-
severity: "ok",
|
|
8876
|
-
message: `${input.label} is explicit enough for downstream sync.`,
|
|
8877
|
-
path: input.path.path,
|
|
8878
|
-
source: input.path.source,
|
|
8879
|
-
path_exists: input.pathExists
|
|
8880
|
-
};
|
|
8881
|
-
}
|
|
8882
|
-
function workspaceDiagnostics(input) {
|
|
8883
|
-
const checkPaths = canCheckPathForMachine(input.machine, input.localMachineId);
|
|
8884
|
-
const diagnostics = [];
|
|
8885
|
-
if (!input.machine) {
|
|
8886
|
-
diagnostics.push({
|
|
8887
|
-
id: "manifest",
|
|
8888
|
-
status: "missing_manifest",
|
|
8889
|
-
severity: "fail",
|
|
8890
|
-
message: "Machine is not present in topology or manifest.",
|
|
8891
|
-
path: null,
|
|
8892
|
-
source: "manifest",
|
|
8893
|
-
path_exists: null
|
|
8894
|
-
});
|
|
8895
|
-
} else if (!input.resolution.evidence.manifest_declared) {
|
|
8896
|
-
diagnostics.push({
|
|
8897
|
-
id: "manifest",
|
|
8898
|
-
status: "missing_manifest",
|
|
8899
|
-
severity: "warn",
|
|
8900
|
-
message: "Machine came from live topology but is not declared in the manifest.",
|
|
8901
|
-
path: null,
|
|
8902
|
-
source: "manifest",
|
|
8903
|
-
path_exists: null
|
|
8904
|
-
});
|
|
8496
|
+
function workspaceDiagnostics(input) {
|
|
8497
|
+
const checkPaths = canCheckPathForMachine(input.machine, input.localMachineId);
|
|
8498
|
+
const diagnostics = [];
|
|
8499
|
+
if (!input.machine) {
|
|
8500
|
+
diagnostics.push({
|
|
8501
|
+
id: "manifest",
|
|
8502
|
+
status: "missing_manifest",
|
|
8503
|
+
severity: "fail",
|
|
8504
|
+
message: "Machine is not present in topology or manifest.",
|
|
8505
|
+
path: null,
|
|
8506
|
+
source: "manifest",
|
|
8507
|
+
path_exists: null
|
|
8508
|
+
});
|
|
8509
|
+
} else if (!input.resolution.evidence.manifest_declared) {
|
|
8510
|
+
diagnostics.push({
|
|
8511
|
+
id: "manifest",
|
|
8512
|
+
status: "missing_manifest",
|
|
8513
|
+
severity: "warn",
|
|
8514
|
+
message: "Machine came from live topology but is not declared in the manifest.",
|
|
8515
|
+
path: null,
|
|
8516
|
+
source: "manifest",
|
|
8517
|
+
path_exists: null
|
|
8518
|
+
});
|
|
8905
8519
|
}
|
|
8906
8520
|
diagnostics.push(pathDiagnostic({
|
|
8907
8521
|
id: "workspace_root",
|
|
@@ -9138,80 +8752,478 @@ function resolveMachineWorkspace(options) {
|
|
|
9138
8752
|
}),
|
|
9139
8753
|
warnings
|
|
9140
8754
|
};
|
|
9141
|
-
const diagnostics = workspaceDiagnostics({
|
|
9142
|
-
machine,
|
|
9143
|
-
localMachineId: topology.local_machine_id,
|
|
9144
|
-
resolution,
|
|
9145
|
-
openFilesRepoName
|
|
9146
|
-
});
|
|
8755
|
+
const diagnostics = workspaceDiagnostics({
|
|
8756
|
+
machine,
|
|
8757
|
+
localMachineId: topology.local_machine_id,
|
|
8758
|
+
resolution,
|
|
8759
|
+
openFilesRepoName
|
|
8760
|
+
});
|
|
8761
|
+
return {
|
|
8762
|
+
...resolution,
|
|
8763
|
+
diagnostics: diagnostics.diagnostics,
|
|
8764
|
+
repair_hints: diagnostics.repairHints
|
|
8765
|
+
};
|
|
8766
|
+
}
|
|
8767
|
+
|
|
8768
|
+
// src/commands/ssh.ts
|
|
8769
|
+
function shellQuote2(value) {
|
|
8770
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
8771
|
+
}
|
|
8772
|
+
function resolveSshTarget(machineId, options = {}) {
|
|
8773
|
+
const resolved = resolveMachineRoute(machineId, options);
|
|
8774
|
+
if (!resolved.ok || !resolved.target) {
|
|
8775
|
+
throw new Error(`Machine route not found: ${machineId}`);
|
|
8776
|
+
}
|
|
8777
|
+
if (resolved.route !== "local" && resolved.route !== "lan" && resolved.route !== "tailscale" && resolved.route !== "ssh") {
|
|
8778
|
+
throw new Error(`Machine route is not SSH-capable: ${machineId}`);
|
|
8779
|
+
}
|
|
8780
|
+
return {
|
|
8781
|
+
machineId: resolved.machine_id ?? machineId,
|
|
8782
|
+
target: resolved.target,
|
|
8783
|
+
route: resolved.route,
|
|
8784
|
+
confidence: resolved.confidence,
|
|
8785
|
+
warnings: resolved.warnings
|
|
8786
|
+
};
|
|
8787
|
+
}
|
|
8788
|
+
function buildSshCommand(machineId, remoteCommand, options = {}) {
|
|
8789
|
+
const resolved = resolveSshTarget(machineId, options);
|
|
8790
|
+
return remoteCommand ? `ssh ${resolved.target} ${shellQuote2(remoteCommand)}` : `ssh ${resolved.target}`;
|
|
8791
|
+
}
|
|
8792
|
+
|
|
8793
|
+
// src/remote.ts
|
|
8794
|
+
function shellQuote3(value) {
|
|
8795
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
8796
|
+
}
|
|
8797
|
+
function machineIsLocal(machineId, localMachineId) {
|
|
8798
|
+
return machineId === "local" || machineId === "localhost" || machineId === localMachineId || machineId === hostname4();
|
|
8799
|
+
}
|
|
8800
|
+
function resolveMachineCommand(machineId, command, localMachineId = getLocalMachineId()) {
|
|
8801
|
+
if (machineIsLocal(machineId, localMachineId)) {
|
|
8802
|
+
return { source: "local", shellCommand: command };
|
|
8803
|
+
}
|
|
8804
|
+
try {
|
|
8805
|
+
return {
|
|
8806
|
+
source: resolveSshTarget(machineId).route,
|
|
8807
|
+
shellCommand: buildSshCommand(machineId, command)
|
|
8808
|
+
};
|
|
8809
|
+
} catch (error) {
|
|
8810
|
+
const message = String(error.message ?? error);
|
|
8811
|
+
if (message.includes("Machine route not found") || message.includes("Machine not found in manifest")) {
|
|
8812
|
+
return { source: "ssh", shellCommand: `ssh ${shellQuote3(machineId)} ${shellQuote3(command)}` };
|
|
8813
|
+
}
|
|
8814
|
+
throw error;
|
|
8815
|
+
}
|
|
8816
|
+
}
|
|
8817
|
+
function runMachineCommand(machineId, command) {
|
|
8818
|
+
const resolved = resolveMachineCommand(machineId, command);
|
|
8819
|
+
const result = spawnSync2("bash", ["-c", resolved.shellCommand], {
|
|
8820
|
+
encoding: "utf8",
|
|
8821
|
+
env: process.env
|
|
8822
|
+
});
|
|
8823
|
+
return {
|
|
8824
|
+
machineId,
|
|
8825
|
+
source: resolved.source,
|
|
8826
|
+
stdout: result.stdout || "",
|
|
8827
|
+
stderr: result.stderr || "",
|
|
8828
|
+
exitCode: result.status ?? 1
|
|
8829
|
+
};
|
|
8830
|
+
}
|
|
8831
|
+
function describeMachineCommandFailure(operation, result) {
|
|
8832
|
+
const detail = (result.stderr || result.stdout || "").trim();
|
|
8833
|
+
const suffix = detail ? `: ${detail}` : "";
|
|
8834
|
+
return `${operation} failed on ${result.machineId} via ${result.source} (exit ${result.exitCode})${suffix}`;
|
|
8835
|
+
}
|
|
8836
|
+
function requireMachineCommandSuccess(operation, result) {
|
|
8837
|
+
if (result.exitCode !== 0) {
|
|
8838
|
+
throw new Error(describeMachineCommandFailure(operation, result));
|
|
8839
|
+
}
|
|
8840
|
+
return result;
|
|
8841
|
+
}
|
|
8842
|
+
|
|
8843
|
+
// src/commands/setup.ts
|
|
8844
|
+
function quote(value) {
|
|
8845
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
8846
|
+
}
|
|
8847
|
+
function buildBaseSteps(machine) {
|
|
8848
|
+
const steps = [
|
|
8849
|
+
{
|
|
8850
|
+
id: "workspace",
|
|
8851
|
+
title: "Ensure workspace directory exists",
|
|
8852
|
+
command: `mkdir -p ${quote(machine.workspacePath)}`,
|
|
8853
|
+
manager: "shell"
|
|
8854
|
+
},
|
|
8855
|
+
{
|
|
8856
|
+
id: "bun",
|
|
8857
|
+
title: "Install Bun if missing",
|
|
8858
|
+
command: "command -v bun >/dev/null 2>&1 || curl -fsSL https://bun.sh/install | bash",
|
|
8859
|
+
manager: "shell"
|
|
8860
|
+
}
|
|
8861
|
+
];
|
|
8862
|
+
if (machine.platform === "linux") {
|
|
8863
|
+
steps.push({
|
|
8864
|
+
id: "apt-base",
|
|
8865
|
+
title: "Install core Linux tooling",
|
|
8866
|
+
command: "sudo apt-get update && sudo apt-get install -y git curl unzip build-essential",
|
|
8867
|
+
manager: "apt",
|
|
8868
|
+
privileged: true
|
|
8869
|
+
});
|
|
8870
|
+
} else if (machine.platform === "macos") {
|
|
8871
|
+
steps.push({
|
|
8872
|
+
id: "brew-base",
|
|
8873
|
+
title: "Install Homebrew if missing",
|
|
8874
|
+
command: 'command -v brew >/dev/null 2>&1 || /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"',
|
|
8875
|
+
manager: "brew"
|
|
8876
|
+
});
|
|
8877
|
+
steps.push({
|
|
8878
|
+
id: "brew-core",
|
|
8879
|
+
title: "Install core macOS tooling",
|
|
8880
|
+
command: "brew install git coreutils",
|
|
8881
|
+
manager: "brew"
|
|
8882
|
+
});
|
|
8883
|
+
}
|
|
8884
|
+
return steps;
|
|
8885
|
+
}
|
|
8886
|
+
function buildPackageSteps(machine) {
|
|
8887
|
+
return (machine.packages || []).map((pkg, index) => {
|
|
8888
|
+
const manager = pkg.manager || (machine.platform === "macos" ? "brew" : "apt");
|
|
8889
|
+
let command = pkg.name;
|
|
8890
|
+
if (manager === "bun") {
|
|
8891
|
+
command = `bun install -g ${quote(pkg.name)}`;
|
|
8892
|
+
} else if (manager === "brew") {
|
|
8893
|
+
command = `brew install ${quote(pkg.name)}`;
|
|
8894
|
+
} else if (manager === "apt") {
|
|
8895
|
+
command = `sudo apt-get install -y ${quote(pkg.name)}`;
|
|
8896
|
+
}
|
|
8897
|
+
return {
|
|
8898
|
+
id: `package-${index + 1}`,
|
|
8899
|
+
title: `Install package ${pkg.name}`,
|
|
8900
|
+
command,
|
|
8901
|
+
manager,
|
|
8902
|
+
privileged: manager === "apt"
|
|
8903
|
+
};
|
|
8904
|
+
});
|
|
8905
|
+
}
|
|
8906
|
+
function buildSetupPlan(machineId) {
|
|
8907
|
+
const manifest = readManifest();
|
|
8908
|
+
const currentMachineId = getLocalMachineId();
|
|
8909
|
+
const selected = machineId ? manifest.machines.find((machine) => machine.id === machineId) : manifest.machines.find((machine) => machine.id === currentMachineId);
|
|
8910
|
+
const target = selected || {
|
|
8911
|
+
id: currentMachineId,
|
|
8912
|
+
platform: "linux",
|
|
8913
|
+
workspacePath: `${homedir3()}/workspace`
|
|
8914
|
+
};
|
|
8915
|
+
const steps = [...buildBaseSteps(target), ...buildPackageSteps(target)];
|
|
8916
|
+
return {
|
|
8917
|
+
machineId: target.id,
|
|
8918
|
+
mode: "plan",
|
|
8919
|
+
steps,
|
|
8920
|
+
executed: 0
|
|
8921
|
+
};
|
|
8922
|
+
}
|
|
8923
|
+
function runSetup(machineId, options = {}, runner = runMachineCommand) {
|
|
8924
|
+
const plan = buildSetupPlan(machineId);
|
|
8925
|
+
if (!options.apply) {
|
|
8926
|
+
return plan;
|
|
8927
|
+
}
|
|
8928
|
+
if (!options.yes) {
|
|
8929
|
+
throw new Error("Setup execution requires --yes.");
|
|
8930
|
+
}
|
|
8931
|
+
let executed = 0;
|
|
8932
|
+
for (const step of plan.steps) {
|
|
8933
|
+
const result = runner(plan.machineId, step.command);
|
|
8934
|
+
if (result.exitCode !== 0) {
|
|
8935
|
+
recordSetupRun(plan.machineId, "failed", {
|
|
8936
|
+
executed,
|
|
8937
|
+
failedStep: step,
|
|
8938
|
+
stderr: result.stderr,
|
|
8939
|
+
stdout: result.stdout,
|
|
8940
|
+
exitCode: result.exitCode,
|
|
8941
|
+
source: result.source
|
|
8942
|
+
});
|
|
8943
|
+
throw new Error(describeMachineCommandFailure(`Setup step ${step.id}`, result));
|
|
8944
|
+
}
|
|
8945
|
+
executed += 1;
|
|
8946
|
+
}
|
|
8947
|
+
const summary = {
|
|
8948
|
+
machineId: plan.machineId,
|
|
8949
|
+
mode: "apply",
|
|
8950
|
+
steps: plan.steps,
|
|
8951
|
+
executed
|
|
8952
|
+
};
|
|
8953
|
+
recordSetupRun(plan.machineId, "completed", summary);
|
|
8954
|
+
return summary;
|
|
8955
|
+
}
|
|
8956
|
+
|
|
8957
|
+
// src/commands/backup.ts
|
|
8958
|
+
import { homedir as homedir4, hostname as hostname5 } from "os";
|
|
8959
|
+
import { join as join4 } from "path";
|
|
8960
|
+
var MACHINES_BACKUP_BUCKET_ENV = "HASNA_MACHINES_S3_BUCKET";
|
|
8961
|
+
var MACHINES_BACKUP_BUCKET_FALLBACK_ENV = "MACHINES_S3_BUCKET";
|
|
8962
|
+
var MACHINES_BACKUP_PREFIX_ENV = "HASNA_MACHINES_S3_PREFIX";
|
|
8963
|
+
var MACHINES_BACKUP_PREFIX_FALLBACK_ENV = "MACHINES_S3_PREFIX";
|
|
8964
|
+
var DEFAULT_BACKUP_PREFIX = "machines";
|
|
8965
|
+
function quote2(value) {
|
|
8966
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
8967
|
+
}
|
|
8968
|
+
function readEnv(name) {
|
|
8969
|
+
const value = process.env[name]?.trim();
|
|
8970
|
+
return value || undefined;
|
|
8971
|
+
}
|
|
8972
|
+
function readBackupBucketEnv() {
|
|
8973
|
+
const primary = readEnv(MACHINES_BACKUP_BUCKET_ENV);
|
|
8974
|
+
if (primary)
|
|
8975
|
+
return { bucket: primary, bucketSource: MACHINES_BACKUP_BUCKET_ENV };
|
|
8976
|
+
const fallback = readEnv(MACHINES_BACKUP_BUCKET_FALLBACK_ENV);
|
|
8977
|
+
if (fallback)
|
|
8978
|
+
return { bucket: fallback, bucketSource: MACHINES_BACKUP_BUCKET_FALLBACK_ENV };
|
|
8979
|
+
return null;
|
|
8980
|
+
}
|
|
8981
|
+
function readBackupPrefixEnv() {
|
|
8982
|
+
const primary = readEnv(MACHINES_BACKUP_PREFIX_ENV);
|
|
8983
|
+
if (primary)
|
|
8984
|
+
return { prefix: primary, prefixSource: MACHINES_BACKUP_PREFIX_ENV };
|
|
8985
|
+
const fallback = readEnv(MACHINES_BACKUP_PREFIX_FALLBACK_ENV);
|
|
8986
|
+
if (fallback)
|
|
8987
|
+
return { prefix: fallback, prefixSource: MACHINES_BACKUP_PREFIX_FALLBACK_ENV };
|
|
8988
|
+
return null;
|
|
8989
|
+
}
|
|
8990
|
+
function resolveBackupTarget(options = {}) {
|
|
8991
|
+
const explicitBucket = options.bucket?.trim();
|
|
8992
|
+
const envBucket = explicitBucket ? null : readBackupBucketEnv();
|
|
8993
|
+
const bucket = explicitBucket || envBucket?.bucket;
|
|
8994
|
+
if (!bucket) {
|
|
8995
|
+
throw new Error(`Missing S3 backup bucket. Pass --bucket or set ${MACHINES_BACKUP_BUCKET_ENV} or ${MACHINES_BACKUP_BUCKET_FALLBACK_ENV}.`);
|
|
8996
|
+
}
|
|
8997
|
+
const explicitPrefix = options.prefix?.trim();
|
|
8998
|
+
const envPrefix = explicitPrefix ? null : readBackupPrefixEnv();
|
|
8999
|
+
return {
|
|
9000
|
+
bucket,
|
|
9001
|
+
prefix: explicitPrefix || envPrefix?.prefix || DEFAULT_BACKUP_PREFIX,
|
|
9002
|
+
bucketSource: explicitBucket ? "argument" : envBucket.bucketSource,
|
|
9003
|
+
prefixSource: explicitPrefix ? "argument" : envPrefix?.prefixSource || "default"
|
|
9004
|
+
};
|
|
9005
|
+
}
|
|
9006
|
+
function defaultBackupSources() {
|
|
9007
|
+
const home = homedir4();
|
|
9008
|
+
return [
|
|
9009
|
+
join4(home, ".hasna"),
|
|
9010
|
+
join4(home, ".ssh"),
|
|
9011
|
+
join4(home, ".secrets")
|
|
9012
|
+
];
|
|
9013
|
+
}
|
|
9014
|
+
function buildBackupPlan(bucket, prefix) {
|
|
9015
|
+
const target = resolveBackupTarget({ bucket, prefix });
|
|
9016
|
+
const archivePath = join4(homedir4(), ".hasna", "machines", "backup.tgz");
|
|
9017
|
+
const sources = defaultBackupSources();
|
|
9018
|
+
const steps = [
|
|
9019
|
+
{
|
|
9020
|
+
id: "backup-archive",
|
|
9021
|
+
title: "Create compressed machine backup archive",
|
|
9022
|
+
command: `tar -czf ${quote2(archivePath)} ${sources.map((source) => quote2(source)).join(" ")}`,
|
|
9023
|
+
manager: "shell"
|
|
9024
|
+
},
|
|
9025
|
+
{
|
|
9026
|
+
id: "backup-upload",
|
|
9027
|
+
title: "Upload archive to S3",
|
|
9028
|
+
command: `aws s3 cp ${quote2(archivePath)} ${quote2(`s3://${target.bucket}/${target.prefix}/${hostname5()}-backup.tgz`)}`,
|
|
9029
|
+
manager: "custom"
|
|
9030
|
+
}
|
|
9031
|
+
];
|
|
9032
|
+
return {
|
|
9033
|
+
machineId: process.env["HASNA_MACHINES_MACHINE_ID"] || "local",
|
|
9034
|
+
mode: "plan",
|
|
9035
|
+
steps,
|
|
9036
|
+
executed: 0
|
|
9037
|
+
};
|
|
9038
|
+
}
|
|
9039
|
+
function runBackup(bucket, prefix, options = {}) {
|
|
9040
|
+
const plan = buildBackupPlan(bucket, prefix);
|
|
9041
|
+
if (!options.apply)
|
|
9042
|
+
return plan;
|
|
9043
|
+
if (!options.yes) {
|
|
9044
|
+
throw new Error("Backup execution requires --yes.");
|
|
9045
|
+
}
|
|
9046
|
+
let executed = 0;
|
|
9047
|
+
for (const step of plan.steps) {
|
|
9048
|
+
const result = Bun.spawnSync(["bash", "-lc", step.command], {
|
|
9049
|
+
stdout: "pipe",
|
|
9050
|
+
stderr: "pipe",
|
|
9051
|
+
env: process.env
|
|
9052
|
+
});
|
|
9053
|
+
if (result.exitCode !== 0) {
|
|
9054
|
+
throw new Error(`Backup step failed (${step.id}): ${result.stderr.toString().trim()}`);
|
|
9055
|
+
}
|
|
9056
|
+
executed += 1;
|
|
9057
|
+
}
|
|
9147
9058
|
return {
|
|
9148
|
-
|
|
9149
|
-
|
|
9150
|
-
|
|
9059
|
+
machineId: plan.machineId,
|
|
9060
|
+
mode: "apply",
|
|
9061
|
+
steps: plan.steps,
|
|
9062
|
+
executed
|
|
9151
9063
|
};
|
|
9152
9064
|
}
|
|
9153
9065
|
|
|
9154
|
-
// src/commands/
|
|
9155
|
-
|
|
9156
|
-
|
|
9066
|
+
// src/commands/cert.ts
|
|
9067
|
+
import { homedir as homedir5, platform as platform3 } from "os";
|
|
9068
|
+
import { join as join5 } from "path";
|
|
9069
|
+
function quote3(value) {
|
|
9070
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
9157
9071
|
}
|
|
9158
|
-
function
|
|
9159
|
-
|
|
9160
|
-
|
|
9161
|
-
|
|
9072
|
+
function certDir() {
|
|
9073
|
+
return join5(homedir5(), ".hasna", "machines", "certs");
|
|
9074
|
+
}
|
|
9075
|
+
function buildCertPlan(domains) {
|
|
9076
|
+
if (domains.length === 0) {
|
|
9077
|
+
throw new Error("At least one domain is required.");
|
|
9162
9078
|
}
|
|
9163
|
-
|
|
9164
|
-
|
|
9079
|
+
const primary = domains[0];
|
|
9080
|
+
const certPath = join5(certDir(), `${primary}.pem`);
|
|
9081
|
+
const keyPath = join5(certDir(), `${primary}-key.pem`);
|
|
9082
|
+
const steps = [];
|
|
9083
|
+
if (platform3() === "darwin") {
|
|
9084
|
+
steps.push({
|
|
9085
|
+
id: "mkcert-install-macos",
|
|
9086
|
+
title: "Install mkcert on macOS",
|
|
9087
|
+
command: "brew install mkcert nss",
|
|
9088
|
+
manager: "brew"
|
|
9089
|
+
});
|
|
9090
|
+
} else {
|
|
9091
|
+
steps.push({
|
|
9092
|
+
id: "mkcert-install-linux",
|
|
9093
|
+
title: "Install mkcert on Linux",
|
|
9094
|
+
command: "sudo apt-get update && sudo apt-get install -y mkcert libnss3-tools",
|
|
9095
|
+
manager: "apt",
|
|
9096
|
+
privileged: true
|
|
9097
|
+
});
|
|
9165
9098
|
}
|
|
9099
|
+
steps.push({
|
|
9100
|
+
id: "mkcert-local-ca",
|
|
9101
|
+
title: "Install local mkcert CA",
|
|
9102
|
+
command: "mkcert -install",
|
|
9103
|
+
manager: "custom"
|
|
9104
|
+
}, {
|
|
9105
|
+
id: "mkcert-issue",
|
|
9106
|
+
title: `Issue certificate for ${domains.join(", ")}`,
|
|
9107
|
+
command: `mkdir -p ${quote3(certDir())} && mkcert -cert-file ${quote3(certPath)} -key-file ${quote3(keyPath)} ${domains.map((domain) => quote3(domain)).join(" ")}`,
|
|
9108
|
+
manager: "custom"
|
|
9109
|
+
});
|
|
9166
9110
|
return {
|
|
9167
|
-
machineId:
|
|
9168
|
-
|
|
9169
|
-
|
|
9170
|
-
|
|
9171
|
-
warnings: resolved.warnings
|
|
9111
|
+
machineId: process.env["HASNA_MACHINES_MACHINE_ID"] || "local",
|
|
9112
|
+
mode: "plan",
|
|
9113
|
+
steps,
|
|
9114
|
+
executed: 0
|
|
9172
9115
|
};
|
|
9173
9116
|
}
|
|
9174
|
-
function
|
|
9175
|
-
const
|
|
9176
|
-
|
|
9117
|
+
function runCertPlan(domains, options = {}) {
|
|
9118
|
+
const plan = buildCertPlan(domains);
|
|
9119
|
+
if (!options.apply)
|
|
9120
|
+
return plan;
|
|
9121
|
+
if (!options.yes) {
|
|
9122
|
+
throw new Error("Certificate generation requires --yes.");
|
|
9123
|
+
}
|
|
9124
|
+
let executed = 0;
|
|
9125
|
+
for (const step of plan.steps) {
|
|
9126
|
+
const result = Bun.spawnSync(["bash", "-lc", step.command], {
|
|
9127
|
+
stdout: "pipe",
|
|
9128
|
+
stderr: "pipe",
|
|
9129
|
+
env: process.env
|
|
9130
|
+
});
|
|
9131
|
+
if (result.exitCode !== 0) {
|
|
9132
|
+
throw new Error(`Certificate step failed (${step.id}): ${result.stderr.toString().trim()}`);
|
|
9133
|
+
}
|
|
9134
|
+
executed += 1;
|
|
9135
|
+
}
|
|
9136
|
+
return {
|
|
9137
|
+
machineId: plan.machineId,
|
|
9138
|
+
mode: "apply",
|
|
9139
|
+
steps: plan.steps,
|
|
9140
|
+
executed
|
|
9141
|
+
};
|
|
9177
9142
|
}
|
|
9178
9143
|
|
|
9179
|
-
// src/
|
|
9180
|
-
|
|
9181
|
-
|
|
9144
|
+
// src/commands/dns.ts
|
|
9145
|
+
init_paths();
|
|
9146
|
+
import { existsSync as existsSync6, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
9147
|
+
import { join as join6 } from "path";
|
|
9148
|
+
function getDnsPath() {
|
|
9149
|
+
return join6(getDataDir(), "dns.json");
|
|
9182
9150
|
}
|
|
9183
|
-
function
|
|
9184
|
-
|
|
9151
|
+
function readMappings() {
|
|
9152
|
+
const path = getDnsPath();
|
|
9153
|
+
if (!existsSync6(path))
|
|
9154
|
+
return [];
|
|
9155
|
+
return JSON.parse(readFileSync3(path, "utf8"));
|
|
9185
9156
|
}
|
|
9186
|
-
function
|
|
9187
|
-
|
|
9188
|
-
|
|
9189
|
-
}
|
|
9190
|
-
|
|
9191
|
-
|
|
9192
|
-
|
|
9193
|
-
|
|
9194
|
-
|
|
9195
|
-
|
|
9196
|
-
|
|
9197
|
-
|
|
9198
|
-
|
|
9199
|
-
|
|
9200
|
-
|
|
9157
|
+
function writeMappings(mappings) {
|
|
9158
|
+
const path = getDnsPath();
|
|
9159
|
+
ensureParentDir(path);
|
|
9160
|
+
writeFileSync2(path, `${JSON.stringify(mappings, null, 2)}
|
|
9161
|
+
`, "utf8");
|
|
9162
|
+
return path;
|
|
9163
|
+
}
|
|
9164
|
+
function addDomainMapping(domain, port, targetHost = "127.0.0.1") {
|
|
9165
|
+
const mappings = readMappings().filter((entry) => entry.domain !== domain);
|
|
9166
|
+
mappings.push({ domain, port, targetHost });
|
|
9167
|
+
writeMappings(mappings);
|
|
9168
|
+
return mappings.sort((left, right) => left.domain.localeCompare(right.domain));
|
|
9169
|
+
}
|
|
9170
|
+
function listDomainMappings() {
|
|
9171
|
+
return readMappings().sort((left, right) => left.domain.localeCompare(right.domain));
|
|
9172
|
+
}
|
|
9173
|
+
function renderDomainMapping(domain) {
|
|
9174
|
+
const entry = readMappings().find((mapping) => mapping.domain === domain);
|
|
9175
|
+
if (!entry) {
|
|
9176
|
+
throw new Error(`Domain mapping not found: ${domain}`);
|
|
9201
9177
|
}
|
|
9178
|
+
return {
|
|
9179
|
+
hostsEntry: `${entry.targetHost} ${entry.domain}`,
|
|
9180
|
+
caddySnippet: `${entry.domain} {
|
|
9181
|
+
reverse_proxy 127.0.0.1:${entry.port}
|
|
9182
|
+
tls ${join6(getDataDir(), "certs", `${entry.domain}.pem`)} ${join6(getDataDir(), "certs", `${entry.domain}-key.pem`)}
|
|
9183
|
+
}`,
|
|
9184
|
+
certPath: join6(getDataDir(), "certs", `${entry.domain}.pem`),
|
|
9185
|
+
keyPath: join6(getDataDir(), "certs", `${entry.domain}-key.pem`)
|
|
9186
|
+
};
|
|
9202
9187
|
}
|
|
9203
|
-
|
|
9204
|
-
|
|
9205
|
-
|
|
9206
|
-
|
|
9207
|
-
|
|
9208
|
-
|
|
9188
|
+
|
|
9189
|
+
// src/commands/diff.ts
|
|
9190
|
+
function packageNames(machine) {
|
|
9191
|
+
return (machine.packages || []).map((pkg) => pkg.name).sort();
|
|
9192
|
+
}
|
|
9193
|
+
function fileTargets(machine) {
|
|
9194
|
+
return (machine.files || []).map((file) => `${file.source}->${file.target}`).sort();
|
|
9195
|
+
}
|
|
9196
|
+
function diffMachines(leftMachineId, rightMachineId) {
|
|
9197
|
+
const left = getManifestMachine(leftMachineId);
|
|
9198
|
+
if (!left) {
|
|
9199
|
+
throw new Error(`Machine not found in manifest: ${leftMachineId}`);
|
|
9200
|
+
}
|
|
9201
|
+
const right = rightMachineId ? getManifestMachine(rightMachineId) : detectCurrentMachineManifest();
|
|
9202
|
+
if (!right) {
|
|
9203
|
+
throw new Error(`Machine not found in manifest: ${rightMachineId}`);
|
|
9204
|
+
}
|
|
9205
|
+
const changedFields = [
|
|
9206
|
+
left.platform !== right.platform ? "platform" : null,
|
|
9207
|
+
left.connection !== right.connection ? "connection" : null,
|
|
9208
|
+
left.workspacePath !== right.workspacePath ? "workspacePath" : null,
|
|
9209
|
+
left.bunPath !== right.bunPath ? "bunPath" : null
|
|
9210
|
+
].filter(Boolean);
|
|
9211
|
+
const leftPackages = new Set(packageNames(left));
|
|
9212
|
+
const rightPackages = new Set(packageNames(right));
|
|
9213
|
+
const leftFiles = new Set(fileTargets(left));
|
|
9214
|
+
const rightFiles = new Set(fileTargets(right));
|
|
9209
9215
|
return {
|
|
9210
|
-
|
|
9211
|
-
|
|
9212
|
-
|
|
9213
|
-
|
|
9214
|
-
|
|
9216
|
+
leftMachineId: left.id,
|
|
9217
|
+
rightMachineId: right.id,
|
|
9218
|
+
changedFields,
|
|
9219
|
+
missingPackages: {
|
|
9220
|
+
leftOnly: [...leftPackages].filter((pkg) => !rightPackages.has(pkg)),
|
|
9221
|
+
rightOnly: [...rightPackages].filter((pkg) => !leftPackages.has(pkg))
|
|
9222
|
+
},
|
|
9223
|
+
missingFiles: {
|
|
9224
|
+
leftOnly: [...leftFiles].filter((file) => !rightFiles.has(file)),
|
|
9225
|
+
rightOnly: [...rightFiles].filter((file) => !leftFiles.has(file))
|
|
9226
|
+
}
|
|
9215
9227
|
};
|
|
9216
9228
|
}
|
|
9217
9229
|
|
|
@@ -9307,27 +9319,28 @@ function buildAppsPlan(machineId) {
|
|
|
9307
9319
|
executed: 0
|
|
9308
9320
|
};
|
|
9309
9321
|
}
|
|
9310
|
-
function getAppsStatus(machineId) {
|
|
9322
|
+
function getAppsStatus(machineId, runner = runMachineCommand) {
|
|
9311
9323
|
const machine = resolveMachine(machineId);
|
|
9324
|
+
const readiness = requireMachineCommandSuccess("Apps status readiness check", runner(machine.id, "true"));
|
|
9312
9325
|
const apps = (machine.apps || []).map((app) => {
|
|
9313
|
-
const probe =
|
|
9326
|
+
const probe = requireMachineCommandSuccess(`App probe ${app.name}`, runner(machine.id, buildAppProbeCommand(machine, app)));
|
|
9314
9327
|
return parseProbeOutput(app, machine, probe.stdout);
|
|
9315
9328
|
});
|
|
9316
9329
|
return {
|
|
9317
9330
|
machineId: machine.id,
|
|
9318
|
-
source:
|
|
9331
|
+
source: readiness.source,
|
|
9319
9332
|
apps
|
|
9320
9333
|
};
|
|
9321
9334
|
}
|
|
9322
|
-
function diffApps(machineId) {
|
|
9323
|
-
const status = getAppsStatus(machineId);
|
|
9335
|
+
function diffApps(machineId, runner = runMachineCommand) {
|
|
9336
|
+
const status = getAppsStatus(machineId, runner);
|
|
9324
9337
|
return {
|
|
9325
9338
|
...status,
|
|
9326
9339
|
missing: status.apps.filter((app) => !app.installed).map((app) => app.name),
|
|
9327
9340
|
installed: status.apps.filter((app) => app.installed).map((app) => app.name)
|
|
9328
9341
|
};
|
|
9329
9342
|
}
|
|
9330
|
-
function runAppsInstall(machineId, options = {}) {
|
|
9343
|
+
function runAppsInstall(machineId, options = {}, runner = runMachineCommand) {
|
|
9331
9344
|
const plan = buildAppsPlan(machineId);
|
|
9332
9345
|
if (!options.apply)
|
|
9333
9346
|
return plan;
|
|
@@ -9336,14 +9349,7 @@ function runAppsInstall(machineId, options = {}) {
|
|
|
9336
9349
|
}
|
|
9337
9350
|
let executed = 0;
|
|
9338
9351
|
for (const step of plan.steps) {
|
|
9339
|
-
|
|
9340
|
-
stdout: "pipe",
|
|
9341
|
-
stderr: "pipe",
|
|
9342
|
-
env: process.env
|
|
9343
|
-
});
|
|
9344
|
-
if (result.exitCode !== 0) {
|
|
9345
|
-
throw new Error(`App install failed (${step.id}): ${result.stderr.toString().trim()}`);
|
|
9346
|
-
}
|
|
9352
|
+
requireMachineCommandSuccess(`App install ${step.id}`, runner(plan.machineId, step.command));
|
|
9347
9353
|
executed += 1;
|
|
9348
9354
|
}
|
|
9349
9355
|
return {
|
|
@@ -9414,25 +9420,28 @@ function buildClaudeInstallPlan(machineId, tools) {
|
|
|
9414
9420
|
executed: 0
|
|
9415
9421
|
};
|
|
9416
9422
|
}
|
|
9417
|
-
function getClaudeCliStatus(machineId, tools) {
|
|
9423
|
+
function getClaudeCliStatus(machineId, tools, runner = runMachineCommand) {
|
|
9418
9424
|
const machine = resolveMachine2(machineId);
|
|
9419
9425
|
const normalizedTools = normalizeTools(tools);
|
|
9420
|
-
const route =
|
|
9426
|
+
const route = requireMachineCommandSuccess("AI CLI status readiness check", runner(machine.id, "true")).source;
|
|
9421
9427
|
return {
|
|
9422
9428
|
machineId: machine.id,
|
|
9423
9429
|
source: route,
|
|
9424
|
-
tools: normalizedTools.map((tool) =>
|
|
9430
|
+
tools: normalizedTools.map((tool) => {
|
|
9431
|
+
const result = requireMachineCommandSuccess(`AI CLI probe ${tool}`, runner(machine.id, buildProbeCommand(tool)));
|
|
9432
|
+
return parseProbe(tool, result.stdout);
|
|
9433
|
+
})
|
|
9425
9434
|
};
|
|
9426
9435
|
}
|
|
9427
|
-
function diffClaudeCli(machineId, tools) {
|
|
9428
|
-
const status = getClaudeCliStatus(machineId, tools);
|
|
9436
|
+
function diffClaudeCli(machineId, tools, runner = runMachineCommand) {
|
|
9437
|
+
const status = getClaudeCliStatus(machineId, tools, runner);
|
|
9429
9438
|
return {
|
|
9430
9439
|
...status,
|
|
9431
9440
|
missing: status.tools.filter((tool) => !tool.installed).map((tool) => tool.tool),
|
|
9432
9441
|
installed: status.tools.filter((tool) => tool.installed).map((tool) => tool.tool)
|
|
9433
9442
|
};
|
|
9434
9443
|
}
|
|
9435
|
-
function runClaudeInstall(machineId, tools, options = {}) {
|
|
9444
|
+
function runClaudeInstall(machineId, tools, options = {}, runner = runMachineCommand) {
|
|
9436
9445
|
const plan = buildClaudeInstallPlan(machineId, tools);
|
|
9437
9446
|
if (!options.apply)
|
|
9438
9447
|
return plan;
|
|
@@ -9441,14 +9450,7 @@ function runClaudeInstall(machineId, tools, options = {}) {
|
|
|
9441
9450
|
}
|
|
9442
9451
|
let executed = 0;
|
|
9443
9452
|
for (const step of plan.steps) {
|
|
9444
|
-
|
|
9445
|
-
stdout: "pipe",
|
|
9446
|
-
stderr: "pipe",
|
|
9447
|
-
env: process.env
|
|
9448
|
-
});
|
|
9449
|
-
if (result.exitCode !== 0) {
|
|
9450
|
-
throw new Error(`AI CLI install failed (${step.id}): ${result.stderr.toString().trim()}`);
|
|
9451
|
-
}
|
|
9453
|
+
requireMachineCommandSuccess(`AI CLI install ${step.id}`, runner(plan.machineId, step.command));
|
|
9452
9454
|
executed += 1;
|
|
9453
9455
|
}
|
|
9454
9456
|
return {
|
|
@@ -9500,7 +9502,7 @@ function buildTailscaleInstallPlan(machineId) {
|
|
|
9500
9502
|
executed: 0
|
|
9501
9503
|
};
|
|
9502
9504
|
}
|
|
9503
|
-
function runTailscaleInstall(machineId, options = {}) {
|
|
9505
|
+
function runTailscaleInstall(machineId, options = {}, runner = runMachineCommand) {
|
|
9504
9506
|
const plan = buildTailscaleInstallPlan(machineId);
|
|
9505
9507
|
if (!options.apply)
|
|
9506
9508
|
return plan;
|
|
@@ -9509,14 +9511,7 @@ function runTailscaleInstall(machineId, options = {}) {
|
|
|
9509
9511
|
}
|
|
9510
9512
|
let executed = 0;
|
|
9511
9513
|
for (const step of plan.steps) {
|
|
9512
|
-
|
|
9513
|
-
stdout: "pipe",
|
|
9514
|
-
stderr: "pipe",
|
|
9515
|
-
env: process.env
|
|
9516
|
-
});
|
|
9517
|
-
if (result.exitCode !== 0) {
|
|
9518
|
-
throw new Error(`Tailscale install failed (${step.id}): ${result.stderr.toString().trim()}`);
|
|
9519
|
-
}
|
|
9514
|
+
requireMachineCommandSuccess(`Tailscale install ${step.id}`, runner(plan.machineId, step.command));
|
|
9520
9515
|
executed += 1;
|
|
9521
9516
|
}
|
|
9522
9517
|
return {
|
|
@@ -10557,16 +10552,17 @@ function quote4(value) {
|
|
|
10557
10552
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
10558
10553
|
}
|
|
10559
10554
|
function packageCheckCommand(machine, packageName, manager = machine.platform === "macos" ? "brew" : "apt") {
|
|
10555
|
+
const quotedPackageName = quote4(packageName);
|
|
10560
10556
|
if (manager === "bun") {
|
|
10561
|
-
return `bun pm ls -g --all | grep -F ${
|
|
10557
|
+
return `if bun pm ls -g --all 2>/dev/null | grep -F ${quotedPackageName} >/dev/null 2>&1; then printf 'installed=1\\n'; else printf 'installed=0\\n'; fi`;
|
|
10562
10558
|
}
|
|
10563
10559
|
if (manager === "brew") {
|
|
10564
|
-
return `brew list --versions ${
|
|
10560
|
+
return `if brew list --versions ${quotedPackageName} >/dev/null 2>&1; then printf 'installed=1\\n'; else printf 'installed=0\\n'; fi`;
|
|
10565
10561
|
}
|
|
10566
10562
|
if (manager === "apt") {
|
|
10567
|
-
return `dpkg -s ${
|
|
10563
|
+
return `if dpkg -s ${quotedPackageName} >/dev/null 2>&1; then printf 'installed=1\\n'; else printf 'installed=0\\n'; fi`;
|
|
10568
10564
|
}
|
|
10569
|
-
return `command -v ${
|
|
10565
|
+
return `if command -v ${quotedPackageName} >/dev/null 2>&1; then printf 'installed=1\\n'; else printf 'installed=0\\n'; fi`;
|
|
10570
10566
|
}
|
|
10571
10567
|
function packageInstallCommand(machine, packageName, manager = machine.platform === "macos" ? "brew" : "apt") {
|
|
10572
10568
|
if (manager === "bun") {
|
|
@@ -10580,15 +10576,15 @@ function packageInstallCommand(machine, packageName, manager = machine.platform
|
|
|
10580
10576
|
}
|
|
10581
10577
|
return packageName;
|
|
10582
10578
|
}
|
|
10583
|
-
function detectPackageActions(machine) {
|
|
10579
|
+
function detectPackageActions(machine, runner) {
|
|
10584
10580
|
return (machine.packages || []).map((pkg, index) => {
|
|
10585
10581
|
const manager = pkg.manager || (machine.platform === "macos" ? "brew" : "apt");
|
|
10586
|
-
const check =
|
|
10587
|
-
|
|
10588
|
-
|
|
10589
|
-
|
|
10590
|
-
|
|
10591
|
-
|
|
10582
|
+
const check = runner(machine.id, packageCheckCommand(machine, pkg.name, manager));
|
|
10583
|
+
if (check.exitCode !== 0) {
|
|
10584
|
+
throw new Error(describeMachineCommandFailure(`Sync package probe ${pkg.name}`, check));
|
|
10585
|
+
}
|
|
10586
|
+
const installed = check.stdout.split(`
|
|
10587
|
+
`).some((line) => line.trim() === "installed=1");
|
|
10592
10588
|
return {
|
|
10593
10589
|
id: `package-${index + 1}`,
|
|
10594
10590
|
title: `${installed ? "Package present" : "Install package"} ${pkg.name}`,
|
|
@@ -10599,6 +10595,9 @@ function detectPackageActions(machine) {
|
|
|
10599
10595
|
});
|
|
10600
10596
|
}
|
|
10601
10597
|
function detectFileActions(machine) {
|
|
10598
|
+
if ((machine.files || []).length > 0 && resolveMachineCommand(machine.id, "true").source !== "local") {
|
|
10599
|
+
throw new Error(`Remote file sync planning is not supported for ${machine.id}; refusing to inspect or apply local paths as remote state.`);
|
|
10600
|
+
}
|
|
10602
10601
|
return (machine.files || []).map((file, index) => {
|
|
10603
10602
|
const sourceExists = existsSync9(file.source);
|
|
10604
10603
|
const targetExists = existsSync9(file.target);
|
|
@@ -10622,7 +10621,7 @@ function detectFileActions(machine) {
|
|
|
10622
10621
|
};
|
|
10623
10622
|
});
|
|
10624
10623
|
}
|
|
10625
|
-
function buildSyncPlan(machineId) {
|
|
10624
|
+
function buildSyncPlan(machineId, runner = runMachineCommand) {
|
|
10626
10625
|
const manifest = readManifest();
|
|
10627
10626
|
const currentMachineId = getLocalMachineId();
|
|
10628
10627
|
const selected = machineId ? manifest.machines.find((machine) => machine.id === machineId) : manifest.machines.find((machine) => machine.id === currentMachineId);
|
|
@@ -10632,7 +10631,7 @@ function buildSyncPlan(machineId) {
|
|
|
10632
10631
|
workspacePath: `${homedir7()}/workspace`
|
|
10633
10632
|
};
|
|
10634
10633
|
const actions = [
|
|
10635
|
-
...detectPackageActions(target),
|
|
10634
|
+
...detectPackageActions(target, runner),
|
|
10636
10635
|
...detectFileActions(target)
|
|
10637
10636
|
];
|
|
10638
10637
|
return {
|
|
@@ -10662,8 +10661,8 @@ function applyFileAction(command) {
|
|
|
10662
10661
|
symlinkSync(sourcePath, targetPath);
|
|
10663
10662
|
}
|
|
10664
10663
|
}
|
|
10665
|
-
function runSync(machineId, options = {}) {
|
|
10666
|
-
const plan = buildSyncPlan(machineId);
|
|
10664
|
+
function runSync(machineId, options = {}, runner = runMachineCommand) {
|
|
10665
|
+
const plan = buildSyncPlan(machineId, runner);
|
|
10667
10666
|
if (!options.apply) {
|
|
10668
10667
|
return plan;
|
|
10669
10668
|
}
|
|
@@ -10677,18 +10676,17 @@ function runSync(machineId, options = {}) {
|
|
|
10677
10676
|
if (action.kind === "file") {
|
|
10678
10677
|
applyFileAction(action.command);
|
|
10679
10678
|
} else {
|
|
10680
|
-
const result =
|
|
10681
|
-
stdout: "pipe",
|
|
10682
|
-
stderr: "pipe",
|
|
10683
|
-
env: process.env
|
|
10684
|
-
});
|
|
10679
|
+
const result = runner(plan.machineId, action.command);
|
|
10685
10680
|
if (result.exitCode !== 0) {
|
|
10686
10681
|
recordSyncRun(plan.machineId, "failed", {
|
|
10687
10682
|
executed,
|
|
10688
10683
|
failedAction: action,
|
|
10689
|
-
stderr: result.stderr
|
|
10684
|
+
stderr: result.stderr,
|
|
10685
|
+
stdout: result.stdout,
|
|
10686
|
+
exitCode: result.exitCode,
|
|
10687
|
+
source: result.source
|
|
10690
10688
|
});
|
|
10691
|
-
throw new Error(`Sync action
|
|
10689
|
+
throw new Error(describeMachineCommandFailure(`Sync action ${action.id}`, result));
|
|
10692
10690
|
}
|
|
10693
10691
|
}
|
|
10694
10692
|
executed += 1;
|