shared-things-server 1.0.5 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,17 +1,23 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { Command } from "commander";
5
- import { input, confirm } from "@inquirer/prompts";
4
+ import { spawn } from "child_process";
5
+ import * as fs2 from "fs";
6
+ import * as os2 from "os";
7
+ import * as path2 from "path";
8
+ import cors from "@fastify/cors";
9
+ import { confirm, input } from "@inquirer/prompts";
6
10
  import chalk from "chalk";
11
+ import { Command } from "commander";
12
+ import Fastify from "fastify";
7
13
  import updateNotifier from "update-notifier";
8
14
 
9
15
  // src/db.ts
10
- import Database from "better-sqlite3";
11
- import * as path from "path";
12
- import * as os from "os";
13
- import * as fs from "fs";
14
16
  import * as crypto from "crypto";
17
+ import * as fs from "fs";
18
+ import * as os from "os";
19
+ import * as path from "path";
20
+ import Database from "better-sqlite3";
15
21
  var DATA_DIR = process.env.DATA_DIR || path.join(os.homedir(), ".shared-things-server");
16
22
  var DB_PATH = path.join(DATA_DIR, "data.db");
17
23
  function initDatabase() {
@@ -278,14 +284,6 @@ function resetUserData(db, userId) {
278
284
  };
279
285
  }
280
286
 
281
- // src/cli.ts
282
- import * as fs2 from "fs";
283
- import * as path2 from "path";
284
- import * as os2 from "os";
285
- import { spawn } from "child_process";
286
- import Fastify from "fastify";
287
- import cors from "@fastify/cors";
288
-
289
287
  // src/auth.ts
290
288
  function authMiddleware(db) {
291
289
  return (request, reply, done) => {
@@ -294,7 +292,10 @@ function authMiddleware(db) {
294
292
  }
295
293
  const authHeader = request.headers.authorization;
296
294
  if (!authHeader || !authHeader.startsWith("Bearer ")) {
297
- reply.code(401).send({ error: "Missing or invalid authorization header", code: "UNAUTHORIZED" });
295
+ reply.code(401).send({
296
+ error: "Missing or invalid authorization header",
297
+ code: "UNAUTHORIZED"
298
+ });
298
299
  return;
299
300
  }
300
301
  const apiKey = authHeader.slice(7);
@@ -313,7 +314,7 @@ function registerRoutes(app, db) {
313
314
  app.get("/health", async () => {
314
315
  return { status: "ok", timestamp: (/* @__PURE__ */ new Date()).toISOString() };
315
316
  });
316
- app.get("/state", async (request) => {
317
+ app.get("/state", async (_request) => {
317
318
  const headings = getAllHeadings(db);
318
319
  const todos = getAllTodos(db);
319
320
  return {
@@ -342,59 +343,73 @@ function registerRoutes(app, db) {
342
343
  syncedAt: (/* @__PURE__ */ new Date()).toISOString()
343
344
  };
344
345
  });
345
- app.post("/push", async (request, reply) => {
346
- const { headings, todos } = request.body;
347
- const userId = request.user.id;
348
- const conflicts = [];
349
- try {
350
- for (const thingsId of headings.deleted) {
351
- deleteHeading(db, thingsId, userId);
352
- }
353
- for (const heading of headings.upserted) {
354
- upsertHeading(db, heading.thingsId, heading.title, heading.position, userId);
355
- }
356
- for (const serverId of todos.deleted) {
357
- deleteTodoByServerId(db, serverId, userId);
358
- }
359
- for (const todo of todos.upserted) {
360
- let headingId = null;
361
- if (todo.headingId) {
362
- const headingRow = db.prepare(`SELECT id FROM headings WHERE things_id = ?`).get(todo.headingId);
363
- headingId = headingRow?.id || null;
346
+ app.post(
347
+ "/push",
348
+ async (request, reply) => {
349
+ const { headings, todos } = request.body;
350
+ const userId = request.user.id;
351
+ const conflicts = [];
352
+ try {
353
+ for (const thingsId of headings.deleted) {
354
+ deleteHeading(db, thingsId, userId);
364
355
  }
365
- upsertTodoByServerId(db, todo.serverId, {
366
- thingsId: todo.thingsId,
367
- title: todo.title,
368
- notes: todo.notes,
369
- dueDate: todo.dueDate,
370
- tags: todo.tags,
371
- status: todo.status,
372
- headingId,
373
- position: todo.position
374
- }, userId);
375
- }
376
- } catch (err) {
377
- const error = err;
378
- if (error.message?.includes("UNIQUE constraint failed")) {
379
- reply.status(409);
380
- return {
381
- error: 'Sync conflict: Server has data that conflicts with your local state. Run "shared-things reset --server" to start fresh.',
382
- code: "SYNC_CONFLICT"
383
- };
356
+ for (const heading of headings.upserted) {
357
+ upsertHeading(
358
+ db,
359
+ heading.thingsId,
360
+ heading.title,
361
+ heading.position,
362
+ userId
363
+ );
364
+ }
365
+ for (const serverId of todos.deleted) {
366
+ deleteTodoByServerId(db, serverId, userId);
367
+ }
368
+ for (const todo of todos.upserted) {
369
+ let headingId = null;
370
+ if (todo.headingId) {
371
+ const headingRow = db.prepare(`SELECT id FROM headings WHERE things_id = ?`).get(todo.headingId);
372
+ headingId = headingRow?.id || null;
373
+ }
374
+ upsertTodoByServerId(
375
+ db,
376
+ todo.serverId,
377
+ {
378
+ thingsId: todo.thingsId,
379
+ title: todo.title,
380
+ notes: todo.notes,
381
+ dueDate: todo.dueDate,
382
+ tags: todo.tags,
383
+ status: todo.status,
384
+ headingId,
385
+ position: todo.position
386
+ },
387
+ userId
388
+ );
389
+ }
390
+ } catch (err) {
391
+ const error = err;
392
+ if (error.message?.includes("UNIQUE constraint failed")) {
393
+ reply.status(409);
394
+ return {
395
+ error: 'Sync conflict: Server has data that conflicts with your local state. Run "shared-things reset --server" to start fresh.',
396
+ code: "SYNC_CONFLICT"
397
+ };
398
+ }
399
+ throw err;
384
400
  }
385
- throw err;
401
+ const currentHeadings = getAllHeadings(db);
402
+ const currentTodos = getAllTodos(db);
403
+ return {
404
+ state: {
405
+ headings: currentHeadings,
406
+ todos: currentTodos,
407
+ syncedAt: (/* @__PURE__ */ new Date()).toISOString()
408
+ },
409
+ conflicts
410
+ };
386
411
  }
387
- const currentHeadings = getAllHeadings(db);
388
- const currentTodos = getAllTodos(db);
389
- return {
390
- state: {
391
- headings: currentHeadings,
392
- todos: currentTodos,
393
- syncedAt: (/* @__PURE__ */ new Date()).toISOString()
394
- },
395
- conflicts
396
- };
397
- });
412
+ );
398
413
  app.delete("/reset", async (request) => {
399
414
  const userId = request.user.id;
400
415
  const result = resetUserData(db, userId);
@@ -409,7 +424,9 @@ function registerRoutes(app, db) {
409
424
  }
410
425
 
411
426
  // src/cli.ts
412
- var pkg = JSON.parse(fs2.readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
427
+ var pkg = JSON.parse(
428
+ fs2.readFileSync(new URL("../package.json", import.meta.url), "utf-8")
429
+ );
413
430
  var updateCheckInterval = 1e3 * 60 * 60;
414
431
  var notifier = updateNotifier({ pkg, updateCheckInterval });
415
432
  if (notifier.update) {
@@ -439,8 +456,10 @@ if (notifier.update && notifier.update.current !== notifier.update.latest) {
439
456
  process.on("exit", () => {
440
457
  if (notifier.update && notifier.update.current !== notifier.update.latest) {
441
458
  console.error(
442
- chalk.yellow(`
443
- Update available: ${notifier.update.current} \u2192 ${notifier.update.latest}`) + chalk.dim(`
459
+ chalk.yellow(
460
+ `
461
+ Update available: ${notifier.update.current} \u2192 ${notifier.update.latest}`
462
+ ) + chalk.dim(`
444
463
  Run: npm i -g ${pkg.name}
445
464
  `)
446
465
  );
@@ -476,9 +495,13 @@ program.command("start").description("Start the sync server").option("-p, --port
476
495
  if (!isChildProcess) {
477
496
  const status = isServerRunning();
478
497
  if (status.running) {
479
- console.log(chalk.yellow(`
480
- \u26A0\uFE0F Server already running (PID: ${status.pid})`));
481
- console.log(chalk.dim('Use "shared-things-server stop" to stop it first.\n'));
498
+ console.log(
499
+ chalk.yellow(`
500
+ \u26A0\uFE0F Server already running (PID: ${status.pid})`)
501
+ );
502
+ console.log(
503
+ chalk.dim('Use "shared-things-server stop" to stop it first.\n')
504
+ );
482
505
  return;
483
506
  }
484
507
  }
@@ -486,11 +509,15 @@ program.command("start").description("Start the sync server").option("-p, --port
486
509
  ensureDataDir();
487
510
  const logFd = fs2.openSync(LOG_FILE, "a");
488
511
  const scriptPath = process.argv[1];
489
- const child = spawn(process.execPath, [scriptPath, "start", "--port", String(PORT), "--host", HOST], {
490
- detached: true,
491
- stdio: ["ignore", logFd, logFd],
492
- env: { ...process.env, SHARED_THINGS_DETACHED: "1" }
493
- });
512
+ const child = spawn(
513
+ process.execPath,
514
+ [scriptPath, "start", "--port", String(PORT), "--host", HOST],
515
+ {
516
+ detached: true,
517
+ stdio: ["ignore", logFd, logFd],
518
+ env: { ...process.env, SHARED_THINGS_DETACHED: "1" }
519
+ }
520
+ );
494
521
  fs2.writeFileSync(PID_FILE, String(child.pid));
495
522
  child.unref();
496
523
  fs2.closeSync(logFd);
@@ -499,13 +526,17 @@ program.command("start").description("Start the sync server").option("-p, --port
499
526
  console.log(` ${chalk.dim("PID:")} ${child.pid}`);
500
527
  console.log(` ${chalk.dim("URL:")} http://${HOST}:${PORT}`);
501
528
  console.log(` ${chalk.dim("Logs:")} ${LOG_FILE}`);
502
- console.log(chalk.dim('\nUse "shared-things-server logs -f" to follow logs'));
503
- console.log(chalk.dim('Use "shared-things-server stop" to stop the server\n'));
529
+ console.log(
530
+ chalk.dim('\nUse "shared-things-server logs -f" to follow logs')
531
+ );
532
+ console.log(
533
+ chalk.dim('Use "shared-things-server stop" to stop the server\n')
534
+ );
504
535
  return;
505
536
  }
506
537
  const db = initDatabase();
507
538
  const app = Fastify({
508
- logger: isChildProcess ? true : true
539
+ logger: true
509
540
  });
510
541
  await app.register(cors, {
511
542
  origin: true
@@ -525,9 +556,11 @@ program.command("start").description("Start the sync server").option("-p, --port
525
556
  try {
526
557
  await app.listen({ port: PORT, host: HOST });
527
558
  if (!process.env.SHARED_THINGS_DETACHED) {
528
- console.log(chalk.green(`
559
+ console.log(
560
+ chalk.green(`
529
561
  \u2705 Server running at http://${HOST}:${PORT}
530
- `));
562
+ `)
563
+ );
531
564
  }
532
565
  } catch (err) {
533
566
  app.log.error(err);
@@ -599,7 +632,9 @@ program.command("logs").description("Show server logs").option("-f, --follow", "
599
632
  process.exit(0);
600
633
  });
601
634
  } else {
602
- const tail = spawn("tail", ["-n", options.lines, LOG_FILE], { stdio: "inherit" });
635
+ const tail = spawn("tail", ["-n", options.lines, LOG_FILE], {
636
+ stdio: "inherit"
637
+ });
603
638
  tail.on("close", () => process.exit(0));
604
639
  }
605
640
  });
@@ -612,7 +647,8 @@ program.command("create-user").description("Create a new user and generate API k
612
647
  message: "Username",
613
648
  validate: (value) => {
614
649
  if (!value.trim()) return "Username is required";
615
- if (userExists(db, value.trim())) return `User "${value.trim()}" already exists`;
650
+ if (userExists(db, value.trim()))
651
+ return `User "${value.trim()}" already exists`;
616
652
  return true;
617
653
  }
618
654
  });
@@ -628,14 +664,18 @@ program.command("create-user").description("Create a new user and generate API k
628
664
  console.log(` ${chalk.dim("ID:")} ${id}`);
629
665
  console.log(` ${chalk.dim("Name:")} ${name}`);
630
666
  console.log(` ${chalk.dim("API Key:")} ${chalk.cyan(apiKey)}`);
631
- console.log(chalk.yellow("\n\u26A0\uFE0F Save this API key - it cannot be retrieved later!\n"));
667
+ console.log(
668
+ chalk.yellow("\n\u26A0\uFE0F Save this API key - it cannot be retrieved later!\n")
669
+ );
632
670
  });
633
671
  program.command("list-users").description("List all users").action(async () => {
634
672
  const db = initDatabase();
635
673
  const users = listUsers(db);
636
674
  if (users.length === 0) {
637
675
  console.log(chalk.yellow("\nNo users found.\n"));
638
- console.log(chalk.dim("Create a user with: shared-things-server create-user\n"));
676
+ console.log(
677
+ chalk.dim("Create a user with: shared-things-server create-user\n")
678
+ );
639
679
  } else {
640
680
  console.log(chalk.bold(`
641
681
  \u{1F465} Users (${users.length})
@@ -666,7 +706,8 @@ program.command("delete-user").description("Delete a user").option("-n, --name <
666
706
  message: "Username to delete",
667
707
  validate: (value) => {
668
708
  if (!value.trim()) return "Username is required";
669
- if (!users.find((u) => u.name === value.trim())) return "User not found";
709
+ if (!users.find((u) => u.name === value.trim()))
710
+ return "User not found";
670
711
  return true;
671
712
  }
672
713
  });
@@ -724,7 +765,7 @@ ${title}
724
765
  const statusColor = todo.status === "completed" ? chalk.green : todo.status === "canceled" ? chalk.red : chalk.white;
725
766
  console.log(` ${statusColor(statusIcon)} ${chalk.white(todo.title)}`);
726
767
  if (todo.notes) {
727
- const shortNotes = todo.notes.length > 50 ? todo.notes.substring(0, 50) + "..." : todo.notes;
768
+ const shortNotes = todo.notes.length > 50 ? `${todo.notes.substring(0, 50)}...` : todo.notes;
728
769
  console.log(` ${chalk.dim("Notes:")} ${shortNotes}`);
729
770
  }
730
771
  if (todo.dueDate) {
@@ -733,7 +774,9 @@ ${title}
733
774
  if (todo.tags && todo.tags.length > 0) {
734
775
  console.log(` ${chalk.dim("Tags:")} ${todo.tags.join(", ")}`);
735
776
  }
736
- console.log(` ${chalk.dim("Status:")} ${todo.status} ${chalk.dim("|")} ${chalk.dim("By:")} ${userName} ${chalk.dim("|")} ${todo.updatedAt}`);
777
+ console.log(
778
+ ` ${chalk.dim("Status:")} ${todo.status} ${chalk.dim("|")} ${chalk.dim("By:")} ${userName} ${chalk.dim("|")} ${todo.updatedAt}`
779
+ );
737
780
  console.log();
738
781
  }
739
782
  });
@@ -760,7 +803,9 @@ program.command("reset").description("Delete all todos and headings (keeps users
760
803
  db.prepare("DELETE FROM todos").run();
761
804
  db.prepare("DELETE FROM headings").run();
762
805
  db.prepare("DELETE FROM deleted_items").run();
763
- console.log(chalk.green("\n\u2705 All todos and headings deleted. Users preserved.\n"));
806
+ console.log(
807
+ chalk.green("\n\u2705 All todos and headings deleted. Users preserved.\n")
808
+ );
764
809
  });
765
810
  program.command("purge").description("Delete entire database (all data including users)").action(async () => {
766
811
  const dataDir = process.env.DATA_DIR || path2.join(os2.homedir(), ".shared-things-server");
@@ -781,8 +826,12 @@ program.command("purge").description("Delete entire database (all data including
781
826
  return;
782
827
  }
783
828
  fs2.unlinkSync(dbPath);
784
- if (fs2.existsSync(dbPath + "-wal")) fs2.unlinkSync(dbPath + "-wal");
785
- if (fs2.existsSync(dbPath + "-shm")) fs2.unlinkSync(dbPath + "-shm");
786
- console.log(chalk.green('\n\u2705 Database deleted. Run "shared-things-server create-user" to start fresh.\n'));
829
+ if (fs2.existsSync(`${dbPath}-wal`)) fs2.unlinkSync(`${dbPath}-wal`);
830
+ if (fs2.existsSync(`${dbPath}-shm`)) fs2.unlinkSync(`${dbPath}-shm`);
831
+ console.log(
832
+ chalk.green(
833
+ '\n\u2705 Database deleted. Run "shared-things-server create-user" to start fresh.\n'
834
+ )
835
+ );
787
836
  });
788
837
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shared-things-server",
3
- "version": "1.0.5",
3
+ "version": "1.1.0",
4
4
  "description": "Sync server for Things 3 projects",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -9,6 +9,7 @@
9
9
  },
10
10
  "files": [
11
11
  "dist",
12
+ "scripts",
12
13
  "README.md"
13
14
  ],
14
15
  "keywords": [
@@ -55,6 +56,8 @@
55
56
  },
56
57
  "scripts": {
57
58
  "build": "tsup",
58
- "dev": "tsx watch src/cli.ts start"
59
+ "dev": "tsx watch src/cli.ts start",
60
+ "typecheck": "tsc --noEmit",
61
+ "postinstall": "node scripts/postinstall.js || true"
59
62
  }
60
63
  }
@@ -0,0 +1,124 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Postinstall script: Automatically restart the server if it's running.
5
+ * This ensures users get the new version after `npm update -g shared-things-server`.
6
+ *
7
+ * Safe behaviors:
8
+ * - Silent success if server is not running
9
+ * - Never fails npm install (catches all errors)
10
+ * - Works on any platform (Linux, macOS)
11
+ */
12
+
13
+ import { execSync } from "node:child_process";
14
+ import * as fs from "node:fs";
15
+ import * as os from "node:os";
16
+ import * as path from "node:path";
17
+
18
+ const DATA_DIR =
19
+ process.env.DATA_DIR || path.join(os.homedir(), ".shared-things-server");
20
+ const PID_FILE = path.join(DATA_DIR, "server.pid");
21
+
22
+ function isServerRunning() {
23
+ if (!fs.existsSync(PID_FILE)) {
24
+ return { running: false };
25
+ }
26
+
27
+ const pid = parseInt(fs.readFileSync(PID_FILE, "utf-8").trim(), 10);
28
+
29
+ try {
30
+ // Check if process exists (signal 0 doesn't kill, just checks)
31
+ process.kill(pid, 0);
32
+ return { running: true, pid };
33
+ } catch {
34
+ // Process doesn't exist, clean up stale PID file
35
+ try {
36
+ fs.unlinkSync(PID_FILE);
37
+ } catch {
38
+ // Ignore cleanup errors
39
+ }
40
+ return { running: false };
41
+ }
42
+ }
43
+
44
+ function stopServer(pid) {
45
+ try {
46
+ process.kill(pid, "SIGTERM");
47
+ // Wait for graceful shutdown
48
+ let attempts = 0;
49
+ while (attempts < 10) {
50
+ try {
51
+ process.kill(pid, 0);
52
+ // Still running, wait
53
+ execSync("sleep 0.2", { stdio: "pipe" });
54
+ attempts++;
55
+ } catch {
56
+ // Process stopped
57
+ break;
58
+ }
59
+ }
60
+ // Clean up PID file if still exists
61
+ if (fs.existsSync(PID_FILE)) {
62
+ fs.unlinkSync(PID_FILE);
63
+ }
64
+ return true;
65
+ } catch {
66
+ return false;
67
+ }
68
+ }
69
+
70
+ function startServer() {
71
+ // Verify the command exists
72
+ try {
73
+ execSync("which shared-things-server", { stdio: "pipe" });
74
+ } catch {
75
+ // Command not in PATH, can't start
76
+ return false;
77
+ }
78
+
79
+ // Use the CLI's built-in detach mode (-d flag)
80
+ // This properly handles daemonization and PID file management
81
+ try {
82
+ execSync("shared-things-server start -d", {
83
+ stdio: "pipe",
84
+ timeout: 10000,
85
+ });
86
+ return true;
87
+ } catch {
88
+ return false;
89
+ }
90
+ }
91
+
92
+ function main() {
93
+ const status = isServerRunning();
94
+
95
+ if (!status.running) {
96
+ // Server not running, nothing to restart
97
+ return;
98
+ }
99
+
100
+ // Stop the old server
101
+ const stopped = stopServer(status.pid);
102
+ if (!stopped) {
103
+ console.log(
104
+ ' ℹ️ Could not auto-restart server. Run "shared-things-server stop && shared-things-server start -d" manually.',
105
+ );
106
+ return;
107
+ }
108
+
109
+ // Start with new version
110
+ const started = startServer();
111
+ if (started) {
112
+ console.log(" ✅ Server restarted with new version");
113
+ } else {
114
+ console.log(
115
+ ' ℹ️ Server stopped. Run "shared-things-server start -d" to start it again.',
116
+ );
117
+ }
118
+ }
119
+
120
+ try {
121
+ main();
122
+ } catch {
123
+ // Never fail npm install
124
+ }