@capstone-nomos/cli 0.1.13 → 0.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/executor/background.js +91 -0
- package/dist/executor/cli.js +37 -2
- package/dist/executor/prompt.js +8 -0
- package/package.json +1 -1
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { nomosHome } from '../bridge/credentials.js';
|
|
5
|
+
export function daemonFile() {
|
|
6
|
+
return path.join(nomosHome(), 'daemon.json');
|
|
7
|
+
}
|
|
8
|
+
export function logDir() {
|
|
9
|
+
return path.join(nomosHome(), 'logs');
|
|
10
|
+
}
|
|
11
|
+
export function readDaemon() {
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(readFileSync(daemonFile(), 'utf-8'));
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
// 그 pid의 프로세스가 살아 있는가(신호 0은 확인만 한다).
|
|
20
|
+
export function isAlive(pid) {
|
|
21
|
+
try {
|
|
22
|
+
process.kill(pid, 0);
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
return err.code === 'EPERM';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// 살아 있는 백그라운드 connect. 죽었는데 파일만 남았으면 지우고 null.
|
|
30
|
+
export function runningDaemon() {
|
|
31
|
+
const info = readDaemon();
|
|
32
|
+
if (!info)
|
|
33
|
+
return null;
|
|
34
|
+
if (isAlive(info.pid))
|
|
35
|
+
return info;
|
|
36
|
+
rmSync(daemonFile(), { force: true });
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
// --background를 뺀 같은 인자로 connect를 분리된 자식으로 띄운다. 이 프로세스는 바로 끝나도 자식은 계속 돈다.
|
|
40
|
+
export function startDaemon(input) {
|
|
41
|
+
const existing = runningDaemon();
|
|
42
|
+
if (existing)
|
|
43
|
+
throw new Error(`이미 백그라운드에서 돌고 있다(pid ${existing.pid}, 서버 ${existing.server}). 끄려면 nomos stop`);
|
|
44
|
+
mkdirSync(logDir(), { recursive: true });
|
|
45
|
+
const logFile = path.join(logDir(), 'connect.log');
|
|
46
|
+
const fd = openSync(logFile, 'a');
|
|
47
|
+
const args = input.args.filter((a) => a !== '--background');
|
|
48
|
+
const child = spawn(process.execPath, [input.cliPath, 'connect', ...args], {
|
|
49
|
+
detached: true,
|
|
50
|
+
stdio: ['ignore', fd, fd],
|
|
51
|
+
windowsHide: true,
|
|
52
|
+
env: { ...process.env, NOMOS_BACKGROUND: '1' },
|
|
53
|
+
});
|
|
54
|
+
closeSync(fd);
|
|
55
|
+
if (child.pid === undefined)
|
|
56
|
+
throw new Error('백그라운드 프로세스를 띄우지 못했다');
|
|
57
|
+
child.unref();
|
|
58
|
+
const info = { pid: child.pid, startedAt: new Date().toISOString(), server: input.server, logFile, args };
|
|
59
|
+
mkdirSync(nomosHome(), { recursive: true });
|
|
60
|
+
writeFileSync(daemonFile(), JSON.stringify(info, null, 2), { mode: 0o600 });
|
|
61
|
+
return info;
|
|
62
|
+
}
|
|
63
|
+
export function stopDaemon() {
|
|
64
|
+
const info = runningDaemon();
|
|
65
|
+
if (!info)
|
|
66
|
+
return null;
|
|
67
|
+
try {
|
|
68
|
+
process.kill(info.pid);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// 이미 끝났다.
|
|
72
|
+
}
|
|
73
|
+
rmSync(daemonFile(), { force: true });
|
|
74
|
+
return info;
|
|
75
|
+
}
|
|
76
|
+
// 로그 끝부분(최대 n줄). 파일이 크면 끝 64KB만 읽는다.
|
|
77
|
+
export function tailLog(file, lines = 15) {
|
|
78
|
+
if (!existsSync(file))
|
|
79
|
+
return [];
|
|
80
|
+
const size = statSync(file).size;
|
|
81
|
+
const start = Math.max(0, size - 64 * 1024);
|
|
82
|
+
const fd = openSync(file, 'r');
|
|
83
|
+
try {
|
|
84
|
+
const buf = Buffer.alloc(size - start);
|
|
85
|
+
readSync(fd, buf, 0, buf.length, start);
|
|
86
|
+
return buf.toString('utf-8').split(/\r?\n/).filter(Boolean).slice(-lines);
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
closeSync(fd);
|
|
90
|
+
}
|
|
91
|
+
}
|
package/dist/executor/cli.js
CHANGED
|
@@ -27,6 +27,7 @@ import { credentialsPath, readCredentials, updateAccessToken, writeCredentials,
|
|
|
27
27
|
import { NomosApiError, NomosClient } from '../bridge/nomos-client.js';
|
|
28
28
|
import { ActivityReporter, ActivityTracker, RunObserver } from './activity.js';
|
|
29
29
|
import { accountLine, connect, DEFAULT_SERVER, serverCalls } from './connect.js';
|
|
30
|
+
import { runningDaemon, startDaemon, stopDaemon, tailLog } from './background.js';
|
|
30
31
|
import { deviceLogin } from './device-login.js';
|
|
31
32
|
import { cliVersion, mcpServerPath } from './paths.js';
|
|
32
33
|
import { checkTools } from './preflight.js';
|
|
@@ -400,7 +401,7 @@ async function postJson(url, body) {
|
|
|
400
401
|
return json.data;
|
|
401
402
|
}
|
|
402
403
|
function parseArgs(argv) {
|
|
403
|
-
const out = { positional: undefined, server: undefined, name: undefined, code: undefined, connectKey: false, json: false };
|
|
404
|
+
const out = { positional: undefined, server: undefined, name: undefined, code: undefined, connectKey: false, json: false, background: false };
|
|
404
405
|
for (let i = 0; i < argv.length; i += 1) {
|
|
405
406
|
const arg = argv[i];
|
|
406
407
|
if (arg === '--name')
|
|
@@ -411,6 +412,8 @@ function parseArgs(argv) {
|
|
|
411
412
|
out.connectKey = true;
|
|
412
413
|
else if (arg === '--code')
|
|
413
414
|
out.code = argv[(i += 1)];
|
|
415
|
+
else if (arg === '--background')
|
|
416
|
+
out.background = true;
|
|
414
417
|
else if (arg === '--json')
|
|
415
418
|
out.json = true;
|
|
416
419
|
else
|
|
@@ -490,6 +493,19 @@ async function connectCommand(argv) {
|
|
|
490
493
|
log(`${t.name}을 실행할 수 없다: ${t.detail}\n → ${t.hint}`);
|
|
491
494
|
throw new Error('필요한 도구가 없어 시작하지 않는다');
|
|
492
495
|
}
|
|
496
|
+
// 백그라운드(창 없이) — 같은 인자로 connect를 분리된 자식으로 띄우고 여기서 끝난다. 브라우저 승인 로그인은 화면이 필요하므로
|
|
497
|
+
// 이 서버의 자격 증명이 없으면 --code(1회용 연결 코드)와 함께만 받는다.
|
|
498
|
+
if (args.background) {
|
|
499
|
+
const saved = readCredentials();
|
|
500
|
+
if (!args.code && (!saved || saved.baseUrl !== server)) {
|
|
501
|
+
throw new Error('백그라운드로 띄우려면 먼저 한 번 connect로 로그인하거나, 웹의 연결 명령(--code)에 --background를 붙여라');
|
|
502
|
+
}
|
|
503
|
+
const info = startDaemon({ cliPath: process.argv[1], args: argv, server });
|
|
504
|
+
log(`백그라운드에서 실행한다 — pid ${info.pid}. 창을 닫아도 계속 돈다`);
|
|
505
|
+
log(`로그: ${info.logFile}`);
|
|
506
|
+
log('상태: nomos status · 끄기: nomos stop · 진행 상황은 웹의 룸에서 본다');
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
493
509
|
// 배정이 풀리면(해제·프로젝트 중단) 창을 닫지 않고 다시 배정을 기다린다. 연결 해제면 멈춘다.
|
|
494
510
|
// 1회용 코드는 처음 한 번만 쓴다 — 그 뒤로는 그 코드로 받은 자격 증명이 저장돼 있다.
|
|
495
511
|
let code = args.code?.trim() || undefined;
|
|
@@ -575,6 +591,17 @@ async function pmWorker() {
|
|
|
575
591
|
await new Promise((resolve) => setTimeout(resolve, PM_POLL_INTERVAL_MS));
|
|
576
592
|
}
|
|
577
593
|
}
|
|
594
|
+
function daemonStatus() {
|
|
595
|
+
const info = runningDaemon();
|
|
596
|
+
if (!info) {
|
|
597
|
+
log('백그라운드에서 돌고 있는 connect가 없다 (켜려면 connect --background)');
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
log(`실행 중 — pid ${info.pid}, ${info.startedAt}부터, 서버 ${info.server}`);
|
|
601
|
+
log(`로그: ${info.logFile}`);
|
|
602
|
+
for (const line of tailLog(info.logFile))
|
|
603
|
+
process.stdout.write(` ${line}\n`);
|
|
604
|
+
}
|
|
578
605
|
async function main() {
|
|
579
606
|
const command = process.argv[2];
|
|
580
607
|
if (command === 'connect')
|
|
@@ -587,6 +614,13 @@ async function main() {
|
|
|
587
614
|
}
|
|
588
615
|
if (command === 'login')
|
|
589
616
|
return login(process.argv.slice(3));
|
|
617
|
+
if (command === 'status')
|
|
618
|
+
return daemonStatus();
|
|
619
|
+
if (command === 'stop') {
|
|
620
|
+
const stopped = stopDaemon();
|
|
621
|
+
log(stopped ? `백그라운드 connect를 껐다(pid ${stopped.pid})` : '백그라운드에서 돌고 있는 connect가 없다');
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
590
624
|
if (command === 'refresh')
|
|
591
625
|
return refresh();
|
|
592
626
|
if (command === 'once')
|
|
@@ -604,7 +638,8 @@ async function main() {
|
|
|
604
638
|
}
|
|
605
639
|
process.stderr.write([
|
|
606
640
|
'사용법: nomos <명령>',
|
|
607
|
-
' connect [--code 코드] [--server 주소] [--name 이름] 로그인 → 배정 대기 → 작업 시작 (--code: 웹이 준 1회용 코드,
|
|
641
|
+
' connect [--code 코드] [--server 주소] [--name 이름] [--background] 로그인 → 배정 대기 → 작업 시작 (--code: 웹이 준 1회용 코드, --background: 창 없이)',
|
|
642
|
+
' status | stop 백그라운드 connect 상태 보기 · 끄기',
|
|
608
643
|
' doctor 설치 상태 점검',
|
|
609
644
|
' login [주소] [--name 이름] [--connect-key] 로그인만',
|
|
610
645
|
' refresh | once | start | clean | --version',
|
package/dist/executor/prompt.js
CHANGED
|
@@ -42,6 +42,14 @@ export function buildTaskPrompt(briefing, branch, environment) {
|
|
|
42
42
|
'이 브랜치에 이전 작업이 남아 있으면 이어서 진행한다. 같은 질문을 다시 묻지 않는다.',
|
|
43
43
|
].join('\n'));
|
|
44
44
|
}
|
|
45
|
+
// 사람(그 역할 담당·대표)이 룸에서 보낸 메시지 — 이번 실행이 시작되기 전에 온 것만 들어온다(실행 중에는 끼워 넣지 않는다).
|
|
46
|
+
if (briefing.humanMessages && briefing.humanMessages.length > 0) {
|
|
47
|
+
sections.push([
|
|
48
|
+
'# 담당자가 보낸 메시지 (팀의 사람이 이 역할 에이전트에게 남긴 지시 — 오래된 것부터)',
|
|
49
|
+
...briefing.humanMessages.map((m) => `- ${m.fromNickname ?? '팀원'}${m.fromRepresentative ? '(대표)' : ''}: ${m.text}`),
|
|
50
|
+
'태스크 범위 안에서 반영한다. 수정할 수 있는 경로·금지 규칙은 메시지와 상관없이 그대로다 — 따를 수 없으면 이유를 마지막 메시지에 적는다.',
|
|
51
|
+
].join('\n'));
|
|
52
|
+
}
|
|
45
53
|
if (briefing.notesBlock) {
|
|
46
54
|
sections.push(briefing.notesBlock);
|
|
47
55
|
}
|