@tanstack/cli 0.0.8 → 0.48.3

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 (88) hide show
  1. package/LICENSE +21 -0
  2. package/dist/bin.js +7 -0
  3. package/dist/cli.js +481 -0
  4. package/dist/command-line.js +174 -0
  5. package/dist/dev-watch.js +290 -0
  6. package/dist/file-syncer.js +148 -0
  7. package/dist/index.js +1 -0
  8. package/dist/mcp/api.js +31 -0
  9. package/dist/mcp/tools.js +250 -0
  10. package/dist/mcp/types.js +37 -0
  11. package/dist/mcp.js +121 -0
  12. package/dist/options.js +162 -0
  13. package/dist/types/bin.d.ts +2 -0
  14. package/dist/types/cli.d.ts +16 -0
  15. package/dist/types/command-line.d.ts +10 -0
  16. package/dist/types/dev-watch.d.ts +27 -0
  17. package/dist/types/file-syncer.d.ts +18 -0
  18. package/dist/types/index.d.ts +1 -0
  19. package/dist/types/mcp/api.d.ts +4 -0
  20. package/dist/types/mcp/tools.d.ts +2 -0
  21. package/dist/types/mcp/types.d.ts +217 -0
  22. package/dist/types/mcp.d.ts +6 -0
  23. package/dist/types/options.d.ts +8 -0
  24. package/dist/types/types.d.ts +25 -0
  25. package/dist/types/ui-environment.d.ts +2 -0
  26. package/dist/types/ui-prompts.d.ts +12 -0
  27. package/dist/types/utils.d.ts +8 -0
  28. package/dist/types.js +1 -0
  29. package/dist/ui-environment.js +52 -0
  30. package/dist/ui-prompts.js +244 -0
  31. package/dist/utils.js +30 -0
  32. package/package.json +46 -47
  33. package/src/bin.ts +6 -93
  34. package/src/cli.ts +692 -0
  35. package/src/command-line.ts +236 -0
  36. package/src/dev-watch.ts +430 -0
  37. package/src/file-syncer.ts +205 -0
  38. package/src/index.ts +1 -85
  39. package/src/mcp.ts +190 -0
  40. package/src/options.ts +260 -0
  41. package/src/types.ts +27 -0
  42. package/src/ui-environment.ts +74 -0
  43. package/src/ui-prompts.ts +322 -0
  44. package/src/utils.ts +38 -0
  45. package/tests/command-line.test.ts +304 -0
  46. package/tests/index.test.ts +9 -0
  47. package/tests/mcp.test.ts +225 -0
  48. package/tests/options.test.ts +304 -0
  49. package/tests/setupVitest.ts +6 -0
  50. package/tests/ui-environment.test.ts +97 -0
  51. package/tests/ui-prompts.test.ts +238 -0
  52. package/tsconfig.json +17 -0
  53. package/vitest.config.js +7 -0
  54. package/dist/bin.cjs +0 -769
  55. package/dist/bin.d.cts +0 -1
  56. package/dist/bin.d.mts +0 -1
  57. package/dist/bin.mjs +0 -768
  58. package/dist/fetch-CbFFGJEw.cjs +0 -3
  59. package/dist/fetch-DG5dLrsb.cjs +0 -522
  60. package/dist/fetch-DhlVXS6S.mjs +0 -390
  61. package/dist/fetch-I_OVg8JX.mjs +0 -3
  62. package/dist/index.cjs +0 -37
  63. package/dist/index.d.cts +0 -1172
  64. package/dist/index.d.mts +0 -1172
  65. package/dist/index.mjs +0 -4
  66. package/dist/template-Szi7-AZJ.mjs +0 -2202
  67. package/dist/template-lWrIZhCQ.cjs +0 -2314
  68. package/src/api/fetch.test.ts +0 -114
  69. package/src/api/fetch.ts +0 -278
  70. package/src/cache/index.ts +0 -89
  71. package/src/commands/create.ts +0 -470
  72. package/src/commands/mcp.test.ts +0 -152
  73. package/src/commands/mcp.ts +0 -211
  74. package/src/engine/compile-with-addons.test.ts +0 -302
  75. package/src/engine/compile.test.ts +0 -404
  76. package/src/engine/compile.ts +0 -569
  77. package/src/engine/config-file.test.ts +0 -118
  78. package/src/engine/config-file.ts +0 -61
  79. package/src/engine/custom-addons/integration.ts +0 -323
  80. package/src/engine/custom-addons/shared.test.ts +0 -98
  81. package/src/engine/custom-addons/shared.ts +0 -281
  82. package/src/engine/custom-addons/template.test.ts +0 -288
  83. package/src/engine/custom-addons/template.ts +0 -124
  84. package/src/engine/template.test.ts +0 -256
  85. package/src/engine/template.ts +0 -269
  86. package/src/engine/types.ts +0 -336
  87. package/src/parse-gitignore.d.ts +0 -5
  88. package/src/templates/base.ts +0 -883
@@ -0,0 +1,290 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import chokidar from 'chokidar';
4
+ import chalk from 'chalk';
5
+ import { temporaryDirectory } from 'tempy';
6
+ import { createApp, getFrameworkById, registerFramework, } from '@tanstack/create';
7
+ import { FileSyncer } from './file-syncer.js';
8
+ import { createUIEnvironment } from './ui-environment.js';
9
+ class DebounceQueue {
10
+ constructor(callback, delay = 1000) {
11
+ this.delay = delay;
12
+ this.timer = null;
13
+ this.changes = new Set();
14
+ this.callback = callback;
15
+ }
16
+ add(path) {
17
+ this.changes.add(path);
18
+ if (this.timer) {
19
+ clearTimeout(this.timer);
20
+ }
21
+ this.timer = setTimeout(() => {
22
+ const currentChanges = new Set(this.changes);
23
+ this.callback(currentChanges);
24
+ this.changes.clear();
25
+ }, this.delay);
26
+ }
27
+ size() {
28
+ return this.changes.size;
29
+ }
30
+ clear() {
31
+ if (this.timer) {
32
+ clearTimeout(this.timer);
33
+ this.timer = null;
34
+ }
35
+ this.changes.clear();
36
+ }
37
+ }
38
+ export class DevWatchManager {
39
+ constructor(options) {
40
+ this.options = options;
41
+ this.watcher = null;
42
+ this.tempDir = null;
43
+ this.isBuilding = false;
44
+ this.buildCount = 0;
45
+ this.log = {
46
+ tree: (prefix, msg, isLast = false) => {
47
+ const connector = isLast ? '└─' : '├─';
48
+ console.log(chalk.gray(prefix + connector) + ' ' + msg);
49
+ },
50
+ treeItem: (prefix, msg, isLast = false) => {
51
+ const connector = isLast ? '└─' : '├─';
52
+ console.log(chalk.gray(prefix + ' ' + connector) + ' ' + msg);
53
+ },
54
+ info: (msg) => console.log(msg),
55
+ error: (msg) => console.error(chalk.red('✗') + ' ' + msg),
56
+ success: (msg) => console.log(chalk.green('✓') + ' ' + msg),
57
+ warning: (msg) => console.log(chalk.yellow('⚠') + ' ' + msg),
58
+ section: (title) => console.log('\n' + chalk.bold('▸ ' + title)),
59
+ subsection: (msg) => console.log(' ' + msg),
60
+ };
61
+ this.syncer = new FileSyncer();
62
+ this.debounceQueue = new DebounceQueue((changes) => this.rebuild(changes));
63
+ }
64
+ async start() {
65
+ // Validate watch path
66
+ if (!fs.existsSync(this.options.watchPath)) {
67
+ throw new Error(`Watch path does not exist: ${this.options.watchPath}`);
68
+ }
69
+ // Validate target directory exists (should have been created by createApp)
70
+ if (!fs.existsSync(this.options.targetDir)) {
71
+ throw new Error(`Target directory does not exist: ${this.options.targetDir}`);
72
+ }
73
+ if (this.options.cliOptions.install === false) {
74
+ throw new Error('Cannot use the --no-install flag when using --dev-watch');
75
+ }
76
+ // Log startup with tree style
77
+ console.log();
78
+ console.log(chalk.bold('dev-watch'));
79
+ this.log.tree('', `watching: ${chalk.cyan(this.options.watchPath)}`);
80
+ this.log.tree('', `target: ${chalk.cyan(this.options.targetDir)}`);
81
+ this.log.tree('', 'ready', true);
82
+ // Setup signal handlers
83
+ process.on('SIGINT', () => this.cleanup());
84
+ process.on('SIGTERM', () => this.cleanup());
85
+ // Start watching
86
+ this.startWatcher();
87
+ }
88
+ async stop() {
89
+ console.log();
90
+ this.log.info('Stopping dev watch mode...');
91
+ if (this.watcher) {
92
+ await this.watcher.close();
93
+ this.watcher = null;
94
+ }
95
+ this.debounceQueue.clear();
96
+ this.cleanup();
97
+ }
98
+ startWatcher() {
99
+ const watcherConfig = {
100
+ ignored: [
101
+ '**/node_modules/**',
102
+ '**/.git/**',
103
+ '**/dist/**',
104
+ '**/build/**',
105
+ '**/.DS_Store',
106
+ '**/*.log',
107
+ this.tempDir,
108
+ ],
109
+ persistent: true,
110
+ ignoreInitial: true,
111
+ awaitWriteFinish: {
112
+ stabilityThreshold: 100,
113
+ pollInterval: 100,
114
+ },
115
+ };
116
+ this.watcher = chokidar.watch(this.options.watchPath, watcherConfig);
117
+ this.watcher.on('add', (filePath) => this.handleChange('add', filePath));
118
+ this.watcher.on('change', (filePath) => this.handleChange('change', filePath));
119
+ this.watcher.on('unlink', (filePath) => this.handleChange('unlink', filePath));
120
+ this.watcher.on('error', (error) => this.log.error(`Watcher error: ${error.message}`));
121
+ this.watcher.on('ready', () => {
122
+ // Already shown in startup, no need to repeat
123
+ });
124
+ }
125
+ handleChange(_type, filePath) {
126
+ const relativePath = path.relative(this.options.watchPath, filePath);
127
+ // Log change only once for the first file in debounce queue
128
+ if (this.debounceQueue.size() === 0) {
129
+ this.log.section('change detected');
130
+ this.log.subsection(`└─ ${relativePath}`);
131
+ }
132
+ else {
133
+ this.log.subsection(`└─ ${relativePath}`);
134
+ }
135
+ this.debounceQueue.add(filePath);
136
+ }
137
+ async rebuild(changes) {
138
+ if (this.isBuilding) {
139
+ this.log.warning('Build already in progress, skipping...');
140
+ return;
141
+ }
142
+ this.isBuilding = true;
143
+ this.buildCount++;
144
+ const buildId = this.buildCount;
145
+ try {
146
+ this.log.section(`build #${buildId}`);
147
+ const startTime = Date.now();
148
+ if (!this.options.frameworkDefinitionInitializers) {
149
+ throw new Error('There must be framework initalizers passed to frameworkDefinitionInitializers to use --dev-watch');
150
+ }
151
+ const refreshedFrameworks = this.options.frameworkDefinitionInitializers.map((frameworkInitalizer) => frameworkInitalizer());
152
+ const refreshedFramework = refreshedFrameworks.find((f) => f.id === this.options.framework.id);
153
+ if (!refreshedFramework) {
154
+ throw new Error('Could not identify the framework');
155
+ }
156
+ // Update the chosen addons to use the latest code
157
+ const chosenAddonIds = this.options.cliOptions.chosenAddOns.map((m) => m.id);
158
+ const updatedChosenAddons = refreshedFramework.addOns.filter((f) => chosenAddonIds.includes(f.id));
159
+ // Create temp directory for this build using tempy
160
+ this.tempDir = temporaryDirectory();
161
+ // Register the scanned framework
162
+ registerFramework({
163
+ ...refreshedFramework,
164
+ id: `${refreshedFramework.id}-updated`,
165
+ });
166
+ // Get the registered framework
167
+ const registeredFramework = getFrameworkById(`${refreshedFramework.id}-updated`);
168
+ if (!registeredFramework) {
169
+ throw new Error(`Failed to register framework: ${this.options.framework.id}`);
170
+ }
171
+ // Check if package.json was modified
172
+ const packageJsonModified = Array.from(changes).some((filePath) => path.basename(filePath) === 'package.json');
173
+ const updatedOptions = {
174
+ ...this.options.cliOptions,
175
+ chosenAddOns: updatedChosenAddons,
176
+ framework: registeredFramework,
177
+ targetDir: this.tempDir,
178
+ git: false,
179
+ install: packageJsonModified,
180
+ };
181
+ // Show package installation indicator if needed
182
+ if (packageJsonModified) {
183
+ this.log.tree(' ', `${chalk.yellow('⟳')} installing packages...`);
184
+ }
185
+ // Create app in temp directory with silent environment
186
+ const silentEnvironment = createUIEnvironment(this.options.environment.appName, true);
187
+ await createApp(silentEnvironment, updatedOptions);
188
+ // Sync files to target directory
189
+ const syncResult = await this.syncer.sync(this.tempDir, this.options.targetDir);
190
+ // Clean up temp directory after sync is complete
191
+ try {
192
+ await fs.promises.rm(this.tempDir, { recursive: true, force: true });
193
+ }
194
+ catch (cleanupError) {
195
+ this.log.warning(`Failed to clean up temp directory: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`);
196
+ }
197
+ const elapsed = Date.now() - startTime;
198
+ // Build tree-style summary
199
+ this.log.tree(' ', `duration: ${chalk.cyan(elapsed + 'ms')}`);
200
+ if (packageJsonModified) {
201
+ this.log.tree(' ', `packages: ${chalk.green('✓ installed')}`);
202
+ }
203
+ // Always show the last item in tree without checking for files to show
204
+ const noMoreTreeItems = syncResult.updated.length === 0 &&
205
+ syncResult.created.length === 0 &&
206
+ syncResult.errors.length === 0;
207
+ if (syncResult.updated.length > 0) {
208
+ this.log.tree(' ', `updated: ${chalk.green(syncResult.updated.length + ' file' + (syncResult.updated.length > 1 ? 's' : ''))}`, syncResult.created.length === 0 && syncResult.errors.length === 0);
209
+ }
210
+ if (syncResult.created.length > 0) {
211
+ this.log.tree(' ', `created: ${chalk.green(syncResult.created.length + ' file' + (syncResult.created.length > 1 ? 's' : ''))}`, syncResult.errors.length === 0);
212
+ }
213
+ if (syncResult.errors.length > 0) {
214
+ this.log.tree(' ', `failed: ${chalk.red(syncResult.errors.length + ' file' + (syncResult.errors.length > 1 ? 's' : ''))}`, true);
215
+ }
216
+ // If nothing changed, show that
217
+ if (noMoreTreeItems) {
218
+ this.log.tree(' ', `no changes`, true);
219
+ }
220
+ // Always show changed files with diffs
221
+ if (syncResult.updated.length > 0) {
222
+ syncResult.updated.forEach((update, index) => {
223
+ const isLastFile = index === syncResult.updated.length - 1 &&
224
+ syncResult.created.length === 0;
225
+ // For files with diffs, always use ├─
226
+ const fileIsLast = isLastFile && !update.diff;
227
+ this.log.treeItem(' ', update.path, fileIsLast);
228
+ // Always show diff if available
229
+ if (update.diff) {
230
+ const diffLines = update.diff.split('\n');
231
+ const relevantLines = diffLines
232
+ .slice(4)
233
+ .filter((line) => line.startsWith('+') ||
234
+ line.startsWith('-') ||
235
+ line.startsWith('@'));
236
+ if (relevantLines.length > 0) {
237
+ // Always use │ to continue the tree line through the diff
238
+ const prefix = ' │ ';
239
+ relevantLines.forEach((line) => {
240
+ if (line.startsWith('+') && !line.startsWith('+++')) {
241
+ console.log(chalk.gray(prefix) + ' ' + chalk.green(line));
242
+ }
243
+ else if (line.startsWith('-') && !line.startsWith('---')) {
244
+ console.log(chalk.gray(prefix) + ' ' + chalk.red(line));
245
+ }
246
+ else if (line.startsWith('@')) {
247
+ console.log(chalk.gray(prefix) + ' ' + chalk.cyan(line));
248
+ }
249
+ });
250
+ }
251
+ }
252
+ });
253
+ }
254
+ // Show created files
255
+ if (syncResult.created.length > 0) {
256
+ syncResult.created.forEach((file, index) => {
257
+ const isLast = index === syncResult.created.length - 1;
258
+ this.log.treeItem(' ', `${chalk.green('+')} ${file}`, isLast);
259
+ });
260
+ }
261
+ // Always show errors
262
+ if (syncResult.errors.length > 0) {
263
+ console.log(); // Add spacing
264
+ syncResult.errors.forEach((err, index) => {
265
+ this.log.tree(' ', `${chalk.red('error:')} ${err}`, index === syncResult.errors.length - 1);
266
+ });
267
+ }
268
+ }
269
+ catch (error) {
270
+ this.log.error(`Build #${buildId} failed: ${error instanceof Error ? error.message : String(error)}`);
271
+ }
272
+ finally {
273
+ this.isBuilding = false;
274
+ }
275
+ }
276
+ cleanup() {
277
+ console.log();
278
+ console.log('Cleaning up...');
279
+ // Clean up temp directory
280
+ if (this.tempDir && fs.existsSync(this.tempDir)) {
281
+ try {
282
+ fs.rmSync(this.tempDir, { recursive: true, force: true });
283
+ }
284
+ catch (error) {
285
+ this.log.error(`Failed to clean up temp directory: ${error instanceof Error ? error.message : String(error)}`);
286
+ }
287
+ }
288
+ process.exit(0);
289
+ }
290
+ }
@@ -0,0 +1,148 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import crypto from 'node:crypto';
4
+ import * as diff from 'diff';
5
+ export class FileSyncer {
6
+ async sync(sourceDir, targetDir) {
7
+ const result = {
8
+ updated: [],
9
+ skipped: [],
10
+ created: [],
11
+ errors: [],
12
+ };
13
+ // Ensure directories exist
14
+ if (!fs.existsSync(sourceDir)) {
15
+ throw new Error(`Source directory does not exist: ${sourceDir}`);
16
+ }
17
+ if (!fs.existsSync(targetDir)) {
18
+ throw new Error(`Target directory does not exist: ${targetDir}`);
19
+ }
20
+ // Walk through source directory and sync files
21
+ await this.syncDirectory(sourceDir, targetDir, sourceDir, result);
22
+ return result;
23
+ }
24
+ async syncDirectory(currentPath, targetBase, sourceBase, result) {
25
+ const entries = await fs.promises.readdir(currentPath, {
26
+ withFileTypes: true,
27
+ });
28
+ for (const entry of entries) {
29
+ const sourcePath = path.join(currentPath, entry.name);
30
+ const relativePath = path.relative(sourceBase, sourcePath);
31
+ const targetPath = path.join(targetBase, relativePath);
32
+ // Skip certain directories
33
+ if (entry.isDirectory()) {
34
+ if (this.shouldSkipDirectory(entry.name)) {
35
+ continue;
36
+ }
37
+ // Ensure target directory exists
38
+ if (!fs.existsSync(targetPath)) {
39
+ await fs.promises.mkdir(targetPath, { recursive: true });
40
+ }
41
+ // Recursively sync subdirectory
42
+ await this.syncDirectory(sourcePath, targetBase, sourceBase, result);
43
+ }
44
+ else if (entry.isFile()) {
45
+ // Skip certain files
46
+ if (this.shouldSkipFile(entry.name)) {
47
+ continue;
48
+ }
49
+ try {
50
+ const shouldUpdate = await this.shouldUpdateFile(sourcePath, targetPath);
51
+ if (shouldUpdate) {
52
+ // Check if file exists to generate diff
53
+ let fileDiff;
54
+ const targetExists = fs.existsSync(targetPath);
55
+ if (targetExists) {
56
+ // Generate diff for existing files
57
+ const oldContent = await fs.promises.readFile(targetPath, 'utf-8');
58
+ const newContent = await fs.promises.readFile(sourcePath, 'utf-8');
59
+ const changes = diff.createPatch(relativePath, oldContent, newContent, 'Previous', 'Current');
60
+ // Only include diff if there are actual changes
61
+ if (changes && changes.split('\n').length > 5) {
62
+ fileDiff = changes;
63
+ }
64
+ }
65
+ // Copy file
66
+ await fs.promises.copyFile(sourcePath, targetPath);
67
+ // Touch file to trigger dev server reload
68
+ const now = new Date();
69
+ await fs.promises.utimes(targetPath, now, now);
70
+ if (!targetExists) {
71
+ result.created.push(relativePath);
72
+ }
73
+ else {
74
+ result.updated.push({
75
+ path: relativePath,
76
+ diff: fileDiff,
77
+ });
78
+ }
79
+ }
80
+ else {
81
+ result.skipped.push(relativePath);
82
+ }
83
+ }
84
+ catch (error) {
85
+ result.errors.push(`${relativePath}: ${error instanceof Error ? error.message : String(error)}`);
86
+ }
87
+ }
88
+ }
89
+ }
90
+ async shouldUpdateFile(sourcePath, targetPath) {
91
+ // If target doesn't exist, definitely update
92
+ if (!fs.existsSync(targetPath)) {
93
+ return true;
94
+ }
95
+ // Compare file sizes first (quick check)
96
+ const [sourceStats, targetStats] = await Promise.all([
97
+ fs.promises.stat(sourcePath),
98
+ fs.promises.stat(targetPath),
99
+ ]);
100
+ if (sourceStats.size !== targetStats.size) {
101
+ return true;
102
+ }
103
+ // Compare MD5 hashes for content
104
+ const [sourceHash, targetHash] = await Promise.all([
105
+ this.calculateHash(sourcePath),
106
+ this.calculateHash(targetPath),
107
+ ]);
108
+ return sourceHash !== targetHash;
109
+ }
110
+ async calculateHash(filePath) {
111
+ return new Promise((resolve, reject) => {
112
+ const hash = crypto.createHash('md5');
113
+ const stream = fs.createReadStream(filePath);
114
+ stream.on('data', (data) => hash.update(data));
115
+ stream.on('end', () => resolve(hash.digest('hex')));
116
+ stream.on('error', reject);
117
+ });
118
+ }
119
+ shouldSkipDirectory(name) {
120
+ const skipDirs = [
121
+ 'node_modules',
122
+ '.git',
123
+ 'dist',
124
+ 'build',
125
+ '.next',
126
+ '.nuxt',
127
+ '.cache',
128
+ '.tmp-dev',
129
+ 'coverage',
130
+ '.turbo',
131
+ ];
132
+ return skipDirs.includes(name) || name.startsWith('.');
133
+ }
134
+ shouldSkipFile(name) {
135
+ const skipFiles = [
136
+ '.DS_Store',
137
+ 'Thumbs.db',
138
+ 'desktop.ini',
139
+ '.cta.json', // Skip .cta.json as it contains framework ID that changes each build
140
+ ];
141
+ const skipExtensions = ['.log', '.lock', '.pid', '.seed', '.sqlite'];
142
+ if (skipFiles.includes(name)) {
143
+ return true;
144
+ }
145
+ const ext = path.extname(name).toLowerCase();
146
+ return skipExtensions.includes(ext);
147
+ }
148
+ }
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { cli } from './cli.js';
@@ -0,0 +1,31 @@
1
+ import { LibrariesResponseSchema, PartnersResponseSchema } from './types.js';
2
+ const TANSTACK_API_BASE = 'https://tanstack.com/api/data';
3
+ export async function fetchLibraries() {
4
+ const response = await fetch(`${TANSTACK_API_BASE}/libraries`);
5
+ if (!response.ok) {
6
+ throw new Error(`Failed to fetch libraries: ${response.statusText}`);
7
+ }
8
+ const data = await response.json();
9
+ return LibrariesResponseSchema.parse(data);
10
+ }
11
+ export async function fetchPartners() {
12
+ const response = await fetch(`${TANSTACK_API_BASE}/partners`);
13
+ if (!response.ok) {
14
+ throw new Error(`Failed to fetch partners: ${response.statusText}`);
15
+ }
16
+ const data = await response.json();
17
+ return PartnersResponseSchema.parse(data);
18
+ }
19
+ export async function fetchDocContent(repo, branch, filePath) {
20
+ const url = `https://raw.githubusercontent.com/${repo}/${branch}/${filePath}`;
21
+ const response = await fetch(url, {
22
+ headers: { 'User-Agent': 'tanstack-cli' },
23
+ });
24
+ if (!response.ok) {
25
+ if (response.status === 404) {
26
+ return null;
27
+ }
28
+ throw new Error(`Failed to fetch doc: ${response.statusText}`);
29
+ }
30
+ return response.text();
31
+ }