@wipcomputer/wip-repos 1.9.44 → 1.9.47
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/claude.mjs +248 -0
- package/cli.mjs +6 -0
- package/package.json +1 -1
package/claude.mjs
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wip-repos claude ... generate cross-repo CLAUDE.md ecosystem sections
|
|
3
|
+
*
|
|
4
|
+
* Reads all repos on disk via the manifest, extracts metadata from each,
|
|
5
|
+
* and writes an ## Ecosystem section into each repo's CLAUDE.md.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* wip-repos claude # regenerate all repos
|
|
9
|
+
* wip-repos claude <repo> # regenerate one repo
|
|
10
|
+
* wip-repos claude --init # create CLAUDE.md for repos missing one
|
|
11
|
+
* wip-repos claude --dry-run # preview changes
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { existsSync, readFileSync, writeFileSync, readdirSync, mkdirSync } from 'node:fs';
|
|
15
|
+
import { join, basename, resolve, dirname } from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
|
|
18
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const TEMPLATE_PATH = join(__dirname, '..', '..', 'templates', 'repo-claude-md.template');
|
|
20
|
+
const START_MARKER = '<!-- wip-repos:start -->';
|
|
21
|
+
const END_MARKER = '<!-- wip-repos:end -->';
|
|
22
|
+
|
|
23
|
+
function readJSON(path) {
|
|
24
|
+
try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return null; }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Extract metadata from a single repo on disk.
|
|
29
|
+
*/
|
|
30
|
+
function extractRepoMeta(repoPath) {
|
|
31
|
+
const pkg = readJSON(join(repoPath, 'package.json'));
|
|
32
|
+
const name = pkg?.name || basename(repoPath);
|
|
33
|
+
const description = pkg?.description || '';
|
|
34
|
+
const version = pkg?.version || '';
|
|
35
|
+
const exports = pkg?.exports ? Object.keys(pkg.exports) : [];
|
|
36
|
+
const binCommands = pkg?.bin ? Object.keys(pkg.bin) : [];
|
|
37
|
+
const scripts = pkg?.scripts || {};
|
|
38
|
+
|
|
39
|
+
// Detect interfaces
|
|
40
|
+
const interfaces = [];
|
|
41
|
+
if (binCommands.length > 0) interfaces.push('CLI');
|
|
42
|
+
if (pkg?.main || pkg?.exports) interfaces.push('Module');
|
|
43
|
+
if (existsSync(join(repoPath, 'mcp-server.mjs')) || existsSync(join(repoPath, 'dist', 'mcp-server.js'))) interfaces.push('MCP');
|
|
44
|
+
if (existsSync(join(repoPath, 'openclaw.plugin.json'))) interfaces.push('OpenClaw Plugin');
|
|
45
|
+
if (existsSync(join(repoPath, 'SKILL.md'))) interfaces.push('Skill');
|
|
46
|
+
|
|
47
|
+
// Detect key dirs
|
|
48
|
+
const dirs = [];
|
|
49
|
+
for (const d of ['src', 'lib', 'tools', 'bin', 'dist', 'test', 'scripts', 'ai']) {
|
|
50
|
+
if (existsSync(join(repoPath, d))) dirs.push(d + '/');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
name,
|
|
55
|
+
description,
|
|
56
|
+
version,
|
|
57
|
+
interfaces,
|
|
58
|
+
binCommands,
|
|
59
|
+
dirs,
|
|
60
|
+
scripts,
|
|
61
|
+
path: repoPath,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Determine which repos are relevant to a given repo.
|
|
67
|
+
* Returns only repos in the same category or that share dependencies.
|
|
68
|
+
*/
|
|
69
|
+
function filterRelevant(targetMeta, allMetas, manifest) {
|
|
70
|
+
// Find target's category from manifest
|
|
71
|
+
const targetBase = basename(targetMeta.path);
|
|
72
|
+
let targetCategory = null;
|
|
73
|
+
|
|
74
|
+
for (const [category, repos] of Object.entries(manifest.repos || {})) {
|
|
75
|
+
if (repos.some(r => r.local?.includes(targetBase) || r.name === targetBase)) {
|
|
76
|
+
targetCategory = category;
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Core repos every repo should know about
|
|
82
|
+
const coreNames = ['wip-ldm-os', 'wip-ai-devops-toolbox', 'memory-crystal'];
|
|
83
|
+
|
|
84
|
+
return allMetas.filter(m => {
|
|
85
|
+
if (m.path === targetMeta.path) return false; // skip self
|
|
86
|
+
const mBase = basename(m.path);
|
|
87
|
+
// Same category
|
|
88
|
+
if (targetCategory) {
|
|
89
|
+
for (const repos of Object.values(manifest.repos || {})) {
|
|
90
|
+
if (repos.some(r => r.local?.includes(mBase))) return true;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Core repo
|
|
94
|
+
if (coreNames.some(c => mBase.includes(c))) return true;
|
|
95
|
+
return false;
|
|
96
|
+
}).slice(0, 15); // cap at 15 to keep CLAUDE.md focused
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Generate the ecosystem section for a repo.
|
|
101
|
+
*/
|
|
102
|
+
function generateEcosystem(targetMeta, relevantMetas) {
|
|
103
|
+
const lines = [];
|
|
104
|
+
for (const m of relevantMetas) {
|
|
105
|
+
const relPath = basename(m.path);
|
|
106
|
+
lines.push(`### ${m.name}`);
|
|
107
|
+
lines.push(`**Path:** \`${relPath}\``);
|
|
108
|
+
if (m.description) lines.push(m.description);
|
|
109
|
+
if (m.interfaces.length > 0) lines.push(`**Interfaces:** ${m.interfaces.join(', ')}`);
|
|
110
|
+
if (m.binCommands.length > 0) lines.push(`**CLI:** ${m.binCommands.join(', ')}`);
|
|
111
|
+
if (m.version) lines.push(`**Version:** ${m.version}`);
|
|
112
|
+
lines.push('');
|
|
113
|
+
}
|
|
114
|
+
return lines.join('\n').trim();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Update the ecosystem section in a CLAUDE.md file.
|
|
119
|
+
* Only replaces content between the delimiter comments.
|
|
120
|
+
*/
|
|
121
|
+
function updateEcosystemSection(claudeMdPath, ecosystemContent) {
|
|
122
|
+
const content = readFileSync(claudeMdPath, 'utf8');
|
|
123
|
+
const startIdx = content.indexOf(START_MARKER);
|
|
124
|
+
const endIdx = content.indexOf(END_MARKER);
|
|
125
|
+
|
|
126
|
+
if (startIdx === -1 || endIdx === -1) {
|
|
127
|
+
// No delimiters found. Append section.
|
|
128
|
+
const section = `\n## Ecosystem (auto-generated by wip-repos claude)\n${START_MARKER}\n\n${ecosystemContent}\n\n${END_MARKER}\n`;
|
|
129
|
+
return content + section;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Replace between delimiters
|
|
133
|
+
const before = content.substring(0, startIdx + START_MARKER.length);
|
|
134
|
+
const after = content.substring(endIdx);
|
|
135
|
+
return `${before}\n\n${ecosystemContent}\n\n${after}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Create a starter CLAUDE.md from the template.
|
|
140
|
+
*/
|
|
141
|
+
function createFromTemplate(repoPath, meta) {
|
|
142
|
+
const template = existsSync(TEMPLATE_PATH)
|
|
143
|
+
? readFileSync(TEMPLATE_PATH, 'utf8')
|
|
144
|
+
: '# CLAUDE.md\n\n## Ecosystem\n<!-- wip-repos:start -->\n<!-- wip-repos:end -->\n';
|
|
145
|
+
|
|
146
|
+
let content = template
|
|
147
|
+
.replace(/\{\{repo-name\}\}/g, meta.name)
|
|
148
|
+
.replace(/\{\{description\}\}/g, meta.description || 'No description')
|
|
149
|
+
.replace(/\{\{language\}\}/g, meta.scripts?.build?.includes('tsc') ? 'TypeScript' : 'JavaScript')
|
|
150
|
+
.replace(/\{\{scripts\.test\}\}/g, meta.scripts?.test || 'npm test')
|
|
151
|
+
.replace(/\{\{scripts\.build\}\}/g, meta.scripts?.build || 'npm run build')
|
|
152
|
+
.replace(/\{\{scripts\.lint\}\}/g, meta.scripts?.lint || 'npm run lint')
|
|
153
|
+
.replace(/\{\{detected-dirs\}\}/g, meta.dirs.length > 0 ? meta.dirs.map(d => `- \`${d}\``).join('\n') : '(no standard dirs detected)')
|
|
154
|
+
.replace(/\{\{guardrails\}\}/g, existsSync(join(repoPath, '.license-guard.json')) ? '- Dual-license (MIT + AGPL). See LICENSE.' : '');
|
|
155
|
+
|
|
156
|
+
return content;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Main entry point.
|
|
161
|
+
*/
|
|
162
|
+
export function runClaude(manifestPath, args = []) {
|
|
163
|
+
const dryRun = args.includes('--dry-run');
|
|
164
|
+
const init = args.includes('--init');
|
|
165
|
+
const targetRepo = args.find(a => !a.startsWith('--'));
|
|
166
|
+
|
|
167
|
+
const manifest = readJSON(manifestPath);
|
|
168
|
+
if (!manifest) {
|
|
169
|
+
console.error(' Could not read manifest at ' + manifestPath);
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Find all repo paths from manifest
|
|
174
|
+
const manifestDir = dirname(manifestPath);
|
|
175
|
+
const repoPaths = [];
|
|
176
|
+
for (const [category, repos] of Object.entries(manifest.repos || {})) {
|
|
177
|
+
for (const repo of repos) {
|
|
178
|
+
const localPath = repo.local ? resolve(manifestDir, repo.local) : null;
|
|
179
|
+
if (localPath && existsSync(localPath)) {
|
|
180
|
+
repoPaths.push(localPath);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (repoPaths.length === 0) {
|
|
186
|
+
console.log(' No repos found in manifest.');
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Extract metadata from all repos
|
|
191
|
+
const allMetas = repoPaths.map(p => extractRepoMeta(p));
|
|
192
|
+
|
|
193
|
+
// Filter to target if specified
|
|
194
|
+
const targets = targetRepo
|
|
195
|
+
? allMetas.filter(m => basename(m.path).includes(targetRepo))
|
|
196
|
+
: allMetas;
|
|
197
|
+
|
|
198
|
+
if (targets.length === 0) {
|
|
199
|
+
console.error(` No repo matching "${targetRepo}" found.`);
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
console.log(` Processing ${targets.length} repo(s)...`);
|
|
204
|
+
let created = 0, updated = 0, skipped = 0;
|
|
205
|
+
|
|
206
|
+
for (const meta of targets) {
|
|
207
|
+
const claudeMdPath = join(meta.path, 'CLAUDE.md');
|
|
208
|
+
const repoName = basename(meta.path);
|
|
209
|
+
|
|
210
|
+
if (!existsSync(claudeMdPath)) {
|
|
211
|
+
if (init) {
|
|
212
|
+
const content = createFromTemplate(meta.path, meta);
|
|
213
|
+
if (dryRun) {
|
|
214
|
+
console.log(` [create] ${repoName}/CLAUDE.md`);
|
|
215
|
+
} else {
|
|
216
|
+
writeFileSync(claudeMdPath, content);
|
|
217
|
+
console.log(` + Created ${repoName}/CLAUDE.md`);
|
|
218
|
+
}
|
|
219
|
+
created++;
|
|
220
|
+
} else {
|
|
221
|
+
skipped++;
|
|
222
|
+
}
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Generate ecosystem section
|
|
227
|
+
const relevant = filterRelevant(meta, allMetas, manifest);
|
|
228
|
+
const ecosystem = generateEcosystem(meta, relevant);
|
|
229
|
+
const newContent = updateEcosystemSection(claudeMdPath, ecosystem);
|
|
230
|
+
const oldContent = readFileSync(claudeMdPath, 'utf8');
|
|
231
|
+
|
|
232
|
+
if (newContent === oldContent) {
|
|
233
|
+
skipped++;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (dryRun) {
|
|
238
|
+
console.log(` [update] ${repoName}/CLAUDE.md (${relevant.length} related repos)`);
|
|
239
|
+
} else {
|
|
240
|
+
writeFileSync(claudeMdPath, newContent);
|
|
241
|
+
console.log(` + Updated ${repoName}/CLAUDE.md (${relevant.length} related repos)`);
|
|
242
|
+
}
|
|
243
|
+
updated++;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
console.log('');
|
|
247
|
+
console.log(` ${dryRun ? 'Dry run' : 'Done'}. ${created} created, ${updated} updated, ${skipped} skipped.`);
|
|
248
|
+
}
|
package/cli.mjs
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { check, planSync, executeSync, addRepo, moveRepo, generateReadmeTree, loadManifest } from './core.mjs';
|
|
15
|
+
import { runClaude } from './claude.mjs';
|
|
15
16
|
import { resolve, dirname, join } from 'node:path';
|
|
16
17
|
import { readFileSync } from 'node:fs';
|
|
17
18
|
import { fileURLToPath } from 'node:url';
|
|
@@ -173,6 +174,11 @@ try {
|
|
|
173
174
|
break;
|
|
174
175
|
}
|
|
175
176
|
|
|
177
|
+
case 'claude': {
|
|
178
|
+
runClaude(manifestPath, args.slice(1));
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
|
|
176
182
|
default:
|
|
177
183
|
usage();
|
|
178
184
|
if (command && command !== '--help' && command !== '-h') {
|
package/package.json
CHANGED