@logicseek/hello-cc 0.1.9 → 1.0.1
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/CHANGELOG.md +136 -0
- package/README.md +50 -3
- package/README.zh-CN.md +48 -3
- package/bin/hcc.mjs +3 -5656
- package/docs/README.md +8 -0
- package/docs/README.zh-CN.md +6 -0
- package/docs/architecture.md +29 -18
- package/docs/architecture.zh-CN.md +20 -10
- package/docs/commands.md +13 -4
- package/docs/commands.zh-CN.md +13 -4
- package/docs/guide.md +97 -3
- package/docs/guide.zh-CN.md +89 -2
- package/lib/cli/app.mjs +644 -0
- package/lib/cli/commands/coordination.mjs +249 -0
- package/lib/cli/commands/doctor.mjs +81 -0
- package/lib/cli/commands/gc.mjs +1002 -0
- package/lib/cli/commands/hook.mjs +205 -0
- package/lib/cli/commands/install.mjs +294 -0
- package/lib/cli/commands/lifecycle.mjs +101 -0
- package/lib/cli/commands/lock.mjs +197 -0
- package/lib/cli/commands/misc.mjs +76 -0
- package/lib/cli/commands/msg.mjs +152 -0
- package/lib/cli/commands/peer.mjs +249 -0
- package/lib/cli/commands/query.mjs +96 -0
- package/lib/cli/commands/run.mjs +463 -0
- package/lib/cli/commands/task.mjs +405 -0
- package/lib/cli/commands/team.mjs +131 -0
- package/lib/cli/commands/tmux.mjs +838 -0
- package/lib/cli/commands/up.mjs +135 -0
- package/lib/cli-args.mjs +16 -2
- package/lib/coordination-state.mjs +112 -51
- package/lib/core/coordination/automation.mjs +5 -8
- package/lib/core/coordination/clock-safety.mjs +323 -0
- package/lib/core/coordination/event-retention.mjs +17 -0
- package/lib/core/coordination/gc-plan.mjs +614 -0
- package/lib/core/coordination/lease-renewal.mjs +102 -0
- package/lib/core/coordination/lock-evidence.mjs +77 -0
- package/lib/core/coordination/optimistic-evidence.mjs +26 -0
- package/lib/core/coordination/tasks.mjs +156 -36
- package/lib/core/peers/evidence-runtime.mjs +312 -0
- package/lib/core/peers/evidence.mjs +103 -0
- package/lib/core/peers/hook-owner.mjs +32 -0
- package/lib/core/peers/liveness.mjs +44 -4
- package/lib/core/peers/peer-helpers.mjs +121 -0
- package/lib/core/peers/session.mjs +8 -2
- package/lib/core/peers/tmux-safety.mjs +290 -0
- package/lib/core/sessions/launch.mjs +13 -1
- package/lib/db/connection.mjs +104 -0
- package/lib/db/events.mjs +34 -0
- package/lib/db/migration-backup.mjs +225 -0
- package/lib/db/schema.mjs +102 -5
- package/lib/db/stores/peers.mjs +34 -0
- package/lib/integrations/peers/identity.mjs +62 -16
- package/lib/integrations/shims/script.mjs +111 -9
- package/lib/process/identity.mjs +222 -0
- package/lib/process/pty-lifecycle.mjs +149 -0
- package/lib/runtime/buffer-directory-lease.mjs +48 -0
- package/lib/runtime/buffer-evidence.mjs +336 -0
- package/lib/runtime/buffer-gc-protocol.mjs +167 -0
- package/lib/runtime/buffer-gc.mjs +270 -0
- package/lib/runtime/client.mjs +63 -7
- package/lib/runtime/fatal-shutdown.mjs +54 -0
- package/lib/runtime/project-path.mjs +139 -0
- package/lib/runtime/projects.mjs +320 -40
- package/lib/runtime/state.mjs +227 -27
- package/lib/shared/cleanup-error.mjs +54 -0
- package/lib/shared/clock-grace.mjs +106 -0
- package/lib/shared/errors.mjs +9 -0
- package/lib/shared/file-lock.mjs +377 -0
- package/lib/shared/json-file.mjs +6 -2
- package/lib/shared/redact.mjs +60 -0
- package/lib/shared/socket-lock-worker.mjs +250 -0
- package/lib/terminal/tmux.mjs +131 -27
- package/lib/tmux.mjs +4 -0
- package/lib/ui/help.mjs +53 -15
- package/lib/web/api-version.mjs +42 -0
- package/lib/web/auto-attach.mjs +185 -0
- package/lib/web/buffer-gc-runtime.mjs +175 -0
- package/lib/web/cookie-auth.mjs +119 -0
- package/lib/web/external-sessions.mjs +254 -0
- package/lib/web/http-routes.mjs +542 -0
- package/lib/web/http.mjs +135 -6
- package/lib/web/liveness-reaper.mjs +145 -0
- package/lib/web/peer-actions.mjs +132 -23
- package/lib/web/project-contexts.mjs +183 -0
- package/lib/web/pty-sessions.mjs +203 -0
- package/lib/web/runtime-main.mjs +717 -0
- package/lib/web/runtime.mjs +91 -36
- package/lib/web/session-serialize.mjs +0 -0
- package/lib/web/startup.mjs +335 -0
- package/lib/web/tls.mjs +365 -0
- package/lib/web/tmux-sessions.mjs +1361 -0
- package/lib/web/tmux-stream.mjs +166 -0
- package/lib/web/ui-template.mjs +152 -20
- package/package.json +6 -3
- package/scripts/check-free-identifiers.mjs +290 -0
- package/scripts/regression.mjs +5404 -273
- package/docs/architecture-split-review.md +0 -217
- package/docs/layout-split-stack-audit.md +0 -298
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// Web project context management, extracted from lib/web/runtime-main.mjs.
|
|
2
|
+
// Owns the projectContexts map: normalization, registry-backed discovery,
|
|
3
|
+
// fail-closed path re-resolution, per-project DB connections, and
|
|
4
|
+
// session/peer lookup helpers used by the HTTP and WS layers.
|
|
5
|
+
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { randomBytes } from 'node:crypto';
|
|
8
|
+
import WebSocket from 'ws';
|
|
9
|
+
import { CliError } from '../shared/errors.mjs';
|
|
10
|
+
import { contextForProject } from '../runtime/paths.mjs';
|
|
11
|
+
import { resolveProjectDatabase } from '../runtime/project-path.mjs';
|
|
12
|
+
import {
|
|
13
|
+
projectRecord,
|
|
14
|
+
readProjectRegistry,
|
|
15
|
+
registerProject,
|
|
16
|
+
registerProjectActivity
|
|
17
|
+
} from '../runtime/projects.mjs';
|
|
18
|
+
import { nextSessionId } from '../web/runtime.mjs';
|
|
19
|
+
import { tokenMatches } from '../web/http.mjs';
|
|
20
|
+
|
|
21
|
+
export function createProjectContexts(deps) {
|
|
22
|
+
const {
|
|
23
|
+
ctx, sessions,
|
|
24
|
+
sessionKey, sessionsForProject, resolveSessionPeerId,
|
|
25
|
+
connect, now, addEvent, tx, touchPeer, upsertPeer, detectBranch,
|
|
26
|
+
ACTIVE_PEER_TTL, CLI_NAME, DEFAULT_LOCK_TTL,
|
|
27
|
+
queryInbox, queryOpenTasks, queryTimelineMessages,
|
|
28
|
+
observePeerEvidence, peerEvidenceFromDb,
|
|
29
|
+
observeClockSafetyInTransaction,
|
|
30
|
+
assertTaskOwnerForMutation, claimNextTasksForPeer, takeOverTaskForPeer,
|
|
31
|
+
positiveIntOpt, sameResolvedPath
|
|
32
|
+
} = deps;
|
|
33
|
+
|
|
34
|
+
const projectContexts = new Map();
|
|
35
|
+
|
|
36
|
+
function newSessionActionToken() {
|
|
37
|
+
return randomBytes(32).toString('base64url');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function rememberProject(projectCtx, { activity = false, register = false, nonblocking = false } = {}) {
|
|
41
|
+
const normalized = contextForProject(projectCtx.root, projectCtx.dbPath, { cwd: projectCtx.cwd, json: ctx.json });
|
|
42
|
+
const isNew = !projectContexts.has(normalized.root);
|
|
43
|
+
// Activity refresh is already nonblocking and throttled before it tries the
|
|
44
|
+
// registry lock. Calling it on each request keeps last_seen_at current
|
|
45
|
+
// without putting the synchronous lock back on the HTTP hot path.
|
|
46
|
+
if (activity) registerProjectActivity(normalized);
|
|
47
|
+
else if (isNew || register) {
|
|
48
|
+
try {
|
|
49
|
+
registerProject(normalized, { nonblocking });
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (nonblocking && ['ERR_FILE_LOCK_BUSY', 'ERR_FILE_LOCK_TIMEOUT'].includes(error?.code)) {
|
|
52
|
+
throw new CliError('REGISTRY_BUSY', 'Project registry is busy; retry the request');
|
|
53
|
+
}
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
projectContexts.set(normalized.root, normalized);
|
|
58
|
+
return normalized;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function knownProjects() {
|
|
62
|
+
const rows = readProjectRegistry();
|
|
63
|
+
if (!rows.some((p) => sameResolvedPath(p.root, ctx.root))) rows.unshift(projectRecord(ctx));
|
|
64
|
+
for (const project of rows) {
|
|
65
|
+
if (!projectContexts.has(project.root)) {
|
|
66
|
+
projectContexts.set(project.root, contextForProject(project.root, project.db, { json: ctx.json }));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return rows;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function resolveWebProjectContext(root, db) {
|
|
73
|
+
const first = resolveProjectDatabase({ root, db, createStateDir: true });
|
|
74
|
+
// Re-read every filesystem identity immediately before the returned
|
|
75
|
+
// context can reach connect(). A path rebound after the first pass fails
|
|
76
|
+
// closed instead of carrying a stale lexical decision into SQLite.
|
|
77
|
+
const final = resolveProjectDatabase({
|
|
78
|
+
root: first.root,
|
|
79
|
+
db: first.db,
|
|
80
|
+
createStateDir: true
|
|
81
|
+
});
|
|
82
|
+
return contextForProject(final.root, final.db, { cwd: final.root, json: ctx.json });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function connectWebProject(projectCtx, options = {}) {
|
|
86
|
+
const final = resolveProjectDatabase({
|
|
87
|
+
root: projectCtx.root,
|
|
88
|
+
db: projectCtx.dbPath,
|
|
89
|
+
createStateDir: options.create !== false
|
|
90
|
+
});
|
|
91
|
+
return connect(contextForProject(final.root, final.db, {
|
|
92
|
+
cwd: final.root,
|
|
93
|
+
json: projectCtx.json
|
|
94
|
+
}), options);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function projectFromRequest(req, url) {
|
|
98
|
+
const requestedRoot = url.searchParams.get('root') ||
|
|
99
|
+
url.searchParams.get('project') ||
|
|
100
|
+
req.headers['x-hcc-root'] ||
|
|
101
|
+
ctx.root;
|
|
102
|
+
const requestedDb = url.searchParams.get('db') ||
|
|
103
|
+
req.headers['x-hcc-db'] ||
|
|
104
|
+
path.join(requestedRoot, '.hello-cc', 'mesh.db');
|
|
105
|
+
return rememberProject(
|
|
106
|
+
resolveWebProjectContext(requestedRoot, requestedDb),
|
|
107
|
+
{ activity: true }
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function getSession(projectCtx, id, db = null) {
|
|
112
|
+
const direct = sessions.get(sessionKey(projectCtx, id));
|
|
113
|
+
if (direct) return direct;
|
|
114
|
+
for (const session of sessionsForProject(projectCtx)) {
|
|
115
|
+
if (session.peerId === id) return session;
|
|
116
|
+
}
|
|
117
|
+
if (db) {
|
|
118
|
+
for (const session of sessionsForProject(projectCtx)) {
|
|
119
|
+
if (resolveSessionPeerId(db, session) === id) return session;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function readActionToken(input, req) {
|
|
126
|
+
const headerToken = req.headers["x-hcc-session-token"];
|
|
127
|
+
return String(input.action_token || input.actionToken || headerToken || "").trim();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function resolveWebActionSession(projectCtx, peer, input, req) {
|
|
131
|
+
const db = connectWebProject(projectCtx);
|
|
132
|
+
let session;
|
|
133
|
+
try {
|
|
134
|
+
session = getSession(projectCtx, peer, db);
|
|
135
|
+
} finally {
|
|
136
|
+
db.close();
|
|
137
|
+
}
|
|
138
|
+
if (!session || session.status !== "running") {
|
|
139
|
+
throw new CliError("PEER_IDENTITY_REQUIRED", "Web peer action requires a running managed session for " + peer, { peer });
|
|
140
|
+
}
|
|
141
|
+
const actorPeer = session.peerId || peer;
|
|
142
|
+
if (actorPeer !== peer && session.id !== peer) {
|
|
143
|
+
throw new CliError("PEER_IDENTITY_MISMATCH", "Web peer action target " + peer + " does not match managed session " + actorPeer, {
|
|
144
|
+
peer, actor_peer: actorPeer, session_id: session.id
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
const provided = readActionToken(input, req);
|
|
148
|
+
const authorized = Boolean(provided) && [...(session.actionTokens || [])]
|
|
149
|
+
.some((candidate) => {
|
|
150
|
+
if (!tokenMatches(provided, candidate)) return false;
|
|
151
|
+
const socket = session.actionTokenSockets?.get(candidate);
|
|
152
|
+
return socket?.readyState === WebSocket.OPEN;
|
|
153
|
+
});
|
|
154
|
+
if (!authorized) {
|
|
155
|
+
throw new CliError("PEER_IDENTITY_REQUIRED", "Web peer action for " + peer + " requires the managed session action token", { peer });
|
|
156
|
+
}
|
|
157
|
+
return actorPeer;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function knownPeerIds(projectCtx) {
|
|
161
|
+
const db = connectWebProject(projectCtx);
|
|
162
|
+
try {
|
|
163
|
+
return db.prepare("SELECT id FROM peers").all().map((row) => row.id);
|
|
164
|
+
} finally {
|
|
165
|
+
db.close();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function nextProjectSessionId(projectCtx, kind) {
|
|
170
|
+
return nextSessionId([
|
|
171
|
+
...sessionsForProject(projectCtx).map((session) => session.id),
|
|
172
|
+
...knownPeerIds(projectCtx)
|
|
173
|
+
], kind);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
projectContexts,
|
|
178
|
+
newSessionActionToken, rememberProject, knownProjects,
|
|
179
|
+
resolveWebProjectContext, connectWebProject, projectFromRequest,
|
|
180
|
+
getSession, readActionToken, resolveWebActionSession,
|
|
181
|
+
knownPeerIds, nextProjectSessionId
|
|
182
|
+
};
|
|
183
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// PTY-backed web sessions, extracted from lib/web/runtime-main.mjs.
|
|
2
|
+
// startPtySession spawns node-pty children as tracked peers; the input
|
|
3
|
+
// normalization helpers (options, peer id, backend choice) feed both the
|
|
4
|
+
// PTY and tmux session factories.
|
|
5
|
+
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { CliError } from '../shared/errors.mjs';
|
|
8
|
+
import process from 'node:process';
|
|
9
|
+
import {
|
|
10
|
+
buildPeerCommand,
|
|
11
|
+
defaultSessionCommand,
|
|
12
|
+
hasResumeOpts
|
|
13
|
+
} from '../integrations/providers.mjs';
|
|
14
|
+
import { providerSessionPeerId } from '../core/peers/session.mjs';
|
|
15
|
+
import { childSessionEnv } from '../core/sessions/launch.mjs';
|
|
16
|
+
import { detectBranch } from '../project-context.mjs';
|
|
17
|
+
|
|
18
|
+
export function createPtySessions(deps) {
|
|
19
|
+
const {
|
|
20
|
+
pty, ctx, sessions,
|
|
21
|
+
broadcast, closeSessionClients, sessionKey,
|
|
22
|
+
connectWebProject, nextProjectSessionId,
|
|
23
|
+
startTmuxManagedSession,
|
|
24
|
+
now, addEvent, auditPayload, refreshPeerIoHeartbeat,
|
|
25
|
+
upsertPeer, upsertCanonicalPeerBinding,
|
|
26
|
+
requestActorPeer, requestSource
|
|
27
|
+
} = deps;
|
|
28
|
+
|
|
29
|
+
function startPtySession(input) {
|
|
30
|
+
const pctx = input.projectCtx || ctx;
|
|
31
|
+
const kind = input.kind || 'shell';
|
|
32
|
+
const id = input.id || nextProjectSessionId(pctx, kind);
|
|
33
|
+
const actorPeer = requestActorPeer(input, id);
|
|
34
|
+
const auditSource = requestSource(input, 'web');
|
|
35
|
+
const key = sessionKey(pctx, id);
|
|
36
|
+
if (sessions.has(key) && sessions.get(key).status === 'running') {
|
|
37
|
+
return sessions.get(key);
|
|
38
|
+
}
|
|
39
|
+
const role = input.role || 'peer';
|
|
40
|
+
const command = input.command || defaultSessionCommand(kind);
|
|
41
|
+
const cwd = path.resolve(input.cwd || pctx.root);
|
|
42
|
+
const callerEnv = input.env && typeof input.env === 'object' ? input.env : process.env;
|
|
43
|
+
const shell = callerEnv.SHELL || process.env.SHELL || 'bash';
|
|
44
|
+
const env = childSessionEnv({
|
|
45
|
+
HCC_PEER: id,
|
|
46
|
+
HCC_ROOT: pctx.root,
|
|
47
|
+
HCC_DB: pctx.dbPath,
|
|
48
|
+
TERM: 'xterm-256color'
|
|
49
|
+
}, callerEnv);
|
|
50
|
+
const size = input.size || { cols: 100, rows: 30 };
|
|
51
|
+
const child = pty.spawn(shell, ['-c', command], {
|
|
52
|
+
name: 'xterm-256color',
|
|
53
|
+
cols: size.cols,
|
|
54
|
+
rows: size.rows,
|
|
55
|
+
cwd,
|
|
56
|
+
env
|
|
57
|
+
});
|
|
58
|
+
const session = {
|
|
59
|
+
id,
|
|
60
|
+
peerId: id,
|
|
61
|
+
actorPeer,
|
|
62
|
+
auditSource,
|
|
63
|
+
actionTokens: new Set(),
|
|
64
|
+
root: pctx.root,
|
|
65
|
+
ctx: pctx,
|
|
66
|
+
kind,
|
|
67
|
+
role,
|
|
68
|
+
command,
|
|
69
|
+
cwd,
|
|
70
|
+
pid: child.pid,
|
|
71
|
+
pty: child,
|
|
72
|
+
status: 'running',
|
|
73
|
+
createdAt: now(),
|
|
74
|
+
exitedAt: null,
|
|
75
|
+
buffer: '',
|
|
76
|
+
clients: new Set()
|
|
77
|
+
};
|
|
78
|
+
sessions.set(key, session);
|
|
79
|
+
const db = connectWebProject(pctx);
|
|
80
|
+
try {
|
|
81
|
+
upsertPeer(db, {
|
|
82
|
+
id,
|
|
83
|
+
kind,
|
|
84
|
+
role,
|
|
85
|
+
worktree: cwd,
|
|
86
|
+
branch: detectBranch(cwd),
|
|
87
|
+
pid: child.pid,
|
|
88
|
+
status: 'running',
|
|
89
|
+
capabilities: 'web-pty'
|
|
90
|
+
});
|
|
91
|
+
const canonical = upsertCanonicalPeerBinding(db, {
|
|
92
|
+
peer: id,
|
|
93
|
+
provider: input.binding?.provider || kind,
|
|
94
|
+
provider_session_id: input.binding?.provider_session_id || null,
|
|
95
|
+
provider_session_name: input.binding?.provider_session_name || null,
|
|
96
|
+
resume_mode: input.binding?.resume_mode || 'new',
|
|
97
|
+
resume_arg: input.binding?.resume_arg || null,
|
|
98
|
+
command,
|
|
99
|
+
transport: 'web-pty',
|
|
100
|
+
runtime_session_id: id
|
|
101
|
+
}, Boolean(input.force));
|
|
102
|
+
session.peerId = canonical.peer;
|
|
103
|
+
session.binding = { ...canonical.binding };
|
|
104
|
+
addEvent(db, 'web.session.started', actorPeer, null, auditPayload({
|
|
105
|
+
actor: actorPeer,
|
|
106
|
+
target: session.peerId || id,
|
|
107
|
+
source: auditSource,
|
|
108
|
+
admin: actorPeer !== (session.peerId || id),
|
|
109
|
+
command,
|
|
110
|
+
cwd,
|
|
111
|
+
pid: child.pid
|
|
112
|
+
}));
|
|
113
|
+
} finally {
|
|
114
|
+
db.close();
|
|
115
|
+
}
|
|
116
|
+
child.onData((data) => {
|
|
117
|
+
session.buffer += data;
|
|
118
|
+
if (session.buffer.length > 250000) session.buffer = session.buffer.slice(-200000);
|
|
119
|
+
broadcast(session, { type: 'data', data });
|
|
120
|
+
refreshPeerIoHeartbeat(session);
|
|
121
|
+
});
|
|
122
|
+
child.onExit((event) => {
|
|
123
|
+
session.status = 'exited';
|
|
124
|
+
session.exitedAt = now();
|
|
125
|
+
const db = connectWebProject(pctx);
|
|
126
|
+
try {
|
|
127
|
+
db.prepare('UPDATE peers SET status = ?, last_seen_at = ? WHERE id = ?').run('exited', now(), id);
|
|
128
|
+
addEvent(db, 'web.session.exited', session.actorPeer || id, null, auditPayload({
|
|
129
|
+
actor: session.actorPeer || id,
|
|
130
|
+
target: session.peerId || id,
|
|
131
|
+
source: session.auditSource || 'web',
|
|
132
|
+
...event
|
|
133
|
+
}));
|
|
134
|
+
} finally {
|
|
135
|
+
db.close();
|
|
136
|
+
}
|
|
137
|
+
broadcast(session, { type: 'exit', event });
|
|
138
|
+
closeSessionClients(session);
|
|
139
|
+
});
|
|
140
|
+
return session;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function webSessionBuildOptions(input) {
|
|
144
|
+
const mode = input.mode || 'new';
|
|
145
|
+
if (mode === 'new') return {};
|
|
146
|
+
if (mode === 'resume') {
|
|
147
|
+
const resume = String(input.resume || '').trim();
|
|
148
|
+
if (!resume) throw new CliError('BAD_REQUEST', 'resume session required');
|
|
149
|
+
return { resume };
|
|
150
|
+
}
|
|
151
|
+
if (mode === 'last') return { last: true };
|
|
152
|
+
if (mode === 'continue') return { continue: true };
|
|
153
|
+
throw new CliError('BAD_REQUEST', `Unsupported session mode: ${mode}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function webSessionPeerId(projectCtx, kind, opts, input) {
|
|
157
|
+
if (input.id) return input.id;
|
|
158
|
+
if (opts.resume) return providerSessionPeerId(kind, opts.resume);
|
|
159
|
+
return nextProjectSessionId(projectCtx, kind);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function normalizeWebSessionInput(input) {
|
|
163
|
+
const pctx = input.projectCtx || ctx;
|
|
164
|
+
const kind = input.kind || 'shell';
|
|
165
|
+
if (!['claude', 'codex', 'shell'].includes(kind)) {
|
|
166
|
+
throw new CliError('BAD_REQUEST', `Unsupported session kind: ${kind}`);
|
|
167
|
+
}
|
|
168
|
+
if (input.command || input.binding) return { ...input, kind, projectCtx: pctx };
|
|
169
|
+
|
|
170
|
+
const opts = webSessionBuildOptions(input);
|
|
171
|
+
if (kind === 'shell' && hasResumeOpts(opts)) {
|
|
172
|
+
throw new CliError('BAD_REQUEST', 'Resume modes are only supported for codex and claude sessions');
|
|
173
|
+
}
|
|
174
|
+
if (kind !== 'claude' && opts.continue) {
|
|
175
|
+
throw new CliError('BAD_REQUEST', 'continue is only supported for claude sessions');
|
|
176
|
+
}
|
|
177
|
+
if (kind !== 'codex' && opts.last) {
|
|
178
|
+
throw new CliError('BAD_REQUEST', 'last is only supported for codex sessions');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const id = webSessionPeerId(pctx, kind, opts, input);
|
|
182
|
+
const built = buildPeerCommand(id, kind, opts, []);
|
|
183
|
+
return {
|
|
184
|
+
...input,
|
|
185
|
+
id,
|
|
186
|
+
kind,
|
|
187
|
+
command: built.command,
|
|
188
|
+
binding: built.binding,
|
|
189
|
+
projectCtx: pctx
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function startSession(input) {
|
|
194
|
+
const normalized = normalizeWebSessionInput(input);
|
|
195
|
+
if (normalized.backend === 'pty') return startPtySession(normalized);
|
|
196
|
+
return startTmuxManagedSession(normalized);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
startPtySession, webSessionBuildOptions, webSessionPeerId,
|
|
201
|
+
normalizeWebSessionInput, startSession
|
|
202
|
+
};
|
|
203
|
+
}
|