fapony 0.2.0 → 0.2.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/README.md +42 -8
- package/fapony.ts +4 -2
- package/package.json +1 -1
- package/src/analyze.ts +162 -4
- package/src/conventions-seed.ts +3 -2
- package/src/db/defaults.ts +13 -5
- package/src/db/getters.ts +8 -6
- package/src/db/load.ts +2 -2
- package/src/db/types.ts +1 -3
- package/src/debt.ts +9 -4
- package/src/digest/collect.ts +3 -2
- package/src/hook.ts +245 -12
- package/src/init-mem.ts +5 -5
- package/src/init.ts +6 -5
- package/src/install/claude.ts +37 -3
- package/src/install/codex.ts +105 -13
- package/src/install/opencode.ts +111 -1
- package/src/install.ts +2 -1
- package/src/lint-baseline.ts +2 -1
- package/src/mcp/evidence.ts +14 -2
- package/src/mcp/transport.ts +1 -1
- package/src/mem/store.ts +6 -4
- package/src/memory.ts +6 -5
- package/src/plan-seed.ts +7 -6
- package/src/setup.ts +3 -2
- package/src/stats/data.ts +6 -18
package/src/install/opencode.ts
CHANGED
|
@@ -113,6 +113,7 @@ export function cmdInstallOpencode(
|
|
|
113
113
|
reportSkills(linkSkills(skillsDir, dryRun), skillsDir, dryRun);
|
|
114
114
|
installReadHintPlugin(dryRun, getHome);
|
|
115
115
|
installCommitHintPlugin(dryRun, getHome);
|
|
116
|
+
installEditHintPlugin(dryRun, getHome);
|
|
116
117
|
return;
|
|
117
118
|
}
|
|
118
119
|
|
|
@@ -136,6 +137,7 @@ export function cmdInstallOpencode(
|
|
|
136
137
|
console.log(computeDiff(before, after));
|
|
137
138
|
installReadHintPlugin(dryRun, getHome);
|
|
138
139
|
installCommitHintPlugin(dryRun, getHome);
|
|
140
|
+
installEditHintPlugin(dryRun, getHome);
|
|
139
141
|
return;
|
|
140
142
|
}
|
|
141
143
|
|
|
@@ -157,6 +159,7 @@ export function cmdInstallOpencode(
|
|
|
157
159
|
reportSkills(linkSkills(skillsDir, dryRun), skillsDir, dryRun);
|
|
158
160
|
installReadHintPlugin(dryRun, getHome);
|
|
159
161
|
installCommitHintPlugin(dryRun, getHome);
|
|
162
|
+
installEditHintPlugin(dryRun, getHome);
|
|
160
163
|
}
|
|
161
164
|
|
|
162
165
|
/**
|
|
@@ -228,7 +231,7 @@ export const FaponyReadHint = async ({ directory }) => {
|
|
|
228
231
|
const { relative } = require("node:path");
|
|
229
232
|
const { join: pjoin } = require("node:path");
|
|
230
233
|
const wt = ctx.worktree;
|
|
231
|
-
const abs = realpathSync(filePath.startsWith("/") ? filePath : pjoin(
|
|
234
|
+
const abs = realpathSync(filePath.startsWith("/") ? filePath : pjoin(directory, filePath));
|
|
232
235
|
const r = relative(wt, abs).split("\\\\").join("/");
|
|
233
236
|
return r.startsWith("..") ? null : r;
|
|
234
237
|
} catch { return null; }
|
|
@@ -308,6 +311,80 @@ export const FaponyCommitHint = async ({ directory }) => {
|
|
|
308
311
|
`;
|
|
309
312
|
}
|
|
310
313
|
|
|
314
|
+
/**
|
|
315
|
+
* OpenCode plugin — the edit hint's in-process shape. `tool.execute.after`
|
|
316
|
+
* on the edit and write tools: when the touched file has importers, appends
|
|
317
|
+
* the importer count plus the review-seed command that lists them. Annotate
|
|
318
|
+
* only (no block, no dedupe beyond what editHintFor does itself).
|
|
319
|
+
*
|
|
320
|
+
* This is the OpenCode parity of Claude's Edit PreToolUse hook
|
|
321
|
+
* (PLAN-edit-importer-hint): same shared logic (editHintFor in src/hook.ts),
|
|
322
|
+
* same once-per-(session, file) dedupe, same `"edit"` fire surface. One
|
|
323
|
+
* known difference: OpenCode's only annotate channel is `after`, so the hint
|
|
324
|
+
* lands one step after the edit instead of before it — the `before` hook can
|
|
325
|
+
* only mutate args or throw, and a hint must never block a tool call.
|
|
326
|
+
*
|
|
327
|
+
* `write` is guarded alongside `edit` for free: a write to a new file
|
|
328
|
+
* resolves to null inside editHintFor (nothing imports it yet), so only a
|
|
329
|
+
* write that overwrites an imported file fires. `apply_patch` is out of
|
|
330
|
+
* scope — its paths live inside patchText, not filePath (separate plan).
|
|
331
|
+
*/
|
|
332
|
+
export function editHintPluginSource(root: string): string {
|
|
333
|
+
const hookModule = JSON.stringify(join(root, "src", "hook.ts"));
|
|
334
|
+
return `// fapony edit hint — annotates edits to source files that have importers
|
|
335
|
+
// with the importer count plus the review-seed command that lists them.
|
|
336
|
+
// Annotate only: never blocks, dedupe per (session, file) inside editHintFor.
|
|
337
|
+
// Generated by \`fapony install\` — edit src/hook.ts in the fapony checkout.
|
|
338
|
+
import { editHintFor, recordHintFire } from ${hookModule};
|
|
339
|
+
|
|
340
|
+
export const FaponyEditHint = async ({ directory }) => {
|
|
341
|
+
return {
|
|
342
|
+
"tool.execute.after": async (input, output) => {
|
|
343
|
+
try {
|
|
344
|
+
if (input.tool !== "edit" && input.tool !== "write") return;
|
|
345
|
+
// The only channel that reaches the agent is output.output; if it is
|
|
346
|
+
// not a string the hint cannot surface, so bail before editHintFor
|
|
347
|
+
// spends the per-session dedupe row or logs a fire that never showed.
|
|
348
|
+
if (typeof output.output !== "string") return;
|
|
349
|
+
const filePath = input.args?.filePath;
|
|
350
|
+
const hint = editHintFor({
|
|
351
|
+
filePath,
|
|
352
|
+
cwd: directory,
|
|
353
|
+
session: input.sessionID,
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
// --- hint-fire log (PLAN-edit-importer-hint, opencode parity) ---
|
|
357
|
+
// After output — best-effort, never block the hint.
|
|
358
|
+
if (hint) {
|
|
359
|
+
output.output = output.output + "\\n" + hint;
|
|
360
|
+
try {
|
|
361
|
+
const { spawnSync } = require("node:child_process");
|
|
362
|
+
const git = spawnSync("git", ["rev-parse", "--show-toplevel"], { cwd: directory, encoding: "utf-8" });
|
|
363
|
+
if (git.status === 0) {
|
|
364
|
+
const worktree = require("node:fs").realpathSync(git.stdout.trim());
|
|
365
|
+
const rel = typeof filePath === "string" ? (() => {
|
|
366
|
+
try {
|
|
367
|
+
const { relative, join: pjoin } = require("node:path");
|
|
368
|
+
const abs = require("node:fs").realpathSync(filePath.startsWith("/") ? filePath : pjoin(directory, filePath));
|
|
369
|
+
const r = relative(worktree, abs).split("\\\\").join("/");
|
|
370
|
+
return r.startsWith("..") ? null : r;
|
|
371
|
+
} catch { return null; }
|
|
372
|
+
})() : null;
|
|
373
|
+
recordHintFire({ ts: new Date().toISOString(), worktree, surface: "edit", file: rel, count: 1 });
|
|
374
|
+
}
|
|
375
|
+
} catch {
|
|
376
|
+
// best-effort
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
} catch {
|
|
380
|
+
// a hint must never break an edit
|
|
381
|
+
}
|
|
382
|
+
},
|
|
383
|
+
};
|
|
384
|
+
};
|
|
385
|
+
`;
|
|
386
|
+
}
|
|
387
|
+
|
|
311
388
|
function installReadHintPlugin(dryRun: boolean, getHome: () => string): void {
|
|
312
389
|
const pluginsDir = join(getHome(), ".config", "opencode", "plugins");
|
|
313
390
|
const pluginPath = join(pluginsDir, "fapony-read-hint.ts");
|
|
@@ -373,3 +450,36 @@ function installCommitHintPlugin(dryRun: boolean, getHome: () => string): void {
|
|
|
373
450
|
` commit hint: ${dryRun ? "would write" : "wrote"} ${pluginPath}`,
|
|
374
451
|
);
|
|
375
452
|
}
|
|
453
|
+
|
|
454
|
+
function installEditHintPlugin(dryRun: boolean, getHome: () => string): void {
|
|
455
|
+
const pluginsDir = join(getHome(), ".config", "opencode", "plugins");
|
|
456
|
+
const pluginPath = join(pluginsDir, "fapony-edit-hint.ts");
|
|
457
|
+
if (existsSync(pluginPath)) {
|
|
458
|
+
let current = "";
|
|
459
|
+
try {
|
|
460
|
+
current = readFileSync(pluginPath, "utf-8");
|
|
461
|
+
} catch {
|
|
462
|
+
current = "";
|
|
463
|
+
}
|
|
464
|
+
if (current.includes("editHintFor")) {
|
|
465
|
+
console.error(` edit hint: already installed — no change`);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
console.error(
|
|
469
|
+
` edit hint: ${pluginPath} exists but isn't fapony's — not overwriting.`,
|
|
470
|
+
);
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
if (!dryRun) {
|
|
474
|
+
try {
|
|
475
|
+
mkdirSync(pluginsDir, { recursive: true });
|
|
476
|
+
writeFileSync(pluginPath, editHintPluginSource(INSTALL_ROOT), "utf-8");
|
|
477
|
+
} catch (e) {
|
|
478
|
+
console.error(` edit hint: failed to write — ${(e as Error).message}`);
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
console.error(
|
|
483
|
+
` edit hint: ${dryRun ? "would write" : "wrote"} ${pluginPath}`,
|
|
484
|
+
);
|
|
485
|
+
}
|
package/src/install.ts
CHANGED
|
@@ -28,12 +28,13 @@ export {
|
|
|
28
28
|
claudeGetPointsToFapony,
|
|
29
29
|
cmdInstallClaude,
|
|
30
30
|
} from "./install/claude.js";
|
|
31
|
-
export { cmdInstallCodex } from "./install/codex.js";
|
|
31
|
+
export { cmdInstallCodex, findCodexHooksJson } from "./install/codex.js";
|
|
32
32
|
export { cmdInstallCursor } from "./install/cursor.js";
|
|
33
33
|
export { detectClients } from "./install/detect.js";
|
|
34
34
|
export {
|
|
35
35
|
cmdInstallOpencode,
|
|
36
36
|
commitHintPluginSource,
|
|
37
|
+
editHintPluginSource,
|
|
37
38
|
readHintPluginSource,
|
|
38
39
|
} from "./install/opencode.js";
|
|
39
40
|
export {
|
package/src/lint-baseline.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { execSync } from "node:child_process";
|
|
15
15
|
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
16
16
|
import { join, relative } from "node:path";
|
|
17
|
+
import { DEFAULT_EVIDENCE_FILE } from "./db/index.js";
|
|
17
18
|
import { faponyDir } from "./db/load.js";
|
|
18
19
|
import { assertSafe } from "./safety.js";
|
|
19
20
|
|
|
@@ -57,7 +58,7 @@ function baselinePath(worktree: string): string {
|
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
function readEvidenceLintCmd(worktree: string): string | null {
|
|
60
|
-
const evidence = join(worktree,
|
|
61
|
+
const evidence = join(worktree, DEFAULT_EVIDENCE_FILE);
|
|
61
62
|
try {
|
|
62
63
|
const parsed = JSON.parse(readFileSync(evidence, "utf-8")) as {
|
|
63
64
|
commands?: { name?: string; cmd?: string }[];
|
package/src/mcp/evidence.ts
CHANGED
|
@@ -17,7 +17,13 @@
|
|
|
17
17
|
import { execSync } from "node:child_process";
|
|
18
18
|
import { existsSync, readFileSync } from "node:fs";
|
|
19
19
|
import { join, relative } from "node:path";
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
type Config,
|
|
22
|
+
EVIDENCE_FILENAME,
|
|
23
|
+
evidenceFile,
|
|
24
|
+
FAPONY_DIR,
|
|
25
|
+
safetyDeny,
|
|
26
|
+
} from "../db/index.js";
|
|
21
27
|
import { assertSafe } from "../safety.js";
|
|
22
28
|
import type { EvidenceItem, EvidenceStatus } from "./primitives.js";
|
|
23
29
|
|
|
@@ -80,7 +86,13 @@ export function resolveEvidencePath(
|
|
|
80
86
|
APP_GROUPS.includes(group) &&
|
|
81
87
|
heads.every(([g, a]) => g === group && a === app)
|
|
82
88
|
) {
|
|
83
|
-
const candidate = join(
|
|
89
|
+
const candidate = join(
|
|
90
|
+
worktree,
|
|
91
|
+
group,
|
|
92
|
+
app,
|
|
93
|
+
FAPONY_DIR,
|
|
94
|
+
EVIDENCE_FILENAME,
|
|
95
|
+
);
|
|
84
96
|
if (existsSync(candidate)) return candidate;
|
|
85
97
|
}
|
|
86
98
|
}
|
package/src/mcp/transport.ts
CHANGED
|
@@ -110,7 +110,7 @@ function writeStatuslineCache(toolResult: ToolResult): void {
|
|
|
110
110
|
|
|
111
111
|
const MCP_PROTOCOL_VERSION = "2025-03-26";
|
|
112
112
|
const SERVER_NAME = "fapony-handcheck";
|
|
113
|
-
const SERVER_VERSION = "0.2.
|
|
113
|
+
const SERVER_VERSION = "0.2.1";
|
|
114
114
|
|
|
115
115
|
// --- JSON-RPC dispatch ---
|
|
116
116
|
|
package/src/mem/store.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from "node:fs";
|
|
16
16
|
import { hostname, networkInterfaces } from "node:os";
|
|
17
17
|
import { basename, dirname, join, relative } from "node:path";
|
|
18
|
+
import { CONFIG_FILENAME, DEFAULT_MEM_DIR, FAPONY_DIR } from "../db/index.js";
|
|
18
19
|
import { whereMemDir } from "../memory.js";
|
|
19
20
|
|
|
20
21
|
// --- types ---
|
|
@@ -174,7 +175,7 @@ export function initStore(worktree: string, overrideMemDir?: string): void {
|
|
|
174
175
|
`two or more .fapony/.memory dirs under ${root} — refusing to guess:\n` +
|
|
175
176
|
`${list}\ncd into one, set paths.memDir, or pass --mem-dir`;
|
|
176
177
|
}
|
|
177
|
-
dir = resolved.dir ?? join(root,
|
|
178
|
+
dir = resolved.dir ?? join(root, DEFAULT_MEM_DIR);
|
|
178
179
|
|
|
179
180
|
// planBase: the .fapony dir — plan/done/conventions live here.
|
|
180
181
|
// Derive from dir by going up from .fapony/.memory → .fapony
|
|
@@ -184,10 +185,11 @@ export function initStore(worktree: string, overrideMemDir?: string): void {
|
|
|
184
185
|
app = dirname(planBase);
|
|
185
186
|
|
|
186
187
|
// Read fapony.config.json
|
|
187
|
-
const configDir =
|
|
188
|
+
const configDir =
|
|
189
|
+
planBase === `${root}/${FAPONY_DIR}` ? root : dirname(planBase);
|
|
188
190
|
const configPaths = ((): Record<string, string> => {
|
|
189
191
|
try {
|
|
190
|
-
const raw = readFileSync(`${configDir}
|
|
192
|
+
const raw = readFileSync(`${configDir}/${CONFIG_FILENAME}`, "utf8");
|
|
191
193
|
return (JSON.parse(raw)?.paths ?? {}) as Record<string, string>;
|
|
192
194
|
} catch {
|
|
193
195
|
return {};
|
|
@@ -198,7 +200,7 @@ export function initStore(worktree: string, overrideMemDir?: string): void {
|
|
|
198
200
|
? join(configDir, configPaths[key])
|
|
199
201
|
: null;
|
|
200
202
|
|
|
201
|
-
planDir =
|
|
203
|
+
planDir = `${planBase}/plan`;
|
|
202
204
|
|
|
203
205
|
// done/ sits beside plan/ (same depth, relative links survive)
|
|
204
206
|
doneDir =
|
package/src/memory.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { execSync } from "node:child_process";
|
|
6
6
|
import { existsSync, readdirSync, readFileSync, realpathSync } from "node:fs";
|
|
7
7
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
8
|
+
import { CONFIG_FILENAME, DEFAULT_MEM_DIR, FAPONY_DIR } from "./db/defaults.js";
|
|
8
9
|
import { type Config, loadConfig, safetyDeny } from "./db/index.js";
|
|
9
10
|
import { assertSafe } from "./safety.js";
|
|
10
11
|
import { templateArgs } from "./util.js";
|
|
@@ -202,7 +203,7 @@ function walkUpForMemDir(fromDir: string, acceptEmpty: boolean): string | null {
|
|
|
202
203
|
const boundary = repoRootOf(start) ?? "/";
|
|
203
204
|
let dir = start;
|
|
204
205
|
while (true) {
|
|
205
|
-
const candidate = join(dir,
|
|
206
|
+
const candidate = join(dir, DEFAULT_MEM_DIR);
|
|
206
207
|
if (existsSync(candidate) && (acceptEmpty || hasMemLogs(candidate))) {
|
|
207
208
|
return candidate;
|
|
208
209
|
}
|
|
@@ -254,8 +255,8 @@ function findMemDirsUnder(root: string): string[] {
|
|
|
254
255
|
}
|
|
255
256
|
for (const e of entries) {
|
|
256
257
|
if (!e.isDirectory()) continue;
|
|
257
|
-
if (e.name ===
|
|
258
|
-
const candidate = join(dir,
|
|
258
|
+
if (e.name === FAPONY_DIR) {
|
|
259
|
+
const candidate = join(dir, DEFAULT_MEM_DIR);
|
|
259
260
|
if (hasMemLogs(candidate)) found.push(candidate);
|
|
260
261
|
continue;
|
|
261
262
|
}
|
|
@@ -298,7 +299,7 @@ function resolveMemDirFrom(
|
|
|
298
299
|
// path source here — spec §1 pins this to fapony.config.json)
|
|
299
300
|
const configDir = root ?? cwd;
|
|
300
301
|
try {
|
|
301
|
-
const configPath = join(configDir,
|
|
302
|
+
const configPath = join(configDir, CONFIG_FILENAME);
|
|
302
303
|
if (existsSync(configPath)) {
|
|
303
304
|
const config = loadConfig(configPath);
|
|
304
305
|
if (config?.paths?.memDir) {
|
|
@@ -328,7 +329,7 @@ function resolveMemDirFrom(
|
|
|
328
329
|
|
|
329
330
|
// Step 4: <repo root>/.fapony/.memory/ — where a new log is created
|
|
330
331
|
if (root) {
|
|
331
|
-
const rootDir = join(root,
|
|
332
|
+
const rootDir = join(root, DEFAULT_MEM_DIR);
|
|
332
333
|
if (existsSync(rootDir)) return { dir: rootDir, step: "repo-root" };
|
|
333
334
|
}
|
|
334
335
|
|
package/src/plan-seed.ts
CHANGED
|
@@ -38,6 +38,7 @@ import { basename, join, relative, resolve, sep } from "node:path";
|
|
|
38
38
|
import { collectSourceFiles, isSkippedDir, SCAN_EXTS } from "./analyze.js";
|
|
39
39
|
import { computeModelFit } from "./context/projectHealth.js";
|
|
40
40
|
import { doneDir, planDir, specDir } from "./db/getters.js";
|
|
41
|
+
import { CONFIG_FILENAME } from "./db/index.js";
|
|
41
42
|
import { loadConfig } from "./db/load.js";
|
|
42
43
|
import type { Config } from "./db/types.js";
|
|
43
44
|
import { extractExports } from "./map.js";
|
|
@@ -97,7 +98,7 @@ function renderPriorArt(cwd: string, config: Config, roots: string[]): string {
|
|
|
97
98
|
if (keys.length === 0) return placeholder;
|
|
98
99
|
|
|
99
100
|
const hits: { shipped: string; line: string }[] = [];
|
|
100
|
-
for (const dir of [doneDir(config), specDir(
|
|
101
|
+
for (const dir of [doneDir(config), specDir()]) {
|
|
101
102
|
const abs = join(cwd, dir);
|
|
102
103
|
let names: string[];
|
|
103
104
|
try {
|
|
@@ -524,7 +525,7 @@ export function cmdPlanSeed(args: string[]): void {
|
|
|
524
525
|
process.exit(1);
|
|
525
526
|
}
|
|
526
527
|
const cwd = process.cwd();
|
|
527
|
-
const config = loadConfig(join(cwd,
|
|
528
|
+
const config = loadConfig(join(cwd, CONFIG_FILENAME));
|
|
528
529
|
// Resolve the git worktree root so mem/ledger queries hit the same key
|
|
529
530
|
// state.db uses (git rev-parse --show-toplevel). Running from a subdir
|
|
530
531
|
// would otherwise mismatch: stats return empty, Context (fapony) always
|
|
@@ -568,7 +569,7 @@ export function cmdPlanSeed(args: string[]): void {
|
|
|
568
569
|
);
|
|
569
570
|
}
|
|
570
571
|
|
|
571
|
-
const planDirAbs = join(cwd, planDir(
|
|
572
|
+
const planDirAbs = join(cwd, planDir());
|
|
572
573
|
const planPath = join(planDirAbs, `PLAN-${name}.md`);
|
|
573
574
|
if (existsSync(planPath)) {
|
|
574
575
|
console.error(
|
|
@@ -582,7 +583,7 @@ export function cmdPlanSeed(args: string[]): void {
|
|
|
582
583
|
|
|
583
584
|
let specLink: string | null = null;
|
|
584
585
|
if (withSpec) {
|
|
585
|
-
const specDirAbs = join(cwd, specDir(
|
|
586
|
+
const specDirAbs = join(cwd, specDir());
|
|
586
587
|
const specPath = join(specDirAbs, `SPEC-${name}.md`);
|
|
587
588
|
if (existsSync(specPath)) {
|
|
588
589
|
console.error(
|
|
@@ -602,7 +603,7 @@ export function cmdPlanSeed(args: string[]): void {
|
|
|
602
603
|
: null,
|
|
603
604
|
),
|
|
604
605
|
);
|
|
605
|
-
specLink = `../${specDir(
|
|
606
|
+
specLink = `../${specDir().split("/").pop()}/SPEC-${name}.md`;
|
|
606
607
|
}
|
|
607
608
|
|
|
608
609
|
mkdirSync(planDirAbs, { recursive: true });
|
|
@@ -613,7 +614,7 @@ export function cmdPlanSeed(args: string[]): void {
|
|
|
613
614
|
priorArt,
|
|
614
615
|
contextFapony,
|
|
615
616
|
specLink,
|
|
616
|
-
`${planDir(
|
|
617
|
+
`${planDir()}/PLAN-${name}.md`,
|
|
617
618
|
),
|
|
618
619
|
);
|
|
619
620
|
console.log(`wrote ${planPath}${specLink ? ` + SPEC-${name}.md` : ""}`);
|
package/src/setup.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { existsSync, statSync, writeFileSync } from "node:fs";
|
|
|
6
6
|
import { join, resolve } from "node:path";
|
|
7
7
|
import { createInterface } from "node:readline";
|
|
8
8
|
import { seedConventionsFile } from "./conventions-seed.js";
|
|
9
|
+
import { CONFIG_FILENAME, FAPONY_DIR } from "./db/index.js";
|
|
9
10
|
import { initProject } from "./init.js";
|
|
10
11
|
import { isAffirmative } from "./util.js";
|
|
11
12
|
|
|
@@ -181,7 +182,7 @@ export async function cmdSetup(deps: SetupDeps = {}): Promise<void> {
|
|
|
181
182
|
enableMemory,
|
|
182
183
|
});
|
|
183
184
|
|
|
184
|
-
const configPath = join(process.cwd(),
|
|
185
|
+
const configPath = join(process.cwd(), CONFIG_FILENAME);
|
|
185
186
|
if (existsSync(configPath)) {
|
|
186
187
|
const overwrite = await askFn(
|
|
187
188
|
"⚠ fapony.config.json already exists. Overwrite? (y/n)",
|
|
@@ -197,7 +198,7 @@ export async function cmdSetup(deps: SetupDeps = {}): Promise<void> {
|
|
|
197
198
|
console.log(`\n ✓ Wrote ${configPath}`);
|
|
198
199
|
|
|
199
200
|
// --- scaffold worktree ---
|
|
200
|
-
const faponyDir = join(worktreePath,
|
|
201
|
+
const faponyDir = join(worktreePath, FAPONY_DIR);
|
|
201
202
|
if (!existsSync(faponyDir)) {
|
|
202
203
|
try {
|
|
203
204
|
initProject(worktreePath);
|
package/src/stats/data.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
// src/stats/data.ts — StatsData shape + getStatsData()
|
|
2
2
|
|
|
3
|
-
import { readdirSync
|
|
3
|
+
import { readdirSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
|
|
6
|
-
import { type Event, openDb, type Run } from "../db/index.js";
|
|
5
|
+
import { type Event, openDb, PLAN_DIR, type Run } from "../db/index.js";
|
|
7
6
|
import { loadConfig } from "../db/load.js";
|
|
8
7
|
import { enrichGateWindows } from "../gates.js";
|
|
9
8
|
import { avg, minutesBetween } from "../math.js";
|
|
@@ -89,9 +88,7 @@ function enrichGates(
|
|
|
89
88
|
/**
|
|
90
89
|
* Count un-shipped plan files in a worktree's planDir.
|
|
91
90
|
*
|
|
92
|
-
*
|
|
93
|
-
* central config's worktrees map is optional and usually absent, and each repo
|
|
94
|
-
* picks its own plan dir (vela uses apps/vela/plan, not .fapony/plan).
|
|
91
|
+
* planDir is hardcoded to .fapony/plan — not configurable (gitignored = private).
|
|
95
92
|
*
|
|
96
93
|
* Returns null — never 0 — when the path isn't a readable directory, so a
|
|
97
94
|
* sentinel row like "mcp-external" renders as "—" instead of claiming
|
|
@@ -99,19 +96,10 @@ function enrichGates(
|
|
|
99
96
|
*/
|
|
100
97
|
export function countPendingPlans(worktree: string): number | null {
|
|
101
98
|
if (!worktree.startsWith("/")) return null;
|
|
102
|
-
let planDir = ".fapony/plan";
|
|
103
|
-
try {
|
|
104
|
-
const cfg = JSON.parse(
|
|
105
|
-
readFileSync(join(worktree, "fapony.config.json"), "utf8"),
|
|
106
|
-
) as { paths?: { planDir?: unknown } };
|
|
107
|
-
if (typeof cfg.paths?.planDir === "string" && cfg.paths.planDir)
|
|
108
|
-
planDir = cfg.paths.planDir;
|
|
109
|
-
} catch {
|
|
110
|
-
// no config (or unreadable/malformed) — fall back to the scaffold default
|
|
111
|
-
}
|
|
112
99
|
try {
|
|
113
|
-
return readdirSync(join(worktree,
|
|
114
|
-
.
|
|
100
|
+
return readdirSync(join(worktree, PLAN_DIR)).filter((f) =>
|
|
101
|
+
f.endsWith(".md"),
|
|
102
|
+
).length;
|
|
115
103
|
} catch {
|
|
116
104
|
return null;
|
|
117
105
|
}
|