@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,209 @@
|
|
|
1
|
+
import { AUTOMATED_REPAIRS, REPAIR_RECIPES, recipe } from '../repair-catalog';
|
|
2
|
+
import { repairPlan } from '../repair-plan';
|
|
3
|
+
import { makeReport, reportExitCode } from '../report';
|
|
4
|
+
import { FleetUsageError } from '../errors';
|
|
5
|
+
import { inspectRepair } from './repair-inspect';
|
|
6
|
+
import { applyRepairs } from './repair-apply';
|
|
7
|
+
import { repairScript } from './repair-script';
|
|
8
|
+
import { repairPlanHash } from './repair-guard';
|
|
9
|
+
import type { AutomatedRepair } from '../repair-catalog';
|
|
10
|
+
import type { RepairStep } from '../repair-plan';
|
|
11
|
+
import type { FleetServices } from './fleet';
|
|
12
|
+
import type { CliOptions } from './options';
|
|
13
|
+
import type { Settings } from './settings';
|
|
14
|
+
import type { CommandOutcome } from './contracts';
|
|
15
|
+
|
|
16
|
+
export function requestedRepairs(
|
|
17
|
+
value?: string
|
|
18
|
+
): AutomatedRepair[] | undefined {
|
|
19
|
+
if (value === undefined) return undefined;
|
|
20
|
+
const ids = value.split(',');
|
|
21
|
+
if (
|
|
22
|
+
!ids.length ||
|
|
23
|
+
ids.some((id) => !AUTOMATED_REPAIRS.includes(id as AutomatedRepair))
|
|
24
|
+
)
|
|
25
|
+
throw new FleetUsageError(
|
|
26
|
+
'--repair expects workspace,workstation,providers. Native logins require operator approval; inspect fleet-repair --list.'
|
|
27
|
+
);
|
|
28
|
+
return [...new Set(ids)] as AutomatedRepair[];
|
|
29
|
+
}
|
|
30
|
+
export function repairCatalog(): CommandOutcome {
|
|
31
|
+
return {
|
|
32
|
+
exitCode: 0,
|
|
33
|
+
data: {
|
|
34
|
+
version: 1,
|
|
35
|
+
command: 'repairs',
|
|
36
|
+
ok: true,
|
|
37
|
+
recipes: REPAIR_RECIPES,
|
|
38
|
+
credentials: [
|
|
39
|
+
'Authenticate each computer independently with the intended identity. Never clone OAuth caches, browser cookies, keychains, SSH private keys or Vault sessions.',
|
|
40
|
+
"Shared secrets come from the fleet's secrets provider with this machine's own access (see `secrets` in the fleet configuration).",
|
|
41
|
+
'A login does not grant every account, team or resource permission.',
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function executeRepairCommand(
|
|
48
|
+
command: string,
|
|
49
|
+
options: CliOptions,
|
|
50
|
+
settings: Settings,
|
|
51
|
+
services: FleetServices
|
|
52
|
+
): Promise<CommandOutcome> {
|
|
53
|
+
return command === 'repairs'
|
|
54
|
+
? repairCatalog()
|
|
55
|
+
: executeRepair(options, settings, services);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function executeRepair(
|
|
59
|
+
options: CliOptions,
|
|
60
|
+
settings: Settings,
|
|
61
|
+
services: FleetServices
|
|
62
|
+
): Promise<CommandOutcome> {
|
|
63
|
+
const selected = requestedRepairs(options.repair);
|
|
64
|
+
const before = await inspectRepair(options, settings, services);
|
|
65
|
+
const steps = selectRepairs(
|
|
66
|
+
repairPlan(before.reports, before.targets),
|
|
67
|
+
before,
|
|
68
|
+
selected
|
|
69
|
+
);
|
|
70
|
+
const planHash = repairPlanHash(steps, before.targets);
|
|
71
|
+
if (options.planHash && options.planHash !== planHash)
|
|
72
|
+
throw new FleetUsageError(
|
|
73
|
+
'Repair plan changed since review. Generate and review a fresh preview; nothing was applied.'
|
|
74
|
+
);
|
|
75
|
+
const initial = makeReport('repair', before.reports);
|
|
76
|
+
const healthOk = reportExitCode(initial, true) === 0;
|
|
77
|
+
const script = options.script
|
|
78
|
+
? repairScript(steps, before.targets)
|
|
79
|
+
: undefined;
|
|
80
|
+
if (!options.apply)
|
|
81
|
+
return {
|
|
82
|
+
exitCode: 0,
|
|
83
|
+
data: {
|
|
84
|
+
...initial,
|
|
85
|
+
ok: true,
|
|
86
|
+
applied: false,
|
|
87
|
+
planHash,
|
|
88
|
+
healthOk,
|
|
89
|
+
coverage: before.coverage,
|
|
90
|
+
steps,
|
|
91
|
+
...(script ? { script } : {}),
|
|
92
|
+
authorize:
|
|
93
|
+
'Preview only. Review effects, then repeat with --apply --yes. Only eligible automatic steps execute. Native logins and manual steps remain operator work.',
|
|
94
|
+
},
|
|
95
|
+
text: script ?? renderSteps(steps),
|
|
96
|
+
};
|
|
97
|
+
return appliedOutcome(steps, before, { options, settings, services });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function appliedOutcome(
|
|
101
|
+
steps: RepairStep[],
|
|
102
|
+
before: Awaited<ReturnType<typeof inspectRepair>>,
|
|
103
|
+
context: {
|
|
104
|
+
options: CliOptions;
|
|
105
|
+
settings: Settings;
|
|
106
|
+
services: FleetServices;
|
|
107
|
+
}
|
|
108
|
+
): Promise<CommandOutcome> {
|
|
109
|
+
const { options, settings, services } = context;
|
|
110
|
+
const executions = await applyRepairs(
|
|
111
|
+
before.targets,
|
|
112
|
+
steps,
|
|
113
|
+
options,
|
|
114
|
+
services
|
|
115
|
+
);
|
|
116
|
+
const after = await recheckRepair(before, options, settings, services);
|
|
117
|
+
const checked = makeReport('repair', after.reports);
|
|
118
|
+
const executionOk = executions.every((node) =>
|
|
119
|
+
node.checks.every((check) => check.status === 'pass')
|
|
120
|
+
);
|
|
121
|
+
const verified = reportExitCode(checked, true) === 0;
|
|
122
|
+
const ok = executionOk && verified;
|
|
123
|
+
return {
|
|
124
|
+
exitCode: ok ? 0 : 1,
|
|
125
|
+
data: {
|
|
126
|
+
...checked,
|
|
127
|
+
ok,
|
|
128
|
+
applied: true,
|
|
129
|
+
executionOk,
|
|
130
|
+
healthOk: verified,
|
|
131
|
+
coverage: after.coverage,
|
|
132
|
+
steps,
|
|
133
|
+
executions,
|
|
134
|
+
remaining: repairPlan(after.reports, after.targets),
|
|
135
|
+
completed: executions
|
|
136
|
+
.flatMap((node) => node.checks)
|
|
137
|
+
.filter((check) => check.status === 'pass').length,
|
|
138
|
+
},
|
|
139
|
+
text: `Repair commands ${executionOk ? 'completed' : 'need attention'}; fresh health ${verified ? 'verified' : 'still has failing or unverified checks'}.\n${renderSteps(repairPlan(after.reports, after.targets))}`,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function selectRepairs(
|
|
144
|
+
steps: RepairStep[],
|
|
145
|
+
inspection: Awaited<ReturnType<typeof inspectRepair>>,
|
|
146
|
+
selected?: AutomatedRepair[]
|
|
147
|
+
): RepairStep[] {
|
|
148
|
+
if (!selected) return steps;
|
|
149
|
+
const filtered = steps.filter(
|
|
150
|
+
(step) => !step.automatic || selected.includes(step.id as AutomatedRepair)
|
|
151
|
+
);
|
|
152
|
+
for (const target of inspection.targets) {
|
|
153
|
+
const report = inspection.reports.find(
|
|
154
|
+
(entry) => entry.node === target.name
|
|
155
|
+
);
|
|
156
|
+
if (!report?.snapshot) continue;
|
|
157
|
+
for (const id of selected)
|
|
158
|
+
if (!filtered.some((step) => step.node === target.name && step.id === id))
|
|
159
|
+
filtered.push({
|
|
160
|
+
...recipe(id),
|
|
161
|
+
node: target.name,
|
|
162
|
+
eligible: true,
|
|
163
|
+
checks: ['explicit:request'],
|
|
164
|
+
next: [
|
|
165
|
+
'Explicitly requested owner action; fresh diagnostics still follow.',
|
|
166
|
+
],
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
return filtered;
|
|
170
|
+
}
|
|
171
|
+
function renderSteps(steps: RepairStep[]): string {
|
|
172
|
+
return steps
|
|
173
|
+
.map(
|
|
174
|
+
(step) =>
|
|
175
|
+
`${step.node} / ${step.id}: ${step.automatic && step.eligible ? 'automatic after --apply --yes' : 'operator action'}\n ${step.action}`
|
|
176
|
+
)
|
|
177
|
+
.join('\n');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function recheckRepair(
|
|
181
|
+
before: Awaited<ReturnType<typeof inspectRepair>>,
|
|
182
|
+
options: CliOptions,
|
|
183
|
+
settings: Settings,
|
|
184
|
+
services: FleetServices
|
|
185
|
+
): Promise<Awaited<ReturnType<typeof inspectRepair>>> {
|
|
186
|
+
try {
|
|
187
|
+
return await inspectRepair(options, settings, services);
|
|
188
|
+
} catch {
|
|
189
|
+
return {
|
|
190
|
+
reports: before.reports.map((report) => ({
|
|
191
|
+
node: report.node,
|
|
192
|
+
checks: [
|
|
193
|
+
{
|
|
194
|
+
id: 'repair:recheck',
|
|
195
|
+
status: 'fail',
|
|
196
|
+
summary:
|
|
197
|
+
'Fresh diagnostics could not complete after repair. Earlier diagnostics are not being treated as fresh; inspect this computer and rerun health.',
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
})),
|
|
201
|
+
targets: [],
|
|
202
|
+
coverage: {
|
|
203
|
+
...before.coverage,
|
|
204
|
+
limitation:
|
|
205
|
+
'Post-repair diagnostics failed. Execution results are retained; machine health remains unverified.',
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { mkdir, open, unlink } from 'node:fs/promises';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { parseConfig } from '../config';
|
|
4
|
+
import { FleetUsageError } from '../errors';
|
|
5
|
+
import type { FleetConfig, FleetNode } from '../types';
|
|
6
|
+
import { capabilities } from './options';
|
|
7
|
+
import type { CliOptions } from './options';
|
|
8
|
+
import { STATE_ROSTER, readRosterText, writeRosterText } from '../state/roster';
|
|
9
|
+
import {
|
|
10
|
+
mutateState,
|
|
11
|
+
stateContext,
|
|
12
|
+
type Standing,
|
|
13
|
+
type StateContext,
|
|
14
|
+
} from '../state/sync';
|
|
15
|
+
|
|
16
|
+
export async function readRoster(path: string): Promise<FleetConfig | null> {
|
|
17
|
+
try {
|
|
18
|
+
const text = await readRosterText(path);
|
|
19
|
+
return text === null ? null : parseConfig(JSON.parse(text) as unknown);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
if (error instanceof FleetUsageError) throw error;
|
|
22
|
+
throw new FleetUsageError(
|
|
23
|
+
'Cannot read a valid roster. Use fleet-nodes --schema or choose --config PATH.'
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The roster a fleet-sync needs: a missing one is an error, not an empty fleet. */
|
|
29
|
+
export async function loadRoster(path: string): Promise<FleetConfig> {
|
|
30
|
+
let value: unknown;
|
|
31
|
+
try {
|
|
32
|
+
const text = await readRosterText(path);
|
|
33
|
+
if (text === null) throw new Error();
|
|
34
|
+
value = JSON.parse(text);
|
|
35
|
+
} catch {
|
|
36
|
+
throw new FleetUsageError(
|
|
37
|
+
'Cannot read fleet config. Run devfleet fleet-node-add or use --config PATH.'
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
return parseConfig(value);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function nodeFromOptions(options: CliOptions): FleetNode {
|
|
44
|
+
if (!options.id || !options.ssh || !options.path)
|
|
45
|
+
throw new FleetUsageError(
|
|
46
|
+
'fleet-node-add requires --id NAME --ssh ALIAS --path /absolute/checkout.'
|
|
47
|
+
);
|
|
48
|
+
const node = {
|
|
49
|
+
id: options.id,
|
|
50
|
+
ssh: options.ssh,
|
|
51
|
+
workspace: options.path,
|
|
52
|
+
...(options.runtime ? { runtime: options.runtime } : {}),
|
|
53
|
+
required: capabilities(options.require ?? 'github,vault,t3,toolchain'),
|
|
54
|
+
};
|
|
55
|
+
return parseConfig({ version: 1, nodes: [node] }).nodes[0]!;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function appendNode(
|
|
59
|
+
existing: FleetConfig | null,
|
|
60
|
+
node: FleetNode
|
|
61
|
+
): FleetConfig {
|
|
62
|
+
return parseConfig({ version: 1, nodes: [...(existing?.nodes ?? []), node] });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface AddedNode {
|
|
66
|
+
roster: FleetConfig;
|
|
67
|
+
/** How the fleet state stands with the bucket; absent for a roster file. */
|
|
68
|
+
standing?: Standing;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function addNode(
|
|
72
|
+
path: string,
|
|
73
|
+
node: FleetNode,
|
|
74
|
+
apply: boolean,
|
|
75
|
+
context: () => StateContext = stateContext
|
|
76
|
+
): Promise<AddedNode> {
|
|
77
|
+
const save = async (): Promise<FleetConfig> => {
|
|
78
|
+
const roster = appendNode(await readRoster(path), node);
|
|
79
|
+
await writeRosterText(path, `${JSON.stringify(roster, null, 2)}\n`);
|
|
80
|
+
return roster;
|
|
81
|
+
};
|
|
82
|
+
if (!apply) return { roster: appendNode(await readRoster(path), node) };
|
|
83
|
+
if (path === STATE_ROSTER) {
|
|
84
|
+
const { value, standing } = await mutateState(context(), save);
|
|
85
|
+
return { roster: value, standing };
|
|
86
|
+
}
|
|
87
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
88
|
+
const lock = await acquireLock(path);
|
|
89
|
+
try {
|
|
90
|
+
return { roster: await save() };
|
|
91
|
+
} finally {
|
|
92
|
+
await lock.close();
|
|
93
|
+
await unlink(`${path}.lock`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function acquireLock(path: string) {
|
|
98
|
+
try {
|
|
99
|
+
return await open(`${path}.lock`, 'wx', 0o600);
|
|
100
|
+
} catch {
|
|
101
|
+
throw new FleetUsageError(
|
|
102
|
+
'Roster is locked or unwritable. Check permissions and concurrent writers; remove a stale .lock only after verifying no operation is running.'
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import Fuse from 'fuse.js';
|
|
2
|
+
import type { CommandSpec } from './contracts';
|
|
3
|
+
|
|
4
|
+
export function searchCommands(
|
|
5
|
+
commands: CommandSpec[],
|
|
6
|
+
term: string
|
|
7
|
+
): CommandSpec[] {
|
|
8
|
+
const query = term.trim().toLowerCase();
|
|
9
|
+
if (!query) return commands;
|
|
10
|
+
const words = query.split(/\s+/);
|
|
11
|
+
const exact = commands.filter((command) => {
|
|
12
|
+
const text = [
|
|
13
|
+
command.id,
|
|
14
|
+
command.title,
|
|
15
|
+
command.description,
|
|
16
|
+
command.group,
|
|
17
|
+
...command.keywords,
|
|
18
|
+
]
|
|
19
|
+
.join(' ')
|
|
20
|
+
.toLowerCase();
|
|
21
|
+
return words.every((word) => text.includes(word));
|
|
22
|
+
});
|
|
23
|
+
const fuzzy = new Fuse(commands, {
|
|
24
|
+
keys: ['id', 'title', 'keywords', 'description'],
|
|
25
|
+
threshold: 0.35,
|
|
26
|
+
ignoreLocation: true,
|
|
27
|
+
})
|
|
28
|
+
.search(query)
|
|
29
|
+
.map((result) => result.item);
|
|
30
|
+
return [
|
|
31
|
+
...new Map(
|
|
32
|
+
[...exact, ...fuzzy].map((command) => [command.id, command])
|
|
33
|
+
).values(),
|
|
34
|
+
];
|
|
35
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { access } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { PACKAGE_ROOT, packageInfo } from '../package-info';
|
|
4
|
+
import { STATE_ROSTER } from '../state/roster';
|
|
5
|
+
|
|
6
|
+
export interface Settings {
|
|
7
|
+
workspace: string;
|
|
8
|
+
/** A roster file, or `state:roster` for the roster in the fleet state. */
|
|
9
|
+
config: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function exists(path: string): Promise<boolean> {
|
|
13
|
+
try {
|
|
14
|
+
await access(path);
|
|
15
|
+
return true;
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function findUp(start: string, name: string): Promise<string | null> {
|
|
22
|
+
let current = resolve(start);
|
|
23
|
+
while (true) {
|
|
24
|
+
if (await exists(join(current, name))) return join(current, name);
|
|
25
|
+
const parent = dirname(current);
|
|
26
|
+
if (parent === current) return null;
|
|
27
|
+
current = parent;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A dev-fleet checkout, as opposed to any other project that has a mise.toml. */
|
|
32
|
+
const CHECKOUT_MARKER = 'bootstrap/fleet-manager-run.sh';
|
|
33
|
+
|
|
34
|
+
/** Where the CLI runs from. A published package is its own workspace, whatever
|
|
35
|
+
* project it is run in; code run from a checkout uses the dev-fleet checkout
|
|
36
|
+
* the command is started in (development).
|
|
37
|
+
*/
|
|
38
|
+
async function findCheckout(start: string): Promise<string> {
|
|
39
|
+
if (packageInfo().revision) return PACKAGE_ROOT;
|
|
40
|
+
let current = resolve(start);
|
|
41
|
+
while (true) {
|
|
42
|
+
if (await exists(join(current, CHECKOUT_MARKER))) return current;
|
|
43
|
+
const parent = dirname(current);
|
|
44
|
+
if (parent === current) return PACKAGE_ROOT;
|
|
45
|
+
current = parent;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function resolveSettings(
|
|
50
|
+
overrides: Partial<Settings> = {},
|
|
51
|
+
cwd = process.cwd(),
|
|
52
|
+
env = process.env
|
|
53
|
+
): Promise<Settings> {
|
|
54
|
+
const workspace = resolve(
|
|
55
|
+
overrides.workspace ??
|
|
56
|
+
env['PERSONAL_WORKSPACE'] ??
|
|
57
|
+
(await findCheckout(cwd))
|
|
58
|
+
);
|
|
59
|
+
// A roster file is named explicitly; otherwise it is kept in the fleet state.
|
|
60
|
+
const named =
|
|
61
|
+
overrides.config ??
|
|
62
|
+
env['PERSONAL_FLEET_CONFIG'] ??
|
|
63
|
+
(await findUp(cwd, '.fleet.local.json'));
|
|
64
|
+
const config =
|
|
65
|
+
!named || named === STATE_ROSTER ? STATE_ROSTER : resolve(named);
|
|
66
|
+
return { workspace, config };
|
|
67
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import type { CommandOutcome } from './contracts';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import type { Settings } from './settings';
|
|
8
|
+
|
|
9
|
+
interface SourceOperation {
|
|
10
|
+
title: string;
|
|
11
|
+
description: string;
|
|
12
|
+
keywords: string[];
|
|
13
|
+
mutation: boolean;
|
|
14
|
+
command: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Commands implemented by another owner in this checkout and streamed through. */
|
|
18
|
+
export const SOURCE_OPERATIONS: Record<string, SourceOperation> = {
|
|
19
|
+
cliproxy: {
|
|
20
|
+
title: 'CLIProxyAPI service',
|
|
21
|
+
description:
|
|
22
|
+
'Install, inspect, back up, recover and validate model health (model-health, model-suite) of the CLIProxyAPI service.',
|
|
23
|
+
keywords: ['cliproxy', 'codex', 'proxy', 'compose', 'backup', 'models'],
|
|
24
|
+
mutation: true,
|
|
25
|
+
command: ['python3', '-B', 'cliproxy/scripts/cli.py'],
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export function sourceOperation(args: string[]): string | undefined {
|
|
30
|
+
return args[0] && Object.hasOwn(SOURCE_OPERATIONS, args[0])
|
|
31
|
+
? args[0]
|
|
32
|
+
: undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** These owners install from Git (a service keeps the commit it runs), so
|
|
36
|
+
* they run from a checkout: the one this CLI is in, or, for an installed
|
|
37
|
+
* package, the fleet-owned checkout of the revision fleet-sync applied on
|
|
38
|
+
* this machine. Null before the first fleet-sync.
|
|
39
|
+
*/
|
|
40
|
+
export function ownerCheckout(
|
|
41
|
+
workspace: string,
|
|
42
|
+
home = homedir()
|
|
43
|
+
): string | null {
|
|
44
|
+
if (existsSync(join(workspace, '.git'))) return workspace;
|
|
45
|
+
const applied = join(home, '.local/share/personal/fleet/current');
|
|
46
|
+
return existsSync(join(applied, '.git')) ? applied : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const CHECKOUT_REQUIRED =
|
|
50
|
+
'This command runs from the revision fleet-sync applied on this machine, and none is applied yet. Run devfleet fleet-sync first.\n';
|
|
51
|
+
|
|
52
|
+
export async function forwardSourceOperation(
|
|
53
|
+
args: string[],
|
|
54
|
+
settings: Settings
|
|
55
|
+
): Promise<number> {
|
|
56
|
+
const operation = SOURCE_OPERATIONS[args[0]!]!;
|
|
57
|
+
const [runtime, ...ownerArgs] = operation.command;
|
|
58
|
+
const checkout = ownerCheckout(settings.workspace);
|
|
59
|
+
if (!checkout) {
|
|
60
|
+
process.stderr.write(CHECKOUT_REQUIRED);
|
|
61
|
+
return 1;
|
|
62
|
+
}
|
|
63
|
+
const resolved = ownerArgs.map((arg) =>
|
|
64
|
+
arg.includes('/') ? join(checkout, arg) : arg
|
|
65
|
+
);
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
const child = spawn(runtime!, [...resolved, ...args.slice(1)], {
|
|
68
|
+
cwd: checkout,
|
|
69
|
+
stdio: 'inherit',
|
|
70
|
+
});
|
|
71
|
+
child.on('error', () => {
|
|
72
|
+
process.stderr.write(
|
|
73
|
+
'This command requires python3 and the toolchain fleet-sync installs.\n'
|
|
74
|
+
);
|
|
75
|
+
resolve(1);
|
|
76
|
+
});
|
|
77
|
+
child.on('exit', (code) => resolve(code ?? 1));
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function registerSourceOperations(
|
|
82
|
+
program: Command,
|
|
83
|
+
defaults: Settings,
|
|
84
|
+
complete: (result: CommandOutcome) => void
|
|
85
|
+
): void {
|
|
86
|
+
for (const [name, operation] of Object.entries(SOURCE_OPERATIONS))
|
|
87
|
+
program
|
|
88
|
+
.command(`${name} [args...]`)
|
|
89
|
+
.description(operation.title)
|
|
90
|
+
.allowUnknownOption()
|
|
91
|
+
.addHelpText(
|
|
92
|
+
'after',
|
|
93
|
+
`\n${operation.description}\nArguments are forwarded to the owning implementation; run devfleet ${name} help for its usage.`
|
|
94
|
+
)
|
|
95
|
+
.action(async (args: string[]) => {
|
|
96
|
+
const exitCode = await forwardSourceOperation(
|
|
97
|
+
[name, ...args],
|
|
98
|
+
defaults
|
|
99
|
+
);
|
|
100
|
+
complete({ exitCode, data: { version: 1, ok: exitCode === 0 } });
|
|
101
|
+
});
|
|
102
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { deployment } from '../node-deployment';
|
|
2
|
+
import type { CommandOutcome } from '../contracts';
|
|
3
|
+
import type { Settings } from '../settings';
|
|
4
|
+
import { FleetUsageError } from '../../errors';
|
|
5
|
+
import { desired, nodeId, paths, type T3Options } from '../../t3/desired';
|
|
6
|
+
import { importT3, reconcileT3 } from '../../t3/reconcile';
|
|
7
|
+
|
|
8
|
+
export async function t3Command(
|
|
9
|
+
command: string,
|
|
10
|
+
options: T3Options,
|
|
11
|
+
settings: Settings
|
|
12
|
+
): Promise<CommandOutcome> {
|
|
13
|
+
const deployed = options.definition
|
|
14
|
+
? null
|
|
15
|
+
: await deployment(settings.workspace);
|
|
16
|
+
const workspace = deployed?.workspace ?? settings.workspace;
|
|
17
|
+
const target = paths(options, workspace);
|
|
18
|
+
if (command === 't3-apply' && options.import !== undefined)
|
|
19
|
+
return importCommand(target, options);
|
|
20
|
+
const instances = await declared(command, target.definition, workspace, {
|
|
21
|
+
...options,
|
|
22
|
+
id: nodeId(options.id ?? deployed?.node),
|
|
23
|
+
});
|
|
24
|
+
const apply = shouldApply(command, options);
|
|
25
|
+
if (apply) requireSupportedPlatform(instances);
|
|
26
|
+
const data = await reconcileT3(
|
|
27
|
+
target,
|
|
28
|
+
instances,
|
|
29
|
+
reconcileOptions(apply, options)
|
|
30
|
+
);
|
|
31
|
+
return {
|
|
32
|
+
exitCode: outcomeCode(command, data),
|
|
33
|
+
data: { command, ...data },
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function declared(
|
|
38
|
+
command: string,
|
|
39
|
+
definition: string,
|
|
40
|
+
workspace: string,
|
|
41
|
+
options: T3Options & { id: string }
|
|
42
|
+
): Promise<Record<string, unknown>> {
|
|
43
|
+
if (command === 't3-cleanup') return Promise.resolve({});
|
|
44
|
+
// An explicit definition is the complete instance set.
|
|
45
|
+
const roster =
|
|
46
|
+
options.roster ?? (options.definition ? async () => [] : undefined);
|
|
47
|
+
return desired(definition, workspace, options.id, roster);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function importCommand(
|
|
51
|
+
target: ReturnType<typeof paths>,
|
|
52
|
+
options: T3Options
|
|
53
|
+
): Promise<CommandOutcome> {
|
|
54
|
+
if (options.dryRun)
|
|
55
|
+
throw new FleetUsageError(
|
|
56
|
+
't3-apply --import adopts ownership and cannot be combined with --dry-run; inspect t3-apply --dry-run first.'
|
|
57
|
+
);
|
|
58
|
+
if (!options.import || !/^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/.test(options.import))
|
|
59
|
+
throw new FleetUsageError(
|
|
60
|
+
't3-apply --import expects an instance ID. Import authorizes management of that entire provider instance.'
|
|
61
|
+
);
|
|
62
|
+
return {
|
|
63
|
+
exitCode: 0,
|
|
64
|
+
data: { command: 't3-import', ...(await importT3(target, options.import)) },
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function reconcileOptions(apply: boolean, options: T3Options) {
|
|
69
|
+
return {
|
|
70
|
+
apply,
|
|
71
|
+
...(options.planHash ? { planHash: options.planHash } : {}),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function shouldApply(command: string, options: T3Options): boolean {
|
|
76
|
+
return ['t3-apply', 't3-cleanup'].includes(command) && !options.dryRun;
|
|
77
|
+
}
|
|
78
|
+
function outcomeCode(
|
|
79
|
+
command: string,
|
|
80
|
+
data: { ok: boolean; changed: boolean }
|
|
81
|
+
): number {
|
|
82
|
+
return !data.ok || (command === 't3-status' && data.changed) ? 1 : 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function requireSupportedPlatform(instances: Record<string, unknown>): void {
|
|
86
|
+
if (process.platform === 'win32' && Object.keys(instances).length)
|
|
87
|
+
throw new FleetUsageError(
|
|
88
|
+
'Managed Codex proxy execution requires macOS/Linux/WSL; native Windows settings were preserved.'
|
|
89
|
+
);
|
|
90
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import type { CommandOutcome } from '../contracts';
|
|
3
|
+
import { resolveSettings, type Settings } from '../settings';
|
|
4
|
+
import { t3Command } from './command';
|
|
5
|
+
import type { T3Options } from '../../t3/desired';
|
|
6
|
+
|
|
7
|
+
export const T3_COMMANDS = [
|
|
8
|
+
['t3-status', 'Check managed T3 configuration for drift'],
|
|
9
|
+
[
|
|
10
|
+
't3-apply',
|
|
11
|
+
'Reconcile declared T3 provider instances; --dry-run plans, --import adopts',
|
|
12
|
+
],
|
|
13
|
+
[
|
|
14
|
+
't3-cleanup',
|
|
15
|
+
'Remove only provider instances recorded in personal ownership state',
|
|
16
|
+
],
|
|
17
|
+
] as const;
|
|
18
|
+
|
|
19
|
+
export function registerT3(
|
|
20
|
+
program: Command,
|
|
21
|
+
defaults: Settings,
|
|
22
|
+
complete: (outcome: CommandOutcome) => void
|
|
23
|
+
): void {
|
|
24
|
+
for (const [name, description] of T3_COMMANDS) {
|
|
25
|
+
const command = program
|
|
26
|
+
.command(name)
|
|
27
|
+
.description(description)
|
|
28
|
+
.option('--id <name>', 'Node ID; defaults to hostname')
|
|
29
|
+
.option(
|
|
30
|
+
'--t3-home <path>',
|
|
31
|
+
'T3 base directory; defaults to T3CODE_HOME or ~/.t3'
|
|
32
|
+
)
|
|
33
|
+
.option(
|
|
34
|
+
'--definition <path>',
|
|
35
|
+
'Version 1 declarative T3 provider definition'
|
|
36
|
+
);
|
|
37
|
+
if (name !== 't3-status')
|
|
38
|
+
command
|
|
39
|
+
.option(
|
|
40
|
+
'--plan-hash <sha256>',
|
|
41
|
+
'Reject any changes since the reviewed plan'
|
|
42
|
+
)
|
|
43
|
+
.option('--dry-run', 'Preview without writing configuration or state');
|
|
44
|
+
if (name === 't3-apply')
|
|
45
|
+
command.option(
|
|
46
|
+
'--import <id>',
|
|
47
|
+
'Adopt an existing instance into personal ownership instead of reconciling'
|
|
48
|
+
);
|
|
49
|
+
command.action(async (_options, command: Command) => {
|
|
50
|
+
const options = command.optsWithGlobals<T3Options & Partial<Settings>>();
|
|
51
|
+
const settings = await resolveSettings({ ...defaults, ...options });
|
|
52
|
+
complete(await t3Command(name, options, settings));
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|