@spunto/build 0.6.1 → 0.7.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spunto/build",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "Spunto's shared Build engine — the devcontainer image protocol and VS Code extension registry clients, with no database, no HTTP framework and no UI.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -48,6 +48,7 @@ const WINDOWS: Record<string, number> = {
48
48
  "claude-mythos-5-1": 1_000_000,
49
49
  "claude-mythos-5": 1_000_000,
50
50
  "claude-mythos-preview": 1_000_000,
51
+ "claude-opus-5-5": 1_000_000,
51
52
  "claude-opus-5": 1_000_000,
52
53
  "claude-opus-4-8": 1_000_000,
53
54
  "claude-opus-4-7": 1_000_000,
@@ -427,7 +427,7 @@ function buildFeatureInstallScript(ociRef: string, options?: Record<string, stri
427
427
  `if [ -f "$_FEAT_DIR/devcontainer-feature.json" ]; then`,
428
428
  ` _EP=$(grep -o '"entrypoint"[[:space:]]*:[[:space:]]*"[^"]*"' "$_FEAT_DIR/devcontainer-feature.json" | head -1 | cut -d'"' -f4)`,
429
429
  ` if [ -n "$_EP" ]; then`,
430
- ` echo "$_EP" >> " + FEATURE_ENTRYPOINTS_STAGING_FILE + "`,
430
+ ` echo "$_EP" >> ${FEATURE_ENTRYPOINTS_STAGING_FILE}`,
431
431
  ` echo "[feature] ${featureId}: registered entrypoint $_EP"`,
432
432
  ` fi`,
433
433
  `fi`,
@@ -66,3 +66,15 @@ export type SetupStatus = {
66
66
  }
67
67
 
68
68
  export type { BuildStep, BuildStepKind, BuildStepState } from "../steps/build-steps"
69
+
70
+ // Le vocabulaire de statuts d'un worker — la liste, le `statusMeta` qui dit pourquoi, et le graphe
71
+ // des transitions. Même raison d'être que `SetupStatus` au-dessus : c'est un contrat versionné
72
+ // entre deux plans de contrôle, pas le détail d'une application.
73
+ export {
74
+ WORKER_STATUSES,
75
+ WORKER_STATUS_TRANSITIONS,
76
+ canTransition,
77
+ isSettingUp,
78
+ isTerminal,
79
+ } from "./worker-status"
80
+ export type { WorkerStatus, WorkerStatusReason, WorkerStatusMeta } from "./worker-status"
@@ -0,0 +1,141 @@
1
+ // Le vocabulaire de statuts d'un worker : la liste, ce qu'un statut porte avec lui, et les
2
+ // transitions permises.
3
+ //
4
+ // Ça vit ici et pas dans le design system pour la raison écrite dans la RFC 0021 § 2 : le design
5
+ // system est le lieu des formes **structurelles et laxistes** (une carte doit survivre à une valeur
6
+ // qu'elle n'a jamais vue), celui-ci est le lieu des formes **canoniques et strictes**. Un plan de
7
+ // contrôle qui écrit un statut a besoin de la seconde ; une pastille qui l'affiche se contente de
8
+ // la première. Les deux paquets restent indépendants : le typage structurel de TypeScript fait la
9
+ // jointure sans import.
10
+ //
11
+ // Contexte complet, et la mesure qui a motivé tout ça (un conteneur mort laisse la ligne `ready`
12
+ // indéfiniment) : `rfc/0026-les-etats-d-un-worker.md` dans le dépôt Spunto Cloud.
13
+
14
+ /**
15
+ * Tous les statuts qu'un worker peut porter.
16
+ *
17
+ * **Aucun produit n'est tenu de tous les émettre.** Spunto Lite n'a ni `pulling`, ni `stopping`,
18
+ * ni `deleting` — son socket Docker est local, donc ces trois-là n'existent pas chez lui : ce sont
19
+ * des allers-retours réseau qui ont reçu un nom. La liste est l'union, pas un contrat d'exhaustivité.
20
+ */
21
+ export const WORKER_STATUSES = [
22
+ "provisioning",
23
+ "building",
24
+ "pulling",
25
+ "starting",
26
+ "ready",
27
+ "stopping",
28
+ "stopped",
29
+ "deleting",
30
+ "exited",
31
+ "unknown",
32
+ "error",
33
+ ] as const
34
+
35
+ export type WorkerStatus = (typeof WORKER_STATUSES)[number]
36
+
37
+ /**
38
+ * Pourquoi un worker est dans le statut où il est. Porté par `WorkerStatusMeta`, et c'est ce qui
39
+ * distingue les trois « ça va mal » les uns des autres :
40
+ *
41
+ * - `stopped` — on l'a demandé ;
42
+ * - `exited` — le conteneur est mort tout seul (`exited` / `oom-killed` / `completed`) ;
43
+ * - `error` — une **opération de la plateforme** a échoué.
44
+ */
45
+ export type WorkerStatusReason =
46
+ /** Le conteneur s'est terminé seul, code ≠ 0. */
47
+ | "exited"
48
+ /** Idem, tué par le kernel faute de mémoire (`OOMKilled`, ou code 137). */
49
+ | "oom-killed"
50
+ /** Idem, code 0 : le process principal a rendu la main. Ni un arrêt demandé, ni un échec. */
51
+ | "completed"
52
+ /** Le conteneur n'est plus dans l'inventaire du node alors qu'on l'y attendait. */
53
+ | "removed-externally"
54
+ /** Le setup dans le conteneur a échoué (clone, `postCreate`…). */
55
+ | "setup-failed"
56
+ /** L'image du projet n'a pas pu être construite. */
57
+ | "build-failed"
58
+ /** L'attente du spawn est morte avec le process du plan de contrôle. */
59
+ | "spawn-orphaned"
60
+ /** Le node a disparu **pendant une opération en vol**, avant qu'un conteneur existe. */
61
+ | "node-lost"
62
+ /** Le node a disparu alors que le conteneur existait : on ne sait plus, on ne prétend rien. */
63
+ | "node-disconnected"
64
+
65
+ /**
66
+ * Ce qu'un statut emporte avec lui. Un seul objet, donc un nouveau cas n'est pas une migration.
67
+ *
68
+ * `previousStatus` n'est pas décoratif : basculer en `unknown` **détruirait** sinon le dernier
69
+ * statut connu, dont dépend ce qu'on propose à l'utilisateur — « unknown, c'était `ready` » et
70
+ * « unknown, c'était `stopped` » n'ouvrent pas les mêmes boutons.
71
+ */
72
+ export type WorkerStatusMeta = {
73
+ reason?: WorkerStatusReason
74
+ /** Lisible par un humain, tel qu'affiché : « Container ran out of memory (exit 137) ». */
75
+ message?: string
76
+ /** Code de sortie du conteneur, quand il y en a un. `0` est une valeur, pas une absence. */
77
+ exitCode?: number | null
78
+ oomKilled?: boolean
79
+ /** Le statut qu'on quitte, quand il faudra pouvoir y revenir (`unknown`). */
80
+ previousStatus?: WorkerStatus
81
+ /** ISO 8601 du moment où ce statut a été écrit. */
82
+ at?: string
83
+ }
84
+
85
+ /**
86
+ * Les transitions permises, **en graphe et pas en liste ordonnée**, parce que le cycle de vie n'est
87
+ * pas linéaire : `building` est une attente *imbriquée* dans la préparation, donc
88
+ * `building → provisioning` est légitime (l'image est là, on reprend où on en était).
89
+ *
90
+ * Deux décisions sont encodées ici plutôt qu'écrites quelque part :
91
+ *
92
+ * 1. **Un worker pré-conteneur ne passe jamais en `unknown`.** `provisioning`, `building` et
93
+ * `pulling` mènent à `error` quand le node tombe, et c'est juste : l'attente vivait dans le
94
+ * plan de contrôle et personne ne la reprendra. `unknown` veut dire « le conteneur existe
95
+ * probablement, on ne le voit plus » — il n'y a rien à ne pas voir avant qu'il existe.
96
+ * 2. **`deleting` ne mène nulle part** sauf à l'échec : après, la ligne n'existe plus, et
97
+ * « supprimé » n'est pas un statut.
98
+ */
99
+ export const WORKER_STATUS_TRANSITIONS: Record<WorkerStatus, readonly WorkerStatus[]> = {
100
+ provisioning: ["building", "pulling", "starting", "error", "deleting"],
101
+ building: ["provisioning", "pulling", "starting", "error", "deleting"],
102
+ pulling: ["starting", "error", "deleting"],
103
+ starting: ["ready", "stopping", "stopped", "exited", "unknown", "error", "deleting"],
104
+ ready: ["starting", "stopping", "stopped", "exited", "unknown", "error", "deleting"],
105
+ stopping: ["stopped", "error", "deleting"],
106
+ stopped: ["starting", "unknown", "error", "deleting"],
107
+ // Relancer un worker mort repart de `starting` : l'image et le volume sont toujours là.
108
+ exited: ["starting", "stopped", "unknown", "error", "deleting"],
109
+ // La réconciliation tranche au retour du node : l'inventaire dit ce qui existe vraiment.
110
+ unknown: ["ready", "starting", "stopped", "exited", "error", "deleting"],
111
+ deleting: ["error"],
112
+ // Un worker en erreur est relevé quand le node revient avec son conteneur, ou reconstruit.
113
+ error: ["provisioning", "starting", "ready", "stopped", "exited", "unknown", "deleting"],
114
+ }
115
+
116
+ /** `true` si passer de `from` à `to` est une transition prévue. */
117
+ export function canTransition(from: WorkerStatus, to: WorkerStatus): boolean {
118
+ return WORKER_STATUS_TRANSITIONS[from]?.includes(to) ?? false
119
+ }
120
+
121
+ /**
122
+ * Le worker n'est pas encore utilisable et **la plateforme travaille dessus** : c'est le prédicat
123
+ * qui décide d'afficher une barre de progression plutôt qu'un bouton.
124
+ *
125
+ * `unknown` n'en fait pas partie : on ne sait pas si quelque chose est en cours, et annoncer un
126
+ * setup en vol pour une machine qu'on ne voit plus serait exactement le mensonge que ce statut
127
+ * existe pour éviter.
128
+ */
129
+ export function isSettingUp(status: WorkerStatus): boolean {
130
+ return status === "provisioning" || status === "building" || status === "pulling" || status === "starting"
131
+ }
132
+
133
+ /**
134
+ * Plus rien ne bouge tout seul : il faudra une action pour en sortir.
135
+ *
136
+ * `unknown` n'est pas terminal (la reconnexion du node le tranchera) et `deleting` non plus (il
137
+ * finit par la disparition de la ligne, pas par un statut).
138
+ */
139
+ export function isTerminal(status: WorkerStatus): boolean {
140
+ return status === "stopped" || status === "exited" || status === "error"
141
+ }