mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-15 02:49:33 +01:00
It stores true rather than clearing the key, so the line now says what happened. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
111 lines
6.3 KiB
TypeScript
111 lines
6.3 KiB
TypeScript
/**
|
|
* @fileoverview One-time boot migration that lets the 1.50.0 agent skill default reach installs
|
|
* that saved App Settings before it.
|
|
*
|
|
* `agentSkillEnabled` is ON by default since 1.50.0 (an absent key is on, only an explicit false
|
|
* is off). Up to 1.49.0 the App Settings switch loaded an absent key as off (`?? false`) and Save
|
|
* sends the whole form, so every install whose App Settings were ever saved holds an explicit
|
|
* `false` the user may never have chosen, and the new default would never reach it. On the first
|
|
* start of a server that has this module, a stored `false` is switched on, exactly once per data
|
|
* dir. It cannot tell a deliberate off from the old default; that is accepted and stated as a
|
|
* behaviour change in the release notes. An off set after this ran is never touched again.
|
|
*
|
|
* Why a stored `false` becomes `true` rather than being deleted: deleting it would not reach the
|
|
* page. Every browser that saved App Settings keeps the form in its localStorage blob, and
|
|
* `loadAppSettingsFromServer()` only overwrites a synced key the server HAS: a key the server
|
|
* lacks keeps the device's `false`, the switch loads off, and the next Save of any setting writes
|
|
* `false` back (measured with the real page code in the JSDOM harness of
|
|
* test/agent-skill-setting-ui.test.ts, which pins the shipped shape). A stored `true` wins on
|
|
* every device, including a browser still running a cached settings-ui.js from before the upgrade.
|
|
* Every reader treats `true` and an absent key alike (`!== false`).
|
|
*
|
|
* Why the "done" marker is its own file (`agent-skill-default-migrated.json` in the data dir)
|
|
* and not a key in settings.json:
|
|
* - it is not a setting, so `GET /api/settings` never returns it, the page never holds it, and
|
|
* the page's Save (rebuilt from the form) can neither send it back nor drop it; the `.strict()`
|
|
* `SettingsUpdateSchema` never sees it;
|
|
* - a PUT or any other settings.json writer cannot erase it, which would re-arm the migration and
|
|
* switch a deliberate later off back on;
|
|
* - it is not in `CONFIG_BACKUP_FILES` (like `config-backup-id`), so a restore leaves it alone:
|
|
* once the migration has run, a restored settings.json, even one from before 1.50.0 holding
|
|
* `false`, is what the user asked for and stays as restored.
|
|
* A data dir is one instance, and settings.json is one file for the whole server, so in
|
|
* multi-user mode this runs once for the server, never per user.
|
|
*
|
|
* Order: the marker is checked first (present: nothing is read), the settings change goes through
|
|
* `mutateSettingsFile()` (serialized, atomic, refuses a file that does not parse), and the marker
|
|
* is written only after that resolves. A refused or failed settings write writes no marker, so the
|
|
* migration runs again at the next start (after the user fixed the file). Until then it has not
|
|
* run, so an off saved after the fix and before that start is switched on with the rest: the same
|
|
* "cannot tell" trade as above, in a window that needs an unparseable settings.json at the first
|
|
* 1.50.0 start. A crash between the two writes is harmless: the next start finds `true` (nothing
|
|
* to change) and writes the marker.
|
|
*
|
|
* Called once from `WebServer.start()`, before the first settings read and before the server
|
|
* listens, so no request path can see the old value.
|
|
*/
|
|
|
|
import fs from 'node:fs/promises';
|
|
import { dirname } from 'node:path';
|
|
import { dataPath } from '../config/instance.js';
|
|
import { getErrorMessage } from '../types.js';
|
|
import { mutateSettingsFile, SettingsWriteRefusedError } from './settings-writer.js';
|
|
|
|
/** The marker's file name in the data dir. Never in CONFIG_BACKUP_FILES (see the file comment). */
|
|
export const AGENT_SKILL_DEFAULT_MARKER = 'agent-skill-default-migrated.json';
|
|
|
|
/**
|
|
* - `switched-on`: settings.json held `agentSkillEnabled: false`; it now holds `true`.
|
|
* - `nothing-to-change`: the key was absent or not `false` (or there is no settings.json); nothing was written to it.
|
|
* - `already-done`: the marker exists; settings.json was not read.
|
|
* - `refused`: settings.json could not be read, parsed or written, or the marker could not be checked.
|
|
* Nothing changed and no marker was written, so the next start tries again.
|
|
*/
|
|
export type AgentSkillDefaultMigration = 'switched-on' | 'nothing-to-change' | 'already-done' | 'refused';
|
|
|
|
/** The log line of a migration that switched the skill on. */
|
|
export const AGENT_SKILL_DEFAULT_MIGRATED_LOG =
|
|
'[agent-skill] switched on the off stored before 1.50.0, so the new default applies (turn it off again under App Settings, Agents & CLIs, Codeman Agent Skill; that choice is kept)';
|
|
|
|
export async function migrateAgentSkillDefault(
|
|
markerPath: string = dataPath(AGENT_SKILL_DEFAULT_MARKER)
|
|
): Promise<AgentSkillDefaultMigration> {
|
|
try {
|
|
await fs.lstat(markerPath);
|
|
return 'already-done';
|
|
} catch (err) {
|
|
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
|
|
// Not knowing whether it ran must not switch a deliberate off on a second time.
|
|
console.warn(`[agent-skill] default migration skipped: cannot check ${markerPath} (${getErrorMessage(err)})`);
|
|
return 'refused';
|
|
}
|
|
}
|
|
|
|
let switched = false;
|
|
try {
|
|
await mutateSettingsFile((current) => {
|
|
if (current.agentSkillEnabled !== false) return undefined;
|
|
switched = true;
|
|
return { ...current, agentSkillEnabled: true };
|
|
});
|
|
} catch (err) {
|
|
// A refusal is already logged by the writer (`[settings] write refused: ...`), with the fix.
|
|
const why = err instanceof SettingsWriteRefusedError ? 'settings.json was left as it is' : getErrorMessage(err);
|
|
console.warn(`[agent-skill] default migration skipped (${why}); it runs again at the next start`);
|
|
return 'refused';
|
|
}
|
|
|
|
try {
|
|
await fs.mkdir(dirname(markerPath), { recursive: true });
|
|
const marker = { migratedAt: new Date().toISOString(), switchedOn: switched };
|
|
await fs.writeFile(markerPath, `${JSON.stringify(marker, null, 2)}\n`, { mode: 0o600 });
|
|
} catch (err) {
|
|
// settings.json is already right; without the marker the next start runs again, which
|
|
// changes nothing unless the user has switched it off by then.
|
|
console.warn(`[agent-skill] could not record the default migration at ${markerPath}: ${getErrorMessage(err)}`);
|
|
}
|
|
|
|
if (switched) console.log(AGENT_SKILL_DEFAULT_MIGRATED_LOG);
|
|
return switched ? 'switched-on' : 'nothing-to-change';
|
|
}
|