@forwardimpact/pathway 0.25.24 → 0.25.26

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.
@@ -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 'npx fit-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: npx fit-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
 
@@ -12,7 +12,12 @@
12
12
 
13
13
  import { createEntityCommand } from "./command-factory.js";
14
14
  import { disciplineToMarkdown } from "../formatters/discipline/markdown.js";
15
- import { formatTable } from "@forwardimpact/libcli";
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 'npx fit-pathway discipline --list' for IDs and names`);
47
- console.log(`Run 'npx fit-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
  /**
@@ -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 'npx fit-pathway driver --list' for IDs and names`);
56
- console.log(`Run 'npx fit-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
 
@@ -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
 
@@ -20,7 +20,13 @@ import {
20
20
  generateJobTitle,
21
21
  generateAllJobs,
22
22
  } from "@forwardimpact/libskill/derivation";
23
- import { formatTable } from "@forwardimpact/libcli";
23
+ import {
24
+ formatTable,
25
+ formatHeader,
26
+ formatSubheader,
27
+ formatBullet,
28
+ formatError,
29
+ } from "@forwardimpact/libcli";
24
30
  import {
25
31
  deriveChecklist,
26
32
  formatChecklistMarkdown,
@@ -66,7 +72,9 @@ function printJobList(filteredJobs) {
66
72
  */
67
73
  function printJobSummary(filteredJobs, options) {
68
74
  const trackLabel = options.track ? ` — ${options.track}` : "";
69
- console.log(`\n💼 Jobs${trackLabel}\n`);
75
+ process.stdout.write(
76
+ "\n" + formatHeader(`\u{1F4BC} Jobs${trackLabel}`) + "\n\n",
77
+ );
70
78
 
71
79
  const byDiscipline = {};
72
80
  for (const job of filteredJobs) {
@@ -91,15 +99,24 @@ function printJobSummary(filteredJobs, options) {
91
99
  info.count,
92
100
  info.tracks.size > 0 ? [...info.tracks].join(", ") : "—",
93
101
  ]);
94
- console.log(
95
- formatTable(["ID", "Specialization", "Type", "Jobs", "Tracks"], rows),
102
+ process.stdout.write(
103
+ formatTable(["ID", "Specialization", "Type", "Jobs", "Tracks"], rows) +
104
+ "\n",
96
105
  );
97
- console.log(`\nTotal: ${filteredJobs.length} valid job combinations`);
98
- console.log(
99
- `\nRun 'npx fit-pathway job --list' for all combinations with titles`,
106
+ process.stdout.write(
107
+ "\n" +
108
+ formatSubheader(`Total: ${filteredJobs.length} valid job combinations`) +
109
+ "\n\n",
100
110
  );
101
- console.log(
102
- `Run 'npx fit-pathway job <discipline> <level> [--track=<track>]' for details\n`,
111
+ process.stdout.write(
112
+ formatBullet(
113
+ "Run 'npx fit-pathway job --list' for all combinations with titles",
114
+ ) + "\n",
115
+ );
116
+ process.stdout.write(
117
+ formatBullet(
118
+ "Run 'npx fit-pathway job <discipline> <level> [--track=<track>]' for details",
119
+ ) + "\n\n",
103
120
  );
104
121
  }
105
122
 
@@ -112,22 +129,28 @@ function handleSingleArg(arg, data) {
112
129
  const isLevel = data.levels.some((g) => g.id === arg);
113
130
  const isTrack = data.tracks.some((t) => t.id === arg);
114
131
  if (isLevel) {
115
- console.error(
116
- `Missing discipline. Usage: npx fit-pathway job <discipline> ${arg} [--track=<track>]`,
132
+ process.stderr.write(
133
+ formatError(
134
+ `Missing discipline. Usage: npx fit-pathway job <discipline> ${arg} [--track=<track>]`,
135
+ ) + "\n",
117
136
  );
118
- console.error(
119
- `Disciplines: ${data.disciplines.map((d) => d.id).join(", ")}`,
137
+ process.stderr.write(
138
+ `Disciplines: ${data.disciplines.map((d) => d.id).join(", ")}\n`,
120
139
  );
121
140
  } else if (isTrack) {
122
- console.error(`Track must be passed as a flag: --track=${arg}`);
123
- console.error(
124
- `Usage: npx fit-pathway job <discipline> <level> --track=${arg}`,
141
+ process.stderr.write(
142
+ formatError(`Track must be passed as a flag: --track=${arg}`) + "\n",
143
+ );
144
+ process.stderr.write(
145
+ `Usage: npx fit-pathway job <discipline> <level> --track=${arg}\n`,
125
146
  );
126
147
  } else {
127
- console.error(
128
- "Usage: npx fit-pathway job <discipline> <level> [--track=<track>]",
148
+ process.stderr.write(
149
+ formatError(
150
+ "Usage: npx fit-pathway job <discipline> <level> [--track=<track>]",
151
+ ) + "\n",
129
152
  );
130
- console.error(" npx fit-pathway job --list");
153
+ process.stderr.write(" npx fit-pathway job --list\n");
131
154
  }
132
155
  process.exit(1);
133
156
  }
@@ -150,14 +173,18 @@ function resolveJobEntities(data, args, options) {
150
173
  const maybeLevel = data.levels.find((g) => g.id === args[0]);
151
174
  const maybeDiscipline = data.disciplines.find((d) => d.id === args[1]);
152
175
  if (maybeLevel && maybeDiscipline) {
153
- console.error(`Arguments are in the wrong order. Try:`);
154
- console.error(
155
- ` npx fit-pathway job ${args[1]} ${args[0]}${options.track ? ` --track=${options.track}` : ""}`,
176
+ process.stderr.write(
177
+ formatError("Arguments are in the wrong order. Try:") + "\n",
178
+ );
179
+ process.stderr.write(
180
+ ` npx fit-pathway job ${args[1]} ${args[0]}${options.track ? ` --track=${options.track}` : ""}\n`,
156
181
  );
157
182
  } else {
158
- console.error(`Discipline not found: ${args[0]}`);
159
- console.error(
160
- `Available: ${data.disciplines.map((d) => d.id).join(", ")}`,
183
+ process.stderr.write(
184
+ formatError(`Discipline not found: ${args[0]}`) + "\n",
185
+ );
186
+ process.stderr.write(
187
+ `Available: ${data.disciplines.map((d) => d.id).join(", ")}\n`,
161
188
  );
162
189
  }
163
190
  process.exit(1);
@@ -166,23 +193,33 @@ function resolveJobEntities(data, args, options) {
166
193
  if (!level) {
167
194
  const isTrack = data.tracks.some((t) => t.id === args[1]);
168
195
  if (isTrack) {
169
- console.error(
170
- `Track must be passed as a flag, not a positional argument:`,
196
+ process.stderr.write(
197
+ formatError(
198
+ "Track must be passed as a flag, not a positional argument:",
199
+ ) + "\n",
200
+ );
201
+ process.stderr.write(
202
+ ` npx fit-pathway job ${args[0]} <level> --track=${args[1]}\n`,
171
203
  );
172
- console.error(
173
- ` npx fit-pathway job ${args[0]} <level> --track=${args[1]}`,
204
+ process.stderr.write(
205
+ `Levels: ${data.levels.map((g) => g.id).join(", ")}\n`,
174
206
  );
175
- console.error(`Levels: ${data.levels.map((g) => g.id).join(", ")}`);
176
207
  } else {
177
- console.error(`Level not found: ${args[1]}`);
178
- console.error(`Available: ${data.levels.map((g) => g.id).join(", ")}`);
208
+ process.stderr.write(formatError(`Level not found: ${args[1]}`) + "\n");
209
+ process.stderr.write(
210
+ `Available: ${data.levels.map((g) => g.id).join(", ")}\n`,
211
+ );
179
212
  }
180
213
  process.exit(1);
181
214
  }
182
215
 
183
216
  if (options.track && !track) {
184
- console.error(`Track not found: ${options.track}`);
185
- console.error(`Available: ${data.tracks.map((t) => t.id).join(", ")}`);
217
+ process.stderr.write(
218
+ formatError(`Track not found: ${options.track}`) + "\n",
219
+ );
220
+ process.stderr.write(
221
+ `Available: ${data.tracks.map((t) => t.id).join(", ")}\n`,
222
+ );
186
223
  process.exit(1);
187
224
  }
188
225
 
@@ -198,8 +235,8 @@ function resolveJobEntities(data, args, options) {
198
235
  function handleChecklist(view, data, stageId) {
199
236
  const validStages = data.stages.map((s) => s.id);
200
237
  if (!validStages.includes(stageId)) {
201
- console.error(`Invalid stage: ${stageId}`);
202
- console.error(`Available: ${validStages.join(", ")}`);
238
+ process.stderr.write(formatError(`Invalid stage: ${stageId}`) + "\n");
239
+ process.stderr.write(`Available: ${validStages.join(", ")}\n`);
203
240
  process.exit(1);
204
241
  }
205
242
 
@@ -211,21 +248,24 @@ function handleChecklist(view, data, stageId) {
211
248
  });
212
249
 
213
250
  if (readChecklist.length === 0 && confirmChecklist.length === 0) {
214
- console.log(`\nNo checklist items for ${stageId} stage\n`);
251
+ process.stdout.write(
252
+ "\n" +
253
+ formatSubheader(`No checklist items for ${stageId} stage`) +
254
+ "\n\n",
255
+ );
215
256
  return;
216
257
  }
217
258
 
259
+ // Markdown output (#/##) is intentional — this is consumed as markdown.
218
260
  const stageLabel = stageId.charAt(0).toUpperCase() + stageId.slice(1);
219
- console.log(`\n# ${view.title} — ${stageLabel} Stage Checklist\n`);
261
+ process.stdout.write(`\n# ${view.title} — ${stageLabel} Stage Checklist\n\n`);
220
262
  if (readChecklist.length > 0) {
221
- console.log("## Read-Then-Do\n");
222
- console.log(formatChecklistMarkdown(readChecklist));
223
- console.log("");
263
+ process.stdout.write("## Read-Then-Do\n\n");
264
+ process.stdout.write(formatChecklistMarkdown(readChecklist) + "\n\n");
224
265
  }
225
266
  if (confirmChecklist.length > 0) {
226
- console.log("## Do-Then-Confirm\n");
227
- console.log(formatChecklistMarkdown(confirmChecklist));
228
- console.log("");
267
+ process.stdout.write("## Do-Then-Confirm\n\n");
268
+ process.stdout.write(formatChecklistMarkdown(confirmChecklist) + "\n\n");
229
269
  }
230
270
  }
231
271
 
@@ -247,16 +287,22 @@ function validateTrackFilter(filteredJobs, data, options) {
247
287
  if (!options.track || filteredJobs.length > 0) return;
248
288
  const trackExists = data.tracks.some((t) => t.id === options.track);
249
289
  if (!trackExists) {
250
- console.error(`Track not found: ${options.track}`);
251
- console.error(`Available: ${data.tracks.map((t) => t.id).join(", ")}`);
290
+ process.stderr.write(
291
+ formatError(`Track not found: ${options.track}`) + "\n",
292
+ );
293
+ process.stderr.write(
294
+ `Available: ${data.tracks.map((t) => t.id).join(", ")}\n`,
295
+ );
252
296
  } else {
253
- console.error(`No jobs found for track: ${options.track}`);
297
+ process.stderr.write(
298
+ formatError(`No jobs found for track: ${options.track}`) + "\n",
299
+ );
254
300
  const trackDisciplines = data.disciplines
255
301
  .filter((d) => d.validTracks && d.validTracks.includes(options.track))
256
302
  .map((d) => d.id);
257
303
  if (trackDisciplines.length > 0) {
258
- console.error(
259
- `Disciplines with this track: ${trackDisciplines.join(", ")}`,
304
+ process.stderr.write(
305
+ `Disciplines with this track: ${trackDisciplines.join(", ")}\n`,
260
306
  );
261
307
  }
262
308
  }
@@ -274,23 +320,23 @@ function reportInvalidCombination(discipline, level, track, data) {
274
320
  const combo = track
275
321
  ? `${discipline.id} × ${level.id} × ${track.id}`
276
322
  : `${discipline.id} × ${level.id}`;
277
- console.error(`Invalid combination: ${combo}`);
323
+ process.stderr.write(formatError(`Invalid combination: ${combo}`) + "\n");
278
324
  if (track) {
279
325
  const validTracks = discipline.validTracks?.filter((t) => t !== null) || [];
280
326
  if (validTracks.length > 0) {
281
- console.error(
282
- `Valid tracks for ${discipline.id}: ${validTracks.join(", ")}`,
327
+ process.stderr.write(
328
+ `Valid tracks for ${discipline.id}: ${validTracks.join(", ")}\n`,
283
329
  );
284
330
  } else {
285
- console.error(`${discipline.id} does not support tracks`);
331
+ process.stderr.write(`${discipline.id} does not support tracks\n`);
286
332
  }
287
333
  }
288
334
  if (discipline.minLevel) {
289
335
  const levelIndex = data.levels.findIndex((g) => g.id === level.id);
290
336
  const minIndex = data.levels.findIndex((g) => g.id === discipline.minLevel);
291
337
  if (levelIndex >= 0 && minIndex >= 0 && levelIndex < minIndex) {
292
- console.error(
293
- `${discipline.id} requires minimum level: ${discipline.minLevel}`,
338
+ process.stderr.write(
339
+ `${discipline.id} requires minimum level: ${discipline.minLevel}\n`,
294
340
  );
295
341
  }
296
342
  }
@@ -12,7 +12,12 @@
12
12
 
13
13
  import { createEntityCommand } from "./command-factory.js";
14
14
  import { levelToMarkdown } from "../formatters/level/markdown.js";
15
- import { formatTable } from "@forwardimpact/libcli";
15
+ import {
16
+ formatTable,
17
+ formatHeader,
18
+ formatSubheader,
19
+ formatBullet,
20
+ } from "@forwardimpact/libcli";
16
21
  import { getConceptEmoji } from "@forwardimpact/map/levels";
17
22
  import { capitalize } from "../formatters/shared.js";
18
23
 
@@ -34,7 +39,7 @@ function formatSummary(levels, data) {
34
39
  const { framework } = data;
35
40
  const emoji = framework ? getConceptEmoji(framework, "level") : "📊";
36
41
 
37
- console.log(`\n${emoji} Levels\n`);
42
+ process.stdout.write("\n" + formatHeader(`${emoji} Levels`) + "\n\n");
38
43
 
39
44
  const rows = levels.map((g) => [
40
45
  g.id,
@@ -44,7 +49,7 @@ function formatSummary(levels, data) {
44
49
  capitalize(g.baseSkillProficiencies?.primary || "-"),
45
50
  ]);
46
51
 
47
- console.log(
52
+ process.stdout.write(
48
53
  formatTable(
49
54
  [
50
55
  "ID",
@@ -54,11 +59,18 @@ function formatSummary(levels, data) {
54
59
  "Primary Level",
55
60
  ],
56
61
  rows,
57
- ),
62
+ ) + "\n",
63
+ );
64
+ process.stdout.write(
65
+ "\n" + formatSubheader(`Total: ${levels.length} levels`) + "\n\n",
66
+ );
67
+ process.stdout.write(
68
+ formatBullet("Run 'npx fit-pathway level --list' for IDs and titles") +
69
+ "\n",
70
+ );
71
+ process.stdout.write(
72
+ formatBullet("Run 'npx fit-pathway level <id>' for details") + "\n\n",
58
73
  );
59
- console.log(`\nTotal: ${levels.length} levels`);
60
- console.log(`\nRun 'npx fit-pathway level --list' for IDs and titles`);
61
- console.log(`Run 'npx fit-pathway level <id>' for details\n`);
62
74
  }
63
75
 
64
76
  /**
@@ -15,13 +15,14 @@ import {
15
15
  getDefaultTargetLevel,
16
16
  } from "../formatters/progress/shared.js";
17
17
  import { progressToMarkdown } from "../formatters/progress/markdown.js";
18
+ import { formatError } from "@forwardimpact/libcli";
18
19
 
19
20
  /**
20
21
  * Format progress output
21
22
  * @param {Object} view - Presenter view
22
23
  */
23
24
  function formatProgress(view) {
24
- console.log(progressToMarkdown(view));
25
+ process.stdout.write(progressToMarkdown(view) + "\n");
25
26
  }
26
27
 
27
28
  export const runProgressCommand = createCompositeCommand({
@@ -38,13 +39,17 @@ export const runProgressCommand = createCompositeCommand({
38
39
  if (options.compare) {
39
40
  targetLevel = data.levels.find((g) => g.id === options.compare);
40
41
  if (!targetLevel) {
41
- console.error(`Target level not found: ${options.compare}`);
42
+ process.stderr.write(
43
+ formatError(`Target level not found: ${options.compare}`) + "\n",
44
+ );
42
45
  process.exit(1);
43
46
  }
44
47
  } else {
45
48
  targetLevel = getDefaultTargetLevel(level, data.levels);
46
49
  if (!targetLevel) {
47
- console.error("No next level available for progression.");
50
+ process.stderr.write(
51
+ formatError("No next level available for progression.") + "\n",
52
+ );
48
53
  process.exit(1);
49
54
  }
50
55
  }