@coldtea/abide 0.0.1
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/LICENSE +21 -0
- package/README.md +10 -0
- package/dist/abide-hook.d.ts +2 -0
- package/dist/abide-hook.d.ts.map +1 -0
- package/dist/abide-hook.js +23 -0
- package/dist/abide-hook.js.map +1 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +64 -0
- package/dist/bin.js.map +1 -0
- package/dist/commands/audit.d.ts +3 -0
- package/dist/commands/audit.d.ts.map +1 -0
- package/dist/commands/audit.js +79 -0
- package/dist/commands/audit.js.map +1 -0
- package/dist/commands/bench.d.ts +3 -0
- package/dist/commands/bench.d.ts.map +1 -0
- package/dist/commands/bench.js +206 -0
- package/dist/commands/bench.js.map +1 -0
- package/dist/commands/calibrate.d.ts +3 -0
- package/dist/commands/calibrate.d.ts.map +1 -0
- package/dist/commands/calibrate.js +159 -0
- package/dist/commands/calibrate.js.map +1 -0
- package/dist/commands/check.d.ts +3 -0
- package/dist/commands/check.d.ts.map +1 -0
- package/dist/commands/check.js +106 -0
- package/dist/commands/check.js.map +1 -0
- package/dist/commands/compile.d.ts +3 -0
- package/dist/commands/compile.d.ts.map +1 -0
- package/dist/commands/compile.js +116 -0
- package/dist/commands/compile.js.map +1 -0
- package/dist/commands/init.d.ts +4 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +93 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/login.d.ts +3 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +58 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/replay.d.ts +4 -0
- package/dist/commands/replay.d.ts.map +1 -0
- package/dist/commands/replay.js +118 -0
- package/dist/commands/replay.js.map +1 -0
- package/dist/commands/report.d.ts +5 -0
- package/dist/commands/report.d.ts.map +1 -0
- package/dist/commands/report.js +78 -0
- package/dist/commands/report.js.map +1 -0
- package/dist/commands/rubric.d.ts +3 -0
- package/dist/commands/rubric.d.ts.map +1 -0
- package/dist/commands/rubric.js +40 -0
- package/dist/commands/rubric.js.map +1 -0
- package/dist/commands/uninstall.d.ts +2 -0
- package/dist/commands/uninstall.d.ts.map +1 -0
- package/dist/commands/uninstall.js +31 -0
- package/dist/commands/uninstall.js.map +1 -0
- package/dist/hooks/postToolUse.d.ts +3 -0
- package/dist/hooks/postToolUse.d.ts.map +1 -0
- package/dist/hooks/postToolUse.js +145 -0
- package/dist/hooks/postToolUse.js.map +1 -0
- package/dist/hooks/sessionStart.d.ts +11 -0
- package/dist/hooks/sessionStart.d.ts.map +1 -0
- package/dist/hooks/sessionStart.js +83 -0
- package/dist/hooks/sessionStart.js.map +1 -0
- package/dist/hooks/stop.d.ts +26 -0
- package/dist/hooks/stop.d.ts.map +1 -0
- package/dist/hooks/stop.js +227 -0
- package/dist/hooks/stop.js.map +1 -0
- package/dist/hooks/turnStart.d.ts +8 -0
- package/dist/hooks/turnStart.d.ts.map +1 -0
- package/dist/hooks/turnStart.js +40 -0
- package/dist/hooks/turnStart.js.map +1 -0
- package/dist/lib/applyPatch.d.ts +22 -0
- package/dist/lib/applyPatch.d.ts.map +1 -0
- package/dist/lib/applyPatch.js +63 -0
- package/dist/lib/applyPatch.js.map +1 -0
- package/dist/lib/audit.d.ts +40 -0
- package/dist/lib/audit.d.ts.map +1 -0
- package/dist/lib/audit.js +215 -0
- package/dist/lib/audit.js.map +1 -0
- package/dist/lib/band.d.ts +20 -0
- package/dist/lib/band.d.ts.map +1 -0
- package/dist/lib/band.js +40 -0
- package/dist/lib/band.js.map +1 -0
- package/dist/lib/calibration.d.ts +13 -0
- package/dist/lib/calibration.d.ts.map +1 -0
- package/dist/lib/calibration.js +25 -0
- package/dist/lib/calibration.js.map +1 -0
- package/dist/lib/checkRunner.d.ts +34 -0
- package/dist/lib/checkRunner.d.ts.map +1 -0
- package/dist/lib/checkRunner.js +76 -0
- package/dist/lib/checkRunner.js.map +1 -0
- package/dist/lib/claude.d.ts +4 -0
- package/dist/lib/claude.d.ts.map +1 -0
- package/dist/lib/claude.js +28 -0
- package/dist/lib/claude.js.map +1 -0
- package/dist/lib/compilePrompt.d.ts +22 -0
- package/dist/lib/compilePrompt.d.ts.map +1 -0
- package/dist/lib/compilePrompt.js +33 -0
- package/dist/lib/compilePrompt.js.map +1 -0
- package/dist/lib/constants.d.ts +33 -0
- package/dist/lib/constants.d.ts.map +1 -0
- package/dist/lib/constants.js +33 -0
- package/dist/lib/constants.js.map +1 -0
- package/dist/lib/coverage.d.ts +7 -0
- package/dist/lib/coverage.d.ts.map +1 -0
- package/dist/lib/coverage.js +17 -0
- package/dist/lib/coverage.js.map +1 -0
- package/dist/lib/credentials.d.ts +27 -0
- package/dist/lib/credentials.d.ts.map +1 -0
- package/dist/lib/credentials.js +89 -0
- package/dist/lib/credentials.js.map +1 -0
- package/dist/lib/diff.d.ts +30 -0
- package/dist/lib/diff.d.ts.map +1 -0
- package/dist/lib/diff.js +139 -0
- package/dist/lib/diff.js.map +1 -0
- package/dist/lib/events.d.ts +5 -0
- package/dist/lib/events.d.ts.map +1 -0
- package/dist/lib/events.js +38 -0
- package/dist/lib/events.js.map +1 -0
- package/dist/lib/git.d.ts +37 -0
- package/dist/lib/git.d.ts.map +1 -0
- package/dist/lib/git.js +163 -0
- package/dist/lib/git.js.map +1 -0
- package/dist/lib/hookRunner.d.ts +10 -0
- package/dist/lib/hookRunner.d.ts.map +1 -0
- package/dist/lib/hookRunner.js +52 -0
- package/dist/lib/hookRunner.js.map +1 -0
- package/dist/lib/hosts.d.ts +20 -0
- package/dist/lib/hosts.d.ts.map +1 -0
- package/dist/lib/hosts.js +101 -0
- package/dist/lib/hosts.js.map +1 -0
- package/dist/lib/jev.d.ts +24 -0
- package/dist/lib/jev.d.ts.map +1 -0
- package/dist/lib/jev.js +169 -0
- package/dist/lib/jev.js.map +1 -0
- package/dist/lib/lintConfig.d.ts +3 -0
- package/dist/lib/lintConfig.d.ts.map +1 -0
- package/dist/lib/lintConfig.js +55 -0
- package/dist/lib/lintConfig.js.map +1 -0
- package/dist/lib/loadRules.d.ts +12 -0
- package/dist/lib/loadRules.d.ts.map +1 -0
- package/dist/lib/loadRules.js +31 -0
- package/dist/lib/loadRules.js.map +1 -0
- package/dist/lib/opencodePlugin.d.ts +10 -0
- package/dist/lib/opencodePlugin.d.ts.map +1 -0
- package/dist/lib/opencodePlugin.js +36 -0
- package/dist/lib/opencodePlugin.js.map +1 -0
- package/dist/lib/output.d.ts +6 -0
- package/dist/lib/output.d.ts.map +1 -0
- package/dist/lib/output.js +35 -0
- package/dist/lib/output.js.map +1 -0
- package/dist/lib/packageRoot.d.ts +12 -0
- package/dist/lib/packageRoot.d.ts.map +1 -0
- package/dist/lib/packageRoot.js +50 -0
- package/dist/lib/packageRoot.js.map +1 -0
- package/dist/lib/paths.d.ts +24 -0
- package/dist/lib/paths.d.ts.map +1 -0
- package/dist/lib/paths.js +75 -0
- package/dist/lib/paths.js.map +1 -0
- package/dist/lib/reason.d.ts +11 -0
- package/dist/lib/reason.d.ts.map +1 -0
- package/dist/lib/reason.js +31 -0
- package/dist/lib/reason.js.map +1 -0
- package/dist/lib/regularFile.d.ts +26 -0
- package/dist/lib/regularFile.d.ts.map +1 -0
- package/dist/lib/regularFile.js +77 -0
- package/dist/lib/regularFile.js.map +1 -0
- package/dist/lib/replay.d.ts +74 -0
- package/dist/lib/replay.d.ts.map +1 -0
- package/dist/lib/replay.js +250 -0
- package/dist/lib/replay.js.map +1 -0
- package/dist/lib/replayCodex.d.ts +6 -0
- package/dist/lib/replayCodex.d.ts.map +1 -0
- package/dist/lib/replayCodex.js +132 -0
- package/dist/lib/replayCodex.js.map +1 -0
- package/dist/lib/replayOpencode.d.ts +15 -0
- package/dist/lib/replayOpencode.d.ts.map +1 -0
- package/dist/lib/replayOpencode.js +154 -0
- package/dist/lib/replayOpencode.js.map +1 -0
- package/dist/lib/rubricFile.d.ts +31 -0
- package/dist/lib/rubricFile.d.ts.map +1 -0
- package/dist/lib/rubricFile.js +75 -0
- package/dist/lib/rubricFile.js.map +1 -0
- package/dist/lib/scope.d.ts +4 -0
- package/dist/lib/scope.d.ts.map +1 -0
- package/dist/lib/scope.js +14 -0
- package/dist/lib/scope.js.map +1 -0
- package/dist/lib/session.d.ts +48 -0
- package/dist/lib/session.d.ts.map +1 -0
- package/dist/lib/session.js +182 -0
- package/dist/lib/session.js.map +1 -0
- package/dist/lib/settings.d.ts +26 -0
- package/dist/lib/settings.d.ts.map +1 -0
- package/dist/lib/settings.js +104 -0
- package/dist/lib/settings.js.map +1 -0
- package/dist/lib/sources.d.ts +28 -0
- package/dist/lib/sources.d.ts.map +1 -0
- package/dist/lib/sources.js +137 -0
- package/dist/lib/sources.js.map +1 -0
- package/dist/lib/stdin.d.ts +3 -0
- package/dist/lib/stdin.d.ts.map +1 -0
- package/dist/lib/stdin.js +24 -0
- package/dist/lib/stdin.js.map +1 -0
- package/dist/lib/transcript.d.ts +3 -0
- package/dist/lib/transcript.d.ts.map +1 -0
- package/dist/lib/transcript.js +57 -0
- package/dist/lib/transcript.js.map +1 -0
- package/dist/lib/ui.d.ts +8 -0
- package/dist/lib/ui.d.ts.map +1 -0
- package/dist/lib/ui.js +34 -0
- package/dist/lib/ui.js.map +1 -0
- package/dist/ui/components/Buckets.d.ts +6 -0
- package/dist/ui/components/Buckets.d.ts.map +1 -0
- package/dist/ui/components/Buckets.js +11 -0
- package/dist/ui/components/Buckets.js.map +1 -0
- package/dist/ui/components/Callout.d.ts +11 -0
- package/dist/ui/components/Callout.d.ts.map +1 -0
- package/dist/ui/components/Callout.js +10 -0
- package/dist/ui/components/Callout.js.map +1 -0
- package/dist/ui/components/Checklist.d.ts +10 -0
- package/dist/ui/components/Checklist.d.ts.map +1 -0
- package/dist/ui/components/Checklist.js +8 -0
- package/dist/ui/components/Checklist.js.map +1 -0
- package/dist/ui/components/Columns.d.ts +28 -0
- package/dist/ui/components/Columns.d.ts.map +1 -0
- package/dist/ui/components/Columns.js +20 -0
- package/dist/ui/components/Columns.js.map +1 -0
- package/dist/ui/components/Header.d.ts +9 -0
- package/dist/ui/components/Header.d.ts.map +1 -0
- package/dist/ui/components/Header.js +7 -0
- package/dist/ui/components/Header.js.map +1 -0
- package/dist/ui/components/Live.d.ts +16 -0
- package/dist/ui/components/Live.d.ts.map +1 -0
- package/dist/ui/components/Live.js +40 -0
- package/dist/ui/components/Live.js.map +1 -0
- package/dist/ui/components/RuleTable.d.ts +15 -0
- package/dist/ui/components/RuleTable.d.ts.map +1 -0
- package/dist/ui/components/RuleTable.js +81 -0
- package/dist/ui/components/RuleTable.js.map +1 -0
- package/dist/ui/components/Section.d.ts +10 -0
- package/dist/ui/components/Section.d.ts.map +1 -0
- package/dist/ui/components/Section.js +8 -0
- package/dist/ui/components/Section.js.map +1 -0
- package/dist/ui/components/Verdicts.d.ts +9 -0
- package/dist/ui/components/Verdicts.d.ts.map +1 -0
- package/dist/ui/components/Verdicts.js +12 -0
- package/dist/ui/components/Verdicts.js.map +1 -0
- package/dist/ui/render.d.ts +20 -0
- package/dist/ui/render.d.ts.map +1 -0
- package/dist/ui/render.js +32 -0
- package/dist/ui/render.js.map +1 -0
- package/dist/ui/theme.d.ts +37 -0
- package/dist/ui/theme.d.ts.map +1 -0
- package/dist/ui/theme.js +63 -0
- package/dist/ui/theme.js.map +1 -0
- package/dist/ui/views/AuditView.d.ts +18 -0
- package/dist/ui/views/AuditView.d.ts.map +1 -0
- package/dist/ui/views/AuditView.js +48 -0
- package/dist/ui/views/AuditView.js.map +1 -0
- package/dist/ui/views/BenchView.d.ts +33 -0
- package/dist/ui/views/BenchView.d.ts.map +1 -0
- package/dist/ui/views/BenchView.js +19 -0
- package/dist/ui/views/BenchView.js.map +1 -0
- package/dist/ui/views/CalibrateView.d.ts +26 -0
- package/dist/ui/views/CalibrateView.d.ts.map +1 -0
- package/dist/ui/views/CalibrateView.js +42 -0
- package/dist/ui/views/CalibrateView.js.map +1 -0
- package/dist/ui/views/CheckView.d.ts +19 -0
- package/dist/ui/views/CheckView.d.ts.map +1 -0
- package/dist/ui/views/CheckView.js +15 -0
- package/dist/ui/views/CheckView.js.map +1 -0
- package/dist/ui/views/ErrorView.d.ts +4 -0
- package/dist/ui/views/ErrorView.d.ts.map +1 -0
- package/dist/ui/views/ErrorView.js +26 -0
- package/dist/ui/views/ErrorView.js.map +1 -0
- package/dist/ui/views/HelpView.d.ts +2 -0
- package/dist/ui/views/HelpView.d.ts.map +1 -0
- package/dist/ui/views/HelpView.js +25 -0
- package/dist/ui/views/HelpView.js.map +1 -0
- package/dist/ui/views/InitView.d.ts +27 -0
- package/dist/ui/views/InitView.d.ts.map +1 -0
- package/dist/ui/views/InitView.js +24 -0
- package/dist/ui/views/InitView.js.map +1 -0
- package/dist/ui/views/ReplayView.d.ts +16 -0
- package/dist/ui/views/ReplayView.d.ts.map +1 -0
- package/dist/ui/views/ReplayView.js +44 -0
- package/dist/ui/views/ReplayView.js.map +1 -0
- package/dist/ui/views/ReportView.d.ts +14 -0
- package/dist/ui/views/ReportView.d.ts.map +1 -0
- package/dist/ui/views/ReportView.js +86 -0
- package/dist/ui/views/ReportView.js.map +1 -0
- package/dist/ui/views/RubricView.d.ts +18 -0
- package/dist/ui/views/RubricView.d.ts.map +1 -0
- package/dist/ui/views/RubricView.js +18 -0
- package/dist/ui/views/RubricView.js.map +1 -0
- package/opencode/abide.mjs +246 -0
- package/package.json +50 -0
- package/skills/abide-compile/SKILL.md +173 -0
- package/src/abide-hook.ts +23 -0
- package/src/bin.ts +64 -0
- package/src/commands/audit.ts +97 -0
- package/src/commands/bench.ts +226 -0
- package/src/commands/calibrate.ts +165 -0
- package/src/commands/check.ts +114 -0
- package/src/commands/compile.ts +127 -0
- package/src/commands/init.ts +109 -0
- package/src/commands/login.ts +61 -0
- package/src/commands/replay.ts +148 -0
- package/src/commands/report.ts +83 -0
- package/src/commands/rubric.ts +45 -0
- package/src/commands/uninstall.ts +33 -0
- package/src/hooks/postToolUse.ts +171 -0
- package/src/hooks/sessionStart.ts +100 -0
- package/src/hooks/stop.ts +275 -0
- package/src/hooks/turnStart.ts +36 -0
- package/src/lib/applyPatch.ts +75 -0
- package/src/lib/audit.ts +265 -0
- package/src/lib/band.ts +49 -0
- package/src/lib/calibration.ts +35 -0
- package/src/lib/checkRunner.ts +124 -0
- package/src/lib/claude.ts +35 -0
- package/src/lib/compilePrompt.ts +67 -0
- package/src/lib/constants.ts +39 -0
- package/src/lib/coverage.ts +21 -0
- package/src/lib/credentials.ts +104 -0
- package/src/lib/diff.ts +209 -0
- package/src/lib/events.ts +34 -0
- package/src/lib/git.ts +206 -0
- package/src/lib/hookRunner.ts +55 -0
- package/src/lib/hosts.ts +112 -0
- package/src/lib/jev.ts +223 -0
- package/src/lib/lintConfig.ts +53 -0
- package/src/lib/loadRules.ts +39 -0
- package/src/lib/opencodePlugin.ts +38 -0
- package/src/lib/output.ts +44 -0
- package/src/lib/packageRoot.ts +48 -0
- package/src/lib/paths.ts +96 -0
- package/src/lib/reason.ts +46 -0
- package/src/lib/regularFile.ts +96 -0
- package/src/lib/replay.ts +314 -0
- package/src/lib/replayCodex.ts +130 -0
- package/src/lib/replayOpencode.ts +186 -0
- package/src/lib/rubricFile.ts +99 -0
- package/src/lib/scope.ts +18 -0
- package/src/lib/session.ts +223 -0
- package/src/lib/settings.ts +112 -0
- package/src/lib/sources.ts +154 -0
- package/src/lib/stdin.ts +23 -0
- package/src/lib/transcript.ts +49 -0
- package/src/lib/ui.ts +38 -0
- package/src/ui/components/Buckets.tsx +30 -0
- package/src/ui/components/Callout.tsx +26 -0
- package/src/ui/components/Checklist.tsx +29 -0
- package/src/ui/components/Columns.tsx +62 -0
- package/src/ui/components/Header.tsx +35 -0
- package/src/ui/components/Live.tsx +73 -0
- package/src/ui/components/RuleTable.tsx +94 -0
- package/src/ui/components/Section.tsx +24 -0
- package/src/ui/components/Verdicts.tsx +40 -0
- package/src/ui/render.tsx +51 -0
- package/src/ui/theme.ts +65 -0
- package/src/ui/views/AuditView.tsx +140 -0
- package/src/ui/views/BenchView.tsx +87 -0
- package/src/ui/views/CalibrateView.tsx +108 -0
- package/src/ui/views/CheckView.tsx +56 -0
- package/src/ui/views/ErrorView.tsx +32 -0
- package/src/ui/views/HelpView.tsx +51 -0
- package/src/ui/views/InitView.tsx +103 -0
- package/src/ui/views/ReplayView.tsx +100 -0
- package/src/ui/views/ReportView.tsx +225 -0
- package/src/ui/views/RubricView.tsx +82 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { AbideError } from "@coldtea/abide-schema";
|
|
5
|
+
|
|
6
|
+
const hookEntrySchema = z
|
|
7
|
+
.object({ type: z.string(), command: z.string().optional() })
|
|
8
|
+
.passthrough();
|
|
9
|
+
const hookGroupSchema = z
|
|
10
|
+
.object({ matcher: z.string().optional(), hooks: z.array(hookEntrySchema) })
|
|
11
|
+
.passthrough();
|
|
12
|
+
const settingsSchema = z
|
|
13
|
+
.object({ hooks: z.record(z.string(), z.array(hookGroupSchema)).optional() })
|
|
14
|
+
.passthrough();
|
|
15
|
+
type Settings = z.infer<typeof settingsSchema>;
|
|
16
|
+
type HookEntry = z.infer<typeof hookEntrySchema>;
|
|
17
|
+
type HookGroup = z.infer<typeof hookGroupSchema>;
|
|
18
|
+
|
|
19
|
+
export const ABIDE_HOOK_MARKER = "abide-hook.js";
|
|
20
|
+
|
|
21
|
+
export type HookEvent = "SessionStart" | "UserPromptSubmit" | "PostToolUse" | "Stop";
|
|
22
|
+
|
|
23
|
+
export type HookSpec = { event: HookEvent; matcher?: string; command: string; timeout: number };
|
|
24
|
+
|
|
25
|
+
export const hookSpecs = (hookScript: string): HookSpec[] => {
|
|
26
|
+
const cmd = (name: string): string => `node "${hookScript}" ${name}`;
|
|
27
|
+
return [
|
|
28
|
+
{ event: "SessionStart", command: cmd("session-start"), timeout: 10 },
|
|
29
|
+
{ event: "UserPromptSubmit", command: cmd("turn-start"), timeout: 10 },
|
|
30
|
+
{
|
|
31
|
+
event: "PostToolUse",
|
|
32
|
+
matcher: "Edit|Write|MultiEdit|apply_patch",
|
|
33
|
+
command: cmd("post-tool-use"),
|
|
34
|
+
timeout: 20,
|
|
35
|
+
},
|
|
36
|
+
{ event: "Stop", command: cmd("stop"), timeout: 30 },
|
|
37
|
+
];
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const isOurs = (entry: HookEntry): boolean => entry.command?.includes(ABIDE_HOOK_MARKER) ?? false;
|
|
41
|
+
|
|
42
|
+
/** A group shared with someone else's hook keeps theirs. */
|
|
43
|
+
const withoutOurs = (groups: readonly HookGroup[]): { kept: HookGroup[]; removed: number } => {
|
|
44
|
+
const kept: HookGroup[] = [];
|
|
45
|
+
let removed = 0;
|
|
46
|
+
for (const group of groups) {
|
|
47
|
+
const hooks = group.hooks.filter((h) => !isOurs(h));
|
|
48
|
+
removed += group.hooks.length - hooks.length;
|
|
49
|
+
if (hooks.length > 0) kept.push({ ...group, hooks });
|
|
50
|
+
}
|
|
51
|
+
return { kept, removed };
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const readSettings = (file: string): Settings => {
|
|
55
|
+
let raw: string;
|
|
56
|
+
try {
|
|
57
|
+
raw = readFileSync(file, "utf8");
|
|
58
|
+
} catch {
|
|
59
|
+
return {};
|
|
60
|
+
}
|
|
61
|
+
if (raw.trim() === "") return {};
|
|
62
|
+
try {
|
|
63
|
+
return settingsSchema.parse(JSON.parse(raw));
|
|
64
|
+
} catch (error) {
|
|
65
|
+
throw new AbideError("SETTINGS_INVALID", `${file} is not a settings file abide can edit`, {
|
|
66
|
+
cause: error,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const installHooks = (file: string, specs: readonly HookSpec[]): void => {
|
|
72
|
+
const settings = readSettings(file);
|
|
73
|
+
const hooks = { ...(settings.hooks ?? {}) };
|
|
74
|
+
for (const spec of specs) {
|
|
75
|
+
const { kept } = withoutOurs(hooks[spec.event] ?? []);
|
|
76
|
+
const group: HookGroup = {
|
|
77
|
+
...(spec.matcher === undefined ? {} : { matcher: spec.matcher }),
|
|
78
|
+
hooks: [{ type: "command", command: spec.command, timeout: spec.timeout }],
|
|
79
|
+
};
|
|
80
|
+
hooks[spec.event] = [...kept, group];
|
|
81
|
+
}
|
|
82
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
83
|
+
writeFileSync(file, `${JSON.stringify({ ...settings, hooks }, null, 2)}\n`);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const uninstallHooks = (file: string): number => {
|
|
87
|
+
const settings = readSettings(file);
|
|
88
|
+
if (settings.hooks === undefined) return 0;
|
|
89
|
+
let removed = 0;
|
|
90
|
+
const hooks: Record<string, HookGroup[]> = {};
|
|
91
|
+
for (const [event, groups] of Object.entries(settings.hooks)) {
|
|
92
|
+
const result = withoutOurs(groups);
|
|
93
|
+
removed += result.removed;
|
|
94
|
+
if (result.kept.length > 0) hooks[event] = result.kept;
|
|
95
|
+
}
|
|
96
|
+
writeFileSync(file, `${JSON.stringify({ ...settings, hooks }, null, 2)}\n`);
|
|
97
|
+
return removed;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const installedHookEvents = (file: string): HookEvent[] => {
|
|
101
|
+
const settings = readSettings(file);
|
|
102
|
+
const events: HookEvent[] = [];
|
|
103
|
+
for (const event of [
|
|
104
|
+
"SessionStart",
|
|
105
|
+
"UserPromptSubmit",
|
|
106
|
+
"PostToolUse",
|
|
107
|
+
"Stop",
|
|
108
|
+
] satisfies HookEvent[]) {
|
|
109
|
+
if ((settings.hooks?.[event] ?? []).some((g) => g.hooks.some(isOurs))) events.push(event);
|
|
110
|
+
}
|
|
111
|
+
return events;
|
|
112
|
+
};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { readdirSync, existsSync, type Dirent } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createSourceSha, type Rubric } from "@coldtea/abide-schema";
|
|
4
|
+
import { homeDir, resolveSourcePath, toSourcePath } from "./paths.js";
|
|
5
|
+
import { readRegularFile } from "./regularFile.js";
|
|
6
|
+
|
|
7
|
+
export type SourceCandidate = {
|
|
8
|
+
/** Rubric spelling: repo-relative or "~/...". */
|
|
9
|
+
path: string;
|
|
10
|
+
absolute: string;
|
|
11
|
+
/** Glob the file's rules apply to. */
|
|
12
|
+
scope: string;
|
|
13
|
+
/** Required candidates must appear in the rubric for it to be fresh. */
|
|
14
|
+
required: boolean;
|
|
15
|
+
origin: "root" | "nested" | "global" | "contributing";
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const ROOT_NAMES = ["AGENTS.md", "CLAUDE.md", ".cursorrules"];
|
|
19
|
+
const NESTED_NAMES = ["AGENTS.md", "CLAUDE.md"];
|
|
20
|
+
const GLOBAL_NAMES = ["~/.claude/CLAUDE.md", "~/.codex/AGENTS.md", "~/.config/opencode/AGENTS.md"];
|
|
21
|
+
const SKIP_DIRS = new Set([
|
|
22
|
+
"node_modules",
|
|
23
|
+
".git",
|
|
24
|
+
"dist",
|
|
25
|
+
"build",
|
|
26
|
+
"out",
|
|
27
|
+
".next",
|
|
28
|
+
"vendor",
|
|
29
|
+
"coverage",
|
|
30
|
+
".turbo",
|
|
31
|
+
".cache",
|
|
32
|
+
"target",
|
|
33
|
+
".abide",
|
|
34
|
+
".claude",
|
|
35
|
+
".codex",
|
|
36
|
+
".opencode",
|
|
37
|
+
]);
|
|
38
|
+
const MAX_DEPTH = 6;
|
|
39
|
+
|
|
40
|
+
const walkNested = (root: string, dir: string, depth: number, out: SourceCandidate[]): void => {
|
|
41
|
+
if (depth > MAX_DEPTH) return;
|
|
42
|
+
let entries: Dirent[];
|
|
43
|
+
try {
|
|
44
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
45
|
+
} catch {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
for (const entry of entries) {
|
|
49
|
+
if (!entry.isDirectory() || SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
50
|
+
const sub = path.join(dir, entry.name);
|
|
51
|
+
for (const name of NESTED_NAMES) {
|
|
52
|
+
const file = path.join(sub, name);
|
|
53
|
+
if (existsSync(file)) {
|
|
54
|
+
const rel = toSourcePath(root, sub);
|
|
55
|
+
out.push({
|
|
56
|
+
path: toSourcePath(root, file),
|
|
57
|
+
absolute: file,
|
|
58
|
+
scope: `${rel}/**/*`,
|
|
59
|
+
required: true,
|
|
60
|
+
origin: "nested",
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
walkNested(root, sub, depth + 1, out);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const discoverProjectSources = (root: string): SourceCandidate[] => {
|
|
69
|
+
const found: SourceCandidate[] = [];
|
|
70
|
+
for (const name of ROOT_NAMES) {
|
|
71
|
+
const file = path.join(root, name);
|
|
72
|
+
if (existsSync(file)) {
|
|
73
|
+
found.push({ path: name, absolute: file, scope: "**/*", required: true, origin: "root" });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
walkNested(root, root, 1, found);
|
|
77
|
+
const contributing = path.join(root, "CONTRIBUTING.md");
|
|
78
|
+
if (existsSync(contributing)) {
|
|
79
|
+
found.push({
|
|
80
|
+
path: "CONTRIBUTING.md",
|
|
81
|
+
absolute: contributing,
|
|
82
|
+
scope: "**/*",
|
|
83
|
+
required: false,
|
|
84
|
+
origin: "contributing",
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return found;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export const discoverGlobalSources = (): SourceCandidate[] =>
|
|
91
|
+
GLOBAL_NAMES.flatMap((p) => {
|
|
92
|
+
const absolute = path.join(homeDir(), p.slice(2));
|
|
93
|
+
return existsSync(absolute)
|
|
94
|
+
? [{ path: p, absolute, scope: "**/*", required: true, origin: "global" as const }]
|
|
95
|
+
: [];
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
export const hashFile = (absolute: string): string | undefined => {
|
|
99
|
+
const bytes = readRegularFile(absolute);
|
|
100
|
+
return bytes === undefined ? undefined : createSourceSha(bytes);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export type Staleness =
|
|
104
|
+
| { status: "missing" }
|
|
105
|
+
| { status: "fresh" }
|
|
106
|
+
| { status: "stale"; changed: string[]; added: string[]; removed: string[]; unhashed: string[] };
|
|
107
|
+
|
|
108
|
+
export const checkStaleness = (
|
|
109
|
+
rubric: Rubric | undefined,
|
|
110
|
+
candidates: SourceCandidate[],
|
|
111
|
+
root: string,
|
|
112
|
+
): Staleness => {
|
|
113
|
+
if (rubric === undefined) return { status: "missing" };
|
|
114
|
+
const changed: string[] = [];
|
|
115
|
+
const removed: string[] = [];
|
|
116
|
+
const unhashed: string[] = [];
|
|
117
|
+
for (const source of rubric.sources) {
|
|
118
|
+
const now = hashFile(resolveSourcePath(root, source.path));
|
|
119
|
+
if (now === undefined) removed.push(source.path);
|
|
120
|
+
else if (source.sha === undefined) unhashed.push(source.path);
|
|
121
|
+
else if (source.sha !== now) changed.push(source.path);
|
|
122
|
+
}
|
|
123
|
+
const listed = new Set(rubric.sources.map((s) => path.resolve(resolveSourcePath(root, s.path))));
|
|
124
|
+
const added = candidates
|
|
125
|
+
.filter((c) => c.required && !listed.has(path.resolve(c.absolute)))
|
|
126
|
+
.map((c) => c.path);
|
|
127
|
+
if (changed.length + removed.length + unhashed.length + added.length === 0) {
|
|
128
|
+
return { status: "fresh" };
|
|
129
|
+
}
|
|
130
|
+
return { status: "stale", changed, added, removed, unhashed };
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export const describeStaleness = (s: Staleness): string => {
|
|
134
|
+
switch (s.status) {
|
|
135
|
+
case "missing":
|
|
136
|
+
return "no rubric yet";
|
|
137
|
+
case "fresh":
|
|
138
|
+
return "up to date";
|
|
139
|
+
case "stale": {
|
|
140
|
+
const parts: string[] = [];
|
|
141
|
+
if (s.changed.length) parts.push(`changed: ${s.changed.join(", ")}`);
|
|
142
|
+
if (s.added.length) parts.push(`new: ${s.added.join(", ")}`);
|
|
143
|
+
if (s.removed.length) parts.push(`gone: ${s.removed.join(", ")}`);
|
|
144
|
+
if (s.unhashed.length) parts.push(`never hashed: ${s.unhashed.join(", ")}`);
|
|
145
|
+
return parts.join("; ");
|
|
146
|
+
}
|
|
147
|
+
default:
|
|
148
|
+
return assertNeverStaleness(s);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const assertNeverStaleness = (value: never): never => {
|
|
153
|
+
throw new Error(`Unhandled staleness: ${JSON.stringify(value)}`);
|
|
154
|
+
};
|
package/src/lib/stdin.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { STDIN_TIMEOUT_MS } from "./constants.js";
|
|
2
|
+
|
|
3
|
+
/** Reads all of stdin, giving up after a short wait so a host that sends nothing cannot hang us, and lets go of the handle. */
|
|
4
|
+
export const readStdin = (timeoutMs = STDIN_TIMEOUT_MS): Promise<string> =>
|
|
5
|
+
new Promise((resolve) => {
|
|
6
|
+
const chunks: Buffer[] = [];
|
|
7
|
+
let settled = false;
|
|
8
|
+
const done = (): void => {
|
|
9
|
+
if (settled) return;
|
|
10
|
+
settled = true;
|
|
11
|
+
clearTimeout(timer);
|
|
12
|
+
try {
|
|
13
|
+
process.stdin.destroy();
|
|
14
|
+
} catch {
|
|
15
|
+
// nothing to release
|
|
16
|
+
}
|
|
17
|
+
resolve(Buffer.concat(chunks).toString("utf8"));
|
|
18
|
+
};
|
|
19
|
+
const timer = setTimeout(done, timeoutMs);
|
|
20
|
+
process.stdin.on("data", (c: Buffer) => chunks.push(c));
|
|
21
|
+
process.stdin.on("end", done);
|
|
22
|
+
process.stdin.on("error", done);
|
|
23
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { closeSync, readSync } from "node:fs";
|
|
2
|
+
import { MAX_TASK_CHARS } from "./constants.js";
|
|
3
|
+
import { openRegular } from "./regularFile.js";
|
|
4
|
+
|
|
5
|
+
const TAIL_BYTES = 512 * 1024;
|
|
6
|
+
|
|
7
|
+
const readTail = (file: string): string | undefined => {
|
|
8
|
+
const opened = openRegular(file);
|
|
9
|
+
if (opened === undefined) return undefined;
|
|
10
|
+
const { fd, size } = opened;
|
|
11
|
+
try {
|
|
12
|
+
const start = Math.max(0, size - TAIL_BYTES);
|
|
13
|
+
const buffer = Buffer.alloc(size - start);
|
|
14
|
+
readSync(fd, buffer, 0, buffer.length, start);
|
|
15
|
+
return buffer.toString("utf8");
|
|
16
|
+
} catch {
|
|
17
|
+
return undefined;
|
|
18
|
+
} finally {
|
|
19
|
+
closeSync(fd);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** The user's most recent typed prompt, so the check knows what the change was for. */
|
|
24
|
+
export const lastUserPrompt = (transcriptPath: string | undefined): string | undefined => {
|
|
25
|
+
if (transcriptPath === undefined) return undefined;
|
|
26
|
+
const tail = readTail(transcriptPath);
|
|
27
|
+
if (tail === undefined) return undefined;
|
|
28
|
+
const lines = tail.split("\n");
|
|
29
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
30
|
+
const line = lines[i];
|
|
31
|
+
if (line === undefined || !line.includes('"type":"user"')) continue;
|
|
32
|
+
try {
|
|
33
|
+
const entry: unknown = JSON.parse(line);
|
|
34
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
35
|
+
const record: Record<string, unknown> = { ...entry };
|
|
36
|
+
if (record.type !== "user" || record.isMeta === true) continue;
|
|
37
|
+
const message = record.message;
|
|
38
|
+
if (typeof message !== "object" || message === null) continue;
|
|
39
|
+
const body: Record<string, unknown> = { ...message };
|
|
40
|
+
const content = body.content;
|
|
41
|
+
if (typeof content === "string" && content.trim() !== "") {
|
|
42
|
+
return content.trim().slice(0, MAX_TASK_CHARS);
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
// torn line while the transcript is being written
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
};
|
package/src/lib/ui.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export const say = (line = ""): void => {
|
|
2
|
+
process.stdout.write(`${line}\n`);
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
export const warn = (line: string): void => {
|
|
6
|
+
process.stderr.write(`${line}\n`);
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const table = (rows: readonly (readonly string[])[]): string => {
|
|
10
|
+
const widths: number[] = [];
|
|
11
|
+
for (const row of rows)
|
|
12
|
+
row.forEach((cell, i) => (widths[i] = Math.max(widths[i] ?? 0, cell.length)));
|
|
13
|
+
return rows
|
|
14
|
+
.map((row) =>
|
|
15
|
+
row.map((cell, i) => (i === row.length - 1 ? cell : cell.padEnd(widths[i] ?? 0))).join(" "),
|
|
16
|
+
)
|
|
17
|
+
.join("\n");
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const ms = (n: number): string =>
|
|
21
|
+
n >= 1000 ? `${(n / 1000).toFixed(1)}s` : `${Math.round(n)}ms`;
|
|
22
|
+
export const usd = (n: number): string => (n < 0.01 ? `$${n.toFixed(5)}` : `$${n.toFixed(3)}`);
|
|
23
|
+
|
|
24
|
+
export const median = (values: readonly number[]): number | undefined => {
|
|
25
|
+
if (values.length === 0) return undefined;
|
|
26
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
27
|
+
const mid = Math.floor(sorted.length / 2);
|
|
28
|
+
const a = sorted[mid];
|
|
29
|
+
const b = sorted[mid - 1];
|
|
30
|
+
if (a === undefined) return undefined;
|
|
31
|
+
return sorted.length % 2 === 1 || b === undefined ? a : (a + b) / 2;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const percentile = (values: readonly number[], p: number): number | undefined => {
|
|
35
|
+
if (values.length === 0) return undefined;
|
|
36
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
37
|
+
return sorted[Math.min(sorted.length - 1, Math.ceil((p / 100) * sorted.length) - 1)];
|
|
38
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import type { Rule } from "@coldtea/abide-schema";
|
|
3
|
+
import { bucketCounts } from "../../lib/rubricFile.js";
|
|
4
|
+
import { glyph, palette } from "../theme.js";
|
|
5
|
+
|
|
6
|
+
/** One line: how many rules, and how each one is enforced. */
|
|
7
|
+
export function Buckets({ rules }: { rules: readonly Rule[] }) {
|
|
8
|
+
const c = bucketCounts(rules);
|
|
9
|
+
const part = (n: number, label: string, color: string) => (
|
|
10
|
+
<>
|
|
11
|
+
<Text color={palette.mist}> {glyph.dotSep} </Text>
|
|
12
|
+
<Text color={n > 0 ? color : palette.ash} bold={n > 0}>
|
|
13
|
+
{n}
|
|
14
|
+
</Text>
|
|
15
|
+
<Text color={n > 0 ? palette.cloud : palette.ash}> {label}</Text>
|
|
16
|
+
</>
|
|
17
|
+
);
|
|
18
|
+
return (
|
|
19
|
+
<Box>
|
|
20
|
+
<Text color={palette.cloud} bold>
|
|
21
|
+
{c.total}
|
|
22
|
+
</Text>
|
|
23
|
+
<Text color={palette.cloud}> rules</Text>
|
|
24
|
+
{part(c.model, "checked by Jev", palette.indigo)}
|
|
25
|
+
{part(c.lint, "for your linter", palette.sage)}
|
|
26
|
+
{part(c.deferred, "deferred", palette.ceramicWarm)}
|
|
27
|
+
{part(c.unenforceable, "unenforceable", palette.ash)}
|
|
28
|
+
</Box>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { glyph, toneColor, type Tone } from "../theme.js";
|
|
4
|
+
|
|
5
|
+
type CalloutProps = { tone: Tone; title: string; children?: ReactNode };
|
|
6
|
+
|
|
7
|
+
/** Something the reader should act on, boxed so it is not lost in a table. */
|
|
8
|
+
export function Callout({ tone, title, children }: CalloutProps) {
|
|
9
|
+
const color = toneColor[tone];
|
|
10
|
+
const mark = tone === "ok" ? glyph.check : tone === "bad" ? glyph.cross : glyph.dot;
|
|
11
|
+
return (
|
|
12
|
+
<Box
|
|
13
|
+
flexDirection="column"
|
|
14
|
+
borderStyle="round"
|
|
15
|
+
borderColor={color}
|
|
16
|
+
paddingX={1}
|
|
17
|
+
marginBottom={1}
|
|
18
|
+
alignSelf="flex-start"
|
|
19
|
+
>
|
|
20
|
+
<Text color={color} bold>
|
|
21
|
+
{mark} {title}
|
|
22
|
+
</Text>
|
|
23
|
+
{children ? <Box flexDirection="column">{children}</Box> : null}
|
|
24
|
+
</Box>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import { glyph, palette } from "../theme.js";
|
|
3
|
+
|
|
4
|
+
export type Step = { ok: boolean; text: string; detail?: string };
|
|
5
|
+
|
|
6
|
+
/** What happened, one line per step, so a person can see where an install stopped. */
|
|
7
|
+
export function Checklist({ steps }: { steps: readonly Step[] }) {
|
|
8
|
+
return (
|
|
9
|
+
<Box flexDirection="column">
|
|
10
|
+
{steps.map((step, i) => (
|
|
11
|
+
<Box key={i} flexDirection="column">
|
|
12
|
+
<Box>
|
|
13
|
+
<Text color={step.ok ? palette.sage : palette.rose}>
|
|
14
|
+
{step.ok ? glyph.check : glyph.cross}
|
|
15
|
+
</Text>
|
|
16
|
+
<Text color={palette.cloud}> {step.text}</Text>
|
|
17
|
+
</Box>
|
|
18
|
+
{step.detail ? (
|
|
19
|
+
<Box paddingLeft={2}>
|
|
20
|
+
<Text color={palette.ash} wrap="truncate-end">
|
|
21
|
+
{step.detail}
|
|
22
|
+
</Text>
|
|
23
|
+
</Box>
|
|
24
|
+
) : null}
|
|
25
|
+
</Box>
|
|
26
|
+
))}
|
|
27
|
+
</Box>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Box, Text, useStdout } from "ink";
|
|
2
|
+
import { palette, truncate } from "../theme.js";
|
|
3
|
+
|
|
4
|
+
export type Column = {
|
|
5
|
+
key: string;
|
|
6
|
+
label: string;
|
|
7
|
+
/** Fixed width, or omitted for the one column that takes what is left. */
|
|
8
|
+
width?: number;
|
|
9
|
+
align?: "left" | "right";
|
|
10
|
+
keep?: "head" | "tail";
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type Cell = { text: string; color?: string; bold?: boolean; dim?: boolean };
|
|
14
|
+
export type Row = Record<string, Cell>;
|
|
15
|
+
|
|
16
|
+
type ColumnsProps = { columns: Column[]; rows: Row[]; indent?: number };
|
|
17
|
+
|
|
18
|
+
const GAP = 2;
|
|
19
|
+
const MIN_FLEX = 10;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A table that fits the terminal. Fixed columns keep their width whatever
|
|
23
|
+
* happens, the flexible one takes what is left, and every cell is cut with an
|
|
24
|
+
* ellipsis rather than wrapped, so a row is always one line.
|
|
25
|
+
*/
|
|
26
|
+
export function Columns({ columns, rows, indent = 0 }: ColumnsProps) {
|
|
27
|
+
const { stdout } = useStdout();
|
|
28
|
+
const available = Math.max(60, (stdout?.columns ?? 100) - indent - 1);
|
|
29
|
+
const fixed = columns.reduce((sum, c) => sum + (c.width ?? 0), 0) + GAP * (columns.length - 1);
|
|
30
|
+
const flexWidth = Math.max(MIN_FLEX, available - fixed);
|
|
31
|
+
const widthOf = (c: Column): number => c.width ?? flexWidth;
|
|
32
|
+
const cell = (c: Column, content: Cell, header: boolean, last: boolean) => (
|
|
33
|
+
<Box
|
|
34
|
+
key={c.key}
|
|
35
|
+
width={widthOf(c)}
|
|
36
|
+
flexShrink={0}
|
|
37
|
+
marginRight={last ? 0 : GAP}
|
|
38
|
+
justifyContent={c.align === "right" ? "flex-end" : "flex-start"}
|
|
39
|
+
>
|
|
40
|
+
<Text
|
|
41
|
+
color={header ? palette.ash : (content.color ?? palette.cloud)}
|
|
42
|
+
bold={content.bold}
|
|
43
|
+
dimColor={content.dim}
|
|
44
|
+
wrap="truncate"
|
|
45
|
+
>
|
|
46
|
+
{truncate(content.text, widthOf(c), c.keep ?? "head")}
|
|
47
|
+
</Text>
|
|
48
|
+
</Box>
|
|
49
|
+
);
|
|
50
|
+
return (
|
|
51
|
+
<Box flexDirection="column">
|
|
52
|
+
<Box>{columns.map((c, i) => cell(c, { text: c.label }, true, i === columns.length - 1))}</Box>
|
|
53
|
+
{rows.map((row, r) => (
|
|
54
|
+
<Box key={r}>
|
|
55
|
+
{columns.map((c, i) =>
|
|
56
|
+
cell(c, row[c.key] ?? { text: "" }, false, i === columns.length - 1),
|
|
57
|
+
)}
|
|
58
|
+
</Box>
|
|
59
|
+
))}
|
|
60
|
+
</Box>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import { glyph, palette } from "../theme.js";
|
|
3
|
+
|
|
4
|
+
type HeaderProps = {
|
|
5
|
+
command: string;
|
|
6
|
+
/** Where this ran, on its own line: a person switching between repos should not have to guess. */
|
|
7
|
+
where?: string;
|
|
8
|
+
note?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function Header({ command, where, note }: HeaderProps) {
|
|
12
|
+
return (
|
|
13
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
14
|
+
<Box>
|
|
15
|
+
<Text color={palette.teal} bold>
|
|
16
|
+
abide
|
|
17
|
+
</Text>
|
|
18
|
+
<Text color={palette.mist}> {glyph.dotSep} </Text>
|
|
19
|
+
<Text color={palette.cloud} bold>
|
|
20
|
+
{command}
|
|
21
|
+
</Text>
|
|
22
|
+
</Box>
|
|
23
|
+
{where ? (
|
|
24
|
+
<Text color={palette.ash} wrap="truncate-start">
|
|
25
|
+
{where}
|
|
26
|
+
</Text>
|
|
27
|
+
) : null}
|
|
28
|
+
{note ? (
|
|
29
|
+
<Text color={palette.mist} wrap="wrap">
|
|
30
|
+
{note}
|
|
31
|
+
</Text>
|
|
32
|
+
) : null}
|
|
33
|
+
</Box>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Box, Text, useApp } from "ink";
|
|
2
|
+
import Spinner from "ink-spinner";
|
|
3
|
+
import { useEffect, useState, type ReactNode } from "react";
|
|
4
|
+
import { palette } from "../theme.js";
|
|
5
|
+
|
|
6
|
+
export type Progress = (label: string) => void;
|
|
7
|
+
|
|
8
|
+
type LiveProps<T> = {
|
|
9
|
+
/** Shown above the spinner while `run` is going. */
|
|
10
|
+
header: ReactNode;
|
|
11
|
+
run: (progress: Progress) => Promise<T>;
|
|
12
|
+
done: (result: T) => ReactNode;
|
|
13
|
+
failed: (error: unknown) => ReactNode;
|
|
14
|
+
onExit?: (code: number) => void;
|
|
15
|
+
/** Redraw the progress line. Off on a pipe, where every frame would be printed as a new line. */
|
|
16
|
+
animate?: boolean;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type State<T> =
|
|
20
|
+
| { kind: "running"; label: string }
|
|
21
|
+
| { kind: "done"; result: T }
|
|
22
|
+
| { kind: "failed"; error: unknown };
|
|
23
|
+
|
|
24
|
+
/** Runs a job and draws its progress, then its result, then leaves. */
|
|
25
|
+
export function Live<T>({ header, run, done, failed, onExit, animate = true }: LiveProps<T>) {
|
|
26
|
+
const { exit } = useApp();
|
|
27
|
+
const [state, setState] = useState<State<T>>({ kind: "running", label: "starting" });
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
let alive = true;
|
|
30
|
+
run((label) => {
|
|
31
|
+
if (alive && animate) setState({ kind: "running", label });
|
|
32
|
+
})
|
|
33
|
+
.then((result) => {
|
|
34
|
+
if (alive) setState({ kind: "done", result });
|
|
35
|
+
})
|
|
36
|
+
.catch((error: unknown) => {
|
|
37
|
+
if (alive) setState({ kind: "failed", error });
|
|
38
|
+
});
|
|
39
|
+
return () => {
|
|
40
|
+
alive = false;
|
|
41
|
+
};
|
|
42
|
+
// The job runs once for the life of the view.
|
|
43
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
44
|
+
}, []);
|
|
45
|
+
// Leave only after the final frame has been drawn, not in the same tick it was asked for.
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (state.kind === "running") return;
|
|
48
|
+
onExit?.(state.kind === "done" ? 0 : 1);
|
|
49
|
+
exit();
|
|
50
|
+
}, [state.kind, exit, onExit]);
|
|
51
|
+
// The result view draws its own header, so the wrapper shows one only while there is nothing else to show.
|
|
52
|
+
return (
|
|
53
|
+
<Box flexDirection="column">
|
|
54
|
+
{state.kind === "running" ? header : null}
|
|
55
|
+
{state.kind === "running" ? (
|
|
56
|
+
animate ? (
|
|
57
|
+
<Box>
|
|
58
|
+
<Text color={palette.teal}>
|
|
59
|
+
<Spinner type="dots" />
|
|
60
|
+
</Text>
|
|
61
|
+
<Text color={palette.mist}> {state.label}</Text>
|
|
62
|
+
</Box>
|
|
63
|
+
) : (
|
|
64
|
+
<Text color={palette.mist}>working</Text>
|
|
65
|
+
)
|
|
66
|
+
) : state.kind === "done" ? (
|
|
67
|
+
done(state.result)
|
|
68
|
+
) : (
|
|
69
|
+
failed(state.error)
|
|
70
|
+
)}
|
|
71
|
+
</Box>
|
|
72
|
+
);
|
|
73
|
+
}
|