@askrjs/cli 0.0.7 → 0.0.9
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/add.js +10 -1
- package/dist/create.js +1 -1
- package/dist/{discovery-Djq8TxJu.js → discovery-BX-lnFRK.js} +1 -2
- package/dist/generate.js +278 -20
- package/dist/{planner-ZAX9SFjY.js → planner-BDfYDKnI.js} +112 -31
- package/dist/registry-BVliEbUv.js +95 -0
- package/dist/skills/askr-ssr-ssg/SKILL.md +1 -1
- package/dist/{skills-B7CbWur9.js → skills-C2KzfTl9.js} +0 -1
- package/dist/skills.js +1 -1
- package/dist/{range-YUs9eimn.js → specification-BSlq_n9A.js} +50 -4
- package/dist/ssg.d.ts +0 -1
- package/dist/ssg.js +4 -5
- package/dist/templates/full-stack/src/server/app.ts +34 -16
- package/dist/templates/full-stack/src/server/dependencies.ts +7 -2
- package/dist/templates/spa/README.md +2 -2
- package/dist/templates/spa/src/main.tsx +2 -3
- package/dist/templates/spa/src/pages/_routes.tsx +2 -2
- package/dist/update.js +2 -2
- package/package.json +6 -2
- package/dist/registry-D2APi6x0.js +0 -228
- package/dist/specification-DXnDOC-0.js +0 -49
package/dist/add.js
CHANGED
|
@@ -226,9 +226,18 @@ function renderServerActionRegistry(actions) {
|
|
|
226
226
|
...actions.map((action) => `import { ${action.handlerName} } from './actions/${action.slug}.js';`),
|
|
227
227
|
"import type { AppDependencies } from './dependencies.js';",
|
|
228
228
|
"",
|
|
229
|
+
"function csrfSecret(): string {",
|
|
230
|
+
" const secret = process.env.CSRF_SECRET;",
|
|
231
|
+
" if (process.env.NODE_ENV !== 'production') return secret ?? 'development-only-secret';",
|
|
232
|
+
" if (!secret || secret.length < 32 || new Set(secret).size < 12) {",
|
|
233
|
+
" throw new Error('Production requires a strong CSRF_SECRET (at least 32 varied characters).');",
|
|
234
|
+
" }",
|
|
235
|
+
" return secret;",
|
|
236
|
+
"}",
|
|
237
|
+
"",
|
|
229
238
|
"export function createActions(deps: AppDependencies) {",
|
|
230
239
|
" return defineServerActions({ dependencies: deps,",
|
|
231
|
-
" csrf: { secret:
|
|
240
|
+
" csrf: { secret: csrfSecret() },",
|
|
232
241
|
" },",
|
|
233
242
|
...actions.map((action) => ` handleAction(${action.descriptorName}, ${action.handlerName}),`),
|
|
234
243
|
" );",
|
package/dist/create.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { t as isDirectExecution } from "./is-direct-execution-Cdlr-ZUl.js";
|
|
3
|
-
import { t as installBundledSkills } from "./skills-
|
|
3
|
+
import { t as installBundledSkills } from "./skills-C2KzfTl9.js";
|
|
4
4
|
import { n as publishStagedDirectory, t as createSiblingStage } from "./directory-swap-DWoHtx7C.js";
|
|
5
5
|
import fs from "node:fs/promises";
|
|
6
6
|
import path from "node:path";
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t as parseDependencySpecification } from "./specification-DXnDOC-0.js";
|
|
1
|
+
import { n as analyzeRange, t as parseDependencySpecification } from "./specification-BSlq_n9A.js";
|
|
3
2
|
import fs from "node:fs/promises";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
import { load } from "js-yaml";
|
package/dist/generate.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import { mkdir, mkdtemp, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
2
|
-
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
1
|
+
import { mkdir, mkdtemp, readFile, readdir, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
3
3
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
4
|
+
import { lookup } from "node:dns/promises";
|
|
5
|
+
import { request } from "node:https";
|
|
6
|
+
import { BlockList, isIP } from "node:net";
|
|
4
7
|
import { load } from "js-yaml";
|
|
5
8
|
//#region src/generate/generator.ts
|
|
6
9
|
const OWNED = [
|
|
@@ -119,30 +122,202 @@ function parseOpenApiDocument(contents, source) {
|
|
|
119
122
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new GenerationError(`OpenAPI document must be an object: ${source}`);
|
|
120
123
|
return parsed;
|
|
121
124
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
125
|
+
const blockedAddresses = new BlockList();
|
|
126
|
+
for (const [network, prefix] of [
|
|
127
|
+
["0.0.0.0", 8],
|
|
128
|
+
["10.0.0.0", 8],
|
|
129
|
+
["100.64.0.0", 10],
|
|
130
|
+
["127.0.0.0", 8],
|
|
131
|
+
["169.254.0.0", 16],
|
|
132
|
+
["172.16.0.0", 12],
|
|
133
|
+
["192.0.0.0", 24],
|
|
134
|
+
["192.0.2.0", 24],
|
|
135
|
+
["192.168.0.0", 16],
|
|
136
|
+
["198.18.0.0", 15],
|
|
137
|
+
["198.51.100.0", 24],
|
|
138
|
+
["203.0.113.0", 24],
|
|
139
|
+
["224.0.0.0", 4],
|
|
140
|
+
["240.0.0.0", 4]
|
|
141
|
+
]) blockedAddresses.addSubnet(network, prefix, "ipv4");
|
|
142
|
+
for (const [network, prefix] of [
|
|
143
|
+
["::", 128],
|
|
144
|
+
["::1", 128],
|
|
145
|
+
["fc00::", 7],
|
|
146
|
+
["fe80::", 10],
|
|
147
|
+
["ff00::", 8],
|
|
148
|
+
["2001:db8::", 32],
|
|
149
|
+
["2001:2::", 48]
|
|
150
|
+
]) blockedAddresses.addSubnet(network, prefix, "ipv6");
|
|
151
|
+
function privateAddress(address) {
|
|
152
|
+
const family = isIP(address);
|
|
153
|
+
if (family === 4) return blockedAddresses.check(address, "ipv4");
|
|
154
|
+
if (family !== 6) return true;
|
|
155
|
+
const mappedSuffix = /^::ffff:(.+)$/i.exec(address)?.[1];
|
|
156
|
+
const mapped = mappedSuffix?.includes(".") ? mappedSuffix : mappedSuffix && /^[0-9a-f]{1,4}:[0-9a-f]{1,4}$/i.test(mappedSuffix) ? mappedSuffix.split(":").flatMap((part) => {
|
|
157
|
+
const value = Number.parseInt(part, 16);
|
|
158
|
+
return [value >> 8, value & 255];
|
|
159
|
+
}).join(".") : void 0;
|
|
160
|
+
return mapped ? privateAddress(mapped) : blockedAddresses.check(address, "ipv6");
|
|
161
|
+
}
|
|
162
|
+
function remaining(deadline, uri) {
|
|
163
|
+
const value = deadline - Date.now();
|
|
164
|
+
if (value <= 0) throw new GenerationError(`Timed out fetching OpenAPI reference ${uri}`);
|
|
165
|
+
return value;
|
|
166
|
+
}
|
|
167
|
+
async function withDeadline(promise, deadline, uri) {
|
|
168
|
+
let timer;
|
|
169
|
+
try {
|
|
170
|
+
return await Promise.race([promise, new Promise((_, reject) => {
|
|
171
|
+
timer = setTimeout(() => reject(new GenerationError(`Timed out fetching OpenAPI reference ${uri}`)), remaining(deadline, uri));
|
|
172
|
+
})]);
|
|
173
|
+
} finally {
|
|
174
|
+
if (timer) clearTimeout(timer);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async function vettedAddresses(url, options, deadline) {
|
|
178
|
+
const hostname = url.hostname.replace(/^\[|\]$/g, "");
|
|
179
|
+
const answers = isIP(hostname) ? [{
|
|
180
|
+
address: hostname,
|
|
181
|
+
family: isIP(hostname)
|
|
182
|
+
}] : await withDeadline(lookup(hostname, {
|
|
183
|
+
all: true,
|
|
184
|
+
verbatim: true
|
|
185
|
+
}), deadline, url.href);
|
|
186
|
+
if (answers.length === 0 || !options.allowPrivateHosts && answers.some(({ address }) => privateAddress(address))) throw new GenerationError(`OpenAPI reference resolves to a private, reserved, or link-local address: ${url.origin}`);
|
|
187
|
+
return answers.map(({ address, family }) => ({
|
|
188
|
+
address,
|
|
189
|
+
family
|
|
190
|
+
}));
|
|
191
|
+
}
|
|
192
|
+
function retryableConnectionError(error) {
|
|
193
|
+
if (!(error instanceof Error)) return false;
|
|
194
|
+
const code = error.code ?? "";
|
|
195
|
+
return !/^ERR_TLS_|^CERT_|SELF_SIGNED|CERTIFICATE/.test(code) && [
|
|
196
|
+
"ECONNREFUSED",
|
|
197
|
+
"ECONNRESET",
|
|
198
|
+
"EHOSTUNREACH",
|
|
199
|
+
"ENETUNREACH",
|
|
200
|
+
"ETIMEDOUT",
|
|
201
|
+
"EPIPE"
|
|
202
|
+
].includes(code);
|
|
203
|
+
}
|
|
204
|
+
function requestAddress(url, vetted, deadline) {
|
|
205
|
+
return new Promise((resolve, reject) => {
|
|
206
|
+
let receivedHeaders = false;
|
|
207
|
+
const request$1 = request(url, {
|
|
208
|
+
method: "GET",
|
|
209
|
+
headers: {
|
|
210
|
+
accept: "application/json, application/yaml, text/yaml, */*",
|
|
211
|
+
"accept-encoding": "identity"
|
|
212
|
+
},
|
|
213
|
+
servername: url.hostname,
|
|
214
|
+
lookup: (_hostname, _options, callback) => callback(null, vetted.address, vetted.family)
|
|
215
|
+
}, (message) => {
|
|
216
|
+
receivedHeaders = true;
|
|
217
|
+
resolve({
|
|
218
|
+
status: message.statusCode ?? 0,
|
|
219
|
+
statusText: message.statusMessage ?? "",
|
|
220
|
+
headers: message.headers,
|
|
221
|
+
message
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
request$1.setTimeout(remaining(deadline, url.href), () => {
|
|
225
|
+
request$1.destroy(Object.assign(/* @__PURE__ */ new Error("connection timed out"), { code: "ETIMEDOUT" }));
|
|
226
|
+
});
|
|
227
|
+
request$1.on("error", (error) => reject(Object.assign(error, { receivedHeaders })));
|
|
228
|
+
request$1.end();
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
async function requestVetted(url, addresses, deadline) {
|
|
232
|
+
let lastError;
|
|
233
|
+
for (const address of addresses) try {
|
|
234
|
+
return await withDeadline(requestAddress(url, address, deadline), deadline, url.href);
|
|
235
|
+
} catch (error) {
|
|
236
|
+
lastError = error;
|
|
237
|
+
if (!retryableConnectionError(error)) throw error;
|
|
238
|
+
}
|
|
239
|
+
throw lastError;
|
|
240
|
+
}
|
|
241
|
+
async function responseText(response, uri, maxBytes, deadline) {
|
|
242
|
+
const declared = Number(response.headers["content-length"]);
|
|
243
|
+
if (Number.isFinite(declared) && declared > maxBytes) throw new GenerationError(`OpenAPI reference exceeds ${maxBytes} bytes: ${uri}`);
|
|
244
|
+
const encoding = response.headers["content-encoding"];
|
|
245
|
+
if (encoding && encoding !== "identity") throw new GenerationError(`OpenAPI reference returned unsupported content encoding: ${encoding}`);
|
|
246
|
+
const chunks = [];
|
|
247
|
+
let size = 0;
|
|
248
|
+
try {
|
|
249
|
+
await withDeadline(new Promise((resolve, reject) => {
|
|
250
|
+
response.message.on("data", (value) => {
|
|
251
|
+
size += value.byteLength;
|
|
252
|
+
if (size > maxBytes) {
|
|
253
|
+
response.message.destroy();
|
|
254
|
+
reject(new GenerationError(`OpenAPI reference exceeds ${maxBytes} bytes: ${uri}`));
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
chunks.push(value);
|
|
258
|
+
});
|
|
259
|
+
response.message.on("end", resolve);
|
|
260
|
+
response.message.on("error", reject);
|
|
261
|
+
response.message.on("aborted", () => reject(new GenerationError(`OpenAPI response body was aborted: ${uri}`)));
|
|
262
|
+
}), deadline, uri);
|
|
263
|
+
} catch (error) {
|
|
264
|
+
response.message.destroy();
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
if (size > maxBytes) throw new GenerationError(`OpenAPI reference exceeds ${maxBytes} bytes: ${uri}`);
|
|
268
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
269
|
+
}
|
|
270
|
+
async function fetchSource(uri, options) {
|
|
271
|
+
let current = new URL(uri);
|
|
272
|
+
const deadline = Date.now() + options.timeoutMs;
|
|
273
|
+
for (let redirects = 0;; redirects += 1) {
|
|
274
|
+
if (current.protocol !== "https:") throw new GenerationError(`Remote OpenAPI references must use HTTPS: ${current.href}`);
|
|
275
|
+
if (current.username || current.password) throw new GenerationError(`Remote OpenAPI references must not include credentials: ${current.origin}`);
|
|
276
|
+
if (current.origin !== options.remoteRootOrigin && !options.allowedReferenceOrigins.has(current.origin)) throw new GenerationError(`Cross-origin OpenAPI reference is not allowed: ${current.origin}`);
|
|
277
|
+
const addresses = await vettedAddresses(current, options, deadline);
|
|
278
|
+
const response = await requestVetted(current, addresses, deadline);
|
|
279
|
+
if (response.status >= 300 && response.status < 400) {
|
|
280
|
+
if (redirects >= options.maxRedirects) throw new GenerationError(`Too many redirects while fetching OpenAPI reference ${uri}`);
|
|
281
|
+
response.message.resume();
|
|
282
|
+
const location = response.headers.location;
|
|
283
|
+
if (!location) throw new GenerationError(`OpenAPI redirect is missing Location: ${current.href}`);
|
|
284
|
+
current = new URL(location, current);
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
if (response.status < 200 || response.status >= 300) {
|
|
288
|
+
response.message.resume();
|
|
289
|
+
throw new GenerationError(`Unable to fetch OpenAPI reference ${current.href}: ${response.status} ${response.statusText}`);
|
|
290
|
+
}
|
|
126
291
|
return {
|
|
127
|
-
uri,
|
|
128
|
-
document: parseOpenApiDocument(await response.
|
|
292
|
+
uri: current.href,
|
|
293
|
+
document: parseOpenApiDocument(await responseText(response, current.href, options.maxBytes, deadline), current.href)
|
|
129
294
|
};
|
|
130
295
|
}
|
|
296
|
+
}
|
|
297
|
+
async function readSource(uri, options) {
|
|
298
|
+
if (uri.startsWith("http://") || uri.startsWith("https://")) return fetchSource(uri, options);
|
|
131
299
|
const path = fileURLToPath(uri);
|
|
300
|
+
const canonical = await realpath(path);
|
|
301
|
+
const relativePath = options.localRootDirectory ? relative(options.localRootDirectory, canonical) : "..";
|
|
302
|
+
if (!options.localRootDirectory || relativePath === ".." || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath)) throw new GenerationError(`Local OpenAPI reference escapes the specification directory: ${path}`);
|
|
303
|
+
const contents = await readFile(canonical);
|
|
304
|
+
if (contents.byteLength > options.maxBytes) throw new GenerationError(`OpenAPI reference exceeds ${options.maxBytes} bytes: ${path}`);
|
|
132
305
|
return {
|
|
133
|
-
uri,
|
|
134
|
-
document: parseOpenApiDocument(
|
|
306
|
+
uri: pathToFileURL(canonical).href,
|
|
307
|
+
document: parseOpenApiDocument(contents.toString("utf8"), canonical)
|
|
135
308
|
};
|
|
136
309
|
}
|
|
137
310
|
var OpenApiBundler = class {
|
|
138
311
|
root;
|
|
139
312
|
rootUri;
|
|
313
|
+
options;
|
|
140
314
|
sources = /* @__PURE__ */ new Map();
|
|
141
315
|
aliases = /* @__PURE__ */ new Map();
|
|
142
316
|
names = /* @__PURE__ */ new Map();
|
|
143
|
-
constructor(root, rootUri) {
|
|
317
|
+
constructor(root, rootUri, options) {
|
|
144
318
|
this.root = root;
|
|
145
319
|
this.rootUri = rootUri;
|
|
320
|
+
this.options = options;
|
|
146
321
|
}
|
|
147
322
|
async bundle() {
|
|
148
323
|
if (!this.root.components) this.root.components = {};
|
|
@@ -153,7 +328,7 @@ var OpenApiBundler = class {
|
|
|
153
328
|
async source(uri) {
|
|
154
329
|
let pending = this.sources.get(uri);
|
|
155
330
|
if (!pending) {
|
|
156
|
-
pending = readSource(uri);
|
|
331
|
+
pending = readSource(uri, this.options);
|
|
157
332
|
this.sources.set(uri, pending);
|
|
158
333
|
}
|
|
159
334
|
return pending;
|
|
@@ -175,6 +350,7 @@ var OpenApiBundler = class {
|
|
|
175
350
|
for (const [key, child] of Object.entries(value)) value[key] = await this.expand(child, uri, document, stack);
|
|
176
351
|
}
|
|
177
352
|
async expand(value, uri, document, stack) {
|
|
353
|
+
if (stack.size > this.options.maxDepth) throw new GenerationError(`OpenAPI reference depth exceeds ${this.options.maxDepth}`);
|
|
178
354
|
if (Array.isArray(value)) return Promise.all(value.map((item) => this.expand(item, uri, document, stack)));
|
|
179
355
|
if (!value || typeof value !== "object") return value;
|
|
180
356
|
const object = value;
|
|
@@ -215,9 +391,36 @@ var OpenApiBundler = class {
|
|
|
215
391
|
return this.expand(target, targetSource.uri, targetSource.document, new Set(stack).add(canonical));
|
|
216
392
|
}
|
|
217
393
|
};
|
|
218
|
-
async function loadOpenApi(input) {
|
|
219
|
-
const
|
|
220
|
-
|
|
394
|
+
async function loadOpenApi(input, options = {}) {
|
|
395
|
+
const remote = /^https?:\/\//.test(input);
|
|
396
|
+
const localPath = remote ? void 0 : await realpath(resolve(input));
|
|
397
|
+
const uri = remote ? new URL(input).href : pathToFileURL(localPath).href;
|
|
398
|
+
const rootUrl = new URL(uri);
|
|
399
|
+
if (remote && rootUrl.protocol !== "https:") throw new GenerationError("Remote OpenAPI roots must use HTTPS.");
|
|
400
|
+
const positiveOption = (name, value) => {
|
|
401
|
+
if (!Number.isSafeInteger(value) || value <= 0) throw new GenerationError(`${name} must be a positive safe integer.`);
|
|
402
|
+
return value;
|
|
403
|
+
};
|
|
404
|
+
const resolved = {
|
|
405
|
+
allowedReferenceOrigins: new Set((options.allowedReferenceOrigins ?? []).map((origin) => {
|
|
406
|
+
let parsed;
|
|
407
|
+
try {
|
|
408
|
+
parsed = new URL(origin);
|
|
409
|
+
} catch {
|
|
410
|
+
throw new GenerationError(`Allowed reference origin must be an HTTPS origin: ${origin}`);
|
|
411
|
+
}
|
|
412
|
+
if (parsed.protocol !== "https:" || parsed.username || parsed.password || parsed.pathname !== "/" || parsed.search || parsed.hash || parsed.href !== parsed.origin + "/") throw new GenerationError(`Allowed reference origin must be an HTTPS origin: ${origin}`);
|
|
413
|
+
return parsed.origin;
|
|
414
|
+
})),
|
|
415
|
+
allowPrivateHosts: options.allowPrivateHosts ?? false,
|
|
416
|
+
maxBytes: positiveOption("maxBytes", options.maxBytes ?? 5 * 1024 * 1024),
|
|
417
|
+
maxDepth: positiveOption("maxDepth", options.maxDepth ?? 32),
|
|
418
|
+
maxRedirects: positiveOption("maxRedirects", options.maxRedirects ?? 5),
|
|
419
|
+
timeoutMs: positiveOption("timeoutMs", options.timeoutMs ?? 1e4),
|
|
420
|
+
...remote ? { remoteRootOrigin: rootUrl.origin } : { localRootDirectory: await realpath(dirname(localPath)) }
|
|
421
|
+
};
|
|
422
|
+
const source = await readSource(uri, resolved);
|
|
423
|
+
return new OpenApiBundler(source.document, source.uri, resolved).bundle();
|
|
221
424
|
}
|
|
222
425
|
function generateFiles(document) {
|
|
223
426
|
const version = String(document.openapi ?? "");
|
|
@@ -392,13 +595,13 @@ async function writeGenerated(directory, files, check) {
|
|
|
392
595
|
});
|
|
393
596
|
}
|
|
394
597
|
}
|
|
395
|
-
async function generate(input, output, check = false) {
|
|
598
|
+
async function generate(input, output, check = false, loadOptions = {}) {
|
|
396
599
|
if (!/^https?:\/\//.test(input)) {
|
|
397
600
|
const resolvedInput = resolve(input);
|
|
398
601
|
const inputWithinOutput = relative(resolve(output), resolvedInput);
|
|
399
602
|
if (!inputWithinOutput.startsWith("..") && !isAbsolute(inputWithinOutput)) throw new GenerationError("Generated output must not contain its OpenAPI input document");
|
|
400
603
|
}
|
|
401
|
-
const files = generateFiles(await loadOpenApi(input));
|
|
604
|
+
const files = generateFiles(await loadOpenApi(input, loadOptions));
|
|
402
605
|
await mkdir(dirname(resolve(output)), { recursive: true });
|
|
403
606
|
await writeGenerated(output, files, check);
|
|
404
607
|
}
|
|
@@ -410,13 +613,65 @@ async function runGenerateCli(args, io = console) {
|
|
|
410
613
|
let check = false;
|
|
411
614
|
let json = false;
|
|
412
615
|
let help = false;
|
|
616
|
+
const allowedReferenceOrigins = [];
|
|
617
|
+
const referenceLimits = {};
|
|
413
618
|
const inputs = [];
|
|
414
619
|
for (let i = 0; i < args.length; i++) {
|
|
415
620
|
const arg = args[i];
|
|
416
621
|
if (arg === "--check") check = true;
|
|
417
622
|
else if (arg === "--json") json = true;
|
|
418
623
|
else if (arg === "--help" || arg === "-h") help = true;
|
|
419
|
-
else if (arg === "-
|
|
624
|
+
else if (arg === "--allow-ref-origin") {
|
|
625
|
+
const value = args[i + 1];
|
|
626
|
+
if (!value || value.startsWith("-")) {
|
|
627
|
+
const message = `${arg} requires an HTTPS origin`;
|
|
628
|
+
io.error(wantsJson ? JSON.stringify({
|
|
629
|
+
status: "error",
|
|
630
|
+
error: message
|
|
631
|
+
}) : message);
|
|
632
|
+
return 1;
|
|
633
|
+
}
|
|
634
|
+
allowedReferenceOrigins.push(value);
|
|
635
|
+
i += 1;
|
|
636
|
+
} else if (arg.startsWith("--allow-ref-origin=")) allowedReferenceOrigins.push(arg.slice(19));
|
|
637
|
+
else if (arg === "--ref-timeout-ms" || arg === "--ref-max-bytes" || arg === "--ref-max-depth" || arg === "--ref-max-redirects") {
|
|
638
|
+
const value = args[i + 1];
|
|
639
|
+
const parsed = Number(value);
|
|
640
|
+
if (!value || value.startsWith("-") || !Number.isSafeInteger(parsed) || parsed <= 0) {
|
|
641
|
+
const message = `${arg} requires a positive safe integer`;
|
|
642
|
+
io.error(wantsJson ? JSON.stringify({
|
|
643
|
+
status: "error",
|
|
644
|
+
error: message
|
|
645
|
+
}) : message);
|
|
646
|
+
return 1;
|
|
647
|
+
}
|
|
648
|
+
const key = {
|
|
649
|
+
"--ref-timeout-ms": "timeoutMs",
|
|
650
|
+
"--ref-max-bytes": "maxBytes",
|
|
651
|
+
"--ref-max-depth": "maxDepth",
|
|
652
|
+
"--ref-max-redirects": "maxRedirects"
|
|
653
|
+
}[arg];
|
|
654
|
+
referenceLimits[key] = parsed;
|
|
655
|
+
i += 1;
|
|
656
|
+
} else if (/^--ref-(?:timeout-ms|max-bytes|max-depth|max-redirects)=/.test(arg)) {
|
|
657
|
+
const [flag, value = ""] = arg.split("=", 2);
|
|
658
|
+
const parsed = Number(value);
|
|
659
|
+
if (!Number.isSafeInteger(parsed) || parsed <= 0) {
|
|
660
|
+
const message = `${flag} requires a positive safe integer`;
|
|
661
|
+
io.error(wantsJson ? JSON.stringify({
|
|
662
|
+
status: "error",
|
|
663
|
+
error: message
|
|
664
|
+
}) : message);
|
|
665
|
+
return 1;
|
|
666
|
+
}
|
|
667
|
+
const key = {
|
|
668
|
+
"--ref-timeout-ms": "timeoutMs",
|
|
669
|
+
"--ref-max-bytes": "maxBytes",
|
|
670
|
+
"--ref-max-depth": "maxDepth",
|
|
671
|
+
"--ref-max-redirects": "maxRedirects"
|
|
672
|
+
}[flag];
|
|
673
|
+
referenceLimits[key] = parsed;
|
|
674
|
+
} else if (arg === "-o" || arg === "--output") {
|
|
420
675
|
const value = args[i + 1];
|
|
421
676
|
if (!value || value.startsWith("-")) {
|
|
422
677
|
const message = `${arg} requires a path`;
|
|
@@ -438,7 +693,7 @@ async function runGenerateCli(args, io = console) {
|
|
|
438
693
|
return 1;
|
|
439
694
|
} else inputs.push(arg);
|
|
440
695
|
}
|
|
441
|
-
const usage = "Usage: askr generate <input> -o <output-directory> [--check] [--json]";
|
|
696
|
+
const usage = "Usage: askr generate <input> -o <output-directory> [--check] [--json] [--allow-ref-origin <https-origin>] [--ref-timeout-ms <n>] [--ref-max-bytes <n>] [--ref-max-depth <n>] [--ref-max-redirects <n>]";
|
|
442
697
|
if (help) {
|
|
443
698
|
io.log(usage);
|
|
444
699
|
return 0;
|
|
@@ -451,7 +706,10 @@ async function runGenerateCli(args, io = console) {
|
|
|
451
706
|
return 1;
|
|
452
707
|
}
|
|
453
708
|
try {
|
|
454
|
-
await generate(inputs[0], output, check
|
|
709
|
+
await generate(inputs[0], output, check, {
|
|
710
|
+
allowedReferenceOrigins,
|
|
711
|
+
...referenceLimits
|
|
712
|
+
});
|
|
455
713
|
io.log(json ? JSON.stringify({
|
|
456
714
|
status: "ok",
|
|
457
715
|
action: check ? "checked" : "generated",
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { n as
|
|
2
|
-
import { t as parseDependencySpecification } from "./specification-DXnDOC-0.js";
|
|
1
|
+
import { i as rewriteRange, n as analyzeRange, r as isBreakingChange, t as parseDependencySpecification } from "./specification-BSlq_n9A.js";
|
|
3
2
|
import semver from "semver";
|
|
4
3
|
//#region src/update/planner.ts
|
|
5
4
|
const STATUS_PRIORITY = {
|
|
@@ -123,12 +122,13 @@ function planOne(occurrence, packument, failure, tag, mode, chosen, blocker) {
|
|
|
123
122
|
reason: blocker
|
|
124
123
|
}
|
|
125
124
|
};
|
|
126
|
-
|
|
125
|
+
const declared = semver.minVersion(occurrence.currentSpecification)?.version ?? allowed;
|
|
126
|
+
if (!semver.gt(selected, declared)) return {
|
|
127
127
|
targetVersion: target,
|
|
128
128
|
occurrence: {
|
|
129
129
|
...withVersions,
|
|
130
130
|
status: "current",
|
|
131
|
-
reason: blocker ?? "
|
|
131
|
+
reason: blocker ?? "the declared version is current"
|
|
132
132
|
}
|
|
133
133
|
};
|
|
134
134
|
const analysis = analyzeRange(occurrence.currentSpecification);
|
|
@@ -149,11 +149,20 @@ function planOne(occurrence, packument, failure, tag, mode, chosen, blocker) {
|
|
|
149
149
|
reason: "breaking update is available via askr upgrade"
|
|
150
150
|
}
|
|
151
151
|
};
|
|
152
|
+
const proposedSpecification = rewriteRange(analysis.shape, selected, breaking);
|
|
153
|
+
if (proposedSpecification === occurrence.currentSpecification) return {
|
|
154
|
+
targetVersion: target,
|
|
155
|
+
occurrence: {
|
|
156
|
+
...withVersions,
|
|
157
|
+
status: "current",
|
|
158
|
+
reason: "the selected version cannot further rebase this range style"
|
|
159
|
+
}
|
|
160
|
+
};
|
|
152
161
|
return {
|
|
153
162
|
targetVersion: target,
|
|
154
163
|
occurrence: {
|
|
155
164
|
...withVersions,
|
|
156
|
-
proposedSpecification
|
|
165
|
+
proposedSpecification,
|
|
157
166
|
status: breaking ? "breaking" : "safe",
|
|
158
167
|
reason: selected === target ? "selected tag target is eligible" : `compatible version ${selected} selected below ${tag}@${target}`
|
|
159
168
|
}
|
|
@@ -185,10 +194,8 @@ function solveWorkspace(workspace, selectedOccurrences, context, packuments, tag
|
|
|
185
194
|
}
|
|
186
195
|
const variables = [...states.entries()].filter(([, state]) => state.selected && state.candidates.length > 0).sort(([a], [b]) => a.localeCompare(b));
|
|
187
196
|
const fixed = new Map([...states].flatMap(([name, state]) => state.selected ? [] : state.current ? [[name, state.current]] : []));
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
let firstFailure = "no jointly peer-compatible published version set exists";
|
|
191
|
-
const validate = (choices) => {
|
|
197
|
+
const currentChoices = new Map(variables.flatMap(([name, state]) => state.current ? [[name, state.current]] : []));
|
|
198
|
+
const validate = (choices, domains) => {
|
|
192
199
|
const installed = new Map([...fixed, ...choices]);
|
|
193
200
|
for (const [name, version] of installed) {
|
|
194
201
|
const packument = packuments.get(name);
|
|
@@ -198,10 +205,12 @@ function solveWorkspace(workspace, selectedOccurrences, context, packuments, tag
|
|
|
198
205
|
for (const [peer, requirement] of Object.entries(meta.peerDependencies ?? {}).sort(([a], [b]) => a.localeCompare(b))) {
|
|
199
206
|
if (typeof requirement !== "string") continue;
|
|
200
207
|
const providerChanged = states.get(name)?.current !== version;
|
|
201
|
-
const
|
|
208
|
+
const assignedPeer = choices.get(peer);
|
|
209
|
+
const peerChanged = assignedPeer !== void 0 && states.get(peer)?.current !== assignedPeer;
|
|
202
210
|
if (!providerChanged && !peerChanged) continue;
|
|
203
|
-
const peerVersion =
|
|
211
|
+
const peerVersion = assignedPeer ?? (states.get(peer)?.selected ? void 0 : installed.get(peer));
|
|
204
212
|
if (!peerVersion) {
|
|
213
|
+
if ((domains?.get(peer))?.some((candidate) => semver.satisfies(candidate, requirement, { includePrerelease: true }))) continue;
|
|
205
214
|
if (optionalPeer(meta, peer)) continue;
|
|
206
215
|
return `${name}@${version} requires missing peer ${peer}@${requirement}`;
|
|
207
216
|
}
|
|
@@ -210,31 +219,103 @@ function solveWorkspace(workspace, selectedOccurrences, context, packuments, tag
|
|
|
210
219
|
}
|
|
211
220
|
return null;
|
|
212
221
|
};
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
222
|
+
const edges = new Map(variables.map(([name]) => [name, /* @__PURE__ */ new Set()]));
|
|
223
|
+
for (const [name, state] of variables) for (const version of state.candidates) {
|
|
224
|
+
const peers = metadata(packuments.get(name), version)?.peerDependencies ?? {};
|
|
225
|
+
for (const peer of Object.keys(peers)) {
|
|
226
|
+
if (!edges.has(peer)) continue;
|
|
227
|
+
edges.get(name).add(peer);
|
|
228
|
+
edges.get(peer).add(name);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const components = [];
|
|
232
|
+
const unseen = new Set(edges.keys());
|
|
233
|
+
while (unseen.size > 0) {
|
|
234
|
+
const start = [...unseen].sort()[0];
|
|
235
|
+
const component = [];
|
|
236
|
+
const pending = [start];
|
|
237
|
+
unseen.delete(start);
|
|
238
|
+
while (pending.length > 0) {
|
|
239
|
+
const name = pending.pop();
|
|
240
|
+
component.push(name);
|
|
241
|
+
for (const neighbor of [...edges.get(name)].sort().reverse()) {
|
|
242
|
+
if (!unseen.delete(neighbor)) continue;
|
|
243
|
+
pending.push(neighbor);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
components.push(component.sort());
|
|
247
|
+
}
|
|
248
|
+
const choices = new Map(currentChoices);
|
|
249
|
+
const blockers = /* @__PURE__ */ new Map();
|
|
250
|
+
for (const component of components) {
|
|
251
|
+
const componentSet = new Set(component);
|
|
252
|
+
const baseChoices = new Map([...choices].filter(([name]) => !componentSet.has(name)));
|
|
253
|
+
const domains = new Map(component.map((name) => [name, states.get(name).candidates]));
|
|
254
|
+
let best = null;
|
|
255
|
+
let bestChanged = -1;
|
|
256
|
+
let bestVector = [];
|
|
257
|
+
let statesVisited = 0;
|
|
258
|
+
let exhausted = false;
|
|
259
|
+
let firstFailure = "no jointly peer-compatible published version set exists";
|
|
260
|
+
const memo = /* @__PURE__ */ new Set();
|
|
261
|
+
const visit = (assigned) => {
|
|
262
|
+
if (exhausted) return;
|
|
263
|
+
statesVisited += 1;
|
|
264
|
+
if (statesVisited > 5e4) {
|
|
265
|
+
exhausted = true;
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const merged = new Map([...baseChoices, ...assigned]);
|
|
269
|
+
const remaining = component.filter((name) => !assigned.has(name));
|
|
270
|
+
const changed = component.filter((name) => assigned.has(name) && assigned.get(name) !== states.get(name).current).length;
|
|
271
|
+
if (changed + remaining.length < bestChanged) return;
|
|
272
|
+
const pruned = /* @__PURE__ */ new Map();
|
|
273
|
+
for (const name of remaining) {
|
|
274
|
+
const viable = domains.get(name).filter((version) => validate(new Map([...merged, [name, version]]), domains) === null);
|
|
275
|
+
if (viable.length === 0) {
|
|
276
|
+
firstFailure = validate(merged, domains) ?? firstFailure;
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
pruned.set(name, viable);
|
|
280
|
+
}
|
|
281
|
+
const failure = validate(merged, new Map([...domains, ...pruned]));
|
|
216
282
|
if (failure) {
|
|
217
283
|
firstFailure = failure;
|
|
218
284
|
return;
|
|
219
285
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
bestChanged
|
|
286
|
+
if (remaining.length === 0) {
|
|
287
|
+
const vector = component.map((name) => states.get(name).candidates.indexOf(assigned.get(name)));
|
|
288
|
+
const newer = vector.some((value, index) => value < (bestVector[index] ?? Number.POSITIVE_INFINITY) && vector.slice(0, index).every((prior, priorIndex) => prior === bestVector[priorIndex]));
|
|
289
|
+
if (changed > bestChanged || changed === bestChanged && newer) {
|
|
290
|
+
best = new Map(assigned);
|
|
291
|
+
bestChanged = changed;
|
|
292
|
+
bestVector = vector;
|
|
293
|
+
}
|
|
294
|
+
return;
|
|
224
295
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
296
|
+
const next = remaining.map((name) => [name, pruned.get(name)]).sort(([leftName, left], [rightName, right]) => left.length - right.length || leftName.localeCompare(rightName))[0];
|
|
297
|
+
const signature = component.map((name) => {
|
|
298
|
+
const assignedVersion = assigned.get(name);
|
|
299
|
+
if (assignedVersion !== void 0) return `${name}=assigned:${assignedVersion}`;
|
|
300
|
+
return `${name}=domain:${(pruned.get(name) ?? []).join(",")}`;
|
|
301
|
+
}).join("|");
|
|
302
|
+
if (memo.has(signature)) return;
|
|
303
|
+
memo.add(signature);
|
|
304
|
+
for (const version of next[1]) {
|
|
305
|
+
assigned.set(next[0], version);
|
|
306
|
+
visit(assigned);
|
|
307
|
+
assigned.delete(next[0]);
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
visit(/* @__PURE__ */ new Map());
|
|
311
|
+
if (exhausted) {
|
|
312
|
+
const reason = "peer compatibility search exceeded the 50,000-state budget; resolve this component manually";
|
|
313
|
+
for (const name of component) blockers.set(name, reason);
|
|
314
|
+
} else if (!best) for (const name of component) blockers.set(name, firstFailure);
|
|
315
|
+
else for (const [name, version] of best) choices.set(name, version);
|
|
316
|
+
}
|
|
237
317
|
for (const [name, state] of variables) {
|
|
318
|
+
if (blockers.has(name)) continue;
|
|
238
319
|
if (choices.get(name) !== state.current || state.candidates[0] === state.current) continue;
|
|
239
320
|
const attempted = new Map(choices).set(name, state.candidates[0]);
|
|
240
321
|
blockers.set(name, validate(attempted) ?? "no jointly peer-compatible update advances this dependency");
|
|
@@ -271,7 +352,7 @@ function summarize(decisions) {
|
|
|
271
352
|
function planUpdates(options) {
|
|
272
353
|
const failures = options.failures ?? /* @__PURE__ */ new Map();
|
|
273
354
|
const tags = options.tags ?? {};
|
|
274
|
-
const mode = options.mode ??
|
|
355
|
+
const mode = options.mode ?? "update";
|
|
275
356
|
const context = options.contextOccurrences ?? options.occurrences;
|
|
276
357
|
const workspaceSolutions = /* @__PURE__ */ new Map();
|
|
277
358
|
if (mode !== "force") for (const workspace of new Set(options.occurrences.map((entry) => entry.workspace))) workspaceSolutions.set(workspace, solveWorkspace(workspace, options.occurrences.filter((entry) => entry.workspace === workspace), context, options.packuments, tags, options.cliTag, options.localVersions ?? /* @__PURE__ */ new Map(), mode));
|