@hasna/machines 0.0.18 → 0.0.20

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
@@ -49,13 +49,20 @@ machines self-test
49
49
 
50
50
  ## Topology SDK
51
51
 
52
- `@hasna/machines` exposes a compact topology SDK for other open-core packages
53
- that need machine identity without importing CLI internals. Consumers that only
54
- need the stable app-to-app contract should import `@hasna/machines/consumer`:
52
+ `@hasna/machines` exposes a compact consumer SDK for other open-core packages
53
+ that need machine identity without importing CLI, MCP, agent, installer, or
54
+ storage-heavy internals. Consumers that only need the stable app-to-app contract
55
+ should import `@hasna/machines/consumer`:
55
56
 
56
57
  ```ts
57
- import { discoverMachineTopology, getLocalMachineTopology, resolveMachineRoute } from "@hasna/machines/consumer";
58
-
58
+ import {
59
+ MACHINES_CONSUMER_CONTRACT,
60
+ discoverMachineTopology,
61
+ getLocalMachineTopology,
62
+ resolveMachineRoute,
63
+ } from "@hasna/machines/consumer";
64
+
65
+ console.log(MACHINES_CONSUMER_CONTRACT.schema_version);
59
66
  const topology = discoverMachineTopology();
60
67
  const local = getLocalMachineTopology();
61
68
  const route = resolveMachineRoute("spark01");
@@ -67,9 +74,11 @@ The SDK merges manifest entries, local heartbeats, SSH route hints, and
67
74
  when present, and fall back to local probes or app-local machine registries when
68
75
  it is absent.
69
76
 
70
- Topology, route, and compatibility JSON include `schema_version`, package
71
- version metadata, and capability flags. The current consumer contract version is
72
- `1`.
77
+ Topology, route, workspace, and compatibility JSON include `schema_version`,
78
+ package version metadata, and capability flags. The current consumer contract
79
+ version is `1`; the exported `MACHINES_CONSUMER_CONTRACT` records the stable
80
+ entrypoint, envelope names, and stable exports used by downstream apps such as
81
+ `@hasna/knowledge`.
73
82
 
74
83
  CLI and MCP expose the same topology view:
75
84
 
@@ -79,6 +88,31 @@ machines topology --no-tailscale --json
79
88
  machines route --machine spark01 --json
80
89
  ```
81
90
 
91
+ Consumers that need repo paths can resolve trust-aware workspace mappings
92
+ without importing the full machines app:
93
+
94
+ ```ts
95
+ import { resolveMachineWorkspace } from "@hasna/machines/consumer";
96
+
97
+ const workspace = resolveMachineWorkspace({
98
+ machineId: "spark01",
99
+ projectId: "open-knowledge",
100
+ repoName: "open-knowledge",
101
+ });
102
+
103
+ console.log(workspace.paths.project_root.path);
104
+ console.log(workspace.paths.open_files_root.path);
105
+ ```
106
+
107
+ The resolver returns the machine workspace root, project repo root,
108
+ open-files root, current/primary flags, trust/auth status, and redacted
109
+ diagnostics. It uses explicit manifest metadata first and deterministic
110
+ workspace inference second; consumers can still pass manual overrides.
111
+
112
+ ```bash
113
+ machines workspace resolve --machine spark01 --project open-knowledge --repo open-knowledge --json
114
+ ```
115
+
82
116
  ## Compatibility SDK
83
117
 
84
118
  Open-core consumers can use `@hasna/machines` to preflight a peer before
@@ -131,6 +165,20 @@ Configure database storage with `HASNA_MACHINES_DATABASE_URL` or fallback
131
165
  `HASNA_MACHINES_STORAGE_MODE` or `MACHINES_STORAGE_MODE` with `local`,
132
166
  `hybrid`, or `remote`.
133
167
 
168
+ Machine backups are preview-only unless `--apply --yes` is passed. The backup
169
+ target can be explicit or environment-backed:
170
+
171
+ ```bash
172
+ machines backup --bucket fleet-backups --prefix machines --json
173
+ HASNA_MACHINES_S3_BUCKET=hasna-xyz-opensource-machines-prod machines backup --json
174
+ ```
175
+
176
+ `--bucket` and `--prefix` always win. Without `--bucket`, the backup command
177
+ uses `HASNA_MACHINES_S3_BUCKET` or fallback `MACHINES_S3_BUCKET`; prefix uses
178
+ `HASNA_MACHINES_S3_PREFIX`, fallback `MACHINES_S3_PREFIX`, or `machines`.
179
+ This keeps the open-source CLI local/self-hosted by default while allowing
180
+ Hasna deployments to route app-owned backups through canonical storage metadata.
181
+
134
182
  ## Applications and tooling
135
183
 
136
184
  ```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;
@@ -7583,6 +7628,16 @@ import { spawnSync } from "child_process";
7583
7628
  init_paths();
7584
7629
  var MACHINES_CONSUMER_CONTRACT_VERSION = 1;
7585
7630
  var MACHINES_PACKAGE_NAME = "@hasna/machines";
7631
+ var MACHINES_CONSUMER_CAPABILITIES = {
7632
+ topology: true,
7633
+ compatibility: true,
7634
+ route_resolution: true,
7635
+ cli_json_fallback: true,
7636
+ workspace_path_mapping: true
7637
+ };
7638
+ function getMachinesConsumerCapabilities() {
7639
+ return { ...MACHINES_CONSUMER_CAPABILITIES };
7640
+ }
7586
7641
  function normalizePlatform2(value = platform3()) {
7587
7642
  const normalized = value.toLowerCase();
7588
7643
  if (normalized === "darwin" || normalized === "macos")
@@ -7783,12 +7838,7 @@ function discoverMachineTopology(options = {}) {
7783
7838
  name: MACHINES_PACKAGE_NAME,
7784
7839
  version: getPackageVersion()
7785
7840
  },
7786
- capabilities: {
7787
- topology: true,
7788
- compatibility: true,
7789
- route_resolution: true,
7790
- cli_json_fallback: true
7791
- },
7841
+ capabilities: getMachinesConsumerCapabilities(),
7792
7842
  generated_at: now.toISOString(),
7793
7843
  local_machine_id: localMachineId,
7794
7844
  local_hostname: hostname3(),
@@ -7912,6 +7962,215 @@ function resolveMachineRoute(machineId, options = {}) {
7912
7962
  warnings
7913
7963
  };
7914
7964
  }
7965
+ function isRecord(value) {
7966
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
7967
+ }
7968
+ function metadataString(metadata, keys) {
7969
+ for (const key of keys) {
7970
+ const value = metadata[key];
7971
+ if (typeof value === "string" && value.trim())
7972
+ return value.trim();
7973
+ }
7974
+ return null;
7975
+ }
7976
+ function metadataBoolean(metadata, keys) {
7977
+ for (const key of keys) {
7978
+ const value = metadata[key];
7979
+ if (typeof value === "boolean")
7980
+ return value;
7981
+ }
7982
+ return null;
7983
+ }
7984
+ function metadataStringArray(metadata, keys) {
7985
+ for (const key of keys) {
7986
+ const value = metadata[key];
7987
+ if (Array.isArray(value))
7988
+ return value.filter((entry) => typeof entry === "string");
7989
+ }
7990
+ return [];
7991
+ }
7992
+ function readMappedPath(input) {
7993
+ for (const containerName of input.containers) {
7994
+ const container = input.metadata[containerName];
7995
+ if (!isRecord(container))
7996
+ continue;
7997
+ for (const key of input.keys) {
7998
+ const value = container[key];
7999
+ if (typeof value === "string" && value.trim())
8000
+ return value.trim();
8001
+ if (isRecord(value)) {
8002
+ const path = metadataString(value, ["path", "root", "workspacePath", "workspace_path"]);
8003
+ if (path)
8004
+ return path;
8005
+ }
8006
+ }
8007
+ }
8008
+ return null;
8009
+ }
8010
+ function trimTrailingSlash(value) {
8011
+ return value.replace(/\/+$/, "");
8012
+ }
8013
+ function joinPath(left, right) {
8014
+ return `${trimTrailingSlash(left)}/${right.replace(/^\/+/, "")}`;
8015
+ }
8016
+ function inferRepoRoot(workspaceRoot, repoName) {
8017
+ if (!workspaceRoot || !repoName)
8018
+ return null;
8019
+ const root = trimTrailingSlash(workspaceRoot);
8020
+ if (root.endsWith(`/${repoName}`) || root === repoName)
8021
+ return root;
8022
+ if (root.endsWith("/workspace") || root.endsWith("/Workspace")) {
8023
+ return joinPath(root, `hasna/opensource/${repoName}`);
8024
+ }
8025
+ return joinPath(root, repoName);
8026
+ }
8027
+ function projectPathFromMetadata(metadata, projectId, repoName) {
8028
+ const keys = [projectId, repoName].filter((value) => Boolean(value));
8029
+ return readMappedPath({
8030
+ metadata,
8031
+ containers: ["workspace_paths", "workspacePaths", "repo_paths", "repoPaths", "project_paths", "projectPaths", "projects"],
8032
+ keys
8033
+ });
8034
+ }
8035
+ function openFilesPathFromMetadata(metadata, projectId, repoName) {
8036
+ const direct = metadataString(metadata, ["open_files_root", "openFilesRoot", "open_files_path", "openFilesPath"]);
8037
+ if (direct)
8038
+ return direct;
8039
+ const keys = [projectId, repoName, "open-files", "open_files", "default"].filter((value) => Boolean(value));
8040
+ return readMappedPath({
8041
+ metadata,
8042
+ containers: ["open_files_roots", "openFilesRoots", "open_files_paths", "openFilesPaths"],
8043
+ keys
8044
+ });
8045
+ }
8046
+ function trustStatus(machine) {
8047
+ if (!machine)
8048
+ return "unknown";
8049
+ const explicit = metadataString(machine.metadata, ["trust_status", "trustStatus"]);
8050
+ if (explicit === "trusted" || explicit === "untrusted" || explicit === "unknown")
8051
+ return explicit;
8052
+ const trusted = metadataBoolean(machine.metadata, ["trusted", "syncTrusted", "sync_trusted"]);
8053
+ if (trusted === true)
8054
+ return "trusted";
8055
+ if (trusted === false)
8056
+ return "untrusted";
8057
+ if (machine.route_hints.some((hint) => hint.kind === "local"))
8058
+ return "trusted";
8059
+ if (machine.tags.includes("trusted"))
8060
+ return "trusted";
8061
+ return "unknown";
8062
+ }
8063
+ function authStatus(machine) {
8064
+ if (!machine)
8065
+ return "unknown";
8066
+ const explicit = metadataString(machine.metadata, ["auth_status", "authStatus"]);
8067
+ if (explicit === "authenticated" || explicit === "unauthenticated" || explicit === "unknown")
8068
+ return explicit;
8069
+ const authenticated = metadataBoolean(machine.metadata, ["authenticated", "sshAuthorized", "ssh_authorized"]);
8070
+ if (authenticated === true)
8071
+ return "authenticated";
8072
+ if (authenticated === false)
8073
+ return "unauthenticated";
8074
+ if (machine.route_hints.some((hint) => hint.kind === "local"))
8075
+ return "authenticated";
8076
+ return "unknown";
8077
+ }
8078
+ function primaryMachine(machine, projectId, primaryMachineId) {
8079
+ if (!machine)
8080
+ return false;
8081
+ if (primaryMachineId)
8082
+ return machine.machine_id === primaryMachineId;
8083
+ if (metadataBoolean(machine.metadata, ["primary", "primary_machine", "primaryMachine"]) === true)
8084
+ return true;
8085
+ const primaryProjects = metadataStringArray(machine.metadata, ["primary_projects", "primaryProjects"]);
8086
+ if (primaryProjects.includes(projectId))
8087
+ return true;
8088
+ return machine.tags.includes("primary");
8089
+ }
8090
+ function metadataKeysForDiagnostics(metadata) {
8091
+ return Object.keys(metadata).filter((key) => !/(secret|token|key|password|credential)/i.test(key)).sort();
8092
+ }
8093
+ function resolveMachineWorkspace(options) {
8094
+ const topology = options.topology ?? discoverMachineTopology(options);
8095
+ const warnings = [...topology.warnings];
8096
+ const { machine, matchedBy } = findRouteMachine(topology, options.machineId);
8097
+ const generatedAt = (options.now ?? new Date).toISOString();
8098
+ const repoName = options.repoName ?? options.projectId;
8099
+ const openFilesRepoName = options.openFilesRepoName ?? "open-files";
8100
+ if (!machine) {
8101
+ warnings.push(`machine_not_found:${options.machineId}`);
8102
+ return {
8103
+ schema_version: MACHINES_CONSUMER_CONTRACT_VERSION,
8104
+ package: topology.package,
8105
+ ok: false,
8106
+ requested_machine_id: options.machineId,
8107
+ machine_id: null,
8108
+ generated_at: generatedAt,
8109
+ project: { project_id: options.projectId, repo_name: repoName, canonical: Boolean(options.projectId) },
8110
+ machine: { current: false, primary: false, trust_status: "unknown", auth_status: "unknown" },
8111
+ paths: {
8112
+ workspace_root: { path: null, source: "unresolved" },
8113
+ project_root: { path: null, source: "unresolved" },
8114
+ open_files_root: { path: null, source: "unresolved" }
8115
+ },
8116
+ evidence: {
8117
+ topology: true,
8118
+ matched_by: matchedBy,
8119
+ manifest_declared: null,
8120
+ metadata_keys: []
8121
+ },
8122
+ warnings
8123
+ };
8124
+ }
8125
+ const metadata = machine.metadata;
8126
+ const workspaceRootPath = options.workspaceRoot ?? machine.workspace_path;
8127
+ const workspaceRootSource = options.workspaceRoot ? "argument" : machine.workspace_path ? "manifest" : "unresolved";
8128
+ const metadataProjectRoot = projectPathFromMetadata(metadata, options.projectId, repoName);
8129
+ const inferredProjectRoot = inferRepoRoot(workspaceRootPath, repoName);
8130
+ const projectRootPath = options.projectRoot ?? metadataProjectRoot ?? inferredProjectRoot;
8131
+ const projectRootSource = options.projectRoot ? "argument" : metadataProjectRoot ? "manifest_metadata" : inferredProjectRoot ? "inferred" : "unresolved";
8132
+ const metadataOpenFilesRoot = openFilesPathFromMetadata(metadata, options.projectId, openFilesRepoName);
8133
+ const inferredOpenFilesRoot = inferRepoRoot(workspaceRootPath, openFilesRepoName);
8134
+ const openFilesRootPath = options.openFilesRoot ?? metadataOpenFilesRoot ?? inferredOpenFilesRoot;
8135
+ const openFilesRootSource = options.openFilesRoot ? "argument" : metadataOpenFilesRoot ? "manifest_metadata" : inferredOpenFilesRoot ? "inferred" : "unresolved";
8136
+ if (projectRootSource === "inferred")
8137
+ warnings.push(`project_root_inferred:${options.projectId}`);
8138
+ if (openFilesRootSource === "inferred")
8139
+ warnings.push(`open_files_root_inferred:${options.projectId}`);
8140
+ if (!projectRootPath)
8141
+ warnings.push(`project_root_unresolved:${options.projectId}`);
8142
+ return {
8143
+ schema_version: MACHINES_CONSUMER_CONTRACT_VERSION,
8144
+ package: topology.package,
8145
+ ok: Boolean(projectRootPath),
8146
+ requested_machine_id: options.machineId,
8147
+ machine_id: machine.machine_id,
8148
+ generated_at: generatedAt,
8149
+ project: {
8150
+ project_id: options.projectId,
8151
+ repo_name: repoName,
8152
+ canonical: Boolean(options.projectId && repoName)
8153
+ },
8154
+ machine: {
8155
+ current: machine.machine_id === topology.local_machine_id,
8156
+ primary: primaryMachine(machine, options.projectId, options.primaryMachineId),
8157
+ trust_status: trustStatus(machine),
8158
+ auth_status: authStatus(machine)
8159
+ },
8160
+ paths: {
8161
+ workspace_root: { path: workspaceRootPath, source: workspaceRootSource },
8162
+ project_root: { path: projectRootPath, source: projectRootSource },
8163
+ open_files_root: { path: openFilesRootPath, source: openFilesRootSource }
8164
+ },
8165
+ evidence: {
8166
+ topology: true,
8167
+ matched_by: matchedBy,
8168
+ manifest_declared: machine.manifest_declared,
8169
+ metadata_keys: metadataKeysForDiagnostics(metadata)
8170
+ },
8171
+ warnings
8172
+ };
8173
+ }
7915
8174
 
7916
8175
  // src/commands/ssh.ts
7917
8176
  function shellQuote(value) {
@@ -9011,12 +9270,7 @@ function checkMachineCompatibility(options = {}) {
9011
9270
  name: MACHINES_PACKAGE_NAME,
9012
9271
  version: getPackageVersion()
9013
9272
  },
9014
- capabilities: {
9015
- topology: true,
9016
- compatibility: true,
9017
- route_resolution: true,
9018
- cli_json_fallback: true
9019
- },
9273
+ capabilities: getMachinesConsumerCapabilities(),
9020
9274
  ok: summary.fail === 0,
9021
9275
  machine_id: machineId,
9022
9276
  source: checks[0]?.source ?? "local",
@@ -10514,6 +10768,22 @@ function renderCompatibilityResult(result) {
10514
10768
  ].join(`
10515
10769
  `);
10516
10770
  }
10771
+ function renderWorkspaceResolution(result) {
10772
+ return renderKeyValueTable([
10773
+ ["machine", result.machine_id ?? result.requested_machine_id],
10774
+ ["ok", String(result.ok)],
10775
+ ["project", result.project.project_id],
10776
+ ["repo", result.project.repo_name ?? "unknown"],
10777
+ ["current", String(result.machine.current)],
10778
+ ["primary", String(result.machine.primary)],
10779
+ ["trust", result.machine.trust_status],
10780
+ ["auth", result.machine.auth_status],
10781
+ ["workspace root", `${result.paths.workspace_root.path ?? "unresolved"} (${result.paths.workspace_root.source})`],
10782
+ ["project root", `${result.paths.project_root.path ?? "unresolved"} (${result.paths.project_root.source})`],
10783
+ ["open-files root", `${result.paths.open_files_root.path ?? "unresolved"} (${result.paths.open_files_root.source})`],
10784
+ ["warnings", result.warnings.join(", ") || "none"]
10785
+ ]);
10786
+ }
10517
10787
  function renderFleetStatus(status) {
10518
10788
  return [
10519
10789
  renderKeyValueTable([
@@ -10664,11 +10934,28 @@ program2.command("compatibility").description("Check remote package, command, an
10664
10934
  if (!result.ok && !options.json)
10665
10935
  process.exitCode = 1;
10666
10936
  });
10937
+ var workspaceCommand = program2.command("workspace").description("Resolve sync-safe workspace paths for open-* consumers");
10938
+ 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) => {
10939
+ const result = resolveMachineWorkspace({
10940
+ machineId: options.machine,
10941
+ projectId: options.project,
10942
+ repoName: options.repo,
10943
+ openFilesRepoName: options.openFilesRepo,
10944
+ primaryMachineId: options.primaryMachine,
10945
+ workspaceRoot: options.workspaceRoot,
10946
+ projectRoot: options.projectRoot,
10947
+ openFilesRoot: options.openFilesRoot,
10948
+ includeTailscale: options.tailscale !== false
10949
+ });
10950
+ printJsonOrText(result, renderWorkspaceResolution(result), options.json);
10951
+ if (!result.ok && !options.json)
10952
+ process.exitCode = 1;
10953
+ });
10667
10954
  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) => {
10668
10955
  const result = diffMachines(options.left, options.right);
10669
10956
  console.log(JSON.stringify(result, null, 2));
10670
10957
  });
10671
- 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) => {
10958
+ 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) => {
10672
10959
  const result = options.apply ? runBackup(options.bucket, options.prefix, { apply: true, yes: options.yes }) : buildBackupPlan(options.bucket, options.prefix);
10673
10960
  console.log(JSON.stringify(result, null, 2));
10674
10961
  });
@@ -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":"compatibility.d.ts","sourceRoot":"","sources":["../src/compatibility.ts"],"names":[],"mappings":"AACA,OAAO,EAAqB,KAAK,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAC3E,OAAO,EAAE,kCAAkC,EAAyB,KAAK,uBAAuB,EAAE,KAAK,4BAA4B,EAAE,MAAM,eAAe,CAAC;AAG3J,MAAM,MAAM,mBAAmB,GAAG,IAAI,GAAG,MAAM,GAAG,MAAM,CAAC;AACzD,MAAM,MAAM,mBAAmB,GAAG,oBAAoB,CAAC,QAAQ,CAAC,CAAC;AAEjE,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,0BAA0B;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,SAAS,GAAG,SAAS,GAAG,WAAW,CAAC;IAC1C,MAAM,EAAE,mBAAmB,CAAC;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,mBAAmB,CAAC;CAC7B;AAED,MAAM,WAAW,0BAA0B;IACzC,cAAc,EAAE,OAAO,kCAAkC,CAAC;IAC1D,OAAO,EAAE,uBAAuB,CAAC;IACjC,YAAY,EAAE,4BAA4B,CAAC;IAC3C,EAAE,EAAE,OAAO,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,mBAAmB,CAAC;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,kBAAkB,EAAE,CAAC;IAC7B,OAAO,EAAE;QACP,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;CACH;AAED,MAAM,MAAM,0BAA0B,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,oBAAoB,CAAC;AAEtG,MAAM,WAAW,2BAA2B;IAC1C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,wBAAwB,EAAE,CAAC;IACtC,QAAQ,CAAC,EAAE,wBAAwB,EAAE,CAAC;IACtC,UAAU,CAAC,EAAE,0BAA0B,EAAE,CAAC;IAC1C,MAAM,CAAC,EAAE,0BAA0B,CAAC;IACpC,GAAG,CAAC,EAAE,IAAI,CAAC;CACZ;AAiQD,wBAAgB,yBAAyB,CAAC,OAAO,GAAE,2BAAgC,GAAG,0BAA0B,CAqC/G"}
1
+ {"version":3,"file":"compatibility.d.ts","sourceRoot":"","sources":["../src/compatibility.ts"],"names":[],"mappings":"AACA,OAAO,EAAqB,KAAK,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAC3E,OAAO,EAAE,kCAAkC,EAA0D,KAAK,uBAAuB,EAAE,KAAK,4BAA4B,EAAE,MAAM,eAAe,CAAC;AAG5L,MAAM,MAAM,mBAAmB,GAAG,IAAI,GAAG,MAAM,GAAG,MAAM,CAAC;AACzD,MAAM,MAAM,mBAAmB,GAAG,oBAAoB,CAAC,QAAQ,CAAC,CAAC;AAEjE,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,0BAA0B;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,SAAS,GAAG,SAAS,GAAG,WAAW,CAAC;IAC1C,MAAM,EAAE,mBAAmB,CAAC;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,mBAAmB,CAAC;CAC7B;AAED,MAAM,WAAW,0BAA0B;IACzC,cAAc,EAAE,OAAO,kCAAkC,CAAC;IAC1D,OAAO,EAAE,uBAAuB,CAAC;IACjC,YAAY,EAAE,4BAA4B,CAAC;IAC3C,EAAE,EAAE,OAAO,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,mBAAmB,CAAC;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,kBAAkB,EAAE,CAAC;IAC7B,OAAO,EAAE;QACP,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;CACH;AAED,MAAM,MAAM,0BAA0B,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,oBAAoB,CAAC;AAEtG,MAAM,WAAW,2BAA2B;IAC1C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,wBAAwB,EAAE,CAAC;IACtC,QAAQ,CAAC,EAAE,wBAAwB,EAAE,CAAC;IACtC,UAAU,CAAC,EAAE,0BAA0B,EAAE,CAAC;IAC1C,MAAM,CAAC,EAAE,0BAA0B,CAAC;IACpC,GAAG,CAAC,EAAE,IAAI,CAAC;CACZ;AAiQD,wBAAgB,yBAAyB,CAAC,OAAO,GAAE,2BAAgC,GAAG,0BAA0B,CAgC/G"}
@@ -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_CAPABILITIES, MACHINES_CONSUMER_CONTRACT, MACHINES_CONSUMER_ENTRYPOINT, MACHINES_CONSUMER_CONTRACT_VERSION, MACHINES_PACKAGE_NAME, discoverMachineTopology, getMachinesConsumerCapabilities, getLocalMachineTopology, resolveMachineRoute, resolveMachineWorkspace, } from "./topology.js";
2
+ export type { MachineRouteConfidence, MachineRouteHint, MachineRouteKind, MachineRouteOptions, MachineRouteResolution, MachineTopology, MachineTopologyEntry, MachineTopologyOptions, MachineWorkspaceAuthStatus, MachineWorkspaceOptions, MachineWorkspacePath, MachineWorkspacePathSource, MachineWorkspaceProject, MachineWorkspaceResolution, MachineWorkspaceTrustStatus, MachinesConsumerContract, MachinesConsumerEnvelope, 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,8BAA8B,EAC9B,0BAA0B,EAC1B,4BAA4B,EAC5B,kCAAkC,EAClC,qBAAqB,EACrB,uBAAuB,EACvB,+BAA+B,EAC/B,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,wBAAwB,EACxB,wBAAwB,EACxB,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"}