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