@hasna/machines 0.0.30 → 0.0.32
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 +1069 -1055
- 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 +88 -70
- package/dist/mcp/index.js +88 -70
- 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",
|
|
@@ -9151,73 +8759,474 @@ function resolveMachineWorkspace(options) {
|
|
|
9151
8759
|
openFilesRepoName
|
|
9152
8760
|
});
|
|
9153
8761
|
return {
|
|
9154
|
-
...resolution,
|
|
9155
|
-
diagnostics: diagnostics.diagnostics,
|
|
9156
|
-
repair_hints: diagnostics.repairHints
|
|
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
|
+
if (machineId && !selected) {
|
|
8911
|
+
throw new Error(`Machine not found in manifest: ${machineId}`);
|
|
8912
|
+
}
|
|
8913
|
+
const target = selected || {
|
|
8914
|
+
id: currentMachineId,
|
|
8915
|
+
platform: "linux",
|
|
8916
|
+
workspacePath: `${homedir3()}/workspace`
|
|
8917
|
+
};
|
|
8918
|
+
const steps = [...buildBaseSteps(target), ...buildPackageSteps(target)];
|
|
8919
|
+
return {
|
|
8920
|
+
machineId: target.id,
|
|
8921
|
+
mode: "plan",
|
|
8922
|
+
steps,
|
|
8923
|
+
executed: 0
|
|
8924
|
+
};
|
|
8925
|
+
}
|
|
8926
|
+
function runSetup(machineId, options = {}, runner = runMachineCommand) {
|
|
8927
|
+
const plan = buildSetupPlan(machineId);
|
|
8928
|
+
if (!options.apply) {
|
|
8929
|
+
return plan;
|
|
8930
|
+
}
|
|
8931
|
+
if (!options.yes) {
|
|
8932
|
+
throw new Error("Setup execution requires --yes.");
|
|
8933
|
+
}
|
|
8934
|
+
let executed = 0;
|
|
8935
|
+
for (const step of plan.steps) {
|
|
8936
|
+
const result = runner(plan.machineId, step.command);
|
|
8937
|
+
if (result.exitCode !== 0) {
|
|
8938
|
+
recordSetupRun(plan.machineId, "failed", {
|
|
8939
|
+
executed,
|
|
8940
|
+
failedStep: step,
|
|
8941
|
+
stderr: result.stderr,
|
|
8942
|
+
stdout: result.stdout,
|
|
8943
|
+
exitCode: result.exitCode,
|
|
8944
|
+
source: result.source
|
|
8945
|
+
});
|
|
8946
|
+
throw new Error(describeMachineCommandFailure(`Setup step ${step.id}`, result));
|
|
8947
|
+
}
|
|
8948
|
+
executed += 1;
|
|
8949
|
+
}
|
|
8950
|
+
const summary = {
|
|
8951
|
+
machineId: plan.machineId,
|
|
8952
|
+
mode: "apply",
|
|
8953
|
+
steps: plan.steps,
|
|
8954
|
+
executed
|
|
8955
|
+
};
|
|
8956
|
+
recordSetupRun(plan.machineId, "completed", summary);
|
|
8957
|
+
return summary;
|
|
8958
|
+
}
|
|
8959
|
+
|
|
8960
|
+
// src/commands/backup.ts
|
|
8961
|
+
import { homedir as homedir4, hostname as hostname5 } from "os";
|
|
8962
|
+
import { join as join4 } from "path";
|
|
8963
|
+
var MACHINES_BACKUP_BUCKET_ENV = "HASNA_MACHINES_S3_BUCKET";
|
|
8964
|
+
var MACHINES_BACKUP_BUCKET_FALLBACK_ENV = "MACHINES_S3_BUCKET";
|
|
8965
|
+
var MACHINES_BACKUP_PREFIX_ENV = "HASNA_MACHINES_S3_PREFIX";
|
|
8966
|
+
var MACHINES_BACKUP_PREFIX_FALLBACK_ENV = "MACHINES_S3_PREFIX";
|
|
8967
|
+
var DEFAULT_BACKUP_PREFIX = "machines";
|
|
8968
|
+
function quote2(value) {
|
|
8969
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
8970
|
+
}
|
|
8971
|
+
function readEnv(name) {
|
|
8972
|
+
const value = process.env[name]?.trim();
|
|
8973
|
+
return value || undefined;
|
|
8974
|
+
}
|
|
8975
|
+
function readBackupBucketEnv() {
|
|
8976
|
+
const primary = readEnv(MACHINES_BACKUP_BUCKET_ENV);
|
|
8977
|
+
if (primary)
|
|
8978
|
+
return { bucket: primary, bucketSource: MACHINES_BACKUP_BUCKET_ENV };
|
|
8979
|
+
const fallback = readEnv(MACHINES_BACKUP_BUCKET_FALLBACK_ENV);
|
|
8980
|
+
if (fallback)
|
|
8981
|
+
return { bucket: fallback, bucketSource: MACHINES_BACKUP_BUCKET_FALLBACK_ENV };
|
|
8982
|
+
return null;
|
|
8983
|
+
}
|
|
8984
|
+
function readBackupPrefixEnv() {
|
|
8985
|
+
const primary = readEnv(MACHINES_BACKUP_PREFIX_ENV);
|
|
8986
|
+
if (primary)
|
|
8987
|
+
return { prefix: primary, prefixSource: MACHINES_BACKUP_PREFIX_ENV };
|
|
8988
|
+
const fallback = readEnv(MACHINES_BACKUP_PREFIX_FALLBACK_ENV);
|
|
8989
|
+
if (fallback)
|
|
8990
|
+
return { prefix: fallback, prefixSource: MACHINES_BACKUP_PREFIX_FALLBACK_ENV };
|
|
8991
|
+
return null;
|
|
8992
|
+
}
|
|
8993
|
+
function resolveBackupTarget(options = {}) {
|
|
8994
|
+
const explicitBucket = options.bucket?.trim();
|
|
8995
|
+
const envBucket = explicitBucket ? null : readBackupBucketEnv();
|
|
8996
|
+
const bucket = explicitBucket || envBucket?.bucket;
|
|
8997
|
+
if (!bucket) {
|
|
8998
|
+
throw new Error(`Missing S3 backup bucket. Pass --bucket or set ${MACHINES_BACKUP_BUCKET_ENV} or ${MACHINES_BACKUP_BUCKET_FALLBACK_ENV}.`);
|
|
8999
|
+
}
|
|
9000
|
+
const explicitPrefix = options.prefix?.trim();
|
|
9001
|
+
const envPrefix = explicitPrefix ? null : readBackupPrefixEnv();
|
|
9002
|
+
return {
|
|
9003
|
+
bucket,
|
|
9004
|
+
prefix: explicitPrefix || envPrefix?.prefix || DEFAULT_BACKUP_PREFIX,
|
|
9005
|
+
bucketSource: explicitBucket ? "argument" : envBucket.bucketSource,
|
|
9006
|
+
prefixSource: explicitPrefix ? "argument" : envPrefix?.prefixSource || "default"
|
|
9007
|
+
};
|
|
9008
|
+
}
|
|
9009
|
+
function defaultBackupSources() {
|
|
9010
|
+
const home = homedir4();
|
|
9011
|
+
return [
|
|
9012
|
+
join4(home, ".hasna"),
|
|
9013
|
+
join4(home, ".ssh"),
|
|
9014
|
+
join4(home, ".secrets")
|
|
9015
|
+
];
|
|
9016
|
+
}
|
|
9017
|
+
function buildBackupPlan(bucket, prefix) {
|
|
9018
|
+
const target = resolveBackupTarget({ bucket, prefix });
|
|
9019
|
+
const archivePath = join4(homedir4(), ".hasna", "machines", "backup.tgz");
|
|
9020
|
+
const sources = defaultBackupSources();
|
|
9021
|
+
const steps = [
|
|
9022
|
+
{
|
|
9023
|
+
id: "backup-archive",
|
|
9024
|
+
title: "Create compressed machine backup archive",
|
|
9025
|
+
command: `tar -czf ${quote2(archivePath)} ${sources.map((source) => quote2(source)).join(" ")}`,
|
|
9026
|
+
manager: "shell"
|
|
9027
|
+
},
|
|
9028
|
+
{
|
|
9029
|
+
id: "backup-upload",
|
|
9030
|
+
title: "Upload archive to S3",
|
|
9031
|
+
command: `aws s3 cp ${quote2(archivePath)} ${quote2(`s3://${target.bucket}/${target.prefix}/${hostname5()}-backup.tgz`)}`,
|
|
9032
|
+
manager: "custom"
|
|
9033
|
+
}
|
|
9034
|
+
];
|
|
9035
|
+
return {
|
|
9036
|
+
machineId: process.env["HASNA_MACHINES_MACHINE_ID"] || "local",
|
|
9037
|
+
mode: "plan",
|
|
9038
|
+
steps,
|
|
9039
|
+
executed: 0
|
|
9040
|
+
};
|
|
9041
|
+
}
|
|
9042
|
+
function runBackup(bucket, prefix, options = {}) {
|
|
9043
|
+
const plan = buildBackupPlan(bucket, prefix);
|
|
9044
|
+
if (!options.apply)
|
|
9045
|
+
return plan;
|
|
9046
|
+
if (!options.yes) {
|
|
9047
|
+
throw new Error("Backup execution requires --yes.");
|
|
9048
|
+
}
|
|
9049
|
+
let executed = 0;
|
|
9050
|
+
for (const step of plan.steps) {
|
|
9051
|
+
const result = Bun.spawnSync(["bash", "-lc", step.command], {
|
|
9052
|
+
stdout: "pipe",
|
|
9053
|
+
stderr: "pipe",
|
|
9054
|
+
env: process.env
|
|
9055
|
+
});
|
|
9056
|
+
if (result.exitCode !== 0) {
|
|
9057
|
+
throw new Error(`Backup step failed (${step.id}): ${result.stderr.toString().trim()}`);
|
|
9058
|
+
}
|
|
9059
|
+
executed += 1;
|
|
9060
|
+
}
|
|
9061
|
+
return {
|
|
9062
|
+
machineId: plan.machineId,
|
|
9063
|
+
mode: "apply",
|
|
9064
|
+
steps: plan.steps,
|
|
9065
|
+
executed
|
|
9157
9066
|
};
|
|
9158
9067
|
}
|
|
9159
9068
|
|
|
9160
|
-
// src/commands/
|
|
9161
|
-
|
|
9162
|
-
|
|
9069
|
+
// src/commands/cert.ts
|
|
9070
|
+
import { homedir as homedir5, platform as platform3 } from "os";
|
|
9071
|
+
import { join as join5 } from "path";
|
|
9072
|
+
function quote3(value) {
|
|
9073
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
9163
9074
|
}
|
|
9164
|
-
function
|
|
9165
|
-
|
|
9166
|
-
|
|
9167
|
-
|
|
9075
|
+
function certDir() {
|
|
9076
|
+
return join5(homedir5(), ".hasna", "machines", "certs");
|
|
9077
|
+
}
|
|
9078
|
+
function buildCertPlan(domains) {
|
|
9079
|
+
if (domains.length === 0) {
|
|
9080
|
+
throw new Error("At least one domain is required.");
|
|
9168
9081
|
}
|
|
9169
|
-
|
|
9170
|
-
|
|
9082
|
+
const primary = domains[0];
|
|
9083
|
+
const certPath = join5(certDir(), `${primary}.pem`);
|
|
9084
|
+
const keyPath = join5(certDir(), `${primary}-key.pem`);
|
|
9085
|
+
const steps = [];
|
|
9086
|
+
if (platform3() === "darwin") {
|
|
9087
|
+
steps.push({
|
|
9088
|
+
id: "mkcert-install-macos",
|
|
9089
|
+
title: "Install mkcert on macOS",
|
|
9090
|
+
command: "brew install mkcert nss",
|
|
9091
|
+
manager: "brew"
|
|
9092
|
+
});
|
|
9093
|
+
} else {
|
|
9094
|
+
steps.push({
|
|
9095
|
+
id: "mkcert-install-linux",
|
|
9096
|
+
title: "Install mkcert on Linux",
|
|
9097
|
+
command: "sudo apt-get update && sudo apt-get install -y mkcert libnss3-tools",
|
|
9098
|
+
manager: "apt",
|
|
9099
|
+
privileged: true
|
|
9100
|
+
});
|
|
9171
9101
|
}
|
|
9102
|
+
steps.push({
|
|
9103
|
+
id: "mkcert-local-ca",
|
|
9104
|
+
title: "Install local mkcert CA",
|
|
9105
|
+
command: "mkcert -install",
|
|
9106
|
+
manager: "custom"
|
|
9107
|
+
}, {
|
|
9108
|
+
id: "mkcert-issue",
|
|
9109
|
+
title: `Issue certificate for ${domains.join(", ")}`,
|
|
9110
|
+
command: `mkdir -p ${quote3(certDir())} && mkcert -cert-file ${quote3(certPath)} -key-file ${quote3(keyPath)} ${domains.map((domain) => quote3(domain)).join(" ")}`,
|
|
9111
|
+
manager: "custom"
|
|
9112
|
+
});
|
|
9172
9113
|
return {
|
|
9173
|
-
machineId:
|
|
9174
|
-
|
|
9175
|
-
|
|
9176
|
-
|
|
9177
|
-
warnings: resolved.warnings
|
|
9114
|
+
machineId: process.env["HASNA_MACHINES_MACHINE_ID"] || "local",
|
|
9115
|
+
mode: "plan",
|
|
9116
|
+
steps,
|
|
9117
|
+
executed: 0
|
|
9178
9118
|
};
|
|
9179
9119
|
}
|
|
9180
|
-
function
|
|
9181
|
-
const
|
|
9182
|
-
|
|
9120
|
+
function runCertPlan(domains, options = {}) {
|
|
9121
|
+
const plan = buildCertPlan(domains);
|
|
9122
|
+
if (!options.apply)
|
|
9123
|
+
return plan;
|
|
9124
|
+
if (!options.yes) {
|
|
9125
|
+
throw new Error("Certificate generation requires --yes.");
|
|
9126
|
+
}
|
|
9127
|
+
let executed = 0;
|
|
9128
|
+
for (const step of plan.steps) {
|
|
9129
|
+
const result = Bun.spawnSync(["bash", "-lc", step.command], {
|
|
9130
|
+
stdout: "pipe",
|
|
9131
|
+
stderr: "pipe",
|
|
9132
|
+
env: process.env
|
|
9133
|
+
});
|
|
9134
|
+
if (result.exitCode !== 0) {
|
|
9135
|
+
throw new Error(`Certificate step failed (${step.id}): ${result.stderr.toString().trim()}`);
|
|
9136
|
+
}
|
|
9137
|
+
executed += 1;
|
|
9138
|
+
}
|
|
9139
|
+
return {
|
|
9140
|
+
machineId: plan.machineId,
|
|
9141
|
+
mode: "apply",
|
|
9142
|
+
steps: plan.steps,
|
|
9143
|
+
executed
|
|
9144
|
+
};
|
|
9183
9145
|
}
|
|
9184
9146
|
|
|
9185
|
-
// src/
|
|
9186
|
-
|
|
9187
|
-
|
|
9147
|
+
// src/commands/dns.ts
|
|
9148
|
+
init_paths();
|
|
9149
|
+
import { existsSync as existsSync6, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
9150
|
+
import { join as join6 } from "path";
|
|
9151
|
+
function getDnsPath() {
|
|
9152
|
+
return join6(getDataDir(), "dns.json");
|
|
9188
9153
|
}
|
|
9189
|
-
function
|
|
9190
|
-
|
|
9154
|
+
function readMappings() {
|
|
9155
|
+
const path = getDnsPath();
|
|
9156
|
+
if (!existsSync6(path))
|
|
9157
|
+
return [];
|
|
9158
|
+
return JSON.parse(readFileSync3(path, "utf8"));
|
|
9191
9159
|
}
|
|
9192
|
-
function
|
|
9193
|
-
|
|
9194
|
-
|
|
9195
|
-
}
|
|
9196
|
-
|
|
9197
|
-
|
|
9198
|
-
|
|
9199
|
-
|
|
9200
|
-
|
|
9201
|
-
|
|
9202
|
-
|
|
9203
|
-
|
|
9204
|
-
|
|
9205
|
-
|
|
9206
|
-
|
|
9160
|
+
function writeMappings(mappings) {
|
|
9161
|
+
const path = getDnsPath();
|
|
9162
|
+
ensureParentDir(path);
|
|
9163
|
+
writeFileSync2(path, `${JSON.stringify(mappings, null, 2)}
|
|
9164
|
+
`, "utf8");
|
|
9165
|
+
return path;
|
|
9166
|
+
}
|
|
9167
|
+
function addDomainMapping(domain, port, targetHost = "127.0.0.1") {
|
|
9168
|
+
const mappings = readMappings().filter((entry) => entry.domain !== domain);
|
|
9169
|
+
mappings.push({ domain, port, targetHost });
|
|
9170
|
+
writeMappings(mappings);
|
|
9171
|
+
return mappings.sort((left, right) => left.domain.localeCompare(right.domain));
|
|
9172
|
+
}
|
|
9173
|
+
function listDomainMappings() {
|
|
9174
|
+
return readMappings().sort((left, right) => left.domain.localeCompare(right.domain));
|
|
9175
|
+
}
|
|
9176
|
+
function renderDomainMapping(domain) {
|
|
9177
|
+
const entry = readMappings().find((mapping) => mapping.domain === domain);
|
|
9178
|
+
if (!entry) {
|
|
9179
|
+
throw new Error(`Domain mapping not found: ${domain}`);
|
|
9207
9180
|
}
|
|
9181
|
+
return {
|
|
9182
|
+
hostsEntry: `${entry.targetHost} ${entry.domain}`,
|
|
9183
|
+
caddySnippet: `${entry.domain} {
|
|
9184
|
+
reverse_proxy 127.0.0.1:${entry.port}
|
|
9185
|
+
tls ${join6(getDataDir(), "certs", `${entry.domain}.pem`)} ${join6(getDataDir(), "certs", `${entry.domain}-key.pem`)}
|
|
9186
|
+
}`,
|
|
9187
|
+
certPath: join6(getDataDir(), "certs", `${entry.domain}.pem`),
|
|
9188
|
+
keyPath: join6(getDataDir(), "certs", `${entry.domain}-key.pem`)
|
|
9189
|
+
};
|
|
9208
9190
|
}
|
|
9209
|
-
|
|
9210
|
-
|
|
9211
|
-
|
|
9212
|
-
|
|
9213
|
-
|
|
9214
|
-
|
|
9191
|
+
|
|
9192
|
+
// src/commands/diff.ts
|
|
9193
|
+
function packageNames(machine) {
|
|
9194
|
+
return (machine.packages || []).map((pkg) => pkg.name).sort();
|
|
9195
|
+
}
|
|
9196
|
+
function fileTargets(machine) {
|
|
9197
|
+
return (machine.files || []).map((file) => `${file.source}->${file.target}`).sort();
|
|
9198
|
+
}
|
|
9199
|
+
function diffMachines(leftMachineId, rightMachineId) {
|
|
9200
|
+
const left = getManifestMachine(leftMachineId);
|
|
9201
|
+
if (!left) {
|
|
9202
|
+
throw new Error(`Machine not found in manifest: ${leftMachineId}`);
|
|
9203
|
+
}
|
|
9204
|
+
const right = rightMachineId ? getManifestMachine(rightMachineId) : detectCurrentMachineManifest();
|
|
9205
|
+
if (!right) {
|
|
9206
|
+
throw new Error(`Machine not found in manifest: ${rightMachineId}`);
|
|
9207
|
+
}
|
|
9208
|
+
const changedFields = [
|
|
9209
|
+
left.platform !== right.platform ? "platform" : null,
|
|
9210
|
+
left.connection !== right.connection ? "connection" : null,
|
|
9211
|
+
left.workspacePath !== right.workspacePath ? "workspacePath" : null,
|
|
9212
|
+
left.bunPath !== right.bunPath ? "bunPath" : null
|
|
9213
|
+
].filter(Boolean);
|
|
9214
|
+
const leftPackages = new Set(packageNames(left));
|
|
9215
|
+
const rightPackages = new Set(packageNames(right));
|
|
9216
|
+
const leftFiles = new Set(fileTargets(left));
|
|
9217
|
+
const rightFiles = new Set(fileTargets(right));
|
|
9215
9218
|
return {
|
|
9216
|
-
|
|
9217
|
-
|
|
9218
|
-
|
|
9219
|
-
|
|
9220
|
-
|
|
9219
|
+
leftMachineId: left.id,
|
|
9220
|
+
rightMachineId: right.id,
|
|
9221
|
+
changedFields,
|
|
9222
|
+
missingPackages: {
|
|
9223
|
+
leftOnly: [...leftPackages].filter((pkg) => !rightPackages.has(pkg)),
|
|
9224
|
+
rightOnly: [...rightPackages].filter((pkg) => !leftPackages.has(pkg))
|
|
9225
|
+
},
|
|
9226
|
+
missingFiles: {
|
|
9227
|
+
leftOnly: [...leftFiles].filter((file) => !rightFiles.has(file)),
|
|
9228
|
+
rightOnly: [...rightFiles].filter((file) => !leftFiles.has(file))
|
|
9229
|
+
}
|
|
9221
9230
|
};
|
|
9222
9231
|
}
|
|
9223
9232
|
|
|
@@ -9282,7 +9291,14 @@ function buildAppSteps(machine) {
|
|
|
9282
9291
|
}));
|
|
9283
9292
|
}
|
|
9284
9293
|
function resolveMachine(machineId) {
|
|
9285
|
-
|
|
9294
|
+
if (!machineId)
|
|
9295
|
+
return detectCurrentMachineManifest();
|
|
9296
|
+
return getManifestMachine(machineId) || {
|
|
9297
|
+
id: machineId,
|
|
9298
|
+
platform: "linux",
|
|
9299
|
+
workspacePath: "",
|
|
9300
|
+
apps: []
|
|
9301
|
+
};
|
|
9286
9302
|
}
|
|
9287
9303
|
function parseProbeOutput(app, machine, stdout) {
|
|
9288
9304
|
const lines = stdout.trim().split(`
|
|
@@ -9313,27 +9329,28 @@ function buildAppsPlan(machineId) {
|
|
|
9313
9329
|
executed: 0
|
|
9314
9330
|
};
|
|
9315
9331
|
}
|
|
9316
|
-
function getAppsStatus(machineId) {
|
|
9332
|
+
function getAppsStatus(machineId, runner = runMachineCommand) {
|
|
9317
9333
|
const machine = resolveMachine(machineId);
|
|
9334
|
+
const readiness = requireMachineCommandSuccess("Apps status readiness check", runner(machine.id, "true"));
|
|
9318
9335
|
const apps = (machine.apps || []).map((app) => {
|
|
9319
|
-
const probe =
|
|
9336
|
+
const probe = requireMachineCommandSuccess(`App probe ${app.name}`, runner(machine.id, buildAppProbeCommand(machine, app)));
|
|
9320
9337
|
return parseProbeOutput(app, machine, probe.stdout);
|
|
9321
9338
|
});
|
|
9322
9339
|
return {
|
|
9323
9340
|
machineId: machine.id,
|
|
9324
|
-
source:
|
|
9341
|
+
source: readiness.source,
|
|
9325
9342
|
apps
|
|
9326
9343
|
};
|
|
9327
9344
|
}
|
|
9328
|
-
function diffApps(machineId) {
|
|
9329
|
-
const status = getAppsStatus(machineId);
|
|
9345
|
+
function diffApps(machineId, runner = runMachineCommand) {
|
|
9346
|
+
const status = getAppsStatus(machineId, runner);
|
|
9330
9347
|
return {
|
|
9331
9348
|
...status,
|
|
9332
9349
|
missing: status.apps.filter((app) => !app.installed).map((app) => app.name),
|
|
9333
9350
|
installed: status.apps.filter((app) => app.installed).map((app) => app.name)
|
|
9334
9351
|
};
|
|
9335
9352
|
}
|
|
9336
|
-
function runAppsInstall(machineId, options = {}) {
|
|
9353
|
+
function runAppsInstall(machineId, options = {}, runner = runMachineCommand) {
|
|
9337
9354
|
const plan = buildAppsPlan(machineId);
|
|
9338
9355
|
if (!options.apply)
|
|
9339
9356
|
return plan;
|
|
@@ -9342,14 +9359,7 @@ function runAppsInstall(machineId, options = {}) {
|
|
|
9342
9359
|
}
|
|
9343
9360
|
let executed = 0;
|
|
9344
9361
|
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
|
-
}
|
|
9362
|
+
requireMachineCommandSuccess(`App install ${step.id}`, runner(plan.machineId, step.command));
|
|
9353
9363
|
executed += 1;
|
|
9354
9364
|
}
|
|
9355
9365
|
return {
|
|
@@ -9393,7 +9403,13 @@ function buildInstallSteps(machine, tools) {
|
|
|
9393
9403
|
}));
|
|
9394
9404
|
}
|
|
9395
9405
|
function resolveMachine2(machineId) {
|
|
9396
|
-
|
|
9406
|
+
if (!machineId)
|
|
9407
|
+
return detectCurrentMachineManifest();
|
|
9408
|
+
return getManifestMachine(machineId) || {
|
|
9409
|
+
id: machineId,
|
|
9410
|
+
platform: "linux",
|
|
9411
|
+
workspacePath: ""
|
|
9412
|
+
};
|
|
9397
9413
|
}
|
|
9398
9414
|
function buildProbeCommand(tool) {
|
|
9399
9415
|
const binary = getToolBinary(tool);
|
|
@@ -9420,25 +9436,28 @@ function buildClaudeInstallPlan(machineId, tools) {
|
|
|
9420
9436
|
executed: 0
|
|
9421
9437
|
};
|
|
9422
9438
|
}
|
|
9423
|
-
function getClaudeCliStatus(machineId, tools) {
|
|
9439
|
+
function getClaudeCliStatus(machineId, tools, runner = runMachineCommand) {
|
|
9424
9440
|
const machine = resolveMachine2(machineId);
|
|
9425
9441
|
const normalizedTools = normalizeTools(tools);
|
|
9426
|
-
const route =
|
|
9442
|
+
const route = requireMachineCommandSuccess("AI CLI status readiness check", runner(machine.id, "true")).source;
|
|
9427
9443
|
return {
|
|
9428
9444
|
machineId: machine.id,
|
|
9429
9445
|
source: route,
|
|
9430
|
-
tools: normalizedTools.map((tool) =>
|
|
9446
|
+
tools: normalizedTools.map((tool) => {
|
|
9447
|
+
const result = requireMachineCommandSuccess(`AI CLI probe ${tool}`, runner(machine.id, buildProbeCommand(tool)));
|
|
9448
|
+
return parseProbe(tool, result.stdout);
|
|
9449
|
+
})
|
|
9431
9450
|
};
|
|
9432
9451
|
}
|
|
9433
|
-
function diffClaudeCli(machineId, tools) {
|
|
9434
|
-
const status = getClaudeCliStatus(machineId, tools);
|
|
9452
|
+
function diffClaudeCli(machineId, tools, runner = runMachineCommand) {
|
|
9453
|
+
const status = getClaudeCliStatus(machineId, tools, runner);
|
|
9435
9454
|
return {
|
|
9436
9455
|
...status,
|
|
9437
9456
|
missing: status.tools.filter((tool) => !tool.installed).map((tool) => tool.tool),
|
|
9438
9457
|
installed: status.tools.filter((tool) => tool.installed).map((tool) => tool.tool)
|
|
9439
9458
|
};
|
|
9440
9459
|
}
|
|
9441
|
-
function runClaudeInstall(machineId, tools, options = {}) {
|
|
9460
|
+
function runClaudeInstall(machineId, tools, options = {}, runner = runMachineCommand) {
|
|
9442
9461
|
const plan = buildClaudeInstallPlan(machineId, tools);
|
|
9443
9462
|
if (!options.apply)
|
|
9444
9463
|
return plan;
|
|
@@ -9447,14 +9466,7 @@ function runClaudeInstall(machineId, tools, options = {}) {
|
|
|
9447
9466
|
}
|
|
9448
9467
|
let executed = 0;
|
|
9449
9468
|
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
|
-
}
|
|
9469
|
+
requireMachineCommandSuccess(`AI CLI install ${step.id}`, runner(plan.machineId, step.command));
|
|
9458
9470
|
executed += 1;
|
|
9459
9471
|
}
|
|
9460
9472
|
return {
|
|
@@ -9498,7 +9510,10 @@ function buildInstallSteps2(machine) {
|
|
|
9498
9510
|
];
|
|
9499
9511
|
}
|
|
9500
9512
|
function buildTailscaleInstallPlan(machineId) {
|
|
9501
|
-
const machine =
|
|
9513
|
+
const machine = machineId ? getManifestMachine(machineId) : detectCurrentMachineManifest();
|
|
9514
|
+
if (!machine) {
|
|
9515
|
+
throw new Error(`Machine not found in manifest: ${machineId}`);
|
|
9516
|
+
}
|
|
9502
9517
|
return {
|
|
9503
9518
|
machineId: machine.id,
|
|
9504
9519
|
mode: "plan",
|
|
@@ -9506,7 +9521,7 @@ function buildTailscaleInstallPlan(machineId) {
|
|
|
9506
9521
|
executed: 0
|
|
9507
9522
|
};
|
|
9508
9523
|
}
|
|
9509
|
-
function runTailscaleInstall(machineId, options = {}) {
|
|
9524
|
+
function runTailscaleInstall(machineId, options = {}, runner = runMachineCommand) {
|
|
9510
9525
|
const plan = buildTailscaleInstallPlan(machineId);
|
|
9511
9526
|
if (!options.apply)
|
|
9512
9527
|
return plan;
|
|
@@ -9515,14 +9530,7 @@ function runTailscaleInstall(machineId, options = {}) {
|
|
|
9515
9530
|
}
|
|
9516
9531
|
let executed = 0;
|
|
9517
9532
|
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
|
-
}
|
|
9533
|
+
requireMachineCommandSuccess(`Tailscale install ${step.id}`, runner(plan.machineId, step.command));
|
|
9526
9534
|
executed += 1;
|
|
9527
9535
|
}
|
|
9528
9536
|
return {
|
|
@@ -10563,16 +10571,17 @@ function quote4(value) {
|
|
|
10563
10571
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
10564
10572
|
}
|
|
10565
10573
|
function packageCheckCommand(machine, packageName, manager = machine.platform === "macos" ? "brew" : "apt") {
|
|
10574
|
+
const quotedPackageName = quote4(packageName);
|
|
10566
10575
|
if (manager === "bun") {
|
|
10567
|
-
return `bun pm ls -g --all | grep -F ${
|
|
10576
|
+
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
10577
|
}
|
|
10569
10578
|
if (manager === "brew") {
|
|
10570
|
-
return `brew list --versions ${
|
|
10579
|
+
return `if brew list --versions ${quotedPackageName} >/dev/null 2>&1; then printf 'installed=1\\n'; else printf 'installed=0\\n'; fi`;
|
|
10571
10580
|
}
|
|
10572
10581
|
if (manager === "apt") {
|
|
10573
|
-
return `dpkg -s ${
|
|
10582
|
+
return `if dpkg -s ${quotedPackageName} >/dev/null 2>&1; then printf 'installed=1\\n'; else printf 'installed=0\\n'; fi`;
|
|
10574
10583
|
}
|
|
10575
|
-
return `command -v ${
|
|
10584
|
+
return `if command -v ${quotedPackageName} >/dev/null 2>&1; then printf 'installed=1\\n'; else printf 'installed=0\\n'; fi`;
|
|
10576
10585
|
}
|
|
10577
10586
|
function packageInstallCommand(machine, packageName, manager = machine.platform === "macos" ? "brew" : "apt") {
|
|
10578
10587
|
if (manager === "bun") {
|
|
@@ -10586,15 +10595,15 @@ function packageInstallCommand(machine, packageName, manager = machine.platform
|
|
|
10586
10595
|
}
|
|
10587
10596
|
return packageName;
|
|
10588
10597
|
}
|
|
10589
|
-
function detectPackageActions(machine) {
|
|
10598
|
+
function detectPackageActions(machine, runner) {
|
|
10590
10599
|
return (machine.packages || []).map((pkg, index) => {
|
|
10591
10600
|
const manager = pkg.manager || (machine.platform === "macos" ? "brew" : "apt");
|
|
10592
|
-
const check =
|
|
10593
|
-
|
|
10594
|
-
|
|
10595
|
-
|
|
10596
|
-
|
|
10597
|
-
|
|
10601
|
+
const check = runner(machine.id, packageCheckCommand(machine, pkg.name, manager));
|
|
10602
|
+
if (check.exitCode !== 0) {
|
|
10603
|
+
throw new Error(describeMachineCommandFailure(`Sync package probe ${pkg.name}`, check));
|
|
10604
|
+
}
|
|
10605
|
+
const installed = check.stdout.split(`
|
|
10606
|
+
`).some((line) => line.trim() === "installed=1");
|
|
10598
10607
|
return {
|
|
10599
10608
|
id: `package-${index + 1}`,
|
|
10600
10609
|
title: `${installed ? "Package present" : "Install package"} ${pkg.name}`,
|
|
@@ -10605,6 +10614,9 @@ function detectPackageActions(machine) {
|
|
|
10605
10614
|
});
|
|
10606
10615
|
}
|
|
10607
10616
|
function detectFileActions(machine) {
|
|
10617
|
+
if ((machine.files || []).length > 0 && resolveMachineCommand(machine.id, "true").source !== "local") {
|
|
10618
|
+
throw new Error(`Remote file sync planning is not supported for ${machine.id}; refusing to inspect or apply local paths as remote state.`);
|
|
10619
|
+
}
|
|
10608
10620
|
return (machine.files || []).map((file, index) => {
|
|
10609
10621
|
const sourceExists = existsSync9(file.source);
|
|
10610
10622
|
const targetExists = existsSync9(file.target);
|
|
@@ -10628,17 +10640,20 @@ function detectFileActions(machine) {
|
|
|
10628
10640
|
};
|
|
10629
10641
|
});
|
|
10630
10642
|
}
|
|
10631
|
-
function buildSyncPlan(machineId) {
|
|
10643
|
+
function buildSyncPlan(machineId, runner = runMachineCommand) {
|
|
10632
10644
|
const manifest = readManifest();
|
|
10633
10645
|
const currentMachineId = getLocalMachineId();
|
|
10634
10646
|
const selected = machineId ? manifest.machines.find((machine) => machine.id === machineId) : manifest.machines.find((machine) => machine.id === currentMachineId);
|
|
10647
|
+
if (machineId && !selected) {
|
|
10648
|
+
throw new Error(`Machine not found in manifest: ${machineId}`);
|
|
10649
|
+
}
|
|
10635
10650
|
const target = selected || {
|
|
10636
10651
|
id: currentMachineId,
|
|
10637
10652
|
platform: "linux",
|
|
10638
10653
|
workspacePath: `${homedir7()}/workspace`
|
|
10639
10654
|
};
|
|
10640
10655
|
const actions = [
|
|
10641
|
-
...detectPackageActions(target),
|
|
10656
|
+
...detectPackageActions(target, runner),
|
|
10642
10657
|
...detectFileActions(target)
|
|
10643
10658
|
];
|
|
10644
10659
|
return {
|
|
@@ -10668,8 +10683,8 @@ function applyFileAction(command) {
|
|
|
10668
10683
|
symlinkSync(sourcePath, targetPath);
|
|
10669
10684
|
}
|
|
10670
10685
|
}
|
|
10671
|
-
function runSync(machineId, options = {}) {
|
|
10672
|
-
const plan = buildSyncPlan(machineId);
|
|
10686
|
+
function runSync(machineId, options = {}, runner = runMachineCommand) {
|
|
10687
|
+
const plan = buildSyncPlan(machineId, runner);
|
|
10673
10688
|
if (!options.apply) {
|
|
10674
10689
|
return plan;
|
|
10675
10690
|
}
|
|
@@ -10683,18 +10698,17 @@ function runSync(machineId, options = {}) {
|
|
|
10683
10698
|
if (action.kind === "file") {
|
|
10684
10699
|
applyFileAction(action.command);
|
|
10685
10700
|
} else {
|
|
10686
|
-
const result =
|
|
10687
|
-
stdout: "pipe",
|
|
10688
|
-
stderr: "pipe",
|
|
10689
|
-
env: process.env
|
|
10690
|
-
});
|
|
10701
|
+
const result = runner(plan.machineId, action.command);
|
|
10691
10702
|
if (result.exitCode !== 0) {
|
|
10692
10703
|
recordSyncRun(plan.machineId, "failed", {
|
|
10693
10704
|
executed,
|
|
10694
10705
|
failedAction: action,
|
|
10695
|
-
stderr: result.stderr
|
|
10706
|
+
stderr: result.stderr,
|
|
10707
|
+
stdout: result.stdout,
|
|
10708
|
+
exitCode: result.exitCode,
|
|
10709
|
+
source: result.source
|
|
10696
10710
|
});
|
|
10697
|
-
throw new Error(`Sync action
|
|
10711
|
+
throw new Error(describeMachineCommandFailure(`Sync action ${action.id}`, result));
|
|
10698
10712
|
}
|
|
10699
10713
|
}
|
|
10700
10714
|
executed += 1;
|