@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 +80 -51
- package/README.zh.md +79 -48
- package/dist/pmt.exe +0 -0
- package/package.json +1 -1
- package/src/cli.ts +79 -18
- package/src/command-apply.ts +10 -10
- package/src/command-completion.ts +3 -3
- package/src/command-config.ts +22 -22
- package/src/command-link.ts +9 -9
- package/src/command-open.ts +1 -1
- package/src/command-search.ts +4 -4
- package/src/command-status.ts +1 -1
- package/src/commands.ts +25 -25
- package/src/content-source.ts +1 -1
- package/src/link-manager.ts +7 -7
- package/src/lock.ts +2 -2
- package/src/scope.ts +1 -1
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
|
-
[
|
|
5
|
+
English | [简体中文](./README.zh.md)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
[](https://www.npmjs.com/package/@yuchenrx/pmt-cli)
|
|
8
|
+
[](https://github.com/YuChenRX/pmt/actions/workflows/release.yml)
|
|
9
|
+
[](#)
|
|
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
|
-
##
|
|
18
|
+
## Quickstart
|
|
15
19
|
|
|
16
|
-
|
|
20
|
+
1) Initialize a global prompt repo:
|
|
17
21
|
|
|
18
22
|
```bash
|
|
19
|
-
|
|
23
|
+
pmt init
|
|
20
24
|
```
|
|
21
25
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
### From source
|
|
26
|
+
2) Pick a prompt interactively and create a link:
|
|
25
27
|
|
|
26
28
|
```bash
|
|
27
|
-
|
|
29
|
+
pmt link
|
|
28
30
|
```
|
|
29
31
|
|
|
30
|
-
|
|
32
|
+
3) Or link a specific prompt name:
|
|
31
33
|
|
|
32
34
|
```bash
|
|
33
|
-
pmt
|
|
35
|
+
pmt link rules
|
|
34
36
|
```
|
|
35
37
|
|
|
36
|
-
|
|
38
|
+
4) Or link to a specific target path:
|
|
37
39
|
|
|
38
40
|
```bash
|
|
39
|
-
|
|
41
|
+
pmt link rules docs/AGENTS.md
|
|
40
42
|
```
|
|
41
43
|
|
|
42
|
-
|
|
44
|
+
5) Check link status:
|
|
43
45
|
|
|
44
46
|
```bash
|
|
45
|
-
|
|
47
|
+
pmt status
|
|
46
48
|
```
|
|
47
49
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
## Quick Start
|
|
50
|
+
## Installation
|
|
51
51
|
|
|
52
|
-
###
|
|
53
|
-
|
|
54
|
-
Global prompt repo:
|
|
52
|
+
### npm (Windows)
|
|
55
53
|
|
|
56
54
|
```bash
|
|
57
|
-
pmt
|
|
55
|
+
npm i -g @yuchenrx/pmt-cli
|
|
58
56
|
```
|
|
59
57
|
|
|
60
|
-
|
|
58
|
+
This package ships a Windows binary. For other platforms, build from source.
|
|
61
59
|
|
|
62
|
-
###
|
|
60
|
+
### From source
|
|
63
61
|
|
|
64
|
-
|
|
62
|
+
Requirements: Bun.
|
|
65
63
|
|
|
66
64
|
```bash
|
|
67
|
-
|
|
65
|
+
bun install
|
|
66
|
+
bun run pmt
|
|
68
67
|
```
|
|
69
68
|
|
|
70
|
-
|
|
69
|
+
## Commands
|
|
71
70
|
|
|
72
|
-
|
|
73
|
-
pmt link 规范 docs/AGENTS.md
|
|
74
|
-
```
|
|
71
|
+
Common commands:
|
|
75
72
|
|
|
76
|
-
|
|
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
|
-
|
|
79
|
-
pmt link
|
|
80
|
-
pmt link -i
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
### 3) Status
|
|
82
|
+
For full options:
|
|
84
83
|
|
|
85
84
|
```bash
|
|
86
|
-
pmt
|
|
85
|
+
pmt --help
|
|
86
|
+
pmt <command> --help
|
|
87
87
|
```
|
|
88
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
|
|
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
|
-
-
|
|
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
|
|
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
|
-
##
|
|
129
|
+
## Build
|
|
129
130
|
|
|
130
131
|
```bash
|
|
131
|
-
|
|
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
|
+
[](https://www.npmjs.com/package/@yuchenrx/pmt-cli)
|
|
8
|
+
[](https://github.com/YuChenRX/pmt/actions/workflows/release.yml)
|
|
9
|
+
[](#)
|
|
10
|
+
|
|
11
|
+
## 概览
|
|
8
12
|
|
|
9
13
|
- 全局集中管理 prompt/spec 文件(`prompts_root`)。
|
|
10
|
-
- 在任意项目创建 link 文件(硬链接
|
|
14
|
+
- 在任意项目创建 link 文件(硬链接 -> 软链接 -> meta-link 降级)。
|
|
11
15
|
- 后台追踪 link 重命名/移动(只监听已有目录,不全盘扫描)。
|
|
12
16
|
- `status` 查看所有 link 状态(不扫描磁盘)。
|
|
13
17
|
|
|
14
|
-
##
|
|
18
|
+
## 快速开始
|
|
15
19
|
|
|
16
|
-
|
|
20
|
+
1)初始化全局 prompt 仓库:
|
|
17
21
|
|
|
18
22
|
```bash
|
|
19
|
-
|
|
23
|
+
pmt init
|
|
20
24
|
```
|
|
21
25
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
### 源码安装
|
|
26
|
+
2)交互选择一个 prompt 并创建 link:
|
|
25
27
|
|
|
26
28
|
```bash
|
|
27
|
-
|
|
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
|
-
|
|
41
|
+
pmt link rules docs/AGENTS.md
|
|
40
42
|
```
|
|
41
43
|
|
|
42
|
-
|
|
44
|
+
5)查看 link 状态:
|
|
43
45
|
|
|
44
46
|
```bash
|
|
45
|
-
|
|
47
|
+
pmt status
|
|
46
48
|
```
|
|
47
49
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
## 快速开始
|
|
51
|
-
|
|
52
|
-
### 1)初始化
|
|
50
|
+
## 安装
|
|
53
51
|
|
|
54
|
-
|
|
52
|
+
### npm(Windows)
|
|
55
53
|
|
|
56
54
|
```bash
|
|
57
|
-
pmt
|
|
55
|
+
npm i -g @yuchenrx/pmt-cli
|
|
58
56
|
```
|
|
59
57
|
|
|
60
|
-
|
|
58
|
+
该包发布的是 Windows 二进制文件,其他平台请从源码构建。
|
|
61
59
|
|
|
62
|
-
###
|
|
60
|
+
### 源码安装
|
|
63
61
|
|
|
64
|
-
|
|
62
|
+
要求:Bun。
|
|
65
63
|
|
|
66
64
|
```bash
|
|
67
|
-
|
|
65
|
+
bun install
|
|
66
|
+
bun run pmt
|
|
68
67
|
```
|
|
69
68
|
|
|
70
|
-
|
|
69
|
+
## 命令
|
|
71
70
|
|
|
72
|
-
|
|
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
|
-
|
|
79
|
-
pmt link
|
|
80
|
-
pmt link -i
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
### 3)查看状态
|
|
82
|
+
查看完整参数:
|
|
84
83
|
|
|
85
84
|
```bash
|
|
86
|
-
pmt
|
|
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
|
|
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
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
|
|
10
|
+
declare const PMT_VERSION: string | undefined
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
17
|
-
|
|
35
|
+
for (const packageJsonPath of candidates) {
|
|
36
|
+
const version = readPackageVersion(packageJsonPath)
|
|
37
|
+
if (version) return version
|
|
38
|
+
}
|
|
18
39
|
|
|
19
|
-
|
|
20
|
-
if (!args.length) {
|
|
21
|
-
program.outputHelp()
|
|
22
|
-
process.exit(0)
|
|
40
|
+
return '0.0.0'
|
|
23
41
|
}
|
|
24
42
|
|
|
25
|
-
|
|
26
|
-
program
|
|
27
|
-
|
|
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
|
|
31
|
-
await program.parseAsync(process.argv)
|
|
92
|
+
await run()
|
package/src/command-apply.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
15
|
-
fail(
|
|
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(
|
|
24
|
+
fail(`Auto-uninstall failed: ${message}`)
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
|
package/src/command-config.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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('
|
|
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('
|
|
95
|
+
const promptDir = await ask('Project prompts directory', dirDefault)
|
|
96
96
|
config.prompts_dir = promptDir
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
const linkDefault = await ask('
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
158
|
+
log(`Available files: ${suggestions.join(', ')}`)
|
|
159
159
|
}
|
|
160
|
-
fail(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
208
|
+
fail(`Config file not found. Run: ${formatCommand('init')}`)
|
|
209
209
|
return
|
|
210
210
|
}
|
|
211
211
|
|
|
212
212
|
if (!merged.resolvedSources[name]) {
|
|
213
|
-
fail(
|
|
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(
|
|
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(
|
|
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('
|
|
233
|
+
log('No sources configured')
|
|
234
234
|
return
|
|
235
235
|
}
|
|
236
236
|
|
package/src/command-link.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
115
|
+
log(`Created hardlink: ${targetPath}`)
|
|
116
116
|
return
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
if (mode === 'symlink') {
|
|
120
|
-
log(
|
|
120
|
+
log(`Created symlink: ${targetPath}`)
|
|
121
121
|
return
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
log(
|
|
124
|
+
log(`Created link file: ${targetPath}`)
|
|
125
125
|
void ensureRenameDaemon()
|
|
126
126
|
}
|
package/src/command-open.ts
CHANGED
|
@@ -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(
|
|
41
|
+
fail(`Config file not found. Run: ${formatCommand('init')}`)
|
|
42
42
|
return
|
|
43
43
|
}
|
|
44
44
|
|
package/src/command-search.ts
CHANGED
|
@@ -19,7 +19,7 @@ const pickFromList = async (names: string[]) => {
|
|
|
19
19
|
|
|
20
20
|
const prompt = new AutoComplete({
|
|
21
21
|
name: 'prompt',
|
|
22
|
-
message: '
|
|
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(
|
|
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('
|
|
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
|
|
package/src/command-status.ts
CHANGED
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>', '
|
|
25
|
-
.argument('<file>', '
|
|
26
|
-
.argument('[scope]', 'project | 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>', '
|
|
41
|
-
.argument('[scope]', 'project | 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('
|
|
72
|
-
.argument('[name]', '
|
|
73
|
-
.argument('[file]', '
|
|
74
|
-
.argument('[scope]', 'project | 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('
|
|
116
|
-
.argument('[file]', '
|
|
117
|
-
.argument('[scope]', 'project | 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
|
|
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('
|
|
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
|
})
|
package/src/content-source.ts
CHANGED
|
@@ -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(
|
|
8
|
+
throw new Error(`Failed to fetch remote file: ${response.status}`)
|
|
9
9
|
}
|
|
10
10
|
return response.text()
|
|
11
11
|
}
|
package/src/link-manager.ts
CHANGED
|
@@ -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(
|
|
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 '
|
|
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 '
|
|
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('
|
|
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('
|
|
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
|
|
12
|
+
return { scope: 'global', error: 'scope must be project or global' }
|
|
13
13
|
}
|