swarm-tickets 2.0.3 → 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 };
@@ -104,8 +104,9 @@ class JsonAdapter extends BaseAdapter {
104
104
  }
105
105
 
106
106
  async createTicket(ticketData) {
107
+ const now = new Date().toISOString();
107
108
  const ticket = {
108
- id: this.generateTicketId(),
109
+ id: ticketData.id || this.generateTicketId(), // Allow custom ID for migration
109
110
  route: ticketData.route || '',
110
111
  f12Errors: ticketData.f12Errors || '',
111
112
  serverErrors: ticketData.serverErrors || '',
@@ -116,8 +117,8 @@ class JsonAdapter extends BaseAdapter {
116
117
  swarmActions: ticketData.swarmActions || [],
117
118
  comments: ticketData.comments || [],
118
119
  namespace: ticketData.namespace || null,
119
- createdAt: new Date().toISOString(),
120
- updatedAt: new Date().toISOString()
120
+ createdAt: ticketData.createdAt || now, // Allow custom timestamps for migration
121
+ updatedAt: ticketData.updatedAt || now
121
122
  };
122
123
 
123
124
  this.data.tickets.push(ticket);
@@ -248,8 +248,8 @@ class SqliteAdapter extends BaseAdapter {
248
248
  }
249
249
 
250
250
  async createTicket(ticketData) {
251
- const id = this.generateTicketId();
252
251
  const now = new Date().toISOString();
252
+ const id = ticketData.id || this.generateTicketId(); // Allow custom ID for migration
253
253
 
254
254
  const ticket = {
255
255
  id,
@@ -260,8 +260,8 @@ class SqliteAdapter extends BaseAdapter {
260
260
  status: ticketData.status || 'open',
261
261
  priority: ticketData.priority || null,
262
262
  namespace: ticketData.namespace || null,
263
- createdAt: now,
264
- updatedAt: now
263
+ createdAt: ticketData.createdAt || now, // Allow custom timestamps for migration
264
+ updatedAt: ticketData.updatedAt || now
265
265
  };
266
266
 
267
267
  const transaction = this.db.transaction(() => {
@@ -246,8 +246,8 @@ class SupabaseAdapter extends BaseAdapter {
246
246
  }
247
247
 
248
248
  async createTicket(ticketData) {
249
- const id = this.generateTicketId();
250
249
  const now = new Date().toISOString();
250
+ const id = ticketData.id || this.generateTicketId(); // Allow custom ID for migration
251
251
 
252
252
  const ticket = {
253
253
  id,
@@ -258,8 +258,8 @@ class SupabaseAdapter extends BaseAdapter {
258
258
  status: ticketData.status || 'open',
259
259
  priority: ticketData.priority || null,
260
260
  namespace: ticketData.namespace || null,
261
- created_at: now,
262
- updated_at: now
261
+ created_at: ticketData.createdAt || now, // Allow custom timestamps for migration
262
+ updated_at: ticketData.updatedAt || now
263
263
  };
264
264
 
265
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",
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