panopticon-cli 0.1.3 → 0.1.6
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 +408 -9
- package/dist/chunk-RM3WGTFO.js +1058 -0
- package/dist/chunk-RM3WGTFO.js.map +1 -0
- package/dist/cli/index.js +1725 -717
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +368 -5
- package/dist/index.js +43 -1
- package/package.json +2 -1
- package/templates/traefik/README.md +106 -0
- package/templates/traefik/docker-compose.yml +40 -0
- package/templates/traefik/dynamic/panopticon.yml +51 -0
- package/templates/traefik/dynamic/workspace.yml.template +34 -0
- package/templates/traefik/traefik.yml +45 -0
- package/dist/chunk-FR2P66GU.js +0 -352
- package/dist/chunk-FR2P66GU.js.map +0 -1
|
@@ -0,0 +1,1058 @@
|
|
|
1
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
2
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
3
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
4
|
+
}) : x)(function(x) {
|
|
5
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
6
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
+
});
|
|
8
|
+
var __commonJS = (cb, mod) => function __require2() {
|
|
9
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/lib/paths.ts
|
|
13
|
+
import { homedir } from "os";
|
|
14
|
+
import { join } from "path";
|
|
15
|
+
import { fileURLToPath } from "url";
|
|
16
|
+
import { dirname } from "path";
|
|
17
|
+
var PANOPTICON_HOME = join(homedir(), ".panopticon");
|
|
18
|
+
var CONFIG_DIR = PANOPTICON_HOME;
|
|
19
|
+
var SKILLS_DIR = join(PANOPTICON_HOME, "skills");
|
|
20
|
+
var COMMANDS_DIR = join(PANOPTICON_HOME, "commands");
|
|
21
|
+
var AGENTS_DIR = join(PANOPTICON_HOME, "agents");
|
|
22
|
+
var BACKUPS_DIR = join(PANOPTICON_HOME, "backups");
|
|
23
|
+
var COSTS_DIR = join(PANOPTICON_HOME, "costs");
|
|
24
|
+
var TRAEFIK_DIR = join(PANOPTICON_HOME, "traefik");
|
|
25
|
+
var TRAEFIK_DYNAMIC_DIR = join(TRAEFIK_DIR, "dynamic");
|
|
26
|
+
var TRAEFIK_CERTS_DIR = join(TRAEFIK_DIR, "certs");
|
|
27
|
+
var CERTS_DIR = join(PANOPTICON_HOME, "certs");
|
|
28
|
+
var CONFIG_FILE = join(CONFIG_DIR, "config.toml");
|
|
29
|
+
var CLAUDE_DIR = join(homedir(), ".claude");
|
|
30
|
+
var CODEX_DIR = join(homedir(), ".codex");
|
|
31
|
+
var CURSOR_DIR = join(homedir(), ".cursor");
|
|
32
|
+
var GEMINI_DIR = join(homedir(), ".gemini");
|
|
33
|
+
var SYNC_TARGETS = {
|
|
34
|
+
claude: {
|
|
35
|
+
skills: join(CLAUDE_DIR, "skills"),
|
|
36
|
+
commands: join(CLAUDE_DIR, "commands")
|
|
37
|
+
},
|
|
38
|
+
codex: {
|
|
39
|
+
skills: join(CODEX_DIR, "skills"),
|
|
40
|
+
commands: join(CODEX_DIR, "commands")
|
|
41
|
+
},
|
|
42
|
+
cursor: {
|
|
43
|
+
skills: join(CURSOR_DIR, "skills"),
|
|
44
|
+
commands: join(CURSOR_DIR, "commands")
|
|
45
|
+
},
|
|
46
|
+
gemini: {
|
|
47
|
+
skills: join(GEMINI_DIR, "skills"),
|
|
48
|
+
commands: join(GEMINI_DIR, "commands")
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
var TEMPLATES_DIR = join(PANOPTICON_HOME, "templates");
|
|
52
|
+
var CLAUDE_MD_TEMPLATES = join(TEMPLATES_DIR, "claude-md", "sections");
|
|
53
|
+
var currentFile = fileURLToPath(import.meta.url);
|
|
54
|
+
var distDir = dirname(currentFile);
|
|
55
|
+
var packageRoot = dirname(distDir);
|
|
56
|
+
var SOURCE_TEMPLATES_DIR = join(packageRoot, "templates");
|
|
57
|
+
var SOURCE_TRAEFIK_TEMPLATES = join(SOURCE_TEMPLATES_DIR, "traefik");
|
|
58
|
+
var INIT_DIRS = [
|
|
59
|
+
PANOPTICON_HOME,
|
|
60
|
+
SKILLS_DIR,
|
|
61
|
+
COMMANDS_DIR,
|
|
62
|
+
AGENTS_DIR,
|
|
63
|
+
BACKUPS_DIR,
|
|
64
|
+
COSTS_DIR,
|
|
65
|
+
TEMPLATES_DIR,
|
|
66
|
+
CLAUDE_MD_TEMPLATES,
|
|
67
|
+
CERTS_DIR,
|
|
68
|
+
TRAEFIK_DIR,
|
|
69
|
+
TRAEFIK_DYNAMIC_DIR,
|
|
70
|
+
TRAEFIK_CERTS_DIR
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
// src/lib/config.ts
|
|
74
|
+
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
75
|
+
import { parse, stringify } from "@iarna/toml";
|
|
76
|
+
var DEFAULT_CONFIG = {
|
|
77
|
+
panopticon: {
|
|
78
|
+
version: "1.0.0"
|
|
79
|
+
},
|
|
80
|
+
sync: {
|
|
81
|
+
targets: ["claude"],
|
|
82
|
+
backup_before_sync: true,
|
|
83
|
+
auto_sync: false,
|
|
84
|
+
strategy: "symlink"
|
|
85
|
+
},
|
|
86
|
+
trackers: {
|
|
87
|
+
primary: "linear",
|
|
88
|
+
linear: {
|
|
89
|
+
type: "linear",
|
|
90
|
+
api_key_env: "LINEAR_API_KEY"
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
dashboard: {
|
|
94
|
+
port: 3001,
|
|
95
|
+
api_port: 3002
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
function loadConfig() {
|
|
99
|
+
if (!existsSync(CONFIG_FILE)) {
|
|
100
|
+
return DEFAULT_CONFIG;
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
const content = readFileSync(CONFIG_FILE, "utf8");
|
|
104
|
+
const parsed = parse(content);
|
|
105
|
+
return { ...DEFAULT_CONFIG, ...parsed };
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.error("Warning: Failed to parse config, using defaults");
|
|
108
|
+
return DEFAULT_CONFIG;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function saveConfig(config) {
|
|
112
|
+
const content = stringify(config);
|
|
113
|
+
writeFileSync(CONFIG_FILE, content, "utf8");
|
|
114
|
+
}
|
|
115
|
+
function getDefaultConfig() {
|
|
116
|
+
return { ...DEFAULT_CONFIG };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// src/lib/shell.ts
|
|
120
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, appendFileSync } from "fs";
|
|
121
|
+
import { homedir as homedir2 } from "os";
|
|
122
|
+
import { join as join2 } from "path";
|
|
123
|
+
function detectShell() {
|
|
124
|
+
const shell = process.env.SHELL || "";
|
|
125
|
+
if (shell.includes("zsh")) return "zsh";
|
|
126
|
+
if (shell.includes("bash")) return "bash";
|
|
127
|
+
if (shell.includes("fish")) return "fish";
|
|
128
|
+
return "unknown";
|
|
129
|
+
}
|
|
130
|
+
function getShellRcFile(shell) {
|
|
131
|
+
const home = homedir2();
|
|
132
|
+
switch (shell) {
|
|
133
|
+
case "zsh":
|
|
134
|
+
return join2(home, ".zshrc");
|
|
135
|
+
case "bash":
|
|
136
|
+
const bashrc = join2(home, ".bashrc");
|
|
137
|
+
if (existsSync2(bashrc)) return bashrc;
|
|
138
|
+
return join2(home, ".bash_profile");
|
|
139
|
+
case "fish":
|
|
140
|
+
return join2(home, ".config", "fish", "config.fish");
|
|
141
|
+
default:
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
var ALIAS_LINE = 'alias pan="panopticon"';
|
|
146
|
+
var ALIAS_MARKER = "# Panopticon CLI alias";
|
|
147
|
+
function hasAlias(rcFile) {
|
|
148
|
+
if (!existsSync2(rcFile)) return false;
|
|
149
|
+
const content = readFileSync2(rcFile, "utf8");
|
|
150
|
+
return content.includes(ALIAS_MARKER) || content.includes(ALIAS_LINE);
|
|
151
|
+
}
|
|
152
|
+
function addAlias(rcFile) {
|
|
153
|
+
if (hasAlias(rcFile)) return;
|
|
154
|
+
const aliasBlock = `
|
|
155
|
+
${ALIAS_MARKER}
|
|
156
|
+
${ALIAS_LINE}
|
|
157
|
+
`;
|
|
158
|
+
appendFileSync(rcFile, aliasBlock, "utf8");
|
|
159
|
+
}
|
|
160
|
+
function getAliasInstructions(shell) {
|
|
161
|
+
const rcFile = getShellRcFile(shell);
|
|
162
|
+
if (!rcFile) {
|
|
163
|
+
return `Add this to your shell config:
|
|
164
|
+
${ALIAS_LINE}`;
|
|
165
|
+
}
|
|
166
|
+
return `Alias added to ${rcFile}. Run:
|
|
167
|
+
source ${rcFile}`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// src/lib/backup.ts
|
|
171
|
+
import { existsSync as existsSync3, mkdirSync, readdirSync, cpSync, rmSync } from "fs";
|
|
172
|
+
import { join as join3, basename } from "path";
|
|
173
|
+
function createBackupTimestamp() {
|
|
174
|
+
return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
175
|
+
}
|
|
176
|
+
function createBackup(sourceDirs) {
|
|
177
|
+
const timestamp = createBackupTimestamp();
|
|
178
|
+
const backupPath = join3(BACKUPS_DIR, timestamp);
|
|
179
|
+
mkdirSync(backupPath, { recursive: true });
|
|
180
|
+
const targets = [];
|
|
181
|
+
for (const sourceDir of sourceDirs) {
|
|
182
|
+
if (!existsSync3(sourceDir)) continue;
|
|
183
|
+
const targetName = basename(sourceDir);
|
|
184
|
+
const targetPath = join3(backupPath, targetName);
|
|
185
|
+
cpSync(sourceDir, targetPath, { recursive: true });
|
|
186
|
+
targets.push(targetName);
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
timestamp,
|
|
190
|
+
path: backupPath,
|
|
191
|
+
targets
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function listBackups() {
|
|
195
|
+
if (!existsSync3(BACKUPS_DIR)) return [];
|
|
196
|
+
const entries = readdirSync(BACKUPS_DIR, { withFileTypes: true });
|
|
197
|
+
return entries.filter((e) => e.isDirectory()).map((e) => {
|
|
198
|
+
const backupPath = join3(BACKUPS_DIR, e.name);
|
|
199
|
+
const contents = readdirSync(backupPath);
|
|
200
|
+
return {
|
|
201
|
+
timestamp: e.name,
|
|
202
|
+
path: backupPath,
|
|
203
|
+
targets: contents
|
|
204
|
+
};
|
|
205
|
+
}).sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
206
|
+
}
|
|
207
|
+
function restoreBackup(timestamp, targetDirs) {
|
|
208
|
+
const backupPath = join3(BACKUPS_DIR, timestamp);
|
|
209
|
+
if (!existsSync3(backupPath)) {
|
|
210
|
+
throw new Error(`Backup not found: ${timestamp}`);
|
|
211
|
+
}
|
|
212
|
+
const contents = readdirSync(backupPath, { withFileTypes: true });
|
|
213
|
+
for (const entry of contents) {
|
|
214
|
+
if (!entry.isDirectory()) continue;
|
|
215
|
+
const sourcePath = join3(backupPath, entry.name);
|
|
216
|
+
const targetPath = targetDirs[entry.name];
|
|
217
|
+
if (!targetPath) continue;
|
|
218
|
+
if (existsSync3(targetPath)) {
|
|
219
|
+
rmSync(targetPath, { recursive: true });
|
|
220
|
+
}
|
|
221
|
+
cpSync(sourcePath, targetPath, { recursive: true });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function cleanOldBackups(keepCount = 10) {
|
|
225
|
+
const backups = listBackups();
|
|
226
|
+
if (backups.length <= keepCount) return 0;
|
|
227
|
+
const toRemove = backups.slice(keepCount);
|
|
228
|
+
let removed = 0;
|
|
229
|
+
for (const backup of toRemove) {
|
|
230
|
+
rmSync(backup.path, { recursive: true });
|
|
231
|
+
removed++;
|
|
232
|
+
}
|
|
233
|
+
return removed;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// src/lib/sync.ts
|
|
237
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync as readdirSync2, symlinkSync, unlinkSync, lstatSync, readlinkSync, rmSync as rmSync2 } from "fs";
|
|
238
|
+
import { join as join4 } from "path";
|
|
239
|
+
function removeTarget(targetPath) {
|
|
240
|
+
const stats = lstatSync(targetPath);
|
|
241
|
+
if (stats.isDirectory() && !stats.isSymbolicLink()) {
|
|
242
|
+
rmSync2(targetPath, { recursive: true, force: true });
|
|
243
|
+
} else {
|
|
244
|
+
unlinkSync(targetPath);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function isPanopticonSymlink(targetPath) {
|
|
248
|
+
if (!existsSync4(targetPath)) return false;
|
|
249
|
+
try {
|
|
250
|
+
const stats = lstatSync(targetPath);
|
|
251
|
+
if (!stats.isSymbolicLink()) return false;
|
|
252
|
+
const linkTarget = readlinkSync(targetPath);
|
|
253
|
+
return linkTarget.includes(".panopticon");
|
|
254
|
+
} catch {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function planSync(runtime) {
|
|
259
|
+
const targets = SYNC_TARGETS[runtime];
|
|
260
|
+
const plan = {
|
|
261
|
+
runtime,
|
|
262
|
+
skills: [],
|
|
263
|
+
commands: []
|
|
264
|
+
};
|
|
265
|
+
if (existsSync4(SKILLS_DIR)) {
|
|
266
|
+
const skills = readdirSync2(SKILLS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
267
|
+
for (const skill of skills) {
|
|
268
|
+
const sourcePath = join4(SKILLS_DIR, skill.name);
|
|
269
|
+
const targetPath = join4(targets.skills, skill.name);
|
|
270
|
+
let status = "new";
|
|
271
|
+
if (existsSync4(targetPath)) {
|
|
272
|
+
if (isPanopticonSymlink(targetPath)) {
|
|
273
|
+
status = "symlink";
|
|
274
|
+
} else {
|
|
275
|
+
status = "conflict";
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
plan.skills.push({ name: skill.name, sourcePath, targetPath, status });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (existsSync4(COMMANDS_DIR)) {
|
|
282
|
+
const commands = readdirSync2(COMMANDS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
283
|
+
for (const cmd of commands) {
|
|
284
|
+
const sourcePath = join4(COMMANDS_DIR, cmd.name);
|
|
285
|
+
const targetPath = join4(targets.commands, cmd.name);
|
|
286
|
+
let status = "new";
|
|
287
|
+
if (existsSync4(targetPath)) {
|
|
288
|
+
if (isPanopticonSymlink(targetPath)) {
|
|
289
|
+
status = "symlink";
|
|
290
|
+
} else {
|
|
291
|
+
status = "conflict";
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
plan.commands.push({ name: cmd.name, sourcePath, targetPath, status });
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return plan;
|
|
298
|
+
}
|
|
299
|
+
function executeSync(runtime, options = {}) {
|
|
300
|
+
const plan = planSync(runtime);
|
|
301
|
+
const result = {
|
|
302
|
+
created: [],
|
|
303
|
+
skipped: [],
|
|
304
|
+
conflicts: []
|
|
305
|
+
};
|
|
306
|
+
const targets = SYNC_TARGETS[runtime];
|
|
307
|
+
mkdirSync2(targets.skills, { recursive: true });
|
|
308
|
+
mkdirSync2(targets.commands, { recursive: true });
|
|
309
|
+
for (const item of plan.skills) {
|
|
310
|
+
if (options.dryRun) {
|
|
311
|
+
if (item.status === "new" || item.status === "symlink") {
|
|
312
|
+
result.created.push(item.name);
|
|
313
|
+
} else {
|
|
314
|
+
result.conflicts.push(item.name);
|
|
315
|
+
}
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (item.status === "conflict" && !options.force) {
|
|
319
|
+
result.conflicts.push(item.name);
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
if (existsSync4(item.targetPath)) {
|
|
323
|
+
removeTarget(item.targetPath);
|
|
324
|
+
}
|
|
325
|
+
symlinkSync(item.sourcePath, item.targetPath);
|
|
326
|
+
result.created.push(item.name);
|
|
327
|
+
}
|
|
328
|
+
for (const item of plan.commands) {
|
|
329
|
+
if (options.dryRun) {
|
|
330
|
+
if (item.status === "new" || item.status === "symlink") {
|
|
331
|
+
result.created.push(item.name);
|
|
332
|
+
} else {
|
|
333
|
+
result.conflicts.push(item.name);
|
|
334
|
+
}
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
if (item.status === "conflict" && !options.force) {
|
|
338
|
+
result.conflicts.push(item.name);
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
if (existsSync4(item.targetPath)) {
|
|
342
|
+
removeTarget(item.targetPath);
|
|
343
|
+
}
|
|
344
|
+
symlinkSync(item.sourcePath, item.targetPath);
|
|
345
|
+
result.created.push(item.name);
|
|
346
|
+
}
|
|
347
|
+
return result;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// src/lib/tracker/interface.ts
|
|
351
|
+
var NotImplementedError = class extends Error {
|
|
352
|
+
constructor(feature) {
|
|
353
|
+
super(`Not implemented: ${feature}`);
|
|
354
|
+
this.name = "NotImplementedError";
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
var IssueNotFoundError = class extends Error {
|
|
358
|
+
constructor(id, tracker) {
|
|
359
|
+
super(`Issue not found: ${id} (tracker: ${tracker})`);
|
|
360
|
+
this.name = "IssueNotFoundError";
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
var TrackerAuthError = class extends Error {
|
|
364
|
+
constructor(tracker, message) {
|
|
365
|
+
super(`Authentication failed for ${tracker}: ${message}`);
|
|
366
|
+
this.name = "TrackerAuthError";
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
// src/lib/tracker/linear.ts
|
|
371
|
+
import { LinearClient } from "@linear/sdk";
|
|
372
|
+
var STATE_MAP = {
|
|
373
|
+
backlog: "open",
|
|
374
|
+
unstarted: "open",
|
|
375
|
+
started: "in_progress",
|
|
376
|
+
completed: "closed",
|
|
377
|
+
canceled: "closed"
|
|
378
|
+
};
|
|
379
|
+
var LinearTracker = class {
|
|
380
|
+
name = "linear";
|
|
381
|
+
client;
|
|
382
|
+
defaultTeam;
|
|
383
|
+
constructor(apiKey, options) {
|
|
384
|
+
if (!apiKey) {
|
|
385
|
+
throw new TrackerAuthError("linear", "API key is required");
|
|
386
|
+
}
|
|
387
|
+
this.client = new LinearClient({ apiKey });
|
|
388
|
+
this.defaultTeam = options?.team;
|
|
389
|
+
}
|
|
390
|
+
async listIssues(filters) {
|
|
391
|
+
const team = filters?.team ?? this.defaultTeam;
|
|
392
|
+
const result = await this.client.issues({
|
|
393
|
+
first: filters?.limit ?? 50,
|
|
394
|
+
filter: {
|
|
395
|
+
team: team ? { key: { eq: team } } : void 0,
|
|
396
|
+
state: filters?.state ? { type: { eq: this.reverseMapState(filters.state) } } : filters?.includeClosed ? void 0 : { type: { neq: "completed" } },
|
|
397
|
+
labels: filters?.labels?.length ? { name: { in: filters.labels } } : void 0,
|
|
398
|
+
assignee: filters?.assignee ? { name: { containsIgnoreCase: filters.assignee } } : void 0
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
const issues = [];
|
|
402
|
+
for (const node of result.nodes) {
|
|
403
|
+
issues.push(await this.normalizeIssue(node));
|
|
404
|
+
}
|
|
405
|
+
return issues;
|
|
406
|
+
}
|
|
407
|
+
async getIssue(id) {
|
|
408
|
+
try {
|
|
409
|
+
const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id);
|
|
410
|
+
if (isUuid) {
|
|
411
|
+
const issue = await this.client.issue(id);
|
|
412
|
+
if (issue) {
|
|
413
|
+
return this.normalizeIssue(issue);
|
|
414
|
+
}
|
|
415
|
+
} else {
|
|
416
|
+
const match = id.match(/^([A-Z]+)-(\d+)$/i);
|
|
417
|
+
if (match) {
|
|
418
|
+
const [, teamKey, number] = match;
|
|
419
|
+
const results = await this.client.searchIssues(id, { first: 1 });
|
|
420
|
+
if (results.nodes.length > 0) {
|
|
421
|
+
return this.normalizeIssue(results.nodes[0]);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
throw new IssueNotFoundError(id, "linear");
|
|
426
|
+
} catch (error) {
|
|
427
|
+
if (error instanceof IssueNotFoundError) throw error;
|
|
428
|
+
throw new IssueNotFoundError(id, "linear");
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
async updateIssue(id, update) {
|
|
432
|
+
const issue = await this.getIssue(id);
|
|
433
|
+
const updatePayload = {};
|
|
434
|
+
if (update.title !== void 0) {
|
|
435
|
+
updatePayload.title = update.title;
|
|
436
|
+
}
|
|
437
|
+
if (update.description !== void 0) {
|
|
438
|
+
updatePayload.description = update.description;
|
|
439
|
+
}
|
|
440
|
+
if (update.priority !== void 0) {
|
|
441
|
+
updatePayload.priority = update.priority;
|
|
442
|
+
}
|
|
443
|
+
if (update.dueDate !== void 0) {
|
|
444
|
+
updatePayload.dueDate = update.dueDate;
|
|
445
|
+
}
|
|
446
|
+
if (update.state !== void 0) {
|
|
447
|
+
await this.transitionIssue(id, update.state);
|
|
448
|
+
}
|
|
449
|
+
if (update.labels !== void 0) {
|
|
450
|
+
}
|
|
451
|
+
if (Object.keys(updatePayload).length > 0) {
|
|
452
|
+
await this.client.updateIssue(issue.id, updatePayload);
|
|
453
|
+
}
|
|
454
|
+
return this.getIssue(id);
|
|
455
|
+
}
|
|
456
|
+
async createIssue(newIssue) {
|
|
457
|
+
const team = newIssue.team ?? this.defaultTeam;
|
|
458
|
+
if (!team) {
|
|
459
|
+
throw new Error("Team is required to create an issue");
|
|
460
|
+
}
|
|
461
|
+
const teams = await this.client.teams({
|
|
462
|
+
filter: { key: { eq: team } }
|
|
463
|
+
});
|
|
464
|
+
if (teams.nodes.length === 0) {
|
|
465
|
+
throw new Error(`Team not found: ${team}`);
|
|
466
|
+
}
|
|
467
|
+
const teamId = teams.nodes[0].id;
|
|
468
|
+
const result = await this.client.createIssue({
|
|
469
|
+
teamId,
|
|
470
|
+
title: newIssue.title,
|
|
471
|
+
description: newIssue.description,
|
|
472
|
+
priority: newIssue.priority,
|
|
473
|
+
dueDate: newIssue.dueDate
|
|
474
|
+
});
|
|
475
|
+
const created = await result.issue;
|
|
476
|
+
if (!created) {
|
|
477
|
+
throw new Error("Failed to create issue");
|
|
478
|
+
}
|
|
479
|
+
return this.normalizeIssue(created);
|
|
480
|
+
}
|
|
481
|
+
async getComments(issueId) {
|
|
482
|
+
const issue = await this.client.issue(issueId);
|
|
483
|
+
const comments = await issue.comments();
|
|
484
|
+
return comments.nodes.map((c) => ({
|
|
485
|
+
id: c.id,
|
|
486
|
+
issueId,
|
|
487
|
+
body: c.body,
|
|
488
|
+
author: c.user?.then((u) => u?.name ?? "Unknown"),
|
|
489
|
+
// Simplified
|
|
490
|
+
createdAt: c.createdAt.toISOString(),
|
|
491
|
+
updatedAt: c.updatedAt.toISOString()
|
|
492
|
+
}));
|
|
493
|
+
}
|
|
494
|
+
async addComment(issueId, body) {
|
|
495
|
+
const result = await this.client.createComment({
|
|
496
|
+
issueId,
|
|
497
|
+
body
|
|
498
|
+
});
|
|
499
|
+
const comment = await result.comment;
|
|
500
|
+
if (!comment) {
|
|
501
|
+
throw new Error("Failed to create comment");
|
|
502
|
+
}
|
|
503
|
+
return {
|
|
504
|
+
id: comment.id,
|
|
505
|
+
issueId,
|
|
506
|
+
body: comment.body,
|
|
507
|
+
author: "Panopticon",
|
|
508
|
+
// Simplified
|
|
509
|
+
createdAt: comment.createdAt.toISOString(),
|
|
510
|
+
updatedAt: comment.updatedAt.toISOString()
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
async transitionIssue(id, state) {
|
|
514
|
+
const issue = await this.getIssue(id);
|
|
515
|
+
const linearIssue = await this.client.issue(issue.id);
|
|
516
|
+
const team = await linearIssue.team;
|
|
517
|
+
if (!team) {
|
|
518
|
+
throw new Error("Could not determine issue team");
|
|
519
|
+
}
|
|
520
|
+
const states = await team.states();
|
|
521
|
+
const targetStateType = this.reverseMapState(state);
|
|
522
|
+
const targetState = states.nodes.find((s) => s.type === targetStateType);
|
|
523
|
+
if (!targetState) {
|
|
524
|
+
throw new Error(`No state found matching type: ${targetStateType}`);
|
|
525
|
+
}
|
|
526
|
+
await this.client.updateIssue(issue.id, {
|
|
527
|
+
stateId: targetState.id
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
async linkPR(issueId, prUrl) {
|
|
531
|
+
const issue = await this.getIssue(issueId);
|
|
532
|
+
await this.client.createAttachment({
|
|
533
|
+
issueId: issue.id,
|
|
534
|
+
title: "Pull Request",
|
|
535
|
+
url: prUrl
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
async normalizeIssue(linearIssue) {
|
|
539
|
+
const state = await linearIssue.state;
|
|
540
|
+
const assignee = await linearIssue.assignee;
|
|
541
|
+
const labels = await linearIssue.labels();
|
|
542
|
+
let dueDate;
|
|
543
|
+
if (linearIssue.dueDate) {
|
|
544
|
+
dueDate = linearIssue.dueDate instanceof Date ? linearIssue.dueDate.toISOString() : String(linearIssue.dueDate);
|
|
545
|
+
}
|
|
546
|
+
return {
|
|
547
|
+
id: linearIssue.id,
|
|
548
|
+
ref: linearIssue.identifier,
|
|
549
|
+
title: linearIssue.title,
|
|
550
|
+
description: linearIssue.description ?? "",
|
|
551
|
+
state: this.mapState(state?.type ?? "backlog"),
|
|
552
|
+
labels: labels?.nodes?.map((l) => l.name) ?? [],
|
|
553
|
+
assignee: assignee?.name,
|
|
554
|
+
url: linearIssue.url,
|
|
555
|
+
tracker: "linear",
|
|
556
|
+
priority: linearIssue.priority,
|
|
557
|
+
dueDate,
|
|
558
|
+
createdAt: linearIssue.createdAt instanceof Date ? linearIssue.createdAt.toISOString() : String(linearIssue.createdAt),
|
|
559
|
+
updatedAt: linearIssue.updatedAt instanceof Date ? linearIssue.updatedAt.toISOString() : String(linearIssue.updatedAt)
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
mapState(linearState) {
|
|
563
|
+
return STATE_MAP[linearState] ?? "open";
|
|
564
|
+
}
|
|
565
|
+
reverseMapState(state) {
|
|
566
|
+
switch (state) {
|
|
567
|
+
case "open":
|
|
568
|
+
return "unstarted";
|
|
569
|
+
case "in_progress":
|
|
570
|
+
return "started";
|
|
571
|
+
case "closed":
|
|
572
|
+
return "completed";
|
|
573
|
+
default:
|
|
574
|
+
return "unstarted";
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
// src/lib/tracker/github.ts
|
|
580
|
+
import { Octokit } from "@octokit/rest";
|
|
581
|
+
var GitHubTracker = class {
|
|
582
|
+
name = "github";
|
|
583
|
+
octokit;
|
|
584
|
+
owner;
|
|
585
|
+
repo;
|
|
586
|
+
constructor(token, owner, repo) {
|
|
587
|
+
if (!token) {
|
|
588
|
+
throw new TrackerAuthError("github", "Token is required");
|
|
589
|
+
}
|
|
590
|
+
if (!owner || !repo) {
|
|
591
|
+
throw new Error("GitHub owner and repo are required");
|
|
592
|
+
}
|
|
593
|
+
this.octokit = new Octokit({ auth: token });
|
|
594
|
+
this.owner = owner;
|
|
595
|
+
this.repo = repo;
|
|
596
|
+
}
|
|
597
|
+
async listIssues(filters) {
|
|
598
|
+
const state = this.mapStateToGitHub(filters?.state);
|
|
599
|
+
const response = await this.octokit.issues.listForRepo({
|
|
600
|
+
owner: this.owner,
|
|
601
|
+
repo: this.repo,
|
|
602
|
+
state: filters?.includeClosed ? "all" : state,
|
|
603
|
+
labels: filters?.labels?.join(",") || void 0,
|
|
604
|
+
assignee: filters?.assignee || void 0,
|
|
605
|
+
per_page: filters?.limit ?? 50
|
|
606
|
+
});
|
|
607
|
+
const issues = response.data.filter((item) => !item.pull_request);
|
|
608
|
+
return issues.map((issue) => this.normalizeIssue(issue));
|
|
609
|
+
}
|
|
610
|
+
async getIssue(id) {
|
|
611
|
+
try {
|
|
612
|
+
const issueNumber = parseInt(id.replace(/^#/, ""), 10);
|
|
613
|
+
if (isNaN(issueNumber)) {
|
|
614
|
+
throw new IssueNotFoundError(id, "github");
|
|
615
|
+
}
|
|
616
|
+
const { data: issue } = await this.octokit.issues.get({
|
|
617
|
+
owner: this.owner,
|
|
618
|
+
repo: this.repo,
|
|
619
|
+
issue_number: issueNumber
|
|
620
|
+
});
|
|
621
|
+
return this.normalizeIssue(issue);
|
|
622
|
+
} catch (error) {
|
|
623
|
+
if (error?.status === 404) {
|
|
624
|
+
throw new IssueNotFoundError(id, "github");
|
|
625
|
+
}
|
|
626
|
+
throw error;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
async updateIssue(id, update) {
|
|
630
|
+
const issueNumber = parseInt(id.replace(/^#/, ""), 10);
|
|
631
|
+
const updatePayload = {};
|
|
632
|
+
if (update.title !== void 0) {
|
|
633
|
+
updatePayload.title = update.title;
|
|
634
|
+
}
|
|
635
|
+
if (update.description !== void 0) {
|
|
636
|
+
updatePayload.body = update.description;
|
|
637
|
+
}
|
|
638
|
+
if (update.state !== void 0) {
|
|
639
|
+
updatePayload.state = update.state === "closed" ? "closed" : "open";
|
|
640
|
+
}
|
|
641
|
+
if (update.labels !== void 0) {
|
|
642
|
+
updatePayload.labels = update.labels;
|
|
643
|
+
}
|
|
644
|
+
if (update.assignee !== void 0) {
|
|
645
|
+
updatePayload.assignees = update.assignee ? [update.assignee] : [];
|
|
646
|
+
}
|
|
647
|
+
await this.octokit.issues.update({
|
|
648
|
+
owner: this.owner,
|
|
649
|
+
repo: this.repo,
|
|
650
|
+
issue_number: issueNumber,
|
|
651
|
+
...updatePayload
|
|
652
|
+
});
|
|
653
|
+
return this.getIssue(id);
|
|
654
|
+
}
|
|
655
|
+
async createIssue(newIssue) {
|
|
656
|
+
const { data: issue } = await this.octokit.issues.create({
|
|
657
|
+
owner: this.owner,
|
|
658
|
+
repo: this.repo,
|
|
659
|
+
title: newIssue.title,
|
|
660
|
+
body: newIssue.description,
|
|
661
|
+
labels: newIssue.labels,
|
|
662
|
+
assignees: newIssue.assignee ? [newIssue.assignee] : void 0
|
|
663
|
+
});
|
|
664
|
+
return this.normalizeIssue(issue);
|
|
665
|
+
}
|
|
666
|
+
async getComments(issueId) {
|
|
667
|
+
const issueNumber = parseInt(issueId.replace(/^#/, ""), 10);
|
|
668
|
+
const { data: comments } = await this.octokit.issues.listComments({
|
|
669
|
+
owner: this.owner,
|
|
670
|
+
repo: this.repo,
|
|
671
|
+
issue_number: issueNumber
|
|
672
|
+
});
|
|
673
|
+
return comments.map((c) => ({
|
|
674
|
+
id: String(c.id),
|
|
675
|
+
issueId,
|
|
676
|
+
body: c.body ?? "",
|
|
677
|
+
author: c.user?.login ?? "Unknown",
|
|
678
|
+
createdAt: c.created_at,
|
|
679
|
+
updatedAt: c.updated_at
|
|
680
|
+
}));
|
|
681
|
+
}
|
|
682
|
+
async addComment(issueId, body) {
|
|
683
|
+
const issueNumber = parseInt(issueId.replace(/^#/, ""), 10);
|
|
684
|
+
const { data: comment } = await this.octokit.issues.createComment({
|
|
685
|
+
owner: this.owner,
|
|
686
|
+
repo: this.repo,
|
|
687
|
+
issue_number: issueNumber,
|
|
688
|
+
body
|
|
689
|
+
});
|
|
690
|
+
return {
|
|
691
|
+
id: String(comment.id),
|
|
692
|
+
issueId,
|
|
693
|
+
body: comment.body ?? "",
|
|
694
|
+
author: comment.user?.login ?? "Unknown",
|
|
695
|
+
createdAt: comment.created_at,
|
|
696
|
+
updatedAt: comment.updated_at
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
async transitionIssue(id, state) {
|
|
700
|
+
await this.updateIssue(id, { state });
|
|
701
|
+
}
|
|
702
|
+
async linkPR(issueId, prUrl) {
|
|
703
|
+
await this.addComment(
|
|
704
|
+
issueId,
|
|
705
|
+
`Linked Pull Request: ${prUrl}`
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
normalizeIssue(ghIssue) {
|
|
709
|
+
return {
|
|
710
|
+
id: String(ghIssue.id),
|
|
711
|
+
ref: `#${ghIssue.number}`,
|
|
712
|
+
title: ghIssue.title,
|
|
713
|
+
description: ghIssue.body ?? "",
|
|
714
|
+
state: this.mapStateFromGitHub(ghIssue.state),
|
|
715
|
+
labels: ghIssue.labels.map(
|
|
716
|
+
(l) => typeof l === "string" ? l : l.name
|
|
717
|
+
),
|
|
718
|
+
assignee: ghIssue.assignee?.login,
|
|
719
|
+
url: ghIssue.html_url,
|
|
720
|
+
tracker: "github",
|
|
721
|
+
priority: void 0,
|
|
722
|
+
// GitHub doesn't have priority
|
|
723
|
+
dueDate: void 0,
|
|
724
|
+
// GitHub doesn't have due dates on issues
|
|
725
|
+
createdAt: ghIssue.created_at,
|
|
726
|
+
updatedAt: ghIssue.updated_at
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
mapStateFromGitHub(ghState) {
|
|
730
|
+
return ghState === "closed" ? "closed" : "open";
|
|
731
|
+
}
|
|
732
|
+
mapStateToGitHub(state) {
|
|
733
|
+
if (!state) return "open";
|
|
734
|
+
if (state === "closed") return "closed";
|
|
735
|
+
return "open";
|
|
736
|
+
}
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
// src/lib/tracker/gitlab.ts
|
|
740
|
+
var GitLabTracker = class {
|
|
741
|
+
constructor(token, projectId) {
|
|
742
|
+
this.token = token;
|
|
743
|
+
this.projectId = projectId;
|
|
744
|
+
}
|
|
745
|
+
name = "gitlab";
|
|
746
|
+
async listIssues(_filters) {
|
|
747
|
+
throw new NotImplementedError(
|
|
748
|
+
"GitLab tracker is not yet implemented. Coming soon!"
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
async getIssue(_id) {
|
|
752
|
+
throw new NotImplementedError(
|
|
753
|
+
"GitLab tracker is not yet implemented. Coming soon!"
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
async updateIssue(_id, _update) {
|
|
757
|
+
throw new NotImplementedError(
|
|
758
|
+
"GitLab tracker is not yet implemented. Coming soon!"
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
async createIssue(_issue) {
|
|
762
|
+
throw new NotImplementedError(
|
|
763
|
+
"GitLab tracker is not yet implemented. Coming soon!"
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
async getComments(_issueId) {
|
|
767
|
+
throw new NotImplementedError(
|
|
768
|
+
"GitLab tracker is not yet implemented. Coming soon!"
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
async addComment(_issueId, _body) {
|
|
772
|
+
throw new NotImplementedError(
|
|
773
|
+
"GitLab tracker is not yet implemented. Coming soon!"
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
async transitionIssue(_id, _state) {
|
|
777
|
+
throw new NotImplementedError(
|
|
778
|
+
"GitLab tracker is not yet implemented. Coming soon!"
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
async linkPR(_issueId, _prUrl) {
|
|
782
|
+
throw new NotImplementedError(
|
|
783
|
+
"GitLab tracker is not yet implemented. Coming soon!"
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
};
|
|
787
|
+
|
|
788
|
+
// src/lib/tracker/factory.ts
|
|
789
|
+
function createTracker(config) {
|
|
790
|
+
switch (config.type) {
|
|
791
|
+
case "linear": {
|
|
792
|
+
const apiKey = config.apiKeyEnv ? process.env[config.apiKeyEnv] : process.env.LINEAR_API_KEY;
|
|
793
|
+
if (!apiKey) {
|
|
794
|
+
throw new TrackerAuthError(
|
|
795
|
+
"linear",
|
|
796
|
+
`API key not found. Set ${config.apiKeyEnv ?? "LINEAR_API_KEY"} environment variable.`
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
return new LinearTracker(apiKey, { team: config.team });
|
|
800
|
+
}
|
|
801
|
+
case "github": {
|
|
802
|
+
const token = config.tokenEnv ? process.env[config.tokenEnv] : process.env.GITHUB_TOKEN;
|
|
803
|
+
if (!token) {
|
|
804
|
+
throw new TrackerAuthError(
|
|
805
|
+
"github",
|
|
806
|
+
`Token not found. Set ${config.tokenEnv ?? "GITHUB_TOKEN"} environment variable.`
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
if (!config.owner || !config.repo) {
|
|
810
|
+
throw new Error(
|
|
811
|
+
"GitHub tracker requires owner and repo configuration"
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
return new GitHubTracker(token, config.owner, config.repo);
|
|
815
|
+
}
|
|
816
|
+
case "gitlab": {
|
|
817
|
+
const token = config.tokenEnv ? process.env[config.tokenEnv] : process.env.GITLAB_TOKEN;
|
|
818
|
+
if (!token) {
|
|
819
|
+
throw new TrackerAuthError(
|
|
820
|
+
"gitlab",
|
|
821
|
+
`Token not found. Set ${config.tokenEnv ?? "GITLAB_TOKEN"} environment variable.`
|
|
822
|
+
);
|
|
823
|
+
}
|
|
824
|
+
if (!config.projectId) {
|
|
825
|
+
throw new Error("GitLab tracker requires projectId configuration");
|
|
826
|
+
}
|
|
827
|
+
return new GitLabTracker(token, config.projectId);
|
|
828
|
+
}
|
|
829
|
+
default:
|
|
830
|
+
throw new Error(`Unknown tracker type: ${config.type}`);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
function createTrackerFromConfig(trackersConfig, trackerType) {
|
|
834
|
+
const config = trackersConfig[trackerType];
|
|
835
|
+
if (!config) {
|
|
836
|
+
throw new Error(
|
|
837
|
+
`No configuration found for tracker: ${trackerType}. Add [trackers.${trackerType}] to config.`
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
return createTracker({ ...config, type: trackerType });
|
|
841
|
+
}
|
|
842
|
+
function getPrimaryTracker(trackersConfig) {
|
|
843
|
+
return createTrackerFromConfig(trackersConfig, trackersConfig.primary);
|
|
844
|
+
}
|
|
845
|
+
function getSecondaryTracker(trackersConfig) {
|
|
846
|
+
if (!trackersConfig.secondary) {
|
|
847
|
+
return null;
|
|
848
|
+
}
|
|
849
|
+
return createTrackerFromConfig(trackersConfig, trackersConfig.secondary);
|
|
850
|
+
}
|
|
851
|
+
function getAllTrackers(trackersConfig) {
|
|
852
|
+
const trackers = [getPrimaryTracker(trackersConfig)];
|
|
853
|
+
const secondary = getSecondaryTracker(trackersConfig);
|
|
854
|
+
if (secondary) {
|
|
855
|
+
trackers.push(secondary);
|
|
856
|
+
}
|
|
857
|
+
return trackers;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// src/lib/tracker/linking.ts
|
|
861
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
|
|
862
|
+
import { join as join5 } from "path";
|
|
863
|
+
import { homedir as homedir3 } from "os";
|
|
864
|
+
function parseIssueRef(ref) {
|
|
865
|
+
if (ref.startsWith("github#")) {
|
|
866
|
+
return { tracker: "github", ref: `#${ref.slice(7)}` };
|
|
867
|
+
}
|
|
868
|
+
if (ref.startsWith("gitlab#")) {
|
|
869
|
+
return { tracker: "gitlab", ref: `#${ref.slice(7)}` };
|
|
870
|
+
}
|
|
871
|
+
if (ref.startsWith("linear:")) {
|
|
872
|
+
return { tracker: "linear", ref: ref.slice(7) };
|
|
873
|
+
}
|
|
874
|
+
if (/^#\d+$/.test(ref)) {
|
|
875
|
+
return { tracker: "github", ref };
|
|
876
|
+
}
|
|
877
|
+
if (/^[A-Z]+-\d+$/i.test(ref)) {
|
|
878
|
+
return { tracker: "linear", ref: ref.toUpperCase() };
|
|
879
|
+
}
|
|
880
|
+
return null;
|
|
881
|
+
}
|
|
882
|
+
function formatIssueRef(ref, tracker) {
|
|
883
|
+
if (tracker === "github") {
|
|
884
|
+
return ref.startsWith("#") ? `github${ref}` : `github#${ref}`;
|
|
885
|
+
}
|
|
886
|
+
if (tracker === "gitlab") {
|
|
887
|
+
return ref.startsWith("#") ? `gitlab${ref}` : `gitlab#${ref}`;
|
|
888
|
+
}
|
|
889
|
+
return ref;
|
|
890
|
+
}
|
|
891
|
+
var LinkManager = class {
|
|
892
|
+
storePath;
|
|
893
|
+
store;
|
|
894
|
+
constructor(storePath) {
|
|
895
|
+
this.storePath = storePath ?? join5(homedir3(), ".panopticon", "links.json");
|
|
896
|
+
this.store = this.load();
|
|
897
|
+
}
|
|
898
|
+
load() {
|
|
899
|
+
if (existsSync5(this.storePath)) {
|
|
900
|
+
try {
|
|
901
|
+
const data = JSON.parse(readFileSync3(this.storePath, "utf-8"));
|
|
902
|
+
if (data.version === 1) {
|
|
903
|
+
return data;
|
|
904
|
+
}
|
|
905
|
+
} catch {
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
return { version: 1, links: [] };
|
|
909
|
+
}
|
|
910
|
+
save() {
|
|
911
|
+
const dir = join5(this.storePath, "..");
|
|
912
|
+
if (!existsSync5(dir)) {
|
|
913
|
+
mkdirSync3(dir, { recursive: true });
|
|
914
|
+
}
|
|
915
|
+
writeFileSync2(this.storePath, JSON.stringify(this.store, null, 2));
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* Add a link between two issues
|
|
919
|
+
*/
|
|
920
|
+
addLink(source, target, direction = "related") {
|
|
921
|
+
const existing = this.store.links.find(
|
|
922
|
+
(l) => l.sourceIssueRef === source.ref && l.sourceTracker === source.tracker && l.targetIssueRef === target.ref && l.targetTracker === target.tracker
|
|
923
|
+
);
|
|
924
|
+
if (existing) {
|
|
925
|
+
if (existing.direction !== direction) {
|
|
926
|
+
existing.direction = direction;
|
|
927
|
+
this.save();
|
|
928
|
+
}
|
|
929
|
+
return existing;
|
|
930
|
+
}
|
|
931
|
+
const link = {
|
|
932
|
+
sourceIssueRef: source.ref,
|
|
933
|
+
sourceTracker: source.tracker,
|
|
934
|
+
targetIssueRef: target.ref,
|
|
935
|
+
targetTracker: target.tracker,
|
|
936
|
+
direction,
|
|
937
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
938
|
+
};
|
|
939
|
+
this.store.links.push(link);
|
|
940
|
+
this.save();
|
|
941
|
+
return link;
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* Remove a link between two issues
|
|
945
|
+
*/
|
|
946
|
+
removeLink(source, target) {
|
|
947
|
+
const index = this.store.links.findIndex(
|
|
948
|
+
(l) => l.sourceIssueRef === source.ref && l.sourceTracker === source.tracker && l.targetIssueRef === target.ref && l.targetTracker === target.tracker
|
|
949
|
+
);
|
|
950
|
+
if (index >= 0) {
|
|
951
|
+
this.store.links.splice(index, 1);
|
|
952
|
+
this.save();
|
|
953
|
+
return true;
|
|
954
|
+
}
|
|
955
|
+
return false;
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Get all issues linked to a given issue
|
|
959
|
+
*/
|
|
960
|
+
getLinkedIssues(ref, tracker) {
|
|
961
|
+
return this.store.links.filter(
|
|
962
|
+
(l) => l.sourceIssueRef === ref && l.sourceTracker === tracker || l.targetIssueRef === ref && l.targetTracker === tracker
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* Get all links (for debugging/admin)
|
|
967
|
+
*/
|
|
968
|
+
getAllLinks() {
|
|
969
|
+
return [...this.store.links];
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* Find linked issue in another tracker
|
|
973
|
+
*/
|
|
974
|
+
findLinkedIssue(ref, sourceTracker, targetTracker) {
|
|
975
|
+
const asSource = this.store.links.find(
|
|
976
|
+
(l) => l.sourceIssueRef === ref && l.sourceTracker === sourceTracker && l.targetTracker === targetTracker
|
|
977
|
+
);
|
|
978
|
+
if (asSource) return asSource.targetIssueRef;
|
|
979
|
+
const asTarget = this.store.links.find(
|
|
980
|
+
(l) => l.targetIssueRef === ref && l.targetTracker === sourceTracker && l.sourceTracker === targetTracker
|
|
981
|
+
);
|
|
982
|
+
if (asTarget) return asTarget.sourceIssueRef;
|
|
983
|
+
return null;
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Clear all links (for testing)
|
|
987
|
+
*/
|
|
988
|
+
clear() {
|
|
989
|
+
this.store.links = [];
|
|
990
|
+
this.save();
|
|
991
|
+
}
|
|
992
|
+
};
|
|
993
|
+
var _linkManager = null;
|
|
994
|
+
function getLinkManager() {
|
|
995
|
+
if (!_linkManager) {
|
|
996
|
+
_linkManager = new LinkManager();
|
|
997
|
+
}
|
|
998
|
+
return _linkManager;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
export {
|
|
1002
|
+
__require,
|
|
1003
|
+
__commonJS,
|
|
1004
|
+
PANOPTICON_HOME,
|
|
1005
|
+
CONFIG_DIR,
|
|
1006
|
+
SKILLS_DIR,
|
|
1007
|
+
COMMANDS_DIR,
|
|
1008
|
+
AGENTS_DIR,
|
|
1009
|
+
BACKUPS_DIR,
|
|
1010
|
+
COSTS_DIR,
|
|
1011
|
+
TRAEFIK_DIR,
|
|
1012
|
+
TRAEFIK_DYNAMIC_DIR,
|
|
1013
|
+
TRAEFIK_CERTS_DIR,
|
|
1014
|
+
CERTS_DIR,
|
|
1015
|
+
CONFIG_FILE,
|
|
1016
|
+
CLAUDE_DIR,
|
|
1017
|
+
CODEX_DIR,
|
|
1018
|
+
CURSOR_DIR,
|
|
1019
|
+
GEMINI_DIR,
|
|
1020
|
+
SYNC_TARGETS,
|
|
1021
|
+
TEMPLATES_DIR,
|
|
1022
|
+
CLAUDE_MD_TEMPLATES,
|
|
1023
|
+
SOURCE_TEMPLATES_DIR,
|
|
1024
|
+
SOURCE_TRAEFIK_TEMPLATES,
|
|
1025
|
+
INIT_DIRS,
|
|
1026
|
+
loadConfig,
|
|
1027
|
+
saveConfig,
|
|
1028
|
+
getDefaultConfig,
|
|
1029
|
+
detectShell,
|
|
1030
|
+
getShellRcFile,
|
|
1031
|
+
hasAlias,
|
|
1032
|
+
addAlias,
|
|
1033
|
+
getAliasInstructions,
|
|
1034
|
+
createBackupTimestamp,
|
|
1035
|
+
createBackup,
|
|
1036
|
+
listBackups,
|
|
1037
|
+
restoreBackup,
|
|
1038
|
+
cleanOldBackups,
|
|
1039
|
+
isPanopticonSymlink,
|
|
1040
|
+
planSync,
|
|
1041
|
+
executeSync,
|
|
1042
|
+
NotImplementedError,
|
|
1043
|
+
IssueNotFoundError,
|
|
1044
|
+
TrackerAuthError,
|
|
1045
|
+
LinearTracker,
|
|
1046
|
+
GitHubTracker,
|
|
1047
|
+
GitLabTracker,
|
|
1048
|
+
createTracker,
|
|
1049
|
+
createTrackerFromConfig,
|
|
1050
|
+
getPrimaryTracker,
|
|
1051
|
+
getSecondaryTracker,
|
|
1052
|
+
getAllTrackers,
|
|
1053
|
+
parseIssueRef,
|
|
1054
|
+
formatIssueRef,
|
|
1055
|
+
LinkManager,
|
|
1056
|
+
getLinkManager
|
|
1057
|
+
};
|
|
1058
|
+
//# sourceMappingURL=chunk-RM3WGTFO.js.map
|