@spunto/build 0.3.0 → 0.4.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/README.md +51 -1
- package/package.json +27 -5
- package/src/agent-stream/agent-stream.ts +561 -0
- package/src/agent-stream/index.ts +42 -0
- package/src/agent-stream/model-window.ts +189 -0
- package/src/catalogs/index.ts +216 -0
- package/src/index.ts +4 -0
- package/src/script/index.ts +30 -0
- package/src/script/local-features.ts +39 -0
- package/src/script/setup-script.ts +1932 -0
- package/src/types/index.ts +68 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How big a session's context window is.
|
|
3
|
+
*
|
|
4
|
+
* Split out of `agent-stream.ts` because this is not a fact about a dialect. Which harness wrote
|
|
5
|
+
* the numbers is that file's problem; "how full is this window" outlives the harness, and it has
|
|
6
|
+
* two answers of very different quality:
|
|
7
|
+
*
|
|
8
|
+
* 1. **The catalogue below** — a table, and therefore already wrong on the day a model ships.
|
|
9
|
+
* It costs nothing: no credential, no network, no latency. It is the floor, not the answer.
|
|
10
|
+
* 2. **The Models API** — `GET /v1/models/{id}` → `max_input_tokens`, the model's own answer
|
|
11
|
+
* about itself. Used whenever the task's secrets carry an Anthropic credential of either
|
|
12
|
+
* kind (`ANTHROPIC_API_KEY`, or a subscription token — see `ModelCredential`), which is
|
|
13
|
+
* exactly where a session's credentials already live, and memoised per process because a
|
|
14
|
+
* model's window does not move.
|
|
15
|
+
*
|
|
16
|
+
* Neither of them guesses. A model in neither source is worth `null`, and the panel then shows a
|
|
17
|
+
* token count with no percentage — the rule this file inherits from the meter it feeds: a
|
|
18
|
+
* percentage against a window we invented is worse than no percentage, because the token count
|
|
19
|
+
* is still true and a wrong bar is not.
|
|
20
|
+
*
|
|
21
|
+
* **Why the catalogue is not a pattern match.** It used to be one — `/claude|opus|sonnet|haiku/`
|
|
22
|
+
* → 200K, with the `[1m]` suffix as the only way to reach 1M. That reads as a safe default and
|
|
23
|
+
* is not one: every current Claude model except Haiku 4.5 carries a 1M window, so the regex
|
|
24
|
+
* answered 200K for a session with five times that room, and the meter pegged at 100% somewhere
|
|
25
|
+
* around a fifth of the way in. Worse, the escape hatch never fired: `[1m]` is how Claude Code
|
|
26
|
+
* spells the *opt-in* to a long window, and a model whose 1M is native (Opus 5 and the whole
|
|
27
|
+
* 4.6-and-later line — 1M is both its default and its maximum) is spelled plainly. Measured from
|
|
28
|
+
* a real `system/init` line: `"model":"claude-opus-5"`, no suffix, 1M of room, 200K on the bar.
|
|
29
|
+
*
|
|
30
|
+
* So: exact ids, `null` for anything else, and a live lookup for the cases a table cannot cover.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/** A Claude Code session opened on the long-context *variant* of its model — `claude-opus-5[1m]`. */
|
|
34
|
+
const LONG_CONTEXT_SUFFIX = "[1m]"
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Windows by exact API id, in tokens.
|
|
38
|
+
*
|
|
39
|
+
* Exact, because a family prefix is precisely how the previous guess went wrong. A model missing
|
|
40
|
+
* from here is not assumed into a neighbour's window — it gets `null` and the Models API is asked
|
|
41
|
+
* instead. That is the property worth keeping as models keep shipping: the next generation reads
|
|
42
|
+
* as unknown, never as 200K.
|
|
43
|
+
*/
|
|
44
|
+
const WINDOWS: Record<string, number> = {
|
|
45
|
+
// 1M native — no opt-in, no suffix, nothing to detect in the stream.
|
|
46
|
+
"claude-fable-5-1": 1_000_000,
|
|
47
|
+
"claude-fable-5": 1_000_000,
|
|
48
|
+
"claude-mythos-5-1": 1_000_000,
|
|
49
|
+
"claude-mythos-5": 1_000_000,
|
|
50
|
+
"claude-mythos-preview": 1_000_000,
|
|
51
|
+
"claude-opus-5": 1_000_000,
|
|
52
|
+
"claude-opus-4-8": 1_000_000,
|
|
53
|
+
"claude-opus-4-7": 1_000_000,
|
|
54
|
+
"claude-opus-4-6": 1_000_000,
|
|
55
|
+
"claude-sonnet-5": 1_000_000,
|
|
56
|
+
"claude-sonnet-4-6": 1_000_000,
|
|
57
|
+
// 200K — the generation the old default was written against, plus Haiku, which stayed there.
|
|
58
|
+
"claude-haiku-4-5": 200_000,
|
|
59
|
+
"claude-opus-4-5": 200_000,
|
|
60
|
+
"claude-opus-4-1": 200_000,
|
|
61
|
+
"claude-opus-4-0": 200_000,
|
|
62
|
+
"claude-opus-4": 200_000,
|
|
63
|
+
"claude-sonnet-4-5": 200_000,
|
|
64
|
+
"claude-sonnet-4-0": 200_000,
|
|
65
|
+
"claude-sonnet-4": 200_000,
|
|
66
|
+
"claude-3-7-sonnet": 200_000,
|
|
67
|
+
"claude-3-5-sonnet": 200_000,
|
|
68
|
+
"claude-3-5-haiku": 200_000,
|
|
69
|
+
"claude-3-haiku": 200_000,
|
|
70
|
+
"claude-3-opus": 200_000,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The API id inside whatever a harness wrote, so one catalogue covers every spelling of a model.
|
|
75
|
+
*
|
|
76
|
+
* Claude Code adds `[1m]`, Bedrock prefixes `anthropic.` (sometimes behind a region), Vertex
|
|
77
|
+
* separates the snapshot with `@`, and a pinned id carries a trailing date. None of that changes
|
|
78
|
+
* which model ran.
|
|
79
|
+
*/
|
|
80
|
+
export function apiModelId(model: string): string {
|
|
81
|
+
return model
|
|
82
|
+
.trim()
|
|
83
|
+
.toLowerCase()
|
|
84
|
+
.replaceAll(LONG_CONTEXT_SUFFIX, "")
|
|
85
|
+
.replace(/^(?:[a-z]{2}\.)?anthropic\./, "")
|
|
86
|
+
.replace(/@.*$/, "")
|
|
87
|
+
.replace(/-v\d+:\d+$/, "")
|
|
88
|
+
.replace(/-\d{8}$/, "")
|
|
89
|
+
.trim()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The window a model string implies, from the catalogue alone — no credential, no network.
|
|
94
|
+
*
|
|
95
|
+
* `null` for anything unrecognised, which now includes any model newer than this table: see the
|
|
96
|
+
* header. Kept as the synchronous fallback for `summarizeUsage`, which reduces events that were
|
|
97
|
+
* stored before any of this existed.
|
|
98
|
+
*/
|
|
99
|
+
export function contextWindowOf(model: string | null | undefined): number | null {
|
|
100
|
+
if (typeof model !== "string" || !model) return null
|
|
101
|
+
if (model.includes(LONG_CONTEXT_SUFFIX)) return 1_000_000
|
|
102
|
+
return WINDOWS[apiModelId(model)] ?? null
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const MODELS_ENDPOINT = "https://api.anthropic.com/v1/models"
|
|
106
|
+
/** A read path polled every two seconds waits for nothing; the catalogue answers if this does not. */
|
|
107
|
+
const LOOKUP_TIMEOUT_MS = 4_000
|
|
108
|
+
/** A window is immutable, so a hit is kept for the life of the process. A miss is retried later. */
|
|
109
|
+
const MISS_TTL_MS = 10 * 60_000
|
|
110
|
+
|
|
111
|
+
const memo = new Map<string, { window: number | null; at: number }>()
|
|
112
|
+
|
|
113
|
+
/** Only for tests — the memo is process-wide on purpose. */
|
|
114
|
+
export function resetModelWindowCache(): void {
|
|
115
|
+
memo.clear()
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* How a task's session authenticates against Anthropic, because the two kinds do not use the
|
|
120
|
+
* same header and sending both at once is rejected outright.
|
|
121
|
+
*
|
|
122
|
+
* - `api-key` — `ANTHROPIC_API_KEY`, on `x-api-key`.
|
|
123
|
+
* - `oauth` — a subscription credential (`CLAUDE_CODE_OAUTH_TOKEN`, `ANTHROPIC_AUTH_TOKEN`), on
|
|
124
|
+
* `Authorization: Bearer` **plus** the `oauth-2025-04-20` beta header. Converting one to the
|
|
125
|
+
* other is a header change, not a value swap, which is exactly why this is a tagged union and
|
|
126
|
+
* not a string: a token sent as `x-api-key` fails, quietly, as a 401 that looks like a
|
|
127
|
+
* revoked key.
|
|
128
|
+
*/
|
|
129
|
+
export type ModelCredential = { kind: "api-key" | "oauth"; value: string }
|
|
130
|
+
|
|
131
|
+
function authHeaders(credential: ModelCredential): Record<string, string> {
|
|
132
|
+
return credential.kind === "oauth"
|
|
133
|
+
? { authorization: `Bearer ${credential.value}`, "anthropic-beta": "oauth-2025-04-20" }
|
|
134
|
+
: { "x-api-key": credential.value }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Ask the Models API what the model's window is.
|
|
139
|
+
*
|
|
140
|
+
* `max_input_tokens` is the field; there is no `context_window` in the response. Every failure
|
|
141
|
+
* mode — no network, a key that was revoked, a model id the API does not serve, a token whose
|
|
142
|
+
* scopes do not reach this endpoint, a 429 — resolves to `null` rather than throwing: this is
|
|
143
|
+
* called while ingesting a session, and a session must not stop streaming because a catalogue
|
|
144
|
+
* lookup did not come back.
|
|
145
|
+
*/
|
|
146
|
+
export async function liveContextWindow(model: string, credential: ModelCredential): Promise<number | null> {
|
|
147
|
+
const id = apiModelId(model)
|
|
148
|
+
if (!id) return null
|
|
149
|
+
|
|
150
|
+
const hit = memo.get(id)
|
|
151
|
+
if (hit && (hit.window !== null || Date.now() - hit.at < MISS_TTL_MS)) return hit.window
|
|
152
|
+
|
|
153
|
+
let window: number | null = null
|
|
154
|
+
try {
|
|
155
|
+
const res = await fetch(`${MODELS_ENDPOINT}/${encodeURIComponent(id)}`, {
|
|
156
|
+
headers: { ...authHeaders(credential), "anthropic-version": "2023-06-01" },
|
|
157
|
+
signal: AbortSignal.timeout(LOOKUP_TIMEOUT_MS),
|
|
158
|
+
})
|
|
159
|
+
if (res.ok) {
|
|
160
|
+
const body = (await res.json()) as { max_input_tokens?: unknown }
|
|
161
|
+
const max = Number(body?.max_input_tokens)
|
|
162
|
+
if (Number.isFinite(max) && max > 0) window = Math.trunc(max)
|
|
163
|
+
}
|
|
164
|
+
} catch {
|
|
165
|
+
// Left as null. The catalogue is the fallback and the caller already knows that.
|
|
166
|
+
}
|
|
167
|
+
memo.set(id, { window, at: Date.now() })
|
|
168
|
+
return window
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* The window to record for a session that opened on `model`, best source first.
|
|
173
|
+
*
|
|
174
|
+
* The `[1m]` suffix wins outright: it is a statement about *this session* (the harness opted it
|
|
175
|
+
* into a long window), where the catalogue and the API both report a property of the model. For
|
|
176
|
+
* everything else the API's `max_input_tokens` is preferred over the table, and the table is the
|
|
177
|
+
* fallback — including when the task carries no Anthropic credential at all, which is an ordinary
|
|
178
|
+
* case and not an error.
|
|
179
|
+
*/
|
|
180
|
+
export async function resolveContextWindow(
|
|
181
|
+
model: string | null | undefined,
|
|
182
|
+
credential: ModelCredential | null | undefined,
|
|
183
|
+
): Promise<number | null> {
|
|
184
|
+
if (typeof model !== "string" || !model) return null
|
|
185
|
+
if (model.includes(LONG_CONTEXT_SUFFIX)) return 1_000_000
|
|
186
|
+
const catalogued = contextWindowOf(model)
|
|
187
|
+
if (!credential?.value) return catalogued
|
|
188
|
+
return (await liveContextWindow(model, credential)) ?? catalogued
|
|
189
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// The curated devcontainer catalogs: what a project picker offers before anyone types anything.
|
|
2
|
+
//
|
|
3
|
+
// Data, not logic — but shared data, because the alternative is what happened: two hand-maintained
|
|
4
|
+
// copies that drifted. Spunto Lite's copy was missing `java`, `kubectl-helm-minikube` and
|
|
5
|
+
// `google-cloud-cli`, and described `common-utils` as installing Oh My Zsh at a time when the image
|
|
6
|
+
// recipe explicitly passes `installOhMyZsh: false`. Nobody decided either of those; they are the
|
|
7
|
+
// shape of a catalog nobody owns.
|
|
8
|
+
//
|
|
9
|
+
// **Project templates are deliberately not here.** Spunto Cloud's templates clone a starter
|
|
10
|
+
// repository from GitHub and carry onboarding-gallery presentation (logo, accent, category); Lite's
|
|
11
|
+
// only configure an environment. They answer different questions and diverge on purpose — the test
|
|
12
|
+
// from the README applies, and they fail it.
|
|
13
|
+
|
|
14
|
+
import type { ExtensionSuggestion } from "../extensions/extensions"
|
|
15
|
+
|
|
16
|
+
/** A base image of the catalog. */
|
|
17
|
+
export type DevImage = {
|
|
18
|
+
id: string
|
|
19
|
+
label: string
|
|
20
|
+
image: string
|
|
21
|
+
description: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** One configurable option of a feature, as the catalog advertises it. */
|
|
25
|
+
export type DevFeatureOption = {
|
|
26
|
+
name: string
|
|
27
|
+
default: string
|
|
28
|
+
description: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type DevFeature = {
|
|
32
|
+
id: string
|
|
33
|
+
label: string
|
|
34
|
+
/** OCI reference the feature is installed from. Mutually exclusive with `localScript`. */
|
|
35
|
+
ociRef?: string
|
|
36
|
+
/** Filename of an install script the platform ships itself. Takes precedence over `ociRef`. */
|
|
37
|
+
localScript?: string
|
|
38
|
+
description: string
|
|
39
|
+
/** Options a user may configure. */
|
|
40
|
+
options?: DevFeatureOption[]
|
|
41
|
+
/** Options always passed to the install script. A user's choice is merged over these. */
|
|
42
|
+
defaultOptions?: Record<string, string>
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const AVAILABLE_IMAGES: DevImage[] = [
|
|
46
|
+
{
|
|
47
|
+
id: "typescript-node-20",
|
|
48
|
+
label: "Node.js 20 (TypeScript)",
|
|
49
|
+
image: "mcr.microsoft.com/devcontainers/typescript-node:20",
|
|
50
|
+
description: "Node.js 20 + TypeScript, npm/yarn/pnpm",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: "javascript-node-20",
|
|
54
|
+
label: "Node.js 20 (JavaScript)",
|
|
55
|
+
image: "mcr.microsoft.com/devcontainers/javascript-node:20",
|
|
56
|
+
description: "Node.js 20, npm/yarn/pnpm",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: "python-312",
|
|
60
|
+
label: "Python 3.12",
|
|
61
|
+
image: "mcr.microsoft.com/devcontainers/python:3.12",
|
|
62
|
+
description: "Python 3.12, pip, conda",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: "go-121",
|
|
66
|
+
label: "Go 1.21",
|
|
67
|
+
image: "mcr.microsoft.com/devcontainers/go:1.21",
|
|
68
|
+
description: "Go 1.21, go tools",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: "rust",
|
|
72
|
+
label: "Rust",
|
|
73
|
+
image: "mcr.microsoft.com/devcontainers/rust:latest",
|
|
74
|
+
description: "Rust stable, cargo",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: "java-21",
|
|
78
|
+
label: "Java 21",
|
|
79
|
+
image: "mcr.microsoft.com/devcontainers/java:21",
|
|
80
|
+
description: "Java 21 (JDK), Maven, Gradle",
|
|
81
|
+
},
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Curated devcontainer features. Install scripts are fetched from their OCI ref at image-build
|
|
86
|
+
* time — see the image recipe for how, and for the two that are always installed whether or not
|
|
87
|
+
* they appear here.
|
|
88
|
+
*/
|
|
89
|
+
export const AVAILABLE_FEATURES: DevFeature[] = [
|
|
90
|
+
{
|
|
91
|
+
id: "docker-in-docker",
|
|
92
|
+
label: "Docker-in-Docker",
|
|
93
|
+
// Fork of ghcr.io/devcontainers/features/docker-in-docker (github.com/coderhammer/features).
|
|
94
|
+
// Upstream resolves "latest" for buildx/compose/compose-switch via git tags and optimistically
|
|
95
|
+
// downloads an asset named after that tag — breaks whenever a tag is pushed before its release
|
|
96
|
+
// asset is published (happened with docker/buildx v0.35.0 / v0.35.0-rc1, 404ing the whole
|
|
97
|
+
// feature install). Our fork verifies the release asset exists before committing to a version.
|
|
98
|
+
ociRef: "ghcr.io/coderhammer/features/docker-in-docker:3",
|
|
99
|
+
description: "Docker Engine inside the container (DinD)",
|
|
100
|
+
options: [
|
|
101
|
+
{ name: "version", default: "latest", description: "Docker/Moby Engine version" },
|
|
102
|
+
],
|
|
103
|
+
// moby-cli is unavailable on Debian trixie and newer — use official Docker instead
|
|
104
|
+
defaultOptions: { moby: "false" },
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: "claude-code",
|
|
108
|
+
label: "Claude Code",
|
|
109
|
+
ociRef: "ghcr.io/stu-bell/devcontainer-features/claude-code:0",
|
|
110
|
+
description: "Claude code inside the container"
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
id: "node",
|
|
114
|
+
label: "Node.js",
|
|
115
|
+
ociRef: "ghcr.io/devcontainers/features/node:1",
|
|
116
|
+
description: "Node.js, nvm, yarn, pnpm",
|
|
117
|
+
options: [
|
|
118
|
+
{ name: "version", default: "lts", description: "Node.js version (lts, 20, 22, etc.)" },
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
id: "python",
|
|
123
|
+
label: "Python",
|
|
124
|
+
ociRef: "ghcr.io/devcontainers/features/python:1",
|
|
125
|
+
description: "Python, pip, venv",
|
|
126
|
+
options: [
|
|
127
|
+
{ name: "version", default: "latest", description: "Python version" },
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: "go",
|
|
132
|
+
label: "Go",
|
|
133
|
+
ociRef: "ghcr.io/devcontainers/features/go:1",
|
|
134
|
+
description: "Go compiler and tools",
|
|
135
|
+
options: [
|
|
136
|
+
{ name: "version", default: "latest", description: "Go version" },
|
|
137
|
+
],
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
id: "rust",
|
|
141
|
+
label: "Rust",
|
|
142
|
+
ociRef: "ghcr.io/devcontainers/features/rust:1",
|
|
143
|
+
description: "Rust, cargo, rustup",
|
|
144
|
+
options: [
|
|
145
|
+
{ name: "version", default: "latest", description: "Rust version" },
|
|
146
|
+
],
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
id: "java",
|
|
150
|
+
label: "Java",
|
|
151
|
+
ociRef: "ghcr.io/devcontainers/features/java:1",
|
|
152
|
+
description: "JDK, Maven, Gradle",
|
|
153
|
+
options: [
|
|
154
|
+
{ name: "version", default: "latest", description: "Java version" },
|
|
155
|
+
],
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
id: "aws-cli",
|
|
159
|
+
label: "AWS CLI",
|
|
160
|
+
ociRef: "ghcr.io/devcontainers/features/aws-cli:1",
|
|
161
|
+
description: "AWS CLI v2",
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
id: "github-cli",
|
|
165
|
+
label: "GitHub CLI",
|
|
166
|
+
ociRef: "ghcr.io/devcontainers/features/github-cli:1",
|
|
167
|
+
description: "GitHub CLI (gh)",
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
id: "kubectl-helm-minikube",
|
|
171
|
+
label: "kubectl + Helm",
|
|
172
|
+
ociRef: "ghcr.io/devcontainers/features/kubectl-helm-minikube:1",
|
|
173
|
+
description: "Kubernetes CLI, Helm, Minikube",
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
id: "terraform",
|
|
177
|
+
label: "Terraform",
|
|
178
|
+
ociRef: "ghcr.io/devcontainers/features/terraform:1",
|
|
179
|
+
description: "HashiCorp Terraform CLI",
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
id: "google-cloud-cli",
|
|
183
|
+
label: "Google Cloud CLI",
|
|
184
|
+
localScript: "google-cloud-cli.sh",
|
|
185
|
+
description: "gcloud, gsutil, bq (local install script)",
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
// Always installed, whether or not it's picked here (lib/setup-script.ts § the two features
|
|
189
|
+
// every worker image is made of): it's what creates the `vscode` user. Listed anyway so a
|
|
190
|
+
// project can override its options — they're merged over ours rather than installed twice.
|
|
191
|
+
id: "common-utils",
|
|
192
|
+
label: "Common Utilities",
|
|
193
|
+
ociRef: "ghcr.io/devcontainers/features/common-utils:2",
|
|
194
|
+
description: "zsh, git, curl, etc. — already in every image; pick it to tune its options",
|
|
195
|
+
},
|
|
196
|
+
]
|
|
197
|
+
|
|
198
|
+
// Default state of the extension picker, served when the org-scoped route is called without a query.
|
|
199
|
+
//
|
|
200
|
+
// Ids only, no registry URLs: the catalog is registry-agnostic, so the route attaches links for
|
|
201
|
+
// whichever registry the org is on (`withRegistryUrls`). The bar for being listed here is being
|
|
202
|
+
// published on Open VSX — the default registry, and the one every org gets with no configuration.
|
|
203
|
+
// Re-check with:
|
|
204
|
+
// curl -s https://open-vsx.org/api/<publisher>/<name> | head -c 200
|
|
205
|
+
// (a 404 means the id isn't on Open VSX — drop it or swap it for the equivalent that is, so the
|
|
206
|
+
// defaults stay installable without any extra configuration).
|
|
207
|
+
export const SUGGESTED_EXTENSIONS: ExtensionSuggestion[] = [
|
|
208
|
+
{ id: "esbenp.prettier-vscode", label: "Prettier - Code formatter", publisher: "esbenp" },
|
|
209
|
+
{ id: "dbaeumer.vscode-eslint", label: "ESLint", publisher: "dbaeumer" },
|
|
210
|
+
{ id: "ms-python.python", label: "Python", publisher: "ms-python" },
|
|
211
|
+
{ id: "golang.go", label: "Go", publisher: "golang" },
|
|
212
|
+
{ id: "rust-lang.rust-analyzer", label: "rust-analyzer", publisher: "rust-lang" },
|
|
213
|
+
{ id: "bradlc.vscode-tailwindcss", label: "Tailwind CSS IntelliSense", publisher: "bradlc" },
|
|
214
|
+
{ id: "eamodio.gitlens", label: "GitLens — Git supercharged", publisher: "eamodio" },
|
|
215
|
+
{ id: "ms-azuretools.vscode-docker", label: "Docker", publisher: "ms-azuretools" },
|
|
216
|
+
]
|
package/src/index.ts
CHANGED
|
@@ -9,3 +9,7 @@ export * from "./steps/index"
|
|
|
9
9
|
export * from "./extensions/index"
|
|
10
10
|
export * from "./naming/index"
|
|
11
11
|
export * from "./spec/index"
|
|
12
|
+
export * from "./types/index"
|
|
13
|
+
export * from "./catalogs/index"
|
|
14
|
+
export * from "./script/index"
|
|
15
|
+
export * from "./agent-stream/index"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Turning a project's spec into the shell a worker runs.
|
|
2
|
+
//
|
|
3
|
+
// Four generators, one recipe. `buildImageScript` bakes the project image (features, extensions);
|
|
4
|
+
// `buildSetupScript` is the first boot (credentials, dotfiles, clones, postCreate);
|
|
5
|
+
// `buildStartScript` is every boot (code-server, entrypoints, postStart); `buildWorkerScript`
|
|
6
|
+
// assembles the container CMD from the two.
|
|
7
|
+
//
|
|
8
|
+
// Pure string generation — no filesystem, no Docker, no clock. What the scripts *write* (the status
|
|
9
|
+
// file, the extensions directory, the entrypoints list) is named in `../naming`, and what they
|
|
10
|
+
// *print* so a control plane can follow along is in `../steps`.
|
|
11
|
+
//
|
|
12
|
+
// `buildContainerScript` and `buildSetupPlan` are shaped by one product's model — an agent that
|
|
13
|
+
// orchestrates setup phase by phase instead of a container that runs one long CMD (Spunto Cloud,
|
|
14
|
+
// RFC 0017). They live here anyway because they share every helper in the file; splitting them out
|
|
15
|
+
// would fork the helpers, which is the duplication this package exists to remove.
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
buildContainerScript,
|
|
19
|
+
buildImageScript,
|
|
20
|
+
buildSetupPlan,
|
|
21
|
+
buildSetupScript,
|
|
22
|
+
buildStartScript,
|
|
23
|
+
buildWorkerScript,
|
|
24
|
+
defaultVscodeUserSettings,
|
|
25
|
+
IMAGE_RECIPE_VERSION,
|
|
26
|
+
resolveRepoBranch,
|
|
27
|
+
} from "./setup-script"
|
|
28
|
+
export type { ResolvedFeature, SetupPhase, SetupScriptParams, StartScriptParams } from "./setup-script"
|
|
29
|
+
|
|
30
|
+
export { LOCAL_FEATURE_SCRIPTS, localFeatureScript } from "./local-features"
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Install scripts the platform ships itself, for features that have no usable OCI release.
|
|
2
|
+
//
|
|
3
|
+
// Embedded as strings rather than read from disk: the package does no I/O (see the README), and a
|
|
4
|
+
// build script that inlines its own payload is also a build script that cannot fail on a missing
|
|
5
|
+
// file. There is one today, and one is the right number — a local script is a maintenance burden
|
|
6
|
+
// the devcontainer ecosystem is supposed to carry, so the bar for adding another is "the upstream
|
|
7
|
+
// feature does not exist or is broken", not "this was quicker".
|
|
8
|
+
//
|
|
9
|
+
// Keyed by the catalog's `localScript` filename so the two stay legible together.
|
|
10
|
+
|
|
11
|
+
export const LOCAL_FEATURE_SCRIPTS: Record<string, string> = {
|
|
12
|
+
// Google's own apt repository; the upstream devcontainer feature pins a gcloud version and
|
|
13
|
+
// breaks whenever Google retires it.
|
|
14
|
+
"google-cloud-cli.sh": `#!/bin/sh
|
|
15
|
+
set -e
|
|
16
|
+
|
|
17
|
+
echo "[feature] google-cloud-cli: installing..."
|
|
18
|
+
|
|
19
|
+
apt-get update -y -qq
|
|
20
|
+
apt-get install -y -qq apt-transport-https ca-certificates gnupg curl
|
|
21
|
+
|
|
22
|
+
# Modern apt key setup (no deprecated apt-key)
|
|
23
|
+
curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg \
|
|
24
|
+
| gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg
|
|
25
|
+
|
|
26
|
+
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" \
|
|
27
|
+
| tee /etc/apt/sources.list.d/google-cloud-sdk.list
|
|
28
|
+
|
|
29
|
+
apt-get update -y -qq
|
|
30
|
+
apt-get install -y -qq google-cloud-cli
|
|
31
|
+
|
|
32
|
+
echo "[feature] google-cloud-cli: $(gcloud --version | head -1)"
|
|
33
|
+
`,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The script a catalog entry's `localScript` names, or undefined if the platform has no such script. */
|
|
37
|
+
export function localFeatureScript(filename: string): string | undefined {
|
|
38
|
+
return LOCAL_FEATURE_SCRIPTS[filename]
|
|
39
|
+
}
|