fix: abort squash-merge on conflict and stop auto-mode instead of looping (#merge-bug-fix)
mergeSliceToMain now runs git reset --hard if git merge --squash fails, restoring a clean working tree instead of leaving conflict markers. The merge guard catch block in auto.ts now: 1. Detects leftover conflicted state (UU/AA/UD in porcelain status) 2. Resets the working tree if conflicts remain 3. Stops auto-mode with a clear error instead of continuing with corrupted .gsd/ state files that cause an infinite dispatch loop Also fixes conflict markers in loader.ts, logo.ts, and postinstall.js that were baked into main from a prior bad merge resolution.
This commit is contained in:
parent
63f9a84e8a
commit
39f0df45d5
5 changed files with 109 additions and 24 deletions
|
|
@ -29,10 +29,32 @@ function run(cmd, options = {}) {
|
|||
// ---------------------------------------------------------------------------
|
||||
process.stdout.write = process.stderr.write.bind(process.stderr)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ASCII banner — printed before clack UI for brand recognition
|
||||
// ---------------------------------------------------------------------------
|
||||
const cyan = '\x1b[36m'
|
||||
const dim = '\x1b[2m'
|
||||
const reset = '\x1b[0m'
|
||||
|
||||
const banner =
|
||||
'\n' +
|
||||
cyan +
|
||||
' ██████╗ ███████╗██████╗ \n' +
|
||||
' ██╔════╝ ██╔════╝██╔══██╗\n' +
|
||||
' ██║ ███╗███████╗██║ ██║\n' +
|
||||
' ██║ ██║╚════██║██║ ██║\n' +
|
||||
' ╚██████╔╝███████║██████╔╝\n' +
|
||||
' ╚═════╝ ╚══════╝╚═════╝ ' +
|
||||
reset + '\n' +
|
||||
'\n' +
|
||||
` Get Shit Done ${dim}v${pkg.version}${reset}\n`
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main — wrapped in async IIFE, with graceful fallback if clack fails
|
||||
// ---------------------------------------------------------------------------
|
||||
;(async () => {
|
||||
process.stderr.write(banner)
|
||||
|
||||
let p, pc
|
||||
|
||||
try {
|
||||
|
|
@ -40,15 +62,14 @@ process.stdout.write = process.stderr.write.bind(process.stderr)
|
|||
pc = (await import('picocolors')).default
|
||||
} catch {
|
||||
// Clack or picocolors unavailable — fall back to minimal output
|
||||
process.stderr.write(`\n GSD v${pkg.version} installed.\n Run gsd to get started.\n\n`)
|
||||
process.stderr.write(` Run gsd to get started.\n\n`)
|
||||
await run('npx patch-package')
|
||||
const args = os.platform() === 'linux' ? '--with-deps' : ''
|
||||
await run(`npx playwright install chromium ${args}`)
|
||||
await run('npx playwright install chromium')
|
||||
return
|
||||
}
|
||||
|
||||
// --- Branded intro -------------------------------------------------------
|
||||
p.intro(pc.bgCyan(pc.black(' gsd ')) + ' ' + pc.dim(`v${pkg.version}`))
|
||||
p.intro('Setup')
|
||||
|
||||
const results = []
|
||||
const s = p.spinner()
|
||||
|
|
@ -68,18 +89,28 @@ process.stdout.write = process.stderr.write.bind(process.stderr)
|
|||
}
|
||||
|
||||
// --- Step 2: Playwright browser ------------------------------------------
|
||||
// Avoid --with-deps: install scripts should not block on interactive sudo
|
||||
// prompts. If Linux libs are missing, suggest the explicit follow-up.
|
||||
s.start('Setting up browser tools…')
|
||||
const pwArgs = os.platform() === 'linux' ? ' --with-deps' : ''
|
||||
const pwResult = await run(`npx playwright install chromium${pwArgs}`)
|
||||
const pwResult = await run('npx playwright install chromium')
|
||||
if (pwResult.ok) {
|
||||
s.stop('Browser tools ready')
|
||||
results.push({ label: 'Browser tools ready', ok: true })
|
||||
} else {
|
||||
s.stop(pc.yellow('Browser tools — skipped (non-fatal)'))
|
||||
results.push({
|
||||
label: 'Browser tools unavailable — run ' + pc.cyan('npx playwright install chromium'),
|
||||
ok: false,
|
||||
})
|
||||
const output = `${pwResult.stdout ?? ''}${pwResult.stderr ?? ''}`
|
||||
if (os.platform() === 'linux' && output.includes('Host system is missing dependencies to run browsers.')) {
|
||||
s.stop(pc.yellow('Browser downloaded, missing Linux deps'))
|
||||
results.push({
|
||||
label: 'Run ' + pc.cyan('sudo npx playwright install-deps chromium') + ' to finish setup',
|
||||
ok: false,
|
||||
})
|
||||
} else {
|
||||
s.stop(pc.yellow('Browser tools — skipped (non-fatal)'))
|
||||
results.push({
|
||||
label: 'Browser tools unavailable — run ' + pc.cyan('npx playwright install chromium'),
|
||||
ok: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// --- Summary note --------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
#!/usr/bin/env node
|
||||
import { fileURLToPath } from 'url'
|
||||
import { dirname, resolve, join } from 'path'
|
||||
import { readFileSync } from 'fs'
|
||||
import { agentDir } from './app-paths.js'
|
||||
import { existsSync, readFileSync } from 'fs'
|
||||
import { agentDir, appRoot } from './app-paths.js'
|
||||
import { renderLogo } from './logo.js'
|
||||
|
||||
// pkg/ is a shim directory: contains gsd's piConfig (package.json) and pi's
|
||||
// theme assets (dist/modes/interactive/theme/) without a src/ directory.
|
||||
|
|
@ -17,7 +18,25 @@ process.env.PI_PACKAGE_DIR = pkgDir
|
|||
process.env.PI_SKIP_VERSION_CHECK = '1' // GSD ships its own update check — suppress pi's
|
||||
process.title = 'gsd'
|
||||
|
||||
// First-launch branding is handled by the onboarding wizard (src/onboarding.ts)
|
||||
// Print branded banner on first launch (before ~/.gsd/ exists)
|
||||
if (!existsSync(appRoot)) {
|
||||
const cyan = '\x1b[36m'
|
||||
const green = '\x1b[32m'
|
||||
const dim = '\x1b[2m'
|
||||
const reset = '\x1b[0m'
|
||||
const colorCyan = (s: string) => `${cyan}${s}${reset}`
|
||||
let version = ''
|
||||
try {
|
||||
const pkgJson = JSON.parse(readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf-8'))
|
||||
version = pkgJson.version ?? ''
|
||||
} catch { /* ignore */ }
|
||||
process.stderr.write(
|
||||
renderLogo(colorCyan) +
|
||||
'\n' +
|
||||
` Get Shit Done ${dim}v${version}${reset}\n` +
|
||||
` ${green}Welcome.${reset} Setting up your environment...\n\n`
|
||||
)
|
||||
}
|
||||
|
||||
// GSD_CODING_AGENT_DIR — tells pi's getAgentDir() to return ~/.gsd/agent/ instead of ~/.gsd/agent/
|
||||
process.env.GSD_CODING_AGENT_DIR = agentDir
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@
|
|||
*
|
||||
* Single source of truth — imported by:
|
||||
* - scripts/postinstall.js (via dist/logo.js)
|
||||
* - src/onboarding.ts (via ./logo.js)
|
||||
* - src/loader.ts (via ./logo.js)
|
||||
*/
|
||||
|
||||
/** Raw logo lines — no ANSI codes, no leading newline. */
|
||||
export const GSD_LOGO: string[] = [
|
||||
export const GSD_LOGO: readonly string[] = [
|
||||
' ██████╗ ███████╗██████╗ ',
|
||||
' ██╔════╝ ██╔════╝██╔══██╗',
|
||||
' ██║ ███╗███████╗██║ ██║',
|
||||
|
|
@ -19,7 +19,7 @@ export const GSD_LOGO: string[] = [
|
|||
/**
|
||||
* Render the logo block with a color function applied to each line.
|
||||
*
|
||||
* @param color — e.g. picocolors.cyan or `(s) => `\x1b[36m${s}\x1b[0m``
|
||||
* @param color — e.g. `(s) => `\x1b[36m${s}\x1b[0m`` or picocolors.cyan
|
||||
* @returns Ready-to-write string with leading/trailing newlines.
|
||||
*/
|
||||
export function renderLogo(color: (s: string) => string): string {
|
||||
|
|
|
|||
|
|
@ -1023,14 +1023,35 @@ async function dispatchNextUnit(
|
|||
midTitle = state.activeMilestone?.title;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
|
||||
// Safety net: if mergeSliceToMain failed to clean up (or the error
|
||||
// came from switchToMain), ensure the working tree isn't left in a
|
||||
// conflicted/dirty merge state. Without this, state derivation reads
|
||||
// conflict-marker-filled files, produces a corrupt phase, and
|
||||
// dispatch loops forever (see: merge-bug-fix).
|
||||
try {
|
||||
const { runGit } = await import("./git-service.ts");
|
||||
const status = runGit(basePath, ["status", "--porcelain"], { allowFailure: true });
|
||||
if (status && (status.includes("UU ") || status.includes("AA ") || status.includes("UD "))) {
|
||||
runGit(basePath, ["reset", "--hard", "HEAD"], { allowFailure: true });
|
||||
ctx.ui.notify(
|
||||
`Cleaned up conflicted merge state after failed squash-merge.`,
|
||||
"warning",
|
||||
);
|
||||
}
|
||||
} catch { /* best-effort cleanup */ }
|
||||
|
||||
ctx.ui.notify(
|
||||
`Slice merge failed: ${message}`,
|
||||
`Slice merge failed — stopping auto-mode. Fix conflicts manually and restart.\n${message}`,
|
||||
"error",
|
||||
);
|
||||
// Re-derive state so dispatch can figure out what to do
|
||||
state = await deriveState(basePath);
|
||||
mid = state.activeMilestone?.id;
|
||||
midTitle = state.activeMilestone?.title;
|
||||
if (currentUnit) {
|
||||
const modelId = ctx.model?.id ?? "unknown";
|
||||
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
||||
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
||||
}
|
||||
await stopAuto(ctx, pi);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -446,8 +446,22 @@ export class GitServiceImpl {
|
|||
commitType, milestoneId, sliceId, sliceTitle, mainBranch, branch,
|
||||
);
|
||||
|
||||
// Squash merge
|
||||
this.git(["merge", "--squash", branch]);
|
||||
// Squash merge — abort cleanly on conflict so the working tree is never
|
||||
// left in a half-merged state (see: merge-bug-fix).
|
||||
try {
|
||||
this.git(["merge", "--squash", branch]);
|
||||
} catch (mergeError) {
|
||||
// git merge --squash exits non-zero on conflict. The working tree now
|
||||
// has conflict markers and a dirty index. Reset to restore a clean state.
|
||||
this.git(["reset", "--hard", "HEAD"], { allowFailure: true });
|
||||
const msg = mergeError instanceof Error ? mergeError.message : String(mergeError);
|
||||
throw new Error(
|
||||
`Squash-merge of "${branch}" into "${mainBranch}" failed with conflicts. ` +
|
||||
`Working tree has been reset to a clean state. ` +
|
||||
`Resolve manually: git checkout ${mainBranch} && git merge --squash ${branch}\n` +
|
||||
`Original error: ${msg}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Commit with rich message via stdin pipe
|
||||
this.git(["commit", "-F", "-"], { input: message });
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue