stikfix 1.3.2 → 1.6.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/README.md +53 -7
- package/dist/host/src/bootstrap/register.js +108 -17
- package/dist/host/src/config.js +42 -8
- package/dist/host/src/doctor.js +459 -0
- package/dist/host/src/exe-main.js +254 -0
- package/dist/host/src/git-sync.js +146 -0
- package/dist/host/src/index.js +156 -117
- package/dist/host/src/native-host.js +34 -7
- package/dist/host/src/server.js +23 -3
- package/dist/host/stikfix-init.cjs +61 -28
- package/dist/host/stikfix-native.cjs +17 -2
- package/dist/lib/pin-position.js +69 -0
- package/package.json +8 -1
package/dist/host/src/index.js
CHANGED
|
@@ -12,135 +12,174 @@
|
|
|
12
12
|
import { writeFileSync, readFileSync, existsSync } from 'node:fs';
|
|
13
13
|
import { join } from 'node:path';
|
|
14
14
|
import { parseArgs } from 'node:util';
|
|
15
|
+
import { pathToFileURL } from 'node:url';
|
|
15
16
|
import { resolveConfig, resolveConfigValues, ensureNotesDir, writeTokenFile } from './config.js';
|
|
16
17
|
import { createHostServer } from './server.js';
|
|
17
18
|
import { bindServer, BIND_HOST } from './bind.js';
|
|
18
19
|
import { probeExistingHost } from './probe.js';
|
|
19
20
|
import { startTray } from './tray.js';
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Start the HTTP host. Extracted from the former top-level module body so the
|
|
23
|
+
* single-executable dispatcher (exe-main.ts) can call it as `serve`, while
|
|
24
|
+
* `node dist/host/src/index.js` keeps running it via the direct-entry guard at
|
|
25
|
+
* the bottom of this file. Behavior is byte-for-byte identical to the previous
|
|
26
|
+
* top-level code — the only change is that argv is passed in (defaulting to
|
|
27
|
+
* process.argv.slice(2)) instead of being read from the global process.argv.
|
|
28
|
+
*/
|
|
29
|
+
export async function startHttpHost(argv = process.argv.slice(2)) {
|
|
30
|
+
// -------------------------------------------------------------------------
|
|
31
|
+
// CLI parsing (preserve Phase 1 stub options block — HOST-13)
|
|
32
|
+
// -------------------------------------------------------------------------
|
|
33
|
+
const { values: rawValues } = parseArgs({
|
|
34
|
+
args: argv,
|
|
35
|
+
options: {
|
|
36
|
+
root: { type: 'string' },
|
|
37
|
+
origin: { type: 'string', multiple: true },
|
|
38
|
+
name: { type: 'string' },
|
|
39
|
+
'notes-dir': { type: 'string' },
|
|
40
|
+
port: { type: 'string' },
|
|
41
|
+
token: { type: 'string' },
|
|
42
|
+
'git-sync': { type: 'boolean' },
|
|
43
|
+
},
|
|
44
|
+
strict: false,
|
|
45
|
+
});
|
|
46
|
+
// Apply three-tier env resolution (flag > STIKFIX_* > npm_config_*) so that
|
|
47
|
+
// `npm run host -- --root <dir>` works in Windows PowerShell, where npm 11.x
|
|
48
|
+
// intercepts unknown flags and re-exposes them as npm_config_<key> env vars.
|
|
49
|
+
const values = resolveConfigValues(rawValues);
|
|
50
|
+
if (!values['root']) {
|
|
51
|
+
console.error('stikfix-host: --root is required');
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
// -------------------------------------------------------------------------
|
|
55
|
+
// Config resolution + filesystem setup
|
|
56
|
+
// -------------------------------------------------------------------------
|
|
57
|
+
const cfg = resolveConfig(values);
|
|
58
|
+
ensureNotesDir(cfg.notesDir);
|
|
59
|
+
// -------------------------------------------------------------------------
|
|
60
|
+
// FIX-SI: Single-instance guard — BEFORE any file mutation
|
|
61
|
+
// -------------------------------------------------------------------------
|
|
62
|
+
const portFilePath = join(cfg.root, '.stikfix-port');
|
|
63
|
+
if (existsSync(portFilePath)) {
|
|
64
|
+
const portStr = readFileSync(portFilePath, 'utf8').trim();
|
|
65
|
+
const existingPort = Number(portStr);
|
|
66
|
+
if (Number.isInteger(existingPort) && existingPort > 0 && existingPort <= 65535) {
|
|
67
|
+
const live = await probeExistingHost(cfg.root, existingPort);
|
|
68
|
+
if (live !== null) {
|
|
69
|
+
console.log(`stikfix-host: already running for ${cfg.root} on port ${live.port} — not starting a second instance.`);
|
|
70
|
+
process.exit(0);
|
|
71
|
+
}
|
|
72
|
+
// else: stale port file — continue with normal startup
|
|
59
73
|
}
|
|
60
|
-
// else: stale port file — continue with normal startup
|
|
61
74
|
}
|
|
62
|
-
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
75
|
+
// -------------------------------------------------------------------------
|
|
76
|
+
// FIX-TP: Token persistence — reuse existing token if no explicit token given
|
|
77
|
+
// -------------------------------------------------------------------------
|
|
78
|
+
// An "explicit" token is one supplied via --token / STIKFIX_TOKEN / npm_config_token.
|
|
79
|
+
// resolveConfig already resolved these; if none was supplied, randomUUID() was called.
|
|
80
|
+
// We detect the "no explicit token" case by checking the raw resolved values BEFORE
|
|
81
|
+
// randomUUID was applied.
|
|
82
|
+
const rawToken = values['token'] ??
|
|
83
|
+
(process.env['STIKFIX_TOKEN']) ??
|
|
84
|
+
(process.env['npm_config_token']);
|
|
85
|
+
const tokenFilePath = join(cfg.root, '.stikfix-token');
|
|
86
|
+
let finalToken = cfg.token;
|
|
87
|
+
if (!rawToken) {
|
|
88
|
+
// No explicit token provided — try to reuse existing token file
|
|
89
|
+
if (existsSync(tokenFilePath)) {
|
|
90
|
+
const existing = readFileSync(tokenFilePath, 'utf8').trim();
|
|
91
|
+
if (existing.length > 0) {
|
|
92
|
+
finalToken = existing;
|
|
93
|
+
}
|
|
94
|
+
// else: empty file — keep freshly generated UUID from resolveConfig
|
|
95
|
+
}
|
|
96
|
+
// else: no existing file — keep freshly generated UUID from resolveConfig
|
|
97
|
+
}
|
|
98
|
+
// Write the final token (idempotent — fixes perms to 0o600 via writeTokenFile)
|
|
99
|
+
writeTokenFile(cfg.root, finalToken);
|
|
100
|
+
// -------------------------------------------------------------------------
|
|
101
|
+
// Port discovery — bind-or-fail loop on 127.0.0.1 (Pattern 1 / D-08)
|
|
102
|
+
// -------------------------------------------------------------------------
|
|
103
|
+
const server = createHostServer({ ...cfg, token: finalToken });
|
|
104
|
+
const boundPort = await bindServer(server, cfg.port);
|
|
105
|
+
// Safety assertion: binding must be 127.0.0.1 (T-02-bind)
|
|
106
|
+
const addr = server.address();
|
|
107
|
+
if (addr.address !== BIND_HOST) {
|
|
108
|
+
throw new Error(`FATAL: server bound to ${addr.address} instead of ${BIND_HOST}`);
|
|
109
|
+
}
|
|
110
|
+
// Publish bound port to disk so the native host can read it without a port scan
|
|
111
|
+
// (RESEARCH Open Question 2 / A5). Mode 0o600 alongside existing token file.
|
|
112
|
+
writeFileSync(join(cfg.root, '.stikfix-port'), String(boundPort), { encoding: 'utf8', mode: 0o600 });
|
|
113
|
+
// Startup JSON line — read by smoke test via readline (Pattern 12)
|
|
114
|
+
console.log(JSON.stringify({
|
|
115
|
+
app: 'stikfix',
|
|
116
|
+
name: cfg.name,
|
|
117
|
+
root: cfg.root,
|
|
118
|
+
port: boundPort,
|
|
119
|
+
token: finalToken,
|
|
120
|
+
notesDir: cfg.notesDir,
|
|
121
|
+
origins: cfg.origins,
|
|
122
|
+
}));
|
|
123
|
+
// -------------------------------------------------------------------------
|
|
124
|
+
// Windows system-tray indicator (best-effort, cosmetic, win32-only)
|
|
125
|
+
// -------------------------------------------------------------------------
|
|
126
|
+
// Only reached on genuine startup — the single-instance guard above exits(0)
|
|
127
|
+
// before here, so a second instance never spawns a duplicate tray.
|
|
128
|
+
const tray = startTray({
|
|
129
|
+
port: boundPort,
|
|
130
|
+
root: cfg.root,
|
|
131
|
+
name: cfg.name,
|
|
132
|
+
notesDir: cfg.notesDir,
|
|
133
|
+
hostPid: process.pid,
|
|
134
|
+
});
|
|
135
|
+
// Kill the tray child when the host shuts down so a dead host has no tray.
|
|
136
|
+
// (No prior signal handling existed here; these are additive.)
|
|
137
|
+
let shuttingDown = false;
|
|
138
|
+
function shutdown(signal) {
|
|
139
|
+
if (shuttingDown)
|
|
140
|
+
return;
|
|
141
|
+
shuttingDown = true;
|
|
142
|
+
try {
|
|
143
|
+
tray?.kill();
|
|
81
144
|
}
|
|
82
|
-
|
|
145
|
+
catch { /* best-effort */ }
|
|
146
|
+
server.close(() => process.exit(0));
|
|
147
|
+
// Fallback: exit even if server.close hangs on lingering connections.
|
|
148
|
+
setTimeout(() => process.exit(0), 1000).unref();
|
|
149
|
+
void signal;
|
|
83
150
|
}
|
|
84
|
-
|
|
151
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
152
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
153
|
+
// Belt-and-suspenders: also reap the tray on plain process exit.
|
|
154
|
+
process.on('exit', () => { try {
|
|
155
|
+
tray?.kill();
|
|
156
|
+
}
|
|
157
|
+
catch { /* best-effort */ } });
|
|
158
|
+
// Server runs indefinitely (Pitfall 2 — no process.exit here)
|
|
85
159
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
// Safety assertion: binding must be 127.0.0.1 (T-02-bind)
|
|
94
|
-
const addr = server.address();
|
|
95
|
-
if (addr.address !== BIND_HOST) {
|
|
96
|
-
throw new Error(`FATAL: server bound to ${addr.address} instead of ${BIND_HOST}`);
|
|
160
|
+
function isBundled() {
|
|
161
|
+
try {
|
|
162
|
+
return typeof __STIKFIX_BUNDLED__ !== 'undefined' && __STIKFIX_BUNDLED__ === true;
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
97
167
|
}
|
|
98
|
-
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
app: 'stikfix',
|
|
104
|
-
name: cfg.name,
|
|
105
|
-
root: cfg.root,
|
|
106
|
-
port: boundPort,
|
|
107
|
-
token: finalToken,
|
|
108
|
-
notesDir: cfg.notesDir,
|
|
109
|
-
origins: cfg.origins,
|
|
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;
|
|
168
|
+
function isDirectEntry() {
|
|
169
|
+
// In the single-executable bundle, exe-main.ts dispatches explicitly — this
|
|
170
|
+
// guard must never fire there (it would double-start the host).
|
|
171
|
+
if (isBundled())
|
|
172
|
+
return false;
|
|
130
173
|
try {
|
|
131
|
-
|
|
174
|
+
return import.meta.url === pathToFileURL(process.argv[1] ?? '').href;
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return false;
|
|
132
178
|
}
|
|
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
179
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
process.
|
|
143
|
-
|
|
180
|
+
if (isDirectEntry()) {
|
|
181
|
+
startHttpHost().catch((err) => {
|
|
182
|
+
console.error(err instanceof Error ? (err.stack ?? err.message) : String(err));
|
|
183
|
+
process.exit(1);
|
|
184
|
+
});
|
|
144
185
|
}
|
|
145
|
-
catch { /* best-effort */ } });
|
|
146
|
-
// Server runs indefinitely (Pitfall 2 — no process.exit here)
|
|
@@ -78,11 +78,19 @@ export function handleStartHost(cfg, root, spawnFn = spawn, out) {
|
|
|
78
78
|
}
|
|
79
79
|
const nodePath = typeof cfg.nodePath === 'string' && cfg.nodePath.length > 0 ? cfg.nodePath : process.execPath;
|
|
80
80
|
const hostEntry = resolveHostEntry(cfg);
|
|
81
|
+
// Standalone-exe install path: when config points at a single-executable host,
|
|
82
|
+
// spawn `<exe> serve --root <root>` (the exe self-detects the subcommand).
|
|
83
|
+
// Otherwise keep the exact prior behavior: `node <hostEntry> --root <root>`.
|
|
84
|
+
const hostExe = typeof cfg.hostExe === 'string' && cfg.hostExe.length > 0 ? cfg.hostExe : undefined;
|
|
85
|
+
const spawnCmd = hostExe ?? nodePath;
|
|
86
|
+
const spawnArgs = hostExe
|
|
87
|
+
? ['serve', '--root', resolvedRoot]
|
|
88
|
+
: [hostEntry, '--root', resolvedRoot];
|
|
81
89
|
// CRITICAL (T-09-07): the native host MUST NOT bind/listen itself. It only
|
|
82
90
|
// spawns a SEPARATE, detached process that is the HTTP host. Detached +
|
|
83
91
|
// stdio:'ignore' + unref() so the child outlives this one-shot native process.
|
|
84
92
|
try {
|
|
85
|
-
const child = spawnFn(
|
|
93
|
+
const child = spawnFn(spawnCmd, spawnArgs, {
|
|
86
94
|
detached: true,
|
|
87
95
|
stdio: 'ignore',
|
|
88
96
|
windowsHide: true,
|
|
@@ -239,12 +247,31 @@ export function main() {
|
|
|
239
247
|
process.exit(0);
|
|
240
248
|
});
|
|
241
249
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
250
|
+
/**
|
|
251
|
+
* Async-friendly entry alias for the SEA dispatcher (exe-main.ts). The native
|
|
252
|
+
* message loop is event-driven, so this resolves immediately after wiring up the
|
|
253
|
+
* stdin listener; the process stays alive until stdin closes (Chrome closes the
|
|
254
|
+
* pipe) or a handler calls process.exit. Delegates to main() so the direct
|
|
255
|
+
* require.main path below is unchanged.
|
|
256
|
+
*/
|
|
257
|
+
export async function runNativeHost() {
|
|
258
|
+
main();
|
|
259
|
+
}
|
|
260
|
+
function bundledInExe() {
|
|
261
|
+
try {
|
|
262
|
+
return typeof __STIKFIX_BUNDLED__ !== 'undefined' && __STIKFIX_BUNDLED__ === true;
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
// Run only when invoked as the entry point (standalone stikfix-native.cjs),
|
|
269
|
+
// never on import. In that esbuild CJS bundle `require`/`module` are CJS scope
|
|
270
|
+
// locals and `require.main === module` is true when run directly. The node:test
|
|
271
|
+
// compile path emits ESM where these are undefined at runtime — the `typeof`
|
|
272
|
+
// guards keep main() from running on import there.
|
|
273
|
+
if (!bundledInExe() &&
|
|
274
|
+
typeof require !== 'undefined' &&
|
|
248
275
|
typeof module !== 'undefined' &&
|
|
249
276
|
require.main === module) {
|
|
250
277
|
main();
|
package/dist/host/src/server.js
CHANGED
|
@@ -16,6 +16,7 @@ import { withSerialLock, getNextSerial } from './serial.js';
|
|
|
16
16
|
import { writeNote } from './write-note.js';
|
|
17
17
|
import { listAnnotations, editNote, deleteNote } from './read-note.js';
|
|
18
18
|
import { validateChosenFolder } from './validate-folder.js';
|
|
19
|
+
import { isGitRepo, getLastGitSyncStatus, gitSyncNote } from './git-sync.js';
|
|
19
20
|
// ---------------------------------------------------------------------------
|
|
20
21
|
// Per-request notes-dir resolution (D-04 — targetDir support)
|
|
21
22
|
// ---------------------------------------------------------------------------
|
|
@@ -81,7 +82,7 @@ function setPreflightHeaders(req, res) {
|
|
|
81
82
|
// ---------------------------------------------------------------------------
|
|
82
83
|
// Route handlers
|
|
83
84
|
// ---------------------------------------------------------------------------
|
|
84
|
-
function handleStatus(req, res, cfg) {
|
|
85
|
+
async function handleStatus(req, res, cfg) {
|
|
85
86
|
setCorsHeaders(req, res);
|
|
86
87
|
const body = JSON.stringify({
|
|
87
88
|
app: 'stikfix',
|
|
@@ -90,6 +91,10 @@ function handleStatus(req, res, cfg) {
|
|
|
90
91
|
root: cfg.root,
|
|
91
92
|
notesDir: cfg.notesDir,
|
|
92
93
|
origins: cfg.origins,
|
|
94
|
+
// git-sync surface (shared contract): is cfg.root a git work tree, and the
|
|
95
|
+
// result of the most recent git-sync attempt (null if none yet).
|
|
96
|
+
gitRepo: await isGitRepo(cfg.root),
|
|
97
|
+
gitSyncStatus: getLastGitSyncStatus(),
|
|
93
98
|
});
|
|
94
99
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
95
100
|
res.end(body);
|
|
@@ -162,12 +167,21 @@ async function handleAnnotation(req, res, cfg) {
|
|
|
162
167
|
// Writes are confined to <notesDir> (= cfg.notesDir OR <validated targetDir>/notes).
|
|
163
168
|
// targetDir is NOT persisted into the note frontmatter (routing-only field).
|
|
164
169
|
try {
|
|
170
|
+
let serialNum = 0;
|
|
165
171
|
const { file, serial } = await withSerialLock(async () => {
|
|
166
|
-
|
|
172
|
+
serialNum = getNextSerial(notesDir);
|
|
167
173
|
return writeNote(notesDir, payload, serialNum);
|
|
168
174
|
});
|
|
169
175
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
170
176
|
res.end(JSON.stringify({ ok: true, file, serial }));
|
|
177
|
+
// git-sync (opt-in) — AFTER the 200 is sent, outside withSerialLock, and
|
|
178
|
+
// fire-and-forget so Send stays instant. Runs iff the CLI/machine default
|
|
179
|
+
// (cfg.gitSync) OR the per-send flag (payload.gitSync) is true. Its own queue
|
|
180
|
+
// serializes concurrent notes; .catch-guarded so it can never crash the host.
|
|
181
|
+
const doSync = cfg.gitSync === true || payload.gitSync === true;
|
|
182
|
+
if (doSync) {
|
|
183
|
+
void gitSyncNote({ root: cfg.root, notesDir, serial: serialNum }).catch(() => { });
|
|
184
|
+
}
|
|
171
185
|
}
|
|
172
186
|
catch (e) {
|
|
173
187
|
// CR-02: propagate statusCode from write-phase errors (e.g. bad screenshot → 400)
|
|
@@ -384,7 +398,13 @@ export function createHostServer(cfg) {
|
|
|
384
398
|
return;
|
|
385
399
|
}
|
|
386
400
|
if (method === 'GET' && path === '/status') {
|
|
387
|
-
handleStatus(req, res, cfg)
|
|
401
|
+
handleStatus(req, res, cfg).catch((e) => {
|
|
402
|
+
if (!res.headersSent) {
|
|
403
|
+
setCorsHeaders(req, res);
|
|
404
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
405
|
+
res.end(JSON.stringify({ ok: false, error: String(e) }));
|
|
406
|
+
}
|
|
407
|
+
});
|
|
388
408
|
return;
|
|
389
409
|
}
|
|
390
410
|
if (method === 'POST' && path === '/annotation') {
|
|
@@ -30,6 +30,8 @@ var REG_CHROME_KEY = `HKCU\\Software\\Google\\Chrome\\NativeMessagingHosts\\${NA
|
|
|
30
30
|
var REG_EDGE_KEY = `HKCU\\Software\\Microsoft\\Edge\\NativeMessagingHosts\\${NATIVE_HOST_NAME}`;
|
|
31
31
|
var REG_FIREFOX_KEY = `HKCU\\Software\\Mozilla\\NativeMessagingHosts\\${NATIVE_HOST_NAME}`;
|
|
32
32
|
var DEFAULT_GECKO_ID = "stikfix@stikfix.com";
|
|
33
|
+
var CONFIG_DIR = (home) => (0, import_node_path2.join)(home, ".config", "stikfix");
|
|
34
|
+
var CONFIG_PATH = (home) => (0, import_node_path2.join)(CONFIG_DIR(home), "config.json");
|
|
33
35
|
var LAUNCHER_BATCH_FILENAME = "stikfix-host.bat";
|
|
34
36
|
var LAUNCHER_VBS_FILENAME = "stikfix-host.vbs";
|
|
35
37
|
var LAUNCHER_LNK_FILENAME = "Stikfix Host.lnk";
|
|
@@ -176,14 +178,17 @@ function createLauncherFiles(opts) {
|
|
|
176
178
|
const hostEntry = opts.hostEntryPath;
|
|
177
179
|
const rootArg = opts.root;
|
|
178
180
|
const portArg = opts.port !== void 0 ? ` --port ${opts.port}` : "";
|
|
181
|
+
const useExe = typeof opts.hostExe === "string" && opts.hostExe.length > 0;
|
|
182
|
+
const hostExe = opts.hostExe ?? "";
|
|
179
183
|
if (plat === "win32") {
|
|
184
|
+
const batchLaunch = useExe ? `"${hostExe}" serve --root "${rootArg}"${portArg}` : `"${nodeCmd}" "${hostEntry}" --root "${rootArg}"${portArg}`;
|
|
180
185
|
const batchContent = [
|
|
181
186
|
"@echo off",
|
|
182
187
|
`rem Stikfix HTTP host launcher \u2014 double-click to start the backend`,
|
|
183
188
|
`rem Generated by: npx stikfix init`,
|
|
184
189
|
`rem Root: ${rootArg}`,
|
|
185
190
|
``,
|
|
186
|
-
|
|
191
|
+
batchLaunch,
|
|
187
192
|
`if %ERRORLEVEL% NEQ 0 pause`
|
|
188
193
|
].join("\r\n");
|
|
189
194
|
const batchPath = paths.launcher;
|
|
@@ -191,17 +196,20 @@ function createLauncherFiles(opts) {
|
|
|
191
196
|
written.push(batchPath);
|
|
192
197
|
const vq = (s) => s.replace(/"/g, '""');
|
|
193
198
|
const vbsHostEntry = vq(hostEntry);
|
|
199
|
+
const vbsHostExe = vq(hostExe);
|
|
194
200
|
const vbsRoot = vq(rootArg);
|
|
195
201
|
const vbsPortArg = opts.port !== void 0 ? ` --port ${opts.port}` : "";
|
|
202
|
+
const vbsRunLine = useExe ? `sh.Run "cmd /c """ & hostExe & """ serve --root """ & root & """${vbsPortArg}", 0, False` : `sh.Run "cmd /c node """ & hostEntry & """ --root """ & root & """${vbsPortArg}", 0, False`;
|
|
196
203
|
const vbsContent = [
|
|
197
204
|
"Option Explicit",
|
|
198
|
-
"Dim sh, fso, hostEntry, root, portFile, msg, port, f",
|
|
205
|
+
"Dim sh, fso, hostEntry, hostExe, root, portFile, msg, port, f",
|
|
199
206
|
'Set sh = CreateObject("WScript.Shell")',
|
|
200
207
|
'Set fso = CreateObject("Scripting.FileSystemObject")',
|
|
201
208
|
`hostEntry = "${vbsHostEntry}"`,
|
|
209
|
+
`hostExe = "${vbsHostExe}"`,
|
|
202
210
|
`root = "${vbsRoot}"`,
|
|
203
211
|
"' Launch the host hidden (window style 0), do not wait",
|
|
204
|
-
|
|
212
|
+
vbsRunLine,
|
|
205
213
|
"' Give it a moment to bind and write the port file",
|
|
206
214
|
"WScript.Sleep 1800",
|
|
207
215
|
'msg = "Stikfix host is running." & vbCrLf & vbCrLf & "You can start dropping notes."',
|
|
@@ -218,21 +226,22 @@ function createLauncherFiles(opts) {
|
|
|
218
226
|
(0, import_node_fs.writeFileSync)(vbsPath, vbsContent, { encoding: "utf8" });
|
|
219
227
|
written.push(vbsPath);
|
|
220
228
|
if (paths.shortcut) {
|
|
221
|
-
const
|
|
229
|
+
const guessedLnkPath = paths.shortcut;
|
|
222
230
|
const iconPath = opts.iconPath ?? "";
|
|
223
|
-
const
|
|
231
|
+
const safeLnkFileName = LAUNCHER_LNK_FILENAME.replace(/'/g, "''");
|
|
224
232
|
const safeIconPath = iconPath.replace(/'/g, "''");
|
|
225
233
|
const wscriptPath = (0, import_node_path2.join)(process.env["SystemRoot"] ?? "C:\\Windows", "System32", "wscript.exe");
|
|
226
234
|
const safeWscript = wscriptPath.replace(/'/g, "''");
|
|
227
235
|
const safeVbsPath = vbsPath.replace(/'/g, "''");
|
|
228
|
-
const psScript = `$ws = New-Object -ComObject WScript.Shell;$s = $ws.CreateShortcut(
|
|
236
|
+
const psScript = `$desktop = [Environment]::GetFolderPath('Desktop');$lnkPath = Join-Path $desktop '${safeLnkFileName}';$ws = New-Object -ComObject WScript.Shell;$s = $ws.CreateShortcut($lnkPath);$s.TargetPath = '${safeWscript}';$s.Arguments = '"${safeVbsPath}"';$s.Description = 'Start the Stikfix HTTP backend host';` + (safeIconPath ? `$s.IconLocation = '${safeIconPath},0';` : "") + `$s.Save();Write-Output $lnkPath`;
|
|
229
237
|
try {
|
|
230
|
-
(0, import_node_child_process.execFileSync)(
|
|
238
|
+
const stdout = (0, import_node_child_process.execFileSync)(
|
|
231
239
|
"powershell.exe",
|
|
232
240
|
["-NoProfile", "-NonInteractive", "-Command", psScript],
|
|
233
|
-
{ timeout: 15e3,
|
|
241
|
+
{ timeout: 15e3, encoding: "utf8" }
|
|
234
242
|
);
|
|
235
|
-
|
|
243
|
+
const resolvedLnkPath = stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).pop() ?? guessedLnkPath;
|
|
244
|
+
written.push(resolvedLnkPath);
|
|
236
245
|
} catch (err) {
|
|
237
246
|
warnings.push(
|
|
238
247
|
`Desktop shortcut creation failed (non-fatal): ${String(err.message)}. The batch file at ${batchPath} can be used directly.`
|
|
@@ -245,7 +254,7 @@ function createLauncherFiles(opts) {
|
|
|
245
254
|
`# Stikfix HTTP host launcher \u2014 double-click in Finder or drag to Dock`,
|
|
246
255
|
`# Generated by: npx stikfix init`,
|
|
247
256
|
``,
|
|
248
|
-
`exec "${nodeCmd}" "${hostEntry}" --root "${rootArg}"${portArg}`
|
|
257
|
+
useExe ? `exec "${hostExe}" serve --root "${rootArg}"${portArg}` : `exec "${nodeCmd}" "${hostEntry}" --root "${rootArg}"${portArg}`
|
|
249
258
|
].join("\n");
|
|
250
259
|
const commandPath = paths.launcher;
|
|
251
260
|
(0, import_node_fs.writeFileSync)(commandPath, commandContent, { encoding: "utf8", mode: 493 });
|
|
@@ -256,7 +265,7 @@ function createLauncherFiles(opts) {
|
|
|
256
265
|
`# Stikfix HTTP host launcher`,
|
|
257
266
|
`# Generated by: npx stikfix init`,
|
|
258
267
|
``,
|
|
259
|
-
`exec "${nodeCmd}" "${hostEntry}" --root "${rootArg}"${portArg}`
|
|
268
|
+
useExe ? `exec "${hostExe}" serve --root "${rootArg}"${portArg}` : `exec "${nodeCmd}" "${hostEntry}" --root "${rootArg}"${portArg}`
|
|
260
269
|
].join("\n");
|
|
261
270
|
const shPath = paths.launcher;
|
|
262
271
|
(0, import_node_fs.writeFileSync)(shPath, shContent, { encoding: "utf8" });
|
|
@@ -321,8 +330,8 @@ function registerNativeHost(opts) {
|
|
|
321
330
|
const home = opts.home ?? (0, import_node_os.homedir)();
|
|
322
331
|
const browser = opts.browser ?? "chrome";
|
|
323
332
|
const manifestPath = nativeManifestPath(plat, home, browser);
|
|
324
|
-
const
|
|
325
|
-
const manifest = buildManifest(opts.extensionId,
|
|
333
|
+
const manifestTarget = opts.directPath ? (0, import_node_path2.resolve)(opts.hostBinPath) : writeNativeWrapper(opts.hostBinPath, plat, home, browser);
|
|
334
|
+
const manifest = buildManifest(opts.extensionId, manifestTarget, browser);
|
|
326
335
|
writeManifest(manifest, manifestPath);
|
|
327
336
|
if (plat === "win32") {
|
|
328
337
|
const execReg = opts.execReg ?? ((args) => {
|
|
@@ -373,6 +382,34 @@ function unregisterNativeHost(opts = {}) {
|
|
|
373
382
|
}
|
|
374
383
|
}
|
|
375
384
|
}
|
|
385
|
+
function teardownHost(opts = {}) {
|
|
386
|
+
const plat = opts.plat ?? process.platform;
|
|
387
|
+
const home = opts.home ?? (0, import_node_os.homedir)();
|
|
388
|
+
const browser = opts.browser ?? "chrome";
|
|
389
|
+
const result = {
|
|
390
|
+
manifestRemoved: true,
|
|
391
|
+
configPath: CONFIG_PATH(home),
|
|
392
|
+
configRemoved: false
|
|
393
|
+
};
|
|
394
|
+
try {
|
|
395
|
+
unregisterNativeHost({ plat, home, browser });
|
|
396
|
+
} catch (err) {
|
|
397
|
+
result.manifestRemoved = false;
|
|
398
|
+
result.manifestError = err instanceof Error ? err.message : String(err);
|
|
399
|
+
}
|
|
400
|
+
try {
|
|
401
|
+
unregisterStartup({ plat, home });
|
|
402
|
+
} catch (err) {
|
|
403
|
+
result.startupError = err instanceof Error ? err.message : String(err);
|
|
404
|
+
}
|
|
405
|
+
try {
|
|
406
|
+
(0, import_node_fs.rmSync)(result.configPath, { force: true });
|
|
407
|
+
result.configRemoved = true;
|
|
408
|
+
} catch {
|
|
409
|
+
result.configRemoved = false;
|
|
410
|
+
}
|
|
411
|
+
return result;
|
|
412
|
+
}
|
|
376
413
|
|
|
377
414
|
// host/src/extension-id.ts
|
|
378
415
|
var import_node_crypto2 = require("node:crypto");
|
|
@@ -430,8 +467,8 @@ function resolveStartupChoice(forceOn, forceOff, isTTY) {
|
|
|
430
467
|
}
|
|
431
468
|
return false;
|
|
432
469
|
}
|
|
433
|
-
var
|
|
434
|
-
var
|
|
470
|
+
var CONFIG_DIR2 = (0, import_node_path3.join)((0, import_node_os2.homedir)(), ".config", "stikfix");
|
|
471
|
+
var CONFIG_PATH2 = (0, import_node_path3.join)(CONFIG_DIR2, "config.json");
|
|
435
472
|
if (subcommand === "init") {
|
|
436
473
|
const rawRoot = values["root"];
|
|
437
474
|
const rawExtId = values["extension-id"];
|
|
@@ -449,9 +486,9 @@ if (subcommand === "init") {
|
|
|
449
486
|
const name = (0, import_node_path3.basename)(root);
|
|
450
487
|
const notesDir = (0, import_node_path3.join)(root, "notes");
|
|
451
488
|
const hostEntryPath = (0, import_node_path3.resolve)((0, import_node_path3.join)(__dirname, "src", "index.js"));
|
|
452
|
-
(0, import_node_fs2.mkdirSync)(
|
|
489
|
+
(0, import_node_fs2.mkdirSync)(CONFIG_DIR2, { recursive: true });
|
|
453
490
|
const config = { root, name, notesDir, hostEntry: hostEntryPath, nodePath: process.execPath };
|
|
454
|
-
(0, import_node_fs2.writeFileSync)(
|
|
491
|
+
(0, import_node_fs2.writeFileSync)(CONFIG_PATH2, JSON.stringify(config, null, 2), { encoding: "utf8", mode: 384 });
|
|
455
492
|
const hostBinPath = (0, import_node_path3.resolve)((0, import_node_path3.join)(__dirname, "stikfix-native.cjs"));
|
|
456
493
|
try {
|
|
457
494
|
registerNativeHost({ extensionId, hostBinPath, browser });
|
|
@@ -540,9 +577,9 @@ if (subcommand === "init") {
|
|
|
540
577
|
console.log("");
|
|
541
578
|
console.log(" 2. Start the backend \u2014 double-click the desktop launcher:");
|
|
542
579
|
if (process.platform === "win32") {
|
|
543
|
-
const lnkPath =
|
|
580
|
+
const lnkPath = launcherResult.written.find((p) => p.endsWith(".lnk")) ?? "";
|
|
544
581
|
const batchPath = launcherResult.written.find((p) => p.endsWith(".bat")) ?? "";
|
|
545
|
-
if ((0, import_node_fs2.existsSync)(lnkPath)) {
|
|
582
|
+
if (lnkPath && (0, import_node_fs2.existsSync)(lnkPath)) {
|
|
546
583
|
console.log(' Desktop shortcut: "Stikfix Host" (icon on your Desktop)');
|
|
547
584
|
} else if (batchPath) {
|
|
548
585
|
console.log(" Batch file: " + batchPath);
|
|
@@ -567,17 +604,13 @@ if (subcommand === "init") {
|
|
|
567
604
|
console.log(" npx --yes stikfix@latest init --root " + root);
|
|
568
605
|
} else if (subcommand === "uninstall") {
|
|
569
606
|
const browser = resolveBrowser(values["browser"]);
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
console.error("stikfix uninstall: error removing native-host manifest:", String(err));
|
|
607
|
+
const teardown = teardownHost({ browser });
|
|
608
|
+
if (teardown.manifestError) {
|
|
609
|
+
console.error("stikfix uninstall: error removing native-host manifest:", teardown.manifestError);
|
|
574
610
|
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
} catch (err) {
|
|
578
|
-
console.error("stikfix uninstall: error removing startup entry:", String(err));
|
|
611
|
+
if (teardown.startupError) {
|
|
612
|
+
console.error("stikfix uninstall: error removing startup entry:", teardown.startupError);
|
|
579
613
|
}
|
|
580
|
-
(0, import_node_fs2.rmSync)(CONFIG_PATH, { force: true });
|
|
581
614
|
console.log("stikfix: native host unregistered.");
|
|
582
615
|
console.log(" manifest removed");
|
|
583
616
|
console.log(" launcher files removed");
|