slashdev 0.1.0 → 1.0.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/.gitmodules +3 -0
- package/CLAUDE.md +87 -0
- package/README.md +158 -21
- package/bin/check-setup.js +27 -0
- package/claude-skills/agentswarm/SKILL.md +479 -0
- package/claude-skills/bug-diagnosis/SKILL.md +34 -0
- package/claude-skills/code-review/SKILL.md +26 -0
- package/claude-skills/frontend-design/LICENSE.txt +177 -0
- package/claude-skills/frontend-design/SKILL.md +42 -0
- package/claude-skills/pr-description/SKILL.md +35 -0
- package/claude-skills/scope-estimate/SKILL.md +37 -0
- package/hooks/post-response.sh +242 -0
- package/package.json +11 -3
- package/skills/front-end-design/prompts/system.md +37 -0
- package/skills/front-end-testing/prompts/system.md +66 -0
- package/skills/github-manager/prompts/system.md +79 -0
- package/skills/product-expert/prompts/system.md +52 -0
- package/skills/server-admin/prompts/system.md +39 -0
- package/src/auth/index.js +115 -0
- package/src/cli.js +188 -18
- package/src/commands/setup-internals.js +137 -0
- package/src/commands/setup.js +104 -0
- package/src/commands/update.js +60 -0
- package/src/connections/index.js +449 -0
- package/src/connections/providers/github.js +71 -0
- package/src/connections/providers/servers.js +175 -0
- package/src/connections/registry.js +21 -0
- package/src/core/claude.js +78 -0
- package/src/core/codebase.js +119 -0
- package/src/core/config.js +110 -0
- package/src/index.js +8 -1
- package/src/info.js +54 -21
- package/src/skills/index.js +252 -0
- package/src/utils/ssh-keys.js +67 -0
- package/vendor/gstack/.env.example +5 -0
- package/vendor/gstack/autoplan/SKILL.md +1116 -0
- package/vendor/gstack/browse/SKILL.md +538 -0
- package/vendor/gstack/canary/SKILL.md +587 -0
- package/vendor/gstack/careful/SKILL.md +59 -0
- package/vendor/gstack/codex/SKILL.md +862 -0
- package/vendor/gstack/connect-chrome/SKILL.md +549 -0
- package/vendor/gstack/cso/ACKNOWLEDGEMENTS.md +14 -0
- package/vendor/gstack/cso/SKILL.md +929 -0
- package/vendor/gstack/design-consultation/SKILL.md +962 -0
- package/vendor/gstack/design-review/SKILL.md +1314 -0
- package/vendor/gstack/design-shotgun/SKILL.md +730 -0
- package/vendor/gstack/document-release/SKILL.md +718 -0
- package/vendor/gstack/freeze/SKILL.md +82 -0
- package/vendor/gstack/gstack-upgrade/SKILL.md +232 -0
- package/vendor/gstack/guard/SKILL.md +82 -0
- package/vendor/gstack/investigate/SKILL.md +504 -0
- package/vendor/gstack/land-and-deploy/SKILL.md +1367 -0
- package/vendor/gstack/office-hours/SKILL.md +1317 -0
- package/vendor/gstack/plan-ceo-review/SKILL.md +1537 -0
- package/vendor/gstack/plan-design-review/SKILL.md +1227 -0
- package/vendor/gstack/plan-eng-review/SKILL.md +1120 -0
- package/vendor/gstack/qa/SKILL.md +1136 -0
- package/vendor/gstack/qa/references/issue-taxonomy.md +85 -0
- package/vendor/gstack/qa/templates/qa-report-template.md +126 -0
- package/vendor/gstack/qa-only/SKILL.md +726 -0
- package/vendor/gstack/retro/SKILL.md +1197 -0
- package/vendor/gstack/review/SKILL.md +1138 -0
- package/vendor/gstack/review/TODOS-format.md +62 -0
- package/vendor/gstack/review/checklist.md +220 -0
- package/vendor/gstack/review/design-checklist.md +132 -0
- package/vendor/gstack/review/greptile-triage.md +220 -0
- package/vendor/gstack/setup-browser-cookies/SKILL.md +348 -0
- package/vendor/gstack/setup-deploy/SKILL.md +528 -0
- package/vendor/gstack/ship/SKILL.md +1931 -0
- package/vendor/gstack/unfreeze/SKILL.md +40 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { join, dirname } from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { existsSync, readFileSync, mkdirSync } from 'fs';
|
|
7
|
+
import { homedir } from 'os';
|
|
8
|
+
import { getConfig, setConfig, getSlashdevDir } from '../core/config.js';
|
|
9
|
+
import { installHooks, installSkills, installGstack } from './setup-internals.js';
|
|
10
|
+
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = dirname(__filename);
|
|
13
|
+
const PACKAGE_ROOT = join(__dirname, '..', '..');
|
|
14
|
+
|
|
15
|
+
const CLAUDE_DIR = join(homedir(), '.claude');
|
|
16
|
+
|
|
17
|
+
const BRAND = chalk.hex('#215ff6');
|
|
18
|
+
|
|
19
|
+
function validateEmail(email) {
|
|
20
|
+
if (!email || !email.endsWith('@slashdev.io')) {
|
|
21
|
+
return 'Email must be a valid @slashdev.io address';
|
|
22
|
+
}
|
|
23
|
+
if (!/^[^\s@]+@slashdev\.io$/.test(email)) {
|
|
24
|
+
return 'Invalid email format';
|
|
25
|
+
}
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function setup() {
|
|
30
|
+
console.log(BRAND('\n Slashdev Claude Tools Setup\n'));
|
|
31
|
+
|
|
32
|
+
// Check if already configured
|
|
33
|
+
const existingEngineer = getConfig('engineer');
|
|
34
|
+
if (existingEngineer) {
|
|
35
|
+
const { overwrite } = await inquirer.prompt([
|
|
36
|
+
{
|
|
37
|
+
type: 'confirm',
|
|
38
|
+
name: 'overwrite',
|
|
39
|
+
message: `Already configured for ${chalk.cyan(existingEngineer)}. Overwrite?`,
|
|
40
|
+
default: false,
|
|
41
|
+
},
|
|
42
|
+
]);
|
|
43
|
+
if (!overwrite) {
|
|
44
|
+
console.log(chalk.yellow('\n Setup cancelled.\n'));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Prompt for email
|
|
50
|
+
const { email } = await inquirer.prompt([
|
|
51
|
+
{
|
|
52
|
+
type: 'input',
|
|
53
|
+
name: 'email',
|
|
54
|
+
message: 'Enter your Slashdev email (e.g. you@slashdev.io):',
|
|
55
|
+
validate: validateEmail,
|
|
56
|
+
},
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
const spinner = ora('Installing Slashdev Claude tools...').start();
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
// Write engineer config
|
|
63
|
+
setConfig('engineer', email);
|
|
64
|
+
|
|
65
|
+
const pkg = JSON.parse(readFileSync(join(PACKAGE_ROOT, 'package.json'), 'utf-8'));
|
|
66
|
+
setConfig('packageVersion', pkg.version);
|
|
67
|
+
|
|
68
|
+
// Ensure Claude directory exists
|
|
69
|
+
if (!existsSync(CLAUDE_DIR)) {
|
|
70
|
+
mkdirSync(CLAUDE_DIR, { recursive: true });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Install hooks
|
|
74
|
+
spinner.text = 'Installing hooks...';
|
|
75
|
+
const hookResult = installHooks();
|
|
76
|
+
|
|
77
|
+
// Install skills
|
|
78
|
+
spinner.text = 'Installing Claude Code skills...';
|
|
79
|
+
const skillResult = installSkills();
|
|
80
|
+
|
|
81
|
+
// Install gstack skills
|
|
82
|
+
spinner.text = 'Installing gstack skills...';
|
|
83
|
+
const gstackResult = installGstack();
|
|
84
|
+
|
|
85
|
+
spinner.succeed('Slashdev Claude tools installed');
|
|
86
|
+
|
|
87
|
+
console.log('');
|
|
88
|
+
console.log(` ${chalk.green('✓')} Engineer: ${chalk.cyan(email)}`);
|
|
89
|
+
console.log(` ${chalk.green('✓')} Hooks: ${hookResult.installed} installed`);
|
|
90
|
+
console.log(` ${chalk.green('✓')} Skills: ${skillResult.installed} installed`);
|
|
91
|
+
console.log(` ${chalk.green('✓')} Gstack: ${gstackResult.installed} skills installed`);
|
|
92
|
+
if (gstackResult.warning) {
|
|
93
|
+
console.log(` ${chalk.yellow('!')} ${gstackResult.warning}`);
|
|
94
|
+
}
|
|
95
|
+
console.log(` ${chalk.green('✓')} Config: ${join(getSlashdevDir(), 'config.json')}`);
|
|
96
|
+
console.log('');
|
|
97
|
+
console.log(chalk.dim(' Hooks and skills have been installed to ~/.claude/'));
|
|
98
|
+
console.log(chalk.dim(' Prompt logs will be sent to the Slashdev API for quality review.\n'));
|
|
99
|
+
} catch (err) {
|
|
100
|
+
spinner.fail('Setup failed');
|
|
101
|
+
console.error(chalk.red(`\n Error: ${err.message}\n`));
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
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 } = 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
|
+
// Update version in config
|
|
41
|
+
const pkg = JSON.parse(readFileSync(join(PACKAGE_ROOT, 'package.json'), 'utf-8'));
|
|
42
|
+
setConfig('packageVersion', pkg.version);
|
|
43
|
+
|
|
44
|
+
spinner.succeed(`Updated to v${pkg.version}`);
|
|
45
|
+
|
|
46
|
+
console.log('');
|
|
47
|
+
console.log(` ${chalk.green('✓')} Hooks: ${hookResult.installed} updated`);
|
|
48
|
+
console.log(` ${chalk.green('✓')} Skills: ${skillResult.installed} updated`);
|
|
49
|
+
console.log(` ${chalk.green('✓')} Gstack: ${gstackResult.installed} skills updated`);
|
|
50
|
+
if (gstackResult.warning) {
|
|
51
|
+
console.log(` ${chalk.yellow('!')} ${gstackResult.warning}`);
|
|
52
|
+
}
|
|
53
|
+
console.log(` ${chalk.green('✓')} Config: version set to ${pkg.version}`);
|
|
54
|
+
console.log('');
|
|
55
|
+
} catch (err) {
|
|
56
|
+
spinner.fail('Update failed');
|
|
57
|
+
console.error(chalk.red(`\n Error: ${err.message}\n`));
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -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
|
+
};
|