Skip to content

Commit 44d8787

Browse files
authored
refactor: clean up design debt — deprecated APIs, duplicated validation, dead plugin wrappers (#1065)
Three improvements from the design debt audit: 1. Remove deprecated `tabId` field and `getActiveTabId()` method - Delete `tabId` from DaemonCommand (daemon-client.ts) and Command (protocol.ts) - Delete `getActiveTabId()` from IPage interface (types.ts) and Page class (page.ts) - Update extension resolveCommandTabId() to remove legacy fallback - Update handleTabs select case to remove tabId check - The tab→page migration is now complete 2. Unify argument validation into single code path - Remove `normalizeArgValue()` from commanderAdapter.ts - Commander adapter now passes raw values to prepareCommandArgs() - All coercion (bool, int, number) and validation (required, choices) happens once in coerceAndValidateArgs() in execution.ts - Eliminates duplicated boolean normalization 3. Remove dead plugin filesystem wrappers - Delete `promoteDir()` — never called in production code - Delete `replaceDir()` — thin wrapper over beginReplaceDir, never called - Remove corresponding test-only exports and tests - Rename PromoteDirFsOps → ReplaceDirFsOps to match remaining usage - Transaction infrastructure (runTransaction, beginReplaceDir, beginReplaceSymlink) retained — used by publishStandalonePlugin and publishMonorepoPlugins for atomic multi-step operations
1 parent cb9521d commit 44d8787

9 files changed

Lines changed: 10 additions & 126 deletions

File tree

extension/src/background.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -416,12 +416,12 @@ function setWorkspaceSession(workspace: string, session: Omit<AutomationSession,
416416
}
417417

418418
/**
419-
* Resolve tabId from command's page (targetId) or legacy tabId field.
420-
* page (targetId) takes precedence. Returns undefined if neither is provided.
419+
* Resolve tabId from command's page (targetId).
420+
* Returns undefined if no page identity is provided.
421421
*/
422422
async function resolveCommandTabId(cmd: Command): Promise<number | undefined> {
423423
if (cmd.page) return identity.resolveTabId(cmd.page);
424-
return cmd.tabId;
424+
return undefined;
425425
}
426426

427427
type ResolvedTab = { tabId: number; tab: chrome.tabs.Tab | null };
@@ -686,7 +686,7 @@ async function handleTabs(cmd: Command, workspace: string): Promise<Result> {
686686
return { id: cmd.id, ok: true, data: { closed: closedPage } };
687687
}
688688
case 'select': {
689-
if (cmd.index === undefined && cmd.page === undefined && cmd.tabId === undefined)
689+
if (cmd.index === undefined && cmd.page === undefined)
690690
return { id: cmd.id, ok: false, error: 'Missing index or page' };
691691
const cmdTabId = await resolveCommandTabId(cmd);
692692
if (cmdTabId !== undefined) {

extension/src/protocol.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,8 @@ export interface Command {
2525
id: string;
2626
/** Action type */
2727
action: Action;
28-
/** Target page identity (targetId). Cross-layer contract — preferred over tabId. */
28+
/** Target page identity (targetId). Cross-layer contract with the daemon. */
2929
page?: string;
30-
/** @deprecated Legacy tab ID — use `page` (targetId) instead. Kept for backward compat. */
31-
tabId?: number;
3230
/** JS code to evaluate in page context (exec action) */
3331
code?: string;
3432
/** Logical workspace for automation session reuse */

src/browser/daemon-client.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,8 @@ function generateId(): string {
2222
export interface DaemonCommand {
2323
id: string;
2424
action: 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window' | 'sessions' | 'set-file-input' | 'insert-text' | 'bind-current' | 'network-capture-start' | 'network-capture-read' | 'cdp';
25-
/** Target page identity (targetId). Cross-layer contract — preferred over tabId. */
25+
/** Target page identity (targetId). Cross-layer contract with the extension. */
2626
page?: string;
27-
/** @deprecated Legacy tab ID — use `page` (targetId) instead. */
28-
tabId?: number;
2927
code?: string;
3028
workspace?: string;
3129
url?: string;

src/browser/page.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -108,11 +108,6 @@ export class Page extends BasePage {
108108
return this._page;
109109
}
110110

111-
/** @deprecated Use getActivePage() instead */
112-
getActiveTabId(): number | undefined {
113-
return undefined;
114-
}
115-
116111
private _markUnsupportedNetworkCapture(): void {
117112
this._networkCaptureUnsupported = true;
118113
if (this._networkCaptureWarned) return;

src/commanderAdapter.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ describe('commanderAdapter arg passing', () => {
7777

7878
await program.parseAsync(['node', 'opencli', 'paperreview', 'submit', './paper.pdf', '--dry-run', 'maybe']);
7979

80-
// normalizeArgValue validates bools eagerly; executeCommand should not be reached
80+
// prepareCommandArgs validates bools before dispatch; executeCommand should not be reached
8181
expect(mockExecuteCommand).not.toHaveBeenCalled();
8282
});
8383
});

src/commanderAdapter.ts

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,23 +20,10 @@ import { executeCommand, prepareCommandArgs } from './execution.js';
2020
import {
2121
CliError,
2222
EXIT_CODES,
23-
ArgumentError,
2423
toEnvelope,
2524
} from './errors.js';
2625
import { isDiagnosticEnabled } from './diagnostic.js';
2726

28-
export function normalizeArgValue(argType: string | undefined, value: unknown, name: string): unknown {
29-
if (argType !== 'bool' && argType !== 'boolean') return value;
30-
if (typeof value === 'boolean') return value;
31-
if (value == null || value === '') return false;
32-
33-
const normalized = String(value).trim().toLowerCase();
34-
if (normalized === 'true') return true;
35-
if (normalized === 'false') return false;
36-
37-
throw new ArgumentError(`"${name}" must be either "true" or "false".`);
38-
}
39-
4027
/**
4128
* Register a single CliCommand as a Commander subcommand.
4229
*/
@@ -85,7 +72,7 @@ export function registerCommandToProgram(siteCmd: Command, cmd: CliCommand): voi
8572
if (arg.positional) continue;
8673
const camelName = arg.name.replace(/-([a-z])/g, (_m, ch: string) => ch.toUpperCase());
8774
const v = optionsRecord[arg.name] ?? optionsRecord[camelName];
88-
if (v !== undefined) rawKwargs[arg.name] = normalizeArgValue(arg.type, v, arg.name);
75+
if (v !== undefined) rawKwargs[arg.name] = v;
8976
}
9077
const kwargs = prepareCommandArgs(cmd, rawKwargs);
9178

src/plugin.test.ts

Lines changed: 0 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ const {
2020
_getCommitHash,
2121
_installDependencies,
2222
_postInstallMonorepoLifecycle,
23-
_promoteDir,
24-
_replaceDir,
2523
installPlugin,
2624
listPlugins,
2725
_readLockFile,
@@ -1048,68 +1046,6 @@ describe('moveDir', () => {
10481046
});
10491047
});
10501048

1051-
describe('promoteDir', () => {
1052-
it('cleans up temporary publish dir when final rename fails', () => {
1053-
const staging = path.join(os.tmpdir(), 'opencli-promote-stage');
1054-
const dest = path.join(os.tmpdir(), 'opencli-promote-dest');
1055-
const publishErr = new Error('publish failed');
1056-
const existsSync = vi.fn(() => false);
1057-
const mkdirSync = vi.fn(() => undefined);
1058-
const cpSync = vi.fn(() => undefined);
1059-
const rmSync = vi.fn(() => undefined);
1060-
const renameSync = vi.fn((src, _target) => {
1061-
if (String(src) === staging) return;
1062-
throw publishErr;
1063-
});
1064-
1065-
expect(() => _promoteDir(staging, dest, { existsSync, mkdirSync, renameSync, cpSync, rmSync })).toThrow(publishErr);
1066-
1067-
const tempDest = renameSync.mock.calls[0][1];
1068-
expect(renameSync).toHaveBeenNthCalledWith(1, staging, tempDest);
1069-
expect(renameSync).toHaveBeenNthCalledWith(2, tempDest, dest);
1070-
expect(rmSync).toHaveBeenCalledWith(tempDest, { recursive: true, force: true });
1071-
});
1072-
});
1073-
1074-
describe('replaceDir', () => {
1075-
it('rolls back the original destination when swap fails', () => {
1076-
const staging = path.join(os.tmpdir(), 'opencli-replace-stage');
1077-
const dest = path.join(os.tmpdir(), 'opencli-replace-dest');
1078-
const publishErr = new Error('swap failed');
1079-
const existingPaths = new Set([dest]);
1080-
const existsSync = vi.fn((p) => existingPaths.has(String(p)));
1081-
const mkdirSync = vi.fn(() => undefined);
1082-
const cpSync = vi.fn(() => undefined);
1083-
const rmSync = vi.fn(() => undefined);
1084-
const renameSync = vi.fn((src, target) => {
1085-
if (String(src) === staging) {
1086-
existingPaths.add(String(target));
1087-
return;
1088-
}
1089-
if (String(src) === dest) {
1090-
existingPaths.delete(dest);
1091-
existingPaths.add(String(target));
1092-
return;
1093-
}
1094-
if (String(target) === dest) throw publishErr;
1095-
if (existingPaths.has(String(src))) {
1096-
existingPaths.delete(String(src));
1097-
existingPaths.add(String(target));
1098-
}
1099-
});
1100-
1101-
expect(() => _replaceDir(staging, dest, { existsSync, mkdirSync, renameSync, cpSync, rmSync })).toThrow(publishErr);
1102-
1103-
const tempDest = renameSync.mock.calls[0][1];
1104-
const backupDest = renameSync.mock.calls[1][1];
1105-
expect(renameSync).toHaveBeenNthCalledWith(1, staging, tempDest);
1106-
expect(renameSync).toHaveBeenNthCalledWith(2, dest, backupDest);
1107-
expect(renameSync).toHaveBeenNthCalledWith(3, tempDest, dest);
1108-
expect(renameSync).toHaveBeenNthCalledWith(4, backupDest, dest);
1109-
expect(rmSync).toHaveBeenCalledWith(tempDest, { recursive: true, force: true });
1110-
});
1111-
});
1112-
11131049
describe('installPlugin transactional staging', () => {
11141050
const standaloneSource = 'github:user/opencli-plugin-__test-transactional-standalone__';
11151051
const standaloneName = '__test-transactional-standalone__';

src/plugin.ts

Lines changed: 2 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -217,39 +217,13 @@ function moveDir(src: string, dest: string, fsOps: MoveDirFsOps = fs): void {
217217
}
218218
}
219219

220-
type PromoteDirFsOps = MoveDirFsOps & Pick<typeof fs, 'existsSync' | 'mkdirSync'>;
220+
type ReplaceDirFsOps = MoveDirFsOps & Pick<typeof fs, 'existsSync' | 'mkdirSync'>;
221221

222222
function createSiblingTempPath(dest: string, kind: 'tmp' | 'bak'): string {
223223
const suffix = `${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
224224
return path.join(path.dirname(dest), `.${path.basename(dest)}.${kind}-${suffix}`);
225225
}
226226

227-
/**
228-
* Promote a prepared staging directory into its final location.
229-
* The final path is only exposed after the directory has been fully prepared.
230-
*/
231-
function promoteDir(stagingDir: string, dest: string, fsOps: PromoteDirFsOps = fs): void {
232-
if (fsOps.existsSync(dest)) {
233-
throw new PluginError(`Destination already exists: ${dest}`);
234-
}
235-
236-
fsOps.mkdirSync(path.dirname(dest), { recursive: true });
237-
const tempDest = createSiblingTempPath(dest, 'tmp');
238-
239-
try {
240-
moveDir(stagingDir, tempDest, fsOps);
241-
fsOps.renameSync(tempDest, dest);
242-
} catch (err) {
243-
try { fsOps.rmSync(tempDest, { recursive: true, force: true }); } catch {}
244-
throw err;
245-
}
246-
}
247-
248-
function replaceDir(stagingDir: string, dest: string, fsOps: PromoteDirFsOps = fs): void {
249-
const replacement = beginReplaceDir(stagingDir, dest, fsOps);
250-
replacement.finalize();
251-
}
252-
253227
function cloneRepoToTemp(cloneUrl: string): string {
254228
const tmpCloneDir = path.join(
255229
os.tmpdir(),
@@ -359,7 +333,7 @@ function runTransaction<T>(work: (tx: Transaction) => T): T {
359333
function beginReplaceDir(
360334
stagingDir: string,
361335
dest: string,
362-
fsOps: PromoteDirFsOps = fs,
336+
fsOps: ReplaceDirFsOps = fs,
363337
): TransactionHandle {
364338
const destExisted = fsOps.existsSync(dest);
365339
fsOps.mkdirSync(path.dirname(dest), { recursive: true });
@@ -1590,8 +1564,6 @@ export {
15901564
installLocalPlugin as _installLocalPlugin,
15911565
isLocalPluginSource as _isLocalPluginSource,
15921566
moveDir as _moveDir,
1593-
promoteDir as _promoteDir,
1594-
replaceDir as _replaceDir,
15951567
resolvePluginSource as _resolvePluginSource,
15961568
resolveStoredPluginSource as _resolveStoredPluginSource,
15971569
toStoredPluginSource as _toStoredPluginSource,

src/types.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,6 @@ export interface IPage {
8686
getCurrentUrl?(): Promise<string | null>;
8787
/** Returns the active page identity (targetId), or undefined if not yet resolved. */
8888
getActivePage?(): string | undefined;
89-
/** @deprecated Use getActivePage() instead */
90-
getActiveTabId?(): number | undefined;
9189
/** Send a raw CDP command via chrome.debugger passthrough. */
9290
cdp?(method: string, params?: Record<string, unknown>): Promise<unknown>;
9391
/** Click at native coordinates via CDP Input.dispatchMouseEvent. */

0 commit comments

Comments
 (0)