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 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: new Date().toISOString(),
117
- updatedAt: new Date().toISOString()
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.2",
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) {
@@ -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">