@mccaskie-labs/workbench-config 0.1.27 → 0.1.28
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/CHANGELOG.md +6 -0
- package/capabilities/schema.json +183 -0
- package/dist/capability-schema.d.ts +82 -0
- package/dist/capability-schema.js +72 -0
- package/dist/cli.js +55 -8
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/skills-plan.d.ts +7 -0
- package/dist/skills-plan.js +26 -0
- package/dist/skills.d.ts +7 -0
- package/dist/skills.js +91 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @mccaskie-labs/workbench-config
|
|
2
2
|
|
|
3
|
+
## 0.1.28
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Client questions: manifests gain an optional `offer` section (docs/capabilities-manifest.md §9), checked for unique question ids, two to four answers on a cards question, no written "Other", and each package option set once. `renderSkill` and `npx workbench-config skills [--check] [dir]` write or compare the project skills `.claude/skills/workbench-<component>/SKILL.md` generated from them. No manifest has an offer yet.
|
|
8
|
+
|
|
3
9
|
## 0.1.27
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/capabilities/schema.json
CHANGED
|
@@ -401,6 +401,189 @@
|
|
|
401
401
|
],
|
|
402
402
|
"additionalProperties": false
|
|
403
403
|
}
|
|
404
|
+
},
|
|
405
|
+
"offer": {
|
|
406
|
+
"type": "object",
|
|
407
|
+
"properties": {
|
|
408
|
+
"when": {
|
|
409
|
+
"type": "string",
|
|
410
|
+
"minLength": 1
|
|
411
|
+
},
|
|
412
|
+
"say": {
|
|
413
|
+
"type": "string",
|
|
414
|
+
"minLength": 1
|
|
415
|
+
},
|
|
416
|
+
"answers_file": {
|
|
417
|
+
"type": "string",
|
|
418
|
+
"pattern": "^workbench\\/[a-z0-9<>/._-]+\\.json$"
|
|
419
|
+
},
|
|
420
|
+
"per": {
|
|
421
|
+
"type": "string",
|
|
422
|
+
"minLength": 1
|
|
423
|
+
},
|
|
424
|
+
"generator": {
|
|
425
|
+
"type": "string",
|
|
426
|
+
"minLength": 1
|
|
427
|
+
},
|
|
428
|
+
"questions": {
|
|
429
|
+
"minItems": 1,
|
|
430
|
+
"type": "array",
|
|
431
|
+
"items": {
|
|
432
|
+
"type": "object",
|
|
433
|
+
"properties": {
|
|
434
|
+
"id": {
|
|
435
|
+
"type": "string",
|
|
436
|
+
"pattern": "^[a-z0-9][a-z0-9.-]*$"
|
|
437
|
+
},
|
|
438
|
+
"sets": {
|
|
439
|
+
"anyOf": [
|
|
440
|
+
{
|
|
441
|
+
"type": "string",
|
|
442
|
+
"minLength": 1
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
"minItems": 1,
|
|
446
|
+
"type": "array",
|
|
447
|
+
"items": {
|
|
448
|
+
"type": "string",
|
|
449
|
+
"minLength": 1
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
]
|
|
453
|
+
},
|
|
454
|
+
"ask": {
|
|
455
|
+
"type": "string",
|
|
456
|
+
"minLength": 1
|
|
457
|
+
},
|
|
458
|
+
"style": {
|
|
459
|
+
"type": "string",
|
|
460
|
+
"enum": [
|
|
461
|
+
"cards",
|
|
462
|
+
"table"
|
|
463
|
+
]
|
|
464
|
+
},
|
|
465
|
+
"answers": {
|
|
466
|
+
"minItems": 2,
|
|
467
|
+
"maxItems": 4,
|
|
468
|
+
"type": "array",
|
|
469
|
+
"items": {
|
|
470
|
+
"type": "object",
|
|
471
|
+
"properties": {
|
|
472
|
+
"label": {
|
|
473
|
+
"type": "string",
|
|
474
|
+
"minLength": 1,
|
|
475
|
+
"maxLength": 40
|
|
476
|
+
},
|
|
477
|
+
"value": {
|
|
478
|
+
"type": [
|
|
479
|
+
"string",
|
|
480
|
+
"number",
|
|
481
|
+
"boolean",
|
|
482
|
+
"null"
|
|
483
|
+
]
|
|
484
|
+
},
|
|
485
|
+
"means": {
|
|
486
|
+
"type": "string",
|
|
487
|
+
"minLength": 1
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
"required": [
|
|
491
|
+
"label",
|
|
492
|
+
"value"
|
|
493
|
+
],
|
|
494
|
+
"additionalProperties": false
|
|
495
|
+
}
|
|
496
|
+
},
|
|
497
|
+
"other": {
|
|
498
|
+
"type": "object",
|
|
499
|
+
"properties": {
|
|
500
|
+
"kind": {
|
|
501
|
+
"type": "string",
|
|
502
|
+
"enum": [
|
|
503
|
+
"text",
|
|
504
|
+
"whole",
|
|
505
|
+
"money",
|
|
506
|
+
"date",
|
|
507
|
+
"list"
|
|
508
|
+
]
|
|
509
|
+
},
|
|
510
|
+
"min": {
|
|
511
|
+
"type": [
|
|
512
|
+
"number",
|
|
513
|
+
"string"
|
|
514
|
+
]
|
|
515
|
+
},
|
|
516
|
+
"max": {
|
|
517
|
+
"type": [
|
|
518
|
+
"number",
|
|
519
|
+
"string"
|
|
520
|
+
]
|
|
521
|
+
},
|
|
522
|
+
"description": {
|
|
523
|
+
"type": "string",
|
|
524
|
+
"minLength": 1
|
|
525
|
+
}
|
|
526
|
+
},
|
|
527
|
+
"required": [
|
|
528
|
+
"kind"
|
|
529
|
+
],
|
|
530
|
+
"additionalProperties": false
|
|
531
|
+
},
|
|
532
|
+
"for": {
|
|
533
|
+
"minItems": 1,
|
|
534
|
+
"type": "array",
|
|
535
|
+
"items": {
|
|
536
|
+
"type": "string",
|
|
537
|
+
"minLength": 1
|
|
538
|
+
}
|
|
539
|
+
},
|
|
540
|
+
"must_ask": {
|
|
541
|
+
"type": "boolean"
|
|
542
|
+
},
|
|
543
|
+
"note": {
|
|
544
|
+
"type": "string",
|
|
545
|
+
"minLength": 1
|
|
546
|
+
}
|
|
547
|
+
},
|
|
548
|
+
"required": [
|
|
549
|
+
"id",
|
|
550
|
+
"sets",
|
|
551
|
+
"ask",
|
|
552
|
+
"style"
|
|
553
|
+
],
|
|
554
|
+
"additionalProperties": false
|
|
555
|
+
}
|
|
556
|
+
},
|
|
557
|
+
"not_asked": {
|
|
558
|
+
"minItems": 1,
|
|
559
|
+
"type": "array",
|
|
560
|
+
"items": {
|
|
561
|
+
"type": "object",
|
|
562
|
+
"properties": {
|
|
563
|
+
"sets": {
|
|
564
|
+
"type": "string",
|
|
565
|
+
"minLength": 1
|
|
566
|
+
},
|
|
567
|
+
"why": {
|
|
568
|
+
"type": "string",
|
|
569
|
+
"minLength": 1
|
|
570
|
+
}
|
|
571
|
+
},
|
|
572
|
+
"required": [
|
|
573
|
+
"sets",
|
|
574
|
+
"why"
|
|
575
|
+
],
|
|
576
|
+
"additionalProperties": false
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
},
|
|
580
|
+
"required": [
|
|
581
|
+
"when",
|
|
582
|
+
"say",
|
|
583
|
+
"answers_file",
|
|
584
|
+
"questions"
|
|
585
|
+
],
|
|
586
|
+
"additionalProperties": false
|
|
404
587
|
}
|
|
405
588
|
},
|
|
406
589
|
"required": [
|
|
@@ -87,6 +87,46 @@ export declare const allowedSchemas: {
|
|
|
87
87
|
}>>;
|
|
88
88
|
}, z.core.$strict>;
|
|
89
89
|
};
|
|
90
|
+
declare const offer: z.ZodObject<{
|
|
91
|
+
when: z.ZodString;
|
|
92
|
+
say: z.ZodString;
|
|
93
|
+
answers_file: z.ZodString;
|
|
94
|
+
per: z.ZodOptional<z.ZodString>;
|
|
95
|
+
generator: z.ZodOptional<z.ZodString>;
|
|
96
|
+
questions: z.ZodArray<z.ZodObject<{
|
|
97
|
+
id: z.ZodString;
|
|
98
|
+
sets: z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>;
|
|
99
|
+
ask: z.ZodString;
|
|
100
|
+
style: z.ZodEnum<{
|
|
101
|
+
cards: "cards";
|
|
102
|
+
table: "table";
|
|
103
|
+
}>;
|
|
104
|
+
answers: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
105
|
+
label: z.ZodString;
|
|
106
|
+
value: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>;
|
|
107
|
+
means: z.ZodOptional<z.ZodString>;
|
|
108
|
+
}, z.core.$strict>>>;
|
|
109
|
+
other: z.ZodOptional<z.ZodObject<{
|
|
110
|
+
kind: z.ZodEnum<{
|
|
111
|
+
date: "date";
|
|
112
|
+
list: "list";
|
|
113
|
+
money: "money";
|
|
114
|
+
text: "text";
|
|
115
|
+
whole: "whole";
|
|
116
|
+
}>;
|
|
117
|
+
min: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>>;
|
|
118
|
+
max: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>>;
|
|
119
|
+
description: z.ZodOptional<z.ZodString>;
|
|
120
|
+
}, z.core.$strict>>;
|
|
121
|
+
for: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
122
|
+
must_ask: z.ZodOptional<z.ZodBoolean>;
|
|
123
|
+
note: z.ZodOptional<z.ZodString>;
|
|
124
|
+
}, z.core.$strict>>;
|
|
125
|
+
not_asked: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
126
|
+
sets: z.ZodString;
|
|
127
|
+
why: z.ZodString;
|
|
128
|
+
}, z.core.$strict>>>;
|
|
129
|
+
}, z.core.$strict>;
|
|
90
130
|
/** One file in `capabilities/`. Parsing checks shape only; cross-references are checked by `checkManifests`. */
|
|
91
131
|
export declare const capabilitySchema: z.ZodObject<{
|
|
92
132
|
$schema: z.ZodLiteral<"./schema.json">;
|
|
@@ -221,8 +261,49 @@ export declare const capabilitySchema: z.ZodObject<{
|
|
|
221
261
|
two_step: z.ZodOptional<z.ZodBoolean>;
|
|
222
262
|
note: z.ZodOptional<z.ZodString>;
|
|
223
263
|
}, z.core.$strict>>;
|
|
264
|
+
offer: z.ZodOptional<z.ZodObject<{
|
|
265
|
+
when: z.ZodString;
|
|
266
|
+
say: z.ZodString;
|
|
267
|
+
answers_file: z.ZodString;
|
|
268
|
+
per: z.ZodOptional<z.ZodString>;
|
|
269
|
+
generator: z.ZodOptional<z.ZodString>;
|
|
270
|
+
questions: z.ZodArray<z.ZodObject<{
|
|
271
|
+
id: z.ZodString;
|
|
272
|
+
sets: z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>;
|
|
273
|
+
ask: z.ZodString;
|
|
274
|
+
style: z.ZodEnum<{
|
|
275
|
+
cards: "cards";
|
|
276
|
+
table: "table";
|
|
277
|
+
}>;
|
|
278
|
+
answers: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
279
|
+
label: z.ZodString;
|
|
280
|
+
value: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>;
|
|
281
|
+
means: z.ZodOptional<z.ZodString>;
|
|
282
|
+
}, z.core.$strict>>>;
|
|
283
|
+
other: z.ZodOptional<z.ZodObject<{
|
|
284
|
+
kind: z.ZodEnum<{
|
|
285
|
+
date: "date";
|
|
286
|
+
list: "list";
|
|
287
|
+
money: "money";
|
|
288
|
+
text: "text";
|
|
289
|
+
whole: "whole";
|
|
290
|
+
}>;
|
|
291
|
+
min: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>>;
|
|
292
|
+
max: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>>;
|
|
293
|
+
description: z.ZodOptional<z.ZodString>;
|
|
294
|
+
}, z.core.$strict>>;
|
|
295
|
+
for: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
296
|
+
must_ask: z.ZodOptional<z.ZodBoolean>;
|
|
297
|
+
note: z.ZodOptional<z.ZodString>;
|
|
298
|
+
}, z.core.$strict>>;
|
|
299
|
+
not_asked: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
300
|
+
sets: z.ZodString;
|
|
301
|
+
why: z.ZodString;
|
|
302
|
+
}, z.core.$strict>>>;
|
|
303
|
+
}, z.core.$strict>>;
|
|
224
304
|
}, z.core.$strict>;
|
|
225
305
|
export type Capability = z.infer<typeof capabilitySchema>;
|
|
306
|
+
export type Offer = z.infer<typeof offer>;
|
|
226
307
|
export interface ManifestProblem {
|
|
227
308
|
file: string;
|
|
228
309
|
check: "schema" | "proof" | "ids" | "surfaces" | "errors" | "generated";
|
|
@@ -236,3 +317,4 @@ export declare function checkManifests(files: Record<string, unknown>): {
|
|
|
236
317
|
manifests: Capability[];
|
|
237
318
|
problems: ManifestProblem[];
|
|
238
319
|
};
|
|
320
|
+
export {};
|
|
@@ -136,6 +136,42 @@ export const allowedSchemas = {
|
|
|
136
136
|
environments: z.array(envName).min(1),
|
|
137
137
|
}),
|
|
138
138
|
};
|
|
139
|
+
// Client questions (docs/capabilities-manifest.md §9): what Claude Code asks
|
|
140
|
+
// the client when it builds with this component. A project skill is
|
|
141
|
+
// generated from it (`workbench-config skills`).
|
|
142
|
+
const answerValue = z.union([z.string(), z.number(), z.boolean(), z.null()]);
|
|
143
|
+
const otherRule = z.strictObject({
|
|
144
|
+
kind: z.enum(["text", "whole", "money", "date", "list"]),
|
|
145
|
+
min: z.union([z.number(), z.string()]).optional(),
|
|
146
|
+
max: z.union([z.number(), z.string()]).optional(),
|
|
147
|
+
description: z.string().min(1).optional(),
|
|
148
|
+
});
|
|
149
|
+
const question = z.strictObject({
|
|
150
|
+
id,
|
|
151
|
+
sets: z.union([z.string().min(1), z.array(z.string().min(1)).min(1)]),
|
|
152
|
+
ask: z.string().min(1),
|
|
153
|
+
/** `cards`: two to four answers, recommended first. `table`: a table of proposed values to accept or correct. */
|
|
154
|
+
style: z.enum(["cards", "table"]),
|
|
155
|
+
answers: z
|
|
156
|
+
.array(z.strictObject({ label: z.string().min(1).max(40), value: answerValue, means: z.string().min(1).optional() }))
|
|
157
|
+
.min(2)
|
|
158
|
+
.max(4)
|
|
159
|
+
.optional(),
|
|
160
|
+
/** Absent: "Other" isn't allowed. Present: what a typed answer must satisfy. */
|
|
161
|
+
other: otherRule.optional(),
|
|
162
|
+
for: z.array(z.string().min(1)).min(1).optional(),
|
|
163
|
+
must_ask: z.boolean().optional(),
|
|
164
|
+
note: z.string().min(1).optional(),
|
|
165
|
+
});
|
|
166
|
+
const offer = z.strictObject({
|
|
167
|
+
when: z.string().min(1),
|
|
168
|
+
say: z.string().min(1),
|
|
169
|
+
answers_file: z.string().regex(/^workbench\/[a-z0-9<>/._-]+\.json$/, "a JSON file under workbench/"),
|
|
170
|
+
per: z.string().min(1).optional(),
|
|
171
|
+
generator: z.string().min(1).optional(),
|
|
172
|
+
questions: z.array(question).min(1),
|
|
173
|
+
not_asked: z.array(z.strictObject({ sets: z.string().min(1), why: z.string().min(1) })).min(1).optional(),
|
|
174
|
+
});
|
|
139
175
|
const baseShape = {
|
|
140
176
|
$schema: z.literal("./schema.json"),
|
|
141
177
|
format: z.literal(1),
|
|
@@ -158,6 +194,7 @@ const baseShape = {
|
|
|
158
194
|
rules: z.array(rule).min(1),
|
|
159
195
|
examples: z.array(example).min(1).optional(),
|
|
160
196
|
probes: z.array(probe).min(1),
|
|
197
|
+
offer: offer.optional(),
|
|
161
198
|
};
|
|
162
199
|
/** One file in `capabilities/`. Parsing checks shape only; cross-references are checked by `checkManifests`. */
|
|
163
200
|
export const capabilitySchema = z.strictObject(baseShape).superRefine((m, ctx) => {
|
|
@@ -175,6 +212,39 @@ export const capabilitySchema = z.strictObject(baseShape).superRefine((m, ctx) =
|
|
|
175
212
|
}
|
|
176
213
|
}
|
|
177
214
|
});
|
|
215
|
+
function checkOffer(file, component, o, seen) {
|
|
216
|
+
const problems = [];
|
|
217
|
+
const named = new Map();
|
|
218
|
+
const name = (option, by) => {
|
|
219
|
+
const prior = named.get(option);
|
|
220
|
+
if (prior)
|
|
221
|
+
problems.push({ file, check: "ids", message: `offer: ${option} is set by ${prior} and ${by}` });
|
|
222
|
+
else
|
|
223
|
+
named.set(option, by);
|
|
224
|
+
};
|
|
225
|
+
for (const q of o.questions) {
|
|
226
|
+
seen("question", q.id);
|
|
227
|
+
if (!q.id.startsWith(`${component}.`))
|
|
228
|
+
problems.push({ file, check: "ids", message: `question ${q.id} must start with ${component}.` });
|
|
229
|
+
if (q.style === "cards" && !q.answers)
|
|
230
|
+
problems.push({ file, check: "schema", message: `question ${q.id} is cards but has no answers` });
|
|
231
|
+
if (q.style === "table" && q.answers)
|
|
232
|
+
problems.push({ file, check: "schema", message: `question ${q.id} is a table, so it lists no answers` });
|
|
233
|
+
const labels = new Set();
|
|
234
|
+
for (const a of q.answers ?? []) {
|
|
235
|
+
if (labels.has(a.label))
|
|
236
|
+
problems.push({ file, check: "ids", message: `question ${q.id} has the answer ${a.label} twice` });
|
|
237
|
+
labels.add(a.label);
|
|
238
|
+
if (/^other$/i.test(a.label))
|
|
239
|
+
problems.push({ file, check: "schema", message: `question ${q.id}: "Other" is added by Claude Code; use \`other\` instead` });
|
|
240
|
+
}
|
|
241
|
+
for (const option of typeof q.sets === "string" ? [q.sets] : q.sets)
|
|
242
|
+
name(option, `question ${q.id}`);
|
|
243
|
+
}
|
|
244
|
+
for (const n of o.not_asked ?? [])
|
|
245
|
+
name(n.sets, "not_asked");
|
|
246
|
+
return problems;
|
|
247
|
+
}
|
|
178
248
|
/**
|
|
179
249
|
* The checks that need only the manifests themselves (§5: schema, proof, ids).
|
|
180
250
|
* `files` maps a file name (e.g. `ai.json`) to its parsed JSON.
|
|
@@ -244,6 +314,8 @@ export function checkManifests(files) {
|
|
|
244
314
|
if (a.gate === "two_step_when" && !a.when)
|
|
245
315
|
problems.push({ file, check: "schema", message: `action ${a.tool} is two_step_when but has no when` });
|
|
246
316
|
}
|
|
317
|
+
if (m.offer)
|
|
318
|
+
problems.push(...checkOffer(file, m.component, m.offer, seen));
|
|
247
319
|
}
|
|
248
320
|
return { manifests, problems };
|
|
249
321
|
}
|
package/dist/cli.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// `npx workbench-config check [dir]`: compares the variable names in
|
|
3
3
|
// <dir>/.dev.vars with <dir>/.dev.vars.example. Prints names only.
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
// `npx workbench-config skills [--check] [dir]`: writes (or, with --check,
|
|
5
|
+
// compares) the project skills generated from the manifests' offer sections
|
|
6
|
+
// (docs/capabilities-manifest.md §9).
|
|
7
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { dirname, join } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
6
10
|
import { compareDevVars } from "./dev-vars.js";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
console.error("Usage: workbench-config check [dir]");
|
|
11
|
-
return 2;
|
|
12
|
-
}
|
|
11
|
+
import { planSkills } from "./skills-plan.js";
|
|
12
|
+
const USAGE = "Usage: workbench-config check [dir]\n workbench-config skills [--check] [dir]";
|
|
13
|
+
function checkDevVars(dir) {
|
|
13
14
|
const examplePath = join(dir, ".dev.vars.example");
|
|
14
15
|
const devVarsPath = join(dir, ".dev.vars");
|
|
15
16
|
if (!existsSync(examplePath)) {
|
|
@@ -31,4 +32,50 @@ function main(argv) {
|
|
|
31
32
|
console.error(`In .dev.vars but not in .dev.vars.example: ${result.extra.join(", ")}`);
|
|
32
33
|
return 1;
|
|
33
34
|
}
|
|
35
|
+
function skills(args) {
|
|
36
|
+
const check = args.includes("--check");
|
|
37
|
+
const dir = args.find((a) => a !== "--check") ?? ".";
|
|
38
|
+
const pkg = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
39
|
+
const version = JSON.parse(readFileSync(join(pkg, "package.json"), "utf8")).version;
|
|
40
|
+
const { files, stale, problems } = planSkills(join(pkg, "capabilities"), dir);
|
|
41
|
+
if (problems.length) {
|
|
42
|
+
for (const p of problems)
|
|
43
|
+
console.error(p);
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
if (check) {
|
|
47
|
+
const wrong = Object.entries(files).filter(([path, text]) => !existsSync(join(dir, path)) || readFileSync(join(dir, path), "utf8") !== text).map(([path]) => path);
|
|
48
|
+
for (const path of wrong)
|
|
49
|
+
console.error(`${path} is missing or differs from workbench-config ${version}.`);
|
|
50
|
+
for (const path of stale)
|
|
51
|
+
console.error(`${path} is no longer generated by workbench-config ${version}.`);
|
|
52
|
+
if (wrong.length || stale.length) {
|
|
53
|
+
console.error("Run `npx workbench-config skills` and commit the result.");
|
|
54
|
+
return 1;
|
|
55
|
+
}
|
|
56
|
+
console.log(`Skills match workbench-config ${version} (${Object.keys(files).length}).`);
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
for (const [path, text] of Object.entries(files)) {
|
|
60
|
+
mkdirSync(dirname(join(dir, path)), { recursive: true });
|
|
61
|
+
writeFileSync(join(dir, path), text);
|
|
62
|
+
console.log(`wrote ${path}`);
|
|
63
|
+
}
|
|
64
|
+
for (const path of stale) {
|
|
65
|
+
rmSync(join(dir, path));
|
|
66
|
+
if (readdirSync(dirname(join(dir, path))).length === 0)
|
|
67
|
+
rmSync(dirname(join(dir, path)), { recursive: true });
|
|
68
|
+
console.log(`removed ${path}`);
|
|
69
|
+
}
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
function main(argv) {
|
|
73
|
+
const [command, ...rest] = argv;
|
|
74
|
+
if (command === "check")
|
|
75
|
+
return checkDevVars(rest[0] ?? ".");
|
|
76
|
+
if (command === "skills")
|
|
77
|
+
return skills(rest);
|
|
78
|
+
console.error(USAGE);
|
|
79
|
+
return 2;
|
|
80
|
+
}
|
|
34
81
|
process.exitCode = main(process.argv.slice(2));
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,8 @@ export { WORKBENCH_ENVS, DEFAULT_GATEWAY_URL, workbenchEnv, type WorkbenchEnv }
|
|
|
2
2
|
export { defineConfig, type Config } from "./define-config.ts";
|
|
3
3
|
export { sharedSecret, workerSecret, redact, redactAll, type SecretsStoreSecretLike } from "./secrets.ts";
|
|
4
4
|
export { WorkbenchConfigError, type ConfigErrorCode, type ConfigProblem } from "./errors.ts";
|
|
5
|
-
export { capabilitySchema, checkManifests, allowedSchemas, COMPONENTS, type Capability, type Component, type ManifestProblem, } from "./capability-schema.ts";
|
|
5
|
+
export { capabilitySchema, checkManifests, allowedSchemas, COMPONENTS, type Capability, type Component, type ManifestProblem, type Offer, } from "./capability-schema.ts";
|
|
6
|
+
export { renderSkill, renderSkills, skillName, SKILLS_DIR } from "./skills.ts";
|
|
6
7
|
export { capabilities, MANIFEST_VERSION, type Alias, type GatewayScope } from "./capabilities.gen.ts";
|
|
7
8
|
export { compareDevVars, parseDevVarNames } from "./dev-vars.ts";
|
|
8
9
|
export { brandSchema, defineBrand, BRAND_FILE, resolveBrand, BRAND_DEFAULTS, type Brand, type ResolvedBrand } from "./brand.ts";
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ export { defineConfig } from "./define-config.js";
|
|
|
3
3
|
export { sharedSecret, workerSecret, redact, redactAll } from "./secrets.js";
|
|
4
4
|
export { WorkbenchConfigError } from "./errors.js";
|
|
5
5
|
export { capabilitySchema, checkManifests, allowedSchemas, COMPONENTS, } from "./capability-schema.js";
|
|
6
|
+
export { renderSkill, renderSkills, skillName, SKILLS_DIR } from "./skills.js";
|
|
6
7
|
export { capabilities, MANIFEST_VERSION } from "./capabilities.gen.js";
|
|
7
8
|
export { compareDevVars, parseDevVarNames } from "./dev-vars.js";
|
|
8
9
|
export { brandSchema, defineBrand, BRAND_FILE, resolveBrand, BRAND_DEFAULTS } from "./brand.js";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const GENERATED_MARK = "Generated by `npx workbench-config skills`";
|
|
2
|
+
/** Skill files the manifests in `manifestDir` produce, and the generated ones in `projectDir` they don't. */
|
|
3
|
+
export declare function planSkills(manifestDir: string, projectDir: string): {
|
|
4
|
+
files: Record<string, string>;
|
|
5
|
+
stale: string[];
|
|
6
|
+
problems: string[];
|
|
7
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Node only: reads a manifests directory and a project, for
|
|
2
|
+
// `workbench-config skills` (cli.ts). Kept out of index.ts, which runs in Workers.
|
|
3
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { checkManifests } from "./capability-schema.js";
|
|
6
|
+
import { renderSkills, SKILLS_DIR } from "./skills.js";
|
|
7
|
+
export const GENERATED_MARK = "Generated by `npx workbench-config skills`";
|
|
8
|
+
/** Skill files the manifests in `manifestDir` produce, and the generated ones in `projectDir` they don't. */
|
|
9
|
+
export function planSkills(manifestDir, projectDir) {
|
|
10
|
+
const json = {};
|
|
11
|
+
for (const f of readdirSync(manifestDir).filter((f) => f.endsWith(".json") && f !== "schema.json")) {
|
|
12
|
+
json[f] = JSON.parse(readFileSync(join(manifestDir, f), "utf8"));
|
|
13
|
+
}
|
|
14
|
+
const { manifests, problems } = checkManifests(json);
|
|
15
|
+
const files = renderSkills(manifests);
|
|
16
|
+
const stale = [];
|
|
17
|
+
const skillsDir = join(projectDir, SKILLS_DIR);
|
|
18
|
+
if (existsSync(skillsDir)) {
|
|
19
|
+
for (const d of readdirSync(skillsDir)) {
|
|
20
|
+
const path = `${SKILLS_DIR}/${d}/SKILL.md`;
|
|
21
|
+
if (d.startsWith("workbench-") && existsSync(join(projectDir, path)) && !(path in files) && readFileSync(join(projectDir, path), "utf8").includes(GENERATED_MARK))
|
|
22
|
+
stale.push(path);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return { files, stale, problems: problems.map((p) => `${p.file}: [${p.check}] ${p.message}`) };
|
|
26
|
+
}
|
package/dist/skills.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Capability } from "./capability-schema.ts";
|
|
2
|
+
export declare const SKILLS_DIR = ".claude/skills";
|
|
3
|
+
export declare const skillName: (component: string) => string;
|
|
4
|
+
/** The SKILL.md for one manifest, or undefined when it has no offer. */
|
|
5
|
+
export declare function renderSkill(m: Capability): string | undefined;
|
|
6
|
+
/** Every skill the manifests produce, keyed by its path in a project. */
|
|
7
|
+
export declare function renderSkills(manifests: readonly Capability[]): Record<string, string>;
|
package/dist/skills.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Project skills generated from the manifests' `offer` sections
|
|
2
|
+
// (docs/capabilities-manifest.md §9). `workbench-config skills` writes one
|
|
3
|
+
// `.claude/skills/workbench-<component>/SKILL.md` per offer into a project,
|
|
4
|
+
// and `--check` fails when a file differs, so the questions Claude Code asks
|
|
5
|
+
// a client never drift from what the package accepts.
|
|
6
|
+
export const SKILLS_DIR = ".claude/skills";
|
|
7
|
+
export const skillName = (component) => `workbench-${component}`;
|
|
8
|
+
const show = (v) => (v === null ? "none" : `\`${JSON.stringify(v)}\``);
|
|
9
|
+
const cell = (s) => s.replaceAll("|", "\\|").replaceAll("\n", " ");
|
|
10
|
+
const sets = (q) => (typeof q.sets === "string" ? [q.sets] : q.sets).map((s) => `\`${s}\``).join(", ");
|
|
11
|
+
function otherLine(q) {
|
|
12
|
+
if (!q.other)
|
|
13
|
+
return "Other: not allowed. Only the answers above are accepted.";
|
|
14
|
+
const { kind, min, max, description } = q.other;
|
|
15
|
+
const range = min !== undefined && max !== undefined ? ` from ${min} to ${max}` : min !== undefined ? ` of at least ${min}` : max !== undefined ? ` of at most ${max}` : "";
|
|
16
|
+
const what = { text: "text", whole: "a whole number", money: "an amount", date: "a date (YYYY-MM-DD)", list: "a list, one item per line" }[kind];
|
|
17
|
+
const length = kind === "text" && range ? ` (length in characters)` : "";
|
|
18
|
+
return `Other: allowed. The client must type ${what}${range}${length}.${description ? ` ${description}` : ""} Refuse anything else and ask again.`;
|
|
19
|
+
}
|
|
20
|
+
function questionBlock(q, n) {
|
|
21
|
+
const lines = [`### ${n}. ${q.ask}`, "", `Id \`${q.id}\`. Sets ${sets(q)}.`];
|
|
22
|
+
if (q.for)
|
|
23
|
+
lines.push(`Only for: ${q.for.map((f) => `\`${f}\``).join(", ")}.`);
|
|
24
|
+
if (q.must_ask)
|
|
25
|
+
lines.push("**Must ask:** there is no safe default, so always ask it, even when the client accepts every recommendation.");
|
|
26
|
+
lines.push("");
|
|
27
|
+
if (q.style === "table") {
|
|
28
|
+
lines.push("Ask as a table: propose a value for each row, show the table, then ask one question with the answers \"Accept all (Recommended)\", \"Change some\" and \"Add or remove\".");
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
lines.push("| Answer | Value | Means |", "| --- | --- | --- |");
|
|
32
|
+
(q.answers ?? []).forEach((a, i) => lines.push(`| ${cell(a.label)}${i === 0 ? " (Recommended)" : ""} | ${show(a.value)} | ${cell(a.means ?? "")} |`));
|
|
33
|
+
}
|
|
34
|
+
lines.push("", otherLine(q));
|
|
35
|
+
if (q.note)
|
|
36
|
+
lines.push("", q.note);
|
|
37
|
+
return lines.join("\n");
|
|
38
|
+
}
|
|
39
|
+
/** The SKILL.md for one manifest, or undefined when it has no offer. */
|
|
40
|
+
export function renderSkill(m) {
|
|
41
|
+
const o = m.offer;
|
|
42
|
+
if (!o)
|
|
43
|
+
return undefined;
|
|
44
|
+
const description = `${o.when} Offers Workbench ${m.title} and asks the client for its settings.`.replaceAll("\n", " ");
|
|
45
|
+
const out = [
|
|
46
|
+
"---",
|
|
47
|
+
`name: ${skillName(m.component)}`,
|
|
48
|
+
`description: ${JSON.stringify(description)}`,
|
|
49
|
+
"---",
|
|
50
|
+
"",
|
|
51
|
+
`<!-- Generated by \`npx workbench-config skills\` from @mccaskie-labs/workbench-config, capabilities/${m.component}.json. Don't edit: \`npx workbench-config skills --check\` fails when this file differs. -->`,
|
|
52
|
+
"",
|
|
53
|
+
`# ${m.title}`,
|
|
54
|
+
"",
|
|
55
|
+
m.summary,
|
|
56
|
+
"",
|
|
57
|
+
"## When this applies",
|
|
58
|
+
"",
|
|
59
|
+
o.when,
|
|
60
|
+
"",
|
|
61
|
+
"## What to say",
|
|
62
|
+
"",
|
|
63
|
+
`Say: "${o.say}" Don't ask whether to use Workbench or something of the client's own. If the client says they don't want it, stop and say what they give up.`,
|
|
64
|
+
"",
|
|
65
|
+
"## How to ask",
|
|
66
|
+
"",
|
|
67
|
+
"- Ask with the AskUserQuestion tool, up to four questions per call. Give each question's answers in the order below; the first is recommended, so label it \"(Recommended)\". The tool adds \"Other\" itself.",
|
|
68
|
+
"- Where the client accepts every recommendation, still ask each \"Must ask\" question.",
|
|
69
|
+
"- An \"Other\" answer must meet the rule under its question. When it doesn't, say why and ask again. Never store a value outside it.",
|
|
70
|
+
"- Ask nothing that isn't listed here, and invent no setting. A need the questions don't cover goes back to the client as a plain question, and isn't written to the answers file.",
|
|
71
|
+
`- Write the answers to \`${o.answers_file}\`${o.per ? `, one entry per ${o.per}` : ""}: for each question its id, the value, and the client's own words when they typed "Other" or changed a recommendation.${o.generator ? ` Then run \`${o.generator}\`, which writes the code and refuses an out-of-range answer. Never edit what it writes; change the answers file and run it again.` : ""}`,
|
|
72
|
+
"",
|
|
73
|
+
"## Questions",
|
|
74
|
+
"",
|
|
75
|
+
o.questions.map((q, i) => questionBlock(q, i + 1)).join("\n\n"),
|
|
76
|
+
];
|
|
77
|
+
if (o.not_asked?.length) {
|
|
78
|
+
out.push("", "## Not asked", "", ...o.not_asked.map((n) => `- \`${n.sets}\`: ${n.why}`));
|
|
79
|
+
}
|
|
80
|
+
return `${out.join("\n")}\n`;
|
|
81
|
+
}
|
|
82
|
+
/** Every skill the manifests produce, keyed by its path in a project. */
|
|
83
|
+
export function renderSkills(manifests) {
|
|
84
|
+
const files = {};
|
|
85
|
+
for (const m of manifests) {
|
|
86
|
+
const text = renderSkill(m);
|
|
87
|
+
if (text)
|
|
88
|
+
files[`${SKILLS_DIR}/${skillName(m.component)}/SKILL.md`] = text;
|
|
89
|
+
}
|
|
90
|
+
return files;
|
|
91
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mccaskie-labs/workbench-config",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.28",
|
|
4
4
|
"description": "Workbench app configuration: WORKBENCH_ENV, typed variables that fail by name, secrets that are never printed, and the capabilities manifest.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|