@yuchenrx/pmt-cli 0.1.2 → 0.1.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.
package/README.md CHANGED
@@ -2,115 +2,116 @@
2
2
 
3
3
  Prompt/spec manager CLI that keeps shared rules synchronized across projects.
4
4
 
5
- [中文文档](./README.zh.md)
5
+ English | [简体中文](./README.zh.md)
6
6
 
7
- ## What it does
7
+ [![npm version](https://img.shields.io/npm/v/@yuchenrx/pmt-cli)](https://www.npmjs.com/package/@yuchenrx/pmt-cli)
8
+ [![release](https://github.com/YuChenRX/pmt/actions/workflows/release.yml/badge.svg)](https://github.com/YuChenRX/pmt/actions/workflows/release.yml)
9
+ [![platform](https://img.shields.io/badge/platform-windows-blue)](#)
10
+
11
+ ## Overview
8
12
 
9
13
  - Centralizes prompt/spec files in a global repo (`prompts_root`).
10
14
  - Creates link files in any project (hardlink -> symlink -> meta-link fallback).
11
15
  - Tracks link moves/renames in background (no full-disk scans).
12
16
  - Shows link status without scanning the filesystem.
13
17
 
14
- ## Install
18
+ ## Quickstart
15
19
 
16
- ### npm (Windows)
20
+ 1) Initialize a global prompt repo:
17
21
 
18
22
  ```bash
19
- npm i -g @yuchenrx/pmt-cli
23
+ pmt init
20
24
  ```
21
25
 
22
- This package publishes a Windows binary. For other platforms, build from source.
23
-
24
- ### From source
26
+ 2) Pick a prompt interactively and create a link:
25
27
 
26
28
  ```bash
27
- bun install
29
+ pmt link
28
30
  ```
29
31
 
30
- ## Run (global)
32
+ 3) Or link a specific prompt name:
31
33
 
32
34
  ```bash
33
- pmt
35
+ pmt link rules
34
36
  ```
35
37
 
36
- ## Run (dev)
38
+ 4) Or link to a specific target path:
37
39
 
38
40
  ```bash
39
- bun run pmt
41
+ pmt link rules docs/AGENTS.md
40
42
  ```
41
43
 
42
- ## Build
44
+ 5) Check link status:
43
45
 
44
46
  ```bash
45
- bun run build
47
+ pmt status
46
48
  ```
47
49
 
48
- Output: `dist/pmt.exe`
49
-
50
- ## Quick Start
50
+ ## Installation
51
51
 
52
- ### 1) Init
53
-
54
- Global prompt repo:
52
+ ### npm (Windows)
55
53
 
56
54
  ```bash
57
- pmt init
55
+ npm i -g @yuchenrx/pmt-cli
58
56
  ```
59
57
 
60
- `init` initializes the global prompt repo and runs `git init` under `prompts_root` if needed.
58
+ This package ships a Windows binary. For other platforms, build from source.
61
59
 
62
- ### 2) Link
60
+ ### From source
63
61
 
64
- Create a link file using the source filename as default:
62
+ Requirements: Bun.
65
63
 
66
64
  ```bash
67
- pmt link 规范
65
+ bun install
66
+ bun run pmt
68
67
  ```
69
68
 
70
- Create to a specific path:
69
+ ## Commands
71
70
 
72
- ```bash
73
- pmt link 规范 docs/AGENTS.md
74
- ```
71
+ Common commands:
75
72
 
76
- Interactive search:
73
+ - `pmt init`: initialize global config / prompt root
74
+ - `pmt set <name> <file> [scope]`: save a named source (path/URL)
75
+ - `pmt use <name> [scope]`: switch current source
76
+ - `pmt link [name] [file] [scope]`: create a link file (omit `name` to pick from a list; use `-i` to force interactive)
77
+ - `pmt status`: show link status
78
+ - `pmt open-config [scope]` (alias `open`): open config folder
79
+ - `pmt insert <file>`: insert placeholder
80
+ - `pmt apply [files...]`: apply current source to target files
77
81
 
78
- ```bash
79
- pmt link
80
- pmt link -i
81
- ```
82
-
83
- ### 3) Status
82
+ For full options:
84
83
 
85
84
  ```bash
86
- pmt status
85
+ pmt --help
86
+ pmt <command> --help
87
87
  ```
88
88
 
89
- Example output:
90
-
91
- ```
92
- docs/AGENTS.md -> C:\...\prompts\规范.md [hardlink] ok
93
- AGENTS.md -> C:\...\prompts\rules.md [meta] missing
94
- roots: C:\repo, D:\shared
95
- ```
96
-
97
- ## Config
89
+ ## Configuration
98
90
 
99
91
  Config files:
100
92
 
101
93
  - Project: `.promptslotrc.json`
102
94
  - Global: `~/.promptslotrc.json`
103
95
  - Link store: `.promptslot-links.json`
104
- - Link index: `.promptslot-index.json`
96
+ - Index: `.promptslot-index.json`
105
97
 
106
98
  Common keys:
107
99
 
108
100
  - `prompts_root`: global prompt repo directory
109
101
  - `prompts_dir`: project prompt directory (fallback)
110
102
  - `link_default`: default link filename
111
- - `sources`: name file path/URL
103
+ - `sources`: name -> file path/URL
112
104
  - `current`: active source name
113
105
 
106
+ Minimal example (`.promptslotrc.json`):
107
+
108
+ ```json
109
+ {
110
+ "prompts_root": "C:/Users/you/prompts",
111
+ "link_default": "AGENTS.md"
112
+ }
113
+ ```
114
+
114
115
  ## Link modes
115
116
 
116
117
  1) **Hardlink**: real-time sync at filesystem level
@@ -125,10 +126,38 @@ Meta-link is for read-only or cross-device cases (e.g. URL sources).
125
126
  - It only watches directories that already contain link files.
126
127
  - No full-disk scanning; updates `.promptslot-links.json` on rename.
127
128
 
128
- ## CLI name override
129
+ ## Build
129
130
 
130
131
  ```bash
131
- CLI_NAME=pmt
132
+ bun run build
132
133
  ```
133
134
 
135
+ Output: `dist/pmt.exe`
136
+
137
+ ## Release (Maintainers)
138
+
139
+ Releases are automated via GitHub Actions on tag push.
134
140
 
141
+ 1) Create and push a semver tag:
142
+
143
+ ```bash
144
+ git tag vX.Y.Z
145
+ git push origin vX.Y.Z
146
+ ```
147
+
148
+ 2) The workflow `.github/workflows/release.yml` will:
149
+
150
+ - run `bun test`
151
+ - build `dist/pmt.exe`
152
+ - create a GitHub Release with the binary attached
153
+ - publish the npm package (`npm publish --access public`)
154
+
155
+ Notes:
156
+
157
+ - If your npm account enforces 2FA for publishing, CI must use an npm token that can bypass OTP (Automation token or granular token with bypass 2FA enabled). Otherwise `npm publish` fails with `EOTP`.
158
+
159
+ ## CLI name override
160
+
161
+ ```bash
162
+ CLI_NAME=pmt
163
+ ```
package/README.zh.md CHANGED
@@ -2,96 +2,88 @@
2
2
 
3
3
  用于统一管理/同步规范与提示文件的 CLI。
4
4
 
5
- [English](./README.md)
5
+ [English](./README.md) | 简体中文
6
6
 
7
- ## 功能概述
7
+ [![npm version](https://img.shields.io/npm/v/@yuchenrx/pmt-cli)](https://www.npmjs.com/package/@yuchenrx/pmt-cli)
8
+ [![release](https://github.com/YuChenRX/pmt/actions/workflows/release.yml/badge.svg)](https://github.com/YuChenRX/pmt/actions/workflows/release.yml)
9
+ [![platform](https://img.shields.io/badge/platform-windows-blue)](#)
10
+
11
+ ## 概览
8
12
 
9
13
  - 全局集中管理 prompt/spec 文件(`prompts_root`)。
10
- - 在任意项目创建 link 文件(硬链接 软链接 meta-link 降级)。
14
+ - 在任意项目创建 link 文件(硬链接 -> 软链接 -> meta-link 降级)。
11
15
  - 后台追踪 link 重命名/移动(只监听已有目录,不全盘扫描)。
12
16
  - `status` 查看所有 link 状态(不扫描磁盘)。
13
17
 
14
- ## 安装
18
+ ## 快速开始
15
19
 
16
- ### npm(Windows)
20
+ 1)初始化全局 prompt 仓库:
17
21
 
18
22
  ```bash
19
- npm i -g @yuchenrx/pmt-cli
23
+ pmt init
20
24
  ```
21
25
 
22
- 该包发布的是 Windows 二进制文件,其他平台请从源码构建。
23
-
24
- ### 源码安装
26
+ 2)交互选择一个 prompt 并创建 link:
25
27
 
26
28
  ```bash
27
- bun install
29
+ pmt link
28
30
  ```
29
31
 
30
- ## 运行(全局)
32
+ 3)或指定 prompt 名:
31
33
 
32
34
  ```bash
33
- pmt
35
+ pmt link rules
34
36
  ```
35
37
 
36
- ## 运行(开发)
38
+ 4)或指定目标路径:
37
39
 
38
40
  ```bash
39
- bun run pmt
41
+ pmt link rules docs/AGENTS.md
40
42
  ```
41
43
 
42
- ## 构建
44
+ 5)查看 link 状态:
43
45
 
44
46
  ```bash
45
- bun run build
47
+ pmt status
46
48
  ```
47
49
 
48
- 输出:`dist/pmt.exe`
49
-
50
- ## 快速开始
51
-
52
- ### 1)初始化
50
+ ## 安装
53
51
 
54
- 全局 prompt 仓库:
52
+ ### npm(Windows)
55
53
 
56
54
  ```bash
57
- pmt init
55
+ npm i -g @yuchenrx/pmt-cli
58
56
  ```
59
57
 
60
- `init` 会在 `prompts_root` 下自动执行 `git init`(若未初始化)。
58
+ 该包发布的是 Windows 二进制文件,其他平台请从源码构建。
61
59
 
62
- ### 2)创建链接
60
+ ### 源码安装
63
61
 
64
- 默认使用源文件名作为目标名:
62
+ 要求:Bun。
65
63
 
66
64
  ```bash
67
- pmt link 规范
65
+ bun install
66
+ bun run pmt
68
67
  ```
69
68
 
70
- 指定目标路径:
69
+ ## 命令
71
70
 
72
- ```bash
73
- pmt link 规范 docs/AGENTS.md
74
- ```
71
+ 常用命令:
75
72
 
76
- 交互搜索:
73
+ - `pmt init`:初始化全局配置 / prompt root
74
+ - `pmt set <name> <file> [scope]`:保存一个命名 source(本地路径/URL)
75
+ - `pmt use <name> [scope]`:切换当前 source
76
+ - `pmt link [name] [file] [scope]`:创建 link 文件(不传 `name` 默认交互选择;`-i` 可强制交互)
77
+ - `pmt status`:查看 link 状态
78
+ - `pmt open-config [scope]`(别名 `open`):打开配置所在文件夹
79
+ - `pmt insert <file>`:插入占位符
80
+ - `pmt apply [files...]`:将当前 source 应用到目标文件
77
81
 
78
- ```bash
79
- pmt link
80
- pmt link -i
81
- ```
82
-
83
- ### 3)查看状态
82
+ 查看完整参数:
84
83
 
85
84
  ```bash
86
- pmt status
87
- ```
88
-
89
- 示例输出:
90
-
91
- ```
92
- docs/AGENTS.md -> C:\...\prompts\规范.md [hardlink] ok
93
- AGENTS.md -> C:\...\prompts\rules.md [meta] missing
94
- roots: C:\repo, D:\shared
85
+ pmt --help
86
+ pmt <command> --help
95
87
  ```
96
88
 
97
89
  ## 配置
@@ -108,9 +100,18 @@ roots: C:\repo, D:\shared
108
100
  - `prompts_root`:全局 prompt 仓库目录
109
101
  - `prompts_dir`:项目 prompts 目录(fallback)
110
102
  - `link_default`:默认 link 文件名
111
- - `sources`:name 文件路径/URL
103
+ - `sources`:name -> 文件路径/URL
112
104
  - `current`:当前激活的 source 名
113
105
 
106
+ 最小示例(`.promptslotrc.json`):
107
+
108
+ ```json
109
+ {
110
+ "prompts_root": "C:/Users/you/prompts",
111
+ "link_default": "AGENTS.md"
112
+ }
113
+ ```
114
+
114
115
  ## 链接模式
115
116
 
116
117
  1)**硬链接**:文件系统级实时同步
@@ -125,6 +126,36 @@ Meta-link 用于跨盘、只读或 URL 等场景。
125
126
  - 仅监听已有 link 文件所在目录。
126
127
  - 不做全盘扫描,仅更新 `.promptslot-links.json`。
127
128
 
129
+ ## 构建
130
+
131
+ ```bash
132
+ bun run build
133
+ ```
134
+
135
+ 输出:`dist/pmt.exe`
136
+
137
+ ## 发布(维护者)
138
+
139
+ 通过 GitHub Actions 自动发布:push tag 即触发。
140
+
141
+ 1)创建并推送 semver tag:
142
+
143
+ ```bash
144
+ git tag vX.Y.Z
145
+ git push origin vX.Y.Z
146
+ ```
147
+
148
+ 2)工作流 `.github/workflows/release.yml` 会:
149
+
150
+ - 执行 `bun test`
151
+ - 构建 `dist/pmt.exe`
152
+ - 创建 GitHub Release,并上传二进制
153
+ - 发布 npm 包(`npm publish --access public`)
154
+
155
+ 注意:
156
+
157
+ - 如果 npm 账号开启了“发布需要 2FA”,CI 必须使用可绕过 OTP 的 token(Automation token 或启用 bypass 2FA 的 granular token),否则 `npm publish` 会报 `EOTP`。
158
+
128
159
  ## CLI 名称覆盖
129
160
 
130
161
  ```bash
package/dist/pmt.exe CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuchenrx/pmt-cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "pmt": "dist/pmt.exe"
package/src/cli.ts CHANGED
@@ -1,31 +1,92 @@
1
1
  #!/usr/bin/env bun
2
- import { Command } from 'commander'
2
+ import { Command, CommanderError } from 'commander'
3
+ import { readFileSync } from 'node:fs'
4
+ import path from 'node:path'
3
5
  import { getCliName } from './cli-name'
4
6
  import { initCompletion } from './completion'
5
7
  import { registerCommands } from './commands'
6
8
  import { autoRepairLinks } from './link-repair'
7
9
 
8
- const program = new Command()
10
+ declare const PMT_VERSION: string | undefined
9
11
 
10
- program
11
- .name(getCliName())
12
- .description('占位符 prompt 管理 CLI')
13
- .helpOption('-h, --help', '显示帮助')
14
- .version('0.1.0', '-v, --version', '显示版本')
12
+ const readPackageVersion = (packageJsonPath: string) => {
13
+ try {
14
+ const raw = readFileSync(packageJsonPath, 'utf8')
15
+ const parsed = JSON.parse(raw) as { version?: unknown }
16
+ if (typeof parsed.version !== 'string') return null
17
+ const trimmed = parsed.version.trim()
18
+ return trimmed ? trimmed : null
19
+ } catch {
20
+ return null
21
+ }
22
+ }
23
+
24
+ const getCliVersion = () => {
25
+ if (typeof PMT_VERSION === 'string') {
26
+ const trimmed = PMT_VERSION.trim()
27
+ if (trimmed) return trimmed
28
+ }
29
+
30
+ const candidates = [
31
+ path.resolve(path.dirname(process.execPath), '..', 'package.json'),
32
+ path.resolve(process.cwd(), 'package.json'),
33
+ ]
15
34
 
16
- registerCommands(program)
17
- initCompletion()
35
+ for (const packageJsonPath of candidates) {
36
+ const version = readPackageVersion(packageJsonPath)
37
+ if (version) return version
38
+ }
18
39
 
19
- const args = process.argv.slice(2)
20
- if (!args.length) {
21
- program.outputHelp()
22
- process.exit(0)
40
+ return '0.0.0'
23
41
  }
24
42
 
25
- if (args.length === 1 && (args[0] === '-h' || args[0] === '--help')) {
26
- program.outputHelp()
27
- process.exit(0)
43
+ const run = async () => {
44
+ const program = new Command()
45
+
46
+ program
47
+ .name(getCliName())
48
+ .description('Prompt/spec manager CLI')
49
+ .helpOption('-h, --help', 'Show help')
50
+ .version(getCliVersion(), '-v, --version', 'Show version')
51
+ .showHelpAfterError('(use --help for additional information)')
52
+ .showSuggestionAfterError(true)
53
+ .configureHelp({ sortSubcommands: true, sortOptions: true })
54
+ .addHelpText(
55
+ 'afterAll',
56
+ `\nExamples:\n ${getCliName()} init\n ${getCliName()} link\n ${getCliName()} link rules docs/AGENTS.md\n`,
57
+ )
58
+ .exitOverride()
59
+
60
+ registerCommands(program)
61
+ initCompletion()
62
+
63
+ const args = process.argv.slice(2)
64
+ if (!args.length) {
65
+ program.outputHelp()
66
+ return
67
+ }
68
+
69
+ if (args.length === 1 && (args[0] === '-h' || args[0] === '--help')) {
70
+ program.outputHelp()
71
+ return
72
+ }
73
+
74
+ try {
75
+ await autoRepairLinks()
76
+ await program.parseAsync(process.argv)
77
+ } catch (err) {
78
+ if (err instanceof CommanderError) {
79
+ if (err.code === 'commander.helpDisplayed' || err.code === 'commander.version') {
80
+ return
81
+ }
82
+
83
+ process.exitCode = err.exitCode
84
+ return
85
+ }
86
+
87
+ console.error(err)
88
+ process.exitCode = 1
89
+ }
28
90
  }
29
91
 
30
- await autoRepairLinks()
31
- await program.parseAsync(process.argv)
92
+ await run()
@@ -14,7 +14,7 @@ const fail = (text: string) => {
14
14
  const requireMergedConfig = async () => {
15
15
  const merged = await loadMergedConfig()
16
16
  if (!merged.hasConfig) {
17
- fail(`未找到配置文件,请先运行: ${formatCommand('init')}`)
17
+ fail(`Config file not found. Run: ${formatCommand('init')}`)
18
18
  return null
19
19
  }
20
20
  return merged
@@ -22,7 +22,7 @@ const requireMergedConfig = async () => {
22
22
 
23
23
  export const insertPlaceholder = async (target?: string) => {
24
24
  if (!target) {
25
- fail(`用法: ${formatCommand('insert <file>')}`)
25
+ fail(`Usage: ${formatCommand('insert <file>')}`)
26
26
  return
27
27
  }
28
28
 
@@ -32,19 +32,19 @@ export const insertPlaceholder = async (target?: string) => {
32
32
  const resolved = resolvePath(target)
33
33
  const exists = await fileExists(resolved)
34
34
  if (!exists) {
35
- fail(`文件不存在: ${resolved}`)
35
+ fail(`File does not exist: ${resolved}`)
36
36
  return
37
37
  }
38
38
 
39
39
  const content = await fs.readFile(resolved, 'utf8')
40
40
  if (content.includes(merged.config.placeholder)) {
41
- log('文件已包含占位符')
41
+ log('File already contains placeholder')
42
42
  return
43
43
  }
44
44
 
45
45
  const next = content.replace(/\s*$/, `\n${merged.config.placeholder}\n`)
46
46
  await fs.writeFile(resolved, next)
47
- log(`已插入占位符: ${target}`)
47
+ log(`Inserted placeholder: ${target}`)
48
48
  }
49
49
 
50
50
  export const applyCurrent = async (targets: string[]) => {
@@ -53,20 +53,20 @@ export const applyCurrent = async (targets: string[]) => {
53
53
 
54
54
  const current = merged.config.current
55
55
  if (!current) {
56
- fail(`未设置当前值,请先运行: ${formatCommand('set <name> <file>')}`)
56
+ fail(`No current source set. Run: ${formatCommand('set <name> <file>')}`)
57
57
  return
58
58
  }
59
59
 
60
60
  const sourcePath = merged.resolvedSources[current]
61
61
  if (!sourcePath) {
62
- fail(`当前值未配置路径: ${current}`)
62
+ fail(`Current source has no path: ${current}`)
63
63
  return
64
64
  }
65
65
 
66
66
  if (!isUrl(sourcePath)) {
67
67
  const sourceExists = await fileExists(sourcePath)
68
68
  if (!sourceExists) {
69
- fail(`当前文件不存在: ${sourcePath}`)
69
+ fail(`Current source file does not exist: ${sourcePath}`)
70
70
  return
71
71
  }
72
72
  }
@@ -76,9 +76,9 @@ export const applyCurrent = async (targets: string[]) => {
76
76
  const changed = await applyToFiles(allTargets, value, merged.config)
77
77
 
78
78
  if (!changed.length) {
79
- log('未找到需要替换的文件')
79
+ log('No files to update')
80
80
  return
81
81
  }
82
82
 
83
- log(`已更新 ${changed.length} 个文件`)
83
+ log(`Updated ${changed.length} files`)
84
84
  }
@@ -11,8 +11,8 @@ export const completionInstall = async () => {
11
11
  installCompletion()
12
12
  } catch (error) {
13
13
  const message = error instanceof Error ? error.message : String(error)
14
- fail(`自动安装失败: ${message}`)
15
- fail(`可手动执行: ${formatCommand('completion show')}`)
14
+ fail(`Auto-install failed: ${message}`)
15
+ fail(`Run manually: ${formatCommand('completion show')}`)
16
16
  }
17
17
  }
18
18
 
@@ -21,7 +21,7 @@ export const completionCleanup = async () => {
21
21
  cleanupCompletion()
22
22
  } catch (error) {
23
23
  const message = error instanceof Error ? error.message : String(error)
24
- fail(`自动卸载失败: ${message}`)
24
+ fail(`Auto-uninstall failed: ${message}`)
25
25
  }
26
26
  }
27
27
 
@@ -26,7 +26,7 @@ const requireConfig = async (scope: ConfigScope) => {
26
26
  const config = await readConfigFile(scope)
27
27
  if (!config) {
28
28
  const configPath = getConfigPathByScope(scope)
29
- fail(`未找到配置文件: ${configPath}`)
29
+ fail(`Config file not found: ${configPath}`)
30
30
  return null
31
31
  }
32
32
  return config
@@ -72,8 +72,8 @@ const ensurePromptRepo = async (promptsRoot: string) => {
72
72
  const { spawnSync } = await import('node:child_process')
73
73
  const result = spawnSync('git', ['init'], { cwd: promptsRoot, encoding: 'utf8' })
74
74
  if (result.status !== 0) {
75
- const message = result.stderr || result.stdout || 'git init 失败'
76
- log(`初始化 git 失败: ${message.trim()}`)
75
+ const message = result.stderr || result.stdout || 'git init failed'
76
+ log(`git init failed: ${message.trim()}`)
77
77
  }
78
78
  }
79
79
 
@@ -81,22 +81,22 @@ export const initConfig = async (scope: ConfigScope) => {
81
81
  const configPath = getConfigPathByScope(scope)
82
82
  const exists = await fileExists(configPath)
83
83
  if (exists) {
84
- log(`配置已存在: ${configPath}`)
84
+ log(`Config already exists: ${configPath}`)
85
85
  return
86
86
  }
87
87
 
88
88
  const config = createDefaultConfig()
89
89
  if (scope === 'global') {
90
90
  const rootDefault = '.promptslot/prompts'
91
- const promptRoot = await ask('全局 prompts 目录', rootDefault)
91
+ const promptRoot = await ask('Global prompts directory', rootDefault)
92
92
  config.prompts_root = promptRoot
93
93
  } else {
94
94
  const dirDefault = 'prompts'
95
- const promptDir = await ask('项目 prompts 目录', dirDefault)
95
+ const promptDir = await ask('Project prompts directory', dirDefault)
96
96
  config.prompts_dir = promptDir
97
97
  }
98
98
 
99
- const linkDefault = await ask('默认 link 文件名', config.link_default ?? 'AGENTS.md')
99
+ const linkDefault = await ask('Default link filename', config.link_default ?? 'AGENTS.md')
100
100
  config.link_default = linkDefault
101
101
 
102
102
 
@@ -116,12 +116,12 @@ export const initConfig = async (scope: ConfigScope) => {
116
116
  await ensurePromptRepo(promptsRoot)
117
117
  }
118
118
 
119
- log(`已生成配置: ${configPath}`)
119
+ log(`Created config: ${configPath}`)
120
120
  }
121
121
 
122
122
  export const setSource = async (name: string, source: string, scope: ConfigScope) => {
123
123
  if (!name || !source) {
124
- fail(`用法: ${formatCommand('set <name> <file>')}`)
124
+ fail(`Usage: ${formatCommand('set <name> <file>')}`)
125
125
  return
126
126
  }
127
127
 
@@ -132,7 +132,7 @@ export const setSource = async (name: string, source: string, scope: ConfigScope
132
132
  loaded.config.sources[name] = source
133
133
  loaded.config.current = name
134
134
  await writeConfigFile(scope, loaded.config)
135
- log(`已设置 ${name} -> ${source}`)
135
+ log(`Set ${name} -> ${source}`)
136
136
  return
137
137
  }
138
138
 
@@ -155,15 +155,15 @@ export const setSource = async (name: string, source: string, scope: ConfigScope
155
155
  if (!resolved) {
156
156
  const suggestions = await listPromptSuggestions(promptsRoot)
157
157
  if (suggestions.length) {
158
- log(`可选文件: ${suggestions.join(', ')}`)
158
+ log(`Available files: ${suggestions.join(', ')}`)
159
159
  }
160
- fail(`文件不存在: ${baseResolved}`)
160
+ fail(`File does not exist: ${baseResolved}`)
161
161
  return
162
162
  }
163
163
 
164
164
  const isFilePath = await isFile(resolved)
165
165
  if (!isFilePath) {
166
- fail(`目标不是文件: ${resolved}`)
166
+ fail(`Target is not a file: ${resolved}`)
167
167
  return
168
168
  }
169
169
  let relativeToPrompts: string | null = null
@@ -179,12 +179,12 @@ export const setSource = async (name: string, source: string, scope: ConfigScope
179
179
  loaded.config.sources[name] = stored
180
180
  loaded.config.current = name
181
181
  await writeConfigFile(scope, loaded.config)
182
- log(`已设置 ${name} -> ${stored}`)
182
+ log(`Set ${name} -> ${stored}`)
183
183
  }
184
184
 
185
185
  export const useSource = async (name: string, scope: ConfigScope) => {
186
186
  if (!name) {
187
- fail(`用法: ${formatCommand('use <name>')}`)
187
+ fail(`Usage: ${formatCommand('use <name>')}`)
188
188
  return
189
189
  }
190
190
 
@@ -193,24 +193,24 @@ export const useSource = async (name: string, scope: ConfigScope) => {
193
193
  if (!loaded) return
194
194
 
195
195
  if (!loaded.config.sources[name]) {
196
- fail(`未找到命名值: ${name}`)
196
+ fail(`Source not found: ${name}`)
197
197
  return
198
198
  }
199
199
 
200
200
  loaded.config.current = name
201
201
  await writeConfigFile(scope, loaded.config)
202
- log(`已切换到 ${name}`)
202
+ log(`Switched to ${name}`)
203
203
  return
204
204
  }
205
205
 
206
206
  const merged = await loadMergedConfig()
207
207
  if (!merged.hasConfig) {
208
- fail(`未找到配置文件,请先运行: ${formatCommand('init')}`)
208
+ fail(`Config file not found. Run: ${formatCommand('init')}`)
209
209
  return
210
210
  }
211
211
 
212
212
  if (!merged.resolvedSources[name]) {
213
- fail(`未找到命名值: ${name}`)
213
+ fail(`Source not found: ${name}`)
214
214
  return
215
215
  }
216
216
 
@@ -218,19 +218,19 @@ export const useSource = async (name: string, scope: ConfigScope) => {
218
218
  if (!project) return
219
219
  project.config.current = name
220
220
  await writeConfigFile(scope, project.config)
221
- log(`已切换到 ${name}`)
221
+ log(`Switched to ${name}`)
222
222
  }
223
223
 
224
224
  export const listSources = async () => {
225
225
  const merged = await loadMergedConfig()
226
226
  if (!merged.hasConfig) {
227
- fail(`未找到配置文件,请先运行: ${formatCommand('init')}`)
227
+ fail(`Config file not found. Run: ${formatCommand('init')}`)
228
228
  return
229
229
  }
230
230
 
231
231
  const entries = Object.entries(merged.resolvedSources)
232
232
  if (!entries.length) {
233
- log('未配置任何 source')
233
+ log('No sources configured')
234
234
  return
235
235
  }
236
236
 
@@ -64,26 +64,26 @@ const resolveLinkSource = async (root: string, name: string) => {
64
64
 
65
65
  const suggestions = await listPromptNames(root)
66
66
  if (suggestions.length) {
67
- log(`可选文件: ${suggestions.join(', ')}`)
67
+ log(`Available files: ${suggestions.join(', ')}`)
68
68
  }
69
69
  return null
70
70
  }
71
71
 
72
72
  export const linkFile = async (name: string, target?: string, scope?: ConfigScope) => {
73
73
  if (!name) {
74
- fail(`用法: ${formatCommand('link <name> [file] [scope]')}`)
74
+ fail(`Usage: ${formatCommand('link <name> [file] [scope]')}`)
75
75
  return
76
76
  }
77
77
 
78
78
  const root = await resolveRoot(scope)
79
79
  if (!root && !isUrl(name)) {
80
- fail('未配置 prompts_root,请先运行 init 配置全局目录')
80
+ fail(`prompts_root is not configured. Run ${formatCommand('init')} first.`)
81
81
  return
82
82
  }
83
83
 
84
84
  const sourcePath = await resolveLinkSource(root ?? '', name)
85
85
  if (!sourcePath) {
86
- fail(`未找到 prompt 文件: ${name}`)
86
+ fail(`Prompt not found: ${name}`)
87
87
  return
88
88
  }
89
89
 
@@ -93,7 +93,7 @@ export const linkFile = async (name: string, target?: string, scope?: ConfigScop
93
93
  } else {
94
94
  const defaultName = await resolveDefaultTargetName(sourcePath)
95
95
  if (!defaultName) {
96
- fail('无法确定目标文件名,请显式传入目标路径')
96
+ fail('Unable to determine target filename. Provide an explicit target path.')
97
97
  return
98
98
  }
99
99
  baseTarget = resolveTargetPath(defaultName, defaultName)
@@ -102,7 +102,7 @@ export const linkFile = async (name: string, target?: string, scope?: ConfigScop
102
102
  const targetPath = ensureTargetExtension(baseTarget, sourcePath)
103
103
  const exists = await fileExists(targetPath)
104
104
  if (exists) {
105
- fail(`目标文件已存在: ${targetPath}`)
105
+ fail(`Target file already exists: ${targetPath}`)
106
106
  return
107
107
  }
108
108
 
@@ -112,15 +112,15 @@ export const linkFile = async (name: string, target?: string, scope?: ConfigScop
112
112
  await recordLink(sourcePath, targetPath, process.cwd())
113
113
 
114
114
  if (mode === 'hardlink') {
115
- log(`已创建硬链接: ${targetPath}`)
115
+ log(`Created hardlink: ${targetPath}`)
116
116
  return
117
117
  }
118
118
 
119
119
  if (mode === 'symlink') {
120
- log(`已创建软链接: ${targetPath}`)
120
+ log(`Created symlink: ${targetPath}`)
121
121
  return
122
122
  }
123
123
 
124
- log(`已创建 link 文件: ${targetPath}`)
124
+ log(`Created link file: ${targetPath}`)
125
125
  void ensureRenameDaemon()
126
126
  }
@@ -38,7 +38,7 @@ const resolveConfigPath = async (scope?: ConfigScope) => {
38
38
  export const openConfigFolder = async (scope?: ConfigScope) => {
39
39
  const config = await resolveConfigPath(scope)
40
40
  if (!config) {
41
- fail(`未找到配置文件,请先运行: ${formatCommand('init')}`)
41
+ fail(`Config file not found. Run: ${formatCommand('init')}`)
42
42
  return
43
43
  }
44
44
 
@@ -19,7 +19,7 @@ const pickFromList = async (names: string[]) => {
19
19
 
20
20
  const prompt = new AutoComplete({
21
21
  name: 'prompt',
22
- message: '选择 prompt 文件',
22
+ message: 'Select a prompt file',
23
23
  choices: names,
24
24
  limit: 10,
25
25
  })
@@ -32,19 +32,19 @@ const pickFromList = async (names: string[]) => {
32
32
  export const searchAndLink = async (target?: string, scope?: ConfigScope) => {
33
33
  const info = await loadPromptRoot(scope)
34
34
  if (!info) {
35
- fail(`未配置 prompts_root,请先运行: ${formatCommand('init')}`)
35
+ fail(`prompts_root is not configured. Run: ${formatCommand('init')} (this creates your global config and prompts_root)`)
36
36
  return
37
37
  }
38
38
 
39
39
  const names = await listFiles(info.root)
40
40
  if (!names.length) {
41
- log('未找到任何 prompt 文件')
41
+ log('No prompt files found')
42
42
  return
43
43
  }
44
44
 
45
45
  const selected = await pickFromList(names.sort())
46
46
  if (!selected) {
47
- log('未选择任何文件')
47
+ log('No file selected')
48
48
  return
49
49
  }
50
50
 
@@ -9,7 +9,7 @@ export const statusLinks = async () => {
9
9
  const links = await readLinks()
10
10
  const entries = Object.entries(links)
11
11
  if (!entries.length) {
12
- log('未发现 link 记录')
12
+ log('No links found')
13
13
  return
14
14
  }
15
15
 
package/src/commands.ts CHANGED
@@ -13,17 +13,17 @@ import type { ConfigScope } from './config'
13
13
  export const registerCommands = (program: Command) => {
14
14
  program
15
15
  .command('init')
16
- .description('初始化全局配置')
16
+ .description('Initialize global config')
17
17
  .action(async () => {
18
18
  await initConfig('global')
19
19
  })
20
20
 
21
21
  program
22
22
  .command('set')
23
- .description('设置并切换值')
24
- .argument('<name>', '命名 key,如 spec')
25
- .argument('<file>', 'prompt 文件路径')
26
- .argument('[scope]', 'project | global,默认 global')
23
+ .description('Save and switch source')
24
+ .argument('<name>', 'Source name, e.g. spec')
25
+ .argument('<file>', 'Prompt file path or URL')
26
+ .argument('[scope]', 'project | global (default: global)')
27
27
  .action(async (name, file, scopeInput) => {
28
28
  const { scope, error } = resolveScope(scopeInput)
29
29
  if (error) {
@@ -36,9 +36,9 @@ export const registerCommands = (program: Command) => {
36
36
 
37
37
  program
38
38
  .command('use')
39
- .description('切换到已保存的值')
40
- .argument('<name>', '命名 key,如 spec')
41
- .argument('[scope]', 'project | global,默认 global')
39
+ .description('Switch to a saved source')
40
+ .argument('<name>', 'Source name, e.g. spec')
41
+ .argument('[scope]', 'project | global (default: global)')
42
42
  .action(async (name, scopeInput) => {
43
43
  const { scope, error } = resolveScope(scopeInput)
44
44
  if (error) {
@@ -52,27 +52,27 @@ export const registerCommands = (program: Command) => {
52
52
 
53
53
  program
54
54
  .command('insert')
55
- .description('插入占位符')
56
- .argument('<file>', '目标文件路径')
55
+ .description('Insert placeholder')
56
+ .argument('<file>', 'Target file path')
57
57
  .action(async (file) => {
58
58
  await insertPlaceholder(file)
59
59
  })
60
60
 
61
61
  program
62
62
  .command('apply')
63
- .description('应用当前值')
64
- .argument('[files...]', '指定要替换的文件列表')
63
+ .description('Apply current source')
64
+ .argument('[files...]', 'Files to apply (optional)')
65
65
  .action(async (files: string[] = []) => {
66
66
  await applyCurrent(files)
67
67
  })
68
68
 
69
69
  program
70
70
  .command('link')
71
- .description('创建 link 文件')
72
- .argument('[name]', 'prompt 文件名(可省略进入交互)')
73
- .argument('[file]', '目标文件路径')
74
- .argument('[scope]', 'project | global,默认 global')
75
- .option('-i', '交互搜索')
71
+ .description('Create link file')
72
+ .argument('[name]', 'Prompt name (omit for interactive)')
73
+ .argument('[file]', 'Target file path')
74
+ .argument('[scope]', 'project | global (default: global)')
75
+ .option('-i', 'Interactive search')
76
76
  .action(async (name, file, scopeInput, options) => {
77
77
  const interactive = options.i || !name || name === '-'
78
78
  let targetFile = file
@@ -111,10 +111,10 @@ export const registerCommands = (program: Command) => {
111
111
  })
112
112
 
113
113
  program
114
- .command('search')
115
- .description('交互搜索并创建 link')
116
- .argument('[file]', '目标文件路径')
117
- .argument('[scope]', 'project | global,默认 global')
114
+ .command('search', { hidden: true })
115
+ .description('Alias for `link -i` (deprecated)')
116
+ .argument('[file]', 'Target file path')
117
+ .argument('[scope]', 'project | global (default: global)')
118
118
  .action(async (file, scopeInput) => {
119
119
  let scope: ConfigScope | undefined
120
120
  if (scopeInput) {
@@ -131,8 +131,8 @@ export const registerCommands = (program: Command) => {
131
131
 
132
132
  program
133
133
  .command('open-config')
134
- .description('打开配置所在文件夹')
135
- .argument('[scope]', 'project | global,默认 global')
134
+ .description('Open config folder')
135
+ .argument('[scope]', 'project | global (default: global)')
136
136
  .action(async (scopeInput) => {
137
137
  let scope: ConfigScope | undefined
138
138
  if (scopeInput) {
@@ -150,14 +150,14 @@ export const registerCommands = (program: Command) => {
150
150
 
151
151
  program
152
152
  .command('status')
153
- .description('查看 link 状态')
153
+ .description('Show link status')
154
154
  .action(async () => {
155
155
  await statusLinks()
156
156
  })
157
157
 
158
158
  program
159
159
  .command('rename-daemon', { hidden: true })
160
- .description('内部重命名监听')
160
+ .description('Internal rename watcher')
161
161
  .action(async () => {
162
162
  await runRenameDaemon()
163
163
  })
@@ -5,7 +5,7 @@ export const readSourceText = async (sourcePath: string) => {
5
5
  if (isUrl(sourcePath)) {
6
6
  const response = await fetch(sourcePath)
7
7
  if (!response.ok) {
8
- throw new Error(`无法读取远程文件: ${response.status}`)
8
+ throw new Error(`Failed to fetch remote file: ${response.status}`)
9
9
  }
10
10
  return response.text()
11
11
  }
@@ -21,7 +21,7 @@ const readSource = async (sourcePath: string) => {
21
21
  const readRemote = async (sourcePath: string) => {
22
22
  const response = await fetch(sourcePath)
23
23
  if (!response.ok) {
24
- throw new Error(`无法读取远程文件: ${response.status}`)
24
+ throw new Error(`Failed to fetch remote file: ${response.status}`)
25
25
  }
26
26
  const content = await response.text()
27
27
  return { content, hash: hashContent(content) }
@@ -77,7 +77,7 @@ export const pullLink = async (sourcePath: string, targetPath: string) => {
77
77
  const resolveConflict = (sourceHash: string, linkHash: string | null, payloadHash: string) => {
78
78
  if (!linkHash) return null
79
79
  if (sourceHash !== linkHash && payloadHash !== linkHash) {
80
- return '检测到冲突,请先 pull 更新后再写入'
80
+ return 'Conflict detected. Pull latest changes before writing.'
81
81
  }
82
82
  return null
83
83
  }
@@ -85,20 +85,20 @@ const resolveConflict = (sourceHash: string, linkHash: string | null, payloadHas
85
85
  const resolveScopeChange = (sourceHash: string, linkHash: string | null, payloadHash: string) => {
86
86
  if (!linkHash) return null
87
87
  if (sourceHash !== linkHash && payloadHash === linkHash) {
88
- return '源文件已更新,请先 pull 同步'
88
+ return 'Source updated upstream. Pull latest changes first.'
89
89
  }
90
90
  return null
91
91
  }
92
92
 
93
93
  export const pushLink = async (sourcePath: string, targetPath: string) => {
94
94
  if (isRemote(sourcePath)) {
95
- throw new Error('远程文件不支持写入')
95
+ throw new Error('Remote sources are read-only.')
96
96
  }
97
97
 
98
98
  const lockPath = getLockPath(sourcePath)
99
99
  const link = await readLinkMeta(targetPath)
100
100
  if (!link) {
101
- throw new Error('文件不是 link 文件')
101
+ throw new Error('Target is not a link file.')
102
102
  }
103
103
 
104
104
  const payload = stripLinkHeader(link.content)
@@ -128,13 +128,13 @@ export const pushLink = async (sourcePath: string, targetPath: string) => {
128
128
 
129
129
  export const syncLink = async (sourcePath: string, targetPath: string) => {
130
130
  if (isRemote(sourcePath)) {
131
- throw new Error('远程文件不支持写入')
131
+ throw new Error('Remote sources are read-only.')
132
132
  }
133
133
 
134
134
  const lockPath = getLockPath(sourcePath)
135
135
  const link = await readLinkMeta(targetPath)
136
136
  if (!link) {
137
- throw new Error('文件不是 link 文件')
137
+ throw new Error('Target is not a link file.')
138
138
  }
139
139
 
140
140
  const payload = stripLinkHeader(link.content)
package/src/lock.ts CHANGED
@@ -29,7 +29,7 @@ export const waitForUnlock = async (lockPath: string) => {
29
29
 
30
30
  const elapsed = Date.now() - start
31
31
  if (elapsed >= LOCK_TIMEOUT_MS) {
32
- throw new Error('写入锁等待超时,请稍后重试')
32
+ throw new Error('Timed out waiting for write lock. Please try again.')
33
33
  }
34
34
 
35
35
  await sleep(LOCK_INTERVAL_MS)
@@ -45,7 +45,7 @@ export const withLock = async <T>(lockPath: string, task: () => Promise<T>) => {
45
45
 
46
46
  const elapsed = Date.now() - start
47
47
  if (elapsed >= LOCK_TIMEOUT_MS) {
48
- throw new Error('写入锁等待超时,请稍后重试')
48
+ throw new Error('Timed out waiting for write lock. Please try again.')
49
49
  }
50
50
 
51
51
  await sleep(LOCK_INTERVAL_MS)
package/src/scope.ts CHANGED
@@ -9,5 +9,5 @@ export const resolveScope = (input?: string): ScopeResult => {
9
9
  if (!input) return { scope: 'global', error: null }
10
10
  if (input === 'global') return { scope: 'global', error: null }
11
11
  if (input === 'project') return { scope: 'project', error: null }
12
- return { scope: 'global', error: 'scope 必须是 project global' }
12
+ return { scope: 'global', error: 'scope must be project or global' }
13
13
  }