@tinybirdco/sdk 0.0.7 → 0.0.9

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 (56) hide show
  1. package/dist/api/resources.d.ts +2 -0
  2. package/dist/api/resources.d.ts.map +1 -1
  3. package/dist/api/resources.js +1 -0
  4. package/dist/api/resources.js.map +1 -1
  5. package/dist/cli/commands/init.d.ts.map +1 -1
  6. package/dist/cli/commands/init.js +301 -205
  7. package/dist/cli/commands/init.js.map +1 -1
  8. package/dist/cli/commands/init.test.js +67 -93
  9. package/dist/cli/commands/init.test.js.map +1 -1
  10. package/dist/cli/config.d.ts +3 -7
  11. package/dist/cli/config.d.ts.map +1 -1
  12. package/dist/cli/config.js +9 -20
  13. package/dist/cli/config.js.map +1 -1
  14. package/dist/cli/config.test.js +11 -29
  15. package/dist/cli/config.test.js.map +1 -1
  16. package/dist/cli/git.d.ts +5 -0
  17. package/dist/cli/git.d.ts.map +1 -1
  18. package/dist/cli/git.js +15 -0
  19. package/dist/cli/git.js.map +1 -1
  20. package/dist/cli/index.js +42 -25
  21. package/dist/cli/index.js.map +1 -1
  22. package/dist/cli/utils/package-manager.d.ts +9 -0
  23. package/dist/cli/utils/package-manager.d.ts.map +1 -1
  24. package/dist/cli/utils/package-manager.js +130 -35
  25. package/dist/cli/utils/package-manager.js.map +1 -1
  26. package/dist/cli/utils/package-manager.test.js +124 -32
  27. package/dist/cli/utils/package-manager.test.js.map +1 -1
  28. package/dist/codegen/index.d.ts +4 -0
  29. package/dist/codegen/index.d.ts.map +1 -1
  30. package/dist/codegen/index.js +96 -0
  31. package/dist/codegen/index.js.map +1 -1
  32. package/dist/codegen/index.test.js +10 -0
  33. package/dist/codegen/index.test.js.map +1 -1
  34. package/dist/generator/datasource.d.ts.map +1 -1
  35. package/dist/generator/datasource.js +20 -0
  36. package/dist/generator/datasource.js.map +1 -1
  37. package/dist/generator/datasource.test.js +11 -0
  38. package/dist/generator/datasource.test.js.map +1 -1
  39. package/dist/schema/datasource.d.ts +5 -0
  40. package/dist/schema/datasource.d.ts.map +1 -1
  41. package/dist/schema/datasource.js.map +1 -1
  42. package/package.json +1 -1
  43. package/src/api/resources.ts +4 -0
  44. package/src/cli/commands/init.test.ts +67 -107
  45. package/src/cli/commands/init.ts +350 -218
  46. package/src/cli/config.test.ts +12 -42
  47. package/src/cli/config.ts +9 -23
  48. package/src/cli/git.ts +15 -0
  49. package/src/cli/index.ts +46 -27
  50. package/src/cli/utils/package-manager.test.ts +165 -33
  51. package/src/cli/utils/package-manager.ts +133 -30
  52. package/src/codegen/index.test.ts +12 -0
  53. package/src/codegen/index.ts +120 -0
  54. package/src/generator/datasource.test.ts +13 -0
  55. package/src/generator/datasource.ts +24 -0
  56. package/src/schema/datasource.ts +5 -0
@@ -11,15 +11,40 @@ import {
11
11
  getRelativeTinybirdDir,
12
12
  getConfigPath,
13
13
  updateConfig,
14
+ loadConfig,
14
15
  type DevMode,
15
16
  } from "../config.js";
16
17
  import { browserLogin } from "../auth.js";
17
18
  import { saveTinybirdToken } from "../env.js";
19
+ import { getGitRoot } from "../git.js";
20
+ import { fetchAllResources } from "../../api/resources.js";
21
+ import { generateCombinedFile } from "../../codegen/index.js";
18
22
 
19
23
  /**
20
- * Default starter content for datasources.ts
24
+ * Default starter content for tinybird.ts (single file with everything)
21
25
  */
22
- const DATASOURCES_CONTENT = `import { defineDatasource, t, engine, type InferRow } from "@tinybirdco/sdk";
26
+ const TINYBIRD_CONTENT = `/**
27
+ * Tinybird Definitions
28
+ *
29
+ * Define your datasources, endpoints, and client here.
30
+ */
31
+
32
+ import {
33
+ defineDatasource,
34
+ defineEndpoint,
35
+ createTinybirdClient,
36
+ node,
37
+ t,
38
+ p,
39
+ engine,
40
+ type InferRow,
41
+ type InferParams,
42
+ type InferOutputRow,
43
+ } from "@tinybirdco/sdk";
44
+
45
+ // ============================================================================
46
+ // Datasources
47
+ // ============================================================================
23
48
 
24
49
  /**
25
50
  * Page views datasource - tracks page view events
@@ -37,14 +62,11 @@ export const pageViews = defineDatasource("page_views", {
37
62
  }),
38
63
  });
39
64
 
40
- // Row type - use this for ingesting data
41
65
  export type PageViewsRow = InferRow<typeof pageViews>;
42
- `;
43
66
 
44
- /**
45
- * Default starter content for endpoints.ts
46
- */
47
- const ENDPOINTS_CONTENT = `import { defineEndpoint, node, t, p, type InferParams, type InferOutputRow } from "@tinybirdco/sdk";
67
+ // ============================================================================
68
+ // Endpoints
69
+ // ============================================================================
48
70
 
49
71
  /**
50
72
  * Top pages endpoint - get the most visited pages
@@ -78,40 +100,17 @@ export const topPages = defineEndpoint("top_pages", {
78
100
  },
79
101
  });
80
102
 
81
- // Endpoint types - use these for calling the API
82
103
  export type TopPagesParams = InferParams<typeof topPages>;
83
104
  export type TopPagesOutput = InferOutputRow<typeof topPages>;
84
- `;
85
-
86
- /**
87
- * Default starter content for client.ts
88
- */
89
- const CLIENT_CONTENT = `/**
90
- * Tinybird Client
91
- *
92
- * This file defines the typed Tinybird client for your project.
93
- * Add your datasources and endpoints here as you create them.
94
- */
95
105
 
96
- import { createTinybirdClient } from "@tinybirdco/sdk";
106
+ // ============================================================================
107
+ // Client
108
+ // ============================================================================
97
109
 
98
- // Import datasources and their row types
99
- import { pageViews, type PageViewsRow } from "./datasources";
100
-
101
- // Import endpoints and their types
102
- import { topPages, type TopPagesParams, type TopPagesOutput } from "./endpoints";
103
-
104
- // Create the typed Tinybird client
105
110
  export const tinybird = createTinybirdClient({
106
111
  datasources: { pageViews },
107
112
  pipes: { topPages },
108
113
  });
109
-
110
- // Re-export types for convenience
111
- export type { PageViewsRow, TopPagesParams, TopPagesOutput };
112
-
113
- // Re-export entities
114
- export { pageViews, topPages };
115
114
  `;
116
115
 
117
116
  const TINYBIRD_CI_WORKFLOW = `name: Tinybird CI
@@ -136,6 +135,7 @@ jobs:
136
135
  node-version: "20"
137
136
  cache: "pnpm"
138
137
  - run: pnpm install --frozen-lockfile
138
+ - run: pnpm run tinybird:build
139
139
  - run: pnpm run tinybird:deploy -- --check
140
140
  env:
141
141
  TINYBIRD_TOKEN: \${{ secrets.TINYBIRD_TOKEN }}
@@ -165,26 +165,66 @@ jobs:
165
165
  node-version: "20"
166
166
  cache: "pnpm"
167
167
  - run: pnpm install --frozen-lockfile
168
+ - run: pnpm run tinybird:build
168
169
  - run: pnpm run tinybird:deploy
169
170
  env:
170
171
  TINYBIRD_TOKEN: \${{ secrets.TINYBIRD_TOKEN }}
171
172
  `;
172
173
 
173
- const TINYBIRD_GITLAB_CI = `stages:\n - tinybird\n\ntinybird_ci:\n stage: tinybird\n image: node:20\n rules:\n - changes:\n - tinybird.json\n - src/tinybird/**\n - tinybird/**\n - **/*.datasource\n - **/*.pipe\n script:\n - corepack enable\n - pnpm install --frozen-lockfile\n - pnpm run tinybird:deploy -- --check\n variables:\n TINYBIRD_TOKEN: \${TINYBIRD_TOKEN}\n\ntinybird_cd:\n stage: tinybird\n image: node:20\n rules:\n - if: '$CI_COMMIT_BRANCH == \"main\"'\n changes:\n - tinybird.json\n - src/tinybird/**\n - tinybird/**\n - **/*.datasource\n - **/*.pipe\n script:\n - corepack enable\n - pnpm install --frozen-lockfile\n - pnpm run tinybird:deploy\n variables:\n TINYBIRD_TOKEN: \${TINYBIRD_TOKEN}\n`;
174
+ const TINYBIRD_GITLAB_CI_WORKFLOW = `stages:
175
+ - tinybird
176
+
177
+ tinybird_ci:
178
+ stage: tinybird
179
+ image: node:20
180
+ rules:
181
+ - changes:
182
+ - tinybird.json
183
+ - src/tinybird/**
184
+ - tinybird/**
185
+ - "**/*.datasource"
186
+ - "**/*.pipe"
187
+ script:
188
+ - corepack enable
189
+ - pnpm install --frozen-lockfile
190
+ - pnpm run tinybird:build
191
+ - pnpm run tinybird:deploy -- --check
192
+ variables:
193
+ TINYBIRD_TOKEN: \${TINYBIRD_TOKEN}
194
+ `;
195
+
196
+ const TINYBIRD_GITLAB_CD_WORKFLOW = `stages:
197
+ - tinybird
198
+
199
+ tinybird_cd:
200
+ stage: tinybird
201
+ image: node:20
202
+ rules:
203
+ - if: '$CI_COMMIT_BRANCH == "main"'
204
+ changes:
205
+ - tinybird.json
206
+ - src/tinybird/**
207
+ - tinybird/**
208
+ - "**/*.datasource"
209
+ - "**/*.pipe"
210
+ script:
211
+ - corepack enable
212
+ - pnpm install --frozen-lockfile
213
+ - pnpm run tinybird:build
214
+ - pnpm run tinybird:deploy
215
+ variables:
216
+ TINYBIRD_TOKEN: \${TINYBIRD_TOKEN}
217
+ `;
174
218
 
175
219
  /**
176
220
  * Default config content generator
177
221
  */
178
222
  function createDefaultConfig(
179
- tinybirdDir: string,
223
+ tinybirdFilePath: string,
180
224
  devMode: DevMode,
181
225
  additionalIncludes: string[] = []
182
226
  ) {
183
- const include = [
184
- `${tinybirdDir}/datasources.ts`,
185
- `${tinybirdDir}/endpoints.ts`,
186
- ...additionalIncludes,
187
- ];
227
+ const include = [tinybirdFilePath, ...additionalIncludes];
188
228
  return {
189
229
  include,
190
230
  token: "${TINYBIRD_TOKEN}",
@@ -338,7 +378,7 @@ export async function runInit(options: InitOptions = {}): Promise<InitResult> {
338
378
 
339
379
  if (!options.devMode) {
340
380
  // Show interactive prompt for workflow selection
341
- p.intro(pc.cyan("tinybird init"));
381
+ p.intro(pc.cyan("tinybird.json"));
342
382
 
343
383
  const workflowChoice = await p.select({
344
384
  message: "How do you want to develop with Tinybird?",
@@ -377,7 +417,7 @@ export async function runInit(options: InitOptions = {}): Promise<InitResult> {
377
417
  if (!options.clientPath && !options.devMode) {
378
418
  // Ask user to confirm or change the client path
379
419
  const clientPathChoice = await p.text({
380
- message: "Where should we create Tinybird definitions?",
420
+ message: "Where should we create initial tinybird.ts file?",
381
421
  placeholder: defaultRelativePath,
382
422
  defaultValue: defaultRelativePath,
383
423
  });
@@ -403,62 +443,27 @@ export async function runInit(options: InitOptions = {}): Promise<InitResult> {
403
443
  let includeCdWorkflow = options.includeCdWorkflow ?? false;
404
444
  const shouldPromptWorkflows =
405
445
  !skipWorkflowPrompt && options.includeCiWorkflow === undefined;
406
- const shouldPromptProvider = !skipWorkflowPrompt && !workflowProvider;
407
-
408
- if (shouldPromptProvider && shouldPromptWorkflows) {
409
- const providerChoice = await p.select({
410
- message: "Which git provider are you using?",
411
- options: [
412
- { value: "github", label: "GitHub" },
413
- { value: "gitlab", label: "GitLab" },
414
- ],
415
- });
416
-
417
- if (p.isCancel(providerChoice)) {
418
- p.cancel("Operation cancelled");
419
- return {
420
- success: false,
421
- created: [],
422
- skipped: [],
423
- error: "Cancelled by user",
424
- };
425
- }
426
-
427
- didPrompt = true;
428
- workflowProvider = providerChoice as "github" | "gitlab";
429
- }
430
446
 
431
447
  if (shouldPromptWorkflows) {
432
- const confirmWorkflows = await p.confirm({
433
- message: "Create CI/CD workflows for Tinybird?",
434
- initialValue: true,
435
- });
436
-
437
- if (p.isCancel(confirmWorkflows)) {
438
- p.cancel("Operation cancelled");
439
- return {
440
- success: false,
441
- created: [],
442
- skipped: [],
443
- error: "Cancelled by user",
444
- };
445
- }
446
-
447
- didPrompt = true;
448
- includeCiWorkflow = confirmWorkflows;
449
- includeCdWorkflow = confirmWorkflows;
450
- }
451
-
452
- if (shouldPromptProvider && !shouldPromptWorkflows && !workflowProvider) {
453
- const providerChoice = await p.select({
454
- message: "Which git provider are you using?",
448
+ const ciChoice = await p.select({
449
+ message: "Set up CI/CD workflows?",
455
450
  options: [
456
- { value: "github", label: "GitHub" },
457
- { value: "gitlab", label: "GitLab" },
451
+ {
452
+ value: "github",
453
+ label: "GitHub Actions",
454
+ },
455
+ {
456
+ value: "gitlab",
457
+ label: "GitLab CI",
458
+ },
459
+ {
460
+ value: "skip",
461
+ label: "Skip",
462
+ },
458
463
  ],
459
464
  });
460
465
 
461
- if (p.isCancel(providerChoice)) {
466
+ if (p.isCancel(ciChoice)) {
462
467
  p.cancel("Operation cancelled");
463
468
  return {
464
469
  success: false,
@@ -469,49 +474,56 @@ export async function runInit(options: InitOptions = {}): Promise<InitResult> {
469
474
  }
470
475
 
471
476
  didPrompt = true;
472
- workflowProvider = providerChoice as "github" | "gitlab";
473
- }
474
- if ((includeCiWorkflow || includeCdWorkflow) && !workflowProvider) {
477
+ if (ciChoice !== "skip") {
478
+ includeCiWorkflow = true;
479
+ includeCdWorkflow = true;
480
+ workflowProvider = ciChoice as "github" | "gitlab";
481
+ }
482
+ } else if ((includeCiWorkflow || includeCdWorkflow) && !workflowProvider) {
475
483
  workflowProvider = "github";
476
484
  }
477
485
 
478
486
  // Ask about existing datafiles if found
487
+ let datafileAction: DatafileAction = "skip";
479
488
  if (foundDatafiles.length > 0 && !options.skipDatafilePrompt) {
480
- const includeDatafiles =
481
- options.includeExistingDatafiles ??
482
- (await (async () => {
483
- didPrompt = true;
484
- return promptForExistingDatafiles(foundDatafiles);
485
- })());
486
-
487
- if (includeDatafiles) {
489
+ if (options.includeExistingDatafiles !== undefined) {
490
+ datafileAction = options.includeExistingDatafiles ? "include" : "skip";
491
+ } else {
492
+ didPrompt = true;
493
+ datafileAction = await promptForExistingDatafiles(foundDatafiles);
494
+ }
495
+
496
+ if (datafileAction === "include") {
488
497
  existingDatafiles = foundDatafiles;
489
498
  }
499
+ // Note: "codegen" option is handled after file creation
490
500
  } else if (options.includeExistingDatafiles && foundDatafiles.length > 0) {
491
501
  existingDatafiles = foundDatafiles;
502
+ datafileAction = "include";
492
503
  }
493
504
 
494
505
  if (didPrompt) {
495
506
  const devModeLabel = devMode === "local" ? "Tinybird Local" : "Branches";
496
- const datafileSummary =
497
- foundDatafiles.length > 0
498
- ? existingDatafiles.length > 0
499
- ? `${existingDatafiles.length} included`
500
- : "none included"
501
- : "none found";
502
- let workflowSummary = "none";
507
+ let datafileSummary = "none found";
508
+ if (foundDatafiles.length > 0) {
509
+ if (datafileAction === "include") {
510
+ datafileSummary = `${foundDatafiles.length} included`;
511
+ } else if (datafileAction === "codegen") {
512
+ datafileSummary = `${foundDatafiles.length} will generate .ts`;
513
+ } else {
514
+ datafileSummary = "skipped";
515
+ }
516
+ }
517
+ let cicdSummary = "skipped";
503
518
  if (includeCiWorkflow || includeCdWorkflow) {
504
- workflowSummary =
505
- workflowProvider === "gitlab"
506
- ? "GitLab (.gitlab-ci.yml)"
507
- : "GitHub Actions (tinybird-ci.yaml, tinybird-cd.yaml)";
519
+ cicdSummary = workflowProvider === "gitlab" ? "GitLab" : "GitHub";
508
520
  }
509
521
 
510
522
  const summaryLines = [
511
523
  `Mode: ${devModeLabel}`,
512
- `Definitions: ${relativeTinybirdDir}/`,
524
+ `Folder: ${relativeTinybirdDir}/`,
513
525
  `Existing datafiles: ${datafileSummary}`,
514
- `Workflows: ${workflowSummary}`,
526
+ `CI/CD: ${cicdSummary}`,
515
527
  ];
516
528
 
517
529
  p.note(summaryLines.join("\n"), "Installation Summary");
@@ -532,17 +544,32 @@ export async function runInit(options: InitOptions = {}): Promise<InitResult> {
532
544
  }
533
545
  }
534
546
 
535
- const tinybirdDir = path.join(cwd, relativeTinybirdDir);
536
-
537
- // File paths
538
- const datasourcesPath = path.join(tinybirdDir, "datasources.ts");
539
- const endpointsPath = path.join(tinybirdDir, "endpoints.ts");
540
- const clientPath = path.join(tinybirdDir, "client.ts");
547
+ // relativeTinybirdDir is now a file path like "src/lib/tinybird.ts"
548
+ const tinybirdFilePath = path.join(cwd, relativeTinybirdDir);
549
+ const tinybirdDir = path.dirname(tinybirdFilePath);
541
550
 
542
551
  // Create config file (tinybird.json)
543
552
  const configPath = getConfigPath(cwd);
544
553
  if (fs.existsSync(configPath) && !force) {
545
- skipped.push("tinybird.json");
554
+ try {
555
+ const config = createDefaultConfig(
556
+ relativeTinybirdDir,
557
+ devMode,
558
+ existingDatafiles
559
+ );
560
+ updateConfig(configPath, {
561
+ include: config.include,
562
+ devMode: config.devMode,
563
+ });
564
+ created.push("tinybird.json (updated)");
565
+ } catch (error) {
566
+ return {
567
+ success: false,
568
+ created,
569
+ skipped,
570
+ error: `Failed to update tinybird.json: ${(error as Error).message}`,
571
+ };
572
+ }
546
573
  } else {
547
574
  try {
548
575
  const config = createDefaultConfig(
@@ -562,7 +589,7 @@ export async function runInit(options: InitOptions = {}): Promise<InitResult> {
562
589
  }
563
590
  }
564
591
 
565
- // Create tinybird directory
592
+ // Create lib directory
566
593
  try {
567
594
  fs.mkdirSync(tinybirdDir, { recursive: true });
568
595
  } catch (error) {
@@ -570,60 +597,30 @@ export async function runInit(options: InitOptions = {}): Promise<InitResult> {
570
597
  success: false,
571
598
  created,
572
599
  skipped,
573
- error: `Failed to create ${relativeTinybirdDir} folder: ${
600
+ error: `Failed to create ${path.dirname(relativeTinybirdDir)} folder: ${
574
601
  (error as Error).message
575
602
  }`,
576
603
  };
577
604
  }
578
605
 
579
- // Create datasources.ts
580
- if (fs.existsSync(datasourcesPath) && !force) {
581
- skipped.push(`${relativeTinybirdDir}/datasources.ts`);
582
- } else {
583
- try {
584
- fs.writeFileSync(datasourcesPath, DATASOURCES_CONTENT);
585
- created.push(`${relativeTinybirdDir}/datasources.ts`);
586
- } catch (error) {
587
- return {
588
- success: false,
589
- created,
590
- skipped,
591
- error: `Failed to create datasources.ts: ${(error as Error).message}`,
592
- };
593
- }
594
- }
595
-
596
- // Create endpoints.ts
597
- if (fs.existsSync(endpointsPath) && !force) {
598
- skipped.push(`${relativeTinybirdDir}/endpoints.ts`);
599
- } else {
600
- try {
601
- fs.writeFileSync(endpointsPath, ENDPOINTS_CONTENT);
602
- created.push(`${relativeTinybirdDir}/endpoints.ts`);
603
- } catch (error) {
604
- return {
605
- success: false,
606
- created,
607
- skipped,
608
- error: `Failed to create endpoints.ts: ${(error as Error).message}`,
609
- };
610
- }
611
- }
612
-
613
- // Create client.ts
614
- if (fs.existsSync(clientPath) && !force) {
615
- skipped.push(`${relativeTinybirdDir}/client.ts`);
616
- } else {
617
- try {
618
- fs.writeFileSync(clientPath, CLIENT_CONTENT);
619
- created.push(`${relativeTinybirdDir}/client.ts`);
620
- } catch (error) {
621
- return {
622
- success: false,
623
- created,
624
- skipped,
625
- error: `Failed to create client.ts: ${(error as Error).message}`,
626
- };
606
+ // Create tinybird.ts (skip if codegen will generate it)
607
+ if (datafileAction !== "codegen") {
608
+ if (fs.existsSync(tinybirdFilePath) && !force) {
609
+ skipped.push(relativeTinybirdDir);
610
+ } else {
611
+ try {
612
+ fs.writeFileSync(tinybirdFilePath, TINYBIRD_CONTENT);
613
+ created.push(relativeTinybirdDir);
614
+ } catch (error) {
615
+ return {
616
+ success: false,
617
+ created,
618
+ skipped,
619
+ error: `Failed to create ${relativeTinybirdDir}: ${
620
+ (error as Error).message
621
+ }`,
622
+ };
623
+ }
627
624
  }
628
625
  }
629
626
 
@@ -665,42 +662,68 @@ export async function runInit(options: InitOptions = {}): Promise<InitResult> {
665
662
  }
666
663
  }
667
664
 
668
- const workflowsDir = path.join(cwd, ".github", "workflows");
669
- const ciWorkflowPath = path.join(workflowsDir, "tinybird-ci.yaml");
670
- const cdWorkflowPath = path.join(workflowsDir, "tinybird-cd.yaml");
671
- const gitlabWorkflowPath = path.join(cwd, ".gitlab-ci.yml");
665
+ // Use git root for workflow files, fallback to cwd if not in a git repo
666
+ const projectRoot = getGitRoot() ?? cwd;
667
+ const githubWorkflowsDir = path.join(projectRoot, ".github", "workflows");
668
+ const gitlabWorkflowsDir = path.join(projectRoot, ".gitlab");
669
+ const githubCiPath = path.join(githubWorkflowsDir, "tinybird-ci.yaml");
670
+ const githubCdPath = path.join(githubWorkflowsDir, "tinybird-cd.yaml");
671
+ const gitlabCiPath = path.join(gitlabWorkflowsDir, "tinybird-ci.yaml");
672
+ const gitlabCdPath = path.join(gitlabWorkflowsDir, "tinybird-cd.yaml");
672
673
 
673
674
  if (includeCiWorkflow || includeCdWorkflow) {
674
- if (workflowProvider === "github") {
675
- try {
676
- fs.mkdirSync(workflowsDir, { recursive: true });
677
- } catch (error) {
678
- return {
679
- success: false,
680
- created,
681
- skipped,
682
- error: `Failed to create .github/workflows folder: ${
683
- (error as Error).message
684
- }`,
685
- };
675
+ const workflowsDir =
676
+ workflowProvider === "github" ? githubWorkflowsDir : gitlabWorkflowsDir;
677
+ try {
678
+ fs.mkdirSync(workflowsDir, { recursive: true });
679
+ } catch (error) {
680
+ return {
681
+ success: false,
682
+ created,
683
+ skipped,
684
+ error: `Failed to create ${
685
+ workflowProvider === "github" ? ".github/workflows" : ".gitlab"
686
+ } folder: ${(error as Error).message}`,
687
+ };
688
+ }
689
+ }
690
+
691
+ if (workflowProvider === "github") {
692
+ if (includeCiWorkflow) {
693
+ if (fs.existsSync(githubCiPath) && !force) {
694
+ skipped.push(".github/workflows/tinybird-ci.yaml");
695
+ } else {
696
+ try {
697
+ fs.writeFileSync(githubCiPath, TINYBIRD_CI_WORKFLOW);
698
+ created.push(".github/workflows/tinybird-ci.yaml");
699
+ ciWorkflowCreated = true;
700
+ } catch (error) {
701
+ return {
702
+ success: false,
703
+ created,
704
+ skipped,
705
+ error: `Failed to create .github/workflows/tinybird-ci.yaml: ${
706
+ (error as Error).message
707
+ }`,
708
+ };
709
+ }
686
710
  }
687
711
  }
688
712
 
689
- if (workflowProvider === "gitlab") {
690
- if (fs.existsSync(gitlabWorkflowPath) && !force) {
691
- skipped.push(".gitlab-ci.yml");
713
+ if (includeCdWorkflow) {
714
+ if (fs.existsSync(githubCdPath) && !force) {
715
+ skipped.push(".github/workflows/tinybird-cd.yaml");
692
716
  } else {
693
717
  try {
694
- fs.writeFileSync(gitlabWorkflowPath, TINYBIRD_GITLAB_CI);
695
- created.push(".gitlab-ci.yml");
696
- ciWorkflowCreated = includeCiWorkflow;
697
- cdWorkflowCreated = includeCdWorkflow;
718
+ fs.writeFileSync(githubCdPath, TINYBIRD_CD_WORKFLOW);
719
+ created.push(".github/workflows/tinybird-cd.yaml");
720
+ cdWorkflowCreated = true;
698
721
  } catch (error) {
699
722
  return {
700
723
  success: false,
701
724
  created,
702
725
  skipped,
703
- error: `Failed to create .gitlab-ci.yml: ${
726
+ error: `Failed to create .github/workflows/tinybird-cd.yaml: ${
704
727
  (error as Error).message
705
728
  }`,
706
729
  };
@@ -709,21 +732,21 @@ export async function runInit(options: InitOptions = {}): Promise<InitResult> {
709
732
  }
710
733
  }
711
734
 
712
- if (workflowProvider === "github") {
735
+ if (workflowProvider === "gitlab") {
713
736
  if (includeCiWorkflow) {
714
- if (fs.existsSync(ciWorkflowPath) && !force) {
715
- skipped.push(".github/workflows/tinybird-ci.yaml");
737
+ if (fs.existsSync(gitlabCiPath) && !force) {
738
+ skipped.push(".gitlab/tinybird-ci.yaml");
716
739
  } else {
717
740
  try {
718
- fs.writeFileSync(ciWorkflowPath, TINYBIRD_CI_WORKFLOW);
719
- created.push(".github/workflows/tinybird-ci.yaml");
741
+ fs.writeFileSync(gitlabCiPath, TINYBIRD_GITLAB_CI_WORKFLOW);
742
+ created.push(".gitlab/tinybird-ci.yaml");
720
743
  ciWorkflowCreated = true;
721
744
  } catch (error) {
722
745
  return {
723
746
  success: false,
724
747
  created,
725
748
  skipped,
726
- error: `Failed to create tinybird-ci.yaml: ${
749
+ error: `Failed to create .gitlab/tinybird-ci.yaml: ${
727
750
  (error as Error).message
728
751
  }`,
729
752
  };
@@ -732,19 +755,19 @@ export async function runInit(options: InitOptions = {}): Promise<InitResult> {
732
755
  }
733
756
 
734
757
  if (includeCdWorkflow) {
735
- if (fs.existsSync(cdWorkflowPath) && !force) {
736
- skipped.push(".github/workflows/tinybird-cd.yaml");
758
+ if (fs.existsSync(gitlabCdPath) && !force) {
759
+ skipped.push(".gitlab/tinybird-cd.yaml");
737
760
  } else {
738
761
  try {
739
- fs.writeFileSync(cdWorkflowPath, TINYBIRD_CD_WORKFLOW);
740
- created.push(".github/workflows/tinybird-cd.yaml");
762
+ fs.writeFileSync(gitlabCdPath, TINYBIRD_GITLAB_CD_WORKFLOW);
763
+ created.push(".gitlab/tinybird-cd.yaml");
741
764
  cdWorkflowCreated = true;
742
765
  } catch (error) {
743
766
  return {
744
767
  success: false,
745
768
  created,
746
769
  skipped,
747
- error: `Failed to create tinybird-cd.yaml: ${
770
+ error: `Failed to create .gitlab/tinybird-cd.yaml: ${
748
771
  (error as Error).message
749
772
  }`,
750
773
  };
@@ -773,6 +796,19 @@ export async function runInit(options: InitOptions = {}): Promise<InitResult> {
773
796
  updateConfig(configPath, { baseUrl });
774
797
  }
775
798
 
799
+ // Generate TypeScript from existing Tinybird resources if requested
800
+ if (datafileAction === "codegen") {
801
+ const tinybirdDir = path.join(cwd, relativeTinybirdDir);
802
+ await runCodegen(
803
+ baseUrl,
804
+ authResult.token,
805
+ tinybirdDir,
806
+ relativeTinybirdDir,
807
+ created,
808
+ foundDatafiles
809
+ );
810
+ }
811
+
776
812
  return {
777
813
  success: true,
778
814
  created,
@@ -825,6 +861,31 @@ export async function runInit(options: InitOptions = {}): Promise<InitResult> {
825
861
  }
826
862
  }
827
863
 
864
+ // Generate TypeScript from existing Tinybird resources if requested
865
+ // (when user is already logged in)
866
+ if (datafileAction === "codegen" && hasValidToken(cwd)) {
867
+ try {
868
+ const config = loadConfig(cwd);
869
+ const tinybirdDir = path.join(cwd, relativeTinybirdDir);
870
+ await runCodegen(
871
+ config.baseUrl,
872
+ config.token,
873
+ tinybirdDir,
874
+ relativeTinybirdDir,
875
+ created,
876
+ foundDatafiles
877
+ );
878
+ } catch (error) {
879
+ console.error(
880
+ `Warning: Failed to generate TypeScript: ${(error as Error).message}`
881
+ );
882
+ }
883
+ }
884
+
885
+ if (didPrompt) {
886
+ p.outro("Done!");
887
+ }
888
+
828
889
  return {
829
890
  success: true,
830
891
  created,
@@ -839,12 +900,69 @@ export async function runInit(options: InitOptions = {}): Promise<InitResult> {
839
900
  };
840
901
  }
841
902
 
903
+ type DatafileAction = "include" | "codegen" | "skip";
904
+
905
+ /**
906
+ * Generate TypeScript files from Tinybird workspace resources
907
+ * Only generates for resources that match the local datafiles
908
+ */
909
+ async function runCodegen(
910
+ baseUrl: string,
911
+ token: string,
912
+ tinybirdFilePath: string,
913
+ relativeTinybirdPath: string,
914
+ created: string[],
915
+ localDatafiles: string[]
916
+ ): Promise<void> {
917
+ try {
918
+ // Extract names from local datafiles (without extension)
919
+ const localDatasourceNames = new Set(
920
+ localDatafiles
921
+ .filter((f) => f.endsWith(".datasource"))
922
+ .map((f) => path.basename(f, ".datasource"))
923
+ );
924
+ const localPipeNames = new Set(
925
+ localDatafiles
926
+ .filter((f) => f.endsWith(".pipe"))
927
+ .map((f) => path.basename(f, ".pipe"))
928
+ );
929
+
930
+ const resources = await fetchAllResources({ baseUrl, token });
931
+
932
+ // Filter to only resources matching local files
933
+ const matchedDatasources = resources.datasources.filter((ds) =>
934
+ localDatasourceNames.has(ds.name)
935
+ );
936
+ const matchedPipes = resources.pipes.filter((p) =>
937
+ localPipeNames.has(p.name)
938
+ );
939
+
940
+ if (matchedDatasources.length > 0 || matchedPipes.length > 0) {
941
+ // Generate combined tinybird.ts file
942
+ const content = generateCombinedFile(matchedDatasources, matchedPipes);
943
+ // Ensure directory exists
944
+ const dir = path.dirname(tinybirdFilePath);
945
+ if (!fs.existsSync(dir)) {
946
+ fs.mkdirSync(dir, { recursive: true });
947
+ }
948
+ fs.writeFileSync(tinybirdFilePath, content);
949
+ created.push(`${relativeTinybirdPath} (generated)`);
950
+ }
951
+ } catch (error) {
952
+ console.error(
953
+ `Warning: Failed to generate TypeScript from workspace: ${
954
+ (error as Error).message
955
+ }`
956
+ );
957
+ }
958
+ }
959
+
842
960
  /**
843
961
  * Prompt user about including existing datafiles
844
962
  */
845
963
  async function promptForExistingDatafiles(
846
964
  datafiles: string[]
847
- ): Promise<boolean> {
965
+ ): Promise<DatafileAction> {
848
966
  const datasourceCount = datafiles.filter((f) =>
849
967
  f.endsWith(".datasource")
850
968
  ).length;
@@ -860,16 +978,30 @@ async function promptForExistingDatafiles(
860
978
  parts.push(`${pipeCount} .pipe file${pipeCount > 1 ? "s" : ""}`);
861
979
  }
862
980
 
863
- const confirmInclude = await p.confirm({
864
- message: `Found ${parts.join(
865
- " and "
866
- )} in your project. Include them in tinybird.json?`,
867
- initialValue: true,
981
+ const choice = await p.select({
982
+ message: `Found ${parts.join(" and ")} in your project.`,
983
+ options: [
984
+ {
985
+ value: "include",
986
+ label: "Include existing resources",
987
+ hint: "Add to tinybird.json",
988
+ },
989
+ {
990
+ value: "codegen",
991
+ label: "Define resources in TypeScript",
992
+ hint: "Generate TypeScript definitions from existing resources",
993
+ },
994
+ {
995
+ value: "skip",
996
+ label: "Skip",
997
+ hint: "Don't include existing resources",
998
+ },
999
+ ],
868
1000
  });
869
1001
 
870
- if (p.isCancel(confirmInclude)) {
871
- return false;
1002
+ if (p.isCancel(choice)) {
1003
+ return "skip";
872
1004
  }
873
1005
 
874
- return confirmInclude;
1006
+ return choice as DatafileAction;
875
1007
  }