@wirecat/cli-base 0.63.0 → 0.65.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 +232 -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 +31 -0
- package/dist/dev/bundle.js +280 -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 +8 -0
- package/dist/standards/flags-calendar.d.ts +2 -0
- package/dist/standards/flags-calendar.js +72 -0
- package/dist/standards/flags-how.js +7 -1
- package/dist/standards/flags-what.js +7 -0
- package/dist/standards/vocabulary.js +13 -2
- package/docs/standards/README.md +5 -5
- package/docs/standards/accounts.md +44 -11
- package/docs/standards/architecture.md +19 -12
- package/docs/standards/ci.md +33 -12
- package/docs/standards/commands.md +35 -10
- package/docs/standards/data-model.md +5 -1
- package/docs/standards/docs.md +4 -1
- package/docs/standards/kit-surface.md +6 -5
- package/docs/standards/layout.md +32 -0
- package/docs/standards/lifecycle.md +69 -8
- package/docs/standards/mcp.md +48 -24
- package/docs/standards/new-cli.md +30 -16
- package/docs/standards/output.md +13 -2
- package/docs/standards/permissions.md +46 -3
- package/docs/standards/releases.md +21 -5
- package/docs/standards/security.md +16 -8
- package/docs/standards/store.md +4 -3
- package/docs/standards/testing.md +19 -8
- package/docs/standards/time.md +8 -1
- package/docs/standards/toolchain.md +24 -18
- package/package.json +6 -1
|
@@ -0,0 +1,280 @@
|
|
|
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
|
+
/**
|
|
170
|
+
* Whether the loader runs a server: `serve`, `watch` or `mcp serve`, or `mcp` alone as before cli-core 0.463. Flags
|
|
171
|
+
* are skipped, so `<cli> personal --read-only mcp serve` counts; four words leave room for one flag's value. It is
|
|
172
|
+
* written into the loader as source, so it uses nothing from outside.
|
|
173
|
+
*/
|
|
174
|
+
export const isServer = (words) => {
|
|
175
|
+
const plain = words.filter((word) => !word.startsWith("-"));
|
|
176
|
+
return plain
|
|
177
|
+
.slice(0, 4)
|
|
178
|
+
.some((word, at) => word === "serve" || word === "watch" || (word === "mcp" && plain[at + 1] === undefined));
|
|
179
|
+
};
|
|
180
|
+
/** The bin that replaces the compiled one: `name` is the bundle's entry, `chunks` what a server preloads. */
|
|
181
|
+
export const loaderSource = (name, version, chunks) => `#!/usr/bin/env node
|
|
182
|
+
${LOADER_MARK}import * as module from "node:module"
|
|
183
|
+
|
|
184
|
+
const words = process.argv.slice(2)
|
|
185
|
+
// As the CLI answers it, unless a test records the argv: then the CLI writes the record too.
|
|
186
|
+
if (
|
|
187
|
+
words.length === 1 &&
|
|
188
|
+
(words[0] === "--version" || words[0] === "-V") &&
|
|
189
|
+
!Object.keys(process.env).some((key) => key.endsWith("_TEST_ARGV_LOG"))
|
|
190
|
+
) {
|
|
191
|
+
process.stdout.write(${JSON.stringify(`${version}\n`)})
|
|
192
|
+
} else {
|
|
193
|
+
// Before the bundle compiles: the bundle's own call comes after its chunks.
|
|
194
|
+
module.enableCompileCache()
|
|
195
|
+
const serves = (${isServer.toString()})(words)
|
|
196
|
+
if (serves && module.registerHooks) {
|
|
197
|
+
const bundle = new URL("../bundle/", import.meta.url).href
|
|
198
|
+
module.registerHooks({
|
|
199
|
+
resolve: (specifier, context, next) =>
|
|
200
|
+
specifier.startsWith("./") && context.parentURL?.startsWith(bundle)
|
|
201
|
+
? { url: new URL(specifier, context.parentURL).href, format: "module", shortCircuit: true }
|
|
202
|
+
: next(specifier, context),
|
|
203
|
+
})
|
|
204
|
+
}
|
|
205
|
+
const running = import(${JSON.stringify(`../bundle/${name}.js`)})
|
|
206
|
+
const chunks = ${JSON.stringify(chunks)}
|
|
207
|
+
// After the server answers its first request, so it starts no later.
|
|
208
|
+
if (serves) setTimeout(() => void Promise.allSettled(chunks.map((chunk) => import(chunk))), 2000).unref()
|
|
209
|
+
await running
|
|
210
|
+
}
|
|
211
|
+
`;
|
|
212
|
+
export const bundle = async (options) => {
|
|
213
|
+
const { build } = await import("esbuild");
|
|
214
|
+
const root = options.root;
|
|
215
|
+
const dist = join(root, "dist");
|
|
216
|
+
const out = join(dist, "bundle");
|
|
217
|
+
const bin = join(root, options.bin);
|
|
218
|
+
const name = basename(bin).replace(/\.[cm]?js$/, "");
|
|
219
|
+
const external = [...DEFAULT_EXTERNAL, ...(options.external ?? [])];
|
|
220
|
+
const isExternal = matcher(external);
|
|
221
|
+
const problems = [];
|
|
222
|
+
if (!bin.startsWith(dist + sep))
|
|
223
|
+
return { problems: [`${options.bin} is not under dist/`], chunks: [] };
|
|
224
|
+
if (readFileSync(bin, "utf8").includes(LOADER_MARK))
|
|
225
|
+
return { problems: [`${options.bin} is already the loader — compile it again (tsc) first`], chunks: [] };
|
|
226
|
+
const { version } = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
227
|
+
rmSync(out, { recursive: true, force: true });
|
|
228
|
+
const { metafile } = await build({
|
|
229
|
+
absWorkingDir: root,
|
|
230
|
+
entryPoints: { [name]: bin },
|
|
231
|
+
outdir: out,
|
|
232
|
+
bundle: true,
|
|
233
|
+
splitting: true,
|
|
234
|
+
format: "esm",
|
|
235
|
+
platform: "node",
|
|
236
|
+
target: "node22",
|
|
237
|
+
chunkNames: "[name]-[hash]",
|
|
238
|
+
// Two modules with one name would otherwise become `name2`, and `name` reaches errors and `commands --json`.
|
|
239
|
+
keepNames: true,
|
|
240
|
+
external,
|
|
241
|
+
// Bundled CommonJS calls `require` for Node's own modules.
|
|
242
|
+
banner: {
|
|
243
|
+
js: 'import { createRequire as __bundleRequire } from "node:module";\nconst require = __bundleRequire(import.meta.url);',
|
|
244
|
+
},
|
|
245
|
+
plugins: [
|
|
246
|
+
commonJsNames,
|
|
247
|
+
ownPlaces(dist, out, [...DEFAULT_ANCHORED, ...(options.anchored ?? [])], [...DEFAULT_HARMLESS, ...(options.harmless ?? [])], isExternal, problems),
|
|
248
|
+
],
|
|
249
|
+
metafile: true,
|
|
250
|
+
logLevel: "silent",
|
|
251
|
+
});
|
|
252
|
+
// Only the CLI's own modules may import a package that stays outside: it resolves from the bundle, the CLI's place.
|
|
253
|
+
const own = `${posix(relative(root, dist))}/`;
|
|
254
|
+
for (const [input, { imports }] of Object.entries(metafile.inputs)) {
|
|
255
|
+
if (input.startsWith(own))
|
|
256
|
+
continue;
|
|
257
|
+
for (const { path, external: outside } of imports) {
|
|
258
|
+
if (outside && !isBuiltin(path) && !["bun:sqlite", "<runtime>"].includes(path))
|
|
259
|
+
problems.push(`${input} imports ${path}, which would resolve from the bundle`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (problems.length > 0)
|
|
263
|
+
return { problems, chunks: [] };
|
|
264
|
+
// An upgrade replaces the chunks under a running server, and their names carry a hash: a server that had not yet
|
|
265
|
+
// loaded one would fail to find it. A server loads them all soon after it starts, and finds a chunk it holds without
|
|
266
|
+
// looking on disk, where Node's resolver would look again for each new importer.
|
|
267
|
+
const lazy = new Set();
|
|
268
|
+
for (const { imports } of Object.values(metafile.outputs)) {
|
|
269
|
+
for (const { path, kind, external: outside } of imports) {
|
|
270
|
+
if (kind === "dynamic-import" && !outside)
|
|
271
|
+
lazy.add(`../bundle/${basename(path)}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const chunks = [...lazy].sort();
|
|
275
|
+
const missing = chunks.filter((chunk) => !existsSync(join(out, basename(chunk))));
|
|
276
|
+
if (missing.length > 0)
|
|
277
|
+
return { problems: missing.map((chunk) => `the loader names ${chunk}, not built`), chunks };
|
|
278
|
+
writeFileSync(bin, loaderSource(name, version, chunks));
|
|
279
|
+
return { problems: [], chunks };
|
|
280
|
+
};
|
package/dist/dev/main.js
CHANGED
|
@@ -66,7 +66,15 @@ const USAGE = `usage: cli-dev <command> [options]
|
|
|
66
66
|
rules by its own lint/ folder; the rest by --baseline
|
|
67
67
|
(audit/baseline). --groups replaces PARITY_GROUPS with the
|
|
68
68
|
JSON list of parity groups in that file; --differences adds
|
|
69
|
-
to the groups in use
|
|
69
|
+
to the groups in use
|
|
70
|
+
bundle --bin <dist/bin/name.js> [--external <package>]... [--anchor <package>]... [--harmless <path>]...
|
|
71
|
+
bundles the compiled bin with esbuild (a devDependency) into
|
|
72
|
+
dist/bundle/ and turns the bin into its loader. --external keeps
|
|
73
|
+
a package outside (* matches anything); --anchor adds a package
|
|
74
|
+
whose modules keep their installed place, as cli-core's do;
|
|
75
|
+
--harmless names a third-party file whose import.meta or
|
|
76
|
+
__dirname was read and found harmless. Fails when a bundled
|
|
77
|
+
module would look for a file or package in the wrong place`;
|
|
70
78
|
const load = async (cwd, path) => (await import(pathToFileURL(isAbsolute(path) ? path : join(cwd, path)).href));
|
|
71
79
|
const report = (io, problems, ok) => {
|
|
72
80
|
for (const problem of problems)
|
|
@@ -604,6 +612,43 @@ const auditRun = (args, io) => {
|
|
|
604
612
|
io.err(problem);
|
|
605
613
|
return problems.length > 0 ? 1 : 0;
|
|
606
614
|
};
|
|
615
|
+
const bundleCommand = async (args, io) => {
|
|
616
|
+
const { values } = parseArgs({
|
|
617
|
+
args,
|
|
618
|
+
options: {
|
|
619
|
+
bin: { type: "string" },
|
|
620
|
+
external: { type: "string", multiple: true },
|
|
621
|
+
anchor: { type: "string", multiple: true },
|
|
622
|
+
harmless: { type: "string", multiple: true },
|
|
623
|
+
},
|
|
624
|
+
});
|
|
625
|
+
if (!values.bin) {
|
|
626
|
+
io.err("bundle needs --bin <dist/bin/name.js>");
|
|
627
|
+
return 2;
|
|
628
|
+
}
|
|
629
|
+
// esbuild is an optional peer: the other commands, and a CLI that does not bundle, never load it.
|
|
630
|
+
try {
|
|
631
|
+
await import("esbuild");
|
|
632
|
+
}
|
|
633
|
+
catch {
|
|
634
|
+
io.err("cli-dev bundle needs esbuild: add it as a devDependency, in cli-base's peer range");
|
|
635
|
+
return 1;
|
|
636
|
+
}
|
|
637
|
+
const { bundle } = await import("./bundle.js");
|
|
638
|
+
const result = await bundle({
|
|
639
|
+
root: io.cwd,
|
|
640
|
+
bin: values.bin,
|
|
641
|
+
external: values.external ?? [],
|
|
642
|
+
anchored: values.anchor ?? [],
|
|
643
|
+
harmless: values.harmless ?? [],
|
|
644
|
+
});
|
|
645
|
+
if (result.problems.length > 0) {
|
|
646
|
+
io.err(`the bundle would look for these in the wrong place, or is incomplete:\n${result.problems.join("\n")}`);
|
|
647
|
+
return 1;
|
|
648
|
+
}
|
|
649
|
+
io.out(`bundle: ${values.bin} loads dist/bundle/; a server preloads ${result.chunks.length} chunks`);
|
|
650
|
+
return 0;
|
|
651
|
+
};
|
|
607
652
|
export const main = async (argv, io) => {
|
|
608
653
|
const [command, ...args] = argv;
|
|
609
654
|
if (args.includes("--help")) {
|
|
@@ -649,6 +694,8 @@ export const main = async (argv, io) => {
|
|
|
649
694
|
return lint(args, io);
|
|
650
695
|
case "audit":
|
|
651
696
|
return audit(args, io);
|
|
697
|
+
case "bundle":
|
|
698
|
+
return bundleCommand(args, io);
|
|
652
699
|
case "help":
|
|
653
700
|
case "--help":
|
|
654
701
|
case "-h":
|
package/dist/skill/index.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export interface SkillApp {
|
|
|
11
11
|
}
|
|
12
12
|
export declare const SKILL_TARGETS: readonly ["claude", "agents"];
|
|
13
13
|
export type SkillTarget = (typeof SKILL_TARGETS)[number];
|
|
14
|
-
/** `~/.claude/skills` is Claude Code's; `~/.agents/skills` is read by Codex
|
|
14
|
+
/** `~/.claude/skills` is Claude Code's; `~/.agents/skills` is read by Codex, Gemini CLI and OpenClaw. */
|
|
15
15
|
export declare const skillPath: (target: SkillTarget, appName: string, env: Env) => string;
|
|
16
16
|
/**
|
|
17
17
|
* `metadata.version` from the frontmatter — where the Agent Skills specification keeps fields of its
|
package/dist/skill/index.js
CHANGED
|
@@ -16,7 +16,7 @@ import { executableEnvironment, executableOnPath } from "../executable.js";
|
|
|
16
16
|
import { CHECK_EVERY_MS, isNewer, readNoticeState, writeUpdateState } from "../update/index.js";
|
|
17
17
|
export const SKILL_TARGETS = ["claude", "agents"];
|
|
18
18
|
const homeOf = (env) => env.HOME || env.USERPROFILE || homedir();
|
|
19
|
-
/** `~/.claude/skills` is Claude Code's; `~/.agents/skills` is read by Codex
|
|
19
|
+
/** `~/.claude/skills` is Claude Code's; `~/.agents/skills` is read by Codex, Gemini CLI and OpenClaw. */
|
|
20
20
|
export const skillPath = (target, appName, env) => join(homeOf(env), target === "claude" ? ".claude" : ".agents", "skills", appName, "SKILL.md");
|
|
21
21
|
const FRONTMATTER = /^---(\r?\n)([\s\S]*?\r?\n)?---(\r?\n|$)/;
|
|
22
22
|
const scalar = (key, indent) => new RegExp(`^${indent}${key}:[ \\t]*["']?([^"'\\r\\n]*?)["']?[ \\t]*\\r?\\n?$`);
|
|
@@ -234,7 +234,7 @@ export const skillCommand = (app, skill, environment, more = {}) => {
|
|
|
234
234
|
const named = Object.keys(more);
|
|
235
235
|
const show = annotate(command.command("show"), { effects: { remote: "none", local: "read" } })
|
|
236
236
|
.summary("print the tool's instructions for AI agents")
|
|
237
|
-
.description(`print SKILL.md — \`${app.command} skill install\` puts it where Claude Code, Codex
|
|
237
|
+
.description(`print SKILL.md — \`${app.command} skill install\` puts it where Claude Code, Codex, Gemini CLI and OpenClaw look for it, and where Hermes does on request; --for prints the copy already there`);
|
|
238
238
|
if (named.length > 0)
|
|
239
239
|
show.argument("[name]", `one of the skills shipped for a task: ${named.join(", ")}`);
|
|
240
240
|
show.addOption(new Option("--for <agents>", "print the copy installed for these agents instead of this version's").choices(SKILL_TARGETS));
|
package/dist/standards/audit.js
CHANGED
|
@@ -68,6 +68,14 @@ export const PARITY_GROUPS = defineParityGroups([
|
|
|
68
68
|
covers: ["commands", "doctor", "skill", "mcp", "upgrade", "search", "notes", "meetings"],
|
|
69
69
|
differences: [],
|
|
70
70
|
},
|
|
71
|
+
// gcal is compared on what it already shares with every CLI; doctor, mcp, search and skill wait until the published
|
|
72
|
+
// CLIs agree there (https://github.com/WireCatLabs/cli-base/issues/390), then gcal moves back into "all".
|
|
73
|
+
{
|
|
74
|
+
name: "gcal-shared",
|
|
75
|
+
members: ["tg", "max", "memo", "drive", "zm", "wa", "gcal"],
|
|
76
|
+
covers: ["commands", "upgrade", "notes", "meetings"],
|
|
77
|
+
differences: [],
|
|
78
|
+
},
|
|
71
79
|
]);
|
|
72
80
|
const requiredRootSchema = v.strictObject({ path: v.pipe(v.array(segment), v.minLength(1)), meaning: text });
|
|
73
81
|
export const defineRequiredRoots = (input) => parseRegistry("required roots", v.array(requiredRootSchema), input, (roots) => duplicates(roots.map((root) => root.path.join(" "))).map((path) => `"${path}" is listed twice`));
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export const CALENDAR_FLAGS = [
|
|
2
|
+
{ name: "--end-time", value: "<time>", role: "value", type: "time", meaning: "when it ends" },
|
|
3
|
+
{
|
|
4
|
+
name: "--start-date",
|
|
5
|
+
value: "<date>",
|
|
6
|
+
role: "value",
|
|
7
|
+
type: "time",
|
|
8
|
+
meaning: "the first day of something that lasts whole days, such as an all-day event",
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
name: "--end-date",
|
|
12
|
+
value: "<date>",
|
|
13
|
+
role: "value",
|
|
14
|
+
type: "time",
|
|
15
|
+
meaning: "the last day of something that lasts whole days, such as an all-day event",
|
|
16
|
+
},
|
|
17
|
+
{ name: "--location", value: "<text>", role: "value", type: "string", meaning: "where it takes place" },
|
|
18
|
+
{
|
|
19
|
+
name: "--attendee",
|
|
20
|
+
value: "<email>",
|
|
21
|
+
role: "value",
|
|
22
|
+
type: "string",
|
|
23
|
+
meaning: "a person invited to the event, by email address; repeat it for more",
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: "--id",
|
|
27
|
+
value: "<id>",
|
|
28
|
+
role: "value",
|
|
29
|
+
type: "id",
|
|
30
|
+
meaning: "the id to create it with, chosen beforehand, so a retry finds it instead of making a second one",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: "--calendar",
|
|
34
|
+
value: "<id>",
|
|
35
|
+
role: "target",
|
|
36
|
+
type: "id",
|
|
37
|
+
meaning: "the calendar it acts on, when the thing is on more than one",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: "--repeat",
|
|
41
|
+
value: "<rule>",
|
|
42
|
+
role: "value",
|
|
43
|
+
type: "string",
|
|
44
|
+
meaning: "how it repeats, as an RFC 5545 rule such as FREQ=WEEKLY;BYDAY=MO;COUNT=10",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "--hours",
|
|
48
|
+
value: "<from-to>",
|
|
49
|
+
role: "target",
|
|
50
|
+
type: "string",
|
|
51
|
+
meaning: "the working hours of each day to look in, such as 09:00-17:00, in the owner's time zone",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: "--series",
|
|
55
|
+
role: "target",
|
|
56
|
+
type: "switch",
|
|
57
|
+
meaning: "act on the whole repeating event, not only this occurrence",
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: "--following",
|
|
61
|
+
role: "target",
|
|
62
|
+
type: "switch",
|
|
63
|
+
meaning: "act on this occurrence and every later one of the repeating event",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: "--to-calendar",
|
|
67
|
+
value: "<id>",
|
|
68
|
+
role: "target",
|
|
69
|
+
type: "id",
|
|
70
|
+
meaning: "the calendar it moves to, of the same account",
|
|
71
|
+
},
|
|
72
|
+
];
|
|
@@ -308,7 +308,13 @@ export const HOW_FLAGS = [
|
|
|
308
308
|
meaning: "do not keep this run, whatever the configuration says",
|
|
309
309
|
},
|
|
310
310
|
{ name: "--no-registries", role: "mode", type: "switch", meaning: "do not ask the public ban lists" },
|
|
311
|
-
{
|
|
311
|
+
{
|
|
312
|
+
name: "--notify",
|
|
313
|
+
role: "mode",
|
|
314
|
+
type: "switch",
|
|
315
|
+
meaning: "tell the people it concerns about it: the chat's members, or an event's attendees",
|
|
316
|
+
},
|
|
317
|
+
{ name: "--no-notify", role: "mode", type: "switch", meaning: "tell nobody about it" },
|
|
312
318
|
{ name: "--offline", role: "mode", type: "switch", meaning: "answer from what was recorded and never connect" },
|
|
313
319
|
{
|
|
314
320
|
name: "--operation-id",
|
|
@@ -187,6 +187,13 @@ export const WHAT_FLAGS = [
|
|
|
187
187
|
type: "string",
|
|
188
188
|
meaning: "the profile to use, instead of the first word",
|
|
189
189
|
},
|
|
190
|
+
{
|
|
191
|
+
name: "--profiles",
|
|
192
|
+
value: "<names|all>",
|
|
193
|
+
role: "scope",
|
|
194
|
+
type: "list",
|
|
195
|
+
meaning: "the profiles an MCP server serves, comma-separated, or all of them",
|
|
196
|
+
},
|
|
190
197
|
{
|
|
191
198
|
name: "--history-since-time",
|
|
192
199
|
value: "<time>",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as v from "valibot";
|
|
2
|
+
import { CALENDAR_FLAGS } from "./flags-calendar.js";
|
|
2
3
|
import { HOW_FLAGS } from "./flags-how.js";
|
|
3
4
|
import { WHAT_FLAGS } from "./flags-what.js";
|
|
4
5
|
import { duplicates, parseRegistry, text, word } from "./registry.js";
|
|
@@ -114,7 +115,9 @@ const verbs = [
|
|
|
114
115
|
["start", "start a running process, or a login session"],
|
|
115
116
|
["stop", "stop a running process"],
|
|
116
117
|
["restart", "stop and start a running process"],
|
|
118
|
+
["serve", "run a server in this process and answer its clients until stopped"],
|
|
117
119
|
["end", "end a login session"],
|
|
120
|
+
["revoke", "take back what was granted, so it stops working: a login, an invite link, a consent"],
|
|
118
121
|
["install", "install a system unit"],
|
|
119
122
|
["uninstall", "remove a system unit"],
|
|
120
123
|
["cancel", "stop a job or a pending thing"],
|
|
@@ -123,6 +126,7 @@ const verbs = [
|
|
|
123
126
|
"bring the local store up to date from the source: only what changed, or the whole list where the source cannot say; a mirror syncs both ways",
|
|
124
127
|
],
|
|
125
128
|
["fetch", "one bounded pull of one named thing into the store, not a catch-up"],
|
|
129
|
+
["unsync", "stop syncing a thing; what the store already holds stays"],
|
|
126
130
|
["export", "write from the store to a file"],
|
|
127
131
|
["import", "bring a file or list the user gives into the store or the messenger, once"],
|
|
128
132
|
["download", "save a file from the source to this machine"],
|
|
@@ -153,6 +157,7 @@ const verbs = [
|
|
|
153
157
|
["unstar", "take the owner's star off a message"],
|
|
154
158
|
["vote", "answer a poll"],
|
|
155
159
|
["answer", "reply to a button someone pressed under a bot's message"],
|
|
160
|
+
["respond", "answer an invitation: accept, tentatively accept or decline it"],
|
|
156
161
|
["press", "press a button under a bot's message; the bot sees who pressed it"],
|
|
157
162
|
["action", "show what a bot is doing in a chat, such as typing, for a few seconds"],
|
|
158
163
|
["watch", "follow new events as they come, and keep them"],
|
|
@@ -175,7 +180,9 @@ const verbs = [
|
|
|
175
180
|
["acknowledge", "record successful delivery using the exact receipt issued when the item was claimed"],
|
|
176
181
|
["confirm", "the owner accepts what an agent proposed"],
|
|
177
182
|
["reject", "the owner refuses what an agent proposed"],
|
|
178
|
-
["
|
|
183
|
+
["review", "list what waits for the owner's decision"],
|
|
184
|
+
["resolve", "settle one thing that waits for the owner's decision, with the owner's choice"],
|
|
185
|
+
["move", "move a thing to another place in the source: a file to a folder, an event to a calendar"],
|
|
179
186
|
["copy", "copy a file in the source"],
|
|
180
187
|
["run", "run a saved job once, now, as its timer would"],
|
|
181
188
|
["claim", "take a thing for a while so no other agent works it at once; the claim lapses by itself"],
|
|
@@ -241,6 +248,9 @@ const collections = [
|
|
|
241
248
|
["files", "files in the source, such as Google Drive, read and changed there"],
|
|
242
249
|
["comments", "comments people left on a thing in the source: a channel post or a file"],
|
|
243
250
|
["meetings", "stored meetings, from Zoom or another provider"],
|
|
251
|
+
["calendars", "calendars of an account, each synced or not"],
|
|
252
|
+
["events", "calendar entries: meetings, appointments and other things on a calendar"],
|
|
253
|
+
["people", "persons the owner deals with, such as the attendees of events"],
|
|
244
254
|
["recordings", "the files a provider recorded of a meeting"],
|
|
245
255
|
["jobs", "work this tool runs in the background or on a timer"],
|
|
246
256
|
["actions", "what an agent proposes to do; the owner confirms or rejects each"],
|
|
@@ -302,6 +312,7 @@ const views = [
|
|
|
302
312
|
["evidence", "the stored messages or lines a result or a reference rests on"],
|
|
303
313
|
["passage", "a cited transcript line with bounded surrounding lines from the same revision"],
|
|
304
314
|
["agenda", "what needs a look today"],
|
|
315
|
+
["busy", "when calendars or people are taken, without what takes them"],
|
|
305
316
|
["categories", "the categories a thing takes, with what each means"],
|
|
306
317
|
["domains", "the internet domains links point to, most linked first"],
|
|
307
318
|
["places", "the street addresses confirmed as places, most mentioned first"],
|
|
@@ -348,5 +359,5 @@ export const VOCABULARY = defineVocabulary({
|
|
|
348
359
|
],
|
|
349
360
|
views: views.map(([entry, meaning]) => ({ word: entry, meaning })),
|
|
350
361
|
roots: roots.map(([entry, meaning]) => ({ word: entry, meaning })),
|
|
351
|
-
flags: [...HOW_FLAGS, ...WHAT_FLAGS],
|
|
362
|
+
flags: [...HOW_FLAGS, ...WHAT_FLAGS, ...CALENDAR_FLAGS],
|
|
352
363
|
});
|
package/docs/standards/README.md
CHANGED
|
@@ -28,14 +28,14 @@ the four steps that need a person. A new CLI is ready for its first release when
|
|
|
28
28
|
| Page | Area id | Rules | What it covers |
|
|
29
29
|
|---|---|---:|---|
|
|
30
30
|
| [Toolchain](toolchain.md) | `TOOL` | 20 | TypeScript, Biome, grit plugins, size caps, import cycles, spelling, Markdown, Go |
|
|
31
|
-
| [Layout](layout.md) | `LAY` |
|
|
31
|
+
| [Layout](layout.md) | `LAY` | 10 | Repository layout, entry points, exports, file size, one job per file, splitting a unit, the bundled bin |
|
|
32
32
|
| [Commands](commands.md) | `CMD` | 28 | Paths, vocabulary, flags, arguments, effects, lifetime, query, help, external references |
|
|
33
33
|
| [Output](output.md) | `OUT` | 23 | Data on stdout only, envelopes, errors, exit codes, partial answers, projection |
|
|
34
34
|
| [MCP](mcp.md) | `MCP` | 13 | The MCP tools, how they come from commands, what each permission shows |
|
|
35
|
-
| [Accounts](accounts.md) | `ACCT` |
|
|
36
|
-
| [Permissions](permissions.md) | `PERM` |
|
|
35
|
+
| [Accounts](accounts.md) | `ACCT` | 12 | The explicit profile, reads across accounts, scope flags, linked identities, sign-in commands |
|
|
36
|
+
| [Permissions](permissions.md) | `PERM` | 22 | Levels, keys, defaults, hard limits, the write guard and its journal |
|
|
37
37
|
| [Architecture](architecture.md) | `ARCH` | 17 | Layers, dependency direction, vendor types, what goes in which package |
|
|
38
|
-
| [Lifecycle](lifecycle.md) | `LIFE` |
|
|
38
|
+
| [Lifecycle](lifecycle.md) | `LIFE` | 15 | Opening late, who closes, deadlines, long-running commands, the `serve` relay, resumable syncs and their request budget, the run log |
|
|
39
39
|
| [Store](store.md) | `STORE` | 14 | Store areas, account scope, migrations, test isolation |
|
|
40
40
|
| [Data model](data-model.md) | `DATA` | 9 | Core entities, entities and source records, `external_id`, field names, links and duplicates, files, calendar entries |
|
|
41
41
|
| [Testing](testing.md) | `TEST` | 17 | Sandbox, no network, no real waits, leak tests, coverage, contracts, test matrix, parity |
|
|
@@ -84,7 +84,7 @@ Every rule has the same shape. A rule may add a sentence or a short list under i
|
|
|
84
84
|
CLI's `node_modules`;
|
|
85
85
|
- a check in another repository, as `<repo>: <check>`, such as `cli-core: pnpm parity:check` or
|
|
86
86
|
`cli-core: biome noRestrictedImports`. `<repo>` is the repository's name on GitHub: `cli-core`, `cli-testing`,
|
|
87
|
-
`cli-docs`, `community`, `tg-cli`, `max-cli`, `cli-memo`, `drive-cli`, `zoom-cli` or `
|
|
87
|
+
`cli-docs`, `community`, `tg-cli`, `max-cli`, `cli-memo`, `drive-cli`, `zoom-cli`, `whatsapp-cli` or `gcal-cli`. `<app>`
|
|
88
88
|
stands for each CLI the rule applies to, when every one of them runs that check in its own repository.
|
|
89
89
|
- **Since** is the cli-base release whose handbook first carried the rule. A feature can be older than its rule.
|
|
90
90
|
|