feat: add /update skill for pulling upstream changes (#372)
Interactive skill that guides Claude through fetching upstream NanoClaw, previewing changes, merging with customizations, running migrations, and verifying the result. Includes: - SKILL.md with 9-step update flow - fetch-upstream.sh: detects remote, fetches, extracts tracked paths - run-migrations.ts: discovers and runs version-ordered migrations - post-update.ts: clears backup after conflict resolution - update-core.ts: adds --json and --preview-only flags - BASE_INCLUDES moved to constants.ts as single source of truth - 16 new tests covering fetch, migrations, and CLI flags Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
5
scripts/post-update.ts
Normal file
5
scripts/post-update.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env tsx
|
||||
import { clearBackup } from '../skills-engine/backup.js';
|
||||
|
||||
clearBackup();
|
||||
console.log('Backup cleared.');
|
||||
104
scripts/run-migrations.ts
Normal file
104
scripts/run-migrations.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env tsx
|
||||
import { execFileSync, execSync } from 'child_process';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import { compareSemver } from '../skills-engine/state.js';
|
||||
|
||||
// Resolve tsx binary once to avoid npx race conditions across migrations
|
||||
function resolveTsx(): string {
|
||||
// Check local node_modules first
|
||||
const local = path.resolve('node_modules/.bin/tsx');
|
||||
if (fs.existsSync(local)) return local;
|
||||
// Fall back to whichever tsx is in PATH
|
||||
try {
|
||||
return execSync('which tsx', { encoding: 'utf-8' }).trim();
|
||||
} catch {
|
||||
return 'npx'; // last resort
|
||||
}
|
||||
}
|
||||
|
||||
const tsxBin = resolveTsx();
|
||||
|
||||
const fromVersion = process.argv[2];
|
||||
const toVersion = process.argv[3];
|
||||
const newCorePath = process.argv[4];
|
||||
|
||||
if (!fromVersion || !toVersion || !newCorePath) {
|
||||
console.error(
|
||||
'Usage: tsx scripts/run-migrations.ts <from-version> <to-version> <new-core-path>',
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
interface MigrationResult {
|
||||
version: string;
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const results: MigrationResult[] = [];
|
||||
|
||||
// Look for migrations in the new core
|
||||
const migrationsDir = path.join(newCorePath, 'migrations');
|
||||
|
||||
if (!fs.existsSync(migrationsDir)) {
|
||||
console.log(
|
||||
JSON.stringify({ migrationsRun: 0, results: [] }, null, 2),
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Discover migration directories (version-named)
|
||||
const entries = fs.readdirSync(migrationsDir, { withFileTypes: true });
|
||||
const migrationVersions = entries
|
||||
.filter((e) => e.isDirectory() && /^\d+\.\d+\.\d+$/.test(e.name))
|
||||
.map((e) => e.name)
|
||||
.filter(
|
||||
(v) =>
|
||||
compareSemver(v, fromVersion) > 0 && compareSemver(v, toVersion) <= 0,
|
||||
)
|
||||
.sort(compareSemver);
|
||||
|
||||
const projectRoot = process.cwd();
|
||||
|
||||
for (const version of migrationVersions) {
|
||||
const migrationIndex = path.join(migrationsDir, version, 'index.ts');
|
||||
if (!fs.existsSync(migrationIndex)) {
|
||||
results.push({
|
||||
version,
|
||||
success: false,
|
||||
error: `Migration ${version}/index.ts not found`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const tsxArgs = tsxBin.endsWith('npx')
|
||||
? ['tsx', migrationIndex, projectRoot]
|
||||
: [migrationIndex, projectRoot];
|
||||
execFileSync(tsxBin, tsxArgs, {
|
||||
stdio: 'pipe',
|
||||
cwd: projectRoot,
|
||||
timeout: 120_000,
|
||||
});
|
||||
results.push({ version, success: true });
|
||||
} catch (err) {
|
||||
const message =
|
||||
err instanceof Error ? err.message : String(err);
|
||||
results.push({ version, success: false, error: message });
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{ migrationsRun: results.length, results },
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
|
||||
// Exit with error if any migration failed
|
||||
if (results.some((r) => !r.success)) {
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -1,34 +1,59 @@
|
||||
#!/usr/bin/env tsx
|
||||
import { applyUpdate, previewUpdate } from '../skills-engine/update.js';
|
||||
|
||||
const newCorePath = process.argv[2];
|
||||
const args = process.argv.slice(2);
|
||||
const jsonMode = args.includes('--json');
|
||||
const previewOnly = args.includes('--preview-only');
|
||||
const newCorePath = args.find((a) => !a.startsWith('--'));
|
||||
|
||||
if (!newCorePath) {
|
||||
console.error('Usage: tsx scripts/update-core.ts <path-to-new-core>');
|
||||
console.error(
|
||||
'Usage: tsx scripts/update-core.ts [--json] [--preview-only] <path-to-new-core>',
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Preview
|
||||
const preview = previewUpdate(newCorePath);
|
||||
console.log('=== Update Preview ===');
|
||||
console.log(`Current version: ${preview.currentVersion}`);
|
||||
console.log(`New version: ${preview.newVersion}`);
|
||||
console.log(`Files changed: ${preview.filesChanged.length}`);
|
||||
if (preview.filesChanged.length > 0) {
|
||||
for (const f of preview.filesChanged) {
|
||||
console.log(` ${f}`);
|
||||
|
||||
if (jsonMode && previewOnly) {
|
||||
console.log(JSON.stringify(preview, null, 2));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
function printPreview(): void {
|
||||
console.log('=== Update Preview ===');
|
||||
console.log(`Current version: ${preview.currentVersion}`);
|
||||
console.log(`New version: ${preview.newVersion}`);
|
||||
console.log(`Files changed: ${preview.filesChanged.length}`);
|
||||
if (preview.filesChanged.length > 0) {
|
||||
for (const f of preview.filesChanged) {
|
||||
console.log(` ${f}`);
|
||||
}
|
||||
}
|
||||
if (preview.conflictRisk.length > 0) {
|
||||
console.log(`Conflict risk: ${preview.conflictRisk.join(', ')}`);
|
||||
}
|
||||
if (preview.customPatchesAtRisk.length > 0) {
|
||||
console.log(
|
||||
`Custom patches at risk: ${preview.customPatchesAtRisk.join(', ')}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (preview.conflictRisk.length > 0) {
|
||||
console.log(`Conflict risk: ${preview.conflictRisk.join(', ')}`);
|
||||
}
|
||||
if (preview.customPatchesAtRisk.length > 0) {
|
||||
console.log(`Custom patches at risk: ${preview.customPatchesAtRisk.join(', ')}`);
|
||||
}
|
||||
console.log('');
|
||||
|
||||
// Apply
|
||||
console.log('Applying update...');
|
||||
if (previewOnly) {
|
||||
printPreview();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!jsonMode) {
|
||||
printPreview();
|
||||
console.log('');
|
||||
console.log('Applying update...');
|
||||
}
|
||||
|
||||
const result = await applyUpdate(newCorePath);
|
||||
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
|
||||
if (!result.success) {
|
||||
|
||||
Reference in New Issue
Block a user