@yuchenrx/pmt-cli 0.1.2
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/AGENTS.md +128 -0
- package/README.md +134 -0
- package/README.zh.md +132 -0
- package/dist/pmt.exe +0 -0
- package/package.json +44 -0
- package/src/apply.ts +59 -0
- package/src/cli-name.ts +13 -0
- package/src/cli.ts +31 -0
- package/src/command-apply.ts +84 -0
- package/src/command-completion.ts +31 -0
- package/src/command-config.ts +244 -0
- package/src/command-link.ts +126 -0
- package/src/command-open.ts +47 -0
- package/src/command-search.ts +52 -0
- package/src/command-status.ts +27 -0
- package/src/commands.ts +165 -0
- package/src/completion.ts +96 -0
- package/src/config.ts +159 -0
- package/src/content-source.ts +14 -0
- package/src/helpers.ts +95 -0
- package/src/link-index.ts +101 -0
- package/src/link-manager.ts +180 -0
- package/src/link-repair.ts +30 -0
- package/src/link.ts +82 -0
- package/src/links-store.ts +47 -0
- package/src/lock.ts +59 -0
- package/src/prompt-root.ts +27 -0
- package/src/prompt.ts +20 -0
- package/src/rename-daemon.ts +98 -0
- package/src/rename-watcher.ts +110 -0
- package/src/scope.ts +13 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import os from 'node:os'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import {
|
|
4
|
+
createDefaultConfig,
|
|
5
|
+
ensurePromptsRoot,
|
|
6
|
+
getGlobalConfigPath,
|
|
7
|
+
getProjectConfigPath,
|
|
8
|
+
loadMergedConfig,
|
|
9
|
+
readConfigFile,
|
|
10
|
+
resolvePromptsRoot,
|
|
11
|
+
writeConfigFile,
|
|
12
|
+
type ConfigScope,
|
|
13
|
+
} from './config'
|
|
14
|
+
import { formatCommand } from './cli-name'
|
|
15
|
+
import { ask } from './prompt'
|
|
16
|
+
import { fileExists, isFile, isSubpath, isUrl, listFiles, resolvePath } from './helpers'
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
const log = (text: string) => console.log(text)
|
|
20
|
+
const fail = (text: string) => {
|
|
21
|
+
console.error(text)
|
|
22
|
+
process.exitCode = 1
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const requireConfig = async (scope: ConfigScope) => {
|
|
26
|
+
const config = await readConfigFile(scope)
|
|
27
|
+
if (!config) {
|
|
28
|
+
const configPath = getConfigPathByScope(scope)
|
|
29
|
+
fail(`未找到配置文件: ${configPath}`)
|
|
30
|
+
return null
|
|
31
|
+
}
|
|
32
|
+
return config
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const getConfigPathByScope = (scope: ConfigScope) => {
|
|
36
|
+
if (scope === 'global') return getGlobalConfigPath()
|
|
37
|
+
return getProjectConfigPath()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const getBaseDir = (scope: ConfigScope) => {
|
|
41
|
+
if (scope === 'global') return os.homedir()
|
|
42
|
+
return process.cwd()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const resolvePromptsPath = (base: string | null, source: string) => {
|
|
46
|
+
if (!base) return null
|
|
47
|
+
return resolvePath(source, base)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const resolveExisting = (
|
|
51
|
+
primary: string,
|
|
52
|
+
primaryExists: boolean,
|
|
53
|
+
fallback: string | null,
|
|
54
|
+
fallbackExists: boolean,
|
|
55
|
+
) => {
|
|
56
|
+
if (primaryExists) return primary
|
|
57
|
+
if (fallbackExists && fallback) return fallback
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const listPromptSuggestions = async (root: string | null) => {
|
|
62
|
+
if (!root) return []
|
|
63
|
+
const files = await listFiles(root)
|
|
64
|
+
return files.sort()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const ensurePromptRepo = async (promptsRoot: string) => {
|
|
68
|
+
const gitDir = resolvePath('.git', promptsRoot)
|
|
69
|
+
const exists = await fileExists(gitDir)
|
|
70
|
+
if (exists) return
|
|
71
|
+
|
|
72
|
+
const { spawnSync } = await import('node:child_process')
|
|
73
|
+
const result = spawnSync('git', ['init'], { cwd: promptsRoot, encoding: 'utf8' })
|
|
74
|
+
if (result.status !== 0) {
|
|
75
|
+
const message = result.stderr || result.stdout || 'git init 失败'
|
|
76
|
+
log(`初始化 git 失败: ${message.trim()}`)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const initConfig = async (scope: ConfigScope) => {
|
|
81
|
+
const configPath = getConfigPathByScope(scope)
|
|
82
|
+
const exists = await fileExists(configPath)
|
|
83
|
+
if (exists) {
|
|
84
|
+
log(`配置已存在: ${configPath}`)
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const config = createDefaultConfig()
|
|
89
|
+
if (scope === 'global') {
|
|
90
|
+
const rootDefault = '.promptslot/prompts'
|
|
91
|
+
const promptRoot = await ask('全局 prompts 目录', rootDefault)
|
|
92
|
+
config.prompts_root = promptRoot
|
|
93
|
+
} else {
|
|
94
|
+
const dirDefault = 'prompts'
|
|
95
|
+
const promptDir = await ask('项目 prompts 目录', dirDefault)
|
|
96
|
+
config.prompts_dir = promptDir
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const linkDefault = await ask('默认 link 文件名', config.link_default ?? 'AGENTS.md')
|
|
100
|
+
config.link_default = linkDefault
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
if (scope === 'project') {
|
|
104
|
+
const defaultSpec = resolvePath('规范.md')
|
|
105
|
+
const hasSpec = await fileExists(defaultSpec)
|
|
106
|
+
if (hasSpec) {
|
|
107
|
+
config.sources.spec = '规范.md'
|
|
108
|
+
config.current = 'spec'
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
await writeConfigFile(scope, config)
|
|
113
|
+
const promptsRoot = await ensurePromptsRoot(config, getBaseDir(scope))
|
|
114
|
+
|
|
115
|
+
if (scope === 'global' && promptsRoot) {
|
|
116
|
+
await ensurePromptRepo(promptsRoot)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
log(`已生成配置: ${configPath}`)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export const setSource = async (name: string, source: string, scope: ConfigScope) => {
|
|
123
|
+
if (!name || !source) {
|
|
124
|
+
fail(`用法: ${formatCommand('set <name> <file>')}`)
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const loaded = await requireConfig(scope)
|
|
129
|
+
if (!loaded) return
|
|
130
|
+
|
|
131
|
+
if (isUrl(source)) {
|
|
132
|
+
loaded.config.sources[name] = source
|
|
133
|
+
loaded.config.current = name
|
|
134
|
+
await writeConfigFile(scope, loaded.config)
|
|
135
|
+
log(`已设置 ${name} -> ${source}`)
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const baseDir = getBaseDir(scope)
|
|
140
|
+
const promptsRoot = resolvePromptsRoot(loaded.config, loaded.baseDir)
|
|
141
|
+
const baseResolved = resolvePath(source, baseDir)
|
|
142
|
+
const promptsResolved = resolvePromptsPath(promptsRoot, source)
|
|
143
|
+
const baseExists = await fileExists(baseResolved)
|
|
144
|
+
let promptsExists = false
|
|
145
|
+
if (promptsResolved) {
|
|
146
|
+
promptsExists = await fileExists(promptsResolved)
|
|
147
|
+
}
|
|
148
|
+
const resolved = resolveExisting(
|
|
149
|
+
baseResolved,
|
|
150
|
+
baseExists,
|
|
151
|
+
promptsResolved,
|
|
152
|
+
promptsExists,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
if (!resolved) {
|
|
156
|
+
const suggestions = await listPromptSuggestions(promptsRoot)
|
|
157
|
+
if (suggestions.length) {
|
|
158
|
+
log(`可选文件: ${suggestions.join(', ')}`)
|
|
159
|
+
}
|
|
160
|
+
fail(`文件不存在: ${baseResolved}`)
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const isFilePath = await isFile(resolved)
|
|
165
|
+
if (!isFilePath) {
|
|
166
|
+
fail(`目标不是文件: ${resolved}`)
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
let relativeToPrompts: string | null = null
|
|
170
|
+
if (promptsRoot) {
|
|
171
|
+
relativeToPrompts = path.relative(promptsRoot, resolved)
|
|
172
|
+
}
|
|
173
|
+
const relativeToBase = path.relative(baseDir, resolved)
|
|
174
|
+
let stored = relativeToBase
|
|
175
|
+
if (promptsRoot && relativeToPrompts && isSubpath(promptsRoot, resolved)) {
|
|
176
|
+
stored = relativeToPrompts
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
loaded.config.sources[name] = stored
|
|
180
|
+
loaded.config.current = name
|
|
181
|
+
await writeConfigFile(scope, loaded.config)
|
|
182
|
+
log(`已设置 ${name} -> ${stored}`)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export const useSource = async (name: string, scope: ConfigScope) => {
|
|
186
|
+
if (!name) {
|
|
187
|
+
fail(`用法: ${formatCommand('use <name>')}`)
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (scope === 'global') {
|
|
192
|
+
const loaded = await requireConfig(scope)
|
|
193
|
+
if (!loaded) return
|
|
194
|
+
|
|
195
|
+
if (!loaded.config.sources[name]) {
|
|
196
|
+
fail(`未找到命名值: ${name}`)
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
loaded.config.current = name
|
|
201
|
+
await writeConfigFile(scope, loaded.config)
|
|
202
|
+
log(`已切换到 ${name}`)
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const merged = await loadMergedConfig()
|
|
207
|
+
if (!merged.hasConfig) {
|
|
208
|
+
fail(`未找到配置文件,请先运行: ${formatCommand('init')}`)
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (!merged.resolvedSources[name]) {
|
|
213
|
+
fail(`未找到命名值: ${name}`)
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const project = await requireConfig(scope)
|
|
218
|
+
if (!project) return
|
|
219
|
+
project.config.current = name
|
|
220
|
+
await writeConfigFile(scope, project.config)
|
|
221
|
+
log(`已切换到 ${name}`)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export const listSources = async () => {
|
|
225
|
+
const merged = await loadMergedConfig()
|
|
226
|
+
if (!merged.hasConfig) {
|
|
227
|
+
fail(`未找到配置文件,请先运行: ${formatCommand('init')}`)
|
|
228
|
+
return
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const entries = Object.entries(merged.resolvedSources)
|
|
232
|
+
if (!entries.length) {
|
|
233
|
+
log('未配置任何 source')
|
|
234
|
+
return
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
entries.forEach(([name, source]) => {
|
|
238
|
+
let tag = ''
|
|
239
|
+
if (merged.config.current === name) {
|
|
240
|
+
tag = ' (current)'
|
|
241
|
+
}
|
|
242
|
+
log(`${name}: ${source}${tag}`)
|
|
243
|
+
})
|
|
244
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { formatCommand } from './cli-name'
|
|
4
|
+
import type { ConfigScope } from './config'
|
|
5
|
+
import { ensureTargetExtension, fileExists, getUrlBasename, isUrl, listFiles, parseContentDisposition } from './helpers'
|
|
6
|
+
import { createAutoLink, resolvePromptFile } from './link-manager'
|
|
7
|
+
import { loadPromptRoot } from './prompt-root'
|
|
8
|
+
import { upsertLink } from './links-store'
|
|
9
|
+
import { recordLink } from './link-index'
|
|
10
|
+
import { ensureRenameDaemon } from './rename-daemon'
|
|
11
|
+
|
|
12
|
+
const log = (text: string) => console.log(text)
|
|
13
|
+
const fail = (text: string) => {
|
|
14
|
+
console.error(text)
|
|
15
|
+
process.exitCode = 1
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const resolveTargetPath = (targetInput: string | undefined, fallback: string) => {
|
|
19
|
+
let target = fallback
|
|
20
|
+
if (targetInput) {
|
|
21
|
+
const trimmed = targetInput.trim()
|
|
22
|
+
if (trimmed) {
|
|
23
|
+
target = trimmed
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return path.resolve(process.cwd(), target)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const listPromptNames = async (root: string) => {
|
|
30
|
+
const files = await listFiles(root)
|
|
31
|
+
return files.sort()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const resolveRoot = async (scope?: ConfigScope) => {
|
|
35
|
+
const info = await loadPromptRoot(scope)
|
|
36
|
+
if (info) return info.root
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const resolveRemoteFilename = async (url: string) => {
|
|
41
|
+
const response = await fetch(url, { method: 'HEAD' }).catch(() => null)
|
|
42
|
+
if (!response) return null
|
|
43
|
+
if (!response.ok) return null
|
|
44
|
+
|
|
45
|
+
const header = response.headers.get('content-disposition')
|
|
46
|
+
return parseContentDisposition(header)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const resolveDefaultTargetName = async (sourcePath: string) => {
|
|
50
|
+
if (isUrl(sourcePath)) {
|
|
51
|
+
const remoteName = await resolveRemoteFilename(sourcePath)
|
|
52
|
+
if (remoteName) return remoteName
|
|
53
|
+
return getUrlBasename(sourcePath)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return path.basename(sourcePath)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const resolveLinkSource = async (root: string, name: string) => {
|
|
60
|
+
if (isUrl(name)) return name
|
|
61
|
+
|
|
62
|
+
const source = await resolvePromptFile(root, name)
|
|
63
|
+
if (source) return source
|
|
64
|
+
|
|
65
|
+
const suggestions = await listPromptNames(root)
|
|
66
|
+
if (suggestions.length) {
|
|
67
|
+
log(`可选文件: ${suggestions.join(', ')}`)
|
|
68
|
+
}
|
|
69
|
+
return null
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const linkFile = async (name: string, target?: string, scope?: ConfigScope) => {
|
|
73
|
+
if (!name) {
|
|
74
|
+
fail(`用法: ${formatCommand('link <name> [file] [scope]')}`)
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const root = await resolveRoot(scope)
|
|
79
|
+
if (!root && !isUrl(name)) {
|
|
80
|
+
fail('未配置 prompts_root,请先运行 init 配置全局目录')
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const sourcePath = await resolveLinkSource(root ?? '', name)
|
|
85
|
+
if (!sourcePath) {
|
|
86
|
+
fail(`未找到 prompt 文件: ${name}`)
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let baseTarget = ''
|
|
91
|
+
if (target) {
|
|
92
|
+
baseTarget = resolveTargetPath(target, target)
|
|
93
|
+
} else {
|
|
94
|
+
const defaultName = await resolveDefaultTargetName(sourcePath)
|
|
95
|
+
if (!defaultName) {
|
|
96
|
+
fail('无法确定目标文件名,请显式传入目标路径')
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
baseTarget = resolveTargetPath(defaultName, defaultName)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const targetPath = ensureTargetExtension(baseTarget, sourcePath)
|
|
103
|
+
const exists = await fileExists(targetPath)
|
|
104
|
+
if (exists) {
|
|
105
|
+
fail(`目标文件已存在: ${targetPath}`)
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true })
|
|
110
|
+
const { mode } = await createAutoLink(sourcePath, targetPath)
|
|
111
|
+
await upsertLink(targetPath, { source: sourcePath, mode })
|
|
112
|
+
await recordLink(sourcePath, targetPath, process.cwd())
|
|
113
|
+
|
|
114
|
+
if (mode === 'hardlink') {
|
|
115
|
+
log(`已创建硬链接: ${targetPath}`)
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (mode === 'symlink') {
|
|
120
|
+
log(`已创建软链接: ${targetPath}`)
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
log(`已创建 link 文件: ${targetPath}`)
|
|
125
|
+
void ensureRenameDaemon()
|
|
126
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { readConfigFile, type ConfigScope } from './config'
|
|
3
|
+
import { formatCommand } from './cli-name'
|
|
4
|
+
|
|
5
|
+
const fail = (text: string) => {
|
|
6
|
+
console.error(text)
|
|
7
|
+
process.exitCode = 1
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const openFolder = async (folder: string) => {
|
|
11
|
+
const { spawn } = await import('node:child_process')
|
|
12
|
+
let command = 'xdg-open'
|
|
13
|
+
const args = [folder]
|
|
14
|
+
|
|
15
|
+
if (process.platform === 'win32') {
|
|
16
|
+
command = 'explorer'
|
|
17
|
+
} else if (process.platform === 'darwin') {
|
|
18
|
+
command = 'open'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const proc = spawn(command, args, {
|
|
22
|
+
detached: true,
|
|
23
|
+
stdio: 'ignore',
|
|
24
|
+
})
|
|
25
|
+
proc.unref()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const resolveConfigPath = async (scope?: ConfigScope) => {
|
|
29
|
+
if (scope === 'project') return readConfigFile('project')
|
|
30
|
+
if (scope === 'global') return readConfigFile('global')
|
|
31
|
+
|
|
32
|
+
const project = await readConfigFile('project')
|
|
33
|
+
if (project) return project
|
|
34
|
+
|
|
35
|
+
return readConfigFile('global')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const openConfigFolder = async (scope?: ConfigScope) => {
|
|
39
|
+
const config = await resolveConfigPath(scope)
|
|
40
|
+
if (!config) {
|
|
41
|
+
fail(`未找到配置文件,请先运行: ${formatCommand('init')}`)
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const folder = path.dirname(config.configPath)
|
|
46
|
+
await openFolder(folder)
|
|
47
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { linkFile } from './command-link'
|
|
2
|
+
import type { ConfigScope } from './config'
|
|
3
|
+
import { formatCommand } from './cli-name'
|
|
4
|
+
import { listFiles } from './helpers'
|
|
5
|
+
import { loadPromptRoot } from './prompt-root'
|
|
6
|
+
|
|
7
|
+
const log = (text: string) => console.log(text)
|
|
8
|
+
const fail = (text: string) => {
|
|
9
|
+
console.error(text)
|
|
10
|
+
process.exitCode = 1
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const pickFromList = async (names: string[]) => {
|
|
14
|
+
if (!process.stdin.isTTY) return null
|
|
15
|
+
if (!names.length) return null
|
|
16
|
+
|
|
17
|
+
const module = await import('enquirer')
|
|
18
|
+
const AutoComplete = (module as unknown as { AutoComplete: new (...args: unknown[]) => { run: () => Promise<string> } }).AutoComplete
|
|
19
|
+
|
|
20
|
+
const prompt = new AutoComplete({
|
|
21
|
+
name: 'prompt',
|
|
22
|
+
message: '选择 prompt 文件',
|
|
23
|
+
choices: names,
|
|
24
|
+
limit: 10,
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const selected = await prompt.run().catch(() => null)
|
|
28
|
+
if (typeof selected === 'string') return selected
|
|
29
|
+
return null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const searchAndLink = async (target?: string, scope?: ConfigScope) => {
|
|
33
|
+
const info = await loadPromptRoot(scope)
|
|
34
|
+
if (!info) {
|
|
35
|
+
fail(`未配置 prompts_root,请先运行: ${formatCommand('init')}`)
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const names = await listFiles(info.root)
|
|
40
|
+
if (!names.length) {
|
|
41
|
+
log('未找到任何 prompt 文件')
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const selected = await pickFromList(names.sort())
|
|
46
|
+
if (!selected) {
|
|
47
|
+
log('未选择任何文件')
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
await linkFile(selected, target, scope)
|
|
52
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { readIndex } from './link-index'
|
|
3
|
+
import { readLinks } from './links-store'
|
|
4
|
+
import { fileExists } from './helpers'
|
|
5
|
+
|
|
6
|
+
const log = (text: string) => console.log(text)
|
|
7
|
+
|
|
8
|
+
export const statusLinks = async () => {
|
|
9
|
+
const links = await readLinks()
|
|
10
|
+
const entries = Object.entries(links)
|
|
11
|
+
if (!entries.length) {
|
|
12
|
+
log('未发现 link 记录')
|
|
13
|
+
return
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
for (const [target, entry] of entries) {
|
|
17
|
+
const targetPath = path.resolve(process.cwd(), target)
|
|
18
|
+
const exists = await fileExists(targetPath)
|
|
19
|
+
const flag = exists ? 'ok' : 'missing'
|
|
20
|
+
log(`${target} -> ${entry.source} [${entry.mode}] ${flag}`)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const index = await readIndex()
|
|
24
|
+
if (index.roots.length) {
|
|
25
|
+
log(`roots: ${index.roots.join(', ')}`)
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/commands.ts
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import { applyCurrent, insertPlaceholder } from './command-apply'
|
|
3
|
+
import { initConfig, listSources, setSource, useSource } from './command-config'
|
|
4
|
+
import { linkFile } from './command-link'
|
|
5
|
+
import { searchAndLink } from './command-search'
|
|
6
|
+
import { openConfigFolder } from './command-open'
|
|
7
|
+
import { statusLinks } from './command-status'
|
|
8
|
+
import { runRenameDaemon } from './rename-daemon'
|
|
9
|
+
import { resolveScope } from './scope'
|
|
10
|
+
import type { ConfigScope } from './config'
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
export const registerCommands = (program: Command) => {
|
|
14
|
+
program
|
|
15
|
+
.command('init')
|
|
16
|
+
.description('初始化全局配置')
|
|
17
|
+
.action(async () => {
|
|
18
|
+
await initConfig('global')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
program
|
|
22
|
+
.command('set')
|
|
23
|
+
.description('设置并切换值')
|
|
24
|
+
.argument('<name>', '命名 key,如 spec')
|
|
25
|
+
.argument('<file>', 'prompt 文件路径')
|
|
26
|
+
.argument('[scope]', 'project | global,默认 global')
|
|
27
|
+
.action(async (name, file, scopeInput) => {
|
|
28
|
+
const { scope, error } = resolveScope(scopeInput)
|
|
29
|
+
if (error) {
|
|
30
|
+
console.error(error)
|
|
31
|
+
process.exitCode = 1
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
await setSource(name, file, scope)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
program
|
|
38
|
+
.command('use')
|
|
39
|
+
.description('切换到已保存的值')
|
|
40
|
+
.argument('<name>', '命名 key,如 spec')
|
|
41
|
+
.argument('[scope]', 'project | global,默认 global')
|
|
42
|
+
.action(async (name, scopeInput) => {
|
|
43
|
+
const { scope, error } = resolveScope(scopeInput)
|
|
44
|
+
if (error) {
|
|
45
|
+
console.error(error)
|
|
46
|
+
process.exitCode = 1
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
await useSource(name, scope)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
program
|
|
54
|
+
.command('insert')
|
|
55
|
+
.description('插入占位符')
|
|
56
|
+
.argument('<file>', '目标文件路径')
|
|
57
|
+
.action(async (file) => {
|
|
58
|
+
await insertPlaceholder(file)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
program
|
|
62
|
+
.command('apply')
|
|
63
|
+
.description('应用当前值')
|
|
64
|
+
.argument('[files...]', '指定要替换的文件列表')
|
|
65
|
+
.action(async (files: string[] = []) => {
|
|
66
|
+
await applyCurrent(files)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
program
|
|
70
|
+
.command('link')
|
|
71
|
+
.description('创建 link 文件')
|
|
72
|
+
.argument('[name]', 'prompt 文件名(可省略进入交互)')
|
|
73
|
+
.argument('[file]', '目标文件路径')
|
|
74
|
+
.argument('[scope]', 'project | global,默认 global')
|
|
75
|
+
.option('-i', '交互搜索')
|
|
76
|
+
.action(async (name, file, scopeInput, options) => {
|
|
77
|
+
const interactive = options.i || !name || name === '-'
|
|
78
|
+
let targetFile = file
|
|
79
|
+
let scopeArg = scopeInput
|
|
80
|
+
|
|
81
|
+
if (interactive) {
|
|
82
|
+
if (name && name !== '-') {
|
|
83
|
+
targetFile = name
|
|
84
|
+
scopeArg = file
|
|
85
|
+
}
|
|
86
|
+
let scope: ConfigScope | undefined
|
|
87
|
+
if (scopeArg) {
|
|
88
|
+
const result = resolveScope(scopeArg)
|
|
89
|
+
if (result.error) {
|
|
90
|
+
console.error(result.error)
|
|
91
|
+
process.exitCode = 1
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
scope = result.scope
|
|
95
|
+
}
|
|
96
|
+
await searchAndLink(targetFile, scope)
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let scope: ConfigScope | undefined
|
|
101
|
+
if (scopeInput) {
|
|
102
|
+
const result = resolveScope(scopeInput)
|
|
103
|
+
if (result.error) {
|
|
104
|
+
console.error(result.error)
|
|
105
|
+
process.exitCode = 1
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
scope = result.scope
|
|
109
|
+
}
|
|
110
|
+
await linkFile(name, file, scope)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
program
|
|
114
|
+
.command('search')
|
|
115
|
+
.description('交互搜索并创建 link')
|
|
116
|
+
.argument('[file]', '目标文件路径')
|
|
117
|
+
.argument('[scope]', 'project | global,默认 global')
|
|
118
|
+
.action(async (file, scopeInput) => {
|
|
119
|
+
let scope: ConfigScope | undefined
|
|
120
|
+
if (scopeInput) {
|
|
121
|
+
const result = resolveScope(scopeInput)
|
|
122
|
+
if (result.error) {
|
|
123
|
+
console.error(result.error)
|
|
124
|
+
process.exitCode = 1
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
scope = result.scope
|
|
128
|
+
}
|
|
129
|
+
await searchAndLink(file, scope)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
program
|
|
133
|
+
.command('open-config')
|
|
134
|
+
.description('打开配置所在文件夹')
|
|
135
|
+
.argument('[scope]', 'project | global,默认 global')
|
|
136
|
+
.action(async (scopeInput) => {
|
|
137
|
+
let scope: ConfigScope | undefined
|
|
138
|
+
if (scopeInput) {
|
|
139
|
+
const result = resolveScope(scopeInput)
|
|
140
|
+
if (result.error) {
|
|
141
|
+
console.error(result.error)
|
|
142
|
+
process.exitCode = 1
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
scope = result.scope
|
|
146
|
+
}
|
|
147
|
+
await openConfigFolder(scope)
|
|
148
|
+
})
|
|
149
|
+
.alias('open')
|
|
150
|
+
|
|
151
|
+
program
|
|
152
|
+
.command('status')
|
|
153
|
+
.description('查看 link 状态')
|
|
154
|
+
.action(async () => {
|
|
155
|
+
await statusLinks()
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
program
|
|
159
|
+
.command('rename-daemon', { hidden: true })
|
|
160
|
+
.description('内部重命名监听')
|
|
161
|
+
.action(async () => {
|
|
162
|
+
await runRenameDaemon()
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
}
|