slashdev 0.1.0 → 1.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 (73) hide show
  1. package/.gitmodules +3 -0
  2. package/CLAUDE.md +88 -0
  3. package/GETTING-STARTED.md +68 -0
  4. package/README.md +158 -21
  5. package/bin/check-setup.js +27 -0
  6. package/claude-skills/agentswarm/SKILL.md +479 -0
  7. package/claude-skills/bug-diagnosis/SKILL.md +34 -0
  8. package/claude-skills/code-review/SKILL.md +26 -0
  9. package/claude-skills/frontend-design/LICENSE.txt +177 -0
  10. package/claude-skills/frontend-design/SKILL.md +42 -0
  11. package/claude-skills/pr-description/SKILL.md +35 -0
  12. package/claude-skills/scope-estimate/SKILL.md +37 -0
  13. package/claude-skills/secure-deploy/SKILL.md +219 -0
  14. package/hooks/post-response.sh +242 -0
  15. package/hooks/pre-install-guard.sh +88 -0
  16. package/package.json +11 -3
  17. package/skills/front-end-design/prompts/system.md +37 -0
  18. package/skills/front-end-testing/prompts/system.md +66 -0
  19. package/skills/github-manager/prompts/system.md +79 -0
  20. package/skills/product-expert/prompts/system.md +52 -0
  21. package/skills/server-admin/prompts/system.md +39 -0
  22. package/src/auth/index.js +115 -0
  23. package/src/cli.js +188 -18
  24. package/src/commands/setup-internals.js +221 -0
  25. package/src/commands/setup.js +110 -0
  26. package/src/commands/update.js +64 -0
  27. package/src/connections/index.js +449 -0
  28. package/src/connections/providers/github.js +71 -0
  29. package/src/connections/providers/servers.js +175 -0
  30. package/src/connections/registry.js +21 -0
  31. package/src/core/claude.js +78 -0
  32. package/src/core/codebase.js +119 -0
  33. package/src/core/config.js +110 -0
  34. package/src/index.js +8 -1
  35. package/src/info.js +54 -21
  36. package/src/skills/index.js +252 -0
  37. package/src/utils/ssh-keys.js +67 -0
  38. package/vendor/gstack/.env.example +5 -0
  39. package/vendor/gstack/autoplan/SKILL.md +1116 -0
  40. package/vendor/gstack/browse/SKILL.md +538 -0
  41. package/vendor/gstack/canary/SKILL.md +587 -0
  42. package/vendor/gstack/careful/SKILL.md +59 -0
  43. package/vendor/gstack/codex/SKILL.md +862 -0
  44. package/vendor/gstack/connect-chrome/SKILL.md +549 -0
  45. package/vendor/gstack/cso/ACKNOWLEDGEMENTS.md +14 -0
  46. package/vendor/gstack/cso/SKILL.md +929 -0
  47. package/vendor/gstack/design-consultation/SKILL.md +962 -0
  48. package/vendor/gstack/design-review/SKILL.md +1314 -0
  49. package/vendor/gstack/design-shotgun/SKILL.md +730 -0
  50. package/vendor/gstack/document-release/SKILL.md +718 -0
  51. package/vendor/gstack/freeze/SKILL.md +82 -0
  52. package/vendor/gstack/gstack-upgrade/SKILL.md +232 -0
  53. package/vendor/gstack/guard/SKILL.md +82 -0
  54. package/vendor/gstack/investigate/SKILL.md +504 -0
  55. package/vendor/gstack/land-and-deploy/SKILL.md +1367 -0
  56. package/vendor/gstack/office-hours/SKILL.md +1317 -0
  57. package/vendor/gstack/plan-ceo-review/SKILL.md +1537 -0
  58. package/vendor/gstack/plan-design-review/SKILL.md +1227 -0
  59. package/vendor/gstack/plan-eng-review/SKILL.md +1120 -0
  60. package/vendor/gstack/qa/SKILL.md +1136 -0
  61. package/vendor/gstack/qa/references/issue-taxonomy.md +85 -0
  62. package/vendor/gstack/qa/templates/qa-report-template.md +126 -0
  63. package/vendor/gstack/qa-only/SKILL.md +726 -0
  64. package/vendor/gstack/retro/SKILL.md +1197 -0
  65. package/vendor/gstack/review/SKILL.md +1138 -0
  66. package/vendor/gstack/review/TODOS-format.md +62 -0
  67. package/vendor/gstack/review/checklist.md +220 -0
  68. package/vendor/gstack/review/design-checklist.md +132 -0
  69. package/vendor/gstack/review/greptile-triage.md +220 -0
  70. package/vendor/gstack/setup-browser-cookies/SKILL.md +348 -0
  71. package/vendor/gstack/setup-deploy/SKILL.md +528 -0
  72. package/vendor/gstack/ship/SKILL.md +1931 -0
  73. package/vendor/gstack/unfreeze/SKILL.md +40 -0
@@ -0,0 +1,64 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { readFileSync } from 'fs';
4
+ import { join, dirname } from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ import { getConfig, setConfig } from '../core/config.js';
7
+ import { setup } from './setup.js';
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = dirname(__filename);
11
+ const PACKAGE_ROOT = join(__dirname, '..', '..');
12
+
13
+ const BRAND = chalk.hex('#215ff6');
14
+
15
+ export async function update() {
16
+ console.log(BRAND('\n Slashdev Claude Tools Update\n'));
17
+
18
+ const engineer = getConfig('engineer');
19
+ if (!engineer) {
20
+ console.log(chalk.yellow(' Not configured yet. Running setup instead...\n'));
21
+ await setup();
22
+ return;
23
+ }
24
+
25
+ const spinner = ora('Updating hooks and skills...').start();
26
+
27
+ try {
28
+ // Re-import setup internals by calling setup's install functions
29
+ // We import dynamically to avoid circular deps
30
+ const { installHooks, installSkills, installGstack, writeGlobalClaudeMd } = await import('./setup-internals.js');
31
+
32
+ const hookResult = installHooks();
33
+
34
+ spinner.text = 'Updating Claude Code skills...';
35
+ const skillResult = installSkills();
36
+
37
+ spinner.text = 'Updating gstack skills...';
38
+ const gstackResult = installGstack();
39
+
40
+ spinner.text = 'Updating global Claude rules...';
41
+ const rulesResult = writeGlobalClaudeMd();
42
+
43
+ // Update version in config
44
+ const pkg = JSON.parse(readFileSync(join(PACKAGE_ROOT, 'package.json'), 'utf-8'));
45
+ setConfig('packageVersion', pkg.version);
46
+
47
+ spinner.succeed(`Updated to v${pkg.version}`);
48
+
49
+ console.log('');
50
+ console.log(` ${chalk.green('✓')} Hooks: ${hookResult.installed} updated`);
51
+ console.log(` ${chalk.green('✓')} Skills: ${skillResult.installed} updated`);
52
+ console.log(` ${chalk.green('✓')} Gstack: ${gstackResult.installed} skills updated`);
53
+ if (gstackResult.warning) {
54
+ console.log(` ${chalk.yellow('!')} ${gstackResult.warning}`);
55
+ }
56
+ console.log(` ${chalk.green('✓')} Rules: ${rulesResult.path}`);
57
+ console.log(` ${chalk.green('✓')} Config: version set to ${pkg.version}`);
58
+ console.log('');
59
+ } catch (err) {
60
+ spinner.fail('Update failed');
61
+ console.error(chalk.red(`\n Error: ${err.message}\n`));
62
+ process.exit(1);
63
+ }
64
+ }
@@ -0,0 +1,449 @@
1
+ import ora from 'ora';
2
+ import chalk from 'chalk';
3
+ import inquirer from 'inquirer';
4
+ import { credentials } from '../core/config.js';
5
+ import { getProvider, getAllProviders, getProviderIds } from './registry.js';
6
+
7
+ // --- Storage helpers ---
8
+
9
+ function getStoredConnections() {
10
+ return credentials.get('connections') || {};
11
+ }
12
+
13
+ function getStoredConnection(providerId) {
14
+ const connections = getStoredConnections();
15
+ return connections[providerId] || null;
16
+ }
17
+
18
+ function storeConnection(providerId, data) {
19
+ const provider = getProvider(providerId);
20
+ const connections = getStoredConnections();
21
+
22
+ if (provider?.multiple) {
23
+ const existing = Array.isArray(connections[providerId]) ? connections[providerId] : [];
24
+ existing.push({
25
+ ...data,
26
+ connectedAt: new Date().toISOString(),
27
+ });
28
+ connections[providerId] = existing;
29
+ } else {
30
+ connections[providerId] = {
31
+ ...data,
32
+ connectedAt: new Date().toISOString(),
33
+ };
34
+ }
35
+
36
+ credentials.set('connections', connections);
37
+ }
38
+
39
+ function removeConnection(providerId, entryIndex = null) {
40
+ const connections = getStoredConnections();
41
+ const provider = getProvider(providerId);
42
+
43
+ if (provider?.multiple && entryIndex !== null) {
44
+ const entries = connections[providerId] || [];
45
+ entries.splice(entryIndex, 1);
46
+ if (entries.length === 0) {
47
+ delete connections[providerId];
48
+ } else {
49
+ connections[providerId] = entries;
50
+ }
51
+ } else {
52
+ delete connections[providerId];
53
+ }
54
+
55
+ credentials.set('connections', connections);
56
+ }
57
+
58
+ // --- Public API ---
59
+
60
+ export function isConnected(providerId) {
61
+ const provider = getProvider(providerId);
62
+ const stored = getStoredConnection(providerId);
63
+
64
+ if (provider?.multiple) {
65
+ return Array.isArray(stored) && stored.length > 0;
66
+ }
67
+
68
+ return !!stored;
69
+ }
70
+
71
+ export function getConnection(providerId) {
72
+ return getStoredConnection(providerId);
73
+ }
74
+
75
+ export function getConnectionClient(providerId) {
76
+ const provider = getProvider(providerId);
77
+ if (!provider) {
78
+ throw new Error(`Unknown connection: ${providerId}`);
79
+ }
80
+
81
+ const stored = getStoredConnection(providerId);
82
+ if (!stored) {
83
+ throw new Error(
84
+ `Not connected to ${provider.name}. Run \`slashdev connect ${providerId}\` first.`
85
+ );
86
+ }
87
+
88
+ return provider.createClient(stored);
89
+ }
90
+
91
+ export async function connect(providerId) {
92
+ const provider = getProvider(providerId);
93
+
94
+ if (!provider) {
95
+ if (!providerId) {
96
+ return await connectInteractive();
97
+ }
98
+ console.log();
99
+ console.log(chalk.red(` Unknown connection: ${providerId}`));
100
+ console.log(
101
+ chalk.dim(` Available: ${getProviderIds().join(', ')}`)
102
+ );
103
+ console.log();
104
+ return;
105
+ }
106
+
107
+ // Multi-entry providers (e.g. servers)
108
+ if (provider.multiple) {
109
+ const existing = getStoredConnection(providerId);
110
+ if (Array.isArray(existing) && existing.length > 0) {
111
+ console.log();
112
+ console.log(
113
+ chalk.hex('#4d7fff')(
114
+ ` ${existing.length} server${existing.length > 1 ? 's' : ''} currently connected:`
115
+ )
116
+ );
117
+ for (const entry of existing) {
118
+ console.log(
119
+ chalk.dim(` - ${entry.name} (${entry.username}@${entry.host}:${entry.port})`)
120
+ );
121
+ }
122
+ }
123
+
124
+ console.log();
125
+ console.log(chalk.hex('#215ff6').bold(` Add ${provider.name} Connection`));
126
+ console.log(chalk.hex('#4d7fff')(' ─────────────────────────────────────────'));
127
+
128
+ const creds = await provider.promptForCredentials();
129
+
130
+ // Check for duplicate alias
131
+ const entries = Array.isArray(existing) ? existing : [];
132
+ const duplicate = entries.find(
133
+ (e) => e.name.toLowerCase() === creds.name.toLowerCase()
134
+ );
135
+ if (duplicate) {
136
+ console.log();
137
+ const { overwrite } = await inquirer.prompt([
138
+ {
139
+ type: 'confirm',
140
+ name: 'overwrite',
141
+ message: chalk.hex('#7a9fff')(
142
+ `A server named "${creds.name}" already exists. Replace it?`
143
+ ),
144
+ default: false,
145
+ },
146
+ ]);
147
+
148
+ if (!overwrite) {
149
+ console.log(chalk.dim(' Cancelled.'));
150
+ return;
151
+ }
152
+
153
+ const idx = entries.findIndex(
154
+ (e) => e.name.toLowerCase() === creds.name.toLowerCase()
155
+ );
156
+ removeConnection(providerId, idx);
157
+ }
158
+
159
+ const spinner = ora({
160
+ text: chalk.hex('#4d7fff')(`Adding ${provider.name.toLowerCase()} connection...`),
161
+ color: 'blue',
162
+ }).start();
163
+
164
+ const result = await provider.validate(creds);
165
+
166
+ if (!result.valid) {
167
+ spinner.fail(chalk.red(result.error || 'Validation failed.'));
168
+ return;
169
+ }
170
+
171
+ storeConnection(providerId, creds);
172
+
173
+ spinner.succeed(
174
+ chalk.hex('#4d7fff')(
175
+ `Added server ${chalk.bold(creds.name)} (${creds.username}@${creds.host}:${creds.port})`
176
+ )
177
+ );
178
+ console.log();
179
+ return;
180
+ }
181
+
182
+ // Single-entry providers (e.g. GitHub)
183
+ const existing = getStoredConnection(providerId);
184
+ if (existing) {
185
+ console.log();
186
+ console.log(
187
+ chalk.hex('#4d7fff')(
188
+ ` Already connected to ${provider.name}${
189
+ existing.username ? ` as ${chalk.bold(existing.username)}` : ''
190
+ }.`
191
+ )
192
+ );
193
+
194
+ const { overwrite } = await inquirer.prompt([
195
+ {
196
+ type: 'confirm',
197
+ name: 'overwrite',
198
+ message: chalk.hex('#7a9fff')('Replace existing connection?'),
199
+ default: false,
200
+ },
201
+ ]);
202
+
203
+ if (!overwrite) {
204
+ return;
205
+ }
206
+ }
207
+
208
+ console.log();
209
+ console.log(chalk.hex('#215ff6').bold(` Connect ${provider.name}`));
210
+ console.log(chalk.hex('#4d7fff')(' ─────────────────────────────────────────'));
211
+
212
+ const creds = await provider.promptForCredentials();
213
+
214
+ const spinner = ora({
215
+ text: chalk.hex('#4d7fff')(`Validating ${provider.name} token...`),
216
+ color: 'blue',
217
+ }).start();
218
+
219
+ const result = await provider.validate(creds);
220
+
221
+ if (!result.valid) {
222
+ spinner.fail(chalk.red(result.error || 'Validation failed.'));
223
+ return;
224
+ }
225
+
226
+ const dataToStore = { ...creds };
227
+ if (result.username) {
228
+ dataToStore.username = result.username;
229
+ }
230
+ storeConnection(providerId, dataToStore);
231
+
232
+ spinner.succeed(
233
+ chalk.hex('#4d7fff')(
234
+ `Connected to ${provider.name}${
235
+ result.username ? ` as ${chalk.bold(result.username)}` : ''
236
+ }!`
237
+ )
238
+ );
239
+ console.log();
240
+ }
241
+
242
+ async function connectInteractive() {
243
+ const providers = getAllProviders();
244
+ const choices = Object.values(providers).map((p) => ({
245
+ name: `${p.name} — ${p.description}`,
246
+ value: p.id,
247
+ }));
248
+
249
+ console.log();
250
+ console.log(chalk.hex('#215ff6').bold(' Connect a Service'));
251
+ console.log(chalk.hex('#4d7fff')(' ─────────────────────────────────────────'));
252
+ console.log();
253
+
254
+ const { selected } = await inquirer.prompt([
255
+ {
256
+ type: 'list',
257
+ name: 'selected',
258
+ message: chalk.hex('#7a9fff')('Which service do you want to connect?'),
259
+ choices,
260
+ },
261
+ ]);
262
+
263
+ await connect(selected);
264
+ }
265
+
266
+ export async function disconnect(providerId) {
267
+ const provider = getProvider(providerId);
268
+
269
+ if (!provider) {
270
+ if (!providerId) {
271
+ return await disconnectInteractive();
272
+ }
273
+ console.log();
274
+ console.log(chalk.red(` Unknown connection: ${providerId}`));
275
+ console.log();
276
+ return;
277
+ }
278
+
279
+ if (!isConnected(providerId)) {
280
+ console.log();
281
+ console.log(chalk.hex('#7a9fff')(` Not connected to ${provider.name}.`));
282
+ console.log();
283
+ return;
284
+ }
285
+
286
+ // Multi-entry providers
287
+ if (provider.multiple) {
288
+ const entries = getStoredConnection(providerId);
289
+
290
+ if (entries.length === 1) {
291
+ removeConnection(providerId, 0);
292
+ console.log();
293
+ console.log(
294
+ chalk.hex('#4d7fff')(
295
+ ` Removed server ${chalk.bold(entries[0].name)}.`
296
+ )
297
+ );
298
+ console.log();
299
+ return;
300
+ }
301
+
302
+ const choices = entries.map((entry, idx) => ({
303
+ name: `${entry.name} (${entry.username}@${entry.host}:${entry.port})`,
304
+ value: idx,
305
+ }));
306
+ choices.push({ name: chalk.red('Remove all servers'), value: '__all__' });
307
+
308
+ const { selected } = await inquirer.prompt([
309
+ {
310
+ type: 'list',
311
+ name: 'selected',
312
+ message: chalk.hex('#7a9fff')('Which server do you want to remove?'),
313
+ choices,
314
+ },
315
+ ]);
316
+
317
+ if (selected === '__all__') {
318
+ removeConnection(providerId);
319
+ console.log();
320
+ console.log(chalk.hex('#4d7fff')(' Removed all server connections.'));
321
+ console.log();
322
+ } else {
323
+ const entry = entries[selected];
324
+ removeConnection(providerId, selected);
325
+ console.log();
326
+ console.log(
327
+ chalk.hex('#4d7fff')(` Removed server ${chalk.bold(entry.name)}.`)
328
+ );
329
+ console.log();
330
+ }
331
+ return;
332
+ }
333
+
334
+ // Single-entry providers
335
+ const stored = getStoredConnection(providerId);
336
+ removeConnection(providerId);
337
+
338
+ console.log();
339
+ console.log(
340
+ chalk.hex('#4d7fff')(
341
+ ` Disconnected from ${provider.name}${
342
+ stored.username ? ` (${stored.username})` : ''
343
+ }.`
344
+ )
345
+ );
346
+ console.log();
347
+ }
348
+
349
+ async function disconnectInteractive() {
350
+ const connections = getStoredConnections();
351
+ const connectedIds = Object.keys(connections);
352
+
353
+ if (connectedIds.length === 0) {
354
+ console.log();
355
+ console.log(chalk.hex('#7a9fff')(' No active connections.'));
356
+ console.log(
357
+ chalk.dim(` Run ${chalk.hex('#215ff6')('slashdev connect')} to add one.`)
358
+ );
359
+ console.log();
360
+ return;
361
+ }
362
+
363
+ const choices = [];
364
+
365
+ for (const id of connectedIds) {
366
+ const provider = getProvider(id);
367
+
368
+ if (provider?.multiple && Array.isArray(connections[id])) {
369
+ for (let i = 0; i < connections[id].length; i++) {
370
+ const entry = connections[id][i];
371
+ choices.push({
372
+ name: `${provider.name}: ${entry.name} (${entry.username}@${entry.host}:${entry.port})`,
373
+ value: { providerId: id, index: i },
374
+ });
375
+ }
376
+ } else {
377
+ const data = connections[id];
378
+ choices.push({
379
+ name: `${provider?.name || id}${data.username ? ` (${data.username})` : ''}`,
380
+ value: { providerId: id, index: null },
381
+ });
382
+ }
383
+ }
384
+
385
+ const { selected } = await inquirer.prompt([
386
+ {
387
+ type: 'list',
388
+ name: 'selected',
389
+ message: chalk.hex('#7a9fff')('Which connection do you want to remove?'),
390
+ choices,
391
+ },
392
+ ]);
393
+
394
+ if (selected.index !== null) {
395
+ const entries = connections[selected.providerId];
396
+ const entry = entries[selected.index];
397
+ removeConnection(selected.providerId, selected.index);
398
+ console.log();
399
+ console.log(
400
+ chalk.hex('#4d7fff')(` Removed server ${chalk.bold(entry.name)}.`)
401
+ );
402
+ console.log();
403
+ } else {
404
+ await disconnect(selected.providerId);
405
+ }
406
+ }
407
+
408
+ export function listConnections() {
409
+ const providers = getAllProviders();
410
+ const stored = getStoredConnections();
411
+
412
+ console.log();
413
+ console.log(chalk.hex('#215ff6').bold(' Connections'));
414
+ console.log(chalk.hex('#4d7fff')(' ─────────────────────────────────────────'));
415
+ console.log();
416
+
417
+ for (const [id, provider] of Object.entries(providers)) {
418
+ const conn = stored[id];
419
+
420
+ console.log(` ${chalk.hex('#4d7fff')('◈')} ${chalk.hex('#215ff6').bold(provider.name)}`);
421
+ console.log(` ${chalk.dim(provider.description)}`);
422
+
423
+ if (provider.multiple) {
424
+ if (Array.isArray(conn) && conn.length > 0) {
425
+ console.log(
426
+ ` ${chalk.hex('#4d7fff')('●')} ${conn.length} server${conn.length > 1 ? 's' : ''} connected`
427
+ );
428
+ for (const entry of conn) {
429
+ const authInfo = entry.authType === 'key' ? 'key' : 'password';
430
+ console.log(
431
+ chalk.dim(
432
+ ` - ${entry.name}: ${entry.username}@${entry.host}:${entry.port} (${authInfo})`
433
+ )
434
+ );
435
+ }
436
+ } else {
437
+ console.log(` ${chalk.dim('○ No servers configured')}`);
438
+ }
439
+ } else {
440
+ const status = conn
441
+ ? chalk.hex('#4d7fff')('● Connected') +
442
+ (conn.username ? chalk.dim(` as ${conn.username}`) : '')
443
+ : chalk.dim('○ Not connected');
444
+ console.log(` ${status}`);
445
+ }
446
+
447
+ console.log();
448
+ }
449
+ }
@@ -0,0 +1,71 @@
1
+ import inquirer from 'inquirer';
2
+ import chalk from 'chalk';
3
+ import { Octokit } from '@octokit/rest';
4
+
5
+ export const githubProvider = {
6
+ id: 'github',
7
+ name: 'GitHub',
8
+ description: 'Access repositories, issues, and pull requests',
9
+ tokenUrl: 'https://github.com/settings/tokens',
10
+ requiredScopes: ['repo', 'read:user'],
11
+
12
+ async promptForCredentials() {
13
+ console.log();
14
+ console.log(chalk.dim(' Create a personal access token at:'));
15
+ console.log(
16
+ chalk.dim(' ') +
17
+ chalk.hex('#215ff6').underline('https://github.com/settings/tokens')
18
+ );
19
+ console.log(
20
+ chalk.dim(' Required scopes: ') +
21
+ chalk.hex('#7a9fff')('repo, read:user')
22
+ );
23
+ console.log();
24
+
25
+ const answers = await inquirer.prompt([
26
+ {
27
+ type: 'password',
28
+ name: 'token',
29
+ message: chalk.hex('#7a9fff')('Enter your GitHub personal access token:'),
30
+ mask: '*',
31
+ validate: (input) => {
32
+ if (!input || input.trim().length === 0) {
33
+ return 'Token is required';
34
+ }
35
+ const trimmed = input.trim();
36
+ if (
37
+ !trimmed.startsWith('ghp_') &&
38
+ !trimmed.startsWith('github_pat_') &&
39
+ !trimmed.startsWith('gho_') &&
40
+ !trimmed.startsWith('ghu_')
41
+ ) {
42
+ return 'Invalid token format. GitHub tokens start with ghp_, github_pat_, gho_, or ghu_';
43
+ }
44
+ return true;
45
+ },
46
+ },
47
+ ]);
48
+
49
+ return { token: answers.token.trim() };
50
+ },
51
+
52
+ async validate(credentials) {
53
+ try {
54
+ const octokit = new Octokit({ auth: credentials.token });
55
+ const { data } = await octokit.rest.users.getAuthenticated();
56
+ return { valid: true, username: data.login };
57
+ } catch (error) {
58
+ return {
59
+ valid: false,
60
+ error:
61
+ error.status === 401
62
+ ? 'Invalid token. Please check and try again.'
63
+ : `GitHub API error: ${error.message}`,
64
+ };
65
+ }
66
+ },
67
+
68
+ createClient(credentials) {
69
+ return new Octokit({ auth: credentials.token });
70
+ },
71
+ };