cyrus-ai 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/app.js DELETED
@@ -1,1697 +0,0 @@
1
- #!/usr/bin/env node
2
- import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync, } from "node:fs";
3
- import http from "node:http";
4
- import { homedir } from "node:os";
5
- import { basename, dirname, resolve } from "node:path";
6
- import readline from "node:readline";
7
- import { fileURLToPath } from "node:url";
8
- import { DEFAULT_PROXY_URL, } from "cyrus-core";
9
- import { EdgeWorker, SharedApplicationServer } from "cyrus-edge-worker";
10
- import dotenv from "dotenv";
11
- import open from "open";
12
- // Parse command line arguments
13
- const args = process.argv.slice(2);
14
- const envFileArg = args.find((arg) => arg.startsWith("--env-file="));
15
- const cyrusHomeArg = args.find((arg) => arg.startsWith("--cyrus-home="));
16
- // Constants are imported from cyrus-core
17
- // Determine the Cyrus home directory once at startup
18
- let CYRUS_HOME;
19
- if (cyrusHomeArg) {
20
- const customPath = cyrusHomeArg.split("=")[1];
21
- if (customPath) {
22
- CYRUS_HOME = resolve(customPath);
23
- }
24
- else {
25
- console.error("Error: --cyrus-home flag requires a directory path");
26
- process.exit(1);
27
- }
28
- }
29
- else {
30
- CYRUS_HOME = resolve(homedir(), ".cyrus");
31
- }
32
- // CRITICAL: Source .env file from CYRUS_HOME before any other operations
33
- // This ensures CLOUDFLARE_TOKEN, CYRUS_API_KEY, and other credentials are available
34
- const cyrusEnvPath = resolve(CYRUS_HOME, ".env");
35
- if (existsSync(cyrusEnvPath)) {
36
- dotenv.config({ path: cyrusEnvPath });
37
- }
38
- // Get the directory of the current module for reading package.json
39
- const __filename = fileURLToPath(import.meta.url);
40
- const __dirname = dirname(__filename);
41
- // Read package.json to get the actual version
42
- const packageJsonPath = resolve(__dirname, "..", "package.json");
43
- const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
44
- // Handle --version argument
45
- if (args.includes("--version")) {
46
- console.log(packageJson.version);
47
- process.exit(0);
48
- }
49
- // Handle --help argument
50
- if (args.includes("--help") || args.includes("-h")) {
51
- console.log(`
52
- cyrus - AI-powered Linear issue automation using Claude
53
-
54
- Usage: cyrus [command] [options]
55
-
56
- Commands:
57
- start Start the edge worker (default)
58
- auth <auth-key> Authenticate with Cyrus Pro plan using auth key
59
- check-tokens Check the status of all Linear tokens
60
- refresh-token Refresh a specific Linear token
61
- add-repository Add a new repository configuration
62
- billing Open Stripe billing portal (Pro plan only)
63
- set-customer-id Set your Stripe customer ID
64
-
65
- Options:
66
- --version Show version number
67
- --help, -h Show help
68
- --env-file=<path> Load environment variables from file
69
- --cyrus-home=<dir> Specify custom Cyrus config directory (default: ~/.cyrus)
70
-
71
- Examples:
72
- cyrus Start the edge worker
73
- cyrus auth <your-auth-key> Authenticate and start using Pro plan
74
- cyrus check-tokens Check all Linear token statuses
75
- cyrus refresh-token Interactive token refresh
76
- cyrus add-repository Add a new repository interactively
77
- cyrus --cyrus-home=/tmp/cyrus Use custom config directory
78
- `);
79
- process.exit(0);
80
- }
81
- // Load environment variables only if --env-file is specified
82
- if (envFileArg) {
83
- const envFile = envFileArg.split("=")[1];
84
- if (envFile) {
85
- dotenv.config({ path: envFile });
86
- }
87
- }
88
- /**
89
- * Edge application that uses EdgeWorker from package
90
- */
91
- class EdgeApp {
92
- edgeWorker = null;
93
- isShuttingDown = false;
94
- cyrusHome;
95
- constructor(cyrusHome) {
96
- this.cyrusHome = cyrusHome;
97
- }
98
- /**
99
- * Get the edge configuration file path
100
- */
101
- getEdgeConfigPath() {
102
- return resolve(this.cyrusHome, "config.json");
103
- }
104
- /**
105
- * Get the legacy edge configuration file path (for migration)
106
- */
107
- getLegacyEdgeConfigPath() {
108
- return resolve(process.cwd(), ".edge-config.json");
109
- }
110
- /**
111
- * Migrate configuration from legacy location if needed
112
- */
113
- migrateConfigIfNeeded() {
114
- const newConfigPath = this.getEdgeConfigPath();
115
- const legacyConfigPath = this.getLegacyEdgeConfigPath();
116
- // If new config already exists, no migration needed
117
- if (existsSync(newConfigPath)) {
118
- return;
119
- }
120
- // If legacy config doesn't exist, no migration needed
121
- if (!existsSync(legacyConfigPath)) {
122
- return;
123
- }
124
- try {
125
- // Ensure the ~/.cyrus directory exists
126
- const configDir = dirname(newConfigPath);
127
- if (!existsSync(configDir)) {
128
- mkdirSync(configDir, { recursive: true });
129
- }
130
- // Copy the legacy config to the new location
131
- copyFileSync(legacyConfigPath, newConfigPath);
132
- console.log(`šŸ“¦ Migrated configuration from ${legacyConfigPath} to ${newConfigPath}`);
133
- console.log(`šŸ’” You can safely remove the old ${legacyConfigPath} file if desired`);
134
- }
135
- catch (error) {
136
- console.warn(`āš ļø Failed to migrate config from ${legacyConfigPath}:`, error.message);
137
- console.warn(` Please manually copy your configuration to ${newConfigPath}`);
138
- }
139
- }
140
- /**
141
- * Load edge configuration (credentials and repositories)
142
- * Note: Strips promptTemplatePath from all repositories to ensure built-in template is used
143
- */
144
- loadEdgeConfig() {
145
- // Migrate from legacy location if needed
146
- this.migrateConfigIfNeeded();
147
- const edgeConfigPath = this.getEdgeConfigPath();
148
- let config = { repositories: [] };
149
- if (existsSync(edgeConfigPath)) {
150
- try {
151
- config = JSON.parse(readFileSync(edgeConfigPath, "utf-8"));
152
- }
153
- catch (e) {
154
- console.error("Failed to load edge config:", e.message);
155
- }
156
- }
157
- // Strip promptTemplatePath from all repositories to ensure built-in template is used
158
- if (config.repositories) {
159
- config.repositories = config.repositories.map((repo) => {
160
- const { promptTemplatePath, ...repoWithoutTemplate } = repo;
161
- if (promptTemplatePath) {
162
- console.log(`Ignoring custom prompt template for repository: ${repo.name} (using built-in template)`);
163
- }
164
- return repoWithoutTemplate;
165
- });
166
- }
167
- return config;
168
- }
169
- /**
170
- * Save edge configuration
171
- */
172
- saveEdgeConfig(config) {
173
- const edgeConfigPath = this.getEdgeConfigPath();
174
- const configDir = dirname(edgeConfigPath);
175
- // Ensure the ~/.cyrus directory exists
176
- if (!existsSync(configDir)) {
177
- mkdirSync(configDir, { recursive: true });
178
- }
179
- writeFileSync(edgeConfigPath, JSON.stringify(config, null, 2));
180
- }
181
- /**
182
- * Interactive setup wizard for repository configuration
183
- */
184
- async setupRepositoryWizard(linearCredentials, rl) {
185
- const shouldCloseRl = !rl;
186
- if (!rl) {
187
- rl = readline.createInterface({
188
- input: process.stdin,
189
- output: process.stdout,
190
- });
191
- }
192
- const question = (prompt) => new Promise((resolve) => {
193
- rl.question(prompt, resolve);
194
- });
195
- console.log("\nšŸ“ Repository Setup");
196
- console.log("─".repeat(50));
197
- try {
198
- // Ask for repository details
199
- const repositoryPath = (await question(`Repository path (default: ${process.cwd()}): `)) ||
200
- process.cwd();
201
- const repositoryName = (await question(`Repository name (default: ${basename(repositoryPath)}): `)) || basename(repositoryPath);
202
- const baseBranch = (await question("Base branch (default: main): ")) || "main";
203
- // Create a path-safe version of the repository name for namespacing
204
- const repoNameSafe = repositoryName
205
- .replace(/[^a-zA-Z0-9-_]/g, "-")
206
- .toLowerCase();
207
- const workspaceBaseDir = resolve(this.cyrusHome, "workspaces", repoNameSafe);
208
- // Note: Prompt template is now hardcoded - no longer configurable
209
- // Set reasonable defaults for configuration
210
- // Allowed tools - default to all tools except Bash, plus Bash(git:*) and Bash(gh:*)
211
- // Note: MCP tools (mcp__linear, mcp__cyrus-mcp-tools) are automatically added by EdgeWorker
212
- const allowedTools = [
213
- "Read(**)",
214
- "Edit(**)",
215
- "Bash(git:*)",
216
- "Bash(gh:*)",
217
- "Task",
218
- "WebFetch",
219
- "WebSearch",
220
- "TodoRead",
221
- "TodoWrite",
222
- "NotebookRead",
223
- "NotebookEdit",
224
- "Batch",
225
- ];
226
- // Label prompts - default to common label mappings
227
- const labelPrompts = {
228
- debugger: {
229
- labels: ["Bug"],
230
- },
231
- builder: {
232
- labels: ["Feature", "Improvement"],
233
- },
234
- scoper: {
235
- labels: ["PRD"],
236
- },
237
- orchestrator: {
238
- labels: ["Orchestrator"],
239
- allowedTools: "coordinator", // Uses coordinator tools (all except file editing)
240
- },
241
- };
242
- if (shouldCloseRl) {
243
- rl.close();
244
- }
245
- // Create repository configuration
246
- const repository = {
247
- id: `${linearCredentials.linearWorkspaceId}-${Date.now()}`,
248
- name: repositoryName,
249
- repositoryPath: resolve(repositoryPath),
250
- baseBranch,
251
- linearWorkspaceId: linearCredentials.linearWorkspaceId,
252
- linearToken: linearCredentials.linearToken,
253
- workspaceBaseDir: resolve(workspaceBaseDir),
254
- isActive: true,
255
- allowedTools,
256
- labelPrompts,
257
- };
258
- return repository;
259
- }
260
- catch (error) {
261
- if (shouldCloseRl) {
262
- rl.close();
263
- }
264
- throw error;
265
- }
266
- }
267
- /**
268
- * Start OAuth flow to get Linear token using EdgeWorker's shared server
269
- */
270
- async startOAuthFlow(proxyUrl) {
271
- if (this.edgeWorker) {
272
- // Use existing EdgeWorker's OAuth flow
273
- const port = this.edgeWorker.getServerPort();
274
- const callbackBaseUrl = process.env.CYRUS_BASE_URL || `http://localhost:${port}`;
275
- const authUrl = `${proxyUrl}/oauth/authorize?callback=${callbackBaseUrl}/callback`;
276
- // Let SharedApplicationServer print the messages, but we handle browser opening
277
- const resultPromise = this.edgeWorker.startOAuthFlow(proxyUrl);
278
- // Open browser after SharedApplicationServer prints its messages
279
- open(authUrl).catch(() => {
280
- // Error is already communicated by SharedApplicationServer
281
- });
282
- return resultPromise;
283
- }
284
- else {
285
- // Create temporary SharedApplicationServer for OAuth flow during initial setup
286
- const serverPort = process.env.CYRUS_SERVER_PORT
287
- ? parseInt(process.env.CYRUS_SERVER_PORT, 10)
288
- : 3456;
289
- const tempServer = new SharedApplicationServer(serverPort);
290
- try {
291
- // Start the server
292
- await tempServer.start();
293
- const port = tempServer.getPort();
294
- const callbackBaseUrl = process.env.CYRUS_BASE_URL || `http://localhost:${port}`;
295
- const authUrl = `${proxyUrl}/oauth/authorize?callback=${callbackBaseUrl}/callback`;
296
- // Start OAuth flow (this prints the messages)
297
- const resultPromise = tempServer.startOAuthFlow(proxyUrl);
298
- // Open browser after SharedApplicationServer prints its messages
299
- open(authUrl).catch(() => {
300
- // Error is already communicated by SharedApplicationServer
301
- });
302
- // Wait for OAuth flow to complete
303
- const result = await resultPromise;
304
- return {
305
- linearToken: result.linearToken,
306
- linearWorkspaceId: result.linearWorkspaceId,
307
- linearWorkspaceName: result.linearWorkspaceName,
308
- };
309
- }
310
- finally {
311
- // Clean up temporary server
312
- await tempServer.stop();
313
- }
314
- }
315
- }
316
- /**
317
- * Get ngrok auth token from config or prompt user
318
- */
319
- async getNgrokAuthToken(config) {
320
- // Return existing token if available
321
- if (config.ngrokAuthToken) {
322
- return config.ngrokAuthToken;
323
- }
324
- // Skip ngrok setup if using external host
325
- const isExternalHost = process.env.CYRUS_HOST_EXTERNAL?.toLowerCase().trim() === "true";
326
- if (isExternalHost) {
327
- console.log(`\nšŸ“” Using external host configuration (CYRUS_HOST_EXTERNAL=true)`);
328
- console.log(` Skipping ngrok setup - using ${process.env.CYRUS_BASE_URL || "configured base URL"}`);
329
- return undefined;
330
- }
331
- // Prompt user for ngrok auth token
332
- console.log(`\nšŸ”— Ngrok Setup Required`);
333
- console.log(`─`.repeat(50));
334
- console.log(`Linear payloads need to reach your computer, so we use the secure technology ngrok for that.`);
335
- console.log(`This requires a free ngrok account and auth token.`);
336
- console.log(``);
337
- console.log(`To get your ngrok auth token:`);
338
- console.log(`1. Sign up at https://ngrok.com/ (free)`);
339
- console.log(`2. Go to https://dashboard.ngrok.com/get-started/your-authtoken`);
340
- console.log(`3. Copy your auth token`);
341
- console.log(``);
342
- console.log(`Alternatively, you can set CYRUS_HOST_EXTERNAL=true and CYRUS_BASE_URL`);
343
- console.log(`to handle port forwarding or reverse proxy yourself.`);
344
- console.log(``);
345
- const rl = readline.createInterface({
346
- input: process.stdin,
347
- output: process.stdout,
348
- });
349
- return new Promise((resolve) => {
350
- rl.question(`Enter your ngrok auth token (or press Enter to skip): `, async (token) => {
351
- rl.close();
352
- if (!token.trim()) {
353
- console.log(`\nāš ļø Skipping ngrok setup. You can set CYRUS_HOST_EXTERNAL=true and CYRUS_BASE_URL manually.`);
354
- resolve(undefined);
355
- return;
356
- }
357
- // Save token to config
358
- config.ngrokAuthToken = token.trim();
359
- try {
360
- this.saveEdgeConfig(config);
361
- console.log(`āœ… Ngrok auth token saved to config`);
362
- resolve(token.trim());
363
- }
364
- catch (error) {
365
- console.error(`āŒ Failed to save ngrok auth token:`, error);
366
- resolve(token.trim()); // Still use the token for this session
367
- }
368
- });
369
- });
370
- }
371
- /**
372
- * Start the EdgeWorker with given configuration
373
- */
374
- async startEdgeWorker({ proxyUrl, repositories, }) {
375
- // Get ngrok auth token (prompt if needed and not external host)
376
- let ngrokAuthToken;
377
- const isExternalHost = process.env.CYRUS_HOST_EXTERNAL?.toLowerCase().trim() === "true";
378
- if (!isExternalHost) {
379
- const config = this.loadEdgeConfig();
380
- ngrokAuthToken = await this.getNgrokAuthToken(config);
381
- }
382
- // Create EdgeWorker configuration
383
- const config = {
384
- proxyUrl,
385
- repositories,
386
- cyrusHome: this.cyrusHome,
387
- defaultAllowedTools: process.env.ALLOWED_TOOLS?.split(",").map((t) => t.trim()) || [],
388
- defaultDisallowedTools: process.env.DISALLOWED_TOOLS?.split(",").map((t) => t.trim()) ||
389
- undefined,
390
- // Model configuration: environment variables take precedence over config file
391
- defaultModel: process.env.CYRUS_DEFAULT_MODEL || this.loadEdgeConfig().defaultModel,
392
- defaultFallbackModel: process.env.CYRUS_DEFAULT_FALLBACK_MODEL ||
393
- this.loadEdgeConfig().defaultFallbackModel,
394
- webhookBaseUrl: process.env.CYRUS_BASE_URL,
395
- serverPort: process.env.CYRUS_SERVER_PORT
396
- ? parseInt(process.env.CYRUS_SERVER_PORT, 10)
397
- : 3456,
398
- serverHost: isExternalHost ? "0.0.0.0" : "localhost",
399
- ngrokAuthToken,
400
- features: {
401
- enableContinuation: true,
402
- },
403
- handlers: {
404
- createWorkspace: async (issue, repository) => {
405
- return this.createGitWorktree(issue, repository);
406
- },
407
- onOAuthCallback: async (token, workspaceId, workspaceName) => {
408
- const linearCredentials = {
409
- linearToken: token,
410
- linearWorkspaceId: workspaceId,
411
- linearWorkspaceName: workspaceName,
412
- };
413
- // Handle OAuth completion for repository setup
414
- if (this.edgeWorker) {
415
- console.log("\nšŸ“‹ Setting up new repository for workspace:", workspaceName);
416
- console.log("─".repeat(50));
417
- try {
418
- const newRepo = await this.setupRepositoryWizard(linearCredentials);
419
- // Add to existing repositories
420
- const edgeConfig = this.loadEdgeConfig();
421
- console.log(`šŸ“Š Current config has ${edgeConfig.repositories?.length || 0} repositories`);
422
- edgeConfig.repositories = [
423
- ...(edgeConfig.repositories || []),
424
- newRepo,
425
- ];
426
- console.log(`šŸ“Š Adding repository "${newRepo.name}", new total: ${edgeConfig.repositories.length}`);
427
- this.saveEdgeConfig(edgeConfig);
428
- console.log("\nāœ… Repository configured successfully!");
429
- console.log("šŸ“ ~/.cyrus/config.json file has been updated with your new repository configuration.");
430
- console.log("šŸ’” You can edit this file and restart Cyrus at any time to modify settings.");
431
- console.log("šŸ“– Configuration docs: https://github.com/ceedaragents/cyrus#configuration");
432
- // Restart edge worker with new config
433
- await this.edgeWorker.stop();
434
- this.edgeWorker = null;
435
- // Give a small delay to ensure file is written
436
- await new Promise((resolve) => setTimeout(resolve, 100));
437
- // Reload configuration and restart worker without going through setup
438
- const updatedConfig = this.loadEdgeConfig();
439
- console.log(`\nšŸ”„ Reloading with ${updatedConfig.repositories?.length || 0} repositories from config file`);
440
- return this.startEdgeWorker({
441
- proxyUrl,
442
- repositories: updatedConfig.repositories || [],
443
- });
444
- }
445
- catch (error) {
446
- console.error("\nāŒ Repository setup failed:", error.message);
447
- }
448
- }
449
- },
450
- },
451
- };
452
- // Create and start EdgeWorker
453
- this.edgeWorker = new EdgeWorker(config);
454
- // Set config path for dynamic reloading
455
- const configPath = this.getEdgeConfigPath();
456
- this.edgeWorker.setConfigPath(configPath);
457
- // Set up event handlers
458
- this.setupEventHandlers();
459
- // Start the worker
460
- await this.edgeWorker.start();
461
- console.log("\nāœ… Edge worker started successfully");
462
- console.log(`Configured proxy URL: ${config.proxyUrl}`);
463
- console.log(`Managing ${repositories.length} repositories:`);
464
- repositories.forEach((repo) => {
465
- console.log(` - ${repo.name} (${repo.repositoryPath})`);
466
- });
467
- }
468
- /**
469
- * Start Cloudflare tunnel client (Pro plan only)
470
- */
471
- async startCloudflareClient() {
472
- // Validate required environment variables
473
- const cloudflareToken = process.env.CLOUDFLARE_TOKEN;
474
- const cyrusApiKey = process.env.CYRUS_API_KEY;
475
- if (!cloudflareToken || !cyrusApiKey) {
476
- console.error("\nāŒ Missing required credentials");
477
- console.log("─".repeat(50));
478
- console.log("Cloudflare tunnel mode requires authentication.");
479
- console.log("\nRequired environment variables:");
480
- console.log(` CLOUDFLARE_TOKEN: ${cloudflareToken ? "āœ… Set" : "āŒ Missing"}`);
481
- console.log(` CYRUS_API_KEY: ${cyrusApiKey ? "āœ… Set" : "āŒ Missing"}`);
482
- console.log("\nPlease run: cyrus auth <auth-key>");
483
- console.log("Get your auth key from: https://www.atcyrus.com/onboarding/auth-cyrus");
484
- process.exit(1);
485
- }
486
- console.log("\nšŸŒ©ļø Starting Cloudflare Tunnel Client");
487
- console.log("─".repeat(50));
488
- try {
489
- const { CloudflareTunnelClient } = await import("cyrus-cloudflare-tunnel-client");
490
- // Get auth key from config or environment
491
- // For now, we'll use the API key as the auth key since it's what we validate against
492
- const authKey = cyrusApiKey;
493
- const client = new CloudflareTunnelClient({
494
- authKey,
495
- cyrusHome: this.cyrusHome,
496
- onWebhook: (payload) => {
497
- console.log("\nšŸ“Ø Webhook received from Linear");
498
- console.log(`Action: ${payload.action || "Unknown"}`);
499
- console.log(`Type: ${payload.type || "Unknown"}`);
500
- // TODO: Forward webhook to EdgeWorker or handle directly
501
- },
502
- onConfigUpdate: () => {
503
- console.log("\nšŸ”„ Configuration updated from cyrus-hosted");
504
- },
505
- onError: (error) => {
506
- console.error("\nāŒ Cloudflare client error:", error.message);
507
- },
508
- onReady: (tunnelUrl) => {
509
- console.log("\nāœ… Cloudflare tunnel established");
510
- console.log(`šŸ”— Tunnel URL: ${tunnelUrl}`);
511
- console.log("─".repeat(50));
512
- console.log("\nšŸ’Ž Pro Plan Active - Using Cloudflare Tunnel");
513
- console.log("šŸš€ Cyrus is now ready to receive webhooks and config updates");
514
- console.log("─".repeat(50));
515
- },
516
- });
517
- // Authenticate and start the tunnel
518
- await client.authenticate();
519
- // Handle graceful shutdown
520
- process.on("SIGINT", () => {
521
- console.log("\n\nšŸ›‘ Shutting down Cloudflare tunnel...");
522
- client.disconnect();
523
- process.exit(0);
524
- });
525
- process.on("SIGTERM", () => {
526
- console.log("\n\nšŸ›‘ Shutting down Cloudflare tunnel...");
527
- client.disconnect();
528
- process.exit(0);
529
- });
530
- }
531
- catch (error) {
532
- console.error("\nāŒ Failed to start Cloudflare tunnel:");
533
- console.error(error.message);
534
- console.log("\nIf you're having issues, try re-authenticating with: cyrus auth <auth-key>");
535
- process.exit(1);
536
- }
537
- }
538
- /**
539
- * Check subscription status with the Cyrus API
540
- */
541
- async checkSubscriptionStatus(customerId) {
542
- const response = await fetch(`https://www.atcyrus.com/api/subscription-status?customerId=${encodeURIComponent(customerId)}`, {
543
- method: "GET",
544
- headers: {
545
- "Content-Type": "application/json",
546
- },
547
- });
548
- if (!response.ok) {
549
- if (response.status === 400) {
550
- const data = (await response.json());
551
- throw new Error(data.error || "Invalid customer ID format");
552
- }
553
- throw new Error(`HTTP error! status: ${response.status}`);
554
- }
555
- const data = (await response.json());
556
- return data;
557
- }
558
- /**
559
- * Validate customer ID format
560
- */
561
- validateCustomerId(customerId) {
562
- if (!customerId.startsWith("cus_")) {
563
- console.error("\nāŒ Invalid customer ID format");
564
- console.log('Customer IDs should start with "cus_"');
565
- process.exit(1);
566
- }
567
- }
568
- /**
569
- * Handle subscription validation failure
570
- */
571
- handleSubscriptionFailure(subscriptionStatus) {
572
- console.error("\nāŒ Subscription Invalid");
573
- console.log("─".repeat(50));
574
- if (subscriptionStatus.isReturningCustomer) {
575
- console.log("Your subscription has expired or been cancelled.");
576
- console.log(`Status: ${subscriptionStatus.status}`);
577
- console.log("\nPlease visit https://www.atcyrus.com/pricing to reactivate your subscription.");
578
- }
579
- else {
580
- console.log("No active subscription found for this customer ID.");
581
- console.log("\nPlease visit https://www.atcyrus.com/pricing to start a subscription.");
582
- console.log("Once you obtain a valid customer ID,");
583
- console.log("Run: cyrus set-customer-id cus_XXXXX");
584
- }
585
- process.exit(1);
586
- }
587
- /**
588
- * Validate subscription and handle failures
589
- */
590
- async validateAndHandleSubscription(customerId) {
591
- console.log("\nšŸ” Validating subscription...");
592
- try {
593
- const subscriptionStatus = await this.checkSubscriptionStatus(customerId);
594
- if (subscriptionStatus.requiresPayment) {
595
- this.handleSubscriptionFailure(subscriptionStatus);
596
- }
597
- console.log(`āœ… Subscription active (${subscriptionStatus.status})`);
598
- }
599
- catch (error) {
600
- console.error("\nāŒ Failed to validate subscription");
601
- console.log(`Error: ${error.message}`);
602
- console.log('Run "cyrus set-customer-id cus_XXXXX" with a valid customer ID');
603
- process.exit(1);
604
- }
605
- }
606
- /**
607
- * Create readline interface and ask question
608
- */
609
- async askQuestion(prompt) {
610
- const rl = readline.createInterface({
611
- input: process.stdin,
612
- output: process.stdout,
613
- });
614
- return new Promise((resolve) => {
615
- rl.question(prompt, (answer) => {
616
- rl.close();
617
- resolve(answer.trim());
618
- });
619
- });
620
- }
621
- /**
622
- * Start the edge application
623
- */
624
- async start() {
625
- try {
626
- // Set proxy URL with default
627
- const proxyUrl = process.env.PROXY_URL || DEFAULT_PROXY_URL;
628
- // No need to validate Claude CLI - using Claude TypeScript SDK now
629
- // Load edge configuration
630
- let edgeConfig = this.loadEdgeConfig();
631
- let repositories = edgeConfig.repositories || [];
632
- // Check if using default proxy URL without a customer ID
633
- const isUsingDefaultProxy = proxyUrl === DEFAULT_PROXY_URL;
634
- const hasCustomerId = !!edgeConfig.stripeCustomerId;
635
- if (isUsingDefaultProxy && !hasCustomerId) {
636
- console.log("\nšŸŽÆ Pro Plan Required");
637
- console.log("─".repeat(50));
638
- console.log("You are using the default Cyrus proxy URL.");
639
- console.log("\nWith Cyrus Pro you get:");
640
- console.log("• No-hassle configuration");
641
- console.log("• Priority support");
642
- console.log("• Help fund product development");
643
- console.log("\nChoose an option:");
644
- console.log("1. Start a free trial");
645
- console.log("2. I have a customer ID to enter");
646
- console.log("3. Setup your own proxy (advanced)");
647
- console.log("4. Exit");
648
- const choice = await this.askQuestion("\nYour choice (1-4): ");
649
- if (choice === "1") {
650
- console.log("\nšŸ‘‰ Opening your browser to start a free trial...");
651
- console.log("Visit: https://www.atcyrus.com/pricing");
652
- await open("https://www.atcyrus.com/pricing");
653
- process.exit(0);
654
- }
655
- else if (choice === "2") {
656
- console.log("\nšŸ“‹ After completing payment, you'll see your customer ID on the success page.");
657
- console.log('It starts with "cus_" and can be copied from the website.');
658
- const customerId = await this.askQuestion("\nPaste your customer ID here: ");
659
- this.validateCustomerId(customerId);
660
- edgeConfig.stripeCustomerId = customerId;
661
- this.saveEdgeConfig(edgeConfig);
662
- console.log("āœ… Customer ID saved successfully!");
663
- console.log("Continuing with startup...\n");
664
- // Reload config to include the new customer ID
665
- edgeConfig = this.loadEdgeConfig();
666
- }
667
- else if (choice === "3") {
668
- console.log("\nšŸ”§ Self-Hosted Proxy Setup");
669
- console.log("─".repeat(50));
670
- console.log("Configure your own Linear app and proxy to have full control over your stack.");
671
- console.log("\nDocumentation:");
672
- console.log("• Linear OAuth setup: https://linear.app/developers/agents");
673
- console.log("• Proxy implementation: https://github.com/ceedaragents/cyrus/tree/main/apps/proxy-worker");
674
- console.log("\nOnce deployed, set the PROXY_URL environment variable:");
675
- console.log("export PROXY_URL=https://your-proxy-url.com");
676
- process.exit(0);
677
- }
678
- else {
679
- console.log("\nExiting...");
680
- process.exit(0);
681
- }
682
- }
683
- // If using default proxy and has customer ID, validate subscription
684
- if (isUsingDefaultProxy && edgeConfig.stripeCustomerId) {
685
- try {
686
- await this.validateAndHandleSubscription(edgeConfig.stripeCustomerId);
687
- }
688
- catch (error) {
689
- console.error("\nāš ļø Warning: Could not validate subscription");
690
- console.log("─".repeat(50));
691
- console.error("Unable to connect to subscription service:", error.message);
692
- process.exit(1);
693
- }
694
- }
695
- // Check if we need to set up
696
- const needsSetup = repositories.length === 0;
697
- const hasLinearCredentials = repositories.some((r) => r.linearToken) ||
698
- process.env.LINEAR_OAUTH_TOKEN;
699
- if (needsSetup) {
700
- console.log("šŸš€ Welcome to Cyrus Edge Worker!");
701
- // Check if they want to use existing credentials or add new workspace
702
- let linearCredentials = null;
703
- if (hasLinearCredentials) {
704
- // Show available workspaces from existing repos
705
- const workspaces = new Map();
706
- for (const repo of edgeConfig.repositories || []) {
707
- if (!workspaces.has(repo.linearWorkspaceId)) {
708
- workspaces.set(repo.linearWorkspaceId, {
709
- id: repo.linearWorkspaceId,
710
- name: "Unknown Workspace",
711
- token: repo.linearToken,
712
- });
713
- }
714
- }
715
- if (workspaces.size === 1) {
716
- // Only one workspace, use it
717
- const ws = Array.from(workspaces.values())[0];
718
- if (ws) {
719
- linearCredentials = {
720
- linearToken: ws.token,
721
- linearWorkspaceId: ws.id,
722
- linearWorkspaceName: ws.name,
723
- };
724
- console.log(`\nšŸ“‹ Using Linear workspace: ${linearCredentials.linearWorkspaceName}`);
725
- }
726
- }
727
- else if (workspaces.size > 1) {
728
- // Multiple workspaces, let user choose
729
- console.log("\nšŸ“‹ Available Linear workspaces:");
730
- const workspaceList = Array.from(workspaces.values());
731
- workspaceList.forEach((ws, i) => {
732
- console.log(`${i + 1}. ${ws.name}`);
733
- });
734
- const choice = await this.askQuestion("\nSelect workspace (number) or press Enter for new: ");
735
- const index = parseInt(choice, 10) - 1;
736
- if (index >= 0 && index < workspaceList.length) {
737
- const ws = workspaceList[index];
738
- if (ws) {
739
- linearCredentials = {
740
- linearToken: ws.token,
741
- linearWorkspaceId: ws.id,
742
- linearWorkspaceName: ws.name,
743
- };
744
- console.log(`Using workspace: ${linearCredentials.linearWorkspaceName}`);
745
- }
746
- }
747
- else {
748
- // Get new credentials
749
- linearCredentials = null;
750
- }
751
- }
752
- else if (process.env.LINEAR_OAUTH_TOKEN) {
753
- // Use env vars
754
- linearCredentials = {
755
- linearToken: process.env.LINEAR_OAUTH_TOKEN,
756
- linearWorkspaceId: process.env.LINEAR_WORKSPACE_ID || "unknown",
757
- linearWorkspaceName: "Your Workspace",
758
- };
759
- }
760
- if (linearCredentials) {
761
- console.log("(OAuth server will start with EdgeWorker to connect additional workspaces)");
762
- }
763
- }
764
- else {
765
- // Get new Linear credentials
766
- console.log("\nšŸ“‹ Step 1: Connect to Linear");
767
- console.log("─".repeat(50));
768
- try {
769
- linearCredentials = await this.startOAuthFlow(proxyUrl);
770
- console.log("\nāœ… Linear connected successfully!");
771
- }
772
- catch (error) {
773
- console.error("\nāŒ OAuth flow failed:", error.message);
774
- console.log("\nAlternatively, you can:");
775
- console.log("1. Visit", `${proxyUrl}/oauth/authorize`, "in your browser");
776
- console.log("2. Copy the token after authorization");
777
- console.log("3. Add it to your .env.cyrus file as LINEAR_OAUTH_TOKEN");
778
- process.exit(1);
779
- }
780
- }
781
- if (!linearCredentials) {
782
- console.error("āŒ No Linear credentials available");
783
- process.exit(1);
784
- }
785
- // Now set up repository
786
- console.log("\nšŸ“‹ Step 2: Configure Repository");
787
- console.log("─".repeat(50));
788
- // Create a single readline interface for the entire repository setup process
789
- const rl = readline.createInterface({
790
- input: process.stdin,
791
- output: process.stdout,
792
- });
793
- try {
794
- // Loop to allow adding multiple repositories
795
- let continueAdding = true;
796
- while (continueAdding) {
797
- try {
798
- const newRepo = await this.setupRepositoryWizard(linearCredentials, rl);
799
- // Add to repositories
800
- repositories = [...(edgeConfig.repositories || []), newRepo];
801
- edgeConfig.repositories = repositories;
802
- this.saveEdgeConfig(edgeConfig);
803
- console.log("\nāœ… Repository configured successfully!");
804
- console.log("šŸ“ ~/.cyrus/config.json file has been updated with your repository configuration.");
805
- console.log("šŸ’” You can edit this file and restart Cyrus at any time to modify settings.");
806
- console.log("šŸ“– Configuration docs: https://github.com/ceedaragents/cyrus#configuration");
807
- // Ask if they want to add another
808
- const addAnother = await new Promise((resolve) => {
809
- rl.question("\nAdd another repository? (y/N): ", (answer) => {
810
- resolve(answer.toLowerCase() === "y");
811
- });
812
- });
813
- continueAdding = addAnother;
814
- if (continueAdding) {
815
- console.log("\nšŸ“‹ Configure Additional Repository");
816
- console.log("─".repeat(50));
817
- }
818
- }
819
- catch (error) {
820
- console.error("\nāŒ Repository setup failed:", error.message);
821
- throw error;
822
- }
823
- }
824
- }
825
- finally {
826
- // Always close the readline interface when done
827
- rl.close();
828
- }
829
- }
830
- // Check if using Cloudflare tunnel mode (Pro plan)
831
- const isLegacy = edgeConfig.isLegacy !== false; // Default to true if not set
832
- if (!isLegacy) {
833
- // Pro plan with Cloudflare tunnel
834
- console.log("\nšŸ’Ž Pro Plan Detected");
835
- console.log("─".repeat(50));
836
- console.log("Using Cloudflare tunnel for secure connectivity");
837
- // Start Cloudflare tunnel client (will validate credentials and start)
838
- await this.startCloudflareClient();
839
- return; // Exit early - Cloudflare client handles everything
840
- }
841
- // Legacy mode - validate we have repositories
842
- if (repositories.length === 0) {
843
- console.error("āŒ No repositories configured");
844
- console.log("\nUse the authorization link above to configure your first repository.");
845
- process.exit(1);
846
- }
847
- // Start the edge worker (legacy mode)
848
- await this.startEdgeWorker({ proxyUrl, repositories });
849
- // Display plan status
850
- const isUsingDefaultProxyForStatus = proxyUrl === DEFAULT_PROXY_URL;
851
- const hasCustomerIdForStatus = !!edgeConfig.stripeCustomerId;
852
- console.log(`\n${"─".repeat(70)}`);
853
- if (isUsingDefaultProxyForStatus && hasCustomerIdForStatus) {
854
- console.log("šŸ’Ž Plan: Cyrus Pro");
855
- console.log(`šŸ“‹ Customer ID: ${edgeConfig.stripeCustomerId}`);
856
- console.log('šŸ’³ Manage subscription: Run "cyrus billing"');
857
- }
858
- else if (!isUsingDefaultProxyForStatus) {
859
- console.log("šŸ› ļø Plan: Community (Self-hosted proxy)");
860
- console.log(`šŸ”— Proxy URL: ${proxyUrl}`);
861
- }
862
- console.log("─".repeat(70));
863
- // Display OAuth information after EdgeWorker is started
864
- const serverPort = this.edgeWorker?.getServerPort() || 3456;
865
- const oauthCallbackBaseUrl = process.env.CYRUS_BASE_URL || `http://localhost:${serverPort}`;
866
- console.log(`\nšŸ” OAuth server running on port ${serverPort}`);
867
- console.log(`šŸ‘‰ To authorize Linear (new workspace or re-auth):`);
868
- console.log(` ${proxyUrl}/oauth/authorize?callback=${oauthCallbackBaseUrl}/callback`);
869
- console.log("─".repeat(70));
870
- // Handle graceful shutdown
871
- process.on("SIGINT", () => this.shutdown());
872
- process.on("SIGTERM", () => this.shutdown());
873
- // Handle uncaught exceptions and unhandled promise rejections
874
- process.on("uncaughtException", (error) => {
875
- console.error("🚨 Uncaught Exception:", error.message);
876
- console.error("Error type:", error.constructor.name);
877
- console.error("Stack:", error.stack);
878
- console.error("This error was caught by the global handler, preventing application crash");
879
- // Attempt graceful shutdown but don't wait indefinitely
880
- this.shutdown().finally(() => {
881
- console.error("Process exiting due to uncaught exception");
882
- process.exit(1);
883
- });
884
- });
885
- process.on("unhandledRejection", (reason, promise) => {
886
- console.error("🚨 Unhandled Promise Rejection at:", promise);
887
- console.error("Reason:", reason);
888
- console.error("This rejection was caught by the global handler, continuing operation");
889
- // Log stack trace if reason is an Error
890
- if (reason instanceof Error && reason.stack) {
891
- console.error("Stack:", reason.stack);
892
- }
893
- // Log the error but don't exit the process for promise rejections
894
- // as they might be recoverable
895
- });
896
- }
897
- catch (error) {
898
- console.error("\nāŒ Failed to start edge application:", error.message);
899
- // Provide more specific guidance for common errors
900
- if (error.message?.includes("Failed to connect any repositories")) {
901
- console.error("\nšŸ’” This usually happens when:");
902
- console.error(" - All Linear OAuth tokens have expired");
903
- console.error(" - The Linear API is temporarily unavailable");
904
- console.error(" - Your network connection is having issues");
905
- console.error("\nPlease check your edge configuration and try again.");
906
- }
907
- await this.shutdown();
908
- process.exit(1);
909
- }
910
- }
911
- /**
912
- * Check if a branch exists locally or remotely
913
- */
914
- async branchExists(branchName, repoPath) {
915
- const { execSync } = await import("node:child_process");
916
- try {
917
- // Check if branch exists locally
918
- execSync(`git rev-parse --verify "${branchName}"`, {
919
- cwd: repoPath,
920
- stdio: "pipe",
921
- });
922
- return true;
923
- }
924
- catch {
925
- // Branch doesn't exist locally, check remote
926
- try {
927
- const remoteOutput = execSync(`git ls-remote --heads origin "${branchName}"`, {
928
- cwd: repoPath,
929
- stdio: "pipe",
930
- });
931
- // Check if output is non-empty (branch actually exists on remote)
932
- return remoteOutput && remoteOutput.toString().trim().length > 0;
933
- }
934
- catch {
935
- return false;
936
- }
937
- }
938
- }
939
- /**
940
- * Set up event handlers for EdgeWorker
941
- */
942
- setupEventHandlers() {
943
- if (!this.edgeWorker)
944
- return;
945
- // Session events
946
- this.edgeWorker.on("session:started", (issueId, _issue, repositoryId) => {
947
- console.log(`Started session for issue ${issueId} in repository ${repositoryId}`);
948
- });
949
- this.edgeWorker.on("session:ended", (issueId, exitCode, repositoryId) => {
950
- console.log(`Session for issue ${issueId} ended with exit code ${exitCode} in repository ${repositoryId}`);
951
- });
952
- // Connection events
953
- this.edgeWorker.on("connected", (token) => {
954
- console.log(`āœ… Connected to proxy with token ending in ...${token.slice(-4)}`);
955
- });
956
- this.edgeWorker.on("disconnected", (token, reason) => {
957
- console.error(`āŒ Disconnected from proxy (token ...${token.slice(-4)}): ${reason || "Unknown reason"}`);
958
- });
959
- // Error events
960
- this.edgeWorker.on("error", (error) => {
961
- console.error("EdgeWorker error:", error);
962
- });
963
- }
964
- /**
965
- * Run a setup script with proper error handling and logging
966
- */
967
- async runSetupScript(scriptPath, scriptType, workspacePath, issue) {
968
- const { execSync } = await import("node:child_process");
969
- const { existsSync, statSync } = await import("node:fs");
970
- const { basename } = await import("node:path");
971
- const os = await import("node:os");
972
- // Expand ~ to home directory
973
- const expandedPath = scriptPath.replace(/^~/, os.homedir());
974
- // Check if script exists
975
- if (!existsSync(expandedPath)) {
976
- console.warn(`āš ļø ${scriptType === "global" ? "Global" : "Repository"} setup script not found: ${scriptPath}`);
977
- return;
978
- }
979
- // Check if script is executable (Unix only)
980
- if (process.platform !== "win32") {
981
- try {
982
- const stats = statSync(expandedPath);
983
- // Check if file has execute permission for the owner
984
- if (!(stats.mode & 0o100)) {
985
- console.warn(`āš ļø ${scriptType === "global" ? "Global" : "Repository"} setup script is not executable: ${scriptPath}`);
986
- console.warn(` Run: chmod +x "${expandedPath}"`);
987
- return;
988
- }
989
- }
990
- catch (error) {
991
- console.warn(`āš ļø Cannot check permissions for ${scriptType} setup script: ${error.message}`);
992
- return;
993
- }
994
- }
995
- const scriptName = basename(expandedPath);
996
- console.log(`ā„¹ļø Running ${scriptType} setup script: ${scriptName}`);
997
- try {
998
- // Determine the command based on the script extension and platform
999
- let command;
1000
- const isWindows = process.platform === "win32";
1001
- if (scriptPath.endsWith(".ps1")) {
1002
- command = `powershell -ExecutionPolicy Bypass -File "${expandedPath}"`;
1003
- }
1004
- else if (scriptPath.endsWith(".cmd") || scriptPath.endsWith(".bat")) {
1005
- command = `"${expandedPath}"`;
1006
- }
1007
- else if (isWindows) {
1008
- // On Windows, try to run with bash if available (Git Bash/WSL)
1009
- command = `bash "${expandedPath}"`;
1010
- }
1011
- else {
1012
- // On Unix, run directly with bash
1013
- command = `bash "${expandedPath}"`;
1014
- }
1015
- execSync(command, {
1016
- cwd: workspacePath,
1017
- stdio: "inherit",
1018
- env: {
1019
- ...process.env,
1020
- LINEAR_ISSUE_ID: issue.id,
1021
- LINEAR_ISSUE_IDENTIFIER: issue.identifier,
1022
- LINEAR_ISSUE_TITLE: issue.title || "",
1023
- },
1024
- timeout: 5 * 60 * 1000, // 5 minute timeout
1025
- });
1026
- console.log(`āœ… ${scriptType === "global" ? "Global" : "Repository"} setup script completed successfully`);
1027
- }
1028
- catch (error) {
1029
- const errorMessage = error.signal === "SIGTERM"
1030
- ? "Script execution timed out (exceeded 5 minutes)"
1031
- : error.message;
1032
- console.error(`āŒ ${scriptType === "global" ? "Global" : "Repository"} setup script failed: ${errorMessage}`);
1033
- // Log stderr if available
1034
- if (error.stderr) {
1035
- console.error(" stderr:", error.stderr.toString());
1036
- }
1037
- // Continue execution despite setup script failure
1038
- console.log(` Continuing with worktree creation...`);
1039
- }
1040
- }
1041
- /**
1042
- * Create a git worktree for an issue
1043
- */
1044
- async createGitWorktree(issue, repository) {
1045
- const { execSync } = await import("node:child_process");
1046
- const { existsSync } = await import("node:fs");
1047
- const { join } = await import("node:path");
1048
- try {
1049
- // Verify this is a git repository
1050
- try {
1051
- execSync("git rev-parse --git-dir", {
1052
- cwd: repository.repositoryPath,
1053
- stdio: "pipe",
1054
- });
1055
- }
1056
- catch (_e) {
1057
- console.error(`${repository.repositoryPath} is not a git repository`);
1058
- throw new Error("Not a git repository");
1059
- }
1060
- // Sanitize branch name by removing backticks to prevent command injection
1061
- const sanitizeBranchName = (name) => name ? name.replace(/`/g, "") : name;
1062
- // Use Linear's preferred branch name, or generate one if not available
1063
- const rawBranchName = issue.branchName ||
1064
- `${issue.identifier}-${issue.title
1065
- ?.toLowerCase()
1066
- .replace(/\s+/g, "-")
1067
- .substring(0, 30)}`;
1068
- const branchName = sanitizeBranchName(rawBranchName);
1069
- const workspacePath = join(repository.workspaceBaseDir, issue.identifier);
1070
- // Ensure workspace directory exists
1071
- mkdirSync(repository.workspaceBaseDir, { recursive: true });
1072
- // Check if worktree already exists
1073
- try {
1074
- const worktrees = execSync("git worktree list --porcelain", {
1075
- cwd: repository.repositoryPath,
1076
- encoding: "utf-8",
1077
- });
1078
- if (worktrees.includes(workspacePath)) {
1079
- console.log(`Worktree already exists at ${workspacePath}, using existing`);
1080
- return {
1081
- path: workspacePath,
1082
- isGitWorktree: true,
1083
- };
1084
- }
1085
- }
1086
- catch (_e) {
1087
- // git worktree command failed, continue with creation
1088
- }
1089
- // Check if branch already exists
1090
- let createBranch = true;
1091
- try {
1092
- execSync(`git rev-parse --verify "${branchName}"`, {
1093
- cwd: repository.repositoryPath,
1094
- stdio: "pipe",
1095
- });
1096
- createBranch = false;
1097
- }
1098
- catch (_e) {
1099
- // Branch doesn't exist, we'll create it
1100
- }
1101
- // Determine base branch for this issue
1102
- let baseBranch = repository.baseBranch;
1103
- // Check if issue has a parent
1104
- try {
1105
- const parent = await issue.parent;
1106
- if (parent) {
1107
- console.log(`Issue ${issue.identifier} has parent: ${parent.identifier}`);
1108
- // Get parent's branch name
1109
- const parentRawBranchName = parent.branchName ||
1110
- `${parent.identifier}-${parent.title
1111
- ?.toLowerCase()
1112
- .replace(/\s+/g, "-")
1113
- .substring(0, 30)}`;
1114
- const parentBranchName = sanitizeBranchName(parentRawBranchName);
1115
- // Check if parent branch exists
1116
- const parentBranchExists = await this.branchExists(parentBranchName, repository.repositoryPath);
1117
- if (parentBranchExists) {
1118
- baseBranch = parentBranchName;
1119
- console.log(`Using parent issue branch '${parentBranchName}' as base for sub-issue ${issue.identifier}`);
1120
- }
1121
- else {
1122
- console.log(`Parent branch '${parentBranchName}' not found, using default base branch '${repository.baseBranch}'`);
1123
- }
1124
- }
1125
- }
1126
- catch (_error) {
1127
- // Parent field might not exist or couldn't be fetched, use default base branch
1128
- console.log(`No parent issue found for ${issue.identifier}, using default base branch '${repository.baseBranch}'`);
1129
- }
1130
- // Fetch latest changes from remote
1131
- console.log("Fetching latest changes from remote...");
1132
- let hasRemote = true;
1133
- try {
1134
- execSync("git fetch origin", {
1135
- cwd: repository.repositoryPath,
1136
- stdio: "pipe",
1137
- });
1138
- }
1139
- catch (e) {
1140
- console.warn("Warning: git fetch failed, proceeding with local branch:", e.message);
1141
- hasRemote = false;
1142
- }
1143
- // Create the worktree - use determined base branch
1144
- let worktreeCmd;
1145
- if (createBranch) {
1146
- if (hasRemote) {
1147
- // Check if the base branch exists remotely
1148
- let useRemoteBranch = false;
1149
- try {
1150
- const remoteOutput = execSync(`git ls-remote --heads origin "${baseBranch}"`, {
1151
- cwd: repository.repositoryPath,
1152
- stdio: "pipe",
1153
- });
1154
- // Check if output is non-empty (branch actually exists on remote)
1155
- useRemoteBranch =
1156
- remoteOutput && remoteOutput.toString().trim().length > 0;
1157
- if (!useRemoteBranch) {
1158
- console.log(`Base branch '${baseBranch}' not found on remote, checking locally...`);
1159
- }
1160
- }
1161
- catch {
1162
- // Base branch doesn't exist remotely, use local or fall back to default
1163
- console.log(`Base branch '${baseBranch}' not found on remote, checking locally...`);
1164
- }
1165
- if (useRemoteBranch) {
1166
- // Use remote version of base branch
1167
- const remoteBranch = `origin/${baseBranch}`;
1168
- console.log(`Creating git worktree at ${workspacePath} from ${remoteBranch}`);
1169
- worktreeCmd = `git worktree add "${workspacePath}" -b "${branchName}" "${remoteBranch}"`;
1170
- }
1171
- else {
1172
- // Check if base branch exists locally
1173
- try {
1174
- execSync(`git rev-parse --verify "${baseBranch}"`, {
1175
- cwd: repository.repositoryPath,
1176
- stdio: "pipe",
1177
- });
1178
- // Use local base branch
1179
- console.log(`Creating git worktree at ${workspacePath} from local ${baseBranch}`);
1180
- worktreeCmd = `git worktree add "${workspacePath}" -b "${branchName}" "${baseBranch}"`;
1181
- }
1182
- catch {
1183
- // Base branch doesn't exist locally either, fall back to remote default
1184
- console.log(`Base branch '${baseBranch}' not found locally, falling back to remote ${repository.baseBranch}`);
1185
- const defaultRemoteBranch = `origin/${repository.baseBranch}`;
1186
- worktreeCmd = `git worktree add "${workspacePath}" -b "${branchName}" "${defaultRemoteBranch}"`;
1187
- }
1188
- }
1189
- }
1190
- else {
1191
- // No remote, use local branch
1192
- console.log(`Creating git worktree at ${workspacePath} from local ${baseBranch}`);
1193
- worktreeCmd = `git worktree add "${workspacePath}" -b "${branchName}" "${baseBranch}"`;
1194
- }
1195
- }
1196
- else {
1197
- // Branch already exists, just check it out
1198
- console.log(`Creating git worktree at ${workspacePath} with existing branch ${branchName}`);
1199
- worktreeCmd = `git worktree add "${workspacePath}" "${branchName}"`;
1200
- }
1201
- execSync(worktreeCmd, {
1202
- cwd: repository.repositoryPath,
1203
- stdio: "pipe",
1204
- });
1205
- // First, run the global setup script if configured
1206
- const config = this.loadEdgeConfig();
1207
- if (config.global_setup_script) {
1208
- await this.runSetupScript(config.global_setup_script, "global", workspacePath, issue);
1209
- }
1210
- // Then, check for repository setup scripts (cross-platform)
1211
- const isWindows = process.platform === "win32";
1212
- const setupScripts = [
1213
- {
1214
- file: "cyrus-setup.sh",
1215
- platform: "unix",
1216
- },
1217
- {
1218
- file: "cyrus-setup.ps1",
1219
- platform: "windows",
1220
- },
1221
- {
1222
- file: "cyrus-setup.cmd",
1223
- platform: "windows",
1224
- },
1225
- {
1226
- file: "cyrus-setup.bat",
1227
- platform: "windows",
1228
- },
1229
- ];
1230
- // Find the first available setup script for the current platform
1231
- const availableScript = setupScripts.find((script) => {
1232
- const scriptPath = join(repository.repositoryPath, script.file);
1233
- const isCompatible = isWindows
1234
- ? script.platform === "windows"
1235
- : script.platform === "unix";
1236
- return existsSync(scriptPath) && isCompatible;
1237
- });
1238
- // Fallback: on Windows, try bash if no Windows scripts found (for Git Bash/WSL users)
1239
- const fallbackScript = !availableScript && isWindows
1240
- ? setupScripts.find((script) => {
1241
- const scriptPath = join(repository.repositoryPath, script.file);
1242
- return script.platform === "unix" && existsSync(scriptPath);
1243
- })
1244
- : null;
1245
- const scriptToRun = availableScript || fallbackScript;
1246
- if (scriptToRun) {
1247
- const scriptPath = join(repository.repositoryPath, scriptToRun.file);
1248
- await this.runSetupScript(scriptPath, "repository", workspacePath, issue);
1249
- }
1250
- return {
1251
- path: workspacePath,
1252
- isGitWorktree: true,
1253
- };
1254
- }
1255
- catch (error) {
1256
- console.error("Failed to create git worktree:", error.message);
1257
- // Fall back to regular directory if git worktree fails
1258
- const fallbackPath = join(repository.workspaceBaseDir, issue.identifier);
1259
- mkdirSync(fallbackPath, { recursive: true });
1260
- return {
1261
- path: fallbackPath,
1262
- isGitWorktree: false,
1263
- };
1264
- }
1265
- }
1266
- /**
1267
- * Shut down the application
1268
- */
1269
- async shutdown() {
1270
- if (this.isShuttingDown)
1271
- return;
1272
- this.isShuttingDown = true;
1273
- console.log("\nShutting down edge worker...");
1274
- // Stop edge worker (includes stopping shared application server)
1275
- if (this.edgeWorker) {
1276
- await this.edgeWorker.stop();
1277
- }
1278
- console.log("Shutdown complete");
1279
- process.exit(0);
1280
- }
1281
- }
1282
- // Helper function to check Linear token status
1283
- async function checkLinearToken(token) {
1284
- try {
1285
- const response = await fetch("https://api.linear.app/graphql", {
1286
- method: "POST",
1287
- headers: {
1288
- "Content-Type": "application/json",
1289
- Authorization: token,
1290
- },
1291
- body: JSON.stringify({
1292
- query: "{ viewer { id email name } }",
1293
- }),
1294
- });
1295
- const data = (await response.json());
1296
- if (data.errors) {
1297
- return {
1298
- valid: false,
1299
- error: data.errors[0]?.message || "Unknown error",
1300
- };
1301
- }
1302
- return { valid: true };
1303
- }
1304
- catch (error) {
1305
- return { valid: false, error: error.message };
1306
- }
1307
- }
1308
- // Command: check-tokens
1309
- async function checkTokensCommand() {
1310
- const app = new EdgeApp(CYRUS_HOME);
1311
- const configPath = app.getEdgeConfigPath();
1312
- if (!existsSync(configPath)) {
1313
- console.error("No edge configuration found. Please run setup first.");
1314
- process.exit(1);
1315
- }
1316
- const config = JSON.parse(readFileSync(configPath, "utf-8"));
1317
- console.log("Checking Linear tokens...\n");
1318
- for (const repo of config.repositories) {
1319
- process.stdout.write(`${repo.name} (${repo.linearWorkspaceName}): `);
1320
- const result = await checkLinearToken(repo.linearToken);
1321
- if (result.valid) {
1322
- console.log("āœ… Valid");
1323
- }
1324
- else {
1325
- console.log(`āŒ Invalid - ${result.error}`);
1326
- }
1327
- }
1328
- }
1329
- // Command: refresh-token
1330
- async function refreshTokenCommand() {
1331
- const app = new EdgeApp(CYRUS_HOME);
1332
- const configPath = app.getEdgeConfigPath();
1333
- if (!existsSync(configPath)) {
1334
- console.error("No edge configuration found. Please run setup first.");
1335
- process.exit(1);
1336
- }
1337
- const config = JSON.parse(readFileSync(configPath, "utf-8"));
1338
- // Show repositories with their token status
1339
- console.log("Checking current token status...\n");
1340
- const tokenStatuses = [];
1341
- for (const repo of config.repositories) {
1342
- const result = await checkLinearToken(repo.linearToken);
1343
- tokenStatuses.push({ repo, valid: result.valid });
1344
- console.log(`${tokenStatuses.length}. ${repo.name} (${repo.linearWorkspaceName}): ${result.valid ? "āœ… Valid" : "āŒ Invalid"}`);
1345
- }
1346
- // Ask which token to refresh
1347
- const answer = await app.askQuestion('\nWhich repository token would you like to refresh? (Enter number or "all"): ');
1348
- const indicesToRefresh = [];
1349
- if (answer.toLowerCase() === "all") {
1350
- indicesToRefresh.push(...Array.from({ length: tokenStatuses.length }, (_, i) => i));
1351
- }
1352
- else {
1353
- const index = parseInt(answer, 10) - 1;
1354
- if (Number.isNaN(index) || index < 0 || index >= tokenStatuses.length) {
1355
- console.error("Invalid selection");
1356
- process.exit(1);
1357
- }
1358
- indicesToRefresh.push(index);
1359
- }
1360
- // Refresh tokens
1361
- for (const index of indicesToRefresh) {
1362
- const tokenStatus = tokenStatuses[index];
1363
- if (!tokenStatus)
1364
- continue;
1365
- const { repo } = tokenStatus;
1366
- console.log(`\nRefreshing token for ${repo.name} (${repo.linearWorkspaceName || repo.linearWorkspaceId})...`);
1367
- console.log("Opening Linear OAuth flow in your browser...");
1368
- // Use the proxy's OAuth flow with a callback to localhost
1369
- const serverPort = process.env.CYRUS_SERVER_PORT
1370
- ? parseInt(process.env.CYRUS_SERVER_PORT, 10)
1371
- : 3456;
1372
- const callbackUrl = `http://localhost:${serverPort}/callback`;
1373
- const proxyUrl = process.env.PROXY_URL || DEFAULT_PROXY_URL;
1374
- const oauthUrl = `${proxyUrl}/oauth/authorize?callback=${encodeURIComponent(callbackUrl)}`;
1375
- console.log(`\nPlease complete the OAuth flow in your browser.`);
1376
- console.log(`If the browser doesn't open automatically, visit:\n${oauthUrl}\n`);
1377
- // Start a temporary server to receive the OAuth callback
1378
- let tokenReceived = null;
1379
- const server = await new Promise((resolve) => {
1380
- const s = http.createServer((req, res) => {
1381
- if (req.url?.startsWith("/callback")) {
1382
- const url = new URL(req.url, `http://localhost:${serverPort}`);
1383
- tokenReceived = url.searchParams.get("token");
1384
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1385
- res.end(`
1386
- <html>
1387
- <head>
1388
- <meta charset="UTF-8">
1389
- </head>
1390
- <body style="font-family: system-ui; padding: 40px; text-align: center;">
1391
- <h2>āœ… Authorization successful!</h2>
1392
- <p>You can close this window and return to your terminal.</p>
1393
- <script>setTimeout(() => window.close(), 2000);</script>
1394
- </body>
1395
- </html>
1396
- `);
1397
- }
1398
- else {
1399
- res.writeHead(404);
1400
- res.end("Not found");
1401
- }
1402
- });
1403
- s.listen(serverPort, () => {
1404
- console.log("Waiting for OAuth callback...");
1405
- resolve(s);
1406
- });
1407
- });
1408
- await open(oauthUrl);
1409
- // Wait for the token with timeout
1410
- const startTime = Date.now();
1411
- while (!tokenReceived && Date.now() - startTime < 120000) {
1412
- await new Promise((resolve) => setTimeout(resolve, 100));
1413
- }
1414
- server.close();
1415
- const newToken = tokenReceived;
1416
- if (!newToken || !newToken.startsWith("lin_oauth_")) {
1417
- console.error("Invalid token received from OAuth flow");
1418
- continue;
1419
- }
1420
- // Verify the new token
1421
- const verifyResult = await checkLinearToken(newToken);
1422
- if (!verifyResult.valid) {
1423
- console.error(`āŒ New token is invalid: ${verifyResult.error}`);
1424
- continue;
1425
- }
1426
- // Update the config - update ALL repositories that had the same old token
1427
- const oldToken = repo.linearToken;
1428
- let updatedCount = 0;
1429
- for (let i = 0; i < config.repositories.length; i++) {
1430
- const currentRepo = config.repositories[i];
1431
- if (currentRepo && currentRepo.linearToken === oldToken) {
1432
- currentRepo.linearToken = newToken;
1433
- updatedCount++;
1434
- console.log(`āœ… Updated token for ${currentRepo.name}`);
1435
- }
1436
- }
1437
- if (updatedCount > 1) {
1438
- console.log(`\nšŸ“ Updated ${updatedCount} repositories that shared the same token`);
1439
- }
1440
- }
1441
- // Save the updated config
1442
- writeFileSync(configPath, JSON.stringify(config, null, 2));
1443
- console.log("\nāœ… Configuration saved");
1444
- }
1445
- // Command: add-repository
1446
- async function addRepositoryCommand() {
1447
- const app = new EdgeApp(CYRUS_HOME);
1448
- console.log("šŸ“‹ Add New Repository");
1449
- console.log("─".repeat(50));
1450
- console.log();
1451
- try {
1452
- // Load existing configuration
1453
- const config = app.loadEdgeConfig();
1454
- // Check if we have any Linear credentials
1455
- const existingRepos = config.repositories || [];
1456
- let linearCredentials = null;
1457
- if (existingRepos.length > 0) {
1458
- // Try to get credentials from existing repositories
1459
- const repoWithToken = existingRepos.find((r) => r.linearToken);
1460
- if (repoWithToken) {
1461
- linearCredentials = {
1462
- linearToken: repoWithToken.linearToken,
1463
- linearWorkspaceId: repoWithToken.linearWorkspaceId,
1464
- linearWorkspaceName: repoWithToken.linearWorkspaceName || "Your Workspace",
1465
- };
1466
- console.log(`āœ… Using Linear credentials from existing configuration`);
1467
- console.log(` Workspace: ${linearCredentials.linearWorkspaceName}`);
1468
- }
1469
- }
1470
- // If no credentials found, run OAuth flow
1471
- if (!linearCredentials) {
1472
- console.log("šŸ” No Linear credentials found. Starting OAuth flow...");
1473
- // Start OAuth flow using the default proxy URL
1474
- const proxyUrl = process.env.PROXY_URL || DEFAULT_PROXY_URL;
1475
- linearCredentials = await app.startOAuthFlow(proxyUrl);
1476
- if (!linearCredentials) {
1477
- throw new Error("OAuth flow cancelled or failed");
1478
- }
1479
- }
1480
- // Now set up the new repository
1481
- console.log("\nšŸ“‚ Configure New Repository");
1482
- console.log("─".repeat(50));
1483
- const newRepo = await app.setupRepositoryWizard(linearCredentials);
1484
- // Add to existing repositories
1485
- config.repositories = [...existingRepos, newRepo];
1486
- // Save the updated configuration
1487
- app.saveEdgeConfig(config);
1488
- console.log("\nāœ… Repository added successfully!");
1489
- console.log(`šŸ“ Repository: ${newRepo.name}`);
1490
- console.log(`šŸ”— Path: ${newRepo.repositoryPath}`);
1491
- console.log(`🌿 Base branch: ${newRepo.baseBranch}`);
1492
- console.log(`šŸ“‚ Workspace directory: ${newRepo.workspaceBaseDir}`);
1493
- }
1494
- catch (error) {
1495
- console.error("\nāŒ Failed to add repository:", error);
1496
- throw error;
1497
- }
1498
- }
1499
- // Command: set-customer-id
1500
- async function setCustomerIdCommand() {
1501
- const app = new EdgeApp(CYRUS_HOME);
1502
- const configPath = app.getEdgeConfigPath();
1503
- // Get customer ID from command line args
1504
- const customerId = args[1];
1505
- if (!customerId) {
1506
- console.error("Please provide a customer ID");
1507
- console.log("Usage: cyrus set-customer-id cus_XXXXX");
1508
- process.exit(1);
1509
- }
1510
- app.validateCustomerId(customerId);
1511
- try {
1512
- // Check if using default proxy
1513
- const proxyUrl = process.env.PROXY_URL || DEFAULT_PROXY_URL;
1514
- const isUsingDefaultProxy = proxyUrl === DEFAULT_PROXY_URL;
1515
- // Validate subscription for default proxy users
1516
- if (isUsingDefaultProxy) {
1517
- await app.validateAndHandleSubscription(customerId);
1518
- }
1519
- // Load existing config or create new one
1520
- let config = { repositories: [] };
1521
- if (existsSync(configPath)) {
1522
- config = JSON.parse(readFileSync(configPath, "utf-8"));
1523
- }
1524
- // Update customer ID
1525
- config.stripeCustomerId = customerId;
1526
- // Save config
1527
- app.saveEdgeConfig(config);
1528
- console.log("\nāœ… Customer ID saved successfully!");
1529
- console.log("─".repeat(50));
1530
- console.log(`Customer ID: ${customerId}`);
1531
- if (isUsingDefaultProxy) {
1532
- console.log("\nYou now have access to Cyrus Pro features.");
1533
- }
1534
- console.log('Run "cyrus" to start the edge worker.');
1535
- }
1536
- catch (error) {
1537
- console.error("Failed to save customer ID:", error.message);
1538
- process.exit(1);
1539
- }
1540
- }
1541
- // Command: auth
1542
- async function authCommand() {
1543
- // Get auth key from command line arguments
1544
- const authKey = args[1];
1545
- if (!authKey || typeof authKey !== "string" || authKey.trim().length === 0) {
1546
- console.error("āŒ Error: Auth key is required");
1547
- console.log("\nUsage: cyrus auth <auth-key>");
1548
- console.log("\nGet your auth key from: https://www.atcyrus.com/onboarding/auth-cyrus");
1549
- process.exit(1);
1550
- }
1551
- console.log("\nšŸ”‘ Authenticating with Cyrus...");
1552
- console.log("─".repeat(50));
1553
- try {
1554
- // Import ConfigApiClient
1555
- const { ConfigApiClient } = await import("cyrus-cloudflare-tunnel-client");
1556
- // Call the config API to get credentials
1557
- console.log("Validating auth key...");
1558
- const configResponse = await ConfigApiClient.getConfig(authKey);
1559
- if (!ConfigApiClient.isValid(configResponse)) {
1560
- console.error("\nāŒ Authentication failed");
1561
- console.error(configResponse.error || "Invalid response from server");
1562
- console.log("\nPlease verify your auth key is correct.");
1563
- console.log("Get your auth key from: https://www.atcyrus.com/onboarding/auth-cyrus");
1564
- process.exit(1);
1565
- }
1566
- console.log("āœ… Authentication successful!");
1567
- // Ensure CYRUS_HOME directory exists
1568
- if (!existsSync(CYRUS_HOME)) {
1569
- mkdirSync(CYRUS_HOME, { recursive: true });
1570
- }
1571
- // Store tokens in ~/.cyrus/.env file
1572
- const envPath = resolve(CYRUS_HOME, ".env");
1573
- const envContent = `# Cyrus Authentication Credentials
1574
- # Generated on ${new Date().toISOString()}
1575
- CLOUDFLARE_TOKEN=${configResponse.config.cloudflareToken}
1576
- CYRUS_API_KEY=${configResponse.config.apiKey}
1577
- `;
1578
- writeFileSync(envPath, envContent, "utf-8");
1579
- console.log(`āœ… Credentials saved to ${envPath}`);
1580
- // Update config.json with isLegacy: false
1581
- const app = new EdgeApp(CYRUS_HOME);
1582
- const configPath = app.getEdgeConfigPath();
1583
- let config = { repositories: [] };
1584
- if (existsSync(configPath)) {
1585
- try {
1586
- config = JSON.parse(readFileSync(configPath, "utf-8"));
1587
- }
1588
- catch (_e) {
1589
- console.warn("āš ļø Could not read existing config, will create new one");
1590
- }
1591
- }
1592
- // Set isLegacy to false to enable Cloudflare tunnel mode
1593
- config.isLegacy = false;
1594
- app.saveEdgeConfig(config);
1595
- console.log(`āœ… Configuration updated (isLegacy: false)`);
1596
- console.log("\n✨ Setup complete! Starting Cyrus...");
1597
- console.log("─".repeat(50));
1598
- console.log();
1599
- // Start the edge app with the new configuration
1600
- const edgeApp = new EdgeApp(CYRUS_HOME);
1601
- await edgeApp.start();
1602
- }
1603
- catch (error) {
1604
- console.error("\nāŒ Authentication failed:");
1605
- console.error(error.message);
1606
- console.log("\nPlease try again or contact support if the issue persists.");
1607
- process.exit(1);
1608
- }
1609
- }
1610
- // Command: billing
1611
- async function billingCommand() {
1612
- const app = new EdgeApp(CYRUS_HOME);
1613
- const configPath = app.getEdgeConfigPath();
1614
- if (!existsSync(configPath)) {
1615
- console.error('No configuration found. Please run "cyrus" to set up first.');
1616
- process.exit(1);
1617
- }
1618
- const config = JSON.parse(readFileSync(configPath, "utf-8"));
1619
- if (!config.stripeCustomerId) {
1620
- console.log("\nšŸŽÆ No Pro Plan Active");
1621
- console.log("─".repeat(50));
1622
- console.log("You don't have an active subscription.");
1623
- console.log("Please start a free trial at:");
1624
- console.log("\n https://www.atcyrus.com/pricing\n");
1625
- console.log("After signing up, your customer ID will be saved automatically.");
1626
- process.exit(0);
1627
- }
1628
- console.log("\n🌐 Opening Billing Portal...");
1629
- console.log("─".repeat(50));
1630
- try {
1631
- // Open atcyrus.com with the customer ID to handle Stripe redirect
1632
- const billingUrl = `https://www.atcyrus.com/billing/${config.stripeCustomerId}`;
1633
- console.log("āœ… Opening billing portal in browser...");
1634
- console.log(`\nšŸ‘‰ URL: ${billingUrl}\n`);
1635
- // Open the billing portal URL in the default browser
1636
- await open(billingUrl);
1637
- console.log("The billing portal should now be open in your browser.");
1638
- console.log("You can manage your subscription, update payment methods, and download invoices.");
1639
- }
1640
- catch (error) {
1641
- console.error("āŒ Failed to open billing portal:", error.message);
1642
- console.log("\nPlease visit: https://www.atcyrus.com/billing");
1643
- console.log("Customer ID:", config.stripeCustomerId);
1644
- process.exit(1);
1645
- }
1646
- }
1647
- // Parse command
1648
- const command = args[0] || "start";
1649
- // Execute appropriate command
1650
- switch (command) {
1651
- case "check-tokens":
1652
- checkTokensCommand().catch((error) => {
1653
- console.error("Error:", error);
1654
- process.exit(1);
1655
- });
1656
- break;
1657
- case "refresh-token":
1658
- refreshTokenCommand().catch((error) => {
1659
- console.error("Error:", error);
1660
- process.exit(1);
1661
- });
1662
- break;
1663
- case "add-repository":
1664
- addRepositoryCommand().catch((error) => {
1665
- console.error("Error:", error);
1666
- process.exit(1);
1667
- });
1668
- break;
1669
- case "auth":
1670
- authCommand().catch((error) => {
1671
- console.error("Error:", error);
1672
- process.exit(1);
1673
- });
1674
- break;
1675
- case "billing":
1676
- billingCommand().catch((error) => {
1677
- console.error("Error:", error);
1678
- process.exit(1);
1679
- });
1680
- break;
1681
- case "set-customer-id":
1682
- setCustomerIdCommand().catch((error) => {
1683
- console.error("Error:", error);
1684
- process.exit(1);
1685
- });
1686
- break;
1687
- default: {
1688
- // Create and start the app
1689
- const app = new EdgeApp(CYRUS_HOME);
1690
- app.start().catch((error) => {
1691
- console.error("Fatal error:", error);
1692
- process.exit(1);
1693
- });
1694
- break;
1695
- }
1696
- }
1697
- //# sourceMappingURL=app.js.map