tcdona_unilib 1.0.10 → 1.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 (71) hide show
  1. package/.prettierrc.json +6 -0
  2. package/animejs.ts +13 -1
  3. package/ast-grep.ts +42 -0
  4. package/big.ts +2 -2
  5. package/clipboardy.ts +1 -1
  6. package/dayjs.ts +2 -2
  7. package/dotenvx.ts +15 -2
  8. package/eff/eff.delay.ts +21 -0
  9. package/eff/eff.ts +226 -0
  10. package/eff/effcan.ts +286 -0
  11. package/eff/throwable.ts +11 -0
  12. package/effector.ts +33 -2
  13. package/es-toolkit.ts +1 -7
  14. package/exome.ts +10 -2
  15. package/hono.ts +18 -2
  16. package/hotkeys-js.ts +3 -2
  17. package/idmeta.json +42 -0
  18. package/inquirer.ts +1 -1
  19. package/koka/async.ts +4 -0
  20. package/koka/ctx.ts +4 -0
  21. package/koka/err.ts +4 -0
  22. package/koka/gen.ts +4 -0
  23. package/koka/index.ts +2 -0
  24. package/koka/opt.ts +4 -0
  25. package/koka/result.ts +10 -0
  26. package/koka/task.ts +4 -0
  27. package/koka-domain.ts +18 -0
  28. package/koka.ts +34 -0
  29. package/magic-regexp.ts +28 -2
  30. package/marked.ts +28 -2
  31. package/nanoid.ts +7 -1
  32. package/nanostores.ts +31 -2
  33. package/neverthrow.ts +2 -2
  34. package/package.json +15 -15
  35. package/pathe.ts +16 -1
  36. package/pinyin-pro.ts +16 -2
  37. package/prettier.ts +20 -2
  38. package/staticMeta/enum.api.ts +111 -82
  39. package/staticMeta/err.ts +64 -0
  40. package/staticMeta/file.yml.ts +12 -12
  41. package/staticMeta/md.html2md.ts +38 -0
  42. package/staticMeta/md.md2html.ts +203 -0
  43. package/staticMeta/path.init.ts +12 -12
  44. package/staticMeta/string.nanoid.ts +7 -7
  45. package/staticMeta/url.ts +57 -54
  46. package/tinypool.ts +29 -2
  47. package/turndown.ts +1 -1
  48. package/vite.ts +2 -2
  49. package/viteplugin/md.plugin.dist.d.ts +18 -0
  50. package/viteplugin/md.plugin.dist.js +1133 -0
  51. package/viteplugin/md.plugin.ts +22 -0
  52. package/viteplugin/vite-env.d.ts +6 -0
  53. package/viteplugin/vite.config.ts +62 -0
  54. package/vue.ts +2 -12
  55. package/zod.ts +19 -2
  56. package/zx.ts +25 -1
  57. package/@ast-grep.ts +0 -18
  58. package/comctx.ts +0 -2
  59. package/hono/cors.ts +0 -1
  60. package/hono/logger.ts +0 -1
  61. package/hono/timeout.ts +0 -1
  62. package/staticMeta/ast.scan.ts +0 -131
  63. package/staticMeta/ast.ts +0 -259
  64. package/staticMeta/eff.delay.ts +0 -17
  65. package/staticMeta/eff.ts +0 -203
  66. package/staticMeta/iduniq.ts +0 -320
  67. package/staticMeta/idupdate.ts +0 -374
  68. package/staticMeta/pkg.json.ts +0 -138
  69. package/staticMeta/project.ts +0 -98
  70. package/staticMeta/sync.ts +0 -296
  71. package/tcproject.ts +0 -60
@@ -1,374 +0,0 @@
1
- /**
2
- * 自动更新所有 fileInit() 调用的路径参数为正确的相对路径
3
- */
4
-
5
- import { fileURLToPath } from "node:url"
6
- import { err, ok } from "neverthrow"
7
- import type { EffContext } from "./eff.js"
8
- import { Eff } from "./eff.js"
9
- import type { Result } from "neverthrow"
10
- import type { SgNode } from "@ast-grep/napi"
11
- import { fileInit } from "./enum.api.js"
12
- import {
13
- extractArgumentValue,
14
- findFunctionCalls,
15
- hasImport,
16
- replaceRanges,
17
- } from "./ast.js"
18
- import {
19
- getRelativePath,
20
- parseFileAsync,
21
- readFileAsync,
22
- scanTypeScriptFilesAsync,
23
- writeFileAsync,
24
- } from "./ast.scan.js"
25
-
26
- type NonEmptyArray<T> = readonly [T, ...T[]]
27
-
28
- // ==================== 日志配置 ====================
29
-
30
- const fileId = fileInit("staticMeta/idupdate.ts")
31
-
32
- // ==================== 类型定义 ====================
33
-
34
- type UpdateError =
35
- | { type: "VALIDATION_FILENAME_FAILED"; message: string }
36
- | { type: "FILE_ERROR"; message: string; filePath: string }
37
-
38
- interface StaticMetaCallInfo {
39
- startOffset: number
40
- endOffset: number
41
- currentValue: string | null
42
- }
43
-
44
- interface InvalidCall {
45
- reason: "NO_ARGUMENT" | "VARIABLE_ARGUMENT" | "TEMPLATE_STRING"
46
- line: number
47
- text: string
48
- }
49
-
50
- // ==================== 初始化 ====================
51
-
52
- const __filename = fileURLToPath(import.meta.url)
53
-
54
- // ==================== 验证 ====================
55
-
56
- function validate(): {
57
- isErr: () => boolean
58
- isOk: () => boolean
59
- value?: void
60
- error?: UpdateError
61
- } {
62
- // 验证 staticMeta 功能
63
- const currentFileAbsPath = fileURLToPath(import.meta.url)
64
- const expectedRelativePath = getRelativePath(currentFileAbsPath)
65
-
66
- const conf = fileId("validate1").conf
67
- if (conf.srcFile !== expectedRelativePath) {
68
- return err({
69
- type: "VALIDATION_FILENAME_FAILED",
70
- message: `__filename 不正确:\n 期望: ${expectedRelativePath}\n 实际: ${conf.srcFile}`,
71
- })
72
- }
73
-
74
- return ok(undefined)
75
- }
76
-
77
- // ==================== 文件处理 ====================
78
-
79
- /**
80
- * 找出文件中所有 fileInit 调用(有效和无效)
81
- */
82
- function findFileInitCalls(
83
- root: SgNode
84
- ): [StaticMetaCallInfo[], InvalidCall[]] {
85
- const validCalls: StaticMetaCallInfo[] = []
86
- const invalidCalls: InvalidCall[] = []
87
-
88
- const allCalls = findFunctionCalls(root, "fileInit")
89
-
90
- for (const call of allCalls) {
91
- const children = call.children()
92
- if (children.length < 2) continue
93
-
94
- const args = children[1]
95
-
96
- if (args?.kind() !== "arguments") continue
97
-
98
- const value = extractArgumentValue(args)
99
- const range = call.range()
100
-
101
- if (value === null) {
102
- const argText = args.text()
103
- let reason: InvalidCall["reason"] = "NO_ARGUMENT"
104
-
105
- if (argText.includes("`")) {
106
- reason = "TEMPLATE_STRING"
107
- } else if (
108
- argText.length > 2 &&
109
- !argText.includes('"') &&
110
- !argText.includes("'")
111
- ) {
112
- reason = "VARIABLE_ARGUMENT"
113
- }
114
-
115
- invalidCalls.push({
116
- reason,
117
- line: range.start.line,
118
- text: call.text(),
119
- })
120
- continue
121
- }
122
-
123
- validCalls.push({
124
- startOffset: range.start.index,
125
- endOffset: range.end.index,
126
- currentValue: value,
127
- })
128
- }
129
-
130
- return [validCalls, invalidCalls]
131
- }
132
-
133
- /**
134
- * 更新文件中所有 fileInit 调用的路径参数
135
- */
136
- async function updateStaticMeta(
137
- filePath: string,
138
- content: string,
139
- root: SgNode,
140
- dryRun: boolean,
141
- signal: AbortSignal
142
- ): Promise<{ isErr: boolean; error?: UpdateError; value?: boolean }> {
143
- // 计算正确路径
144
- const relativePath = getRelativePath(filePath)
145
-
146
- // 检测导入 fileInit
147
- if (!hasImport(root, "fileInit")) {
148
- return { isErr: false, value: false }
149
- }
150
-
151
- // 查找 fileInit 调用
152
- const [validCalls, invalidCalls] = findFileInitCalls(root)
153
-
154
- // 记录无效调用
155
- if (invalidCalls.length > 0) {
156
- fileId("invalidFileInitCalls").warn(
157
- `⚠️ ${filePath} 包含无效的 fileInit 调用:`
158
- )
159
- for (const invalid of invalidCalls) {
160
- fileId("invalidFileInitDetail").warn(
161
- ` L${invalid.line} [${invalid.reason}]: ${invalid.text}`
162
- )
163
- }
164
- }
165
-
166
- // 如果没有有效调用则跳过
167
- if (validCalls.length === 0) {
168
- return { isErr: false, value: false }
169
- }
170
-
171
- // 替换内容
172
- const updatedContent = replaceRanges(
173
- content,
174
- validCalls.map((call) => ({
175
- startIndex: call.startOffset,
176
- endIndex: call.endOffset,
177
- replacement: `fileInit('${relativePath}')`,
178
- }))
179
- )
180
-
181
- // 写入文件
182
- if (!dryRun) {
183
- const writeResult = await writeFileAsync(filePath, updatedContent, signal)
184
- if (writeResult.isErr()) {
185
- return {
186
- isErr: true,
187
- error: {
188
- type: "FILE_ERROR",
189
- message: `无法写入文件: ${writeResult.error.message}`,
190
- filePath,
191
- },
192
- }
193
- }
194
- }
195
-
196
- return { isErr: false, value: true }
197
- }
198
-
199
- // ==================== 更新文件执行器 ====================
200
-
201
- /**
202
- * 为单个文件创建更新任务 executor
203
- */
204
- function createUpdateExecutor(
205
- file: string,
206
- dryRun: boolean
207
- ): (
208
- ctx: EffContext
209
- ) => Promise<Result<{ updated: boolean; file: string } | null, UpdateError>> {
210
- return async ({ signal: innerSignal }: EffContext) => {
211
- if (innerSignal.aborted) {
212
- return err({ type: "FILE_ERROR", message: "任务已取消", filePath: file })
213
- }
214
-
215
- const readResult = await readFileAsync(file, innerSignal)
216
- if (readResult.isErr()) {
217
- return err({
218
- type: "FILE_ERROR",
219
- message: `无法读取文件: ${readResult.error.message}`,
220
- filePath: file,
221
- })
222
- }
223
-
224
- const parseResult = await parseFileAsync(file, innerSignal)
225
- if (parseResult.isErr()) {
226
- return err({
227
- type: "FILE_ERROR",
228
- message: `AST 解析失败: ${parseResult.error.message}`,
229
- filePath: file,
230
- })
231
- }
232
-
233
- const updateResult = await updateStaticMeta(
234
- file,
235
- readResult.value,
236
- parseResult.value.root,
237
- dryRun,
238
- innerSignal
239
- )
240
- if (updateResult.isErr) {
241
- return err(updateResult.error!)
242
- }
243
-
244
- return ok(updateResult.value ? { updated: true, file } : null)
245
- }
246
- }
247
-
248
- // ==================== 结果处理与汇总 ====================
249
-
250
- /**
251
- * 统计、分类并输出更新结果
252
- */
253
- function collectAndPrintResults(
254
- results: Result<{ updated: boolean; file: string } | null, UpdateError>[],
255
- files: string[],
256
- dryRun: boolean
257
- ): { updatedFiles: string[]; errors: UpdateError[] } {
258
- const updatedFiles: string[] = []
259
- const skippedFiles: string[] = []
260
- const errors: UpdateError[] = []
261
-
262
- for (let i = 0; i < results.length; i++) {
263
- const result = results[i]
264
- const file = files[i]
265
-
266
- if (result.isErr()) {
267
- errors.push(result.error)
268
- } else if (result.value?.updated) {
269
- updatedFiles.push(getRelativePath(result.value.file))
270
- fileId("updateProgress").info(` ✓ ${getRelativePath(result.value.file)}`)
271
- } else {
272
- skippedFiles.push(getRelativePath(file))
273
- }
274
- }
275
-
276
- // 打印汇总
277
- fileId("summaryHeaderLine1").info(`\n${"=".repeat(60)}`)
278
- fileId("summaryHeaderLine2").info("更新结果汇总")
279
- fileId("summaryHeaderLine3").info(`${"=".repeat(60)}`)
280
-
281
- if (updatedFiles.length > 0) {
282
- fileId("updateSuccess").info(
283
- `\n✅ ${dryRun ? "将更新" : "已更新"} ${updatedFiles.length} 个文件`
284
- )
285
- }
286
-
287
- if (skippedFiles.length > 0) {
288
- fileId("updateSkipped").info(
289
- `⊘ 已跳过 ${skippedFiles.length} 个文件 (无 fileInit 调用)`
290
- )
291
- }
292
-
293
- if (errors.length > 0) {
294
- fileId("updateFailed").error(`\n❌ 更新失败 ${errors.length} 个文件:`)
295
- for (const error of errors) {
296
- const filePath = error.type === "FILE_ERROR" ? error.filePath : "(验证)"
297
- const message = error.message
298
- fileId("updateErrorPath").error(` ${filePath}`)
299
- fileId("updateErrorMessage").error(` └─ ${message}`)
300
- }
301
- }
302
-
303
- fileId("summaryFooter").info(`${"=".repeat(60)}\n`)
304
-
305
- return { updatedFiles, errors }
306
- }
307
-
308
- // ==================== 主函数 ====================
309
-
310
- const main = () => {
311
- return Eff.root(async ({ signal }) => {
312
- if (signal.aborted) return err(new Error("操作已取消"))
313
-
314
- const args = process.argv.slice(2)
315
- const dryRun = args.includes("--dry-run") || args.includes("-n")
316
-
317
- // 验证 staticMeta 功能
318
- // const validationResult = validate();
319
- // if (validationResult.isErr?.()) {
320
- // fileId('validateFailed').error(
321
- // '验证失败:',
322
- // (validationResult as any).error.message,
323
- // );
324
- // // 下面的这个 as any 会让类型推断中断,从而无法保证,return 后面的所有语句的推导正确性,再一次证明了 as any 要少用
325
- // return err((validationResult as any).error);
326
- // }
327
-
328
- // 扫描文件
329
- const files = await scanTypeScriptFilesAsync(["**/staticMeta/idupdate.ts"])
330
- fileId("updateStart").info(
331
- `开始更新 fileInit... (共 ${files.length} 个文件)\n`
332
- )
333
-
334
- if (files.length === 0) {
335
- return ok(undefined)
336
- }
337
-
338
- // 创建并行更新任务
339
- type UpdateResult = { updated: boolean; file: string } | null
340
- const executors = files.map((file) =>
341
- createUpdateExecutor(file, dryRun)
342
- ) as unknown as NonEmptyArray<
343
- (ctx: EffContext) => Promise<Result<UpdateResult, UpdateError>>
344
- >
345
-
346
- // 执行并行更新
347
- const allResults = await Eff.allSettled(executors, signal)
348
- const results = allResults.isOk() ? allResults.value : []
349
-
350
- // 统计结果并输出汇总
351
- const { errors } = collectAndPrintResults(results, files, dryRun)
352
-
353
- if (errors.length > 0) {
354
- const error = errors[0]
355
- const message =
356
- error.type === "FILE_ERROR"
357
- ? `${error.message} (${error.filePath})`
358
- : error.message
359
- return err(new Error(message))
360
- }
361
-
362
- return ok(undefined)
363
- })
364
- }
365
-
366
- main().match(
367
- () => {
368
- /* success */
369
- },
370
- (err) => {
371
- fileId("scriptFailed").error(`脚本执行失败: ${err.message ?? String(err)}`)
372
- process.exit(1)
373
- }
374
- )
@@ -1,138 +0,0 @@
1
- /**
2
- * 自动生成 package.json 中的 staticMeta 相关脚本配置
3
- * 依赖 zx 库查找外层的 package.json 并生成正确的脚本条目
4
- *
5
- * 使用方式:
6
- * bun run node_modules/tcdona_unilib/staticMeta/pkg.json.ts # 更新 package.json
7
- * bun run node_modules/tcdona_unilib/staticMeta/pkg.json.ts --dry-run # 预览将要进行的更改
8
- */
9
-
10
- import { $ } from 'zx';
11
- import { resolve } from 'node:path';
12
- import { readFileSync, writeFileSync } from 'node:fs';
13
- import { fileInit, TcErr } from './enum.api.js';
14
-
15
- // ==================== 初始化 ====================
16
-
17
- const fileId = fileInit('staticMeta/pkg.json.ts');
18
-
19
- // ==================== 脚本配置定义 ====================
20
-
21
- /**
22
- * staticMeta 脚本配置映射表
23
- * 这是唯一的配置源,所有脚本更新都基于这个定义
24
- */
25
- const STATIC_META_SCRIPTS = {
26
- 'iduniq': 'bun run node_modules/tcdona_unilib/staticMeta/iduniq.ts',
27
- 'idupdate': 'bun run node_modules/tcdona_unilib/staticMeta/idupdate.ts',
28
- 'idupdate:dry': 'bun run node_modules/tcdona_unilib/staticMeta/idupdate.ts --dry-run',
29
- } as const;
30
-
31
- // ==================== 查找 package.json ====================
32
-
33
- /**
34
- * 从当前文件向上递归查找 package.json
35
- * 优先使用 git 仓库根目录
36
- */
37
- async function findPackageJsonPath(): Promise<string> {
38
- try {
39
- const result = await $`git rev-parse --show-toplevel`;
40
- const gitRoot = result.stdout.trim();
41
- const pkgPath = resolve(gitRoot, 'package.json');
42
- return pkgPath;
43
- } catch (error) {
44
- throw new TcErr({
45
- fildId: fileId('findPkgPathFailed'),
46
- message: '无法查找 package.json',
47
- cause: error,
48
- });
49
- }
50
- }
51
-
52
- // ==================== 更新 package.json ====================
53
-
54
- /**
55
- * 更新 package.json 中的脚本配置
56
- */
57
- function updatePackageJson(pkgPath: string, dryRun: boolean = false): void {
58
- try {
59
- const content = readFileSync(pkgPath, 'utf-8');
60
- const pkg = JSON.parse(content);
61
-
62
- if (!pkg.scripts) {
63
- pkg.scripts = {};
64
- }
65
-
66
- let updated = false;
67
- const changes: { key: string; oldValue?: string; newValue: string }[] = [];
68
-
69
- // 检查并收集需要更新的脚本
70
- for (const [key, value] of Object.entries(STATIC_META_SCRIPTS)) {
71
- const oldValue = pkg.scripts[key];
72
- if (oldValue !== value) {
73
- changes.push({ key, oldValue, newValue: value });
74
- updated = true;
75
- }
76
- }
77
-
78
- // 输出变更信息
79
- if (changes.length > 0) {
80
- fileId('detectChanges').info(`检测到 ${changes.length} 个需要更新的脚本:`);
81
- for (const change of changes) {
82
- if (change.oldValue) {
83
- fileId('changeKey').info(` • ${change.key}`);
84
- fileId('changeOldValue').warn(` - 旧值: ${change.oldValue}`);
85
- fileId('changeNewValue').info(` + 新值: ${change.newValue}`);
86
- } else {
87
- fileId('addNewScript').info(` + ${change.key}: ${change.newValue}`);
88
- }
89
- }
90
- }
91
-
92
- // 写入文件
93
- if (updated && !dryRun) {
94
- for (const { key, newValue } of changes) {
95
- pkg.scripts[key] = newValue;
96
- }
97
- writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
98
- fileId('updateSuccess').info(`\n✅ 已成功更新 ${pkgPath}`);
99
- } else if (updated && dryRun) {
100
- fileId('dryRunWarning').warn('\n⊘ [Dry Run] 将要更新以上脚本 (使用 bun run pkg:json 执行实际更新)');
101
- } else if (!updated) {
102
- fileId('alreadyLatest').info('\n✓ 所有脚本配置已是最新');
103
- }
104
- } catch (error) {
105
- throw new TcErr({
106
- fildId: fileId('updatePkgJsonFailed'),
107
- pkgPath,
108
- cause: error,
109
- });
110
- }
111
- }
112
-
113
- // ==================== 主函数 ====================
114
-
115
- export const tz = async () => {
116
- try {
117
- const args = process.argv.slice(2);
118
- const dryRun = args.includes('--dry-run') || args.includes('-n');
119
-
120
- const modeLabel = dryRun ? '[预览]' : '[更新]';
121
- fileId('startup').info(`${modeLabel} 生成 staticMeta 脚本配置\n`);
122
-
123
- const pkgPath = await findPackageJsonPath();
124
- fileId('foundPkgPath').info(`位置: ${pkgPath}\n`);
125
-
126
- updatePackageJson(pkgPath, dryRun);
127
- } catch (error) {
128
- if (error instanceof TcErr) {
129
- // TcErr 已经在构造器中输出了
130
- process.exit(1);
131
- }
132
-
133
- throw new TcErr({
134
- fildId: fileId('mainFailed'),
135
- cause: error,
136
- });
137
- }
138
- };
@@ -1,98 +0,0 @@
1
- import { readFileSync, writeFileSync } from 'node:fs';
2
- import { homedir } from 'node:os';
3
- import { join } from 'node:path';
4
- import { yml } from './file.yml.js';
5
-
6
- export interface ProjectConfig {
7
- projects: {
8
- [name: string]: {
9
- path: string;
10
- };
11
- };
12
- }
13
-
14
- /**
15
- * Generates a project.ts file in the current working directory
16
- * that exports project configuration from ~/.tcdona_unilib.yml
17
- *
18
- * @param outputPath - The path where to write the project.ts file (defaults to './project.ts')
19
- *
20
- * @example
21
- * import { generateProjectFile } from 'tcdona_unilib/generateProjectFile';
22
- *
23
- * // Generate project.ts in current directory
24
- * generateProjectFile();
25
- *
26
- * // Then in project.ts you can use:
27
- * // import { get } from './project';
28
- * // const resolveTcDoc = get('tcdoc');
29
- * // const filePath = resolveTcDoc('src', 'index.ts');
30
- */
31
- export function generateProjectFile(outputPath: string = './tcproject.ts'): void {
32
- const configPath = join(homedir(), '.tcdona_unilib.yml');
33
-
34
- try {
35
- // Read and parse the YAML configuration
36
- const config = yml(configPath) as ProjectConfig;
37
-
38
- // Generate the TypeScript file content
39
- const tsContent = `import { resolve } from 'tcdona_unilib/pathe';
40
-
41
- // Auto-generated from ~/.tcdona_unilib.yml
42
- // Do not edit manually
43
-
44
- export const projects = ${JSON.stringify(config.projects, null, 2)} as const;
45
-
46
- /**
47
- * Get a resolve function bound to a project's root directory
48
- * @param name - The project name
49
- * @param paths - Additional path segments to resolve
50
- * @returns A bound resolve function or the resolved path if paths are provided
51
- *
52
- * @example
53
- * // Get a bound resolver for later use
54
- * const resolveInTcDoc = get('tcdoc');
55
- * const filePath = resolveInTcDoc('src', 'index.ts');
56
- *
57
- * @example
58
- * // Directly resolve a path
59
- * const filePath = get('tcdoc', 'src', 'index.ts')();
60
- */
61
- export function get(name: keyof typeof projects): typeof resolve;
62
- export function get(name: keyof typeof projects, ...paths: string[]): () => string;
63
- export function get(name: string): typeof resolve | undefined;
64
- export function get(name: string, ...paths: string[]): (() => string) | undefined;
65
- export function get(name: string, ...paths: string[]): any {
66
- const project = projects[name as keyof typeof projects];
67
- if (!project) {
68
- console.warn(\`Project "\${name}" not found in configuration\`);
69
- return undefined;
70
- }
71
-
72
- if (paths.length === 0) {
73
- // Return a bound function that can be called later
74
- return resolve.bind(null, project.path);
75
- }
76
-
77
- // Return a bound function with the paths already included
78
- return resolve.bind(null, project.path, ...paths);
79
- }
80
- `;
81
-
82
- // Write the TypeScript file
83
- writeFileSync(outputPath, tsContent, 'utf8');
84
- console.log(`✅ Generated ${outputPath} with ${Object.keys(config.projects || {}).length} projects`);
85
- } catch (error) {
86
- if ((error as any).code === 'ENOENT') {
87
- console.error(`❌ Configuration file not found: ${configPath}`);
88
- console.error('Please create ~/.tcdona_unilib.yml first');
89
- } else {
90
- console.error('❌ Error generating project file:', error);
91
- }
92
- throw error;
93
- }
94
- }
95
-
96
- // Export as default as well for convenience
97
- export default generateProjectFile;
98
- generateProjectFile();