@wirecat/cli-base 0.63.0 → 0.64.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +113 -0
- package/dist/codegen/discovery.d.ts +3 -3
- package/dist/codegen/discovery.js +18 -9
- package/dist/codegen/index.d.ts +3 -1
- package/dist/codegen/index.js +3 -1
- package/dist/codegen/json-schema.d.ts +8 -0
- package/dist/codegen/json-schema.js +78 -0
- package/dist/commands/index.d.ts +1 -1
- package/dist/commands/index.js +1 -1
- package/dist/credentials.d.ts +1 -1
- package/dist/dev/bundle.d.ts +25 -0
- package/dist/dev/bundle.js +271 -0
- package/dist/dev/main.js +48 -1
- package/dist/skill/index.d.ts +1 -1
- package/dist/skill/index.js +2 -2
- package/dist/standards/audit.js +1 -1
- package/dist/standards/flags-calendar.d.ts +2 -0
- package/dist/standards/flags-calendar.js +59 -0
- package/dist/standards/flags-how.js +7 -1
- package/dist/standards/vocabulary.js +10 -1
- package/docs/standards/README.md +3 -3
- package/docs/standards/accounts.md +2 -2
- package/docs/standards/ci.md +33 -12
- package/docs/standards/commands.md +3 -2
- package/docs/standards/kit-surface.md +3 -3
- package/docs/standards/layout.md +32 -0
- package/docs/standards/new-cli.md +28 -14
- package/docs/standards/permissions.md +33 -3
- package/docs/standards/releases.md +13 -3
- package/docs/standards/security.md +4 -3
- package/docs/standards/store.md +1 -1
- package/docs/standards/testing.md +9 -3
- package/docs/standards/toolchain.md +17 -16
- package/package.json +6 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,119 @@ Versions before 0.8.0 are in the [git tags](https://github.com/WireCatLabs/cli-b
|
|
|
8
8
|
Every entry says what changed as a caller sees it, why, and what to watch for — the rules are
|
|
9
9
|
[`docs/dev/CONVENTIONS.md`](docs/dev/CONVENTIONS.md#the-changelog).
|
|
10
10
|
|
|
11
|
+
## 0.64.0 — 27.10.2026
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`cli-dev bundle --bin dist/bin/<cli>.js` builds the published bin as an esbuild bundle, the way tg-cli 0.100.0
|
|
16
|
+
ships.** It moves tg's `scripts/bundle.ts` into cli-base so every CLI builds the same way: code splitting that keeps
|
|
17
|
+
each lazy `import()` lazy; `import.meta.url` and run-time package lookups that stay at the original module's place
|
|
18
|
+
in the CLI, cli-core and cli-base; CommonJS named exports for a dynamic `import("yaml")`; kept names; native and
|
|
19
|
+
by-name packages left outside. The bin becomes a loader that answers `--version`, turns the compile cache on and,
|
|
20
|
+
under `serve`, `watch` and `mcp`, preloads every chunk so a running server survives an in-place upgrade. The build
|
|
21
|
+
fails when a bundled module would look in the wrong place. Add `esbuild` (an optional peer, `^0.28.0`) as a
|
|
22
|
+
devDependency and name the CLI's own outside packages with `--external`. The rule is
|
|
23
|
+
[WC-LAY-10](docs/standards/layout.md#wc-lay-10--ship-the-bin-as-a-bundle-that-cli-dev-bundle-builds).
|
|
24
|
+
- **WC-PERM-22 on the [Permissions](docs/standards/permissions.md) page: a CLI that is not a messenger declares its
|
|
25
|
+
own permission keys and defaults with cli-core's `commandPermissions`** (cli-core 0.450.0). It says how such a CLI
|
|
26
|
+
keys its commands, picks built-in defaults for what its writes do, and runs writes through the read-only lock and the
|
|
27
|
+
write guard. The example is a calendar: event writes ask, deleting a whole series and mailing attendees are denied
|
|
28
|
+
until the owner allows them. The page covered messenger CLIs only, and WC-PERM-6's "ask only before what cannot be
|
|
29
|
+
undone" read as if it applied to a calendar too; it now says it is the messengers' list. drive, zoom and memo do
|
|
30
|
+
not declare `commandPermissions` yet; the rule names them as known gaps.
|
|
31
|
+
- **`cli-dev audit` compares gcal (`@wirecat/gcal-cli`, the Google Calendar CLI) in the `all` parity group**, next to
|
|
32
|
+
tg, max, memo, drive, zm and wa, so its shared root commands are held to the same options and summaries. A run
|
|
33
|
+
without gcal's command tree skips it, as before; the weekly audit adds it once gcal is published on npm.
|
|
34
|
+
- **The command vocabulary has the words a calendar CLI needs.** Verbs `unsync` (stop syncing a thing; what is
|
|
35
|
+
stored stays), `review` (list what waits for the owner's decision) and `resolve` (settle one such thing with the
|
|
36
|
+
owner's choice); collections `calendars`, `events` and `people`. `events` was only a view word, so a command group
|
|
37
|
+
named `events` was reported by `unknown-resource`.
|
|
38
|
+
- **The vocabulary has the flags and the verb of calendar writes** (cli-core's `events create | update | delete |
|
|
39
|
+
respond`). Flags `--end-time`, `--start-date`, `--end-date`, `--location`, `--attendee` (repeat it for more),
|
|
40
|
+
`--id` (the id a create is made with) as `value`; `--calendar` and `--series` as `target`; `--no-notify` as `mode`.
|
|
41
|
+
`--notify` now means telling the people a change concerns — a chat's members or an event's attendees. Verb
|
|
42
|
+
`respond` (answer an invitation). Without them `option-role` and `unknown-verb` reported every calendar CLI's
|
|
43
|
+
write commands.
|
|
44
|
+
- **The vocabulary has the words of free/busy and repeating events** (cli-core's `calendars busy` and
|
|
45
|
+
`events create | update --repeat`). View `busy` (when calendars or people are taken, without what takes them);
|
|
46
|
+
flags `--repeat <rule>` (an RFC 5545 rule) as `value` and `--hours <from-to>` (the working hours to look in) as
|
|
47
|
+
`target`. gcal-cli overrides the four lint reports they cause until it takes this release.
|
|
48
|
+
|
|
49
|
+
- **`requestSchema(operation, definitions)` in `@wirecat/cli-base/codegen` builds the JSON Schema of a generated
|
|
50
|
+
operation's request body**, every referenced definition under `$defs`, or returns `null` when the operation takes
|
|
51
|
+
no body. drive-cli and gcal-cli each kept an identical copy for `api show`. Pass a manifest operation and the
|
|
52
|
+
`definitions` that `definitionsGenerator` wrote.
|
|
53
|
+
|
|
54
|
+
### Changed — may break callers
|
|
55
|
+
|
|
56
|
+
- **A lint baseline entry or override for a word that is now in the vocabulary goes stale, and `cli-dev lint` fails
|
|
57
|
+
on it.** This release adds `unsync`, `review`, `resolve`, `calendars`, `events` and `people`. zoom-cli lists
|
|
58
|
+
`events` in `lint/baseline/unknown-resource.json`; gcal-cli lists `calendars`, `events`, `people`,
|
|
59
|
+
`calendars unsync`, `links review` and `links resolve` in its baseline; drive-cli overrides `unknown-verb` for
|
|
60
|
+
`comments resolve`, and cli-memo for `search people`, in `lint/overrides.json`. Remove those entries in the same
|
|
61
|
+
change that takes this version.
|
|
62
|
+
- **The calendar write words make gcal-cli's write baseline stale too.** Its `lint/baseline/option-role.json` (20
|
|
63
|
+
entries under `events create | update | delete | respond`) and `events respond` in `unknown-verb.json` go stale
|
|
64
|
+
with this release; remove them when gcal takes it. No other CLI's baseline or overrides on `main` name these words.
|
|
65
|
+
- **The free/busy and repeat words make four of gcal-cli's overrides stale.** `lint/overrides.json` names
|
|
66
|
+
`calendars busy` (`unknown-verb`), `calendars busy --hours` and `events create | update --repeat` (`option-role`);
|
|
67
|
+
remove them when gcal takes this release.
|
|
68
|
+
|
|
69
|
+
- **`describeAlone` is no longer exported from `@wirecat/cli-base/commands`.** It was made public by accident in
|
|
70
|
+
0.62.0; it is a helper of the shared-command check and only works with a map kept for one check. No known caller
|
|
71
|
+
imports it. Describe a whole tree with `describeProgram` instead.
|
|
72
|
+
|
|
73
|
+
### Fixed
|
|
74
|
+
|
|
75
|
+
- **Handbook corrections from gcal-cli's first-release audit.**
|
|
76
|
+
- The Toolchain, CI, Security, Testing and Store pages listed "the ten repositories" and "the six CLIs" without
|
|
77
|
+
gcal. They now say "the WireCat repositories": every WireCat CLI, a new one from its first commit, and the shared
|
|
78
|
+
repositories. `gcal-cli` is a known repository name for "Enforced by".
|
|
79
|
+
- WC-SECR-4 said the keyring service is the command's name. It is the CLI's app name (`max-cli`, `gcal-cli`,
|
|
80
|
+
`cli-memo`), as cli-core passes it.
|
|
81
|
+
- WC-REL-16 says what a CLI's package `files` holds: `dist`, `skills`, `README.md`, `LICENSE` and what the CLI
|
|
82
|
+
reads or runs after install (`scenarios`, `install`, notices); `CHANGELOG.md` may ship. Never `src/`, tests,
|
|
83
|
+
scripts, workflows, `bin/release`, `config/`, `lint/`, `contracts/` or `template.json`.
|
|
84
|
+
- WC-REL-11 names its one exception: a package's first version, published by the owner with `bin/release --local`,
|
|
85
|
+
since npm sets a trusted publisher only for a package that exists. Step 2 of
|
|
86
|
+
[New CLI](docs/standards/new-cli.md) says the same.
|
|
87
|
+
- [New CLI](docs/standards/new-cli.md) says which copied files a CLI rewrites (seeds) and which it leaves to
|
|
88
|
+
cli-template (template-owned), and that a template-owned page wrong for the CLI is fixed in cli-template.
|
|
89
|
+
- "Done when" told authors to add a new CLI's shared audit findings to `audit/baseline`, which has not accepted
|
|
90
|
+
them since cli-base 0.51.0. It now says to fix each one, or declare an exact difference in the parity group.
|
|
91
|
+
- WC-CI-8 says the fresh-agent audit is given the pinned cli-base workflows.
|
|
92
|
+
- WC-TEST-12 has one exception: commands generated from an API description share one reason that names an
|
|
93
|
+
options-against-the-snapshot test and a test that drives a generated command through `run()`.
|
|
94
|
+
|
|
95
|
+
- **`syncDiscovery` writes the same snapshot for the same revision of a Google API.** Google serves one revision with
|
|
96
|
+
its object keys in a different order on each download, so two syncs gave different files, hashes and generated
|
|
97
|
+
code. The snapshot is now the parsed document with its keys sorted and 2-space indentation; values and array order
|
|
98
|
+
are unchanged. `sha256` is stable; `sourceSha256` is still the hash of the bytes Google served, so it can change
|
|
99
|
+
between syncs. The first sync after upgrading rewrites each snapshot once, and `pnpm generate` then reorders the
|
|
100
|
+
generated types, schemas and manifest once.
|
|
101
|
+
- **`skill show` now names every agent `skill install` serves: Claude Code, Codex, Gemini CLI, OpenClaw, and Hermes
|
|
102
|
+
on request.** Its help named only the first three, so OpenClaw and Hermes users could not tell the skill was meant
|
|
103
|
+
for them.
|
|
104
|
+
- **The [CI](docs/standards/ci.md) page lists the monthly cron minutes WireCat repositories already use**, under
|
|
105
|
+
WC-CI-9, and [New CLI](docs/standards/new-cli.md) step 3 points to it. Finding a free minute meant reading
|
|
106
|
+
`monthly.yml` in every repository. A new CLI adds its minute to the list in the same pull request that joins the
|
|
107
|
+
parity groups.
|
|
108
|
+
- **The [Kit surface](docs/standards/kit-surface.md) page lists every cli-core and cli-base export cli-template
|
|
109
|
+
0.7.0 imports**, among them the ones for MCP over HTTP: `mcpServerCommand`, `serveOverHttp`, `httpLockFile` and
|
|
110
|
+
the rest. The page left them out, so a change to them would not have been seen as one that reaches every new CLI.
|
|
111
|
+
- **[New CLI](docs/standards/new-cli.md) lets the fresh audit agent read the cli-base workflow files a CLI's
|
|
112
|
+
workflows pin.** WC-CI-8 tells an audit to read them, but the audit was limited to the handbook and the
|
|
113
|
+
repository. A release audit prompt kept in a CLI repository needs the same change.
|
|
114
|
+
- **WC-TEST-5 on the [Testing](docs/standards/testing.md) page says that a test may wait for a local process or
|
|
115
|
+
server it started**, polling with a timeout shorter than the test's own; a fixed sleep is still not allowed. It
|
|
116
|
+
was unclear whether such a wait counts as waiting for real time.
|
|
117
|
+
- **WC-CMD-6 on the [Commands](docs/standards/commands.md) page names `mcp --revoke` as an exception.** The rule says
|
|
118
|
+
an option never turns a command into a different action, and `--revoke` does: it forgets every browser login
|
|
119
|
+
instead of starting a server. The owner kept it an option on `mcp` because it belongs with the HTTP server's login
|
|
120
|
+
state. No other option gets this exception.
|
|
121
|
+
- **The handbook's examples start `gcal-cli` with the command `gcal`, not `cal`**, the system calendar program on macOS
|
|
122
|
+
and many Linux systems. A CLI named after it would shadow it.
|
|
123
|
+
|
|
11
124
|
## 0.63.0 — 27.10.2026
|
|
12
125
|
|
|
13
126
|
### Added
|
|
@@ -33,7 +33,7 @@ export interface DiscoveryProvenance {
|
|
|
33
33
|
version: string;
|
|
34
34
|
/** Of the committed file; `readDiscovery` refuses a snapshot that no longer matches it. */
|
|
35
35
|
sha256: string;
|
|
36
|
-
/** Of the bytes Google served.
|
|
36
|
+
/** Of the bytes Google served. Google varies its key order, so this one can change while `sha256` does not. */
|
|
37
37
|
sourceSha256: string;
|
|
38
38
|
normalization: string;
|
|
39
39
|
}
|
|
@@ -43,8 +43,8 @@ export interface SyncDiscoveryOptions {
|
|
|
43
43
|
fetch?: typeof globalThis.fetch;
|
|
44
44
|
}
|
|
45
45
|
/**
|
|
46
|
-
* Downloads each Discovery document
|
|
47
|
-
* the diff: the build never fetches. `provenance.json` keeps the order of `sources`, and that order is
|
|
46
|
+
* Downloads each Discovery document, sorts its object keys so the same revision always gives the same file, and
|
|
47
|
+
* records where it came from. Run by hand, then review the diff: the build never fetches. `provenance.json` keeps the order of `sources`, and that order is
|
|
48
48
|
* the order of the generated schemas.
|
|
49
49
|
*/
|
|
50
50
|
export declare const syncDiscovery: (sources: Readonly<Record<string, string>>, options: SyncDiscoveryOptions) => Promise<Record<string, DiscoveryProvenance>>;
|
|
@@ -240,9 +240,18 @@ export const adaptDiscovery = (documents, options = {}) => {
|
|
|
240
240
|
};
|
|
241
241
|
};
|
|
242
242
|
const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
243
|
+
/** Google serves the same revision with its keys in a different order each time; arrays keep theirs. */
|
|
244
|
+
const sortKeys = (value) => Array.isArray(value)
|
|
245
|
+
? value.map(sortKeys)
|
|
246
|
+
: object(value)
|
|
247
|
+
? Object.fromEntries(Object.keys(value)
|
|
248
|
+
.sort()
|
|
249
|
+
.map((key) => [key, sortKeys(value[key])]))
|
|
250
|
+
: value;
|
|
251
|
+
const NORMALIZATION = "object keys sorted, 2-space JSON; values and array order unchanged";
|
|
243
252
|
/**
|
|
244
|
-
* Downloads each Discovery document
|
|
245
|
-
* the diff: the build never fetches. `provenance.json` keeps the order of `sources`, and that order is
|
|
253
|
+
* Downloads each Discovery document, sorts its object keys so the same revision always gives the same file, and
|
|
254
|
+
* records where it came from. Run by hand, then review the diff: the build never fetches. `provenance.json` keeps the order of `sources`, and that order is
|
|
246
255
|
* the order of the generated schemas.
|
|
247
256
|
*/
|
|
248
257
|
export const syncDiscovery = async (sources, options) => {
|
|
@@ -257,20 +266,20 @@ export const syncDiscovery = async (sources, options) => {
|
|
|
257
266
|
const document = JSON.parse(raw);
|
|
258
267
|
if (!object(document) || document.name !== service)
|
|
259
268
|
throw new Error(`${source} is not the Discovery document of ${service}`);
|
|
260
|
-
const
|
|
261
|
-
downloaded[service] =
|
|
269
|
+
const written = `${JSON.stringify(sortKeys(document), null, 2)}\n`;
|
|
270
|
+
downloaded[service] = written;
|
|
262
271
|
provenance[service] = {
|
|
263
272
|
source,
|
|
264
273
|
revision: String(document.revision),
|
|
265
274
|
version: String(document.version),
|
|
266
|
-
sha256:
|
|
267
|
-
sourceSha256:
|
|
268
|
-
normalization:
|
|
275
|
+
sha256: sha256(written),
|
|
276
|
+
sourceSha256: sha256(raw),
|
|
277
|
+
normalization: NORMALIZATION,
|
|
269
278
|
};
|
|
270
279
|
}
|
|
271
280
|
mkdirSync(options.dir, { recursive: true });
|
|
272
|
-
for (const [service,
|
|
273
|
-
writeFileSync(join(options.dir, `${service}.json`),
|
|
281
|
+
for (const [service, text] of Object.entries(downloaded))
|
|
282
|
+
writeFileSync(join(options.dir, `${service}.json`), text);
|
|
274
283
|
writeFileSync(join(options.dir, "provenance.json"), `${JSON.stringify(provenance, null, 2)}\n`);
|
|
275
284
|
return provenance;
|
|
276
285
|
};
|
package/dist/codegen/index.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Turns an official API description into committed code: types, Valibot schemas, an operation
|
|
3
|
-
* manifest and a coverage page. Build-time
|
|
3
|
+
* manifest and a coverage page. Build-time, apart from the small helpers a CLI reads the generated
|
|
4
|
+
* files with (`identifier`, `requestSchema`), and format-neutral: it parses no file format and
|
|
4
5
|
* adds no dependency. `adaptOpenApi` reads an OpenAPI document and `adaptDiscovery` Google Discovery
|
|
5
6
|
* documents the consumer has parsed; an adapter from Postman or anything else lives in the consumer.
|
|
6
7
|
*/
|
|
7
8
|
export { adaptDiscovery, type DiscoveryMetadata, type DiscoveryOptions, type DiscoveryProvenance, type DiscoveryResult, type DiscoveryService, readDiscovery, type SyncDiscoveryOptions, syncDiscovery, } from "./discovery.js";
|
|
9
|
+
export { requestSchema } from "./json-schema.js";
|
|
8
10
|
export { type CoverageOptions, coverageGenerator, definitionsGenerator, type ManifestOperation, manifestGenerator, } from "./manifest.js";
|
|
9
11
|
export type { ApiBody, ApiModel, ApiOperation, ApiParameter, ApiSchema, Binding, CodeGenerator, Confidence, Discriminator, Effect, GeneratedArtifact, HttpBinding, HttpMethod, Override, RpcBinding, SchemaNode, SourceInfo, SourceReference, } from "./model.js";
|
|
10
12
|
export { identifier, kebab } from "./names.js";
|
package/dist/codegen/index.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Turns an official API description into committed code: types, Valibot schemas, an operation
|
|
3
|
-
* manifest and a coverage page. Build-time
|
|
3
|
+
* manifest and a coverage page. Build-time, apart from the small helpers a CLI reads the generated
|
|
4
|
+
* files with (`identifier`, `requestSchema`), and format-neutral: it parses no file format and
|
|
4
5
|
* adds no dependency. `adaptOpenApi` reads an OpenAPI document and `adaptDiscovery` Google Discovery
|
|
5
6
|
* documents the consumer has parsed; an adapter from Postman or anything else lives in the consumer.
|
|
6
7
|
*/
|
|
7
8
|
export { adaptDiscovery, readDiscovery, syncDiscovery, } from "./discovery.js";
|
|
9
|
+
export { requestSchema } from "./json-schema.js";
|
|
8
10
|
export { coverageGenerator, definitionsGenerator, manifestGenerator, } from "./manifest.js";
|
|
9
11
|
export { identifier, kebab } from "./names.js";
|
|
10
12
|
export { adaptOpenApi, OpenApiError } from "./openapi.js";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ManifestOperation } from "./manifest.js";
|
|
2
|
+
import type { SchemaNode } from "./model.js";
|
|
3
|
+
/**
|
|
4
|
+
* The JSON Schema (draft 2020-12) of an operation's request body, built from the `definitions` that
|
|
5
|
+
* `definitionsGenerator` wrote, with every referenced definition under `$defs`; `null` when the operation takes no
|
|
6
|
+
* body. For a CLI's `api show`, so an agent sees what to send before it calls.
|
|
7
|
+
*/
|
|
8
|
+
export declare const requestSchema: (operation: Pick<ManifestOperation, "request">, definitions: Readonly<Record<string, SchemaNode>>) => Record<string, unknown> | null;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { identifier } from "./names.js";
|
|
2
|
+
/**
|
|
3
|
+
* The JSON Schema (draft 2020-12) of an operation's request body, built from the `definitions` that
|
|
4
|
+
* `definitionsGenerator` wrote, with every referenced definition under `$defs`; `null` when the operation takes no
|
|
5
|
+
* body. For a CLI's `api show`, so an agent sees what to send before it calls.
|
|
6
|
+
*/
|
|
7
|
+
export const requestSchema = (operation, definitions) => {
|
|
8
|
+
if (!operation.request?.schema)
|
|
9
|
+
return null;
|
|
10
|
+
const defs = {};
|
|
11
|
+
const visiting = new Set();
|
|
12
|
+
const node = (value) => {
|
|
13
|
+
const common = { ...(value.description ? { description: value.description } : {}) };
|
|
14
|
+
switch (value.type) {
|
|
15
|
+
case "ref": {
|
|
16
|
+
const name = identifier(value.ref);
|
|
17
|
+
add(name);
|
|
18
|
+
return { ...common, $ref: `#/$defs/${name}` };
|
|
19
|
+
}
|
|
20
|
+
case "unknown":
|
|
21
|
+
return common;
|
|
22
|
+
case "union":
|
|
23
|
+
return { ...common, anyOf: value.of.map(node) };
|
|
24
|
+
case "allOf":
|
|
25
|
+
return { ...common, allOf: value.of.map(node) };
|
|
26
|
+
case "object":
|
|
27
|
+
return {
|
|
28
|
+
...common,
|
|
29
|
+
type: "object",
|
|
30
|
+
properties: Object.fromEntries(Object.entries(value.properties).map(([key, item]) => [key, node(item)])),
|
|
31
|
+
required: value.required,
|
|
32
|
+
additionalProperties: value.additionalProperties ? node(value.additionalProperties) : true,
|
|
33
|
+
};
|
|
34
|
+
case "array":
|
|
35
|
+
return {
|
|
36
|
+
...common,
|
|
37
|
+
type: "array",
|
|
38
|
+
items: node(value.items),
|
|
39
|
+
...(value.minItems === undefined ? {} : { minItems: value.minItems }),
|
|
40
|
+
...(value.maxItems === undefined ? {} : { maxItems: value.maxItems }),
|
|
41
|
+
};
|
|
42
|
+
case "string":
|
|
43
|
+
return {
|
|
44
|
+
...common,
|
|
45
|
+
type: "string",
|
|
46
|
+
...(value.enum ? { enum: value.enum } : {}),
|
|
47
|
+
...(value.pattern ? { pattern: value.pattern } : {}),
|
|
48
|
+
...(value.minLength === undefined ? {} : { minLength: value.minLength }),
|
|
49
|
+
...(value.maxLength === undefined ? {} : { maxLength: value.maxLength }),
|
|
50
|
+
};
|
|
51
|
+
case "boolean":
|
|
52
|
+
return { ...common, type: "boolean" };
|
|
53
|
+
case "integer":
|
|
54
|
+
case "number":
|
|
55
|
+
return {
|
|
56
|
+
...common,
|
|
57
|
+
type: value.type,
|
|
58
|
+
...(value.minimum === undefined ? {} : { minimum: value.minimum }),
|
|
59
|
+
...(value.maximum === undefined ? {} : { maximum: value.maximum }),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
const add = (name) => {
|
|
64
|
+
if (visiting.has(name))
|
|
65
|
+
return;
|
|
66
|
+
visiting.add(name);
|
|
67
|
+
const value = definitions[name];
|
|
68
|
+
if (!value)
|
|
69
|
+
throw new Error(`generated schema reference is missing: ${name}`);
|
|
70
|
+
defs[name] = node(value);
|
|
71
|
+
};
|
|
72
|
+
add(operation.request.schema);
|
|
73
|
+
return {
|
|
74
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
75
|
+
$ref: `#/$defs/${operation.request.schema}`,
|
|
76
|
+
$defs: defs,
|
|
77
|
+
};
|
|
78
|
+
};
|
package/dist/commands/index.d.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import type { Command } from "commander";
|
|
13
13
|
export { type CommandsDocument, type CommandsDocumentAbout, commandsDocument, commandsDocumentJsonSchema, commandsDocumentSchema, parseCommandsDocument, } from "./document.js";
|
|
14
14
|
export { COMMANDS_PAGE_LABELS, type CommandsPage, type CommandsPageLabels, type CommandsPageText, commandsPage, } from "./page.js";
|
|
15
|
-
export
|
|
15
|
+
export { type ArgumentInfo, annotate, annotateOption, type CommandInfo, type CommandLifetime, type CommandMeta, type CommandOutput, type CommandReach, type CommandState, describeOptions, describeProgram, flatten, metaOf, type OptionInfo, type OptionMeta, type Origin, optionMetaOf, stepTargets, } from "./registry.js";
|
|
16
16
|
/**
|
|
17
17
|
* Refuses a command path that arrived as one argument — `"projects add" --help`, what a zsh loop over
|
|
18
18
|
* `$c` passes. Commander settles `--help` for an unknown word before any hook runs and prints the root
|
package/dist/commands/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CliError } from "../errors.js";
|
|
2
2
|
export { commandsDocument, commandsDocumentJsonSchema, commandsDocumentSchema, parseCommandsDocument, } from "./document.js";
|
|
3
3
|
export { COMMANDS_PAGE_LABELS, commandsPage, } from "./page.js";
|
|
4
|
-
export
|
|
4
|
+
export { annotate, annotateOption, describeOptions, describeProgram, flatten, metaOf, optionMetaOf, stepTargets, } from "./registry.js";
|
|
5
5
|
/**
|
|
6
6
|
* Refuses a command path that arrived as one argument — `"projects add" --help`, what a zsh loop over
|
|
7
7
|
* `$c` passes. Commander settles `--help` for an unknown word before any hook runs and prints the root
|
package/dist/credentials.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export interface StoredCredential {
|
|
|
9
9
|
export interface CredentialsOptions {
|
|
10
10
|
/** Where the fallback file lives when the keyring cannot be used. */
|
|
11
11
|
configDir: string;
|
|
12
|
-
/** The keyring service name — the
|
|
12
|
+
/** The keyring service name — the CLI's app name, e.g. `max-cli`, not its command word. */
|
|
13
13
|
service: string;
|
|
14
14
|
/**
|
|
15
15
|
* **True when the config directory came from the environment rather than the OS convention.**
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface BundleOptions {
|
|
2
|
+
/** The CLI's root: `dist/` and `package.json` are under it. */
|
|
3
|
+
root: string;
|
|
4
|
+
/** The compiled bin, relative to `root`, e.g. `dist/bin/tg.js`. It becomes the loader. */
|
|
5
|
+
bin: string;
|
|
6
|
+
/** Packages that stay outside the bundle, besides `DEFAULT_EXTERNAL`. `*` matches anything. */
|
|
7
|
+
external?: readonly string[];
|
|
8
|
+
/** Packages whose modules keep their installed place, besides `DEFAULT_ANCHORED`. */
|
|
9
|
+
anchored?: readonly string[];
|
|
10
|
+
/** Third-party files read and found harmless, besides `DEFAULT_HARMLESS` (a path suffix each). */
|
|
11
|
+
harmless?: readonly string[];
|
|
12
|
+
}
|
|
13
|
+
export interface BundleResult {
|
|
14
|
+
problems: string[];
|
|
15
|
+
/** The chunks a server preloads, as the loader names them. */
|
|
16
|
+
chunks: string[];
|
|
17
|
+
}
|
|
18
|
+
/** Native modules, Bun's SQLite, and what cli-core loads only by name or starts as a worker from its own file. */
|
|
19
|
+
export declare const DEFAULT_EXTERNAL: string[];
|
|
20
|
+
export declare const DEFAULT_ANCHORED: string[];
|
|
21
|
+
export declare const DEFAULT_HARMLESS: string[];
|
|
22
|
+
export declare const LOADER_MARK = "// cli-dev bundle loader\n";
|
|
23
|
+
/** The bin that replaces the compiled one: `name` is the bundle's entry, `chunks` what a server preloads. */
|
|
24
|
+
export declare const loaderSource: (name: string, version: string, chunks: readonly string[]) => string;
|
|
25
|
+
export declare const bundle: (options: BundleOptions) => Promise<BundleResult>;
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cli-dev bundle`: the published bin starts from an esbuild bundle of `dist/`, so a command loads a few dozen files
|
|
3
|
+
* instead of about a thousand. Moved from tg-cli's `scripts/bundle.ts`, where it was proven first.
|
|
4
|
+
*
|
|
5
|
+
* - `dist/bundle/` holds the bundle. Its chunks keep every lazy `import()` lazy.
|
|
6
|
+
* - The bin becomes a loader: it answers `--version`, turns the compile cache on before any of the bundle compiles,
|
|
7
|
+
* and for a server preloads every chunk so an in-place upgrade cannot pull one from under it.
|
|
8
|
+
* - A module moved into a chunk would look for files and packages from the chunk. In the CLI's own modules and in the
|
|
9
|
+
* anchored packages (cli-core, cli-base), `import.meta.url` becomes the module's own place, and a package they load
|
|
10
|
+
* at run time is resolved from there too: pnpm installs cli-core's dependencies beside cli-core, not beside the CLI.
|
|
11
|
+
* - The build fails when a bundled module would still look in the wrong place.
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { isBuiltin } from "node:module";
|
|
15
|
+
import { basename, dirname, join, relative, sep } from "node:path";
|
|
16
|
+
import { pathToFileURL } from "node:url";
|
|
17
|
+
/** Native modules, Bun's SQLite, and what cli-core loads only by name or starts as a worker from its own file. */
|
|
18
|
+
export const DEFAULT_EXTERNAL = [
|
|
19
|
+
"bun:sqlite",
|
|
20
|
+
"*.node",
|
|
21
|
+
"better-sqlite3",
|
|
22
|
+
"@napi-rs/*",
|
|
23
|
+
"@resvg/*",
|
|
24
|
+
"@huggingface/*",
|
|
25
|
+
"sherpa-onnx*",
|
|
26
|
+
"onnxruntime-*",
|
|
27
|
+
"@wirecat/cli-core-onnx",
|
|
28
|
+
"@wirecat/cli-core-sqlite",
|
|
29
|
+
"ogg-opus-decoder",
|
|
30
|
+
];
|
|
31
|
+
export const DEFAULT_ANCHORED = ["@wirecat/cli-core", "@wirecat/cli-base"];
|
|
32
|
+
// commander and @colors name these in comments and messages; liquidjs resolves partials with explicit `paths`, so the
|
|
33
|
+
// requiring file does not matter.
|
|
34
|
+
export const DEFAULT_HARMLESS = [
|
|
35
|
+
"/commander/lib/command.js",
|
|
36
|
+
"/@colors/colors/lib/colors.js",
|
|
37
|
+
"/liquidjs/dist/liquid.node.js",
|
|
38
|
+
];
|
|
39
|
+
export const LOADER_MARK = "// cli-dev bundle loader\n";
|
|
40
|
+
const posix = (path) => path.split(sep).join("/");
|
|
41
|
+
const matcher = (patterns) => {
|
|
42
|
+
const expressions = patterns.map((pattern) => new RegExp(`^${pattern.replaceAll(".", "\\.").replaceAll("*", ".*")}$`));
|
|
43
|
+
return (specifier) => expressions.some((expression) => expression.test(specifier));
|
|
44
|
+
};
|
|
45
|
+
// Node has no "resolve from this parent" call; a resolve hook for the one call gives Node's own answer. Bun has no
|
|
46
|
+
// hooks but has `Bun.resolveSync(specifier, parent)`; when it finds nothing, the plain lookup keeps Bun's own error.
|
|
47
|
+
const FROM = "cli-dev-bundle:from";
|
|
48
|
+
const FROM_SOURCE = `import * as module from "node:module"
|
|
49
|
+
import { dirname } from "node:path"
|
|
50
|
+
import { fileURLToPath, pathToFileURL } from "node:url"
|
|
51
|
+
export const resolveFrom = (parent, specifier) => {
|
|
52
|
+
if (globalThis.Bun) {
|
|
53
|
+
if (specifier.includes(":")) return import.meta.resolve(specifier)
|
|
54
|
+
try {
|
|
55
|
+
return pathToFileURL(globalThis.Bun.resolveSync(specifier, dirname(fileURLToPath(parent)))).href
|
|
56
|
+
} catch {
|
|
57
|
+
return import.meta.resolve(specifier)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (!module.registerHooks) return import.meta.resolve(specifier)
|
|
61
|
+
const hooks = module.registerHooks({
|
|
62
|
+
resolve: (asked, context, next) => next(asked, asked === specifier ? { ...context, parentURL: parent } : context),
|
|
63
|
+
})
|
|
64
|
+
try {
|
|
65
|
+
return import.meta.resolve(specifier)
|
|
66
|
+
} finally {
|
|
67
|
+
hooks.deregister()
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export const importFrom = async (parent, specifier) => import(resolveFrom(parent, specifier))
|
|
71
|
+
`;
|
|
72
|
+
const BY_NAME = /(?<![\w$.])import\(\s*(?!["'`.])/g;
|
|
73
|
+
const LITERAL = /(?<![\w$.])import\(\s*(["'])([^"'.][^"']*)\1\s*\)/g;
|
|
74
|
+
const ownPlaces = (dist, out, anchored, harmless, isExternal, problems) => {
|
|
75
|
+
const placeOf = (file) => {
|
|
76
|
+
if (file.startsWith(dist + sep))
|
|
77
|
+
return `new URL(${JSON.stringify(posix(relative(out, file)))}, import.meta.url).href`;
|
|
78
|
+
for (const name of anchored) {
|
|
79
|
+
const marker = `${sep}node_modules${sep}${name.replace("/", sep)}${sep}`;
|
|
80
|
+
const at = file.lastIndexOf(marker);
|
|
81
|
+
if (at < 0)
|
|
82
|
+
continue;
|
|
83
|
+
const packageRoot = file.slice(0, at + marker.length);
|
|
84
|
+
const manifest = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
|
|
85
|
+
const entry = manifest.exports?.["."]?.default;
|
|
86
|
+
if (typeof entry !== "string")
|
|
87
|
+
throw new Error(`${name}: package.json has no exports["."].default to anchor on`);
|
|
88
|
+
const path = posix(relative(dirname(join(packageRoot, entry)), file));
|
|
89
|
+
return `new URL(${JSON.stringify(path)}, import.meta.resolve(${JSON.stringify(name)})).href`;
|
|
90
|
+
}
|
|
91
|
+
return undefined;
|
|
92
|
+
};
|
|
93
|
+
return {
|
|
94
|
+
name: "own-places",
|
|
95
|
+
setup(context) {
|
|
96
|
+
context.onResolve({ filter: new RegExp(`^${FROM}$`) }, () => ({ path: FROM, namespace: FROM }));
|
|
97
|
+
context.onLoad({ filter: /.*/, namespace: FROM }, () => ({ contents: FROM_SOURCE, loader: "js" }));
|
|
98
|
+
context.onLoad({ filter: /\.[cm]?js$/ }, (args) => {
|
|
99
|
+
const source = readFileSync(args.path, "utf8");
|
|
100
|
+
const place = placeOf(args.path);
|
|
101
|
+
if (!place) {
|
|
102
|
+
const known = harmless.some((path) => posix(args.path).endsWith(path));
|
|
103
|
+
if (!known && /import\.meta|__dirname|__filename|require\.resolve/.test(source))
|
|
104
|
+
problems.push(args.path);
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
const loadsOutside = [...source.matchAll(LITERAL)].some(([, , specifier]) => specifier && isExternal(specifier));
|
|
108
|
+
if (!source.includes("import.meta") && source.search(BY_NAME) < 0 && !loadsOutside)
|
|
109
|
+
return undefined;
|
|
110
|
+
const other = source.match(/import\.meta\.(?!url\b|resolve\b)\w+/);
|
|
111
|
+
if (other)
|
|
112
|
+
problems.push(`${args.path}: ${other[0]}`);
|
|
113
|
+
// `place` holds `import.meta.url` or `import.meta.resolve`, so it goes in last.
|
|
114
|
+
const contents = source
|
|
115
|
+
.replaceAll(/import\.meta\.resolve\(/g, "resolveFrom(PLACE, ")
|
|
116
|
+
.replaceAll(BY_NAME, "importFrom(PLACE, ")
|
|
117
|
+
.replaceAll(LITERAL, (call, quote, specifier) => isExternal(specifier) ? `importFrom(PLACE, ${quote}${specifier}${quote})` : call)
|
|
118
|
+
.replaceAll(/import\.meta\.url\b/g, "PLACE")
|
|
119
|
+
.replaceAll(/\bPLACE\b/g, place);
|
|
120
|
+
return { contents: `import { importFrom, resolveFrom } from "${FROM}";\n${contents}`, loader: "js" };
|
|
121
|
+
});
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
const isCommonJs = (file) => {
|
|
126
|
+
if (file.endsWith(".cjs"))
|
|
127
|
+
return true;
|
|
128
|
+
if (!file.endsWith(".js"))
|
|
129
|
+
return false;
|
|
130
|
+
for (let directory = dirname(file); directory !== dirname(directory); directory = dirname(directory)) {
|
|
131
|
+
const manifest = join(directory, "package.json");
|
|
132
|
+
if (existsSync(manifest))
|
|
133
|
+
return JSON.parse(readFileSync(manifest, "utf8")).type !== "module";
|
|
134
|
+
}
|
|
135
|
+
return true;
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* `await import("yaml")` gets CommonJS's named exports from Node, but only `default` from a bundled chunk. A dynamic
|
|
139
|
+
* import of CommonJS goes through a module that exports the names Node itself finds in it.
|
|
140
|
+
*/
|
|
141
|
+
const commonJsNames = {
|
|
142
|
+
name: "common-js-names",
|
|
143
|
+
setup(context) {
|
|
144
|
+
context.onResolve({ filter: /.*/ }, async (args) => {
|
|
145
|
+
if (args.kind !== "dynamic-import" || args.pluginData === "inner")
|
|
146
|
+
return undefined;
|
|
147
|
+
const resolved = await context.resolve(args.path, {
|
|
148
|
+
kind: "dynamic-import",
|
|
149
|
+
importer: args.importer,
|
|
150
|
+
resolveDir: args.resolveDir,
|
|
151
|
+
pluginData: "inner",
|
|
152
|
+
});
|
|
153
|
+
if (resolved.errors.length > 0 || resolved.external || !isCommonJs(resolved.path))
|
|
154
|
+
return undefined;
|
|
155
|
+
return { path: resolved.path, namespace: "common-js-names" };
|
|
156
|
+
});
|
|
157
|
+
context.onLoad({ filter: /.*/, namespace: "common-js-names" }, async (args) => {
|
|
158
|
+
const names = Object.keys(await import(pathToFileURL(args.path).href)).filter((name) => name !== "default");
|
|
159
|
+
const values = names.map((name, index) => `const n${index} = cjs[${JSON.stringify(name)}]`);
|
|
160
|
+
const exported = names.map((name, index) => `n${index} as ${JSON.stringify(name)}`);
|
|
161
|
+
return {
|
|
162
|
+
contents: `const cjs = require(${JSON.stringify(args.path)})\n${values.join("\n")}\nexport default cjs\nexport { ${exported.join(", ")} }\n`,
|
|
163
|
+
resolveDir: dirname(args.path),
|
|
164
|
+
loader: "js",
|
|
165
|
+
};
|
|
166
|
+
});
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
/** The bin that replaces the compiled one: `name` is the bundle's entry, `chunks` what a server preloads. */
|
|
170
|
+
export const loaderSource = (name, version, chunks) => `#!/usr/bin/env node
|
|
171
|
+
${LOADER_MARK}import * as module from "node:module"
|
|
172
|
+
|
|
173
|
+
const words = process.argv.slice(2)
|
|
174
|
+
// As the CLI answers it, unless a test records the argv: then the CLI writes the record too.
|
|
175
|
+
if (
|
|
176
|
+
words.length === 1 &&
|
|
177
|
+
(words[0] === "--version" || words[0] === "-V") &&
|
|
178
|
+
!Object.keys(process.env).some((key) => key.endsWith("_TEST_ARGV_LOG"))
|
|
179
|
+
) {
|
|
180
|
+
process.stdout.write(${JSON.stringify(`${version}\n`)})
|
|
181
|
+
} else {
|
|
182
|
+
// Before the bundle compiles: the bundle's own call comes after its chunks.
|
|
183
|
+
module.enableCompileCache()
|
|
184
|
+
const serves = words
|
|
185
|
+
.slice(0, 3)
|
|
186
|
+
.some((word, at) => ["serve", "watch"].includes(word) || (word === "mcp" && !/^[^-]/.test(words[at + 1] ?? "")))
|
|
187
|
+
if (serves && module.registerHooks) {
|
|
188
|
+
const bundle = new URL("../bundle/", import.meta.url).href
|
|
189
|
+
module.registerHooks({
|
|
190
|
+
resolve: (specifier, context, next) =>
|
|
191
|
+
specifier.startsWith("./") && context.parentURL?.startsWith(bundle)
|
|
192
|
+
? { url: new URL(specifier, context.parentURL).href, format: "module", shortCircuit: true }
|
|
193
|
+
: next(specifier, context),
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
const running = import(${JSON.stringify(`../bundle/${name}.js`)})
|
|
197
|
+
const chunks = ${JSON.stringify(chunks)}
|
|
198
|
+
// After the server answers its first request, so it starts no later.
|
|
199
|
+
if (serves) setTimeout(() => void Promise.allSettled(chunks.map((chunk) => import(chunk))), 2000).unref()
|
|
200
|
+
await running
|
|
201
|
+
}
|
|
202
|
+
`;
|
|
203
|
+
export const bundle = async (options) => {
|
|
204
|
+
const { build } = await import("esbuild");
|
|
205
|
+
const root = options.root;
|
|
206
|
+
const dist = join(root, "dist");
|
|
207
|
+
const out = join(dist, "bundle");
|
|
208
|
+
const bin = join(root, options.bin);
|
|
209
|
+
const name = basename(bin).replace(/\.[cm]?js$/, "");
|
|
210
|
+
const external = [...DEFAULT_EXTERNAL, ...(options.external ?? [])];
|
|
211
|
+
const isExternal = matcher(external);
|
|
212
|
+
const problems = [];
|
|
213
|
+
if (!bin.startsWith(dist + sep))
|
|
214
|
+
return { problems: [`${options.bin} is not under dist/`], chunks: [] };
|
|
215
|
+
if (readFileSync(bin, "utf8").includes(LOADER_MARK))
|
|
216
|
+
return { problems: [`${options.bin} is already the loader — compile it again (tsc) first`], chunks: [] };
|
|
217
|
+
const { version } = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
218
|
+
rmSync(out, { recursive: true, force: true });
|
|
219
|
+
const { metafile } = await build({
|
|
220
|
+
absWorkingDir: root,
|
|
221
|
+
entryPoints: { [name]: bin },
|
|
222
|
+
outdir: out,
|
|
223
|
+
bundle: true,
|
|
224
|
+
splitting: true,
|
|
225
|
+
format: "esm",
|
|
226
|
+
platform: "node",
|
|
227
|
+
target: "node22",
|
|
228
|
+
chunkNames: "[name]-[hash]",
|
|
229
|
+
// Two modules with one name would otherwise become `name2`, and `name` reaches errors and `commands --json`.
|
|
230
|
+
keepNames: true,
|
|
231
|
+
external,
|
|
232
|
+
// Bundled CommonJS calls `require` for Node's own modules.
|
|
233
|
+
banner: {
|
|
234
|
+
js: 'import { createRequire as __bundleRequire } from "node:module";\nconst require = __bundleRequire(import.meta.url);',
|
|
235
|
+
},
|
|
236
|
+
plugins: [
|
|
237
|
+
commonJsNames,
|
|
238
|
+
ownPlaces(dist, out, [...DEFAULT_ANCHORED, ...(options.anchored ?? [])], [...DEFAULT_HARMLESS, ...(options.harmless ?? [])], isExternal, problems),
|
|
239
|
+
],
|
|
240
|
+
metafile: true,
|
|
241
|
+
logLevel: "silent",
|
|
242
|
+
});
|
|
243
|
+
// Only the CLI's own modules may import a package that stays outside: it resolves from the bundle, the CLI's place.
|
|
244
|
+
const own = `${posix(relative(root, dist))}/`;
|
|
245
|
+
for (const [input, { imports }] of Object.entries(metafile.inputs)) {
|
|
246
|
+
if (input.startsWith(own))
|
|
247
|
+
continue;
|
|
248
|
+
for (const { path, external: outside } of imports) {
|
|
249
|
+
if (outside && !isBuiltin(path) && !["bun:sqlite", "<runtime>"].includes(path))
|
|
250
|
+
problems.push(`${input} imports ${path}, which would resolve from the bundle`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (problems.length > 0)
|
|
254
|
+
return { problems, chunks: [] };
|
|
255
|
+
// An upgrade replaces the chunks under a running server, and their names carry a hash: a server that had not yet
|
|
256
|
+
// loaded one would fail to find it. A server loads them all soon after it starts, and finds a chunk it holds without
|
|
257
|
+
// looking on disk, where Node's resolver would look again for each new importer.
|
|
258
|
+
const lazy = new Set();
|
|
259
|
+
for (const { imports } of Object.values(metafile.outputs)) {
|
|
260
|
+
for (const { path, kind, external: outside } of imports) {
|
|
261
|
+
if (kind === "dynamic-import" && !outside)
|
|
262
|
+
lazy.add(`../bundle/${basename(path)}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const chunks = [...lazy].sort();
|
|
266
|
+
const missing = chunks.filter((chunk) => !existsSync(join(out, basename(chunk))));
|
|
267
|
+
if (missing.length > 0)
|
|
268
|
+
return { problems: missing.map((chunk) => `the loader names ${chunk}, not built`), chunks };
|
|
269
|
+
writeFileSync(bin, loaderSource(name, version, chunks));
|
|
270
|
+
return { problems: [], chunks };
|
|
271
|
+
};
|