hotsheet 0.9.0 → 0.10.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/dist/cli.js +1114 -962
- package/dist/client/app.global.js +48 -48
- package/dist/client/styles.css +1 -1
- package/package.json +9 -4
package/dist/cli.js
CHANGED
|
@@ -315,7 +315,7 @@ async function getDashboardStats(days) {
|
|
|
315
315
|
ticket_number: r.ticket_number,
|
|
316
316
|
title: r.title,
|
|
317
317
|
completed_at: r.completed_at,
|
|
318
|
-
|
|
318
|
+
hours: Math.max(0, (new Date(r.completed_at).getTime() - new Date(r.created_at).getTime()) / 36e5)
|
|
319
319
|
}));
|
|
320
320
|
const catResult = await db2.query(
|
|
321
321
|
`SELECT category, COUNT(*) as count FROM tickets
|
|
@@ -356,8 +356,8 @@ async function getDashboardStats(days) {
|
|
|
356
356
|
`SELECT COUNT(*) as count FROM tickets WHERE created_at >= $1`,
|
|
357
357
|
[weekStart.toISOString()]
|
|
358
358
|
);
|
|
359
|
-
const
|
|
360
|
-
const medianCycleTimeDays =
|
|
359
|
+
const cycleHours = cycleTime.map((c) => c.hours).sort((a, b) => a - b);
|
|
360
|
+
const medianCycleTimeDays = cycleHours.length > 0 ? Math.round(cycleHours[Math.floor(cycleHours.length / 2)] / 24) : null;
|
|
361
361
|
return {
|
|
362
362
|
throughput,
|
|
363
363
|
cycleTime,
|
|
@@ -450,16 +450,16 @@ __export(channel_config_exports, {
|
|
|
450
450
|
triggerChannel: () => triggerChannel,
|
|
451
451
|
unregisterChannel: () => unregisterChannel
|
|
452
452
|
});
|
|
453
|
-
import { existsSync as existsSync6, readFileSync as
|
|
454
|
-
import { dirname, join as join8, resolve as
|
|
453
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "fs";
|
|
454
|
+
import { dirname, join as join8, resolve as resolve3 } from "path";
|
|
455
455
|
import { fileURLToPath } from "url";
|
|
456
456
|
function getChannelServerPath() {
|
|
457
457
|
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
458
|
-
const distPath =
|
|
458
|
+
const distPath = resolve3(thisDir, "channel.js");
|
|
459
459
|
if (existsSync6(distPath)) {
|
|
460
460
|
return { command: "node", args: [distPath] };
|
|
461
461
|
}
|
|
462
|
-
const srcPath =
|
|
462
|
+
const srcPath = resolve3(thisDir, "channel.ts");
|
|
463
463
|
if (existsSync6(srcPath)) {
|
|
464
464
|
return { command: "npx", args: ["tsx", srcPath] };
|
|
465
465
|
}
|
|
@@ -472,7 +472,7 @@ function registerChannel(dataDir2) {
|
|
|
472
472
|
let config = {};
|
|
473
473
|
if (existsSync6(mcpPath)) {
|
|
474
474
|
try {
|
|
475
|
-
config = JSON.parse(
|
|
475
|
+
config = JSON.parse(readFileSync5(mcpPath, "utf-8"));
|
|
476
476
|
} catch {
|
|
477
477
|
}
|
|
478
478
|
}
|
|
@@ -481,24 +481,24 @@ function registerChannel(dataDir2) {
|
|
|
481
481
|
command,
|
|
482
482
|
args: [...args, "--data-dir", dataDir2]
|
|
483
483
|
};
|
|
484
|
-
|
|
484
|
+
writeFileSync5(mcpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
485
485
|
}
|
|
486
486
|
function unregisterChannel() {
|
|
487
487
|
const cwd = process.cwd();
|
|
488
488
|
const mcpPath = join8(cwd, ".mcp.json");
|
|
489
489
|
if (!existsSync6(mcpPath)) return;
|
|
490
490
|
try {
|
|
491
|
-
const config = JSON.parse(
|
|
491
|
+
const config = JSON.parse(readFileSync5(mcpPath, "utf-8"));
|
|
492
492
|
if (config.mcpServers?.[MCP_SERVER_KEY]) {
|
|
493
493
|
delete config.mcpServers[MCP_SERVER_KEY];
|
|
494
|
-
|
|
494
|
+
writeFileSync5(mcpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
495
495
|
}
|
|
496
496
|
} catch {
|
|
497
497
|
}
|
|
498
498
|
}
|
|
499
499
|
function getChannelPort(dataDir2) {
|
|
500
500
|
try {
|
|
501
|
-
const portStr =
|
|
501
|
+
const portStr = readFileSync5(join8(dataDir2, "channel-port"), "utf-8").trim();
|
|
502
502
|
const port2 = parseInt(portStr, 10);
|
|
503
503
|
return isNaN(port2) ? null : port2;
|
|
504
504
|
} catch {
|
|
@@ -552,7 +552,7 @@ var init_channel_config = __esm({
|
|
|
552
552
|
// src/cli.ts
|
|
553
553
|
import { mkdirSync as mkdirSync6 } from "fs";
|
|
554
554
|
import { tmpdir } from "os";
|
|
555
|
-
import { join as
|
|
555
|
+
import { join as join13, resolve as resolve4 } from "path";
|
|
556
556
|
|
|
557
557
|
// src/backup.ts
|
|
558
558
|
init_connection();
|
|
@@ -751,76 +751,10 @@ function initBackupScheduler(dataDir2) {
|
|
|
751
751
|
// src/cleanup.ts
|
|
752
752
|
import { rmSync as rmSync3 } from "fs";
|
|
753
753
|
|
|
754
|
-
// src/
|
|
755
|
-
|
|
756
|
-
{ id: "issue", label: "Issue", shortLabel: "ISS", color: "#6b7280", shortcutKey: "i", description: "General issues that need attention" },
|
|
757
|
-
{ id: "bug", label: "Bug", shortLabel: "BUG", color: "#ef4444", shortcutKey: "b", description: "Bugs that should be fixed in the codebase" },
|
|
758
|
-
{ id: "feature", label: "Feature", shortLabel: "FEA", color: "#22c55e", shortcutKey: "f", description: "New features to be implemented" },
|
|
759
|
-
{ id: "requirement_change", label: "Req Change", shortLabel: "REQ", color: "#f97316", shortcutKey: "r", description: "Changes to existing requirements" },
|
|
760
|
-
{ id: "task", label: "Task", shortLabel: "TSK", color: "#3b82f6", shortcutKey: "k", description: "General tasks to complete" },
|
|
761
|
-
{ id: "investigation", label: "Investigation", shortLabel: "INV", color: "#8b5cf6", shortcutKey: "g", description: "Items requiring research or analysis" }
|
|
762
|
-
];
|
|
763
|
-
var CATEGORY_PRESETS = [
|
|
764
|
-
{
|
|
765
|
-
id: "software",
|
|
766
|
-
name: "Software Development",
|
|
767
|
-
categories: DEFAULT_CATEGORIES
|
|
768
|
-
},
|
|
769
|
-
{
|
|
770
|
-
id: "design",
|
|
771
|
-
name: "Design / Creative",
|
|
772
|
-
categories: [
|
|
773
|
-
{ id: "concept", label: "Concept", shortLabel: "CON", color: "#8b5cf6", shortcutKey: "c", description: "Design concepts and explorations" },
|
|
774
|
-
{ id: "revision", label: "Revision", shortLabel: "REV", color: "#f97316", shortcutKey: "r", description: "Revisions to existing designs" },
|
|
775
|
-
{ id: "feedback", label: "Feedback", shortLabel: "FDB", color: "#3b82f6", shortcutKey: "f", description: "Client or stakeholder feedback to address" },
|
|
776
|
-
{ id: "asset", label: "Asset", shortLabel: "AST", color: "#22c55e", shortcutKey: "a", description: "Assets to produce or deliver" },
|
|
777
|
-
{ id: "research", label: "Research", shortLabel: "RSC", color: "#6b7280", shortcutKey: "s", description: "User research or competitive analysis" },
|
|
778
|
-
{ id: "bug", label: "Bug", shortLabel: "BUG", color: "#ef4444", shortcutKey: "b", description: "Visual or UI bugs" }
|
|
779
|
-
]
|
|
780
|
-
},
|
|
781
|
-
{
|
|
782
|
-
id: "product",
|
|
783
|
-
name: "Product Management",
|
|
784
|
-
categories: [
|
|
785
|
-
{ id: "epic", label: "Epic", shortLabel: "EPC", color: "#8b5cf6", shortcutKey: "e", description: "Large initiatives spanning multiple stories" },
|
|
786
|
-
{ id: "story", label: "Story", shortLabel: "STY", color: "#3b82f6", shortcutKey: "s", description: "User stories describing desired functionality" },
|
|
787
|
-
{ id: "bug", label: "Bug", shortLabel: "BUG", color: "#ef4444", shortcutKey: "b", description: "Bugs that need to be fixed" },
|
|
788
|
-
{ id: "task", label: "Task", shortLabel: "TSK", color: "#22c55e", shortcutKey: "t", description: "Tasks to complete" },
|
|
789
|
-
{ id: "spike", label: "Spike", shortLabel: "SPK", color: "#f97316", shortcutKey: "k", description: "Research or investigation spikes" },
|
|
790
|
-
{ id: "debt", label: "Tech Debt", shortLabel: "DBT", color: "#6b7280", shortcutKey: "d", description: "Technical debt to address" }
|
|
791
|
-
]
|
|
792
|
-
},
|
|
793
|
-
{
|
|
794
|
-
id: "marketing",
|
|
795
|
-
name: "Marketing",
|
|
796
|
-
categories: [
|
|
797
|
-
{ id: "campaign", label: "Campaign", shortLabel: "CMP", color: "#8b5cf6", shortcutKey: "c", description: "Marketing campaigns" },
|
|
798
|
-
{ id: "content", label: "Content", shortLabel: "CNT", color: "#3b82f6", shortcutKey: "n", description: "Content to create or publish" },
|
|
799
|
-
{ id: "design", label: "Design", shortLabel: "DES", color: "#22c55e", shortcutKey: "d", description: "Design requests and assets" },
|
|
800
|
-
{ id: "analytics", label: "Analytics", shortLabel: "ANL", color: "#f97316", shortcutKey: "a", description: "Analytics and reporting tasks" },
|
|
801
|
-
{ id: "outreach", label: "Outreach", shortLabel: "OUT", color: "#6b7280", shortcutKey: "o", description: "Outreach and partnership activities" },
|
|
802
|
-
{ id: "event", label: "Event", shortLabel: "EVT", color: "#ef4444", shortcutKey: "e", description: "Events to plan or manage" }
|
|
803
|
-
]
|
|
804
|
-
},
|
|
805
|
-
{
|
|
806
|
-
id: "personal",
|
|
807
|
-
name: "Personal",
|
|
808
|
-
categories: [
|
|
809
|
-
{ id: "task", label: "Task", shortLabel: "TSK", color: "#3b82f6", shortcutKey: "t", description: "Things to do" },
|
|
810
|
-
{ id: "idea", label: "Idea", shortLabel: "IDA", color: "#22c55e", shortcutKey: "i", description: "Ideas to explore" },
|
|
811
|
-
{ id: "note", label: "Note", shortLabel: "NTE", color: "#6b7280", shortcutKey: "n", description: "Notes and references" },
|
|
812
|
-
{ id: "errand", label: "Errand", shortLabel: "ERR", color: "#f97316", shortcutKey: "e", description: "Errands and appointments" },
|
|
813
|
-
{ id: "project", label: "Project", shortLabel: "PRJ", color: "#8b5cf6", shortcutKey: "p", description: "Larger projects" },
|
|
814
|
-
{ id: "urgent", label: "Urgent", shortLabel: "URG", color: "#ef4444", shortcutKey: "u", description: "Urgent items" }
|
|
815
|
-
]
|
|
816
|
-
}
|
|
817
|
-
];
|
|
818
|
-
var CATEGORIES = DEFAULT_CATEGORIES.map((c) => ({ value: c.id, label: c.label, color: c.color }));
|
|
819
|
-
var CATEGORY_DESCRIPTIONS = Object.fromEntries(
|
|
820
|
-
DEFAULT_CATEGORIES.map((c) => [c.id, c.description])
|
|
821
|
-
);
|
|
754
|
+
// src/db/tickets.ts
|
|
755
|
+
init_connection();
|
|
822
756
|
|
|
823
|
-
// src/db/
|
|
757
|
+
// src/db/notes.ts
|
|
824
758
|
init_connection();
|
|
825
759
|
var noteCounter = 0;
|
|
826
760
|
function generateNoteId() {
|
|
@@ -863,6 +797,8 @@ async function deleteNote(ticketId, noteId2) {
|
|
|
863
797
|
await db2.query(`UPDATE tickets SET notes = $1, updated_at = NOW() WHERE id = $2`, [JSON.stringify(notes), ticketId]);
|
|
864
798
|
return notes;
|
|
865
799
|
}
|
|
800
|
+
|
|
801
|
+
// src/db/tickets.ts
|
|
866
802
|
async function nextTicketNumber() {
|
|
867
803
|
const db2 = await getDb();
|
|
868
804
|
const result = await db2.query("SELECT nextval('ticket_seq')");
|
|
@@ -1074,38 +1010,6 @@ async function toggleUpNext(id) {
|
|
|
1074
1010
|
);
|
|
1075
1011
|
return result.rows[0] ?? null;
|
|
1076
1012
|
}
|
|
1077
|
-
async function addAttachment(ticketId, originalFilename, storedPath) {
|
|
1078
|
-
const db2 = await getDb();
|
|
1079
|
-
const result = await db2.query(
|
|
1080
|
-
`INSERT INTO attachments (ticket_id, original_filename, stored_path) VALUES ($1, $2, $3) RETURNING *`,
|
|
1081
|
-
[ticketId, originalFilename, storedPath]
|
|
1082
|
-
);
|
|
1083
|
-
return result.rows[0];
|
|
1084
|
-
}
|
|
1085
|
-
async function getAttachments(ticketId) {
|
|
1086
|
-
const db2 = await getDb();
|
|
1087
|
-
const result = await db2.query(
|
|
1088
|
-
`SELECT * FROM attachments WHERE ticket_id = $1 ORDER BY created_at ASC`,
|
|
1089
|
-
[ticketId]
|
|
1090
|
-
);
|
|
1091
|
-
return result.rows;
|
|
1092
|
-
}
|
|
1093
|
-
async function getAttachment(id) {
|
|
1094
|
-
const db2 = await getDb();
|
|
1095
|
-
const result = await db2.query(
|
|
1096
|
-
`SELECT * FROM attachments WHERE id = $1`,
|
|
1097
|
-
[id]
|
|
1098
|
-
);
|
|
1099
|
-
return result.rows[0] ?? null;
|
|
1100
|
-
}
|
|
1101
|
-
async function deleteAttachment(id) {
|
|
1102
|
-
const db2 = await getDb();
|
|
1103
|
-
const result = await db2.query(
|
|
1104
|
-
`DELETE FROM attachments WHERE id = $1 RETURNING *`,
|
|
1105
|
-
[id]
|
|
1106
|
-
);
|
|
1107
|
-
return result.rows[0] ?? null;
|
|
1108
|
-
}
|
|
1109
1013
|
async function getTicketsForCleanup(verifiedDays = 30, trashDays = 3) {
|
|
1110
1014
|
const db2 = await getDb();
|
|
1111
1015
|
const result = await db2.query(`
|
|
@@ -1210,69 +1114,6 @@ async function queryTickets(logic, conditions, sortBy, sortDir, requiredTag) {
|
|
|
1210
1114
|
);
|
|
1211
1115
|
return result.rows;
|
|
1212
1116
|
}
|
|
1213
|
-
function normalizeTag(input) {
|
|
1214
|
-
return input.replace(/[^a-zA-Z0-9]+/g, " ").trim().toLowerCase();
|
|
1215
|
-
}
|
|
1216
|
-
function extractBracketTags(input) {
|
|
1217
|
-
const tags = [];
|
|
1218
|
-
const cleaned = input.replace(/\[([^\]]*)\]/g, (_match, content) => {
|
|
1219
|
-
const tag = normalizeTag(content);
|
|
1220
|
-
if (tag && !tags.includes(tag)) tags.push(tag);
|
|
1221
|
-
return " ";
|
|
1222
|
-
});
|
|
1223
|
-
const title = cleaned.replace(/\s+/g, " ").trim();
|
|
1224
|
-
return { title, tags };
|
|
1225
|
-
}
|
|
1226
|
-
async function getAllTags() {
|
|
1227
|
-
const db2 = await getDb();
|
|
1228
|
-
const result = await db2.query(`SELECT DISTINCT tags FROM tickets WHERE tags != '[]' AND status != 'deleted'`);
|
|
1229
|
-
const tagSet = /* @__PURE__ */ new Set();
|
|
1230
|
-
for (const row of result.rows) {
|
|
1231
|
-
try {
|
|
1232
|
-
const parsed = JSON.parse(row.tags);
|
|
1233
|
-
if (Array.isArray(parsed)) {
|
|
1234
|
-
for (const tag of parsed) {
|
|
1235
|
-
if (typeof tag === "string" && tag.trim()) {
|
|
1236
|
-
const norm = normalizeTag(tag);
|
|
1237
|
-
if (norm) tagSet.add(norm);
|
|
1238
|
-
}
|
|
1239
|
-
}
|
|
1240
|
-
}
|
|
1241
|
-
} catch {
|
|
1242
|
-
}
|
|
1243
|
-
}
|
|
1244
|
-
return Array.from(tagSet).sort();
|
|
1245
|
-
}
|
|
1246
|
-
async function getCategories() {
|
|
1247
|
-
const settings = await getSettings();
|
|
1248
|
-
if (settings.categories) {
|
|
1249
|
-
try {
|
|
1250
|
-
const parsed = JSON.parse(settings.categories);
|
|
1251
|
-
if (Array.isArray(parsed) && parsed.length > 0) return parsed;
|
|
1252
|
-
} catch {
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
|
-
return DEFAULT_CATEGORIES;
|
|
1256
|
-
}
|
|
1257
|
-
async function saveCategories(categories) {
|
|
1258
|
-
await updateSetting("categories", JSON.stringify(categories));
|
|
1259
|
-
}
|
|
1260
|
-
async function getSettings() {
|
|
1261
|
-
const db2 = await getDb();
|
|
1262
|
-
const result = await db2.query("SELECT key, value FROM settings");
|
|
1263
|
-
const settings = {};
|
|
1264
|
-
for (const row of result.rows) {
|
|
1265
|
-
settings[row.key] = row.value;
|
|
1266
|
-
}
|
|
1267
|
-
return settings;
|
|
1268
|
-
}
|
|
1269
|
-
async function updateSetting(key, value) {
|
|
1270
|
-
const db2 = await getDb();
|
|
1271
|
-
await db2.query(
|
|
1272
|
-
`INSERT INTO settings (key, value) VALUES ($1, $2) ON CONFLICT (key) DO UPDATE SET value = $2`,
|
|
1273
|
-
[key, value]
|
|
1274
|
-
);
|
|
1275
|
-
}
|
|
1276
1117
|
async function restoreTicket(id) {
|
|
1277
1118
|
return updateTicket(id, { status: "not_started" });
|
|
1278
1119
|
}
|
|
@@ -1324,86 +1165,259 @@ async function getTicketStats() {
|
|
|
1324
1165
|
};
|
|
1325
1166
|
}
|
|
1326
1167
|
|
|
1327
|
-
// src/
|
|
1328
|
-
async function cleanupAttachments() {
|
|
1329
|
-
try {
|
|
1330
|
-
const settings = await getSettings();
|
|
1331
|
-
const verifiedDays = parseInt(settings.verified_cleanup_days, 10) || 30;
|
|
1332
|
-
const trashDays = parseInt(settings.trash_cleanup_days, 10) || 3;
|
|
1333
|
-
const tickets = await getTicketsForCleanup(verifiedDays, trashDays);
|
|
1334
|
-
if (tickets.length === 0) return;
|
|
1335
|
-
let cleaned = 0;
|
|
1336
|
-
for (const ticket of tickets) {
|
|
1337
|
-
const attachments = await getAttachments(ticket.id);
|
|
1338
|
-
for (const att of attachments) {
|
|
1339
|
-
try {
|
|
1340
|
-
rmSync3(att.stored_path, { force: true });
|
|
1341
|
-
} catch {
|
|
1342
|
-
}
|
|
1343
|
-
}
|
|
1344
|
-
await hardDeleteTicket(ticket.id);
|
|
1345
|
-
cleaned++;
|
|
1346
|
-
}
|
|
1347
|
-
if (cleaned > 0) {
|
|
1348
|
-
console.log(` Cleaned up ${cleaned} old ticket(s) and their attachments.`);
|
|
1349
|
-
}
|
|
1350
|
-
} catch (err) {
|
|
1351
|
-
console.error("Attachment cleanup failed:", err);
|
|
1352
|
-
}
|
|
1353
|
-
}
|
|
1354
|
-
|
|
1355
|
-
// src/cli.ts
|
|
1168
|
+
// src/db/attachments.ts
|
|
1356
1169
|
init_connection();
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
lockPath = join4(dataDir2, "hotsheet.lock");
|
|
1365
|
-
if (existsSync3(lockPath)) {
|
|
1366
|
-
try {
|
|
1367
|
-
const contents = JSON.parse(readFileSync3(lockPath, "utf-8"));
|
|
1368
|
-
const pid = contents.pid;
|
|
1369
|
-
try {
|
|
1370
|
-
process.kill(pid, 0);
|
|
1371
|
-
console.error(`
|
|
1372
|
-
Error: Another Hot Sheet instance (PID ${pid}) is already using this data directory.`);
|
|
1373
|
-
console.error(` Directory: ${dataDir2}`);
|
|
1374
|
-
console.error(` Stop that instance first, or use --data-dir to point to a different location.
|
|
1375
|
-
`);
|
|
1376
|
-
process.exit(1);
|
|
1377
|
-
} catch {
|
|
1378
|
-
console.log(` Removing stale lock from PID ${pid}`);
|
|
1379
|
-
rmSync4(lockPath, { force: true });
|
|
1380
|
-
}
|
|
1381
|
-
} catch {
|
|
1382
|
-
rmSync4(lockPath, { force: true });
|
|
1383
|
-
}
|
|
1384
|
-
}
|
|
1385
|
-
writeFileSync3(lockPath, JSON.stringify({ pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
1386
|
-
const cleanup = () => releaseLock();
|
|
1387
|
-
process.on("exit", cleanup);
|
|
1388
|
-
process.on("SIGINT", () => {
|
|
1389
|
-
cleanup();
|
|
1390
|
-
process.exit(0);
|
|
1391
|
-
});
|
|
1392
|
-
process.on("SIGTERM", () => {
|
|
1393
|
-
cleanup();
|
|
1394
|
-
process.exit(0);
|
|
1395
|
-
});
|
|
1170
|
+
async function addAttachment(ticketId, originalFilename, storedPath) {
|
|
1171
|
+
const db2 = await getDb();
|
|
1172
|
+
const result = await db2.query(
|
|
1173
|
+
`INSERT INTO attachments (ticket_id, original_filename, stored_path) VALUES ($1, $2, $3) RETURNING *`,
|
|
1174
|
+
[ticketId, originalFilename, storedPath]
|
|
1175
|
+
);
|
|
1176
|
+
return result.rows[0];
|
|
1396
1177
|
}
|
|
1397
|
-
function
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
}
|
|
1178
|
+
async function getAttachments(ticketId) {
|
|
1179
|
+
const db2 = await getDb();
|
|
1180
|
+
const result = await db2.query(
|
|
1181
|
+
`SELECT * FROM attachments WHERE ticket_id = $1 ORDER BY created_at ASC`,
|
|
1182
|
+
[ticketId]
|
|
1183
|
+
);
|
|
1184
|
+
return result.rows;
|
|
1405
1185
|
}
|
|
1406
|
-
|
|
1186
|
+
async function getAttachment(id) {
|
|
1187
|
+
const db2 = await getDb();
|
|
1188
|
+
const result = await db2.query(
|
|
1189
|
+
`SELECT * FROM attachments WHERE id = $1`,
|
|
1190
|
+
[id]
|
|
1191
|
+
);
|
|
1192
|
+
return result.rows[0] ?? null;
|
|
1193
|
+
}
|
|
1194
|
+
async function deleteAttachment(id) {
|
|
1195
|
+
const db2 = await getDb();
|
|
1196
|
+
const result = await db2.query(
|
|
1197
|
+
`DELETE FROM attachments WHERE id = $1 RETURNING *`,
|
|
1198
|
+
[id]
|
|
1199
|
+
);
|
|
1200
|
+
return result.rows[0] ?? null;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// src/types.ts
|
|
1204
|
+
var DEFAULT_CATEGORIES = [
|
|
1205
|
+
{ id: "issue", label: "Issue", shortLabel: "ISS", color: "#6b7280", shortcutKey: "i", description: "General issues that need attention" },
|
|
1206
|
+
{ id: "bug", label: "Bug", shortLabel: "BUG", color: "#ef4444", shortcutKey: "b", description: "Bugs that should be fixed in the codebase" },
|
|
1207
|
+
{ id: "feature", label: "Feature", shortLabel: "FEA", color: "#22c55e", shortcutKey: "f", description: "New features to be implemented" },
|
|
1208
|
+
{ id: "requirement_change", label: "Req Change", shortLabel: "REQ", color: "#f97316", shortcutKey: "r", description: "Changes to existing requirements" },
|
|
1209
|
+
{ id: "task", label: "Task", shortLabel: "TSK", color: "#3b82f6", shortcutKey: "k", description: "General tasks to complete" },
|
|
1210
|
+
{ id: "investigation", label: "Investigation", shortLabel: "INV", color: "#8b5cf6", shortcutKey: "g", description: "Items requiring research or analysis" }
|
|
1211
|
+
];
|
|
1212
|
+
var CATEGORY_PRESETS = [
|
|
1213
|
+
{
|
|
1214
|
+
id: "software",
|
|
1215
|
+
name: "Software Development",
|
|
1216
|
+
categories: DEFAULT_CATEGORIES
|
|
1217
|
+
},
|
|
1218
|
+
{
|
|
1219
|
+
id: "design",
|
|
1220
|
+
name: "Design / Creative",
|
|
1221
|
+
categories: [
|
|
1222
|
+
{ id: "concept", label: "Concept", shortLabel: "CON", color: "#8b5cf6", shortcutKey: "c", description: "Design concepts and explorations" },
|
|
1223
|
+
{ id: "revision", label: "Revision", shortLabel: "REV", color: "#f97316", shortcutKey: "r", description: "Revisions to existing designs" },
|
|
1224
|
+
{ id: "feedback", label: "Feedback", shortLabel: "FDB", color: "#3b82f6", shortcutKey: "f", description: "Client or stakeholder feedback to address" },
|
|
1225
|
+
{ id: "asset", label: "Asset", shortLabel: "AST", color: "#22c55e", shortcutKey: "a", description: "Assets to produce or deliver" },
|
|
1226
|
+
{ id: "research", label: "Research", shortLabel: "RSC", color: "#6b7280", shortcutKey: "s", description: "User research or competitive analysis" },
|
|
1227
|
+
{ id: "bug", label: "Bug", shortLabel: "BUG", color: "#ef4444", shortcutKey: "b", description: "Visual or UI bugs" }
|
|
1228
|
+
]
|
|
1229
|
+
},
|
|
1230
|
+
{
|
|
1231
|
+
id: "product",
|
|
1232
|
+
name: "Product Management",
|
|
1233
|
+
categories: [
|
|
1234
|
+
{ id: "epic", label: "Epic", shortLabel: "EPC", color: "#8b5cf6", shortcutKey: "e", description: "Large initiatives spanning multiple stories" },
|
|
1235
|
+
{ id: "story", label: "Story", shortLabel: "STY", color: "#3b82f6", shortcutKey: "s", description: "User stories describing desired functionality" },
|
|
1236
|
+
{ id: "bug", label: "Bug", shortLabel: "BUG", color: "#ef4444", shortcutKey: "b", description: "Bugs that need to be fixed" },
|
|
1237
|
+
{ id: "task", label: "Task", shortLabel: "TSK", color: "#22c55e", shortcutKey: "t", description: "Tasks to complete" },
|
|
1238
|
+
{ id: "spike", label: "Spike", shortLabel: "SPK", color: "#f97316", shortcutKey: "k", description: "Research or investigation spikes" },
|
|
1239
|
+
{ id: "debt", label: "Tech Debt", shortLabel: "DBT", color: "#6b7280", shortcutKey: "d", description: "Technical debt to address" }
|
|
1240
|
+
]
|
|
1241
|
+
},
|
|
1242
|
+
{
|
|
1243
|
+
id: "marketing",
|
|
1244
|
+
name: "Marketing",
|
|
1245
|
+
categories: [
|
|
1246
|
+
{ id: "campaign", label: "Campaign", shortLabel: "CMP", color: "#8b5cf6", shortcutKey: "c", description: "Marketing campaigns" },
|
|
1247
|
+
{ id: "content", label: "Content", shortLabel: "CNT", color: "#3b82f6", shortcutKey: "n", description: "Content to create or publish" },
|
|
1248
|
+
{ id: "design", label: "Design", shortLabel: "DES", color: "#22c55e", shortcutKey: "d", description: "Design requests and assets" },
|
|
1249
|
+
{ id: "analytics", label: "Analytics", shortLabel: "ANL", color: "#f97316", shortcutKey: "a", description: "Analytics and reporting tasks" },
|
|
1250
|
+
{ id: "outreach", label: "Outreach", shortLabel: "OUT", color: "#6b7280", shortcutKey: "o", description: "Outreach and partnership activities" },
|
|
1251
|
+
{ id: "event", label: "Event", shortLabel: "EVT", color: "#ef4444", shortcutKey: "e", description: "Events to plan or manage" }
|
|
1252
|
+
]
|
|
1253
|
+
},
|
|
1254
|
+
{
|
|
1255
|
+
id: "personal",
|
|
1256
|
+
name: "Personal",
|
|
1257
|
+
categories: [
|
|
1258
|
+
{ id: "task", label: "Task", shortLabel: "TSK", color: "#3b82f6", shortcutKey: "t", description: "Things to do" },
|
|
1259
|
+
{ id: "idea", label: "Idea", shortLabel: "IDA", color: "#22c55e", shortcutKey: "i", description: "Ideas to explore" },
|
|
1260
|
+
{ id: "note", label: "Note", shortLabel: "NTE", color: "#6b7280", shortcutKey: "n", description: "Notes and references" },
|
|
1261
|
+
{ id: "errand", label: "Errand", shortLabel: "ERR", color: "#f97316", shortcutKey: "e", description: "Errands and appointments" },
|
|
1262
|
+
{ id: "project", label: "Project", shortLabel: "PRJ", color: "#8b5cf6", shortcutKey: "p", description: "Larger projects" },
|
|
1263
|
+
{ id: "urgent", label: "Urgent", shortLabel: "URG", color: "#ef4444", shortcutKey: "u", description: "Urgent items" }
|
|
1264
|
+
]
|
|
1265
|
+
}
|
|
1266
|
+
];
|
|
1267
|
+
var CATEGORIES = DEFAULT_CATEGORIES.map((c) => ({ value: c.id, label: c.label, color: c.color }));
|
|
1268
|
+
var CATEGORY_DESCRIPTIONS = Object.fromEntries(
|
|
1269
|
+
DEFAULT_CATEGORIES.map((c) => [c.id, c.description])
|
|
1270
|
+
);
|
|
1271
|
+
|
|
1272
|
+
// src/db/settings.ts
|
|
1273
|
+
init_connection();
|
|
1274
|
+
async function getSettings() {
|
|
1275
|
+
const db2 = await getDb();
|
|
1276
|
+
const result = await db2.query("SELECT key, value FROM settings");
|
|
1277
|
+
const settings = {};
|
|
1278
|
+
for (const row of result.rows) {
|
|
1279
|
+
settings[row.key] = row.value;
|
|
1280
|
+
}
|
|
1281
|
+
return settings;
|
|
1282
|
+
}
|
|
1283
|
+
async function updateSetting(key, value) {
|
|
1284
|
+
const db2 = await getDb();
|
|
1285
|
+
await db2.query(
|
|
1286
|
+
`INSERT INTO settings (key, value) VALUES ($1, $2) ON CONFLICT (key) DO UPDATE SET value = $2`,
|
|
1287
|
+
[key, value]
|
|
1288
|
+
);
|
|
1289
|
+
}
|
|
1290
|
+
async function getCategories() {
|
|
1291
|
+
const settings = await getSettings();
|
|
1292
|
+
if (settings.categories) {
|
|
1293
|
+
try {
|
|
1294
|
+
const parsed = JSON.parse(settings.categories);
|
|
1295
|
+
if (Array.isArray(parsed) && parsed.length > 0) return parsed;
|
|
1296
|
+
} catch {
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
return DEFAULT_CATEGORIES;
|
|
1300
|
+
}
|
|
1301
|
+
async function saveCategories(categories) {
|
|
1302
|
+
await updateSetting("categories", JSON.stringify(categories));
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
// src/db/tags.ts
|
|
1306
|
+
init_connection();
|
|
1307
|
+
function normalizeTag(input) {
|
|
1308
|
+
return input.replace(/[^a-zA-Z0-9]+/g, " ").trim().toLowerCase();
|
|
1309
|
+
}
|
|
1310
|
+
function extractBracketTags(input) {
|
|
1311
|
+
const tags = [];
|
|
1312
|
+
const cleaned = input.replace(/\[([^\]]*)\]/g, (_match, content) => {
|
|
1313
|
+
const tag = normalizeTag(content);
|
|
1314
|
+
if (tag && !tags.includes(tag)) tags.push(tag);
|
|
1315
|
+
return " ";
|
|
1316
|
+
});
|
|
1317
|
+
const title = cleaned.replace(/\s+/g, " ").trim();
|
|
1318
|
+
return { title, tags };
|
|
1319
|
+
}
|
|
1320
|
+
async function getAllTags() {
|
|
1321
|
+
const db2 = await getDb();
|
|
1322
|
+
const result = await db2.query(`SELECT DISTINCT tags FROM tickets WHERE tags != '[]' AND status != 'deleted'`);
|
|
1323
|
+
const tagSet = /* @__PURE__ */ new Set();
|
|
1324
|
+
for (const row of result.rows) {
|
|
1325
|
+
try {
|
|
1326
|
+
const parsed = JSON.parse(row.tags);
|
|
1327
|
+
if (Array.isArray(parsed)) {
|
|
1328
|
+
for (const tag of parsed) {
|
|
1329
|
+
if (typeof tag === "string" && tag.trim()) {
|
|
1330
|
+
const norm = normalizeTag(tag);
|
|
1331
|
+
if (norm) tagSet.add(norm);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
} catch {
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
return Array.from(tagSet).sort();
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
// src/cleanup.ts
|
|
1342
|
+
async function cleanupAttachments() {
|
|
1343
|
+
try {
|
|
1344
|
+
const settings = await getSettings();
|
|
1345
|
+
const verifiedDays = parseInt(settings.verified_cleanup_days, 10) || 30;
|
|
1346
|
+
const trashDays = parseInt(settings.trash_cleanup_days, 10) || 3;
|
|
1347
|
+
const tickets = await getTicketsForCleanup(verifiedDays, trashDays);
|
|
1348
|
+
if (tickets.length === 0) return;
|
|
1349
|
+
let cleaned = 0;
|
|
1350
|
+
for (const ticket of tickets) {
|
|
1351
|
+
const attachments = await getAttachments(ticket.id);
|
|
1352
|
+
for (const att of attachments) {
|
|
1353
|
+
try {
|
|
1354
|
+
rmSync3(att.stored_path, { force: true });
|
|
1355
|
+
} catch {
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
await hardDeleteTicket(ticket.id);
|
|
1359
|
+
cleaned++;
|
|
1360
|
+
}
|
|
1361
|
+
if (cleaned > 0) {
|
|
1362
|
+
console.log(` Cleaned up ${cleaned} old ticket(s) and their attachments.`);
|
|
1363
|
+
}
|
|
1364
|
+
} catch (err) {
|
|
1365
|
+
console.error("Attachment cleanup failed:", err);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
// src/cli.ts
|
|
1370
|
+
init_connection();
|
|
1371
|
+
init_file_settings();
|
|
1372
|
+
|
|
1373
|
+
// src/lock.ts
|
|
1374
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, rmSync as rmSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
1375
|
+
import { join as join4 } from "path";
|
|
1376
|
+
var lockPath = null;
|
|
1377
|
+
function acquireLock(dataDir2) {
|
|
1378
|
+
lockPath = join4(dataDir2, "hotsheet.lock");
|
|
1379
|
+
if (existsSync3(lockPath)) {
|
|
1380
|
+
try {
|
|
1381
|
+
const contents = JSON.parse(readFileSync3(lockPath, "utf-8"));
|
|
1382
|
+
const pid = contents.pid;
|
|
1383
|
+
try {
|
|
1384
|
+
process.kill(pid, 0);
|
|
1385
|
+
console.error(`
|
|
1386
|
+
Error: Another Hot Sheet instance (PID ${pid}) is already using this data directory.`);
|
|
1387
|
+
console.error(` Directory: ${dataDir2}`);
|
|
1388
|
+
console.error(` Stop that instance first, or use --data-dir to point to a different location.
|
|
1389
|
+
`);
|
|
1390
|
+
process.exit(1);
|
|
1391
|
+
} catch {
|
|
1392
|
+
console.log(` Removing stale lock from PID ${pid}`);
|
|
1393
|
+
rmSync4(lockPath, { force: true });
|
|
1394
|
+
}
|
|
1395
|
+
} catch {
|
|
1396
|
+
rmSync4(lockPath, { force: true });
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
writeFileSync3(lockPath, JSON.stringify({ pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
1400
|
+
const cleanup = () => releaseLock();
|
|
1401
|
+
process.on("exit", cleanup);
|
|
1402
|
+
process.on("SIGINT", () => {
|
|
1403
|
+
cleanup();
|
|
1404
|
+
process.exit(0);
|
|
1405
|
+
});
|
|
1406
|
+
process.on("SIGTERM", () => {
|
|
1407
|
+
cleanup();
|
|
1408
|
+
process.exit(0);
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
1411
|
+
function releaseLock() {
|
|
1412
|
+
if (lockPath) {
|
|
1413
|
+
try {
|
|
1414
|
+
rmSync4(lockPath, { force: true });
|
|
1415
|
+
} catch {
|
|
1416
|
+
}
|
|
1417
|
+
lockPath = null;
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1407
1421
|
// src/demo.ts
|
|
1408
1422
|
init_connection();
|
|
1409
1423
|
var DEMO_SCENARIOS = [
|
|
@@ -2387,290 +2401,31 @@ init_file_settings();
|
|
|
2387
2401
|
import { serve } from "@hono/node-server";
|
|
2388
2402
|
import { exec } from "child_process";
|
|
2389
2403
|
import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
|
|
2390
|
-
import { Hono as
|
|
2391
|
-
import { dirname as dirname2, join as
|
|
2404
|
+
import { Hono as Hono9 } from "hono";
|
|
2405
|
+
import { dirname as dirname2, join as join11 } from "path";
|
|
2392
2406
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2393
2407
|
|
|
2394
2408
|
// src/routes/api.ts
|
|
2395
|
-
import {
|
|
2409
|
+
import { Hono as Hono6 } from "hono";
|
|
2410
|
+
|
|
2411
|
+
// src/routes/attachments.ts
|
|
2412
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, rmSync as rmSync5 } from "fs";
|
|
2396
2413
|
import { Hono } from "hono";
|
|
2397
|
-
import { basename, extname, join as
|
|
2414
|
+
import { basename, extname, join as join7, resolve as resolve2 } from "path";
|
|
2398
2415
|
|
|
2399
|
-
// src/
|
|
2416
|
+
// src/sync/markdown.ts
|
|
2417
|
+
import { writeFileSync as writeFileSync4 } from "fs";
|
|
2418
|
+
import { join as join6 } from "path";
|
|
2400
2419
|
init_file_settings();
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
var
|
|
2404
|
-
var
|
|
2405
|
-
var
|
|
2406
|
-
var
|
|
2407
|
-
function
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
}
|
|
2411
|
-
function setSkillCategories(categories) {
|
|
2412
|
-
skillCategories = categories;
|
|
2413
|
-
}
|
|
2414
|
-
function buildTicketSkills() {
|
|
2415
|
-
return skillCategories.map((cat) => ({
|
|
2416
|
-
name: `hs-${cat.id.replace(/_/g, "-")}`,
|
|
2417
|
-
category: cat.id,
|
|
2418
|
-
label: cat.label.toLowerCase(),
|
|
2419
|
-
description: cat.description
|
|
2420
|
-
}));
|
|
2421
|
-
}
|
|
2422
|
-
function versionHeader() {
|
|
2423
|
-
return `<!-- hotsheet-skill-version: ${SKILL_VERSION} -->`;
|
|
2424
|
-
}
|
|
2425
|
-
function parseVersionHeader(content) {
|
|
2426
|
-
const match = content.match(/<!-- hotsheet-skill-version: (\d+)(?: port: \d+)? -->/);
|
|
2427
|
-
if (!match) return null;
|
|
2428
|
-
return parseInt(match[1], 10);
|
|
2429
|
-
}
|
|
2430
|
-
function updateFile(path, content) {
|
|
2431
|
-
if (existsSync5(path)) {
|
|
2432
|
-
const existing = readFileSync5(path, "utf-8");
|
|
2433
|
-
const version = parseVersionHeader(existing);
|
|
2434
|
-
if (version !== null && version >= SKILL_VERSION) {
|
|
2435
|
-
return false;
|
|
2436
|
-
}
|
|
2437
|
-
}
|
|
2438
|
-
writeFileSync4(path, content, "utf-8");
|
|
2439
|
-
return true;
|
|
2440
|
-
}
|
|
2441
|
-
function ticketSkillBody(skill) {
|
|
2442
|
-
const settings = readFileSettings(skillDataDir);
|
|
2443
|
-
const secret = settings.secret || "";
|
|
2444
|
-
const secretLine = secret ? ` -H "X-Hotsheet-Secret: ${secret}" \\` : "";
|
|
2445
|
-
const lines = [
|
|
2446
|
-
`Create a new Hot Sheet **${skill.label}** ticket. ${skill.description}.`,
|
|
2447
|
-
"",
|
|
2448
|
-
"**Parsing the input:**",
|
|
2449
|
-
'- If the input starts with "next", "up next", or "do next" (case-insensitive), set `up_next` to `true` and use the remaining text as the title',
|
|
2450
|
-
"- Otherwise, use the entire input as the title",
|
|
2451
|
-
"",
|
|
2452
|
-
"**Create the ticket** by running:",
|
|
2453
|
-
"```bash",
|
|
2454
|
-
`curl -s -X POST http://localhost:${skillPort}/api/tickets \\`,
|
|
2455
|
-
' -H "Content-Type: application/json" \\'
|
|
2456
|
-
];
|
|
2457
|
-
if (secretLine) lines.push(secretLine);
|
|
2458
|
-
lines.push(
|
|
2459
|
-
` -d '{"title": "<TITLE>", "defaults": {"category": "${skill.category}", "up_next": <true|false>}}'`,
|
|
2460
|
-
"```",
|
|
2461
|
-
"",
|
|
2462
|
-
`If the request fails (connection refused or 403), re-read \`.hotsheet/settings.json\` for the current \`port\` and \`secret\` values \u2014 you may be connecting to the wrong Hot Sheet instance.`,
|
|
2463
|
-
"",
|
|
2464
|
-
"Report the created ticket number and title to the user."
|
|
2465
|
-
);
|
|
2466
|
-
return lines.join("\n");
|
|
2467
|
-
}
|
|
2468
|
-
function mainSkillBody() {
|
|
2469
|
-
const worklistRel = relative(process.cwd(), join6(skillDataDir, "worklist.md"));
|
|
2470
|
-
const settingsRel = relative(process.cwd(), join6(skillDataDir, "settings.json"));
|
|
2471
|
-
return [
|
|
2472
|
-
`Base directory for this skill: ${join6(process.cwd(), ".claude", "skills", "hotsheet")}`,
|
|
2473
|
-
"",
|
|
2474
|
-
`Read \`${worklistRel}\` and work through the tickets in priority order.`,
|
|
2475
|
-
"",
|
|
2476
|
-
"For each ticket:",
|
|
2477
|
-
"1. Read the ticket details carefully",
|
|
2478
|
-
"2. Implement the work described",
|
|
2479
|
-
"3. When complete, mark it done via the Hot Sheet UI",
|
|
2480
|
-
"",
|
|
2481
|
-
"Work through them in order of priority, where reasonable.",
|
|
2482
|
-
"",
|
|
2483
|
-
`If API calls fail (connection refused or 403), re-read \`${settingsRel}\` for the current \`port\` and \`secret\` values \u2014 you may be connecting to the wrong Hot Sheet instance.`
|
|
2484
|
-
].join("\n");
|
|
2485
|
-
}
|
|
2486
|
-
var HOTSHEET_ALLOW_PATTERNS = [
|
|
2487
|
-
"Bash(curl * http://localhost:417*/api/*)",
|
|
2488
|
-
"Bash(curl * http://localhost:418*/api/*)"
|
|
2489
|
-
];
|
|
2490
|
-
var HOTSHEET_CURL_RE = /^Bash\(curl \* http:\/\/localhost:\d+\/api\/\*\)$|^Bash\(curl \* http:\/\/localhost:41[78]\*\/api\/\*\)$/;
|
|
2491
|
-
function ensureClaudePermissions(cwd) {
|
|
2492
|
-
if (skillPort < 4170 || skillPort > 4189) return false;
|
|
2493
|
-
const settingsPath2 = join6(cwd, ".claude", "settings.json");
|
|
2494
|
-
let settings = {};
|
|
2495
|
-
if (existsSync5(settingsPath2)) {
|
|
2496
|
-
try {
|
|
2497
|
-
settings = JSON.parse(readFileSync5(settingsPath2, "utf-8"));
|
|
2498
|
-
} catch {
|
|
2499
|
-
}
|
|
2500
|
-
}
|
|
2501
|
-
if (!settings.permissions) settings.permissions = {};
|
|
2502
|
-
if (!settings.permissions.allow) settings.permissions.allow = [];
|
|
2503
|
-
const allow = settings.permissions.allow;
|
|
2504
|
-
if (HOTSHEET_ALLOW_PATTERNS.every((p) => allow.includes(p))) return false;
|
|
2505
|
-
settings.permissions.allow = allow.filter((p) => !HOTSHEET_CURL_RE.test(p));
|
|
2506
|
-
settings.permissions.allow.push(...HOTSHEET_ALLOW_PATTERNS);
|
|
2507
|
-
writeFileSync4(settingsPath2, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
2508
|
-
return true;
|
|
2509
|
-
}
|
|
2510
|
-
function ensureClaudeSkills(cwd) {
|
|
2511
|
-
let updated = false;
|
|
2512
|
-
const skillsDir = join6(cwd, ".claude", "skills");
|
|
2513
|
-
if (ensureClaudePermissions(cwd)) updated = true;
|
|
2514
|
-
const mainDir = join6(skillsDir, "hotsheet");
|
|
2515
|
-
mkdirSync3(mainDir, { recursive: true });
|
|
2516
|
-
const mainContent = [
|
|
2517
|
-
"---",
|
|
2518
|
-
"name: hotsheet",
|
|
2519
|
-
"description: Read the Hot Sheet worklist and work through the current priority items",
|
|
2520
|
-
"allowed-tools: Read, Grep, Glob, Edit, Write, Bash",
|
|
2521
|
-
"---",
|
|
2522
|
-
versionHeader(),
|
|
2523
|
-
"",
|
|
2524
|
-
mainSkillBody(),
|
|
2525
|
-
""
|
|
2526
|
-
].join("\n");
|
|
2527
|
-
if (updateFile(join6(mainDir, "SKILL.md"), mainContent)) updated = true;
|
|
2528
|
-
for (const skill of buildTicketSkills()) {
|
|
2529
|
-
const dir = join6(skillsDir, skill.name);
|
|
2530
|
-
mkdirSync3(dir, { recursive: true });
|
|
2531
|
-
const content = [
|
|
2532
|
-
"---",
|
|
2533
|
-
`name: ${skill.name}`,
|
|
2534
|
-
`description: Create a new ${skill.label} ticket in Hot Sheet`,
|
|
2535
|
-
"allowed-tools: Bash",
|
|
2536
|
-
"---",
|
|
2537
|
-
versionHeader(),
|
|
2538
|
-
"",
|
|
2539
|
-
ticketSkillBody(skill),
|
|
2540
|
-
""
|
|
2541
|
-
].join("\n");
|
|
2542
|
-
if (updateFile(join6(dir, "SKILL.md"), content)) updated = true;
|
|
2543
|
-
}
|
|
2544
|
-
return updated;
|
|
2545
|
-
}
|
|
2546
|
-
function ensureCursorRules(cwd) {
|
|
2547
|
-
let updated = false;
|
|
2548
|
-
const rulesDir = join6(cwd, ".cursor", "rules");
|
|
2549
|
-
mkdirSync3(rulesDir, { recursive: true });
|
|
2550
|
-
const mainContent = [
|
|
2551
|
-
"---",
|
|
2552
|
-
"description: Read the Hot Sheet worklist and work through the current priority items",
|
|
2553
|
-
"alwaysApply: false",
|
|
2554
|
-
"---",
|
|
2555
|
-
versionHeader(),
|
|
2556
|
-
"",
|
|
2557
|
-
mainSkillBody(),
|
|
2558
|
-
""
|
|
2559
|
-
].join("\n");
|
|
2560
|
-
if (updateFile(join6(rulesDir, "hotsheet.mdc"), mainContent)) updated = true;
|
|
2561
|
-
for (const skill of buildTicketSkills()) {
|
|
2562
|
-
const content = [
|
|
2563
|
-
"---",
|
|
2564
|
-
`description: Create a new ${skill.label} ticket in Hot Sheet`,
|
|
2565
|
-
"alwaysApply: false",
|
|
2566
|
-
"---",
|
|
2567
|
-
versionHeader(),
|
|
2568
|
-
"",
|
|
2569
|
-
ticketSkillBody(skill),
|
|
2570
|
-
""
|
|
2571
|
-
].join("\n");
|
|
2572
|
-
if (updateFile(join6(rulesDir, `${skill.name}.mdc`), content)) updated = true;
|
|
2573
|
-
}
|
|
2574
|
-
return updated;
|
|
2575
|
-
}
|
|
2576
|
-
function ensureCopilotPrompts(cwd) {
|
|
2577
|
-
let updated = false;
|
|
2578
|
-
const promptsDir = join6(cwd, ".github", "prompts");
|
|
2579
|
-
mkdirSync3(promptsDir, { recursive: true });
|
|
2580
|
-
const mainContent = [
|
|
2581
|
-
"---",
|
|
2582
|
-
"description: Read the Hot Sheet worklist and work through the current priority items",
|
|
2583
|
-
"---",
|
|
2584
|
-
versionHeader(),
|
|
2585
|
-
"",
|
|
2586
|
-
mainSkillBody(),
|
|
2587
|
-
""
|
|
2588
|
-
].join("\n");
|
|
2589
|
-
if (updateFile(join6(promptsDir, "hotsheet.prompt.md"), mainContent)) updated = true;
|
|
2590
|
-
for (const skill of buildTicketSkills()) {
|
|
2591
|
-
const content = [
|
|
2592
|
-
"---",
|
|
2593
|
-
`description: Create a new ${skill.label} ticket in Hot Sheet`,
|
|
2594
|
-
"---",
|
|
2595
|
-
versionHeader(),
|
|
2596
|
-
"",
|
|
2597
|
-
ticketSkillBody(skill),
|
|
2598
|
-
""
|
|
2599
|
-
].join("\n");
|
|
2600
|
-
if (updateFile(join6(promptsDir, `${skill.name}.prompt.md`), content)) updated = true;
|
|
2601
|
-
}
|
|
2602
|
-
return updated;
|
|
2603
|
-
}
|
|
2604
|
-
function ensureWindsurfRules(cwd) {
|
|
2605
|
-
let updated = false;
|
|
2606
|
-
const rulesDir = join6(cwd, ".windsurf", "rules");
|
|
2607
|
-
mkdirSync3(rulesDir, { recursive: true });
|
|
2608
|
-
const mainContent = [
|
|
2609
|
-
"---",
|
|
2610
|
-
"trigger: manual",
|
|
2611
|
-
"description: Read the Hot Sheet worklist and work through the current priority items",
|
|
2612
|
-
"---",
|
|
2613
|
-
versionHeader(),
|
|
2614
|
-
"",
|
|
2615
|
-
mainSkillBody(),
|
|
2616
|
-
""
|
|
2617
|
-
].join("\n");
|
|
2618
|
-
if (updateFile(join6(rulesDir, "hotsheet.md"), mainContent)) updated = true;
|
|
2619
|
-
for (const skill of buildTicketSkills()) {
|
|
2620
|
-
const content = [
|
|
2621
|
-
"---",
|
|
2622
|
-
"trigger: manual",
|
|
2623
|
-
`description: Create a new ${skill.label} ticket in Hot Sheet`,
|
|
2624
|
-
"---",
|
|
2625
|
-
versionHeader(),
|
|
2626
|
-
"",
|
|
2627
|
-
ticketSkillBody(skill),
|
|
2628
|
-
""
|
|
2629
|
-
].join("\n");
|
|
2630
|
-
if (updateFile(join6(rulesDir, `${skill.name}.md`), content)) updated = true;
|
|
2631
|
-
}
|
|
2632
|
-
return updated;
|
|
2633
|
-
}
|
|
2634
|
-
var pendingCreatedFlag = false;
|
|
2635
|
-
function ensureSkills() {
|
|
2636
|
-
const cwd = process.cwd();
|
|
2637
|
-
const platforms = [];
|
|
2638
|
-
if (existsSync5(join6(cwd, ".claude"))) {
|
|
2639
|
-
if (ensureClaudeSkills(cwd)) platforms.push("Claude Code");
|
|
2640
|
-
}
|
|
2641
|
-
if (existsSync5(join6(cwd, ".cursor"))) {
|
|
2642
|
-
if (ensureCursorRules(cwd)) platforms.push("Cursor");
|
|
2643
|
-
}
|
|
2644
|
-
if (existsSync5(join6(cwd, ".github", "prompts")) || existsSync5(join6(cwd, ".github", "copilot-instructions.md"))) {
|
|
2645
|
-
if (ensureCopilotPrompts(cwd)) platforms.push("GitHub Copilot");
|
|
2646
|
-
}
|
|
2647
|
-
if (existsSync5(join6(cwd, ".windsurf"))) {
|
|
2648
|
-
if (ensureWindsurfRules(cwd)) platforms.push("Windsurf");
|
|
2649
|
-
}
|
|
2650
|
-
if (platforms.length > 0) {
|
|
2651
|
-
pendingCreatedFlag = true;
|
|
2652
|
-
}
|
|
2653
|
-
return platforms;
|
|
2654
|
-
}
|
|
2655
|
-
function consumeSkillsCreatedFlag() {
|
|
2656
|
-
const result = pendingCreatedFlag;
|
|
2657
|
-
pendingCreatedFlag = false;
|
|
2658
|
-
return result;
|
|
2659
|
-
}
|
|
2660
|
-
|
|
2661
|
-
// src/sync/markdown.ts
|
|
2662
|
-
import { writeFileSync as writeFileSync5 } from "fs";
|
|
2663
|
-
import { join as join7 } from "path";
|
|
2664
|
-
init_file_settings();
|
|
2665
|
-
var dataDir;
|
|
2666
|
-
var port;
|
|
2667
|
-
var worklistTimeout = null;
|
|
2668
|
-
var openTicketsTimeout = null;
|
|
2669
|
-
var WORKLIST_DEBOUNCE = 500;
|
|
2670
|
-
var OPEN_TICKETS_DEBOUNCE = 5e3;
|
|
2671
|
-
function initMarkdownSync(dir, serverPort) {
|
|
2672
|
-
dataDir = dir;
|
|
2673
|
-
port = serverPort;
|
|
2420
|
+
var dataDir;
|
|
2421
|
+
var port;
|
|
2422
|
+
var worklistTimeout = null;
|
|
2423
|
+
var openTicketsTimeout = null;
|
|
2424
|
+
var WORKLIST_DEBOUNCE = 500;
|
|
2425
|
+
var OPEN_TICKETS_DEBOUNCE = 5e3;
|
|
2426
|
+
function initMarkdownSync(dir, serverPort) {
|
|
2427
|
+
dataDir = dir;
|
|
2428
|
+
port = serverPort;
|
|
2674
2429
|
}
|
|
2675
2430
|
function scheduleWorklistSync() {
|
|
2676
2431
|
if (worklistTimeout) clearTimeout(worklistTimeout);
|
|
@@ -2811,7 +2566,23 @@ async function syncWorklist() {
|
|
|
2811
2566
|
sections.push("Set `up_next: true` only for items that should be prioritized immediately.");
|
|
2812
2567
|
sections.push("");
|
|
2813
2568
|
if (tickets.length === 0) {
|
|
2814
|
-
|
|
2569
|
+
const dbSettings = await getSettings();
|
|
2570
|
+
const autoOrder = dbSettings.auto_order !== "false";
|
|
2571
|
+
if (autoOrder) {
|
|
2572
|
+
sections.push("## Auto-Prioritize");
|
|
2573
|
+
sections.push("");
|
|
2574
|
+
sections.push("No items are in the Up Next list, but **auto-prioritize is enabled**. Before doing anything else:");
|
|
2575
|
+
sections.push("");
|
|
2576
|
+
sections.push("1. Read `.hotsheet/open-tickets.md` to see all open tickets.");
|
|
2577
|
+
sections.push("2. Evaluate them by priority, urgency, and dependencies.");
|
|
2578
|
+
sections.push("3. Choose the most important ticket(s) to work on next.");
|
|
2579
|
+
sections.push(`4. Mark them as Up Next: \`curl -s -X PATCH http://localhost:${port}/api/tickets/{id} -H "Content-Type: application/json"${secretHeader} -d '{"up_next": true}'\``);
|
|
2580
|
+
sections.push('5. Then work through them as normal (set status to "started", implement, set to "completed" with notes).');
|
|
2581
|
+
sections.push("");
|
|
2582
|
+
sections.push("If there are no open tickets at all, there is nothing to do.");
|
|
2583
|
+
} else {
|
|
2584
|
+
sections.push("No items in the Up Next list.");
|
|
2585
|
+
}
|
|
2815
2586
|
} else {
|
|
2816
2587
|
const autoContext = await loadAutoContext();
|
|
2817
2588
|
for (const ticket of tickets) {
|
|
@@ -2827,7 +2598,7 @@ async function syncWorklist() {
|
|
|
2827
2598
|
sections.push(await formatCategoryDescriptions(categories));
|
|
2828
2599
|
}
|
|
2829
2600
|
sections.push("");
|
|
2830
|
-
|
|
2601
|
+
writeFileSync4(join6(dataDir, "worklist.md"), sections.join("\n"), "utf-8");
|
|
2831
2602
|
} catch (err) {
|
|
2832
2603
|
console.error("Failed to sync worklist.md:", err);
|
|
2833
2604
|
}
|
|
@@ -2872,14 +2643,13 @@ async function syncOpenTickets() {
|
|
|
2872
2643
|
sections.push(await formatCategoryDescriptions(categories));
|
|
2873
2644
|
}
|
|
2874
2645
|
sections.push("");
|
|
2875
|
-
|
|
2646
|
+
writeFileSync4(join6(dataDir, "open-tickets.md"), sections.join("\n"), "utf-8");
|
|
2876
2647
|
} catch (err) {
|
|
2877
2648
|
console.error("Failed to sync open-tickets.md:", err);
|
|
2878
2649
|
}
|
|
2879
2650
|
}
|
|
2880
2651
|
|
|
2881
|
-
// src/routes/
|
|
2882
|
-
var apiRoutes = new Hono();
|
|
2652
|
+
// src/routes/notify.ts
|
|
2883
2653
|
var changeVersion = 0;
|
|
2884
2654
|
var pollWaiters = [];
|
|
2885
2655
|
function notifyChange() {
|
|
@@ -2890,216 +2660,32 @@ function notifyChange() {
|
|
|
2890
2660
|
resolve5(changeVersion);
|
|
2891
2661
|
}
|
|
2892
2662
|
}
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
if (category !== void 0 && category !== "") filters.category = category;
|
|
2912
|
-
const priority = c.req.query("priority");
|
|
2913
|
-
if (priority !== void 0 && priority !== "") filters.priority = priority;
|
|
2914
|
-
const status = c.req.query("status");
|
|
2915
|
-
if (status !== void 0 && status !== "") filters.status = status;
|
|
2916
|
-
const upNext = c.req.query("up_next");
|
|
2917
|
-
if (upNext !== void 0) filters.up_next = upNext === "true";
|
|
2918
|
-
const search = c.req.query("search");
|
|
2919
|
-
if (search !== void 0 && search !== "") filters.search = search;
|
|
2920
|
-
const sortBy = c.req.query("sort_by");
|
|
2921
|
-
if (sortBy !== void 0 && sortBy !== "") filters.sort_by = sortBy;
|
|
2922
|
-
const sortDir = c.req.query("sort_dir");
|
|
2923
|
-
if (sortDir !== void 0 && sortDir !== "") filters.sort_dir = sortDir;
|
|
2924
|
-
const tickets = await getTickets(filters);
|
|
2925
|
-
return c.json(tickets);
|
|
2926
|
-
});
|
|
2927
|
-
apiRoutes.post("/tickets", async (c) => {
|
|
2928
|
-
const body = await c.req.json();
|
|
2929
|
-
let title = body.title || "";
|
|
2930
|
-
const defaults = body.defaults || {};
|
|
2931
|
-
const { title: cleanTitle, tags: bracketTags } = extractBracketTags(title);
|
|
2932
|
-
if (bracketTags.length > 0) {
|
|
2933
|
-
title = cleanTitle || title;
|
|
2934
|
-
let existingTags = [];
|
|
2935
|
-
if (defaults.tags) {
|
|
2936
|
-
try {
|
|
2937
|
-
existingTags = JSON.parse(defaults.tags);
|
|
2938
|
-
} catch {
|
|
2939
|
-
}
|
|
2940
|
-
}
|
|
2941
|
-
for (const tag of bracketTags) {
|
|
2942
|
-
if (!existingTags.some((t) => t.toLowerCase() === tag.toLowerCase())) existingTags.push(tag);
|
|
2943
|
-
}
|
|
2944
|
-
defaults.tags = JSON.stringify(existingTags);
|
|
2945
|
-
}
|
|
2946
|
-
const ticket = await createTicket(title, defaults);
|
|
2947
|
-
scheduleAllSync();
|
|
2948
|
-
notifyChange();
|
|
2949
|
-
return c.json(ticket, 201);
|
|
2950
|
-
});
|
|
2951
|
-
apiRoutes.get("/tickets/:id", async (c) => {
|
|
2952
|
-
const id = parseInt(c.req.param("id"), 10);
|
|
2953
|
-
const ticket = await getTicket(id);
|
|
2954
|
-
if (!ticket) return c.json({ error: "Not found" }, 404);
|
|
2955
|
-
const attachments = await getAttachments(id);
|
|
2956
|
-
const notes = parseNotes(ticket.notes);
|
|
2957
|
-
return c.json({ ...ticket, notes: JSON.stringify(notes), attachments });
|
|
2958
|
-
});
|
|
2959
|
-
apiRoutes.patch("/tickets/:id", async (c) => {
|
|
2960
|
-
const id = parseInt(c.req.param("id"), 10);
|
|
2961
|
-
const body = await c.req.json();
|
|
2962
|
-
const ticket = await updateTicket(id, body);
|
|
2963
|
-
if (!ticket) return c.json({ error: "Not found" }, 404);
|
|
2964
|
-
scheduleAllSync();
|
|
2965
|
-
notifyChange();
|
|
2966
|
-
return c.json(ticket);
|
|
2967
|
-
});
|
|
2968
|
-
apiRoutes.delete("/tickets/:id", async (c) => {
|
|
2969
|
-
const id = parseInt(c.req.param("id"), 10);
|
|
2970
|
-
await deleteTicket(id);
|
|
2971
|
-
scheduleAllSync();
|
|
2972
|
-
notifyChange();
|
|
2973
|
-
return c.json({ ok: true });
|
|
2974
|
-
});
|
|
2975
|
-
apiRoutes.put("/tickets/:id/notes-bulk", async (c) => {
|
|
2976
|
-
const id = parseInt(c.req.param("id"), 10);
|
|
2977
|
-
const body = await c.req.json();
|
|
2978
|
-
const db2 = await Promise.resolve().then(() => (init_connection(), connection_exports)).then((m) => m.getDb());
|
|
2979
|
-
const result = await (await db2).query(
|
|
2980
|
-
`UPDATE tickets SET notes = $1, updated_at = NOW() WHERE id = $2 RETURNING id`,
|
|
2981
|
-
[body.notes, id]
|
|
2982
|
-
);
|
|
2983
|
-
if (result.rows.length === 0) return c.json({ error: "Not found" }, 404);
|
|
2984
|
-
scheduleAllSync();
|
|
2985
|
-
notifyChange();
|
|
2986
|
-
return c.json({ ok: true });
|
|
2987
|
-
});
|
|
2988
|
-
apiRoutes.patch("/tickets/:id/notes/:noteId", async (c) => {
|
|
2989
|
-
const id = parseInt(c.req.param("id"), 10);
|
|
2990
|
-
const noteId2 = c.req.param("noteId");
|
|
2991
|
-
const body = await c.req.json();
|
|
2992
|
-
const notes = await editNote(id, noteId2, body.text);
|
|
2993
|
-
if (!notes) return c.json({ error: "Not found" }, 404);
|
|
2994
|
-
scheduleAllSync();
|
|
2995
|
-
notifyChange();
|
|
2996
|
-
return c.json(notes);
|
|
2997
|
-
});
|
|
2998
|
-
apiRoutes.delete("/tickets/:id/notes/:noteId", async (c) => {
|
|
2999
|
-
const id = parseInt(c.req.param("id"), 10);
|
|
3000
|
-
const noteId2 = c.req.param("noteId");
|
|
3001
|
-
const notes = await deleteNote(id, noteId2);
|
|
3002
|
-
if (!notes) return c.json({ error: "Not found" }, 404);
|
|
3003
|
-
scheduleAllSync();
|
|
3004
|
-
notifyChange();
|
|
3005
|
-
return c.json(notes);
|
|
3006
|
-
});
|
|
3007
|
-
apiRoutes.delete("/tickets/:id/hard", async (c) => {
|
|
3008
|
-
const id = parseInt(c.req.param("id"), 10);
|
|
3009
|
-
const attachments = await getAttachments(id);
|
|
3010
|
-
for (const att of attachments) {
|
|
3011
|
-
try {
|
|
3012
|
-
rmSync5(att.stored_path, { force: true });
|
|
3013
|
-
} catch {
|
|
3014
|
-
}
|
|
3015
|
-
}
|
|
3016
|
-
await hardDeleteTicket(id);
|
|
3017
|
-
scheduleAllSync();
|
|
3018
|
-
notifyChange();
|
|
3019
|
-
return c.json({ ok: true });
|
|
3020
|
-
});
|
|
3021
|
-
apiRoutes.post("/tickets/batch", async (c) => {
|
|
3022
|
-
const body = await c.req.json();
|
|
3023
|
-
switch (body.action) {
|
|
3024
|
-
case "delete":
|
|
3025
|
-
await batchDeleteTickets(body.ids);
|
|
3026
|
-
break;
|
|
3027
|
-
case "restore":
|
|
3028
|
-
await batchRestoreTickets(body.ids);
|
|
3029
|
-
break;
|
|
3030
|
-
case "category":
|
|
3031
|
-
await batchUpdateTickets(body.ids, { category: body.value });
|
|
3032
|
-
break;
|
|
3033
|
-
case "priority":
|
|
3034
|
-
await batchUpdateTickets(body.ids, { priority: body.value });
|
|
3035
|
-
break;
|
|
3036
|
-
case "status":
|
|
3037
|
-
await batchUpdateTickets(body.ids, { status: body.value });
|
|
3038
|
-
break;
|
|
3039
|
-
case "up_next":
|
|
3040
|
-
await batchUpdateTickets(body.ids, { up_next: body.value });
|
|
3041
|
-
break;
|
|
3042
|
-
}
|
|
3043
|
-
scheduleAllSync();
|
|
3044
|
-
notifyChange();
|
|
3045
|
-
return c.json({ ok: true });
|
|
3046
|
-
});
|
|
3047
|
-
apiRoutes.post("/tickets/duplicate", async (c) => {
|
|
3048
|
-
const body = await c.req.json();
|
|
3049
|
-
const created = await duplicateTickets(body.ids);
|
|
3050
|
-
scheduleAllSync();
|
|
3051
|
-
notifyChange();
|
|
3052
|
-
return c.json(created, 201);
|
|
3053
|
-
});
|
|
3054
|
-
apiRoutes.post("/tickets/:id/restore", async (c) => {
|
|
3055
|
-
const id = parseInt(c.req.param("id"), 10);
|
|
3056
|
-
const ticket = await restoreTicket(id);
|
|
3057
|
-
if (!ticket) return c.json({ error: "Not found" }, 404);
|
|
3058
|
-
scheduleAllSync();
|
|
3059
|
-
notifyChange();
|
|
3060
|
-
return c.json(ticket);
|
|
3061
|
-
});
|
|
3062
|
-
apiRoutes.post("/trash/empty", async (c) => {
|
|
3063
|
-
const deleted = await getTickets({ status: "deleted" });
|
|
3064
|
-
for (const ticket of deleted) {
|
|
3065
|
-
const attachments = await getAttachments(ticket.id);
|
|
3066
|
-
for (const att of attachments) {
|
|
3067
|
-
try {
|
|
3068
|
-
rmSync5(att.stored_path, { force: true });
|
|
3069
|
-
} catch {
|
|
3070
|
-
}
|
|
3071
|
-
}
|
|
3072
|
-
}
|
|
3073
|
-
await emptyTrash();
|
|
3074
|
-
scheduleAllSync();
|
|
3075
|
-
notifyChange();
|
|
3076
|
-
return c.json({ ok: true });
|
|
3077
|
-
});
|
|
3078
|
-
apiRoutes.post("/tickets/:id/up-next", async (c) => {
|
|
3079
|
-
const id = parseInt(c.req.param("id"), 10);
|
|
3080
|
-
const ticket = await toggleUpNext(id);
|
|
3081
|
-
if (!ticket) return c.json({ error: "Not found" }, 404);
|
|
3082
|
-
scheduleAllSync();
|
|
3083
|
-
notifyChange();
|
|
3084
|
-
return c.json(ticket);
|
|
3085
|
-
});
|
|
3086
|
-
apiRoutes.post("/tickets/:id/attachments", async (c) => {
|
|
3087
|
-
const id = parseInt(c.req.param("id"), 10);
|
|
3088
|
-
const ticket = await getTicket(id);
|
|
3089
|
-
if (!ticket) return c.json({ error: "Ticket not found" }, 404);
|
|
3090
|
-
const dataDir2 = c.get("dataDir");
|
|
3091
|
-
const body = await c.req.parseBody();
|
|
3092
|
-
const file = body["file"];
|
|
3093
|
-
if (typeof file === "string") {
|
|
3094
|
-
return c.json({ error: "No file uploaded" }, 400);
|
|
2663
|
+
function getChangeVersion() {
|
|
2664
|
+
return changeVersion;
|
|
2665
|
+
}
|
|
2666
|
+
function addPollWaiter(resolve5) {
|
|
2667
|
+
pollWaiters.push(resolve5);
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2670
|
+
// src/routes/attachments.ts
|
|
2671
|
+
var attachmentRoutes = new Hono();
|
|
2672
|
+
attachmentRoutes.post("/tickets/:id/attachments", async (c) => {
|
|
2673
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
2674
|
+
const ticket = await getTicket(id);
|
|
2675
|
+
if (!ticket) return c.json({ error: "Ticket not found" }, 404);
|
|
2676
|
+
const dataDir2 = c.get("dataDir");
|
|
2677
|
+
const body = await c.req.parseBody();
|
|
2678
|
+
const file = body["file"];
|
|
2679
|
+
if (typeof file === "string") {
|
|
2680
|
+
return c.json({ error: "No file uploaded" }, 400);
|
|
3095
2681
|
}
|
|
3096
2682
|
const originalName = file.name;
|
|
3097
2683
|
const ext = extname(originalName);
|
|
3098
2684
|
const baseName = basename(originalName, ext);
|
|
3099
2685
|
const storedName = `${ticket.ticket_number}_${baseName}${ext}`;
|
|
3100
|
-
const attachDir =
|
|
3101
|
-
|
|
3102
|
-
const storedPath =
|
|
2686
|
+
const attachDir = join7(dataDir2, "attachments");
|
|
2687
|
+
mkdirSync3(attachDir, { recursive: true });
|
|
2688
|
+
const storedPath = join7(attachDir, storedName);
|
|
3103
2689
|
const buffer = Buffer.from(await file.arrayBuffer());
|
|
3104
2690
|
const { writeFileSync: writeFileSync8 } = await import("fs");
|
|
3105
2691
|
writeFileSync8(storedPath, buffer);
|
|
@@ -3108,7 +2694,7 @@ apiRoutes.post("/tickets/:id/attachments", async (c) => {
|
|
|
3108
2694
|
notifyChange();
|
|
3109
2695
|
return c.json(attachment, 201);
|
|
3110
2696
|
});
|
|
3111
|
-
|
|
2697
|
+
attachmentRoutes.delete("/attachments/:id", async (c) => {
|
|
3112
2698
|
const id = parseInt(c.req.param("id"), 10);
|
|
3113
2699
|
const attachment = await deleteAttachment(id);
|
|
3114
2700
|
if (!attachment) return c.json({ error: "Not found" }, 404);
|
|
@@ -3120,11 +2706,11 @@ apiRoutes.delete("/attachments/:id", async (c) => {
|
|
|
3120
2706
|
notifyChange();
|
|
3121
2707
|
return c.json({ ok: true });
|
|
3122
2708
|
});
|
|
3123
|
-
|
|
2709
|
+
attachmentRoutes.post("/attachments/:id/reveal", async (c) => {
|
|
3124
2710
|
const id = parseInt(c.req.param("id"), 10);
|
|
3125
2711
|
const attachment = await getAttachment(id);
|
|
3126
2712
|
if (!attachment) return c.json({ error: "Not found" }, 404);
|
|
3127
|
-
if (!
|
|
2713
|
+
if (!existsSync5(attachment.stored_path)) return c.json({ error: "File not found on disk" }, 404);
|
|
3128
2714
|
const { execFile } = await import("child_process");
|
|
3129
2715
|
const { dirname: dirname4 } = await import("path");
|
|
3130
2716
|
const platform = process.platform;
|
|
@@ -3137,15 +2723,15 @@ apiRoutes.post("/attachments/:id/reveal", async (c) => {
|
|
|
3137
2723
|
}
|
|
3138
2724
|
return c.json({ ok: true });
|
|
3139
2725
|
});
|
|
3140
|
-
|
|
2726
|
+
attachmentRoutes.get("/attachments/file/*", async (c) => {
|
|
3141
2727
|
const filePath = c.req.path.replace("/api/attachments/file/", "");
|
|
3142
2728
|
const dataDir2 = c.get("dataDir");
|
|
3143
|
-
const attachDir =
|
|
3144
|
-
const fullPath =
|
|
2729
|
+
const attachDir = resolve2(join7(dataDir2, "attachments"));
|
|
2730
|
+
const fullPath = resolve2(join7(attachDir, filePath));
|
|
3145
2731
|
if (!fullPath.startsWith(attachDir + "/") && fullPath !== attachDir) {
|
|
3146
2732
|
return c.json({ error: "Invalid path" }, 403);
|
|
3147
2733
|
}
|
|
3148
|
-
if (!
|
|
2734
|
+
if (!existsSync5(fullPath)) {
|
|
3149
2735
|
return c.json({ error: "File not found" }, 404);
|
|
3150
2736
|
}
|
|
3151
2737
|
const { readFileSync: readFileSync9 } = await import("fs");
|
|
@@ -3167,226 +2753,712 @@ apiRoutes.get("/attachments/file/*", async (c) => {
|
|
|
3167
2753
|
headers: { "Content-Type": contentType }
|
|
3168
2754
|
});
|
|
3169
2755
|
});
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
2756
|
+
|
|
2757
|
+
// src/routes/channel.ts
|
|
2758
|
+
import { Hono as Hono2 } from "hono";
|
|
2759
|
+
var channelRoutes = new Hono2();
|
|
2760
|
+
var channelDoneFlag = false;
|
|
2761
|
+
channelRoutes.get("/channel/claude-check", async (c) => {
|
|
2762
|
+
const { execFileSync } = await import("child_process");
|
|
2763
|
+
try {
|
|
2764
|
+
const version = execFileSync("claude", ["--version"], { timeout: 5e3, encoding: "utf-8" }).trim();
|
|
2765
|
+
const match = version.match(/(\d+\.\d+\.\d+)/);
|
|
2766
|
+
const versionNum = match ? match[1] : null;
|
|
2767
|
+
const parts = versionNum ? versionNum.split(".").map(Number) : [];
|
|
2768
|
+
const meetsMinimum = parts.length === 3 && (parts[0] > 2 || parts[0] === 2 && parts[1] > 1 || parts[0] === 2 && parts[1] === 1 && parts[2] >= 80);
|
|
2769
|
+
return c.json({ installed: true, version: versionNum, meetsMinimum });
|
|
2770
|
+
} catch {
|
|
2771
|
+
return c.json({ installed: false, version: null, meetsMinimum: false });
|
|
2772
|
+
}
|
|
3174
2773
|
});
|
|
3175
|
-
|
|
3176
|
-
const
|
|
3177
|
-
|
|
2774
|
+
channelRoutes.get("/channel/status", async (c) => {
|
|
2775
|
+
const { isChannelAlive: isChannelAlive2, getChannelPort: getChannelPort2 } = await Promise.resolve().then(() => (init_channel_config(), channel_config_exports));
|
|
2776
|
+
const dataDir2 = c.get("dataDir");
|
|
2777
|
+
const settings = await getSettings();
|
|
2778
|
+
const enabled = settings.channel_enabled === "true";
|
|
2779
|
+
const port2 = getChannelPort2(dataDir2);
|
|
2780
|
+
const alive = enabled ? await isChannelAlive2(dataDir2) : false;
|
|
2781
|
+
const done = channelDoneFlag;
|
|
2782
|
+
if (done) channelDoneFlag = false;
|
|
2783
|
+
return c.json({ enabled, alive, port: port2, done });
|
|
3178
2784
|
});
|
|
3179
|
-
|
|
3180
|
-
const
|
|
3181
|
-
|
|
2785
|
+
channelRoutes.post("/channel/trigger", async (c) => {
|
|
2786
|
+
const { triggerChannel: triggerChannel2 } = await Promise.resolve().then(() => (init_channel_config(), channel_config_exports));
|
|
2787
|
+
const dataDir2 = c.get("dataDir");
|
|
2788
|
+
const serverPort = parseInt(new URL(c.req.url).port || "4174", 10);
|
|
2789
|
+
const body = await c.req.json().catch(() => ({ message: void 0 }));
|
|
2790
|
+
channelDoneFlag = false;
|
|
2791
|
+
const ok = await triggerChannel2(dataDir2, serverPort, body.message);
|
|
2792
|
+
return c.json({ ok });
|
|
3182
2793
|
});
|
|
3183
|
-
|
|
3184
|
-
const
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
2794
|
+
channelRoutes.get("/channel/permission", async (c) => {
|
|
2795
|
+
const { getChannelPort: getChannelPort2 } = await Promise.resolve().then(() => (init_channel_config(), channel_config_exports));
|
|
2796
|
+
const dataDir2 = c.get("dataDir");
|
|
2797
|
+
const port2 = getChannelPort2(dataDir2);
|
|
2798
|
+
if (!port2) return c.json({ pending: null });
|
|
2799
|
+
try {
|
|
2800
|
+
const res = await fetch(`http://127.0.0.1:${port2}/permission`);
|
|
2801
|
+
const data = await res.json();
|
|
2802
|
+
return c.json(data);
|
|
2803
|
+
} catch {
|
|
2804
|
+
return c.json({ pending: null });
|
|
2805
|
+
}
|
|
3189
2806
|
});
|
|
3190
|
-
|
|
3191
|
-
|
|
2807
|
+
channelRoutes.post("/channel/permission/respond", async (c) => {
|
|
2808
|
+
const { getChannelPort: getChannelPort2 } = await Promise.resolve().then(() => (init_channel_config(), channel_config_exports));
|
|
2809
|
+
const dataDir2 = c.get("dataDir");
|
|
2810
|
+
const port2 = getChannelPort2(dataDir2);
|
|
2811
|
+
if (!port2) return c.json({ error: "Channel not available" }, 503);
|
|
2812
|
+
const body = await c.req.json();
|
|
2813
|
+
try {
|
|
2814
|
+
const res = await fetch(`http://127.0.0.1:${port2}/permission/respond`, {
|
|
2815
|
+
method: "POST",
|
|
2816
|
+
headers: { "Content-Type": "application/json" },
|
|
2817
|
+
body: JSON.stringify(body)
|
|
2818
|
+
});
|
|
2819
|
+
return c.json(await res.json());
|
|
2820
|
+
} catch {
|
|
2821
|
+
return c.json({ error: "Failed to reach channel server" }, 503);
|
|
2822
|
+
}
|
|
3192
2823
|
});
|
|
3193
|
-
|
|
3194
|
-
const
|
|
3195
|
-
|
|
2824
|
+
channelRoutes.post("/channel/permission/dismiss", async (c) => {
|
|
2825
|
+
const { getChannelPort: getChannelPort2 } = await Promise.resolve().then(() => (init_channel_config(), channel_config_exports));
|
|
2826
|
+
const dataDir2 = c.get("dataDir");
|
|
2827
|
+
const port2 = getChannelPort2(dataDir2);
|
|
2828
|
+
if (!port2) return c.json({ ok: true });
|
|
2829
|
+
try {
|
|
2830
|
+
await fetch(`http://127.0.0.1:${port2}/permission/dismiss`, { method: "POST" });
|
|
2831
|
+
} catch {
|
|
2832
|
+
}
|
|
2833
|
+
return c.json({ ok: true });
|
|
3196
2834
|
});
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
2835
|
+
channelRoutes.post("/channel/done", async (_c) => {
|
|
2836
|
+
channelDoneFlag = true;
|
|
2837
|
+
notifyChange();
|
|
2838
|
+
return _c.json({ ok: true });
|
|
2839
|
+
});
|
|
2840
|
+
channelRoutes.post("/channel/enable", async (c) => {
|
|
2841
|
+
const { registerChannel: registerChannel2 } = await Promise.resolve().then(() => (init_channel_config(), channel_config_exports));
|
|
2842
|
+
const dataDir2 = c.get("dataDir");
|
|
2843
|
+
await updateSetting("channel_enabled", "true");
|
|
2844
|
+
registerChannel2(dataDir2);
|
|
2845
|
+
notifyChange();
|
|
2846
|
+
return c.json({ ok: true });
|
|
2847
|
+
});
|
|
2848
|
+
channelRoutes.post("/channel/disable", async (c) => {
|
|
2849
|
+
const { unregisterChannel: unregisterChannel2 } = await Promise.resolve().then(() => (init_channel_config(), channel_config_exports));
|
|
2850
|
+
await updateSetting("channel_enabled", "false");
|
|
2851
|
+
unregisterChannel2();
|
|
2852
|
+
notifyChange();
|
|
2853
|
+
return c.json({ ok: true });
|
|
2854
|
+
});
|
|
2855
|
+
|
|
2856
|
+
// src/routes/dashboard.ts
|
|
2857
|
+
import { Hono as Hono3 } from "hono";
|
|
2858
|
+
import { join as join10, relative as relative2 } from "path";
|
|
2859
|
+
|
|
2860
|
+
// src/skills.ts
|
|
2861
|
+
init_file_settings();
|
|
2862
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
2863
|
+
import { join as join9, relative } from "path";
|
|
2864
|
+
var SKILL_VERSION = 5;
|
|
2865
|
+
var skillPort;
|
|
2866
|
+
var skillDataDir;
|
|
2867
|
+
var skillCategories = DEFAULT_CATEGORIES;
|
|
2868
|
+
function initSkills(port2, dataDir2) {
|
|
2869
|
+
skillPort = port2;
|
|
2870
|
+
skillDataDir = dataDir2;
|
|
2871
|
+
}
|
|
2872
|
+
function setSkillCategories(categories) {
|
|
2873
|
+
skillCategories = categories;
|
|
2874
|
+
}
|
|
2875
|
+
function buildTicketSkills() {
|
|
2876
|
+
return skillCategories.map((cat) => ({
|
|
2877
|
+
name: `hs-${cat.id.replace(/_/g, "-")}`,
|
|
2878
|
+
category: cat.id,
|
|
2879
|
+
label: cat.label.toLowerCase(),
|
|
2880
|
+
description: cat.description
|
|
2881
|
+
}));
|
|
2882
|
+
}
|
|
2883
|
+
function versionHeader() {
|
|
2884
|
+
return `<!-- hotsheet-skill-version: ${SKILL_VERSION} -->`;
|
|
2885
|
+
}
|
|
2886
|
+
function parseVersionHeader(content) {
|
|
2887
|
+
const match = content.match(/<!-- hotsheet-skill-version: (\d+)(?: port: \d+)? -->/);
|
|
2888
|
+
if (!match) return null;
|
|
2889
|
+
return parseInt(match[1], 10);
|
|
2890
|
+
}
|
|
2891
|
+
function updateFile(path, content) {
|
|
2892
|
+
if (existsSync7(path)) {
|
|
2893
|
+
const existing = readFileSync6(path, "utf-8");
|
|
2894
|
+
const version = parseVersionHeader(existing);
|
|
2895
|
+
if (version !== null && version >= SKILL_VERSION) {
|
|
2896
|
+
return false;
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
writeFileSync6(path, content, "utf-8");
|
|
2900
|
+
return true;
|
|
2901
|
+
}
|
|
2902
|
+
function ticketSkillBody(skill) {
|
|
2903
|
+
const settings = readFileSettings(skillDataDir);
|
|
2904
|
+
const secret = settings.secret || "";
|
|
2905
|
+
const secretLine = secret ? ` -H "X-Hotsheet-Secret: ${secret}" \\` : "";
|
|
2906
|
+
const lines = [
|
|
2907
|
+
`Create a new Hot Sheet **${skill.label}** ticket. ${skill.description}.`,
|
|
2908
|
+
"",
|
|
2909
|
+
"**Parsing the input:**",
|
|
2910
|
+
'- If the input starts with "next", "up next", or "do next" (case-insensitive), set `up_next` to `true` and use the remaining text as the title',
|
|
2911
|
+
"- Otherwise, use the entire input as the title",
|
|
2912
|
+
"",
|
|
2913
|
+
"**Create the ticket** by running:",
|
|
2914
|
+
"```bash",
|
|
2915
|
+
`curl -s -X POST http://localhost:${skillPort}/api/tickets \\`,
|
|
2916
|
+
' -H "Content-Type: application/json" \\'
|
|
2917
|
+
];
|
|
2918
|
+
if (secretLine) lines.push(secretLine);
|
|
2919
|
+
lines.push(
|
|
2920
|
+
` -d '{"title": "<TITLE>", "defaults": {"category": "${skill.category}", "up_next": <true|false>}}'`,
|
|
2921
|
+
"```",
|
|
2922
|
+
"",
|
|
2923
|
+
`If the request fails (connection refused or 403), re-read \`.hotsheet/settings.json\` for the current \`port\` and \`secret\` values \u2014 you may be connecting to the wrong Hot Sheet instance.`,
|
|
2924
|
+
"",
|
|
2925
|
+
"Report the created ticket number and title to the user."
|
|
2926
|
+
);
|
|
2927
|
+
return lines.join("\n");
|
|
2928
|
+
}
|
|
2929
|
+
function mainSkillBody() {
|
|
2930
|
+
const worklistRel = relative(process.cwd(), join9(skillDataDir, "worklist.md"));
|
|
2931
|
+
const settingsRel = relative(process.cwd(), join9(skillDataDir, "settings.json"));
|
|
2932
|
+
return [
|
|
2933
|
+
`Base directory for this skill: ${join9(process.cwd(), ".claude", "skills", "hotsheet")}`,
|
|
2934
|
+
"",
|
|
2935
|
+
`Read \`${worklistRel}\` and work through the tickets in priority order.`,
|
|
2936
|
+
"",
|
|
2937
|
+
"For each ticket:",
|
|
2938
|
+
"1. Read the ticket details carefully",
|
|
2939
|
+
"2. Implement the work described",
|
|
2940
|
+
"3. When complete, mark it done via the Hot Sheet UI",
|
|
2941
|
+
"",
|
|
2942
|
+
"Work through them in order of priority, where reasonable.",
|
|
2943
|
+
"",
|
|
2944
|
+
'If the worklist says "Auto-Prioritize", follow those instructions to choose and mark tickets as Up Next before working on them.',
|
|
2945
|
+
"",
|
|
2946
|
+
`If API calls fail (connection refused or 403), re-read \`${settingsRel}\` for the current \`port\` and \`secret\` values \u2014 you may be connecting to the wrong Hot Sheet instance.`
|
|
2947
|
+
].join("\n");
|
|
2948
|
+
}
|
|
2949
|
+
var HOTSHEET_ALLOW_PATTERNS = [
|
|
2950
|
+
"Bash(curl * http://localhost:417*/api/*)",
|
|
2951
|
+
"Bash(curl * http://localhost:418*/api/*)"
|
|
2952
|
+
];
|
|
2953
|
+
var HOTSHEET_CURL_RE = /^Bash\(curl \* http:\/\/localhost:\d+\/api\/\*\)$|^Bash\(curl \* http:\/\/localhost:41[78]\*\/api\/\*\)$/;
|
|
2954
|
+
function ensureClaudePermissions(cwd) {
|
|
2955
|
+
if (skillPort < 4170 || skillPort > 4189) return false;
|
|
2956
|
+
const settingsPath2 = join9(cwd, ".claude", "settings.json");
|
|
2957
|
+
let settings = {};
|
|
2958
|
+
if (existsSync7(settingsPath2)) {
|
|
2959
|
+
try {
|
|
2960
|
+
settings = JSON.parse(readFileSync6(settingsPath2, "utf-8"));
|
|
2961
|
+
} catch {
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
if (!settings.permissions) settings.permissions = {};
|
|
2965
|
+
if (!settings.permissions.allow) settings.permissions.allow = [];
|
|
2966
|
+
const allow = settings.permissions.allow;
|
|
2967
|
+
if (HOTSHEET_ALLOW_PATTERNS.every((p) => allow.includes(p))) return false;
|
|
2968
|
+
settings.permissions.allow = allow.filter((p) => !HOTSHEET_CURL_RE.test(p));
|
|
2969
|
+
settings.permissions.allow.push(...HOTSHEET_ALLOW_PATTERNS);
|
|
2970
|
+
writeFileSync6(settingsPath2, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
2971
|
+
return true;
|
|
2972
|
+
}
|
|
2973
|
+
function ensureClaudeSkills(cwd) {
|
|
2974
|
+
let updated = false;
|
|
2975
|
+
const skillsDir = join9(cwd, ".claude", "skills");
|
|
2976
|
+
if (ensureClaudePermissions(cwd)) updated = true;
|
|
2977
|
+
const mainDir = join9(skillsDir, "hotsheet");
|
|
2978
|
+
mkdirSync4(mainDir, { recursive: true });
|
|
2979
|
+
const mainContent = [
|
|
2980
|
+
"---",
|
|
2981
|
+
"name: hotsheet",
|
|
2982
|
+
"description: Read the Hot Sheet worklist and work through the current priority items",
|
|
2983
|
+
"allowed-tools: Read, Grep, Glob, Edit, Write, Bash",
|
|
2984
|
+
"---",
|
|
2985
|
+
versionHeader(),
|
|
2986
|
+
"",
|
|
2987
|
+
mainSkillBody(),
|
|
2988
|
+
""
|
|
2989
|
+
].join("\n");
|
|
2990
|
+
if (updateFile(join9(mainDir, "SKILL.md"), mainContent)) updated = true;
|
|
2991
|
+
for (const skill of buildTicketSkills()) {
|
|
2992
|
+
const dir = join9(skillsDir, skill.name);
|
|
2993
|
+
mkdirSync4(dir, { recursive: true });
|
|
2994
|
+
const content = [
|
|
2995
|
+
"---",
|
|
2996
|
+
`name: ${skill.name}`,
|
|
2997
|
+
`description: Create a new ${skill.label} ticket in Hot Sheet`,
|
|
2998
|
+
"allowed-tools: Bash",
|
|
2999
|
+
"---",
|
|
3000
|
+
versionHeader(),
|
|
3001
|
+
"",
|
|
3002
|
+
ticketSkillBody(skill),
|
|
3003
|
+
""
|
|
3004
|
+
].join("\n");
|
|
3005
|
+
if (updateFile(join9(dir, "SKILL.md"), content)) updated = true;
|
|
3006
|
+
}
|
|
3007
|
+
return updated;
|
|
3008
|
+
}
|
|
3009
|
+
function ensureCursorRules(cwd) {
|
|
3010
|
+
let updated = false;
|
|
3011
|
+
const rulesDir = join9(cwd, ".cursor", "rules");
|
|
3012
|
+
mkdirSync4(rulesDir, { recursive: true });
|
|
3013
|
+
const mainContent = [
|
|
3014
|
+
"---",
|
|
3015
|
+
"description: Read the Hot Sheet worklist and work through the current priority items",
|
|
3016
|
+
"alwaysApply: false",
|
|
3017
|
+
"---",
|
|
3018
|
+
versionHeader(),
|
|
3019
|
+
"",
|
|
3020
|
+
mainSkillBody(),
|
|
3021
|
+
""
|
|
3022
|
+
].join("\n");
|
|
3023
|
+
if (updateFile(join9(rulesDir, "hotsheet.mdc"), mainContent)) updated = true;
|
|
3024
|
+
for (const skill of buildTicketSkills()) {
|
|
3025
|
+
const content = [
|
|
3026
|
+
"---",
|
|
3027
|
+
`description: Create a new ${skill.label} ticket in Hot Sheet`,
|
|
3028
|
+
"alwaysApply: false",
|
|
3029
|
+
"---",
|
|
3030
|
+
versionHeader(),
|
|
3031
|
+
"",
|
|
3032
|
+
ticketSkillBody(skill),
|
|
3033
|
+
""
|
|
3034
|
+
].join("\n");
|
|
3035
|
+
if (updateFile(join9(rulesDir, `${skill.name}.mdc`), content)) updated = true;
|
|
3036
|
+
}
|
|
3037
|
+
return updated;
|
|
3038
|
+
}
|
|
3039
|
+
function ensureCopilotPrompts(cwd) {
|
|
3040
|
+
let updated = false;
|
|
3041
|
+
const promptsDir = join9(cwd, ".github", "prompts");
|
|
3042
|
+
mkdirSync4(promptsDir, { recursive: true });
|
|
3043
|
+
const mainContent = [
|
|
3044
|
+
"---",
|
|
3045
|
+
"description: Read the Hot Sheet worklist and work through the current priority items",
|
|
3046
|
+
"---",
|
|
3047
|
+
versionHeader(),
|
|
3048
|
+
"",
|
|
3049
|
+
mainSkillBody(),
|
|
3050
|
+
""
|
|
3051
|
+
].join("\n");
|
|
3052
|
+
if (updateFile(join9(promptsDir, "hotsheet.prompt.md"), mainContent)) updated = true;
|
|
3053
|
+
for (const skill of buildTicketSkills()) {
|
|
3054
|
+
const content = [
|
|
3055
|
+
"---",
|
|
3056
|
+
`description: Create a new ${skill.label} ticket in Hot Sheet`,
|
|
3057
|
+
"---",
|
|
3058
|
+
versionHeader(),
|
|
3059
|
+
"",
|
|
3060
|
+
ticketSkillBody(skill),
|
|
3061
|
+
""
|
|
3062
|
+
].join("\n");
|
|
3063
|
+
if (updateFile(join9(promptsDir, `${skill.name}.prompt.md`), content)) updated = true;
|
|
3064
|
+
}
|
|
3065
|
+
return updated;
|
|
3066
|
+
}
|
|
3067
|
+
function ensureWindsurfRules(cwd) {
|
|
3068
|
+
let updated = false;
|
|
3069
|
+
const rulesDir = join9(cwd, ".windsurf", "rules");
|
|
3070
|
+
mkdirSync4(rulesDir, { recursive: true });
|
|
3071
|
+
const mainContent = [
|
|
3072
|
+
"---",
|
|
3073
|
+
"trigger: manual",
|
|
3074
|
+
"description: Read the Hot Sheet worklist and work through the current priority items",
|
|
3075
|
+
"---",
|
|
3076
|
+
versionHeader(),
|
|
3077
|
+
"",
|
|
3078
|
+
mainSkillBody(),
|
|
3079
|
+
""
|
|
3080
|
+
].join("\n");
|
|
3081
|
+
if (updateFile(join9(rulesDir, "hotsheet.md"), mainContent)) updated = true;
|
|
3082
|
+
for (const skill of buildTicketSkills()) {
|
|
3083
|
+
const content = [
|
|
3084
|
+
"---",
|
|
3085
|
+
"trigger: manual",
|
|
3086
|
+
`description: Create a new ${skill.label} ticket in Hot Sheet`,
|
|
3087
|
+
"---",
|
|
3088
|
+
versionHeader(),
|
|
3089
|
+
"",
|
|
3090
|
+
ticketSkillBody(skill),
|
|
3091
|
+
""
|
|
3092
|
+
].join("\n");
|
|
3093
|
+
if (updateFile(join9(rulesDir, `${skill.name}.md`), content)) updated = true;
|
|
3094
|
+
}
|
|
3095
|
+
return updated;
|
|
3096
|
+
}
|
|
3097
|
+
var pendingCreatedFlag = false;
|
|
3098
|
+
function ensureSkills() {
|
|
3099
|
+
const cwd = process.cwd();
|
|
3100
|
+
const platforms = [];
|
|
3101
|
+
if (existsSync7(join9(cwd, ".claude"))) {
|
|
3102
|
+
if (ensureClaudeSkills(cwd)) platforms.push("Claude Code");
|
|
3103
|
+
}
|
|
3104
|
+
if (existsSync7(join9(cwd, ".cursor"))) {
|
|
3105
|
+
if (ensureCursorRules(cwd)) platforms.push("Cursor");
|
|
3106
|
+
}
|
|
3107
|
+
if (existsSync7(join9(cwd, ".github", "prompts")) || existsSync7(join9(cwd, ".github", "copilot-instructions.md"))) {
|
|
3108
|
+
if (ensureCopilotPrompts(cwd)) platforms.push("GitHub Copilot");
|
|
3109
|
+
}
|
|
3110
|
+
if (existsSync7(join9(cwd, ".windsurf"))) {
|
|
3111
|
+
if (ensureWindsurfRules(cwd)) platforms.push("Windsurf");
|
|
3112
|
+
}
|
|
3113
|
+
if (platforms.length > 0) {
|
|
3114
|
+
pendingCreatedFlag = true;
|
|
3115
|
+
}
|
|
3116
|
+
return platforms;
|
|
3117
|
+
}
|
|
3118
|
+
function consumeSkillsCreatedFlag() {
|
|
3119
|
+
const result = pendingCreatedFlag;
|
|
3120
|
+
pendingCreatedFlag = false;
|
|
3121
|
+
return result;
|
|
3122
|
+
}
|
|
3123
|
+
|
|
3124
|
+
// src/routes/dashboard.ts
|
|
3125
|
+
var dashboardRoutes = new Hono3();
|
|
3126
|
+
dashboardRoutes.get("/poll", async (c) => {
|
|
3127
|
+
const clientVersion = parseInt(c.req.query("version") || "0", 10);
|
|
3128
|
+
const changeVersion2 = getChangeVersion();
|
|
3129
|
+
if (changeVersion2 > clientVersion) {
|
|
3130
|
+
return c.json({ version: changeVersion2 });
|
|
3131
|
+
}
|
|
3132
|
+
const version = await Promise.race([
|
|
3133
|
+
new Promise((resolve5) => {
|
|
3134
|
+
addPollWaiter(resolve5);
|
|
3135
|
+
}),
|
|
3136
|
+
new Promise((resolve5) => {
|
|
3137
|
+
setTimeout(() => resolve5(getChangeVersion()), 3e4);
|
|
3138
|
+
})
|
|
3139
|
+
]);
|
|
3140
|
+
return c.json({ version });
|
|
3141
|
+
});
|
|
3142
|
+
dashboardRoutes.get("/stats", async (c) => {
|
|
3143
|
+
const stats = await getTicketStats();
|
|
3144
|
+
return c.json(stats);
|
|
3145
|
+
});
|
|
3146
|
+
dashboardRoutes.get("/dashboard", async (c) => {
|
|
3147
|
+
const { getDashboardStats: getDashboardStats2, getSnapshots: getSnapshots2 } = await Promise.resolve().then(() => (init_stats(), stats_exports));
|
|
3148
|
+
const days = parseInt(c.req.query("days") || "30", 10);
|
|
3149
|
+
const [stats, snapshots] = await Promise.all([
|
|
3150
|
+
getDashboardStats2(days),
|
|
3151
|
+
getSnapshots2(days)
|
|
3152
|
+
]);
|
|
3153
|
+
return c.json({ ...stats, snapshots });
|
|
3154
|
+
});
|
|
3155
|
+
dashboardRoutes.get("/worklist-info", (c) => {
|
|
3156
|
+
const dataDir2 = c.get("dataDir");
|
|
3157
|
+
const cwd = process.cwd();
|
|
3158
|
+
const worklistRel = relative2(cwd, join10(dataDir2, "worklist.md"));
|
|
3159
|
+
const prompt = `Read ${worklistRel} for current work items.`;
|
|
3160
|
+
ensureSkills();
|
|
3161
|
+
const skillCreated = consumeSkillsCreatedFlag();
|
|
3162
|
+
return c.json({ prompt, skillCreated });
|
|
3163
|
+
});
|
|
3164
|
+
var glassboxAvailable = null;
|
|
3165
|
+
dashboardRoutes.get("/glassbox/status", async (c) => {
|
|
3166
|
+
if (glassboxAvailable === null) {
|
|
3167
|
+
const { execFileSync } = await import("child_process");
|
|
3168
|
+
try {
|
|
3169
|
+
execFileSync("which", ["glassbox"], { stdio: "ignore" });
|
|
3170
|
+
glassboxAvailable = true;
|
|
3171
|
+
} catch {
|
|
3172
|
+
glassboxAvailable = false;
|
|
3173
|
+
}
|
|
3174
|
+
}
|
|
3175
|
+
return c.json({ available: glassboxAvailable });
|
|
3176
|
+
});
|
|
3177
|
+
dashboardRoutes.post("/glassbox/launch", async (c) => {
|
|
3178
|
+
if (!glassboxAvailable) return c.json({ error: "Glassbox not available" }, 404);
|
|
3179
|
+
const { spawn } = await import("child_process");
|
|
3180
|
+
spawn("glassbox", [], {
|
|
3181
|
+
cwd: process.cwd(),
|
|
3182
|
+
detached: true,
|
|
3183
|
+
stdio: "ignore"
|
|
3184
|
+
}).unref();
|
|
3185
|
+
return c.json({ ok: true });
|
|
3186
|
+
});
|
|
3187
|
+
dashboardRoutes.get("/gitignore/status", async (c) => {
|
|
3188
|
+
const { isGitRepo: isGitRepo2, isHotsheetGitignored: isHotsheetGitignored2 } = await Promise.resolve().then(() => (init_gitignore(), gitignore_exports));
|
|
3189
|
+
const cwd = process.cwd();
|
|
3190
|
+
if (!isGitRepo2(cwd)) return c.json({ inGitRepo: false, ignored: false });
|
|
3191
|
+
return c.json({ inGitRepo: true, ignored: isHotsheetGitignored2(cwd) });
|
|
3192
|
+
});
|
|
3193
|
+
dashboardRoutes.post("/gitignore/add", async (c) => {
|
|
3194
|
+
const { ensureGitignore: ensureGitignore2 } = await Promise.resolve().then(() => (init_gitignore(), gitignore_exports));
|
|
3195
|
+
ensureGitignore2(process.cwd());
|
|
3196
|
+
return c.json({ ok: true });
|
|
3197
|
+
});
|
|
3198
|
+
dashboardRoutes.post("/print", async (c) => {
|
|
3199
|
+
const { html } = await c.req.json();
|
|
3200
|
+
const { writeFileSync: writeFileSync8 } = await import("fs");
|
|
3201
|
+
const { tmpdir: tmpdir2 } = await import("os");
|
|
3202
|
+
const { join: pathJoin } = await import("path");
|
|
3203
|
+
const { execFile } = await import("child_process");
|
|
3204
|
+
const tmpPath = pathJoin(tmpdir2(), `hotsheet-print-${Date.now()}.html`);
|
|
3205
|
+
writeFileSync8(tmpPath, html, "utf-8");
|
|
3206
|
+
const platform = process.platform;
|
|
3207
|
+
if (platform === "darwin") {
|
|
3208
|
+
execFile("open", [tmpPath]);
|
|
3209
|
+
} else if (platform === "win32") {
|
|
3210
|
+
execFile("start", ["", tmpPath], { shell: true });
|
|
3211
|
+
} else {
|
|
3212
|
+
execFile("xdg-open", [tmpPath]);
|
|
3213
|
+
}
|
|
3214
|
+
return c.json({ ok: true, path: tmpPath });
|
|
3215
|
+
});
|
|
3216
|
+
|
|
3217
|
+
// src/routes/settings.ts
|
|
3218
|
+
import { Hono as Hono4 } from "hono";
|
|
3219
|
+
var settingsRoutes = new Hono4();
|
|
3220
|
+
settingsRoutes.get("/tags", async (c) => {
|
|
3221
|
+
const tags = await getAllTags();
|
|
3222
|
+
return c.json(tags);
|
|
3223
|
+
});
|
|
3224
|
+
settingsRoutes.get("/categories", async (c) => {
|
|
3225
|
+
const categories = await getCategories();
|
|
3226
|
+
return c.json(categories);
|
|
3227
|
+
});
|
|
3228
|
+
settingsRoutes.put("/categories", async (c) => {
|
|
3229
|
+
const categories = await c.req.json();
|
|
3230
|
+
await saveCategories(categories);
|
|
3231
|
+
scheduleAllSync();
|
|
3232
|
+
notifyChange();
|
|
3233
|
+
return c.json(categories);
|
|
3234
|
+
});
|
|
3235
|
+
settingsRoutes.get("/category-presets", (c) => {
|
|
3236
|
+
return c.json(CATEGORY_PRESETS);
|
|
3237
|
+
});
|
|
3238
|
+
settingsRoutes.get("/settings", async (c) => {
|
|
3239
|
+
const settings = await getSettings();
|
|
3240
|
+
return c.json(settings);
|
|
3241
|
+
});
|
|
3242
|
+
settingsRoutes.patch("/settings", async (c) => {
|
|
3243
|
+
const body = await c.req.json();
|
|
3244
|
+
for (const [key, value] of Object.entries(body)) {
|
|
3245
|
+
await updateSetting(key, value);
|
|
3246
|
+
}
|
|
3247
|
+
return c.json({ ok: true });
|
|
3248
|
+
});
|
|
3249
|
+
settingsRoutes.get("/file-settings", async (c) => {
|
|
3250
|
+
const { readFileSettings: readFileSettings2 } = await Promise.resolve().then(() => (init_file_settings(), file_settings_exports));
|
|
3251
|
+
const dataDir2 = c.get("dataDir");
|
|
3252
|
+
const { secret, secretPathHash, port: port2, ...safe } = readFileSettings2(dataDir2);
|
|
3253
|
+
return c.json(safe);
|
|
3254
|
+
});
|
|
3255
|
+
settingsRoutes.patch("/file-settings", async (c) => {
|
|
3256
|
+
const { writeFileSettings: writeFileSettings2 } = await Promise.resolve().then(() => (init_file_settings(), file_settings_exports));
|
|
3257
|
+
const dataDir2 = c.get("dataDir");
|
|
3258
|
+
const body = await c.req.json();
|
|
3259
|
+
const updated = writeFileSettings2(dataDir2, body);
|
|
3260
|
+
return c.json(updated);
|
|
3261
|
+
});
|
|
3262
|
+
|
|
3263
|
+
// src/routes/tickets.ts
|
|
3264
|
+
import { rmSync as rmSync6 } from "fs";
|
|
3265
|
+
import { Hono as Hono5 } from "hono";
|
|
3266
|
+
var ticketRoutes = new Hono5();
|
|
3267
|
+
ticketRoutes.get("/tickets", async (c) => {
|
|
3268
|
+
const filters = {};
|
|
3269
|
+
const category = c.req.query("category");
|
|
3270
|
+
if (category !== void 0 && category !== "") filters.category = category;
|
|
3271
|
+
const priority = c.req.query("priority");
|
|
3272
|
+
if (priority !== void 0 && priority !== "") filters.priority = priority;
|
|
3273
|
+
const status = c.req.query("status");
|
|
3274
|
+
if (status !== void 0 && status !== "") filters.status = status;
|
|
3275
|
+
const upNext = c.req.query("up_next");
|
|
3276
|
+
if (upNext !== void 0) filters.up_next = upNext === "true";
|
|
3277
|
+
const search = c.req.query("search");
|
|
3278
|
+
if (search !== void 0 && search !== "") filters.search = search;
|
|
3279
|
+
const sortBy = c.req.query("sort_by");
|
|
3280
|
+
if (sortBy !== void 0 && sortBy !== "") filters.sort_by = sortBy;
|
|
3281
|
+
const sortDir = c.req.query("sort_dir");
|
|
3282
|
+
if (sortDir !== void 0 && sortDir !== "") filters.sort_dir = sortDir;
|
|
3283
|
+
const tickets = await getTickets(filters);
|
|
3284
|
+
return c.json(tickets);
|
|
3285
|
+
});
|
|
3286
|
+
ticketRoutes.post("/tickets", async (c) => {
|
|
3287
|
+
const body = await c.req.json();
|
|
3288
|
+
let title = body.title || "";
|
|
3289
|
+
const defaults = body.defaults || {};
|
|
3290
|
+
const { title: cleanTitle, tags: bracketTags } = extractBracketTags(title);
|
|
3291
|
+
if (bracketTags.length > 0) {
|
|
3292
|
+
title = cleanTitle || title;
|
|
3293
|
+
let existingTags = [];
|
|
3294
|
+
if (defaults.tags) {
|
|
3295
|
+
try {
|
|
3296
|
+
existingTags = JSON.parse(defaults.tags);
|
|
3297
|
+
} catch {
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
3300
|
+
for (const tag of bracketTags) {
|
|
3301
|
+
if (!existingTags.some((t) => t.toLowerCase() === tag.toLowerCase())) existingTags.push(tag);
|
|
3302
|
+
}
|
|
3303
|
+
defaults.tags = JSON.stringify(existingTags);
|
|
3304
|
+
}
|
|
3305
|
+
const ticket = await createTicket(title, defaults);
|
|
3306
|
+
scheduleAllSync();
|
|
3307
|
+
notifyChange();
|
|
3308
|
+
return c.json(ticket, 201);
|
|
3309
|
+
});
|
|
3310
|
+
ticketRoutes.get("/tickets/:id", async (c) => {
|
|
3311
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
3312
|
+
const ticket = await getTicket(id);
|
|
3313
|
+
if (!ticket) return c.json({ error: "Not found" }, 404);
|
|
3314
|
+
const attachments = await getAttachments(id);
|
|
3315
|
+
const notes = parseNotes(ticket.notes);
|
|
3316
|
+
return c.json({ ...ticket, notes: JSON.stringify(notes), attachments });
|
|
3317
|
+
});
|
|
3318
|
+
ticketRoutes.patch("/tickets/:id", async (c) => {
|
|
3319
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
3320
|
+
const body = await c.req.json();
|
|
3321
|
+
const ticket = await updateTicket(id, body);
|
|
3322
|
+
if (!ticket) return c.json({ error: "Not found" }, 404);
|
|
3323
|
+
scheduleAllSync();
|
|
3324
|
+
notifyChange();
|
|
3325
|
+
return c.json(ticket);
|
|
3205
3326
|
});
|
|
3206
|
-
|
|
3207
|
-
const
|
|
3208
|
-
|
|
3327
|
+
ticketRoutes.delete("/tickets/:id", async (c) => {
|
|
3328
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
3329
|
+
await deleteTicket(id);
|
|
3330
|
+
scheduleAllSync();
|
|
3331
|
+
notifyChange();
|
|
3332
|
+
return c.json({ ok: true });
|
|
3209
3333
|
});
|
|
3210
|
-
|
|
3334
|
+
ticketRoutes.put("/tickets/:id/notes-bulk", async (c) => {
|
|
3335
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
3211
3336
|
const body = await c.req.json();
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3337
|
+
const db2 = await Promise.resolve().then(() => (init_connection(), connection_exports)).then((m) => m.getDb());
|
|
3338
|
+
const result = await (await db2).query(
|
|
3339
|
+
`UPDATE tickets SET notes = $1, updated_at = NOW() WHERE id = $2 RETURNING id`,
|
|
3340
|
+
[body.notes, id]
|
|
3341
|
+
);
|
|
3342
|
+
if (result.rows.length === 0) return c.json({ error: "Not found" }, 404);
|
|
3343
|
+
scheduleAllSync();
|
|
3344
|
+
notifyChange();
|
|
3215
3345
|
return c.json({ ok: true });
|
|
3216
3346
|
});
|
|
3217
|
-
|
|
3218
|
-
const
|
|
3219
|
-
const
|
|
3220
|
-
const { secret, secretPathHash, port: port2, ...safe } = readFileSettings2(dataDir2);
|
|
3221
|
-
return c.json(safe);
|
|
3222
|
-
});
|
|
3223
|
-
apiRoutes.patch("/file-settings", async (c) => {
|
|
3224
|
-
const { writeFileSettings: writeFileSettings2 } = await Promise.resolve().then(() => (init_file_settings(), file_settings_exports));
|
|
3225
|
-
const dataDir2 = c.get("dataDir");
|
|
3347
|
+
ticketRoutes.patch("/tickets/:id/notes/:noteId", async (c) => {
|
|
3348
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
3349
|
+
const noteId2 = c.req.param("noteId");
|
|
3226
3350
|
const body = await c.req.json();
|
|
3227
|
-
const
|
|
3228
|
-
return c.json(
|
|
3351
|
+
const notes = await editNote(id, noteId2, body.text);
|
|
3352
|
+
if (!notes) return c.json({ error: "Not found" }, 404);
|
|
3353
|
+
scheduleAllSync();
|
|
3354
|
+
notifyChange();
|
|
3355
|
+
return c.json(notes);
|
|
3229
3356
|
});
|
|
3230
|
-
|
|
3231
|
-
const
|
|
3232
|
-
const
|
|
3233
|
-
const
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
return c.json(
|
|
3357
|
+
ticketRoutes.delete("/tickets/:id/notes/:noteId", async (c) => {
|
|
3358
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
3359
|
+
const noteId2 = c.req.param("noteId");
|
|
3360
|
+
const notes = await deleteNote(id, noteId2);
|
|
3361
|
+
if (!notes) return c.json({ error: "Not found" }, 404);
|
|
3362
|
+
scheduleAllSync();
|
|
3363
|
+
notifyChange();
|
|
3364
|
+
return c.json(notes);
|
|
3238
3365
|
});
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3366
|
+
ticketRoutes.delete("/tickets/:id/hard", async (c) => {
|
|
3367
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
3368
|
+
const attachments = await getAttachments(id);
|
|
3369
|
+
for (const att of attachments) {
|
|
3243
3370
|
try {
|
|
3244
|
-
|
|
3245
|
-
glassboxAvailable = true;
|
|
3371
|
+
rmSync6(att.stored_path, { force: true });
|
|
3246
3372
|
} catch {
|
|
3247
|
-
glassboxAvailable = false;
|
|
3248
3373
|
}
|
|
3249
3374
|
}
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
if (!glassboxAvailable) return c.json({ error: "Glassbox not available" }, 404);
|
|
3254
|
-
const { spawn } = await import("child_process");
|
|
3255
|
-
spawn("glassbox", [], {
|
|
3256
|
-
cwd: process.cwd(),
|
|
3257
|
-
detached: true,
|
|
3258
|
-
stdio: "ignore"
|
|
3259
|
-
}).unref();
|
|
3260
|
-
return c.json({ ok: true });
|
|
3261
|
-
});
|
|
3262
|
-
apiRoutes.get("/gitignore/status", async (c) => {
|
|
3263
|
-
const { isGitRepo: isGitRepo2, isHotsheetGitignored: isHotsheetGitignored2 } = await Promise.resolve().then(() => (init_gitignore(), gitignore_exports));
|
|
3264
|
-
const cwd = process.cwd();
|
|
3265
|
-
if (!isGitRepo2(cwd)) return c.json({ inGitRepo: false, ignored: false });
|
|
3266
|
-
return c.json({ inGitRepo: true, ignored: isHotsheetGitignored2(cwd) });
|
|
3267
|
-
});
|
|
3268
|
-
apiRoutes.post("/gitignore/add", async (c) => {
|
|
3269
|
-
const { ensureGitignore: ensureGitignore2 } = await Promise.resolve().then(() => (init_gitignore(), gitignore_exports));
|
|
3270
|
-
ensureGitignore2(process.cwd());
|
|
3375
|
+
await hardDeleteTicket(id);
|
|
3376
|
+
scheduleAllSync();
|
|
3377
|
+
notifyChange();
|
|
3271
3378
|
return c.json({ ok: true });
|
|
3272
3379
|
});
|
|
3273
|
-
|
|
3274
|
-
const { execFileSync } = await import("child_process");
|
|
3275
|
-
try {
|
|
3276
|
-
const version = execFileSync("claude", ["--version"], { timeout: 5e3, encoding: "utf-8" }).trim();
|
|
3277
|
-
const match = version.match(/(\d+\.\d+\.\d+)/);
|
|
3278
|
-
const versionNum = match ? match[1] : null;
|
|
3279
|
-
const parts = versionNum ? versionNum.split(".").map(Number) : [];
|
|
3280
|
-
const meetsMinimum = parts.length === 3 && (parts[0] > 2 || parts[0] === 2 && parts[1] > 1 || parts[0] === 2 && parts[1] === 1 && parts[2] >= 80);
|
|
3281
|
-
return c.json({ installed: true, version: versionNum, meetsMinimum });
|
|
3282
|
-
} catch {
|
|
3283
|
-
return c.json({ installed: false, version: null, meetsMinimum: false });
|
|
3284
|
-
}
|
|
3285
|
-
});
|
|
3286
|
-
var channelDoneFlag = false;
|
|
3287
|
-
apiRoutes.get("/channel/status", async (c) => {
|
|
3288
|
-
const { isChannelAlive: isChannelAlive2, getChannelPort: getChannelPort2 } = await Promise.resolve().then(() => (init_channel_config(), channel_config_exports));
|
|
3289
|
-
const dataDir2 = c.get("dataDir");
|
|
3290
|
-
const settings = await getSettings();
|
|
3291
|
-
const enabled = settings.channel_enabled === "true";
|
|
3292
|
-
const port2 = getChannelPort2(dataDir2);
|
|
3293
|
-
const alive = enabled ? await isChannelAlive2(dataDir2) : false;
|
|
3294
|
-
const done = channelDoneFlag;
|
|
3295
|
-
if (done) channelDoneFlag = false;
|
|
3296
|
-
return c.json({ enabled, alive, port: port2, done });
|
|
3297
|
-
});
|
|
3298
|
-
apiRoutes.post("/channel/trigger", async (c) => {
|
|
3299
|
-
const { triggerChannel: triggerChannel2 } = await Promise.resolve().then(() => (init_channel_config(), channel_config_exports));
|
|
3300
|
-
const dataDir2 = c.get("dataDir");
|
|
3301
|
-
const serverPort = parseInt(new URL(c.req.url).port || "4174", 10);
|
|
3302
|
-
const body = await c.req.json().catch(() => ({ message: void 0 }));
|
|
3303
|
-
channelDoneFlag = false;
|
|
3304
|
-
const ok = await triggerChannel2(dataDir2, serverPort, body.message);
|
|
3305
|
-
return c.json({ ok });
|
|
3306
|
-
});
|
|
3307
|
-
apiRoutes.get("/channel/permission", async (c) => {
|
|
3308
|
-
const { getChannelPort: getChannelPort2 } = await Promise.resolve().then(() => (init_channel_config(), channel_config_exports));
|
|
3309
|
-
const dataDir2 = c.get("dataDir");
|
|
3310
|
-
const port2 = getChannelPort2(dataDir2);
|
|
3311
|
-
if (!port2) return c.json({ pending: null });
|
|
3312
|
-
try {
|
|
3313
|
-
const res = await fetch(`http://127.0.0.1:${port2}/permission`);
|
|
3314
|
-
const data = await res.json();
|
|
3315
|
-
return c.json(data);
|
|
3316
|
-
} catch {
|
|
3317
|
-
return c.json({ pending: null });
|
|
3318
|
-
}
|
|
3319
|
-
});
|
|
3320
|
-
apiRoutes.post("/channel/permission/respond", async (c) => {
|
|
3321
|
-
const { getChannelPort: getChannelPort2 } = await Promise.resolve().then(() => (init_channel_config(), channel_config_exports));
|
|
3322
|
-
const dataDir2 = c.get("dataDir");
|
|
3323
|
-
const port2 = getChannelPort2(dataDir2);
|
|
3324
|
-
if (!port2) return c.json({ error: "Channel not available" }, 503);
|
|
3380
|
+
ticketRoutes.post("/tickets/batch", async (c) => {
|
|
3325
3381
|
const body = await c.req.json();
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3382
|
+
switch (body.action) {
|
|
3383
|
+
case "delete":
|
|
3384
|
+
await batchDeleteTickets(body.ids);
|
|
3385
|
+
break;
|
|
3386
|
+
case "restore":
|
|
3387
|
+
await batchRestoreTickets(body.ids);
|
|
3388
|
+
break;
|
|
3389
|
+
case "category":
|
|
3390
|
+
await batchUpdateTickets(body.ids, { category: body.value });
|
|
3391
|
+
break;
|
|
3392
|
+
case "priority":
|
|
3393
|
+
await batchUpdateTickets(body.ids, { priority: body.value });
|
|
3394
|
+
break;
|
|
3395
|
+
case "status":
|
|
3396
|
+
await batchUpdateTickets(body.ids, { status: body.value });
|
|
3397
|
+
break;
|
|
3398
|
+
case "up_next":
|
|
3399
|
+
await batchUpdateTickets(body.ids, { up_next: body.value });
|
|
3400
|
+
break;
|
|
3345
3401
|
}
|
|
3402
|
+
scheduleAllSync();
|
|
3403
|
+
notifyChange();
|
|
3346
3404
|
return c.json({ ok: true });
|
|
3347
3405
|
});
|
|
3348
|
-
|
|
3349
|
-
|
|
3406
|
+
ticketRoutes.post("/tickets/duplicate", async (c) => {
|
|
3407
|
+
const body = await c.req.json();
|
|
3408
|
+
const created = await duplicateTickets(body.ids);
|
|
3409
|
+
scheduleAllSync();
|
|
3350
3410
|
notifyChange();
|
|
3351
|
-
return
|
|
3411
|
+
return c.json(created, 201);
|
|
3352
3412
|
});
|
|
3353
|
-
|
|
3354
|
-
const
|
|
3355
|
-
const
|
|
3356
|
-
|
|
3357
|
-
|
|
3413
|
+
ticketRoutes.post("/tickets/:id/restore", async (c) => {
|
|
3414
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
3415
|
+
const ticket = await restoreTicket(id);
|
|
3416
|
+
if (!ticket) return c.json({ error: "Not found" }, 404);
|
|
3417
|
+
scheduleAllSync();
|
|
3358
3418
|
notifyChange();
|
|
3359
|
-
return c.json(
|
|
3419
|
+
return c.json(ticket);
|
|
3360
3420
|
});
|
|
3361
|
-
|
|
3362
|
-
const
|
|
3363
|
-
|
|
3364
|
-
|
|
3421
|
+
ticketRoutes.post("/trash/empty", async (c) => {
|
|
3422
|
+
const deleted = await getTickets({ status: "deleted" });
|
|
3423
|
+
for (const ticket of deleted) {
|
|
3424
|
+
const attachments = await getAttachments(ticket.id);
|
|
3425
|
+
for (const att of attachments) {
|
|
3426
|
+
try {
|
|
3427
|
+
rmSync6(att.stored_path, { force: true });
|
|
3428
|
+
} catch {
|
|
3429
|
+
}
|
|
3430
|
+
}
|
|
3431
|
+
}
|
|
3432
|
+
await emptyTrash();
|
|
3433
|
+
scheduleAllSync();
|
|
3365
3434
|
notifyChange();
|
|
3366
3435
|
return c.json({ ok: true });
|
|
3367
3436
|
});
|
|
3368
|
-
|
|
3369
|
-
const
|
|
3370
|
-
const
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
execFile("start", ["", tmpPath], { shell: true });
|
|
3381
|
-
} else {
|
|
3382
|
-
execFile("xdg-open", [tmpPath]);
|
|
3383
|
-
}
|
|
3384
|
-
return c.json({ ok: true, path: tmpPath });
|
|
3437
|
+
ticketRoutes.post("/tickets/:id/up-next", async (c) => {
|
|
3438
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
3439
|
+
const ticket = await toggleUpNext(id);
|
|
3440
|
+
if (!ticket) return c.json({ error: "Not found" }, 404);
|
|
3441
|
+
scheduleAllSync();
|
|
3442
|
+
notifyChange();
|
|
3443
|
+
return c.json(ticket);
|
|
3444
|
+
});
|
|
3445
|
+
ticketRoutes.post("/tickets/query", async (c) => {
|
|
3446
|
+
const body = await c.req.json();
|
|
3447
|
+
const tickets = await queryTickets(body.logic, body.conditions, body.sort_by, body.sort_dir, body.required_tag);
|
|
3448
|
+
return c.json(tickets);
|
|
3385
3449
|
});
|
|
3386
3450
|
|
|
3451
|
+
// src/routes/api.ts
|
|
3452
|
+
var apiRoutes = new Hono6();
|
|
3453
|
+
apiRoutes.route("/", ticketRoutes);
|
|
3454
|
+
apiRoutes.route("/", attachmentRoutes);
|
|
3455
|
+
apiRoutes.route("/", channelRoutes);
|
|
3456
|
+
apiRoutes.route("/", settingsRoutes);
|
|
3457
|
+
apiRoutes.route("/", dashboardRoutes);
|
|
3458
|
+
|
|
3387
3459
|
// src/routes/backups.ts
|
|
3388
|
-
import { Hono as
|
|
3389
|
-
var backupRoutes = new
|
|
3460
|
+
import { Hono as Hono7 } from "hono";
|
|
3461
|
+
var backupRoutes = new Hono7();
|
|
3390
3462
|
backupRoutes.get("/", (c) => {
|
|
3391
3463
|
const dataDir2 = c.get("dataDir");
|
|
3392
3464
|
const backups = listBackups(dataDir2);
|
|
@@ -3435,7 +3507,7 @@ backupRoutes.post("/restore", async (c) => {
|
|
|
3435
3507
|
});
|
|
3436
3508
|
|
|
3437
3509
|
// src/routes/pages.tsx
|
|
3438
|
-
import { Hono as
|
|
3510
|
+
import { Hono as Hono8 } from "hono";
|
|
3439
3511
|
|
|
3440
3512
|
// src/utils/escapeHtml.ts
|
|
3441
3513
|
function escapeHtml(str) {
|
|
@@ -3630,7 +3702,7 @@ function Layout({ title, children }) {
|
|
|
3630
3702
|
}
|
|
3631
3703
|
|
|
3632
3704
|
// src/routes/pages.tsx
|
|
3633
|
-
var pageRoutes = new
|
|
3705
|
+
var pageRoutes = new Hono8();
|
|
3634
3706
|
pageRoutes.get("/", (c) => {
|
|
3635
3707
|
const html = /* @__PURE__ */ jsx(Layout, { title: "Hot Sheet", children: [
|
|
3636
3708
|
/* @__PURE__ */ jsx("div", { className: "app", children: [
|
|
@@ -3732,17 +3804,66 @@ pageRoutes.get("/", (c) => {
|
|
|
3732
3804
|
/* @__PURE__ */ jsx("path", { d: "M12 8v8" })
|
|
3733
3805
|
] }) })
|
|
3734
3806
|
] }),
|
|
3735
|
-
/* @__PURE__ */ jsx("button", { className: "sidebar-item active", "data-view": "all", children:
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3807
|
+
/* @__PURE__ */ jsx("button", { className: "sidebar-item active", "data-view": "all", children: [
|
|
3808
|
+
/* @__PURE__ */ jsx("span", { className: "sidebar-icon", children: /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
3809
|
+
/* @__PURE__ */ jsx("path", { d: "M3 12h18" }),
|
|
3810
|
+
/* @__PURE__ */ jsx("path", { d: "M3 6h18" }),
|
|
3811
|
+
/* @__PURE__ */ jsx("path", { d: "M3 18h18" })
|
|
3812
|
+
] }) }),
|
|
3813
|
+
" All Tickets"
|
|
3814
|
+
] }),
|
|
3815
|
+
/* @__PURE__ */ jsx("button", { className: "sidebar-item", "data-view": "non-verified", children: [
|
|
3816
|
+
/* @__PURE__ */ jsx("span", { className: "sidebar-icon", children: "\u25D4" }),
|
|
3817
|
+
" Non-Verified"
|
|
3818
|
+
] }),
|
|
3819
|
+
/* @__PURE__ */ jsx("button", { className: "sidebar-item", "data-view": "up-next", children: [
|
|
3820
|
+
/* @__PURE__ */ jsx("span", { className: "sidebar-icon", children: "\u2605" }),
|
|
3821
|
+
" Up Next"
|
|
3822
|
+
] }),
|
|
3823
|
+
/* @__PURE__ */ jsx("button", { className: "sidebar-item", "data-view": "open", children: [
|
|
3824
|
+
/* @__PURE__ */ jsx("span", { className: "sidebar-icon", children: "\u25CB" }),
|
|
3825
|
+
" Open"
|
|
3826
|
+
] }),
|
|
3827
|
+
/* @__PURE__ */ jsx("button", { className: "sidebar-item", "data-view": "completed", children: [
|
|
3828
|
+
/* @__PURE__ */ jsx("span", { className: "sidebar-icon", children: "\u2713" }),
|
|
3829
|
+
" Completed"
|
|
3830
|
+
] }),
|
|
3831
|
+
/* @__PURE__ */ jsx("button", { className: "sidebar-item", "data-view": "verified", children: [
|
|
3832
|
+
/* @__PURE__ */ jsx("span", { className: "sidebar-icon", children: /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
3833
|
+
/* @__PURE__ */ jsx("path", { d: "M18 6 7 17l-5-5" }),
|
|
3834
|
+
/* @__PURE__ */ jsx("path", { d: "m22 10-7.5 7.5L13 16" })
|
|
3835
|
+
] }) }),
|
|
3836
|
+
" Verified"
|
|
3837
|
+
] }),
|
|
3741
3838
|
/* @__PURE__ */ jsx("div", { id: "custom-views-container" }),
|
|
3742
3839
|
/* @__PURE__ */ jsx("div", { className: "sidebar-divider" }),
|
|
3743
|
-
/* @__PURE__ */ jsx("button", { className: "sidebar-item", "data-view": "backlog", children:
|
|
3744
|
-
|
|
3745
|
-
|
|
3840
|
+
/* @__PURE__ */ jsx("button", { className: "sidebar-item", "data-view": "backlog", children: [
|
|
3841
|
+
/* @__PURE__ */ jsx("span", { className: "sidebar-icon", children: /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
3842
|
+
/* @__PURE__ */ jsx("rect", { width: "18", height: "18", x: "3", y: "4", rx: "2" }),
|
|
3843
|
+
/* @__PURE__ */ jsx("path", { d: "M16 2v4" }),
|
|
3844
|
+
/* @__PURE__ */ jsx("path", { d: "M8 2v4" }),
|
|
3845
|
+
/* @__PURE__ */ jsx("path", { d: "M3 10h18" })
|
|
3846
|
+
] }) }),
|
|
3847
|
+
" Backlog"
|
|
3848
|
+
] }),
|
|
3849
|
+
/* @__PURE__ */ jsx("button", { className: "sidebar-item", "data-view": "archive", children: [
|
|
3850
|
+
/* @__PURE__ */ jsx("span", { className: "sidebar-icon", children: /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
3851
|
+
/* @__PURE__ */ jsx("rect", { width: "20", height: "5", x: "2", y: "3", rx: "1" }),
|
|
3852
|
+
/* @__PURE__ */ jsx("path", { d: "M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8" }),
|
|
3853
|
+
/* @__PURE__ */ jsx("path", { d: "M10 12h4" })
|
|
3854
|
+
] }) }),
|
|
3855
|
+
" Archive"
|
|
3856
|
+
] }),
|
|
3857
|
+
/* @__PURE__ */ jsx("button", { className: "sidebar-item", "data-view": "trash", children: [
|
|
3858
|
+
/* @__PURE__ */ jsx("span", { className: "sidebar-icon", children: /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
3859
|
+
/* @__PURE__ */ jsx("path", { d: "M3 6h18" }),
|
|
3860
|
+
/* @__PURE__ */ jsx("path", { d: "M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" }),
|
|
3861
|
+
/* @__PURE__ */ jsx("path", { d: "M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" }),
|
|
3862
|
+
/* @__PURE__ */ jsx("line", { x1: "10", x2: "10", y1: "11", y2: "17" }),
|
|
3863
|
+
/* @__PURE__ */ jsx("line", { x1: "14", x2: "14", y1: "11", y2: "17" })
|
|
3864
|
+
] }) }),
|
|
3865
|
+
" Trash"
|
|
3866
|
+
] })
|
|
3746
3867
|
] }),
|
|
3747
3868
|
/* @__PURE__ */ jsx("div", { className: "sidebar-section", id: "sidebar-categories", children: [
|
|
3748
3869
|
/* @__PURE__ */ jsx("div", { className: "sidebar-label", children: "Category" }),
|
|
@@ -3773,11 +3894,35 @@ pageRoutes.get("/", (c) => {
|
|
|
3773
3894
|
] }),
|
|
3774
3895
|
/* @__PURE__ */ jsx("div", { className: "sidebar-section", children: [
|
|
3775
3896
|
/* @__PURE__ */ jsx("div", { className: "sidebar-label", children: "Priority" }),
|
|
3776
|
-
/* @__PURE__ */ jsx("button", { className: "sidebar-item", "data-view": "priority:highest", children:
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3897
|
+
/* @__PURE__ */ jsx("button", { className: "sidebar-item", "data-view": "priority:highest", children: [
|
|
3898
|
+
/* @__PURE__ */ jsx("span", { className: "sidebar-icon", style: "color:#ef4444", children: /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
3899
|
+
/* @__PURE__ */ jsx("path", { d: "m7 11 5-5 5 5" }),
|
|
3900
|
+
/* @__PURE__ */ jsx("path", { d: "m7 17 5-5 5 5" })
|
|
3901
|
+
] }) }),
|
|
3902
|
+
" Highest"
|
|
3903
|
+
] }),
|
|
3904
|
+
/* @__PURE__ */ jsx("button", { className: "sidebar-item", "data-view": "priority:high", children: [
|
|
3905
|
+
/* @__PURE__ */ jsx("span", { className: "sidebar-icon", style: "color:#f97316", children: /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx("path", { d: "m18 15-6-6-6 6" }) }) }),
|
|
3906
|
+
" High"
|
|
3907
|
+
] }),
|
|
3908
|
+
/* @__PURE__ */ jsx("button", { className: "sidebar-item", "data-view": "priority:default", children: [
|
|
3909
|
+
/* @__PURE__ */ jsx("span", { className: "sidebar-icon", style: "color:#6b7280", children: /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
3910
|
+
/* @__PURE__ */ jsx("path", { d: "m7 15 5 5 5-5" }),
|
|
3911
|
+
/* @__PURE__ */ jsx("path", { d: "m7 9 5-5 5 5" })
|
|
3912
|
+
] }) }),
|
|
3913
|
+
" Default"
|
|
3914
|
+
] }),
|
|
3915
|
+
/* @__PURE__ */ jsx("button", { className: "sidebar-item", "data-view": "priority:low", children: [
|
|
3916
|
+
/* @__PURE__ */ jsx("span", { className: "sidebar-icon", style: "color:#3b82f6", children: /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx("path", { d: "m6 9 6 6 6-6" }) }) }),
|
|
3917
|
+
" Low"
|
|
3918
|
+
] }),
|
|
3919
|
+
/* @__PURE__ */ jsx("button", { className: "sidebar-item", "data-view": "priority:lowest", children: [
|
|
3920
|
+
/* @__PURE__ */ jsx("span", { className: "sidebar-icon", style: "color:#94a3b8", children: /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
3921
|
+
/* @__PURE__ */ jsx("path", { d: "m7 7 5 5 5-5" }),
|
|
3922
|
+
/* @__PURE__ */ jsx("path", { d: "m7 13 5 5 5-5" })
|
|
3923
|
+
] }) }),
|
|
3924
|
+
" Lowest"
|
|
3925
|
+
] })
|
|
3781
3926
|
] }),
|
|
3782
3927
|
/* @__PURE__ */ jsx("div", { className: "sidebar-stats", id: "stats-bar" })
|
|
3783
3928
|
] }),
|
|
@@ -3984,6 +4129,13 @@ pageRoutes.get("/", (c) => {
|
|
|
3984
4129
|
/* @__PURE__ */ jsx("label", { children: "Auto-clear verified after (days)" }),
|
|
3985
4130
|
/* @__PURE__ */ jsx("input", { type: "number", id: "settings-verified-days", min: "1", value: "30" })
|
|
3986
4131
|
] }),
|
|
4132
|
+
/* @__PURE__ */ jsx("div", { className: "settings-field settings-field-checkbox", children: [
|
|
4133
|
+
/* @__PURE__ */ jsx("label", { children: [
|
|
4134
|
+
/* @__PURE__ */ jsx("input", { type: "checkbox", id: "settings-auto-order", checked: true }),
|
|
4135
|
+
" Auto-prioritize tickets"
|
|
4136
|
+
] }),
|
|
4137
|
+
/* @__PURE__ */ jsx("span", { className: "settings-hint", children: "When no Up Next items exist, the AI will evaluate open tickets and choose what to work on next." })
|
|
4138
|
+
] }),
|
|
3987
4139
|
/* @__PURE__ */ jsx("div", { className: "settings-field", children: [
|
|
3988
4140
|
/* @__PURE__ */ jsx("label", { children: "When Claude needs permission" }),
|
|
3989
4141
|
/* @__PURE__ */ jsx("select", { id: "settings-notify-permission", children: [
|
|
@@ -4088,24 +4240,24 @@ function tryServe(fetch2, port2) {
|
|
|
4088
4240
|
});
|
|
4089
4241
|
}
|
|
4090
4242
|
async function startServer(port2, dataDir2, options) {
|
|
4091
|
-
const app = new
|
|
4243
|
+
const app = new Hono9();
|
|
4092
4244
|
app.use("*", async (c, next) => {
|
|
4093
4245
|
c.set("dataDir", dataDir2);
|
|
4094
4246
|
await next();
|
|
4095
4247
|
});
|
|
4096
4248
|
const selfDir = dirname2(fileURLToPath2(import.meta.url));
|
|
4097
|
-
const distDir = existsSync8(
|
|
4249
|
+
const distDir = existsSync8(join11(selfDir, "client", "styles.css")) ? join11(selfDir, "client") : join11(selfDir, "..", "dist", "client");
|
|
4098
4250
|
app.get("/static/styles.css", (c) => {
|
|
4099
|
-
const css = readFileSync7(
|
|
4251
|
+
const css = readFileSync7(join11(distDir, "styles.css"), "utf-8");
|
|
4100
4252
|
return c.text(css, 200, { "Content-Type": "text/css", "Cache-Control": "no-cache" });
|
|
4101
4253
|
});
|
|
4102
4254
|
app.get("/static/app.js", (c) => {
|
|
4103
|
-
const js = readFileSync7(
|
|
4255
|
+
const js = readFileSync7(join11(distDir, "app.global.js"), "utf-8");
|
|
4104
4256
|
return c.text(js, 200, { "Content-Type": "application/javascript", "Cache-Control": "no-cache" });
|
|
4105
4257
|
});
|
|
4106
4258
|
app.get("/static/assets/:filename", (c) => {
|
|
4107
4259
|
const filename = c.req.param("filename");
|
|
4108
|
-
const filePath =
|
|
4260
|
+
const filePath = join11(distDir, "assets", filename);
|
|
4109
4261
|
if (!existsSync8(filePath)) return c.notFound();
|
|
4110
4262
|
const content = readFileSync7(filePath);
|
|
4111
4263
|
const ext = filename.split(".").pop();
|
|
@@ -4185,15 +4337,15 @@ async function startServer(port2, dataDir2, options) {
|
|
|
4185
4337
|
import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "fs";
|
|
4186
4338
|
import { get } from "https";
|
|
4187
4339
|
import { homedir } from "os";
|
|
4188
|
-
import { dirname as dirname3, join as
|
|
4340
|
+
import { dirname as dirname3, join as join12 } from "path";
|
|
4189
4341
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
4190
|
-
var DATA_DIR =
|
|
4191
|
-
var CHECK_FILE =
|
|
4342
|
+
var DATA_DIR = join12(homedir(), ".hotsheet");
|
|
4343
|
+
var CHECK_FILE = join12(DATA_DIR, "last-update-check");
|
|
4192
4344
|
var PACKAGE_NAME = "hotsheet";
|
|
4193
4345
|
function getCurrentVersion() {
|
|
4194
4346
|
try {
|
|
4195
4347
|
const dir = dirname3(fileURLToPath3(import.meta.url));
|
|
4196
|
-
const pkg = JSON.parse(readFileSync8(
|
|
4348
|
+
const pkg = JSON.parse(readFileSync8(join12(dir, "..", "package.json"), "utf-8"));
|
|
4197
4349
|
return pkg.version;
|
|
4198
4350
|
} catch {
|
|
4199
4351
|
return "0.0.0";
|
|
@@ -4315,7 +4467,7 @@ Examples:
|
|
|
4315
4467
|
function parseArgs(argv) {
|
|
4316
4468
|
const args = argv.slice(2);
|
|
4317
4469
|
let port2 = 4174;
|
|
4318
|
-
let dataDir2 =
|
|
4470
|
+
let dataDir2 = join13(process.cwd(), ".hotsheet");
|
|
4319
4471
|
let demo = null;
|
|
4320
4472
|
let forceUpdateCheck = false;
|
|
4321
4473
|
let noOpen = false;
|
|
@@ -4382,7 +4534,7 @@ async function main() {
|
|
|
4382
4534
|
}
|
|
4383
4535
|
process.exit(1);
|
|
4384
4536
|
}
|
|
4385
|
-
dataDir2 =
|
|
4537
|
+
dataDir2 = join13(tmpdir(), `hotsheet-demo-${demo}-${Date.now()}`);
|
|
4386
4538
|
console.log(`
|
|
4387
4539
|
DEMO MODE: ${scenario.label}
|
|
4388
4540
|
`);
|