@foro-sh/foro 0.10.0 → 0.11.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 CHANGED
@@ -4,16 +4,18 @@ This package is the TypeScript SDK for Foro.
4
4
 
5
5
  ## `@foro-sh/foro/manifest-cases`
6
6
 
7
- The shared `foro.yaml` validation table, as typed data. Foro's Python
7
+ The shared project-config validation table, as typed data. Foro's Python
8
8
  `_manifest.py` is a port of [foro-sh/platform]'s
9
9
  `apps/api/src/services/manifest.ts`, so both implementations run this same
10
- table to guarantee they never disagree about what a valid manifest is — if
11
- they did, `foro check` would pass locally and the deploy would fail.
10
+ table to guarantee they never disagree about what a valid `pyproject.toml` or
11
+ `package.json` is — if they did, `foro check` would pass locally and the deploy
12
+ would fail.
12
13
 
13
14
  ```ts
14
15
  import { manifestCases } from '@foro-sh/foro/manifest-cases'
15
16
 
16
- for (const { name, yaml, expect } of manifestCases) {
17
+ for (const { name, files, expect } of manifestCases) {
18
+ // write `files` into an empty directory, then validate it
17
19
  // expect is { ok: true } or { ok: false, reason: ManifestRejectionReason }
18
20
  }
19
21
  ```
@@ -1,221 +1,487 @@
1
1
  export declare const rawManifestCases: readonly [{
2
- readonly name: "minimal-valid";
3
- readonly yaml: "name: my-server\nentrypoint: server.py\n";
2
+ readonly name: "python-minimal";
3
+ readonly files: {
4
+ readonly "pyproject.toml": "[project]\nname = \"my-server\"\n";
5
+ readonly "server.py": "";
6
+ };
4
7
  readonly expect: {
5
8
  readonly ok: true;
6
9
  };
7
10
  }, {
8
- readonly name: "nested-entrypoint";
9
- readonly yaml: "name: my-server\nentrypoint: src/server.py\n";
11
+ readonly name: "python-poetry-only-name";
12
+ readonly files: {
13
+ readonly "pyproject.toml": "[tool.poetry]\nname = \"my-server\"\n";
14
+ readonly "main.py": "";
15
+ };
10
16
  readonly expect: {
11
17
  readonly ok: true;
12
18
  };
13
19
  }, {
14
- readonly name: "all-fields";
15
- readonly yaml: "name: my-server\nentrypoint: server.py\nbuild_path: app\nruntime: python\nruntime_version: \"3.11\"\nport: 9000\ndependency_manager: poetry\n";
20
+ readonly name: "python-explicit-entrypoint";
21
+ readonly files: {
22
+ readonly "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"src/server.py\"\n";
23
+ };
16
24
  readonly expect: {
17
25
  readonly ok: true;
18
26
  };
19
27
  }, {
20
- readonly name: "unquoted-runtime-version";
21
- readonly yaml: "name: my-server\nentrypoint: server.py\nruntime_version: 3.12\n";
28
+ readonly name: "python-all-fields";
29
+ readonly files: {
30
+ readonly "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nbuild_path = \"app\"\nentrypoint = \"server.py\"\nruntime = \"python\"\nruntime_version = \"3.11\"\nport = 9000\ndependency_manager = \"poetry\"\n";
31
+ };
22
32
  readonly expect: {
23
33
  readonly ok: true;
24
34
  };
25
35
  }, {
26
- readonly name: "bad-name-uppercase";
27
- readonly yaml: "name: My-Server\nentrypoint: server.py\n";
36
+ readonly name: "python-requires-python-range";
37
+ readonly files: {
38
+ readonly "pyproject.toml": "[project]\nname = \"my-server\"\nrequires-python = \">=3.11,<3.13\"\n";
39
+ readonly "server.py": "";
40
+ };
41
+ readonly expect: {
42
+ readonly ok: true;
43
+ };
44
+ }, {
45
+ readonly name: "python-requires-python-exclusion";
46
+ readonly files: {
47
+ readonly "pyproject.toml": "[project]\nname = \"my-server\"\nrequires-python = \">=3.11,!=3.13\"\n";
48
+ readonly "server.py": "";
49
+ };
50
+ readonly expect: {
51
+ readonly ok: true;
52
+ };
53
+ }, {
54
+ readonly name: "python-requires-python-excludes-every-supported-version-one-by-one";
55
+ readonly files: {
56
+ readonly "pyproject.toml": "[project]\nname = \"my-server\"\nrequires-python = \">=3.11,!=3.11,!=3.12,!=3.13\"\n";
57
+ readonly "server.py": "";
58
+ };
28
59
  readonly expect: {
29
60
  readonly ok: false;
30
- readonly reason: "invalid_name";
61
+ readonly reason: "invalid_runtime_version";
62
+ };
63
+ }, {
64
+ readonly name: "python-hyphen-inside-a-path-segment-is-fine";
65
+ readonly files: {
66
+ readonly "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"my-tools/mcp-server.py\"\n";
67
+ };
68
+ readonly expect: {
69
+ readonly ok: true;
31
70
  };
32
71
  }, {
33
- readonly name: "bad-name-too-short";
34
- readonly yaml: "name: ab\nentrypoint: server.py\n";
72
+ readonly name: "python-scoped-unspellable-name-is-normalised";
73
+ readonly files: {
74
+ readonly "pyproject.toml": "[project]\nname = \"My_Server\"\n";
75
+ readonly "server.py": "";
76
+ };
77
+ readonly expect: {
78
+ readonly ok: true;
79
+ };
80
+ }, {
81
+ readonly name: "python-no-entry-file";
82
+ readonly files: {
83
+ readonly "pyproject.toml": "[project]\nname = \"my-server\"\n";
84
+ };
35
85
  readonly expect: {
36
86
  readonly ok: false;
37
- readonly reason: "invalid_name";
87
+ readonly reason: "invalid_entrypoint";
38
88
  };
39
89
  }, {
40
- readonly name: "missing-name";
41
- readonly yaml: "entrypoint: server.py\n";
90
+ readonly name: "python-entrypoint-traversal";
91
+ readonly files: {
92
+ readonly "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"../server.py\"\n";
93
+ };
42
94
  readonly expect: {
43
95
  readonly ok: false;
44
- readonly reason: "invalid_name";
96
+ readonly reason: "invalid_entrypoint";
45
97
  };
46
98
  }, {
47
- readonly name: "bad-entrypoint-traversal";
48
- readonly yaml: "name: my-server\nentrypoint: ../server.py\n";
99
+ readonly name: "python-entrypoint-absolute";
100
+ readonly files: {
101
+ readonly "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"/server.py\"\n";
102
+ };
49
103
  readonly expect: {
50
104
  readonly ok: false;
51
105
  readonly reason: "invalid_entrypoint";
52
106
  };
53
107
  }, {
54
- readonly name: "bad-entrypoint-absolute";
55
- readonly yaml: "name: my-server\nentrypoint: /server.py\n";
108
+ readonly name: "python-entrypoint-shell-metachar";
109
+ readonly files: {
110
+ readonly "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py; rm -rf x\"\n";
111
+ };
56
112
  readonly expect: {
57
113
  readonly ok: false;
58
114
  readonly reason: "invalid_entrypoint";
59
115
  };
60
116
  }, {
61
- readonly name: "bad-entrypoint-shell-metachar";
62
- readonly yaml: "name: my-server\nentrypoint: \"server.py; rm -rf x\"\n";
117
+ readonly name: "python-entrypoint-leading-dash";
118
+ readonly files: {
119
+ readonly "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"--isolated\"\n";
120
+ };
63
121
  readonly expect: {
64
122
  readonly ok: false;
65
123
  readonly reason: "invalid_entrypoint";
66
124
  };
67
125
  }, {
68
- readonly name: "missing-entrypoint";
69
- readonly yaml: "name: my-server\n";
126
+ readonly name: "python-entrypoint-leading-dash-nested";
127
+ readonly files: {
128
+ readonly "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"src/-rf.py\"\n";
129
+ };
70
130
  readonly expect: {
71
131
  readonly ok: false;
72
132
  readonly reason: "invalid_entrypoint";
73
133
  };
74
134
  }, {
75
- readonly name: "bad-build-path-traversal";
76
- readonly yaml: "name: my-server\nentrypoint: server.py\nbuild_path: ../x\n";
135
+ readonly name: "python-build-path-traversal";
136
+ readonly files: {
137
+ readonly "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\nbuild_path = \"../x\"\n";
138
+ };
77
139
  readonly expect: {
78
140
  readonly ok: false;
79
141
  readonly reason: "invalid_build_path";
80
142
  };
81
143
  }, {
82
- readonly name: "bad-build-path-shell-metachar";
83
- readonly yaml: "name: my-server\nentrypoint: server.py\nbuild_path: \". && whoami\"\n";
144
+ readonly name: "python-build-path-shell-metachar";
145
+ readonly files: {
146
+ readonly "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\nbuild_path = \". && whoami\"\n";
147
+ };
84
148
  readonly expect: {
85
149
  readonly ok: false;
86
150
  readonly reason: "invalid_build_path";
87
151
  };
88
152
  }, {
89
- readonly name: "bad-build-path-space";
90
- readonly yaml: "name: my-server\nentrypoint: server.py\nbuild_path: \"my services\"\n";
153
+ readonly name: "python-build-path-space";
154
+ readonly files: {
155
+ readonly "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\nbuild_path = \"my services\"\n";
156
+ };
91
157
  readonly expect: {
92
158
  readonly ok: false;
93
159
  readonly reason: "invalid_build_path";
94
160
  };
95
161
  }, {
96
- readonly name: "bad-build-path-newline";
97
- readonly yaml: "name: my-server\nentrypoint: server.py\nbuild_path: \"app\\nUSER root\"\n";
162
+ readonly name: "python-build-path-newline";
163
+ readonly files: {
164
+ readonly "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\nbuild_path = \"app\\nUSER root\"\n";
165
+ };
98
166
  readonly expect: {
99
167
  readonly ok: false;
100
168
  readonly reason: "invalid_build_path";
101
169
  };
102
170
  }, {
103
- readonly name: "bad-runtime-version";
104
- readonly yaml: "name: my-server\nentrypoint: server.py\nruntime_version: \"3.9\"\n";
171
+ readonly name: "python-build-path-leading-dash";
172
+ readonly files: {
173
+ readonly "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\nbuild_path = \"-rf\"\n";
174
+ };
175
+ readonly expect: {
176
+ readonly ok: false;
177
+ readonly reason: "invalid_build_path";
178
+ };
179
+ }, {
180
+ readonly name: "python-runtime-version-outside-allowlist";
181
+ readonly files: {
182
+ readonly "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\nruntime_version = \"3.9\"\n";
183
+ };
105
184
  readonly expect: {
106
185
  readonly ok: false;
107
186
  readonly reason: "invalid_runtime_version";
108
187
  };
109
188
  }, {
110
- readonly name: "bad-runtime";
111
- readonly yaml: "name: my-server\nentrypoint: server.py\nruntime: node\n";
189
+ readonly name: "python-requires-python-excludes-every-supported-version";
190
+ readonly files: {
191
+ readonly "pyproject.toml": "[project]\nname = \"my-server\"\nrequires-python = \"==3.9\"\n";
192
+ readonly "server.py": "";
193
+ };
112
194
  readonly expect: {
113
195
  readonly ok: false;
114
- readonly reason: "invalid_runtime";
196
+ readonly reason: "invalid_runtime_version";
115
197
  };
116
198
  }, {
117
- readonly name: "runtime-version-from-another-runtime";
118
- readonly yaml: "name: my-server\nentrypoint: server.py\nruntime: python\nruntime_version: \"22\"\n";
199
+ readonly name: "python-runtime-version-from-another-runtime";
200
+ readonly files: {
201
+ readonly "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\nruntime_version = \"22\"\n";
202
+ };
119
203
  readonly expect: {
120
204
  readonly ok: false;
121
205
  readonly reason: "invalid_runtime_version";
122
206
  };
207
+ }, {
208
+ readonly name: "runtime-outside-allowlist";
209
+ readonly files: {
210
+ readonly "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\nruntime = \"ruby\"\n";
211
+ };
212
+ readonly expect: {
213
+ readonly ok: false;
214
+ readonly reason: "invalid_runtime";
215
+ };
216
+ }, {
217
+ readonly name: "runtime-disagrees-with-the-file-it-was-read-from";
218
+ readonly files: {
219
+ readonly "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\nruntime = \"node\"\n";
220
+ };
221
+ readonly expect: {
222
+ readonly ok: false;
223
+ readonly reason: "invalid_runtime";
224
+ };
225
+ }, {
226
+ readonly name: "both-config-files-and-neither-claims-the-runtime";
227
+ readonly files: {
228
+ readonly "pyproject.toml": "[project]\nname = \"my-server\"\n";
229
+ readonly "package.json": "{\"name\": \"my-server\", \"main\": \"index.js\"}";
230
+ readonly "server.py": "";
231
+ };
232
+ readonly expect: {
233
+ readonly ok: false;
234
+ readonly reason: "invalid_runtime";
235
+ };
236
+ }, {
237
+ readonly name: "both-config-files-and-both-claim-the-runtime";
238
+ readonly files: {
239
+ readonly "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nruntime = \"python\"\n";
240
+ readonly "package.json": "{\"name\": \"my-server\", \"main\": \"index.js\", \"foro\": {\"runtime\": \"node\"}}";
241
+ readonly "server.py": "";
242
+ };
243
+ readonly expect: {
244
+ readonly ok: false;
245
+ readonly reason: "invalid_runtime";
246
+ };
247
+ }, {
248
+ readonly name: "both-config-files-python-claims-the-runtime";
249
+ readonly files: {
250
+ readonly "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nruntime = \"python\"\n";
251
+ readonly "package.json": "{\"name\": \"tooling-only\"}";
252
+ readonly "server.py": "";
253
+ };
254
+ readonly expect: {
255
+ readonly ok: true;
256
+ };
257
+ }, {
258
+ readonly name: "both-config-files-node-claims-the-runtime";
259
+ readonly files: {
260
+ readonly "pyproject.toml": "[project]\nname = \"my-server\"\n";
261
+ readonly "package.json": "{\"name\": \"my-server\", \"main\": \"index.js\", \"foro\": {\"runtime\": \"node\"}}";
262
+ readonly "server.py": "";
263
+ };
264
+ readonly expect: {
265
+ readonly ok: true;
266
+ };
267
+ }, {
268
+ readonly name: "no-config-file-only-a-legacy-manifest";
269
+ readonly files: {
270
+ readonly "foro.yaml": "name: my-server\nentrypoint: server.py\n";
271
+ readonly "server.py": "";
272
+ };
273
+ readonly expect: {
274
+ readonly ok: false;
275
+ readonly reason: "missing_manifest";
276
+ };
123
277
  }, {
124
278
  readonly name: "unknown-field-stale-python-version";
125
- readonly yaml: "name: my-server\nentrypoint: server.py\npython_version: \"3.13\"\n";
279
+ readonly files: {
280
+ readonly "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\npython_version = \"3.13\"\n";
281
+ };
126
282
  readonly expect: {
127
283
  readonly ok: false;
128
284
  readonly reason: "unknown_field";
129
285
  };
130
286
  }, {
131
287
  readonly name: "unknown-field-typo";
132
- readonly yaml: "name: my-server\nentrypoint: server.py\nentrypoints: server.py\n";
288
+ readonly files: {
289
+ readonly "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\nentrypoints = \"server.py\"\n";
290
+ };
133
291
  readonly expect: {
134
292
  readonly ok: false;
135
293
  readonly reason: "unknown_field";
136
294
  };
137
295
  }, {
138
- readonly name: "bad-port-too-low";
139
- readonly yaml: "name: my-server\nentrypoint: server.py\nport: 80\n";
296
+ readonly name: "unknown-field-name-belongs-to-the-package";
297
+ readonly files: {
298
+ readonly "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\nname = \"other\"\n";
299
+ };
300
+ readonly expect: {
301
+ readonly ok: false;
302
+ readonly reason: "unknown_field";
303
+ };
304
+ }, {
305
+ readonly name: "port-too-low";
306
+ readonly files: {
307
+ readonly "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\nport = 80\n";
308
+ };
140
309
  readonly expect: {
141
310
  readonly ok: false;
142
311
  readonly reason: "invalid_port";
143
312
  };
144
313
  }, {
145
- readonly name: "bad-port-too-high";
146
- readonly yaml: "name: my-server\nentrypoint: server.py\nport: 70000\n";
314
+ readonly name: "port-too-high";
315
+ readonly files: {
316
+ readonly "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\nport = 70000\n";
317
+ };
147
318
  readonly expect: {
148
319
  readonly ok: false;
149
320
  readonly reason: "invalid_port";
150
321
  };
151
322
  }, {
152
- readonly name: "bad-port-sidecar-reserved";
153
- readonly yaml: "name: my-server\nentrypoint: server.py\nport: 8001\n";
323
+ readonly name: "port-sidecar-reserved";
324
+ readonly files: {
325
+ readonly "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\nport = 8001\n";
326
+ };
154
327
  readonly expect: {
155
328
  readonly ok: false;
156
329
  readonly reason: "invalid_port";
157
330
  };
158
331
  }, {
159
- readonly name: "bad-port-non-integer";
160
- readonly yaml: "name: my-server\nentrypoint: server.py\nport: \"abc\"\n";
332
+ readonly name: "port-gate-reserved";
333
+ readonly files: {
334
+ readonly "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\nport = 8002\n";
335
+ };
161
336
  readonly expect: {
162
337
  readonly ok: false;
163
338
  readonly reason: "invalid_port";
164
339
  };
165
340
  }, {
166
- readonly name: "bad-dependency-manager";
167
- readonly yaml: "name: my-server\nentrypoint: server.py\ndependency_manager: pipx\n";
341
+ readonly name: "port-non-integer";
342
+ readonly files: {
343
+ readonly "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\nport = \"abc\"\n";
344
+ };
168
345
  readonly expect: {
169
346
  readonly ok: false;
170
- readonly reason: "invalid_dependency_manager";
347
+ readonly reason: "invalid_port";
171
348
  };
172
349
  }, {
173
- readonly name: "invalid-yaml";
174
- readonly yaml: "name: [unclosed\n";
350
+ readonly name: "dependency-manager-outside-allowlist";
351
+ readonly files: {
352
+ readonly "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\ndependency_manager = \"pipx\"\n";
353
+ };
175
354
  readonly expect: {
176
355
  readonly ok: false;
177
- readonly reason: "invalid_yaml";
356
+ readonly reason: "invalid_dependency_manager";
178
357
  };
179
358
  }, {
180
- readonly name: "invalid-shape-scalar";
181
- readonly yaml: "just a string\n";
359
+ readonly name: "node-dependency-manager-on-python";
360
+ readonly files: {
361
+ readonly "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\ndependency_manager = \"npm\"\n";
362
+ };
182
363
  readonly expect: {
183
364
  readonly ok: false;
184
- readonly reason: "invalid_shape";
365
+ readonly reason: "invalid_dependency_manager";
185
366
  };
186
367
  }, {
187
- readonly name: "invalid-shape-list";
188
- readonly yaml: "- one\n- two\n";
368
+ readonly name: "unparseable-pyproject";
369
+ readonly files: {
370
+ readonly "pyproject.toml": "[project\nname = \"my-server\"\n";
371
+ };
189
372
  readonly expect: {
190
373
  readonly ok: false;
191
374
  readonly reason: "invalid_shape";
192
375
  };
193
376
  }, {
194
- readonly name: "bad-entrypoint-leading-dash";
195
- readonly yaml: "name: my-server\nentrypoint: --isolated\n";
377
+ readonly name: "node-minimal";
378
+ readonly files: {
379
+ readonly "package.json": "{\"name\": \"my-server\", \"main\": \"dist/index.js\"}";
380
+ };
381
+ readonly expect: {
382
+ readonly ok: true;
383
+ };
384
+ }, {
385
+ readonly name: "node-scoped-name";
386
+ readonly files: {
387
+ readonly "package.json": "{\"name\": \"@acme/my-server\", \"main\": \"dist/index.js\"}";
388
+ };
389
+ readonly expect: {
390
+ readonly ok: true;
391
+ };
392
+ }, {
393
+ readonly name: "node-bin-map";
394
+ readonly files: {
395
+ readonly "package.json": "{\"name\": \"my-server\", \"bin\": {\"my-server\": \"dist/cli.js\"}}";
396
+ };
397
+ readonly expect: {
398
+ readonly ok: true;
399
+ };
400
+ }, {
401
+ readonly name: "node-implicit-index";
402
+ readonly files: {
403
+ readonly "package.json": "{\"name\": \"my-server\"}";
404
+ readonly "index.js": "";
405
+ };
406
+ readonly expect: {
407
+ readonly ok: true;
408
+ };
409
+ }, {
410
+ readonly name: "node-engines-range";
411
+ readonly files: {
412
+ readonly "package.json": "{\"name\": \"my-server\", \"main\": \"index.js\", \"engines\": {\"node\": \">=22\"}}";
413
+ };
414
+ readonly expect: {
415
+ readonly ok: true;
416
+ };
417
+ }, {
418
+ readonly name: "node-engines-union-of-every-supported-major";
419
+ readonly files: {
420
+ readonly "package.json": "{\"name\": \"my-server\", \"main\": \"index.js\", \"engines\": {\"node\": \"^22 || ^24\"}}";
421
+ };
422
+ readonly expect: {
423
+ readonly ok: true;
424
+ };
425
+ }, {
426
+ readonly name: "node-engines-union-with-one-unsupported-branch";
427
+ readonly files: {
428
+ readonly "package.json": "{\"name\": \"my-server\", \"main\": \"index.js\", \"engines\": {\"node\": \"^20 || ^22\"}}";
429
+ };
430
+ readonly expect: {
431
+ readonly ok: true;
432
+ };
433
+ }, {
434
+ readonly name: "node-all-fields";
435
+ readonly files: {
436
+ readonly "package.json": "{\"name\": \"my-server\", \"main\": \"dist/index.js\", \"foro\": {\"runtime\": \"node\", \"runtime_version\": \"22\", \"port\": 9000, \"dependency_manager\": \"pnpm\"}}";
437
+ };
438
+ readonly expect: {
439
+ readonly ok: true;
440
+ };
441
+ }, {
442
+ readonly name: "node-no-entry-file";
443
+ readonly files: {
444
+ readonly "package.json": "{\"name\": \"my-server\"}";
445
+ };
196
446
  readonly expect: {
197
447
  readonly ok: false;
198
448
  readonly reason: "invalid_entrypoint";
199
449
  };
200
450
  }, {
201
- readonly name: "bad-entrypoint-leading-dash-nested";
202
- readonly yaml: "name: my-server\nentrypoint: src/-rf.py\n";
451
+ readonly name: "node-engines-excludes-every-supported-version";
452
+ readonly files: {
453
+ readonly "package.json": "{\"name\": \"my-server\", \"main\": \"index.js\", \"engines\": {\"node\": \"^20\"}}";
454
+ };
203
455
  readonly expect: {
204
456
  readonly ok: false;
205
- readonly reason: "invalid_entrypoint";
457
+ readonly reason: "invalid_runtime_version";
206
458
  };
207
459
  }, {
208
- readonly name: "bad-build-path-leading-dash";
209
- readonly yaml: "name: my-server\nentrypoint: server.py\nbuild_path: -rf\n";
460
+ readonly name: "node-runtime-version-from-another-runtime";
461
+ readonly files: {
462
+ readonly "package.json": "{\"name\": \"my-server\", \"main\": \"index.js\", \"foro\": {\"runtime_version\": \"3.12\"}}";
463
+ };
210
464
  readonly expect: {
211
465
  readonly ok: false;
212
- readonly reason: "invalid_build_path";
466
+ readonly reason: "invalid_runtime_version";
213
467
  };
214
468
  }, {
215
- readonly name: "hyphen-inside-a-path-segment-is-fine";
216
- readonly yaml: "name: my-server\nentrypoint: my-tools/mcp-server.py\n";
469
+ readonly name: "python-dependency-manager-on-node";
470
+ readonly files: {
471
+ readonly "package.json": "{\"name\": \"my-server\", \"main\": \"index.js\", \"foro\": {\"dependency_manager\": \"uv\"}}";
472
+ };
217
473
  readonly expect: {
218
- readonly ok: true;
474
+ readonly ok: false;
475
+ readonly reason: "invalid_dependency_manager";
476
+ };
477
+ }, {
478
+ readonly name: "unparseable-package-json";
479
+ readonly files: {
480
+ readonly "package.json": "{\"name\": ";
481
+ };
482
+ readonly expect: {
483
+ readonly ok: false;
484
+ readonly reason: "invalid_shape";
219
485
  };
220
486
  }];
221
487
  //# sourceMappingURL=_generated-manifest-cases.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"_generated-manifest-cases.d.ts","sourceRoot":"","sources":["../src/_generated-manifest-cases.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4PnB,CAAA"}
1
+ {"version":3,"file":"_generated-manifest-cases.d.ts","sourceRoot":"","sources":["../src/_generated-manifest-cases.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4hBnB,CAAA"}
@@ -2,148 +2,316 @@
2
2
  // manifest-cases.json. Do not edit - edit the JSON and rebuild.
3
3
  export const rawManifestCases = [
4
4
  {
5
- "name": "minimal-valid",
6
- "yaml": "name: my-server\nentrypoint: server.py\n",
5
+ "name": "python-minimal",
6
+ "files": {
7
+ "pyproject.toml": "[project]\nname = \"my-server\"\n",
8
+ "server.py": ""
9
+ },
7
10
  "expect": {
8
11
  "ok": true
9
12
  }
10
13
  },
11
14
  {
12
- "name": "nested-entrypoint",
13
- "yaml": "name: my-server\nentrypoint: src/server.py\n",
15
+ "name": "python-poetry-only-name",
16
+ "files": {
17
+ "pyproject.toml": "[tool.poetry]\nname = \"my-server\"\n",
18
+ "main.py": ""
19
+ },
14
20
  "expect": {
15
21
  "ok": true
16
22
  }
17
23
  },
18
24
  {
19
- "name": "all-fields",
20
- "yaml": "name: my-server\nentrypoint: server.py\nbuild_path: app\nruntime: python\nruntime_version: \"3.11\"\nport: 9000\ndependency_manager: poetry\n",
25
+ "name": "python-explicit-entrypoint",
26
+ "files": {
27
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"src/server.py\"\n"
28
+ },
21
29
  "expect": {
22
30
  "ok": true
23
31
  }
24
32
  },
25
33
  {
26
- "name": "unquoted-runtime-version",
27
- "yaml": "name: my-server\nentrypoint: server.py\nruntime_version: 3.12\n",
34
+ "name": "python-all-fields",
35
+ "files": {
36
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nbuild_path = \"app\"\nentrypoint = \"server.py\"\nruntime = \"python\"\nruntime_version = \"3.11\"\nport = 9000\ndependency_manager = \"poetry\"\n"
37
+ },
28
38
  "expect": {
29
39
  "ok": true
30
40
  }
31
41
  },
32
42
  {
33
- "name": "bad-name-uppercase",
34
- "yaml": "name: My-Server\nentrypoint: server.py\n",
43
+ "name": "python-requires-python-range",
44
+ "files": {
45
+ "pyproject.toml": "[project]\nname = \"my-server\"\nrequires-python = \">=3.11,<3.13\"\n",
46
+ "server.py": ""
47
+ },
48
+ "expect": {
49
+ "ok": true
50
+ }
51
+ },
52
+ {
53
+ "name": "python-requires-python-exclusion",
54
+ "files": {
55
+ "pyproject.toml": "[project]\nname = \"my-server\"\nrequires-python = \">=3.11,!=3.13\"\n",
56
+ "server.py": ""
57
+ },
58
+ "expect": {
59
+ "ok": true
60
+ }
61
+ },
62
+ {
63
+ "name": "python-requires-python-excludes-every-supported-version-one-by-one",
64
+ "files": {
65
+ "pyproject.toml": "[project]\nname = \"my-server\"\nrequires-python = \">=3.11,!=3.11,!=3.12,!=3.13\"\n",
66
+ "server.py": ""
67
+ },
35
68
  "expect": {
36
69
  "ok": false,
37
- "reason": "invalid_name"
70
+ "reason": "invalid_runtime_version"
71
+ }
72
+ },
73
+ {
74
+ "name": "python-hyphen-inside-a-path-segment-is-fine",
75
+ "files": {
76
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"my-tools/mcp-server.py\"\n"
77
+ },
78
+ "expect": {
79
+ "ok": true
80
+ }
81
+ },
82
+ {
83
+ "name": "python-scoped-unspellable-name-is-normalised",
84
+ "files": {
85
+ "pyproject.toml": "[project]\nname = \"My_Server\"\n",
86
+ "server.py": ""
87
+ },
88
+ "expect": {
89
+ "ok": true
38
90
  }
39
91
  },
40
92
  {
41
- "name": "bad-name-too-short",
42
- "yaml": "name: ab\nentrypoint: server.py\n",
93
+ "name": "python-no-entry-file",
94
+ "files": {
95
+ "pyproject.toml": "[project]\nname = \"my-server\"\n"
96
+ },
43
97
  "expect": {
44
98
  "ok": false,
45
- "reason": "invalid_name"
99
+ "reason": "invalid_entrypoint"
46
100
  }
47
101
  },
48
102
  {
49
- "name": "missing-name",
50
- "yaml": "entrypoint: server.py\n",
103
+ "name": "python-entrypoint-traversal",
104
+ "files": {
105
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"../server.py\"\n"
106
+ },
51
107
  "expect": {
52
108
  "ok": false,
53
- "reason": "invalid_name"
109
+ "reason": "invalid_entrypoint"
54
110
  }
55
111
  },
56
112
  {
57
- "name": "bad-entrypoint-traversal",
58
- "yaml": "name: my-server\nentrypoint: ../server.py\n",
113
+ "name": "python-entrypoint-absolute",
114
+ "files": {
115
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"/server.py\"\n"
116
+ },
59
117
  "expect": {
60
118
  "ok": false,
61
119
  "reason": "invalid_entrypoint"
62
120
  }
63
121
  },
64
122
  {
65
- "name": "bad-entrypoint-absolute",
66
- "yaml": "name: my-server\nentrypoint: /server.py\n",
123
+ "name": "python-entrypoint-shell-metachar",
124
+ "files": {
125
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py; rm -rf x\"\n"
126
+ },
67
127
  "expect": {
68
128
  "ok": false,
69
129
  "reason": "invalid_entrypoint"
70
130
  }
71
131
  },
72
132
  {
73
- "name": "bad-entrypoint-shell-metachar",
74
- "yaml": "name: my-server\nentrypoint: \"server.py; rm -rf x\"\n",
133
+ "name": "python-entrypoint-leading-dash",
134
+ "files": {
135
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"--isolated\"\n"
136
+ },
75
137
  "expect": {
76
138
  "ok": false,
77
139
  "reason": "invalid_entrypoint"
78
140
  }
79
141
  },
80
142
  {
81
- "name": "missing-entrypoint",
82
- "yaml": "name: my-server\n",
143
+ "name": "python-entrypoint-leading-dash-nested",
144
+ "files": {
145
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"src/-rf.py\"\n"
146
+ },
83
147
  "expect": {
84
148
  "ok": false,
85
149
  "reason": "invalid_entrypoint"
86
150
  }
87
151
  },
88
152
  {
89
- "name": "bad-build-path-traversal",
90
- "yaml": "name: my-server\nentrypoint: server.py\nbuild_path: ../x\n",
153
+ "name": "python-build-path-traversal",
154
+ "files": {
155
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\nbuild_path = \"../x\"\n"
156
+ },
91
157
  "expect": {
92
158
  "ok": false,
93
159
  "reason": "invalid_build_path"
94
160
  }
95
161
  },
96
162
  {
97
- "name": "bad-build-path-shell-metachar",
98
- "yaml": "name: my-server\nentrypoint: server.py\nbuild_path: \". && whoami\"\n",
163
+ "name": "python-build-path-shell-metachar",
164
+ "files": {
165
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\nbuild_path = \". && whoami\"\n"
166
+ },
99
167
  "expect": {
100
168
  "ok": false,
101
169
  "reason": "invalid_build_path"
102
170
  }
103
171
  },
104
172
  {
105
- "name": "bad-build-path-space",
106
- "yaml": "name: my-server\nentrypoint: server.py\nbuild_path: \"my services\"\n",
173
+ "name": "python-build-path-space",
174
+ "files": {
175
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\nbuild_path = \"my services\"\n"
176
+ },
107
177
  "expect": {
108
178
  "ok": false,
109
179
  "reason": "invalid_build_path"
110
180
  }
111
181
  },
112
182
  {
113
- "name": "bad-build-path-newline",
114
- "yaml": "name: my-server\nentrypoint: server.py\nbuild_path: \"app\\nUSER root\"\n",
183
+ "name": "python-build-path-newline",
184
+ "files": {
185
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\nbuild_path = \"app\\nUSER root\"\n"
186
+ },
115
187
  "expect": {
116
188
  "ok": false,
117
189
  "reason": "invalid_build_path"
118
190
  }
119
191
  },
120
192
  {
121
- "name": "bad-runtime-version",
122
- "yaml": "name: my-server\nentrypoint: server.py\nruntime_version: \"3.9\"\n",
193
+ "name": "python-build-path-leading-dash",
194
+ "files": {
195
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\nbuild_path = \"-rf\"\n"
196
+ },
197
+ "expect": {
198
+ "ok": false,
199
+ "reason": "invalid_build_path"
200
+ }
201
+ },
202
+ {
203
+ "name": "python-runtime-version-outside-allowlist",
204
+ "files": {
205
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\nruntime_version = \"3.9\"\n"
206
+ },
123
207
  "expect": {
124
208
  "ok": false,
125
209
  "reason": "invalid_runtime_version"
126
210
  }
127
211
  },
128
212
  {
129
- "name": "bad-runtime",
130
- "yaml": "name: my-server\nentrypoint: server.py\nruntime: node\n",
213
+ "name": "python-requires-python-excludes-every-supported-version",
214
+ "files": {
215
+ "pyproject.toml": "[project]\nname = \"my-server\"\nrequires-python = \"==3.9\"\n",
216
+ "server.py": ""
217
+ },
131
218
  "expect": {
132
219
  "ok": false,
133
- "reason": "invalid_runtime"
220
+ "reason": "invalid_runtime_version"
134
221
  }
135
222
  },
136
223
  {
137
- "name": "runtime-version-from-another-runtime",
138
- "yaml": "name: my-server\nentrypoint: server.py\nruntime: python\nruntime_version: \"22\"\n",
224
+ "name": "python-runtime-version-from-another-runtime",
225
+ "files": {
226
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\nruntime_version = \"22\"\n"
227
+ },
139
228
  "expect": {
140
229
  "ok": false,
141
230
  "reason": "invalid_runtime_version"
142
231
  }
143
232
  },
233
+ {
234
+ "name": "runtime-outside-allowlist",
235
+ "files": {
236
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\nruntime = \"ruby\"\n"
237
+ },
238
+ "expect": {
239
+ "ok": false,
240
+ "reason": "invalid_runtime"
241
+ }
242
+ },
243
+ {
244
+ "name": "runtime-disagrees-with-the-file-it-was-read-from",
245
+ "files": {
246
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\nruntime = \"node\"\n"
247
+ },
248
+ "expect": {
249
+ "ok": false,
250
+ "reason": "invalid_runtime"
251
+ }
252
+ },
253
+ {
254
+ "name": "both-config-files-and-neither-claims-the-runtime",
255
+ "files": {
256
+ "pyproject.toml": "[project]\nname = \"my-server\"\n",
257
+ "package.json": "{\"name\": \"my-server\", \"main\": \"index.js\"}",
258
+ "server.py": ""
259
+ },
260
+ "expect": {
261
+ "ok": false,
262
+ "reason": "invalid_runtime"
263
+ }
264
+ },
265
+ {
266
+ "name": "both-config-files-and-both-claim-the-runtime",
267
+ "files": {
268
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nruntime = \"python\"\n",
269
+ "package.json": "{\"name\": \"my-server\", \"main\": \"index.js\", \"foro\": {\"runtime\": \"node\"}}",
270
+ "server.py": ""
271
+ },
272
+ "expect": {
273
+ "ok": false,
274
+ "reason": "invalid_runtime"
275
+ }
276
+ },
277
+ {
278
+ "name": "both-config-files-python-claims-the-runtime",
279
+ "files": {
280
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nruntime = \"python\"\n",
281
+ "package.json": "{\"name\": \"tooling-only\"}",
282
+ "server.py": ""
283
+ },
284
+ "expect": {
285
+ "ok": true
286
+ }
287
+ },
288
+ {
289
+ "name": "both-config-files-node-claims-the-runtime",
290
+ "files": {
291
+ "pyproject.toml": "[project]\nname = \"my-server\"\n",
292
+ "package.json": "{\"name\": \"my-server\", \"main\": \"index.js\", \"foro\": {\"runtime\": \"node\"}}",
293
+ "server.py": ""
294
+ },
295
+ "expect": {
296
+ "ok": true
297
+ }
298
+ },
299
+ {
300
+ "name": "no-config-file-only-a-legacy-manifest",
301
+ "files": {
302
+ "foro.yaml": "name: my-server\nentrypoint: server.py\n",
303
+ "server.py": ""
304
+ },
305
+ "expect": {
306
+ "ok": false,
307
+ "reason": "missing_manifest"
308
+ }
309
+ },
144
310
  {
145
311
  "name": "unknown-field-stale-python-version",
146
- "yaml": "name: my-server\nentrypoint: server.py\npython_version: \"3.13\"\n",
312
+ "files": {
313
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\npython_version = \"3.13\"\n"
314
+ },
147
315
  "expect": {
148
316
  "ok": false,
149
317
  "reason": "unknown_field"
@@ -151,105 +319,225 @@ export const rawManifestCases = [
151
319
  },
152
320
  {
153
321
  "name": "unknown-field-typo",
154
- "yaml": "name: my-server\nentrypoint: server.py\nentrypoints: server.py\n",
322
+ "files": {
323
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\nentrypoints = \"server.py\"\n"
324
+ },
325
+ "expect": {
326
+ "ok": false,
327
+ "reason": "unknown_field"
328
+ }
329
+ },
330
+ {
331
+ "name": "unknown-field-name-belongs-to-the-package",
332
+ "files": {
333
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\nname = \"other\"\n"
334
+ },
155
335
  "expect": {
156
336
  "ok": false,
157
337
  "reason": "unknown_field"
158
338
  }
159
339
  },
160
340
  {
161
- "name": "bad-port-too-low",
162
- "yaml": "name: my-server\nentrypoint: server.py\nport: 80\n",
341
+ "name": "port-too-low",
342
+ "files": {
343
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\nport = 80\n"
344
+ },
163
345
  "expect": {
164
346
  "ok": false,
165
347
  "reason": "invalid_port"
166
348
  }
167
349
  },
168
350
  {
169
- "name": "bad-port-too-high",
170
- "yaml": "name: my-server\nentrypoint: server.py\nport: 70000\n",
351
+ "name": "port-too-high",
352
+ "files": {
353
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\nport = 70000\n"
354
+ },
171
355
  "expect": {
172
356
  "ok": false,
173
357
  "reason": "invalid_port"
174
358
  }
175
359
  },
176
360
  {
177
- "name": "bad-port-sidecar-reserved",
178
- "yaml": "name: my-server\nentrypoint: server.py\nport: 8001\n",
361
+ "name": "port-sidecar-reserved",
362
+ "files": {
363
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\nport = 8001\n"
364
+ },
179
365
  "expect": {
180
366
  "ok": false,
181
367
  "reason": "invalid_port"
182
368
  }
183
369
  },
184
370
  {
185
- "name": "bad-port-non-integer",
186
- "yaml": "name: my-server\nentrypoint: server.py\nport: \"abc\"\n",
371
+ "name": "port-gate-reserved",
372
+ "files": {
373
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\nport = 8002\n"
374
+ },
187
375
  "expect": {
188
376
  "ok": false,
189
377
  "reason": "invalid_port"
190
378
  }
191
379
  },
192
380
  {
193
- "name": "bad-dependency-manager",
194
- "yaml": "name: my-server\nentrypoint: server.py\ndependency_manager: pipx\n",
381
+ "name": "port-non-integer",
382
+ "files": {
383
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\nport = \"abc\"\n"
384
+ },
195
385
  "expect": {
196
386
  "ok": false,
197
- "reason": "invalid_dependency_manager"
387
+ "reason": "invalid_port"
198
388
  }
199
389
  },
200
390
  {
201
- "name": "invalid-yaml",
202
- "yaml": "name: [unclosed\n",
391
+ "name": "dependency-manager-outside-allowlist",
392
+ "files": {
393
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\ndependency_manager = \"pipx\"\n"
394
+ },
203
395
  "expect": {
204
396
  "ok": false,
205
- "reason": "invalid_yaml"
397
+ "reason": "invalid_dependency_manager"
206
398
  }
207
399
  },
208
400
  {
209
- "name": "invalid-shape-scalar",
210
- "yaml": "just a string\n",
401
+ "name": "node-dependency-manager-on-python",
402
+ "files": {
403
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\ndependency_manager = \"npm\"\n"
404
+ },
211
405
  "expect": {
212
406
  "ok": false,
213
- "reason": "invalid_shape"
407
+ "reason": "invalid_dependency_manager"
214
408
  }
215
409
  },
216
410
  {
217
- "name": "invalid-shape-list",
218
- "yaml": "- one\n- two\n",
411
+ "name": "unparseable-pyproject",
412
+ "files": {
413
+ "pyproject.toml": "[project\nname = \"my-server\"\n"
414
+ },
219
415
  "expect": {
220
416
  "ok": false,
221
417
  "reason": "invalid_shape"
222
418
  }
223
419
  },
224
420
  {
225
- "name": "bad-entrypoint-leading-dash",
226
- "yaml": "name: my-server\nentrypoint: --isolated\n",
421
+ "name": "node-minimal",
422
+ "files": {
423
+ "package.json": "{\"name\": \"my-server\", \"main\": \"dist/index.js\"}"
424
+ },
425
+ "expect": {
426
+ "ok": true
427
+ }
428
+ },
429
+ {
430
+ "name": "node-scoped-name",
431
+ "files": {
432
+ "package.json": "{\"name\": \"@acme/my-server\", \"main\": \"dist/index.js\"}"
433
+ },
434
+ "expect": {
435
+ "ok": true
436
+ }
437
+ },
438
+ {
439
+ "name": "node-bin-map",
440
+ "files": {
441
+ "package.json": "{\"name\": \"my-server\", \"bin\": {\"my-server\": \"dist/cli.js\"}}"
442
+ },
443
+ "expect": {
444
+ "ok": true
445
+ }
446
+ },
447
+ {
448
+ "name": "node-implicit-index",
449
+ "files": {
450
+ "package.json": "{\"name\": \"my-server\"}",
451
+ "index.js": ""
452
+ },
453
+ "expect": {
454
+ "ok": true
455
+ }
456
+ },
457
+ {
458
+ "name": "node-engines-range",
459
+ "files": {
460
+ "package.json": "{\"name\": \"my-server\", \"main\": \"index.js\", \"engines\": {\"node\": \">=22\"}}"
461
+ },
462
+ "expect": {
463
+ "ok": true
464
+ }
465
+ },
466
+ {
467
+ "name": "node-engines-union-of-every-supported-major",
468
+ "files": {
469
+ "package.json": "{\"name\": \"my-server\", \"main\": \"index.js\", \"engines\": {\"node\": \"^22 || ^24\"}}"
470
+ },
471
+ "expect": {
472
+ "ok": true
473
+ }
474
+ },
475
+ {
476
+ "name": "node-engines-union-with-one-unsupported-branch",
477
+ "files": {
478
+ "package.json": "{\"name\": \"my-server\", \"main\": \"index.js\", \"engines\": {\"node\": \"^20 || ^22\"}}"
479
+ },
480
+ "expect": {
481
+ "ok": true
482
+ }
483
+ },
484
+ {
485
+ "name": "node-all-fields",
486
+ "files": {
487
+ "package.json": "{\"name\": \"my-server\", \"main\": \"dist/index.js\", \"foro\": {\"runtime\": \"node\", \"runtime_version\": \"22\", \"port\": 9000, \"dependency_manager\": \"pnpm\"}}"
488
+ },
489
+ "expect": {
490
+ "ok": true
491
+ }
492
+ },
493
+ {
494
+ "name": "node-no-entry-file",
495
+ "files": {
496
+ "package.json": "{\"name\": \"my-server\"}"
497
+ },
227
498
  "expect": {
228
499
  "ok": false,
229
500
  "reason": "invalid_entrypoint"
230
501
  }
231
502
  },
232
503
  {
233
- "name": "bad-entrypoint-leading-dash-nested",
234
- "yaml": "name: my-server\nentrypoint: src/-rf.py\n",
504
+ "name": "node-engines-excludes-every-supported-version",
505
+ "files": {
506
+ "package.json": "{\"name\": \"my-server\", \"main\": \"index.js\", \"engines\": {\"node\": \"^20\"}}"
507
+ },
235
508
  "expect": {
236
509
  "ok": false,
237
- "reason": "invalid_entrypoint"
510
+ "reason": "invalid_runtime_version"
238
511
  }
239
512
  },
240
513
  {
241
- "name": "bad-build-path-leading-dash",
242
- "yaml": "name: my-server\nentrypoint: server.py\nbuild_path: -rf\n",
514
+ "name": "node-runtime-version-from-another-runtime",
515
+ "files": {
516
+ "package.json": "{\"name\": \"my-server\", \"main\": \"index.js\", \"foro\": {\"runtime_version\": \"3.12\"}}"
517
+ },
243
518
  "expect": {
244
519
  "ok": false,
245
- "reason": "invalid_build_path"
520
+ "reason": "invalid_runtime_version"
246
521
  }
247
522
  },
248
523
  {
249
- "name": "hyphen-inside-a-path-segment-is-fine",
250
- "yaml": "name: my-server\nentrypoint: my-tools/mcp-server.py\n",
524
+ "name": "python-dependency-manager-on-node",
525
+ "files": {
526
+ "package.json": "{\"name\": \"my-server\", \"main\": \"index.js\", \"foro\": {\"dependency_manager\": \"uv\"}}"
527
+ },
251
528
  "expect": {
252
- "ok": true
529
+ "ok": false,
530
+ "reason": "invalid_dependency_manager"
531
+ }
532
+ },
533
+ {
534
+ "name": "unparseable-package-json",
535
+ "files": {
536
+ "package.json": "{\"name\": "
537
+ },
538
+ "expect": {
539
+ "ok": false,
540
+ "reason": "invalid_shape"
253
541
  }
254
542
  }
255
543
  ];
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The shared `foro.yaml` validation table.
2
+ * The shared project-config validation table.
3
3
  *
4
4
  * foro's `_manifest.py` is a port of foro-sh/platform's
5
5
  * `apps/api/src/services/manifest.ts`, and the two can never be allowed to
@@ -19,8 +19,10 @@ export type ManifestRejectionReason = 'missing_manifest' | 'unsupported_language
19
19
  export interface ManifestCase {
20
20
  /** Stable identifier, unique across the table - use it as the test name. */
21
21
  readonly name: string;
22
- /** Verbatim `foro.yaml` contents to validate. */
23
- readonly yaml: string;
22
+ /** Files to write into an empty directory before validating it, keyed by
23
+ * repo-relative path. Always the config file under test, plus whatever the
24
+ * entry-file inference has to find on disk. */
25
+ readonly files: Readonly<Record<string, string>>;
24
26
  readonly expect: {
25
27
  readonly ok: true;
26
28
  } | {
@@ -1 +1 @@
1
- {"version":3,"file":"manifest-cases.d.ts","sourceRoot":"","sources":["../src/manifest-cases.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH;;kCAEkC;AAClC,MAAM,MAAM,uBAAuB,GAC/B,kBAAkB,GAClB,sBAAsB,GACtB,cAAc,GACd,eAAe,GACf,cAAc,GACd,oBAAoB,GACpB,oBAAoB,GACpB,iBAAiB,GACjB,yBAAyB,GACzB,cAAc,GACd,4BAA4B,GAC5B,qBAAqB,GACrB,eAAe,CAAA;AAEnB,MAAM,WAAW,YAAY;IAC3B,4EAA4E;IAC5E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,iDAAiD;IACjD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,MAAM,EACX;QAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAA;KAAE,GACrB;QAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,uBAAuB,CAAA;KAAE,CAAA;CACrE;AAED,eAAO,MAAM,aAAa,EAAE,SAAS,YAAY,EAAqB,CAAA"}
1
+ {"version":3,"file":"manifest-cases.d.ts","sourceRoot":"","sources":["../src/manifest-cases.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH;;kCAEkC;AAClC,MAAM,MAAM,uBAAuB,GAC/B,kBAAkB,GAClB,sBAAsB,GACtB,cAAc,GACd,eAAe,GACf,cAAc,GACd,oBAAoB,GACpB,oBAAoB,GACpB,iBAAiB,GACjB,yBAAyB,GACzB,cAAc,GACd,4BAA4B,GAC5B,qBAAqB,GACrB,eAAe,CAAA;AAEnB,MAAM,WAAW,YAAY;IAC3B,4EAA4E;IAC5E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB;;oDAEgD;IAChD,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IAChD,QAAQ,CAAC,MAAM,EACX;QAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAA;KAAE,GACrB;QAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,uBAAuB,CAAA;KAAE,CAAA;CACrE;AAED,eAAO,MAAM,aAAa,EAAE,SAAS,YAAY,EAAqB,CAAA"}
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The shared `foro.yaml` validation table.
2
+ * The shared project-config validation table.
3
3
  *
4
4
  * foro's `_manifest.py` is a port of foro-sh/platform's
5
5
  * `apps/api/src/services/manifest.ts`, and the two can never be allowed to
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foro-sh/foro",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "TypeScript SDK for Foro",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",