@loomweaver/cli 0.7.5 → 0.7.7
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/main.mjs +853 -178
- package/package.json +1 -1
package/dist/main.mjs
CHANGED
|
@@ -6,7 +6,7 @@ function isUnsafePath(path) {
|
|
|
6
6
|
}
|
|
7
7
|
function generate(recipe, input) {
|
|
8
8
|
const files = recipe.build(input);
|
|
9
|
-
const unsafe = Object.keys(files).find(isUnsafePath);
|
|
9
|
+
const unsafe = Object.keys(files).find((path) => isUnsafePath(path));
|
|
10
10
|
if (unsafe !== void 0) {
|
|
11
11
|
throw new Error(`Recipe "${recipe.id}" produced an unsafe path: "${unsafe}".`);
|
|
12
12
|
}
|
|
@@ -17,8 +17,16 @@ function amendments(recipe, input) {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
// ../devkit/src/lib/amend/merge.ts
|
|
20
|
+
function normalizeProjectRoot(value) {
|
|
21
|
+
const rooted = value.replace(/^\.?\/*/, "");
|
|
22
|
+
let end = rooted.length;
|
|
23
|
+
while (end > 0 && rooted[end - 1] === "/") {
|
|
24
|
+
end -= 1;
|
|
25
|
+
}
|
|
26
|
+
return rooted.slice(0, end);
|
|
27
|
+
}
|
|
20
28
|
function joinProjectPath(projectRoot, path) {
|
|
21
|
-
const root = projectRoot
|
|
29
|
+
const root = normalizeProjectRoot(projectRoot);
|
|
22
30
|
return root ? `${root}/${path}` : path;
|
|
23
31
|
}
|
|
24
32
|
function resolveAssetInput(glob, projectRoot) {
|
|
@@ -34,8 +42,8 @@ function ensurePostcssPlugin(existing, amendment) {
|
|
|
34
42
|
declined: [`${amendment.file}: "plugins" is not an object`]
|
|
35
43
|
};
|
|
36
44
|
}
|
|
37
|
-
const next = { ...plugins
|
|
38
|
-
if (amendment.plugin
|
|
45
|
+
const next = { ...plugins };
|
|
46
|
+
if (Object.hasOwn(next, amendment.plugin)) {
|
|
39
47
|
return { value: root, added: [], declined: [] };
|
|
40
48
|
}
|
|
41
49
|
next[amendment.plugin] = {};
|
|
@@ -45,11 +53,37 @@ function ensurePostcssPlugin(existing, amendment) {
|
|
|
45
53
|
declined: []
|
|
46
54
|
};
|
|
47
55
|
}
|
|
56
|
+
function ensureDependency(manifest2, amendment) {
|
|
57
|
+
const root = asObject(manifest2) ?? {};
|
|
58
|
+
const dependencies = asObject(root["dependencies"]);
|
|
59
|
+
if (dependencies === void 0 && root["dependencies"] !== void 0) {
|
|
60
|
+
return {
|
|
61
|
+
value: root,
|
|
62
|
+
added: [],
|
|
63
|
+
declined: ['package.json: "dependencies" is not an object']
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const recorded = dependencies?.[amendment.name] ?? asObject(root["devDependencies"])?.[amendment.name];
|
|
67
|
+
if (recorded !== void 0) {
|
|
68
|
+
return { value: root, added: [], declined: [] };
|
|
69
|
+
}
|
|
70
|
+
const next = { ...dependencies, [amendment.name]: amendment.version };
|
|
71
|
+
return {
|
|
72
|
+
value: {
|
|
73
|
+
...root,
|
|
74
|
+
dependencies: Object.fromEntries(
|
|
75
|
+
Object.entries(next).toSorted(([a], [b]) => a.localeCompare(b))
|
|
76
|
+
)
|
|
77
|
+
},
|
|
78
|
+
added: [`dependencies: ${amendment.name}@${amendment.version}`],
|
|
79
|
+
declined: []
|
|
80
|
+
};
|
|
81
|
+
}
|
|
48
82
|
function ensureBuildTarget(target, amendment, projectRoot) {
|
|
49
|
-
const next = { ...asObject(target)
|
|
83
|
+
const next = { ...asObject(target) };
|
|
50
84
|
const added = [];
|
|
51
85
|
const declined = [];
|
|
52
|
-
const options = { ...asObject(next["options"])
|
|
86
|
+
const options = { ...asObject(next["options"]) };
|
|
53
87
|
const styles = ensureStrings(
|
|
54
88
|
options["styles"],
|
|
55
89
|
amendment.styles.map((style) => joinProjectPath(projectRoot, style))
|
|
@@ -65,8 +99,8 @@ function ensureBuildTarget(target, amendment, projectRoot) {
|
|
|
65
99
|
}
|
|
66
100
|
next["options"] = options;
|
|
67
101
|
if (amendment.inlineCritical !== void 0 || amendment.serviceWorker) {
|
|
68
|
-
const configurations = { ...asObject(next["configurations"])
|
|
69
|
-
const production = { ...asObject(configurations["production"])
|
|
102
|
+
const configurations = { ...asObject(next["configurations"]) };
|
|
103
|
+
const production = { ...asObject(configurations["production"]) };
|
|
70
104
|
if (amendment.serviceWorker && production["serviceWorker"] === void 0) {
|
|
71
105
|
production["serviceWorker"] = joinProjectPath(
|
|
72
106
|
projectRoot,
|
|
@@ -96,8 +130,8 @@ function ensureBuildTarget(target, amendment, projectRoot) {
|
|
|
96
130
|
return { value: next, added, declined };
|
|
97
131
|
}
|
|
98
132
|
function ensureStylesheetSource(css, source) {
|
|
99
|
-
const quoted = source.
|
|
100
|
-
if (new RegExp(`@source
|
|
133
|
+
const quoted = source.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
|
|
134
|
+
if (new RegExp(String.raw`@source\s+['"]${quoted}/?['"]`).test(css)) {
|
|
101
135
|
return css;
|
|
102
136
|
}
|
|
103
137
|
return `${css.trimEnd()}
|
|
@@ -109,7 +143,7 @@ function ensureInlineCritical(optimization, inlineCritical) {
|
|
|
109
143
|
if (typeof optimization === "boolean") {
|
|
110
144
|
return { value: optimization, changed: false, declined: true };
|
|
111
145
|
}
|
|
112
|
-
const root = { ...asObject(optimization)
|
|
146
|
+
const root = { ...asObject(optimization) };
|
|
113
147
|
const styles = asObject(root["styles"]);
|
|
114
148
|
if (styles === void 0 && root["styles"] !== void 0) {
|
|
115
149
|
return { value: optimization, changed: false, declined: true };
|
|
@@ -118,7 +152,7 @@ function ensureInlineCritical(optimization, inlineCritical) {
|
|
|
118
152
|
return { value: optimization, changed: false, declined: false };
|
|
119
153
|
}
|
|
120
154
|
return {
|
|
121
|
-
value: { ...root, styles: { ...styles
|
|
155
|
+
value: { ...root, styles: { ...styles, inlineCritical } },
|
|
122
156
|
changed: true,
|
|
123
157
|
declined: false
|
|
124
158
|
};
|
|
@@ -127,10 +161,11 @@ function ensureStrings(existing, wanted) {
|
|
|
127
161
|
const list2 = Array.isArray(existing) ? [...existing] : [];
|
|
128
162
|
const added = [];
|
|
129
163
|
for (const entry of wanted) {
|
|
130
|
-
if (
|
|
131
|
-
|
|
132
|
-
added.push(entry);
|
|
164
|
+
if (list2.includes(entry)) {
|
|
165
|
+
continue;
|
|
133
166
|
}
|
|
167
|
+
list2.push(entry);
|
|
168
|
+
added.push(entry);
|
|
134
169
|
}
|
|
135
170
|
return { value: list2, added };
|
|
136
171
|
}
|
|
@@ -145,7 +180,7 @@ function ensureAssets(existing, wanted, projectRoot) {
|
|
|
145
180
|
list2.push({
|
|
146
181
|
glob: glob.glob,
|
|
147
182
|
input,
|
|
148
|
-
...glob.output
|
|
183
|
+
...glob.output !== void 0 && { output: glob.output }
|
|
149
184
|
});
|
|
150
185
|
added.push(input);
|
|
151
186
|
}
|
|
@@ -163,34 +198,60 @@ function asObject(value) {
|
|
|
163
198
|
}
|
|
164
199
|
|
|
165
200
|
// ../devkit/src/lib/amend/compose.ts
|
|
166
|
-
var
|
|
201
|
+
var APP_CONFIG = /export\s+const\s+appConfig\s*:[^=]*=\s*\{/;
|
|
202
|
+
var PROVIDERS_OPEN = /providers\s*:\s*\[/g;
|
|
167
203
|
var SHELL_IMPORT = /import\s*\{([^}]*)\}\s*from\s*'@loomweaver\/shell';/;
|
|
204
|
+
function closingLine(source, from) {
|
|
205
|
+
let close = source.indexOf("],", from);
|
|
206
|
+
while (close !== -1) {
|
|
207
|
+
let start = close;
|
|
208
|
+
while (start > from && source.charAt(start - 1).trim() === "") {
|
|
209
|
+
start -= 1;
|
|
210
|
+
}
|
|
211
|
+
const gap = source.slice(start, close);
|
|
212
|
+
const newline = gap.indexOf("\n");
|
|
213
|
+
if (newline !== -1) {
|
|
214
|
+
return { insertAt: start + newline, indent: gap.slice(newline + 1) };
|
|
215
|
+
}
|
|
216
|
+
close = source.indexOf("],", close + 2);
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
function providersBlock(source) {
|
|
221
|
+
const declaration = APP_CONFIG.exec(source);
|
|
222
|
+
if (!declaration) {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
PROVIDERS_OPEN.lastIndex = declaration.index + declaration[0].length;
|
|
226
|
+
const open = PROVIDERS_OPEN.exec(source);
|
|
227
|
+
return open ? closingLine(source, open.index + open[0].length) : null;
|
|
228
|
+
}
|
|
168
229
|
function composePlugin(source, amendment, importPath) {
|
|
169
230
|
if (source.includes(amendment.symbol)) {
|
|
170
231
|
return { source, composed: true };
|
|
171
232
|
}
|
|
172
|
-
const providers = PROVIDERS.exec(source);
|
|
173
233
|
const shellImport = SHELL_IMPORT.exec(source);
|
|
174
|
-
if (!
|
|
234
|
+
if (!shellImport || !providersBlock(source)) {
|
|
175
235
|
return { source, composed: false };
|
|
176
236
|
}
|
|
177
237
|
const withImports = source.replace(
|
|
178
238
|
SHELL_IMPORT,
|
|
179
|
-
`import {${withShellSymbols(shellImport[1])}} from '@loomweaver/shell';
|
|
239
|
+
() => `import {${withShellSymbols(shellImport[1])}} from '@loomweaver/shell';
|
|
180
240
|
import { ${amendment.symbol} } from '${importPath}';`
|
|
181
241
|
);
|
|
182
|
-
const
|
|
242
|
+
const block = providersBlock(withImports);
|
|
243
|
+
if (!block) {
|
|
244
|
+
return { source, composed: false };
|
|
245
|
+
}
|
|
246
|
+
const indent = `${block.indent} `;
|
|
183
247
|
const lines = [
|
|
184
248
|
`${indent}provideTranslationNamespaces('${amendment.id}'),`,
|
|
185
249
|
`${indent}provideCapabilityGrants({ ${amendment.id}: [${amendment.capabilities.map((capability) => `'${capability}'`).join(", ")}] }),`,
|
|
186
250
|
`${indent}...providePlugins(${amendment.symbol}),`
|
|
187
251
|
].join("\n");
|
|
188
252
|
return {
|
|
189
|
-
source: withImports.
|
|
190
|
-
|
|
191
|
-
(_all, head, body, tail) => `${head}${body}
|
|
192
|
-
${lines}${tail}`
|
|
193
|
-
),
|
|
253
|
+
source: `${withImports.slice(0, block.insertAt)}
|
|
254
|
+
${lines}${withImports.slice(block.insertAt)}`,
|
|
194
255
|
composed: true
|
|
195
256
|
};
|
|
196
257
|
}
|
|
@@ -211,13 +272,13 @@ function withShellSymbols(existing) {
|
|
|
211
272
|
];
|
|
212
273
|
const present = existing.split(",").map((symbol) => symbol.trim()).filter(Boolean);
|
|
213
274
|
const missing = wanted.filter(
|
|
214
|
-
(symbol) =>
|
|
275
|
+
(symbol) => present.every((entry) => entry.replace(/^type\s+/, "") !== symbol)
|
|
215
276
|
);
|
|
216
277
|
if (missing.length === 0) {
|
|
217
278
|
return existing;
|
|
218
279
|
}
|
|
219
280
|
const multiline = existing.includes("\n");
|
|
220
|
-
const all = [...present, ...missing].
|
|
281
|
+
const all = [...present, ...missing].toSorted((a, b) => a.localeCompare(b));
|
|
221
282
|
return multiline ? `
|
|
222
283
|
${all.join(",\n ")},
|
|
223
284
|
` : ` ${all.join(", ")} `;
|
|
@@ -228,6 +289,9 @@ function describeAmendment(amendment) {
|
|
|
228
289
|
if (amendment.kind === "postcss") {
|
|
229
290
|
return `Write ${amendment.file} beside your package.json, naming ${amendment.plugin}. Without it the stylesheet is read as plain CSS: no utility class is emitted, the workbench renders unstyled, and the build still reports success.`;
|
|
230
291
|
}
|
|
292
|
+
if (amendment.kind === "package") {
|
|
293
|
+
return `Add ${amendment.name}@${amendment.version} to your project's dependencies and install it. Without it the generated files import a package that is not there, so the very first build fails.`;
|
|
294
|
+
}
|
|
231
295
|
if (amendment.kind === "stylesheet-source") {
|
|
232
296
|
return `Add an @source entry for '${amendment.sourceRoot}' to the application's entry stylesheet, resolved from that stylesheet. Without it none of that code's utilities are emitted.`;
|
|
233
297
|
}
|
|
@@ -239,7 +303,9 @@ function describeAmendment(amendment) {
|
|
|
239
303
|
return [
|
|
240
304
|
...amendment.styles.length > 0 ? [`name ${amendment.styles.join(", ")} in styles`] : [],
|
|
241
305
|
...amendment.assets.length > 0 ? [
|
|
242
|
-
`add assets for ${amendment.assets.map((asset) => asset.input).join(
|
|
306
|
+
`add assets for ${amendment.assets.map((asset) => asset.input).join(
|
|
307
|
+
", "
|
|
308
|
+
)} (the shell fetches its own strings at runtime, so without that glob every label in the chrome renders as its raw translation key)`
|
|
243
309
|
] : [],
|
|
244
310
|
...amendment.serviceWorker ? [
|
|
245
311
|
`set serviceWorker to ${amendment.serviceWorker} in the production configuration (provideShell registers a worker that 404s otherwise)`
|
|
@@ -338,6 +404,589 @@ function validateCapabilities(capabilities, known) {
|
|
|
338
404
|
return findings;
|
|
339
405
|
}
|
|
340
406
|
|
|
407
|
+
// ../devkit/src/recipes/angular-weaver/agent-panel.ts
|
|
408
|
+
function panelFile(w) {
|
|
409
|
+
return `import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
|
|
410
|
+
import { EventType, type BaseEvent, type Tool } from '@ag-ui/core';
|
|
411
|
+
import { ${w.propertyName}Agent } from './${w.id}-agent';
|
|
412
|
+
import { askAgent } from './${w.id}-agent-source';
|
|
413
|
+
|
|
414
|
+
interface Line {
|
|
415
|
+
readonly kind: 'you' | 'agent' | 'call' | 'result' | 'note';
|
|
416
|
+
readonly text: string;
|
|
417
|
+
readonly args?: string;
|
|
418
|
+
readonly failed?: boolean;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
@Component({
|
|
422
|
+
selector: '${w.prefix}-${w.id}-agent-panel',
|
|
423
|
+
templateUrl: './${w.id}-agent-panel.html',
|
|
424
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
425
|
+
})
|
|
426
|
+
export class ${w.className}AgentPanel {
|
|
427
|
+
protected readonly offered = signal<readonly Tool[]>([]);
|
|
428
|
+
|
|
429
|
+
protected readonly lines = signal<readonly Line[]>([]);
|
|
430
|
+
|
|
431
|
+
protected readonly busy = signal(false);
|
|
432
|
+
|
|
433
|
+
private runs = 0;
|
|
434
|
+
|
|
435
|
+
constructor() {
|
|
436
|
+
this.offered.set(${w.propertyName}Agent()?.list() ?? []);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
protected async ask(name: string): Promise<void> {
|
|
440
|
+
const tools = ${w.propertyName}Agent();
|
|
441
|
+
if (!tools || this.busy()) {
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
this.busy.set(true);
|
|
445
|
+
try {
|
|
446
|
+
// Ask for the list again, every run. What a plugin may reach changes as plugins load and the
|
|
447
|
+
// session changes, and a list kept from earlier offers actions that are no longer there.
|
|
448
|
+
const offered = tools.list();
|
|
449
|
+
this.offered.set(offered);
|
|
450
|
+
this.push({ kind: 'you', text: \`Run \${name}\` });
|
|
451
|
+
this.push({ kind: 'note', text: \`\${offered.length} tool(s) offered right now\` });
|
|
452
|
+
|
|
453
|
+
const request = {
|
|
454
|
+
runId: \`run-\${++this.runs}\`,
|
|
455
|
+
prompt: name,
|
|
456
|
+
tools: offered,
|
|
457
|
+
};
|
|
458
|
+
for await (const event of askAgent(request)) {
|
|
459
|
+
this.draw(event);
|
|
460
|
+
// Every event goes over, unfiltered. The adapter decides which ones matter; a filter here is
|
|
461
|
+
// how a call ends up half-assembled.
|
|
462
|
+
const message = await tools.receive(event);
|
|
463
|
+
if (message) {
|
|
464
|
+
this.push({
|
|
465
|
+
kind: 'result',
|
|
466
|
+
text: message.error ?? message.content,
|
|
467
|
+
failed: Boolean(message.error),
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
// A run that ends without its closing event leaves a call open; flush() answers it.
|
|
472
|
+
const last = await tools.flush();
|
|
473
|
+
if (last) {
|
|
474
|
+
this.push({
|
|
475
|
+
kind: 'result',
|
|
476
|
+
text: last.error ?? last.content,
|
|
477
|
+
failed: Boolean(last.error),
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
} finally {
|
|
481
|
+
this.busy.set(false);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
private draw(event: BaseEvent): void {
|
|
486
|
+
const raw = event as unknown as Record<string, unknown>;
|
|
487
|
+
switch (event.type) {
|
|
488
|
+
case EventType.TEXT_MESSAGE_START:
|
|
489
|
+
this.push({ kind: 'agent', text: '' });
|
|
490
|
+
break;
|
|
491
|
+
case EventType.TEXT_MESSAGE_CONTENT:
|
|
492
|
+
this.grow('text', String(raw['delta'] ?? ''));
|
|
493
|
+
break;
|
|
494
|
+
case EventType.TOOL_CALL_START:
|
|
495
|
+
this.push({
|
|
496
|
+
kind: 'call',
|
|
497
|
+
text: String(raw['toolCallName'] ?? ''),
|
|
498
|
+
args: '',
|
|
499
|
+
});
|
|
500
|
+
break;
|
|
501
|
+
case EventType.TOOL_CALL_ARGS:
|
|
502
|
+
this.grow('args', String(raw['delta'] ?? ''));
|
|
503
|
+
break;
|
|
504
|
+
default:
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
private push(line: Line): void {
|
|
510
|
+
this.lines.update((all) => [...all, line]);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
private grow(field: 'text' | 'args', delta: string): void {
|
|
514
|
+
this.lines.update((all) => {
|
|
515
|
+
const last = all[all.length - 1];
|
|
516
|
+
return [
|
|
517
|
+
...all.slice(0, -1),
|
|
518
|
+
{ ...last, [field]: \`\${last[field] ?? ''}\${delta}\` },
|
|
519
|
+
];
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
`;
|
|
524
|
+
}
|
|
525
|
+
function panelTemplateFile(w) {
|
|
526
|
+
return `<div class="flex h-full flex-col gap-3">
|
|
527
|
+
<p
|
|
528
|
+
class="shrink-0 rounded-md border border-border bg-surface-raised px-3 py-2 text-xs text-content-muted"
|
|
529
|
+
>
|
|
530
|
+
This is a stand-in, not an assistant. It speaks the protocol so you can watch the whole path;
|
|
531
|
+
replace <code class="text-content">${w.id}-agent-source.ts</code> with your own transport.
|
|
532
|
+
</p>
|
|
533
|
+
|
|
534
|
+
<div class="min-h-0 flex-1 overflow-auto">
|
|
535
|
+
<ul class="flex flex-col gap-2.5">
|
|
536
|
+
@for (line of lines(); track $index) {
|
|
537
|
+
@if (line.kind === 'you') {
|
|
538
|
+
<li class="max-w-full self-end rounded-lg bg-brand-fill px-3 py-2 text-sm text-on-brand">
|
|
539
|
+
{{ line.text }}
|
|
540
|
+
</li>
|
|
541
|
+
} @else if (line.kind === 'agent') {
|
|
542
|
+
<li
|
|
543
|
+
class="max-w-full self-start rounded-lg bg-surface-raised px-3 py-2 text-sm text-content"
|
|
544
|
+
>
|
|
545
|
+
{{ line.text }}
|
|
546
|
+
</li>
|
|
547
|
+
} @else if (line.kind === 'call') {
|
|
548
|
+
<li
|
|
549
|
+
class="rounded-md border border-border px-3 py-2 font-mono text-xs break-all text-content-muted"
|
|
550
|
+
>
|
|
551
|
+
{{ line.text }}<span class="text-content-faint">({{ line.args }})</span>
|
|
552
|
+
</li>
|
|
553
|
+
} @else if (line.kind === 'result') {
|
|
554
|
+
<li
|
|
555
|
+
class="px-1 text-xs"
|
|
556
|
+
[class.text-negative]="line.failed"
|
|
557
|
+
[class.text-content-muted]="!line.failed"
|
|
558
|
+
>
|
|
559
|
+
{{ line.text }}
|
|
560
|
+
</li>
|
|
561
|
+
} @else {
|
|
562
|
+
<li class="px-1 text-xs text-content-faint">{{ line.text }}</li>
|
|
563
|
+
}
|
|
564
|
+
} @empty {
|
|
565
|
+
<li class="px-1 py-6 text-center text-sm text-content-muted">
|
|
566
|
+
Pick something below and watch the call go through.
|
|
567
|
+
</li>
|
|
568
|
+
}
|
|
569
|
+
</ul>
|
|
570
|
+
</div>
|
|
571
|
+
|
|
572
|
+
<div class="flex shrink-0 flex-col gap-2 border-t border-border pt-3">
|
|
573
|
+
<span class="px-1 text-xs font-medium text-content-muted">
|
|
574
|
+
Offered right now ({{ offered().length }})
|
|
575
|
+
</span>
|
|
576
|
+
@for (tool of offered(); track tool.name) {
|
|
577
|
+
<button
|
|
578
|
+
type="button"
|
|
579
|
+
class="lw-btn lw-btn--default lw-btn--sm h-auto justify-start py-2 text-left whitespace-normal"
|
|
580
|
+
[disabled]="busy()"
|
|
581
|
+
(click)="ask(tool.name)"
|
|
582
|
+
>
|
|
583
|
+
{{ tool.description }}
|
|
584
|
+
</button>
|
|
585
|
+
} @empty {
|
|
586
|
+
<span class="px-1 text-xs text-content-faint">
|
|
587
|
+
Nothing is offered. A command reaches this list only when it is registered with
|
|
588
|
+
<code class="text-content">callable: true</code>, and reaching another plugin's commands
|
|
589
|
+
needs the <code class="text-content">automation</code> capability granted.
|
|
590
|
+
</span>
|
|
591
|
+
}
|
|
592
|
+
</div>
|
|
593
|
+
</div>
|
|
594
|
+
`;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// ../devkit/src/recipes/angular-weaver/agent-stand-in.ts
|
|
598
|
+
function standInFile(w) {
|
|
599
|
+
return `import { EventType, type BaseEvent, type Tool } from '@ag-ui/core';
|
|
600
|
+
|
|
601
|
+
// WHAT THIS IS: a stand-in for an agent, and not an agent. It speaks the AG-UI protocol and nothing
|
|
602
|
+
// else \u2014 no model, no network, no judgement \u2014 so that the whole path from the offered tools through
|
|
603
|
+
// a call to its outcome runs on the first serve, before you have connected anything.
|
|
604
|
+
//
|
|
605
|
+
// WHAT REPLACES IT: this file, and only this file. Point askAgent at your own endpoint and yield the
|
|
606
|
+
// events it streams back. The panel and the connection beside it stay exactly as they are.
|
|
607
|
+
|
|
608
|
+
const PACE = 40;
|
|
609
|
+
|
|
610
|
+
export interface AgentRequest {
|
|
611
|
+
readonly runId: string;
|
|
612
|
+
readonly prompt: string;
|
|
613
|
+
/** What the workbench offers right now \u2014 the same list a real agent would be handed. */
|
|
614
|
+
readonly tools: readonly Tool[];
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
export async function* askAgent(
|
|
618
|
+
request: AgentRequest,
|
|
619
|
+
): AsyncGenerator<BaseEvent> {
|
|
620
|
+
const picked = request.tools.find((tool) =>
|
|
621
|
+
request.prompt.includes(tool.name),
|
|
622
|
+
);
|
|
623
|
+
|
|
624
|
+
yield event(EventType.RUN_STARTED, {
|
|
625
|
+
threadId: '${w.id}-stand-in',
|
|
626
|
+
runId: request.runId,
|
|
627
|
+
});
|
|
628
|
+
yield* speak(
|
|
629
|
+
\`\${request.runId}.says\`,
|
|
630
|
+
picked
|
|
631
|
+
? \`You asked for "\${request.prompt}". I can see \${request.tools.length} tool(s) and \${picked.name} is one of them, so I will call it.\`
|
|
632
|
+
: \`You asked for "\${request.prompt}", and nothing among the \${request.tools.length} tool(s) I was offered matches it, so I will not call anything.\`,
|
|
633
|
+
);
|
|
634
|
+
|
|
635
|
+
if (picked) {
|
|
636
|
+
const toolCallId = \`\${request.runId}.call\`;
|
|
637
|
+
yield event(EventType.TOOL_CALL_START, {
|
|
638
|
+
toolCallId,
|
|
639
|
+
toolCallName: picked.name,
|
|
640
|
+
});
|
|
641
|
+
// 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)) {
|
|
646
|
+
yield event(EventType.TOOL_CALL_ARGS, { toolCallId, delta: piece });
|
|
647
|
+
await pause();
|
|
648
|
+
}
|
|
649
|
+
yield event(EventType.TOOL_CALL_END, { toolCallId });
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
yield event(EventType.RUN_FINISHED, {
|
|
653
|
+
threadId: '${w.id}-stand-in',
|
|
654
|
+
runId: request.runId,
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
async function* speak(
|
|
659
|
+
messageId: string,
|
|
660
|
+
text: string,
|
|
661
|
+
): AsyncGenerator<BaseEvent> {
|
|
662
|
+
yield event(EventType.TEXT_MESSAGE_START, { messageId, role: 'assistant' });
|
|
663
|
+
for (const piece of chunks(text, 10)) {
|
|
664
|
+
yield event(EventType.TEXT_MESSAGE_CONTENT, { messageId, delta: piece });
|
|
665
|
+
await pause();
|
|
666
|
+
}
|
|
667
|
+
yield event(EventType.TEXT_MESSAGE_END, { messageId });
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function chunks(text: string, size: number): readonly string[] {
|
|
671
|
+
const pieces: string[] = [];
|
|
672
|
+
for (let at = 0; at < text.length; at += size) {
|
|
673
|
+
pieces.push(text.slice(at, at + size));
|
|
674
|
+
}
|
|
675
|
+
return pieces;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function event(type: EventType, fields: Record<string, unknown>): BaseEvent {
|
|
679
|
+
return { type, ...fields } as unknown as BaseEvent;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function pause(): Promise<void> {
|
|
683
|
+
return new Promise((done) => setTimeout(done, PACE));
|
|
684
|
+
}
|
|
685
|
+
`;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// ../devkit/src/recipes/angular-weaver/agent-files.ts
|
|
689
|
+
var AG_UI_ADAPTER_VERSION = "0.7.7";
|
|
690
|
+
var AG_UI_PROTOCOL_VERSION = "0.0.x";
|
|
691
|
+
function connectionFile(w) {
|
|
692
|
+
return `import { signal } from '@angular/core';
|
|
693
|
+
import {
|
|
694
|
+
commandTools,
|
|
695
|
+
type CommandTools,
|
|
696
|
+
type PendingToolCall,
|
|
697
|
+
type ToolDecision,
|
|
698
|
+
} from '@loomweaver/ag-ui';
|
|
699
|
+
import type { PluginContext } from '@loomweaver/plugin-sdk';
|
|
700
|
+
|
|
701
|
+
// Commands an agent may not run on its own word. Each one asks the person at the keyboard first, and
|
|
702
|
+
// declining stops it: the workbench never sees the call. This weaver's own command is listed as an
|
|
703
|
+
// example \u2014 replace it with the ones that actually cost something.
|
|
704
|
+
const CONSEQUENTIAL = new Set(['${w.id}.hello']);
|
|
705
|
+
|
|
706
|
+
// A factory, not a module-level connection: everything a run needs lives in the closure, so a second
|
|
707
|
+
// one never shares state with the first.
|
|
708
|
+
export function ${w.propertyName}Connection(ctx: PluginContext): CommandTools {
|
|
709
|
+
return commandTools(ctx, { before: (call) => decide(ctx, call) });
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// The one connection this plugin activates, published for its panel. Set in activate(), cleared in
|
|
713
|
+
// deactivate(), so the panel renders an honest empty state either side of that.
|
|
714
|
+
export const ${w.propertyName}Agent = signal<CommandTools | null>(null);
|
|
715
|
+
|
|
716
|
+
async function decide(
|
|
717
|
+
ctx: PluginContext,
|
|
718
|
+
call: PendingToolCall,
|
|
719
|
+
): Promise<ToolDecision> {
|
|
720
|
+
if (!CONSEQUENTIAL.has(call.commandId)) {
|
|
721
|
+
return { decision: 'run' };
|
|
722
|
+
}
|
|
723
|
+
const yes = await ctx.ui.confirm({
|
|
724
|
+
title: '${w.id}.agent.confirm.title',
|
|
725
|
+
message: '${w.id}.agent.confirm.message',
|
|
726
|
+
confirmLabel: '${w.id}.agent.confirm.yes',
|
|
727
|
+
cancelLabel: '${w.id}.agent.confirm.no',
|
|
728
|
+
tone: 'warning',
|
|
729
|
+
});
|
|
730
|
+
// A decision can only narrow. Letting a call through does not make it reachable: the workbench
|
|
731
|
+
// still refuses whatever it always refused.
|
|
732
|
+
return yes
|
|
733
|
+
? { decision: 'run' }
|
|
734
|
+
: { decision: 'decline', reason: 'the person at the keyboard said no.' };
|
|
735
|
+
}
|
|
736
|
+
`;
|
|
737
|
+
}
|
|
738
|
+
function specFile(w) {
|
|
739
|
+
return `import { EventType, type BaseEvent } from '@ag-ui/core';
|
|
740
|
+
import type { CommandArguments, PluginContext } from '@loomweaver/plugin-sdk';
|
|
741
|
+
import { ${w.propertyName}Connection } from './${w.id}-agent';
|
|
742
|
+
|
|
743
|
+
interface Asked {
|
|
744
|
+
readonly id: string;
|
|
745
|
+
readonly args?: CommandArguments;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function contextThat(confirms: boolean, ran: Asked[]): PluginContext {
|
|
749
|
+
return {
|
|
750
|
+
invocableCommands: () => [
|
|
751
|
+
{ id: '${w.id}.hello', title: '${w.name} action', description: 'Shows a short message.' },
|
|
752
|
+
],
|
|
753
|
+
invokeCommand: (id: string, args?: CommandArguments) => {
|
|
754
|
+
ran.push({ id, args });
|
|
755
|
+
return Promise.resolve({ outcome: 'answered', value: 'it ran' });
|
|
756
|
+
},
|
|
757
|
+
ui: { confirm: () => Promise.resolve(confirms) },
|
|
758
|
+
} as unknown as PluginContext;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function event(type: EventType, fields: Record<string, unknown>): BaseEvent {
|
|
762
|
+
return { type, ...fields } as unknown as BaseEvent;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
describe('${w.propertyName}Connection', () => {
|
|
766
|
+
it('offers what the workbench offers', () => {
|
|
767
|
+
const tools = ${w.propertyName}Connection(contextThat(true, []));
|
|
768
|
+
expect(tools.list().map((tool) => tool.name)).toEqual(['${w.id}.hello']);
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
it('assembles a call from its events and answers with the outcome', async () => {
|
|
772
|
+
const ran: Asked[] = [];
|
|
773
|
+
const tools = ${w.propertyName}Connection(contextThat(true, ran));
|
|
774
|
+
|
|
775
|
+
expect(
|
|
776
|
+
await tools.receive(
|
|
777
|
+
event(EventType.TOOL_CALL_START, {
|
|
778
|
+
toolCallId: 'c1',
|
|
779
|
+
toolCallName: '${w.id}.hello',
|
|
780
|
+
}),
|
|
781
|
+
),
|
|
782
|
+
).toBeNull();
|
|
783
|
+
await tools.receive(
|
|
784
|
+
event(EventType.TOOL_CALL_ARGS, { toolCallId: 'c1', delta: '{"who"' }),
|
|
785
|
+
);
|
|
786
|
+
await tools.receive(
|
|
787
|
+
event(EventType.TOOL_CALL_ARGS, { toolCallId: 'c1', delta: ':"you"}' }),
|
|
788
|
+
);
|
|
789
|
+
const answer = await tools.receive(
|
|
790
|
+
event(EventType.TOOL_CALL_END, { toolCallId: 'c1' }),
|
|
791
|
+
);
|
|
792
|
+
|
|
793
|
+
expect(ran).toEqual([{ id: '${w.id}.hello', args: { who: 'you' } }]);
|
|
794
|
+
expect(answer?.content).toBe('it ran');
|
|
795
|
+
expect(answer?.error).toBeUndefined();
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
it('never reaches the workbench when a consequential call is declined', async () => {
|
|
799
|
+
const ran: Asked[] = [];
|
|
800
|
+
const tools = ${w.propertyName}Connection(contextThat(false, ran));
|
|
801
|
+
|
|
802
|
+
await tools.receive(
|
|
803
|
+
event(EventType.TOOL_CALL_START, {
|
|
804
|
+
toolCallId: 'c2',
|
|
805
|
+
toolCallName: '${w.id}.hello',
|
|
806
|
+
}),
|
|
807
|
+
);
|
|
808
|
+
await tools.receive(
|
|
809
|
+
event(EventType.TOOL_CALL_ARGS, { toolCallId: 'c2', delta: '{}' }),
|
|
810
|
+
);
|
|
811
|
+
const answer = await tools.receive(
|
|
812
|
+
event(EventType.TOOL_CALL_END, { toolCallId: 'c2' }),
|
|
813
|
+
);
|
|
814
|
+
|
|
815
|
+
expect(ran).toEqual([]);
|
|
816
|
+
expect(answer?.error).toContain('did not run');
|
|
817
|
+
});
|
|
818
|
+
});
|
|
819
|
+
`;
|
|
820
|
+
}
|
|
821
|
+
function agentSurfaceBlock(w) {
|
|
822
|
+
return [
|
|
823
|
+
" ctx.registerSurface({",
|
|
824
|
+
` id: '${w.id}.agent',`,
|
|
825
|
+
` title: '${w.id}.agent.title',`,
|
|
826
|
+
` icon: '${w.id}',`,
|
|
827
|
+
" docks: ['right-panel'],",
|
|
828
|
+
` component: ${w.className}AgentPanel,`,
|
|
829
|
+
" });"
|
|
830
|
+
].join("\n");
|
|
831
|
+
}
|
|
832
|
+
function agentFiles(w) {
|
|
833
|
+
const files = {
|
|
834
|
+
[`src/lib/agent/${w.id}-agent.ts`]: connectionFile(w),
|
|
835
|
+
[`src/lib/agent/${w.id}-agent-source.ts`]: standInFile(w),
|
|
836
|
+
[`src/lib/agent/${w.id}-agent-panel.ts`]: panelFile(w),
|
|
837
|
+
[`src/lib/agent/${w.id}-agent-panel.html`]: panelTemplateFile(w)
|
|
838
|
+
};
|
|
839
|
+
if (w.features.spec) {
|
|
840
|
+
files[`src/lib/agent/${w.id}-agent.spec.ts`] = specFile(w);
|
|
841
|
+
}
|
|
842
|
+
return files;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// ../devkit/src/recipes/angular-weaver/weaver-terms.ts
|
|
846
|
+
var CONTAINER_EXAMPLE_ID = "example";
|
|
847
|
+
function capabilityItems(capabilities) {
|
|
848
|
+
return capabilities.map((capability) => `'${capability}'`).join(", ");
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// ../devkit/src/recipes/angular-weaver/weaver-readme.ts
|
|
852
|
+
function surfaceNotes(w) {
|
|
853
|
+
const railNote = "Rail and bar items reference region ids (`primary`, `status`) that must exist in your layout.";
|
|
854
|
+
if (w.features.container) {
|
|
855
|
+
return [
|
|
856
|
+
`The surface is a **container**: it is routable at \`/${w.id}/:id\`, and its tab holds a`,
|
|
857
|
+
"nested pane tree of child surfaces. The host draws the inner tabs, splits and drag targets; this",
|
|
858
|
+
"weaver only declares which children it offers.",
|
|
859
|
+
"",
|
|
860
|
+
`- \`children\` is what the inner "new tab" picker lists \u2014 the host access-gates it for you.`,
|
|
861
|
+
"- `initial` is what a freshly opened container tab starts with.",
|
|
862
|
+
`- The children declare \`docks: []\`. That is the container-only convention: they are never seeded`,
|
|
863
|
+
" into a sidebar, they exist solely inside this container.",
|
|
864
|
+
`- Each child reads the container's \`:id\` from an injected \`ActivatedRoute\` \u2014 the host supplies a`,
|
|
865
|
+
" synthetic one, so a child needs no knowledge of where it is mounted. Two open container tabs are",
|
|
866
|
+
" two independent trees, each scoped to its own id.",
|
|
867
|
+
`- The inner tree is **sealed**: a child cannot be dragged out, and nothing can be dragged in. It`,
|
|
868
|
+
" travels with the tab, including into a sidebar or a pop-out window.",
|
|
869
|
+
"",
|
|
870
|
+
`The rail item opens the fixed id \`${CONTAINER_EXAMPLE_ID}\`. Replace that with whatever the user`,
|
|
871
|
+
"actually picked \u2014 a document, a run, a project.",
|
|
872
|
+
"",
|
|
873
|
+
railNote
|
|
874
|
+
];
|
|
875
|
+
}
|
|
876
|
+
if (w.features.instanceable) {
|
|
877
|
+
return [
|
|
878
|
+
`The surface is **docked** into the \`primary\` region and marked \`instanceable\`, so the host shows a`,
|
|
879
|
+
"switcher for saving, naming, renaming and deleting several configurations of it, each with its own",
|
|
880
|
+
"`VIEW_STATE` blob.",
|
|
881
|
+
"",
|
|
882
|
+
"It is deliberately **not** routable. Named instances exist only for a docked surface \u2014 a routable",
|
|
883
|
+
"one holds the URL pane instead, and the host drops `instanceable` on that path. The rail item",
|
|
884
|
+
"therefore reveals the surface (`ctx.revealSurface`) rather than navigating to a URL, which focuses",
|
|
885
|
+
"it wherever the user has since moved it.",
|
|
886
|
+
"",
|
|
887
|
+
"The generated view already uses that blob for its sort order, because a hidden surface is destroyed",
|
|
888
|
+
"as soon as it is clean: state kept in a component field survives neither a tab switch nor",
|
|
889
|
+
"a collapsed sidebar, and never survived a reload. The rule is *evictable = reload-safe* \u2014 anything",
|
|
890
|
+
"that must not be lost goes through `VIEW_STATE`, and `set()` replaces the whole blob, so spread it.",
|
|
891
|
+
"",
|
|
892
|
+
railNote
|
|
893
|
+
];
|
|
894
|
+
}
|
|
895
|
+
return [
|
|
896
|
+
`The surface is routable at \`/${w.id}\`; ${railNote.charAt(0).toLowerCase()}${railNote.slice(1)}`,
|
|
897
|
+
"",
|
|
898
|
+
"A routable surface has **no `VIEW_STATE` handle** \u2014 injecting the token there throws. It owns a URL,",
|
|
899
|
+
"so anything shareable (a filter, the active sub-tab) belongs in route params or `subRoutes`, where it",
|
|
900
|
+
"survives a deep link too; unsaved edits are `DirtySurface`, and an instance that is expensive to",
|
|
901
|
+
"rebuild declares `retain: 'always'`. Generate with `--instanceable` for the docked, `VIEW_STATE`",
|
|
902
|
+
"flavour instead."
|
|
903
|
+
];
|
|
904
|
+
}
|
|
905
|
+
function agentNotes(w) {
|
|
906
|
+
return [
|
|
907
|
+
"",
|
|
908
|
+
"## The agent connection",
|
|
909
|
+
"",
|
|
910
|
+
`\`src/lib/agent/\` holds three files and one of them is meant to be thrown away.`,
|
|
911
|
+
"",
|
|
912
|
+
`- \`${w.id}-agent.ts\` is the connection: the workbench's own commands offered as tools, and a`,
|
|
913
|
+
" seam where this weaver decides about a call before it runs. Nothing is registered twice \u2014 the",
|
|
914
|
+
" list comes from the workbench, already narrowed by everything that would refuse the call.",
|
|
915
|
+
`- \`${w.id}-agent-panel.ts\` shows what is offered, the call as it streams, and the outcome.`,
|
|
916
|
+
`- \`${w.id}-agent-source.ts\` is a **stand-in**, not an assistant: it produces the protocol's own`,
|
|
917
|
+
" events so the whole path runs before you have connected anything. Replace that one file with",
|
|
918
|
+
" your transport and nothing else changes. No transport, credential or model is generated for",
|
|
919
|
+
" you, because none of them can be guessed.",
|
|
920
|
+
"",
|
|
921
|
+
"Three things are easy to get wrong and invisible when they are, so the generated code does them",
|
|
922
|
+
"rather than explaining them: the offered list is asked for again every run, every event is handed",
|
|
923
|
+
"over unfiltered, and a decision before a call can only narrow what the workbench would have",
|
|
924
|
+
"allowed anyway.",
|
|
925
|
+
"",
|
|
926
|
+
`The generated weaver needs two packages your project may not carry yet:`,
|
|
927
|
+
"",
|
|
928
|
+
"```bash",
|
|
929
|
+
`npm i @loomweaver/ag-ui @ag-ui/core`,
|
|
930
|
+
"```",
|
|
931
|
+
"",
|
|
932
|
+
"The Nx generator and the CLI record them for you; the MCP route names them instead."
|
|
933
|
+
];
|
|
934
|
+
}
|
|
935
|
+
function readmeFile(w) {
|
|
936
|
+
return [
|
|
937
|
+
`# ${w.name} weaver`,
|
|
938
|
+
"",
|
|
939
|
+
`A LoomWeaver weaver (a domain plugin bundle). It consumes only the public \`@loomweaver/plugin-sdk\` contract.`,
|
|
940
|
+
"",
|
|
941
|
+
"## Wire it into a distribution",
|
|
942
|
+
"",
|
|
943
|
+
`1. Add the plugin to \`providePlugins\` in \`src/app/app.config.ts\`. It is **variadic** and`,
|
|
944
|
+
` returns an array, so spread it:`,
|
|
945
|
+
"",
|
|
946
|
+
" ```ts",
|
|
947
|
+
` import { ${w.propertyName}Plugin } from '${w.importPath}'; // Nx: the workspace alias; without one, a relative path to this library's src/index.ts`,
|
|
948
|
+
` ...providePlugins(${w.propertyName}Plugin),`,
|
|
949
|
+
" ```",
|
|
950
|
+
"",
|
|
951
|
+
`2. Grant its capabilities (default-deny) via \`provideCapabilityGrants\`:`,
|
|
952
|
+
"",
|
|
953
|
+
" ```ts",
|
|
954
|
+
` provideCapabilityGrants({ '${w.id}': [${capabilityItems(w.capabilities)}] });`,
|
|
955
|
+
" ```",
|
|
956
|
+
"",
|
|
957
|
+
`3. Compose its translations with \`provideTranslationNamespaces('${w.id}')\` \u2014 and serve the`,
|
|
958
|
+
` bundle by adding an assets glob to your application's build target, so the loader can fetch`,
|
|
959
|
+
` \`/i18n/${w.id}/<lang>.json\` (the Nx generator adds this glob for you):`,
|
|
960
|
+
"",
|
|
961
|
+
" ```json",
|
|
962
|
+
` { "glob": "**/*.json", "input": "<path to this library>/src/lib/i18n", "output": "i18n/${w.id}" }`,
|
|
963
|
+
" ```",
|
|
964
|
+
"",
|
|
965
|
+
`4. If your application compiles the shell's theme with Tailwind, name this library as a source`,
|
|
966
|
+
` for it, so the utility classes in these templates are emitted. Tailwind also detects sources`,
|
|
967
|
+
` by itself, but that depends on where it resolves the project root and on \`.gitignore\`, and`,
|
|
968
|
+
` what the scaffold names covers the application alone (the Nx generator adds this line for`,
|
|
969
|
+
` you). Applications scaffolded with \`--styles precompiled\` run no Tailwind and need nothing:`,
|
|
970
|
+
"",
|
|
971
|
+
" ```css",
|
|
972
|
+
` @source '<path from that stylesheet to this library>/src';`,
|
|
973
|
+
" ```",
|
|
974
|
+
"",
|
|
975
|
+
...surfaceNotes(w),
|
|
976
|
+
...w.features.agent ? agentNotes(w) : [],
|
|
977
|
+
"",
|
|
978
|
+
"## After scaffolding",
|
|
979
|
+
"",
|
|
980
|
+
"- `src/lib/i18n/de.json` starts as a copy of the English strings \u2014 translate it.",
|
|
981
|
+
`- A scaffolded command defaults its shortcut to \`mod+shift+<first letter of the id>\` \u2014 two weavers whose ids share a first letter collide; pass \`--shortcut\` or edit the command.`,
|
|
982
|
+
"- The project is generated **untagged**: Nx tags belong to your `depConstraints`, and inventing",
|
|
983
|
+
" one would fail a lint policy you never opted this project into. If your workspace enforces",
|
|
984
|
+
" module boundaries, give it tags your constraints allow \u2014 `--tags` at generation time, or",
|
|
985
|
+
" `tags` in `project.json` afterwards.",
|
|
986
|
+
""
|
|
987
|
+
].join("\n");
|
|
988
|
+
}
|
|
989
|
+
|
|
341
990
|
// ../devkit/src/recipes/angular-weaver/weaver-i18n.ts
|
|
342
991
|
function i18nBundle(w) {
|
|
343
992
|
const bundle = { title: w.name };
|
|
@@ -350,6 +999,17 @@ function i18nBundle(w) {
|
|
|
350
999
|
bundle["actionDescription"] = `Shows a short ${w.name} message.`;
|
|
351
1000
|
}
|
|
352
1001
|
if (w.features.about) bundle["about"] = `About ${w.name}`;
|
|
1002
|
+
if (w.features.agent) {
|
|
1003
|
+
bundle["agent"] = {
|
|
1004
|
+
title: `${w.name} assistant`,
|
|
1005
|
+
confirm: {
|
|
1006
|
+
title: "Run this command?",
|
|
1007
|
+
message: "An agent asked to run a command that was marked consequential.",
|
|
1008
|
+
yes: "Run it",
|
|
1009
|
+
no: "Not now"
|
|
1010
|
+
}
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
353
1013
|
if (w.features.settings)
|
|
354
1014
|
bundle["settings"] = { title: w.name, enabled: "Enabled", note: "Note" };
|
|
355
1015
|
return bundle;
|
|
@@ -384,18 +1044,16 @@ function resolveMenuSlot(menu) {
|
|
|
384
1044
|
}
|
|
385
1045
|
return typeof menu === "string" && menu.length ? menu : void 0;
|
|
386
1046
|
}
|
|
387
|
-
var PLATFORM_BOUND_CHORD_TOKENS = [
|
|
1047
|
+
var PLATFORM_BOUND_CHORD_TOKENS = /* @__PURE__ */ new Set([
|
|
388
1048
|
"cmd",
|
|
389
1049
|
"command",
|
|
390
1050
|
"ctrl",
|
|
391
1051
|
"control",
|
|
392
1052
|
"meta"
|
|
393
|
-
];
|
|
1053
|
+
]);
|
|
394
1054
|
function assertPlatformNeutralChord(shortcut) {
|
|
395
1055
|
const tokens = shortcut.toLowerCase().split("+").map((token) => token.trim());
|
|
396
|
-
const bound = tokens.find(
|
|
397
|
-
(token) => PLATFORM_BOUND_CHORD_TOKENS.includes(token)
|
|
398
|
-
);
|
|
1056
|
+
const bound = tokens.find((token) => PLATFORM_BOUND_CHORD_TOKENS.has(token));
|
|
399
1057
|
if (bound) {
|
|
400
1058
|
throw new Error(
|
|
401
1059
|
`Shortcut "${shortcut}" binds the platform-specific "${bound}" key. Use the neutral 'mod' token (e.g. 'mod+shift+k') \u2014 the host renders it as \u2318 on macOS and Ctrl elsewhere.`
|
|
@@ -416,8 +1074,9 @@ function resolveFeatures(id, input) {
|
|
|
416
1074
|
'A surface cannot be both a container and instanceable: a container tab holds its own ":id" and is therefore routable, while named instances exist only for a docked, non-routable surface. Pick one.'
|
|
417
1075
|
);
|
|
418
1076
|
}
|
|
1077
|
+
const agent = Boolean(input?.agent);
|
|
419
1078
|
return {
|
|
420
|
-
command: Boolean(input?.command) || menuSlot !== void 0 || barItem || hasShortcut,
|
|
1079
|
+
command: Boolean(input?.command) || menuSlot !== void 0 || barItem || hasShortcut || agent,
|
|
421
1080
|
menuSlot,
|
|
422
1081
|
settings: Boolean(input?.settings),
|
|
423
1082
|
access: input?.access ? accessLiteral(input.access) : void 0,
|
|
@@ -426,6 +1085,7 @@ function resolveFeatures(id, input) {
|
|
|
426
1085
|
about: Boolean(input?.about),
|
|
427
1086
|
instanceable,
|
|
428
1087
|
container,
|
|
1088
|
+
agent,
|
|
429
1089
|
spec: input?.spec !== false
|
|
430
1090
|
};
|
|
431
1091
|
}
|
|
@@ -436,6 +1096,10 @@ function deriveCapabilities(features) {
|
|
|
436
1096
|
set.add("ui");
|
|
437
1097
|
set.add("host");
|
|
438
1098
|
}
|
|
1099
|
+
if (features.agent) {
|
|
1100
|
+
set.add("ui");
|
|
1101
|
+
set.add("automation");
|
|
1102
|
+
}
|
|
439
1103
|
return KNOWN_CAPABILITIES.filter((capability) => set.has(capability));
|
|
440
1104
|
}
|
|
441
1105
|
function resolveWeaverInput(input) {
|
|
@@ -457,10 +1121,6 @@ function resolveWeaverInput(input) {
|
|
|
457
1121
|
importPath: input.importPath?.trim() || `@loomweaver/${input.id}-weaver`
|
|
458
1122
|
};
|
|
459
1123
|
}
|
|
460
|
-
function capabilityItems(capabilities) {
|
|
461
|
-
return capabilities.map((capability) => `'${capability}'`).join(", ");
|
|
462
|
-
}
|
|
463
|
-
var CONTAINER_EXAMPLE_ID = "example";
|
|
464
1124
|
function containerChildIds(w) {
|
|
465
1125
|
return [`${w.id}.canvas`, `${w.id}.details`];
|
|
466
1126
|
}
|
|
@@ -624,6 +1284,12 @@ function pluginFile(w) {
|
|
|
624
1284
|
`import { ${w.className}AboutDialog } from '../dialogs/${w.id}-about-dialog';`
|
|
625
1285
|
);
|
|
626
1286
|
}
|
|
1287
|
+
if (w.features.agent) {
|
|
1288
|
+
imports.push(
|
|
1289
|
+
`import { ${w.propertyName}Agent, ${w.propertyName}Connection } from '../agent/${w.id}-agent';`,
|
|
1290
|
+
`import { ${w.className}AgentPanel } from '../agent/${w.id}-agent-panel';`
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
627
1293
|
if (w.features.settings) {
|
|
628
1294
|
imports.unshift("import { signal } from '@angular/core';");
|
|
629
1295
|
}
|
|
@@ -636,6 +1302,11 @@ function pluginFile(w) {
|
|
|
636
1302
|
);
|
|
637
1303
|
}
|
|
638
1304
|
const body = [` ctx.contributeIcons({ '${w.id}': icon });`];
|
|
1305
|
+
if (w.features.agent) {
|
|
1306
|
+
body.push(
|
|
1307
|
+
` ${w.propertyName}Agent.set(${w.propertyName}Connection(ctx));`
|
|
1308
|
+
);
|
|
1309
|
+
}
|
|
639
1310
|
if (w.features.command) body.push(commandBlock(w));
|
|
640
1311
|
if (w.features.about) body.push(aboutCommandBlock(w));
|
|
641
1312
|
body.push(surfaceBlock(w), railBlock(w));
|
|
@@ -643,6 +1314,11 @@ function pluginFile(w) {
|
|
|
643
1314
|
if (w.features.barItem) body.push(barItemBlock(w));
|
|
644
1315
|
if (w.features.menuSlot) body.push(menuBlock(w));
|
|
645
1316
|
if (w.features.settings) body.push(settingsBlock(w));
|
|
1317
|
+
if (w.features.agent) body.push(agentSurfaceBlock(w));
|
|
1318
|
+
const deactivate = w.features.agent ? `
|
|
1319
|
+
deactivate() {
|
|
1320
|
+
${w.propertyName}Agent.set(null);
|
|
1321
|
+
},` : "";
|
|
646
1322
|
return `${imports.join("\n")}
|
|
647
1323
|
|
|
648
1324
|
${consts.join("\n")}
|
|
@@ -655,7 +1331,7 @@ export const ${w.propertyName}Plugin: Plugin = {
|
|
|
655
1331
|
},
|
|
656
1332
|
activate(ctx) {
|
|
657
1333
|
${body.join("\n")}
|
|
658
|
-
}
|
|
1334
|
+
},${deactivate}
|
|
659
1335
|
};
|
|
660
1336
|
`;
|
|
661
1337
|
}
|
|
@@ -771,7 +1447,7 @@ function childViewTemplateFile(w, heading) {
|
|
|
771
1447
|
</div>
|
|
772
1448
|
`;
|
|
773
1449
|
}
|
|
774
|
-
function
|
|
1450
|
+
function specFile2(w) {
|
|
775
1451
|
return `import { ${w.propertyName}Plugin } from './${w.id}.plugin';
|
|
776
1452
|
|
|
777
1453
|
describe('${w.propertyName}Plugin', () => {
|
|
@@ -782,112 +1458,6 @@ describe('${w.propertyName}Plugin', () => {
|
|
|
782
1458
|
});
|
|
783
1459
|
`;
|
|
784
1460
|
}
|
|
785
|
-
function surfaceNotes(w) {
|
|
786
|
-
const railNote = "Rail and bar items reference region ids (`primary`, `status`) that must exist in your layout.";
|
|
787
|
-
if (w.features.container) {
|
|
788
|
-
return [
|
|
789
|
-
`The surface is a **container**: it is routable at \`/${w.id}/:id\`, and its tab holds a`,
|
|
790
|
-
"nested pane tree of child surfaces. The host draws the inner tabs, splits and drag targets; this",
|
|
791
|
-
"weaver only declares which children it offers.",
|
|
792
|
-
"",
|
|
793
|
-
`- \`children\` is what the inner "new tab" picker lists \u2014 the host access-gates it for you.`,
|
|
794
|
-
"- `initial` is what a freshly opened container tab starts with.",
|
|
795
|
-
`- The children declare \`docks: []\`. That is the container-only convention: they are never seeded`,
|
|
796
|
-
" into a sidebar, they exist solely inside this container.",
|
|
797
|
-
`- Each child reads the container's \`:id\` from an injected \`ActivatedRoute\` \u2014 the host supplies a`,
|
|
798
|
-
" synthetic one, so a child needs no knowledge of where it is mounted. Two open container tabs are",
|
|
799
|
-
" two independent trees, each scoped to its own id.",
|
|
800
|
-
`- The inner tree is **sealed**: a child cannot be dragged out, and nothing can be dragged in. It`,
|
|
801
|
-
" travels with the tab, including into a sidebar or a pop-out window.",
|
|
802
|
-
"",
|
|
803
|
-
`The rail item opens the fixed id \`${CONTAINER_EXAMPLE_ID}\`. Replace that with whatever the user`,
|
|
804
|
-
"actually picked \u2014 a document, a run, a project.",
|
|
805
|
-
"",
|
|
806
|
-
railNote
|
|
807
|
-
];
|
|
808
|
-
}
|
|
809
|
-
if (w.features.instanceable) {
|
|
810
|
-
return [
|
|
811
|
-
`The surface is **docked** into the \`primary\` region and marked \`instanceable\`, so the host shows a`,
|
|
812
|
-
"switcher for saving, naming, renaming and deleting several configurations of it, each with its own",
|
|
813
|
-
"`VIEW_STATE` blob.",
|
|
814
|
-
"",
|
|
815
|
-
"It is deliberately **not** routable. Named instances exist only for a docked surface \u2014 a routable",
|
|
816
|
-
"one holds the URL pane instead, and the host drops `instanceable` on that path. The rail item",
|
|
817
|
-
"therefore reveals the surface (`ctx.revealSurface`) rather than navigating to a URL, which focuses",
|
|
818
|
-
"it wherever the user has since moved it.",
|
|
819
|
-
"",
|
|
820
|
-
"The generated view already uses that blob for its sort order, because a hidden surface is destroyed",
|
|
821
|
-
"as soon as it is clean: state kept in a component field survives neither a tab switch nor",
|
|
822
|
-
"a collapsed sidebar, and never survived a reload. The rule is *evictable = reload-safe* \u2014 anything",
|
|
823
|
-
"that must not be lost goes through `VIEW_STATE`, and `set()` replaces the whole blob, so spread it.",
|
|
824
|
-
"",
|
|
825
|
-
railNote
|
|
826
|
-
];
|
|
827
|
-
}
|
|
828
|
-
return [
|
|
829
|
-
`The surface is routable at \`/${w.id}\`; ${railNote.charAt(0).toLowerCase()}${railNote.slice(1)}`,
|
|
830
|
-
"",
|
|
831
|
-
"A routable surface has **no `VIEW_STATE` handle** \u2014 injecting the token there throws. It owns a URL,",
|
|
832
|
-
"so anything shareable (a filter, the active sub-tab) belongs in route params or `subRoutes`, where it",
|
|
833
|
-
"survives a deep link too; unsaved edits are `DirtySurface`, and an instance that is expensive to",
|
|
834
|
-
"rebuild declares `retain: 'always'`. Generate with `--instanceable` for the docked, `VIEW_STATE`",
|
|
835
|
-
"flavour instead."
|
|
836
|
-
];
|
|
837
|
-
}
|
|
838
|
-
function readmeFile(w) {
|
|
839
|
-
return [
|
|
840
|
-
`# ${w.name} weaver`,
|
|
841
|
-
"",
|
|
842
|
-
`A LoomWeaver weaver (a domain plugin bundle). It consumes only the public \`@loomweaver/plugin-sdk\` contract.`,
|
|
843
|
-
"",
|
|
844
|
-
"## Wire it into a distribution",
|
|
845
|
-
"",
|
|
846
|
-
`1. Add the plugin to \`providePlugins\` in \`src/app/app.config.ts\`. It is **variadic** and`,
|
|
847
|
-
` returns an array, so spread it:`,
|
|
848
|
-
"",
|
|
849
|
-
" ```ts",
|
|
850
|
-
` import { ${w.propertyName}Plugin } from '${w.importPath}'; // Nx: the workspace alias; without one, a relative path to this library's src/index.ts`,
|
|
851
|
-
` ...providePlugins(${w.propertyName}Plugin),`,
|
|
852
|
-
" ```",
|
|
853
|
-
"",
|
|
854
|
-
`2. Grant its capabilities (default-deny) via \`provideCapabilityGrants\`:`,
|
|
855
|
-
"",
|
|
856
|
-
" ```ts",
|
|
857
|
-
` provideCapabilityGrants({ '${w.id}': [${capabilityItems(w.capabilities)}] });`,
|
|
858
|
-
" ```",
|
|
859
|
-
"",
|
|
860
|
-
`3. Compose its translations with \`provideTranslationNamespaces('${w.id}')\` \u2014 and serve the`,
|
|
861
|
-
` bundle by adding an assets glob to your application's build target, so the loader can fetch`,
|
|
862
|
-
` \`/i18n/${w.id}/<lang>.json\` (the Nx generator adds this glob for you):`,
|
|
863
|
-
"",
|
|
864
|
-
" ```json",
|
|
865
|
-
` { "glob": "**/*.json", "input": "<path to this library>/src/lib/i18n", "output": "i18n/${w.id}" }`,
|
|
866
|
-
" ```",
|
|
867
|
-
"",
|
|
868
|
-
`4. If your application compiles the shell's theme with Tailwind, name this library as a source`,
|
|
869
|
-
` for it, so the utility classes in these templates are emitted. Tailwind also detects sources`,
|
|
870
|
-
` by itself, but that depends on where it resolves the project root and on \`.gitignore\`, and`,
|
|
871
|
-
` what the scaffold names covers the application alone (the Nx generator adds this line for`,
|
|
872
|
-
` you). Applications scaffolded with \`--styles precompiled\` run no Tailwind and need nothing:`,
|
|
873
|
-
"",
|
|
874
|
-
" ```css",
|
|
875
|
-
` @source '<path from that stylesheet to this library>/src';`,
|
|
876
|
-
" ```",
|
|
877
|
-
"",
|
|
878
|
-
...surfaceNotes(w),
|
|
879
|
-
"",
|
|
880
|
-
"## After scaffolding",
|
|
881
|
-
"",
|
|
882
|
-
"- `src/lib/i18n/de.json` starts as a copy of the English strings \u2014 translate it.",
|
|
883
|
-
`- A scaffolded command defaults its shortcut to \`mod+shift+<first letter of the id>\` \u2014 two weavers whose ids share a first letter collide; pass \`--shortcut\` or edit the command.`,
|
|
884
|
-
"- The project is generated **untagged**: Nx tags belong to your `depConstraints`, and inventing",
|
|
885
|
-
" one would fail a lint policy you never opted this project into. If your workspace enforces",
|
|
886
|
-
" module boundaries, give it tags your constraints allow \u2014 `--tags` at generation time, or",
|
|
887
|
-
" `tags` in `project.json` afterwards.",
|
|
888
|
-
""
|
|
889
|
-
].join("\n");
|
|
890
|
-
}
|
|
891
1461
|
var angularWeaver = {
|
|
892
1462
|
id: "angular-weaver",
|
|
893
1463
|
build(input) {
|
|
@@ -926,8 +1496,11 @@ var angularWeaver = {
|
|
|
926
1496
|
files[`src/lib/dialogs/${w.id}-about-dialog.ts`] = aboutDialogFile(w);
|
|
927
1497
|
files[`src/lib/dialogs/${w.id}-about-dialog.html`] = aboutDialogTemplateFile(w);
|
|
928
1498
|
}
|
|
1499
|
+
if (w.features.agent) {
|
|
1500
|
+
Object.assign(files, agentFiles(w));
|
|
1501
|
+
}
|
|
929
1502
|
if (w.features.spec) {
|
|
930
|
-
files[`src/lib/plugin/${w.id}.plugin.spec.ts`] =
|
|
1503
|
+
files[`src/lib/plugin/${w.id}.plugin.spec.ts`] = specFile2(w);
|
|
931
1504
|
}
|
|
932
1505
|
return files;
|
|
933
1506
|
}
|
|
@@ -1384,7 +1957,7 @@ import { provideProductIdentity } from '@loomweaver/plugin-sdk';
|
|
|
1384
1957
|
'status-bar' (bar) are what the scaffolded weaver targets. */
|
|
1385
1958
|
export const layout: ShellLayout = {
|
|
1386
1959
|
regions: [
|
|
1387
|
-
${renderRegions("
|
|
1960
|
+
${renderRegions(" ".repeat(4))}
|
|
1388
1961
|
],
|
|
1389
1962
|
};
|
|
1390
1963
|
|
|
@@ -1569,7 +2142,7 @@ var angularDistribution = {
|
|
|
1569
2142
|
return {
|
|
1570
2143
|
"src/main.ts": mainTs(),
|
|
1571
2144
|
"src/app/app.config.ts": appConfigTs(d),
|
|
1572
|
-
...d.withTests
|
|
2145
|
+
...d.withTests && { "src/app/app.config.spec.ts": appConfigSpec() },
|
|
1573
2146
|
"src/app/app.ts": appTs(),
|
|
1574
2147
|
"src/app/app.html": appHtml(),
|
|
1575
2148
|
"src/index.html": indexHtml(d),
|
|
@@ -1841,7 +2414,7 @@ import { ShellLayout } from '@loomweaver/shell';
|
|
|
1841
2414
|
|
|
1842
2415
|
export const ${l.propertyName}Layout: ShellLayout = {
|
|
1843
2416
|
regions: [
|
|
1844
|
-
${renderRegions("
|
|
2417
|
+
${renderRegions(" ".repeat(4))}
|
|
1845
2418
|
],
|
|
1846
2419
|
};
|
|
1847
2420
|
`;
|
|
@@ -1857,11 +2430,24 @@ var layout = {
|
|
|
1857
2430
|
// ../devkit/src/recipes/angular-weaver/amendments.ts
|
|
1858
2431
|
function weaverAmendments(input, where) {
|
|
1859
2432
|
const w = resolveWeaverInput(input);
|
|
1860
|
-
const
|
|
2433
|
+
const packages = w.features.agent ? [
|
|
2434
|
+
{
|
|
2435
|
+
kind: "package",
|
|
2436
|
+
name: "@loomweaver/ag-ui",
|
|
2437
|
+
version: `^${AG_UI_ADAPTER_VERSION}`
|
|
2438
|
+
},
|
|
2439
|
+
{
|
|
2440
|
+
kind: "package",
|
|
2441
|
+
name: "@ag-ui/core",
|
|
2442
|
+
version: AG_UI_PROTOCOL_VERSION
|
|
2443
|
+
}
|
|
2444
|
+
] : [];
|
|
2445
|
+
const directory = normalizeProjectRoot(where ?? "");
|
|
1861
2446
|
if (!directory) {
|
|
1862
|
-
return
|
|
2447
|
+
return packages;
|
|
1863
2448
|
}
|
|
1864
2449
|
return [
|
|
2450
|
+
...packages,
|
|
1865
2451
|
{
|
|
1866
2452
|
kind: "build-target",
|
|
1867
2453
|
styles: [],
|
|
@@ -1901,6 +2487,7 @@ function weaverInput(values) {
|
|
|
1901
2487
|
about: bool(values, "about"),
|
|
1902
2488
|
instanceable: bool(values, "instanceable"),
|
|
1903
2489
|
container: bool(values, "container"),
|
|
2490
|
+
agent: bool(values, "agent"),
|
|
1904
2491
|
access: str(values, "access"),
|
|
1905
2492
|
spec: bool(values, "spec")
|
|
1906
2493
|
}
|
|
@@ -1925,7 +2512,7 @@ function bool(values, name) {
|
|
|
1925
2512
|
return typeof value === "boolean" ? value : void 0;
|
|
1926
2513
|
}
|
|
1927
2514
|
function kebabCase(name) {
|
|
1928
|
-
return name.
|
|
2515
|
+
return name.replaceAll(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
|
|
1929
2516
|
}
|
|
1930
2517
|
var ID_PATTERN = "^[a-z][a-z0-9]*(-[a-z0-9]+)*$";
|
|
1931
2518
|
var PLACEMENT_OPTIONS = [
|
|
@@ -2027,6 +2614,12 @@ var SCAFFOLDS = [
|
|
|
2027
2614
|
description: "Make the surface a container: a routable tab at '<id>/:id' holding a nested pane tree of child surfaces, which the host draws. Not combinable with --instanceable.",
|
|
2028
2615
|
default: false
|
|
2029
2616
|
},
|
|
2617
|
+
{
|
|
2618
|
+
name: "agent",
|
|
2619
|
+
type: "boolean",
|
|
2620
|
+
description: "Also scaffold the connection that lets an AG-UI agent run the commands this workbench offers: a docked panel, the seam where a call is decided before it runs, and a local stand-in that speaks the protocol so the whole path works on the first serve. Implies --command.",
|
|
2621
|
+
default: false
|
|
2622
|
+
},
|
|
2030
2623
|
{
|
|
2031
2624
|
name: "access",
|
|
2032
2625
|
type: "string",
|
|
@@ -2338,6 +2931,14 @@ function validateEntry(raw, index, known) {
|
|
|
2338
2931
|
}
|
|
2339
2932
|
];
|
|
2340
2933
|
}
|
|
2934
|
+
return [
|
|
2935
|
+
...validateEntryIdentity(raw, index),
|
|
2936
|
+
...validateCapabilities2(raw["capabilities"], index, known),
|
|
2937
|
+
...validateEntryMetadata(raw, index),
|
|
2938
|
+
...validateEntryKeys(raw, index)
|
|
2939
|
+
];
|
|
2940
|
+
}
|
|
2941
|
+
function validateEntryIdentity(raw, index) {
|
|
2341
2942
|
const findings = [];
|
|
2342
2943
|
if (typeof raw["id"] !== "string" || raw["id"].length === 0) {
|
|
2343
2944
|
findings.push({
|
|
@@ -2379,7 +2980,10 @@ function validateEntry(raw, index, known) {
|
|
|
2379
2980
|
path: at(index, "version")
|
|
2380
2981
|
});
|
|
2381
2982
|
}
|
|
2382
|
-
findings
|
|
2983
|
+
return findings;
|
|
2984
|
+
}
|
|
2985
|
+
function validateEntryMetadata(raw, index) {
|
|
2986
|
+
const findings = [];
|
|
2383
2987
|
if (raw["downloads"] !== void 0 && (typeof raw["downloads"] !== "number" || raw["downloads"] < 0)) {
|
|
2384
2988
|
findings.push({
|
|
2385
2989
|
level: "warning",
|
|
@@ -2404,6 +3008,10 @@ function validateEntry(raw, index, known) {
|
|
|
2404
3008
|
path: at(index, "repository")
|
|
2405
3009
|
});
|
|
2406
3010
|
}
|
|
3011
|
+
return findings;
|
|
3012
|
+
}
|
|
3013
|
+
function validateEntryKeys(raw, index) {
|
|
3014
|
+
const findings = [];
|
|
2407
3015
|
for (const key of Object.keys(raw)) {
|
|
2408
3016
|
if (!CATALOG_ENTRY_KEYS.includes(key)) {
|
|
2409
3017
|
findings.push({
|
|
@@ -2443,7 +3051,7 @@ function validateCatalog(catalog, known = KNOWN_CAPABILITIES) {
|
|
|
2443
3051
|
}
|
|
2444
3052
|
const findings = [];
|
|
2445
3053
|
const seen = /* @__PURE__ */ new Set();
|
|
2446
|
-
catalog.
|
|
3054
|
+
for (const [index, entry] of catalog.entries()) {
|
|
2447
3055
|
findings.push(...validateEntry(entry, index, known));
|
|
2448
3056
|
const id = isPlainObject(entry) ? entry["id"] : void 0;
|
|
2449
3057
|
if (typeof id === "string" && id.length > 0) {
|
|
@@ -2457,7 +3065,7 @@ function validateCatalog(catalog, known = KNOWN_CAPABILITIES) {
|
|
|
2457
3065
|
}
|
|
2458
3066
|
seen.add(id);
|
|
2459
3067
|
}
|
|
2460
|
-
}
|
|
3068
|
+
}
|
|
2461
3069
|
return findings;
|
|
2462
3070
|
}
|
|
2463
3071
|
|
|
@@ -2489,8 +3097,8 @@ function assign(flags, token, next) {
|
|
|
2489
3097
|
function parseArgs(argv) {
|
|
2490
3098
|
const flags = {};
|
|
2491
3099
|
let command = "";
|
|
2492
|
-
for (let
|
|
2493
|
-
const token = argv[
|
|
3100
|
+
for (let index = 0; index < argv.length; index++) {
|
|
3101
|
+
const token = argv[index];
|
|
2494
3102
|
if (token === "-h" || token === "--help") {
|
|
2495
3103
|
flags["help"] = true;
|
|
2496
3104
|
continue;
|
|
@@ -2500,8 +3108,8 @@ function parseArgs(argv) {
|
|
|
2500
3108
|
continue;
|
|
2501
3109
|
}
|
|
2502
3110
|
if (token.startsWith("--")) {
|
|
2503
|
-
if (assign(flags, token, argv[
|
|
2504
|
-
|
|
3111
|
+
if (assign(flags, token, argv[index + 1])) {
|
|
3112
|
+
index++;
|
|
2505
3113
|
}
|
|
2506
3114
|
continue;
|
|
2507
3115
|
}
|
|
@@ -2581,7 +3189,7 @@ function resolveBuildProject(workspace, target) {
|
|
|
2581
3189
|
`No project with a build target found in ${workspace.configFile ?? workspace.root}.`
|
|
2582
3190
|
);
|
|
2583
3191
|
}
|
|
2584
|
-
const inside = projects.filter((project) => contains(project.root, relativeTo(workspace.root, target))).
|
|
3192
|
+
const inside = projects.filter((project) => contains(project.root, relativeTo(workspace.root, target))).toSorted((a, b) => b.root.length - a.root.length);
|
|
2585
3193
|
if (inside.length > 0) {
|
|
2586
3194
|
return inside[0];
|
|
2587
3195
|
}
|
|
@@ -2633,8 +3241,15 @@ function contains(projectRoot, target) {
|
|
|
2633
3241
|
function relativeTo(root, target) {
|
|
2634
3242
|
return relative(root, resolve(target)).split(sep).join("/");
|
|
2635
3243
|
}
|
|
3244
|
+
function withoutTrailingSlashes(path) {
|
|
3245
|
+
let end = path.length;
|
|
3246
|
+
while (end > 0 && path[end - 1] === "/") {
|
|
3247
|
+
end -= 1;
|
|
3248
|
+
}
|
|
3249
|
+
return path.slice(0, end);
|
|
3250
|
+
}
|
|
2636
3251
|
function normalise(value) {
|
|
2637
|
-
return typeof value === "string" ? value.replace(/^\.?\/*/, "")
|
|
3252
|
+
return typeof value === "string" ? withoutTrailingSlashes(value.replace(/^\.?\/*/, "")) : "";
|
|
2638
3253
|
}
|
|
2639
3254
|
function asObject2(value) {
|
|
2640
3255
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
@@ -2656,7 +3271,7 @@ function planAmend(amendments2, target) {
|
|
|
2656
3271
|
return {
|
|
2657
3272
|
amendments: [],
|
|
2658
3273
|
remaining: [
|
|
2659
|
-
`No workspace was found above the target directory, so nothing could be wired here. Add it by hand, or generate inside the workspace: ${amendments2.map(describeAmendment).join(" \xB7 ")}`
|
|
3274
|
+
`No workspace was found above the target directory, so nothing could be wired here. Add it by hand, or generate inside the workspace: ${amendments2.map((amendment) => describeAmendment(amendment)).join(" \xB7 ")}`
|
|
2660
3275
|
]
|
|
2661
3276
|
};
|
|
2662
3277
|
}
|
|
@@ -2671,15 +3286,22 @@ var Amender = class {
|
|
|
2671
3286
|
constructor(workspace, target) {
|
|
2672
3287
|
this.workspace = workspace;
|
|
2673
3288
|
this.target = target;
|
|
2674
|
-
this.planned = [];
|
|
2675
|
-
this.remaining = [];
|
|
2676
|
-
this.configAdded = [];
|
|
2677
3289
|
}
|
|
3290
|
+
workspace;
|
|
3291
|
+
target;
|
|
3292
|
+
planned = [];
|
|
3293
|
+
remaining = [];
|
|
3294
|
+
configAdded = [];
|
|
3295
|
+
manifestAdded = [];
|
|
3296
|
+
config;
|
|
3297
|
+
manifest;
|
|
3298
|
+
project;
|
|
2678
3299
|
plan(amendments2) {
|
|
2679
3300
|
for (const amendment of amendments2) {
|
|
2680
3301
|
this.planOne(amendment);
|
|
2681
3302
|
}
|
|
2682
3303
|
this.flushConfig();
|
|
3304
|
+
this.flushManifest();
|
|
2683
3305
|
return { amendments: this.planned, remaining: this.remaining };
|
|
2684
3306
|
}
|
|
2685
3307
|
planOne(amendment) {
|
|
@@ -2687,6 +3309,10 @@ var Amender = class {
|
|
|
2687
3309
|
this.planPostcss(amendment);
|
|
2688
3310
|
return;
|
|
2689
3311
|
}
|
|
3312
|
+
if (amendment.kind === "package") {
|
|
3313
|
+
this.planPackage(amendment);
|
|
3314
|
+
return;
|
|
3315
|
+
}
|
|
2690
3316
|
if (this.workspace.kind !== "angular") {
|
|
2691
3317
|
this.remaining.push(this.nonAngularNote(amendment));
|
|
2692
3318
|
return;
|
|
@@ -2730,6 +3356,39 @@ var Amender = class {
|
|
|
2730
3356
|
`
|
|
2731
3357
|
});
|
|
2732
3358
|
}
|
|
3359
|
+
planPackage(amendment) {
|
|
3360
|
+
const file = resolve2(this.workspace.root, "package.json");
|
|
3361
|
+
if (!existsSync2(file)) {
|
|
3362
|
+
this.remaining.push(describeAmendment(amendment));
|
|
3363
|
+
return;
|
|
3364
|
+
}
|
|
3365
|
+
const manifest2 = this.manifest ?? readJsonFile(file);
|
|
3366
|
+
const result = ensureDependency(manifest2, amendment);
|
|
3367
|
+
this.remaining.push(...result.declined);
|
|
3368
|
+
if (result.added.length === 0) {
|
|
3369
|
+
return;
|
|
3370
|
+
}
|
|
3371
|
+
this.manifest = result.value;
|
|
3372
|
+
this.manifestAdded.push(...result.added);
|
|
3373
|
+
}
|
|
3374
|
+
flushManifest() {
|
|
3375
|
+
if (this.manifestAdded.length === 0 || !this.manifest) {
|
|
3376
|
+
return;
|
|
3377
|
+
}
|
|
3378
|
+
const file = resolve2(this.workspace.root, "package.json");
|
|
3379
|
+
this.planned.push({
|
|
3380
|
+
file,
|
|
3381
|
+
display: this.displayName(file),
|
|
3382
|
+
added: this.manifestAdded,
|
|
3383
|
+
content: `${JSON.stringify(this.manifest, null, 2)}
|
|
3384
|
+
`
|
|
3385
|
+
});
|
|
3386
|
+
this.remaining.push(
|
|
3387
|
+
`Install what was just recorded in ${this.displayName(file)} (${this.manifestAdded.map((entry) => entry.replace("dependencies: ", "")).join(
|
|
3388
|
+
", "
|
|
3389
|
+
)}) \u2014 recording it is not installing it, and the build fails until you do.`
|
|
3390
|
+
);
|
|
3391
|
+
}
|
|
2733
3392
|
planBuildTarget(amendment, project) {
|
|
2734
3393
|
const target = this.buildTarget(project.name);
|
|
2735
3394
|
if (!target) {
|
|
@@ -2799,7 +3458,9 @@ var Amender = class {
|
|
|
2799
3458
|
this.planned.push({
|
|
2800
3459
|
file: root,
|
|
2801
3460
|
display: this.displayName(root),
|
|
2802
|
-
added: [
|
|
3461
|
+
added: [
|
|
3462
|
+
`${amendment.symbol}, its translations and its capability grants`
|
|
3463
|
+
],
|
|
2803
3464
|
content: result.source
|
|
2804
3465
|
});
|
|
2805
3466
|
}
|
|
@@ -2854,7 +3515,9 @@ var Amender = class {
|
|
|
2854
3515
|
return void 0;
|
|
2855
3516
|
}
|
|
2856
3517
|
entryStylesheet(project) {
|
|
2857
|
-
const styles = asObject3(
|
|
3518
|
+
const styles = asObject3(
|
|
3519
|
+
asObject3(this.buildTarget(project.name)?.value)?.["options"]
|
|
3520
|
+
)?.["styles"];
|
|
2858
3521
|
if (!Array.isArray(styles)) {
|
|
2859
3522
|
return void 0;
|
|
2860
3523
|
}
|
|
@@ -2972,7 +3635,7 @@ function planWrite(files, root) {
|
|
|
2972
3635
|
const absoluteRoot = resolve4(root);
|
|
2973
3636
|
const planned = [];
|
|
2974
3637
|
const conflicts = [];
|
|
2975
|
-
for (const path of Object.keys(files).
|
|
3638
|
+
for (const path of Object.keys(files).toSorted((a, b) => a.localeCompare(b))) {
|
|
2976
3639
|
const absolute = resolve4(absoluteRoot, path);
|
|
2977
3640
|
const inside = relative4(absoluteRoot, absolute);
|
|
2978
3641
|
if (inside.startsWith("..") || isAbsolute(inside)) {
|
|
@@ -3016,7 +3679,7 @@ function replaceSymlinkEntry(absolute) {
|
|
|
3016
3679
|
}
|
|
3017
3680
|
|
|
3018
3681
|
// src/lib/run.ts
|
|
3019
|
-
var VERSION = "0.7.
|
|
3682
|
+
var VERSION = "0.7.7";
|
|
3020
3683
|
function help() {
|
|
3021
3684
|
const commands = SCAFFOLDS.map((s) => ` ${s.name.padEnd(16)}${s.summary}`);
|
|
3022
3685
|
return [
|
|
@@ -3061,7 +3724,7 @@ function reportFindings(io, findings, strict) {
|
|
|
3061
3724
|
io.out("No findings.");
|
|
3062
3725
|
return 0;
|
|
3063
3726
|
}
|
|
3064
|
-
|
|
3727
|
+
for (const f of findings) io.err(`${f.level}: ${f.message}`);
|
|
3065
3728
|
if (findings.some((f) => f.level === "error")) {
|
|
3066
3729
|
return 1;
|
|
3067
3730
|
}
|
|
@@ -3134,12 +3797,16 @@ function reportAmendments(io, amend, done) {
|
|
|
3134
3797
|
);
|
|
3135
3798
|
for (const amendment of amend.amendments) {
|
|
3136
3799
|
io.out(` ${amendment.display}`);
|
|
3137
|
-
|
|
3800
|
+
for (const entry of amendment.added) {
|
|
3801
|
+
io.out(` + ${entry}`);
|
|
3802
|
+
}
|
|
3138
3803
|
}
|
|
3139
3804
|
}
|
|
3140
3805
|
if (amend.remaining.length > 0) {
|
|
3141
3806
|
io.out("Still to do by hand:");
|
|
3142
|
-
|
|
3807
|
+
for (const entry of amend.remaining) {
|
|
3808
|
+
io.out(` - ${entry}`);
|
|
3809
|
+
}
|
|
3143
3810
|
}
|
|
3144
3811
|
}
|
|
3145
3812
|
function scaffold(args, io) {
|
|
@@ -3157,12 +3824,16 @@ function scaffold(args, io) {
|
|
|
3157
3824
|
const amend = planAmend(amendmentsFor(descriptor, args), out);
|
|
3158
3825
|
if (boolFlag(args, "dry-run")) {
|
|
3159
3826
|
io.out(`Would write ${paths.length} file(s) into ${plan.root}:`);
|
|
3160
|
-
|
|
3827
|
+
for (const path of paths) {
|
|
3828
|
+
io.out(` ${path}`);
|
|
3829
|
+
}
|
|
3161
3830
|
if (plan.conflicts.length > 0) {
|
|
3162
3831
|
io.out(
|
|
3163
3832
|
`${plan.conflicts.length} of them already exist and would need --force:`
|
|
3164
3833
|
);
|
|
3165
|
-
plan.conflicts
|
|
3834
|
+
for (const path of plan.conflicts) {
|
|
3835
|
+
io.out(` ${path}`);
|
|
3836
|
+
}
|
|
3166
3837
|
}
|
|
3167
3838
|
reportAmendments(io, amend, false);
|
|
3168
3839
|
return 0;
|
|
@@ -3171,13 +3842,17 @@ function scaffold(args, io) {
|
|
|
3171
3842
|
io.err(
|
|
3172
3843
|
`${plan.conflicts.length} file(s) already exist; pass --force to overwrite:`
|
|
3173
3844
|
);
|
|
3174
|
-
plan.conflicts
|
|
3845
|
+
for (const path of plan.conflicts) {
|
|
3846
|
+
io.err(` ${path}`);
|
|
3847
|
+
}
|
|
3175
3848
|
return 1;
|
|
3176
3849
|
}
|
|
3177
3850
|
applyWrite(files, plan);
|
|
3178
3851
|
applyAmend(amend);
|
|
3179
3852
|
io.out(`Wrote ${paths.length} file(s) into ${plan.root}:`);
|
|
3180
|
-
|
|
3853
|
+
for (const path of paths) {
|
|
3854
|
+
io.out(` ${path}`);
|
|
3855
|
+
}
|
|
3181
3856
|
reportAmendments(io, amend, true);
|
|
3182
3857
|
return 0;
|
|
3183
3858
|
}
|