@codyswann/lisa 2.316.2 → 2.317.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/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +139 -9
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/package.json +1 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-atlassian-access/SKILL.md +17 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-automation-status/SKILL.md +23 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-doctor/SKILL.md +33 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-implement/SKILL.md +17 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-notion-access/SKILL.md +17 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-remote-dispatch/SKILL.md +99 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-remote-dispatch/agents/openai.yaml +4 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-remote-dispatch/scripts/dispatch.mjs +264 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/SKILL.md +137 -51
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +217 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/envfile.mjs +78 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +105 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/providers.mjs +240 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/read-secret-note.mjs +82 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/resolve-secret.mjs +151 -176
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/rotate-secret.mjs +266 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/surfaces.mjs +147 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/validate-config.mjs +224 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-automations/SKILL.md +69 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-automations/scripts/generate-workflow.mjs +185 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/SKILL.md +150 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/agents/openai.yaml +4 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/assets/setup.sh +45 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +227 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/toolchain.mjs +169 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +186 -0
- package/plugins/lisa/commands/implement.md +5 -3
- package/plugins/lisa/commands/setup/remote-env.md +7 -0
- package/plugins/lisa/skills/lisa-atlassian-access/SKILL.md +17 -0
- package/plugins/lisa/skills/lisa-automation-status/SKILL.md +23 -0
- package/plugins/lisa/skills/lisa-doctor/SKILL.md +33 -0
- package/plugins/lisa/skills/lisa-implement/SKILL.md +17 -0
- package/plugins/lisa/skills/lisa-notion-access/SKILL.md +17 -0
- package/plugins/lisa/skills/lisa-remote-dispatch/SKILL.md +99 -0
- package/plugins/lisa/skills/lisa-remote-dispatch/agents/openai.yaml +4 -0
- package/plugins/lisa/skills/lisa-remote-dispatch/scripts/dispatch.mjs +264 -0
- package/plugins/lisa/skills/lisa-secrets-access/SKILL.md +138 -52
- package/plugins/lisa/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +217 -0
- package/plugins/lisa/skills/lisa-secrets-access/scripts/envfile.mjs +78 -0
- package/plugins/lisa/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +105 -0
- package/plugins/lisa/skills/lisa-secrets-access/scripts/providers.mjs +240 -0
- package/plugins/lisa/skills/lisa-secrets-access/scripts/read-secret-note.mjs +82 -0
- package/plugins/lisa/skills/lisa-secrets-access/scripts/resolve-secret.mjs +151 -176
- package/plugins/lisa/skills/lisa-secrets-access/scripts/rotate-secret.mjs +266 -0
- package/plugins/lisa/skills/lisa-secrets-access/scripts/surfaces.mjs +147 -0
- package/plugins/lisa/skills/lisa-secrets-access/scripts/validate-config.mjs +224 -0
- package/plugins/lisa/skills/lisa-setup-automations/SKILL.md +69 -0
- package/plugins/lisa/skills/lisa-setup-automations/scripts/generate-workflow.mjs +185 -0
- package/plugins/lisa/skills/lisa-setup-remote-env/SKILL.md +150 -0
- package/plugins/lisa/skills/lisa-setup-remote-env/agents/openai.yaml +4 -0
- package/plugins/lisa/skills/lisa-setup-remote-env/assets/setup.sh +45 -0
- package/plugins/lisa/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +227 -0
- package/plugins/lisa/skills/lisa-setup-remote-env/scripts/toolchain.mjs +169 -0
- package/plugins/lisa/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +186 -0
- package/plugins/lisa-agy/commands/lisa/implement.md +5 -3
- package/plugins/lisa-agy/commands/lisa/setup/remote-env.md +7 -0
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-atlassian-access/SKILL.md +17 -0
- package/plugins/lisa-agy/skills/lisa-automation-status/SKILL.md +23 -0
- package/plugins/lisa-agy/skills/lisa-doctor/SKILL.md +33 -0
- package/plugins/lisa-agy/skills/lisa-implement/SKILL.md +17 -0
- package/plugins/lisa-agy/skills/lisa-notion-access/SKILL.md +17 -0
- package/plugins/lisa-agy/skills/lisa-remote-dispatch/SKILL.md +99 -0
- package/plugins/lisa-agy/skills/lisa-remote-dispatch/scripts/dispatch.mjs +264 -0
- package/plugins/lisa-agy/skills/lisa-secrets-access/SKILL.md +138 -52
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +217 -0
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/envfile.mjs +78 -0
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +105 -0
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/providers.mjs +240 -0
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/read-secret-note.mjs +82 -0
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/resolve-secret.mjs +151 -176
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/rotate-secret.mjs +266 -0
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/surfaces.mjs +147 -0
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/validate-config.mjs +224 -0
- package/plugins/lisa-agy/skills/lisa-setup-automations/SKILL.md +69 -0
- package/plugins/lisa-agy/skills/lisa-setup-automations/scripts/generate-workflow.mjs +185 -0
- package/plugins/lisa-agy/skills/lisa-setup-remote-env/SKILL.md +150 -0
- package/plugins/lisa-agy/skills/lisa-setup-remote-env/assets/setup.sh +45 -0
- package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +227 -0
- package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/toolchain.mjs +169 -0
- package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +186 -0
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/commands/lisa/implement.md +5 -3
- package/plugins/lisa-copilot/commands/lisa/setup/remote-env.md +7 -0
- package/plugins/lisa-copilot/skills/lisa-atlassian-access/SKILL.md +17 -0
- package/plugins/lisa-copilot/skills/lisa-automation-status/SKILL.md +23 -0
- package/plugins/lisa-copilot/skills/lisa-doctor/SKILL.md +33 -0
- package/plugins/lisa-copilot/skills/lisa-implement/SKILL.md +17 -0
- package/plugins/lisa-copilot/skills/lisa-notion-access/SKILL.md +17 -0
- package/plugins/lisa-copilot/skills/lisa-remote-dispatch/SKILL.md +99 -0
- package/plugins/lisa-copilot/skills/lisa-remote-dispatch/scripts/dispatch.mjs +264 -0
- package/plugins/lisa-copilot/skills/lisa-secrets-access/SKILL.md +138 -52
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +217 -0
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/envfile.mjs +78 -0
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +105 -0
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/providers.mjs +240 -0
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/read-secret-note.mjs +82 -0
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/resolve-secret.mjs +151 -176
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/rotate-secret.mjs +266 -0
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/surfaces.mjs +147 -0
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/validate-config.mjs +224 -0
- package/plugins/lisa-copilot/skills/lisa-setup-automations/SKILL.md +69 -0
- package/plugins/lisa-copilot/skills/lisa-setup-automations/scripts/generate-workflow.mjs +185 -0
- package/plugins/lisa-copilot/skills/lisa-setup-remote-env/SKILL.md +150 -0
- package/plugins/lisa-copilot/skills/lisa-setup-remote-env/assets/setup.sh +45 -0
- package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +227 -0
- package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/toolchain.mjs +169 -0
- package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +186 -0
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/commands/lisa/implement.md +5 -3
- package/plugins/lisa-cursor/commands/lisa/setup/remote-env.md +7 -0
- package/plugins/lisa-cursor/skills/lisa-atlassian-access/SKILL.md +17 -0
- package/plugins/lisa-cursor/skills/lisa-automation-status/SKILL.md +23 -0
- package/plugins/lisa-cursor/skills/lisa-doctor/SKILL.md +33 -0
- package/plugins/lisa-cursor/skills/lisa-implement/SKILL.md +17 -0
- package/plugins/lisa-cursor/skills/lisa-notion-access/SKILL.md +17 -0
- package/plugins/lisa-cursor/skills/lisa-remote-dispatch/SKILL.md +99 -0
- package/plugins/lisa-cursor/skills/lisa-remote-dispatch/scripts/dispatch.mjs +264 -0
- package/plugins/lisa-cursor/skills/lisa-secrets-access/SKILL.md +138 -52
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +217 -0
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/envfile.mjs +78 -0
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +105 -0
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/providers.mjs +240 -0
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/read-secret-note.mjs +82 -0
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/resolve-secret.mjs +151 -176
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/rotate-secret.mjs +266 -0
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/surfaces.mjs +147 -0
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/validate-config.mjs +224 -0
- package/plugins/lisa-cursor/skills/lisa-setup-automations/SKILL.md +69 -0
- package/plugins/lisa-cursor/skills/lisa-setup-automations/scripts/generate-workflow.mjs +185 -0
- package/plugins/lisa-cursor/skills/lisa-setup-remote-env/SKILL.md +150 -0
- package/plugins/lisa-cursor/skills/lisa-setup-remote-env/assets/setup.sh +45 -0
- package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +227 -0
- package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/toolchain.mjs +169 -0
- package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +186 -0
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/commands/implement.md +5 -3
- package/plugins/src/base/commands/setup/remote-env.md +7 -0
- package/plugins/src/base/skills/lisa-atlassian-access/SKILL.md +17 -0
- package/plugins/src/base/skills/lisa-automation-status/SKILL.md +23 -0
- package/plugins/src/base/skills/lisa-doctor/SKILL.md +33 -0
- package/plugins/src/base/skills/lisa-implement/SKILL.md +17 -0
- package/plugins/src/base/skills/lisa-notion-access/SKILL.md +17 -0
- package/plugins/src/base/skills/lisa-remote-dispatch/SKILL.md +99 -0
- package/plugins/src/base/skills/lisa-remote-dispatch/scripts/dispatch.mjs +264 -0
- package/plugins/src/base/skills/lisa-secrets-access/SKILL.md +138 -52
- package/plugins/src/base/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +217 -0
- package/plugins/src/base/skills/lisa-secrets-access/scripts/envfile.mjs +78 -0
- package/plugins/src/base/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +105 -0
- package/plugins/src/base/skills/lisa-secrets-access/scripts/providers.mjs +240 -0
- package/plugins/src/base/skills/lisa-secrets-access/scripts/read-secret-note.mjs +82 -0
- package/plugins/src/base/skills/lisa-secrets-access/scripts/resolve-secret.mjs +151 -176
- package/plugins/src/base/skills/lisa-secrets-access/scripts/rotate-secret.mjs +266 -0
- package/plugins/src/base/skills/lisa-secrets-access/scripts/surfaces.mjs +147 -0
- package/plugins/src/base/skills/lisa-secrets-access/scripts/validate-config.mjs +224 -0
- package/plugins/src/base/skills/lisa-setup-automations/SKILL.md +69 -0
- package/plugins/src/base/skills/lisa-setup-automations/scripts/generate-workflow.mjs +185 -0
- package/plugins/src/base/skills/lisa-setup-remote-env/SKILL.md +150 -0
- package/plugins/src/base/skills/lisa-setup-remote-env/assets/setup.sh +45 -0
- package/plugins/src/base/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +227 -0
- package/plugins/src/base/skills/lisa-setup-remote-env/scripts/toolchain.mjs +169 -0
- package/plugins/src/base/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +186 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: lisa-setup-remote-env
|
|
3
|
+
description: "Provision and verify a remote execution environment for a host project — Codex Cloud today, other remote surfaces as they are added. Generates a repository-owned setup script that installs the declared toolchain, materializes secrets through lisa-secrets-access, and runs the project's own hook. Provisions by API where one exists, by driving the vendor console where one does not, and by emitting exact config otherwise — then proves the result with the same read-back regardless of which tier did the work. Use before dispatching any work with executionEnv."
|
|
4
|
+
allowed-tools: ["Bash", "Read", "Write", "Edit", "Skill"]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Setup Remote Environment: $ARGUMENTS
|
|
8
|
+
|
|
9
|
+
Prepare a remote surface so a host project can execute there. Today that means **Codex Cloud**; the shape is deliberately surface-agnostic so Claude Code web, Cursor remote, and others slot in without redesign.
|
|
10
|
+
|
|
11
|
+
## What lives where
|
|
12
|
+
|
|
13
|
+
The remote environment's own configuration fields stay **two one-line calls into the repository**. Nothing is pasted into a vendor UI.
|
|
14
|
+
|
|
15
|
+
```text
|
|
16
|
+
setup: bash scripts/lisa-remote-env/setup.sh
|
|
17
|
+
maintenance: bash scripts/lisa-remote-env/setup.sh
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
They are the same script. A container may be built fresh or resumed from cache; every step is idempotent and version-aware, so running it twice is correct, and running it on resume is what picks up a rotated value, an edited note, or a changed version pin.
|
|
21
|
+
|
|
22
|
+
The complete logic is repository-owned so it is reviewed, versioned, tested, and reusable. A large inline installer in a settings field is none of those things.
|
|
23
|
+
|
|
24
|
+
## The three phases
|
|
25
|
+
|
|
26
|
+
1. **Toolchain** — assert what must already be present, install what is not.
|
|
27
|
+
2. **Secrets** — materialize through `lisa-secrets-access`. This skill never reimplements any part of that contract; the single chokepoint is what makes the one-store rule enforceable.
|
|
28
|
+
3. **Project hook** — an optional `setup.local.sh` the project owns.
|
|
29
|
+
|
|
30
|
+
Order matters. The toolchain comes first because the secrets step needs the provider CLI it installs. The hook comes last because it is the only part that may assume everything else is ready.
|
|
31
|
+
|
|
32
|
+
## Toolchain manifest — two entry kinds
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"remoteEnv": {
|
|
37
|
+
"tools": {
|
|
38
|
+
"require": [
|
|
39
|
+
{ "name": "python3" },
|
|
40
|
+
{ "name": "node", "minVersion": "20" },
|
|
41
|
+
{ "name": "curl" },
|
|
42
|
+
{ "name": "unzip" }
|
|
43
|
+
],
|
|
44
|
+
"install": [
|
|
45
|
+
{
|
|
46
|
+
"name": "bws",
|
|
47
|
+
"version": "2.1.0",
|
|
48
|
+
"install": "release-zip",
|
|
49
|
+
"url": "https://<vendor>/releases/download/bws-v2.1.0/bws-<platform>-2.1.0.zip",
|
|
50
|
+
"sha256": "<sha256 published with that exact release>"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"name": "codex",
|
|
54
|
+
"version": "0.144.6",
|
|
55
|
+
"install": "npm-global",
|
|
56
|
+
"package": "@openai/codex"
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
},
|
|
60
|
+
"hook": "scripts/lisa-remote-env/setup.local.sh",
|
|
61
|
+
"surfaces": {
|
|
62
|
+
"codex-cloud": {
|
|
63
|
+
"environmentId": "<id>",
|
|
64
|
+
"repository": "<org>/<repo>",
|
|
65
|
+
"branch": "main"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**`require`** — assert present, optionally at a minimum version. For everything the base image already provides.
|
|
73
|
+
|
|
74
|
+
**`install`** — provision it, pinned and checksummed. Only for what genuinely is not there.
|
|
75
|
+
|
|
76
|
+
Expected shape for most projects: a short `require` list, an empty or near-empty `install` list, and the provider CLI as the only thing always provisioned.
|
|
77
|
+
|
|
78
|
+
### Rules
|
|
79
|
+
|
|
80
|
+
- **The base image is not a contract.** A vendor can change it. A project quietly depending on a preinstalled `jq` should break loudly at setup when that happens, not mysteriously mid-task weeks later. `require` is what converts an implicit assumption into an explicit check.
|
|
81
|
+
- **Detect first, install second.** Check the installed version, skip when it matches the pin, reinstall only when the pin changed. This is what makes setup and maintenance the same script, and it is the cheap path on cache resume.
|
|
82
|
+
- **Version-assert, not just presence.** A base-image bump that moves off a supported Node or Python must be caught. Presence alone is not enough when a project depends on a specific version.
|
|
83
|
+
- **Prefer what is there.** A second Node beside the image's creates PATH ambiguity and wastes container time.
|
|
84
|
+
- **Pin and checksum together, in one reviewed commit.** A version bump that does not move the checksum fails before installation, not after. An archive is verified *before* it is unpacked, so unexpected contents never reach a directory on PATH.
|
|
85
|
+
|
|
86
|
+
## Tooling is never derived from secret notes
|
|
87
|
+
|
|
88
|
+
Deliberately rejected:
|
|
89
|
+
|
|
90
|
+
- Most tooling has **no credential** — `jq`, ripgrep, browsers, a linter — so notes could only ever cover a subset, and a second mechanism would be needed anyway.
|
|
91
|
+
- Most credentials **imply no tool**: a webhook is curl'd, a REST key needs no CLI.
|
|
92
|
+
- The mapping is **guesswork** — which CLI, what version, pinned how — and is exactly the inference the secrets contract already forbids from authorising anything.
|
|
93
|
+
- It makes **prose load-bearing**: a note lives provider-side, is editable outside review, and is unfixable by a read-only account.
|
|
94
|
+
- It is a **supply-chain path** — provider write access would become arbitrary install access.
|
|
95
|
+
|
|
96
|
+
**Notes are an assertion target instead.** At verify time, a tool that declares a credential which is not materialized is an **error**; a credential materialized with no declared consumer is a **warning**. The arrow points the safe way: the repository declares intent, and the provider is checked against it.
|
|
97
|
+
|
|
98
|
+
## Provisioning tiers
|
|
99
|
+
|
|
100
|
+
Preference order, falling back:
|
|
101
|
+
|
|
102
|
+
1. **API** — provision programmatically.
|
|
103
|
+
2. **Console** — drive the vendor's web console.
|
|
104
|
+
3. **Emit** — generate exact config for a human to paste.
|
|
105
|
+
|
|
106
|
+
**For Codex Cloud today, start at tier 2 or 3.** Verified on 2026-08-01: `codex cloud` exposes only `exec | status | list | apply | diff`. There is no environment-provisioning subcommand, so there is nothing to call. Do not claim tier 1 for this surface until the CLI grows one.
|
|
107
|
+
|
|
108
|
+
When emitting, produce exactly:
|
|
109
|
+
|
|
110
|
+
```text
|
|
111
|
+
Environment name: <project> remote executor
|
|
112
|
+
Repository: <org>/<repo> (must be the default checkout)
|
|
113
|
+
Setup script: bash scripts/lisa-remote-env/setup.sh
|
|
114
|
+
Maintenance: bash scripts/lisa-remote-env/setup.sh
|
|
115
|
+
Environment vars: LISA_SECRETS_SURFACE=codex-cloud
|
|
116
|
+
BWS_ACCESS_TOKEN=<from the provider; an environment
|
|
117
|
+
variable, not a task secret — setup and cache-resume
|
|
118
|
+
maintenance both run before task secrets exist>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Verification is tier-independent
|
|
122
|
+
|
|
123
|
+
**Whatever tier provisioned it, the same read-back proves it.** Trust comes from the verify, not the mechanism — which is what makes emit-tier as trustworthy as API-tier.
|
|
124
|
+
|
|
125
|
+
```sh
|
|
126
|
+
scripts/verify-remote-env.mjs [SECRETS_DIR]
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Asserts, without printing a value: each declared tool at its pinned or minimum version, the detected surface, the secrets directory at mode `0700`, both files at mode `0600`, and a clean checkout.
|
|
130
|
+
|
|
131
|
+
**Never verify against vendor UI state.** On 2026-08-01 a Codex environments table reported zero tasks for an environment that had demonstrably completed one, because the task records carried a null environment identifier and the `--env` filter was correspondingly unreliable. Reconcile through durable identifiers only.
|
|
132
|
+
|
|
133
|
+
## Preconditions
|
|
134
|
+
|
|
135
|
+
Checked when setup runs, not at 3am:
|
|
136
|
+
|
|
137
|
+
- the environment exists and is bound to **this** repository as its default checkout;
|
|
138
|
+
- the bootstrap credential resolves;
|
|
139
|
+
- Lisa has been applied to the repository, so its skills are present in that checkout.
|
|
140
|
+
|
|
141
|
+
Fail with a message naming what is missing. Never provision-and-hope.
|
|
142
|
+
|
|
143
|
+
## Bootstrap credential placement
|
|
144
|
+
|
|
145
|
+
On Codex Cloud the bootstrap must be an **environment variable, not a task secret**: setup runs on a new container and maintenance runs on cache resume, both before task secrets exist. The tradeoff is that the variable remains visible during the task, so compensate by keeping the machine account narrowly scoped and instructing the task never to inspect or use it.
|
|
146
|
+
|
|
147
|
+
## Related
|
|
148
|
+
|
|
149
|
+
- `lisa-secrets-access` — owns every part of the secrets contract this skill composes with.
|
|
150
|
+
- `lisa-remote-dispatch` — dispatches work to an environment this skill prepared.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Remote environment entrypoint. Installed by /lisa:setup:remote-env; the remote
|
|
4
|
+
# environment's setup AND maintenance fields both call this exact path.
|
|
5
|
+
#
|
|
6
|
+
# They are the same script on purpose. A container may be built fresh or resumed
|
|
7
|
+
# from cache, and everything below is idempotent and version-aware — so running
|
|
8
|
+
# it twice is correct, and running it on resume is what picks up a rotated
|
|
9
|
+
# value, an edited note, or a changed version pin.
|
|
10
|
+
#
|
|
11
|
+
# This file is deliberately thin. It resolves an interpreter and hands off; the
|
|
12
|
+
# reviewed, tested, versioned logic lives in the Lisa skill rather than here,
|
|
13
|
+
# and emphatically not in a vendor settings field.
|
|
14
|
+
set -euo pipefail
|
|
15
|
+
|
|
16
|
+
# Node is the one thing that cannot be installed by the installer, since the
|
|
17
|
+
# installer is written in it. Fail with an actionable message rather than a
|
|
18
|
+
# "command not found" forty lines deep.
|
|
19
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
20
|
+
echo "node is required to prepare this environment but is not present." >&2
|
|
21
|
+
echo "It cannot be installed by the toolchain step, because that step runs" >&2
|
|
22
|
+
echo "on node. Pin a base image that provides it." >&2
|
|
23
|
+
exit 1
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
# The skill ships under whichever agent directory this project uses. Search
|
|
27
|
+
# rather than hardcode, so one entrypoint serves every supported harness.
|
|
28
|
+
runner=""
|
|
29
|
+
for candidate in \
|
|
30
|
+
".claude/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs" \
|
|
31
|
+
".agents/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs" \
|
|
32
|
+
".codex/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs"; do
|
|
33
|
+
if [ -f "$candidate" ]; then
|
|
34
|
+
runner="$candidate"
|
|
35
|
+
break
|
|
36
|
+
fi
|
|
37
|
+
done
|
|
38
|
+
|
|
39
|
+
if [ -z "$runner" ]; then
|
|
40
|
+
echo "Cannot find the lisa-setup-remote-env skill in this checkout." >&2
|
|
41
|
+
echo "Run 'lisa apply' so the skills are present, then re-run setup." >&2
|
|
42
|
+
exit 1
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
exec node "$runner" "$@"
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Prepare a remote environment: toolchain, then secrets, then the project hook.
|
|
4
|
+
*
|
|
5
|
+
* This is the script a remote environment's setup **and** maintenance fields
|
|
6
|
+
* call. They are the same script on purpose. A container may be built fresh or
|
|
7
|
+
* resumed from cache, and every step here is idempotent and version-aware, so
|
|
8
|
+
* running it twice is correct and running it on resume is what picks up a
|
|
9
|
+
* rotated value, an edited note, or a changed version pin.
|
|
10
|
+
*
|
|
11
|
+
* Order matters. The toolchain comes first because the secrets step needs the
|
|
12
|
+
* provider CLI that the toolchain installs. The project hook comes last because
|
|
13
|
+
* it is the only part that may assume everything else is ready.
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* setup-remote-env.mjs [--dry-run]
|
|
17
|
+
* @module setup-remote-env
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { execFileSync } from "node:child_process";
|
|
21
|
+
import {
|
|
22
|
+
chmodSync,
|
|
23
|
+
existsSync,
|
|
24
|
+
mkdirSync,
|
|
25
|
+
readFileSync,
|
|
26
|
+
rmSync,
|
|
27
|
+
} from "node:fs";
|
|
28
|
+
import { dirname, join, resolve } from "node:path";
|
|
29
|
+
import { fileURLToPath } from "node:url";
|
|
30
|
+
|
|
31
|
+
import { assertPinned, extractVersion, planToolchain } from "./toolchain.mjs";
|
|
32
|
+
|
|
33
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Locate a sibling skill's script.
|
|
37
|
+
*
|
|
38
|
+
* Secrets belong to `lisa-secrets-access`, and this script must not reimplement
|
|
39
|
+
* any part of that contract — the single chokepoint is what makes the one-store
|
|
40
|
+
* rule enforceable. Resolving relative to this file works in every install
|
|
41
|
+
* layout, because skills are always siblings under one `skills/` directory.
|
|
42
|
+
* @param {string} skill Sibling skill slug.
|
|
43
|
+
* @param {string} script Script filename.
|
|
44
|
+
* @returns {string} Absolute path.
|
|
45
|
+
*/
|
|
46
|
+
function siblingScript(skill, script) {
|
|
47
|
+
const path = resolve(HERE, "..", "..", skill, "scripts", script);
|
|
48
|
+
if (!existsSync(path)) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`cannot find ${skill}/scripts/${script} beside this skill.\n` +
|
|
51
|
+
`Remote setup composes with that skill rather than reimplementing it.`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return path;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Read the `remoteEnv` block from `.lisa.config.json`.
|
|
59
|
+
* @param {string} [cwd] Directory to look in.
|
|
60
|
+
* @returns {{tools: object, hook: string|null, surfaces: object}} Configuration.
|
|
61
|
+
*/
|
|
62
|
+
export function readRemoteEnvConfig(cwd = process.cwd()) {
|
|
63
|
+
const path = join(cwd, ".lisa.config.json");
|
|
64
|
+
const defaults = {
|
|
65
|
+
tools: { require: [], install: [] },
|
|
66
|
+
hook: null,
|
|
67
|
+
surfaces: {},
|
|
68
|
+
};
|
|
69
|
+
if (!existsSync(path)) return defaults;
|
|
70
|
+
const cfg = JSON.parse(readFileSync(path, "utf8")).remoteEnv;
|
|
71
|
+
if (!cfg) return defaults;
|
|
72
|
+
return {
|
|
73
|
+
tools: { require: [], install: [], ...(cfg.tools ?? {}) },
|
|
74
|
+
hook: cfg.hook ?? null,
|
|
75
|
+
surfaces: cfg.surfaces ?? {},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Probe what a tool reports for its version, treating absence as not present.
|
|
81
|
+
* @param {string} name Executable name.
|
|
82
|
+
* @returns {{version: string|null, present: boolean}} Probe result.
|
|
83
|
+
*/
|
|
84
|
+
function probe(name) {
|
|
85
|
+
try {
|
|
86
|
+
const out = execFileSync(name, ["--version"], {
|
|
87
|
+
encoding: "utf8",
|
|
88
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
89
|
+
});
|
|
90
|
+
return { present: true, version: extractVersion(out) };
|
|
91
|
+
} catch {
|
|
92
|
+
return { present: false, version: null };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Install a pinned archive, refusing anything whose checksum does not match.
|
|
98
|
+
* @param {object} tool Manifest entry.
|
|
99
|
+
* @param {string} binDir Directory to install into.
|
|
100
|
+
*/
|
|
101
|
+
function installReleaseZip(tool, binDir) {
|
|
102
|
+
const temporary = join(binDir, `.${tool.name}-download`);
|
|
103
|
+
mkdirSync(temporary, { recursive: true });
|
|
104
|
+
try {
|
|
105
|
+
const archive = join(temporary, "download.zip");
|
|
106
|
+
execFileSync("curl", ["-fsSL", tool.url, "-o", archive], {
|
|
107
|
+
stdio: "inherit",
|
|
108
|
+
});
|
|
109
|
+
// Verify before unpacking, not after. An unexpected archive must fail
|
|
110
|
+
// before any of its contents reach a directory that is on PATH.
|
|
111
|
+
execFileSync("sha256sum", ["-c", "-"], {
|
|
112
|
+
input: `${tool.sha256} ${archive}\n`,
|
|
113
|
+
stdio: ["pipe", "ignore", "inherit"],
|
|
114
|
+
});
|
|
115
|
+
execFileSync("unzip", ["-q", "-o", archive, "-d", temporary], {
|
|
116
|
+
stdio: "inherit",
|
|
117
|
+
});
|
|
118
|
+
const binary = join(temporary, tool.binary ?? tool.name);
|
|
119
|
+
execFileSync("install", ["-m", "0755", binary, join(binDir, tool.name)], {
|
|
120
|
+
stdio: "inherit",
|
|
121
|
+
});
|
|
122
|
+
} finally {
|
|
123
|
+
rmSync(temporary, { recursive: true, force: true });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Install a pinned global npm package.
|
|
129
|
+
* @param {object} tool Manifest entry.
|
|
130
|
+
*/
|
|
131
|
+
function installNpmGlobal(tool) {
|
|
132
|
+
execFileSync(
|
|
133
|
+
"npm",
|
|
134
|
+
["install", "--global", `${tool.package}@${tool.version}`],
|
|
135
|
+
{
|
|
136
|
+
stdio: "inherit",
|
|
137
|
+
}
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Execute one install decision.
|
|
143
|
+
* @param {object} tool Manifest entry.
|
|
144
|
+
* @param {string} binDir Directory for downloaded binaries.
|
|
145
|
+
*/
|
|
146
|
+
function installTool(tool, binDir) {
|
|
147
|
+
assertPinned(tool);
|
|
148
|
+
if (tool.install === "release-zip") installReleaseZip(tool, binDir);
|
|
149
|
+
else installNpmGlobal(tool);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Apply the toolchain plan, reporting every decision.
|
|
154
|
+
* @param {object} tools Manifest.
|
|
155
|
+
* @param {boolean} dryRun Whether to plan only.
|
|
156
|
+
* @returns {Array<object>} The plan that was applied.
|
|
157
|
+
*/
|
|
158
|
+
function applyToolchain(tools, dryRun) {
|
|
159
|
+
const plan = planToolchain(tools, probe);
|
|
160
|
+
const blocked = plan.filter(
|
|
161
|
+
p => p.action === "missing" || p.action === "invalid"
|
|
162
|
+
);
|
|
163
|
+
if (blocked.length) {
|
|
164
|
+
throw new Error(blocked.map(p => p.reason).join("\n\n"));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const binDir = join(process.env.HOME ?? "", ".local", "bin");
|
|
168
|
+
mkdirSync(binDir, { recursive: true, mode: 0o755 });
|
|
169
|
+
const byName = new Map((tools.install ?? []).map(t => [t.name, t]));
|
|
170
|
+
|
|
171
|
+
for (const step of plan) {
|
|
172
|
+
console.log(` ${step.action.padEnd(8)} ${step.reason}`);
|
|
173
|
+
if (step.action === "install" && !dryRun)
|
|
174
|
+
installTool(byName.get(step.name), binDir);
|
|
175
|
+
}
|
|
176
|
+
return plan;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Run the project's own setup hook when it has one.
|
|
181
|
+
*
|
|
182
|
+
* A declarative manifest will never cover everything, and without an escape
|
|
183
|
+
* hatch a project's only option is to fork Lisa's script — at which point it
|
|
184
|
+
* stops receiving any of the fixes.
|
|
185
|
+
* @param {string|null} hook Repository-relative path.
|
|
186
|
+
* @param {boolean} dryRun Whether to plan only.
|
|
187
|
+
*/
|
|
188
|
+
function runHook(hook, dryRun) {
|
|
189
|
+
if (!hook) return;
|
|
190
|
+
const path = resolve(process.cwd(), hook);
|
|
191
|
+
if (!existsSync(path)) {
|
|
192
|
+
throw new Error(`remoteEnv.hook points at ${hook}, which does not exist`);
|
|
193
|
+
}
|
|
194
|
+
console.log(`\nProject hook: ${hook}`);
|
|
195
|
+
if (dryRun) return;
|
|
196
|
+
chmodSync(path, 0o755);
|
|
197
|
+
execFileSync("bash", [path], { stdio: "inherit" });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function main() {
|
|
201
|
+
const dryRun = process.argv.includes("--dry-run");
|
|
202
|
+
const cfg = readRemoteEnvConfig();
|
|
203
|
+
|
|
204
|
+
console.log("Toolchain:");
|
|
205
|
+
applyToolchain(cfg.tools, dryRun);
|
|
206
|
+
|
|
207
|
+
console.log("\nSecrets:");
|
|
208
|
+
const materialize = siblingScript(
|
|
209
|
+
"lisa-secrets-access",
|
|
210
|
+
"materialize-secrets.mjs"
|
|
211
|
+
);
|
|
212
|
+
execFileSync("node", dryRun ? [materialize, "--dry-run"] : [materialize], {
|
|
213
|
+
stdio: "inherit",
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
runHook(cfg.hook, dryRun);
|
|
217
|
+
console.log(`\nRemote environment ${dryRun ? "plan complete" : "ready"}.`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
221
|
+
try {
|
|
222
|
+
main();
|
|
223
|
+
} catch (err) {
|
|
224
|
+
console.error(err.message);
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Decide what a remote environment already has and what it still needs.
|
|
4
|
+
*
|
|
5
|
+
* Remote environments arrive with a base image that already provides much of
|
|
6
|
+
* what a project needs, so the manifest has two entry kinds rather than one:
|
|
7
|
+
*
|
|
8
|
+
* require — assert present, fail setup with a clear message if missing
|
|
9
|
+
* install — provision it, pinned and checksummed
|
|
10
|
+
*
|
|
11
|
+
* The distinction is not bookkeeping. A base image is **not a contract**: the
|
|
12
|
+
* vendor can change it. A project quietly depending on a preinstalled tool
|
|
13
|
+
* should break loudly at setup when that happens, rather than mysteriously
|
|
14
|
+
* mid-task weeks later. `require` is what converts an implicit assumption into
|
|
15
|
+
* an explicit check.
|
|
16
|
+
*
|
|
17
|
+
* Planning is separated from execution so the whole decision table is testable
|
|
18
|
+
* without a container, a network, or a real binary anywhere.
|
|
19
|
+
* @module toolchain
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Compare dotted version strings numerically.
|
|
24
|
+
*
|
|
25
|
+
* String comparison gets this wrong in the case that matters: "10" sorts before
|
|
26
|
+
* "9", so a base-image bump to a newer major would read as a downgrade and pass
|
|
27
|
+
* a minimum-version check it should fail.
|
|
28
|
+
* @param {string} a Left version.
|
|
29
|
+
* @param {string} b Right version.
|
|
30
|
+
* @returns {number} Negative, zero, or positive.
|
|
31
|
+
*/
|
|
32
|
+
export function compareVersions(a, b) {
|
|
33
|
+
const left = String(a).split(".").map(Number);
|
|
34
|
+
const right = String(b).split(".").map(Number);
|
|
35
|
+
for (let i = 0; i < Math.max(left.length, right.length); i += 1) {
|
|
36
|
+
const diff = (left[i] || 0) - (right[i] || 0);
|
|
37
|
+
if (diff) return diff;
|
|
38
|
+
}
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Pull a dotted version out of whatever a tool prints for `--version`.
|
|
44
|
+
*
|
|
45
|
+
* Tools are wildly inconsistent here ("bws 2.1.0", "v20.11.0", "Python 3.12.1"),
|
|
46
|
+
* and normalising with a regex is more honest than maintaining a per-tool parser
|
|
47
|
+
* table that silently rots.
|
|
48
|
+
* @param {string} output Raw `--version` output.
|
|
49
|
+
* @returns {string|null} The version, or null when none is recognisable.
|
|
50
|
+
*/
|
|
51
|
+
export function extractVersion(output) {
|
|
52
|
+
const match = /(\d+(?:\.\d+)*)/.exec(String(output ?? ""));
|
|
53
|
+
return match ? match[1] : null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Classify one required tool against what the environment actually provides.
|
|
58
|
+
* @param {object} tool Manifest entry.
|
|
59
|
+
* @param {{version: string|null, present: boolean}} found Probe result.
|
|
60
|
+
* @returns {{name: string, action: string, reason: string}} The decision.
|
|
61
|
+
*/
|
|
62
|
+
function planRequired(tool, found) {
|
|
63
|
+
if (!found.present) {
|
|
64
|
+
return {
|
|
65
|
+
name: tool.name,
|
|
66
|
+
action: "missing",
|
|
67
|
+
reason:
|
|
68
|
+
`${tool.name} is required but not present. The base image is not a ` +
|
|
69
|
+
`contract — if it used to provide this, the vendor has changed it. ` +
|
|
70
|
+
`Either add an install entry or pin a different image.`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (
|
|
74
|
+
tool.minVersion &&
|
|
75
|
+
compareVersions(found.version ?? "0", tool.minVersion) < 0
|
|
76
|
+
) {
|
|
77
|
+
return {
|
|
78
|
+
name: tool.name,
|
|
79
|
+
action: "missing",
|
|
80
|
+
reason:
|
|
81
|
+
`${tool.name} ${found.version ?? "(unknown)"} is older than the ` +
|
|
82
|
+
`required ${tool.minVersion}. Presence alone is not enough when a ` +
|
|
83
|
+
`project depends on a specific version.`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
name: tool.name,
|
|
88
|
+
action: "present",
|
|
89
|
+
reason: `${tool.name} ${found.version ?? ""}`.trim(),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Classify one installable tool against what is already on disk.
|
|
95
|
+
*
|
|
96
|
+
* Detect first, install second. Skipping a matching version is what makes setup
|
|
97
|
+
* and maintenance the same script, and it is the cheap path when a container
|
|
98
|
+
* resumes from cache rather than being built fresh.
|
|
99
|
+
* @param {object} tool Manifest entry.
|
|
100
|
+
* @param {{version: string|null, present: boolean}} found Probe result.
|
|
101
|
+
* @returns {{name: string, action: string, reason: string}} The decision.
|
|
102
|
+
*/
|
|
103
|
+
function planInstallable(tool, found) {
|
|
104
|
+
if (!tool.version) {
|
|
105
|
+
return {
|
|
106
|
+
name: tool.name,
|
|
107
|
+
action: "invalid",
|
|
108
|
+
reason: `${tool.name} has no pinned version. An unpinned install is not reproducible.`,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
if (found.present && found.version === tool.version) {
|
|
112
|
+
return {
|
|
113
|
+
name: tool.name,
|
|
114
|
+
action: "skip",
|
|
115
|
+
reason: `${tool.name} ${tool.version} already installed`,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
name: tool.name,
|
|
120
|
+
action: "install",
|
|
121
|
+
reason: found.present
|
|
122
|
+
? `${tool.name} ${found.version ?? "(unknown)"} does not match pin ${tool.version}`
|
|
123
|
+
: `${tool.name} ${tool.version} not installed`,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Produce the complete plan for a toolchain manifest.
|
|
129
|
+
* @param {{require?: object[], install?: object[]}} tools Manifest.
|
|
130
|
+
* @param {(name: string) => {version: string|null, present: boolean}} probe Version probe.
|
|
131
|
+
* @returns {Array<{name: string, action: string, reason: string}>} Ordered decisions.
|
|
132
|
+
*/
|
|
133
|
+
export function planToolchain(tools, probe) {
|
|
134
|
+
const plan = [];
|
|
135
|
+
for (const tool of tools.require ?? [])
|
|
136
|
+
plan.push(planRequired(tool, probe(tool.name)));
|
|
137
|
+
for (const tool of tools.install ?? [])
|
|
138
|
+
plan.push(planInstallable(tool, probe(tool.name)));
|
|
139
|
+
return plan;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Reject a manifest entry that could install something unverifiable.
|
|
144
|
+
*
|
|
145
|
+
* A pinned version with no checksum still trusts whatever the URL serves today.
|
|
146
|
+
* Requiring both, changed together in one reviewed commit, is what makes an
|
|
147
|
+
* unexpected archive fail before installation rather than after.
|
|
148
|
+
* @param {object} tool Manifest entry.
|
|
149
|
+
*/
|
|
150
|
+
export function assertPinned(tool) {
|
|
151
|
+
if (tool.install === "release-zip") {
|
|
152
|
+
if (!tool.url || !tool.sha256) {
|
|
153
|
+
throw new Error(
|
|
154
|
+
`${tool.name}: a release-zip install needs both url and sha256.\n` +
|
|
155
|
+
`A version bump must move the checksum in the same reviewed commit.`
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (tool.install === "npm-global") {
|
|
161
|
+
if (!tool.package)
|
|
162
|
+
throw new Error(`${tool.name}: npm-global install needs a package`);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
throw new Error(
|
|
166
|
+
`${tool.name}: unknown install method "${tool.install}".\n` +
|
|
167
|
+
`Supported: release-zip, npm-global.`
|
|
168
|
+
);
|
|
169
|
+
}
|