@juicesharp/rpiv-pi 0.6.0 → 0.7.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.
@@ -4,17 +4,9 @@
4
4
  * Pure utility. No ExtensionAPI interactions.
5
5
  */
6
6
 
7
- import {
8
- existsSync,
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) + "\n", "utf-8");
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 = 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, relative, sep, isAbsolute, join } from "node:path";
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) => (stdout += d.toString()));
31
- proc.stderr?.on("data", (d) => (stderr += d.toString()));
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 + `\n[timed out after ${timeoutMs}ms]` });
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 { isToolCallEventType, type ExtensionAPI } from "@mariozechner/pi-coding-agent";
11
- import { clearInjectionState, handleToolCallGuidance, injectRootGuidance } from "./guidance.js";
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 { syncBundledAgents, type SyncResult } from "./agents.js";
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 { type SiblingPlugin } from "./siblings.js";
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) {
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
7
- import { syncBundledAgents, type SyncResult } from "./agents.js";
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(summary, result.errors.map((e) => e.message));
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.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Skill-based development workflow for Pi Agent — discover, research, design, plan, implement, validate",
5
- "keywords": ["pi-package", "pi-extension", "rpiv", "skills", "workflow"],
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-pi.git"
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-pi/issues"
22
+ "url": "https://github.com/juicesharp/rpiv-mono/issues"
16
23
  },
17
24
  "publishConfig": {
18
25
  "access": "public"
19
26
  },
20
- "files": ["extensions/", "skills/", "agents/", "scripts/", "README.md", "LICENSE"],
27
+ "files": [
28
+ "extensions/",
29
+ "skills/",
30
+ "agents/",
31
+ "scripts/",
32
+ "README.md",
33
+ "LICENSE"
34
+ ],
21
35
  "pi": {
22
- "extensions": ["./extensions"],
23
- "skills": ["./skills"]
36
+ "extensions": [
37
+ "./extensions"
38
+ ],
39
+ "skills": [
40
+ "./skills"
41
+ ]
24
42
  },
25
43
  "peerDependencies": {
26
44
  "@mariozechner/pi-coding-agent": "*",
@@ -1,242 +1,245 @@
1
- import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, readdirSync, statSync } from 'fs';
2
- import { join, dirname, relative, sep } from 'path';
3
- import { execSync } from 'child_process';
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
- let projectDir = process.cwd();
7
- let deleteOriginals = false;
8
- let dryRun = false;
9
- let force = false;
10
- for (let i = 2; i < argv.length; i++) {
11
- if (argv[i] === '--project-dir' && argv[i + 1]) {
12
- projectDir = argv[++i];
13
- }
14
- else if (argv[i] === '--delete-originals') {
15
- deleteOriginals = true;
16
- }
17
- else if (argv[i] === '--dry-run') {
18
- dryRun = true;
19
- }
20
- else if (argv[i] === '--force') {
21
- force = true;
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
- 'node_modules', 'dist', 'build', '.git', 'vendor', '.rpiv',
29
- '.next', '.nuxt', '.output', 'coverage', '__pycache__', '.venv',
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
- const gitDir = join(projectDir, '.git');
33
- if (existsSync(gitDir)) {
34
- return discoverViaGit(projectDir);
35
- }
36
- return discoverViaWalk(projectDir);
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
- try {
40
- const output = execSync('git ls-files --cached --others --exclude-standard', {
41
- cwd: projectDir,
42
- encoding: 'utf-8',
43
- maxBuffer: 10 * 1024 * 1024,
44
- });
45
- return output
46
- .split('\n')
47
- .filter((f) => f.endsWith('/CLAUDE.md') || f === 'CLAUDE.md')
48
- .filter((f) => !f.startsWith('.rpiv/'));
49
- }
50
- catch {
51
- // git command failed — fall back to walk
52
- return discoverViaWalk(projectDir);
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
- const results = [];
57
- function walk(dir) {
58
- let entries;
59
- try {
60
- entries = readdirSync(dir);
61
- }
62
- catch {
63
- return; // permission error, skip
64
- }
65
- for (const entry of entries) {
66
- if (HARDCODED_EXCLUDES.has(entry))
67
- continue;
68
- const fullPath = join(dir, entry);
69
- let stat;
70
- try {
71
- stat = statSync(fullPath);
72
- }
73
- catch {
74
- continue;
75
- }
76
- if (stat.isDirectory()) {
77
- walk(fullPath);
78
- }
79
- else if (entry === 'CLAUDE.md') {
80
- const rel = relative(projectDir, fullPath).split(sep).join('/');
81
- if (!rel.startsWith('.rpiv/')) {
82
- results.push(rel);
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
- const dir = dirname(claudeMdRelative);
93
- if (dir === '.') {
94
- return '.rpiv/guidance/architecture.md';
95
- }
96
- return join('.rpiv', 'guidance', dir, 'architecture.md').split(sep).join('/');
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
- let refsTransformed = 0;
100
- const warnings = [];
101
- // Pattern 1: Backtick-wrapped path references like `src/core/CLAUDE.md`
102
- let transformed = content.replace(/`((?:[\w][\w./-]*\/)?CLAUDE\.md)`/g, (_match, claudePath) => {
103
- const replacement = claudePathToGuidancePath(claudePath);
104
- refsTransformed++;
105
- return '`' + replacement + '`';
106
- });
107
- // Pattern 2: Bare path references (with directory prefix) not inside backticks
108
- // Match things like "src/core/CLAUDE.md" but not already-backtick-wrapped
109
- transformed = transformed.replace(/(?<!`)([\w][\w./-]*\/CLAUDE\.md)(?!`)/g, (_match, claudePath) => {
110
- const replacement = claudePathToGuidancePath(claudePath);
111
- refsTransformed++;
112
- return replacement;
113
- });
114
- // Pattern 3: Standalone "CLAUDE.md" that references the root file
115
- // Only match when it looks like a file reference (not part of a longer word)
116
- // Avoid matching inside paths already transformed above
117
- transformed = transformed.replace(/(?<![/\w`])CLAUDE\.md(?![/\w`])/g, () => {
118
- refsTransformed++;
119
- return '.rpiv/guidance/architecture.md';
120
- });
121
- // Scan for remaining prose references that might need manual attention
122
- const lines = transformed.split('\n');
123
- for (let i = 0; i < lines.length; i++) {
124
- // Look for prose patterns like "see X CLAUDE.md" or "X layer CLAUDE.md"
125
- if (/\b\w+\s+CLAUDE\.md\b/i.test(content.split('\n')[i] ?? '') &&
126
- !/(src|lib|app|packages|apps)\//.test(content.split('\n')[i] ?? '')) {
127
- // Check if this line still has an untransformed prose reference
128
- if (/CLAUDE\.md/i.test(lines[i])) {
129
- warnings.push({
130
- file: targetPath,
131
- line: i + 1,
132
- message: `Prose reference to CLAUDE.md may need manual update: "${lines[i].trim()}"`,
133
- });
134
- }
135
- }
136
- }
137
- return { content: transformed, refsTransformed, warnings };
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
- const dir = dirname(claudePath);
141
- if (dir === '.') {
142
- return '.rpiv/guidance/architecture.md';
143
- }
144
- return '.rpiv/guidance/' + dir + '/architecture.md';
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
- const { projectDir, deleteOriginals, dryRun, force } = parseArgs(process.argv);
149
- process.stderr.write(`[rpiv:migrate] scanning ${projectDir} for CLAUDE.md files\n`);
150
- const claudeFiles = discoverClaudeMdFiles(projectDir);
151
- if (claudeFiles.length === 0) {
152
- const report = {
153
- migrated: [],
154
- conflicts: [],
155
- warnings: [],
156
- originalsDeleted: false,
157
- dryRun,
158
- };
159
- process.stdout.write(JSON.stringify(report, null, 2));
160
- return;
161
- }
162
- process.stderr.write(`[rpiv:migrate] found ${claudeFiles.length} CLAUDE.md file(s)\n`);
163
- const migrated = [];
164
- const conflicts = [];
165
- const allWarnings = [];
166
- const writtenFiles = [];
167
- for (const source of claudeFiles) {
168
- const target = computeTargetPath(source);
169
- const targetAbs = join(projectDir, target);
170
- // Check for conflicts
171
- if (existsSync(targetAbs) && !force) {
172
- conflicts.push(target);
173
- continue;
174
- }
175
- // Read source content
176
- const sourceAbs = join(projectDir, source);
177
- let content;
178
- try {
179
- content = readFileSync(sourceAbs, 'utf-8');
180
- }
181
- catch (err) {
182
- allWarnings.push({
183
- file: source,
184
- line: 0,
185
- message: `Failed to read: ${err instanceof Error ? err.message : String(err)}`,
186
- });
187
- continue;
188
- }
189
- if (content.trim().length === 0) {
190
- allWarnings.push({
191
- file: source,
192
- line: 0,
193
- message: 'Empty file, skipped',
194
- });
195
- continue;
196
- }
197
- // Transform content
198
- const { content: transformed, refsTransformed, warnings } = transformContent(content, target);
199
- const lines = transformed.split('\n').length;
200
- migrated.push({ source, target, lines, refsTransformed });
201
- allWarnings.push(...warnings);
202
- if (!dryRun) {
203
- writtenFiles.push({ targetAbs, content: transformed });
204
- }
205
- }
206
- // Write all files (all-or-nothing approach for safety)
207
- if (!dryRun) {
208
- for (const { targetAbs, content } of writtenFiles) {
209
- mkdirSync(dirname(targetAbs), { recursive: true });
210
- writeFileSync(targetAbs, content, 'utf-8');
211
- }
212
- process.stderr.write(`[rpiv:migrate] wrote ${writtenFiles.length} file(s)\n`);
213
- }
214
- // Delete originals only after all writes succeed
215
- let originalsDeleted = false;
216
- if (!dryRun && deleteOriginals && writtenFiles.length > 0) {
217
- for (const entry of migrated) {
218
- const sourceAbs = join(projectDir, entry.source);
219
- try {
220
- unlinkSync(sourceAbs);
221
- }
222
- catch (err) {
223
- allWarnings.push({
224
- file: entry.source,
225
- line: 0,
226
- message: `Failed to delete original: ${err instanceof Error ? err.message : String(err)}`,
227
- });
228
- }
229
- }
230
- originalsDeleted = true;
231
- process.stderr.write(`[rpiv:migrate] deleted ${migrated.length} original CLAUDE.md file(s)\n`);
232
- }
233
- const report = {
234
- migrated,
235
- conflicts,
236
- warnings: allWarnings,
237
- originalsDeleted,
238
- dryRun,
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();