swarm-tickets 1.0.0
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.txt +21 -0
- package/README.md +238 -0
- package/SKILL.md +235 -0
- package/backup-tickets.sh +39 -0
- package/package.json +54 -0
- package/setup.js +60 -0
- package/ticket-cli.js +120 -0
- package/ticket-server.js +327 -0
- package/ticket-tracker.html +755 -0
- package/tickets.example.json +74 -0
package/ticket-cli.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const readline = require('readline');
|
|
4
|
+
const fs = require('fs').promises;
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const TICKETS_FILE = path.join(__dirname, 'tickets.json');
|
|
8
|
+
|
|
9
|
+
const rl = readline.createInterface({
|
|
10
|
+
input: process.stdin,
|
|
11
|
+
output: process.stdout
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
function question(prompt) {
|
|
15
|
+
return new Promise((resolve) => {
|
|
16
|
+
rl.question(prompt, resolve);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function readTickets() {
|
|
21
|
+
try {
|
|
22
|
+
const data = await fs.readFile(TICKETS_FILE, 'utf8');
|
|
23
|
+
return JSON.parse(data);
|
|
24
|
+
} catch {
|
|
25
|
+
return { tickets: [] };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function writeTickets(data) {
|
|
30
|
+
await fs.writeFile(TICKETS_FILE, JSON.stringify(data, null, 2));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function createTicket() {
|
|
34
|
+
console.log('\nš« Create New Ticket\n');
|
|
35
|
+
|
|
36
|
+
const route = await question('Route/Webpage: ');
|
|
37
|
+
|
|
38
|
+
console.log('\nPaste F12 Console Errors (press Enter twice when done):');
|
|
39
|
+
let f12Errors = '';
|
|
40
|
+
let line;
|
|
41
|
+
while ((line = await question('')) !== '') {
|
|
42
|
+
f12Errors += line + '\n';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
console.log('\nPaste Server Console Errors (press Enter twice when done):');
|
|
46
|
+
let serverErrors = '';
|
|
47
|
+
while ((line = await question('')) !== '') {
|
|
48
|
+
serverErrors += line + '\n';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const description = await question('\nDescription (optional): ');
|
|
52
|
+
|
|
53
|
+
const ticket = {
|
|
54
|
+
id: 'TKT-' + Date.now(),
|
|
55
|
+
route: route,
|
|
56
|
+
f12Errors: f12Errors.trim(),
|
|
57
|
+
serverErrors: serverErrors.trim(),
|
|
58
|
+
description: description,
|
|
59
|
+
status: 'open',
|
|
60
|
+
priority: null,
|
|
61
|
+
relatedTickets: [],
|
|
62
|
+
swarmActions: [],
|
|
63
|
+
namespace: null,
|
|
64
|
+
createdAt: new Date().toISOString(),
|
|
65
|
+
updatedAt: new Date().toISOString()
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const data = await readTickets();
|
|
69
|
+
data.tickets.push(ticket);
|
|
70
|
+
await writeTickets(data);
|
|
71
|
+
|
|
72
|
+
console.log(`\nā
Ticket created: ${ticket.id}\n`);
|
|
73
|
+
rl.close();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function listTickets() {
|
|
77
|
+
const data = await readTickets();
|
|
78
|
+
|
|
79
|
+
if (data.tickets.length === 0) {
|
|
80
|
+
console.log('No tickets found.');
|
|
81
|
+
rl.close();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
console.log(`\nš Total Tickets: ${data.tickets.length}\n`);
|
|
86
|
+
|
|
87
|
+
data.tickets
|
|
88
|
+
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
|
|
89
|
+
.forEach(ticket => {
|
|
90
|
+
console.log(`${ticket.id} - ${ticket.status.toUpperCase()}`);
|
|
91
|
+
console.log(` Route: ${ticket.route}`);
|
|
92
|
+
if (ticket.priority) console.log(` Priority: ${ticket.priority}`);
|
|
93
|
+
console.log(` Created: ${new Date(ticket.createdAt).toLocaleString()}`);
|
|
94
|
+
console.log('');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
rl.close();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function main() {
|
|
101
|
+
const args = process.argv.slice(2);
|
|
102
|
+
|
|
103
|
+
if (args[0] === 'list' || args[0] === 'ls') {
|
|
104
|
+
await listTickets();
|
|
105
|
+
} else if (args[0] === 'create' || args[0] === 'new' || args.length === 0) {
|
|
106
|
+
await createTicket();
|
|
107
|
+
} else {
|
|
108
|
+
console.log('Usage:');
|
|
109
|
+
console.log(' node ticket-cli.js - Create a new ticket');
|
|
110
|
+
console.log(' node ticket-cli.js create - Create a new ticket');
|
|
111
|
+
console.log(' node ticket-cli.js list - List all tickets');
|
|
112
|
+
rl.close();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
main().catch(error => {
|
|
117
|
+
console.error('Error:', error);
|
|
118
|
+
rl.close();
|
|
119
|
+
process.exit(1);
|
|
120
|
+
});
|
package/ticket-server.js
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const express = require('express');
|
|
4
|
+
const fs = require('fs').promises;
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const cors = require('cors');
|
|
7
|
+
|
|
8
|
+
const app = express();
|
|
9
|
+
const PORT = process.env.PORT || 3456;
|
|
10
|
+
|
|
11
|
+
// When installed as package, look for tickets.json in the project root (cwd)
|
|
12
|
+
// When run standalone, look in the same directory as the script
|
|
13
|
+
const projectRoot = process.cwd();
|
|
14
|
+
const TICKETS_FILE = path.join(projectRoot, 'tickets.json');
|
|
15
|
+
const BACKUP_DIR = path.join(projectRoot, 'ticket-backups');
|
|
16
|
+
|
|
17
|
+
// Serve static files from the package directory (where ticket-tracker.html is)
|
|
18
|
+
const packageDir = __dirname;
|
|
19
|
+
|
|
20
|
+
app.use(cors());
|
|
21
|
+
app.use(express.json());
|
|
22
|
+
app.use(express.static(projectRoot)); // Serve from project root first
|
|
23
|
+
app.use(express.static(packageDir)); // Fall back to package directory
|
|
24
|
+
|
|
25
|
+
// Auto-find available port
|
|
26
|
+
async function findAvailablePort(startPort) {
|
|
27
|
+
const net = require('net');
|
|
28
|
+
|
|
29
|
+
return new Promise((resolve) => {
|
|
30
|
+
const server = net.createServer();
|
|
31
|
+
server.unref();
|
|
32
|
+
server.on('error', () => {
|
|
33
|
+
resolve(findAvailablePort(startPort + 1));
|
|
34
|
+
});
|
|
35
|
+
server.listen(startPort, () => {
|
|
36
|
+
const { port } = server.address();
|
|
37
|
+
server.close(() => {
|
|
38
|
+
resolve(port);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Initialize tickets file if it doesn't exist
|
|
45
|
+
async function initTicketsFile() {
|
|
46
|
+
try {
|
|
47
|
+
await fs.access(TICKETS_FILE);
|
|
48
|
+
} catch {
|
|
49
|
+
await fs.writeFile(TICKETS_FILE, JSON.stringify({ tickets: [] }, null, 2));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Create backup directory
|
|
53
|
+
try {
|
|
54
|
+
await fs.mkdir(BACKUP_DIR, { recursive: true });
|
|
55
|
+
} catch (error) {
|
|
56
|
+
// Directory already exists, that's fine
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Read tickets
|
|
61
|
+
async function readTickets() {
|
|
62
|
+
try {
|
|
63
|
+
const data = await fs.readFile(TICKETS_FILE, 'utf8');
|
|
64
|
+
return JSON.parse(data);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
return { tickets: [] };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Create backup before writing
|
|
71
|
+
async function createBackup() {
|
|
72
|
+
try {
|
|
73
|
+
// Check if tickets.json exists
|
|
74
|
+
await fs.access(TICKETS_FILE);
|
|
75
|
+
|
|
76
|
+
// Create timestamped backup in folder
|
|
77
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
78
|
+
const timestampedBackup = path.join(BACKUP_DIR, `tickets-${timestamp}.json`);
|
|
79
|
+
await fs.copyFile(TICKETS_FILE, timestampedBackup);
|
|
80
|
+
|
|
81
|
+
// Rotate backups - keep last 10
|
|
82
|
+
const files = await fs.readdir(BACKUP_DIR);
|
|
83
|
+
const backupFiles = files
|
|
84
|
+
.filter(f => f.startsWith('tickets-') && f.endsWith('.json'))
|
|
85
|
+
.sort()
|
|
86
|
+
.reverse();
|
|
87
|
+
|
|
88
|
+
if (backupFiles.length > 10) {
|
|
89
|
+
for (let i = 10; i < backupFiles.length; i++) {
|
|
90
|
+
await fs.unlink(path.join(BACKUP_DIR, backupFiles[i]));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
console.log(`ā
Backup created: ${timestampedBackup}`);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.warn('ā ļø Backup failed:', error.message);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Write tickets (with automatic backup)
|
|
101
|
+
async function writeTickets(data) {
|
|
102
|
+
await createBackup();
|
|
103
|
+
await fs.writeFile(TICKETS_FILE, JSON.stringify(data, null, 2));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// GET all tickets
|
|
107
|
+
app.get('/api/tickets', async (req, res) => {
|
|
108
|
+
try {
|
|
109
|
+
const data = await readTickets();
|
|
110
|
+
res.json(data.tickets);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
res.status(500).json({ error: error.message });
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// GET single ticket
|
|
117
|
+
app.get('/api/tickets/:id', async (req, res) => {
|
|
118
|
+
try {
|
|
119
|
+
const data = await readTickets();
|
|
120
|
+
const ticket = data.tickets.find(t => t.id === req.params.id);
|
|
121
|
+
if (ticket) {
|
|
122
|
+
res.json(ticket);
|
|
123
|
+
} else {
|
|
124
|
+
res.status(404).json({ error: 'Ticket not found' });
|
|
125
|
+
}
|
|
126
|
+
} catch (error) {
|
|
127
|
+
res.status(500).json({ error: error.message });
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// POST new ticket
|
|
132
|
+
app.post('/api/tickets', async (req, res) => {
|
|
133
|
+
try {
|
|
134
|
+
const data = await readTickets();
|
|
135
|
+
|
|
136
|
+
const ticket = {
|
|
137
|
+
id: 'TKT-' + Date.now(),
|
|
138
|
+
route: req.body.route,
|
|
139
|
+
f12Errors: req.body.f12Errors || '',
|
|
140
|
+
serverErrors: req.body.serverErrors || '',
|
|
141
|
+
description: req.body.description || '',
|
|
142
|
+
status: req.body.status || 'open',
|
|
143
|
+
priority: req.body.priority || null,
|
|
144
|
+
relatedTickets: req.body.relatedTickets || [],
|
|
145
|
+
swarmActions: req.body.swarmActions || [],
|
|
146
|
+
namespace: req.body.namespace || null,
|
|
147
|
+
createdAt: new Date().toISOString(),
|
|
148
|
+
updatedAt: new Date().toISOString()
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
data.tickets.push(ticket);
|
|
152
|
+
await writeTickets(data);
|
|
153
|
+
|
|
154
|
+
res.status(201).json(ticket);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
res.status(500).json({ error: error.message });
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// PATCH update ticket
|
|
161
|
+
app.patch('/api/tickets/:id', async (req, res) => {
|
|
162
|
+
try {
|
|
163
|
+
const data = await readTickets();
|
|
164
|
+
const ticketIndex = data.tickets.findIndex(t => t.id === req.params.id);
|
|
165
|
+
|
|
166
|
+
if (ticketIndex === -1) {
|
|
167
|
+
return res.status(404).json({ error: 'Ticket not found' });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Update allowed fields
|
|
171
|
+
const allowedFields = [
|
|
172
|
+
'status', 'priority', 'relatedTickets', 'swarmActions',
|
|
173
|
+
'namespace', 'description', 'f12Errors', 'serverErrors'
|
|
174
|
+
];
|
|
175
|
+
|
|
176
|
+
allowedFields.forEach(field => {
|
|
177
|
+
if (req.body[field] !== undefined) {
|
|
178
|
+
data.tickets[ticketIndex][field] = req.body[field];
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
data.tickets[ticketIndex].updatedAt = new Date().toISOString();
|
|
183
|
+
|
|
184
|
+
await writeTickets(data);
|
|
185
|
+
res.json(data.tickets[ticketIndex]);
|
|
186
|
+
} catch (error) {
|
|
187
|
+
res.status(500).json({ error: error.message });
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// POST add swarm action to ticket
|
|
192
|
+
app.post('/api/tickets/:id/swarm-action', async (req, res) => {
|
|
193
|
+
try {
|
|
194
|
+
const data = await readTickets();
|
|
195
|
+
const ticket = data.tickets.find(t => t.id === req.params.id);
|
|
196
|
+
|
|
197
|
+
if (!ticket) {
|
|
198
|
+
return res.status(404).json({ error: 'Ticket not found' });
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const action = {
|
|
202
|
+
timestamp: new Date().toISOString(),
|
|
203
|
+
action: req.body.action,
|
|
204
|
+
result: req.body.result || null
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
ticket.swarmActions.push(action);
|
|
208
|
+
ticket.updatedAt = new Date().toISOString();
|
|
209
|
+
|
|
210
|
+
await writeTickets(data);
|
|
211
|
+
res.json(ticket);
|
|
212
|
+
} catch (error) {
|
|
213
|
+
res.status(500).json({ error: error.message });
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// POST analyze ticket with swarm (placeholder for swarm integration)
|
|
218
|
+
app.post('/api/tickets/:id/analyze', async (req, res) => {
|
|
219
|
+
try {
|
|
220
|
+
const data = await readTickets();
|
|
221
|
+
const ticket = data.tickets.find(t => t.id === req.params.id);
|
|
222
|
+
|
|
223
|
+
if (!ticket) {
|
|
224
|
+
return res.status(404).json({ error: 'Ticket not found' });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// This is where you'd integrate with your swarm to analyze the ticket
|
|
228
|
+
// For now, we'll do a simple analysis
|
|
229
|
+
|
|
230
|
+
// Determine priority based on errors
|
|
231
|
+
let priority = 'low';
|
|
232
|
+
if (ticket.f12Errors.toLowerCase().includes('error') ||
|
|
233
|
+
ticket.serverErrors.toLowerCase().includes('error')) {
|
|
234
|
+
priority = 'medium';
|
|
235
|
+
}
|
|
236
|
+
if (ticket.f12Errors.toLowerCase().includes('uncaught') ||
|
|
237
|
+
ticket.serverErrors.toLowerCase().includes('fatal') ||
|
|
238
|
+
ticket.serverErrors.toLowerCase().includes('crash')) {
|
|
239
|
+
priority = 'high';
|
|
240
|
+
}
|
|
241
|
+
if (ticket.route.includes('auth') || ticket.route.includes('payment')) {
|
|
242
|
+
priority = 'critical';
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Find related tickets (simple route-based matching)
|
|
246
|
+
const relatedTickets = data.tickets
|
|
247
|
+
.filter(t => t.id !== ticket.id && t.route === ticket.route)
|
|
248
|
+
.map(t => t.id)
|
|
249
|
+
.slice(0, 3);
|
|
250
|
+
|
|
251
|
+
ticket.priority = priority;
|
|
252
|
+
ticket.relatedTickets = relatedTickets;
|
|
253
|
+
ticket.swarmActions.push({
|
|
254
|
+
timestamp: new Date().toISOString(),
|
|
255
|
+
action: 'auto-analysis',
|
|
256
|
+
result: `Priority set to ${priority}, found ${relatedTickets.length} related tickets`
|
|
257
|
+
});
|
|
258
|
+
ticket.updatedAt = new Date().toISOString();
|
|
259
|
+
|
|
260
|
+
await writeTickets(data);
|
|
261
|
+
res.json(ticket);
|
|
262
|
+
} catch (error) {
|
|
263
|
+
res.status(500).json({ error: error.message });
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// DELETE ticket
|
|
268
|
+
app.delete('/api/tickets/:id', async (req, res) => {
|
|
269
|
+
try {
|
|
270
|
+
const data = await readTickets();
|
|
271
|
+
const ticketIndex = data.tickets.findIndex(t => t.id === req.params.id);
|
|
272
|
+
|
|
273
|
+
if (ticketIndex === -1) {
|
|
274
|
+
return res.status(404).json({ error: 'Ticket not found' });
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
data.tickets.splice(ticketIndex, 1);
|
|
278
|
+
await writeTickets(data);
|
|
279
|
+
|
|
280
|
+
res.json({ message: 'Ticket deleted' });
|
|
281
|
+
} catch (error) {
|
|
282
|
+
res.status(500).json({ error: error.message });
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// GET stats
|
|
287
|
+
app.get('/api/stats', async (req, res) => {
|
|
288
|
+
try {
|
|
289
|
+
const data = await readTickets();
|
|
290
|
+
const tickets = data.tickets;
|
|
291
|
+
|
|
292
|
+
const stats = {
|
|
293
|
+
total: tickets.length,
|
|
294
|
+
byStatus: {
|
|
295
|
+
open: tickets.filter(t => t.status === 'open').length,
|
|
296
|
+
inProgress: tickets.filter(t => t.status === 'in-progress').length,
|
|
297
|
+
fixed: tickets.filter(t => t.status === 'fixed').length,
|
|
298
|
+
closed: tickets.filter(t => t.status === 'closed').length
|
|
299
|
+
},
|
|
300
|
+
byPriority: {
|
|
301
|
+
critical: tickets.filter(t => t.priority === 'critical').length,
|
|
302
|
+
high: tickets.filter(t => t.priority === 'high').length,
|
|
303
|
+
medium: tickets.filter(t => t.priority === 'medium').length,
|
|
304
|
+
low: tickets.filter(t => t.priority === 'low').length
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
res.json(stats);
|
|
309
|
+
} catch (error) {
|
|
310
|
+
res.status(500).json({ error: error.message });
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// Initialize and start server
|
|
315
|
+
initTicketsFile().then(async () => {
|
|
316
|
+
const availablePort = await findAvailablePort(PORT);
|
|
317
|
+
|
|
318
|
+
app.listen(availablePort, () => {
|
|
319
|
+
console.log(`š« Ticket Tracker API running on http://localhost:${availablePort}`);
|
|
320
|
+
console.log(`š Open http://localhost:${availablePort}/ticket-tracker.html to view the UI`);
|
|
321
|
+
console.log(`š Tickets stored at: ${TICKETS_FILE}`);
|
|
322
|
+
|
|
323
|
+
if (availablePort !== PORT) {
|
|
324
|
+
console.log(`ā ļø Port ${PORT} was busy, using port ${availablePort} instead`);
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
});
|