@enter-pro/enter-cli 0.4.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.
Files changed (51) hide show
  1. package/README.md +0 -0
  2. package/dist/auth.d.ts +12 -0
  3. package/dist/auth.js +39 -0
  4. package/dist/auth.js.map +1 -0
  5. package/dist/client.d.ts +11 -0
  6. package/dist/client.js +140 -0
  7. package/dist/client.js.map +1 -0
  8. package/dist/commands/config.d.ts +2 -0
  9. package/dist/commands/config.js +37 -0
  10. package/dist/commands/config.js.map +1 -0
  11. package/dist/commands/domain.d.ts +2 -0
  12. package/dist/commands/domain.js +65 -0
  13. package/dist/commands/domain.js.map +1 -0
  14. package/dist/commands/login.d.ts +2 -0
  15. package/dist/commands/login.js +143 -0
  16. package/dist/commands/login.js.map +1 -0
  17. package/dist/commands/logout.d.ts +2 -0
  18. package/dist/commands/logout.js +9 -0
  19. package/dist/commands/logout.js.map +1 -0
  20. package/dist/commands/models.d.ts +2 -0
  21. package/dist/commands/models.js +27 -0
  22. package/dist/commands/project.d.ts +2 -0
  23. package/dist/commands/project.js +452 -0
  24. package/dist/commands/project.js.map +1 -0
  25. package/dist/commands/skill.d.ts +2 -0
  26. package/dist/commands/skill.js +118 -0
  27. package/dist/commands/thread.d.ts +2 -0
  28. package/dist/commands/thread.js +578 -0
  29. package/dist/commands/thread.js.map +1 -0
  30. package/dist/commands/whoami.d.ts +2 -0
  31. package/dist/commands/whoami.js +28 -0
  32. package/dist/commands/whoami.js.map +1 -0
  33. package/dist/commands/workspace.d.ts +2 -0
  34. package/dist/commands/workspace.js +178 -0
  35. package/dist/commands/workspace.js.map +1 -0
  36. package/dist/config.d.ts +13 -0
  37. package/dist/config.js +68 -0
  38. package/dist/config.js.map +1 -0
  39. package/dist/errors.d.ts +13 -0
  40. package/dist/errors.js +33 -0
  41. package/dist/index.d.ts +2 -0
  42. package/dist/index.js +45 -0
  43. package/dist/index.js.map +1 -0
  44. package/dist/lifecycle.d.ts +9 -0
  45. package/dist/lifecycle.js +47 -0
  46. package/dist/output.d.ts +26 -0
  47. package/dist/output.js +89 -0
  48. package/dist/output.js.map +1 -0
  49. package/dist/poll.d.ts +9 -0
  50. package/dist/poll.js +24 -0
  51. package/package.json +41 -0
@@ -0,0 +1,578 @@
1
+ import { Command } from "commander";
2
+ import { writeFileSync, readFileSync } from "fs";
3
+ import * as client from "../client.js";
4
+ import { print, printMessage, printResult, printTable, pickList, getFormat } from "../output.js";
5
+ import { pollUntil, TimeoutError } from "../poll.js";
6
+ import { resolveLifecycleStatus } from "../lifecycle.js";
7
+ export const threadCmd = new Command("thread").description("Manage project threads and chat");
8
+ // A turn has reached a terminal state when it appears in this set; everything
9
+ // else (running/pending/queued/agent_start/agent_running/...) means in-flight.
10
+ const TERMINAL_TURN_STATUSES = new Set(["completed", "cancelled", "error", "failed"]);
11
+ // Server may return PascalCase or snake_case fields; normalize to snake_case.
12
+ function normalizeAction(raw) {
13
+ return {
14
+ action_id: String(raw.action_id ?? raw.ActionID ?? ""),
15
+ tool_name: String(raw.tool_name ?? raw.ToolName ?? ""),
16
+ status: String(raw.status ?? raw.Status ?? ""),
17
+ turn: (raw.turn ?? raw.Turn ?? ""),
18
+ thread_id: String(raw.thread_id ?? raw.ThreadID ?? ""),
19
+ tool_call_id: String(raw.tool_call_id ?? raw.ToolCallID ?? ""),
20
+ created_at: String(raw.created_at ?? raw.CreatedAt ?? ""),
21
+ updated_at: String(raw.updated_at ?? raw.UpdatedAt ?? ""),
22
+ };
23
+ }
24
+ async function loadToolCallArgs(projectId, action, toolName) {
25
+ const turn = String(action.turn);
26
+ const data = await client.get(`/v1/projects/${projectId}/thread/messages`, { start_turn: turn, end_turn: turn });
27
+ for (const e of data.messages ?? []) {
28
+ if (e.message_type !== "tool_call_end")
29
+ continue;
30
+ const detail = e.detail?.tool_call_end;
31
+ if (!detail || detail.tool_name !== toolName)
32
+ continue;
33
+ // When the action carries a tool_call_id, prefer the matching event so we
34
+ // don't pick up a stale invocation from an earlier round in the same turn.
35
+ if (action.tool_call_id && detail.tool_call_id !== action.tool_call_id)
36
+ continue;
37
+ const raw = String(detail.full_arguments ?? "");
38
+ try {
39
+ return { kind: "ok", value: JSON.parse(raw) };
40
+ }
41
+ catch {
42
+ return { kind: "parse_error", raw_arguments: raw };
43
+ }
44
+ }
45
+ return { kind: "not_found" };
46
+ }
47
+ const TOOL_HANDLERS = {
48
+ // ask_user_question: surface the questions array directly so callers don't
49
+ // have to fetch + parse thread messages to know what to ask the user.
50
+ ask_user_question: {
51
+ kind: "questions",
52
+ approveSuffix: (pid, a) => `--answers '<json>' OR enter-cli thread approve ${pid} ${a.action_id} --skip-answers`,
53
+ instructions: "Questions are included in this action's `questions` field. Forward each question (with its options and multiSelect) to the user, collect answers, then run approve_command --answers '<json>' (or --skip-answers).",
54
+ enrich: async (projectId, action) => {
55
+ const args = await loadToolCallArgs(projectId, action, "ask_user_question");
56
+ switch (args.kind) {
57
+ case "ok":
58
+ return { questions: args.value.questions ?? [] };
59
+ case "parse_error":
60
+ return { questions: { raw_arguments: args.raw_arguments, parse_error: true } };
61
+ case "not_found":
62
+ return { questions: { not_found: true } };
63
+ }
64
+ },
65
+ },
66
+ supabase_add_secret: {
67
+ kind: "secret",
68
+ approveSuffix: () => `--secret-name <NAME> --secret-value <VALUE>`,
69
+ instructions: "Ask the user for the secret name and value, then substitute into approve_command.",
70
+ },
71
+ stripe_enable: {
72
+ kind: "secret",
73
+ approveSuffix: () => `--secret-name STRIPE_SECRET_KEY --secret-value <sk_...>`,
74
+ instructions: "Ask the user for the secret name and value, then substitute into approve_command.",
75
+ },
76
+ // confirm_plan_mode: surface the plan text directly on the action so callers
77
+ // don't have to fetch + parse thread messages themselves.
78
+ confirm_plan_mode: {
79
+ kind: "none",
80
+ approveSuffix: () => "",
81
+ instructions: "Plan is included in this action's `plan` field. Show it to the user, then run approve_command.",
82
+ enrich: async (projectId, action) => {
83
+ const args = await loadToolCallArgs(projectId, action, "confirm_plan_mode");
84
+ switch (args.kind) {
85
+ case "ok": {
86
+ const v = args.value;
87
+ return {
88
+ plan: {
89
+ detail: v.plan_detail ?? "",
90
+ file_path: v.plan_file_path ?? "",
91
+ },
92
+ };
93
+ }
94
+ case "parse_error":
95
+ return { plan: { raw_arguments: args.raw_arguments, parse_error: true } };
96
+ case "not_found":
97
+ return { plan: { not_found: true } };
98
+ }
99
+ },
100
+ },
101
+ };
102
+ const DEFAULT_HANDLER = {
103
+ kind: "none",
104
+ approveSuffix: () => "",
105
+ instructions: "No input required. Run the approve_command as-is.",
106
+ };
107
+ function handlerFor(toolName) {
108
+ return TOOL_HANDLERS[toolName] ?? DEFAULT_HANDLER;
109
+ }
110
+ function buildApproveCommand(projectId, action) {
111
+ const base = `enter-cli thread approve ${projectId} ${action.action_id}`;
112
+ const suffix = handlerFor(action.tool_name).approveSuffix(projectId, action);
113
+ return suffix ? `${base} ${suffix}` : base;
114
+ }
115
+ async function fetchPendingActions(projectId) {
116
+ const data = await client.post(`/v1/projects/${projectId}/thread/actions`, {});
117
+ const resp = data;
118
+ const all = (resp.actions || []).map(normalizeAction);
119
+ return all.filter((a) => a.status === "waiting_response");
120
+ }
121
+ threadCmd
122
+ .command("chat <project_id>")
123
+ .description("Send a chat message to project thread")
124
+ .option("-m, --message <text>", "Chat message content")
125
+ .option("--file <path>", "Read message content from a file (for long or multi-line messages)")
126
+ .option("--auto-approve", "Pass auto_approve flag to the server")
127
+ .action(async (id, opts, cmd) => {
128
+ let content;
129
+ if (opts.file) {
130
+ content = readFileSync(opts.file, "utf-8");
131
+ }
132
+ else if (opts.message) {
133
+ content = opts.message;
134
+ }
135
+ else {
136
+ console.error("Error: either -m <text> or --file <path> is required");
137
+ process.exit(1);
138
+ }
139
+ const body = { prompt: content, attachments: [] };
140
+ if (opts.autoApprove)
141
+ body.auto_approve = true;
142
+ const data = await client.post(`/v1/projects/${id}/thread/chat`, body);
143
+ print(getFormat(cmd), data);
144
+ });
145
+ threadCmd
146
+ .command("messages <project_id>")
147
+ .description("Get thread messages. Must specify one of --latest / --turn / --start-turn / --tail / --follow.")
148
+ .option("--start-turn <n>", "Start turn number")
149
+ .option("--end-turn <n>", "End turn number")
150
+ .option("--turn <n>", "Get messages for a specific turn (shorthand for --start-turn N --end-turn N)")
151
+ .option("--latest", "Get messages from the most recent turn (most common usage)")
152
+ .option("--tail <n>", "Get messages from the last N turns")
153
+ .option("--follow", "Follow the latest turn in real-time via WebSocket")
154
+ .action(async (id, opts, cmd) => {
155
+ if (opts.follow) {
156
+ await followThreadStream(id, opts);
157
+ return;
158
+ }
159
+ // Resolve turn range
160
+ let startTurn = opts.startTurn;
161
+ let endTurn = opts.endTurn;
162
+ if (opts.turn) {
163
+ startTurn = opts.turn;
164
+ endTurn = opts.turn;
165
+ }
166
+ else if (opts.latest || opts.tail) {
167
+ const turnsData = await client.get(`/v1/projects/${id}/thread/turns`);
168
+ const resp = turnsData;
169
+ const turns = resp.turns || [];
170
+ if (turns.length === 0) {
171
+ printMessage("No turns found.");
172
+ return;
173
+ }
174
+ if (opts.latest) {
175
+ const last = turns[0];
176
+ startTurn = String(last.turn);
177
+ endTurn = String(last.turn);
178
+ }
179
+ else if (opts.tail) {
180
+ const n = parseInt(opts.tail, 10);
181
+ // turns are newest-first; take the first n, then re-order so the
182
+ // start_turn / end_turn pair is ascending.
183
+ const slice = turns.slice(0, n);
184
+ const turnNums = slice.map((t) => Number(t.turn)).sort((a, b) => a - b);
185
+ startTurn = String(turnNums[0]);
186
+ endTurn = String(turnNums[turnNums.length - 1]);
187
+ }
188
+ }
189
+ if (!startTurn) {
190
+ console.error("Error: specify --start-turn, --turn, --latest, --tail, or --follow");
191
+ process.exit(1);
192
+ }
193
+ const params = {};
194
+ params.start_turn = startTurn;
195
+ if (endTurn)
196
+ params.end_turn = endTurn;
197
+ const data = await client.get(`/v1/projects/${id}/thread/messages`, params);
198
+ print(getFormat(cmd), data);
199
+ });
200
+ async function followThreadStream(projectId, _opts) {
201
+ const wsBase = (await import("../config.js")).baseURL()
202
+ .replace(/^https?:\/\//, "ws://")
203
+ .replace(/^wss?:\/\//, "ws://");
204
+ const { getToken } = await import("../auth.js");
205
+ const url = `${wsBase}/v1/projects/${projectId}/thread/stream`;
206
+ const token = getToken();
207
+ // Use native WebSocket available in Node 22+ or ws package fallback
208
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
209
+ let WS;
210
+ try {
211
+ WS = WebSocket;
212
+ }
213
+ catch {
214
+ try {
215
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
216
+ // @ts-ignore -- optional peer dependency
217
+ const mod = await import("ws");
218
+ WS = mod.default;
219
+ }
220
+ catch {
221
+ console.error("Error: WebSocket not available. Node 22+ required or install 'ws' package.");
222
+ process.exit(1);
223
+ }
224
+ }
225
+ const wsUrl = token ? `${url}?token=${encodeURIComponent(token)}` : url;
226
+ const ws = new WS(wsUrl, {
227
+ headers: token ? { Authorization: `Bearer ${token}` } : {},
228
+ });
229
+ const terminalTypes = new Set([
230
+ "turn_end", "turn_cancelled", "turn_error", "build_end",
231
+ ]);
232
+ let connected = false;
233
+ ws.on("open", () => {
234
+ connected = true;
235
+ console.error("[follow] WebSocket connected, streaming messages...");
236
+ });
237
+ ws.on("message", (data) => {
238
+ try {
239
+ const msg = JSON.parse(data.toString());
240
+ process.stdout.write(JSON.stringify(msg) + "\n");
241
+ if (typeof msg.message_type === "string" && terminalTypes.has(msg.message_type)) {
242
+ console.error(`[follow] Turn ended (${msg.message_type}), closing.`);
243
+ ws.close();
244
+ }
245
+ }
246
+ catch {
247
+ process.stdout.write(data.toString() + "\n");
248
+ }
249
+ });
250
+ ws.on("error", (err) => {
251
+ console.error(`[follow] WebSocket error: ${err.message}`);
252
+ if (!connected)
253
+ process.exit(1);
254
+ });
255
+ ws.on("close", () => {
256
+ console.error("[follow] Connection closed.");
257
+ });
258
+ // Keep process alive until ws closes
259
+ await new Promise((resolve) => ws.on("close", resolve));
260
+ }
261
+ threadCmd
262
+ .command("turns <project_id>")
263
+ .description("List thread turns")
264
+ .option("--latest", "Show only the most recent turn")
265
+ .action(async (id, opts, cmd) => {
266
+ const data = await client.get(`/v1/projects/${id}/thread/turns`);
267
+ const resp = data;
268
+ const turns = resp.turns || [];
269
+ const items = opts.latest ? turns.slice(-1) : turns;
270
+ const picked = pickList(items, [
271
+ "id", "turn", "turn_name", "status", "model", "credits_consumed", "created_at",
272
+ ]);
273
+ const format = getFormat(cmd);
274
+ if (format !== "table") {
275
+ print(format, { items: picked, total: picked.length });
276
+ return;
277
+ }
278
+ const rows = picked.map((t) => [
279
+ String(t.turn ?? ""),
280
+ String(t.turn_name ?? ""),
281
+ String(t.status ?? ""),
282
+ String(t.model ?? ""),
283
+ String(t.credits_consumed ?? ""),
284
+ String(t.created_at ?? ""),
285
+ ]);
286
+ printTable(["Turn", "Name", "Status", "Model", "Credits", "Created"], rows);
287
+ });
288
+ threadCmd
289
+ .command("wait <project_id>")
290
+ .description("Wait until the latest turn reaches a terminal state. Exits with status: completed | blocked | failed.")
291
+ .option("--timeout <seconds>", "Timeout in seconds", "300")
292
+ .action(async (id, opts, cmd) => {
293
+ const timeoutMs = parseInt(opts.timeout, 10) * 1000;
294
+ console.error("Waiting for turn to complete...");
295
+ let lastTurn = null;
296
+ try {
297
+ const turns = await pollUntil(async () => {
298
+ const data = await client.get(`/v1/projects/${id}/thread/turns`);
299
+ const resp = data;
300
+ return resp.turns || [];
301
+ }, (turns) => {
302
+ if (turns.length === 0)
303
+ return false;
304
+ // turns are returned newest-first.
305
+ const latest = turns[0];
306
+ return TERMINAL_TURN_STATUSES.has(String(latest.status));
307
+ }, {
308
+ intervalMs: 2000,
309
+ timeoutMs,
310
+ onTick: (elapsed) => {
311
+ process.stderr.write(`\rWaiting for turn... ${Math.round(elapsed / 1000)}s elapsed`);
312
+ },
313
+ });
314
+ process.stderr.write("\n");
315
+ lastTurn = turns[0] ?? null;
316
+ }
317
+ catch (err) {
318
+ if (err instanceof TimeoutError) {
319
+ console.error(`\nTimed out after ${opts.timeout}s waiting for the turn to terminate`);
320
+ process.exit(1);
321
+ }
322
+ throw err;
323
+ }
324
+ const turnStatus = String(lastTurn?.status ?? "");
325
+ if (turnStatus === "cancelled" || turnStatus === "error") {
326
+ print(getFormat(cmd), {
327
+ status: "failed",
328
+ reason: `turn_${turnStatus}`,
329
+ project_id: id,
330
+ turn: lastTurn,
331
+ });
332
+ process.exit(1);
333
+ }
334
+ // Turn is completed — but a "completed" turn can still be blocked
335
+ // by a pending tool action. Check actions exactly once now.
336
+ const pending = await fetchPendingActions(id);
337
+ if (pending.length > 0) {
338
+ const enriched = await Promise.all(pending.map(async (a) => {
339
+ const handler = handlerFor(a.tool_name);
340
+ const base = {
341
+ action_id: a.action_id,
342
+ tool_name: a.tool_name,
343
+ turn: a.turn,
344
+ input_kind: handler.kind,
345
+ approve_command: buildApproveCommand(id, a),
346
+ instructions: handler.instructions,
347
+ };
348
+ if (!handler.enrich)
349
+ return base;
350
+ try {
351
+ const extra = await handler.enrich(id, a);
352
+ return { ...base, ...extra };
353
+ }
354
+ catch (err) {
355
+ return { ...base, enrich_error: err.message };
356
+ }
357
+ }));
358
+ print(getFormat(cmd), {
359
+ status: "blocked",
360
+ reason: "pending_actions",
361
+ project_id: id,
362
+ actions: enriched,
363
+ });
364
+ return;
365
+ }
366
+ // Turn done, no gate. Return current project snapshot. Note: a completed
367
+ // turn may not produce a commit (e.g. config-only changes), so we do not
368
+ // wait for build_status.commit_id here. Callers that need a committed
369
+ // build (e.g. `proj publish`) check that themselves.
370
+ const detail = await client.get(`/v1/projects/${id}/detail`);
371
+ const project = (detail.project ?? detail);
372
+ print(getFormat(cmd), {
373
+ status: "completed",
374
+ project_id: id,
375
+ project: { ...project, lifecycle_status: resolveLifecycleStatus(project) },
376
+ });
377
+ });
378
+ threadCmd
379
+ .command("diff <project_id> <turn_number>")
380
+ .description("Get diff for a turn")
381
+ .option("--out <path>", "Write diff to file (JSON or .patch)")
382
+ .action(async (projectId, turnNumber, opts, cmd) => {
383
+ const data = await client.get(`/v1/projects/${projectId}/thread/turn/${turnNumber}/diff`);
384
+ if (opts.out) {
385
+ const path = opts.out;
386
+ if (path.endsWith(".patch") || path.endsWith(".diff")) {
387
+ const d = data;
388
+ const diffText = d.diff ?? d.patch ?? JSON.stringify(data, null, 2);
389
+ writeFileSync(path, String(diffText));
390
+ }
391
+ else {
392
+ writeFileSync(path, JSON.stringify(data, null, 2));
393
+ }
394
+ printMessage(`Diff written to ${path}`);
395
+ return;
396
+ }
397
+ print(getFormat(cmd), data);
398
+ });
399
+ threadCmd
400
+ .command("cancel <project_id>")
401
+ .description("Cancel the currently running turn. No-op if nothing is running.")
402
+ .action(async (id, _opts, cmd) => {
403
+ // Pre-check the latest turn — the server returns code 1000 even when
404
+ // nothing is running, which is misleading. We want a clean idempotent no-op.
405
+ const turnsResp = await client.get(`/v1/projects/${id}/thread/turns`);
406
+ const turns = turnsResp.turns ?? [];
407
+ const latest = turns[0];
408
+ const isRunning = latest && !TERMINAL_TURN_STATUSES.has(String(latest.status ?? ""));
409
+ const format = getFormat(cmd);
410
+ if (!isRunning) {
411
+ printResult(format, {
412
+ cancelled: false,
413
+ reason: "no_running_turn",
414
+ latest_turn: latest?.turn ?? null,
415
+ latest_status: latest?.status ?? null,
416
+ }, `Nothing to cancel (latest turn ${latest?.turn ?? "?"} status: ${latest?.status ?? "unknown"}).`);
417
+ return;
418
+ }
419
+ await client.post(`/v1/projects/${id}/thread/cancel`);
420
+ printResult(format, { cancelled: true, turn: latest.turn }, `Cancelled turn ${latest.turn}.`);
421
+ });
422
+ threadCmd
423
+ .command("restore <project_id>")
424
+ .description("Restore thread to a specific turn (1-based turn number from `thread turns`)")
425
+ .requiredOption("--turn <n>", "1-based turn number to restore to (NOT a turn UUID)")
426
+ .action(async (id, opts, cmd) => {
427
+ const turn = parseInt(opts.turn, 10);
428
+ if (!Number.isFinite(turn) || turn < 1) {
429
+ throw new Error("--turn must be a positive integer (1-based turn number)");
430
+ }
431
+ const data = await client.post(`/v1/projects/${id}/thread/restore`, { turn: turn });
432
+ print(getFormat(cmd), data);
433
+ });
434
+ threadCmd
435
+ .command("actions <project_id>")
436
+ .description("List tool actions for a project thread")
437
+ .option("--pending", "Show only actions awaiting approval (status: waiting_response)")
438
+ .option("--ids <ids>", "Comma-separated action IDs to filter")
439
+ .action(async (id, opts, cmd) => {
440
+ const body = {};
441
+ if (opts.ids) {
442
+ body.actions = opts.ids.split(",").map((s) => s.trim());
443
+ }
444
+ const data = await client.post(`/v1/projects/${id}/thread/actions`, body);
445
+ const resp = data;
446
+ let actions = (resp.actions || []).map(normalizeAction);
447
+ if (opts.pending) {
448
+ actions = actions.filter((a) => a.status === "waiting_response");
449
+ }
450
+ const format = getFormat(cmd);
451
+ if (format !== "table") {
452
+ print(format, { actions, total: actions.length });
453
+ return;
454
+ }
455
+ if (actions.length === 0) {
456
+ printMessage("No pending actions.");
457
+ return;
458
+ }
459
+ const rows = actions.map((a) => [
460
+ a.action_id,
461
+ a.tool_name,
462
+ a.status,
463
+ String(a.turn ?? ""),
464
+ a.created_at,
465
+ ]);
466
+ printTable(["Action ID", "Tool", "Status", "Turn", "Created"], rows);
467
+ });
468
+ async function resolveActionId(projectId, explicit) {
469
+ if (explicit)
470
+ return { kind: "ok", action_id: explicit };
471
+ const pending = await fetchPendingActions(projectId);
472
+ if (pending.length === 0)
473
+ return { kind: "none", pending };
474
+ if (pending.length > 1)
475
+ return { kind: "ambiguous", pending };
476
+ return { kind: "ok", action_id: pending[0].action_id };
477
+ }
478
+ // Emit a structured no-op result + return null (caller should not call the API),
479
+ // or return the resolved action_id when ok. Exits non-zero on ambiguity since
480
+ // it's a real error that scripts need to notice.
481
+ function handleResolution(projectId, verb, result, format) {
482
+ switch (result.kind) {
483
+ case "ok":
484
+ return result.action_id;
485
+ case "none": {
486
+ const past = verb === "approve" ? "approved" : "rejected";
487
+ printResult(format, { [past]: false, reason: "no_pending_actions", project_id: projectId }, `Nothing to ${verb} (no pending actions on this project's thread).`);
488
+ return null;
489
+ }
490
+ case "ambiguous": {
491
+ const past = verb === "approve" ? "approved" : "rejected";
492
+ const pending = result.pending.map((a) => ({
493
+ action_id: a.action_id,
494
+ tool_name: a.tool_name,
495
+ turn: a.turn,
496
+ }));
497
+ if (format === "json" || format === "yaml") {
498
+ print(format, {
499
+ [past]: false,
500
+ reason: "ambiguous_pending_actions",
501
+ project_id: projectId,
502
+ pending,
503
+ });
504
+ }
505
+ else {
506
+ printMessage(`${result.pending.length} pending actions found; pass an action_id explicitly:`);
507
+ for (const a of result.pending) {
508
+ printMessage(` - ${a.action_id} ${a.tool_name}`);
509
+ }
510
+ }
511
+ process.exit(1);
512
+ }
513
+ }
514
+ }
515
+ threadCmd
516
+ .command("approve <project_id> [action_id]")
517
+ .description([
518
+ "Approve a pending tool action. action_id is optional when exactly one pending action exists.",
519
+ "Tool-specific options:",
520
+ " supabase_add_secret : --secret-name <name> --secret-value <value>",
521
+ " stripe_enable : --secret-name STRIPE_SECRET_KEY --secret-value sk_...",
522
+ " ask_user_question : --answers '<json>' or --skip-answers",
523
+ " others (supabase_enable, stripe_create_products_and_prices,",
524
+ " enable_ai_capability, confirm_skill): no extra flags needed",
525
+ ].join("\n"))
526
+ .option("--secret-name <name>", "Secret variable name (supabase_add_secret / stripe_enable)")
527
+ .option("--secret-value <value>", "Secret variable value (supabase_add_secret / stripe_enable)")
528
+ .option("--tool-result <result>", "Custom tool result string")
529
+ .option("--answers <json>", 'Answers for ask_user_question, JSON: \'{"Q text": {"selected_options": ["A"], "other_text": ""}}\'')
530
+ .option("--skip-answers", "Skip all questions for ask_user_question (sets skipped: true)")
531
+ .action(async (projectId, actionIdArg, opts, cmd) => {
532
+ const format = getFormat(cmd);
533
+ const resolved = await resolveActionId(projectId, actionIdArg);
534
+ const actionId = handleResolution(projectId, "approve", resolved, format);
535
+ if (!actionId)
536
+ return;
537
+ const actionResponse = {
538
+ action_id: actionId,
539
+ response: "approved",
540
+ };
541
+ if (opts.secretName)
542
+ actionResponse.secret_name = opts.secretName;
543
+ if (opts.secretValue)
544
+ actionResponse.secret_value = opts.secretValue;
545
+ if (opts.toolResult)
546
+ actionResponse.tool_result = opts.toolResult;
547
+ if (opts.skipAnswers) {
548
+ actionResponse.question_answers = { answers: {}, skipped: true };
549
+ }
550
+ else if (opts.answers) {
551
+ try {
552
+ const parsed = JSON.parse(opts.answers);
553
+ actionResponse.question_answers = { answers: parsed, skipped: false };
554
+ }
555
+ catch {
556
+ console.error("Error: --answers must be valid JSON");
557
+ process.exit(1);
558
+ }
559
+ }
560
+ const data = await client.post(`/v1/projects/${projectId}/thread/chat`, {
561
+ action_response: actionResponse,
562
+ });
563
+ print(format, data);
564
+ });
565
+ threadCmd
566
+ .command("reject <project_id> [action_id]")
567
+ .description("Reject a pending tool action. action_id is optional when exactly one pending action exists.")
568
+ .action(async (projectId, actionIdArg, _opts, cmd) => {
569
+ const format = getFormat(cmd);
570
+ const resolved = await resolveActionId(projectId, actionIdArg);
571
+ const actionId = handleResolution(projectId, "reject", resolved, format);
572
+ if (!actionId)
573
+ return;
574
+ const data = await client.post(`/v1/projects/${projectId}/thread/chat`, {
575
+ action_response: { action_id: actionId, response: "rejected" },
576
+ });
577
+ print(format, data);
578
+ });
@@ -0,0 +1 @@
1
+ {"version":3,"file":"thread.js","sourceRoot":"","sources":["../../src/commands/thread.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const whoamiCmd: Command;
@@ -0,0 +1,28 @@
1
+ import { Command } from "commander";
2
+ import { isAuthenticated } from "../auth.js";
3
+ import * as client from "../client.js";
4
+ import { print, printTable } from "../output.js";
5
+ export const whoamiCmd = new Command("whoami")
6
+ .description("Show current user info")
7
+ .action(async (_opts, cmd) => {
8
+ if (!isAuthenticated()) {
9
+ throw new Error("Not authenticated. Run `enter login` or set ENTER_API_KEY environment variable.");
10
+ }
11
+ const data = await client.get("/v1/users/info");
12
+ const format = cmd.optsWithGlobals().output || "json";
13
+ const wrapper = data;
14
+ const user = (wrapper.user ?? data);
15
+ if (format !== "table") {
16
+ print(format, user);
17
+ return;
18
+ }
19
+ printTable(["ID", "Email", "Name", "Public ID", "Status"], [
20
+ [
21
+ String(user.user_id ?? ""),
22
+ String(user.email ?? ""),
23
+ String(user.name ?? ""),
24
+ String(user.public_user_id ?? ""),
25
+ String(user.status ?? ""),
26
+ ],
27
+ ]);
28
+ });
@@ -0,0 +1 @@
1
+ {"version":3,"file":"whoami.js","sourceRoot":"","sources":["../../src/commands/whoami.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,KAAK,MAAM,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,KAAK,EAAa,UAAU,EAAE,MAAM,cAAc,CAAC;AAE5D,MAAM,CAAC,MAAM,SAAS,GAAG,IAAI,OAAO,CAAC,QAAQ,CAAC;KAC3C,WAAW,CAAC,wBAAwB,CAAC;KACrC,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;IAC3B,IAAI,CAAC,eAAe,EAAE,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CAAC,6CAA6C,CAAC,CAAC;IACjE,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;IAChD,MAAM,MAAM,GAAG,GAAG,CAAC,eAAe,EAAE,CAAC,MAAM,IAAI,MAAM,CAAC;IACtD,MAAM,OAAO,GAAG,IAA+B,CAAC;IAChD,MAAM,IAAI,GAAG,CAAC,OAAO,CAAC,IAAI,IAAI,IAAI,CAA4B,CAAC;IAE/D,IAAI,MAAM,KAAK,OAAO,EAAE,CAAC;QACvB,KAAK,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QACpB,OAAO;IACT,CAAC;IAED,UAAU,CACR,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,CAAC,EAC9C;QACE;YACE,MAAM,CAAC,IAAI,CAAC,OAAO,IAAI,EAAE,CAAC;YAC1B,MAAM,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;YACxB,MAAM,CAAC,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC;YACvB,MAAM,CAAC,IAAI,CAAC,cAAc,IAAI,EAAE,CAAC;YACjC,MAAM,CAAC,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC;SAC1B;KACF,CACF,CAAC;AACJ,CAAC,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const workspaceCmd: Command;