@hasna/machines 0.0.18 → 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/dist/consumer.js CHANGED
@@ -4212,6 +4212,7 @@ var machineSchema = exports_external.object({
4212
4212
  workspacePath: exports_external.string(),
4213
4213
  bunPath: exports_external.string().optional(),
4214
4214
  tags: exports_external.array(exports_external.string()).optional(),
4215
+ metadata: exports_external.record(exports_external.unknown()).optional(),
4215
4216
  packages: exports_external.array(packageSchema).optional(),
4216
4217
  apps: exports_external.array(appSchema).optional(),
4217
4218
  files: exports_external.array(fileSchema).optional()
@@ -4514,7 +4515,8 @@ function discoverMachineTopology(options = {}) {
4514
4515
  topology: true,
4515
4516
  compatibility: true,
4516
4517
  route_resolution: true,
4517
- cli_json_fallback: true
4518
+ cli_json_fallback: true,
4519
+ workspace_path_mapping: true
4518
4520
  },
4519
4521
  generated_at: now.toISOString(),
4520
4522
  local_machine_id: localMachineId,
@@ -4639,6 +4641,215 @@ function resolveMachineRoute(machineId, options = {}) {
4639
4641
  warnings
4640
4642
  };
4641
4643
  }
4644
+ function isRecord(value) {
4645
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
4646
+ }
4647
+ function metadataString(metadata, keys) {
4648
+ for (const key of keys) {
4649
+ const value = metadata[key];
4650
+ if (typeof value === "string" && value.trim())
4651
+ return value.trim();
4652
+ }
4653
+ return null;
4654
+ }
4655
+ function metadataBoolean(metadata, keys) {
4656
+ for (const key of keys) {
4657
+ const value = metadata[key];
4658
+ if (typeof value === "boolean")
4659
+ return value;
4660
+ }
4661
+ return null;
4662
+ }
4663
+ function metadataStringArray(metadata, keys) {
4664
+ for (const key of keys) {
4665
+ const value = metadata[key];
4666
+ if (Array.isArray(value))
4667
+ return value.filter((entry) => typeof entry === "string");
4668
+ }
4669
+ return [];
4670
+ }
4671
+ function readMappedPath(input) {
4672
+ for (const containerName of input.containers) {
4673
+ const container = input.metadata[containerName];
4674
+ if (!isRecord(container))
4675
+ continue;
4676
+ for (const key of input.keys) {
4677
+ const value = container[key];
4678
+ if (typeof value === "string" && value.trim())
4679
+ return value.trim();
4680
+ if (isRecord(value)) {
4681
+ const path = metadataString(value, ["path", "root", "workspacePath", "workspace_path"]);
4682
+ if (path)
4683
+ return path;
4684
+ }
4685
+ }
4686
+ }
4687
+ return null;
4688
+ }
4689
+ function trimTrailingSlash(value) {
4690
+ return value.replace(/\/+$/, "");
4691
+ }
4692
+ function joinPath(left, right) {
4693
+ return `${trimTrailingSlash(left)}/${right.replace(/^\/+/, "")}`;
4694
+ }
4695
+ function inferRepoRoot(workspaceRoot, repoName) {
4696
+ if (!workspaceRoot || !repoName)
4697
+ return null;
4698
+ const root = trimTrailingSlash(workspaceRoot);
4699
+ if (root.endsWith(`/${repoName}`) || root === repoName)
4700
+ return root;
4701
+ if (root.endsWith("/workspace") || root.endsWith("/Workspace")) {
4702
+ return joinPath(root, `hasna/opensource/${repoName}`);
4703
+ }
4704
+ return joinPath(root, repoName);
4705
+ }
4706
+ function projectPathFromMetadata(metadata, projectId, repoName) {
4707
+ const keys = [projectId, repoName].filter((value) => Boolean(value));
4708
+ return readMappedPath({
4709
+ metadata,
4710
+ containers: ["workspace_paths", "workspacePaths", "repo_paths", "repoPaths", "project_paths", "projectPaths", "projects"],
4711
+ keys
4712
+ });
4713
+ }
4714
+ function openFilesPathFromMetadata(metadata, projectId, repoName) {
4715
+ const direct = metadataString(metadata, ["open_files_root", "openFilesRoot", "open_files_path", "openFilesPath"]);
4716
+ if (direct)
4717
+ return direct;
4718
+ const keys = [projectId, repoName, "open-files", "open_files", "default"].filter((value) => Boolean(value));
4719
+ return readMappedPath({
4720
+ metadata,
4721
+ containers: ["open_files_roots", "openFilesRoots", "open_files_paths", "openFilesPaths"],
4722
+ keys
4723
+ });
4724
+ }
4725
+ function trustStatus(machine) {
4726
+ if (!machine)
4727
+ return "unknown";
4728
+ const explicit = metadataString(machine.metadata, ["trust_status", "trustStatus"]);
4729
+ if (explicit === "trusted" || explicit === "untrusted" || explicit === "unknown")
4730
+ return explicit;
4731
+ const trusted = metadataBoolean(machine.metadata, ["trusted", "syncTrusted", "sync_trusted"]);
4732
+ if (trusted === true)
4733
+ return "trusted";
4734
+ if (trusted === false)
4735
+ return "untrusted";
4736
+ if (machine.route_hints.some((hint) => hint.kind === "local"))
4737
+ return "trusted";
4738
+ if (machine.tags.includes("trusted"))
4739
+ return "trusted";
4740
+ return "unknown";
4741
+ }
4742
+ function authStatus(machine) {
4743
+ if (!machine)
4744
+ return "unknown";
4745
+ const explicit = metadataString(machine.metadata, ["auth_status", "authStatus"]);
4746
+ if (explicit === "authenticated" || explicit === "unauthenticated" || explicit === "unknown")
4747
+ return explicit;
4748
+ const authenticated = metadataBoolean(machine.metadata, ["authenticated", "sshAuthorized", "ssh_authorized"]);
4749
+ if (authenticated === true)
4750
+ return "authenticated";
4751
+ if (authenticated === false)
4752
+ return "unauthenticated";
4753
+ if (machine.route_hints.some((hint) => hint.kind === "local"))
4754
+ return "authenticated";
4755
+ return "unknown";
4756
+ }
4757
+ function primaryMachine(machine, projectId, primaryMachineId) {
4758
+ if (!machine)
4759
+ return false;
4760
+ if (primaryMachineId)
4761
+ return machine.machine_id === primaryMachineId;
4762
+ if (metadataBoolean(machine.metadata, ["primary", "primary_machine", "primaryMachine"]) === true)
4763
+ return true;
4764
+ const primaryProjects = metadataStringArray(machine.metadata, ["primary_projects", "primaryProjects"]);
4765
+ if (primaryProjects.includes(projectId))
4766
+ return true;
4767
+ return machine.tags.includes("primary");
4768
+ }
4769
+ function metadataKeysForDiagnostics(metadata) {
4770
+ return Object.keys(metadata).filter((key) => !/(secret|token|key|password|credential)/i.test(key)).sort();
4771
+ }
4772
+ function resolveMachineWorkspace(options) {
4773
+ const topology = options.topology ?? discoverMachineTopology(options);
4774
+ const warnings = [...topology.warnings];
4775
+ const { machine, matchedBy } = findRouteMachine(topology, options.machineId);
4776
+ const generatedAt = (options.now ?? new Date).toISOString();
4777
+ const repoName = options.repoName ?? options.projectId;
4778
+ const openFilesRepoName = options.openFilesRepoName ?? "open-files";
4779
+ if (!machine) {
4780
+ warnings.push(`machine_not_found:${options.machineId}`);
4781
+ return {
4782
+ schema_version: MACHINES_CONSUMER_CONTRACT_VERSION,
4783
+ package: topology.package,
4784
+ ok: false,
4785
+ requested_machine_id: options.machineId,
4786
+ machine_id: null,
4787
+ generated_at: generatedAt,
4788
+ project: { project_id: options.projectId, repo_name: repoName, canonical: Boolean(options.projectId) },
4789
+ machine: { current: false, primary: false, trust_status: "unknown", auth_status: "unknown" },
4790
+ paths: {
4791
+ workspace_root: { path: null, source: "unresolved" },
4792
+ project_root: { path: null, source: "unresolved" },
4793
+ open_files_root: { path: null, source: "unresolved" }
4794
+ },
4795
+ evidence: {
4796
+ topology: true,
4797
+ matched_by: matchedBy,
4798
+ manifest_declared: null,
4799
+ metadata_keys: []
4800
+ },
4801
+ warnings
4802
+ };
4803
+ }
4804
+ const metadata = machine.metadata;
4805
+ const workspaceRootPath = options.workspaceRoot ?? machine.workspace_path;
4806
+ const workspaceRootSource = options.workspaceRoot ? "argument" : machine.workspace_path ? "manifest" : "unresolved";
4807
+ const metadataProjectRoot = projectPathFromMetadata(metadata, options.projectId, repoName);
4808
+ const inferredProjectRoot = inferRepoRoot(workspaceRootPath, repoName);
4809
+ const projectRootPath = options.projectRoot ?? metadataProjectRoot ?? inferredProjectRoot;
4810
+ const projectRootSource = options.projectRoot ? "argument" : metadataProjectRoot ? "manifest_metadata" : inferredProjectRoot ? "inferred" : "unresolved";
4811
+ const metadataOpenFilesRoot = openFilesPathFromMetadata(metadata, options.projectId, openFilesRepoName);
4812
+ const inferredOpenFilesRoot = inferRepoRoot(workspaceRootPath, openFilesRepoName);
4813
+ const openFilesRootPath = options.openFilesRoot ?? metadataOpenFilesRoot ?? inferredOpenFilesRoot;
4814
+ const openFilesRootSource = options.openFilesRoot ? "argument" : metadataOpenFilesRoot ? "manifest_metadata" : inferredOpenFilesRoot ? "inferred" : "unresolved";
4815
+ if (projectRootSource === "inferred")
4816
+ warnings.push(`project_root_inferred:${options.projectId}`);
4817
+ if (openFilesRootSource === "inferred")
4818
+ warnings.push(`open_files_root_inferred:${options.projectId}`);
4819
+ if (!projectRootPath)
4820
+ warnings.push(`project_root_unresolved:${options.projectId}`);
4821
+ return {
4822
+ schema_version: MACHINES_CONSUMER_CONTRACT_VERSION,
4823
+ package: topology.package,
4824
+ ok: Boolean(projectRootPath),
4825
+ requested_machine_id: options.machineId,
4826
+ machine_id: machine.machine_id,
4827
+ generated_at: generatedAt,
4828
+ project: {
4829
+ project_id: options.projectId,
4830
+ repo_name: repoName,
4831
+ canonical: Boolean(options.projectId && repoName)
4832
+ },
4833
+ machine: {
4834
+ current: machine.machine_id === topology.local_machine_id,
4835
+ primary: primaryMachine(machine, options.projectId, options.primaryMachineId),
4836
+ trust_status: trustStatus(machine),
4837
+ auth_status: authStatus(machine)
4838
+ },
4839
+ paths: {
4840
+ workspace_root: { path: workspaceRootPath, source: workspaceRootSource },
4841
+ project_root: { path: projectRootPath, source: projectRootSource },
4842
+ open_files_root: { path: openFilesRootPath, source: openFilesRootSource }
4843
+ },
4844
+ evidence: {
4845
+ topology: true,
4846
+ matched_by: matchedBy,
4847
+ manifest_declared: machine.manifest_declared,
4848
+ metadata_keys: metadataKeysForDiagnostics(metadata)
4849
+ },
4850
+ warnings
4851
+ };
4852
+ }
4642
4853
  function getLocalMachineTopology(options = {}) {
4643
4854
  const topology = discoverMachineTopology(options);
4644
4855
  return topology.machines.find((machine) => machine.machine_id === topology.local_machine_id) ?? {
@@ -4974,6 +5185,7 @@ function checkMachineCompatibility(options = {}) {
4974
5185
  export {
4975
5186
  runMachineCommand,
4976
5187
  resolveSshTarget,
5188
+ resolveMachineWorkspace,
4977
5189
  resolveMachineRoute,
4978
5190
  resolveMachineCommand,
4979
5191
  getPackageVersion,
package/dist/index.js CHANGED
@@ -11008,6 +11008,7 @@ var machineSchema = exports_external.object({
11008
11008
  workspacePath: exports_external.string(),
11009
11009
  bunPath: exports_external.string().optional(),
11010
11010
  tags: exports_external.array(exports_external.string()).optional(),
11011
+ metadata: exports_external.record(exports_external.unknown()).optional(),
11011
11012
  packages: exports_external.array(packageSchema).optional(),
11012
11013
  apps: exports_external.array(appSchema).optional(),
11013
11014
  files: exports_external.array(fileSchema).optional()
@@ -11314,7 +11315,8 @@ function discoverMachineTopology(options = {}) {
11314
11315
  topology: true,
11315
11316
  compatibility: true,
11316
11317
  route_resolution: true,
11317
- cli_json_fallback: true
11318
+ cli_json_fallback: true,
11319
+ workspace_path_mapping: true
11318
11320
  },
11319
11321
  generated_at: now.toISOString(),
11320
11322
  local_machine_id: localMachineId,
@@ -11439,6 +11441,215 @@ function resolveMachineRoute(machineId, options = {}) {
11439
11441
  warnings
11440
11442
  };
11441
11443
  }
11444
+ function isRecord(value) {
11445
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
11446
+ }
11447
+ function metadataString(metadata, keys) {
11448
+ for (const key of keys) {
11449
+ const value = metadata[key];
11450
+ if (typeof value === "string" && value.trim())
11451
+ return value.trim();
11452
+ }
11453
+ return null;
11454
+ }
11455
+ function metadataBoolean(metadata, keys) {
11456
+ for (const key of keys) {
11457
+ const value = metadata[key];
11458
+ if (typeof value === "boolean")
11459
+ return value;
11460
+ }
11461
+ return null;
11462
+ }
11463
+ function metadataStringArray(metadata, keys) {
11464
+ for (const key of keys) {
11465
+ const value = metadata[key];
11466
+ if (Array.isArray(value))
11467
+ return value.filter((entry) => typeof entry === "string");
11468
+ }
11469
+ return [];
11470
+ }
11471
+ function readMappedPath(input) {
11472
+ for (const containerName of input.containers) {
11473
+ const container = input.metadata[containerName];
11474
+ if (!isRecord(container))
11475
+ continue;
11476
+ for (const key of input.keys) {
11477
+ const value = container[key];
11478
+ if (typeof value === "string" && value.trim())
11479
+ return value.trim();
11480
+ if (isRecord(value)) {
11481
+ const path = metadataString(value, ["path", "root", "workspacePath", "workspace_path"]);
11482
+ if (path)
11483
+ return path;
11484
+ }
11485
+ }
11486
+ }
11487
+ return null;
11488
+ }
11489
+ function trimTrailingSlash(value) {
11490
+ return value.replace(/\/+$/, "");
11491
+ }
11492
+ function joinPath(left, right) {
11493
+ return `${trimTrailingSlash(left)}/${right.replace(/^\/+/, "")}`;
11494
+ }
11495
+ function inferRepoRoot(workspaceRoot, repoName) {
11496
+ if (!workspaceRoot || !repoName)
11497
+ return null;
11498
+ const root = trimTrailingSlash(workspaceRoot);
11499
+ if (root.endsWith(`/${repoName}`) || root === repoName)
11500
+ return root;
11501
+ if (root.endsWith("/workspace") || root.endsWith("/Workspace")) {
11502
+ return joinPath(root, `hasna/opensource/${repoName}`);
11503
+ }
11504
+ return joinPath(root, repoName);
11505
+ }
11506
+ function projectPathFromMetadata(metadata, projectId, repoName) {
11507
+ const keys = [projectId, repoName].filter((value) => Boolean(value));
11508
+ return readMappedPath({
11509
+ metadata,
11510
+ containers: ["workspace_paths", "workspacePaths", "repo_paths", "repoPaths", "project_paths", "projectPaths", "projects"],
11511
+ keys
11512
+ });
11513
+ }
11514
+ function openFilesPathFromMetadata(metadata, projectId, repoName) {
11515
+ const direct = metadataString(metadata, ["open_files_root", "openFilesRoot", "open_files_path", "openFilesPath"]);
11516
+ if (direct)
11517
+ return direct;
11518
+ const keys = [projectId, repoName, "open-files", "open_files", "default"].filter((value) => Boolean(value));
11519
+ return readMappedPath({
11520
+ metadata,
11521
+ containers: ["open_files_roots", "openFilesRoots", "open_files_paths", "openFilesPaths"],
11522
+ keys
11523
+ });
11524
+ }
11525
+ function trustStatus(machine) {
11526
+ if (!machine)
11527
+ return "unknown";
11528
+ const explicit = metadataString(machine.metadata, ["trust_status", "trustStatus"]);
11529
+ if (explicit === "trusted" || explicit === "untrusted" || explicit === "unknown")
11530
+ return explicit;
11531
+ const trusted = metadataBoolean(machine.metadata, ["trusted", "syncTrusted", "sync_trusted"]);
11532
+ if (trusted === true)
11533
+ return "trusted";
11534
+ if (trusted === false)
11535
+ return "untrusted";
11536
+ if (machine.route_hints.some((hint) => hint.kind === "local"))
11537
+ return "trusted";
11538
+ if (machine.tags.includes("trusted"))
11539
+ return "trusted";
11540
+ return "unknown";
11541
+ }
11542
+ function authStatus(machine) {
11543
+ if (!machine)
11544
+ return "unknown";
11545
+ const explicit = metadataString(machine.metadata, ["auth_status", "authStatus"]);
11546
+ if (explicit === "authenticated" || explicit === "unauthenticated" || explicit === "unknown")
11547
+ return explicit;
11548
+ const authenticated = metadataBoolean(machine.metadata, ["authenticated", "sshAuthorized", "ssh_authorized"]);
11549
+ if (authenticated === true)
11550
+ return "authenticated";
11551
+ if (authenticated === false)
11552
+ return "unauthenticated";
11553
+ if (machine.route_hints.some((hint) => hint.kind === "local"))
11554
+ return "authenticated";
11555
+ return "unknown";
11556
+ }
11557
+ function primaryMachine(machine, projectId, primaryMachineId) {
11558
+ if (!machine)
11559
+ return false;
11560
+ if (primaryMachineId)
11561
+ return machine.machine_id === primaryMachineId;
11562
+ if (metadataBoolean(machine.metadata, ["primary", "primary_machine", "primaryMachine"]) === true)
11563
+ return true;
11564
+ const primaryProjects = metadataStringArray(machine.metadata, ["primary_projects", "primaryProjects"]);
11565
+ if (primaryProjects.includes(projectId))
11566
+ return true;
11567
+ return machine.tags.includes("primary");
11568
+ }
11569
+ function metadataKeysForDiagnostics(metadata) {
11570
+ return Object.keys(metadata).filter((key) => !/(secret|token|key|password|credential)/i.test(key)).sort();
11571
+ }
11572
+ function resolveMachineWorkspace(options) {
11573
+ const topology = options.topology ?? discoverMachineTopology(options);
11574
+ const warnings = [...topology.warnings];
11575
+ const { machine, matchedBy } = findRouteMachine(topology, options.machineId);
11576
+ const generatedAt = (options.now ?? new Date).toISOString();
11577
+ const repoName = options.repoName ?? options.projectId;
11578
+ const openFilesRepoName = options.openFilesRepoName ?? "open-files";
11579
+ if (!machine) {
11580
+ warnings.push(`machine_not_found:${options.machineId}`);
11581
+ return {
11582
+ schema_version: MACHINES_CONSUMER_CONTRACT_VERSION,
11583
+ package: topology.package,
11584
+ ok: false,
11585
+ requested_machine_id: options.machineId,
11586
+ machine_id: null,
11587
+ generated_at: generatedAt,
11588
+ project: { project_id: options.projectId, repo_name: repoName, canonical: Boolean(options.projectId) },
11589
+ machine: { current: false, primary: false, trust_status: "unknown", auth_status: "unknown" },
11590
+ paths: {
11591
+ workspace_root: { path: null, source: "unresolved" },
11592
+ project_root: { path: null, source: "unresolved" },
11593
+ open_files_root: { path: null, source: "unresolved" }
11594
+ },
11595
+ evidence: {
11596
+ topology: true,
11597
+ matched_by: matchedBy,
11598
+ manifest_declared: null,
11599
+ metadata_keys: []
11600
+ },
11601
+ warnings
11602
+ };
11603
+ }
11604
+ const metadata = machine.metadata;
11605
+ const workspaceRootPath = options.workspaceRoot ?? machine.workspace_path;
11606
+ const workspaceRootSource = options.workspaceRoot ? "argument" : machine.workspace_path ? "manifest" : "unresolved";
11607
+ const metadataProjectRoot = projectPathFromMetadata(metadata, options.projectId, repoName);
11608
+ const inferredProjectRoot = inferRepoRoot(workspaceRootPath, repoName);
11609
+ const projectRootPath = options.projectRoot ?? metadataProjectRoot ?? inferredProjectRoot;
11610
+ const projectRootSource = options.projectRoot ? "argument" : metadataProjectRoot ? "manifest_metadata" : inferredProjectRoot ? "inferred" : "unresolved";
11611
+ const metadataOpenFilesRoot = openFilesPathFromMetadata(metadata, options.projectId, openFilesRepoName);
11612
+ const inferredOpenFilesRoot = inferRepoRoot(workspaceRootPath, openFilesRepoName);
11613
+ const openFilesRootPath = options.openFilesRoot ?? metadataOpenFilesRoot ?? inferredOpenFilesRoot;
11614
+ const openFilesRootSource = options.openFilesRoot ? "argument" : metadataOpenFilesRoot ? "manifest_metadata" : inferredOpenFilesRoot ? "inferred" : "unresolved";
11615
+ if (projectRootSource === "inferred")
11616
+ warnings.push(`project_root_inferred:${options.projectId}`);
11617
+ if (openFilesRootSource === "inferred")
11618
+ warnings.push(`open_files_root_inferred:${options.projectId}`);
11619
+ if (!projectRootPath)
11620
+ warnings.push(`project_root_unresolved:${options.projectId}`);
11621
+ return {
11622
+ schema_version: MACHINES_CONSUMER_CONTRACT_VERSION,
11623
+ package: topology.package,
11624
+ ok: Boolean(projectRootPath),
11625
+ requested_machine_id: options.machineId,
11626
+ machine_id: machine.machine_id,
11627
+ generated_at: generatedAt,
11628
+ project: {
11629
+ project_id: options.projectId,
11630
+ repo_name: repoName,
11631
+ canonical: Boolean(options.projectId && repoName)
11632
+ },
11633
+ machine: {
11634
+ current: machine.machine_id === topology.local_machine_id,
11635
+ primary: primaryMachine(machine, options.projectId, options.primaryMachineId),
11636
+ trust_status: trustStatus(machine),
11637
+ auth_status: authStatus(machine)
11638
+ },
11639
+ paths: {
11640
+ workspace_root: { path: workspaceRootPath, source: workspaceRootSource },
11641
+ project_root: { path: projectRootPath, source: projectRootSource },
11642
+ open_files_root: { path: openFilesRootPath, source: openFilesRootSource }
11643
+ },
11644
+ evidence: {
11645
+ topology: true,
11646
+ matched_by: matchedBy,
11647
+ manifest_declared: machine.manifest_declared,
11648
+ metadata_keys: metadataKeysForDiagnostics(metadata)
11649
+ },
11650
+ warnings
11651
+ };
11652
+ }
11442
11653
  function getLocalMachineTopology(options = {}) {
11443
11654
  const topology = discoverMachineTopology(options);
11444
11655
  return topology.machines.find((machine) => machine.machine_id === topology.local_machine_id) ?? {
@@ -11796,9 +12007,52 @@ function getAgentStatus(machineId = getLocalMachineId()) {
11796
12007
  // src/commands/backup.ts
11797
12008
  import { homedir as homedir2 } from "os";
11798
12009
  import { join as join3 } from "path";
12010
+ var MACHINES_BACKUP_BUCKET_ENV = "HASNA_MACHINES_S3_BUCKET";
12011
+ var MACHINES_BACKUP_BUCKET_FALLBACK_ENV = "MACHINES_S3_BUCKET";
12012
+ var MACHINES_BACKUP_PREFIX_ENV = "HASNA_MACHINES_S3_PREFIX";
12013
+ var MACHINES_BACKUP_PREFIX_FALLBACK_ENV = "MACHINES_S3_PREFIX";
12014
+ var DEFAULT_BACKUP_PREFIX = "machines";
11799
12015
  function quote(value) {
11800
12016
  return `'${value.replace(/'/g, `'\\''`)}'`;
11801
12017
  }
12018
+ function readEnv2(name) {
12019
+ const value = process.env[name]?.trim();
12020
+ return value || undefined;
12021
+ }
12022
+ function readBackupBucketEnv() {
12023
+ const primary = readEnv2(MACHINES_BACKUP_BUCKET_ENV);
12024
+ if (primary)
12025
+ return { bucket: primary, bucketSource: MACHINES_BACKUP_BUCKET_ENV };
12026
+ const fallback = readEnv2(MACHINES_BACKUP_BUCKET_FALLBACK_ENV);
12027
+ if (fallback)
12028
+ return { bucket: fallback, bucketSource: MACHINES_BACKUP_BUCKET_FALLBACK_ENV };
12029
+ return null;
12030
+ }
12031
+ function readBackupPrefixEnv() {
12032
+ const primary = readEnv2(MACHINES_BACKUP_PREFIX_ENV);
12033
+ if (primary)
12034
+ return { prefix: primary, prefixSource: MACHINES_BACKUP_PREFIX_ENV };
12035
+ const fallback = readEnv2(MACHINES_BACKUP_PREFIX_FALLBACK_ENV);
12036
+ if (fallback)
12037
+ return { prefix: fallback, prefixSource: MACHINES_BACKUP_PREFIX_FALLBACK_ENV };
12038
+ return null;
12039
+ }
12040
+ function resolveBackupTarget(options = {}) {
12041
+ const explicitBucket = options.bucket?.trim();
12042
+ const envBucket = explicitBucket ? null : readBackupBucketEnv();
12043
+ const bucket = explicitBucket || envBucket?.bucket;
12044
+ if (!bucket) {
12045
+ throw new Error(`Missing S3 backup bucket. Pass --bucket or set ${MACHINES_BACKUP_BUCKET_ENV} or ${MACHINES_BACKUP_BUCKET_FALLBACK_ENV}.`);
12046
+ }
12047
+ const explicitPrefix = options.prefix?.trim();
12048
+ const envPrefix = explicitPrefix ? null : readBackupPrefixEnv();
12049
+ return {
12050
+ bucket,
12051
+ prefix: explicitPrefix || envPrefix?.prefix || DEFAULT_BACKUP_PREFIX,
12052
+ bucketSource: explicitBucket ? "argument" : envBucket.bucketSource,
12053
+ prefixSource: explicitPrefix ? "argument" : envPrefix?.prefixSource || "default"
12054
+ };
12055
+ }
11802
12056
  function defaultBackupSources() {
11803
12057
  const home = homedir2();
11804
12058
  return [
@@ -11807,7 +12061,8 @@ function defaultBackupSources() {
11807
12061
  join3(home, ".secrets")
11808
12062
  ];
11809
12063
  }
11810
- function buildBackupPlan(bucket, prefix = "machines") {
12064
+ function buildBackupPlan(bucket, prefix) {
12065
+ const target = resolveBackupTarget({ bucket, prefix });
11811
12066
  const archivePath = join3(homedir2(), ".hasna", "machines", "backup.tgz");
11812
12067
  const sources = defaultBackupSources();
11813
12068
  const steps = [
@@ -11820,7 +12075,7 @@ function buildBackupPlan(bucket, prefix = "machines") {
11820
12075
  {
11821
12076
  id: "backup-upload",
11822
12077
  title: "Upload archive to S3",
11823
- command: `aws s3 cp ${quote(archivePath)} s3://${bucket}/${prefix}/$(hostname)-backup.tgz`,
12078
+ command: `aws s3 cp ${quote(archivePath)} s3://${target.bucket}/${target.prefix}/$(hostname)-backup.tgz`,
11824
12079
  manager: "custom"
11825
12080
  }
11826
12081
  ];
@@ -11831,7 +12086,7 @@ function buildBackupPlan(bucket, prefix = "machines") {
11831
12086
  executed: 0
11832
12087
  };
11833
12088
  }
11834
- function runBackup(bucket, prefix = "machines", options = {}) {
12089
+ function runBackup(bucket, prefix, options = {}) {
11835
12090
  const plan = buildBackupPlan(bucket, prefix);
11836
12091
  if (!options.apply)
11837
12092
  return plan;
@@ -22228,6 +22483,7 @@ var MACHINE_MCP_TOOL_NAMES = [
22228
22483
  "machines_install_claude_preview",
22229
22484
  "machines_install_claude_apply",
22230
22485
  "machines_route_resolve",
22486
+ "machines_workspace_resolve",
22231
22487
  "machines_ssh_resolve",
22232
22488
  "machines_ports",
22233
22489
  "machines_backup_preview",
@@ -22341,14 +22597,50 @@ function createMcpServer(version2) {
22341
22597
  server.tool("machines_route_resolve", "Resolve the best route for a machine using manifest, heartbeat, SSH, LAN, and Tailscale topology.", { machine_id: exports_external.string().describe("Machine identifier"), include_tailscale: exports_external.boolean().optional().describe("Whether to probe tailscale status --json") }, async ({ machine_id, include_tailscale }) => ({
22342
22598
  content: [{ type: "text", text: JSON.stringify(resolveMachineRoute(machine_id, { includeTailscale: include_tailscale !== false }), null, 2) }]
22343
22599
  }));
22600
+ server.tool("machines_workspace_resolve", "Resolve sync-safe repo and open-files roots for an open-* consumer.", {
22601
+ machine_id: exports_external.string().describe("Machine identifier"),
22602
+ project_id: exports_external.string().describe("Canonical project id"),
22603
+ repo_name: exports_external.string().optional().describe("Repository name; defaults to project id"),
22604
+ open_files_repo_name: exports_external.string().optional().describe("Open-files repository name"),
22605
+ primary_machine_id: exports_external.string().optional().describe("Primary machine id for this project"),
22606
+ workspace_root: exports_external.string().optional().describe("Override the machine workspace root"),
22607
+ project_root: exports_external.string().optional().describe("Override the resolved project root"),
22608
+ open_files_root: exports_external.string().optional().describe("Override the resolved open-files root"),
22609
+ include_tailscale: exports_external.boolean().optional().describe("Whether to probe tailscale status --json")
22610
+ }, async ({
22611
+ machine_id,
22612
+ project_id,
22613
+ repo_name,
22614
+ open_files_repo_name,
22615
+ primary_machine_id,
22616
+ workspace_root,
22617
+ project_root,
22618
+ open_files_root,
22619
+ include_tailscale
22620
+ }) => ({
22621
+ content: [{
22622
+ type: "text",
22623
+ text: JSON.stringify(resolveMachineWorkspace({
22624
+ machineId: machine_id,
22625
+ projectId: project_id,
22626
+ repoName: repo_name,
22627
+ openFilesRepoName: open_files_repo_name,
22628
+ primaryMachineId: primary_machine_id,
22629
+ workspaceRoot: workspace_root,
22630
+ projectRoot: project_root,
22631
+ openFilesRoot: open_files_root,
22632
+ includeTailscale: include_tailscale !== false
22633
+ }), null, 2)
22634
+ }]
22635
+ }));
22344
22636
  server.tool("machines_ssh_resolve", "Resolve the best SSH route for a machine.", { machine_id: exports_external.string().describe("Machine identifier"), remote_command: exports_external.string().optional().describe("Optional remote command") }, async ({ machine_id, remote_command }) => ({
22345
22637
  content: [{ type: "text", text: JSON.stringify({ resolved: resolveMachineRoute(machine_id), command: buildSshCommand(machine_id, remote_command) }, null, 2) }]
22346
22638
  }));
22347
22639
  server.tool("machines_ports", "List listening ports on a machine.", { machine_id: exports_external.string().optional().describe("Machine identifier") }, async ({ machine_id }) => ({
22348
22640
  content: [{ type: "text", text: JSON.stringify(listPorts(machine_id), null, 2) }]
22349
22641
  }));
22350
- server.tool("machines_backup_preview", "Preview backup steps for the current machine.", { bucket: exports_external.string().describe("S3 bucket name"), prefix: exports_external.string().optional().describe("S3 key prefix") }, async ({ bucket, prefix }) => ({ content: [{ type: "text", text: JSON.stringify(buildBackupPlan(bucket, prefix), null, 2) }] }));
22351
- server.tool("machines_backup_apply", "Execute backup steps for the current machine.", { bucket: exports_external.string().describe("S3 bucket name"), prefix: exports_external.string().optional().describe("S3 key prefix"), yes: exports_external.boolean().describe("Confirmation flag for execution") }, async ({ bucket, prefix, yes }) => ({ content: [{ type: "text", text: JSON.stringify(runBackup(bucket, prefix, { apply: true, yes }), null, 2) }] }));
22642
+ server.tool("machines_backup_preview", "Preview backup steps for the current machine.", { bucket: exports_external.string().optional().describe("S3 bucket name; defaults to HASNA_MACHINES_S3_BUCKET or MACHINES_S3_BUCKET"), prefix: exports_external.string().optional().describe("S3 key prefix; defaults to HASNA_MACHINES_S3_PREFIX, MACHINES_S3_PREFIX, or machines") }, async ({ bucket, prefix }) => ({ content: [{ type: "text", text: JSON.stringify(buildBackupPlan(bucket, prefix), null, 2) }] }));
22643
+ server.tool("machines_backup_apply", "Execute backup steps for the current machine.", { bucket: exports_external.string().optional().describe("S3 bucket name; defaults to HASNA_MACHINES_S3_BUCKET or MACHINES_S3_BUCKET"), prefix: exports_external.string().optional().describe("S3 key prefix; defaults to HASNA_MACHINES_S3_PREFIX, MACHINES_S3_PREFIX, or machines"), yes: exports_external.boolean().describe("Confirmation flag for execution") }, async ({ bucket, prefix, yes }) => ({ content: [{ type: "text", text: JSON.stringify(runBackup(bucket, prefix, { apply: true, yes }), null, 2) }] }));
22352
22644
  server.tool("machines_cert_preview", "Preview mkcert steps for one or more domains.", { domains: exports_external.array(exports_external.string()).describe("Domains to issue certificates for") }, async ({ domains }) => ({ content: [{ type: "text", text: JSON.stringify(buildCertPlan(domains), null, 2) }] }));
22353
22645
  server.tool("machines_cert_apply", "Execute mkcert steps for one or more domains.", { domains: exports_external.array(exports_external.string()).describe("Domains to issue certificates for"), yes: exports_external.boolean().describe("Confirmation flag for execution") }, async ({ domains, yes }) => ({ content: [{ type: "text", text: JSON.stringify(runCertPlan(domains, { apply: true, yes }), null, 2) }] }));
22354
22646
  server.tool("machines_dns_add", "Add or replace a local domain mapping.", { domain: exports_external.string().describe("Domain name"), port: exports_external.number().describe("Target port"), target_host: exports_external.string().optional().describe("Target host") }, async ({ domain, port, target_host }) => ({ content: [{ type: "text", text: JSON.stringify(addDomainMapping(domain, port, target_host), null, 2) }] }));
@@ -22407,7 +22699,9 @@ export {
22407
22699
  runAppsInstall,
22408
22700
  resolveTables,
22409
22701
  resolveSshTarget,
22702
+ resolveMachineWorkspace,
22410
22703
  resolveMachineRoute,
22704
+ resolveBackupTarget,
22411
22705
  renderDomainMapping,
22412
22706
  renderDashboardHtml,
22413
22707
  removeNotificationChannel,
@@ -22493,5 +22787,10 @@ export {
22493
22787
  MACHINES_STORAGE_ENV,
22494
22788
  MACHINES_PACKAGE_NAME,
22495
22789
  MACHINES_CONSUMER_CONTRACT_VERSION,
22790
+ MACHINES_BACKUP_PREFIX_FALLBACK_ENV,
22791
+ MACHINES_BACKUP_PREFIX_ENV,
22792
+ MACHINES_BACKUP_BUCKET_FALLBACK_ENV,
22793
+ MACHINES_BACKUP_BUCKET_ENV,
22794
+ DEFAULT_BACKUP_PREFIX,
22496
22795
  CROSSREFS_KEY
22497
22796
  };