omnikey-cli 1.0.16 → 1.0.18
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 +73 -3
- package/backend-dist/agent/agentPrompts.js +40 -73
- package/backend-dist/agent/agentServer.js +130 -126
- package/dist/daemon.js +99 -44
- package/dist/index.js +25 -2
- package/dist/removeConfig.js +44 -18
- package/dist/setConfig.js +19 -0
- package/dist/showConfig.js +45 -0
- package/package.json +1 -1
- package/src/daemon.ts +107 -49
- package/src/index.ts +28 -2
- package/src/removeConfig.ts +39 -21
- package/src/setConfig.ts +16 -0
- package/src/showConfig.ts +47 -0
package/dist/daemon.js
CHANGED
|
@@ -4,18 +4,18 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.startDaemon = startDaemon;
|
|
7
|
-
const child_process_1 = require("child_process");
|
|
8
7
|
const path_1 = __importDefault(require("path"));
|
|
9
8
|
const fs_1 = __importDefault(require("fs"));
|
|
10
|
-
const
|
|
9
|
+
const child_process_1 = require("child_process");
|
|
10
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
11
11
|
const utils_1 = require("./utils");
|
|
12
12
|
/**
|
|
13
13
|
* Start the Omnikey API backend as a daemon on the specified port.
|
|
14
14
|
* On macOS: creates and registers a launchd agent for persistence.
|
|
15
|
-
* On Windows:
|
|
15
|
+
* On Windows: installs an NSSM Windows service for boot-time persistence.
|
|
16
16
|
* @param port The port to run the backend on
|
|
17
17
|
*/
|
|
18
|
-
function startDaemon(port = 7071) {
|
|
18
|
+
async function startDaemon(port = 7071) {
|
|
19
19
|
const backendPath = path_1.default.resolve(__dirname, '../backend-dist/index.js');
|
|
20
20
|
const configDir = (0, utils_1.getConfigDir)();
|
|
21
21
|
const configPath = (0, utils_1.getConfigPath)();
|
|
@@ -33,7 +33,7 @@ function startDaemon(port = 7071) {
|
|
|
33
33
|
const logPath = path_1.default.join(configDir, 'daemon.log');
|
|
34
34
|
const errorLogPath = path_1.default.join(configDir, 'daemon-error.log');
|
|
35
35
|
if (utils_1.isWindows) {
|
|
36
|
-
startDaemonWindows({
|
|
36
|
+
await startDaemonWindows({
|
|
37
37
|
port,
|
|
38
38
|
configDir,
|
|
39
39
|
configVars,
|
|
@@ -47,53 +47,108 @@ function startDaemon(port = 7071) {
|
|
|
47
47
|
startDaemonMacOS({ port, configDir, configVars, nodePath, backendPath, logPath, errorLogPath });
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
-
function
|
|
51
|
-
const { port, configDir, configVars, nodePath, backendPath, logPath, errorLogPath } = opts;
|
|
52
|
-
// Write a wrapper .cmd script that sets env vars and launches the backend
|
|
53
|
-
const wrapperPath = path_1.default.join(configDir, 'start-daemon.cmd');
|
|
54
|
-
const envSetLines = Object.entries({ ...configVars, OMNIKEY_PORT: String(port) })
|
|
55
|
-
.map(([k, v]) => `set "${k}=${v}"`)
|
|
56
|
-
.join('\r\n');
|
|
57
|
-
const wrapperContent = [
|
|
58
|
-
'@echo off',
|
|
59
|
-
envSetLines,
|
|
60
|
-
`"${nodePath}" "${backendPath}" >> "${logPath}" 2>> "${errorLogPath}"`,
|
|
61
|
-
'',
|
|
62
|
-
].join('\r\n');
|
|
50
|
+
function resolveNssm() {
|
|
63
51
|
try {
|
|
64
|
-
|
|
65
|
-
fs_1.default.writeFileSync(wrapperPath, wrapperContent, 'utf-8');
|
|
52
|
+
return (0, child_process_1.execSync)('where nssm', { stdio: 'pipe' }).toString().trim().split('\n')[0].trim();
|
|
66
53
|
}
|
|
67
|
-
catch
|
|
68
|
-
|
|
69
|
-
return;
|
|
54
|
+
catch {
|
|
55
|
+
return null;
|
|
70
56
|
}
|
|
71
|
-
|
|
72
|
-
|
|
57
|
+
}
|
|
58
|
+
async function startDaemonWindows(opts) {
|
|
59
|
+
const { port, configDir, configVars, nodePath, backendPath, logPath, errorLogPath } = opts;
|
|
60
|
+
const serviceName = 'OmnikeyDaemon';
|
|
61
|
+
let nssmPath = resolveNssm();
|
|
62
|
+
if (!nssmPath) {
|
|
63
|
+
const { install } = await inquirer_1.default.prompt([
|
|
64
|
+
{
|
|
65
|
+
type: 'confirm',
|
|
66
|
+
name: 'install',
|
|
67
|
+
message: 'NSSM is required but not found. Install it now via winget?',
|
|
68
|
+
default: true,
|
|
69
|
+
},
|
|
70
|
+
]);
|
|
71
|
+
if (!install) {
|
|
72
|
+
console.log('Aborted. Install NSSM manually and re-run in an elevated (Administrator) terminal.');
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
console.log('Installing NSSM via winget...');
|
|
76
|
+
try {
|
|
77
|
+
(0, child_process_1.execSync)('winget install nssm --accept-package-agreements --accept-source-agreements', {
|
|
78
|
+
stdio: 'inherit',
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
console.error('winget install failed:', e?.message ?? e);
|
|
83
|
+
console.log('Try manually: scoop install nssm or choco install nssm');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
// winget updates the machine PATH in the registry but the current process
|
|
87
|
+
// won't see it — spawn a fresh cmd to resolve the new location.
|
|
88
|
+
try {
|
|
89
|
+
nssmPath = (0, child_process_1.execSync)('cmd /c where nssm', { stdio: 'pipe' })
|
|
90
|
+
.toString().trim().split('\n')[0].trim();
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
nssmPath = null;
|
|
94
|
+
}
|
|
95
|
+
if (!nssmPath) {
|
|
96
|
+
console.log('NSSM installed successfully.');
|
|
97
|
+
console.log('Please open a new elevated (Administrator) terminal and re-run this command.');
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
(0, utils_1.initLogFiles)(logPath, errorLogPath);
|
|
102
|
+
// Remove any existing service (stop first, then remove)
|
|
73
103
|
try {
|
|
74
|
-
|
|
75
|
-
(0, child_process_2.execSync)(`schtasks /delete /tn "${taskName}" /f`, { stdio: 'pipe' });
|
|
104
|
+
(0, child_process_1.execFileSync)(nssmPath, ['stop', serviceName], { stdio: 'pipe' });
|
|
76
105
|
}
|
|
77
|
-
catch {
|
|
78
|
-
|
|
106
|
+
catch { /* not running */ }
|
|
107
|
+
try {
|
|
108
|
+
(0, child_process_1.execFileSync)(nssmPath, ['remove', serviceName, 'confirm'], { stdio: 'pipe' });
|
|
79
109
|
}
|
|
110
|
+
catch { /* didn't exist */ }
|
|
111
|
+
// NSSM services run as LocalSystem; pass USERPROFILE so the backend's
|
|
112
|
+
// getHomeDir() resolves to the correct user config directory.
|
|
113
|
+
const env = {
|
|
114
|
+
...configVars,
|
|
115
|
+
OMNIKEY_PORT: String(port),
|
|
116
|
+
USERPROFILE: process.env.USERPROFILE || configDir.replace(/[/\\]\.omnikey$/, ''),
|
|
117
|
+
HOME: process.env.USERPROFILE || configDir.replace(/[/\\]\.omnikey$/, ''),
|
|
118
|
+
};
|
|
80
119
|
try {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
120
|
+
// Install: nssm install <name> <application> [args...]
|
|
121
|
+
(0, child_process_1.execFileSync)(nssmPath, ['install', serviceName, nodePath, backendPath], { stdio: 'pipe' });
|
|
122
|
+
(0, child_process_1.execFileSync)(nssmPath, ['set', serviceName, 'AppDirectory', configDir], { stdio: 'pipe' });
|
|
123
|
+
// Pass all env vars in a single call (replaces the entire AppEnvironmentExtra key)
|
|
124
|
+
const envEntries = Object.entries(env).map(([k, v]) => `${k}=${v}`);
|
|
125
|
+
(0, child_process_1.execFileSync)(nssmPath, ['set', serviceName, 'AppEnvironmentExtra', ...envEntries], { stdio: 'pipe' });
|
|
126
|
+
(0, child_process_1.execFileSync)(nssmPath, ['set', serviceName, 'AppStdout', logPath], { stdio: 'pipe' });
|
|
127
|
+
(0, child_process_1.execFileSync)(nssmPath, ['set', serviceName, 'AppStderr', errorLogPath], { stdio: 'pipe' });
|
|
128
|
+
(0, child_process_1.execFileSync)(nssmPath, ['set', serviceName, 'AppRotateFiles', '1'], { stdio: 'pipe' });
|
|
129
|
+
// Restart automatically after a 3-second delay on any exit
|
|
130
|
+
(0, child_process_1.execFileSync)(nssmPath, ['set', serviceName, 'AppExit', 'Default', 'Restart'], { stdio: 'pipe' });
|
|
131
|
+
(0, child_process_1.execFileSync)(nssmPath, ['set', serviceName, 'AppRestartDelay', '3000'], { stdio: 'pipe' });
|
|
132
|
+
// Start automatically at boot (no login required)
|
|
133
|
+
(0, child_process_1.execFileSync)(nssmPath, ['set', serviceName, 'Start', 'SERVICE_AUTO_START'], { stdio: 'pipe' });
|
|
134
|
+
(0, child_process_1.execFileSync)(nssmPath, ['set', serviceName, 'DisplayName', 'Omnikey API Backend'], { stdio: 'pipe' });
|
|
135
|
+
(0, child_process_1.execFileSync)(nssmPath, ['set', serviceName, 'Description', 'Omnikey API Backend Daemon'], { stdio: 'pipe' });
|
|
136
|
+
(0, child_process_1.execFileSync)(nssmPath, ['start', serviceName], { stdio: 'pipe' });
|
|
137
|
+
console.log(`NSSM service installed and started: ${serviceName}`);
|
|
138
|
+
console.log('Omnikey daemon runs on boot, without login, and auto-restarts on crash.');
|
|
139
|
+
console.log(`Logs: ${logPath}`);
|
|
140
|
+
console.log(` ${errorLogPath}`);
|
|
84
141
|
}
|
|
85
142
|
catch (e) {
|
|
86
|
-
|
|
143
|
+
const msg = e?.stderr?.toString() || e?.message || String(e);
|
|
144
|
+
if (msg.toLowerCase().includes('access') || msg.toLowerCase().includes('privilege')) {
|
|
145
|
+
console.error('Failed to install NSSM service: administrator privileges are required.');
|
|
146
|
+
console.error('Re-run this command in an elevated (Administrator) terminal.');
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
console.error('Failed to install NSSM service:', msg);
|
|
150
|
+
}
|
|
87
151
|
}
|
|
88
|
-
// Also start the backend immediately for the current session
|
|
89
|
-
const { out, err } = (0, utils_1.initLogFiles)(logPath, errorLogPath);
|
|
90
|
-
const child = (0, child_process_1.spawn)(nodePath, [backendPath], {
|
|
91
|
-
env: { ...configVars, OMNIKEY_PORT: String(port) },
|
|
92
|
-
detached: true,
|
|
93
|
-
stdio: ['ignore', out, err],
|
|
94
|
-
});
|
|
95
|
-
child.unref();
|
|
96
|
-
console.log(`Omnikey API backend started as a daemon on port ${port}. PID: ${child.pid}`);
|
|
97
152
|
}
|
|
98
153
|
function startDaemonMacOS(opts) {
|
|
99
154
|
const { port, configDir, configVars, nodePath, backendPath, logPath, errorLogPath } = opts;
|
|
@@ -136,8 +191,8 @@ function startDaemonMacOS(opts) {
|
|
|
136
191
|
fs_1.default.mkdirSync(launchAgentsDir, { recursive: true });
|
|
137
192
|
fs_1.default.writeFileSync(plistPath, plistContent, 'utf-8');
|
|
138
193
|
(0, utils_1.initLogFiles)(logPath, errorLogPath);
|
|
139
|
-
(0,
|
|
140
|
-
(0,
|
|
194
|
+
(0, child_process_1.execSync)(`launchctl unload "${plistPath}" || true`);
|
|
195
|
+
(0, child_process_1.execSync)(`launchctl load "${plistPath}"`);
|
|
141
196
|
console.log(`Launch agent created and loaded: ${plistPath}`);
|
|
142
197
|
console.log('Omnikey daemon will auto-restart and persist across reboots.');
|
|
143
198
|
// launchd starts the process via RunAtLoad — no manual spawn needed here.
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,8 @@ const killDaemon_1 = require("./killDaemon");
|
|
|
8
8
|
const removeConfig_1 = require("./removeConfig");
|
|
9
9
|
const status_1 = require("./status");
|
|
10
10
|
const showLogs_1 = require("./showLogs");
|
|
11
|
+
const showConfig_1 = require("./showConfig");
|
|
12
|
+
const setConfig_1 = require("./setConfig");
|
|
11
13
|
const program = new commander_1.Command();
|
|
12
14
|
program
|
|
13
15
|
.name('omnikey')
|
|
@@ -23,9 +25,9 @@ program
|
|
|
23
25
|
.command('daemon')
|
|
24
26
|
.description('Start the Omnikey API backend as a daemon on a specified port')
|
|
25
27
|
.option('--port <port>', 'Port to run the backend on', '7071')
|
|
26
|
-
.action((options) => {
|
|
28
|
+
.action(async (options) => {
|
|
27
29
|
const port = Number(options.port) || 7071;
|
|
28
|
-
(0, daemon_1.startDaemon)(port);
|
|
30
|
+
await (0, daemon_1.startDaemon)(port);
|
|
29
31
|
});
|
|
30
32
|
program
|
|
31
33
|
.command('kill-daemon')
|
|
@@ -56,4 +58,25 @@ program
|
|
|
56
58
|
const errorsOnly = !!options.errors;
|
|
57
59
|
(0, showLogs_1.showLogs)(lines, errorsOnly);
|
|
58
60
|
});
|
|
61
|
+
program
|
|
62
|
+
.command('config')
|
|
63
|
+
.description('Show the current Omnikey configuration (API keys are masked)')
|
|
64
|
+
.action(() => {
|
|
65
|
+
(0, showConfig_1.showConfig)();
|
|
66
|
+
});
|
|
67
|
+
program
|
|
68
|
+
.command('set <key> <value>')
|
|
69
|
+
.description('Set a single configuration key (e.g. omnikey set OMNIKEY_PORT 8080)')
|
|
70
|
+
.action((key, value) => {
|
|
71
|
+
(0, setConfig_1.setConfig)(key, value);
|
|
72
|
+
});
|
|
73
|
+
program
|
|
74
|
+
.command('restart-daemon')
|
|
75
|
+
.description('Restart the Omnikey API backend daemon')
|
|
76
|
+
.option('--port <port>', 'Port to run the backend on', '7071')
|
|
77
|
+
.action(async (options) => {
|
|
78
|
+
(0, killDaemon_1.killDaemon)();
|
|
79
|
+
const port = Number(options.port) || 7071;
|
|
80
|
+
await (0, daemon_1.startDaemon)(port);
|
|
81
|
+
});
|
|
59
82
|
program.parseAsync(process.argv);
|
package/dist/removeConfig.js
CHANGED
|
@@ -29,29 +29,47 @@ function killLaunchdAgent() {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
function killWindowsTask() {
|
|
32
|
-
const
|
|
32
|
+
const serviceName = 'OmnikeyDaemon';
|
|
33
|
+
// Try NSSM first (current implementation)
|
|
34
|
+
let nssmPath = null;
|
|
33
35
|
try {
|
|
34
|
-
(0, child_process_1.execSync)(
|
|
36
|
+
nssmPath = (0, child_process_1.execSync)('where nssm', { stdio: 'pipe' }).toString().trim().split('\n')[0].trim();
|
|
35
37
|
}
|
|
36
|
-
catch {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
catch { /* NSSM not installed */ }
|
|
39
|
+
if (nssmPath) {
|
|
40
|
+
try {
|
|
41
|
+
(0, child_process_1.execFileSync)(nssmPath, ['stop', serviceName], { stdio: 'pipe' });
|
|
42
|
+
}
|
|
43
|
+
catch { /* not running */ }
|
|
44
|
+
try {
|
|
45
|
+
(0, child_process_1.execFileSync)(nssmPath, ['remove', serviceName, 'confirm'], { stdio: 'pipe' });
|
|
46
|
+
console.log(`Removed NSSM service: ${serviceName}`);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
console.log(`NSSM service does not exist: ${serviceName}`);
|
|
50
|
+
}
|
|
42
51
|
}
|
|
43
|
-
|
|
44
|
-
|
|
52
|
+
else {
|
|
53
|
+
// Fallback: remove legacy Task Scheduler task from previous installs
|
|
54
|
+
try {
|
|
55
|
+
(0, child_process_1.execSync)(`schtasks /end /tn "${serviceName}"`, { stdio: 'pipe' });
|
|
56
|
+
}
|
|
57
|
+
catch { /* not running */ }
|
|
58
|
+
try {
|
|
59
|
+
(0, child_process_1.execSync)(`schtasks /delete /tn "${serviceName}" /f`, { stdio: 'pipe' });
|
|
60
|
+
console.log(`Removed Windows Task Scheduler task: ${serviceName}`);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
console.log(`Windows Task Scheduler task does not exist: ${serviceName}`);
|
|
64
|
+
}
|
|
45
65
|
}
|
|
46
|
-
//
|
|
66
|
+
// Remove legacy wrapper script if present
|
|
47
67
|
const wrapperPath = path_1.default.join((0, utils_1.getConfigDir)(), 'start-daemon.cmd');
|
|
48
68
|
if (fs_1.default.existsSync(wrapperPath)) {
|
|
49
69
|
try {
|
|
50
70
|
fs_1.default.rmSync(wrapperPath);
|
|
51
71
|
}
|
|
52
|
-
catch {
|
|
53
|
-
// Ignore
|
|
54
|
-
}
|
|
72
|
+
catch { /* ignore */ }
|
|
55
73
|
}
|
|
56
74
|
}
|
|
57
75
|
/**
|
|
@@ -117,14 +135,22 @@ function removeConfigAndDb(includeDb = false) {
|
|
|
117
135
|
else {
|
|
118
136
|
console.log('Skipping SQLite database removal (use --db to remove it).');
|
|
119
137
|
}
|
|
120
|
-
// Remove .omnikey
|
|
138
|
+
// Remove all files/folders inside .omnikey except the SQLite database
|
|
121
139
|
if (fs_1.default.existsSync(configDir)) {
|
|
122
140
|
try {
|
|
123
|
-
fs_1.default.
|
|
124
|
-
|
|
141
|
+
const entries = fs_1.default.readdirSync(configDir);
|
|
142
|
+
for (const entry of entries) {
|
|
143
|
+
if (entry.endsWith('.sqlite')) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const entryPath = path_1.default.join(configDir, entry);
|
|
147
|
+
fs_1.default.rmSync(entryPath, { recursive: true, force: true });
|
|
148
|
+
console.log(`Removed: ${entryPath}`);
|
|
149
|
+
}
|
|
150
|
+
console.log(`Cleared config directory (SQLite preserved): ${configDir}`);
|
|
125
151
|
}
|
|
126
152
|
catch (e) {
|
|
127
|
-
console.error(`Failed to
|
|
153
|
+
console.error(`Failed to clear config directory: ${e}`);
|
|
128
154
|
}
|
|
129
155
|
}
|
|
130
156
|
else {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.setConfig = setConfig;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const utils_1 = require("./utils");
|
|
9
|
+
function setConfig(key, value) {
|
|
10
|
+
const configDir = (0, utils_1.getConfigDir)();
|
|
11
|
+
const configPath = (0, utils_1.getConfigPath)();
|
|
12
|
+
if (!fs_1.default.existsSync(configDir)) {
|
|
13
|
+
fs_1.default.mkdirSync(configDir, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
const config = (0, utils_1.readConfig)();
|
|
16
|
+
config[key] = value;
|
|
17
|
+
fs_1.default.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
18
|
+
console.log(`Set ${key} in ${configPath}`);
|
|
19
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.showConfig = showConfig;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const utils_1 = require("./utils");
|
|
9
|
+
const API_KEY_FIELDS = [
|
|
10
|
+
'OPENAI_API_KEY',
|
|
11
|
+
'ANTHROPIC_API_KEY',
|
|
12
|
+
'GEMINI_API_KEY',
|
|
13
|
+
'SERPER_API_KEY',
|
|
14
|
+
'BRAVE_SEARCH_API_KEY',
|
|
15
|
+
'TAVILY_API_KEY',
|
|
16
|
+
];
|
|
17
|
+
function maskSecret(value) {
|
|
18
|
+
if (value.length <= 8)
|
|
19
|
+
return '****';
|
|
20
|
+
return value.slice(0, 4) + '****' + value.slice(-4);
|
|
21
|
+
}
|
|
22
|
+
function showConfig() {
|
|
23
|
+
const configPath = (0, utils_1.getConfigPath)();
|
|
24
|
+
const configDir = (0, utils_1.getConfigDir)();
|
|
25
|
+
if (!fs_1.default.existsSync(configPath)) {
|
|
26
|
+
console.log('No configuration found. Run `omnikey onboard` to get started.');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const config = (0, utils_1.readConfig)();
|
|
30
|
+
const keys = Object.keys(config);
|
|
31
|
+
if (keys.length === 0) {
|
|
32
|
+
console.log('Configuration file exists but is empty.');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
console.log(`Config file: ${configPath}\n`);
|
|
36
|
+
console.log('Current configuration:');
|
|
37
|
+
console.log('─'.repeat(50));
|
|
38
|
+
for (const key of keys) {
|
|
39
|
+
const raw = String(config[key]);
|
|
40
|
+
const display = API_KEY_FIELDS.includes(key) ? maskSecret(raw) : raw;
|
|
41
|
+
console.log(` ${key}: ${display}`);
|
|
42
|
+
}
|
|
43
|
+
console.log('─'.repeat(50));
|
|
44
|
+
console.log(`\nConfig directory: ${configDir}`);
|
|
45
|
+
}
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"access": "public",
|
|
5
5
|
"registry": "https://registry.npmjs.org/"
|
|
6
6
|
},
|
|
7
|
-
"version": "1.0.
|
|
7
|
+
"version": "1.0.18",
|
|
8
8
|
"description": "CLI for onboarding users to Omnikey AI and configuring OPENAI_API_KEY. Use Yarn for install/build.",
|
|
9
9
|
"engines": {
|
|
10
10
|
"node": ">=14.0.0",
|
package/src/daemon.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { spawn } from 'child_process';
|
|
2
1
|
import path from 'path';
|
|
3
2
|
import fs from 'fs';
|
|
4
|
-
import { execSync } from 'child_process';
|
|
3
|
+
import { execSync, execFileSync } from 'child_process';
|
|
4
|
+
import inquirer from 'inquirer';
|
|
5
5
|
import {
|
|
6
6
|
isWindows,
|
|
7
7
|
getHomeDir,
|
|
@@ -14,10 +14,10 @@ import {
|
|
|
14
14
|
/**
|
|
15
15
|
* Start the Omnikey API backend as a daemon on the specified port.
|
|
16
16
|
* On macOS: creates and registers a launchd agent for persistence.
|
|
17
|
-
* On Windows:
|
|
17
|
+
* On Windows: installs an NSSM Windows service for boot-time persistence.
|
|
18
18
|
* @param port The port to run the backend on
|
|
19
19
|
*/
|
|
20
|
-
export function startDaemon(port: number = 7071) {
|
|
20
|
+
export async function startDaemon(port: number = 7071) {
|
|
21
21
|
const backendPath = path.resolve(__dirname, '../backend-dist/index.js');
|
|
22
22
|
|
|
23
23
|
const configDir = getConfigDir();
|
|
@@ -38,7 +38,7 @@ export function startDaemon(port: number = 7071) {
|
|
|
38
38
|
const errorLogPath = path.join(configDir, 'daemon-error.log');
|
|
39
39
|
|
|
40
40
|
if (isWindows) {
|
|
41
|
-
startDaemonWindows({
|
|
41
|
+
await startDaemonWindows({
|
|
42
42
|
port,
|
|
43
43
|
configDir,
|
|
44
44
|
configVars,
|
|
@@ -62,57 +62,115 @@ interface DaemonOptions {
|
|
|
62
62
|
errorLogPath: string;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
function
|
|
65
|
+
function resolveNssm(): string | null {
|
|
66
|
+
try {
|
|
67
|
+
return execSync('where nssm', { stdio: 'pipe' }).toString().trim().split('\n')[0].trim();
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function startDaemonWindows(opts: DaemonOptions) {
|
|
66
74
|
const { port, configDir, configVars, nodePath, backendPath, logPath, errorLogPath } = opts;
|
|
75
|
+
const serviceName = 'OmnikeyDaemon';
|
|
67
76
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
].join('\r\n');
|
|
77
|
+
let nssmPath = resolveNssm();
|
|
78
|
+
if (!nssmPath) {
|
|
79
|
+
const { install } = await inquirer.prompt<{ install: boolean }>([
|
|
80
|
+
{
|
|
81
|
+
type: 'confirm',
|
|
82
|
+
name: 'install',
|
|
83
|
+
message: 'NSSM is required but not found. Install it now via winget?',
|
|
84
|
+
default: true,
|
|
85
|
+
},
|
|
86
|
+
]);
|
|
79
87
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
console.error('Failed to write start-daemon.cmd:', e);
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
88
|
+
if (!install) {
|
|
89
|
+
console.log('Aborted. Install NSSM manually and re-run in an elevated (Administrator) terminal.');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
87
92
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
console.log('Installing NSSM via winget...');
|
|
94
|
+
try {
|
|
95
|
+
execSync('winget install nssm --accept-package-agreements --accept-source-agreements', {
|
|
96
|
+
stdio: 'inherit',
|
|
97
|
+
});
|
|
98
|
+
} catch (e) {
|
|
99
|
+
console.error('winget install failed:', (e as any)?.message ?? e);
|
|
100
|
+
console.log('Try manually: scoop install nssm or choco install nssm');
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// winget updates the machine PATH in the registry but the current process
|
|
105
|
+
// won't see it — spawn a fresh cmd to resolve the new location.
|
|
106
|
+
try {
|
|
107
|
+
nssmPath = execSync('cmd /c where nssm', { stdio: 'pipe' })
|
|
108
|
+
.toString().trim().split('\n')[0].trim();
|
|
109
|
+
} catch {
|
|
110
|
+
nssmPath = null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!nssmPath) {
|
|
114
|
+
console.log('NSSM installed successfully.');
|
|
115
|
+
console.log('Please open a new elevated (Administrator) terminal and re-run this command.');
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
95
118
|
}
|
|
119
|
+
|
|
120
|
+
initLogFiles(logPath, errorLogPath);
|
|
121
|
+
|
|
122
|
+
// Remove any existing service (stop first, then remove)
|
|
123
|
+
try { execFileSync(nssmPath, ['stop', serviceName], { stdio: 'pipe' }); } catch { /* not running */ }
|
|
124
|
+
try { execFileSync(nssmPath, ['remove', serviceName, 'confirm'], { stdio: 'pipe' }); } catch { /* didn't exist */ }
|
|
125
|
+
|
|
126
|
+
// NSSM services run as LocalSystem; pass USERPROFILE so the backend's
|
|
127
|
+
// getHomeDir() resolves to the correct user config directory.
|
|
128
|
+
const env: Record<string, string> = {
|
|
129
|
+
...configVars,
|
|
130
|
+
OMNIKEY_PORT: String(port),
|
|
131
|
+
USERPROFILE: process.env.USERPROFILE || configDir.replace(/[/\\]\.omnikey$/, ''),
|
|
132
|
+
HOME: process.env.USERPROFILE || configDir.replace(/[/\\]\.omnikey$/, ''),
|
|
133
|
+
};
|
|
134
|
+
|
|
96
135
|
try {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
);
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
136
|
+
// Install: nssm install <name> <application> [args...]
|
|
137
|
+
execFileSync(nssmPath, ['install', serviceName, nodePath, backendPath], { stdio: 'pipe' });
|
|
138
|
+
|
|
139
|
+
execFileSync(nssmPath, ['set', serviceName, 'AppDirectory', configDir], { stdio: 'pipe' });
|
|
140
|
+
|
|
141
|
+
// Pass all env vars in a single call (replaces the entire AppEnvironmentExtra key)
|
|
142
|
+
const envEntries = Object.entries(env).map(([k, v]) => `${k}=${v}`);
|
|
143
|
+
execFileSync(nssmPath, ['set', serviceName, 'AppEnvironmentExtra', ...envEntries], { stdio: 'pipe' });
|
|
106
144
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
145
|
+
execFileSync(nssmPath, ['set', serviceName, 'AppStdout', logPath], { stdio: 'pipe' });
|
|
146
|
+
execFileSync(nssmPath, ['set', serviceName, 'AppStderr', errorLogPath], { stdio: 'pipe' });
|
|
147
|
+
execFileSync(nssmPath, ['set', serviceName, 'AppRotateFiles', '1'], { stdio: 'pipe' });
|
|
148
|
+
|
|
149
|
+
// Restart automatically after a 3-second delay on any exit
|
|
150
|
+
execFileSync(nssmPath, ['set', serviceName, 'AppExit', 'Default', 'Restart'], { stdio: 'pipe' });
|
|
151
|
+
execFileSync(nssmPath, ['set', serviceName, 'AppRestartDelay', '3000'], { stdio: 'pipe' });
|
|
152
|
+
|
|
153
|
+
// Start automatically at boot (no login required)
|
|
154
|
+
execFileSync(nssmPath, ['set', serviceName, 'Start', 'SERVICE_AUTO_START'], { stdio: 'pipe' });
|
|
155
|
+
|
|
156
|
+
execFileSync(nssmPath, ['set', serviceName, 'DisplayName', 'Omnikey API Backend'], { stdio: 'pipe' });
|
|
157
|
+
execFileSync(nssmPath, ['set', serviceName, 'Description', 'Omnikey API Backend Daemon'], { stdio: 'pipe' });
|
|
158
|
+
|
|
159
|
+
execFileSync(nssmPath, ['start', serviceName], { stdio: 'pipe' });
|
|
160
|
+
|
|
161
|
+
console.log(`NSSM service installed and started: ${serviceName}`);
|
|
162
|
+
console.log('Omnikey daemon runs on boot, without login, and auto-restarts on crash.');
|
|
163
|
+
console.log(`Logs: ${logPath}`);
|
|
164
|
+
console.log(` ${errorLogPath}`);
|
|
165
|
+
} catch (e: any) {
|
|
166
|
+
const msg: string = e?.stderr?.toString() || e?.message || String(e);
|
|
167
|
+
if (msg.toLowerCase().includes('access') || msg.toLowerCase().includes('privilege')) {
|
|
168
|
+
console.error('Failed to install NSSM service: administrator privileges are required.');
|
|
169
|
+
console.error('Re-run this command in an elevated (Administrator) terminal.');
|
|
170
|
+
} else {
|
|
171
|
+
console.error('Failed to install NSSM service:', msg);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
116
174
|
}
|
|
117
175
|
|
|
118
176
|
function startDaemonMacOS(opts: DaemonOptions) {
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,8 @@ import { killDaemon } from './killDaemon';
|
|
|
7
7
|
import { removeConfigAndDb } from './removeConfig';
|
|
8
8
|
import { statusCmd } from './status';
|
|
9
9
|
import { showLogs } from './showLogs';
|
|
10
|
+
import { showConfig } from './showConfig';
|
|
11
|
+
import { setConfig } from './setConfig';
|
|
10
12
|
|
|
11
13
|
const program = new Command();
|
|
12
14
|
|
|
@@ -26,9 +28,9 @@ program
|
|
|
26
28
|
.command('daemon')
|
|
27
29
|
.description('Start the Omnikey API backend as a daemon on a specified port')
|
|
28
30
|
.option('--port <port>', 'Port to run the backend on', '7071')
|
|
29
|
-
.action((options) => {
|
|
31
|
+
.action(async (options) => {
|
|
30
32
|
const port = Number(options.port) || 7071;
|
|
31
|
-
startDaemon(port);
|
|
33
|
+
await startDaemon(port);
|
|
32
34
|
});
|
|
33
35
|
|
|
34
36
|
program
|
|
@@ -64,4 +66,28 @@ program
|
|
|
64
66
|
showLogs(lines, errorsOnly);
|
|
65
67
|
});
|
|
66
68
|
|
|
69
|
+
program
|
|
70
|
+
.command('config')
|
|
71
|
+
.description('Show the current Omnikey configuration (API keys are masked)')
|
|
72
|
+
.action(() => {
|
|
73
|
+
showConfig();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
program
|
|
77
|
+
.command('set <key> <value>')
|
|
78
|
+
.description('Set a single configuration key (e.g. omnikey set OMNIKEY_PORT 8080)')
|
|
79
|
+
.action((key: string, value: string) => {
|
|
80
|
+
setConfig(key, value);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
program
|
|
84
|
+
.command('restart-daemon')
|
|
85
|
+
.description('Restart the Omnikey API backend daemon')
|
|
86
|
+
.option('--port <port>', 'Port to run the backend on', '7071')
|
|
87
|
+
.action(async (options) => {
|
|
88
|
+
killDaemon();
|
|
89
|
+
const port = Number(options.port) || 7071;
|
|
90
|
+
await startDaemon(port);
|
|
91
|
+
});
|
|
92
|
+
|
|
67
93
|
program.parseAsync(process.argv);
|