@wipcomputer/wip-ai-devops-toolbox 1.9.20

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.
Files changed (146) hide show
  1. package/.license-guard.json +7 -0
  2. package/.publish-skill.json +4 -0
  3. package/CHANGELOG.md +1120 -0
  4. package/CLA.md +19 -0
  5. package/DEV-GUIDE-GENERAL-PUBLIC.md +882 -0
  6. package/LICENSE +52 -0
  7. package/README.md +238 -0
  8. package/SKILL.md +728 -0
  9. package/TECHNICAL.md +282 -0
  10. package/UNIVERSAL-INTERFACE.md +180 -0
  11. package/_trash/RELEASE-NOTES-v1-8-0.md +29 -0
  12. package/_trash/RELEASE-NOTES-v1-8-1.md +7 -0
  13. package/_trash/RELEASE-NOTES-v1-8-2.md +7 -0
  14. package/_trash/RELEASE-NOTES-v1-9-0.md +37 -0
  15. package/_trash/RELEASE-NOTES-v1-9-1.md +38 -0
  16. package/_trash/RELEASE-NOTES-v1-9-10.md +40 -0
  17. package/_trash/RELEASE-NOTES-v1-9-2.md +40 -0
  18. package/_trash/RELEASE-NOTES-v1-9-6.md +72 -0
  19. package/_trash/RELEASE-NOTES-v1-9-7.md +23 -0
  20. package/_trash/RELEASE-NOTES-v1-9-9.md +75 -0
  21. package/_trash/guide 2/DEV-GUIDE.md +487 -0
  22. package/_trash/guide 2/scripts/deploy-public.sh +152 -0
  23. package/package.json +27 -0
  24. package/scripts/SKILL-deploy-public.md +61 -0
  25. package/scripts/SKILL-post-merge-rename.md +47 -0
  26. package/scripts/deploy-public.sh +264 -0
  27. package/scripts/post-merge-rename.sh +205 -0
  28. package/scripts/publish-skill.sh +134 -0
  29. package/tools/deploy-public/LICENSE +52 -0
  30. package/tools/deploy-public/README.md +31 -0
  31. package/tools/deploy-public/SKILL.md +71 -0
  32. package/tools/deploy-public/deploy-public.sh +264 -0
  33. package/tools/deploy-public/package.json +9 -0
  34. package/tools/ldm-jobs/LICENSE +52 -0
  35. package/tools/ldm-jobs/README.md +46 -0
  36. package/tools/ldm-jobs/backup.sh +16 -0
  37. package/tools/ldm-jobs/branch-protect.sh +39 -0
  38. package/tools/ldm-jobs/crystal-capture.sh +19 -0
  39. package/tools/ldm-jobs/setup-shell.sh +27 -0
  40. package/tools/ldm-jobs/visibility-audit.sh +27 -0
  41. package/tools/post-merge-rename/LICENSE +52 -0
  42. package/tools/post-merge-rename/README.md +29 -0
  43. package/tools/post-merge-rename/SKILL.md +57 -0
  44. package/tools/post-merge-rename/package.json +9 -0
  45. package/tools/post-merge-rename/post-merge-rename.sh +122 -0
  46. package/tools/wip-branch-guard/INSTALL.md +41 -0
  47. package/tools/wip-branch-guard/guard.mjs +259 -0
  48. package/tools/wip-branch-guard/package.json +11 -0
  49. package/tools/wip-file-guard/CHANGELOG.md +6 -0
  50. package/tools/wip-file-guard/LICENSE +52 -0
  51. package/tools/wip-file-guard/README.md +113 -0
  52. package/tools/wip-file-guard/REFERENCE.md +86 -0
  53. package/tools/wip-file-guard/SKILL.md +105 -0
  54. package/tools/wip-file-guard/guard.mjs +128 -0
  55. package/tools/wip-file-guard/openclaw.plugin.json +8 -0
  56. package/tools/wip-file-guard/package.json +27 -0
  57. package/tools/wip-file-guard/test.sh +119 -0
  58. package/tools/wip-license-guard/LICENSE +52 -0
  59. package/tools/wip-license-guard/README.md +32 -0
  60. package/tools/wip-license-guard/SKILL.md +65 -0
  61. package/tools/wip-license-guard/cli.mjs +464 -0
  62. package/tools/wip-license-guard/core.mjs +310 -0
  63. package/tools/wip-license-guard/hook.mjs +146 -0
  64. package/tools/wip-license-guard/package.json +15 -0
  65. package/tools/wip-license-hook/CHANGELOG.md +17 -0
  66. package/tools/wip-license-hook/LICENSE +52 -0
  67. package/tools/wip-license-hook/README.md +200 -0
  68. package/tools/wip-license-hook/SKILL.md +111 -0
  69. package/tools/wip-license-hook/dist/cli/index.d.ts +15 -0
  70. package/tools/wip-license-hook/dist/cli/index.js +170 -0
  71. package/tools/wip-license-hook/dist/cli/index.js.map +1 -0
  72. package/tools/wip-license-hook/dist/core/detector.d.ts +12 -0
  73. package/tools/wip-license-hook/dist/core/detector.js +104 -0
  74. package/tools/wip-license-hook/dist/core/detector.js.map +1 -0
  75. package/tools/wip-license-hook/dist/core/index.d.ts +4 -0
  76. package/tools/wip-license-hook/dist/core/index.js +5 -0
  77. package/tools/wip-license-hook/dist/core/index.js.map +1 -0
  78. package/tools/wip-license-hook/dist/core/ledger.d.ts +49 -0
  79. package/tools/wip-license-hook/dist/core/ledger.js +72 -0
  80. package/tools/wip-license-hook/dist/core/ledger.js.map +1 -0
  81. package/tools/wip-license-hook/dist/core/reporter.d.ts +14 -0
  82. package/tools/wip-license-hook/dist/core/reporter.js +227 -0
  83. package/tools/wip-license-hook/dist/core/reporter.js.map +1 -0
  84. package/tools/wip-license-hook/dist/core/scanner.d.ts +39 -0
  85. package/tools/wip-license-hook/dist/core/scanner.js +325 -0
  86. package/tools/wip-license-hook/dist/core/scanner.js.map +1 -0
  87. package/tools/wip-license-hook/hooks/pre-pull.sh +55 -0
  88. package/tools/wip-license-hook/hooks/pre-push.sh +51 -0
  89. package/tools/wip-license-hook/mcp-server.mjs +119 -0
  90. package/tools/wip-license-hook/package-lock.json +54 -0
  91. package/tools/wip-license-hook/package.json +43 -0
  92. package/tools/wip-license-hook/src/cli/index.ts +189 -0
  93. package/tools/wip-license-hook/src/core/detector.ts +130 -0
  94. package/tools/wip-license-hook/src/core/index.ts +4 -0
  95. package/tools/wip-license-hook/src/core/ledger.ts +116 -0
  96. package/tools/wip-license-hook/src/core/reporter.ts +255 -0
  97. package/tools/wip-license-hook/src/core/scanner.ts +367 -0
  98. package/tools/wip-license-hook/tsconfig.json +16 -0
  99. package/tools/wip-readme-format/README.md +49 -0
  100. package/tools/wip-readme-format/SKILL.md +84 -0
  101. package/tools/wip-readme-format/format.mjs +570 -0
  102. package/tools/wip-readme-format/package.json +15 -0
  103. package/tools/wip-release/CHANGELOG.md +42 -0
  104. package/tools/wip-release/LICENSE +52 -0
  105. package/tools/wip-release/README.md +45 -0
  106. package/tools/wip-release/REFERENCE.md +100 -0
  107. package/tools/wip-release/SKILL.md +139 -0
  108. package/tools/wip-release/cli.js +161 -0
  109. package/tools/wip-release/core.mjs +1174 -0
  110. package/tools/wip-release/mcp-server.mjs +109 -0
  111. package/tools/wip-release/package.json +36 -0
  112. package/tools/wip-repo-init/README.md +38 -0
  113. package/tools/wip-repo-init/SKILL.md +77 -0
  114. package/tools/wip-repo-init/init.mjs +142 -0
  115. package/tools/wip-repo-init/package.json +11 -0
  116. package/tools/wip-repo-permissions-hook/LICENSE +52 -0
  117. package/tools/wip-repo-permissions-hook/README.md +86 -0
  118. package/tools/wip-repo-permissions-hook/SKILL.md +73 -0
  119. package/tools/wip-repo-permissions-hook/cli.js +83 -0
  120. package/tools/wip-repo-permissions-hook/core.mjs +122 -0
  121. package/tools/wip-repo-permissions-hook/guard.mjs +64 -0
  122. package/tools/wip-repo-permissions-hook/mcp-server.mjs +92 -0
  123. package/tools/wip-repo-permissions-hook/openclaw.plugin.json +8 -0
  124. package/tools/wip-repo-permissions-hook/package.json +31 -0
  125. package/tools/wip-repos/LICENSE +52 -0
  126. package/tools/wip-repos/README.md +77 -0
  127. package/tools/wip-repos/SKILL.md +80 -0
  128. package/tools/wip-repos/cli.mjs +176 -0
  129. package/tools/wip-repos/core.mjs +290 -0
  130. package/tools/wip-repos/mcp-server.mjs +157 -0
  131. package/tools/wip-repos/package.json +34 -0
  132. package/tools/wip-universal-installer/CHANGELOG.md +57 -0
  133. package/tools/wip-universal-installer/LICENSE +52 -0
  134. package/tools/wip-universal-installer/README.md +81 -0
  135. package/tools/wip-universal-installer/REFERENCE.md +122 -0
  136. package/tools/wip-universal-installer/SKILL.md +87 -0
  137. package/tools/wip-universal-installer/SPEC.md +180 -0
  138. package/tools/wip-universal-installer/detect.mjs +130 -0
  139. package/tools/wip-universal-installer/examples/minimal/README.md +20 -0
  140. package/tools/wip-universal-installer/examples/minimal/SKILL.md +28 -0
  141. package/tools/wip-universal-installer/examples/minimal/cli.mjs +4 -0
  142. package/tools/wip-universal-installer/examples/minimal/core.mjs +8 -0
  143. package/tools/wip-universal-installer/examples/minimal/mcp-server.mjs +27 -0
  144. package/tools/wip-universal-installer/examples/minimal/package.json +12 -0
  145. package/tools/wip-universal-installer/install.js +930 -0
  146. package/tools/wip-universal-installer/package.json +36 -0
@@ -0,0 +1,176 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * wip-repos CLI ... repo manifest reconciler
5
+ *
6
+ * Commands:
7
+ * check - Diff filesystem against manifest, flag drift
8
+ * sync - Move local folders to match the manifest
9
+ * add - Add a repo to the manifest
10
+ * move - Move a repo to a different category in the manifest
11
+ * tree - Generate directory tree from manifest
12
+ */
13
+
14
+ import { check, planSync, executeSync, addRepo, moveRepo, generateReadmeTree, loadManifest } from './core.mjs';
15
+ import { resolve, dirname } from 'node:path';
16
+
17
+ const args = process.argv.slice(2);
18
+ const command = args[0];
19
+
20
+ function usage() {
21
+ console.log(`wip-repos ... repo manifest reconciler
22
+
23
+ Usage:
24
+ wip-repos check [--manifest path] [--root path]
25
+ wip-repos sync [--manifest path] [--root path] [--dry-run]
26
+ wip-repos add <path> --remote <org/repo> [--category cat] [--description desc]
27
+ wip-repos move <path> --to <new-path>
28
+ wip-repos tree [--manifest path]
29
+
30
+ Options:
31
+ --manifest Path to repos-manifest.json (default: ./repos-manifest.json)
32
+ --root Path to repos root directory (default: directory containing manifest)
33
+ --dry-run Show what would happen without making changes
34
+ --json Output as JSON`);
35
+ }
36
+
37
+ function getFlag(flag) {
38
+ const idx = args.indexOf(flag);
39
+ if (idx === -1) return undefined;
40
+ return args[idx + 1];
41
+ }
42
+
43
+ function hasFlag(flag) {
44
+ return args.includes(flag);
45
+ }
46
+
47
+ const manifestPath = resolve(getFlag('--manifest') || 'repos-manifest.json');
48
+ const reposRoot = resolve(getFlag('--root') || dirname(manifestPath));
49
+ const dryRun = hasFlag('--dry-run');
50
+ const jsonOutput = hasFlag('--json');
51
+
52
+ try {
53
+ switch (command) {
54
+ case 'check': {
55
+ const result = check(manifestPath, reposRoot);
56
+
57
+ if (jsonOutput) {
58
+ console.log(JSON.stringify(result, null, 2));
59
+ break;
60
+ }
61
+
62
+ console.log(`Manifest: ${result.total.manifest} repos`);
63
+ console.log(`On disk: ${result.total.disk} repos`);
64
+ console.log(`Matched: ${result.total.matched}`);
65
+ console.log();
66
+
67
+ if (result.onDiskOnly.length > 0) {
68
+ console.log(`On disk but NOT in manifest (${result.onDiskOnly.length}):`);
69
+ for (const p of result.onDiskOnly) {
70
+ console.log(` + ${p}`);
71
+ }
72
+ console.log();
73
+ }
74
+
75
+ if (result.inManifestOnly.length > 0) {
76
+ console.log(`In manifest but NOT on disk (${result.inManifestOnly.length}):`);
77
+ for (const p of result.inManifestOnly) {
78
+ console.log(` - ${p}`);
79
+ }
80
+ console.log();
81
+ }
82
+
83
+ if (result.onDiskOnly.length === 0 && result.inManifestOnly.length === 0) {
84
+ console.log('No drift. Manifest and filesystem are in sync.');
85
+ }
86
+
87
+ // Exit code 1 if drift detected
88
+ if (result.onDiskOnly.length > 0 || result.inManifestOnly.length > 0) {
89
+ process.exit(1);
90
+ }
91
+ break;
92
+ }
93
+
94
+ case 'sync': {
95
+ const moves = planSync(manifestPath, reposRoot);
96
+
97
+ if (moves.length === 0) {
98
+ console.log('Nothing to sync. All repos are in the right place.');
99
+ break;
100
+ }
101
+
102
+ if (jsonOutput) {
103
+ console.log(JSON.stringify(moves, null, 2));
104
+ break;
105
+ }
106
+
107
+ console.log(`Found ${moves.length} repo(s) to move:\n`);
108
+ for (const m of moves) {
109
+ console.log(` ${m.from} -> ${m.to}`);
110
+ console.log(` (remote: ${m.remote})`);
111
+ console.log();
112
+ }
113
+
114
+ if (dryRun) {
115
+ console.log('Dry run. No changes made.');
116
+ break;
117
+ }
118
+
119
+ const results = executeSync(moves, reposRoot);
120
+ for (const r of results) {
121
+ if (r.status === 'moved') {
122
+ console.log(` Moved: ${r.from} -> ${r.to}`);
123
+ } else if (r.status === 'skipped') {
124
+ console.log(` Skipped: ${r.from} (${r.reason})`);
125
+ } else {
126
+ console.log(` Error: ${r.from} (${r.reason})`);
127
+ }
128
+ }
129
+ break;
130
+ }
131
+
132
+ case 'add': {
133
+ const repoPath = args[1];
134
+ const remote = getFlag('--remote');
135
+ if (!repoPath || !remote) {
136
+ console.error('Usage: wip-repos add <path> --remote <org/repo>');
137
+ process.exit(1);
138
+ }
139
+ const entry = addRepo(manifestPath, repoPath, remote, {
140
+ category: getFlag('--category'),
141
+ description: getFlag('--description'),
142
+ });
143
+ console.log(`Added: ${repoPath} -> ${remote}`);
144
+ if (jsonOutput) console.log(JSON.stringify(entry, null, 2));
145
+ break;
146
+ }
147
+
148
+ case 'move': {
149
+ const fromPath = args[1];
150
+ const toPath = getFlag('--to');
151
+ if (!fromPath || !toPath) {
152
+ console.error('Usage: wip-repos move <path> --to <new-path>');
153
+ process.exit(1);
154
+ }
155
+ const entry = moveRepo(manifestPath, fromPath, toPath);
156
+ console.log(`Moved in manifest: ${fromPath} -> ${toPath}`);
157
+ if (jsonOutput) console.log(JSON.stringify(entry, null, 2));
158
+ break;
159
+ }
160
+
161
+ case 'tree': {
162
+ const tree = generateReadmeTree(manifestPath);
163
+ console.log(tree);
164
+ break;
165
+ }
166
+
167
+ default:
168
+ usage();
169
+ if (command && command !== '--help' && command !== '-h') {
170
+ process.exit(1);
171
+ }
172
+ }
173
+ } catch (err) {
174
+ console.error(`Error: ${err.message}`);
175
+ process.exit(1);
176
+ }
@@ -0,0 +1,290 @@
1
+ /**
2
+ * wip-repos core ... repo manifest reconciler
3
+ *
4
+ * The manifest is the source of truth. The filesystem adapts to it.
5
+ * Like prettier for folder structure.
6
+ */
7
+
8
+ import { readFileSync, writeFileSync, existsSync, readdirSync, statSync, mkdirSync, renameSync } from 'node:fs';
9
+ import { join, dirname, relative, resolve } from 'node:path';
10
+
11
+ /**
12
+ * Load and parse a repos-manifest.json file.
13
+ * Supports both v1 (flat key-value) and v2 (rich objects) formats.
14
+ */
15
+ export function loadManifest(manifestPath) {
16
+ if (!existsSync(manifestPath)) {
17
+ throw new Error(`Manifest not found: ${manifestPath}`);
18
+ }
19
+ const raw = JSON.parse(readFileSync(manifestPath, 'utf8'));
20
+
21
+ // Detect format
22
+ const meta = {};
23
+ const repos = {};
24
+
25
+ for (const [key, value] of Object.entries(raw)) {
26
+ if (key.startsWith('_')) {
27
+ meta[key] = value;
28
+ continue;
29
+ }
30
+ if (typeof value === 'string') {
31
+ // v1 flat format: path -> remote
32
+ repos[key] = { remote: value };
33
+ } else if (typeof value === 'object' && value !== null) {
34
+ // v2 rich format
35
+ repos[key] = value;
36
+ }
37
+ }
38
+
39
+ return { meta, repos, format: meta._format || 'v1', path: manifestPath };
40
+ }
41
+
42
+ /**
43
+ * Save manifest back to disk.
44
+ */
45
+ export function saveManifest(manifestPath, meta, repos) {
46
+ const out = {};
47
+ for (const [key, value] of Object.entries(meta)) {
48
+ out[key] = value;
49
+ }
50
+ for (const [key, value] of Object.entries(repos)) {
51
+ // v1: just write the remote string
52
+ if (Object.keys(value).length === 1 && value.remote) {
53
+ out[key] = value.remote;
54
+ } else {
55
+ out[key] = value;
56
+ }
57
+ }
58
+ writeFileSync(manifestPath, JSON.stringify(out, null, 2) + '\n', 'utf8');
59
+ }
60
+
61
+ /**
62
+ * Walk a directory tree and find all git repos (directories containing .git/).
63
+ * Returns array of relative paths from the root.
64
+ */
65
+ export function walkRepos(rootDir, prefix = '') {
66
+ const results = [];
67
+ if (!existsSync(rootDir)) return results;
68
+
69
+ const entries = readdirSync(rootDir, { withFileTypes: true });
70
+ for (const entry of entries) {
71
+ if (!entry.isDirectory()) continue;
72
+ if (entry.name === '.git' || entry.name === 'node_modules') continue;
73
+ if (entry.name === '.DS_Store') continue;
74
+
75
+ const fullPath = join(rootDir, entry.name);
76
+ const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
77
+
78
+ // If this directory has a .git, it's a repo
79
+ if (existsSync(join(fullPath, '.git'))) {
80
+ results.push(relPath);
81
+ } else {
82
+ // Recurse into non-repo directories (organizational folders)
83
+ results.push(...walkRepos(fullPath, relPath));
84
+ }
85
+ }
86
+ return results;
87
+ }
88
+
89
+ /**
90
+ * Check: diff the filesystem against the manifest.
91
+ * Returns { inManifestOnly, onDiskOnly, matched }
92
+ */
93
+ export function check(manifestPath, reposRoot) {
94
+ const manifest = loadManifest(manifestPath);
95
+ const manifestPaths = new Set(Object.keys(manifest.repos));
96
+ const diskPaths = new Set(walkRepos(reposRoot));
97
+
98
+ const inManifestOnly = [];
99
+ const onDiskOnly = [];
100
+ const matched = [];
101
+
102
+ for (const p of manifestPaths) {
103
+ if (diskPaths.has(p)) {
104
+ matched.push(p);
105
+ } else {
106
+ inManifestOnly.push(p);
107
+ }
108
+ }
109
+
110
+ for (const p of diskPaths) {
111
+ if (!manifestPaths.has(p)) {
112
+ onDiskOnly.push(p);
113
+ }
114
+ }
115
+
116
+ return {
117
+ inManifestOnly: inManifestOnly.sort(),
118
+ onDiskOnly: onDiskOnly.sort(),
119
+ matched: matched.sort(),
120
+ total: {
121
+ manifest: manifestPaths.size,
122
+ disk: diskPaths.size,
123
+ matched: matched.length,
124
+ },
125
+ };
126
+ }
127
+
128
+ /**
129
+ * Sync: move local folders to match the manifest.
130
+ * Returns array of { from, to } moves that were performed.
131
+ *
132
+ * Only handles repos that exist on disk but at the wrong path.
133
+ * Matches by remote URL (git remote -v) against manifest remotes.
134
+ */
135
+ export function planSync(manifestPath, reposRoot) {
136
+ const manifest = loadManifest(manifestPath);
137
+ const diskPaths = walkRepos(reposRoot);
138
+ const moves = [];
139
+
140
+ // Build a map of remote -> manifest path
141
+ const remoteToManifest = new Map();
142
+ for (const [mPath, info] of Object.entries(manifest.repos)) {
143
+ if (info.remote) {
144
+ remoteToManifest.set(info.remote, mPath);
145
+ }
146
+ }
147
+
148
+ // For each repo on disk, check if its remote matches a manifest entry at a different path
149
+ for (const diskPath of diskPaths) {
150
+ const fullPath = join(reposRoot, diskPath);
151
+ const gitConfig = join(fullPath, '.git', 'config');
152
+ if (!existsSync(gitConfig)) continue;
153
+
154
+ const configText = readFileSync(gitConfig, 'utf8');
155
+ // Extract remote URL
156
+ const match = configText.match(/url\s*=\s*.*[:/]([^/]+\/[^/\s.]+?)(?:\.git)?\s*$/m);
157
+ if (!match) continue;
158
+
159
+ const remote = match[1];
160
+ const expectedPath = remoteToManifest.get(remote);
161
+
162
+ if (expectedPath && expectedPath !== diskPath) {
163
+ moves.push({
164
+ from: diskPath,
165
+ to: expectedPath,
166
+ remote,
167
+ fromFull: fullPath,
168
+ toFull: join(reposRoot, expectedPath),
169
+ });
170
+ }
171
+ }
172
+
173
+ return moves;
174
+ }
175
+
176
+ /**
177
+ * Execute sync moves. Creates parent directories as needed.
178
+ */
179
+ export function executeSync(moves, reposRoot) {
180
+ const results = [];
181
+ for (const move of moves) {
182
+ const parentDir = dirname(move.toFull);
183
+ if (!existsSync(parentDir)) {
184
+ mkdirSync(parentDir, { recursive: true });
185
+ }
186
+ if (existsSync(move.toFull)) {
187
+ results.push({ ...move, status: 'skipped', reason: 'target exists' });
188
+ continue;
189
+ }
190
+ try {
191
+ renameSync(move.fromFull, move.toFull);
192
+ results.push({ ...move, status: 'moved' });
193
+ } catch (err) {
194
+ results.push({ ...move, status: 'error', reason: err.message });
195
+ }
196
+ }
197
+ return results;
198
+ }
199
+
200
+ /**
201
+ * Add a repo to the manifest.
202
+ */
203
+ export function addRepo(manifestPath, repoPath, remote, opts = {}) {
204
+ const manifest = loadManifest(manifestPath);
205
+
206
+ if (manifest.repos[repoPath]) {
207
+ throw new Error(`Already in manifest: ${repoPath}`);
208
+ }
209
+
210
+ const entry = { remote };
211
+ if (opts.description) entry.description = opts.description;
212
+ if (opts.category) entry.category = opts.category;
213
+ if (opts.public) entry.public = opts.public;
214
+ if (opts.privatized !== undefined) entry.privatized = opts.privatized;
215
+
216
+ manifest.repos[repoPath] = entry;
217
+ saveManifest(manifestPath, manifest.meta, manifest.repos);
218
+
219
+ return entry;
220
+ }
221
+
222
+ /**
223
+ * Move a repo in the manifest from one path to another.
224
+ */
225
+ export function moveRepo(manifestPath, fromPath, toPath) {
226
+ const manifest = loadManifest(manifestPath);
227
+
228
+ if (!manifest.repos[fromPath]) {
229
+ throw new Error(`Not in manifest: ${fromPath}`);
230
+ }
231
+ if (manifest.repos[toPath]) {
232
+ throw new Error(`Target already exists in manifest: ${toPath}`);
233
+ }
234
+
235
+ manifest.repos[toPath] = manifest.repos[fromPath];
236
+ delete manifest.repos[fromPath];
237
+ saveManifest(manifestPath, manifest.meta, manifest.repos);
238
+
239
+ return manifest.repos[toPath];
240
+ }
241
+
242
+ /**
243
+ * Generate a markdown directory tree from the manifest.
244
+ */
245
+ export function generateReadmeTree(manifestPath) {
246
+ const manifest = loadManifest(manifestPath);
247
+ const paths = Object.keys(manifest.repos).sort();
248
+
249
+ // Build tree structure
250
+ const tree = {};
251
+ for (const p of paths) {
252
+ const parts = p.split('/');
253
+ let node = tree;
254
+ for (const part of parts) {
255
+ if (!node[part]) node[part] = {};
256
+ node = node[part];
257
+ }
258
+ }
259
+
260
+ // Render tree
261
+ const lines = ['repos/'];
262
+ function render(node, prefix, isLast) {
263
+ const entries = Object.keys(node).sort((a, b) => {
264
+ // underscore-prefixed folders sort to top
265
+ const aUnder = a.startsWith('_');
266
+ const bUnder = b.startsWith('_');
267
+ if (aUnder && !bUnder) return 1;
268
+ if (!aUnder && bUnder) return -1;
269
+ return a.localeCompare(b);
270
+ });
271
+
272
+ for (let i = 0; i < entries.length; i++) {
273
+ const name = entries[i];
274
+ const last = i === entries.length - 1;
275
+ const connector = last ? '└── ' : '├── ';
276
+ const childPrefix = last ? ' ' : '│ ';
277
+ const children = Object.keys(node[name]);
278
+
279
+ if (children.length > 0) {
280
+ lines.push(`${prefix}${connector}${name}/`);
281
+ render(node[name], prefix + childPrefix, last);
282
+ } else {
283
+ lines.push(`${prefix}${connector}${name}`);
284
+ }
285
+ }
286
+ }
287
+
288
+ render(tree, '', false);
289
+ return lines.join('\n');
290
+ }
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env node
2
+ // wip-repos/mcp-server.mjs
3
+ // MCP server exposing repo manifest reconciler as tools.
4
+ // Wraps core.mjs. Registered via .mcp.json.
5
+
6
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
7
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
8
+ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
9
+ import {
10
+ check, planSync, addRepo, moveRepo, generateReadmeTree,
11
+ } from './core.mjs';
12
+
13
+ const server = new Server(
14
+ { name: 'wip-repos', version: '0.1.0' },
15
+ { capabilities: { tools: {} } }
16
+ );
17
+
18
+ // ── Tool Definitions ──
19
+
20
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
21
+ tools: [
22
+ {
23
+ name: 'repos_check',
24
+ description: 'Diff the filesystem against the manifest. Shows repos in manifest but not on disk, on disk but not in manifest, and matched.',
25
+ inputSchema: {
26
+ type: 'object',
27
+ properties: {
28
+ manifestPath: { type: 'string', description: 'Path to repos-manifest.json' },
29
+ reposRoot: { type: 'string', description: 'Root directory containing repos' },
30
+ },
31
+ required: ['manifestPath', 'reposRoot'],
32
+ },
33
+ },
34
+ {
35
+ name: 'repos_sync_plan',
36
+ description: 'Plan moves to reconcile filesystem with manifest. Matches repos by git remote URL. Returns planned moves without executing.',
37
+ inputSchema: {
38
+ type: 'object',
39
+ properties: {
40
+ manifestPath: { type: 'string', description: 'Path to repos-manifest.json' },
41
+ reposRoot: { type: 'string', description: 'Root directory containing repos' },
42
+ },
43
+ required: ['manifestPath', 'reposRoot'],
44
+ },
45
+ },
46
+ {
47
+ name: 'repos_add',
48
+ description: 'Add a repo to the manifest.',
49
+ inputSchema: {
50
+ type: 'object',
51
+ properties: {
52
+ manifestPath: { type: 'string', description: 'Path to repos-manifest.json' },
53
+ repoPath: { type: 'string', description: 'Relative path in the manifest (e.g. ldm-os/utilities/my-tool)' },
54
+ remote: { type: 'string', description: 'GitHub remote (e.g. wipcomputer/my-tool)' },
55
+ description: { type: 'string', description: 'Short description' },
56
+ },
57
+ required: ['manifestPath', 'repoPath', 'remote'],
58
+ },
59
+ },
60
+ {
61
+ name: 'repos_move',
62
+ description: 'Move a repo in the manifest from one path to another.',
63
+ inputSchema: {
64
+ type: 'object',
65
+ properties: {
66
+ manifestPath: { type: 'string', description: 'Path to repos-manifest.json' },
67
+ fromPath: { type: 'string', description: 'Current manifest path' },
68
+ toPath: { type: 'string', description: 'New manifest path' },
69
+ },
70
+ required: ['manifestPath', 'fromPath', 'toPath'],
71
+ },
72
+ },
73
+ {
74
+ name: 'repos_tree',
75
+ description: 'Generate a markdown directory tree from the manifest.',
76
+ inputSchema: {
77
+ type: 'object',
78
+ properties: {
79
+ manifestPath: { type: 'string', description: 'Path to repos-manifest.json' },
80
+ },
81
+ required: ['manifestPath'],
82
+ },
83
+ },
84
+ ],
85
+ }));
86
+
87
+ // ── Tool Handlers ──
88
+
89
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
90
+ const { name, arguments: args } = req.params;
91
+
92
+ try {
93
+ if (name === 'repos_check') {
94
+ const result = check(args.manifestPath, args.reposRoot);
95
+ return {
96
+ content: [{
97
+ type: 'text',
98
+ text: JSON.stringify(result, null, 2),
99
+ }],
100
+ };
101
+ }
102
+
103
+ if (name === 'repos_sync_plan') {
104
+ const moves = planSync(args.manifestPath, args.reposRoot);
105
+ return {
106
+ content: [{
107
+ type: 'text',
108
+ text: moves.length === 0
109
+ ? 'No moves needed. Filesystem matches manifest.'
110
+ : JSON.stringify(moves.map(m => ({ from: m.from, to: m.to, remote: m.remote })), null, 2),
111
+ }],
112
+ };
113
+ }
114
+
115
+ if (name === 'repos_add') {
116
+ const entry = addRepo(args.manifestPath, args.repoPath, args.remote, {
117
+ description: args.description,
118
+ });
119
+ return {
120
+ content: [{
121
+ type: 'text',
122
+ text: `Added ${args.repoPath} -> ${args.remote}`,
123
+ }],
124
+ };
125
+ }
126
+
127
+ if (name === 'repos_move') {
128
+ moveRepo(args.manifestPath, args.fromPath, args.toPath);
129
+ return {
130
+ content: [{
131
+ type: 'text',
132
+ text: `Moved ${args.fromPath} -> ${args.toPath}`,
133
+ }],
134
+ };
135
+ }
136
+
137
+ if (name === 'repos_tree') {
138
+ const tree = generateReadmeTree(args.manifestPath);
139
+ return {
140
+ content: [{ type: 'text', text: tree }],
141
+ };
142
+ }
143
+
144
+ return {
145
+ content: [{ type: 'text', text: `Unknown tool: ${name}` }],
146
+ isError: true,
147
+ };
148
+ } catch (err) {
149
+ return {
150
+ content: [{ type: 'text', text: `Error: ${err.message}` }],
151
+ isError: true,
152
+ };
153
+ }
154
+ });
155
+
156
+ const transport = new StdioServerTransport();
157
+ await server.connect(transport);
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@wipcomputer/wip-repos",
3
+ "version": "1.9.20",
4
+ "type": "module",
5
+ "description": "Repo manifest reconciler. Single source of truth for repo organization. Like prettier for folder structure.",
6
+ "main": "core.mjs",
7
+ "bin": {
8
+ "wip-repos": "./cli.mjs"
9
+ },
10
+ "exports": {
11
+ ".": "./core.mjs"
12
+ },
13
+ "scripts": {
14
+ "test": "node cli.mjs --help"
15
+ },
16
+ "keywords": [
17
+ "repo-manifest",
18
+ "monorepo",
19
+ "toolbox",
20
+ "organization",
21
+ "enterprise",
22
+ "ai-dev-tools"
23
+ ],
24
+ "author": "Parker Todd Brooks",
25
+ "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/wipcomputer/wip-ai-devops-toolbox.git",
29
+ "directory": "tools/wip-repos"
30
+ },
31
+ "dependencies": {
32
+ "@modelcontextprotocol/sdk": "^1.0.0"
33
+ }
34
+ }