fapony 0.1.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/LICENSE +21 -0
- package/README.md +473 -0
- package/fapony.ts +78 -0
- package/package.json +42 -0
- package/skill/git-commit-conventional/SKILL.md +68 -0
- package/skill/git-ship/SKILL.md +144 -0
- package/skill/move-to-done/SKILL.md +126 -0
- package/skill/plan-with-pony/SKILL.md +263 -0
- package/skill/review-pony/SKILL.md +254 -0
- package/src/analyze.ts +517 -0
- package/src/context/index.ts +11 -0
- package/src/context/projectHealth.ts +359 -0
- package/src/conventions-seed.ts +420 -0
- package/src/db/defaults.ts +26 -0
- package/src/db/getters.ts +33 -0
- package/src/db/index.ts +7 -0
- package/src/db/load.ts +57 -0
- package/src/db/store.ts +286 -0
- package/src/db/types.ts +79 -0
- package/src/debt.ts +667 -0
- package/src/digest/cli.ts +75 -0
- package/src/digest/collect.ts +625 -0
- package/src/digest/html.ts +208 -0
- package/src/digest/text.ts +191 -0
- package/src/gate.ts +153 -0
- package/src/gates.ts +194 -0
- package/src/hook.ts +436 -0
- package/src/init-mem.ts +71 -0
- package/src/init.ts +237 -0
- package/src/install/claude.ts +361 -0
- package/src/install/codex.ts +61 -0
- package/src/install/cursor.ts +167 -0
- package/src/install/detect.ts +78 -0
- package/src/install/opencode.ts +234 -0
- package/src/install/skills.ts +106 -0
- package/src/install/types.ts +69 -0
- package/src/install/utils.ts +29 -0
- package/src/install/zcode.ts +120 -0
- package/src/install.ts +176 -0
- package/src/lint-baseline.ts +260 -0
- package/src/map.ts +320 -0
- package/src/math.ts +13 -0
- package/src/mcp/evidence.ts +332 -0
- package/src/mcp/primitives.ts +316 -0
- package/src/mcp/tools/check.ts +243 -0
- package/src/mcp/tools/collect.ts +157 -0
- package/src/mcp/tools/context.ts +66 -0
- package/src/mcp/tools/index.ts +309 -0
- package/src/mcp/tools/mem.ts +95 -0
- package/src/mcp/tools/plans.ts +255 -0
- package/src/mcp/tools/report.ts +285 -0
- package/src/mcp/tools/stats.ts +96 -0
- package/src/mcp/tools/usage.ts +211 -0
- package/src/mcp/tools/verdict.ts +148 -0
- package/src/mcp/transport.ts +241 -0
- package/src/mcp/types.ts +54 -0
- package/src/mcp/worktree.ts +27 -0
- package/src/memory.ts +264 -0
- package/src/parse.ts +71 -0
- package/src/plan-seed.ts +599 -0
- package/src/price/fetch.ts +146 -0
- package/src/price/index.ts +8 -0
- package/src/price/resolve.ts +213 -0
- package/src/report/cli.ts +92 -0
- package/src/report/format.ts +37 -0
- package/src/report/index.ts +4 -0
- package/src/report/render.ts +206 -0
- package/src/review-seed.ts +932 -0
- package/src/safety.ts +18 -0
- package/src/session/activeSession.ts +153 -0
- package/src/session/claude-code.ts +412 -0
- package/src/session/codex.ts +347 -0
- package/src/session/findModel.ts +376 -0
- package/src/session/helpers.ts +640 -0
- package/src/session/index.ts +31 -0
- package/src/session/opencode.ts +167 -0
- package/src/session/registry.ts +45 -0
- package/src/session/types.ts +128 -0
- package/src/session/zcode.ts +151 -0
- package/src/setup.ts +242 -0
- package/src/stats/cli.ts +44 -0
- package/src/stats/data.ts +1019 -0
- package/src/stats/format.ts +584 -0
- package/src/stats/index.ts +19 -0
- package/src/telemetry.ts +364 -0
- package/src/test.ts +2 -0
- package/src/update.ts +212 -0
- package/src/usage/cache.ts +125 -0
- package/src/usage/cli.ts +120 -0
- package/src/usage/format.ts +29 -0
- package/src/usage/index.ts +4 -0
- package/src/usage/render.ts +523 -0
- package/src/usage/scan.ts +161 -0
- package/src/util.ts +32 -0
- package/src/web/html.ts +33 -0
- package/templates/PLAN.md +90 -0
- package/templates/SPEC.md +30 -0
- package/templates/mem/commands/plan.ts +360 -0
- package/templates/mem/commands/read.ts +194 -0
- package/templates/mem/commands/rotate.ts +59 -0
- package/templates/mem/commands/selftest.ts +450 -0
- package/templates/mem/commands/write.ts +214 -0
- package/templates/mem/mem.ts +68 -0
- package/templates/mem/render.ts +63 -0
- package/templates/mem/selectors.ts +144 -0
- package/templates/mem/store.ts +285 -0
package/src/setup.ts
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
// src/setup.ts — interactive wizard: config + scaffold in one step.
|
|
2
|
+
// Replaces the manual cp + edit + init flow.
|
|
3
|
+
|
|
4
|
+
import { execSync } from "node:child_process";
|
|
5
|
+
import { existsSync, statSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { join, resolve } from "node:path";
|
|
7
|
+
import { createInterface } from "node:readline";
|
|
8
|
+
import { seedConventionsFile } from "./conventions-seed.js";
|
|
9
|
+
import { DEFAULT_MEMORY_ENTRY } from "./db/index.js";
|
|
10
|
+
import { initProject } from "./init.js";
|
|
11
|
+
import { isAffirmative } from "./util.js";
|
|
12
|
+
|
|
13
|
+
export function ask(
|
|
14
|
+
rl: ReturnType<typeof createInterface>,
|
|
15
|
+
question: string,
|
|
16
|
+
defaultVal?: string,
|
|
17
|
+
): Promise<string> {
|
|
18
|
+
return new Promise((resolve) => {
|
|
19
|
+
const suffix = defaultVal !== undefined ? ` (${defaultVal})` : "";
|
|
20
|
+
rl.question(` ${question}${suffix}: `, (answer) => {
|
|
21
|
+
resolve(answer.trim() || defaultVal || "");
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function defaultDetectGitRoot(): string | null {
|
|
27
|
+
try {
|
|
28
|
+
const root = execSync("git rev-parse --show-toplevel", {
|
|
29
|
+
encoding: "utf-8",
|
|
30
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
31
|
+
timeout: 15_000,
|
|
32
|
+
}).trim();
|
|
33
|
+
return root;
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function defaultCheckCmd(cmd: string): boolean {
|
|
40
|
+
// Allowlist first: cmd names are hardcoded at every call site, so anything
|
|
41
|
+
// outside [word chars, dot, dash] is rejected before touching a shell.
|
|
42
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(cmd)) return false;
|
|
43
|
+
try {
|
|
44
|
+
execSync(`command -v ${cmd}`, { stdio: "pipe", timeout: 15_000 });
|
|
45
|
+
return true;
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Minimal seam for cmdSetup — only the I/O cmdSetup calls directly (the
|
|
52
|
+
* prerequisites check, git-root detection, prompting, and exit). fs/cwd stay
|
|
53
|
+
* real: tests run against a temp dir. Every field is used by both the
|
|
54
|
+
* default (production) path and the test path. */
|
|
55
|
+
export interface SetupDeps {
|
|
56
|
+
checkCmd?: (cmd: string) => boolean;
|
|
57
|
+
detectGitRoot?: () => string | null;
|
|
58
|
+
ask?: (question: string, defaultVal?: string) => Promise<string>;
|
|
59
|
+
exit?: (code: number) => never;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface SetupAnswers {
|
|
63
|
+
worktreeName: string;
|
|
64
|
+
worktreePath: string;
|
|
65
|
+
enableMemory: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Pure config builder — the config-write path of cmdSetup, minus prompting.
|
|
69
|
+
* No executor/gate/auto-loop keys — nothing spawns agents anymore, the CLI
|
|
70
|
+
* loop was removed in Wave 2. Remaining advanced/optional keys are left for
|
|
71
|
+
* the user to hand-edit — see
|
|
72
|
+
* fapony.config.example.json. */
|
|
73
|
+
export function buildSetupConfig(a: SetupAnswers): Record<string, unknown> {
|
|
74
|
+
const config: Record<string, unknown> = {
|
|
75
|
+
worktrees: { [a.worktreeName]: a.worktreePath },
|
|
76
|
+
review: { maxRounds: 2 },
|
|
77
|
+
memory: null,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
if (a.enableMemory) {
|
|
81
|
+
const memEntry = DEFAULT_MEMORY_ENTRY;
|
|
82
|
+
config.memory = {
|
|
83
|
+
claim: ["bun", memEntry, "claim", "{id}"],
|
|
84
|
+
close: ["bun", memEntry, "close", "{id}", "{msg}"],
|
|
85
|
+
add: ["bun", memEntry, "add", "{kind}", "{text}"],
|
|
86
|
+
kickoff: ["bun", memEntry, "kickoff"],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return config;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Null = path usable; otherwise the error message cmdSetup prints. */
|
|
94
|
+
export function validateWorktreePath(worktreePath: string): string | null {
|
|
95
|
+
if (!worktreePath) return "Path must not be empty.";
|
|
96
|
+
if (!existsSync(worktreePath)) return `Path does not exist: ${worktreePath}`;
|
|
97
|
+
try {
|
|
98
|
+
if (!statSync(worktreePath).isDirectory())
|
|
99
|
+
return `Path is not a directory: ${worktreePath}`;
|
|
100
|
+
} catch (e) {
|
|
101
|
+
return `Cannot stat path: ${(e as Error).message}`;
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Overwrite guard — only an affirmative answer proceeds with the write. */
|
|
107
|
+
export function shouldOverwriteConfig(answer: string): boolean {
|
|
108
|
+
return isAffirmative(answer);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function cmdSetup(deps: SetupDeps = {}): Promise<void> {
|
|
112
|
+
console.log("\n🔧 fapony setup — interactive wizard\n");
|
|
113
|
+
|
|
114
|
+
const checkCmdFn = deps.checkCmd ?? defaultCheckCmd;
|
|
115
|
+
const detectGitRootFn = deps.detectGitRoot ?? defaultDetectGitRoot;
|
|
116
|
+
const exitFn = deps.exit ?? ((code: number): never => process.exit(code));
|
|
117
|
+
/** Log + exit. Throws if a custom exit() ever returns instead of
|
|
118
|
+
* terminating (production process.exit never returns; without this,
|
|
119
|
+
* execution would fall through with git/bun missing). */
|
|
120
|
+
const fail = (msg: string): never => {
|
|
121
|
+
console.error(msg);
|
|
122
|
+
exitFn(1);
|
|
123
|
+
throw new Error("unreachable: exit() returned");
|
|
124
|
+
};
|
|
125
|
+
// Real readline only exists on the default path — injected ask (tests)
|
|
126
|
+
// never touches stdin.
|
|
127
|
+
const rl = deps.ask
|
|
128
|
+
? null
|
|
129
|
+
: createInterface({ input: process.stdin, output: process.stdout });
|
|
130
|
+
const askFn =
|
|
131
|
+
deps.ask ??
|
|
132
|
+
((question: string, defaultVal?: string) => ask(rl!, question, defaultVal));
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
// --- prerequisites ---
|
|
136
|
+
if (!checkCmdFn("git")) {
|
|
137
|
+
fail("❌ git is required but not found on PATH.");
|
|
138
|
+
}
|
|
139
|
+
if (!checkCmdFn("bun")) {
|
|
140
|
+
fail(
|
|
141
|
+
"❌ bun is required but not found on PATH.\n" +
|
|
142
|
+
" Install: curl -fsSL https://bun.sh/install | bash",
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// --- worktree path ---
|
|
147
|
+
const gitRoot = detectGitRootFn();
|
|
148
|
+
const defaultPath = gitRoot || process.cwd();
|
|
149
|
+
const worktreePath = resolve(await askFn("Worktree path", defaultPath));
|
|
150
|
+
|
|
151
|
+
const pathError = validateWorktreePath(worktreePath);
|
|
152
|
+
if (pathError) {
|
|
153
|
+
fail(`❌ ${pathError}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// --- worktree name ---
|
|
157
|
+
const defaultName = worktreePath.split("/").pop() || "myapp";
|
|
158
|
+
const worktreeName = await askFn(
|
|
159
|
+
"Worktree name (key for CLI)",
|
|
160
|
+
defaultName,
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
// --- memory ---
|
|
164
|
+
console.log();
|
|
165
|
+
const enableMemory = isAffirmative(
|
|
166
|
+
await askFn("Enable project memory? (y/n)", "n"),
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
// --- write config ---
|
|
170
|
+
// loadConfig() resolves to FAPONY_CONFIG or cwd/fapony.config.json, so a
|
|
171
|
+
// cwd write is consistent — but warn when cwd isn't a fapony checkout,
|
|
172
|
+
// or the config lands in a worktree where agents can see it.
|
|
173
|
+
if (!existsSync(join(process.cwd(), "fapony.ts"))) {
|
|
174
|
+
console.error(
|
|
175
|
+
"⚠ cwd doesn't look like a fapony checkout (no fapony.ts) — " +
|
|
176
|
+
"fapony.config.json will be written here. " +
|
|
177
|
+
"Run setup from the fapony repo root if that's not what you want.",
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
const config = buildSetupConfig({
|
|
181
|
+
worktreeName,
|
|
182
|
+
worktreePath,
|
|
183
|
+
enableMemory,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const configPath = join(process.cwd(), "fapony.config.json");
|
|
187
|
+
if (existsSync(configPath)) {
|
|
188
|
+
const overwrite = await askFn(
|
|
189
|
+
"⚠ fapony.config.json already exists. Overwrite? (y/n)",
|
|
190
|
+
"n",
|
|
191
|
+
);
|
|
192
|
+
if (!shouldOverwriteConfig(overwrite)) {
|
|
193
|
+
console.log("\n Skipped config write. Existing file kept.");
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
199
|
+
console.log(`\n ✓ Wrote ${configPath}`);
|
|
200
|
+
|
|
201
|
+
// --- scaffold worktree ---
|
|
202
|
+
const faponyDir = join(worktreePath, ".fapony");
|
|
203
|
+
if (!existsSync(faponyDir)) {
|
|
204
|
+
try {
|
|
205
|
+
initProject(worktreePath);
|
|
206
|
+
const seed = await seedConventionsFile(worktreePath);
|
|
207
|
+
if (!seed.kept) {
|
|
208
|
+
console.log(
|
|
209
|
+
` ✓ Conventions: ${seed.eslintRows} from eslint, ${seed.wrapperRows} from wrappers`,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
for (const s of seed.skipped) {
|
|
213
|
+
console.log(` ⚠ eslint config ${s}`);
|
|
214
|
+
}
|
|
215
|
+
console.log(` ✓ Scaffolded .fapony/ in ${worktreePath}`);
|
|
216
|
+
} catch (e) {
|
|
217
|
+
console.log(` ⚠ Scaffold skipped: ${(e as Error).message}`);
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
console.log(` ✓ .fapony/ already exists in ${worktreePath}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// --- done ---
|
|
224
|
+
console.log(`
|
|
225
|
+
┌─────────────────────────────────────────┐
|
|
226
|
+
│ Setup complete! Next steps: │
|
|
227
|
+
│ │
|
|
228
|
+
│ 1. Wire fapony into your MCP client: │
|
|
229
|
+
│ fapony install --platform opencode │
|
|
230
|
+
│ fapony install --platform claude │
|
|
231
|
+
│ │
|
|
232
|
+
│ 2. Ask your agent: │
|
|
233
|
+
│ "Run fapony_stats and fapony_usage" │
|
|
234
|
+
│ │
|
|
235
|
+
│ 3. Read a run's report: │
|
|
236
|
+
│ fapony report <run-id> │
|
|
237
|
+
└─────────────────────────────────────────┘
|
|
238
|
+
`);
|
|
239
|
+
} finally {
|
|
240
|
+
rl?.close();
|
|
241
|
+
}
|
|
242
|
+
}
|
package/src/stats/cli.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// src/stats/cli.ts — cmdStats CLI entry point
|
|
2
|
+
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
import { getStatsData } from "./data.js";
|
|
5
|
+
import { formatStatsText, formatVerdictText } from "./format.js";
|
|
6
|
+
|
|
7
|
+
/** Absolute path of the repo/worktree the process was started in, or null outside git. */
|
|
8
|
+
export function currentWorktree(): string | null {
|
|
9
|
+
try {
|
|
10
|
+
return execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
11
|
+
encoding: "utf-8",
|
|
12
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
13
|
+
}).trim();
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function cmdStats(args: string[]): void {
|
|
20
|
+
// Default to the project you are standing in. Averaging several projects
|
|
21
|
+
// together reads as "in this project" while being no such thing, so going
|
|
22
|
+
// global is opt-in and the header always says which one you got.
|
|
23
|
+
const worktree = args.includes("--all")
|
|
24
|
+
? undefined
|
|
25
|
+
: (currentWorktree() ?? undefined);
|
|
26
|
+
const data = getStatsData(worktree);
|
|
27
|
+
|
|
28
|
+
const modeIdx = args.indexOf("--mode");
|
|
29
|
+
const mode =
|
|
30
|
+
modeIdx !== -1 && typeof args[modeIdx + 1] === "string"
|
|
31
|
+
? args[modeIdx + 1]
|
|
32
|
+
: undefined;
|
|
33
|
+
|
|
34
|
+
if (mode === "verdict") {
|
|
35
|
+
const regimeIdx = args.indexOf("--regime");
|
|
36
|
+
const regime =
|
|
37
|
+
regimeIdx !== -1 && typeof args[regimeIdx + 1] === "string"
|
|
38
|
+
? args[regimeIdx + 1]
|
|
39
|
+
: undefined;
|
|
40
|
+
console.log(formatVerdictText(data, regime));
|
|
41
|
+
} else {
|
|
42
|
+
console.log(formatStatsText(data));
|
|
43
|
+
}
|
|
44
|
+
}
|