coxpit 5.10.0 → 5.12.0
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/LICENSE.md +717 -0
- package/README.md +5 -1
- package/package.json +2 -2
- package/src/cockpit.ts +429 -163
- package/src/db/index.ts +1 -0
- package/src/db/schema.ts +1 -0
- package/src/orchestrator.ts +53 -1
- package/src/server.ts +26 -1
- package/LICENSE +0 -21
package/src/db/index.ts
CHANGED
|
@@ -106,4 +106,5 @@ export async function ensureSchema(): Promise<void> {
|
|
|
106
106
|
try { await client.execute("ALTER TABLE repos ADD COLUMN verify_cmd TEXT NOT NULL DEFAULT ''"); } catch { /* exists */ }
|
|
107
107
|
try { await client.execute("ALTER TABLE agent_runs ADD COLUMN verify_status TEXT NOT NULL DEFAULT ''"); } catch { /* exists */ }
|
|
108
108
|
try { await client.execute("ALTER TABLE agent_runs ADD COLUMN verify_output TEXT NOT NULL DEFAULT ''"); } catch { /* exists */ }
|
|
109
|
+
try { await client.execute("ALTER TABLE repos ADD COLUMN kind TEXT NOT NULL DEFAULT 'git'"); } catch { /* exists */ }
|
|
109
110
|
}
|
package/src/db/schema.ts
CHANGED
|
@@ -20,6 +20,7 @@ export const repos = sqliteTable('repos', {
|
|
|
20
20
|
name: text('name').notNull(),
|
|
21
21
|
defaultBranch: text('default_branch').notNull().default('main'),
|
|
22
22
|
verifyCmd: text('verify_cmd').notNull().default(''), // 정착한 run 을 검증하는 명령(테스트·빌드). 빈값 = 검증 없음
|
|
23
|
+
kind: text('kind').notNull().default('git'), // 'git' = 실제 repo | 'sessions' = 자유 세션 담는 가상 버킷(프로젝트 아님)
|
|
23
24
|
});
|
|
24
25
|
|
|
25
26
|
/** Design Mode 캡처 — 북마클릿 인스펙터가 보낸 UI 요소 컨텍스트. */
|
package/src/orchestrator.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { existsSync, statSync, openSync, readSync, closeSync, mkdirSync } from '
|
|
|
5
5
|
import { mkdir, copyFile, readFile, writeFile, rm, unlink } from 'node:fs/promises';
|
|
6
6
|
import { homedir } from 'node:os';
|
|
7
7
|
import type { ChildProcess } from 'node:child_process';
|
|
8
|
-
import { eq, inArray } from 'drizzle-orm';
|
|
8
|
+
import { eq, inArray, and } from 'drizzle-orm';
|
|
9
9
|
import { config } from './config';
|
|
10
10
|
import { db } from './db';
|
|
11
11
|
import { agentRuns, agentEvents, tasks, repos, machines, designCaptures, docSnapshots, taskGroups } from './db/schema';
|
|
@@ -1025,6 +1025,58 @@ export async function openWorkbench(repoId: number, title: string, root = false)
|
|
|
1025
1025
|
return { ok: true, detail: root ? 'root session open' : 'workbench open', taskId: task.id, runId };
|
|
1026
1026
|
}
|
|
1027
1027
|
|
|
1028
|
+
/**
|
|
1029
|
+
* 머신별 가상 "Sessions" 버킷(kind='sessions') 찾기-또는-만들기.
|
|
1030
|
+
* 자유 세션은 실제 프로젝트(repo)에 소속되지 않도록 이 버킷 밑에 담긴다 — 트리에서 별도 SESSIONS 섹션.
|
|
1031
|
+
*/
|
|
1032
|
+
async function ensureSessionsRepo(machineId: number): Promise<typeof repos.$inferSelect> {
|
|
1033
|
+
const found = await db.select().from(repos).where(and(eq(repos.machineId, machineId), eq(repos.kind, 'sessions'))).limit(1);
|
|
1034
|
+
if (found[0]) return found[0];
|
|
1035
|
+
const ins = await db.insert(repos).values({ machineId, path: homedir(), name: 'Sessions', defaultBranch: '', kind: 'sessions' }).returning();
|
|
1036
|
+
return ins[0]!;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
/**
|
|
1040
|
+
* 자유 세션 — 임의 폴더에서 tmux 셸을 연다. 특정 프로젝트에 소속되지 않음(가상 Sessions 버킷).
|
|
1041
|
+
* git worktree 아님(branch=''), merge 자동 거부·cleanup 은 tmux 만 정리(폴더 보존).
|
|
1042
|
+
*/
|
|
1043
|
+
export async function openSessionAt(machineSlug: string, path: string, title: string): Promise<{
|
|
1044
|
+
ok: boolean; detail: string; taskId?: number; runId?: number;
|
|
1045
|
+
}> {
|
|
1046
|
+
const dir = (path || '').trim();
|
|
1047
|
+
if (!dir.startsWith('/')) return { ok: false, detail: 'absolute path required' };
|
|
1048
|
+
const mr = await db.select().from(machines).where(eq(machines.slug, machineSlug)).limit(1);
|
|
1049
|
+
const m = mr[0];
|
|
1050
|
+
if (!m) return { ok: false, detail: 'machine not found' };
|
|
1051
|
+
const machine: MachineTarget = { slug: m.slug, kind: m.kind, address: m.address, sshUser: m.sshUser };
|
|
1052
|
+
const chk = await runShellOn(machine, `test -d ${shq(dir)} && echo yes`, 8000);
|
|
1053
|
+
if (!/yes/.test(chk.stdout)) return { ok: false, detail: 'folder not found: ' + dir };
|
|
1054
|
+
|
|
1055
|
+
const bucket = await ensureSessionsRepo(m.id);
|
|
1056
|
+
const name = title || dir.split('/').filter(Boolean).pop() || dir;
|
|
1057
|
+
const tIns = await db.insert(tasks).values({ repoId: bucket.id, title: name, prompt: '(session)' }).returning();
|
|
1058
|
+
const task = tIns[0]!;
|
|
1059
|
+
const rIns = await db.insert(agentRuns).values({ taskId: task.id, machineId: m.id, agent: 'session', status: 'pending' }).returning();
|
|
1060
|
+
const run = rIns[0]!;
|
|
1061
|
+
const runId = run.id;
|
|
1062
|
+
broadcast({ type: 'run', runId, taskId: task.id, status: 'pending', agent: 'session', branch: '', filesChanged: 0 });
|
|
1063
|
+
|
|
1064
|
+
const session = `coxpit-r${runId}`;
|
|
1065
|
+
const prep = await runShellOn(
|
|
1066
|
+
machine,
|
|
1067
|
+
`export LANG=${shq(config.lang)}; { tmux kill-session -t ${shq('=' + session)} 2>/dev/null || true; }` +
|
|
1068
|
+
` && tmux new-session -d -s ${shq(session)} -c ${shq(dir)}`,
|
|
1069
|
+
15000,
|
|
1070
|
+
);
|
|
1071
|
+
if (!prep.ok) {
|
|
1072
|
+
await setRun(runId, { status: 'error', endedAt: new Date(), exitSummary: 'session prep failed' });
|
|
1073
|
+
return { ok: false, detail: (prep.stderr || prep.stdout).trim().slice(0, 300) };
|
|
1074
|
+
}
|
|
1075
|
+
await setRun(runId, { status: 'open', branch: '', worktreePath: dir, tmuxWindow: session, startedAt: new Date() });
|
|
1076
|
+
await recordEvent(runId, 'meta', JSON.stringify({ session: true, path: dir }));
|
|
1077
|
+
return { ok: true, detail: 'session open', taskId: task.id, runId };
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1028
1080
|
/**
|
|
1029
1081
|
* 그룹에 속한 태스크 1개를 만들고 run 1개를 발사한다(공용 helper).
|
|
1030
1082
|
* planFanout(plan 형제) 과 /api/groups/:id/spawn(+New attempt) 이 공유하는
|
package/src/server.ts
CHANGED
|
@@ -19,7 +19,7 @@ import { db } from './db';
|
|
|
19
19
|
import { machines, repos, tasks, agentRuns, agentEvents, designCaptures, shareLinks, taskGroups } from './db/schema';
|
|
20
20
|
import { BOOKMARKLET_JS } from './design';
|
|
21
21
|
import { runShellOn, shq } from './exec';
|
|
22
|
-
import { launchRun, cleanupRun, stopRun, getRunDiff, loadRunDocs, mergeRun, getRunTermInfo, steerRun, exportRun, prRun, integrateRuns, planFanout, reviewTask, syncRun, openWorkbench, spawnSubtasks, listSubtasks, resolveAgentToken, taskCloseRisk, launchGroupTask, isRunLive, askGroupCoordinator, computeRunOutputs, normalizeOutputs, listReclaimableWorktrees, pruneWorktrees, noopSignal, groupOverlap, landTarget, mergePreview, startLandResolve, listDocuments, verifyRun } from './orchestrator';
|
|
22
|
+
import { launchRun, cleanupRun, stopRun, getRunDiff, loadRunDocs, mergeRun, getRunTermInfo, steerRun, exportRun, prRun, integrateRuns, planFanout, reviewTask, syncRun, openWorkbench, spawnSubtasks, listSubtasks, resolveAgentToken, taskCloseRisk, launchGroupTask, isRunLive, askGroupCoordinator, computeRunOutputs, normalizeOutputs, listReclaimableWorktrees, pruneWorktrees, noopSignal, groupOverlap, landTarget, mergePreview, startLandResolve, listDocuments, verifyRun, openSessionAt } from './orchestrator';
|
|
23
23
|
import { openTerm } from './term';
|
|
24
24
|
import { addSink, removeSink, broadcast } from './hub';
|
|
25
25
|
import { getProvider, listProviders } from './providers';
|
|
@@ -846,6 +846,20 @@ export async function buildServer(): Promise<FastifyInstance> {
|
|
|
846
846
|
return { task: tr[0], runs };
|
|
847
847
|
});
|
|
848
848
|
|
|
849
|
+
// 태스크 이름 변경(=세션 이름 변경). title 만 갱신.
|
|
850
|
+
app.patch('/api/tasks/:id', async (req, reply) => {
|
|
851
|
+
const id = Number((req.params as { id: string }).id);
|
|
852
|
+
const b = (req.body ?? {}) as { title?: string };
|
|
853
|
+
const title = (b.title ?? '').trim();
|
|
854
|
+
if (!title) return reply.code(400).send({ error: 'title required' });
|
|
855
|
+
if (title.length > 140) return reply.code(400).send({ error: 'title too long (max 140)' });
|
|
856
|
+
const tr = await db.select().from(tasks).where(eq(tasks.id, id)).limit(1);
|
|
857
|
+
if (!tr[0]) return reply.code(404).send({ error: 'not found' });
|
|
858
|
+
await db.update(tasks).set({ title }).where(eq(tasks.id, id));
|
|
859
|
+
broadcast({ type: 'task', taskId: id, title });
|
|
860
|
+
return { ok: true, title };
|
|
861
|
+
});
|
|
862
|
+
|
|
849
863
|
// N개의 에이전트 run 을 만들고 각자 오케스트레이션 시작(fire-and-forget).
|
|
850
864
|
app.post('/api/tasks/:id/run', async (req, reply) => {
|
|
851
865
|
const id = Number((req.params as { id: string }).id);
|
|
@@ -1020,6 +1034,17 @@ export async function buildServer(): Promise<FastifyInstance> {
|
|
|
1020
1034
|
return reply.code(201).send(res);
|
|
1021
1035
|
});
|
|
1022
1036
|
|
|
1037
|
+
// 자유 세션 — 임의 폴더에서 tmux 셸(프로젝트 비소속, 가상 Sessions 버킷).
|
|
1038
|
+
app.post('/api/session', async (req, reply) => {
|
|
1039
|
+
const b = (req.body ?? {}) as { machineSlug?: string; path?: string; title?: string };
|
|
1040
|
+
const machineSlug = (b.machineSlug ?? '').trim();
|
|
1041
|
+
const path = (b.path ?? '').trim();
|
|
1042
|
+
if (!machineSlug || !path) return reply.code(400).send({ error: 'machineSlug and path required' });
|
|
1043
|
+
const res = await openSessionAt(machineSlug, path, (b.title ?? '').trim());
|
|
1044
|
+
if (!res.ok) return reply.code(422).send(res);
|
|
1045
|
+
return reply.code(201).send(res);
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1023
1048
|
// Plan fan-out — 목표 하나 → 플래너가 태스크 분해 → 전부 자동 발사.
|
|
1024
1049
|
// (real 플래너는 repo 를 읽고 계획하느라 1~3분 걸릴 수 있음 — 클라이언트는 대기)
|
|
1025
1050
|
app.post('/api/plan', async (req, reply) => {
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 gc.yang
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|