Skip to content

Instantly share code, notes, and snippets.

@albannurkollari
Created November 25, 2025 17:12
Show Gist options
  • Select an option

  • Save albannurkollari/46db431ebeb080dc380a1ae79ebe3fbf to your computer and use it in GitHub Desktop.

Select an option

Save albannurkollari/46db431ebeb080dc380a1ae79ebe3fbf to your computer and use it in GitHub Desktop.
NodeJS Command Builder
/** biome-ignore-all lint/suspicious/noExplicitAny: <allow it> */
import type { SpawnSyncReturns } from 'node:child_process';
import { execSync } from 'node:child_process';
// import pc from 'picocolors';
type ChainableGetter<Keys extends string, T extends Record<string, any>> = {
readonly [K in Keys]: ChainableGetter<Keys, T> & T;
};
export class Runner {
protected readonly cmd = 'pnpm';
protected exec(cmd: string) {
// console.log(pc.bgMagenta(`> ${cmd}`), '\n');
console.log(`> ${cmd}`, '\n');
try {
const stdout = execSync(cmd, { encoding: 'utf-8', stdio: 'inherit' });
return {
ok: true,
code: 0,
stdout,
stderr: '',
};
} catch (err) {
const result = err as SpawnSyncReturns<string>;
return {
ok: false,
code: result.status ?? 1,
stdout: result.stdout?.toString() ?? '',
stderr: result.stderr?.toString() ?? '',
};
}
}
}
export class CommandBuilder<
Main extends string,
Subs extends string,
Flags extends string = string
> extends Runner {
#parts: Array<Main | Subs | Flags | (string & {})> = [];
#main: Main;
constructor(config: { main: Main; sub: Subs[]; flags?: Flags[] }) {
super();
this.#main = config.main;
this.reset();
config.sub.forEach((subCmd) => {
Object.defineProperty(this, subCmd, {
get() {
this.#parts.push(subCmd);
return this;
},
configurable: false,
enumerable: true,
});
});
}
run(...args: Array<Flags>) {
const cmd = [...this.#parts, ...args].join(' ');
this.reset();
return this.exec(cmd);
}
reset() {
this.#parts = [this.cmd, this.#main];
}
}
export const createCommand = <
Main extends string,
Subs extends string,
Flags extends string = string
>(config: {
main: Main;
sub: Subs[];
flags?: Flags[];
}) => {
const builder = new CommandBuilder(config);
return builder as ChainableGetter<Main | Subs, typeof builder>;
};
@albannurkollari
Copy link
Author

Example:

const git = createCommand({
  main: 'git',
  sub: ['diff'],
  flags: ['--name-only', '--quiet', 'HEAD'],
});

Then you'll get chainable autocomplete:

git.diff.run('--name-only', '--quiet');

Which always returns this interface:

type ExecReturnValue = {
    ok: boolean;
    code: number;
    stdout: string;
    stderr: string;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment