@skhema/cli 0.3.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 +163 -0
- package/bin/skhema.js +2 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +92 -0
- package/dist/commands/agent.d.ts +3 -0
- package/dist/commands/agent.d.ts.map +1 -0
- package/dist/commands/agent.js +154 -0
- package/dist/commands/api.d.ts +19 -0
- package/dist/commands/api.d.ts.map +1 -0
- package/dist/commands/api.js +97 -0
- package/dist/commands/auth.d.ts +3 -0
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/auth.js +403 -0
- package/dist/commands/component.d.ts +3 -0
- package/dist/commands/component.d.ts.map +1 -0
- package/dist/commands/component.js +221 -0
- package/dist/commands/contribute.d.ts +3 -0
- package/dist/commands/contribute.d.ts.map +1 -0
- package/dist/commands/contribute.js +318 -0
- package/dist/commands/element.d.ts +7 -0
- package/dist/commands/element.d.ts.map +1 -0
- package/dist/commands/element.js +167 -0
- package/dist/commands/export.d.ts +9 -0
- package/dist/commands/export.d.ts.map +1 -0
- package/dist/commands/export.js +42 -0
- package/dist/commands/init.d.ts +49 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +275 -0
- package/dist/commands/link.d.ts +8 -0
- package/dist/commands/link.d.ts.map +1 -0
- package/dist/commands/link.js +70 -0
- package/dist/commands/resource.d.ts +3 -0
- package/dist/commands/resource.d.ts.map +1 -0
- package/dist/commands/resource.js +88 -0
- package/dist/commands/skills.d.ts +3 -0
- package/dist/commands/skills.d.ts.map +1 -0
- package/dist/commands/skills.js +113 -0
- package/dist/commands/strategy.d.ts +3 -0
- package/dist/commands/strategy.d.ts.map +1 -0
- package/dist/commands/strategy.js +143 -0
- package/dist/commands/webhook.d.ts +3 -0
- package/dist/commands/webhook.d.ts.map +1 -0
- package/dist/commands/webhook.js +77 -0
- package/dist/commands/workspace.d.ts +8 -0
- package/dist/commands/workspace.d.ts.map +1 -0
- package/dist/commands/workspace.js +219 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/lib/api/client.d.ts +27 -0
- package/dist/lib/api/client.d.ts.map +1 -0
- package/dist/lib/api/client.js +71 -0
- package/dist/lib/api/command-helpers.d.ts +27 -0
- package/dist/lib/api/command-helpers.d.ts.map +1 -0
- package/dist/lib/api/command-helpers.js +77 -0
- package/dist/lib/api/credential-context.d.ts +3 -0
- package/dist/lib/api/credential-context.d.ts.map +1 -0
- package/dist/lib/api/credential-context.js +13 -0
- package/dist/lib/api/export-cli.d.ts +14 -0
- package/dist/lib/api/export-cli.d.ts.map +1 -0
- package/dist/lib/api/export-cli.js +26 -0
- package/dist/lib/api/formats.d.ts +12 -0
- package/dist/lib/api/formats.d.ts.map +1 -0
- package/dist/lib/api/formats.js +18 -0
- package/dist/lib/api/passthrough.d.ts +34 -0
- package/dist/lib/api/passthrough.d.ts.map +1 -0
- package/dist/lib/api/passthrough.js +106 -0
- package/dist/lib/api/payload.d.ts +16 -0
- package/dist/lib/api/payload.d.ts.map +1 -0
- package/dist/lib/api/payload.js +53 -0
- package/dist/lib/api/run-command.d.ts +69 -0
- package/dist/lib/api/run-command.d.ts.map +1 -0
- package/dist/lib/api/run-command.js +127 -0
- package/dist/lib/api/scope.d.ts +8 -0
- package/dist/lib/api/scope.d.ts.map +1 -0
- package/dist/lib/api/scope.js +16 -0
- package/dist/lib/api/transfer.d.ts +57 -0
- package/dist/lib/api/transfer.d.ts.map +1 -0
- package/dist/lib/api/transfer.js +153 -0
- package/dist/lib/auth/api-key-admin.d.ts +39 -0
- package/dist/lib/auth/api-key-admin.d.ts.map +1 -0
- package/dist/lib/auth/api-key-admin.js +66 -0
- package/dist/lib/auth/api-key-store.d.ts +8 -0
- package/dist/lib/auth/api-key-store.d.ts.map +1 -0
- package/dist/lib/auth/api-key-store.js +109 -0
- package/dist/lib/auth/device-flow.d.ts +4 -0
- package/dist/lib/auth/device-flow.d.ts.map +1 -0
- package/dist/lib/auth/device-flow.js +109 -0
- package/dist/lib/auth/entitlements.d.ts +16 -0
- package/dist/lib/auth/entitlements.d.ts.map +1 -0
- package/dist/lib/auth/entitlements.js +89 -0
- package/dist/lib/auth/localhost-flow.d.ts +4 -0
- package/dist/lib/auth/localhost-flow.d.ts.map +1 -0
- package/dist/lib/auth/localhost-flow.js +134 -0
- package/dist/lib/auth/oauth-client.d.ts +33 -0
- package/dist/lib/auth/oauth-client.d.ts.map +1 -0
- package/dist/lib/auth/oauth-client.js +96 -0
- package/dist/lib/auth/token-store.d.ts +6 -0
- package/dist/lib/auth/token-store.d.ts.map +1 -0
- package/dist/lib/auth/token-store.js +136 -0
- package/dist/lib/auth/types.d.ts +49 -0
- package/dist/lib/auth/types.d.ts.map +1 -0
- package/dist/lib/auth/types.js +1 -0
- package/dist/lib/banner.d.ts +2 -0
- package/dist/lib/banner.d.ts.map +1 -0
- package/dist/lib/banner.js +82 -0
- package/dist/lib/config-store.d.ts +23 -0
- package/dist/lib/config-store.d.ts.map +1 -0
- package/dist/lib/config-store.js +53 -0
- package/dist/lib/config.d.ts +20 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +33 -0
- package/dist/lib/contribute/author-resolver.d.ts +13 -0
- package/dist/lib/contribute/author-resolver.d.ts.map +1 -0
- package/dist/lib/contribute/author-resolver.js +63 -0
- package/dist/lib/contribute/embed-generator.d.ts +4 -0
- package/dist/lib/contribute/embed-generator.d.ts.map +1 -0
- package/dist/lib/contribute/embed-generator.js +108 -0
- package/dist/lib/contribute/link-generator.d.ts +11 -0
- package/dist/lib/contribute/link-generator.d.ts.map +1 -0
- package/dist/lib/contribute/link-generator.js +119 -0
- package/dist/lib/contribute/method-schema.d.ts +239 -0
- package/dist/lib/contribute/method-schema.d.ts.map +1 -0
- package/dist/lib/contribute/method-schema.js +172 -0
- package/dist/lib/contribute/profile-resolver.d.ts +2 -0
- package/dist/lib/contribute/profile-resolver.d.ts.map +1 -0
- package/dist/lib/contribute/profile-resolver.js +25 -0
- package/dist/lib/contribute/types.d.ts +91 -0
- package/dist/lib/contribute/types.d.ts.map +1 -0
- package/dist/lib/contribute/types.js +1 -0
- package/dist/lib/mcp/registrar.d.ts +26 -0
- package/dist/lib/mcp/registrar.d.ts.map +1 -0
- package/dist/lib/mcp/registrar.js +69 -0
- package/dist/lib/mcp/registry.d.ts +54 -0
- package/dist/lib/mcp/registry.d.ts.map +1 -0
- package/dist/lib/mcp/registry.js +105 -0
- package/dist/lib/open.d.ts +2 -0
- package/dist/lib/open.d.ts.map +1 -0
- package/dist/lib/open.js +24 -0
- package/dist/lib/output.d.ts +25 -0
- package/dist/lib/output.d.ts.map +1 -0
- package/dist/lib/output.js +74 -0
- package/dist/lib/skills/installer.d.ts +4 -0
- package/dist/lib/skills/installer.d.ts.map +1 -0
- package/dist/lib/skills/installer.js +100 -0
- package/dist/lib/skills/lock.d.ts +4 -0
- package/dist/lib/skills/lock.d.ts.map +1 -0
- package/dist/lib/skills/lock.js +28 -0
- package/dist/lib/skills/platform-detect.d.ts +4 -0
- package/dist/lib/skills/platform-detect.d.ts.map +1 -0
- package/dist/lib/skills/platform-detect.js +89 -0
- package/dist/lib/skills/types.d.ts +29 -0
- package/dist/lib/skills/types.d.ts.map +1 -0
- package/dist/lib/skills/types.js +1 -0
- package/dist/postinstall.d.ts +2 -0
- package/dist/postinstall.d.ts.map +1 -0
- package/dist/postinstall.js +11 -0
- package/package.json +67 -0
- package/skills/.manifest.json +15 -0
- package/skills/skhema-challenge-framing/SKILL.md +28 -0
- package/skills/skhema-coherence-check/SKILL.md +28 -0
- package/skills/skhema-element-decomposition/SKILL.md +28 -0
- package/skills/skhema-element-writer/SKILL.md +20 -0
- package/skills/skhema-judgment-audit/SKILL.md +28 -0
- package/skills/skhema-semantic-sharpening/SKILL.md +28 -0
- package/skills/skhema-strategy-advisor/SKILL.md +20 -0
- package/skills/skhema-workspace-navigator/SKILL.md +20 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"localhost-flow.d.ts","sourceRoot":"","sources":["../../../src/lib/auth/localhost-flow.ts"],"names":[],"mappings":"AAsCA,wBAAsB,kBAAkB,CAAC,OAAO,CAAC,EAAE;IACjD,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB,GAAG,OAAO,CAAC,IAAI,CAAC,CA6HhB"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import http from 'http';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { LOGIN_TIMEOUT } from '../config.js';
|
|
5
|
+
import { openBrowser } from '../open.js';
|
|
6
|
+
import { log } from '../output.js';
|
|
7
|
+
import { buildAuthorizeUrl, exchangeCodeForTokens, fetchUserInfo, generatePKCE, generateState, } from './oauth-client.js';
|
|
8
|
+
import { storeCredentials } from './token-store.js';
|
|
9
|
+
const SUCCESS_HTML = `<!DOCTYPE html>
|
|
10
|
+
<html>
|
|
11
|
+
<head><title>Skhema CLI</title><style>
|
|
12
|
+
body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; background: #0a0a0a; color: #fafafa; }
|
|
13
|
+
.card { text-align: center; padding: 2rem; }
|
|
14
|
+
h1 { font-size: 1.5rem; margin-bottom: 0.5rem; }
|
|
15
|
+
p { color: #a1a1aa; }
|
|
16
|
+
</style></head>
|
|
17
|
+
<body><div class="card"><h1>Authenticated</h1><p>You can close this window and return to the terminal.</p></div></body>
|
|
18
|
+
</html>`;
|
|
19
|
+
const ERROR_HTML = `<!DOCTYPE html>
|
|
20
|
+
<html>
|
|
21
|
+
<head><title>Skhema CLI - Error</title><style>
|
|
22
|
+
body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; background: #0a0a0a; color: #fafafa; }
|
|
23
|
+
.card { text-align: center; padding: 2rem; }
|
|
24
|
+
h1 { font-size: 1.5rem; color: #ef4444; margin-bottom: 0.5rem; }
|
|
25
|
+
p { color: #a1a1aa; }
|
|
26
|
+
</style></head>
|
|
27
|
+
<body><div class="card"><h1>Authentication Failed</h1><p>Please return to the terminal and try again.</p></div></body>
|
|
28
|
+
</html>`;
|
|
29
|
+
export async function startLocalhostFlow(options) {
|
|
30
|
+
const { codeVerifier, codeChallenge } = generatePKCE();
|
|
31
|
+
const state = generateState();
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
const server = http.createServer();
|
|
34
|
+
// Assigned once below, but read by `cleanup` before that point, so it must
|
|
35
|
+
// be declared up front (prefer-const cannot apply to a deferred init).
|
|
36
|
+
// eslint-disable-next-line prefer-const
|
|
37
|
+
let timeoutId;
|
|
38
|
+
const cleanup = () => {
|
|
39
|
+
clearTimeout(timeoutId);
|
|
40
|
+
server.close();
|
|
41
|
+
};
|
|
42
|
+
server.on('request', async (req, res) => {
|
|
43
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
44
|
+
if (url.pathname !== '/callback') {
|
|
45
|
+
res.writeHead(404);
|
|
46
|
+
res.end('Not found');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const code = url.searchParams.get('code');
|
|
50
|
+
const returnedState = url.searchParams.get('state');
|
|
51
|
+
const error = url.searchParams.get('error');
|
|
52
|
+
if (error) {
|
|
53
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
54
|
+
res.end(ERROR_HTML);
|
|
55
|
+
cleanup();
|
|
56
|
+
reject(new Error(`Authorization denied: ${error}`));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (!code || returnedState !== state) {
|
|
60
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
61
|
+
res.end(ERROR_HTML);
|
|
62
|
+
cleanup();
|
|
63
|
+
reject(new Error('Invalid callback: missing code or state mismatch'));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const spinner = ora('Exchanging authorization code...').start();
|
|
67
|
+
try {
|
|
68
|
+
const address = server.address();
|
|
69
|
+
const redirectUri = `http://127.0.0.1:${address.port}/callback`;
|
|
70
|
+
const tokens = await exchangeCodeForTokens({
|
|
71
|
+
code,
|
|
72
|
+
redirectUri,
|
|
73
|
+
codeVerifier,
|
|
74
|
+
});
|
|
75
|
+
spinner.text = 'Fetching user info...';
|
|
76
|
+
const userInfo = await fetchUserInfo(tokens.access_token);
|
|
77
|
+
const credentials = {
|
|
78
|
+
access_token: tokens.access_token,
|
|
79
|
+
refresh_token: tokens.refresh_token,
|
|
80
|
+
token_type: tokens.token_type,
|
|
81
|
+
expires_at: Date.now() + tokens.expires_in * 1000,
|
|
82
|
+
scope: tokens.scope,
|
|
83
|
+
user: {
|
|
84
|
+
id: userInfo.sub,
|
|
85
|
+
email: userInfo.email,
|
|
86
|
+
name: userInfo.name,
|
|
87
|
+
},
|
|
88
|
+
organization: userInfo.organizations?.[0]
|
|
89
|
+
? {
|
|
90
|
+
id: userInfo.organizations[0].id,
|
|
91
|
+
name: userInfo.organizations[0].name,
|
|
92
|
+
slug: userInfo.organizations[0].slug,
|
|
93
|
+
}
|
|
94
|
+
: undefined,
|
|
95
|
+
};
|
|
96
|
+
await storeCredentials(credentials);
|
|
97
|
+
spinner.succeed(`Logged in as ${chalk.cyan(userInfo.name)} (${userInfo.email})`);
|
|
98
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
99
|
+
res.end(SUCCESS_HTML);
|
|
100
|
+
cleanup();
|
|
101
|
+
resolve();
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
spinner.fail('Authentication failed');
|
|
105
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
106
|
+
res.end(ERROR_HTML);
|
|
107
|
+
cleanup();
|
|
108
|
+
reject(err);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
// Bind to random available port on localhost
|
|
112
|
+
server.listen(0, '127.0.0.1', () => {
|
|
113
|
+
const address = server.address();
|
|
114
|
+
const redirectUri = `http://127.0.0.1:${address.port}/callback`;
|
|
115
|
+
const authorizeUrl = buildAuthorizeUrl({
|
|
116
|
+
redirectUri,
|
|
117
|
+
codeChallenge,
|
|
118
|
+
state,
|
|
119
|
+
organizationId: options?.organizationId,
|
|
120
|
+
});
|
|
121
|
+
log(chalk.dim('Opening browser for authentication...'));
|
|
122
|
+
log(chalk.dim(`If the browser doesn't open, visit: ${authorizeUrl}`));
|
|
123
|
+
openBrowser(authorizeUrl).catch(() => {
|
|
124
|
+
log(chalk.yellow('Could not open browser automatically.'));
|
|
125
|
+
log(`Please visit: ${chalk.cyan(authorizeUrl)}`);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
// 5-minute timeout
|
|
129
|
+
timeoutId = setTimeout(() => {
|
|
130
|
+
cleanup();
|
|
131
|
+
reject(new Error('Login timed out after 5 minutes. Please try again.'));
|
|
132
|
+
}, LOGIN_TIMEOUT);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { PKCEPair, TokenResponse, UserInfoResponse } from './types.js';
|
|
2
|
+
export declare function generatePKCE(): PKCEPair;
|
|
3
|
+
export declare function generateState(): string;
|
|
4
|
+
export declare function buildAuthorizeUrl(params: {
|
|
5
|
+
redirectUri: string;
|
|
6
|
+
codeChallenge: string;
|
|
7
|
+
state: string;
|
|
8
|
+
organizationId?: string;
|
|
9
|
+
}): string;
|
|
10
|
+
export declare function exchangeCodeForTokens(params: {
|
|
11
|
+
code: string;
|
|
12
|
+
redirectUri: string;
|
|
13
|
+
codeVerifier: string;
|
|
14
|
+
}): Promise<TokenResponse>;
|
|
15
|
+
export declare function refreshAccessToken(refreshToken: string): Promise<TokenResponse>;
|
|
16
|
+
export declare class ConsentRequiredError extends Error {
|
|
17
|
+
constructor();
|
|
18
|
+
}
|
|
19
|
+
export interface SwitchOrganizationResponse {
|
|
20
|
+
access_token: string;
|
|
21
|
+
refresh_token: string;
|
|
22
|
+
token_type: string;
|
|
23
|
+
expires_in: number;
|
|
24
|
+
scope: string;
|
|
25
|
+
organization?: {
|
|
26
|
+
id: string;
|
|
27
|
+
name: string;
|
|
28
|
+
slug: string;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export declare function switchOrganization(accessToken: string, organizationId: string): Promise<SwitchOrganizationResponse>;
|
|
32
|
+
export declare function fetchUserInfo(accessToken: string): Promise<UserInfoResponse>;
|
|
33
|
+
//# sourceMappingURL=oauth-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth-client.d.ts","sourceRoot":"","sources":["../../../src/lib/auth/oauth-client.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAE3E,wBAAgB,YAAY,IAAI,QAAQ,CAOvC;AAED,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE;IACxC,WAAW,EAAE,MAAM,CAAA;IACnB,aAAa,EAAE,MAAM,CAAA;IACrB,KAAK,EAAE,MAAM,CAAA;IACb,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB,GAAG,MAAM,CAaT;AAED,wBAAsB,qBAAqB,CAAC,MAAM,EAAE;IAClD,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;CACrB,GAAG,OAAO,CAAC,aAAa,CAAC,CAqBzB;AAED,wBAAsB,kBAAkB,CACtC,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,aAAa,CAAC,CAkBxB;AAED,qBAAa,oBAAqB,SAAQ,KAAK;;CAK9C;AAED,MAAM,WAAW,0BAA0B;IACzC,YAAY,EAAE,MAAM,CAAA;IACpB,aAAa,EAAE,MAAM,CAAA;IACrB,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,MAAM,CAAA;IACb,YAAY,CAAC,EAAE;QACb,EAAE,EAAE,MAAM,CAAA;QACV,IAAI,EAAE,MAAM,CAAA;QACZ,IAAI,EAAE,MAAM,CAAA;KACb,CAAA;CACF;AAED,wBAAsB,kBAAkB,CACtC,WAAW,EAAE,MAAM,EACnB,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC,0BAA0B,CAAC,CAgCrC;AAED,wBAAsB,aAAa,CACjC,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,gBAAgB,CAAC,CAU3B"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import { AUTH_BASE_URL, CLI_CLIENT_ID, OAUTH_SCOPES } from '../config.js';
|
|
3
|
+
export function generatePKCE() {
|
|
4
|
+
const codeVerifier = crypto.randomBytes(32).toString('base64url');
|
|
5
|
+
const codeChallenge = crypto
|
|
6
|
+
.createHash('sha256')
|
|
7
|
+
.update(codeVerifier)
|
|
8
|
+
.digest('base64url');
|
|
9
|
+
return { codeVerifier, codeChallenge };
|
|
10
|
+
}
|
|
11
|
+
export function generateState() {
|
|
12
|
+
return crypto.randomBytes(16).toString('base64url');
|
|
13
|
+
}
|
|
14
|
+
export function buildAuthorizeUrl(params) {
|
|
15
|
+
const url = new URL(`${AUTH_BASE_URL}/api/auth/oauth/authorize`);
|
|
16
|
+
url.searchParams.set('client_id', CLI_CLIENT_ID);
|
|
17
|
+
url.searchParams.set('redirect_uri', params.redirectUri);
|
|
18
|
+
url.searchParams.set('response_type', 'code');
|
|
19
|
+
url.searchParams.set('scope', OAUTH_SCOPES);
|
|
20
|
+
url.searchParams.set('code_challenge', params.codeChallenge);
|
|
21
|
+
url.searchParams.set('code_challenge_method', 'S256');
|
|
22
|
+
url.searchParams.set('state', params.state);
|
|
23
|
+
if (params.organizationId) {
|
|
24
|
+
url.searchParams.set('organization_id', params.organizationId);
|
|
25
|
+
}
|
|
26
|
+
return url.toString();
|
|
27
|
+
}
|
|
28
|
+
export async function exchangeCodeForTokens(params) {
|
|
29
|
+
const response = await fetch(`${AUTH_BASE_URL}/api/auth/oauth/token`, {
|
|
30
|
+
method: 'POST',
|
|
31
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
32
|
+
body: new URLSearchParams({
|
|
33
|
+
grant_type: 'authorization_code',
|
|
34
|
+
client_id: CLI_CLIENT_ID,
|
|
35
|
+
code: params.code,
|
|
36
|
+
redirect_uri: params.redirectUri,
|
|
37
|
+
code_verifier: params.codeVerifier,
|
|
38
|
+
}),
|
|
39
|
+
});
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
const error = await response.json().catch(() => ({}));
|
|
42
|
+
throw new Error(`Token exchange failed: ${error.error || response.statusText}`);
|
|
43
|
+
}
|
|
44
|
+
return response.json();
|
|
45
|
+
}
|
|
46
|
+
export async function refreshAccessToken(refreshToken) {
|
|
47
|
+
const response = await fetch(`${AUTH_BASE_URL}/api/auth/oauth/token`, {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
50
|
+
body: new URLSearchParams({
|
|
51
|
+
grant_type: 'refresh_token',
|
|
52
|
+
client_id: CLI_CLIENT_ID,
|
|
53
|
+
refresh_token: refreshToken,
|
|
54
|
+
}),
|
|
55
|
+
});
|
|
56
|
+
if (!response.ok) {
|
|
57
|
+
throw new Error('Token refresh failed. Please re-authenticate with `skhema auth login`.');
|
|
58
|
+
}
|
|
59
|
+
return response.json();
|
|
60
|
+
}
|
|
61
|
+
export class ConsentRequiredError extends Error {
|
|
62
|
+
constructor() {
|
|
63
|
+
super('Consent required for this organization');
|
|
64
|
+
this.name = 'ConsentRequiredError';
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export async function switchOrganization(accessToken, organizationId) {
|
|
68
|
+
const response = await fetch(`${AUTH_BASE_URL}/api/auth/oauth/switch-organization`, {
|
|
69
|
+
method: 'POST',
|
|
70
|
+
headers: {
|
|
71
|
+
'Content-Type': 'application/json',
|
|
72
|
+
Authorization: `Bearer ${accessToken}`,
|
|
73
|
+
},
|
|
74
|
+
body: JSON.stringify({ organization_id: organizationId }),
|
|
75
|
+
});
|
|
76
|
+
if (response.status === 403) {
|
|
77
|
+
const body = (await response.json().catch(() => ({})));
|
|
78
|
+
if (body.error === 'consent_required') {
|
|
79
|
+
throw new ConsentRequiredError();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (!response.ok) {
|
|
83
|
+
const error = (await response.json().catch(() => ({})));
|
|
84
|
+
throw new Error(`Organization switch failed: ${error.error || response.statusText}`);
|
|
85
|
+
}
|
|
86
|
+
return response.json();
|
|
87
|
+
}
|
|
88
|
+
export async function fetchUserInfo(accessToken) {
|
|
89
|
+
const response = await fetch(`${AUTH_BASE_URL}/api/auth/oauth/userinfo`, {
|
|
90
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
91
|
+
});
|
|
92
|
+
if (!response.ok) {
|
|
93
|
+
throw new Error(`Failed to fetch user info: ${response.statusText}`);
|
|
94
|
+
}
|
|
95
|
+
return response.json();
|
|
96
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { StoredCredentials } from './types.js';
|
|
2
|
+
export declare function storeCredentials(credentials: StoredCredentials): Promise<void>;
|
|
3
|
+
export declare function getStoredCredentials(): Promise<StoredCredentials | null>;
|
|
4
|
+
export declare function clearCredentials(): Promise<void>;
|
|
5
|
+
export declare function getStorageLocation(): string;
|
|
6
|
+
//# sourceMappingURL=token-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-store.d.ts","sourceRoot":"","sources":["../../../src/lib/auth/token-store.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAoFnD,wBAAsB,gBAAgB,CACpC,WAAW,EAAE,iBAAiB,GAC7B,OAAO,CAAC,IAAI,CAAC,CAOf;AAED,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAuC9E;AAED,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAGtD;AAED,wBAAgB,kBAAkB,IAAI,MAAM,CAQ3C"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { CREDENTIALS_DIR, CREDENTIALS_FILE, KEYCHAIN_ACCOUNT, KEYCHAIN_SERVICE, } from '../config.js';
|
|
6
|
+
import { fetchUserInfo, refreshAccessToken } from './oauth-client.js';
|
|
7
|
+
const TOKEN_REFRESH_BUFFER = 5 * 60 * 1000; // 5 minutes before expiry
|
|
8
|
+
function getCredentialsFilePath() {
|
|
9
|
+
return path.join(os.homedir(), CREDENTIALS_DIR, CREDENTIALS_FILE);
|
|
10
|
+
}
|
|
11
|
+
function tryKeychainStore(data) {
|
|
12
|
+
if (process.platform !== 'darwin')
|
|
13
|
+
return false;
|
|
14
|
+
try {
|
|
15
|
+
// Delete existing entry if any
|
|
16
|
+
try {
|
|
17
|
+
execSync(`security delete-generic-password -s "${KEYCHAIN_SERVICE}" -a "${KEYCHAIN_ACCOUNT}" 2>/dev/null`, { stdio: 'ignore' });
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
/* ignore if doesn't exist */
|
|
21
|
+
}
|
|
22
|
+
execSync(`security add-generic-password -s "${KEYCHAIN_SERVICE}" -a "${KEYCHAIN_ACCOUNT}" -w "${data.replace(/"/g, '\\"')}"`, { stdio: 'ignore' });
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function tryKeychainLoad() {
|
|
30
|
+
if (process.platform !== 'darwin')
|
|
31
|
+
return null;
|
|
32
|
+
try {
|
|
33
|
+
const result = execSync(`security find-generic-password -s "${KEYCHAIN_SERVICE}" -a "${KEYCHAIN_ACCOUNT}" -w 2>/dev/null`, { encoding: 'utf-8' });
|
|
34
|
+
return result.trim();
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function tryKeychainDelete() {
|
|
41
|
+
if (process.platform !== 'darwin')
|
|
42
|
+
return false;
|
|
43
|
+
try {
|
|
44
|
+
execSync(`security delete-generic-password -s "${KEYCHAIN_SERVICE}" -a "${KEYCHAIN_ACCOUNT}" 2>/dev/null`, { stdio: 'ignore' });
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function storeToFile(data) {
|
|
52
|
+
const dir = path.join(os.homedir(), CREDENTIALS_DIR);
|
|
53
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
54
|
+
const filePath = getCredentialsFilePath();
|
|
55
|
+
fs.writeFileSync(filePath, data, { mode: 0o600 });
|
|
56
|
+
}
|
|
57
|
+
function loadFromFile() {
|
|
58
|
+
try {
|
|
59
|
+
const filePath = getCredentialsFilePath();
|
|
60
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function deleteFile() {
|
|
67
|
+
try {
|
|
68
|
+
const filePath = getCredentialsFilePath();
|
|
69
|
+
if (fs.existsSync(filePath)) {
|
|
70
|
+
fs.unlinkSync(filePath);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
/* ignore */
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export async function storeCredentials(credentials) {
|
|
78
|
+
const data = JSON.stringify(credentials);
|
|
79
|
+
// Try keychain first, fall back to file
|
|
80
|
+
if (!tryKeychainStore(data)) {
|
|
81
|
+
storeToFile(data);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export async function getStoredCredentials() {
|
|
85
|
+
// Try keychain first, then file
|
|
86
|
+
const data = tryKeychainLoad() ?? loadFromFile();
|
|
87
|
+
if (!data)
|
|
88
|
+
return null;
|
|
89
|
+
try {
|
|
90
|
+
const credentials = JSON.parse(data);
|
|
91
|
+
// Auto-refresh if within buffer of expiry
|
|
92
|
+
if (credentials.expires_at - Date.now() < TOKEN_REFRESH_BUFFER) {
|
|
93
|
+
try {
|
|
94
|
+
const newTokens = await refreshAccessToken(credentials.refresh_token);
|
|
95
|
+
const userInfo = await fetchUserInfo(newTokens.access_token);
|
|
96
|
+
const refreshed = {
|
|
97
|
+
...credentials,
|
|
98
|
+
access_token: newTokens.access_token,
|
|
99
|
+
refresh_token: newTokens.refresh_token,
|
|
100
|
+
expires_at: Date.now() + newTokens.expires_in * 1000,
|
|
101
|
+
scope: newTokens.scope,
|
|
102
|
+
user: {
|
|
103
|
+
id: userInfo.sub,
|
|
104
|
+
email: userInfo.email,
|
|
105
|
+
name: userInfo.name,
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
await storeCredentials(refreshed);
|
|
109
|
+
return refreshed;
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// Refresh failed - return stale credentials, caller can handle
|
|
113
|
+
return credentials;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return credentials;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
export async function clearCredentials() {
|
|
123
|
+
tryKeychainDelete();
|
|
124
|
+
deleteFile();
|
|
125
|
+
}
|
|
126
|
+
export function getStorageLocation() {
|
|
127
|
+
if (process.platform === 'darwin') {
|
|
128
|
+
const keychainData = tryKeychainLoad();
|
|
129
|
+
if (keychainData)
|
|
130
|
+
return `macOS Keychain (${KEYCHAIN_SERVICE})`;
|
|
131
|
+
}
|
|
132
|
+
const filePath = getCredentialsFilePath();
|
|
133
|
+
if (fs.existsSync(filePath))
|
|
134
|
+
return filePath;
|
|
135
|
+
return 'Not stored';
|
|
136
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export interface StoredCredentials {
|
|
2
|
+
access_token: string;
|
|
3
|
+
refresh_token: string;
|
|
4
|
+
token_type: string;
|
|
5
|
+
expires_at: number;
|
|
6
|
+
scope: string;
|
|
7
|
+
user: {
|
|
8
|
+
id: string;
|
|
9
|
+
email: string;
|
|
10
|
+
name: string;
|
|
11
|
+
};
|
|
12
|
+
organization?: {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
slug: string;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export interface DeviceCodeResponse {
|
|
19
|
+
device_code: string;
|
|
20
|
+
user_code: string;
|
|
21
|
+
verification_uri: string;
|
|
22
|
+
verification_uri_complete: string;
|
|
23
|
+
expires_in: number;
|
|
24
|
+
interval: number;
|
|
25
|
+
}
|
|
26
|
+
export interface TokenResponse {
|
|
27
|
+
access_token: string;
|
|
28
|
+
token_type: string;
|
|
29
|
+
expires_in: number;
|
|
30
|
+
refresh_token: string;
|
|
31
|
+
scope: string;
|
|
32
|
+
}
|
|
33
|
+
export interface PKCEPair {
|
|
34
|
+
codeVerifier: string;
|
|
35
|
+
codeChallenge: string;
|
|
36
|
+
}
|
|
37
|
+
export interface UserInfoResponse {
|
|
38
|
+
sub: string;
|
|
39
|
+
email: string;
|
|
40
|
+
name: string;
|
|
41
|
+
email_verified: boolean;
|
|
42
|
+
organizations?: Array<{
|
|
43
|
+
id: string;
|
|
44
|
+
name: string;
|
|
45
|
+
slug: string;
|
|
46
|
+
logo?: string;
|
|
47
|
+
}>;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/lib/auth/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB;IAChC,YAAY,EAAE,MAAM,CAAA;IACpB,aAAa,EAAE,MAAM,CAAA;IACrB,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE;QACJ,EAAE,EAAE,MAAM,CAAA;QACV,KAAK,EAAE,MAAM,CAAA;QACb,IAAI,EAAE,MAAM,CAAA;KACb,CAAA;IACD,YAAY,CAAC,EAAE;QACb,EAAE,EAAE,MAAM,CAAA;QACV,IAAI,EAAE,MAAM,CAAA;QACZ,IAAI,EAAE,MAAM,CAAA;KACb,CAAA;CACF;AAED,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,gBAAgB,EAAE,MAAM,CAAA;IACxB,yBAAyB,EAAE,MAAM,CAAA;IACjC,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,MAAM,CAAA;IACpB,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,aAAa,EAAE,MAAM,CAAA;IACrB,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,QAAQ;IACvB,YAAY,EAAE,MAAM,CAAA;IACpB,aAAa,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,cAAc,EAAE,OAAO,CAAA;IACvB,aAAa,CAAC,EAAE,KAAK,CAAC;QACpB,EAAE,EAAE,MAAM,CAAA;QACV,IAAI,EAAE,MAAM,CAAA;QACZ,IAAI,EAAE,MAAM,CAAA;QACZ,IAAI,CAAC,EAAE,MAAM,CAAA;KACd,CAAC,CAAA;CACH"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"banner.d.ts","sourceRoot":"","sources":["../../src/lib/banner.ts"],"names":[],"mappings":"AAmEA,wBAAgB,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAkCnD"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
// ── Brand colors ────────────────────────────────────────────
|
|
3
|
+
const BRAND_SECONDARY = '#ff82a2';
|
|
4
|
+
function brandGradient(lines) {
|
|
5
|
+
return lines.map((line, i) => {
|
|
6
|
+
const t = lines.length > 1 ? i / (lines.length - 1) : 0;
|
|
7
|
+
const r = Math.round(0xff + (0xcd - 0xff) * t);
|
|
8
|
+
const g = Math.round(0x82 + (0x47 - 0x82) * t);
|
|
9
|
+
const b = Math.round(0xa2 + (0x6a - 0xa2) * t);
|
|
10
|
+
const hex = `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
|
|
11
|
+
return chalk.hex(hex)(line);
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
// ── ANSI helpers ──────────────────────────────────────────────
|
|
15
|
+
function stripAnsi(str) {
|
|
16
|
+
// eslint-disable-next-line no-control-regex
|
|
17
|
+
return str.replace(/\x1b\[[0-9;]*m/g, '');
|
|
18
|
+
}
|
|
19
|
+
// ── Box-drawing ──────────────────────────────────────────────
|
|
20
|
+
const BOX = {
|
|
21
|
+
tl: '\u250c', // ┌
|
|
22
|
+
tr: '\u2510', // ┐
|
|
23
|
+
bl: '\u2514', // └
|
|
24
|
+
br: '\u2518', // ┘
|
|
25
|
+
h: '\u2500', // ─
|
|
26
|
+
v: '\u2502', // │
|
|
27
|
+
};
|
|
28
|
+
function drawBox(title, lines, width) {
|
|
29
|
+
const innerWidth = width - 2;
|
|
30
|
+
const label = ` ${title} `;
|
|
31
|
+
const remaining = innerWidth - label.length;
|
|
32
|
+
const left = Math.floor(remaining / 2);
|
|
33
|
+
const right = remaining - left;
|
|
34
|
+
const top = `${BOX.tl}${BOX.h.repeat(left)}${label}${BOX.h.repeat(right)}${BOX.tr}`;
|
|
35
|
+
const rows = lines.map((line) => {
|
|
36
|
+
const visible = stripAnsi(line).length;
|
|
37
|
+
const pad = innerWidth - visible;
|
|
38
|
+
return `${BOX.v}${line}${' '.repeat(Math.max(0, pad))}${BOX.v}`;
|
|
39
|
+
});
|
|
40
|
+
const bottom = `${BOX.bl}${BOX.h.repeat(innerWidth)}${BOX.br}`;
|
|
41
|
+
return [top, ...rows, bottom].map((l) => chalk.dim(l)).join('\n');
|
|
42
|
+
}
|
|
43
|
+
// ── ASCII art (ANSI Shadow style) ────────────────────────────
|
|
44
|
+
const ASCII_ART = `
|
|
45
|
+
███████╗██╗ ██╗██╗ ██╗███████╗███╗ ███╗ █████╗
|
|
46
|
+
██╔════╝██║ ██╔╝██║ ██║██╔════╝████╗ ████║██╔══██╗
|
|
47
|
+
███████╗█████╔╝ ███████║█████╗ ██╔████╔██║███████║
|
|
48
|
+
╚════██║██╔═██╗ ██╔══██║██╔══╝ ██║╚██╔╝██║██╔══██║
|
|
49
|
+
███████║██║ ██╗██║ ██║███████╗██║ ╚═╝ ██║██║ ██║ ██╗
|
|
50
|
+
╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝`.trimStart();
|
|
51
|
+
// ── Main export ──────────────────────────────────────────────
|
|
52
|
+
export function showBanner(cliVersion) {
|
|
53
|
+
const args = process.argv;
|
|
54
|
+
if (args.includes('--quiet') || args.includes('--json'))
|
|
55
|
+
return;
|
|
56
|
+
if (!process.stdout.isTTY)
|
|
57
|
+
return;
|
|
58
|
+
const boxWidth = 58;
|
|
59
|
+
// ── ASCII art ──
|
|
60
|
+
const rawLines = ASCII_ART.split('\n');
|
|
61
|
+
const artLines = brandGradient(rawLines).map((line) => ' ' + line);
|
|
62
|
+
console.log('');
|
|
63
|
+
console.log(artLines.join('\n'));
|
|
64
|
+
// ── Version ──
|
|
65
|
+
console.log(chalk.dim(` v${cliVersion}`));
|
|
66
|
+
console.log('');
|
|
67
|
+
// ── Getting Started box ──
|
|
68
|
+
const tips = [
|
|
69
|
+
['skhema auth login', 'Browser OAuth login'],
|
|
70
|
+
['skhema auth login --headless', 'Device code (SSH)'],
|
|
71
|
+
['skhema auth status', 'Show auth status'],
|
|
72
|
+
['skhema skills install', 'Install AI skills'],
|
|
73
|
+
['skhema skills list', 'Show installed skills'],
|
|
74
|
+
['skhema --help', 'Show all commands'],
|
|
75
|
+
];
|
|
76
|
+
const cmdWidth = 32;
|
|
77
|
+
const tipLines = tips.map(([cmd, desc]) => {
|
|
78
|
+
return ` ${chalk.hex(BRAND_SECONDARY)(cmd.padEnd(cmdWidth))}${desc}`;
|
|
79
|
+
});
|
|
80
|
+
console.log(drawBox('Getting Started', tipLines, boxWidth));
|
|
81
|
+
console.log('');
|
|
82
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User-level CLI preferences persisted at `~/.skhema/config.json` (mode 0600).
|
|
3
|
+
* Distinct from the credential stores (keychain/`credentials.json`/`api-key.json`)
|
|
4
|
+
* — this file holds only non-secret defaults such as the active workspace.
|
|
5
|
+
* Writes are merge-safe: unknown keys already in the file are preserved.
|
|
6
|
+
*/
|
|
7
|
+
export interface CliConfig {
|
|
8
|
+
/** Default workspace id used when a command omits `--workspace`. */
|
|
9
|
+
defaultWorkspaceId?: string;
|
|
10
|
+
[key: string]: unknown;
|
|
11
|
+
}
|
|
12
|
+
/** Read the full config object. Returns `{}` when the file is absent or invalid. */
|
|
13
|
+
export declare function readConfig(): CliConfig;
|
|
14
|
+
/**
|
|
15
|
+
* Merge `patch` into the existing config and persist it. Keys set to `undefined`
|
|
16
|
+
* are removed. The directory (0700) and file (0600) are created as needed.
|
|
17
|
+
*/
|
|
18
|
+
export declare function writeConfig(patch: CliConfig): CliConfig;
|
|
19
|
+
export declare function getDefaultWorkspaceId(): string | undefined;
|
|
20
|
+
/** Set (or, with `undefined`, clear) the default workspace id. */
|
|
21
|
+
export declare function setDefaultWorkspaceId(workspaceId: string | undefined): void;
|
|
22
|
+
export declare function getConfigLocation(): string;
|
|
23
|
+
//# sourceMappingURL=config-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-store.d.ts","sourceRoot":"","sources":["../../src/lib/config-store.ts"],"names":[],"mappings":"AAKA;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACxB,oEAAoE;IACpE,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACvB;AAUD,oFAAoF;AACpF,wBAAgB,UAAU,IAAI,SAAS,CAWtC;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,SAAS,GAAG,SAAS,CAavD;AAED,wBAAgB,qBAAqB,IAAI,MAAM,GAAG,SAAS,CAE1D;AAED,kEAAkE;AAClE,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAE3E;AAED,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { CONFIG_FILE, CREDENTIALS_DIR } from './config.js';
|
|
5
|
+
function getConfigDir() {
|
|
6
|
+
return path.join(os.homedir(), CREDENTIALS_DIR);
|
|
7
|
+
}
|
|
8
|
+
function getConfigFilePath() {
|
|
9
|
+
return path.join(getConfigDir(), CONFIG_FILE);
|
|
10
|
+
}
|
|
11
|
+
/** Read the full config object. Returns `{}` when the file is absent or invalid. */
|
|
12
|
+
export function readConfig() {
|
|
13
|
+
try {
|
|
14
|
+
const raw = fs.readFileSync(getConfigFilePath(), 'utf-8');
|
|
15
|
+
const parsed = JSON.parse(raw);
|
|
16
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
17
|
+
return parsed;
|
|
18
|
+
}
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Merge `patch` into the existing config and persist it. Keys set to `undefined`
|
|
27
|
+
* are removed. The directory (0700) and file (0600) are created as needed.
|
|
28
|
+
*/
|
|
29
|
+
export function writeConfig(patch) {
|
|
30
|
+
const current = readConfig();
|
|
31
|
+
const next = { ...current };
|
|
32
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
33
|
+
if (value === undefined)
|
|
34
|
+
delete next[key];
|
|
35
|
+
else
|
|
36
|
+
next[key] = value;
|
|
37
|
+
}
|
|
38
|
+
fs.mkdirSync(getConfigDir(), { recursive: true, mode: 0o700 });
|
|
39
|
+
fs.writeFileSync(getConfigFilePath(), JSON.stringify(next, null, 2) + '\n', {
|
|
40
|
+
mode: 0o600,
|
|
41
|
+
});
|
|
42
|
+
return next;
|
|
43
|
+
}
|
|
44
|
+
export function getDefaultWorkspaceId() {
|
|
45
|
+
return readConfig().defaultWorkspaceId;
|
|
46
|
+
}
|
|
47
|
+
/** Set (or, with `undefined`, clear) the default workspace id. */
|
|
48
|
+
export function setDefaultWorkspaceId(workspaceId) {
|
|
49
|
+
writeConfig({ defaultWorkspaceId: workspaceId });
|
|
50
|
+
}
|
|
51
|
+
export function getConfigLocation() {
|
|
52
|
+
return getConfigFilePath();
|
|
53
|
+
}
|