@crvouga/dev-fleet 0.0.0-stage → 0.2.0
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/.mise/locks/aws-cdk/2.1146.1/aube-lock.yaml +24 -0
- package/.mise/locks/aws-cdk/2.1146.1/package.json +7 -0
- package/.mise/locks/azure-cli/2.91.0~a837e014/pyproject.toml +5 -0
- package/.mise/locks/azure-cli/2.91.0~a837e014/uv.lock +2794 -0
- package/.mise/locks/gemini/0.66.0~84aac8e6/aube-lock.yaml +123 -0
- package/.mise/locks/gemini/0.66.0~84aac8e6/package.json +12 -0
- package/.mise/locks/npm-devcontainers-cli/0.90.0/aube-lock.yaml +24 -0
- package/.mise/locks/npm-devcontainers-cli/0.90.0/package.json +7 -0
- package/.mise/locks/npm-modelcontextprotocol-inspector/2.13.0~4c109bea/aube-lock.yaml +1432 -0
- package/.mise/locks/npm-modelcontextprotocol-inspector/2.13.0~4c109bea/package.json +7 -0
- package/.mise/locks/npm-vercel/65.8.0/aube-lock.yaml +3595 -0
- package/.mise/locks/npm-vercel/65.8.0/package.json +7 -0
- package/.mise/locks/npm-wrangler/4.156.0/aube-lock.yaml +895 -0
- package/.mise/locks/npm-wrangler/4.156.0/package.json +7 -0
- package/.mise/locks/npm-yarnpkg-cli-dist/4.18.1/aube-lock.yaml +24 -0
- package/.mise/locks/npm-yarnpkg-cli-dist/4.18.1/package.json +7 -0
- package/LICENSE +21 -0
- package/README.md +357 -2
- package/agents/antigravity-proxy.ts +3 -0
- package/agents/audit.md +89 -0
- package/agents/claude-proxy +5 -0
- package/agents/claude-proxy.ts +3 -0
- package/agents/codex-proxy.ts +3 -0
- package/agents/grok-proxy.ts +3 -0
- package/agents/managed-proxy.ts +55 -0
- package/agents/opencode-proxy.ts +3 -0
- package/agents/registry.json +209 -0
- package/agents/registry.schema.json +149 -0
- package/agents/setup.md +158 -0
- package/agents/t3.json +40 -0
- package/ansible/macos/manager.yml +30 -0
- package/ansible/macos/policy.json +9 -0
- package/ansible/monitoring/README.md +215 -0
- package/ansible/monitoring/agent-darwin.yml +198 -0
- package/ansible/monitoring/agent.yml +172 -0
- package/ansible/monitoring/beszel-agent.service +42 -0
- package/ansible/monitoring/compose.yml +74 -0
- package/ansible/monitoring/hub-darwin.yml +102 -0
- package/ansible/monitoring/hub-route-darwin.yml +51 -0
- package/ansible/monitoring/lock.json +16 -0
- package/ansible/net/remote-probe.sh +13 -0
- package/ansible/pyproject.toml +12 -0
- package/ansible/ubuntu/authorize-sudo.sh +51 -0
- package/ansible/ubuntu/baseline.yml +89 -0
- package/ansible/ubuntu/disk-gc.py +336 -0
- package/ansible/ubuntu/disk-guard.py +104 -0
- package/ansible/ubuntu/policy.json +52 -0
- package/ansible/ubuntu/recovery.yml +15 -0
- package/ansible/ubuntu/t3/policy.json +7 -0
- package/ansible/ubuntu/t3-desktop.yml +220 -0
- package/ansible/ubuntu/t3-health.sh +33 -0
- package/ansible/ubuntu/t3-policy.yml +76 -0
- package/ansible/ubuntu/t3-route.yml +28 -0
- package/ansible/ubuntu/tailscale.yml +78 -0
- package/ansible/ubuntu/tasks/credentials.yml +47 -0
- package/ansible/ubuntu/tasks/docker.yml +31 -0
- package/ansible/ubuntu/tasks/packages.yml +56 -0
- package/ansible/ubuntu/tasks/power.yml +51 -0
- package/ansible/ubuntu/tasks/recovery.yml +39 -0
- package/ansible/ubuntu/tasks/resources.yml +312 -0
- package/ansible/ubuntu/tasks/services.yml +31 -0
- package/ansible/ubuntu/tasks/ssh.yml +71 -0
- package/ansible/ubuntu/tasks/user.yml +120 -0
- package/ansible/uv.lock +190 -0
- package/bin/devfleet.mjs +36 -0
- package/bootstrap/bootstrap.ps1 +50 -0
- package/bootstrap/bootstrap.sh +80 -0
- package/bootstrap/devfleet +7 -0
- package/bootstrap/docker-desktop.sh +33 -0
- package/bootstrap/doctor.ts +234 -0
- package/bootstrap/fleet-manager-run.sh +103 -0
- package/bootstrap/fleet-remote-bootstrap.sh +258 -0
- package/bootstrap/global-config.ts +171 -0
- package/bootstrap/install-launcher.sh +10 -0
- package/bootstrap/native-aws.ts +86 -0
- package/bootstrap/native-t3.ts +154 -0
- package/bootstrap/npm.ts +358 -0
- package/bootstrap/release-version.ts +157 -0
- package/bootstrap/setup-status.ts +51 -0
- package/bootstrap/setup.ts +88 -0
- package/bootstrap/shell-path.ts +105 -0
- package/bootstrap/verify-t3-archive.py +49 -0
- package/bootstrap/worker-auth.ts +65 -0
- package/bootstrap/worker-github.ts +47 -0
- package/bootstrap/worker-tools.ts +24 -0
- package/cliproxy/AGENTS.md +11 -0
- package/cliproxy/README.md +269 -0
- package/cliproxy/clients/bootstrap.py +143 -0
- package/cliproxy/clients/launcher.py +57 -0
- package/cliproxy/compose.yaml +90 -0
- package/cliproxy/config.template.yaml +70 -0
- package/cliproxy/docs/cloud-adoption.md +16 -0
- package/cliproxy/docs/deployment.png +0 -0
- package/cliproxy/docs/deployment.svg +109 -0
- package/cliproxy/docs/health-workflow.example.yml +185 -0
- package/cliproxy/docs/migration.md +131 -0
- package/cliproxy/docs/model-health-report.schema.json +364 -0
- package/cliproxy/docs/model-health.md +393 -0
- package/cliproxy/image.lock.json +5 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-backup-check.plist +38 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-backup.plist +48 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-fleet.plist +32 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-gc.plist +38 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-monitor.plist +29 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-vault-renew.plist +31 -0
- package/cliproxy/models.json +1618 -0
- package/cliproxy/requirements.lock +78 -0
- package/cliproxy/scripts/accounts.py +38 -0
- package/cliproxy/scripts/audit.py +123 -0
- package/cliproxy/scripts/backup +234 -0
- package/cliproxy/scripts/check.py +34 -0
- package/cliproxy/scripts/checkpoint.py +226 -0
- package/cliproxy/scripts/cli.py +64 -0
- package/cliproxy/scripts/cliproxy_host.py +257 -0
- package/cliproxy/scripts/cloud_state.py +150 -0
- package/cliproxy/scripts/container-health +6 -0
- package/cliproxy/scripts/docker +24 -0
- package/cliproxy/scripts/external-health.py +79 -0
- package/cliproxy/scripts/fleet_config.py +43 -0
- package/cliproxy/scripts/fleet_sync.py +274 -0
- package/cliproxy/scripts/initialize +84 -0
- package/cliproxy/scripts/install-dependencies +48 -0
- package/cliproxy/scripts/install.py +406 -0
- package/cliproxy/scripts/model_health.py +16 -0
- package/cliproxy/scripts/modelhealth/__init__.py +5 -0
- package/cliproxy/scripts/modelhealth/checks.py +429 -0
- package/cliproxy/scripts/modelhealth/cli.py +117 -0
- package/cliproxy/scripts/modelhealth/config.py +186 -0
- package/cliproxy/scripts/modelhealth/emulator.py +536 -0
- package/cliproxy/scripts/modelhealth/expect.py +88 -0
- package/cliproxy/scripts/modelhealth/findings.py +183 -0
- package/cliproxy/scripts/modelhealth/protocol.py +330 -0
- package/cliproxy/scripts/modelhealth/redact.py +28 -0
- package/cliproxy/scripts/modelhealth/report.py +150 -0
- package/cliproxy/scripts/modelhealth/runner.py +390 -0
- package/cliproxy/scripts/modelhealth/scenario.py +131 -0
- package/cliproxy/scripts/modelhealth/schema.py +63 -0
- package/cliproxy/scripts/modelhealth/simulate.py +111 -0
- package/cliproxy/scripts/modelhealth/transport.py +213 -0
- package/cliproxy/scripts/monitor +68 -0
- package/cliproxy/scripts/ops +527 -0
- package/cliproxy/scripts/public-endpoint +127 -0
- package/cliproxy/scripts/recover_node.py +177 -0
- package/cliproxy/scripts/restore-verify +60 -0
- package/cliproxy/scripts/retention.py +144 -0
- package/cliproxy/scripts/service-start +7 -0
- package/cliproxy/scripts/validate-config +56 -0
- package/cliproxy/scripts/vault-access.py +57 -0
- package/cliproxy/scripts/vault-backup-login +55 -0
- package/cliproxy/scripts/vault-recovery +56 -0
- package/cliproxy/scripts/vault-renew +30 -0
- package/cliproxy/systemd/cliproxy-backup-check.service +10 -0
- package/cliproxy/systemd/cliproxy-backup-check.timer +8 -0
- package/cliproxy/systemd/cliproxy-backup.service +13 -0
- package/cliproxy/systemd/cliproxy-backup.timer +8 -0
- package/cliproxy/systemd/cliproxy-fleet.service +17 -0
- package/cliproxy/systemd/cliproxy-gc.service +13 -0
- package/cliproxy/systemd/cliproxy-gc.timer +8 -0
- package/cliproxy/systemd/cliproxy-monitor.service +8 -0
- package/cliproxy/systemd/cliproxy-monitor.timer +7 -0
- package/cliproxy/systemd/cliproxy-vault-renew.service +8 -0
- package/cliproxy/systemd/cliproxy-vault-renew.timer +7 -0
- package/cliproxy/vendor/LICENSE +22 -0
- package/cliproxy/vendor/config.upstream.yaml +1349 -0
- package/docs/consumer-setup.md +408 -0
- package/docs/declared.md +89 -0
- package/docs/machine-setup.md +399 -0
- package/docs/operations.md +276 -0
- package/docs/releasing.md +103 -0
- package/docs/state.md +254 -0
- package/docs/topology.md +67 -0
- package/docs/ubuntu-sync.md +351 -0
- package/docs/worker-contract.md +67 -0
- package/examples/fleet.json +100 -0
- package/examples/roster.json +13 -0
- package/llms.txt +35 -0
- package/mise.lock +1521 -0
- package/mise.toml +110 -0
- package/package.json +74 -4
- package/schema/fleet.schema.json +420 -0
- package/schema/roster.schema.json +77 -0
- package/src/agents/account-relay.ts +111 -0
- package/src/agents/accounts.ts +85 -0
- package/src/agents/audit.ts +77 -0
- package/src/agents/cli.ts +13 -0
- package/src/agents/command.ts +204 -0
- package/src/agents/converge.ts +123 -0
- package/src/agents/doctor.ts +107 -0
- package/src/agents/errors.ts +2 -0
- package/src/agents/files.ts +73 -0
- package/src/agents/fleet-verify.ts +165 -0
- package/src/agents/launch.ts +290 -0
- package/src/agents/native-verify.ts +371 -0
- package/src/agents/provider-errors.ts +57 -0
- package/src/agents/registry.ts +61 -0
- package/src/agents/render.ts +110 -0
- package/src/agents/round-trip.ts +192 -0
- package/src/agents/secrets.ts +13 -0
- package/src/agents/toolchain.ts +34 -0
- package/src/agents/verify-cache.ts +103 -0
- package/src/agents/verify.ts +127 -0
- package/src/auth-checks.ts +163 -0
- package/src/cli/agent-guide.ts +129 -0
- package/src/cli/app.ts +122 -0
- package/src/cli/cli.ts +28 -0
- package/src/cli/command-index.ts +57 -0
- package/src/cli/contracts.ts +25 -0
- package/src/cli/fleet-reference.ts +17 -0
- package/src/cli/fleet-state.ts +499 -0
- package/src/cli/fleet-sync-checkout.ts +130 -0
- package/src/cli/fleet-sync-enrollment.ts +140 -0
- package/src/cli/fleet-sync-forward.ts +134 -0
- package/src/cli/fleet-sync-manager.ts +249 -0
- package/src/cli/fleet-sync-monitoring.ts +185 -0
- package/src/cli/fleet-sync-output.ts +317 -0
- package/src/cli/fleet-sync-privilege.ts +27 -0
- package/src/cli/fleet-sync-progress.ts +109 -0
- package/src/cli/fleet-sync-remote.ts +242 -0
- package/src/cli/fleet-sync-report.ts +26 -0
- package/src/cli/fleet-sync-schedule.ts +138 -0
- package/src/cli/fleet-sync-state.ts +86 -0
- package/src/cli/fleet-sync-vault.ts +166 -0
- package/src/cli/fleet-sync.ts +375 -0
- package/src/cli/fleet.ts +165 -0
- package/src/cli/managed-health.ts +219 -0
- package/src/cli/node-deployment.ts +34 -0
- package/src/cli/node-sync.ts +121 -0
- package/src/cli/node-t3.ts +31 -0
- package/src/cli/options.ts +141 -0
- package/src/cli/probe-source.ts +7 -0
- package/src/cli/program.ts +185 -0
- package/src/cli/project.ts +51 -0
- package/src/cli/register-fleet.ts +296 -0
- package/src/cli/registry.ts +185 -0
- package/src/cli/repair-apply.ts +67 -0
- package/src/cli/repair-guard.ts +11 -0
- package/src/cli/repair-inspect.ts +150 -0
- package/src/cli/repair-script.ts +76 -0
- package/src/cli/repair.ts +209 -0
- package/src/cli/roster.ts +105 -0
- package/src/cli/search.ts +35 -0
- package/src/cli/settings.ts +67 -0
- package/src/cli/source-operations.ts +102 -0
- package/src/cli/t3/command.ts +90 -0
- package/src/cli/t3/register.ts +55 -0
- package/src/cli/tailnet.ts +127 -0
- package/src/cli/ui-actions.ts +133 -0
- package/src/cli/ui-controller.ts +113 -0
- package/src/cli/ui-data.ts +106 -0
- package/src/cli/ui-execute.ts +163 -0
- package/src/cli/ui-repairs.ts +34 -0
- package/src/cli/ui-reports.ts +211 -0
- package/src/cli/ui-targets.ts +100 -0
- package/src/cli/ui-terminal.ts +109 -0
- package/src/cli/ui-types.ts +49 -0
- package/src/cli/ui-wizard.ts +101 -0
- package/src/cli/ui.ts +9 -0
- package/src/cli/worker-health-output.ts +37 -0
- package/src/config.ts +186 -0
- package/src/configure.ts +255 -0
- package/src/definition.ts +290 -0
- package/src/diagnostics.ts +92 -0
- package/src/doctor-check.ts +10 -0
- package/src/errors.ts +1 -0
- package/src/fingerprint.ts +120 -0
- package/src/fleet-config.ts +174 -0
- package/src/input-hashes.ts +73 -0
- package/src/machine/local-source.ts +52 -0
- package/src/machine/lock-error.ts +2 -0
- package/src/machine/platform.ts +44 -0
- package/src/machine/source.ts +68 -0
- package/src/machine/state.ts +173 -0
- package/src/machine-health.ts +95 -0
- package/src/managed-source.ts +47 -0
- package/src/manager-failure.ts +48 -0
- package/src/manager-sync.ts +405 -0
- package/src/manager.ts +62 -0
- package/src/monitoring/agent-evidence.ts +146 -0
- package/src/monitoring/apply.ts +183 -0
- package/src/monitoring/beszel.ts +198 -0
- package/src/monitoring/enroll.ts +111 -0
- package/src/monitoring/hub-access.ts +114 -0
- package/src/monitoring/hub-backup.ts +121 -0
- package/src/monitoring/hub-viewer.ts +142 -0
- package/src/monitoring/hub.ts +93 -0
- package/src/monitoring/manager-agent.ts +73 -0
- package/src/monitoring/manager-hub.ts +248 -0
- package/src/monitoring/remote.ts +65 -0
- package/src/network-definition.ts +83 -0
- package/src/node-converge-linux.ts +374 -0
- package/src/node-converge-shared.ts +83 -0
- package/src/node-converge.ts +40 -0
- package/src/onboard.ts +35 -0
- package/src/package-info.ts +45 -0
- package/src/platform/ansible-receipt.ts +128 -0
- package/src/platform/ansible.ts +66 -0
- package/src/platform/darwin.ts +203 -0
- package/src/platform/detect.ts +11 -0
- package/src/platform/fallback.ts +110 -0
- package/src/platform/index.ts +38 -0
- package/src/platform/launchd.ts +98 -0
- package/src/platform/linux.ts +139 -0
- package/src/platform/ps.ts +45 -0
- package/src/platform/resource-verify.ts +132 -0
- package/src/platform/systemd-user.ts +95 -0
- package/src/platform/t3-policy.ts +137 -0
- package/src/platform/t3-route.ts +68 -0
- package/src/platform/t3-service.ts +75 -0
- package/src/platform/types.ts +123 -0
- package/src/platform/ubuntu-audit.ts +66 -0
- package/src/platform/ubuntu-verify.ts +105 -0
- package/src/platform/windows.ts +148 -0
- package/src/power-recovery.ts +55 -0
- package/src/probe.ts +51 -0
- package/src/protocol.ts +119 -0
- package/src/proxy-converge.ts +85 -0
- package/src/remote-bootstrap.ts +210 -0
- package/src/remote-entry.ts +89 -0
- package/src/remote-lock.ts +12 -0
- package/src/repair-catalog.ts +206 -0
- package/src/repair-plan.ts +78 -0
- package/src/repair-runner.ts +84 -0
- package/src/report.ts +71 -0
- package/src/runner.ts +104 -0
- package/src/secrets.ts +169 -0
- package/src/service-auth.ts +115 -0
- package/src/ssh.ts +135 -0
- package/src/state/bucket.ts +208 -0
- package/src/state/check.ts +74 -0
- package/src/state/connection.ts +92 -0
- package/src/state/enrollment.ts +45 -0
- package/src/state/legacy.ts +89 -0
- package/src/state/litestream.ts +143 -0
- package/src/state/operator.ts +20 -0
- package/src/state/paths.ts +40 -0
- package/src/state/roster.ts +62 -0
- package/src/state/store.ts +169 -0
- package/src/state/sync.ts +352 -0
- package/src/sync.ts +104 -0
- package/src/t3/accounts.ts +81 -0
- package/src/t3/desired.ts +165 -0
- package/src/t3/files.ts +81 -0
- package/src/t3/names-check.ts +53 -0
- package/src/t3/names.ts +155 -0
- package/src/t3/plan.ts +82 -0
- package/src/t3/providers.ts +74 -0
- package/src/t3/reconcile.ts +123 -0
- package/src/t3-endpoint.ts +104 -0
- package/src/t3-linked-check.ts +123 -0
- package/src/tailnet-health.ts +231 -0
- package/src/tailnet-matching.ts +37 -0
- package/src/tailscale.ts +141 -0
- package/src/toolchain-checks.ts +108 -0
- package/src/topology.ts +24 -0
- package/src/transport.ts +122 -0
- package/src/types.ts +99 -0
- package/src/vault-config.ts +38 -0
- package/src/worker-access-checks.ts +168 -0
- package/src/worker-capacity.ts +34 -0
- package/src/worker-host-checks.ts +181 -0
- package/src/worker-policy.ts +64 -0
- package/src/worker-readiness.ts +64 -0
- package/src/workstation-checks.ts +29 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { record, reportChecks, text } from './ui-data';
|
|
2
|
+
import type { CheckRow, Choice, Session } from './ui-types';
|
|
3
|
+
|
|
4
|
+
export async function inspectChecks(
|
|
5
|
+
session: Session,
|
|
6
|
+
data: unknown
|
|
7
|
+
): Promise<void> {
|
|
8
|
+
const checks = reportChecks(data);
|
|
9
|
+
let node = '*';
|
|
10
|
+
let status = '*';
|
|
11
|
+
while (true) {
|
|
12
|
+
const visible = checks.filter(
|
|
13
|
+
(check) =>
|
|
14
|
+
(node === '*' || check.node === node) &&
|
|
15
|
+
(status === '*' || check.status === status)
|
|
16
|
+
);
|
|
17
|
+
const choice = await session.prompts.search(
|
|
18
|
+
`Report · ${visible.length}/${checks.length} checks · node ${node} · status ${status}`,
|
|
19
|
+
(term) => checkChoices(visible, term)
|
|
20
|
+
);
|
|
21
|
+
if (choice === 'back') return;
|
|
22
|
+
if (choice === 'node') node = await nodeFilter(session, checks);
|
|
23
|
+
else if (choice === 'status') status = await statusFilter(session);
|
|
24
|
+
else {
|
|
25
|
+
const check = visible[Number(choice)];
|
|
26
|
+
if (check) showCheck(session, check);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function checkChoices(checks: CheckRow[], term: string): Choice[] {
|
|
32
|
+
const choices = checks.flatMap((check, index) => {
|
|
33
|
+
const name = `${check.status.toUpperCase()} · ${check.node} · ${check.id}`;
|
|
34
|
+
return `${name} ${check.summary}`.toLowerCase().includes(term.toLowerCase())
|
|
35
|
+
? [{ name, value: String(index), description: check.summary }]
|
|
36
|
+
: [];
|
|
37
|
+
});
|
|
38
|
+
return [
|
|
39
|
+
...(choices.length
|
|
40
|
+
? choices
|
|
41
|
+
: [
|
|
42
|
+
{
|
|
43
|
+
name: 'No matching checks — change search or filters',
|
|
44
|
+
value: 'none',
|
|
45
|
+
},
|
|
46
|
+
]),
|
|
47
|
+
{ name: 'Filter by node', value: 'node' },
|
|
48
|
+
{ name: 'Filter by status', value: 'status' },
|
|
49
|
+
{ name: 'Back to result', value: 'back' },
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function nodeFilter(session: Session, checks: CheckRow[]): Promise<string> {
|
|
54
|
+
return session.prompts.select('Node filter', [
|
|
55
|
+
{ name: 'All nodes', value: '*' },
|
|
56
|
+
...[...new Set(checks.map((check) => check.node))].map((node) => ({
|
|
57
|
+
name: node,
|
|
58
|
+
value: node,
|
|
59
|
+
})),
|
|
60
|
+
]);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function statusFilter(session: Session): Promise<string> {
|
|
64
|
+
return session.prompts.select('Status filter', [
|
|
65
|
+
{ name: 'All statuses (including warnings and skips)', value: '*' },
|
|
66
|
+
...['fail', 'warn', 'skip', 'pass'].map((status) => ({
|
|
67
|
+
name: status.toUpperCase(),
|
|
68
|
+
value: status,
|
|
69
|
+
})),
|
|
70
|
+
]);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function showCheck(session: Session, check: CheckRow): void {
|
|
74
|
+
session.display.panel(
|
|
75
|
+
`${check.status.toUpperCase()} · ${check.node} · ${check.id}`,
|
|
76
|
+
check.summary
|
|
77
|
+
);
|
|
78
|
+
session.display.write(
|
|
79
|
+
`Remedy: ${check.remedy || 'No automated remedy. Review this check manually.'}`
|
|
80
|
+
);
|
|
81
|
+
if (check.command)
|
|
82
|
+
session.display.write(
|
|
83
|
+
`Suggested operator command (not executed):\n${check.command}`
|
|
84
|
+
);
|
|
85
|
+
session.display.write(
|
|
86
|
+
'Remedies are guidance only. Credentials, SSH and host-key configuration stay operator-owned.'
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function inspectDetails(
|
|
91
|
+
session: Session,
|
|
92
|
+
data: unknown
|
|
93
|
+
): Promise<void> {
|
|
94
|
+
const sections = detailSections(data);
|
|
95
|
+
if (!sections.length) {
|
|
96
|
+
session.display.write(
|
|
97
|
+
'No additional safe display fields. Use the reproducible command for the structured document.'
|
|
98
|
+
);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
let next = true;
|
|
102
|
+
while (next) {
|
|
103
|
+
const selection = await session.prompts.select('Details', [
|
|
104
|
+
...sections.map((section, index) => ({
|
|
105
|
+
name: section.title,
|
|
106
|
+
value: String(index),
|
|
107
|
+
})),
|
|
108
|
+
{ name: 'Back to result', value: 'back' },
|
|
109
|
+
]);
|
|
110
|
+
const section = sections[Number(selection)];
|
|
111
|
+
if (selection === 'back') next = false;
|
|
112
|
+
else if (section) session.display.panel(section.title, section.body);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function detailSections(data: unknown): { title: string; body: string }[] {
|
|
117
|
+
if (!record(data)) return [];
|
|
118
|
+
const sections = [
|
|
119
|
+
'help',
|
|
120
|
+
'guide',
|
|
121
|
+
'next',
|
|
122
|
+
'authorize',
|
|
123
|
+
'path',
|
|
124
|
+
'sshTemplate',
|
|
125
|
+
'workspaceCommand',
|
|
126
|
+
'verifyCommand',
|
|
127
|
+
'invocation',
|
|
128
|
+
'discovery',
|
|
129
|
+
].flatMap((key) =>
|
|
130
|
+
typeof data[key] === 'string' ? [{ title: key, body: text(data[key]) }] : []
|
|
131
|
+
);
|
|
132
|
+
for (const key of ['steps', 'targets', 'safety', 'examples']) {
|
|
133
|
+
if (Array.isArray(data[key]))
|
|
134
|
+
sections.push(
|
|
135
|
+
...data[key].flatMap((value: unknown, index: number) =>
|
|
136
|
+
typeof value === 'string'
|
|
137
|
+
? [{ title: `${key} · ${index + 1}`, body: text(value) }]
|
|
138
|
+
: []
|
|
139
|
+
)
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
if (Array.isArray(data['nodes']))
|
|
143
|
+
sections.push(...nodeSections(data['nodes']));
|
|
144
|
+
if (Array.isArray(data['commands']))
|
|
145
|
+
sections.push(...commandSections(data['commands']));
|
|
146
|
+
for (const key of ['configuration', 'output', 'coverage']) {
|
|
147
|
+
if (record(data[key]))
|
|
148
|
+
sections.push({ title: key, body: safeFields(data[key]) });
|
|
149
|
+
}
|
|
150
|
+
if (typeof data['$schema'] === 'string')
|
|
151
|
+
sections.push({
|
|
152
|
+
title: 'JSON schema',
|
|
153
|
+
body: JSON.stringify(data, null, 2),
|
|
154
|
+
});
|
|
155
|
+
return sections;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function commandSections(
|
|
159
|
+
commands: unknown[]
|
|
160
|
+
): { title: string; body: string }[] {
|
|
161
|
+
return commands.flatMap((command) => {
|
|
162
|
+
if (!record(command)) return [];
|
|
163
|
+
return [
|
|
164
|
+
{
|
|
165
|
+
title: text(command['id'], 'Command'),
|
|
166
|
+
body: [
|
|
167
|
+
text(command['title']),
|
|
168
|
+
text(command['description']),
|
|
169
|
+
...(Array.isArray(command['examples'])
|
|
170
|
+
? command['examples'].map((value: unknown) => text(value))
|
|
171
|
+
: []),
|
|
172
|
+
]
|
|
173
|
+
.filter(Boolean)
|
|
174
|
+
.join('\n'),
|
|
175
|
+
},
|
|
176
|
+
];
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function safeFields(value: Record<string, unknown>): string {
|
|
181
|
+
return Object.entries(value)
|
|
182
|
+
.flatMap(([key, field]) =>
|
|
183
|
+
typeof field === 'string' ? [`${key}: ${text(field)}`] : []
|
|
184
|
+
)
|
|
185
|
+
.join('\n');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function nodeSections(nodes: unknown[]): { title: string; body: string }[] {
|
|
189
|
+
return nodes.flatMap((node) => {
|
|
190
|
+
if (!record(node)) return [];
|
|
191
|
+
const id = text(node['id'], text(node['node'], 'Node'));
|
|
192
|
+
const body = ['id', 'ssh', 'workspace', 'runtime'].flatMap((key) =>
|
|
193
|
+
typeof node[key] === 'string' ? [`${key}: ${text(node[key])}`] : []
|
|
194
|
+
);
|
|
195
|
+
if (Array.isArray(node['required']))
|
|
196
|
+
body.push(
|
|
197
|
+
`required: ${node['required'].map((value: unknown) => text(value)).join(', ') || 'none'}`
|
|
198
|
+
);
|
|
199
|
+
return [
|
|
200
|
+
...(body.length ? [{ title: id, body: body.join('\n') }] : []),
|
|
201
|
+
...detailSections({
|
|
202
|
+
steps: node['steps'],
|
|
203
|
+
sshTemplate: node['sshTemplate'],
|
|
204
|
+
verifyCommand: node['verifyCommand'],
|
|
205
|
+
}).map((section) => ({
|
|
206
|
+
title: `${id} · ${section.title}`,
|
|
207
|
+
body: section.body,
|
|
208
|
+
})),
|
|
209
|
+
];
|
|
210
|
+
});
|
|
211
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { execute, showOutcome, successful } from './ui-execute';
|
|
2
|
+
import { record, text } from './ui-data';
|
|
3
|
+
import type { Choice, Session } from './ui-types';
|
|
4
|
+
|
|
5
|
+
export async function chooseTargets(
|
|
6
|
+
session: Session,
|
|
7
|
+
action: string
|
|
8
|
+
): Promise<string[] | undefined> {
|
|
9
|
+
const selection = await session.prompts.select('Choose target scope', [
|
|
10
|
+
...(action === 'onboard'
|
|
11
|
+
? []
|
|
12
|
+
: [
|
|
13
|
+
{
|
|
14
|
+
name: 'Manager only / local',
|
|
15
|
+
value: 'local',
|
|
16
|
+
description:
|
|
17
|
+
'No roster or SSH required; uses this session workspace.',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: allTargetsName(action),
|
|
21
|
+
value: 'all',
|
|
22
|
+
description: allTargetsDescription(action),
|
|
23
|
+
},
|
|
24
|
+
]),
|
|
25
|
+
{ name: 'Select roster nodes…', value: 'selected' },
|
|
26
|
+
{ name: 'Back to palette', value: 'back' },
|
|
27
|
+
]);
|
|
28
|
+
if (selection === 'back') return undefined;
|
|
29
|
+
if (selection === 'local') return ['--local'];
|
|
30
|
+
if (selection === 'all' && action === 'repair') return [];
|
|
31
|
+
const nodes = await availableNodes(session);
|
|
32
|
+
if (!nodes.length) return missingRoster(session, action);
|
|
33
|
+
if (selection === 'all') return [];
|
|
34
|
+
const selected = await session.prompts.checkbox(
|
|
35
|
+
'Select nodes · space to toggle, enter to continue',
|
|
36
|
+
nodes
|
|
37
|
+
);
|
|
38
|
+
if (!selected.length) {
|
|
39
|
+
session.display.write(
|
|
40
|
+
'No nodes selected. Nothing ran; returning to the palette.'
|
|
41
|
+
);
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
return selected.flatMap((id) => ['--node', id]);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function availableNodes(session: Session): Promise<Choice[]> {
|
|
48
|
+
const outcome = await execute(
|
|
49
|
+
session,
|
|
50
|
+
['fleet-nodes'],
|
|
51
|
+
'Reading local roster (no SSH)'
|
|
52
|
+
);
|
|
53
|
+
if (!successful(outcome)) {
|
|
54
|
+
showOutcome(session, outcome);
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
if (!record(outcome.data) || !Array.isArray(outcome.data['nodes'])) return [];
|
|
58
|
+
return outcome.data['nodes'].flatMap((node: unknown) => {
|
|
59
|
+
if (!record(node) || typeof node['id'] !== 'string') return [];
|
|
60
|
+
return [
|
|
61
|
+
{
|
|
62
|
+
name: text(node['id']),
|
|
63
|
+
value: node['id'],
|
|
64
|
+
description: `${text(node['ssh'])} · ${text(node['workspace'])}`,
|
|
65
|
+
},
|
|
66
|
+
];
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function missingRoster(
|
|
71
|
+
session: Session,
|
|
72
|
+
action: string
|
|
73
|
+
): Promise<string[] | undefined> {
|
|
74
|
+
session.display.panel(
|
|
75
|
+
'Roster needed',
|
|
76
|
+
[
|
|
77
|
+
'No usable roster nodes. Run fleet-node-add to register an existing computer.',
|
|
78
|
+
'Manager-only status works without a roster or SSH. No login, host-key change or repair is automatic.',
|
|
79
|
+
].join('\n')
|
|
80
|
+
);
|
|
81
|
+
if (action !== 'doctor') return undefined;
|
|
82
|
+
const choice = await session.prompts.select('Continue without a roster?', [
|
|
83
|
+
{ name: 'Run manager-only diagnostics', value: 'local' },
|
|
84
|
+
{ name: 'Back to palette', value: 'back' },
|
|
85
|
+
]);
|
|
86
|
+
return choice === 'local' ? ['--local'] : undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function allTargetsName(action: string): string {
|
|
90
|
+
return action === 'repair'
|
|
91
|
+
? 'All visible Tailscale computers'
|
|
92
|
+
: 'All roster nodes';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function allTargetsDescription(action: string): string {
|
|
96
|
+
if (action === 'doctor') return 'Manager + every configured node; read-only.';
|
|
97
|
+
return action === 'repair'
|
|
98
|
+
? 'Inspect this manager and every visible computer; preview repairs before applying.'
|
|
99
|
+
: 'Every configured remote checkout.';
|
|
100
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { checkbox, confirm, input, search, select } from '@inquirer/prompts';
|
|
2
|
+
import { Chalk } from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import boxen from 'boxen';
|
|
5
|
+
import Table from 'cli-table3';
|
|
6
|
+
import { clean } from './ui-data';
|
|
7
|
+
import type { Display, Prompts } from './ui-types';
|
|
8
|
+
|
|
9
|
+
function plainTerminal(): boolean {
|
|
10
|
+
return (
|
|
11
|
+
process.env['NO_COLOR'] !== undefined || process.env['TERM'] === 'dumb'
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function pageSize(): number {
|
|
16
|
+
return Math.max(3, Math.min(10, (process.stderr.rows || 24) - 10));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function terminalPrompts(): Prompts {
|
|
20
|
+
const context = { output: process.stderr, clearPromptOnDone: false };
|
|
21
|
+
return {
|
|
22
|
+
select: (message, choices) =>
|
|
23
|
+
select({ message, choices, pageSize: pageSize(), loop: false }, context),
|
|
24
|
+
search: (message, source) =>
|
|
25
|
+
search(
|
|
26
|
+
{
|
|
27
|
+
message,
|
|
28
|
+
source: (term) => source(term ?? ''),
|
|
29
|
+
pageSize: pageSize(),
|
|
30
|
+
theme: { style: { highlight: (value: string) => value } },
|
|
31
|
+
},
|
|
32
|
+
context
|
|
33
|
+
),
|
|
34
|
+
checkbox: (message, choices) =>
|
|
35
|
+
checkbox(
|
|
36
|
+
{ message, choices, pageSize: pageSize(), loop: false },
|
|
37
|
+
context
|
|
38
|
+
),
|
|
39
|
+
input: (message, initial = '', validate = () => true) =>
|
|
40
|
+
input({ message, default: initial, validate }, context),
|
|
41
|
+
confirm: (message) => confirm({ message, default: false }, context),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function terminalDisplay(): Display {
|
|
46
|
+
const color = new Chalk({
|
|
47
|
+
level: !plainTerminal() && process.stderr.isTTY ? 1 : 0,
|
|
48
|
+
});
|
|
49
|
+
const write = (message: string): void => {
|
|
50
|
+
process.stderr.write(`${message}\n`);
|
|
51
|
+
};
|
|
52
|
+
return {
|
|
53
|
+
write,
|
|
54
|
+
panel: (title, body) => {
|
|
55
|
+
const content = `${color.bold(clean(title))}\n${body}`;
|
|
56
|
+
if ((process.stderr.columns || 80) < 48 || plainTerminal()) {
|
|
57
|
+
write(`\n${content}\n`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
write(
|
|
61
|
+
boxen(content, {
|
|
62
|
+
padding: 1,
|
|
63
|
+
borderStyle: 'round',
|
|
64
|
+
...(color.level ? { borderColor: 'cyan' as const } : {}),
|
|
65
|
+
width: Math.min(process.stderr.columns || 80, 96),
|
|
66
|
+
})
|
|
67
|
+
);
|
|
68
|
+
},
|
|
69
|
+
table: (headers, rows) => drawTable(write, headers, rows),
|
|
70
|
+
progress: (message) => {
|
|
71
|
+
const spinner = ora({
|
|
72
|
+
text: message,
|
|
73
|
+
stream: process.stderr,
|
|
74
|
+
isEnabled: Boolean(process.stderr.isTTY) && !plainTerminal(),
|
|
75
|
+
isSilent: !process.stderr.isTTY || plainTerminal(),
|
|
76
|
+
discardStdin: false,
|
|
77
|
+
});
|
|
78
|
+
if (spinner.isEnabled) spinner.start();
|
|
79
|
+
else write(`… ${message}`);
|
|
80
|
+
return {
|
|
81
|
+
stop: () => {
|
|
82
|
+
spinner.stop();
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function drawTable(
|
|
90
|
+
write: (text: string) => void,
|
|
91
|
+
headers: string[],
|
|
92
|
+
rows: string[][]
|
|
93
|
+
): void {
|
|
94
|
+
if ((process.stderr.columns || 80) < 64 || plainTerminal()) {
|
|
95
|
+
rows.forEach((row) => write(row.map((cell) => clean(cell)).join(' · ')));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const width = Math.floor(
|
|
99
|
+
((process.stderr.columns || 80) - headers.length * 3 - 1) / headers.length
|
|
100
|
+
);
|
|
101
|
+
const table = new Table({
|
|
102
|
+
head: headers,
|
|
103
|
+
colWidths: headers.map(() => Math.max(8, width)),
|
|
104
|
+
wordWrap: true,
|
|
105
|
+
style: { head: [], border: [] },
|
|
106
|
+
});
|
|
107
|
+
table.push(...rows.map((row) => row.map((cell) => clean(cell))));
|
|
108
|
+
write(table.toString());
|
|
109
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { CommandOutcome, InteractiveOptions } from './contracts';
|
|
2
|
+
|
|
3
|
+
export interface Choice {
|
|
4
|
+
name: string;
|
|
5
|
+
value: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
checked?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface Prompts {
|
|
11
|
+
select(message: string, choices: Choice[]): Promise<string>;
|
|
12
|
+
search(message: string, source: (term: string) => Choice[]): Promise<string>;
|
|
13
|
+
checkbox(message: string, choices: Choice[]): Promise<string[]>;
|
|
14
|
+
input(
|
|
15
|
+
message: string,
|
|
16
|
+
initial?: string,
|
|
17
|
+
validate?: (value: string) => boolean | string
|
|
18
|
+
): Promise<string>;
|
|
19
|
+
confirm(message: string): Promise<boolean>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface Display {
|
|
23
|
+
write(text: string): void;
|
|
24
|
+
panel(title: string, body: string): void;
|
|
25
|
+
table(headers: string[], rows: string[][]): void;
|
|
26
|
+
progress(message: string): { stop(): void };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface Session {
|
|
30
|
+
options: InteractiveOptions;
|
|
31
|
+
prompts: Prompts;
|
|
32
|
+
display: Display;
|
|
33
|
+
workspace?: string;
|
|
34
|
+
config?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface CheckRow {
|
|
38
|
+
node: string;
|
|
39
|
+
id: string;
|
|
40
|
+
status: string;
|
|
41
|
+
summary: string;
|
|
42
|
+
remedy?: string;
|
|
43
|
+
command?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface Execution {
|
|
47
|
+
outcome: CommandOutcome;
|
|
48
|
+
args: string[];
|
|
49
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { confirmMutation, runWithResults } from './ui-actions';
|
|
2
|
+
import { absolutePath, clean, reproduction } from './ui-data';
|
|
3
|
+
import type { Session } from './ui-types';
|
|
4
|
+
|
|
5
|
+
const CAPABILITIES = ['github', 'vault', 't3', 'vscode', 'toolchain'];
|
|
6
|
+
|
|
7
|
+
export async function addNode(session: Session): Promise<void> {
|
|
8
|
+
showIntro(session);
|
|
9
|
+
await registerNode(session);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function showIntro(session: Session): void {
|
|
13
|
+
session.display.panel(
|
|
14
|
+
'fleet-node-add',
|
|
15
|
+
[
|
|
16
|
+
'Register an EXISTING computer in your local secret-free roster.',
|
|
17
|
+
'Use a previously verified SSH alias. This wizard never connects, provisions, logs in or changes host keys.',
|
|
18
|
+
'Cancel with Ctrl+C. No write occurs until you approve the preview.',
|
|
19
|
+
].join('\n')
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function registerNode(session: Session): Promise<void> {
|
|
24
|
+
const id = await session.prompts.input(
|
|
25
|
+
'Node ID (lowercase, e.g. dev-linux)',
|
|
26
|
+
'',
|
|
27
|
+
validId
|
|
28
|
+
);
|
|
29
|
+
const alias = await session.prompts.input(
|
|
30
|
+
'Existing SSH alias / hostname',
|
|
31
|
+
id,
|
|
32
|
+
validAlias
|
|
33
|
+
);
|
|
34
|
+
const workspace = await session.prompts.input(
|
|
35
|
+
'Absolute POSIX workspace on the node',
|
|
36
|
+
'',
|
|
37
|
+
absolutePath
|
|
38
|
+
);
|
|
39
|
+
const runtime = await session.prompts.input(
|
|
40
|
+
'Optional absolute Node/Bun executable (blank = node on SSH PATH)',
|
|
41
|
+
'',
|
|
42
|
+
(value) => value === '' || absolutePath(value)
|
|
43
|
+
);
|
|
44
|
+
const capabilities = await session.prompts.checkbox(
|
|
45
|
+
'Required capabilities · space to toggle',
|
|
46
|
+
CAPABILITIES.map((capability) => ({
|
|
47
|
+
name: capability,
|
|
48
|
+
value: capability,
|
|
49
|
+
checked: capability !== 'vscode',
|
|
50
|
+
}))
|
|
51
|
+
);
|
|
52
|
+
const args = [
|
|
53
|
+
'fleet-node-add',
|
|
54
|
+
'--id',
|
|
55
|
+
id,
|
|
56
|
+
'--ssh',
|
|
57
|
+
alias,
|
|
58
|
+
'--path',
|
|
59
|
+
workspace,
|
|
60
|
+
...(runtime ? ['--runtime', runtime] : []),
|
|
61
|
+
'--require',
|
|
62
|
+
capabilities.join(',') || 'none',
|
|
63
|
+
'--apply',
|
|
64
|
+
'--yes',
|
|
65
|
+
];
|
|
66
|
+
session.display.table(
|
|
67
|
+
['Field', 'Preview'],
|
|
68
|
+
[
|
|
69
|
+
['ID', clean(id)],
|
|
70
|
+
['SSH', clean(alias)],
|
|
71
|
+
['Workspace', clean(workspace)],
|
|
72
|
+
['Runtime', clean(runtime) || 'node (remote non-login PATH)'],
|
|
73
|
+
['Required', capabilities.join(', ') || 'none'],
|
|
74
|
+
]
|
|
75
|
+
);
|
|
76
|
+
session.display.write(`$ ${reproduction(session, args)}`);
|
|
77
|
+
if (!(await confirmMutation(session, args))) {
|
|
78
|
+
session.display.write('Cancelled — no roster was written.');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
await runWithResults(session, args, 'Saving roster node (no SSH)', {
|
|
82
|
+
mutation: true,
|
|
83
|
+
});
|
|
84
|
+
session.display.write(
|
|
85
|
+
'Next: fleet-nodes --onboard --node ID for operator-owned SSH / authentication guidance, then fleet-status.'
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function validId(value: string): boolean | string {
|
|
90
|
+
return (
|
|
91
|
+
/^[a-z][a-z0-9-]{0,62}$/.test(value) ||
|
|
92
|
+
'Use 1–63 lowercase letters, digits or hyphens; start with a letter.'
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function validAlias(value: string): boolean | string {
|
|
97
|
+
return (
|
|
98
|
+
/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,252}$/.test(value) ||
|
|
99
|
+
'Use an existing plain SSH alias/hostname; no user@host, options or ports.'
|
|
100
|
+
);
|
|
101
|
+
}
|
package/src/cli/ui.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { InteractiveOptions } from './contracts';
|
|
2
|
+
import { runInteractive } from './ui-controller';
|
|
3
|
+
import { terminalDisplay, terminalPrompts } from './ui-terminal';
|
|
4
|
+
|
|
5
|
+
export async function startInteractive(
|
|
6
|
+
options: InteractiveOptions
|
|
7
|
+
): Promise<void> {
|
|
8
|
+
await runInteractive(options, terminalPrompts(), terminalDisplay());
|
|
9
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Report } from '../types';
|
|
2
|
+
import { stableUrls } from './fleet-sync-output';
|
|
3
|
+
|
|
4
|
+
/** The human command answers what remains; the JSON contract retains all evidence. */
|
|
5
|
+
export function workerHealthText(
|
|
6
|
+
report: Report,
|
|
7
|
+
scope: { computers: number; excludedUnmanaged: string[] },
|
|
8
|
+
dashboardUrl: string | null = null
|
|
9
|
+
): string {
|
|
10
|
+
const lines = [
|
|
11
|
+
`Fleet health: ${report.ok ? 'ready' : 'attention required'}`,
|
|
12
|
+
`Scope: ${scope.computers} canonical workers; ${scope.excludedUnmanaged.length} other computers outside worker scope.`,
|
|
13
|
+
`Dashboard: ${dashboardUrl ?? 'unavailable; see the dashboard checks below'}`,
|
|
14
|
+
];
|
|
15
|
+
for (const node of report.nodes) {
|
|
16
|
+
if (
|
|
17
|
+
node.node === 'tailscale' &&
|
|
18
|
+
node.checks.every((check) => check.status === 'pass')
|
|
19
|
+
)
|
|
20
|
+
continue;
|
|
21
|
+
const gaps = node.checks.filter(
|
|
22
|
+
(check) => check.required !== false && check.status !== 'pass'
|
|
23
|
+
);
|
|
24
|
+
lines.push(
|
|
25
|
+
'',
|
|
26
|
+
`${node.node}: ${gaps.length ? `${gaps.length} remaining` : 'ready'}`
|
|
27
|
+
);
|
|
28
|
+
for (const check of gaps) {
|
|
29
|
+
lines.push(
|
|
30
|
+
` ${check.status.toUpperCase()} ${check.id}: ${check.summary}`
|
|
31
|
+
);
|
|
32
|
+
if (check.remedy) lines.push(` Next: ${check.remedy.action}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
lines.push('', 'Use --json for the full versioned evidence report.');
|
|
36
|
+
return stableUrls(lines.join('\n'));
|
|
37
|
+
}
|