@hasna/machines 0.0.30 → 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 +1050 -1058
- 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 +2 -2
package/dist/cli/index.js
CHANGED
|
@@ -3164,15 +3164,9 @@ function print(value, json, text) {
|
|
|
3164
3164
|
else
|
|
3165
3165
|
console.log(text);
|
|
3166
3166
|
}
|
|
3167
|
-
function hasJsonOption(options) {
|
|
3168
|
-
return Boolean(options?.json || options?.opts?.().json || options?.optsWithGlobals?.().json || options?.parent?.opts?.().json || options?.parent?.optsWithGlobals?.().json);
|
|
3169
|
-
}
|
|
3170
|
-
function wantsJson(actionOptions, command) {
|
|
3171
|
-
return hasJsonOption(actionOptions) || hasJsonOption(command);
|
|
3172
|
-
}
|
|
3173
3167
|
function registerWebhookCommands(program2, options) {
|
|
3174
3168
|
const webhooks = program2.command(options.webhooksCommandName ?? "webhooks").description("Manage Hasna event webhook subscriptions");
|
|
3175
|
-
webhooks.command("add").description("Add or replace a webhook or command subscription").argument("<target>", "Webhook URL or command binary").requiredOption("--id <id>", "Subscription/channel identifier").option("--transport <kind>", "Transport kind: webhook or command", "webhook").option("--name <name>", "Display name").option("--type <pattern>", "Event type filter, e.g. todos.task.*").option("--source <pattern>", "Event source filter").option("--subject <pattern>", "Event subject filter").option("--severity <pattern>", "Event severity filter").option("--secret <secret>", "Webhook HMAC secret").option("--header <name=value...>", "Webhook header", collectValues, []).option("--arg <arg...>", "Command argument", collectValues, []).option("--timeout-ms <ms>", "Transport timeout in milliseconds", parseNumber).option("--retry-attempts <n>", "Maximum delivery attempts", parseNumber).option("--retry-backoff-ms <ms>", "Initial retry backoff in milliseconds", parseNumber).option("--redact <path...>", "Event field path to redact before delivery", collectValues, []).option("--disabled", "Create channel disabled", false).option("-j, --json", "Print JSON output", false).action(async (target, actionOptions
|
|
3169
|
+
webhooks.command("add").description("Add or replace a webhook or command subscription").argument("<target>", "Webhook URL or command binary").requiredOption("--id <id>", "Subscription/channel identifier").option("--transport <kind>", "Transport kind: webhook or command", "webhook").option("--name <name>", "Display name").option("--type <pattern>", "Event type filter, e.g. todos.task.*").option("--source <pattern>", "Event source filter").option("--subject <pattern>", "Event subject filter").option("--severity <pattern>", "Event severity filter").option("--secret <secret>", "Webhook HMAC secret").option("--header <name=value...>", "Webhook header", collectValues, []).option("--arg <arg...>", "Command argument", collectValues, []).option("--timeout-ms <ms>", "Transport timeout in milliseconds", parseNumber).option("--retry-attempts <n>", "Maximum delivery attempts", parseNumber).option("--retry-backoff-ms <ms>", "Initial retry backoff in milliseconds", parseNumber).option("--redact <path...>", "Event field path to redact before delivery", collectValues, []).option("--disabled", "Create channel disabled", false).option("-j, --json", "Print JSON output", false).action(async (target, actionOptions) => {
|
|
3176
3170
|
const timestamp = new Date().toISOString();
|
|
3177
3171
|
const channel = {
|
|
3178
3172
|
id: actionOptions.id,
|
|
@@ -3193,11 +3187,11 @@ function registerWebhookCommands(program2, options) {
|
|
|
3193
3187
|
throw new Error(`Transport ${actionOptions.transport} is reserved for future use and cannot be added yet`);
|
|
3194
3188
|
}
|
|
3195
3189
|
const saved = await createClient(options).addChannel(channel);
|
|
3196
|
-
print(sanitizeChannelForOutput(saved),
|
|
3190
|
+
print(sanitizeChannelForOutput(saved), Boolean(actionOptions.json), `Added ${saved.transport} channel ${saved.id}`);
|
|
3197
3191
|
});
|
|
3198
|
-
webhooks.command("list").description("List configured subscriptions").option("-j, --json", "Print JSON output", false).action(async (actionOptions
|
|
3192
|
+
webhooks.command("list").description("List configured subscriptions").option("-j, --json", "Print JSON output", false).action(async (actionOptions) => {
|
|
3199
3193
|
const channels = await createClient(options).listChannels();
|
|
3200
|
-
if (
|
|
3194
|
+
if (actionOptions.json) {
|
|
3201
3195
|
console.log(JSON.stringify(sanitizeChannelsForOutput(channels), null, 2));
|
|
3202
3196
|
return;
|
|
3203
3197
|
}
|
|
@@ -3209,11 +3203,11 @@ function registerWebhookCommands(program2, options) {
|
|
|
3209
3203
|
console.log(`${channel.id} ${channel.enabled ? "enabled" : "disabled"} ${channel.transport} ${channel.webhook?.url ?? channel.command?.command ?? channel.transport}`);
|
|
3210
3204
|
}
|
|
3211
3205
|
});
|
|
3212
|
-
webhooks.command("remove").description("Remove a subscription").argument("<id>", "Subscription/channel identifier").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions
|
|
3206
|
+
webhooks.command("remove").description("Remove a subscription").argument("<id>", "Subscription/channel identifier").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions) => {
|
|
3213
3207
|
const removed = await createClient(options).removeChannel(id);
|
|
3214
|
-
print({ removed },
|
|
3208
|
+
print({ removed }, Boolean(actionOptions.json), removed ? `Removed ${id}` : `Channel not found: ${id}`);
|
|
3215
3209
|
});
|
|
3216
|
-
webhooks.command("test").description("Send a test event to one subscription").argument("<id>", "Subscription/channel identifier").option("--type <type>", "Event type", "events.test").option("--subject <subject>", "Event subject").option("--message <message>", "Event message", "Hasna events test delivery").option("--data <json>", "Event data JSON object").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions
|
|
3210
|
+
webhooks.command("test").description("Send a test event to one subscription").argument("<id>", "Subscription/channel identifier").option("--type <type>", "Event type", "events.test").option("--subject <subject>", "Event subject").option("--message <message>", "Event message", "Hasna events test delivery").option("--data <json>", "Event data JSON object").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions) => {
|
|
3217
3211
|
const result = await createClient(options).testChannel(id, {
|
|
3218
3212
|
source: options.source,
|
|
3219
3213
|
type: actionOptions.type,
|
|
@@ -3221,13 +3215,13 @@ function registerWebhookCommands(program2, options) {
|
|
|
3221
3215
|
message: actionOptions.message,
|
|
3222
3216
|
data: parseJsonObject(actionOptions.data, { test: true })
|
|
3223
3217
|
});
|
|
3224
|
-
print(result,
|
|
3218
|
+
print(result, Boolean(actionOptions.json), `${result.status}: ${result.channelId}`);
|
|
3225
3219
|
});
|
|
3226
3220
|
return webhooks;
|
|
3227
3221
|
}
|
|
3228
3222
|
function registerEventCommands(program2, options) {
|
|
3229
3223
|
const events = program2.command(options.eventsCommandName ?? "events").description("Emit, list, and replay Hasna events");
|
|
3230
|
-
events.command("emit").description("Emit an event from this app").argument("<type>", "Event type").option("--source <source>", "Event source override").option("--subject <subject>", "Event subject").option("--severity <severity>", "Event severity", "info").option("--message <message>", "Event message").option("--dedupe-key <key>", "Dedupe key").option("--data <json>", "Event data JSON object").option("--metadata <json>", "Event metadata JSON object").option("--no-deliver", "Record without delivering").option("--no-dedupe", "Allow duplicate id/dedupeKey events").option("-j, --json", "Print JSON output", false).action(async (type, actionOptions
|
|
3224
|
+
events.command("emit").description("Emit an event from this app").argument("<type>", "Event type").option("--source <source>", "Event source override").option("--subject <subject>", "Event subject").option("--severity <severity>", "Event severity", "info").option("--message <message>", "Event message").option("--dedupe-key <key>", "Dedupe key").option("--data <json>", "Event data JSON object").option("--metadata <json>", "Event metadata JSON object").option("--no-deliver", "Record without delivering").option("--no-dedupe", "Allow duplicate id/dedupeKey events").option("-j, --json", "Print JSON output", false).action(async (type, actionOptions) => {
|
|
3231
3225
|
const result = await createClient(options).emit({
|
|
3232
3226
|
source: actionOptions.source ?? options.source,
|
|
3233
3227
|
type,
|
|
@@ -3238,9 +3232,9 @@ function registerEventCommands(program2, options) {
|
|
|
3238
3232
|
data: parseJsonObject(actionOptions.data, {}),
|
|
3239
3233
|
metadata: parseJsonObject(actionOptions.metadata, {})
|
|
3240
3234
|
}, { deliver: actionOptions.deliver, dedupe: actionOptions.dedupe });
|
|
3241
|
-
print(result,
|
|
3235
|
+
print(result, Boolean(actionOptions.json), `${result.deduped ? "Deduped" : "Emitted"} ${result.event.id} to ${result.deliveries.length} channel(s)`);
|
|
3242
3236
|
});
|
|
3243
|
-
events.command("list").description("List recorded events").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--limit <n>", "Limit results", parseNumber).option("-j, --json", "Print JSON output", false).action(async (actionOptions
|
|
3237
|
+
events.command("list").description("List recorded events").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--limit <n>", "Limit results", parseNumber).option("-j, --json", "Print JSON output", false).action(async (actionOptions) => {
|
|
3244
3238
|
let rows = await createClient(options).listEvents();
|
|
3245
3239
|
if (actionOptions.source)
|
|
3246
3240
|
rows = rows.filter((event) => event.source === actionOptions.source);
|
|
@@ -3248,7 +3242,7 @@ function registerEventCommands(program2, options) {
|
|
|
3248
3242
|
rows = rows.filter((event) => event.type === actionOptions.type);
|
|
3249
3243
|
if (actionOptions.limit)
|
|
3250
3244
|
rows = rows.slice(-actionOptions.limit);
|
|
3251
|
-
if (
|
|
3245
|
+
if (actionOptions.json) {
|
|
3252
3246
|
console.log(JSON.stringify(rows, null, 2));
|
|
3253
3247
|
return;
|
|
3254
3248
|
}
|
|
@@ -3259,14 +3253,14 @@ function registerEventCommands(program2, options) {
|
|
|
3259
3253
|
for (const event of rows)
|
|
3260
3254
|
console.log(`${event.time} ${event.id} ${event.source} ${event.type} ${event.severity}`);
|
|
3261
3255
|
});
|
|
3262
|
-
events.command("replay").description("Replay recorded events").option("--id <id>", "Replay one event id").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--dry-run", "Preview without delivery", false).option("-j, --json", "Print JSON output", false).action(async (actionOptions
|
|
3256
|
+
events.command("replay").description("Replay recorded events").option("--id <id>", "Replay one event id").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--dry-run", "Preview without delivery", false).option("-j, --json", "Print JSON output", false).action(async (actionOptions) => {
|
|
3263
3257
|
const result = await createClient(options).replay({
|
|
3264
3258
|
eventId: actionOptions.id,
|
|
3265
3259
|
source: actionOptions.source,
|
|
3266
3260
|
type: actionOptions.type,
|
|
3267
3261
|
dryRun: actionOptions.dryRun
|
|
3268
3262
|
});
|
|
3269
|
-
print(result,
|
|
3263
|
+
print(result, Boolean(actionOptions.json), `Replayed ${result.events.length} event(s), ${result.deliveries.length} delivery result(s)`);
|
|
3270
3264
|
});
|
|
3271
3265
|
return events;
|
|
3272
3266
|
}
|
|
@@ -7918,996 +7912,610 @@ function manifestValidate() {
|
|
|
7918
7912
|
// src/commands/setup.ts
|
|
7919
7913
|
import { homedir as homedir3 } from "os";
|
|
7920
7914
|
init_db();
|
|
7921
|
-
|
|
7922
|
-
|
|
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 };
|
|
7923
7944
|
}
|
|
7924
|
-
function
|
|
7925
|
-
const
|
|
7926
|
-
|
|
7927
|
-
|
|
7928
|
-
|
|
7929
|
-
|
|
7930
|
-
|
|
7931
|
-
|
|
7932
|
-
|
|
7933
|
-
id: "bun",
|
|
7934
|
-
title: "Install Bun if missing",
|
|
7935
|
-
command: "command -v bun >/dev/null 2>&1 || curl -fsSL https://bun.sh/install | bash",
|
|
7936
|
-
manager: "shell"
|
|
7937
|
-
}
|
|
7938
|
-
];
|
|
7939
|
-
if (machine.platform === "linux") {
|
|
7940
|
-
steps.push({
|
|
7941
|
-
id: "apt-base",
|
|
7942
|
-
title: "Install core Linux tooling",
|
|
7943
|
-
command: "sudo apt-get update && sudo apt-get install -y git curl unzip build-essential",
|
|
7944
|
-
manager: "apt",
|
|
7945
|
-
privileged: true
|
|
7946
|
-
});
|
|
7947
|
-
} else if (machine.platform === "macos") {
|
|
7948
|
-
steps.push({
|
|
7949
|
-
id: "brew-base",
|
|
7950
|
-
title: "Install Homebrew if missing",
|
|
7951
|
-
command: 'command -v brew >/dev/null 2>&1 || /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"',
|
|
7952
|
-
manager: "brew"
|
|
7953
|
-
});
|
|
7954
|
-
steps.push({
|
|
7955
|
-
id: "brew-core",
|
|
7956
|
-
title: "Install core macOS tooling",
|
|
7957
|
-
command: "brew install git coreutils",
|
|
7958
|
-
manager: "brew"
|
|
7959
|
-
});
|
|
7960
|
-
}
|
|
7961
|
-
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;
|
|
7962
7954
|
}
|
|
7963
|
-
function
|
|
7964
|
-
|
|
7965
|
-
|
|
7966
|
-
|
|
7967
|
-
if (manager === "bun") {
|
|
7968
|
-
command = `bun install -g ${quote(pkg.name)}`;
|
|
7969
|
-
} else if (manager === "brew") {
|
|
7970
|
-
command = `brew install ${quote(pkg.name)}`;
|
|
7971
|
-
} else if (manager === "apt") {
|
|
7972
|
-
command = `sudo apt-get install -y ${quote(pkg.name)}`;
|
|
7973
|
-
}
|
|
7974
|
-
return {
|
|
7975
|
-
id: `package-${index + 1}`,
|
|
7976
|
-
title: `Install package ${pkg.name}`,
|
|
7977
|
-
command,
|
|
7978
|
-
manager,
|
|
7979
|
-
privileged: manager === "apt"
|
|
7980
|
-
};
|
|
7955
|
+
function defaultRunner(command) {
|
|
7956
|
+
const result = spawnSync("bash", ["-c", command], {
|
|
7957
|
+
encoding: "utf8",
|
|
7958
|
+
env: process.env
|
|
7981
7959
|
});
|
|
7982
|
-
}
|
|
7983
|
-
function buildSetupPlan(machineId) {
|
|
7984
|
-
const manifest = readManifest();
|
|
7985
|
-
const currentMachineId = getLocalMachineId();
|
|
7986
|
-
const selected = machineId ? manifest.machines.find((machine) => machine.id === machineId) : manifest.machines.find((machine) => machine.id === currentMachineId);
|
|
7987
|
-
const target = selected || {
|
|
7988
|
-
id: currentMachineId,
|
|
7989
|
-
platform: "linux",
|
|
7990
|
-
workspacePath: `${homedir3()}/workspace`
|
|
7991
|
-
};
|
|
7992
|
-
const steps = [...buildBaseSteps(target), ...buildPackageSteps(target)];
|
|
7993
7960
|
return {
|
|
7994
|
-
|
|
7995
|
-
|
|
7996
|
-
|
|
7997
|
-
executed: 0
|
|
7961
|
+
stdout: result.stdout || "",
|
|
7962
|
+
stderr: result.stderr || "",
|
|
7963
|
+
exitCode: result.status ?? 1
|
|
7998
7964
|
};
|
|
7999
7965
|
}
|
|
8000
|
-
function
|
|
8001
|
-
|
|
8002
|
-
|
|
8003
|
-
|
|
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;
|
|
8004
7977
|
}
|
|
8005
|
-
|
|
8006
|
-
|
|
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;
|
|
8007
7984
|
}
|
|
8008
|
-
|
|
8009
|
-
|
|
8010
|
-
|
|
8011
|
-
|
|
8012
|
-
stderr: "pipe",
|
|
8013
|
-
env: process.env
|
|
8014
|
-
});
|
|
8015
|
-
if (result.exitCode !== 0) {
|
|
8016
|
-
recordSetupRun(plan.machineId, "failed", {
|
|
8017
|
-
executed,
|
|
8018
|
-
failedStep: step,
|
|
8019
|
-
stderr: result.stderr.toString()
|
|
8020
|
-
});
|
|
8021
|
-
throw new Error(`Setup step failed (${step.id}): ${result.stderr.toString().trim()}`);
|
|
8022
|
-
}
|
|
8023
|
-
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;
|
|
8024
7989
|
}
|
|
8025
|
-
const
|
|
8026
|
-
|
|
8027
|
-
|
|
8028
|
-
|
|
8029
|
-
|
|
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);
|
|
8030
8001
|
};
|
|
8031
|
-
|
|
8032
|
-
|
|
8002
|
+
addPeer(status.Self);
|
|
8003
|
+
for (const peer of Object.values(status.Peer ?? {}))
|
|
8004
|
+
addPeer(peer);
|
|
8005
|
+
return peers;
|
|
8033
8006
|
}
|
|
8034
|
-
|
|
8035
|
-
|
|
8036
|
-
|
|
8037
|
-
|
|
8038
|
-
|
|
8039
|
-
|
|
8040
|
-
|
|
8041
|
-
|
|
8042
|
-
var DEFAULT_BACKUP_PREFIX = "machines";
|
|
8043
|
-
function quote2(value) {
|
|
8044
|
-
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));
|
|
8045
8015
|
}
|
|
8046
|
-
function
|
|
8047
|
-
|
|
8048
|
-
|
|
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;
|
|
8049
8025
|
}
|
|
8050
|
-
function
|
|
8051
|
-
const
|
|
8052
|
-
|
|
8053
|
-
return { bucket: primary, bucketSource: MACHINES_BACKUP_BUCKET_ENV };
|
|
8054
|
-
const fallback = readEnv(MACHINES_BACKUP_BUCKET_FALLBACK_ENV);
|
|
8055
|
-
if (fallback)
|
|
8056
|
-
return { bucket: fallback, bucketSource: MACHINES_BACKUP_BUCKET_FALLBACK_ENV };
|
|
8057
|
-
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));
|
|
8058
8029
|
}
|
|
8059
|
-
function
|
|
8060
|
-
const
|
|
8061
|
-
if (
|
|
8062
|
-
return
|
|
8063
|
-
|
|
8064
|
-
if (fallback)
|
|
8065
|
-
return { prefix: fallback, prefixSource: MACHINES_BACKUP_PREFIX_FALLBACK_ENV };
|
|
8066
|
-
return null;
|
|
8030
|
+
function manifestHostReachable(target) {
|
|
8031
|
+
const overrides = envReachableHosts();
|
|
8032
|
+
if (overrides.size === 0)
|
|
8033
|
+
return null;
|
|
8034
|
+
return overrides.has(target);
|
|
8067
8035
|
}
|
|
8068
|
-
function
|
|
8069
|
-
const
|
|
8070
|
-
|
|
8071
|
-
|
|
8072
|
-
if (!bucket) {
|
|
8073
|
-
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 });
|
|
8074
8040
|
}
|
|
8075
|
-
|
|
8076
|
-
|
|
8077
|
-
return {
|
|
8078
|
-
bucket,
|
|
8079
|
-
prefix: explicitPrefix || envPrefix?.prefix || DEFAULT_BACKUP_PREFIX,
|
|
8080
|
-
bucketSource: explicitBucket ? "argument" : envBucket.bucketSource,
|
|
8081
|
-
prefixSource: explicitPrefix ? "argument" : envPrefix?.prefixSource || "default"
|
|
8082
|
-
};
|
|
8083
|
-
}
|
|
8084
|
-
function defaultBackupSources() {
|
|
8085
|
-
const home = homedir4();
|
|
8086
|
-
return [
|
|
8087
|
-
join4(home, ".hasna"),
|
|
8088
|
-
join4(home, ".ssh"),
|
|
8089
|
-
join4(home, ".secrets")
|
|
8090
|
-
];
|
|
8091
|
-
}
|
|
8092
|
-
function buildBackupPlan(bucket, prefix) {
|
|
8093
|
-
const target = resolveBackupTarget({ bucket, prefix });
|
|
8094
|
-
const archivePath = join4(homedir4(), ".hasna", "machines", "backup.tgz");
|
|
8095
|
-
const sources = defaultBackupSources();
|
|
8096
|
-
const steps = [
|
|
8097
|
-
{
|
|
8098
|
-
id: "backup-archive",
|
|
8099
|
-
title: "Create compressed machine backup archive",
|
|
8100
|
-
command: `tar -czf ${quote2(archivePath)} ${sources.map((source) => quote2(source)).join(" ")}`,
|
|
8101
|
-
manager: "shell"
|
|
8102
|
-
},
|
|
8103
|
-
{
|
|
8104
|
-
id: "backup-upload",
|
|
8105
|
-
title: "Upload archive to S3",
|
|
8106
|
-
command: `aws s3 cp ${quote2(archivePath)} ${quote2(`s3://${target.bucket}/${target.prefix}/${hostname3()}-backup.tgz`)}`,
|
|
8107
|
-
manager: "custom"
|
|
8108
|
-
}
|
|
8109
|
-
];
|
|
8110
|
-
return {
|
|
8111
|
-
machineId: process.env["HASNA_MACHINES_MACHINE_ID"] || "local",
|
|
8112
|
-
mode: "plan",
|
|
8113
|
-
steps,
|
|
8114
|
-
executed: 0
|
|
8115
|
-
};
|
|
8116
|
-
}
|
|
8117
|
-
function runBackup(bucket, prefix, options = {}) {
|
|
8118
|
-
const plan = buildBackupPlan(bucket, prefix);
|
|
8119
|
-
if (!options.apply)
|
|
8120
|
-
return plan;
|
|
8121
|
-
if (!options.yes) {
|
|
8122
|
-
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) });
|
|
8123
8043
|
}
|
|
8124
|
-
|
|
8125
|
-
|
|
8126
|
-
const result = Bun.spawnSync(["bash", "-lc", step.command], {
|
|
8127
|
-
stdout: "pipe",
|
|
8128
|
-
stderr: "pipe",
|
|
8129
|
-
env: process.env
|
|
8130
|
-
});
|
|
8131
|
-
if (result.exitCode !== 0) {
|
|
8132
|
-
throw new Error(`Backup step failed (${step.id}): ${result.stderr.toString().trim()}`);
|
|
8133
|
-
}
|
|
8134
|
-
executed += 1;
|
|
8044
|
+
if (input.manifest?.hostname) {
|
|
8045
|
+
hints.push({ kind: "lan", target: input.manifest.hostname, reachable: manifestHostReachable(input.manifest.hostname) });
|
|
8135
8046
|
}
|
|
8136
|
-
|
|
8137
|
-
|
|
8138
|
-
|
|
8139
|
-
|
|
8140
|
-
|
|
8141
|
-
};
|
|
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;
|
|
8142
8052
|
}
|
|
8143
|
-
|
|
8144
|
-
|
|
8145
|
-
|
|
8146
|
-
|
|
8147
|
-
|
|
8148
|
-
|
|
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;
|
|
8149
8071
|
}
|
|
8150
|
-
function
|
|
8151
|
-
return
|
|
8072
|
+
function selectRouteHint(hints) {
|
|
8073
|
+
return [...hints].sort((left, right) => routeRank(left) - routeRank(right))[0] ?? null;
|
|
8152
8074
|
}
|
|
8153
|
-
function
|
|
8154
|
-
|
|
8155
|
-
|
|
8156
|
-
|
|
8157
|
-
|
|
8158
|
-
|
|
8159
|
-
|
|
8160
|
-
|
|
8161
|
-
if (platform2() === "darwin") {
|
|
8162
|
-
steps.push({
|
|
8163
|
-
id: "mkcert-install-macos",
|
|
8164
|
-
title: "Install mkcert on macOS",
|
|
8165
|
-
command: "brew install mkcert nss",
|
|
8166
|
-
manager: "brew"
|
|
8167
|
-
});
|
|
8168
|
-
} else {
|
|
8169
|
-
steps.push({
|
|
8170
|
-
id: "mkcert-install-linux",
|
|
8171
|
-
title: "Install mkcert on Linux",
|
|
8172
|
-
command: "sudo apt-get update && sudo apt-get install -y mkcert libnss3-tools",
|
|
8173
|
-
manager: "apt",
|
|
8174
|
-
privileged: true
|
|
8175
|
-
});
|
|
8176
|
-
}
|
|
8177
|
-
steps.push({
|
|
8178
|
-
id: "mkcert-local-ca",
|
|
8179
|
-
title: "Install local mkcert CA",
|
|
8180
|
-
command: "mkcert -install",
|
|
8181
|
-
manager: "custom"
|
|
8182
|
-
}, {
|
|
8183
|
-
id: "mkcert-issue",
|
|
8184
|
-
title: `Issue certificate for ${domains.join(", ")}`,
|
|
8185
|
-
command: `mkdir -p ${quote3(certDir())} && mkcert -cert-file ${quote3(certPath)} -key-file ${quote3(keyPath)} ${domains.map((domain) => quote3(domain)).join(" ")}`,
|
|
8186
|
-
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
|
|
8187
8083
|
});
|
|
8084
|
+
const selectedRoute = selectRouteHint(hints);
|
|
8085
|
+
const route = selectedRoute?.kind === "ssh" ? "ssh" : selectedRoute?.kind ?? "unknown";
|
|
8188
8086
|
return {
|
|
8189
|
-
|
|
8190
|
-
|
|
8191
|
-
|
|
8192
|
-
|
|
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 ?? {}
|
|
8193
8111
|
};
|
|
8194
8112
|
}
|
|
8195
|
-
function
|
|
8196
|
-
const
|
|
8197
|
-
|
|
8198
|
-
|
|
8199
|
-
|
|
8200
|
-
|
|
8201
|
-
|
|
8202
|
-
|
|
8203
|
-
|
|
8204
|
-
|
|
8205
|
-
|
|
8206
|
-
|
|
8207
|
-
|
|
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)
|
|
8208
8137
|
});
|
|
8209
|
-
|
|
8210
|
-
throw new Error(`Certificate step failed (${step.id}): ${result.stderr.toString().trim()}`);
|
|
8211
|
-
}
|
|
8212
|
-
executed += 1;
|
|
8213
|
-
}
|
|
8138
|
+
});
|
|
8214
8139
|
return {
|
|
8215
|
-
|
|
8216
|
-
|
|
8217
|
-
|
|
8218
|
-
|
|
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
|
|
8219
8153
|
};
|
|
8220
8154
|
}
|
|
8221
|
-
|
|
8222
|
-
|
|
8223
|
-
init_paths();
|
|
8224
|
-
import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
8225
|
-
import { join as join6 } from "path";
|
|
8226
|
-
function getDnsPath() {
|
|
8227
|
-
return join6(getDataDir(), "dns.json");
|
|
8228
|
-
}
|
|
8229
|
-
function readMappings() {
|
|
8230
|
-
const path = getDnsPath();
|
|
8231
|
-
if (!existsSync5(path))
|
|
8232
|
-
return [];
|
|
8233
|
-
return JSON.parse(readFileSync3(path, "utf8"));
|
|
8234
|
-
}
|
|
8235
|
-
function writeMappings(mappings) {
|
|
8236
|
-
const path = getDnsPath();
|
|
8237
|
-
ensureParentDir(path);
|
|
8238
|
-
writeFileSync2(path, `${JSON.stringify(mappings, null, 2)}
|
|
8239
|
-
`, "utf8");
|
|
8240
|
-
return path;
|
|
8241
|
-
}
|
|
8242
|
-
function addDomainMapping(domain, port, targetHost = "127.0.0.1") {
|
|
8243
|
-
const mappings = readMappings().filter((entry) => entry.domain !== domain);
|
|
8244
|
-
mappings.push({ domain, port, targetHost });
|
|
8245
|
-
writeMappings(mappings);
|
|
8246
|
-
return mappings.sort((left, right) => left.domain.localeCompare(right.domain));
|
|
8155
|
+
function normalizeMachineAlias(value) {
|
|
8156
|
+
return value.trim().replace(/\.$/, "").toLowerCase();
|
|
8247
8157
|
}
|
|
8248
|
-
function
|
|
8249
|
-
|
|
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);
|
|
8250
8170
|
}
|
|
8251
|
-
function
|
|
8252
|
-
const
|
|
8253
|
-
if (
|
|
8254
|
-
|
|
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
|
+
};
|
|
8255
8178
|
}
|
|
8256
|
-
|
|
8257
|
-
|
|
8258
|
-
|
|
8259
|
-
|
|
8260
|
-
|
|
8261
|
-
}
|
|
8262
|
-
|
|
8263
|
-
|
|
8264
|
-
|
|
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 };
|
|
8265
8197
|
}
|
|
8266
|
-
|
|
8267
|
-
|
|
8268
|
-
|
|
8269
|
-
|
|
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";
|
|
8270
8210
|
}
|
|
8271
|
-
function
|
|
8272
|
-
return (
|
|
8211
|
+
function addMilliseconds(date, milliseconds) {
|
|
8212
|
+
return new Date(date.getTime() + milliseconds).toISOString();
|
|
8273
8213
|
}
|
|
8274
|
-
function
|
|
8275
|
-
|
|
8276
|
-
|
|
8277
|
-
|
|
8278
|
-
|
|
8279
|
-
|
|
8280
|
-
|
|
8281
|
-
|
|
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
|
+
};
|
|
8282
8308
|
}
|
|
8283
|
-
const
|
|
8284
|
-
|
|
8285
|
-
|
|
8286
|
-
|
|
8287
|
-
|
|
8288
|
-
].filter(Boolean);
|
|
8289
|
-
const leftPackages = new Set(packageNames(left));
|
|
8290
|
-
const rightPackages = new Set(packageNames(right));
|
|
8291
|
-
const leftFiles = new Set(fileTargets(left));
|
|
8292
|
-
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);
|
|
8293
8314
|
return {
|
|
8294
|
-
|
|
8295
|
-
|
|
8296
|
-
|
|
8297
|
-
|
|
8298
|
-
|
|
8299
|
-
|
|
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
|
|
8300
8334
|
},
|
|
8301
|
-
|
|
8302
|
-
|
|
8303
|
-
|
|
8304
|
-
|
|
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
|
|
8305
8345
|
};
|
|
8306
8346
|
}
|
|
8307
|
-
|
|
8308
|
-
|
|
8309
|
-
init_db();
|
|
8310
|
-
import { spawnSync as spawnSync2 } from "child_process";
|
|
8311
|
-
import { hostname as hostname5 } from "os";
|
|
8312
|
-
|
|
8313
|
-
// src/topology.ts
|
|
8314
|
-
init_db();
|
|
8315
|
-
import { existsSync as existsSync6 } from "fs";
|
|
8316
|
-
import { arch as arch2, hostname as hostname4, platform as platform3, userInfo as userInfo2 } from "os";
|
|
8317
|
-
import { spawnSync } from "child_process";
|
|
8318
|
-
init_paths();
|
|
8319
|
-
var MACHINES_CONSUMER_CONTRACT_VERSION = 1;
|
|
8320
|
-
var MACHINES_PACKAGE_NAME = "@hasna/machines";
|
|
8321
|
-
var DEFAULT_MACHINE_RESOLVER_TTL_MS = 24 * 60 * 60 * 1000;
|
|
8322
|
-
var MACHINES_CONSUMER_CAPABILITIES = {
|
|
8323
|
-
topology: true,
|
|
8324
|
-
compatibility: true,
|
|
8325
|
-
route_resolution: true,
|
|
8326
|
-
cli_json_fallback: true,
|
|
8327
|
-
workspace_path_mapping: true,
|
|
8328
|
-
workspace_diagnostics: true,
|
|
8329
|
-
schema_artifacts: true,
|
|
8330
|
-
cacheability_metadata: true,
|
|
8331
|
-
resolver_snapshots: true,
|
|
8332
|
-
field_capability_descriptors: true
|
|
8333
|
-
};
|
|
8334
|
-
function getMachinesConsumerCapabilities() {
|
|
8335
|
-
return { ...MACHINES_CONSUMER_CAPABILITIES };
|
|
8347
|
+
function isRecord(value) {
|
|
8348
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
8336
8349
|
}
|
|
8337
|
-
function
|
|
8338
|
-
const
|
|
8339
|
-
|
|
8340
|
-
|
|
8341
|
-
|
|
8342
|
-
|
|
8343
|
-
|
|
8344
|
-
return "linux";
|
|
8345
|
-
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;
|
|
8346
8357
|
}
|
|
8347
|
-
function
|
|
8348
|
-
const
|
|
8349
|
-
|
|
8350
|
-
|
|
8351
|
-
|
|
8352
|
-
return {
|
|
8353
|
-
stdout: result.stdout || "",
|
|
8354
|
-
stderr: result.stderr || "",
|
|
8355
|
-
exitCode: result.status ?? 1
|
|
8356
|
-
};
|
|
8357
|
-
}
|
|
8358
|
-
function hasCommand(command, runner) {
|
|
8359
|
-
return runner(`command -v ${command} >/dev/null 2>&1`).exitCode === 0;
|
|
8360
|
-
}
|
|
8361
|
-
function parseTailscaleStatus(raw) {
|
|
8362
|
-
try {
|
|
8363
|
-
const parsed = JSON.parse(raw);
|
|
8364
|
-
if (!parsed || typeof parsed !== "object")
|
|
8365
|
-
return null;
|
|
8366
|
-
return parsed;
|
|
8367
|
-
} catch {
|
|
8368
|
-
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;
|
|
8369
8363
|
}
|
|
8364
|
+
return null;
|
|
8370
8365
|
}
|
|
8371
|
-
function
|
|
8372
|
-
const
|
|
8373
|
-
|
|
8374
|
-
|
|
8375
|
-
|
|
8376
|
-
}
|
|
8377
|
-
const result = runner("tailscale status --json");
|
|
8378
|
-
if (result.exitCode !== 0) {
|
|
8379
|
-
warnings.push(`tailscale_status_failed:${result.stderr.trim() || result.exitCode}`);
|
|
8380
|
-
return peers;
|
|
8381
|
-
}
|
|
8382
|
-
const status = parseTailscaleStatus(result.stdout);
|
|
8383
|
-
if (!status) {
|
|
8384
|
-
warnings.push("tailscale_status_invalid_json");
|
|
8385
|
-
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");
|
|
8386
8371
|
}
|
|
8387
|
-
|
|
8388
|
-
if (!peer)
|
|
8389
|
-
return;
|
|
8390
|
-
const id = peer.HostName || peer.DNSName?.split(".")[0];
|
|
8391
|
-
if (id)
|
|
8392
|
-
peers.set(id, peer);
|
|
8393
|
-
};
|
|
8394
|
-
addPeer(status.Self);
|
|
8395
|
-
for (const peer of Object.values(status.Peer ?? {}))
|
|
8396
|
-
addPeer(peer);
|
|
8397
|
-
return peers;
|
|
8398
|
-
}
|
|
8399
|
-
function machineKeys(machine) {
|
|
8400
|
-
return [
|
|
8401
|
-
machine.id,
|
|
8402
|
-
machine.hostname,
|
|
8403
|
-
machine.tailscaleName?.split(".")[0],
|
|
8404
|
-
machine.tailscaleName,
|
|
8405
|
-
machine.sshAddress?.split("@").pop()
|
|
8406
|
-
].filter((value) => Boolean(value));
|
|
8372
|
+
return [];
|
|
8407
8373
|
}
|
|
8408
|
-
function
|
|
8409
|
-
|
|
8410
|
-
|
|
8411
|
-
|
|
8412
|
-
|
|
8413
|
-
|
|
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
|
+
}
|
|
8414
8388
|
}
|
|
8415
8389
|
}
|
|
8416
|
-
return
|
|
8390
|
+
return null;
|
|
8417
8391
|
}
|
|
8418
|
-
function
|
|
8419
|
-
|
|
8420
|
-
return new Set((raw || "").split(",").map((value) => value.trim()).filter(Boolean));
|
|
8392
|
+
function trimTrailingSlash(value) {
|
|
8393
|
+
return value.replace(/\/+$/, "");
|
|
8421
8394
|
}
|
|
8422
|
-
function
|
|
8423
|
-
|
|
8424
|
-
if (overrides.size === 0)
|
|
8425
|
-
return null;
|
|
8426
|
-
return overrides.has(target);
|
|
8395
|
+
function joinPath(left, right) {
|
|
8396
|
+
return `${trimTrailingSlash(left)}/${right.replace(/^\/+/, "")}`;
|
|
8427
8397
|
}
|
|
8428
|
-
function
|
|
8429
|
-
|
|
8430
|
-
|
|
8431
|
-
|
|
8432
|
-
}
|
|
8433
|
-
|
|
8434
|
-
|
|
8435
|
-
|
|
8436
|
-
if (input.manifest?.hostname) {
|
|
8437
|
-
hints.push({ kind: "lan", target: input.manifest.hostname, reachable: manifestHostReachable(input.manifest.hostname) });
|
|
8438
|
-
}
|
|
8439
|
-
const tailscaleTarget = input.manifest?.tailscaleName ?? input.peer?.DNSName ?? input.peer?.TailscaleIPs?.[0];
|
|
8440
|
-
if (tailscaleTarget) {
|
|
8441
|
-
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}`);
|
|
8442
8406
|
}
|
|
8443
|
-
return
|
|
8407
|
+
return joinPath(root, repoName);
|
|
8444
8408
|
}
|
|
8445
|
-
function
|
|
8446
|
-
|
|
8447
|
-
return 0;
|
|
8448
|
-
if (hint.reachable === true && hint.kind === "ssh")
|
|
8449
|
-
return 1;
|
|
8450
|
-
if (hint.reachable === true && hint.kind === "lan")
|
|
8451
|
-
return 2;
|
|
8452
|
-
if (hint.reachable === true && hint.kind === "tailscale")
|
|
8453
|
-
return 3;
|
|
8454
|
-
if (hint.reachable === false)
|
|
8455
|
-
return 8;
|
|
8456
|
-
if (hint.kind === "ssh")
|
|
8457
|
-
return 4;
|
|
8458
|
-
if (hint.kind === "lan")
|
|
8459
|
-
return 5;
|
|
8460
|
-
if (hint.kind === "tailscale")
|
|
8461
|
-
return 6;
|
|
8462
|
-
return 9;
|
|
8409
|
+
function shellQuote(value) {
|
|
8410
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
8463
8411
|
}
|
|
8464
|
-
function
|
|
8465
|
-
return
|
|
8412
|
+
function shellCommand(command) {
|
|
8413
|
+
return command.map(shellQuote).join(" ");
|
|
8466
8414
|
}
|
|
8467
|
-
function
|
|
8468
|
-
|
|
8469
|
-
|
|
8470
|
-
|
|
8471
|
-
|
|
8472
|
-
|
|
8473
|
-
manifest,
|
|
8474
|
-
peer
|
|
8475
|
-
});
|
|
8476
|
-
const selectedRoute = selectRouteHint(hints);
|
|
8477
|
-
const route = selectedRoute?.kind === "ssh" ? "ssh" : selectedRoute?.kind ?? "unknown";
|
|
8478
|
-
return {
|
|
8479
|
-
machine_id: input.machineId,
|
|
8480
|
-
hostname: manifest?.hostname ?? peer?.HostName ?? null,
|
|
8481
|
-
platform: manifest?.platform ?? (peer?.OS ? normalizePlatform2(peer.OS) : null),
|
|
8482
|
-
os: peer?.OS ?? null,
|
|
8483
|
-
user: typeof manifest?.metadata?.user === "string" ? manifest.metadata.user : null,
|
|
8484
|
-
workspace_path: manifest?.workspacePath ?? null,
|
|
8485
|
-
manifest_declared: Boolean(manifest),
|
|
8486
|
-
heartbeat_status: input.heartbeat?.status ?? "unknown",
|
|
8487
|
-
last_heartbeat_at: input.heartbeat?.updated_at ?? null,
|
|
8488
|
-
tailscale: {
|
|
8489
|
-
dns_name: manifest?.tailscaleName ?? peer?.DNSName?.replace(/\.$/, "") ?? null,
|
|
8490
|
-
ips: peer?.TailscaleIPs ?? [],
|
|
8491
|
-
online: peer?.Online ?? null,
|
|
8492
|
-
active: peer?.Active ?? null,
|
|
8493
|
-
last_seen: peer?.LastSeen ?? null
|
|
8494
|
-
},
|
|
8495
|
-
ssh: {
|
|
8496
|
-
address: manifest?.sshAddress ?? null,
|
|
8497
|
-
route,
|
|
8498
|
-
command_target: selectedRoute?.target ?? null
|
|
8499
|
-
},
|
|
8500
|
-
route_hints: hints,
|
|
8501
|
-
tags: manifest?.tags ?? [],
|
|
8502
|
-
metadata: manifest?.metadata ?? {}
|
|
8503
|
-
};
|
|
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");
|
|
8504
8421
|
}
|
|
8505
|
-
function
|
|
8506
|
-
|
|
8507
|
-
|
|
8508
|
-
|
|
8509
|
-
|
|
8510
|
-
|
|
8511
|
-
const
|
|
8512
|
-
|
|
8513
|
-
|
|
8514
|
-
|
|
8515
|
-
|
|
8516
|
-
|
|
8517
|
-
|
|
8518
|
-
|
|
8519
|
-
]
|
|
8520
|
-
|
|
8521
|
-
|
|
8522
|
-
|
|
8523
|
-
|
|
8524
|
-
|
|
8525
|
-
|
|
8526
|
-
manifest: manifestMachine,
|
|
8527
|
-
peer: findTailscalePeer(manifestMachine ?? null, machineId, peers),
|
|
8528
|
-
heartbeat: heartbeatByMachine.get(machineId)
|
|
8529
|
-
});
|
|
8530
|
-
});
|
|
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"];
|
|
8531
8443
|
return {
|
|
8532
|
-
|
|
8533
|
-
|
|
8534
|
-
|
|
8535
|
-
|
|
8536
|
-
|
|
8537
|
-
|
|
8538
|
-
generated_at: now2.toISOString(),
|
|
8539
|
-
local_machine_id: localMachineId,
|
|
8540
|
-
local_hostname: hostname4(),
|
|
8541
|
-
current_platform: normalizePlatform2(),
|
|
8542
|
-
manifest_path_known: existsSync6(getManifestPath()),
|
|
8543
|
-
machines,
|
|
8544
|
-
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)
|
|
8545
8450
|
};
|
|
8546
8451
|
}
|
|
8547
|
-
function
|
|
8548
|
-
|
|
8549
|
-
}
|
|
8550
|
-
function routeTargetMatches(machine, requested) {
|
|
8551
|
-
const normalized = normalizeMachineAlias(requested);
|
|
8552
|
-
const values = [
|
|
8553
|
-
machine.ssh.address,
|
|
8554
|
-
machine.ssh.command_target,
|
|
8555
|
-
machine.tailscale.dns_name,
|
|
8556
|
-
machine.tailscale.dns_name?.split(".")[0],
|
|
8557
|
-
...machine.tailscale.ips,
|
|
8558
|
-
...machine.route_hints.map((hint) => hint.target),
|
|
8559
|
-
...machine.route_hints.map((hint) => hint.target.split("@").pop() ?? hint.target)
|
|
8560
|
-
].filter((value) => Boolean(value));
|
|
8561
|
-
return values.some((value) => normalizeMachineAlias(value) === normalized);
|
|
8562
|
-
}
|
|
8563
|
-
function findRouteMachine(topology, requestedMachineId) {
|
|
8564
|
-
const requested = normalizeMachineAlias(requestedMachineId);
|
|
8565
|
-
if (requested === "local" || requested === "localhost" || requested === normalizeMachineAlias(hostname4()) || requested === normalizeMachineAlias(topology.local_machine_id)) {
|
|
8452
|
+
function pathDiagnostic(input) {
|
|
8453
|
+
if (!input.path.path) {
|
|
8566
8454
|
return {
|
|
8567
|
-
|
|
8568
|
-
|
|
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
|
|
8569
8462
|
};
|
|
8570
8463
|
}
|
|
8571
|
-
|
|
8572
|
-
if (machineIdMatch)
|
|
8573
|
-
return { machine: machineIdMatch, matchedBy: "machine_id" };
|
|
8574
|
-
const hostnameMatch = topology.machines.find((machine) => machine.hostname && normalizeMachineAlias(machine.hostname) === requested);
|
|
8575
|
-
if (hostnameMatch)
|
|
8576
|
-
return { machine: hostnameMatch, matchedBy: "hostname" };
|
|
8577
|
-
const tailscaleMatch = topology.machines.find((machine) => {
|
|
8578
|
-
if (!machine.tailscale.dns_name)
|
|
8579
|
-
return false;
|
|
8580
|
-
const dns = normalizeMachineAlias(machine.tailscale.dns_name);
|
|
8581
|
-
return dns === requested || dns.split(".")[0] === requested;
|
|
8582
|
-
});
|
|
8583
|
-
if (tailscaleMatch)
|
|
8584
|
-
return { machine: tailscaleMatch, matchedBy: "tailscale" };
|
|
8585
|
-
const routeMatch = topology.machines.find((machine) => routeTargetMatches(machine, requestedMachineId));
|
|
8586
|
-
if (routeMatch)
|
|
8587
|
-
return { machine: routeMatch, matchedBy: "route_target" };
|
|
8588
|
-
return { machine: null, matchedBy: null };
|
|
8589
|
-
}
|
|
8590
|
-
function routeConfidence(input) {
|
|
8591
|
-
if (input.matchedBy === "local_alias")
|
|
8592
|
-
return "exact";
|
|
8593
|
-
if (input.hint?.kind === "local")
|
|
8594
|
-
return "exact";
|
|
8595
|
-
if (input.hint?.reachable === true)
|
|
8596
|
-
return "high";
|
|
8597
|
-
if (input.machine.manifest_declared && (input.hint?.kind === "ssh" || input.hint?.kind === "lan"))
|
|
8598
|
-
return "medium";
|
|
8599
|
-
if (input.hint)
|
|
8600
|
-
return "low";
|
|
8601
|
-
return "none";
|
|
8602
|
-
}
|
|
8603
|
-
function addMilliseconds(date, milliseconds) {
|
|
8604
|
-
return new Date(date.getTime() + milliseconds).toISOString();
|
|
8605
|
-
}
|
|
8606
|
-
function routeAuthority(input) {
|
|
8607
|
-
if (!input.machine)
|
|
8608
|
-
return "unresolved";
|
|
8609
|
-
if (input.matchedBy === "fallback")
|
|
8610
|
-
return "fallback";
|
|
8611
|
-
if (input.selectedHint?.kind === "local")
|
|
8612
|
-
return "live_topology";
|
|
8613
|
-
if (input.selectedHint?.kind === "tailscale" || input.machine.tailscale.online !== null)
|
|
8614
|
-
return "live_topology";
|
|
8615
|
-
if (input.machine.manifest_declared)
|
|
8616
|
-
return "manifest";
|
|
8617
|
-
return "open-machines";
|
|
8618
|
-
}
|
|
8619
|
-
function workspaceAuthority(paths) {
|
|
8620
|
-
const sources = [paths.workspace_root.source, paths.project_root.source, paths.open_files_root.source];
|
|
8621
|
-
if (sources.some((source) => source === "argument"))
|
|
8622
|
-
return "argument";
|
|
8623
|
-
if (sources.some((source) => source === "manifest_metadata"))
|
|
8624
|
-
return "manifest_metadata";
|
|
8625
|
-
if (sources.some((source) => source === "manifest"))
|
|
8626
|
-
return "manifest";
|
|
8627
|
-
if (sources.some((source) => source === "inferred"))
|
|
8628
|
-
return "inferred";
|
|
8629
|
-
if (sources.every((source) => source === "unresolved"))
|
|
8630
|
-
return "unresolved";
|
|
8631
|
-
return "open-machines";
|
|
8632
|
-
}
|
|
8633
|
-
function cacheability(input) {
|
|
8634
|
-
const ttlMs = input.ttlMs === undefined ? DEFAULT_MACHINE_RESOLVER_TTL_MS : input.ttlMs;
|
|
8635
|
-
const expiresAt = typeof ttlMs === "number" && ttlMs > 0 ? addMilliseconds(input.observedAt, ttlMs) : null;
|
|
8636
|
-
const stale = expiresAt ? input.now.getTime() > new Date(expiresAt).getTime() : false;
|
|
8637
|
-
const confidenceCacheable = input.confidence !== "none" && input.confidence !== "low";
|
|
8638
|
-
const cacheable = input.ok && confidenceCacheable && !stale && input.authority !== "unresolved";
|
|
8639
|
-
const reasons = [...input.reasons];
|
|
8640
|
-
if (!input.ok)
|
|
8641
|
-
reasons.push("resolver_not_ok");
|
|
8642
|
-
if (!confidenceCacheable)
|
|
8643
|
-
reasons.push(`low_confidence:${input.confidence}`);
|
|
8644
|
-
if (stale)
|
|
8645
|
-
reasons.push("stale");
|
|
8646
|
-
if (input.authority === "unresolved")
|
|
8647
|
-
reasons.push("unresolved_authority");
|
|
8648
|
-
return {
|
|
8649
|
-
observed_at: input.observedAt.toISOString(),
|
|
8650
|
-
verified_at: input.ok ? input.now.toISOString() : null,
|
|
8651
|
-
expires_at: expiresAt,
|
|
8652
|
-
ttl_ms: typeof ttlMs === "number" && ttlMs > 0 ? ttlMs : null,
|
|
8653
|
-
source_authority: input.authority,
|
|
8654
|
-
confidence: input.confidence,
|
|
8655
|
-
cacheable,
|
|
8656
|
-
stale,
|
|
8657
|
-
reasons: [...new Set(reasons)].sort()
|
|
8658
|
-
};
|
|
8659
|
-
}
|
|
8660
|
-
function resolveMachineRoute(machineId, options = {}) {
|
|
8661
|
-
const now2 = options.now ?? new Date;
|
|
8662
|
-
const topology = options.topology ?? discoverMachineTopology(options);
|
|
8663
|
-
const warnings = [...topology.warnings];
|
|
8664
|
-
const { machine, matchedBy } = findRouteMachine(topology, machineId);
|
|
8665
|
-
const generatedAt = now2.toISOString();
|
|
8666
|
-
if (!machine) {
|
|
8667
|
-
warnings.push(`machine_not_found:${machineId}`);
|
|
8464
|
+
if (input.pathExists === false) {
|
|
8668
8465
|
return {
|
|
8669
|
-
|
|
8670
|
-
|
|
8671
|
-
|
|
8672
|
-
|
|
8673
|
-
|
|
8674
|
-
|
|
8675
|
-
|
|
8676
|
-
|
|
8677
|
-
|
|
8678
|
-
|
|
8679
|
-
|
|
8680
|
-
|
|
8681
|
-
|
|
8682
|
-
|
|
8683
|
-
|
|
8684
|
-
|
|
8685
|
-
|
|
8686
|
-
|
|
8687
|
-
selected_hint: null
|
|
8688
|
-
},
|
|
8689
|
-
cacheability: cacheability({
|
|
8690
|
-
ok: false,
|
|
8691
|
-
observedAt: now2,
|
|
8692
|
-
now: now2,
|
|
8693
|
-
ttlMs: options.resolverTtlMs,
|
|
8694
|
-
authority: "unresolved",
|
|
8695
|
-
confidence: "none",
|
|
8696
|
-
reasons: [`machine_not_found:${machineId}`]
|
|
8697
|
-
}),
|
|
8698
|
-
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
|
|
8699
8484
|
};
|
|
8700
8485
|
}
|
|
8701
|
-
const selectedHint = selectRouteHint(machine.route_hints);
|
|
8702
|
-
const route = selectedHint?.kind ?? machine.ssh.route ?? "unknown";
|
|
8703
|
-
const local = route === "local" || machine.machine_id === topology.local_machine_id;
|
|
8704
|
-
const confidence = routeConfidence({ machine, hint: selectedHint, matchedBy });
|
|
8705
|
-
const ok = Boolean(selectedHint?.target);
|
|
8706
8486
|
return {
|
|
8707
|
-
|
|
8708
|
-
|
|
8709
|
-
ok,
|
|
8710
|
-
|
|
8711
|
-
|
|
8712
|
-
|
|
8713
|
-
|
|
8714
|
-
source: route,
|
|
8715
|
-
target: selectedHint?.target ?? null,
|
|
8716
|
-
command_target: selectedHint?.target ?? null,
|
|
8717
|
-
confidence,
|
|
8718
|
-
local,
|
|
8719
|
-
evidence: {
|
|
8720
|
-
topology: true,
|
|
8721
|
-
matched_by: matchedBy,
|
|
8722
|
-
manifest_declared: machine.manifest_declared,
|
|
8723
|
-
heartbeat_status: machine.heartbeat_status,
|
|
8724
|
-
tailscale_online: machine.tailscale.online,
|
|
8725
|
-
selected_hint: selectedHint
|
|
8726
|
-
},
|
|
8727
|
-
cacheability: cacheability({
|
|
8728
|
-
ok,
|
|
8729
|
-
observedAt: now2,
|
|
8730
|
-
now: now2,
|
|
8731
|
-
ttlMs: options.resolverTtlMs,
|
|
8732
|
-
authority: routeAuthority({ machine, selectedHint, matchedBy }),
|
|
8733
|
-
confidence,
|
|
8734
|
-
reasons: selectedHint ? [] : ["route_target_unresolved"]
|
|
8735
|
-
}),
|
|
8736
|
-
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
|
|
8737
8494
|
};
|
|
8738
8495
|
}
|
|
8739
|
-
function
|
|
8740
|
-
|
|
8741
|
-
|
|
8742
|
-
|
|
8743
|
-
|
|
8744
|
-
|
|
8745
|
-
|
|
8746
|
-
|
|
8747
|
-
|
|
8748
|
-
|
|
8749
|
-
|
|
8750
|
-
|
|
8751
|
-
|
|
8752
|
-
|
|
8753
|
-
|
|
8754
|
-
|
|
8755
|
-
|
|
8756
|
-
|
|
8757
|
-
|
|
8758
|
-
|
|
8759
|
-
|
|
8760
|
-
|
|
8761
|
-
|
|
8762
|
-
return value.filter((entry) => typeof entry === "string");
|
|
8763
|
-
}
|
|
8764
|
-
return [];
|
|
8765
|
-
}
|
|
8766
|
-
function readMappedPath(input) {
|
|
8767
|
-
for (const containerName of input.containers) {
|
|
8768
|
-
const container = input.metadata[containerName];
|
|
8769
|
-
if (!isRecord(container))
|
|
8770
|
-
continue;
|
|
8771
|
-
for (const key of input.keys) {
|
|
8772
|
-
const value = container[key];
|
|
8773
|
-
if (typeof value === "string" && value.trim())
|
|
8774
|
-
return value.trim();
|
|
8775
|
-
if (isRecord(value)) {
|
|
8776
|
-
const path = metadataString(value, ["path", "root", "workspacePath", "workspace_path"]);
|
|
8777
|
-
if (path)
|
|
8778
|
-
return path;
|
|
8779
|
-
}
|
|
8780
|
-
}
|
|
8781
|
-
}
|
|
8782
|
-
return null;
|
|
8783
|
-
}
|
|
8784
|
-
function trimTrailingSlash(value) {
|
|
8785
|
-
return value.replace(/\/+$/, "");
|
|
8786
|
-
}
|
|
8787
|
-
function joinPath(left, right) {
|
|
8788
|
-
return `${trimTrailingSlash(left)}/${right.replace(/^\/+/, "")}`;
|
|
8789
|
-
}
|
|
8790
|
-
function inferRepoRoot(workspaceRoot, repoName) {
|
|
8791
|
-
if (!workspaceRoot || !repoName)
|
|
8792
|
-
return null;
|
|
8793
|
-
const root = trimTrailingSlash(workspaceRoot);
|
|
8794
|
-
if (root.endsWith(`/${repoName}`) || root === repoName)
|
|
8795
|
-
return root;
|
|
8796
|
-
if (root.endsWith("/workspace") || root.endsWith("/Workspace")) {
|
|
8797
|
-
return joinPath(root, `hasna/opensource/${repoName}`);
|
|
8798
|
-
}
|
|
8799
|
-
return joinPath(root, repoName);
|
|
8800
|
-
}
|
|
8801
|
-
function shellQuote(value) {
|
|
8802
|
-
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
8803
|
-
}
|
|
8804
|
-
function shellCommand(command) {
|
|
8805
|
-
return command.map(shellQuote).join(" ");
|
|
8806
|
-
}
|
|
8807
|
-
function canCheckPathForMachine(machine, localMachineId) {
|
|
8808
|
-
if (!machine)
|
|
8809
|
-
return false;
|
|
8810
|
-
if (machine.machine_id === localMachineId)
|
|
8811
|
-
return true;
|
|
8812
|
-
return machine.route_hints.some((hint) => hint.kind === "local");
|
|
8813
|
-
}
|
|
8814
|
-
function checkedPathExists(path, check) {
|
|
8815
|
-
if (!path || !check)
|
|
8816
|
-
return null;
|
|
8817
|
-
return existsSync6(path);
|
|
8818
|
-
}
|
|
8819
|
-
function repairHint(input) {
|
|
8820
|
-
const command = [
|
|
8821
|
-
"machines",
|
|
8822
|
-
"workspace",
|
|
8823
|
-
"repair",
|
|
8824
|
-
"--machine",
|
|
8825
|
-
input.machineId,
|
|
8826
|
-
"--project",
|
|
8827
|
-
input.projectId
|
|
8828
|
-
];
|
|
8829
|
-
if (input.repoName)
|
|
8830
|
-
command.push("--repo", input.repoName);
|
|
8831
|
-
if (input.openFilesRepoName)
|
|
8832
|
-
command.push("--open-files-repo", input.openFilesRepoName);
|
|
8833
|
-
command.push("--json");
|
|
8834
|
-
const applyCommand = [...command.slice(0, -1), "--apply", "--json"];
|
|
8835
|
-
return {
|
|
8836
|
-
id: `repair:${input.machineId}:${input.projectId}`,
|
|
8837
|
-
reason: input.reason,
|
|
8838
|
-
command,
|
|
8839
|
-
shell_command: shellCommand(command),
|
|
8840
|
-
apply_command: applyCommand,
|
|
8841
|
-
apply_shell_command: shellCommand(applyCommand)
|
|
8842
|
-
};
|
|
8843
|
-
}
|
|
8844
|
-
function pathDiagnostic(input) {
|
|
8845
|
-
if (!input.path.path) {
|
|
8846
|
-
return {
|
|
8847
|
-
id: input.id,
|
|
8848
|
-
status: "missing",
|
|
8849
|
-
severity: input.required ? "fail" : "warn",
|
|
8850
|
-
message: `${input.label} is unresolved.`,
|
|
8851
|
-
path: null,
|
|
8852
|
-
source: input.path.source,
|
|
8853
|
-
path_exists: null
|
|
8854
|
-
};
|
|
8855
|
-
}
|
|
8856
|
-
if (input.pathExists === false) {
|
|
8857
|
-
return {
|
|
8858
|
-
id: input.id,
|
|
8859
|
-
status: "stale",
|
|
8860
|
-
severity: "fail",
|
|
8861
|
-
message: `${input.label} points to a path that does not exist on this machine.`,
|
|
8862
|
-
path: input.path.path,
|
|
8863
|
-
source: input.path.source,
|
|
8864
|
-
path_exists: false
|
|
8865
|
-
};
|
|
8866
|
-
}
|
|
8867
|
-
if (input.path.source === "inferred") {
|
|
8868
|
-
return {
|
|
8869
|
-
id: input.id,
|
|
8870
|
-
status: "inferred",
|
|
8871
|
-
severity: "warn",
|
|
8872
|
-
message: `${input.label} was inferred from the workspace root; write an explicit manifest mapping for repeatable downstream sync.`,
|
|
8873
|
-
path: input.path.path,
|
|
8874
|
-
source: input.path.source,
|
|
8875
|
-
path_exists: input.pathExists
|
|
8876
|
-
};
|
|
8877
|
-
}
|
|
8878
|
-
return {
|
|
8879
|
-
id: input.id,
|
|
8880
|
-
status: "ok",
|
|
8881
|
-
severity: "ok",
|
|
8882
|
-
message: `${input.label} is explicit enough for downstream sync.`,
|
|
8883
|
-
path: input.path.path,
|
|
8884
|
-
source: input.path.source,
|
|
8885
|
-
path_exists: input.pathExists
|
|
8886
|
-
};
|
|
8887
|
-
}
|
|
8888
|
-
function workspaceDiagnostics(input) {
|
|
8889
|
-
const checkPaths = canCheckPathForMachine(input.machine, input.localMachineId);
|
|
8890
|
-
const diagnostics = [];
|
|
8891
|
-
if (!input.machine) {
|
|
8892
|
-
diagnostics.push({
|
|
8893
|
-
id: "manifest",
|
|
8894
|
-
status: "missing_manifest",
|
|
8895
|
-
severity: "fail",
|
|
8896
|
-
message: "Machine is not present in topology or manifest.",
|
|
8897
|
-
path: null,
|
|
8898
|
-
source: "manifest",
|
|
8899
|
-
path_exists: null
|
|
8900
|
-
});
|
|
8901
|
-
} else if (!input.resolution.evidence.manifest_declared) {
|
|
8902
|
-
diagnostics.push({
|
|
8903
|
-
id: "manifest",
|
|
8904
|
-
status: "missing_manifest",
|
|
8905
|
-
severity: "warn",
|
|
8906
|
-
message: "Machine came from live topology but is not declared in the manifest.",
|
|
8907
|
-
path: null,
|
|
8908
|
-
source: "manifest",
|
|
8909
|
-
path_exists: null
|
|
8910
|
-
});
|
|
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
|
+
});
|
|
8911
8519
|
}
|
|
8912
8520
|
diagnostics.push(pathDiagnostic({
|
|
8913
8521
|
id: "workspace_root",
|
|
@@ -9144,80 +8752,478 @@ function resolveMachineWorkspace(options) {
|
|
|
9144
8752
|
}),
|
|
9145
8753
|
warnings
|
|
9146
8754
|
};
|
|
9147
|
-
const diagnostics = workspaceDiagnostics({
|
|
9148
|
-
machine,
|
|
9149
|
-
localMachineId: topology.local_machine_id,
|
|
9150
|
-
resolution,
|
|
9151
|
-
openFilesRepoName
|
|
9152
|
-
});
|
|
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
|
+
}
|
|
9153
9058
|
return {
|
|
9154
|
-
|
|
9155
|
-
|
|
9156
|
-
|
|
9059
|
+
machineId: plan.machineId,
|
|
9060
|
+
mode: "apply",
|
|
9061
|
+
steps: plan.steps,
|
|
9062
|
+
executed
|
|
9157
9063
|
};
|
|
9158
9064
|
}
|
|
9159
9065
|
|
|
9160
|
-
// src/commands/
|
|
9161
|
-
|
|
9162
|
-
|
|
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, `'\\''`)}'`;
|
|
9163
9071
|
}
|
|
9164
|
-
function
|
|
9165
|
-
|
|
9166
|
-
|
|
9167
|
-
|
|
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.");
|
|
9168
9078
|
}
|
|
9169
|
-
|
|
9170
|
-
|
|
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
|
+
});
|
|
9171
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
|
+
});
|
|
9172
9110
|
return {
|
|
9173
|
-
machineId:
|
|
9174
|
-
|
|
9175
|
-
|
|
9176
|
-
|
|
9177
|
-
warnings: resolved.warnings
|
|
9111
|
+
machineId: process.env["HASNA_MACHINES_MACHINE_ID"] || "local",
|
|
9112
|
+
mode: "plan",
|
|
9113
|
+
steps,
|
|
9114
|
+
executed: 0
|
|
9178
9115
|
};
|
|
9179
9116
|
}
|
|
9180
|
-
function
|
|
9181
|
-
const
|
|
9182
|
-
|
|
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
|
+
};
|
|
9183
9142
|
}
|
|
9184
9143
|
|
|
9185
|
-
// src/
|
|
9186
|
-
|
|
9187
|
-
|
|
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");
|
|
9188
9150
|
}
|
|
9189
|
-
function
|
|
9190
|
-
|
|
9151
|
+
function readMappings() {
|
|
9152
|
+
const path = getDnsPath();
|
|
9153
|
+
if (!existsSync6(path))
|
|
9154
|
+
return [];
|
|
9155
|
+
return JSON.parse(readFileSync3(path, "utf8"));
|
|
9191
9156
|
}
|
|
9192
|
-
function
|
|
9193
|
-
|
|
9194
|
-
|
|
9195
|
-
}
|
|
9196
|
-
|
|
9197
|
-
|
|
9198
|
-
|
|
9199
|
-
|
|
9200
|
-
|
|
9201
|
-
|
|
9202
|
-
|
|
9203
|
-
|
|
9204
|
-
|
|
9205
|
-
|
|
9206
|
-
|
|
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}`);
|
|
9207
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
|
+
};
|
|
9208
9187
|
}
|
|
9209
|
-
|
|
9210
|
-
|
|
9211
|
-
|
|
9212
|
-
|
|
9213
|
-
|
|
9214
|
-
|
|
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));
|
|
9215
9215
|
return {
|
|
9216
|
-
|
|
9217
|
-
|
|
9218
|
-
|
|
9219
|
-
|
|
9220
|
-
|
|
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
|
+
}
|
|
9221
9227
|
};
|
|
9222
9228
|
}
|
|
9223
9229
|
|
|
@@ -9313,27 +9319,28 @@ function buildAppsPlan(machineId) {
|
|
|
9313
9319
|
executed: 0
|
|
9314
9320
|
};
|
|
9315
9321
|
}
|
|
9316
|
-
function getAppsStatus(machineId) {
|
|
9322
|
+
function getAppsStatus(machineId, runner = runMachineCommand) {
|
|
9317
9323
|
const machine = resolveMachine(machineId);
|
|
9324
|
+
const readiness = requireMachineCommandSuccess("Apps status readiness check", runner(machine.id, "true"));
|
|
9318
9325
|
const apps = (machine.apps || []).map((app) => {
|
|
9319
|
-
const probe =
|
|
9326
|
+
const probe = requireMachineCommandSuccess(`App probe ${app.name}`, runner(machine.id, buildAppProbeCommand(machine, app)));
|
|
9320
9327
|
return parseProbeOutput(app, machine, probe.stdout);
|
|
9321
9328
|
});
|
|
9322
9329
|
return {
|
|
9323
9330
|
machineId: machine.id,
|
|
9324
|
-
source:
|
|
9331
|
+
source: readiness.source,
|
|
9325
9332
|
apps
|
|
9326
9333
|
};
|
|
9327
9334
|
}
|
|
9328
|
-
function diffApps(machineId) {
|
|
9329
|
-
const status = getAppsStatus(machineId);
|
|
9335
|
+
function diffApps(machineId, runner = runMachineCommand) {
|
|
9336
|
+
const status = getAppsStatus(machineId, runner);
|
|
9330
9337
|
return {
|
|
9331
9338
|
...status,
|
|
9332
9339
|
missing: status.apps.filter((app) => !app.installed).map((app) => app.name),
|
|
9333
9340
|
installed: status.apps.filter((app) => app.installed).map((app) => app.name)
|
|
9334
9341
|
};
|
|
9335
9342
|
}
|
|
9336
|
-
function runAppsInstall(machineId, options = {}) {
|
|
9343
|
+
function runAppsInstall(machineId, options = {}, runner = runMachineCommand) {
|
|
9337
9344
|
const plan = buildAppsPlan(machineId);
|
|
9338
9345
|
if (!options.apply)
|
|
9339
9346
|
return plan;
|
|
@@ -9342,14 +9349,7 @@ function runAppsInstall(machineId, options = {}) {
|
|
|
9342
9349
|
}
|
|
9343
9350
|
let executed = 0;
|
|
9344
9351
|
for (const step of plan.steps) {
|
|
9345
|
-
|
|
9346
|
-
stdout: "pipe",
|
|
9347
|
-
stderr: "pipe",
|
|
9348
|
-
env: process.env
|
|
9349
|
-
});
|
|
9350
|
-
if (result.exitCode !== 0) {
|
|
9351
|
-
throw new Error(`App install failed (${step.id}): ${result.stderr.toString().trim()}`);
|
|
9352
|
-
}
|
|
9352
|
+
requireMachineCommandSuccess(`App install ${step.id}`, runner(plan.machineId, step.command));
|
|
9353
9353
|
executed += 1;
|
|
9354
9354
|
}
|
|
9355
9355
|
return {
|
|
@@ -9420,25 +9420,28 @@ function buildClaudeInstallPlan(machineId, tools) {
|
|
|
9420
9420
|
executed: 0
|
|
9421
9421
|
};
|
|
9422
9422
|
}
|
|
9423
|
-
function getClaudeCliStatus(machineId, tools) {
|
|
9423
|
+
function getClaudeCliStatus(machineId, tools, runner = runMachineCommand) {
|
|
9424
9424
|
const machine = resolveMachine2(machineId);
|
|
9425
9425
|
const normalizedTools = normalizeTools(tools);
|
|
9426
|
-
const route =
|
|
9426
|
+
const route = requireMachineCommandSuccess("AI CLI status readiness check", runner(machine.id, "true")).source;
|
|
9427
9427
|
return {
|
|
9428
9428
|
machineId: machine.id,
|
|
9429
9429
|
source: route,
|
|
9430
|
-
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
|
+
})
|
|
9431
9434
|
};
|
|
9432
9435
|
}
|
|
9433
|
-
function diffClaudeCli(machineId, tools) {
|
|
9434
|
-
const status = getClaudeCliStatus(machineId, tools);
|
|
9436
|
+
function diffClaudeCli(machineId, tools, runner = runMachineCommand) {
|
|
9437
|
+
const status = getClaudeCliStatus(machineId, tools, runner);
|
|
9435
9438
|
return {
|
|
9436
9439
|
...status,
|
|
9437
9440
|
missing: status.tools.filter((tool) => !tool.installed).map((tool) => tool.tool),
|
|
9438
9441
|
installed: status.tools.filter((tool) => tool.installed).map((tool) => tool.tool)
|
|
9439
9442
|
};
|
|
9440
9443
|
}
|
|
9441
|
-
function runClaudeInstall(machineId, tools, options = {}) {
|
|
9444
|
+
function runClaudeInstall(machineId, tools, options = {}, runner = runMachineCommand) {
|
|
9442
9445
|
const plan = buildClaudeInstallPlan(machineId, tools);
|
|
9443
9446
|
if (!options.apply)
|
|
9444
9447
|
return plan;
|
|
@@ -9447,14 +9450,7 @@ function runClaudeInstall(machineId, tools, options = {}) {
|
|
|
9447
9450
|
}
|
|
9448
9451
|
let executed = 0;
|
|
9449
9452
|
for (const step of plan.steps) {
|
|
9450
|
-
|
|
9451
|
-
stdout: "pipe",
|
|
9452
|
-
stderr: "pipe",
|
|
9453
|
-
env: process.env
|
|
9454
|
-
});
|
|
9455
|
-
if (result.exitCode !== 0) {
|
|
9456
|
-
throw new Error(`AI CLI install failed (${step.id}): ${result.stderr.toString().trim()}`);
|
|
9457
|
-
}
|
|
9453
|
+
requireMachineCommandSuccess(`AI CLI install ${step.id}`, runner(plan.machineId, step.command));
|
|
9458
9454
|
executed += 1;
|
|
9459
9455
|
}
|
|
9460
9456
|
return {
|
|
@@ -9506,7 +9502,7 @@ function buildTailscaleInstallPlan(machineId) {
|
|
|
9506
9502
|
executed: 0
|
|
9507
9503
|
};
|
|
9508
9504
|
}
|
|
9509
|
-
function runTailscaleInstall(machineId, options = {}) {
|
|
9505
|
+
function runTailscaleInstall(machineId, options = {}, runner = runMachineCommand) {
|
|
9510
9506
|
const plan = buildTailscaleInstallPlan(machineId);
|
|
9511
9507
|
if (!options.apply)
|
|
9512
9508
|
return plan;
|
|
@@ -9515,14 +9511,7 @@ function runTailscaleInstall(machineId, options = {}) {
|
|
|
9515
9511
|
}
|
|
9516
9512
|
let executed = 0;
|
|
9517
9513
|
for (const step of plan.steps) {
|
|
9518
|
-
|
|
9519
|
-
stdout: "pipe",
|
|
9520
|
-
stderr: "pipe",
|
|
9521
|
-
env: process.env
|
|
9522
|
-
});
|
|
9523
|
-
if (result.exitCode !== 0) {
|
|
9524
|
-
throw new Error(`Tailscale install failed (${step.id}): ${result.stderr.toString().trim()}`);
|
|
9525
|
-
}
|
|
9514
|
+
requireMachineCommandSuccess(`Tailscale install ${step.id}`, runner(plan.machineId, step.command));
|
|
9526
9515
|
executed += 1;
|
|
9527
9516
|
}
|
|
9528
9517
|
return {
|
|
@@ -10563,16 +10552,17 @@ function quote4(value) {
|
|
|
10563
10552
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
10564
10553
|
}
|
|
10565
10554
|
function packageCheckCommand(machine, packageName, manager = machine.platform === "macos" ? "brew" : "apt") {
|
|
10555
|
+
const quotedPackageName = quote4(packageName);
|
|
10566
10556
|
if (manager === "bun") {
|
|
10567
|
-
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`;
|
|
10568
10558
|
}
|
|
10569
10559
|
if (manager === "brew") {
|
|
10570
|
-
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`;
|
|
10571
10561
|
}
|
|
10572
10562
|
if (manager === "apt") {
|
|
10573
|
-
return `dpkg -s ${
|
|
10563
|
+
return `if dpkg -s ${quotedPackageName} >/dev/null 2>&1; then printf 'installed=1\\n'; else printf 'installed=0\\n'; fi`;
|
|
10574
10564
|
}
|
|
10575
|
-
return `command -v ${
|
|
10565
|
+
return `if command -v ${quotedPackageName} >/dev/null 2>&1; then printf 'installed=1\\n'; else printf 'installed=0\\n'; fi`;
|
|
10576
10566
|
}
|
|
10577
10567
|
function packageInstallCommand(machine, packageName, manager = machine.platform === "macos" ? "brew" : "apt") {
|
|
10578
10568
|
if (manager === "bun") {
|
|
@@ -10586,15 +10576,15 @@ function packageInstallCommand(machine, packageName, manager = machine.platform
|
|
|
10586
10576
|
}
|
|
10587
10577
|
return packageName;
|
|
10588
10578
|
}
|
|
10589
|
-
function detectPackageActions(machine) {
|
|
10579
|
+
function detectPackageActions(machine, runner) {
|
|
10590
10580
|
return (machine.packages || []).map((pkg, index) => {
|
|
10591
10581
|
const manager = pkg.manager || (machine.platform === "macos" ? "brew" : "apt");
|
|
10592
|
-
const check =
|
|
10593
|
-
|
|
10594
|
-
|
|
10595
|
-
|
|
10596
|
-
|
|
10597
|
-
|
|
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");
|
|
10598
10588
|
return {
|
|
10599
10589
|
id: `package-${index + 1}`,
|
|
10600
10590
|
title: `${installed ? "Package present" : "Install package"} ${pkg.name}`,
|
|
@@ -10605,6 +10595,9 @@ function detectPackageActions(machine) {
|
|
|
10605
10595
|
});
|
|
10606
10596
|
}
|
|
10607
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
|
+
}
|
|
10608
10601
|
return (machine.files || []).map((file, index) => {
|
|
10609
10602
|
const sourceExists = existsSync9(file.source);
|
|
10610
10603
|
const targetExists = existsSync9(file.target);
|
|
@@ -10628,7 +10621,7 @@ function detectFileActions(machine) {
|
|
|
10628
10621
|
};
|
|
10629
10622
|
});
|
|
10630
10623
|
}
|
|
10631
|
-
function buildSyncPlan(machineId) {
|
|
10624
|
+
function buildSyncPlan(machineId, runner = runMachineCommand) {
|
|
10632
10625
|
const manifest = readManifest();
|
|
10633
10626
|
const currentMachineId = getLocalMachineId();
|
|
10634
10627
|
const selected = machineId ? manifest.machines.find((machine) => machine.id === machineId) : manifest.machines.find((machine) => machine.id === currentMachineId);
|
|
@@ -10638,7 +10631,7 @@ function buildSyncPlan(machineId) {
|
|
|
10638
10631
|
workspacePath: `${homedir7()}/workspace`
|
|
10639
10632
|
};
|
|
10640
10633
|
const actions = [
|
|
10641
|
-
...detectPackageActions(target),
|
|
10634
|
+
...detectPackageActions(target, runner),
|
|
10642
10635
|
...detectFileActions(target)
|
|
10643
10636
|
];
|
|
10644
10637
|
return {
|
|
@@ -10668,8 +10661,8 @@ function applyFileAction(command) {
|
|
|
10668
10661
|
symlinkSync(sourcePath, targetPath);
|
|
10669
10662
|
}
|
|
10670
10663
|
}
|
|
10671
|
-
function runSync(machineId, options = {}) {
|
|
10672
|
-
const plan = buildSyncPlan(machineId);
|
|
10664
|
+
function runSync(machineId, options = {}, runner = runMachineCommand) {
|
|
10665
|
+
const plan = buildSyncPlan(machineId, runner);
|
|
10673
10666
|
if (!options.apply) {
|
|
10674
10667
|
return plan;
|
|
10675
10668
|
}
|
|
@@ -10683,18 +10676,17 @@ function runSync(machineId, options = {}) {
|
|
|
10683
10676
|
if (action.kind === "file") {
|
|
10684
10677
|
applyFileAction(action.command);
|
|
10685
10678
|
} else {
|
|
10686
|
-
const result =
|
|
10687
|
-
stdout: "pipe",
|
|
10688
|
-
stderr: "pipe",
|
|
10689
|
-
env: process.env
|
|
10690
|
-
});
|
|
10679
|
+
const result = runner(plan.machineId, action.command);
|
|
10691
10680
|
if (result.exitCode !== 0) {
|
|
10692
10681
|
recordSyncRun(plan.machineId, "failed", {
|
|
10693
10682
|
executed,
|
|
10694
10683
|
failedAction: action,
|
|
10695
|
-
stderr: result.stderr
|
|
10684
|
+
stderr: result.stderr,
|
|
10685
|
+
stdout: result.stdout,
|
|
10686
|
+
exitCode: result.exitCode,
|
|
10687
|
+
source: result.source
|
|
10696
10688
|
});
|
|
10697
|
-
throw new Error(`Sync action
|
|
10689
|
+
throw new Error(describeMachineCommandFailure(`Sync action ${action.id}`, result));
|
|
10698
10690
|
}
|
|
10699
10691
|
}
|
|
10700
10692
|
executed += 1;
|