jobtrack 1.0.10 → 1.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,13 +10,22 @@ road with the same company. Single user, runs on your machine, data in a local S
10
10
 
11
11
  ## Install
12
12
 
13
+ **On Windows**, prefer the installer:
14
+ [**JobTrack-Setup**](https://github.com/CuplexUser/JobTrack/releases/latest). It needs no admin
15
+ rights and no Node.js, adds a native tray icon and a settings dialog, and starts silently at
16
+ sign-in rather than opening a console window.
17
+
18
+ Everywhere else — and for development on Windows too:
19
+
13
20
  ```bash
14
21
  npm install -g jobtrack
15
22
  jobtrack
16
23
  ```
17
24
 
18
- Requires **Node.js 24+** already installed on the machine this runs the app's TypeScript
19
- sources via `tsx` rather than shipping a compiled, dependency-free binary.
25
+ That route requires **Node.js 24+** already installed on the machine, since it runs the app's
26
+ TypeScript sources via `tsx` rather than shipping a compiled, dependency-free binary. Both routes
27
+ use the same `%APPDATA%\jobtrack` data directory, so they share one database — install only one of
28
+ them at a time, or the two will collide on port 3001.
20
29
 
21
30
  ## What you get
22
31
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jobtrack",
3
- "version": "1.0.10",
3
+ "version": "1.0.11",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Runs the JobTrack API + web UI as one background process with a Windows tray icon",
@@ -10,6 +10,9 @@
10
10
  "directory": "apps/tray"
11
11
  },
12
12
  "homepage": "https://github.com/CuplexUser/JobTrack#readme",
13
+ "engines": {
14
+ "node": ">=24.0.0"
15
+ },
13
16
  "main": "./src/cli.ts",
14
17
  "bin": {
15
18
  "jobtrack": "./bin/jobtrack.js"
package/src/cli.ts CHANGED
@@ -4,19 +4,55 @@
4
4
  * Starts the API + web UI as one process (server.ts) and, on Windows, shows a tray icon for
5
5
  * opening the UI, toggling autostart, and opening .env (tray.ts). Elsewhere it just runs
6
6
  * headless — Ctrl+C to stop — until a native tray for those platforms is worth building.
7
+ *
8
+ * The Windows installer (see `windows/`) is the one other caller: it draws its own native tray
9
+ * and supervises this process, so it passes `--no-tray` and `--home`, reads the JOBTRACK_READY
10
+ * line below off stdout, and asks for a clean stop by writing `quit` to stdin. None of that
11
+ * changes what plain `jobtrack` does.
7
12
  */
13
+ import { resolve } from 'node:path';
8
14
  import { startServer } from './server.js';
9
- import { createTray } from './tray.js';
10
15
  import { isAutostartEnabled, enableAutostart, disableAutostart } from './autostart.js';
11
16
  import { openSettingsFile } from './settings.js';
12
17
  import { openUrl } from './os.js';
13
18
  import { APP_VERSION } from './version.js';
14
19
 
20
+ const argv = process.argv.slice(2);
21
+
22
+ function flagValue(name: string): string | undefined {
23
+ const index = argv.indexOf(name);
24
+ return index === -1 ? undefined : argv[index + 1];
25
+ }
26
+
27
+ // Before startServer(), and that ordering is the whole point: `resolveAppDataDir()` in
28
+ // apps/api/src/config.ts reads process.env when it's called, and everything under the data
29
+ // directory — the database, the model cache, .env itself — hangs off what it returns.
30
+ //
31
+ // A flag rather than an inherited JOBTRACK_HOME because bin/jobtrack.js documents that handing
32
+ // a reconstructed environment to a Node child breaks tsx's loader; a supervisor that never has
33
+ // to set an environment variable can't trip over that at all.
34
+ const home = flagValue('--home');
35
+ if (home) process.env.JOBTRACK_HOME = resolve(home);
36
+
37
+ const noTray = argv.includes('--no-tray') || process.env.JOBTRACK_NO_TRAY === '1';
38
+
15
39
  const { app, config, repos, search } = await startServer();
16
40
 
17
41
  const url = `http://${config.host === '0.0.0.0' ? '127.0.0.1' : config.host}:${config.port}`;
18
42
  console.log(`JobTrack v${APP_VERSION} running at ${url} (driver: ${config.driver})`);
19
43
 
44
+ // The same facts again, in one parseable line, so a supervisor knows the server is up and on
45
+ // which address without scraping prose or guessing at a port it may have only set in .env.
46
+ console.log(
47
+ `JOBTRACK_READY ${JSON.stringify({
48
+ url,
49
+ host: config.host,
50
+ port: config.port,
51
+ version: APP_VERSION,
52
+ driver: config.driver,
53
+ })}`,
54
+ );
55
+
20
56
  let shuttingDown = false;
21
57
  async function shutdown(): Promise<void> {
22
58
  if (shuttingDown) return;
@@ -31,7 +67,36 @@ for (const signal of ['SIGINT', 'SIGTERM'] as const) {
31
67
  process.once(signal, () => void shutdown());
32
68
  }
33
69
 
34
- if (process.platform === 'win32') {
70
+ /**
71
+ * Line-oriented commands from a supervising process. Windows has no dependable way to send a
72
+ * running process anything like SIGTERM from outside, so stdin is how the installer's host asks
73
+ * for the orderly shutdown above — closing SQLite properly — before it resorts to killing the
74
+ * job object.
75
+ */
76
+ function listenForSupervisorCommands(): void {
77
+ process.stdin.setEncoding('utf8');
78
+ let buffered = '';
79
+ process.stdin.on('data', (chunk: string) => {
80
+ buffered += chunk;
81
+ let newline: number;
82
+ while ((newline = buffered.indexOf('\n')) !== -1) {
83
+ const line = buffered.slice(0, newline).trim();
84
+ buffered = buffered.slice(newline + 1);
85
+ if (line === 'quit') void shutdown();
86
+ }
87
+ });
88
+ // The supervisor is gone; there is nobody left to serve.
89
+ process.stdin.on('end', () => void shutdown());
90
+ }
91
+
92
+ if (noTray) {
93
+ listenForSupervisorCommands();
94
+ } else if (process.platform === 'win32') {
95
+ // Imported here rather than at the top so that `systray` — 35 MB of Go tray binaries for
96
+ // three platforms — is only ever loaded when a tray is actually going to be drawn. That is
97
+ // what lets the Windows installer's payload delete the package outright: its native host
98
+ // draws the tray itself and always passes --no-tray.
99
+ const { createTray } = await import('./tray.js');
35
100
  const tray = createTray({
36
101
  autostartEnabled: isAutostartEnabled(),
37
102
  onOpen: () => openUrl(url),
package/src/server.ts CHANGED
@@ -17,6 +17,12 @@ import { TransformersEmbedder } from '@jobtrack/api/search/transformers-embedder
17
17
  import { resolveWebDist } from './assets.js';
18
18
  import { APP_PACKAGE } from './version.js';
19
19
 
20
+ /**
21
+ * Exit code for "the port is taken". Distinct from a generic crash so a supervisor can tell a
22
+ * misconfiguration it should surface to the user from a fault it should just retry.
23
+ */
24
+ export const EXIT_PORT_IN_USE = 3;
25
+
20
26
  export interface RunningServer {
21
27
  app: FastifyInstance;
22
28
  config: Config;
@@ -68,7 +74,25 @@ export async function startServer(): Promise<RunningServer> {
68
74
  }
69
75
 
70
76
  await search.start();
71
- await app.listen({ host: config.host, port: config.port });
77
+
78
+ try {
79
+ await app.listen({ host: config.host, port: config.port });
80
+ } catch (error) {
81
+ // Overwhelmingly this is a second JobTrack — autostart plus a manual launch. Left alone it
82
+ // surfaces as an unhandled rejection and a stack trace; a supervisor (and a person) can do
83
+ // something useful with a named cause and a distinct exit code.
84
+ if ((error as NodeJS.ErrnoException).code !== 'EADDRINUSE') throw error;
85
+ console.error(
86
+ `JOBTRACK_ERROR ${JSON.stringify({ code: 'EADDRINUSE', host: config.host, port: config.port })}`,
87
+ );
88
+ console.error(
89
+ `Port ${config.port} is already in use — JobTrack may already be running. Stop it, or set PORT in .env to something else.`,
90
+ );
91
+ search.stop();
92
+ await app.close();
93
+ await repos.close();
94
+ process.exit(EXIT_PORT_IN_USE);
95
+ }
72
96
 
73
97
  return { app, config, repos, search };
74
98
  }