Commit a67a33b

HPCesia <me@hpcesia.com>
2025-05-23 07:33:59
chore(tool): move cli tool to monorepo
1 parent 56fef8b
scaffolds/draft.md → packages/cli-tool/scaffolds/draft.md
File renamed without changes
scripts/config.ts → packages/cli-tool/src/config.ts
File renamed without changes
scripts/new.ts → packages/cli-tool/src/new.ts
@@ -1,5 +1,5 @@
 import { config as scriptConfig } from './config';
-import { changeFrontmatter, findAvailableFileName, slugify } from './utils';
+import { changeFrontmatter, findAvailableFileName, findMonorepoRoot, slugify } from './utils';
 import type { AvailableFileNameInfo } from './utils';
 import { L, type Locales } from '@astral-halo/i18n';
 import { Command, OptionValues } from '@commander-js/extra-typings';
@@ -14,6 +14,7 @@ interface CLIOptions extends OptionValues {
   title?: string;
   category?: string;
   tags?: string;
+  root?: string;
 }
 
 const program = new Command<[], CLIOptions>();
@@ -38,8 +39,9 @@ program
   .option('-t, --title <titleString>', 'Article title')
   .option('-c, --category <categoryString>', 'Article category (optional)')
   .option('-T, --tags <tagsString>', 'Article tags, comma-separated (optional)')
+  .option('--root <path>', 'Specify the root directory of the project')
   .action(async (options: CLIOptions) => {
-    const { title: cliTitle, category: cliCategory, tags: cliTags } = options;
+    const { title: cliTitle, category: cliCategory, tags: cliTags, root: cliRootDir } = options;
 
     let title = cliTitle;
     let category = cliCategory;
@@ -74,7 +76,25 @@ program
     const fileExtension = '.md';
     const currentFileName = `${baseSlugForFile}${fileExtension}`;
 
-    let targetDir = path.resolve(process.cwd(), scriptConfig.draftsDir);
+    let projectRootDir: string;
+    try {
+      if (cliRootDir) {
+        projectRootDir = path.resolve(cliRootDir);
+        // Verify if the provided rootDir is valid by checking for a known file/dir, e.g., package.json
+        // This is a simple check, can be made more robust.
+        await fs.access(path.join(projectRootDir, 'package.json'));
+      } else {
+        projectRootDir = await findMonorepoRoot();
+      }
+    } catch (error) {
+      console.error(
+        L[currentLocale].cli.error.failed_to_find_root({ message: (error as Error).message })
+      );
+      console.error(L[currentLocale].cli.info.provide_root_dir_guidance({ option: '--root' }));
+      process.exit(1);
+    }
+
+    let targetDir = path.resolve(projectRootDir, scriptConfig.draftsDir);
     if (scriptConfig.draftStructure === 'category' && category && category.trim() !== '') {
       const categorySlug = slugify(category);
       targetDir = path.join(targetDir, categorySlug);
@@ -176,7 +196,7 @@ program
         }
       }
 
-      const scaffoldPath = path.resolve(process.cwd(), 'scaffolds/draft.md');
+      const scaffoldPath = './scaffolds/draft.md';
       let content = await fs.readFile(scaffoldPath, 'utf-8');
 
       const frontmatterChanges: Record<string, unknown> = {
scripts/pub.ts → packages/cli-tool/src/pub.ts
File renamed without changes
scripts/utils.ts → packages/cli-tool/src/utils.ts
@@ -120,3 +120,35 @@ export async function findAvailableFileName(
     }
   }
 }
+
+/**
+ * Finds the monorepo root directory by searching upwards for a 'pnpm-workspace.yaml' file.
+ * @param startDir The directory to start searching from. Defaults to the current working directory of the script.
+ * @returns The path to the monorepo root directory.
+ * @throws Error if 'pnpm-workspace.yaml' is not found after searching up to the filesystem root.
+ */
+export async function findMonorepoRoot(
+  startDir: string = path.dirname(new URL(import.meta.url).pathname)
+): Promise<string> {
+  let currentDir = startDir;
+  // Adjust for Windows if path starts with /C: -> C:
+  if (process.platform === 'win32' && currentDir.match(/^\/[A-Za-z]:/)) {
+    currentDir = currentDir.substring(1);
+  }
+
+  while (true) {
+    const workspaceFilePath = path.join(currentDir, 'pnpm-workspace.yaml');
+    try {
+      await fs.access(workspaceFilePath);
+      return currentDir; // Found the file, this is the root
+    } catch {
+      // File not found, move up one directory
+      const parentDir = path.dirname(currentDir);
+      if (parentDir === currentDir) {
+        // Reached the filesystem root and haven't found the file
+        throw new Error("Could not find 'pnpm-workspace.yaml'.");
+      }
+      currentDir = parentDir;
+    }
+  }
+}
packages/cli-tool/package.json
@@ -0,0 +1,20 @@
+{
+  "name": "@astral-halo/cli-tool",
+  "scripts": {
+    "new": "tsx src/new.ts",
+    "pub": "tsx src/pub.ts"
+  },
+  "dependencies": {
+    "@commander-js/extra-typings": "^14.0.0",
+    "@inquirer/core": "^10.1.11",
+    "@inquirer/prompts": "^7.5.1",
+    "commander": "^14.0.0",
+    "os-locale": "^6.0.2",
+    "inquirer": "^12.6.1",
+    "js-yaml": "^4.1.0",
+    "tsx": "^4.19.4"
+  },
+  "devDependencies": {
+    "@types/js-yaml": "^4.0.9"
+  }
+}
packages/i18n/src/en/cli/index.ts
@@ -46,6 +46,8 @@ const en_cli = {
   },
   info: {
     cancelled_by_user: 'Operation cancelled by user.',
+    provide_root_dir_guidance:
+      'Try running the command with the {option:string} option to specify the root directory.',
   },
   error: {
     unexpected: 'An unexpected error occurred: {message:string}',
@@ -54,6 +56,7 @@ const en_cli = {
     create_file: 'Error creating file: {message:string}',
     empty_filename: 'File name cannot be empty.',
     rename_to_original_conflict: 'Cannot rename to original file name: {fileName:string}',
+    failed_to_find_root: 'Failed to determine project root: {message:string}',
   },
 } satisfies BaseTranslation;
 
packages/i18n/src/zh-CN/cli/index.ts
@@ -45,6 +45,7 @@ const zh_CN_cli = {
   },
   info: {
     cancelled_by_user: '用户已取消操作。',
+    provide_root_dir_guidance: '尝试使用 {option} 选项运行命令以指定根目录。',
   },
   error: {
     unexpected: '发生意外错误:{message}',
@@ -53,6 +54,7 @@ const zh_CN_cli = {
     create_file: '创建文件时出错:{message}',
     empty_filename: '文件名不能为空。',
     rename_to_original_conflict: '新文件名与原文件名冲突:{fileName}',
+    failed_to_find_root: '无法确定项目根目录:{message}',
   },
 } satisfies NamespaceCliTranslation;
 
packages/i18n/src/zh-TW/cli/index.ts
@@ -45,6 +45,7 @@ const zh_TW_cli = {
   },
   info: {
     cancelled_by_user: '用戶已取消操作。',
+    provide_root_dir_guidance: '嘗試使用 {option} 選項運行命令以指定根目錄。',
   },
   error: {
     unexpected: '發生意外錯誤:{message}',
@@ -53,6 +54,7 @@ const zh_TW_cli = {
     create_file: '創建文件時出錯:{message}',
     empty_filename: '文件名不能為空。',
     rename_to_original_conflict: '新文件名與原文件名衝突:{fileName}',
+    failed_to_find_root: '無法確定項目根目錄:{message}',
   },
 } satisfies NamespaceCliTranslation;
 
packages/i18n/src/i18n-types.ts
@@ -129,6 +129,11 @@ export type NamespaceCliTranslation = {
 		 * O​p​e​r​a​t​i​o​n​ ​c​a​n​c​e​l​l​e​d​ ​b​y​ ​u​s​e​r​.
 		 */
 		cancelled_by_user: string
+		/**
+		 * T​r​y​ ​r​u​n​n​i​n​g​ ​t​h​e​ ​c​o​m​m​a​n​d​ ​w​i​t​h​ ​t​h​e​ ​{​o​p​t​i​o​n​}​ ​o​p​t​i​o​n​ ​t​o​ ​s​p​e​c​i​f​y​ ​t​h​e​ ​r​o​o​t​ ​d​i​r​e​c​t​o​r​y​.
+		 * @param {string} option
+		 */
+		provide_root_dir_guidance: RequiredParams<'option'>
 	}
 	error: {
 		/**
@@ -160,6 +165,11 @@ export type NamespaceCliTranslation = {
 		 * @param {string} fileName
 		 */
 		rename_to_original_conflict: RequiredParams<'fileName'>
+		/**
+		 * F​a​i​l​e​d​ ​t​o​ ​d​e​t​e​r​m​i​n​e​ ​p​r​o​j​e​c​t​ ​r​o​o​t​:​ ​{​m​e​s​s​a​g​e​}
+		 * @param {string} message
+		 */
+		failed_to_find_root: RequiredParams<'message'>
 	}
 }
 
@@ -523,6 +533,10 @@ export type TranslationFunctions = {
 			 * Operation cancelled by user.
 			 */
 			cancelled_by_user: () => LocalizedString
+			/**
+			 * Try running the command with the {option} option to specify the root directory.
+			 */
+			provide_root_dir_guidance: (arg: { option: string }) => LocalizedString
 		}
 		error: {
 			/**
@@ -549,6 +563,10 @@ export type TranslationFunctions = {
 			 * Cannot rename to original file name: {fileName}
 			 */
 			rename_to_original_conflict: (arg: { fileName: string }) => LocalizedString
+			/**
+			 * Failed to determine project root: {message}
+			 */
+			failed_to_find_root: (arg: { message: string }) => LocalizedString
 		}
 	}
 	web: {
package.json
@@ -9,8 +9,8 @@
     "astro": "astro",
     "lint": "eslint ./src --fix && stylelint ./src/**/*.{scss,css,astro} --fix && astro check",
     "format": "prettier --write ./src",
-    "new": "tsx scripts/new.ts",
-    "pub": "tsx scripts/pub.ts"
+    "new": "pnpm --filter @astral-halo/cli-tool run new",
+    "pub": "pnpm --filter @astral-halo/cli-tool run pub"
   },
   "dependencies": {
     "@astral-halo/i18n": "workspace:*",
@@ -70,14 +70,10 @@
   "devDependencies": {
     "@astrojs/check": "^0.9.4",
     "@astrojs/ts-plugin": "^1.10.4",
-    "@commander-js/extra-typings": "^14.0.0",
     "@eslint/js": "^9.27.0",
     "@iconify/types": "^2.0.0",
-    "@inquirer/core": "^10.1.11",
-    "@inquirer/prompts": "^7.5.1",
     "@trivago/prettier-plugin-sort-imports": "^5.2.2",
     "@types/hast": "^3.0.4",
-    "@types/js-yaml": "^4.0.9",
     "@types/markdown-it": "^14.1.2",
     "@types/mdast": "^4.0.4",
     "@types/node": "^22.15.18",
@@ -85,14 +81,10 @@
     "@types/unist": "^3.0.3",
     "@typescript-eslint/parser": "^8.32.1",
     "astro-eslint-parser": "^1.2.2",
-    "commander": "^14.0.0",
     "eslint": "^9.27.0",
     "eslint-plugin-astro": "^1.3.1",
     "github-slugger": "^2.0.0",
     "globals": "^15.15.0",
-    "inquirer": "^12.6.1",
-    "js-yaml": "^4.1.0",
-    "os-locale": "^6.0.2",
     "postcss-html": "^1.8.0",
     "prettier": "^3.5.3",
     "prettier-plugin-astro": "^0.14.1",
@@ -100,7 +92,6 @@
     "prettier-plugin-tailwindcss": "^0.6.11",
     "stylelint": "^16.19.1",
     "stylelint-config-html": "^1.1.0",
-    "tsx": "^4.19.4",
     "typescript-eslint": "^8.32.1"
   },
   "pnpm": {