@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,375 @@
|
|
|
1
|
+
import { localCheckoutRevision } from '../machine/local-source';
|
|
2
|
+
import {
|
|
3
|
+
definitionSource,
|
|
4
|
+
fleet as canonicalDefinition,
|
|
5
|
+
reloadFleet,
|
|
6
|
+
} from '../fleet-config';
|
|
7
|
+
import { managerFailure } from '../manager-failure';
|
|
8
|
+
import { managerText, stdoutColor } from './fleet-sync-output';
|
|
9
|
+
import { managerSchedule, type ScheduleAction } from './fleet-sync-schedule';
|
|
10
|
+
import { enrollManager } from './fleet-sync-enrollment';
|
|
11
|
+
import { authorizeManager } from './fleet-sync-vault';
|
|
12
|
+
import { homedir } from 'node:os';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { existsSync, renameSync } from 'node:fs';
|
|
15
|
+
import {
|
|
16
|
+
isRevision,
|
|
17
|
+
withMachineLock,
|
|
18
|
+
ensurePrivateRoot,
|
|
19
|
+
type Enrollment,
|
|
20
|
+
} from '../machine/state';
|
|
21
|
+
import { loadEnrollment } from '../state/enrollment';
|
|
22
|
+
import { legacyHome } from '../state/paths';
|
|
23
|
+
import {
|
|
24
|
+
DIVERGED_REMEDY,
|
|
25
|
+
refresh,
|
|
26
|
+
stateContext,
|
|
27
|
+
type Standing,
|
|
28
|
+
} from '../state/sync';
|
|
29
|
+
import { runRecord } from './fleet-sync-state';
|
|
30
|
+
import { runCommand } from '../runner';
|
|
31
|
+
import type { Runner } from '../types';
|
|
32
|
+
import type { Settings } from './settings';
|
|
33
|
+
import type { CommandOutcome } from './contracts';
|
|
34
|
+
import { FleetUsageError } from '../errors';
|
|
35
|
+
import { FleetLockError } from '../machine/lock-error';
|
|
36
|
+
import { selected } from './fleet-sync-remote';
|
|
37
|
+
import { managerIdentity } from './fleet-sync-manager';
|
|
38
|
+
import { managerRunScript, rehome } from './fleet-sync-checkout';
|
|
39
|
+
import { packageInfo } from '../package-info';
|
|
40
|
+
import {
|
|
41
|
+
forwardingDisabled,
|
|
42
|
+
forwardToManager,
|
|
43
|
+
type ForwardSpawn,
|
|
44
|
+
} from './fleet-sync-forward';
|
|
45
|
+
|
|
46
|
+
export interface FleetSyncOptions {
|
|
47
|
+
revision?: string;
|
|
48
|
+
dryRun?: boolean;
|
|
49
|
+
apply?: boolean;
|
|
50
|
+
yes?: boolean;
|
|
51
|
+
json?: boolean;
|
|
52
|
+
nonInteractive?: boolean;
|
|
53
|
+
node?: string[];
|
|
54
|
+
all?: boolean;
|
|
55
|
+
schedule?: ScheduleAction;
|
|
56
|
+
/** Internal: this revision already converged the whole fleet, so no canary
|
|
57
|
+
* has to prove it first and every worker converges at once.
|
|
58
|
+
*/
|
|
59
|
+
rolledOut?: boolean;
|
|
60
|
+
/** Internal: set for interactive terminal runs (sudo enrollment prompts). */
|
|
61
|
+
interactiveAuthorization?: boolean;
|
|
62
|
+
expectedPlanFingerprint?: string;
|
|
63
|
+
}
|
|
64
|
+
export function machineRoot(): string {
|
|
65
|
+
const root = join(homedir(), '.local/share/personal/fleet/manager');
|
|
66
|
+
// One-time move from the pre-topology name; keeps the approved revision.
|
|
67
|
+
const legacy = join(homedir(), '.local/share/personal/fleet/controller');
|
|
68
|
+
if (!existsSync(root) && existsSync(legacy)) renameSync(legacy, root);
|
|
69
|
+
return root;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Public contract v3: the manager converges itself, then every enrolled worker. */
|
|
73
|
+
export async function fleetSync(
|
|
74
|
+
options: FleetSyncOptions,
|
|
75
|
+
settings: Settings,
|
|
76
|
+
run: Runner = runCommand,
|
|
77
|
+
forward?: ForwardSpawn
|
|
78
|
+
): Promise<CommandOutcome> {
|
|
79
|
+
validate(options);
|
|
80
|
+
if (!(await onManager(run)))
|
|
81
|
+
return offManager(options, settings, run, forward);
|
|
82
|
+
if (options.schedule === 'status') return scheduleStatus(settings, run);
|
|
83
|
+
return managerSync(options, settings, {
|
|
84
|
+
run,
|
|
85
|
+
root: machineRoot(),
|
|
86
|
+
restart: () => fleetSync(options, settings, run, forward),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface ManagerSync {
|
|
91
|
+
run: Runner;
|
|
92
|
+
/** This machine's run lock and caches. */
|
|
93
|
+
root: string;
|
|
94
|
+
/** Starts the request over once the fleet state has been replaced. */
|
|
95
|
+
restart: () => Promise<CommandOutcome>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** fleet-sync on the machine already known to be the manager. */
|
|
99
|
+
export async function managerSync(
|
|
100
|
+
options: FleetSyncOptions,
|
|
101
|
+
settings: Settings,
|
|
102
|
+
{ run, root, restart }: ManagerSync
|
|
103
|
+
): Promise<CommandOutcome> {
|
|
104
|
+
await ensurePrivateRoot(root);
|
|
105
|
+
const state = await loadEnrollment(legacyHome() ? root : null);
|
|
106
|
+
const revision = await selectedRevision(
|
|
107
|
+
options.revision,
|
|
108
|
+
state?.approvedRevision,
|
|
109
|
+
settings,
|
|
110
|
+
run
|
|
111
|
+
);
|
|
112
|
+
if (!isRevision(revision))
|
|
113
|
+
return actionRequired(
|
|
114
|
+
'FLEET_REVISION_REQUIRED',
|
|
115
|
+
'No revision to converge to. Run the published dev-fleet package (it names the commit it was built from), or select a full commit SHA with --revision.'
|
|
116
|
+
);
|
|
117
|
+
if (options.schedule)
|
|
118
|
+
return scheduleRequest(options, revision, settings, run);
|
|
119
|
+
const rehomed = await rehome(
|
|
120
|
+
options,
|
|
121
|
+
{ revision, workspace: settings.workspace, root },
|
|
122
|
+
run
|
|
123
|
+
);
|
|
124
|
+
if (rehomed === 'unavailable')
|
|
125
|
+
return actionRequired(
|
|
126
|
+
'FLEET_SOURCE_UNAVAILABLE',
|
|
127
|
+
`Revision ${revision} could not be taken from ${canonicalDefinition.source.repository}, so nothing was changed. Check the network and Git access to it, and that the revision is pushed there; fleet-sync never converges from the package or working tree it was started in.`,
|
|
128
|
+
revision
|
|
129
|
+
);
|
|
130
|
+
if (rehomed !== 'here') return rehomed;
|
|
131
|
+
if (options.dryRun) return selected({ ...options, revision }, settings, run);
|
|
132
|
+
// The bucket is the source of truth: a run starts from its version of the
|
|
133
|
+
// configuration, the roster and the approval, not from a stale local copy.
|
|
134
|
+
const fresh = await refresh(stateContext(run));
|
|
135
|
+
if (fresh.standing === 'diverged')
|
|
136
|
+
return actionRequired('FLEET_STATE_DIVERGED', DIVERGED_REMEDY, revision);
|
|
137
|
+
if (fresh.replaced) {
|
|
138
|
+
// Everything read so far came from the stale copy, the manager's own
|
|
139
|
+
// identity included.
|
|
140
|
+
reloadFleet();
|
|
141
|
+
return restart();
|
|
142
|
+
}
|
|
143
|
+
return authorizeAndApply(root, state, options, {
|
|
144
|
+
settings,
|
|
145
|
+
run,
|
|
146
|
+
revision,
|
|
147
|
+
standing: fresh.standing,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
interface ApplyContext {
|
|
152
|
+
settings: Settings;
|
|
153
|
+
run: Runner;
|
|
154
|
+
revision: string;
|
|
155
|
+
/** How the fleet state stood with the bucket when this run started. */
|
|
156
|
+
standing: Standing;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Applies without a confirmation prompt; --dry-run is the preview. */
|
|
160
|
+
async function authorizeAndApply(
|
|
161
|
+
root: string,
|
|
162
|
+
state: Enrollment | null,
|
|
163
|
+
options: FleetSyncOptions,
|
|
164
|
+
context: ApplyContext
|
|
165
|
+
): Promise<CommandOutcome> {
|
|
166
|
+
const { settings, run, revision } = context;
|
|
167
|
+
const request = { ...options, revision, apply: true, yes: true };
|
|
168
|
+
if (!interactive(options)) return applyLocked(root, state, request, context);
|
|
169
|
+
await enrollManager(settings.config, run);
|
|
170
|
+
// Needed to hand each node its own Vault token; nodes report any gap.
|
|
171
|
+
await authorizeManager({ interactive: true }, run);
|
|
172
|
+
return applyLocked(
|
|
173
|
+
root,
|
|
174
|
+
state,
|
|
175
|
+
{ ...request, interactiveAuthorization: true },
|
|
176
|
+
context
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Records the approved revision, converges, records what was applied, then
|
|
181
|
+
* uploads the fleet state: the report says whether the bucket holds it.
|
|
182
|
+
*/
|
|
183
|
+
function applyLocked(
|
|
184
|
+
root: string,
|
|
185
|
+
state: Enrollment | null,
|
|
186
|
+
request: FleetSyncOptions & { revision: string },
|
|
187
|
+
context: ApplyContext
|
|
188
|
+
): Promise<CommandOutcome> {
|
|
189
|
+
const { settings, run, revision } = context;
|
|
190
|
+
const record = runRecord(state, revision, {
|
|
191
|
+
context: stateContext(run),
|
|
192
|
+
standing: context.standing,
|
|
193
|
+
...(definitionSource().kind === 'file'
|
|
194
|
+
? { definition: JSON.stringify(canonicalDefinition, null, 2) + '\n' }
|
|
195
|
+
: {}),
|
|
196
|
+
});
|
|
197
|
+
return withMachineLock(root, async () => {
|
|
198
|
+
record.approve();
|
|
199
|
+
try {
|
|
200
|
+
return await selected(
|
|
201
|
+
{ ...request, rolledOut: state?.appliedRevision === revision },
|
|
202
|
+
settings,
|
|
203
|
+
run,
|
|
204
|
+
(report) => record.settle(report.ok)
|
|
205
|
+
);
|
|
206
|
+
} finally {
|
|
207
|
+
await record.abandon();
|
|
208
|
+
}
|
|
209
|
+
}).catch((error: unknown) => {
|
|
210
|
+
if (error instanceof FleetLockError)
|
|
211
|
+
return actionRequired(
|
|
212
|
+
'FLEET_MANAGER_LOCKED',
|
|
213
|
+
'Another manager run or interrupted lock exists. Inspect manager and remote processes before removing an abandoned sync.lock; never steal active package-manager locks.'
|
|
214
|
+
);
|
|
215
|
+
throw error;
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
/** Every run (previews and schedules too) happens on the manager: a worker can
|
|
219
|
+
* neither converge the manager's singletons nor observe its T3 links, so a
|
|
220
|
+
* partial run there only produces misleading failures.
|
|
221
|
+
*/
|
|
222
|
+
async function onManager(run: Runner): Promise<boolean> {
|
|
223
|
+
const { manager } = canonicalDefinition;
|
|
224
|
+
return (await managerIdentity(manager.id, run)).status === 'pass';
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Hands the request to the manager; refuses (without touching any worker)
|
|
228
|
+
* when hand-off is disabled, e.g. on the manager side of a hand-off.
|
|
229
|
+
*/
|
|
230
|
+
async function offManager(
|
|
231
|
+
options: FleetSyncOptions,
|
|
232
|
+
settings: Settings,
|
|
233
|
+
run: Runner,
|
|
234
|
+
forward: ForwardSpawn | undefined
|
|
235
|
+
): Promise<CommandOutcome> {
|
|
236
|
+
const definition = canonicalDefinition;
|
|
237
|
+
const revision =
|
|
238
|
+
options.revision ??
|
|
239
|
+
packageRevision() ??
|
|
240
|
+
(await localCheckoutRevision(settings.workspace, run));
|
|
241
|
+
if (forwardingDisabled())
|
|
242
|
+
return actionRequired(
|
|
243
|
+
'FLEET_NOT_MANAGER',
|
|
244
|
+
`fleet-sync runs only on the manager (${definition.manager.id}); nothing was changed. Run it there, or from a worker that can SSH to the manager.`,
|
|
245
|
+
revision ?? null
|
|
246
|
+
);
|
|
247
|
+
const runScript = revision
|
|
248
|
+
? await managerRunScript(settings.workspace, revision, run)
|
|
249
|
+
: null;
|
|
250
|
+
return forwardToManager(definition, options, revision, {
|
|
251
|
+
spawn: forward,
|
|
252
|
+
script: runScript,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function scheduleStatus(
|
|
257
|
+
settings: Settings,
|
|
258
|
+
run: Runner
|
|
259
|
+
): Promise<CommandOutcome> {
|
|
260
|
+
return managerSchedule(
|
|
261
|
+
{
|
|
262
|
+
action: 'status',
|
|
263
|
+
revision: '',
|
|
264
|
+
config: settings.config,
|
|
265
|
+
authorized: false,
|
|
266
|
+
},
|
|
267
|
+
run
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function actionRequired(
|
|
272
|
+
code: string,
|
|
273
|
+
remedy: string,
|
|
274
|
+
revision: string | null = null
|
|
275
|
+
): CommandOutcome {
|
|
276
|
+
const report = managerFailure(canonicalDefinition, revision, code, remedy);
|
|
277
|
+
return {
|
|
278
|
+
exitCode: 1,
|
|
279
|
+
data: report,
|
|
280
|
+
text: managerText(report, { color: stdoutColor() }),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function validate(options: FleetSyncOptions): void {
|
|
285
|
+
validateSchedule(options.schedule);
|
|
286
|
+
if (options.all && options.node?.length)
|
|
287
|
+
throw new FleetUsageError('Choose --all or --node, not both.');
|
|
288
|
+
if (options.revision !== undefined && !isRevision(options.revision))
|
|
289
|
+
throw new FleetUsageError(
|
|
290
|
+
'--revision requires an exact lowercase 40-character commit SHA.'
|
|
291
|
+
);
|
|
292
|
+
if (options.dryRun && (options.apply || options.yes))
|
|
293
|
+
throw new FleetUsageError(
|
|
294
|
+
'--dry-run cannot be combined with apply confirmation.'
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
function interactive(options: FleetSyncOptions): boolean {
|
|
298
|
+
return (
|
|
299
|
+
![
|
|
300
|
+
options.dryRun,
|
|
301
|
+
options.apply,
|
|
302
|
+
options.yes,
|
|
303
|
+
options.json,
|
|
304
|
+
options.nonInteractive,
|
|
305
|
+
process.env['CI'],
|
|
306
|
+
].some(Boolean) && Boolean(process.stdin.isTTY && process.stderr.isTTY)
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** The commit an installed package was published from: the version of
|
|
311
|
+
* dev-fleet you run is the revision the fleet moves to.
|
|
312
|
+
*/
|
|
313
|
+
function packageRevision(): string | undefined {
|
|
314
|
+
return packageInfo().revision ?? undefined;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async function scheduleRequest(
|
|
318
|
+
options: FleetSyncOptions,
|
|
319
|
+
revision: string,
|
|
320
|
+
settings: Settings,
|
|
321
|
+
run: Runner
|
|
322
|
+
): Promise<CommandOutcome> {
|
|
323
|
+
const root = machineRoot();
|
|
324
|
+
|
|
325
|
+
if (options.dryRun)
|
|
326
|
+
return actionRequired(
|
|
327
|
+
'FLEET_SCHEDULE_PREVIEW',
|
|
328
|
+
'No schedule changed. Inspect --schedule status; scheduling writes require --apply --yes.'
|
|
329
|
+
);
|
|
330
|
+
if (!options.apply || !options.yes)
|
|
331
|
+
return managerSchedule(
|
|
332
|
+
{
|
|
333
|
+
action: options.schedule!,
|
|
334
|
+
revision,
|
|
335
|
+
config: settings.config,
|
|
336
|
+
authorized: false,
|
|
337
|
+
},
|
|
338
|
+
run
|
|
339
|
+
);
|
|
340
|
+
return withMachineLock(root, () =>
|
|
341
|
+
managerSchedule(
|
|
342
|
+
{
|
|
343
|
+
action: options.schedule!,
|
|
344
|
+
revision,
|
|
345
|
+
config: settings.config,
|
|
346
|
+
authorized: Boolean(options.apply && options.yes),
|
|
347
|
+
},
|
|
348
|
+
run
|
|
349
|
+
)
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function validateSchedule(schedule: unknown): void {
|
|
354
|
+
if (
|
|
355
|
+
schedule !== undefined &&
|
|
356
|
+
!['enable', 'pause', 'resume', 'disable', 'status'].includes(
|
|
357
|
+
String(schedule)
|
|
358
|
+
)
|
|
359
|
+
)
|
|
360
|
+
throw new FleetUsageError('Unknown manager schedule action.');
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async function selectedRevision(
|
|
364
|
+
explicit: string | undefined,
|
|
365
|
+
approved: string | undefined,
|
|
366
|
+
settings: Settings,
|
|
367
|
+
run: Runner
|
|
368
|
+
): Promise<string | undefined> {
|
|
369
|
+
return (
|
|
370
|
+
explicit ??
|
|
371
|
+
packageRevision() ??
|
|
372
|
+
(await localCheckoutRevision(settings.workspace, run)) ??
|
|
373
|
+
approved
|
|
374
|
+
);
|
|
375
|
+
}
|
package/src/cli/fleet.ts
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { selectNodes } from '../config';
|
|
2
|
+
import { doctor } from '../manager';
|
|
3
|
+
import { FleetUsageError } from '../errors';
|
|
4
|
+
import { onboarding } from '../onboard';
|
|
5
|
+
import { makeReport, renderReport, reportExitCode } from '../report';
|
|
6
|
+
import { runCommand } from '../runner';
|
|
7
|
+
import schema from '../../schema/roster.schema.json';
|
|
8
|
+
import type { FleetConfig, FleetNode, NodeReport, Runner } from '../types';
|
|
9
|
+
import type { CommandOutcome } from './contracts';
|
|
10
|
+
import type { Settings } from './settings';
|
|
11
|
+
import { capabilities, timeout, validateOptions } from './options';
|
|
12
|
+
import type { CliOptions } from './options';
|
|
13
|
+
import { addNode, nodeFromOptions, readRoster } from './roster';
|
|
14
|
+
import { executeTailnet } from './tailnet';
|
|
15
|
+
import { probeSource } from './probe-source';
|
|
16
|
+
import { executeRepairCommand } from './repair';
|
|
17
|
+
|
|
18
|
+
export interface FleetServices {
|
|
19
|
+
run: Runner;
|
|
20
|
+
source: () => Promise<string>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const SERVICES: FleetServices = { run: runCommand, source: probeSource };
|
|
24
|
+
|
|
25
|
+
export async function executeFleet(
|
|
26
|
+
command: string,
|
|
27
|
+
options: CliOptions,
|
|
28
|
+
settings: Settings,
|
|
29
|
+
services: FleetServices = SERVICES
|
|
30
|
+
): Promise<CommandOutcome> {
|
|
31
|
+
validateOptions(command, options);
|
|
32
|
+
if (['repair', 'repairs'].includes(command))
|
|
33
|
+
return executeRepairCommand(command, options, settings, services);
|
|
34
|
+
if (['discover', 'health'].includes(command))
|
|
35
|
+
return executeTailnet(command, options, settings, services);
|
|
36
|
+
if (command === 'schema') return result(schema);
|
|
37
|
+
if (command === 'add') return register(settings, options);
|
|
38
|
+
const roster = options.local ? null : await readRoster(settings.config);
|
|
39
|
+
const nodes = resolveNodes(command, options, roster);
|
|
40
|
+
if (['nodes', 'onboard'].includes(command))
|
|
41
|
+
return rosterResult(command, nodes, roster !== null);
|
|
42
|
+
return diagnose(
|
|
43
|
+
nodes,
|
|
44
|
+
options,
|
|
45
|
+
{ ...settings, missingRoster: !roster },
|
|
46
|
+
services
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function rosterResult(
|
|
51
|
+
command: string,
|
|
52
|
+
nodes: FleetNode[],
|
|
53
|
+
configured: boolean
|
|
54
|
+
): CommandOutcome {
|
|
55
|
+
return result({
|
|
56
|
+
version: 1,
|
|
57
|
+
command,
|
|
58
|
+
ok: true,
|
|
59
|
+
...(command === 'nodes' ? { configured } : {}),
|
|
60
|
+
nodes:
|
|
61
|
+
command === 'onboard'
|
|
62
|
+
? nodes.map((node) => onboarding(node, 'devfleet'))
|
|
63
|
+
: nodes,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function resolveNodes(
|
|
68
|
+
command: string,
|
|
69
|
+
options: CliOptions,
|
|
70
|
+
roster: FleetConfig | null
|
|
71
|
+
): FleetNode[] {
|
|
72
|
+
if (!roster && !options.local && !['doctor', 'nodes'].includes(command))
|
|
73
|
+
throw new FleetUsageError(
|
|
74
|
+
'No fleet roster. Use fleet-node-add or the interactive Register action; use fleet-status --local for this machine.'
|
|
75
|
+
);
|
|
76
|
+
if (options.node?.length && !roster)
|
|
77
|
+
throw new FleetUsageError(
|
|
78
|
+
'Cannot select nodes without a roster. Use fleet-node-add first.'
|
|
79
|
+
);
|
|
80
|
+
return roster ? selectNodes(roster, options.node ?? []) : [];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function result(data: unknown, text?: string): CommandOutcome {
|
|
84
|
+
return { exitCode: 0, data, ...(text ? { text } : {}) };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function register(
|
|
88
|
+
settings: Settings,
|
|
89
|
+
options: CliOptions
|
|
90
|
+
): Promise<CommandOutcome> {
|
|
91
|
+
const node = nodeFromOptions(options);
|
|
92
|
+
const added = await addNode(settings.config, node, options.apply === true);
|
|
93
|
+
return result({
|
|
94
|
+
version: 1,
|
|
95
|
+
command: 'add',
|
|
96
|
+
ok: true,
|
|
97
|
+
applied: options.apply === true,
|
|
98
|
+
path: settings.config,
|
|
99
|
+
// How the fleet state stands with the bucket after this change.
|
|
100
|
+
...(added.standing ? { state: added.standing } : {}),
|
|
101
|
+
node,
|
|
102
|
+
nodes: added.roster.nodes,
|
|
103
|
+
next: options.apply
|
|
104
|
+
? 'Run fleet-nodes --onboard, then fleet-status --json.'
|
|
105
|
+
: 'Review this preview, then repeat with --apply --yes.',
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function diagnose(
|
|
110
|
+
nodes: FleetNode[],
|
|
111
|
+
options: CliOptions,
|
|
112
|
+
settings: Settings & { missingRoster: boolean },
|
|
113
|
+
services: FleetServices
|
|
114
|
+
): Promise<CommandOutcome> {
|
|
115
|
+
const source = nodes.length ? await services.source() : undefined;
|
|
116
|
+
const reports = await doctor(
|
|
117
|
+
nodes,
|
|
118
|
+
{
|
|
119
|
+
root: settings.workspace,
|
|
120
|
+
local: options.local === true,
|
|
121
|
+
required: capabilities(options.require),
|
|
122
|
+
timeoutMs: timeout(options),
|
|
123
|
+
...(source ? { source } : {}),
|
|
124
|
+
},
|
|
125
|
+
services.run
|
|
126
|
+
);
|
|
127
|
+
if (!options.local && settings.missingRoster) reports.push(missingRoster());
|
|
128
|
+
const report = {
|
|
129
|
+
...makeReport('doctor', reports),
|
|
130
|
+
coverage: {
|
|
131
|
+
scope: options.local
|
|
132
|
+
? 'local'
|
|
133
|
+
: options.node?.length
|
|
134
|
+
? 'selected'
|
|
135
|
+
: 'all-configured',
|
|
136
|
+
manager: true,
|
|
137
|
+
configuredNodesTested: nodes.map((node) => node.id),
|
|
138
|
+
rosterAvailable: !settings.missingRoster,
|
|
139
|
+
note: 'Directed SSH checks from this manager only. T3 relay/peer and VS Code forwarding require operator tests.',
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
return {
|
|
143
|
+
exitCode: reportExitCode(report, options.strict === true),
|
|
144
|
+
data: report,
|
|
145
|
+
text: renderReport(report),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function missingRoster(): NodeReport {
|
|
150
|
+
return {
|
|
151
|
+
node: 'fleet',
|
|
152
|
+
checks: [
|
|
153
|
+
{
|
|
154
|
+
id: 'fleet:roster',
|
|
155
|
+
status: 'fail',
|
|
156
|
+
summary:
|
|
157
|
+
'No developer fleet roster is configured. Only this manager could be checked; fleet-wide health is unknown.',
|
|
158
|
+
remedy: {
|
|
159
|
+
action:
|
|
160
|
+
'Open personal --interactive and choose Register a developer computer, or run devfleet fleet-node-add --help. Then run fleet-status --json.',
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
};
|
|
165
|
+
}
|