swarm-tickets 2.0.2 ā 2.1.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/README.md +40 -0
- package/lib/migrate.js +169 -0
- package/lib/storage/json-adapter.js +7 -3
- package/lib/storage/sqlite-adapter.js +7 -3
- package/lib/storage/supabase-adapter.js +6 -3
- package/package.json +1 -1
- package/ticket-server.js +46 -1
- package/ticket-tracker.html +0 -3
package/README.md
CHANGED
|
@@ -199,6 +199,46 @@ CREATE INDEX IF NOT EXISTS idx_swarm_actions_ticket_id ON swarm_actions(ticket_i
|
|
|
199
199
|
CREATE INDEX IF NOT EXISTS idx_comments_ticket_id ON comments(ticket_id);
|
|
200
200
|
```
|
|
201
201
|
|
|
202
|
+
## š Migrating Existing Tickets
|
|
203
|
+
|
|
204
|
+
Already have tickets in JSON and want to switch to SQLite or Supabase? Use the migration tool:
|
|
205
|
+
|
|
206
|
+
### Migrate to SQLite
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
# Install SQLite dependency
|
|
210
|
+
npm install better-sqlite3
|
|
211
|
+
|
|
212
|
+
# Run migration
|
|
213
|
+
npx swarm-tickets migrate --to sqlite
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Migrate to Supabase
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
# Install Supabase dependency
|
|
220
|
+
npm install @supabase/supabase-js
|
|
221
|
+
|
|
222
|
+
# Set environment variables
|
|
223
|
+
export SUPABASE_URL=https://your-project.supabase.co
|
|
224
|
+
export SUPABASE_ANON_KEY=your-anon-key
|
|
225
|
+
|
|
226
|
+
# Run migration (create tables first - see Supabase setup above)
|
|
227
|
+
npx swarm-tickets migrate --to supabase
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
The migration tool:
|
|
231
|
+
- Preserves ticket IDs, timestamps, and all data
|
|
232
|
+
- Skips tickets that already exist in the target
|
|
233
|
+
- Leaves your original `tickets.json` unchanged
|
|
234
|
+
- Shows a summary of migrated/skipped/failed tickets
|
|
235
|
+
|
|
236
|
+
After migration, start using the new storage:
|
|
237
|
+
```bash
|
|
238
|
+
export SWARM_TICKETS_STORAGE=sqlite # or supabase
|
|
239
|
+
npx swarm-tickets
|
|
240
|
+
```
|
|
241
|
+
|
|
202
242
|
## š¤ Using with Claude
|
|
203
243
|
|
|
204
244
|
The package includes a Claude skill that teaches Claude how to:
|
package/lib/migrate.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Migration tool for swarm-tickets
|
|
4
|
+
* Migrates tickets from JSON to SQLite or Supabase
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
async function migrate(targetStorage, options = {}) {
|
|
11
|
+
const projectRoot = process.cwd();
|
|
12
|
+
const jsonPath = options.jsonPath || path.join(projectRoot, 'tickets.json');
|
|
13
|
+
|
|
14
|
+
console.log('\nš Swarm Tickets Migration Tool\n');
|
|
15
|
+
|
|
16
|
+
// Validate target
|
|
17
|
+
if (!['sqlite', 'supabase'].includes(targetStorage)) {
|
|
18
|
+
console.error('ā Invalid target. Use: sqlite or supabase');
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Check JSON source exists
|
|
23
|
+
if (!fs.existsSync(jsonPath)) {
|
|
24
|
+
console.error(`ā Source file not found: ${jsonPath}`);
|
|
25
|
+
console.error(' Make sure you have a tickets.json file to migrate from.');
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Read source JSON
|
|
30
|
+
console.log(`š Reading from: ${jsonPath}`);
|
|
31
|
+
let sourceData;
|
|
32
|
+
try {
|
|
33
|
+
sourceData = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error(`ā Failed to read JSON: ${error.message}`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const tickets = sourceData.tickets || [];
|
|
40
|
+
if (tickets.length === 0) {
|
|
41
|
+
console.log('ā¹ļø No tickets to migrate.');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
console.log(`š Found ${tickets.length} tickets to migrate\n`);
|
|
46
|
+
|
|
47
|
+
// Set up target storage
|
|
48
|
+
const { createStorageAdapter } = require('./storage');
|
|
49
|
+
|
|
50
|
+
// Override config for target
|
|
51
|
+
const targetConfig = {
|
|
52
|
+
type: targetStorage,
|
|
53
|
+
sqlitePath: options.sqlitePath || path.join(projectRoot, 'tickets.db'),
|
|
54
|
+
supabaseUrl: process.env.SUPABASE_URL,
|
|
55
|
+
supabaseKey: process.env.SUPABASE_ANON_KEY,
|
|
56
|
+
supabaseServiceKey: process.env.SUPABASE_SERVICE_ROLE_KEY
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Validate Supabase config
|
|
60
|
+
if (targetStorage === 'supabase') {
|
|
61
|
+
if (!targetConfig.supabaseUrl || !targetConfig.supabaseKey) {
|
|
62
|
+
console.error('ā Supabase migration requires environment variables:');
|
|
63
|
+
console.error(' SUPABASE_URL=https://your-project.supabase.co');
|
|
64
|
+
console.error(' SUPABASE_ANON_KEY=your-anon-key');
|
|
65
|
+
console.error(' SUPABASE_SERVICE_ROLE_KEY=your-service-key (optional, for auto table creation)');
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let targetAdapter;
|
|
71
|
+
try {
|
|
72
|
+
console.log(`šÆ Connecting to ${targetStorage}...`);
|
|
73
|
+
targetAdapter = await createStorageAdapter(targetConfig);
|
|
74
|
+
console.log(`ā
Connected to ${targetStorage}\n`);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error(`ā Failed to connect to ${targetStorage}: ${error.message}`);
|
|
77
|
+
if (targetStorage === 'sqlite') {
|
|
78
|
+
console.error(' Make sure better-sqlite3 is installed: npm install better-sqlite3');
|
|
79
|
+
} else {
|
|
80
|
+
console.error(' Make sure @supabase/supabase-js is installed: npm install @supabase/supabase-js');
|
|
81
|
+
console.error(' And that your environment variables are set correctly.');
|
|
82
|
+
}
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Migrate tickets
|
|
87
|
+
let migrated = 0;
|
|
88
|
+
let skipped = 0;
|
|
89
|
+
let failed = 0;
|
|
90
|
+
|
|
91
|
+
for (const ticket of tickets) {
|
|
92
|
+
process.stdout.write(` Migrating ${ticket.id}... `);
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
// Check if ticket already exists
|
|
96
|
+
const existing = await targetAdapter.getTicket(ticket.id);
|
|
97
|
+
if (existing) {
|
|
98
|
+
console.log('āļø already exists, skipping');
|
|
99
|
+
skipped++;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Create the ticket with original ID and timestamps
|
|
104
|
+
await targetAdapter.createTicket({
|
|
105
|
+
id: ticket.id,
|
|
106
|
+
route: ticket.route || '',
|
|
107
|
+
f12Errors: ticket.f12Errors || '',
|
|
108
|
+
serverErrors: ticket.serverErrors || '',
|
|
109
|
+
description: ticket.description || '',
|
|
110
|
+
status: ticket.status || 'open',
|
|
111
|
+
priority: ticket.priority || null,
|
|
112
|
+
namespace: ticket.namespace || null,
|
|
113
|
+
relatedTickets: ticket.relatedTickets || [],
|
|
114
|
+
swarmActions: (ticket.swarmActions || []).map(a => {
|
|
115
|
+
if (typeof a === 'string') {
|
|
116
|
+
return { action: a, result: null, timestamp: ticket.createdAt };
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
action: a.action,
|
|
120
|
+
result: a.result || null,
|
|
121
|
+
timestamp: a.timestamp || ticket.createdAt
|
|
122
|
+
};
|
|
123
|
+
}),
|
|
124
|
+
comments: (ticket.comments || []).map(c => ({
|
|
125
|
+
id: c.id,
|
|
126
|
+
type: c.type || 'human',
|
|
127
|
+
author: c.author || 'anonymous',
|
|
128
|
+
content: c.content || '',
|
|
129
|
+
metadata: c.metadata || {},
|
|
130
|
+
timestamp: c.timestamp
|
|
131
|
+
})),
|
|
132
|
+
createdAt: ticket.createdAt,
|
|
133
|
+
updatedAt: ticket.updatedAt
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
console.log('ā
');
|
|
137
|
+
migrated++;
|
|
138
|
+
} catch (error) {
|
|
139
|
+
console.log(`ā ${error.message}`);
|
|
140
|
+
failed++;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Summary
|
|
145
|
+
console.log('\n' + 'ā'.repeat(40));
|
|
146
|
+
console.log('š Migration Summary:');
|
|
147
|
+
console.log(` ā
Migrated: ${migrated}`);
|
|
148
|
+
console.log(` āļø Skipped: ${skipped}`);
|
|
149
|
+
console.log(` ā Failed: ${failed}`);
|
|
150
|
+
console.log('ā'.repeat(40));
|
|
151
|
+
|
|
152
|
+
if (migrated > 0) {
|
|
153
|
+
console.log(`\nš Migration complete!`);
|
|
154
|
+
console.log(`\nTo use ${targetStorage} storage, start the server with:`);
|
|
155
|
+
if (targetStorage === 'sqlite') {
|
|
156
|
+
console.log(' export SWARM_TICKETS_STORAGE=sqlite');
|
|
157
|
+
console.log(' npx swarm-tickets');
|
|
158
|
+
} else {
|
|
159
|
+
console.log(' export SWARM_TICKETS_STORAGE=supabase');
|
|
160
|
+
console.log(' npx swarm-tickets');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
console.log(`\nš” Tip: Your original tickets.json is unchanged.`);
|
|
165
|
+
console.log(` You can rename it to tickets.json.backup once migration looks good.`);
|
|
166
|
+
console.log('');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
module.exports = { migrate };
|
|
@@ -86,6 +86,9 @@ class JsonAdapter extends BaseAdapter {
|
|
|
86
86
|
if (filters.status) {
|
|
87
87
|
tickets = tickets.filter(t => t.status === filters.status);
|
|
88
88
|
}
|
|
89
|
+
if (filters.excludeStatus) {
|
|
90
|
+
tickets = tickets.filter(t => t.status !== filters.excludeStatus);
|
|
91
|
+
}
|
|
89
92
|
if (filters.priority) {
|
|
90
93
|
tickets = tickets.filter(t => t.priority === filters.priority);
|
|
91
94
|
}
|
|
@@ -101,8 +104,9 @@ class JsonAdapter extends BaseAdapter {
|
|
|
101
104
|
}
|
|
102
105
|
|
|
103
106
|
async createTicket(ticketData) {
|
|
107
|
+
const now = new Date().toISOString();
|
|
104
108
|
const ticket = {
|
|
105
|
-
id: this.generateTicketId(),
|
|
109
|
+
id: ticketData.id || this.generateTicketId(), // Allow custom ID for migration
|
|
106
110
|
route: ticketData.route || '',
|
|
107
111
|
f12Errors: ticketData.f12Errors || '',
|
|
108
112
|
serverErrors: ticketData.serverErrors || '',
|
|
@@ -113,8 +117,8 @@ class JsonAdapter extends BaseAdapter {
|
|
|
113
117
|
swarmActions: ticketData.swarmActions || [],
|
|
114
118
|
comments: ticketData.comments || [],
|
|
115
119
|
namespace: ticketData.namespace || null,
|
|
116
|
-
createdAt:
|
|
117
|
-
updatedAt:
|
|
120
|
+
createdAt: ticketData.createdAt || now, // Allow custom timestamps for migration
|
|
121
|
+
updatedAt: ticketData.updatedAt || now
|
|
118
122
|
};
|
|
119
123
|
|
|
120
124
|
this.data.tickets.push(ticket);
|
|
@@ -223,6 +223,10 @@ class SqliteAdapter extends BaseAdapter {
|
|
|
223
223
|
query += ' AND status = ?';
|
|
224
224
|
params.push(filters.status);
|
|
225
225
|
}
|
|
226
|
+
if (filters.excludeStatus) {
|
|
227
|
+
query += ' AND status != ?';
|
|
228
|
+
params.push(filters.excludeStatus);
|
|
229
|
+
}
|
|
226
230
|
if (filters.priority) {
|
|
227
231
|
query += ' AND priority = ?';
|
|
228
232
|
params.push(filters.priority);
|
|
@@ -244,8 +248,8 @@ class SqliteAdapter extends BaseAdapter {
|
|
|
244
248
|
}
|
|
245
249
|
|
|
246
250
|
async createTicket(ticketData) {
|
|
247
|
-
const id = this.generateTicketId();
|
|
248
251
|
const now = new Date().toISOString();
|
|
252
|
+
const id = ticketData.id || this.generateTicketId(); // Allow custom ID for migration
|
|
249
253
|
|
|
250
254
|
const ticket = {
|
|
251
255
|
id,
|
|
@@ -256,8 +260,8 @@ class SqliteAdapter extends BaseAdapter {
|
|
|
256
260
|
status: ticketData.status || 'open',
|
|
257
261
|
priority: ticketData.priority || null,
|
|
258
262
|
namespace: ticketData.namespace || null,
|
|
259
|
-
createdAt: now,
|
|
260
|
-
updatedAt: now
|
|
263
|
+
createdAt: ticketData.createdAt || now, // Allow custom timestamps for migration
|
|
264
|
+
updatedAt: ticketData.updatedAt || now
|
|
261
265
|
};
|
|
262
266
|
|
|
263
267
|
const transaction = this.db.transaction(() => {
|
|
@@ -218,6 +218,9 @@ class SupabaseAdapter extends BaseAdapter {
|
|
|
218
218
|
if (filters.status) {
|
|
219
219
|
query = query.eq('status', filters.status);
|
|
220
220
|
}
|
|
221
|
+
if (filters.excludeStatus) {
|
|
222
|
+
query = query.neq('status', filters.excludeStatus);
|
|
223
|
+
}
|
|
221
224
|
if (filters.priority) {
|
|
222
225
|
query = query.eq('priority', filters.priority);
|
|
223
226
|
}
|
|
@@ -243,8 +246,8 @@ class SupabaseAdapter extends BaseAdapter {
|
|
|
243
246
|
}
|
|
244
247
|
|
|
245
248
|
async createTicket(ticketData) {
|
|
246
|
-
const id = this.generateTicketId();
|
|
247
249
|
const now = new Date().toISOString();
|
|
250
|
+
const id = ticketData.id || this.generateTicketId(); // Allow custom ID for migration
|
|
248
251
|
|
|
249
252
|
const ticket = {
|
|
250
253
|
id,
|
|
@@ -255,8 +258,8 @@ class SupabaseAdapter extends BaseAdapter {
|
|
|
255
258
|
status: ticketData.status || 'open',
|
|
256
259
|
priority: ticketData.priority || null,
|
|
257
260
|
namespace: ticketData.namespace || null,
|
|
258
|
-
created_at: now,
|
|
259
|
-
updated_at: now
|
|
261
|
+
created_at: ticketData.createdAt || now, // Allow custom timestamps for migration
|
|
262
|
+
updated_at: ticketData.updatedAt || now
|
|
260
263
|
};
|
|
261
264
|
|
|
262
265
|
const { error } = await this.client.from('tickets').insert(ticket);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "swarm-tickets",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Lightweight ticket tracking system for AI-powered bug fixing with Claude-flow/Claude Code. Supports JSON, SQLite, and Supabase storage.",
|
|
5
5
|
"main": "ticket-server.js",
|
|
6
6
|
"bin": {
|
package/ticket-server.js
CHANGED
|
@@ -10,6 +10,45 @@ const path = require('path');
|
|
|
10
10
|
const cors = require('cors');
|
|
11
11
|
const { createStorageAdapter, getStorageConfig } = require('./lib/storage');
|
|
12
12
|
|
|
13
|
+
// Handle CLI commands before starting server
|
|
14
|
+
const args = process.argv.slice(2);
|
|
15
|
+
if (args[0] === 'migrate') {
|
|
16
|
+
const { migrate } = require('./lib/migrate');
|
|
17
|
+
const toIndex = args.indexOf('--to');
|
|
18
|
+
const target = toIndex !== -1 ? args[toIndex + 1] : null;
|
|
19
|
+
|
|
20
|
+
if (!target) {
|
|
21
|
+
console.log('\nUsage: npx swarm-tickets migrate --to <target>\n');
|
|
22
|
+
console.log('Targets:');
|
|
23
|
+
console.log(' sqlite - Migrate to local SQLite database');
|
|
24
|
+
console.log(' supabase - Migrate to Supabase (requires env vars)\n');
|
|
25
|
+
console.log('Examples:');
|
|
26
|
+
console.log(' npx swarm-tickets migrate --to sqlite');
|
|
27
|
+
console.log(' npx swarm-tickets migrate --to supabase\n');
|
|
28
|
+
process.exit(0);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
migrate(target).then(() => process.exit(0)).catch(err => {
|
|
32
|
+
console.error('Migration failed:', err);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
});
|
|
35
|
+
} else if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') {
|
|
36
|
+
console.log('\nš« Swarm Tickets - Bug tracking for AI-powered development\n');
|
|
37
|
+
console.log('Usage: npx swarm-tickets [command]\n');
|
|
38
|
+
console.log('Commands:');
|
|
39
|
+
console.log(' (none) Start the ticket server');
|
|
40
|
+
console.log(' migrate --to <db> Migrate tickets from JSON to sqlite/supabase');
|
|
41
|
+
console.log(' help Show this help message\n');
|
|
42
|
+
console.log('Environment Variables:');
|
|
43
|
+
console.log(' PORT Server port (default: 3456)');
|
|
44
|
+
console.log(' SWARM_TICKETS_STORAGE Storage type: json, sqlite, supabase');
|
|
45
|
+
console.log(' SWARM_TICKETS_SQLITE_PATH SQLite database path');
|
|
46
|
+
console.log(' SUPABASE_URL Supabase project URL');
|
|
47
|
+
console.log(' SUPABASE_ANON_KEY Supabase anonymous key');
|
|
48
|
+
console.log(' SUPABASE_SERVICE_ROLE_KEY Supabase service role key\n');
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
51
|
+
|
|
13
52
|
const app = express();
|
|
14
53
|
const PORT = process.env.PORT || 3456;
|
|
15
54
|
|
|
@@ -47,7 +86,7 @@ async function findAvailablePort(startPort) {
|
|
|
47
86
|
|
|
48
87
|
// ==================== TICKET ENDPOINTS ====================
|
|
49
88
|
|
|
50
|
-
// GET all tickets
|
|
89
|
+
// GET all tickets (excludes closed by default for performance)
|
|
51
90
|
app.get('/api/tickets', async (req, res) => {
|
|
52
91
|
try {
|
|
53
92
|
const filters = {};
|
|
@@ -55,6 +94,12 @@ app.get('/api/tickets', async (req, res) => {
|
|
|
55
94
|
if (req.query.priority) filters.priority = req.query.priority;
|
|
56
95
|
if (req.query.route) filters.route = req.query.route;
|
|
57
96
|
|
|
97
|
+
// Exclude closed tickets by default (use ?include_closed=true to include them)
|
|
98
|
+
const includeClosed = req.query.include_closed === 'true';
|
|
99
|
+
if (!includeClosed && !filters.status) {
|
|
100
|
+
filters.excludeStatus = 'closed';
|
|
101
|
+
}
|
|
102
|
+
|
|
58
103
|
const tickets = await storage.getAllTickets(filters);
|
|
59
104
|
res.json(tickets);
|
|
60
105
|
} catch (error) {
|
package/ticket-tracker.html
CHANGED
|
@@ -445,7 +445,6 @@
|
|
|
445
445
|
<option value="open">Open</option>
|
|
446
446
|
<option value="in-progress">In Progress</option>
|
|
447
447
|
<option value="fixed">Fixed</option>
|
|
448
|
-
<option value="closed">Closed</option>
|
|
449
448
|
</select>
|
|
450
449
|
</div>
|
|
451
450
|
|
|
@@ -464,7 +463,6 @@
|
|
|
464
463
|
<option value="open">Open</option>
|
|
465
464
|
<option value="in-progress">In Progress</option>
|
|
466
465
|
<option value="fixed">Fixed</option>
|
|
467
|
-
<option value="closed">Closed</option>
|
|
468
466
|
</select>
|
|
469
467
|
</div>
|
|
470
468
|
|
|
@@ -575,7 +573,6 @@
|
|
|
575
573
|
<option value="open">Open</option>
|
|
576
574
|
<option value="in-progress">In Progress</option>
|
|
577
575
|
<option value="fixed">Fixed</option>
|
|
578
|
-
<option value="closed">Closed</option>
|
|
579
576
|
</select>
|
|
580
577
|
</div>
|
|
581
578
|
<div class="form-group">
|