stikfix 1.2.0 → 1.3.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/host/src/bootstrap/register.js +58 -0
- package/dist/host/src/index.js +36 -0
- package/dist/host/src/native-host.js +115 -7
- package/dist/host/src/tray.js +230 -0
- package/dist/host/stikfix-init.cjs +96 -3
- package/dist/host/stikfix-native.cjs +84 -14
- package/package.json +1 -1
|
@@ -405,6 +405,64 @@ export function createLauncherFiles(opts) {
|
|
|
405
405
|
}
|
|
406
406
|
return { written, warnings };
|
|
407
407
|
}
|
|
408
|
+
// ---------------------------------------------------------------------------
|
|
409
|
+
// registerStartup / unregisterStartup — HKCU Run entry (Windows startup autoload)
|
|
410
|
+
// ---------------------------------------------------------------------------
|
|
411
|
+
// HKCU Run key + value name for the "start host on Windows login" feature.
|
|
412
|
+
// HKCU (Pitfall 5 — never HKLM); the value data launches the existing hidden
|
|
413
|
+
// VBS launcher via wscript.exe so no console window ever appears.
|
|
414
|
+
const REG_RUN_KEY = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run';
|
|
415
|
+
const REG_RUN_VALUE = 'stikfix-host';
|
|
416
|
+
/**
|
|
417
|
+
* Register the stikfix host to start automatically on Windows login by adding
|
|
418
|
+
* an HKCU Run entry that launches the existing hidden VBS launcher (created by
|
|
419
|
+
* createLauncherFiles) via wscript.exe — so no console window is shown.
|
|
420
|
+
*
|
|
421
|
+
* Value data: `wscript.exe "<abs vbsPath>"` — reuses the same VBS path that
|
|
422
|
+
* createLauncherFiles/getLauncherPaths compute (LAUNCHER_VBS_FILENAME in the
|
|
423
|
+
* stikfix data dir).
|
|
424
|
+
*
|
|
425
|
+
* Non-win32: no-op (startup autoload is Windows-only for now).
|
|
426
|
+
* execFileSync is used (NEVER exec, NEVER shell) — mirrors registerNativeHost.
|
|
427
|
+
*/
|
|
428
|
+
export function registerStartup(opts = {}) {
|
|
429
|
+
const plat = opts.plat ?? process.platform;
|
|
430
|
+
if (plat !== 'win32') {
|
|
431
|
+
// Startup autoload is Windows-only for now — no-op on other platforms.
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
const home = opts.home ?? homedir();
|
|
435
|
+
const vbsPath = join(launcherDir(plat, home), LAUNCHER_VBS_FILENAME);
|
|
436
|
+
const wscriptPath = join(process.env['SystemRoot'] ?? 'C:\\Windows', 'System32', 'wscript.exe');
|
|
437
|
+
// Value data: wscript.exe "<abs vbsPath>" — the VBS launches the host hidden.
|
|
438
|
+
const runData = `"${wscriptPath}" "${vbsPath}"`;
|
|
439
|
+
const execReg = opts.execReg ?? ((args) => {
|
|
440
|
+
execFileSync('reg', args);
|
|
441
|
+
});
|
|
442
|
+
// /f overwrites any existing value without prompting (idempotent re-register).
|
|
443
|
+
execReg(['ADD', REG_RUN_KEY, '/v', REG_RUN_VALUE, '/t', 'REG_SZ', '/d', runData, '/f']);
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Remove the HKCU Run entry created by registerStartup. Idempotent: does not
|
|
447
|
+
* throw if the value is already absent (reg DELETE /f tolerates a missing value;
|
|
448
|
+
* a non-zero exit is swallowed). Non-win32: no-op.
|
|
449
|
+
*/
|
|
450
|
+
export function unregisterStartup(opts = {}) {
|
|
451
|
+
const plat = opts.plat ?? process.platform;
|
|
452
|
+
if (plat !== 'win32') {
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
const execReg = opts.execReg ?? ((args) => {
|
|
456
|
+
execFileSync('reg', args);
|
|
457
|
+
});
|
|
458
|
+
// /f suppresses the confirmation prompt and tolerates an absent value.
|
|
459
|
+
try {
|
|
460
|
+
execReg(['DELETE', REG_RUN_KEY, '/v', REG_RUN_VALUE, '/f']);
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
// Value may not exist — ignore (idempotent removal).
|
|
464
|
+
}
|
|
465
|
+
}
|
|
408
466
|
/**
|
|
409
467
|
* Write the native-messaging manifest and, on Windows, register the matching
|
|
410
468
|
* HKCU registry key(s):
|
package/dist/host/src/index.js
CHANGED
|
@@ -16,6 +16,7 @@ import { resolveConfig, resolveConfigValues, ensureNotesDir, writeTokenFile } fr
|
|
|
16
16
|
import { createHostServer } from './server.js';
|
|
17
17
|
import { bindServer, BIND_HOST } from './bind.js';
|
|
18
18
|
import { probeExistingHost } from './probe.js';
|
|
19
|
+
import { startTray } from './tray.js';
|
|
19
20
|
// ---------------------------------------------------------------------------
|
|
20
21
|
// CLI parsing (preserve Phase 1 stub options block — HOST-13)
|
|
21
22
|
// ---------------------------------------------------------------------------
|
|
@@ -107,4 +108,39 @@ console.log(JSON.stringify({
|
|
|
107
108
|
notesDir: cfg.notesDir,
|
|
108
109
|
origins: cfg.origins,
|
|
109
110
|
}));
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// Windows system-tray indicator (best-effort, cosmetic, win32-only)
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// Only reached on genuine startup — the single-instance guard above exits(0)
|
|
115
|
+
// before here, so a second instance never spawns a duplicate tray.
|
|
116
|
+
const tray = startTray({
|
|
117
|
+
port: boundPort,
|
|
118
|
+
root: cfg.root,
|
|
119
|
+
name: cfg.name,
|
|
120
|
+
notesDir: cfg.notesDir,
|
|
121
|
+
hostPid: process.pid,
|
|
122
|
+
});
|
|
123
|
+
// Kill the tray child when the host shuts down so a dead host has no tray.
|
|
124
|
+
// (No prior signal handling existed here; these are additive.)
|
|
125
|
+
let shuttingDown = false;
|
|
126
|
+
function shutdown(signal) {
|
|
127
|
+
if (shuttingDown)
|
|
128
|
+
return;
|
|
129
|
+
shuttingDown = true;
|
|
130
|
+
try {
|
|
131
|
+
tray?.kill();
|
|
132
|
+
}
|
|
133
|
+
catch { /* best-effort */ }
|
|
134
|
+
server.close(() => process.exit(0));
|
|
135
|
+
// Fallback: exit even if server.close hangs on lingering connections.
|
|
136
|
+
setTimeout(() => process.exit(0), 1000).unref();
|
|
137
|
+
void signal;
|
|
138
|
+
}
|
|
139
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
140
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
141
|
+
// Belt-and-suspenders: also reap the tray on plain process exit.
|
|
142
|
+
process.on('exit', () => { try {
|
|
143
|
+
tray?.kill();
|
|
144
|
+
}
|
|
145
|
+
catch { /* best-effort */ } });
|
|
110
146
|
// Server runs indefinitely (Pitfall 2 — no process.exit here)
|
|
@@ -14,9 +14,10 @@
|
|
|
14
14
|
*
|
|
15
15
|
* Node builtins only — no WXT, no Chrome imports.
|
|
16
16
|
*/
|
|
17
|
-
import { readFileSync } from 'node:fs';
|
|
18
|
-
import { join } from 'node:path';
|
|
17
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
18
|
+
import { join, basename, resolve } from 'node:path';
|
|
19
19
|
import { homedir } from 'node:os';
|
|
20
|
+
import { spawn } from 'node:child_process';
|
|
20
21
|
import { sendNativeMessage, readNativeMessages } from './native-msg.js';
|
|
21
22
|
import { pickFolder } from './folder-picker.js';
|
|
22
23
|
import { validateChosenFolder } from './validate-folder.js';
|
|
@@ -27,6 +28,74 @@ export { validateChosenFolder };
|
|
|
27
28
|
// Config + token/port resolution
|
|
28
29
|
// ---------------------------------------------------------------------------
|
|
29
30
|
const CONFIG_PATH = join(homedir(), '.config', 'stikfix', 'config.json');
|
|
31
|
+
/**
|
|
32
|
+
* Resolve the absolute path to the HTTP host entry bundle. Prefers the
|
|
33
|
+
* `hostEntry` field written into config.json by `npx stikfix init`. If absent
|
|
34
|
+
* (older config), falls back to resolving `src/index.js` relative to THIS
|
|
35
|
+
* native host bundle's own directory — the shipped layout is
|
|
36
|
+
* dist/host/stikfix-native.cjs alongside dist/host/src/index.js, so __dirname/src/index.js
|
|
37
|
+
* is the co-located HTTP host entry.
|
|
38
|
+
*/
|
|
39
|
+
function resolveHostEntry(cfg) {
|
|
40
|
+
if (typeof cfg.hostEntry === 'string' && cfg.hostEntry.length > 0) {
|
|
41
|
+
return cfg.hostEntry;
|
|
42
|
+
}
|
|
43
|
+
// Fallback: dist/host/stikfix-native.cjs → dist/host/src/index.js
|
|
44
|
+
// In the esbuild CJS bundle, __dirname is the bundle's directory.
|
|
45
|
+
return join(__dirname, 'src', 'index.js');
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Handle a START_HOST native message: validate `root` is an existing directory,
|
|
49
|
+
* then SPAWN the HTTP host as a DETACHED process (never bind/listen here —
|
|
50
|
+
* invariant T-09-07) so it outlives this one-shot native process. Replies
|
|
51
|
+
* HOST_STARTING on success, ERROR on invalid input. `out` is injectable for tests.
|
|
52
|
+
*
|
|
53
|
+
* Returns true if the host was spawned (caller should exit 0), false on a
|
|
54
|
+
* validation error (caller should exit 1). The actual process.exit is left to
|
|
55
|
+
* the caller so this helper stays unit-testable without side effects.
|
|
56
|
+
*/
|
|
57
|
+
export function handleStartHost(cfg, root, spawnFn = spawn, out) {
|
|
58
|
+
const emit = (msg) => (out ? sendNativeMessage(msg, out) : sendNativeMessage(msg));
|
|
59
|
+
if (typeof root !== 'string' || root.length === 0) {
|
|
60
|
+
emit({ type: 'ERROR', error: 'START_HOST requires a non-empty "root" string.' });
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
// Normalize the incoming root (collapse `..`, consistent separators) before
|
|
64
|
+
// using it for the directory check and the spawned host's --root arg. No
|
|
65
|
+
// confinement check — cross-project roots over the extension-exclusive native
|
|
66
|
+
// channel are intentional; normalization is the correct hardening.
|
|
67
|
+
const resolvedRoot = resolve(root);
|
|
68
|
+
// Validate root is an existing directory (no silent failure).
|
|
69
|
+
try {
|
|
70
|
+
if (!statSync(resolvedRoot).isDirectory()) {
|
|
71
|
+
emit({ type: 'ERROR', error: `START_HOST root is not a directory: ${resolvedRoot}` });
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
emit({ type: 'ERROR', error: `START_HOST root does not exist: ${resolvedRoot}` });
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
const nodePath = typeof cfg.nodePath === 'string' && cfg.nodePath.length > 0 ? cfg.nodePath : process.execPath;
|
|
80
|
+
const hostEntry = resolveHostEntry(cfg);
|
|
81
|
+
// CRITICAL (T-09-07): the native host MUST NOT bind/listen itself. It only
|
|
82
|
+
// spawns a SEPARATE, detached process that is the HTTP host. Detached +
|
|
83
|
+
// stdio:'ignore' + unref() so the child outlives this one-shot native process.
|
|
84
|
+
try {
|
|
85
|
+
const child = spawnFn(nodePath, [hostEntry, '--root', resolvedRoot], {
|
|
86
|
+
detached: true,
|
|
87
|
+
stdio: 'ignore',
|
|
88
|
+
windowsHide: true,
|
|
89
|
+
});
|
|
90
|
+
child.unref();
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
emit({ type: 'ERROR', error: `Failed to start host: ${String(err.message)}` });
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
emit({ type: 'HOST_STARTING', root: resolvedRoot });
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
30
99
|
// ---------------------------------------------------------------------------
|
|
31
100
|
// PICK_FOLDER handler — open the OS dialog, validate, respond, exit (Pitfall 8)
|
|
32
101
|
// Folder validation lives in validate-folder.ts (shared with the HTTP server).
|
|
@@ -88,19 +157,52 @@ export function main() {
|
|
|
88
157
|
readNativeMessages((msg) => {
|
|
89
158
|
const m = msg;
|
|
90
159
|
if (m.type === 'GET_TOKEN') {
|
|
160
|
+
// Optional `root`: if the inbound message carries a valid existing
|
|
161
|
+
// directory, read the token/port/identity from THAT root instead of
|
|
162
|
+
// cfg.root. Absent/invalid → exact current behavior (backward compatible).
|
|
163
|
+
let tokenRoot = cfg.root;
|
|
164
|
+
let name = cfg.name;
|
|
165
|
+
let notesDir = cfg.notesDir;
|
|
166
|
+
if (typeof m.root === 'string' && m.root.length > 0) {
|
|
167
|
+
// Caller explicitly asked for a specific root. Normalize it (collapse
|
|
168
|
+
// `..`, consistent separators), then require it to be an existing
|
|
169
|
+
// directory. NEVER silently fall back to cfg.root — that would return a
|
|
170
|
+
// DIFFERENT project's token mislabeled as the requested one.
|
|
171
|
+
const resolvedRoot = resolve(m.root);
|
|
172
|
+
let isDir = false;
|
|
173
|
+
try {
|
|
174
|
+
isDir = statSync(resolvedRoot).isDirectory();
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
isDir = false;
|
|
178
|
+
}
|
|
179
|
+
if (!isDir) {
|
|
180
|
+
sendNativeMessage({
|
|
181
|
+
type: 'ERROR',
|
|
182
|
+
error: 'GET_TOKEN root not accessible: ' + resolvedRoot,
|
|
183
|
+
});
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
tokenRoot = resolvedRoot;
|
|
187
|
+
name = basename(resolvedRoot);
|
|
188
|
+
notesDir = join(resolvedRoot, 'notes');
|
|
189
|
+
}
|
|
91
190
|
// Token is required ONLY for GET_TOKEN — read it lazily here.
|
|
92
191
|
let token;
|
|
93
192
|
try {
|
|
94
|
-
token = readFileSync(join(
|
|
193
|
+
token = readFileSync(join(tokenRoot, '.stikfix-token'), 'utf8').trim();
|
|
95
194
|
}
|
|
96
195
|
catch {
|
|
97
|
-
sendNativeMessage({
|
|
196
|
+
sendNativeMessage({
|
|
197
|
+
type: 'ERROR',
|
|
198
|
+
error: 'No .stikfix-token in ' + tokenRoot + '. Start the host first.',
|
|
199
|
+
});
|
|
98
200
|
process.exit(1);
|
|
99
201
|
}
|
|
100
202
|
// Port is optional — read if present; SW falls back to port scan (A5 fallback)
|
|
101
203
|
let port;
|
|
102
204
|
try {
|
|
103
|
-
const raw = readFileSync(join(
|
|
205
|
+
const raw = readFileSync(join(tokenRoot, '.stikfix-port'), 'utf8').trim();
|
|
104
206
|
const parsed = parseInt(raw, 10);
|
|
105
207
|
if (!isNaN(parsed)) {
|
|
106
208
|
port = parsed;
|
|
@@ -114,11 +216,17 @@ export function main() {
|
|
|
114
216
|
type: 'TOKEN',
|
|
115
217
|
token,
|
|
116
218
|
port,
|
|
117
|
-
name
|
|
118
|
-
notesDir
|
|
219
|
+
name,
|
|
220
|
+
notesDir,
|
|
119
221
|
});
|
|
120
222
|
process.exit(0);
|
|
121
223
|
}
|
|
224
|
+
if (m.type === 'START_HOST') {
|
|
225
|
+
// Spawn a DETACHED HTTP host for the requested root, then exit (one-shot).
|
|
226
|
+
// The native host itself NEVER binds/listens (T-09-07) — it only spawns.
|
|
227
|
+
const ok = handleStartHost(cfg, m.root);
|
|
228
|
+
process.exit(ok ? 0 : 1);
|
|
229
|
+
}
|
|
122
230
|
if (m.type === 'PICK_FOLDER') {
|
|
123
231
|
// PICK_FOLDER is a SEPARATE spawn from GET_TOKEN (Pitfall 8) — the dialog
|
|
124
232
|
// never blocks a token fetch. It does NOT require the token (read lazily
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Windows system-tray indicator for stikfix-host.
|
|
3
|
+
*
|
|
4
|
+
* Best-effort, cosmetic, Windows-only. Spawns a hidden PowerShell WinForms
|
|
5
|
+
* NotifyIcon helper that polls the host's token-less /status endpoint and shows
|
|
6
|
+
* running (green) / not-responding (grey) state, plus a context menu to open the
|
|
7
|
+
* notes folder, stop the host, or quit the tray.
|
|
8
|
+
*
|
|
9
|
+
* Hard constraints (see project CLAUDE.md):
|
|
10
|
+
* - Node builtins only — NO new npm dependency. The tray is a PowerShell helper,
|
|
11
|
+
* not a Node native tray lib.
|
|
12
|
+
* - Windows-only: complete no-op (returns null) on macOS/Linux.
|
|
13
|
+
* - Never crash the host: any failure to write the script or spawn PowerShell is
|
|
14
|
+
* caught, logged as a single stderr line, and swallowed. A dropped note or a
|
|
15
|
+
* failed host start because of the tray would be an unacceptable regression.
|
|
16
|
+
* - Security: the tray only GETs http://127.0.0.1:<port>/status (existing
|
|
17
|
+
* token-less health endpoint). It weakens nothing.
|
|
18
|
+
*
|
|
19
|
+
* No side effects at import time: startTray() must be *called* to spawn anything.
|
|
20
|
+
*/
|
|
21
|
+
import { spawn } from 'node:child_process';
|
|
22
|
+
import { writeFileSync, mkdirSync } from 'node:fs';
|
|
23
|
+
import { join } from 'node:path';
|
|
24
|
+
import { homedir } from 'node:os';
|
|
25
|
+
/**
|
|
26
|
+
* Directory + path where the embedded tray script is written on each start.
|
|
27
|
+
* Rewriting every start keeps the tray script in lockstep with the host version.
|
|
28
|
+
* Exported (pure) for unit testing without spawning PowerShell.
|
|
29
|
+
*/
|
|
30
|
+
export function trayScriptPath() {
|
|
31
|
+
return join(homedir(), '.local', 'share', 'stikfix', 'stikfix-tray.ps1');
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The embedded PowerShell tray script. A template constant so writing it is a
|
|
35
|
+
* plain fs write — no external file to ship, always matches this host version.
|
|
36
|
+
*
|
|
37
|
+
* Design notes:
|
|
38
|
+
* - Robust icon: try to load a shipped stikfix.ico from a few candidate paths
|
|
39
|
+
* (relative to the host bundle and to $Root); fall back to SystemIcons.
|
|
40
|
+
* - Running/stopped state is shown *robustly* by switching between two stock
|
|
41
|
+
* SystemIcons (Application = running, Warning = not responding) AND always
|
|
42
|
+
* reflecting state in the tooltip text, so the state is obvious regardless of
|
|
43
|
+
* whether the custom .ico loaded.
|
|
44
|
+
* - A Timer (~3s) polls /status with a short timeout. Throw / non-200 => grey.
|
|
45
|
+
* - The timer also checks the host process: if Get-Process -Id $HostPid is gone,
|
|
46
|
+
* dispose the icon and exit, so a dead host removes the tray (honest presence).
|
|
47
|
+
* - Proper WinForms message loop via ApplicationContext so menu + timer work.
|
|
48
|
+
*/
|
|
49
|
+
export const TRAY_PS1 = String.raw `param(
|
|
50
|
+
[int]$Port,
|
|
51
|
+
[string]$Root,
|
|
52
|
+
[string]$Name,
|
|
53
|
+
[string]$NotesDir,
|
|
54
|
+
[int]$HostPid
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
$ErrorActionPreference = 'SilentlyContinue'
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
61
|
+
Add-Type -AssemblyName System.Drawing
|
|
62
|
+
} catch {
|
|
63
|
+
# Without WinForms/Drawing there is no tray to draw. Exit quietly.
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# --- Resolve a custom app icon, else fall back to a stock icon -------------
|
|
68
|
+
function Get-BaseIcon {
|
|
69
|
+
$candidates = @(
|
|
70
|
+
(Join-Path $PSScriptRoot 'stikfix.ico'),
|
|
71
|
+
(Join-Path $Root '.output\chrome-mv3\icon\stikfix.ico'),
|
|
72
|
+
(Join-Path $Root 'public\icon\stikfix.ico')
|
|
73
|
+
)
|
|
74
|
+
foreach ($p in $candidates) {
|
|
75
|
+
if ($p -and (Test-Path -LiteralPath $p)) {
|
|
76
|
+
try { return New-Object System.Drawing.Icon($p) } catch { }
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return $null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
$customIcon = Get-BaseIcon
|
|
83
|
+
$iconRunning = if ($customIcon) { $customIcon } else { [System.Drawing.SystemIcons]::Application }
|
|
84
|
+
$iconStopped = [System.Drawing.SystemIcons]::Warning
|
|
85
|
+
|
|
86
|
+
# --- NotifyIcon -------------------------------------------------------------
|
|
87
|
+
$notify = New-Object System.Windows.Forms.NotifyIcon
|
|
88
|
+
$notify.Icon = $iconRunning
|
|
89
|
+
$notify.Visible = $true
|
|
90
|
+
|
|
91
|
+
# NotifyIcon.Text has a 63-char limit; guard so a long $Name never throws.
|
|
92
|
+
function Set-SafeText([string]$t) {
|
|
93
|
+
if ($t.Length -gt 63) { $t = $t.Substring(0, 60) + '...' }
|
|
94
|
+
$notify.Text = $t
|
|
95
|
+
}
|
|
96
|
+
Set-SafeText "stikfix - $Name - starting on :$Port"
|
|
97
|
+
|
|
98
|
+
# --- Context menu -----------------------------------------------------------
|
|
99
|
+
$menu = New-Object System.Windows.Forms.ContextMenuStrip
|
|
100
|
+
|
|
101
|
+
$miOpen = New-Object System.Windows.Forms.ToolStripMenuItem
|
|
102
|
+
$miOpen.Text = 'Open notes folder'
|
|
103
|
+
$miOpen.Add_Click({
|
|
104
|
+
try { Start-Process explorer.exe -ArgumentList @($NotesDir) } catch { }
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
$miStop = New-Object System.Windows.Forms.ToolStripMenuItem
|
|
108
|
+
$miStop.Text = 'Stop host'
|
|
109
|
+
$miStop.Add_Click({
|
|
110
|
+
try { Stop-Process -Id $HostPid -Force -ErrorAction SilentlyContinue } catch { }
|
|
111
|
+
try { $notify.Visible = $false; $notify.Dispose() } catch { }
|
|
112
|
+
[System.Windows.Forms.Application]::Exit()
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
$miQuit = New-Object System.Windows.Forms.ToolStripMenuItem
|
|
116
|
+
$miQuit.Text = 'Quit tray (host keeps running)'
|
|
117
|
+
$miQuit.Add_Click({
|
|
118
|
+
try { $notify.Visible = $false; $notify.Dispose() } catch { }
|
|
119
|
+
[System.Windows.Forms.Application]::Exit()
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
[void]$menu.Items.Add($miOpen)
|
|
123
|
+
[void]$menu.Items.Add((New-Object System.Windows.Forms.ToolStripSeparator))
|
|
124
|
+
[void]$menu.Items.Add($miStop)
|
|
125
|
+
[void]$menu.Items.Add($miQuit)
|
|
126
|
+
$notify.ContextMenuStrip = $menu
|
|
127
|
+
|
|
128
|
+
# --- Status polling ---------------------------------------------------------
|
|
129
|
+
function Test-HostAlive {
|
|
130
|
+
# $HostPid gone => host is dead.
|
|
131
|
+
$proc = Get-Process -Id $HostPid -ErrorAction SilentlyContinue
|
|
132
|
+
return [bool]$proc
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function Test-StatusOk {
|
|
136
|
+
try {
|
|
137
|
+
$req = [System.Net.WebRequest]::Create("http://127.0.0.1:$Port/status")
|
|
138
|
+
$req.Method = 'GET'
|
|
139
|
+
$req.Timeout = 1500
|
|
140
|
+
$resp = $req.GetResponse()
|
|
141
|
+
$code = [int]$resp.StatusCode
|
|
142
|
+
$resp.Close()
|
|
143
|
+
return ($code -eq 200)
|
|
144
|
+
} catch {
|
|
145
|
+
return $false
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function Update-State {
|
|
150
|
+
# If the host process is gone, remove the tray entirely (honest presence).
|
|
151
|
+
if (-not (Test-HostAlive)) {
|
|
152
|
+
try { $notify.Visible = $false; $notify.Dispose() } catch { }
|
|
153
|
+
[System.Windows.Forms.Application]::Exit()
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (Test-StatusOk) {
|
|
158
|
+
$notify.Icon = $iconRunning
|
|
159
|
+
Set-SafeText "stikfix - $Name - running on :$Port"
|
|
160
|
+
} else {
|
|
161
|
+
$notify.Icon = $iconStopped
|
|
162
|
+
Set-SafeText "stikfix - $Name - not responding"
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
$timer = New-Object System.Windows.Forms.Timer
|
|
167
|
+
$timer.Interval = 3000
|
|
168
|
+
$timer.Add_Tick({ Update-State })
|
|
169
|
+
$timer.Start()
|
|
170
|
+
|
|
171
|
+
# First update immediately so the tooltip is correct without waiting 3s.
|
|
172
|
+
Update-State
|
|
173
|
+
|
|
174
|
+
# --- Message loop -----------------------------------------------------------
|
|
175
|
+
$ctx = New-Object System.Windows.Forms.ApplicationContext
|
|
176
|
+
[System.Windows.Forms.Application]::Run($ctx)
|
|
177
|
+
|
|
178
|
+
# Cleanup on exit.
|
|
179
|
+
try { $timer.Stop(); $timer.Dispose() } catch { }
|
|
180
|
+
try { $notify.Visible = $false; $notify.Dispose() } catch { }
|
|
181
|
+
`;
|
|
182
|
+
/**
|
|
183
|
+
* Start the Windows tray helper.
|
|
184
|
+
*
|
|
185
|
+
* Returns the spawned ChildProcess (so the host can kill it on shutdown), or
|
|
186
|
+
* null on any non-win32 platform or on any failure. NEVER throws.
|
|
187
|
+
*/
|
|
188
|
+
export function startTray(opts) {
|
|
189
|
+
if (process.platform !== 'win32') {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
const ps1Path = trayScriptPath();
|
|
194
|
+
mkdirSync(join(homedir(), '.local', 'share', 'stikfix'), { recursive: true });
|
|
195
|
+
writeFileSync(ps1Path, TRAY_PS1, { encoding: 'utf8' });
|
|
196
|
+
const child = spawn('powershell.exe', [
|
|
197
|
+
'-NoProfile',
|
|
198
|
+
'-ExecutionPolicy',
|
|
199
|
+
'Bypass',
|
|
200
|
+
'-WindowStyle',
|
|
201
|
+
'Hidden',
|
|
202
|
+
'-File',
|
|
203
|
+
ps1Path,
|
|
204
|
+
'-Port',
|
|
205
|
+
String(opts.port),
|
|
206
|
+
'-Root',
|
|
207
|
+
opts.root,
|
|
208
|
+
'-Name',
|
|
209
|
+
opts.name,
|
|
210
|
+
'-NotesDir',
|
|
211
|
+
opts.notesDir,
|
|
212
|
+
'-HostPid',
|
|
213
|
+
String(opts.hostPid),
|
|
214
|
+
], {
|
|
215
|
+
// Not detached: we want it to be a child the host can kill on exit.
|
|
216
|
+
windowsHide: true,
|
|
217
|
+
stdio: 'ignore',
|
|
218
|
+
});
|
|
219
|
+
// A spawn error (e.g. powershell.exe missing) arrives asynchronously; swallow
|
|
220
|
+
// it so it never becomes an unhandled 'error' event that crashes the host.
|
|
221
|
+
child.on('error', (e) => {
|
|
222
|
+
console.error('stikfix: tray unavailable:', e instanceof Error ? e.message : String(e));
|
|
223
|
+
});
|
|
224
|
+
return child;
|
|
225
|
+
}
|
|
226
|
+
catch (e) {
|
|
227
|
+
console.error('stikfix: tray unavailable:', e instanceof Error ? e.message : String(e));
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -287,6 +287,35 @@ function createLauncherFiles(opts) {
|
|
|
287
287
|
}
|
|
288
288
|
return { written, warnings };
|
|
289
289
|
}
|
|
290
|
+
var REG_RUN_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
|
|
291
|
+
var REG_RUN_VALUE = "stikfix-host";
|
|
292
|
+
function registerStartup(opts = {}) {
|
|
293
|
+
const plat = opts.plat ?? process.platform;
|
|
294
|
+
if (plat !== "win32") {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const home = opts.home ?? (0, import_node_os.homedir)();
|
|
298
|
+
const vbsPath = (0, import_node_path2.join)(launcherDir(plat, home), LAUNCHER_VBS_FILENAME);
|
|
299
|
+
const wscriptPath = (0, import_node_path2.join)(process.env["SystemRoot"] ?? "C:\\Windows", "System32", "wscript.exe");
|
|
300
|
+
const runData = `"${wscriptPath}" "${vbsPath}"`;
|
|
301
|
+
const execReg = opts.execReg ?? ((args) => {
|
|
302
|
+
(0, import_node_child_process.execFileSync)("reg", args);
|
|
303
|
+
});
|
|
304
|
+
execReg(["ADD", REG_RUN_KEY, "/v", REG_RUN_VALUE, "/t", "REG_SZ", "/d", runData, "/f"]);
|
|
305
|
+
}
|
|
306
|
+
function unregisterStartup(opts = {}) {
|
|
307
|
+
const plat = opts.plat ?? process.platform;
|
|
308
|
+
if (plat !== "win32") {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const execReg = opts.execReg ?? ((args) => {
|
|
312
|
+
(0, import_node_child_process.execFileSync)("reg", args);
|
|
313
|
+
});
|
|
314
|
+
try {
|
|
315
|
+
execReg(["DELETE", REG_RUN_KEY, "/v", REG_RUN_VALUE, "/f"]);
|
|
316
|
+
} catch {
|
|
317
|
+
}
|
|
318
|
+
}
|
|
290
319
|
function registerNativeHost(opts) {
|
|
291
320
|
const plat = opts.plat ?? process.platform;
|
|
292
321
|
const home = opts.home ?? (0, import_node_os.homedir)();
|
|
@@ -356,7 +385,9 @@ var { values, positionals } = (0, import_node_util.parseArgs)({
|
|
|
356
385
|
root: { type: "string" },
|
|
357
386
|
"extension-id": { type: "string" },
|
|
358
387
|
port: { type: "string" },
|
|
359
|
-
browser: { type: "string" }
|
|
388
|
+
browser: { type: "string" },
|
|
389
|
+
startup: { type: "boolean" },
|
|
390
|
+
"no-startup": { type: "boolean" }
|
|
360
391
|
},
|
|
361
392
|
strict: false
|
|
362
393
|
});
|
|
@@ -369,6 +400,36 @@ function resolveBrowser(raw) {
|
|
|
369
400
|
}
|
|
370
401
|
return raw.toLowerCase() === "firefox" ? "firefox" : "chrome";
|
|
371
402
|
}
|
|
403
|
+
function promptLineSync() {
|
|
404
|
+
const { readSync } = require("node:fs");
|
|
405
|
+
const buf = Buffer.alloc(1);
|
|
406
|
+
let line = "";
|
|
407
|
+
for (; ; ) {
|
|
408
|
+
let bytes = 0;
|
|
409
|
+
try {
|
|
410
|
+
bytes = readSync(0, buf, 0, 1, null);
|
|
411
|
+
} catch {
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
414
|
+
if (bytes === 0) break;
|
|
415
|
+
const ch = buf.toString("utf8", 0, 1);
|
|
416
|
+
if (ch === "\n") break;
|
|
417
|
+
if (ch === "\r") continue;
|
|
418
|
+
line += ch;
|
|
419
|
+
}
|
|
420
|
+
return line.trim();
|
|
421
|
+
}
|
|
422
|
+
function resolveStartupChoice(forceOn, forceOff, isTTY) {
|
|
423
|
+
if (forceOn) return true;
|
|
424
|
+
if (forceOff) return false;
|
|
425
|
+
if (isTTY) {
|
|
426
|
+
process.stdout.write("Start stikfix host automatically on Windows login? [Y/n] ");
|
|
427
|
+
const answer = promptLineSync().toLowerCase();
|
|
428
|
+
if (answer === "n" || answer === "no") return false;
|
|
429
|
+
return true;
|
|
430
|
+
}
|
|
431
|
+
return false;
|
|
432
|
+
}
|
|
372
433
|
var CONFIG_DIR = (0, import_node_path3.join)((0, import_node_os2.homedir)(), ".config", "stikfix");
|
|
373
434
|
var CONFIG_PATH = (0, import_node_path3.join)(CONFIG_DIR, "config.json");
|
|
374
435
|
if (subcommand === "init") {
|
|
@@ -388,8 +449,9 @@ if (subcommand === "init") {
|
|
|
388
449
|
const root = (0, import_node_path3.resolve)(rawRoot);
|
|
389
450
|
const name = (0, import_node_path3.basename)(root);
|
|
390
451
|
const notesDir = (0, import_node_path3.join)(root, "notes");
|
|
452
|
+
const hostEntryPath = (0, import_node_path3.resolve)((0, import_node_path3.join)(__dirname, "src", "index.js"));
|
|
391
453
|
(0, import_node_fs2.mkdirSync)(CONFIG_DIR, { recursive: true });
|
|
392
|
-
const config = { root, name, notesDir };
|
|
454
|
+
const config = { root, name, notesDir, hostEntry: hostEntryPath, nodePath: process.execPath };
|
|
393
455
|
(0, import_node_fs2.writeFileSync)(CONFIG_PATH, JSON.stringify(config, null, 2), { encoding: "utf8", mode: 384 });
|
|
394
456
|
const hostBinPath = (0, import_node_path3.resolve)((0, import_node_path3.join)(__dirname, "stikfix-native.cjs"));
|
|
395
457
|
try {
|
|
@@ -398,7 +460,6 @@ if (subcommand === "init") {
|
|
|
398
460
|
console.error("stikfix init: failed to register native host:", String(err));
|
|
399
461
|
process.exit(1);
|
|
400
462
|
}
|
|
401
|
-
const hostEntryPath = (0, import_node_path3.resolve)((0, import_node_path3.join)(__dirname, "src", "index.js"));
|
|
402
463
|
const projectRoot = (0, import_node_path3.resolve)((0, import_node_path3.join)(__dirname, "..", ".."));
|
|
403
464
|
const firefoxOutputCandidates = [
|
|
404
465
|
(0, import_node_path3.join)(projectRoot, ".output", "firefox-mv2"),
|
|
@@ -426,6 +487,32 @@ if (subcommand === "init") {
|
|
|
426
487
|
for (const warn of launcherResult.warnings) {
|
|
427
488
|
console.warn(" [warn] " + warn);
|
|
428
489
|
}
|
|
490
|
+
if (process.platform === "win32") {
|
|
491
|
+
const wantStartup = resolveStartupChoice(
|
|
492
|
+
values["startup"] === true,
|
|
493
|
+
values["no-startup"] === true,
|
|
494
|
+
Boolean(process.stdin.isTTY)
|
|
495
|
+
);
|
|
496
|
+
if (wantStartup) {
|
|
497
|
+
try {
|
|
498
|
+
registerStartup();
|
|
499
|
+
console.log(" Startup: enabled \u2014 the host will start on Windows login.");
|
|
500
|
+
} catch (err) {
|
|
501
|
+
console.warn(
|
|
502
|
+
" [warn] Could not register Windows startup autoload (non-fatal): " + String(err)
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
} else {
|
|
506
|
+
try {
|
|
507
|
+
unregisterStartup();
|
|
508
|
+
} catch {
|
|
509
|
+
}
|
|
510
|
+
const hint = values["no-startup"] !== true && !process.stdin.isTTY ? " (enable later with: npx stikfix init --root <dir> --startup)" : "";
|
|
511
|
+
console.log(" Startup: not enabled" + hint + ".");
|
|
512
|
+
}
|
|
513
|
+
} else if (values["startup"] === true) {
|
|
514
|
+
console.log(" Startup: skipped \u2014 startup autoload is Windows-only for now.");
|
|
515
|
+
}
|
|
429
516
|
console.log("");
|
|
430
517
|
console.log("stikfix: native host registered successfully.");
|
|
431
518
|
console.log("");
|
|
@@ -486,10 +573,16 @@ if (subcommand === "init") {
|
|
|
486
573
|
} catch (err) {
|
|
487
574
|
console.error("stikfix uninstall: error removing native-host manifest:", String(err));
|
|
488
575
|
}
|
|
576
|
+
try {
|
|
577
|
+
unregisterStartup();
|
|
578
|
+
} catch (err) {
|
|
579
|
+
console.error("stikfix uninstall: error removing startup entry:", String(err));
|
|
580
|
+
}
|
|
489
581
|
(0, import_node_fs2.rmSync)(CONFIG_PATH, { force: true });
|
|
490
582
|
console.log("stikfix: native host unregistered.");
|
|
491
583
|
console.log(" manifest removed");
|
|
492
584
|
console.log(" launcher files removed");
|
|
585
|
+
console.log(" startup entry removed");
|
|
493
586
|
console.log(" config removed");
|
|
494
587
|
} else {
|
|
495
588
|
console.error("Usage: npx stikfix <init|uninstall> [--root <dir>] [--browser <chrome|firefox>] [--extension-id <id>] [--port <port>]");
|
|
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var native_host_exports = {};
|
|
22
22
|
__export(native_host_exports, {
|
|
23
23
|
handlePickFolder: () => handlePickFolder,
|
|
24
|
+
handleStartHost: () => handleStartHost,
|
|
24
25
|
main: () => main,
|
|
25
26
|
validateChosenFolder: () => validateChosenFolder
|
|
26
27
|
});
|
|
@@ -28,6 +29,7 @@ module.exports = __toCommonJS(native_host_exports);
|
|
|
28
29
|
var import_node_fs2 = require("node:fs");
|
|
29
30
|
var import_node_path2 = require("node:path");
|
|
30
31
|
var import_node_os = require("node:os");
|
|
32
|
+
var import_node_child_process2 = require("node:child_process");
|
|
31
33
|
|
|
32
34
|
// host/src/native-msg.ts
|
|
33
35
|
function encodeNativeMessage(msg) {
|
|
@@ -107,11 +109,11 @@ function buildPickerArgs(plat = process.platform, title = "Choose folder") {
|
|
|
107
109
|
}
|
|
108
110
|
async function pickFolder(title = "Choose a project folder", plat = process.platform) {
|
|
109
111
|
if (plat === "darwin") {
|
|
110
|
-
return new Promise((
|
|
112
|
+
return new Promise((resolve3) => {
|
|
111
113
|
const { args } = buildPickerArgs("darwin", title);
|
|
112
114
|
(0, import_node_child_process.execFile)("osascript", args, { timeout: 12e4 }, (err, stdout) => {
|
|
113
115
|
if (err || !stdout.trim()) {
|
|
114
|
-
|
|
116
|
+
resolve3(null);
|
|
115
117
|
return;
|
|
116
118
|
}
|
|
117
119
|
const raw = stdout.trim();
|
|
@@ -120,7 +122,7 @@ async function pickFolder(title = "Choose a project folder", plat = process.plat
|
|
|
120
122
|
["-e", `POSIX path of (${raw})`],
|
|
121
123
|
{},
|
|
122
124
|
(e2, out2) => {
|
|
123
|
-
|
|
125
|
+
resolve3(e2 ? null : out2.trim() || null);
|
|
124
126
|
}
|
|
125
127
|
);
|
|
126
128
|
});
|
|
@@ -128,9 +130,9 @@ async function pickFolder(title = "Choose a project folder", plat = process.plat
|
|
|
128
130
|
}
|
|
129
131
|
if (plat === "win32") {
|
|
130
132
|
const { cmd, args } = buildPickerArgs("win32", title);
|
|
131
|
-
return new Promise((
|
|
133
|
+
return new Promise((resolve3) => {
|
|
132
134
|
(0, import_node_child_process.execFile)(cmd, args, { timeout: 12e4 }, (err, stdout) => {
|
|
133
|
-
|
|
135
|
+
resolve3(err ? null : stdout.trim() || null);
|
|
134
136
|
});
|
|
135
137
|
});
|
|
136
138
|
}
|
|
@@ -141,19 +143,19 @@ async function pickFolder(title = "Choose a project folder", plat = process.plat
|
|
|
141
143
|
return null;
|
|
142
144
|
}
|
|
143
145
|
function tryLinuxPicker(tool, title) {
|
|
144
|
-
return new Promise((
|
|
146
|
+
return new Promise((resolve3) => {
|
|
145
147
|
const args = tool === "zenity" ? ["--file-selection", "--directory", `--title=${title}`] : ["--getexistingdirectory", "/home"];
|
|
146
148
|
(0, import_node_child_process.execFile)(tool, args, { timeout: 12e4 }, (err, stdout) => {
|
|
147
149
|
if (err) {
|
|
148
150
|
const e = err;
|
|
149
151
|
if (e.code === "ENOENT") {
|
|
150
|
-
|
|
152
|
+
resolve3(void 0);
|
|
151
153
|
} else {
|
|
152
|
-
|
|
154
|
+
resolve3(null);
|
|
153
155
|
}
|
|
154
156
|
return;
|
|
155
157
|
}
|
|
156
|
-
|
|
158
|
+
resolve3(stdout.trim() || null);
|
|
157
159
|
});
|
|
158
160
|
});
|
|
159
161
|
}
|
|
@@ -186,6 +188,44 @@ function validateChosenFolder(folder, plat = process.platform) {
|
|
|
186
188
|
|
|
187
189
|
// host/src/native-host.ts
|
|
188
190
|
var CONFIG_PATH = (0, import_node_path2.join)((0, import_node_os.homedir)(), ".config", "stikfix", "config.json");
|
|
191
|
+
function resolveHostEntry(cfg) {
|
|
192
|
+
if (typeof cfg.hostEntry === "string" && cfg.hostEntry.length > 0) {
|
|
193
|
+
return cfg.hostEntry;
|
|
194
|
+
}
|
|
195
|
+
return (0, import_node_path2.join)(__dirname, "src", "index.js");
|
|
196
|
+
}
|
|
197
|
+
function handleStartHost(cfg, root, spawnFn = import_node_child_process2.spawn, out) {
|
|
198
|
+
const emit = (msg) => out ? sendNativeMessage(msg, out) : sendNativeMessage(msg);
|
|
199
|
+
if (typeof root !== "string" || root.length === 0) {
|
|
200
|
+
emit({ type: "ERROR", error: 'START_HOST requires a non-empty "root" string.' });
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
const resolvedRoot = (0, import_node_path2.resolve)(root);
|
|
204
|
+
try {
|
|
205
|
+
if (!(0, import_node_fs2.statSync)(resolvedRoot).isDirectory()) {
|
|
206
|
+
emit({ type: "ERROR", error: `START_HOST root is not a directory: ${resolvedRoot}` });
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
} catch {
|
|
210
|
+
emit({ type: "ERROR", error: `START_HOST root does not exist: ${resolvedRoot}` });
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
const nodePath = typeof cfg.nodePath === "string" && cfg.nodePath.length > 0 ? cfg.nodePath : process.execPath;
|
|
214
|
+
const hostEntry = resolveHostEntry(cfg);
|
|
215
|
+
try {
|
|
216
|
+
const child = spawnFn(nodePath, [hostEntry, "--root", resolvedRoot], {
|
|
217
|
+
detached: true,
|
|
218
|
+
stdio: "ignore",
|
|
219
|
+
windowsHide: true
|
|
220
|
+
});
|
|
221
|
+
child.unref();
|
|
222
|
+
} catch (err) {
|
|
223
|
+
emit({ type: "ERROR", error: `Failed to start host: ${String(err.message)}` });
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
emit({ type: "HOST_STARTING", root: resolvedRoot });
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
189
229
|
async function handlePickFolder(origin, pickFn = pickFolder, plat = process.platform, out) {
|
|
190
230
|
let chosen;
|
|
191
231
|
try {
|
|
@@ -211,16 +251,41 @@ function main() {
|
|
|
211
251
|
readNativeMessages((msg) => {
|
|
212
252
|
const m = msg;
|
|
213
253
|
if (m.type === "GET_TOKEN") {
|
|
254
|
+
let tokenRoot = cfg.root;
|
|
255
|
+
let name = cfg.name;
|
|
256
|
+
let notesDir = cfg.notesDir;
|
|
257
|
+
if (typeof m.root === "string" && m.root.length > 0) {
|
|
258
|
+
const resolvedRoot = (0, import_node_path2.resolve)(m.root);
|
|
259
|
+
let isDir = false;
|
|
260
|
+
try {
|
|
261
|
+
isDir = (0, import_node_fs2.statSync)(resolvedRoot).isDirectory();
|
|
262
|
+
} catch {
|
|
263
|
+
isDir = false;
|
|
264
|
+
}
|
|
265
|
+
if (!isDir) {
|
|
266
|
+
sendNativeMessage({
|
|
267
|
+
type: "ERROR",
|
|
268
|
+
error: "GET_TOKEN root not accessible: " + resolvedRoot
|
|
269
|
+
});
|
|
270
|
+
process.exit(1);
|
|
271
|
+
}
|
|
272
|
+
tokenRoot = resolvedRoot;
|
|
273
|
+
name = (0, import_node_path2.basename)(resolvedRoot);
|
|
274
|
+
notesDir = (0, import_node_path2.join)(resolvedRoot, "notes");
|
|
275
|
+
}
|
|
214
276
|
let token;
|
|
215
277
|
try {
|
|
216
|
-
token = (0, import_node_fs2.readFileSync)((0, import_node_path2.join)(
|
|
278
|
+
token = (0, import_node_fs2.readFileSync)((0, import_node_path2.join)(tokenRoot, ".stikfix-token"), "utf8").trim();
|
|
217
279
|
} catch {
|
|
218
|
-
sendNativeMessage({
|
|
280
|
+
sendNativeMessage({
|
|
281
|
+
type: "ERROR",
|
|
282
|
+
error: "No .stikfix-token in " + tokenRoot + ". Start the host first."
|
|
283
|
+
});
|
|
219
284
|
process.exit(1);
|
|
220
285
|
}
|
|
221
286
|
let port;
|
|
222
287
|
try {
|
|
223
|
-
const raw = (0, import_node_fs2.readFileSync)((0, import_node_path2.join)(
|
|
288
|
+
const raw = (0, import_node_fs2.readFileSync)((0, import_node_path2.join)(tokenRoot, ".stikfix-port"), "utf8").trim();
|
|
224
289
|
const parsed = parseInt(raw, 10);
|
|
225
290
|
if (!isNaN(parsed)) {
|
|
226
291
|
port = parsed;
|
|
@@ -231,11 +296,15 @@ function main() {
|
|
|
231
296
|
type: "TOKEN",
|
|
232
297
|
token,
|
|
233
298
|
port,
|
|
234
|
-
name
|
|
235
|
-
notesDir
|
|
299
|
+
name,
|
|
300
|
+
notesDir
|
|
236
301
|
});
|
|
237
302
|
process.exit(0);
|
|
238
303
|
}
|
|
304
|
+
if (m.type === "START_HOST") {
|
|
305
|
+
const ok = handleStartHost(cfg, m.root);
|
|
306
|
+
process.exit(ok ? 0 : 1);
|
|
307
|
+
}
|
|
239
308
|
if (m.type === "PICK_FOLDER") {
|
|
240
309
|
handlePickFolder(m.origin).then(
|
|
241
310
|
() => process.exit(0),
|
|
@@ -252,6 +321,7 @@ if (typeof require !== "undefined" && typeof module !== "undefined" && require.m
|
|
|
252
321
|
// Annotate the CommonJS export names for ESM import in node:
|
|
253
322
|
0 && (module.exports = {
|
|
254
323
|
handlePickFolder,
|
|
324
|
+
handleStartHost,
|
|
255
325
|
main,
|
|
256
326
|
validateChosenFolder
|
|
257
327
|
});
|