skilluse 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.
- package/README.md +100 -0
- package/dist/app.d.ts +6 -0
- package/dist/app.js +6 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +167 -0
- package/dist/commands/demo.d.ts +14 -0
- package/dist/commands/demo.js +46 -0
- package/dist/commands/index.d.ts +8 -0
- package/dist/commands/index.js +77 -0
- package/dist/commands/list.d.ts +14 -0
- package/dist/commands/list.js +54 -0
- package/dist/commands/login.d.ts +14 -0
- package/dist/commands/login.js +153 -0
- package/dist/commands/logout.d.ts +8 -0
- package/dist/commands/logout.js +47 -0
- package/dist/commands/repo/add.d.ts +22 -0
- package/dist/commands/repo/add.js +139 -0
- package/dist/commands/repo/edit.d.ts +19 -0
- package/dist/commands/repo/edit.js +117 -0
- package/dist/commands/repo/index.d.ts +8 -0
- package/dist/commands/repo/index.js +47 -0
- package/dist/commands/repo/list.d.ts +8 -0
- package/dist/commands/repo/list.js +47 -0
- package/dist/commands/repo/remove.d.ts +16 -0
- package/dist/commands/repo/remove.js +83 -0
- package/dist/commands/repo/sync.d.ts +10 -0
- package/dist/commands/repo/sync.js +78 -0
- package/dist/commands/repo/use.d.ts +10 -0
- package/dist/commands/repo/use.js +56 -0
- package/dist/commands/repos.d.ts +11 -0
- package/dist/commands/repos.js +50 -0
- package/dist/commands/search.d.ts +16 -0
- package/dist/commands/search.js +199 -0
- package/dist/commands/skills.d.ts +11 -0
- package/dist/commands/skills.js +43 -0
- package/dist/commands/whoami.d.ts +8 -0
- package/dist/commands/whoami.js +69 -0
- package/dist/components/CLIError.d.ts +27 -0
- package/dist/components/CLIError.js +24 -0
- package/dist/components/ProgressBar.d.ts +7 -0
- package/dist/components/ProgressBar.js +9 -0
- package/dist/components/Select.d.ts +11 -0
- package/dist/components/Select.js +6 -0
- package/dist/components/Spinner.d.ts +6 -0
- package/dist/components/Spinner.js +7 -0
- package/dist/components/StatusMessage.d.ts +9 -0
- package/dist/components/StatusMessage.js +13 -0
- package/dist/components/Table.d.ts +9 -0
- package/dist/components/Table.js +27 -0
- package/dist/components/TextInput.d.ts +9 -0
- package/dist/components/TextInput.js +6 -0
- package/dist/components/index.d.ts +8 -0
- package/dist/components/index.js +8 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +43815 -0
- package/dist/services/credentials.d.ts +69 -0
- package/dist/services/credentials.js +216 -0
- package/dist/services/index.d.ts +6 -0
- package/dist/services/index.js +10 -0
- package/dist/services/oauth.d.ts +106 -0
- package/dist/services/oauth.js +208 -0
- package/dist/services/paths.d.ts +19 -0
- package/dist/services/paths.js +21 -0
- package/dist/services/store.d.ts +64 -0
- package/dist/services/store.js +107 -0
- package/dist/services/update.d.ts +20 -0
- package/dist/services/update.js +93 -0
- package/package.json +70 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secure credential storage service.
|
|
3
|
+
*
|
|
4
|
+
* Stores OAuth tokens securely using:
|
|
5
|
+
* 1. System keychain (preferred) - macOS Keychain, Windows Credential Manager, Linux libsecret
|
|
6
|
+
* 2. Encrypted file fallback - when keychain is unavailable
|
|
7
|
+
*
|
|
8
|
+
* Storage strategy:
|
|
9
|
+
* - User token → Keychain/encrypted file (sensitive)
|
|
10
|
+
* - Installation list → Config file JSON via store.ts (metadata)
|
|
11
|
+
* - Installation token → Memory cache (short-lived, 1 hour)
|
|
12
|
+
*/
|
|
13
|
+
/** @deprecated Use UserCredentials instead */
|
|
14
|
+
export interface Credentials {
|
|
15
|
+
token: string;
|
|
16
|
+
user: string;
|
|
17
|
+
}
|
|
18
|
+
export interface UserCredentials {
|
|
19
|
+
token: string;
|
|
20
|
+
userName: string;
|
|
21
|
+
}
|
|
22
|
+
export interface InstallationTokenCache {
|
|
23
|
+
installationId: number;
|
|
24
|
+
token: string;
|
|
25
|
+
expiresAt: Date;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Check if the system keychain is available and working.
|
|
29
|
+
*/
|
|
30
|
+
export declare function isKeychainAvailable(): Promise<boolean>;
|
|
31
|
+
/**
|
|
32
|
+
* Get stored credentials from keychain or encrypted file.
|
|
33
|
+
*/
|
|
34
|
+
export declare function getCredentials(): Promise<Credentials | null>;
|
|
35
|
+
/**
|
|
36
|
+
* Store credentials in keychain or encrypted file.
|
|
37
|
+
*/
|
|
38
|
+
export declare function setCredentials(token: string, user: string): Promise<void>;
|
|
39
|
+
/**
|
|
40
|
+
* Clear stored credentials from both keychain and encrypted file.
|
|
41
|
+
*/
|
|
42
|
+
export declare function clearCredentials(): Promise<void>;
|
|
43
|
+
/**
|
|
44
|
+
* Store user credentials (token and username) securely.
|
|
45
|
+
*/
|
|
46
|
+
export declare function setUserCredentials(token: string, userName: string): Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* Get user credentials from secure storage.
|
|
49
|
+
*/
|
|
50
|
+
export declare function getUserCredentials(): Promise<UserCredentials | null>;
|
|
51
|
+
/**
|
|
52
|
+
* Get cached installation token if it exists and is not expired.
|
|
53
|
+
* Returns null if no valid token is cached.
|
|
54
|
+
*/
|
|
55
|
+
export declare function getCachedInstallationToken(installationId: number): InstallationTokenCache | null;
|
|
56
|
+
/**
|
|
57
|
+
* Cache an installation token.
|
|
58
|
+
*/
|
|
59
|
+
export declare function setCachedInstallationToken(cache: InstallationTokenCache): void;
|
|
60
|
+
/**
|
|
61
|
+
* Clear all cached installation tokens.
|
|
62
|
+
*/
|
|
63
|
+
export declare function clearInstallationTokenCache(): void;
|
|
64
|
+
/**
|
|
65
|
+
* Clear all credentials: user token, and installation token cache.
|
|
66
|
+
* Note: Installation list and default installation are cleared via store.ts
|
|
67
|
+
*/
|
|
68
|
+
export declare function clearAllCredentials(): Promise<void>;
|
|
69
|
+
//# sourceMappingURL=credentials.d.ts.map
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secure credential storage service.
|
|
3
|
+
*
|
|
4
|
+
* Stores OAuth tokens securely using:
|
|
5
|
+
* 1. System keychain (preferred) - macOS Keychain, Windows Credential Manager, Linux libsecret
|
|
6
|
+
* 2. Encrypted file fallback - when keychain is unavailable
|
|
7
|
+
*
|
|
8
|
+
* Storage strategy:
|
|
9
|
+
* - User token → Keychain/encrypted file (sensitive)
|
|
10
|
+
* - Installation list → Config file JSON via store.ts (metadata)
|
|
11
|
+
* - Installation token → Memory cache (short-lived, 1 hour)
|
|
12
|
+
*/
|
|
13
|
+
import crypto from "crypto";
|
|
14
|
+
import fs from "fs/promises";
|
|
15
|
+
import os from "os";
|
|
16
|
+
import path from "path";
|
|
17
|
+
import keytar from "keytar";
|
|
18
|
+
import { dataPath } from "./paths.js";
|
|
19
|
+
const SERVICE_NAME = "skilluse";
|
|
20
|
+
const CREDENTIALS_FILE = "credentials.enc";
|
|
21
|
+
// In-memory cache for short-lived installation tokens
|
|
22
|
+
const installationTokenCache = new Map();
|
|
23
|
+
// Cache keychain availability to avoid repeated checks
|
|
24
|
+
let keychainAvailable = null;
|
|
25
|
+
/**
|
|
26
|
+
* Check if the system keychain is available and working.
|
|
27
|
+
*/
|
|
28
|
+
export async function isKeychainAvailable() {
|
|
29
|
+
if (keychainAvailable !== null) {
|
|
30
|
+
return keychainAvailable;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
// Try a test operation to see if keychain is working
|
|
34
|
+
const testKey = "__keychain_test__";
|
|
35
|
+
await keytar.setPassword(SERVICE_NAME, testKey, "test");
|
|
36
|
+
await keytar.deletePassword(SERVICE_NAME, testKey);
|
|
37
|
+
keychainAvailable = true;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
keychainAvailable = false;
|
|
41
|
+
}
|
|
42
|
+
return keychainAvailable;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Get stored credentials from keychain or encrypted file.
|
|
46
|
+
*/
|
|
47
|
+
export async function getCredentials() {
|
|
48
|
+
if (await isKeychainAvailable()) {
|
|
49
|
+
return getCredentialsFromKeychain();
|
|
50
|
+
}
|
|
51
|
+
return getCredentialsFromFile();
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Store credentials in keychain or encrypted file.
|
|
55
|
+
*/
|
|
56
|
+
export async function setCredentials(token, user) {
|
|
57
|
+
if (await isKeychainAvailable()) {
|
|
58
|
+
await setCredentialsToKeychain(token, user);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
await setCredentialsToFile(token, user);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Clear stored credentials from both keychain and encrypted file.
|
|
66
|
+
*/
|
|
67
|
+
export async function clearCredentials() {
|
|
68
|
+
// Clear from keychain
|
|
69
|
+
try {
|
|
70
|
+
await keytar.deletePassword(SERVICE_NAME, "github-token");
|
|
71
|
+
await keytar.deletePassword(SERVICE_NAME, "github-user");
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Ignore keychain errors
|
|
75
|
+
}
|
|
76
|
+
// Clear encrypted file
|
|
77
|
+
try {
|
|
78
|
+
const filePath = path.join(dataPath, CREDENTIALS_FILE);
|
|
79
|
+
await fs.unlink(filePath);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// Ignore file errors (file may not exist)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// --- Keychain operations ---
|
|
86
|
+
async function getCredentialsFromKeychain() {
|
|
87
|
+
try {
|
|
88
|
+
const token = await keytar.getPassword(SERVICE_NAME, "github-token");
|
|
89
|
+
const user = await keytar.getPassword(SERVICE_NAME, "github-user");
|
|
90
|
+
if (token && user) {
|
|
91
|
+
return { token, user };
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async function setCredentialsToKeychain(token, user) {
|
|
100
|
+
await keytar.setPassword(SERVICE_NAME, "github-token", token);
|
|
101
|
+
await keytar.setPassword(SERVICE_NAME, "github-user", user);
|
|
102
|
+
}
|
|
103
|
+
// --- Encrypted file operations ---
|
|
104
|
+
/**
|
|
105
|
+
* Derive an encryption key from machine-specific information.
|
|
106
|
+
* This provides basic protection against copying the file to another machine.
|
|
107
|
+
*/
|
|
108
|
+
function deriveKey() {
|
|
109
|
+
const machineInfo = `${os.hostname()}:${os.userInfo().username}:${SERVICE_NAME}`;
|
|
110
|
+
return crypto.scryptSync(machineInfo, "skilluse-salt", 32);
|
|
111
|
+
}
|
|
112
|
+
function encrypt(data) {
|
|
113
|
+
const key = deriveKey();
|
|
114
|
+
const iv = crypto.randomBytes(16);
|
|
115
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
|
|
116
|
+
let encrypted = cipher.update(data, "utf8", "hex");
|
|
117
|
+
encrypted += cipher.final("hex");
|
|
118
|
+
const authTag = cipher.getAuthTag();
|
|
119
|
+
// Format: iv:authTag:encryptedData
|
|
120
|
+
return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`;
|
|
121
|
+
}
|
|
122
|
+
function decrypt(encryptedData) {
|
|
123
|
+
const [ivHex, authTagHex, encrypted] = encryptedData.split(":");
|
|
124
|
+
const key = deriveKey();
|
|
125
|
+
const iv = Buffer.from(ivHex, "hex");
|
|
126
|
+
const authTag = Buffer.from(authTagHex, "hex");
|
|
127
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
|
|
128
|
+
decipher.setAuthTag(authTag);
|
|
129
|
+
let decrypted = decipher.update(encrypted, "hex", "utf8");
|
|
130
|
+
decrypted += decipher.final("utf8");
|
|
131
|
+
return decrypted;
|
|
132
|
+
}
|
|
133
|
+
async function getCredentialsFromFile() {
|
|
134
|
+
try {
|
|
135
|
+
const filePath = path.join(dataPath, CREDENTIALS_FILE);
|
|
136
|
+
const encryptedData = await fs.readFile(filePath, "utf8");
|
|
137
|
+
const decrypted = decrypt(encryptedData);
|
|
138
|
+
return JSON.parse(decrypted);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async function setCredentialsToFile(token, user) {
|
|
145
|
+
const credentials = { token, user };
|
|
146
|
+
const encrypted = encrypt(JSON.stringify(credentials));
|
|
147
|
+
// Ensure data directory exists
|
|
148
|
+
await fs.mkdir(dataPath, { recursive: true });
|
|
149
|
+
const filePath = path.join(dataPath, CREDENTIALS_FILE);
|
|
150
|
+
await fs.writeFile(filePath, encrypted, { mode: 0o600 }); // Owner read/write only
|
|
151
|
+
}
|
|
152
|
+
// ============================================================================
|
|
153
|
+
// New GitHub App Credential Functions
|
|
154
|
+
// ============================================================================
|
|
155
|
+
/**
|
|
156
|
+
* Store user credentials (token and username) securely.
|
|
157
|
+
*/
|
|
158
|
+
export async function setUserCredentials(token, userName) {
|
|
159
|
+
// Reuse existing implementation with new interface
|
|
160
|
+
await setCredentials(token, userName);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Get user credentials from secure storage.
|
|
164
|
+
*/
|
|
165
|
+
export async function getUserCredentials() {
|
|
166
|
+
const creds = await getCredentials();
|
|
167
|
+
if (!creds)
|
|
168
|
+
return null;
|
|
169
|
+
return {
|
|
170
|
+
token: creds.token,
|
|
171
|
+
userName: creds.user,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
// ============================================================================
|
|
175
|
+
// Installation Token Cache (in-memory, short-lived)
|
|
176
|
+
// ============================================================================
|
|
177
|
+
/**
|
|
178
|
+
* Get cached installation token if it exists and is not expired.
|
|
179
|
+
* Returns null if no valid token is cached.
|
|
180
|
+
*/
|
|
181
|
+
export function getCachedInstallationToken(installationId) {
|
|
182
|
+
const cached = installationTokenCache.get(installationId);
|
|
183
|
+
if (!cached)
|
|
184
|
+
return null;
|
|
185
|
+
// Check if token is expired (with 5 minute buffer for safety)
|
|
186
|
+
const bufferMs = 5 * 60 * 1000;
|
|
187
|
+
if (new Date(cached.expiresAt).getTime() - bufferMs < Date.now()) {
|
|
188
|
+
// Token is expired or about to expire, remove it
|
|
189
|
+
installationTokenCache.delete(installationId);
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
return cached;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Cache an installation token.
|
|
196
|
+
*/
|
|
197
|
+
export function setCachedInstallationToken(cache) {
|
|
198
|
+
installationTokenCache.set(cache.installationId, cache);
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Clear all cached installation tokens.
|
|
202
|
+
*/
|
|
203
|
+
export function clearInstallationTokenCache() {
|
|
204
|
+
installationTokenCache.clear();
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Clear all credentials: user token, and installation token cache.
|
|
208
|
+
* Note: Installation list and default installation are cleared via store.ts
|
|
209
|
+
*/
|
|
210
|
+
export async function clearAllCredentials() {
|
|
211
|
+
// Clear user credentials
|
|
212
|
+
await clearCredentials();
|
|
213
|
+
// Clear installation token cache
|
|
214
|
+
clearInstallationTokenCache();
|
|
215
|
+
}
|
|
216
|
+
//# sourceMappingURL=credentials.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { requestDeviceCode, pollForAccessToken, pollUntilComplete, openBrowser, sleep, getUserInstallations, getInstallationRepositories, getInstallationToken, type DeviceCodeResponse, type AccessTokenResponse, type OAuthError, type PollResult, type Installation, type Repository, type InstallationToken, } from "./oauth.js";
|
|
2
|
+
export { getConfig, addRepo, removeRepo, setDefaultRepo, addInstalledSkill, removeInstalledSkill, setInstallations, getInstallations, setDefaultInstallation, getDefaultInstallation, clearInstallations, isFirstRun, type Config, type RepoConfig, type InstalledSkill, type StoredInstallation, } from "./store.js";
|
|
3
|
+
export { configPath, dataPath, cachePath, logPath, tempPath, } from "./paths.js";
|
|
4
|
+
export { getCredentials, setCredentials, clearCredentials, isKeychainAvailable, type Credentials, setUserCredentials, getUserCredentials, getCachedInstallationToken, setCachedInstallationToken, clearInstallationTokenCache, clearAllCredentials, type UserCredentials, type InstallationTokenCache, } from "./credentials.js";
|
|
5
|
+
export { checkForUpdate, getCurrentVersion, type UpdateInfo, } from "./update.js";
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { requestDeviceCode, pollForAccessToken, pollUntilComplete, openBrowser, sleep, getUserInstallations, getInstallationRepositories, getInstallationToken, } from "./oauth.js";
|
|
2
|
+
export { getConfig, addRepo, removeRepo, setDefaultRepo, addInstalledSkill, removeInstalledSkill, setInstallations, getInstallations, setDefaultInstallation, getDefaultInstallation, clearInstallations, isFirstRun, } from "./store.js";
|
|
3
|
+
export { configPath, dataPath, cachePath, logPath, tempPath, } from "./paths.js";
|
|
4
|
+
export {
|
|
5
|
+
// Legacy (deprecated)
|
|
6
|
+
getCredentials, setCredentials, clearCredentials, isKeychainAvailable,
|
|
7
|
+
// New GitHub App credential functions
|
|
8
|
+
setUserCredentials, getUserCredentials, getCachedInstallationToken, setCachedInstallationToken, clearInstallationTokenCache, clearAllCredentials, } from "./credentials.js";
|
|
9
|
+
export { checkForUpdate, getCurrentVersion, } from "./update.js";
|
|
10
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub OAuth Device Flow implementation
|
|
3
|
+
* Reference: https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#device-flow
|
|
4
|
+
*/
|
|
5
|
+
export interface Installation {
|
|
6
|
+
id: number;
|
|
7
|
+
account: {
|
|
8
|
+
login: string;
|
|
9
|
+
type: "User" | "Organization";
|
|
10
|
+
};
|
|
11
|
+
repository_selection: "all" | "selected";
|
|
12
|
+
permissions: Record<string, string>;
|
|
13
|
+
}
|
|
14
|
+
export interface Repository {
|
|
15
|
+
id: number;
|
|
16
|
+
name: string;
|
|
17
|
+
full_name: string;
|
|
18
|
+
private: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface InstallationToken {
|
|
21
|
+
token: string;
|
|
22
|
+
expires_at: string;
|
|
23
|
+
permissions: Record<string, string>;
|
|
24
|
+
repositories?: Repository[];
|
|
25
|
+
}
|
|
26
|
+
export interface DeviceCodeResponse {
|
|
27
|
+
device_code: string;
|
|
28
|
+
user_code: string;
|
|
29
|
+
verification_uri: string;
|
|
30
|
+
expires_in: number;
|
|
31
|
+
interval: number;
|
|
32
|
+
}
|
|
33
|
+
export interface AccessTokenResponse {
|
|
34
|
+
access_token: string;
|
|
35
|
+
token_type: string;
|
|
36
|
+
scope: string;
|
|
37
|
+
}
|
|
38
|
+
export interface OAuthError {
|
|
39
|
+
error: string;
|
|
40
|
+
error_description?: string;
|
|
41
|
+
}
|
|
42
|
+
export type PollResult = {
|
|
43
|
+
status: "success";
|
|
44
|
+
token: AccessTokenResponse;
|
|
45
|
+
} | {
|
|
46
|
+
status: "pending";
|
|
47
|
+
} | {
|
|
48
|
+
status: "slow_down";
|
|
49
|
+
newInterval: number;
|
|
50
|
+
} | {
|
|
51
|
+
status: "expired";
|
|
52
|
+
} | {
|
|
53
|
+
status: "access_denied";
|
|
54
|
+
} | {
|
|
55
|
+
status: "error";
|
|
56
|
+
message: string;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Request a device code from GitHub
|
|
60
|
+
* This is the first step in the device flow
|
|
61
|
+
*/
|
|
62
|
+
export declare function requestDeviceCode(clientId: string, scope?: string): Promise<DeviceCodeResponse>;
|
|
63
|
+
/**
|
|
64
|
+
* Poll GitHub for the access token
|
|
65
|
+
* Returns the poll result which can be:
|
|
66
|
+
* - success: user authorized, token received
|
|
67
|
+
* - pending: user hasn't authorized yet
|
|
68
|
+
* - slow_down: polling too fast, increase interval
|
|
69
|
+
* - expired: device code expired
|
|
70
|
+
* - access_denied: user denied authorization
|
|
71
|
+
* - error: other error occurred
|
|
72
|
+
*/
|
|
73
|
+
export declare function pollForAccessToken(clientId: string, deviceCode: string): Promise<PollResult>;
|
|
74
|
+
/**
|
|
75
|
+
* Helper to sleep for a given number of milliseconds
|
|
76
|
+
*/
|
|
77
|
+
export declare function sleep(ms: number): Promise<void>;
|
|
78
|
+
/**
|
|
79
|
+
* Poll for access token with automatic retry
|
|
80
|
+
* This handles the full polling loop until success, timeout, or error
|
|
81
|
+
*/
|
|
82
|
+
export declare function pollUntilComplete(clientId: string, deviceCode: string, expiresIn: number, initialInterval: number, onPoll?: (attempt: number) => void): Promise<{
|
|
83
|
+
success: true;
|
|
84
|
+
token: AccessTokenResponse;
|
|
85
|
+
} | {
|
|
86
|
+
success: false;
|
|
87
|
+
reason: "expired" | "denied" | "error";
|
|
88
|
+
message?: string;
|
|
89
|
+
}>;
|
|
90
|
+
/**
|
|
91
|
+
* Open URL in the default browser
|
|
92
|
+
*/
|
|
93
|
+
export declare function openBrowser(url: string): Promise<void>;
|
|
94
|
+
/**
|
|
95
|
+
* Get all GitHub App installations for the authenticated user
|
|
96
|
+
*/
|
|
97
|
+
export declare function getUserInstallations(userToken: string): Promise<Installation[]>;
|
|
98
|
+
/**
|
|
99
|
+
* Get repositories accessible by a specific installation
|
|
100
|
+
*/
|
|
101
|
+
export declare function getInstallationRepositories(userToken: string, installationId: number): Promise<Repository[]>;
|
|
102
|
+
/**
|
|
103
|
+
* Get an installation access token for API operations
|
|
104
|
+
*/
|
|
105
|
+
export declare function getInstallationToken(userToken: string, installationId: number): Promise<InstallationToken>;
|
|
106
|
+
//# sourceMappingURL=oauth.d.ts.map
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub OAuth Device Flow implementation
|
|
3
|
+
* Reference: https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#device-flow
|
|
4
|
+
*/
|
|
5
|
+
const DEVICE_CODE_URL = "https://github.com/login/device/code";
|
|
6
|
+
const ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token";
|
|
7
|
+
/**
|
|
8
|
+
* Request a device code from GitHub
|
|
9
|
+
* This is the first step in the device flow
|
|
10
|
+
*/
|
|
11
|
+
export async function requestDeviceCode(clientId, scope = "repo") {
|
|
12
|
+
const response = await fetch(DEVICE_CODE_URL, {
|
|
13
|
+
method: "POST",
|
|
14
|
+
headers: {
|
|
15
|
+
Accept: "application/json",
|
|
16
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
17
|
+
},
|
|
18
|
+
body: new URLSearchParams({
|
|
19
|
+
client_id: clientId,
|
|
20
|
+
scope,
|
|
21
|
+
}),
|
|
22
|
+
});
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
throw new Error(`Failed to request device code: ${response.status}`);
|
|
25
|
+
}
|
|
26
|
+
const data = (await response.json());
|
|
27
|
+
if (data.error) {
|
|
28
|
+
throw new Error(data.error_description || data.error);
|
|
29
|
+
}
|
|
30
|
+
return data;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Poll GitHub for the access token
|
|
34
|
+
* Returns the poll result which can be:
|
|
35
|
+
* - success: user authorized, token received
|
|
36
|
+
* - pending: user hasn't authorized yet
|
|
37
|
+
* - slow_down: polling too fast, increase interval
|
|
38
|
+
* - expired: device code expired
|
|
39
|
+
* - access_denied: user denied authorization
|
|
40
|
+
* - error: other error occurred
|
|
41
|
+
*/
|
|
42
|
+
export async function pollForAccessToken(clientId, deviceCode) {
|
|
43
|
+
const response = await fetch(ACCESS_TOKEN_URL, {
|
|
44
|
+
method: "POST",
|
|
45
|
+
headers: {
|
|
46
|
+
Accept: "application/json",
|
|
47
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
48
|
+
},
|
|
49
|
+
body: new URLSearchParams({
|
|
50
|
+
client_id: clientId,
|
|
51
|
+
device_code: deviceCode,
|
|
52
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
53
|
+
}),
|
|
54
|
+
});
|
|
55
|
+
if (!response.ok) {
|
|
56
|
+
return {
|
|
57
|
+
status: "error",
|
|
58
|
+
message: `HTTP error: ${response.status}`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const data = (await response.json());
|
|
62
|
+
// Check for error responses
|
|
63
|
+
if (data.error) {
|
|
64
|
+
switch (data.error) {
|
|
65
|
+
case "authorization_pending":
|
|
66
|
+
return { status: "pending" };
|
|
67
|
+
case "slow_down":
|
|
68
|
+
// GitHub requires adding 5 seconds to the interval
|
|
69
|
+
return { status: "slow_down", newInterval: 5 };
|
|
70
|
+
case "expired_token":
|
|
71
|
+
return { status: "expired" };
|
|
72
|
+
case "access_denied":
|
|
73
|
+
return { status: "access_denied" };
|
|
74
|
+
default:
|
|
75
|
+
return {
|
|
76
|
+
status: "error",
|
|
77
|
+
message: data.error_description || data.error,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Success - we got the token
|
|
82
|
+
return {
|
|
83
|
+
status: "success",
|
|
84
|
+
token: data,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Helper to sleep for a given number of milliseconds
|
|
89
|
+
*/
|
|
90
|
+
export function sleep(ms) {
|
|
91
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Poll for access token with automatic retry
|
|
95
|
+
* This handles the full polling loop until success, timeout, or error
|
|
96
|
+
*/
|
|
97
|
+
export async function pollUntilComplete(clientId, deviceCode, expiresIn, initialInterval, onPoll) {
|
|
98
|
+
const startTime = Date.now();
|
|
99
|
+
const expiresAt = startTime + expiresIn * 1000;
|
|
100
|
+
let interval = initialInterval;
|
|
101
|
+
let attempt = 0;
|
|
102
|
+
while (Date.now() < expiresAt) {
|
|
103
|
+
await sleep(interval * 1000);
|
|
104
|
+
attempt++;
|
|
105
|
+
if (onPoll) {
|
|
106
|
+
onPoll(attempt);
|
|
107
|
+
}
|
|
108
|
+
const result = await pollForAccessToken(clientId, deviceCode);
|
|
109
|
+
switch (result.status) {
|
|
110
|
+
case "success":
|
|
111
|
+
return { success: true, token: result.token };
|
|
112
|
+
case "pending":
|
|
113
|
+
// Continue polling
|
|
114
|
+
break;
|
|
115
|
+
case "slow_down":
|
|
116
|
+
interval += result.newInterval;
|
|
117
|
+
break;
|
|
118
|
+
case "expired":
|
|
119
|
+
return { success: false, reason: "expired" };
|
|
120
|
+
case "access_denied":
|
|
121
|
+
return { success: false, reason: "denied" };
|
|
122
|
+
case "error":
|
|
123
|
+
return { success: false, reason: "error", message: result.message };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return { success: false, reason: "expired" };
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Open URL in the default browser
|
|
130
|
+
*/
|
|
131
|
+
export async function openBrowser(url) {
|
|
132
|
+
const { exec } = await import("child_process");
|
|
133
|
+
const { promisify } = await import("util");
|
|
134
|
+
const execAsync = promisify(exec);
|
|
135
|
+
// Determine the command based on the platform
|
|
136
|
+
const platform = process.platform;
|
|
137
|
+
let command;
|
|
138
|
+
if (platform === "darwin") {
|
|
139
|
+
command = `open "${url}"`;
|
|
140
|
+
}
|
|
141
|
+
else if (platform === "win32") {
|
|
142
|
+
command = `start "" "${url}"`;
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
// Linux and others
|
|
146
|
+
command = `xdg-open "${url}"`;
|
|
147
|
+
}
|
|
148
|
+
await execAsync(command);
|
|
149
|
+
}
|
|
150
|
+
// ============================================================================
|
|
151
|
+
// GitHub App Installation Management
|
|
152
|
+
// ============================================================================
|
|
153
|
+
const GITHUB_API_URL = "https://api.github.com";
|
|
154
|
+
/**
|
|
155
|
+
* Get all GitHub App installations for the authenticated user
|
|
156
|
+
*/
|
|
157
|
+
export async function getUserInstallations(userToken) {
|
|
158
|
+
const response = await fetch(`${GITHUB_API_URL}/user/installations`, {
|
|
159
|
+
headers: {
|
|
160
|
+
Accept: "application/vnd.github+json",
|
|
161
|
+
Authorization: `Bearer ${userToken}`,
|
|
162
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
if (!response.ok) {
|
|
166
|
+
const error = await response.text();
|
|
167
|
+
throw new Error(`Failed to get installations: ${response.status} - ${error}`);
|
|
168
|
+
}
|
|
169
|
+
const data = (await response.json());
|
|
170
|
+
return data.installations;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Get repositories accessible by a specific installation
|
|
174
|
+
*/
|
|
175
|
+
export async function getInstallationRepositories(userToken, installationId) {
|
|
176
|
+
const response = await fetch(`${GITHUB_API_URL}/user/installations/${installationId}/repositories`, {
|
|
177
|
+
headers: {
|
|
178
|
+
Accept: "application/vnd.github+json",
|
|
179
|
+
Authorization: `Bearer ${userToken}`,
|
|
180
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
if (!response.ok) {
|
|
184
|
+
const error = await response.text();
|
|
185
|
+
throw new Error(`Failed to get installation repositories: ${response.status} - ${error}`);
|
|
186
|
+
}
|
|
187
|
+
const data = (await response.json());
|
|
188
|
+
return data.repositories;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Get an installation access token for API operations
|
|
192
|
+
*/
|
|
193
|
+
export async function getInstallationToken(userToken, installationId) {
|
|
194
|
+
const response = await fetch(`${GITHUB_API_URL}/user/installations/${installationId}/access_tokens`, {
|
|
195
|
+
method: "POST",
|
|
196
|
+
headers: {
|
|
197
|
+
Accept: "application/vnd.github+json",
|
|
198
|
+
Authorization: `Bearer ${userToken}`,
|
|
199
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
if (!response.ok) {
|
|
203
|
+
const error = await response.text();
|
|
204
|
+
throw new Error(`Failed to get installation token: ${response.status} - ${error}`);
|
|
205
|
+
}
|
|
206
|
+
return (await response.json());
|
|
207
|
+
}
|
|
208
|
+
//# sourceMappingURL=oauth.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform-native paths for configuration, data, and cache storage.
|
|
3
|
+
*
|
|
4
|
+
* Uses env-paths to provide appropriate directories per platform:
|
|
5
|
+
* - macOS: ~/Library/Application Support/skilluse/, ~/Library/Caches/skilluse/
|
|
6
|
+
* - Linux: ~/.config/skilluse/, ~/.local/share/skilluse/, ~/.cache/skilluse/
|
|
7
|
+
* - Windows: %APPDATA%/skilluse/, %LOCALAPPDATA%/skilluse/
|
|
8
|
+
*/
|
|
9
|
+
/** Directory for user configuration (settings, preferences) */
|
|
10
|
+
export declare const configPath: string;
|
|
11
|
+
/** Directory for application data (installed skills, repos) */
|
|
12
|
+
export declare const dataPath: string;
|
|
13
|
+
/** Directory for cached data (temporary files) */
|
|
14
|
+
export declare const cachePath: string;
|
|
15
|
+
/** Directory for log files */
|
|
16
|
+
export declare const logPath: string;
|
|
17
|
+
/** Directory for temporary files */
|
|
18
|
+
export declare const tempPath: string;
|
|
19
|
+
//# sourceMappingURL=paths.d.ts.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform-native paths for configuration, data, and cache storage.
|
|
3
|
+
*
|
|
4
|
+
* Uses env-paths to provide appropriate directories per platform:
|
|
5
|
+
* - macOS: ~/Library/Application Support/skilluse/, ~/Library/Caches/skilluse/
|
|
6
|
+
* - Linux: ~/.config/skilluse/, ~/.local/share/skilluse/, ~/.cache/skilluse/
|
|
7
|
+
* - Windows: %APPDATA%/skilluse/, %LOCALAPPDATA%/skilluse/
|
|
8
|
+
*/
|
|
9
|
+
import envPaths from "env-paths";
|
|
10
|
+
const paths = envPaths("skilluse", { suffix: "" });
|
|
11
|
+
/** Directory for user configuration (settings, preferences) */
|
|
12
|
+
export const configPath = paths.config;
|
|
13
|
+
/** Directory for application data (installed skills, repos) */
|
|
14
|
+
export const dataPath = paths.data;
|
|
15
|
+
/** Directory for cached data (temporary files) */
|
|
16
|
+
export const cachePath = paths.cache;
|
|
17
|
+
/** Directory for log files */
|
|
18
|
+
export const logPath = paths.log;
|
|
19
|
+
/** Directory for temporary files */
|
|
20
|
+
export const tempPath = paths.temp;
|
|
21
|
+
//# sourceMappingURL=paths.js.map
|