#!/usr/bin/env node
/**
* update CLI — Safely update the current branch from its base branch
* (feature/release ← develop, hotfix ← main). Merge by default (ff when the
* branch has no own commits), rebase opt-in. On conflict the operation is
* aborted: the branch is left untouched.
*/
import { parseArgs } from 'node:util';
import { validate } from './validate.js';
import { execute } from './execute.js';
import { UpdateResultSchema } from './types.js';
async function main() {
try {
const args = parseArgs({
options: {
spec: { type: 'string' },
workdir: { type: 'string' },
json: { type: 'boolean', default: false },
help: { type: 'boolean', short: 'h' },
},
allowPositionals: false,
});
if (args.values.help) {
console.log(`
cli:gitflow-update — SmartStack
Safely updates the CURRENT branch from its BASE branch (feature/release ← develop,
hotfix ← main), using the fetched origin/ ref. Merge by default; on conflict
the operation is aborted and the branch is left untouched.
Usage:
npx tsx index.ts --spec '' [--workdir ] [--json]
Spec fields:
strategy "merge" | "rebase" (default: "merge")
push push the result to origin/ (rebase uses --force-with-lease)
dryRun report the planned action without touching anything
Options:
--spec JSON input spec (required)
--workdir Working directory for git operations (defaults to cwd)
--json Output result as JSON
--help, -h Show this help
`);
process.exit(0);
}
if (!args.values.spec) {
console.error('Error: --spec is required');
process.exit(2);
}
let specObj: unknown;
try {
specObj = JSON.parse(args.values.spec);
} catch {
console.error('Error: --spec must be valid JSON');
process.exit(2);
}
const validation = validate(specObj);
if (!validation.valid) {
if (args.values.json) {
console.log(JSON.stringify({
success: false,
error: validation.blockers.join('; '),
}, null, 2));
} else {
console.error('Validation failed:');
validation.blockers.forEach(b => console.error(` - ${b}`));
}
process.exit(2);
}
if (!args.values.json) {
validation.warnings.forEach(w => console.warn(`[WARNING] ${w}`));
}
const result = await execute(validation.data!, args.values.workdir);
const validated = UpdateResultSchema.parse(result);
if (args.values.json) {
console.log(JSON.stringify(validated, null, 2));
} else {
if (validated.success) {
const head = validated.dryRun ? 'Dry-run' : 'Update';
if (validated.action === 'none') {
console.log(`✓ ${validated.branch} is already up to date with ${validated.baseRef}`);
} else {
console.log(`✓ ${head}: ${validated.action} ${validated.baseRef} into ${validated.branch} (${validated.behind} commit(s) behind, ${validated.ahead} ahead)`);
}
if (validated.pushed) console.log(` Pushed to origin${validated.forcePushed ? ' (--force-with-lease)' : ''}`);
} else {
console.error(`✗ ${validated.error ?? 'update failed'}`);
if (validated.conflictFiles && validated.conflictFiles.length > 0) {
console.error(` Conflicts in: ${validated.conflictFiles.join(', ')}`);
}
if (validated.guidance) console.error(` ${validated.guidance}`);
}
(validated.warnings ?? []).forEach(w => console.warn(`[WARNING] ${w}`));
}
process.exit(validated.success ? 0 : 1);
} catch (err: unknown) {
console.error('Fatal error:', (err as Error).message);
process.exit(2);
}
}
main();