@crvouga/dev-fleet 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.mise/locks/aws-cdk/2.1146.1/aube-lock.yaml +24 -0
- package/.mise/locks/aws-cdk/2.1146.1/package.json +7 -0
- package/.mise/locks/azure-cli/2.91.0~a837e014/pyproject.toml +5 -0
- package/.mise/locks/azure-cli/2.91.0~a837e014/uv.lock +2794 -0
- package/.mise/locks/gemini/0.66.0~84aac8e6/aube-lock.yaml +123 -0
- package/.mise/locks/gemini/0.66.0~84aac8e6/package.json +12 -0
- package/.mise/locks/npm-devcontainers-cli/0.90.0/aube-lock.yaml +24 -0
- package/.mise/locks/npm-devcontainers-cli/0.90.0/package.json +7 -0
- package/.mise/locks/npm-modelcontextprotocol-inspector/2.13.0~4c109bea/aube-lock.yaml +1432 -0
- package/.mise/locks/npm-modelcontextprotocol-inspector/2.13.0~4c109bea/package.json +7 -0
- package/.mise/locks/npm-vercel/65.8.0/aube-lock.yaml +3595 -0
- package/.mise/locks/npm-vercel/65.8.0/package.json +7 -0
- package/.mise/locks/npm-wrangler/4.156.0/aube-lock.yaml +895 -0
- package/.mise/locks/npm-wrangler/4.156.0/package.json +7 -0
- package/.mise/locks/npm-yarnpkg-cli-dist/4.18.1/aube-lock.yaml +24 -0
- package/.mise/locks/npm-yarnpkg-cli-dist/4.18.1/package.json +7 -0
- package/LICENSE +21 -0
- package/README.md +157 -2
- package/agents/antigravity-proxy.ts +3 -0
- package/agents/audit.md +89 -0
- package/agents/claude-proxy +5 -0
- package/agents/claude-proxy.ts +3 -0
- package/agents/codex-proxy.ts +3 -0
- package/agents/grok-proxy.ts +3 -0
- package/agents/managed-proxy.ts +55 -0
- package/agents/opencode-proxy.ts +3 -0
- package/agents/registry.json +209 -0
- package/agents/registry.schema.json +149 -0
- package/agents/setup.md +128 -0
- package/agents/t3.json +40 -0
- package/ansible/macos/manager.yml +30 -0
- package/ansible/macos/policy.json +17 -0
- package/ansible/monitoring/README.md +154 -0
- package/ansible/monitoring/agent-darwin.yml +198 -0
- package/ansible/monitoring/agent.yml +134 -0
- package/ansible/monitoring/beszel-agent.service +42 -0
- package/ansible/monitoring/compose.yml +74 -0
- package/ansible/monitoring/hub-darwin.yml +102 -0
- package/ansible/monitoring/lock.json +11 -0
- package/ansible/net/remote-probe.sh +13 -0
- package/ansible/ubuntu/authorize-sudo.sh +51 -0
- package/ansible/ubuntu/baseline.yml +89 -0
- package/ansible/ubuntu/disk-guard.py +104 -0
- package/ansible/ubuntu/policy.json +43 -0
- package/ansible/ubuntu/recovery.yml +15 -0
- package/ansible/ubuntu/t3/policy.json +7 -0
- package/ansible/ubuntu/t3-desktop.yml +220 -0
- package/ansible/ubuntu/t3-health.sh +33 -0
- package/ansible/ubuntu/t3-policy.yml +76 -0
- package/ansible/ubuntu/t3-route.yml +28 -0
- package/ansible/ubuntu/tailscale.yml +75 -0
- package/ansible/ubuntu/tasks/credentials.yml +47 -0
- package/ansible/ubuntu/tasks/docker.yml +31 -0
- package/ansible/ubuntu/tasks/packages.yml +56 -0
- package/ansible/ubuntu/tasks/power.yml +51 -0
- package/ansible/ubuntu/tasks/recovery.yml +39 -0
- package/ansible/ubuntu/tasks/resources.yml +252 -0
- package/ansible/ubuntu/tasks/services.yml +31 -0
- package/ansible/ubuntu/tasks/ssh.yml +71 -0
- package/ansible/ubuntu/tasks/user.yml +120 -0
- package/bin/devfleet.mjs +36 -0
- package/bootstrap/bootstrap.ps1 +50 -0
- package/bootstrap/bootstrap.sh +73 -0
- package/bootstrap/devfleet +7 -0
- package/bootstrap/docker-desktop.sh +33 -0
- package/bootstrap/doctor.ts +189 -0
- package/bootstrap/fleet-manager-run.sh +76 -0
- package/bootstrap/fleet-remote-bootstrap.sh +185 -0
- package/bootstrap/global-config.ts +113 -0
- package/bootstrap/install-launcher.sh +7 -0
- package/bootstrap/native-aws.ts +86 -0
- package/bootstrap/native-t3.ts +154 -0
- package/bootstrap/npm.ts +350 -0
- package/bootstrap/setup-status.ts +51 -0
- package/bootstrap/setup.ts +88 -0
- package/bootstrap/shell-path.ts +105 -0
- package/bootstrap/verify-t3-archive.py +49 -0
- package/bootstrap/worker-auth.ts +65 -0
- package/bootstrap/worker-github.ts +47 -0
- package/bootstrap/worker-tools.ts +24 -0
- package/cliproxy/AGENTS.md +11 -0
- package/cliproxy/README.md +269 -0
- package/cliproxy/clients/bootstrap.py +143 -0
- package/cliproxy/clients/launcher.py +57 -0
- package/cliproxy/compose.yaml +90 -0
- package/cliproxy/config.template.yaml +70 -0
- package/cliproxy/docs/cloud-adoption.md +16 -0
- package/cliproxy/docs/deployment.png +0 -0
- package/cliproxy/docs/deployment.svg +109 -0
- package/cliproxy/docs/health-workflow.example.yml +185 -0
- package/cliproxy/docs/migration.md +131 -0
- package/cliproxy/docs/model-health-report.schema.json +364 -0
- package/cliproxy/docs/model-health.md +393 -0
- package/cliproxy/image.lock.json +5 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-backup-check.plist +38 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-backup.plist +48 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-fleet.plist +32 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-gc.plist +38 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-monitor.plist +29 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-vault-renew.plist +31 -0
- package/cliproxy/models.json +1618 -0
- package/cliproxy/requirements.lock +78 -0
- package/cliproxy/scripts/accounts.py +38 -0
- package/cliproxy/scripts/audit.py +123 -0
- package/cliproxy/scripts/backup +234 -0
- package/cliproxy/scripts/check.py +34 -0
- package/cliproxy/scripts/checkpoint.py +226 -0
- package/cliproxy/scripts/cli.py +64 -0
- package/cliproxy/scripts/cliproxy_host.py +257 -0
- package/cliproxy/scripts/cloud_state.py +150 -0
- package/cliproxy/scripts/container-health +6 -0
- package/cliproxy/scripts/docker +24 -0
- package/cliproxy/scripts/external-health.py +79 -0
- package/cliproxy/scripts/fleet_config.py +43 -0
- package/cliproxy/scripts/fleet_sync.py +274 -0
- package/cliproxy/scripts/initialize +84 -0
- package/cliproxy/scripts/install-dependencies +48 -0
- package/cliproxy/scripts/install.py +406 -0
- package/cliproxy/scripts/model_health.py +16 -0
- package/cliproxy/scripts/modelhealth/__init__.py +5 -0
- package/cliproxy/scripts/modelhealth/checks.py +429 -0
- package/cliproxy/scripts/modelhealth/cli.py +117 -0
- package/cliproxy/scripts/modelhealth/config.py +186 -0
- package/cliproxy/scripts/modelhealth/emulator.py +536 -0
- package/cliproxy/scripts/modelhealth/expect.py +88 -0
- package/cliproxy/scripts/modelhealth/findings.py +183 -0
- package/cliproxy/scripts/modelhealth/protocol.py +330 -0
- package/cliproxy/scripts/modelhealth/redact.py +28 -0
- package/cliproxy/scripts/modelhealth/report.py +150 -0
- package/cliproxy/scripts/modelhealth/runner.py +390 -0
- package/cliproxy/scripts/modelhealth/scenario.py +131 -0
- package/cliproxy/scripts/modelhealth/schema.py +63 -0
- package/cliproxy/scripts/modelhealth/simulate.py +111 -0
- package/cliproxy/scripts/modelhealth/transport.py +213 -0
- package/cliproxy/scripts/monitor +68 -0
- package/cliproxy/scripts/ops +527 -0
- package/cliproxy/scripts/public-endpoint +127 -0
- package/cliproxy/scripts/recover_node.py +177 -0
- package/cliproxy/scripts/restore-verify +60 -0
- package/cliproxy/scripts/retention.py +144 -0
- package/cliproxy/scripts/service-start +7 -0
- package/cliproxy/scripts/validate-config +56 -0
- package/cliproxy/scripts/vault-access.py +57 -0
- package/cliproxy/scripts/vault-backup-login +55 -0
- package/cliproxy/scripts/vault-recovery +56 -0
- package/cliproxy/scripts/vault-renew +30 -0
- package/cliproxy/systemd/cliproxy-backup-check.service +10 -0
- package/cliproxy/systemd/cliproxy-backup-check.timer +8 -0
- package/cliproxy/systemd/cliproxy-backup.service +13 -0
- package/cliproxy/systemd/cliproxy-backup.timer +8 -0
- package/cliproxy/systemd/cliproxy-fleet.service +17 -0
- package/cliproxy/systemd/cliproxy-gc.service +13 -0
- package/cliproxy/systemd/cliproxy-gc.timer +8 -0
- package/cliproxy/systemd/cliproxy-monitor.service +8 -0
- package/cliproxy/systemd/cliproxy-monitor.timer +7 -0
- package/cliproxy/systemd/cliproxy-vault-renew.service +8 -0
- package/cliproxy/systemd/cliproxy-vault-renew.timer +7 -0
- package/cliproxy/vendor/LICENSE +22 -0
- package/cliproxy/vendor/config.upstream.yaml +1349 -0
- package/examples/fleet.json +95 -0
- package/examples/roster.json +13 -0
- package/mise.lock +1492 -0
- package/mise.toml +99 -0
- package/package.json +65 -4
- package/schema/fleet.schema.json +364 -0
- package/schema/roster.schema.json +77 -0
- package/src/agents/account-relay.ts +111 -0
- package/src/agents/accounts.ts +85 -0
- package/src/agents/audit.ts +77 -0
- package/src/agents/cli.ts +13 -0
- package/src/agents/command.ts +204 -0
- package/src/agents/converge.ts +123 -0
- package/src/agents/doctor.ts +107 -0
- package/src/agents/errors.ts +2 -0
- package/src/agents/files.ts +73 -0
- package/src/agents/fleet-verify.ts +156 -0
- package/src/agents/launch.ts +290 -0
- package/src/agents/native-verify.ts +226 -0
- package/src/agents/provider-errors.ts +50 -0
- package/src/agents/registry.ts +55 -0
- package/src/agents/render.ts +110 -0
- package/src/agents/round-trip.ts +192 -0
- package/src/agents/secrets.ts +13 -0
- package/src/agents/toolchain.ts +34 -0
- package/src/agents/verify-cache.ts +103 -0
- package/src/agents/verify.ts +127 -0
- package/src/auth-checks.ts +163 -0
- package/src/cli/agent-guide.ts +95 -0
- package/src/cli/app.ts +115 -0
- package/src/cli/cli.ts +21 -0
- package/src/cli/command-index.ts +50 -0
- package/src/cli/contracts.ts +25 -0
- package/src/cli/fleet-reference.ts +17 -0
- package/src/cli/fleet-sync-checkout.ts +91 -0
- package/src/cli/fleet-sync-enrollment.ts +144 -0
- package/src/cli/fleet-sync-forward.ts +134 -0
- package/src/cli/fleet-sync-manager.ts +193 -0
- package/src/cli/fleet-sync-monitoring.ts +185 -0
- package/src/cli/fleet-sync-output.ts +314 -0
- package/src/cli/fleet-sync-privilege.ts +27 -0
- package/src/cli/fleet-sync-progress.ts +109 -0
- package/src/cli/fleet-sync-remote.ts +236 -0
- package/src/cli/fleet-sync-report.ts +26 -0
- package/src/cli/fleet-sync-schedule.ts +138 -0
- package/src/cli/fleet-sync-vault.ts +166 -0
- package/src/cli/fleet-sync.ts +309 -0
- package/src/cli/fleet.ts +163 -0
- package/src/cli/managed-health.ts +219 -0
- package/src/cli/node-deployment.ts +34 -0
- package/src/cli/node-sync.ts +114 -0
- package/src/cli/node-t3.ts +31 -0
- package/src/cli/options.ts +141 -0
- package/src/cli/probe-source.ts +7 -0
- package/src/cli/program.ts +169 -0
- package/src/cli/register-fleet.ts +246 -0
- package/src/cli/registry.ts +158 -0
- package/src/cli/repair-apply.ts +67 -0
- package/src/cli/repair-guard.ts +11 -0
- package/src/cli/repair-inspect.ts +150 -0
- package/src/cli/repair-script.ts +76 -0
- package/src/cli/repair.ts +209 -0
- package/src/cli/roster.ts +89 -0
- package/src/cli/search.ts +35 -0
- package/src/cli/settings.ts +64 -0
- package/src/cli/source-operations.ts +78 -0
- package/src/cli/t3/command.ts +90 -0
- package/src/cli/t3/register.ts +55 -0
- package/src/cli/tailnet.ts +127 -0
- package/src/cli/ui-actions.ts +133 -0
- package/src/cli/ui-controller.ts +113 -0
- package/src/cli/ui-data.ts +106 -0
- package/src/cli/ui-execute.ts +163 -0
- package/src/cli/ui-repairs.ts +34 -0
- package/src/cli/ui-reports.ts +211 -0
- package/src/cli/ui-targets.ts +100 -0
- package/src/cli/ui-terminal.ts +109 -0
- package/src/cli/ui-types.ts +49 -0
- package/src/cli/ui-wizard.ts +101 -0
- package/src/cli/ui.ts +9 -0
- package/src/cli/worker-health-output.ts +37 -0
- package/src/config.ts +186 -0
- package/src/configure.ts +255 -0
- package/src/definition.ts +260 -0
- package/src/diagnostics.ts +92 -0
- package/src/doctor-check.ts +10 -0
- package/src/errors.ts +1 -0
- package/src/fingerprint.ts +120 -0
- package/src/fleet-config.ts +88 -0
- package/src/index.ts +6 -0
- package/src/input-hashes.ts +73 -0
- package/src/machine/local-source.ts +52 -0
- package/src/machine/lock-error.ts +2 -0
- package/src/machine/platform.ts +44 -0
- package/src/machine/source.ts +68 -0
- package/src/machine/state.ts +185 -0
- package/src/machine-health.ts +95 -0
- package/src/managed-source.ts +47 -0
- package/src/manager-failure.ts +48 -0
- package/src/manager-sync.ts +400 -0
- package/src/manager.ts +62 -0
- package/src/monitoring/apply.ts +171 -0
- package/src/monitoring/beszel.ts +191 -0
- package/src/monitoring/enroll.ts +110 -0
- package/src/monitoring/hub-access.ts +113 -0
- package/src/monitoring/hub-viewer.ts +142 -0
- package/src/monitoring/hub.ts +93 -0
- package/src/monitoring/manager-agent.ts +73 -0
- package/src/monitoring/manager-hub.ts +232 -0
- package/src/monitoring/remote.ts +65 -0
- package/src/network-definition.ts +83 -0
- package/src/node-converge-linux.ts +372 -0
- package/src/node-converge-shared.ts +83 -0
- package/src/node-converge.ts +40 -0
- package/src/onboard.ts +35 -0
- package/src/platform/ansible-receipt.ts +128 -0
- package/src/platform/ansible.ts +62 -0
- package/src/platform/darwin.ts +203 -0
- package/src/platform/detect.ts +11 -0
- package/src/platform/fallback.ts +110 -0
- package/src/platform/index.ts +38 -0
- package/src/platform/launchd.ts +98 -0
- package/src/platform/linux.ts +139 -0
- package/src/platform/ps.ts +45 -0
- package/src/platform/resource-verify.ts +128 -0
- package/src/platform/systemd-user.ts +95 -0
- package/src/platform/t3-policy.ts +137 -0
- package/src/platform/t3-route.ts +68 -0
- package/src/platform/t3-service.ts +75 -0
- package/src/platform/types.ts +123 -0
- package/src/platform/ubuntu-audit.ts +63 -0
- package/src/platform/ubuntu-verify.ts +105 -0
- package/src/platform/windows.ts +148 -0
- package/src/power-recovery.ts +55 -0
- package/src/probe.ts +51 -0
- package/src/protocol.ts +119 -0
- package/src/proxy-converge.ts +85 -0
- package/src/remote-bootstrap.ts +212 -0
- package/src/remote-entry.ts +89 -0
- package/src/remote-lock.ts +12 -0
- package/src/repair-catalog.ts +206 -0
- package/src/repair-plan.ts +78 -0
- package/src/repair-runner.ts +84 -0
- package/src/report.ts +71 -0
- package/src/runner.ts +104 -0
- package/src/secrets.ts +169 -0
- package/src/service-auth.ts +115 -0
- package/src/ssh.ts +135 -0
- package/src/sync.ts +104 -0
- package/src/t3/accounts.ts +81 -0
- package/src/t3/desired.ts +165 -0
- package/src/t3/files.ts +81 -0
- package/src/t3/names-check.ts +53 -0
- package/src/t3/names.ts +155 -0
- package/src/t3/plan.ts +82 -0
- package/src/t3/providers.ts +74 -0
- package/src/t3/reconcile.ts +123 -0
- package/src/t3-endpoint.ts +48 -0
- package/src/t3-linked-check.ts +101 -0
- package/src/tailnet-health.ts +231 -0
- package/src/tailnet-matching.ts +37 -0
- package/src/tailscale.ts +141 -0
- package/src/test-fixtures.ts +36 -0
- package/src/toolchain-checks.ts +83 -0
- package/src/topology.ts +24 -0
- package/src/transport.ts +122 -0
- package/src/types.ts +95 -0
- package/src/vault-config.ts +38 -0
- package/src/worker-access-checks.ts +115 -0
- package/src/worker-capacity.ts +34 -0
- package/src/worker-host-checks.ts +176 -0
- package/src/worker-policy.ts +64 -0
- package/src/worker-readiness.ts +64 -0
- package/src/workstation-checks.ts +29 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { automaticRepairs } from '../repair-plan';
|
|
2
|
+
import { recipe } from '../repair-catalog';
|
|
3
|
+
import { shellQuote, sshArgs } from '../ssh';
|
|
4
|
+
import type { RepairTarget, RepairStep } from '../repair-plan';
|
|
5
|
+
|
|
6
|
+
export function targetCommand(
|
|
7
|
+
target: RepairTarget,
|
|
8
|
+
commands: string[][]
|
|
9
|
+
): string {
|
|
10
|
+
const command = `cd -- ${shellQuote(target.root)} && ${commands.map((args) => args.map(shellQuote).join(' ')).join(' && ')}`;
|
|
11
|
+
if (!target.node) return command;
|
|
12
|
+
return [
|
|
13
|
+
...sshArgs(target.node)
|
|
14
|
+
.map((arg) => (arg === '-T' ? '-t' : arg))
|
|
15
|
+
.map(shellQuote),
|
|
16
|
+
shellQuote(command),
|
|
17
|
+
].join(' ');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Generates a reviewable operator script. It is never written or executed by this command. */
|
|
21
|
+
export function repairScript(
|
|
22
|
+
steps: RepairStep[],
|
|
23
|
+
targets: RepairTarget[]
|
|
24
|
+
): string {
|
|
25
|
+
const lines = [
|
|
26
|
+
'#!/bin/sh',
|
|
27
|
+
'set -eu',
|
|
28
|
+
'# Generated repair plan. Review before running in your own terminal.',
|
|
29
|
+
'# Automated steps use existing owners. Native login commands below are commented and require operator approval.',
|
|
30
|
+
'# POSIX shell required; Windows operators can use the personal commands directly.',
|
|
31
|
+
];
|
|
32
|
+
for (const target of targets) {
|
|
33
|
+
const actions = automaticRepairs(steps, target.name);
|
|
34
|
+
if (!actions.length) continue;
|
|
35
|
+
const commands = actions.flatMap((step) =>
|
|
36
|
+
step.id === 'workspace'
|
|
37
|
+
? [
|
|
38
|
+
[
|
|
39
|
+
'bun',
|
|
40
|
+
'src/cli/cli.ts',
|
|
41
|
+
'fleet-repair',
|
|
42
|
+
'--local',
|
|
43
|
+
'--repair',
|
|
44
|
+
'workspace',
|
|
45
|
+
'--apply',
|
|
46
|
+
'--yes',
|
|
47
|
+
'--json',
|
|
48
|
+
],
|
|
49
|
+
]
|
|
50
|
+
: recipe(step.id).commands
|
|
51
|
+
);
|
|
52
|
+
lines.push('', targetCommand(target, commands));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const step of steps.filter(
|
|
56
|
+
(entry) => !entry.automatic || !entry.eligible
|
|
57
|
+
)) {
|
|
58
|
+
lines.push(
|
|
59
|
+
'',
|
|
60
|
+
`# ${comment(step.node)} / ${comment(step.title)}`,
|
|
61
|
+
`# ${comment(step.action)}`
|
|
62
|
+
);
|
|
63
|
+
const target = targets.find((entry) => entry.name === step.node);
|
|
64
|
+
if (target && step.eligible && step.commands.length)
|
|
65
|
+
lines.push(`# ${targetCommand(target, step.commands)}`);
|
|
66
|
+
else for (const next of step.next) lines.push(`# ${comment(next)}`);
|
|
67
|
+
}
|
|
68
|
+
lines.push(
|
|
69
|
+
'',
|
|
70
|
+
'# Re-run devfleet fleet-status --tailnet --json for fresh whole-tailnet coverage.'
|
|
71
|
+
);
|
|
72
|
+
return `${lines.join('\n')}\n`;
|
|
73
|
+
}
|
|
74
|
+
function comment(value: string): string {
|
|
75
|
+
return value.replace(/[\r\n\x00-\x1f\x7f]/g, ' ');
|
|
76
|
+
}
|
|
@@ -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,89 @@
|
|
|
1
|
+
import { lstat, mkdir, open, readFile, rename, unlink } from 'node:fs/promises';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { parseConfig, record } from '../config';
|
|
5
|
+
import { FleetUsageError } from '../errors';
|
|
6
|
+
import type { FleetConfig, FleetNode } from '../types';
|
|
7
|
+
import { capabilities } from './options';
|
|
8
|
+
import type { CliOptions } from './options';
|
|
9
|
+
|
|
10
|
+
export async function readRoster(path: string): Promise<FleetConfig | null> {
|
|
11
|
+
try {
|
|
12
|
+
const info = await lstat(path);
|
|
13
|
+
if (!info.isFile() || info.size > 1024 * 1024)
|
|
14
|
+
throw new FleetUsageError(
|
|
15
|
+
'Roster must be a regular file smaller than 1 MiB; symlinks are not supported.'
|
|
16
|
+
);
|
|
17
|
+
return parseConfig(JSON.parse(await readFile(path, 'utf8')) as unknown);
|
|
18
|
+
} catch (error) {
|
|
19
|
+
if (record(error) && error['code'] === 'ENOENT') return null;
|
|
20
|
+
if (error instanceof FleetUsageError) throw error;
|
|
21
|
+
throw new FleetUsageError(
|
|
22
|
+
'Cannot read a valid roster. Use fleet-nodes --schema or choose --config PATH.'
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function nodeFromOptions(options: CliOptions): FleetNode {
|
|
28
|
+
if (!options.id || !options.ssh || !options.path)
|
|
29
|
+
throw new FleetUsageError(
|
|
30
|
+
'fleet-node-add requires --id NAME --ssh ALIAS --path /absolute/checkout.'
|
|
31
|
+
);
|
|
32
|
+
const node = {
|
|
33
|
+
id: options.id,
|
|
34
|
+
ssh: options.ssh,
|
|
35
|
+
workspace: options.path,
|
|
36
|
+
...(options.runtime ? { runtime: options.runtime } : {}),
|
|
37
|
+
required: capabilities(options.require ?? 'github,vault,t3,toolchain'),
|
|
38
|
+
};
|
|
39
|
+
return parseConfig({ version: 1, nodes: [node] }).nodes[0]!;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function appendNode(
|
|
43
|
+
existing: FleetConfig | null,
|
|
44
|
+
node: FleetNode
|
|
45
|
+
): FleetConfig {
|
|
46
|
+
return parseConfig({ version: 1, nodes: [...(existing?.nodes ?? []), node] });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function addNode(
|
|
50
|
+
path: string,
|
|
51
|
+
node: FleetNode,
|
|
52
|
+
apply: boolean
|
|
53
|
+
): Promise<FleetConfig> {
|
|
54
|
+
if (!apply) return appendNode(await readRoster(path), node);
|
|
55
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
56
|
+
const lock = await acquireLock(path);
|
|
57
|
+
try {
|
|
58
|
+
const roster = appendNode(await readRoster(path), node);
|
|
59
|
+
await atomicWrite(path, roster);
|
|
60
|
+
return roster;
|
|
61
|
+
} finally {
|
|
62
|
+
await lock.close();
|
|
63
|
+
await unlink(`${path}.lock`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function acquireLock(path: string) {
|
|
68
|
+
try {
|
|
69
|
+
return await open(`${path}.lock`, 'wx', 0o600);
|
|
70
|
+
} catch {
|
|
71
|
+
throw new FleetUsageError(
|
|
72
|
+
'Roster is locked or unwritable. Check permissions and concurrent writers; remove a stale .lock only after verifying no operation is running.'
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function atomicWrite(path: string, roster: FleetConfig): Promise<void> {
|
|
78
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
79
|
+
const handle = await open(temporary, 'wx', 0o600);
|
|
80
|
+
try {
|
|
81
|
+
await handle.writeFile(`${JSON.stringify(roster, null, 2)}\n`);
|
|
82
|
+
await handle.sync();
|
|
83
|
+
await handle.close();
|
|
84
|
+
await rename(temporary, path);
|
|
85
|
+
} finally {
|
|
86
|
+
await handle.close();
|
|
87
|
+
await unlink(temporary).catch(() => undefined);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -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,64 @@
|
|
|
1
|
+
import { access } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
export interface Settings {
|
|
7
|
+
workspace: string;
|
|
8
|
+
config: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function exists(path: string): Promise<boolean> {
|
|
12
|
+
try {
|
|
13
|
+
await access(path);
|
|
14
|
+
return true;
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function findUp(start: string, name: string): Promise<string | null> {
|
|
21
|
+
let current = resolve(start);
|
|
22
|
+
while (true) {
|
|
23
|
+
if (await exists(join(current, name))) return join(current, name);
|
|
24
|
+
const parent = dirname(current);
|
|
25
|
+
if (parent === current) return null;
|
|
26
|
+
current = parent;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A dev-fleet checkout, as opposed to any other project that has a mise.toml. */
|
|
31
|
+
const CHECKOUT_MARKER = 'bootstrap/fleet-manager-run.sh';
|
|
32
|
+
|
|
33
|
+
/** Where this CLI's own files live: the package root, also when run by npx. */
|
|
34
|
+
const packageRoot = (): string =>
|
|
35
|
+
fileURLToPath(new URL('../..', import.meta.url));
|
|
36
|
+
|
|
37
|
+
async function findCheckout(start: string): Promise<string> {
|
|
38
|
+
let current = resolve(start);
|
|
39
|
+
while (true) {
|
|
40
|
+
if (await exists(join(current, CHECKOUT_MARKER))) return current;
|
|
41
|
+
const parent = dirname(current);
|
|
42
|
+
if (parent === current) return packageRoot();
|
|
43
|
+
current = parent;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function resolveSettings(
|
|
48
|
+
overrides: Partial<Settings> = {},
|
|
49
|
+
cwd = process.cwd(),
|
|
50
|
+
env = process.env
|
|
51
|
+
): Promise<Settings> {
|
|
52
|
+
const workspace = resolve(
|
|
53
|
+
overrides.workspace ??
|
|
54
|
+
env['PERSONAL_WORKSPACE'] ??
|
|
55
|
+
(await findCheckout(cwd))
|
|
56
|
+
);
|
|
57
|
+
const config = resolve(
|
|
58
|
+
overrides.config ??
|
|
59
|
+
env['PERSONAL_FLEET_CONFIG'] ??
|
|
60
|
+
(await findUp(cwd, '.fleet.local.json')) ??
|
|
61
|
+
join(homedir(), '.config/personal/fleet.json')
|
|
62
|
+
);
|
|
63
|
+
return { workspace, config };
|
|
64
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import type { CommandOutcome } from './contracts';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import type { Settings } from './settings';
|
|
6
|
+
|
|
7
|
+
interface SourceOperation {
|
|
8
|
+
title: string;
|
|
9
|
+
description: string;
|
|
10
|
+
keywords: string[];
|
|
11
|
+
mutation: boolean;
|
|
12
|
+
command: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Commands implemented by another owner in this checkout and streamed through. */
|
|
16
|
+
export const SOURCE_OPERATIONS: Record<string, SourceOperation> = {
|
|
17
|
+
cliproxy: {
|
|
18
|
+
title: 'CLIProxyAPI service',
|
|
19
|
+
description:
|
|
20
|
+
'Install, inspect, back up, recover and validate model health (model-health, model-suite) of the CLIProxyAPI service.',
|
|
21
|
+
keywords: ['cliproxy', 'codex', 'proxy', 'compose', 'backup', 'models'],
|
|
22
|
+
mutation: true,
|
|
23
|
+
command: ['python3', '-B', 'cliproxy/scripts/cli.py'],
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function sourceOperation(args: string[]): string | undefined {
|
|
28
|
+
return args[0] && Object.hasOwn(SOURCE_OPERATIONS, args[0])
|
|
29
|
+
? args[0]
|
|
30
|
+
: undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function forwardSourceOperation(
|
|
34
|
+
args: string[],
|
|
35
|
+
settings: Settings
|
|
36
|
+
): Promise<number> {
|
|
37
|
+
const operation = SOURCE_OPERATIONS[args[0]!]!;
|
|
38
|
+
const [runtime, ...ownerArgs] = operation.command;
|
|
39
|
+
const resolved = ownerArgs.map((arg) =>
|
|
40
|
+
arg.includes('/') ? join(settings.workspace, arg) : arg
|
|
41
|
+
);
|
|
42
|
+
return new Promise((resolve) => {
|
|
43
|
+
const child = spawn(runtime!, [...resolved, ...args.slice(1)], {
|
|
44
|
+
cwd: settings.workspace,
|
|
45
|
+
stdio: 'inherit',
|
|
46
|
+
});
|
|
47
|
+
child.on('error', () => {
|
|
48
|
+
process.stderr.write(
|
|
49
|
+
'This command requires the dev-fleet checkout and its toolchain.\n'
|
|
50
|
+
);
|
|
51
|
+
resolve(1);
|
|
52
|
+
});
|
|
53
|
+
child.on('exit', (code) => resolve(code ?? 1));
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function registerSourceOperations(
|
|
58
|
+
program: Command,
|
|
59
|
+
defaults: Settings,
|
|
60
|
+
complete: (result: CommandOutcome) => void
|
|
61
|
+
): void {
|
|
62
|
+
for (const [name, operation] of Object.entries(SOURCE_OPERATIONS))
|
|
63
|
+
program
|
|
64
|
+
.command(`${name} [args...]`)
|
|
65
|
+
.description(operation.title)
|
|
66
|
+
.allowUnknownOption()
|
|
67
|
+
.addHelpText(
|
|
68
|
+
'after',
|
|
69
|
+
`\n${operation.description}\nArguments are forwarded to the owning implementation; run devfleet ${name} help for its usage.`
|
|
70
|
+
)
|
|
71
|
+
.action(async (args: string[]) => {
|
|
72
|
+
const exitCode = await forwardSourceOperation(
|
|
73
|
+
[name, ...args],
|
|
74
|
+
defaults
|
|
75
|
+
);
|
|
76
|
+
complete({ exitCode, data: { version: 1, ok: exitCode === 0 } });
|
|
77
|
+
});
|
|
78
|
+
}
|
|
@@ -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
|
+
}
|