@juicesharp/rpiv-pi 0.5.1 → 0.6.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 +2 -0
- package/extensions/rpiv-core/agents.ts +4 -14
- package/extensions/rpiv-core/git-context.ts +2 -4
- package/extensions/rpiv-core/guidance.ts +3 -7
- package/extensions/rpiv-core/package-checks.ts +1 -1
- package/extensions/rpiv-core/pi-installer.ts +7 -3
- package/extensions/rpiv-core/session-hooks.ts +7 -14
- package/extensions/rpiv-core/setup-command.ts +2 -4
- package/extensions/rpiv-core/siblings.ts +5 -0
- package/extensions/rpiv-core/update-agents-command.ts +6 -4
- package/package.json +27 -8
- package/scripts/migrate.js +222 -219
package/README.md
CHANGED
|
@@ -145,6 +145,7 @@ Invoke via `/skill:<name>` from inside a Pi Agent session.
|
|
|
145
145
|
| `/rpiv-setup` | Install all sibling plugins in one go |
|
|
146
146
|
| `/rpiv-update-agents` | Sync rpiv agent profiles: add new, update changed, remove stale |
|
|
147
147
|
| `/advisor` | Configure advisor model and reasoning effort |
|
|
148
|
+
| `/btw` | Ask a side question without polluting the main conversation |
|
|
148
149
|
| `/todos` | Show current todo list |
|
|
149
150
|
| `/web-search-config` | Set Brave Search API key |
|
|
150
151
|
|
|
@@ -180,6 +181,7 @@ Pi Agent discovers extensions via `"extensions": ["./extensions"]` and skills vi
|
|
|
180
181
|
|
|
181
182
|
- **Web search** — run `/web-search-config` to set the Brave Search API key, or set the `BRAVE_SEARCH_API_KEY` environment variable
|
|
182
183
|
- **Advisor** — run `/advisor` to select a reviewer model and reasoning effort
|
|
184
|
+
- **Side questions** — type `/btw <question>` anytime (even mid-stream) to ask the primary model a one-off question; answer appears in a borderless bottom overlay and never enters the main conversation
|
|
183
185
|
- **Agent concurrency** — `@tintinweb/pi-subagents` defaults to 4 concurrent agents; raise via `/agents → Settings → Max concurrency → 48` if skills stall on wide fan-outs
|
|
184
186
|
- **Agent profiles** — editable at `<cwd>/.pi/agents/`; sync from bundled defaults with `/rpiv-update-agents` (overwrites rpiv-managed files, preserves your custom agents)
|
|
185
187
|
|
|
@@ -4,17 +4,9 @@
|
|
|
4
4
|
* Pure utility. No ExtensionAPI interactions.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
mkdirSync,
|
|
10
|
-
readdirSync,
|
|
11
|
-
copyFileSync,
|
|
12
|
-
readFileSync,
|
|
13
|
-
writeFileSync,
|
|
14
|
-
unlinkSync,
|
|
15
|
-
} from "node:fs";
|
|
7
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { dirname, join } from "node:path";
|
|
16
9
|
import { fileURLToPath } from "node:url";
|
|
17
|
-
import { join, dirname } from "node:path";
|
|
18
10
|
|
|
19
11
|
// ---------------------------------------------------------------------------
|
|
20
12
|
// Package-root resolution
|
|
@@ -103,7 +95,7 @@ function readManifest(targetDir: string): string[] {
|
|
|
103
95
|
function writeManifest(targetDir: string, filenames: string[]): void {
|
|
104
96
|
const manifestPath = join(targetDir, MANIFEST_FILE);
|
|
105
97
|
try {
|
|
106
|
-
writeFileSync(manifestPath, JSON.stringify(filenames, null, 2)
|
|
98
|
+
writeFileSync(manifestPath, `${JSON.stringify(filenames, null, 2)}\n`, "utf-8");
|
|
107
99
|
} catch {
|
|
108
100
|
// non-fatal — sync results will still be correct for this run;
|
|
109
101
|
// next run will re-bootstrap if manifest is missing
|
|
@@ -269,9 +261,7 @@ export function syncBundledAgents(cwd: string, apply: boolean): SyncResult {
|
|
|
269
261
|
// apply=true: stale files were removed, so manifest = sourceEntries.
|
|
270
262
|
// apply=false: stale files still exist on disk and must stay tracked
|
|
271
263
|
// so the next apply can remove them.
|
|
272
|
-
const manifestEntries = apply
|
|
273
|
-
? sourceEntries
|
|
274
|
-
: [...sourceEntries, ...result.pendingRemove];
|
|
264
|
+
const manifestEntries = apply ? sourceEntries : [...sourceEntries, ...result.pendingRemove];
|
|
275
265
|
writeManifest(targetDir, manifestEntries);
|
|
276
266
|
|
|
277
267
|
return result;
|
|
@@ -17,7 +17,7 @@ type GitContext = { branch: string; commit: string; user: string };
|
|
|
17
17
|
let lastInjectedSig: string | null = null;
|
|
18
18
|
|
|
19
19
|
// undefined = not loaded yet, null = not a git repo / failed, object = valid
|
|
20
|
-
let cache: GitContext | null | undefined
|
|
20
|
+
let cache: GitContext | null | undefined;
|
|
21
21
|
|
|
22
22
|
export async function getGitContext(pi: ExtensionAPI): Promise<GitContext | null> {
|
|
23
23
|
if (cache !== undefined) return cache;
|
|
@@ -75,7 +75,5 @@ export async function takeGitContextIfChanged(pi: ExtensionAPI): Promise<string
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
export function isGitMutatingCommand(cmd: string): boolean {
|
|
78
|
-
return /\bgit\s+(checkout|switch|commit|merge|rebase|pull|reset|revert|cherry-pick|worktree|am|stash)\b/.test(
|
|
79
|
-
cmd,
|
|
80
|
-
);
|
|
78
|
+
return /\bgit\s+(checkout|switch|commit|merge|rebase|pull|reset|revert|cherry-pick|worktree|am|stash)\b/.test(cmd);
|
|
81
79
|
}
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
import { existsSync, readFileSync } from "node:fs";
|
|
23
|
-
import { dirname,
|
|
23
|
+
import { dirname, isAbsolute, join, relative, sep } from "node:path";
|
|
24
24
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
25
25
|
import { FLAG_DEBUG, MSG_TYPE_GUIDANCE } from "./constants.js";
|
|
26
26
|
|
|
@@ -181,9 +181,7 @@ export function handleToolCallGuidance(
|
|
|
181
181
|
injectedGuidance.add(g.relativePath);
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
-
const contextParts = newFiles.map(
|
|
185
|
-
(g) => `## Project Guidance: ${formatLabel(g)}\n\n${g.content}`,
|
|
186
|
-
);
|
|
184
|
+
const contextParts = newFiles.map((g) => `## Project Guidance: ${formatLabel(g)}\n\n${g.content}`);
|
|
187
185
|
|
|
188
186
|
pi.sendMessage({
|
|
189
187
|
customType: MSG_TYPE_GUIDANCE,
|
|
@@ -202,9 +200,7 @@ export function handleToolCallGuidance(
|
|
|
202
200
|
function formatLabel(g: GuidanceFile): string {
|
|
203
201
|
if (g.kind === "architecture") {
|
|
204
202
|
const stripped = g.relativePath.replace(/^\.rpiv\/guidance\//, "");
|
|
205
|
-
const sub = stripped === "architecture.md"
|
|
206
|
-
? ""
|
|
207
|
-
: stripped.replace(/\/architecture\.md$/, "");
|
|
203
|
+
const sub = stripped === "architecture.md" ? "" : stripped.replace(/\/architecture\.md$/, "");
|
|
208
204
|
return `${sub || "root"} (architecture.md)`;
|
|
209
205
|
}
|
|
210
206
|
const fileName = g.kind === "agents" ? "AGENTS.md" : "CLAUDE.md";
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { existsSync, readFileSync } from "node:fs";
|
|
7
|
-
import { join } from "node:path";
|
|
8
7
|
import { homedir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
9
|
import { SIBLINGS, type SiblingPlugin } from "./siblings.js";
|
|
10
10
|
|
|
11
11
|
const PI_AGENT_SETTINGS = join(homedir(), ".pi", "agent", "settings.json");
|
|
@@ -27,8 +27,12 @@ export function spawnPiInstall(pkg: string, timeoutMs: number): Promise<PiInstal
|
|
|
27
27
|
let stderr = "";
|
|
28
28
|
|
|
29
29
|
const proc = spawn(cmd, args, { ...spawnOpts, stdio: ["ignore", "pipe", "pipe"] });
|
|
30
|
-
proc.stdout?.on("data", (d) =>
|
|
31
|
-
|
|
30
|
+
proc.stdout?.on("data", (d) => {
|
|
31
|
+
stdout += d.toString();
|
|
32
|
+
});
|
|
33
|
+
proc.stderr?.on("data", (d) => {
|
|
34
|
+
stderr += d.toString();
|
|
35
|
+
});
|
|
32
36
|
|
|
33
37
|
const settle = (result: PiInstallResult) => {
|
|
34
38
|
if (settled) return;
|
|
@@ -42,7 +46,7 @@ export function spawnPiInstall(pkg: string, timeoutMs: number): Promise<PiInstal
|
|
|
42
46
|
setTimeout(() => {
|
|
43
47
|
if (!proc.killed) proc.kill("SIGKILL");
|
|
44
48
|
}, 5000);
|
|
45
|
-
settle({ code: 124, stdout, stderr: stderr
|
|
49
|
+
settle({ code: 124, stdout, stderr: `${stderr}\n[timed out after ${timeoutMs}ms]` });
|
|
46
50
|
}, timeoutMs);
|
|
47
51
|
|
|
48
52
|
proc.on("error", (err) => {
|
|
@@ -7,16 +7,16 @@
|
|
|
7
7
|
|
|
8
8
|
import { mkdirSync } from "node:fs";
|
|
9
9
|
import { join } from "node:path";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import { type ExtensionAPI, isToolCallEventType } from "@mariozechner/pi-coding-agent";
|
|
11
|
+
import { type SyncResult, syncBundledAgents } from "./agents.js";
|
|
12
|
+
import { FLAG_DEBUG, MSG_TYPE_GIT_CONTEXT } from "./constants.js";
|
|
12
13
|
import {
|
|
13
14
|
clearGitContextCache,
|
|
14
15
|
isGitMutatingCommand,
|
|
15
16
|
resetInjectedMarker,
|
|
16
17
|
takeGitContextIfChanged,
|
|
17
18
|
} from "./git-context.js";
|
|
18
|
-
import {
|
|
19
|
-
import { FLAG_DEBUG, MSG_TYPE_GIT_CONTEXT } from "./constants.js";
|
|
19
|
+
import { clearInjectionState, handleToolCallGuidance, injectRootGuidance } from "./guidance.js";
|
|
20
20
|
import { findMissingSiblings } from "./package-checks.js";
|
|
21
21
|
|
|
22
22
|
const THOUGHTS_DIRS = [
|
|
@@ -28,8 +28,7 @@ const THOUGHTS_DIRS = [
|
|
|
28
28
|
] as const;
|
|
29
29
|
|
|
30
30
|
const msgAgentsAdded = (n: number) => `Copied ${n} rpiv-pi agent(s) to .pi/agents/`;
|
|
31
|
-
const msgAgentsDrift = (parts: string[]) =>
|
|
32
|
-
`${parts.join(", ")} agent(s). Run /rpiv-update-agents to sync.`;
|
|
31
|
+
const msgAgentsDrift = (parts: string[]) => `${parts.join(", ")} agent(s). Run /rpiv-update-agents to sync.`;
|
|
33
32
|
const msgMissingSiblings = (n: number, list: string) =>
|
|
34
33
|
`rpiv-pi requires ${n} sibling extension(s): ${list}. Run /rpiv-setup to install them.`;
|
|
35
34
|
|
|
@@ -90,10 +89,7 @@ function scaffoldThoughtsDirs(cwd: string): void {
|
|
|
90
89
|
}
|
|
91
90
|
}
|
|
92
91
|
|
|
93
|
-
async function injectGitContext(
|
|
94
|
-
pi: ExtensionAPI,
|
|
95
|
-
send: (msg: string) => void,
|
|
96
|
-
): Promise<void> {
|
|
92
|
+
async function injectGitContext(pi: ExtensionAPI, send: (msg: string) => void): Promise<void> {
|
|
97
93
|
const msg = await takeGitContextIfChanged(pi);
|
|
98
94
|
if (msg) send(msg);
|
|
99
95
|
}
|
|
@@ -113,8 +109,5 @@ function notifyAgentSyncDrift(ui: UI, result: SyncResult): void {
|
|
|
113
109
|
function warnMissingSiblings(ui: UI): void {
|
|
114
110
|
const missing = findMissingSiblings();
|
|
115
111
|
if (missing.length === 0) return;
|
|
116
|
-
ui.notify(
|
|
117
|
-
msgMissingSiblings(missing.length, missing.map((m) => m.pkg.replace(/^npm:/, "")).join(", ")),
|
|
118
|
-
"warning",
|
|
119
|
-
);
|
|
112
|
+
ui.notify(msgMissingSiblings(missing.length, missing.map((m) => m.pkg.replace(/^npm:/, "")).join(", ")), "warning");
|
|
120
113
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
9
9
|
import { findMissingSiblings } from "./package-checks.js";
|
|
10
10
|
import { spawnPiInstall } from "./pi-installer.js";
|
|
11
|
-
import {
|
|
11
|
+
import type { SiblingPlugin } from "./siblings.js";
|
|
12
12
|
|
|
13
13
|
const INSTALL_TIMEOUT_MS = 120_000;
|
|
14
14
|
const STDERR_SNIPPET_CHARS = 300;
|
|
@@ -82,9 +82,7 @@ async function installMissing(
|
|
|
82
82
|
} else {
|
|
83
83
|
failed.push({
|
|
84
84
|
pkg,
|
|
85
|
-
error: (result.stderr || result.stdout || `exit ${result.code}`)
|
|
86
|
-
.trim()
|
|
87
|
-
.slice(0, STDERR_SNIPPET_CHARS),
|
|
85
|
+
error: (result.stderr || result.stdout || `exit ${result.code}`).trim().slice(0, STDERR_SNIPPET_CHARS),
|
|
88
86
|
});
|
|
89
87
|
}
|
|
90
88
|
} catch (err) {
|
|
@@ -40,6 +40,11 @@ export const SIBLINGS: readonly SiblingPlugin[] = [
|
|
|
40
40
|
matches: /rpiv-advisor/i,
|
|
41
41
|
provides: "advisor tool + /advisor command",
|
|
42
42
|
},
|
|
43
|
+
{
|
|
44
|
+
pkg: "npm:@juicesharp/rpiv-btw",
|
|
45
|
+
matches: /rpiv-btw/i,
|
|
46
|
+
provides: "/btw side-question command",
|
|
47
|
+
},
|
|
43
48
|
{
|
|
44
49
|
pkg: "npm:@juicesharp/rpiv-web-tools",
|
|
45
50
|
matches: /rpiv-web-tools/i,
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
7
|
-
import {
|
|
7
|
+
import { type SyncResult, syncBundledAgents } from "./agents.js";
|
|
8
8
|
|
|
9
9
|
const MSG_UP_TO_DATE = "All agents already up-to-date.";
|
|
10
10
|
const MSG_NO_CHANGES = "No changes needed.";
|
|
@@ -15,8 +15,7 @@ const msgSyncedWithErrors = (summary: string, errors: string[]) =>
|
|
|
15
15
|
|
|
16
16
|
export function registerUpdateAgentsCommand(pi: ExtensionAPI): void {
|
|
17
17
|
pi.registerCommand("rpiv-update-agents", {
|
|
18
|
-
description:
|
|
19
|
-
"Sync rpiv-pi bundled agents into .pi/agents/: add new, update changed, remove stale",
|
|
18
|
+
description: "Sync rpiv-pi bundled agents into .pi/agents/: add new, update changed, remove stale",
|
|
20
19
|
handler: async (_args, ctx) => {
|
|
21
20
|
const result = syncBundledAgents(ctx.cwd, true);
|
|
22
21
|
if (!ctx.hasUI) return;
|
|
@@ -36,7 +35,10 @@ function formatSyncReport(result: SyncResult): string {
|
|
|
36
35
|
|
|
37
36
|
const summary = parts.length > 0 ? msgSynced(parts) : MSG_NO_CHANGES;
|
|
38
37
|
if (result.errors.length > 0) {
|
|
39
|
-
return msgSyncedWithErrors(
|
|
38
|
+
return msgSyncedWithErrors(
|
|
39
|
+
summary,
|
|
40
|
+
result.errors.map((e) => e.message),
|
|
41
|
+
);
|
|
40
42
|
}
|
|
41
43
|
return summary;
|
|
42
44
|
}
|
package/package.json
CHANGED
|
@@ -1,26 +1,44 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juicesharp/rpiv-pi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Skill-based development workflow for Pi Agent — discover, research, design, plan, implement, validate",
|
|
5
|
-
"keywords": [
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package",
|
|
7
|
+
"pi-extension",
|
|
8
|
+
"rpiv",
|
|
9
|
+
"skills",
|
|
10
|
+
"workflow"
|
|
11
|
+
],
|
|
6
12
|
"license": "MIT",
|
|
7
13
|
"author": "juicesharp",
|
|
8
14
|
"type": "module",
|
|
9
15
|
"repository": {
|
|
10
16
|
"type": "git",
|
|
11
|
-
"url": "git+https://github.com/juicesharp/rpiv-
|
|
17
|
+
"url": "git+https://github.com/juicesharp/rpiv-mono.git",
|
|
18
|
+
"directory": "packages/rpiv-pi"
|
|
12
19
|
},
|
|
13
|
-
"homepage": "https://github.com/juicesharp/rpiv-pi#readme",
|
|
20
|
+
"homepage": "https://github.com/juicesharp/rpiv-mono/tree/main/packages/rpiv-pi#readme",
|
|
14
21
|
"bugs": {
|
|
15
|
-
"url": "https://github.com/juicesharp/rpiv-
|
|
22
|
+
"url": "https://github.com/juicesharp/rpiv-mono/issues"
|
|
16
23
|
},
|
|
17
24
|
"publishConfig": {
|
|
18
25
|
"access": "public"
|
|
19
26
|
},
|
|
20
|
-
"files": [
|
|
27
|
+
"files": [
|
|
28
|
+
"extensions/",
|
|
29
|
+
"skills/",
|
|
30
|
+
"agents/",
|
|
31
|
+
"scripts/",
|
|
32
|
+
"README.md",
|
|
33
|
+
"LICENSE"
|
|
34
|
+
],
|
|
21
35
|
"pi": {
|
|
22
|
-
"extensions": [
|
|
23
|
-
|
|
36
|
+
"extensions": [
|
|
37
|
+
"./extensions"
|
|
38
|
+
],
|
|
39
|
+
"skills": [
|
|
40
|
+
"./skills"
|
|
41
|
+
]
|
|
24
42
|
},
|
|
25
43
|
"peerDependencies": {
|
|
26
44
|
"@mariozechner/pi-coding-agent": "*",
|
|
@@ -28,6 +46,7 @@
|
|
|
28
46
|
"@juicesharp/rpiv-ask-user-question": "*",
|
|
29
47
|
"@juicesharp/rpiv-todo": "*",
|
|
30
48
|
"@juicesharp/rpiv-advisor": "*",
|
|
49
|
+
"@juicesharp/rpiv-btw": "*",
|
|
31
50
|
"@juicesharp/rpiv-web-tools": "*"
|
|
32
51
|
}
|
|
33
52
|
}
|
package/scripts/migrate.js
CHANGED
|
@@ -1,242 +1,245 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "fs";
|
|
3
|
+
import { dirname, join, relative, sep } from "path";
|
|
4
|
+
|
|
4
5
|
// --- CLI Argument Parsing ---
|
|
5
6
|
function parseArgs(argv) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
return { projectDir, deleteOriginals, dryRun, force };
|
|
7
|
+
let projectDir = process.cwd();
|
|
8
|
+
let deleteOriginals = false;
|
|
9
|
+
let dryRun = false;
|
|
10
|
+
let force = false;
|
|
11
|
+
for (let i = 2; i < argv.length; i++) {
|
|
12
|
+
if (argv[i] === "--project-dir" && argv[i + 1]) {
|
|
13
|
+
projectDir = argv[++i];
|
|
14
|
+
} else if (argv[i] === "--delete-originals") {
|
|
15
|
+
deleteOriginals = true;
|
|
16
|
+
} else if (argv[i] === "--dry-run") {
|
|
17
|
+
dryRun = true;
|
|
18
|
+
} else if (argv[i] === "--force") {
|
|
19
|
+
force = true;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return { projectDir, deleteOriginals, dryRun, force };
|
|
25
23
|
}
|
|
26
24
|
// --- Discovery ---
|
|
27
25
|
const HARDCODED_EXCLUDES = new Set([
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
"node_modules",
|
|
27
|
+
"dist",
|
|
28
|
+
"build",
|
|
29
|
+
".git",
|
|
30
|
+
"vendor",
|
|
31
|
+
".rpiv",
|
|
32
|
+
".next",
|
|
33
|
+
".nuxt",
|
|
34
|
+
".output",
|
|
35
|
+
"coverage",
|
|
36
|
+
"__pycache__",
|
|
37
|
+
".venv",
|
|
30
38
|
]);
|
|
31
39
|
function discoverClaudeMdFiles(projectDir) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
40
|
+
const gitDir = join(projectDir, ".git");
|
|
41
|
+
if (existsSync(gitDir)) {
|
|
42
|
+
return discoverViaGit(projectDir);
|
|
43
|
+
}
|
|
44
|
+
return discoverViaWalk(projectDir);
|
|
37
45
|
}
|
|
38
46
|
function discoverViaGit(projectDir) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
47
|
+
try {
|
|
48
|
+
const output = execSync("git ls-files --cached --others --exclude-standard", {
|
|
49
|
+
cwd: projectDir,
|
|
50
|
+
encoding: "utf-8",
|
|
51
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
52
|
+
});
|
|
53
|
+
return output
|
|
54
|
+
.split("\n")
|
|
55
|
+
.filter((f) => f.endsWith("/CLAUDE.md") || f === "CLAUDE.md")
|
|
56
|
+
.filter((f) => !f.startsWith(".rpiv/"));
|
|
57
|
+
} catch {
|
|
58
|
+
// git command failed — fall back to walk
|
|
59
|
+
return discoverViaWalk(projectDir);
|
|
60
|
+
}
|
|
54
61
|
}
|
|
55
62
|
function discoverViaWalk(projectDir) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
walk(projectDir);
|
|
88
|
-
return results;
|
|
63
|
+
const results = [];
|
|
64
|
+
function walk(dir) {
|
|
65
|
+
let entries;
|
|
66
|
+
try {
|
|
67
|
+
entries = readdirSync(dir);
|
|
68
|
+
} catch {
|
|
69
|
+
return; // permission error, skip
|
|
70
|
+
}
|
|
71
|
+
for (const entry of entries) {
|
|
72
|
+
if (HARDCODED_EXCLUDES.has(entry)) continue;
|
|
73
|
+
const fullPath = join(dir, entry);
|
|
74
|
+
let stat;
|
|
75
|
+
try {
|
|
76
|
+
stat = statSync(fullPath);
|
|
77
|
+
} catch {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (stat.isDirectory()) {
|
|
81
|
+
walk(fullPath);
|
|
82
|
+
} else if (entry === "CLAUDE.md") {
|
|
83
|
+
const rel = relative(projectDir, fullPath).split(sep).join("/");
|
|
84
|
+
if (!rel.startsWith(".rpiv/")) {
|
|
85
|
+
results.push(rel);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
walk(projectDir);
|
|
91
|
+
return results;
|
|
89
92
|
}
|
|
90
93
|
// --- Path Mapping ---
|
|
91
94
|
function computeTargetPath(claudeMdRelative) {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
const dir = dirname(claudeMdRelative);
|
|
96
|
+
if (dir === ".") {
|
|
97
|
+
return ".rpiv/guidance/architecture.md";
|
|
98
|
+
}
|
|
99
|
+
return join(".rpiv", "guidance", dir, "architecture.md").split(sep).join("/");
|
|
97
100
|
}
|
|
98
101
|
function transformContent(content, targetPath) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
102
|
+
let refsTransformed = 0;
|
|
103
|
+
const warnings = [];
|
|
104
|
+
// Pattern 1: Backtick-wrapped path references like `src/core/CLAUDE.md`
|
|
105
|
+
let transformed = content.replace(/`((?:[\w][\w./-]*\/)?CLAUDE\.md)`/g, (_match, claudePath) => {
|
|
106
|
+
const replacement = claudePathToGuidancePath(claudePath);
|
|
107
|
+
refsTransformed++;
|
|
108
|
+
return `\`${replacement}\``;
|
|
109
|
+
});
|
|
110
|
+
// Pattern 2: Bare path references (with directory prefix) not inside backticks
|
|
111
|
+
// Match things like "src/core/CLAUDE.md" but not already-backtick-wrapped
|
|
112
|
+
transformed = transformed.replace(/(?<!`)([\w][\w./-]*\/CLAUDE\.md)(?!`)/g, (_match, claudePath) => {
|
|
113
|
+
const replacement = claudePathToGuidancePath(claudePath);
|
|
114
|
+
refsTransformed++;
|
|
115
|
+
return replacement;
|
|
116
|
+
});
|
|
117
|
+
// Pattern 3: Standalone "CLAUDE.md" that references the root file
|
|
118
|
+
// Only match when it looks like a file reference (not part of a longer word)
|
|
119
|
+
// Avoid matching inside paths already transformed above
|
|
120
|
+
transformed = transformed.replace(/(?<![/\w`])CLAUDE\.md(?![/\w`])/g, () => {
|
|
121
|
+
refsTransformed++;
|
|
122
|
+
return ".rpiv/guidance/architecture.md";
|
|
123
|
+
});
|
|
124
|
+
// Scan for remaining prose references that might need manual attention
|
|
125
|
+
const lines = transformed.split("\n");
|
|
126
|
+
for (let i = 0; i < lines.length; i++) {
|
|
127
|
+
// Look for prose patterns like "see X CLAUDE.md" or "X layer CLAUDE.md"
|
|
128
|
+
if (
|
|
129
|
+
/\b\w+\s+CLAUDE\.md\b/i.test(content.split("\n")[i] ?? "") &&
|
|
130
|
+
!/(src|lib|app|packages|apps)\//.test(content.split("\n")[i] ?? "")
|
|
131
|
+
) {
|
|
132
|
+
// Check if this line still has an untransformed prose reference
|
|
133
|
+
if (/CLAUDE\.md/i.test(lines[i])) {
|
|
134
|
+
warnings.push({
|
|
135
|
+
file: targetPath,
|
|
136
|
+
line: i + 1,
|
|
137
|
+
message: `Prose reference to CLAUDE.md may need manual update: "${lines[i].trim()}"`,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return { content: transformed, refsTransformed, warnings };
|
|
138
143
|
}
|
|
139
144
|
function claudePathToGuidancePath(claudePath) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
+
const dir = dirname(claudePath);
|
|
146
|
+
if (dir === ".") {
|
|
147
|
+
return ".rpiv/guidance/architecture.md";
|
|
148
|
+
}
|
|
149
|
+
return `.rpiv/guidance/${dir}/architecture.md`;
|
|
145
150
|
}
|
|
146
151
|
// --- Main ---
|
|
147
152
|
function main() {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
};
|
|
240
|
-
process.stdout.write(JSON.stringify(report, null, 2));
|
|
153
|
+
const { projectDir, deleteOriginals, dryRun, force } = parseArgs(process.argv);
|
|
154
|
+
process.stderr.write(`[rpiv:migrate] scanning ${projectDir} for CLAUDE.md files\n`);
|
|
155
|
+
const claudeFiles = discoverClaudeMdFiles(projectDir);
|
|
156
|
+
if (claudeFiles.length === 0) {
|
|
157
|
+
const report = {
|
|
158
|
+
migrated: [],
|
|
159
|
+
conflicts: [],
|
|
160
|
+
warnings: [],
|
|
161
|
+
originalsDeleted: false,
|
|
162
|
+
dryRun,
|
|
163
|
+
};
|
|
164
|
+
process.stdout.write(JSON.stringify(report, null, 2));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
process.stderr.write(`[rpiv:migrate] found ${claudeFiles.length} CLAUDE.md file(s)\n`);
|
|
168
|
+
const migrated = [];
|
|
169
|
+
const conflicts = [];
|
|
170
|
+
const allWarnings = [];
|
|
171
|
+
const writtenFiles = [];
|
|
172
|
+
for (const source of claudeFiles) {
|
|
173
|
+
const target = computeTargetPath(source);
|
|
174
|
+
const targetAbs = join(projectDir, target);
|
|
175
|
+
// Check for conflicts
|
|
176
|
+
if (existsSync(targetAbs) && !force) {
|
|
177
|
+
conflicts.push(target);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
// Read source content
|
|
181
|
+
const sourceAbs = join(projectDir, source);
|
|
182
|
+
let content;
|
|
183
|
+
try {
|
|
184
|
+
content = readFileSync(sourceAbs, "utf-8");
|
|
185
|
+
} catch (err) {
|
|
186
|
+
allWarnings.push({
|
|
187
|
+
file: source,
|
|
188
|
+
line: 0,
|
|
189
|
+
message: `Failed to read: ${err instanceof Error ? err.message : String(err)}`,
|
|
190
|
+
});
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (content.trim().length === 0) {
|
|
194
|
+
allWarnings.push({
|
|
195
|
+
file: source,
|
|
196
|
+
line: 0,
|
|
197
|
+
message: "Empty file, skipped",
|
|
198
|
+
});
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
// Transform content
|
|
202
|
+
const { content: transformed, refsTransformed, warnings } = transformContent(content, target);
|
|
203
|
+
const lines = transformed.split("\n").length;
|
|
204
|
+
migrated.push({ source, target, lines, refsTransformed });
|
|
205
|
+
allWarnings.push(...warnings);
|
|
206
|
+
if (!dryRun) {
|
|
207
|
+
writtenFiles.push({ targetAbs, content: transformed });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
// Write all files (all-or-nothing approach for safety)
|
|
211
|
+
if (!dryRun) {
|
|
212
|
+
for (const { targetAbs, content } of writtenFiles) {
|
|
213
|
+
mkdirSync(dirname(targetAbs), { recursive: true });
|
|
214
|
+
writeFileSync(targetAbs, content, "utf-8");
|
|
215
|
+
}
|
|
216
|
+
process.stderr.write(`[rpiv:migrate] wrote ${writtenFiles.length} file(s)\n`);
|
|
217
|
+
}
|
|
218
|
+
// Delete originals only after all writes succeed
|
|
219
|
+
let originalsDeleted = false;
|
|
220
|
+
if (!dryRun && deleteOriginals && writtenFiles.length > 0) {
|
|
221
|
+
for (const entry of migrated) {
|
|
222
|
+
const sourceAbs = join(projectDir, entry.source);
|
|
223
|
+
try {
|
|
224
|
+
unlinkSync(sourceAbs);
|
|
225
|
+
} catch (err) {
|
|
226
|
+
allWarnings.push({
|
|
227
|
+
file: entry.source,
|
|
228
|
+
line: 0,
|
|
229
|
+
message: `Failed to delete original: ${err instanceof Error ? err.message : String(err)}`,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
originalsDeleted = true;
|
|
234
|
+
process.stderr.write(`[rpiv:migrate] deleted ${migrated.length} original CLAUDE.md file(s)\n`);
|
|
235
|
+
}
|
|
236
|
+
const report = {
|
|
237
|
+
migrated,
|
|
238
|
+
conflicts,
|
|
239
|
+
warnings: allWarnings,
|
|
240
|
+
originalsDeleted,
|
|
241
|
+
dryRun,
|
|
242
|
+
};
|
|
243
|
+
process.stdout.write(JSON.stringify(report, null, 2));
|
|
241
244
|
}
|
|
242
245
|
main();
|