@hasna/machines 0.0.17 → 0.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -79,6 +79,31 @@ machines topology --no-tailscale --json
79
79
  machines route --machine spark01 --json
80
80
  ```
81
81
 
82
+ Consumers that need repo paths can resolve trust-aware workspace mappings
83
+ without importing the full machines app:
84
+
85
+ ```ts
86
+ import { resolveMachineWorkspace } from "@hasna/machines/consumer";
87
+
88
+ const workspace = resolveMachineWorkspace({
89
+ machineId: "spark01",
90
+ projectId: "open-knowledge",
91
+ repoName: "open-knowledge",
92
+ });
93
+
94
+ console.log(workspace.paths.project_root.path);
95
+ console.log(workspace.paths.open_files_root.path);
96
+ ```
97
+
98
+ The resolver returns the machine workspace root, project repo root,
99
+ open-files root, current/primary flags, trust/auth status, and redacted
100
+ diagnostics. It uses explicit manifest metadata first and deterministic
101
+ workspace inference second; consumers can still pass manual overrides.
102
+
103
+ ```bash
104
+ machines workspace resolve --machine spark01 --project open-knowledge --repo open-knowledge --json
105
+ ```
106
+
82
107
  ## Compatibility SDK
83
108
 
84
109
  Open-core consumers can use `@hasna/machines` to preflight a peer before
@@ -131,6 +156,20 @@ Configure database storage with `HASNA_MACHINES_DATABASE_URL` or fallback
131
156
  `HASNA_MACHINES_STORAGE_MODE` or `MACHINES_STORAGE_MODE` with `local`,
132
157
  `hybrid`, or `remote`.
133
158
 
159
+ Machine backups are preview-only unless `--apply --yes` is passed. The backup
160
+ target can be explicit or environment-backed:
161
+
162
+ ```bash
163
+ machines backup --bucket fleet-backups --prefix machines --json
164
+ HASNA_MACHINES_S3_BUCKET=hasna-xyz-opensource-machines-prod machines backup --json
165
+ ```
166
+
167
+ `--bucket` and `--prefix` always win. Without `--bucket`, the backup command
168
+ uses `HASNA_MACHINES_S3_BUCKET` or fallback `MACHINES_S3_BUCKET`; prefix uses
169
+ `HASNA_MACHINES_S3_PREFIX`, fallback `MACHINES_S3_PREFIX`, or `machines`.
170
+ This keeps the open-source CLI local/self-hosted by default while allowing
171
+ Hasna deployments to route app-owned backups through canonical storage metadata.
172
+
134
173
  ## Applications and tooling
135
174
 
136
175
  ```bash
package/dist/cli/index.js CHANGED
@@ -2286,7 +2286,7 @@ class PgAdapterAsync {
2286
2286
  var init_remote_storage = () => {};
2287
2287
 
2288
2288
  // src/storage-sync.ts
2289
- function readEnv(name) {
2289
+ function readEnv2(name) {
2290
2290
  const value = process.env[name]?.trim();
2291
2291
  return value || undefined;
2292
2292
  }
@@ -2298,7 +2298,7 @@ function normalizeStorageMode(value) {
2298
2298
  }
2299
2299
  function getStorageDatabaseEnvName() {
2300
2300
  for (const name of STORAGE_DATABASE_ENV) {
2301
- if (readEnv(name))
2301
+ if (readEnv2(name))
2302
2302
  return name;
2303
2303
  }
2304
2304
  return null;
@@ -2309,10 +2309,10 @@ function getStorageDatabaseEnv() {
2309
2309
  }
2310
2310
  function getStorageDatabaseUrl() {
2311
2311
  const env2 = getStorageDatabaseEnv();
2312
- return env2 ? readEnv(env2.name) ?? null : null;
2312
+ return env2 ? readEnv2(env2.name) ?? null : null;
2313
2313
  }
2314
2314
  function getStorageMode() {
2315
- const mode = normalizeStorageMode(readEnv(MACHINES_STORAGE_MODE_ENV)) ?? normalizeStorageMode(readEnv(MACHINES_STORAGE_MODE_FALLBACK_ENV));
2315
+ const mode = normalizeStorageMode(readEnv2(MACHINES_STORAGE_MODE_ENV)) ?? normalizeStorageMode(readEnv2(MACHINES_STORAGE_MODE_FALLBACK_ENV));
2316
2316
  if (mode)
2317
2317
  return mode;
2318
2318
  return getStorageDatabaseUrl() ? "hybrid" : "local";
@@ -7113,6 +7113,7 @@ var machineSchema = exports_external.object({
7113
7113
  workspacePath: exports_external.string(),
7114
7114
  bunPath: exports_external.string().optional(),
7115
7115
  tags: exports_external.array(exports_external.string()).optional(),
7116
+ metadata: exports_external.record(exports_external.unknown()).optional(),
7116
7117
  packages: exports_external.array(packageSchema).optional(),
7117
7118
  apps: exports_external.array(appSchema).optional(),
7118
7119
  files: exports_external.array(fileSchema).optional()
@@ -7344,9 +7345,52 @@ function runSetup(machineId, options = {}) {
7344
7345
  // src/commands/backup.ts
7345
7346
  import { homedir as homedir2 } from "os";
7346
7347
  import { join as join3 } from "path";
7348
+ var MACHINES_BACKUP_BUCKET_ENV = "HASNA_MACHINES_S3_BUCKET";
7349
+ var MACHINES_BACKUP_BUCKET_FALLBACK_ENV = "MACHINES_S3_BUCKET";
7350
+ var MACHINES_BACKUP_PREFIX_ENV = "HASNA_MACHINES_S3_PREFIX";
7351
+ var MACHINES_BACKUP_PREFIX_FALLBACK_ENV = "MACHINES_S3_PREFIX";
7352
+ var DEFAULT_BACKUP_PREFIX = "machines";
7347
7353
  function quote2(value) {
7348
7354
  return `'${value.replace(/'/g, `'\\''`)}'`;
7349
7355
  }
7356
+ function readEnv(name) {
7357
+ const value = process.env[name]?.trim();
7358
+ return value || undefined;
7359
+ }
7360
+ function readBackupBucketEnv() {
7361
+ const primary = readEnv(MACHINES_BACKUP_BUCKET_ENV);
7362
+ if (primary)
7363
+ return { bucket: primary, bucketSource: MACHINES_BACKUP_BUCKET_ENV };
7364
+ const fallback = readEnv(MACHINES_BACKUP_BUCKET_FALLBACK_ENV);
7365
+ if (fallback)
7366
+ return { bucket: fallback, bucketSource: MACHINES_BACKUP_BUCKET_FALLBACK_ENV };
7367
+ return null;
7368
+ }
7369
+ function readBackupPrefixEnv() {
7370
+ const primary = readEnv(MACHINES_BACKUP_PREFIX_ENV);
7371
+ if (primary)
7372
+ return { prefix: primary, prefixSource: MACHINES_BACKUP_PREFIX_ENV };
7373
+ const fallback = readEnv(MACHINES_BACKUP_PREFIX_FALLBACK_ENV);
7374
+ if (fallback)
7375
+ return { prefix: fallback, prefixSource: MACHINES_BACKUP_PREFIX_FALLBACK_ENV };
7376
+ return null;
7377
+ }
7378
+ function resolveBackupTarget(options = {}) {
7379
+ const explicitBucket = options.bucket?.trim();
7380
+ const envBucket = explicitBucket ? null : readBackupBucketEnv();
7381
+ const bucket = explicitBucket || envBucket?.bucket;
7382
+ if (!bucket) {
7383
+ throw new Error(`Missing S3 backup bucket. Pass --bucket or set ${MACHINES_BACKUP_BUCKET_ENV} or ${MACHINES_BACKUP_BUCKET_FALLBACK_ENV}.`);
7384
+ }
7385
+ const explicitPrefix = options.prefix?.trim();
7386
+ const envPrefix = explicitPrefix ? null : readBackupPrefixEnv();
7387
+ return {
7388
+ bucket,
7389
+ prefix: explicitPrefix || envPrefix?.prefix || DEFAULT_BACKUP_PREFIX,
7390
+ bucketSource: explicitBucket ? "argument" : envBucket.bucketSource,
7391
+ prefixSource: explicitPrefix ? "argument" : envPrefix?.prefixSource || "default"
7392
+ };
7393
+ }
7350
7394
  function defaultBackupSources() {
7351
7395
  const home = homedir2();
7352
7396
  return [
@@ -7355,7 +7399,8 @@ function defaultBackupSources() {
7355
7399
  join3(home, ".secrets")
7356
7400
  ];
7357
7401
  }
7358
- function buildBackupPlan(bucket, prefix = "machines") {
7402
+ function buildBackupPlan(bucket, prefix) {
7403
+ const target = resolveBackupTarget({ bucket, prefix });
7359
7404
  const archivePath = join3(homedir2(), ".hasna", "machines", "backup.tgz");
7360
7405
  const sources = defaultBackupSources();
7361
7406
  const steps = [
@@ -7368,7 +7413,7 @@ function buildBackupPlan(bucket, prefix = "machines") {
7368
7413
  {
7369
7414
  id: "backup-upload",
7370
7415
  title: "Upload archive to S3",
7371
- command: `aws s3 cp ${quote2(archivePath)} s3://${bucket}/${prefix}/$(hostname)-backup.tgz`,
7416
+ command: `aws s3 cp ${quote2(archivePath)} s3://${target.bucket}/${target.prefix}/$(hostname)-backup.tgz`,
7372
7417
  manager: "custom"
7373
7418
  }
7374
7419
  ];
@@ -7379,7 +7424,7 @@ function buildBackupPlan(bucket, prefix = "machines") {
7379
7424
  executed: 0
7380
7425
  };
7381
7426
  }
7382
- function runBackup(bucket, prefix = "machines", options = {}) {
7427
+ function runBackup(bucket, prefix, options = {}) {
7383
7428
  const plan = buildBackupPlan(bucket, prefix);
7384
7429
  if (!options.apply)
7385
7430
  return plan;
@@ -7787,7 +7832,8 @@ function discoverMachineTopology(options = {}) {
7787
7832
  topology: true,
7788
7833
  compatibility: true,
7789
7834
  route_resolution: true,
7790
- cli_json_fallback: true
7835
+ cli_json_fallback: true,
7836
+ workspace_path_mapping: true
7791
7837
  },
7792
7838
  generated_at: now.toISOString(),
7793
7839
  local_machine_id: localMachineId,
@@ -7912,8 +7958,220 @@ function resolveMachineRoute(machineId, options = {}) {
7912
7958
  warnings
7913
7959
  };
7914
7960
  }
7961
+ function isRecord(value) {
7962
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
7963
+ }
7964
+ function metadataString(metadata, keys) {
7965
+ for (const key of keys) {
7966
+ const value = metadata[key];
7967
+ if (typeof value === "string" && value.trim())
7968
+ return value.trim();
7969
+ }
7970
+ return null;
7971
+ }
7972
+ function metadataBoolean(metadata, keys) {
7973
+ for (const key of keys) {
7974
+ const value = metadata[key];
7975
+ if (typeof value === "boolean")
7976
+ return value;
7977
+ }
7978
+ return null;
7979
+ }
7980
+ function metadataStringArray(metadata, keys) {
7981
+ for (const key of keys) {
7982
+ const value = metadata[key];
7983
+ if (Array.isArray(value))
7984
+ return value.filter((entry) => typeof entry === "string");
7985
+ }
7986
+ return [];
7987
+ }
7988
+ function readMappedPath(input) {
7989
+ for (const containerName of input.containers) {
7990
+ const container = input.metadata[containerName];
7991
+ if (!isRecord(container))
7992
+ continue;
7993
+ for (const key of input.keys) {
7994
+ const value = container[key];
7995
+ if (typeof value === "string" && value.trim())
7996
+ return value.trim();
7997
+ if (isRecord(value)) {
7998
+ const path = metadataString(value, ["path", "root", "workspacePath", "workspace_path"]);
7999
+ if (path)
8000
+ return path;
8001
+ }
8002
+ }
8003
+ }
8004
+ return null;
8005
+ }
8006
+ function trimTrailingSlash(value) {
8007
+ return value.replace(/\/+$/, "");
8008
+ }
8009
+ function joinPath(left, right) {
8010
+ return `${trimTrailingSlash(left)}/${right.replace(/^\/+/, "")}`;
8011
+ }
8012
+ function inferRepoRoot(workspaceRoot, repoName) {
8013
+ if (!workspaceRoot || !repoName)
8014
+ return null;
8015
+ const root = trimTrailingSlash(workspaceRoot);
8016
+ if (root.endsWith(`/${repoName}`) || root === repoName)
8017
+ return root;
8018
+ if (root.endsWith("/workspace") || root.endsWith("/Workspace")) {
8019
+ return joinPath(root, `hasna/opensource/${repoName}`);
8020
+ }
8021
+ return joinPath(root, repoName);
8022
+ }
8023
+ function projectPathFromMetadata(metadata, projectId, repoName) {
8024
+ const keys = [projectId, repoName].filter((value) => Boolean(value));
8025
+ return readMappedPath({
8026
+ metadata,
8027
+ containers: ["workspace_paths", "workspacePaths", "repo_paths", "repoPaths", "project_paths", "projectPaths", "projects"],
8028
+ keys
8029
+ });
8030
+ }
8031
+ function openFilesPathFromMetadata(metadata, projectId, repoName) {
8032
+ const direct = metadataString(metadata, ["open_files_root", "openFilesRoot", "open_files_path", "openFilesPath"]);
8033
+ if (direct)
8034
+ return direct;
8035
+ const keys = [projectId, repoName, "open-files", "open_files", "default"].filter((value) => Boolean(value));
8036
+ return readMappedPath({
8037
+ metadata,
8038
+ containers: ["open_files_roots", "openFilesRoots", "open_files_paths", "openFilesPaths"],
8039
+ keys
8040
+ });
8041
+ }
8042
+ function trustStatus(machine) {
8043
+ if (!machine)
8044
+ return "unknown";
8045
+ const explicit = metadataString(machine.metadata, ["trust_status", "trustStatus"]);
8046
+ if (explicit === "trusted" || explicit === "untrusted" || explicit === "unknown")
8047
+ return explicit;
8048
+ const trusted = metadataBoolean(machine.metadata, ["trusted", "syncTrusted", "sync_trusted"]);
8049
+ if (trusted === true)
8050
+ return "trusted";
8051
+ if (trusted === false)
8052
+ return "untrusted";
8053
+ if (machine.route_hints.some((hint) => hint.kind === "local"))
8054
+ return "trusted";
8055
+ if (machine.tags.includes("trusted"))
8056
+ return "trusted";
8057
+ return "unknown";
8058
+ }
8059
+ function authStatus(machine) {
8060
+ if (!machine)
8061
+ return "unknown";
8062
+ const explicit = metadataString(machine.metadata, ["auth_status", "authStatus"]);
8063
+ if (explicit === "authenticated" || explicit === "unauthenticated" || explicit === "unknown")
8064
+ return explicit;
8065
+ const authenticated = metadataBoolean(machine.metadata, ["authenticated", "sshAuthorized", "ssh_authorized"]);
8066
+ if (authenticated === true)
8067
+ return "authenticated";
8068
+ if (authenticated === false)
8069
+ return "unauthenticated";
8070
+ if (machine.route_hints.some((hint) => hint.kind === "local"))
8071
+ return "authenticated";
8072
+ return "unknown";
8073
+ }
8074
+ function primaryMachine(machine, projectId, primaryMachineId) {
8075
+ if (!machine)
8076
+ return false;
8077
+ if (primaryMachineId)
8078
+ return machine.machine_id === primaryMachineId;
8079
+ if (metadataBoolean(machine.metadata, ["primary", "primary_machine", "primaryMachine"]) === true)
8080
+ return true;
8081
+ const primaryProjects = metadataStringArray(machine.metadata, ["primary_projects", "primaryProjects"]);
8082
+ if (primaryProjects.includes(projectId))
8083
+ return true;
8084
+ return machine.tags.includes("primary");
8085
+ }
8086
+ function metadataKeysForDiagnostics(metadata) {
8087
+ return Object.keys(metadata).filter((key) => !/(secret|token|key|password|credential)/i.test(key)).sort();
8088
+ }
8089
+ function resolveMachineWorkspace(options) {
8090
+ const topology = options.topology ?? discoverMachineTopology(options);
8091
+ const warnings = [...topology.warnings];
8092
+ const { machine, matchedBy } = findRouteMachine(topology, options.machineId);
8093
+ const generatedAt = (options.now ?? new Date).toISOString();
8094
+ const repoName = options.repoName ?? options.projectId;
8095
+ const openFilesRepoName = options.openFilesRepoName ?? "open-files";
8096
+ if (!machine) {
8097
+ warnings.push(`machine_not_found:${options.machineId}`);
8098
+ return {
8099
+ schema_version: MACHINES_CONSUMER_CONTRACT_VERSION,
8100
+ package: topology.package,
8101
+ ok: false,
8102
+ requested_machine_id: options.machineId,
8103
+ machine_id: null,
8104
+ generated_at: generatedAt,
8105
+ project: { project_id: options.projectId, repo_name: repoName, canonical: Boolean(options.projectId) },
8106
+ machine: { current: false, primary: false, trust_status: "unknown", auth_status: "unknown" },
8107
+ paths: {
8108
+ workspace_root: { path: null, source: "unresolved" },
8109
+ project_root: { path: null, source: "unresolved" },
8110
+ open_files_root: { path: null, source: "unresolved" }
8111
+ },
8112
+ evidence: {
8113
+ topology: true,
8114
+ matched_by: matchedBy,
8115
+ manifest_declared: null,
8116
+ metadata_keys: []
8117
+ },
8118
+ warnings
8119
+ };
8120
+ }
8121
+ const metadata = machine.metadata;
8122
+ const workspaceRootPath = options.workspaceRoot ?? machine.workspace_path;
8123
+ const workspaceRootSource = options.workspaceRoot ? "argument" : machine.workspace_path ? "manifest" : "unresolved";
8124
+ const metadataProjectRoot = projectPathFromMetadata(metadata, options.projectId, repoName);
8125
+ const inferredProjectRoot = inferRepoRoot(workspaceRootPath, repoName);
8126
+ const projectRootPath = options.projectRoot ?? metadataProjectRoot ?? inferredProjectRoot;
8127
+ const projectRootSource = options.projectRoot ? "argument" : metadataProjectRoot ? "manifest_metadata" : inferredProjectRoot ? "inferred" : "unresolved";
8128
+ const metadataOpenFilesRoot = openFilesPathFromMetadata(metadata, options.projectId, openFilesRepoName);
8129
+ const inferredOpenFilesRoot = inferRepoRoot(workspaceRootPath, openFilesRepoName);
8130
+ const openFilesRootPath = options.openFilesRoot ?? metadataOpenFilesRoot ?? inferredOpenFilesRoot;
8131
+ const openFilesRootSource = options.openFilesRoot ? "argument" : metadataOpenFilesRoot ? "manifest_metadata" : inferredOpenFilesRoot ? "inferred" : "unresolved";
8132
+ if (projectRootSource === "inferred")
8133
+ warnings.push(`project_root_inferred:${options.projectId}`);
8134
+ if (openFilesRootSource === "inferred")
8135
+ warnings.push(`open_files_root_inferred:${options.projectId}`);
8136
+ if (!projectRootPath)
8137
+ warnings.push(`project_root_unresolved:${options.projectId}`);
8138
+ return {
8139
+ schema_version: MACHINES_CONSUMER_CONTRACT_VERSION,
8140
+ package: topology.package,
8141
+ ok: Boolean(projectRootPath),
8142
+ requested_machine_id: options.machineId,
8143
+ machine_id: machine.machine_id,
8144
+ generated_at: generatedAt,
8145
+ project: {
8146
+ project_id: options.projectId,
8147
+ repo_name: repoName,
8148
+ canonical: Boolean(options.projectId && repoName)
8149
+ },
8150
+ machine: {
8151
+ current: machine.machine_id === topology.local_machine_id,
8152
+ primary: primaryMachine(machine, options.projectId, options.primaryMachineId),
8153
+ trust_status: trustStatus(machine),
8154
+ auth_status: authStatus(machine)
8155
+ },
8156
+ paths: {
8157
+ workspace_root: { path: workspaceRootPath, source: workspaceRootSource },
8158
+ project_root: { path: projectRootPath, source: projectRootSource },
8159
+ open_files_root: { path: openFilesRootPath, source: openFilesRootSource }
8160
+ },
8161
+ evidence: {
8162
+ topology: true,
8163
+ matched_by: matchedBy,
8164
+ manifest_declared: machine.manifest_declared,
8165
+ metadata_keys: metadataKeysForDiagnostics(metadata)
8166
+ },
8167
+ warnings
8168
+ };
8169
+ }
7915
8170
 
7916
8171
  // src/commands/ssh.ts
8172
+ function shellQuote(value) {
8173
+ return `'${value.replace(/'/g, "'\\''")}'`;
8174
+ }
7917
8175
  function resolveSshTarget(machineId, options = {}) {
7918
8176
  const resolved = resolveMachineRoute(machineId, options);
7919
8177
  if (!resolved.ok || !resolved.target) {
@@ -7932,11 +8190,11 @@ function resolveSshTarget(machineId, options = {}) {
7932
8190
  }
7933
8191
  function buildSshCommand(machineId, remoteCommand, options = {}) {
7934
8192
  const resolved = resolveSshTarget(machineId, options);
7935
- return remoteCommand ? `ssh ${resolved.target} ${JSON.stringify(remoteCommand)}` : `ssh ${resolved.target}`;
8193
+ return remoteCommand ? `ssh ${resolved.target} ${shellQuote(remoteCommand)}` : `ssh ${resolved.target}`;
7936
8194
  }
7937
8195
 
7938
8196
  // src/remote.ts
7939
- function shellQuote(value) {
8197
+ function shellQuote2(value) {
7940
8198
  return `'${value.replace(/'/g, "'\\''")}'`;
7941
8199
  }
7942
8200
  function machineIsLocal(machineId, localMachineId) {
@@ -7954,7 +8212,7 @@ function resolveMachineCommand(machineId, command, localMachineId = getLocalMach
7954
8212
  } catch (error) {
7955
8213
  const message = String(error.message ?? error);
7956
8214
  if (message.includes("Machine route not found") || message.includes("Machine not found in manifest")) {
7957
- return { source: "ssh", shellCommand: `ssh ${shellQuote(machineId)} ${shellQuote(command)}` };
8215
+ return { source: "ssh", shellCommand: `ssh ${shellQuote2(machineId)} ${shellQuote2(command)}` };
7958
8216
  }
7959
8217
  throw error;
7960
8218
  }
@@ -7987,7 +8245,7 @@ function getAppManager(machine, app) {
7987
8245
  return "winget";
7988
8246
  return "apt";
7989
8247
  }
7990
- function shellQuote2(value) {
8248
+ function shellQuote3(value) {
7991
8249
  return `'${value.replace(/'/g, `'\\''`)}'`;
7992
8250
  }
7993
8251
  function buildAppCommand(machine, app) {
@@ -8008,7 +8266,7 @@ function buildAppCommand(machine, app) {
8008
8266
  return `sudo apt-get install -y ${packageName}`;
8009
8267
  }
8010
8268
  function buildAppProbeCommand(machine, app) {
8011
- const packageName = shellQuote2(getPackageName(app));
8269
+ const packageName = shellQuote3(getPackageName(app));
8012
8270
  const manager = getAppManager(machine, app);
8013
8271
  if (manager === "custom") {
8014
8272
  return `if command -v ${packageName} >/dev/null 2>&1; then printf 'installed=1\\nversion=custom\\n'; else printf 'installed=0\\n'; fi`;
@@ -8303,7 +8561,7 @@ var notificationConfigSchema = exports_external.object({
8303
8561
  function sortChannels(channels) {
8304
8562
  return [...channels].sort((left, right) => left.id.localeCompare(right.id));
8305
8563
  }
8306
- function shellQuote3(value) {
8564
+ function shellQuote4(value) {
8307
8565
  return `'${value.replace(/'/g, `'\\''`)}'`;
8308
8566
  }
8309
8567
  function hasCommand2(binary) {
@@ -8350,7 +8608,7 @@ ${message}
8350
8608
  };
8351
8609
  }
8352
8610
  if (hasCommand2("mail")) {
8353
- const command = `printf %s ${shellQuote3(message)} | mail -s ${shellQuote3(subject)} ${shellQuote3(channel.target)}`;
8611
+ const command = `printf %s ${shellQuote4(message)} | mail -s ${shellQuote4(subject)} ${shellQuote4(channel.target)}`;
8354
8612
  const result = Bun.spawnSync(["bash", "-lc", command], {
8355
8613
  stdout: "pipe",
8356
8614
  stderr: "pipe",
@@ -8785,7 +9043,7 @@ var DEFAULT_COMMANDS = [
8785
9043
  function defaultPackages() {
8786
9044
  return [{ name: "@hasna/machines", command: "machines", expectedVersion: getPackageVersion(), required: true }];
8787
9045
  }
8788
- function shellQuote4(value) {
9046
+ function shellQuote5(value) {
8789
9047
  return `'${value.replace(/'/g, "'\\''")}'`;
8790
9048
  }
8791
9049
  function commandId(value) {
@@ -8836,7 +9094,7 @@ function defaultRunner2(machineId, command) {
8836
9094
  return runMachineCommand(machineId, command);
8837
9095
  }
8838
9096
  function inspectCommand(machineId, spec, runner) {
8839
- const command = shellQuote4(spec.command);
9097
+ const command = shellQuote5(spec.command);
8840
9098
  const versionArgs = spec.versionArgs ?? "--version";
8841
9099
  const script = [
8842
9100
  `cmd=${command}`,
@@ -8865,7 +9123,7 @@ function fieldCommand(field) {
8865
9123
  }
8866
9124
  function inspectWorkspace(machineId, spec, runner) {
8867
9125
  const script = [
8868
- `path=${shellQuote4(spec.path)}`,
9126
+ `path=${shellQuote5(spec.path)}`,
8869
9127
  'printf "exists=%s\\n" "$(test -d "$path" && printf yes || printf no)"',
8870
9128
  'pkg="$path/package.json"',
8871
9129
  'printf "package_json=%s\\n" "$(test -f "$pkg" && printf yes || printf no)"',
@@ -10511,6 +10769,22 @@ function renderCompatibilityResult(result) {
10511
10769
  ].join(`
10512
10770
  `);
10513
10771
  }
10772
+ function renderWorkspaceResolution(result) {
10773
+ return renderKeyValueTable([
10774
+ ["machine", result.machine_id ?? result.requested_machine_id],
10775
+ ["ok", String(result.ok)],
10776
+ ["project", result.project.project_id],
10777
+ ["repo", result.project.repo_name ?? "unknown"],
10778
+ ["current", String(result.machine.current)],
10779
+ ["primary", String(result.machine.primary)],
10780
+ ["trust", result.machine.trust_status],
10781
+ ["auth", result.machine.auth_status],
10782
+ ["workspace root", `${result.paths.workspace_root.path ?? "unresolved"} (${result.paths.workspace_root.source})`],
10783
+ ["project root", `${result.paths.project_root.path ?? "unresolved"} (${result.paths.project_root.source})`],
10784
+ ["open-files root", `${result.paths.open_files_root.path ?? "unresolved"} (${result.paths.open_files_root.source})`],
10785
+ ["warnings", result.warnings.join(", ") || "none"]
10786
+ ]);
10787
+ }
10514
10788
  function renderFleetStatus(status) {
10515
10789
  return [
10516
10790
  renderKeyValueTable([
@@ -10661,11 +10935,28 @@ program2.command("compatibility").description("Check remote package, command, an
10661
10935
  if (!result.ok && !options.json)
10662
10936
  process.exitCode = 1;
10663
10937
  });
10938
+ var workspaceCommand = program2.command("workspace").description("Resolve sync-safe workspace paths for open-* consumers");
10939
+ workspaceCommand.command("resolve").description("Resolve repo and open-files roots for a machine/project").requiredOption("--machine <id>", "Machine identifier").requiredOption("--project <id>", "Canonical project id").option("--repo <name>", "Repository name; defaults to project id").option("--open-files-repo <name>", "Open-files repository name", "open-files").option("--primary-machine <id>", "Primary machine id for the project").option("--workspace-root <path>", "Override the machine workspace root").option("--project-root <path>", "Override the resolved project root").option("--open-files-root <path>", "Override the resolved open-files root").option("--no-tailscale", "Skip tailscale status probing").option("-j, --json", "Print JSON output", false).action((options) => {
10940
+ const result = resolveMachineWorkspace({
10941
+ machineId: options.machine,
10942
+ projectId: options.project,
10943
+ repoName: options.repo,
10944
+ openFilesRepoName: options.openFilesRepo,
10945
+ primaryMachineId: options.primaryMachine,
10946
+ workspaceRoot: options.workspaceRoot,
10947
+ projectRoot: options.projectRoot,
10948
+ openFilesRoot: options.openFilesRoot,
10949
+ includeTailscale: options.tailscale !== false
10950
+ });
10951
+ printJsonOrText(result, renderWorkspaceResolution(result), options.json);
10952
+ if (!result.ok && !options.json)
10953
+ process.exitCode = 1;
10954
+ });
10664
10955
  program2.command("diff").description("Show manifest differences between two machines").requiredOption("--left <id>", "Left machine identifier").option("--right <id>", "Right machine identifier (defaults to current machine)").option("-j, --json", "Print JSON output", false).action((options) => {
10665
10956
  const result = diffMachines(options.left, options.right);
10666
10957
  console.log(JSON.stringify(result, null, 2));
10667
10958
  });
10668
- program2.command("backup").description("Create and optionally upload a machine backup archive").requiredOption("--bucket <name>", "S3 bucket name").option("--prefix <prefix>", "S3 key prefix", "machines").option("--apply", "Execute backup commands instead of previewing the plan", false).option("--yes", "Confirm execution when using --apply", false).option("-j, --json", "Print JSON output", false).action((options) => {
10959
+ program2.command("backup").description("Create and optionally upload a machine backup archive").option("--bucket <name>", "S3 bucket name; defaults to HASNA_MACHINES_S3_BUCKET or MACHINES_S3_BUCKET").option("--prefix <prefix>", "S3 key prefix; defaults to HASNA_MACHINES_S3_PREFIX, MACHINES_S3_PREFIX, or machines").option("--apply", "Execute backup commands instead of previewing the plan", false).option("--yes", "Confirm execution when using --apply", false).option("-j, --json", "Print JSON output", false).action((options) => {
10669
10960
  const result = options.apply ? runBackup(options.bucket, options.prefix, { apply: true, yes: options.yes }) : buildBackupPlan(options.bucket, options.prefix);
10670
10961
  console.log(JSON.stringify(result, null, 2));
10671
10962
  });
@@ -1,6 +1,21 @@
1
1
  import type { SetupResult } from "../types.js";
2
- export declare function buildBackupPlan(bucket: string, prefix?: string): SetupResult;
3
- export declare function runBackup(bucket: string, prefix?: string, options?: {
2
+ export declare const MACHINES_BACKUP_BUCKET_ENV = "HASNA_MACHINES_S3_BUCKET";
3
+ export declare const MACHINES_BACKUP_BUCKET_FALLBACK_ENV = "MACHINES_S3_BUCKET";
4
+ export declare const MACHINES_BACKUP_PREFIX_ENV = "HASNA_MACHINES_S3_PREFIX";
5
+ export declare const MACHINES_BACKUP_PREFIX_FALLBACK_ENV = "MACHINES_S3_PREFIX";
6
+ export declare const DEFAULT_BACKUP_PREFIX = "machines";
7
+ export interface BackupTarget {
8
+ bucket: string;
9
+ prefix: string;
10
+ bucketSource: "argument" | typeof MACHINES_BACKUP_BUCKET_ENV | typeof MACHINES_BACKUP_BUCKET_FALLBACK_ENV;
11
+ prefixSource: "argument" | typeof MACHINES_BACKUP_PREFIX_ENV | typeof MACHINES_BACKUP_PREFIX_FALLBACK_ENV | "default";
12
+ }
13
+ export declare function resolveBackupTarget(options?: {
14
+ bucket?: string;
15
+ prefix?: string;
16
+ }): BackupTarget;
17
+ export declare function buildBackupPlan(bucket?: string, prefix?: string): SetupResult;
18
+ export declare function runBackup(bucket?: string, prefix?: string, options?: {
4
19
  apply?: boolean;
5
20
  yes?: boolean;
6
21
  }): SetupResult;
@@ -1 +1 @@
1
- {"version":3,"file":"backup.d.ts","sourceRoot":"","sources":["../../src/commands/backup.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAa,MAAM,aAAa,CAAC;AAe1D,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,SAAa,GAAG,WAAW,CAwBhF;AAED,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,SAAa,EAAE,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,GAAG,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,WAAW,CA0B5H"}
1
+ {"version":3,"file":"backup.d.ts","sourceRoot":"","sources":["../../src/commands/backup.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAa,MAAM,aAAa,CAAC;AAE1D,eAAO,MAAM,0BAA0B,6BAA6B,CAAC;AACrE,eAAO,MAAM,mCAAmC,uBAAuB,CAAC;AACxE,eAAO,MAAM,0BAA0B,6BAA6B,CAAC;AACrE,eAAO,MAAM,mCAAmC,uBAAuB,CAAC;AACxE,eAAO,MAAM,qBAAqB,aAAa,CAAC;AAEhD,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,UAAU,GAAG,OAAO,0BAA0B,GAAG,OAAO,mCAAmC,CAAC;IAC1G,YAAY,EAAE,UAAU,GAAG,OAAO,0BAA0B,GAAG,OAAO,mCAAmC,GAAG,SAAS,CAAC;CACvH;AA2BD,wBAAgB,mBAAmB,CAAC,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,YAAY,CAoBpG;AAWD,wBAAgB,eAAe,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,WAAW,CAyB7E;AAED,wBAAgB,SAAS,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,GAAG,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,WAAW,CA0BzH"}
@@ -1 +1 @@
1
- {"version":3,"file":"ssh.d.ts","sourceRoot":"","sources":["../../src/commands/ssh.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE/E,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,KAAK,GAAG,WAAW,GAAG,OAAO,GAAG,KAAK,CAAC;IAC7C,UAAU,EAAE,OAAO,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC;IACzD,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,GAAE,mBAAwB,GAAG,iBAAiB,CAexG;AAED,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,MAAM,EAAE,OAAO,GAAE,mBAAwB,GAAG,MAAM,CAGpH"}
1
+ {"version":3,"file":"ssh.d.ts","sourceRoot":"","sources":["../../src/commands/ssh.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE/E,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,KAAK,GAAG,WAAW,GAAG,OAAO,GAAG,KAAK,CAAC;IAC7C,UAAU,EAAE,OAAO,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC;IACzD,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAMD,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,GAAE,mBAAwB,GAAG,iBAAiB,CAexG;AAED,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,MAAM,EAAE,OAAO,GAAE,mBAAwB,GAAG,MAAM,CAGpH"}
@@ -1,5 +1,5 @@
1
- export { MACHINES_CONSUMER_CONTRACT_VERSION, MACHINES_PACKAGE_NAME, discoverMachineTopology, getLocalMachineTopology, resolveMachineRoute, } from "./topology.js";
2
- export type { MachineRouteConfidence, MachineRouteHint, MachineRouteKind, MachineRouteOptions, MachineRouteResolution, MachineTopology, MachineTopologyEntry, MachineTopologyOptions, MachinesConsumerCapabilities, MachinesContractPackage, TopologyCommandResult, TopologyCommandRunner, } from "./topology.js";
1
+ export { MACHINES_CONSUMER_CONTRACT_VERSION, MACHINES_PACKAGE_NAME, discoverMachineTopology, getLocalMachineTopology, resolveMachineRoute, resolveMachineWorkspace, } from "./topology.js";
2
+ export type { MachineRouteConfidence, MachineRouteHint, MachineRouteKind, MachineRouteOptions, MachineRouteResolution, MachineTopology, MachineTopologyEntry, MachineTopologyOptions, MachineWorkspaceAuthStatus, MachineWorkspaceOptions, MachineWorkspacePath, MachineWorkspacePathSource, MachineWorkspaceProject, MachineWorkspaceResolution, MachineWorkspaceTrustStatus, MachinesConsumerCapabilities, MachinesContractPackage, TopologyCommandResult, TopologyCommandRunner, } from "./topology.js";
3
3
  export { checkMachineCompatibility, } from "./compatibility.js";
4
4
  export type { CompatibilityCheck, CompatibilityCommandRunner, CompatibilityCommandSpec, CompatibilityPackageSpec, CompatibilitySource, CompatibilityStatus, CompatibilityWorkspaceSpec, MachineCompatibilityOptions, MachineCompatibilityReport, } from "./compatibility.js";
5
5
  export { resolveMachineCommand, runMachineCommand, } from "./remote.js";
@@ -1 +1 @@
1
- {"version":3,"file":"consumer.d.ts","sourceRoot":"","sources":["../src/consumer.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kCAAkC,EAClC,qBAAqB,EACrB,uBAAuB,EACvB,uBAAuB,EACvB,mBAAmB,GACpB,MAAM,eAAe,CAAC;AACvB,YAAY,EACV,sBAAsB,EACtB,gBAAgB,EAChB,gBAAgB,EAChB,mBAAmB,EACnB,sBAAsB,EACtB,eAAe,EACf,oBAAoB,EACpB,sBAAsB,EACtB,4BAA4B,EAC5B,uBAAuB,EACvB,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,yBAAyB,GAC1B,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EACV,kBAAkB,EAClB,0BAA0B,EAC1B,wBAAwB,EACxB,wBAAwB,EACxB,mBAAmB,EACnB,mBAAmB,EACnB,0BAA0B,EAC1B,2BAA2B,EAC3B,0BAA0B,GAC3B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,qBAAqB,EACrB,iBAAiB,GAClB,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,oBAAoB,GACrB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,eAAe,EACf,gBAAgB,GACjB,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EACV,iBAAiB,GAClB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,iBAAiB,GAClB,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"consumer.d.ts","sourceRoot":"","sources":["../src/consumer.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kCAAkC,EAClC,qBAAqB,EACrB,uBAAuB,EACvB,uBAAuB,EACvB,mBAAmB,EACnB,uBAAuB,GACxB,MAAM,eAAe,CAAC;AACvB,YAAY,EACV,sBAAsB,EACtB,gBAAgB,EAChB,gBAAgB,EAChB,mBAAmB,EACnB,sBAAsB,EACtB,eAAe,EACf,oBAAoB,EACpB,sBAAsB,EACtB,0BAA0B,EAC1B,uBAAuB,EACvB,oBAAoB,EACpB,0BAA0B,EAC1B,uBAAuB,EACvB,0BAA0B,EAC1B,2BAA2B,EAC3B,4BAA4B,EAC5B,uBAAuB,EACvB,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,yBAAyB,GAC1B,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EACV,kBAAkB,EAClB,0BAA0B,EAC1B,wBAAwB,EACxB,wBAAwB,EACxB,mBAAmB,EACnB,mBAAmB,EACnB,0BAA0B,EAC1B,2BAA2B,EAC3B,0BAA0B,GAC3B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,qBAAqB,EACrB,iBAAiB,GAClB,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,oBAAoB,GACrB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,eAAe,EACf,gBAAgB,GACjB,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EACV,iBAAiB,GAClB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,iBAAiB,GAClB,MAAM,cAAc,CAAC"}