@eltonssouza/development-utility-kit 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/analyst.md +198 -0
- package/.claude/agents/backend-developer.md +126 -0
- package/.claude/agents/brain-keeper.md +229 -0
- package/.claude/agents/code-reviewer.md +181 -0
- package/.claude/agents/database-engineer.md +94 -0
- package/.claude/agents/devops-engineer.md +141 -0
- package/.claude/agents/frontend-developer.md +97 -0
- package/.claude/agents/gate-keeper.md +118 -0
- package/.claude/agents/migrator.md +291 -0
- package/.claude/agents/mobile-developer.md +80 -0
- package/.claude/agents/n8n-specialist.md +94 -0
- package/.claude/agents/product-owner.md +115 -0
- package/.claude/agents/qa-engineer.md +232 -0
- package/.claude/agents/release-engineer.md +204 -0
- package/.claude/agents/scaffold.md +87 -0
- package/.claude/agents/security-engineer.md +199 -0
- package/.claude/agents/sprint-runner.md +44 -0
- package/.claude/agents/stack-resolver.md +84 -0
- package/.claude/agents/tech-lead.md +182 -0
- package/.claude/agents/update-template.md +54 -0
- package/.claude/agents/ux-designer.md +118 -0
- package/.claude/settings.json +44 -0
- package/.claude/skills/README.md +332 -0
- package/.claude/skills/active-project/SKILL.md +129 -0
- package/.claude/skills/api-integration-test/SKILL.md +64 -0
- package/.claude/skills/auto-test-guard/SKILL.md +237 -0
- package/.claude/skills/auto-test-guard/resources/backend-tests.md +20 -0
- package/.claude/skills/auto-test-guard/resources/e2e-tests.md +24 -0
- package/.claude/skills/auto-test-guard/resources/execution-report.md +49 -0
- package/.claude/skills/auto-test-guard/resources/frontend-tests.md +18 -0
- package/.claude/skills/auto-test-guard/resources/initial-setup.md +108 -0
- package/.claude/skills/auto-test-guard/resources/run-suite.md +48 -0
- package/.claude/skills/auto-test-guard/resources/senior-gate.md +19 -0
- package/.claude/skills/brain-keeper/SKILL.md +60 -0
- package/.claude/skills/brain-keeper/obsidian/app.json +9 -0
- package/.claude/skills/brain-keeper/obsidian/appearance.json +4 -0
- package/.claude/skills/brain-keeper/obsidian/core-plugins.json +20 -0
- package/.claude/skills/brain-keeper/obsidian/daily-notes.json +5 -0
- package/.claude/skills/brain-keeper/obsidian/graph.json +32 -0
- package/.claude/skills/brain-keeper/obsidian/snippets/folder-colors.css +90 -0
- package/.claude/skills/brain-keeper/obsidian/templates.json +5 -0
- package/.claude/skills/brain-keeper/templates/README.md +51 -0
- package/.claude/skills/brain-keeper/templates/adr.md +40 -0
- package/.claude/skills/brain-keeper/templates/bug.md +35 -0
- package/.claude/skills/brain-keeper/templates/daily.md +38 -0
- package/.claude/skills/brain-keeper/templates/feature.md +62 -0
- package/.claude/skills/brain-keeper/templates/meeting.md +34 -0
- package/.claude/skills/brain-keeper/templates/tech-debt.md +21 -0
- package/.claude/skills/caveman/SKILL.md +187 -0
- package/.claude/skills/create-stack-pack/SKILL.md +281 -0
- package/.claude/skills/grill-me/SKILL.md +79 -0
- package/.claude/skills/honcho-memory/SKILL.md +207 -0
- package/.claude/skills/honcho-memory/docs/api-endpoints-verified.md +75 -0
- package/.claude/skills/honcho-memory/hooks/on-prompt-submit.js +221 -0
- package/.claude/skills/honcho-memory/hooks/on-stop.js +193 -0
- package/.claude/skills/honcho-memory/lib/honcho-client.js +363 -0
- package/.claude/skills/honcho-memory/lib/memory-injector.js +93 -0
- package/.claude/skills/honcho-memory/package.json +32 -0
- package/.claude/skills/honcho-memory/scripts/cli.js +370 -0
- package/.claude/skills/honcho-memory/scripts/setup.js +109 -0
- package/.claude/skills/honcho-memory/tests/t001-api-endpoints-verified.test.js +89 -0
- package/.claude/skills/honcho-memory/tests/t002-structure.test.js +97 -0
- package/.claude/skills/honcho-memory/tests/t003-honcho-client.test.js +162 -0
- package/.claude/skills/honcho-memory/tests/t004-soft-delete.test.js +259 -0
- package/.claude/skills/honcho-memory/tests/t005-memory-injector.test.js +175 -0
- package/.claude/skills/honcho-memory/tests/t006-on-prompt-submit.test.js +215 -0
- package/.claude/skills/honcho-memory/tests/t007-on-stop.test.js +165 -0
- package/.claude/skills/honcho-memory/tests/t008-cli.test.js +214 -0
- package/.claude/skills/honcho-memory/tests/t009-setup.test.js +232 -0
- package/.claude/skills/honcho-memory/tests/t010-skill-md.test.js +114 -0
- package/.claude/skills/honcho-memory/tests/t011-settings-hooks.test.js +105 -0
- package/.claude/skills/honcho-memory/tests/t012-docs-update.test.js +106 -0
- package/.claude/skills/honcho-memory/tests/t013-smoke-e2e.test.js +90 -0
- package/.claude/skills/pair-debug/SKILL.md +288 -0
- package/.claude/skills/prd-ready-check/SKILL.md +58 -0
- package/.claude/skills/project-manager/SKILL.md +167 -0
- package/.claude/skills/quality-standards/SKILL.md +201 -0
- package/.claude/skills/quick-feature/SKILL.md +264 -0
- package/.claude/skills/run-sprint/SKILL.md +342 -0
- package/.claude/skills/scaffold/SKILL.md +58 -0
- package/.claude/skills/stack-discovery/SKILL.md +159 -0
- package/.claude/skills/test-coverage-auditor/SKILL.md +59 -0
- package/.claude/skills/to-issues/SKILL.md +163 -0
- package/.claude/skills/to-prd/SKILL.md +130 -0
- package/.claude/skills/update-template/SKILL.md +254 -0
- package/.claude/stacks/CODEOWNERS +30 -0
- package/.claude/stacks/README.md +88 -0
- package/.claude/stacks/_template.md +116 -0
- package/.claude/stacks/java/spring-boot-3.md +376 -0
- package/.claude/stacks/java/spring-boot-4.md +438 -0
- package/.claude/stacks/typescript/angular-18.md +420 -0
- package/.claude/stacks/typescript/angular-19.md +397 -0
- package/.claude/stacks/typescript/angular-21.md +494 -0
- package/CLAUDE.md +453 -0
- package/README.md +391 -0
- package/bin/cli.js +773 -0
- package/bin/lib/backup.js +62 -0
- package/bin/lib/detect-stack.js +476 -0
- package/bin/lib/help.js +233 -0
- package/bin/lib/identity.js +108 -0
- package/bin/lib/local-dir.js +69 -0
- package/bin/lib/manifest.js +236 -0
- package/bin/lib/sync-all.js +394 -0
- package/bin/lib/version-check.js +398 -0
- package/dashboard/db.js +199 -0
- package/dashboard/package.json +22 -0
- package/dashboard/public/app.js +709 -0
- package/dashboard/public/content/docs/agents-reference.en.md +911 -0
- package/dashboard/public/content/docs/architecture-overview.en.md +260 -0
- package/dashboard/public/content/docs/autonomy-matrix.en.md +186 -0
- package/dashboard/public/content/docs/git-flow.en.md +525 -0
- package/dashboard/public/content/docs/honcho-memory.en.md +394 -0
- package/dashboard/public/content/docs/hooks-reference.en.md +420 -0
- package/dashboard/public/content/docs/pipeline.en.md +400 -0
- package/dashboard/public/content/docs/quality-gate.en.md +315 -0
- package/dashboard/public/content/docs/skills-reference.en.md +500 -0
- package/dashboard/public/content/docs/stack-rules.en.md +362 -0
- package/dashboard/public/content/docs/troubleshooting.en.md +637 -0
- package/dashboard/public/content/manifest.json +102 -0
- package/dashboard/public/content/manual/backend.en.md +1138 -0
- package/dashboard/public/content/manual/existing-project.en.md +831 -0
- package/dashboard/public/content/manual/frontend.en.md +1065 -0
- package/dashboard/public/content/manual/fullstack.en.md +1508 -0
- package/dashboard/public/content/manual/mobile.en.md +866 -0
- package/dashboard/public/index.html +108 -0
- package/dashboard/public/style.css +610 -0
- package/dashboard/public/vendor/marked.min.js +69 -0
- package/dashboard/rtk.js +143 -0
- package/dashboard/server-app.js +403 -0
- package/dashboard/server.js +104 -0
- package/dashboard/test/sprint1.test.js +406 -0
- package/dashboard/test/sprint2.test.js +571 -0
- package/dashboard/test/sprint3.test.js +560 -0
- package/package.json +33 -0
- package/scripts/hooks/subagent-telemetry.sh +14 -0
- package/scripts/hooks/telemetry-writer.js +250 -0
- package/scripts/latest-versions.json +56 -0
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* sync-all.js — Batch update of harness across multiple project subdirectories.
|
|
5
|
+
*
|
|
6
|
+
* Default = dry-run preview. `--apply` actually invokes the adoption pipeline
|
|
7
|
+
* via the main CLI's adoptProject() function.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const { compareVersions } = require('./version-check');
|
|
13
|
+
|
|
14
|
+
const HARNESS_DIR_SKIP = new Set([
|
|
15
|
+
'node_modules',
|
|
16
|
+
'.git',
|
|
17
|
+
'.idea',
|
|
18
|
+
'.vscode',
|
|
19
|
+
'dist',
|
|
20
|
+
'build',
|
|
21
|
+
'target',
|
|
22
|
+
'out',
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {{
|
|
27
|
+
* key: 'stack'|'type'|'age'|'harness-version',
|
|
28
|
+
* value: string|number,
|
|
29
|
+
* op: string
|
|
30
|
+
* }} ParsedFilter
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @typedef {{
|
|
35
|
+
* name: string,
|
|
36
|
+
* path: string,
|
|
37
|
+
* harnessVersion: string|null,
|
|
38
|
+
* projectType: string|null,
|
|
39
|
+
* primaryStack: string|null,
|
|
40
|
+
* ageMs: number
|
|
41
|
+
* }} ProjectCandidate
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
// ─── filter parsing ──────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Convert a duration string ("30d", "6m", "1y", "12h") to milliseconds.
|
|
48
|
+
* @param {string} s
|
|
49
|
+
* @returns {number|null}
|
|
50
|
+
*/
|
|
51
|
+
function parseDuration(s) {
|
|
52
|
+
if (!s || typeof s !== 'string') return null;
|
|
53
|
+
const m = s.match(/^(\d+)([hdmy])$/i);
|
|
54
|
+
if (!m) return null;
|
|
55
|
+
const n = parseInt(m[1], 10);
|
|
56
|
+
const unit = m[2].toLowerCase();
|
|
57
|
+
switch (unit) {
|
|
58
|
+
case 'h':
|
|
59
|
+
return n * 60 * 60 * 1000;
|
|
60
|
+
case 'd':
|
|
61
|
+
return n * 24 * 60 * 60 * 1000;
|
|
62
|
+
case 'm':
|
|
63
|
+
return n * 30 * 24 * 60 * 60 * 1000; // month ≈ 30d
|
|
64
|
+
case 'y':
|
|
65
|
+
return n * 365 * 24 * 60 * 60 * 1000;
|
|
66
|
+
default:
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Parse a single filter expression like "stack:java" or "harness-version:<0.2".
|
|
73
|
+
* @param {string} expr
|
|
74
|
+
* @returns {ParsedFilter|null}
|
|
75
|
+
*/
|
|
76
|
+
function parseFilterExpr(expr) {
|
|
77
|
+
if (!expr || typeof expr !== 'string') return null;
|
|
78
|
+
const idx = expr.indexOf(':');
|
|
79
|
+
if (idx === -1) return null;
|
|
80
|
+
const key = expr.slice(0, idx).trim().toLowerCase();
|
|
81
|
+
const rawValue = expr.slice(idx + 1).trim();
|
|
82
|
+
if (!rawValue) return null;
|
|
83
|
+
|
|
84
|
+
switch (key) {
|
|
85
|
+
case 'stack':
|
|
86
|
+
case 'type':
|
|
87
|
+
return { key, value: rawValue.toLowerCase(), op: '=' };
|
|
88
|
+
case 'age': {
|
|
89
|
+
const ms = parseDuration(rawValue);
|
|
90
|
+
if (ms === null) return null;
|
|
91
|
+
return { key, value: ms, op: '>=' }; // ageMs >= threshold = "older than"
|
|
92
|
+
}
|
|
93
|
+
case 'harness-version': {
|
|
94
|
+
const opMatch = rawValue.match(/^(>=|<=|>|<|=)/);
|
|
95
|
+
if (opMatch) {
|
|
96
|
+
return { key, value: rawValue, op: opMatch[1] };
|
|
97
|
+
}
|
|
98
|
+
return { key, value: '=' + rawValue, op: '=' };
|
|
99
|
+
}
|
|
100
|
+
default:
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ─── CLAUDE.md parsing ───────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Extract a key from the `## Project Identity` block of a CLAUDE.md string.
|
|
109
|
+
* Returns the raw value (without surrounding backticks).
|
|
110
|
+
* @param {string} content
|
|
111
|
+
* @param {string} key
|
|
112
|
+
* @returns {string|null}
|
|
113
|
+
*/
|
|
114
|
+
function extractIdentityField(content, key) {
|
|
115
|
+
if (!content) return null;
|
|
116
|
+
const idIdx = content.indexOf('## Project Identity');
|
|
117
|
+
if (idIdx === -1) return null;
|
|
118
|
+
const nextIdx = content.indexOf('\n## ', idIdx + 1);
|
|
119
|
+
const block = nextIdx === -1 ? content.slice(idIdx) : content.slice(idIdx, nextIdx);
|
|
120
|
+
|
|
121
|
+
const re = new RegExp(
|
|
122
|
+
`[*-]\\s*\\*\\*${key.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\$&')}\\*\\*\\s*:\\s*\`?([^\`\n]+?)\`?\\s*$`,
|
|
123
|
+
'im'
|
|
124
|
+
);
|
|
125
|
+
const m = block.match(re);
|
|
126
|
+
return m ? m[1].trim() : null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ─── project discovery ──────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Find candidate projects under dir (subdirs containing .claude/).
|
|
133
|
+
* @param {string} dir
|
|
134
|
+
* @returns {ProjectCandidate[]}
|
|
135
|
+
*/
|
|
136
|
+
function discoverProjects(dir) {
|
|
137
|
+
const out = [];
|
|
138
|
+
let entries;
|
|
139
|
+
try {
|
|
140
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
141
|
+
} catch {
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
for (const ent of entries) {
|
|
146
|
+
if (!ent.isDirectory()) continue;
|
|
147
|
+
if (ent.name.startsWith('.')) continue;
|
|
148
|
+
if (HARNESS_DIR_SKIP.has(ent.name)) continue;
|
|
149
|
+
|
|
150
|
+
const projectPath = path.join(dir, ent.name);
|
|
151
|
+
const claudeDir = path.join(projectPath, '.claude');
|
|
152
|
+
if (!fs.existsSync(claudeDir)) continue;
|
|
153
|
+
|
|
154
|
+
const candidate = inspectProject(projectPath);
|
|
155
|
+
if (candidate) out.push(candidate);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Build a ProjectCandidate by reading .claude/HARNESS_VERSION + CLAUDE.md.
|
|
163
|
+
* @param {string} projectPath
|
|
164
|
+
* @returns {ProjectCandidate|null}
|
|
165
|
+
*/
|
|
166
|
+
function inspectProject(projectPath) {
|
|
167
|
+
const claudeDir = path.join(projectPath, '.claude');
|
|
168
|
+
const versionFile = path.join(claudeDir, 'HARNESS_VERSION');
|
|
169
|
+
const claudeMdPath = path.join(projectPath, 'CLAUDE.md');
|
|
170
|
+
|
|
171
|
+
let harnessVersion = null;
|
|
172
|
+
try {
|
|
173
|
+
harnessVersion = fs.readFileSync(versionFile, 'utf8').trim() || null;
|
|
174
|
+
} catch {
|
|
175
|
+
harnessVersion = null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
let projectType = null;
|
|
179
|
+
let primaryStack = null;
|
|
180
|
+
try {
|
|
181
|
+
const content = fs.readFileSync(claudeMdPath, 'utf8');
|
|
182
|
+
projectType = extractIdentityField(content, 'Project type');
|
|
183
|
+
primaryStack = extractIdentityField(content, 'Primary stack');
|
|
184
|
+
} catch {
|
|
185
|
+
// no CLAUDE.md or unreadable
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
let ageMs = 0;
|
|
189
|
+
try {
|
|
190
|
+
const stat = fs.statSync(claudeDir);
|
|
191
|
+
ageMs = Date.now() - stat.mtimeMs;
|
|
192
|
+
} catch {
|
|
193
|
+
ageMs = 0;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
name: path.basename(projectPath),
|
|
198
|
+
path: projectPath,
|
|
199
|
+
harnessVersion,
|
|
200
|
+
projectType,
|
|
201
|
+
primaryStack,
|
|
202
|
+
ageMs,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ─── filter application ──────────────────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Check whether a project matches one parsed filter.
|
|
210
|
+
* @param {ProjectCandidate} proj
|
|
211
|
+
* @param {ParsedFilter} filter
|
|
212
|
+
* @returns {boolean}
|
|
213
|
+
*/
|
|
214
|
+
function matchesFilter(proj, filter) {
|
|
215
|
+
if (!filter) return true;
|
|
216
|
+
switch (filter.key) {
|
|
217
|
+
case 'stack': {
|
|
218
|
+
const stack = (proj.primaryStack || '').toLowerCase();
|
|
219
|
+
return stack.includes(String(filter.value));
|
|
220
|
+
}
|
|
221
|
+
case 'type': {
|
|
222
|
+
const t = (proj.projectType || '').toLowerCase();
|
|
223
|
+
return t === String(filter.value) || t.includes(String(filter.value));
|
|
224
|
+
}
|
|
225
|
+
case 'age': {
|
|
226
|
+
return proj.ageMs >= Number(filter.value);
|
|
227
|
+
}
|
|
228
|
+
case 'harness-version': {
|
|
229
|
+
if (!proj.harnessVersion) return false;
|
|
230
|
+
const expr = String(filter.value);
|
|
231
|
+
const cmp = compareVersions(proj.harnessVersion, expr);
|
|
232
|
+
// compareVersions returns -1 when the version satisfies the constraint
|
|
233
|
+
// (or 0 for "=X" matches)
|
|
234
|
+
return cmp <= 0;
|
|
235
|
+
}
|
|
236
|
+
default:
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Apply all filters (AND) and excludes (NOT) to a candidate list.
|
|
243
|
+
* @param {ProjectCandidate[]} candidates
|
|
244
|
+
* @param {ParsedFilter[]} filters
|
|
245
|
+
* @param {string[]} excludes
|
|
246
|
+
* @returns {ProjectCandidate[]}
|
|
247
|
+
*/
|
|
248
|
+
function applyFilters(candidates, filters, excludes) {
|
|
249
|
+
const excludeSet = new Set((excludes || []).map((x) => x.toLowerCase()));
|
|
250
|
+
return candidates.filter((c) => {
|
|
251
|
+
if (excludeSet.has(c.name.toLowerCase())) return false;
|
|
252
|
+
return filters.every((f) => matchesFilter(c, f));
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ─── humanizers ──────────────────────────────────────────────────────────────
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Convert ageMs to a short human-readable string.
|
|
260
|
+
* @param {number} ms
|
|
261
|
+
* @returns {string}
|
|
262
|
+
*/
|
|
263
|
+
function humanAge(ms) {
|
|
264
|
+
if (!ms || ms < 0) return 'unknown';
|
|
265
|
+
const days = Math.floor(ms / (24 * 60 * 60 * 1000));
|
|
266
|
+
if (days < 1) return '<1d';
|
|
267
|
+
if (days < 30) return `${days}d`;
|
|
268
|
+
const months = Math.floor(days / 30);
|
|
269
|
+
if (months < 12) return `${months}m`;
|
|
270
|
+
const years = Math.floor(days / 365);
|
|
271
|
+
return `${years}y`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ─── runSyncAll ──────────────────────────────────────────────────────────────
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* @typedef {{ filters: string[], excludes: string[], apply: boolean,
|
|
278
|
+
* adopt?: (opts: {cwd: string, sub: string|null, dryRun: boolean}) => void }} SyncOptions
|
|
279
|
+
*/
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Run sync-all over a directory.
|
|
283
|
+
*
|
|
284
|
+
* The `adopt` option lets the CLI inject its own adoptProject implementation
|
|
285
|
+
* (avoids a circular require with bin/cli.js). When omitted, lazy-loads
|
|
286
|
+
* `../cli.js#adoptProject`.
|
|
287
|
+
*
|
|
288
|
+
* @param {string} dir
|
|
289
|
+
* @param {SyncOptions} options
|
|
290
|
+
* @returns {{ matched: ProjectCandidate[], updated: number, skipped: number, errors: number }}
|
|
291
|
+
*/
|
|
292
|
+
function runSyncAll(dir, options) {
|
|
293
|
+
const opts = options || {};
|
|
294
|
+
const filterStrs = opts.filters || [];
|
|
295
|
+
const excludes = opts.excludes || [];
|
|
296
|
+
const apply = !!opts.apply;
|
|
297
|
+
|
|
298
|
+
if (!dir || !fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
|
|
299
|
+
process.stderr.write(`Error: directory "${dir}" not found or not a directory.\n`);
|
|
300
|
+
process.exit(1);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Parse filters
|
|
304
|
+
const parsedFilters = [];
|
|
305
|
+
for (const f of filterStrs) {
|
|
306
|
+
const parsed = parseFilterExpr(f);
|
|
307
|
+
if (!parsed) {
|
|
308
|
+
process.stderr.write(`Warning: ignoring unrecognized filter "${f}"\n`);
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
parsedFilters.push(parsed);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Discover + filter
|
|
315
|
+
const candidates = discoverProjects(dir);
|
|
316
|
+
const matched = applyFilters(candidates, parsedFilters, excludes);
|
|
317
|
+
|
|
318
|
+
// Print plan
|
|
319
|
+
process.stdout.write(`\nkud sync-all — scanning ${dir}\n`);
|
|
320
|
+
process.stdout.write(`Found ${candidates.length} project(s) with .claude/\n`);
|
|
321
|
+
if (parsedFilters.length) {
|
|
322
|
+
process.stdout.write(`Filters: ${filterStrs.join(', ')}\n`);
|
|
323
|
+
}
|
|
324
|
+
if (excludes.length) {
|
|
325
|
+
process.stdout.write(`Excludes: ${excludes.join(', ')}\n`);
|
|
326
|
+
}
|
|
327
|
+
process.stdout.write(`Matched ${matched.length} project(s)${apply ? '' : ' (dry-run)'}\n\n`);
|
|
328
|
+
|
|
329
|
+
if (matched.length === 0) {
|
|
330
|
+
process.stdout.write('Nothing to do.\n');
|
|
331
|
+
return { matched: [], updated: 0, skipped: 0, errors: 0 };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Print table
|
|
335
|
+
for (const p of matched) {
|
|
336
|
+
process.stdout.write(
|
|
337
|
+
` ${p.name.padEnd(30)} ` +
|
|
338
|
+
`v${(p.harnessVersion || '?').padEnd(8)} ` +
|
|
339
|
+
`${(p.projectType || '?').padEnd(15)} ` +
|
|
340
|
+
`age:${humanAge(p.ageMs).padEnd(6)} ` +
|
|
341
|
+
`${(p.primaryStack || '').slice(0, 40)}\n`
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (!apply) {
|
|
346
|
+
process.stdout.write('\nDry-run only. Re-run with --apply to perform updates.\n');
|
|
347
|
+
return { matched, updated: 0, skipped: 0, errors: 0 };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Resolve adopt function (lazy require to avoid cycles)
|
|
351
|
+
let adopt = opts.adopt;
|
|
352
|
+
if (!adopt) {
|
|
353
|
+
try {
|
|
354
|
+
const cli = require('../cli');
|
|
355
|
+
adopt = cli.adoptProject;
|
|
356
|
+
} catch (e) {
|
|
357
|
+
process.stderr.write(`Error: could not load adoptProject from bin/cli.js — ${e.message}\n`);
|
|
358
|
+
return { matched, updated: 0, skipped: 0, errors: matched.length };
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Apply per project
|
|
363
|
+
let updated = 0;
|
|
364
|
+
let errors = 0;
|
|
365
|
+
process.stdout.write('\nApplying updates...\n');
|
|
366
|
+
for (const p of matched) {
|
|
367
|
+
try {
|
|
368
|
+
adopt({ cwd: p.path, sub: null, dryRun: false });
|
|
369
|
+
process.stdout.write(` [OK] ${p.name}\n`);
|
|
370
|
+
updated += 1;
|
|
371
|
+
} catch (e) {
|
|
372
|
+
process.stdout.write(` [FAIL] ${p.name} — ${e.message}\n`);
|
|
373
|
+
errors += 1;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
process.stdout.write(
|
|
378
|
+
`\nSummary: updated=${updated}, errors=${errors}, skipped=${matched.length - updated - errors}\n`
|
|
379
|
+
);
|
|
380
|
+
return { matched, updated, skipped: matched.length - updated - errors, errors };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
module.exports = {
|
|
384
|
+
runSyncAll,
|
|
385
|
+
// exported for tests
|
|
386
|
+
parseDuration,
|
|
387
|
+
parseFilterExpr,
|
|
388
|
+
extractIdentityField,
|
|
389
|
+
discoverProjects,
|
|
390
|
+
inspectProject,
|
|
391
|
+
matchesFilter,
|
|
392
|
+
applyFilters,
|
|
393
|
+
humanAge,
|
|
394
|
+
};
|