create-task-ops 0.1.7 → 0.1.8
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
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
node packages/create-task-ops/bin/create-task-ops.js my-project
|
|
15
15
|
node packages/create-task-ops/bin/create-task-ops.js create my-project
|
|
16
16
|
node packages/create-task-ops/bin/create-task-ops.js add
|
|
17
|
+
node packages/create-task-ops/bin/create-task-ops.js add --target apps/server
|
|
17
18
|
node packages/create-task-ops/bin/create-task-ops.js add --docs-only
|
|
18
19
|
```
|
|
19
20
|
|
|
@@ -23,6 +24,7 @@ publish 후에는 아래처럼 쓸 수 있게 설계했다.
|
|
|
23
24
|
npx create-task-ops my-project
|
|
24
25
|
npx create-task-ops create my-project
|
|
25
26
|
npx create-task-ops add
|
|
27
|
+
npx create-task-ops add --target apps/server
|
|
26
28
|
npx create-task-ops add --docs-only
|
|
27
29
|
```
|
|
28
30
|
|
|
@@ -34,6 +36,8 @@ npx create-task-ops add --docs-only
|
|
|
34
36
|
위와 같지만 명시적으로 create를 적는 형태
|
|
35
37
|
- `create-task-ops add`
|
|
36
38
|
현재 리포에 task docs + OrbitOps API 추가
|
|
39
|
+
- `create-task-ops add --target apps/server`
|
|
40
|
+
문서와 tasks는 리포 루트에 두고, OrbitOps receiver만 `apps/server` 아래에 추가
|
|
37
41
|
- `create-task-ops add --docs-only`
|
|
38
42
|
현재 리포에 문서와 task 파일 규약만 추가
|
|
39
43
|
|
|
@@ -55,6 +59,8 @@ npx create-task-ops add --docs-only
|
|
|
55
59
|
- `AGENT.md`
|
|
56
60
|
- `docs/TASK_API_CONTRACT.md`
|
|
57
61
|
- `docs/DASHBOARD_CONNECTION.md`
|
|
62
|
+
- `docs/HEARTBEAT_SETUP.md`
|
|
63
|
+
- `scripts/orbitops-heartbeat.mjs`
|
|
58
64
|
- `app/api/orbitops/health/route.ts`
|
|
59
65
|
- `app/api/orbitops/tasks/route.ts`
|
|
60
66
|
- `app/api/orbitops/tasks/[id]/route.ts`
|
|
@@ -62,6 +68,19 @@ npx create-task-ops add --docs-only
|
|
|
62
68
|
|
|
63
69
|
즉 기존 `package.json`, `app/page.tsx`, `app/layout.tsx`, `tsconfig.json` 같은 루트 파일은 건드리지 않는다.
|
|
64
70
|
|
|
71
|
+
모노레포라면 `--target` 으로 receiver를 실제 서버 앱에 넣는 편이 맞다.
|
|
72
|
+
|
|
73
|
+
예:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
npx create-task-ops add --target apps/server
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
이 경우:
|
|
80
|
+
- `tasks/`, `CLAUDE.md`, `AGENT.md`, `docs/*` 는 리포 루트에 생성
|
|
81
|
+
- `scripts/orbitops-heartbeat.mjs` 도 리포 루트에 생성
|
|
82
|
+
- `app/api/orbitops/*`, `lib/orbitops/task-api.ts` 는 `apps/server` 아래에 생성
|
|
83
|
+
|
|
65
84
|
기본 동작에서 기존 `CLAUDE.md`, `AGENT.md`, `tasks/*`, `docs/*` 같은 운영 문서는 스킵한다.
|
|
66
85
|
|
|
67
86
|
대신 아래 receiver 경로가 이미 있으면 충돌 가능성이 크므로 경고 후 중단한다.
|
|
@@ -79,6 +98,7 @@ npx create-task-ops add --docs-only
|
|
|
79
98
|
npm run create-task-ops -- my-project
|
|
80
99
|
npm run create-task-ops -- create my-project
|
|
81
100
|
npm run create-task-ops -- add
|
|
101
|
+
npm run create-task-ops -- add --target apps/server
|
|
82
102
|
npm run create-task-ops -- add --docs-only
|
|
83
103
|
cd packages/create-task-ops
|
|
84
104
|
npm run smoke:full
|
package/bin/create-task-ops.js
CHANGED
|
@@ -22,6 +22,7 @@ function parseArgs(argv) {
|
|
|
22
22
|
mode: "full",
|
|
23
23
|
docsOnly: false,
|
|
24
24
|
force: false,
|
|
25
|
+
target: null,
|
|
25
26
|
};
|
|
26
27
|
|
|
27
28
|
for (let index = 0; index < argv.length; index += 1) {
|
|
@@ -43,6 +44,11 @@ function parseArgs(argv) {
|
|
|
43
44
|
options.docsOnly = true;
|
|
44
45
|
continue;
|
|
45
46
|
}
|
|
47
|
+
if (arg === "--target") {
|
|
48
|
+
options.target = argv[index + 1] ?? null;
|
|
49
|
+
index += 1;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
46
52
|
if (arg === "--force") {
|
|
47
53
|
options.force = true;
|
|
48
54
|
continue;
|
|
@@ -67,6 +73,7 @@ function printUsage() {
|
|
|
67
73
|
console.log(" create-task-ops <project-name>");
|
|
68
74
|
console.log(" create-task-ops create <project-name>");
|
|
69
75
|
console.log(" create-task-ops add");
|
|
76
|
+
console.log(" create-task-ops add --target apps/server");
|
|
70
77
|
console.log(" create-task-ops add --docs-only");
|
|
71
78
|
}
|
|
72
79
|
|
|
@@ -171,6 +178,9 @@ function main() {
|
|
|
171
178
|
const targetDir = options.command === "add"
|
|
172
179
|
? process.cwd()
|
|
173
180
|
: path.resolve(process.cwd(), options.name ?? projectName);
|
|
181
|
+
const receiverTargetDir = options.command === "add" && options.target
|
|
182
|
+
? path.resolve(process.cwd(), options.target)
|
|
183
|
+
: targetDir;
|
|
174
184
|
|
|
175
185
|
if (options.command === "create" && existsSync(targetDir) && readdirSync(targetDir).length > 0 && !options.force) {
|
|
176
186
|
console.error(`Target directory is not empty: ${targetDir}`);
|
|
@@ -178,6 +188,12 @@ function main() {
|
|
|
178
188
|
process.exit(1);
|
|
179
189
|
}
|
|
180
190
|
|
|
191
|
+
if (options.command === "add" && options.target && !existsSync(receiverTargetDir)) {
|
|
192
|
+
console.error(`Receiver target does not exist: ${receiverTargetDir}`);
|
|
193
|
+
console.error("Create the app directory first, then run add with --target.");
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
|
|
181
197
|
mkdirSync(targetDir, { recursive: true });
|
|
182
198
|
|
|
183
199
|
const commonDir = path.join(templatesRoot, "common");
|
|
@@ -186,19 +202,22 @@ function main() {
|
|
|
186
202
|
const skipExistingDocs = options.command === "add";
|
|
187
203
|
|
|
188
204
|
if (options.command === "add" && options.mode === "api") {
|
|
189
|
-
assertReceiverPathsAvailable(
|
|
205
|
+
assertReceiverPathsAvailable(receiverTargetDir, options.force, addApiFiles);
|
|
190
206
|
}
|
|
191
207
|
|
|
192
208
|
copyTemplateTree(commonDir, targetDir, projectName, options.force, skipExistingDocs);
|
|
193
209
|
|
|
194
210
|
if (options.command === "add" && options.mode === "api") {
|
|
195
|
-
copySelectedFiles(modeDir,
|
|
211
|
+
copySelectedFiles(modeDir, receiverTargetDir, projectName, options.force, addApiFiles, false);
|
|
196
212
|
} else {
|
|
197
213
|
copyTemplateTree(modeDir, targetDir, projectName, options.force, skipExistingDocs);
|
|
198
214
|
}
|
|
199
215
|
|
|
200
216
|
console.log(`create-task-ops completed`);
|
|
201
217
|
console.log(`target: ${targetDir}`);
|
|
218
|
+
if (options.command === "add" && options.mode === "api") {
|
|
219
|
+
console.log(`receiver target: ${receiverTargetDir}`);
|
|
220
|
+
}
|
|
202
221
|
console.log(`command: ${options.command}`);
|
|
203
222
|
console.log(`mode: ${options.mode}`);
|
|
204
223
|
console.log(`project: ${projectName}`);
|
package/package.json
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
- `GET /api/orbitops/health`
|
|
8
8
|
- `GET /api/orbitops/tasks`
|
|
9
9
|
- `GET /api/orbitops/tasks/:id`
|
|
10
|
+
- `POST <central>/api/orbitops/agents/heartbeat` optional
|
|
10
11
|
|
|
11
12
|
## 로컬 개발 연결 예시
|
|
12
13
|
|
|
@@ -33,3 +34,4 @@
|
|
|
33
34
|
2. `/api/orbitops/tasks` 응답이 정상인가
|
|
34
35
|
3. source `id` 와 `label` 이 명확한가
|
|
35
36
|
4. 필요하면 API key 또는 reverse proxy 를 설정했는가
|
|
37
|
+
5. presence 가 필요하면 `docs/HEARTBEAT_SETUP.md` 를 따라 heartbeat sender 를 설정했는가
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Heartbeat Setup
|
|
2
|
+
|
|
3
|
+
이 프로젝트는 중앙 대시보드로 `presence` heartbeat 를 보낼 수 있다.
|
|
4
|
+
|
|
5
|
+
## 목적
|
|
6
|
+
|
|
7
|
+
- 리모트 에이전트가 현재 `online`, `idle`, `paused`, `offline` 인지 중앙에서 본다.
|
|
8
|
+
- heartbeat 가 끊기면 중앙은 일정 시간 뒤 `unreachable` 로 처리한다.
|
|
9
|
+
|
|
10
|
+
## 중앙 대시보드 엔드포인트
|
|
11
|
+
|
|
12
|
+
- `POST /api/orbitops/agents/heartbeat`
|
|
13
|
+
|
|
14
|
+
## 포함된 sender 스크립트
|
|
15
|
+
|
|
16
|
+
- `scripts/orbitops-heartbeat.mjs`
|
|
17
|
+
|
|
18
|
+
이 스크립트는:
|
|
19
|
+
|
|
20
|
+
- 시작 시 즉시 heartbeat 전송
|
|
21
|
+
- 이후 주기적으로 heartbeat 반복 전송
|
|
22
|
+
- 프로세스 종료 시 `offline` 전송 시도
|
|
23
|
+
|
|
24
|
+
## 필요한 환경 변수
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
ORBITOPS_HEARTBEAT_URL=http://127.0.0.1:3000/api/orbitops/agents/heartbeat
|
|
28
|
+
ORBITOPS_SOURCE_ID=__PROJECT_NAME__
|
|
29
|
+
ORBITOPS_AGENT_ID=__PROJECT_NAME__-server-01
|
|
30
|
+
ORBITOPS_AGENT=codex
|
|
31
|
+
ORBITOPS_NOTE=Task API receiver is online
|
|
32
|
+
ORBITOPS_HEARTBEAT_INTERVAL_MS=15000
|
|
33
|
+
ORBITOPS_HEARTBEAT_API_KEY=
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
선택값:
|
|
37
|
+
|
|
38
|
+
- `ORBITOPS_PRESENCE`
|
|
39
|
+
기본값 `online`
|
|
40
|
+
- `ORBITOPS_HOST`
|
|
41
|
+
- `ORBITOPS_RUN_ID`
|
|
42
|
+
|
|
43
|
+
## 실행 예시
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
node scripts/orbitops-heartbeat.mjs
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## systemd 또는 tmux 사용 권장
|
|
50
|
+
|
|
51
|
+
실서비스에서는 앱 프로세스와 따로 heartbeat sender 를 띄우기보다:
|
|
52
|
+
|
|
53
|
+
- 앱이 직접 heartbeat 를 보내게 하거나
|
|
54
|
+
- process manager 에서 sender 를 같이 올리는 방식이 낫다.
|
|
55
|
+
|
|
56
|
+
지금 스크립트는 파일럿 및 초기 운영용 기본 예제다.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
|
|
6
|
+
const heartbeatUrl = process.env.ORBITOPS_HEARTBEAT_URL;
|
|
7
|
+
const heartbeatSourceId = process.env.ORBITOPS_SOURCE_ID || "__PROJECT_NAME__";
|
|
8
|
+
const heartbeatAgentId =
|
|
9
|
+
process.env.ORBITOPS_AGENT_ID || `${heartbeatSourceId}-${os.hostname()}-${process.pid}`;
|
|
10
|
+
const heartbeatPresence = process.env.ORBITOPS_PRESENCE || "online";
|
|
11
|
+
const heartbeatAgent = process.env.ORBITOPS_AGENT || "codex";
|
|
12
|
+
const heartbeatNote = process.env.ORBITOPS_NOTE || "Task API receiver is online";
|
|
13
|
+
const heartbeatHost = process.env.ORBITOPS_HOST || os.hostname();
|
|
14
|
+
const heartbeatRunId = process.env.ORBITOPS_RUN_ID || "";
|
|
15
|
+
const heartbeatIntervalMs = Number.parseInt(process.env.ORBITOPS_HEARTBEAT_INTERVAL_MS || "15000", 10);
|
|
16
|
+
const heartbeatApiKey = process.env.ORBITOPS_HEARTBEAT_API_KEY || "";
|
|
17
|
+
|
|
18
|
+
if (!heartbeatUrl) {
|
|
19
|
+
console.error("ORBITOPS_HEARTBEAT_URL is required");
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function sendHeartbeat(presence = heartbeatPresence) {
|
|
24
|
+
const response = await fetch(heartbeatUrl, {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers: {
|
|
27
|
+
"Content-Type": "application/json",
|
|
28
|
+
...(heartbeatApiKey ? { Authorization: `Bearer ${heartbeatApiKey}` } : {}),
|
|
29
|
+
},
|
|
30
|
+
body: JSON.stringify({
|
|
31
|
+
agentId: heartbeatAgentId,
|
|
32
|
+
sourceId: heartbeatSourceId,
|
|
33
|
+
presence,
|
|
34
|
+
host: heartbeatHost,
|
|
35
|
+
pid: process.pid,
|
|
36
|
+
runId: heartbeatRunId || undefined,
|
|
37
|
+
agent: heartbeatAgent,
|
|
38
|
+
note: heartbeatNote,
|
|
39
|
+
sentAt: new Date().toISOString(),
|
|
40
|
+
}),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (!response.ok) {
|
|
44
|
+
const detail = await response.text().catch(() => "");
|
|
45
|
+
throw new Error(`Heartbeat failed: ${response.status} ${detail}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
console.log(`[orbitops] heartbeat sent: ${presence}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
await sendHeartbeat();
|
|
52
|
+
const timer = setInterval(() => {
|
|
53
|
+
void sendHeartbeat().catch((error) => {
|
|
54
|
+
console.error("[orbitops] heartbeat error", error);
|
|
55
|
+
});
|
|
56
|
+
}, heartbeatIntervalMs);
|
|
57
|
+
|
|
58
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
59
|
+
process.on(signal, () => {
|
|
60
|
+
clearInterval(timer);
|
|
61
|
+
void sendHeartbeat("offline")
|
|
62
|
+
.catch(() => null)
|
|
63
|
+
.finally(() => process.exit(0));
|
|
64
|
+
});
|
|
65
|
+
}
|