fapony 0.3.6 → 0.5.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/README.md +181 -427
- package/package.json +2 -1
- package/skill/lookup-before-edit/SKILL.md +2 -2
- package/skill/review-pony/SKILL.md +2 -2
- package/src/adapters/hooks/compute-hint-impact.ts +12 -1
- package/src/adapters/hooks/context-data.ts +31 -5
- package/src/adapters/hooks/edit-hint.ts +10 -0
- package/src/adapters/hooks/index.ts +15 -0
- package/src/adapters/hooks/read-hint.ts +10 -0
- package/src/adapters/hooks/stop.ts +347 -7
- package/src/adapters/mcp/tools/index.ts +13 -1
- package/src/adapters/mcp/tools/mem.ts +30 -5
- package/src/adapters/mcp/transport.ts +1 -1
- package/src/core/hint-log.ts +16 -1
- package/src/core/mem-log.ts +18 -0
- package/src/debt/cli.ts +25 -10
- package/src/hook.ts +15 -0
- package/src/install/antigravity.ts +18 -7
- package/src/install/detect.ts +8 -5
- package/src/install/opencode.ts +142 -177
- package/src/install.ts +20 -8
- package/src/mem/commands/plan.ts +70 -0
- package/src/mem/commands/read.ts +73 -18
- package/src/mem/commands/write.ts +75 -18
- package/src/mem/engine.ts +68 -2
- package/src/mem/index.ts +7 -5
- package/src/mem/render.ts +4 -1
- package/src/mem/selectors.ts +13 -0
- package/src/mem/store.ts +2 -0
- package/src/seed/plan-seed.ts +139 -10
- package/src/seed/review-seed.ts +141 -54
- package/src/update.ts +60 -3
package/src/install/opencode.ts
CHANGED
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Adds mcp.fapony config to ~/.config/opencode/opencode.json or opencode.jsonc.
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
existsSync,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
readdirSync,
|
|
9
|
+
readFileSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
} from "node:fs";
|
|
6
12
|
import { homedir } from "node:os";
|
|
7
13
|
import { join } from "node:path";
|
|
8
14
|
import { claudeSkillsDir, linkSkills, reportSkills } from "./skills.js";
|
|
@@ -84,6 +90,11 @@ function isConfigured(mcp: Record<string, unknown> | undefined): boolean {
|
|
|
84
90
|
export interface OpencodeInstallOpts {
|
|
85
91
|
/** Opt-in only: write the git-autonomy rewrite plugin. Never default. */
|
|
86
92
|
gitAutonomy?: boolean;
|
|
93
|
+
/** Refresh only the generated plugin bodies — skip the opencode.json write
|
|
94
|
+
* and the skills symlink entirely. This is `fapony update`'s post-pull
|
|
95
|
+
* refresh: it must touch only fapony-owned plugin files, never the user's
|
|
96
|
+
* config (rule 6c — overwriting what exists = ask first, or refuse). */
|
|
97
|
+
pluginsOnly?: boolean;
|
|
87
98
|
}
|
|
88
99
|
|
|
89
100
|
export function cmdInstallOpencode(
|
|
@@ -94,6 +105,21 @@ export function cmdInstallOpencode(
|
|
|
94
105
|
const exitFn = deps.exit ?? defaultExit;
|
|
95
106
|
const getHome = deps.homedir ?? homedir;
|
|
96
107
|
|
|
108
|
+
const installPlugins = (): void => {
|
|
109
|
+
installReadHintPlugin(dryRun, getHome);
|
|
110
|
+
installCommitHintPlugin(dryRun, getHome);
|
|
111
|
+
installEditHintPlugin(dryRun, getHome);
|
|
112
|
+
installSessionStartPlugin(dryRun, getHome);
|
|
113
|
+
if (opts.gitAutonomy) installGitAutonomyPlugin(dryRun, getHome);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// Refresh path: plugin bodies only — returns before findOpencodeConfig, so
|
|
117
|
+
// neither opencode.json nor the skills symlink is read or written.
|
|
118
|
+
if (opts.pluginsOnly) {
|
|
119
|
+
installPlugins();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
97
123
|
const configPath = findOpencodeConfig(getHome);
|
|
98
124
|
let before: Record<string, unknown>;
|
|
99
125
|
let isNew = false;
|
|
@@ -117,11 +143,7 @@ export function cmdInstallOpencode(
|
|
|
117
143
|
if (configPath) console.error(` (${configPath})`);
|
|
118
144
|
const skillsDir = claudeSkillsDir(getHome);
|
|
119
145
|
reportSkills(linkSkills(skillsDir, dryRun), skillsDir, dryRun);
|
|
120
|
-
|
|
121
|
-
installCommitHintPlugin(dryRun, getHome);
|
|
122
|
-
installEditHintPlugin(dryRun, getHome);
|
|
123
|
-
installSessionStartPlugin(dryRun, getHome);
|
|
124
|
-
if (opts.gitAutonomy) installGitAutonomyPlugin(dryRun, getHome);
|
|
146
|
+
installPlugins();
|
|
125
147
|
return;
|
|
126
148
|
}
|
|
127
149
|
|
|
@@ -143,11 +165,7 @@ export function cmdInstallOpencode(
|
|
|
143
165
|
console.error(` (${configPath})`);
|
|
144
166
|
}
|
|
145
167
|
console.log(computeDiff(before, after));
|
|
146
|
-
|
|
147
|
-
installCommitHintPlugin(dryRun, getHome);
|
|
148
|
-
installEditHintPlugin(dryRun, getHome);
|
|
149
|
-
installSessionStartPlugin(dryRun, getHome);
|
|
150
|
-
if (opts.gitAutonomy) installGitAutonomyPlugin(dryRun, getHome);
|
|
168
|
+
installPlugins();
|
|
151
169
|
return;
|
|
152
170
|
}
|
|
153
171
|
|
|
@@ -167,11 +185,7 @@ export function cmdInstallOpencode(
|
|
|
167
185
|
console.error(` restart opencode to load the MCP server`);
|
|
168
186
|
const skillsDir = claudeSkillsDir(getHome);
|
|
169
187
|
reportSkills(linkSkills(skillsDir, dryRun), skillsDir, dryRun);
|
|
170
|
-
|
|
171
|
-
installCommitHintPlugin(dryRun, getHome);
|
|
172
|
-
installEditHintPlugin(dryRun, getHome);
|
|
173
|
-
installSessionStartPlugin(dryRun, getHome);
|
|
174
|
-
if (opts.gitAutonomy) installGitAutonomyPlugin(dryRun, getHome);
|
|
188
|
+
installPlugins();
|
|
175
189
|
}
|
|
176
190
|
|
|
177
191
|
/**
|
|
@@ -257,6 +271,9 @@ export const FaponyReadHint = async ({ directory }) => {
|
|
|
257
271
|
if (ctx.memLines.length > 0) {
|
|
258
272
|
recordHintFire({ ts: new Date().toISOString(), worktree: ctx.worktree, surface: "mem", file: rel, count: ctx.memLines.length });
|
|
259
273
|
}
|
|
274
|
+
if (ctx.openBugIds.length > 0) {
|
|
275
|
+
recordHintFire({ ts: new Date().toISOString(), worktree: ctx.worktree, surface: "open-bug", file: rel, count: ctx.openBugIds.length, ids: ctx.openBugIds });
|
|
276
|
+
}
|
|
260
277
|
}
|
|
261
278
|
} catch {
|
|
262
279
|
// a hint must never break a read
|
|
@@ -398,6 +415,9 @@ export const FaponyEditHint = async ({ directory }) => {
|
|
|
398
415
|
if (ctx?.memLines?.length) {
|
|
399
416
|
recordHintFire({ ts: new Date().toISOString(), worktree, surface: "mem", file: rel, count: ctx.memLines.length });
|
|
400
417
|
}
|
|
418
|
+
if (ctx?.openBugIds?.length) {
|
|
419
|
+
recordHintFire({ ts: new Date().toISOString(), worktree, surface: "open-bug", file: rel, count: ctx.openBugIds.length, ids: ctx.openBugIds });
|
|
420
|
+
}
|
|
401
421
|
}
|
|
402
422
|
} catch {
|
|
403
423
|
// best-effort
|
|
@@ -412,102 +432,102 @@ export const FaponyEditHint = async ({ directory }) => {
|
|
|
412
432
|
`;
|
|
413
433
|
}
|
|
414
434
|
|
|
415
|
-
|
|
435
|
+
/**
|
|
436
|
+
* Write one generated OpenCode plugin file, refreshing our own older copy.
|
|
437
|
+
*
|
|
438
|
+
* Ownership is the exported plugin name (`FaponyReadHint`, …) — the one token
|
|
439
|
+
* every generated body carries. A file without it is someone's own plugin and
|
|
440
|
+
* is never touched, same rule as the skill symlinks. A file *with* it but
|
|
441
|
+
* different content is ours and stale: the plugin body is baked at install
|
|
442
|
+
* time, so a `git pull` updates the imported logic (src/hook.ts) but never this
|
|
443
|
+
* file. Refusing to overwrite it is how an old hook stays reachable forever —
|
|
444
|
+
* the Thai-only bug marker survived exactly that way — so ours gets rewritten
|
|
445
|
+
* in place, and only a foreign file is reported and left alone.
|
|
446
|
+
*/
|
|
447
|
+
function installPluginFile(
|
|
448
|
+
dryRun: boolean,
|
|
449
|
+
getHome: () => string,
|
|
450
|
+
fileName: string,
|
|
451
|
+
label: string,
|
|
452
|
+
ownedName: string,
|
|
453
|
+
desired: string,
|
|
454
|
+
): void {
|
|
416
455
|
const pluginsDir = join(getHome(), ".config", "opencode", "plugins");
|
|
417
|
-
const pluginPath = join(pluginsDir,
|
|
418
|
-
|
|
419
|
-
|
|
456
|
+
const pluginPath = join(pluginsDir, fileName);
|
|
457
|
+
const write = (): boolean => {
|
|
458
|
+
if (dryRun) return true;
|
|
420
459
|
try {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
460
|
+
mkdirSync(pluginsDir, { recursive: true });
|
|
461
|
+
writeFileSync(pluginPath, desired, "utf-8");
|
|
462
|
+
return true;
|
|
463
|
+
} catch (e) {
|
|
464
|
+
console.error(` ${label}: failed to write — ${(e as Error).message}`);
|
|
465
|
+
return false;
|
|
424
466
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
if (!existsSync(pluginPath)) {
|
|
470
|
+
if (write()) {
|
|
471
|
+
console.error(
|
|
472
|
+
` ${label}: ${dryRun ? "would write" : "wrote"} ${pluginPath}`,
|
|
473
|
+
);
|
|
428
474
|
}
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
let current = "";
|
|
479
|
+
try {
|
|
480
|
+
current = readFileSync(pluginPath, "utf-8");
|
|
481
|
+
} catch {
|
|
482
|
+
current = "";
|
|
483
|
+
}
|
|
484
|
+
if (current === desired) {
|
|
485
|
+
console.error(` ${label}: already installed — no change`);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
if (!current.includes(ownedName)) {
|
|
429
489
|
console.error(
|
|
430
|
-
`
|
|
490
|
+
` ${label}: ${pluginPath} exists but isn't fapony's — not overwriting.`,
|
|
431
491
|
);
|
|
432
492
|
return;
|
|
433
493
|
}
|
|
434
|
-
if (
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
} catch (e) {
|
|
439
|
-
console.error(` read hint: failed to write — ${(e as Error).message}`);
|
|
440
|
-
return;
|
|
441
|
-
}
|
|
494
|
+
if (write()) {
|
|
495
|
+
console.error(
|
|
496
|
+
` ${label}: ${dryRun ? "would update" : "updated"} ${pluginPath}`,
|
|
497
|
+
);
|
|
442
498
|
}
|
|
443
|
-
|
|
444
|
-
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function installReadHintPlugin(dryRun: boolean, getHome: () => string): void {
|
|
502
|
+
installPluginFile(
|
|
503
|
+
dryRun,
|
|
504
|
+
getHome,
|
|
505
|
+
"fapony-read-hint.ts",
|
|
506
|
+
"read hint",
|
|
507
|
+
"FaponyReadHint",
|
|
508
|
+
readHintPluginSource(INSTALL_ROOT),
|
|
445
509
|
);
|
|
446
510
|
}
|
|
447
511
|
|
|
448
512
|
function installCommitHintPlugin(dryRun: boolean, getHome: () => string): void {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
current = "";
|
|
457
|
-
}
|
|
458
|
-
if (current.includes("commitHintFor")) {
|
|
459
|
-
console.error(` commit hint: already installed — no change`);
|
|
460
|
-
return;
|
|
461
|
-
}
|
|
462
|
-
console.error(
|
|
463
|
-
` commit hint: ${pluginPath} exists but isn't fapony's — not overwriting.`,
|
|
464
|
-
);
|
|
465
|
-
return;
|
|
466
|
-
}
|
|
467
|
-
if (!dryRun) {
|
|
468
|
-
try {
|
|
469
|
-
mkdirSync(pluginsDir, { recursive: true });
|
|
470
|
-
writeFileSync(pluginPath, commitHintPluginSource(INSTALL_ROOT), "utf-8");
|
|
471
|
-
} catch (e) {
|
|
472
|
-
console.error(` commit hint: failed to write — ${(e as Error).message}`);
|
|
473
|
-
return;
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
console.error(
|
|
477
|
-
` commit hint: ${dryRun ? "would write" : "wrote"} ${pluginPath}`,
|
|
513
|
+
installPluginFile(
|
|
514
|
+
dryRun,
|
|
515
|
+
getHome,
|
|
516
|
+
"fapony-commit-hint.ts",
|
|
517
|
+
"commit hint",
|
|
518
|
+
"FaponyCommitHint",
|
|
519
|
+
commitHintPluginSource(INSTALL_ROOT),
|
|
478
520
|
);
|
|
479
521
|
}
|
|
480
522
|
|
|
481
523
|
function installEditHintPlugin(dryRun: boolean, getHome: () => string): void {
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
current = "";
|
|
490
|
-
}
|
|
491
|
-
if (current.includes("editHintFor")) {
|
|
492
|
-
console.error(` edit hint: already installed — no change`);
|
|
493
|
-
return;
|
|
494
|
-
}
|
|
495
|
-
console.error(
|
|
496
|
-
` edit hint: ${pluginPath} exists but isn't fapony's — not overwriting.`,
|
|
497
|
-
);
|
|
498
|
-
return;
|
|
499
|
-
}
|
|
500
|
-
if (!dryRun) {
|
|
501
|
-
try {
|
|
502
|
-
mkdirSync(pluginsDir, { recursive: true });
|
|
503
|
-
writeFileSync(pluginPath, editHintPluginSource(INSTALL_ROOT), "utf-8");
|
|
504
|
-
} catch (e) {
|
|
505
|
-
console.error(` edit hint: failed to write — ${(e as Error).message}`);
|
|
506
|
-
return;
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
console.error(
|
|
510
|
-
` edit hint: ${dryRun ? "would write" : "wrote"} ${pluginPath}`,
|
|
524
|
+
installPluginFile(
|
|
525
|
+
dryRun,
|
|
526
|
+
getHome,
|
|
527
|
+
"fapony-edit-hint.ts",
|
|
528
|
+
"edit hint",
|
|
529
|
+
"FaponyEditHint",
|
|
530
|
+
editHintPluginSource(INSTALL_ROOT),
|
|
511
531
|
);
|
|
512
532
|
}
|
|
513
533
|
|
|
@@ -571,50 +591,13 @@ function installSessionStartPlugin(
|
|
|
571
591
|
dryRun: boolean,
|
|
572
592
|
getHome: () => string,
|
|
573
593
|
): void {
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
current = "";
|
|
582
|
-
}
|
|
583
|
-
if (current.includes("FaponySessionStart")) {
|
|
584
|
-
// Ours — and generated, so never overwritten. A file baked by an older
|
|
585
|
-
// fapony differs from what we generate now: say so instead of "no change",
|
|
586
|
-
// which is how the pre-hook plugin (mub2ezhi) would have been kept.
|
|
587
|
-
if (current !== sessionStartPluginSource(INSTALL_ROOT)) {
|
|
588
|
-
console.error(
|
|
589
|
-
` session start: stale fapony plugin — delete ${pluginPath} and re-run install`,
|
|
590
|
-
);
|
|
591
|
-
return;
|
|
592
|
-
}
|
|
593
|
-
console.error(` session start: already installed — no change`);
|
|
594
|
-
return;
|
|
595
|
-
}
|
|
596
|
-
console.error(
|
|
597
|
-
` session start: ${pluginPath} exists but isn't fapony's — not overwriting.`,
|
|
598
|
-
);
|
|
599
|
-
return;
|
|
600
|
-
}
|
|
601
|
-
if (!dryRun) {
|
|
602
|
-
try {
|
|
603
|
-
mkdirSync(pluginsDir, { recursive: true });
|
|
604
|
-
writeFileSync(
|
|
605
|
-
pluginPath,
|
|
606
|
-
sessionStartPluginSource(INSTALL_ROOT),
|
|
607
|
-
"utf-8",
|
|
608
|
-
);
|
|
609
|
-
} catch (e) {
|
|
610
|
-
console.error(
|
|
611
|
-
` session start: failed to write — ${(e as Error).message}`,
|
|
612
|
-
);
|
|
613
|
-
return;
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
console.error(
|
|
617
|
-
` session start: ${dryRun ? "would write" : "wrote"} ${pluginPath}`,
|
|
594
|
+
installPluginFile(
|
|
595
|
+
dryRun,
|
|
596
|
+
getHome,
|
|
597
|
+
"fapony-session-start.ts",
|
|
598
|
+
"session start",
|
|
599
|
+
"FaponySessionStart",
|
|
600
|
+
sessionStartPluginSource(INSTALL_ROOT),
|
|
618
601
|
);
|
|
619
602
|
}
|
|
620
603
|
|
|
@@ -680,45 +663,27 @@ function installGitAutonomyPlugin(
|
|
|
680
663
|
dryRun: boolean,
|
|
681
664
|
getHome: () => string,
|
|
682
665
|
): void {
|
|
666
|
+
installPluginFile(
|
|
667
|
+
dryRun,
|
|
668
|
+
getHome,
|
|
669
|
+
"fapony-git-autonomy.ts",
|
|
670
|
+
"git-autonomy",
|
|
671
|
+
"FaponyGitAutonomy",
|
|
672
|
+
gitAutonomyPluginSource(INSTALL_ROOT),
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/** The fapony-generated plugin files present in an OpenCode install. Empty =
|
|
677
|
+
* OpenCode has none, so `fapony update` must not install into a client the user
|
|
678
|
+
* never opted into. Callers also use it to tell whether the opt-in git-autonomy
|
|
679
|
+
* plugin is installed, so an update can refresh it without ever creating it. */
|
|
680
|
+
export function opencodePluginFiles(getHome: () => string = homedir): string[] {
|
|
683
681
|
const pluginsDir = join(getHome(), ".config", "opencode", "plugins");
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
try {
|
|
688
|
-
current = readFileSync(pluginPath, "utf-8");
|
|
689
|
-
} catch {
|
|
690
|
-
current = "";
|
|
691
|
-
}
|
|
692
|
-
if (current.includes("FaponyGitAutonomy")) {
|
|
693
|
-
// Ours — and generated, so never overwritten. Same stale-check shape
|
|
694
|
-
// as the session-start plugin: an older generation must say so instead
|
|
695
|
-
// of "no change", or a fixed rewrite never reaches the client.
|
|
696
|
-
if (current !== gitAutonomyPluginSource(INSTALL_ROOT)) {
|
|
697
|
-
console.error(
|
|
698
|
-
` git-autonomy: stale fapony plugin — delete ${pluginPath} and re-run install --git-autonomy`,
|
|
699
|
-
);
|
|
700
|
-
return;
|
|
701
|
-
}
|
|
702
|
-
console.error(` git-autonomy: already installed — no change`);
|
|
703
|
-
return;
|
|
704
|
-
}
|
|
705
|
-
console.error(
|
|
706
|
-
` git-autonomy: ${pluginPath} exists but isn't fapony's — not overwriting.`,
|
|
682
|
+
try {
|
|
683
|
+
return readdirSync(pluginsDir).filter(
|
|
684
|
+
(f) => f.startsWith("fapony-") && f.endsWith(".ts"),
|
|
707
685
|
);
|
|
708
|
-
|
|
686
|
+
} catch {
|
|
687
|
+
return [];
|
|
709
688
|
}
|
|
710
|
-
if (!dryRun) {
|
|
711
|
-
try {
|
|
712
|
-
mkdirSync(pluginsDir, { recursive: true });
|
|
713
|
-
writeFileSync(pluginPath, gitAutonomyPluginSource(INSTALL_ROOT), "utf-8");
|
|
714
|
-
} catch (e) {
|
|
715
|
-
console.error(
|
|
716
|
-
` git-autonomy: failed to write — ${(e as Error).message}`,
|
|
717
|
-
);
|
|
718
|
-
return;
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
console.error(
|
|
722
|
-
` git-autonomy: ${dryRun ? "would write" : "wrote"} ${pluginPath}`,
|
|
723
|
-
);
|
|
724
689
|
}
|
package/src/install.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// src/install.ts — `fapony install --platform opencode|claude|cursor|zcode|codex` command.
|
|
1
|
+
// src/install.ts — `fapony install --platform antigravity|agy|opencode|claude|cursor|zcode|codex` command.
|
|
2
2
|
// opencode: adds mcp.fapony config to ~/.config/opencode/opencode.json or opencode.jsonc.
|
|
3
3
|
// claude: shells out to `claude mcp add` (never parses/writes ~/.claude.json directly).
|
|
4
4
|
// cursor: writes mcpServers.fapony to ~/.cursor/mcp.json directly and merges the
|
|
@@ -39,7 +39,9 @@ export {
|
|
|
39
39
|
editHintPluginSource,
|
|
40
40
|
gitAutonomyPluginSource,
|
|
41
41
|
type OpencodeInstallOpts,
|
|
42
|
+
opencodePluginFiles,
|
|
42
43
|
readHintPluginSource,
|
|
44
|
+
sessionStartPluginSource,
|
|
43
45
|
} from "./install/opencode.js";
|
|
44
46
|
export {
|
|
45
47
|
agentsSkillsDir,
|
|
@@ -57,13 +59,17 @@ export async function cmdInstall(
|
|
|
57
59
|
args: string[],
|
|
58
60
|
deps: InstallDeps = {},
|
|
59
61
|
): Promise<void> {
|
|
60
|
-
const
|
|
62
|
+
const platformArg = args.find((a) => !a.startsWith("--"));
|
|
63
|
+
// `agy` = Antigravity's CLI name — alias, same provider.
|
|
64
|
+
const platform = platformArg === "agy" ? "antigravity" : platformArg;
|
|
61
65
|
const dryRun = args.includes("--dry-run");
|
|
62
66
|
const installAll = args.includes("--all");
|
|
63
67
|
// Opt-in only: the git-autonomy rewrite is an opinion (commit-as-you-go),
|
|
64
68
|
// not a utility — never installed unless this flag rides along, including
|
|
65
69
|
// via --all. Opencode-only; other platforms ignore it.
|
|
66
70
|
const gitAutonomy = args.includes("--git-autonomy");
|
|
71
|
+
// Refresh-only seam for `fapony update`: plugin bodies, never opencode.json.
|
|
72
|
+
const pluginsOnly = args.includes("--plugins-only");
|
|
67
73
|
|
|
68
74
|
// --- explicit platform: original behavior, unchanged ---
|
|
69
75
|
if (platform === "antigravity") {
|
|
@@ -87,15 +93,15 @@ export async function cmdInstall(
|
|
|
87
93
|
return;
|
|
88
94
|
}
|
|
89
95
|
if (platform === "opencode") {
|
|
90
|
-
cmdInstallOpencode(dryRun, deps, { gitAutonomy });
|
|
96
|
+
cmdInstallOpencode(dryRun, deps, { gitAutonomy, pluginsOnly });
|
|
91
97
|
return;
|
|
92
98
|
}
|
|
93
99
|
if (platform !== undefined) {
|
|
94
100
|
console.error(
|
|
95
|
-
`usage: fapony install --platform antigravity|opencode|claude|cursor|zcode|codex [--dry-run] [--git-autonomy]`,
|
|
101
|
+
`usage: fapony install --platform antigravity|agy|opencode|claude|cursor|zcode|codex [--dry-run] [--git-autonomy] [--plugins-only]`,
|
|
96
102
|
);
|
|
97
103
|
console.error(
|
|
98
|
-
` supported platforms: antigravity, opencode, claude, cursor, zcode, codex`,
|
|
104
|
+
` supported platforms: antigravity (agy), opencode, claude, cursor, zcode, codex`,
|
|
99
105
|
);
|
|
100
106
|
(deps.exit ?? defaultExit)(1);
|
|
101
107
|
return;
|
|
@@ -131,7 +137,10 @@ export async function cmdInstall(
|
|
|
131
137
|
if (installAll) {
|
|
132
138
|
console.error();
|
|
133
139
|
for (const client of found) {
|
|
134
|
-
installPlatform(client.platform, dryRun, deps, {
|
|
140
|
+
installPlatform(client.platform, dryRun, deps, {
|
|
141
|
+
gitAutonomy,
|
|
142
|
+
pluginsOnly,
|
|
143
|
+
});
|
|
135
144
|
}
|
|
136
145
|
return;
|
|
137
146
|
}
|
|
@@ -160,7 +169,10 @@ export async function cmdInstall(
|
|
|
160
169
|
for (const client of found) {
|
|
161
170
|
const answer = await askFn(`install into ${client.platform}?`, "Y");
|
|
162
171
|
if (isAffirmative(answer)) {
|
|
163
|
-
installPlatform(client.platform, dryRun, deps, {
|
|
172
|
+
installPlatform(client.platform, dryRun, deps, {
|
|
173
|
+
gitAutonomy,
|
|
174
|
+
pluginsOnly,
|
|
175
|
+
});
|
|
164
176
|
}
|
|
165
177
|
}
|
|
166
178
|
} finally {
|
|
@@ -172,7 +184,7 @@ function installPlatform(
|
|
|
172
184
|
platform: string,
|
|
173
185
|
dryRun: boolean,
|
|
174
186
|
deps: InstallDeps,
|
|
175
|
-
opts: { gitAutonomy?: boolean } = {},
|
|
187
|
+
opts: { gitAutonomy?: boolean; pluginsOnly?: boolean } = {},
|
|
176
188
|
): void {
|
|
177
189
|
switch (platform) {
|
|
178
190
|
case "antigravity":
|
package/src/mem/commands/plan.ts
CHANGED
|
@@ -190,6 +190,68 @@ export const collectDepIssues = (active: string[]): string[] => {
|
|
|
190
190
|
return issues;
|
|
191
191
|
};
|
|
192
192
|
|
|
193
|
+
// W1: status header says not-started but chunks are ticked.
|
|
194
|
+
// W2: all chunks ticked but header never marked shipped.
|
|
195
|
+
// WARN-level only — counted separately from issues, never changes exit code.
|
|
196
|
+
//
|
|
197
|
+
// Vocab measured from .fapony/{plan,done}/*.md (2026-09-23):
|
|
198
|
+
// in-progress 8 · done 4 · shipped/✅ shipped 6 · draft 1
|
|
199
|
+
// frontmatter status: shipped 8 · blocked 2
|
|
200
|
+
const NOT_STARTED =
|
|
201
|
+
/(?:^|\n)>\s*\*?\*?Status:?\*?\*?\s+.*(?:draft|drafted|not[\s-]started)/i;
|
|
202
|
+
const SHIPPED_RE =
|
|
203
|
+
/(?:^|\n)>\s*✅|(?:^|\n)>\s*\*?\*?Status:?\*?\*?\s+.*(?:shipped|done)/i;
|
|
204
|
+
const FM_SHIPPED = /^status:\s*shipped\b/m;
|
|
205
|
+
|
|
206
|
+
export const collectDriftWarns = (active: string[]): string[] => {
|
|
207
|
+
const warns: string[] = [];
|
|
208
|
+
for (const f of active) {
|
|
209
|
+
const fm = parsePlanFrontmatter(f);
|
|
210
|
+
// Skip plans whose frontmatter already says shipped/blocked/superseded or
|
|
211
|
+
// is a tracker — these have their own handling elsewhere.
|
|
212
|
+
if (FM_SHIPPED.test("") && fm.status === "shipped") continue;
|
|
213
|
+
if (fm.status === "blocked" || fm.status === "superseded") continue;
|
|
214
|
+
if (fm.kind === "tracker") continue;
|
|
215
|
+
// frontmatter status: shipped = already done
|
|
216
|
+
if (fm.status === "shipped") continue;
|
|
217
|
+
|
|
218
|
+
const text = readFileSync(f, "utf8");
|
|
219
|
+
const relPath = relative(planBase, f);
|
|
220
|
+
const { checked, unchecked } = countFirstSection(f);
|
|
221
|
+
const total = checked + unchecked;
|
|
222
|
+
if (total === 0) continue;
|
|
223
|
+
|
|
224
|
+
// W1: header says not-started but chunks are ticked
|
|
225
|
+
const head = text.slice(0, 2048);
|
|
226
|
+
if (
|
|
227
|
+
checked > 0 &&
|
|
228
|
+
NOT_STARTED.test(head) &&
|
|
229
|
+
!SHIPPED_RE.test(head) &&
|
|
230
|
+
!FM_SHIPPED.test(text)
|
|
231
|
+
) {
|
|
232
|
+
// Extract the status value for the message
|
|
233
|
+
const statusMatch = />\s*\*?\*?Status:?\*?\*?\s+(.+)/i.exec(head);
|
|
234
|
+
const statusVal = statusMatch?.[1]?.replace(/\*\*/g, "").trim() ?? "?";
|
|
235
|
+
warns.push(
|
|
236
|
+
`${relPath} — status header says "${statusVal}" but ${checked} chunk(s) are ticked\n fix: update the header to 🚧 in-progress or ✅ shipped`,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// W2: all chunks ticked but header never marked shipped
|
|
241
|
+
if (
|
|
242
|
+
checked > 0 &&
|
|
243
|
+
unchecked === 0 &&
|
|
244
|
+
!SHIPPED_RE.test(head) &&
|
|
245
|
+
!FM_SHIPPED.test(text)
|
|
246
|
+
) {
|
|
247
|
+
warns.push(
|
|
248
|
+
`${relPath} — all ${checked} chunk(s) ticked but header never marked shipped\n fix: add ✅ shipped to the header or run ${planSweepCmd} ${basename(f)} --apply`,
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return warns;
|
|
253
|
+
};
|
|
254
|
+
|
|
193
255
|
// status:blocked with every first-section chunk ticked = deferred doc debt:
|
|
194
256
|
// the work reads done but the plan stays in plan/ forever (shippedNotMoved
|
|
195
257
|
// never lists it — HELD excludes it). Trackers never finish, so they are out.
|
|
@@ -735,6 +797,10 @@ export const cmdPlanCheck = (a: string[]) => {
|
|
|
735
797
|
// (HELD excludes it), so without this flag it sits in plan/ silently.
|
|
736
798
|
for (const issue of collectBlockedTickedIssues(active)) issues.push(issue);
|
|
737
799
|
|
|
800
|
+
// 7) Drift warns — W1 (not-started header + ticks) and W2 (all ticked +
|
|
801
|
+
// not shipped). WARN-only: counted separately, never changes exit code.
|
|
802
|
+
const driftWarns = collectDriftWarns(active);
|
|
803
|
+
|
|
738
804
|
if (!quiet) {
|
|
739
805
|
console.log(
|
|
740
806
|
`closed chunks: ${closed} · citing a commit: ${citing} · verified: ${verified}`,
|
|
@@ -759,6 +825,10 @@ export const cmdPlanCheck = (a: string[]) => {
|
|
|
759
825
|
);
|
|
760
826
|
}
|
|
761
827
|
}
|
|
828
|
+
if (driftWarns.length > 0) {
|
|
829
|
+
console.log(`\n⚠ ${driftWarns.length} drift warning(s) (not blocking):`);
|
|
830
|
+
for (const w of driftWarns) console.log(`- ${w}`);
|
|
831
|
+
}
|
|
762
832
|
}
|
|
763
833
|
|
|
764
834
|
if (issues.length === 0) {
|