@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 +39 -0
- package/dist/cli/index.js +310 -19
- package/dist/commands/backup.d.ts +17 -2
- package/dist/commands/backup.d.ts.map +1 -1
- package/dist/commands/ssh.d.ts.map +1 -1
- package/dist/consumer.d.ts +2 -2
- package/dist/consumer.d.ts.map +1 -1
- package/dist/consumer.js +222 -7
- package/dist/index.js +318 -16
- package/dist/manifests.d.ts +8 -0
- package/dist/manifests.d.ts.map +1 -1
- package/dist/mcp/index.js +314 -20
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/topology.d.ts +52 -0
- package/dist/topology.d.ts.map +1 -1
- package/package.json +1 -1
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
|
|
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 (
|
|
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 ?
|
|
2312
|
+
return env2 ? readEnv2(env2.name) ?? null : null;
|
|
2313
2313
|
}
|
|
2314
2314
|
function getStorageMode() {
|
|
2315
|
-
const mode = normalizeStorageMode(
|
|
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
|
|
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
|
|
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} ${
|
|
8193
|
+
return remoteCommand ? `ssh ${resolved.target} ${shellQuote(remoteCommand)}` : `ssh ${resolved.target}`;
|
|
7936
8194
|
}
|
|
7937
8195
|
|
|
7938
8196
|
// src/remote.ts
|
|
7939
|
-
function
|
|
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 ${
|
|
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
|
|
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 =
|
|
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
|
|
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 ${
|
|
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
|
|
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 =
|
|
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=${
|
|
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").
|
|
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
|
|
3
|
-
export declare
|
|
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;
|
|
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;
|
|
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"}
|
package/dist/consumer.d.ts
CHANGED
|
@@ -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";
|
package/dist/consumer.d.ts.map
CHANGED
|
@@ -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,
|
|
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"}
|