create-task-ops 0.1.6 → 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
|
|
|
@@ -138,6 +145,22 @@ function copySelectedFiles(sourceDir, targetDir, projectName, force, selectedFil
|
|
|
138
145
|
}
|
|
139
146
|
}
|
|
140
147
|
|
|
148
|
+
function assertReceiverPathsAvailable(targetDir, force, selectedFiles) {
|
|
149
|
+
if (force) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
for (const relativeFile of selectedFiles) {
|
|
154
|
+
const targetPath = path.join(targetDir, relativeFile);
|
|
155
|
+
if (existsSync(targetPath)) {
|
|
156
|
+
console.error(`Refusing to overwrite existing receiver file: ${targetPath}`);
|
|
157
|
+
console.error("This path is reserved for task API integration.");
|
|
158
|
+
console.error("Use --force if you want to replace the existing receiver files.");
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
141
164
|
function main() {
|
|
142
165
|
const options = parseArgs(args);
|
|
143
166
|
if (options.command === "add") {
|
|
@@ -155,6 +178,9 @@ function main() {
|
|
|
155
178
|
const targetDir = options.command === "add"
|
|
156
179
|
? process.cwd()
|
|
157
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;
|
|
158
184
|
|
|
159
185
|
if (options.command === "create" && existsSync(targetDir) && readdirSync(targetDir).length > 0 && !options.force) {
|
|
160
186
|
console.error(`Target directory is not empty: ${targetDir}`);
|
|
@@ -162,6 +188,12 @@ function main() {
|
|
|
162
188
|
process.exit(1);
|
|
163
189
|
}
|
|
164
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
|
+
|
|
165
197
|
mkdirSync(targetDir, { recursive: true });
|
|
166
198
|
|
|
167
199
|
const commonDir = path.join(templatesRoot, "common");
|
|
@@ -169,16 +201,23 @@ function main() {
|
|
|
169
201
|
|
|
170
202
|
const skipExistingDocs = options.command === "add";
|
|
171
203
|
|
|
204
|
+
if (options.command === "add" && options.mode === "api") {
|
|
205
|
+
assertReceiverPathsAvailable(receiverTargetDir, options.force, addApiFiles);
|
|
206
|
+
}
|
|
207
|
+
|
|
172
208
|
copyTemplateTree(commonDir, targetDir, projectName, options.force, skipExistingDocs);
|
|
173
209
|
|
|
174
210
|
if (options.command === "add" && options.mode === "api") {
|
|
175
|
-
copySelectedFiles(modeDir,
|
|
211
|
+
copySelectedFiles(modeDir, receiverTargetDir, projectName, options.force, addApiFiles, false);
|
|
176
212
|
} else {
|
|
177
213
|
copyTemplateTree(modeDir, targetDir, projectName, options.force, skipExistingDocs);
|
|
178
214
|
}
|
|
179
215
|
|
|
180
216
|
console.log(`create-task-ops completed`);
|
|
181
217
|
console.log(`target: ${targetDir}`);
|
|
218
|
+
if (options.command === "add" && options.mode === "api") {
|
|
219
|
+
console.log(`receiver target: ${receiverTargetDir}`);
|
|
220
|
+
}
|
|
182
221
|
console.log(`command: ${options.command}`);
|
|
183
222
|
console.log(`mode: ${options.mode}`);
|
|
184
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
|
+
}
|