@doist/cli-core 0.0.1 → 0.1.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,6 @@
1
+ ## [0.1.0](https://github.com/Doist/cli-core/compare/v0.0.1...v0.1.0) (2026-05-06)
2
+
3
+ ### Features
4
+
5
+ * 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))
6
+ * 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,63 @@
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
+ * Read and parse a JSON config file strictly, distinguishing missing files
35
+ * from broken ones. The library returns a discriminated result instead of
36
+ * throwing so consumers can format errors with their own copy/codes.
37
+ *
38
+ * `present.config` is typed as `Record<string, unknown>` because cli-core does
39
+ * not validate shape — only that the file parsed to a plain object. Cast or
40
+ * decode in the consumer.
41
+ */
42
+ export declare function readConfigStrict(path: string): Promise<ReadConfigStrictResult>;
43
+ export interface WriteConfigOptions {
44
+ /**
45
+ * When true and the supplied config has no own enumerable keys, delete the
46
+ * file instead of writing an empty `{}`. Default false.
47
+ */
48
+ deleteWhenEmpty?: boolean;
49
+ }
50
+ /**
51
+ * Write a config file with restrictive permissions (parent dir 0700, file 0600)
52
+ * and a trailing newline. Creates parent directories as needed and tightens
53
+ * their permissions even if they already existed.
54
+ */
55
+ export declare function writeConfig<T extends object>(path: string, config: T, options?: WriteConfigOptions): Promise<void>;
56
+ /**
57
+ * Read the existing config strictly, shallow-merge the supplied updates, and
58
+ * write the result. Throws if the existing file is unreadable or unparseable
59
+ * to avoid silently overwriting data that a user might still recover by
60
+ * fixing their config file.
61
+ */
62
+ export declare function updateConfig<T extends object>(path: string, updates: Partial<T>, options?: WriteConfigOptions): Promise<void>;
63
+ //# 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;AAE3D;;;;;;;;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,126 @@
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
+ * Read and parse a JSON config file strictly, distinguishing missing files
30
+ * from broken ones. The library returns a discriminated result instead of
31
+ * throwing so consumers can format errors with their own copy/codes.
32
+ *
33
+ * `present.config` is typed as `Record<string, unknown>` because cli-core does
34
+ * not validate shape — only that the file parsed to a plain object. Cast or
35
+ * decode in the consumer.
36
+ */
37
+ export async function readConfigStrict(path) {
38
+ let content;
39
+ try {
40
+ content = await readFile(path, 'utf-8');
41
+ }
42
+ catch (error) {
43
+ if (isMissingFileError(error))
44
+ return { state: 'missing' };
45
+ return { state: 'read-failed', error: toError(error) };
46
+ }
47
+ let parsed;
48
+ try {
49
+ parsed = JSON.parse(content);
50
+ }
51
+ catch (error) {
52
+ return { state: 'invalid-json', error: toError(error) };
53
+ }
54
+ if (!isPlainObject(parsed)) {
55
+ return { state: 'invalid-shape', actual: describeNonObject(parsed) };
56
+ }
57
+ return { state: 'present', config: parsed };
58
+ }
59
+ /**
60
+ * Write a config file with restrictive permissions (parent dir 0700, file 0600)
61
+ * and a trailing newline. Creates parent directories as needed and tightens
62
+ * their permissions even if they already existed.
63
+ */
64
+ export async function writeConfig(path, config, options = {}) {
65
+ if (options.deleteWhenEmpty && Object.keys(config).length === 0) {
66
+ try {
67
+ await unlink(path);
68
+ }
69
+ catch (error) {
70
+ if (!isMissingFileError(error))
71
+ throw error;
72
+ }
73
+ return;
74
+ }
75
+ const dir = dirname(path);
76
+ await mkdir(dir, { recursive: true, mode: 0o700 });
77
+ await chmod(dir, 0o700);
78
+ await writeFile(path, `${formatJson(config)}\n`, {
79
+ encoding: 'utf-8',
80
+ mode: 0o600,
81
+ });
82
+ await chmod(path, 0o600);
83
+ }
84
+ /**
85
+ * Read the existing config strictly, shallow-merge the supplied updates, and
86
+ * write the result. Throws if the existing file is unreadable or unparseable
87
+ * to avoid silently overwriting data that a user might still recover by
88
+ * fixing their config file.
89
+ */
90
+ export async function updateConfig(path, updates, options = {}) {
91
+ const result = await readConfigStrict(path);
92
+ switch (result.state) {
93
+ case 'missing':
94
+ await writeConfig(path, updates, options);
95
+ return;
96
+ case 'present':
97
+ await writeConfig(path, { ...result.config, ...updates }, options);
98
+ return;
99
+ case 'read-failed':
100
+ throw new Error(`Cannot update config at ${path}: ${result.error.message}`);
101
+ case 'invalid-json':
102
+ throw new Error(`Cannot update config at ${path}: file is not valid JSON (${result.error.message}). Fix or remove the file before retrying.`);
103
+ case 'invalid-shape':
104
+ throw new Error(`Cannot update config at ${path}: file contents are ${result.actual}, not a JSON object. Fix or remove the file before retrying.`);
105
+ }
106
+ }
107
+ function isPlainObject(value) {
108
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
109
+ }
110
+ function isMissingFileError(error) {
111
+ return (error instanceof Error && 'code' in error && error.code === 'ENOENT');
112
+ }
113
+ function toError(value) {
114
+ return value instanceof Error ? value : new Error(String(value));
115
+ }
116
+ function describeNonObject(value) {
117
+ if (Array.isArray(value))
118
+ return 'array';
119
+ if (value === null)
120
+ return 'null';
121
+ const t = typeof value;
122
+ if (t === 'number' || t === 'string' || t === 'boolean')
123
+ return t;
124
+ return 'null';
125
+ }
126
+ //# 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,27 @@
1
+ export type ErrorType = 'error' | 'info';
2
+ export interface CliErrorOptions {
3
+ hints?: string[];
4
+ type?: ErrorType;
5
+ }
6
+ /**
7
+ * Generic CLI error carrying a structured code, optional hints, and a severity type.
8
+ *
9
+ * Pass a string-literal union as `TCode` to constrain codes per CLI:
10
+ *
11
+ * ```ts
12
+ * import { CliError } from '@doist/cli-core'
13
+ * type Code = 'AUTH_FAILED' | 'NOT_FOUND' | (string & {})
14
+ * throw new CliError<Code>('AUTH_FAILED', 'Token rejected', {
15
+ * hints: ['Run td auth login'],
16
+ * })
17
+ * ```
18
+ *
19
+ * The `(string & {})` trick preserves intellisense while accepting dynamic codes.
20
+ */
21
+ export declare class CliError<TCode extends string = string> extends Error {
22
+ readonly code: TCode;
23
+ readonly hints?: string[];
24
+ readonly type: ErrorType;
25
+ constructor(code: TCode, message: string, options?: CliErrorOptions);
26
+ }
27
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,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;;;;;;;;;;;;;;GAcG;AACH,qBAAa,QAAQ,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,CAAE,SAAQ,KAAK;aAK1C,IAAI,EAAE,KAAK;IAJ/B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;IACzB,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;gBAGJ,IAAI,EAAE,KAAK,EAC3B,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,eAAoB;CAOpC"}
package/dist/errors.js ADDED
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Generic CLI error carrying a structured code, optional hints, and a severity type.
3
+ *
4
+ * Pass a string-literal union as `TCode` to constrain codes per CLI:
5
+ *
6
+ * ```ts
7
+ * import { CliError } from '@doist/cli-core'
8
+ * type Code = 'AUTH_FAILED' | 'NOT_FOUND' | (string & {})
9
+ * throw new CliError<Code>('AUTH_FAILED', 'Token rejected', {
10
+ * hints: ['Run td auth login'],
11
+ * })
12
+ * ```
13
+ *
14
+ * The `(string & {})` trick preserves intellisense while accepting dynamic codes.
15
+ */
16
+ export class CliError extends Error {
17
+ code;
18
+ hints;
19
+ type;
20
+ constructor(code, message, options = {}) {
21
+ super(message);
22
+ this.code = code;
23
+ this.name = 'CliError';
24
+ this.hints = options.hints;
25
+ this.type = options.type ?? 'error';
26
+ }
27
+ }
28
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAOA;;;;;;;;;;;;;;GAcG;AACH,MAAM,OAAO,QAAwC,SAAQ,KAAK;IAK1C;IAJX,KAAK,CAAW;IAChB,IAAI,CAAW;IAExB,YACoB,IAAW,EAC3B,OAAe,EACf,UAA2B,EAAE;QAE7B,KAAK,CAAC,OAAO,CAAC,CAAA;QAJE,SAAI,GAAJ,IAAI,CAAO;QAK3B,IAAI,CAAC,IAAI,GAAG,UAAU,CAAA;QACtB,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 { getConfigPath, readConfig, readConfigStrict, updateConfig, writeConfig } from './config.js';
2
+ export type { ReadConfigStrictResult, WriteConfigOptions } from './config.js';
3
+ export { CliError } from './errors.js';
4
+ export type { 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,EAAE,aAAa,EAAE,UAAU,EAAE,gBAAgB,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACpG,YAAY,EAAE,sBAAsB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAC7E,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AACtC,YAAY,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAC7D,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 { 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,EAAE,aAAa,EAAE,UAAU,EAAE,gBAAgB,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAEpG,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.1.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
  }