@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.
Files changed (40) hide show
  1. package/bin/fit-pathway.js +117 -325
  2. package/package.json +2 -2
  3. package/src/commands/agent-io.js +1 -1
  4. package/src/commands/agent-list.js +164 -0
  5. package/src/commands/agent.js +83 -184
  6. package/src/commands/behaviour.js +22 -10
  7. package/src/commands/build-packs.js +208 -34
  8. package/src/commands/build.js +2 -2
  9. package/src/commands/command-factory.js +39 -14
  10. package/src/commands/discipline.js +24 -10
  11. package/src/commands/driver.js +28 -19
  12. package/src/commands/index.js +0 -1
  13. package/src/commands/interview.js +15 -10
  14. package/src/commands/job.js +110 -62
  15. package/src/commands/level.js +23 -11
  16. package/src/commands/progress.js +12 -7
  17. package/src/commands/questions.js +32 -14
  18. package/src/commands/skill.js +36 -18
  19. package/src/commands/stage.js +37 -27
  20. package/src/commands/tool.js +29 -19
  21. package/src/commands/track.js +23 -10
  22. package/src/formatters/questions/yaml.js +1 -1
  23. package/src/index.html +1 -1
  24. package/src/lib/cli-command.js +33 -33
  25. package/src/lib/cli-output.js +9 -189
  26. package/src/pages/agent-builder-install.js +6 -5
  27. package/src/commands/init.js +0 -64
  28. package/starter/behaviours/systems_thinking.yaml +0 -32
  29. package/starter/capabilities/delivery.yaml +0 -105
  30. package/starter/capabilities/reliability.yaml +0 -72
  31. package/starter/disciplines/software_engineering.yaml +0 -46
  32. package/starter/drivers.yaml +0 -10
  33. package/starter/framework.yaml +0 -49
  34. package/starter/levels.yaml +0 -39
  35. package/starter/questions/behaviours/.gitkeep +0 -0
  36. package/starter/questions/capabilities/.gitkeep +0 -0
  37. package/starter/questions/skills/.gitkeep +0 -0
  38. package/starter/stages.yaml +0 -21
  39. package/starter/tracks/forward_deployed.yaml +0 -33
  40. 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
- * and two discovery manifests (`.well-known/agent-skills/index.json` for
6
- * `npx skills` and `apm.yml` for Microsoft APM). See
7
- * specs/320-pathway-ecosystem-distribution for context.
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
- execFileSync("tar", [
221
- "-czf",
222
- archivePath,
223
- "--sort=name",
224
- "--mtime=1970-01-01",
225
- "--owner=0",
226
- "--group=0",
227
- "--numeric-owner",
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
- * Write the `npx skills` discovery manifest.
239
- * @param {string} outputDir
240
- * @param {Array<{name: string, description: string, url: string, digest: string}>} packs
241
- * @param {string} version
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 writeSkillsManifest(outputDir, packs, version) {
244
- const wellKnownDir = join(outputDir, ".well-known", "agent-skills");
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: packs.map((pack) => ({
249
- description: pack.description,
250
- digest: pack.digest,
251
- name: pack.name,
252
- type: "archive",
253
- url: pack.url,
254
- version,
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
- await rm(stagingDir, { recursive: true, force: true });
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
- await writeSkillsManifest(outputDir, packs, version);
388
- console.log(" ✓ .well-known/agent-skills/index.json");
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");
@@ -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/agent-skills/index.json\n ${outputDir}/apm.yml\n` : ""}
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
- bunx serve .
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
- console.log(`No specific validation for ${pluralName}.`);
92
- console.log(`Run 'bunx pathway --validate' for full data validation.`);
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
- console.log(`✅ ${capitalize(pluralName)} validation passed`);
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
- console.log(`⚠️ Warnings:`);
120
+ process.stdout.write(formatWarning(`${warnings.length} warning(s)`) + "\n");
106
121
  for (const w of warnings) {
107
- console.log(` - ${w}`);
122
+ process.stdout.write(formatBullet(w, 1) + "\n");
108
123
  }
109
124
  }
110
125
 
111
126
  if (errors.length > 0) {
112
- console.log(`❌ Errors:`);
127
+ process.stderr.write(formatError(`${errors.length} error(s)`) + "\n");
113
128
  for (const e of errors) {
114
- console.log(` - ${e}`);
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
- console.error(`${capitalize(entityName)} not found: ${id}`);
138
- console.error(`Available: ${data[pluralName].map((e) => e.id).join(", ")}`);
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
- console.error(`Failed to present ${entityName}: ${id}`);
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
- console.error(`Usage: bunx pathway ${commandName} ${argsList}`);
202
+ process.stderr.write(
203
+ formatError(`Usage: npx fit-pathway ${commandName} ${argsList}`) + "\n",
204
+ );
182
205
  if (usageExample) {
183
- console.error(`Example: ${usageExample}`);
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
- console.error(validationError);
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
- console.error(`Failed to generate ${commandName} output.`);
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
- * bunx pathway discipline # Summary with stats
8
- * bunx pathway discipline --list # IDs only (for piping)
9
- * bunx pathway discipline <id> # Detail view
10
- * bunx pathway discipline --validate # Validation checks
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 { formatTable } from "../lib/cli-output.js";
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
- console.log(`\n📋 Disciplines\n`);
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
- console.log(formatTable(["ID", "Specialization", "Type", "Tracks"], rows));
45
- console.log(`\nTotal: ${disciplines.length} disciplines`);
46
- console.log(`\nRun 'bunx pathway discipline --list' for IDs and names`);
47
- console.log(`Run 'bunx pathway discipline <id>' for details\n`);
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
  /**
@@ -4,20 +4,20 @@
4
4
  * Handles driver summary, listing, and detail display in the terminal.
5
5
  *
6
6
  * Usage:
7
- * bunx pathway driver # Summary with stats
8
- * bunx pathway driver --list # IDs only (for piping)
9
- * bunx pathway driver <id> # Detail view
10
- * bunx pathway driver --validate # Validation checks
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 "../lib/cli-output.js";
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
- console.log(`\n${emoji} Drivers\n`);
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
- console.log(formatTable(["ID", "Name", "Skills", "Behaviours"], rows));
54
- console.log(`\nTotal: ${drivers.length} drivers`);
55
- console.log(`\nRun 'bunx pathway driver --list' for IDs and names`);
56
- console.log(`Run 'bunx pathway driver <id>' for details\n`);
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
- console.log(formatHeader(`\n${emoji} ${view.name}\n`));
70
- console.log(`${view.description}\n`);
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
- console.log(formatSubheader("Contributing Skills\n"));
83
+ process.stdout.write(formatSubheader("Contributing Skills") + "\n\n");
75
84
  for (const s of view.contributingSkills) {
76
- console.log(formatBullet(s.name, 1));
85
+ process.stdout.write(formatBullet(s.name, 1) + "\n");
77
86
  }
78
- console.log();
87
+ process.stdout.write("\n");
79
88
  }
80
89
 
81
90
  // Contributing behaviours
82
91
  if (view.contributingBehaviours.length > 0) {
83
- console.log(formatSubheader("Contributing Behaviours\n"));
92
+ process.stdout.write(formatSubheader("Contributing Behaviours") + "\n\n");
84
93
  for (const b of view.contributingBehaviours) {
85
- console.log(formatBullet(b.name, 1));
94
+ process.stdout.write(formatBullet(b.name, 1) + "\n");
86
95
  }
87
- console.log();
96
+ process.stdout.write("\n");
88
97
  }
89
98
  }
90
99
 
@@ -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
- * bunx fit-pathway interview <discipline> <level> # All interview types
8
- * bunx fit-pathway interview <discipline> <level> --track=<track> # With track
9
- * bunx fit-pathway interview <discipline> <level> --track=<track> --type=mission # Single type
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
- console.log(interviewToMarkdown(view, { framework: options.framework }));
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
- console.log("\n" + "─".repeat(80) + "\n");
41
+ process.stdout.write("\n" + horizontalRule(80) + "\n\n");
39
42
  }
40
- console.log(
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
- console.error(`Unknown interview type: ${interviewType}`);
54
- console.error(`Available types: ${VALID_TYPES.join(", ")}`);
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
- "bunx fit-pathway interview software_engineering J090 --track=platform --type=mission",
117
+ "npx fit-pathway interview software_engineering J090 --track=platform --type=mission",
113
118
  });