@forwardimpact/pathway 0.12.0 → 0.14.0
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 +32 -12
- package/package.json +3 -3
- package/src/commands/build.js +98 -2
- package/src/commands/index.js +1 -0
- package/src/commands/interview.js +52 -14
- package/src/commands/job.js +1 -0
- package/src/commands/questions.js +13 -10
- package/src/commands/stage.js +8 -8
- package/src/commands/update.js +133 -0
- package/src/components/command-prompt.js +85 -0
- package/src/components/nav.js +2 -2
- package/src/components/top-bar.js +97 -0
- package/src/css/bundles/app.css +2 -0
- package/src/css/components/badges.css +41 -11
- package/src/css/components/command-prompt.css +98 -0
- package/src/css/components/layout.css +0 -3
- package/src/css/components/nav.css +121 -81
- package/src/css/components/surfaces.css +1 -1
- package/src/css/components/top-bar.css +180 -0
- package/src/css/pages/agent-builder.css +0 -9
- package/src/css/pages/landing.css +4 -0
- package/src/css/pages/lifecycle.css +5 -2
- package/src/css/reset.css +1 -1
- package/src/css/tokens.css +25 -11
- package/src/css/views/slide-base.css +2 -1
- package/src/formatters/agent/dom.js +0 -26
- package/src/formatters/agent/profile.js +13 -7
- package/src/formatters/agent/skill.js +4 -4
- package/src/formatters/interview/markdown.js +62 -3
- package/src/formatters/interview/shared.js +89 -52
- package/src/formatters/questions/markdown.js +15 -0
- package/src/formatters/questions/shared.js +70 -58
- package/src/formatters/stage/dom.js +13 -10
- package/src/formatters/stage/microdata.js +14 -8
- package/src/formatters/stage/shared.js +4 -4
- package/src/index.html +69 -44
- package/src/lib/cli-command.js +145 -0
- package/src/lib/state.js +2 -0
- package/src/lib/yaml-loader.js +39 -21
- package/src/main.js +47 -26
- package/src/pages/agent-builder.js +0 -28
- package/src/pages/behaviour.js +3 -1
- package/src/pages/discipline.js +3 -1
- package/src/pages/driver.js +6 -1
- package/src/pages/grade.js +6 -1
- package/src/pages/interview.js +61 -5
- package/src/pages/job.js +1 -0
- package/src/pages/landing.js +7 -0
- package/src/pages/skill.js +9 -2
- package/src/pages/track.js +6 -1
- package/src/slides/job.js +1 -0
- package/templates/agent.template.md +17 -10
- package/templates/install.template.sh +33 -0
- package/templates/skill.template.md +15 -7
package/bin/fit-pathway.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* stage [<id>] Show stages
|
|
19
19
|
* tool [<name>] Show tools
|
|
20
20
|
* job [<discipline> <grade>] [--track=TRACK] Generate job definition
|
|
21
|
-
* interview <discipline> <grade> [--track=TRACK] [--type=
|
|
21
|
+
* interview <discipline> <grade> [--track=TRACK] [--type=mission|decomposition|stakeholder] Generate interview
|
|
22
22
|
* progress <discipline> <grade> [--track=TRACK] [--compare=GRADE] Career progression
|
|
23
23
|
* questions [options] Browse interview questions
|
|
24
24
|
* agent [<discipline> <track>] [--output=PATH] Generate AI agent
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
|
|
32
32
|
import { join, resolve } from "path";
|
|
33
33
|
import { existsSync } from "fs";
|
|
34
|
+
import { homedir } from "os";
|
|
34
35
|
import { loadAllData } from "@forwardimpact/schema/loader";
|
|
35
36
|
import { formatError } from "../src/lib/cli-output.js";
|
|
36
37
|
|
|
@@ -51,6 +52,7 @@ import { runAgentCommand } from "../src/commands/agent.js";
|
|
|
51
52
|
import { runDevCommand } from "../src/commands/dev.js";
|
|
52
53
|
import { runInitCommand } from "../src/commands/init.js";
|
|
53
54
|
import { runBuildCommand } from "../src/commands/build.js";
|
|
55
|
+
import { runUpdateCommand } from "../src/commands/update.js";
|
|
54
56
|
|
|
55
57
|
const COMMANDS = {
|
|
56
58
|
discipline: runDisciplineCommand,
|
|
@@ -86,7 +88,8 @@ GETTING STARTED
|
|
|
86
88
|
|
|
87
89
|
init Create ./data/ with example data
|
|
88
90
|
dev [--port=PORT] Run live development server
|
|
89
|
-
build [--output=PATH]
|
|
91
|
+
build [--output=PATH] [--url=URL] Generate static site + distribution bundle
|
|
92
|
+
update [--url=URL] Update local ~/.fit/pathway/ installation
|
|
90
93
|
|
|
91
94
|
────────────────────────────────────────────────────────────────────────────────
|
|
92
95
|
ENTITY COMMANDS
|
|
@@ -167,13 +170,14 @@ INTERVIEW COMMAND
|
|
|
167
170
|
Generate interview question sets based on job requirements.
|
|
168
171
|
|
|
169
172
|
Usage:
|
|
170
|
-
npx fit-pathway interview <discipline> <grade>
|
|
171
|
-
npx fit-pathway interview <d> <g> --track=<track>
|
|
172
|
-
npx fit-pathway interview <d> <g> --type=<type>
|
|
173
|
+
npx fit-pathway interview <discipline> <grade> All types
|
|
174
|
+
npx fit-pathway interview <d> <g> --track=<track> With track
|
|
175
|
+
npx fit-pathway interview <d> <g> --track=<t> --type=<type> Single type
|
|
173
176
|
|
|
174
177
|
Options:
|
|
175
178
|
--track=TRACK Track specialization
|
|
176
|
-
--type=TYPE Interview type:
|
|
179
|
+
--type=TYPE Interview type: mission, decomposition, stakeholder
|
|
180
|
+
(omit for all types)
|
|
177
181
|
|
|
178
182
|
────────────────────────────────────────────────────────────────────────────────
|
|
179
183
|
PROGRESS COMMAND
|
|
@@ -252,6 +256,7 @@ function parseArgs(args) {
|
|
|
252
256
|
path: null,
|
|
253
257
|
// Site command options
|
|
254
258
|
clean: true,
|
|
259
|
+
url: null,
|
|
255
260
|
};
|
|
256
261
|
|
|
257
262
|
for (const arg of args) {
|
|
@@ -307,6 +312,8 @@ function parseArgs(args) {
|
|
|
307
312
|
result.path = arg.slice(7);
|
|
308
313
|
} else if (arg === "--no-clean") {
|
|
309
314
|
result.clean = false;
|
|
315
|
+
} else if (arg.startsWith("--url=")) {
|
|
316
|
+
result.url = arg.slice(6);
|
|
310
317
|
} else if (!arg.startsWith("-")) {
|
|
311
318
|
if (!result.command) {
|
|
312
319
|
result.command = arg;
|
|
@@ -331,9 +338,10 @@ function printHelp() {
|
|
|
331
338
|
* Resolution order:
|
|
332
339
|
* 1. --data=<path> flag (explicit override)
|
|
333
340
|
* 2. PATHWAY_DATA environment variable
|
|
334
|
-
* 3.
|
|
335
|
-
* 4. ./
|
|
336
|
-
* 5.
|
|
341
|
+
* 3. ~/.fit/pathway/data/ (home directory install)
|
|
342
|
+
* 4. ./data/ relative to current working directory
|
|
343
|
+
* 5. ./examples/ relative to current working directory
|
|
344
|
+
* 6. apps/schema/examples/ for monorepo development
|
|
337
345
|
*
|
|
338
346
|
* @param {Object} options - Parsed command options
|
|
339
347
|
* @returns {string} Resolved absolute path to data directory
|
|
@@ -349,19 +357,25 @@ function resolveDataPath(options) {
|
|
|
349
357
|
return resolve(process.env.PATHWAY_DATA);
|
|
350
358
|
}
|
|
351
359
|
|
|
352
|
-
// 3.
|
|
360
|
+
// 3. Home directory install (~/.fit/pathway/data/)
|
|
361
|
+
const homeData = join(homedir(), ".fit", "pathway", "data");
|
|
362
|
+
if (existsSync(homeData)) {
|
|
363
|
+
return homeData;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// 4. Current working directory ./data/
|
|
353
367
|
const cwdData = join(process.cwd(), "data");
|
|
354
368
|
if (existsSync(cwdData)) {
|
|
355
369
|
return cwdData;
|
|
356
370
|
}
|
|
357
371
|
|
|
358
|
-
//
|
|
372
|
+
// 5. Current working directory ./examples/
|
|
359
373
|
const cwdExamples = join(process.cwd(), "examples");
|
|
360
374
|
if (existsSync(cwdExamples)) {
|
|
361
375
|
return cwdExamples;
|
|
362
376
|
}
|
|
363
377
|
|
|
364
|
-
//
|
|
378
|
+
// 6. Monorepo: apps/schema/examples/
|
|
365
379
|
const schemaExamples = join(process.cwd(), "apps/schema/examples");
|
|
366
380
|
if (existsSync(schemaExamples)) {
|
|
367
381
|
return schemaExamples;
|
|
@@ -413,6 +427,12 @@ async function main() {
|
|
|
413
427
|
process.exit(0);
|
|
414
428
|
}
|
|
415
429
|
|
|
430
|
+
// Handle update command (re-downloads bundle for local install)
|
|
431
|
+
if (command === "update") {
|
|
432
|
+
await runUpdateCommand({ dataDir, options });
|
|
433
|
+
process.exit(0);
|
|
434
|
+
}
|
|
435
|
+
|
|
416
436
|
const handler = COMMANDS[command];
|
|
417
437
|
|
|
418
438
|
if (!handler) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forwardimpact/pathway",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Career progression web app and CLI for exploring roles and generating agents",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
"./commands": "./src/commands/index.js"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@forwardimpact/schema": "^0.
|
|
44
|
-
"@forwardimpact/model": "^0.
|
|
43
|
+
"@forwardimpact/schema": "^0.6.0",
|
|
44
|
+
"@forwardimpact/model": "^0.8.0",
|
|
45
45
|
"mustache": "^4.2.0",
|
|
46
46
|
"simple-icons": "^16.7.0",
|
|
47
47
|
"yaml": "^2.3.4"
|
package/src/commands/build.js
CHANGED
|
@@ -3,11 +3,24 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Generates a static site from the Engineering Pathway data.
|
|
5
5
|
* Copies all necessary files (HTML, JS, CSS) and data to an output directory.
|
|
6
|
+
* Optionally generates a distribution bundle (bundle.tar.gz + install.sh)
|
|
7
|
+
* for local installs by individual engineers.
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
cp,
|
|
12
|
+
mkdir,
|
|
13
|
+
rm,
|
|
14
|
+
access,
|
|
15
|
+
realpath,
|
|
16
|
+
readFile,
|
|
17
|
+
writeFile,
|
|
18
|
+
} from "fs/promises";
|
|
19
|
+
import { readFileSync } from "fs";
|
|
9
20
|
import { join, dirname, relative, resolve } from "path";
|
|
10
21
|
import { fileURLToPath } from "url";
|
|
22
|
+
import { execFileSync } from "child_process";
|
|
23
|
+
import Mustache from "mustache";
|
|
11
24
|
import { generateAllIndexes } from "@forwardimpact/schema/index-generator";
|
|
12
25
|
import { loadFrameworkConfig } from "@forwardimpact/schema/loader";
|
|
13
26
|
|
|
@@ -151,14 +164,97 @@ ${framework.emojiIcon} Generating ${framework.title} static site...
|
|
|
151
164
|
console.log(` ✓ data/ (from ${relative(process.cwd(), dataDir)})`);
|
|
152
165
|
}
|
|
153
166
|
|
|
167
|
+
// Generate distribution bundle if siteUrl is configured
|
|
168
|
+
const siteUrl = options.url || framework.distribution?.siteUrl;
|
|
169
|
+
if (siteUrl) {
|
|
170
|
+
await generateBundle({ outputDir, dataDir, siteUrl, framework });
|
|
171
|
+
}
|
|
172
|
+
|
|
154
173
|
// Show summary
|
|
155
174
|
console.log(`
|
|
156
175
|
✅ Site generated successfully!
|
|
157
176
|
|
|
158
177
|
Output: ${outputDir}
|
|
159
|
-
|
|
178
|
+
${siteUrl ? `\nDistribution:\n ${outputDir}/bundle.tar.gz\n ${outputDir}/install.sh\n` : ""}
|
|
160
179
|
To serve locally:
|
|
161
180
|
cd ${relative(process.cwd(), outputDir) || "."}
|
|
162
181
|
npx serve .
|
|
163
182
|
`);
|
|
164
183
|
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Read the pathway package version from package.json
|
|
187
|
+
* @returns {string} Package version
|
|
188
|
+
*/
|
|
189
|
+
function getPathwayVersion() {
|
|
190
|
+
const pkgPath = join(appDir, "..", "package.json");
|
|
191
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
192
|
+
return pkg.version;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Generate distribution bundle (bundle.tar.gz + install.sh)
|
|
197
|
+
* @param {Object} params
|
|
198
|
+
* @param {string} params.outputDir - Build output directory
|
|
199
|
+
* @param {string} params.dataDir - Source data directory
|
|
200
|
+
* @param {string} params.siteUrl - Base URL for the published site
|
|
201
|
+
* @param {Object} params.framework - Framework configuration
|
|
202
|
+
*/
|
|
203
|
+
async function generateBundle({ outputDir, dataDir, siteUrl, framework }) {
|
|
204
|
+
console.log("📦 Generating distribution bundle...");
|
|
205
|
+
|
|
206
|
+
const version = getPathwayVersion();
|
|
207
|
+
const frameworkTitle = framework.title || "Engineering Pathway";
|
|
208
|
+
|
|
209
|
+
// 1. Create temporary bundle directory
|
|
210
|
+
const bundleDir = join(outputDir, "_bundle");
|
|
211
|
+
await mkdir(bundleDir, { recursive: true });
|
|
212
|
+
|
|
213
|
+
// 2. Generate minimal package.json for the bundle
|
|
214
|
+
const bundlePkg = {
|
|
215
|
+
name: "fit-pathway-local",
|
|
216
|
+
version: version,
|
|
217
|
+
private: true,
|
|
218
|
+
dependencies: {
|
|
219
|
+
"@forwardimpact/pathway": `^${version}`,
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
await writeFile(
|
|
223
|
+
join(bundleDir, "package.json"),
|
|
224
|
+
JSON.stringify(bundlePkg, null, 2) + "\n",
|
|
225
|
+
);
|
|
226
|
+
console.log(` ✓ package.json (pathway ^${version})`);
|
|
227
|
+
|
|
228
|
+
// 3. Copy data files into bundle
|
|
229
|
+
await cp(dataDir, join(bundleDir, "data"), {
|
|
230
|
+
recursive: true,
|
|
231
|
+
dereference: true,
|
|
232
|
+
});
|
|
233
|
+
console.log(" ✓ data/");
|
|
234
|
+
|
|
235
|
+
// 4. Create tar.gz from the bundle directory
|
|
236
|
+
execFileSync("tar", [
|
|
237
|
+
"-czf",
|
|
238
|
+
join(outputDir, "bundle.tar.gz"),
|
|
239
|
+
"-C",
|
|
240
|
+
outputDir,
|
|
241
|
+
"_bundle",
|
|
242
|
+
]);
|
|
243
|
+
console.log(" ✓ bundle.tar.gz");
|
|
244
|
+
|
|
245
|
+
// 5. Clean up temporary bundle directory
|
|
246
|
+
await rm(bundleDir, { recursive: true });
|
|
247
|
+
|
|
248
|
+
// 6. Render install.sh from template
|
|
249
|
+
const templatePath = join(appDir, "..", "templates", "install.template.sh");
|
|
250
|
+
const template = await readFile(templatePath, "utf8");
|
|
251
|
+
const installScript = Mustache.render(template, {
|
|
252
|
+
siteUrl: siteUrl.replace(/\/$/, ""),
|
|
253
|
+
version,
|
|
254
|
+
frameworkTitle,
|
|
255
|
+
});
|
|
256
|
+
await writeFile(join(outputDir, "install.sh"), installScript, {
|
|
257
|
+
mode: 0o755,
|
|
258
|
+
});
|
|
259
|
+
console.log(" ✓ install.sh");
|
|
260
|
+
}
|
package/src/commands/index.js
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* Generates and displays interview questions in the terminal.
|
|
5
5
|
*
|
|
6
6
|
* Usage:
|
|
7
|
-
* npx pathway interview <discipline> <grade>
|
|
8
|
-
* npx pathway interview <discipline> <grade> --track=<track>
|
|
9
|
-
* npx pathway interview <discipline> <grade> --track=<track> --type=
|
|
7
|
+
* npx fit-pathway interview <discipline> <grade> # All interview types
|
|
8
|
+
* npx fit-pathway interview <discipline> <grade> --track=<track> # With track
|
|
9
|
+
* npx fit-pathway interview <discipline> <grade> --track=<track> --type=mission # Single type
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { createCompositeCommand } from "./command-factory.js";
|
|
@@ -16,8 +16,10 @@ import {
|
|
|
16
16
|
} from "../formatters/interview/shared.js";
|
|
17
17
|
import { interviewToMarkdown } from "../formatters/interview/markdown.js";
|
|
18
18
|
|
|
19
|
+
const VALID_TYPES = Object.keys(INTERVIEW_TYPES);
|
|
20
|
+
|
|
19
21
|
/**
|
|
20
|
-
* Format interview
|
|
22
|
+
* Format a single interview type as markdown
|
|
21
23
|
* @param {Object} view - Presenter view
|
|
22
24
|
* @param {Object} options - Options including framework
|
|
23
25
|
*/
|
|
@@ -25,15 +27,31 @@ function formatInterview(view, options) {
|
|
|
25
27
|
console.log(interviewToMarkdown(view, { framework: options.framework }));
|
|
26
28
|
}
|
|
27
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Format all interview types as markdown with separators
|
|
32
|
+
* @param {Array<Object>} views - Array of presenter views
|
|
33
|
+
* @param {Object} options - Options including framework
|
|
34
|
+
*/
|
|
35
|
+
function formatAllInterviews(views, options) {
|
|
36
|
+
for (let i = 0; i < views.length; i++) {
|
|
37
|
+
if (i > 0) {
|
|
38
|
+
console.log("\n" + "─".repeat(80) + "\n");
|
|
39
|
+
}
|
|
40
|
+
console.log(
|
|
41
|
+
interviewToMarkdown(views[i], { framework: options.framework }),
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
28
46
|
export const runInterviewCommand = createCompositeCommand({
|
|
29
47
|
commandName: "interview",
|
|
30
48
|
requiredArgs: ["discipline_id", "grade_id"],
|
|
31
49
|
findEntities: (data, args, options) => {
|
|
32
|
-
const interviewType = options.type
|
|
50
|
+
const interviewType = options.type === "full" ? null : options.type;
|
|
33
51
|
|
|
34
|
-
if (!INTERVIEW_TYPES[interviewType]) {
|
|
52
|
+
if (interviewType && !INTERVIEW_TYPES[interviewType]) {
|
|
35
53
|
console.error(`Unknown interview type: ${interviewType}`);
|
|
36
|
-
console.error(
|
|
54
|
+
console.error(`Available types: ${VALID_TYPES.join(", ")}`);
|
|
37
55
|
process.exit(1);
|
|
38
56
|
}
|
|
39
57
|
|
|
@@ -58,18 +76,38 @@ export const runInterviewCommand = createCompositeCommand({
|
|
|
58
76
|
}
|
|
59
77
|
return null;
|
|
60
78
|
},
|
|
61
|
-
presenter: (entities, data, _options) =>
|
|
62
|
-
|
|
79
|
+
presenter: (entities, data, _options) => {
|
|
80
|
+
const params = {
|
|
63
81
|
discipline: entities.discipline,
|
|
64
82
|
grade: entities.grade,
|
|
65
83
|
track: entities.track,
|
|
66
84
|
skills: data.skills,
|
|
67
85
|
behaviours: data.behaviours,
|
|
68
86
|
questions: data.questions,
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Single type: return one view
|
|
90
|
+
if (entities.interviewType) {
|
|
91
|
+
return prepareInterviewDetail({
|
|
92
|
+
...params,
|
|
93
|
+
interviewType: entities.interviewType,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// All types: return array of views
|
|
98
|
+
return VALID_TYPES.map((type) =>
|
|
99
|
+
prepareInterviewDetail({ ...params, interviewType: type }),
|
|
100
|
+
).filter(Boolean);
|
|
101
|
+
},
|
|
102
|
+
formatter: (view, options, data) => {
|
|
103
|
+
const opts = { ...options, framework: data.framework };
|
|
104
|
+
|
|
105
|
+
if (Array.isArray(view)) {
|
|
106
|
+
formatAllInterviews(view, opts);
|
|
107
|
+
} else {
|
|
108
|
+
formatInterview(view, opts);
|
|
109
|
+
}
|
|
110
|
+
},
|
|
73
111
|
usageExample:
|
|
74
|
-
"npx pathway interview software_engineering
|
|
112
|
+
"npx fit-pathway interview software_engineering J090 --track=platform --type=mission",
|
|
75
113
|
});
|
package/src/commands/job.js
CHANGED
|
@@ -56,12 +56,15 @@ function showQuestionsSummary(data) {
|
|
|
56
56
|
"practitioner",
|
|
57
57
|
"expert",
|
|
58
58
|
];
|
|
59
|
+
const roleTypes = ["professionalQuestions", "managementQuestions"];
|
|
59
60
|
const skillRows = skillLevels.map((level) => {
|
|
60
61
|
let count = 0;
|
|
61
62
|
for (const skill of skills) {
|
|
62
|
-
const sq = questions.
|
|
63
|
-
if (sq
|
|
64
|
-
|
|
63
|
+
const sq = questions.skillLevels?.[skill.id];
|
|
64
|
+
if (sq) {
|
|
65
|
+
for (const roleType of roleTypes) {
|
|
66
|
+
count += (sq[roleType]?.[level] || []).length;
|
|
67
|
+
}
|
|
65
68
|
}
|
|
66
69
|
}
|
|
67
70
|
return [level, count];
|
|
@@ -81,9 +84,11 @@ function showQuestionsSummary(data) {
|
|
|
81
84
|
const behaviourRows = maturities.map((maturity) => {
|
|
82
85
|
let count = 0;
|
|
83
86
|
for (const behaviour of behaviours) {
|
|
84
|
-
const bq = questions.
|
|
85
|
-
if (bq
|
|
86
|
-
|
|
87
|
+
const bq = questions.behaviourMaturities?.[behaviour.id];
|
|
88
|
+
if (bq) {
|
|
89
|
+
for (const roleType of roleTypes) {
|
|
90
|
+
count += (bq[roleType]?.[maturity] || []).length;
|
|
91
|
+
}
|
|
87
92
|
}
|
|
88
93
|
}
|
|
89
94
|
return [maturity.replace(/_/g, " "), count];
|
|
@@ -138,10 +143,8 @@ export async function runQuestionsCommand({
|
|
|
138
143
|
behaviours: data.behaviours,
|
|
139
144
|
filter,
|
|
140
145
|
});
|
|
141
|
-
for (const
|
|
142
|
-
|
|
143
|
-
console.log(q.id);
|
|
144
|
-
}
|
|
146
|
+
for (const q of view.questions) {
|
|
147
|
+
console.log(q.id);
|
|
145
148
|
}
|
|
146
149
|
return;
|
|
147
150
|
}
|
package/src/commands/stage.js
CHANGED
|
@@ -60,19 +60,19 @@ function formatDetail(viewAndContext, _framework) {
|
|
|
60
60
|
console.log(formatHeader(`\n${emoji} ${view.name}\n`));
|
|
61
61
|
console.log(`${view.description}\n`);
|
|
62
62
|
|
|
63
|
-
//
|
|
64
|
-
if (view.
|
|
65
|
-
console.log(formatSubheader("
|
|
66
|
-
for (const item of view.
|
|
63
|
+
// Read checklist
|
|
64
|
+
if (view.readChecklist.length > 0) {
|
|
65
|
+
console.log(formatSubheader("Read-Then-Do Checklist\n"));
|
|
66
|
+
for (const item of view.readChecklist) {
|
|
67
67
|
console.log(formatBullet(item, 1));
|
|
68
68
|
}
|
|
69
69
|
console.log();
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
//
|
|
73
|
-
if (view.
|
|
74
|
-
console.log(formatSubheader("
|
|
75
|
-
for (const item of view.
|
|
72
|
+
// Confirm checklist
|
|
73
|
+
if (view.confirmChecklist.length > 0) {
|
|
74
|
+
console.log(formatSubheader("Do-Then-Confirm Checklist\n"));
|
|
75
|
+
for (const item of view.confirmChecklist) {
|
|
76
76
|
console.log(formatBullet(item, 1));
|
|
77
77
|
}
|
|
78
78
|
console.log();
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Update Command
|
|
3
|
+
*
|
|
4
|
+
* Re-downloads the distribution bundle from the published site URL
|
|
5
|
+
* and updates the local ~/.fit/pathway/ installation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { cp, mkdir, rm, readFile, writeFile, access } from "fs/promises";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
import { homedir } from "os";
|
|
11
|
+
import { execFileSync, execSync } from "child_process";
|
|
12
|
+
import { loadFrameworkConfig } from "@forwardimpact/schema/loader";
|
|
13
|
+
|
|
14
|
+
const INSTALL_DIR = join(homedir(), ".fit", "pathway");
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Run the update command.
|
|
18
|
+
* Reads siteUrl from the installed framework.yaml, re-downloads the bundle,
|
|
19
|
+
* extracts data, and runs npm install to update dependencies.
|
|
20
|
+
*
|
|
21
|
+
* @param {Object} params - Command parameters
|
|
22
|
+
* @param {string} params.dataDir - Path to data directory (may be the installed one)
|
|
23
|
+
* @param {Object} params.options - Command options
|
|
24
|
+
*/
|
|
25
|
+
export async function runUpdateCommand({ dataDir: _dataDir, options }) {
|
|
26
|
+
const installDataDir = join(INSTALL_DIR, "data");
|
|
27
|
+
|
|
28
|
+
// Verify we have a home-directory installation
|
|
29
|
+
try {
|
|
30
|
+
await access(installDataDir);
|
|
31
|
+
} catch {
|
|
32
|
+
console.error("Error: No local installation found at ~/.fit/pathway/");
|
|
33
|
+
console.error(
|
|
34
|
+
"Install first using the install.sh script from your organization's pathway site.",
|
|
35
|
+
);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Load framework config to get siteUrl
|
|
40
|
+
const framework = await loadFrameworkConfig(installDataDir);
|
|
41
|
+
const siteUrl = options.url || framework.distribution?.siteUrl;
|
|
42
|
+
|
|
43
|
+
if (!siteUrl) {
|
|
44
|
+
console.error(
|
|
45
|
+
"Error: No siteUrl found in ~/.fit/pathway/data/framework.yaml (distribution.siteUrl)",
|
|
46
|
+
);
|
|
47
|
+
console.error("Provide one with --url=<URL> or add it to framework.yaml.");
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const baseUrl = siteUrl.replace(/\/$/, "");
|
|
52
|
+
const bundleName = "bundle.tar.gz";
|
|
53
|
+
|
|
54
|
+
console.log(`\n🔄 Updating from ${baseUrl}...\n`);
|
|
55
|
+
|
|
56
|
+
// 1. Download bundle to temp location
|
|
57
|
+
const tmpDir = join(INSTALL_DIR, "_update_tmp");
|
|
58
|
+
await mkdir(tmpDir, { recursive: true });
|
|
59
|
+
|
|
60
|
+
const tmpBundle = join(tmpDir, bundleName);
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
console.log(" Downloading bundle...");
|
|
64
|
+
execFileSync("curl", [
|
|
65
|
+
"-fsSL",
|
|
66
|
+
`${baseUrl}/${bundleName}`,
|
|
67
|
+
"-o",
|
|
68
|
+
tmpBundle,
|
|
69
|
+
]);
|
|
70
|
+
console.log(" ✓ Downloaded");
|
|
71
|
+
|
|
72
|
+
// 2. Extract bundle
|
|
73
|
+
console.log(" Extracting...");
|
|
74
|
+
const extractDir = join(tmpDir, "extracted");
|
|
75
|
+
await mkdir(extractDir, { recursive: true });
|
|
76
|
+
execFileSync("tar", [
|
|
77
|
+
"-xzf",
|
|
78
|
+
tmpBundle,
|
|
79
|
+
"-C",
|
|
80
|
+
extractDir,
|
|
81
|
+
"--strip-components=1",
|
|
82
|
+
]);
|
|
83
|
+
console.log(" ✓ Extracted");
|
|
84
|
+
|
|
85
|
+
// 3. Compare versions
|
|
86
|
+
const newPkgPath = join(extractDir, "package.json");
|
|
87
|
+
const oldPkgPath = join(INSTALL_DIR, "package.json");
|
|
88
|
+
const newPkg = JSON.parse(await readFile(newPkgPath, "utf8"));
|
|
89
|
+
let oldPkg;
|
|
90
|
+
try {
|
|
91
|
+
oldPkg = JSON.parse(await readFile(oldPkgPath, "utf8"));
|
|
92
|
+
} catch {
|
|
93
|
+
oldPkg = { version: "unknown", dependencies: {} };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const oldVersion =
|
|
97
|
+
oldPkg.dependencies?.["@forwardimpact/pathway"] || "unknown";
|
|
98
|
+
const newVersion =
|
|
99
|
+
newPkg.dependencies?.["@forwardimpact/pathway"] || "unknown";
|
|
100
|
+
|
|
101
|
+
// 4. Replace data
|
|
102
|
+
console.log(" Updating data files...");
|
|
103
|
+
await rm(installDataDir, { recursive: true });
|
|
104
|
+
await cp(join(extractDir, "data"), installDataDir, { recursive: true });
|
|
105
|
+
console.log(" ✓ Data updated");
|
|
106
|
+
|
|
107
|
+
// 5. Update package.json if version changed
|
|
108
|
+
if (oldVersion !== newVersion) {
|
|
109
|
+
console.log(` Updating pathway ${oldVersion} → ${newVersion}...`);
|
|
110
|
+
await writeFile(oldPkgPath, JSON.stringify(newPkg, null, 2) + "\n");
|
|
111
|
+
console.log(" ✓ package.json updated");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 6. Run npm install
|
|
115
|
+
console.log(" Installing dependencies...");
|
|
116
|
+
execSync("npm install --production --ignore-scripts --no-audit --no-fund", {
|
|
117
|
+
cwd: INSTALL_DIR,
|
|
118
|
+
stdio: "ignore",
|
|
119
|
+
});
|
|
120
|
+
console.log(" ✓ Dependencies installed");
|
|
121
|
+
|
|
122
|
+
// 7. Report
|
|
123
|
+
console.log(`
|
|
124
|
+
✅ Update complete!
|
|
125
|
+
|
|
126
|
+
Pathway: ${oldVersion === newVersion ? newVersion + " (unchanged)" : oldVersion + " → " + newVersion}
|
|
127
|
+
Data: updated from ${baseUrl}
|
|
128
|
+
`);
|
|
129
|
+
} finally {
|
|
130
|
+
// Clean up temp directory
|
|
131
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command Prompt Component
|
|
3
|
+
*
|
|
4
|
+
* Reusable terminal-style command display with copy button.
|
|
5
|
+
* Shows a `$` prompt, monospace command text, and a copy-to-clipboard button.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { div, span, button } from "../lib/render.js";
|
|
9
|
+
|
|
10
|
+
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create the copy icon SVG (two overlapping rectangles)
|
|
14
|
+
* @returns {SVGSVGElement}
|
|
15
|
+
*/
|
|
16
|
+
function createCopyIcon() {
|
|
17
|
+
const svg = document.createElementNS(SVG_NS, "svg");
|
|
18
|
+
svg.setAttribute("viewBox", "0 0 24 24");
|
|
19
|
+
|
|
20
|
+
const rect = document.createElementNS(SVG_NS, "rect");
|
|
21
|
+
rect.setAttribute("x", "9");
|
|
22
|
+
rect.setAttribute("y", "9");
|
|
23
|
+
rect.setAttribute("width", "13");
|
|
24
|
+
rect.setAttribute("height", "13");
|
|
25
|
+
rect.setAttribute("rx", "2");
|
|
26
|
+
|
|
27
|
+
const path = document.createElementNS(SVG_NS, "path");
|
|
28
|
+
path.setAttribute(
|
|
29
|
+
"d",
|
|
30
|
+
"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1",
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
svg.appendChild(rect);
|
|
34
|
+
svg.appendChild(path);
|
|
35
|
+
return svg;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Copy text to clipboard with fallback for older browsers
|
|
40
|
+
* @param {string} text
|
|
41
|
+
* @param {HTMLButtonElement} btn
|
|
42
|
+
*/
|
|
43
|
+
async function copyToClipboard(text, btn) {
|
|
44
|
+
try {
|
|
45
|
+
await navigator.clipboard.writeText(text);
|
|
46
|
+
} catch {
|
|
47
|
+
const textarea = document.createElement("textarea");
|
|
48
|
+
textarea.value = text;
|
|
49
|
+
textarea.style.position = "fixed";
|
|
50
|
+
textarea.style.opacity = "0";
|
|
51
|
+
document.body.appendChild(textarea);
|
|
52
|
+
textarea.select();
|
|
53
|
+
document.execCommand("copy");
|
|
54
|
+
document.body.removeChild(textarea);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
btn.classList.add("copied");
|
|
58
|
+
btn.setAttribute("aria-label", "Copied!");
|
|
59
|
+
setTimeout(() => {
|
|
60
|
+
btn.classList.remove("copied");
|
|
61
|
+
btn.setAttribute("aria-label", "Copy command");
|
|
62
|
+
}, 2000);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Create a command prompt element with copy button
|
|
67
|
+
* @param {string} command - The command text to display
|
|
68
|
+
* @returns {HTMLElement}
|
|
69
|
+
*/
|
|
70
|
+
export function createCommandPrompt(command) {
|
|
71
|
+
const copyBtn = button({
|
|
72
|
+
className: "command-prompt__copy",
|
|
73
|
+
"aria-label": "Copy command",
|
|
74
|
+
type: "button",
|
|
75
|
+
});
|
|
76
|
+
copyBtn.appendChild(createCopyIcon());
|
|
77
|
+
copyBtn.addEventListener("click", () => copyToClipboard(command, copyBtn));
|
|
78
|
+
|
|
79
|
+
return div(
|
|
80
|
+
{ className: "command-prompt" },
|
|
81
|
+
span({ className: "command-prompt__prompt" }, "$"),
|
|
82
|
+
span({ className: "command-prompt__text" }, command),
|
|
83
|
+
copyBtn,
|
|
84
|
+
);
|
|
85
|
+
}
|