pi-crew 0.9.26 → 0.9.28

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 (64) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/NOTICE.md +14 -0
  3. package/assets/crew-vibes.ttf +0 -0
  4. package/assets/runner-spritesheet.png +0 -0
  5. package/dist/build-meta.json +245 -48
  6. package/dist/index.mjs +1611 -243
  7. package/dist/index.mjs.map +4 -4
  8. package/docs/perf/optimization-plan-2026-07-verified.md +396 -0
  9. package/package.json +9 -2
  10. package/scripts/bench-check.mjs +96 -0
  11. package/scripts/bench-cold-start.mjs +216 -0
  12. package/scripts/build-bundle.mjs +84 -0
  13. package/scripts/build-crew-vibes-font.py +328 -0
  14. package/scripts/check-all-skills.ts +294 -0
  15. package/scripts/check-bundle-staleness.mjs +97 -0
  16. package/scripts/check-conflict-markers.mjs +91 -0
  17. package/scripts/check-lazy-imports.mjs +28 -0
  18. package/scripts/install-crew-vibes-font.mjs +91 -0
  19. package/scripts/postinstall.mjs +47 -0
  20. package/scripts/profile-startup.mjs +125 -0
  21. package/scripts/release-smoke.mjs +74 -0
  22. package/scripts/run-bench.mjs +47 -0
  23. package/scripts/run-real-chain.ts +41 -0
  24. package/scripts/test-issue-29-crash.ts +111 -0
  25. package/scripts/test-issue-29-e2e.ts +183 -0
  26. package/scripts/test-issue-29-real-runtime.ts +330 -0
  27. package/scripts/test-issue-29-real-tasks.ts +387 -0
  28. package/scripts/test-issue-29-team-tool.ts +105 -0
  29. package/scripts/test-runner.mjs +74 -0
  30. package/scripts/verify-flicker-fix.ts +109 -0
  31. package/scripts/verify-skill.ts +550 -0
  32. package/scripts/watch-bundle.mjs +259 -0
  33. package/scripts/watchdog-harness.ts +119 -0
  34. package/src/agents/agent-config.ts +2 -0
  35. package/src/agents/discover-agents.ts +10 -1
  36. package/src/config/defaults.ts +6 -0
  37. package/src/extension/crew-vibes/cat-frames.ts +18 -0
  38. package/src/extension/crew-vibes/config.ts +195 -0
  39. package/src/extension/crew-vibes/figures.ts +94 -0
  40. package/src/extension/crew-vibes/font-detect.ts +58 -0
  41. package/src/extension/crew-vibes/index.ts +385 -0
  42. package/src/extension/crew-vibes/provider-usage.ts +396 -0
  43. package/src/extension/crew-vibes/render.ts +206 -0
  44. package/src/extension/crew-vibes/speed.ts +286 -0
  45. package/src/extension/knowledge-injection.ts +12 -3
  46. package/src/extension/management.ts +23 -3
  47. package/src/extension/register.ts +7 -0
  48. package/src/extension/team-tool.ts +11 -0
  49. package/src/prompt/prompt-runtime.ts +65 -0
  50. package/src/runtime/child-pi.ts +123 -10
  51. package/src/runtime/crew-agent-records.ts +10 -3
  52. package/src/runtime/pi-args.ts +2 -0
  53. package/src/runtime/retry-executor.ts +4 -1
  54. package/src/runtime/task-runner/state-helpers.ts +9 -30
  55. package/src/runtime/task-runner.ts +1 -0
  56. package/src/runtime/team-runner.ts +5 -3
  57. package/src/state/atomic-write.ts +153 -49
  58. package/src/state/event-log.ts +16 -10
  59. package/src/state/locks.ts +7 -8
  60. package/src/state/mailbox.ts +15 -4
  61. package/src/state/state-store.ts +39 -10
  62. package/src/teams/discover-teams.ts +56 -1
  63. package/src/utils/safe-paths.ts +2 -1
  64. package/src/workflows/discover-workflows.ts +72 -1
@@ -0,0 +1,294 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Check All Skills Script
5
+ *
6
+ * Runs verify-skill.ts against all skills and produces a summary report.
7
+ *
8
+ * Usage:
9
+ * node scripts/check-all-skills.ts
10
+ */
11
+
12
+ import { spawnSync } from "child_process";
13
+ import * as fs from "fs";
14
+ import * as path from "path";
15
+
16
+ interface SkillSummary {
17
+ name: string;
18
+ path: string;
19
+ status: "pass" | "fail" | "warning";
20
+ warnings: string[];
21
+ errors: string[];
22
+ }
23
+
24
+ interface Report {
25
+ total: number;
26
+ passed: number;
27
+ failed: number;
28
+ warningsOnly: number;
29
+ skills: SkillSummary[];
30
+ timestamp: string;
31
+ }
32
+
33
+ const SKILLS_DIR = path.join(process.cwd(), "skills");
34
+
35
+ /**
36
+ * Get all skill directories
37
+ */
38
+ function getSkillDirs(): string[] {
39
+ if (!fs.existsSync(SKILLS_DIR)) {
40
+ console.error("Skills directory not found: " + SKILLS_DIR);
41
+ return [];
42
+ }
43
+
44
+ const dirs: string[] = [];
45
+ const entries = fs.readdirSync(SKILLS_DIR, { withFileTypes: true });
46
+
47
+ for (const entry of entries) {
48
+ if (entry.isDirectory()) {
49
+ const skillPath = path.join(SKILLS_DIR, entry.name, "SKILL.md");
50
+ if (fs.existsSync(skillPath)) {
51
+ dirs.push(entry.name);
52
+ }
53
+ }
54
+ }
55
+
56
+ return dirs.sort();
57
+ }
58
+
59
+ /**
60
+ * Run verify-skill.ts for a single skill
61
+ */
62
+ function verifySingleSkill(skillName: string): {
63
+ passed: boolean;
64
+ warnings: string[];
65
+ errors: string[];
66
+ } {
67
+ const skillPath = path.join(SKILLS_DIR, skillName, "SKILL.md");
68
+
69
+ const result = spawnSync("node", ["scripts/verify-skill.ts", skillPath], {
70
+ cwd: process.cwd(),
71
+ encoding: "utf-8",
72
+ });
73
+
74
+ const output = result.stdout + result.stderr;
75
+ const warnings: string[] = [];
76
+ const errors: string[] = [];
77
+
78
+ // Parse output
79
+ const warningMatches = output.match(/[^\n]*⚠️[^\n]*/g);
80
+ const errorMatches = output.match(/[^\n]+FAIL[^\n]+/g);
81
+
82
+ if (warningMatches) {
83
+ warnings.push(...warningMatches.map((w) => w.trim()));
84
+ }
85
+ if (errorMatches) {
86
+ errors.push(...errorMatches.map((e) => e.trim()));
87
+ }
88
+
89
+ const passed = result.status === 0;
90
+
91
+ return { passed, warnings, errors };
92
+ }
93
+
94
+ /**
95
+ * Generate markdown report
96
+ */
97
+ function generateMarkdownReport(report: Report): string {
98
+ const lines: string[] = [];
99
+
100
+ lines.push("# Skill Verification Report");
101
+ lines.push("");
102
+ lines.push("Generated: " + report.timestamp);
103
+ lines.push("");
104
+
105
+ // Summary table
106
+ lines.push("## Summary");
107
+ lines.push("");
108
+ lines.push("| Status | Count |");
109
+ lines.push("|--------|-------|");
110
+ lines.push("| PASS | " + report.passed + " |");
111
+ lines.push("| FAIL | " + report.failed + " |");
112
+ lines.push("| Warnings | " + report.warningsOnly + " |");
113
+ lines.push("| **Total** | **" + report.total + "** |");
114
+ lines.push("");
115
+
116
+ // Skill list by status
117
+ lines.push("## Skills by Status");
118
+ lines.push("");
119
+
120
+ // Passed skills
121
+ const passedSkills = report.skills.filter((s) => s.status === "pass");
122
+ if (passedSkills.length > 0) {
123
+ lines.push("### Passing Skills");
124
+ lines.push("");
125
+ for (const skill of passedSkills) {
126
+ lines.push("- " + skill.name);
127
+ }
128
+ lines.push("");
129
+ }
130
+
131
+ // Failed skills
132
+ const failedSkills = report.skills.filter((s) => s.status === "fail");
133
+ if (failedSkills.length > 0) {
134
+ lines.push("### Failing Skills");
135
+ lines.push("");
136
+ for (const skill of failedSkills) {
137
+ lines.push("- **" + skill.name + "**");
138
+ for (const error of skill.errors) {
139
+ lines.push(" - " + error);
140
+ }
141
+ }
142
+ lines.push("");
143
+ }
144
+
145
+ // Warning skills
146
+ const warningSkills = report.skills.filter((s) => s.status === "warning");
147
+ if (warningSkills.length > 0) {
148
+ lines.push("### Skills with Warnings");
149
+ lines.push("");
150
+ for (const skill of warningSkills) {
151
+ lines.push("- **" + skill.name + "**");
152
+ for (const warning of skill.warnings) {
153
+ lines.push(" - " + warning);
154
+ }
155
+ }
156
+ lines.push("");
157
+ }
158
+
159
+ // Recommendations
160
+ lines.push("## Recommendations");
161
+ lines.push("");
162
+ if (failedSkills.length > 0) {
163
+ lines.push("1. Fix " + failedSkills.length + " failing skills to have proper RED/GREEN gates");
164
+ lines.push("2. Add trigger sections (When to Activate) if missing");
165
+ lines.push("3. Add anti-patterns sections to prevent common mistakes");
166
+ lines.push("");
167
+ }
168
+ if (warningSkills.length > 0) {
169
+ lines.push("4. Review " + warningSkills.length + " skills with warnings for improvements");
170
+ }
171
+
172
+ return lines.join("\n");
173
+ }
174
+
175
+ /**
176
+ * Generate JSON report
177
+ */
178
+ function generateJsonReport(report: Report): string {
179
+ return JSON.stringify(report, null, 2);
180
+ }
181
+
182
+ /**
183
+ * Main entry point
184
+ */
185
+ async function main() {
186
+ console.log("Checking all skills...\n");
187
+
188
+ const skillNames = getSkillDirs();
189
+ console.log("Found " + skillNames.length + " skills\n");
190
+
191
+ const report: Report = {
192
+ total: skillNames.length,
193
+ passed: 0,
194
+ failed: 0,
195
+ warningsOnly: 0,
196
+ skills: [],
197
+ timestamp: new Date().toISOString(),
198
+ };
199
+
200
+ for (const skillName of skillNames) {
201
+ process.stdout.write(".");
202
+
203
+ const result = verifySingleSkill(skillName);
204
+
205
+ let status: "pass" | "fail" | "warning" = "pass";
206
+ if (!result.passed && result.errors.length > 0) {
207
+ status = "fail";
208
+ report.failed++;
209
+ } else if (result.warnings.length > 0) {
210
+ status = "warning";
211
+ report.warningsOnly++;
212
+ } else {
213
+ report.passed++;
214
+ }
215
+
216
+ report.skills.push({
217
+ name: skillName,
218
+ path: path.join("skills", skillName, "SKILL.md"),
219
+ status,
220
+ warnings: result.warnings,
221
+ errors: result.errors,
222
+ });
223
+ }
224
+
225
+ console.log("\n\n");
226
+
227
+ // Print summary
228
+ console.log("=== Skill Verification Summary ===");
229
+ console.log("Total: " + report.total);
230
+ console.log("Passed: " + report.passed);
231
+ console.log("Failed: " + report.failed);
232
+ console.log("Warnings only: " + report.warningsOnly);
233
+ console.log("");
234
+
235
+ // List failed skills
236
+ if (report.failed > 0) {
237
+ console.log("Failing skills:");
238
+ for (const skill of report.skills.filter((s) => s.status === "fail")) {
239
+ console.log(" - " + skill.name);
240
+ for (const error of skill.errors) {
241
+ console.log(" " + error);
242
+ }
243
+ }
244
+ console.log("");
245
+ }
246
+
247
+ // List skills with warnings
248
+ if (report.warningsOnly > 0) {
249
+ console.log("Skills with warnings:");
250
+ for (const skill of report.skills.filter((s) => s.status === "warning")) {
251
+ console.log(" - " + skill.name);
252
+ for (const warning of skill.warnings) {
253
+ console.log(" " + warning);
254
+ }
255
+ }
256
+ console.log("");
257
+ }
258
+
259
+ // Write reports
260
+ const reportDir = path.join(process.cwd(), "reports");
261
+ if (!fs.existsSync(reportDir)) {
262
+ fs.mkdirSync(reportDir, { recursive: true });
263
+ }
264
+
265
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
266
+ const mdReportPath = path.join(reportDir, "skill-verification-" + timestamp + ".md");
267
+ const jsonReportPath = path.join(reportDir, "skill-verification-" + timestamp + ".json");
268
+ const latestMdPath = path.join(reportDir, "skill-verification-latest.md");
269
+ const latestJsonPath = path.join(reportDir, "skill-verification-latest.json");
270
+
271
+ fs.writeFileSync(mdReportPath, generateMarkdownReport(report));
272
+ fs.writeFileSync(jsonReportPath, generateJsonReport(report));
273
+ fs.writeFileSync(latestMdPath, generateMarkdownReport(report));
274
+ fs.writeFileSync(latestJsonPath, generateJsonReport(report));
275
+
276
+ console.log("Reports written to:");
277
+ console.log(" - " + mdReportPath);
278
+ console.log(" - " + jsonReportPath);
279
+ console.log(" - " + latestMdPath + " (latest)");
280
+ console.log(" - " + latestJsonPath + " (latest)");
281
+
282
+ // Exit with appropriate code
283
+ if (report.failed > 0) {
284
+ process.exit(1);
285
+ } else if (report.warningsOnly > 0) {
286
+ process.exit(2);
287
+ }
288
+ process.exit(0);
289
+ }
290
+
291
+ main().catch((err) => {
292
+ console.error("Fatal error:", err);
293
+ process.exit(1);
294
+ });
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CI gate: detect stale dist/index.mjs vs src/.
4
+ *
5
+ * Compares the newest mtime in src/ against dist/index.mjs mtime. If
6
+ * src/ is newer, the bundle is stale (someone edited source without
7
+ * rebuilding). Exits non-zero in that case.
8
+ *
9
+ * Why: in v0.9.17 the entrypoint prefers the bundle (with strip-types
10
+ * fallback). Stale bundles silently run old code — see Phase 5 H2
11
+ * investigation (2026-07-01) for the regression risk.
12
+ *
13
+ * Skips if dist/index.mjs does not exist (the strip-types fallback path
14
+ * is fine and the build:bundle automation will produce it).
15
+ *
16
+ * Exits:
17
+ * 0 — dist is fresh OR absent
18
+ * 1 — dist is stale (run `npm run build:bundle`)
19
+ */
20
+
21
+ import { execSync } from "node:child_process";
22
+ import { existsSync, statSync } from "node:fs";
23
+
24
+ const distPath = "dist/index.mjs";
25
+ if (!existsSync(distPath)) {
26
+ console.log("[check-bundle-staleness] dist/index.mjs absent — strip-types fallback will be used. OK.");
27
+ process.exit(0);
28
+ }
29
+
30
+ const distMtimeMs = statSync(distPath).mtimeMs;
31
+
32
+ // `git ls-files -o -- src` lists untracked files in src/. We want every
33
+ // tracked file's mtime plus the newest untracked one. Cheap heuristic:
34
+ // use `find` style by asking git for the full list, then statSync each.
35
+ let trackedFiles;
36
+ try {
37
+ trackedFiles = execSync(`git ls-files src`, { encoding: "utf-8" })
38
+ .split("\n")
39
+ .filter(Boolean);
40
+ } catch {
41
+ // Not a git repo (e.g. npm pack test) — skip.
42
+ console.log("[check-bundle-staleness] not a git repo; skipping staleness check.");
43
+ process.exit(0);
44
+ }
45
+
46
+ let newestMtimeMs = 0;
47
+ let newestFile = "";
48
+ for (const f of trackedFiles) {
49
+ try {
50
+ const mt = statSync(f).mtimeMs;
51
+ if (mt > newestMtimeMs) {
52
+ newestMtimeMs = mt;
53
+ newestFile = f;
54
+ }
55
+ } catch {
56
+ // File listed but missing on disk — ignore.
57
+ }
58
+ }
59
+
60
+ // Add untracked .ts files in src/ (devs editing a new file)
61
+ try {
62
+ const untracked = execSync(`git ls-files -o --exclude-standard src`, { encoding: "utf-8" })
63
+ .split("\n")
64
+ .filter(Boolean);
65
+ for (const f of untracked) {
66
+ if (!f.endsWith(".ts")) continue;
67
+ try {
68
+ const mt = statSync(f).mtimeMs;
69
+ if (mt > newestMtimeMs) {
70
+ newestMtimeMs = mt;
71
+ newestFile = f;
72
+ }
73
+ } catch {
74
+ // ignore
75
+ }
76
+ }
77
+ } catch {
78
+ // git may fail; ignore
79
+ }
80
+
81
+ if (newestMtimeMs === 0) {
82
+ console.log("[check-bundle-staleness] no src/*.ts files found; skipping.");
83
+ process.exit(0);
84
+ }
85
+
86
+ if (newestMtimeMs > distMtimeMs) {
87
+ console.error(
88
+ `[check-bundle-staleness] FAIL: dist/index.mjs is stale.\n` +
89
+ ` Newest src file: ${newestFile} (mtime=${newestMtimeMs.toFixed(0)})\n` +
90
+ ` dist/index.mjs mtime: ${distMtimeMs.toFixed(0)}\n` +
91
+ ` → run \`npm run build:bundle\` to refresh.`,
92
+ );
93
+ process.exit(1);
94
+ }
95
+
96
+ const ageSec = (distMtimeMs - newestMtimeMs) / 1000;
97
+ console.log(`[check-bundle-staleness] OK: dist/index.mjs is ${ageSec.toFixed(1)}s newer than newest src/ file.`);
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CI gate: detect unresolved git merge conflict markers in source.
4
+ *
5
+ * Scans tracked source files for `<<<<<<<`, `=======` (lone), and
6
+ * `>>>>>>>` patterns that indicate a botched merge. Fails CI if any
7
+ * are found so they can't reach the bundle (which esbuild would
8
+ * choke on with a parse error).
9
+ *
10
+ * History: 2026-07-01 CI #28498831579 (Ubuntu) failed because
11
+ * src/runtime/child-pi.ts and src/extension/pi-api.ts shipped with
12
+ * unresolved conflict markers left over from earlier stash/pop cycles
13
+ * during Phase 2-5 implementation. The esbuild transformer in the
14
+ * test runner hit `Expected identifier but found "<<"` at
15
+ * src/runtime/child-pi.ts:1073 — a syntax error caused by the markers.
16
+ *
17
+ * Scope:
18
+ * - Tracks files in src/, test/, scripts/, workflows/, teams/,
19
+ * agents/, themes/ — anywhere a conflict would land.
20
+ * - Excludes test/unit/conflict-detect.test.ts which INTENTIONALLY
21
+ * contains `<<<<<<< HEAD` etc. as test fixtures (test the
22
+ detector itself).
23
+ * - Detects both conflict-style (`<<<<<<<`, `=======`, `>>>>>>>`)
24
+ * and diff3-style (`|||||||`) markers.
25
+ *
26
+ * Exits:
27
+ * 0 — clean (no markers found, or only allowed locations)
28
+ * 1 — markers found (with file:line listing)
29
+ */
30
+
31
+ import { execSync } from "node:child_process";
32
+ import { readFileSync } from "node:fs";
33
+
34
+ const MARKER_PATTERNS = [
35
+ /^<<<<<<< /m, // ours/incoming/conflict markers
36
+ /^=======$/m, // conflict separator (lone, not bash =======)
37
+ /^>>>>>>> /m, // theirs/outgoing markers
38
+ /^\|\|\|\|\|\|\| /m, // diff3 base marker
39
+ ];
40
+
41
+ const SCAN_PATHS = ["src/", "test/", "scripts/", "workflows/", "teams/", "agents/", "themes/"];
42
+ const EXCLUDE_FILES = new Set([
43
+ // Intentionally contains conflict markers as test fixtures.
44
+ "test/unit/conflict-detect.test.ts",
45
+ ]);
46
+
47
+ function listTrackedFiles() {
48
+ try {
49
+ const all = execSync("git ls-files", { encoding: "utf-8" });
50
+ return all
51
+ .split("\n")
52
+ .filter(Boolean)
53
+ .filter((f) => SCAN_PATHS.some((p) => f.startsWith(p) || f === p.replace(/\/$/, "")))
54
+ .filter((f) => !EXCLUDE_FILES.has(f));
55
+ } catch {
56
+ console.error("[conflict-markers] not a git repo; skipping check.");
57
+ process.exit(0);
58
+ }
59
+ }
60
+
61
+ const files = listTrackedFiles();
62
+ const offenders = [];
63
+
64
+ for (const file of files) {
65
+ let content;
66
+ try {
67
+ content = readFileSync(file, "utf-8");
68
+ } catch {
69
+ // File listed but unreadable — skip silently.
70
+ continue;
71
+ }
72
+ const lines = content.split(/\r?\n/);
73
+ for (let i = 0; i < lines.length; i += 1) {
74
+ const line = lines[i];
75
+ if (MARKER_PATTERNS.some((re) => re.test(line))) {
76
+ offenders.push({ file, line: i + 1, text: line });
77
+ }
78
+ }
79
+ }
80
+
81
+ if (offenders.length === 0) {
82
+ console.log(`[conflict-markers] OK: scanned ${files.length} files, no markers found.`);
83
+ process.exit(0);
84
+ }
85
+
86
+ console.error(`[conflict-markers] FAIL: ${offenders.length} conflict marker(s) found.`);
87
+ for (const o of offenders) {
88
+ console.error(` ${o.file}:${o.line}: ${o.text.trim().slice(0, 80)}`);
89
+ }
90
+ console.error("\nResolve with: git checkout --ours|--theirs or git add ... after manual edit.");
91
+ process.exit(1);
@@ -0,0 +1,28 @@
1
+ import { execSync } from "node:child_process";
2
+ import { readFileSync } from "node:fs";
3
+
4
+ const out = execSync(
5
+ `git grep -nE "await import\\(" -- "src/**/*.ts"`,
6
+ { encoding: "utf-8" },
7
+ );
8
+
9
+ const bad = [];
10
+ const fileCache = new Map();
11
+
12
+ for (const line of out.split("\n").filter(Boolean)) {
13
+ if (line.includes("// LAZY:")) continue;
14
+ const m = line.match(/^([^:]+):(\d+):/);
15
+ if (!m) continue;
16
+ const [, file, lineNum] = m;
17
+ if (!fileCache.has(file)) fileCache.set(file, readFileSync(file, "utf-8").split(/\r?\n/));
18
+ const lines = fileCache.get(file);
19
+ const prevLine = lines[Number(lineNum) - 2] ?? "";
20
+ if (!prevLine.includes("// LAZY:")) bad.push(line);
21
+ }
22
+
23
+ if (bad.length > 0) {
24
+ console.error("Dynamic imports without `// LAZY:` marker:\n" + bad.join("\n"));
25
+ process.exit(1);
26
+ }
27
+
28
+ console.log("All dynamic imports have `// LAZY:` marker.");
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Installs the bundled crew-vibes.ttf (PUA glyphs U+E700..U+E70A) into the
4
+ * user fonts directory so the crew-vibes speed + capacity figures render
5
+ * instead of tofu boxes. Cross-platform mirror of pi-speeed's install-font.
6
+ *
7
+ * macOS ~/Library/Fonts/crew-vibes.ttf
8
+ * Linux ~/.local/share/fonts/crew-vibes.ttf (+ fc-cache -f)
9
+ * Windows %LOCALAPPDATA%\Microsoft\Windows\Fonts\crew-vibes.ttf (per-user)
10
+ *
11
+ * Invoked by the npm `postinstall` hook and by `npm run install:crew-font`.
12
+ * Best-effort: never fails the install.
13
+ */
14
+ import { spawnSync } from "node:child_process";
15
+ import { copyFileSync, existsSync, mkdirSync } from "node:fs";
16
+ import { homedir, platform } from "node:os";
17
+ import { dirname, join } from "node:path";
18
+ import { fileURLToPath } from "node:url";
19
+
20
+ const __dirname = dirname(fileURLToPath(import.meta.url));
21
+ const source = join(__dirname, "..", "assets", "crew-vibes.ttf");
22
+ const FONT_NAME = "crew-vibes.ttf";
23
+
24
+ function log(message) {
25
+ console.log(`[pi-crew] ${message}`);
26
+ }
27
+
28
+ function installFont() {
29
+ if (!existsSync(source)) {
30
+ log(`crew-vibes font missing: ${source}`);
31
+ return;
32
+ }
33
+
34
+ const os = platform();
35
+ let targetDir;
36
+
37
+ if (os === "darwin") {
38
+ targetDir = join(homedir(), "Library", "Fonts");
39
+ } else if (os === "linux") {
40
+ targetDir = join(homedir(), ".local", "share", "fonts");
41
+ } else if (os === "win32") {
42
+ const localAppData = process.env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local");
43
+ targetDir = join(localAppData, "Microsoft", "Windows", "Fonts");
44
+ } else {
45
+ log(`Skipping font install on unsupported platform: ${os}`);
46
+ return;
47
+ }
48
+
49
+ mkdirSync(targetDir, { recursive: true });
50
+ const target = join(targetDir, FONT_NAME);
51
+ copyFileSync(source, target);
52
+ log(`Installed crew-vibes font to ${target}`);
53
+
54
+ if (os === "linux") {
55
+ // Refresh fontconfig cache so newly installed font is immediately
56
+ // available to all terminals. -fv for verbose + forced rebuild.
57
+ const result = spawnSync("fc-cache", ["-fv", targetDir], { stdio: "ignore" });
58
+ if (result.status === 0) log("Refreshed font cache (fc-cache -fv)");
59
+ else log("fontconfig fc-cache not available; restart terminal if font is not visible");
60
+
61
+ // Verify font is actually indexed by fontconfig
62
+ const verify = spawnSync("fc-match", [":charset=E700"], { encoding: "utf8" });
63
+ if (verify.stdout && verify.stdout.includes(FONT_NAME)) {
64
+ log("Font verified: fc-match U+E700 -> crew-vibes.ttf");
65
+ } else {
66
+ log("WARNING: fc-match did not resolve U+E700 to crew-vibes.ttf");
67
+ log(" Run: fc-cache -fv " + targetDir);
68
+ log(" Then restart your terminal.");
69
+ }
70
+ }
71
+
72
+ if (os === "win32") {
73
+ // Register the per-user font in HKCU so Windows Terminal and other
74
+ // apps pick it up for PUA glyph fallback. Copying alone is not enough.
75
+ const regValue = "Crew Vibes (TrueType)";
76
+ const regResult = spawnSync("reg", ["add", "HKCU\\Software\\Microsoft\\Windows NT\\CurrentVersion\\Fonts", "/v", regValue, "/t", "REG_SZ", "/d", target, "/f"], { stdio: "ignore" });
77
+ if (regResult.status === 0) log("Registered crew-vibes font in Windows registry (HKCU)");
78
+ else log("Windows registry registration skipped (reg.exe unavailable); glyphs may not render until font is installed via Settings");
79
+ }
80
+
81
+ if (os === "darwin" || os === "win32") {
82
+ log("Restart your terminal (or select a Crew-Vibes-capable font) if glyphs still show as boxes");
83
+ }
84
+ }
85
+
86
+ try {
87
+ installFont();
88
+ } catch (error) {
89
+ log(`Font install failed: ${error instanceof Error ? error.message : String(error)}`);
90
+ process.exitCode = 0;
91
+ }
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Cross-platform postinstall orchestrator.
4
+ *
5
+ * 1. Build the ESM bundle (best-effort; on failure we log a fallback and
6
+ * let Pi fall back to strip-types loading).
7
+ * 2. Install the bundled crew-vibes.ttf into the user fonts directory so
8
+ * the crew-vibes speed/capacity PUA glyphs render.
9
+ *
10
+ * Replaces the old `postinstall` shell chain so the font install runs on
11
+ * every platform without relying on shell-specific chaining (`;`/`&&`).
12
+ */
13
+ import { existsSync } from "node:fs";
14
+ import { spawnSync } from "node:child_process";
15
+ import { dirname, join } from "node:path";
16
+ import { fileURLToPath } from "node:url";
17
+
18
+ const __dirname = dirname(fileURLToPath(import.meta.url));
19
+ const root = join(__dirname, "..");
20
+
21
+ function run(scriptRel) {
22
+ const abs = join(root, scriptRel);
23
+ if (!existsSync(abs)) return 1;
24
+ const result = spawnSync(process.execPath, [abs], { stdio: "inherit" });
25
+ return result.status ?? 1;
26
+ }
27
+
28
+ function main() {
29
+ try {
30
+ // Dev clones ship scripts/build-bundle.mjs and devDeps (esbuild) so the
31
+ // bundle rebuilds; published packages omit both and rely on committed
32
+ // dist/index.mjs, so this best-effort build simply no-ops.
33
+ const bundleStatus = run("scripts/build-bundle.mjs");
34
+ if (bundleStatus !== 0) {
35
+ console.warn(
36
+ "[pi-crew] postinstall: bundle build skipped or failed; using committed dist/ (or strip-types fallback). Run npm run build:bundle to retry.",
37
+ );
38
+ }
39
+ // Font install is best-effort and must never fail the install.
40
+ run("scripts/install-crew-vibes-font.mjs");
41
+ } catch (err) {
42
+ // Postinstall must NEVER fail the install (SEC-M2).
43
+ console.warn("[pi-crew] postinstall: best-effort step failed:", err instanceof Error ? err.message : err);
44
+ }
45
+ }
46
+
47
+ main();