@crvouga/dev-fleet 0.0.0-stage → 0.1.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 +157 -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 +128 -0
- package/agents/t3.json +40 -0
- package/ansible/macos/manager.yml +30 -0
- package/ansible/macos/policy.json +17 -0
- package/ansible/monitoring/README.md +154 -0
- package/ansible/monitoring/agent-darwin.yml +198 -0
- package/ansible/monitoring/agent.yml +134 -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/lock.json +11 -0
- package/ansible/net/remote-probe.sh +13 -0
- package/ansible/ubuntu/authorize-sudo.sh +51 -0
- package/ansible/ubuntu/baseline.yml +89 -0
- package/ansible/ubuntu/disk-guard.py +104 -0
- package/ansible/ubuntu/policy.json +43 -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 +75 -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 +252 -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/bin/devfleet.mjs +36 -0
- package/bootstrap/bootstrap.ps1 +50 -0
- package/bootstrap/bootstrap.sh +73 -0
- package/bootstrap/devfleet +7 -0
- package/bootstrap/docker-desktop.sh +33 -0
- package/bootstrap/doctor.ts +189 -0
- package/bootstrap/fleet-manager-run.sh +76 -0
- package/bootstrap/fleet-remote-bootstrap.sh +185 -0
- package/bootstrap/global-config.ts +113 -0
- package/bootstrap/install-launcher.sh +7 -0
- package/bootstrap/native-aws.ts +86 -0
- package/bootstrap/native-t3.ts +154 -0
- package/bootstrap/npm.ts +350 -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/examples/fleet.json +95 -0
- package/examples/roster.json +13 -0
- package/mise.lock +1492 -0
- package/mise.toml +99 -0
- package/package.json +65 -4
- package/schema/fleet.schema.json +364 -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 +156 -0
- package/src/agents/launch.ts +290 -0
- package/src/agents/native-verify.ts +226 -0
- package/src/agents/provider-errors.ts +50 -0
- package/src/agents/registry.ts +55 -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 +95 -0
- package/src/cli/app.ts +115 -0
- package/src/cli/cli.ts +21 -0
- package/src/cli/command-index.ts +50 -0
- package/src/cli/contracts.ts +25 -0
- package/src/cli/fleet-reference.ts +17 -0
- package/src/cli/fleet-sync-checkout.ts +91 -0
- package/src/cli/fleet-sync-enrollment.ts +144 -0
- package/src/cli/fleet-sync-forward.ts +134 -0
- package/src/cli/fleet-sync-manager.ts +193 -0
- package/src/cli/fleet-sync-monitoring.ts +185 -0
- package/src/cli/fleet-sync-output.ts +314 -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 +236 -0
- package/src/cli/fleet-sync-report.ts +26 -0
- package/src/cli/fleet-sync-schedule.ts +138 -0
- package/src/cli/fleet-sync-vault.ts +166 -0
- package/src/cli/fleet-sync.ts +309 -0
- package/src/cli/fleet.ts +163 -0
- package/src/cli/managed-health.ts +219 -0
- package/src/cli/node-deployment.ts +34 -0
- package/src/cli/node-sync.ts +114 -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 +169 -0
- package/src/cli/register-fleet.ts +246 -0
- package/src/cli/registry.ts +158 -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 +89 -0
- package/src/cli/search.ts +35 -0
- package/src/cli/settings.ts +64 -0
- package/src/cli/source-operations.ts +78 -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 +260 -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 +88 -0
- package/src/index.ts +6 -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 +185 -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 +400 -0
- package/src/manager.ts +62 -0
- package/src/monitoring/apply.ts +171 -0
- package/src/monitoring/beszel.ts +191 -0
- package/src/monitoring/enroll.ts +110 -0
- package/src/monitoring/hub-access.ts +113 -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 +232 -0
- package/src/monitoring/remote.ts +65 -0
- package/src/network-definition.ts +83 -0
- package/src/node-converge-linux.ts +372 -0
- package/src/node-converge-shared.ts +83 -0
- package/src/node-converge.ts +40 -0
- package/src/onboard.ts +35 -0
- package/src/platform/ansible-receipt.ts +128 -0
- package/src/platform/ansible.ts +62 -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 +128 -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 +63 -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 +212 -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/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 +48 -0
- package/src/t3-linked-check.ts +101 -0
- package/src/tailnet-health.ts +231 -0
- package/src/tailnet-matching.ts +37 -0
- package/src/tailscale.ts +141 -0
- package/src/test-fixtures.ts +36 -0
- package/src/toolchain-checks.ts +83 -0
- package/src/topology.ts +24 -0
- package/src/transport.ts +122 -0
- package/src/types.ts +95 -0
- package/src/vault-config.ts +38 -0
- package/src/worker-access-checks.ts +115 -0
- package/src/worker-capacity.ts +34 -0
- package/src/worker-host-checks.ts +176 -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,127 @@
|
|
|
1
|
+
import { workerHealthText } from './worker-health-output';
|
|
2
|
+
import { fleetReference } from './fleet-reference';
|
|
3
|
+
import {
|
|
4
|
+
managedHealthScope,
|
|
5
|
+
appendManagerLinks,
|
|
6
|
+
appendDashboardEvidence,
|
|
7
|
+
} from './managed-health';
|
|
8
|
+
import { discoverTailnet } from '../tailscale';
|
|
9
|
+
import { tailnetHealth } from '../tailnet-health';
|
|
10
|
+
import { makeReport, renderReport, reportExitCode } from '../report';
|
|
11
|
+
import { readRoster } from './roster';
|
|
12
|
+
import { PUBLIC_HUB_URL } from '../monitoring/beszel';
|
|
13
|
+
import { stableUrls } from './fleet-sync-output';
|
|
14
|
+
import { timeout } from './options';
|
|
15
|
+
import type { FleetServices } from './fleet';
|
|
16
|
+
import type { Settings } from './settings';
|
|
17
|
+
import type { CliOptions } from './options';
|
|
18
|
+
import type { CommandOutcome } from './contracts';
|
|
19
|
+
|
|
20
|
+
export async function executeTailnet(
|
|
21
|
+
command: string,
|
|
22
|
+
options: CliOptions,
|
|
23
|
+
settings: Settings,
|
|
24
|
+
services: FleetServices
|
|
25
|
+
): Promise<CommandOutcome> {
|
|
26
|
+
const { tailnet, discovered, registered, coverage } = await healthContext(
|
|
27
|
+
command,
|
|
28
|
+
options,
|
|
29
|
+
settings,
|
|
30
|
+
services
|
|
31
|
+
);
|
|
32
|
+
if (command === 'discover') {
|
|
33
|
+
const ok = tailnet.checks.every((check) => check.status === 'pass');
|
|
34
|
+
return {
|
|
35
|
+
exitCode: ok ? 0 : 1,
|
|
36
|
+
data: {
|
|
37
|
+
version: 1,
|
|
38
|
+
command,
|
|
39
|
+
ok,
|
|
40
|
+
coverage,
|
|
41
|
+
computers: tailnet.computers,
|
|
42
|
+
checks: tailnet.checks,
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const nodes = await tailnetHealth(
|
|
47
|
+
tailnet,
|
|
48
|
+
registered,
|
|
49
|
+
{
|
|
50
|
+
...(options.workers ? { reference: fleetReference() } : {}),
|
|
51
|
+
includeManagerReport: options.workers !== true,
|
|
52
|
+
root: settings.workspace,
|
|
53
|
+
timeoutMs: timeout({ ...options, timeout: options.timeout ?? '300000' }),
|
|
54
|
+
source: services.source,
|
|
55
|
+
},
|
|
56
|
+
services.run
|
|
57
|
+
);
|
|
58
|
+
if (options.managed)
|
|
59
|
+
await appendManagerLinks(nodes, registered, services.run);
|
|
60
|
+
// The hub runs on the manager, which worker scope deliberately filters out of
|
|
61
|
+
// `tailnet`; its visibility comes from the unfiltered discovery.
|
|
62
|
+
const dashboard = options.workers
|
|
63
|
+
? await appendDashboardEvidence(nodes, discovered)
|
|
64
|
+
: null;
|
|
65
|
+
const report = makeReport('health', nodes);
|
|
66
|
+
const exitCode = reportExitCode(report, true);
|
|
67
|
+
const verified = exitCode === 0;
|
|
68
|
+
return {
|
|
69
|
+
exitCode,
|
|
70
|
+
data: {
|
|
71
|
+
...report,
|
|
72
|
+
ok: verified,
|
|
73
|
+
...(options.workers ? { dashboardUrl: publicDashboard(dashboard) } : {}),
|
|
74
|
+
coverage: {
|
|
75
|
+
...coverage,
|
|
76
|
+
verified,
|
|
77
|
+
inspected: nodes.filter((node) => node.snapshot).length,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
text: options.workers
|
|
81
|
+
? workerHealthText(
|
|
82
|
+
{ ...report, ok: verified },
|
|
83
|
+
coverage,
|
|
84
|
+
publicDashboard(dashboard)
|
|
85
|
+
)
|
|
86
|
+
: `${stableUrls(renderReport({ ...report, ok: verified }))}\nCoverage: ${coverage.computers} visible computers; ${coverage.excludedMobile} mobile devices excluded.\n${coverage.limitation}`,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Shown to people: the stable public name, never the tailnet address. */
|
|
91
|
+
function publicDashboard(privateHub: string | null): string | null {
|
|
92
|
+
return privateHub ? PUBLIC_HUB_URL : null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function healthContext(
|
|
96
|
+
command: string,
|
|
97
|
+
options: CliOptions,
|
|
98
|
+
settings: Settings,
|
|
99
|
+
services: FleetServices
|
|
100
|
+
) {
|
|
101
|
+
const discovered = await discoverTailnet(services.run);
|
|
102
|
+
const roster =
|
|
103
|
+
command === 'health' ? await readRoster(settings.config) : null;
|
|
104
|
+
const registered = roster === null ? [] : roster.nodes;
|
|
105
|
+
const managed = options.managed
|
|
106
|
+
? managedHealthScope(discovered, registered, options.workers !== true)
|
|
107
|
+
: null;
|
|
108
|
+
const tailnet = managed === null ? discovered : managed.tailnet;
|
|
109
|
+
const coverage = {
|
|
110
|
+
scope: options.workers
|
|
111
|
+
? 'canonical-workers'
|
|
112
|
+
: options.managed
|
|
113
|
+
? 'canonical-fleet-and-manager'
|
|
114
|
+
: 'tailscale-visible-computers',
|
|
115
|
+
excludedUnmanaged: managed === null ? [] : managed.excluded,
|
|
116
|
+
computers: tailnet.computers.length,
|
|
117
|
+
excludedMobile: tailnet.excludedMobile,
|
|
118
|
+
limitation:
|
|
119
|
+
'This manager can inspect only devices visible to its Tailscale account/ACLs. Other tailnets, hidden devices and all-pairs connectivity are not verified.',
|
|
120
|
+
};
|
|
121
|
+
return {
|
|
122
|
+
tailnet,
|
|
123
|
+
discovered,
|
|
124
|
+
registered: managed === null ? registered : managed.nodes,
|
|
125
|
+
coverage,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { CommandOutcome, CommandSpec } from './contracts';
|
|
2
|
+
import { execute, showOutcome, successful } from './ui-execute';
|
|
3
|
+
import { clean, reportChecks, reproduction, record } from './ui-data';
|
|
4
|
+
import { inspectChecks, inspectDetails } from './ui-reports';
|
|
5
|
+
import { chooseTargets } from './ui-targets';
|
|
6
|
+
import type { Choice, Session } from './ui-types';
|
|
7
|
+
|
|
8
|
+
export async function runCommand(
|
|
9
|
+
session: Session,
|
|
10
|
+
command: CommandSpec
|
|
11
|
+
): Promise<void> {
|
|
12
|
+
session.display.panel(clean(command.title), clean(command.description));
|
|
13
|
+
const args = await actionArgs(session, command);
|
|
14
|
+
if (!args) return;
|
|
15
|
+
const preview = isRepair(args);
|
|
16
|
+
const mutation = !preview && (command.mutation || args.includes('--apply'));
|
|
17
|
+
if (mutation && !(await confirmMutation(session, args))) return;
|
|
18
|
+
await runWithResults(session, args, command.title, { preview, mutation });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isRepair(args: string[]): boolean {
|
|
22
|
+
return args[0] === 'fleet-repair' && !args.includes('--list');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Palette entries with flags are fixed scopes; bare roster commands ask for targets. */
|
|
26
|
+
function targetAction(args: string[]): string | undefined {
|
|
27
|
+
if (isRepair(args)) return 'repair';
|
|
28
|
+
if (args[0] === 'fleet-status' && args.length === 1) return 'doctor';
|
|
29
|
+
if (args[0] === 'fleet-nodes' && args.includes('--onboard')) return 'onboard';
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function actionArgs(
|
|
34
|
+
session: Session,
|
|
35
|
+
command: CommandSpec
|
|
36
|
+
): Promise<string[] | undefined> {
|
|
37
|
+
const args = [...command.args];
|
|
38
|
+
const action = targetAction(args);
|
|
39
|
+
if (!action) return args;
|
|
40
|
+
const targets = await chooseTargets(session, action);
|
|
41
|
+
return targets ? [...args, ...targets] : undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function runWithResults(
|
|
45
|
+
session: Session,
|
|
46
|
+
initialArgs: string[],
|
|
47
|
+
title: string,
|
|
48
|
+
settings: { preview?: boolean; mutation?: boolean } = {}
|
|
49
|
+
): Promise<void> {
|
|
50
|
+
let args = initialArgs;
|
|
51
|
+
let outcome = await execute(session, args, title);
|
|
52
|
+
let preview = settings.preview === true;
|
|
53
|
+
let mutation = settings.mutation === true;
|
|
54
|
+
while (true) {
|
|
55
|
+
showOutcome(session, outcome);
|
|
56
|
+
const selection = await session.prompts.select(
|
|
57
|
+
'Next step',
|
|
58
|
+
resultChoices(outcome, preview)
|
|
59
|
+
);
|
|
60
|
+
if (selection === 'back') return;
|
|
61
|
+
await inspectSelection(session, outcome, selection);
|
|
62
|
+
if (selection === 'apply') {
|
|
63
|
+
const appliedArgs = applyArgs(args, outcome);
|
|
64
|
+
if (!(await confirmMutation(session, appliedArgs))) continue;
|
|
65
|
+
args = appliedArgs;
|
|
66
|
+
preview = false;
|
|
67
|
+
mutation = true;
|
|
68
|
+
outcome = await execute(session, args, title);
|
|
69
|
+
}
|
|
70
|
+
if (selection === 'retry') {
|
|
71
|
+
if (mutation && !(await confirmMutation(session, args))) continue;
|
|
72
|
+
outcome = await execute(session, args, title);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function inspectSelection(
|
|
78
|
+
session: Session,
|
|
79
|
+
outcome: CommandOutcome,
|
|
80
|
+
selection: string
|
|
81
|
+
): Promise<void> {
|
|
82
|
+
if (selection === 'checks') await inspectChecks(session, outcome.data);
|
|
83
|
+
if (selection === 'details') await inspectDetails(session, outcome.data);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function resultChoices(outcome: CommandOutcome, preview: boolean): Choice[] {
|
|
87
|
+
return [
|
|
88
|
+
{ name: 'Back to commands', value: 'back' },
|
|
89
|
+
...(reportChecks(outcome.data).length
|
|
90
|
+
? [{ name: 'Explore checks and remedies…', value: 'checks' }]
|
|
91
|
+
: []),
|
|
92
|
+
{ name: 'Inspect structured details / plan…', value: 'details' },
|
|
93
|
+
...(preview && successful(outcome)
|
|
94
|
+
? [{ name: 'Review and explicitly apply…', value: 'apply' }]
|
|
95
|
+
: []),
|
|
96
|
+
...(!successful(outcome)
|
|
97
|
+
? [{ name: 'Retry command…', value: 'retry' }]
|
|
98
|
+
: []),
|
|
99
|
+
];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function confirmMutation(
|
|
103
|
+
session: Session,
|
|
104
|
+
args: string[]
|
|
105
|
+
): Promise<boolean> {
|
|
106
|
+
const action = args[0];
|
|
107
|
+
const warning =
|
|
108
|
+
action === 'fleet-repair'
|
|
109
|
+
? 'Repair runs the eligible owner actions in the displayed plan, including trusted setup/install scripts. Workstation actions may read this machine’s Vault session and write managed 0600 provider configuration. No login session is transferred between computers. Failures can leave partial changes; fresh diagnostics follow.'
|
|
110
|
+
: action === 'fleet-node-add'
|
|
111
|
+
? 'This writes the local roster only. No cloud resources, SSH configuration, credentials or host keys are changed.'
|
|
112
|
+
: 'This command applies changes on invocation as documented in its --help. Failure can leave partial changes; inspect before retrying. No credentials are copied between computers.';
|
|
113
|
+
session.display.panel(
|
|
114
|
+
'Explicit approval required',
|
|
115
|
+
[
|
|
116
|
+
warning,
|
|
117
|
+
`Workspace: ${clean(session.workspace ?? session.options.workspace)}`,
|
|
118
|
+
`Config: ${clean(session.config ?? session.options.config)}`,
|
|
119
|
+
`$ ${reproduction(session, args)}`,
|
|
120
|
+
].join('\n')
|
|
121
|
+
);
|
|
122
|
+
return session.prompts.confirm('Apply this exact command? (default: no)');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function applyArgs(args: string[], outcome: CommandOutcome): string[] {
|
|
126
|
+
const guard =
|
|
127
|
+
isRepair(args) &&
|
|
128
|
+
record(outcome.data) &&
|
|
129
|
+
typeof outcome.data['planHash'] === 'string'
|
|
130
|
+
? ['--plan-hash', outcome.data['planHash']]
|
|
131
|
+
: [];
|
|
132
|
+
return [...args, ...guard, '--apply', '--yes'];
|
|
133
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { InteractiveOptions } from './contracts';
|
|
2
|
+
import { releaseLabel } from './program';
|
|
3
|
+
import { runCommand } from './ui-actions';
|
|
4
|
+
import { clean, commandChoices, isCancellation } from './ui-data';
|
|
5
|
+
import { addNode } from './ui-wizard';
|
|
6
|
+
import type { Choice, Display, Prompts, Session } from './ui-types';
|
|
7
|
+
|
|
8
|
+
export async function runInteractive(
|
|
9
|
+
options: InteractiveOptions,
|
|
10
|
+
prompts: Prompts,
|
|
11
|
+
display: Display
|
|
12
|
+
): Promise<void> {
|
|
13
|
+
const session: Session = { options, prompts, display };
|
|
14
|
+
showHeader(session);
|
|
15
|
+
let running = true;
|
|
16
|
+
while (running) {
|
|
17
|
+
try {
|
|
18
|
+
const choice = await prompts.search(
|
|
19
|
+
'Command palette · type to search · arrows + enter',
|
|
20
|
+
(term) => paletteChoices(session, term)
|
|
21
|
+
);
|
|
22
|
+
running = await dispatch(session, choice);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
if (isCancellation(error)) {
|
|
25
|
+
display.write('\nGoodbye. No further actions will run.');
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
display.write(
|
|
29
|
+
'The interactive session could not continue safely. Use personal commands --json for noninteractive help.'
|
|
30
|
+
);
|
|
31
|
+
throw new Error('Interactive session failed safely.');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
display.write('Goodbye. Session context was not saved.');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function showHeader(session: Session): void {
|
|
38
|
+
session.display.panel(
|
|
39
|
+
`personal · ${clean(releaseLabel(session.options))}`,
|
|
40
|
+
[
|
|
41
|
+
'Your developer fleet, without the guesswork.',
|
|
42
|
+
`Workspace: ${clean(session.workspace ?? session.options.workspace)}`,
|
|
43
|
+
`Config: ${clean(session.config ?? session.options.config)}`,
|
|
44
|
+
'Read-only first · explicit writes · no automatic repairs',
|
|
45
|
+
'Search IDs, keywords or descriptions. Ctrl+C exits safely. Nothing is cleared.',
|
|
46
|
+
].join('\n')
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function paletteChoices(session: Session, term: string): Choice[] {
|
|
51
|
+
const commands = commandChoices(session.options.commands, term);
|
|
52
|
+
const navigation = [
|
|
53
|
+
{
|
|
54
|
+
name: 'Session: Workspace / config…',
|
|
55
|
+
value: 'context',
|
|
56
|
+
description: 'Session-only overrides; no hidden configuration writes.',
|
|
57
|
+
},
|
|
58
|
+
{ name: 'Exit', value: 'exit', description: 'Close the command palette.' },
|
|
59
|
+
].filter((choice) =>
|
|
60
|
+
`${choice.name} ${choice.description}`
|
|
61
|
+
.toLowerCase()
|
|
62
|
+
.includes(term.toLowerCase().trim())
|
|
63
|
+
);
|
|
64
|
+
const choices = [...navigation, ...commands].sort((a, b) =>
|
|
65
|
+
a.name.localeCompare(b.name, 'en')
|
|
66
|
+
);
|
|
67
|
+
return choices.length
|
|
68
|
+
? choices
|
|
69
|
+
: [{ name: 'No matching commands — change search', value: 'none' }];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function dispatch(session: Session, choice: string): Promise<boolean> {
|
|
73
|
+
if (choice === 'exit') return false;
|
|
74
|
+
if (choice === 'context') await changeContext(session);
|
|
75
|
+
if (choice.startsWith('command:'))
|
|
76
|
+
await openCommand(session, choice.slice(8));
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function openCommand(session: Session, id: string): Promise<void> {
|
|
81
|
+
const command = session.options.commands.find((entry) => entry.id === id);
|
|
82
|
+
if (!command) return;
|
|
83
|
+
if (command.id === 'fleet-node-add') await addNode(session);
|
|
84
|
+
else await runCommand(session, command);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function changeContext(session: Session): Promise<void> {
|
|
88
|
+
session.display.write(
|
|
89
|
+
'Session only. Blank inputs reset to startup defaults; no file is written.'
|
|
90
|
+
);
|
|
91
|
+
const workspace = await session.prompts.input(
|
|
92
|
+
'Workspace override',
|
|
93
|
+
session.workspace ?? '',
|
|
94
|
+
validContext
|
|
95
|
+
);
|
|
96
|
+
const config = await session.prompts.input(
|
|
97
|
+
'Config override',
|
|
98
|
+
session.config ?? '',
|
|
99
|
+
validContext
|
|
100
|
+
);
|
|
101
|
+
if (workspace) session.workspace = workspace;
|
|
102
|
+
else delete session.workspace;
|
|
103
|
+
if (config) session.config = config;
|
|
104
|
+
else delete session.config;
|
|
105
|
+
showHeader(session);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function validContext(value: string): boolean | string {
|
|
109
|
+
return (
|
|
110
|
+
!/[\x00-\x1f\x7f]/.test(value) ||
|
|
111
|
+
'Control characters are not allowed in paths.'
|
|
112
|
+
);
|
|
113
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { CommandSpec } from './contracts';
|
|
2
|
+
import type { CheckRow, Choice, Session } from './ui-types';
|
|
3
|
+
import { searchCommands } from './search';
|
|
4
|
+
|
|
5
|
+
export function record(value: unknown): value is Record<string, unknown> {
|
|
6
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function clean(value: string, limit = 2000): string {
|
|
10
|
+
return value
|
|
11
|
+
.replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, '')
|
|
12
|
+
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, '')
|
|
13
|
+
.replace(/[\x00-\x1f\x7f-\x9f\u202a-\u202e\u2066-\u2069]/g, ' ')
|
|
14
|
+
.slice(0, limit);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function text(value: unknown, fallback = ''): string {
|
|
18
|
+
return typeof value === 'string' ? clean(value) : fallback;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function quote(value: string): string {
|
|
22
|
+
return /^[a-zA-Z0-9_./:=,@-]+$/.test(value)
|
|
23
|
+
? value
|
|
24
|
+
: `'${value.replaceAll("'", "'\\''")}'`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function callArgs(session: Session, args: string[]): string[] {
|
|
28
|
+
return [
|
|
29
|
+
...args,
|
|
30
|
+
...(session.workspace ? ['--workspace', session.workspace] : []),
|
|
31
|
+
...(session.config ? ['--config', session.config] : []),
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function reproduction(session: Session, args: string[]): string {
|
|
36
|
+
return [
|
|
37
|
+
'personal',
|
|
38
|
+
...args,
|
|
39
|
+
'--workspace',
|
|
40
|
+
session.workspace ?? session.options.workspace,
|
|
41
|
+
'--config',
|
|
42
|
+
session.config ?? session.options.config,
|
|
43
|
+
'--json',
|
|
44
|
+
]
|
|
45
|
+
.map(quote)
|
|
46
|
+
.join(' ');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function commandChoices(
|
|
50
|
+
commands: CommandSpec[],
|
|
51
|
+
term: string
|
|
52
|
+
): Choice[] {
|
|
53
|
+
return searchCommands(commands, term).map((command) => ({
|
|
54
|
+
name: `${clean(command.group)}: ${clean(command.title)}`,
|
|
55
|
+
value: `command:${command.id}`,
|
|
56
|
+
description: `${clean(command.description)}${command.mutation ? ' · confirmation required' : ''}`,
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function reportChecks(data: unknown): CheckRow[] {
|
|
61
|
+
if (!record(data)) return [];
|
|
62
|
+
const nodes = Array.isArray(data['nodes']) ? data['nodes'] : [];
|
|
63
|
+
return [
|
|
64
|
+
...nodes.flatMap(nodeChecks),
|
|
65
|
+
...nodeChecks({ node: 'tailscale', checks: data['checks'] }),
|
|
66
|
+
];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function nodeChecks(value: unknown): CheckRow[] {
|
|
70
|
+
if (!record(value) || !Array.isArray(value['checks'])) return [];
|
|
71
|
+
const node = text(value['node'], 'manager');
|
|
72
|
+
return value['checks'].flatMap((check: unknown) => {
|
|
73
|
+
if (!record(check) || !isStatus(check['status'])) return [];
|
|
74
|
+
const remedy = record(check['remedy']) ? check['remedy'] : {};
|
|
75
|
+
return [
|
|
76
|
+
{
|
|
77
|
+
node,
|
|
78
|
+
id: text(check['id'], 'check'),
|
|
79
|
+
status: check['status'],
|
|
80
|
+
summary: text(check['summary']),
|
|
81
|
+
remedy: text(remedy['action']),
|
|
82
|
+
command: text(remedy['command']),
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function isStatus(value: unknown): value is string {
|
|
89
|
+
return ['pass', 'warn', 'fail', 'skip'].includes(String(value));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function isCancellation(error: unknown): boolean {
|
|
93
|
+
if (!record(error)) return false;
|
|
94
|
+
return ['ExitPromptError', 'AbortPromptError', 'AbortError'].includes(
|
|
95
|
+
text(error['name'])
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function absolutePath(value: string): boolean | string {
|
|
100
|
+
return (
|
|
101
|
+
(value.startsWith('/') &&
|
|
102
|
+
value.length <= 4096 &&
|
|
103
|
+
!/[\x00-\x1f\x7f]/.test(value)) ||
|
|
104
|
+
'Enter an absolute POSIX path (no ~ or control characters).'
|
|
105
|
+
);
|
|
106
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type { CommandOutcome } from './contracts';
|
|
2
|
+
import { showRepairs } from './ui-repairs';
|
|
3
|
+
import {
|
|
4
|
+
callArgs,
|
|
5
|
+
clean,
|
|
6
|
+
text,
|
|
7
|
+
isCancellation,
|
|
8
|
+
record,
|
|
9
|
+
reproduction,
|
|
10
|
+
reportChecks,
|
|
11
|
+
} from './ui-data';
|
|
12
|
+
import type { Session } from './ui-types';
|
|
13
|
+
|
|
14
|
+
export async function execute(
|
|
15
|
+
session: Session,
|
|
16
|
+
args: string[],
|
|
17
|
+
title: string
|
|
18
|
+
): Promise<CommandOutcome> {
|
|
19
|
+
session.display.write(`\n$ ${reproduction(session, args)}`);
|
|
20
|
+
const progress = session.display.progress(title);
|
|
21
|
+
try {
|
|
22
|
+
return await session.options.execute(callArgs(session, args));
|
|
23
|
+
} catch (error) {
|
|
24
|
+
if (isCancellation(error)) throw error;
|
|
25
|
+
return { exitCode: 1, data: { ok: false, code: 'operation_failed' } };
|
|
26
|
+
} finally {
|
|
27
|
+
progress.stop();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function successful(outcome: CommandOutcome): boolean {
|
|
32
|
+
return (
|
|
33
|
+
outcome.exitCode === 0 &&
|
|
34
|
+
!(record(outcome.data) && outcome.data['ok'] === false)
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function showOutcome(session: Session, outcome: CommandOutcome): void {
|
|
39
|
+
const checks = reportChecks(outcome.data);
|
|
40
|
+
const incomplete = checks.filter((check) =>
|
|
41
|
+
['warn', 'skip'].includes(check.status)
|
|
42
|
+
).length;
|
|
43
|
+
const state = outcomeState(outcome, incomplete);
|
|
44
|
+
const counts = ['pass', 'warn', 'fail', 'skip'].map(
|
|
45
|
+
(status) =>
|
|
46
|
+
`${checks.filter((check) => check.status === status).length} ${status}`
|
|
47
|
+
);
|
|
48
|
+
session.display.panel(
|
|
49
|
+
state,
|
|
50
|
+
checks.length ? counts.join(' · ') : outcomeMessage(outcome)
|
|
51
|
+
);
|
|
52
|
+
showAttention(session, checks);
|
|
53
|
+
showData(session, outcome);
|
|
54
|
+
if (!successful(outcome))
|
|
55
|
+
session.display.write(
|
|
56
|
+
record(outcome.data) && outcome.data['applied'] === true
|
|
57
|
+
? 'Actions were attempted. Review execution results, partial changes and fresh checks before retrying.'
|
|
58
|
+
: 'No automatic repair was attempted. Review the command, context and setup.'
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function outcomeMessage(outcome: CommandOutcome): string {
|
|
63
|
+
if (!successful(outcome)) return failureMessage(outcome.data);
|
|
64
|
+
if (record(outcome.data) && outcome.data['applied'] === false) {
|
|
65
|
+
return 'Preview only — no changes applied. Review before explicitly authorizing a write.';
|
|
66
|
+
}
|
|
67
|
+
return 'Command completed. Warnings and skipped checks are not passes.';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function failureMessage(data: unknown): string {
|
|
71
|
+
if (!record(data))
|
|
72
|
+
return 'Operation failed. Raw errors and subprocess output are intentionally hidden.';
|
|
73
|
+
if (data['code'] === 'invalid_usage' && typeof data['error'] === 'string')
|
|
74
|
+
return clean(data['error']);
|
|
75
|
+
const messages: Record<string, string> = {
|
|
76
|
+
invalid_usage:
|
|
77
|
+
'Configuration or arguments need attention. Run fleet-nodes --schema to check the roster, or pass --config.',
|
|
78
|
+
config_missing:
|
|
79
|
+
'No roster found. Run fleet-node-add, or run manager-only diagnostics with fleet-status --local.',
|
|
80
|
+
operation_failed:
|
|
81
|
+
'Operation failed safely. Check connectivity and configuration before retrying.',
|
|
82
|
+
};
|
|
83
|
+
return (
|
|
84
|
+
messages[String(data['code'])] ??
|
|
85
|
+
'Operation did not complete. Review setup and the noninteractive command.'
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function showNodes(session: Session, data: Record<string, unknown>): void {
|
|
90
|
+
const nodes = Array.isArray(data['nodes'])
|
|
91
|
+
? data['nodes'].filter(record)
|
|
92
|
+
: [];
|
|
93
|
+
if (!nodes.length) {
|
|
94
|
+
session.display.write(
|
|
95
|
+
'No computers registered yet. Run fleet-node-add to add one, or fleet-discover to list Tailscale computers.'
|
|
96
|
+
);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
session.display.table(
|
|
100
|
+
['Computer', 'SSH', 'Workspace'],
|
|
101
|
+
nodes.map((node) => [
|
|
102
|
+
text(node['id']),
|
|
103
|
+
text(node['ssh']),
|
|
104
|
+
text(node['workspace']),
|
|
105
|
+
])
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function showComputers(session: Session, data: Record<string, unknown>): void {
|
|
110
|
+
const computers = Array.isArray(data['computers'])
|
|
111
|
+
? data['computers'].filter(record)
|
|
112
|
+
: [];
|
|
113
|
+
session.display.table(
|
|
114
|
+
['Computer', 'OS', 'Online'],
|
|
115
|
+
computers.map((peer) => [
|
|
116
|
+
text(peer['dns']),
|
|
117
|
+
text(peer['os']),
|
|
118
|
+
peer['online'] === true ? 'yes' : 'no',
|
|
119
|
+
])
|
|
120
|
+
);
|
|
121
|
+
session.display.write(
|
|
122
|
+
'Visible Tailscale computers only; mobile devices excluded. Register each remote computer with its existing SSH alias and checkout path before full health checks.'
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function showAttention(
|
|
127
|
+
session: Session,
|
|
128
|
+
checks: ReturnType<typeof reportChecks>
|
|
129
|
+
): void {
|
|
130
|
+
const attention = checks.filter((check) => check.status !== 'pass');
|
|
131
|
+
if (!attention.length) return;
|
|
132
|
+
session.display.table(
|
|
133
|
+
['Computer', 'Check', 'Status / next step'],
|
|
134
|
+
attention
|
|
135
|
+
.slice(0, 15)
|
|
136
|
+
.map((check) => [
|
|
137
|
+
check.node,
|
|
138
|
+
check.id,
|
|
139
|
+
`${check.status}: ${check.remedy ?? check.summary}`,
|
|
140
|
+
])
|
|
141
|
+
);
|
|
142
|
+
if (attention.length > 15)
|
|
143
|
+
session.display.write(
|
|
144
|
+
`Showing 15 of ${attention.length} checks needing attention. Explore checks and remedies for the complete report.`
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function showData(session: Session, outcome: CommandOutcome): void {
|
|
149
|
+
if (!record(outcome.data)) return;
|
|
150
|
+
if (['repair', 'repairs'].includes(String(outcome.data['command'])))
|
|
151
|
+
showRepairs(session, outcome.data);
|
|
152
|
+
if (successful(outcome) && outcome.data['command'] === 'nodes')
|
|
153
|
+
showNodes(session, outcome.data);
|
|
154
|
+
if (outcome.data['command'] === 'discover')
|
|
155
|
+
showComputers(session, outcome.data);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function outcomeState(outcome: CommandOutcome, incomplete: number): string {
|
|
159
|
+
if (record(outcome.data) && outcome.data['applied'] === false)
|
|
160
|
+
return 'PREVIEW';
|
|
161
|
+
if (!successful(outcome)) return 'FAILED';
|
|
162
|
+
return incomplete ? 'WARNING' : 'SUCCESS';
|
|
163
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { record, text } from './ui-data';
|
|
2
|
+
import type { Session } from './ui-types';
|
|
3
|
+
|
|
4
|
+
export function showRepairs(
|
|
5
|
+
session: Session,
|
|
6
|
+
data: Record<string, unknown>
|
|
7
|
+
): void {
|
|
8
|
+
const catalog = data['command'] === 'repairs';
|
|
9
|
+
const entries = data[catalog ? 'recipes' : 'steps'];
|
|
10
|
+
if (!Array.isArray(entries)) return;
|
|
11
|
+
const steps = entries.filter(record);
|
|
12
|
+
session.display.table(
|
|
13
|
+
['Computer / owner', 'Repair', 'Execution'],
|
|
14
|
+
steps
|
|
15
|
+
.slice(0, 15)
|
|
16
|
+
.map((step) => [
|
|
17
|
+
text(step[catalog ? 'owner' : 'node']),
|
|
18
|
+
text(step['title']),
|
|
19
|
+
step['automatic'] === true && (catalog || step['eligible'] === true)
|
|
20
|
+
? 'Owner command after explicit apply'
|
|
21
|
+
: 'Operator action; inspect instructions',
|
|
22
|
+
])
|
|
23
|
+
);
|
|
24
|
+
if (steps.length > 15)
|
|
25
|
+
session.display.write(
|
|
26
|
+
`Showing 15 of ${steps.length} repair entries. Inspect structured details for the complete plan and native login commands.`
|
|
27
|
+
);
|
|
28
|
+
if (!catalog)
|
|
29
|
+
session.display.write(
|
|
30
|
+
data['applied'] === true
|
|
31
|
+
? `Owner commands completed: ${text(data['completed'])}. Fresh health verified: ${data['healthOk'] === true ? 'yes' : 'no; review remaining checks'}.`
|
|
32
|
+
: 'Preview only. Review the repair effects and native login instructions in structured details before applying.'
|
|
33
|
+
);
|
|
34
|
+
}
|