@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,220 @@
1
+ import * as http from 'node:http';
2
+ import * as crypto from 'node:crypto';
3
+ import { getApiUrl } from '../config/env.js';
4
+ import { getActiveProfile } from '../config/store.js';
5
+ import { createCredentialStore } from './keyring.js';
6
+
7
+ /**
8
+ * pkce.ts
9
+ *
10
+ * OAuth 2.0 PKCE (RFC 7636) interactive login flow for the Doow CLI.
11
+ *
12
+ * Steps:
13
+ * 1. Generate PKCE code_verifier + code_challenge
14
+ * 2. Generate CSRF state token
15
+ * 3. Start a localhost HTTP server on a random port
16
+ * 4. Open browser to the authorize URL
17
+ * 5. Wait for the OAuth callback (120 s default timeout)
18
+ * 6. Exchange authorization code for tokens
19
+ * 7. Store tokens via CredentialStore
20
+ * 8. Close server and return result
21
+ */
22
+ // ---------------------------------------------------------------------------
23
+ // HTML responses
24
+ // ---------------------------------------------------------------------------
25
+ const SUCCESS_HTML = `<!DOCTYPE html><html><body style="font-family:system-ui;text-align:center;padding:40px">
26
+ <h2>Logged in to Doow</h2><p>You can close this tab.</p>
27
+ </body></html>`;
28
+ const ERROR_HTML = `<!DOCTYPE html><html><body style="font-family:system-ui;text-align:center;padding:40px">
29
+ <h2>Login failed</h2><p>CSRF state mismatch. Please try again.</p>
30
+ </body></html>`;
31
+ // ---------------------------------------------------------------------------
32
+ // PKCE helpers
33
+ // ---------------------------------------------------------------------------
34
+ /**
35
+ * Generates a PKCE code_verifier and code_challenge pair.
36
+ * - code_verifier: 32 random bytes, base64url-encoded (43 chars)
37
+ * - code_challenge: SHA-256 of verifier, base64url-encoded
38
+ */
39
+ function generatePkcePair() {
40
+ const verifierBytes = crypto.randomBytes(32);
41
+ const codeVerifier = verifierBytes
42
+ .toString('base64')
43
+ .replace(/\+/g, '-')
44
+ .replace(/\//g, '_')
45
+ .replace(/=/g, '');
46
+ const challengeBytes = crypto.createHash('sha256').update(codeVerifier).digest();
47
+ const codeChallenge = challengeBytes
48
+ .toString('base64')
49
+ .replace(/\+/g, '-')
50
+ .replace(/\//g, '_')
51
+ .replace(/=/g, '');
52
+ return { codeVerifier, codeChallenge };
53
+ }
54
+ /**
55
+ * Generates a random CSRF state token (32 bytes, base64url).
56
+ */
57
+ function generateState() {
58
+ return crypto
59
+ .randomBytes(32)
60
+ .toString('base64')
61
+ .replace(/\+/g, '-')
62
+ .replace(/\//g, '_')
63
+ .replace(/=/g, '');
64
+ }
65
+ /**
66
+ * Starts a localhost HTTP server on a random port and waits for the OAuth
67
+ * callback. Resolves with { code, state } on success.
68
+ * Rejects if the CSRF state doesn't match.
69
+ */
70
+ function startCallbackServer(expectedState, timeoutMs) {
71
+ return new Promise((resolveServer, rejectServer) => {
72
+ const server = http.createServer();
73
+ const callbackPromise = new Promise((resolveCallback, rejectCallback) => {
74
+ let timeoutHandle;
75
+ // Set the timeout for the callback wait — server.close() is handled
76
+ // by the finally block in executePkceFlow, not here.
77
+ timeoutHandle = setTimeout(() => {
78
+ rejectCallback(new Error('Login timed out after 120 seconds. Try doow login --device for headless environments.'));
79
+ }, timeoutMs);
80
+ server.on('request', (req, res) => {
81
+ // Only handle /callback path
82
+ if (!req.url?.startsWith('/callback')) {
83
+ res.writeHead(404);
84
+ res.end('Not found');
85
+ return;
86
+ }
87
+ const url = new URL(req.url, 'http://127.0.0.1');
88
+ const code = url.searchParams.get('code');
89
+ const state = url.searchParams.get('state');
90
+ const errorParam = url.searchParams.get('error');
91
+ // Handle OAuth error from server
92
+ if (errorParam) {
93
+ clearTimeout(timeoutHandle);
94
+ res.writeHead(400, { 'Content-Type': 'text/html' });
95
+ res.end(ERROR_HTML);
96
+ rejectCallback(new Error(`Authorization failed: ${errorParam}`));
97
+ return;
98
+ }
99
+ // Validate required params
100
+ if (!code || !state) {
101
+ res.writeHead(400, { 'Content-Type': 'text/html' });
102
+ res.end(ERROR_HTML);
103
+ return;
104
+ }
105
+ // CSRF state validation
106
+ if (state !== expectedState) {
107
+ clearTimeout(timeoutHandle);
108
+ res.writeHead(400, { 'Content-Type': 'text/html' });
109
+ res.end(ERROR_HTML);
110
+ rejectCallback(new Error('CSRF state mismatch — possible attack. Try again.'));
111
+ return;
112
+ }
113
+ // Success
114
+ clearTimeout(timeoutHandle);
115
+ res.writeHead(200, { 'Content-Type': 'text/html' });
116
+ res.end(SUCCESS_HTML);
117
+ resolveCallback({ code, state });
118
+ });
119
+ });
120
+ server.on('error', (err) => {
121
+ rejectServer(new Error(`Failed to start local callback server: ${err.message}. Try doow login --device for headless environments.`));
122
+ });
123
+ // Bind to port 0 — OS assigns a random available port
124
+ server.listen(0, '127.0.0.1', () => {
125
+ const addr = server.address();
126
+ if (!addr || typeof addr === 'string') {
127
+ server.close();
128
+ rejectServer(new Error('Failed to determine callback server port. Try doow login --device for headless environments.'));
129
+ return;
130
+ }
131
+ resolveServer({ server, port: addr.port, callbackPromise });
132
+ });
133
+ });
134
+ }
135
+ // ---------------------------------------------------------------------------
136
+ // Token exchange
137
+ // ---------------------------------------------------------------------------
138
+ async function exchangeCodeForTokens(apiUrl, code, codeVerifier, state) {
139
+ const response = await fetch(`${apiUrl}/v1/auth/cli/token`, {
140
+ method: 'POST',
141
+ headers: { 'Content-Type': 'application/json' },
142
+ body: JSON.stringify({
143
+ grant_type: 'authorization_code',
144
+ code,
145
+ code_verifier: codeVerifier,
146
+ state,
147
+ }),
148
+ });
149
+ if (!response.ok) {
150
+ const body = await response.text().catch(() => '(no body)');
151
+ throw new Error(`Token exchange failed: HTTP ${response.status} — ${body}`);
152
+ }
153
+ return response.json();
154
+ }
155
+ // ---------------------------------------------------------------------------
156
+ // Main flow
157
+ // ---------------------------------------------------------------------------
158
+ /**
159
+ * Executes the full PKCE browser-based OAuth 2.0 login flow.
160
+ *
161
+ * Opens the system browser to the Doow authorize endpoint, waits for the
162
+ * localhost callback, exchanges the authorization code for tokens, and
163
+ * persists the tokens via the credential store.
164
+ */
165
+ async function executePkceFlow(options = {}) {
166
+ // Resolve options
167
+ const profile = await getActiveProfile();
168
+ const apiUrl = options.apiUrl ?? getApiUrl(profile);
169
+ const profileName = options.profileName ?? profile.name;
170
+ const credentialStore = options.credentialStore ?? (await createCredentialStore());
171
+ const timeout = options.timeout ?? 120_000;
172
+ // Step 1 & 2: Generate PKCE pair and CSRF state
173
+ const { codeVerifier, codeChallenge } = generatePkcePair();
174
+ const state = generateState();
175
+ // Step 3: Start callback server
176
+ const { server, port, callbackPromise } = await startCallbackServer(state, timeout);
177
+ try {
178
+ // Step 4: Build authorize URL
179
+ const redirectUri = `http://127.0.0.1:${port}/callback`;
180
+ const authorizeUrl = `${apiUrl}/v1/auth/cli/authorize` +
181
+ `?response_type=code` +
182
+ `&code_challenge=${codeChallenge}` +
183
+ `&code_challenge_method=S256` +
184
+ `&state=${state}` +
185
+ `&redirect_uri=${encodeURIComponent(redirectUri)}`;
186
+ // Step 5: Open browser
187
+ try {
188
+ const { default: open } = await import('open');
189
+ await open(authorizeUrl);
190
+ }
191
+ catch (err) {
192
+ const msg = err instanceof Error ? err.message : String(err);
193
+ throw new Error(`Failed to open browser: ${msg}. Try doow login --device for headless environments.`);
194
+ }
195
+ // Step 6: Wait for callback
196
+ const { code } = await callbackPromise;
197
+ // Step 7: Exchange code for tokens
198
+ const tokenResponse = await exchangeCodeForTokens(apiUrl, code, codeVerifier, state);
199
+ // Step 8: Store tokens
200
+ const expiresAt = new Date(Date.now() + tokenResponse.expires_in * 1000).toISOString();
201
+ const result = {
202
+ accessToken: tokenResponse.access_token,
203
+ refreshToken: tokenResponse.refresh_token,
204
+ expiresAt,
205
+ };
206
+ await credentialStore.set(profileName, {
207
+ accessToken: result.accessToken,
208
+ refreshToken: result.refreshToken,
209
+ expiresAt: result.expiresAt,
210
+ });
211
+ return result;
212
+ }
213
+ finally {
214
+ // Always close the server — success, error, or timeout
215
+ server.close();
216
+ }
217
+ }
218
+
219
+ export { executePkceFlow, generatePkcePair };
220
+ //# sourceMappingURL=pkce.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pkce.js","sources":["../../../../src/auth/pkce.ts"],"sourcesContent":[null],"names":[],"mappings":"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"}
@@ -0,0 +1,198 @@
1
+ import { writeFile, unlink, readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { getConfigDir } from '../config/store.js';
4
+ import { getApiUrl } from '../config/env.js';
5
+ import { createCredentialStore } from './keyring.js';
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Constants
9
+ // ---------------------------------------------------------------------------
10
+ const REFRESH_BUFFER_SECONDS = 60;
11
+ const STALE_LOCK_THRESHOLD_MS = 60_000;
12
+ const LOCK_RETRY_DELAY_MS = 500;
13
+ const LOCK_MAX_RETRIES = 10;
14
+ // ---------------------------------------------------------------------------
15
+ // needsRefresh
16
+ // ---------------------------------------------------------------------------
17
+ /**
18
+ * Returns true when the credential token needs a refresh:
19
+ * - no expiresAt present, OR
20
+ * - token expires within the 60-second buffer window
21
+ *
22
+ * Pure function — no I/O.
23
+ */
24
+ function needsRefresh(creds) {
25
+ if (!creds.expiresAt)
26
+ return true;
27
+ const expiresAt = new Date(creds.expiresAt).getTime();
28
+ if (isNaN(expiresAt))
29
+ return true;
30
+ const bufferMs = REFRESH_BUFFER_SECONDS * 1_000;
31
+ return Date.now() >= expiresAt - bufferMs;
32
+ }
33
+ // ---------------------------------------------------------------------------
34
+ // Lockfile helpers
35
+ // ---------------------------------------------------------------------------
36
+ /**
37
+ * Returns true when the process identified by `pid` is still running.
38
+ * Uses kill(pid, 0) which sends no signal but checks for process existence.
39
+ */
40
+ function isPidAlive(pid) {
41
+ try {
42
+ process.kill(pid, 0);
43
+ return true;
44
+ }
45
+ catch {
46
+ return false;
47
+ }
48
+ }
49
+ /**
50
+ * Acquires an exclusive per-profile lock file.
51
+ *
52
+ * Uses O_EXCL (writeFile flag:'wx') for atomic exclusive creation.
53
+ * If a lock exists, checks staleness (>60s or dead PID) and cleans up if stale.
54
+ * Retries up to 10 times with 500ms back-off before throwing.
55
+ */
56
+ async function acquireLock(profileName) {
57
+ const configDir = await getConfigDir();
58
+ const lockPath = join(configDir, `.${profileName}.refresh.lock`);
59
+ for (let attempt = 0; attempt < LOCK_MAX_RETRIES; attempt++) {
60
+ try {
61
+ const content = { pid: process.pid, timestamp: Date.now() };
62
+ await writeFile(lockPath, JSON.stringify(content), { flag: 'wx', mode: 0o600 });
63
+ // Acquired — return handle with inline release
64
+ return {
65
+ lockPath,
66
+ async release() {
67
+ await unlink(lockPath).catch(() => undefined);
68
+ },
69
+ };
70
+ }
71
+ catch (err) {
72
+ // Only handle EEXIST — lock file already exists
73
+ if (!isEexist(err))
74
+ throw err;
75
+ // Read existing lock and decide whether it's stale
76
+ const isStale = await checkAndCleanStaleLock(lockPath);
77
+ if (isStale) {
78
+ // Stale lock deleted — retry immediately (don't sleep)
79
+ continue;
80
+ }
81
+ // Live lock — wait before retrying
82
+ if (attempt < LOCK_MAX_RETRIES - 1) {
83
+ await sleep(LOCK_RETRY_DELAY_MS);
84
+ }
85
+ }
86
+ }
87
+ throw new Error('Could not acquire refresh lock. Another process may be refreshing.');
88
+ }
89
+ /**
90
+ * Reads the lock file at `lockPath`, checks whether it's stale, and
91
+ * if so deletes it. Returns true if the lock was stale (and deleted).
92
+ */
93
+ async function checkAndCleanStaleLock(lockPath) {
94
+ let content;
95
+ try {
96
+ const raw = await readFile(lockPath, 'utf-8');
97
+ content = JSON.parse(raw);
98
+ }
99
+ catch {
100
+ // File vanished between our EEXIST and this read — treat as stale
101
+ return true;
102
+ }
103
+ const isOlderThanThreshold = Date.now() - content.timestamp > STALE_LOCK_THRESHOLD_MS;
104
+ const isProcDead = !isPidAlive(content.pid);
105
+ if (isOlderThanThreshold || isProcDead) {
106
+ await unlink(lockPath).catch(() => undefined);
107
+ return true;
108
+ }
109
+ return false;
110
+ }
111
+ /**
112
+ * Releases a previously acquired lock handle. Best-effort — never throws.
113
+ */
114
+ async function releaseLock(handle) {
115
+ await handle.release();
116
+ }
117
+ // ---------------------------------------------------------------------------
118
+ // refreshToken
119
+ // ---------------------------------------------------------------------------
120
+ /**
121
+ * Performs a transparent token refresh with double-checked locking.
122
+ *
123
+ * 1. Acquires a per-profile lockfile
124
+ * 2. Re-reads credentials — another process may have already refreshed
125
+ * 3. If tokens are still fresh, skips the network call (wasRefreshed: false)
126
+ * 4. Otherwise calls POST /v1/auth/refresh and stores the new tokens
127
+ * 5. Always releases the lock in a finally block
128
+ *
129
+ * Throws with a user-friendly message if the session has expired.
130
+ */
131
+ async function refreshToken(options = {}) {
132
+ const profileName = options.profileName ?? 'default';
133
+ const store = options.credentialStore ?? (await createCredentialStore());
134
+ const apiUrl = options.apiUrl ?? getApiUrl();
135
+ const lock = await acquireLock(profileName);
136
+ try {
137
+ // Double-checked locking: re-read credentials now that we hold the lock.
138
+ // Another process may have already refreshed while we waited.
139
+ const latestCreds = await store.get(profileName);
140
+ if (latestCreds && !needsRefresh(latestCreds)) {
141
+ // Already refreshed by another process — return current tokens.
142
+ return {
143
+ accessToken: latestCreds.accessToken ?? '',
144
+ refreshToken: latestCreds.refreshToken ?? '',
145
+ expiresAt: latestCreds.expiresAt ?? '',
146
+ wasRefreshed: false,
147
+ };
148
+ }
149
+ // We need to refresh — verify we have a refresh token to use.
150
+ if (!latestCreds?.refreshToken) {
151
+ await store.clear(profileName);
152
+ throw new Error('Session expired. Run doow login to re-authenticate.');
153
+ }
154
+ // Call the refresh endpoint.
155
+ const response = await fetch(`${apiUrl}/v1/auth/refresh`, {
156
+ method: 'POST',
157
+ headers: { 'Content-Type': 'application/json' },
158
+ body: JSON.stringify({ refresh_token: latestCreds.refreshToken }),
159
+ });
160
+ if (!response.ok) {
161
+ // 401 or any non-2xx → session expired
162
+ await store.clear(profileName);
163
+ throw new Error('Session expired. Run doow login to re-authenticate.');
164
+ }
165
+ const data = (await response.json());
166
+ const expiresAt = new Date(Date.now() + data.expires_in * 1_000).toISOString();
167
+ const newCreds = {
168
+ accessToken: data.access_token,
169
+ refreshToken: data.refresh_token,
170
+ expiresAt,
171
+ };
172
+ await store.set(profileName, newCreds);
173
+ return {
174
+ accessToken: data.access_token,
175
+ refreshToken: data.refresh_token,
176
+ expiresAt,
177
+ wasRefreshed: true,
178
+ };
179
+ }
180
+ finally {
181
+ await lock.release();
182
+ }
183
+ }
184
+ // ---------------------------------------------------------------------------
185
+ // Utilities
186
+ // ---------------------------------------------------------------------------
187
+ function isEexist(err) {
188
+ return (typeof err === 'object' &&
189
+ err !== null &&
190
+ 'code' in err &&
191
+ err.code === 'EEXIST');
192
+ }
193
+ function sleep(ms) {
194
+ return new Promise((resolve) => setTimeout(resolve, ms));
195
+ }
196
+
197
+ export { acquireLock, needsRefresh, refreshToken, releaseLock };
198
+ //# sourceMappingURL=refresh.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"refresh.js","sources":["../../../../src/auth/refresh.ts"],"sourcesContent":[null],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ /** True when stdout is an interactive terminal. */
2
+ function isTTY() {
3
+ return process.stdout.isTTY === true;
4
+ }
5
+ /** True when running inside a CI environment (any common CI sets $CI). */
6
+ function isCI() {
7
+ return !!process.env['CI'];
8
+ }
9
+ /**
10
+ * True when the CLI is invoked by an automated agent rather than a human.
11
+ * Detected via $DOOW_AGENT_MODE env var or the --agent CLI flag (which
12
+ * Commander stores on the global options object as `process.env` is the
13
+ * canonical signal here — Commander integration is wired up in cli.ts).
14
+ */
15
+ function isAgentMode() {
16
+ return !!process.env['DOOW_AGENT_MODE'];
17
+ }
18
+ /**
19
+ * True when interactive UI (spinners, prompts, color) should be shown.
20
+ * Requires a real TTY, no CI env, and not running in agent mode.
21
+ */
22
+ function shouldShowUI() {
23
+ return isTTY() && !isCI() && !isAgentMode();
24
+ }
25
+ /**
26
+ * Resolve the API base URL.
27
+ * Precedence: profile.apiUrl → $DOOW_API_URL → hardcoded default.
28
+ */
29
+ function getApiUrl(profile) {
30
+ if (profile?.apiUrl)
31
+ return profile.apiUrl;
32
+ if (process.env['DOOW_API_URL'])
33
+ return process.env['DOOW_API_URL'];
34
+ return 'https://api.doow.com';
35
+ }
36
+
37
+ export { getApiUrl, isAgentMode, isCI, isTTY, shouldShowUI };
38
+ //# sourceMappingURL=env.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env.js","sources":["../../../../src/config/env.ts"],"sourcesContent":[null],"names":[],"mappings":"AAEA;SACgB,KAAK,GAAA;AACnB,IAAA,OAAO,OAAO,CAAC,MAAM,CAAC,KAAK,KAAK,IAAI;AACtC;AAEA;SACgB,IAAI,GAAA;IAClB,OAAO,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC;AAC5B;AAEA;;;;;AAKG;SACa,WAAW,GAAA;IACzB,OAAO,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC;AACzC;AAEA;;;AAGG;SACa,YAAY,GAAA;IAC1B,OAAO,KAAK,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE;AAC7C;AAEA;;;AAGG;AACG,SAAU,SAAS,CAAC,OAAiB,EAAA;IACzC,IAAI,OAAO,EAAE,MAAM;QAAE,OAAO,OAAO,CAAC,MAAM;AAC1C,IAAA,IAAI,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;AAAE,QAAA,OAAO,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;AACnE,IAAA,OAAO,sBAAsB;AAC/B;;;;"}
@@ -0,0 +1,166 @@
1
+ import { mkdir, chmod, readFile, writeFile, rename, rm } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Constants / defaults
7
+ // ---------------------------------------------------------------------------
8
+ const DEFAULT_PROFILE_NAME = 'default';
9
+ const DEFAULT_CONFIG = {
10
+ activeProfile: DEFAULT_PROFILE_NAME,
11
+ profiles: {
12
+ [DEFAULT_PROFILE_NAME]: { name: DEFAULT_PROFILE_NAME },
13
+ },
14
+ };
15
+ const DEFAULT_CREDENTIALS = {
16
+ profiles: {},
17
+ };
18
+ // ---------------------------------------------------------------------------
19
+ // Config directory
20
+ // ---------------------------------------------------------------------------
21
+ /**
22
+ * Returns the Doow config directory path.
23
+ * Prefers $DOOW_CONFIG_DIR when set; falls back to ~/.doow.
24
+ * Creates the directory (mode 0o700) if it does not yet exist.
25
+ */
26
+ async function getConfigDir() {
27
+ const dir = process.env['DOOW_CONFIG_DIR'] ?? join(homedir(), '.doow');
28
+ await mkdir(dir, { recursive: true, mode: 0o700 });
29
+ await chmod(dir, 0o700);
30
+ return dir;
31
+ }
32
+ // ---------------------------------------------------------------------------
33
+ // Atomic write helper
34
+ // ---------------------------------------------------------------------------
35
+ async function atomicWrite(filePath, data) {
36
+ const tmp = `${filePath}.tmp`;
37
+ try {
38
+ await writeFile(tmp, data, { encoding: 'utf-8', mode: 0o600 });
39
+ await rename(tmp, filePath);
40
+ }
41
+ catch (err) {
42
+ // Clean up temp file on failure, best-effort
43
+ await rm(tmp, { force: true }).catch(() => undefined);
44
+ throw err;
45
+ }
46
+ }
47
+ // ---------------------------------------------------------------------------
48
+ // config.json
49
+ // ---------------------------------------------------------------------------
50
+ /** Reads ~/.doow/config.json. Returns the default config if the file is absent. */
51
+ async function readConfig() {
52
+ const dir = await getConfigDir();
53
+ const filePath = join(dir, 'config.json');
54
+ try {
55
+ const raw = await readFile(filePath, 'utf-8');
56
+ return JSON.parse(raw);
57
+ }
58
+ catch (err) {
59
+ if (isEnoent(err))
60
+ return structuredClone(DEFAULT_CONFIG);
61
+ throw err;
62
+ }
63
+ }
64
+ /** Writes ~/.doow/config.json atomically with 0o600 perms. */
65
+ async function writeConfig(config) {
66
+ const dir = await getConfigDir();
67
+ const filePath = join(dir, 'config.json');
68
+ await atomicWrite(filePath, JSON.stringify(config, null, 2));
69
+ }
70
+ // ---------------------------------------------------------------------------
71
+ // credentials.json
72
+ // ---------------------------------------------------------------------------
73
+ /** Reads ~/.doow/credentials.json. Returns empty credentials if absent. */
74
+ async function readCredentials() {
75
+ const dir = await getConfigDir();
76
+ const filePath = join(dir, 'credentials.json');
77
+ try {
78
+ const raw = await readFile(filePath, 'utf-8');
79
+ return JSON.parse(raw);
80
+ }
81
+ catch (err) {
82
+ if (isEnoent(err))
83
+ return structuredClone(DEFAULT_CREDENTIALS);
84
+ throw err;
85
+ }
86
+ }
87
+ /** Writes ~/.doow/credentials.json atomically with 0o600 perms. */
88
+ async function writeCredentials(creds) {
89
+ const dir = await getConfigDir();
90
+ const filePath = join(dir, 'credentials.json');
91
+ await atomicWrite(filePath, JSON.stringify(creds, null, 2));
92
+ }
93
+ // ---------------------------------------------------------------------------
94
+ // Active profile
95
+ // ---------------------------------------------------------------------------
96
+ /** Returns the currently active Profile object. */
97
+ async function getActiveProfile() {
98
+ const config = await readConfig();
99
+ const profile = config.profiles[config.activeProfile];
100
+ if (!profile) {
101
+ // Graceful degradation: return synthetic default
102
+ return { name: config.activeProfile };
103
+ }
104
+ return profile;
105
+ }
106
+ /** Sets the active profile name and persists config. Throws if the profile doesn't exist. */
107
+ async function setActiveProfile(name) {
108
+ const config = await readConfig();
109
+ if (!config.profiles[name]) {
110
+ throw new Error(`Profile "${name}" does not exist.`);
111
+ }
112
+ config.activeProfile = name;
113
+ await writeConfig(config);
114
+ }
115
+ // ---------------------------------------------------------------------------
116
+ // Profile credentials
117
+ // ---------------------------------------------------------------------------
118
+ /**
119
+ * Returns credentials for the given profile name.
120
+ * Defaults to the currently active profile if name is omitted.
121
+ */
122
+ async function getProfileCredentials(profileName) {
123
+ const name = profileName ?? (await readConfig()).activeProfile;
124
+ const creds = await readCredentials();
125
+ return creds.profiles[name];
126
+ }
127
+ /** Stores credentials for the given profile, merging with existing. */
128
+ async function setProfileCredentials(profileName, creds) {
129
+ const existing = await readCredentials();
130
+ existing.profiles[profileName] = creds;
131
+ await writeCredentials(existing);
132
+ }
133
+ /** Removes all credentials for the given profile. */
134
+ async function clearProfileCredentials(profileName) {
135
+ const creds = await readCredentials();
136
+ delete creds.profiles[profileName];
137
+ await writeCredentials(creds);
138
+ }
139
+ // ---------------------------------------------------------------------------
140
+ // Profile lifecycle
141
+ // ---------------------------------------------------------------------------
142
+ /**
143
+ * Deletes a profile from config + credentials.
144
+ * Throws if the profile is currently active — caller must switch first.
145
+ */
146
+ async function deleteProfile(name) {
147
+ const config = await readConfig();
148
+ if (config.activeProfile === name) {
149
+ throw new Error(`Cannot delete the active profile "${name}". Switch to another profile first.`);
150
+ }
151
+ delete config.profiles[name];
152
+ await writeConfig(config);
153
+ await clearProfileCredentials(name);
154
+ }
155
+ // ---------------------------------------------------------------------------
156
+ // Utility
157
+ // ---------------------------------------------------------------------------
158
+ function isEnoent(err) {
159
+ return (typeof err === 'object' &&
160
+ err !== null &&
161
+ 'code' in err &&
162
+ err.code === 'ENOENT');
163
+ }
164
+
165
+ export { clearProfileCredentials, deleteProfile, getActiveProfile, getConfigDir, getProfileCredentials, readConfig, readCredentials, setActiveProfile, setProfileCredentials, writeConfig, writeCredentials };
166
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sources":["../../../../src/config/store.ts"],"sourcesContent":[null],"names":[],"mappings":"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"}