@loomweaver/cli 0.14.2 → 0.14.4

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 (3) hide show
  1. package/README.md +3 -0
  2. package/dist/main.mjs +156 -137
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -17,6 +17,9 @@ the style pipeline, the build target, the entry stylesheet, the composition root
17
17
  the output needs, listing each line it added. Anywhere else it writes the files and names what is
18
18
  left to wire by hand.
19
19
 
20
+ A weaver's components are named with the selector prefix the Angular application declares in
21
+ `angular.json`, or `app` where it declares none; `--prefix` names another.
22
+
20
23
  It also validates what the platform parses defensively, which is where mistakes go unreported:
21
24
 
22
25
  ```bash
package/dist/main.mjs CHANGED
@@ -638,9 +638,9 @@ function renderRegions(indent) {
638
638
  // ../devkit/src/recipes/angular-weaver/agent-panel.ts
639
639
  function panelFile(weaver) {
640
640
  return `import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
641
- import { EventType, type BaseEvent, type Tool, type ToolMessage } from '@ag-ui/core';
642
- import { ${weaver.propertyName}Agent } from './${weaver.id}-agent';
643
- import { askAgent } from './${weaver.id}-agent-source';
641
+ import { EventType, type AGUIEvent, type Tool, type ToolMessage } from '@ag-ui/core';
642
+ import { askAgent } from './${weaver.id}-agent';
643
+ import { ${weaver.propertyName}Tools } from './${weaver.id}-connection';
644
644
 
645
645
  interface Line {
646
646
  readonly kind: 'you' | 'agent' | 'call' | 'result' | 'note';
@@ -670,11 +670,11 @@ export class ${weaver.className}AgentPanel {
670
670
  // The list is read again whenever the panel gains focus, because the weaver's own translations
671
671
  // and other plugins' commands may arrive after this component was built.
672
672
  protected refreshOffered(): void {
673
- this.offered.set(${weaver.propertyName}Agent()?.list() ?? []);
673
+ this.offered.set(${weaver.propertyName}Tools()?.list() ?? []);
674
674
  }
675
675
 
676
676
  protected async ask(name: string): Promise<void> {
677
- const tools = ${weaver.propertyName}Agent();
677
+ const tools = ${weaver.propertyName}Tools();
678
678
  if (!tools || this.busy()) {
679
679
  return;
680
680
  }
@@ -717,24 +717,19 @@ export class ${weaver.className}AgentPanel {
717
717
  }
718
718
  }
719
719
 
720
- private draw(event: BaseEvent): void {
721
- const raw = event as unknown as Record<string, unknown>;
720
+ private draw(event: AGUIEvent): void {
722
721
  switch (event.type) {
723
722
  case EventType.TEXT_MESSAGE_START:
724
723
  this.push({ kind: 'agent', text: '' });
725
724
  break;
726
725
  case EventType.TEXT_MESSAGE_CONTENT:
727
- this.grow('text', String(raw['delta'] ?? ''));
726
+ this.grow('text', event.delta);
728
727
  break;
729
728
  case EventType.TOOL_CALL_START:
730
- this.push({
731
- kind: 'call',
732
- text: String(raw['toolCallName'] ?? ''),
733
- args: '',
734
- });
729
+ this.push({ kind: 'call', text: event.toolCallName, args: '' });
735
730
  break;
736
731
  case EventType.TOOL_CALL_ARGS:
737
- this.grow('args', String(raw['delta'] ?? ''));
732
+ this.grow('args', event.delta);
738
733
  break;
739
734
  default:
740
735
  break;
@@ -763,7 +758,7 @@ function panelTemplateFile(weaver) {
763
758
  class="shrink-0 rounded-md border border-border bg-surface-raised px-3 py-2 text-xs text-content-muted"
764
759
  >
765
760
  This is a stand-in, not an assistant. It speaks the protocol so you can watch the whole path;
766
- replace <code class="text-content">${weaver.id}-agent-source.ts</code> with your own transport.
761
+ replace <code class="text-content">${weaver.id}-agent.ts</code> with your own transport.
767
762
  </p>
768
763
 
769
764
  <div class="min-h-0 flex-1 overflow-auto">
@@ -831,7 +826,7 @@ function panelTemplateFile(weaver) {
831
826
 
832
827
  // ../devkit/src/recipes/angular-weaver/agent-stand-in.ts
833
828
  function standInFile(weaver) {
834
- return `import { EventType, type BaseEvent, type Tool } from '@ag-ui/core';
829
+ return `import { EventType, type AGUIEvent, type Tool } from '@ag-ui/core';
835
830
 
836
831
  // WHAT THIS IS: a stand-in for an agent, and not an agent. It speaks the AG-UI protocol and nothing
837
832
  // else \u2014 no model, no network, no judgement \u2014 so that the whole path from the offered tools through
@@ -851,15 +846,16 @@ export interface AgentRequest {
851
846
 
852
847
  export async function* askAgent(
853
848
  request: AgentRequest,
854
- ): AsyncGenerator<BaseEvent> {
849
+ ): AsyncGenerator<AGUIEvent> {
855
850
  const picked = request.tools.find((tool) =>
856
851
  request.prompt.includes(tool.name),
857
852
  );
858
853
 
859
- yield event(EventType.RUN_STARTED, {
854
+ yield {
855
+ type: EventType.RUN_STARTED,
860
856
  threadId: '${weaver.id}-stand-in',
861
857
  runId: request.runId,
862
- });
858
+ };
863
859
  yield* speak(
864
860
  \`\${request.runId}.says\`,
865
861
  picked
@@ -869,37 +865,35 @@ export async function* askAgent(
869
865
 
870
866
  if (picked) {
871
867
  const toolCallId = \`\${request.runId}.call\`;
872
- yield event(EventType.TOOL_CALL_START, {
873
- toolCallId,
874
- toolCallName: picked.name,
875
- });
868
+ yield { type: EventType.TOOL_CALL_START, toolCallId, toolCallName: picked.name };
876
869
  // Arguments arrive in pieces, exactly as they do from a real model. The adapter assembles them;
877
870
  // nothing downstream ever sees a half-written call. The values come from the JSON Schema the
878
871
  // workbench described in picked.parameters: the first declared choice of the first argument that
879
872
  // has choices, and nothing for the rest, so the command applies its own defaults there.
880
873
  for (const piece of chunks(JSON.stringify(sampleArguments(picked)), 4)) {
881
- yield event(EventType.TOOL_CALL_ARGS, { toolCallId, delta: piece });
874
+ yield { type: EventType.TOOL_CALL_ARGS, toolCallId, delta: piece };
882
875
  await pause();
883
876
  }
884
- yield event(EventType.TOOL_CALL_END, { toolCallId });
877
+ yield { type: EventType.TOOL_CALL_END, toolCallId };
885
878
  }
886
879
 
887
- yield event(EventType.RUN_FINISHED, {
880
+ yield {
881
+ type: EventType.RUN_FINISHED,
888
882
  threadId: '${weaver.id}-stand-in',
889
883
  runId: request.runId,
890
- });
884
+ };
891
885
  }
892
886
 
893
887
  async function* speak(
894
888
  messageId: string,
895
889
  text: string,
896
- ): AsyncGenerator<BaseEvent> {
897
- yield event(EventType.TEXT_MESSAGE_START, { messageId, role: 'assistant' });
890
+ ): AsyncGenerator<AGUIEvent> {
891
+ yield { type: EventType.TEXT_MESSAGE_START, messageId, role: 'assistant' };
898
892
  for (const piece of chunks(text, 10)) {
899
- yield event(EventType.TEXT_MESSAGE_CONTENT, { messageId, delta: piece });
893
+ yield { type: EventType.TEXT_MESSAGE_CONTENT, messageId, delta: piece };
900
894
  await pause();
901
895
  }
902
- yield event(EventType.TEXT_MESSAGE_END, { messageId });
896
+ yield { type: EventType.TEXT_MESSAGE_END, messageId };
903
897
  }
904
898
 
905
899
  function sampleArguments(tool: Tool): Record<string, unknown> {
@@ -922,10 +916,6 @@ function chunks(text: string, size: number): readonly string[] {
922
916
  return pieces;
923
917
  }
924
918
 
925
- function event(type: EventType, fields: Record<string, unknown>): BaseEvent {
926
- return { type, ...fields } as unknown as BaseEvent;
927
- }
928
-
929
919
  function pause(): Promise<void> {
930
920
  return new Promise((done) => setTimeout(done, PAUSE_MS));
931
921
  }
@@ -944,15 +934,18 @@ import {
944
934
  } from '@loomweaver/ag-ui';
945
935
  import type { PluginContext } from '@loomweaver/plugin-sdk';
946
936
 
937
+ // The connection is the workbench's half: the commands offered as tools, and each call put to the
938
+ // workbench and answered. The agent is yours and lives beside it, in ${weaver.id}-agent.ts.
939
+ //
947
940
  // A factory, not a module-level connection: everything a run needs lives in the closure, so a second
948
941
  // one never shares state with the first.
949
- export function ${weaver.propertyName}Connection(ctx: PluginContext): CommandTools {
942
+ export function connect${weaver.className}(ctx: PluginContext): CommandTools {
950
943
  return commandTools(ctx, { before: (call) => decide(ctx, call) });
951
944
  }
952
945
 
953
946
  // The one connection this plugin activates, published for its panel. Set in activate(), cleared in
954
947
  // deactivate(), so the panel renders an honest empty state either side of that.
955
- export const ${weaver.propertyName}Agent = signal<CommandTools | null>(null);
948
+ export const ${weaver.propertyName}Tools = signal<CommandTools | null>(null);
956
949
 
957
950
  // What an agent's word is enough for is the command's own statement, declared where the command is
958
951
  // registered and read off the call here. No list of ids lives beside the commands: a list drifts from
@@ -987,9 +980,10 @@ async function decide(
987
980
  `;
988
981
  }
989
982
  function specFile(weaver) {
990
- return `import { EventType, type BaseEvent } from '@ag-ui/core';
983
+ return `import { EventType, type AGUIEvent, type ToolMessage } from '@ag-ui/core';
984
+ import type { CommandTools } from '@loomweaver/ag-ui';
991
985
  import type { CommandArguments, PluginContext } from '@loomweaver/plugin-sdk';
992
- import { ${weaver.propertyName}Connection } from './${weaver.id}-agent';
986
+ import { connect${weaver.className} } from './${weaver.id}-connection';
993
987
 
994
988
  interface Asked {
995
989
  readonly id: string;
@@ -1010,37 +1004,36 @@ function contextThat(confirms: boolean, ran: Asked[]): PluginContext {
1010
1004
  } as unknown as PluginContext;
1011
1005
  }
1012
1006
 
1013
- function event(type: EventType, fields: Record<string, unknown>): BaseEvent {
1014
- return { type, ...fields } as unknown as BaseEvent;
1007
+ function streamedCall(toolCallId: string, ...deltas: string[]): AGUIEvent[] {
1008
+ return [
1009
+ { type: EventType.TOOL_CALL_START, toolCallId, toolCallName: '${weaver.id}.hello' },
1010
+ ...deltas.map((delta): AGUIEvent => ({ type: EventType.TOOL_CALL_ARGS, toolCallId, delta })),
1011
+ { type: EventType.TOOL_CALL_END, toolCallId },
1012
+ ];
1013
+ }
1014
+
1015
+ async function answersTo(tools: CommandTools, events: AGUIEvent[]): Promise<ToolMessage[]> {
1016
+ const answers: ToolMessage[] = [];
1017
+ for (const event of events) {
1018
+ const answer = await tools.receive(event);
1019
+ if (answer) {
1020
+ answers.push(answer);
1021
+ }
1022
+ }
1023
+ return answers;
1015
1024
  }
1016
1025
 
1017
- describe('${weaver.propertyName}Connection', () => {
1026
+ describe('connect${weaver.className}', () => {
1018
1027
  it('offers what the workbench offers', () => {
1019
- const tools = ${weaver.propertyName}Connection(contextThat(true, []));
1028
+ const tools = connect${weaver.className}(contextThat(true, []));
1020
1029
  expect(tools.list().map((tool) => tool.name)).toEqual(['${weaver.id}.hello']);
1021
1030
  });
1022
1031
 
1023
1032
  it('assembles a call from its events and answers with the outcome', async () => {
1024
1033
  const ran: Asked[] = [];
1025
- const tools = ${weaver.propertyName}Connection(contextThat(true, ran));
1034
+ const tools = connect${weaver.className}(contextThat(true, ran));
1026
1035
 
1027
- expect(
1028
- await tools.receive(
1029
- event(EventType.TOOL_CALL_START, {
1030
- toolCallId: 'c1',
1031
- toolCallName: '${weaver.id}.hello',
1032
- }),
1033
- ),
1034
- ).toBeNull();
1035
- await tools.receive(
1036
- event(EventType.TOOL_CALL_ARGS, { toolCallId: 'c1', delta: '{"tone"' }),
1037
- );
1038
- await tools.receive(
1039
- event(EventType.TOOL_CALL_ARGS, { toolCallId: 'c1', delta: ':"success"}' }),
1040
- );
1041
- const answer = await tools.receive(
1042
- event(EventType.TOOL_CALL_END, { toolCallId: 'c1' }),
1043
- );
1036
+ const [answer] = await answersTo(tools, streamedCall('c1', '{"tone"', ':"success"}'));
1044
1037
 
1045
1038
  expect(ran).toEqual([{ id: '${weaver.id}.hello', args: { tone: 'success' } }]);
1046
1039
  expect(JSON.parse(answer?.content ?? '{}').tone).toBe('success');
@@ -1049,20 +1042,9 @@ describe('${weaver.propertyName}Connection', () => {
1049
1042
 
1050
1043
  it('never reaches the workbench when a consequential call is declined', async () => {
1051
1044
  const ran: Asked[] = [];
1052
- const tools = ${weaver.propertyName}Connection(contextThat(false, ran));
1045
+ const tools = connect${weaver.className}(contextThat(false, ran));
1053
1046
 
1054
- await tools.receive(
1055
- event(EventType.TOOL_CALL_START, {
1056
- toolCallId: 'c2',
1057
- toolCallName: '${weaver.id}.hello',
1058
- }),
1059
- );
1060
- await tools.receive(
1061
- event(EventType.TOOL_CALL_ARGS, { toolCallId: 'c2', delta: '{}' }),
1062
- );
1063
- const answer = await tools.receive(
1064
- event(EventType.TOOL_CALL_END, { toolCallId: 'c2' }),
1065
- );
1047
+ const [answer] = await answersTo(tools, streamedCall('c2', '{}'));
1066
1048
 
1067
1049
  expect(ran).toEqual([]);
1068
1050
  expect(answer?.error).toContain('did not run');
@@ -1084,13 +1066,13 @@ function agentSurfaceBlock(weaver) {
1084
1066
  }
1085
1067
  function agentFiles(weaver) {
1086
1068
  const files = {
1087
- [`src/lib/agent/${weaver.id}-agent.ts`]: connectionFile(weaver),
1088
- [`src/lib/agent/${weaver.id}-agent-source.ts`]: standInFile(weaver),
1069
+ [`src/lib/agent/${weaver.id}-connection.ts`]: connectionFile(weaver),
1070
+ [`src/lib/agent/${weaver.id}-agent.ts`]: standInFile(weaver),
1089
1071
  [`src/lib/agent/${weaver.id}-agent-panel.ts`]: panelFile(weaver),
1090
1072
  [`src/lib/agent/${weaver.id}-agent-panel.html`]: panelTemplateFile(weaver)
1091
1073
  };
1092
1074
  if (weaver.features.spec) {
1093
- files[`src/lib/agent/${weaver.id}-agent.spec.ts`] = specFile(weaver);
1075
+ files[`src/lib/agent/${weaver.id}-connection.spec.ts`] = specFile(weaver);
1094
1076
  }
1095
1077
  return files;
1096
1078
  }
@@ -1129,6 +1111,45 @@ function i18nFile(weaver) {
1129
1111
  return JSON.stringify(i18nBundle(weaver), null, 2) + "\n";
1130
1112
  }
1131
1113
 
1114
+ // ../devkit/src/lib/scaffolds/scaffold-values.ts
1115
+ var NEUTRAL_PREFIX = "app";
1116
+ function stringValue(values, name) {
1117
+ const value = values[name];
1118
+ return typeof value === "string" ? value : void 0;
1119
+ }
1120
+ function booleanValue(values, name) {
1121
+ const value = values[name];
1122
+ return typeof value === "boolean" ? value : void 0;
1123
+ }
1124
+ var PLACEMENT_OPTIONS = [
1125
+ {
1126
+ name: "directory",
1127
+ type: "string",
1128
+ description: "Project root, relative to the workspace root.",
1129
+ workspaceOnly: true
1130
+ },
1131
+ {
1132
+ name: "tags",
1133
+ type: "string",
1134
+ description: "Comma-separated Nx tags for the project.",
1135
+ workspaceOnly: true
1136
+ },
1137
+ {
1138
+ name: "unitTestRunner",
1139
+ type: "string",
1140
+ description: "Test wiring to emit. 'vitest' uses @nx/angular:unit-test; 'none' emits no test target.",
1141
+ choices: ["vitest", "none"],
1142
+ default: "vitest",
1143
+ workspaceOnly: true
1144
+ }
1145
+ ];
1146
+ var APP_OPTION = {
1147
+ name: "app",
1148
+ type: "string",
1149
+ description: "Application to drop into. Inferred when the workspace has exactly one.",
1150
+ workspaceOnly: true
1151
+ };
1152
+
1132
1153
  // ../devkit/src/lib/validate/manifest.ts
1133
1154
  var KNOWN_CAPABILITIES = [
1134
1155
  "contributions",
@@ -1288,6 +1309,18 @@ function deriveCapabilities(features) {
1288
1309
  }
1289
1310
  return KNOWN_CAPABILITIES.filter((capability) => set.has(capability));
1290
1311
  }
1312
+ function resolvePrefix(prefix) {
1313
+ const trimmed = prefix?.trim();
1314
+ if (!trimmed) {
1315
+ return NEUTRAL_PREFIX;
1316
+ }
1317
+ if (!isKebabId(trimmed)) {
1318
+ throw new Error(
1319
+ `Selector prefix must be kebab-case (e.g. "acme"); got "${trimmed}".`
1320
+ );
1321
+ }
1322
+ return trimmed;
1323
+ }
1291
1324
  function resolveWeaverInput(input) {
1292
1325
  if (!isKebabId(input.id)) {
1293
1326
  throw new Error(
@@ -1303,7 +1336,7 @@ function resolveWeaverInput(input) {
1303
1336
  propertyName: toCamelCase(input.id),
1304
1337
  capabilities,
1305
1338
  features,
1306
- prefix: input.prefix?.trim() || "lw",
1339
+ prefix: resolvePrefix(input.prefix),
1307
1340
  importPath: input.importPath?.trim() || `@loomweaver/${input.id}-weaver`
1308
1341
  };
1309
1342
  }
@@ -1500,7 +1533,7 @@ function pluginImports(weaver) {
1500
1533
  }
1501
1534
  if (weaver.features.agent) {
1502
1535
  imports.push(
1503
- `import { ${weaver.propertyName}Agent, ${weaver.propertyName}Connection } from '../agent/${weaver.id}-agent';`,
1536
+ `import { connect${weaver.className}, ${weaver.propertyName}Tools } from '../agent/${weaver.id}-connection';`,
1504
1537
  `import { ${weaver.className}AgentPanel } from '../agent/${weaver.id}-agent-panel';`
1505
1538
  );
1506
1539
  }
@@ -1525,7 +1558,7 @@ function pluginBody(weaver) {
1525
1558
  const body = [` ctx.contributeIcons({ '${weaver.id}': icon });`];
1526
1559
  if (weaver.features.agent) {
1527
1560
  body.push(
1528
- ` ${weaver.propertyName}Agent.set(${weaver.propertyName}Connection(ctx));`
1561
+ ` ${weaver.propertyName}Tools.set(connect${weaver.className}(ctx));`
1529
1562
  );
1530
1563
  }
1531
1564
  if (weaver.features.command) body.push(commandBlock(weaver));
@@ -1542,7 +1575,7 @@ function pluginFile(weaver) {
1542
1575
  const body = pluginBody(weaver);
1543
1576
  const deactivate = weaver.features.agent ? `
1544
1577
  deactivate() {
1545
- ${weaver.propertyName}Agent.set(null);
1578
+ ${weaver.propertyName}Tools.set(null);
1546
1579
  },` : "";
1547
1580
  return `${pluginImports(weaver).join("\n")}
1548
1581
 
@@ -1565,7 +1598,7 @@ function accessLine(weaver) {
1565
1598
  }
1566
1599
 
1567
1600
  // ../devkit/src/recipes/platform-version.ts
1568
- var PLATFORM_VERSION = "0.14.2";
1601
+ var PLATFORM_VERSION = "0.14.4";
1569
1602
 
1570
1603
  // ../devkit/src/recipes/readme-notes.ts
1571
1604
  var UNTAGGED_NOTE = [
@@ -1639,11 +1672,11 @@ function agentNotes(weaver) {
1639
1672
  "",
1640
1673
  `\`src/lib/agent/\` holds three files and one of them is meant to be thrown away.`,
1641
1674
  "",
1642
- `- \`${weaver.id}-agent.ts\` is the connection: the workbench's own commands offered as tools, and a`,
1675
+ `- \`${weaver.id}-connection.ts\` is the connection: the workbench's own commands offered as tools, and a`,
1643
1676
  " seam where this weaver decides about a call before it runs. Nothing is registered twice \u2014 the",
1644
1677
  " list comes from the workbench, already narrowed by everything that would refuse the call.",
1645
1678
  `- \`${weaver.id}-agent-panel.ts\` shows what is offered, the call as it streams, and the outcome.`,
1646
- `- \`${weaver.id}-agent-source.ts\` is a **stand-in**, not an assistant: it produces the protocol's own`,
1679
+ `- \`${weaver.id}-agent.ts\` is a **stand-in** for the agent, not an assistant: it produces the protocol's own`,
1647
1680
  " events so the whole path runs before you have connected anything. Replace that one file with",
1648
1681
  " your transport and nothing else changes. No transport, credential or model is generated for",
1649
1682
  " you, because none of them can be guessed.",
@@ -3008,52 +3041,6 @@ var layout = {
3008
3041
  }
3009
3042
  };
3010
3043
 
3011
- // ../devkit/src/lib/scaffolds/scaffold-values.ts
3012
- function stringValue(values, name) {
3013
- const value = values[name];
3014
- return typeof value === "string" ? value : void 0;
3015
- }
3016
- function booleanValue(values, name) {
3017
- const value = values[name];
3018
- return typeof value === "boolean" ? value : void 0;
3019
- }
3020
- var PLACEMENT_OPTIONS = [
3021
- {
3022
- name: "directory",
3023
- type: "string",
3024
- description: "Project root, relative to the workspace root.",
3025
- workspaceOnly: true
3026
- },
3027
- {
3028
- name: "tags",
3029
- type: "string",
3030
- description: "Comma-separated Nx tags for the project.",
3031
- workspaceOnly: true
3032
- },
3033
- {
3034
- name: "prefix",
3035
- type: "string",
3036
- description: "Selector prefix for generated components and directives.",
3037
- default: "lw",
3038
- pattern: KEBAB_ID_PATTERN,
3039
- workspaceOnly: true
3040
- },
3041
- {
3042
- name: "unitTestRunner",
3043
- type: "string",
3044
- description: "Test wiring to emit. 'vitest' uses @nx/angular:unit-test; 'none' emits no test target.",
3045
- choices: ["vitest", "none"],
3046
- default: "vitest",
3047
- workspaceOnly: true
3048
- }
3049
- ];
3050
- var APP_OPTION = {
3051
- name: "app",
3052
- type: "string",
3053
- description: "Application to drop into. Inferred when the workspace has exactly one.",
3054
- workspaceOnly: true
3055
- };
3056
-
3057
3044
  // ../devkit/src/recipes/angular-distribution/scaffold.ts
3058
3045
  function distributionInput(values) {
3059
3046
  return {
@@ -3092,6 +3079,13 @@ var distributionScaffold = {
3092
3079
  description: "Compose into the application already at that path, replacing the bootstrap files this scaffold owns and merging its build targets. Without it an existing project is an error.",
3093
3080
  workspaceOnly: true
3094
3081
  },
3082
+ {
3083
+ name: "prefix",
3084
+ type: "string",
3085
+ description: "Selector prefix the application declares for its components, which a weaver generated into it takes. Defaults to 'app'.",
3086
+ pattern: KEBAB_ID_PATTERN,
3087
+ workspaceOnly: true
3088
+ },
3095
3089
  ...PLACEMENT_OPTIONS
3096
3090
  ],
3097
3091
  build: (values) => generate(angularDistribution, distributionInput(values)),
@@ -3255,6 +3249,12 @@ var weaverScaffold = {
3255
3249
  description: "Import path for the workspace alias. Defaults to the workspace scope plus the project name.",
3256
3250
  workspaceOnly: true
3257
3251
  },
3252
+ {
3253
+ name: "prefix",
3254
+ type: "string",
3255
+ description: "Selector prefix for the generated components: the one the application declares, as the prefix of its project in angular.json or project.json. Read from the workspace where it can be, else 'app'.",
3256
+ pattern: KEBAB_ID_PATTERN
3257
+ },
3258
3258
  APP_OPTION,
3259
3259
  ...PLACEMENT_OPTIONS
3260
3260
  ],
@@ -3986,7 +3986,8 @@ function buildProjects(config) {
3986
3986
  const projects = asObject2(asObject2(config)?.["projects"]) ?? {};
3987
3987
  return Object.entries(projects).filter(([, project]) => buildTargetIn(project) !== void 0).map(([name, project]) => ({
3988
3988
  name,
3989
- root: normaliseRoot(asObject2(project)?.["root"])
3989
+ root: normaliseRoot(asObject2(project)?.["root"]),
3990
+ prefix: declaredPrefix(asObject2(project)?.["prefix"])
3990
3991
  }));
3991
3992
  }
3992
3993
  function buildTargetOf(config, project) {
@@ -4015,6 +4016,9 @@ function buildTargetIn(project) {
4015
4016
  }
4016
4017
  return void 0;
4017
4018
  }
4019
+ function declaredPrefix(value) {
4020
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
4021
+ }
4018
4022
  function normaliseRoot(value) {
4019
4023
  return typeof value === "string" ? withoutTrailingSlashes(value.replace(/^\.?\/*/, "")) : "";
4020
4024
  }
@@ -4153,18 +4157,33 @@ function directoryFromOut(out) {
4153
4157
  }
4154
4158
  return below || ".";
4155
4159
  }
4160
+ function declaredPrefix2(out) {
4161
+ const target = resolve3(out ?? ".");
4162
+ const workspace = findWorkspace(target);
4163
+ if (workspace?.kind !== "angular") {
4164
+ return void 0;
4165
+ }
4166
+ try {
4167
+ return resolveBuildProject(workspace, target).prefix;
4168
+ } catch (error) {
4169
+ if (error instanceof WorkspaceError) {
4170
+ return void 0;
4171
+ }
4172
+ throw error;
4173
+ }
4174
+ }
4156
4175
  function scaffoldValues(scaffold2, args) {
4176
+ const flag = args.flags["out"];
4177
+ const out = typeof flag === "string" ? flag : void 0;
4157
4178
  const values = valuesFor(scaffold2, args);
4179
+ const readsPrefix = values["prefix"] === void 0 && portableOptions(scaffold2).some((option) => option.name === "prefix");
4158
4180
  const takesDirectory = scaffold2.options.some(
4159
4181
  (option) => option.name === "directory"
4160
4182
  );
4161
- if (!takesDirectory) {
4162
- return values;
4163
- }
4164
- const out = args.flags["out"];
4165
4183
  return {
4166
4184
  ...values,
4167
- directory: directoryFromOut(typeof out === "string" ? out : void 0)
4185
+ ...readsPrefix && { prefix: declaredPrefix2(out) },
4186
+ ...takesDirectory && { directory: directoryFromOut(out) }
4168
4187
  };
4169
4188
  }
4170
4189
 
@@ -5180,7 +5199,7 @@ function validateCatalogCommand(args, io) {
5180
5199
  }
5181
5200
 
5182
5201
  // src/lib/run.ts
5183
- var VERSION = "0.14.2";
5202
+ var VERSION = "0.14.4";
5184
5203
  var COMMANDS = /* @__PURE__ */ new Map([
5185
5204
  ["init", init],
5186
5205
  ["list", list],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loomweaver/cli",
3
- "version": "0.14.2",
3
+ "version": "0.14.4",
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",