@loomweaver/cli 0.9.0-preview.1 → 0.9.0-preview.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.mjs +341 -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.9.0-preview.
|
|
707
|
+
var AG_UI_ADAPTER_VERSION = "0.9.0-preview.2";
|
|
690
708
|
var AG_UI_PROTOCOL_VERSION = "0.0.x";
|
|
691
709
|
function connectionFile(w) {
|
|
692
710
|
return `import { signal } from '@angular/core';
|
|
@@ -752,7 +770,7 @@ function contextThat(confirms: boolean, ran: Asked[]): PluginContext {
|
|
|
752
770
|
],
|
|
753
771
|
invokeCommand: (id: string, args?: CommandArguments) => {
|
|
754
772
|
ran.push({ id, args });
|
|
755
|
-
return Promise.resolve({ outcome: 'answered', value: '
|
|
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
|
|
|
@@ -996,7 +1014,9 @@ function i18nBundle(w) {
|
|
|
996
1014
|
}
|
|
997
1015
|
if (w.features.command) {
|
|
998
1016
|
bundle["action"] = `${w.name} action`;
|
|
999
|
-
bundle["actionDescription"] = `Shows a short ${w.name} message.`;
|
|
1017
|
+
bundle["actionDescription"] = `Shows a short ${w.name} message in the tone the caller chooses.`;
|
|
1018
|
+
bundle["actionTone"] = "How the message is shown: info, success or warning. Use info unless the caller asked for emphasis.";
|
|
1019
|
+
bundle["actionAnswers"] = "The tone the message was shown in.";
|
|
1000
1020
|
}
|
|
1001
1021
|
if (w.features.about) bundle["about"] = `About ${w.name}`;
|
|
1002
1022
|
if (w.features.agent) {
|
|
@@ -1018,6 +1038,38 @@ function i18nFile(w) {
|
|
|
1018
1038
|
return JSON.stringify(i18nBundle(w), null, 2) + "\n";
|
|
1019
1039
|
}
|
|
1020
1040
|
|
|
1041
|
+
// ../devkit/src/recipes/angular-weaver/weaver-command.ts
|
|
1042
|
+
function commandBlock(w) {
|
|
1043
|
+
return [
|
|
1044
|
+
" ctx.registerCommand({",
|
|
1045
|
+
` id: '${w.id}.hello',`,
|
|
1046
|
+
` title: '${w.id}.action',`,
|
|
1047
|
+
` description: '${w.id}.actionDescription',`,
|
|
1048
|
+
" arguments: [",
|
|
1049
|
+
` { name: 'tone', kind: 'choice', choices: ['info', 'success', 'warning'], description: '${w.id}.actionTone' },`,
|
|
1050
|
+
" ],",
|
|
1051
|
+
` answers: '${w.id}.actionAnswers',`,
|
|
1052
|
+
` shortcut: '${w.features.shortcut}',`,
|
|
1053
|
+
" callable: true,",
|
|
1054
|
+
" run: (_context, args) => {",
|
|
1055
|
+
" const tone = toneOf(args?.['tone']);",
|
|
1056
|
+
` ctx.ui.toast({ message: '${w.id}.action', kind: tone });`,
|
|
1057
|
+
" return { tone };",
|
|
1058
|
+
" },",
|
|
1059
|
+
" });"
|
|
1060
|
+
].join("\n");
|
|
1061
|
+
}
|
|
1062
|
+
function toneHelper() {
|
|
1063
|
+
return [
|
|
1064
|
+
"type Tone = 'info' | 'success' | 'warning';",
|
|
1065
|
+
"",
|
|
1066
|
+
"// The workbench already refused anything outside the declared choices; this narrows the type.",
|
|
1067
|
+
"function toneOf(value: unknown): Tone {",
|
|
1068
|
+
" return value === 'success' || value === 'warning' ? value : 'info';",
|
|
1069
|
+
"}"
|
|
1070
|
+
].join("\n");
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1021
1073
|
// ../devkit/src/recipes/angular-weaver/recipe.ts
|
|
1022
1074
|
var DEFAULT_MENU_SLOT = "content/tab/context";
|
|
1023
1075
|
var SURFACE_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><rect x="4" y="4" width="16" height="16" rx="3"/><path d="M8 9h8M8 13h8M8 17h5"/></svg>';
|
|
@@ -1196,18 +1248,6 @@ function railBlock(w) {
|
|
|
1196
1248
|
lines.push(" });");
|
|
1197
1249
|
return lines.join("\n");
|
|
1198
1250
|
}
|
|
1199
|
-
function commandBlock(w) {
|
|
1200
|
-
return [
|
|
1201
|
-
" ctx.registerCommand({",
|
|
1202
|
-
` id: '${w.id}.hello',`,
|
|
1203
|
-
` title: '${w.id}.action',`,
|
|
1204
|
-
` description: '${w.id}.actionDescription',`,
|
|
1205
|
-
` shortcut: '${w.features.shortcut}',`,
|
|
1206
|
-
" callable: true,",
|
|
1207
|
-
` run: () => ctx.ui.toast({ message: '${w.id}.action', kind: 'info' }),`,
|
|
1208
|
-
" });"
|
|
1209
|
-
].join("\n");
|
|
1210
|
-
}
|
|
1211
1251
|
function barItemBlock(w) {
|
|
1212
1252
|
return [
|
|
1213
1253
|
" ctx.registerBarItem({",
|
|
@@ -1319,9 +1359,10 @@ function pluginFile(w) {
|
|
|
1319
1359
|
deactivate() {
|
|
1320
1360
|
${w.propertyName}Agent.set(null);
|
|
1321
1361
|
},` : "";
|
|
1362
|
+
if (w.features.command) consts.push(toneHelper());
|
|
1322
1363
|
return `${imports.join("\n")}
|
|
1323
1364
|
|
|
1324
|
-
${consts.join("\n")}
|
|
1365
|
+
${consts.join("\n\n")}
|
|
1325
1366
|
|
|
1326
1367
|
export const ${w.propertyName}Plugin: Plugin = {
|
|
1327
1368
|
manifest: {
|
|
@@ -1760,9 +1801,9 @@ function readme2(d) {
|
|
|
1760
1801
|
"project's `project.json`, keeping the targets, `implicitDependencies` and tags it already had.",
|
|
1761
1802
|
"",
|
|
1762
1803
|
"Two leftovers from `ng new` are no longer referenced and can go: `src/app/app.routes.ts` (the",
|
|
1763
|
-
"shell owns content routing via `provideShellRouter()`) and `src/app/app.css`. The
|
|
1764
|
-
"`src/app/app.spec.ts`
|
|
1765
|
-
"
|
|
1804
|
+
"shell owns content routing via `provideShellRouter()`) and `src/app/app.css`. The starter test",
|
|
1805
|
+
"`src/app/app.spec.ts` was replaced by one that boots the shell with this composition root, so",
|
|
1806
|
+
"`ng test` is green as generated; test your own components beside it.",
|
|
1766
1807
|
"",
|
|
1767
1808
|
"The project is generated **untagged**: Nx tags belong to your `depConstraints`, and inventing",
|
|
1768
1809
|
"one would fail a lint policy you never opted this project into. If your workspace enforces",
|
|
@@ -2016,6 +2057,31 @@ describe('layout', () => {
|
|
|
2016
2057
|
});
|
|
2017
2058
|
`;
|
|
2018
2059
|
}
|
|
2060
|
+
function appSpec() {
|
|
2061
|
+
return `import { TestBed } from '@angular/core/testing';
|
|
2062
|
+
import { App } from './app';
|
|
2063
|
+
import { appConfig } from './app.config';
|
|
2064
|
+
|
|
2065
|
+
/* The application's starter test, rewritten for a root component that renders the shell: it boots
|
|
2066
|
+
with the composition root's own providers, so whatever you compose in is what the test boots
|
|
2067
|
+
with, and it asserts only that the shell mounted. */
|
|
2068
|
+
describe('App', () => {
|
|
2069
|
+
beforeEach(async () => {
|
|
2070
|
+
await TestBed.configureTestingModule({
|
|
2071
|
+
imports: [App],
|
|
2072
|
+
providers: appConfig.providers,
|
|
2073
|
+
}).compileComponents();
|
|
2074
|
+
});
|
|
2075
|
+
|
|
2076
|
+
it('boots the shell', async () => {
|
|
2077
|
+
const fixture = TestBed.createComponent(App);
|
|
2078
|
+
await fixture.whenStable();
|
|
2079
|
+
const compiled = fixture.nativeElement as HTMLElement;
|
|
2080
|
+
expect(compiled.querySelector('lw-shell')).toBeTruthy();
|
|
2081
|
+
});
|
|
2082
|
+
});
|
|
2083
|
+
`;
|
|
2084
|
+
}
|
|
2019
2085
|
function indexHtml(d) {
|
|
2020
2086
|
return `<!DOCTYPE html>
|
|
2021
2087
|
<html lang="en">
|
|
@@ -2142,7 +2208,10 @@ var angularDistribution = {
|
|
|
2142
2208
|
return {
|
|
2143
2209
|
"src/main.ts": mainTs(),
|
|
2144
2210
|
"src/app/app.config.ts": appConfigTs(d),
|
|
2145
|
-
...d.withTests && {
|
|
2211
|
+
...d.withTests && {
|
|
2212
|
+
"src/app/app.config.spec.ts": appConfigSpec(),
|
|
2213
|
+
"src/app/app.spec.ts": appSpec()
|
|
2214
|
+
},
|
|
2146
2215
|
"src/app/app.ts": appTs(),
|
|
2147
2216
|
"src/app/app.html": appHtml(),
|
|
2148
2217
|
"src/index.html": indexHtml(d),
|
|
@@ -3071,6 +3140,184 @@ function validateCatalog(catalog, known = KNOWN_CAPABILITIES) {
|
|
|
3071
3140
|
return findings;
|
|
3072
3141
|
}
|
|
3073
3142
|
|
|
3143
|
+
// ../devkit/src/lib/validate/commands.ts
|
|
3144
|
+
var RUNTIME_NOTE = "Grants, access and the window decide what is finally offered at runtime; this check judged the registrations alone.";
|
|
3145
|
+
function validateCommands(sources, ts) {
|
|
3146
|
+
const findings = [];
|
|
3147
|
+
for (const source of sources) {
|
|
3148
|
+
const file = ts.createSourceFile(source.path, source.text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
3149
|
+
for (const call of registerCommandCalls(ts, file)) {
|
|
3150
|
+
findings.push(...judge(read(ts, file, call)));
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3153
|
+
findings.push({ level: "info", code: "commands.runtime", message: RUNTIME_NOTE });
|
|
3154
|
+
return findings;
|
|
3155
|
+
}
|
|
3156
|
+
function registerCommandCalls(ts, file) {
|
|
3157
|
+
const calls = [];
|
|
3158
|
+
const visit = (node) => {
|
|
3159
|
+
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "registerCommand") {
|
|
3160
|
+
calls.push(node);
|
|
3161
|
+
}
|
|
3162
|
+
ts.forEachChild(node, visit);
|
|
3163
|
+
};
|
|
3164
|
+
visit(file);
|
|
3165
|
+
return calls;
|
|
3166
|
+
}
|
|
3167
|
+
function read(ts, file, call) {
|
|
3168
|
+
const line = file.getLineAndCharacterOfPosition(call.getStart(file)).line + 1;
|
|
3169
|
+
const at2 = `${file.fileName}:${line}`;
|
|
3170
|
+
const literal = call.arguments[0];
|
|
3171
|
+
const empty = { at: at2, callable: false, described: false, answers: false, returnsValue: false, arguments: [] };
|
|
3172
|
+
if (!literal || !ts.isObjectLiteralExpression(literal)) {
|
|
3173
|
+
return { ...empty, unreadable: "its argument is not an object literal" };
|
|
3174
|
+
}
|
|
3175
|
+
const properties = /* @__PURE__ */ new Map();
|
|
3176
|
+
for (const property of literal.properties) {
|
|
3177
|
+
if (ts.isSpreadAssignment(property)) {
|
|
3178
|
+
return { ...empty, unreadable: "it spreads another value into the registration" };
|
|
3179
|
+
}
|
|
3180
|
+
if (ts.isPropertyAssignment(property) && (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name))) {
|
|
3181
|
+
properties.set(property.name.text, property.initializer);
|
|
3182
|
+
} else if (ts.isMethodDeclaration(property) && ts.isIdentifier(property.name)) {
|
|
3183
|
+
properties.set(property.name.text, property);
|
|
3184
|
+
}
|
|
3185
|
+
}
|
|
3186
|
+
const id = properties.get("id");
|
|
3187
|
+
if (!id || !ts.isStringLiteral(id)) {
|
|
3188
|
+
return { ...empty, unreadable: "its id is not a string literal" };
|
|
3189
|
+
}
|
|
3190
|
+
const callable = properties.get("callable");
|
|
3191
|
+
return {
|
|
3192
|
+
at: at2,
|
|
3193
|
+
id: id.text,
|
|
3194
|
+
callable: callable !== void 0 && callable.kind === ts.SyntaxKind.TrueKeyword,
|
|
3195
|
+
described: properties.has("description"),
|
|
3196
|
+
answers: properties.has("answers"),
|
|
3197
|
+
returnsValue: returnsValue(ts, properties.get("run")),
|
|
3198
|
+
arguments: readArguments(ts, properties.get("arguments"))
|
|
3199
|
+
};
|
|
3200
|
+
}
|
|
3201
|
+
function readArguments(ts, value) {
|
|
3202
|
+
if (!value || !ts.isArrayLiteralExpression(value)) {
|
|
3203
|
+
return [];
|
|
3204
|
+
}
|
|
3205
|
+
return value.elements.map((element, index) => {
|
|
3206
|
+
if (!ts.isObjectLiteralExpression(element)) {
|
|
3207
|
+
return { name: `#${index + 1}`, described: false };
|
|
3208
|
+
}
|
|
3209
|
+
let name = `#${index + 1}`;
|
|
3210
|
+
let described = false;
|
|
3211
|
+
for (const property of element.properties) {
|
|
3212
|
+
if (!ts.isPropertyAssignment(property) || !ts.isIdentifier(property.name)) {
|
|
3213
|
+
continue;
|
|
3214
|
+
}
|
|
3215
|
+
if (property.name.text === "name" && ts.isStringLiteral(property.initializer)) {
|
|
3216
|
+
name = property.initializer.text;
|
|
3217
|
+
}
|
|
3218
|
+
if (property.name.text === "description") {
|
|
3219
|
+
described = true;
|
|
3220
|
+
}
|
|
3221
|
+
}
|
|
3222
|
+
return { name, described };
|
|
3223
|
+
});
|
|
3224
|
+
}
|
|
3225
|
+
function returnsValue(ts, run2) {
|
|
3226
|
+
if (!run2) {
|
|
3227
|
+
return false;
|
|
3228
|
+
}
|
|
3229
|
+
if (ts.isArrowFunction(run2) && !ts.isBlock(run2.body)) {
|
|
3230
|
+
return ts.isObjectLiteralExpression(run2.body) || ts.isArrayLiteralExpression(run2.body);
|
|
3231
|
+
}
|
|
3232
|
+
let found = false;
|
|
3233
|
+
const visit = (node) => {
|
|
3234
|
+
if (found || ts.isFunctionLike(node) && node !== run2) {
|
|
3235
|
+
return;
|
|
3236
|
+
}
|
|
3237
|
+
if (ts.isReturnStatement(node) && node.expression && node.expression.kind !== ts.SyntaxKind.UndefinedKeyword) {
|
|
3238
|
+
found = true;
|
|
3239
|
+
return;
|
|
3240
|
+
}
|
|
3241
|
+
ts.forEachChild(node, visit);
|
|
3242
|
+
};
|
|
3243
|
+
visit(run2);
|
|
3244
|
+
return found;
|
|
3245
|
+
}
|
|
3246
|
+
function judge(registration) {
|
|
3247
|
+
const { at: at2, id } = registration;
|
|
3248
|
+
if (registration.unreadable) {
|
|
3249
|
+
return [
|
|
3250
|
+
{
|
|
3251
|
+
level: "info",
|
|
3252
|
+
code: "command.unreadable",
|
|
3253
|
+
path: at2,
|
|
3254
|
+
message: `A registerCommand call at ${at2} could not be read: ${registration.unreadable}. What it offers an agent is unknown here.`
|
|
3255
|
+
}
|
|
3256
|
+
];
|
|
3257
|
+
}
|
|
3258
|
+
if (!registration.callable) {
|
|
3259
|
+
return [
|
|
3260
|
+
{
|
|
3261
|
+
level: "info",
|
|
3262
|
+
code: "command.private",
|
|
3263
|
+
path: at2,
|
|
3264
|
+
message: `${id}: not offered to an agent, because it is not callable; an agent never sees it.`
|
|
3265
|
+
}
|
|
3266
|
+
];
|
|
3267
|
+
}
|
|
3268
|
+
const findings = [];
|
|
3269
|
+
if (!registration.described) {
|
|
3270
|
+
findings.push({
|
|
3271
|
+
level: "warning",
|
|
3272
|
+
code: "command.description",
|
|
3273
|
+
path: at2,
|
|
3274
|
+
message: `${id}: offered to an agent without a description; the agent sees only the id and has to guess what it does.`
|
|
3275
|
+
});
|
|
3276
|
+
}
|
|
3277
|
+
for (const argument of registration.arguments) {
|
|
3278
|
+
if (!argument.described) {
|
|
3279
|
+
findings.push({
|
|
3280
|
+
level: "info",
|
|
3281
|
+
code: "command.argument",
|
|
3282
|
+
path: at2,
|
|
3283
|
+
message: `${id}: argument "${argument.name}" has no description; an agent sees a bare name and guesses its value.`
|
|
3284
|
+
});
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
if (registration.returnsValue && !registration.answers) {
|
|
3288
|
+
findings.push({
|
|
3289
|
+
level: "info",
|
|
3290
|
+
code: "command.answers",
|
|
3291
|
+
path: at2,
|
|
3292
|
+
message: `${id}: run returns a value but declares no answers; an agent hears that the command ran, not what it answered.`
|
|
3293
|
+
});
|
|
3294
|
+
}
|
|
3295
|
+
if (findings.length === 0) {
|
|
3296
|
+
const count = registration.arguments.length;
|
|
3297
|
+
findings.push({
|
|
3298
|
+
level: "info",
|
|
3299
|
+
code: "command.offered",
|
|
3300
|
+
path: at2,
|
|
3301
|
+
message: `${id}: offered to an agent, with ${count} described argument${count === 1 ? "" : "s"}${registration.answers ? " and a declared answer" : ""}.`
|
|
3302
|
+
});
|
|
3303
|
+
}
|
|
3304
|
+
return findings;
|
|
3305
|
+
}
|
|
3306
|
+
|
|
3307
|
+
// ../devkit/src/lib/validate/typescript.ts
|
|
3308
|
+
import { createRequire } from "node:module";
|
|
3309
|
+
import { resolve } from "node:path";
|
|
3310
|
+
function loadTypeScript(fromDir) {
|
|
3311
|
+
for (const from of [resolve(fromDir, "package.json"), resolve(process.cwd(), "package.json")]) {
|
|
3312
|
+
try {
|
|
3313
|
+
return createRequire(from)("typescript");
|
|
3314
|
+
} catch {
|
|
3315
|
+
continue;
|
|
3316
|
+
}
|
|
3317
|
+
}
|
|
3318
|
+
return null;
|
|
3319
|
+
}
|
|
3320
|
+
|
|
3074
3321
|
// src/lib/run.ts
|
|
3075
3322
|
import { readdirSync, readFileSync as readFileSync3 } from "node:fs";
|
|
3076
3323
|
import { join } from "node:path";
|
|
@@ -3164,17 +3411,17 @@ function boolFlag(args, name) {
|
|
|
3164
3411
|
|
|
3165
3412
|
// src/lib/amend.ts
|
|
3166
3413
|
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "node:fs";
|
|
3167
|
-
import { dirname as dirname2, posix, relative as relative2, resolve as
|
|
3414
|
+
import { dirname as dirname2, posix, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
|
|
3168
3415
|
|
|
3169
3416
|
// src/lib/workspace.ts
|
|
3170
3417
|
import { existsSync, readFileSync } from "node:fs";
|
|
3171
|
-
import { dirname, relative, resolve, sep } from "node:path";
|
|
3418
|
+
import { dirname, relative, resolve as resolve2, sep } from "node:path";
|
|
3172
3419
|
var WorkspaceError = class extends Error {
|
|
3173
3420
|
};
|
|
3174
3421
|
function findWorkspace(from) {
|
|
3175
|
-
let dir =
|
|
3422
|
+
let dir = resolve2(from);
|
|
3176
3423
|
for (; ; ) {
|
|
3177
|
-
if (existsSync(
|
|
3424
|
+
if (existsSync(resolve2(dir, "package.json"))) {
|
|
3178
3425
|
return { root: dir, ...buildConfigIn(dir) };
|
|
3179
3426
|
}
|
|
3180
3427
|
const parent = dirname(dir);
|
|
@@ -3212,11 +3459,11 @@ function readJsonFile(file) {
|
|
|
3212
3459
|
}
|
|
3213
3460
|
}
|
|
3214
3461
|
function buildConfigIn(dir) {
|
|
3215
|
-
const angular =
|
|
3462
|
+
const angular = resolve2(dir, "angular.json");
|
|
3216
3463
|
if (existsSync(angular)) {
|
|
3217
3464
|
return { configFile: angular, kind: "angular" };
|
|
3218
3465
|
}
|
|
3219
|
-
const nx =
|
|
3466
|
+
const nx = resolve2(dir, "nx.json");
|
|
3220
3467
|
if (existsSync(nx)) {
|
|
3221
3468
|
return { configFile: nx, kind: "nx" };
|
|
3222
3469
|
}
|
|
@@ -3241,7 +3488,7 @@ function contains(projectRoot, target) {
|
|
|
3241
3488
|
return projectRoot === "" || target === projectRoot || target.startsWith(`${projectRoot}/`);
|
|
3242
3489
|
}
|
|
3243
3490
|
function relativeTo(root, target) {
|
|
3244
|
-
return relative(root,
|
|
3491
|
+
return relative(root, resolve2(target)).split(sep).join("/");
|
|
3245
3492
|
}
|
|
3246
3493
|
function withoutTrailingSlashes(path) {
|
|
3247
3494
|
let end = path.length;
|
|
@@ -3333,7 +3580,7 @@ var Amender = class {
|
|
|
3333
3580
|
}
|
|
3334
3581
|
planPostcss(amendment) {
|
|
3335
3582
|
const inTheWay = JS_POSTCSS_CONFIGS.find(
|
|
3336
|
-
(name) => existsSync2(
|
|
3583
|
+
(name) => existsSync2(resolve3(this.workspace.root, name))
|
|
3337
3584
|
);
|
|
3338
3585
|
if (inTheWay) {
|
|
3339
3586
|
this.remaining.push(
|
|
@@ -3341,7 +3588,7 @@ var Amender = class {
|
|
|
3341
3588
|
);
|
|
3342
3589
|
return;
|
|
3343
3590
|
}
|
|
3344
|
-
const file =
|
|
3591
|
+
const file = resolve3(this.workspace.root, amendment.file);
|
|
3345
3592
|
const result = ensurePostcssPlugin(
|
|
3346
3593
|
existsSync2(file) ? readJsonFile(file) : void 0,
|
|
3347
3594
|
amendment
|
|
@@ -3359,7 +3606,7 @@ var Amender = class {
|
|
|
3359
3606
|
});
|
|
3360
3607
|
}
|
|
3361
3608
|
planPackage(amendment) {
|
|
3362
|
-
const file =
|
|
3609
|
+
const file = resolve3(this.workspace.root, "package.json");
|
|
3363
3610
|
if (!existsSync2(file)) {
|
|
3364
3611
|
this.remaining.push(describeAmendment(amendment));
|
|
3365
3612
|
return;
|
|
@@ -3377,7 +3624,7 @@ var Amender = class {
|
|
|
3377
3624
|
if (this.manifestAdded.length === 0 || !this.manifest) {
|
|
3378
3625
|
return;
|
|
3379
3626
|
}
|
|
3380
|
-
const file =
|
|
3627
|
+
const file = resolve3(this.workspace.root, "package.json");
|
|
3381
3628
|
this.planned.push({
|
|
3382
3629
|
file,
|
|
3383
3630
|
display: this.displayName(file),
|
|
@@ -3435,7 +3682,7 @@ var Amender = class {
|
|
|
3435
3682
|
});
|
|
3436
3683
|
}
|
|
3437
3684
|
planComposePlugin(amendment, project) {
|
|
3438
|
-
const root =
|
|
3685
|
+
const root = resolve3(
|
|
3439
3686
|
this.workspace.root,
|
|
3440
3687
|
project.root,
|
|
3441
3688
|
"src/app/app.config.ts"
|
|
@@ -3526,7 +3773,7 @@ var Amender = class {
|
|
|
3526
3773
|
const entry = styles.find(
|
|
3527
3774
|
(style) => typeof style === "string" && style.endsWith(".css")
|
|
3528
3775
|
);
|
|
3529
|
-
return entry === void 0 ? void 0 :
|
|
3776
|
+
return entry === void 0 ? void 0 : resolve3(this.workspace.root, entry);
|
|
3530
3777
|
}
|
|
3531
3778
|
nonAngularNote(amendment) {
|
|
3532
3779
|
const where = this.workspace.kind === "nx" ? "the project's own project.json" : "your build configuration";
|
|
@@ -3546,7 +3793,7 @@ function relativeImport(fromDir, sourceRoot) {
|
|
|
3546
3793
|
}
|
|
3547
3794
|
|
|
3548
3795
|
// src/lib/scaffold.ts
|
|
3549
|
-
import { relative as relative3, resolve as
|
|
3796
|
+
import { relative as relative3, resolve as resolve4 } from "node:path";
|
|
3550
3797
|
function findScaffold2(name) {
|
|
3551
3798
|
const scaffold2 = findScaffold(name);
|
|
3552
3799
|
if (!scaffold2) {
|
|
@@ -3598,7 +3845,7 @@ function valuesFor(scaffold2, args) {
|
|
|
3598
3845
|
return values;
|
|
3599
3846
|
}
|
|
3600
3847
|
function directoryFromOut(out) {
|
|
3601
|
-
const below = relative3(process.cwd(),
|
|
3848
|
+
const below = relative3(process.cwd(), resolve4(out ?? "."));
|
|
3602
3849
|
return below.startsWith("..") ? "" : below;
|
|
3603
3850
|
}
|
|
3604
3851
|
function scaffoldValues(scaffold2, args) {
|
|
@@ -3630,15 +3877,15 @@ import {
|
|
|
3630
3877
|
rmSync,
|
|
3631
3878
|
writeFileSync as writeFileSync2
|
|
3632
3879
|
} from "node:fs";
|
|
3633
|
-
import { dirname as dirname3, isAbsolute, relative as relative4, resolve as
|
|
3880
|
+
import { dirname as dirname3, isAbsolute, relative as relative4, resolve as resolve5 } from "node:path";
|
|
3634
3881
|
var WriteError = class extends Error {
|
|
3635
3882
|
};
|
|
3636
3883
|
function planWrite(files, root) {
|
|
3637
|
-
const absoluteRoot =
|
|
3884
|
+
const absoluteRoot = resolve5(root);
|
|
3638
3885
|
const planned = [];
|
|
3639
3886
|
const conflicts = [];
|
|
3640
3887
|
for (const path of Object.keys(files).toSorted((a, b) => a.localeCompare(b))) {
|
|
3641
|
-
const absolute =
|
|
3888
|
+
const absolute = resolve5(absoluteRoot, path);
|
|
3642
3889
|
const inside = relative4(absoluteRoot, absolute);
|
|
3643
3890
|
if (inside.startsWith("..") || isAbsolute(inside)) {
|
|
3644
3891
|
throw new WriteError(`Refusing to write outside the target directory: ${path}`);
|
|
@@ -3681,7 +3928,7 @@ function replaceSymlinkEntry(absolute) {
|
|
|
3681
3928
|
}
|
|
3682
3929
|
|
|
3683
3930
|
// src/lib/run.ts
|
|
3684
|
-
var VERSION = "0.9.0-preview.
|
|
3931
|
+
var VERSION = "0.9.0-preview.2";
|
|
3685
3932
|
function help() {
|
|
3686
3933
|
const commands = SCAFFOLDS.map((s) => ` ${s.name.padEnd(16)}${s.summary}`);
|
|
3687
3934
|
return [
|
|
@@ -3697,6 +3944,7 @@ function help() {
|
|
|
3697
3944
|
" validate-manifest --id <id> [--name <name>] [--capabilities <a,b>]",
|
|
3698
3945
|
" validate-i18n --dir <dir> check <lang>.json bundles for key parity",
|
|
3699
3946
|
" validate-catalog --file <path> check a plugin store catalog the host parses defensively",
|
|
3947
|
+
" validate-commands --dir <dir> say, per command, whether an agent is offered it and what it would guess at",
|
|
3700
3948
|
"",
|
|
3701
3949
|
"Options:",
|
|
3702
3950
|
" --out <dir> where to write (default: the current directory)",
|
|
@@ -3726,11 +3974,14 @@ function reportFindings(io, findings, strict) {
|
|
|
3726
3974
|
io.out("No findings.");
|
|
3727
3975
|
return 0;
|
|
3728
3976
|
}
|
|
3729
|
-
for (const f of findings)
|
|
3730
|
-
|
|
3977
|
+
for (const f of findings) {
|
|
3978
|
+
(f.level === "info" ? io.out : io.err)(`${f.level}: ${f.message}`);
|
|
3979
|
+
}
|
|
3980
|
+
const gating = findings.filter((f) => f.level !== "info");
|
|
3981
|
+
if (gating.some((f) => f.level === "error")) {
|
|
3731
3982
|
return 1;
|
|
3732
3983
|
}
|
|
3733
|
-
return strict ? 1 : 0;
|
|
3984
|
+
return strict && gating.length > 0 ? 1 : 0;
|
|
3734
3985
|
}
|
|
3735
3986
|
function validateManifestCommand(args, io) {
|
|
3736
3987
|
rejectUnknownFlags(args, ["id", "name", "capabilities", "strict"]);
|
|
@@ -3771,6 +4022,42 @@ function validateI18nCommand(args, io) {
|
|
|
3771
4022
|
boolFlag(args, "strict") === true
|
|
3772
4023
|
);
|
|
3773
4024
|
}
|
|
4025
|
+
function readSources(dir) {
|
|
4026
|
+
const sources = [];
|
|
4027
|
+
const walk = (folder) => {
|
|
4028
|
+
for (const entry of readdirSync(folder, { withFileTypes: true })) {
|
|
4029
|
+
if (entry.name === "node_modules" || entry.name === "dist" || entry.name.startsWith(".")) {
|
|
4030
|
+
continue;
|
|
4031
|
+
}
|
|
4032
|
+
const path = join(folder, entry.name);
|
|
4033
|
+
if (entry.isDirectory()) {
|
|
4034
|
+
walk(path);
|
|
4035
|
+
} else if (entry.name.endsWith(".ts") && !entry.name.endsWith(".d.ts") && !entry.name.endsWith(".spec.ts")) {
|
|
4036
|
+
sources.push({ path, text: readFileSync3(path, "utf8") });
|
|
4037
|
+
}
|
|
4038
|
+
}
|
|
4039
|
+
};
|
|
4040
|
+
try {
|
|
4041
|
+
walk(dir);
|
|
4042
|
+
} catch (error) {
|
|
4043
|
+
throw new ArgError(`Cannot read ${dir}: ${error.message}`);
|
|
4044
|
+
}
|
|
4045
|
+
if (sources.length === 0) {
|
|
4046
|
+
throw new ArgError(`No TypeScript sources found under ${dir}.`);
|
|
4047
|
+
}
|
|
4048
|
+
return sources;
|
|
4049
|
+
}
|
|
4050
|
+
function validateCommandsCommand(args, io) {
|
|
4051
|
+
rejectUnknownFlags(args, ["dir", "strict"]);
|
|
4052
|
+
const dir = requiredFlag(args, "dir");
|
|
4053
|
+
const ts = loadTypeScript(dir);
|
|
4054
|
+
if (!ts) {
|
|
4055
|
+
throw new ArgError(
|
|
4056
|
+
`typescript is not installed where ${dir} can reach it; the check reads sources with the TypeScript compiler API, so run it inside the project.`
|
|
4057
|
+
);
|
|
4058
|
+
}
|
|
4059
|
+
return reportFindings(io, validateCommands(readSources(dir), ts), boolFlag(args, "strict") === true);
|
|
4060
|
+
}
|
|
3774
4061
|
function readCatalog(file) {
|
|
3775
4062
|
let raw;
|
|
3776
4063
|
try {
|
|
@@ -3891,6 +4178,9 @@ function run(argv, io) {
|
|
|
3891
4178
|
if (args.command === "validate-catalog") {
|
|
3892
4179
|
return validateCatalogCommand(args, io);
|
|
3893
4180
|
}
|
|
4181
|
+
if (args.command === "validate-commands") {
|
|
4182
|
+
return validateCommandsCommand(args, io);
|
|
4183
|
+
}
|
|
3894
4184
|
return scaffold(args, io);
|
|
3895
4185
|
} catch (error) {
|
|
3896
4186
|
io.err(error.message);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loomweaver/cli",
|
|
3
|
-
"version": "0.9.0-preview.
|
|
3
|
+
"version": "0.9.0-preview.2",
|
|
4
4
|
"description": "LoomWeaver: open-source plugin platform for Angular workbenches. The scaffolding CLI: generates weavers, distributions and integrations into any project, with no Nx workspace and no checkout required.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"loomweaver",
|