@loomweaver/cli 0.8.3 → 0.9.0-preview.2

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.
Files changed (2) hide show
  1. package/dist/main.mjs +343 -51
  2. package/package.json +1 -1
package/dist/main.mjs CHANGED
@@ -433,6 +433,12 @@ export class ${w.className}AgentPanel {
433
433
  private runs = 0;
434
434
 
435
435
  constructor() {
436
+ this.refreshOffered();
437
+ }
438
+
439
+ // The list is read again whenever the panel gains focus, because the weaver's own translations
440
+ // and other plugins' commands may arrive after this component was built.
441
+ protected refreshOffered(): void {
436
442
  this.offered.set(${w.propertyName}Agent()?.list() ?? []);
437
443
  }
438
444
 
@@ -523,7 +529,7 @@ export class ${w.className}AgentPanel {
523
529
  `;
524
530
  }
525
531
  function panelTemplateFile(w) {
526
- return `<div class="flex h-full flex-col gap-3">
532
+ return `<div class="flex h-full flex-col gap-3" (focusin)="refreshOffered()">
527
533
  <p
528
534
  class="shrink-0 rounded-md border border-border bg-surface-raised px-3 py-2 text-xs text-content-muted"
529
535
  >
@@ -639,10 +645,10 @@ export async function* askAgent(
639
645
  toolCallName: picked.name,
640
646
  });
641
647
  // Arguments arrive in pieces, exactly as they do from a real model. The adapter assembles them;
642
- // nothing downstream ever sees a half-written call. A real agent fills them from the JSON Schema
643
- // the workbench described in picked.parameters; a stand-in has nothing to fill them from, so it
644
- // sends none and lets the command apply its own defaults.
645
- for (const piece of chunks(JSON.stringify({}), 4)) {
648
+ // nothing downstream ever sees a half-written call. The values come from the JSON Schema the
649
+ // workbench described in picked.parameters: the first declared choice of the first argument that
650
+ // has choices, and nothing for the rest, so the command applies its own defaults there.
651
+ for (const piece of chunks(JSON.stringify(sampleArguments(picked)), 4)) {
646
652
  yield event(EventType.TOOL_CALL_ARGS, { toolCallId, delta: piece });
647
653
  await pause();
648
654
  }
@@ -667,6 +673,18 @@ async function* speak(
667
673
  yield event(EventType.TEXT_MESSAGE_END, { messageId });
668
674
  }
669
675
 
676
+ function sampleArguments(tool: Tool): Record<string, unknown> {
677
+ const schema = tool.parameters as
678
+ | { properties?: Record<string, { enum?: readonly unknown[] }> }
679
+ | undefined;
680
+ for (const [name, property] of Object.entries(schema?.properties ?? {})) {
681
+ if (property.enum && property.enum.length > 0) {
682
+ return { [name]: property.enum[0] };
683
+ }
684
+ }
685
+ return {};
686
+ }
687
+
670
688
  function chunks(text: string, size: number): readonly string[] {
671
689
  const pieces: string[] = [];
672
690
  for (let at = 0; at < text.length; at += size) {
@@ -686,7 +704,7 @@ function pause(): Promise<void> {
686
704
  }
687
705
 
688
706
  // ../devkit/src/recipes/angular-weaver/agent-files.ts
689
- var AG_UI_ADAPTER_VERSION = "0.8.3";
707
+ var AG_UI_ADAPTER_VERSION = "0.9.0-preview.2";
690
708
  var AG_UI_PROTOCOL_VERSION = "0.0.x";
691
709
  function connectionFile(w) {
692
710
  return `import { signal } from '@angular/core';
@@ -752,7 +770,7 @@ function contextThat(confirms: boolean, ran: Asked[]): PluginContext {
752
770
  ],
753
771
  invokeCommand: (id: string, args?: CommandArguments) => {
754
772
  ran.push({ id, args });
755
- return Promise.resolve({ outcome: 'answered', value: 'it ran' });
773
+ return Promise.resolve({ outcome: 'answered', value: { tone: args?.['tone'] ?? 'info' } });
756
774
  },
757
775
  ui: { confirm: () => Promise.resolve(confirms) },
758
776
  } as unknown as PluginContext;
@@ -781,17 +799,17 @@ describe('${w.propertyName}Connection', () => {
781
799
  ),
782
800
  ).toBeNull();
783
801
  await tools.receive(
784
- event(EventType.TOOL_CALL_ARGS, { toolCallId: 'c1', delta: '{"who"' }),
802
+ event(EventType.TOOL_CALL_ARGS, { toolCallId: 'c1', delta: '{"tone"' }),
785
803
  );
786
804
  await tools.receive(
787
- event(EventType.TOOL_CALL_ARGS, { toolCallId: 'c1', delta: ':"you"}' }),
805
+ event(EventType.TOOL_CALL_ARGS, { toolCallId: 'c1', delta: ':"success"}' }),
788
806
  );
789
807
  const answer = await tools.receive(
790
808
  event(EventType.TOOL_CALL_END, { toolCallId: 'c1' }),
791
809
  );
792
810
 
793
- expect(ran).toEqual([{ id: '${w.id}.hello', args: { who: 'you' } }]);
794
- expect(answer?.content).toBe('it ran');
811
+ expect(ran).toEqual([{ id: '${w.id}.hello', args: { tone: 'success' } }]);
812
+ expect(JSON.parse(answer?.content ?? '{}').tone).toBe('success');
795
813
  expect(answer?.error).toBeUndefined();
796
814
  });
797
815
 
@@ -996,7 +1014,9 @@ function i18nBundle(w) {
996
1014
  }
997
1015
  if (w.features.command) {
998
1016
  bundle["action"] = `${w.name} action`;
999
- bundle["actionDescription"] = `Shows a short ${w.name} message.`;
1017
+ bundle["actionDescription"] = `Shows a short ${w.name} message in the tone the caller chooses.`;
1018
+ bundle["actionTone"] = "How the message is shown: info, success or warning. Use info unless the caller asked for emphasis.";
1019
+ bundle["actionAnswers"] = "The tone the message was shown in.";
1000
1020
  }
1001
1021
  if (w.features.about) bundle["about"] = `About ${w.name}`;
1002
1022
  if (w.features.agent) {
@@ -1018,6 +1038,38 @@ function i18nFile(w) {
1018
1038
  return JSON.stringify(i18nBundle(w), null, 2) + "\n";
1019
1039
  }
1020
1040
 
1041
+ // ../devkit/src/recipes/angular-weaver/weaver-command.ts
1042
+ function commandBlock(w) {
1043
+ return [
1044
+ " ctx.registerCommand({",
1045
+ ` id: '${w.id}.hello',`,
1046
+ ` title: '${w.id}.action',`,
1047
+ ` description: '${w.id}.actionDescription',`,
1048
+ " arguments: [",
1049
+ ` { name: 'tone', kind: 'choice', choices: ['info', 'success', 'warning'], description: '${w.id}.actionTone' },`,
1050
+ " ],",
1051
+ ` answers: '${w.id}.actionAnswers',`,
1052
+ ` shortcut: '${w.features.shortcut}',`,
1053
+ " callable: true,",
1054
+ " run: (_context, args) => {",
1055
+ " const tone = toneOf(args?.['tone']);",
1056
+ ` ctx.ui.toast({ message: '${w.id}.action', kind: tone });`,
1057
+ " return { tone };",
1058
+ " },",
1059
+ " });"
1060
+ ].join("\n");
1061
+ }
1062
+ function toneHelper() {
1063
+ return [
1064
+ "type Tone = 'info' | 'success' | 'warning';",
1065
+ "",
1066
+ "// The workbench already refused anything outside the declared choices; this narrows the type.",
1067
+ "function toneOf(value: unknown): Tone {",
1068
+ " return value === 'success' || value === 'warning' ? value : 'info';",
1069
+ "}"
1070
+ ].join("\n");
1071
+ }
1072
+
1021
1073
  // ../devkit/src/recipes/angular-weaver/recipe.ts
1022
1074
  var DEFAULT_MENU_SLOT = "content/tab/context";
1023
1075
  var SURFACE_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><rect x="4" y="4" width="16" height="16" rx="3"/><path d="M8 9h8M8 13h8M8 17h5"/></svg>';
@@ -1196,18 +1248,6 @@ function railBlock(w) {
1196
1248
  lines.push(" });");
1197
1249
  return lines.join("\n");
1198
1250
  }
1199
- function commandBlock(w) {
1200
- return [
1201
- " ctx.registerCommand({",
1202
- ` id: '${w.id}.hello',`,
1203
- ` title: '${w.id}.action',`,
1204
- ` description: '${w.id}.actionDescription',`,
1205
- ` shortcut: '${w.features.shortcut}',`,
1206
- " callable: true,",
1207
- ` run: () => ctx.ui.toast({ message: '${w.id}.action', kind: 'info' }),`,
1208
- " });"
1209
- ].join("\n");
1210
- }
1211
1251
  function barItemBlock(w) {
1212
1252
  return [
1213
1253
  " ctx.registerBarItem({",
@@ -1319,9 +1359,10 @@ function pluginFile(w) {
1319
1359
  deactivate() {
1320
1360
  ${w.propertyName}Agent.set(null);
1321
1361
  },` : "";
1362
+ if (w.features.command) consts.push(toneHelper());
1322
1363
  return `${imports.join("\n")}
1323
1364
 
1324
- ${consts.join("\n")}
1365
+ ${consts.join("\n\n")}
1325
1366
 
1326
1367
  export const ${w.propertyName}Plugin: Plugin = {
1327
1368
  manifest: {
@@ -1760,9 +1801,9 @@ function readme2(d) {
1760
1801
  "project's `project.json`, keeping the targets, `implicitDependencies` and tags it already had.",
1761
1802
  "",
1762
1803
  "Two leftovers from `ng new` are no longer referenced and can go: `src/app/app.routes.ts` (the",
1763
- "shell owns content routing via `provideShellRouter()`) and `src/app/app.css`. The generated",
1764
- "`src/app/app.spec.ts` now fails \u2014 `App` pulls the whole shell into a bare `TestBed` \u2014 so delete",
1765
- "it and test your own components instead of the composition root.",
1804
+ "shell owns content routing via `provideShellRouter()`) and `src/app/app.css`. The starter test",
1805
+ "`src/app/app.spec.ts` was replaced by one that boots the shell with this composition root, so",
1806
+ "`ng test` is green as generated; test your own components beside it.",
1766
1807
  "",
1767
1808
  "The project is generated **untagged**: Nx tags belong to your `depConstraints`, and inventing",
1768
1809
  "one would fail a lint policy you never opted this project into. If your workspace enforces",
@@ -2016,6 +2057,31 @@ describe('layout', () => {
2016
2057
  });
2017
2058
  `;
2018
2059
  }
2060
+ function appSpec() {
2061
+ return `import { TestBed } from '@angular/core/testing';
2062
+ import { App } from './app';
2063
+ import { appConfig } from './app.config';
2064
+
2065
+ /* The application's starter test, rewritten for a root component that renders the shell: it boots
2066
+ with the composition root's own providers, so whatever you compose in is what the test boots
2067
+ with, and it asserts only that the shell mounted. */
2068
+ describe('App', () => {
2069
+ beforeEach(async () => {
2070
+ await TestBed.configureTestingModule({
2071
+ imports: [App],
2072
+ providers: appConfig.providers,
2073
+ }).compileComponents();
2074
+ });
2075
+
2076
+ it('boots the shell', async () => {
2077
+ const fixture = TestBed.createComponent(App);
2078
+ await fixture.whenStable();
2079
+ const compiled = fixture.nativeElement as HTMLElement;
2080
+ expect(compiled.querySelector('lw-shell')).toBeTruthy();
2081
+ });
2082
+ });
2083
+ `;
2084
+ }
2019
2085
  function indexHtml(d) {
2020
2086
  return `<!DOCTYPE html>
2021
2087
  <html lang="en">
@@ -2142,7 +2208,10 @@ var angularDistribution = {
2142
2208
  return {
2143
2209
  "src/main.ts": mainTs(),
2144
2210
  "src/app/app.config.ts": appConfigTs(d),
2145
- ...d.withTests && { "src/app/app.config.spec.ts": appConfigSpec() },
2211
+ ...d.withTests && {
2212
+ "src/app/app.config.spec.ts": appConfigSpec(),
2213
+ "src/app/app.spec.ts": appSpec()
2214
+ },
2146
2215
  "src/app/app.ts": appTs(),
2147
2216
  "src/app/app.html": appHtml(),
2148
2217
  "src/index.html": indexHtml(d),
@@ -2382,6 +2451,8 @@ function bootstrapFile(t) {
2382
2451
  --lw-on-info: #000;
2383
2452
 
2384
2453
  --lw-scrim: rgb(0 0 0 / 0.5);
2454
+ --lw-scroll-thumb: rgb(0 0 0 / 0.3);
2455
+ --lw-scroll-track: transparent;
2385
2456
 
2386
2457
  --lw-font-sans: var(--bs-body-font-family);
2387
2458
  --lw-font-mono: var(--bs-font-monospace);
@@ -3069,6 +3140,184 @@ function validateCatalog(catalog, known = KNOWN_CAPABILITIES) {
3069
3140
  return findings;
3070
3141
  }
3071
3142
 
3143
+ // ../devkit/src/lib/validate/commands.ts
3144
+ var RUNTIME_NOTE = "Grants, access and the window decide what is finally offered at runtime; this check judged the registrations alone.";
3145
+ function validateCommands(sources, ts) {
3146
+ const findings = [];
3147
+ for (const source of sources) {
3148
+ const file = ts.createSourceFile(source.path, source.text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
3149
+ for (const call of registerCommandCalls(ts, file)) {
3150
+ findings.push(...judge(read(ts, file, call)));
3151
+ }
3152
+ }
3153
+ findings.push({ level: "info", code: "commands.runtime", message: RUNTIME_NOTE });
3154
+ return findings;
3155
+ }
3156
+ function registerCommandCalls(ts, file) {
3157
+ const calls = [];
3158
+ const visit = (node) => {
3159
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "registerCommand") {
3160
+ calls.push(node);
3161
+ }
3162
+ ts.forEachChild(node, visit);
3163
+ };
3164
+ visit(file);
3165
+ return calls;
3166
+ }
3167
+ function read(ts, file, call) {
3168
+ const line = file.getLineAndCharacterOfPosition(call.getStart(file)).line + 1;
3169
+ const at2 = `${file.fileName}:${line}`;
3170
+ const literal = call.arguments[0];
3171
+ const empty = { at: at2, callable: false, described: false, answers: false, returnsValue: false, arguments: [] };
3172
+ if (!literal || !ts.isObjectLiteralExpression(literal)) {
3173
+ return { ...empty, unreadable: "its argument is not an object literal" };
3174
+ }
3175
+ const properties = /* @__PURE__ */ new Map();
3176
+ for (const property of literal.properties) {
3177
+ if (ts.isSpreadAssignment(property)) {
3178
+ return { ...empty, unreadable: "it spreads another value into the registration" };
3179
+ }
3180
+ if (ts.isPropertyAssignment(property) && (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name))) {
3181
+ properties.set(property.name.text, property.initializer);
3182
+ } else if (ts.isMethodDeclaration(property) && ts.isIdentifier(property.name)) {
3183
+ properties.set(property.name.text, property);
3184
+ }
3185
+ }
3186
+ const id = properties.get("id");
3187
+ if (!id || !ts.isStringLiteral(id)) {
3188
+ return { ...empty, unreadable: "its id is not a string literal" };
3189
+ }
3190
+ const callable = properties.get("callable");
3191
+ return {
3192
+ at: at2,
3193
+ id: id.text,
3194
+ callable: callable !== void 0 && callable.kind === ts.SyntaxKind.TrueKeyword,
3195
+ described: properties.has("description"),
3196
+ answers: properties.has("answers"),
3197
+ returnsValue: returnsValue(ts, properties.get("run")),
3198
+ arguments: readArguments(ts, properties.get("arguments"))
3199
+ };
3200
+ }
3201
+ function readArguments(ts, value) {
3202
+ if (!value || !ts.isArrayLiteralExpression(value)) {
3203
+ return [];
3204
+ }
3205
+ return value.elements.map((element, index) => {
3206
+ if (!ts.isObjectLiteralExpression(element)) {
3207
+ return { name: `#${index + 1}`, described: false };
3208
+ }
3209
+ let name = `#${index + 1}`;
3210
+ let described = false;
3211
+ for (const property of element.properties) {
3212
+ if (!ts.isPropertyAssignment(property) || !ts.isIdentifier(property.name)) {
3213
+ continue;
3214
+ }
3215
+ if (property.name.text === "name" && ts.isStringLiteral(property.initializer)) {
3216
+ name = property.initializer.text;
3217
+ }
3218
+ if (property.name.text === "description") {
3219
+ described = true;
3220
+ }
3221
+ }
3222
+ return { name, described };
3223
+ });
3224
+ }
3225
+ function returnsValue(ts, run2) {
3226
+ if (!run2) {
3227
+ return false;
3228
+ }
3229
+ if (ts.isArrowFunction(run2) && !ts.isBlock(run2.body)) {
3230
+ return ts.isObjectLiteralExpression(run2.body) || ts.isArrayLiteralExpression(run2.body);
3231
+ }
3232
+ let found = false;
3233
+ const visit = (node) => {
3234
+ if (found || ts.isFunctionLike(node) && node !== run2) {
3235
+ return;
3236
+ }
3237
+ if (ts.isReturnStatement(node) && node.expression && node.expression.kind !== ts.SyntaxKind.UndefinedKeyword) {
3238
+ found = true;
3239
+ return;
3240
+ }
3241
+ ts.forEachChild(node, visit);
3242
+ };
3243
+ visit(run2);
3244
+ return found;
3245
+ }
3246
+ function judge(registration) {
3247
+ const { at: at2, id } = registration;
3248
+ if (registration.unreadable) {
3249
+ return [
3250
+ {
3251
+ level: "info",
3252
+ code: "command.unreadable",
3253
+ path: at2,
3254
+ message: `A registerCommand call at ${at2} could not be read: ${registration.unreadable}. What it offers an agent is unknown here.`
3255
+ }
3256
+ ];
3257
+ }
3258
+ if (!registration.callable) {
3259
+ return [
3260
+ {
3261
+ level: "info",
3262
+ code: "command.private",
3263
+ path: at2,
3264
+ message: `${id}: not offered to an agent, because it is not callable; an agent never sees it.`
3265
+ }
3266
+ ];
3267
+ }
3268
+ const findings = [];
3269
+ if (!registration.described) {
3270
+ findings.push({
3271
+ level: "warning",
3272
+ code: "command.description",
3273
+ path: at2,
3274
+ message: `${id}: offered to an agent without a description; the agent sees only the id and has to guess what it does.`
3275
+ });
3276
+ }
3277
+ for (const argument of registration.arguments) {
3278
+ if (!argument.described) {
3279
+ findings.push({
3280
+ level: "info",
3281
+ code: "command.argument",
3282
+ path: at2,
3283
+ message: `${id}: argument "${argument.name}" has no description; an agent sees a bare name and guesses its value.`
3284
+ });
3285
+ }
3286
+ }
3287
+ if (registration.returnsValue && !registration.answers) {
3288
+ findings.push({
3289
+ level: "info",
3290
+ code: "command.answers",
3291
+ path: at2,
3292
+ message: `${id}: run returns a value but declares no answers; an agent hears that the command ran, not what it answered.`
3293
+ });
3294
+ }
3295
+ if (findings.length === 0) {
3296
+ const count = registration.arguments.length;
3297
+ findings.push({
3298
+ level: "info",
3299
+ code: "command.offered",
3300
+ path: at2,
3301
+ message: `${id}: offered to an agent, with ${count} described argument${count === 1 ? "" : "s"}${registration.answers ? " and a declared answer" : ""}.`
3302
+ });
3303
+ }
3304
+ return findings;
3305
+ }
3306
+
3307
+ // ../devkit/src/lib/validate/typescript.ts
3308
+ import { createRequire } from "node:module";
3309
+ import { resolve } from "node:path";
3310
+ function loadTypeScript(fromDir) {
3311
+ for (const from of [resolve(fromDir, "package.json"), resolve(process.cwd(), "package.json")]) {
3312
+ try {
3313
+ return createRequire(from)("typescript");
3314
+ } catch {
3315
+ continue;
3316
+ }
3317
+ }
3318
+ return null;
3319
+ }
3320
+
3072
3321
  // src/lib/run.ts
3073
3322
  import { readdirSync, readFileSync as readFileSync3 } from "node:fs";
3074
3323
  import { join } from "node:path";
@@ -3162,17 +3411,17 @@ function boolFlag(args, name) {
3162
3411
 
3163
3412
  // src/lib/amend.ts
3164
3413
  import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "node:fs";
3165
- import { dirname as dirname2, posix, relative as relative2, resolve as resolve2, sep as sep2 } from "node:path";
3414
+ import { dirname as dirname2, posix, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
3166
3415
 
3167
3416
  // src/lib/workspace.ts
3168
3417
  import { existsSync, readFileSync } from "node:fs";
3169
- import { dirname, relative, resolve, sep } from "node:path";
3418
+ import { dirname, relative, resolve as resolve2, sep } from "node:path";
3170
3419
  var WorkspaceError = class extends Error {
3171
3420
  };
3172
3421
  function findWorkspace(from) {
3173
- let dir = resolve(from);
3422
+ let dir = resolve2(from);
3174
3423
  for (; ; ) {
3175
- if (existsSync(resolve(dir, "package.json"))) {
3424
+ if (existsSync(resolve2(dir, "package.json"))) {
3176
3425
  return { root: dir, ...buildConfigIn(dir) };
3177
3426
  }
3178
3427
  const parent = dirname(dir);
@@ -3210,11 +3459,11 @@ function readJsonFile(file) {
3210
3459
  }
3211
3460
  }
3212
3461
  function buildConfigIn(dir) {
3213
- const angular = resolve(dir, "angular.json");
3462
+ const angular = resolve2(dir, "angular.json");
3214
3463
  if (existsSync(angular)) {
3215
3464
  return { configFile: angular, kind: "angular" };
3216
3465
  }
3217
- const nx = resolve(dir, "nx.json");
3466
+ const nx = resolve2(dir, "nx.json");
3218
3467
  if (existsSync(nx)) {
3219
3468
  return { configFile: nx, kind: "nx" };
3220
3469
  }
@@ -3239,7 +3488,7 @@ function contains(projectRoot, target) {
3239
3488
  return projectRoot === "" || target === projectRoot || target.startsWith(`${projectRoot}/`);
3240
3489
  }
3241
3490
  function relativeTo(root, target) {
3242
- return relative(root, resolve(target)).split(sep).join("/");
3491
+ return relative(root, resolve2(target)).split(sep).join("/");
3243
3492
  }
3244
3493
  function withoutTrailingSlashes(path) {
3245
3494
  let end = path.length;
@@ -3331,7 +3580,7 @@ var Amender = class {
3331
3580
  }
3332
3581
  planPostcss(amendment) {
3333
3582
  const inTheWay = JS_POSTCSS_CONFIGS.find(
3334
- (name) => existsSync2(resolve2(this.workspace.root, name))
3583
+ (name) => existsSync2(resolve3(this.workspace.root, name))
3335
3584
  );
3336
3585
  if (inTheWay) {
3337
3586
  this.remaining.push(
@@ -3339,7 +3588,7 @@ var Amender = class {
3339
3588
  );
3340
3589
  return;
3341
3590
  }
3342
- const file = resolve2(this.workspace.root, amendment.file);
3591
+ const file = resolve3(this.workspace.root, amendment.file);
3343
3592
  const result = ensurePostcssPlugin(
3344
3593
  existsSync2(file) ? readJsonFile(file) : void 0,
3345
3594
  amendment
@@ -3357,7 +3606,7 @@ var Amender = class {
3357
3606
  });
3358
3607
  }
3359
3608
  planPackage(amendment) {
3360
- const file = resolve2(this.workspace.root, "package.json");
3609
+ const file = resolve3(this.workspace.root, "package.json");
3361
3610
  if (!existsSync2(file)) {
3362
3611
  this.remaining.push(describeAmendment(amendment));
3363
3612
  return;
@@ -3375,7 +3624,7 @@ var Amender = class {
3375
3624
  if (this.manifestAdded.length === 0 || !this.manifest) {
3376
3625
  return;
3377
3626
  }
3378
- const file = resolve2(this.workspace.root, "package.json");
3627
+ const file = resolve3(this.workspace.root, "package.json");
3379
3628
  this.planned.push({
3380
3629
  file,
3381
3630
  display: this.displayName(file),
@@ -3433,7 +3682,7 @@ var Amender = class {
3433
3682
  });
3434
3683
  }
3435
3684
  planComposePlugin(amendment, project) {
3436
- const root = resolve2(
3685
+ const root = resolve3(
3437
3686
  this.workspace.root,
3438
3687
  project.root,
3439
3688
  "src/app/app.config.ts"
@@ -3524,7 +3773,7 @@ var Amender = class {
3524
3773
  const entry = styles.find(
3525
3774
  (style) => typeof style === "string" && style.endsWith(".css")
3526
3775
  );
3527
- return entry === void 0 ? void 0 : resolve2(this.workspace.root, entry);
3776
+ return entry === void 0 ? void 0 : resolve3(this.workspace.root, entry);
3528
3777
  }
3529
3778
  nonAngularNote(amendment) {
3530
3779
  const where = this.workspace.kind === "nx" ? "the project's own project.json" : "your build configuration";
@@ -3544,7 +3793,7 @@ function relativeImport(fromDir, sourceRoot) {
3544
3793
  }
3545
3794
 
3546
3795
  // src/lib/scaffold.ts
3547
- import { relative as relative3, resolve as resolve3 } from "node:path";
3796
+ import { relative as relative3, resolve as resolve4 } from "node:path";
3548
3797
  function findScaffold2(name) {
3549
3798
  const scaffold2 = findScaffold(name);
3550
3799
  if (!scaffold2) {
@@ -3596,7 +3845,7 @@ function valuesFor(scaffold2, args) {
3596
3845
  return values;
3597
3846
  }
3598
3847
  function directoryFromOut(out) {
3599
- const below = relative3(process.cwd(), resolve3(out ?? "."));
3848
+ const below = relative3(process.cwd(), resolve4(out ?? "."));
3600
3849
  return below.startsWith("..") ? "" : below;
3601
3850
  }
3602
3851
  function scaffoldValues(scaffold2, args) {
@@ -3628,15 +3877,15 @@ import {
3628
3877
  rmSync,
3629
3878
  writeFileSync as writeFileSync2
3630
3879
  } from "node:fs";
3631
- import { dirname as dirname3, isAbsolute, relative as relative4, resolve as resolve4 } from "node:path";
3880
+ import { dirname as dirname3, isAbsolute, relative as relative4, resolve as resolve5 } from "node:path";
3632
3881
  var WriteError = class extends Error {
3633
3882
  };
3634
3883
  function planWrite(files, root) {
3635
- const absoluteRoot = resolve4(root);
3884
+ const absoluteRoot = resolve5(root);
3636
3885
  const planned = [];
3637
3886
  const conflicts = [];
3638
3887
  for (const path of Object.keys(files).toSorted((a, b) => a.localeCompare(b))) {
3639
- const absolute = resolve4(absoluteRoot, path);
3888
+ const absolute = resolve5(absoluteRoot, path);
3640
3889
  const inside = relative4(absoluteRoot, absolute);
3641
3890
  if (inside.startsWith("..") || isAbsolute(inside)) {
3642
3891
  throw new WriteError(`Refusing to write outside the target directory: ${path}`);
@@ -3679,7 +3928,7 @@ function replaceSymlinkEntry(absolute) {
3679
3928
  }
3680
3929
 
3681
3930
  // src/lib/run.ts
3682
- var VERSION = "0.8.3";
3931
+ var VERSION = "0.9.0-preview.2";
3683
3932
  function help() {
3684
3933
  const commands = SCAFFOLDS.map((s) => ` ${s.name.padEnd(16)}${s.summary}`);
3685
3934
  return [
@@ -3695,6 +3944,7 @@ function help() {
3695
3944
  " validate-manifest --id <id> [--name <name>] [--capabilities <a,b>]",
3696
3945
  " validate-i18n --dir <dir> check <lang>.json bundles for key parity",
3697
3946
  " validate-catalog --file <path> check a plugin store catalog the host parses defensively",
3947
+ " validate-commands --dir <dir> say, per command, whether an agent is offered it and what it would guess at",
3698
3948
  "",
3699
3949
  "Options:",
3700
3950
  " --out <dir> where to write (default: the current directory)",
@@ -3724,11 +3974,14 @@ function reportFindings(io, findings, strict) {
3724
3974
  io.out("No findings.");
3725
3975
  return 0;
3726
3976
  }
3727
- for (const f of findings) io.err(`${f.level}: ${f.message}`);
3728
- if (findings.some((f) => f.level === "error")) {
3977
+ for (const f of findings) {
3978
+ (f.level === "info" ? io.out : io.err)(`${f.level}: ${f.message}`);
3979
+ }
3980
+ const gating = findings.filter((f) => f.level !== "info");
3981
+ if (gating.some((f) => f.level === "error")) {
3729
3982
  return 1;
3730
3983
  }
3731
- return strict ? 1 : 0;
3984
+ return strict && gating.length > 0 ? 1 : 0;
3732
3985
  }
3733
3986
  function validateManifestCommand(args, io) {
3734
3987
  rejectUnknownFlags(args, ["id", "name", "capabilities", "strict"]);
@@ -3769,6 +4022,42 @@ function validateI18nCommand(args, io) {
3769
4022
  boolFlag(args, "strict") === true
3770
4023
  );
3771
4024
  }
4025
+ function readSources(dir) {
4026
+ const sources = [];
4027
+ const walk = (folder) => {
4028
+ for (const entry of readdirSync(folder, { withFileTypes: true })) {
4029
+ if (entry.name === "node_modules" || entry.name === "dist" || entry.name.startsWith(".")) {
4030
+ continue;
4031
+ }
4032
+ const path = join(folder, entry.name);
4033
+ if (entry.isDirectory()) {
4034
+ walk(path);
4035
+ } else if (entry.name.endsWith(".ts") && !entry.name.endsWith(".d.ts") && !entry.name.endsWith(".spec.ts")) {
4036
+ sources.push({ path, text: readFileSync3(path, "utf8") });
4037
+ }
4038
+ }
4039
+ };
4040
+ try {
4041
+ walk(dir);
4042
+ } catch (error) {
4043
+ throw new ArgError(`Cannot read ${dir}: ${error.message}`);
4044
+ }
4045
+ if (sources.length === 0) {
4046
+ throw new ArgError(`No TypeScript sources found under ${dir}.`);
4047
+ }
4048
+ return sources;
4049
+ }
4050
+ function validateCommandsCommand(args, io) {
4051
+ rejectUnknownFlags(args, ["dir", "strict"]);
4052
+ const dir = requiredFlag(args, "dir");
4053
+ const ts = loadTypeScript(dir);
4054
+ if (!ts) {
4055
+ throw new ArgError(
4056
+ `typescript is not installed where ${dir} can reach it; the check reads sources with the TypeScript compiler API, so run it inside the project.`
4057
+ );
4058
+ }
4059
+ return reportFindings(io, validateCommands(readSources(dir), ts), boolFlag(args, "strict") === true);
4060
+ }
3772
4061
  function readCatalog(file) {
3773
4062
  let raw;
3774
4063
  try {
@@ -3889,6 +4178,9 @@ function run(argv, io) {
3889
4178
  if (args.command === "validate-catalog") {
3890
4179
  return validateCatalogCommand(args, io);
3891
4180
  }
4181
+ if (args.command === "validate-commands") {
4182
+ return validateCommandsCommand(args, io);
4183
+ }
3892
4184
  return scaffold(args, io);
3893
4185
  } catch (error) {
3894
4186
  io.err(error.message);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loomweaver/cli",
3
- "version": "0.8.3",
3
+ "version": "0.9.0-preview.2",
4
4
  "description": "LoomWeaver: open-source plugin platform for Angular workbenches. The scaffolding CLI: generates weavers, distributions and integrations into any project, with no Nx workspace and no checkout required.",
5
5
  "keywords": [
6
6
  "loomweaver",