specrune 0.0.0-stage → 0.8.1
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/LICENSE +21 -0
- package/README.md +48 -2
- package/bin/specrune.js +4 -0
- package/lib/launcher.js +210 -0
- package/package.json +38 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 jsfaint
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,49 @@
|
|
|
1
|
-
#
|
|
1
|
+
# specrune
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
SpecRune 的 npm 安装入口:一个很小的 Node 启动器,按平台拉起对应的原生 Go 二进制。
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm i -g specrune
|
|
7
|
+
specrune version
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
真正的二进制放在 6 个平台包(`specrune-linux-x64`、`specrune-darwin-arm64`、`specrune-windows-x64` …)里,
|
|
11
|
+
由本包的 `optionalDependencies` 精确锁定版本。npm 只安装与当前平台匹配的那一个,所以安装体积就是一个
|
|
12
|
+
二进制的体积。
|
|
13
|
+
|
|
14
|
+
## 运行要求
|
|
15
|
+
|
|
16
|
+
- **Node.js >= 18**。
|
|
17
|
+
- **只支持全局安装**(`npm i -g specrune`)。项目内安装与 `npx specrune init` 都不受支持:npx 把包装进临时 `_npx` 目录,命令退出后即被清掉,`init` 装配的宿主钩子会指向不存在的路径(`init` 在该目录下会拒绝并提示先全局安装)。
|
|
18
|
+
|
|
19
|
+
## 升级
|
|
20
|
+
|
|
21
|
+
二进制归 npm 管理,`specrune upgrade` 会拒绝自更新(`--check` 同样拒绝)。升级用:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm i -g specrune@latest
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## 可选依赖被跳过时
|
|
28
|
+
|
|
29
|
+
启动器找不到平台包会直接退出并说明原因。常见情形:
|
|
30
|
+
|
|
31
|
+
- `npm i -g specrune --omit=optional`
|
|
32
|
+
- `npm i -g specrune --ignore-optional`
|
|
33
|
+
- pnpm 的 `--no-optional` 或镜像跳过了可选依赖
|
|
34
|
+
|
|
35
|
+
处置:`npm i -g specrune@latest` 重装。若该平台没有预编译包,改用
|
|
36
|
+
[Releases](https://github.com/jsfaint/SpecRune/releases) 里的独立二进制。
|
|
37
|
+
|
|
38
|
+
## 安装渠道
|
|
39
|
+
|
|
40
|
+
启动器给子进程注入 `SPECRUNE_LAUNCHER=npm`,SpecRune 据此识别自己由 npm 安装(这类安装不应走
|
|
41
|
+
`specrune upgrade` 自更新)。
|
|
42
|
+
|
|
43
|
+
npm 安装不执行任何 install/postinstall 脚本,也没有额外的网络下载。
|
|
44
|
+
|
|
45
|
+
## 卸载
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm uninstall -g specrune
|
|
49
|
+
```
|
package/bin/specrune.js
ADDED
package/lib/launcher.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const os = require("node:os");
|
|
5
|
+
const path = require("node:path");
|
|
6
|
+
const { spawn } = require("node:child_process");
|
|
7
|
+
|
|
8
|
+
// 唯一平台映射表:npm 打包(stage.mjs)与启动器共用,避免两处各自维护而漂移。
|
|
9
|
+
// 键是 Node 的 `${process.platform}-${process.arch}`,goos/goarch 只给打包侧用。
|
|
10
|
+
const TARGETS = {
|
|
11
|
+
"linux-x64": {
|
|
12
|
+
package: "specrune-linux-x64",
|
|
13
|
+
os: "linux",
|
|
14
|
+
cpu: "x64",
|
|
15
|
+
goos: "linux",
|
|
16
|
+
goarch: "amd64",
|
|
17
|
+
binary: "bin/specrune",
|
|
18
|
+
},
|
|
19
|
+
"linux-arm64": {
|
|
20
|
+
package: "specrune-linux-arm64",
|
|
21
|
+
os: "linux",
|
|
22
|
+
cpu: "arm64",
|
|
23
|
+
goos: "linux",
|
|
24
|
+
goarch: "arm64",
|
|
25
|
+
binary: "bin/specrune",
|
|
26
|
+
},
|
|
27
|
+
"darwin-x64": {
|
|
28
|
+
package: "specrune-darwin-x64",
|
|
29
|
+
os: "darwin",
|
|
30
|
+
cpu: "x64",
|
|
31
|
+
goos: "darwin",
|
|
32
|
+
goarch: "amd64",
|
|
33
|
+
binary: "bin/specrune",
|
|
34
|
+
},
|
|
35
|
+
"darwin-arm64": {
|
|
36
|
+
package: "specrune-darwin-arm64",
|
|
37
|
+
os: "darwin",
|
|
38
|
+
cpu: "arm64",
|
|
39
|
+
goos: "darwin",
|
|
40
|
+
goarch: "arm64",
|
|
41
|
+
binary: "bin/specrune",
|
|
42
|
+
},
|
|
43
|
+
// 包名用 windows 而不是 win32:npm 会把新包名里的 `win32` 判成垃圾名并回 403
|
|
44
|
+
// "Package name triggered spam detection"(同 git-cliff 等包的遭遇)。`os` 字段
|
|
45
|
+
// 反过来必须是 Node 的平台名 win32,两者不是一回事,别顺手统一。
|
|
46
|
+
"win32-x64": {
|
|
47
|
+
package: "specrune-windows-x64",
|
|
48
|
+
os: "win32",
|
|
49
|
+
cpu: "x64",
|
|
50
|
+
goos: "windows",
|
|
51
|
+
goarch: "amd64",
|
|
52
|
+
binary: "bin/specrune.exe",
|
|
53
|
+
},
|
|
54
|
+
"win32-arm64": {
|
|
55
|
+
package: "specrune-windows-arm64",
|
|
56
|
+
os: "win32",
|
|
57
|
+
cpu: "arm64",
|
|
58
|
+
goos: "windows",
|
|
59
|
+
goarch: "arm64",
|
|
60
|
+
binary: "bin/specrune.exe",
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// 子进程退出时要原样复现的信号。stdio 已继承,前台终端里子进程本身也会收到这些信号;
|
|
65
|
+
// 父进程转发是为了覆盖「信号只发给启动器」的场景(例如 CI 只 kill 最外层 PID)。
|
|
66
|
+
const FORWARDED_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
67
|
+
|
|
68
|
+
function fail(message) {
|
|
69
|
+
process.stderr.write(`specrune: ${message}\n`);
|
|
70
|
+
process.exitCode = 1;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function supportedTargets() {
|
|
74
|
+
return Object.keys(TARGETS).join("、");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function resolveTarget(platform = process.platform, arch = process.arch) {
|
|
78
|
+
return TARGETS[`${platform}-${arch}`] ?? null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function resolvePackageJson(packageName, fromDir) {
|
|
82
|
+
return require.resolve(`${packageName}/package.json`, { paths: [fromDir] });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function injectLauncherEnv(env) {
|
|
86
|
+
return { ...env, SPECRUNE_LAUNCHER: "npm" };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function describeMissingPackage(target, version) {
|
|
90
|
+
return [
|
|
91
|
+
`未找到当前平台的 npm 包 ${target.package}(${target.os}-${target.cpu})。`,
|
|
92
|
+
"",
|
|
93
|
+
"可能原因与处置:",
|
|
94
|
+
` 1. 安装时省略了可选依赖(--omit=optional / --ignore-optional / pnpm 的 --no-optional):`,
|
|
95
|
+
` npm i -g specrune@${version}`,
|
|
96
|
+
" 2. 包管理器或镜像跳过了可选依赖:换回官方 registry 后重新安装",
|
|
97
|
+
" 3. 该平台暂无预编译包:从 https://github.com/jsfaint/SpecRune/releases 下载独立二进制",
|
|
98
|
+
].join("\n");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function describeVersionMismatch(target, launcherVersion, packageVersion) {
|
|
102
|
+
return [
|
|
103
|
+
"平台包版本与启动器不一致。",
|
|
104
|
+
` 启动器 specrune:${launcherVersion}`,
|
|
105
|
+
` 平台包 ${target.package}:${packageVersion}`,
|
|
106
|
+
"",
|
|
107
|
+
"处置:npm i -g specrune@latest 重装;若仍不一致,删除全局 node_modules 下的 specrune-* 目录后重装。",
|
|
108
|
+
].join("\n");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function ensureExecutable(binPath) {
|
|
112
|
+
if (process.platform === "win32") return;
|
|
113
|
+
try {
|
|
114
|
+
fs.chmodSync(binPath, 0o755);
|
|
115
|
+
} catch {
|
|
116
|
+
// best-effort:npm 的 bin 链接通常已经恢复执行位,失败就交给 spawn 的 EACCES 报错。
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function reRaise(signal) {
|
|
121
|
+
for (const forwarded of FORWARDED_SIGNALS) process.removeAllListeners(forwarded);
|
|
122
|
+
const number = os.constants.signals[signal];
|
|
123
|
+
// Windows 没有信号投递:process.kill(self, sig) 不触发默认动作,而是以退出码 1 强杀自己,
|
|
124
|
+
// 下面的 128+n 永远轮不到。直接设退出码,否则「按信号退出」在 Windows 上退化成 1。
|
|
125
|
+
if (process.platform !== "win32") {
|
|
126
|
+
try {
|
|
127
|
+
process.kill(process.pid, signal);
|
|
128
|
+
} catch {
|
|
129
|
+
// 信号被忽略(例如 nohup)时同样落到 128+n。
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
process.exitCode = number ? 128 + number : 1;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function run(binPath, args) {
|
|
136
|
+
const child = spawn(binPath, args, {
|
|
137
|
+
stdio: "inherit",
|
|
138
|
+
env: injectLauncherEnv(process.env),
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
for (const signal of FORWARDED_SIGNALS) {
|
|
142
|
+
process.on(signal, () => {
|
|
143
|
+
try {
|
|
144
|
+
child.kill(signal);
|
|
145
|
+
} catch {
|
|
146
|
+
// 子进程已退出时 kill 会抛 ESRCH,忽略即可。
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
child.on("error", (error) => {
|
|
152
|
+
fail(
|
|
153
|
+
[
|
|
154
|
+
`无法执行 ${binPath}:${error.message}`,
|
|
155
|
+
"处置:npm i -g specrune@latest 重装。",
|
|
156
|
+
].join("\n"),
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
child.on("exit", (code, signal) => {
|
|
161
|
+
for (const forwarded of FORWARDED_SIGNALS) {
|
|
162
|
+
process.removeAllListeners(forwarded);
|
|
163
|
+
}
|
|
164
|
+
if (signal) {
|
|
165
|
+
reRaise(signal);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
process.exitCode = code ?? 1;
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function main(argv = process.argv.slice(2)) {
|
|
173
|
+
const target = resolveTarget();
|
|
174
|
+
if (!target) {
|
|
175
|
+
fail(
|
|
176
|
+
[
|
|
177
|
+
`不支持当前平台 ${process.platform}-${process.arch}。`,
|
|
178
|
+
`支持的平台:${supportedTargets()}`,
|
|
179
|
+
"独立二进制见 https://github.com/jsfaint/SpecRune/releases",
|
|
180
|
+
].join("\n"),
|
|
181
|
+
);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const { version: launcherVersion } = require("../package.json");
|
|
186
|
+
let packageJsonPath;
|
|
187
|
+
try {
|
|
188
|
+
packageJsonPath = resolvePackageJson(target.package, path.join(__dirname, ".."));
|
|
189
|
+
} catch (error) {
|
|
190
|
+
if (error && error.code === "MODULE_NOT_FOUND") {
|
|
191
|
+
fail(describeMissingPackage(target, launcherVersion));
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
fail(`解析平台包 ${target.package} 失败:${error && error.message ? error.message : error}`);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const packageRoot = path.dirname(packageJsonPath);
|
|
199
|
+
const { version: packageVersion } = require(packageJsonPath);
|
|
200
|
+
if (packageVersion !== launcherVersion) {
|
|
201
|
+
fail(describeVersionMismatch(target, launcherVersion, packageVersion));
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const binPath = path.join(packageRoot, target.binary);
|
|
206
|
+
ensureExecutable(binPath);
|
|
207
|
+
run(binPath, argv);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
module.exports = { TARGETS, main, resolveTarget, supportedTargets };
|
package/package.json
CHANGED
|
@@ -1,6 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "specrune",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.8.1",
|
|
4
|
+
"description": "SpecRune 命令行:规格驱动的 AI 编码工作流引擎(单二进制 Go CLI 的 npm 启动器)",
|
|
5
|
+
"bin": {
|
|
6
|
+
"specrune": "bin/specrune.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin",
|
|
10
|
+
"lib",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/jsfaint/SpecRune.git",
|
|
19
|
+
"directory": "npm/cli"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://github.com/jsfaint/SpecRune#readme",
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/jsfaint/SpecRune/issues"
|
|
24
|
+
},
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"keywords": [
|
|
27
|
+
"spec-driven",
|
|
28
|
+
"workflow",
|
|
29
|
+
"ai-agents",
|
|
30
|
+
"cli"
|
|
31
|
+
],
|
|
32
|
+
"optionalDependencies": {
|
|
33
|
+
"specrune-linux-x64": "0.8.1",
|
|
34
|
+
"specrune-linux-arm64": "0.8.1",
|
|
35
|
+
"specrune-darwin-x64": "0.8.1",
|
|
36
|
+
"specrune-darwin-arm64": "0.8.1",
|
|
37
|
+
"specrune-windows-x64": "0.8.1",
|
|
38
|
+
"specrune-windows-arm64": "0.8.1"
|
|
39
|
+
}
|
|
40
|
+
}
|