@tiny-fish/cli 0.1.1 → 0.1.2-next.15

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.
@@ -1,7 +1,7 @@
1
1
  import { spawn } from "child_process";
2
2
  import * as readline from "readline";
3
3
  import { DASHBOARD_URL, clearConfig, loadConfig, maskKey, saveConfig, validateKeyFormat, } from "../lib/auth.js";
4
- import { err, out, outLine } from "../lib/output.js";
4
+ import { err, errLine, out, outLine } from "../lib/output.js";
5
5
  /**
6
6
  * Read a key from stdin without echoing it to the terminal.
7
7
  * Uses setRawMode on TTY so characters are never written to the screen.
@@ -71,7 +71,7 @@ export function registerAuth(program) {
71
71
  .command("login")
72
72
  .description("Open the API keys page and save your key interactively")
73
73
  .action(async () => {
74
- process.stderr.write(`Opening ${DASHBOARD_URL} in your browser...\n`);
74
+ errLine(`Opening ${DASHBOARD_URL} in your browser...`);
75
75
  try {
76
76
  // Use spawn with an explicit args array — no shell, no interpolation
77
77
  let cmd;
@@ -166,6 +166,7 @@ export function registerAuth(program) {
166
166
  else {
167
167
  out(result);
168
168
  }
169
+ process.exit(1);
169
170
  });
170
171
  auth
171
172
  .command("logout")
@@ -1,6 +1,6 @@
1
1
  import { getApiKey } from "../lib/auth.js";
2
- import { runAsync, runStream, runSync } from "../lib/client.js";
3
- import { err, handleApiError, out, outLine } from "../lib/output.js";
2
+ import { cancelRun, runAsync, runStream, runSync } from "../lib/client.js";
3
+ import { err, errLine, handleApiError, out, outLine } from "../lib/output.js";
4
4
  import { RunStatus, StreamEventType } from "../lib/types.js";
5
5
  import { BASE_URL } from "../lib/constants.js";
6
6
  const RUN_TIMEOUT_MS = 20 * 60 * 1000; // 20 minutes
@@ -53,6 +53,10 @@ export function registerRun(agentCmd) {
53
53
  process.exit(1);
54
54
  }
55
55
  }
56
+ if (!goal.trim()) {
57
+ err({ error: 'Goal cannot be empty' });
58
+ process.exit(1);
59
+ }
56
60
  const apiKey = getApiKey();
57
61
  const req = { goal, url: normalizedUrl };
58
62
  if (opts.async) {
@@ -67,7 +71,7 @@ export function registerRun(agentCmd) {
67
71
  const { run_id: _asyncRunId, ...asyncRest } = result; // eslint-disable-line @typescript-eslint/no-unused-vars
68
72
  const asyncResultWithUrl = { run_id: result.run_id, run_url: `${BASE_URL}/runs/${result.run_id}`, ...asyncRest };
69
73
  if (opts.pretty) {
70
- outLine(`Run submitted\nID: ${asyncResultWithUrl.run_id}\nStatus: ${asyncResultWithUrl.status}\nURL: ${asyncResultWithUrl.run_url}`);
74
+ outLine(`Run submitted\nID: ${asyncResultWithUrl.run_id}\nURL: ${asyncResultWithUrl.run_url}`);
71
75
  }
72
76
  else {
73
77
  out(asyncResultWithUrl);
@@ -75,7 +79,7 @@ export function registerRun(agentCmd) {
75
79
  return;
76
80
  }
77
81
  if (opts.sync) {
78
- process.stderr.write("Waiting for result...\n");
82
+ errLine("Waiting for result...");
79
83
  let result;
80
84
  try {
81
85
  result = await runSync(req, apiKey, AbortSignal.timeout(RUN_TIMEOUT_MS));
@@ -97,6 +101,7 @@ export function registerRun(agentCmd) {
97
101
  // Default: SSE stream — 20 min timeout + Ctrl+C cancellation
98
102
  const controller = new AbortController();
99
103
  let siginted = false;
104
+ let capturedRunId = null;
100
105
  const timeout = setTimeout(() => controller.abort(), RUN_TIMEOUT_MS);
101
106
  const onSigint = () => {
102
107
  siginted = true;
@@ -106,18 +111,26 @@ export function registerRun(agentCmd) {
106
111
  let streamFailed = false;
107
112
  try {
108
113
  for await (const event of runStream(req, apiKey, controller.signal)) {
109
- if (!handleStreamEvent(event, opts.pretty)) {
114
+ if (!handleStreamEvent(event, opts.pretty, (id) => { capturedRunId = id; })) {
110
115
  streamFailed = true;
111
116
  break;
112
117
  }
113
118
  }
114
119
  }
115
120
  catch (e) {
116
- if (controller.signal.aborted) {
117
- if (siginted) {
118
- process.stderr.write("\n");
119
- process.exit(130);
121
+ if (controller.signal.aborted && siginted) {
122
+ let cancelled = false;
123
+ if (capturedRunId) {
124
+ try {
125
+ await cancelRun(capturedRunId, apiKey);
126
+ cancelled = true;
127
+ }
128
+ catch { /* ignore */ }
120
129
  }
130
+ process.stderr.write(cancelled ? "\nRun cancelled.\n" : "\n");
131
+ process.exit(130);
132
+ }
133
+ if (controller.signal.aborted) {
121
134
  err({ error: "Run timed out after 20 minutes" });
122
135
  process.exit(1);
123
136
  }
@@ -136,7 +149,7 @@ export function registerRun(agentCmd) {
136
149
  * Handle one SSE event from the stream.
137
150
  * Returns false on terminal error (caller should exit 1); true otherwise.
138
151
  */
139
- function handleStreamEvent(event, pretty) {
152
+ function handleStreamEvent(event, pretty, onRunId) {
140
153
  if (event.type === StreamEventType.ERROR) {
141
154
  err({ error: event.error, run_id: event.run_id, status: StreamEventType.ERROR });
142
155
  return false;
@@ -148,6 +161,8 @@ function handleStreamEvent(event, pretty) {
148
161
  if (pretty) {
149
162
  switch (event.type) {
150
163
  case StreamEventType.STARTED:
164
+ if (onRunId && event.run_id)
165
+ onRunId(event.run_id);
151
166
  outLine(`▶ Run started`);
152
167
  break;
153
168
  case StreamEventType.STREAMING_URL:
@@ -1,5 +1,5 @@
1
1
  import { getApiKey } from "../lib/auth.js";
2
- import { getRun, listRuns } from "../lib/client.js";
2
+ import { cancelRun, getRun, listRuns } from "../lib/client.js";
3
3
  import { err, handleApiError, out, outLine } from "../lib/output.js";
4
4
  import { RunStatus } from "../lib/types.js";
5
5
  const VALID_STATUSES = Object.values(RunStatus);
@@ -95,4 +95,31 @@ export function registerRuns(runCmd) {
95
95
  handleApiError(e);
96
96
  }
97
97
  });
98
+ // ── run cancel ────────────────────────────────────────────────────────────
99
+ runCmd
100
+ .command("cancel <run_id>")
101
+ .description("Cancel a run by ID")
102
+ .option("--pretty", "Human-readable output")
103
+ .action(async (runId, opts) => {
104
+ const apiKey = getApiKey();
105
+ try {
106
+ const result = await cancelRun(runId, apiKey);
107
+ if (opts.pretty) {
108
+ if (result.status === RunStatus.CANCELLED) {
109
+ outLine(result.message
110
+ ? `Run ${result.run_id} cancelled (${result.message}).`
111
+ : `Run ${result.run_id} cancelled.`);
112
+ }
113
+ else {
114
+ outLine(`Run ${result.run_id} already finished (${result.status}).`);
115
+ }
116
+ }
117
+ else {
118
+ out(result);
119
+ }
120
+ }
121
+ catch (e) {
122
+ handleApiError(e);
123
+ }
124
+ });
98
125
  }
@@ -1,6 +1,7 @@
1
- import type { ListRunsOptions, ListRunsResponse, RunApiResponse, RunRequest, RunResult, StreamEvent } from "./types.js";
1
+ import type { CancelRunResponse, ListRunsOptions, ListRunsResponse, RunApiResponse, RunRequest, RunResult, StreamEvent } from "./types.js";
2
2
  export declare function runSync(req: RunRequest, apiKey: string, signal?: AbortSignal): Promise<RunResult>;
3
3
  export declare function runAsync(req: RunRequest, apiKey: string): Promise<RunResult>;
4
4
  export declare function runStream(req: RunRequest, apiKey: string, signal?: AbortSignal): AsyncGenerator<StreamEvent>;
5
5
  export declare function listRuns(opts: ListRunsOptions, apiKey: string): Promise<ListRunsResponse>;
6
6
  export declare function getRun(runId: string, apiKey: string): Promise<RunApiResponse>;
7
+ export declare function cancelRun(runId: string, apiKey: string): Promise<CancelRunResponse>;
@@ -1,5 +1,6 @@
1
1
  import { BASE_URL } from "./constants.js";
2
2
  import { ApiError } from "./output.js";
3
+ const API_TIMEOUT_MS = 30_000;
3
4
  // ── Debug logging ─────────────────────────────────────────────────────────────
4
5
  const DEBUG_ENABLED = /^(1|true)$/i.test(process.env["TINYFISH_DEBUG"] ?? "");
5
6
  function debugLog(msg) {
@@ -73,7 +74,7 @@ export async function runSync(req, apiKey, signal) {
73
74
  }
74
75
  export async function runAsync(req, apiKey) {
75
76
  // Async just submits the job — 30 s is plenty to get an ACK
76
- const res = await postJson("/v1/automation/run-async", req, apiKey, AbortSignal.timeout(30_000));
77
+ const res = await postJson("/v1/automation/run-async", req, apiKey, AbortSignal.timeout(API_TIMEOUT_MS));
77
78
  return res.json();
78
79
  }
79
80
  export async function* runStream(req, apiKey, signal) {
@@ -111,8 +112,12 @@ export async function listRuns(opts, apiKey) {
111
112
  if (opts.cursor)
112
113
  params.set("cursor", opts.cursor);
113
114
  const qs = params.size ? `?${params}` : "";
114
- return getJson(`/v1/runs${qs}`, apiKey, AbortSignal.timeout(30_000));
115
+ return getJson(`/v1/runs${qs}`, apiKey, AbortSignal.timeout(API_TIMEOUT_MS));
115
116
  }
116
117
  export async function getRun(runId, apiKey) {
117
- return getJson(`/v1/runs/${encodeURIComponent(runId)}`, apiKey, AbortSignal.timeout(30_000));
118
+ return getJson(`/v1/runs/${encodeURIComponent(runId)}`, apiKey, AbortSignal.timeout(API_TIMEOUT_MS));
119
+ }
120
+ export async function cancelRun(runId, apiKey) {
121
+ const res = await postJson(`/v1/runs/${encodeURIComponent(runId)}/cancel`, {}, apiKey, AbortSignal.timeout(API_TIMEOUT_MS));
122
+ return res.json();
118
123
  }
@@ -5,5 +5,6 @@ export declare class ApiError extends Error {
5
5
  }
6
6
  export declare function out(data: unknown): void;
7
7
  export declare function outLine(line: string): void;
8
+ export declare function errLine(line: string): void;
8
9
  export declare function err(data: unknown): void;
9
10
  export declare function handleApiError(e: unknown): never;
@@ -14,6 +14,9 @@ export function out(data) {
14
14
  export function outLine(line) {
15
15
  process.stdout.write(line + "\n");
16
16
  }
17
+ export function errLine(line) {
18
+ process.stderr.write(line + "\n");
19
+ }
17
20
  export function err(data) {
18
21
  const payload = typeof data === "string" ? { error: data } : data;
19
22
  process.stderr.write(JSON.stringify(payload) + "\n");
@@ -94,3 +94,9 @@ export interface ListRunsOptions {
94
94
  limit?: number;
95
95
  cursor?: string;
96
96
  }
97
+ export interface CancelRunResponse {
98
+ run_id: string;
99
+ status: RunStatus | string;
100
+ cancelled_at: string;
101
+ message: string | null;
102
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tiny-fish/cli",
3
- "version": "0.1.1",
4
- "description": "TinyFish CLI \u2014 run web automations from your terminal",
3
+ "version": "0.1.2-next.15",
4
+ "description": "TinyFish CLI run web automations from your terminal",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "tinyfish": "./dist/index.js"
@@ -39,7 +39,6 @@
39
39
  "node": ">=24.0.0"
40
40
  },
41
41
  "publishConfig": {
42
- "registry": "https://registry.npmjs.org/",
43
- "access": "restricted"
42
+ "registry": "https://registry.npmjs.org/"
44
43
  }
45
44
  }