codepiper 0.1.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/.env.example +28 -0
- package/CHANGELOG.md +10 -0
- package/LEGAL_NOTICE.md +39 -0
- package/LICENSE +21 -0
- package/README.md +524 -0
- package/package.json +90 -0
- package/packages/cli/package.json +13 -0
- package/packages/cli/src/commands/analytics.ts +157 -0
- package/packages/cli/src/commands/attach.ts +299 -0
- package/packages/cli/src/commands/audit.ts +50 -0
- package/packages/cli/src/commands/auth.ts +261 -0
- package/packages/cli/src/commands/daemon.ts +162 -0
- package/packages/cli/src/commands/doctor.ts +303 -0
- package/packages/cli/src/commands/env-set.ts +162 -0
- package/packages/cli/src/commands/hook-forward.ts +268 -0
- package/packages/cli/src/commands/keys.ts +77 -0
- package/packages/cli/src/commands/kill.ts +19 -0
- package/packages/cli/src/commands/logs.ts +419 -0
- package/packages/cli/src/commands/model.ts +172 -0
- package/packages/cli/src/commands/policy-set.ts +185 -0
- package/packages/cli/src/commands/policy.ts +227 -0
- package/packages/cli/src/commands/providers.ts +114 -0
- package/packages/cli/src/commands/resize.ts +34 -0
- package/packages/cli/src/commands/send.ts +184 -0
- package/packages/cli/src/commands/sessions.ts +202 -0
- package/packages/cli/src/commands/slash.ts +92 -0
- package/packages/cli/src/commands/start.ts +243 -0
- package/packages/cli/src/commands/stop.ts +19 -0
- package/packages/cli/src/commands/tail.ts +137 -0
- package/packages/cli/src/commands/workflow.ts +786 -0
- package/packages/cli/src/commands/workspace.ts +127 -0
- package/packages/cli/src/lib/api.ts +78 -0
- package/packages/cli/src/lib/args.ts +72 -0
- package/packages/cli/src/lib/format.ts +93 -0
- package/packages/cli/src/main.ts +563 -0
- package/packages/core/package.json +7 -0
- package/packages/core/src/config.ts +30 -0
- package/packages/core/src/errors.ts +38 -0
- package/packages/core/src/eventBus.ts +56 -0
- package/packages/core/src/eventBusAdapter.ts +143 -0
- package/packages/core/src/index.ts +10 -0
- package/packages/core/src/sqliteEventBus.ts +336 -0
- package/packages/core/src/types.ts +63 -0
- package/packages/daemon/package.json +11 -0
- package/packages/daemon/src/api/analyticsRoutes.ts +343 -0
- package/packages/daemon/src/api/authRoutes.ts +344 -0
- package/packages/daemon/src/api/bodyLimit.ts +133 -0
- package/packages/daemon/src/api/envSetRoutes.ts +170 -0
- package/packages/daemon/src/api/gitRoutes.ts +409 -0
- package/packages/daemon/src/api/hooks.ts +588 -0
- package/packages/daemon/src/api/inputPolicy.ts +249 -0
- package/packages/daemon/src/api/notificationRoutes.ts +532 -0
- package/packages/daemon/src/api/policyRoutes.ts +234 -0
- package/packages/daemon/src/api/policySetRoutes.ts +445 -0
- package/packages/daemon/src/api/routeUtils.ts +28 -0
- package/packages/daemon/src/api/routes.ts +1004 -0
- package/packages/daemon/src/api/server.ts +1388 -0
- package/packages/daemon/src/api/settingsRoutes.ts +367 -0
- package/packages/daemon/src/api/sqliteErrors.ts +47 -0
- package/packages/daemon/src/api/stt.ts +143 -0
- package/packages/daemon/src/api/terminalRoutes.ts +200 -0
- package/packages/daemon/src/api/validation.ts +287 -0
- package/packages/daemon/src/api/validationRoutes.ts +174 -0
- package/packages/daemon/src/api/workflowRoutes.ts +567 -0
- package/packages/daemon/src/api/workspaceRoutes.ts +151 -0
- package/packages/daemon/src/api/ws.ts +1588 -0
- package/packages/daemon/src/auth/apiRateLimiter.ts +73 -0
- package/packages/daemon/src/auth/authMiddleware.ts +305 -0
- package/packages/daemon/src/auth/authService.ts +496 -0
- package/packages/daemon/src/auth/rateLimiter.ts +137 -0
- package/packages/daemon/src/config/pricing.ts +79 -0
- package/packages/daemon/src/crypto/encryption.ts +196 -0
- package/packages/daemon/src/db/db.ts +2745 -0
- package/packages/daemon/src/db/index.ts +16 -0
- package/packages/daemon/src/db/migrations.ts +182 -0
- package/packages/daemon/src/db/policyDb.ts +349 -0
- package/packages/daemon/src/db/schema.sql +408 -0
- package/packages/daemon/src/db/workflowDb.ts +464 -0
- package/packages/daemon/src/git/gitUtils.ts +544 -0
- package/packages/daemon/src/index.ts +6 -0
- package/packages/daemon/src/main.ts +525 -0
- package/packages/daemon/src/notifications/pushNotifier.ts +369 -0
- package/packages/daemon/src/providers/codexAppServerScaffold.ts +49 -0
- package/packages/daemon/src/providers/registry.ts +111 -0
- package/packages/daemon/src/providers/types.ts +82 -0
- package/packages/daemon/src/sessions/auditLogger.ts +103 -0
- package/packages/daemon/src/sessions/policyEngine.ts +165 -0
- package/packages/daemon/src/sessions/policyMatcher.ts +114 -0
- package/packages/daemon/src/sessions/policyTypes.ts +94 -0
- package/packages/daemon/src/sessions/ptyProcess.ts +141 -0
- package/packages/daemon/src/sessions/sessionManager.ts +1770 -0
- package/packages/daemon/src/sessions/tmuxSession.ts +1073 -0
- package/packages/daemon/src/sessions/transcriptManager.ts +110 -0
- package/packages/daemon/src/sessions/transcriptParser.ts +149 -0
- package/packages/daemon/src/sessions/transcriptTailer.ts +214 -0
- package/packages/daemon/src/tracking/tokenTracker.ts +168 -0
- package/packages/daemon/src/workflows/contextManager.ts +83 -0
- package/packages/daemon/src/workflows/index.ts +31 -0
- package/packages/daemon/src/workflows/resultExtractor.ts +118 -0
- package/packages/daemon/src/workflows/waitConditionPoller.ts +131 -0
- package/packages/daemon/src/workflows/workflowParser.ts +217 -0
- package/packages/daemon/src/workflows/workflowRunner.ts +969 -0
- package/packages/daemon/src/workflows/workflowTypes.ts +188 -0
- package/packages/daemon/src/workflows/workflowValidator.ts +533 -0
- package/packages/providers/claude-code/package.json +11 -0
- package/packages/providers/claude-code/src/index.ts +7 -0
- package/packages/providers/claude-code/src/overlaySettings.ts +198 -0
- package/packages/providers/claude-code/src/provider.ts +311 -0
- package/packages/web/dist/android-chrome-192x192.png +0 -0
- package/packages/web/dist/android-chrome-512x512.png +0 -0
- package/packages/web/dist/apple-touch-icon.png +0 -0
- package/packages/web/dist/assets/AnalyticsPage-BIopKWRf.js +17 -0
- package/packages/web/dist/assets/PoliciesPage-CjdLN3dl.js +11 -0
- package/packages/web/dist/assets/SessionDetailPage-BtSA0V0M.js +179 -0
- package/packages/web/dist/assets/SettingsPage-Dbbz4Ca5.js +37 -0
- package/packages/web/dist/assets/WorkflowsPage-Dv6f3GgU.js +1 -0
- package/packages/web/dist/assets/chart-vendor-DlOHLaCG.js +49 -0
- package/packages/web/dist/assets/codicon-ngg6Pgfi.ttf +0 -0
- package/packages/web/dist/assets/css.worker-BvV5MPou.js +93 -0
- package/packages/web/dist/assets/editor.worker-CKy7Pnvo.js +26 -0
- package/packages/web/dist/assets/html.worker-BLJhxQJQ.js +470 -0
- package/packages/web/dist/assets/index-BbdhRfr2.css +1 -0
- package/packages/web/dist/assets/index-hgphORiw.js +204 -0
- package/packages/web/dist/assets/json.worker-usMZ-FED.js +58 -0
- package/packages/web/dist/assets/monaco-core-B_19GPAS.css +1 -0
- package/packages/web/dist/assets/monaco-core-DQ5Mk8AK.js +1234 -0
- package/packages/web/dist/assets/monaco-react-DfZNWvtW.js +11 -0
- package/packages/web/dist/assets/monacoSetup-DvBj52bT.js +1 -0
- package/packages/web/dist/assets/pencil-Dbczxz59.js +11 -0
- package/packages/web/dist/assets/react-vendor-B5MgMUHH.js +136 -0
- package/packages/web/dist/assets/refresh-cw-B0MGsYPL.js +6 -0
- package/packages/web/dist/assets/tabs-C8LsWiR5.js +1 -0
- package/packages/web/dist/assets/terminal-vendor-Cs8KPbV3.js +9 -0
- package/packages/web/dist/assets/terminal-vendor-LcAfv9l9.css +32 -0
- package/packages/web/dist/assets/trash-2-Btlg0d4l.js +6 -0
- package/packages/web/dist/assets/ts.worker-DGHjMaqB.js +67731 -0
- package/packages/web/dist/favicon.ico +0 -0
- package/packages/web/dist/icon.svg +1 -0
- package/packages/web/dist/index.html +29 -0
- package/packages/web/dist/manifest.json +29 -0
- package/packages/web/dist/og-image.png +0 -0
- package/packages/web/dist/originals/android-chrome-192x192.png +0 -0
- package/packages/web/dist/originals/android-chrome-512x512.png +0 -0
- package/packages/web/dist/originals/apple-touch-icon.png +0 -0
- package/packages/web/dist/originals/favicon.ico +0 -0
- package/packages/web/dist/piper.svg +1 -0
- package/packages/web/dist/sounds/codepiper-soft-chime.wav +0 -0
- package/packages/web/dist/sw.js +257 -0
- package/scripts/postinstall-link-workspaces.mjs +58 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type BillingMode,
|
|
3
|
+
type ProviderId,
|
|
4
|
+
type SessionHandle,
|
|
5
|
+
SUPPORTED_PROVIDERS,
|
|
6
|
+
} from "@codepiper/core";
|
|
7
|
+
import { readErrorJson, readJson, responseErrorMessage } from "../lib/api";
|
|
8
|
+
import { getRequiredValue } from "../lib/args";
|
|
9
|
+
|
|
10
|
+
export interface StartOptions {
|
|
11
|
+
provider: ProviderId;
|
|
12
|
+
dir: string;
|
|
13
|
+
socket: string;
|
|
14
|
+
additionalArgs?: string[];
|
|
15
|
+
billingMode?: BillingMode;
|
|
16
|
+
dangerous?: boolean;
|
|
17
|
+
envSetIds?: string[];
|
|
18
|
+
worktree?: { enabled: boolean; branch?: string; createBranch?: boolean };
|
|
19
|
+
workspaceId?: string;
|
|
20
|
+
validate?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type ParsedStartOptions = StartOptions;
|
|
24
|
+
|
|
25
|
+
const PROVIDER_ALIASES: Record<string, ProviderId> = {
|
|
26
|
+
claude: "claude-code",
|
|
27
|
+
"claude-code": "claude-code",
|
|
28
|
+
codex: "codex",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const VALID_PROVIDERS: ProviderId[] = [...SUPPORTED_PROVIDERS];
|
|
32
|
+
|
|
33
|
+
export function parseStartOptions(args: string[]): ParsedStartOptions {
|
|
34
|
+
let provider: string | undefined;
|
|
35
|
+
let dir: string = process.cwd();
|
|
36
|
+
let socket: string = "/tmp/codepiper.sock";
|
|
37
|
+
let billingMode: BillingMode | undefined;
|
|
38
|
+
let dangerous = false;
|
|
39
|
+
const envSetIds: string[] = [];
|
|
40
|
+
let worktree: StartOptions["worktree"];
|
|
41
|
+
let workspaceId: string | undefined;
|
|
42
|
+
let validate = false;
|
|
43
|
+
const additionalArgs: string[] = [];
|
|
44
|
+
|
|
45
|
+
let inAdditionalArgs = false;
|
|
46
|
+
|
|
47
|
+
for (let i = 0; i < args.length; i++) {
|
|
48
|
+
const arg = args[i];
|
|
49
|
+
if (arg === undefined) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (inAdditionalArgs) {
|
|
54
|
+
additionalArgs.push(arg);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (arg === "--") {
|
|
59
|
+
inAdditionalArgs = true;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (arg === "--provider" || arg === "-p") {
|
|
64
|
+
provider = getRequiredValue(args, i, arg);
|
|
65
|
+
i++;
|
|
66
|
+
} else if (arg === "--dir" || arg === "-d") {
|
|
67
|
+
dir = getRequiredValue(args, i, arg);
|
|
68
|
+
i++;
|
|
69
|
+
} else if (arg === "--socket" || arg === "-s") {
|
|
70
|
+
socket = getRequiredValue(args, i, arg);
|
|
71
|
+
i++;
|
|
72
|
+
} else if (arg === "--billing" || arg === "-b") {
|
|
73
|
+
const mode = getRequiredValue(args, i, arg);
|
|
74
|
+
i++;
|
|
75
|
+
if (mode !== "subscription" && mode !== "api") {
|
|
76
|
+
throw new Error(`Invalid billing mode: ${mode}. Must be "subscription" or "api"`);
|
|
77
|
+
}
|
|
78
|
+
billingMode = mode as BillingMode;
|
|
79
|
+
} else if (arg === "--env-set") {
|
|
80
|
+
envSetIds.push(getRequiredValue(args, i, arg));
|
|
81
|
+
i++;
|
|
82
|
+
} else if (arg === "--worktree") {
|
|
83
|
+
worktree = { enabled: true, ...worktree };
|
|
84
|
+
} else if (arg === "--create-branch") {
|
|
85
|
+
const branch = getRequiredValue(args, i, arg);
|
|
86
|
+
i++;
|
|
87
|
+
worktree = { enabled: true, branch, createBranch: true };
|
|
88
|
+
} else if (arg === "--workspace") {
|
|
89
|
+
workspaceId = getRequiredValue(args, i, arg);
|
|
90
|
+
i++;
|
|
91
|
+
} else if (arg === "--validate") {
|
|
92
|
+
validate = true;
|
|
93
|
+
} else if (arg === "--dangerous") {
|
|
94
|
+
dangerous = true;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!provider) {
|
|
99
|
+
throw new Error("--provider is required");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const resolvedProvider = PROVIDER_ALIASES[provider];
|
|
103
|
+
if (!(resolvedProvider && VALID_PROVIDERS.includes(resolvedProvider))) {
|
|
104
|
+
throw new Error(`Invalid provider: ${provider}. Valid options: ${VALID_PROVIDERS.join(", ")}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const parsed: ParsedStartOptions = {
|
|
108
|
+
provider: resolvedProvider,
|
|
109
|
+
dir,
|
|
110
|
+
socket,
|
|
111
|
+
};
|
|
112
|
+
if (additionalArgs.length > 0) {
|
|
113
|
+
parsed.additionalArgs = additionalArgs;
|
|
114
|
+
}
|
|
115
|
+
if (billingMode !== undefined) {
|
|
116
|
+
parsed.billingMode = billingMode;
|
|
117
|
+
}
|
|
118
|
+
if (dangerous) {
|
|
119
|
+
parsed.dangerous = true;
|
|
120
|
+
}
|
|
121
|
+
if (envSetIds.length > 0) {
|
|
122
|
+
parsed.envSetIds = envSetIds;
|
|
123
|
+
}
|
|
124
|
+
if (worktree !== undefined) {
|
|
125
|
+
parsed.worktree = worktree;
|
|
126
|
+
}
|
|
127
|
+
if (workspaceId !== undefined) {
|
|
128
|
+
parsed.workspaceId = workspaceId;
|
|
129
|
+
}
|
|
130
|
+
if (validate) {
|
|
131
|
+
parsed.validate = true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return parsed;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function startSession(options: StartOptions): Promise<SessionHandle> {
|
|
138
|
+
const payload: Record<string, unknown> = {
|
|
139
|
+
provider: options.provider,
|
|
140
|
+
cwd: options.dir,
|
|
141
|
+
args: options.additionalArgs,
|
|
142
|
+
};
|
|
143
|
+
if (options.billingMode) payload.billingMode = options.billingMode;
|
|
144
|
+
if (options.dangerous) payload.dangerousMode = true;
|
|
145
|
+
if (options.envSetIds) payload.envSetIds = options.envSetIds;
|
|
146
|
+
if (options.worktree) payload.worktree = options.worktree;
|
|
147
|
+
if (options.workspaceId) payload.workspaceId = options.workspaceId;
|
|
148
|
+
if (options.validate) payload.validate = options.validate;
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const response = await fetch("http://localhost/sessions", {
|
|
152
|
+
unix: options.socket,
|
|
153
|
+
method: "POST",
|
|
154
|
+
headers: {
|
|
155
|
+
"Content-Type": "application/json",
|
|
156
|
+
},
|
|
157
|
+
body: JSON.stringify(payload),
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
if (!response.ok) {
|
|
161
|
+
const errorData = await readErrorJson(response);
|
|
162
|
+
throw new Error(responseErrorMessage(response, errorData));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const data = await readJson<{ session: any }>(response);
|
|
166
|
+
const session = data.session; // API returns { session: {...} }
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
id: session.id,
|
|
170
|
+
provider: session.provider,
|
|
171
|
+
cwd: session.cwd,
|
|
172
|
+
status: session.status,
|
|
173
|
+
createdAt: new Date(session.createdAt),
|
|
174
|
+
updatedAt: new Date(session.updatedAt),
|
|
175
|
+
pid: session.pid,
|
|
176
|
+
transcriptPath: session.transcriptPath,
|
|
177
|
+
metadata: session.metadata,
|
|
178
|
+
};
|
|
179
|
+
} catch (error: any) {
|
|
180
|
+
if (error.code === "ENOENT" || error.message?.includes("ENOENT")) {
|
|
181
|
+
throw new Error(`Failed to connect to daemon at ${options.socket}. Is the daemon running?`);
|
|
182
|
+
}
|
|
183
|
+
throw error;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ANSI color codes
|
|
188
|
+
const colors = {
|
|
189
|
+
reset: "\x1b[0m",
|
|
190
|
+
bold: "\x1b[1m",
|
|
191
|
+
dim: "\x1b[2m",
|
|
192
|
+
green: "\x1b[32m",
|
|
193
|
+
yellow: "\x1b[33m",
|
|
194
|
+
cyan: "\x1b[36m",
|
|
195
|
+
gray: "\x1b[90m",
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
export async function runStartCommand(args: string[]): Promise<void> {
|
|
199
|
+
const options = parseStartOptions(args);
|
|
200
|
+
|
|
201
|
+
console.log(
|
|
202
|
+
`${colors.dim}Starting ${colors.cyan}${options.provider}${colors.dim} session in ${colors.reset}${options.dir}${colors.dim}...${colors.reset}`
|
|
203
|
+
);
|
|
204
|
+
if (options.dangerous) {
|
|
205
|
+
console.log(
|
|
206
|
+
`${colors.yellow}Warning:${colors.reset} dangerous mode enabled — CodePiper policy checks are bypassed for this session.`
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const session = await startSession(options);
|
|
211
|
+
|
|
212
|
+
console.log(
|
|
213
|
+
`\n${colors.green}✓${colors.reset} ${colors.bold}Session created successfully!${colors.reset}`
|
|
214
|
+
);
|
|
215
|
+
console.log(
|
|
216
|
+
` ${colors.dim}ID:${colors.reset} ${colors.cyan}${session.id}${colors.reset}`
|
|
217
|
+
);
|
|
218
|
+
console.log(` ${colors.dim}Provider:${colors.reset} ${session.provider}`);
|
|
219
|
+
console.log(` ${colors.dim}Directory:${colors.reset} ${session.cwd}`);
|
|
220
|
+
console.log(
|
|
221
|
+
` ${colors.dim}Status:${colors.reset} ${colors.yellow}${session.status}${colors.reset}`
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
if (session.pid) {
|
|
225
|
+
console.log(` ${colors.dim}PID:${colors.reset} ${session.pid}`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Show output log location
|
|
229
|
+
console.log(
|
|
230
|
+
` ${colors.dim}Output:${colors.reset} ${colors.gray}~/.codepiper/sessions/${session.id.slice(0, 8)}.../output.log${colors.reset}`
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
console.log(`\n${colors.bold}Next steps:${colors.reset}`);
|
|
234
|
+
console.log(
|
|
235
|
+
` ${colors.cyan}codepiper attach ${session.id}${colors.reset} ${colors.dim}# Attach to session${colors.reset}`
|
|
236
|
+
);
|
|
237
|
+
console.log(
|
|
238
|
+
` ${colors.cyan}codepiper send ${session.id.slice(0, 8)}... "prompt"${colors.reset} ${colors.dim}# Send input${colors.reset}`
|
|
239
|
+
);
|
|
240
|
+
console.log(
|
|
241
|
+
` ${colors.cyan}codepiper logs ${session.id.slice(0, 8)}...${colors.reset} ${colors.dim}# View events${colors.reset}`
|
|
242
|
+
);
|
|
243
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { daemonFetch } from "../lib/api";
|
|
2
|
+
import { getSocket } from "../lib/args";
|
|
3
|
+
import { colors, success } from "../lib/format";
|
|
4
|
+
|
|
5
|
+
export async function runStopCommand(args: string[]): Promise<void> {
|
|
6
|
+
const sessionId = args.find((a) => !a.startsWith("-"));
|
|
7
|
+
if (!sessionId) {
|
|
8
|
+
throw new Error("session-id is required");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const socket = getSocket(args);
|
|
12
|
+
|
|
13
|
+
await daemonFetch(`/sessions/${sessionId}/stop`, {
|
|
14
|
+
method: "POST",
|
|
15
|
+
socket,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
success(`Session ${colors.cyan}${sessionId.slice(0, 8)}...${colors.reset} stopped`);
|
|
19
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { getRequiredValue } from "../lib/args";
|
|
2
|
+
|
|
3
|
+
export interface TailOptions {
|
|
4
|
+
sessionId: string;
|
|
5
|
+
follow: boolean;
|
|
6
|
+
lines: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// ANSI color codes
|
|
10
|
+
const colors = {
|
|
11
|
+
reset: "\x1b[0m",
|
|
12
|
+
dim: "\x1b[2m",
|
|
13
|
+
cyan: "\x1b[36m",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function parseTailOptions(args: string[]): TailOptions {
|
|
17
|
+
let sessionId: string | undefined;
|
|
18
|
+
let follow = false;
|
|
19
|
+
let lines = 50; // Default to last 50 lines
|
|
20
|
+
|
|
21
|
+
for (let i = 0; i < args.length; i++) {
|
|
22
|
+
const arg = args[i];
|
|
23
|
+
if (arg === undefined) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (arg === "--follow" || arg === "-f") {
|
|
28
|
+
follow = true;
|
|
29
|
+
} else if (arg === "--lines" || arg === "-n") {
|
|
30
|
+
const linesValue = getRequiredValue(args, i, arg);
|
|
31
|
+
lines = Number.parseInt(linesValue, 10);
|
|
32
|
+
i++;
|
|
33
|
+
} else if (!(arg.startsWith("-") || sessionId)) {
|
|
34
|
+
sessionId = arg;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!sessionId) {
|
|
39
|
+
throw new Error("session-id is required");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { sessionId, follow, lines };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function getOutputLogPath(sessionId: string): Promise<string> {
|
|
46
|
+
return `${process.env.HOME}/.codepiper/sessions/${sessionId}/output.log`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function tailFile(path: string, lines: number): Promise<void> {
|
|
50
|
+
const fs = require("node:fs");
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const content = await fs.promises.readFile(path, "utf-8");
|
|
54
|
+
const allLines = content.split("\n");
|
|
55
|
+
const lastLines = allLines.slice(-lines);
|
|
56
|
+
|
|
57
|
+
process.stdout.write(lastLines.join("\n"));
|
|
58
|
+
|
|
59
|
+
if (!lastLines[lastLines.length - 1]?.endsWith("\n")) {
|
|
60
|
+
process.stdout.write("\n");
|
|
61
|
+
}
|
|
62
|
+
} catch (err: any) {
|
|
63
|
+
if (err.code === "ENOENT") {
|
|
64
|
+
console.error(`${colors.dim}No output log found at: ${path}${colors.reset}`);
|
|
65
|
+
} else {
|
|
66
|
+
throw err;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function followFile(path: string): Promise<void> {
|
|
72
|
+
const fs = require("node:fs");
|
|
73
|
+
|
|
74
|
+
let lastSize = 0;
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const stats = await fs.promises.stat(path);
|
|
78
|
+
lastSize = stats.size;
|
|
79
|
+
|
|
80
|
+
// Display initial content
|
|
81
|
+
const content = await fs.promises.readFile(path, "utf-8");
|
|
82
|
+
process.stdout.write(content);
|
|
83
|
+
} catch (err: any) {
|
|
84
|
+
if (err.code === "ENOENT") {
|
|
85
|
+
console.log(`${colors.dim}Waiting for output log...${colors.reset}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Poll for new content
|
|
90
|
+
const interval = setInterval(async () => {
|
|
91
|
+
try {
|
|
92
|
+
const stats = await fs.promises.stat(path);
|
|
93
|
+
if (stats.size > lastSize) {
|
|
94
|
+
const stream = fs.createReadStream(path, {
|
|
95
|
+
start: lastSize,
|
|
96
|
+
end: stats.size,
|
|
97
|
+
encoding: "utf-8",
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
for await (const chunk of stream) {
|
|
101
|
+
process.stdout.write(chunk);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
lastSize = stats.size;
|
|
105
|
+
}
|
|
106
|
+
} catch {
|
|
107
|
+
// File might not exist yet, keep polling
|
|
108
|
+
}
|
|
109
|
+
}, 100);
|
|
110
|
+
|
|
111
|
+
// Handle Ctrl+C
|
|
112
|
+
process.on("SIGINT", () => {
|
|
113
|
+
clearInterval(interval);
|
|
114
|
+
console.log(`\n${colors.dim}Stopped following output${colors.reset}`);
|
|
115
|
+
process.exit(0);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function runTailCommand(args: string[]): Promise<void> {
|
|
120
|
+
const options = parseTailOptions(args);
|
|
121
|
+
const logPath = await getOutputLogPath(options.sessionId);
|
|
122
|
+
|
|
123
|
+
const shortId = options.sessionId.slice(0, 8);
|
|
124
|
+
|
|
125
|
+
if (options.follow) {
|
|
126
|
+
console.log(
|
|
127
|
+
`${colors.dim}Following output for session ${colors.cyan}${shortId}...${colors.reset}`
|
|
128
|
+
);
|
|
129
|
+
console.log(`${colors.dim}Press Ctrl+C to stop${colors.reset}\n`);
|
|
130
|
+
await followFile(logPath);
|
|
131
|
+
} else {
|
|
132
|
+
console.log(
|
|
133
|
+
`${colors.dim}Last ${options.lines} lines for session ${colors.cyan}${shortId}...${colors.reset}\n`
|
|
134
|
+
);
|
|
135
|
+
await tailFile(logPath, options.lines);
|
|
136
|
+
}
|
|
137
|
+
}
|