diff --git a/bun.lock b/bun.lock index b2b0ca63c0..697a5f29c5 100644 --- a/bun.lock +++ b/bun.lock @@ -38,7 +38,7 @@ }, "packages/rt-client": { "name": "@mattstack/rt-client", - "version": "0.30.0", + "version": "0.31.0", "dependencies": { "jsonc-parser": "^3.3.1", }, diff --git a/packages/rt-client/package.json b/packages/rt-client/package.json index 21c4cac6af..5181a7324d 100644 --- a/packages/rt-client/package.json +++ b/packages/rt-client/package.json @@ -1,6 +1,6 @@ { "name": "@mattstack/rt-client", - "version": "0.30.0", + "version": "0.31.0", "type": "module", "exports": { ".": { diff --git a/packages/rt-client/src/settings/__tests__/write.test.ts b/packages/rt-client/src/settings/__tests__/write.test.ts index e9cc670917..11f3fe8c7c 100644 --- a/packages/rt-client/src/settings/__tests__/write.test.ts +++ b/packages/rt-client/src/settings/__tests__/write.test.ts @@ -521,6 +521,19 @@ describe("settings/unset", () => { expect(() => unsetSetting("rt.doesNotExist", "user")).toThrow(/unknown setting/); }); + test("removes a retired key a store still carries, from any scope", () => { + mkdirSync(dirname(machineSettingsPath()), { recursive: true }); + writeFileSync(machineSettingsPath(), `{\n "mattstack.mode": "dev",\n "mattstack.appPath": "/Applications/mattstack-dev.app"\n}\n`); + expect(unsetSetting("mattstack.mode", "machine")).toBe(true); + const after = readFileSync(machineSettingsPath(), "utf8"); + expect(after).not.toContain("mattstack.mode"); + expect(after).toContain("mattstack.appPath"); + }); + + test("refuses a retired key with a repo identity", () => { + expect(() => unsetSetting("mattstack.mode", "user", { repoIdentity: IDENTITY })).toThrow(/not repo-scoped/); + }); + test("refuses a scope the def does not allow", () => { expect(() => unsetSetting("rt.repoIdentityOverrides", "user")).toThrow(/cannot be unset in the user store/); }); diff --git a/packages/rt-client/src/settings/write.ts b/packages/rt-client/src/settings/write.ts index 48cb5f449e..7fe753a515 100644 --- a/packages/rt-client/src/settings/write.ts +++ b/packages/rt-client/src/settings/write.ts @@ -98,7 +98,7 @@ import { applyEdits, modify, parseTree, type JSONPath, type Node, type ParseErro import { randomBytes } from "crypto"; import { dirname } from "path"; import { machineSettingsPath, teamSettingsPath, userSettingsPath } from "./paths.ts"; -import { getDef, isMigrated, validateValue, type SettingDef, type SettingScope } from "./registry-machinery.ts"; +import { getDef, isMigrated, isRetiredKey, validateValue, type SettingDef, type SettingScope } from "./registry-machinery.ts"; import { listTeams } from "./stores.ts"; import { isJoinedTeam } from "./team-local-read.ts"; @@ -171,9 +171,15 @@ export function setSetting(key: string, value: unknown, scope: SettingScope, opt * with no local store included — nothing to remove is success, not an error), * and a key not present in the store is a no-op. Returns whether anything was * actually removed; the local-only reminder prints only on a real removal. + * A retired key is not in the registry but may linger in a store, so it can + * still be removed from any scope. */ export function unsetSetting(key: string, scope: SettingScope, opts: SetSettingOpts = {}): boolean { const def = getDef(key); + if (!def && isRetiredKey(key)) { + if (opts.repoIdentity !== undefined) refuse(`"${key}" is not repo-scoped; omit the repo identity`); + return removeKeyFromScope(key, scope, opts, [key]); + } if (!def) { refuse(`unknown setting "${key}" — not in the settings registry (see \`rt settings list\`)`); } @@ -190,10 +196,14 @@ export function unsetSetting(key: string, scope: SettingScope, opts: SetSettingO refuse(`"${key}" is not repo-scoped — omit the repo identity`); } + const jsonPath: JSONPath = opts.repoIdentity !== undefined ? ["repos", opts.repoIdentity, key] : [key]; + return removeKeyFromScope(key, scope, opts, jsonPath); +} + +function removeKeyFromScope(key: string, scope: SettingScope, opts: SetSettingOpts, jsonPath: JSONPath): boolean { const storePath = resolveStorePathForUnset(scope, opts); if (storePath === null || !existsSync(storePath)) return false; - const jsonPath: JSONPath = opts.repoIdentity !== undefined ? ["repos", opts.repoIdentity, key] : [key]; const removed = removeFromStore(storePath, jsonPath); if (removed) {