@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
package/bootstrap/npm.ts
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `bun run bootstrap`: the one-time npm setup for publishing this package.
|
|
3
|
+
*
|
|
4
|
+
* npm can publish new versions through OIDC but cannot create a package that
|
|
5
|
+
* does not exist yet. So once, with your own npm login: publish the seed
|
|
6
|
+
* version, make the Release workflow the package's trusted publisher, and tag
|
|
7
|
+
* the commit that was published. Every step measures first and skips what is
|
|
8
|
+
* already done, so it is safe to run again after any interruption.
|
|
9
|
+
*
|
|
10
|
+
* bun run bootstrap walk through and do it
|
|
11
|
+
* bun run bootstrap --yes do not ask before publishing
|
|
12
|
+
* bun run bootstrap --skip-check do not run `bun run check` first
|
|
13
|
+
*/
|
|
14
|
+
import { spawnSync } from 'node:child_process';
|
|
15
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
16
|
+
import { tmpdir } from 'node:os';
|
|
17
|
+
import { join, resolve } from 'node:path';
|
|
18
|
+
import { confirm } from '@inquirer/prompts';
|
|
19
|
+
|
|
20
|
+
export const SEED_VERSION = '0.1.0';
|
|
21
|
+
export const WORKFLOW_FILE = 'release.yml';
|
|
22
|
+
const MIN_NPM = [11, 10] as const;
|
|
23
|
+
|
|
24
|
+
export interface Exec {
|
|
25
|
+
/** Run quietly and return the output. */
|
|
26
|
+
capture(command: string[], cwd?: string): Result;
|
|
27
|
+
/** Run with the terminal attached, for prompts and browser sign-in. */
|
|
28
|
+
interactive(command: string[], cwd?: string): boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface Result {
|
|
32
|
+
ok: boolean;
|
|
33
|
+
stdout: string;
|
|
34
|
+
stderr: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface Target {
|
|
38
|
+
/** `@scope/name` from package.json. */
|
|
39
|
+
name: string;
|
|
40
|
+
/** `owner/repo` from package.json. */
|
|
41
|
+
repo: string;
|
|
42
|
+
root: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface State {
|
|
46
|
+
npmVersion: string | null;
|
|
47
|
+
user: string | null;
|
|
48
|
+
published: boolean;
|
|
49
|
+
/** Null until the package exists: there is nothing to look at yet. */
|
|
50
|
+
trusted: boolean | null;
|
|
51
|
+
tagLocal: boolean;
|
|
52
|
+
tagRemote: boolean;
|
|
53
|
+
/** Commit npm recorded for the seed version, when it was published. */
|
|
54
|
+
publishedCommit: string | null;
|
|
55
|
+
clean: boolean;
|
|
56
|
+
onMain: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type StepId = 'login' | 'publish' | 'trust' | 'tag';
|
|
60
|
+
|
|
61
|
+
export interface Step {
|
|
62
|
+
id: StepId;
|
|
63
|
+
title: string;
|
|
64
|
+
done: boolean;
|
|
65
|
+
/** Why the step cannot run right now. */
|
|
66
|
+
blocked: string | null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const seedTag = `v${SEED_VERSION}`;
|
|
70
|
+
|
|
71
|
+
export function readTarget(root: string): Target {
|
|
72
|
+
const manifest = JSON.parse(
|
|
73
|
+
readFileSync(join(root, 'package.json'), 'utf8')
|
|
74
|
+
) as { name: string; repository: string };
|
|
75
|
+
return {
|
|
76
|
+
name: manifest.name,
|
|
77
|
+
repo: manifest.repository.replace(/^github:/, ''),
|
|
78
|
+
root,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function npmIsRecent(version: string | null): boolean {
|
|
83
|
+
const [major = 0, minor = 0] = (version ?? '').split('.').map(Number);
|
|
84
|
+
return major > MIN_NPM[0] || (major === MIN_NPM[0] && minor >= MIN_NPM[1]);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function measure(exec: Exec, target: Target): State {
|
|
88
|
+
const { name, repo, root } = target;
|
|
89
|
+
const npm = exec.capture(['npm', '--version']);
|
|
90
|
+
const whoami = exec.capture(['npm', 'whoami']);
|
|
91
|
+
const view = exec.capture([
|
|
92
|
+
'npm',
|
|
93
|
+
'view',
|
|
94
|
+
`${name}@${SEED_VERSION}`,
|
|
95
|
+
'gitHead',
|
|
96
|
+
]);
|
|
97
|
+
const any = exec.capture(['npm', 'view', name, 'name']);
|
|
98
|
+
if (!any.ok && !/E404/.test(any.stderr))
|
|
99
|
+
throw new Error('Could not reach the npm registry. Check the network.');
|
|
100
|
+
const published = any.ok;
|
|
101
|
+
const trust = published
|
|
102
|
+
? exec.capture(['npm', 'trust', 'list', name, '--json'])
|
|
103
|
+
: null;
|
|
104
|
+
exec.capture(['git', 'fetch', '--quiet', 'origin', 'main', '--tags'], root);
|
|
105
|
+
const head = exec.capture(['git', 'rev-parse', 'HEAD'], root).stdout.trim();
|
|
106
|
+
const main = exec
|
|
107
|
+
.capture(['git', 'rev-parse', 'origin/main'], root)
|
|
108
|
+
.stdout.trim();
|
|
109
|
+
return {
|
|
110
|
+
npmVersion: npm.ok ? npm.stdout.trim() : null,
|
|
111
|
+
user: whoami.ok ? whoami.stdout.trim() : null,
|
|
112
|
+
published,
|
|
113
|
+
trusted: trust
|
|
114
|
+
? trust.ok &&
|
|
115
|
+
trust.stdout.includes(repo) &&
|
|
116
|
+
trust.stdout.includes(WORKFLOW_FILE)
|
|
117
|
+
: null,
|
|
118
|
+
tagLocal: exec.capture(
|
|
119
|
+
['git', 'rev-parse', '--verify', '--quiet', `refs/tags/${seedTag}`],
|
|
120
|
+
root
|
|
121
|
+
).ok,
|
|
122
|
+
tagRemote:
|
|
123
|
+
exec
|
|
124
|
+
.capture(
|
|
125
|
+
['git', 'ls-remote', '--tags', 'origin', `refs/tags/${seedTag}`],
|
|
126
|
+
root
|
|
127
|
+
)
|
|
128
|
+
.stdout.trim() !== '',
|
|
129
|
+
publishedCommit: view.ok ? view.stdout.trim() || null : null,
|
|
130
|
+
clean:
|
|
131
|
+
exec.capture(['git', 'status', '--porcelain'], root).stdout.trim() === '',
|
|
132
|
+
onMain: head !== '' && head === main,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function plan(state: State): Step[] {
|
|
137
|
+
const needsLogin = state.user === null;
|
|
138
|
+
const publishBlocked = !state.clean
|
|
139
|
+
? 'The working tree has uncommitted changes; the seed is published from a commit.'
|
|
140
|
+
: !state.onMain
|
|
141
|
+
? 'HEAD is not origin/main. Merge the change, then run this on main.'
|
|
142
|
+
: null;
|
|
143
|
+
return [
|
|
144
|
+
{
|
|
145
|
+
id: 'login',
|
|
146
|
+
title: 'Sign in to npm',
|
|
147
|
+
done: !needsLogin,
|
|
148
|
+
blocked: null,
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
id: 'publish',
|
|
152
|
+
title: `Publish ${SEED_VERSION}, the seed version`,
|
|
153
|
+
done: state.published,
|
|
154
|
+
blocked: state.published ? null : publishBlocked,
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
id: 'trust',
|
|
158
|
+
title: `Trust ${WORKFLOW_FILE} to publish every later version`,
|
|
159
|
+
done: state.trusted === true,
|
|
160
|
+
blocked: null,
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
id: 'tag',
|
|
164
|
+
title: `Tag ${seedTag} on the commit that was published`,
|
|
165
|
+
done: state.tagRemote,
|
|
166
|
+
blocked: null,
|
|
167
|
+
},
|
|
168
|
+
];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export interface Deps {
|
|
172
|
+
exec: Exec;
|
|
173
|
+
/** Publish the seed version from a checkout of HEAD. */
|
|
174
|
+
seed(target: Target): boolean;
|
|
175
|
+
log(line: string): void;
|
|
176
|
+
ask(question: string): Promise<boolean>;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
interface Options {
|
|
180
|
+
yes: boolean;
|
|
181
|
+
skipCheck: boolean;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function show(deps: Deps, state: State, steps: Step[]): void {
|
|
185
|
+
deps.log(
|
|
186
|
+
`npm ${state.npmVersion ?? 'missing'} · ${state.user ? `signed in as ${state.user}` : 'not signed in'}`
|
|
187
|
+
);
|
|
188
|
+
for (const step of steps)
|
|
189
|
+
deps.log(
|
|
190
|
+
`${step.done ? '✓' : step.blocked ? '✗' : '→'} ${step.title}${step.blocked ? ` (${step.blocked})` : ''}`
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Publish from a throwaway checkout of HEAD so the working tree is never
|
|
195
|
+
* edited and npm records the commit it published.
|
|
196
|
+
*/
|
|
197
|
+
function publishSeed(exec: Exec, target: Target): boolean {
|
|
198
|
+
const dir = mkdtempSync(join(tmpdir(), 'dev-fleet-seed-'));
|
|
199
|
+
const checkout = join(dir, 'checkout');
|
|
200
|
+
try {
|
|
201
|
+
exec.capture(
|
|
202
|
+
['git', 'worktree', 'add', '--detach', checkout, 'HEAD'],
|
|
203
|
+
target.root
|
|
204
|
+
);
|
|
205
|
+
const file = join(checkout, 'package.json');
|
|
206
|
+
const manifest = JSON.parse(readFileSync(file, 'utf8')) as object;
|
|
207
|
+
writeFileSync(
|
|
208
|
+
file,
|
|
209
|
+
`${JSON.stringify({ ...manifest, version: SEED_VERSION }, null, 2)}\n`
|
|
210
|
+
);
|
|
211
|
+
return exec.interactive(['npm', 'publish', '--access', 'public'], checkout);
|
|
212
|
+
} finally {
|
|
213
|
+
exec.capture(
|
|
214
|
+
['git', 'worktree', 'remove', '--force', checkout],
|
|
215
|
+
target.root
|
|
216
|
+
);
|
|
217
|
+
rmSync(dir, { recursive: true, force: true });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function tagPublished(exec: Exec, target: Target, state: State): boolean {
|
|
222
|
+
const commit =
|
|
223
|
+
state.publishedCommit ??
|
|
224
|
+
exec
|
|
225
|
+
.capture(['npm', 'view', `${target.name}@${SEED_VERSION}`, 'gitHead'])
|
|
226
|
+
.stdout.trim();
|
|
227
|
+
if (!commit) return false;
|
|
228
|
+
if (!state.tagLocal)
|
|
229
|
+
exec.capture(['git', 'tag', seedTag, commit], target.root);
|
|
230
|
+
return exec.interactive(
|
|
231
|
+
['git', 'push', 'origin', `refs/tags/${seedTag}`],
|
|
232
|
+
target.root
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export async function bootstrap(
|
|
237
|
+
deps: Deps,
|
|
238
|
+
target: Target,
|
|
239
|
+
options: Options
|
|
240
|
+
): Promise<number> {
|
|
241
|
+
const { exec, log } = deps;
|
|
242
|
+
let state = measure(exec, target);
|
|
243
|
+
const steps = plan(state);
|
|
244
|
+
show(deps, state, steps);
|
|
245
|
+
if (!npmIsRecent(state.npmVersion)) {
|
|
246
|
+
log(
|
|
247
|
+
`npm ${MIN_NPM.join('.')} or newer is required (for "npm trust"). Run: npm install -g npm@latest`
|
|
248
|
+
);
|
|
249
|
+
return 1;
|
|
250
|
+
}
|
|
251
|
+
const todo = steps.filter((step) => !step.done);
|
|
252
|
+
if (todo.length === 0) {
|
|
253
|
+
log('Nothing to do: publishing is bootstrapped.');
|
|
254
|
+
return 0;
|
|
255
|
+
}
|
|
256
|
+
const blocked = todo.find((step) => step.blocked);
|
|
257
|
+
if (blocked) return 1;
|
|
258
|
+
if (
|
|
259
|
+
!options.yes &&
|
|
260
|
+
!(await deps.ask(`Run ${todo.length} step(s) as listed?`))
|
|
261
|
+
)
|
|
262
|
+
return 130;
|
|
263
|
+
if (todo.some((s) => s.id === 'publish') && !options.skipCheck) {
|
|
264
|
+
log('Running bun run check before publishing…');
|
|
265
|
+
if (!exec.interactive(['bun', 'run', 'check'], target.root)) return 1;
|
|
266
|
+
}
|
|
267
|
+
for (const step of todo) {
|
|
268
|
+
log(`→ ${step.title}`);
|
|
269
|
+
if (!(await run(step.id, deps, target, state))) {
|
|
270
|
+
log(`✗ ${step.title} failed; run bun run bootstrap again to continue.`);
|
|
271
|
+
return 1;
|
|
272
|
+
}
|
|
273
|
+
state = measure(exec, target);
|
|
274
|
+
}
|
|
275
|
+
show(deps, state, plan(state));
|
|
276
|
+
return plan(state).every((step) => step.done) ? 0 : 1;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function run(
|
|
280
|
+
id: StepId,
|
|
281
|
+
deps: Deps,
|
|
282
|
+
target: Target,
|
|
283
|
+
state: State
|
|
284
|
+
): Promise<boolean> {
|
|
285
|
+
const { exec } = deps;
|
|
286
|
+
switch (id) {
|
|
287
|
+
case 'login':
|
|
288
|
+
return exec.interactive(['npm', 'login', '--auth-type=web']);
|
|
289
|
+
case 'publish':
|
|
290
|
+
return deps.seed(target);
|
|
291
|
+
case 'trust':
|
|
292
|
+
return exec.interactive([
|
|
293
|
+
'npm',
|
|
294
|
+
'trust',
|
|
295
|
+
'github',
|
|
296
|
+
target.name,
|
|
297
|
+
'--repo',
|
|
298
|
+
target.repo,
|
|
299
|
+
'--file',
|
|
300
|
+
WORKFLOW_FILE,
|
|
301
|
+
'--allow-publish',
|
|
302
|
+
'--yes',
|
|
303
|
+
]);
|
|
304
|
+
case 'tag':
|
|
305
|
+
return tagPublished(exec, target, state);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const system: Exec = {
|
|
310
|
+
capture(command, cwd) {
|
|
311
|
+
const result = spawnSync(command[0]!, command.slice(1), {
|
|
312
|
+
...(cwd ? { cwd } : {}),
|
|
313
|
+
encoding: 'utf8',
|
|
314
|
+
});
|
|
315
|
+
return {
|
|
316
|
+
ok: result.status === 0,
|
|
317
|
+
stdout: result.stdout ?? '',
|
|
318
|
+
stderr: result.stderr ?? '',
|
|
319
|
+
};
|
|
320
|
+
},
|
|
321
|
+
interactive(command, cwd) {
|
|
322
|
+
const result = spawnSync(command[0]!, command.slice(1), {
|
|
323
|
+
...(cwd ? { cwd } : {}),
|
|
324
|
+
stdio: 'inherit',
|
|
325
|
+
});
|
|
326
|
+
return result.status === 0;
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
if (import.meta.main) {
|
|
331
|
+
const args = process.argv.slice(2);
|
|
332
|
+
const options: Options = {
|
|
333
|
+
yes: args.includes('--yes'),
|
|
334
|
+
skipCheck: args.includes('--skip-check'),
|
|
335
|
+
};
|
|
336
|
+
if (!options.yes && !process.stdin.isTTY) {
|
|
337
|
+
console.error('Not a terminal: pass --yes to run unattended.');
|
|
338
|
+
process.exit(2);
|
|
339
|
+
}
|
|
340
|
+
process.exitCode = await bootstrap(
|
|
341
|
+
{
|
|
342
|
+
exec: system,
|
|
343
|
+
seed: (target) => publishSeed(system, target),
|
|
344
|
+
log: (line) => console.log(line),
|
|
345
|
+
ask: (message) => confirm({ message, default: true }),
|
|
346
|
+
},
|
|
347
|
+
readTarget(resolve(import.meta.dir, '..')),
|
|
348
|
+
options
|
|
349
|
+
);
|
|
350
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** Nonsecret installer evidence; never contains commands, paths or raw errors. */
|
|
2
|
+
import { lstatSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { isAbsolute } from 'node:path';
|
|
4
|
+
export type SetupPhase =
|
|
5
|
+
| 'managed-config'
|
|
6
|
+
| 'mise'
|
|
7
|
+
| 'aws'
|
|
8
|
+
| 't3'
|
|
9
|
+
| 'global-config'
|
|
10
|
+
| 'shell'
|
|
11
|
+
| 'dependencies';
|
|
12
|
+
export function setupFailureCode(error: unknown): string | undefined {
|
|
13
|
+
const known: Record<string, string> = {
|
|
14
|
+
'Pinned T3 artifact unavailable.': 'T3_DOWNLOAD_UNAVAILABLE',
|
|
15
|
+
'T3 artifact checksum mismatch.': 'T3_CHECKSUM_MISMATCH',
|
|
16
|
+
'T3 cached artifact drift; preserve running service and review maintenance recovery.':
|
|
17
|
+
'T3_CACHE_DRIFT',
|
|
18
|
+
'T3 extraction failed.': 'T3_EXTRACTION_FAILED',
|
|
19
|
+
'Managed T3 files differ from the pinned artifact; maintenance is required.':
|
|
20
|
+
'T3_INSTALLATION_DRIFT',
|
|
21
|
+
'Pinned T3 executable verification failed.': 'T3_EXECUTABLE_FAILED',
|
|
22
|
+
};
|
|
23
|
+
return error instanceof Error && Object.hasOwn(known, error.message)
|
|
24
|
+
? known[error.message]
|
|
25
|
+
: undefined;
|
|
26
|
+
}
|
|
27
|
+
export function setupStatus(path: string | undefined) {
|
|
28
|
+
if (path && !isAbsolute(path))
|
|
29
|
+
throw new Error('Setup status path must be absolute.');
|
|
30
|
+
return (
|
|
31
|
+
phase: SetupPhase,
|
|
32
|
+
status: 'running' | 'failed' | 'passed',
|
|
33
|
+
code?: string
|
|
34
|
+
): void => {
|
|
35
|
+
if (!path) return;
|
|
36
|
+
try {
|
|
37
|
+
if (!lstatSync(path).isFile())
|
|
38
|
+
throw new Error('Unsafe setup status file.');
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
|
|
41
|
+
}
|
|
42
|
+
writeFileSync(
|
|
43
|
+
path,
|
|
44
|
+
JSON.stringify({ version: 1, phase, status, ...(code ? { code } : {}) }) +
|
|
45
|
+
'\n',
|
|
46
|
+
{
|
|
47
|
+
mode: 0o600,
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { workerTools } from './worker-tools';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
/** Used by both OS bootstraps, dev-fleet tools sync and dev-fleet update. No workspace dependencies. */
|
|
5
|
+
import { setupStatus, setupFailureCode, type SetupPhase } from './setup-status';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import {
|
|
8
|
+
assertManagedConfig,
|
|
9
|
+
globalConfigDir,
|
|
10
|
+
publishGlobalConfig,
|
|
11
|
+
workspaceRoot,
|
|
12
|
+
} from './global-config';
|
|
13
|
+
import { configureShellPath } from './shell-path';
|
|
14
|
+
import { installNativeAws } from './native-aws';
|
|
15
|
+
import { installNativeT3 } from './native-t3';
|
|
16
|
+
|
|
17
|
+
function run(args: string[]): void {
|
|
18
|
+
const result = spawnSync(args[0]!, args.slice(1), {
|
|
19
|
+
cwd: workspaceRoot,
|
|
20
|
+
stdio: 'inherit',
|
|
21
|
+
});
|
|
22
|
+
if (result.error) throw result.error;
|
|
23
|
+
if (result.status !== 0)
|
|
24
|
+
throw new Error(`${args.join(' ')} failed (${result.status})`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function syncTools(
|
|
28
|
+
observe: (phase: SetupPhase) => void = () => {},
|
|
29
|
+
worker = false
|
|
30
|
+
): Promise<void> {
|
|
31
|
+
observe('managed-config');
|
|
32
|
+
const dir = globalConfigDir();
|
|
33
|
+
assertManagedConfig(dir);
|
|
34
|
+
observe('mise');
|
|
35
|
+
const config = Bun.TOML.parse(
|
|
36
|
+
readFileSync(join(workspaceRoot, 'mise.toml'), 'utf8')
|
|
37
|
+
) as { tools: Record<string, unknown>; vars: { worker_tools: unknown } };
|
|
38
|
+
const selected = worker
|
|
39
|
+
? Object.keys(workerTools(config.tools, config.vars.worker_tools))
|
|
40
|
+
: [];
|
|
41
|
+
run(['mise', 'install', '--locked', ...selected]);
|
|
42
|
+
observe('aws');
|
|
43
|
+
if (!worker) await installNativeAws();
|
|
44
|
+
observe('t3');
|
|
45
|
+
await installNativeT3();
|
|
46
|
+
observe('global-config');
|
|
47
|
+
publishGlobalConfig(dir);
|
|
48
|
+
run(['mise', 'reshim']);
|
|
49
|
+
observe('shell');
|
|
50
|
+
configureShellPath();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (import.meta.main) {
|
|
54
|
+
const flag = process.argv.indexOf('--status-file');
|
|
55
|
+
const report = setupStatus(flag < 0 ? undefined : process.argv[flag + 1]);
|
|
56
|
+
let phase: SetupPhase = 'managed-config';
|
|
57
|
+
try {
|
|
58
|
+
await syncTools((current) => {
|
|
59
|
+
phase = current;
|
|
60
|
+
report(phase, 'running');
|
|
61
|
+
}, process.argv.includes('--worker'));
|
|
62
|
+
if (!process.argv.includes('--tools-only')) {
|
|
63
|
+
phase = 'dependencies';
|
|
64
|
+
report(phase, 'running');
|
|
65
|
+
run([
|
|
66
|
+
'mise',
|
|
67
|
+
'exec',
|
|
68
|
+
'--locked',
|
|
69
|
+
'--',
|
|
70
|
+
'bun',
|
|
71
|
+
'install',
|
|
72
|
+
'--frozen-lockfile',
|
|
73
|
+
]);
|
|
74
|
+
}
|
|
75
|
+
report(phase, 'passed');
|
|
76
|
+
} catch (error) {
|
|
77
|
+
report(phase, 'failed', setupFailureCode(error));
|
|
78
|
+
throw new Error(
|
|
79
|
+
`Shared toolchain phase ${phase} failed; inspect the declared installer prerequisites and retry.`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
console.log(
|
|
83
|
+
'Shared toolchain installed globally. Reopen terminals/editors, or refresh this terminal with: exec "$SHELL" -l (Unix).'
|
|
84
|
+
);
|
|
85
|
+
console.log(
|
|
86
|
+
'Verify: mise exec -- bun bootstrap/doctor.ts (add --docker to require a running Docker engine).'
|
|
87
|
+
);
|
|
88
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/** Persistent shims make the same tool commands work in interactive shells and agents. */
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
|
|
7
|
+
export function appendOnce(path: string, block: string): void {
|
|
8
|
+
const current = existsSync(path) ? readFileSync(path, 'utf8') : '';
|
|
9
|
+
if (current.includes(block.trim())) return;
|
|
10
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
11
|
+
writeFileSync(path, `${current}\n${block}\n`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function miseDataDir(
|
|
15
|
+
platform = process.platform,
|
|
16
|
+
env = process.env,
|
|
17
|
+
home = homedir()
|
|
18
|
+
): string {
|
|
19
|
+
const fallback =
|
|
20
|
+
platform === 'win32'
|
|
21
|
+
? env['LOCALAPPDATA'] || join(home, 'AppData', 'Local')
|
|
22
|
+
: join(home, '.local', 'share');
|
|
23
|
+
return env['MISE_DATA_DIR'] || join(env['XDG_DATA_HOME'] || fallback, 'mise');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function persistWindowsPath(bin: string, shims: string): void {
|
|
27
|
+
const script =
|
|
28
|
+
"$paths = @($env:WORKSPACE_TOOL_SHIMS, $env:WORKSPACE_TOOL_BIN); $old = [Environment]::GetEnvironmentVariable('Path', 'User'); $new = (($paths + ($old -split ';') | Where-Object { $_ } | Select-Object -Unique) -join ';'); [Environment]::SetEnvironmentVariable('Path', $new, 'User')";
|
|
29
|
+
const result = spawnSync(
|
|
30
|
+
'powershell.exe',
|
|
31
|
+
['-NoProfile', '-Command', script],
|
|
32
|
+
{
|
|
33
|
+
stdio: 'inherit',
|
|
34
|
+
env: {
|
|
35
|
+
...process.env,
|
|
36
|
+
WORKSPACE_TOOL_BIN: bin,
|
|
37
|
+
WORKSPACE_TOOL_SHIMS: shims,
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
);
|
|
41
|
+
if (result.status !== 0)
|
|
42
|
+
throw new Error(
|
|
43
|
+
'Could not persist the user PATH. See docs/machine-setup.md.'
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function configurePosixShellPath(
|
|
48
|
+
bin: string,
|
|
49
|
+
shims: string,
|
|
50
|
+
home = homedir(),
|
|
51
|
+
zdotdir = process.env['ZDOTDIR'] || home
|
|
52
|
+
): void {
|
|
53
|
+
const quote = (value: string) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
54
|
+
const paths = quote(`${shims}:${bin}`);
|
|
55
|
+
const legacy = `# dev-fleet toolchain\nexport PATH=${paths}:"$PATH"`;
|
|
56
|
+
const block = `# dev-fleet toolchain\ncase ":$PATH:" in\n *${quote(`:${shims}:${bin}:`)}*) ;;\n *) export PATH=${paths}:"$PATH" ;;\nesac`;
|
|
57
|
+
const targets = new Set([
|
|
58
|
+
join(home, '.profile'),
|
|
59
|
+
...['.bash_profile', '.bash_login']
|
|
60
|
+
.map((name) => join(home, name))
|
|
61
|
+
.filter(existsSync),
|
|
62
|
+
join(home, '.bashrc'),
|
|
63
|
+
join(zdotdir, '.zshenv'),
|
|
64
|
+
join(zdotdir, '.zshrc'),
|
|
65
|
+
]);
|
|
66
|
+
// Version managers and vendor installers (nvm, a native Claude installer's
|
|
67
|
+
// ~/.local/bin) prepend their own directories later in these files; the
|
|
68
|
+
// last line puts the pinned shims back in front, so every shell and the
|
|
69
|
+
// apps that inherit its PATH (T3) run the locked tool versions.
|
|
70
|
+
// Guarded like the block above so nested shells do not grow PATH.
|
|
71
|
+
const unguarded = `${TAIL_MARKER}\nexport PATH=${quote(shims)}:"$PATH"\n`;
|
|
72
|
+
const tail = `${TAIL_MARKER}\ncase "$PATH" in\n ${quote(shims)}:*) ;;\n *) export PATH=${quote(shims)}:"$PATH" ;;\nesac\n`;
|
|
73
|
+
for (const path of targets) {
|
|
74
|
+
const current = existsSync(path) ? readFileSync(path, 'utf8') : '';
|
|
75
|
+
const body = current
|
|
76
|
+
.replace(`${block}\n`, '')
|
|
77
|
+
.replace(legacy, '')
|
|
78
|
+
.split(tail)
|
|
79
|
+
.join('')
|
|
80
|
+
.split(unguarded)
|
|
81
|
+
.join('');
|
|
82
|
+
const next = `${block}\n${body.replace(/\n*$/, '\n')}${tail}`;
|
|
83
|
+
if (next === current) continue;
|
|
84
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
85
|
+
writeFileSync(path, next);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const TAIL_MARKER = '# pinned tool shims first (dev-fleet, keep this last)';
|
|
90
|
+
|
|
91
|
+
export function configureShellPath(): void {
|
|
92
|
+
const bin = join(homedir(), '.local', 'bin');
|
|
93
|
+
const shims = process.env['MISE_SHIMS_DIR'] || join(miseDataDir(), 'shims');
|
|
94
|
+
if (process.platform === 'win32') {
|
|
95
|
+
persistWindowsPath(bin, shims);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
configurePosixShellPath(bin, shims);
|
|
99
|
+
const fishQuote = (value: string) =>
|
|
100
|
+
`'${value.replaceAll('\\', '\\\\').replaceAll("'", "\\'")}'`;
|
|
101
|
+
appendOnce(
|
|
102
|
+
join(homedir(), '.config/fish/config.fish'),
|
|
103
|
+
`# dev-fleet toolchain\nfish_add_path --prepend ${fishQuote(shims)} ${fishQuote(bin)}`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Verify the pinned T3 archive against its user-owned installation.
|
|
2
|
+
|
|
3
|
+
Release-builder numeric identities are not portable. All installed members must
|
|
4
|
+
instead belong to the invoking development user; content, modes and links must
|
|
5
|
+
still match. This reads the archive and installation without modifying either.
|
|
6
|
+
"""
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import stat
|
|
10
|
+
import sys
|
|
11
|
+
import tarfile
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def verify(archive, root):
|
|
15
|
+
root = Path(root).resolve()
|
|
16
|
+
with tarfile.open(archive, 'r:gz') as source:
|
|
17
|
+
for member in source:
|
|
18
|
+
path = root / member.name
|
|
19
|
+
if not path.parent.resolve().is_relative_to(root):
|
|
20
|
+
return False
|
|
21
|
+
observed = path.lstat()
|
|
22
|
+
if observed.st_uid != os.getuid():
|
|
23
|
+
return False
|
|
24
|
+
if member.issym():
|
|
25
|
+
if not stat.S_ISLNK(observed.st_mode) or os.readlink(path) != member.linkname:
|
|
26
|
+
return False
|
|
27
|
+
elif member.isdir():
|
|
28
|
+
if not stat.S_ISDIR(observed.st_mode):
|
|
29
|
+
return False
|
|
30
|
+
elif member.isfile():
|
|
31
|
+
if not stat.S_ISREG(observed.st_mode) or observed.st_size != member.size:
|
|
32
|
+
return False
|
|
33
|
+
if stat.S_IMODE(observed.st_mode) != member.mode:
|
|
34
|
+
return False
|
|
35
|
+
with source.extractfile(member) as expected, path.open('rb') as actual:
|
|
36
|
+
while chunk := expected.read(1024 * 1024):
|
|
37
|
+
if actual.read(len(chunk)) != chunk:
|
|
38
|
+
return False
|
|
39
|
+
else:
|
|
40
|
+
return False
|
|
41
|
+
return True
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
if __name__ == '__main__':
|
|
45
|
+
try:
|
|
46
|
+
valid = verify(sys.argv[1], sys.argv[2])
|
|
47
|
+
except (OSError, ValueError, tarfile.TarError):
|
|
48
|
+
valid = False
|
|
49
|
+
sys.exit(0 if valid else 1)
|