cyrus-ai 0.1.36 ā 0.1.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -0
- package/dist/app.js +499 -225
- package/dist/app.js.map +1 -1
- package/dist/vitest.config.d.ts.map +1 -1
- package/dist/vitest.config.js +7 -7
- package/dist/vitest.config.js.map +1 -1
- package/package.json +6 -6
package/dist/app.js
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import
|
|
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 { EdgeWorker, SharedApplicationServer, } from "cyrus-edge-worker";
|
|
8
|
+
import dotenv from "dotenv";
|
|
9
|
+
import open from "open";
|
|
10
10
|
// Parse command line arguments
|
|
11
11
|
const args = process.argv.slice(2);
|
|
12
|
-
const envFileArg = args.find(arg => arg.startsWith(
|
|
12
|
+
const envFileArg = args.find((arg) => arg.startsWith("--env-file="));
|
|
13
13
|
// Note: __dirname removed since version is now hardcoded
|
|
14
14
|
// Handle --version argument
|
|
15
|
-
if (args.includes(
|
|
16
|
-
console.log(
|
|
15
|
+
if (args.includes("--version")) {
|
|
16
|
+
console.log("0.1.37");
|
|
17
17
|
process.exit(0);
|
|
18
18
|
}
|
|
19
19
|
// Handle --help argument
|
|
20
|
-
if (args.includes(
|
|
20
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
21
21
|
console.log(`
|
|
22
22
|
cyrus - AI-powered Linear issue automation using Claude
|
|
23
23
|
|
|
@@ -27,6 +27,9 @@ Commands:
|
|
|
27
27
|
start Start the edge worker (default)
|
|
28
28
|
check-tokens Check the status of all Linear tokens
|
|
29
29
|
refresh-token Refresh a specific Linear token
|
|
30
|
+
add-repository Add a new repository configuration
|
|
31
|
+
billing Open Stripe billing portal (Pro plan only)
|
|
32
|
+
set-customer-id Set your Stripe customer ID
|
|
30
33
|
|
|
31
34
|
Options:
|
|
32
35
|
--version Show version number
|
|
@@ -37,12 +40,13 @@ Examples:
|
|
|
37
40
|
cyrus Start the edge worker
|
|
38
41
|
cyrus check-tokens Check all Linear token statuses
|
|
39
42
|
cyrus refresh-token Interactive token refresh
|
|
43
|
+
cyrus add-repository Add a new repository interactively
|
|
40
44
|
`);
|
|
41
45
|
process.exit(0);
|
|
42
46
|
}
|
|
43
47
|
// Load environment variables only if --env-file is specified
|
|
44
48
|
if (envFileArg) {
|
|
45
|
-
const envFile = envFileArg.split(
|
|
49
|
+
const envFile = envFileArg.split("=")[1];
|
|
46
50
|
if (envFile) {
|
|
47
51
|
dotenv.config({ path: envFile });
|
|
48
52
|
}
|
|
@@ -57,13 +61,13 @@ class EdgeApp {
|
|
|
57
61
|
* Get the edge configuration file path
|
|
58
62
|
*/
|
|
59
63
|
getEdgeConfigPath() {
|
|
60
|
-
return resolve(homedir(),
|
|
64
|
+
return resolve(homedir(), ".cyrus", "config.json");
|
|
61
65
|
}
|
|
62
66
|
/**
|
|
63
67
|
* Get the legacy edge configuration file path (for migration)
|
|
64
68
|
*/
|
|
65
69
|
getLegacyEdgeConfigPath() {
|
|
66
|
-
return resolve(process.cwd(),
|
|
70
|
+
return resolve(process.cwd(), ".edge-config.json");
|
|
67
71
|
}
|
|
68
72
|
/**
|
|
69
73
|
* Migrate configuration from legacy location if needed
|
|
@@ -106,15 +110,15 @@ class EdgeApp {
|
|
|
106
110
|
let config = { repositories: [] };
|
|
107
111
|
if (existsSync(edgeConfigPath)) {
|
|
108
112
|
try {
|
|
109
|
-
config = JSON.parse(readFileSync(edgeConfigPath,
|
|
113
|
+
config = JSON.parse(readFileSync(edgeConfigPath, "utf-8"));
|
|
110
114
|
}
|
|
111
115
|
catch (e) {
|
|
112
|
-
console.error(
|
|
116
|
+
console.error("Failed to load edge config:", e.message);
|
|
113
117
|
}
|
|
114
118
|
}
|
|
115
119
|
// Strip promptTemplatePath from all repositories to ensure built-in template is used
|
|
116
120
|
if (config.repositories) {
|
|
117
|
-
config.repositories = config.repositories.map(repo => {
|
|
121
|
+
config.repositories = config.repositories.map((repo) => {
|
|
118
122
|
const { promptTemplatePath, ...repoWithoutTemplate } = repo;
|
|
119
123
|
if (promptTemplatePath) {
|
|
120
124
|
console.log(`Ignoring custom prompt template for repository: ${repo.name} (using built-in template)`);
|
|
@@ -142,66 +146,81 @@ class EdgeApp {
|
|
|
142
146
|
async setupRepositoryWizard(linearCredentials) {
|
|
143
147
|
const rl = readline.createInterface({
|
|
144
148
|
input: process.stdin,
|
|
145
|
-
output: process.stdout
|
|
149
|
+
output: process.stdout,
|
|
146
150
|
});
|
|
147
151
|
const question = (prompt) => new Promise((resolve) => {
|
|
148
152
|
rl.question(prompt, resolve);
|
|
149
153
|
});
|
|
150
|
-
console.log(
|
|
151
|
-
console.log(
|
|
154
|
+
console.log("\nš Repository Setup");
|
|
155
|
+
console.log("ā".repeat(50));
|
|
152
156
|
try {
|
|
153
157
|
// Ask for repository details
|
|
154
|
-
const repositoryPath = await question(`Repository path (default: ${process.cwd()}): `) ||
|
|
155
|
-
|
|
156
|
-
const
|
|
158
|
+
const repositoryPath = (await question(`Repository path (default: ${process.cwd()}): `)) ||
|
|
159
|
+
process.cwd();
|
|
160
|
+
const repositoryName = (await question(`Repository name (default: ${basename(repositoryPath)}): `)) || basename(repositoryPath);
|
|
161
|
+
const baseBranch = (await question("Base branch (default: main): ")) || "main";
|
|
157
162
|
// Create a path-safe version of the repository name for namespacing
|
|
158
|
-
const repoNameSafe = repositoryName
|
|
159
|
-
|
|
160
|
-
|
|
163
|
+
const repoNameSafe = repositoryName
|
|
164
|
+
.replace(/[^a-zA-Z0-9-_]/g, "-")
|
|
165
|
+
.toLowerCase();
|
|
166
|
+
const defaultWorkspaceDir = resolve(homedir(), ".cyrus", "workspaces", repoNameSafe);
|
|
167
|
+
const workspaceBaseDir = (await question(`Workspace directory (default: ${defaultWorkspaceDir}): `)) || defaultWorkspaceDir;
|
|
161
168
|
// Note: Prompt template is now hardcoded - no longer configurable
|
|
162
169
|
// Ask for MCP configuration
|
|
163
|
-
console.log(
|
|
164
|
-
console.log(
|
|
165
|
-
console.log(
|
|
166
|
-
console.log(
|
|
167
|
-
console.log(
|
|
170
|
+
console.log("\nš§ MCP (Model Context Protocol) Configuration");
|
|
171
|
+
console.log("MCP allows Claude to access external tools and data sources.");
|
|
172
|
+
console.log("Examples: filesystem access, database connections, API integrations");
|
|
173
|
+
console.log("See: https://docs.anthropic.com/en/docs/claude-code/mcp");
|
|
174
|
+
console.log("");
|
|
168
175
|
const mcpConfigInput = await question('MCP config file path (optional, format: {"mcpServers": {...}}, e.g., ./mcp-config.json): ');
|
|
169
176
|
const mcpConfigPath = mcpConfigInput.trim() || undefined;
|
|
170
177
|
// Ask for allowed tools configuration
|
|
171
|
-
console.log(
|
|
172
|
-
console.log(
|
|
173
|
-
console.log(
|
|
174
|
-
console.log(
|
|
178
|
+
console.log("\nš§ Tool Configuration");
|
|
179
|
+
console.log("Available tools: Read(**),Edit(**),Bash,Task,WebFetch,WebSearch,TodoRead,TodoWrite,NotebookRead,NotebookEdit,Batch");
|
|
180
|
+
console.log("");
|
|
181
|
+
console.log("ā ļø SECURITY NOTE: Bash tool requires special configuration for safety:");
|
|
175
182
|
console.log(' ⢠Use "Bash" for full access (not recommended in production)');
|
|
176
183
|
console.log(' ⢠Use "Bash(npm:*)" to restrict to npm commands only');
|
|
177
184
|
console.log(' ⢠Use "Bash(git:*)" to restrict to git commands only');
|
|
178
|
-
console.log(
|
|
179
|
-
console.log(
|
|
180
|
-
console.log(
|
|
181
|
-
const allowedToolsInput = await question(
|
|
182
|
-
const allowedTools = allowedToolsInput
|
|
185
|
+
console.log(" ⢠See: https://docs.anthropic.com/en/docs/claude-code/settings#permissions");
|
|
186
|
+
console.log("");
|
|
187
|
+
console.log("Default: All tools except Bash (leave blank for all non-Bash tools)");
|
|
188
|
+
const allowedToolsInput = await question("Allowed tools (comma-separated, default: all except Bash): ");
|
|
189
|
+
const allowedTools = allowedToolsInput
|
|
190
|
+
? allowedToolsInput.split(",").map((t) => t.trim())
|
|
191
|
+
: undefined;
|
|
183
192
|
// Ask for team keys configuration
|
|
184
|
-
console.log(
|
|
185
|
-
console.log(
|
|
186
|
-
console.log(
|
|
187
|
-
console.log(
|
|
188
|
-
const teamKeysInput = await question(
|
|
189
|
-
const teamKeys = teamKeysInput
|
|
193
|
+
console.log("\nš·ļø Team-Based Routing (Optional)");
|
|
194
|
+
console.log("Configure specific Linear team keys to route issues to this repository.");
|
|
195
|
+
console.log("Example: CEE,FRONT,BACK for teams with those prefixes");
|
|
196
|
+
console.log("Leave blank to receive all issues from the workspace.");
|
|
197
|
+
const teamKeysInput = await question("Team keys (comma-separated, optional): ");
|
|
198
|
+
const teamKeys = teamKeysInput
|
|
199
|
+
? teamKeysInput.split(",").map((t) => t.trim().toUpperCase())
|
|
200
|
+
: undefined;
|
|
190
201
|
// Ask for label-based system prompt configuration
|
|
191
|
-
console.log(
|
|
192
|
-
console.log(
|
|
193
|
-
console.log(
|
|
194
|
-
console.log(
|
|
195
|
-
console.log(
|
|
196
|
-
console.log(
|
|
202
|
+
console.log("\nšÆ Label-Based System Prompts (Optional)");
|
|
203
|
+
console.log("Cyrus can use different strategies based on Linear issue labels.");
|
|
204
|
+
console.log("Configure which labels trigger each specialized mode:");
|
|
205
|
+
console.log("⢠Debugger mode: Focuses on systematic problem investigation");
|
|
206
|
+
console.log("⢠Builder mode: Emphasizes feature implementation and code quality");
|
|
207
|
+
console.log("⢠Scoper mode: Helps analyze requirements and create technical plans");
|
|
197
208
|
const debuggerLabelsInput = await question('Labels for debugger mode (comma-separated, e.g., "Bug"): ');
|
|
198
209
|
const builderLabelsInput = await question('Labels for builder mode (comma-separated, e.g., "Feature,Improvement"): ');
|
|
199
210
|
const scoperLabelsInput = await question('Labels for scoper mode (comma-separated, e.g., "PRD"): ');
|
|
200
|
-
const labelPrompts =
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
211
|
+
const labelPrompts = debuggerLabelsInput || builderLabelsInput || scoperLabelsInput
|
|
212
|
+
? {
|
|
213
|
+
...(debuggerLabelsInput && {
|
|
214
|
+
debugger: debuggerLabelsInput.split(",").map((l) => l.trim()),
|
|
215
|
+
}),
|
|
216
|
+
...(builderLabelsInput && {
|
|
217
|
+
builder: builderLabelsInput.split(",").map((l) => l.trim()),
|
|
218
|
+
}),
|
|
219
|
+
...(scoperLabelsInput && {
|
|
220
|
+
scoper: scoperLabelsInput.split(",").map((l) => l.trim()),
|
|
221
|
+
}),
|
|
222
|
+
}
|
|
223
|
+
: undefined;
|
|
205
224
|
rl.close();
|
|
206
225
|
// Create repository configuration
|
|
207
226
|
const repository = {
|
|
@@ -216,7 +235,7 @@ class EdgeApp {
|
|
|
216
235
|
...(allowedTools && { allowedTools }),
|
|
217
236
|
...(mcpConfigPath && { mcpConfigPath: resolve(mcpConfigPath) }),
|
|
218
237
|
...(teamKeys && { teamKeys }),
|
|
219
|
-
...(labelPrompts && { labelPrompts })
|
|
238
|
+
...(labelPrompts && { labelPrompts }),
|
|
220
239
|
};
|
|
221
240
|
return repository;
|
|
222
241
|
}
|
|
@@ -246,7 +265,9 @@ class EdgeApp {
|
|
|
246
265
|
}
|
|
247
266
|
else {
|
|
248
267
|
// Create temporary SharedApplicationServer for OAuth flow during initial setup
|
|
249
|
-
const serverPort = process.env.CYRUS_SERVER_PORT
|
|
268
|
+
const serverPort = process.env.CYRUS_SERVER_PORT
|
|
269
|
+
? parseInt(process.env.CYRUS_SERVER_PORT, 10)
|
|
270
|
+
: 3456;
|
|
250
271
|
const tempServer = new SharedApplicationServer(serverPort);
|
|
251
272
|
try {
|
|
252
273
|
// Start the server
|
|
@@ -267,7 +288,7 @@ class EdgeApp {
|
|
|
267
288
|
return {
|
|
268
289
|
linearToken: result.linearToken,
|
|
269
290
|
linearWorkspaceId: result.linearWorkspaceId,
|
|
270
|
-
linearWorkspaceName: result.linearWorkspaceName
|
|
291
|
+
linearWorkspaceName: result.linearWorkspaceName,
|
|
271
292
|
};
|
|
272
293
|
}
|
|
273
294
|
finally {
|
|
@@ -300,7 +321,7 @@ class EdgeApp {
|
|
|
300
321
|
console.log(``);
|
|
301
322
|
const rl = readline.createInterface({
|
|
302
323
|
input: process.stdin,
|
|
303
|
-
output: process.stdout
|
|
324
|
+
output: process.stdout,
|
|
304
325
|
});
|
|
305
326
|
return new Promise((resolve) => {
|
|
306
327
|
rl.question(`Enter your ngrok auth token (or press Enter to skip): `, async (token) => {
|
|
@@ -327,7 +348,7 @@ class EdgeApp {
|
|
|
327
348
|
/**
|
|
328
349
|
* Start the EdgeWorker with given configuration
|
|
329
350
|
*/
|
|
330
|
-
async startEdgeWorker({ proxyUrl, repositories }) {
|
|
351
|
+
async startEdgeWorker({ proxyUrl, repositories, }) {
|
|
331
352
|
// Get ngrok auth token (prompt if needed and not external host)
|
|
332
353
|
let ngrokAuthToken;
|
|
333
354
|
if (process.env.CYRUS_HOST_EXTERNAL !== "true") {
|
|
@@ -338,13 +359,15 @@ class EdgeApp {
|
|
|
338
359
|
const config = {
|
|
339
360
|
proxyUrl,
|
|
340
361
|
repositories,
|
|
341
|
-
defaultAllowedTools: process.env.ALLOWED_TOOLS?.split(",").map(t => t.trim()) || [],
|
|
362
|
+
defaultAllowedTools: process.env.ALLOWED_TOOLS?.split(",").map((t) => t.trim()) || [],
|
|
342
363
|
webhookBaseUrl: process.env.CYRUS_BASE_URL,
|
|
343
|
-
serverPort: process.env.CYRUS_SERVER_PORT
|
|
364
|
+
serverPort: process.env.CYRUS_SERVER_PORT
|
|
365
|
+
? parseInt(process.env.CYRUS_SERVER_PORT, 10)
|
|
366
|
+
: 3456,
|
|
344
367
|
serverHost: process.env.CYRUS_HOST_EXTERNAL === "true" ? "0.0.0.0" : "localhost",
|
|
345
368
|
ngrokAuthToken,
|
|
346
369
|
features: {
|
|
347
|
-
enableContinuation: true
|
|
370
|
+
enableContinuation: true,
|
|
348
371
|
},
|
|
349
372
|
handlers: {
|
|
350
373
|
createWorkspace: async (issue, repository) => {
|
|
@@ -354,7 +377,7 @@ class EdgeApp {
|
|
|
354
377
|
const linearCredentials = {
|
|
355
378
|
linearToken: token,
|
|
356
379
|
linearWorkspaceId: workspaceId,
|
|
357
|
-
linearWorkspaceName: workspaceName
|
|
380
|
+
linearWorkspaceName: workspaceName,
|
|
358
381
|
};
|
|
359
382
|
// Handle OAuth completion for repository setup
|
|
360
383
|
if (this.edgeWorker) {
|
|
@@ -363,33 +386,36 @@ class EdgeApp {
|
|
|
363
386
|
try {
|
|
364
387
|
const newRepo = await this.setupRepositoryWizard(linearCredentials);
|
|
365
388
|
// Add to existing repositories
|
|
366
|
-
|
|
389
|
+
const edgeConfig = this.loadEdgeConfig();
|
|
367
390
|
console.log(`š Current config has ${edgeConfig.repositories?.length || 0} repositories`);
|
|
368
|
-
edgeConfig.repositories = [
|
|
391
|
+
edgeConfig.repositories = [
|
|
392
|
+
...(edgeConfig.repositories || []),
|
|
393
|
+
newRepo,
|
|
394
|
+
];
|
|
369
395
|
console.log(`š Adding repository "${newRepo.name}", new total: ${edgeConfig.repositories.length}`);
|
|
370
396
|
this.saveEdgeConfig(edgeConfig);
|
|
371
|
-
console.log(
|
|
372
|
-
console.log(
|
|
373
|
-
console.log(
|
|
397
|
+
console.log("\nā
Repository configured successfully!");
|
|
398
|
+
console.log("š ~/.cyrus/config.json file has been updated with your new repository configuration.");
|
|
399
|
+
console.log("š” You can edit this file and restart Cyrus at any time to modify settings.");
|
|
374
400
|
// Restart edge worker with new config
|
|
375
401
|
await this.edgeWorker.stop();
|
|
376
402
|
this.edgeWorker = null;
|
|
377
403
|
// Give a small delay to ensure file is written
|
|
378
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
404
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
379
405
|
// Reload configuration and restart worker without going through setup
|
|
380
406
|
const updatedConfig = this.loadEdgeConfig();
|
|
381
407
|
console.log(`\nš Reloading with ${updatedConfig.repositories?.length || 0} repositories from config file`);
|
|
382
408
|
return this.startEdgeWorker({
|
|
383
409
|
proxyUrl,
|
|
384
|
-
repositories: updatedConfig.repositories || []
|
|
410
|
+
repositories: updatedConfig.repositories || [],
|
|
385
411
|
});
|
|
386
412
|
}
|
|
387
413
|
catch (error) {
|
|
388
|
-
console.error(
|
|
414
|
+
console.error("\nā Repository setup failed:", error.message);
|
|
389
415
|
}
|
|
390
416
|
}
|
|
391
|
-
}
|
|
392
|
-
}
|
|
417
|
+
},
|
|
418
|
+
},
|
|
393
419
|
};
|
|
394
420
|
// Create and start EdgeWorker
|
|
395
421
|
this.edgeWorker = new EdgeWorker(config);
|
|
@@ -397,10 +423,10 @@ class EdgeApp {
|
|
|
397
423
|
this.setupEventHandlers();
|
|
398
424
|
// Start the worker
|
|
399
425
|
await this.edgeWorker.start();
|
|
400
|
-
console.log(
|
|
426
|
+
console.log("\nā
Edge worker started successfully");
|
|
401
427
|
console.log(`Configured proxy URL: ${config.proxyUrl}`);
|
|
402
428
|
console.log(`Managing ${repositories.length} repositories:`);
|
|
403
|
-
repositories.forEach(repo => {
|
|
429
|
+
repositories.forEach((repo) => {
|
|
404
430
|
console.log(` - ${repo.name} (${repo.repositoryPath})`);
|
|
405
431
|
});
|
|
406
432
|
}
|
|
@@ -410,27 +436,101 @@ class EdgeApp {
|
|
|
410
436
|
async start() {
|
|
411
437
|
try {
|
|
412
438
|
// Set proxy URL with default
|
|
413
|
-
const proxyUrl = process.env.PROXY_URL ||
|
|
439
|
+
const proxyUrl = process.env.PROXY_URL || "https://cyrus-proxy.ceedar.workers.dev";
|
|
414
440
|
// No need to validate Claude CLI - using Claude TypeScript SDK now
|
|
415
441
|
// Load edge configuration
|
|
416
442
|
let edgeConfig = this.loadEdgeConfig();
|
|
417
443
|
let repositories = edgeConfig.repositories || [];
|
|
444
|
+
// Check if using default proxy URL without a customer ID
|
|
445
|
+
const defaultProxyUrl = "https://cyrus-proxy.ceedar.workers.dev";
|
|
446
|
+
const isUsingDefaultProxy = proxyUrl === defaultProxyUrl;
|
|
447
|
+
const hasCustomerId = !!edgeConfig.stripeCustomerId;
|
|
448
|
+
if (isUsingDefaultProxy && !hasCustomerId) {
|
|
449
|
+
console.log("\nšÆ Pro Plan Required");
|
|
450
|
+
console.log("ā".repeat(50));
|
|
451
|
+
console.log("You are using the default Cyrus proxy URL.");
|
|
452
|
+
console.log("\nWith Cyrus Pro you get:");
|
|
453
|
+
console.log("⢠No-hassle configuration");
|
|
454
|
+
console.log("⢠Priority support");
|
|
455
|
+
console.log("⢠Help fund product development");
|
|
456
|
+
console.log("\nChoose an option:");
|
|
457
|
+
console.log("1. Start a free trial");
|
|
458
|
+
console.log("2. I have a customer ID to enter");
|
|
459
|
+
console.log("3. Setup your own proxy (advanced)");
|
|
460
|
+
console.log("4. Exit");
|
|
461
|
+
const rl = readline.createInterface({
|
|
462
|
+
input: process.stdin,
|
|
463
|
+
output: process.stdout,
|
|
464
|
+
});
|
|
465
|
+
const choice = await new Promise((resolve) => {
|
|
466
|
+
rl.question("\nYour choice (1-4): ", (answer) => {
|
|
467
|
+
resolve(answer.trim());
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
if (choice === "1") {
|
|
471
|
+
console.log("\nš Opening your browser to start a free trial...");
|
|
472
|
+
console.log("Visit: https://www.atcyrus.com/pricing");
|
|
473
|
+
await open("https://www.atcyrus.com/pricing");
|
|
474
|
+
rl.close();
|
|
475
|
+
process.exit(0);
|
|
476
|
+
}
|
|
477
|
+
else if (choice === "2") {
|
|
478
|
+
console.log("\nš After completing payment, you'll see your customer ID on the success page.");
|
|
479
|
+
console.log('It starts with "cus_" and can be copied from the website.');
|
|
480
|
+
const customerId = await new Promise((resolve) => {
|
|
481
|
+
rl.question("\nPaste your customer ID here: ", (answer) => {
|
|
482
|
+
resolve(answer.trim());
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
rl.close();
|
|
486
|
+
if (!customerId.startsWith("cus_")) {
|
|
487
|
+
console.error("\nā Invalid customer ID format");
|
|
488
|
+
console.log('Customer IDs should start with "cus_"');
|
|
489
|
+
process.exit(1);
|
|
490
|
+
}
|
|
491
|
+
// Save the customer ID
|
|
492
|
+
edgeConfig.stripeCustomerId = customerId;
|
|
493
|
+
this.saveEdgeConfig(edgeConfig);
|
|
494
|
+
console.log("\nā
Customer ID saved successfully!");
|
|
495
|
+
console.log("Continuing with startup...\n");
|
|
496
|
+
// Reload config to include the new customer ID
|
|
497
|
+
edgeConfig = this.loadEdgeConfig();
|
|
498
|
+
}
|
|
499
|
+
else if (choice === "3") {
|
|
500
|
+
console.log("\nš§ Self-Hosted Proxy Setup");
|
|
501
|
+
console.log("ā".repeat(50));
|
|
502
|
+
console.log("Configure your own Linear app and proxy to have full control over your stack.");
|
|
503
|
+
console.log("\nDocumentation:");
|
|
504
|
+
console.log("⢠Linear OAuth setup: https://linear.app/developers/agents");
|
|
505
|
+
console.log("⢠Proxy implementation: https://github.com/ceedaragents/cyrus/tree/main/apps/proxy-worker");
|
|
506
|
+
console.log("\nOnce deployed, set the PROXY_URL environment variable:");
|
|
507
|
+
console.log("export PROXY_URL=https://your-proxy-url.com");
|
|
508
|
+
rl.close();
|
|
509
|
+
process.exit(0);
|
|
510
|
+
}
|
|
511
|
+
else {
|
|
512
|
+
rl.close();
|
|
513
|
+
console.log("\nExiting...");
|
|
514
|
+
process.exit(0);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
418
517
|
// Check if we need to set up
|
|
419
518
|
const needsSetup = repositories.length === 0;
|
|
420
|
-
const hasLinearCredentials = repositories.some(r => r.linearToken) ||
|
|
519
|
+
const hasLinearCredentials = repositories.some((r) => r.linearToken) ||
|
|
520
|
+
process.env.LINEAR_OAUTH_TOKEN;
|
|
421
521
|
if (needsSetup) {
|
|
422
|
-
console.log(
|
|
522
|
+
console.log("š Welcome to Cyrus Edge Worker!");
|
|
423
523
|
// Check if they want to use existing credentials or add new workspace
|
|
424
524
|
let linearCredentials = null;
|
|
425
525
|
if (hasLinearCredentials) {
|
|
426
526
|
// Show available workspaces from existing repos
|
|
427
527
|
const workspaces = new Map();
|
|
428
|
-
for (const repo of
|
|
528
|
+
for (const repo of edgeConfig.repositories || []) {
|
|
429
529
|
if (!workspaces.has(repo.linearWorkspaceId)) {
|
|
430
530
|
workspaces.set(repo.linearWorkspaceId, {
|
|
431
531
|
id: repo.linearWorkspaceId,
|
|
432
|
-
name:
|
|
433
|
-
token: repo.linearToken
|
|
532
|
+
name: "Unknown Workspace",
|
|
533
|
+
token: repo.linearToken,
|
|
434
534
|
});
|
|
435
535
|
}
|
|
436
536
|
}
|
|
@@ -441,24 +541,24 @@ class EdgeApp {
|
|
|
441
541
|
linearCredentials = {
|
|
442
542
|
linearToken: ws.token,
|
|
443
543
|
linearWorkspaceId: ws.id,
|
|
444
|
-
linearWorkspaceName: ws.name
|
|
544
|
+
linearWorkspaceName: ws.name,
|
|
445
545
|
};
|
|
446
546
|
console.log(`\nš Using Linear workspace: ${linearCredentials.linearWorkspaceName}`);
|
|
447
547
|
}
|
|
448
548
|
}
|
|
449
549
|
else if (workspaces.size > 1) {
|
|
450
550
|
// Multiple workspaces, let user choose
|
|
451
|
-
console.log(
|
|
551
|
+
console.log("\nš Available Linear workspaces:");
|
|
452
552
|
const workspaceList = Array.from(workspaces.values());
|
|
453
553
|
workspaceList.forEach((ws, i) => {
|
|
454
554
|
console.log(`${i + 1}. ${ws.name}`);
|
|
455
555
|
});
|
|
456
556
|
const rl = readline.createInterface({
|
|
457
557
|
input: process.stdin,
|
|
458
|
-
output: process.stdout
|
|
558
|
+
output: process.stdout,
|
|
459
559
|
});
|
|
460
|
-
const choice = await new Promise(resolve => {
|
|
461
|
-
rl.question(
|
|
560
|
+
const choice = await new Promise((resolve) => {
|
|
561
|
+
rl.question("\nSelect workspace (number) or press Enter for new: ", resolve);
|
|
462
562
|
});
|
|
463
563
|
rl.close();
|
|
464
564
|
const index = parseInt(choice) - 1;
|
|
@@ -468,7 +568,7 @@ class EdgeApp {
|
|
|
468
568
|
linearCredentials = {
|
|
469
569
|
linearToken: ws.token,
|
|
470
570
|
linearWorkspaceId: ws.id,
|
|
471
|
-
linearWorkspaceName: ws.name
|
|
571
|
+
linearWorkspaceName: ws.name,
|
|
472
572
|
};
|
|
473
573
|
console.log(`Using workspace: ${linearCredentials.linearWorkspaceName}`);
|
|
474
574
|
}
|
|
@@ -482,56 +582,56 @@ class EdgeApp {
|
|
|
482
582
|
// Use env vars
|
|
483
583
|
linearCredentials = {
|
|
484
584
|
linearToken: process.env.LINEAR_OAUTH_TOKEN,
|
|
485
|
-
linearWorkspaceId: process.env.LINEAR_WORKSPACE_ID ||
|
|
486
|
-
linearWorkspaceName:
|
|
585
|
+
linearWorkspaceId: process.env.LINEAR_WORKSPACE_ID || "unknown",
|
|
586
|
+
linearWorkspaceName: "Your Workspace",
|
|
487
587
|
};
|
|
488
588
|
}
|
|
489
589
|
if (linearCredentials) {
|
|
490
|
-
console.log(
|
|
590
|
+
console.log("(OAuth server will start with EdgeWorker to connect additional workspaces)");
|
|
491
591
|
}
|
|
492
592
|
}
|
|
493
593
|
else {
|
|
494
594
|
// Get new Linear credentials
|
|
495
|
-
console.log(
|
|
496
|
-
console.log(
|
|
595
|
+
console.log("\nš Step 1: Connect to Linear");
|
|
596
|
+
console.log("ā".repeat(50));
|
|
497
597
|
try {
|
|
498
598
|
linearCredentials = await this.startOAuthFlow(proxyUrl);
|
|
499
|
-
console.log(
|
|
599
|
+
console.log("\nā
Linear connected successfully!");
|
|
500
600
|
}
|
|
501
601
|
catch (error) {
|
|
502
|
-
console.error(
|
|
503
|
-
console.log(
|
|
504
|
-
console.log(
|
|
505
|
-
console.log(
|
|
506
|
-
console.log(
|
|
602
|
+
console.error("\nā OAuth flow failed:", error.message);
|
|
603
|
+
console.log("\nAlternatively, you can:");
|
|
604
|
+
console.log("1. Visit", `${proxyUrl}/oauth/authorize`, "in your browser");
|
|
605
|
+
console.log("2. Copy the token after authorization");
|
|
606
|
+
console.log("3. Add it to your .env.cyrus file as LINEAR_OAUTH_TOKEN");
|
|
507
607
|
process.exit(1);
|
|
508
608
|
}
|
|
509
609
|
}
|
|
510
610
|
if (!linearCredentials) {
|
|
511
|
-
console.error(
|
|
611
|
+
console.error("ā No Linear credentials available");
|
|
512
612
|
process.exit(1);
|
|
513
613
|
}
|
|
514
614
|
// Now set up repository
|
|
515
|
-
console.log(
|
|
516
|
-
console.log(
|
|
615
|
+
console.log("\nš Step 2: Configure Repository");
|
|
616
|
+
console.log("ā".repeat(50));
|
|
517
617
|
try {
|
|
518
618
|
const newRepo = await this.setupRepositoryWizard(linearCredentials);
|
|
519
619
|
// Add to repositories
|
|
520
620
|
repositories = [...(edgeConfig.repositories || []), newRepo];
|
|
521
621
|
edgeConfig.repositories = repositories;
|
|
522
622
|
this.saveEdgeConfig(edgeConfig);
|
|
523
|
-
console.log(
|
|
524
|
-
console.log(
|
|
525
|
-
console.log(
|
|
623
|
+
console.log("\nā
Repository configured successfully!");
|
|
624
|
+
console.log("š ~/.cyrus/config.json file has been updated with your repository configuration.");
|
|
625
|
+
console.log("š” You can edit this file and restart Cyrus at any time to modify settings.");
|
|
526
626
|
// Ask if they want to add another
|
|
527
627
|
const rl = readline.createInterface({
|
|
528
628
|
input: process.stdin,
|
|
529
|
-
output: process.stdout
|
|
629
|
+
output: process.stdout,
|
|
530
630
|
});
|
|
531
|
-
const addAnother = await new Promise(resolve => {
|
|
532
|
-
rl.question(
|
|
631
|
+
const addAnother = await new Promise((resolve) => {
|
|
632
|
+
rl.question("\nAdd another repository? (y/N): ", (answer) => {
|
|
533
633
|
rl.close();
|
|
534
|
-
resolve(answer.toLowerCase() ===
|
|
634
|
+
resolve(answer.toLowerCase() === "y");
|
|
535
635
|
});
|
|
536
636
|
});
|
|
537
637
|
if (addAnother) {
|
|
@@ -540,61 +640,76 @@ class EdgeApp {
|
|
|
540
640
|
}
|
|
541
641
|
}
|
|
542
642
|
catch (error) {
|
|
543
|
-
console.error(
|
|
643
|
+
console.error("\nā Repository setup failed:", error.message);
|
|
544
644
|
process.exit(1);
|
|
545
645
|
}
|
|
546
646
|
}
|
|
547
647
|
// Validate we have repositories
|
|
548
648
|
if (repositories.length === 0) {
|
|
549
|
-
console.error(
|
|
550
|
-
console.log(
|
|
649
|
+
console.error("ā No repositories configured");
|
|
650
|
+
console.log("\nUse the authorization link above to configure your first repository.");
|
|
551
651
|
process.exit(1);
|
|
552
652
|
}
|
|
553
653
|
// Start the edge worker
|
|
554
654
|
await this.startEdgeWorker({ proxyUrl, repositories });
|
|
655
|
+
// Display plan status
|
|
656
|
+
const defaultProxyUrlForStatus = "https://cyrus-proxy.ceedar.workers.dev";
|
|
657
|
+
const isUsingDefaultProxyForStatus = proxyUrl === defaultProxyUrlForStatus;
|
|
658
|
+
const hasCustomerIdForStatus = !!edgeConfig.stripeCustomerId;
|
|
659
|
+
console.log(`\n${"ā".repeat(70)}`);
|
|
660
|
+
if (isUsingDefaultProxyForStatus && hasCustomerIdForStatus) {
|
|
661
|
+
console.log("š Plan: Cyrus Pro");
|
|
662
|
+
console.log(`š Customer ID: ${edgeConfig.stripeCustomerId}`);
|
|
663
|
+
console.log('š³ Manage subscription: Run "cyrus billing"');
|
|
664
|
+
}
|
|
665
|
+
else if (!isUsingDefaultProxyForStatus) {
|
|
666
|
+
console.log("š ļø Plan: Community (Self-hosted proxy)");
|
|
667
|
+
console.log(`š Proxy URL: ${proxyUrl}`);
|
|
668
|
+
}
|
|
669
|
+
console.log("ā".repeat(70));
|
|
555
670
|
// Display OAuth information after EdgeWorker is started
|
|
556
671
|
const serverPort = this.edgeWorker?.getServerPort() || 3456;
|
|
557
672
|
const oauthCallbackBaseUrl = process.env.CYRUS_BASE_URL || `http://localhost:${serverPort}`;
|
|
558
673
|
console.log(`\nš OAuth server running on port ${serverPort}`);
|
|
559
674
|
console.log(`š To authorize Linear (new workspace or re-auth):`);
|
|
560
675
|
console.log(` ${proxyUrl}/oauth/authorize?callback=${oauthCallbackBaseUrl}/callback`);
|
|
561
|
-
console.log(
|
|
676
|
+
console.log("ā".repeat(70));
|
|
562
677
|
// Handle graceful shutdown
|
|
563
|
-
process.on(
|
|
564
|
-
process.on(
|
|
678
|
+
process.on("SIGINT", () => this.shutdown());
|
|
679
|
+
process.on("SIGTERM", () => this.shutdown());
|
|
565
680
|
// Handle uncaught exceptions and unhandled promise rejections
|
|
566
|
-
process.on(
|
|
567
|
-
console.error(
|
|
568
|
-
console.error(
|
|
569
|
-
console.error(
|
|
570
|
-
console.error(
|
|
681
|
+
process.on("uncaughtException", (error) => {
|
|
682
|
+
console.error("šØ Uncaught Exception:", error.message);
|
|
683
|
+
console.error("Error type:", error.constructor.name);
|
|
684
|
+
console.error("Stack:", error.stack);
|
|
685
|
+
console.error("This error was caught by the global handler, preventing application crash");
|
|
571
686
|
// Attempt graceful shutdown but don't wait indefinitely
|
|
572
687
|
this.shutdown().finally(() => {
|
|
573
|
-
console.error(
|
|
688
|
+
console.error("Process exiting due to uncaught exception");
|
|
574
689
|
process.exit(1);
|
|
575
690
|
});
|
|
576
691
|
});
|
|
577
|
-
process.on(
|
|
578
|
-
console.error(
|
|
579
|
-
console.error(
|
|
580
|
-
console.error(
|
|
692
|
+
process.on("unhandledRejection", (reason, promise) => {
|
|
693
|
+
console.error("šØ Unhandled Promise Rejection at:", promise);
|
|
694
|
+
console.error("Reason:", reason);
|
|
695
|
+
console.error("This rejection was caught by the global handler, continuing operation");
|
|
581
696
|
// Log stack trace if reason is an Error
|
|
582
697
|
if (reason instanceof Error && reason.stack) {
|
|
583
|
-
console.error(
|
|
698
|
+
console.error("Stack:", reason.stack);
|
|
584
699
|
}
|
|
585
700
|
// Log the error but don't exit the process for promise rejections
|
|
586
701
|
// as they might be recoverable
|
|
587
702
|
});
|
|
588
703
|
}
|
|
589
704
|
catch (error) {
|
|
590
|
-
console.error(
|
|
705
|
+
console.error("\nā Failed to start edge application:", error.message);
|
|
591
706
|
// Provide more specific guidance for common errors
|
|
592
|
-
if (error.message?.includes(
|
|
593
|
-
console.error(
|
|
594
|
-
console.error(
|
|
595
|
-
console.error(
|
|
596
|
-
console.error(
|
|
597
|
-
console.error(
|
|
707
|
+
if (error.message?.includes("Failed to connect any repositories")) {
|
|
708
|
+
console.error("\nš” This usually happens when:");
|
|
709
|
+
console.error(" - All Linear OAuth tokens have expired");
|
|
710
|
+
console.error(" - The Linear API is temporarily unavailable");
|
|
711
|
+
console.error(" - Your network connection is having issues");
|
|
712
|
+
console.error("\nPlease check your edge configuration and try again.");
|
|
598
713
|
}
|
|
599
714
|
await this.shutdown();
|
|
600
715
|
process.exit(1);
|
|
@@ -604,12 +719,12 @@ class EdgeApp {
|
|
|
604
719
|
* Check if a branch exists locally or remotely
|
|
605
720
|
*/
|
|
606
721
|
async branchExists(branchName, repoPath) {
|
|
607
|
-
const { execSync } = await import(
|
|
722
|
+
const { execSync } = await import("node:child_process");
|
|
608
723
|
try {
|
|
609
724
|
// Check if branch exists locally
|
|
610
725
|
execSync(`git rev-parse --verify "${branchName}"`, {
|
|
611
726
|
cwd: repoPath,
|
|
612
|
-
stdio:
|
|
727
|
+
stdio: "pipe",
|
|
613
728
|
});
|
|
614
729
|
return true;
|
|
615
730
|
}
|
|
@@ -618,7 +733,7 @@ class EdgeApp {
|
|
|
618
733
|
try {
|
|
619
734
|
execSync(`git ls-remote --heads origin "${branchName}"`, {
|
|
620
735
|
cwd: repoPath,
|
|
621
|
-
stdio:
|
|
736
|
+
stdio: "pipe",
|
|
622
737
|
});
|
|
623
738
|
return true;
|
|
624
739
|
}
|
|
@@ -634,69 +749,73 @@ class EdgeApp {
|
|
|
634
749
|
if (!this.edgeWorker)
|
|
635
750
|
return;
|
|
636
751
|
// Session events
|
|
637
|
-
this.edgeWorker.on(
|
|
752
|
+
this.edgeWorker.on("session:started", (issueId, _issue, repositoryId) => {
|
|
638
753
|
console.log(`Started session for issue ${issueId} in repository ${repositoryId}`);
|
|
639
754
|
});
|
|
640
|
-
this.edgeWorker.on(
|
|
755
|
+
this.edgeWorker.on("session:ended", (issueId, exitCode, repositoryId) => {
|
|
641
756
|
console.log(`Session for issue ${issueId} ended with exit code ${exitCode} in repository ${repositoryId}`);
|
|
642
757
|
});
|
|
643
758
|
// Connection events
|
|
644
|
-
this.edgeWorker.on(
|
|
759
|
+
this.edgeWorker.on("connected", (token) => {
|
|
645
760
|
console.log(`ā
Connected to proxy with token ending in ...${token.slice(-4)}`);
|
|
646
761
|
});
|
|
647
|
-
this.edgeWorker.on(
|
|
648
|
-
console.error(`ā Disconnected from proxy (token ...${token.slice(-4)}): ${reason ||
|
|
762
|
+
this.edgeWorker.on("disconnected", (token, reason) => {
|
|
763
|
+
console.error(`ā Disconnected from proxy (token ...${token.slice(-4)}): ${reason || "Unknown reason"}`);
|
|
649
764
|
});
|
|
650
765
|
// Error events
|
|
651
|
-
this.edgeWorker.on(
|
|
652
|
-
console.error(
|
|
766
|
+
this.edgeWorker.on("error", (error) => {
|
|
767
|
+
console.error("EdgeWorker error:", error);
|
|
653
768
|
});
|
|
654
769
|
}
|
|
655
770
|
/**
|
|
656
771
|
* Create a git worktree for an issue
|
|
657
772
|
*/
|
|
658
773
|
async createGitWorktree(issue, repository) {
|
|
659
|
-
const { execSync } = await import(
|
|
660
|
-
const { existsSync } = await import(
|
|
661
|
-
const { join } = await import(
|
|
774
|
+
const { execSync } = await import("node:child_process");
|
|
775
|
+
const { existsSync } = await import("node:fs");
|
|
776
|
+
const { join } = await import("node:path");
|
|
662
777
|
try {
|
|
663
778
|
// Verify this is a git repository
|
|
664
779
|
try {
|
|
665
|
-
execSync(
|
|
780
|
+
execSync("git rev-parse --git-dir", {
|
|
666
781
|
cwd: repository.repositoryPath,
|
|
667
|
-
stdio:
|
|
782
|
+
stdio: "pipe",
|
|
668
783
|
});
|
|
669
784
|
}
|
|
670
|
-
catch (
|
|
785
|
+
catch (_e) {
|
|
671
786
|
console.error(`${repository.repositoryPath} is not a git repository`);
|
|
672
|
-
throw new Error(
|
|
787
|
+
throw new Error("Not a git repository");
|
|
673
788
|
}
|
|
674
789
|
// Sanitize branch name by removing backticks to prevent command injection
|
|
675
|
-
const sanitizeBranchName = (name) => name ? name.replace(/`/g,
|
|
790
|
+
const sanitizeBranchName = (name) => name ? name.replace(/`/g, "") : name;
|
|
676
791
|
// Use Linear's preferred branch name, or generate one if not available
|
|
677
|
-
const rawBranchName = issue.branchName ||
|
|
792
|
+
const rawBranchName = issue.branchName ||
|
|
793
|
+
`${issue.identifier}-${issue.title
|
|
794
|
+
?.toLowerCase()
|
|
795
|
+
.replace(/\s+/g, "-")
|
|
796
|
+
.substring(0, 30)}`;
|
|
678
797
|
const branchName = sanitizeBranchName(rawBranchName);
|
|
679
798
|
const workspacePath = join(repository.workspaceBaseDir, issue.identifier);
|
|
680
799
|
// Ensure workspace directory exists
|
|
681
800
|
execSync(`mkdir -p "${repository.workspaceBaseDir}"`, {
|
|
682
801
|
cwd: repository.repositoryPath,
|
|
683
|
-
stdio:
|
|
802
|
+
stdio: "pipe",
|
|
684
803
|
});
|
|
685
804
|
// Check if worktree already exists
|
|
686
805
|
try {
|
|
687
|
-
const worktrees = execSync(
|
|
806
|
+
const worktrees = execSync("git worktree list --porcelain", {
|
|
688
807
|
cwd: repository.repositoryPath,
|
|
689
|
-
encoding:
|
|
808
|
+
encoding: "utf-8",
|
|
690
809
|
});
|
|
691
810
|
if (worktrees.includes(workspacePath)) {
|
|
692
811
|
console.log(`Worktree already exists at ${workspacePath}, using existing`);
|
|
693
812
|
return {
|
|
694
813
|
path: workspacePath,
|
|
695
|
-
isGitWorktree: true
|
|
814
|
+
isGitWorktree: true,
|
|
696
815
|
};
|
|
697
816
|
}
|
|
698
817
|
}
|
|
699
|
-
catch (
|
|
818
|
+
catch (_e) {
|
|
700
819
|
// git worktree command failed, continue with creation
|
|
701
820
|
}
|
|
702
821
|
// Check if branch already exists
|
|
@@ -704,11 +823,11 @@ class EdgeApp {
|
|
|
704
823
|
try {
|
|
705
824
|
execSync(`git rev-parse --verify "${branchName}"`, {
|
|
706
825
|
cwd: repository.repositoryPath,
|
|
707
|
-
stdio:
|
|
826
|
+
stdio: "pipe",
|
|
708
827
|
});
|
|
709
828
|
createBranch = false;
|
|
710
829
|
}
|
|
711
|
-
catch (
|
|
830
|
+
catch (_e) {
|
|
712
831
|
// Branch doesn't exist, we'll create it
|
|
713
832
|
}
|
|
714
833
|
// Determine base branch for this issue
|
|
@@ -719,7 +838,11 @@ class EdgeApp {
|
|
|
719
838
|
if (parent) {
|
|
720
839
|
console.log(`Issue ${issue.identifier} has parent: ${parent.identifier}`);
|
|
721
840
|
// Get parent's branch name
|
|
722
|
-
const parentRawBranchName = parent.branchName ||
|
|
841
|
+
const parentRawBranchName = parent.branchName ||
|
|
842
|
+
`${parent.identifier}-${parent.title
|
|
843
|
+
?.toLowerCase()
|
|
844
|
+
.replace(/\s+/g, "-")
|
|
845
|
+
.substring(0, 30)}`;
|
|
723
846
|
const parentBranchName = sanitizeBranchName(parentRawBranchName);
|
|
724
847
|
// Check if parent branch exists
|
|
725
848
|
const parentBranchExists = await this.branchExists(parentBranchName, repository.repositoryPath);
|
|
@@ -732,21 +855,21 @@ class EdgeApp {
|
|
|
732
855
|
}
|
|
733
856
|
}
|
|
734
857
|
}
|
|
735
|
-
catch (
|
|
858
|
+
catch (_error) {
|
|
736
859
|
// Parent field might not exist or couldn't be fetched, use default base branch
|
|
737
860
|
console.log(`No parent issue found for ${issue.identifier}, using default base branch '${repository.baseBranch}'`);
|
|
738
861
|
}
|
|
739
862
|
// Fetch latest changes from remote
|
|
740
|
-
console.log(
|
|
863
|
+
console.log("Fetching latest changes from remote...");
|
|
741
864
|
let hasRemote = true;
|
|
742
865
|
try {
|
|
743
|
-
execSync(
|
|
866
|
+
execSync("git fetch origin", {
|
|
744
867
|
cwd: repository.repositoryPath,
|
|
745
|
-
stdio:
|
|
868
|
+
stdio: "pipe",
|
|
746
869
|
});
|
|
747
870
|
}
|
|
748
871
|
catch (e) {
|
|
749
|
-
console.warn(
|
|
872
|
+
console.warn("Warning: git fetch failed, proceeding with local branch:", e.message);
|
|
750
873
|
hasRemote = false;
|
|
751
874
|
}
|
|
752
875
|
// Create the worktree - use determined base branch
|
|
@@ -771,42 +894,42 @@ class EdgeApp {
|
|
|
771
894
|
}
|
|
772
895
|
execSync(worktreeCmd, {
|
|
773
896
|
cwd: repository.repositoryPath,
|
|
774
|
-
stdio:
|
|
897
|
+
stdio: "pipe",
|
|
775
898
|
});
|
|
776
899
|
// Check for cyrus-setup.sh script in the repository root
|
|
777
|
-
const setupScriptPath = join(repository.repositoryPath,
|
|
900
|
+
const setupScriptPath = join(repository.repositoryPath, "cyrus-setup.sh");
|
|
778
901
|
if (existsSync(setupScriptPath)) {
|
|
779
|
-
console.log(
|
|
902
|
+
console.log("Running cyrus-setup.sh in new worktree...");
|
|
780
903
|
try {
|
|
781
|
-
execSync(
|
|
904
|
+
execSync("bash cyrus-setup.sh", {
|
|
782
905
|
cwd: workspacePath,
|
|
783
|
-
stdio:
|
|
906
|
+
stdio: "inherit",
|
|
784
907
|
env: {
|
|
785
908
|
...process.env,
|
|
786
909
|
LINEAR_ISSUE_ID: issue.id,
|
|
787
910
|
LINEAR_ISSUE_IDENTIFIER: issue.identifier,
|
|
788
|
-
LINEAR_ISSUE_TITLE: issue.title ||
|
|
789
|
-
}
|
|
911
|
+
LINEAR_ISSUE_TITLE: issue.title || "",
|
|
912
|
+
},
|
|
790
913
|
});
|
|
791
914
|
}
|
|
792
915
|
catch (error) {
|
|
793
|
-
console.warn(
|
|
916
|
+
console.warn("Warning: cyrus-setup.sh failed:", error.message);
|
|
794
917
|
// Continue despite setup script failure
|
|
795
918
|
}
|
|
796
919
|
}
|
|
797
920
|
return {
|
|
798
921
|
path: workspacePath,
|
|
799
|
-
isGitWorktree: true
|
|
922
|
+
isGitWorktree: true,
|
|
800
923
|
};
|
|
801
924
|
}
|
|
802
925
|
catch (error) {
|
|
803
|
-
console.error(
|
|
926
|
+
console.error("Failed to create git worktree:", error.message);
|
|
804
927
|
// Fall back to regular directory if git worktree fails
|
|
805
928
|
const fallbackPath = join(repository.workspaceBaseDir, issue.identifier);
|
|
806
|
-
execSync(`mkdir -p "${fallbackPath}"`, { stdio:
|
|
929
|
+
execSync(`mkdir -p "${fallbackPath}"`, { stdio: "pipe" });
|
|
807
930
|
return {
|
|
808
931
|
path: fallbackPath,
|
|
809
|
-
isGitWorktree: false
|
|
932
|
+
isGitWorktree: false,
|
|
810
933
|
};
|
|
811
934
|
}
|
|
812
935
|
}
|
|
@@ -817,31 +940,34 @@ class EdgeApp {
|
|
|
817
940
|
if (this.isShuttingDown)
|
|
818
941
|
return;
|
|
819
942
|
this.isShuttingDown = true;
|
|
820
|
-
console.log(
|
|
943
|
+
console.log("\nShutting down edge worker...");
|
|
821
944
|
// Stop edge worker (includes stopping shared application server)
|
|
822
945
|
if (this.edgeWorker) {
|
|
823
946
|
await this.edgeWorker.stop();
|
|
824
947
|
}
|
|
825
|
-
console.log(
|
|
948
|
+
console.log("Shutdown complete");
|
|
826
949
|
process.exit(0);
|
|
827
950
|
}
|
|
828
951
|
}
|
|
829
952
|
// Helper function to check Linear token status
|
|
830
953
|
async function checkLinearToken(token) {
|
|
831
954
|
try {
|
|
832
|
-
const response = await fetch(
|
|
833
|
-
method:
|
|
955
|
+
const response = await fetch("https://api.linear.app/graphql", {
|
|
956
|
+
method: "POST",
|
|
834
957
|
headers: {
|
|
835
|
-
|
|
836
|
-
|
|
958
|
+
"Content-Type": "application/json",
|
|
959
|
+
Authorization: token,
|
|
837
960
|
},
|
|
838
961
|
body: JSON.stringify({
|
|
839
|
-
query:
|
|
840
|
-
})
|
|
962
|
+
query: "{ viewer { id email name } }",
|
|
963
|
+
}),
|
|
841
964
|
});
|
|
842
|
-
const data = await response.json();
|
|
965
|
+
const data = (await response.json());
|
|
843
966
|
if (data.errors) {
|
|
844
|
-
return {
|
|
967
|
+
return {
|
|
968
|
+
valid: false,
|
|
969
|
+
error: data.errors[0]?.message || "Unknown error",
|
|
970
|
+
};
|
|
845
971
|
}
|
|
846
972
|
return { valid: true };
|
|
847
973
|
}
|
|
@@ -854,16 +980,16 @@ async function checkTokensCommand() {
|
|
|
854
980
|
const app = new EdgeApp();
|
|
855
981
|
const configPath = app.getEdgeConfigPath();
|
|
856
982
|
if (!existsSync(configPath)) {
|
|
857
|
-
console.error(
|
|
983
|
+
console.error("No edge configuration found. Please run setup first.");
|
|
858
984
|
process.exit(1);
|
|
859
985
|
}
|
|
860
|
-
const config = JSON.parse(readFileSync(configPath,
|
|
861
|
-
console.log(
|
|
986
|
+
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
987
|
+
console.log("Checking Linear tokens...\n");
|
|
862
988
|
for (const repo of config.repositories) {
|
|
863
989
|
process.stdout.write(`${repo.name} (${repo.linearWorkspaceName}): `);
|
|
864
990
|
const result = await checkLinearToken(repo.linearToken);
|
|
865
991
|
if (result.valid) {
|
|
866
|
-
console.log(
|
|
992
|
+
console.log("ā
Valid");
|
|
867
993
|
}
|
|
868
994
|
else {
|
|
869
995
|
console.log(`ā Invalid - ${result.error}`);
|
|
@@ -875,34 +1001,34 @@ async function refreshTokenCommand() {
|
|
|
875
1001
|
const app = new EdgeApp();
|
|
876
1002
|
const configPath = app.getEdgeConfigPath();
|
|
877
1003
|
if (!existsSync(configPath)) {
|
|
878
|
-
console.error(
|
|
1004
|
+
console.error("No edge configuration found. Please run setup first.");
|
|
879
1005
|
process.exit(1);
|
|
880
1006
|
}
|
|
881
|
-
const config = JSON.parse(readFileSync(configPath,
|
|
1007
|
+
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
882
1008
|
// Show repositories with their token status
|
|
883
|
-
console.log(
|
|
1009
|
+
console.log("Checking current token status...\n");
|
|
884
1010
|
const tokenStatuses = [];
|
|
885
1011
|
for (const repo of config.repositories) {
|
|
886
1012
|
const result = await checkLinearToken(repo.linearToken);
|
|
887
1013
|
tokenStatuses.push({ repo, valid: result.valid });
|
|
888
|
-
console.log(`${tokenStatuses.length}. ${repo.name} (${repo.linearWorkspaceName}): ${result.valid ?
|
|
1014
|
+
console.log(`${tokenStatuses.length}. ${repo.name} (${repo.linearWorkspaceName}): ${result.valid ? "ā
Valid" : "ā Invalid"}`);
|
|
889
1015
|
}
|
|
890
1016
|
// Ask which token to refresh
|
|
891
1017
|
const rl = readline.createInterface({
|
|
892
1018
|
input: process.stdin,
|
|
893
|
-
output: process.stdout
|
|
1019
|
+
output: process.stdout,
|
|
894
1020
|
});
|
|
895
|
-
const answer = await new Promise(resolve => {
|
|
1021
|
+
const answer = await new Promise((resolve) => {
|
|
896
1022
|
rl.question('\nWhich repository token would you like to refresh? (Enter number or "all"): ', resolve);
|
|
897
1023
|
});
|
|
898
1024
|
const indicesToRefresh = [];
|
|
899
|
-
if (answer.toLowerCase() ===
|
|
1025
|
+
if (answer.toLowerCase() === "all") {
|
|
900
1026
|
indicesToRefresh.push(...Array.from({ length: tokenStatuses.length }, (_, i) => i));
|
|
901
1027
|
}
|
|
902
1028
|
else {
|
|
903
1029
|
const index = parseInt(answer) - 1;
|
|
904
|
-
if (isNaN(index) || index < 0 || index >= tokenStatuses.length) {
|
|
905
|
-
console.error(
|
|
1030
|
+
if (Number.isNaN(index) || index < 0 || index >= tokenStatuses.length) {
|
|
1031
|
+
console.error("Invalid selection");
|
|
906
1032
|
rl.close();
|
|
907
1033
|
process.exit(1);
|
|
908
1034
|
}
|
|
@@ -915,9 +1041,11 @@ async function refreshTokenCommand() {
|
|
|
915
1041
|
continue;
|
|
916
1042
|
const { repo } = tokenStatus;
|
|
917
1043
|
console.log(`\nRefreshing token for ${repo.name} (${repo.linearWorkspaceName || repo.linearWorkspaceId})...`);
|
|
918
|
-
console.log(
|
|
1044
|
+
console.log("Opening Linear OAuth flow in your browser...");
|
|
919
1045
|
// Use the proxy's OAuth flow with a callback to localhost
|
|
920
|
-
const serverPort = process.env.CYRUS_SERVER_PORT
|
|
1046
|
+
const serverPort = process.env.CYRUS_SERVER_PORT
|
|
1047
|
+
? parseInt(process.env.CYRUS_SERVER_PORT, 10)
|
|
1048
|
+
: 3456;
|
|
921
1049
|
const callbackUrl = `http://localhost:${serverPort}/callback`;
|
|
922
1050
|
const oauthUrl = `https://cyrus-proxy.ceedar.workers.dev/oauth/authorize?callback=${encodeURIComponent(callbackUrl)}`;
|
|
923
1051
|
console.log(`\nPlease complete the OAuth flow in your browser.`);
|
|
@@ -926,10 +1054,10 @@ async function refreshTokenCommand() {
|
|
|
926
1054
|
let tokenReceived = null;
|
|
927
1055
|
const server = await new Promise((resolve) => {
|
|
928
1056
|
const s = http.createServer((req, res) => {
|
|
929
|
-
if (req.url?.startsWith(
|
|
1057
|
+
if (req.url?.startsWith("/callback")) {
|
|
930
1058
|
const url = new URL(req.url, `http://localhost:${serverPort}`);
|
|
931
|
-
tokenReceived = url.searchParams.get(
|
|
932
|
-
res.writeHead(200, {
|
|
1059
|
+
tokenReceived = url.searchParams.get("token");
|
|
1060
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
933
1061
|
res.end(`
|
|
934
1062
|
<html>
|
|
935
1063
|
<head>
|
|
@@ -945,11 +1073,11 @@ async function refreshTokenCommand() {
|
|
|
945
1073
|
}
|
|
946
1074
|
else {
|
|
947
1075
|
res.writeHead(404);
|
|
948
|
-
res.end(
|
|
1076
|
+
res.end("Not found");
|
|
949
1077
|
}
|
|
950
1078
|
});
|
|
951
1079
|
s.listen(serverPort, () => {
|
|
952
|
-
console.log(
|
|
1080
|
+
console.log("Waiting for OAuth callback...");
|
|
953
1081
|
resolve(s);
|
|
954
1082
|
});
|
|
955
1083
|
});
|
|
@@ -957,12 +1085,12 @@ async function refreshTokenCommand() {
|
|
|
957
1085
|
// Wait for the token with timeout
|
|
958
1086
|
const startTime = Date.now();
|
|
959
1087
|
while (!tokenReceived && Date.now() - startTime < 120000) {
|
|
960
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
1088
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
961
1089
|
}
|
|
962
1090
|
server.close();
|
|
963
1091
|
const newToken = tokenReceived;
|
|
964
|
-
if (!newToken || !newToken.startsWith(
|
|
965
|
-
console.error(
|
|
1092
|
+
if (!newToken || !newToken.startsWith("lin_oauth_")) {
|
|
1093
|
+
console.error("Invalid token received from OAuth flow");
|
|
966
1094
|
continue;
|
|
967
1095
|
}
|
|
968
1096
|
// Verify the new token
|
|
@@ -988,33 +1116,179 @@ async function refreshTokenCommand() {
|
|
|
988
1116
|
}
|
|
989
1117
|
// Save the updated config
|
|
990
1118
|
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
991
|
-
console.log(
|
|
1119
|
+
console.log("\nā
Configuration saved");
|
|
992
1120
|
rl.close();
|
|
993
1121
|
}
|
|
1122
|
+
// Command: add-repository
|
|
1123
|
+
async function addRepositoryCommand() {
|
|
1124
|
+
const app = new EdgeApp();
|
|
1125
|
+
console.log("š Add New Repository");
|
|
1126
|
+
console.log("ā".repeat(50));
|
|
1127
|
+
console.log();
|
|
1128
|
+
try {
|
|
1129
|
+
// Load existing configuration
|
|
1130
|
+
const config = app.loadEdgeConfig();
|
|
1131
|
+
// Check if we have any Linear credentials
|
|
1132
|
+
const existingRepos = config.repositories || [];
|
|
1133
|
+
let linearCredentials = null;
|
|
1134
|
+
if (existingRepos.length > 0) {
|
|
1135
|
+
// Try to get credentials from existing repositories
|
|
1136
|
+
const repoWithToken = existingRepos.find((r) => r.linearToken);
|
|
1137
|
+
if (repoWithToken) {
|
|
1138
|
+
linearCredentials = {
|
|
1139
|
+
linearToken: repoWithToken.linearToken,
|
|
1140
|
+
linearWorkspaceId: repoWithToken.linearWorkspaceId,
|
|
1141
|
+
linearWorkspaceName: repoWithToken.linearWorkspaceName || "Your Workspace",
|
|
1142
|
+
};
|
|
1143
|
+
console.log(`ā
Using Linear credentials from existing configuration`);
|
|
1144
|
+
console.log(` Workspace: ${linearCredentials.linearWorkspaceName}`);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
// If no credentials found, run OAuth flow
|
|
1148
|
+
if (!linearCredentials) {
|
|
1149
|
+
console.log("š No Linear credentials found. Starting OAuth flow...");
|
|
1150
|
+
// Start OAuth flow using the default proxy URL
|
|
1151
|
+
const proxyUrl = process.env.PROXY_URL || "https://cyrus-proxy.ceedar.workers.dev";
|
|
1152
|
+
linearCredentials = await app.startOAuthFlow(proxyUrl);
|
|
1153
|
+
if (!linearCredentials) {
|
|
1154
|
+
throw new Error("OAuth flow cancelled or failed");
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
// Now set up the new repository
|
|
1158
|
+
console.log("\nš Configure New Repository");
|
|
1159
|
+
console.log("ā".repeat(50));
|
|
1160
|
+
const newRepo = await app.setupRepositoryWizard(linearCredentials);
|
|
1161
|
+
// Add to existing repositories
|
|
1162
|
+
config.repositories = [...existingRepos, newRepo];
|
|
1163
|
+
// Save the updated configuration
|
|
1164
|
+
app.saveEdgeConfig(config);
|
|
1165
|
+
console.log("\nā
Repository added successfully!");
|
|
1166
|
+
console.log(`š Repository: ${newRepo.name}`);
|
|
1167
|
+
console.log(`š Path: ${newRepo.repositoryPath}`);
|
|
1168
|
+
console.log(`šæ Base branch: ${newRepo.baseBranch}`);
|
|
1169
|
+
console.log(`š Workspace directory: ${newRepo.workspaceBaseDir}`);
|
|
1170
|
+
}
|
|
1171
|
+
catch (error) {
|
|
1172
|
+
console.error("\nā Failed to add repository:", error);
|
|
1173
|
+
throw error;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
// Command: set-customer-id
|
|
1177
|
+
async function setCustomerIdCommand() {
|
|
1178
|
+
const app = new EdgeApp();
|
|
1179
|
+
const configPath = app.getEdgeConfigPath();
|
|
1180
|
+
// Get customer ID from command line args
|
|
1181
|
+
const customerId = args[1];
|
|
1182
|
+
if (!customerId) {
|
|
1183
|
+
console.error("Please provide a customer ID");
|
|
1184
|
+
console.log("Usage: cyrus set-customer-id cus_XXXXX");
|
|
1185
|
+
process.exit(1);
|
|
1186
|
+
}
|
|
1187
|
+
if (!customerId.startsWith("cus_")) {
|
|
1188
|
+
console.error("Invalid customer ID format");
|
|
1189
|
+
console.log('Customer IDs should start with "cus_"');
|
|
1190
|
+
process.exit(1);
|
|
1191
|
+
}
|
|
1192
|
+
try {
|
|
1193
|
+
// Load existing config or create new one
|
|
1194
|
+
let config = { repositories: [] };
|
|
1195
|
+
if (existsSync(configPath)) {
|
|
1196
|
+
config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
1197
|
+
}
|
|
1198
|
+
// Update customer ID
|
|
1199
|
+
config.stripeCustomerId = customerId;
|
|
1200
|
+
// Save config
|
|
1201
|
+
app.saveEdgeConfig(config);
|
|
1202
|
+
console.log("\nā
Customer ID saved successfully!");
|
|
1203
|
+
console.log("ā".repeat(50));
|
|
1204
|
+
console.log(`Customer ID: ${customerId}`);
|
|
1205
|
+
console.log("\nYou now have access to Cyrus Pro features.");
|
|
1206
|
+
console.log('Run "cyrus" to start the edge worker.');
|
|
1207
|
+
}
|
|
1208
|
+
catch (error) {
|
|
1209
|
+
console.error("Failed to save customer ID:", error.message);
|
|
1210
|
+
process.exit(1);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
// Command: billing
|
|
1214
|
+
async function billingCommand() {
|
|
1215
|
+
const app = new EdgeApp();
|
|
1216
|
+
const configPath = app.getEdgeConfigPath();
|
|
1217
|
+
if (!existsSync(configPath)) {
|
|
1218
|
+
console.error('No configuration found. Please run "cyrus" to set up first.');
|
|
1219
|
+
process.exit(1);
|
|
1220
|
+
}
|
|
1221
|
+
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
1222
|
+
if (!config.stripeCustomerId) {
|
|
1223
|
+
console.log("\nšÆ No Pro Plan Active");
|
|
1224
|
+
console.log("ā".repeat(50));
|
|
1225
|
+
console.log("You don't have an active subscription.");
|
|
1226
|
+
console.log("Please start a free trial at:");
|
|
1227
|
+
console.log("\n https://www.atcyrus.com/pricing\n");
|
|
1228
|
+
console.log("After signing up, your customer ID will be saved automatically.");
|
|
1229
|
+
process.exit(0);
|
|
1230
|
+
}
|
|
1231
|
+
console.log("\nš Opening Billing Portal...");
|
|
1232
|
+
console.log("ā".repeat(50));
|
|
1233
|
+
try {
|
|
1234
|
+
// Open atcyrus.com with the customer ID to handle Stripe redirect
|
|
1235
|
+
const billingUrl = `https://www.atcyrus.com/billing/${config.stripeCustomerId}`;
|
|
1236
|
+
console.log("ā
Opening billing portal in browser...");
|
|
1237
|
+
console.log(`\nš URL: ${billingUrl}\n`);
|
|
1238
|
+
// Open the billing portal URL in the default browser
|
|
1239
|
+
await open(billingUrl);
|
|
1240
|
+
console.log("The billing portal should now be open in your browser.");
|
|
1241
|
+
console.log("You can manage your subscription, update payment methods, and download invoices.");
|
|
1242
|
+
}
|
|
1243
|
+
catch (error) {
|
|
1244
|
+
console.error("ā Failed to open billing portal:", error.message);
|
|
1245
|
+
console.log("\nPlease visit: https://www.atcyrus.com/billing");
|
|
1246
|
+
console.log("Customer ID:", config.stripeCustomerId);
|
|
1247
|
+
process.exit(1);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
994
1250
|
// Parse command
|
|
995
|
-
const command = args[0] ||
|
|
1251
|
+
const command = args[0] || "start";
|
|
996
1252
|
// Execute appropriate command
|
|
997
1253
|
switch (command) {
|
|
998
|
-
case
|
|
999
|
-
checkTokensCommand().catch(error => {
|
|
1000
|
-
console.error(
|
|
1254
|
+
case "check-tokens":
|
|
1255
|
+
checkTokensCommand().catch((error) => {
|
|
1256
|
+
console.error("Error:", error);
|
|
1257
|
+
process.exit(1);
|
|
1258
|
+
});
|
|
1259
|
+
break;
|
|
1260
|
+
case "refresh-token":
|
|
1261
|
+
refreshTokenCommand().catch((error) => {
|
|
1262
|
+
console.error("Error:", error);
|
|
1263
|
+
process.exit(1);
|
|
1264
|
+
});
|
|
1265
|
+
break;
|
|
1266
|
+
case "add-repository":
|
|
1267
|
+
addRepositoryCommand().catch((error) => {
|
|
1268
|
+
console.error("Error:", error);
|
|
1001
1269
|
process.exit(1);
|
|
1002
1270
|
});
|
|
1003
1271
|
break;
|
|
1004
|
-
case
|
|
1005
|
-
|
|
1006
|
-
console.error(
|
|
1272
|
+
case "billing":
|
|
1273
|
+
billingCommand().catch((error) => {
|
|
1274
|
+
console.error("Error:", error);
|
|
1007
1275
|
process.exit(1);
|
|
1008
1276
|
});
|
|
1009
1277
|
break;
|
|
1010
|
-
case
|
|
1011
|
-
|
|
1278
|
+
case "set-customer-id":
|
|
1279
|
+
setCustomerIdCommand().catch((error) => {
|
|
1280
|
+
console.error("Error:", error);
|
|
1281
|
+
process.exit(1);
|
|
1282
|
+
});
|
|
1283
|
+
break;
|
|
1284
|
+
default: {
|
|
1012
1285
|
// Create and start the app
|
|
1013
1286
|
const app = new EdgeApp();
|
|
1014
|
-
app.start().catch(error => {
|
|
1015
|
-
console.error(
|
|
1287
|
+
app.start().catch((error) => {
|
|
1288
|
+
console.error("Fatal error:", error);
|
|
1016
1289
|
process.exit(1);
|
|
1017
1290
|
});
|
|
1018
1291
|
break;
|
|
1292
|
+
}
|
|
1019
1293
|
}
|
|
1020
1294
|
//# sourceMappingURL=app.js.map
|