shared-things-server 1.0.4 → 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 +188 -97
- package/package.json +7 -3
- package/scripts/postinstall.js +124 -0
package/dist/cli.js
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
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";
|
|
13
|
+
import updateNotifier from "update-notifier";
|
|
7
14
|
|
|
8
15
|
// src/db.ts
|
|
9
|
-
import Database from "better-sqlite3";
|
|
10
|
-
import * as path from "path";
|
|
11
|
-
import * as os from "os";
|
|
12
|
-
import * as fs from "fs";
|
|
13
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";
|
|
14
21
|
var DATA_DIR = process.env.DATA_DIR || path.join(os.homedir(), ".shared-things-server");
|
|
15
22
|
var DB_PATH = path.join(DATA_DIR, "data.db");
|
|
16
23
|
function initDatabase() {
|
|
@@ -277,14 +284,6 @@ function resetUserData(db, userId) {
|
|
|
277
284
|
};
|
|
278
285
|
}
|
|
279
286
|
|
|
280
|
-
// src/cli.ts
|
|
281
|
-
import * as fs2 from "fs";
|
|
282
|
-
import * as path2 from "path";
|
|
283
|
-
import * as os2 from "os";
|
|
284
|
-
import { spawn } from "child_process";
|
|
285
|
-
import Fastify from "fastify";
|
|
286
|
-
import cors from "@fastify/cors";
|
|
287
|
-
|
|
288
287
|
// src/auth.ts
|
|
289
288
|
function authMiddleware(db) {
|
|
290
289
|
return (request, reply, done) => {
|
|
@@ -293,7 +292,10 @@ function authMiddleware(db) {
|
|
|
293
292
|
}
|
|
294
293
|
const authHeader = request.headers.authorization;
|
|
295
294
|
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
296
|
-
reply.code(401).send({
|
|
295
|
+
reply.code(401).send({
|
|
296
|
+
error: "Missing or invalid authorization header",
|
|
297
|
+
code: "UNAUTHORIZED"
|
|
298
|
+
});
|
|
297
299
|
return;
|
|
298
300
|
}
|
|
299
301
|
const apiKey = authHeader.slice(7);
|
|
@@ -312,7 +314,7 @@ function registerRoutes(app, db) {
|
|
|
312
314
|
app.get("/health", async () => {
|
|
313
315
|
return { status: "ok", timestamp: (/* @__PURE__ */ new Date()).toISOString() };
|
|
314
316
|
});
|
|
315
|
-
app.get("/state", async (
|
|
317
|
+
app.get("/state", async (_request) => {
|
|
316
318
|
const headings = getAllHeadings(db);
|
|
317
319
|
const todos = getAllTodos(db);
|
|
318
320
|
return {
|
|
@@ -341,59 +343,73 @@ function registerRoutes(app, db) {
|
|
|
341
343
|
syncedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
342
344
|
};
|
|
343
345
|
});
|
|
344
|
-
app.post(
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
upsertHeading(db, heading.thingsId, heading.title, heading.position, userId);
|
|
354
|
-
}
|
|
355
|
-
for (const serverId of todos.deleted) {
|
|
356
|
-
deleteTodoByServerId(db, serverId, userId);
|
|
357
|
-
}
|
|
358
|
-
for (const todo of todos.upserted) {
|
|
359
|
-
let headingId = null;
|
|
360
|
-
if (todo.headingId) {
|
|
361
|
-
const headingRow = db.prepare(`SELECT id FROM headings WHERE things_id = ?`).get(todo.headingId);
|
|
362
|
-
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);
|
|
363
355
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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;
|
|
383
400
|
}
|
|
384
|
-
|
|
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
|
+
};
|
|
385
411
|
}
|
|
386
|
-
|
|
387
|
-
const currentTodos = getAllTodos(db);
|
|
388
|
-
return {
|
|
389
|
-
state: {
|
|
390
|
-
headings: currentHeadings,
|
|
391
|
-
todos: currentTodos,
|
|
392
|
-
syncedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
393
|
-
},
|
|
394
|
-
conflicts
|
|
395
|
-
};
|
|
396
|
-
});
|
|
412
|
+
);
|
|
397
413
|
app.delete("/reset", async (request) => {
|
|
398
414
|
const userId = request.user.id;
|
|
399
415
|
const result = resetUserData(db, userId);
|
|
@@ -408,7 +424,47 @@ function registerRoutes(app, db) {
|
|
|
408
424
|
}
|
|
409
425
|
|
|
410
426
|
// src/cli.ts
|
|
411
|
-
var pkg = JSON.parse(
|
|
427
|
+
var pkg = JSON.parse(
|
|
428
|
+
fs2.readFileSync(new URL("../package.json", import.meta.url), "utf-8")
|
|
429
|
+
);
|
|
430
|
+
var updateCheckInterval = 1e3 * 60 * 60;
|
|
431
|
+
var notifier = updateNotifier({ pkg, updateCheckInterval });
|
|
432
|
+
if (notifier.update) {
|
|
433
|
+
notifier.update.current = pkg.version;
|
|
434
|
+
if (notifier.update.current === notifier.update.latest) {
|
|
435
|
+
notifier.update = void 0;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
var lastCheck = notifier.config?.get("lastUpdateCheck") ?? 0;
|
|
439
|
+
var isFirstRun = Date.now() - lastCheck < 1e3;
|
|
440
|
+
var intervalPassed = Date.now() - lastCheck >= updateCheckInterval;
|
|
441
|
+
if (!notifier.update && (isFirstRun || intervalPassed)) {
|
|
442
|
+
try {
|
|
443
|
+
const update = await notifier.fetchInfo();
|
|
444
|
+
notifier.config?.set("lastUpdateCheck", Date.now());
|
|
445
|
+
if (update && update.type !== "latest") {
|
|
446
|
+
notifier.update = update;
|
|
447
|
+
}
|
|
448
|
+
} catch {
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
if (notifier.update && notifier.update.current !== notifier.update.latest) {
|
|
452
|
+
notifier.config?.set("update", notifier.update);
|
|
453
|
+
} else {
|
|
454
|
+
notifier.config?.delete("update");
|
|
455
|
+
}
|
|
456
|
+
process.on("exit", () => {
|
|
457
|
+
if (notifier.update && notifier.update.current !== notifier.update.latest) {
|
|
458
|
+
console.error(
|
|
459
|
+
chalk.yellow(
|
|
460
|
+
`
|
|
461
|
+
Update available: ${notifier.update.current} \u2192 ${notifier.update.latest}`
|
|
462
|
+
) + chalk.dim(`
|
|
463
|
+
Run: npm i -g ${pkg.name}
|
|
464
|
+
`)
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
});
|
|
412
468
|
var DATA_DIR2 = process.env.DATA_DIR || path2.join(os2.homedir(), ".shared-things-server");
|
|
413
469
|
var PID_FILE = path2.join(DATA_DIR2, "server.pid");
|
|
414
470
|
var LOG_FILE = path2.join(DATA_DIR2, "server.log");
|
|
@@ -439,9 +495,13 @@ program.command("start").description("Start the sync server").option("-p, --port
|
|
|
439
495
|
if (!isChildProcess) {
|
|
440
496
|
const status = isServerRunning();
|
|
441
497
|
if (status.running) {
|
|
442
|
-
console.log(
|
|
443
|
-
|
|
444
|
-
|
|
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
|
+
);
|
|
445
505
|
return;
|
|
446
506
|
}
|
|
447
507
|
}
|
|
@@ -449,11 +509,15 @@ program.command("start").description("Start the sync server").option("-p, --port
|
|
|
449
509
|
ensureDataDir();
|
|
450
510
|
const logFd = fs2.openSync(LOG_FILE, "a");
|
|
451
511
|
const scriptPath = process.argv[1];
|
|
452
|
-
const child = spawn(
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
+
);
|
|
457
521
|
fs2.writeFileSync(PID_FILE, String(child.pid));
|
|
458
522
|
child.unref();
|
|
459
523
|
fs2.closeSync(logFd);
|
|
@@ -462,13 +526,17 @@ program.command("start").description("Start the sync server").option("-p, --port
|
|
|
462
526
|
console.log(` ${chalk.dim("PID:")} ${child.pid}`);
|
|
463
527
|
console.log(` ${chalk.dim("URL:")} http://${HOST}:${PORT}`);
|
|
464
528
|
console.log(` ${chalk.dim("Logs:")} ${LOG_FILE}`);
|
|
465
|
-
console.log(
|
|
466
|
-
|
|
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
|
+
);
|
|
467
535
|
return;
|
|
468
536
|
}
|
|
469
537
|
const db = initDatabase();
|
|
470
538
|
const app = Fastify({
|
|
471
|
-
logger:
|
|
539
|
+
logger: true
|
|
472
540
|
});
|
|
473
541
|
await app.register(cors, {
|
|
474
542
|
origin: true
|
|
@@ -488,9 +556,11 @@ program.command("start").description("Start the sync server").option("-p, --port
|
|
|
488
556
|
try {
|
|
489
557
|
await app.listen({ port: PORT, host: HOST });
|
|
490
558
|
if (!process.env.SHARED_THINGS_DETACHED) {
|
|
491
|
-
console.log(
|
|
559
|
+
console.log(
|
|
560
|
+
chalk.green(`
|
|
492
561
|
\u2705 Server running at http://${HOST}:${PORT}
|
|
493
|
-
`)
|
|
562
|
+
`)
|
|
563
|
+
);
|
|
494
564
|
}
|
|
495
565
|
} catch (err) {
|
|
496
566
|
app.log.error(err);
|
|
@@ -522,24 +592,29 @@ program.command("stop").description("Stop the background server").action(() => {
|
|
|
522
592
|
program.command("status").description("Show server status").action(() => {
|
|
523
593
|
const status = isServerRunning();
|
|
524
594
|
console.log(chalk.bold("\n\u{1F4CA} Server Status\n"));
|
|
595
|
+
let versionLine = ` ${chalk.dim("Version:")} ${pkg.version}`;
|
|
596
|
+
if (notifier.update && notifier.update.current !== notifier.update.latest) {
|
|
597
|
+
versionLine += chalk.yellow(` \u2192 ${notifier.update.latest} available`);
|
|
598
|
+
}
|
|
599
|
+
console.log(versionLine);
|
|
525
600
|
if (status.running) {
|
|
526
|
-
console.log(` ${chalk.dim("Status:")}
|
|
527
|
-
console.log(` ${chalk.dim("PID:")}
|
|
601
|
+
console.log(` ${chalk.dim("Status:")} ${chalk.green("\u25CF running")}`);
|
|
602
|
+
console.log(` ${chalk.dim("PID:")} ${status.pid}`);
|
|
528
603
|
} else {
|
|
529
|
-
console.log(` ${chalk.dim("Status:")}
|
|
604
|
+
console.log(` ${chalk.dim("Status:")} ${chalk.red("\u25CB stopped")}`);
|
|
530
605
|
}
|
|
531
606
|
if (fs2.existsSync(LOG_FILE)) {
|
|
532
607
|
const stats = fs2.statSync(LOG_FILE);
|
|
533
608
|
const sizeKB = Math.round(stats.size / 1024);
|
|
534
|
-
console.log(` ${chalk.dim("Logs:")}
|
|
609
|
+
console.log(` ${chalk.dim("Logs:")} ${LOG_FILE} (${sizeKB}KB)`);
|
|
535
610
|
}
|
|
536
611
|
const dbPath = path2.join(DATA_DIR2, "data.db");
|
|
537
612
|
if (fs2.existsSync(dbPath)) {
|
|
538
613
|
const db = initDatabase();
|
|
539
614
|
const users = listUsers(db);
|
|
540
615
|
const todos = getAllTodos(db);
|
|
541
|
-
console.log(` ${chalk.dim("Users:")}
|
|
542
|
-
console.log(` ${chalk.dim("Todos:")}
|
|
616
|
+
console.log(` ${chalk.dim("Users:")} ${users.length}`);
|
|
617
|
+
console.log(` ${chalk.dim("Todos:")} ${todos.length}`);
|
|
543
618
|
}
|
|
544
619
|
console.log();
|
|
545
620
|
});
|
|
@@ -557,7 +632,9 @@ program.command("logs").description("Show server logs").option("-f, --follow", "
|
|
|
557
632
|
process.exit(0);
|
|
558
633
|
});
|
|
559
634
|
} else {
|
|
560
|
-
const tail = spawn("tail", ["-n", options.lines, LOG_FILE], {
|
|
635
|
+
const tail = spawn("tail", ["-n", options.lines, LOG_FILE], {
|
|
636
|
+
stdio: "inherit"
|
|
637
|
+
});
|
|
561
638
|
tail.on("close", () => process.exit(0));
|
|
562
639
|
}
|
|
563
640
|
});
|
|
@@ -570,7 +647,8 @@ program.command("create-user").description("Create a new user and generate API k
|
|
|
570
647
|
message: "Username",
|
|
571
648
|
validate: (value) => {
|
|
572
649
|
if (!value.trim()) return "Username is required";
|
|
573
|
-
if (userExists(db, value.trim()))
|
|
650
|
+
if (userExists(db, value.trim()))
|
|
651
|
+
return `User "${value.trim()}" already exists`;
|
|
574
652
|
return true;
|
|
575
653
|
}
|
|
576
654
|
});
|
|
@@ -586,14 +664,18 @@ program.command("create-user").description("Create a new user and generate API k
|
|
|
586
664
|
console.log(` ${chalk.dim("ID:")} ${id}`);
|
|
587
665
|
console.log(` ${chalk.dim("Name:")} ${name}`);
|
|
588
666
|
console.log(` ${chalk.dim("API Key:")} ${chalk.cyan(apiKey)}`);
|
|
589
|
-
console.log(
|
|
667
|
+
console.log(
|
|
668
|
+
chalk.yellow("\n\u26A0\uFE0F Save this API key - it cannot be retrieved later!\n")
|
|
669
|
+
);
|
|
590
670
|
});
|
|
591
671
|
program.command("list-users").description("List all users").action(async () => {
|
|
592
672
|
const db = initDatabase();
|
|
593
673
|
const users = listUsers(db);
|
|
594
674
|
if (users.length === 0) {
|
|
595
675
|
console.log(chalk.yellow("\nNo users found.\n"));
|
|
596
|
-
console.log(
|
|
676
|
+
console.log(
|
|
677
|
+
chalk.dim("Create a user with: shared-things-server create-user\n")
|
|
678
|
+
);
|
|
597
679
|
} else {
|
|
598
680
|
console.log(chalk.bold(`
|
|
599
681
|
\u{1F465} Users (${users.length})
|
|
@@ -624,7 +706,8 @@ program.command("delete-user").description("Delete a user").option("-n, --name <
|
|
|
624
706
|
message: "Username to delete",
|
|
625
707
|
validate: (value) => {
|
|
626
708
|
if (!value.trim()) return "Username is required";
|
|
627
|
-
if (!users.find((u) => u.name === value.trim()))
|
|
709
|
+
if (!users.find((u) => u.name === value.trim()))
|
|
710
|
+
return "User not found";
|
|
628
711
|
return true;
|
|
629
712
|
}
|
|
630
713
|
});
|
|
@@ -682,7 +765,7 @@ ${title}
|
|
|
682
765
|
const statusColor = todo.status === "completed" ? chalk.green : todo.status === "canceled" ? chalk.red : chalk.white;
|
|
683
766
|
console.log(` ${statusColor(statusIcon)} ${chalk.white(todo.title)}`);
|
|
684
767
|
if (todo.notes) {
|
|
685
|
-
const shortNotes = todo.notes.length > 50 ? todo.notes.substring(0, 50)
|
|
768
|
+
const shortNotes = todo.notes.length > 50 ? `${todo.notes.substring(0, 50)}...` : todo.notes;
|
|
686
769
|
console.log(` ${chalk.dim("Notes:")} ${shortNotes}`);
|
|
687
770
|
}
|
|
688
771
|
if (todo.dueDate) {
|
|
@@ -691,7 +774,9 @@ ${title}
|
|
|
691
774
|
if (todo.tags && todo.tags.length > 0) {
|
|
692
775
|
console.log(` ${chalk.dim("Tags:")} ${todo.tags.join(", ")}`);
|
|
693
776
|
}
|
|
694
|
-
console.log(
|
|
777
|
+
console.log(
|
|
778
|
+
` ${chalk.dim("Status:")} ${todo.status} ${chalk.dim("|")} ${chalk.dim("By:")} ${userName} ${chalk.dim("|")} ${todo.updatedAt}`
|
|
779
|
+
);
|
|
695
780
|
console.log();
|
|
696
781
|
}
|
|
697
782
|
});
|
|
@@ -718,7 +803,9 @@ program.command("reset").description("Delete all todos and headings (keeps users
|
|
|
718
803
|
db.prepare("DELETE FROM todos").run();
|
|
719
804
|
db.prepare("DELETE FROM headings").run();
|
|
720
805
|
db.prepare("DELETE FROM deleted_items").run();
|
|
721
|
-
console.log(
|
|
806
|
+
console.log(
|
|
807
|
+
chalk.green("\n\u2705 All todos and headings deleted. Users preserved.\n")
|
|
808
|
+
);
|
|
722
809
|
});
|
|
723
810
|
program.command("purge").description("Delete entire database (all data including users)").action(async () => {
|
|
724
811
|
const dataDir = process.env.DATA_DIR || path2.join(os2.homedir(), ".shared-things-server");
|
|
@@ -739,8 +826,12 @@ program.command("purge").description("Delete entire database (all data including
|
|
|
739
826
|
return;
|
|
740
827
|
}
|
|
741
828
|
fs2.unlinkSync(dbPath);
|
|
742
|
-
if (fs2.existsSync(dbPath
|
|
743
|
-
if (fs2.existsSync(dbPath
|
|
744
|
-
console.log(
|
|
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
|
+
);
|
|
745
836
|
});
|
|
746
837
|
program.parse();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shared-things-server",
|
|
3
|
-
"version": "1.0
|
|
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": [
|
|
@@ -42,7 +43,8 @@
|
|
|
42
43
|
"better-sqlite3": "^11.0.0",
|
|
43
44
|
"chalk": "^5.6.2",
|
|
44
45
|
"commander": "^12.0.0",
|
|
45
|
-
"fastify": "^4.26.0"
|
|
46
|
+
"fastify": "^4.26.0",
|
|
47
|
+
"update-notifier": "^7.0.0"
|
|
46
48
|
},
|
|
47
49
|
"devDependencies": {
|
|
48
50
|
"@types/better-sqlite3": "^7.6.8",
|
|
@@ -54,6 +56,8 @@
|
|
|
54
56
|
},
|
|
55
57
|
"scripts": {
|
|
56
58
|
"build": "tsup",
|
|
57
|
-
"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"
|
|
58
62
|
}
|
|
59
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
|
+
}
|