@hasna/machines 0.0.34 → 0.0.36

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
@@ -36,9 +36,18 @@ machines manifest validate
36
36
  machines manifest list
37
37
  ```
38
38
 
39
+ Public packages should keep private fleet state behind an opaque source/ref
40
+ boundary. `HASNA_MACHINES_PRIVATE_MANIFEST_REF` (or
41
+ `MACHINES_PRIVATE_MANIFEST_REF`) may point at a private backend, but
42
+ open-machines only reports the redacted ref and falls back to the local
43
+ `machines.json` unless a caller supplies a manifest adapter. The adapter
44
+ contract is backend-agnostic and lives in the package root exports; it does not
45
+ pull in secrets managers, storage SDKs, or org-specific fleet internals.
46
+
39
47
  ## Provision and reconcile
40
48
 
41
49
  ```bash
50
+ machines setup --machine linux-dev-01
42
51
  machines setup --machine linux-dev-01 --json
43
52
  machines setup --machine linux-dev-01 --apply --yes
44
53
  machines sync --machine linux-dev-01 --json
@@ -47,6 +56,25 @@ machines doctor --machine linux-dev-01
47
56
  machines self-test
48
57
  ```
49
58
 
59
+ `machines setup` is a dry-run plan by default. The generated playbook favors
60
+ idempotent operations (`mkdir -p`, command-existence guards, package-manager
61
+ installs) and only executes when both `--apply` and `--yes` are provided.
62
+ The default plan also adds update-check/download settings without enabling
63
+ automatic OS installation: Linux uses apt periodic download-only settings, and
64
+ macOS uses `softwareupdate`/`defaults` with `AutomaticallyInstallMacOSUpdates`
65
+ left disabled.
66
+ `doctor --json` includes public-safe source/ref diagnostics plus optional
67
+ adapter hook results for secrets, configs, monitors, repos, MCPs, and shield
68
+ checks. When no adapter is configured, those checks report a skipped fallback
69
+ instead of importing private dependencies.
70
+ It also reports noninteractive sudo readiness, SSH certificate support, and
71
+ GitHub App secret-reference readiness without printing credentials or private
72
+ keys.
73
+
74
+ Apple device management belongs in the private deployment layer. The public
75
+ setup plan can report enrollment status with `profiles status -type enrollment`,
76
+ but it does not enroll devices, install profiles, or publish team identifiers.
77
+
50
78
  ## Topology SDK
51
79
 
52
80
  `@hasna/machines` exposes a compact consumer SDK for other open-core packages
@@ -151,6 +179,11 @@ machines defaults to
151
179
  `machines/screen-sharing/screen-<machine>-vnc-password`, or the namespace set in
152
180
  `HASNA_MACHINES_SCREEN_SECRET_NAMESPACE`. The user comes from the manifest
153
181
  (`metadata.user`) when present, or `--user`.
182
+
183
+ For GitHub automation, prefer GitHub App installation tokens over personal user
184
+ tokens. Public manifests and docs should store only opaque secret references
185
+ for the app id/private key material; private adapters or `open-secrets` should
186
+ resolve those references at runtime.
154
187
  `screen-credentials` verifies the resolved user and secret key for a machine or
155
188
  the full fleet without printing secret values.
156
189
 
package/dist/cli/index.js CHANGED
@@ -7092,6 +7092,104 @@ var coerce = {
7092
7092
  var NEVER = INVALID;
7093
7093
  // src/manifests.ts
7094
7094
  init_paths();
7095
+
7096
+ // src/redaction.ts
7097
+ var REDACTED_VALUE = "[redacted]";
7098
+ var SENSITIVE_KEY_PATTERN = /(password|passwd|token|credential|private[_-]?key|privateKey|api[_-]?key|github.*key|pem|secret)/i;
7099
+ var SECRET_REFERENCE_KEY_PATTERN = /(secret(ref(erence)?|key)?|secretRef|secretKey)$/i;
7100
+ var SENSITIVE_VALUE_PATTERNS = [
7101
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----/,
7102
+ /\bghp_[A-Za-z0-9_]{20,}\b/,
7103
+ /\bgithub_pat_[A-Za-z0-9_]{20,}\b/,
7104
+ /\bxox[baprs]-[A-Za-z0-9-]{20,}\b/,
7105
+ /\bAKIA[0-9A-Z]{16}\b/,
7106
+ /\bsk-[A-Za-z0-9_-]{20,}\b/
7107
+ ];
7108
+ function isSensitiveKey(key) {
7109
+ return SENSITIVE_KEY_PATTERN.test(key);
7110
+ }
7111
+ function isSecretReferenceKey(key) {
7112
+ return SECRET_REFERENCE_KEY_PATTERN.test(key);
7113
+ }
7114
+ function looksSensitiveString(value) {
7115
+ return SENSITIVE_VALUE_PATTERNS.some((pattern) => pattern.test(value));
7116
+ }
7117
+ function isRecord(value) {
7118
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
7119
+ }
7120
+ function redactPath(value) {
7121
+ return value.replace(/\/home\/[^/\s]+/g, "/home/<user>").replace(/\/Users\/[^/\s]+/g, "/Users/<user>").replace(/[A-Za-z]:\\Users\\[^\\\s]+/g, "C:\\Users\\<user>");
7122
+ }
7123
+ function redactPrivateRef(value) {
7124
+ const trimmed = value.trim();
7125
+ const scheme = trimmed.match(/^([a-z][a-z0-9+.-]*:\/\/)/i);
7126
+ if (scheme)
7127
+ return `${scheme[1]}<redacted>`;
7128
+ const colon = trimmed.match(/^([a-z][a-z0-9+.-]*):/i);
7129
+ if (colon)
7130
+ return `${colon[1]}:<redacted>`;
7131
+ return "<private-manifest-ref:redacted>";
7132
+ }
7133
+ function redactIdentifier(value) {
7134
+ return value.replace(/[^a-zA-Z0-9_.-]/g, "_").slice(0, 80) || "adapter";
7135
+ }
7136
+ function redactSensitiveValue(value, key = "") {
7137
+ if (typeof value === "string") {
7138
+ if (isSensitiveKey(key) && !(isSecretReferenceKey(key) && !looksSensitiveString(value))) {
7139
+ return REDACTED_VALUE;
7140
+ }
7141
+ if (looksSensitiveString(value))
7142
+ return REDACTED_VALUE;
7143
+ return redactPath(value);
7144
+ }
7145
+ if (Array.isArray(value)) {
7146
+ return value.map((entry) => redactSensitiveValue(entry, key));
7147
+ }
7148
+ if (isRecord(value)) {
7149
+ const redacted = {};
7150
+ for (const [entryKey, entryValue] of Object.entries(value)) {
7151
+ redacted[entryKey] = redactSensitiveValue(entryValue, entryKey);
7152
+ }
7153
+ return redacted;
7154
+ }
7155
+ return value;
7156
+ }
7157
+ function publicMetadataKeys(metadata) {
7158
+ return Object.keys(metadata ?? {}).filter((key) => !isSensitiveKey(key)).sort();
7159
+ }
7160
+ function redactMetadata(metadata) {
7161
+ return redactSensitiveValue(metadata ?? {});
7162
+ }
7163
+ function redactManifestForDiagnostics(machine) {
7164
+ const metadata = redactMetadata(machine.metadata);
7165
+ for (const key of ["user", "username", "login"]) {
7166
+ if (typeof metadata[key] === "string")
7167
+ metadata[key] = REDACTED_VALUE;
7168
+ }
7169
+ return {
7170
+ id: machine.id,
7171
+ hostname: machine.hostname ? REDACTED_VALUE : undefined,
7172
+ sshAddress: machine.sshAddress ? REDACTED_VALUE : undefined,
7173
+ tailscaleName: machine.tailscaleName ? REDACTED_VALUE : undefined,
7174
+ platform: machine.platform,
7175
+ connection: machine.connection,
7176
+ workspacePath: redactPath(machine.workspacePath),
7177
+ bunPath: machine.bunPath ? redactPath(machine.bunPath) : undefined,
7178
+ tags: machine.tags ?? [],
7179
+ metadata,
7180
+ packages: machine.packages?.map((pkg) => ({ ...pkg })),
7181
+ apps: machine.apps?.map((app) => ({ ...app })),
7182
+ files: machine.files?.map((file) => ({
7183
+ ...file,
7184
+ source: redactPath(file.source),
7185
+ target: redactPath(file.target)
7186
+ }))
7187
+ };
7188
+ }
7189
+
7190
+ // src/manifests.ts
7191
+ var PRIVATE_MANIFEST_REF_ENV = "HASNA_MACHINES_PRIVATE_MANIFEST_REF";
7192
+ var PRIVATE_MANIFEST_BACKEND_ENV = "HASNA_MACHINES_PRIVATE_MANIFEST_BACKEND";
7095
7193
  var packageSchema = exports_external.object({
7096
7194
  name: exports_external.string(),
7097
7195
  manager: exports_external.enum(["bun", "brew", "apt", "custom"]).optional(),
@@ -7144,6 +7242,42 @@ function normalizePlatform() {
7144
7242
  function normalizeMachines(machines) {
7145
7243
  return [...machines].sort((left, right) => left.id.localeCompare(right.id));
7146
7244
  }
7245
+ function inferPrivateBackend(rawRef, explicitBackend) {
7246
+ if (explicitBackend?.trim())
7247
+ return explicitBackend.trim();
7248
+ const scheme = rawRef.trim().match(/^([a-z][a-z0-9+.-]*)(?::\/\/|:)/i);
7249
+ return scheme?.[1] ?? null;
7250
+ }
7251
+ function fileSourceRef(path) {
7252
+ return {
7253
+ kind: "file",
7254
+ ref: redactPath(path),
7255
+ backend: "file",
7256
+ private: false,
7257
+ publicSafe: true
7258
+ };
7259
+ }
7260
+ function privateSourceRef(rawRef, backend) {
7261
+ return {
7262
+ kind: "private-ref",
7263
+ ref: redactPrivateRef(rawRef),
7264
+ backend: inferPrivateBackend(rawRef, backend),
7265
+ private: true,
7266
+ publicSafe: true
7267
+ };
7268
+ }
7269
+ function privateRefFromOptions(options) {
7270
+ const env2 = options.env ?? process.env;
7271
+ return options.privateRef?.trim() || env2[PRIVATE_MANIFEST_REF_ENV]?.trim() || env2["MACHINES_PRIVATE_MANIFEST_REF"]?.trim() || null;
7272
+ }
7273
+ function getManifestSourceRef(options = {}) {
7274
+ const rawPrivateRef = privateRefFromOptions(options);
7275
+ if (rawPrivateRef) {
7276
+ const env2 = options.env ?? process.env;
7277
+ return privateSourceRef(rawPrivateRef, options.privateBackend ?? env2[PRIVATE_MANIFEST_BACKEND_ENV]);
7278
+ }
7279
+ return fileSourceRef(options.path ?? getManifestPath());
7280
+ }
7147
7281
  function getDefaultManifest() {
7148
7282
  return {
7149
7283
  version: 1,
@@ -7158,6 +7292,53 @@ function readManifest(path = getManifestPath()) {
7158
7292
  const raw = JSON.parse(readFileSync2(path, "utf8"));
7159
7293
  return fleetSchema.parse(raw);
7160
7294
  }
7295
+ function readManifestWithSource(options = {}) {
7296
+ const path = options.path ?? getManifestPath();
7297
+ const source = getManifestSourceRef(options);
7298
+ const warnings = [];
7299
+ if (source.kind === "private-ref") {
7300
+ const rawRef = privateRefFromOptions(options);
7301
+ if (rawRef && options.adapter) {
7302
+ try {
7303
+ const manifest2 = options.adapter.readManifest({ source, rawRef });
7304
+ if (manifest2) {
7305
+ return {
7306
+ manifest: fleetSchema.parse(manifest2),
7307
+ info: {
7308
+ source,
7309
+ loadedFrom: "private-ref",
7310
+ warnings
7311
+ }
7312
+ };
7313
+ }
7314
+ warnings.push(`private_manifest_adapter_empty:${redactIdentifier(options.adapter.id)}`);
7315
+ } catch (error) {
7316
+ warnings.push(`private_manifest_adapter_failed:${redactIdentifier(options.adapter.id)}`);
7317
+ }
7318
+ } else {
7319
+ warnings.push("private_manifest_ref_without_adapter");
7320
+ }
7321
+ const fallbackSource = fileSourceRef(path);
7322
+ const manifest = readManifest(path);
7323
+ return {
7324
+ manifest,
7325
+ info: {
7326
+ source,
7327
+ loadedFrom: existsSync3(path) ? "fallback" : "default",
7328
+ fallbackSource,
7329
+ warnings
7330
+ }
7331
+ };
7332
+ }
7333
+ return {
7334
+ manifest: readManifest(path),
7335
+ info: {
7336
+ source,
7337
+ loadedFrom: existsSync3(path) ? "file" : "default",
7338
+ warnings
7339
+ }
7340
+ };
7341
+ }
7161
7342
  function validateManifest(path = getManifestPath()) {
7162
7343
  return readManifest(path);
7163
7344
  }
@@ -7444,7 +7625,7 @@ function buildEntry(input) {
7444
7625
  },
7445
7626
  route_hints: hints,
7446
7627
  tags: manifest?.tags ?? [],
7447
- metadata: manifest?.metadata ?? {}
7628
+ metadata: redactMetadata(manifest?.metadata)
7448
7629
  };
7449
7630
  }
7450
7631
  function discoverMachineTopology(options = {}) {
@@ -7682,7 +7863,7 @@ function resolveMachineRoute(machineId, options = {}) {
7682
7863
  warnings
7683
7864
  };
7684
7865
  }
7685
- function isRecord(value) {
7866
+ function isRecord2(value) {
7686
7867
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
7687
7868
  }
7688
7869
  function metadataString(metadata, keys) {
@@ -7712,13 +7893,13 @@ function metadataStringArray(metadata, keys) {
7712
7893
  function readMappedPath(input) {
7713
7894
  for (const containerName of input.containers) {
7714
7895
  const container = input.metadata[containerName];
7715
- if (!isRecord(container))
7896
+ if (!isRecord2(container))
7716
7897
  continue;
7717
7898
  for (const key of input.keys) {
7718
7899
  const value = container[key];
7719
7900
  if (typeof value === "string" && value.trim())
7720
7901
  return value.trim();
7721
- if (isRecord(value)) {
7902
+ if (isRecord2(value)) {
7722
7903
  const path = metadataString(value, ["path", "root", "workspacePath", "workspace_path"]);
7723
7904
  if (path)
7724
7905
  return path;
@@ -7972,7 +8153,7 @@ function primaryMachine(machine, projectId, primaryMachineId) {
7972
8153
  return machine.tags.includes("primary");
7973
8154
  }
7974
8155
  function metadataKeysForDiagnostics(metadata) {
7975
- return Object.keys(metadata).filter((key) => !/(secret|token|key|password|credential)/i.test(key)).sort();
8156
+ return publicMetadataKeys(metadata);
7976
8157
  }
7977
8158
  function resolveMachineWorkspace(options) {
7978
8159
  const now = options.now ?? new Date;
@@ -8205,6 +8386,13 @@ function buildBaseSteps(machine) {
8205
8386
  manager: "apt",
8206
8387
  privileged: true
8207
8388
  });
8389
+ steps.push({
8390
+ id: "linux-update-downloads",
8391
+ title: "Enable Linux package list refresh and download-only upgrades",
8392
+ command: `printf '%s\\n' 'APT::Periodic::Update-Package-Lists "1";' 'APT::Periodic::Download-Upgradeable-Packages "1";' 'APT::Periodic::Unattended-Upgrade "0";' | sudo tee /etc/apt/apt.conf.d/20auto-upgrades >/dev/null`,
8393
+ manager: "apt",
8394
+ privileged: true
8395
+ });
8208
8396
  } else if (machine.platform === "macos") {
8209
8397
  steps.push({
8210
8398
  id: "brew-base",
@@ -8218,7 +8406,26 @@ function buildBaseSteps(machine) {
8218
8406
  command: "brew install git coreutils",
8219
8407
  manager: "brew"
8220
8408
  });
8409
+ steps.push({
8410
+ id: "macos-update-downloads",
8411
+ title: "Enable macOS update checks and downloads without automatic install",
8412
+ command: "sudo softwareupdate --schedule on && sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate AutomaticCheckEnabled -bool true && sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate AutomaticDownload -int 1 && sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate AutomaticallyInstallMacOSUpdates -int 0",
8413
+ manager: "custom",
8414
+ privileged: true
8415
+ });
8416
+ steps.push({
8417
+ id: "macos-management-readiness",
8418
+ title: "Report Apple management readiness without enrolling devices",
8419
+ command: "profiles status -type enrollment 2>/dev/null || true",
8420
+ manager: "custom"
8421
+ });
8221
8422
  }
8423
+ steps.push({
8424
+ id: "github-app-auth-readiness",
8425
+ title: "Check GitHub CLI/App auth readiness without printing credentials",
8426
+ command: "command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1 || true",
8427
+ manager: "custom"
8428
+ });
8222
8429
  return steps;
8223
8430
  }
8224
8431
  function buildPackageSteps(machine) {
@@ -9578,20 +9785,20 @@ function getStatus() {
9578
9785
 
9579
9786
  // src/commands/workspace.ts
9580
9787
  init_paths();
9581
- function isRecord2(value) {
9788
+ function isRecord3(value) {
9582
9789
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
9583
9790
  }
9584
9791
  function cloneMetadata(metadata) {
9585
- return isRecord2(metadata) ? { ...metadata } : {};
9792
+ return isRecord3(metadata) ? { ...metadata } : {};
9586
9793
  }
9587
9794
  function mappedPath(metadata, field, key) {
9588
9795
  const container = metadata[field];
9589
- if (!isRecord2(container))
9796
+ if (!isRecord3(container))
9590
9797
  return null;
9591
9798
  const value = container[key];
9592
9799
  if (typeof value === "string" && value.trim())
9593
9800
  return value.trim();
9594
- if (isRecord2(value)) {
9801
+ if (isRecord3(value)) {
9595
9802
  const nested = value["path"] ?? value["root"] ?? value["workspacePath"] ?? value["workspace_path"];
9596
9803
  if (typeof nested === "string" && nested.trim())
9597
9804
  return nested.trim();
@@ -9600,7 +9807,7 @@ function mappedPath(metadata, field, key) {
9600
9807
  }
9601
9808
  function writeMappedPath(metadata, field, key, path) {
9602
9809
  const existing = metadata[field];
9603
- const container = isRecord2(existing) ? { ...existing } : {};
9810
+ const container = isRecord3(existing) ? { ...existing } : {};
9604
9811
  container[key] = path;
9605
9812
  metadata[field] = container;
9606
9813
  }
@@ -9970,12 +10177,28 @@ function checkMachineCompatibility(options = {}) {
9970
10177
 
9971
10178
  // src/commands/doctor.ts
9972
10179
  init_db();
9973
- function makeCheck2(id, status, summary, detail) {
9974
- return { id, status, summary, detail };
10180
+ var DOCTOR_OPTIONAL_ADAPTER_DOMAINS = ["secrets", "configs", "monitor", "repos", "mcps", "shield"];
10181
+ function makeCheck2(id, status, summary, detail, extra = {}) {
10182
+ const { data, ...rest } = extra;
10183
+ return {
10184
+ ...rest,
10185
+ id,
10186
+ status,
10187
+ summary,
10188
+ detail,
10189
+ data: data ? redactSensitiveValue(data) : undefined
10190
+ };
9975
10191
  }
9976
10192
  function parseKeyValueOutput(stdout) {
9977
- return Object.fromEntries(stdout.trim().split(`
9978
- `).map((line) => line.split("=")).filter((parts) => parts.length === 2).map(([key, value]) => [key, value]));
10193
+ const result = {};
10194
+ for (const line of stdout.trim().split(`
10195
+ `)) {
10196
+ const index = line.indexOf("=");
10197
+ if (index <= 0)
10198
+ continue;
10199
+ result[line.slice(0, index)] = line.slice(index + 1);
10200
+ }
10201
+ return result;
9979
10202
  }
9980
10203
  function buildDoctorCommand() {
9981
10204
  return [
@@ -9983,9 +10206,11 @@ function buildDoctorCommand() {
9983
10206
  'manifest_path="${HASNA_MACHINES_MANIFEST_PATH:-$data_dir/machines.json}"',
9984
10207
  'db_path="${HASNA_MACHINES_DB_PATH:-$data_dir/machines.db}"',
9985
10208
  'notifications_path="${HASNA_MACHINES_NOTIFICATIONS_PATH:-$data_dir/notifications.json}"',
10209
+ `printf 'data_dir=%s\\n' "$data_dir"`,
9986
10210
  `printf 'manifest_path=%s\\n' "$manifest_path"`,
9987
10211
  `printf 'db_path=%s\\n' "$db_path"`,
9988
10212
  `printf 'notifications_path=%s\\n' "$notifications_path"`,
10213
+ `printf 'data_dir_exists=%s\\n' "$(test -d "$data_dir" && printf yes || printf no)"`,
9989
10214
  `printf 'manifest_exists=%s\\n' "$(test -e "$manifest_path" && printf yes || printf no)"`,
9990
10215
  `printf 'db_exists=%s\\n' "$(test -e "$db_path" && printf yes || printf no)"`,
9991
10216
  `printf 'notifications_exists=%s\\n' "$(test -e "$notifications_path" && printf yes || printf no)"`,
@@ -9993,31 +10218,139 @@ function buildDoctorCommand() {
9993
10218
  `printf 'ssh=%s\\n' "$(command -v ssh >/dev/null 2>&1 && printf ok || printf missing)"`,
9994
10219
  `printf 'machines=%s\\n' "$(command -v machines 2>/dev/null || printf missing)"`,
9995
10220
  `printf 'machines_agent=%s\\n' "$(command -v machines-agent 2>/dev/null || printf missing)"`,
9996
- `printf 'machines_mcp=%s\\n' "$(command -v machines-mcp 2>/dev/null || printf missing)"`
10221
+ `printf 'machines_mcp=%s\\n' "$(command -v machines-mcp 2>/dev/null || printf missing)"`,
10222
+ `printf 'sudo_noninteractive=%s\\n' "$(sudo -n true >/dev/null 2>&1 && printf ok || printf unavailable)"`,
10223
+ `printf 'ssh_cert_support=%s\\n' "$(ssh -Q key-cert 2>/dev/null | grep -q 'ssh-ed25519-cert-v01@openssh.com' && printf ok || printf unavailable)"`,
10224
+ `printf 'gh_cli=%s\\n' "$(command -v gh 2>/dev/null || printf missing)"`,
10225
+ `printf 'gh_auth=%s\\n' "$(gh auth status >/dev/null 2>&1 && printf ok || printf unavailable)"`,
10226
+ "printf 'github_app_ref=%s\\n' \"$(test -n \\\"${HASNA_GITHUB_APP_ID:-}\\\" -a -n \\\"${HASNA_GITHUB_APP_PRIVATE_KEY_REF:-}\\\" && printf configured || printf missing)\""
9997
10227
  ].join("; ");
9998
10228
  }
9999
- function runDoctor(machineId = getLocalMachineId()) {
10000
- const manifest = readManifest();
10229
+ function fallbackAdapterCheck(domain) {
10230
+ return makeCheck2(`${domain}-adapter`, "ok", `Optional ${domain} adapter`, `No ${domain} adapter configured; skipped optional private integration check.`, {
10231
+ optional: true,
10232
+ source: "open-machines",
10233
+ data: { configured: false, fallback: true }
10234
+ });
10235
+ }
10236
+ function sanitizeAdapterCheck(check, domain, adapterId) {
10237
+ const safeAdapterId = redactIdentifier(adapterId);
10238
+ return makeCheck2(check.id.startsWith(`${domain}-`) || check.id.startsWith(`${domain}:`) ? check.id : `${domain}:${check.id}`, check.status, check.summary, String(redactSensitiveValue(check.detail)), {
10239
+ ...check,
10240
+ optional: check.optional ?? true,
10241
+ source: check.source ? String(redactSensitiveValue(check.source)) : `adapter:${safeAdapterId}`,
10242
+ data: check.data ? redactSensitiveValue(check.data) : undefined
10243
+ });
10244
+ }
10245
+ function runOptionalAdapterChecks(context, adapters) {
10246
+ const checks = [];
10247
+ for (const domain of DOCTOR_OPTIONAL_ADAPTER_DOMAINS) {
10248
+ const adapter2 = adapters.find((candidate) => candidate.checks?.[domain]);
10249
+ const hook = adapter2?.checks?.[domain];
10250
+ if (!adapter2 || !hook) {
10251
+ checks.push(fallbackAdapterCheck(domain));
10252
+ continue;
10253
+ }
10254
+ try {
10255
+ const result = hook(context);
10256
+ const domainChecks = Array.isArray(result) ? result : result ? [result] : [fallbackAdapterCheck(domain)];
10257
+ checks.push(...domainChecks.map((check) => sanitizeAdapterCheck(check, domain, adapter2.id)));
10258
+ } catch {
10259
+ const safeAdapterId = redactIdentifier(adapter2.id);
10260
+ checks.push(makeCheck2(`${domain}-adapter`, "warn", `Optional ${domain} adapter failed`, "Adapter failed; details are intentionally hidden to avoid leaking private refs or credentials.", {
10261
+ optional: true,
10262
+ source: `adapter:${safeAdapterId}`,
10263
+ data: { adapter: safeAdapterId, fallback: true }
10264
+ }));
10265
+ }
10266
+ }
10267
+ return checks;
10268
+ }
10269
+ function runDoctor(machineId = getLocalMachineId(), options = {}) {
10270
+ const now = options.now ?? new Date;
10271
+ const { manifest, info: manifestSource } = readManifestWithSource({ adapter: options.manifestAdapter ?? null });
10001
10272
  const commandChecks = runMachineCommand(machineId, buildDoctorCommand());
10002
10273
  const details = parseKeyValueOutput(commandChecks.stdout);
10003
10274
  const machineInManifest = manifest.machines.find((machine) => machine.id === machineId);
10275
+ const optionalAdapterChecks = options.includeOptionalAdapters === false ? [] : runOptionalAdapterChecks({
10276
+ machineId,
10277
+ manifest,
10278
+ manifestSource,
10279
+ commandDetails: details,
10280
+ now
10281
+ }, options.adapters ?? []);
10004
10282
  const checks = [
10005
- makeCheck2("manifest-entry", machineInManifest ? "ok" : "warn", machineInManifest ? "Machine exists in manifest" : "Machine missing from manifest", machineInManifest ? JSON.stringify(machineInManifest) : `No manifest entry for ${machineId}`),
10006
- makeCheck2("manifest-path", details["manifest_exists"] === "yes" ? "ok" : "warn", "Manifest path check", `${details["manifest_path"] || "unknown"} ${details["manifest_exists"] === "yes" ? "exists" : "missing"}`),
10007
- makeCheck2("db-path", details["db_exists"] === "yes" ? "ok" : "warn", "DB path check", `${details["db_path"] || "unknown"} ${details["db_exists"] === "yes" ? "exists" : "missing"}`),
10008
- makeCheck2("notifications-path", details["notifications_exists"] === "yes" ? "ok" : "warn", "Notifications path check", `${details["notifications_path"] || "unknown"} ${details["notifications_exists"] === "yes" ? "exists" : "missing"}`),
10283
+ makeCheck2("manifest-source", manifestSource.warnings.length > 0 ? "warn" : "ok", "Manifest source boundary", `${manifestSource.source.kind}:${manifestSource.source.ref} loaded from ${manifestSource.loadedFrom}`, {
10284
+ data: {
10285
+ source: manifestSource.source,
10286
+ loadedFrom: manifestSource.loadedFrom,
10287
+ fallbackSource: manifestSource.fallbackSource,
10288
+ warnings: manifestSource.warnings
10289
+ },
10290
+ remediation: manifestSource.warnings.length > 0 ? ["Provide a private manifest adapter or unset the private manifest ref to use the local manifest only."] : undefined
10291
+ }),
10292
+ makeCheck2("manifest-entry", machineInManifest ? "ok" : "warn", machineInManifest ? "Machine exists in manifest" : "Machine missing from manifest", machineInManifest ? JSON.stringify(redactManifestForDiagnostics(machineInManifest)) : `No manifest entry for ${machineId}`, {
10293
+ data: {
10294
+ declared: Boolean(machineInManifest),
10295
+ machine: machineInManifest ? redactManifestForDiagnostics(machineInManifest) : null
10296
+ }
10297
+ }),
10298
+ makeCheck2("data-dir", details["data_dir_exists"] === "yes" ? "ok" : "warn", "Data directory check", `${redactPath(details["data_dir"] || "unknown")} ${details["data_dir_exists"] === "yes" ? "exists" : "missing"}`, {
10299
+ data: {
10300
+ path: redactPath(details["data_dir"] || "unknown"),
10301
+ exists: details["data_dir_exists"] === "yes"
10302
+ }
10303
+ }),
10304
+ makeCheck2("manifest-path", details["manifest_exists"] === "yes" ? "ok" : "warn", "Manifest path check", `${redactPath(details["manifest_path"] || "unknown")} ${details["manifest_exists"] === "yes" ? "exists" : "missing"}`, {
10305
+ data: {
10306
+ path: redactPath(details["manifest_path"] || "unknown"),
10307
+ exists: details["manifest_exists"] === "yes"
10308
+ }
10309
+ }),
10310
+ makeCheck2("db-path", details["db_exists"] === "yes" ? "ok" : "warn", "DB path check", `${redactPath(details["db_path"] || "unknown")} ${details["db_exists"] === "yes" ? "exists" : "missing"}`, {
10311
+ data: {
10312
+ path: redactPath(details["db_path"] || "unknown"),
10313
+ exists: details["db_exists"] === "yes"
10314
+ }
10315
+ }),
10316
+ makeCheck2("notifications-path", details["notifications_exists"] === "yes" ? "ok" : "warn", "Notifications path check", `${redactPath(details["notifications_path"] || "unknown")} ${details["notifications_exists"] === "yes" ? "exists" : "missing"}`, {
10317
+ data: {
10318
+ path: redactPath(details["notifications_path"] || "unknown"),
10319
+ exists: details["notifications_exists"] === "yes"
10320
+ }
10321
+ }),
10009
10322
  makeCheck2("bun", details["bun"] && details["bun"] !== "missing" ? "ok" : "fail", "Bun availability", details["bun"] || "missing"),
10010
10323
  makeCheck2("machines-cli", details["machines"] && details["machines"] !== "missing" ? "ok" : "warn", "machines CLI availability", details["machines"] || "missing"),
10011
10324
  makeCheck2("machines-agent-cli", details["machines_agent"] && details["machines_agent"] !== "missing" ? "ok" : "warn", "machines-agent availability", details["machines_agent"] || "missing"),
10012
10325
  makeCheck2("machines-mcp-cli", details["machines_mcp"] && details["machines_mcp"] !== "missing" ? "ok" : "warn", "machines-mcp availability", details["machines_mcp"] || "missing"),
10013
- makeCheck2("ssh", details["ssh"] === "ok" ? "ok" : "warn", "SSH availability", details["ssh"] || "missing")
10326
+ makeCheck2("ssh", details["ssh"] === "ok" ? "ok" : "warn", "SSH availability", details["ssh"] || "missing"),
10327
+ makeCheck2("sudo-noninteractive", details["sudo_noninteractive"] === "ok" ? "ok" : "warn", "Noninteractive sudo availability", details["sudo_noninteractive"] === "ok" ? "sudo -n is available" : "sudo -n unavailable; setup may require user-provided approval or password handling.", {
10328
+ data: { available: details["sudo_noninteractive"] === "ok" },
10329
+ remediation: details["sudo_noninteractive"] === "ok" ? undefined : ["Configure explicit sudo policy or run setup commands manually; do not store sudo passwords in public manifests."]
10330
+ }),
10331
+ makeCheck2("ssh-cert-support", details["ssh_cert_support"] === "ok" ? "ok" : "warn", "SSH certificate support", details["ssh_cert_support"] === "ok" ? "OpenSSH reports ed25519 certificate support" : "OpenSSH certificate support not detected.", {
10332
+ data: { supported: details["ssh_cert_support"] === "ok" },
10333
+ remediation: details["ssh_cert_support"] === "ok" ? undefined : ["Install or update OpenSSH before adopting SSH certificate auth for this machine."]
10334
+ }),
10335
+ makeCheck2("github-app-auth", details["github_app_ref"] === "configured" ? "ok" : "warn", "GitHub App auth references", details["github_app_ref"] === "configured" ? "GitHub App id and private-key reference are configured" : "GitHub App id/private-key reference missing; use secret references, not user tokens or raw private keys.", {
10336
+ data: {
10337
+ gh_cli: details["gh_cli"] && details["gh_cli"] !== "missing",
10338
+ gh_auth: details["gh_auth"] === "ok",
10339
+ app_ref_configured: details["github_app_ref"] === "configured"
10340
+ },
10341
+ remediation: details["github_app_ref"] === "configured" ? undefined : ["Set HASNA_GITHUB_APP_ID plus HASNA_GITHUB_APP_PRIVATE_KEY_REF or provide an equivalent open-secrets adapter."]
10342
+ }),
10343
+ ...optionalAdapterChecks
10014
10344
  ];
10015
10345
  return {
10016
10346
  machineId,
10017
10347
  source: commandChecks.source,
10018
- manifestPath: details["manifest_path"],
10019
- dbPath: details["db_path"],
10020
- notificationsPath: details["notifications_path"],
10348
+ schemaVersion: 1,
10349
+ generatedAt: now.toISOString(),
10350
+ manifestSource,
10351
+ manifestPath: details["manifest_path"] ? redactPath(details["manifest_path"]) : undefined,
10352
+ dbPath: details["db_path"] ? redactPath(details["db_path"]) : undefined,
10353
+ notificationsPath: details["notifications_path"] ? redactPath(details["notifications_path"]) : undefined,
10021
10354
  checks
10022
10355
  };
10023
10356
  }
@@ -1,3 +1,24 @@
1
- import type { DoctorReport } from "../types.js";
2
- export declare function runDoctor(machineId?: string): DoctorReport;
1
+ import { type ManifestSourceAdapter } from "../manifests.js";
2
+ import type { DoctorCheck, DoctorReport, FleetManifest, ManifestLoadInfo } from "../types.js";
3
+ export declare const DOCTOR_OPTIONAL_ADAPTER_DOMAINS: readonly ["secrets", "configs", "monitor", "repos", "mcps", "shield"];
4
+ export type DoctorOptionalAdapterDomain = typeof DOCTOR_OPTIONAL_ADAPTER_DOMAINS[number];
5
+ export interface DoctorAdapterContext {
6
+ machineId: string;
7
+ manifest: FleetManifest;
8
+ manifestSource: ManifestLoadInfo;
9
+ commandDetails: Record<string, string>;
10
+ now: Date;
11
+ }
12
+ export type DoctorAdapterHook = (context: DoctorAdapterContext) => DoctorCheck | DoctorCheck[] | null | undefined;
13
+ export interface DoctorAdapter {
14
+ id: string;
15
+ checks?: Partial<Record<DoctorOptionalAdapterDomain, DoctorAdapterHook>>;
16
+ }
17
+ export interface DoctorOptions {
18
+ now?: Date;
19
+ manifestAdapter?: ManifestSourceAdapter | null;
20
+ adapters?: DoctorAdapter[];
21
+ includeOptionalAdapters?: boolean;
22
+ }
23
+ export declare function runDoctor(machineId?: string, options?: DoctorOptions): DoctorReport;
3
24
  //# sourceMappingURL=doctor.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAe,YAAY,EAAE,MAAM,aAAa,CAAC;AAqC7D,wBAAgB,SAAS,CAAC,SAAS,SAAsB,GAAG,YAAY,CAuEvE"}
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"AACA,OAAO,EAA0B,KAAK,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAGrF,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE9F,eAAO,MAAM,+BAA+B,uEAAwE,CAAC;AAErH,MAAM,MAAM,2BAA2B,GAAG,OAAO,+BAA+B,CAAC,MAAM,CAAC,CAAC;AAEzF,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,aAAa,CAAC;IACxB,cAAc,EAAE,gBAAgB,CAAC;IACjC,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,GAAG,EAAE,IAAI,CAAC;CACX;AAED,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,EAAE,oBAAoB,KAAK,WAAW,GAAG,WAAW,EAAE,GAAG,IAAI,GAAG,SAAS,CAAC;AAElH,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,2BAA2B,EAAE,iBAAiB,CAAC,CAAC,CAAC;CAC1E;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,CAAC,EAAE,IAAI,CAAC;IACX,eAAe,CAAC,EAAE,qBAAqB,GAAG,IAAI,CAAC;IAC/C,QAAQ,CAAC,EAAE,aAAa,EAAE,CAAC;IAC3B,uBAAuB,CAAC,EAAE,OAAO,CAAC;CACnC;AAuHD,wBAAgB,SAAS,CAAC,SAAS,SAAsB,EAAE,OAAO,GAAE,aAAkB,GAAG,YAAY,CAwLpG"}
@@ -1 +1 @@
1
- {"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../src/commands/setup.ts"],"names":[],"mappings":"AAGA,OAAO,EAAoD,KAAK,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAC3G,OAAO,KAAK,EAAmB,WAAW,EAAa,MAAM,aAAa,CAAC;AAsE3E,wBAAgB,cAAc,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,WAAW,CAyB9D;AAED,wBAAgB,QAAQ,CACtB,SAAS,CAAC,EAAE,MAAM,EAClB,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,GAAG,CAAC,EAAE,OAAO,CAAA;CAAO,EAChD,MAAM,GAAE,oBAAwC,GAC/C,WAAW,CAmCb"}
1
+ {"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../src/commands/setup.ts"],"names":[],"mappings":"AAGA,OAAO,EAAoD,KAAK,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAC3G,OAAO,KAAK,EAAmB,WAAW,EAAa,MAAM,aAAa,CAAC;AAiG3E,wBAAgB,cAAc,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,WAAW,CAyB9D;AAED,wBAAgB,QAAQ,CACtB,SAAS,CAAC,EAAE,MAAM,EAClB,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,GAAG,CAAC,EAAE,OAAO,CAAA;CAAO,EAChD,MAAM,GAAE,oBAAwC,GAC/C,WAAW,CAmCb"}