@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,234 @@
|
|
|
1
|
+
import { workerTools } from './worker-tools';
|
|
2
|
+
/** Read-only toolchain checks; no logins, installations or changes to Docker. */
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { homedir, tmpdir } from 'node:os';
|
|
7
|
+
import {
|
|
8
|
+
AGENT_WORKTREE_ROOTS,
|
|
9
|
+
globalConfigDir,
|
|
10
|
+
globalConfigStanding,
|
|
11
|
+
workspaceRoot,
|
|
12
|
+
type GlobalConfigStanding,
|
|
13
|
+
} from './global-config';
|
|
14
|
+
|
|
15
|
+
type Tool = string | { version: string; lazy?: boolean; os?: string[] };
|
|
16
|
+
type Config = {
|
|
17
|
+
tools: Record<string, Tool>;
|
|
18
|
+
vars?: { worker_tools?: unknown };
|
|
19
|
+
};
|
|
20
|
+
export type Check = {
|
|
21
|
+
id: string;
|
|
22
|
+
status: 'pass' | 'warn' | 'fail';
|
|
23
|
+
detail: string;
|
|
24
|
+
};
|
|
25
|
+
export type Runner = (command: string[]) => { ok: boolean; output: string };
|
|
26
|
+
const COMMANDS: Record<string, string> = {
|
|
27
|
+
'npm:@yarnpkg/cli-dist': 'yarn',
|
|
28
|
+
'npm:vercel': 'vercel',
|
|
29
|
+
'npm:wrangler': 'wrangler',
|
|
30
|
+
'aqua:anomalyco/opencode': 'opencode',
|
|
31
|
+
'docker-cli': 'docker',
|
|
32
|
+
opentofu: 'tofu',
|
|
33
|
+
ripgrep: 'rg',
|
|
34
|
+
rust: 'rustc',
|
|
35
|
+
'npm:@devcontainers/cli': 'devcontainer',
|
|
36
|
+
'http:grok': 'grok',
|
|
37
|
+
'http:antigravity': 'antigravity',
|
|
38
|
+
};
|
|
39
|
+
const VERSION_ARGS: Record<string, string[]> = {
|
|
40
|
+
go: ['version'],
|
|
41
|
+
gitleaks: ['version'],
|
|
42
|
+
kubectl: ['version', '--client=true', '--output=json'],
|
|
43
|
+
helm: ['version', '--short'],
|
|
44
|
+
};
|
|
45
|
+
const run: Runner = (command) => {
|
|
46
|
+
const result = spawnSync(command[0]!, command.slice(1), {
|
|
47
|
+
encoding: 'utf8',
|
|
48
|
+
timeout: 30_000,
|
|
49
|
+
windowsHide: true,
|
|
50
|
+
cwd: tmpdir(),
|
|
51
|
+
});
|
|
52
|
+
return {
|
|
53
|
+
ok: result.status === 0,
|
|
54
|
+
output: `${result.stdout ?? ''}${result.stderr ?? ''}`.trim(),
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** mise's `os` names for the platforms it installs a tool on. */
|
|
59
|
+
const MISE_OS: Record<string, string> = {
|
|
60
|
+
darwin: 'macos',
|
|
61
|
+
linux: 'linux',
|
|
62
|
+
win32: 'windows',
|
|
63
|
+
};
|
|
64
|
+
/** Installed on Windows by a native installer (native-aws.ts), not by mise. */
|
|
65
|
+
const NATIVE_WINDOWS = new Set(['aws']);
|
|
66
|
+
|
|
67
|
+
function unavailable(id: string, tool: Tool, platform: string): boolean {
|
|
68
|
+
if (typeof tool === 'string' || !tool.os) return false;
|
|
69
|
+
if (platform === 'win32' && NATIVE_WINDOWS.has(id)) return false;
|
|
70
|
+
return !tool.os.includes(MISE_OS[platform] ?? platform);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function toolChecks(
|
|
74
|
+
tools: Record<string, Tool>,
|
|
75
|
+
execute: Runner = run,
|
|
76
|
+
platform: string = process.platform
|
|
77
|
+
): Check[] {
|
|
78
|
+
const checks: Check[] = [];
|
|
79
|
+
for (const [id, tool] of Object.entries(tools)) {
|
|
80
|
+
if (typeof tool !== 'string' && tool.lazy) continue;
|
|
81
|
+
// A tool pinned only for other platforms has no artifact to verify here.
|
|
82
|
+
if (unavailable(id, tool, platform)) continue;
|
|
83
|
+
const version = typeof tool === 'string' ? tool : tool.version;
|
|
84
|
+
const command = COMMANDS[id] ?? id;
|
|
85
|
+
const result = execute([command, ...(VERSION_ARGS[id] ?? ['--version'])]);
|
|
86
|
+
const exact = new RegExp(
|
|
87
|
+
`(?:^|[^0-9])${version.replaceAll('.', '\\.')}($|(?![0-9]|\\.[0-9]))`
|
|
88
|
+
);
|
|
89
|
+
const matches = result.ok && exact.test(result.output);
|
|
90
|
+
checks.push({
|
|
91
|
+
id,
|
|
92
|
+
status: matches ? 'pass' : 'fail',
|
|
93
|
+
detail: matches
|
|
94
|
+
? version
|
|
95
|
+
: `Expected ${command} ${version}; run dev-fleet tools sync and reopen the shell. Check for competing PATH entries.`,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
for (const command of ['git', 'npm']) {
|
|
99
|
+
const result = execute([command, '--version']);
|
|
100
|
+
checks.push({
|
|
101
|
+
id: command,
|
|
102
|
+
status: result.ok ? 'pass' : 'fail',
|
|
103
|
+
detail: result.ok
|
|
104
|
+
? result.output
|
|
105
|
+
: `Missing ${command}; rerun the OS bootstrap.`,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return checks;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function dockerChecks(
|
|
112
|
+
required: boolean,
|
|
113
|
+
execute: Runner = run
|
|
114
|
+
): Check[] {
|
|
115
|
+
return [
|
|
116
|
+
['docker-engine', ['docker', 'info', '--format', '{{.ServerVersion}}']],
|
|
117
|
+
['docker-compose', ['docker', 'compose', 'version']],
|
|
118
|
+
['docker-buildx', ['docker', 'buildx', 'version']],
|
|
119
|
+
].map(([id, command]) => {
|
|
120
|
+
const result = execute(command as string[]);
|
|
121
|
+
return {
|
|
122
|
+
id: id as string,
|
|
123
|
+
status: result.ok ? 'pass' : required ? 'fail' : 'warn',
|
|
124
|
+
detail: result.ok
|
|
125
|
+
? result.output
|
|
126
|
+
: 'Install/start Docker Desktop or Docker Engine with Compose + Buildx. See docs/machine-setup.md.',
|
|
127
|
+
};
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Agent worktree roots must be trusted, or every mise shim fails there. */
|
|
132
|
+
export function worktreeTrustCheck(
|
|
133
|
+
execute: Runner = run,
|
|
134
|
+
home: string = homedir()
|
|
135
|
+
): Check {
|
|
136
|
+
const expand = (path: string) =>
|
|
137
|
+
path === '~' || path.startsWith('~/') ? home + path.slice(1) : path;
|
|
138
|
+
const result = execute(['mise', 'settings', 'get', 'trusted_config_paths']);
|
|
139
|
+
let trusted: string[] = [];
|
|
140
|
+
try {
|
|
141
|
+
const parsed: unknown = result.ok ? JSON.parse(result.output) : [];
|
|
142
|
+
if (Array.isArray(parsed))
|
|
143
|
+
trusted = parsed.filter((p): p is string => typeof p === 'string');
|
|
144
|
+
} catch {
|
|
145
|
+
trusted = [];
|
|
146
|
+
}
|
|
147
|
+
const have = new Set(trusted.map(expand));
|
|
148
|
+
const missing = AGENT_WORKTREE_ROOTS.filter(
|
|
149
|
+
(root) => !have.has(expand(root))
|
|
150
|
+
);
|
|
151
|
+
return {
|
|
152
|
+
id: 'agent-worktree-trust',
|
|
153
|
+
status: missing.length ? 'fail' : 'pass',
|
|
154
|
+
detail: missing.length
|
|
155
|
+
? `mise does not trust ${missing.join(', ')}; agent CLIs fail in new worktrees there. Run dev-fleet tools sync.`
|
|
156
|
+
: AGENT_WORKTREE_ROOTS.join(', '),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const STANDING: Record<GlobalConfigStanding, string> = {
|
|
161
|
+
published: 'Published by dev-fleet from this revision.',
|
|
162
|
+
missing: 'There is no global mise configuration.',
|
|
163
|
+
replaced:
|
|
164
|
+
'Another project or tool replaced the global mise configuration dev-fleet publishes.',
|
|
165
|
+
changed:
|
|
166
|
+
'The global mise configuration is not the one this revision publishes.',
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/** On a fleet machine the global mise configuration belongs to dev-fleet.
|
|
170
|
+
* When anything else replaced it, its pins and the trusted agent worktree
|
|
171
|
+
* roots are gone until the next fleet-sync.
|
|
172
|
+
*/
|
|
173
|
+
export function globalConfigCheck(
|
|
174
|
+
dir?: string,
|
|
175
|
+
root: string = workspaceRoot
|
|
176
|
+
): Check {
|
|
177
|
+
let standing: GlobalConfigStanding;
|
|
178
|
+
try {
|
|
179
|
+
standing = globalConfigStanding(dir ?? globalConfigDir(), root);
|
|
180
|
+
} catch {
|
|
181
|
+
return {
|
|
182
|
+
id: 'global-mise-config',
|
|
183
|
+
status: 'fail',
|
|
184
|
+
detail:
|
|
185
|
+
'MISE_GLOBAL_CONFIG_FILE moves the global mise configuration away from the one dev-fleet publishes; unset it.',
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
id: 'global-mise-config',
|
|
190
|
+
status: standing === 'published' ? 'pass' : 'fail',
|
|
191
|
+
detail:
|
|
192
|
+
standing === 'published'
|
|
193
|
+
? STANDING[standing]
|
|
194
|
+
: `${STANDING[standing]} Run fleet-sync; projects pin their own tools in a project mise.toml and never publish a global one.`,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function checkMachine(requireDocker = false, worker = false): Check[] {
|
|
199
|
+
const config = Bun.TOML.parse(
|
|
200
|
+
readFileSync(join(workspaceRoot, 'mise.toml'), 'utf8')
|
|
201
|
+
) as Config;
|
|
202
|
+
const auth = run(['gh', 'auth', 'status']);
|
|
203
|
+
return [
|
|
204
|
+
...toolChecks(
|
|
205
|
+
worker
|
|
206
|
+
? workerTools(config.tools, config.vars?.worker_tools)
|
|
207
|
+
: config.tools
|
|
208
|
+
),
|
|
209
|
+
...dockerChecks(requireDocker),
|
|
210
|
+
worktreeTrustCheck(),
|
|
211
|
+
globalConfigCheck(),
|
|
212
|
+
{
|
|
213
|
+
id: 'github-auth',
|
|
214
|
+
status: auth.ok ? 'pass' : 'warn',
|
|
215
|
+
detail: auth.ok
|
|
216
|
+
? 'Authenticated'
|
|
217
|
+
: 'Run gh auth login, then gh auth setup-git.',
|
|
218
|
+
},
|
|
219
|
+
];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (import.meta.main) {
|
|
223
|
+
const checks = checkMachine(
|
|
224
|
+
process.argv.includes('--docker'),
|
|
225
|
+
process.argv.includes('--worker')
|
|
226
|
+
);
|
|
227
|
+
const ok = checks.every((check) => check.status !== 'fail');
|
|
228
|
+
if (process.argv.includes('--json'))
|
|
229
|
+
console.log(JSON.stringify({ ok, checks }, null, 2));
|
|
230
|
+
else
|
|
231
|
+
for (const check of checks)
|
|
232
|
+
console.log(`${check.status.toUpperCase()} ${check.id}: ${check.detail}`);
|
|
233
|
+
process.exitCode = ok ? 0 : 1;
|
|
234
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Every fleet-sync on the manager runs through this, whether started there or
|
|
3
|
+
# handed off from a worker over strict SSH. Runs fleet-sync from a clean,
|
|
4
|
+
# fleet-owned checkout of the exact approved revision, so the result never
|
|
5
|
+
# depends on the working tree it was started in (uncommitted edits, another
|
|
6
|
+
# branch, a removed worktree). Checkouts are reused when warm and pruned to the
|
|
7
|
+
# three most recent plus the one T3's providers launch from. Never touches the
|
|
8
|
+
# human's working tree.
|
|
9
|
+
set -eu
|
|
10
|
+
revision=$1
|
|
11
|
+
shift
|
|
12
|
+
case "$revision" in *[!a-f0-9]*|'') exit 2;; esac
|
|
13
|
+
[ "${#revision}" = 40 ] || exit 2
|
|
14
|
+
say() { printf 'fleet-sync (manager): %s\n' "$1" >&2; }
|
|
15
|
+
# A preview (--dry-run) changes nothing outside dev-fleet's own disposable
|
|
16
|
+
# caches: it never publishes the global mise configuration, never edits shell
|
|
17
|
+
# profiles and never prunes run checkouts. Only an apply does.
|
|
18
|
+
preview=false
|
|
19
|
+
for arg in "$@"; do [ "$arg" != --dry-run ] || preview=true; done
|
|
20
|
+
# mise is installed here by the toolchain step below; a login shell is not
|
|
21
|
+
# required to find it.
|
|
22
|
+
export PATH="$HOME/.local/bin:$PATH"
|
|
23
|
+
fleet="$HOME/.local/share/personal/fleet"
|
|
24
|
+
# The repository to take the revision from. Nobody keeps a clone of dev-fleet:
|
|
25
|
+
# fleet-sync names the one it prepared, and a hand-off (which names none) uses
|
|
26
|
+
# the manager's own copy of the source repository, or the checkout the devfleet
|
|
27
|
+
# launcher is linked into.
|
|
28
|
+
repo=${DEVFLEET_REPO:-}
|
|
29
|
+
if [ -z "$repo" ] && git -C "$fleet/manager/source" rev-parse --git-dir >/dev/null 2>&1; then
|
|
30
|
+
repo="$fleet/manager/source"
|
|
31
|
+
fi
|
|
32
|
+
if [ -z "$repo" ]; then
|
|
33
|
+
launcher=$(command -v devfleet) || { say 'this manager has no copy of the source yet; run fleet-sync once on the manager itself, from the project that holds your fleet.json.'; exit 3; }
|
|
34
|
+
repo=$(dirname "$(readlink -f "$launcher")")
|
|
35
|
+
fi
|
|
36
|
+
repo=$(git -C "$repo" rev-parse --show-toplevel) || { say 'cannot read the source repository on the manager; run fleet-sync once on the manager itself to fetch it again.'; exit 3; }
|
|
37
|
+
runs="$fleet/manager-runs"
|
|
38
|
+
dir="$runs/$revision"
|
|
39
|
+
ready() {
|
|
40
|
+
[ "$(git -C "$dir" rev-parse HEAD 2>/dev/null)" = "$revision" ] &&
|
|
41
|
+
[ -z "$(git -C "$dir" status --porcelain 2>/dev/null)" ]
|
|
42
|
+
}
|
|
43
|
+
if ! ready; then
|
|
44
|
+
git -C "$repo" cat-file -e "$revision^{commit}" 2>/dev/null || git -C "$repo" fetch --quiet origin || true
|
|
45
|
+
git -C "$repo" cat-file -e "$revision^{commit}" 2>/dev/null || git -C "$repo" fetch --quiet --no-tags origin "$revision" || true
|
|
46
|
+
git -C "$repo" cat-file -e "$revision^{commit}" 2>/dev/null || { say "revision $revision is neither on the manager nor on origin; push it first."; exit 4; }
|
|
47
|
+
mkdir -p "$runs"
|
|
48
|
+
chmod 700 "$runs"
|
|
49
|
+
# A fleet-owned, disposable run checkout: recreate it when stale or dirty.
|
|
50
|
+
if [ -e "$dir" ]; then git -C "$repo" worktree remove --force "$dir" 2>/dev/null || rm -rf -- "$dir"; fi
|
|
51
|
+
git -C "$repo" worktree prune
|
|
52
|
+
git -C "$repo" worktree add --quiet --detach "$dir" "$revision"
|
|
53
|
+
fi
|
|
54
|
+
touch "$dir"
|
|
55
|
+
cd "$dir"
|
|
56
|
+
mise trust --quiet "$dir" >/dev/null 2>&1 || true
|
|
57
|
+
# The manager runs the same locked toolchain (and so the same agent versions)
|
|
58
|
+
# as every worker: publish this revision's global config, install it and the
|
|
59
|
+
# frozen dependencies. Warm: the toolchain is skipped when the same inputs
|
|
60
|
+
# (tool pins, locks, setup code) were installed within a day, so a revision
|
|
61
|
+
# that changes none of them only installs its own dependencies.
|
|
62
|
+
# Stamp and log sit beside the checkout: inside it they would make it dirty.
|
|
63
|
+
stamp="$runs/toolchain-ready"
|
|
64
|
+
log="$runs/$revision.setup.log"
|
|
65
|
+
key=$(git rev-parse HEAD:mise.toml HEAD:mise.lock HEAD:.mise HEAD:bun.lock HEAD:package.json HEAD:bootstrap 2>/dev/null | tr '\n' ' ')
|
|
66
|
+
[ -n "$key" ] || key=$revision
|
|
67
|
+
failed() {
|
|
68
|
+
tail -n 20 "$log" >&2
|
|
69
|
+
say "toolchain or dependency install failed in the run checkout; full log: $log"
|
|
70
|
+
exit 5
|
|
71
|
+
}
|
|
72
|
+
if [ "$preview" = true ]; then
|
|
73
|
+
# The revision's own locked Bun is enough to preview; mise keeps it beside
|
|
74
|
+
# its other versions without making it the global one.
|
|
75
|
+
command -v mise >/dev/null 2>&1 || { say 'this manager has no toolchain yet, so a preview cannot run; fleet-sync without --dry-run installs it.'; exit 6; }
|
|
76
|
+
mise install --locked bun >"$log" 2>&1 || failed
|
|
77
|
+
elif [ "$(cat "$stamp" 2>/dev/null)" != "$key" ] || [ -z "$(find "$stamp" -mmin -1440 2>/dev/null)" ]; then
|
|
78
|
+
rm -f -- "$stamp"
|
|
79
|
+
# The toolchain bootstrap installs the pinned mise first when the manager
|
|
80
|
+
# has none (or another version), so Git is all a new manager needs.
|
|
81
|
+
bash bootstrap/bootstrap.sh --tools-only >"$log" 2>&1 || failed
|
|
82
|
+
printf '%s' "$key" >"$stamp"
|
|
83
|
+
fi
|
|
84
|
+
if [ ! -d node_modules ]; then
|
|
85
|
+
mise exec --locked bun -- bun install --frozen-lockfile --silent >>"$log" 2>&1 || failed
|
|
86
|
+
fi
|
|
87
|
+
# Keep the three most recent run checkouts (names are commit SHAs) and the
|
|
88
|
+
# applied one: fleet/current points at it and T3's providers launch from there.
|
|
89
|
+
# A preview leaves them for the next apply to prune.
|
|
90
|
+
if [ "$preview" = false ]; then
|
|
91
|
+
applied=$(basename "$(readlink "$(dirname "$runs")/current" 2>/dev/null || true)")
|
|
92
|
+
# shellcheck disable=SC2012
|
|
93
|
+
for old in $(ls -1td "$runs"/*/ | tail -n +4); do
|
|
94
|
+
[ "$(basename "$old")" != "$applied" ] || continue
|
|
95
|
+
# A checkout made from another repository (an earlier clone) is not this
|
|
96
|
+
# repository's worktree: remove the directory itself.
|
|
97
|
+
git -C "$repo" worktree remove --force "$old" 2>/dev/null || rm -rf -- "$old"
|
|
98
|
+
rm -f -- "${old%/}.setup.log"
|
|
99
|
+
done
|
|
100
|
+
git -C "$repo" worktree prune
|
|
101
|
+
fi
|
|
102
|
+
export PERSONAL_WORKSPACE="$dir" PERSONAL_FLEET_RUN_CHECKOUT="$revision"
|
|
103
|
+
exec mise exec --locked bun -- bun src/cli/cli.ts fleet-sync --revision "$revision" "$@"
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Executed over strict SSH from the selected verified Git revision. stdin is
|
|
3
|
+
# one line holding the base64 fleet configuration (empty when there is none),
|
|
4
|
+
# then the base64 Git bundle (nothing in cached mode). All executable content
|
|
5
|
+
# remains in this tracked source.
|
|
6
|
+
set -eu
|
|
7
|
+
umask 077
|
|
8
|
+
revision=$1
|
|
9
|
+
bundle_sha=$2
|
|
10
|
+
node=$3
|
|
11
|
+
source_mode=${4:-transfer}
|
|
12
|
+
case "$source_mode" in cached|transfer) ;; *) exit 2;; esac
|
|
13
|
+
case "$revision" in *[!a-f0-9]*|'') exit 2;; esac
|
|
14
|
+
[ "${#revision}" = 40 ] || exit 2
|
|
15
|
+
case "$bundle_sha" in *[!a-f0-9]*|'') exit 2;; esac
|
|
16
|
+
[ "${#bundle_sha}" = 64 ] || exit 2
|
|
17
|
+
case "$node" in *[!a-z0-9-]*|'') exit 2;; esac
|
|
18
|
+
phase=bootstrap
|
|
19
|
+
bundle=''
|
|
20
|
+
code=0
|
|
21
|
+
apt_log=''
|
|
22
|
+
failure_code=REMOTE_BOOTSTRAP_INCOMPLETE
|
|
23
|
+
die() { failure_code=$1; exit 1; }
|
|
24
|
+
# One remedy per failure: what the operator does next. Never raw output.
|
|
25
|
+
remedy() {
|
|
26
|
+
case "$1" in
|
|
27
|
+
FLEET_REMOTE_LOCKED) echo 'Another fleet operation holds this machine. Wait for it to finish; rerun fleet-sync afterwards. Never remove the lock while its holder runs.';;
|
|
28
|
+
ROOT_DEVELOPMENT_USER_REJECTED) echo 'Connect as the non-root developer account the fleet configuration names.';;
|
|
29
|
+
UBUNTU_BASELINE_MISMATCH|UBUNTU_ARCHITECTURE_UNSUPPORTED|MACOS_BASELINE_MISMATCH|MACOS_ARCHITECTURE_UNSUPPORTED|PLATFORM_UNSUPPORTED) echo 'Workers run Ubuntu 26.04 on x86_64 or aarch64. Reinstall the machine; fleet-sync never upgrades an operating system.';;
|
|
30
|
+
FLEET_STATE_PATH_UNSAFE) echo 'A fleet state directory under ~/.local/share/personal is a symbolic link. Replace it with a real directory.';;
|
|
31
|
+
BOOTSTRAP_CAPACITY_UNVERIFIED|BOOTSTRAP_DISK_CAPACITY_REQUIRED) echo 'Free at least 2 GiB and 50000 inodes on the home file system, then rerun fleet-sync.';;
|
|
32
|
+
SOURCE_DIGEST_MISMATCH) echo 'The bundle changed in transit. Rerun fleet-sync; if it repeats, check the SSH route.';;
|
|
33
|
+
SUDO_AUTHORIZATION_REQUIRED) echo 'Grant the developer account passwordless sudo, or run fleet-sync interactively once to authorize it.';;
|
|
34
|
+
APT_LOCK_CONTENDED) echo 'Another package operation held the APT lock past the bounded wait. Let it finish (it is never killed), then rerun fleet-sync.';;
|
|
35
|
+
APT_DNS_UNAVAILABLE) echo 'The worker cannot resolve the Ubuntu archive. Restore its DNS, then rerun fleet-sync.';;
|
|
36
|
+
APT_REPOSITORY_UNAVAILABLE) echo 'The Ubuntu archive could not be reached. Check the network or archive status, then rerun fleet-sync.';;
|
|
37
|
+
APT_DISK_FULL) echo 'Package installation ran out of disk space. Free space under /var and /usr, then rerun fleet-sync.';;
|
|
38
|
+
DPKG_INTERRUPTED) echo 'An earlier package installation that fleet-sync did not start was interrupted. Run sudo dpkg --configure -a once no package process is running, then rerun fleet-sync.';;
|
|
39
|
+
APT_UPDATE_FAILED|APT_INSTALL_FAILED) echo 'Run sudo apt-get update and install the bootstrap packages by hand to see the error, then rerun fleet-sync.';;
|
|
40
|
+
XCODE_COMMAND_LINE_TOOLS_REQUIRED) echo 'Install the Xcode Command Line Tools (xcode-select --install), then rerun fleet-sync.';;
|
|
41
|
+
BOOTSTRAP_LOCK_TOOL_REQUIRED) echo 'Install python3 so the operation lock can be taken, then rerun fleet-sync.';;
|
|
42
|
+
SOURCE_CACHE_CHANGED|SOURCE_STATUS_FAILED|SOURCE_CHECKOUT_CONFLICT) echo 'The fleet-owned checkout of this revision holds changes fleet-sync did not make. Inspect and remove that revisions directory, then rerun fleet-sync.';;
|
|
43
|
+
CONFIGURATION_INSTALL_FAILED) echo 'The fleet configuration could not be written under ~/.config/dev-fleet. Check that directory, then rerun fleet-sync.';;
|
|
44
|
+
TOOLCHAIN_*) echo 'The pinned toolchain did not converge. Rerun fleet-sync; it resumes where it stopped. If it repeats, run devfleet doctor on the worker.';;
|
|
45
|
+
*) echo 'Inspect the remote operation lock and rerun the approved revision. No success receipt advanced.';;
|
|
46
|
+
esac
|
|
47
|
+
}
|
|
48
|
+
# Names an APT failure by the fixed messages of the pinned Ubuntu release.
|
|
49
|
+
apt_failure() {
|
|
50
|
+
if grep -q 'Could not get lock' "$apt_log" 2>/dev/null; then die APT_LOCK_CONTENDED
|
|
51
|
+
elif grep -q 'No space left on device' "$apt_log" 2>/dev/null; then die APT_DISK_FULL
|
|
52
|
+
elif grep -qE 'Temporary failure resolving|Could not resolve' "$apt_log" 2>/dev/null; then die APT_DNS_UNAVAILABLE
|
|
53
|
+
elif grep -qE 'Failed to fetch|Unable to connect|Connection failed|Unable to locate package' "$apt_log" 2>/dev/null; then die APT_REPOSITORY_UNAVAILABLE
|
|
54
|
+
elif grep -q 'dpkg was interrupted' "$apt_log" 2>/dev/null; then die DPKG_INTERRUPTED
|
|
55
|
+
else die "$1"
|
|
56
|
+
fi
|
|
57
|
+
}
|
|
58
|
+
fail() {
|
|
59
|
+
printf '{"version":1,"checks":[{"id":"bootstrap:%s","status":"fail","summary":"%s","remedy":{"action":"%s"}}]}\n' "$phase" "$failure_code" "$(remedy "$failure_code")"
|
|
60
|
+
}
|
|
61
|
+
trap 'code=$?; [ -z "$bundle" ] || rm -f -- "$bundle"; [ -z "$apt_log" ] || rm -f -- "$apt_log"; if [ "$code" != 0 ]; then fail; fi' EXIT
|
|
62
|
+
# Preserve the lock holder if its SSH channel disappears mid-install.
|
|
63
|
+
trap '' HUP
|
|
64
|
+
trap 'exit 1' INT TERM
|
|
65
|
+
# No shell startup assumptions, and no implicit root development user.
|
|
66
|
+
[ "$(id -u)" != 0 ] || die ROOT_DEVELOPMENT_USER_REJECTED
|
|
67
|
+
# Ubuntu 26.04 is the accepted worker platform; macOS 14+ follows the same
|
|
68
|
+
# phases best effort with launchd instead of systemd.
|
|
69
|
+
platform=$(uname -s)
|
|
70
|
+
case "$platform" in
|
|
71
|
+
Linux)
|
|
72
|
+
# shellcheck source=/dev/null
|
|
73
|
+
. /etc/os-release
|
|
74
|
+
[ "$ID" = ubuntu ] && [ "$VERSION_ID" = 26.04 ] || die UBUNTU_BASELINE_MISMATCH
|
|
75
|
+
case "$(uname -m)" in x86_64|aarch64) ;; *) die UBUNTU_ARCHITECTURE_UNSUPPORTED;; esac ;;
|
|
76
|
+
Darwin)
|
|
77
|
+
major=$(sw_vers -productVersion | cut -d. -f1)
|
|
78
|
+
case "$major" in ''|*[!0-9]*) die MACOS_BASELINE_MISMATCH;; esac
|
|
79
|
+
[ "$major" -ge 14 ] || die MACOS_BASELINE_MISMATCH
|
|
80
|
+
case "$(uname -m)" in arm64|x86_64) ;; *) die MACOS_ARCHITECTURE_UNSUPPORTED;; esac ;;
|
|
81
|
+
*) die PLATFORM_UNSUPPORTED;;
|
|
82
|
+
esac
|
|
83
|
+
root="$HOME/.local/share/personal/fleet"
|
|
84
|
+
# Refuse symlinked state roots rather than allowing writes through them.
|
|
85
|
+
for path in "$HOME/.local" "$HOME/.local/share" "$HOME/.local/share/personal" "$root"; do
|
|
86
|
+
[ ! -L "$path" ] || die FLEET_STATE_PATH_UNSAFE
|
|
87
|
+
done
|
|
88
|
+
mkdir -p "$root"
|
|
89
|
+
phase=lock
|
|
90
|
+
[ ! -L "$root/remote-operation.lock" ] || exit 1
|
|
91
|
+
exec 9>"$root/remote-operation.lock"
|
|
92
|
+
# macOS has no flock(1); lock the same descriptor through fcntl. The lock
|
|
93
|
+
# follows this shell's open descriptor and is released when it exits.
|
|
94
|
+
if command -v flock >/dev/null 2>&1; then
|
|
95
|
+
flock -n 9 || die FLEET_REMOTE_LOCKED
|
|
96
|
+
else
|
|
97
|
+
command -v python3 >/dev/null 2>&1 || die BOOTSTRAP_LOCK_TOOL_REQUIRED
|
|
98
|
+
python3 -c 'import fcntl; fcntl.flock(9, fcntl.LOCK_EX | fcntl.LOCK_NB)' 9>&9 || die FLEET_REMOTE_LOCKED
|
|
99
|
+
fi
|
|
100
|
+
printf '{"version":1,"pid":%s,"revision":"%s","node":"%s"}\n' "$$" "$revision" "$node" >"$root/remote-operation.json"
|
|
101
|
+
# The fleet configuration is installed under the lock, so two overlapping
|
|
102
|
+
# runs never write it at once. It holds no secret.
|
|
103
|
+
phase='configuration-install'
|
|
104
|
+
IFS= read -r configuration || configuration=''
|
|
105
|
+
if [ -n "$configuration" ]; then
|
|
106
|
+
config_dir="$HOME/.config/dev-fleet"
|
|
107
|
+
mkdir -p "$config_dir" || die CONFIGURATION_INSTALL_FAILED
|
|
108
|
+
printf '%s' "$configuration" | base64 -d >"$config_dir/fleet.json.new" || die CONFIGURATION_INSTALL_FAILED
|
|
109
|
+
mv "$config_dir/fleet.json.new" "$config_dir/fleet.json" || die CONFIGURATION_INSTALL_FAILED
|
|
110
|
+
fi
|
|
111
|
+
phase=resources
|
|
112
|
+
# A bootstrap floor protects source transfer and minimal prerequisites. The
|
|
113
|
+
# Workstation baseline subsequently enforces its stricter declared capacity.
|
|
114
|
+
free_kib=$(df -Pk "$root" | awk 'END {print $4}')
|
|
115
|
+
free_inodes=$(df -Pi "$root" | awk 'END {print $4}')
|
|
116
|
+
case "$free_kib:$free_inodes" in *[!0-9:]*|:*|*:) die BOOTSTRAP_CAPACITY_UNVERIFIED;; esac
|
|
117
|
+
[ "$free_kib" -ge 2097152 ] && [ "$free_inodes" -ge 50000 ] || die BOOTSTRAP_DISK_CAPACITY_REQUIRED
|
|
118
|
+
phase=source-transfer
|
|
119
|
+
if [ "$source_mode" = transfer ]; then
|
|
120
|
+
bundle=$(mktemp "$root/source.XXXXXX")
|
|
121
|
+
# Leave no executable payload copies; this temporary file is only Git data.
|
|
122
|
+
base64 -d >"$bundle"
|
|
123
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
124
|
+
printf '%s %s\n' "$bundle_sha" "$bundle" | sha256sum -c - >/dev/null 2>&1 || die SOURCE_DIGEST_MISMATCH
|
|
125
|
+
else
|
|
126
|
+
[ "$(shasum -a 256 "$bundle" | cut -d' ' -f1)" = "$bundle_sha" ] || die SOURCE_DIGEST_MISMATCH
|
|
127
|
+
fi
|
|
128
|
+
fi
|
|
129
|
+
phase=prerequisites
|
|
130
|
+
if [ "$platform" = Linux ]; then
|
|
131
|
+
missing=''
|
|
132
|
+
for package in git curl ca-certificates unzip xz-utils build-essential python3 ansible-core; do
|
|
133
|
+
dpkg-query -W -f='${db:Status-Status}' "$package" 2>/dev/null | grep -qx installed || missing="$missing $package"
|
|
134
|
+
done
|
|
135
|
+
if [ -n "$missing" ]; then
|
|
136
|
+
sudo -n true >/dev/null 2>&1 || die SUDO_AUTHORIZATION_REQUIRED
|
|
137
|
+
# APT's own messages, in a private file read only for fixed patterns and
|
|
138
|
+
# removed on exit: they tell a held lock, DNS, an unreachable archive and a
|
|
139
|
+
# full disk apart. Nothing of them is reported.
|
|
140
|
+
apt_log=$(mktemp "$root/apt.XXXXXX")
|
|
141
|
+
pending="$root/prerequisites-pending"
|
|
142
|
+
# An installation this script started and that was interrupted (the shell
|
|
143
|
+
# killed, the machine powered off) leaves dpkg half-configured. Finishing
|
|
144
|
+
# our own transaction is safe; one fleet-sync did not start is reported.
|
|
145
|
+
# dpkg reads its database lock even to audit, which needs root.
|
|
146
|
+
if [ -n "$(sudo -n dpkg --audit 2>/dev/null)" ]; then
|
|
147
|
+
[ -f "$pending" ] || die DPKG_INTERRUPTED
|
|
148
|
+
# shellcheck disable=SC2024
|
|
149
|
+
sudo -n timeout 600 env DEBIAN_FRONTEND=noninteractive LC_ALL=C dpkg --configure -a >"$apt_log" 2>&1 || apt_failure APT_INSTALL_FAILED
|
|
150
|
+
fi
|
|
151
|
+
: >"$pending"
|
|
152
|
+
# The log belongs to the developer account; sudo only runs the command.
|
|
153
|
+
# shellcheck disable=SC2024
|
|
154
|
+
# Never remove APT locks or kill package processes. Timeout keeps genuine
|
|
155
|
+
# transactions bounded; Ubuntu archive signatures/TLS remain enabled.
|
|
156
|
+
sudo -n timeout 300 env LC_ALL=C apt-get -o DPkg::Lock::Timeout=120 update >"$apt_log" 2>&1 || apt_failure APT_UPDATE_FAILED
|
|
157
|
+
# shellcheck disable=SC2086,SC2024
|
|
158
|
+
sudo -n timeout 600 env DEBIAN_FRONTEND=noninteractive LC_ALL=C apt-get -o DPkg::Lock::Timeout=120 --no-install-recommends install -y $missing >>"$apt_log" 2>&1 || apt_failure APT_INSTALL_FAILED
|
|
159
|
+
rm -f -- "$pending" "$apt_log"
|
|
160
|
+
apt_log=''
|
|
161
|
+
fi
|
|
162
|
+
else
|
|
163
|
+
# Apple Command Line Tools provide git and python3; Ansible runs through the
|
|
164
|
+
# pinned uv tool runner installed by the shared mise toolchain below.
|
|
165
|
+
xcode-select -p >/dev/null 2>&1 || die XCODE_COMMAND_LINE_TOOLS_REQUIRED
|
|
166
|
+
command -v git >/dev/null 2>&1 || die XCODE_COMMAND_LINE_TOOLS_REQUIRED
|
|
167
|
+
fi
|
|
168
|
+
phase=source-verification
|
|
169
|
+
deployment="$root/revisions/$revision"
|
|
170
|
+
[ ! -L "$root/revisions" ] && [ ! -L "$deployment" ] || exit 1
|
|
171
|
+
mkdir -p "$deployment"
|
|
172
|
+
if [ ! -e "$deployment/.git" ]; then
|
|
173
|
+
[ -z "$(ls -A "$deployment")" ] || exit 1
|
|
174
|
+
git -C "$deployment" init -q >/dev/null 2>&1 || exit 1
|
|
175
|
+
fi
|
|
176
|
+
# An interrupted initial fetch or checkout leaves a repository without a
|
|
177
|
+
# commit, possibly with some of the revision's files written. Resume only
|
|
178
|
+
# that state: every file present must be a file of the revision, so the
|
|
179
|
+
# checkout may write over it. An existing commit or other work is never reset.
|
|
180
|
+
if ! git -C "$deployment" rev-parse --verify HEAD >/dev/null 2>&1; then
|
|
181
|
+
[ "$source_mode" = transfer ] || die SOURCE_CACHE_CHANGED
|
|
182
|
+
[ "$(git -C "$deployment" rev-parse --show-toplevel)" = "$deployment" ] || exit 1
|
|
183
|
+
# The bundle holds this commit without its history: declare the checkout
|
|
184
|
+
# shallow at it so Git accepts a commit whose parents are absent.
|
|
185
|
+
printf '%s\n' "$revision" >"$deployment/.git/shallow"
|
|
186
|
+
git -C "$deployment" fetch --no-tags "$bundle" "$revision" >/dev/null 2>&1 || exit 1
|
|
187
|
+
present=$(mktemp "$root/present.XXXXXX")
|
|
188
|
+
expected=$(mktemp "$root/expected.XXXXXX")
|
|
189
|
+
git -C "$deployment" -c core.quotePath=false ls-files --others | LC_ALL=C sort >"$present" || { rm -f -- "$present" "$expected"; die SOURCE_STATUS_FAILED; }
|
|
190
|
+
git -C "$deployment" -c core.quotePath=false ls-tree -r --name-only "$revision" | LC_ALL=C sort >"$expected" || { rm -f -- "$present" "$expected"; die SOURCE_STATUS_FAILED; }
|
|
191
|
+
foreign=$(LC_ALL=C comm -23 "$present" "$expected")
|
|
192
|
+
rm -f -- "$present" "$expected"
|
|
193
|
+
[ -z "$foreign" ] || die SOURCE_CHECKOUT_CONFLICT
|
|
194
|
+
git -C "$deployment" checkout --force --detach "$revision" >/dev/null 2>&1 || exit 1
|
|
195
|
+
fi
|
|
196
|
+
[ -z "$bundle" ] || rm -f "$bundle"
|
|
197
|
+
bundle=''
|
|
198
|
+
[ "$(git -C "$deployment" rev-parse HEAD)" = "$revision" ] || exit 1
|
|
199
|
+
[ "$(git -C "$deployment" rev-parse --show-toplevel)" = "$deployment" ] || exit 1
|
|
200
|
+
clean=$(git -C "$deployment" status --porcelain) || die SOURCE_STATUS_FAILED
|
|
201
|
+
[ -z "$clean" ] || die SOURCE_CHECKOUT_CONFLICT
|
|
202
|
+
phase=toolchain
|
|
203
|
+
cd "$deployment"
|
|
204
|
+
export PATH="$HOME/.local/bin:$HOME/.local/share/mise/shims:/usr/local/bin:/usr/bin:/bin"
|
|
205
|
+
status_file="$root/toolchain-status.json"
|
|
206
|
+
ready_file="$root/toolchain-ready"
|
|
207
|
+
[ ! -L "$status_file" ] && [ ! -L "$ready_file" ] || die FLEET_STATE_PATH_UNSAFE
|
|
208
|
+
# Warm: the same toolchain inputs (tool pins, locks and the setup code itself)
|
|
209
|
+
# passed within a day, so a revision that changes none of them skips the full
|
|
210
|
+
# setup. Changed inputs, a failure or an older stamp run it; agent versions
|
|
211
|
+
# are still checked (and reinstalled on drift) by node convergence.
|
|
212
|
+
toolchain_key=$(git rev-parse HEAD:mise.toml HEAD:mise.lock HEAD:.mise HEAD:bun.lock HEAD:package.json HEAD:bootstrap 2>/dev/null | tr '\n' ' ')
|
|
213
|
+
[ -n "$toolchain_key" ] || toolchain_key=$revision
|
|
214
|
+
if [ "$(cat "$ready_file" 2>/dev/null)" = "$toolchain_key" ] && [ -n "$(find "$ready_file" -mmin -1440 2>/dev/null)" ]; then
|
|
215
|
+
toolchain_warm=1
|
|
216
|
+
else
|
|
217
|
+
toolchain_warm=''
|
|
218
|
+
rm -f -- "$status_file" "$ready_file"
|
|
219
|
+
fi
|
|
220
|
+
if command -v timeout >/dev/null 2>&1; then bounded='timeout 1800'; else bounded=''; fi
|
|
221
|
+
# shellcheck disable=SC2086
|
|
222
|
+
if [ -z "$toolchain_warm" ] && ! $bounded bash bootstrap/bootstrap.sh --worker --non-interactive --status-file "$status_file" >/dev/null 2>&1; then
|
|
223
|
+
tool_phase=$(sed -n 's/.*"phase":"\([a-z0-9-]*\)".*/\1/p' "$status_file" 2>/dev/null) || tool_phase=''
|
|
224
|
+
case "$tool_phase" in
|
|
225
|
+
managed-config) die TOOLCHAIN_CONFIG_CONFLICT;;
|
|
226
|
+
mise) die TOOLCHAIN_MISE_FAILED;;
|
|
227
|
+
aws) die TOOLCHAIN_AWS_FAILED;;
|
|
228
|
+
t3) die TOOLCHAIN_T3_FAILED;;
|
|
229
|
+
global-config|shell) die TOOLCHAIN_PATH_CONFIGURATION_FAILED;;
|
|
230
|
+
dependencies) die TOOLCHAIN_FROZEN_DEPENDENCIES_FAILED;;
|
|
231
|
+
*) die TOOLCHAIN_CONVERGENCE_FAILED;;
|
|
232
|
+
esac
|
|
233
|
+
fi
|
|
234
|
+
# A new revision's checkout still needs its own (cached, frozen) dependencies.
|
|
235
|
+
if [ -n "$toolchain_warm" ] && [ ! -d node_modules ]; then
|
|
236
|
+
# shellcheck disable=SC2086
|
|
237
|
+
$bounded mise exec --locked bun -- bun install --frozen-lockfile >/dev/null 2>&1 || die TOOLCHAIN_FROZEN_DEPENDENCIES_FAILED
|
|
238
|
+
fi
|
|
239
|
+
[ -n "$toolchain_warm" ] || printf '%s' "$toolchain_key" >"$ready_file"
|
|
240
|
+
# Every synced revision is a full checkout with its own dependencies (about
|
|
241
|
+
# 1 GB). Keep this one, the one T3's providers launch from and the two most
|
|
242
|
+
# recently used others; the rest would slowly fill the disk.
|
|
243
|
+
phase=cleanup
|
|
244
|
+
touch "$deployment"
|
|
245
|
+
applied=$(basename "$(readlink "$root/current" 2>/dev/null || true)")
|
|
246
|
+
kept=0
|
|
247
|
+
# shellcheck disable=SC2012
|
|
248
|
+
for old in $(ls -1t "$root/revisions"); do
|
|
249
|
+
case "$old" in *[!a-f0-9]*) continue;; esac
|
|
250
|
+
[ "${#old}" = 40 ] || continue
|
|
251
|
+
[ "$old" != "$revision" ] && [ "$old" != "$applied" ] || continue
|
|
252
|
+
kept=$((kept + 1))
|
|
253
|
+
[ "$kept" -gt 2 ] || continue
|
|
254
|
+
rm -rf -- "${root:?}/revisions/$old" || true
|
|
255
|
+
done
|
|
256
|
+
phase=configuration
|
|
257
|
+
trap - EXIT INT TERM
|
|
258
|
+
exec mise exec --locked bun -- bun src/node-converge.ts "$node" "$revision"
|