@foro-sh/foro 0.9.3 → 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,193 +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
+ };
7
+ readonly expect: {
8
+ readonly ok: true;
9
+ };
10
+ }, {
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
+ };
16
+ readonly expect: {
17
+ readonly ok: true;
18
+ };
19
+ }, {
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
+ };
4
24
  readonly expect: {
5
25
  readonly ok: true;
6
26
  };
7
27
  }, {
8
- readonly name: "nested-entrypoint";
9
- readonly yaml: "name: my-server\nentrypoint: src/server.py\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
+ };
10
32
  readonly expect: {
11
33
  readonly ok: true;
12
34
  };
13
35
  }, {
14
- readonly name: "all-fields";
15
- readonly yaml: "name: my-server\nentrypoint: server.py\nbuild_path: app\npython_version: \"3.11\"\nport: 9000\ndependency_manager: poetry\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
+ };
16
41
  readonly expect: {
17
42
  readonly ok: true;
18
43
  };
19
44
  }, {
20
- readonly name: "unquoted-python-version";
21
- readonly yaml: "name: my-server\nentrypoint: server.py\npython_version: 3.12\n";
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
+ };
22
50
  readonly expect: {
23
51
  readonly ok: true;
24
52
  };
25
53
  }, {
26
- readonly name: "bad-name-uppercase";
27
- readonly yaml: "name: My-Server\nentrypoint: server.py\n";
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";
31
62
  };
32
63
  }, {
33
- readonly name: "bad-name-too-short";
34
- readonly yaml: "name: ab\nentrypoint: server.py\n";
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;
70
+ };
71
+ }, {
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
+ };
139
+ readonly expect: {
140
+ readonly ok: false;
141
+ readonly reason: "invalid_build_path";
142
+ };
143
+ }, {
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
+ };
77
148
  readonly expect: {
78
149
  readonly ok: false;
79
150
  readonly reason: "invalid_build_path";
80
151
  };
81
152
  }, {
82
- readonly name: "bad-build-path-shell-metachar";
83
- readonly yaml: "name: my-server\nentrypoint: server.py\nbuild_path: \". && whoami\"\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
+ };
84
157
  readonly expect: {
85
158
  readonly ok: false;
86
159
  readonly reason: "invalid_build_path";
87
160
  };
88
161
  }, {
89
- readonly name: "bad-build-path-space";
90
- readonly yaml: "name: my-server\nentrypoint: server.py\nbuild_path: \"my services\"\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
+ };
91
166
  readonly expect: {
92
167
  readonly ok: false;
93
168
  readonly reason: "invalid_build_path";
94
169
  };
95
170
  }, {
96
- readonly name: "bad-build-path-newline";
97
- readonly yaml: "name: my-server\nentrypoint: server.py\nbuild_path: \"app\\nUSER root\"\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
+ };
98
175
  readonly expect: {
99
176
  readonly ok: false;
100
177
  readonly reason: "invalid_build_path";
101
178
  };
102
179
  }, {
103
- readonly name: "bad-python-version";
104
- readonly yaml: "name: my-server\nentrypoint: server.py\npython_version: \"3.9\"\n";
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
+ };
184
+ readonly expect: {
185
+ readonly ok: false;
186
+ readonly reason: "invalid_runtime_version";
187
+ };
188
+ }, {
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
+ };
194
+ readonly expect: {
195
+ readonly ok: false;
196
+ readonly reason: "invalid_runtime_version";
197
+ };
198
+ }, {
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
+ };
203
+ readonly expect: {
204
+ readonly ok: false;
205
+ readonly reason: "invalid_runtime_version";
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
+ };
277
+ }, {
278
+ readonly name: "unknown-field-stale-python-version";
279
+ readonly files: {
280
+ readonly "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\npython_version = \"3.13\"\n";
281
+ };
282
+ readonly expect: {
283
+ readonly ok: false;
284
+ readonly reason: "unknown_field";
285
+ };
286
+ }, {
287
+ readonly name: "unknown-field-typo";
288
+ readonly files: {
289
+ readonly "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\nentrypoints = \"server.py\"\n";
290
+ };
105
291
  readonly expect: {
106
292
  readonly ok: false;
107
- readonly reason: "invalid_python_version";
293
+ readonly reason: "unknown_field";
108
294
  };
109
295
  }, {
110
- readonly name: "bad-port-too-low";
111
- 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
+ };
112
309
  readonly expect: {
113
310
  readonly ok: false;
114
311
  readonly reason: "invalid_port";
115
312
  };
116
313
  }, {
117
- readonly name: "bad-port-too-high";
118
- 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
+ };
119
318
  readonly expect: {
120
319
  readonly ok: false;
121
320
  readonly reason: "invalid_port";
122
321
  };
123
322
  }, {
124
- readonly name: "bad-port-sidecar-reserved";
125
- 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
+ };
126
327
  readonly expect: {
127
328
  readonly ok: false;
128
329
  readonly reason: "invalid_port";
129
330
  };
130
331
  }, {
131
- readonly name: "bad-port-non-integer";
132
- 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
+ };
133
336
  readonly expect: {
134
337
  readonly ok: false;
135
338
  readonly reason: "invalid_port";
136
339
  };
137
340
  }, {
138
- readonly name: "bad-dependency-manager";
139
- 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
+ };
140
345
  readonly expect: {
141
346
  readonly ok: false;
142
- readonly reason: "invalid_dependency_manager";
347
+ readonly reason: "invalid_port";
143
348
  };
144
349
  }, {
145
- readonly name: "invalid-yaml";
146
- 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
+ };
147
354
  readonly expect: {
148
355
  readonly ok: false;
149
- readonly reason: "invalid_yaml";
356
+ readonly reason: "invalid_dependency_manager";
150
357
  };
151
358
  }, {
152
- readonly name: "invalid-shape-scalar";
153
- 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
+ };
154
363
  readonly expect: {
155
364
  readonly ok: false;
156
- readonly reason: "invalid_shape";
365
+ readonly reason: "invalid_dependency_manager";
157
366
  };
158
367
  }, {
159
- readonly name: "invalid-shape-list";
160
- readonly yaml: "- one\n- two\n";
368
+ readonly name: "unparseable-pyproject";
369
+ readonly files: {
370
+ readonly "pyproject.toml": "[project\nname = \"my-server\"\n";
371
+ };
161
372
  readonly expect: {
162
373
  readonly ok: false;
163
374
  readonly reason: "invalid_shape";
164
375
  };
165
376
  }, {
166
- readonly name: "bad-entrypoint-leading-dash";
167
- 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
+ };
168
446
  readonly expect: {
169
447
  readonly ok: false;
170
448
  readonly reason: "invalid_entrypoint";
171
449
  };
172
450
  }, {
173
- readonly name: "bad-entrypoint-leading-dash-nested";
174
- 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
+ };
175
455
  readonly expect: {
176
456
  readonly ok: false;
177
- readonly reason: "invalid_entrypoint";
457
+ readonly reason: "invalid_runtime_version";
178
458
  };
179
459
  }, {
180
- readonly name: "bad-build-path-leading-dash";
181
- 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
+ };
182
464
  readonly expect: {
183
465
  readonly ok: false;
184
- readonly reason: "invalid_build_path";
466
+ readonly reason: "invalid_runtime_version";
185
467
  };
186
468
  }, {
187
- readonly name: "hyphen-inside-a-path-segment-is-fine";
188
- 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
+ };
189
473
  readonly expect: {
190
- 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";
191
485
  };
192
486
  }];
193
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4NnB,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,222 +2,542 @@
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\npython_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-python-version",
27
- "yaml": "name: my-server\nentrypoint: server.py\npython_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
+ },
157
+ "expect": {
158
+ "ok": false,
159
+ "reason": "invalid_build_path"
160
+ }
161
+ },
162
+ {
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
+ },
91
167
  "expect": {
92
168
  "ok": false,
93
169
  "reason": "invalid_build_path"
94
170
  }
95
171
  },
96
172
  {
97
- "name": "bad-build-path-shell-metachar",
98
- "yaml": "name: my-server\nentrypoint: server.py\nbuild_path: \". && whoami\"\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
+ },
99
177
  "expect": {
100
178
  "ok": false,
101
179
  "reason": "invalid_build_path"
102
180
  }
103
181
  },
104
182
  {
105
- "name": "bad-build-path-space",
106
- "yaml": "name: my-server\nentrypoint: server.py\nbuild_path: \"my services\"\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
+ },
107
187
  "expect": {
108
188
  "ok": false,
109
189
  "reason": "invalid_build_path"
110
190
  }
111
191
  },
112
192
  {
113
- "name": "bad-build-path-newline",
114
- "yaml": "name: my-server\nentrypoint: server.py\nbuild_path: \"app\\nUSER root\"\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
+ },
115
197
  "expect": {
116
198
  "ok": false,
117
199
  "reason": "invalid_build_path"
118
200
  }
119
201
  },
120
202
  {
121
- "name": "bad-python-version",
122
- "yaml": "name: my-server\nentrypoint: server.py\npython_version: \"3.9\"\n",
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
+ },
207
+ "expect": {
208
+ "ok": false,
209
+ "reason": "invalid_runtime_version"
210
+ }
211
+ },
212
+ {
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
+ },
218
+ "expect": {
219
+ "ok": false,
220
+ "reason": "invalid_runtime_version"
221
+ }
222
+ },
223
+ {
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
+ },
228
+ "expect": {
229
+ "ok": false,
230
+ "reason": "invalid_runtime_version"
231
+ }
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
+ },
310
+ {
311
+ "name": "unknown-field-stale-python-version",
312
+ "files": {
313
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\npython_version = \"3.13\"\n"
314
+ },
315
+ "expect": {
316
+ "ok": false,
317
+ "reason": "unknown_field"
318
+ }
319
+ },
320
+ {
321
+ "name": "unknown-field-typo",
322
+ "files": {
323
+ "pyproject.toml": "[project]\nname = \"my-server\"\n\n[tool.foro]\nentrypoint = \"server.py\"\nentrypoints = \"server.py\"\n"
324
+ },
123
325
  "expect": {
124
326
  "ok": false,
125
- "reason": "invalid_python_version"
327
+ "reason": "unknown_field"
126
328
  }
127
329
  },
128
330
  {
129
- "name": "bad-port-too-low",
130
- "yaml": "name: my-server\nentrypoint: server.py\nport: 80\n",
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
+ },
335
+ "expect": {
336
+ "ok": false,
337
+ "reason": "unknown_field"
338
+ }
339
+ },
340
+ {
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
+ },
131
345
  "expect": {
132
346
  "ok": false,
133
347
  "reason": "invalid_port"
134
348
  }
135
349
  },
136
350
  {
137
- "name": "bad-port-too-high",
138
- "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
+ },
139
355
  "expect": {
140
356
  "ok": false,
141
357
  "reason": "invalid_port"
142
358
  }
143
359
  },
144
360
  {
145
- "name": "bad-port-sidecar-reserved",
146
- "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
+ },
147
365
  "expect": {
148
366
  "ok": false,
149
367
  "reason": "invalid_port"
150
368
  }
151
369
  },
152
370
  {
153
- "name": "bad-port-non-integer",
154
- "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
+ },
155
375
  "expect": {
156
376
  "ok": false,
157
377
  "reason": "invalid_port"
158
378
  }
159
379
  },
160
380
  {
161
- "name": "bad-dependency-manager",
162
- "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
+ },
163
385
  "expect": {
164
386
  "ok": false,
165
- "reason": "invalid_dependency_manager"
387
+ "reason": "invalid_port"
166
388
  }
167
389
  },
168
390
  {
169
- "name": "invalid-yaml",
170
- "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
+ },
171
395
  "expect": {
172
396
  "ok": false,
173
- "reason": "invalid_yaml"
397
+ "reason": "invalid_dependency_manager"
174
398
  }
175
399
  },
176
400
  {
177
- "name": "invalid-shape-scalar",
178
- "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
+ },
179
405
  "expect": {
180
406
  "ok": false,
181
- "reason": "invalid_shape"
407
+ "reason": "invalid_dependency_manager"
182
408
  }
183
409
  },
184
410
  {
185
- "name": "invalid-shape-list",
186
- "yaml": "- one\n- two\n",
411
+ "name": "unparseable-pyproject",
412
+ "files": {
413
+ "pyproject.toml": "[project\nname = \"my-server\"\n"
414
+ },
187
415
  "expect": {
188
416
  "ok": false,
189
417
  "reason": "invalid_shape"
190
418
  }
191
419
  },
192
420
  {
193
- "name": "bad-entrypoint-leading-dash",
194
- "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
+ },
195
498
  "expect": {
196
499
  "ok": false,
197
500
  "reason": "invalid_entrypoint"
198
501
  }
199
502
  },
200
503
  {
201
- "name": "bad-entrypoint-leading-dash-nested",
202
- "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
+ },
203
508
  "expect": {
204
509
  "ok": false,
205
- "reason": "invalid_entrypoint"
510
+ "reason": "invalid_runtime_version"
206
511
  }
207
512
  },
208
513
  {
209
- "name": "bad-build-path-leading-dash",
210
- "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
+ },
211
518
  "expect": {
212
519
  "ok": false,
213
- "reason": "invalid_build_path"
520
+ "reason": "invalid_runtime_version"
214
521
  }
215
522
  },
216
523
  {
217
- "name": "hyphen-inside-a-path-segment-is-fine",
218
- "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
+ },
219
528
  "expect": {
220
- "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"
221
541
  }
222
542
  }
223
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
@@ -15,12 +15,14 @@
15
15
  /** Mirrors `ManifestRejectionReason` in foro-sh/platform's `@foro/types`.
16
16
  * Kept in sync deliberately: it is what makes a reason added on one side a
17
17
  * compile error on the other. */
18
- export type ManifestRejectionReason = 'missing_manifest' | 'unsupported_language' | 'invalid_yaml' | 'invalid_shape' | 'invalid_name' | 'invalid_entrypoint' | 'invalid_build_path' | 'invalid_python_version' | 'invalid_port' | 'invalid_dependency_manager' | 'unsupported_project';
18
+ export type ManifestRejectionReason = 'missing_manifest' | 'unsupported_language' | 'invalid_yaml' | 'invalid_shape' | 'invalid_name' | 'invalid_entrypoint' | 'invalid_build_path' | 'invalid_runtime' | 'invalid_runtime_version' | 'invalid_port' | 'invalid_dependency_manager' | 'unsupported_project' | 'unknown_field';
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,wBAAwB,GACxB,cAAc,GACd,4BAA4B,GAC5B,qBAAqB,CAAA;AAEzB,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.9.3",
3
+ "version": "0.11.0",
4
4
  "description": "TypeScript SDK for Foro",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",