@doist/cli-core 0.3.0 → 0.5.0

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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## [0.5.0](https://github.com/Doist/cli-core/compare/v0.4.0...v0.5.0) (2026-05-08)
2
+
3
+ ### Features
4
+
5
+ * add global args parser + factories ([#7](https://github.com/Doist/cli-core/issues/7)) ([419243e](https://github.com/Doist/cli-core/commit/419243e8543f180e15a2f6efe91d99a4c93bee40))
6
+
7
+ ## [0.4.0](https://github.com/Doist/cli-core/compare/v0.3.0...v0.4.0) (2026-05-08)
8
+
9
+ ### Features
10
+
11
+ * add printEmpty + describeEmptyMachineOutput helpers ([#6](https://github.com/Doist/cli-core/issues/6)) ([2c0a74e](https://github.com/Doist/cli-core/commit/2c0a74e7874ef47184a071f9fc15f22f254ca20a))
12
+
1
13
  ## [0.3.0](https://github.com/Doist/cli-core/compare/v0.2.0...v0.3.0) (2026-05-06)
2
14
 
3
15
  ### Features
@@ -0,0 +1,15 @@
1
+ import type { ViewOptions } from './options.js';
2
+ /**
3
+ * Gate the empty-state print on the active output mode:
4
+ * --json → prints exactly `'[]'`
5
+ * --ndjson → prints nothing (no stray newline; ndjson EOF = end of stream)
6
+ * neither → prints the human-readable message
7
+ *
8
+ * Use at every list/array empty-result branch so machine consumers never see
9
+ * human strings on stdout when they asked for `--json` / `--ndjson`.
10
+ */
11
+ export declare function printEmpty({ options, message }: {
12
+ options: ViewOptions;
13
+ message: string;
14
+ }): void;
15
+ //# sourceMappingURL=empty.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"empty.d.ts","sourceRoot":"","sources":["../src/empty.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAE/C;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE;IAAE,OAAO,EAAE,WAAW,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAShG"}
package/dist/empty.js ADDED
@@ -0,0 +1,21 @@
1
+ import { formatJson } from './json.js';
2
+ /**
3
+ * Gate the empty-state print on the active output mode:
4
+ * --json → prints exactly `'[]'`
5
+ * --ndjson → prints nothing (no stray newline; ndjson EOF = end of stream)
6
+ * neither → prints the human-readable message
7
+ *
8
+ * Use at every list/array empty-result branch so machine consumers never see
9
+ * human strings on stdout when they asked for `--json` / `--ndjson`.
10
+ */
11
+ export function printEmpty({ options, message }) {
12
+ if (options.json) {
13
+ console.log(formatJson([]));
14
+ return;
15
+ }
16
+ if (options.ndjson) {
17
+ return;
18
+ }
19
+ console.log(message);
20
+ }
21
+ //# sourceMappingURL=empty.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"empty.js","sourceRoot":"","sources":["../src/empty.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAGtC;;;;;;;;GAQG;AACH,MAAM,UAAU,UAAU,CAAC,EAAE,OAAO,EAAE,OAAO,EAA6C;IACtF,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;QACf,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,CAAA;QAC3B,OAAM;IACV,CAAC;IACD,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;QACjB,OAAM;IACV,CAAC;IACD,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;AACxB,CAAC"}
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Centralized, type-safe parsing of well-known global CLI flags shared
3
+ * across the Doist CLIs.
4
+ *
5
+ * Replaces scattered `process.argv.includes()` checks with a single parse
6
+ * that correctly handles grouped short flags (e.g., `-vq`), repeated flags
7
+ * (e.g., `-vvv`), `--flag=value` forms, and avoids false-positives from
8
+ * option values.
9
+ *
10
+ * The parser is pure — pass an explicit argv for testing, or use
11
+ * `createGlobalArgsStore` for the lazy-cached singleton pattern. The store
12
+ * is generic so per-CLI extensions (e.g. todoist's `--user`/`--raw`,
13
+ * twist's `--non-interactive`) can layer their own fields over `GlobalArgs`.
14
+ */
15
+ import type { ViewOptions } from './options.js';
16
+ export type GlobalArgs = Required<Pick<ViewOptions, 'json' | 'ndjson'>> & {
17
+ quiet: boolean;
18
+ verbose: 0 | 1 | 2 | 3 | 4;
19
+ accessible: boolean;
20
+ noSpinner: boolean;
21
+ /** false = absent, true = present without path, string = path. */
22
+ progressJsonl: string | true | false;
23
+ };
24
+ /**
25
+ * Parse well-known global flags from `argv`. Pure — pass an explicit array
26
+ * for testing, or omit to read `process.argv.slice(2)`.
27
+ *
28
+ * The parser scans the entire argv: a CLI-specific positional that happens
29
+ * to look like a global short flag (`td comment add 123 -q`) will flip the
30
+ * matching global state. Workaround: use the standard `--` terminator
31
+ * (`td comment add 123 -- -q`) so the parser stops before the positional.
32
+ * The trade-off is intentional — callers run this before Commander has
33
+ * parsed argv, so we can't yet distinguish positionals from option values.
34
+ *
35
+ * `--progress-jsonl` accepts only the bare form (output to stderr) and the
36
+ * `--progress-jsonl=path` form. The space-separated `--progress-jsonl path`
37
+ * form is intentionally unsupported because it silently consumes the next
38
+ * positional argument (e.g., `td task add --progress-jsonl "Buy milk"`
39
+ * would treat `Buy milk` as a file path).
40
+ */
41
+ export declare function parseGlobalArgs(argv?: string[]): GlobalArgs;
42
+ export type GlobalArgsStore<T extends GlobalArgs = GlobalArgs> = {
43
+ get(): T;
44
+ /** Clear the cached parse result. Call from test teardown. */
45
+ reset(): void;
46
+ };
47
+ /**
48
+ * Lazy-cached singleton wrapper around a parser function. Each CLI builds
49
+ * one store at startup; callers read via `store.get()`. Tests reset between
50
+ * cases so a mutated `process.argv` is re-parsed on next access.
51
+ *
52
+ * ```ts
53
+ * // Vanilla — canonical fields only.
54
+ * const store = createGlobalArgsStore()
55
+ * export const isJsonMode = () => store.get().json
56
+ * export const resetGlobalArgs = store.reset
57
+ *
58
+ * // Extended — layer CLI-specific fields over GlobalArgs.
59
+ * type CliArgs = GlobalArgs & { user: string | undefined; raw: boolean }
60
+ * const store = createGlobalArgsStore<CliArgs>(() => {
61
+ * const base = parseGlobalArgs()
62
+ * const argv = process.argv.slice(2)
63
+ * return { ...base, user: parseUser(argv), raw: argv.includes('--raw') }
64
+ * })
65
+ * ```
66
+ */
67
+ export declare function createGlobalArgsStore(): GlobalArgsStore<GlobalArgs>;
68
+ export declare function createGlobalArgsStore<T extends GlobalArgs>(parse: () => T): GlobalArgsStore<T>;
69
+ export declare function isProgressJsonlEnabled(args: GlobalArgs): boolean;
70
+ export declare function getProgressJsonlPath(args: GlobalArgs): string | undefined;
71
+ export type AccessibleGateOptions = {
72
+ /** Env var that, when set to `'1'`, forces accessible mode (e.g. `TD_ACCESSIBLE`). */
73
+ envVar: string;
74
+ getArgs: () => GlobalArgs;
75
+ };
76
+ /**
77
+ * Build an `isAccessible` predicate that combines the `--accessible` flag
78
+ * with a CLI-specific opt-in env var (e.g. `TD_ACCESSIBLE=1`).
79
+ *
80
+ * ```ts
81
+ * const store = createGlobalArgsStore()
82
+ * export const isAccessible = createAccessibleGate({
83
+ * envVar: 'TD_ACCESSIBLE',
84
+ * getArgs: store.get,
85
+ * })
86
+ * ```
87
+ */
88
+ export declare function createAccessibleGate(opts: AccessibleGateOptions): () => boolean;
89
+ export type SpinnerGateOptions = {
90
+ /** Env var that, when set to `'false'`, force-disables the spinner (e.g. `TD_SPINNER`). */
91
+ envVar: string;
92
+ getArgs: () => GlobalArgs;
93
+ /**
94
+ * CLI-specific extra disable triggers — e.g. twist returns true when
95
+ * `--non-interactive` is set. Evaluated only after the canonical checks
96
+ * already returned false.
97
+ */
98
+ extraTriggers?: () => boolean;
99
+ };
100
+ /**
101
+ * Build a `shouldDisableSpinner` predicate. Disables on:
102
+ * - env var equals `'false'`
103
+ * - `isCI()`
104
+ * - any of `--json`, `--ndjson`, `--no-spinner`, `--progress-jsonl`, `--verbose`
105
+ * - `extraTriggers?.()` returning true
106
+ *
107
+ * Pair with `createSpinner({ isDisabled })` from `./spinner.js`.
108
+ *
109
+ * ```ts
110
+ * const store = createGlobalArgsStore()
111
+ *
112
+ * // todoist: env var + canonical flags only.
113
+ * const shouldDisableSpinner = createSpinnerGate({
114
+ * envVar: 'TD_SPINNER',
115
+ * getArgs: store.get,
116
+ * })
117
+ *
118
+ * // twist: also disable when --non-interactive is set.
119
+ * const shouldDisableSpinner = createSpinnerGate({
120
+ * envVar: 'TW_SPINNER',
121
+ * getArgs: store.get,
122
+ * extraTriggers: () => isNonInteractive(),
123
+ * })
124
+ *
125
+ * const { withSpinner } = createSpinner({ isDisabled: shouldDisableSpinner })
126
+ * ```
127
+ */
128
+ export declare function createSpinnerGate(opts: SpinnerGateOptions): () => boolean;
129
+ //# sourceMappingURL=global-args.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"global-args.d.ts","sourceRoot":"","sources":["../src/global-args.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAG/C,MAAM,MAAM,UAAU,GAAG,QAAQ,CAAC,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,QAAQ,CAAC,CAAC,GAAG;IACtE,KAAK,EAAE,OAAO,CAAA;IACd,OAAO,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IAC1B,UAAU,EAAE,OAAO,CAAA;IACnB,SAAS,EAAE,OAAO,CAAA;IAClB,kEAAkE;IAClE,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,KAAK,CAAA;CACvC,CAAA;AAOD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,UAAU,CAiD3D;AAED,MAAM,MAAM,eAAe,CAAC,CAAC,SAAS,UAAU,GAAG,UAAU,IAAI;IAC7D,GAAG,IAAI,CAAC,CAAA;IACR,8DAA8D;IAC9D,KAAK,IAAI,IAAI,CAAA;CAChB,CAAA;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,qBAAqB,IAAI,eAAe,CAAC,UAAU,CAAC,CAAA;AACpE,wBAAgB,qBAAqB,CAAC,CAAC,SAAS,UAAU,EAAE,KAAK,EAAE,MAAM,CAAC,GAAG,eAAe,CAAC,CAAC,CAAC,CAAA;AAkB/F,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAEhE;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,GAAG,SAAS,CAEzE;AAED,MAAM,MAAM,qBAAqB,GAAG;IAChC,sFAAsF;IACtF,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,UAAU,CAAA;CAC5B,CAAA;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,qBAAqB,GAAG,MAAM,OAAO,CAE/E;AAED,MAAM,MAAM,kBAAkB,GAAG;IAC7B,2FAA2F;IAC3F,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,UAAU,CAAA;IACzB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,OAAO,CAAA;CAChC,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,kBAAkB,GAAG,MAAM,OAAO,CAgBzE"}
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Centralized, type-safe parsing of well-known global CLI flags shared
3
+ * across the Doist CLIs.
4
+ *
5
+ * Replaces scattered `process.argv.includes()` checks with a single parse
6
+ * that correctly handles grouped short flags (e.g., `-vq`), repeated flags
7
+ * (e.g., `-vvv`), `--flag=value` forms, and avoids false-positives from
8
+ * option values.
9
+ *
10
+ * The parser is pure — pass an explicit argv for testing, or use
11
+ * `createGlobalArgsStore` for the lazy-cached singleton pattern. The store
12
+ * is generic so per-CLI extensions (e.g. todoist's `--user`/`--raw`,
13
+ * twist's `--non-interactive`) can layer their own fields over `GlobalArgs`.
14
+ */
15
+ import { isCI } from './terminal.js';
16
+ const SHORT_FLAGS = {
17
+ q: 'quiet',
18
+ v: 'verbose',
19
+ };
20
+ /**
21
+ * Parse well-known global flags from `argv`. Pure — pass an explicit array
22
+ * for testing, or omit to read `process.argv.slice(2)`.
23
+ *
24
+ * The parser scans the entire argv: a CLI-specific positional that happens
25
+ * to look like a global short flag (`td comment add 123 -q`) will flip the
26
+ * matching global state. Workaround: use the standard `--` terminator
27
+ * (`td comment add 123 -- -q`) so the parser stops before the positional.
28
+ * The trade-off is intentional — callers run this before Commander has
29
+ * parsed argv, so we can't yet distinguish positionals from option values.
30
+ *
31
+ * `--progress-jsonl` accepts only the bare form (output to stderr) and the
32
+ * `--progress-jsonl=path` form. The space-separated `--progress-jsonl path`
33
+ * form is intentionally unsupported because it silently consumes the next
34
+ * positional argument (e.g., `td task add --progress-jsonl "Buy milk"`
35
+ * would treat `Buy milk` as a file path).
36
+ */
37
+ export function parseGlobalArgs(argv) {
38
+ const args = argv ?? process.argv.slice(2);
39
+ const result = {
40
+ json: false,
41
+ ndjson: false,
42
+ quiet: false,
43
+ verbose: 0,
44
+ accessible: false,
45
+ noSpinner: false,
46
+ progressJsonl: false,
47
+ };
48
+ for (let i = 0; i < args.length; i++) {
49
+ const arg = args[i];
50
+ if (arg === '--')
51
+ break;
52
+ if (arg === '--json') {
53
+ result.json = true;
54
+ }
55
+ else if (arg === '--ndjson') {
56
+ result.ndjson = true;
57
+ }
58
+ else if (arg === '--quiet') {
59
+ result.quiet = true;
60
+ }
61
+ else if (arg === '--verbose') {
62
+ result.verbose = Math.min(result.verbose + 1, 4);
63
+ }
64
+ else if (arg === '--accessible') {
65
+ result.accessible = true;
66
+ }
67
+ else if (arg === '--no-spinner') {
68
+ result.noSpinner = true;
69
+ }
70
+ else if (arg === '--progress-jsonl') {
71
+ result.progressJsonl = true;
72
+ }
73
+ else if (arg.startsWith('--progress-jsonl=')) {
74
+ result.progressJsonl = arg.slice('--progress-jsonl='.length);
75
+ }
76
+ else if (arg.length > 1 && arg[0] === '-' && arg[1] !== '-') {
77
+ // Short-flag group: -v, -vq, -vvv, etc. Unknown shorts are
78
+ // silently ignored — they belong to Commander or subcommands.
79
+ for (let j = 1; j < arg.length; j++) {
80
+ const mapped = SHORT_FLAGS[arg[j]];
81
+ if (mapped === 'verbose') {
82
+ result.verbose = Math.min(result.verbose + 1, 4);
83
+ }
84
+ else if (mapped === 'quiet') {
85
+ result.quiet = true;
86
+ }
87
+ }
88
+ }
89
+ }
90
+ return result;
91
+ }
92
+ export function createGlobalArgsStore(parse) {
93
+ // Overloads ensure callers passing a custom `T` must supply a matching
94
+ // parser; the implementation default only kicks in for the no-arg
95
+ // canonical case where `T` collapses to `GlobalArgs`.
96
+ const parser = parse ?? parseGlobalArgs;
97
+ let cached = null;
98
+ return {
99
+ get() {
100
+ if (cached === null)
101
+ cached = parser();
102
+ return cached;
103
+ },
104
+ reset() {
105
+ cached = null;
106
+ },
107
+ };
108
+ }
109
+ export function isProgressJsonlEnabled(args) {
110
+ return args.progressJsonl !== false;
111
+ }
112
+ export function getProgressJsonlPath(args) {
113
+ return typeof args.progressJsonl === 'string' ? args.progressJsonl : undefined;
114
+ }
115
+ /**
116
+ * Build an `isAccessible` predicate that combines the `--accessible` flag
117
+ * with a CLI-specific opt-in env var (e.g. `TD_ACCESSIBLE=1`).
118
+ *
119
+ * ```ts
120
+ * const store = createGlobalArgsStore()
121
+ * export const isAccessible = createAccessibleGate({
122
+ * envVar: 'TD_ACCESSIBLE',
123
+ * getArgs: store.get,
124
+ * })
125
+ * ```
126
+ */
127
+ export function createAccessibleGate(opts) {
128
+ return () => process.env[opts.envVar] === '1' || opts.getArgs().accessible;
129
+ }
130
+ /**
131
+ * Build a `shouldDisableSpinner` predicate. Disables on:
132
+ * - env var equals `'false'`
133
+ * - `isCI()`
134
+ * - any of `--json`, `--ndjson`, `--no-spinner`, `--progress-jsonl`, `--verbose`
135
+ * - `extraTriggers?.()` returning true
136
+ *
137
+ * Pair with `createSpinner({ isDisabled })` from `./spinner.js`.
138
+ *
139
+ * ```ts
140
+ * const store = createGlobalArgsStore()
141
+ *
142
+ * // todoist: env var + canonical flags only.
143
+ * const shouldDisableSpinner = createSpinnerGate({
144
+ * envVar: 'TD_SPINNER',
145
+ * getArgs: store.get,
146
+ * })
147
+ *
148
+ * // twist: also disable when --non-interactive is set.
149
+ * const shouldDisableSpinner = createSpinnerGate({
150
+ * envVar: 'TW_SPINNER',
151
+ * getArgs: store.get,
152
+ * extraTriggers: () => isNonInteractive(),
153
+ * })
154
+ *
155
+ * const { withSpinner } = createSpinner({ isDisabled: shouldDisableSpinner })
156
+ * ```
157
+ */
158
+ export function createSpinnerGate(opts) {
159
+ return () => {
160
+ if (process.env[opts.envVar] === 'false')
161
+ return true;
162
+ if (isCI())
163
+ return true;
164
+ const args = opts.getArgs();
165
+ if (args.json ||
166
+ args.ndjson ||
167
+ args.noSpinner ||
168
+ isProgressJsonlEnabled(args) ||
169
+ args.verbose > 0) {
170
+ return true;
171
+ }
172
+ return opts.extraTriggers?.() ?? false;
173
+ };
174
+ }
175
+ //# sourceMappingURL=global-args.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"global-args.js","sourceRoot":"","sources":["../src/global-args.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,8 +1,12 @@
1
1
  export { BROKEN_CONFIG_STATE_TO_CODE, getConfigPath, readConfig, readConfigStrict, updateConfig, writeConfig, } from './config.js';
2
2
  export type { ConfigErrorCode, ReadConfigStrictResult, WriteConfigOptions } from './config.js';
3
+ export { printEmpty } from './empty.js';
3
4
  export { CliError } from './errors.js';
4
5
  export type { CliErrorCode, CliErrorOptions, ErrorType } from './errors.js';
6
+ export { createAccessibleGate, createGlobalArgsStore, createSpinnerGate, getProgressJsonlPath, isProgressJsonlEnabled, parseGlobalArgs, } from './global-args.js';
7
+ export type { AccessibleGateOptions, GlobalArgs, GlobalArgsStore, SpinnerGateOptions, } from './global-args.js';
5
8
  export { formatJson, formatNdjson } from './json.js';
9
+ export type { ViewOptions } from './options.js';
6
10
  export { createSpinner } from './spinner.js';
7
11
  export type { LoadingSpinner, SpinnerColor, SpinnerConfig, SpinnerKit, SpinnerOptions, } from './spinner.js';
8
12
  export { isCI, isStderrTTY, isStdinTTY, isStdoutTTY } from './terminal.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,2BAA2B,EAC3B,aAAa,EACb,UAAU,EACV,gBAAgB,EAChB,YAAY,EACZ,WAAW,GACd,MAAM,aAAa,CAAA;AACpB,YAAY,EAAE,eAAe,EAAE,sBAAsB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAC9F,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AACtC,YAAY,EAAE,YAAY,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAC3E,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAC5C,YAAY,EACR,cAAc,EACd,YAAY,EACZ,aAAa,EACb,UAAU,EACV,cAAc,GACjB,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,2BAA2B,EAC3B,aAAa,EACb,UAAU,EACV,gBAAgB,EAChB,YAAY,EACZ,WAAW,GACd,MAAM,aAAa,CAAA;AACpB,YAAY,EAAE,eAAe,EAAE,sBAAsB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAC9F,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AACtC,YAAY,EAAE,YAAY,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAC3E,OAAO,EACH,oBAAoB,EACpB,qBAAqB,EACrB,iBAAiB,EACjB,oBAAoB,EACpB,sBAAsB,EACtB,eAAe,GAClB,MAAM,kBAAkB,CAAA;AACzB,YAAY,EACR,qBAAqB,EACrB,UAAU,EACV,eAAe,EACf,kBAAkB,GACrB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AACpD,YAAY,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC/C,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAC5C,YAAY,EACR,cAAc,EACd,YAAY,EACZ,aAAa,EACb,UAAU,EACV,cAAc,GACjB,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA"}
package/dist/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  export { BROKEN_CONFIG_STATE_TO_CODE, getConfigPath, readConfig, readConfigStrict, updateConfig, writeConfig, } from './config.js';
2
+ export { printEmpty } from './empty.js';
2
3
  export { CliError } from './errors.js';
4
+ export { createAccessibleGate, createGlobalArgsStore, createSpinnerGate, getProgressJsonlPath, isProgressJsonlEnabled, parseGlobalArgs, } from './global-args.js';
3
5
  export { formatJson, formatNdjson } from './json.js';
4
6
  export { createSpinner } from './spinner.js';
5
7
  export { isCI, isStderrTTY, isStdinTTY, isStdoutTTY } from './terminal.js';
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,2BAA2B,EAC3B,aAAa,EACb,UAAU,EACV,gBAAgB,EAChB,YAAY,EACZ,WAAW,GACd,MAAM,aAAa,CAAA;AAEpB,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAEtC,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAQ5C,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,2BAA2B,EAC3B,aAAa,EACb,UAAU,EACV,gBAAgB,EAChB,YAAY,EACZ,WAAW,GACd,MAAM,aAAa,CAAA;AAEpB,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAEtC,OAAO,EACH,oBAAoB,EACpB,qBAAqB,EACrB,iBAAiB,EACjB,oBAAoB,EACpB,sBAAsB,EACtB,eAAe,GAClB,MAAM,kBAAkB,CAAA;AAOzB,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AAEpD,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAQ5C,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Shared shape for commands that respect the canonical machine-output flags.
3
+ * Seeded narrow so the type only declares what cli-core helpers actually read
4
+ * today; will grow (`full?`, `raw?`, etc.) as the global-args parser extraction
5
+ * lands (see EXTRACTION_ROADMAP.md, Tier 1).
6
+ *
7
+ * Per-CLI `ViewOptions` types should extend this rather than re-declare the
8
+ * `json` / `ndjson` fields.
9
+ */
10
+ export type ViewOptions = {
11
+ json?: boolean;
12
+ ndjson?: boolean;
13
+ };
14
+ //# sourceMappingURL=options.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,MAAM,WAAW,GAAG;IACtB,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,MAAM,CAAC,EAAE,OAAO,CAAA;CACnB,CAAA"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=options.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"options.js","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":""}
@@ -0,0 +1,21 @@
1
+ type EmptyOutputConfig = {
2
+ setup: () => void | Promise<void>;
3
+ run: (extraArgs: string[]) => Promise<void>;
4
+ humanMessage: string | RegExp;
5
+ };
6
+ /**
7
+ * Asserts the standard `printEmpty` contract for a command:
8
+ * --json → writes exactly `'[]\n'` to stdout
9
+ * --ndjson → writes nothing to stdout (no stray newline)
10
+ * neither → writes exactly the human message + `\n` to stdout
11
+ *
12
+ * Captures bytes from both `console.log` (which vitest intercepts before
13
+ * it reaches the real stream) and `process.stdout.write`, so commands
14
+ * using either pathway satisfy the contract.
15
+ *
16
+ * Spies are installed AFTER `setup` so any `vi.clearAllMocks()` inside
17
+ * `setup` doesn't clobber them.
18
+ */
19
+ export declare function describeEmptyMachineOutput(label: string, config: EmptyOutputConfig): void;
20
+ export {};
21
+ //# sourceMappingURL=testing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAEA,KAAK,iBAAiB,GAAG;IACrB,KAAK,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACjC,GAAG,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IAC3C,YAAY,EAAE,MAAM,GAAG,MAAM,CAAA;CAChC,CAAA;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,GAAG,IAAI,CA8CzF"}
@@ -0,0 +1,56 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ /**
3
+ * Asserts the standard `printEmpty` contract for a command:
4
+ * --json → writes exactly `'[]\n'` to stdout
5
+ * --ndjson → writes nothing to stdout (no stray newline)
6
+ * neither → writes exactly the human message + `\n` to stdout
7
+ *
8
+ * Captures bytes from both `console.log` (which vitest intercepts before
9
+ * it reaches the real stream) and `process.stdout.write`, so commands
10
+ * using either pathway satisfy the contract.
11
+ *
12
+ * Spies are installed AFTER `setup` so any `vi.clearAllMocks()` inside
13
+ * `setup` doesn't clobber them.
14
+ */
15
+ export function describeEmptyMachineOutput(label, config) {
16
+ describe(label, () => {
17
+ let captured = '';
18
+ let consoleSpy;
19
+ let writeSpy;
20
+ beforeEach(async () => {
21
+ await config.setup();
22
+ captured = '';
23
+ consoleSpy = vi.spyOn(console, 'log').mockImplementation((...args) => {
24
+ captured += `${args.map((a) => (typeof a === 'string' ? a : String(a))).join(' ')}\n`;
25
+ });
26
+ writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(((chunk) => {
27
+ captured += typeof chunk === 'string' ? chunk : chunk.toString();
28
+ return true;
29
+ }));
30
+ });
31
+ afterEach(() => {
32
+ consoleSpy?.mockRestore();
33
+ writeSpy?.mockRestore();
34
+ consoleSpy = undefined;
35
+ writeSpy = undefined;
36
+ });
37
+ it('writes exactly "[]\\n" to stdout for --json', async () => {
38
+ await config.run(['--json']);
39
+ expect(captured).toBe('[]\n');
40
+ });
41
+ it('writes nothing to stdout for --ndjson (no stray newline)', async () => {
42
+ await config.run(['--ndjson']);
43
+ expect(captured).toBe('');
44
+ });
45
+ it('writes exactly the human message to stdout when no machine flag is set', async () => {
46
+ await config.run([]);
47
+ if (typeof config.humanMessage === 'string') {
48
+ expect(captured).toBe(`${config.humanMessage}\n`);
49
+ }
50
+ else {
51
+ expect(captured).toMatch(config.humanMessage);
52
+ }
53
+ });
54
+ });
55
+ }
56
+ //# sourceMappingURL=testing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testing.js","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAA;AAQxE;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,0BAA0B,CAAC,KAAa,EAAE,MAAyB;IAC/E,QAAQ,CAAC,KAAK,EAAE,GAAG,EAAE;QACjB,IAAI,QAAQ,GAAG,EAAE,CAAA;QACjB,IAAI,UAAmD,CAAA;QACvD,IAAI,QAAiD,CAAA;QAErD,UAAU,CAAC,KAAK,IAAI,EAAE;YAClB,MAAM,MAAM,CAAC,KAAK,EAAE,CAAA;YACpB,QAAQ,GAAG,EAAE,CAAA;YACb,UAAU,GAAG,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,kBAAkB,CAAC,CAAC,GAAG,IAAe,EAAE,EAAE;gBAC5E,QAAQ,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAA;YACzF,CAAC,CAAC,CAAA;YACF,QAAQ,GAAG,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAC7D,KAA0B,EACnB,EAAE;gBACT,QAAQ,IAAI,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAA;gBAChE,OAAO,IAAI,CAAA;YACf,CAAC,CAAgC,CAAC,CAAA;QACtC,CAAC,CAAC,CAAA;QAEF,SAAS,CAAC,GAAG,EAAE;YACX,UAAU,EAAE,WAAW,EAAE,CAAA;YACzB,QAAQ,EAAE,WAAW,EAAE,CAAA;YACvB,UAAU,GAAG,SAAS,CAAA;YACtB,QAAQ,GAAG,SAAS,CAAA;QACxB,CAAC,CAAC,CAAA;QAEF,EAAE,CAAC,6CAA6C,EAAE,KAAK,IAAI,EAAE;YACzD,MAAM,MAAM,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAA;YAC5B,MAAM,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;QACjC,CAAC,CAAC,CAAA;QAEF,EAAE,CAAC,0DAA0D,EAAE,KAAK,IAAI,EAAE;YACtE,MAAM,MAAM,CAAC,GAAG,CAAC,CAAC,UAAU,CAAC,CAAC,CAAA;YAC9B,MAAM,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QAC7B,CAAC,CAAC,CAAA;QAEF,EAAE,CAAC,wEAAwE,EAAE,KAAK,IAAI,EAAE;YACpF,MAAM,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;YACpB,IAAI,OAAO,MAAM,CAAC,YAAY,KAAK,QAAQ,EAAE,CAAC;gBAC1C,MAAM,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,YAAY,IAAI,CAAC,CAAA;YACrD,CAAC;iBAAM,CAAC;gBACJ,MAAM,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,YAAY,CAAC,CAAA;YACjD,CAAC;QACL,CAAC,CAAC,CAAA;IACN,CAAC,CAAC,CAAA;AACN,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doist/cli-core",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Shared core utilities for Doist CLI projects",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -9,6 +9,10 @@
9
9
  ".": {
10
10
  "types": "./dist/index.d.ts",
11
11
  "import": "./dist/index.js"
12
+ },
13
+ "./testing": {
14
+ "types": "./dist/testing.d.ts",
15
+ "import": "./dist/testing.js"
12
16
  }
13
17
  },
14
18
  "scripts": {
@@ -62,5 +66,13 @@
62
66
  "dependencies": {
63
67
  "chalk": "5.6.2",
64
68
  "yocto-spinner": "1.1.0"
69
+ },
70
+ "peerDependencies": {
71
+ "vitest": ">=4.1"
72
+ },
73
+ "peerDependenciesMeta": {
74
+ "vitest": {
75
+ "optional": true
76
+ }
65
77
  }
66
78
  }