@chris1807/claude-kit 2.1.17 → 2.1.18
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/bin/cli.js
CHANGED
|
@@ -11,6 +11,8 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
11
11
|
const __dirname = dirname(__filename);
|
|
12
12
|
const TEMPLATES_DIR = join(__dirname, '..', 'templates');
|
|
13
13
|
const GLOBAL_CLAUDE_DIR = join(process.env.HOME || process.env.USERPROFILE, '.claude');
|
|
14
|
+
const PKG = await fs.readJson(join(__dirname, '..', 'package.json'));
|
|
15
|
+
const KIT_VERSION = PKG.version;
|
|
14
16
|
|
|
15
17
|
// ============================================================================
|
|
16
18
|
// CLI Arguments
|
|
@@ -110,6 +112,47 @@ async function installFile(src, dest, label) {
|
|
|
110
112
|
return true;
|
|
111
113
|
}
|
|
112
114
|
|
|
115
|
+
// ============================================================================
|
|
116
|
+
// Helper: additive merge for settings.json
|
|
117
|
+
// ============================================================================
|
|
118
|
+
// Top-level keys: keep user's value if present, add kit's only if missing.
|
|
119
|
+
// `hooks`: union of matcher blocks; within each block, union of hook commands
|
|
120
|
+
// (compared by `command` string). Never removes user-added entries.
|
|
121
|
+
function mergeSettings(existing, template) {
|
|
122
|
+
const merged = { ...existing };
|
|
123
|
+
for (const [key, value] of Object.entries(template)) {
|
|
124
|
+
if (key === 'hooks') {
|
|
125
|
+
merged.hooks = mergeHooks(existing.hooks || {}, value);
|
|
126
|
+
} else if (!(key in merged)) {
|
|
127
|
+
merged[key] = value;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return merged;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function mergeHooks(existing, template) {
|
|
134
|
+
const merged = { ...existing };
|
|
135
|
+
for (const [event, templateBlocks] of Object.entries(template)) {
|
|
136
|
+
const resultBlocks = [...(merged[event] || [])];
|
|
137
|
+
for (const tplBlock of templateBlocks) {
|
|
138
|
+
const idx = resultBlocks.findIndex(b => b.matcher === tplBlock.matcher);
|
|
139
|
+
if (idx === -1) {
|
|
140
|
+
resultBlocks.push(tplBlock);
|
|
141
|
+
} else {
|
|
142
|
+
const newHooks = [...(resultBlocks[idx].hooks || [])];
|
|
143
|
+
for (const tplHook of tplBlock.hooks || []) {
|
|
144
|
+
if (!newHooks.some(h => h.command === tplHook.command)) {
|
|
145
|
+
newHooks.push(tplHook);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
resultBlocks[idx] = { ...resultBlocks[idx], hooks: newHooks };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
merged[event] = resultBlocks;
|
|
152
|
+
}
|
|
153
|
+
return merged;
|
|
154
|
+
}
|
|
155
|
+
|
|
113
156
|
// ============================================================================
|
|
114
157
|
// Step 1: Global Agents (always)
|
|
115
158
|
// ============================================================================
|
|
@@ -243,6 +286,7 @@ async function main() {
|
|
|
243
286
|
// ── MCP Servers (interactive selection) ───────────────────────────────
|
|
244
287
|
let dbType = 'none';
|
|
245
288
|
let selectedServers = [];
|
|
289
|
+
let adoOrgChoice = null;
|
|
246
290
|
if (components.includes('mcp')) {
|
|
247
291
|
console.log(chalk.yellow.bold('\n🔌 MCP Servers → .mcp.json\n'));
|
|
248
292
|
|
|
@@ -379,6 +423,7 @@ async function main() {
|
|
|
379
423
|
|
|
380
424
|
dbType = mcpChoices.db;
|
|
381
425
|
selectedServers = mcpChoices.servers;
|
|
426
|
+
adoOrgChoice = mcpChoices.adoOrg || null;
|
|
382
427
|
|
|
383
428
|
const mcpPath = join(targetDir, '.mcp.json');
|
|
384
429
|
if (await fs.pathExists(mcpPath)) {
|
|
@@ -388,15 +433,16 @@ async function main() {
|
|
|
388
433
|
if (newContent.trim() === existingContent.trim()) {
|
|
389
434
|
console.log(chalk.gray(' = .mcp.json (identical, skipped)'));
|
|
390
435
|
} else {
|
|
436
|
+
// In --all mode, merge by default — preserves user-added MCP servers.
|
|
391
437
|
const { action } = installAll
|
|
392
|
-
? { action: '
|
|
438
|
+
? { action: 'merge' }
|
|
393
439
|
: await inquirer.prompt([{
|
|
394
440
|
type: 'list',
|
|
395
441
|
name: 'action',
|
|
396
442
|
message: '.mcp.json already exists:',
|
|
397
443
|
choices: [
|
|
444
|
+
{ name: 'Merge (add missing servers, keep yours)', value: 'merge' },
|
|
398
445
|
{ name: 'Overwrite with new config', value: 'overwrite' },
|
|
399
|
-
{ name: 'Merge (add missing servers)', value: 'merge' },
|
|
400
446
|
{ name: 'Skip', value: 'skip' },
|
|
401
447
|
],
|
|
402
448
|
}]);
|
|
@@ -422,11 +468,27 @@ async function main() {
|
|
|
422
468
|
// ── Settings ──────────────────────────────────────────────────────────
|
|
423
469
|
if (components.includes('settings')) {
|
|
424
470
|
console.log(chalk.yellow.bold('\n⚙️ Settings → .claude/settings.json\n'));
|
|
425
|
-
await
|
|
426
|
-
join(TEMPLATES_DIR, 'infrastructure', 'settings.json')
|
|
427
|
-
join(targetDir, '.claude', 'settings.json'),
|
|
428
|
-
'settings.json'
|
|
471
|
+
const templateSettings = await fs.readJson(
|
|
472
|
+
join(TEMPLATES_DIR, 'infrastructure', 'settings.json')
|
|
429
473
|
);
|
|
474
|
+
const settingsPath = join(targetDir, '.claude', 'settings.json');
|
|
475
|
+
await fs.ensureDir(dirname(settingsPath));
|
|
476
|
+
|
|
477
|
+
if (!await fs.pathExists(settingsPath)) {
|
|
478
|
+
await fs.writeJson(settingsPath, templateSettings, { spaces: 2 });
|
|
479
|
+
console.log(chalk.green(' ✓ settings.json'));
|
|
480
|
+
} else {
|
|
481
|
+
const existing = await fs.readJson(settingsPath);
|
|
482
|
+
const merged = mergeSettings(existing, templateSettings);
|
|
483
|
+
const before = JSON.stringify(existing);
|
|
484
|
+
const after = JSON.stringify(merged);
|
|
485
|
+
if (before === after) {
|
|
486
|
+
console.log(chalk.gray(' = settings.json (no kit changes needed)'));
|
|
487
|
+
} else {
|
|
488
|
+
await fs.writeJson(settingsPath, merged, { spaces: 2 });
|
|
489
|
+
console.log(chalk.green(' ✓ settings.json (merged — kit hooks added, user customizations preserved)'));
|
|
490
|
+
}
|
|
491
|
+
}
|
|
430
492
|
}
|
|
431
493
|
|
|
432
494
|
// ── CLAUDE.md Workflow ────────────────────────────────────────────────
|
|
@@ -468,18 +530,27 @@ async function main() {
|
|
|
468
530
|
existing = await fs.readFile(claudeMdPath, 'utf8');
|
|
469
531
|
|
|
470
532
|
if (existing.includes('Claude Kit Workflow') || existing.includes('Care Solutions AI Workflow')) {
|
|
471
|
-
|
|
472
|
-
|
|
533
|
+
// Check whether the existing workflow section matches the current template.
|
|
534
|
+
// If it does, nothing to do. If not, replace it (auto in --all, prompt otherwise).
|
|
535
|
+
const cleaned = existing.replace(/\n## (?:Claude Kit Workflow|Care Solutions AI Workflow)[\s\S]*$/, '').trimEnd();
|
|
536
|
+
const expected = `${cleaned}\n\n${workflowContent}`;
|
|
537
|
+
if (existing.trim() === expected.trim()) {
|
|
538
|
+
console.log(chalk.gray(' = Workflow section up to date'));
|
|
539
|
+
} else if (installAll) {
|
|
540
|
+
await fs.writeFile(claudeMdPath, expected);
|
|
541
|
+
console.log(chalk.green(' ✓ Workflow section re-synced to current template'));
|
|
542
|
+
} else {
|
|
473
543
|
const { replace } = await inquirer.prompt([{
|
|
474
544
|
type: 'confirm',
|
|
475
545
|
name: 'replace',
|
|
476
|
-
message: '
|
|
477
|
-
default:
|
|
546
|
+
message: 'Workflow section differs from template — replace?',
|
|
547
|
+
default: true,
|
|
478
548
|
}]);
|
|
479
549
|
if (replace) {
|
|
480
|
-
|
|
481
|
-
await fs.writeFile(claudeMdPath, `${cleaned}\n\n${workflowContent}`);
|
|
550
|
+
await fs.writeFile(claudeMdPath, expected);
|
|
482
551
|
console.log(chalk.green(' ✓ Workflow section replaced'));
|
|
552
|
+
} else {
|
|
553
|
+
console.log(chalk.gray(' ⊘ Workflow section left as-is'));
|
|
483
554
|
}
|
|
484
555
|
}
|
|
485
556
|
} else {
|
|
@@ -518,6 +589,19 @@ async function main() {
|
|
|
518
589
|
}
|
|
519
590
|
}
|
|
520
591
|
|
|
592
|
+
// ── Install Manifest ──────────────────────────────────────────────────
|
|
593
|
+
// Records the kit version and the choices made (db, adoOrg) so the
|
|
594
|
+
// kit-update-check hook can re-run the installer non-interactively.
|
|
595
|
+
const manifestPath = join(targetDir, '.claude', '.kit-install.json');
|
|
596
|
+
await fs.ensureDir(dirname(manifestPath));
|
|
597
|
+
await fs.writeJson(manifestPath, {
|
|
598
|
+
version: KIT_VERSION,
|
|
599
|
+
choices: {
|
|
600
|
+
db: dbType,
|
|
601
|
+
adoOrg: selectedServers.includes('azuredevops') ? adoOrgChoice : null,
|
|
602
|
+
},
|
|
603
|
+
}, { spaces: 2 });
|
|
604
|
+
|
|
521
605
|
// ── Summary ───────────────────────────────────────────────────────────
|
|
522
606
|
const agentCount = (await fs.pathExists(join(targetDir, '.claude', 'agents')))
|
|
523
607
|
? (await fs.readdir(join(targetDir, '.claude', 'agents'))).filter(f => f.endsWith('.md')).length
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chris1807/claude-kit",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.18",
|
|
4
4
|
"description": "Claude Code starter kit for Azure DevOps teams — agents, hooks, MCP servers, slash commands, and end-to-end work item → PR → release → deploy workflow automation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# kit-update-check.sh — SessionStart hook
|
|
3
|
+
#
|
|
4
|
+
# Checks npm for a newer @chris1807/claude-kit and re-runs the installer
|
|
5
|
+
# non-interactively when one is available. Must never block claude startup —
|
|
6
|
+
# every external call is timeout-bounded and every failure exits 0 silently.
|
|
7
|
+
#
|
|
8
|
+
# Throttled to once per 24h via a marker file. Reads .claude/.kit-install.json
|
|
9
|
+
# for the installed version and the choices to pass to the re-install (db, adoOrg).
|
|
10
|
+
|
|
11
|
+
set +e # never propagate failures
|
|
12
|
+
|
|
13
|
+
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
14
|
+
CLAUDE_DIR="$(cd "$HOOK_DIR/.." && pwd)"
|
|
15
|
+
PROJECT_DIR="$(cd "$CLAUDE_DIR/.." && pwd)"
|
|
16
|
+
MANIFEST="$CLAUDE_DIR/.kit-install.json"
|
|
17
|
+
MARKER="$CLAUDE_DIR/.kit-update-check"
|
|
18
|
+
LOG="$CLAUDE_DIR/.kit-update.log"
|
|
19
|
+
|
|
20
|
+
# No manifest = legacy install or global-only; nothing to do.
|
|
21
|
+
[ -f "$MANIFEST" ] || exit 0
|
|
22
|
+
|
|
23
|
+
# Throttle to one check per 24h. A failed check still touches the marker so
|
|
24
|
+
# unreachable npm doesn't retry every session.
|
|
25
|
+
if [ -f "$MARKER" ]; then
|
|
26
|
+
if command -v stat >/dev/null 2>&1; then
|
|
27
|
+
LAST=$(stat -f %m "$MARKER" 2>/dev/null || stat -c %Y "$MARKER" 2>/dev/null || echo 0)
|
|
28
|
+
NOW=$(date +%s)
|
|
29
|
+
if [ "$((NOW - LAST))" -lt 86400 ]; then
|
|
30
|
+
exit 0
|
|
31
|
+
fi
|
|
32
|
+
fi
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
# Crude JSON extraction (avoids requiring jq).
|
|
36
|
+
extract() {
|
|
37
|
+
grep -o "\"$1\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" "$MANIFEST" | head -1 | sed 's/.*"\([^"]*\)"$/\1/'
|
|
38
|
+
}
|
|
39
|
+
INSTALLED=$(extract version)
|
|
40
|
+
DB=$(extract db)
|
|
41
|
+
ADO_ORG=$(extract adoOrg)
|
|
42
|
+
|
|
43
|
+
# Fetch latest with a 5s timeout — must never hang. `timeout` isn't on macOS
|
|
44
|
+
# by default; perl's alarm() is the most portable fallback.
|
|
45
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
46
|
+
LATEST=$(timeout 5 npm view @chris1807/claude-kit version 2>/dev/null)
|
|
47
|
+
elif command -v gtimeout >/dev/null 2>&1; then
|
|
48
|
+
LATEST=$(gtimeout 5 npm view @chris1807/claude-kit version 2>/dev/null)
|
|
49
|
+
elif command -v perl >/dev/null 2>&1; then
|
|
50
|
+
LATEST=$(perl -e 'alarm 5; exec @ARGV or exit 1' npm view @chris1807/claude-kit version 2>/dev/null)
|
|
51
|
+
else
|
|
52
|
+
# No timeout mechanism available — skip rather than risk a hang.
|
|
53
|
+
touch "$MARKER"
|
|
54
|
+
exit 0
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
touch "$MARKER"
|
|
58
|
+
|
|
59
|
+
[ -z "$LATEST" ] && exit 0
|
|
60
|
+
[ -z "$INSTALLED" ] && exit 0
|
|
61
|
+
[ "$LATEST" = "$INSTALLED" ] && exit 0
|
|
62
|
+
|
|
63
|
+
# Only proceed if LATEST sorts strictly greater than INSTALLED.
|
|
64
|
+
NEWER=$(printf '%s\n%s\n' "$INSTALLED" "$LATEST" | sort -V | tail -1)
|
|
65
|
+
[ "$NEWER" = "$LATEST" ] || exit 0
|
|
66
|
+
[ "$NEWER" = "$INSTALLED" ] && exit 0
|
|
67
|
+
|
|
68
|
+
echo "claude-kit: updating $INSTALLED → $LATEST..." >&2
|
|
69
|
+
|
|
70
|
+
ARGS=("@chris1807/claude-kit" "init" "$PROJECT_DIR" "--all")
|
|
71
|
+
if [ -n "$DB" ] && [ "$DB" != "null" ]; then
|
|
72
|
+
ARGS+=("--db=$DB")
|
|
73
|
+
fi
|
|
74
|
+
if [ -n "$ADO_ORG" ] && [ "$ADO_ORG" != "null" ]; then
|
|
75
|
+
ARGS+=("--ado-org=$ADO_ORG")
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
if npx -y "${ARGS[@]}" >"$LOG" 2>&1; then
|
|
79
|
+
echo "claude-kit: updated to $LATEST (log: $LOG)" >&2
|
|
80
|
+
else
|
|
81
|
+
echo "claude-kit: update failed (see $LOG)" >&2
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
exit 0
|