@forwardimpact/pathway 0.25.22 → 0.25.25
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/bin/fit-pathway.js +117 -325
- package/package.json +2 -2
- package/src/commands/agent-io.js +1 -1
- package/src/commands/agent-list.js +164 -0
- package/src/commands/agent.js +83 -184
- package/src/commands/behaviour.js +22 -10
- package/src/commands/build-packs.js +208 -34
- package/src/commands/build.js +2 -2
- package/src/commands/command-factory.js +39 -14
- package/src/commands/discipline.js +24 -10
- package/src/commands/driver.js +28 -19
- package/src/commands/index.js +0 -1
- package/src/commands/interview.js +15 -10
- package/src/commands/job.js +110 -62
- package/src/commands/level.js +23 -11
- package/src/commands/progress.js +12 -7
- package/src/commands/questions.js +32 -14
- package/src/commands/skill.js +36 -18
- package/src/commands/stage.js +37 -27
- package/src/commands/tool.js +29 -19
- package/src/commands/track.js +23 -10
- package/src/formatters/questions/yaml.js +1 -1
- package/src/index.html +1 -1
- package/src/lib/cli-command.js +33 -33
- package/src/lib/cli-output.js +9 -189
- package/src/pages/agent-builder-install.js +6 -5
- package/src/commands/init.js +0 -64
- package/starter/behaviours/systems_thinking.yaml +0 -32
- package/starter/capabilities/delivery.yaml +0 -105
- package/starter/capabilities/reliability.yaml +0 -72
- package/starter/disciplines/software_engineering.yaml +0 -46
- package/starter/drivers.yaml +0 -10
- package/starter/framework.yaml +0 -49
- package/starter/levels.yaml +0 -39
- package/starter/questions/behaviours/.gitkeep +0 -0
- package/starter/questions/capabilities/.gitkeep +0 -0
- package/starter/questions/skills/.gitkeep +0 -0
- package/starter/stages.yaml +0 -21
- package/starter/tracks/forward_deployed.yaml +0 -33
- package/starter/tracks/platform.yaml +0 -33
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pack generation for Pathway distribution.
|
|
3
3
|
*
|
|
4
|
-
* Emits one pre-built agent/skill pack per valid discipline/track combination
|
|
5
|
-
*
|
|
6
|
-
* `
|
|
7
|
-
*
|
|
4
|
+
* Emits one pre-built agent/skill pack per valid discipline/track combination.
|
|
5
|
+
* Each pack becomes its own `npx skills`-compatible repository at
|
|
6
|
+
* `packs/{name}/.well-known/skills/`, with an aggregate repository at
|
|
7
|
+
* `packs/.well-known/skills/` listing every skill from every pack.
|
|
8
|
+
* An `apm.yml` for Microsoft APM is also written at the site root.
|
|
9
|
+
*
|
|
10
|
+
* See specs/320-pathway-ecosystem-distribution for context.
|
|
8
11
|
*
|
|
9
12
|
* Invoked from build.js after the distribution bundle has been generated.
|
|
10
13
|
*/
|
|
11
14
|
|
|
12
|
-
import { mkdir, rm, readFile, writeFile } from "fs/promises";
|
|
15
|
+
import { mkdir, rm, readFile, writeFile, readdir, cp } from "fs/promises";
|
|
16
|
+
import { utimesSync } from "fs";
|
|
13
17
|
import { join } from "path";
|
|
14
18
|
import { execFileSync } from "child_process";
|
|
15
19
|
import { createHash } from "crypto";
|
|
@@ -209,50 +213,205 @@ function derivePackContent({
|
|
|
209
213
|
return { profiles, skillFiles, teamInstructions };
|
|
210
214
|
}
|
|
211
215
|
|
|
216
|
+
/**
|
|
217
|
+
* Recursively collect all paths (files and directories) under `dir`,
|
|
218
|
+
* relative to `dir`, in sorted order.
|
|
219
|
+
*/
|
|
220
|
+
async function collectPaths(dir, prefix = ".") {
|
|
221
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
222
|
+
const result = [];
|
|
223
|
+
for (const entry of entries) {
|
|
224
|
+
const rel = prefix + "/" + entry.name;
|
|
225
|
+
const abs = join(dir, entry.name);
|
|
226
|
+
if (entry.isDirectory()) {
|
|
227
|
+
result.push(rel);
|
|
228
|
+
result.push(...(await collectPaths(abs, rel)));
|
|
229
|
+
} else {
|
|
230
|
+
result.push(rel);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return result;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Set mtime and atime to the Unix epoch for every entry under `dir`.
|
|
238
|
+
*/
|
|
239
|
+
async function resetTimestamps(dir) {
|
|
240
|
+
const epoch = new Date(0);
|
|
241
|
+
const paths = await collectPaths(dir);
|
|
242
|
+
for (const rel of paths) {
|
|
243
|
+
utimesSync(join(dir, rel), epoch, epoch);
|
|
244
|
+
}
|
|
245
|
+
utimesSync(dir, epoch, epoch);
|
|
246
|
+
}
|
|
247
|
+
|
|
212
248
|
/**
|
|
213
249
|
* Archive a staged pack directory as a deterministic tar.gz and return its
|
|
214
250
|
* sha256 digest.
|
|
251
|
+
*
|
|
252
|
+
* Determinism strategy (works on GNU tar and BSD tar):
|
|
253
|
+
* 1. Reset all file timestamps to epoch via Node's utimesSync.
|
|
254
|
+
* 2. Collect and sort the file list in JS — no reliance on --sort=name.
|
|
255
|
+
* 3. Create an uncompressed tar to stdout with the sorted list.
|
|
256
|
+
* 4. Pipe through `gzip -n` to suppress the gzip header timestamp.
|
|
257
|
+
*
|
|
215
258
|
* @param {string} packDir - Staging directory containing the pack files
|
|
216
259
|
* @param {string} archivePath - Destination path for the tar.gz
|
|
217
260
|
* @returns {Promise<string>} sha256 digest string (e.g. "sha256:abc...")
|
|
218
261
|
*/
|
|
219
262
|
async function archivePack(packDir, archivePath) {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
"-C",
|
|
229
|
-
packDir,
|
|
230
|
-
".",
|
|
231
|
-
]);
|
|
263
|
+
await resetTimestamps(packDir);
|
|
264
|
+
|
|
265
|
+
const files = await collectPaths(packDir);
|
|
266
|
+
files.sort();
|
|
267
|
+
|
|
268
|
+
const tarBuf = execFileSync("tar", ["-cf", "-", "-C", packDir, ...files]);
|
|
269
|
+
const gzBuf = execFileSync("gzip", ["-n"], { input: tarBuf });
|
|
270
|
+
await writeFile(archivePath, gzBuf);
|
|
232
271
|
|
|
233
272
|
const bytes = await readFile(archivePath);
|
|
234
273
|
return "sha256:" + createHash("sha256").update(bytes).digest("hex");
|
|
235
274
|
}
|
|
236
275
|
|
|
237
276
|
/**
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
* @param {
|
|
241
|
-
* @param {string}
|
|
277
|
+
* Collect all file paths under `dir`, relative to `dir`, for the manifest
|
|
278
|
+
* `files` array. Returns sorted paths with forward slashes.
|
|
279
|
+
* @param {string} dir
|
|
280
|
+
* @param {string} [prefix]
|
|
281
|
+
* @returns {Promise<string[]>}
|
|
282
|
+
*/
|
|
283
|
+
async function collectFileList(dir, prefix = "") {
|
|
284
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
285
|
+
const result = [];
|
|
286
|
+
for (const entry of entries) {
|
|
287
|
+
const rel = prefix ? prefix + "/" + entry.name : entry.name;
|
|
288
|
+
if (entry.isDirectory()) {
|
|
289
|
+
result.push(...(await collectFileList(join(dir, entry.name), rel)));
|
|
290
|
+
} else {
|
|
291
|
+
result.push(rel);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return result.sort();
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Parse YAML frontmatter from a SKILL.md file. Returns an object with
|
|
299
|
+
* the key/value pairs found between the `---` fences.
|
|
300
|
+
* @param {string} content
|
|
301
|
+
* @returns {Record<string, string>}
|
|
302
|
+
*/
|
|
303
|
+
function parseFrontmatter(content) {
|
|
304
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
305
|
+
if (!match) return {};
|
|
306
|
+
const result = {};
|
|
307
|
+
for (const line of match[1].split("\n")) {
|
|
308
|
+
const idx = line.indexOf(":");
|
|
309
|
+
if (idx > 0) result[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
|
|
310
|
+
}
|
|
311
|
+
return result;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Build a single skill index entry from a staged skill directory.
|
|
316
|
+
* @param {string} skillDir - Path containing SKILL.md and optional extras
|
|
317
|
+
* @param {string} name - Skill name for the manifest
|
|
318
|
+
* @returns {Promise<{name: string, description: string, files: string[]}>}
|
|
319
|
+
*/
|
|
320
|
+
async function buildSkillEntry(skillDir, name) {
|
|
321
|
+
const skillMd = await readFile(join(skillDir, "SKILL.md"), "utf-8");
|
|
322
|
+
const fm = parseFrontmatter(skillMd);
|
|
323
|
+
const files = await collectFileList(skillDir);
|
|
324
|
+
return { description: fm.description || "", files, name };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Write a `npx skills`-compatible repository for a single pack.
|
|
329
|
+
*
|
|
330
|
+
* Each pack becomes its own skill repository at
|
|
331
|
+
* `packs/{name}/.well-known/skills/` so that individual skills
|
|
332
|
+
* within the pack can be discovered and installed independently:
|
|
333
|
+
*
|
|
334
|
+
* npx skills add domain.org/packs/se-platform --all
|
|
335
|
+
* npx skills add domain.org/packs/se-platform -s architecture-design
|
|
336
|
+
*
|
|
337
|
+
* @param {string} packsOutputDir - The `packs/` output directory
|
|
338
|
+
* @param {string} packStagingDir - Staging directory for this pack
|
|
339
|
+
* @param {string} packName - Pack name (e.g. "se-platform")
|
|
340
|
+
* @returns {Promise<Array<{name: string, description: string, files: string[]}>>}
|
|
341
|
+
* The skill entries written, for use in the aggregate manifest.
|
|
242
342
|
*/
|
|
243
|
-
async function
|
|
244
|
-
const wellKnownDir = join(
|
|
343
|
+
async function writePackRepository(packsOutputDir, packStagingDir, packName) {
|
|
344
|
+
const wellKnownDir = join(packsOutputDir, packName, ".well-known", "skills");
|
|
245
345
|
await mkdir(wellKnownDir, { recursive: true });
|
|
346
|
+
|
|
347
|
+
// Discover individual skills from the staged pack's .claude/skills/
|
|
348
|
+
const skillsSrcDir = join(packStagingDir, ".claude", "skills");
|
|
349
|
+
const skillDirs = (
|
|
350
|
+
await readdir(skillsSrcDir, { withFileTypes: true })
|
|
351
|
+
).filter((e) => e.isDirectory());
|
|
352
|
+
|
|
353
|
+
const entries = [];
|
|
354
|
+
for (const dir of skillDirs) {
|
|
355
|
+
const src = join(skillsSrcDir, dir.name);
|
|
356
|
+
const dest = join(wellKnownDir, dir.name);
|
|
357
|
+
await cp(src, dest, { recursive: true });
|
|
358
|
+
entries.push(await buildSkillEntry(dest, dir.name));
|
|
359
|
+
}
|
|
360
|
+
|
|
246
361
|
const manifest = {
|
|
247
362
|
$schema: "https://schemas.agentskills.io/discovery/0.2.0/schema.json",
|
|
248
|
-
skills:
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
363
|
+
skills: entries,
|
|
364
|
+
};
|
|
365
|
+
await writeFile(
|
|
366
|
+
join(wellKnownDir, "index.json"),
|
|
367
|
+
stringifySorted(manifest),
|
|
368
|
+
"utf-8",
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
return entries;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Write an aggregate `npx skills` repository at `packs/` that lists every
|
|
376
|
+
* unique skill across all packs. Skills with the same name produce identical
|
|
377
|
+
* SKILL.md content regardless of discipline/track, so we deduplicate by name
|
|
378
|
+
* and write one copy.
|
|
379
|
+
*
|
|
380
|
+
* npx skills add domain.org/packs --list
|
|
381
|
+
*
|
|
382
|
+
* @param {string} packsOutputDir
|
|
383
|
+
* @param {Array<{packName: string, entries: Array}>} allPackEntries
|
|
384
|
+
*/
|
|
385
|
+
async function writeAggregateRepository(packsOutputDir, allPackEntries) {
|
|
386
|
+
const wellKnownDir = join(packsOutputDir, ".well-known", "skills");
|
|
387
|
+
await mkdir(wellKnownDir, { recursive: true });
|
|
388
|
+
|
|
389
|
+
// Deduplicate: first occurrence of each skill name wins (content is identical)
|
|
390
|
+
const seen = new Map();
|
|
391
|
+
for (const { packName, entries } of allPackEntries) {
|
|
392
|
+
for (const entry of entries) {
|
|
393
|
+
if (seen.has(entry.name)) continue;
|
|
394
|
+
seen.set(entry.name, { packName, entry });
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const skills = [];
|
|
399
|
+
for (const [, { packName, entry }] of seen) {
|
|
400
|
+
const dest = join(wellKnownDir, entry.name);
|
|
401
|
+
const src = join(
|
|
402
|
+
packsOutputDir,
|
|
403
|
+
packName,
|
|
404
|
+
".well-known",
|
|
405
|
+
"skills",
|
|
406
|
+
entry.name,
|
|
407
|
+
);
|
|
408
|
+
await cp(src, dest, { recursive: true });
|
|
409
|
+
skills.push(entry);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const manifest = {
|
|
413
|
+
$schema: "https://schemas.agentskills.io/discovery/0.2.0/schema.json",
|
|
414
|
+
skills,
|
|
256
415
|
};
|
|
257
416
|
await writeFile(
|
|
258
417
|
join(wellKnownDir, "index.json"),
|
|
@@ -382,10 +541,25 @@ export async function generatePacks({
|
|
|
382
541
|
console.log(` ✓ packs/${agentName}.tar.gz`);
|
|
383
542
|
}
|
|
384
543
|
|
|
385
|
-
|
|
544
|
+
// Write per-pack skill repositories (one per discipline/track combination)
|
|
545
|
+
const allPackEntries = [];
|
|
546
|
+
for (const pack of packs) {
|
|
547
|
+
const entries = await writePackRepository(
|
|
548
|
+
packsDir,
|
|
549
|
+
join(stagingDir, pack.name),
|
|
550
|
+
pack.name,
|
|
551
|
+
);
|
|
552
|
+
allPackEntries.push({ packName: pack.name, entries });
|
|
553
|
+
console.log(
|
|
554
|
+
` ✓ packs/${pack.name}/.well-known/skills/ (${entries.length} skills)`,
|
|
555
|
+
);
|
|
556
|
+
}
|
|
386
557
|
|
|
387
|
-
|
|
388
|
-
|
|
558
|
+
// Write aggregate repository at packs/ level
|
|
559
|
+
await writeAggregateRepository(packsDir, allPackEntries);
|
|
560
|
+
console.log(" ✓ packs/.well-known/skills/index.json (aggregate)");
|
|
561
|
+
|
|
562
|
+
await rm(stagingDir, { recursive: true, force: true });
|
|
389
563
|
|
|
390
564
|
await writeApmManifest(outputDir, packs, version, frameworkTitle);
|
|
391
565
|
console.log(" ✓ apm.yml");
|
package/src/commands/build.js
CHANGED
|
@@ -222,10 +222,10 @@ ${framework.emojiIcon} Generating ${framework.title} static site...
|
|
|
222
222
|
✅ Site generated successfully!
|
|
223
223
|
|
|
224
224
|
Output: ${outputDir}
|
|
225
|
-
${siteUrl ? `\nDistribution:\n ${outputDir}/bundle.tar.gz\n ${outputDir}/install.sh\n ${outputDir}/packs/ (agent/skill packs)\n ${outputDir}/.well-known/
|
|
225
|
+
${siteUrl ? `\nDistribution:\n ${outputDir}/bundle.tar.gz\n ${outputDir}/install.sh\n ${outputDir}/packs/ (agent/skill packs)\n ${outputDir}/packs/{name}/.well-known/skills/ (per-pack skill repositories)\n ${outputDir}/packs/.well-known/skills/ (aggregate skill repository)\n ${outputDir}/apm.yml\n` : ""}
|
|
226
226
|
To serve locally:
|
|
227
227
|
cd ${relative(process.cwd(), outputDir) || "."}
|
|
228
|
-
|
|
228
|
+
npx serve .
|
|
229
229
|
`);
|
|
230
230
|
}
|
|
231
231
|
|
|
@@ -12,6 +12,12 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { capitalize } from "../formatters/shared.js";
|
|
15
|
+
import {
|
|
16
|
+
formatSuccess,
|
|
17
|
+
formatWarning,
|
|
18
|
+
formatError,
|
|
19
|
+
formatBullet,
|
|
20
|
+
} from "@forwardimpact/libcli";
|
|
15
21
|
|
|
16
22
|
/**
|
|
17
23
|
* Create an entity command with standard behavior
|
|
@@ -88,8 +94,15 @@ export function createEntityCommand({
|
|
|
88
94
|
*/
|
|
89
95
|
function handleValidate({ data, _entityName, pluralName, validate }) {
|
|
90
96
|
if (!validate) {
|
|
91
|
-
|
|
92
|
-
|
|
97
|
+
process.stdout.write(
|
|
98
|
+
formatBullet(`No specific validation for ${pluralName}.`, 0) + "\n",
|
|
99
|
+
);
|
|
100
|
+
process.stdout.write(
|
|
101
|
+
formatBullet(
|
|
102
|
+
"Run 'npx fit-pathway --validate' for full data validation.",
|
|
103
|
+
0,
|
|
104
|
+
) + "\n",
|
|
105
|
+
);
|
|
93
106
|
return;
|
|
94
107
|
}
|
|
95
108
|
|
|
@@ -97,21 +110,23 @@ function handleValidate({ data, _entityName, pluralName, validate }) {
|
|
|
97
110
|
const { errors = [], warnings = [] } = result;
|
|
98
111
|
|
|
99
112
|
if (errors.length === 0 && warnings.length === 0) {
|
|
100
|
-
|
|
113
|
+
process.stdout.write(
|
|
114
|
+
formatSuccess(`${capitalize(pluralName)} validation passed`) + "\n",
|
|
115
|
+
);
|
|
101
116
|
return;
|
|
102
117
|
}
|
|
103
118
|
|
|
104
119
|
if (warnings.length > 0) {
|
|
105
|
-
|
|
120
|
+
process.stdout.write(formatWarning(`${warnings.length} warning(s)`) + "\n");
|
|
106
121
|
for (const w of warnings) {
|
|
107
|
-
|
|
122
|
+
process.stdout.write(formatBullet(w, 1) + "\n");
|
|
108
123
|
}
|
|
109
124
|
}
|
|
110
125
|
|
|
111
126
|
if (errors.length > 0) {
|
|
112
|
-
|
|
127
|
+
process.stderr.write(formatError(`${errors.length} error(s)`) + "\n");
|
|
113
128
|
for (const e of errors) {
|
|
114
|
-
|
|
129
|
+
process.stderr.write(formatBullet(e, 1) + "\n");
|
|
115
130
|
}
|
|
116
131
|
process.exit(1);
|
|
117
132
|
}
|
|
@@ -134,15 +149,21 @@ function handleDetail({
|
|
|
134
149
|
const entity = findEntity(data, id);
|
|
135
150
|
|
|
136
151
|
if (!entity) {
|
|
137
|
-
|
|
138
|
-
|
|
152
|
+
process.stderr.write(
|
|
153
|
+
formatError(`${capitalize(entityName)} not found: ${id}`) + "\n",
|
|
154
|
+
);
|
|
155
|
+
process.stderr.write(
|
|
156
|
+
`Available: ${data[pluralName].map((e) => e.id).join(", ")}\n`,
|
|
157
|
+
);
|
|
139
158
|
process.exit(1);
|
|
140
159
|
}
|
|
141
160
|
|
|
142
161
|
const view = presentDetail(entity, data, options);
|
|
143
162
|
|
|
144
163
|
if (!view) {
|
|
145
|
-
|
|
164
|
+
process.stderr.write(
|
|
165
|
+
formatError(`Failed to present ${entityName}: ${id}`) + "\n",
|
|
166
|
+
);
|
|
146
167
|
process.exit(1);
|
|
147
168
|
}
|
|
148
169
|
|
|
@@ -178,9 +199,11 @@ export function createCompositeCommand({
|
|
|
178
199
|
return async function runCommand({ data, args, options }) {
|
|
179
200
|
if (args.length < requiredArgs.length) {
|
|
180
201
|
const argsList = requiredArgs.map((arg) => `<${arg}>`).join(" ");
|
|
181
|
-
|
|
202
|
+
process.stderr.write(
|
|
203
|
+
formatError(`Usage: npx fit-pathway ${commandName} ${argsList}`) + "\n",
|
|
204
|
+
);
|
|
182
205
|
if (usageExample) {
|
|
183
|
-
|
|
206
|
+
process.stderr.write(`Example: ${usageExample}\n`);
|
|
184
207
|
}
|
|
185
208
|
process.exit(1);
|
|
186
209
|
}
|
|
@@ -189,14 +212,16 @@ export function createCompositeCommand({
|
|
|
189
212
|
const validationError = validateEntities(entities, data, options);
|
|
190
213
|
|
|
191
214
|
if (validationError) {
|
|
192
|
-
|
|
215
|
+
process.stderr.write(formatError(validationError) + "\n");
|
|
193
216
|
process.exit(1);
|
|
194
217
|
}
|
|
195
218
|
|
|
196
219
|
const view = presenter(entities, data, options);
|
|
197
220
|
|
|
198
221
|
if (!view) {
|
|
199
|
-
|
|
222
|
+
process.stderr.write(
|
|
223
|
+
formatError(`Failed to generate ${commandName} output.`) + "\n",
|
|
224
|
+
);
|
|
200
225
|
process.exit(1);
|
|
201
226
|
}
|
|
202
227
|
|
|
@@ -4,15 +4,20 @@
|
|
|
4
4
|
* Handles discipline summary, listing, and detail display in the terminal.
|
|
5
5
|
*
|
|
6
6
|
* Usage:
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* npx fit-pathway discipline # Summary with stats
|
|
8
|
+
* npx fit-pathway discipline --list # IDs only (for piping)
|
|
9
|
+
* npx fit-pathway discipline <id> # Detail view
|
|
10
|
+
* npx fit-pathway discipline --validate # Validation checks
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { createEntityCommand } from "./command-factory.js";
|
|
14
14
|
import { disciplineToMarkdown } from "../formatters/discipline/markdown.js";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
formatTable,
|
|
17
|
+
formatHeader,
|
|
18
|
+
formatSubheader,
|
|
19
|
+
formatBullet,
|
|
20
|
+
} from "@forwardimpact/libcli";
|
|
16
21
|
|
|
17
22
|
/**
|
|
18
23
|
* Format discipline list item for --list output
|
|
@@ -32,7 +37,7 @@ function formatListItem(discipline) {
|
|
|
32
37
|
* @param {Array} disciplines - Raw discipline entities
|
|
33
38
|
*/
|
|
34
39
|
function formatSummary(disciplines) {
|
|
35
|
-
|
|
40
|
+
process.stdout.write("\n" + formatHeader("\u{1F4CB} Disciplines") + "\n\n");
|
|
36
41
|
|
|
37
42
|
const rows = disciplines.map((d) => {
|
|
38
43
|
const type = d.isProfessional ? "Professional" : "Management";
|
|
@@ -41,10 +46,19 @@ function formatSummary(disciplines) {
|
|
|
41
46
|
return [d.id, d.specialization || d.id, type, trackStr];
|
|
42
47
|
});
|
|
43
48
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
49
|
+
process.stdout.write(
|
|
50
|
+
formatTable(["ID", "Specialization", "Type", "Tracks"], rows) + "\n",
|
|
51
|
+
);
|
|
52
|
+
process.stdout.write(
|
|
53
|
+
"\n" + formatSubheader(`Total: ${disciplines.length} disciplines`) + "\n\n",
|
|
54
|
+
);
|
|
55
|
+
process.stdout.write(
|
|
56
|
+
formatBullet("Run 'npx fit-pathway discipline --list' for IDs and names") +
|
|
57
|
+
"\n",
|
|
58
|
+
);
|
|
59
|
+
process.stdout.write(
|
|
60
|
+
formatBullet("Run 'npx fit-pathway discipline <id>' for details") + "\n\n",
|
|
61
|
+
);
|
|
48
62
|
}
|
|
49
63
|
|
|
50
64
|
/**
|
package/src/commands/driver.js
CHANGED
|
@@ -4,20 +4,20 @@
|
|
|
4
4
|
* Handles driver summary, listing, and detail display in the terminal.
|
|
5
5
|
*
|
|
6
6
|
* Usage:
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* npx fit-pathway driver # Summary with stats
|
|
8
|
+
* npx fit-pathway driver --list # IDs only (for piping)
|
|
9
|
+
* npx fit-pathway driver <id> # Detail view
|
|
10
|
+
* npx fit-pathway driver --validate # Validation checks
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { createEntityCommand } from "./command-factory.js";
|
|
14
14
|
import { prepareDriverDetail } from "../formatters/driver/shared.js";
|
|
15
|
-
import { formatTable } from "../lib/cli-output.js";
|
|
16
15
|
import {
|
|
16
|
+
formatTable,
|
|
17
17
|
formatHeader,
|
|
18
18
|
formatSubheader,
|
|
19
19
|
formatBullet,
|
|
20
|
-
} from "
|
|
20
|
+
} from "@forwardimpact/libcli";
|
|
21
21
|
import { getConceptEmoji } from "@forwardimpact/map/levels";
|
|
22
22
|
|
|
23
23
|
/**
|
|
@@ -38,7 +38,7 @@ function formatSummary(drivers, data) {
|
|
|
38
38
|
const { skills, behaviours, framework } = data;
|
|
39
39
|
const emoji = framework ? getConceptEmoji(framework, "driver") : "🎯";
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
process.stdout.write("\n" + formatHeader(`${emoji} Drivers`) + "\n\n");
|
|
42
42
|
|
|
43
43
|
const rows = drivers.map((d) => {
|
|
44
44
|
const contributingSkills = skills.filter((s) =>
|
|
@@ -50,10 +50,19 @@ function formatSummary(drivers, data) {
|
|
|
50
50
|
return [d.id, d.name, contributingSkills, contributingBehaviours];
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
process.stdout.write(
|
|
54
|
+
formatTable(["ID", "Name", "Skills", "Behaviours"], rows) + "\n",
|
|
55
|
+
);
|
|
56
|
+
process.stdout.write(
|
|
57
|
+
"\n" + formatSubheader(`Total: ${drivers.length} drivers`) + "\n\n",
|
|
58
|
+
);
|
|
59
|
+
process.stdout.write(
|
|
60
|
+
formatBullet("Run 'npx fit-pathway driver --list' for IDs and names") +
|
|
61
|
+
"\n",
|
|
62
|
+
);
|
|
63
|
+
process.stdout.write(
|
|
64
|
+
formatBullet("Run 'npx fit-pathway driver <id>' for details") + "\n\n",
|
|
65
|
+
);
|
|
57
66
|
}
|
|
58
67
|
|
|
59
68
|
/**
|
|
@@ -66,25 +75,25 @@ function formatDetail(viewAndContext, framework) {
|
|
|
66
75
|
const view = prepareDriverDetail(driver, { skills, behaviours });
|
|
67
76
|
const emoji = framework ? getConceptEmoji(framework, "driver") : "🎯";
|
|
68
77
|
|
|
69
|
-
|
|
70
|
-
|
|
78
|
+
process.stdout.write("\n" + formatHeader(`${emoji} ${view.name}`) + "\n\n");
|
|
79
|
+
process.stdout.write(view.description + "\n\n");
|
|
71
80
|
|
|
72
81
|
// Contributing skills
|
|
73
82
|
if (view.contributingSkills.length > 0) {
|
|
74
|
-
|
|
83
|
+
process.stdout.write(formatSubheader("Contributing Skills") + "\n\n");
|
|
75
84
|
for (const s of view.contributingSkills) {
|
|
76
|
-
|
|
85
|
+
process.stdout.write(formatBullet(s.name, 1) + "\n");
|
|
77
86
|
}
|
|
78
|
-
|
|
87
|
+
process.stdout.write("\n");
|
|
79
88
|
}
|
|
80
89
|
|
|
81
90
|
// Contributing behaviours
|
|
82
91
|
if (view.contributingBehaviours.length > 0) {
|
|
83
|
-
|
|
92
|
+
process.stdout.write(formatSubheader("Contributing Behaviours") + "\n\n");
|
|
84
93
|
for (const b of view.contributingBehaviours) {
|
|
85
|
-
|
|
94
|
+
process.stdout.write(formatBullet(b.name, 1) + "\n");
|
|
86
95
|
}
|
|
87
|
-
|
|
96
|
+
process.stdout.write("\n");
|
|
88
97
|
}
|
|
89
98
|
}
|
|
90
99
|
|
package/src/commands/index.js
CHANGED
|
@@ -17,6 +17,5 @@ export { runInterviewCommand } from "./interview.js";
|
|
|
17
17
|
export { runProgressCommand } from "./progress.js";
|
|
18
18
|
export { runQuestionsCommand } from "./questions.js";
|
|
19
19
|
export { runServeCommand } from "./serve.js";
|
|
20
|
-
export { runInitCommand } from "./init.js";
|
|
21
20
|
export { runSiteCommand } from "./site.js";
|
|
22
21
|
export { runUpdateCommand } from "./update.js";
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* Generates and displays interview questions in the terminal.
|
|
5
5
|
*
|
|
6
6
|
* Usage:
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* npx fit-pathway interview <discipline> <level> # All interview types
|
|
8
|
+
* npx fit-pathway interview <discipline> <level> --track=<track> # With track
|
|
9
|
+
* npx fit-pathway interview <discipline> <level> --track=<track> --type=mission # Single type
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { createCompositeCommand } from "./command-factory.js";
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
INTERVIEW_TYPES,
|
|
16
16
|
} from "../formatters/interview/shared.js";
|
|
17
17
|
import { interviewToMarkdown } from "../formatters/interview/markdown.js";
|
|
18
|
+
import { formatError, horizontalRule } from "@forwardimpact/libcli";
|
|
18
19
|
|
|
19
20
|
const VALID_TYPES = Object.keys(INTERVIEW_TYPES);
|
|
20
21
|
|
|
@@ -24,7 +25,9 @@ const VALID_TYPES = Object.keys(INTERVIEW_TYPES);
|
|
|
24
25
|
* @param {Object} options - Options including framework
|
|
25
26
|
*/
|
|
26
27
|
function formatInterview(view, options) {
|
|
27
|
-
|
|
28
|
+
process.stdout.write(
|
|
29
|
+
interviewToMarkdown(view, { framework: options.framework }) + "\n",
|
|
30
|
+
);
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
/**
|
|
@@ -35,10 +38,10 @@ function formatInterview(view, options) {
|
|
|
35
38
|
function formatAllInterviews(views, options) {
|
|
36
39
|
for (let i = 0; i < views.length; i++) {
|
|
37
40
|
if (i > 0) {
|
|
38
|
-
|
|
41
|
+
process.stdout.write("\n" + horizontalRule(80) + "\n\n");
|
|
39
42
|
}
|
|
40
|
-
|
|
41
|
-
interviewToMarkdown(views[i], { framework: options.framework }),
|
|
43
|
+
process.stdout.write(
|
|
44
|
+
interviewToMarkdown(views[i], { framework: options.framework }) + "\n",
|
|
42
45
|
);
|
|
43
46
|
}
|
|
44
47
|
}
|
|
@@ -50,8 +53,10 @@ export const runInterviewCommand = createCompositeCommand({
|
|
|
50
53
|
const interviewType = options.type === "full" ? null : options.type;
|
|
51
54
|
|
|
52
55
|
if (interviewType && !INTERVIEW_TYPES[interviewType]) {
|
|
53
|
-
|
|
54
|
-
|
|
56
|
+
process.stderr.write(
|
|
57
|
+
formatError(`Unknown interview type: ${interviewType}`) + "\n",
|
|
58
|
+
);
|
|
59
|
+
process.stderr.write(`Available types: ${VALID_TYPES.join(", ")}\n`);
|
|
55
60
|
process.exit(1);
|
|
56
61
|
}
|
|
57
62
|
|
|
@@ -109,5 +114,5 @@ export const runInterviewCommand = createCompositeCommand({
|
|
|
109
114
|
}
|
|
110
115
|
},
|
|
111
116
|
usageExample:
|
|
112
|
-
"
|
|
117
|
+
"npx fit-pathway interview software_engineering J090 --track=platform --type=mission",
|
|
113
118
|
});
|