@tinybirdco/sdk 0.0.39 → 0.0.40

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 (36) hide show
  1. package/README.md +3 -2
  2. package/dist/cli/commands/dev.d.ts.map +1 -1
  3. package/dist/cli/commands/dev.js +4 -9
  4. package/dist/cli/commands/dev.js.map +1 -1
  5. package/dist/cli/commands/init.d.ts +12 -0
  6. package/dist/cli/commands/init.d.ts.map +1 -1
  7. package/dist/cli/commands/init.js +217 -2
  8. package/dist/cli/commands/init.js.map +1 -1
  9. package/dist/cli/commands/init.test.js +15 -0
  10. package/dist/cli/commands/init.test.js.map +1 -1
  11. package/dist/cli/config-types.d.ts +1 -1
  12. package/dist/cli/config-types.d.ts.map +1 -1
  13. package/dist/cli/config.d.ts +1 -1
  14. package/dist/cli/config.d.ts.map +1 -1
  15. package/dist/generator/include-paths.d.ts +7 -0
  16. package/dist/generator/include-paths.d.ts.map +1 -0
  17. package/dist/generator/include-paths.js +164 -0
  18. package/dist/generator/include-paths.js.map +1 -0
  19. package/dist/generator/index.d.ts +1 -1
  20. package/dist/generator/index.d.ts.map +1 -1
  21. package/dist/generator/index.test.js +36 -0
  22. package/dist/generator/index.test.js.map +1 -1
  23. package/dist/generator/loader.d.ts +1 -1
  24. package/dist/generator/loader.d.ts.map +1 -1
  25. package/dist/generator/loader.js +5 -8
  26. package/dist/generator/loader.js.map +1 -1
  27. package/package.json +1 -1
  28. package/src/cli/commands/dev.ts +4 -9
  29. package/src/cli/commands/init.test.ts +17 -0
  30. package/src/cli/commands/init.ts +300 -2
  31. package/src/cli/config-types.ts +1 -1
  32. package/src/cli/config.ts +1 -1
  33. package/src/generator/include-paths.ts +234 -0
  34. package/src/generator/index.test.ts +44 -0
  35. package/src/generator/index.ts +1 -1
  36. package/src/generator/loader.ts +6 -10
@@ -21,7 +21,7 @@ import { selectRegion } from "../region-selector.js";
21
21
  import { getGitRoot } from "../git.js";
22
22
  import { fetchAllResources } from "../../api/resources.js";
23
23
  import { generateCombinedFile } from "../../codegen/index.js";
24
- import { execSync } from "child_process";
24
+ import { execSync, spawn } from "child_process";
25
25
  import {
26
26
  detectPackageManager,
27
27
  getPackageManagerAddCmd,
@@ -286,6 +286,8 @@ function createDefaultConfig(
286
286
  };
287
287
  }
288
288
 
289
+ type InstallTool = "skills" | "syntax-highlighting";
290
+
289
291
  /**
290
292
  * Init command options
291
293
  */
@@ -314,6 +316,12 @@ export interface InitOptions {
314
316
  workflowProvider?: "github" | "gitlab";
315
317
  /** Skip auto-installing @tinybirdco/sdk dependency */
316
318
  skipDependencyInstall?: boolean;
319
+ /** Skip install tools prompt */
320
+ skipToolsPrompt?: boolean;
321
+ /** Install selected tools */
322
+ installTools?: InstallTool[];
323
+ /** Skip selected tool installation */
324
+ skipToolsInstall?: boolean;
317
325
  }
318
326
 
319
327
  /**
@@ -346,6 +354,10 @@ export interface InitResult {
346
354
  cdWorkflowCreated?: boolean;
347
355
  /** Git provider used for workflow templates */
348
356
  workflowProvider?: "github" | "gitlab";
357
+ /** Selected install tools */
358
+ installTools?: InstallTool[];
359
+ /** Installed tools */
360
+ installedTools?: string[];
349
361
  }
350
362
 
351
363
  /**
@@ -418,14 +430,18 @@ export async function runInit(options: InitOptions = {}): Promise<InitResult> {
418
430
  const skipLogin = options.skipLogin ?? false;
419
431
  const skipDependencyInstall =
420
432
  options.skipDependencyInstall ?? Boolean(process.env.VITEST);
433
+ const skipToolsInstall =
434
+ options.skipToolsInstall ?? Boolean(process.env.VITEST);
421
435
 
422
436
  const created: string[] = [];
423
437
  const skipped: string[] = [];
438
+ const installedTools: string[] = [];
424
439
  let didPrompt = false;
425
440
  let existingDatafiles: string[] = [];
426
441
  let ciWorkflowCreated = false;
427
442
  let cdWorkflowCreated = false;
428
443
  let workflowProvider = options.workflowProvider;
444
+ let installTools: InstallTool[] = options.installTools ?? [];
429
445
 
430
446
  // Check for existing .datasource and .pipe files
431
447
  const foundDatafiles = findExistingDatafiles(cwd);
@@ -540,6 +556,44 @@ export async function runInit(options: InitOptions = {}): Promise<InitResult> {
540
556
  workflowProvider = "github";
541
557
  }
542
558
 
559
+ const skipToolsPrompt =
560
+ options.skipToolsPrompt ??
561
+ (options.devMode !== undefined || options.clientPath !== undefined);
562
+ const shouldPromptTools =
563
+ !skipToolsPrompt && options.installTools === undefined;
564
+
565
+ if (shouldPromptTools) {
566
+ const toolsChoice = await p.multiselect({
567
+ message: "Install extra tools",
568
+ options: [
569
+ {
570
+ value: "skills",
571
+ label: "Agent skills",
572
+ hint: "Manual: npx skills add tinybirdco/tinybird-agent-skills --skill tinybird --skill tinybird-typescript-sdk-guidelines",
573
+ },
574
+ {
575
+ value: "syntax-highlighting",
576
+ label: "Syntax highlighting (Cursor/VS Code)",
577
+ hint: "Installs Tinybird SQL highlighting extension",
578
+ },
579
+ ],
580
+ required: false,
581
+ });
582
+
583
+ if (p.isCancel(toolsChoice)) {
584
+ p.cancel("Operation cancelled");
585
+ return {
586
+ success: false,
587
+ created: [],
588
+ skipped: [],
589
+ error: "Cancelled by user",
590
+ };
591
+ }
592
+
593
+ didPrompt = true;
594
+ installTools = toolsChoice as InstallTool[];
595
+ }
596
+
543
597
  // Ask about existing datafiles if found
544
598
  let datafileAction: DatafileAction = "skip";
545
599
  if (foundDatafiles.length > 0 && !options.skipDatafilePrompt) {
@@ -575,12 +629,17 @@ export async function runInit(options: InitOptions = {}): Promise<InitResult> {
575
629
  if (includeCiWorkflow || includeCdWorkflow) {
576
630
  cicdSummary = workflowProvider === "gitlab" ? "GitLab" : "GitHub";
577
631
  }
632
+ const toolsSummary =
633
+ installTools.length > 0
634
+ ? installTools.map(getInstallToolLabel).join(", ")
635
+ : "none selected";
578
636
 
579
637
  const summaryLines = [
580
638
  `Mode: ${devModeLabel}`,
581
639
  `Folder: ${relativeTinybirdDir}/`,
582
640
  `Existing datafiles: ${datafileSummary}`,
583
641
  `CI/CD: ${cicdSummary}`,
642
+ `Tools: ${toolsSummary}`,
584
643
  ];
585
644
 
586
645
  p.note(summaryLines.join("\n"), "Installation Summary");
@@ -748,7 +807,9 @@ export async function runInit(options: InitOptions = {}): Promise<InitResult> {
748
807
  const packageManager = detectPackageManager(cwd);
749
808
  const addCmd = getPackageManagerAddCmd(packageManager);
750
809
  try {
751
- execSync(`${addCmd} @tinybirdco/sdk`, { cwd, stdio: "pipe" });
810
+ await flushSpinnerRender();
811
+ const { command, args } = splitCommandPrefix(addCmd);
812
+ await runCommand(command, [...args, "@tinybirdco/sdk"], cwd);
752
813
  s.stop("Installed dependencies");
753
814
  created.push("@tinybirdco/sdk");
754
815
  } catch (error) {
@@ -761,6 +822,16 @@ export async function runInit(options: InitOptions = {}): Promise<InitResult> {
761
822
  }
762
823
  }
763
824
 
825
+ if (installTools.length > 0 && !skipToolsInstall) {
826
+ await installSelectedTools(
827
+ cwd,
828
+ installTools,
829
+ created,
830
+ skipped,
831
+ installedTools
832
+ );
833
+ }
834
+
764
835
  // Use git root for workflow files, fallback to cwd if not in a git repo
765
836
  const projectRoot = getGitRoot() ?? cwd;
766
837
  const githubWorkflowsDir = path.join(projectRoot, ".github", "workflows");
@@ -911,6 +982,8 @@ export async function runInit(options: InitOptions = {}): Promise<InitResult> {
911
982
  ciWorkflowCreated,
912
983
  cdWorkflowCreated,
913
984
  workflowProvider,
985
+ installTools: installTools.length > 0 ? installTools : undefined,
986
+ installedTools: installedTools.length > 0 ? installedTools : undefined,
914
987
  };
915
988
  }
916
989
 
@@ -958,6 +1031,9 @@ export async function runInit(options: InitOptions = {}): Promise<InitResult> {
958
1031
  ciWorkflowCreated,
959
1032
  cdWorkflowCreated,
960
1033
  workflowProvider,
1034
+ installTools: installTools.length > 0 ? installTools : undefined,
1035
+ installedTools:
1036
+ installedTools.length > 0 ? installedTools : undefined,
961
1037
  };
962
1038
  } catch (error) {
963
1039
  // Login succeeded but saving credentials failed
@@ -976,6 +1052,9 @@ export async function runInit(options: InitOptions = {}): Promise<InitResult> {
976
1052
  ciWorkflowCreated,
977
1053
  cdWorkflowCreated,
978
1054
  workflowProvider,
1055
+ installTools: installTools.length > 0 ? installTools : undefined,
1056
+ installedTools:
1057
+ installedTools.length > 0 ? installedTools : undefined,
979
1058
  };
980
1059
  }
981
1060
  } else {
@@ -992,6 +1071,8 @@ export async function runInit(options: InitOptions = {}): Promise<InitResult> {
992
1071
  ciWorkflowCreated,
993
1072
  cdWorkflowCreated,
994
1073
  workflowProvider,
1074
+ installTools: installTools.length > 0 ? installTools : undefined,
1075
+ installedTools: installedTools.length > 0 ? installedTools : undefined,
995
1076
  };
996
1077
  }
997
1078
  }
@@ -1032,11 +1113,228 @@ export async function runInit(options: InitOptions = {}): Promise<InitResult> {
1032
1113
  ciWorkflowCreated,
1033
1114
  cdWorkflowCreated,
1034
1115
  workflowProvider,
1116
+ installTools: installTools.length > 0 ? installTools : undefined,
1117
+ installedTools: installedTools.length > 0 ? installedTools : undefined,
1035
1118
  };
1036
1119
  }
1037
1120
 
1038
1121
  type DatafileAction = "include" | "codegen" | "skip";
1039
1122
 
1123
+ const SKILLS_INSTALL_COMMAND = "npx";
1124
+ const SKILLS_INSTALL_ARGS = [
1125
+ "skills",
1126
+ "add",
1127
+ "tinybirdco/tinybird-agent-skills",
1128
+ "--skill",
1129
+ "tinybird",
1130
+ "--skill",
1131
+ "tinybird-typescript-sdk-guidelines",
1132
+ "--yes",
1133
+ ];
1134
+ const SYNTAX_EXTENSION_DIR = path.join("extension");
1135
+ const SYNTAX_EXTENSION_PREFIX = "tinybird-ts-sdk-extension";
1136
+
1137
+ async function installSelectedTools(
1138
+ cwd: string,
1139
+ installTools: InstallTool[],
1140
+ created: string[],
1141
+ skipped: string[],
1142
+ installedTools: string[]
1143
+ ): Promise<void> {
1144
+ const s = p.spinner();
1145
+ const installedBefore = installedTools.length;
1146
+ s.start("Installing tools");
1147
+ await flushSpinnerRender();
1148
+
1149
+ if (installTools.includes("skills")) {
1150
+ await installSkills(cwd, created, skipped, installedTools);
1151
+ }
1152
+
1153
+ if (installTools.includes("syntax-highlighting")) {
1154
+ await installSyntaxHighlighting(cwd, created, skipped, installedTools);
1155
+ }
1156
+
1157
+ const installedCount = installedTools.length - installedBefore;
1158
+ if (installedCount > 0) {
1159
+ s.stop("Installed tools");
1160
+ } else {
1161
+ s.stop("No tools installed");
1162
+ }
1163
+ }
1164
+
1165
+ async function flushSpinnerRender(): Promise<void> {
1166
+ // Yield once so terminal UI can paint before child process work starts.
1167
+ await new Promise<void>((resolve) => setTimeout(resolve, 20));
1168
+ }
1169
+
1170
+ async function installSkills(
1171
+ cwd: string,
1172
+ created: string[],
1173
+ skipped: string[],
1174
+ installedTools: string[]
1175
+ ): Promise<void> {
1176
+ try {
1177
+ const installCwd = resolveRepoRoot(cwd);
1178
+ await runCommand(SKILLS_INSTALL_COMMAND, SKILLS_INSTALL_ARGS, installCwd);
1179
+ created.push("agent skills (tinybird, tinybird-typescript-sdk-guidelines)");
1180
+ installedTools.push("agent-skills");
1181
+ } catch (error) {
1182
+ skipped.push("agent skills (installation failed)");
1183
+ console.error(
1184
+ `Warning: Failed to install Tinybird agent skills: ${(error as Error).message}`
1185
+ );
1186
+ }
1187
+ }
1188
+
1189
+ function getInstallToolLabel(tool: InstallTool): string {
1190
+ if (tool === "skills") {
1191
+ return "agent skills";
1192
+ }
1193
+
1194
+ return "syntax highlighting";
1195
+ }
1196
+
1197
+ async function installSyntaxHighlighting(
1198
+ cwd: string,
1199
+ created: string[],
1200
+ skipped: string[],
1201
+ installedTools: string[]
1202
+ ): Promise<void> {
1203
+ const editors = [
1204
+ { command: "cursor", label: "Cursor" },
1205
+ { command: "code", label: "VS Code" },
1206
+ ].filter((editor) => isCommandAvailable(editor.command));
1207
+
1208
+ if (editors.length === 0) {
1209
+ skipped.push("syntax highlighting (Cursor/VS Code CLI not found)");
1210
+ return;
1211
+ }
1212
+
1213
+ const vsixPath = findSyntaxHighlightingVsix(cwd);
1214
+ if (!vsixPath) {
1215
+ skipped.push("syntax highlighting (VSIX not found)");
1216
+ return;
1217
+ }
1218
+
1219
+ for (const editor of editors) {
1220
+ try {
1221
+ await runCommand(
1222
+ editor.command,
1223
+ ["--install-extension", vsixPath, "--force"],
1224
+ cwd
1225
+ );
1226
+ created.push(`syntax highlighting (${editor.label})`);
1227
+ installedTools.push(`syntax-highlighting:${editor.command}`);
1228
+ } catch (error) {
1229
+ skipped.push(`syntax highlighting (${editor.label}, install failed)`);
1230
+ console.error(
1231
+ `Warning: Failed to install syntax highlighting in ${editor.label}: ${
1232
+ (error as Error).message
1233
+ }`
1234
+ );
1235
+ }
1236
+ }
1237
+ }
1238
+
1239
+ function resolveRepoRoot(cwd: string): string {
1240
+ try {
1241
+ return execSync("git rev-parse --show-toplevel", {
1242
+ cwd,
1243
+ encoding: "utf-8",
1244
+ stdio: ["pipe", "pipe", "pipe"],
1245
+ }).trim();
1246
+ } catch {
1247
+ return cwd;
1248
+ }
1249
+ }
1250
+
1251
+ function runCommand(command: string, args: string[], cwd: string): Promise<void> {
1252
+ return new Promise((resolve, reject) => {
1253
+ const child = spawn(command, args, {
1254
+ cwd,
1255
+ stdio: "ignore",
1256
+ });
1257
+
1258
+ child.on("error", reject);
1259
+ child.on("close", (code) => {
1260
+ if (code === 0) {
1261
+ resolve();
1262
+ return;
1263
+ }
1264
+
1265
+ reject(
1266
+ new Error(
1267
+ `Command failed (${code ?? "unknown"}): ${command} ${args.join(" ")}`
1268
+ )
1269
+ );
1270
+ });
1271
+ });
1272
+ }
1273
+
1274
+ function splitCommandPrefix(commandPrefix: string): {
1275
+ command: string;
1276
+ args: string[];
1277
+ } {
1278
+ const parts = commandPrefix.trim().split(/\s+/).filter(Boolean);
1279
+ if (parts.length === 0) {
1280
+ throw new Error("Invalid command prefix");
1281
+ }
1282
+ return {
1283
+ command: parts[0],
1284
+ args: parts.slice(1),
1285
+ };
1286
+ }
1287
+
1288
+ function isCommandAvailable(command: string): boolean {
1289
+ try {
1290
+ execSync(`command -v ${command}`, { stdio: "ignore" });
1291
+ return true;
1292
+ } catch {
1293
+ return false;
1294
+ }
1295
+ }
1296
+
1297
+ function findSyntaxHighlightingVsix(cwd: string): string | undefined {
1298
+ const searchRoots = new Set<string>();
1299
+ const gitRoot = getGitRoot();
1300
+ if (gitRoot) {
1301
+ searchRoots.add(gitRoot);
1302
+ }
1303
+
1304
+ let current = path.resolve(cwd);
1305
+ while (true) {
1306
+ searchRoots.add(current);
1307
+ const parent = path.dirname(current);
1308
+ if (parent === current) break;
1309
+ current = parent;
1310
+ }
1311
+
1312
+ for (const root of searchRoots) {
1313
+ const extensionDir = path.join(root, SYNTAX_EXTENSION_DIR);
1314
+ if (!fs.existsSync(extensionDir)) {
1315
+ continue;
1316
+ }
1317
+
1318
+ try {
1319
+ const files = fs
1320
+ .readdirSync(extensionDir)
1321
+ .filter(
1322
+ (file) =>
1323
+ file.endsWith(".vsix") && file.startsWith(SYNTAX_EXTENSION_PREFIX)
1324
+ )
1325
+ .sort();
1326
+
1327
+ if (files.length > 0) {
1328
+ return path.join(extensionDir, files[files.length - 1]);
1329
+ }
1330
+ } catch {
1331
+ // Ignore unreadable directories
1332
+ }
1333
+ }
1334
+
1335
+ return undefined;
1336
+ }
1337
+
1040
1338
  /**
1041
1339
  * Generate TypeScript files from Tinybird workspace resources
1042
1340
  * Only generates for resources that match the local datafiles
@@ -16,7 +16,7 @@ export type DevMode = "branch" | "local";
16
16
  * Tinybird configuration file structure
17
17
  */
18
18
  export interface TinybirdConfig {
19
- /** Array of TypeScript files to scan for datasources and pipes */
19
+ /** Array of file paths or glob patterns to scan for TypeScript/resources */
20
20
  include?: string[];
21
21
  /** @deprecated Use `include` instead. Path to the TypeScript schema entry point */
22
22
  schema?: string;
package/src/cli/config.ts CHANGED
@@ -14,7 +14,7 @@ import type { DevMode, TinybirdConfig } from "./config-types.js";
14
14
  * Resolved configuration with all values expanded
15
15
  */
16
16
  export interface ResolvedConfig {
17
- /** Array of TypeScript files to scan for datasources and pipes */
17
+ /** Array of file paths or glob patterns to scan for datasources and pipes */
18
18
  include: string[];
19
19
  /** Resolved API token (workspace main token) */
20
20
  token: string;
@@ -0,0 +1,234 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+
4
+ export interface ResolvedIncludeFile {
5
+ sourcePath: string;
6
+ absolutePath: string;
7
+ }
8
+
9
+ const GLOB_SEGMENT_REGEX = /[*?[]/;
10
+ const IGNORED_DIRECTORIES = new Set([".git", "node_modules"]);
11
+ const SEGMENT_REGEX_CACHE = new Map<string, RegExp>();
12
+
13
+ function hasGlobPattern(value: string): boolean {
14
+ return GLOB_SEGMENT_REGEX.test(value);
15
+ }
16
+
17
+ function normalizePath(value: string): string {
18
+ return value.replace(/\\/g, "/");
19
+ }
20
+
21
+ function splitAbsolutePath(filePath: string): { root: string; segments: string[] } {
22
+ const absolutePath = path.resolve(filePath);
23
+ const root = path.parse(absolutePath).root;
24
+ const relative = path.relative(root, absolutePath);
25
+
26
+ return {
27
+ root: normalizePath(root),
28
+ segments: normalizePath(relative).split("/").filter(Boolean),
29
+ };
30
+ }
31
+
32
+ function segmentMatcher(segment: string): RegExp {
33
+ const cached = SEGMENT_REGEX_CACHE.get(segment);
34
+ if (cached) {
35
+ return cached;
36
+ }
37
+
38
+ const escaped = segment
39
+ .replace(/[.+^${}()|\\]/g, "\\$&")
40
+ .replace(/\*/g, "[^/]*")
41
+ .replace(/\?/g, "[^/]");
42
+
43
+ const matcher = new RegExp(`^${escaped}$`);
44
+ SEGMENT_REGEX_CACHE.set(segment, matcher);
45
+ return matcher;
46
+ }
47
+
48
+ function matchSegment(patternSegment: string, valueSegment: string): boolean {
49
+ if (!hasGlobPattern(patternSegment)) {
50
+ return patternSegment === valueSegment;
51
+ }
52
+
53
+ return segmentMatcher(patternSegment).test(valueSegment);
54
+ }
55
+
56
+ function matchGlobSegments(
57
+ patternSegments: string[],
58
+ pathSegments: string[],
59
+ patternIndex: number,
60
+ pathIndex: number,
61
+ memo: Map<string, boolean>
62
+ ): boolean {
63
+ const key = `${patternIndex}:${pathIndex}`;
64
+ const cached = memo.get(key);
65
+ if (cached !== undefined) {
66
+ return cached;
67
+ }
68
+
69
+ if (patternIndex === patternSegments.length) {
70
+ const matches = pathIndex === pathSegments.length;
71
+ memo.set(key, matches);
72
+ return matches;
73
+ }
74
+
75
+ const patternSegment = patternSegments[patternIndex];
76
+ let matches = false;
77
+
78
+ if (patternSegment === "**") {
79
+ matches = matchGlobSegments(patternSegments, pathSegments, patternIndex + 1, pathIndex, memo);
80
+
81
+ if (!matches && pathIndex < pathSegments.length) {
82
+ matches = matchGlobSegments(patternSegments, pathSegments, patternIndex, pathIndex + 1, memo);
83
+ }
84
+ } else if (
85
+ pathIndex < pathSegments.length &&
86
+ matchSegment(patternSegment, pathSegments[pathIndex])
87
+ ) {
88
+ matches = matchGlobSegments(patternSegments, pathSegments, patternIndex + 1, pathIndex + 1, memo);
89
+ }
90
+
91
+ memo.set(key, matches);
92
+ return matches;
93
+ }
94
+
95
+ function matchGlobPath(absolutePattern: string, absolutePath: string): boolean {
96
+ const patternParts = splitAbsolutePath(absolutePattern);
97
+ const pathParts = splitAbsolutePath(absolutePath);
98
+
99
+ if (patternParts.root.toLowerCase() !== pathParts.root.toLowerCase()) {
100
+ return false;
101
+ }
102
+
103
+ return matchGlobSegments(
104
+ patternParts.segments,
105
+ pathParts.segments,
106
+ 0,
107
+ 0,
108
+ new Map<string, boolean>()
109
+ );
110
+ }
111
+
112
+ function getGlobRootDirectory(absolutePattern: string): string {
113
+ const { root, segments } = splitAbsolutePath(absolutePattern);
114
+ const firstGlobIndex = segments.findIndex((segment) => hasGlobPattern(segment));
115
+ const baseSegments =
116
+ firstGlobIndex === -1 ? segments : segments.slice(0, firstGlobIndex);
117
+
118
+ return path.join(root, ...baseSegments);
119
+ }
120
+
121
+ function collectFilesRecursive(directory: string, result: string[]): void {
122
+ const entries = fs.readdirSync(directory, { withFileTypes: true });
123
+
124
+ for (const entry of entries) {
125
+ const fullPath = path.join(directory, entry.name);
126
+
127
+ if (entry.isDirectory()) {
128
+ if (IGNORED_DIRECTORIES.has(entry.name)) {
129
+ continue;
130
+ }
131
+
132
+ collectFilesRecursive(fullPath, result);
133
+ continue;
134
+ }
135
+
136
+ if (entry.isFile()) {
137
+ result.push(fullPath);
138
+ }
139
+ }
140
+ }
141
+
142
+ function expandGlobPattern(absolutePattern: string): string[] {
143
+ const rootDirectory = getGlobRootDirectory(absolutePattern);
144
+
145
+ if (!fs.existsSync(rootDirectory)) {
146
+ return [];
147
+ }
148
+
149
+ if (!fs.statSync(rootDirectory).isDirectory()) {
150
+ return [];
151
+ }
152
+
153
+ const files: string[] = [];
154
+ collectFilesRecursive(rootDirectory, files);
155
+
156
+ return files
157
+ .filter((filePath) => matchGlobPath(absolutePattern, filePath))
158
+ .sort((a, b) => a.localeCompare(b));
159
+ }
160
+
161
+ export function resolveIncludeFiles(
162
+ includePaths: string[],
163
+ cwd: string
164
+ ): ResolvedIncludeFile[] {
165
+ const resolved: ResolvedIncludeFile[] = [];
166
+ const seen = new Set<string>();
167
+
168
+ for (const includePath of includePaths) {
169
+ const absoluteIncludePath = path.isAbsolute(includePath)
170
+ ? includePath
171
+ : path.resolve(cwd, includePath);
172
+
173
+ if (hasGlobPattern(includePath)) {
174
+ const matchedFiles = expandGlobPattern(absoluteIncludePath);
175
+
176
+ if (matchedFiles.length === 0) {
177
+ throw new Error(`Include pattern matched no files: ${includePath}`);
178
+ }
179
+
180
+ for (const matchedFile of matchedFiles) {
181
+ if (seen.has(matchedFile)) {
182
+ continue;
183
+ }
184
+
185
+ seen.add(matchedFile);
186
+ resolved.push({
187
+ sourcePath: path.isAbsolute(includePath)
188
+ ? matchedFile
189
+ : path.relative(cwd, matchedFile),
190
+ absolutePath: matchedFile,
191
+ });
192
+ }
193
+ continue;
194
+ }
195
+
196
+ if (!fs.existsSync(absoluteIncludePath)) {
197
+ throw new Error(`Include file not found: ${absoluteIncludePath}`);
198
+ }
199
+
200
+ if (seen.has(absoluteIncludePath)) {
201
+ continue;
202
+ }
203
+
204
+ seen.add(absoluteIncludePath);
205
+ resolved.push({
206
+ sourcePath: includePath,
207
+ absolutePath: absoluteIncludePath,
208
+ });
209
+ }
210
+
211
+ return resolved;
212
+ }
213
+
214
+ export function getIncludeWatchDirectories(
215
+ includePaths: string[],
216
+ cwd: string
217
+ ): string[] {
218
+ const watchDirs = new Set<string>();
219
+
220
+ for (const includePath of includePaths) {
221
+ const absoluteIncludePath = path.isAbsolute(includePath)
222
+ ? includePath
223
+ : path.resolve(cwd, includePath);
224
+
225
+ if (hasGlobPattern(includePath)) {
226
+ watchDirs.add(getGlobRootDirectory(absoluteIncludePath));
227
+ continue;
228
+ }
229
+
230
+ watchDirs.add(path.dirname(absoluteIncludePath));
231
+ }
232
+
233
+ return Array.from(watchDirs);
234
+ }