fix: honor installer migration resolution env override
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user