cli-jaw 2.2.18 → 2.2.19
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 +12 -0
- package/dist/bin/cli-jaw.js +1 -1
- package/dist/bin/cli-jaw.js.map +1 -1
- package/dist/bin/commands/dispatch.js +14 -4
- package/dist/bin/commands/dispatch.js.map +1 -1
- package/dist/bin/commands/service.js +322 -222
- package/dist/bin/commands/service.js.map +1 -1
- package/dist/bin/postinstall.js +20 -4
- package/dist/bin/postinstall.js.map +1 -1
- package/dist/server.js +28 -2
- package/dist/server.js.map +1 -1
- package/dist/src/agent/spawn.js +1 -12
- package/dist/src/agent/spawn.js.map +1 -1
- package/dist/src/cli/cli-status-worker.js +59 -6
- package/dist/src/cli/cli-status-worker.js.map +1 -1
- package/dist/src/core/cli-detect.js +54 -8
- package/dist/src/core/cli-detect.js.map +1 -1
- package/dist/src/core/config.js +10 -0
- package/dist/src/core/config.js.map +1 -1
- package/dist/src/core/instance-lifecycle.js +120 -0
- package/dist/src/core/instance-lifecycle.js.map +1 -0
- package/dist/src/core/runtime-path.js +92 -12
- package/dist/src/core/runtime-path.js.map +1 -1
- package/dist/src/core/runtime-settings.js +4 -1
- package/dist/src/core/runtime-settings.js.map +1 -1
- package/dist/src/core/settings-watch.js +12 -1
- package/dist/src/core/settings-watch.js.map +1 -1
- package/dist/src/notes/search.js +25 -2
- package/dist/src/notes/search.js.map +1 -1
- package/dist/src/prompt/builder.js +59 -7
- package/dist/src/prompt/builder.js.map +1 -1
- package/dist/src/routes/settings.js +8 -1
- package/dist/src/routes/settings.js.map +1 -1
- package/dist/src/routes/system.js +5 -1
- package/dist/src/routes/system.js.map +1 -1
- package/dist/src/routes/wiki.js +17 -12
- package/dist/src/routes/wiki.js.map +1 -1
- package/dist/src/search/coordinator.js +13 -5
- package/dist/src/search/coordinator.js.map +1 -1
- package/dist/src/search/provider.js.map +1 -1
- package/dist/src/search/providers/wiki.js +17 -9
- package/dist/src/search/providers/wiki.js.map +1 -1
- package/dist/src/slack/manifest.js +14 -3
- package/dist/src/slack/manifest.js.map +1 -1
- package/dist/src/wiki/config.js +42 -14
- package/dist/src/wiki/config.js.map +1 -1
- package/dist/src/wiki/prompt.js +2 -2
- package/dist/src/wiki/prompt.js.map +1 -1
- package/package.json +1 -1
- package/public/dist/assets/{app-DqSJmDwP.js → app-is76CHGZ.js} +4 -4
- package/public/dist/assets/{bgtask-badge-D6XYRFI5.js → bgtask-badge-B0B1nXUs.js} +1 -1
- package/public/dist/assets/bgtask-badge-B2jUZ5mC.js +1 -0
- package/public/dist/assets/{employees-B_wKPfTO.js → employees-eiW0cC8z.js} +1 -1
- package/public/dist/assets/{iframe-renderer-D1-zDg8V.js → iframe-renderer-C7qQJX4c.js} +2 -2
- package/public/dist/assets/iframe-renderer-D7PB_yrC.js +1 -0
- package/public/dist/assets/{memory-CNUpXWEC.js → memory--Q8Tb5SZ.js} +1 -1
- package/public/dist/assets/memory-rsKN0aa2.js +1 -0
- package/public/dist/assets/message-history-BttkY77h.js +1 -0
- package/public/dist/assets/{message-history-DB1rynP-.js → message-history-CfAHmCDW.js} +1 -1
- package/public/dist/assets/{render-DXwQzjsJ.js → render-BDso7Yb5.js} +1 -1
- package/public/dist/assets/{settings-CXndjCad.js → settings-CqnIqxkG.js} +1 -1
- package/public/dist/assets/{settings-core-t5yYVRrv.js → settings-core-BxuFkt4a.js} +4 -4
- package/public/dist/assets/settings-core-CdZ7oKhM.js +1 -0
- package/public/dist/assets/settings-pPMHNs0G.js +1 -0
- package/public/dist/assets/{sidebar-DgIj-LuT.js → sidebar-BNT58qR0.js} +3 -3
- package/public/dist/assets/{skills-QqxjqDHg.js → skills-CwRBcm3m.js} +1 -1
- package/public/dist/assets/skills-DiVoaVdS.js +1 -0
- package/public/dist/assets/slash-commands-Bx4saHwR.js +1 -0
- package/public/dist/assets/{slash-commands-B2V_jngP.js → slash-commands-cm0U58n7.js} +1 -1
- package/public/dist/assets/{trace-drawer-CBe9nRjS.js → trace-drawer-BuwO4m2P.js} +1 -1
- package/public/dist/assets/{ui-CDozpEQt.js → ui-C-LHMki1.js} +1 -1
- package/public/dist/assets/ui-XPQDPe-5.js +1 -0
- package/public/dist/index.html +1 -1
- package/public/js/features/channel-onboarding.ts +8 -5
- package/public/locales/en.json +3 -2
- package/public/locales/ja.json +3 -2
- package/public/locales/ko.json +3 -2
- package/public/locales/zh.json +3 -2
- package/scripts/install-officecli.sh +28 -0
- package/public/dist/assets/bgtask-badge-CjWz0F6U.js +0 -1
- package/public/dist/assets/iframe-renderer-qRVLHPnT.js +0 -1
- package/public/dist/assets/memory-CcWGiPy_.js +0 -1
- package/public/dist/assets/message-history-BamMPUnL.js +0 -1
- package/public/dist/assets/settings-RGpifFHy.js +0 -1
- package/public/dist/assets/settings-core-X_5UHfr3.js +0 -1
- package/public/dist/assets/skills-CbwIimit.js +0 -1
- package/public/dist/assets/slash-commands-tCNRlNTC.js +0 -1
- package/public/dist/assets/ui-o-VpvrSv.js +0 -1
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* jaw service — OS 감지 → 설치 + 시작 (원스텝)
|
|
5
5
|
* jaw service --port 3458 — 커스텀 포트로 등록
|
|
6
6
|
* jaw service status — 현재 상태 확인
|
|
7
|
+
* jaw service stop — 이 홈의 서버만 graceful stop
|
|
8
|
+
* jaw service restart — 이 홈의 서버만 graceful restart
|
|
7
9
|
* jaw service unset — 서비스 제거
|
|
8
10
|
* jaw service logs — 로그 보기
|
|
9
11
|
*/
|
|
@@ -13,142 +15,235 @@ import { dirname, join } from 'node:path';
|
|
|
13
15
|
import { parseArgs } from 'node:util';
|
|
14
16
|
import { JAW_HOME } from '../../src/core/config.js';
|
|
15
17
|
import { instanceId, getNodePath, getJawPath, sanitizeUnitName, buildServicePath } from '../../src/core/instance.js';
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
port: { type: 'string', default: '3457' },
|
|
21
|
-
backend: { type: 'string' },
|
|
22
|
-
},
|
|
23
|
-
strict: false,
|
|
24
|
-
allowPositionals: true,
|
|
25
|
-
});
|
|
26
|
-
// unknown flag guard
|
|
27
|
-
const knownKeys = new Set(['port', 'backend']);
|
|
28
|
-
for (const key of Object.keys(opts)) {
|
|
29
|
-
if (!knownKeys.has(key)) {
|
|
30
|
-
console.error(`❌ Unknown option: --${key}`);
|
|
31
|
-
console.error(' Usage: jaw service [--port PORT] [--backend launchd|systemd|docker] [status|unset|logs]');
|
|
32
|
-
process.exit(1);
|
|
33
|
-
}
|
|
18
|
+
import { defaultLifecycleDeps, verifyOwnership } from '../../src/core/instance-lifecycle.js';
|
|
19
|
+
import { detectServiceState, restartServiceInstance, stopServiceInstance } from '../../src/manager/platform-service.js';
|
|
20
|
+
export function nativeOutcome(action, result) {
|
|
21
|
+
return { action, status: result.ok ? 'stopped' : 'foreign', ...(result.pid ? { pid: result.pid } : {}), message: result.message };
|
|
34
22
|
}
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
if (
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
23
|
+
export async function resolveControlNative(port, home, deps) {
|
|
24
|
+
const state = await deps.detectServiceState(port, home);
|
|
25
|
+
if (!state.registered)
|
|
26
|
+
return null;
|
|
27
|
+
return action => (action === 'stop' ? deps.stopServiceInstance(state.label) : deps.restartServiceInstance(state.label))
|
|
28
|
+
.then(result => nativeOutcome(action, result));
|
|
41
29
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
30
|
+
function nonOwnedOutcome(action, verdict) {
|
|
31
|
+
const pid = 'record' in verdict ? verdict.record.pid : undefined;
|
|
32
|
+
const reason = 'reason' in verdict ? `: ${verdict.reason}` : '';
|
|
33
|
+
const message = verdict.status === 'unverifiable' ? `cannot verify ownership${reason}; refusing to signal`
|
|
34
|
+
: verdict.status === 'permission-denied' ? `permission denied for pid ${pid}; refusing to signal`
|
|
35
|
+
: verdict.status === 'no-pidfile' ? 'no pidfile exists for this home'
|
|
36
|
+
: verdict.status === 'already-stopped' ? `pid ${pid} is already stopped` : `${verdict.status} pidfile${reason}`;
|
|
37
|
+
return { action, status: verdict.status, ...(pid ? { pid } : {}), message };
|
|
49
38
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
39
|
+
export async function runServiceLifecycle(action, deps) {
|
|
40
|
+
if (deps.controlNative)
|
|
41
|
+
return deps.controlNative(action);
|
|
42
|
+
const verdict = deps.verifyOwnership();
|
|
43
|
+
if (verdict.status !== 'owned')
|
|
44
|
+
return nonOwnedOutcome(action, verdict);
|
|
45
|
+
const signalResult = deps.verifyAndSignal('SIGTERM');
|
|
46
|
+
if (signalResult === 'raced')
|
|
47
|
+
return { action, status: 'raced', pid: verdict.record.pid, message: 'the instance exited while stopping it; nothing was signalled' };
|
|
48
|
+
if (signalResult === 'not-owned')
|
|
49
|
+
return { action, status: 'stale', pid: verdict.record.pid, message: 'final ownership check denied the pid; nothing was signalled' };
|
|
50
|
+
if (action === 'stop')
|
|
51
|
+
return { action, status: 'stopped', pid: verdict.record.pid, message: `sent SIGTERM to pid ${verdict.record.pid}` };
|
|
52
|
+
if (!await deps.waitForExit(verdict.record.pid, 5000))
|
|
53
|
+
return { action, status: 'timeout', pid: verdict.record.pid, message: `pid ${verdict.record.pid} did not exit within 5000ms` };
|
|
54
|
+
await deps.startInstance?.();
|
|
55
|
+
return { action, status: 'stopped', pid: verdict.record.pid, message: `restarted instance after pid ${verdict.record.pid} exited` };
|
|
56
|
+
}
|
|
57
|
+
// ─── Args ────────────────────────────────────────────
|
|
58
|
+
export async function main() {
|
|
59
|
+
const { values: opts, positionals: pos } = parseArgs({
|
|
60
|
+
args: process.argv.slice(3),
|
|
61
|
+
options: {
|
|
62
|
+
port: { type: 'string', default: '3457' },
|
|
63
|
+
backend: { type: 'string' },
|
|
64
|
+
},
|
|
65
|
+
strict: false,
|
|
66
|
+
allowPositionals: true,
|
|
67
|
+
});
|
|
68
|
+
// unknown flag guard
|
|
69
|
+
const knownKeys = new Set(['port', 'backend']);
|
|
70
|
+
for (const key of Object.keys(opts)) {
|
|
71
|
+
if (!knownKeys.has(key)) {
|
|
72
|
+
console.error(`❌ Unknown option: --${key}`);
|
|
73
|
+
console.error(' Usage: jaw service [--port PORT] [--backend launchd|systemd|docker] [status|stop|restart|unset|logs]');
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// --backend whitelist validation
|
|
78
|
+
const VALID_BACKENDS = new Set(['launchd', 'systemd', 'docker']);
|
|
79
|
+
if (opts.backend && !VALID_BACKENDS.has(opts.backend)) {
|
|
80
|
+
console.error(`❌ Unknown backend: ${opts.backend}`);
|
|
81
|
+
console.error(' Supported: launchd, systemd, docker');
|
|
82
|
+
process.exit(1);
|
|
63
83
|
}
|
|
64
|
-
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
console.
|
|
69
|
-
|
|
84
|
+
const PORT = opts.port;
|
|
85
|
+
// port validation: must be numeric and in valid range
|
|
86
|
+
const portNum = Number(PORT);
|
|
87
|
+
if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) {
|
|
88
|
+
console.error(`\u274c Invalid port: ${PORT}`);
|
|
89
|
+
console.error(' Port must be an integer between 1 and 65535');
|
|
90
|
+
process.exit(1);
|
|
70
91
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
+
const lifecycleAction = pos[0] === 'stop' || pos[0] === 'restart' ? pos[0] : null;
|
|
93
|
+
if (lifecycleAction) {
|
|
94
|
+
const controlNative = await resolveControlNative(portNum, JAW_HOME, {
|
|
95
|
+
detectServiceState, stopServiceInstance, restartServiceInstance,
|
|
96
|
+
});
|
|
97
|
+
let initiallyOwned = null;
|
|
98
|
+
const outcome = await runServiceLifecycle(lifecycleAction, {
|
|
99
|
+
controlNative,
|
|
100
|
+
verifyOwnership: () => {
|
|
101
|
+
const verdict = verifyOwnership(JAW_HOME, defaultLifecycleDeps);
|
|
102
|
+
initiallyOwned = verdict.status === 'owned' ? verdict.record : null;
|
|
103
|
+
return verdict;
|
|
104
|
+
},
|
|
105
|
+
verifyAndSignal: signal => {
|
|
106
|
+
const finalVerdict = verifyOwnership(JAW_HOME, defaultLifecycleDeps);
|
|
107
|
+
if (finalVerdict.status === 'stale' || finalVerdict.status === 'foreign')
|
|
108
|
+
return 'not-owned';
|
|
109
|
+
if (finalVerdict.status !== 'owned')
|
|
110
|
+
return 'raced';
|
|
111
|
+
if (!initiallyOwned
|
|
112
|
+
|| finalVerdict.record.pid !== initiallyOwned.pid
|
|
113
|
+
|| finalVerdict.record.startedAt.source !== initiallyOwned.startedAt.source
|
|
114
|
+
|| finalVerdict.record.startedAt.value !== initiallyOwned.startedAt.value)
|
|
115
|
+
return 'raced';
|
|
116
|
+
try {
|
|
117
|
+
process.kill(finalVerdict.record.pid, signal);
|
|
118
|
+
return 'signalled';
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return 'raced';
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
waitForExit: async (pid, timeoutMs) => {
|
|
125
|
+
const deadline = Date.now() + timeoutMs;
|
|
126
|
+
while (Date.now() < deadline) {
|
|
127
|
+
if (defaultLifecycleDeps.probe(pid).status === 'dead')
|
|
128
|
+
return true;
|
|
129
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
130
|
+
}
|
|
131
|
+
return false;
|
|
132
|
+
},
|
|
133
|
+
startInstance: async () => {
|
|
134
|
+
const child = nodeSpawn(getNodePath(), [getJawPath(), '--home', JAW_HOME, 'serve', '--port', PORT, '--no-open'], {
|
|
135
|
+
detached: true, stdio: 'ignore',
|
|
136
|
+
});
|
|
137
|
+
child.unref();
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
console.log(outcome.message);
|
|
141
|
+
if (!['stopped', 'already-stopped', 'no-pidfile'].includes(outcome.status))
|
|
142
|
+
process.exitCode = 1;
|
|
143
|
+
return;
|
|
92
144
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
'
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
145
|
+
const INSTANCE = instanceId();
|
|
146
|
+
const LOG_DIR = join(JAW_HOME, 'logs');
|
|
147
|
+
function detectBackend() {
|
|
148
|
+
if (process.platform === 'darwin')
|
|
149
|
+
return 'launchd';
|
|
150
|
+
// Docker container detection
|
|
151
|
+
if (existsSync('/.dockerenv'))
|
|
152
|
+
return 'docker';
|
|
153
|
+
// Linux: verify PID 1 is actually systemd (not just binary installed)
|
|
154
|
+
try {
|
|
155
|
+
const pid1 = readFileSync('/proc/1/comm', 'utf8').trim();
|
|
156
|
+
if (pid1 === 'systemd')
|
|
157
|
+
return 'systemd';
|
|
158
|
+
}
|
|
159
|
+
catch { /* /proc may not exist */ }
|
|
160
|
+
// Fallback: check systemctl exists (less reliable but covers more cases)
|
|
161
|
+
try {
|
|
162
|
+
execFileSync('which', ['systemctl'], { stdio: 'pipe' });
|
|
163
|
+
console.warn('⚠️ PID 1이 systemd가 아닐 수 있습니다. --backend systemd로 강제 지정 가능');
|
|
164
|
+
return 'systemd';
|
|
165
|
+
}
|
|
166
|
+
catch { /* no systemctl */ }
|
|
167
|
+
console.error('❌ 지원되지 않는 환경입니다.');
|
|
168
|
+
console.error(' 지원: macOS (launchd), Linux (systemd), Docker');
|
|
169
|
+
console.error(' 수동 설정은 jaw serve를 tmux/screen에서 실행하세요.');
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
const backend = opts.backend || detectBackend();
|
|
173
|
+
// ─── macOS: delegate to launchd command (legacy) ────
|
|
174
|
+
if (backend === 'launchd') {
|
|
175
|
+
// launchd.ts supports: status, unset, default (install)
|
|
176
|
+
// service.ts additionally supports: logs
|
|
177
|
+
// Map unsupported subcommands for launchd
|
|
178
|
+
const subcommand = pos[0];
|
|
179
|
+
if (subcommand === 'logs') {
|
|
180
|
+
// launchd doesn't have 'logs' → show log file path directly
|
|
181
|
+
const logDir = join(JAW_HOME, 'logs');
|
|
182
|
+
console.log(`📋 macOS launchd logs:\n`);
|
|
183
|
+
console.log(` stdout: ${logDir}/jaw-serve.log`);
|
|
184
|
+
console.log(` stderr: ${logDir}/jaw-serve.err\n`);
|
|
185
|
+
console.log(` tail -f ${logDir}/jaw-serve.log`);
|
|
186
|
+
process.exit(0);
|
|
187
|
+
}
|
|
188
|
+
// Rebuild argv from parsed values — eliminates all --backend variants
|
|
189
|
+
// Only pass --port on install (default). status/unset read from existing plist.
|
|
190
|
+
const isInstall = !subcommand || (subcommand !== 'status' && subcommand !== 'unset');
|
|
191
|
+
const portArgs = isInstall && PORT !== '3457' ? ['--port', PORT] : [];
|
|
192
|
+
process.argv = [
|
|
193
|
+
process.argv[0], process.argv[1],
|
|
194
|
+
'launchd',
|
|
195
|
+
...(subcommand ? [subcommand] : []),
|
|
196
|
+
...portArgs,
|
|
197
|
+
];
|
|
198
|
+
process.env["_CLI_JAW_SERVICE_DELEGATE"] = '1';
|
|
199
|
+
await import('./launchd.js');
|
|
200
|
+
process.exit(0);
|
|
126
201
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
202
|
+
// ─── Docker: info only ───────────────────────────────
|
|
203
|
+
if (backend === 'docker') {
|
|
204
|
+
console.log('🐳 Docker 컨테이너 내부에서 실행 중입니다.\n');
|
|
205
|
+
console.log(' 컨테이너 자체가 restart policy로 관리됩니다.');
|
|
206
|
+
console.log(' docker-compose.yml의 restart: unless-stopped 설정을 확인하세요.\n');
|
|
207
|
+
console.log(' 상태 확인: docker ps | grep cli-jaw');
|
|
208
|
+
console.log(' 로그 확인: docker logs -f cli-jaw');
|
|
209
|
+
process.exit(0);
|
|
131
210
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
211
|
+
// ═══════════════════════════════════════════════════════
|
|
212
|
+
// systemd backend
|
|
213
|
+
// ═══════════════════════════════════════════════════════
|
|
214
|
+
const SAFE_INSTANCE = sanitizeUnitName(INSTANCE);
|
|
215
|
+
const UNIT_NAME = `jaw-${SAFE_INSTANCE}`;
|
|
216
|
+
const UNIT_PATH = `/etc/systemd/system/${UNIT_NAME}.service`;
|
|
217
|
+
/** Create secure temp file for unit generation. */
|
|
218
|
+
function makeTmpUnit() {
|
|
219
|
+
try {
|
|
220
|
+
return execFileSync('mktemp', ['/tmp/jaw-unit-XXXXXX.service'], { encoding: 'utf8' }).trim();
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
// fallback if mktemp unavailable
|
|
224
|
+
const fallback = `/tmp/jaw-unit-${Date.now()}-${process.pid}.service`;
|
|
225
|
+
return fallback;
|
|
226
|
+
}
|
|
144
227
|
}
|
|
145
|
-
|
|
146
|
-
|
|
228
|
+
/** Run a sudo command with visible output (password prompt visible). */
|
|
229
|
+
function sudo(args) {
|
|
230
|
+
execFileSync('sudo', args, { stdio: 'inherit' });
|
|
147
231
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
232
|
+
function generateUnit() {
|
|
233
|
+
const nodePath = getNodePath();
|
|
234
|
+
const jawPath = getJawPath();
|
|
235
|
+
const servicePath = buildServicePath(process.env["PATH"] || '', [dirname(nodePath), dirname(jawPath)]);
|
|
236
|
+
let user;
|
|
237
|
+
try {
|
|
238
|
+
user = execFileSync('whoami', { encoding: 'utf8' }).trim();
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
user = 'nobody';
|
|
242
|
+
}
|
|
243
|
+
mkdirSync(LOG_DIR, { recursive: true });
|
|
244
|
+
// systemd quoting: paths with spaces must be quoted
|
|
245
|
+
const q = (s) => s.includes(' ') ? `"${s}"` : s;
|
|
246
|
+
return `[Unit]
|
|
152
247
|
Description=CLI-JAW Server (${SAFE_INSTANCE})
|
|
153
248
|
After=network.target
|
|
154
249
|
|
|
@@ -168,117 +263,122 @@ SyslogIdentifier=${UNIT_NAME}
|
|
|
168
263
|
|
|
169
264
|
[Install]
|
|
170
265
|
WantedBy=multi-user.target`;
|
|
171
|
-
}
|
|
172
|
-
function isActive() {
|
|
173
|
-
try {
|
|
174
|
-
return execFileSync('systemctl', ['is-active', UNIT_NAME], { encoding: 'utf8' }).trim();
|
|
175
|
-
}
|
|
176
|
-
catch {
|
|
177
|
-
return 'inactive';
|
|
178
266
|
}
|
|
179
|
-
|
|
180
|
-
const sub = pos[0];
|
|
181
|
-
switch (sub) {
|
|
182
|
-
case 'unset': {
|
|
183
|
-
if (!existsSync(UNIT_PATH)) {
|
|
184
|
-
console.log('⚠️ jaw serve가 systemd에 등록되어 있지 않습니다');
|
|
185
|
-
break;
|
|
186
|
-
}
|
|
187
|
-
try {
|
|
188
|
-
sudo(['systemctl', 'stop', UNIT_NAME]);
|
|
189
|
-
}
|
|
190
|
-
catch { /* ok if already stopped */ }
|
|
267
|
+
function isActive() {
|
|
191
268
|
try {
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
catch { /* ok */ }
|
|
195
|
-
sudo(['rm', '-f', UNIT_PATH]);
|
|
196
|
-
sudo(['systemctl', 'daemon-reload']);
|
|
197
|
-
console.log('✅ jaw serve 자동 실행 해제 완료');
|
|
198
|
-
break;
|
|
199
|
-
}
|
|
200
|
-
case 'status': {
|
|
201
|
-
if (!existsSync(UNIT_PATH)) {
|
|
202
|
-
console.log('\u26a0\ufe0f jaw serve\uac00 systemd\uc5d0 \ub4f1\ub85d\ub418\uc5b4 \uc788\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4');
|
|
203
|
-
console.log(' \ub4f1\ub85d: jaw service');
|
|
204
|
-
break;
|
|
269
|
+
return execFileSync('systemctl', ['is-active', UNIT_NAME], { encoding: 'utf8' }).trim();
|
|
205
270
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
try {
|
|
209
|
-
const unitContent = readFileSync(UNIT_PATH, 'utf8');
|
|
210
|
-
const portMatch = unitContent.match(/--port\s+(\d+)/);
|
|
211
|
-
if (portMatch)
|
|
212
|
-
displayPort = portMatch[1];
|
|
271
|
+
catch {
|
|
272
|
+
return 'inactive';
|
|
213
273
|
}
|
|
214
|
-
catch { /* use CLI port as fallback */ }
|
|
215
|
-
const state = isActive();
|
|
216
|
-
const icon = state === 'active' ? '\ud83d\udfe2' : state === 'failed' ? '\ud83d\udd34' : '\u26aa';
|
|
217
|
-
console.log(`\ud83e\udd88 jaw serve \u2014 ${icon} ${state}`);
|
|
218
|
-
console.log(` instance: ${INSTANCE}`);
|
|
219
|
-
console.log(` unit: ${UNIT_NAME}`);
|
|
220
|
-
console.log(` port: ${displayPort}`);
|
|
221
|
-
console.log(` unit file: ${UNIT_PATH}`);
|
|
222
|
-
console.log(`\n \ub85c\uadf8: jaw service logs`);
|
|
223
|
-
console.log(` \uc911\uc9c0: jaw service unset`);
|
|
224
|
-
break;
|
|
225
|
-
}
|
|
226
|
-
case 'logs': {
|
|
227
|
-
console.log(`📋 journalctl -u ${UNIT_NAME} -f\n`);
|
|
228
|
-
const child = nodeSpawn('journalctl', ['-u', UNIT_NAME, '-f', '--no-pager', '-n', '50'], {
|
|
229
|
-
stdio: 'inherit',
|
|
230
|
-
});
|
|
231
|
-
// Clean exit on Ctrl+C
|
|
232
|
-
const cleanup = () => { child.kill('SIGINT'); process.exit(0); };
|
|
233
|
-
process.on('SIGINT', cleanup);
|
|
234
|
-
process.on('SIGTERM', cleanup);
|
|
235
|
-
child.on('exit', (code) => process.exit(code ?? 0));
|
|
236
|
-
break;
|
|
237
274
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
275
|
+
const sub = pos[0];
|
|
276
|
+
switch (sub) {
|
|
277
|
+
case 'unset': {
|
|
278
|
+
if (!existsSync(UNIT_PATH)) {
|
|
279
|
+
console.log('⚠️ jaw serve가 systemd에 등록되어 있지 않습니다');
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
243
282
|
try {
|
|
244
283
|
sudo(['systemctl', 'stop', UNIT_NAME]);
|
|
245
284
|
}
|
|
285
|
+
catch { /* ok if already stopped */ }
|
|
286
|
+
try {
|
|
287
|
+
sudo(['systemctl', 'disable', UNIT_NAME]);
|
|
288
|
+
}
|
|
246
289
|
catch { /* ok */ }
|
|
290
|
+
sudo(['rm', '-f', UNIT_PATH]);
|
|
291
|
+
sudo(['systemctl', 'daemon-reload']);
|
|
292
|
+
console.log('✅ jaw serve 자동 실행 해제 완료');
|
|
293
|
+
break;
|
|
247
294
|
}
|
|
248
|
-
|
|
249
|
-
|
|
295
|
+
case 'status': {
|
|
296
|
+
if (!existsSync(UNIT_PATH)) {
|
|
297
|
+
console.log('\u26a0\ufe0f jaw serve\uac00 systemd\uc5d0 \ub4f1\ub85d\ub418\uc5b4 \uc788\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4');
|
|
298
|
+
console.log(' \ub4f1\ub85d: jaw service');
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
// Read actual port from unit file if possible
|
|
302
|
+
let displayPort = PORT;
|
|
303
|
+
try {
|
|
304
|
+
const unitContent = readFileSync(UNIT_PATH, 'utf8');
|
|
305
|
+
const portMatch = unitContent.match(/--port\s+(\d+)/);
|
|
306
|
+
if (portMatch)
|
|
307
|
+
displayPort = portMatch[1];
|
|
308
|
+
}
|
|
309
|
+
catch { /* use CLI port as fallback */ }
|
|
310
|
+
const state = isActive();
|
|
311
|
+
const icon = state === 'active' ? '\ud83d\udfe2' : state === 'failed' ? '\ud83d\udd34' : '\u26aa';
|
|
312
|
+
console.log(`\ud83e\udd88 jaw serve \u2014 ${icon} ${state}`);
|
|
313
|
+
console.log(` instance: ${INSTANCE}`);
|
|
314
|
+
console.log(` unit: ${UNIT_NAME}`);
|
|
315
|
+
console.log(` port: ${displayPort}`);
|
|
316
|
+
console.log(` unit file: ${UNIT_PATH}`);
|
|
317
|
+
console.log(`\n \ub85c\uadf8: jaw service logs`);
|
|
318
|
+
console.log(` \uc911\uc9c0: jaw service unset`);
|
|
319
|
+
break;
|
|
250
320
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
321
|
+
case 'logs': {
|
|
322
|
+
console.log(`📋 journalctl -u ${UNIT_NAME} -f\n`);
|
|
323
|
+
const child = nodeSpawn('journalctl', ['-u', UNIT_NAME, '-f', '--no-pager', '-n', '50'], {
|
|
324
|
+
stdio: 'inherit',
|
|
325
|
+
});
|
|
326
|
+
// Clean exit on Ctrl+C
|
|
327
|
+
const cleanup = () => { child.kill('SIGINT'); process.exit(0); };
|
|
328
|
+
process.on('SIGINT', cleanup);
|
|
329
|
+
process.on('SIGTERM', cleanup);
|
|
330
|
+
child.on('exit', (code) => process.exit(code ?? 0));
|
|
331
|
+
break;
|
|
258
332
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
if (state === 'active') {
|
|
269
|
-
console.log('🦈 jaw serve가 백그라운드에서 실행 중입니다');
|
|
270
|
-
console.log(` instance: ${INSTANCE}`);
|
|
271
|
-
console.log(` http://localhost:${PORT}`);
|
|
272
|
-
console.log(` 로그: jaw service logs`);
|
|
273
|
-
console.log('\n 해제: jaw service unset');
|
|
333
|
+
default: {
|
|
334
|
+
// 원스텝: 유닛 생성 → daemon-reload → enable --now
|
|
335
|
+
console.log('🦈 jaw service setup (systemd)\n');
|
|
336
|
+
if (existsSync(UNIT_PATH)) {
|
|
337
|
+
console.log('📄 기존 유닛 발견 — 재생성합니다');
|
|
338
|
+
try {
|
|
339
|
+
sudo(['systemctl', 'stop', UNIT_NAME]);
|
|
340
|
+
}
|
|
341
|
+
catch { /* ok */ }
|
|
274
342
|
}
|
|
275
343
|
else {
|
|
276
|
-
console.log('
|
|
277
|
-
console.log(` systemctl status ${UNIT_NAME}`);
|
|
278
|
-
console.log(` journalctl -u ${UNIT_NAME} --no-pager -n 20`);
|
|
344
|
+
console.log('📄 유닛 없음 — 새로 생성합니다');
|
|
279
345
|
}
|
|
280
|
-
|
|
281
|
-
|
|
346
|
+
// 1. 유닛 파일 생성 (mktemp → sudo cp)
|
|
347
|
+
const unit = generateUnit();
|
|
348
|
+
const tmpUnit = makeTmpUnit();
|
|
349
|
+
writeFileSync(tmpUnit, unit);
|
|
350
|
+
sudo(['cp', tmpUnit, UNIT_PATH]);
|
|
351
|
+
try {
|
|
352
|
+
writeFileSync(tmpUnit, '');
|
|
353
|
+
}
|
|
354
|
+
catch { /* cleanup */ }
|
|
355
|
+
console.log(`✅ 유닛 저장: ${UNIT_PATH}`);
|
|
356
|
+
// 2. 등록 + 시작
|
|
357
|
+
sudo(['systemctl', 'daemon-reload']);
|
|
358
|
+
sudo(['systemctl', 'enable', '--now', UNIT_NAME]);
|
|
359
|
+
console.log('✅ systemd 등록 + 시작 완료\n');
|
|
360
|
+
// 3. 상태 확인
|
|
361
|
+
setTimeout(() => {
|
|
362
|
+
const state = isActive();
|
|
363
|
+
if (state === 'active') {
|
|
364
|
+
console.log('🦈 jaw serve가 백그라운드에서 실행 중입니다');
|
|
365
|
+
console.log(` instance: ${INSTANCE}`);
|
|
366
|
+
console.log(` http://localhost:${PORT}`);
|
|
367
|
+
console.log(` 로그: jaw service logs`);
|
|
368
|
+
console.log('\n 해제: jaw service unset');
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
console.log('⚠️ 시작되지 않았습니다. 상태를 확인하세요:');
|
|
372
|
+
console.log(` systemctl status ${UNIT_NAME}`);
|
|
373
|
+
console.log(` journalctl -u ${UNIT_NAME} --no-pager -n 20`);
|
|
374
|
+
}
|
|
375
|
+
}, 2000);
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
282
378
|
}
|
|
283
379
|
}
|
|
380
|
+
const isEntryPoint = process.argv[1]?.endsWith('service.js')
|
|
381
|
+
|| process.argv[1]?.endsWith('service.ts');
|
|
382
|
+
if (isEntryPoint)
|
|
383
|
+
void main();
|
|
284
384
|
//# sourceMappingURL=service.js.map
|