@doist/cli-core 0.0.1 → 0.2.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 ADDED
@@ -0,0 +1,12 @@
1
+ ## [0.2.0](https://github.com/Doist/cli-core/compare/v0.1.0...v0.2.0) (2026-05-06)
2
+
3
+ ### Features
4
+
5
+ * bake cli-core codes into CliError, add CliErrorCode aggregator ([#4](https://github.com/Doist/cli-core/issues/4)) ([8c0d959](https://github.com/Doist/cli-core/commit/8c0d95987db3b35b1715cd72eb603cbb99420211))
6
+
7
+ ## [0.1.0](https://github.com/Doist/cli-core/compare/v0.0.1...v0.1.0) (2026-05-06)
8
+
9
+ ### Features
10
+
11
+ - add CliError + config file I/O helpers ([#1](https://github.com/Doist/cli-core/issues/1)) ([8daf2d1](https://github.com/Doist/cli-core/commit/8daf2d1f67f44f91713ffc2192b681704fa86d88))
12
+ - add terminal detection + JSON/NDJSON formatters ([#2](https://github.com/Doist/cli-core/issues/2)) ([f2bfde8](https://github.com/Doist/cli-core/commit/f2bfde8ff3a2a41eb402acc49223980c5d4be393))
package/LICENSE ADDED
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Doist
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md CHANGED
@@ -1,45 +1,27 @@
1
1
  # @doist/cli-core
2
2
 
3
- ## ⚠️ IMPORTANT NOTICE ⚠️
3
+ Shared core utilities for Doist CLI projects ([todoist-cli](https://github.com/Doist/todoist-cli), [twist-cli](https://github.com/Doist/twist-cli), [outline-cli](https://github.com/Doist/outline-cli)).
4
4
 
5
- **This package is created solely for the purpose of setting up OIDC (OpenID Connect) trusted publishing with npm.**
5
+ > **Status:** scaffolding only. No exports yet extraction work is in progress.
6
6
 
7
- This is **NOT** a functional package and contains **NO** code or functionality beyond the OIDC setup configuration.
7
+ ## Install
8
8
 
9
- ## Purpose
9
+ ```bash
10
+ npm install @doist/cli-core
11
+ ```
10
12
 
11
- This package exists to:
12
- 1. Configure OIDC trusted publishing for the package name `@doist/cli-core`
13
- 2. Enable secure, token-less publishing from CI/CD workflows
14
- 3. Establish provenance for packages published under this name
13
+ ## Development
15
14
 
16
- ## What is OIDC Trusted Publishing?
15
+ ```bash
16
+ npm install
17
+ npm run build
18
+ npm test
19
+ npm run check # lint + format
20
+ npm run fix # auto-fix lint + format
21
+ ```
17
22
 
18
- OIDC trusted publishing allows package maintainers to publish packages directly from their CI/CD workflows without needing to manage npm access tokens. Instead, it uses OpenID Connect to establish trust between the CI/CD provider (like GitHub Actions) and npm.
23
+ Requires Node `>=20.18.1`.
19
24
 
20
- ## Setup Instructions
25
+ ## License
21
26
 
22
- To properly configure OIDC trusted publishing for this package:
23
-
24
- 1. Go to [npmjs.com](https://www.npmjs.com/) and navigate to your package settings
25
- 2. Configure the trusted publisher (e.g., GitHub Actions)
26
- 3. Specify the repository and workflow that should be allowed to publish
27
- 4. Use the configured workflow to publish your actual package
28
-
29
- ## DO NOT USE THIS PACKAGE
30
-
31
- This package is a placeholder for OIDC configuration only. It:
32
- - Contains no executable code
33
- - Provides no functionality
34
- - Should not be installed as a dependency
35
- - Exists only for administrative purposes
36
-
37
- ## More Information
38
-
39
- For more details about npm's trusted publishing feature, see:
40
- - [npm Trusted Publishing Documentation](https://docs.npmjs.com/generating-provenance-statements)
41
- - [GitHub Actions OIDC Documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
42
-
43
- ---
44
-
45
- **Maintained for OIDC setup purposes only**
27
+ MIT
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Resolve the canonical config path for a CLI, honouring `XDG_CONFIG_HOME`
3
+ * when set: `${XDG_CONFIG_HOME ?? ~/.config}/<appName>/config.json`.
4
+ */
5
+ export declare function getConfigPath(appName: string): string;
6
+ /**
7
+ * Read and parse a JSON config file leniently. Returns `{}` when the file is
8
+ * missing, unreadable, invalid JSON, or not a JSON object — the shape runtime
9
+ * code paths expect ("no config" looks the same as "empty config").
10
+ *
11
+ * The return type is `Partial<T>` because at runtime any field may be absent;
12
+ * the cast is the consumer's responsibility once they have validated.
13
+ *
14
+ * Use `readConfigStrict` instead when you need to distinguish failure modes
15
+ * (e.g. `doctor`-style inspection commands).
16
+ */
17
+ export declare function readConfig<T extends object>(path: string): Promise<Partial<T>>;
18
+ export type ReadConfigStrictResult = {
19
+ state: 'missing';
20
+ } | {
21
+ state: 'read-failed';
22
+ error: Error;
23
+ } | {
24
+ state: 'invalid-json';
25
+ error: Error;
26
+ } | {
27
+ state: 'invalid-shape';
28
+ actual: 'array' | 'null' | 'number' | 'string' | 'boolean';
29
+ } | {
30
+ state: 'present';
31
+ config: Record<string, unknown>;
32
+ };
33
+ /**
34
+ * Canonical CliError codes for the broken states of `readConfigStrict`. The
35
+ * `satisfies` clause guarantees every failure state has a corresponding code.
36
+ *
37
+ * Exported as both a runtime map (so a future `readConfigOrThrow` helper — and
38
+ * consumers writing their own state-to-throw translation — can look codes up
39
+ * instead of hand-writing strings) and as the `ConfigErrorCode` type alias.
40
+ */
41
+ export declare const BROKEN_CONFIG_STATE_TO_CODE: {
42
+ readonly 'read-failed': "CONFIG_READ_FAILED";
43
+ readonly 'invalid-json': "CONFIG_INVALID_JSON";
44
+ readonly 'invalid-shape': "CONFIG_INVALID_SHAPE";
45
+ };
46
+ /**
47
+ * Canonical CliError codes emitted when `readConfigStrict` reports a broken
48
+ * config file. Derived from `BROKEN_CONFIG_STATE_TO_CODE` so the type and the
49
+ * runtime map can never drift.
50
+ *
51
+ * cli-core does not throw these itself (the library returns a discriminated
52
+ * result so consumers control formatting), but every consumer that does the
53
+ * states-to-throw translation ends up using the same three codes. Including
54
+ * this in each CLI's `ErrorCode` union is also unnecessary now that
55
+ * `CliError`'s constructor accepts the cli-core canonical codes directly.
56
+ */
57
+ export type ConfigErrorCode = (typeof BROKEN_CONFIG_STATE_TO_CODE)[keyof typeof BROKEN_CONFIG_STATE_TO_CODE];
58
+ /**
59
+ * Read and parse a JSON config file strictly, distinguishing missing files
60
+ * from broken ones. The library returns a discriminated result instead of
61
+ * throwing so consumers can format errors with their own copy/codes.
62
+ *
63
+ * `present.config` is typed as `Record<string, unknown>` because cli-core does
64
+ * not validate shape — only that the file parsed to a plain object. Cast or
65
+ * decode in the consumer.
66
+ */
67
+ export declare function readConfigStrict(path: string): Promise<ReadConfigStrictResult>;
68
+ export interface WriteConfigOptions {
69
+ /**
70
+ * When true and the supplied config has no own enumerable keys, delete the
71
+ * file instead of writing an empty `{}`. Default false.
72
+ */
73
+ deleteWhenEmpty?: boolean;
74
+ }
75
+ /**
76
+ * Write a config file with restrictive permissions (parent dir 0700, file 0600)
77
+ * and a trailing newline. Creates parent directories as needed and tightens
78
+ * their permissions even if they already existed.
79
+ */
80
+ export declare function writeConfig<T extends object>(path: string, config: T, options?: WriteConfigOptions): Promise<void>;
81
+ /**
82
+ * Read the existing config strictly, shallow-merge the supplied updates, and
83
+ * write the result. Throws if the existing file is unreadable or unparseable
84
+ * to avoid silently overwriting data that a user might still recover by
85
+ * fixing their config file.
86
+ */
87
+ export declare function updateConfig<T extends object>(path: string, updates: Partial<T>, options?: WriteConfigOptions): Promise<void>;
88
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAKA;;;GAGG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAGrD;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,UAAU,CAAC,CAAC,SAAS,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAGpF;AAED,MAAM,MAAM,sBAAsB,GAC5B;IAAE,KAAK,EAAE,SAAS,CAAA;CAAE,GACpB;IAAE,KAAK,EAAE,aAAa,CAAC;IAAC,KAAK,EAAE,KAAK,CAAA;CAAE,GACtC;IAAE,KAAK,EAAE,cAAc,CAAC;IAAC,KAAK,EAAE,KAAK,CAAA;CAAE,GACvC;IAAE,KAAK,EAAE,eAAe,CAAC;IAAC,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAA;CAAE,GACtF;IAAE,KAAK,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAA;AAU3D;;;;;;;GAOG;AACH,eAAO,MAAM,2BAA2B;;;;CAIc,CAAA;AAEtD;;;;;;;;;;GAUG;AACH,MAAM,MAAM,eAAe,GACvB,CAAC,OAAO,2BAA2B,CAAC,CAAC,MAAM,OAAO,2BAA2B,CAAC,CAAA;AAElF;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAqBpF;AAED,MAAM,WAAW,kBAAkB;IAC/B;;;OAGG;IACH,eAAe,CAAC,EAAE,OAAO,CAAA;CAC5B;AAED;;;;GAIG;AACH,wBAAsB,WAAW,CAAC,CAAC,SAAS,MAAM,EAC9C,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,CAAC,EACT,OAAO,GAAE,kBAAuB,GACjC,OAAO,CAAC,IAAI,CAAC,CAkBf;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,CAAC,SAAS,MAAM,EAC/C,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EACnB,OAAO,GAAE,kBAAuB,GACjC,OAAO,CAAC,IAAI,CAAC,CAoBf"}
package/dist/config.js ADDED
@@ -0,0 +1,139 @@
1
+ import { chmod, mkdir, readFile, unlink, writeFile } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import { dirname, join } from 'node:path';
4
+ import { formatJson } from './json.js';
5
+ /**
6
+ * Resolve the canonical config path for a CLI, honouring `XDG_CONFIG_HOME`
7
+ * when set: `${XDG_CONFIG_HOME ?? ~/.config}/<appName>/config.json`.
8
+ */
9
+ export function getConfigPath(appName) {
10
+ const base = process.env.XDG_CONFIG_HOME || join(homedir(), '.config');
11
+ return join(base, appName, 'config.json');
12
+ }
13
+ /**
14
+ * Read and parse a JSON config file leniently. Returns `{}` when the file is
15
+ * missing, unreadable, invalid JSON, or not a JSON object — the shape runtime
16
+ * code paths expect ("no config" looks the same as "empty config").
17
+ *
18
+ * The return type is `Partial<T>` because at runtime any field may be absent;
19
+ * the cast is the consumer's responsibility once they have validated.
20
+ *
21
+ * Use `readConfigStrict` instead when you need to distinguish failure modes
22
+ * (e.g. `doctor`-style inspection commands).
23
+ */
24
+ export async function readConfig(path) {
25
+ const result = await readConfigStrict(path);
26
+ return result.state === 'present' ? result.config : {};
27
+ }
28
+ /**
29
+ * Canonical CliError codes for the broken states of `readConfigStrict`. The
30
+ * `satisfies` clause guarantees every failure state has a corresponding code.
31
+ *
32
+ * Exported as both a runtime map (so a future `readConfigOrThrow` helper — and
33
+ * consumers writing their own state-to-throw translation — can look codes up
34
+ * instead of hand-writing strings) and as the `ConfigErrorCode` type alias.
35
+ */
36
+ export const BROKEN_CONFIG_STATE_TO_CODE = {
37
+ 'read-failed': 'CONFIG_READ_FAILED',
38
+ 'invalid-json': 'CONFIG_INVALID_JSON',
39
+ 'invalid-shape': 'CONFIG_INVALID_SHAPE',
40
+ };
41
+ /**
42
+ * Read and parse a JSON config file strictly, distinguishing missing files
43
+ * from broken ones. The library returns a discriminated result instead of
44
+ * throwing so consumers can format errors with their own copy/codes.
45
+ *
46
+ * `present.config` is typed as `Record<string, unknown>` because cli-core does
47
+ * not validate shape — only that the file parsed to a plain object. Cast or
48
+ * decode in the consumer.
49
+ */
50
+ export async function readConfigStrict(path) {
51
+ let content;
52
+ try {
53
+ content = await readFile(path, 'utf-8');
54
+ }
55
+ catch (error) {
56
+ if (isMissingFileError(error))
57
+ return { state: 'missing' };
58
+ return { state: 'read-failed', error: toError(error) };
59
+ }
60
+ let parsed;
61
+ try {
62
+ parsed = JSON.parse(content);
63
+ }
64
+ catch (error) {
65
+ return { state: 'invalid-json', error: toError(error) };
66
+ }
67
+ if (!isPlainObject(parsed)) {
68
+ return { state: 'invalid-shape', actual: describeNonObject(parsed) };
69
+ }
70
+ return { state: 'present', config: parsed };
71
+ }
72
+ /**
73
+ * Write a config file with restrictive permissions (parent dir 0700, file 0600)
74
+ * and a trailing newline. Creates parent directories as needed and tightens
75
+ * their permissions even if they already existed.
76
+ */
77
+ export async function writeConfig(path, config, options = {}) {
78
+ if (options.deleteWhenEmpty && Object.keys(config).length === 0) {
79
+ try {
80
+ await unlink(path);
81
+ }
82
+ catch (error) {
83
+ if (!isMissingFileError(error))
84
+ throw error;
85
+ }
86
+ return;
87
+ }
88
+ const dir = dirname(path);
89
+ await mkdir(dir, { recursive: true, mode: 0o700 });
90
+ await chmod(dir, 0o700);
91
+ await writeFile(path, `${formatJson(config)}\n`, {
92
+ encoding: 'utf-8',
93
+ mode: 0o600,
94
+ });
95
+ await chmod(path, 0o600);
96
+ }
97
+ /**
98
+ * Read the existing config strictly, shallow-merge the supplied updates, and
99
+ * write the result. Throws if the existing file is unreadable or unparseable
100
+ * to avoid silently overwriting data that a user might still recover by
101
+ * fixing their config file.
102
+ */
103
+ export async function updateConfig(path, updates, options = {}) {
104
+ const result = await readConfigStrict(path);
105
+ switch (result.state) {
106
+ case 'missing':
107
+ await writeConfig(path, updates, options);
108
+ return;
109
+ case 'present':
110
+ await writeConfig(path, { ...result.config, ...updates }, options);
111
+ return;
112
+ case 'read-failed':
113
+ throw new Error(`Cannot update config at ${path}: ${result.error.message}`);
114
+ case 'invalid-json':
115
+ throw new Error(`Cannot update config at ${path}: file is not valid JSON (${result.error.message}). Fix or remove the file before retrying.`);
116
+ case 'invalid-shape':
117
+ throw new Error(`Cannot update config at ${path}: file contents are ${result.actual}, not a JSON object. Fix or remove the file before retrying.`);
118
+ }
119
+ }
120
+ function isPlainObject(value) {
121
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
122
+ }
123
+ function isMissingFileError(error) {
124
+ return (error instanceof Error && 'code' in error && error.code === 'ENOENT');
125
+ }
126
+ function toError(value) {
127
+ return value instanceof Error ? value : new Error(String(value));
128
+ }
129
+ function describeNonObject(value) {
130
+ if (Array.isArray(value))
131
+ return 'array';
132
+ if (value === null)
133
+ return 'null';
134
+ const t = typeof value;
135
+ if (t === 'number' || t === 'string' || t === 'boolean')
136
+ return t;
137
+ return 'null';
138
+ }
139
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,45 @@
1
+ import type { ConfigErrorCode } from './config.js';
2
+ export type ErrorType = 'error' | 'info';
3
+ export interface CliErrorOptions {
4
+ hints?: string[];
5
+ type?: ErrorType;
6
+ }
7
+ /**
8
+ * Aggregator of every error code that cli-core itself defines. Baked into the
9
+ * `CliError` constructor so consumers don't have to redeclare these strings in
10
+ * their own `ErrorCode` union — they're always accepted.
11
+ *
12
+ * Grows as future modules add their own well-known codes:
13
+ *
14
+ * ```ts
15
+ * export type CliErrorCode = ConfigErrorCode | SpinnerErrorCode | …
16
+ * ```
17
+ */
18
+ export type CliErrorCode = ConfigErrorCode;
19
+ /**
20
+ * Generic CLI error carrying a structured code, optional hints, and a severity
21
+ * type.
22
+ *
23
+ * `code` accepts either the consumer's `TCode` union or any code defined by
24
+ * cli-core itself (`CliErrorCode`). Pass a string-literal union as `TCode` to
25
+ * constrain codes per CLI; the cli-core codes are always allowed alongside.
26
+ *
27
+ * ```ts
28
+ * import { CliError } from '@doist/cli-core'
29
+ * type Code = 'AUTH_FAILED' | 'NOT_FOUND' | (string & {})
30
+ * throw new CliError<Code>('AUTH_FAILED', 'Token rejected', {
31
+ * hints: ['Run td auth login'],
32
+ * })
33
+ * // CONFIG_INVALID_JSON also accepted without listing it in `Code`:
34
+ * throw new CliError<Code>('CONFIG_INVALID_JSON', 'Bad JSON')
35
+ * ```
36
+ *
37
+ * The `(string & {})` trick preserves intellisense while accepting dynamic codes.
38
+ */
39
+ export declare class CliError<TCode extends string = string> extends Error {
40
+ readonly code: TCode | CliErrorCode;
41
+ readonly hints?: string[];
42
+ readonly type: ErrorType;
43
+ constructor(code: TCode | CliErrorCode, message: string, options?: CliErrorOptions);
44
+ }
45
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAElD,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,MAAM,CAAA;AAExC,MAAM,WAAW,eAAe;IAC5B,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;IAChB,IAAI,CAAC,EAAE,SAAS,CAAA;CACnB;AAED;;;;;;;;;;GAUG;AACH,MAAM,MAAM,YAAY,GAAG,eAAe,CAAA;AAE1C;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,QAAQ,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,CAAE,SAAQ,KAAK;IAC9D,QAAQ,CAAC,IAAI,EAAE,KAAK,GAAG,YAAY,CAAA;IACnC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;IACzB,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;gBAEZ,IAAI,EAAE,KAAK,GAAG,YAAY,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB;CAOzF"}
package/dist/errors.js ADDED
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Generic CLI error carrying a structured code, optional hints, and a severity
3
+ * type.
4
+ *
5
+ * `code` accepts either the consumer's `TCode` union or any code defined by
6
+ * cli-core itself (`CliErrorCode`). Pass a string-literal union as `TCode` to
7
+ * constrain codes per CLI; the cli-core codes are always allowed alongside.
8
+ *
9
+ * ```ts
10
+ * import { CliError } from '@doist/cli-core'
11
+ * type Code = 'AUTH_FAILED' | 'NOT_FOUND' | (string & {})
12
+ * throw new CliError<Code>('AUTH_FAILED', 'Token rejected', {
13
+ * hints: ['Run td auth login'],
14
+ * })
15
+ * // CONFIG_INVALID_JSON also accepted without listing it in `Code`:
16
+ * throw new CliError<Code>('CONFIG_INVALID_JSON', 'Bad JSON')
17
+ * ```
18
+ *
19
+ * The `(string & {})` trick preserves intellisense while accepting dynamic codes.
20
+ */
21
+ export class CliError extends Error {
22
+ code;
23
+ hints;
24
+ type;
25
+ constructor(code, message, options = {}) {
26
+ super(message);
27
+ this.name = 'CliError';
28
+ this.code = code;
29
+ this.hints = options.hints;
30
+ this.type = options.type ?? 'error';
31
+ }
32
+ }
33
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAsBA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,OAAO,QAAwC,SAAQ,KAAK;IACrD,IAAI,CAAsB;IAC1B,KAAK,CAAW;IAChB,IAAI,CAAW;IAExB,YAAY,IAA0B,EAAE,OAAe,EAAE,UAA2B,EAAE;QAClF,KAAK,CAAC,OAAO,CAAC,CAAA;QACd,IAAI,CAAC,IAAI,GAAG,UAAU,CAAA;QACtB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;QAChB,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,CAAA;QAC1B,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,OAAO,CAAA;IACvC,CAAC;CACJ"}
@@ -0,0 +1,7 @@
1
+ export { BROKEN_CONFIG_STATE_TO_CODE, getConfigPath, readConfig, readConfigStrict, updateConfig, writeConfig, } from './config.js';
2
+ export type { ConfigErrorCode, ReadConfigStrictResult, WriteConfigOptions } from './config.js';
3
+ export { CliError } from './errors.js';
4
+ export type { CliErrorCode, CliErrorOptions, ErrorType } from './errors.js';
5
+ export { formatJson, formatNdjson } from './json.js';
6
+ export { isCI, isStderrTTY, isStdinTTY, isStdoutTTY } from './terminal.js';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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,IAAI,EAAE,WAAW,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { BROKEN_CONFIG_STATE_TO_CODE, getConfigPath, readConfig, readConfigStrict, updateConfig, writeConfig, } from './config.js';
2
+ export { CliError } from './errors.js';
3
+ export { formatJson, formatNdjson } from './json.js';
4
+ export { isCI, isStderrTTY, isStdinTTY, isStdoutTTY } from './terminal.js';
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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,IAAI,EAAE,WAAW,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA"}
package/dist/json.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Pretty-print a value as JSON with 2-space indentation. Matches the canonical
3
+ * `--json` output style used across the Doist CLIs.
4
+ *
5
+ * Throws if `value` cannot be serialized (top-level `undefined`, a function,
6
+ * a symbol, or an object whose `toJSON()` returns `undefined`) so the
7
+ * `string` return type is honoured.
8
+ */
9
+ export declare function formatJson(value: unknown): string;
10
+ /**
11
+ * Format an array as newline-delimited JSON (NDJSON): one JSON value per line,
12
+ * separated by `\n`, with no trailing newline. Matches the canonical `--ndjson`
13
+ * output style used across the Doist CLIs.
14
+ *
15
+ * Throws if any item cannot be serialized — surfacing the bad index instead of
16
+ * silently emitting blank lines that would corrupt the output stream.
17
+ */
18
+ export declare function formatNdjson(items: readonly unknown[]): string;
19
+ //# sourceMappingURL=json.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json.d.ts","sourceRoot":"","sources":["../src/json.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAQjD;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,SAAS,OAAO,EAAE,GAAG,MAAM,CAY9D"}
package/dist/json.js ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Pretty-print a value as JSON with 2-space indentation. Matches the canonical
3
+ * `--json` output style used across the Doist CLIs.
4
+ *
5
+ * Throws if `value` cannot be serialized (top-level `undefined`, a function,
6
+ * a symbol, or an object whose `toJSON()` returns `undefined`) so the
7
+ * `string` return type is honoured.
8
+ */
9
+ export function formatJson(value) {
10
+ const result = JSON.stringify(value, null, 2);
11
+ if (result === undefined) {
12
+ throw new TypeError('formatJson: value is not JSON-serializable (got undefined, function, or symbol at top level)');
13
+ }
14
+ return result;
15
+ }
16
+ /**
17
+ * Format an array as newline-delimited JSON (NDJSON): one JSON value per line,
18
+ * separated by `\n`, with no trailing newline. Matches the canonical `--ndjson`
19
+ * output style used across the Doist CLIs.
20
+ *
21
+ * Throws if any item cannot be serialized — surfacing the bad index instead of
22
+ * silently emitting blank lines that would corrupt the output stream.
23
+ */
24
+ export function formatNdjson(items) {
25
+ return items
26
+ .map((item, i) => {
27
+ const line = JSON.stringify(item);
28
+ if (line === undefined) {
29
+ throw new TypeError(`formatNdjson: item at index ${i} is not JSON-serializable (got undefined, function, or symbol)`);
30
+ }
31
+ return line;
32
+ })
33
+ .join('\n');
34
+ }
35
+ //# sourceMappingURL=json.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json.js","sourceRoot":"","sources":["../src/json.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,UAAU,UAAU,CAAC,KAAc;IACrC,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA;IAC7C,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACvB,MAAM,IAAI,SAAS,CACf,8FAA8F,CACjG,CAAA;IACL,CAAC;IACD,OAAO,MAAM,CAAA;AACjB,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAAC,KAAyB;IAClD,OAAO,KAAK;SACP,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE;QACb,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAA;QACjC,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACrB,MAAM,IAAI,SAAS,CACf,+BAA+B,CAAC,gEAAgE,CACnG,CAAA;QACL,CAAC;QACD,OAAO,IAAI,CAAA;IACf,CAAC,CAAC;SACD,IAAI,CAAC,IAAI,CAAC,CAAA;AACnB,CAAC"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Terminal-environment detection helpers shared across the Doist CLIs.
3
+ *
4
+ * These primitives read globals (`process.stdout.isTTY`, `process.env.CI`)
5
+ * directly so callers can compose them. CLI-specific fan-out — opt-out env
6
+ * vars like `TD_SPINNER`, `--json` flags, etc. — stays in the consuming CLI;
7
+ * cli-core only owns the platform signals.
8
+ */
9
+ export declare function isStdoutTTY(): boolean;
10
+ export declare function isStdinTTY(): boolean;
11
+ export declare function isStderrTTY(): boolean;
12
+ /**
13
+ * True when the process appears to be running under continuous integration.
14
+ * Checks `process.env.CI`, which every major CI provider (GitHub Actions,
15
+ * GitLab, CircleCI, Buildkite, Travis, …) sets to a truthy value by
16
+ * convention.
17
+ *
18
+ * `CI='false'` is treated as opt-out (handy when a parent environment has
19
+ * `CI=true` set but a nested invocation needs to behave interactively).
20
+ */
21
+ export declare function isCI(): boolean;
22
+ //# sourceMappingURL=terminal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"terminal.d.ts","sourceRoot":"","sources":["../src/terminal.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,wBAAgB,WAAW,IAAI,OAAO,CAErC;AAED,wBAAgB,UAAU,IAAI,OAAO,CAEpC;AAED,wBAAgB,WAAW,IAAI,OAAO,CAErC;AAED;;;;;;;;GAQG;AACH,wBAAgB,IAAI,IAAI,OAAO,CAG9B"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Terminal-environment detection helpers shared across the Doist CLIs.
3
+ *
4
+ * These primitives read globals (`process.stdout.isTTY`, `process.env.CI`)
5
+ * directly so callers can compose them. CLI-specific fan-out — opt-out env
6
+ * vars like `TD_SPINNER`, `--json` flags, etc. — stays in the consuming CLI;
7
+ * cli-core only owns the platform signals.
8
+ */
9
+ export function isStdoutTTY() {
10
+ return Boolean(process.stdout.isTTY);
11
+ }
12
+ export function isStdinTTY() {
13
+ return Boolean(process.stdin.isTTY);
14
+ }
15
+ export function isStderrTTY() {
16
+ return Boolean(process.stderr.isTTY);
17
+ }
18
+ /**
19
+ * True when the process appears to be running under continuous integration.
20
+ * Checks `process.env.CI`, which every major CI provider (GitHub Actions,
21
+ * GitLab, CircleCI, Buildkite, Travis, …) sets to a truthy value by
22
+ * convention.
23
+ *
24
+ * `CI='false'` is treated as opt-out (handy when a parent environment has
25
+ * `CI=true` set but a nested invocation needs to behave interactively).
26
+ */
27
+ export function isCI() {
28
+ const value = process.env.CI;
29
+ return Boolean(value) && value !== 'false';
30
+ }
31
+ //# sourceMappingURL=terminal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"terminal.js","sourceRoot":"","sources":["../src/terminal.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,UAAU,WAAW;IACvB,OAAO,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;AACxC,CAAC;AAED,MAAM,UAAU,UAAU;IACtB,OAAO,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;AACvC,CAAC;AAED,MAAM,UAAU,WAAW;IACvB,OAAO,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;AACxC,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,IAAI;IAChB,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,EAAE,CAAA;IAC5B,OAAO,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,KAAK,OAAO,CAAA;AAC9C,CAAC"}
package/package.json CHANGED
@@ -1,10 +1,62 @@
1
1
  {
2
- "name": "@doist/cli-core",
3
- "version": "0.0.1",
4
- "description": "OIDC trusted publishing setup package for @doist/cli-core",
5
- "keywords": [
6
- "oidc",
7
- "trusted-publishing",
8
- "setup"
9
- ]
2
+ "name": "@doist/cli-core",
3
+ "version": "0.2.0",
4
+ "description": "Shared core utilities for Doist CLI projects",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "scripts": {
15
+ "build": "tsc -p tsconfig.build.json",
16
+ "dev": "tsc -p tsconfig.build.json --watch",
17
+ "type-check": "tsc --noEmit",
18
+ "check": "oxlint src && oxfmt --check",
19
+ "fix": "oxlint src --fix && oxfmt",
20
+ "test": "vitest run --passWithNoTests",
21
+ "test:watch": "vitest",
22
+ "prepublishOnly": "npm run build && npm test"
23
+ },
24
+ "keywords": [
25
+ "cli",
26
+ "doist"
27
+ ],
28
+ "author": "Doist",
29
+ "license": "MIT",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/Doist/cli-core.git"
33
+ },
34
+ "homepage": "https://github.com/Doist/cli-core#readme",
35
+ "bugs": {
36
+ "url": "https://github.com/Doist/cli-core/issues"
37
+ },
38
+ "publishConfig": {
39
+ "access": "public",
40
+ "provenance": true
41
+ },
42
+ "engines": {
43
+ "node": ">=20.18.1"
44
+ },
45
+ "files": [
46
+ "dist",
47
+ "CHANGELOG.md"
48
+ ],
49
+ "devDependencies": {
50
+ "@semantic-release/changelog": "6.0.3",
51
+ "@semantic-release/exec": "7.1.0",
52
+ "@semantic-release/git": "10.0.1",
53
+ "@types/node": "25.6.0",
54
+ "conventional-changelog-conventionalcommits": "9.3.1",
55
+ "lefthook": "2.1.6",
56
+ "oxfmt": "0.46.0",
57
+ "oxlint": "1.61.0",
58
+ "semantic-release": "25.0.3",
59
+ "typescript": "6.0.3",
60
+ "vitest": "4.1.5"
61
+ }
10
62
  }