@standards/cli 1.0.0-alpha.391 → 1.0.0-alpha.393

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/bin.mjs CHANGED
@@ -1,19 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
  /*! © 2025-2026 AND YOU CREATE SAS (RCS Paris 940 190 614). Proprietary — see LICENSE (Standards SDK License 1.0). */
3
+ import { t as discoverBundles } from "./discover-bundles-D5RNCkan.mjs";
3
4
  import { createRequire } from "node:module";
4
5
  import { styleText } from "node:util";
5
- import { StandardsRequestError, createTransport } from "@standards/client";
6
+ import { BundleConflictError, StandardsRequestError, createTransport, syncBundle } from "@standards/client";
6
7
  import { Command } from "commander";
7
- import { CATALOG_KINDS, CATALOG_LOCALES, COUNT_MODES, DEVICE_CHANNEL_WORKER_CLOSE_PROTOCOL_VIOLATION, MEETING_RESPONSES, RECORDING_OVERRIDES, RECORD_ACCESS_ROLES, RESOURCE_VISIBILITIES, SchemaError, SchemaErrorCode, ValidationError, canonicalStringify, computeReconnectDelay, computeRetryDelay, deviceChannelUrl, diffSchemaState, formatByteSize, parseRecordAccessRole, parseSchemaState, parseServerFrame, schemaStateSourcePayload } from "@standards/schema";
8
+ import { BUNDLE_LOCALES, COUNT_MODES, DEVICE_CHANNEL_WORKER_CLOSE_PROTOCOL_VIOLATION, MEETING_RESPONSES, RECORDING_OVERRIDES, RECORD_ACCESS_ROLES, RESOURCE_VISIBILITIES, SchemaError, SchemaErrorCode, ValidationError, canonicalStringify, computeReconnectDelay, computeRetryDelay, deviceChannelUrl, diffSchemaState, formatByteSize, parseRecordAccessRole, parseSchemaState, parseServerFrame, schemaStateSourcePayload } from "@standards/schema";
8
9
  import Table from "cli-table3";
9
10
  import { createAppClient } from "@standards/client/app";
10
- import { homedir, hostname } from "node:os";
11
- import { createHash, randomUUID } from "node:crypto";
11
+ import { constants, createWriteStream } from "node:fs";
12
12
  import { access, chmod, mkdir, readFile, stat, unlink, writeFile } from "node:fs/promises";
13
13
  import { basename, dirname, isAbsolute, join } from "node:path";
14
+ import { homedir, hostname } from "node:os";
15
+ import { createHash, randomUUID } from "node:crypto";
14
16
  import { WebSocket } from "ws";
15
17
  import { spawn } from "node:child_process";
16
- import { constants, createWriteStream } from "node:fs";
17
18
  import { StringDecoder } from "node:string_decoder";
18
19
  import { Readable } from "node:stream";
19
20
  import { pipeline } from "node:stream/promises";
@@ -193,100 +194,125 @@ function registerAuthCommand(program) {
193
194
  });
194
195
  }
195
196
  //#endregion
196
- //#region src/commands/bundles.ts
197
+ //#region src/discovery/order-bundles.ts
197
198
  /**
198
- * System bundles (admin / workspace:manage). A bundle is a code-defined,
199
- * versioned set of schema that can be assigned to — and updated on — a tenant.
199
+ * Orders discovered bundles so each comes after the bundles it `uses`.
200
+ * Bundles a `uses` names that were not discovered are already the host's concern.
200
201
  */
201
- function registerBundlesCommand(program) {
202
- const bundles = program.command("bundles").description("Manage system bundles assigned to the current tenant (admin)");
203
- bundles.command("list").description("List the bundles actively assigned to the current tenant").action(async (_opts, cmd) => {
204
- formatOutput(await getAppClientFromCommand(cmd).bundles.listAssigned({}), getFormat(cmd));
205
- });
206
- bundles.command("updatable").description("List assigned bundles that have a newer version available").action(async (_opts, cmd) => {
207
- formatOutput(await getAppClientFromCommand(cmd).bundles.listUpdatable({}), getFormat(cmd));
208
- });
209
- bundles.command("assign").description("Assign (install) a bundle to the current tenant").argument("<bundleId>", "bundle ID to assign").action(async (bundleId, _opts, cmd) => {
210
- formatOutput(await getAppClientFromCommand(cmd).bundles.assign({ bundleId }), getFormat(cmd));
211
- });
212
- bundles.command("uninstall").description("Uninstall a bundle: deactivate it and demote its now-orphaned objects").argument("<bundleId>", "bundle ID to uninstall").option("--yes", "uninstall without confirmation").action(async (bundleId, opts, cmd) => {
213
- if (!opts.yes) throw new Error("Uninstalling a bundle is explicit. Re-run with \"--yes\" to confirm.");
214
- formatOutput(await getAppClientFromCommand(cmd).bundles.uninstall({ bundleId }), getFormat(cmd));
215
- });
216
- bundles.command("update").description("Apply assigned bundles — picks up newer bundle versions (additive sync)").action(async (_opts, cmd) => {
217
- formatOutput(await getAppClientFromCommand(cmd).bundles.update({}), getFormat(cmd));
218
- });
202
+ async function orderByUses(discovered) {
203
+ const byName = new Map(discovered.map((entry) => [entry.bundle.name, entry]));
204
+ const ordered = [];
205
+ const visited = /* @__PURE__ */ new Set();
206
+ async function visit(entry) {
207
+ if (visited.has(entry.bundle.name)) return;
208
+ visited.add(entry.bundle.name);
209
+ for (const used of (await entry.bundle.build()).uses) {
210
+ const dependency = byName.get(used);
211
+ if (dependency) await visit(dependency);
212
+ }
213
+ ordered.push(entry);
214
+ }
215
+ for (const entry of discovered) await visit(entry);
216
+ return ordered;
219
217
  }
220
218
  //#endregion
221
- //#region src/commands/catalog.ts
222
- /** Reject a typo instead of letting the API answer a 400 on a bare enum. */
223
- function parseChoice(flag, value, choices) {
224
- if (choices.includes(value)) return value;
225
- throw new ValidationError(`${flag} must be one of: ${choices.join(", ")}`, []);
226
- }
227
- /** An empty selection installs nothing, so it is refused before the request. */
228
- function parseSelection(opts) {
229
- const selection = {};
230
- if (opts.entries !== void 0) selection.entries = parseCsvList(opts.entries);
231
- if (opts.packs !== void 0) selection.packs = parseCsvList(opts.packs);
232
- if (!(selection.entries?.length || selection.packs?.length)) throw new ValidationError("Select at least one catalog entry (--entries) or pack (--packs)", []);
233
- return selection;
234
- }
235
- /** `<entryId>=bind`, `<entryId>=skip` or `<entryId>=rename:<name>`, one per collision. */
219
+ //#region src/commands/bundles.ts
220
+ const HASH_PREFIX_LENGTH = 7;
221
+ function shortHash(hash) {
222
+ return hash?.slice(0, HASH_PREFIX_LENGTH) ?? "";
223
+ }
224
+ function describeFailure(name, error) {
225
+ if (error instanceof BundleConflictError && error.owner) return `${name}: ${messageOf(error)} (owned by ${error.owner})`;
226
+ return `${name}: ${messageOf(error)}`;
227
+ }
228
+ /** Runs a command body; any failure prints once on stderr and exits 1. */
229
+ async function run(body) {
230
+ try {
231
+ await body();
232
+ } catch (error) {
233
+ console.error(styleText("red", `✗ ${messageOf(error)}`));
234
+ process.exit(1);
235
+ }
236
+ }
237
+ async function syncDiscovered(cmd, cwd) {
238
+ const transport = getClientFromCommand(cmd);
239
+ for (const { bundle } of await orderByUses(await discoverBundles({ cwd }))) try {
240
+ const { applied, hash } = await syncBundle(transport, bundle);
241
+ console.info(`${bundle.name} ${applied ? "applied" : "unchanged"} ${shortHash(hash)}`);
242
+ } catch (error) {
243
+ throw new Error(describeFailure(bundle.name, error));
244
+ }
245
+ }
246
+ /** Reject a locale typo instead of letting the API answer a 400 on a bare enum. */
247
+ function parseLocale(value) {
248
+ const locale = BUNDLE_LOCALES.find((candidate) => candidate === value);
249
+ if (locale) return locale;
250
+ throw new ValidationError(`--locale must be one of: ${BUNDLE_LOCALES.join(", ")}`, []);
251
+ }
252
+ /** `<pieceId>=bind`, `<pieceId>=skip` or `<pieceId>=rename:<name>`, one per collision. */
236
253
  function collectDecision(value, decisions) {
237
254
  const separator = value.indexOf("=");
238
- const entryId = value.slice(0, separator);
255
+ const pieceId = value.slice(0, separator);
239
256
  const choice = value.slice(separator + 1);
240
257
  if (separator > 0 && (choice === "bind" || choice === "skip")) return {
241
258
  ...decisions,
242
- [entryId]: choice
259
+ [pieceId]: choice
243
260
  };
244
261
  if (separator > 0 && choice.startsWith("rename:") && choice.length > 7) return {
245
262
  ...decisions,
246
- [entryId]: { rename: choice.slice(7) }
263
+ [pieceId]: { rename: choice.slice(7) }
264
+ };
265
+ throw new ValidationError(`--decision must read <pieceId>=bind, <pieceId>=skip or <pieceId>=rename:<name> (received: ${value})`, []);
266
+ }
267
+ /** The input `preview` and `add` share: whole bundles, in one locale. */
268
+ function addInput(names, opts) {
269
+ return {
270
+ bundles: names,
271
+ locale: parseLocale(opts.locale),
272
+ withSamples: opts.withSamples === true
247
273
  };
248
- throw new ValidationError(`--decision must read <entryId>=bind, <entryId>=skip or <entryId>=rename:<name> (received: ${value})`, []);
249
274
  }
250
- /** The flags `preview` and `install` share — both take the same selection. */
251
- function withSelectionOptions(command) {
252
- return command.option("--entries <ids>", "comma-separated catalog entry IDs to install").option("--packs <ids>", "comma-separated catalog pack IDs to install").option("--locale <locale>", `language to compile the pieces in (${CATALOG_LOCALES.join(", ")})`, "en").option("--with-samples", "also install the packs' sample records");
275
+ /** The flags `preview` and `add` share. */
276
+ function withAddOptions(command) {
277
+ return command.argument("<names...>", "bundle names (the bundles each one uses come along)").option("--locale <locale>", `language to compile the pieces in (${BUNDLE_LOCALES.join(", ")})`, "en").option("--with-samples", "also add the bundles' sample records");
253
278
  }
254
279
  /**
255
- * The pre-built catalog: objects, views and agents installed as copies the
256
- * workspace owns. `preview` returns the plan and its fingerprint; `install`
257
- * applies that same plan, refused if the workspace changed since.
280
+ * The workspace's bundles: sync the ones the project declares, list what is
281
+ * installed, remove one, and add a bundle the workspace may add as a copy it
282
+ * owns (`preview`, then `add` with the preview's fingerprint).
258
283
  */
259
- function registerCatalogCommand(program) {
260
- const catalog = program.command("catalog").description("Browse and install pre-built objects, views and agents");
261
- catalog.command("list").description("List the catalog entries and packs, and whether each is installed").option("--kind <kind>", `restrict to one kind (${CATALOG_KINDS.join(", ")})`).option("--search <text>", "match labels and descriptions").option("--locale <locale>", `language of the labels (${CATALOG_LOCALES.join(", ")})`, "en").action(async (opts, cmd) => {
262
- formatOutput(await getAppClientFromCommand(cmd).catalog.list({
263
- locale: parseChoice("--locale", opts.locale, CATALOG_LOCALES),
264
- ...opts.kind !== void 0 && { kind: parseChoice("--kind", opts.kind, CATALOG_KINDS) },
265
- ...opts.search !== void 0 && { q: opts.search }
266
- }), getFormat(cmd));
267
- });
268
- catalog.command("get").description("Get one catalog entry or pack, with the IDs it pulls in").argument("<id>", "catalog entry or pack ID").option("--locale <locale>", `language of the labels (${CATALOG_LOCALES.join(", ")})`, "en").action(async (id, opts, cmd) => {
269
- formatOutput(await getAppClientFromCommand(cmd).catalog.read({
270
- id,
271
- locale: parseChoice("--locale", opts.locale, CATALOG_LOCALES)
272
- }), getFormat(cmd));
273
- });
274
- withSelectionOptions(catalog.command("preview").description("Plan an install: what is new, what collides, and the plan's fingerprint")).action(async (opts, cmd) => {
275
- formatOutput(await getAppClientFromCommand(cmd).catalog.preview({
276
- selection: parseSelection(opts),
277
- locale: parseChoice("--locale", opts.locale, CATALOG_LOCALES),
278
- withSamples: opts.withSamples === true
279
- }), getFormat(cmd));
280
- });
281
- withSelectionOptions(catalog.command("install").description("Install a previewed selection as workspace-owned copies").requiredOption("--fingerprint <fingerprint>", "the fingerprint `catalog preview` returned").option("--decision <entryId=choice>", "how to settle a collision: bind, skip or rename:<name> (repeatable)", collectDecision, {})).action(async (opts, cmd) => {
282
- formatOutput(await getAppClientFromCommand(cmd).catalog.install({
283
- selection: parseSelection(opts),
284
- locale: parseChoice("--locale", opts.locale, CATALOG_LOCALES),
285
- withSamples: opts.withSamples === true,
284
+ function registerBundlesCommand(program) {
285
+ const bundles = program.command("bundles").description("Sync and manage the workspace's bundles");
286
+ bundles.command("sync").description("Discover the project's bundles (standards/bundles/*.ts) and sync each one").option("--cwd <dir>", "project root to discover bundles in").action((opts, cmd) => run(() => syncDiscovered(cmd, opts.cwd)));
287
+ bundles.command("list").description("List the bundles installed in the workspace").action((_opts, cmd) => run(async () => {
288
+ const { installed } = await getAppClientFromCommand(cmd).bundles.list({});
289
+ formatOutput(installed.map((row) => ({
290
+ name: row.name,
291
+ mode: row.mode,
292
+ origin: row.origin ?? "",
293
+ hash: shortHash(row.hash),
294
+ installedAt: row.installedAt
295
+ })), getFormat(cmd));
296
+ }));
297
+ bundles.command("available").description("List the bundles the workspace may add, and whether each is installed").option("--locale <locale>", `language of the labels (${BUNDLE_LOCALES.join(", ")})`, "en").action((opts, cmd) => run(async () => {
298
+ const { available } = await getAppClientFromCommand(cmd).bundles.list({ locale: parseLocale(opts.locale) });
299
+ formatOutput(available, getFormat(cmd));
300
+ }));
301
+ withAddOptions(bundles.command("preview").description("Plan an add: what is new, what collides, and the plan's fingerprint")).action((names, opts, cmd) => run(async () => {
302
+ formatOutput(await getAppClientFromCommand(cmd).bundles.preview(addInput(names, opts)), getFormat(cmd));
303
+ }));
304
+ withAddOptions(bundles.command("add").description("Add previewed bundles as copies the workspace owns").requiredOption("--fingerprint <fingerprint>", "the fingerprint `bundles preview` returned").option("--decision <pieceId=choice>", "how to settle a collision: bind, skip or rename:<name> (repeatable)", collectDecision, {})).action((names, opts, cmd) => run(async () => {
305
+ formatOutput(await getAppClientFromCommand(cmd).bundles.add({
306
+ ...addInput(names, opts),
286
307
  decisions: opts.decision,
287
308
  fingerprint: opts.fingerprint
288
309
  }), getFormat(cmd));
289
- });
310
+ }));
311
+ bundles.command("remove").description("Remove a synced bundle; its pieces are demoted, never deleted").argument("<name>", "bundle name").option("--yes", "remove without confirmation").action((name, opts, cmd) => run(async () => {
312
+ requireConfirmation(opts.yes, `Removing bundle "${name}" demotes its objects to workspace-owned. Re-run with "--yes" to confirm.`);
313
+ await getAppClientFromCommand(cmd).bundles.remove({ name });
314
+ console.info(`${name} removed`);
315
+ }));
290
316
  }
291
317
  //#endregion
292
318
  //#region src/commands/codemod.ts
@@ -294,7 +320,7 @@ function registerCatalogCommand(program) {
294
320
  function registerCodemodCommand(program) {
295
321
  const codemod = program.command("codemod").description("Rewrite code for a breaking SDK change");
296
322
  codemod.command("builder-shorthand").description("Rewrite text({ name, label }) builders to text(name, label)").argument("[paths...]", "files or directories to rewrite", ["."]).option("--dry-run", "report without writing").option("--relative-imports", "also treat relative imports as Standards builders — for the Standards monorepo itself").action(async (paths, opts) => {
297
- const { runBuilderShorthand } = await import("./builder-shorthand-DPVmGTec.mjs");
323
+ const { runBuilderShorthand } = await import("./builder-shorthand-CPJ9hFiW.mjs");
298
324
  const report = await runBuilderShorthand(paths, {
299
325
  write: !opts.dryRun,
300
326
  relativeImports: opts.relativeImports === true
@@ -2014,7 +2040,7 @@ function registerSchemaCommand(program) {
2014
2040
  schema.command("inspect").description("Inspect an object (attributes, types, views)").argument("<object>", "object name or ID").action(async (objectName, _opts, cmd) => {
2015
2041
  formatOutput(await getAppClientFromCommand(cmd).schemaObjects.get({ objectIdOrName: objectName }), getFormat(cmd));
2016
2042
  });
2017
- schema.command("groups").description("List the bundles and schema sources that own objects, with label and icon").action(async (_opts, cmd) => {
2043
+ schema.command("groups").description("List the bundles that own objects, with their label").action(async (_opts, cmd) => {
2018
2044
  formatOutput(await getAppClientFromCommand(cmd).schemaGroups.list({}), getFormat(cmd));
2019
2045
  });
2020
2046
  }
@@ -2043,20 +2069,6 @@ function registerSearchCommand(program) {
2043
2069
  });
2044
2070
  }
2045
2071
  //#endregion
2046
- //#region src/commands/sources.ts
2047
- /** Remote schema sources — definitions pushed by an external codebase through @standards/client. */
2048
- function registerSourcesCommand(program) {
2049
- const sources = program.command("sources").description("Inspect and remove remote schema sources on the current tenant");
2050
- sources.command("list").description("List remote schema sources: id, hash, last apply, object names").action(async (_opts, cmd) => {
2051
- formatOutput(await getAppClientFromCommand(cmd).schemaSources.list({}), getFormat(cmd));
2052
- });
2053
- sources.command("remove").description("Remove a remote schema source: its objects are demoted to custom, records are kept").argument("<sourceId>", "source id declared in code").option("--yes", "remove without confirmation").action(async (sourceId, opts, cmd) => {
2054
- if (!opts.yes) throw new Error("Removing a schema source is explicit. Re-run with \"--yes\" to confirm.");
2055
- await getAppClientFromCommand(cmd).schemaSources.remove({ sourceId });
2056
- formatOutput({ removed: sourceId }, getFormat(cmd));
2057
- });
2058
- }
2059
- //#endregion
2060
2072
  //#region src/version.ts
2061
2073
  /**
2062
2074
  * The installed package's version, read from its own manifest: `src/` and the
@@ -2101,8 +2113,6 @@ function createProgram() {
2101
2113
  registerInvitationsCommand(program);
2102
2114
  registerAuthCommand(program);
2103
2115
  registerBundlesCommand(program);
2104
- registerCatalogCommand(program);
2105
- registerSourcesCommand(program);
2106
2116
  registerCodemodCommand(program);
2107
2117
  registerConnectorsCommand(program);
2108
2118
  registerMcpCommand(program);
@@ -24,7 +24,6 @@ const BUILDERS = /* @__PURE__ */ new Set([
24
24
  "nativeObject"
25
25
  ]);
26
26
  const WITH_ID = /* @__PURE__ */ new Set(["object", "nativeObject"]);
27
- const SOURCE = "defineSchemaSource";
28
27
  const STANDARDS_PACKAGE = /^@standards\/(schema|client)$/;
29
28
  const RELATIVE_MODULE = /^\.{1,2}\//;
30
29
  const COMMENT_IN_CONFIG = "comment in config, rewrite by hand";
@@ -46,7 +45,7 @@ function importedBuilders(clauses) {
46
45
  if (!(bindings && ts.isNamedImports(bindings))) continue;
47
46
  for (const element of bindings.elements) {
48
47
  const exported = element.propertyName?.text ?? element.name.text;
49
- if (BUILDERS.has(exported) || exported === SOURCE) found.set(element.name.text, exported);
48
+ if (BUILDERS.has(exported)) found.set(element.name.text, exported);
50
49
  }
51
50
  }
52
51
  return found;
@@ -109,33 +108,6 @@ function rewriteBuilder(call, exported, file) {
109
108
  edits
110
109
  };
111
110
  }
112
- function rewriteSource(call, file) {
113
- const [id, config] = call.arguments;
114
- if (call.arguments.length !== 2 || id === void 0 || config === void 0) return { kind: "none" };
115
- if (!ts.isObjectLiteralExpression(config)) return {
116
- kind: "skip",
117
- reason: "config is not an object literal"
118
- };
119
- const idText = id.getText(file);
120
- const hasProperties = config.properties.length > 0;
121
- const open = config.getStart(file) + 1;
122
- const spaces = /^[ \t]*/.exec(file.text.slice(open))?.[0].length ?? 0;
123
- const multiline = /^[ \t]*[\r\n]/.test(file.text.slice(open));
124
- const head = hasProperties ? `{ id: ${idText},${multiline ? "" : " "}` : `{ id: ${idText} }`;
125
- const end = hasProperties ? multiline ? open : open + spaces : config.getEnd();
126
- if (hasComment(file, id.getStart(file), end)) return {
127
- kind: "skip",
128
- reason: COMMENT_IN_CONFIG
129
- };
130
- return {
131
- kind: "edit",
132
- edits: [{
133
- start: id.getStart(file),
134
- end,
135
- text: head
136
- }]
137
- };
138
- }
139
111
  function overlaps(x, y) {
140
112
  if (x.start === x.end && y.start === y.end) return x.start === y.start;
141
113
  return x.start < y.end && y.start < x.end;
@@ -161,10 +133,6 @@ function importedNamespaces(clauses) {
161
133
  }
162
134
  /** True when a call to `exported` still has the pre-shorthand argument shape. */
163
135
  function isLegacyShape(call, exported) {
164
- if (exported === SOURCE) {
165
- const config = call.arguments[1];
166
- return call.arguments.length === 2 && config !== void 0 && ts.isObjectLiteralExpression(config);
167
- }
168
136
  return BUILDERS.has(exported) && call.arguments.length === 1;
169
137
  }
170
138
  /** Every TS and JS module a schema can live in; declaration files carry no calls. */
@@ -190,7 +158,7 @@ function rewriteBuilderShorthand(text, fileName, options = {}) {
190
158
  if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
191
159
  const exported = builders.get(node.expression.text);
192
160
  if (exported !== void 0) {
193
- const outcome = exported === SOURCE ? rewriteSource(node, file) : rewriteBuilder(node, exported, file);
161
+ const outcome = rewriteBuilder(node, exported, file);
194
162
  if (outcome.kind === "edit") calls.push({
195
163
  node,
196
164
  edits: outcome.edits
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env node
2
+ /*! © 2025-2026 AND YOU CREATE SAS (RCS Paris 940 190 614). Proprietary — see LICENSE (Standards SDK License 1.0). */
3
+ import { DuplicateError, ValidationError, isBundle } from "@standards/schema";
4
+ import { existsSync } from "node:fs";
5
+ import { glob } from "node:fs/promises";
6
+ import { join, relative, resolve } from "node:path";
7
+ import { createJiti } from "jiti";
8
+ //#region src/discovery/discover-bundles.ts
9
+ const DEFAULT_PATTERNS = ["standards/bundles/*.ts"];
10
+ const CONFIG_FILE = "standards.config.ts";
11
+ function messageOf(error) {
12
+ return error instanceof Error ? error.message : String(error);
13
+ }
14
+ async function importModule(jiti, file, label) {
15
+ try {
16
+ return await jiti.import(file);
17
+ } catch (error) {
18
+ throw new ValidationError(`Could not load ${label}: ${messageOf(error)}`, []);
19
+ }
20
+ }
21
+ async function readConfigPatterns(jiti, cwd) {
22
+ const file = join(cwd, CONFIG_FILE);
23
+ if (!existsSync(file)) return void 0;
24
+ const config = (await importModule(jiti, file, CONFIG_FILE)).default;
25
+ if (config?.bundles === void 0) return void 0;
26
+ return Array.isArray(config.bundles) ? config.bundles : [config.bundles];
27
+ }
28
+ async function matchFiles(cwd, patterns) {
29
+ const files = /* @__PURE__ */ new Set();
30
+ for (const pattern of patterns) for await (const match of glob(pattern, { cwd })) files.add(resolve(cwd, match));
31
+ return [...files].sort();
32
+ }
33
+ /**
34
+ * Finds the bundles a project declares: every export, in every file the
35
+ * patterns match, that is a `BundleBuilder`. Files may hold anything else.
36
+ */
37
+ async function discoverBundles(options = {}) {
38
+ const cwd = resolve(options.cwd ?? process.cwd());
39
+ const jiti = createJiti(import.meta.url, {
40
+ moduleCache: true,
41
+ fsCache: false
42
+ });
43
+ const patterns = options.patterns ?? await readConfigPatterns(jiti, cwd) ?? DEFAULT_PATTERNS;
44
+ const files = await matchFiles(cwd, patterns);
45
+ const found = /* @__PURE__ */ new Map();
46
+ for (const file of files) {
47
+ const label = relative(cwd, file).split("\\").join("/");
48
+ const exports = await importModule(jiti, file, label);
49
+ for (const [exportName, value] of Object.entries(exports)) {
50
+ if (!isBundle(value)) continue;
51
+ const previous = found.get(value.name);
52
+ if (previous?.bundle === value) continue;
53
+ if (previous) {
54
+ const first = relative(cwd, previous.file).split("\\").join("/");
55
+ throw new DuplicateError("bundle", value.name, `Bundle "${value.name}" is exported by ${first} and ${label}`);
56
+ }
57
+ found.set(value.name, {
58
+ bundle: value,
59
+ file,
60
+ exportName
61
+ });
62
+ }
63
+ }
64
+ if (files.length > 0 && found.size === 0) throw new ValidationError(`No bundle found in ${files.length} file(s) matching ${patterns.join(", ")}`, []);
65
+ return [...found.values()].sort((a, b) => a.bundle.name.localeCompare(b.bundle.name));
66
+ }
67
+ //#endregion
68
+ export { discoverBundles as t };
@@ -0,0 +1,32 @@
1
+
2
+ import { BundleBuilder, ObjectBuilderLike } from "@standards/schema";
3
+ //#region src/discovery/define-config.d.ts
4
+ /** What `standards.config.ts` declares. Every key is optional: the conventions apply without a file. */
5
+ interface StandardsCliConfig {
6
+ /** Glob pattern(s), relative to the project root, that locate the bundle files. */
7
+ bundles?: string | string[];
8
+ }
9
+ /** Types a `standards.config.ts` default export. */
10
+ export declare function defineConfig(config: StandardsCliConfig): StandardsCliConfig;
11
+ //#endregion
12
+ //#region src/discovery/discover-bundles.d.ts
13
+ type AnyBundle = BundleBuilder<string, readonly ObjectBuilderLike[]>;
14
+ interface DiscoveredBundle {
15
+ bundle: AnyBundle;
16
+ /** Absolute path of the file that exports the bundle. */
17
+ file: string;
18
+ /** `default` or the name of the named export. */
19
+ exportName: string;
20
+ }
21
+ interface DiscoverBundlesOptions {
22
+ cwd?: string;
23
+ /** Overrides the config and the convention. */
24
+ patterns?: string[];
25
+ }
26
+ /**
27
+ * Finds the bundles a project declares: every export, in every file the
28
+ * patterns match, that is a `BundleBuilder`. Files may hold anything else.
29
+ */
30
+ export declare function discoverBundles(options?: DiscoverBundlesOptions): Promise<DiscoveredBundle[]>;
31
+ //#endregion
32
+ export type { DiscoverBundlesOptions, DiscoveredBundle, StandardsCliConfig };
package/dist/index.mjs ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ /*! © 2025-2026 AND YOU CREATE SAS (RCS Paris 940 190 614). Proprietary — see LICENSE (Standards SDK License 1.0). */
3
+ import { t as discoverBundles } from "./discover-bundles-D5RNCkan.mjs";
4
+ //#region src/discovery/define-config.ts
5
+ /** Types a `standards.config.ts` default export. */
6
+ function defineConfig(config) {
7
+ return config;
8
+ }
9
+ //#endregion
10
+ export { defineConfig, discoverBundles };
package/package.json CHANGED
@@ -1,11 +1,19 @@
1
1
  {
2
2
  "name": "@standards/cli",
3
- "version": "1.0.0-alpha.391",
3
+ "version": "1.0.0-alpha.393",
4
4
  "description": "CLI tool to interact with Standards API",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "standards": "./dist/bin.mjs"
8
8
  },
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.mts",
12
+ "import": "./dist/index.mjs"
13
+ },
14
+ "./package.json": "./package.json"
15
+ },
16
+ "types": "./dist/index.d.mts",
9
17
  "files": [
10
18
  "dist",
11
19
  "LICENSE",
@@ -17,11 +25,12 @@
17
25
  "dependencies": {
18
26
  "cli-table3": "^0.6.5",
19
27
  "commander": "^13.1.0",
28
+ "jiti": "2.7.0",
20
29
  "typescript": "^5.6.3",
21
30
  "ws": "8.21.1",
22
- "@standards/client": "1.0.0-alpha.391",
23
- "@standards/constants": "1.0.0-alpha.391",
24
- "@standards/schema": "1.0.0-alpha.391"
31
+ "@standards/client": "1.0.0-alpha.393",
32
+ "@standards/constants": "1.0.0-alpha.393",
33
+ "@standards/schema": "1.0.0-alpha.393"
25
34
  },
26
35
  "devDependencies": {
27
36
  "@types/node": "^25.6.0",