Commit a67a33b
Changed files (11)
packages
i18n
src
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 = {
* Operation cancelled by user.
*/
cancelled_by_user: string
+ /**
+ * Try running the command with the {option} option to specify the root directory.
+ * @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'>
+ /**
+ * Failed to determine project root: {message}
+ * @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": {