cyrus-ai 0.1.35-alpha.2 ā 0.1.37
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/LICENSE +674 -21
- package/dist/app.js +309 -226
- 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
|
|
|
@@ -42,7 +42,7 @@ Examples:
|
|
|
42
42
|
}
|
|
43
43
|
// Load environment variables only if --env-file is specified
|
|
44
44
|
if (envFileArg) {
|
|
45
|
-
const envFile = envFileArg.split(
|
|
45
|
+
const envFile = envFileArg.split("=")[1];
|
|
46
46
|
if (envFile) {
|
|
47
47
|
dotenv.config({ path: envFile });
|
|
48
48
|
}
|
|
@@ -57,13 +57,13 @@ class EdgeApp {
|
|
|
57
57
|
* Get the edge configuration file path
|
|
58
58
|
*/
|
|
59
59
|
getEdgeConfigPath() {
|
|
60
|
-
return resolve(homedir(),
|
|
60
|
+
return resolve(homedir(), ".cyrus", "config.json");
|
|
61
61
|
}
|
|
62
62
|
/**
|
|
63
63
|
* Get the legacy edge configuration file path (for migration)
|
|
64
64
|
*/
|
|
65
65
|
getLegacyEdgeConfigPath() {
|
|
66
|
-
return resolve(process.cwd(),
|
|
66
|
+
return resolve(process.cwd(), ".edge-config.json");
|
|
67
67
|
}
|
|
68
68
|
/**
|
|
69
69
|
* Migrate configuration from legacy location if needed
|
|
@@ -106,15 +106,15 @@ class EdgeApp {
|
|
|
106
106
|
let config = { repositories: [] };
|
|
107
107
|
if (existsSync(edgeConfigPath)) {
|
|
108
108
|
try {
|
|
109
|
-
config = JSON.parse(readFileSync(edgeConfigPath,
|
|
109
|
+
config = JSON.parse(readFileSync(edgeConfigPath, "utf-8"));
|
|
110
110
|
}
|
|
111
111
|
catch (e) {
|
|
112
|
-
console.error(
|
|
112
|
+
console.error("Failed to load edge config:", e.message);
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
115
|
// Strip promptTemplatePath from all repositories to ensure built-in template is used
|
|
116
116
|
if (config.repositories) {
|
|
117
|
-
config.repositories = config.repositories.map(repo => {
|
|
117
|
+
config.repositories = config.repositories.map((repo) => {
|
|
118
118
|
const { promptTemplatePath, ...repoWithoutTemplate } = repo;
|
|
119
119
|
if (promptTemplatePath) {
|
|
120
120
|
console.log(`Ignoring custom prompt template for repository: ${repo.name} (using built-in template)`);
|
|
@@ -142,66 +142,81 @@ class EdgeApp {
|
|
|
142
142
|
async setupRepositoryWizard(linearCredentials) {
|
|
143
143
|
const rl = readline.createInterface({
|
|
144
144
|
input: process.stdin,
|
|
145
|
-
output: process.stdout
|
|
145
|
+
output: process.stdout,
|
|
146
146
|
});
|
|
147
147
|
const question = (prompt) => new Promise((resolve) => {
|
|
148
148
|
rl.question(prompt, resolve);
|
|
149
149
|
});
|
|
150
|
-
console.log(
|
|
151
|
-
console.log(
|
|
150
|
+
console.log("\nš Repository Setup");
|
|
151
|
+
console.log("ā".repeat(50));
|
|
152
152
|
try {
|
|
153
153
|
// Ask for repository details
|
|
154
|
-
const repositoryPath = await question(`Repository path (default: ${process.cwd()}): `) ||
|
|
155
|
-
|
|
156
|
-
const
|
|
154
|
+
const repositoryPath = (await question(`Repository path (default: ${process.cwd()}): `)) ||
|
|
155
|
+
process.cwd();
|
|
156
|
+
const repositoryName = (await question(`Repository name (default: ${basename(repositoryPath)}): `)) || basename(repositoryPath);
|
|
157
|
+
const baseBranch = (await question("Base branch (default: main): ")) || "main";
|
|
157
158
|
// Create a path-safe version of the repository name for namespacing
|
|
158
|
-
const repoNameSafe = repositoryName
|
|
159
|
-
|
|
160
|
-
|
|
159
|
+
const repoNameSafe = repositoryName
|
|
160
|
+
.replace(/[^a-zA-Z0-9-_]/g, "-")
|
|
161
|
+
.toLowerCase();
|
|
162
|
+
const defaultWorkspaceDir = resolve(homedir(), ".cyrus", "workspaces", repoNameSafe);
|
|
163
|
+
const workspaceBaseDir = (await question(`Workspace directory (default: ${defaultWorkspaceDir}): `)) || defaultWorkspaceDir;
|
|
161
164
|
// Note: Prompt template is now hardcoded - no longer configurable
|
|
162
165
|
// Ask for MCP configuration
|
|
163
|
-
console.log(
|
|
164
|
-
console.log(
|
|
165
|
-
console.log(
|
|
166
|
-
console.log(
|
|
167
|
-
console.log(
|
|
166
|
+
console.log("\nš§ MCP (Model Context Protocol) Configuration");
|
|
167
|
+
console.log("MCP allows Claude to access external tools and data sources.");
|
|
168
|
+
console.log("Examples: filesystem access, database connections, API integrations");
|
|
169
|
+
console.log("See: https://docs.anthropic.com/en/docs/claude-code/mcp");
|
|
170
|
+
console.log("");
|
|
168
171
|
const mcpConfigInput = await question('MCP config file path (optional, format: {"mcpServers": {...}}, e.g., ./mcp-config.json): ');
|
|
169
172
|
const mcpConfigPath = mcpConfigInput.trim() || undefined;
|
|
170
173
|
// Ask for allowed tools configuration
|
|
171
|
-
console.log(
|
|
172
|
-
console.log(
|
|
173
|
-
console.log(
|
|
174
|
-
console.log(
|
|
174
|
+
console.log("\nš§ Tool Configuration");
|
|
175
|
+
console.log("Available tools: Read(**),Edit(**),Bash,Task,WebFetch,WebSearch,TodoRead,TodoWrite,NotebookRead,NotebookEdit,Batch");
|
|
176
|
+
console.log("");
|
|
177
|
+
console.log("ā ļø SECURITY NOTE: Bash tool requires special configuration for safety:");
|
|
175
178
|
console.log(' ⢠Use "Bash" for full access (not recommended in production)');
|
|
176
179
|
console.log(' ⢠Use "Bash(npm:*)" to restrict to npm commands only');
|
|
177
180
|
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
|
|
181
|
+
console.log(" ⢠See: https://docs.anthropic.com/en/docs/claude-code/settings#permissions");
|
|
182
|
+
console.log("");
|
|
183
|
+
console.log("Default: All tools except Bash (leave blank for all non-Bash tools)");
|
|
184
|
+
const allowedToolsInput = await question("Allowed tools (comma-separated, default: all except Bash): ");
|
|
185
|
+
const allowedTools = allowedToolsInput
|
|
186
|
+
? allowedToolsInput.split(",").map((t) => t.trim())
|
|
187
|
+
: undefined;
|
|
183
188
|
// 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
|
|
189
|
+
console.log("\nš·ļø Team-Based Routing (Optional)");
|
|
190
|
+
console.log("Configure specific Linear team keys to route issues to this repository.");
|
|
191
|
+
console.log("Example: CEE,FRONT,BACK for teams with those prefixes");
|
|
192
|
+
console.log("Leave blank to receive all issues from the workspace.");
|
|
193
|
+
const teamKeysInput = await question("Team keys (comma-separated, optional): ");
|
|
194
|
+
const teamKeys = teamKeysInput
|
|
195
|
+
? teamKeysInput.split(",").map((t) => t.trim().toUpperCase())
|
|
196
|
+
: undefined;
|
|
190
197
|
// 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(
|
|
198
|
+
console.log("\nšÆ Label-Based System Prompts (Optional)");
|
|
199
|
+
console.log("Cyrus can use different strategies based on Linear issue labels.");
|
|
200
|
+
console.log("Configure which labels trigger each specialized mode:");
|
|
201
|
+
console.log("⢠Debugger mode: Focuses on systematic problem investigation");
|
|
202
|
+
console.log("⢠Builder mode: Emphasizes feature implementation and code quality");
|
|
203
|
+
console.log("⢠Scoper mode: Helps analyze requirements and create technical plans");
|
|
197
204
|
const debuggerLabelsInput = await question('Labels for debugger mode (comma-separated, e.g., "Bug"): ');
|
|
198
205
|
const builderLabelsInput = await question('Labels for builder mode (comma-separated, e.g., "Feature,Improvement"): ');
|
|
199
206
|
const scoperLabelsInput = await question('Labels for scoper mode (comma-separated, e.g., "PRD"): ');
|
|
200
|
-
const labelPrompts =
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
207
|
+
const labelPrompts = debuggerLabelsInput || builderLabelsInput || scoperLabelsInput
|
|
208
|
+
? {
|
|
209
|
+
...(debuggerLabelsInput && {
|
|
210
|
+
debugger: debuggerLabelsInput.split(",").map((l) => l.trim()),
|
|
211
|
+
}),
|
|
212
|
+
...(builderLabelsInput && {
|
|
213
|
+
builder: builderLabelsInput.split(",").map((l) => l.trim()),
|
|
214
|
+
}),
|
|
215
|
+
...(scoperLabelsInput && {
|
|
216
|
+
scoper: scoperLabelsInput.split(",").map((l) => l.trim()),
|
|
217
|
+
}),
|
|
218
|
+
}
|
|
219
|
+
: undefined;
|
|
205
220
|
rl.close();
|
|
206
221
|
// Create repository configuration
|
|
207
222
|
const repository = {
|
|
@@ -216,7 +231,7 @@ class EdgeApp {
|
|
|
216
231
|
...(allowedTools && { allowedTools }),
|
|
217
232
|
...(mcpConfigPath && { mcpConfigPath: resolve(mcpConfigPath) }),
|
|
218
233
|
...(teamKeys && { teamKeys }),
|
|
219
|
-
...(labelPrompts && { labelPrompts })
|
|
234
|
+
...(labelPrompts && { labelPrompts }),
|
|
220
235
|
};
|
|
221
236
|
return repository;
|
|
222
237
|
}
|
|
@@ -246,7 +261,9 @@ class EdgeApp {
|
|
|
246
261
|
}
|
|
247
262
|
else {
|
|
248
263
|
// Create temporary SharedApplicationServer for OAuth flow during initial setup
|
|
249
|
-
const serverPort = process.env.CYRUS_SERVER_PORT
|
|
264
|
+
const serverPort = process.env.CYRUS_SERVER_PORT
|
|
265
|
+
? parseInt(process.env.CYRUS_SERVER_PORT, 10)
|
|
266
|
+
: 3456;
|
|
250
267
|
const tempServer = new SharedApplicationServer(serverPort);
|
|
251
268
|
try {
|
|
252
269
|
// Start the server
|
|
@@ -267,7 +284,7 @@ class EdgeApp {
|
|
|
267
284
|
return {
|
|
268
285
|
linearToken: result.linearToken,
|
|
269
286
|
linearWorkspaceId: result.linearWorkspaceId,
|
|
270
|
-
linearWorkspaceName: result.linearWorkspaceName
|
|
287
|
+
linearWorkspaceName: result.linearWorkspaceName,
|
|
271
288
|
};
|
|
272
289
|
}
|
|
273
290
|
finally {
|
|
@@ -300,7 +317,7 @@ class EdgeApp {
|
|
|
300
317
|
console.log(``);
|
|
301
318
|
const rl = readline.createInterface({
|
|
302
319
|
input: process.stdin,
|
|
303
|
-
output: process.stdout
|
|
320
|
+
output: process.stdout,
|
|
304
321
|
});
|
|
305
322
|
return new Promise((resolve) => {
|
|
306
323
|
rl.question(`Enter your ngrok auth token (or press Enter to skip): `, async (token) => {
|
|
@@ -327,7 +344,7 @@ class EdgeApp {
|
|
|
327
344
|
/**
|
|
328
345
|
* Start the EdgeWorker with given configuration
|
|
329
346
|
*/
|
|
330
|
-
async startEdgeWorker({ proxyUrl, repositories }) {
|
|
347
|
+
async startEdgeWorker({ proxyUrl, repositories, }) {
|
|
331
348
|
// Get ngrok auth token (prompt if needed and not external host)
|
|
332
349
|
let ngrokAuthToken;
|
|
333
350
|
if (process.env.CYRUS_HOST_EXTERNAL !== "true") {
|
|
@@ -338,13 +355,15 @@ class EdgeApp {
|
|
|
338
355
|
const config = {
|
|
339
356
|
proxyUrl,
|
|
340
357
|
repositories,
|
|
341
|
-
defaultAllowedTools: process.env.ALLOWED_TOOLS?.split(",").map(t => t.trim()) || [],
|
|
358
|
+
defaultAllowedTools: process.env.ALLOWED_TOOLS?.split(",").map((t) => t.trim()) || [],
|
|
342
359
|
webhookBaseUrl: process.env.CYRUS_BASE_URL,
|
|
343
|
-
serverPort: process.env.CYRUS_SERVER_PORT
|
|
360
|
+
serverPort: process.env.CYRUS_SERVER_PORT
|
|
361
|
+
? parseInt(process.env.CYRUS_SERVER_PORT, 10)
|
|
362
|
+
: 3456,
|
|
344
363
|
serverHost: process.env.CYRUS_HOST_EXTERNAL === "true" ? "0.0.0.0" : "localhost",
|
|
345
364
|
ngrokAuthToken,
|
|
346
365
|
features: {
|
|
347
|
-
enableContinuation: true
|
|
366
|
+
enableContinuation: true,
|
|
348
367
|
},
|
|
349
368
|
handlers: {
|
|
350
369
|
createWorkspace: async (issue, repository) => {
|
|
@@ -354,7 +373,7 @@ class EdgeApp {
|
|
|
354
373
|
const linearCredentials = {
|
|
355
374
|
linearToken: token,
|
|
356
375
|
linearWorkspaceId: workspaceId,
|
|
357
|
-
linearWorkspaceName: workspaceName
|
|
376
|
+
linearWorkspaceName: workspaceName,
|
|
358
377
|
};
|
|
359
378
|
// Handle OAuth completion for repository setup
|
|
360
379
|
if (this.edgeWorker) {
|
|
@@ -363,33 +382,36 @@ class EdgeApp {
|
|
|
363
382
|
try {
|
|
364
383
|
const newRepo = await this.setupRepositoryWizard(linearCredentials);
|
|
365
384
|
// Add to existing repositories
|
|
366
|
-
|
|
385
|
+
const edgeConfig = this.loadEdgeConfig();
|
|
367
386
|
console.log(`š Current config has ${edgeConfig.repositories?.length || 0} repositories`);
|
|
368
|
-
edgeConfig.repositories = [
|
|
387
|
+
edgeConfig.repositories = [
|
|
388
|
+
...(edgeConfig.repositories || []),
|
|
389
|
+
newRepo,
|
|
390
|
+
];
|
|
369
391
|
console.log(`š Adding repository "${newRepo.name}", new total: ${edgeConfig.repositories.length}`);
|
|
370
392
|
this.saveEdgeConfig(edgeConfig);
|
|
371
|
-
console.log(
|
|
372
|
-
console.log(
|
|
373
|
-
console.log(
|
|
393
|
+
console.log("\nā
Repository configured successfully!");
|
|
394
|
+
console.log("š ~/.cyrus/config.json file has been updated with your new repository configuration.");
|
|
395
|
+
console.log("š” You can edit this file and restart Cyrus at any time to modify settings.");
|
|
374
396
|
// Restart edge worker with new config
|
|
375
397
|
await this.edgeWorker.stop();
|
|
376
398
|
this.edgeWorker = null;
|
|
377
399
|
// Give a small delay to ensure file is written
|
|
378
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
400
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
379
401
|
// Reload configuration and restart worker without going through setup
|
|
380
402
|
const updatedConfig = this.loadEdgeConfig();
|
|
381
403
|
console.log(`\nš Reloading with ${updatedConfig.repositories?.length || 0} repositories from config file`);
|
|
382
404
|
return this.startEdgeWorker({
|
|
383
405
|
proxyUrl,
|
|
384
|
-
repositories: updatedConfig.repositories || []
|
|
406
|
+
repositories: updatedConfig.repositories || [],
|
|
385
407
|
});
|
|
386
408
|
}
|
|
387
409
|
catch (error) {
|
|
388
|
-
console.error(
|
|
410
|
+
console.error("\nā Repository setup failed:", error.message);
|
|
389
411
|
}
|
|
390
412
|
}
|
|
391
|
-
}
|
|
392
|
-
}
|
|
413
|
+
},
|
|
414
|
+
},
|
|
393
415
|
};
|
|
394
416
|
// Create and start EdgeWorker
|
|
395
417
|
this.edgeWorker = new EdgeWorker(config);
|
|
@@ -397,10 +419,10 @@ class EdgeApp {
|
|
|
397
419
|
this.setupEventHandlers();
|
|
398
420
|
// Start the worker
|
|
399
421
|
await this.edgeWorker.start();
|
|
400
|
-
console.log(
|
|
422
|
+
console.log("\nā
Edge worker started successfully");
|
|
401
423
|
console.log(`Configured proxy URL: ${config.proxyUrl}`);
|
|
402
424
|
console.log(`Managing ${repositories.length} repositories:`);
|
|
403
|
-
repositories.forEach(repo => {
|
|
425
|
+
repositories.forEach((repo) => {
|
|
404
426
|
console.log(` - ${repo.name} (${repo.repositoryPath})`);
|
|
405
427
|
});
|
|
406
428
|
}
|
|
@@ -410,27 +432,28 @@ class EdgeApp {
|
|
|
410
432
|
async start() {
|
|
411
433
|
try {
|
|
412
434
|
// Set proxy URL with default
|
|
413
|
-
const proxyUrl = process.env.PROXY_URL ||
|
|
435
|
+
const proxyUrl = process.env.PROXY_URL || "https://cyrus-proxy.ceedar.workers.dev";
|
|
414
436
|
// No need to validate Claude CLI - using Claude TypeScript SDK now
|
|
415
437
|
// Load edge configuration
|
|
416
|
-
|
|
438
|
+
const edgeConfig = this.loadEdgeConfig();
|
|
417
439
|
let repositories = edgeConfig.repositories || [];
|
|
418
440
|
// Check if we need to set up
|
|
419
441
|
const needsSetup = repositories.length === 0;
|
|
420
|
-
const hasLinearCredentials = repositories.some(r => r.linearToken) ||
|
|
442
|
+
const hasLinearCredentials = repositories.some((r) => r.linearToken) ||
|
|
443
|
+
process.env.LINEAR_OAUTH_TOKEN;
|
|
421
444
|
if (needsSetup) {
|
|
422
|
-
console.log(
|
|
445
|
+
console.log("š Welcome to Cyrus Edge Worker!");
|
|
423
446
|
// Check if they want to use existing credentials or add new workspace
|
|
424
447
|
let linearCredentials = null;
|
|
425
448
|
if (hasLinearCredentials) {
|
|
426
449
|
// Show available workspaces from existing repos
|
|
427
450
|
const workspaces = new Map();
|
|
428
|
-
for (const repo of
|
|
451
|
+
for (const repo of edgeConfig.repositories || []) {
|
|
429
452
|
if (!workspaces.has(repo.linearWorkspaceId)) {
|
|
430
453
|
workspaces.set(repo.linearWorkspaceId, {
|
|
431
454
|
id: repo.linearWorkspaceId,
|
|
432
|
-
name:
|
|
433
|
-
token: repo.linearToken
|
|
455
|
+
name: "Unknown Workspace",
|
|
456
|
+
token: repo.linearToken,
|
|
434
457
|
});
|
|
435
458
|
}
|
|
436
459
|
}
|
|
@@ -441,24 +464,24 @@ class EdgeApp {
|
|
|
441
464
|
linearCredentials = {
|
|
442
465
|
linearToken: ws.token,
|
|
443
466
|
linearWorkspaceId: ws.id,
|
|
444
|
-
linearWorkspaceName: ws.name
|
|
467
|
+
linearWorkspaceName: ws.name,
|
|
445
468
|
};
|
|
446
469
|
console.log(`\nš Using Linear workspace: ${linearCredentials.linearWorkspaceName}`);
|
|
447
470
|
}
|
|
448
471
|
}
|
|
449
472
|
else if (workspaces.size > 1) {
|
|
450
473
|
// Multiple workspaces, let user choose
|
|
451
|
-
console.log(
|
|
474
|
+
console.log("\nš Available Linear workspaces:");
|
|
452
475
|
const workspaceList = Array.from(workspaces.values());
|
|
453
476
|
workspaceList.forEach((ws, i) => {
|
|
454
477
|
console.log(`${i + 1}. ${ws.name}`);
|
|
455
478
|
});
|
|
456
479
|
const rl = readline.createInterface({
|
|
457
480
|
input: process.stdin,
|
|
458
|
-
output: process.stdout
|
|
481
|
+
output: process.stdout,
|
|
459
482
|
});
|
|
460
|
-
const choice = await new Promise(resolve => {
|
|
461
|
-
rl.question(
|
|
483
|
+
const choice = await new Promise((resolve) => {
|
|
484
|
+
rl.question("\nSelect workspace (number) or press Enter for new: ", resolve);
|
|
462
485
|
});
|
|
463
486
|
rl.close();
|
|
464
487
|
const index = parseInt(choice) - 1;
|
|
@@ -468,7 +491,7 @@ class EdgeApp {
|
|
|
468
491
|
linearCredentials = {
|
|
469
492
|
linearToken: ws.token,
|
|
470
493
|
linearWorkspaceId: ws.id,
|
|
471
|
-
linearWorkspaceName: ws.name
|
|
494
|
+
linearWorkspaceName: ws.name,
|
|
472
495
|
};
|
|
473
496
|
console.log(`Using workspace: ${linearCredentials.linearWorkspaceName}`);
|
|
474
497
|
}
|
|
@@ -482,56 +505,56 @@ class EdgeApp {
|
|
|
482
505
|
// Use env vars
|
|
483
506
|
linearCredentials = {
|
|
484
507
|
linearToken: process.env.LINEAR_OAUTH_TOKEN,
|
|
485
|
-
linearWorkspaceId: process.env.LINEAR_WORKSPACE_ID ||
|
|
486
|
-
linearWorkspaceName:
|
|
508
|
+
linearWorkspaceId: process.env.LINEAR_WORKSPACE_ID || "unknown",
|
|
509
|
+
linearWorkspaceName: "Your Workspace",
|
|
487
510
|
};
|
|
488
511
|
}
|
|
489
512
|
if (linearCredentials) {
|
|
490
|
-
console.log(
|
|
513
|
+
console.log("(OAuth server will start with EdgeWorker to connect additional workspaces)");
|
|
491
514
|
}
|
|
492
515
|
}
|
|
493
516
|
else {
|
|
494
517
|
// Get new Linear credentials
|
|
495
|
-
console.log(
|
|
496
|
-
console.log(
|
|
518
|
+
console.log("\nš Step 1: Connect to Linear");
|
|
519
|
+
console.log("ā".repeat(50));
|
|
497
520
|
try {
|
|
498
521
|
linearCredentials = await this.startOAuthFlow(proxyUrl);
|
|
499
|
-
console.log(
|
|
522
|
+
console.log("\nā
Linear connected successfully!");
|
|
500
523
|
}
|
|
501
524
|
catch (error) {
|
|
502
|
-
console.error(
|
|
503
|
-
console.log(
|
|
504
|
-
console.log(
|
|
505
|
-
console.log(
|
|
506
|
-
console.log(
|
|
525
|
+
console.error("\nā OAuth flow failed:", error.message);
|
|
526
|
+
console.log("\nAlternatively, you can:");
|
|
527
|
+
console.log("1. Visit", `${proxyUrl}/oauth/authorize`, "in your browser");
|
|
528
|
+
console.log("2. Copy the token after authorization");
|
|
529
|
+
console.log("3. Add it to your .env.cyrus file as LINEAR_OAUTH_TOKEN");
|
|
507
530
|
process.exit(1);
|
|
508
531
|
}
|
|
509
532
|
}
|
|
510
533
|
if (!linearCredentials) {
|
|
511
|
-
console.error(
|
|
534
|
+
console.error("ā No Linear credentials available");
|
|
512
535
|
process.exit(1);
|
|
513
536
|
}
|
|
514
537
|
// Now set up repository
|
|
515
|
-
console.log(
|
|
516
|
-
console.log(
|
|
538
|
+
console.log("\nš Step 2: Configure Repository");
|
|
539
|
+
console.log("ā".repeat(50));
|
|
517
540
|
try {
|
|
518
541
|
const newRepo = await this.setupRepositoryWizard(linearCredentials);
|
|
519
542
|
// Add to repositories
|
|
520
543
|
repositories = [...(edgeConfig.repositories || []), newRepo];
|
|
521
544
|
edgeConfig.repositories = repositories;
|
|
522
545
|
this.saveEdgeConfig(edgeConfig);
|
|
523
|
-
console.log(
|
|
524
|
-
console.log(
|
|
525
|
-
console.log(
|
|
546
|
+
console.log("\nā
Repository configured successfully!");
|
|
547
|
+
console.log("š ~/.cyrus/config.json file has been updated with your repository configuration.");
|
|
548
|
+
console.log("š” You can edit this file and restart Cyrus at any time to modify settings.");
|
|
526
549
|
// Ask if they want to add another
|
|
527
550
|
const rl = readline.createInterface({
|
|
528
551
|
input: process.stdin,
|
|
529
|
-
output: process.stdout
|
|
552
|
+
output: process.stdout,
|
|
530
553
|
});
|
|
531
|
-
const addAnother = await new Promise(resolve => {
|
|
532
|
-
rl.question(
|
|
554
|
+
const addAnother = await new Promise((resolve) => {
|
|
555
|
+
rl.question("\nAdd another repository? (y/N): ", (answer) => {
|
|
533
556
|
rl.close();
|
|
534
|
-
resolve(answer.toLowerCase() ===
|
|
557
|
+
resolve(answer.toLowerCase() === "y");
|
|
535
558
|
});
|
|
536
559
|
});
|
|
537
560
|
if (addAnother) {
|
|
@@ -540,14 +563,14 @@ class EdgeApp {
|
|
|
540
563
|
}
|
|
541
564
|
}
|
|
542
565
|
catch (error) {
|
|
543
|
-
console.error(
|
|
566
|
+
console.error("\nā Repository setup failed:", error.message);
|
|
544
567
|
process.exit(1);
|
|
545
568
|
}
|
|
546
569
|
}
|
|
547
570
|
// Validate we have repositories
|
|
548
571
|
if (repositories.length === 0) {
|
|
549
|
-
console.error(
|
|
550
|
-
console.log(
|
|
572
|
+
console.error("ā No repositories configured");
|
|
573
|
+
console.log("\nUse the authorization link above to configure your first repository.");
|
|
551
574
|
process.exit(1);
|
|
552
575
|
}
|
|
553
576
|
// Start the edge worker
|
|
@@ -558,48 +581,75 @@ class EdgeApp {
|
|
|
558
581
|
console.log(`\nš OAuth server running on port ${serverPort}`);
|
|
559
582
|
console.log(`š To authorize Linear (new workspace or re-auth):`);
|
|
560
583
|
console.log(` ${proxyUrl}/oauth/authorize?callback=${oauthCallbackBaseUrl}/callback`);
|
|
561
|
-
console.log(
|
|
584
|
+
console.log("ā".repeat(70));
|
|
562
585
|
// Handle graceful shutdown
|
|
563
|
-
process.on(
|
|
564
|
-
process.on(
|
|
586
|
+
process.on("SIGINT", () => this.shutdown());
|
|
587
|
+
process.on("SIGTERM", () => this.shutdown());
|
|
565
588
|
// Handle uncaught exceptions and unhandled promise rejections
|
|
566
|
-
process.on(
|
|
567
|
-
console.error(
|
|
568
|
-
console.error(
|
|
569
|
-
console.error(
|
|
570
|
-
console.error(
|
|
589
|
+
process.on("uncaughtException", (error) => {
|
|
590
|
+
console.error("šØ Uncaught Exception:", error.message);
|
|
591
|
+
console.error("Error type:", error.constructor.name);
|
|
592
|
+
console.error("Stack:", error.stack);
|
|
593
|
+
console.error("This error was caught by the global handler, preventing application crash");
|
|
571
594
|
// Attempt graceful shutdown but don't wait indefinitely
|
|
572
595
|
this.shutdown().finally(() => {
|
|
573
|
-
console.error(
|
|
596
|
+
console.error("Process exiting due to uncaught exception");
|
|
574
597
|
process.exit(1);
|
|
575
598
|
});
|
|
576
599
|
});
|
|
577
|
-
process.on(
|
|
578
|
-
console.error(
|
|
579
|
-
console.error(
|
|
580
|
-
console.error(
|
|
600
|
+
process.on("unhandledRejection", (reason, promise) => {
|
|
601
|
+
console.error("šØ Unhandled Promise Rejection at:", promise);
|
|
602
|
+
console.error("Reason:", reason);
|
|
603
|
+
console.error("This rejection was caught by the global handler, continuing operation");
|
|
581
604
|
// Log stack trace if reason is an Error
|
|
582
605
|
if (reason instanceof Error && reason.stack) {
|
|
583
|
-
console.error(
|
|
606
|
+
console.error("Stack:", reason.stack);
|
|
584
607
|
}
|
|
585
608
|
// Log the error but don't exit the process for promise rejections
|
|
586
609
|
// as they might be recoverable
|
|
587
610
|
});
|
|
588
611
|
}
|
|
589
612
|
catch (error) {
|
|
590
|
-
console.error(
|
|
613
|
+
console.error("\nā Failed to start edge application:", error.message);
|
|
591
614
|
// 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(
|
|
615
|
+
if (error.message?.includes("Failed to connect any repositories")) {
|
|
616
|
+
console.error("\nš” This usually happens when:");
|
|
617
|
+
console.error(" - All Linear OAuth tokens have expired");
|
|
618
|
+
console.error(" - The Linear API is temporarily unavailable");
|
|
619
|
+
console.error(" - Your network connection is having issues");
|
|
620
|
+
console.error("\nPlease check your edge configuration and try again.");
|
|
598
621
|
}
|
|
599
622
|
await this.shutdown();
|
|
600
623
|
process.exit(1);
|
|
601
624
|
}
|
|
602
625
|
}
|
|
626
|
+
/**
|
|
627
|
+
* Check if a branch exists locally or remotely
|
|
628
|
+
*/
|
|
629
|
+
async branchExists(branchName, repoPath) {
|
|
630
|
+
const { execSync } = await import("node:child_process");
|
|
631
|
+
try {
|
|
632
|
+
// Check if branch exists locally
|
|
633
|
+
execSync(`git rev-parse --verify "${branchName}"`, {
|
|
634
|
+
cwd: repoPath,
|
|
635
|
+
stdio: "pipe",
|
|
636
|
+
});
|
|
637
|
+
return true;
|
|
638
|
+
}
|
|
639
|
+
catch {
|
|
640
|
+
// Branch doesn't exist locally, check remote
|
|
641
|
+
try {
|
|
642
|
+
execSync(`git ls-remote --heads origin "${branchName}"`, {
|
|
643
|
+
cwd: repoPath,
|
|
644
|
+
stdio: "pipe",
|
|
645
|
+
});
|
|
646
|
+
return true;
|
|
647
|
+
}
|
|
648
|
+
catch {
|
|
649
|
+
return false;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
603
653
|
/**
|
|
604
654
|
* Set up event handlers for EdgeWorker
|
|
605
655
|
*/
|
|
@@ -607,69 +657,70 @@ class EdgeApp {
|
|
|
607
657
|
if (!this.edgeWorker)
|
|
608
658
|
return;
|
|
609
659
|
// Session events
|
|
610
|
-
this.edgeWorker.on(
|
|
660
|
+
this.edgeWorker.on("session:started", (issueId, _issue, repositoryId) => {
|
|
611
661
|
console.log(`Started session for issue ${issueId} in repository ${repositoryId}`);
|
|
612
662
|
});
|
|
613
|
-
this.edgeWorker.on(
|
|
663
|
+
this.edgeWorker.on("session:ended", (issueId, exitCode, repositoryId) => {
|
|
614
664
|
console.log(`Session for issue ${issueId} ended with exit code ${exitCode} in repository ${repositoryId}`);
|
|
615
665
|
});
|
|
616
666
|
// Connection events
|
|
617
|
-
this.edgeWorker.on(
|
|
667
|
+
this.edgeWorker.on("connected", (token) => {
|
|
618
668
|
console.log(`ā
Connected to proxy with token ending in ...${token.slice(-4)}`);
|
|
619
669
|
});
|
|
620
|
-
this.edgeWorker.on(
|
|
621
|
-
console.error(`ā Disconnected from proxy (token ...${token.slice(-4)}): ${reason ||
|
|
670
|
+
this.edgeWorker.on("disconnected", (token, reason) => {
|
|
671
|
+
console.error(`ā Disconnected from proxy (token ...${token.slice(-4)}): ${reason || "Unknown reason"}`);
|
|
622
672
|
});
|
|
623
673
|
// Error events
|
|
624
|
-
this.edgeWorker.on(
|
|
625
|
-
console.error(
|
|
674
|
+
this.edgeWorker.on("error", (error) => {
|
|
675
|
+
console.error("EdgeWorker error:", error);
|
|
626
676
|
});
|
|
627
677
|
}
|
|
628
678
|
/**
|
|
629
679
|
* Create a git worktree for an issue
|
|
630
680
|
*/
|
|
631
681
|
async createGitWorktree(issue, repository) {
|
|
632
|
-
const { execSync } = await import(
|
|
633
|
-
const { existsSync } = await import(
|
|
634
|
-
const { join } = await import(
|
|
682
|
+
const { execSync } = await import("node:child_process");
|
|
683
|
+
const { existsSync } = await import("node:fs");
|
|
684
|
+
const { join } = await import("node:path");
|
|
635
685
|
try {
|
|
636
686
|
// Verify this is a git repository
|
|
637
687
|
try {
|
|
638
|
-
execSync(
|
|
688
|
+
execSync("git rev-parse --git-dir", {
|
|
639
689
|
cwd: repository.repositoryPath,
|
|
640
|
-
stdio:
|
|
690
|
+
stdio: "pipe",
|
|
641
691
|
});
|
|
642
692
|
}
|
|
643
|
-
catch (
|
|
693
|
+
catch (_e) {
|
|
644
694
|
console.error(`${repository.repositoryPath} is not a git repository`);
|
|
645
|
-
throw new Error(
|
|
695
|
+
throw new Error("Not a git repository");
|
|
646
696
|
}
|
|
647
697
|
// Sanitize branch name by removing backticks to prevent command injection
|
|
648
|
-
const sanitizeBranchName = (name) => name ? name.replace(/`/g,
|
|
698
|
+
const sanitizeBranchName = (name) => name ? name.replace(/`/g, "") : name;
|
|
649
699
|
// Use Linear's preferred branch name, or generate one if not available
|
|
650
|
-
const rawBranchName = issue.branchName ||
|
|
700
|
+
const rawBranchName = issue.branchName ||
|
|
701
|
+
`${issue.identifier}-${issue.title?.toLowerCase().replace(/\s+/g, "-").substring(0, 30)}`;
|
|
651
702
|
const branchName = sanitizeBranchName(rawBranchName);
|
|
652
703
|
const workspacePath = join(repository.workspaceBaseDir, issue.identifier);
|
|
653
704
|
// Ensure workspace directory exists
|
|
654
705
|
execSync(`mkdir -p "${repository.workspaceBaseDir}"`, {
|
|
655
706
|
cwd: repository.repositoryPath,
|
|
656
|
-
stdio:
|
|
707
|
+
stdio: "pipe",
|
|
657
708
|
});
|
|
658
709
|
// Check if worktree already exists
|
|
659
710
|
try {
|
|
660
|
-
const worktrees = execSync(
|
|
711
|
+
const worktrees = execSync("git worktree list --porcelain", {
|
|
661
712
|
cwd: repository.repositoryPath,
|
|
662
|
-
encoding:
|
|
713
|
+
encoding: "utf-8",
|
|
663
714
|
});
|
|
664
715
|
if (worktrees.includes(workspacePath)) {
|
|
665
716
|
console.log(`Worktree already exists at ${workspacePath}, using existing`);
|
|
666
717
|
return {
|
|
667
718
|
path: workspacePath,
|
|
668
|
-
isGitWorktree: true
|
|
719
|
+
isGitWorktree: true,
|
|
669
720
|
};
|
|
670
721
|
}
|
|
671
722
|
}
|
|
672
|
-
catch (
|
|
723
|
+
catch (_e) {
|
|
673
724
|
// git worktree command failed, continue with creation
|
|
674
725
|
}
|
|
675
726
|
// Check if branch already exists
|
|
@@ -677,38 +728,65 @@ class EdgeApp {
|
|
|
677
728
|
try {
|
|
678
729
|
execSync(`git rev-parse --verify "${branchName}"`, {
|
|
679
730
|
cwd: repository.repositoryPath,
|
|
680
|
-
stdio:
|
|
731
|
+
stdio: "pipe",
|
|
681
732
|
});
|
|
682
733
|
createBranch = false;
|
|
683
734
|
}
|
|
684
|
-
catch (
|
|
735
|
+
catch (_e) {
|
|
685
736
|
// Branch doesn't exist, we'll create it
|
|
686
737
|
}
|
|
738
|
+
// Determine base branch for this issue
|
|
739
|
+
let baseBranch = repository.baseBranch;
|
|
740
|
+
// Check if issue has a parent
|
|
741
|
+
try {
|
|
742
|
+
const parent = await issue.parent;
|
|
743
|
+
if (parent) {
|
|
744
|
+
console.log(`Issue ${issue.identifier} has parent: ${parent.identifier}`);
|
|
745
|
+
// Get parent's branch name
|
|
746
|
+
const parentRawBranchName = parent.branchName ||
|
|
747
|
+
`${parent.identifier}-${parent.title?.toLowerCase().replace(/\s+/g, "-").substring(0, 30)}`;
|
|
748
|
+
const parentBranchName = sanitizeBranchName(parentRawBranchName);
|
|
749
|
+
// Check if parent branch exists
|
|
750
|
+
const parentBranchExists = await this.branchExists(parentBranchName, repository.repositoryPath);
|
|
751
|
+
if (parentBranchExists) {
|
|
752
|
+
baseBranch = parentBranchName;
|
|
753
|
+
console.log(`Using parent issue branch '${parentBranchName}' as base for sub-issue ${issue.identifier}`);
|
|
754
|
+
}
|
|
755
|
+
else {
|
|
756
|
+
console.log(`Parent branch '${parentBranchName}' not found, using default base branch '${repository.baseBranch}'`);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
catch (_error) {
|
|
761
|
+
// Parent field might not exist or couldn't be fetched, use default base branch
|
|
762
|
+
console.log(`No parent issue found for ${issue.identifier}, using default base branch '${repository.baseBranch}'`);
|
|
763
|
+
}
|
|
687
764
|
// Fetch latest changes from remote
|
|
688
|
-
console.log(
|
|
765
|
+
console.log("Fetching latest changes from remote...");
|
|
689
766
|
let hasRemote = true;
|
|
690
767
|
try {
|
|
691
|
-
execSync(
|
|
768
|
+
execSync("git fetch origin", {
|
|
692
769
|
cwd: repository.repositoryPath,
|
|
693
|
-
stdio:
|
|
770
|
+
stdio: "pipe",
|
|
694
771
|
});
|
|
695
772
|
}
|
|
696
773
|
catch (e) {
|
|
697
|
-
console.warn(
|
|
774
|
+
console.warn("Warning: git fetch failed, proceeding with local branch:", e.message);
|
|
698
775
|
hasRemote = false;
|
|
699
776
|
}
|
|
700
|
-
// Create the worktree - use
|
|
777
|
+
// Create the worktree - use determined base branch
|
|
701
778
|
let worktreeCmd;
|
|
702
779
|
if (createBranch) {
|
|
703
780
|
if (hasRemote) {
|
|
704
|
-
|
|
781
|
+
// Always prefer remote version if available
|
|
782
|
+
const remoteBranch = `origin/${baseBranch}`;
|
|
705
783
|
console.log(`Creating git worktree at ${workspacePath} from ${remoteBranch}`);
|
|
706
784
|
worktreeCmd = `git worktree add "${workspacePath}" -b "${branchName}" "${remoteBranch}"`;
|
|
707
785
|
}
|
|
708
786
|
else {
|
|
709
|
-
// No remote, use local
|
|
710
|
-
console.log(`Creating git worktree at ${workspacePath} from local ${
|
|
711
|
-
worktreeCmd = `git worktree add "${workspacePath}" -b "${branchName}" "${
|
|
787
|
+
// No remote, use local branch
|
|
788
|
+
console.log(`Creating git worktree at ${workspacePath} from local ${baseBranch}`);
|
|
789
|
+
worktreeCmd = `git worktree add "${workspacePath}" -b "${branchName}" "${baseBranch}"`;
|
|
712
790
|
}
|
|
713
791
|
}
|
|
714
792
|
else {
|
|
@@ -718,42 +796,42 @@ class EdgeApp {
|
|
|
718
796
|
}
|
|
719
797
|
execSync(worktreeCmd, {
|
|
720
798
|
cwd: repository.repositoryPath,
|
|
721
|
-
stdio:
|
|
799
|
+
stdio: "pipe",
|
|
722
800
|
});
|
|
723
801
|
// Check for cyrus-setup.sh script in the repository root
|
|
724
|
-
const setupScriptPath = join(repository.repositoryPath,
|
|
802
|
+
const setupScriptPath = join(repository.repositoryPath, "cyrus-setup.sh");
|
|
725
803
|
if (existsSync(setupScriptPath)) {
|
|
726
|
-
console.log(
|
|
804
|
+
console.log("Running cyrus-setup.sh in new worktree...");
|
|
727
805
|
try {
|
|
728
|
-
execSync(
|
|
806
|
+
execSync("bash cyrus-setup.sh", {
|
|
729
807
|
cwd: workspacePath,
|
|
730
|
-
stdio:
|
|
808
|
+
stdio: "inherit",
|
|
731
809
|
env: {
|
|
732
810
|
...process.env,
|
|
733
811
|
LINEAR_ISSUE_ID: issue.id,
|
|
734
812
|
LINEAR_ISSUE_IDENTIFIER: issue.identifier,
|
|
735
|
-
LINEAR_ISSUE_TITLE: issue.title ||
|
|
736
|
-
}
|
|
813
|
+
LINEAR_ISSUE_TITLE: issue.title || "",
|
|
814
|
+
},
|
|
737
815
|
});
|
|
738
816
|
}
|
|
739
817
|
catch (error) {
|
|
740
|
-
console.warn(
|
|
818
|
+
console.warn("Warning: cyrus-setup.sh failed:", error.message);
|
|
741
819
|
// Continue despite setup script failure
|
|
742
820
|
}
|
|
743
821
|
}
|
|
744
822
|
return {
|
|
745
823
|
path: workspacePath,
|
|
746
|
-
isGitWorktree: true
|
|
824
|
+
isGitWorktree: true,
|
|
747
825
|
};
|
|
748
826
|
}
|
|
749
827
|
catch (error) {
|
|
750
|
-
console.error(
|
|
828
|
+
console.error("Failed to create git worktree:", error.message);
|
|
751
829
|
// Fall back to regular directory if git worktree fails
|
|
752
830
|
const fallbackPath = join(repository.workspaceBaseDir, issue.identifier);
|
|
753
|
-
execSync(`mkdir -p "${fallbackPath}"`, { stdio:
|
|
831
|
+
execSync(`mkdir -p "${fallbackPath}"`, { stdio: "pipe" });
|
|
754
832
|
return {
|
|
755
833
|
path: fallbackPath,
|
|
756
|
-
isGitWorktree: false
|
|
834
|
+
isGitWorktree: false,
|
|
757
835
|
};
|
|
758
836
|
}
|
|
759
837
|
}
|
|
@@ -764,31 +842,34 @@ class EdgeApp {
|
|
|
764
842
|
if (this.isShuttingDown)
|
|
765
843
|
return;
|
|
766
844
|
this.isShuttingDown = true;
|
|
767
|
-
console.log(
|
|
845
|
+
console.log("\nShutting down edge worker...");
|
|
768
846
|
// Stop edge worker (includes stopping shared application server)
|
|
769
847
|
if (this.edgeWorker) {
|
|
770
848
|
await this.edgeWorker.stop();
|
|
771
849
|
}
|
|
772
|
-
console.log(
|
|
850
|
+
console.log("Shutdown complete");
|
|
773
851
|
process.exit(0);
|
|
774
852
|
}
|
|
775
853
|
}
|
|
776
854
|
// Helper function to check Linear token status
|
|
777
855
|
async function checkLinearToken(token) {
|
|
778
856
|
try {
|
|
779
|
-
const response = await fetch(
|
|
780
|
-
method:
|
|
857
|
+
const response = await fetch("https://api.linear.app/graphql", {
|
|
858
|
+
method: "POST",
|
|
781
859
|
headers: {
|
|
782
|
-
|
|
783
|
-
|
|
860
|
+
"Content-Type": "application/json",
|
|
861
|
+
Authorization: token,
|
|
784
862
|
},
|
|
785
863
|
body: JSON.stringify({
|
|
786
|
-
query:
|
|
787
|
-
})
|
|
864
|
+
query: "{ viewer { id email name } }",
|
|
865
|
+
}),
|
|
788
866
|
});
|
|
789
|
-
const data = await response.json();
|
|
867
|
+
const data = (await response.json());
|
|
790
868
|
if (data.errors) {
|
|
791
|
-
return {
|
|
869
|
+
return {
|
|
870
|
+
valid: false,
|
|
871
|
+
error: data.errors[0]?.message || "Unknown error",
|
|
872
|
+
};
|
|
792
873
|
}
|
|
793
874
|
return { valid: true };
|
|
794
875
|
}
|
|
@@ -801,16 +882,16 @@ async function checkTokensCommand() {
|
|
|
801
882
|
const app = new EdgeApp();
|
|
802
883
|
const configPath = app.getEdgeConfigPath();
|
|
803
884
|
if (!existsSync(configPath)) {
|
|
804
|
-
console.error(
|
|
885
|
+
console.error("No edge configuration found. Please run setup first.");
|
|
805
886
|
process.exit(1);
|
|
806
887
|
}
|
|
807
|
-
const config = JSON.parse(readFileSync(configPath,
|
|
808
|
-
console.log(
|
|
888
|
+
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
889
|
+
console.log("Checking Linear tokens...\n");
|
|
809
890
|
for (const repo of config.repositories) {
|
|
810
891
|
process.stdout.write(`${repo.name} (${repo.linearWorkspaceName}): `);
|
|
811
892
|
const result = await checkLinearToken(repo.linearToken);
|
|
812
893
|
if (result.valid) {
|
|
813
|
-
console.log(
|
|
894
|
+
console.log("ā
Valid");
|
|
814
895
|
}
|
|
815
896
|
else {
|
|
816
897
|
console.log(`ā Invalid - ${result.error}`);
|
|
@@ -822,34 +903,34 @@ async function refreshTokenCommand() {
|
|
|
822
903
|
const app = new EdgeApp();
|
|
823
904
|
const configPath = app.getEdgeConfigPath();
|
|
824
905
|
if (!existsSync(configPath)) {
|
|
825
|
-
console.error(
|
|
906
|
+
console.error("No edge configuration found. Please run setup first.");
|
|
826
907
|
process.exit(1);
|
|
827
908
|
}
|
|
828
|
-
const config = JSON.parse(readFileSync(configPath,
|
|
909
|
+
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
829
910
|
// Show repositories with their token status
|
|
830
|
-
console.log(
|
|
911
|
+
console.log("Checking current token status...\n");
|
|
831
912
|
const tokenStatuses = [];
|
|
832
913
|
for (const repo of config.repositories) {
|
|
833
914
|
const result = await checkLinearToken(repo.linearToken);
|
|
834
915
|
tokenStatuses.push({ repo, valid: result.valid });
|
|
835
|
-
console.log(`${tokenStatuses.length}. ${repo.name} (${repo.linearWorkspaceName}): ${result.valid ?
|
|
916
|
+
console.log(`${tokenStatuses.length}. ${repo.name} (${repo.linearWorkspaceName}): ${result.valid ? "ā
Valid" : "ā Invalid"}`);
|
|
836
917
|
}
|
|
837
918
|
// Ask which token to refresh
|
|
838
919
|
const rl = readline.createInterface({
|
|
839
920
|
input: process.stdin,
|
|
840
|
-
output: process.stdout
|
|
921
|
+
output: process.stdout,
|
|
841
922
|
});
|
|
842
|
-
const answer = await new Promise(resolve => {
|
|
923
|
+
const answer = await new Promise((resolve) => {
|
|
843
924
|
rl.question('\nWhich repository token would you like to refresh? (Enter number or "all"): ', resolve);
|
|
844
925
|
});
|
|
845
926
|
const indicesToRefresh = [];
|
|
846
|
-
if (answer.toLowerCase() ===
|
|
927
|
+
if (answer.toLowerCase() === "all") {
|
|
847
928
|
indicesToRefresh.push(...Array.from({ length: tokenStatuses.length }, (_, i) => i));
|
|
848
929
|
}
|
|
849
930
|
else {
|
|
850
931
|
const index = parseInt(answer) - 1;
|
|
851
|
-
if (isNaN(index) || index < 0 || index >= tokenStatuses.length) {
|
|
852
|
-
console.error(
|
|
932
|
+
if (Number.isNaN(index) || index < 0 || index >= tokenStatuses.length) {
|
|
933
|
+
console.error("Invalid selection");
|
|
853
934
|
rl.close();
|
|
854
935
|
process.exit(1);
|
|
855
936
|
}
|
|
@@ -862,9 +943,11 @@ async function refreshTokenCommand() {
|
|
|
862
943
|
continue;
|
|
863
944
|
const { repo } = tokenStatus;
|
|
864
945
|
console.log(`\nRefreshing token for ${repo.name} (${repo.linearWorkspaceName || repo.linearWorkspaceId})...`);
|
|
865
|
-
console.log(
|
|
946
|
+
console.log("Opening Linear OAuth flow in your browser...");
|
|
866
947
|
// Use the proxy's OAuth flow with a callback to localhost
|
|
867
|
-
const serverPort = process.env.CYRUS_SERVER_PORT
|
|
948
|
+
const serverPort = process.env.CYRUS_SERVER_PORT
|
|
949
|
+
? parseInt(process.env.CYRUS_SERVER_PORT, 10)
|
|
950
|
+
: 3456;
|
|
868
951
|
const callbackUrl = `http://localhost:${serverPort}/callback`;
|
|
869
952
|
const oauthUrl = `https://cyrus-proxy.ceedar.workers.dev/oauth/authorize?callback=${encodeURIComponent(callbackUrl)}`;
|
|
870
953
|
console.log(`\nPlease complete the OAuth flow in your browser.`);
|
|
@@ -873,10 +956,10 @@ async function refreshTokenCommand() {
|
|
|
873
956
|
let tokenReceived = null;
|
|
874
957
|
const server = await new Promise((resolve) => {
|
|
875
958
|
const s = http.createServer((req, res) => {
|
|
876
|
-
if (req.url?.startsWith(
|
|
959
|
+
if (req.url?.startsWith("/callback")) {
|
|
877
960
|
const url = new URL(req.url, `http://localhost:${serverPort}`);
|
|
878
|
-
tokenReceived = url.searchParams.get(
|
|
879
|
-
res.writeHead(200, {
|
|
961
|
+
tokenReceived = url.searchParams.get("token");
|
|
962
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
880
963
|
res.end(`
|
|
881
964
|
<html>
|
|
882
965
|
<head>
|
|
@@ -892,11 +975,11 @@ async function refreshTokenCommand() {
|
|
|
892
975
|
}
|
|
893
976
|
else {
|
|
894
977
|
res.writeHead(404);
|
|
895
|
-
res.end(
|
|
978
|
+
res.end("Not found");
|
|
896
979
|
}
|
|
897
980
|
});
|
|
898
981
|
s.listen(serverPort, () => {
|
|
899
|
-
console.log(
|
|
982
|
+
console.log("Waiting for OAuth callback...");
|
|
900
983
|
resolve(s);
|
|
901
984
|
});
|
|
902
985
|
});
|
|
@@ -904,12 +987,12 @@ async function refreshTokenCommand() {
|
|
|
904
987
|
// Wait for the token with timeout
|
|
905
988
|
const startTime = Date.now();
|
|
906
989
|
while (!tokenReceived && Date.now() - startTime < 120000) {
|
|
907
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
990
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
908
991
|
}
|
|
909
992
|
server.close();
|
|
910
993
|
const newToken = tokenReceived;
|
|
911
|
-
if (!newToken || !newToken.startsWith(
|
|
912
|
-
console.error(
|
|
994
|
+
if (!newToken || !newToken.startsWith("lin_oauth_")) {
|
|
995
|
+
console.error("Invalid token received from OAuth flow");
|
|
913
996
|
continue;
|
|
914
997
|
}
|
|
915
998
|
// Verify the new token
|
|
@@ -935,33 +1018,33 @@ async function refreshTokenCommand() {
|
|
|
935
1018
|
}
|
|
936
1019
|
// Save the updated config
|
|
937
1020
|
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
938
|
-
console.log(
|
|
1021
|
+
console.log("\nā
Configuration saved");
|
|
939
1022
|
rl.close();
|
|
940
1023
|
}
|
|
941
1024
|
// Parse command
|
|
942
|
-
const command = args[0] ||
|
|
1025
|
+
const command = args[0] || "start";
|
|
943
1026
|
// Execute appropriate command
|
|
944
1027
|
switch (command) {
|
|
945
|
-
case
|
|
946
|
-
checkTokensCommand().catch(error => {
|
|
947
|
-
console.error(
|
|
1028
|
+
case "check-tokens":
|
|
1029
|
+
checkTokensCommand().catch((error) => {
|
|
1030
|
+
console.error("Error:", error);
|
|
948
1031
|
process.exit(1);
|
|
949
1032
|
});
|
|
950
1033
|
break;
|
|
951
|
-
case
|
|
952
|
-
refreshTokenCommand().catch(error => {
|
|
953
|
-
console.error(
|
|
1034
|
+
case "refresh-token":
|
|
1035
|
+
refreshTokenCommand().catch((error) => {
|
|
1036
|
+
console.error("Error:", error);
|
|
954
1037
|
process.exit(1);
|
|
955
1038
|
});
|
|
956
1039
|
break;
|
|
957
|
-
|
|
958
|
-
default:
|
|
1040
|
+
default: {
|
|
959
1041
|
// Create and start the app
|
|
960
1042
|
const app = new EdgeApp();
|
|
961
|
-
app.start().catch(error => {
|
|
962
|
-
console.error(
|
|
1043
|
+
app.start().catch((error) => {
|
|
1044
|
+
console.error("Fatal error:", error);
|
|
963
1045
|
process.exit(1);
|
|
964
1046
|
});
|
|
965
1047
|
break;
|
|
1048
|
+
}
|
|
966
1049
|
}
|
|
967
1050
|
//# sourceMappingURL=app.js.map
|