@wardby/cli 0.5.2 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/coding/local-git.js +9 -1
- package/dist/coding/registry/pypi.d.ts +25 -1
- package/dist/coding/registry/pypi.js +140 -16
- package/dist/coding/registry/types.d.ts +17 -3
- package/dist/config/providers.d.ts +3 -2
- package/dist/core/http-runtime.js +13 -6
- package/dist/help-index.json +207 -15
- package/dist/mcp/auth/ownership.d.ts +1 -1
- package/dist/mcp/tools/agents.js +1 -1
- package/dist/providers/coding-proxy/mock-upstream.d.ts +33 -0
- package/dist/providers/coding-proxy/mock-upstream.js +116 -0
- package/dist/providers/coding-proxy/registry/prisma-store.d.ts +3 -0
- package/dist/providers/coding-proxy/registry/prisma-store.js +17 -0
- package/dist/providers/coding-proxy/registry/service.d.ts +4 -0
- package/dist/providers/coding-proxy/registry/service.js +20 -3
- package/dist/providers/coding-proxy/registry/store.d.ts +18 -1
- package/dist/providers/coding-proxy/registry/store.js +33 -0
- package/dist/providers/coding-proxy/runtime.js +12 -1
- package/dist/providers/coding-proxy/types.d.ts +2 -0
- package/dist/providers/jobs/kubernetes-isolation.d.ts +13 -0
- package/dist/providers/jobs/kubernetes-isolation.js +52 -5
- package/dist/providers/jobs/kubernetes.d.ts +20 -6
- package/dist/providers/jobs/kubernetes.js +43 -32
- package/dist/providers/llm/openai.js +0 -1
- package/dist/quickstart/coding-db.js +14 -2
- package/dist/quickstart/coding-doctor.d.ts +3 -0
- package/dist/quickstart/coding-doctor.js +3 -0
- package/dist/quickstart/coding-images.d.ts +9 -0
- package/dist/quickstart/coding-images.js +39 -0
- package/dist/quickstart/coding-seed.d.ts +13 -1
- package/dist/quickstart/coding-seed.js +25 -7
- package/dist/quickstart/coding.d.ts +2 -0
- package/dist/quickstart/coding.js +75 -3
- package/dist/quickstart/config.d.ts +2 -0
- package/dist/quickstart/config.js +4 -0
- package/dist/quickstart/images.d.ts +11 -0
- package/dist/quickstart/images.js +74 -12
- package/dist/quickstart/index.d.ts +0 -2
- package/dist/quickstart/index.js +10 -14
- package/dist/quickstart/manifests.d.ts +56 -0
- package/dist/quickstart/manifests.js +523 -0
- package/dist/quickstart/python-detect.d.ts +10 -0
- package/dist/quickstart/python-detect.js +32 -0
- package/dist/quickstart/repo-packages.d.ts +10 -0
- package/dist/quickstart/repo-packages.js +132 -0
- package/dist/quickstart/reviewer-prompt.d.ts +12 -0
- package/dist/quickstart/reviewer-prompt.js +84 -0
- package/dist/quickstart/scenarios.d.ts +19 -0
- package/dist/quickstart/scenarios.js +28 -0
- package/dist/quickstart/sync-reviewer-prompt.d.ts +1 -0
- package/dist/quickstart/sync-reviewer-prompt.js +10 -0
- package/dist/quickstart-images.json +1 -1
- package/dist/viewer/api-schema.d.ts +4 -4
- package/docs/coding-agent-setup.md +17 -0
- package/docs/coding-packages.md +101 -3
- package/docs/coding-worker-byo-images.md +47 -2
- package/docs/coding-worker-isolation.md +20 -9
- package/docs/getting-started.md +139 -11
- package/docs/knowledge.md +11 -0
- package/help/architecture-agent.md +136 -0
- package/help/build-worker-image.md +198 -0
- package/help/coding-packages.md +31 -2
- package/help/creating-agents.md +14 -0
- package/help/deploy-gke.md +29 -5
- package/help/deployment-targets.md +3 -3
- package/help/getting-started.md +19 -0
- package/help/local-repositories.md +36 -5
- package/package.json +3 -2
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure readers for a repository's declared dependencies, used by the
|
|
3
|
+
* quickstart to offer them as local-builder's package allowlist:
|
|
4
|
+
* `pyproject.toml`, `requirements*.txt` and `package.json`. Each returns bare
|
|
5
|
+
* top-level names as written (a Python requirement keeps the extras it names,
|
|
6
|
+
* also as written: `psycopg[binary]`); repo-packages.ts validates, normalizes,
|
|
7
|
+
* de-duplicates and caps them.
|
|
8
|
+
*
|
|
9
|
+
* pyproject.toml is read by a deliberately narrow TOML scanner (no new
|
|
10
|
+
* dependency): it understands tables, arrays of tables, dotted and quoted
|
|
11
|
+
* keys, all four string forms, arrays and inline tables, and treats every
|
|
12
|
+
* other scalar (numbers, booleans, dates) as opaque. Anything it cannot scan
|
|
13
|
+
* throws, and the caller skips the file with a warning.
|
|
14
|
+
*/
|
|
15
|
+
const PEP508_NAME = /^([A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?)/;
|
|
16
|
+
const ARCHIVE = /\.(?:whl|tar\.gz|tgz|tar\.bz2|tar\.xz|zip)$/i;
|
|
17
|
+
/**
|
|
18
|
+
* The project name of a PEP 508 requirement ("requests[socks]>=2; python_version<'4'"),
|
|
19
|
+
* or null for anything that is not a plain registry requirement: a direct URL
|
|
20
|
+
* reference (`name @ url`), a path, a URL, or an archive file.
|
|
21
|
+
*/
|
|
22
|
+
export function requirementName(spec) {
|
|
23
|
+
const value = spec.trim();
|
|
24
|
+
if (ARCHIVE.test(value.split(/[\s;]/)[0]))
|
|
25
|
+
return null;
|
|
26
|
+
const match = value.match(PEP508_NAME);
|
|
27
|
+
if (!match)
|
|
28
|
+
return null;
|
|
29
|
+
const rest = value.slice(match[1].length).trimStart();
|
|
30
|
+
return rest === "" || /^[[(;<>=!~,]/.test(rest) ? match[1] : null;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A PEP 508 requirement's name with the extras it names, as written
|
|
34
|
+
* ("psycopg[binary]" from "psycopg[binary]>=3.2"), or just the name when it
|
|
35
|
+
* names none; null when requirementName is. The extras are not validated here.
|
|
36
|
+
*/
|
|
37
|
+
export function requirementSpec(spec) {
|
|
38
|
+
const name = requirementName(spec);
|
|
39
|
+
if (name === null)
|
|
40
|
+
return null;
|
|
41
|
+
const extras = spec
|
|
42
|
+
.trim()
|
|
43
|
+
.slice(name.length)
|
|
44
|
+
.trimStart()
|
|
45
|
+
.match(/^\[([^\]]*)\]/);
|
|
46
|
+
return extras ? `${name}[${extras[1].trim()}]` : name;
|
|
47
|
+
}
|
|
48
|
+
/** Names from a pip requirements file; options (`-r`, `-c`, `-e`, `--…`), URLs and paths are skipped. */
|
|
49
|
+
export function requirementsNames(text) {
|
|
50
|
+
const names = [];
|
|
51
|
+
// pip joins backslash continuations first, then drops comments.
|
|
52
|
+
const logical = text.replace(/\\\r?\n/g, " ").split(/\r?\n/);
|
|
53
|
+
for (const raw of logical) {
|
|
54
|
+
const line = raw
|
|
55
|
+
.replace(/(?:^|\s)#.*$/, "")
|
|
56
|
+
.replace(/\s+--[A-Za-z].*$/, "")
|
|
57
|
+
.trim();
|
|
58
|
+
if (!line || line.startsWith("-"))
|
|
59
|
+
continue;
|
|
60
|
+
const name = requirementSpec(line);
|
|
61
|
+
if (name)
|
|
62
|
+
names.push(name);
|
|
63
|
+
}
|
|
64
|
+
return names;
|
|
65
|
+
}
|
|
66
|
+
const NPM_DEPENDENCY_FIELDS = ["dependencies", "devDependencies", "optionalDependencies"];
|
|
67
|
+
function isPlainObject(value) {
|
|
68
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Names from package.json's dependencies, devDependencies and
|
|
72
|
+
* optionalDependencies (peer dependencies are the consumer's to install).
|
|
73
|
+
* An `npm:` alias names the real package; file, link, workspace, git, GitHub
|
|
74
|
+
* and URL specs are not registry packages and are skipped.
|
|
75
|
+
*/
|
|
76
|
+
export function packageJsonNames(text) {
|
|
77
|
+
let doc;
|
|
78
|
+
try {
|
|
79
|
+
doc = JSON.parse(text);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// The parser's message can quote the file; keep the note short.
|
|
83
|
+
throw new Error("not valid JSON");
|
|
84
|
+
}
|
|
85
|
+
if (!isPlainObject(doc))
|
|
86
|
+
throw new Error("package.json is not a JSON object");
|
|
87
|
+
const names = [];
|
|
88
|
+
for (const field of NPM_DEPENDENCY_FIELDS) {
|
|
89
|
+
const deps = doc[field];
|
|
90
|
+
if (!isPlainObject(deps))
|
|
91
|
+
continue;
|
|
92
|
+
for (const [name, spec] of Object.entries(deps)) {
|
|
93
|
+
if (typeof spec !== "string")
|
|
94
|
+
continue;
|
|
95
|
+
const value = spec.trim();
|
|
96
|
+
if (value.startsWith("npm:")) {
|
|
97
|
+
const target = value.slice("npm:".length);
|
|
98
|
+
const at = target.indexOf("@", 1);
|
|
99
|
+
names.push(at > 0 ? target.slice(0, at) : target);
|
|
100
|
+
}
|
|
101
|
+
else if (!/[:/]/.test(value)) {
|
|
102
|
+
names.push(name);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return names;
|
|
107
|
+
}
|
|
108
|
+
/** package.json's own `name`, when it is a string: the quickstart never offers
|
|
109
|
+
* the repository's own package for its allowlist (dependency confusion). */
|
|
110
|
+
export function packageJsonOwnNames(text) {
|
|
111
|
+
let doc;
|
|
112
|
+
try {
|
|
113
|
+
doc = JSON.parse(text);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
return isPlainObject(doc) && typeof doc.name === "string" ? [doc.name] : [];
|
|
119
|
+
}
|
|
120
|
+
// ---------------------------------------------------------------- TOML scan
|
|
121
|
+
class Opaque {
|
|
122
|
+
raw;
|
|
123
|
+
constructor(raw) {
|
|
124
|
+
this.raw = raw;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
class Table {
|
|
128
|
+
entries = new Map();
|
|
129
|
+
}
|
|
130
|
+
class TomlScanError extends Error {
|
|
131
|
+
}
|
|
132
|
+
const BARE_KEY = /^[A-Za-z0-9_-]+/;
|
|
133
|
+
const SCALAR = /^[^\s,\]}#[{="']+/;
|
|
134
|
+
const ESCAPES = { b: "\b", t: "\t", n: "\n", f: "\f", r: "\r", '"': '"', "\\": "\\" };
|
|
135
|
+
class Scanner {
|
|
136
|
+
text;
|
|
137
|
+
pos = 0;
|
|
138
|
+
root = new Table();
|
|
139
|
+
constructor(text) {
|
|
140
|
+
this.text = text;
|
|
141
|
+
}
|
|
142
|
+
fail(message) {
|
|
143
|
+
const line = this.text.slice(0, this.pos).split("\n").length;
|
|
144
|
+
throw new TomlScanError(`line ${line}: ${message}`);
|
|
145
|
+
}
|
|
146
|
+
peek(length = 1) {
|
|
147
|
+
return this.text.slice(this.pos, this.pos + length);
|
|
148
|
+
}
|
|
149
|
+
/** Spaces and tabs only. */
|
|
150
|
+
skipSpaces() {
|
|
151
|
+
while (this.peek() === " " || this.peek() === "\t")
|
|
152
|
+
this.pos++;
|
|
153
|
+
}
|
|
154
|
+
skipComment() {
|
|
155
|
+
if (this.peek() !== "#")
|
|
156
|
+
return;
|
|
157
|
+
while (this.pos < this.text.length && this.peek() !== "\n")
|
|
158
|
+
this.pos++;
|
|
159
|
+
}
|
|
160
|
+
/** Whitespace, newlines and comments (inside arrays and between statements). */
|
|
161
|
+
skipBlank() {
|
|
162
|
+
for (;;) {
|
|
163
|
+
const start = this.pos;
|
|
164
|
+
while (/^[ \t\r\n]$/.test(this.peek()))
|
|
165
|
+
this.pos++;
|
|
166
|
+
this.skipComment();
|
|
167
|
+
if (this.pos === start)
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
endOfLine() {
|
|
172
|
+
this.skipSpaces();
|
|
173
|
+
this.skipComment();
|
|
174
|
+
if (this.peek() === "\r")
|
|
175
|
+
this.pos++;
|
|
176
|
+
if (this.pos < this.text.length && this.peek() !== "\n")
|
|
177
|
+
this.fail("unexpected text after a value");
|
|
178
|
+
}
|
|
179
|
+
scan() {
|
|
180
|
+
let current = this.root;
|
|
181
|
+
for (;;) {
|
|
182
|
+
this.skipBlank();
|
|
183
|
+
if (this.pos >= this.text.length)
|
|
184
|
+
return this.root;
|
|
185
|
+
if (this.peek(2) === "[[") {
|
|
186
|
+
this.pos += 2;
|
|
187
|
+
const path = this.keyPath();
|
|
188
|
+
if (this.peek(2) !== "]]")
|
|
189
|
+
this.fail("expected ]]");
|
|
190
|
+
this.pos += 2;
|
|
191
|
+
const parent = this.tableAt(this.root, path.slice(0, -1));
|
|
192
|
+
const last = path[path.length - 1];
|
|
193
|
+
const existing = parent.entries.get(last);
|
|
194
|
+
const list = existing === undefined ? [] : existing;
|
|
195
|
+
if (!Array.isArray(list))
|
|
196
|
+
this.fail(`${last} is not an array of tables`);
|
|
197
|
+
const table = new Table();
|
|
198
|
+
list.push(table);
|
|
199
|
+
parent.entries.set(last, list);
|
|
200
|
+
current = table;
|
|
201
|
+
}
|
|
202
|
+
else if (this.peek() === "[") {
|
|
203
|
+
this.pos++;
|
|
204
|
+
const path = this.keyPath();
|
|
205
|
+
if (this.peek() !== "]")
|
|
206
|
+
this.fail("expected ]");
|
|
207
|
+
this.pos++;
|
|
208
|
+
current = this.tableAt(this.root, path);
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
this.assignment(current);
|
|
212
|
+
}
|
|
213
|
+
this.endOfLine();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
assignment(table) {
|
|
217
|
+
const path = this.keyPath();
|
|
218
|
+
if (this.peek() !== "=")
|
|
219
|
+
this.fail("expected =");
|
|
220
|
+
this.pos++;
|
|
221
|
+
this.skipSpaces();
|
|
222
|
+
const value = this.value();
|
|
223
|
+
const parent = this.tableAt(table, path.slice(0, -1));
|
|
224
|
+
const last = path[path.length - 1];
|
|
225
|
+
if (parent.entries.has(last))
|
|
226
|
+
this.fail(`duplicate key ${last}`);
|
|
227
|
+
parent.entries.set(last, value);
|
|
228
|
+
}
|
|
229
|
+
/** The table at `path` under `table`, created as needed; an array of tables resolves to its last element. */
|
|
230
|
+
tableAt(table, path) {
|
|
231
|
+
let current = table;
|
|
232
|
+
for (const key of path) {
|
|
233
|
+
let next = current.entries.get(key);
|
|
234
|
+
if (next === undefined) {
|
|
235
|
+
next = new Table();
|
|
236
|
+
current.entries.set(key, next);
|
|
237
|
+
}
|
|
238
|
+
if (Array.isArray(next))
|
|
239
|
+
next = next[next.length - 1];
|
|
240
|
+
if (!(next instanceof Table))
|
|
241
|
+
this.fail(`${key} is not a table`);
|
|
242
|
+
current = next;
|
|
243
|
+
}
|
|
244
|
+
return current;
|
|
245
|
+
}
|
|
246
|
+
keyPath() {
|
|
247
|
+
const path = [];
|
|
248
|
+
for (;;) {
|
|
249
|
+
this.skipSpaces();
|
|
250
|
+
path.push(this.key());
|
|
251
|
+
this.skipSpaces();
|
|
252
|
+
if (this.peek() !== ".")
|
|
253
|
+
return path;
|
|
254
|
+
this.pos++;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
key() {
|
|
258
|
+
if (this.peek() === '"')
|
|
259
|
+
return this.basicString();
|
|
260
|
+
if (this.peek() === "'")
|
|
261
|
+
return this.literalString();
|
|
262
|
+
const match = this.text.slice(this.pos).match(BARE_KEY);
|
|
263
|
+
if (!match)
|
|
264
|
+
this.fail("expected a key");
|
|
265
|
+
this.pos += match[0].length;
|
|
266
|
+
return match[0];
|
|
267
|
+
}
|
|
268
|
+
value() {
|
|
269
|
+
if (this.peek(3) === '"""')
|
|
270
|
+
return this.multilineBasicString();
|
|
271
|
+
if (this.peek(3) === "'''")
|
|
272
|
+
return this.multilineLiteralString();
|
|
273
|
+
if (this.peek() === '"')
|
|
274
|
+
return this.basicString();
|
|
275
|
+
if (this.peek() === "'")
|
|
276
|
+
return this.literalString();
|
|
277
|
+
if (this.peek() === "[")
|
|
278
|
+
return this.array();
|
|
279
|
+
if (this.peek() === "{")
|
|
280
|
+
return this.inlineTable();
|
|
281
|
+
const match = this.text.slice(this.pos).match(SCALAR);
|
|
282
|
+
if (!match)
|
|
283
|
+
this.fail("expected a value");
|
|
284
|
+
this.pos += match[0].length;
|
|
285
|
+
let raw = match[0];
|
|
286
|
+
// A local date-time may separate date and time with a space.
|
|
287
|
+
const time = /^\d{4}-\d{2}-\d{2}$/.test(raw) ? this.text.slice(this.pos).match(/^ \d{2}:[^\s,\]}#]*/) : null;
|
|
288
|
+
if (time) {
|
|
289
|
+
this.pos += time[0].length;
|
|
290
|
+
raw += time[0];
|
|
291
|
+
}
|
|
292
|
+
return new Opaque(raw);
|
|
293
|
+
}
|
|
294
|
+
escape() {
|
|
295
|
+
const code = this.peek();
|
|
296
|
+
this.pos++;
|
|
297
|
+
if (code in ESCAPES)
|
|
298
|
+
return ESCAPES[code];
|
|
299
|
+
if (code === "u" || code === "U") {
|
|
300
|
+
const length = code === "u" ? 4 : 8;
|
|
301
|
+
const hex = this.text.slice(this.pos, this.pos + length);
|
|
302
|
+
if (!new RegExp(`^[0-9A-Fa-f]{${length}}$`).test(hex))
|
|
303
|
+
this.fail("bad unicode escape");
|
|
304
|
+
this.pos += length;
|
|
305
|
+
const point = Number.parseInt(hex, 16);
|
|
306
|
+
if (point > 0x10ffff)
|
|
307
|
+
this.fail("bad unicode escape");
|
|
308
|
+
return String.fromCodePoint(point);
|
|
309
|
+
}
|
|
310
|
+
this.fail("bad escape");
|
|
311
|
+
}
|
|
312
|
+
basicString() {
|
|
313
|
+
this.pos++;
|
|
314
|
+
let out = "";
|
|
315
|
+
for (;;) {
|
|
316
|
+
const char = this.peek();
|
|
317
|
+
if (char === "" || char === "\n")
|
|
318
|
+
this.fail("unterminated string");
|
|
319
|
+
this.pos++;
|
|
320
|
+
if (char === '"')
|
|
321
|
+
return out;
|
|
322
|
+
out += char === "\\" ? this.escape() : char;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
literalString() {
|
|
326
|
+
this.pos++;
|
|
327
|
+
const end = this.text.indexOf("'", this.pos);
|
|
328
|
+
const newline = this.text.indexOf("\n", this.pos);
|
|
329
|
+
if (end < 0 || (newline >= 0 && newline < end))
|
|
330
|
+
this.fail("unterminated string");
|
|
331
|
+
const out = this.text.slice(this.pos, end);
|
|
332
|
+
this.pos = end + 1;
|
|
333
|
+
return out;
|
|
334
|
+
}
|
|
335
|
+
multilineBasicString() {
|
|
336
|
+
this.pos += 3;
|
|
337
|
+
if (this.peek() === "\n")
|
|
338
|
+
this.pos++;
|
|
339
|
+
else if (this.peek(2) === "\r\n")
|
|
340
|
+
this.pos += 2;
|
|
341
|
+
let out = "";
|
|
342
|
+
for (;;) {
|
|
343
|
+
if (this.pos >= this.text.length)
|
|
344
|
+
this.fail("unterminated string");
|
|
345
|
+
if (this.peek(3) === '"""') {
|
|
346
|
+
// Up to two quotes may sit right before the closing delimiter.
|
|
347
|
+
let extra = 0;
|
|
348
|
+
while (extra < 2 && this.text[this.pos + 3 + extra] === '"')
|
|
349
|
+
extra++;
|
|
350
|
+
out += '"'.repeat(extra);
|
|
351
|
+
this.pos += 3 + extra;
|
|
352
|
+
return out;
|
|
353
|
+
}
|
|
354
|
+
const char = this.peek();
|
|
355
|
+
this.pos++;
|
|
356
|
+
if (char !== "\\") {
|
|
357
|
+
out += char;
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
// A line-ending backslash trims the newline and following whitespace.
|
|
361
|
+
const rest = this.text.slice(this.pos).match(/^[ \t]*\r?\n[ \t\r\n]*/);
|
|
362
|
+
if (rest)
|
|
363
|
+
this.pos += rest[0].length;
|
|
364
|
+
else
|
|
365
|
+
out += this.escape();
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
multilineLiteralString() {
|
|
369
|
+
this.pos += 3;
|
|
370
|
+
if (this.peek() === "\n")
|
|
371
|
+
this.pos++;
|
|
372
|
+
else if (this.peek(2) === "\r\n")
|
|
373
|
+
this.pos += 2;
|
|
374
|
+
const end = this.text.indexOf("'''", this.pos);
|
|
375
|
+
if (end < 0)
|
|
376
|
+
this.fail("unterminated string");
|
|
377
|
+
let close = end;
|
|
378
|
+
while (close - end < 2 && this.text[close + 3] === "'")
|
|
379
|
+
close++;
|
|
380
|
+
const out = this.text.slice(this.pos, close);
|
|
381
|
+
this.pos = close + 3;
|
|
382
|
+
return out;
|
|
383
|
+
}
|
|
384
|
+
array() {
|
|
385
|
+
this.pos++;
|
|
386
|
+
const items = [];
|
|
387
|
+
for (;;) {
|
|
388
|
+
this.skipBlank();
|
|
389
|
+
if (this.peek() === "]") {
|
|
390
|
+
this.pos++;
|
|
391
|
+
return items;
|
|
392
|
+
}
|
|
393
|
+
items.push(this.value());
|
|
394
|
+
this.skipBlank();
|
|
395
|
+
if (this.peek() === ",")
|
|
396
|
+
this.pos++;
|
|
397
|
+
else if (this.peek() !== "]")
|
|
398
|
+
this.fail("expected , or ] in an array");
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
inlineTable() {
|
|
402
|
+
this.pos++;
|
|
403
|
+
const table = new Table();
|
|
404
|
+
for (;;) {
|
|
405
|
+
this.skipBlank();
|
|
406
|
+
if (this.peek() === "}") {
|
|
407
|
+
this.pos++;
|
|
408
|
+
return table;
|
|
409
|
+
}
|
|
410
|
+
this.assignment(table);
|
|
411
|
+
this.skipBlank();
|
|
412
|
+
if (this.peek() === ",")
|
|
413
|
+
this.pos++;
|
|
414
|
+
else if (this.peek() !== "}")
|
|
415
|
+
this.fail("expected , or } in an inline table");
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
function get(table, ...path) {
|
|
420
|
+
let current = table;
|
|
421
|
+
for (const key of path) {
|
|
422
|
+
if (!(current instanceof Table))
|
|
423
|
+
return undefined;
|
|
424
|
+
current = current.entries.get(key);
|
|
425
|
+
}
|
|
426
|
+
return current;
|
|
427
|
+
}
|
|
428
|
+
function requirementList(value, where) {
|
|
429
|
+
if (value === undefined)
|
|
430
|
+
return [];
|
|
431
|
+
if (!Array.isArray(value))
|
|
432
|
+
throw new TomlScanError(`${where} is not an array`);
|
|
433
|
+
const names = [];
|
|
434
|
+
for (const item of value) {
|
|
435
|
+
if (typeof item !== "string")
|
|
436
|
+
throw new TomlScanError(`${where} holds a non-string entry`);
|
|
437
|
+
const name = requirementSpec(item);
|
|
438
|
+
if (name)
|
|
439
|
+
names.push(name);
|
|
440
|
+
}
|
|
441
|
+
return names;
|
|
442
|
+
}
|
|
443
|
+
const NON_REGISTRY = ["path", "git", "url", "file"];
|
|
444
|
+
/** A Poetry dependency value that comes from a registry (a version string, or a table/list without a path, git or url source). */
|
|
445
|
+
function poetryFromRegistry(value) {
|
|
446
|
+
if (value instanceof Table)
|
|
447
|
+
return !NON_REGISTRY.some((key) => value.entries.has(key));
|
|
448
|
+
if (Array.isArray(value))
|
|
449
|
+
return value.some(poetryFromRegistry);
|
|
450
|
+
return true;
|
|
451
|
+
}
|
|
452
|
+
/** The `extras = [...]` a Poetry dependency table (or each table of a
|
|
453
|
+
* multiple-constraints list) names; anything but a list of strings is ignored. */
|
|
454
|
+
function poetryExtras(value) {
|
|
455
|
+
if (Array.isArray(value))
|
|
456
|
+
return value.flatMap(poetryExtras);
|
|
457
|
+
if (!(value instanceof Table))
|
|
458
|
+
return [];
|
|
459
|
+
const extras = value.entries.get("extras");
|
|
460
|
+
return Array.isArray(extras) && extras.every((item) => typeof item === "string") ? extras : [];
|
|
461
|
+
}
|
|
462
|
+
function poetryNames(value) {
|
|
463
|
+
if (!(value instanceof Table))
|
|
464
|
+
return [];
|
|
465
|
+
const names = [];
|
|
466
|
+
for (const [key, spec] of value.entries) {
|
|
467
|
+
if (key.toLowerCase() === "python" || !poetryFromRegistry(spec))
|
|
468
|
+
continue;
|
|
469
|
+
if (requirementName(key) !== key)
|
|
470
|
+
continue;
|
|
471
|
+
const extras = [...new Set(poetryExtras(spec))];
|
|
472
|
+
names.push(extras.length > 0 ? `${key}[${extras.join(",")}]` : key);
|
|
473
|
+
}
|
|
474
|
+
return names;
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Names from pyproject.toml: `[project] dependencies`, every
|
|
478
|
+
* `[project.optional-dependencies]` group, `[tool.poetry.dependencies]`
|
|
479
|
+
* (except python), `[tool.poetry.group.*.dependencies]` and the legacy
|
|
480
|
+
* `[tool.poetry.dev-dependencies]`, plus the packages pip needs to build the
|
|
481
|
+
* project: `[build-system] requires`, or setuptools and wheel when the file has
|
|
482
|
+
* no `[build-system]` table. Throws when the file cannot be scanned or
|
|
483
|
+
* one of those keys has an unexpected shape.
|
|
484
|
+
*/
|
|
485
|
+
export function pyprojectNames(text) {
|
|
486
|
+
const root = new Scanner(text).scan();
|
|
487
|
+
const names = requirementList(get(root, "project", "dependencies"), "project.dependencies");
|
|
488
|
+
const optional = get(root, "project", "optional-dependencies");
|
|
489
|
+
if (optional !== undefined) {
|
|
490
|
+
if (!(optional instanceof Table))
|
|
491
|
+
throw new TomlScanError("project.optional-dependencies is not a table");
|
|
492
|
+
for (const [group, list] of optional.entries) {
|
|
493
|
+
names.push(...requirementList(list, `project.optional-dependencies.${group}`));
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
const poetry = get(root, "tool", "poetry");
|
|
497
|
+
names.push(...poetryNames(get(poetry, "dependencies")));
|
|
498
|
+
const groups = get(poetry, "group");
|
|
499
|
+
if (groups instanceof Table) {
|
|
500
|
+
for (const group of groups.entries.values())
|
|
501
|
+
names.push(...poetryNames(get(group, "dependencies")));
|
|
502
|
+
}
|
|
503
|
+
names.push(...poetryNames(get(poetry, "dev-dependencies")));
|
|
504
|
+
// pip builds the project first: the declared build backend's packages, or
|
|
505
|
+
// (no [build-system] table) the legacy setuptools backend.
|
|
506
|
+
const buildSystem = get(root, "build-system");
|
|
507
|
+
if (buildSystem === undefined)
|
|
508
|
+
names.push("setuptools", "wheel");
|
|
509
|
+
else
|
|
510
|
+
names.push(...requirementList(get(buildSystem, "requires"), "build-system.requires"));
|
|
511
|
+
return names;
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* The project's own names in pyproject.toml (`[project].name` and
|
|
515
|
+
* `[tool.poetry].name`), as written: the quickstart never offers the
|
|
516
|
+
* repository's own project for its allowlist (dependency confusion), even when
|
|
517
|
+
* an optional-dependency group refers back to it with extras. Throws when the
|
|
518
|
+
* file cannot be scanned.
|
|
519
|
+
*/
|
|
520
|
+
export function pyprojectOwnNames(text) {
|
|
521
|
+
const root = new Scanner(text).scan();
|
|
522
|
+
return [get(root, "project", "name"), get(root, "tool", "poetry", "name")].filter((name) => typeof name === "string");
|
|
523
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface RootEntry {
|
|
2
|
+
name: string;
|
|
3
|
+
mode: string;
|
|
4
|
+
type: string;
|
|
5
|
+
object: string;
|
|
6
|
+
}
|
|
7
|
+
/** The entries of `sha`'s root tree. Throws when `sha` is not a full object id or git fails. */
|
|
8
|
+
export declare function rootEntries(dir: string, sha: string): Promise<RootEntry[]>;
|
|
9
|
+
/** True when `sha`'s root tree holds a Python project file. Any failure means not Python. */
|
|
10
|
+
export declare function detectPythonProject(dir: string, sha: string): Promise<boolean>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Python detection for the quickstart's coding step: reads the committed root
|
|
3
|
+
* of the base commit (never the working tree) with the hardened git helpers.
|
|
4
|
+
*/
|
|
5
|
+
import { localGit } from "../coding/local-git.js";
|
|
6
|
+
const MARKER = /^(?:pyproject\.toml|setup\.py|setup\.cfg|Pipfile|requirements.*\.txt)$/;
|
|
7
|
+
const OBJECT_ID = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
8
|
+
/** The entries of `sha`'s root tree. Throws when `sha` is not a full object id or git fails. */
|
|
9
|
+
export async function rootEntries(dir, sha) {
|
|
10
|
+
if (!OBJECT_ID.test(sha))
|
|
11
|
+
throw new Error("not a full commit id");
|
|
12
|
+
const listing = await localGit(dir, ["ls-tree", "-z", sha]);
|
|
13
|
+
const entries = [];
|
|
14
|
+
for (const entry of listing.split("\0")) {
|
|
15
|
+
// "<mode> <type> <object>\t<name>"
|
|
16
|
+
const tab = entry.indexOf("\t");
|
|
17
|
+
if (tab < 0)
|
|
18
|
+
continue;
|
|
19
|
+
const [mode, type, object] = entry.slice(0, tab).split(" ");
|
|
20
|
+
entries.push({ name: entry.slice(tab + 1), mode, type, object });
|
|
21
|
+
}
|
|
22
|
+
return entries;
|
|
23
|
+
}
|
|
24
|
+
/** True when `sha`'s root tree holds a Python project file. Any failure means not Python. */
|
|
25
|
+
export async function detectPythonProject(dir, sha) {
|
|
26
|
+
try {
|
|
27
|
+
return (await rootEntries(dir, sha)).some((entry) => entry.type === "blob" && MARKER.test(entry.name));
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const MAX_REPO_PACKAGES = 200;
|
|
2
|
+
type Ecosystem = "npm" | "pypi";
|
|
3
|
+
export interface RepoPackages {
|
|
4
|
+
/** Ecosystems with at least one name; sorted, de-duplicated names. */
|
|
5
|
+
allowlist: Partial<Record<Ecosystem, string[]>>;
|
|
6
|
+
/** One line each: a manifest that was skipped, or an ecosystem over the cap. */
|
|
7
|
+
notes: string[];
|
|
8
|
+
}
|
|
9
|
+
export declare function readRepoPackages(dir: string, sha: string): Promise<RepoPackages>;
|
|
10
|
+
export {};
|