@leemour/cli-core 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.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +106 -0
  3. package/dist/config.d.ts +20 -0
  4. package/dist/config.d.ts.map +1 -0
  5. package/dist/config.js +52 -0
  6. package/dist/config.js.map +1 -0
  7. package/dist/credentials.d.ts +48 -0
  8. package/dist/credentials.d.ts.map +1 -0
  9. package/dist/credentials.js +103 -0
  10. package/dist/credentials.js.map +1 -0
  11. package/dist/errors.d.ts +34 -0
  12. package/dist/errors.d.ts.map +1 -0
  13. package/dist/errors.js +36 -0
  14. package/dist/errors.js.map +1 -0
  15. package/dist/exit-codes.d.ts +11 -0
  16. package/dist/exit-codes.d.ts.map +1 -0
  17. package/dist/exit-codes.js +25 -0
  18. package/dist/exit-codes.js.map +1 -0
  19. package/dist/http/index.d.ts +20 -0
  20. package/dist/http/index.d.ts.map +1 -0
  21. package/dist/http/index.js +46 -0
  22. package/dist/http/index.js.map +1 -0
  23. package/dist/index.d.ts +15 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +14 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/keyring.d.ts +27 -0
  28. package/dist/keyring.d.ts.map +1 -0
  29. package/dist/keyring.js +45 -0
  30. package/dist/keyring.js.map +1 -0
  31. package/dist/logger.d.ts +12 -0
  32. package/dist/logger.d.ts.map +1 -0
  33. package/dist/logger.js +7 -0
  34. package/dist/logger.js.map +1 -0
  35. package/dist/logging.d.ts +26 -0
  36. package/dist/logging.d.ts.map +1 -0
  37. package/dist/logging.js +55 -0
  38. package/dist/logging.js.map +1 -0
  39. package/dist/paths.d.ts +28 -0
  40. package/dist/paths.d.ts.map +1 -0
  41. package/dist/paths.js +23 -0
  42. package/dist/paths.js.map +1 -0
  43. package/dist/pretty.d.ts +10 -0
  44. package/dist/pretty.d.ts.map +1 -0
  45. package/dist/pretty.js +76 -0
  46. package/dist/pretty.js.map +1 -0
  47. package/dist/renderer.d.ts +32 -0
  48. package/dist/renderer.d.ts.map +1 -0
  49. package/dist/renderer.js +48 -0
  50. package/dist/renderer.js.map +1 -0
  51. package/dist/retry.d.ts +27 -0
  52. package/dist/retry.d.ts.map +1 -0
  53. package/dist/retry.js +26 -0
  54. package/dist/retry.js.map +1 -0
  55. package/dist/streams.d.ts +24 -0
  56. package/dist/streams.d.ts.map +1 -0
  57. package/dist/streams.js +26 -0
  58. package/dist/streams.js.map +1 -0
  59. package/dist/testing/index.d.ts +24 -0
  60. package/dist/testing/index.d.ts.map +1 -0
  61. package/dist/testing/index.js +29 -0
  62. package/dist/testing/index.js.map +1 -0
  63. package/dist/time.d.ts +21 -0
  64. package/dist/time.d.ts.map +1 -0
  65. package/dist/time.js +19 -0
  66. package/dist/time.js.map +1 -0
  67. package/package.json +64 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Viacheslav Ptsarev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,106 @@
1
+ # @leemour/cli-core
2
+
3
+ The parts every command line tool needs and nobody enjoys writing twice: the two output streams,
4
+ a renderer for people and for machines, a closed error model with stable exit codes, the OS
5
+ keyring behind a testable seam, and injectable clocks.
6
+
7
+ Extracted from [`brazecli`](https://github.com/leemour/brazecli), where each piece earned its
8
+ shape, and shared with [`max-cli`](https://github.com/leemour/max-cli).
9
+
10
+ **Status: 0.1.0, not published yet.** Both extraction steps have landed: the files that move
11
+ unchanged, and the ones that needed a parameter threaded through. 73 tests.
12
+
13
+ ## The rule this package exists to keep
14
+
15
+ **In a machine mode, stdout carries data and nothing else.** No spinner, no `✓`, no warning, no
16
+ ANSI. Diagnostics go to stderr, in every mode including the pretty one. That is the contract a
17
+ script or an agent depends on, and `captureStreams` exists so a test can prove nothing leaked
18
+ across.
19
+
20
+ ```ts
21
+ import { captureStreams, createRenderer } from "@leemour/cli-core"
22
+
23
+ const streams = captureStreams()
24
+ createRenderer({ format: "json", color: false, streams }).result({ chats: 2 })
25
+
26
+ streams.stdout // ['{"chats":2}']
27
+ streams.stderr // []
28
+ ```
29
+
30
+ ## What is in it
31
+
32
+ | | |
33
+ |---|---|
34
+ | `streams` | the stdout/stderr split, and the capture used to test it |
35
+ | `renderer` · `pretty` | `pretty` / `json` / `jsonl`; tables for lists, labelled lines for objects |
36
+ | `errors` · `exit-codes` | 14 closed error codes, one exit number each, so a script can branch on `$?` |
37
+ | `keyring` | `KeyringStore` with the system, memory and deliberately-broken implementations |
38
+ | `time` | monotonic and wall clocks, and the one sleep that both timeouts and backoff use |
39
+ | `logger` | the four-method interface a host adapts Pino to — this package logs nothing itself |
40
+ | `paths` | `env-paths` for config, state and cache, each overridable by environment variable |
41
+ | `config` | JSON config loading that names the bad field, and an atomic write that locks the directory down |
42
+ | `credentials` | environment → keyring → file, warning once and falling through when the keyring refuses |
43
+ | `logging` | a Pino adapter writing JSON lines with secrets redacted by field name |
44
+ | `retry` | full-jitter backoff, and the distinction between "no answer came" and "safe to repeat" |
45
+ | `/testing` | `captureStreams`, `memoryKeyring`, `brokenKeyring`, `fakeClock` |
46
+
47
+ **Nothing in the root export is HTTP.** Status classification, `Retry-After` parsing and the fetch
48
+ seam live in `@leemour/cli-core/http`, so a CLI that speaks a socket never depends on a stack it
49
+ does not call:
50
+
51
+ ```ts
52
+ import { providerWaitMs, statusToCode } from "@leemour/cli-core/http"
53
+ ```
54
+
55
+ **Two traps worth knowing before you use the credential store.** The OS keyring is global: an entry
56
+ is addressed by service and account and knows nothing about which config directory asked for it, so
57
+ a throwaway config directory silently overwrites the real secret unless you pass `isolated: true`.
58
+ And a secret should never be handed to a logger in the first place — redaction by field name is the
59
+ second line of defence, not the first.
60
+
61
+ **Everything the environment knows is passed in.** No `process.env` reads, no config file paths,
62
+ no ambient clock. That is what makes a timeout test finish instantly and a keyring test incapable
63
+ of reaching a real keychain.
64
+
65
+ ## Where secrets actually go, per platform
66
+
67
+ The keyring is real on every desktop and absent on most servers, so the fallback is not an edge
68
+ case — it is the normal path in CI and containers. Checked against `@napi-rs/keyring` 2.1.0 on
69
+ 2026-09-19.
70
+
71
+ | Platform | Backing store | Needs installing |
72
+ |---|---|---|
73
+ | macOS | Keychain, via the Security framework | nothing — part of the OS |
74
+ | Windows | Credential Manager | nothing — part of the OS |
75
+ | Linux desktop (GNOME, KDE) | Secret Service over D-Bus — `gnome-keyring`, `kwallet` | nothing on a normal desktop; the keyring must be **unlocked** |
76
+ | Linux headless, container, WSL, CI | usually **nothing** — no session bus, no secrets daemon | falls back to a `0600` file, with one warning on stderr |
77
+ | FreeBSD | Secret Service, same as Linux | same |
78
+
79
+ **No compiler is involved.** The package ships prebuilt binaries for twelve platform triples —
80
+ macOS arm64/x64, Windows x64/ia32/arm64, Linux x64 and arm64 in both glibc and musl, armv7,
81
+ riscv64, FreeBSD x64 — so there is no Rust toolchain and no node-gyp on any mainstream target.
82
+ The Linux binary links only against libc: it speaks D-Bus itself rather than through libsecret,
83
+ so *libsecret is not a requirement* — a running Secret Service provider is.
84
+
85
+ Two consequences worth designing for rather than discovering:
86
+
87
+ - **`auto` is the right default and `file` must stay available.** `credentialStorage: "file"` skips
88
+ the keyring entirely, which is what a container wants and what a locked keyring makes necessary.
89
+ - **A locked Linux keyring can block on a prompt** rather than failing. That is the one case the
90
+ fallback does not rescue, and a CLI that hangs looks broken rather than locked.
91
+
92
+ ## Both runtimes
93
+
94
+ Node 22+ and Bun, and the Bun half is executed rather than assumed:
95
+
96
+ ```sh
97
+ pnpm test # vitest, Node
98
+ pnpm smoke:bun # the same exports, actually run under Bun
99
+ pnpm lint
100
+ pnpm typecheck
101
+ pnpm build
102
+ ```
103
+
104
+ ## Licence
105
+
106
+ MIT.
@@ -0,0 +1,20 @@
1
+ import * as v from "valibot";
2
+ /**
3
+ * Atomic, and the directory is locked down before the first write.
4
+ *
5
+ * Atomic because a partial credential file loses **every** profile's secret, not just the one
6
+ * being written, and the window for that is exactly a Ctrl+C mid-write. The directory mode matters
7
+ * as much as the file's: `0o600` on a file inside a world-readable directory is worth little.
8
+ * ⚠ Both modes are ignored on Windows.
9
+ */
10
+ export declare const writeSecurely: (path: string, contents: string, mode: number) => void;
11
+ /**
12
+ * Reads and validates a JSON config. A missing file is not an error — it is a program that has not
13
+ * been configured yet — but a malformed one is, and it says which field and why.
14
+ *
15
+ * The schema belongs to the host: what a profile holds is the one thing no shared package can know.
16
+ */
17
+ export declare const loadConfigFile: <TSchema extends v.GenericSchema>(path: string, schema: TSchema, fallback: () => v.InferOutput<TSchema>) => v.InferOutput<TSchema>;
18
+ /** Config is not a secret, so it is `0644`; the credential file next to it is not. */
19
+ export declare const saveConfigFile: (path: string, config: unknown) => void;
20
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,CAAC,MAAM,SAAS,CAAA;AAE5B;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa,GAAI,MAAM,MAAM,EAAE,UAAU,MAAM,EAAE,MAAM,MAAM,KAAG,IAQ5E,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,cAAc,GAAI,OAAO,SAAS,CAAC,CAAC,aAAa,EAC5D,MAAM,MAAM,EACZ,QAAQ,OAAO,EACf,UAAU,MAAM,CAAC,CAAC,WAAW,CAAC,OAAO,CAAC,KACrC,CAAC,CAAC,WAAW,CAAC,OAAO,CAqBvB,CAAA;AAED,sFAAsF;AACtF,eAAO,MAAM,cAAc,GAAI,MAAM,MAAM,EAAE,QAAQ,OAAO,KAAG,IAE9D,CAAA"}
package/dist/config.js ADDED
@@ -0,0 +1,52 @@
1
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import * as v from "valibot";
4
+ /**
5
+ * Atomic, and the directory is locked down before the first write.
6
+ *
7
+ * Atomic because a partial credential file loses **every** profile's secret, not just the one
8
+ * being written, and the window for that is exactly a Ctrl+C mid-write. The directory mode matters
9
+ * as much as the file's: `0o600` on a file inside a world-readable directory is worth little.
10
+ * ⚠ Both modes are ignored on Windows.
11
+ */
12
+ export const writeSecurely = (path, contents, mode) => {
13
+ const dir = dirname(path);
14
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
15
+ const temp = join(dir, `.${Date.now()}-${process.pid}.tmp`);
16
+ writeFileSync(temp, contents, { mode });
17
+ // A rename within one directory is atomic on every platform we target.
18
+ renameSync(temp, path);
19
+ };
20
+ /**
21
+ * Reads and validates a JSON config. A missing file is not an error — it is a program that has not
22
+ * been configured yet — but a malformed one is, and it says which field and why.
23
+ *
24
+ * The schema belongs to the host: what a profile holds is the one thing no shared package can know.
25
+ */
26
+ export const loadConfigFile = (path, schema, fallback) => {
27
+ let text;
28
+ try {
29
+ text = readFileSync(path, "utf8");
30
+ }
31
+ catch {
32
+ return fallback();
33
+ }
34
+ let parsed;
35
+ try {
36
+ parsed = JSON.parse(text);
37
+ }
38
+ catch {
39
+ throw new Error(`${path} is not valid JSON`);
40
+ }
41
+ const result = v.safeParse(schema, parsed);
42
+ if (!result.success) {
43
+ const problems = result.issues.map((issue) => `${v.getDotPath(issue) ?? "(root)"}: ${issue.message}`);
44
+ throw new Error(`${path} is not a valid config:\n ${problems.join("\n ")}`);
45
+ }
46
+ return result.output;
47
+ };
48
+ /** Config is not a secret, so it is `0644`; the credential file next to it is not. */
49
+ export const saveConfigFile = (path, config) => {
50
+ writeSecurely(path, `${JSON.stringify(config, null, 2)}\n`, 0o644);
51
+ };
52
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAC5E,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AACzC,OAAO,KAAK,CAAC,MAAM,SAAS,CAAA;AAE5B;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,IAAY,EAAE,QAAgB,EAAE,IAAY,EAAQ,EAAE;IAClF,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACzB,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAA;IAEhD,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,GAAG,EAAE,IAAI,OAAO,CAAC,GAAG,MAAM,CAAC,CAAA;IAC3D,aAAa,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,IAAI,EAAE,CAAC,CAAA;IACvC,uEAAuE;IACvE,UAAU,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;AACxB,CAAC,CAAA;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAC5B,IAAY,EACZ,MAAe,EACf,QAAsC,EACd,EAAE;IAC1B,IAAI,IAAY,CAAA;IAChB,IAAI,CAAC;QACH,IAAI,GAAG,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;IACnC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,QAAQ,EAAE,CAAA;IACnB,CAAC;IAED,IAAI,MAAe,CAAA;IACnB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IAC3B,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,oBAAoB,CAAC,CAAA;IAC9C,CAAC;IAED,MAAM,MAAM,GAAG,CAAC,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC1C,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,QAAQ,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC,CAAA;QACrG,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,8BAA8B,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;IAC/E,CAAC;IACD,OAAO,MAAM,CAAC,MAAM,CAAA;AACtB,CAAC,CAAA;AAED,sFAAsF;AACtF,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,IAAY,EAAE,MAAe,EAAQ,EAAE;IACpE,aAAa,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;AACpE,CAAC,CAAA"}
@@ -0,0 +1,48 @@
1
+ import { type KeyringStore } from "./keyring.js";
2
+ export declare const CREDENTIAL_STORAGE: readonly ["auto", "keyring", "file"];
3
+ export type CredentialStorage = (typeof CREDENTIAL_STORAGE)[number];
4
+ export type CredentialSource = "environment" | "keyring" | "file";
5
+ export interface StoredCredential {
6
+ secret: string;
7
+ source: CredentialSource;
8
+ }
9
+ export interface CredentialsOptions {
10
+ /** Where the fallback file lives when the keyring cannot be used. */
11
+ configDir: string;
12
+ /** The keyring service name — the command's name, e.g. `max-cli`. */
13
+ service: string;
14
+ /**
15
+ * **True when the config directory came from the environment rather than the OS convention.**
16
+ *
17
+ * The OS keyring is global: an entry is addressed by service and account and knows nothing about
18
+ * which config directory asked for it. So a throwaway config directory looks isolated and is
19
+ * not — it overwrites the real secret for that account. In `brazecli` that destroyed two working
20
+ * keys on 2026-09-14, and a keyring entry cannot be read back out. Passing `true` here scopes the
21
+ * service name to the directory, making the isolation people already assume they have real.
22
+ */
23
+ isolated?: boolean;
24
+ /** Read before the keyring, for CI. */
25
+ envVar?: string;
26
+ storage?: CredentialStorage;
27
+ keyring?: KeyringStore;
28
+ env?: NodeJS.ProcessEnv;
29
+ fileName?: string;
30
+ /** Where the one-line warning goes when the keyring is unavailable. Never stdout. */
31
+ warn?: (message: string) => void;
32
+ }
33
+ export declare const keyringService: (service: string, configDir: string, isolated?: boolean) => string;
34
+ /**
35
+ * Environment first, then the OS keyring, then a file.
36
+ *
37
+ * `auto` does not probe whether a keyring exists: it attempts the operation, and on failure warns
38
+ * once and falls through. A probe would touch the user's keychain for nothing and could still
39
+ * succeed where the real operation fails.
40
+ */
41
+ export declare class Credentials {
42
+ #private;
43
+ constructor(options: CredentialsOptions);
44
+ read(account: string): StoredCredential | undefined;
45
+ write(account: string, secret: string): CredentialSource;
46
+ remove(account: string): CredentialSource[];
47
+ }
48
+ //# sourceMappingURL=credentials.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../src/credentials.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,YAAY,EAAiB,MAAM,cAAc,CAAA;AAE/D,eAAO,MAAM,kBAAkB,sCAAuC,CAAA;AACtE,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,CAAC,CAAA;AAEnE,MAAM,MAAM,gBAAgB,GAAG,aAAa,GAAG,SAAS,GAAG,MAAM,CAAA;AAEjE,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,gBAAgB,CAAA;CACzB;AAED,MAAM,WAAW,kBAAkB;IACjC,qEAAqE;IACrE,SAAS,EAAE,MAAM,CAAA;IACjB,qEAAqE;IACrE,OAAO,EAAE,MAAM,CAAA;IACf;;;;;;;;OAQG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,uCAAuC;IACvC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,iBAAiB,CAAA;IAC3B,OAAO,CAAC,EAAE,YAAY,CAAA;IACtB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAA;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,qFAAqF;IACrF,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CACjC;AAED,eAAO,MAAM,cAAc,GAAI,SAAS,MAAM,EAAE,WAAW,MAAM,EAAE,kBAAgB,KAAG,MACtC,CAAA;AAIhD;;;;;;GAMG;AACH,qBAAa,WAAW;;gBAWV,OAAO,EAAE,kBAAkB;IAWvC,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS;IAanD,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,gBAAgB;IAexD,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,gBAAgB,EAAE;CAiD5C"}
@@ -0,0 +1,103 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { writeSecurely } from "./config.js";
4
+ import { systemKeyring } from "./keyring.js";
5
+ export const CREDENTIAL_STORAGE = ["auto", "keyring", "file"];
6
+ export const keyringService = (service, configDir, isolated = false) => isolated ? `${service}:${configDir}` : service;
7
+ /**
8
+ * Environment first, then the OS keyring, then a file.
9
+ *
10
+ * `auto` does not probe whether a keyring exists: it attempts the operation, and on failure warns
11
+ * once and falls through. A probe would touch the user's keychain for nothing and could still
12
+ * succeed where the real operation fails.
13
+ */
14
+ export class Credentials {
15
+ #configDir;
16
+ #storage;
17
+ #keyring;
18
+ #env;
19
+ #service;
20
+ #envVar;
21
+ #fileName;
22
+ #warn;
23
+ #warned = false;
24
+ constructor(options) {
25
+ this.#configDir = options.configDir;
26
+ this.#storage = options.storage ?? "auto";
27
+ this.#keyring = options.keyring ?? systemKeyring;
28
+ this.#env = options.env ?? process.env;
29
+ this.#envVar = options.envVar;
30
+ this.#fileName = options.fileName ?? "credentials.json";
31
+ this.#warn = options.warn ?? ((message) => process.stderr.write(`${message}\n`));
32
+ this.#service = keyringService(options.service, options.configDir, options.isolated);
33
+ }
34
+ read(account) {
35
+ const fromEnv = this.#envVar ? this.#env[this.#envVar]?.trim() : undefined;
36
+ if (fromEnv)
37
+ return { secret: fromEnv, source: "environment" };
38
+ if (this.#storage !== "file") {
39
+ const fromKeyring = this.#tryKeyring(() => this.#keyring.get(this.#service, account));
40
+ if (fromKeyring)
41
+ return { secret: fromKeyring, source: "keyring" };
42
+ }
43
+ const fromFile = this.#readFile()[account]?.secret;
44
+ return fromFile ? { secret: fromFile, source: "file" } : undefined;
45
+ }
46
+ write(account, secret) {
47
+ if (this.#storage !== "file") {
48
+ const stored = this.#tryKeyring(() => {
49
+ this.#keyring.set(this.#service, account, secret);
50
+ return true;
51
+ });
52
+ if (stored)
53
+ return "keyring";
54
+ }
55
+ const store = this.#readFile();
56
+ store[account] = { secret };
57
+ this.#writeFile(store);
58
+ return "file";
59
+ }
60
+ remove(account) {
61
+ const removed = [];
62
+ if (this.#storage !== "file" && this.#tryKeyring(() => this.#keyring.delete(this.#service, account))) {
63
+ removed.push("keyring");
64
+ }
65
+ const store = this.#readFile();
66
+ if (store[account] !== undefined) {
67
+ delete store[account];
68
+ this.#writeFile(store);
69
+ removed.push("file");
70
+ }
71
+ return removed;
72
+ }
73
+ #path() {
74
+ return join(this.#configDir, this.#fileName);
75
+ }
76
+ #readFile() {
77
+ try {
78
+ return JSON.parse(readFileSync(this.#path(), "utf8"));
79
+ }
80
+ catch {
81
+ return {};
82
+ }
83
+ }
84
+ #writeFile(store) {
85
+ writeSecurely(this.#path(), `${JSON.stringify(store, null, 2)}\n`, 0o600);
86
+ }
87
+ #tryKeyring(operation) {
88
+ if (this.#storage === "keyring")
89
+ return operation();
90
+ try {
91
+ return operation();
92
+ }
93
+ catch (error) {
94
+ if (!this.#warned) {
95
+ this.#warned = true;
96
+ this.#warn(`the OS keyring is unavailable (${error instanceof Error ? error.message : String(error)}); ` +
97
+ `storing in ${this.#path()} instead, readable only by you`);
98
+ }
99
+ return undefined;
100
+ }
101
+ }
102
+ }
103
+ //# sourceMappingURL=credentials.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credentials.js","sourceRoot":"","sources":["../src/credentials.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Every failure a CLI built on this package, and anything consuming its output, can see. The list
3
+ * is closed on purpose: an agent branches on `code`, and a new code is a contract change.
4
+ *
5
+ * "provider" is whatever the CLI talks to — an HTTP API, a socket, a messenger. The word is
6
+ * deliberately not "server": a provider that answered with a refusal is not the same as one that
7
+ * never answered.
8
+ */
9
+ export declare const errorCodes: readonly ["validation_error", "configuration_error", "authentication_error", "permission_error", "not_found", "confirmation_required", "rate_limited", "timeout", "network_error", "provider_error", "provider_unavailable", "invalid_response", "outcome_unknown", "cancelled"];
10
+ export type ErrorCode = (typeof errorCodes)[number];
11
+ /**
12
+ * Anything a caller may attach. Everything is optional because these are diagnostics, not a
13
+ * contract — the contract is `code`.
14
+ *
15
+ * `retryable` says whether **this** failure could be retried, not whether the operation is safe to
16
+ * retry. Those are different questions and conflating them is how a duplicate write happens.
17
+ */
18
+ export interface CliErrorDetails {
19
+ readonly status?: number;
20
+ readonly retryable?: boolean;
21
+ readonly attempts?: number;
22
+ readonly requestId?: string;
23
+ readonly runId?: string;
24
+ readonly retryAfterMs?: number;
25
+ readonly operation?: string;
26
+ readonly [key: string]: unknown;
27
+ }
28
+ export declare class CliError extends Error {
29
+ readonly code: ErrorCode;
30
+ readonly details: CliErrorDetails;
31
+ constructor(code: ErrorCode, message: string, details?: CliErrorDetails);
32
+ }
33
+ export declare const isCliError: (value: unknown) => value is CliError;
34
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,eAAO,MAAM,UAAU,kRAeb,CAAA;AAEV,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,CAAC,CAAA;AAEnD;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAA;IAC5B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAA;IAC9B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAChC;AAED,qBAAa,QAAS,SAAQ,KAAK;IACjC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;IACxB,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAA;gBAErB,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB;CAM5E;AAED,eAAO,MAAM,UAAU,GAAI,OAAO,OAAO,KAAG,KAAK,IAAI,QAAqC,CAAA"}
package/dist/errors.js ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Every failure a CLI built on this package, and anything consuming its output, can see. The list
3
+ * is closed on purpose: an agent branches on `code`, and a new code is a contract change.
4
+ *
5
+ * "provider" is whatever the CLI talks to — an HTTP API, a socket, a messenger. The word is
6
+ * deliberately not "server": a provider that answered with a refusal is not the same as one that
7
+ * never answered.
8
+ */
9
+ export const errorCodes = [
10
+ "validation_error",
11
+ "configuration_error",
12
+ "authentication_error",
13
+ "permission_error",
14
+ "not_found",
15
+ "confirmation_required",
16
+ "rate_limited",
17
+ "timeout",
18
+ "network_error",
19
+ "provider_error",
20
+ "provider_unavailable",
21
+ "invalid_response",
22
+ "outcome_unknown",
23
+ "cancelled",
24
+ ];
25
+ export class CliError extends Error {
26
+ code;
27
+ details;
28
+ constructor(code, message, details = {}) {
29
+ super(message);
30
+ this.name = "CliError";
31
+ this.code = code;
32
+ this.details = details;
33
+ }
34
+ }
35
+ export const isCliError = (value) => value instanceof CliError;
36
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,kBAAkB;IAClB,qBAAqB;IACrB,sBAAsB;IACtB,kBAAkB;IAClB,WAAW;IACX,uBAAuB;IACvB,cAAc;IACd,SAAS;IACT,eAAe;IACf,gBAAgB;IAChB,sBAAsB;IACtB,kBAAkB;IAClB,iBAAiB;IACjB,WAAW;CACH,CAAA;AAsBV,MAAM,OAAO,QAAS,SAAQ,KAAK;IACxB,IAAI,CAAW;IACf,OAAO,CAAiB;IAEjC,YAAY,IAAe,EAAE,OAAe,EAAE,UAA2B,EAAE;QACzE,KAAK,CAAC,OAAO,CAAC,CAAA;QACd,IAAI,CAAC,IAAI,GAAG,UAAU,CAAA;QACtB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;QAChB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAA;IACxB,CAAC;CACF;AAED,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,KAAc,EAAqB,EAAE,CAAC,KAAK,YAAY,QAAQ,CAAA"}
@@ -0,0 +1,11 @@
1
+ import type { ErrorCode } from "./errors.js";
2
+ /**
3
+ * One mapping, so a script can branch on `$?` without parsing text. Defined here rather than at
4
+ * each command site: numbers invented per command are how two of them end up disagreeing.
5
+ *
6
+ * 130 for a cancellation is the shell's convention for SIGINT.
7
+ */
8
+ export declare const EXIT_CODES: Record<ErrorCode, number>;
9
+ export declare const exitCodeFor: (code: ErrorCode) => number;
10
+ export declare const GENERIC_FAILURE = 1;
11
+ //# sourceMappingURL=exit-codes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exit-codes.d.ts","sourceRoot":"","sources":["../src/exit-codes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAE5C;;;;;GAKG;AACH,eAAO,MAAM,UAAU,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,CAehD,CAAA;AAED,eAAO,MAAM,WAAW,GAAI,MAAM,SAAS,KAAG,MAA0B,CAAA;AAExE,eAAO,MAAM,eAAe,IAAI,CAAA"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * One mapping, so a script can branch on `$?` without parsing text. Defined here rather than at
3
+ * each command site: numbers invented per command are how two of them end up disagreeing.
4
+ *
5
+ * 130 for a cancellation is the shell's convention for SIGINT.
6
+ */
7
+ export const EXIT_CODES = {
8
+ validation_error: 2,
9
+ configuration_error: 3,
10
+ authentication_error: 4,
11
+ permission_error: 5,
12
+ not_found: 6,
13
+ confirmation_required: 7,
14
+ rate_limited: 8,
15
+ timeout: 9,
16
+ network_error: 10,
17
+ provider_error: 11,
18
+ provider_unavailable: 12,
19
+ invalid_response: 13,
20
+ outcome_unknown: 14,
21
+ cancelled: 130,
22
+ };
23
+ export const exitCodeFor = (code) => EXIT_CODES[code];
24
+ export const GENERIC_FAILURE = 1;
25
+ //# sourceMappingURL=exit-codes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exit-codes.js","sourceRoot":"","sources":["../src/exit-codes.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,UAAU,GAA8B;IACnD,gBAAgB,EAAE,CAAC;IACnB,mBAAmB,EAAE,CAAC;IACtB,oBAAoB,EAAE,CAAC;IACvB,gBAAgB,EAAE,CAAC;IACnB,SAAS,EAAE,CAAC;IACZ,qBAAqB,EAAE,CAAC;IACxB,YAAY,EAAE,CAAC;IACf,OAAO,EAAE,CAAC;IACV,aAAa,EAAE,EAAE;IACjB,cAAc,EAAE,EAAE;IAClB,oBAAoB,EAAE,EAAE;IACxB,gBAAgB,EAAE,EAAE;IACpB,eAAe,EAAE,EAAE;IACnB,SAAS,EAAE,GAAG;CACf,CAAA;AAED,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,IAAe,EAAU,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,CAAA;AAExE,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,CAAA"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The HTTP layer, deliberately behind its own entry point.
3
+ *
4
+ * A CLI that speaks a socket, a message bus or a local database has no business depending on
5
+ * status-code semantics and `Retry-After` parsing. Import `@leemour/cli-core/http` only when
6
+ * the thing on the other end is actually HTTP.
7
+ */
8
+ import type { ErrorCode } from "../errors.js";
9
+ import type { WallClock } from "../time.js";
10
+ /** The one function an HTTP client needs from its environment. A Worker, a browser, a test and Node all satisfy it. */
11
+ export type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
12
+ /** `Retry-After` is either a number of seconds or an HTTP date. Both forms occur in the wild. */
13
+ export declare const parseRetryAfter: (header: string | null | undefined, now: WallClock) => number | undefined;
14
+ /** A Unix timestamp in seconds, the shape several providers use for `X-RateLimit-Reset`. */
15
+ export declare const parseRateLimitReset: (header: string | null | undefined, now: WallClock) => number | undefined;
16
+ /** What the provider asked us to wait, if it asked at all. Its timing beats our own backoff. */
17
+ export declare const providerWaitMs: (headers: Headers, now: WallClock) => number | undefined;
18
+ export declare const retryableStatus: (status: number) => boolean;
19
+ export declare const statusToCode: (status: number) => ErrorCode;
20
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/http/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAC7C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAE3C,uHAAuH;AACvH,MAAM,MAAM,SAAS,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,GAAG,OAAO,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;AAEhG,iGAAiG;AACjG,eAAO,MAAM,eAAe,GAAI,QAAQ,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,KAAK,SAAS,KAAG,MAAM,GAAG,SAa5F,CAAA;AAED,4FAA4F;AAC5F,eAAO,MAAM,mBAAmB,GAAI,QAAQ,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,KAAK,SAAS,KAAG,MAAM,GAAG,SAQhG,CAAA;AAED,gGAAgG;AAChG,eAAO,MAAM,cAAc,GAAI,SAAS,OAAO,EAAE,KAAK,SAAS,KAAG,MAAM,GAAG,SACqC,CAAA;AAEhH,eAAO,MAAM,eAAe,GAAI,QAAQ,MAAM,KAAG,OAA4D,CAAA;AAE7G,eAAO,MAAM,YAAY,GAAI,QAAQ,MAAM,KAAG,SAS7C,CAAA"}
@@ -0,0 +1,46 @@
1
+ /** `Retry-After` is either a number of seconds or an HTTP date. Both forms occur in the wild. */
2
+ export const parseRetryAfter = (header, now) => {
3
+ if (!header)
4
+ return undefined;
5
+ const trimmed = header.trim();
6
+ if (/^\d+$/.test(trimmed))
7
+ return Number(trimmed) * 1000;
8
+ const at = Date.parse(trimmed);
9
+ if (Number.isNaN(at))
10
+ return undefined;
11
+ const delta = at - now().getTime();
12
+ // A date already in the past would otherwise become a zero wait, and a zero wait against a rate
13
+ // limit is a hot loop.
14
+ return delta > 0 ? delta : undefined;
15
+ };
16
+ /** A Unix timestamp in seconds, the shape several providers use for `X-RateLimit-Reset`. */
17
+ export const parseRateLimitReset = (header, now) => {
18
+ if (!header)
19
+ return undefined;
20
+ const seconds = Number(header.trim());
21
+ if (!Number.isFinite(seconds))
22
+ return undefined;
23
+ const delta = seconds * 1000 - now().getTime();
24
+ return delta > 0 ? delta : undefined;
25
+ };
26
+ /** What the provider asked us to wait, if it asked at all. Its timing beats our own backoff. */
27
+ export const providerWaitMs = (headers, now) => parseRetryAfter(headers.get("retry-after"), now) ?? parseRateLimitReset(headers.get("x-ratelimit-reset"), now);
28
+ export const retryableStatus = (status) => status === 408 || status === 429 || status >= 500;
29
+ export const statusToCode = (status) => {
30
+ if (status === 400 || status === 422)
31
+ return "validation_error";
32
+ if (status === 401)
33
+ return "authentication_error";
34
+ if (status === 403)
35
+ return "permission_error";
36
+ if (status === 404)
37
+ return "not_found";
38
+ if (status === 408)
39
+ return "timeout";
40
+ if (status === 429)
41
+ return "rate_limited";
42
+ if (status === 502 || status === 503 || status === 504)
43
+ return "provider_unavailable";
44
+ return "provider_error";
45
+ };
46
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/http/index.ts"],"names":[],"mappings":"AAaA,iGAAiG;AACjG,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,MAAiC,EAAE,GAAc,EAAsB,EAAE;IACvG,IAAI,CAAC,MAAM;QAAE,OAAO,SAAS,CAAA;IAE7B,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,EAAE,CAAA;IAC7B,IAAI,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC;QAAE,OAAO,MAAM,CAAC,OAAO,CAAC,GAAG,IAAI,CAAA;IAExD,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;IAC9B,IAAI,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;QAAE,OAAO,SAAS,CAAA;IAEtC,MAAM,KAAK,GAAG,EAAE,GAAG,GAAG,EAAE,CAAC,OAAO,EAAE,CAAA;IAClC,gGAAgG;IAChG,uBAAuB;IACvB,OAAO,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAA;AACtC,CAAC,CAAA;AAED,4FAA4F;AAC5F,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,MAAiC,EAAE,GAAc,EAAsB,EAAE;IAC3G,IAAI,CAAC,MAAM;QAAE,OAAO,SAAS,CAAA;IAE7B,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAA;IACrC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC;QAAE,OAAO,SAAS,CAAA;IAE/C,MAAM,KAAK,GAAG,OAAO,GAAG,IAAI,GAAG,GAAG,EAAE,CAAC,OAAO,EAAE,CAAA;IAC9C,OAAO,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAA;AACtC,CAAC,CAAA;AAED,gGAAgG;AAChG,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,OAAgB,EAAE,GAAc,EAAsB,EAAE,CACrF,eAAe,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,EAAE,GAAG,CAAC,IAAI,mBAAmB,CAAC,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,EAAE,GAAG,CAAC,CAAA;AAEhH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,MAAc,EAAW,EAAE,CAAC,MAAM,KAAK,GAAG,IAAI,MAAM,KAAK,GAAG,IAAI,MAAM,IAAI,GAAG,CAAA;AAE7G,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,MAAc,EAAa,EAAE;IACxD,IAAI,MAAM,KAAK,GAAG,IAAI,MAAM,KAAK,GAAG;QAAE,OAAO,kBAAkB,CAAA;IAC/D,IAAI,MAAM,KAAK,GAAG;QAAE,OAAO,sBAAsB,CAAA;IACjD,IAAI,MAAM,KAAK,GAAG;QAAE,OAAO,kBAAkB,CAAA;IAC7C,IAAI,MAAM,KAAK,GAAG;QAAE,OAAO,WAAW,CAAA;IACtC,IAAI,MAAM,KAAK,GAAG;QAAE,OAAO,SAAS,CAAA;IACpC,IAAI,MAAM,KAAK,GAAG;QAAE,OAAO,cAAc,CAAA;IACzC,IAAI,MAAM,KAAK,GAAG,IAAI,MAAM,KAAK,GAAG,IAAI,MAAM,KAAK,GAAG;QAAE,OAAO,sBAAsB,CAAA;IACrF,OAAO,gBAAgB,CAAA;AACzB,CAAC,CAAA"}
@@ -0,0 +1,15 @@
1
+ export { loadConfigFile, saveConfigFile, writeSecurely } from "./config.js";
2
+ export type { CredentialSource, CredentialStorage, CredentialsOptions, StoredCredential } from "./credentials.js";
3
+ export { CREDENTIAL_STORAGE, Credentials, keyringService } from "./credentials.js";
4
+ export { CliError, type CliErrorDetails, type ErrorCode, errorCodes, isCliError } from "./errors.js";
5
+ export { EXIT_CODES, exitCodeFor, GENERIC_FAILURE } from "./exit-codes.js";
6
+ export { brokenKeyring, type KeyringStore, memoryKeyring, systemKeyring } from "./keyring.js";
7
+ export { type Logger, noopLogger } from "./logger.js";
8
+ export { createFileLogger, type FileLogger, type FileLoggerOptions, REDACTED_FIELDS } from "./logging.js";
9
+ export { configFilePath, type Paths, type PathsOptions, pathsAreOverridden, resolvePaths } from "./paths.js";
10
+ export { type PrettyOptions, renderPretty } from "./pretty.js";
11
+ export { createRenderer, RENDER_FORMATS, type Renderer, type RendererOptions, type RenderFormat, } from "./renderer.js";
12
+ export { backoffMs, DEFAULT_RETRY, isTransportFailure, type RetryConfig } from "./retry.js";
13
+ export { captureStreams, processStreams, type Streams } from "./streams.js";
14
+ export { abortError, type MonotonicClock, monotonic, realSleep, type SleepLike, type SleepReason, type WallClock, wallClock, } from "./time.js";
15
+ //# 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,cAAc,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAC3E,YAAY,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AACjH,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAClF,OAAO,EAAE,QAAQ,EAAE,KAAK,eAAe,EAAE,KAAK,SAAS,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACpG,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAC1E,OAAO,EAAE,aAAa,EAAE,KAAK,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAC7F,OAAO,EAAE,KAAK,MAAM,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACrD,OAAO,EAAE,gBAAgB,EAAE,KAAK,UAAU,EAAE,KAAK,iBAAiB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AACzG,OAAO,EAAE,cAAc,EAAE,KAAK,KAAK,EAAE,KAAK,YAAY,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAC5G,OAAO,EAAE,KAAK,aAAa,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC9D,OAAO,EACL,cAAc,EACd,cAAc,EACd,KAAK,QAAQ,EACb,KAAK,eAAe,EACpB,KAAK,YAAY,GAClB,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,kBAAkB,EAAE,KAAK,WAAW,EAAE,MAAM,YAAY,CAAA;AAC3F,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,KAAK,OAAO,EAAE,MAAM,cAAc,CAAA;AAC3E,OAAO,EACL,UAAU,EACV,KAAK,cAAc,EACnB,SAAS,EACT,SAAS,EACT,KAAK,SAAS,EACd,KAAK,WAAW,EAChB,KAAK,SAAS,EACd,SAAS,GACV,MAAM,WAAW,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ export { loadConfigFile, saveConfigFile, writeSecurely } from "./config.js";
2
+ export { CREDENTIAL_STORAGE, Credentials, keyringService } from "./credentials.js";
3
+ export { CliError, errorCodes, isCliError } from "./errors.js";
4
+ export { EXIT_CODES, exitCodeFor, GENERIC_FAILURE } from "./exit-codes.js";
5
+ export { brokenKeyring, memoryKeyring, systemKeyring } from "./keyring.js";
6
+ export { noopLogger } from "./logger.js";
7
+ export { createFileLogger, REDACTED_FIELDS } from "./logging.js";
8
+ export { configFilePath, pathsAreOverridden, resolvePaths } from "./paths.js";
9
+ export { renderPretty } from "./pretty.js";
10
+ export { createRenderer, RENDER_FORMATS, } from "./renderer.js";
11
+ export { backoffMs, DEFAULT_RETRY, isTransportFailure } from "./retry.js";
12
+ export { captureStreams, processStreams } from "./streams.js";
13
+ export { abortError, monotonic, realSleep, wallClock, } from "./time.js";
14
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAE3E,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAClF,OAAO,EAAE,QAAQ,EAAwC,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACpG,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAC1E,OAAO,EAAE,aAAa,EAAqB,aAAa,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAC7F,OAAO,EAAe,UAAU,EAAE,MAAM,aAAa,CAAA;AACrD,OAAO,EAAE,gBAAgB,EAA2C,eAAe,EAAE,MAAM,cAAc,CAAA;AACzG,OAAO,EAAE,cAAc,EAAiC,kBAAkB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAC5G,OAAO,EAAsB,YAAY,EAAE,MAAM,aAAa,CAAA;AAC9D,OAAO,EACL,cAAc,EACd,cAAc,GAIf,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,kBAAkB,EAAoB,MAAM,YAAY,CAAA;AAC3F,OAAO,EAAE,cAAc,EAAE,cAAc,EAAgB,MAAM,cAAc,CAAA;AAC3E,OAAO,EACL,UAAU,EAEV,SAAS,EACT,SAAS,EAIT,SAAS,GACV,MAAM,WAAW,CAAA"}