From 10cb65e60f90ebe99f0eba368b1c2c77c573f844 Mon Sep 17 00:00:00 2001 From: Siddharth Singh Date: Tue, 17 Sep 2024 21:04:26 +0530 Subject: [PATCH] throw out commander in favour of homegrown argument parser --- deno.json | 4 +- deno.lock | 16 +++-- tribes-cli/cli.ts | 173 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 184 insertions(+), 9 deletions(-) create mode 100644 tribes-cli/cli.ts diff --git a/deno.json b/deno.json index 71f778d9..d0c867e5 100644 --- a/deno.json +++ b/deno.json @@ -7,8 +7,7 @@ "db:export": "deno run -A scripts/db-export.ts", "db:import": "deno run -A scripts/db-import.ts", "db:migrate": "deno run -A scripts/db-migrate.ts", - "headless:setup": "deno run -A tribes/setup.ts", - "headless:uploader-config": "deno run -A tribes/uploader-config.ts", + "tribes-cli": "deno run -A tribes-cli/cli.ts", "nostr:pull": "deno run -A scripts/nostr-pull.ts", "debug": "deno run -A --inspect src/server.ts", "test": "deno test -A --junit-path=./deno-test.xml", @@ -52,7 +51,6 @@ "@std/streams": "jsr:@std/streams@^0.223.0", "comlink": "npm:comlink@^4.4.1", "comlink-async-generator": "npm:comlink-async-generator@^0.0.1", - "commander": "npm:commander@12.1.0", "deno-safe-fetch/load": "https://gitlab.com/soapbox-pub/deno-safe-fetch/-/raw/v1.0.0/load.ts", "deno.json": "./deno.json", "entities": "npm:entities@^4.5.0", diff --git a/deno.lock b/deno.lock index 11789c33..558b6e57 100644 --- a/deno.lock +++ b/deno.lock @@ -29,6 +29,7 @@ "jsr:@soapbox/kysely-pglite@^0.0.1": "jsr:@soapbox/kysely-pglite@0.0.1", "jsr:@soapbox/stickynotes@^0.4.0": "jsr:@soapbox/stickynotes@0.4.0", "jsr:@std/assert@^0.213.1": "jsr:@std/assert@0.213.1", + "jsr:@std/assert@^0.217.0": "jsr:@std/assert@0.217.0", "jsr:@std/assert@^0.223.0": "jsr:@std/assert@0.223.0", "jsr:@std/assert@^0.224.0": "jsr:@std/assert@0.224.0", "jsr:@std/assert@^0.225.1": "jsr:@std/assert@0.225.3", @@ -68,7 +69,6 @@ "npm:comlink-async-generator": "npm:comlink-async-generator@0.0.1", "npm:comlink-async-generator@^0.0.1": "npm:comlink-async-generator@0.0.1", "npm:comlink@^4.4.1": "npm:comlink@4.4.1", - "npm:commander@12.1.0": "npm:commander@12.1.0", "npm:entities@^4.5.0": "npm:entities@4.5.0", "npm:fast-stable-stringify@^1.0.0": "npm:fast-stable-stringify@1.0.0", "npm:formdata-helper@^0.3.0": "npm:formdata-helper@0.3.0", @@ -312,6 +312,9 @@ "@std/assert@0.213.1": { "integrity": "24c28178b30c8e0782c18e8e94ea72b16282207569cdd10ffb9d1d26f2edebfe" }, + "@std/assert@0.217.0": { + "integrity": "c98e279362ca6982d5285c3b89517b757c1e3477ee9f14eb2fdf80a45aaa9642" + }, "@std/assert@0.223.0": { "integrity": "eb8d6d879d76e1cc431205bd346ed4d88dc051c6366365b1af47034b0670be24" }, @@ -444,6 +447,12 @@ "jsr:@std/assert@^0.213.1" ] }, + "@std/path@0.217.0": { + "integrity": "1217cc25534bca9a2f672d7fe7c6f356e4027df400c0e85c0ef3e4343bc67d11", + "dependencies": [ + "jsr:@std/assert@^0.217.0" + ] + }, "@std/path@1.0.0-rc.1": { "integrity": "b8c00ae2f19106a6bb7cbf1ab9be52aa70de1605daeb2dbdc4f87a7cbaf10ff6" }, @@ -638,10 +647,6 @@ "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", "dependencies": {} }, - "commander@12.1.0": { - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", - "dependencies": {} - }, "cross-spawn@7.0.3": { "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "dependencies": { @@ -2030,7 +2035,6 @@ "npm:@soapbox.pub/pglite@^0.2.10", "npm:comlink-async-generator@^0.0.1", "npm:comlink@^4.4.1", - "npm:commander@12.1.0", "npm:entities@^4.5.0", "npm:fast-stable-stringify@^1.0.0", "npm:formdata-helper@^0.3.0", diff --git a/tribes-cli/cli.ts b/tribes-cli/cli.ts new file mode 100644 index 00000000..4f25b982 --- /dev/null +++ b/tribes-cli/cli.ts @@ -0,0 +1,173 @@ +interface BaseArg { + kind: 'string' | 'number' | 'bool'; + name: string; + invert?: true; +} + +type Argument = BaseArg & { optional?: true }; +type Option = BaseArg & { short?: string }; + +interface BaseCommand { + options: Option[]; + name: string; +} + +type Command = + | ( + & BaseCommand + & ({ + arguments: Argument[]; + } | { + subcommands: Command[]; + }) + ) + | BaseCommand; + +interface ParseResult { + [key: string]: boolean | string | number | ParseResult; +} + +const parseArgs = (args: string[], config: Command) => { + const result: ParseResult = {}; + + let i = 0; + const opt = (kind: BaseArg['kind'], idx: number) => kind === 'number' ? Number(args[idx]) : args[idx]; + const doOption = (cfg: Option, skip = -1, blockLength = -1) => { + if (cfg.kind === 'number' || cfg.kind === 'string') { + if (skip === -1) { + result[cfg.name] = opt(cfg.kind, i + 1); + i += 1; + } else { + result[cfg.name] = opt(cfg.kind, i + skip); + if (skip === blockLength) { + i += skip; + } + } + } else { + result[cfg.name] = !cfg.invert; + } + }; + + if ('arguments' in config && config.arguments.length) { + const argumentCount = args.filter((arg) => !arg.startsWith('-')).length; + const last = config.arguments.at(-1); + const requiredCount = config.arguments.length - (last?.optional ? 1 : 0); + if (argumentCount !== requiredCount) throw new Error('Invalid number of arguments passed!'); + } + + let argumentsParsed = 0; + for (i; i < args.length; i++) { + const current = args[i]; + if (current[0] === '-') { + if (current.startsWith('--')) { + const opt = current.slice(2); + const cfg = config.options.find((o) => o.name === opt); + if (!cfg) continue; + doOption(cfg); + } else { + const block = current.slice(1).split(''); + for (let idx = 0; idx < block.length; idx++) { + const cfg = config.options.find((o) => o.short === block[idx]); + if (!cfg) continue; + doOption(cfg, idx + 1, block.length); + } + } + } else { + if ('subcommands' in config) { + const subcommand = config.subcommands.find((cmd) => cmd.name === current); + if (subcommand) { + result[subcommand.name] = parseArgs(args.slice(i + 1), subcommand); + } + } else if ('arguments' in config) { + const arg = config.arguments[argumentsParsed++]; + if (arg.kind === 'bool') { + if (args[i] !== 'true' && args[i] !== 'false') { + throw new Error(`Non-boolean value passed for boolean argument ${arg.name}!`); + } + result[arg.name] = args[i] === 'true' ? true : false; + } else if (arg.kind === 'string') { + result[arg.name] = args[i]; + } else if (arg.kind === 'number') { + result[arg.name] = Number(args[i]); + } + } + } + } + return result; +}; + +/** + * TODOs + * - get instance list from dokku for pushing updates + * - fix item duplication issues with colocated postgres + * - make dokku work with existing postgres + * - make new-tribe script work with custom domains + * - get ssh key as -i argument + * - + */ + +const res = parseArgs(Deno.args, { + name: 'tribes-cli', + options: [{ + kind: 'string', + name: 'identity-file', + short: 'i', + }], + subcommands: [{ + name: 'tribe', + options: [{ + name: 'alice', + short: 'a', + kind: 'number', + }, { + name: 'bob', + short: 'b', + kind: 'string', + }, { + name: 'charlie', + short: 'c', + kind: 'number', + }], + subcommands: [{ + name: 'new', + arguments: [{ + name: 'name', + kind: 'string', + }], + options: [], + }], + }], +}); + +console.log(res); + +Deno.exit(1); +/* +if (import.meta.main) { + const tribes = + new Command('tribes-cli') + .description('CLI for managing Ditto Tribes instances.') + .version("0.0.1") + .option('-i --identity-file ', 'Path to the ssh identity file.', defaultIdentityFile()) + .action(async args => { + if (!args.identityFile) { + throw new InvalidArgumentError(`Invalid or missing identity file ${args.identityFile || ''}`); + } + try { + console.log(args.identityFile); + const file = await Deno.readTextFile(args.identityFile); + sessionStorage.setItem('identity', file); + console.log(`Read identity file from ${args.identityFile} successfully.`); + } + catch (e) { + throw new InvalidArgumentError(`Error reading identity file: ${e.message}`); + } + }) + .showHelpAfterError(); + + [tribe, remote].forEach(cmd => tribes.addCommand(cmd)); + const foo = await tribes.parseAsync(Deno.args, { from: 'user' }); + console.log(foo); +} + +*/