favacli 0.0.23 → 0.0.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,11 +11,14 @@ declare abstract class BaseCommand extends Command {
11
11
  abstract requireFavaLib: boolean;
12
12
  errors: ErrorInCommand[];
13
13
  preFormattedOutput: boolean;
14
- json: boolean | undefined;
14
+ rawOutput: boolean;
15
+ format: string | undefined;
15
16
  verbose: boolean | undefined;
16
17
  lockedRepresentationString: LockedRepresentationString;
17
18
  settings: Settings;
18
19
  favaLib: FavaLib;
20
+ get machineOutput(): boolean;
21
+ protected validFormats(): string[];
19
22
  output(string: string): void;
20
23
  execute(): Promise<number>;
21
24
  private addError;
@@ -6,19 +6,35 @@ class BaseCommand extends Command {
6
6
  super(...arguments);
7
7
  this.errors = [];
8
8
  this.preFormattedOutput = false;
9
- this.json = Option.Boolean('--json', {
10
- description: 'output as json',
9
+ // when true, exec()'s result is a raw string to write to stdout verbatim
10
+ // (no JSON.stringify, no escaping/trimming) — e.g. rofi script-mode output
11
+ this.rawOutput = false;
12
+ this.format = Option.String('--format', {
13
+ description: 'output format (e.g. json)',
11
14
  });
12
15
  this.verbose = Option.Boolean('--verbose', {
13
16
  description: 'verbose output',
14
17
  });
15
18
  }
19
+ // machine-readable mode: any --format suppresses human output and triggers
20
+ // serialized output
21
+ get machineOutput() {
22
+ return this.format !== undefined;
23
+ }
24
+ // the set of --format values this command accepts; subclasses extend it
25
+ validFormats() {
26
+ return ['json'];
27
+ }
16
28
  output(string) {
17
- if (!this.json) {
29
+ if (!this.machineOutput) {
18
30
  this.context.stdout.write(string);
19
31
  }
20
32
  }
21
33
  async execute() {
34
+ if (this.format !== undefined &&
35
+ !this.validFormats().includes(this.format)) {
36
+ throw new Error(`Unknown format: ${this.format}. Valid formats: ${this.validFormats().join(', ')}`);
37
+ }
22
38
  const { lockedRepresentationString, settings } = await init();
23
39
  this.settings = settings;
24
40
  this.lockedRepresentationString = lockedRepresentationString;
@@ -34,10 +50,16 @@ class BaseCommand extends Command {
34
50
  if (this.favaLib?.sync) {
35
51
  this.favaLib.sync.closeServerConnection();
36
52
  }
37
- if (this.json) {
53
+ if (this.machineOutput) {
38
54
  // output is already formatted, don't add the result & errors bit
39
55
  if (this.preFormattedOutput) {
40
- this.context.stdout.write(JSON.stringify(result, null, 2) + '\n');
56
+ if (this.rawOutput) {
57
+ // raw passthrough: write the formatter's string exactly as-is
58
+ this.context.stdout.write(result);
59
+ }
60
+ else {
61
+ this.context.stdout.write(JSON.stringify(result, null, 2) + '\n');
62
+ }
41
63
  }
42
64
  else {
43
65
  this.context.stdout.write(JSON.stringify({ result, errors: this.errors }, null, 2) + '\n');
@@ -46,7 +68,7 @@ class BaseCommand extends Command {
46
68
  return 0;
47
69
  }
48
70
  addError(err) {
49
- if (this.json && !this.verbose) {
71
+ if (this.machineOutput && !this.verbose) {
50
72
  this.errors.push({ timestamp: Date.now(), message: err.message });
51
73
  }
52
74
  else {
@@ -1,12 +1,11 @@
1
1
  import type { EntryMeta, EntryMetaWithToken } from 'favalib';
2
2
  import BaseCommand from './BaseCommand.mjs';
3
3
  import type { ErrorInCommand } from './BaseCommand.mjs';
4
- import formatters from './formatters/index.mjs';
5
4
  import { Jsonifiable } from 'type-fest';
6
5
  export type Formatter = (entries: (EntryMeta | EntryMetaWithToken)[], errors: ErrorInCommand[]) => Jsonifiable;
7
6
  declare abstract class BaseListOutputCommand extends BaseCommand {
8
7
  abstract getList(): Promise<(EntryMeta | EntryMetaWithToken)[]>;
9
- format: (typeof formatters)[0]["name"] | undefined;
8
+ protected validFormats(): string[];
10
9
  exec(): Promise<string | number | boolean | {
11
10
  [x: string]: Jsonifiable | undefined;
12
11
  } | {
@@ -1,29 +1,30 @@
1
- import { Option } from 'clipanion';
2
1
  import BaseCommand from './BaseCommand.mjs';
3
2
  import generateEntriesTable from './utils/generateEntriesTable.mjs';
4
3
  import formatters from './formatters/index.mjs';
5
- const formattersMap = new Map(formatters.map((f) => [f.name, f.formatter]));
4
+ const formattersMap = new Map(formatters.map((f) => [f.name, f]));
6
5
  class BaseListOutputCommand extends BaseCommand {
7
- constructor() {
8
- super(...arguments);
9
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
10
- this.format = Option.String('--format', {
11
- description: 'formatter',
12
- });
6
+ validFormats() {
7
+ return ['json', ...formatters.map((f) => f.name)];
13
8
  }
14
9
  async exec() {
15
- this.preFormattedOutput = this.format !== undefined;
16
10
  let formatter = (json) => json;
17
- if (this.format) {
11
+ // 'json' (and no --format) uses the default identity formatter and is
12
+ // wrapped in { result, errors } by BaseCommand; named formatters produce
13
+ // pre-formatted output instead
14
+ if (this.format && this.format !== 'json') {
18
15
  const selectedFormatter = formattersMap.get(this.format);
19
16
  if (!selectedFormatter) {
20
17
  throw new Error(`Formatter ${this.format} not found`);
21
18
  }
22
- formatter = selectedFormatter;
19
+ formatter = selectedFormatter.formatter;
20
+ this.preFormattedOutput = true;
21
+ this.rawOutput = 'raw' in selectedFormatter && selectedFormatter.raw;
23
22
  }
24
23
  const list = await this.getList();
25
24
  if (list.length === 0) {
26
- this.context.stdout.write('No entries\n');
25
+ // output() suppresses this when --format is set, so it can't corrupt the
26
+ // machine-readable formatter output
27
+ this.output('No entries\n');
27
28
  return formatter([], this.errors);
28
29
  }
29
30
  this.output(generateEntriesTable(list));
@@ -0,0 +1,9 @@
1
+ import BaseCommand from '../../BaseCommand.mjs';
2
+ declare class EntriesGetTokenByIdCommand extends BaseCommand {
3
+ static paths: string[][];
4
+ static usage: import("clipanion").Usage;
5
+ requireFavaLib: boolean;
6
+ id: string;
7
+ exec(): Promise<string | null>;
8
+ }
9
+ export default EntriesGetTokenByIdCommand;
@@ -0,0 +1,39 @@
1
+ import { Option } from 'clipanion';
2
+ import { EntryNotFoundError } from 'favalib';
3
+ import BaseCommand from '../../BaseCommand.mjs';
4
+ class EntriesGetTokenByIdCommand extends BaseCommand {
5
+ constructor() {
6
+ super(...arguments);
7
+ this.requireFavaLib = true;
8
+ this.id = Option.String({ required: true });
9
+ }
10
+ static { this.paths = [['entries', 'get-token-by-id']]; }
11
+ static { this.usage = BaseCommand.Usage({
12
+ category: 'Entries',
13
+ description: 'Get the current TOTP token for an entry by its id',
14
+ details: `
15
+ This command generates and prints the current TOTP code for a single
16
+ entry, identified by its id.
17
+ `,
18
+ examples: [
19
+ ['Get the current token for an entry', 'entries get-token-by-id <id>'],
20
+ ],
21
+ }); }
22
+ async exec() {
23
+ const id = this.id;
24
+ try {
25
+ const token = await this.favaLib.vault.generateTokenForEntry(id);
26
+ this.output(`${token.otp}\n`);
27
+ return token.otp;
28
+ }
29
+ catch (err) {
30
+ if (err instanceof EntryNotFoundError) {
31
+ this.output(`No entry found with id ${this.id}\n`);
32
+ this.errors.push({ timestamp: Date.now(), message: err.message });
33
+ return null;
34
+ }
35
+ throw err;
36
+ }
37
+ }
38
+ }
39
+ export default EntriesGetTokenByIdCommand;
@@ -1,5 +1,9 @@
1
- declare const formatters: {
1
+ declare const formatters: ({
2
2
  name: "alfred";
3
3
  formatter: import("../BaseListOutputCommand.mjs").Formatter;
4
- }[];
4
+ } | {
5
+ name: "rofi";
6
+ formatter: import("../BaseListOutputCommand.mjs").Formatter;
7
+ raw: boolean;
8
+ })[];
5
9
  export default formatters;
@@ -1,3 +1,4 @@
1
1
  import alfredFormatter from './alfred.mjs';
2
- const formatters = [alfredFormatter];
2
+ import rofiFormatter from './rofi.mjs';
3
+ const formatters = [alfredFormatter, rofiFormatter];
3
4
  export default formatters;
@@ -0,0 +1,7 @@
1
+ import { Formatter } from '../BaseListOutputCommand.mjs';
2
+ declare const _default: {
3
+ name: "rofi";
4
+ formatter: Formatter;
5
+ raw: boolean;
6
+ };
7
+ export default _default;
@@ -0,0 +1,17 @@
1
+ const NUL = '\0'; // separates display text from the row's options
2
+ const US = '\x1f'; // separates option key/value pairs
3
+ const rofiFormatter = (entries, errors) => {
4
+ const lines = [`${NUL}prompt${US}2FA`];
5
+ if (errors.length > 0) {
6
+ const msg = errors.map((e, i) => `[${i}] ${e.message}`).join(' ');
7
+ lines.push(`${NUL}message${US}${msg}`);
8
+ }
9
+ for (const entry of entries) {
10
+ const display = `${entry.issuer}: ${entry.name}`;
11
+ lines.push(`${display}${NUL}info${US}${entry.id}${US}meta${US}${entry.issuer} ${entry.name}`);
12
+ }
13
+ return lines.join('\n');
14
+ };
15
+ // raw: rofi script mode consumes line-based text with embedded control bytes,
16
+ // so this output must be written to stdout verbatim, not JSON-stringified.
17
+ export default { name: 'rofi', formatter: rofiFormatter, raw: true };
package/build/main.mjs CHANGED
@@ -6,6 +6,7 @@ import VaultRestorePasswordCommand from './commands/vault/restorePassword.mjs';
6
6
  import EntriesAddCommand from './commands/entries/add.mjs';
7
7
  import EntriesListCommand from './commands/entries/list.mjs';
8
8
  import EntriesSearchCommand from './commands/entries/search.mjs';
9
+ import EntriesGetTokenByIdCommand from './commands/entries/getTokenById.mjs';
9
10
  import SyncSetServerUrlCommand from './commands/sync/setServerUrl.mjs';
10
11
  import SyncConnect from './commands/sync/connect.mjs';
11
12
  import SyncResilver from './commands/sync/resilver.mjs';
@@ -23,7 +24,7 @@ const [, , ...args] = process.argv;
23
24
  const cli = new Cli({
24
25
  binaryLabel: 'FavaCli',
25
26
  binaryName: `favacli`,
26
- binaryVersion: '0.0.23',
27
+ binaryVersion: '0.0.26',
27
28
  });
28
29
  cli.register(VaultCreateCommand);
29
30
  cli.register(VaultDeleteCommand);
@@ -31,6 +32,7 @@ cli.register(VaultRestorePasswordCommand);
31
32
  cli.register(EntriesAddCommand);
32
33
  cli.register(EntriesListCommand);
33
34
  cli.register(EntriesSearchCommand);
35
+ cli.register(EntriesGetTokenByIdCommand);
34
36
  cli.register(SyncSetServerUrlCommand);
35
37
  cli.register(SyncConnect);
36
38
  cli.register(SyncResilver);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "favacli",
3
- "version": "0.0.23",
3
+ "version": "0.0.26",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "author": "",
@@ -13,7 +13,7 @@
13
13
  "keytar": "^7.9.0",
14
14
  "tty-table": "^5.0.0",
15
15
  "typanion": "^3.14.0",
16
- "favalib": "0.0.17"
16
+ "favalib": "0.0.18"
17
17
  },
18
18
  "devDependencies": {
19
19
  "type-fest": "^5.6.0"