@forwardimpact/pathway 0.13.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.
Files changed (45) hide show
  1. package/bin/fit-pathway.js +26 -7
  2. package/package.json +2 -2
  3. package/src/commands/build.js +98 -2
  4. package/src/commands/index.js +1 -0
  5. package/src/commands/job.js +1 -0
  6. package/src/commands/stage.js +8 -8
  7. package/src/commands/update.js +133 -0
  8. package/src/components/command-prompt.js +85 -0
  9. package/src/components/nav.js +2 -2
  10. package/src/components/top-bar.js +97 -0
  11. package/src/css/bundles/app.css +2 -0
  12. package/src/css/components/command-prompt.css +98 -0
  13. package/src/css/components/layout.css +0 -3
  14. package/src/css/components/nav.css +121 -81
  15. package/src/css/components/surfaces.css +1 -1
  16. package/src/css/components/top-bar.css +180 -0
  17. package/src/css/pages/agent-builder.css +0 -9
  18. package/src/css/pages/landing.css +4 -0
  19. package/src/css/pages/lifecycle.css +5 -2
  20. package/src/css/reset.css +1 -1
  21. package/src/css/tokens.css +4 -2
  22. package/src/css/views/slide-base.css +2 -1
  23. package/src/formatters/agent/dom.js +0 -26
  24. package/src/formatters/agent/profile.js +13 -7
  25. package/src/formatters/agent/skill.js +4 -4
  26. package/src/formatters/stage/dom.js +13 -10
  27. package/src/formatters/stage/microdata.js +14 -8
  28. package/src/formatters/stage/shared.js +4 -4
  29. package/src/index.html +69 -44
  30. package/src/lib/cli-command.js +145 -0
  31. package/src/lib/state.js +2 -0
  32. package/src/main.js +47 -26
  33. package/src/pages/agent-builder.js +0 -28
  34. package/src/pages/behaviour.js +3 -1
  35. package/src/pages/discipline.js +3 -1
  36. package/src/pages/driver.js +6 -1
  37. package/src/pages/grade.js +6 -1
  38. package/src/pages/job.js +1 -0
  39. package/src/pages/landing.js +7 -0
  40. package/src/pages/skill.js +9 -2
  41. package/src/pages/track.js +6 -1
  42. package/src/slides/job.js +1 -0
  43. package/templates/agent.template.md +17 -10
  44. package/templates/install.template.sh +33 -0
  45. package/templates/skill.template.md +15 -7
@@ -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] Generate static site to ./public/
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
@@ -253,6 +256,7 @@ function parseArgs(args) {
253
256
  path: null,
254
257
  // Site command options
255
258
  clean: true,
259
+ url: null,
256
260
  };
257
261
 
258
262
  for (const arg of args) {
@@ -308,6 +312,8 @@ function parseArgs(args) {
308
312
  result.path = arg.slice(7);
309
313
  } else if (arg === "--no-clean") {
310
314
  result.clean = false;
315
+ } else if (arg.startsWith("--url=")) {
316
+ result.url = arg.slice(6);
311
317
  } else if (!arg.startsWith("-")) {
312
318
  if (!result.command) {
313
319
  result.command = arg;
@@ -332,9 +338,10 @@ function printHelp() {
332
338
  * Resolution order:
333
339
  * 1. --data=<path> flag (explicit override)
334
340
  * 2. PATHWAY_DATA environment variable
335
- * 3. ./data/ relative to current working directory
336
- * 4. ./examples/ relative to current working directory
337
- * 5. apps/schema/examples/ for monorepo development
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
338
345
  *
339
346
  * @param {Object} options - Parsed command options
340
347
  * @returns {string} Resolved absolute path to data directory
@@ -350,19 +357,25 @@ function resolveDataPath(options) {
350
357
  return resolve(process.env.PATHWAY_DATA);
351
358
  }
352
359
 
353
- // 3. Current working directory ./data/
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/
354
367
  const cwdData = join(process.cwd(), "data");
355
368
  if (existsSync(cwdData)) {
356
369
  return cwdData;
357
370
  }
358
371
 
359
- // 4. Current working directory ./examples/
372
+ // 5. Current working directory ./examples/
360
373
  const cwdExamples = join(process.cwd(), "examples");
361
374
  if (existsSync(cwdExamples)) {
362
375
  return cwdExamples;
363
376
  }
364
377
 
365
- // 5. Monorepo: apps/schema/examples/
378
+ // 6. Monorepo: apps/schema/examples/
366
379
  const schemaExamples = join(process.cwd(), "apps/schema/examples");
367
380
  if (existsSync(schemaExamples)) {
368
381
  return schemaExamples;
@@ -414,6 +427,12 @@ async function main() {
414
427
  process.exit(0);
415
428
  }
416
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
+
417
436
  const handler = COMMANDS[command];
418
437
 
419
438
  if (!handler) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forwardimpact/pathway",
3
- "version": "0.13.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": {
@@ -41,7 +41,7 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@forwardimpact/schema": "^0.6.0",
44
- "@forwardimpact/model": "^0.7.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"
@@ -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 { cp, mkdir, rm, access, realpath } from "fs/promises";
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
+ }
@@ -19,3 +19,4 @@ export { runQuestionsCommand } from "./questions.js";
19
19
  export { runServeCommand } from "./serve.js";
20
20
  export { runInitCommand } from "./init.js";
21
21
  export { runSiteCommand } from "./site.js";
22
+ export { runUpdateCommand } from "./update.js";
@@ -132,6 +132,7 @@ export async function runJobCommand({ data, args, options, dataDir }) {
132
132
  behaviours: data.behaviours,
133
133
  drivers: data.drivers,
134
134
  capabilities: data.capabilities,
135
+ stages: data.stages,
135
136
  });
136
137
 
137
138
  if (!view) {
@@ -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
- // Entry criteria
64
- if (view.entryCriteria.length > 0) {
65
- console.log(formatSubheader("Entry Criteria\n"));
66
- for (const item of view.entryCriteria) {
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
- // Exit criteria
73
- if (view.exitCriteria.length > 0) {
74
- console.log(formatSubheader("Exit Criteria\n"));
75
- for (const item of view.exitCriteria) {
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
+ }
@@ -5,11 +5,11 @@
5
5
  import { div, a } from "../lib/render.js";
6
6
 
7
7
  /**
8
- * Update the active navigation link
8
+ * Update the active navigation link in the drawer
9
9
  * @param {string} path - Current path
10
10
  */
11
11
  export function updateActiveNav(path) {
12
- const links = document.querySelectorAll("#nav-links a");
12
+ const links = document.querySelectorAll("#drawer-nav a");
13
13
  links.forEach((link) => {
14
14
  const href = link.getAttribute("href").slice(1); // Remove #
15
15
  const isActive = path === href || (href !== "/" && path.startsWith(href));
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Top Bar Component
3
+ *
4
+ * Fixed bar across the top of the app with:
5
+ * - Sidebar toggle button (left)
6
+ * - CLI command display with copy button (center)
7
+ *
8
+ * Similar to Safari's URL bar with sidebar toggle.
9
+ */
10
+
11
+ import { getCliCommand } from "../lib/cli-command.js";
12
+
13
+ /** @type {HTMLElement|null} */
14
+ let commandDisplay = null;
15
+
16
+ /** @type {HTMLButtonElement|null} */
17
+ let copyButton = null;
18
+
19
+ /**
20
+ * Set up the top bar: wire toggle and initial command display.
21
+ * Call after DOM is ready.
22
+ */
23
+ export function setupTopBar() {
24
+ const app = document.getElementById("app");
25
+ const toggle = document.getElementById("sidebar-toggle");
26
+ const commandEl = document.getElementById("cli-command");
27
+ const copyBtn = document.getElementById("cli-copy");
28
+
29
+ commandDisplay = commandEl;
30
+ copyButton = copyBtn;
31
+
32
+ if (toggle) {
33
+ toggle.addEventListener("click", () => {
34
+ app.classList.toggle("drawer-open");
35
+ });
36
+ }
37
+
38
+ if (copyBtn) {
39
+ copyBtn.addEventListener("click", handleCopy);
40
+ }
41
+
42
+ // Intercept history.replaceState so CLI command updates when pages
43
+ // change the hash without triggering hashchange (e.g. agent builder)
44
+ const originalReplaceState = history.replaceState.bind(history);
45
+ history.replaceState = (...args) => {
46
+ originalReplaceState(...args);
47
+ updateCommand();
48
+ };
49
+
50
+ // Set initial command
51
+ updateCommand();
52
+ }
53
+
54
+ /**
55
+ * Update the CLI command display for the current route.
56
+ * Call on every route change.
57
+ */
58
+ export function updateCommand() {
59
+ if (!commandDisplay) return;
60
+ const path = window.location.hash.slice(1) || "/";
61
+ commandDisplay.textContent = getCliCommand(path);
62
+ // Reset copy button state
63
+ if (copyButton) {
64
+ copyButton.setAttribute("aria-label", "Copy command");
65
+ copyButton.classList.remove("copied");
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Copy the current CLI command to clipboard
71
+ */
72
+ async function handleCopy() {
73
+ if (!commandDisplay || !copyButton) return;
74
+ const text = commandDisplay.textContent;
75
+
76
+ try {
77
+ await navigator.clipboard.writeText(text);
78
+ copyButton.classList.add("copied");
79
+ copyButton.setAttribute("aria-label", "Copied!");
80
+ setTimeout(() => {
81
+ copyButton.classList.remove("copied");
82
+ copyButton.setAttribute("aria-label", "Copy command");
83
+ }, 2000);
84
+ } catch {
85
+ // Fallback for older browsers
86
+ const textarea = document.createElement("textarea");
87
+ textarea.value = text;
88
+ textarea.style.position = "fixed";
89
+ textarea.style.opacity = "0";
90
+ document.body.appendChild(textarea);
91
+ textarea.select();
92
+ document.execCommand("copy");
93
+ document.body.removeChild(textarea);
94
+ copyButton.classList.add("copied");
95
+ setTimeout(() => copyButton.classList.remove("copied"), 2000);
96
+ }
97
+ }
@@ -24,6 +24,8 @@
24
24
  @import "../components/progress.css" layer(components);
25
25
  @import "../components/states.css" layer(components);
26
26
  @import "../components/nav.css" layer(components);
27
+ @import "../components/top-bar.css" layer(components);
28
+ @import "../components/command-prompt.css" layer(components);
27
29
 
28
30
  /* Utilities */
29
31
  @import "../components/utilities.css" layer(utilities);