fix: honor installer migration resolution env override

This commit is contained in:
Tom Boucher
2026-05-15 09:16:41 -04:00
parent db76cc8dd5
commit 544037e132
3 changed files with 79 additions and 7 deletions

View File

@@ -3,4 +3,4 @@ type: Fixed
pr: 3547
---
**Installer migration no longer hangs `/gsd-update` on leftover GSD-looking files** — non-TTY installer runs now default-resolve `prompt-user` migration actions by classification (stale SDK build artifacts under `get-shit-done/sdk/{dist,src}/gsd-*` default to `remove`; user-facing `skills/gsd-*/SKILL.md` defaults to `keep`) and log each resolution. Anything that cannot be safely defaulted still blocks, but the error message now groups blocked paths by reason, lists the documented choices, and names the `GSD_INSTALLER_MIGRATION_RESOLVE` env var as the non-interactive resolution surface. (#0)
**Installer migration no longer hangs `/gsd:update` on leftover GSD-looking files** — non-TTY installer runs now default-resolve `prompt-user` migration actions by classification (stale SDK build artifacts under `get-shit-done/sdk/{dist,src}/gsd-*` default to `remove`; user-facing `skills/gsd-*/SKILL.md` defaults to `keep`) and log each resolution. Anything that cannot be safely defaulted still blocks, but the error message now groups blocked paths by reason, lists the documented choices, and names the `GSD_INSTALLER_MIGRATION_RESOLVE` env var as the non-interactive resolution surface. (#3541)

View File

@@ -140,6 +140,20 @@ function materializeResolution(action, choice) {
return { ...base, type: 'backup-and-remove', backupRelPath: null };
}
function normalizeResolutionChoice(rawValue) {
if (typeof rawValue !== 'string') return null;
const normalized = rawValue.trim().toLowerCase();
return VALID_CHOICES.includes(normalized) ? normalized : null;
}
function actionSupportsChoice(action, choice) {
if (!action || !choice) return false;
if (!Array.isArray(action.choices) || action.choices.length === 0) {
return VALID_CHOICES.includes(choice);
}
return action.choices.includes(choice);
}
// Resolve prompt-user actions when stdin is not a TTY. Mutates the
// passed result so:
// - resolved actions are appended to plan.actions in their concrete
@@ -164,14 +178,34 @@ function resolveInstallerMigrationPromptsForNonTty(result, options = {}) {
return { result, resolutions: [] };
}
const env =
options && options.env && typeof options.env === 'object'
? options.env
: process.env;
const envChoice = normalizeResolutionChoice(env && env[RESOLUTION_ENV_VAR]);
const resolutions = [];
const unresolved = [];
for (const action of blocked) {
if (action && action.type === 'prompt-user') {
const classification = classifyPromptUserAction(action);
if (classification) {
const resolved = materializeResolution(action, classification.choice);
let category = null;
let choice = null;
let source = null;
if (envChoice && actionSupportsChoice(action, envChoice)) {
category = 'operator-override';
choice = envChoice;
source = RESOLUTION_ENV_VAR;
} else {
const classification = classifyPromptUserAction(action);
if (classification) {
category = classification.category;
choice = classification.choice;
source = 'non-tty-default';
}
}
if (choice) {
const resolved = materializeResolution(action, choice);
// Inject the concrete action into plan.actions so the apply
// step picks it up.
if (result.plan && Array.isArray(result.plan.actions)) {
@@ -179,11 +213,11 @@ function resolveInstallerMigrationPromptsForNonTty(result, options = {}) {
}
resolutions.push({
relPath: action.relPath,
category: classification.category,
choice: classification.choice,
category,
choice,
reason: action.reason,
resolvedActionType: resolved.type,
source: 'non-tty-default',
source,
});
continue;
}

View File

@@ -198,4 +198,42 @@ describe('#3541: installer migration prompt-user non-TTY resolution', { concurre
assert.equal(reasons.length, 1, 'two same-reason paths grouped under one key');
assert.equal(captured.blockedByReason[reasons[0]].length, 2);
});
test('Test C: non-TTY env override resolves otherwise-unclassified prompt-user actions', () => {
const result = {
blocked: [
{
type: 'prompt-user',
relPath: 'skills/gsd-custom/SKILL.toml',
reason: 'custom skill metadata requires user decision',
choices: ['keep', 'remove'],
},
],
plan: {
actions: [],
blocked: [
{
type: 'prompt-user',
relPath: 'skills/gsd-custom/SKILL.toml',
reason: 'custom skill metadata requires user decision',
choices: ['keep', 'remove'],
},
],
},
};
const resolved = resolveInstallerMigrationPromptsForNonTty(result, {
isTty: false,
env: { GSD_INSTALLER_MIGRATION_RESOLVE: 'keep' },
});
assert.equal(resolved.resolutions.length, 1, 'env override resolves prompt-user action');
assert.equal(resolved.resolutions[0].choice, 'keep');
assert.equal(resolved.resolutions[0].source, 'GSD_INSTALLER_MIGRATION_RESOLVE');
assert.equal(resolved.resolutions[0].category, 'operator-override');
assert.equal((resolved.result.blocked || []).length, 0);
assert.equal((resolved.result.plan.blocked || []).length, 0);
assert.equal((resolved.result.plan.actions || []).length, 1);
assert.equal(resolved.result.plan.actions[0].type, 'baseline-preserve-user');
});
});