@ttctl/core 0.0.0 → 0.1.0-rc.1
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/README.md +49 -9
- package/dist/__generated__/gateway.d.ts +4546 -0
- package/dist/__generated__/gateway.d.ts.map +1 -0
- package/dist/__generated__/gateway.js +9 -0
- package/dist/__generated__/gateway.js.map +1 -0
- package/dist/__generated__/talent-profile-zod-schemas.d.ts +1187 -0
- package/dist/__generated__/talent-profile-zod-schemas.d.ts.map +1 -0
- package/dist/__generated__/talent-profile-zod-schemas.js +1136 -0
- package/dist/__generated__/talent-profile-zod-schemas.js.map +1 -0
- package/dist/__generated__/talent-profile.d.ts +1397 -0
- package/dist/__generated__/talent-profile.d.ts.map +1 -0
- package/dist/__generated__/talent-profile.js +9 -0
- package/dist/__generated__/talent-profile.js.map +1 -0
- package/dist/__generated__/zod-schemas.d.ts +2895 -0
- package/dist/__generated__/zod-schemas.d.ts.map +1 -0
- package/dist/__generated__/zod-schemas.js +3121 -0
- package/dist/__generated__/zod-schemas.js.map +1 -0
- package/dist/__tests__/fixtures/profile/builders.d.ts +74 -0
- package/dist/__tests__/fixtures/profile/builders.d.ts.map +1 -0
- package/dist/__tests__/fixtures/profile/builders.js +196 -0
- package/dist/__tests__/fixtures/profile/builders.js.map +1 -0
- package/dist/__tests__/fixtures/profile/data.d.ts +39 -0
- package/dist/__tests__/fixtures/profile/data.d.ts.map +1 -0
- package/dist/__tests__/fixtures/profile/data.js +230 -0
- package/dist/__tests__/fixtures/profile/data.js.map +1 -0
- package/dist/__tests__/fixtures/profile/index.d.ts +9 -0
- package/dist/__tests__/fixtures/profile/index.d.ts.map +1 -0
- package/dist/__tests__/fixtures/profile/index.js +10 -0
- package/dist/__tests__/fixtures/profile/index.js.map +1 -0
- package/dist/__tests__/fixtures/profile/types.d.ts +53 -0
- package/dist/__tests__/fixtures/profile/types.d.ts.map +1 -0
- package/dist/__tests__/fixtures/profile/types.js +4 -0
- package/dist/__tests__/fixtures/profile/types.js.map +1 -0
- package/dist/auth/errors.d.ts +82 -0
- package/dist/auth/errors.d.ts.map +1 -0
- package/dist/auth/errors.js +68 -0
- package/dist/auth/errors.js.map +1 -0
- package/dist/auth.d.ts +192 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +294 -0
- package/dist/auth.js.map +1 -0
- package/dist/config.d.ts +212 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +349 -0
- package/dist/config.js.map +1 -0
- package/dist/configLock.d.ts +50 -0
- package/dist/configLock.d.ts.map +1 -0
- package/dist/configLock.js +88 -0
- package/dist/configLock.js.map +1 -0
- package/dist/configWriter.d.ts +97 -0
- package/dist/configWriter.d.ts.map +1 -0
- package/dist/configWriter.js +687 -0
- package/dist/configWriter.js.map +1 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/kill-switch.d.ts +161 -0
- package/dist/kill-switch.d.ts.map +1 -0
- package/dist/kill-switch.js +235 -0
- package/dist/kill-switch.js.map +1 -0
- package/dist/lib/date.d.ts +58 -0
- package/dist/lib/date.d.ts.map +1 -0
- package/dist/lib/date.js +104 -0
- package/dist/lib/date.js.map +1 -0
- package/dist/lib/diagnostic-log.d.ts +159 -0
- package/dist/lib/diagnostic-log.d.ts.map +1 -0
- package/dist/lib/diagnostic-log.js +186 -0
- package/dist/lib/diagnostic-log.js.map +1 -0
- package/dist/lib/package-version.d.ts +19 -0
- package/dist/lib/package-version.d.ts.map +1 -0
- package/dist/lib/package-version.js +38 -0
- package/dist/lib/package-version.js.map +1 -0
- package/dist/lib/redact.d.ts +153 -0
- package/dist/lib/redact.d.ts.map +1 -0
- package/dist/lib/redact.js +207 -0
- package/dist/lib/redact.js.map +1 -0
- package/dist/lib/text.d.ts +14 -0
- package/dist/lib/text.d.ts.map +1 -0
- package/dist/lib/text.js +21 -0
- package/dist/lib/text.js.map +1 -0
- package/dist/lib/wire-shape.d.ts +131 -0
- package/dist/lib/wire-shape.d.ts.map +1 -0
- package/dist/lib/wire-shape.js +376 -0
- package/dist/lib/wire-shape.js.map +1 -0
- package/dist/onepassword.d.ts +29 -0
- package/dist/onepassword.d.ts.map +1 -0
- package/dist/onepassword.js +112 -0
- package/dist/onepassword.js.map +1 -0
- package/dist/services/_shared/transport.d.ts +148 -0
- package/dist/services/_shared/transport.d.ts.map +1 -0
- package/dist/services/_shared/transport.js +102 -0
- package/dist/services/_shared/transport.js.map +1 -0
- package/dist/services/applications/index.d.ts +210 -0
- package/dist/services/applications/index.d.ts.map +1 -0
- package/dist/services/applications/index.js +240 -0
- package/dist/services/applications/index.js.map +1 -0
- package/dist/services/availability/index.d.ts +254 -0
- package/dist/services/availability/index.d.ts.map +1 -0
- package/dist/services/availability/index.js +310 -0
- package/dist/services/availability/index.js.map +1 -0
- package/dist/services/contracts/index.d.ts +132 -0
- package/dist/services/contracts/index.d.ts.map +1 -0
- package/dist/services/contracts/index.js +211 -0
- package/dist/services/contracts/index.js.map +1 -0
- package/dist/services/engagements/index.d.ts +504 -0
- package/dist/services/engagements/index.d.ts.map +1 -0
- package/dist/services/engagements/index.js +613 -0
- package/dist/services/engagements/index.js.map +1 -0
- package/dist/services/jobs/index.d.ts +490 -0
- package/dist/services/jobs/index.d.ts.map +1 -0
- package/dist/services/jobs/index.js +753 -0
- package/dist/services/jobs/index.js.map +1 -0
- package/dist/services/payments/index.d.ts +415 -0
- package/dist/services/payments/index.d.ts.map +1 -0
- package/dist/services/payments/index.js +636 -0
- package/dist/services/payments/index.js.map +1 -0
- package/dist/services/profile/__tests__/fixtures.d.ts +214 -0
- package/dist/services/profile/__tests__/fixtures.d.ts.map +1 -0
- package/dist/services/profile/__tests__/fixtures.js +176 -0
- package/dist/services/profile/__tests__/fixtures.js.map +1 -0
- package/dist/services/profile/basic/index.d.ts +390 -0
- package/dist/services/profile/basic/index.d.ts.map +1 -0
- package/dist/services/profile/basic/index.js +1007 -0
- package/dist/services/profile/basic/index.js.map +1 -0
- package/dist/services/profile/certifications/index.d.ts +74 -0
- package/dist/services/profile/certifications/index.d.ts.map +1 -0
- package/dist/services/profile/certifications/index.js +169 -0
- package/dist/services/profile/certifications/index.js.map +1 -0
- package/dist/services/profile/education/index.d.ts +73 -0
- package/dist/services/profile/education/index.d.ts.map +1 -0
- package/dist/services/profile/education/index.js +168 -0
- package/dist/services/profile/education/index.js.map +1 -0
- package/dist/services/profile/employment/index.d.ts +111 -0
- package/dist/services/profile/employment/index.d.ts.map +1 -0
- package/dist/services/profile/employment/index.js +202 -0
- package/dist/services/profile/employment/index.js.map +1 -0
- package/dist/services/profile/external/index.d.ts +219 -0
- package/dist/services/profile/external/index.d.ts.map +1 -0
- package/dist/services/profile/external/index.js +560 -0
- package/dist/services/profile/external/index.js.map +1 -0
- package/dist/services/profile/index.d.ts +24 -0
- package/dist/services/profile/index.d.ts.map +1 -0
- package/dist/services/profile/index.js +26 -0
- package/dist/services/profile/index.js.map +1 -0
- package/dist/services/profile/industries/index.d.ts +130 -0
- package/dist/services/profile/industries/index.d.ts.map +1 -0
- package/dist/services/profile/industries/index.js +292 -0
- package/dist/services/profile/industries/index.js.map +1 -0
- package/dist/services/profile/portfolio/index.d.ts +352 -0
- package/dist/services/profile/portfolio/index.d.ts.map +1 -0
- package/dist/services/profile/portfolio/index.js +833 -0
- package/dist/services/profile/portfolio/index.js.map +1 -0
- package/dist/services/profile/resume/index.d.ts +60 -0
- package/dist/services/profile/resume/index.d.ts.map +1 -0
- package/dist/services/profile/resume/index.js +212 -0
- package/dist/services/profile/resume/index.js.map +1 -0
- package/dist/services/profile/reviews/index.d.ts +137 -0
- package/dist/services/profile/reviews/index.d.ts.map +1 -0
- package/dist/services/profile/reviews/index.js +431 -0
- package/dist/services/profile/reviews/index.js.map +1 -0
- package/dist/services/profile/shared.d.ts +127 -0
- package/dist/services/profile/shared.d.ts.map +1 -0
- package/dist/services/profile/shared.js +155 -0
- package/dist/services/profile/shared.js.map +1 -0
- package/dist/services/profile/skills/index.d.ts +212 -0
- package/dist/services/profile/skills/index.d.ts.map +1 -0
- package/dist/services/profile/skills/index.js +461 -0
- package/dist/services/profile/skills/index.js.map +1 -0
- package/dist/services/profile/visas/index.d.ts +74 -0
- package/dist/services/profile/visas/index.d.ts.map +1 -0
- package/dist/services/profile/visas/index.js +306 -0
- package/dist/services/profile/visas/index.js.map +1 -0
- package/dist/services/timesheet/index.d.ts +326 -0
- package/dist/services/timesheet/index.d.ts.map +1 -0
- package/dist/services/timesheet/index.js +324 -0
- package/dist/services/timesheet/index.js.map +1 -0
- package/dist/services/translations.d.ts +79 -0
- package/dist/services/translations.d.ts.map +1 -0
- package/dist/services/translations.js +136 -0
- package/dist/services/translations.js.map +1 -0
- package/dist/transport-resilience.d.ts +136 -0
- package/dist/transport-resilience.d.ts.map +1 -0
- package/dist/transport-resilience.js +247 -0
- package/dist/transport-resilience.js.map +1 -0
- package/dist/transport.d.ts +408 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +691 -0
- package/dist/transport.js.map +1 -0
- package/dist/types.d.ts +41 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +18 -0
- package/dist/types.js.map +1 -0
- package/package.json +40 -12
- package/index.js +0 -7
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Source pattern for a 1Password item reference, exported as a STRING (not
|
|
4
|
+
* a `RegExp`) so non-schema consumers (the CLI's interactive `auth init`
|
|
5
|
+
* flow's validation closure) can adopt the canonical pattern without
|
|
6
|
+
* pulling the entire Zod schema into their bundle. Analogous to
|
|
7
|
+
* {@link ./lib/redact.ts#BEARER_PATTERN_SOURCE}.
|
|
8
|
+
*
|
|
9
|
+
* Forms accepted:
|
|
10
|
+
* - `op://VAULT/ITEM` (2-segment) — relies on `op` CLI default-account or
|
|
11
|
+
* `OP_ACCOUNT` env. Works for single-account setups.
|
|
12
|
+
* - `op://ACCOUNT/VAULT/ITEM` (3-segment) — selects a specific account
|
|
13
|
+
* when the user has multiple `op` accounts configured. ACCOUNT is
|
|
14
|
+
* forwarded verbatim to `op item get --account` (accepts UUID,
|
|
15
|
+
* shorthand, or sign-in email — `op` CLI validates).
|
|
16
|
+
*
|
|
17
|
+
* Per-field references (`op://VAULT/ITEM/FIELD` or 4+ segments) are
|
|
18
|
+
* deliberately NOT supported — TTCtl always reads `username` and `password`
|
|
19
|
+
* from a single LOGIN-category item. The pattern rejects 4+ segments.
|
|
20
|
+
*/
|
|
21
|
+
export declare const OP_REF_PATTERN_SOURCE: "^op://(?:[^/]+/)?[^/]+/[^/]+$";
|
|
22
|
+
/**
|
|
23
|
+
* Canonical user-facing hint paired with {@link OP_REF_PATTERN_SOURCE}.
|
|
24
|
+
* Used as the Zod error message AND as the CLI's interactive-prompt
|
|
25
|
+
* validation hint so both surfaces speak the same language. Phrased as a
|
|
26
|
+
* standalone sentence (no field-name prefix) because the schema's
|
|
27
|
+
* `formatLoadValidationError` adds the field path (`auth.credentials:`) and
|
|
28
|
+
* the CLI prompt has its own context.
|
|
29
|
+
*/
|
|
30
|
+
export declare const OP_REF_PATTERN_HINT: "Expected op://[account/]vault/item \u2014 no /field suffix";
|
|
31
|
+
/**
|
|
32
|
+
* Literal credentials in YAML — discouraged for daily use; the 1Password form
|
|
33
|
+
* is recommended.
|
|
34
|
+
*
|
|
35
|
+
* Field name is `username` (matching 1Password's `USERNAME` purpose semantics
|
|
36
|
+
* and the issue's user-facing schema). The value must be a valid email since
|
|
37
|
+
* Toptal's `EmailPasswordSignIn` mutation only accepts emails.
|
|
38
|
+
*/
|
|
39
|
+
declare const LiteralCredentialsSchema: z.ZodObject<{
|
|
40
|
+
username: z.ZodEmail;
|
|
41
|
+
password: z.ZodString;
|
|
42
|
+
}, z.core.$strict>;
|
|
43
|
+
/**
|
|
44
|
+
* Polymorphic credentials value:
|
|
45
|
+
* - string → 1Password item reference (Form A)
|
|
46
|
+
* - object → literal { username, password } (Form B)
|
|
47
|
+
*
|
|
48
|
+
* The runtime `signIn()` flow collapses both into the internal `Credentials`
|
|
49
|
+
* shape `{ email, password }` (the GraphQL mutation parameter name is
|
|
50
|
+
* `email` — the YAML/op:// surface uses `username` to match 1Password's
|
|
51
|
+
* USERNAME purpose).
|
|
52
|
+
*/
|
|
53
|
+
export declare const AuthCredentialsSchema: z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
54
|
+
username: z.ZodEmail;
|
|
55
|
+
password: z.ZodString;
|
|
56
|
+
}, z.core.$strict>]>;
|
|
57
|
+
export type AuthCredentials = z.infer<typeof AuthCredentialsSchema>;
|
|
58
|
+
export type LiteralAuthCredentials = z.infer<typeof LiteralCredentialsSchema>;
|
|
59
|
+
/**
|
|
60
|
+
* `auth` block schema for runtime LOAD operations. Strict — rejects unknown
|
|
61
|
+
* fields and rejects an empty `auth: {}` (the user must author at least one
|
|
62
|
+
* of `credentials` or `token` for the runtime to be useful).
|
|
63
|
+
*
|
|
64
|
+
* Four valid lifecycle states (Forms A-D from the design doc):
|
|
65
|
+
*
|
|
66
|
+
* - **Form A** — `credentials: "op://..."` (1Password reference). No token
|
|
67
|
+
* yet — `auth signin` will write one.
|
|
68
|
+
* - **Form B** — `credentials: { username, password }` (literal). No token
|
|
69
|
+
* yet.
|
|
70
|
+
* - **Form C** — `token: "..."` only (no credentials). The bearer is
|
|
71
|
+
* supplied directly; `auth signin` is not applicable. Common in the E2E
|
|
72
|
+
* sandbox and in deployments that bootstrap a token out-of-band.
|
|
73
|
+
* - **Form D** — both `credentials` AND `token`. Post-signin state for
|
|
74
|
+
* Form A or B configs; the captured bearer is persisted alongside the
|
|
75
|
+
* credentials.
|
|
76
|
+
*/
|
|
77
|
+
declare const AuthBlockLoadSchema: z.ZodObject<{
|
|
78
|
+
credentials: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
79
|
+
username: z.ZodEmail;
|
|
80
|
+
password: z.ZodString;
|
|
81
|
+
}, z.core.$strict>]>>;
|
|
82
|
+
token: z.ZodOptional<z.ZodString>;
|
|
83
|
+
}, z.core.$strict>;
|
|
84
|
+
export type AuthBlock = z.infer<typeof AuthBlockLoadSchema>;
|
|
85
|
+
/**
|
|
86
|
+
* Top-level config schema (load form). Single config; no profiles.
|
|
87
|
+
*
|
|
88
|
+
* Compared to the prior shape:
|
|
89
|
+
* - Removed top-level `auth` polymorphic value (string or `{email,password}`)
|
|
90
|
+
* in favor of a structured `auth: { credentials?, token? }` block.
|
|
91
|
+
* - Removed `auth-token-path` field — the captured bearer now lives inside
|
|
92
|
+
* the same YAML file at `auth.token`. Project-scoped configs are routed
|
|
93
|
+
* via direnv (see README § Configuration).
|
|
94
|
+
*/
|
|
95
|
+
export declare const ConfigLoadSchema: z.ZodObject<{
|
|
96
|
+
auth: z.ZodObject<{
|
|
97
|
+
credentials: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
98
|
+
username: z.ZodEmail;
|
|
99
|
+
password: z.ZodString;
|
|
100
|
+
}, z.core.$strict>]>>;
|
|
101
|
+
token: z.ZodOptional<z.ZodString>;
|
|
102
|
+
}, z.core.$strict>;
|
|
103
|
+
}, z.core.$strict>;
|
|
104
|
+
/**
|
|
105
|
+
* Top-level config schema (write form). Permits empty `auth: {}` for the
|
|
106
|
+
* post-signout transient state when the source was Form C.
|
|
107
|
+
*/
|
|
108
|
+
export declare const ConfigWriteSchema: z.ZodObject<{
|
|
109
|
+
auth: z.ZodObject<{
|
|
110
|
+
credentials: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
111
|
+
username: z.ZodEmail;
|
|
112
|
+
password: z.ZodString;
|
|
113
|
+
}, z.core.$strict>]>>;
|
|
114
|
+
token: z.ZodOptional<z.ZodString>;
|
|
115
|
+
}, z.core.$strict>;
|
|
116
|
+
}, z.core.$strict>;
|
|
117
|
+
/**
|
|
118
|
+
* Runtime-validated config object (load form). The shape clients read from
|
|
119
|
+
* disk after `loadConfigFile`.
|
|
120
|
+
*/
|
|
121
|
+
export type TtctlConfig = z.infer<typeof ConfigLoadSchema>;
|
|
122
|
+
/**
|
|
123
|
+
* Permissive config shape used for write-back (`persistAuthToken`,
|
|
124
|
+
* `signOut`). Empty `auth: {}` is acceptable mid-lifecycle.
|
|
125
|
+
*/
|
|
126
|
+
export type TtctlConfigWritable = z.infer<typeof ConfigWriteSchema>;
|
|
127
|
+
/**
|
|
128
|
+
* Discriminator on `ConfigError` so consumers can switch on error class
|
|
129
|
+
* without pattern-matching prose messages.
|
|
130
|
+
*
|
|
131
|
+
* - `NO_CREDS` — config not found via the resolution chain, or the
|
|
132
|
+
* chosen path doesn't exist on disk.
|
|
133
|
+
* - `PARSE` — YAML parse error (malformed file).
|
|
134
|
+
* - `VALIDATION` — schema validation error (wrong shape, missing fields).
|
|
135
|
+
* - `PERMISSION` — file exists but is not accessible (EACCES / EPERM), OR
|
|
136
|
+
* file is at an unsafe location (sync-root, symlink) per
|
|
137
|
+
* the security gates in the resolution chain.
|
|
138
|
+
* - `LOCKED` — advisory write-back lock could not be acquired within
|
|
139
|
+
* the contention budget (≤1s). Another ttctl process
|
|
140
|
+
* (CLI signin or long-running MCP tool call) holds the
|
|
141
|
+
* sibling lockfile. Retry the operation.
|
|
142
|
+
*/
|
|
143
|
+
export type ConfigErrorCode = "NO_CREDS" | "PARSE" | "VALIDATION" | "PERMISSION" | "LOCKED";
|
|
144
|
+
export declare class ConfigError extends Error {
|
|
145
|
+
readonly code: ConfigErrorCode;
|
|
146
|
+
readonly path?: string | undefined;
|
|
147
|
+
readonly name = "ConfigError";
|
|
148
|
+
constructor(message: string, code: ConfigErrorCode, path?: string | undefined);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Read, parse, and validate the config file at `path`.
|
|
152
|
+
*
|
|
153
|
+
* Pre-flight `fs.stat`:
|
|
154
|
+
* - ENOENT → `ConfigError(code: NO_CREDS)` so a missing explicit path or
|
|
155
|
+
* `TTCTL_CONFIG_FILE` value surfaces uniformly with the
|
|
156
|
+
* "no config found anywhere" branch in `resolveConfig`.
|
|
157
|
+
* - EACCES / EPERM → `ConfigError(code: PERMISSION)`.
|
|
158
|
+
* - Mode-strictness gate (POSIX): refuse to LOAD when the file is
|
|
159
|
+
* world-writable (`mode & 0o002 != 0` — covers 0666, 0777, etc.).
|
|
160
|
+
* Closes a TOCTOU window on credential reads. Throws
|
|
161
|
+
* `ConfigError(PERMISSION)` with the actual mode in the error message.
|
|
162
|
+
* - Mode-warning (POSIX): emit a stderr warning when group/world bits
|
|
163
|
+
* other than world-writable are set (e.g. 0644). Non-fatal; the file
|
|
164
|
+
* may carry a 1Password reference (low-risk on its own) or, in literal
|
|
165
|
+
* form, a plaintext password — `0o600` is the right posture.
|
|
166
|
+
*/
|
|
167
|
+
export declare function loadConfigFile(path: string): TtctlConfig;
|
|
168
|
+
/**
|
|
169
|
+
* Options for `resolveConfig`. Currently only `path`; kept as an options
|
|
170
|
+
* object so future flags don't require another signature break.
|
|
171
|
+
*/
|
|
172
|
+
export interface ResolveConfigOptions {
|
|
173
|
+
/**
|
|
174
|
+
* Explicit config path. Wins over `TTCTL_CONFIG_FILE` and the home default.
|
|
175
|
+
* Used verbatim — no existence pre-check (`loadConfigFile` surfaces ENOENT
|
|
176
|
+
* as `code: 'NO_CREDS'`).
|
|
177
|
+
*/
|
|
178
|
+
path?: string;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Deterministic config-path resolution.
|
|
182
|
+
*
|
|
183
|
+
* Precedence (highest → lowest):
|
|
184
|
+
*
|
|
185
|
+
* 1. Explicit `path` argument (passed by the caller — e.g., `--config`
|
|
186
|
+
* flag from #93). Used verbatim, no existence check.
|
|
187
|
+
* 2. `TTCTL_CONFIG_FILE` env var. Same verbatim treatment as `path`.
|
|
188
|
+
* 3. `~/.ttctl.yaml` (POSIX home dotfile), when the file exists on disk.
|
|
189
|
+
*
|
|
190
|
+
* Returns `null` when none of the above produce a path. Callers that want
|
|
191
|
+
* project-scoped configs use `direnv` to set `TTCTL_CONFIG_FILE` per
|
|
192
|
+
* directory — the CWD `./.ttctl.yaml` is NOT consulted (closed since #92).
|
|
193
|
+
*
|
|
194
|
+
* NOTE: XDG paths (`$XDG_CONFIG_HOME/ttctl/config.yaml`,
|
|
195
|
+
* `~/.config/ttctl/config.yaml`) are NO LONGER consulted. The single-file
|
|
196
|
+
* model places the captured bearer in the same YAML; `~/.ttctl.yaml` is
|
|
197
|
+
* the canonical home location.
|
|
198
|
+
*/
|
|
199
|
+
export declare function discoverConfigPath(path?: string): string | null;
|
|
200
|
+
/**
|
|
201
|
+
* Convenience: discover and load.
|
|
202
|
+
*
|
|
203
|
+
* When the resolution chain returns `null` AND a `./.ttctl.yaml` exists in
|
|
204
|
+
* `process.cwd()`, surface a deprecation message pointing the user at the
|
|
205
|
+
* supported escape hatches. The CWD file is NEVER auto-loaded.
|
|
206
|
+
*/
|
|
207
|
+
export declare function resolveConfig(options?: ResolveConfigOptions): {
|
|
208
|
+
config: TtctlConfig;
|
|
209
|
+
path: string;
|
|
210
|
+
};
|
|
211
|
+
export {};
|
|
212
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,qBAAqB,EAAG,+BAAwC,CAAC;AAE9E;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,EAAG,4DAAgE,CAAC;AAIpG;;;;;;;GAOG;AACH,QAAA,MAAM,wBAAwB;;;kBAKnB,CAAC;AAEZ;;;;;;;;;GASG;AACH,eAAO,MAAM,qBAAqB;;;oBAAkE,CAAC;AAErG,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AACpE,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAE9E;;;;;;;;;;;;;;;;;GAiBG;AACH,QAAA,MAAM,mBAAmB;;;;;;kBAgBrB,CAAC;AAEL,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAiB5D;;;;;;;;;GASG;AACH,eAAO,MAAM,gBAAgB;;;;;;;;kBAIlB,CAAC;AAEZ;;;GAGG;AACH,eAAO,MAAM,iBAAiB;;;;;;;;kBAInB,CAAC;AAEZ;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE3D;;;GAGG;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAEpE;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,eAAe,GAAG,UAAU,GAAG,OAAO,GAAG,YAAY,GAAG,YAAY,GAAG,QAAQ,CAAC;AAE5F,qBAAa,WAAY,SAAQ,KAAK;aAIlB,IAAI,EAAE,eAAe;aACrB,IAAI,CAAC,EAAE,MAAM;IAJ/B,SAAkB,IAAI,iBAAiB;gBAErC,OAAO,EAAE,MAAM,EACC,IAAI,EAAE,eAAe,EACrB,IAAI,CAAC,EAAE,MAAM,YAAA;CAIhC;AAoFD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAkExD;AAED;;;GAGG;AACH,MAAM,WAAW,oBAAoB;IACnC;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAU/D;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,OAAO,GAAE,oBAAyB,GAAG;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAkBvG"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Copyright (C) 2026 Oleksii PELYKH
|
|
3
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
6
|
+
import { parse as parseYaml } from "yaml";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
/**
|
|
9
|
+
* Source pattern for a 1Password item reference, exported as a STRING (not
|
|
10
|
+
* a `RegExp`) so non-schema consumers (the CLI's interactive `auth init`
|
|
11
|
+
* flow's validation closure) can adopt the canonical pattern without
|
|
12
|
+
* pulling the entire Zod schema into their bundle. Analogous to
|
|
13
|
+
* {@link ./lib/redact.ts#BEARER_PATTERN_SOURCE}.
|
|
14
|
+
*
|
|
15
|
+
* Forms accepted:
|
|
16
|
+
* - `op://VAULT/ITEM` (2-segment) — relies on `op` CLI default-account or
|
|
17
|
+
* `OP_ACCOUNT` env. Works for single-account setups.
|
|
18
|
+
* - `op://ACCOUNT/VAULT/ITEM` (3-segment) — selects a specific account
|
|
19
|
+
* when the user has multiple `op` accounts configured. ACCOUNT is
|
|
20
|
+
* forwarded verbatim to `op item get --account` (accepts UUID,
|
|
21
|
+
* shorthand, or sign-in email — `op` CLI validates).
|
|
22
|
+
*
|
|
23
|
+
* Per-field references (`op://VAULT/ITEM/FIELD` or 4+ segments) are
|
|
24
|
+
* deliberately NOT supported — TTCtl always reads `username` and `password`
|
|
25
|
+
* from a single LOGIN-category item. The pattern rejects 4+ segments.
|
|
26
|
+
*/
|
|
27
|
+
export const OP_REF_PATTERN_SOURCE = "^op://(?:[^/]+/)?[^/]+/[^/]+$";
|
|
28
|
+
/**
|
|
29
|
+
* Canonical user-facing hint paired with {@link OP_REF_PATTERN_SOURCE}.
|
|
30
|
+
* Used as the Zod error message AND as the CLI's interactive-prompt
|
|
31
|
+
* validation hint so both surfaces speak the same language. Phrased as a
|
|
32
|
+
* standalone sentence (no field-name prefix) because the schema's
|
|
33
|
+
* `formatLoadValidationError` adds the field path (`auth.credentials:`) and
|
|
34
|
+
* the CLI prompt has its own context.
|
|
35
|
+
*/
|
|
36
|
+
export const OP_REF_PATTERN_HINT = "Expected op://[account/]vault/item — no /field suffix";
|
|
37
|
+
const OnePasswordReferenceSchema = z.string().regex(new RegExp(OP_REF_PATTERN_SOURCE), OP_REF_PATTERN_HINT);
|
|
38
|
+
/**
|
|
39
|
+
* Literal credentials in YAML — discouraged for daily use; the 1Password form
|
|
40
|
+
* is recommended.
|
|
41
|
+
*
|
|
42
|
+
* Field name is `username` (matching 1Password's `USERNAME` purpose semantics
|
|
43
|
+
* and the issue's user-facing schema). The value must be a valid email since
|
|
44
|
+
* Toptal's `EmailPasswordSignIn` mutation only accepts emails.
|
|
45
|
+
*/
|
|
46
|
+
const LiteralCredentialsSchema = z
|
|
47
|
+
.object({
|
|
48
|
+
username: z.email({ message: "auth.credentials.username must be a valid email address" }),
|
|
49
|
+
password: z.string().min(1, { message: "auth.credentials.password must be a non-empty string" }),
|
|
50
|
+
})
|
|
51
|
+
.strict();
|
|
52
|
+
/**
|
|
53
|
+
* Polymorphic credentials value:
|
|
54
|
+
* - string → 1Password item reference (Form A)
|
|
55
|
+
* - object → literal { username, password } (Form B)
|
|
56
|
+
*
|
|
57
|
+
* The runtime `signIn()` flow collapses both into the internal `Credentials`
|
|
58
|
+
* shape `{ email, password }` (the GraphQL mutation parameter name is
|
|
59
|
+
* `email` — the YAML/op:// surface uses `username` to match 1Password's
|
|
60
|
+
* USERNAME purpose).
|
|
61
|
+
*/
|
|
62
|
+
export const AuthCredentialsSchema = z.union([OnePasswordReferenceSchema, LiteralCredentialsSchema]);
|
|
63
|
+
/**
|
|
64
|
+
* `auth` block schema for runtime LOAD operations. Strict — rejects unknown
|
|
65
|
+
* fields and rejects an empty `auth: {}` (the user must author at least one
|
|
66
|
+
* of `credentials` or `token` for the runtime to be useful).
|
|
67
|
+
*
|
|
68
|
+
* Four valid lifecycle states (Forms A-D from the design doc):
|
|
69
|
+
*
|
|
70
|
+
* - **Form A** — `credentials: "op://..."` (1Password reference). No token
|
|
71
|
+
* yet — `auth signin` will write one.
|
|
72
|
+
* - **Form B** — `credentials: { username, password }` (literal). No token
|
|
73
|
+
* yet.
|
|
74
|
+
* - **Form C** — `token: "..."` only (no credentials). The bearer is
|
|
75
|
+
* supplied directly; `auth signin` is not applicable. Common in the E2E
|
|
76
|
+
* sandbox and in deployments that bootstrap a token out-of-band.
|
|
77
|
+
* - **Form D** — both `credentials` AND `token`. Post-signin state for
|
|
78
|
+
* Form A or B configs; the captured bearer is persisted alongside the
|
|
79
|
+
* credentials.
|
|
80
|
+
*/
|
|
81
|
+
const AuthBlockLoadSchema = z
|
|
82
|
+
.object({
|
|
83
|
+
credentials: AuthCredentialsSchema.optional(),
|
|
84
|
+
token: z.string().min(1, { message: "auth.token must be a non-empty string" }).optional(),
|
|
85
|
+
})
|
|
86
|
+
.strict()
|
|
87
|
+
.superRefine((auth, ctx) => {
|
|
88
|
+
if (auth.credentials === undefined && auth.token === undefined) {
|
|
89
|
+
ctx.addIssue({
|
|
90
|
+
code: "custom",
|
|
91
|
+
message: "auth must contain at least one of 'credentials' (op:// reference or { username, password } object) " +
|
|
92
|
+
"or 'token' (bearer string captured by `ttctl auth signin`)",
|
|
93
|
+
path: [],
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
/**
|
|
98
|
+
* `auth` block schema for runtime WRITE operations. Permissive — both
|
|
99
|
+
* `credentials` and `token` are optional, AND empty `auth: {}` is accepted.
|
|
100
|
+
*
|
|
101
|
+
* Used by `persistAuthToken` and the signout flow during the brief window
|
|
102
|
+
* between mutation and disk-write. The strict load schema rejects empty
|
|
103
|
+
* `auth: {}` at the next file-load — see DQ-6 in the design doc.
|
|
104
|
+
*/
|
|
105
|
+
const AuthBlockWriteSchema = z
|
|
106
|
+
.object({
|
|
107
|
+
credentials: AuthCredentialsSchema.optional(),
|
|
108
|
+
token: z.string().min(1).optional(),
|
|
109
|
+
})
|
|
110
|
+
.strict();
|
|
111
|
+
/**
|
|
112
|
+
* Top-level config schema (load form). Single config; no profiles.
|
|
113
|
+
*
|
|
114
|
+
* Compared to the prior shape:
|
|
115
|
+
* - Removed top-level `auth` polymorphic value (string or `{email,password}`)
|
|
116
|
+
* in favor of a structured `auth: { credentials?, token? }` block.
|
|
117
|
+
* - Removed `auth-token-path` field — the captured bearer now lives inside
|
|
118
|
+
* the same YAML file at `auth.token`. Project-scoped configs are routed
|
|
119
|
+
* via direnv (see README § Configuration).
|
|
120
|
+
*/
|
|
121
|
+
export const ConfigLoadSchema = z
|
|
122
|
+
.object({
|
|
123
|
+
auth: AuthBlockLoadSchema,
|
|
124
|
+
})
|
|
125
|
+
.strict();
|
|
126
|
+
/**
|
|
127
|
+
* Top-level config schema (write form). Permits empty `auth: {}` for the
|
|
128
|
+
* post-signout transient state when the source was Form C.
|
|
129
|
+
*/
|
|
130
|
+
export const ConfigWriteSchema = z
|
|
131
|
+
.object({
|
|
132
|
+
auth: AuthBlockWriteSchema,
|
|
133
|
+
})
|
|
134
|
+
.strict();
|
|
135
|
+
export class ConfigError extends Error {
|
|
136
|
+
code;
|
|
137
|
+
path;
|
|
138
|
+
name = "ConfigError";
|
|
139
|
+
constructor(message, code, path) {
|
|
140
|
+
super(message);
|
|
141
|
+
this.code = code;
|
|
142
|
+
this.path = path;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Map a Zod issue path to a human-friendly field reference for surfacing
|
|
147
|
+
* validation failures. Walks the issue's path array to build a dotted-prose
|
|
148
|
+
* descriptor (e.g. `["auth", "credentials", "password"]` → `auth.credentials.password`).
|
|
149
|
+
*
|
|
150
|
+
* The empty-path case (refinement-level errors) returns `auth` as the
|
|
151
|
+
* operative scope so the message reads naturally.
|
|
152
|
+
*/
|
|
153
|
+
function formatIssuePath(path) {
|
|
154
|
+
if (path.length === 0)
|
|
155
|
+
return "auth";
|
|
156
|
+
const parts = [];
|
|
157
|
+
for (const segment of path) {
|
|
158
|
+
if (typeof segment === "number") {
|
|
159
|
+
parts[parts.length - 1] = `${parts[parts.length - 1] ?? ""}[${segment.toString()}]`;
|
|
160
|
+
}
|
|
161
|
+
else if (typeof segment === "string") {
|
|
162
|
+
parts.push(segment);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
// symbol key — zod permits this in object schemas but our config has
|
|
166
|
+
// no symbol-keyed fields. Render via .toString() so the message is
|
|
167
|
+
// still readable rather than dropping the segment.
|
|
168
|
+
parts.push(segment.toString());
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return parts.join(".");
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Detect the legacy pre-#107 config shape (`auth` as a top-level string,
|
|
175
|
+
* e.g. `auth: "op://Personal/ttctl"`) and return a tailored migration error,
|
|
176
|
+
* or `null` if the parsed YAML does not match this shape.
|
|
177
|
+
*
|
|
178
|
+
* The post-#107 `ConfigLoadSchema` rejects `auth: <string>` at union
|
|
179
|
+
* discrimination, so `superRefine` never fires for this case — the dedicated
|
|
180
|
+
* migration message MUST land before Zod runs. Detection point is mechanical:
|
|
181
|
+
* after `parseYaml(raw)`, before `ConfigLoadSchema.safeParse(parsed)`.
|
|
182
|
+
*
|
|
183
|
+
* Other malformed shapes (typos, missing fields, wrong scalar types other
|
|
184
|
+
* than string at `auth`) continue to flow through `formatLoadValidationError`.
|
|
185
|
+
*
|
|
186
|
+
* The error code stays `VALIDATION` — preserves the CLI/MCP exit-code
|
|
187
|
+
* contract from #107. The migration text is part of `ConfigError.message`,
|
|
188
|
+
* not a separate stderr emit, so JSON wire-format consumers see one
|
|
189
|
+
* coherent `{ code, message }` payload.
|
|
190
|
+
*/
|
|
191
|
+
function detectLegacyShape(parsed, path) {
|
|
192
|
+
if (parsed !== null && typeof parsed === "object" && "auth" in parsed && typeof parsed.auth === "string") {
|
|
193
|
+
const literal = parsed.auth;
|
|
194
|
+
return new ConfigError([
|
|
195
|
+
`Config file ${path} uses the legacy pre-#107 shape:`,
|
|
196
|
+
``,
|
|
197
|
+
` auth: "${literal}"`,
|
|
198
|
+
``,
|
|
199
|
+
`Migrate to the structured shape:`,
|
|
200
|
+
``,
|
|
201
|
+
` auth:`,
|
|
202
|
+
` credentials: "${literal}"`,
|
|
203
|
+
``,
|
|
204
|
+
`See CLAUDE.md § Auth Model for the canonical migration recipe.`,
|
|
205
|
+
].join("\n"), "VALIDATION", path);
|
|
206
|
+
}
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Render a Zod safe-parse failure into a single-line `ConfigError` message
|
|
211
|
+
* that names the failing branch / field where possible. Defends against the
|
|
212
|
+
* naked `z.union` "Invalid input" UX.
|
|
213
|
+
*/
|
|
214
|
+
function formatLoadValidationError(err, path) {
|
|
215
|
+
const lines = [];
|
|
216
|
+
for (const issue of err.issues) {
|
|
217
|
+
const fieldPath = formatIssuePath(issue.path);
|
|
218
|
+
lines.push(`${fieldPath}: ${issue.message}`);
|
|
219
|
+
}
|
|
220
|
+
const summary = lines.length === 0 ? "Invalid config shape" : lines.join("; ");
|
|
221
|
+
return new ConfigError(`Config validation failed: ${summary}`, "VALIDATION", path);
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Read, parse, and validate the config file at `path`.
|
|
225
|
+
*
|
|
226
|
+
* Pre-flight `fs.stat`:
|
|
227
|
+
* - ENOENT → `ConfigError(code: NO_CREDS)` so a missing explicit path or
|
|
228
|
+
* `TTCTL_CONFIG_FILE` value surfaces uniformly with the
|
|
229
|
+
* "no config found anywhere" branch in `resolveConfig`.
|
|
230
|
+
* - EACCES / EPERM → `ConfigError(code: PERMISSION)`.
|
|
231
|
+
* - Mode-strictness gate (POSIX): refuse to LOAD when the file is
|
|
232
|
+
* world-writable (`mode & 0o002 != 0` — covers 0666, 0777, etc.).
|
|
233
|
+
* Closes a TOCTOU window on credential reads. Throws
|
|
234
|
+
* `ConfigError(PERMISSION)` with the actual mode in the error message.
|
|
235
|
+
* - Mode-warning (POSIX): emit a stderr warning when group/world bits
|
|
236
|
+
* other than world-writable are set (e.g. 0644). Non-fatal; the file
|
|
237
|
+
* may carry a 1Password reference (low-risk on its own) or, in literal
|
|
238
|
+
* form, a plaintext password — `0o600` is the right posture.
|
|
239
|
+
*/
|
|
240
|
+
export function loadConfigFile(path) {
|
|
241
|
+
const stat = (() => {
|
|
242
|
+
try {
|
|
243
|
+
return statSync(path);
|
|
244
|
+
}
|
|
245
|
+
catch (err) {
|
|
246
|
+
const e = err;
|
|
247
|
+
if (e.code === "ENOENT") {
|
|
248
|
+
throw new ConfigError(`Config file not found: ${path}`, "NO_CREDS", path);
|
|
249
|
+
}
|
|
250
|
+
if (e.code === "EACCES" || e.code === "EPERM") {
|
|
251
|
+
throw new ConfigError(`Cannot access config file (permission denied): ${path}`, "PERMISSION", path);
|
|
252
|
+
}
|
|
253
|
+
throw new ConfigError(`Cannot stat config file: ${e.message}`, "NO_CREDS", path);
|
|
254
|
+
}
|
|
255
|
+
})();
|
|
256
|
+
if (process.platform !== "win32") {
|
|
257
|
+
const mode = stat.mode & 0o777;
|
|
258
|
+
if ((mode & 0o002) !== 0) {
|
|
259
|
+
const modeStr = mode.toString(8).padStart(3, "0");
|
|
260
|
+
throw new ConfigError(`Refusing to load world-writable config file ${path} (mode 0${modeStr}). ` +
|
|
261
|
+
`The file may contain a captured bearer token; world-writable mode opens a TOCTOU window. ` +
|
|
262
|
+
`Run: chmod 600 ${path}`, "PERMISSION", path);
|
|
263
|
+
}
|
|
264
|
+
if ((mode & 0o077) !== 0) {
|
|
265
|
+
const modeStr = mode.toString(8).padStart(3, "0");
|
|
266
|
+
process.stderr.write(`WARNING: config file ${path} is group/world readable (mode 0${modeStr}). ` +
|
|
267
|
+
`It may contain a 1Password reference, plaintext password, or captured bearer. ` +
|
|
268
|
+
`Run: chmod 600 ${path}\n`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
let raw;
|
|
272
|
+
try {
|
|
273
|
+
raw = readFileSync(path, "utf8");
|
|
274
|
+
}
|
|
275
|
+
catch (err) {
|
|
276
|
+
const e = err;
|
|
277
|
+
if (e.code === "EACCES" || e.code === "EPERM") {
|
|
278
|
+
throw new ConfigError(`Cannot read config file (permission denied): ${path}`, "PERMISSION", path);
|
|
279
|
+
}
|
|
280
|
+
throw new ConfigError(`Cannot read config file: ${e.message}`, "NO_CREDS", path);
|
|
281
|
+
}
|
|
282
|
+
let parsed;
|
|
283
|
+
try {
|
|
284
|
+
parsed = parseYaml(raw);
|
|
285
|
+
}
|
|
286
|
+
catch (err) {
|
|
287
|
+
throw new ConfigError(`Invalid YAML: ${err.message}`, "PARSE", path);
|
|
288
|
+
}
|
|
289
|
+
const legacyError = detectLegacyShape(parsed, path);
|
|
290
|
+
if (legacyError !== null) {
|
|
291
|
+
throw legacyError;
|
|
292
|
+
}
|
|
293
|
+
const result = ConfigLoadSchema.safeParse(parsed);
|
|
294
|
+
if (!result.success) {
|
|
295
|
+
throw formatLoadValidationError(result.error, path);
|
|
296
|
+
}
|
|
297
|
+
return result.data;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Deterministic config-path resolution.
|
|
301
|
+
*
|
|
302
|
+
* Precedence (highest → lowest):
|
|
303
|
+
*
|
|
304
|
+
* 1. Explicit `path` argument (passed by the caller — e.g., `--config`
|
|
305
|
+
* flag from #93). Used verbatim, no existence check.
|
|
306
|
+
* 2. `TTCTL_CONFIG_FILE` env var. Same verbatim treatment as `path`.
|
|
307
|
+
* 3. `~/.ttctl.yaml` (POSIX home dotfile), when the file exists on disk.
|
|
308
|
+
*
|
|
309
|
+
* Returns `null` when none of the above produce a path. Callers that want
|
|
310
|
+
* project-scoped configs use `direnv` to set `TTCTL_CONFIG_FILE` per
|
|
311
|
+
* directory — the CWD `./.ttctl.yaml` is NOT consulted (closed since #92).
|
|
312
|
+
*
|
|
313
|
+
* NOTE: XDG paths (`$XDG_CONFIG_HOME/ttctl/config.yaml`,
|
|
314
|
+
* `~/.config/ttctl/config.yaml`) are NO LONGER consulted. The single-file
|
|
315
|
+
* model places the captured bearer in the same YAML; `~/.ttctl.yaml` is
|
|
316
|
+
* the canonical home location.
|
|
317
|
+
*/
|
|
318
|
+
export function discoverConfigPath(path) {
|
|
319
|
+
if (path !== undefined)
|
|
320
|
+
return path;
|
|
321
|
+
const envPath = process.env["TTCTL_CONFIG_FILE"];
|
|
322
|
+
if (envPath !== undefined && envPath !== "")
|
|
323
|
+
return envPath;
|
|
324
|
+
const homePath = join(homedir(), ".ttctl.yaml");
|
|
325
|
+
if (existsSync(homePath))
|
|
326
|
+
return homePath;
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Convenience: discover and load.
|
|
331
|
+
*
|
|
332
|
+
* When the resolution chain returns `null` AND a `./.ttctl.yaml` exists in
|
|
333
|
+
* `process.cwd()`, surface a deprecation message pointing the user at the
|
|
334
|
+
* supported escape hatches. The CWD file is NEVER auto-loaded.
|
|
335
|
+
*/
|
|
336
|
+
export function resolveConfig(options = {}) {
|
|
337
|
+
const path = discoverConfigPath(options.path);
|
|
338
|
+
if (path === null) {
|
|
339
|
+
const legacyCwdPath = resolve(process.cwd(), ".ttctl.yaml");
|
|
340
|
+
if (existsSync(legacyCwdPath)) {
|
|
341
|
+
throw new ConfigError(`No config found via --config, TTCTL_CONFIG_FILE, or ~/.ttctl.yaml. ` +
|
|
342
|
+
`Note: a CWD .ttctl.yaml was found at ${legacyCwdPath} but is not auto-discovered. ` +
|
|
343
|
+
`Set TTCTL_CONFIG_FILE=${legacyCwdPath} (e.g. via direnv) or move it to ~/.ttctl.yaml.`, "NO_CREDS");
|
|
344
|
+
}
|
|
345
|
+
throw new ConfigError("No config found. Pass --config <path>, set TTCTL_CONFIG_FILE, or place config at ~/.ttctl.yaml. See README for setup.", "NO_CREDS");
|
|
346
|
+
}
|
|
347
|
+
return { config: loadConfigFile(path), path };
|
|
348
|
+
}
|
|
349
|
+
//# 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,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handle returned by `acquireConfigLock`. The caller MUST `release()` the
|
|
3
|
+
* handle when done — the recommended pattern is `try { ... } finally { await
|
|
4
|
+
* handle.release(); }` so the lock is released on every error path
|
|
5
|
+
* (read-fail, parse-fail, mtime-drift, write-fail, rename-fail, post-rename
|
|
6
|
+
* verify-fail).
|
|
7
|
+
*
|
|
8
|
+
* `release` is idempotent in practice — `proper-lockfile` registers an
|
|
9
|
+
* exit-handler via `signal-exit` that auto-cleans the lock on process death,
|
|
10
|
+
* so a missed release doesn't strand the lock across runs. But explicit
|
|
11
|
+
* release is still required for the in-process retry semantics: a deferred
|
|
12
|
+
* release means the next `acquireConfigLock` in the SAME process would see
|
|
13
|
+
* the lock held by itself and deadlock.
|
|
14
|
+
*/
|
|
15
|
+
export interface ConfigLockHandle {
|
|
16
|
+
release: () => Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Acquire an advisory exclusive lock for `configPath`.
|
|
20
|
+
*
|
|
21
|
+
* Implementation: `proper-lockfile` creates `<configPath>.lock` as a
|
|
22
|
+
* sibling DIRECTORY via atomic `mkdir(2)`. The `mkdir` syscall is atomic on
|
|
23
|
+
* ext4, APFS, NTFS, and any other filesystem with POSIX-equivalent
|
|
24
|
+
* semantics — `EEXIST` is the contention signal.
|
|
25
|
+
*
|
|
26
|
+
* Why sibling-lockfile and NOT self-lock: the atomic-rename pattern in
|
|
27
|
+
* `performYamlMutation` swaps the inode at `<configPath>` mid-operation. A
|
|
28
|
+
* `flock(fd)` on the original inode does NOT survive the rename and a
|
|
29
|
+
* concurrent locker on the post-rename inode sees no contention. The
|
|
30
|
+
* sibling pattern sidesteps this — the `.lock` directory's existence is the
|
|
31
|
+
* lock signal, independent of the config file's inode.
|
|
32
|
+
*
|
|
33
|
+
* Cross-platform: works identically on Linux/macOS/Windows — `mkdir`-based
|
|
34
|
+
* atomic creation is portable. No Windows no-op needed (in contrast to the
|
|
35
|
+
* existing POSIX-mode logic in `loadConfigFile` and `performYamlMutation`,
|
|
36
|
+
* which IS Windows-skipped because mode bits aren't meaningful).
|
|
37
|
+
*
|
|
38
|
+
* Contention: ≤1.0s wall-clock budget per `LOCK_RETRY_OPTIONS`. On timeout,
|
|
39
|
+
* throws `ConfigError(LOCKED)` naming the path and suggesting retry — never
|
|
40
|
+
* blocks indefinitely.
|
|
41
|
+
*
|
|
42
|
+
* `realpath: false` skips proper-lockfile's pre-flight `realpath()` check.
|
|
43
|
+
* The caller has already resolved `configPath` to an absolute, symlink-free
|
|
44
|
+
* path via `assertSafePath`, AND the config file may not exist yet (e.g.,
|
|
45
|
+
* the lock is acquired BEFORE the stat baseline that throws ENOENT). With
|
|
46
|
+
* `realpath: true` (the library default), a missing file would surface as
|
|
47
|
+
* an opaque proper-lockfile error before our own ENOENT path runs.
|
|
48
|
+
*/
|
|
49
|
+
export declare function acquireConfigLock(configPath: string): Promise<ConfigLockHandle>;
|
|
50
|
+
//# sourceMappingURL=configLock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"configLock.d.ts","sourceRoot":"","sources":["../src/configLock.ts"],"names":[],"mappings":"AAOA;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AA6BD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAsB,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAiCrF"}
|