mirror of
https://gitlab.com/soapbox-pub/ditto.git
synced 2025-12-06 11:29:46 +00:00
switch to deno @std/cli based parser
This commit is contained in:
parent
10cb65e60f
commit
055716fd97
9 changed files with 141 additions and 166 deletions
|
|
@ -1,173 +1,24 @@
|
||||||
interface BaseArg {
|
import { parseArgs } from '@std/cli';
|
||||||
kind: 'string' | 'number' | 'bool';
|
import { tribe } from './tribe/mod.ts';
|
||||||
name: string;
|
import { remote } from './remote/mod.ts';
|
||||||
invert?: true;
|
import { ParsedSubcommand, parseSubcommand } from './utils.ts';
|
||||||
}
|
|
||||||
|
|
||||||
type Argument = BaseArg & { optional?: true };
|
const commands = {
|
||||||
type Option = BaseArg & { short?: string };
|
tribe,
|
||||||
|
remote,
|
||||||
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;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
let parserArgs: Partial<ParsedSubcommand> = {
|
||||||
* TODOs
|
string: ['identity-file'],
|
||||||
* - 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, {
|
for (const [_name, body] of Object.entries(commands)) {
|
||||||
name: 'tribes-cli',
|
for (const subcommand in body) {
|
||||||
options: [{
|
const s = body[subcommand];
|
||||||
kind: 'string',
|
parserArgs = parseSubcommand(s, parserArgs);
|
||||||
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);
|
// TODO: construct a help string here
|
||||||
|
|
||||||
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 <string>', 'Path to the ssh identity file.', defaultIdentityFile())
|
|
||||||
.action(async args => {
|
|
||||||
if (!args.identityFile) {
|
|
||||||
throw new InvalidArgumentError(`Invalid or missing identity file ${args.identityFile || '<empty string>'}`);
|
|
||||||
}
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
*/
|
console.log(parserArgs);
|
||||||
|
|
|
||||||
2
tribes-cli/remote/init.ts
Normal file
2
tribes-cli/remote/init.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export const init = () => {
|
||||||
|
};
|
||||||
9
tribes-cli/remote/mod.ts
Normal file
9
tribes-cli/remote/mod.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { Subcommand } from '../utils.ts';
|
||||||
|
import { init } from './init.ts';
|
||||||
|
|
||||||
|
export const remote: Record<string, Subcommand> = {
|
||||||
|
init: {
|
||||||
|
action: init,
|
||||||
|
description: 'Initialise a brand new Ditto remote.',
|
||||||
|
},
|
||||||
|
};
|
||||||
5
tribes-cli/tribe/config.ts
Normal file
5
tribes-cli/tribe/config.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
/**
|
||||||
|
* Change a tribe's configuration.
|
||||||
|
*/
|
||||||
|
export const config = () => {
|
||||||
|
};
|
||||||
6
tribes-cli/tribe/destroy.ts
Normal file
6
tribes-cli/tribe/destroy.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
/**
|
||||||
|
* Destroy a tribe. Non-reversible.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const destroy = () => {
|
||||||
|
};
|
||||||
18
tribes-cli/tribe/mod.ts
Normal file
18
tribes-cli/tribe/mod.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { create } from './new.ts';
|
||||||
|
import { config } from './config.ts';
|
||||||
|
import { Subcommand } from '../utils.ts';
|
||||||
|
|
||||||
|
export const tribe: Record<string, Subcommand> = {
|
||||||
|
create: {
|
||||||
|
action: create,
|
||||||
|
description: 'Create a new tribe.',
|
||||||
|
options: {
|
||||||
|
'-c --custom-domain': {
|
||||||
|
description: 'Do not use a subdomain of the tribes server; instead, use a custom domain.',
|
||||||
|
},
|
||||||
|
'-s --subdomain': {
|
||||||
|
description: 'Use a subdomain of the tribes server to host the new Ditto instance.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
2
tribes-cli/tribe/new.ts
Normal file
2
tribes-cli/tribe/new.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export const create = () => {
|
||||||
|
};
|
||||||
6
tribes-cli/tribe/update.ts
Normal file
6
tribes-cli/tribe/update.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
/**
|
||||||
|
* Update a tribe's software.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const update = () => {
|
||||||
|
};
|
||||||
76
tribes-cli/utils.ts
Normal file
76
tribes-cli/utils.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
import { join } from '@std/path';
|
||||||
|
|
||||||
|
let identity = '';
|
||||||
|
export const defaultIdentityFile = async () => {
|
||||||
|
if (!identity) {
|
||||||
|
const home = Deno.env.get('HOME');
|
||||||
|
if (home) return identity = await Deno.readTextFile(join(home, '.ssh', 'id_rsa'));
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface Subcommand {
|
||||||
|
action: Function;
|
||||||
|
description: string;
|
||||||
|
options?: Record<
|
||||||
|
string,
|
||||||
|
& { description: string }
|
||||||
|
& ({
|
||||||
|
default?: string;
|
||||||
|
} | {
|
||||||
|
bool: true;
|
||||||
|
default?: boolean;
|
||||||
|
})
|
||||||
|
>;
|
||||||
|
commands?: Record<string, Subcommand>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cleanArg = (arg: string) => {
|
||||||
|
return arg.replace(/^--?/g, '');
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ParsedSubcommand {
|
||||||
|
string: string[];
|
||||||
|
boolean: string[];
|
||||||
|
alias: Record<string, string[]>;
|
||||||
|
default: Record<string, string | boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultParsedSubcommand = (): ParsedSubcommand => {
|
||||||
|
return {
|
||||||
|
string: [],
|
||||||
|
boolean: [],
|
||||||
|
alias: {},
|
||||||
|
default: {},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseSubcommand = (command: Subcommand, existing?: Partial<ParsedSubcommand>): ParsedSubcommand => {
|
||||||
|
const res = Object.assign(defaultParsedSubcommand(), existing);
|
||||||
|
|
||||||
|
if (command.options) {
|
||||||
|
for (const option in command.options) {
|
||||||
|
const split = option.split(' ').toSorted((a, b) => b.length - a.length).map(cleanArg);
|
||||||
|
const name = split[0];
|
||||||
|
const body = command.options[option];
|
||||||
|
if ('bool' in body) {
|
||||||
|
res.boolean.push(name);
|
||||||
|
} else {
|
||||||
|
res.string.push(name);
|
||||||
|
}
|
||||||
|
res.alias[name] = split;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.commands) {
|
||||||
|
for (const subcommand in command.commands) {
|
||||||
|
const parsed = parseSubcommand(command.commands[subcommand]);
|
||||||
|
Object.assign(res.alias, parsed.alias);
|
||||||
|
Object.assign(res.default, parsed.default);
|
||||||
|
res.boolean.push(...parsed.boolean);
|
||||||
|
res.string.push(...parsed.string);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
Loading…
Add table
Reference in a new issue