@doow/cli 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 (43) hide show
  1. package/README.md +75 -0
  2. package/dist/cjs/auth/api-key.js +159 -0
  3. package/dist/cjs/auth/api-key.js.map +1 -0
  4. package/dist/cjs/auth/detect.js +173 -0
  5. package/dist/cjs/auth/detect.js.map +1 -0
  6. package/dist/cjs/auth/device-flow.js +135 -0
  7. package/dist/cjs/auth/device-flow.js.map +1 -0
  8. package/dist/cjs/auth/keyring.js +118 -0
  9. package/dist/cjs/auth/keyring.js.map +1 -0
  10. package/dist/cjs/auth/pkce.js +243 -0
  11. package/dist/cjs/auth/pkce.js.map +1 -0
  12. package/dist/cjs/auth/refresh.js +203 -0
  13. package/dist/cjs/auth/refresh.js.map +1 -0
  14. package/dist/cjs/config/env.js +44 -0
  15. package/dist/cjs/config/env.js.map +1 -0
  16. package/dist/cjs/config/store.js +178 -0
  17. package/dist/cjs/config/store.js.map +1 -0
  18. package/dist/cjs/index.js +48 -0
  19. package/dist/cjs/index.js.map +1 -0
  20. package/dist/cli.cjs +34372 -0
  21. package/dist/cli.cjs.map +1 -0
  22. package/dist/esm/auth/api-key.js +154 -0
  23. package/dist/esm/auth/api-key.js.map +1 -0
  24. package/dist/esm/auth/detect.js +150 -0
  25. package/dist/esm/auth/detect.js.map +1 -0
  26. package/dist/esm/auth/device-flow.js +132 -0
  27. package/dist/esm/auth/device-flow.js.map +1 -0
  28. package/dist/esm/auth/keyring.js +116 -0
  29. package/dist/esm/auth/keyring.js.map +1 -0
  30. package/dist/esm/auth/pkce.js +220 -0
  31. package/dist/esm/auth/pkce.js.map +1 -0
  32. package/dist/esm/auth/refresh.js +198 -0
  33. package/dist/esm/auth/refresh.js.map +1 -0
  34. package/dist/esm/config/env.js +38 -0
  35. package/dist/esm/config/env.js.map +1 -0
  36. package/dist/esm/config/store.js +166 -0
  37. package/dist/esm/config/store.js.map +1 -0
  38. package/dist/esm/index.js +15 -0
  39. package/dist/esm/index.js.map +1 -0
  40. package/dist/mcp.cjs +8 -0
  41. package/dist/mcp.cjs.map +1 -0
  42. package/dist/types/index.d.ts +369 -0
  43. package/package.json +62 -0
@@ -0,0 +1,15 @@
1
+ export { clearProfileCredentials, deleteProfile, getActiveProfile, getConfigDir, getProfileCredentials, readConfig, readCredentials, setActiveProfile, setProfileCredentials, writeConfig, writeCredentials } from './config/store.js';
2
+ export { getApiUrl, isAgentMode, isCI, isTTY, shouldShowUI } from './config/env.js';
3
+ export { createCredentialStore } from './auth/keyring.js';
4
+ export { executePkceFlow, generatePkcePair } from './auth/pkce.js';
5
+ export { executeDeviceFlow } from './auth/device-flow.js';
6
+ export { authenticateWithApiKey, readTokenFromStdin, resolveAuth, validateApiKey } from './auth/api-key.js';
7
+ export { acquireLock, needsRefresh, refreshToken, releaseLock } from './auth/refresh.js';
8
+ export { canBindLocalhost, detectAuthMethod, executeAutoLogin } from './auth/detect.js';
9
+
10
+ // @doow/cli library entry point
11
+ // Re-exports for programmatic usage
12
+ const VERSION = '0.1.0';
13
+
14
+ export { VERSION };
15
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../../../src/index.ts"],"sourcesContent":[null],"names":[],"mappings":";;;;;;;;;AAAA;AACA;AAEO,MAAM,OAAO,GAAG;;;;"}
package/dist/mcp.cjs ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // MCP server entry point — placeholder for Phase 4 (E45)
5
+ // Will use @modelcontextprotocol/sdk for stdio + Streamable HTTP transport
6
+ console.error('doow mcp server: not yet implemented (Phase 4 — E45)');
7
+ process.exit(1);
8
+ //# sourceMappingURL=mcp.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp.cjs","sources":["../src/mcp.ts"],"sourcesContent":[null],"names":[],"mappings":";;;AAAA;AACA;AAEA,OAAO,CAAC,KAAK,CAAC,sDAAsD,CAAC;AACrE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;;"}
@@ -0,0 +1,369 @@
1
+ /**
2
+ * Profile — one named context (org + API endpoint)
3
+ */
4
+ interface Profile {
5
+ name: string;
6
+ apiUrl?: string;
7
+ organizationId?: string;
8
+ email?: string;
9
+ }
10
+ /**
11
+ * Top-level config stored in ~/.doow/config.json
12
+ */
13
+ interface DoowConfig {
14
+ activeProfile: string;
15
+ profiles: Record<string, Profile>;
16
+ }
17
+ /**
18
+ * Stored credentials for a single profile (tokens / API key)
19
+ */
20
+ interface ProfileCredentials {
21
+ accessToken?: string;
22
+ refreshToken?: string;
23
+ /** ISO-8601 datetime string */
24
+ expiresAt?: string;
25
+ apiKey?: string;
26
+ }
27
+ /**
28
+ * Top-level credentials stored in ~/.doow/credentials.json
29
+ */
30
+ interface Credentials {
31
+ profiles: Record<string, ProfileCredentials>;
32
+ }
33
+
34
+ /**
35
+ * Returns the Doow config directory path.
36
+ * Prefers $DOOW_CONFIG_DIR when set; falls back to ~/.doow.
37
+ * Creates the directory (mode 0o700) if it does not yet exist.
38
+ */
39
+ declare function getConfigDir(): Promise<string>;
40
+ /** Reads ~/.doow/config.json. Returns the default config if the file is absent. */
41
+ declare function readConfig(): Promise<DoowConfig>;
42
+ /** Writes ~/.doow/config.json atomically with 0o600 perms. */
43
+ declare function writeConfig(config: DoowConfig): Promise<void>;
44
+ /** Reads ~/.doow/credentials.json. Returns empty credentials if absent. */
45
+ declare function readCredentials(): Promise<Credentials>;
46
+ /** Writes ~/.doow/credentials.json atomically with 0o600 perms. */
47
+ declare function writeCredentials(creds: Credentials): Promise<void>;
48
+ /** Returns the currently active Profile object. */
49
+ declare function getActiveProfile(): Promise<Profile>;
50
+ /** Sets the active profile name and persists config. Throws if the profile doesn't exist. */
51
+ declare function setActiveProfile(name: string): Promise<void>;
52
+ /**
53
+ * Returns credentials for the given profile name.
54
+ * Defaults to the currently active profile if name is omitted.
55
+ */
56
+ declare function getProfileCredentials(profileName?: string): Promise<ProfileCredentials | undefined>;
57
+ /** Stores credentials for the given profile, merging with existing. */
58
+ declare function setProfileCredentials(profileName: string, creds: ProfileCredentials): Promise<void>;
59
+ /** Removes all credentials for the given profile. */
60
+ declare function clearProfileCredentials(profileName: string): Promise<void>;
61
+ /**
62
+ * Deletes a profile from config + credentials.
63
+ * Throws if the profile is currently active — caller must switch first.
64
+ */
65
+ declare function deleteProfile(name: string): Promise<void>;
66
+
67
+ /** True when stdout is an interactive terminal. */
68
+ declare function isTTY(): boolean;
69
+ /** True when running inside a CI environment (any common CI sets $CI). */
70
+ declare function isCI(): boolean;
71
+ /**
72
+ * True when the CLI is invoked by an automated agent rather than a human.
73
+ * Detected via $DOOW_AGENT_MODE env var or the --agent CLI flag (which
74
+ * Commander stores on the global options object as `process.env` is the
75
+ * canonical signal here — Commander integration is wired up in cli.ts).
76
+ */
77
+ declare function isAgentMode(): boolean;
78
+ /**
79
+ * True when interactive UI (spinners, prompts, color) should be shown.
80
+ * Requires a real TTY, no CI env, and not running in agent mode.
81
+ */
82
+ declare function shouldShowUI(): boolean;
83
+ /**
84
+ * Resolve the API base URL.
85
+ * Precedence: profile.apiUrl → $DOOW_API_URL → hardcoded default.
86
+ */
87
+ declare function getApiUrl(profile?: Profile): string;
88
+
89
+ interface CredentialStore {
90
+ get(profileName: string): Promise<ProfileCredentials | undefined>;
91
+ set(profileName: string, creds: ProfileCredentials): Promise<void>;
92
+ clear(profileName: string): Promise<void>;
93
+ backend(): 'keyring' | 'file';
94
+ }
95
+ /**
96
+ * Creates a CredentialStore backed by the system keyring when available,
97
+ * falling back to file storage silently when keytar is not installed or
98
+ * the system keyring does not respond within 3 seconds.
99
+ *
100
+ * Call this once at startup and reuse the returned instance.
101
+ */
102
+ declare function createCredentialStore(): Promise<CredentialStore>;
103
+
104
+ /**
105
+ * pkce.ts
106
+ *
107
+ * OAuth 2.0 PKCE (RFC 7636) interactive login flow for the Doow CLI.
108
+ *
109
+ * Steps:
110
+ * 1. Generate PKCE code_verifier + code_challenge
111
+ * 2. Generate CSRF state token
112
+ * 3. Start a localhost HTTP server on a random port
113
+ * 4. Open browser to the authorize URL
114
+ * 5. Wait for the OAuth callback (120 s default timeout)
115
+ * 6. Exchange authorization code for tokens
116
+ * 7. Store tokens via CredentialStore
117
+ * 8. Close server and return result
118
+ */
119
+
120
+ interface PkceFlowOptions {
121
+ apiUrl?: string;
122
+ profileName?: string;
123
+ credentialStore?: CredentialStore;
124
+ /** Milliseconds. Default 120_000 (2 minutes). */
125
+ timeout?: number;
126
+ }
127
+ interface PkceFlowResult {
128
+ accessToken: string;
129
+ refreshToken: string;
130
+ expiresAt: string;
131
+ }
132
+ /**
133
+ * Generates a PKCE code_verifier and code_challenge pair.
134
+ * - code_verifier: 32 random bytes, base64url-encoded (43 chars)
135
+ * - code_challenge: SHA-256 of verifier, base64url-encoded
136
+ */
137
+ declare function generatePkcePair(): {
138
+ codeVerifier: string;
139
+ codeChallenge: string;
140
+ };
141
+ /**
142
+ * Executes the full PKCE browser-based OAuth 2.0 login flow.
143
+ *
144
+ * Opens the system browser to the Doow authorize endpoint, waits for the
145
+ * localhost callback, exchanges the authorization code for tokens, and
146
+ * persists the tokens via the credential store.
147
+ */
148
+ declare function executePkceFlow(options?: PkceFlowOptions): Promise<PkceFlowResult>;
149
+
150
+ /**
151
+ * device-flow.ts
152
+ *
153
+ * RFC 8628 OAuth 2.0 Device Authorization flow for headless/SSH/container
154
+ * environments where a browser cannot be opened on the same machine.
155
+ *
156
+ * Steps:
157
+ * 1. POST /v1/auth/device/authorize → get device_code + user_code
158
+ * 2. Display verification URI + user_code on stderr
159
+ * 3. Optionally open the browser (best-effort)
160
+ * 4. Poll POST /v1/auth/device/token until granted or expired
161
+ * 5. Store tokens in the credential store
162
+ */
163
+
164
+ interface DeviceFlowOptions {
165
+ apiUrl?: string;
166
+ profileName?: string;
167
+ credentialStore?: CredentialStore;
168
+ /**
169
+ * Injectable URL opener — defaults to the `open` npm package.
170
+ * Provide a custom function in tests to avoid spawning a real browser process.
171
+ */
172
+ openUrl?: (url: string) => Promise<unknown>;
173
+ }
174
+ interface DeviceFlowResult {
175
+ accessToken: string;
176
+ refreshToken: string;
177
+ expiresAt: string;
178
+ }
179
+ /**
180
+ * Execute the RFC 8628 device authorization flow.
181
+ *
182
+ * All user-facing output goes to stderr so stdout stays clean for piping.
183
+ *
184
+ * @throws {Error} if authorization fails or the device code expires.
185
+ */
186
+ declare function executeDeviceFlow(options?: DeviceFlowOptions): Promise<DeviceFlowResult>;
187
+
188
+ /**
189
+ * api-key.ts
190
+ *
191
+ * PAT (Personal Access Token) authentication for CI/scripting contexts.
192
+ *
193
+ * Provides:
194
+ * - validateApiKey — pure format check (dak_ prefix, length ≥ 20)
195
+ * - authenticateWithApiKey — store key + optionally verify against API
196
+ * - readTokenFromStdin — read a piped token from stdin
197
+ * - resolveAuth — precedence chain: flag > env > stdin > stored > none
198
+ */
199
+
200
+ interface ApiKeyAuthOptions {
201
+ key: string;
202
+ apiUrl?: string;
203
+ profileName?: string;
204
+ credentialStore?: CredentialStore;
205
+ /** default true — hit capabilities endpoint to confirm key works */
206
+ verify?: boolean;
207
+ }
208
+ interface ApiKeyAuthResult {
209
+ apiKey: string;
210
+ verified: boolean;
211
+ }
212
+ interface ResolveAuthOptions {
213
+ /** --api-key flag value */
214
+ apiKeyFlag?: string;
215
+ /** --token-stdin flag */
216
+ tokenStdin?: boolean;
217
+ profileName?: string;
218
+ credentialStore?: CredentialStore;
219
+ }
220
+ interface ResolvedAuth {
221
+ type: 'api-key' | 'oauth-token' | 'none';
222
+ /** the bearer token (api key or access token) */
223
+ token?: string;
224
+ /** human-readable source: '--api-key flag', 'DOOW_API_KEY env', 'stdin', 'stored profile' */
225
+ source: string;
226
+ /** true if stored token is expired (expiresAt < now + 60s buffer) */
227
+ needsRefresh?: boolean;
228
+ }
229
+ /**
230
+ * Returns true if key starts with 'dak_' (case-sensitive) and is at least
231
+ * 20 characters total. Pure function — no network call.
232
+ */
233
+ declare function validateApiKey(key: string): boolean;
234
+ /**
235
+ * Validates the key format, stores it in the credential store, and optionally
236
+ * verifies it works by hitting GET /v1/auth/capabilities.
237
+ *
238
+ * @throws {Error} if the key does not have the dak_ prefix
239
+ * @throws {Error} if verify is true and the capabilities request fails
240
+ */
241
+ declare function authenticateWithApiKey(options: ApiKeyAuthOptions): Promise<ApiKeyAuthResult>;
242
+ /**
243
+ * Reads a token from piped stdin input.
244
+ *
245
+ * @throws {Error} if stdin is a TTY (not piped)
246
+ * @throws {Error} if the resulting string is empty after trim
247
+ */
248
+ declare function readTokenFromStdin(): Promise<string>;
249
+ /**
250
+ * Resolves the active auth context by walking the precedence chain:
251
+ * --api-key flag > DOOW_API_KEY env > --token-stdin > stored profile > none
252
+ */
253
+ declare function resolveAuth(options?: ResolveAuthOptions): Promise<ResolvedAuth>;
254
+
255
+ interface RefreshOptions {
256
+ apiUrl?: string;
257
+ profileName?: string;
258
+ credentialStore?: CredentialStore;
259
+ }
260
+ interface RefreshResult {
261
+ accessToken: string;
262
+ refreshToken: string;
263
+ expiresAt: string;
264
+ /** false if another process already refreshed before we could */
265
+ wasRefreshed: boolean;
266
+ }
267
+ interface LockHandle {
268
+ lockPath: string;
269
+ release(): Promise<void>;
270
+ }
271
+ /**
272
+ * Returns true when the credential token needs a refresh:
273
+ * - no expiresAt present, OR
274
+ * - token expires within the 60-second buffer window
275
+ *
276
+ * Pure function — no I/O.
277
+ */
278
+ declare function needsRefresh(creds: ProfileCredentials): boolean;
279
+ /**
280
+ * Acquires an exclusive per-profile lock file.
281
+ *
282
+ * Uses O_EXCL (writeFile flag:'wx') for atomic exclusive creation.
283
+ * If a lock exists, checks staleness (>60s or dead PID) and cleans up if stale.
284
+ * Retries up to 10 times with 500ms back-off before throwing.
285
+ */
286
+ declare function acquireLock(profileName: string): Promise<LockHandle>;
287
+ /**
288
+ * Releases a previously acquired lock handle. Best-effort — never throws.
289
+ */
290
+ declare function releaseLock(handle: LockHandle): Promise<void>;
291
+ /**
292
+ * Performs a transparent token refresh with double-checked locking.
293
+ *
294
+ * 1. Acquires a per-profile lockfile
295
+ * 2. Re-reads credentials — another process may have already refreshed
296
+ * 3. If tokens are still fresh, skips the network call (wasRefreshed: false)
297
+ * 4. Otherwise calls POST /v1/auth/refresh and stores the new tokens
298
+ * 5. Always releases the lock in a finally block
299
+ *
300
+ * Throws with a user-friendly message if the session has expired.
301
+ */
302
+ declare function refreshToken(options?: RefreshOptions): Promise<RefreshResult>;
303
+
304
+ /**
305
+ * detect.ts
306
+ *
307
+ * S122 — Auth auto-detection for the Doow CLI.
308
+ *
309
+ * Determines whether to use PKCE, device, or API-key authentication based on
310
+ * the current runtime environment, then executes the appropriate flow.
311
+ *
312
+ * Detection order:
313
+ * 1. --api-key provided + valid format → api-key
314
+ * 2. --device flag → device
315
+ * 3. Running inside CI ($CI set) → device
316
+ * 4. Non-interactive stdout (!isTTY) → device
317
+ * 5. Can bind 127.0.0.1 on a free port → pkce
318
+ * 6. Fallback → device
319
+ */
320
+
321
+ type AuthMethod = 'pkce' | 'device' | 'api-key';
322
+ interface DetectOptions {
323
+ /** --device flag: force device flow even in interactive environments */
324
+ forceDevice?: boolean;
325
+ /** --api-key flag value */
326
+ apiKey?: string;
327
+ }
328
+ interface AutoLoginOptions {
329
+ forceDevice?: boolean;
330
+ apiKey?: string;
331
+ apiUrl?: string;
332
+ profileName?: string;
333
+ credentialStore?: CredentialStore;
334
+ /** PKCE callback timeout in ms. Default: 120_000 */
335
+ timeout?: number;
336
+ }
337
+ interface LoginResult {
338
+ method: AuthMethod;
339
+ accessToken?: string;
340
+ refreshToken?: string;
341
+ expiresAt?: string;
342
+ apiKey?: string;
343
+ }
344
+ /**
345
+ * Tests whether the process can bind a TCP server on 127.0.0.1 using an
346
+ * OS-assigned ephemeral port. The server is closed immediately on success.
347
+ *
348
+ * Returns true → PKCE callback server will work.
349
+ * Returns false → Network stack can't bind (containers with restricted network
350
+ * policies, permission denied, etc.) — use device flow instead.
351
+ */
352
+ declare function canBindLocalhost(): Promise<boolean>;
353
+ /**
354
+ * Returns the best authentication method for the current environment.
355
+ */
356
+ declare function detectAuthMethod(options?: DetectOptions): Promise<AuthMethod>;
357
+ /**
358
+ * The main login orchestrator.
359
+ *
360
+ * 1. Detects the best auth method.
361
+ * 2. Executes the corresponding flow.
362
+ * 3. If PKCE fails with a server bind error, automatically retries with device flow.
363
+ */
364
+ declare function executeAutoLogin(options?: AutoLoginOptions): Promise<LoginResult>;
365
+
366
+ declare const VERSION = "0.1.0";
367
+
368
+ export { VERSION, acquireLock, authenticateWithApiKey, canBindLocalhost, clearProfileCredentials, createCredentialStore, deleteProfile, detectAuthMethod, executeAutoLogin, executeDeviceFlow, executePkceFlow, generatePkcePair, getActiveProfile, getApiUrl, getConfigDir, getProfileCredentials, isAgentMode, isCI, isTTY, needsRefresh, readConfig, readCredentials, readTokenFromStdin, refreshToken, releaseLock, resolveAuth, setActiveProfile, setProfileCredentials, shouldShowUI, validateApiKey, writeConfig, writeCredentials };
369
+ export type { ApiKeyAuthOptions, ApiKeyAuthResult, AuthMethod, AutoLoginOptions, CredentialStore, Credentials, DetectOptions, DeviceFlowOptions, DeviceFlowResult, DoowConfig, LockHandle, LoginResult, PkceFlowOptions, PkceFlowResult, Profile, ProfileCredentials, RefreshOptions, RefreshResult, ResolveAuthOptions, ResolvedAuth };
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@doow/cli",
3
+ "version": "0.1.0",
4
+ "description": "Doow CLI — manage SaaS spend from your terminal and coding agents",
5
+ "license": "MIT",
6
+ "author": "Doow",
7
+ "main": "./dist/cjs/index.js",
8
+ "module": "./dist/esm/index.js",
9
+ "types": "./dist/types/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "import": "./dist/esm/index.js",
13
+ "require": "./dist/cjs/index.js",
14
+ "types": "./dist/types/index.d.ts"
15
+ }
16
+ },
17
+ "bin": {
18
+ "doow": "./dist/cli.cjs"
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "README.md"
23
+ ],
24
+ "scripts": {
25
+ "build": "rollup -c rollup.config.mjs",
26
+ "test": "vitest run",
27
+ "test:watch": "vitest",
28
+ "typecheck": "tsc --noEmit",
29
+ "prepublishOnly": "npm run build"
30
+ },
31
+ "dependencies": {
32
+ "@modelcontextprotocol/sdk": "^1.29.0",
33
+ "commander": "^12.1.0",
34
+ "open": "^10.1.0",
35
+ "zod": "^4.0.0"
36
+ },
37
+ "devDependencies": {
38
+ "@rollup/plugin-commonjs": "^25.0.8",
39
+ "@rollup/plugin-json": "^6.1.0",
40
+ "@rollup/plugin-node-resolve": "^15.3.1",
41
+ "@rollup/plugin-replace": "^5.0.7",
42
+ "@rollup/plugin-typescript": "^11.1.6",
43
+ "rollup": "^4.60.2",
44
+ "rollup-plugin-dts": "^6.4.1",
45
+ "tslib": "^2.8.1",
46
+ "typescript": "^5.9.3",
47
+ "vitest": "^1.6.1"
48
+ },
49
+ "optionalDependencies": {
50
+ "keytar": "^7.0.0"
51
+ },
52
+ "engines": {
53
+ "node": ">=18.0.0"
54
+ },
55
+ "keywords": [
56
+ "doow",
57
+ "cli",
58
+ "saas",
59
+ "spend",
60
+ "mcp"
61
+ ]
62
+ }