@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.
Files changed (68) hide show
  1. package/dist/coding/local-git.js +9 -1
  2. package/dist/coding/registry/pypi.d.ts +25 -1
  3. package/dist/coding/registry/pypi.js +140 -16
  4. package/dist/coding/registry/types.d.ts +17 -3
  5. package/dist/config/providers.d.ts +3 -2
  6. package/dist/core/http-runtime.js +13 -6
  7. package/dist/help-index.json +207 -15
  8. package/dist/mcp/auth/ownership.d.ts +1 -1
  9. package/dist/mcp/tools/agents.js +1 -1
  10. package/dist/providers/coding-proxy/mock-upstream.d.ts +33 -0
  11. package/dist/providers/coding-proxy/mock-upstream.js +116 -0
  12. package/dist/providers/coding-proxy/registry/prisma-store.d.ts +3 -0
  13. package/dist/providers/coding-proxy/registry/prisma-store.js +17 -0
  14. package/dist/providers/coding-proxy/registry/service.d.ts +4 -0
  15. package/dist/providers/coding-proxy/registry/service.js +20 -3
  16. package/dist/providers/coding-proxy/registry/store.d.ts +18 -1
  17. package/dist/providers/coding-proxy/registry/store.js +33 -0
  18. package/dist/providers/coding-proxy/runtime.js +12 -1
  19. package/dist/providers/coding-proxy/types.d.ts +2 -0
  20. package/dist/providers/jobs/kubernetes-isolation.d.ts +13 -0
  21. package/dist/providers/jobs/kubernetes-isolation.js +52 -5
  22. package/dist/providers/jobs/kubernetes.d.ts +20 -6
  23. package/dist/providers/jobs/kubernetes.js +43 -32
  24. package/dist/providers/llm/openai.js +0 -1
  25. package/dist/quickstart/coding-db.js +14 -2
  26. package/dist/quickstart/coding-doctor.d.ts +3 -0
  27. package/dist/quickstart/coding-doctor.js +3 -0
  28. package/dist/quickstart/coding-images.d.ts +9 -0
  29. package/dist/quickstart/coding-images.js +39 -0
  30. package/dist/quickstart/coding-seed.d.ts +13 -1
  31. package/dist/quickstart/coding-seed.js +25 -7
  32. package/dist/quickstart/coding.d.ts +2 -0
  33. package/dist/quickstart/coding.js +75 -3
  34. package/dist/quickstart/config.d.ts +2 -0
  35. package/dist/quickstart/config.js +4 -0
  36. package/dist/quickstart/images.d.ts +11 -0
  37. package/dist/quickstart/images.js +74 -12
  38. package/dist/quickstart/index.d.ts +0 -2
  39. package/dist/quickstart/index.js +10 -14
  40. package/dist/quickstart/manifests.d.ts +56 -0
  41. package/dist/quickstart/manifests.js +523 -0
  42. package/dist/quickstart/python-detect.d.ts +10 -0
  43. package/dist/quickstart/python-detect.js +32 -0
  44. package/dist/quickstart/repo-packages.d.ts +10 -0
  45. package/dist/quickstart/repo-packages.js +132 -0
  46. package/dist/quickstart/reviewer-prompt.d.ts +12 -0
  47. package/dist/quickstart/reviewer-prompt.js +84 -0
  48. package/dist/quickstart/scenarios.d.ts +19 -0
  49. package/dist/quickstart/scenarios.js +28 -0
  50. package/dist/quickstart/sync-reviewer-prompt.d.ts +1 -0
  51. package/dist/quickstart/sync-reviewer-prompt.js +10 -0
  52. package/dist/quickstart-images.json +1 -1
  53. package/dist/viewer/api-schema.d.ts +4 -4
  54. package/docs/coding-agent-setup.md +17 -0
  55. package/docs/coding-packages.md +101 -3
  56. package/docs/coding-worker-byo-images.md +47 -2
  57. package/docs/coding-worker-isolation.md +20 -9
  58. package/docs/getting-started.md +139 -11
  59. package/docs/knowledge.md +11 -0
  60. package/help/architecture-agent.md +136 -0
  61. package/help/build-worker-image.md +198 -0
  62. package/help/coding-packages.md +31 -2
  63. package/help/creating-agents.md +14 -0
  64. package/help/deploy-gke.md +29 -5
  65. package/help/deployment-targets.md +3 -3
  66. package/help/getting-started.md +19 -0
  67. package/help/local-repositories.md +36 -5
  68. package/package.json +3 -2
@@ -72,7 +72,15 @@ export async function localGitWrite(dir, args, options = {}) {
72
72
  }
73
73
  return await new Promise((resolve, reject) => {
74
74
  const child = execFile("git", gitArgs(dir, args), { env: { ...cleanGitEnv(), ...extra }, maxBuffer: GIT_MAX_BUFFER, encoding: "utf8" }, (error, stdout) => (error ? reject(error) : resolve(stdout)));
75
- child.stdin?.end(options.input ?? "");
75
+ // git may exit before reading stdin (an early error, or a command that never
76
+ // reads it); writing to the closed pipe then raises EPIPE on stdin. The
77
+ // callback above already reports git's own result, so a stdin error is not
78
+ // the outcome — but left unhandled it would be an uncaught exception.
79
+ child.stdin?.on("error", () => { });
80
+ if (options.input === undefined)
81
+ child.stdin?.end();
82
+ else
83
+ child.stdin?.end(options.input);
76
84
  });
77
85
  }
78
86
  /** True for a plain branch/tag name that cannot read as an option or revision expression. */
@@ -1,4 +1,28 @@
1
- import { type RegistryAdapter } from "./types.js";
1
+ import { type FileDependency, type RegistryAdapter } from "./types.js";
2
+ /** The most extras one allowlist entry or dependency line may name. */
3
+ export declare const MAX_EXTRAS = 32;
4
+ export declare function isPypiProjectName(name: string): boolean;
2
5
  export declare function normalizePypiName(name: string): string;
6
+ /** Normalized (PEP 685), sorted, de-duplicated extras from the text inside
7
+ * `[...]`, or null when any is not a valid extra name (PEP 508: the same
8
+ * shape as a project name), the list is empty, or it names too many. */
9
+ export declare function parseExtras(list: string): string[] | null;
10
+ /**
11
+ * The dependencies a wheel's METADATA declares for an install of the package
12
+ * with `extras`: every line without an `extra == "…"` marker clause, plus the
13
+ * lines whose marker names one of `extras` (other marker clauses are ignored,
14
+ * so a requested extra's lines are included on every platform). A line gated
15
+ * on any other extra is skipped. Each dependency carries the extras its line
16
+ * asks of it (`uvicorn[standard]`); a line naming the package itself
17
+ * (`celery[redis]; extra == "all"`) adds those extras to this install
18
+ * instead. Names and extras are validated and normalized.
19
+ *
20
+ * Policy, not pip: a plain install (no `extras`) follows no extras at all,
21
+ * neither its own (a self-referencing line) nor its dependencies' — each
22
+ * dependency comes back bare, so extras are followed only along a chain that
23
+ * starts at an allowlist entry naming extras.
24
+ */
25
+ export declare function requiredDists(metadataText: string, extras?: readonly string[]): FileDependency[];
26
+ /** Dependency names a METADATA declares for a plain install (no extras). */
3
27
  export declare function requiresDist(metadataText: string): string[];
4
28
  export declare const pypiAdapter: RegistryAdapter;
@@ -10,7 +10,10 @@ import { unzipSync, strFromU8 } from "fflate";
10
10
  import { AllowlistEntryError, RegistryError, } from "./types.js";
11
11
  const UPSTREAM = "https://pypi.org/simple/";
12
12
  const SIMPLE_JSON = "application/vnd.pypi.simple.v1+json";
13
- const ENTRY = /^([A-Za-z0-9][A-Za-z0-9._-]*)\s*(.*)$/;
13
+ /** name, an optional `[extras]` group (checked separately), then the specifier. */
14
+ const ENTRY = /^([A-Za-z0-9][A-Za-z0-9._-]*)\s*(?:\[([^\]]*)\])?\s*(.*)$/;
15
+ /** The most extras one allowlist entry or dependency line may name. */
16
+ export const MAX_EXTRAS = 32;
14
17
  const WHEEL_METADATA_LIMIT = 64 * 1024 * 1024;
15
18
  function uploadTime(file) {
16
19
  const time = file["upload-time"] ? Date.parse(file["upload-time"]) : NaN;
@@ -19,6 +22,9 @@ function uploadTime(file) {
19
22
  /** A valid PyPI project name (PEP 508): ASCII letters and digits, with
20
23
  * `.`, `_` and `-` allowed only between them. */
21
24
  const PROJECT_NAME = /^(?:[A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9._-]*[A-Za-z0-9])$/;
25
+ export function isPypiProjectName(name) {
26
+ return PROJECT_NAME.test(name);
27
+ }
22
28
  function decoded(segment) {
23
29
  try {
24
30
  return decodeURIComponent(segment);
@@ -43,19 +49,132 @@ function versionOf(filename) {
43
49
  const sdist = filename.match(/^.+?-(\d[^-]*)\.(?:tar\.gz|zip)$/);
44
50
  return sdist ? sdist[1] : null;
45
51
  }
46
- export function requiresDist(metadataText) {
47
- const names = new Set();
52
+ /** Normalized (PEP 685), sorted, de-duplicated extras from the text inside
53
+ * `[...]`, or null when any is not a valid extra name (PEP 508: the same
54
+ * shape as a project name), the list is empty, or it names too many. */
55
+ export function parseExtras(list) {
56
+ const items = list.split(",").map((item) => item.trim());
57
+ if (items.length > MAX_EXTRAS || items.some((item) => !PROJECT_NAME.test(item)))
58
+ return null;
59
+ return [...new Set(items.map(normalizePypiName))].sort();
60
+ }
61
+ /** The string literals a marker compares `extra` to with `==` or `===`
62
+ * (either operand order), normalized; null when the marker never mentions
63
+ * `extra`. Tokenized, so a quoted value never counts as a clause. Fails
64
+ * closed: when `extra` appears in any shape this does not fully recognise
65
+ * (`extra in "a b"`, `extra.x`, a bare `extra`, an unbalanced quote), the
66
+ * line is gated on no extra at all, so it is never followed. */
67
+ function markerExtras(marker) {
68
+ if (!/\bextra\b/.test(marker))
69
+ return null;
70
+ const tokens = marker.match(/"[^"]*"|'[^']*'|===|==|!=|<=|>=|~=|[A-Za-z_][A-Za-z0-9_.]*|\S/g) ?? [];
71
+ const isLiteral = (token) => token !== undefined && token.length >= 2 && (token[0] === '"' || token[0] === "'");
72
+ const isEquals = (token) => token === "==" || token === "===";
73
+ // A lone quote (unterminated string) means the marker cannot be read.
74
+ if (tokens.some((token) => token === '"' || token === "'"))
75
+ return [];
76
+ const found = [];
77
+ let recognised = 0;
78
+ for (let i = 0; i < tokens.length; i++) {
79
+ if (tokens[i] !== "extra")
80
+ continue;
81
+ let value = null;
82
+ if (isEquals(tokens[i + 1]) && isLiteral(tokens[i + 2]))
83
+ value = tokens[i + 2].slice(1, -1);
84
+ else if (isEquals(tokens[i - 1]) && isLiteral(tokens[i - 2]))
85
+ value = tokens[i - 2].slice(1, -1);
86
+ if (value === null)
87
+ return [];
88
+ recognised++;
89
+ if (PROJECT_NAME.test(value))
90
+ found.push(normalizePypiName(value));
91
+ }
92
+ // `extra` only inside quoted values: not a clause, but not one this reads either.
93
+ return recognised === 0 ? [] : found;
94
+ }
95
+ const REQUIREMENT = /^([A-Za-z0-9][A-Za-z0-9._-]*)\s*(?:\[([^\]]*)\])?/;
96
+ // Splits each Requires-Dist value only at its first `;` (the marker), which is
97
+ // all wheel METADATA needs: no `name @ url` with a `;` in the URL, no comments
98
+ // or line continuations as in a requirements file.
99
+ function requirementLines(metadataText) {
100
+ let self = null;
101
+ const lines = [];
48
102
  for (const line of metadataText.split(/\r?\n/)) {
103
+ // The headers end at the first truly empty line; the description
104
+ // follows. A whitespace-only line is a folded header's continuation.
105
+ if (line === "")
106
+ break;
107
+ if (line.startsWith("Name:")) {
108
+ const name = line.slice("Name:".length).trim();
109
+ if (self === null && PROJECT_NAME.test(name))
110
+ self = normalizePypiName(name);
111
+ continue;
112
+ }
49
113
  if (!line.startsWith("Requires-Dist:"))
50
114
  continue;
51
115
  const value = line.slice("Requires-Dist:".length).trim();
52
- if (/;\s*.*\bextra\s*==/.test(value))
116
+ const match = value.match(REQUIREMENT);
117
+ if (!match || !PROJECT_NAME.test(match[1]))
118
+ continue;
119
+ const semicolon = value.indexOf(";", match[0].length);
120
+ lines.push({
121
+ name: normalizePypiName(match[1]),
122
+ // A malformed extras list still installs the package, just none of its extras.
123
+ extras: match[2] === undefined ? [] : (parseExtras(match[2]) ?? []),
124
+ gate: semicolon === -1 ? null : markerExtras(value.slice(semicolon + 1)),
125
+ });
126
+ }
127
+ return { self, lines };
128
+ }
129
+ /**
130
+ * The dependencies a wheel's METADATA declares for an install of the package
131
+ * with `extras`: every line without an `extra == "…"` marker clause, plus the
132
+ * lines whose marker names one of `extras` (other marker clauses are ignored,
133
+ * so a requested extra's lines are included on every platform). A line gated
134
+ * on any other extra is skipped. Each dependency carries the extras its line
135
+ * asks of it (`uvicorn[standard]`); a line naming the package itself
136
+ * (`celery[redis]; extra == "all"`) adds those extras to this install
137
+ * instead. Names and extras are validated and normalized.
138
+ *
139
+ * Policy, not pip: a plain install (no `extras`) follows no extras at all,
140
+ * neither its own (a self-referencing line) nor its dependencies' — each
141
+ * dependency comes back bare, so extras are followed only along a chain that
142
+ * starts at an allowlist entry naming extras.
143
+ */
144
+ export function requiredDists(metadataText, extras = []) {
145
+ const { self, lines } = requirementLines(metadataText);
146
+ const requested = new Set(extras.map(normalizePypiName));
147
+ const plain = requested.size === 0;
148
+ const included = (line) => line.gate === null || line.gate.some((extra) => requested.has(extra));
149
+ for (let grew = !plain; grew;) {
150
+ grew = false;
151
+ for (const line of lines) {
152
+ if (line.name !== self || !included(line))
153
+ continue;
154
+ for (const extra of line.extras) {
155
+ if (requested.size >= MAX_EXTRAS || requested.has(extra))
156
+ continue;
157
+ requested.add(extra);
158
+ grew = true;
159
+ }
160
+ }
161
+ }
162
+ const out = new Map();
163
+ for (const line of lines) {
164
+ if (line.name === self || !included(line))
53
165
  continue;
54
- const name = value.match(/^([A-Za-z0-9][A-Za-z0-9._-]*)/);
55
- if (name)
56
- names.add(normalizePypiName(name[1]));
166
+ const merged = out.get(line.name) ?? new Set();
167
+ if (!plain)
168
+ for (const extra of line.extras)
169
+ if (merged.size < MAX_EXTRAS)
170
+ merged.add(extra);
171
+ out.set(line.name, merged);
57
172
  }
58
- return [...names];
173
+ return [...out].map(([name, merged]) => ({ name, extras: [...merged].sort() }));
174
+ }
175
+ /** Dependency names a METADATA declares for a plain install (no extras). */
176
+ export function requiresDist(metadataText) {
177
+ return requiredDists(metadataText).map((dependency) => dependency.name);
59
178
  }
60
179
  export const pypiAdapter = {
61
180
  id: "pypi",
@@ -68,12 +187,17 @@ export const pypiAdapter = {
68
187
  if (!match)
69
188
  throw new AllowlistEntryError(`"${raw}" is not a valid Python package entry`);
70
189
  const name = normalizePypiName(match[1]);
71
- const range = match[2].trim();
72
- if (!range)
73
- return { name, wildcard: false };
74
- if (!validRange(range))
190
+ let extras;
191
+ if (match[2] !== undefined) {
192
+ const parsed = parseExtras(match[2]);
193
+ if (!parsed)
194
+ throw new AllowlistEntryError(`"[${match[2]}]" in "${raw}" is not a valid list of extras (comma-separated names, at most ${MAX_EXTRAS})`);
195
+ extras = parsed;
196
+ }
197
+ const range = match[3].trim();
198
+ if (range && !validRange(range))
75
199
  throw new AllowlistEntryError(`"${range}" is not a valid PEP 440 specifier`);
76
- return { name, wildcard: false, range };
200
+ return { name, wildcard: false, ...(extras ? { extras } : {}), ...(range ? { range } : {}) };
77
201
  },
78
202
  normalizeName: normalizePypiName,
79
203
  satisfies: (version, range) => pepSatisfies(version, range),
@@ -182,14 +306,14 @@ export const pypiAdapter = {
182
306
  publishedAt: uploadTime(source),
183
307
  };
184
308
  },
185
- async dependenciesFromFile(route, body) {
309
+ async dependenciesFromFile(route, body, extras) {
186
310
  if (route.kind === "file-metadata")
187
- return requiresDist(new TextDecoder().decode(body));
311
+ return requiredDists(new TextDecoder().decode(body), extras);
188
312
  if (!route.filename.endsWith(".whl") || body.byteLength > WHEEL_METADATA_LIMIT)
189
313
  return [];
190
314
  const entries = unzipSync(body, { filter: (file) => /\.dist-info\/METADATA$/.test(file.name) });
191
315
  const metadata = Object.values(entries)[0];
192
- return metadata ? requiresDist(strFromU8(metadata)) : [];
316
+ return metadata ? requiredDists(strFromU8(metadata), extras) : [];
193
317
  },
194
318
  workerConfig({ registryUrl, token, cacheDir }) {
195
319
  const index = new URL("simple/", registryUrl);
@@ -17,6 +17,11 @@ export interface AllowlistEntry {
17
17
  wildcard: boolean;
18
18
  /** Optional version range in this ecosystem's syntax. */
19
19
  range?: string;
20
+ /** PyPI only: the extras the entry names (`psycopg[binary]`), normalized
21
+ * (PEP 685), sorted and de-duplicated. Absent when it names none. Each
22
+ * one also allows the dependencies the package's own metadata declares
23
+ * under that extra, and nothing else. */
24
+ extras?: readonly string[];
20
25
  }
21
26
  export declare class AllowlistEntryError extends Error {
22
27
  }
@@ -52,6 +57,12 @@ export interface DependencySpec {
52
57
  name: string;
53
58
  range: string;
54
59
  }
60
+ /** One dependency read from a served file: its name and the extras the
61
+ * declaring line asks of it (`uvicorn[standard]`), both normalized. */
62
+ export interface FileDependency {
63
+ name: string;
64
+ extras: readonly string[];
65
+ }
55
66
  export interface VersionInfo {
56
67
  version: string;
57
68
  /** Dependency names (normalized). Empty when discovered from files instead. */
@@ -271,10 +282,13 @@ export interface RegistryAdapter {
271
282
  /** Map a download route to a file in metadata the proxy fetched itself.
272
283
  * Returns null if the route names no known file. */
273
284
  resolveDownload(route: DownloadRoute, meta: PackageMetadata): FileRef | null;
274
- /** For ecosystems whose indexes omit dependencies (PyPI): dependency
275
- * names read from a served file or its metadata file. Omitted when
285
+ /** For ecosystems whose indexes omit dependencies (PyPI): dependencies
286
+ * read from a served file or its metadata file. `extras` are the extras
287
+ * the run allows for this package (from the allowlist or from a parent's
288
+ * dependency line): dependencies the file declares only under one of
289
+ * them are included too, and no others. Omitted when
276
290
  * VersionInfo.dependencies is already complete (npm). */
277
- dependenciesFromFile?(route: DownloadRoute | FileMetadataRoute, body: Uint8Array): Promise<string[]>;
291
+ dependenciesFromFile?(route: DownloadRoute | FileMetadataRoute, body: Uint8Array, extras: readonly string[]): Promise<FileDependency[]>;
278
292
  /** Map a file-metadata route (PEP 658) to the file it describes, from
279
293
  * metadata the proxy fetched itself. Returns null if the route names no
280
294
  * known file. */
@@ -144,8 +144,9 @@ export interface KubernetesJobConfig {
144
144
  preflightTimeoutMs?: number;
145
145
  readyTimeoutMs?: number;
146
146
  /**
147
- * Bound for a single NetworkPolicy-enforcement probe exec (the keeper running two sequential
148
- * connects). Defaults to the launcher's own 10 s, which a resource-constrained keeper (e.g. a
147
+ * Time budget for a single NetworkPolicy-enforcement probe (the keeper running two sequential
148
+ * connects); a streak's probes share one exec whose timeout the launcher derives from this.
149
+ * Defaults to the launcher's own 10 s, which a resource-constrained keeper (e.g. a
149
150
  * laptop `kind` cluster's default 250m CPU / 128Mi limit) can exceed even though the probe
150
151
  * itself is healthy — observed live: raising this to 60_000 was enough on `kind`. GKE Autopilot
151
152
  * is unaffected by leaving this unset. The launcher derives its overall enforcement wall-clock
@@ -27,8 +27,14 @@
27
27
  * client never did, so the connection is upgraded underneath a fetch
28
28
  * implementation that never asked for it. That is why only real APIs broke: the
29
29
  * GitHub App client (refusing every coding run before launch), the
30
- * OpenAI/Anthropic/Bedrock SDKs, and remote-JWKS verification in delegating auth
31
- * mode. Plain h1 — every local test server — is unaffected.
30
+ * Anthropic/Bedrock SDKs, and remote-JWKS verification in delegating auth mode.
31
+ * Plain h1 — every local test server — is unaffected. The OpenAI SDK was NOT
32
+ * among them at the time: openai@4 shipped its own node-fetch plus an
33
+ * agentkeepalive agent and never touched the global dispatcher. openai@5+
34
+ * (wardby pins 7.x) dropped that stack for the built-in `fetch`, so OpenAI
35
+ * traffic now rides this dispatcher too and depends on it staying h1-only —
36
+ * `providers/llm/openai.sdk-stream.test.ts` asserts the SDK's requests go
37
+ * through it.
32
38
  *
33
39
  * FIX. Keep the built-in `fetch` and give it a dispatcher that stays on the
34
40
  * protocol it was written for: undici 8's own `Agent` with `allowH2: false`.
@@ -38,8 +44,9 @@
38
44
  *
39
45
  * WHY NOT replace the globals. Installing undici's `fetch`/`Headers`/`Response`/
40
46
  * `Request`/`FormData` process-wide also works, but it swaps the classes under
41
- * the MCP SDK, the OpenAI/Anthropic/Bedrock SDKs (all of which brand-check
42
- * `Headers`/`Response`), `FormData`/`Blob` uploads, `AbortSignal` semantics and
47
+ * the MCP SDK, the OpenAI (7.x)/Anthropic/Bedrock SDKs (all of which use the
48
+ * built-in fetch and brand-check `Headers`/`Response`), `FormData`/`Blob`
49
+ * uploads, `AbortSignal` semantics and
43
50
  * every cross-realm `instanceof` — real risk, for no gain over one dispatcher
44
51
  * option. It would also move all control-plane egress to h2.
45
52
  *
@@ -58,8 +65,8 @@
58
65
  * silently restore the corruption — the price of fixing this with a dispatcher
59
66
  * instead of owning the fetch stack. Nothing in the runtime image does that
60
67
  * today: the only other `setGlobalDispatcher` call in the tree is
61
- * `node-fetch-native` under the `prisma` CLI, which `deploy/Dockerfile:25`
62
- * asserts is absent from the runtime image. (Prisma 7's client runtime and
68
+ * `node-fetch-native` under the `prisma` CLI, which the runtime stage of
69
+ * `deploy/Dockerfile` asserts is absent. (Prisma 7's client runtime and
63
70
  * `@prisma/adapter-pg` contain none; Prisma 6's `runtime/binary.*` did.)
64
71
  *
65
72
  * Restoring Node's OWN dispatcher instead is not possible, though not for the