@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
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import Config from "@npmcli/config";
|
|
4
|
+
import npmDefinitions from "@npmcli/config/lib/definitions/index.js";
|
|
5
|
+
import registryFetch from "npm-registry-fetch";
|
|
6
|
+
//#region src/update/registry.ts
|
|
7
|
+
function executionEnvironment(env) {
|
|
8
|
+
if (env === process.env) return { ...process.env };
|
|
9
|
+
const inherited = { ...process.env };
|
|
10
|
+
for (const name of Object.keys(inherited)) if (/^npm_config_/i.test(name)) delete inherited[name];
|
|
11
|
+
return {
|
|
12
|
+
...inherited,
|
|
13
|
+
...env
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
async function loadNpmConfiguration(root, env = process.env) {
|
|
17
|
+
const effectiveEnvironment = executionEnvironment(env);
|
|
18
|
+
const { definitions, flatten, shorthands } = npmDefinitions;
|
|
19
|
+
const configuration = new Config({
|
|
20
|
+
npmPath: path.dirname(fileURLToPath(import.meta.resolve("@npmcli/config/package.json"))),
|
|
21
|
+
definitions,
|
|
22
|
+
flatten,
|
|
23
|
+
shorthands,
|
|
24
|
+
argv: [process.execPath, "askr"],
|
|
25
|
+
cwd: root,
|
|
26
|
+
env: effectiveEnvironment,
|
|
27
|
+
execPath: process.execPath,
|
|
28
|
+
warn: false
|
|
29
|
+
});
|
|
30
|
+
await configuration.load();
|
|
31
|
+
configuration.validate();
|
|
32
|
+
return {
|
|
33
|
+
cwd: root,
|
|
34
|
+
env: effectiveEnvironment,
|
|
35
|
+
options: { ...configuration.flat }
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function isRecord(value) {
|
|
39
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
40
|
+
}
|
|
41
|
+
function isPackument(value) {
|
|
42
|
+
return isRecord(value) && isRecord(value["dist-tags"]) && isRecord(value.versions);
|
|
43
|
+
}
|
|
44
|
+
async function fetchPackage(packageName, configuration, _specifications) {
|
|
45
|
+
return registryFetch.json(packageName.replace("/", "%2f"), {
|
|
46
|
+
...configuration.options,
|
|
47
|
+
spec: packageName,
|
|
48
|
+
headers: {
|
|
49
|
+
...isRecord(configuration.options.headers) ? configuration.options.headers : {},
|
|
50
|
+
accept: "application/vnd.npm.install-v1+json"
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
function sanitizeRegistryError(error) {
|
|
55
|
+
const value = error && typeof error === "object" ? error : {};
|
|
56
|
+
if (value.statusCode === 404 || value.code === "E404") return "package was not found in the selected registry";
|
|
57
|
+
if (value.statusCode === 401 || value.statusCode === 403 || value.code === "E401" || value.code === "E403") return "registry authentication or authorization failed";
|
|
58
|
+
const code = typeof value.code === "string" ? value.code.toUpperCase() : "";
|
|
59
|
+
if ([
|
|
60
|
+
"ETIMEDOUT",
|
|
61
|
+
"ETIMEOUT",
|
|
62
|
+
"FETCH_ERROR",
|
|
63
|
+
"ECONNRESET"
|
|
64
|
+
].includes(code)) return "registry request timed out or was interrupted";
|
|
65
|
+
if (/^[A-Z][A-Z0-9_]{1,30}$/.test(code)) return `registry request failed (${code})`;
|
|
66
|
+
return "registry request failed";
|
|
67
|
+
}
|
|
68
|
+
async function fetchPackuments(packageNames, configuration, options = {}) {
|
|
69
|
+
const names = [...new Set(packageNames)].sort((left, right) => left.localeCompare(right));
|
|
70
|
+
const packuments = /* @__PURE__ */ new Map();
|
|
71
|
+
const failures = /* @__PURE__ */ new Map();
|
|
72
|
+
const viewPackage = options.viewPackage ?? fetchPackage;
|
|
73
|
+
const configuredSockets = Number(configuration.options.maxSockets ?? 15);
|
|
74
|
+
const concurrency = Math.max(1, Math.min(names.length, Number.isFinite(configuredSockets) ? configuredSockets : 15));
|
|
75
|
+
let nextIndex = 0;
|
|
76
|
+
const worker = async () => {
|
|
77
|
+
while (nextIndex < names.length) {
|
|
78
|
+
const packageName = names[nextIndex++];
|
|
79
|
+
try {
|
|
80
|
+
const result = await viewPackage(packageName, configuration, options.requirements?.specifications?.get(packageName) ?? []);
|
|
81
|
+
if (isPackument(result)) packuments.set(packageName, result);
|
|
82
|
+
else failures.set(packageName, "registry returned malformed package metadata");
|
|
83
|
+
} catch (error) {
|
|
84
|
+
failures.set(packageName, sanitizeRegistryError(error));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
await Promise.all(Array.from({ length: concurrency }, worker));
|
|
89
|
+
return {
|
|
90
|
+
packuments,
|
|
91
|
+
failures
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
//#endregion
|
|
95
|
+
export { fetchPackuments, loadNpmConfiguration };
|
|
@@ -34,7 +34,7 @@ Use this when the app renders outside the browser or produces static output. The
|
|
|
34
34
|
## Copy This Shape
|
|
35
35
|
|
|
36
36
|
```ts
|
|
37
|
-
|
|
37
|
+
const registry = createRouteRegistry(() => {
|
|
38
38
|
page("/docs/{slug}", DocsPage, {
|
|
39
39
|
entries: async () => [{ slug: "getting-started" }, { slug: "routing" }],
|
|
40
40
|
});
|
|
@@ -303,7 +303,6 @@ const REVIEW_PROMPTS = [
|
|
|
303
303
|
re("local Sidebar", String.raw`export\s+(?:default\s+)?function\s+Sidebar\b`)
|
|
304
304
|
]),
|
|
305
305
|
requireAny("Keeps Askr-native route or state primitives in use.", [
|
|
306
|
-
re("registerRoutes", String.raw`\bregisterRoutes\s*\(`),
|
|
307
306
|
re("createRouteRegistry", String.raw`\bcreateRouteRegistry\s*\(`),
|
|
308
307
|
re("state()", String.raw`\bstate\s*\(`),
|
|
309
308
|
re("resource()", String.raw`\bresource\s*\(`)
|
package/dist/skills.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { n as runSkillsCli, r as syncBundledSkills, t as installBundledSkills } from "./skills-
|
|
2
|
+
import { n as runSkillsCli, r as syncBundledSkills, t as installBundledSkills } from "./skills-C2KzfTl9.js";
|
|
3
3
|
export { installBundledSkills, runSkillsCli, syncBundledSkills };
|
|
@@ -106,12 +106,11 @@ function rewriteXRange(shape, target) {
|
|
|
106
106
|
if (count === 1) return String(parsed.major);
|
|
107
107
|
return `${parsed.major}.${parsed.minor}`;
|
|
108
108
|
}
|
|
109
|
-
function rewriteAtomic(shape, target,
|
|
109
|
+
function rewriteAtomic(shape, target, _forcedBreaking) {
|
|
110
110
|
if (shape.kind === "exact") return target;
|
|
111
111
|
if (shape.kind === "operator") return `${shape.operator}${target}`;
|
|
112
112
|
if (shape.kind === "x") return rewriteXRange(shape, target);
|
|
113
|
-
|
|
114
|
-
return forcedBreaking ? `>=${target} <${boundary}` : `${shape.lower} <${boundary}`;
|
|
113
|
+
return `>=${target} <${nextBreakingBoundary(target)}`;
|
|
115
114
|
}
|
|
116
115
|
function rewriteRange(shape, target, forcedBreaking) {
|
|
117
116
|
if (shape.kind !== "union") return rewriteAtomic(shape, target, forcedBreaking);
|
|
@@ -122,4 +121,51 @@ function rewriteRange(shape, target, forcedBreaking) {
|
|
|
122
121
|
return rewritten.join(" || ");
|
|
123
122
|
}
|
|
124
123
|
//#endregion
|
|
125
|
-
|
|
124
|
+
//#region src/update/specification.ts
|
|
125
|
+
const WINDOWS_PATH = /^[A-Za-z]:[\\/]/;
|
|
126
|
+
const HOSTED_GIT = /^(?:bitbucket|gist|github|gitlab):/i;
|
|
127
|
+
const GIT_URL = /^(?:git(?:\+[^:]+)?:|git@|ssh:)/i;
|
|
128
|
+
const GIT_SHORTHAND = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+(?:#.*)?$/;
|
|
129
|
+
const REGISTRY_TAG = /^[A-Za-z][A-Za-z0-9._-]*$/;
|
|
130
|
+
const SCOPED_PATH = /^@[^/]+\/[^/]+$/;
|
|
131
|
+
function parseDependencySpecification(specification) {
|
|
132
|
+
const rawSpec = specification.trim();
|
|
133
|
+
if (!rawSpec) return {
|
|
134
|
+
type: "unsupported",
|
|
135
|
+
rawSpec
|
|
136
|
+
};
|
|
137
|
+
if (/^npm:/i.test(rawSpec)) return {
|
|
138
|
+
type: "alias",
|
|
139
|
+
rawSpec
|
|
140
|
+
};
|
|
141
|
+
if (/^(?:file|link|workspace):/i.test(rawSpec) || rawSpec.startsWith("./") || rawSpec.startsWith("../") || rawSpec.startsWith("/") || rawSpec.startsWith("~/") || /\.(?:tar\.gz|tgz)$/i.test(rawSpec) || WINDOWS_PATH.test(rawSpec) || /^[A-Za-z]:/.test(rawSpec) || SCOPED_PATH.test(rawSpec)) return {
|
|
142
|
+
type: "file",
|
|
143
|
+
rawSpec
|
|
144
|
+
};
|
|
145
|
+
if (HOSTED_GIT.test(rawSpec) || GIT_URL.test(rawSpec) || GIT_SHORTHAND.test(rawSpec)) return {
|
|
146
|
+
type: "git",
|
|
147
|
+
rawSpec
|
|
148
|
+
};
|
|
149
|
+
if (/^https?:/i.test(rawSpec) || /^[a-z][a-z0-9+.-]*:\/\//i.test(rawSpec)) return {
|
|
150
|
+
type: "remote",
|
|
151
|
+
rawSpec
|
|
152
|
+
};
|
|
153
|
+
if (semver.valid(rawSpec)) return {
|
|
154
|
+
type: "version",
|
|
155
|
+
rawSpec
|
|
156
|
+
};
|
|
157
|
+
if (semver.validRange(rawSpec)) return {
|
|
158
|
+
type: "range",
|
|
159
|
+
rawSpec
|
|
160
|
+
};
|
|
161
|
+
if (REGISTRY_TAG.test(rawSpec)) return {
|
|
162
|
+
type: "tag",
|
|
163
|
+
rawSpec
|
|
164
|
+
};
|
|
165
|
+
return {
|
|
166
|
+
type: "unsupported",
|
|
167
|
+
rawSpec
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
//#endregion
|
|
171
|
+
export { rewriteRange as i, analyzeRange as n, isBreakingChange as r, parseDependencySpecification as t };
|
package/dist/ssg.d.ts
CHANGED
package/dist/ssg.js
CHANGED
|
@@ -458,9 +458,8 @@ async function runSsgCli(args = process.argv.slice(2), deps = {}, io = console)
|
|
|
458
458
|
}
|
|
459
459
|
const configModule = imported;
|
|
460
460
|
const candidate = configModule.default ?? configModule.staticConfig ?? configModule;
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
io.error("Error: Config must provide exactly one route source: routes or registry");
|
|
461
|
+
if (!(candidate.registry !== void 0) || Object.prototype.hasOwnProperty.call(candidate, "routes")) {
|
|
462
|
+
io.error("Error: Config must provide a route registry and no raw routes array");
|
|
464
463
|
return 1;
|
|
465
464
|
}
|
|
466
465
|
const config = candidate;
|
|
@@ -468,7 +467,7 @@ async function runSsgCli(args = process.argv.slice(2), deps = {}, io = console)
|
|
|
468
467
|
io.error("Error: Config must provide siteUrl to generate sitemap.xml, or set sitemap: false");
|
|
469
468
|
return 1;
|
|
470
469
|
}
|
|
471
|
-
io.log(
|
|
470
|
+
io.log("Generating registered routes...");
|
|
472
471
|
const createStaticGen = typeof resolvedDeps.createStaticGen === "function" ? resolvedDeps.createStaticGen : await loadCreateStaticGen();
|
|
473
472
|
cliStagingDir = await createSiblingStage(resolvedOutputDir, "askr-ssg");
|
|
474
473
|
if (parsed.incremental && !parsed.forceFull && await pathExists(resolvedOutputDir)) await fs$1.cp(resolvedOutputDir, cliStagingDir, {
|
|
@@ -477,7 +476,7 @@ async function runSsgCli(args = process.argv.slice(2), deps = {}, io = console)
|
|
|
477
476
|
});
|
|
478
477
|
const generationOutputDir = cliStagingDir;
|
|
479
478
|
const ssg = createStaticGen({
|
|
480
|
-
|
|
479
|
+
registry: config.registry,
|
|
481
480
|
outputDir: generationOutputDir,
|
|
482
481
|
seed: config.seed,
|
|
483
482
|
dataOverrides: config.dataOverrides,
|
|
@@ -7,7 +7,18 @@ import { telemetry } from "../telemetry";
|
|
|
7
7
|
import { createActionHandlers } from "./action-registry";
|
|
8
8
|
import type { AppDependencies } from "./dependencies";
|
|
9
9
|
|
|
10
|
+
function csrfSecret(): string {
|
|
11
|
+
const secret = process.env.CSRF_SECRET;
|
|
12
|
+
if (process.env.NODE_ENV !== "production") return secret ?? "development-only-secret";
|
|
13
|
+
if (!secret || secret.length < 32 || new Set(secret).size < 12) {
|
|
14
|
+
throw new Error("Production requires a strong CSRF_SECRET (at least 32 varied characters).");
|
|
15
|
+
}
|
|
16
|
+
return secret;
|
|
17
|
+
}
|
|
18
|
+
|
|
10
19
|
export function createApp(deps: AppDependencies) {
|
|
20
|
+
const secret = csrfSecret();
|
|
21
|
+
const development = process.env.NODE_ENV !== "production";
|
|
11
22
|
return createAskrApp({
|
|
12
23
|
name: "{{appName}}",
|
|
13
24
|
version: "1.0.0",
|
|
@@ -37,26 +48,33 @@ export function createApp(deps: AppDependencies) {
|
|
|
37
48
|
.summary("Create a message")
|
|
38
49
|
.tags("Messages")
|
|
39
50
|
.use(
|
|
40
|
-
csrf({ secret
|
|
41
|
-
rateLimit({
|
|
51
|
+
csrf({ secret }),
|
|
52
|
+
rateLimit({
|
|
53
|
+
store: deps.rateLimits,
|
|
54
|
+
limit: 30,
|
|
55
|
+
windowMs: 60_000,
|
|
56
|
+
key: (ctx) => `message:${ctx.auth.session?.id ?? "anonymous"}`,
|
|
57
|
+
}),
|
|
42
58
|
)
|
|
43
59
|
.created(Message)
|
|
44
60
|
.badRequest()
|
|
45
61
|
.unprocessableEntity()
|
|
46
62
|
.tooManyRequests();
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
63
|
+
if (development) {
|
|
64
|
+
api
|
|
65
|
+
.post("/session", (ctx) => {
|
|
66
|
+
const response = ctx.redirect("/", 303);
|
|
67
|
+
return ctx.setCookie(response, "askr-session", "demo-session", {
|
|
68
|
+
httpOnly: true,
|
|
69
|
+
sameSite: "lax",
|
|
70
|
+
secure: ctx.url.protocol === "https:",
|
|
71
|
+
path: "/",
|
|
72
|
+
});
|
|
73
|
+
})
|
|
74
|
+
.operationId("createSession")
|
|
75
|
+
.summary("Create a development-only demo session")
|
|
76
|
+
.seeOther();
|
|
77
|
+
}
|
|
60
78
|
},
|
|
61
79
|
},
|
|
62
80
|
auth: {
|
|
@@ -68,7 +86,7 @@ export function createApp(deps: AppDependencies) {
|
|
|
68
86
|
},
|
|
69
87
|
actions: {
|
|
70
88
|
handlers: createActionHandlers(),
|
|
71
|
-
csrf: { secret
|
|
89
|
+
csrf: { secret },
|
|
72
90
|
},
|
|
73
91
|
middleware: [requestId(), securityHeaders()],
|
|
74
92
|
telemetry,
|
|
@@ -29,11 +29,16 @@ export function createDependencies(): AppDependencies {
|
|
|
29
29
|
const counters = new Map<string, { count: number; reset: number }>();
|
|
30
30
|
return {
|
|
31
31
|
sessions: {
|
|
32
|
-
get: async (id) =>
|
|
32
|
+
get: async (id) =>
|
|
33
|
+
process.env.NODE_ENV !== "production" && id === "demo-session"
|
|
34
|
+
? { id, subject: "demo-user" }
|
|
35
|
+
: null,
|
|
33
36
|
},
|
|
34
37
|
principals: {
|
|
35
38
|
get: async (subject) =>
|
|
36
|
-
|
|
39
|
+
process.env.NODE_ENV !== "production" && subject === "demo-user"
|
|
40
|
+
? { id: subject, subject, permissions: ["messages:create"] }
|
|
41
|
+
: null,
|
|
37
42
|
},
|
|
38
43
|
actions: { record: async () => undefined },
|
|
39
44
|
rateLimits: {
|
|
@@ -38,7 +38,7 @@ code lives in `src/adapters`.
|
|
|
38
38
|
|
|
39
39
|
```tsx
|
|
40
40
|
// src/pages/_routes.tsx
|
|
41
|
-
import { fallback, group
|
|
41
|
+
import { createRouteRegistry, fallback, group } from '@askrjs/askr/router';
|
|
42
42
|
import RootLayout from './_layout';
|
|
43
43
|
import NotFoundPage from './not-found';
|
|
44
44
|
import AuthLayout from './auth/_layout';
|
|
@@ -48,7 +48,7 @@ import { registerAppRoutes } from './app/_routes';
|
|
|
48
48
|
import PublicLayout from './public/_layout';
|
|
49
49
|
import { registerPublicRoutes } from './public/_routes';
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
export const pageRegistry = createRouteRegistry(() => {
|
|
52
52
|
group({ layout: RootLayout }, () => {
|
|
53
53
|
group({ layout: PublicLayout }, () => {
|
|
54
54
|
registerPublicRoutes();
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { createSPA } from '@askrjs/askr/boot';
|
|
2
|
-
import {
|
|
2
|
+
import { pageRegistry } from './pages/_routes';
|
|
3
3
|
|
|
4
4
|
import './styles.css';
|
|
5
|
-
import './pages/_routes';
|
|
6
5
|
|
|
7
6
|
await createSPA({
|
|
8
7
|
root: document.getElementById('app')!,
|
|
9
|
-
|
|
8
|
+
registry: pageRegistry,
|
|
10
9
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { fallback, group
|
|
1
|
+
import { createRouteRegistry, fallback, group } from '@askrjs/askr/router';
|
|
2
2
|
import RootLayout from './_layout';
|
|
3
3
|
import AuthLayout from './auth/_layout';
|
|
4
4
|
import { registerAuthRoutes } from './auth/_routes';
|
|
@@ -8,7 +8,7 @@ import NotFoundPage from './not-found';
|
|
|
8
8
|
import { registerPublicRoutes } from './public/_routes';
|
|
9
9
|
import PublicLayout from './public/_layout';
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
export const pageRegistry = createRouteRegistry(() => {
|
|
12
12
|
group({ layout: RootLayout }, () => {
|
|
13
13
|
group({ layout: PublicLayout }, () => {
|
|
14
14
|
registerPublicRoutes();
|
package/dist/update.js
CHANGED
|
@@ -193,7 +193,7 @@ function collectEdits(decisions) {
|
|
|
193
193
|
}] : []));
|
|
194
194
|
}
|
|
195
195
|
async function defaultRegistry(root, packageNames, requirements) {
|
|
196
|
-
const { fetchPackuments, loadNpmConfiguration } = await import("./registry-
|
|
196
|
+
const { fetchPackuments, loadNpmConfiguration } = await import("./registry-BVliEbUv.js");
|
|
197
197
|
return fetchPackuments(packageNames, await loadNpmConfiguration(root), { requirements });
|
|
198
198
|
}
|
|
199
199
|
async function runDependencyCli(command, args, io = console, runtime = {}) {
|
|
@@ -217,7 +217,7 @@ async function runDependencyCli(command, args, io = console, runtime = {}) {
|
|
|
217
217
|
let root = null;
|
|
218
218
|
let selectedWorkspaces = [];
|
|
219
219
|
try {
|
|
220
|
-
const [{ discoverProject }, { planUpdates }] = await Promise.all([import("./discovery-
|
|
220
|
+
const [{ discoverProject }, { planUpdates }] = await Promise.all([import("./discovery-BX-lnFRK.js"), import("./planner-BDfYDKnI.js")]);
|
|
221
221
|
const project = await discoverProject({
|
|
222
222
|
cwd: parsed.cwd,
|
|
223
223
|
packagePatterns: parsed.packagePatterns,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askrjs/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.9",
|
|
4
4
|
"description": "Unified CLI for the Askr platform",
|
|
5
5
|
"homepage": "https://github.com/askrjs/askr-cli#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -44,18 +44,22 @@
|
|
|
44
44
|
"test:publint": "publint",
|
|
45
45
|
"pack:check": "npm pack --ignore-scripts --dry-run --json",
|
|
46
46
|
"test:templates": "node scripts/verify-packed-templates.mjs",
|
|
47
|
+
"bench": "npm run build --silent && node --import tsx benchmarks/cli.mjs --gate",
|
|
48
|
+
"bench:json": "npm run build --silent && node --import tsx benchmarks/cli.mjs --gate --json",
|
|
47
49
|
"check": "npm run lint && npm run typecheck && npm run test:coverage && npm run build && npm run test:publint && npm run pack:check",
|
|
48
50
|
"prepack": "npm run build",
|
|
49
51
|
"prepublishOnly": "npm run check && npm run test:templates"
|
|
50
52
|
},
|
|
51
53
|
"dependencies": {
|
|
54
|
+
"@npmcli/config": "^10.12.0",
|
|
52
55
|
"js-yaml": "^5.2.1",
|
|
53
56
|
"minimatch": "^10.2.5",
|
|
57
|
+
"npm-registry-fetch": "^19.1.1",
|
|
54
58
|
"semver": "^7.8.5",
|
|
55
59
|
"tsx": "^4.23.1"
|
|
56
60
|
},
|
|
57
61
|
"devDependencies": {
|
|
58
|
-
"@askrjs/askr": "
|
|
62
|
+
"@askrjs/askr": "^0.0.66",
|
|
59
63
|
"@askrjs/charts": ">=0.1.0 <0.2.0",
|
|
60
64
|
"@askrjs/logos": ">=0.0.3 <0.1.0",
|
|
61
65
|
"@askrjs/lucide": ">=0.0.3 <0.1.0",
|
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
import { t as parseDependencySpecification } from "./specification-DXnDOC-0.js";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
4
|
-
import { execFile } from "node:child_process";
|
|
5
|
-
import semver from "semver";
|
|
6
|
-
//#region src/update/registry.ts
|
|
7
|
-
function installedNpmCli(executable) {
|
|
8
|
-
const directories = /* @__PURE__ */ new Set([path.dirname(process.execPath)]);
|
|
9
|
-
if (executable && path.isAbsolute(executable)) directories.add(path.dirname(executable));
|
|
10
|
-
for (const directory of directories) for (const candidate of [path.join(directory, "node_modules", "npm", "bin", "npm-cli.js"), path.resolve(directory, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js")]) if (existsSync(candidate)) return candidate;
|
|
11
|
-
return null;
|
|
12
|
-
}
|
|
13
|
-
function npmInvocation(env) {
|
|
14
|
-
const executable = env.npm_execpath;
|
|
15
|
-
const executableName = executable ? path.basename(executable).toLowerCase() : "";
|
|
16
|
-
const isNpmExecutable = executableName === "npm" || executableName === "npm.cmd" || /^npm(?:-cli)?\.(?:cjs|js|mjs)$/.test(executableName);
|
|
17
|
-
if (executable && isNpmExecutable) {
|
|
18
|
-
const extension = path.extname(executable).toLowerCase();
|
|
19
|
-
if ([
|
|
20
|
-
".cjs",
|
|
21
|
-
".js",
|
|
22
|
-
".mjs"
|
|
23
|
-
].includes(extension)) return {
|
|
24
|
-
executable: process.execPath,
|
|
25
|
-
prefix: [executable]
|
|
26
|
-
};
|
|
27
|
-
const npmCli = installedNpmCli(executable);
|
|
28
|
-
return npmCli ? {
|
|
29
|
-
executable: process.execPath,
|
|
30
|
-
prefix: [npmCli]
|
|
31
|
-
} : {
|
|
32
|
-
executable,
|
|
33
|
-
prefix: []
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
const npmCli = installedNpmCli();
|
|
37
|
-
if (npmCli) return {
|
|
38
|
-
executable: process.execPath,
|
|
39
|
-
prefix: [npmCli]
|
|
40
|
-
};
|
|
41
|
-
return {
|
|
42
|
-
executable: process.platform === "win32" ? "npm.cmd" : "npm",
|
|
43
|
-
prefix: []
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
function executionEnvironment(env) {
|
|
47
|
-
if (env === process.env) return { ...process.env };
|
|
48
|
-
const inherited = { ...process.env };
|
|
49
|
-
for (const name of Object.keys(inherited)) if (/^npm_config_/i.test(name)) delete inherited[name];
|
|
50
|
-
return {
|
|
51
|
-
...inherited,
|
|
52
|
-
...env
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
async function loadNpmConfiguration(root, env = process.env) {
|
|
56
|
-
const effectiveEnvironment = executionEnvironment(env);
|
|
57
|
-
return {
|
|
58
|
-
cwd: root,
|
|
59
|
-
env: effectiveEnvironment,
|
|
60
|
-
invocation: npmInvocation(effectiveEnvironment)
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
function npmError(error, stderr) {
|
|
64
|
-
const details = `${String(error.code ?? "")}\n${stderr}`;
|
|
65
|
-
const safe = /* @__PURE__ */ new Error("npm registry lookup failed");
|
|
66
|
-
safe.code = (details.match(/\bE(?:401|403|404|CONNRESET|TIMEDOUT|TIMEOUT)\b/i)?.[0])?.toUpperCase() ?? (typeof error.code === "string" ? error.code : "");
|
|
67
|
-
if (safe.code === "E401") safe.statusCode = 401;
|
|
68
|
-
if (safe.code === "E403") safe.statusCode = 403;
|
|
69
|
-
if (safe.code === "E404") safe.statusCode = 404;
|
|
70
|
-
return safe;
|
|
71
|
-
}
|
|
72
|
-
function executeNpmJson(args, configuration, preferOnline) {
|
|
73
|
-
const [command, ...positionals] = args;
|
|
74
|
-
return new Promise((resolve, reject) => {
|
|
75
|
-
execFile(configuration.invocation.executable, [
|
|
76
|
-
...configuration.invocation.prefix,
|
|
77
|
-
command,
|
|
78
|
-
"--json",
|
|
79
|
-
preferOnline ? "--prefer-online" : "--prefer-offline",
|
|
80
|
-
"--offline=false",
|
|
81
|
-
"--loglevel=error",
|
|
82
|
-
"--update-notifier=false",
|
|
83
|
-
"--",
|
|
84
|
-
...positionals
|
|
85
|
-
], {
|
|
86
|
-
cwd: configuration.cwd,
|
|
87
|
-
encoding: "utf8",
|
|
88
|
-
env: configuration.env,
|
|
89
|
-
maxBuffer: 64 * 1024 * 1024,
|
|
90
|
-
windowsHide: true
|
|
91
|
-
}, (error, stdout, stderr) => {
|
|
92
|
-
if (error) {
|
|
93
|
-
reject(npmError(error, stderr));
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
try {
|
|
97
|
-
resolve(JSON.parse(stdout));
|
|
98
|
-
} catch {
|
|
99
|
-
reject(/* @__PURE__ */ new Error("npm registry returned malformed JSON"));
|
|
100
|
-
}
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
function isRecord(value) {
|
|
105
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
106
|
-
}
|
|
107
|
-
function basePackument(value) {
|
|
108
|
-
if (!isRecord(value) || !Array.isArray(value.versions) || !isRecord(value["dist-tags"])) return null;
|
|
109
|
-
const versions = value.versions.filter((version) => typeof version === "string");
|
|
110
|
-
if (versions.length === 0) return null;
|
|
111
|
-
return {
|
|
112
|
-
"dist-tags": value["dist-tags"],
|
|
113
|
-
versions: Object.fromEntries(versions.map((version) => [version, { version }]))
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
function selectedVersions(packument, specifications) {
|
|
117
|
-
const versions = Object.keys(packument.versions ?? {}).filter((version) => semver.valid(version) !== null);
|
|
118
|
-
const selected = new Set(Object.values(packument["dist-tags"] ?? {}).filter((version) => typeof version === "string" && semver.valid(version) !== null && versions.includes(version)));
|
|
119
|
-
for (const specification of specifications) {
|
|
120
|
-
const parsed = parseDependencySpecification(specification);
|
|
121
|
-
if (parsed.type === "tag") {
|
|
122
|
-
const tagged = packument["dist-tags"]?.[parsed.rawSpec];
|
|
123
|
-
if (typeof tagged === "string" && versions.includes(tagged)) selected.add(tagged);
|
|
124
|
-
} else if (parsed.type === "version") {
|
|
125
|
-
if (versions.includes(parsed.rawSpec)) selected.add(parsed.rawSpec);
|
|
126
|
-
} else if (parsed.type === "range") {
|
|
127
|
-
const matching = semver.maxSatisfying(versions, parsed.rawSpec);
|
|
128
|
-
if (matching) selected.add(matching);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
return [...selected].sort(semver.compare);
|
|
132
|
-
}
|
|
133
|
-
function candidateVersions(packument, specifications) {
|
|
134
|
-
const versions = Object.keys(packument.versions ?? {}).filter((version) => semver.valid(version) !== null);
|
|
135
|
-
const selected = selectedVersions(packument, specifications);
|
|
136
|
-
const lowerBounds = specifications.flatMap((specification) => {
|
|
137
|
-
const parsed = parseDependencySpecification(specification);
|
|
138
|
-
if (parsed.type === "version") return semver.valid(parsed.rawSpec) ? [parsed.rawSpec] : [];
|
|
139
|
-
if (parsed.type === "range") {
|
|
140
|
-
const current = semver.maxSatisfying(versions, parsed.rawSpec);
|
|
141
|
-
return current ? [current] : [];
|
|
142
|
-
}
|
|
143
|
-
return [];
|
|
144
|
-
});
|
|
145
|
-
if (lowerBounds.length === 0 || selected.length === 0) return selected;
|
|
146
|
-
const floor = lowerBounds.sort(semver.compare)[0];
|
|
147
|
-
const ceiling = selected.sort(semver.compare)[selected.length - 1];
|
|
148
|
-
return versions.filter((version) => semver.gte(version, floor) && semver.lte(version, ceiling));
|
|
149
|
-
}
|
|
150
|
-
function mergeVersionMetadata(packument, value, selected) {
|
|
151
|
-
const entries = Array.isArray(value) ? value : [value];
|
|
152
|
-
const merged = /* @__PURE__ */ new Set();
|
|
153
|
-
for (const entry of entries) {
|
|
154
|
-
if (typeof entry === "string") {
|
|
155
|
-
if (packument.versions?.[entry]) merged.add(entry);
|
|
156
|
-
continue;
|
|
157
|
-
}
|
|
158
|
-
if (!isRecord(entry) || typeof entry.version !== "string") continue;
|
|
159
|
-
const metadata = packument.versions?.[entry.version];
|
|
160
|
-
if (!isRecord(metadata)) continue;
|
|
161
|
-
if (isRecord(entry.peerDependencies)) metadata.peerDependencies = entry.peerDependencies;
|
|
162
|
-
if (isRecord(entry.peerDependenciesMeta)) metadata.peerDependenciesMeta = entry.peerDependenciesMeta;
|
|
163
|
-
merged.add(entry.version);
|
|
164
|
-
}
|
|
165
|
-
return selected.every((version) => merged.has(version));
|
|
166
|
-
}
|
|
167
|
-
async function executeNpmView(packageName, configuration, specifications) {
|
|
168
|
-
const packument = basePackument(await executeNpmJson([
|
|
169
|
-
"view",
|
|
170
|
-
packageName,
|
|
171
|
-
"versions",
|
|
172
|
-
"dist-tags"
|
|
173
|
-
], configuration, true));
|
|
174
|
-
if (!packument) return null;
|
|
175
|
-
const versions = candidateVersions(packument, specifications);
|
|
176
|
-
if (versions.length === 0) return packument;
|
|
177
|
-
return mergeVersionMetadata(packument, await executeNpmJson([
|
|
178
|
-
"view",
|
|
179
|
-
`${packageName}@${versions.join(" || ")}`,
|
|
180
|
-
"version",
|
|
181
|
-
"peerDependencies",
|
|
182
|
-
"peerDependenciesMeta"
|
|
183
|
-
], configuration, false), versions) ? packument : null;
|
|
184
|
-
}
|
|
185
|
-
function isPackument(value) {
|
|
186
|
-
return isRecord(value) && isRecord(value["dist-tags"]) && isRecord(value.versions);
|
|
187
|
-
}
|
|
188
|
-
function sanitizeRegistryError(error) {
|
|
189
|
-
const value = error && typeof error === "object" ? error : {};
|
|
190
|
-
if (value.statusCode === 404 || value.code === "E404") return "package was not found in the selected registry";
|
|
191
|
-
if (value.statusCode === 401 || value.statusCode === 403 || value.code === "E401" || value.code === "E403") return "registry authentication or authorization failed";
|
|
192
|
-
const code = typeof value.code === "string" ? value.code.toUpperCase() : "";
|
|
193
|
-
if ([
|
|
194
|
-
"ETIMEDOUT",
|
|
195
|
-
"ETIMEOUT",
|
|
196
|
-
"FETCH_ERROR",
|
|
197
|
-
"ECONNRESET"
|
|
198
|
-
].includes(code)) return "registry request timed out or was interrupted";
|
|
199
|
-
if (/^[A-Z][A-Z0-9_]{1,30}$/.test(code)) return `registry request failed (${code})`;
|
|
200
|
-
return "registry request failed";
|
|
201
|
-
}
|
|
202
|
-
async function fetchPackuments(packageNames, configuration, options = {}) {
|
|
203
|
-
const names = [...new Set(packageNames)].sort((left, right) => left.localeCompare(right));
|
|
204
|
-
const packuments = /* @__PURE__ */ new Map();
|
|
205
|
-
const failures = /* @__PURE__ */ new Map();
|
|
206
|
-
const viewPackage = options.viewPackage ?? executeNpmView;
|
|
207
|
-
let nextIndex = 0;
|
|
208
|
-
const worker = async () => {
|
|
209
|
-
while (nextIndex < names.length) {
|
|
210
|
-
const packageName = names[nextIndex];
|
|
211
|
-
nextIndex += 1;
|
|
212
|
-
try {
|
|
213
|
-
const result = await viewPackage(packageName, configuration, options.requirements?.specifications?.get(packageName) ?? []);
|
|
214
|
-
if (isPackument(result)) packuments.set(packageName, result);
|
|
215
|
-
else failures.set(packageName, "registry returned malformed package metadata");
|
|
216
|
-
} catch (error) {
|
|
217
|
-
failures.set(packageName, sanitizeRegistryError(error));
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
};
|
|
221
|
-
await Promise.all(Array.from({ length: Math.min(8, names.length) }, worker));
|
|
222
|
-
return {
|
|
223
|
-
packuments,
|
|
224
|
-
failures
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
//#endregion
|
|
228
|
-
export { fetchPackuments, loadNpmConfiguration };
|