@hasna/machines 0.0.34 → 0.0.35

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,14 @@ 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
+ `doctor --json` includes public-safe source/ref diagnostics plus optional
63
+ adapter hook results for secrets, configs, monitors, repos, MCPs, and shield
64
+ checks. When no adapter is configured, those checks report a skipped fallback
65
+ instead of importing private dependencies.
66
+
50
67
  ## Topology SDK
51
68
 
52
69
  `@hasna/machines` exposes a compact consumer SDK for other open-core packages
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;
@@ -9578,20 +9759,20 @@ function getStatus() {
9578
9759
 
9579
9760
  // src/commands/workspace.ts
9580
9761
  init_paths();
9581
- function isRecord2(value) {
9762
+ function isRecord3(value) {
9582
9763
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
9583
9764
  }
9584
9765
  function cloneMetadata(metadata) {
9585
- return isRecord2(metadata) ? { ...metadata } : {};
9766
+ return isRecord3(metadata) ? { ...metadata } : {};
9586
9767
  }
9587
9768
  function mappedPath(metadata, field, key) {
9588
9769
  const container = metadata[field];
9589
- if (!isRecord2(container))
9770
+ if (!isRecord3(container))
9590
9771
  return null;
9591
9772
  const value = container[key];
9592
9773
  if (typeof value === "string" && value.trim())
9593
9774
  return value.trim();
9594
- if (isRecord2(value)) {
9775
+ if (isRecord3(value)) {
9595
9776
  const nested = value["path"] ?? value["root"] ?? value["workspacePath"] ?? value["workspace_path"];
9596
9777
  if (typeof nested === "string" && nested.trim())
9597
9778
  return nested.trim();
@@ -9600,7 +9781,7 @@ function mappedPath(metadata, field, key) {
9600
9781
  }
9601
9782
  function writeMappedPath(metadata, field, key, path) {
9602
9783
  const existing = metadata[field];
9603
- const container = isRecord2(existing) ? { ...existing } : {};
9784
+ const container = isRecord3(existing) ? { ...existing } : {};
9604
9785
  container[key] = path;
9605
9786
  metadata[field] = container;
9606
9787
  }
@@ -9970,12 +10151,28 @@ function checkMachineCompatibility(options = {}) {
9970
10151
 
9971
10152
  // src/commands/doctor.ts
9972
10153
  init_db();
9973
- function makeCheck2(id, status, summary, detail) {
9974
- return { id, status, summary, detail };
10154
+ var DOCTOR_OPTIONAL_ADAPTER_DOMAINS = ["secrets", "configs", "monitor", "repos", "mcps", "shield"];
10155
+ function makeCheck2(id, status, summary, detail, extra = {}) {
10156
+ const { data, ...rest } = extra;
10157
+ return {
10158
+ ...rest,
10159
+ id,
10160
+ status,
10161
+ summary,
10162
+ detail,
10163
+ data: data ? redactSensitiveValue(data) : undefined
10164
+ };
9975
10165
  }
9976
10166
  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]));
10167
+ const result = {};
10168
+ for (const line of stdout.trim().split(`
10169
+ `)) {
10170
+ const index = line.indexOf("=");
10171
+ if (index <= 0)
10172
+ continue;
10173
+ result[line.slice(0, index)] = line.slice(index + 1);
10174
+ }
10175
+ return result;
9979
10176
  }
9980
10177
  function buildDoctorCommand() {
9981
10178
  return [
@@ -9983,9 +10180,11 @@ function buildDoctorCommand() {
9983
10180
  'manifest_path="${HASNA_MACHINES_MANIFEST_PATH:-$data_dir/machines.json}"',
9984
10181
  'db_path="${HASNA_MACHINES_DB_PATH:-$data_dir/machines.db}"',
9985
10182
  'notifications_path="${HASNA_MACHINES_NOTIFICATIONS_PATH:-$data_dir/notifications.json}"',
10183
+ `printf 'data_dir=%s\\n' "$data_dir"`,
9986
10184
  `printf 'manifest_path=%s\\n' "$manifest_path"`,
9987
10185
  `printf 'db_path=%s\\n' "$db_path"`,
9988
10186
  `printf 'notifications_path=%s\\n' "$notifications_path"`,
10187
+ `printf 'data_dir_exists=%s\\n' "$(test -d "$data_dir" && printf yes || printf no)"`,
9989
10188
  `printf 'manifest_exists=%s\\n' "$(test -e "$manifest_path" && printf yes || printf no)"`,
9990
10189
  `printf 'db_exists=%s\\n' "$(test -e "$db_path" && printf yes || printf no)"`,
9991
10190
  `printf 'notifications_exists=%s\\n' "$(test -e "$notifications_path" && printf yes || printf no)"`,
@@ -9996,28 +10195,115 @@ function buildDoctorCommand() {
9996
10195
  `printf 'machines_mcp=%s\\n' "$(command -v machines-mcp 2>/dev/null || printf missing)"`
9997
10196
  ].join("; ");
9998
10197
  }
9999
- function runDoctor(machineId = getLocalMachineId()) {
10000
- const manifest = readManifest();
10198
+ function fallbackAdapterCheck(domain) {
10199
+ return makeCheck2(`${domain}-adapter`, "ok", `Optional ${domain} adapter`, `No ${domain} adapter configured; skipped optional private integration check.`, {
10200
+ optional: true,
10201
+ source: "open-machines",
10202
+ data: { configured: false, fallback: true }
10203
+ });
10204
+ }
10205
+ function sanitizeAdapterCheck(check, domain, adapterId) {
10206
+ const safeAdapterId = redactIdentifier(adapterId);
10207
+ return makeCheck2(check.id.startsWith(`${domain}-`) || check.id.startsWith(`${domain}:`) ? check.id : `${domain}:${check.id}`, check.status, check.summary, String(redactSensitiveValue(check.detail)), {
10208
+ ...check,
10209
+ optional: check.optional ?? true,
10210
+ source: check.source ? String(redactSensitiveValue(check.source)) : `adapter:${safeAdapterId}`,
10211
+ data: check.data ? redactSensitiveValue(check.data) : undefined
10212
+ });
10213
+ }
10214
+ function runOptionalAdapterChecks(context, adapters) {
10215
+ const checks = [];
10216
+ for (const domain of DOCTOR_OPTIONAL_ADAPTER_DOMAINS) {
10217
+ const adapter2 = adapters.find((candidate) => candidate.checks?.[domain]);
10218
+ const hook = adapter2?.checks?.[domain];
10219
+ if (!adapter2 || !hook) {
10220
+ checks.push(fallbackAdapterCheck(domain));
10221
+ continue;
10222
+ }
10223
+ try {
10224
+ const result = hook(context);
10225
+ const domainChecks = Array.isArray(result) ? result : result ? [result] : [fallbackAdapterCheck(domain)];
10226
+ checks.push(...domainChecks.map((check) => sanitizeAdapterCheck(check, domain, adapter2.id)));
10227
+ } catch {
10228
+ const safeAdapterId = redactIdentifier(adapter2.id);
10229
+ checks.push(makeCheck2(`${domain}-adapter`, "warn", `Optional ${domain} adapter failed`, "Adapter failed; details are intentionally hidden to avoid leaking private refs or credentials.", {
10230
+ optional: true,
10231
+ source: `adapter:${safeAdapterId}`,
10232
+ data: { adapter: safeAdapterId, fallback: true }
10233
+ }));
10234
+ }
10235
+ }
10236
+ return checks;
10237
+ }
10238
+ function runDoctor(machineId = getLocalMachineId(), options = {}) {
10239
+ const now = options.now ?? new Date;
10240
+ const { manifest, info: manifestSource } = readManifestWithSource({ adapter: options.manifestAdapter ?? null });
10001
10241
  const commandChecks = runMachineCommand(machineId, buildDoctorCommand());
10002
10242
  const details = parseKeyValueOutput(commandChecks.stdout);
10003
10243
  const machineInManifest = manifest.machines.find((machine) => machine.id === machineId);
10244
+ const optionalAdapterChecks = options.includeOptionalAdapters === false ? [] : runOptionalAdapterChecks({
10245
+ machineId,
10246
+ manifest,
10247
+ manifestSource,
10248
+ commandDetails: details,
10249
+ now
10250
+ }, options.adapters ?? []);
10004
10251
  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"}`),
10252
+ makeCheck2("manifest-source", manifestSource.warnings.length > 0 ? "warn" : "ok", "Manifest source boundary", `${manifestSource.source.kind}:${manifestSource.source.ref} loaded from ${manifestSource.loadedFrom}`, {
10253
+ data: {
10254
+ source: manifestSource.source,
10255
+ loadedFrom: manifestSource.loadedFrom,
10256
+ fallbackSource: manifestSource.fallbackSource,
10257
+ warnings: manifestSource.warnings
10258
+ },
10259
+ remediation: manifestSource.warnings.length > 0 ? ["Provide a private manifest adapter or unset the private manifest ref to use the local manifest only."] : undefined
10260
+ }),
10261
+ 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}`, {
10262
+ data: {
10263
+ declared: Boolean(machineInManifest),
10264
+ machine: machineInManifest ? redactManifestForDiagnostics(machineInManifest) : null
10265
+ }
10266
+ }),
10267
+ 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"}`, {
10268
+ data: {
10269
+ path: redactPath(details["data_dir"] || "unknown"),
10270
+ exists: details["data_dir_exists"] === "yes"
10271
+ }
10272
+ }),
10273
+ makeCheck2("manifest-path", details["manifest_exists"] === "yes" ? "ok" : "warn", "Manifest path check", `${redactPath(details["manifest_path"] || "unknown")} ${details["manifest_exists"] === "yes" ? "exists" : "missing"}`, {
10274
+ data: {
10275
+ path: redactPath(details["manifest_path"] || "unknown"),
10276
+ exists: details["manifest_exists"] === "yes"
10277
+ }
10278
+ }),
10279
+ makeCheck2("db-path", details["db_exists"] === "yes" ? "ok" : "warn", "DB path check", `${redactPath(details["db_path"] || "unknown")} ${details["db_exists"] === "yes" ? "exists" : "missing"}`, {
10280
+ data: {
10281
+ path: redactPath(details["db_path"] || "unknown"),
10282
+ exists: details["db_exists"] === "yes"
10283
+ }
10284
+ }),
10285
+ makeCheck2("notifications-path", details["notifications_exists"] === "yes" ? "ok" : "warn", "Notifications path check", `${redactPath(details["notifications_path"] || "unknown")} ${details["notifications_exists"] === "yes" ? "exists" : "missing"}`, {
10286
+ data: {
10287
+ path: redactPath(details["notifications_path"] || "unknown"),
10288
+ exists: details["notifications_exists"] === "yes"
10289
+ }
10290
+ }),
10009
10291
  makeCheck2("bun", details["bun"] && details["bun"] !== "missing" ? "ok" : "fail", "Bun availability", details["bun"] || "missing"),
10010
10292
  makeCheck2("machines-cli", details["machines"] && details["machines"] !== "missing" ? "ok" : "warn", "machines CLI availability", details["machines"] || "missing"),
10011
10293
  makeCheck2("machines-agent-cli", details["machines_agent"] && details["machines_agent"] !== "missing" ? "ok" : "warn", "machines-agent availability", details["machines_agent"] || "missing"),
10012
10294
  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")
10295
+ makeCheck2("ssh", details["ssh"] === "ok" ? "ok" : "warn", "SSH availability", details["ssh"] || "missing"),
10296
+ ...optionalAdapterChecks
10014
10297
  ];
10015
10298
  return {
10016
10299
  machineId,
10017
10300
  source: commandChecks.source,
10018
- manifestPath: details["manifest_path"],
10019
- dbPath: details["db_path"],
10020
- notificationsPath: details["notifications_path"],
10301
+ schemaVersion: 1,
10302
+ generatedAt: now.toISOString(),
10303
+ manifestSource,
10304
+ manifestPath: details["manifest_path"] ? redactPath(details["manifest_path"]) : undefined,
10305
+ dbPath: details["db_path"] ? redactPath(details["db_path"]) : undefined,
10306
+ notificationsPath: details["notifications_path"] ? redactPath(details["notifications_path"]) : undefined,
10021
10307
  checks
10022
10308
  };
10023
10309
  }
@@ -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;AAkHD,wBAAgB,SAAS,CAAC,SAAS,SAAsB,EAAE,OAAO,GAAE,aAAkB,GAAG,YAAY,CA0IpG"}