hotsheet 0.2.2 → 0.2.4

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 CHANGED
@@ -9,88 +9,32 @@ var __export = (target, all) => {
9
9
  __defProp(target, name, { get: all[name], enumerable: true });
10
10
  };
11
11
 
12
- // src/gitignore.ts
13
- var gitignore_exports = {};
14
- __export(gitignore_exports, {
15
- addHotsheetToGitignore: () => addHotsheetToGitignore,
16
- ensureGitignore: () => ensureGitignore,
17
- getGitRoot: () => getGitRoot,
18
- isGitRepo: () => isGitRepo,
19
- isHotsheetGitignored: () => isHotsheetGitignored
20
- });
21
- import { execSync } from "child_process";
22
- import { appendFileSync, existsSync, readFileSync } from "fs";
23
- import { join as join2 } from "path";
24
- function isHotsheetGitignored(repoRoot) {
25
- try {
26
- execSync("git check-ignore -q .hotsheet", { cwd: repoRoot, stdio: "ignore" });
27
- return true;
28
- } catch {
29
- return false;
30
- }
31
- }
32
- function isGitRepo(dir) {
33
- try {
34
- execSync("git rev-parse --is-inside-work-tree", { cwd: dir, stdio: "ignore" });
35
- return true;
36
- } catch {
37
- return false;
38
- }
39
- }
40
- function getGitRoot(dir) {
41
- try {
42
- return execSync("git rev-parse --show-toplevel", { cwd: dir, encoding: "utf-8" }).trim();
43
- } catch {
44
- return null;
45
- }
46
- }
47
- function addHotsheetToGitignore(repoRoot) {
48
- const gitignorePath = join2(repoRoot, ".gitignore");
49
- if (existsSync(gitignorePath)) {
50
- const content = readFileSync(gitignorePath, "utf-8");
51
- if (content.includes(".hotsheet")) return;
52
- const prefix = content.endsWith("\n") ? "" : "\n";
53
- appendFileSync(gitignorePath, `${prefix}.hotsheet/
54
- `);
55
- } else {
56
- appendFileSync(gitignorePath, ".hotsheet/\n");
57
- }
58
- }
59
- function ensureGitignore(cwd) {
60
- if (!isGitRepo(cwd)) return;
61
- const gitRoot = getGitRoot(cwd);
62
- if (gitRoot === null) return;
63
- if (!isHotsheetGitignored(gitRoot)) {
64
- addHotsheetToGitignore(gitRoot);
65
- console.log(" Added .hotsheet/ to .gitignore");
66
- }
67
- }
68
- var init_gitignore = __esm({
69
- "src/gitignore.ts"() {
70
- "use strict";
71
- }
72
- });
73
-
74
- // src/cli.ts
75
- import { mkdirSync as mkdirSync4 } from "fs";
76
- import { tmpdir } from "os";
77
- import { join as join7, resolve } from "path";
78
-
79
- // src/cleanup.ts
80
- import { rmSync as rmSync2 } from "fs";
81
-
82
12
  // src/db/connection.ts
13
+ var connection_exports = {};
14
+ __export(connection_exports, {
15
+ adoptDb: () => adoptDb,
16
+ closeDb: () => closeDb,
17
+ getDb: () => getDb,
18
+ setDataDir: () => setDataDir
19
+ });
83
20
  import { PGlite } from "@electric-sql/pglite";
84
21
  import { mkdirSync, rmSync } from "fs";
85
22
  import { join } from "path";
86
- var db = null;
87
- var currentDbPath = null;
88
23
  function setDataDir(dataDir2) {
89
24
  const dbDir = join(dataDir2, "db");
90
25
  mkdirSync(dbDir, { recursive: true });
91
26
  mkdirSync(join(dataDir2, "attachments"), { recursive: true });
92
27
  currentDbPath = dbDir;
93
28
  }
29
+ async function closeDb() {
30
+ if (db) {
31
+ await db.close();
32
+ db = null;
33
+ }
34
+ }
35
+ function adoptDb(instance) {
36
+ db = instance;
37
+ }
94
38
  async function getDb() {
95
39
  if (db !== null) return db;
96
40
  if (currentDbPath === null) throw new Error("Data directory not set. Call setDataDir() first.");
@@ -166,8 +110,318 @@ async function initSchema(db2) {
166
110
  `).catch(() => {
167
111
  });
168
112
  }
113
+ var db, currentDbPath;
114
+ var init_connection = __esm({
115
+ "src/db/connection.ts"() {
116
+ "use strict";
117
+ db = null;
118
+ currentDbPath = null;
119
+ }
120
+ });
121
+
122
+ // src/file-settings.ts
123
+ var file_settings_exports = {};
124
+ __export(file_settings_exports, {
125
+ getBackupDir: () => getBackupDir,
126
+ readFileSettings: () => readFileSettings,
127
+ writeFileSettings: () => writeFileSettings
128
+ });
129
+ import { existsSync, readFileSync, writeFileSync } from "fs";
130
+ import { join as join2 } from "path";
131
+ function settingsPath(dataDir2) {
132
+ return join2(dataDir2, "settings.json");
133
+ }
134
+ function readFileSettings(dataDir2) {
135
+ const path = settingsPath(dataDir2);
136
+ if (!existsSync(path)) return {};
137
+ try {
138
+ return JSON.parse(readFileSync(path, "utf-8"));
139
+ } catch {
140
+ return {};
141
+ }
142
+ }
143
+ function writeFileSettings(dataDir2, updates) {
144
+ const current = readFileSettings(dataDir2);
145
+ const merged = { ...current, ...updates };
146
+ writeFileSync(settingsPath(dataDir2), JSON.stringify(merged, null, 2) + "\n", "utf-8");
147
+ return merged;
148
+ }
149
+ function getBackupDir(dataDir2) {
150
+ const settings = readFileSettings(dataDir2);
151
+ return settings.backupDir || join2(dataDir2, "backups");
152
+ }
153
+ var init_file_settings = __esm({
154
+ "src/file-settings.ts"() {
155
+ "use strict";
156
+ }
157
+ });
158
+
159
+ // src/gitignore.ts
160
+ var gitignore_exports = {};
161
+ __export(gitignore_exports, {
162
+ addHotsheetToGitignore: () => addHotsheetToGitignore,
163
+ ensureGitignore: () => ensureGitignore,
164
+ getGitRoot: () => getGitRoot,
165
+ isGitRepo: () => isGitRepo,
166
+ isHotsheetGitignored: () => isHotsheetGitignored
167
+ });
168
+ import { execSync } from "child_process";
169
+ import { appendFileSync, existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
170
+ import { join as join5 } from "path";
171
+ function isHotsheetGitignored(repoRoot) {
172
+ try {
173
+ execSync("git check-ignore -q .hotsheet", { cwd: repoRoot, stdio: "ignore" });
174
+ return true;
175
+ } catch {
176
+ return false;
177
+ }
178
+ }
179
+ function isGitRepo(dir) {
180
+ try {
181
+ execSync("git rev-parse --is-inside-work-tree", { cwd: dir, stdio: "ignore" });
182
+ return true;
183
+ } catch {
184
+ return false;
185
+ }
186
+ }
187
+ function getGitRoot(dir) {
188
+ try {
189
+ return execSync("git rev-parse --show-toplevel", { cwd: dir, encoding: "utf-8" }).trim();
190
+ } catch {
191
+ return null;
192
+ }
193
+ }
194
+ function addHotsheetToGitignore(repoRoot) {
195
+ const gitignorePath = join5(repoRoot, ".gitignore");
196
+ if (existsSync4(gitignorePath)) {
197
+ const content = readFileSync4(gitignorePath, "utf-8");
198
+ if (content.includes(".hotsheet")) return;
199
+ const prefix = content.endsWith("\n") ? "" : "\n";
200
+ appendFileSync(gitignorePath, `${prefix}.hotsheet/
201
+ `);
202
+ } else {
203
+ appendFileSync(gitignorePath, ".hotsheet/\n");
204
+ }
205
+ }
206
+ function ensureGitignore(cwd) {
207
+ if (!isGitRepo(cwd)) return;
208
+ const gitRoot = getGitRoot(cwd);
209
+ if (gitRoot === null) return;
210
+ if (!isHotsheetGitignored(gitRoot)) {
211
+ addHotsheetToGitignore(gitRoot);
212
+ console.log(" Added .hotsheet/ to .gitignore");
213
+ }
214
+ }
215
+ var init_gitignore = __esm({
216
+ "src/gitignore.ts"() {
217
+ "use strict";
218
+ }
219
+ });
220
+
221
+ // src/cli.ts
222
+ import { mkdirSync as mkdirSync6 } from "fs";
223
+ import { tmpdir } from "os";
224
+ import { join as join11, resolve } from "path";
225
+
226
+ // src/backup.ts
227
+ init_connection();
228
+ init_file_settings();
229
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync2, rmSync as rmSync2, statSync, writeFileSync as writeFileSync2 } from "fs";
230
+ import { join as join3 } from "path";
231
+ import { PGlite as PGlite2 } from "@electric-sql/pglite";
232
+ var TIERS = {
233
+ "5min": { intervalMs: 5 * 60 * 1e3, maxAge: 60 * 60 * 1e3, maxCount: 12 },
234
+ "hourly": { intervalMs: 60 * 60 * 1e3, maxAge: 12 * 60 * 60 * 1e3, maxCount: 12 },
235
+ "daily": { intervalMs: 24 * 60 * 60 * 1e3, maxAge: 7 * 24 * 60 * 60 * 1e3, maxCount: 7 }
236
+ };
237
+ var backupInProgress = false;
238
+ var previewDb = null;
239
+ var currentDataDir = null;
240
+ function backupsDir(dataDir2) {
241
+ return getBackupDir(dataDir2);
242
+ }
243
+ function tierDir(dataDir2, tier) {
244
+ return join3(backupsDir(dataDir2), tier);
245
+ }
246
+ function formatTimestamp(date) {
247
+ return date.toISOString().replace(/:/g, "-").replace(/\.\d+Z$/, "Z");
248
+ }
249
+ function parseTimestamp(filename) {
250
+ const match = filename.match(/^backup-(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2})Z\.tar\.gz$/);
251
+ if (!match) return null;
252
+ const iso = `${match[1]}T${match[2]}:${match[3]}:${match[4]}Z`;
253
+ const d = new Date(iso);
254
+ return isNaN(d.getTime()) ? null : d;
255
+ }
256
+ async function createBackup(dataDir2, tier) {
257
+ if (backupInProgress) return null;
258
+ backupInProgress = true;
259
+ try {
260
+ const db2 = await getDb();
261
+ const dir = tierDir(dataDir2, tier);
262
+ mkdirSync2(dir, { recursive: true });
263
+ const blob = await db2.dumpDataDir("gzip");
264
+ const buffer = Buffer.from(await blob.arrayBuffer());
265
+ const now = /* @__PURE__ */ new Date();
266
+ const filename = `backup-${formatTimestamp(now)}.tar.gz`;
267
+ const filePath = join3(dir, filename);
268
+ writeFileSync2(filePath, buffer);
269
+ let ticketCount = 0;
270
+ try {
271
+ const result = await db2.query(`SELECT COUNT(*) as count FROM tickets WHERE status != 'deleted'`);
272
+ ticketCount = parseInt(result.rows[0]?.count || "0", 10);
273
+ } catch {
274
+ }
275
+ const info = {
276
+ tier,
277
+ filename,
278
+ createdAt: now.toISOString(),
279
+ ticketCount,
280
+ sizeBytes: buffer.length
281
+ };
282
+ pruneBackups(dataDir2, tier);
283
+ return info;
284
+ } catch (err) {
285
+ console.error(`Backup failed (${tier}):`, err);
286
+ return null;
287
+ } finally {
288
+ backupInProgress = false;
289
+ }
290
+ }
291
+ function pruneBackups(dataDir2, tier) {
292
+ const dir = tierDir(dataDir2, tier);
293
+ if (!existsSync2(dir)) return;
294
+ const config = TIERS[tier];
295
+ const cutoff = Date.now() - config.maxAge;
296
+ const files = readdirSync(dir).filter((f) => f.endsWith(".tar.gz")).map((f) => ({ filename: f, date: parseTimestamp(f) })).filter((f) => f.date !== null).sort((a, b) => b.date.getTime() - a.date.getTime());
297
+ for (let i = 0; i < files.length; i++) {
298
+ if (i >= config.maxCount || files[i].date.getTime() < cutoff) {
299
+ try {
300
+ rmSync2(join3(dir, files[i].filename), { force: true });
301
+ } catch {
302
+ }
303
+ }
304
+ }
305
+ }
306
+ function listBackups(dataDir2) {
307
+ const backups = [];
308
+ for (const tier of Object.keys(TIERS)) {
309
+ const dir = tierDir(dataDir2, tier);
310
+ if (!existsSync2(dir)) continue;
311
+ for (const filename of readdirSync(dir)) {
312
+ if (!filename.endsWith(".tar.gz")) continue;
313
+ const date = parseTimestamp(filename);
314
+ if (!date) continue;
315
+ let sizeBytes = 0;
316
+ try {
317
+ sizeBytes = statSync(join3(dir, filename)).size;
318
+ } catch {
319
+ }
320
+ backups.push({
321
+ tier,
322
+ filename,
323
+ createdAt: date.toISOString(),
324
+ ticketCount: -1,
325
+ // Unknown without opening the backup
326
+ sizeBytes
327
+ });
328
+ }
329
+ }
330
+ return backups.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
331
+ }
332
+ async function loadBackupForPreview(dataDir2, tier, filename) {
333
+ await cleanupPreview();
334
+ const filePath = join3(tierDir(dataDir2, tier), filename);
335
+ if (!existsSync2(filePath)) throw new Error("Backup file not found");
336
+ const buffer = readFileSync2(filePath);
337
+ const blob = new Blob([buffer]);
338
+ const previewDir = join3(backupsDir(dataDir2), "_preview");
339
+ mkdirSync2(previewDir, { recursive: true });
340
+ previewDb = new PGlite2(previewDir, { loadDataDir: blob });
341
+ await previewDb.waitReady;
342
+ const tickets = await previewDb.query(
343
+ `SELECT * FROM tickets WHERE status != 'deleted' ORDER BY created_at DESC`
344
+ );
345
+ const statsResult = await previewDb.query(`
346
+ SELECT
347
+ COUNT(*) FILTER (WHERE status != 'deleted') as total,
348
+ COUNT(*) FILTER (WHERE status IN ('not_started', 'started')) as open,
349
+ COUNT(*) FILTER (WHERE up_next = true AND status != 'deleted') as up_next
350
+ FROM tickets
351
+ `);
352
+ const row = statsResult.rows[0];
353
+ return {
354
+ tickets: tickets.rows,
355
+ stats: {
356
+ total: parseInt(row?.total || "0", 10),
357
+ open: parseInt(row?.open || "0", 10),
358
+ upNext: parseInt(row?.up_next || "0", 10)
359
+ }
360
+ };
361
+ }
362
+ async function cleanupPreview() {
363
+ if (previewDb) {
364
+ try {
365
+ await previewDb.close();
366
+ } catch {
367
+ }
368
+ previewDb = null;
369
+ }
370
+ if (currentDataDir) {
371
+ const previewDir = join3(backupsDir(currentDataDir), "_preview");
372
+ if (existsSync2(previewDir)) {
373
+ rmSync2(previewDir, { recursive: true, force: true });
374
+ }
375
+ }
376
+ }
377
+ async function restoreBackup(dataDir2, tier, filename) {
378
+ await cleanupPreview();
379
+ const filePath = join3(tierDir(dataDir2, tier), filename);
380
+ if (!existsSync2(filePath)) throw new Error("Backup file not found");
381
+ await createBackup(dataDir2, "5min");
382
+ const buffer = readFileSync2(filePath);
383
+ const blob = new Blob([buffer]);
384
+ await closeDb();
385
+ const dbDir = join3(dataDir2, "db");
386
+ rmSync2(dbDir, { recursive: true, force: true });
387
+ setDataDir(dataDir2);
388
+ const { getDb: reinitDb } = await Promise.resolve().then(() => (init_connection(), connection_exports));
389
+ const PGliteClass = (await import("@electric-sql/pglite")).PGlite;
390
+ const newDb = new PGliteClass(dbDir, { loadDataDir: blob });
391
+ await newDb.waitReady;
392
+ const { adoptDb: adoptDb2 } = await Promise.resolve().then(() => (init_connection(), connection_exports));
393
+ adoptDb2(newDb);
394
+ }
395
+ var fiveMinTimer = null;
396
+ function scheduleFiveMinBackup(dataDir2) {
397
+ if (fiveMinTimer) clearTimeout(fiveMinTimer);
398
+ fiveMinTimer = setTimeout(() => {
399
+ void createBackup(dataDir2, "5min").then(() => scheduleFiveMinBackup(dataDir2));
400
+ }, TIERS["5min"].intervalMs);
401
+ }
402
+ async function triggerManualBackup(dataDir2) {
403
+ const result = await createBackup(dataDir2, "5min");
404
+ if (result) scheduleFiveMinBackup(dataDir2);
405
+ return result;
406
+ }
407
+ function initBackupScheduler(dataDir2) {
408
+ currentDataDir = dataDir2;
409
+ const previewDir = join3(backupsDir(dataDir2), "_preview");
410
+ if (existsSync2(previewDir)) {
411
+ rmSync2(previewDir, { recursive: true, force: true });
412
+ }
413
+ setTimeout(() => {
414
+ void createBackup(dataDir2, "5min").then(() => scheduleFiveMinBackup(dataDir2));
415
+ }, 1e4);
416
+ setInterval(() => void createBackup(dataDir2, "hourly"), TIERS["hourly"].intervalMs);
417
+ setInterval(() => void createBackup(dataDir2, "daily"), TIERS["daily"].intervalMs);
418
+ }
419
+
420
+ // src/cleanup.ts
421
+ import { rmSync as rmSync3 } from "fs";
169
422
 
170
423
  // src/db/queries.ts
424
+ init_connection();
171
425
  function parseNotes(raw2) {
172
426
  if (!raw2 || raw2 === "") return [];
173
427
  try {
@@ -203,6 +457,10 @@ async function createTicket(title, defaults) {
203
457
  cols.push("up_next");
204
458
  vals.push(defaults.up_next);
205
459
  }
460
+ if (defaults?.details !== void 0 && defaults.details !== "") {
461
+ cols.push("details");
462
+ vals.push(defaults.details);
463
+ }
206
464
  const placeholders = vals.map((_, i) => `$${i + 1}`).join(", ");
207
465
  const result = await db2.query(
208
466
  `INSERT INTO tickets (${cols.join(", ")}) VALUES (${placeholders}) RETURNING *`,
@@ -461,7 +719,7 @@ async function cleanupAttachments() {
461
719
  const attachments = await getAttachments(ticket.id);
462
720
  for (const att of attachments) {
463
721
  try {
464
- rmSync2(att.stored_path, { force: true });
722
+ rmSync3(att.stored_path, { force: true });
465
723
  } catch {
466
724
  }
467
725
  }
@@ -476,7 +734,59 @@ async function cleanupAttachments() {
476
734
  }
477
735
  }
478
736
 
737
+ // src/cli.ts
738
+ init_connection();
739
+
740
+ // src/lock.ts
741
+ import { existsSync as existsSync3, readFileSync as readFileSync3, rmSync as rmSync4, writeFileSync as writeFileSync3 } from "fs";
742
+ import { join as join4 } from "path";
743
+ var lockPath = null;
744
+ function acquireLock(dataDir2) {
745
+ lockPath = join4(dataDir2, "hotsheet.lock");
746
+ if (existsSync3(lockPath)) {
747
+ try {
748
+ const contents = JSON.parse(readFileSync3(lockPath, "utf-8"));
749
+ const pid = contents.pid;
750
+ try {
751
+ process.kill(pid, 0);
752
+ console.error(`
753
+ Error: Another Hot Sheet instance (PID ${pid}) is already using this data directory.`);
754
+ console.error(` Directory: ${dataDir2}`);
755
+ console.error(` Stop that instance first, or use --data-dir to point to a different location.
756
+ `);
757
+ process.exit(1);
758
+ } catch {
759
+ console.log(` Removing stale lock from PID ${pid}`);
760
+ rmSync4(lockPath, { force: true });
761
+ }
762
+ } catch {
763
+ rmSync4(lockPath, { force: true });
764
+ }
765
+ }
766
+ writeFileSync3(lockPath, JSON.stringify({ pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() }));
767
+ const cleanup = () => releaseLock();
768
+ process.on("exit", cleanup);
769
+ process.on("SIGINT", () => {
770
+ cleanup();
771
+ process.exit(0);
772
+ });
773
+ process.on("SIGTERM", () => {
774
+ cleanup();
775
+ process.exit(0);
776
+ });
777
+ }
778
+ function releaseLock() {
779
+ if (lockPath) {
780
+ try {
781
+ rmSync4(lockPath, { force: true });
782
+ } catch {
783
+ }
784
+ lockPath = null;
785
+ }
786
+ }
787
+
479
788
  // src/demo.ts
789
+ init_connection();
480
790
  var DEMO_SCENARIOS = [
481
791
  { id: 1, label: "Main UI \u2014 all tickets with detail panel" },
482
792
  { id: 2, label: "Quick entry \u2014 bullet-list ticket creation" },
@@ -1246,19 +1556,19 @@ init_gitignore();
1246
1556
  // src/server.ts
1247
1557
  import { serve } from "@hono/node-server";
1248
1558
  import { exec } from "child_process";
1249
- import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
1250
- import { Hono as Hono3 } from "hono";
1251
- import { dirname, join as join5 } from "path";
1559
+ import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
1560
+ import { Hono as Hono4 } from "hono";
1561
+ import { dirname, join as join9 } from "path";
1252
1562
  import { fileURLToPath } from "url";
1253
1563
 
1254
1564
  // src/routes/api.ts
1255
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, rmSync as rmSync3, writeFileSync as writeFileSync2 } from "fs";
1565
+ import { existsSync as existsSync6, mkdirSync as mkdirSync4, rmSync as rmSync5 } from "fs";
1256
1566
  import { Hono } from "hono";
1257
- import { basename, extname, join as join4, relative } from "path";
1567
+ import { basename, extname, join as join8, relative as relative2 } from "path";
1258
1568
 
1259
- // src/sync/markdown.ts
1260
- import { writeFileSync } from "fs";
1261
- import { join as join3 } from "path";
1569
+ // src/skills.ts
1570
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
1571
+ import { join as join6, relative } from "path";
1262
1572
 
1263
1573
  // src/types.ts
1264
1574
  var CATEGORY_DESCRIPTIONS = {
@@ -1270,7 +1580,251 @@ var CATEGORY_DESCRIPTIONS = {
1270
1580
  investigation: "Items requiring research or analysis"
1271
1581
  };
1272
1582
 
1583
+ // src/skills.ts
1584
+ var SKILL_VERSION = 1;
1585
+ var skillPort;
1586
+ var skillDataDir;
1587
+ function initSkills(port2, dataDir2) {
1588
+ skillPort = port2;
1589
+ skillDataDir = dataDir2;
1590
+ }
1591
+ var TICKET_SKILLS = [
1592
+ { name: "hs-bug", category: "bug", label: "bug" },
1593
+ { name: "hs-feature", category: "feature", label: "feature" },
1594
+ { name: "hs-task", category: "task", label: "task" },
1595
+ { name: "hs-issue", category: "issue", label: "issue" },
1596
+ { name: "hs-investigation", category: "investigation", label: "investigation" },
1597
+ { name: "hs-req-change", category: "requirement_change", label: "requirement change" }
1598
+ ];
1599
+ function versionHeader() {
1600
+ return `<!-- hotsheet-skill-version: ${SKILL_VERSION} port: ${skillPort} -->`;
1601
+ }
1602
+ function parseVersionHeader(content) {
1603
+ const match = content.match(/<!-- hotsheet-skill-version: (\d+) port: (\d+) -->/);
1604
+ if (!match) return null;
1605
+ return { version: parseInt(match[1], 10), port: parseInt(match[2], 10) };
1606
+ }
1607
+ function updateFile(path, content) {
1608
+ if (existsSync5(path)) {
1609
+ const existing = readFileSync5(path, "utf-8");
1610
+ const header = parseVersionHeader(existing);
1611
+ if (header && header.version >= SKILL_VERSION && header.port === skillPort) {
1612
+ return false;
1613
+ }
1614
+ }
1615
+ writeFileSync4(path, content, "utf-8");
1616
+ return true;
1617
+ }
1618
+ function ticketSkillBody(skill) {
1619
+ const desc = CATEGORY_DESCRIPTIONS[skill.category];
1620
+ return [
1621
+ `Create a new Hot Sheet **${skill.label}** ticket. ${desc}.`,
1622
+ "",
1623
+ "**Parsing the input:**",
1624
+ '- 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',
1625
+ "- Otherwise, use the entire input as the title",
1626
+ "",
1627
+ "**Create the ticket** by running:",
1628
+ "```bash",
1629
+ `curl -s -X POST http://localhost:${skillPort}/api/tickets \\`,
1630
+ ' -H "Content-Type: application/json" \\',
1631
+ ` -d '{"title": "<TITLE>", "defaults": {"category": "${skill.category}", "up_next": <true|false>}}'`,
1632
+ "```",
1633
+ "",
1634
+ "Report the created ticket number and title to the user."
1635
+ ].join("\n");
1636
+ }
1637
+ function mainSkillBody() {
1638
+ const worklistRel = relative(process.cwd(), join6(skillDataDir, "worklist.md"));
1639
+ return [
1640
+ `Read \`${worklistRel}\` and work through the tickets in priority order.`,
1641
+ "",
1642
+ "For each ticket:",
1643
+ "1. Read the ticket details carefully",
1644
+ "2. Implement the work described",
1645
+ "3. When complete, mark it done via the Hot Sheet UI",
1646
+ "",
1647
+ "Work through them in order of priority, where reasonable."
1648
+ ].join("\n");
1649
+ }
1650
+ var HOTSHEET_ALLOW_PATTERNS = [
1651
+ "Bash(curl * http://localhost:417*/api/*)",
1652
+ "Bash(curl * http://localhost:418*/api/*)"
1653
+ ];
1654
+ var HOTSHEET_CURL_RE = /^Bash\(curl \* http:\/\/localhost:\d+\/api\/\*\)$|^Bash\(curl \* http:\/\/localhost:41[78]\*\/api\/\*\)$/;
1655
+ function ensureClaudePermissions(cwd) {
1656
+ if (skillPort < 4170 || skillPort > 4189) return false;
1657
+ const settingsPath2 = join6(cwd, ".claude", "settings.json");
1658
+ let settings = {};
1659
+ if (existsSync5(settingsPath2)) {
1660
+ try {
1661
+ settings = JSON.parse(readFileSync5(settingsPath2, "utf-8"));
1662
+ } catch {
1663
+ }
1664
+ }
1665
+ if (!settings.permissions) settings.permissions = {};
1666
+ if (!settings.permissions.allow) settings.permissions.allow = [];
1667
+ const allow = settings.permissions.allow;
1668
+ if (HOTSHEET_ALLOW_PATTERNS.every((p) => allow.includes(p))) return false;
1669
+ settings.permissions.allow = allow.filter((p) => !HOTSHEET_CURL_RE.test(p));
1670
+ settings.permissions.allow.push(...HOTSHEET_ALLOW_PATTERNS);
1671
+ writeFileSync4(settingsPath2, JSON.stringify(settings, null, 2) + "\n", "utf-8");
1672
+ return true;
1673
+ }
1674
+ function ensureClaudeSkills(cwd) {
1675
+ let updated = false;
1676
+ const skillsDir = join6(cwd, ".claude", "skills");
1677
+ if (ensureClaudePermissions(cwd)) updated = true;
1678
+ const mainDir = join6(skillsDir, "hotsheet");
1679
+ mkdirSync3(mainDir, { recursive: true });
1680
+ const mainContent = [
1681
+ "---",
1682
+ "name: hotsheet",
1683
+ "description: Read the Hot Sheet worklist and work through the current priority items",
1684
+ "allowed-tools: Read, Grep, Glob, Edit, Write, Bash",
1685
+ "---",
1686
+ versionHeader(),
1687
+ "",
1688
+ mainSkillBody(),
1689
+ ""
1690
+ ].join("\n");
1691
+ if (updateFile(join6(mainDir, "SKILL.md"), mainContent)) updated = true;
1692
+ for (const skill of TICKET_SKILLS) {
1693
+ const dir = join6(skillsDir, skill.name);
1694
+ mkdirSync3(dir, { recursive: true });
1695
+ const content = [
1696
+ "---",
1697
+ `name: ${skill.name}`,
1698
+ `description: Create a new ${skill.label} ticket in Hot Sheet`,
1699
+ "allowed-tools: Bash",
1700
+ "---",
1701
+ versionHeader(),
1702
+ "",
1703
+ ticketSkillBody(skill),
1704
+ ""
1705
+ ].join("\n");
1706
+ if (updateFile(join6(dir, "SKILL.md"), content)) updated = true;
1707
+ }
1708
+ return updated;
1709
+ }
1710
+ function ensureCursorRules(cwd) {
1711
+ let updated = false;
1712
+ const rulesDir = join6(cwd, ".cursor", "rules");
1713
+ mkdirSync3(rulesDir, { recursive: true });
1714
+ const mainContent = [
1715
+ "---",
1716
+ "description: Read the Hot Sheet worklist and work through the current priority items",
1717
+ "alwaysApply: false",
1718
+ "---",
1719
+ versionHeader(),
1720
+ "",
1721
+ mainSkillBody(),
1722
+ ""
1723
+ ].join("\n");
1724
+ if (updateFile(join6(rulesDir, "hotsheet.mdc"), mainContent)) updated = true;
1725
+ for (const skill of TICKET_SKILLS) {
1726
+ const content = [
1727
+ "---",
1728
+ `description: Create a new ${skill.label} ticket in Hot Sheet`,
1729
+ "alwaysApply: false",
1730
+ "---",
1731
+ versionHeader(),
1732
+ "",
1733
+ ticketSkillBody(skill),
1734
+ ""
1735
+ ].join("\n");
1736
+ if (updateFile(join6(rulesDir, `${skill.name}.mdc`), content)) updated = true;
1737
+ }
1738
+ return updated;
1739
+ }
1740
+ function ensureCopilotPrompts(cwd) {
1741
+ let updated = false;
1742
+ const promptsDir = join6(cwd, ".github", "prompts");
1743
+ mkdirSync3(promptsDir, { recursive: true });
1744
+ const mainContent = [
1745
+ "---",
1746
+ "description: Read the Hot Sheet worklist and work through the current priority items",
1747
+ "---",
1748
+ versionHeader(),
1749
+ "",
1750
+ mainSkillBody(),
1751
+ ""
1752
+ ].join("\n");
1753
+ if (updateFile(join6(promptsDir, "hotsheet.prompt.md"), mainContent)) updated = true;
1754
+ for (const skill of TICKET_SKILLS) {
1755
+ const content = [
1756
+ "---",
1757
+ `description: Create a new ${skill.label} ticket in Hot Sheet`,
1758
+ "---",
1759
+ versionHeader(),
1760
+ "",
1761
+ ticketSkillBody(skill),
1762
+ ""
1763
+ ].join("\n");
1764
+ if (updateFile(join6(promptsDir, `${skill.name}.prompt.md`), content)) updated = true;
1765
+ }
1766
+ return updated;
1767
+ }
1768
+ function ensureWindsurfRules(cwd) {
1769
+ let updated = false;
1770
+ const rulesDir = join6(cwd, ".windsurf", "rules");
1771
+ mkdirSync3(rulesDir, { recursive: true });
1772
+ const mainContent = [
1773
+ "---",
1774
+ "trigger: manual",
1775
+ "description: Read the Hot Sheet worklist and work through the current priority items",
1776
+ "---",
1777
+ versionHeader(),
1778
+ "",
1779
+ mainSkillBody(),
1780
+ ""
1781
+ ].join("\n");
1782
+ if (updateFile(join6(rulesDir, "hotsheet.md"), mainContent)) updated = true;
1783
+ for (const skill of TICKET_SKILLS) {
1784
+ const content = [
1785
+ "---",
1786
+ "trigger: manual",
1787
+ `description: Create a new ${skill.label} ticket in Hot Sheet`,
1788
+ "---",
1789
+ versionHeader(),
1790
+ "",
1791
+ ticketSkillBody(skill),
1792
+ ""
1793
+ ].join("\n");
1794
+ if (updateFile(join6(rulesDir, `${skill.name}.md`), content)) updated = true;
1795
+ }
1796
+ return updated;
1797
+ }
1798
+ var pendingCreatedFlag = false;
1799
+ function ensureSkills() {
1800
+ const cwd = process.cwd();
1801
+ const platforms = [];
1802
+ if (existsSync5(join6(cwd, ".claude"))) {
1803
+ if (ensureClaudeSkills(cwd)) platforms.push("Claude Code");
1804
+ }
1805
+ if (existsSync5(join6(cwd, ".cursor"))) {
1806
+ if (ensureCursorRules(cwd)) platforms.push("Cursor");
1807
+ }
1808
+ if (existsSync5(join6(cwd, ".github", "prompts")) || existsSync5(join6(cwd, ".github", "copilot-instructions.md"))) {
1809
+ if (ensureCopilotPrompts(cwd)) platforms.push("GitHub Copilot");
1810
+ }
1811
+ if (existsSync5(join6(cwd, ".windsurf"))) {
1812
+ if (ensureWindsurfRules(cwd)) platforms.push("Windsurf");
1813
+ }
1814
+ if (platforms.length > 0) {
1815
+ pendingCreatedFlag = true;
1816
+ }
1817
+ return platforms;
1818
+ }
1819
+ function consumeSkillsCreatedFlag() {
1820
+ const result = pendingCreatedFlag;
1821
+ pendingCreatedFlag = false;
1822
+ return result;
1823
+ }
1824
+
1273
1825
  // src/sync/markdown.ts
1826
+ import { writeFileSync as writeFileSync5 } from "fs";
1827
+ import { join as join7 } from "path";
1274
1828
  var dataDir;
1275
1829
  var port;
1276
1830
  var worklistTimeout = null;
@@ -1371,6 +1925,20 @@ async function syncWorklist() {
1371
1925
  sections.push("- If an API call fails (e.g. connection refused, error response), log a visible warning to the user and continue your work. Do NOT silently skip status updates.");
1372
1926
  sections.push('- Do NOT set tickets to "verified" \u2014 that status is reserved for human review.');
1373
1927
  sections.push("");
1928
+ sections.push("## Creating Tickets");
1929
+ sections.push("");
1930
+ sections.push("You can create new tickets directly via the API. Use this strategically to:");
1931
+ sections.push("- Break up complex tasks into smaller, trackable sub-tickets");
1932
+ sections.push("- Flag implementation decisions that need human review");
1933
+ sections.push("- Record bugs or issues discovered while working");
1934
+ sections.push("- Create follow-up tasks for items outside the current scope");
1935
+ sections.push("");
1936
+ sections.push("To create a ticket:");
1937
+ sections.push(` \`curl -s -X POST http://localhost:${port}/api/tickets -H "Content-Type: application/json" -d '{"title": "Title", "defaults": {"category": "bug|feature|task|issue|investigation|requirement_change", "up_next": false}}'\``);
1938
+ sections.push("");
1939
+ sections.push('You can also include `"details"` in the defaults object for longer descriptions.');
1940
+ sections.push("Set `up_next: true` only for items that should be prioritized immediately.");
1941
+ sections.push("");
1374
1942
  if (tickets.length === 0) {
1375
1943
  sections.push("No items in the Up Next list.");
1376
1944
  } else {
@@ -1387,7 +1955,7 @@ async function syncWorklist() {
1387
1955
  sections.push(formatCategoryDescriptions(categories));
1388
1956
  }
1389
1957
  sections.push("");
1390
- writeFileSync(join3(dataDir, "worklist.md"), sections.join("\n"), "utf-8");
1958
+ writeFileSync5(join7(dataDir, "worklist.md"), sections.join("\n"), "utf-8");
1391
1959
  } catch (err) {
1392
1960
  console.error("Failed to sync worklist.md:", err);
1393
1961
  }
@@ -1431,7 +1999,7 @@ async function syncOpenTickets() {
1431
1999
  sections.push(formatCategoryDescriptions(categories));
1432
2000
  }
1433
2001
  sections.push("");
1434
- writeFileSync(join3(dataDir, "open-tickets.md"), sections.join("\n"), "utf-8");
2002
+ writeFileSync5(join7(dataDir, "open-tickets.md"), sections.join("\n"), "utf-8");
1435
2003
  } catch (err) {
1436
2004
  console.error("Failed to sync open-tickets.md:", err);
1437
2005
  }
@@ -1518,7 +2086,7 @@ apiRoutes.delete("/tickets/:id/hard", async (c) => {
1518
2086
  const attachments = await getAttachments(id);
1519
2087
  for (const att of attachments) {
1520
2088
  try {
1521
- rmSync3(att.stored_path, { force: true });
2089
+ rmSync5(att.stored_path, { force: true });
1522
2090
  } catch {
1523
2091
  }
1524
2092
  }
@@ -1567,7 +2135,7 @@ apiRoutes.post("/trash/empty", async (c) => {
1567
2135
  const attachments = await getAttachments(ticket.id);
1568
2136
  for (const att of attachments) {
1569
2137
  try {
1570
- rmSync3(att.stored_path, { force: true });
2138
+ rmSync5(att.stored_path, { force: true });
1571
2139
  } catch {
1572
2140
  }
1573
2141
  }
@@ -1599,12 +2167,12 @@ apiRoutes.post("/tickets/:id/attachments", async (c) => {
1599
2167
  const ext = extname(originalName);
1600
2168
  const baseName = basename(originalName, ext);
1601
2169
  const storedName = `${ticket.ticket_number}_${baseName}${ext}`;
1602
- const attachDir = join4(dataDir2, "attachments");
1603
- mkdirSync2(attachDir, { recursive: true });
1604
- const storedPath = join4(attachDir, storedName);
2170
+ const attachDir = join8(dataDir2, "attachments");
2171
+ mkdirSync4(attachDir, { recursive: true });
2172
+ const storedPath = join8(attachDir, storedName);
1605
2173
  const buffer = Buffer.from(await file.arrayBuffer());
1606
- const { writeFileSync: writeFileSync4 } = await import("fs");
1607
- writeFileSync4(storedPath, buffer);
2174
+ const { writeFileSync: writeFileSync7 } = await import("fs");
2175
+ writeFileSync7(storedPath, buffer);
1608
2176
  const attachment = await addAttachment(id, originalName, storedPath);
1609
2177
  scheduleAllSync();
1610
2178
  notifyChange();
@@ -1615,7 +2183,7 @@ apiRoutes.delete("/attachments/:id", async (c) => {
1615
2183
  const attachment = await deleteAttachment(id);
1616
2184
  if (!attachment) return c.json({ error: "Not found" }, 404);
1617
2185
  try {
1618
- rmSync3(attachment.stored_path, { force: true });
2186
+ rmSync5(attachment.stored_path, { force: true });
1619
2187
  } catch {
1620
2188
  }
1621
2189
  scheduleAllSync();
@@ -1625,12 +2193,12 @@ apiRoutes.delete("/attachments/:id", async (c) => {
1625
2193
  apiRoutes.get("/attachments/file/*", async (c) => {
1626
2194
  const filePath = c.req.path.replace("/api/attachments/file/", "");
1627
2195
  const dataDir2 = c.get("dataDir");
1628
- const fullPath = join4(dataDir2, "attachments", filePath);
1629
- if (!existsSync2(fullPath)) {
2196
+ const fullPath = join8(dataDir2, "attachments", filePath);
2197
+ if (!existsSync6(fullPath)) {
1630
2198
  return c.json({ error: "File not found" }, 404);
1631
2199
  }
1632
- const { readFileSync: readFileSync4 } = await import("fs");
1633
- const content = readFileSync4(fullPath);
2200
+ const { readFileSync: readFileSync8 } = await import("fs");
2201
+ const content = readFileSync8(fullPath);
1634
2202
  const ext = extname(fullPath).toLowerCase();
1635
2203
  const mimeTypes = {
1636
2204
  ".png": "image/png",
@@ -1663,39 +2231,25 @@ apiRoutes.patch("/settings", async (c) => {
1663
2231
  }
1664
2232
  return c.json({ ok: true });
1665
2233
  });
2234
+ apiRoutes.get("/file-settings", async (c) => {
2235
+ const { readFileSettings: readFileSettings2 } = await Promise.resolve().then(() => (init_file_settings(), file_settings_exports));
2236
+ const dataDir2 = c.get("dataDir");
2237
+ return c.json(readFileSettings2(dataDir2));
2238
+ });
2239
+ apiRoutes.patch("/file-settings", async (c) => {
2240
+ const { writeFileSettings: writeFileSettings2 } = await Promise.resolve().then(() => (init_file_settings(), file_settings_exports));
2241
+ const dataDir2 = c.get("dataDir");
2242
+ const body = await c.req.json();
2243
+ const updated = writeFileSettings2(dataDir2, body);
2244
+ return c.json(updated);
2245
+ });
1666
2246
  apiRoutes.get("/worklist-info", (c) => {
1667
2247
  const dataDir2 = c.get("dataDir");
1668
2248
  const cwd = process.cwd();
1669
- const worklistRel = relative(cwd, join4(dataDir2, "worklist.md"));
2249
+ const worklistRel = relative2(cwd, join8(dataDir2, "worklist.md"));
1670
2250
  const prompt = `Read ${worklistRel} for current work items.`;
1671
- const claudeDir = join4(cwd, ".claude");
1672
- let skillCreated = false;
1673
- if (existsSync2(claudeDir)) {
1674
- const skillDir = join4(claudeDir, "skills", "hotsheet");
1675
- const skillFile = join4(skillDir, "SKILL.md");
1676
- if (!existsSync2(skillFile)) {
1677
- mkdirSync2(skillDir, { recursive: true });
1678
- const skillContent = [
1679
- "---",
1680
- "name: hotsheet",
1681
- "description: Read the Hot Sheet worklist and work through the current priority items",
1682
- "allowed-tools: Read, Grep, Glob, Edit, Write, Bash",
1683
- "---",
1684
- "",
1685
- `Read \`${worklistRel}\` and work through the tickets in priority order.`,
1686
- "",
1687
- "For each ticket:",
1688
- "1. Read the ticket details carefully",
1689
- "2. Implement the work described",
1690
- "3. When complete, mark it done via the Hot Sheet UI",
1691
- "",
1692
- "Work through them in order of priority, where reasonable.",
1693
- ""
1694
- ].join("\n");
1695
- writeFileSync2(skillFile, skillContent, "utf-8");
1696
- skillCreated = true;
1697
- }
1698
- }
2251
+ ensureSkills();
2252
+ const skillCreated = consumeSkillsCreatedFlag();
1699
2253
  return c.json({ prompt, skillCreated });
1700
2254
  });
1701
2255
  apiRoutes.get("/gitignore/status", async (c) => {
@@ -1710,8 +2264,58 @@ apiRoutes.post("/gitignore/add", async (c) => {
1710
2264
  return c.json({ ok: true });
1711
2265
  });
1712
2266
 
1713
- // src/routes/pages.tsx
2267
+ // src/routes/backups.ts
1714
2268
  import { Hono as Hono2 } from "hono";
2269
+ var backupRoutes = new Hono2();
2270
+ backupRoutes.get("/", (c) => {
2271
+ const dataDir2 = c.get("dataDir");
2272
+ const backups = listBackups(dataDir2);
2273
+ return c.json({ backups });
2274
+ });
2275
+ backupRoutes.post("/create", async (c) => {
2276
+ const dataDir2 = c.get("dataDir");
2277
+ const body = await c.req.json();
2278
+ const info = await createBackup(dataDir2, body.tier);
2279
+ if (!info) return c.json({ error: "Backup already in progress" }, 409);
2280
+ return c.json(info);
2281
+ });
2282
+ backupRoutes.post("/now", async (c) => {
2283
+ const dataDir2 = c.get("dataDir");
2284
+ const info = await triggerManualBackup(dataDir2);
2285
+ if (!info) return c.json({ error: "Backup already in progress" }, 409);
2286
+ return c.json(info);
2287
+ });
2288
+ backupRoutes.get("/preview/:tier/:filename", async (c) => {
2289
+ const dataDir2 = c.get("dataDir");
2290
+ const tier = c.req.param("tier");
2291
+ const filename = c.req.param("filename");
2292
+ try {
2293
+ const result = await loadBackupForPreview(dataDir2, tier, filename);
2294
+ return c.json(result);
2295
+ } catch (err) {
2296
+ const msg = err instanceof Error ? err.message : "Preview failed";
2297
+ return c.json({ error: msg }, 400);
2298
+ }
2299
+ });
2300
+ backupRoutes.post("/preview/cleanup", async (c) => {
2301
+ await cleanupPreview();
2302
+ return c.json({ ok: true });
2303
+ });
2304
+ backupRoutes.post("/restore", async (c) => {
2305
+ const dataDir2 = c.get("dataDir");
2306
+ const body = await c.req.json();
2307
+ try {
2308
+ await restoreBackup(dataDir2, body.tier, body.filename);
2309
+ scheduleAllSync();
2310
+ return c.json({ ok: true });
2311
+ } catch (err) {
2312
+ const msg = err instanceof Error ? err.message : "Restore failed";
2313
+ return c.json({ error: msg }, 500);
2314
+ }
2315
+ });
2316
+
2317
+ // src/routes/pages.tsx
2318
+ import { Hono as Hono3 } from "hono";
1715
2319
 
1716
2320
  // src/utils/escapeHtml.ts
1717
2321
  function escapeHtml(str) {
@@ -1799,7 +2403,7 @@ function Layout({ title, children }) {
1799
2403
  }
1800
2404
 
1801
2405
  // src/routes/pages.tsx
1802
- var pageRoutes = new Hono2();
2406
+ var pageRoutes = new Hono3();
1803
2407
  pageRoutes.get("/", (c) => {
1804
2408
  const html = /* @__PURE__ */ jsx(Layout, { title: "Hot Sheet", children: [
1805
2409
  /* @__PURE__ */ jsx("div", { className: "app", children: [
@@ -1825,6 +2429,24 @@ pageRoutes.get("/", (c) => {
1825
2429
  /* @__PURE__ */ jsx("button", { className: "settings-btn", id: "settings-btn", title: "Settings", children: raw("&#9881;") })
1826
2430
  ] })
1827
2431
  ] }),
2432
+ /* @__PURE__ */ jsx("div", { id: "backup-preview-banner", className: "backup-preview-banner", style: "display:none", children: [
2433
+ /* @__PURE__ */ jsx("span", { id: "backup-preview-label", children: "Previewing backup..." }),
2434
+ /* @__PURE__ */ jsx("div", { className: "backup-preview-actions", children: [
2435
+ /* @__PURE__ */ jsx("button", { id: "backup-restore-btn", className: "btn btn-sm btn-danger", children: "Restore This Backup" }),
2436
+ /* @__PURE__ */ jsx("button", { id: "backup-cancel-btn", className: "btn btn-sm", children: "Cancel Preview" })
2437
+ ] })
2438
+ ] }),
2439
+ /* @__PURE__ */ jsx("div", { id: "skills-banner", className: "skills-banner", style: "display:none", children: [
2440
+ /* @__PURE__ */ jsx("span", { children: "AI tool skills created. Restart your AI tool to use the new ticket creation skills (hs-bug, hs-feature, etc.)." }),
2441
+ /* @__PURE__ */ jsx("button", { id: "skills-banner-dismiss", className: "btn btn-sm", children: "Dismiss" })
2442
+ ] }),
2443
+ /* @__PURE__ */ jsx("div", { id: "update-banner", className: "update-banner", style: "display:none", children: [
2444
+ /* @__PURE__ */ jsx("span", { id: "update-banner-label", children: "Update available" }),
2445
+ /* @__PURE__ */ jsx("div", { className: "update-banner-actions", children: [
2446
+ /* @__PURE__ */ jsx("button", { id: "update-install-btn", className: "btn btn-sm btn-accent", children: "Install Update" }),
2447
+ /* @__PURE__ */ jsx("button", { id: "update-banner-dismiss", className: "btn btn-sm", children: "Later" })
2448
+ ] })
2449
+ ] }),
1828
2450
  /* @__PURE__ */ jsx("div", { className: "app-body", children: [
1829
2451
  /* @__PURE__ */ jsx("nav", { className: "sidebar", children: [
1830
2452
  /* @__PURE__ */ jsx("div", { className: "sidebar-copy-prompt", id: "copy-prompt-section", style: "display:none", children: /* @__PURE__ */ jsx("button", { className: "copy-prompt-btn", id: "copy-prompt-btn", title: "Copy worklist prompt to clipboard", children: [
@@ -2025,6 +2647,18 @@ pageRoutes.get("/", (c) => {
2025
2647
  /* @__PURE__ */ jsx("div", { className: "settings-field", children: [
2026
2648
  /* @__PURE__ */ jsx("label", { children: "Auto-clear verified after (days)" }),
2027
2649
  /* @__PURE__ */ jsx("input", { type: "number", id: "settings-verified-days", min: "1", value: "30" })
2650
+ ] }),
2651
+ /* @__PURE__ */ jsx("div", { className: "settings-section", children: [
2652
+ /* @__PURE__ */ jsx("div", { className: "settings-section-header", children: [
2653
+ /* @__PURE__ */ jsx("h3", { children: "Database Backups" }),
2654
+ /* @__PURE__ */ jsx("button", { className: "btn btn-sm", id: "backup-now-btn", children: "Backup Now" })
2655
+ ] }),
2656
+ /* @__PURE__ */ jsx("div", { className: "settings-field", children: [
2657
+ /* @__PURE__ */ jsx("label", { children: "Backup storage location" }),
2658
+ /* @__PURE__ */ jsx("input", { type: "text", id: "settings-backup-dir", placeholder: "Default: .hotsheet/backups" }),
2659
+ /* @__PURE__ */ jsx("span", { className: "settings-hint", id: "settings-backup-dir-hint", children: "Leave empty to use the default location inside the data directory." })
2660
+ ] }),
2661
+ /* @__PURE__ */ jsx("div", { id: "backup-list", className: "backup-list", children: "Loading backups..." })
2028
2662
  ] })
2029
2663
  ] })
2030
2664
  ] }) })
@@ -2045,22 +2679,23 @@ function tryServe(fetch, port2) {
2045
2679
  });
2046
2680
  }
2047
2681
  async function startServer(port2, dataDir2, options) {
2048
- const app = new Hono3();
2682
+ const app = new Hono4();
2049
2683
  app.use("*", async (c, next) => {
2050
2684
  c.set("dataDir", dataDir2);
2051
2685
  await next();
2052
2686
  });
2053
2687
  const selfDir = dirname(fileURLToPath(import.meta.url));
2054
- const distDir = existsSync3(join5(selfDir, "client", "styles.css")) ? join5(selfDir, "client") : join5(selfDir, "..", "dist", "client");
2688
+ const distDir = existsSync7(join9(selfDir, "client", "styles.css")) ? join9(selfDir, "client") : join9(selfDir, "..", "dist", "client");
2055
2689
  app.get("/static/styles.css", (c) => {
2056
- const css = readFileSync2(join5(distDir, "styles.css"), "utf-8");
2690
+ const css = readFileSync6(join9(distDir, "styles.css"), "utf-8");
2057
2691
  return c.text(css, 200, { "Content-Type": "text/css", "Cache-Control": "no-cache" });
2058
2692
  });
2059
2693
  app.get("/static/app.js", (c) => {
2060
- const js = readFileSync2(join5(distDir, "app.global.js"), "utf-8");
2694
+ const js = readFileSync6(join9(distDir, "app.global.js"), "utf-8");
2061
2695
  return c.text(js, 200, { "Content-Type": "application/javascript", "Cache-Control": "no-cache" });
2062
2696
  });
2063
2697
  app.route("/api", apiRoutes);
2698
+ app.route("/api/backups", backupRoutes);
2064
2699
  app.route("/", pageRoutes);
2065
2700
  let actualPort = port2;
2066
2701
  for (let attempt = 0; attempt < 20; attempt++) {
@@ -2098,18 +2733,18 @@ async function startServer(port2, dataDir2, options) {
2098
2733
  }
2099
2734
 
2100
2735
  // src/update-check.ts
2101
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
2736
+ import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
2102
2737
  import { get } from "https";
2103
2738
  import { homedir } from "os";
2104
- import { dirname as dirname2, join as join6 } from "path";
2739
+ import { dirname as dirname2, join as join10 } from "path";
2105
2740
  import { fileURLToPath as fileURLToPath2 } from "url";
2106
- var DATA_DIR = join6(homedir(), ".hotsheet");
2107
- var CHECK_FILE = join6(DATA_DIR, "last-update-check");
2741
+ var DATA_DIR = join10(homedir(), ".hotsheet");
2742
+ var CHECK_FILE = join10(DATA_DIR, "last-update-check");
2108
2743
  var PACKAGE_NAME = "hotsheet";
2109
2744
  function getCurrentVersion() {
2110
2745
  try {
2111
2746
  const dir = dirname2(fileURLToPath2(import.meta.url));
2112
- const pkg = JSON.parse(readFileSync3(join6(dir, "..", "package.json"), "utf-8"));
2747
+ const pkg = JSON.parse(readFileSync7(join10(dir, "..", "package.json"), "utf-8"));
2113
2748
  return pkg.version;
2114
2749
  } catch {
2115
2750
  return "0.0.0";
@@ -2117,16 +2752,16 @@ function getCurrentVersion() {
2117
2752
  }
2118
2753
  function getLastCheckDate() {
2119
2754
  try {
2120
- if (existsSync4(CHECK_FILE)) {
2121
- return readFileSync3(CHECK_FILE, "utf-8").trim();
2755
+ if (existsSync8(CHECK_FILE)) {
2756
+ return readFileSync7(CHECK_FILE, "utf-8").trim();
2122
2757
  }
2123
2758
  } catch {
2124
2759
  }
2125
2760
  return null;
2126
2761
  }
2127
2762
  function saveCheckDate() {
2128
- mkdirSync3(DATA_DIR, { recursive: true });
2129
- writeFileSync3(CHECK_FILE, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), "utf-8");
2763
+ mkdirSync5(DATA_DIR, { recursive: true });
2764
+ writeFileSync6(CHECK_FILE, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), "utf-8");
2130
2765
  }
2131
2766
  function isFirstUseToday() {
2132
2767
  const last = getLastCheckDate();
@@ -2231,7 +2866,7 @@ Examples:
2231
2866
  function parseArgs(argv) {
2232
2867
  const args = argv.slice(2);
2233
2868
  let port2 = 4174;
2234
- let dataDir2 = join7(process.cwd(), ".hotsheet");
2869
+ let dataDir2 = join11(process.cwd(), ".hotsheet");
2235
2870
  let demo = null;
2236
2871
  let forceUpdateCheck = false;
2237
2872
  let noOpen = false;
@@ -2298,13 +2933,14 @@ async function main() {
2298
2933
  }
2299
2934
  process.exit(1);
2300
2935
  }
2301
- dataDir2 = join7(tmpdir(), `hotsheet-demo-${demo}-${Date.now()}`);
2936
+ dataDir2 = join11(tmpdir(), `hotsheet-demo-${demo}-${Date.now()}`);
2302
2937
  console.log(`
2303
2938
  DEMO MODE: ${scenario.label}
2304
2939
  `);
2305
2940
  }
2306
- mkdirSync4(dataDir2, { recursive: true });
2941
+ mkdirSync6(dataDir2, { recursive: true });
2307
2942
  if (demo === null) {
2943
+ acquireLock(dataDir2);
2308
2944
  ensureGitignore(process.cwd());
2309
2945
  }
2310
2946
  setDataDir(dataDir2);
@@ -2319,6 +2955,16 @@ async function main() {
2319
2955
  const actualPort = await startServer(port2, dataDir2, { noOpen, strictPort });
2320
2956
  initMarkdownSync(dataDir2, actualPort);
2321
2957
  scheduleAllSync();
2958
+ initSkills(actualPort, dataDir2);
2959
+ const updatedPlatforms = ensureSkills();
2960
+ if (updatedPlatforms.length > 0) {
2961
+ console.log(`
2962
+ AI tool skills created/updated for: ${updatedPlatforms.join(", ")}`);
2963
+ console.log(" Restart your AI tool to pick up the new ticket creation skills.\n");
2964
+ }
2965
+ if (demo === null) {
2966
+ initBackupScheduler(dataDir2);
2967
+ }
2322
2968
  }
2323
2969
  main().catch((err) => {
2324
2970
  console.error(err);