@tinybirdco/sdk 0.0.10 → 0.0.12

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 (63) hide show
  1. package/README.md +35 -0
  2. package/dist/api/branches.d.ts +12 -1
  3. package/dist/api/branches.d.ts.map +1 -1
  4. package/dist/api/branches.js +21 -2
  5. package/dist/api/branches.js.map +1 -1
  6. package/dist/api/branches.test.js +95 -5
  7. package/dist/api/branches.test.js.map +1 -1
  8. package/dist/api/local.d.ts +15 -0
  9. package/dist/api/local.d.ts.map +1 -1
  10. package/dist/api/local.js +52 -0
  11. package/dist/api/local.js.map +1 -1
  12. package/dist/api/local.test.js +80 -1
  13. package/dist/api/local.test.js.map +1 -1
  14. package/dist/cli/commands/clear.d.ts +37 -0
  15. package/dist/cli/commands/clear.d.ts.map +1 -0
  16. package/dist/cli/commands/clear.js +141 -0
  17. package/dist/cli/commands/clear.js.map +1 -0
  18. package/dist/cli/index.js +45 -0
  19. package/dist/cli/index.js.map +1 -1
  20. package/dist/generator/datasource.d.ts.map +1 -1
  21. package/dist/generator/datasource.js +28 -0
  22. package/dist/generator/datasource.js.map +1 -1
  23. package/dist/generator/datasource.test.js +65 -0
  24. package/dist/generator/datasource.test.js.map +1 -1
  25. package/dist/generator/pipe.d.ts.map +1 -1
  26. package/dist/generator/pipe.js +22 -0
  27. package/dist/generator/pipe.js.map +1 -1
  28. package/dist/generator/pipe.test.js +55 -0
  29. package/dist/generator/pipe.test.js.map +1 -1
  30. package/dist/index.d.ts +4 -2
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +2 -0
  33. package/dist/index.js.map +1 -1
  34. package/dist/schema/datasource.d.ts +18 -3
  35. package/dist/schema/datasource.d.ts.map +1 -1
  36. package/dist/schema/datasource.js.map +1 -1
  37. package/dist/schema/pipe.d.ts +17 -2
  38. package/dist/schema/pipe.d.ts.map +1 -1
  39. package/dist/schema/pipe.js.map +1 -1
  40. package/dist/schema/token.d.ts +49 -0
  41. package/dist/schema/token.d.ts.map +1 -0
  42. package/dist/schema/token.js +47 -0
  43. package/dist/schema/token.js.map +1 -0
  44. package/dist/schema/token.test.d.ts +2 -0
  45. package/dist/schema/token.test.d.ts.map +1 -0
  46. package/dist/schema/token.test.js +50 -0
  47. package/dist/schema/token.test.js.map +1 -0
  48. package/package.json +1 -1
  49. package/src/api/branches.test.ts +116 -4
  50. package/src/api/branches.ts +28 -2
  51. package/src/api/local.test.ts +106 -0
  52. package/src/api/local.ts +77 -0
  53. package/src/cli/commands/clear.ts +194 -0
  54. package/src/cli/index.ts +56 -0
  55. package/src/generator/datasource.test.ts +77 -0
  56. package/src/generator/datasource.ts +33 -1
  57. package/src/generator/pipe.test.ts +63 -0
  58. package/src/generator/pipe.ts +26 -0
  59. package/src/index.ts +12 -0
  60. package/src/schema/datasource.ts +20 -3
  61. package/src/schema/pipe.ts +19 -2
  62. package/src/schema/token.test.ts +60 -0
  63. package/src/schema/token.ts +75 -0
@@ -7,6 +7,7 @@ import {
7
7
  deleteBranch,
8
8
  branchExists,
9
9
  getOrCreateBranch,
10
+ clearBranch,
10
11
  type BranchApiConfig,
11
12
  } from "./branches.js";
12
13
 
@@ -271,16 +272,39 @@ describe("Branch API client", () => {
271
272
 
272
273
  describe("deleteBranch", () => {
273
274
  it("deletes a branch successfully", async () => {
275
+ const mockBranch = {
276
+ id: "branch-123",
277
+ name: "my-feature",
278
+ token: "p.branch-token",
279
+ created_at: "2024-01-01T00:00:00Z",
280
+ };
281
+
282
+ // 1. getBranch to get the ID
283
+ mockFetch.mockResolvedValueOnce({
284
+ ok: true,
285
+ json: () => Promise.resolve(mockBranch),
286
+ });
287
+
288
+ // 2. DELETE the branch by ID
274
289
  mockFetch.mockResolvedValueOnce({
275
290
  ok: true,
276
291
  });
277
292
 
278
293
  await deleteBranch(config, "my-feature");
279
294
 
280
- const [url, init] = mockFetch.mock.calls[0];
281
- const parsed = expectFromParam(url);
282
- expect(parsed.pathname).toBe("/v1/environments/my-feature");
283
- expect(init).toEqual({
295
+ expect(mockFetch).toHaveBeenCalledTimes(2);
296
+
297
+ // First call: get branch
298
+ const [getUrl, getInit] = mockFetch.mock.calls[0];
299
+ const getParsed = expectFromParam(getUrl);
300
+ expect(getParsed.pathname).toBe("/v0/environments/my-feature");
301
+ expect(getInit.method).toBe("GET");
302
+
303
+ // Second call: delete branch by ID
304
+ const [deleteUrl, deleteInit] = mockFetch.mock.calls[1];
305
+ const deleteParsed = expectFromParam(deleteUrl);
306
+ expect(deleteParsed.pathname).toBe("/v0/environments/branch-123");
307
+ expect(deleteInit).toEqual({
284
308
  method: "DELETE",
285
309
  headers: {
286
310
  Authorization: "Bearer p.test-token",
@@ -382,4 +406,92 @@ describe("Branch API client", () => {
382
406
  expect(mockFetch).toHaveBeenCalledTimes(4);
383
407
  });
384
408
  });
409
+
410
+ describe("clearBranch", () => {
411
+ it("clears a branch by deleting and recreating it", async () => {
412
+ const existingBranch = {
413
+ id: "branch-old",
414
+ name: "my-feature",
415
+ token: "p.old-token",
416
+ created_at: "2024-01-01T00:00:00Z",
417
+ };
418
+
419
+ const newBranch = {
420
+ id: "branch-new",
421
+ name: "my-feature",
422
+ token: "p.new-token",
423
+ created_at: "2024-01-02T00:00:00Z",
424
+ };
425
+
426
+ // 1. GET branch to get ID (for delete)
427
+ mockFetch.mockResolvedValueOnce({
428
+ ok: true,
429
+ json: () => Promise.resolve(existingBranch),
430
+ });
431
+
432
+ // 2. DELETE branch by ID
433
+ mockFetch.mockResolvedValueOnce({
434
+ ok: true,
435
+ });
436
+
437
+ // 3. POST to /v1/environments returns a job
438
+ mockFetch.mockResolvedValueOnce({
439
+ ok: true,
440
+ json: () => Promise.resolve({
441
+ job: { id: "job-789", status: "waiting" },
442
+ workspace: { id: "ws-789" },
443
+ }),
444
+ });
445
+
446
+ // 4. Poll job - done
447
+ mockFetch.mockResolvedValueOnce({
448
+ ok: true,
449
+ json: () => Promise.resolve({ id: "job-789", status: "done" }),
450
+ });
451
+
452
+ // 5. Get branch with token (after create)
453
+ mockFetch.mockResolvedValueOnce({
454
+ ok: true,
455
+ json: () => Promise.resolve(newBranch),
456
+ });
457
+
458
+ const result = await clearBranch(config, "my-feature");
459
+
460
+ expect(mockFetch).toHaveBeenCalledTimes(5);
461
+
462
+ // Verify get was called first
463
+ const [getUrl, getInit] = mockFetch.mock.calls[0];
464
+ const getParsed = expectFromParam(getUrl);
465
+ expect(getParsed.pathname).toBe("/v0/environments/my-feature");
466
+ expect(getInit.method).toBe("GET");
467
+
468
+ // Verify delete was called with ID
469
+ const [deleteUrl, deleteInit] = mockFetch.mock.calls[1];
470
+ const deleteParsed = expectFromParam(deleteUrl);
471
+ expect(deleteParsed.pathname).toBe("/v0/environments/branch-old");
472
+ expect(deleteInit.method).toBe("DELETE");
473
+
474
+ // Verify create was called
475
+ const [createUrl, createInit] = mockFetch.mock.calls[2];
476
+ const createParsed = expectFromParam(createUrl);
477
+ expect(createParsed.pathname).toBe("/v1/environments");
478
+ expect(createParsed.searchParams.get("name")).toBe("my-feature");
479
+ expect(createInit.method).toBe("POST");
480
+
481
+ expect(result).toEqual(newBranch);
482
+ });
483
+
484
+ it("throws BranchApiError when branch does not exist", async () => {
485
+ mockFetch.mockResolvedValueOnce({
486
+ ok: false,
487
+ status: 404,
488
+ statusText: "Not Found",
489
+ text: () => Promise.resolve("Branch not found"),
490
+ });
491
+
492
+ await expect(clearBranch(config, "nonexistent")).rejects.toThrow(
493
+ BranchApiError
494
+ );
495
+ });
496
+ });
385
497
  });
@@ -264,7 +264,10 @@ export async function getBranch(
264
264
 
265
265
  /**
266
266
  * Delete a branch
267
- * DELETE /v1/environments/{name}
267
+ * DELETE /v0/environments/{id}
268
+ *
269
+ * Note: The API requires the branch ID, not name. This function first
270
+ * fetches the branch to get its ID, then deletes it.
268
271
  *
269
272
  * @param config - API configuration
270
273
  * @param name - Branch name to delete
@@ -273,7 +276,10 @@ export async function deleteBranch(
273
276
  config: BranchApiConfig,
274
277
  name: string
275
278
  ): Promise<void> {
276
- const url = new URL(`/v1/environments/${encodeURIComponent(name)}`, config.baseUrl);
279
+ // First get the branch to find its ID
280
+ const branch = await getBranch(config, name);
281
+
282
+ const url = new URL(`/v0/environments/${branch.id}`, config.baseUrl);
277
283
 
278
284
  const response = await tinybirdFetch(url.toString(), {
279
285
  method: "DELETE",
@@ -334,3 +340,23 @@ export async function getOrCreateBranch(
334
340
  throw error;
335
341
  }
336
342
  }
343
+
344
+ /**
345
+ * Clear a branch by deleting and recreating it
346
+ *
347
+ * @param config - API configuration
348
+ * @param name - Branch name to clear
349
+ * @returns The recreated branch with token
350
+ */
351
+ export async function clearBranch(
352
+ config: BranchApiConfig,
353
+ name: string
354
+ ): Promise<TinybirdBranch> {
355
+ // Delete the branch
356
+ await deleteBranch(config, name);
357
+
358
+ // Recreate the branch
359
+ const branch = await createBranch(config, name);
360
+
361
+ return branch;
362
+ }
@@ -6,6 +6,8 @@ import {
6
6
  listLocalWorkspaces,
7
7
  createLocalWorkspace,
8
8
  getOrCreateLocalWorkspace,
9
+ deleteLocalWorkspace,
10
+ clearLocalWorkspace,
9
11
  isLocalRunning,
10
12
  getLocalWorkspaceName,
11
13
  LocalNotRunningError,
@@ -259,4 +261,108 @@ describe("Local API", () => {
259
261
  expect(name1).not.toBe(name2);
260
262
  });
261
263
  });
264
+
265
+ describe("deleteLocalWorkspace", () => {
266
+ it("deletes a workspace successfully", async () => {
267
+ server.use(
268
+ http.delete(`${LOCAL_BASE_URL}/v1/workspaces/ws-123`, () => {
269
+ return new HttpResponse(null, { status: 204 });
270
+ })
271
+ );
272
+
273
+ await deleteLocalWorkspace("user-token", "ws-123");
274
+ // No error means success
275
+ });
276
+
277
+ it("throws LocalApiError on failure", async () => {
278
+ server.use(
279
+ http.delete(`${LOCAL_BASE_URL}/v1/workspaces/ws-123`, () => {
280
+ return new HttpResponse("Not found", { status: 404 });
281
+ })
282
+ );
283
+
284
+ await expect(deleteLocalWorkspace("user-token", "ws-123")).rejects.toThrow(
285
+ LocalApiError
286
+ );
287
+ });
288
+ });
289
+
290
+ describe("clearLocalWorkspace", () => {
291
+ const tokens = {
292
+ user_token: "user-token",
293
+ admin_token: "admin-token",
294
+ workspace_admin_token: "default-token",
295
+ };
296
+
297
+ it("clears a workspace by deleting and recreating it", async () => {
298
+ let deleteCount = 0;
299
+ let createCount = 0;
300
+
301
+ server.use(
302
+ http.get(`${LOCAL_BASE_URL}/v1/user/workspaces`, () => {
303
+ // First call: workspace exists
304
+ // Second call: workspace deleted
305
+ // Third call: workspace recreated
306
+ if (deleteCount === 0) {
307
+ return HttpResponse.json({
308
+ organization_id: "org-123",
309
+ workspaces: [
310
+ { id: "ws-123", name: "MyWorkspace", token: "old-token" },
311
+ ],
312
+ });
313
+ } else if (createCount === 0) {
314
+ return HttpResponse.json({
315
+ organization_id: "org-123",
316
+ workspaces: [],
317
+ });
318
+ } else {
319
+ return HttpResponse.json({
320
+ organization_id: "org-123",
321
+ workspaces: [
322
+ { id: "ws-456", name: "MyWorkspace", token: "new-token" },
323
+ ],
324
+ });
325
+ }
326
+ }),
327
+ http.delete(`${LOCAL_BASE_URL}/v1/workspaces/ws-123`, () => {
328
+ deleteCount++;
329
+ return new HttpResponse(null, { status: 204 });
330
+ }),
331
+ http.post(`${LOCAL_BASE_URL}/v1/workspaces`, () => {
332
+ createCount++;
333
+ return HttpResponse.json({
334
+ id: "ws-456",
335
+ name: "MyWorkspace",
336
+ token: "new-token",
337
+ });
338
+ })
339
+ );
340
+
341
+ const result = await clearLocalWorkspace(tokens, "MyWorkspace");
342
+
343
+ expect(deleteCount).toBe(1);
344
+ expect(createCount).toBe(1);
345
+ expect(result.id).toBe("ws-456");
346
+ expect(result.name).toBe("MyWorkspace");
347
+ expect(result.token).toBe("new-token");
348
+ });
349
+
350
+ it("throws LocalApiError when workspace not found", async () => {
351
+ server.use(
352
+ http.get(`${LOCAL_BASE_URL}/v1/user/workspaces`, () => {
353
+ return HttpResponse.json({
354
+ organization_id: "org-123",
355
+ workspaces: [],
356
+ });
357
+ })
358
+ );
359
+
360
+ await expect(clearLocalWorkspace(tokens, "NonExistent")).rejects.toThrow(
361
+ LocalApiError
362
+ );
363
+ await expect(clearLocalWorkspace(tokens, "NonExistent")).rejects.toThrow(
364
+ "Workspace 'NonExistent' not found"
365
+ );
366
+ });
367
+ });
262
368
  });
package/src/api/local.ts CHANGED
@@ -269,3 +269,80 @@ export function getLocalWorkspaceName(
269
269
  const hash = crypto.createHash("sha256").update(cwd).digest("hex");
270
270
  return `Build_${hash.substring(0, 16)}`;
271
271
  }
272
+
273
+ /**
274
+ * Delete a workspace in local Tinybird
275
+ *
276
+ * @param userToken - User token from getLocalTokens()
277
+ * @param workspaceId - ID of the workspace to delete
278
+ */
279
+ export async function deleteLocalWorkspace(
280
+ userToken: string,
281
+ workspaceId: string
282
+ ): Promise<void> {
283
+ const url = `${LOCAL_BASE_URL}/v1/workspaces/${workspaceId}?hard_delete_confirmation=yes`;
284
+
285
+ const response = await tinybirdFetch(url, {
286
+ method: "DELETE",
287
+ headers: {
288
+ Authorization: `Bearer ${userToken}`,
289
+ },
290
+ });
291
+
292
+ if (!response.ok) {
293
+ const responseBody = await response.text();
294
+ throw new LocalApiError(
295
+ `Failed to delete local workspace: ${response.status} ${response.statusText}`,
296
+ response.status,
297
+ responseBody
298
+ );
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Clear a workspace in local Tinybird by deleting and recreating it
304
+ *
305
+ * @param tokens - Tokens from getLocalTokens()
306
+ * @param workspaceName - Name of the workspace to clear
307
+ * @returns The recreated workspace
308
+ */
309
+ export async function clearLocalWorkspace(
310
+ tokens: LocalTokens,
311
+ workspaceName: string
312
+ ): Promise<LocalWorkspace> {
313
+ // List existing workspaces to find the one to clear
314
+ const { workspaces, organizationId } = await listLocalWorkspaces(tokens.admin_token);
315
+
316
+ // Find the workspace by name
317
+ const workspace = workspaces.find((ws) => ws.name === workspaceName);
318
+ if (!workspace) {
319
+ throw new LocalApiError(`Workspace '${workspaceName}' not found`);
320
+ }
321
+
322
+ // Delete the workspace
323
+ await deleteLocalWorkspace(tokens.user_token, workspace.id);
324
+
325
+ // Verify it was deleted
326
+ const { workspaces: afterDelete } = await listLocalWorkspaces(tokens.admin_token);
327
+ const stillExists = afterDelete.find((ws) => ws.name === workspaceName);
328
+ if (stillExists) {
329
+ throw new LocalApiError(
330
+ `Workspace '${workspaceName}' was not deleted properly. Please try again.`
331
+ );
332
+ }
333
+
334
+ // Recreate the workspace
335
+ await createLocalWorkspace(tokens.user_token, workspaceName, organizationId);
336
+
337
+ // Fetch the workspace again to get the token
338
+ const { workspaces: afterCreate } = await listLocalWorkspaces(tokens.admin_token);
339
+ const newWorkspace = afterCreate.find((ws) => ws.name === workspaceName);
340
+
341
+ if (!newWorkspace) {
342
+ throw new LocalApiError(
343
+ `Workspace '${workspaceName}' was not recreated properly. Please try again.`
344
+ );
345
+ }
346
+
347
+ return newWorkspace;
348
+ }
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Clear command - clears a local workspace or branch by deleting and recreating it
3
+ */
4
+
5
+ import { loadConfig, type ResolvedConfig, type DevMode } from "../config.js";
6
+ import {
7
+ getLocalTokens,
8
+ clearLocalWorkspace,
9
+ getLocalWorkspaceName,
10
+ LocalNotRunningError,
11
+ LocalApiError,
12
+ } from "../../api/local.js";
13
+ import {
14
+ clearBranch,
15
+ BranchApiError,
16
+ } from "../../api/branches.js";
17
+ import {
18
+ setBranchToken,
19
+ removeBranch as removeCachedBranch,
20
+ } from "../branch-store.js";
21
+ import { getWorkspace } from "../../api/workspaces.js";
22
+
23
+ /**
24
+ * Clear command options
25
+ */
26
+ export interface ClearCommandOptions {
27
+ /** Working directory (defaults to cwd) */
28
+ cwd?: string;
29
+ /** Override the dev mode from config */
30
+ devModeOverride?: DevMode;
31
+ }
32
+
33
+ /**
34
+ * Result of clearing a workspace or branch
35
+ */
36
+ export interface ClearResult {
37
+ /** Whether the operation was successful */
38
+ success: boolean;
39
+ /** Name of the cleared workspace or branch */
40
+ name?: string;
41
+ /** Whether local mode was used */
42
+ isLocal?: boolean;
43
+ /** Error message if failed */
44
+ error?: string;
45
+ }
46
+
47
+ /**
48
+ * Clear a local workspace or branch by deleting and recreating it
49
+ *
50
+ * In local mode: deletes and recreates the local workspace
51
+ * In branch mode: deletes and recreates the Tinybird branch
52
+ *
53
+ * @param options - Command options
54
+ * @returns Clear result
55
+ */
56
+ export async function runClear(
57
+ options: ClearCommandOptions = {}
58
+ ): Promise<ClearResult> {
59
+ const cwd = options.cwd ?? process.cwd();
60
+
61
+ let config: ResolvedConfig;
62
+ try {
63
+ config = loadConfig(cwd);
64
+ } catch (error) {
65
+ return {
66
+ success: false,
67
+ error: (error as Error).message,
68
+ };
69
+ }
70
+
71
+ // Determine dev mode
72
+ const devMode = options.devModeOverride ?? config.devMode;
73
+
74
+ if (devMode === "local") {
75
+ return clearLocal(config);
76
+ } else {
77
+ return clearCloudBranch(config);
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Clear a local workspace
83
+ */
84
+ async function clearLocal(config: ResolvedConfig): Promise<ClearResult> {
85
+ // Get workspace name from git branch or path hash
86
+ const workspaceName = getLocalWorkspaceName(config.tinybirdBranch, config.cwd);
87
+
88
+ try {
89
+ // Get local tokens
90
+ const tokens = await getLocalTokens();
91
+
92
+ // Clear the workspace
93
+ await clearLocalWorkspace(tokens, workspaceName);
94
+
95
+ return {
96
+ success: true,
97
+ name: workspaceName,
98
+ isLocal: true,
99
+ };
100
+ } catch (error) {
101
+ if (error instanceof LocalNotRunningError) {
102
+ return {
103
+ success: false,
104
+ error: error.message,
105
+ };
106
+ }
107
+
108
+ if (error instanceof LocalApiError) {
109
+ return {
110
+ success: false,
111
+ error: error.message,
112
+ };
113
+ }
114
+
115
+ return {
116
+ success: false,
117
+ error: (error as Error).message,
118
+ };
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Clear a cloud branch
124
+ */
125
+ async function clearCloudBranch(config: ResolvedConfig): Promise<ClearResult> {
126
+ // Must be on a non-main branch to clear
127
+ if (config.isMainBranch) {
128
+ return {
129
+ success: false,
130
+ error: "Cannot clear the main branch. Use 'tinybird deploy' to manage the main workspace.",
131
+ };
132
+ }
133
+
134
+ const branchName = config.tinybirdBranch;
135
+ if (!branchName) {
136
+ return {
137
+ success: false,
138
+ error: "Could not detect git branch. Make sure you are in a git repository.",
139
+ };
140
+ }
141
+
142
+ try {
143
+ // Get workspace ID for cache management
144
+ const workspace = await getWorkspace({
145
+ baseUrl: config.baseUrl,
146
+ token: config.token,
147
+ });
148
+
149
+ // Clear the branch (delete and recreate)
150
+ const newBranch = await clearBranch(
151
+ {
152
+ baseUrl: config.baseUrl,
153
+ token: config.token,
154
+ },
155
+ branchName
156
+ );
157
+
158
+ // Update the cached token with the new branch token
159
+ if (newBranch.token) {
160
+ setBranchToken(workspace.id, branchName, {
161
+ token: newBranch.token,
162
+ id: newBranch.id,
163
+ createdAt: newBranch.created_at,
164
+ });
165
+ } else {
166
+ // If no token in response, remove cached token
167
+ removeCachedBranch(workspace.id, branchName);
168
+ }
169
+
170
+ return {
171
+ success: true,
172
+ name: branchName,
173
+ isLocal: false,
174
+ };
175
+ } catch (error) {
176
+ if (error instanceof BranchApiError) {
177
+ if (error.status === 404) {
178
+ return {
179
+ success: false,
180
+ error: `Branch '${branchName}' does not exist. Run 'npx tinybird dev' to create it first.`,
181
+ };
182
+ }
183
+ return {
184
+ success: false,
185
+ error: error.message,
186
+ };
187
+ }
188
+
189
+ return {
190
+ success: false,
191
+ error: (error as Error).message,
192
+ };
193
+ }
194
+ }
package/src/cli/index.ts CHANGED
@@ -24,6 +24,7 @@ import {
24
24
  runBranchStatus,
25
25
  runBranchDelete,
26
26
  } from "./commands/branch.js";
27
+ import { runClear } from "./commands/clear.js";
27
28
  import {
28
29
  detectPackageManagerInstallCmd,
29
30
  detectPackageManagerRunCmd,
@@ -532,6 +533,61 @@ function createCli(): Command {
532
533
 
533
534
  program.addCommand(branchCommand);
534
535
 
536
+ // Clear command
537
+ program
538
+ .command("clear")
539
+ .description("Clear the workspace or branch by deleting and recreating it")
540
+ .option("-y, --yes", "Skip confirmation prompt")
541
+ .option("--local", "Use local Tinybird container")
542
+ .option("--branch", "Use Tinybird cloud with branches")
543
+ .action(async (options) => {
544
+ // Determine devMode override
545
+ let devModeOverride: DevMode | undefined;
546
+ if (options.local) {
547
+ devModeOverride = "local";
548
+ } else if (options.branch) {
549
+ devModeOverride = "branch";
550
+ }
551
+
552
+ const modeLabel = devModeOverride === "local" ? "local workspace" : "branch";
553
+
554
+ // Confirmation prompt unless --yes is passed
555
+ if (!options.yes) {
556
+ const readline = await import("readline");
557
+ const rl = readline.createInterface({
558
+ input: process.stdin,
559
+ output: process.stdout,
560
+ });
561
+
562
+ const answer = await new Promise<string>((resolve) => {
563
+ rl.question(
564
+ `Are you sure you want to clear the ${modeLabel}? This will delete all resources. [y/N]: `,
565
+ (ans) => {
566
+ rl.close();
567
+ resolve(ans);
568
+ }
569
+ );
570
+ });
571
+
572
+ if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") {
573
+ console.log("Aborted.");
574
+ return;
575
+ }
576
+ }
577
+
578
+ console.log(`Clearing ${modeLabel}...`);
579
+
580
+ const result = await runClear({ devModeOverride });
581
+
582
+ if (!result.success) {
583
+ console.error(`Error: ${result.error}`);
584
+ process.exit(1);
585
+ }
586
+
587
+ const typeLabel = result.isLocal ? "Workspace" : "Branch";
588
+ console.log(`${typeLabel} '${result.name}' cleared successfully.`);
589
+ });
590
+
535
591
  return program;
536
592
  }
537
593