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.
Files changed (88) hide show
  1. package/README.md +12 -0
  2. package/dist/bin/cli-jaw.js +1 -1
  3. package/dist/bin/cli-jaw.js.map +1 -1
  4. package/dist/bin/commands/dispatch.js +14 -4
  5. package/dist/bin/commands/dispatch.js.map +1 -1
  6. package/dist/bin/commands/service.js +322 -222
  7. package/dist/bin/commands/service.js.map +1 -1
  8. package/dist/bin/postinstall.js +20 -4
  9. package/dist/bin/postinstall.js.map +1 -1
  10. package/dist/server.js +28 -2
  11. package/dist/server.js.map +1 -1
  12. package/dist/src/agent/spawn.js +1 -12
  13. package/dist/src/agent/spawn.js.map +1 -1
  14. package/dist/src/cli/cli-status-worker.js +59 -6
  15. package/dist/src/cli/cli-status-worker.js.map +1 -1
  16. package/dist/src/core/cli-detect.js +54 -8
  17. package/dist/src/core/cli-detect.js.map +1 -1
  18. package/dist/src/core/config.js +10 -0
  19. package/dist/src/core/config.js.map +1 -1
  20. package/dist/src/core/instance-lifecycle.js +120 -0
  21. package/dist/src/core/instance-lifecycle.js.map +1 -0
  22. package/dist/src/core/runtime-path.js +92 -12
  23. package/dist/src/core/runtime-path.js.map +1 -1
  24. package/dist/src/core/runtime-settings.js +4 -1
  25. package/dist/src/core/runtime-settings.js.map +1 -1
  26. package/dist/src/core/settings-watch.js +12 -1
  27. package/dist/src/core/settings-watch.js.map +1 -1
  28. package/dist/src/notes/search.js +25 -2
  29. package/dist/src/notes/search.js.map +1 -1
  30. package/dist/src/prompt/builder.js +59 -7
  31. package/dist/src/prompt/builder.js.map +1 -1
  32. package/dist/src/routes/settings.js +8 -1
  33. package/dist/src/routes/settings.js.map +1 -1
  34. package/dist/src/routes/system.js +5 -1
  35. package/dist/src/routes/system.js.map +1 -1
  36. package/dist/src/routes/wiki.js +17 -12
  37. package/dist/src/routes/wiki.js.map +1 -1
  38. package/dist/src/search/coordinator.js +13 -5
  39. package/dist/src/search/coordinator.js.map +1 -1
  40. package/dist/src/search/provider.js.map +1 -1
  41. package/dist/src/search/providers/wiki.js +17 -9
  42. package/dist/src/search/providers/wiki.js.map +1 -1
  43. package/dist/src/slack/manifest.js +14 -3
  44. package/dist/src/slack/manifest.js.map +1 -1
  45. package/dist/src/wiki/config.js +42 -14
  46. package/dist/src/wiki/config.js.map +1 -1
  47. package/dist/src/wiki/prompt.js +2 -2
  48. package/dist/src/wiki/prompt.js.map +1 -1
  49. package/package.json +1 -1
  50. package/public/dist/assets/{app-DqSJmDwP.js → app-is76CHGZ.js} +4 -4
  51. package/public/dist/assets/{bgtask-badge-D6XYRFI5.js → bgtask-badge-B0B1nXUs.js} +1 -1
  52. package/public/dist/assets/bgtask-badge-B2jUZ5mC.js +1 -0
  53. package/public/dist/assets/{employees-B_wKPfTO.js → employees-eiW0cC8z.js} +1 -1
  54. package/public/dist/assets/{iframe-renderer-D1-zDg8V.js → iframe-renderer-C7qQJX4c.js} +2 -2
  55. package/public/dist/assets/iframe-renderer-D7PB_yrC.js +1 -0
  56. package/public/dist/assets/{memory-CNUpXWEC.js → memory--Q8Tb5SZ.js} +1 -1
  57. package/public/dist/assets/memory-rsKN0aa2.js +1 -0
  58. package/public/dist/assets/message-history-BttkY77h.js +1 -0
  59. package/public/dist/assets/{message-history-DB1rynP-.js → message-history-CfAHmCDW.js} +1 -1
  60. package/public/dist/assets/{render-DXwQzjsJ.js → render-BDso7Yb5.js} +1 -1
  61. package/public/dist/assets/{settings-CXndjCad.js → settings-CqnIqxkG.js} +1 -1
  62. package/public/dist/assets/{settings-core-t5yYVRrv.js → settings-core-BxuFkt4a.js} +4 -4
  63. package/public/dist/assets/settings-core-CdZ7oKhM.js +1 -0
  64. package/public/dist/assets/settings-pPMHNs0G.js +1 -0
  65. package/public/dist/assets/{sidebar-DgIj-LuT.js → sidebar-BNT58qR0.js} +3 -3
  66. package/public/dist/assets/{skills-QqxjqDHg.js → skills-CwRBcm3m.js} +1 -1
  67. package/public/dist/assets/skills-DiVoaVdS.js +1 -0
  68. package/public/dist/assets/slash-commands-Bx4saHwR.js +1 -0
  69. package/public/dist/assets/{slash-commands-B2V_jngP.js → slash-commands-cm0U58n7.js} +1 -1
  70. package/public/dist/assets/{trace-drawer-CBe9nRjS.js → trace-drawer-BuwO4m2P.js} +1 -1
  71. package/public/dist/assets/{ui-CDozpEQt.js → ui-C-LHMki1.js} +1 -1
  72. package/public/dist/assets/ui-XPQDPe-5.js +1 -0
  73. package/public/dist/index.html +1 -1
  74. package/public/js/features/channel-onboarding.ts +8 -5
  75. package/public/locales/en.json +3 -2
  76. package/public/locales/ja.json +3 -2
  77. package/public/locales/ko.json +3 -2
  78. package/public/locales/zh.json +3 -2
  79. package/scripts/install-officecli.sh +28 -0
  80. package/public/dist/assets/bgtask-badge-CjWz0F6U.js +0 -1
  81. package/public/dist/assets/iframe-renderer-qRVLHPnT.js +0 -1
  82. package/public/dist/assets/memory-CcWGiPy_.js +0 -1
  83. package/public/dist/assets/message-history-BamMPUnL.js +0 -1
  84. package/public/dist/assets/settings-RGpifFHy.js +0 -1
  85. package/public/dist/assets/settings-core-X_5UHfr3.js +0 -1
  86. package/public/dist/assets/skills-CbwIimit.js +0 -1
  87. package/public/dist/assets/slash-commands-tCNRlNTC.js +0 -1
  88. 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
- // ─── Args ────────────────────────────────────────────
17
- const { values: opts, positionals: pos } = parseArgs({
18
- args: process.argv.slice(3),
19
- options: {
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
- // --backend whitelist validation
36
- const VALID_BACKENDS = new Set(['launchd', 'systemd', 'docker']);
37
- if (opts.backend && !VALID_BACKENDS.has(opts.backend)) {
38
- console.error(`❌ Unknown backend: ${opts.backend}`);
39
- console.error(' Supported: launchd, systemd, docker');
40
- process.exit(1);
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
- const PORT = opts.port;
43
- // port validation: must be numeric and in valid range
44
- const portNum = Number(PORT);
45
- if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) {
46
- console.error(`\u274c Invalid port: ${PORT}`);
47
- console.error(' Port must be an integer between 1 and 65535');
48
- process.exit(1);
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
- const INSTANCE = instanceId();
51
- const LOG_DIR = join(JAW_HOME, 'logs');
52
- function detectBackend() {
53
- if (process.platform === 'darwin')
54
- return 'launchd';
55
- // Docker container detection
56
- if (existsSync('/.dockerenv'))
57
- return 'docker';
58
- // Linux: verify PID 1 is actually systemd (not just binary installed)
59
- try {
60
- const pid1 = readFileSync('/proc/1/comm', 'utf8').trim();
61
- if (pid1 === 'systemd')
62
- return 'systemd';
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
- catch { /* /proc may not exist */ }
65
- // Fallback: check systemctl exists (less reliable but covers more cases)
66
- try {
67
- execFileSync('which', ['systemctl'], { stdio: 'pipe' });
68
- console.warn('⚠️ PID 1이 systemd가 아닐 수 있습니다. --backend systemd로 강제 지정 가능');
69
- return 'systemd';
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
- catch { /* no systemctl */ }
72
- console.error('❌ 지원되지 않는 환경입니다.');
73
- console.error(' 지원: macOS (launchd), Linux (systemd), Docker');
74
- console.error(' 수동 설정은 jaw serve를 tmux/screen에서 실행하세요.');
75
- process.exit(1);
76
- }
77
- const backend = opts.backend || detectBackend();
78
- // ─── macOS: delegate to launchd command (legacy) ────
79
- if (backend === 'launchd') {
80
- // launchd.ts supports: status, unset, default (install)
81
- // service.ts additionally supports: logs
82
- // Map unsupported subcommands for launchd
83
- const subcommand = pos[0];
84
- if (subcommand === 'logs') {
85
- // launchd doesn't have 'logs' → show log file path directly
86
- const logDir = join(JAW_HOME, 'logs');
87
- console.log(`📋 macOS launchd logs:\n`);
88
- console.log(` stdout: ${logDir}/jaw-serve.log`);
89
- console.log(` stderr: ${logDir}/jaw-serve.err\n`);
90
- console.log(` tail -f ${logDir}/jaw-serve.log`);
91
- process.exit(0);
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
- // Rebuild argv from parsed values — eliminates all --backend variants
94
- // Only pass --port on install (default). status/unset read from existing plist.
95
- const isInstall = !subcommand || (subcommand !== 'status' && subcommand !== 'unset');
96
- const portArgs = isInstall && PORT !== '3457' ? ['--port', PORT] : [];
97
- process.argv = [
98
- process.argv[0], process.argv[1],
99
- 'launchd',
100
- ...(subcommand ? [subcommand] : []),
101
- ...portArgs,
102
- ];
103
- process.env["_CLI_JAW_SERVICE_DELEGATE"] = '1';
104
- await import('./launchd.js');
105
- process.exit(0);
106
- }
107
- // ─── Docker: info only ───────────────────────────────
108
- if (backend === 'docker') {
109
- console.log('🐳 Docker 컨테이너 내부에서 실행 중입니다.\n');
110
- console.log(' 컨테이너 자체가 restart policy로 관리됩니다.');
111
- console.log(' docker-compose.yml의 restart: unless-stopped 설정을 확인하세요.\n');
112
- console.log(' 상태 확인: docker ps | grep cli-jaw');
113
- console.log(' 로그 확인: docker logs -f cli-jaw');
114
- process.exit(0);
115
- }
116
- // ═══════════════════════════════════════════════════════
117
- // systemd backend
118
- // ═══════════════════════════════════════════════════════
119
- const SAFE_INSTANCE = sanitizeUnitName(INSTANCE);
120
- const UNIT_NAME = `jaw-${SAFE_INSTANCE}`;
121
- const UNIT_PATH = `/etc/systemd/system/${UNIT_NAME}.service`;
122
- /** Create secure temp file for unit generation. */
123
- function makeTmpUnit() {
124
- try {
125
- return execFileSync('mktemp', ['/tmp/jaw-unit-XXXXXX.service'], { encoding: 'utf8' }).trim();
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
- catch {
128
- // fallback if mktemp unavailable
129
- const fallback = `/tmp/jaw-unit-${Date.now()}-${process.pid}.service`;
130
- return fallback;
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
- /** Run a sudo command with visible output (password prompt visible). */
134
- function sudo(args) {
135
- execFileSync('sudo', args, { stdio: 'inherit' });
136
- }
137
- function generateUnit() {
138
- const nodePath = getNodePath();
139
- const jawPath = getJawPath();
140
- const servicePath = buildServicePath(process.env["PATH"] || '', [dirname(nodePath), dirname(jawPath)]);
141
- let user;
142
- try {
143
- user = execFileSync('whoami', { encoding: 'utf8' }).trim();
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
- catch {
146
- user = 'nobody';
228
+ /** Run a sudo command with visible output (password prompt visible). */
229
+ function sudo(args) {
230
+ execFileSync('sudo', args, { stdio: 'inherit' });
147
231
  }
148
- mkdirSync(LOG_DIR, { recursive: true });
149
- // systemd quoting: paths with spaces must be quoted
150
- const q = (s) => s.includes(' ') ? `"${s}"` : s;
151
- return `[Unit]
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
- sudo(['systemctl', 'disable', UNIT_NAME]);
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
- // Read actual port from unit file if possible
207
- let displayPort = PORT;
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
- default: {
239
- // 원스텝: 유닛 생성 → daemon-reload → enable --now
240
- console.log('🦈 jaw service setup (systemd)\n');
241
- if (existsSync(UNIT_PATH)) {
242
- console.log('📄 기존 유닛 발견 재생성합니다');
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
- else {
249
- console.log('📄 유닛 없음 — 새로 생성합니다');
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
- // 1. 유닛 파일 생성 (mktemp → sudo cp)
252
- const unit = generateUnit();
253
- const tmpUnit = makeTmpUnit();
254
- writeFileSync(tmpUnit, unit);
255
- sudo(['cp', tmpUnit, UNIT_PATH]);
256
- try {
257
- writeFileSync(tmpUnit, '');
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
- catch { /* cleanup */ }
260
- console.log(`✅ 유닛 저장: ${UNIT_PATH}`);
261
- // 2. 등록 + 시작
262
- sudo(['systemctl', 'daemon-reload']);
263
- sudo(['systemctl', 'enable', '--now', UNIT_NAME]);
264
- console.log('✅ systemd 등록 + 시작 완료\n');
265
- // 3. 상태 확인
266
- setTimeout(() => {
267
- const state = isActive();
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
- }, 2000);
281
- break;
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