clevermation-cli 0.3.2
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/.github/workflows/publish.yml +46 -0
- package/CLAUDE.md +167 -0
- package/README.md +211 -0
- package/bin/cl +2 -0
- package/bin/clever +2 -0
- package/bun.lock +361 -0
- package/package.json +43 -0
- package/scripts/setup-team-member.sh +43 -0
- package/src/commands/auth.ts +302 -0
- package/src/commands/config.ts +174 -0
- package/src/commands/doctor.ts +15 -0
- package/src/commands/explain.ts +113 -0
- package/src/commands/init.ts +429 -0
- package/src/commands/open.ts +104 -0
- package/src/commands/sync.ts +181 -0
- package/src/commands/update.ts +90 -0
- package/src/index.ts +44 -0
- package/src/types/config.ts +90 -0
- package/src/utils/auto-update.ts +169 -0
- package/src/utils/config.ts +85 -0
- package/src/utils/logger.ts +49 -0
- package/src/utils/prerequisites.ts +228 -0
- package/tsconfig.json +29 -0
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { input, select, checkbox, confirm } from '@inquirer/prompts';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import fs from 'fs-extra';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import os from 'os';
|
|
8
|
+
import { execa } from 'execa';
|
|
9
|
+
import { logger } from '../utils/logger.js';
|
|
10
|
+
import { saveProjectConfig, getProjectConfigDir, loadGlobalConfig } from '../utils/config.js';
|
|
11
|
+
import type {
|
|
12
|
+
ProjectConfig,
|
|
13
|
+
ServiceType,
|
|
14
|
+
ProjectType,
|
|
15
|
+
ModelType,
|
|
16
|
+
} from '../types/config.js';
|
|
17
|
+
|
|
18
|
+
interface InitOptions {
|
|
19
|
+
name?: string;
|
|
20
|
+
customer?: string;
|
|
21
|
+
yes?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createInitCommand(): Command {
|
|
25
|
+
return new Command('init')
|
|
26
|
+
.description('Initialisiere ein neues Clevermation Projekt')
|
|
27
|
+
.option('-n, --name <name>', 'Projektname')
|
|
28
|
+
.option('-c, --customer <customer>', 'Kundenname (für Kundenprojekte)')
|
|
29
|
+
.option('-y, --yes', 'Alle Defaults akzeptieren')
|
|
30
|
+
.action(async (options: InitOptions) => {
|
|
31
|
+
await runInit(options);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function runInit(options: InitOptions) {
|
|
36
|
+
logger.title('Clevermation Project Setup');
|
|
37
|
+
|
|
38
|
+
// Prüfen ob bereits ein Projekt existiert
|
|
39
|
+
if (await fs.pathExists(getProjectConfigDir())) {
|
|
40
|
+
logger.warning('Es existiert bereits ein Clevermation Projekt in diesem Verzeichnis.');
|
|
41
|
+
const overwrite = await confirm({
|
|
42
|
+
message: 'Möchtest du die Konfiguration überschreiben?',
|
|
43
|
+
default: false,
|
|
44
|
+
});
|
|
45
|
+
if (!overwrite) {
|
|
46
|
+
logger.info('Setup abgebrochen.');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Step 1: Projekt-Typ
|
|
52
|
+
const projectType = (await select({
|
|
53
|
+
message: 'Projekt-Typ',
|
|
54
|
+
choices: [
|
|
55
|
+
{ name: 'Kundenprojekt (kunde-projekt)', value: 'customer' },
|
|
56
|
+
{ name: 'Internes Projekt (clevermation-name)', value: 'internal' },
|
|
57
|
+
],
|
|
58
|
+
})) as ProjectType;
|
|
59
|
+
|
|
60
|
+
// Step 2: Projektname
|
|
61
|
+
const projectName =
|
|
62
|
+
options.name ||
|
|
63
|
+
(await input({
|
|
64
|
+
message: 'Projektname',
|
|
65
|
+
validate: (v) =>
|
|
66
|
+
/^[a-z0-9-]+$/.test(v) ||
|
|
67
|
+
'Nur Kleinbuchstaben, Zahlen und Bindestriche erlaubt',
|
|
68
|
+
}));
|
|
69
|
+
|
|
70
|
+
// Step 3: Kundenname (falls Kundenprojekt)
|
|
71
|
+
let customerName: string | undefined;
|
|
72
|
+
if (projectType === 'customer') {
|
|
73
|
+
customerName =
|
|
74
|
+
options.customer ||
|
|
75
|
+
(await input({
|
|
76
|
+
message: 'Kundenname',
|
|
77
|
+
validate: (v) =>
|
|
78
|
+
/^[a-z0-9-]+$/.test(v) ||
|
|
79
|
+
'Nur Kleinbuchstaben, Zahlen und Bindestriche erlaubt',
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Step 4: Services auswählen (Checkboxen)
|
|
84
|
+
const services = (await checkbox({
|
|
85
|
+
message: 'Wähle die Services für dein Projekt',
|
|
86
|
+
choices: [
|
|
87
|
+
{ name: 'Supabase (Datenbank, Auth, Storage)', value: 'supabase' },
|
|
88
|
+
{ name: 'N8N (Workflow Automation)', value: 'n8n' },
|
|
89
|
+
{ name: 'ElevenLabs (Voice AI)', value: 'elevenlabs' },
|
|
90
|
+
],
|
|
91
|
+
})) as ServiceType[];
|
|
92
|
+
|
|
93
|
+
// Step 5: Claude Model
|
|
94
|
+
const modelPreference = (await select({
|
|
95
|
+
message: 'Standard Claude Model für dieses Projekt',
|
|
96
|
+
choices: [
|
|
97
|
+
{ name: 'Opus - Höchste Qualität, komplexe Tasks', value: 'opus' },
|
|
98
|
+
{ name: 'Sonnet - Ausgewogen (empfohlen)', value: 'sonnet' },
|
|
99
|
+
{ name: 'Haiku - Schnell und kostengünstig', value: 'haiku' },
|
|
100
|
+
],
|
|
101
|
+
default: 'sonnet',
|
|
102
|
+
})) as ModelType;
|
|
103
|
+
|
|
104
|
+
// Step 6: Optimale Einstellungen
|
|
105
|
+
// Prüfe ob in globaler Config default gesetzt ist
|
|
106
|
+
const globalConfig = await loadGlobalConfig();
|
|
107
|
+
const defaultOptimal = globalConfig?.preferences?.optimaleEinstellungen ?? true;
|
|
108
|
+
|
|
109
|
+
const optimaleEinstellungen = await confirm({
|
|
110
|
+
message: 'Optimale Projekt-Einstellungen vornehmen?',
|
|
111
|
+
default: defaultOptimal,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Step 7: Zusammenfassung
|
|
115
|
+
const repoName =
|
|
116
|
+
projectType === 'customer'
|
|
117
|
+
? `${customerName}-${projectName}`
|
|
118
|
+
: `clevermation-${projectName}`;
|
|
119
|
+
|
|
120
|
+
logger.blank();
|
|
121
|
+
logger.dim('Setup Zusammenfassung:');
|
|
122
|
+
logger.dim(` Repo: Clevermation/${repoName}`);
|
|
123
|
+
logger.dim(
|
|
124
|
+
` Services: GitHub${services.length > 0 ? ', ' + services.join(', ') : ''}`
|
|
125
|
+
);
|
|
126
|
+
logger.dim(` Model: ${modelPreference}`);
|
|
127
|
+
logger.blank();
|
|
128
|
+
|
|
129
|
+
const proceed = await confirm({
|
|
130
|
+
message: 'Projekt mit diesen Einstellungen erstellen?',
|
|
131
|
+
default: true,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
if (!proceed) {
|
|
135
|
+
logger.warning('Setup abgebrochen.');
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Step 7: Projekt erstellen
|
|
140
|
+
const spinner = ora('Erstelle Projektstruktur...').start();
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
// Verzeichnisse erstellen
|
|
144
|
+
await createProjectStructure();
|
|
145
|
+
spinner.text = 'Richte Claude Code Plugins ein...';
|
|
146
|
+
|
|
147
|
+
// Claude Code Konfiguration
|
|
148
|
+
await setupClaudeCode(services, modelPreference);
|
|
149
|
+
|
|
150
|
+
// Optimale Einstellungen (wenn gewählt)
|
|
151
|
+
if (optimaleEinstellungen) {
|
|
152
|
+
spinner.text = 'Wende optimale Einstellungen an...';
|
|
153
|
+
await applyOptimalSettings();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
spinner.text = 'Speichere Projektkonfiguration...';
|
|
157
|
+
|
|
158
|
+
// Projekt Config speichern
|
|
159
|
+
const config: ProjectConfig = {
|
|
160
|
+
version: '1.0.0',
|
|
161
|
+
project: {
|
|
162
|
+
name: projectName,
|
|
163
|
+
type: projectType,
|
|
164
|
+
customer: customerName,
|
|
165
|
+
},
|
|
166
|
+
services: {
|
|
167
|
+
github: {
|
|
168
|
+
enabled: true,
|
|
169
|
+
repo: repoName,
|
|
170
|
+
org: 'Clevermation',
|
|
171
|
+
},
|
|
172
|
+
supabase: services.includes('supabase')
|
|
173
|
+
? { enabled: true }
|
|
174
|
+
: { enabled: false },
|
|
175
|
+
n8n: services.includes('n8n') ? { enabled: true } : { enabled: false },
|
|
176
|
+
elevenlabs: services.includes('elevenlabs')
|
|
177
|
+
? { enabled: true }
|
|
178
|
+
: { enabled: false },
|
|
179
|
+
},
|
|
180
|
+
claudeCode: {
|
|
181
|
+
plugins: getPluginsForServices(services),
|
|
182
|
+
marketplace: 'Clevermation/clevermation-claude-plugins',
|
|
183
|
+
model: modelPreference,
|
|
184
|
+
},
|
|
185
|
+
createdAt: new Date().toISOString(),
|
|
186
|
+
updatedAt: new Date().toISOString(),
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
await saveProjectConfig(config);
|
|
190
|
+
|
|
191
|
+
// Git initialisieren (falls noch nicht vorhanden)
|
|
192
|
+
if (!(await fs.pathExists('.git'))) {
|
|
193
|
+
spinner.text = 'Initialisiere Git Repository...';
|
|
194
|
+
await execa('git', ['init']);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
spinner.succeed('Projekt erfolgreich erstellt!');
|
|
198
|
+
|
|
199
|
+
// Nächste Schritte anzeigen
|
|
200
|
+
showNextSteps(repoName, services);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
spinner.fail('Setup fehlgeschlagen');
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function createProjectStructure(): Promise<void> {
|
|
208
|
+
const dirs = [
|
|
209
|
+
'.clevermation',
|
|
210
|
+
'.claude',
|
|
211
|
+
'.claude/agents',
|
|
212
|
+
'.claude/skills',
|
|
213
|
+
'.claude/commands',
|
|
214
|
+
'src',
|
|
215
|
+
'docs',
|
|
216
|
+
];
|
|
217
|
+
|
|
218
|
+
for (const dir of dirs) {
|
|
219
|
+
await fs.ensureDir(dir);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// .gitignore erstellen/erweitern
|
|
223
|
+
const gitignoreContent = `# Dependencies
|
|
224
|
+
node_modules/
|
|
225
|
+
.bun/
|
|
226
|
+
|
|
227
|
+
# Build
|
|
228
|
+
dist/
|
|
229
|
+
.output/
|
|
230
|
+
|
|
231
|
+
# Environment
|
|
232
|
+
.env
|
|
233
|
+
.env.local
|
|
234
|
+
.env.*.local
|
|
235
|
+
|
|
236
|
+
# Claude Code Local Settings
|
|
237
|
+
.claude/settings.local.json
|
|
238
|
+
|
|
239
|
+
# OS
|
|
240
|
+
.DS_Store
|
|
241
|
+
Thumbs.db
|
|
242
|
+
|
|
243
|
+
# IDE
|
|
244
|
+
.idea/
|
|
245
|
+
.vscode/
|
|
246
|
+
*.swp
|
|
247
|
+
*.swo
|
|
248
|
+
`;
|
|
249
|
+
|
|
250
|
+
const gitignorePath = '.gitignore';
|
|
251
|
+
if (await fs.pathExists(gitignorePath)) {
|
|
252
|
+
const existing = await fs.readFile(gitignorePath, 'utf-8');
|
|
253
|
+
if (!existing.includes('.claude/settings.local.json')) {
|
|
254
|
+
await fs.appendFile(gitignorePath, '\n# Claude Code\n.claude/settings.local.json\n');
|
|
255
|
+
}
|
|
256
|
+
} else {
|
|
257
|
+
await fs.writeFile(gitignorePath, gitignoreContent);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function setupClaudeCode(
|
|
262
|
+
services: ServiceType[],
|
|
263
|
+
model: ModelType
|
|
264
|
+
): Promise<void> {
|
|
265
|
+
// .claude/settings.json erstellen
|
|
266
|
+
const settings = {
|
|
267
|
+
permissions: {
|
|
268
|
+
allow: ['Bash(npm*)', 'Bash(bun*)', 'Bash(git*)'],
|
|
269
|
+
deny: [],
|
|
270
|
+
},
|
|
271
|
+
agents: {
|
|
272
|
+
defaultModel: model,
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
await fs.writeJson('.claude/settings.json', settings, { spaces: 2 });
|
|
277
|
+
|
|
278
|
+
// .claude/settings.local.json Template erstellen
|
|
279
|
+
const envVars: Record<string, string> = {
|
|
280
|
+
FIRECRAWL_API_KEY: '',
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
if (services.includes('supabase')) {
|
|
284
|
+
envVars.SUPABASE_URL = '';
|
|
285
|
+
envVars.SUPABASE_SECRET_KEY = '';
|
|
286
|
+
envVars.SUPABASE_PUBLISHABLE_KEY = '';
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (services.includes('n8n')) {
|
|
290
|
+
envVars.N8N_URL = '';
|
|
291
|
+
envVars.N8N_API_KEY = '';
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (services.includes('elevenlabs')) {
|
|
295
|
+
envVars.ELEVENLABS_API_KEY = '';
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
await fs.writeJson('.claude/settings.local.json', { env: envVars }, { spaces: 2 });
|
|
299
|
+
|
|
300
|
+
// .mcp.json erstellen
|
|
301
|
+
await createMcpConfig(services);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function createMcpConfig(services: ServiceType[]): Promise<void> {
|
|
305
|
+
const mcpConfig: Record<string, unknown> = {
|
|
306
|
+
mcpServers: {
|
|
307
|
+
// Immer Firecrawl für Recherche
|
|
308
|
+
firecrawl: {
|
|
309
|
+
command: 'npx',
|
|
310
|
+
args: ['-y', 'firecrawl-mcp'],
|
|
311
|
+
env: { FIRECRAWL_API_KEY: '${FIRECRAWL_API_KEY}' },
|
|
312
|
+
},
|
|
313
|
+
// Immer Playwright für Frontend-Tests
|
|
314
|
+
playwright: {
|
|
315
|
+
command: 'npx',
|
|
316
|
+
args: ['-y', '@anthropic/mcp-playwright'],
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
const servers = mcpConfig.mcpServers as Record<string, unknown>;
|
|
322
|
+
|
|
323
|
+
if (services.includes('supabase')) {
|
|
324
|
+
servers.supabase = {
|
|
325
|
+
command: 'npx',
|
|
326
|
+
args: ['-y', '@supabase/mcp-server-supabase'],
|
|
327
|
+
env: {
|
|
328
|
+
SUPABASE_URL: '${SUPABASE_URL}',
|
|
329
|
+
SUPABASE_SECRET_KEY: '${SUPABASE_SECRET_KEY}',
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (services.includes('n8n')) {
|
|
335
|
+
servers['n8n-mcp'] = {
|
|
336
|
+
command: 'npx',
|
|
337
|
+
args: ['n8n-mcp'],
|
|
338
|
+
env: {
|
|
339
|
+
MCP_MODE: 'stdio',
|
|
340
|
+
LOG_LEVEL: 'error',
|
|
341
|
+
DISABLE_CONSOLE_OUTPUT: 'true',
|
|
342
|
+
N8N_API_URL: '${N8N_URL}',
|
|
343
|
+
N8N_API_KEY: '${N8N_API_KEY}',
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
await fs.writeJson('.mcp.json', mcpConfig, { spaces: 2 });
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function getPluginsForServices(services: ServiceType[]): string[] {
|
|
352
|
+
const plugins = ['researcher', 'plan-agent', 'frontend-test'];
|
|
353
|
+
|
|
354
|
+
if (services.includes('supabase')) {
|
|
355
|
+
plugins.push('supabase-agent');
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (services.includes('n8n')) {
|
|
359
|
+
plugins.push('n8n-agent');
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return plugins;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function showNextSteps(repoName: string, services: ServiceType[]): void {
|
|
366
|
+
logger.blank();
|
|
367
|
+
logger.title('Nächste Schritte');
|
|
368
|
+
|
|
369
|
+
logger.info('1. Konfiguriere deine API Keys:');
|
|
370
|
+
logger.dim(' Bearbeite .claude/settings.local.json');
|
|
371
|
+
logger.blank();
|
|
372
|
+
|
|
373
|
+
if (services.length > 0) {
|
|
374
|
+
logger.info('2. Authentifiziere dich bei den Services:');
|
|
375
|
+
if (services.includes('supabase')) {
|
|
376
|
+
logger.dim(' $ supabase login');
|
|
377
|
+
}
|
|
378
|
+
if (services.includes('n8n')) {
|
|
379
|
+
logger.dim(' $ cl auth login n8n');
|
|
380
|
+
}
|
|
381
|
+
logger.blank();
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
logger.info(`${services.length > 0 ? '3' : '2'}. Synchronisiere mit GitHub:`);
|
|
385
|
+
logger.dim(` $ cl sync`);
|
|
386
|
+
logger.dim(` (Erstellt Clevermation/${repoName})`);
|
|
387
|
+
logger.blank();
|
|
388
|
+
|
|
389
|
+
logger.info(`${services.length > 0 ? '4' : '3'}. Starte Claude Code:`);
|
|
390
|
+
logger.dim(' $ claude');
|
|
391
|
+
logger.blank();
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Wendet optimale Projekt-Einstellungen an.
|
|
396
|
+
* Erstellt/aktualisiert globale CLAUDE.md mit Clevermation-Kontext.
|
|
397
|
+
* TODO: Wird in separater Session mit User definiert.
|
|
398
|
+
*/
|
|
399
|
+
async function applyOptimalSettings(): Promise<void> {
|
|
400
|
+
const globalClaudeMd = path.join(os.homedir(), '.claude', 'CLAUDE.md');
|
|
401
|
+
|
|
402
|
+
// Prüfe ob globale CLAUDE.md bereits existiert
|
|
403
|
+
const exists = await fs.pathExists(globalClaudeMd);
|
|
404
|
+
|
|
405
|
+
if (!exists) {
|
|
406
|
+
// Erstelle Basis CLAUDE.md (wird später mit User verfeinert)
|
|
407
|
+
await fs.ensureDir(path.dirname(globalClaudeMd));
|
|
408
|
+
|
|
409
|
+
const claudeMdContent = `# Clevermation Entwickler-Kontext
|
|
410
|
+
|
|
411
|
+
## Unternehmen
|
|
412
|
+
Clevermation - No-Code/Low-Code Automation Agentur
|
|
413
|
+
|
|
414
|
+
## Package Manager Standards
|
|
415
|
+
- TypeScript/JavaScript: \`bun\` (nicht npm/yarn/pnpm)
|
|
416
|
+
- Python: \`uv\` (nicht pip/poetry)
|
|
417
|
+
|
|
418
|
+
## Projekt-Konventionen
|
|
419
|
+
- Deutsche Kommentare und Dokumentation
|
|
420
|
+
- TypeScript mit strict mode
|
|
421
|
+
- Formatierung: Prettier (TS/JS), Ruff (Python)
|
|
422
|
+
|
|
423
|
+
---
|
|
424
|
+
*Erstellt von Clevermation CLI - Weitere Einstellungen folgen*
|
|
425
|
+
`;
|
|
426
|
+
|
|
427
|
+
await fs.writeFile(globalClaudeMd, claudeMdContent);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { select } from '@inquirer/prompts';
|
|
3
|
+
import { execa } from 'execa';
|
|
4
|
+
import { logger } from '../utils/logger.js';
|
|
5
|
+
import { loadGlobalConfig, saveGlobalConfig } from '../utils/config.js';
|
|
6
|
+
|
|
7
|
+
type IDE = 'code' | 'cursor' | 'webstorm' | 'zed';
|
|
8
|
+
|
|
9
|
+
const IDE_COMMANDS: Record<IDE, { command: string; name: string }> = {
|
|
10
|
+
code: { command: 'code', name: 'VS Code' },
|
|
11
|
+
cursor: { command: 'cursor', name: 'Cursor' },
|
|
12
|
+
webstorm: { command: 'webstorm', name: 'WebStorm' },
|
|
13
|
+
zed: { command: 'zed', name: 'Zed' },
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function createOpenCommand(): Command {
|
|
17
|
+
const cmd = new Command('open').description('Öffne Projekt in IDE');
|
|
18
|
+
|
|
19
|
+
cmd
|
|
20
|
+
.argument('[path]', 'Pfad zum Öffnen', '.')
|
|
21
|
+
.option('-i, --ide <ide>', 'IDE (code, cursor, webstorm, zed)')
|
|
22
|
+
.option('--set-default <ide>', 'Setze Standard-IDE')
|
|
23
|
+
.action(async (path: string, options: { ide?: string; setDefault?: string }) => {
|
|
24
|
+
if (options.setDefault) {
|
|
25
|
+
await setDefaultIDE(options.setDefault as IDE);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
await openInIDE(path, options.ide as IDE | undefined);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return cmd;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function openInIDE(path: string, ide?: IDE): Promise<void> {
|
|
36
|
+
// IDE ermitteln (Argument > Config > Default)
|
|
37
|
+
let selectedIDE = ide;
|
|
38
|
+
|
|
39
|
+
if (!selectedIDE) {
|
|
40
|
+
const config = await loadGlobalConfig();
|
|
41
|
+
selectedIDE = config?.preferences?.defaultIDE as IDE | undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Wenn immer noch keine IDE, VS Code als Default
|
|
45
|
+
if (!selectedIDE) {
|
|
46
|
+
selectedIDE = 'code';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const ideConfig = IDE_COMMANDS[selectedIDE];
|
|
50
|
+
|
|
51
|
+
if (!ideConfig) {
|
|
52
|
+
logger.error(`Unbekannte IDE: ${selectedIDE}`);
|
|
53
|
+
logger.dim(' Verfügbar: code, cursor, webstorm, zed');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Prüfe ob IDE installiert ist
|
|
58
|
+
try {
|
|
59
|
+
await execa('which', [ideConfig.command]);
|
|
60
|
+
} catch {
|
|
61
|
+
logger.error(`${ideConfig.name} nicht gefunden`);
|
|
62
|
+
logger.dim(` Stelle sicher, dass "${ideConfig.command}" im PATH ist.`);
|
|
63
|
+
|
|
64
|
+
// Frage nach Alternative
|
|
65
|
+
const alternative = (await select({
|
|
66
|
+
message: 'Alternative IDE wählen?',
|
|
67
|
+
choices: Object.entries(IDE_COMMANDS).map(([key, value]) => ({
|
|
68
|
+
name: value.name,
|
|
69
|
+
value: key,
|
|
70
|
+
})),
|
|
71
|
+
})) as IDE;
|
|
72
|
+
|
|
73
|
+
selectedIDE = alternative;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Öffne IDE
|
|
77
|
+
try {
|
|
78
|
+
const finalConfig = IDE_COMMANDS[selectedIDE];
|
|
79
|
+
await execa(finalConfig.command, [path], { stdio: 'ignore', detached: true });
|
|
80
|
+
logger.success(`Geöffnet in ${finalConfig.name}`);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
logger.error('Konnte IDE nicht öffnen');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function setDefaultIDE(ide: IDE): Promise<void> {
|
|
87
|
+
if (!IDE_COMMANDS[ide]) {
|
|
88
|
+
logger.error(`Unbekannte IDE: ${ide}`);
|
|
89
|
+
logger.dim(' Verfügbar: code, cursor, webstorm, zed');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const config = (await loadGlobalConfig()) || {
|
|
94
|
+
version: '1.0.0',
|
|
95
|
+
auth: {},
|
|
96
|
+
preferences: {},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
config.preferences = config.preferences || {};
|
|
100
|
+
config.preferences.defaultIDE = ide;
|
|
101
|
+
|
|
102
|
+
await saveGlobalConfig(config);
|
|
103
|
+
logger.success(`Standard-IDE: ${IDE_COMMANDS[ide].name}`);
|
|
104
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { confirm, input } from '@inquirer/prompts';
|
|
3
|
+
import { execa } from 'execa';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
import { logger } from '../utils/logger.js';
|
|
7
|
+
import { loadProjectConfig } from '../utils/config.js';
|
|
8
|
+
|
|
9
|
+
interface SyncOptions {
|
|
10
|
+
force?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function createSyncCommand(): Command {
|
|
14
|
+
return new Command('sync')
|
|
15
|
+
.description('Synchronisiere Projekt mit GitHub')
|
|
16
|
+
.option('-f, --force', 'Force push (mit Vorsicht verwenden)')
|
|
17
|
+
.action(async (options: SyncOptions) => {
|
|
18
|
+
await runSync(options);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function runSync(options: SyncOptions) {
|
|
23
|
+
logger.title('GitHub Synchronisation');
|
|
24
|
+
|
|
25
|
+
// Step 1: Prüfe gh CLI Installation
|
|
26
|
+
const ghInstalled = await checkGhCli();
|
|
27
|
+
if (!ghInstalled) {
|
|
28
|
+
logger.error('GitHub CLI (gh) ist nicht installiert.');
|
|
29
|
+
logger.info('Installiere mit: brew install gh');
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Step 2: Prüfe gh Authentifizierung
|
|
34
|
+
const ghAuthenticated = await checkGhAuth();
|
|
35
|
+
if (!ghAuthenticated) {
|
|
36
|
+
logger.warning('GitHub CLI ist nicht authentifiziert.');
|
|
37
|
+
const authenticate = await confirm({
|
|
38
|
+
message: 'Möchtest du dich jetzt authentifizieren?',
|
|
39
|
+
default: true,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (authenticate) {
|
|
43
|
+
logger.blank();
|
|
44
|
+
await execa('gh', ['auth', 'login'], { stdio: 'inherit' });
|
|
45
|
+
logger.blank();
|
|
46
|
+
|
|
47
|
+
// Erneut prüfen
|
|
48
|
+
if (!(await checkGhAuth())) {
|
|
49
|
+
logger.error('Authentifizierung fehlgeschlagen.');
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Step 3: Lade Projekt Config
|
|
58
|
+
const config = await loadProjectConfig();
|
|
59
|
+
if (!config) {
|
|
60
|
+
logger.error('Kein Clevermation Projekt gefunden.');
|
|
61
|
+
logger.info('Führe zuerst "cl init" aus.');
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const repoName = config.services.github.repo;
|
|
66
|
+
const org = config.services.github.org;
|
|
67
|
+
const fullRepoName = `${org}/${repoName}`;
|
|
68
|
+
|
|
69
|
+
// Step 4: Prüfe ob Remote existiert
|
|
70
|
+
const hasRemote = await checkRemoteExists();
|
|
71
|
+
|
|
72
|
+
if (!hasRemote) {
|
|
73
|
+
// Prüfe ob Repo auf GitHub existiert
|
|
74
|
+
const repoExists = await checkRepoExists(org, repoName);
|
|
75
|
+
|
|
76
|
+
if (!repoExists) {
|
|
77
|
+
// Repo erstellen
|
|
78
|
+
const createRepo = await confirm({
|
|
79
|
+
message: `Privates Repository ${fullRepoName} erstellen?`,
|
|
80
|
+
default: true,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
if (createRepo) {
|
|
84
|
+
const spinner = ora('Erstelle Repository...').start();
|
|
85
|
+
try {
|
|
86
|
+
await execa('gh', [
|
|
87
|
+
'repo',
|
|
88
|
+
'create',
|
|
89
|
+
fullRepoName,
|
|
90
|
+
'--private',
|
|
91
|
+
'--source',
|
|
92
|
+
'.',
|
|
93
|
+
'--remote',
|
|
94
|
+
'origin',
|
|
95
|
+
'--push',
|
|
96
|
+
]);
|
|
97
|
+
spinner.succeed(`Repository erstellt: ${fullRepoName}`);
|
|
98
|
+
logger.success('Projekt synchronisiert!');
|
|
99
|
+
return;
|
|
100
|
+
} catch (error) {
|
|
101
|
+
spinner.fail('Repository-Erstellung fehlgeschlagen');
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
// Repo existiert, aber kein Remote - Remote hinzufügen
|
|
109
|
+
const spinner = ora('Füge Remote hinzu...').start();
|
|
110
|
+
await execa('git', ['remote', 'add', 'origin', `git@github.com:${fullRepoName}.git`]);
|
|
111
|
+
spinner.succeed('Remote hinzugefügt');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Step 5: Push zu Remote
|
|
116
|
+
const spinner = ora('Pushe zu Remote...').start();
|
|
117
|
+
try {
|
|
118
|
+
const pushArgs = ['push', '-u', 'origin', 'main'];
|
|
119
|
+
if (options.force) {
|
|
120
|
+
pushArgs.push('--force');
|
|
121
|
+
}
|
|
122
|
+
await execa('git', pushArgs);
|
|
123
|
+
spinner.succeed('Mit GitHub synchronisiert!');
|
|
124
|
+
} catch (error) {
|
|
125
|
+
spinner.fail('Push fehlgeschlagen');
|
|
126
|
+
|
|
127
|
+
// Prüfen ob es an fehlendem Commit liegt
|
|
128
|
+
const hasCommits = await checkHasCommits();
|
|
129
|
+
if (!hasCommits) {
|
|
130
|
+
logger.info('Tipp: Du musst erst einen Commit erstellen.');
|
|
131
|
+
logger.dim(' git add . && git commit -m "Initial commit"');
|
|
132
|
+
} else {
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function checkGhCli(): Promise<boolean> {
|
|
139
|
+
try {
|
|
140
|
+
await execa('gh', ['--version']);
|
|
141
|
+
return true;
|
|
142
|
+
} catch {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function checkGhAuth(): Promise<boolean> {
|
|
148
|
+
try {
|
|
149
|
+
await execa('gh', ['auth', 'status']);
|
|
150
|
+
return true;
|
|
151
|
+
} catch {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function checkRemoteExists(): Promise<boolean> {
|
|
157
|
+
try {
|
|
158
|
+
const result = await execa('git', ['remote', 'get-url', 'origin']);
|
|
159
|
+
return result.stdout.length > 0;
|
|
160
|
+
} catch {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function checkRepoExists(org: string, repo: string): Promise<boolean> {
|
|
166
|
+
try {
|
|
167
|
+
await execa('gh', ['repo', 'view', `${org}/${repo}`]);
|
|
168
|
+
return true;
|
|
169
|
+
} catch {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function checkHasCommits(): Promise<boolean> {
|
|
175
|
+
try {
|
|
176
|
+
await execa('git', ['rev-parse', 'HEAD']);
|
|
177
|
+
return true;
|
|
178
|
+
} catch {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
}
|