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 +40 -0
- package/lib/migrate.js +169 -0
- package/lib/storage/json-adapter.js +4 -3
- package/lib/storage/sqlite-adapter.js +3 -3
- package/lib/storage/supabase-adapter.js +3 -3
- package/package.json +1 -1
- package/ticket-server.js +39 -0
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:
|
|
120
|
-
updatedAt:
|
|
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
|
+
"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
|
|