spexcode 0.2.0 → 0.2.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/README.md +68 -65
- package/README.zh-CN.md +135 -0
- package/package.json +1 -1
- package/spec-cli/src/cli.ts +14 -5
- package/spec-cli/src/gateway.ts +15 -11
- package/spec-cli/src/guide.ts +11 -7
- package/spec-cli/src/help.ts +4 -2
- package/spec-cli/src/index.ts +11 -4
- package/spec-cli/src/issues.ts +66 -15
- package/spec-cli/src/localIssues.ts +17 -0
- package/spec-dashboard/dist/assets/{index-BTU-44Os.css → index-Ct_ubwrd.css} +1 -1
- package/spec-dashboard/dist/assets/{index-B0tgHeEQ.js → index-DehTZ-h9.js} +38 -38
- package/spec-dashboard/dist/index.html +2 -2
- package/spec-forge/src/cli.ts +4 -10
- package/spec-forge/src/drivers.ts +13 -0
package/README.md
CHANGED
|
@@ -1,52 +1,52 @@
|
|
|
1
1
|
<img src="docs/sdd-tuxedo-pooh.png" alt="Writing code vs. authoring a living, executable specification artifact" width="420">
|
|
2
2
|
|
|
3
|
+
English | [中文](./README.zh-CN.md)
|
|
4
|
+
|
|
3
5
|
> Spec-driven development fails two ways: the spec drifts out of sync with the code, or it
|
|
4
|
-
> bloats into stale ceremony. SpexCode keeps each spec short and current
|
|
5
|
-
> versioned by git,
|
|
6
|
+
> bloats into stale ceremony. SpexCode keeps each spec short and current: rewritten in place
|
|
7
|
+
> and versioned by git, not an accumulating changelog.
|
|
6
8
|
|
|
7
9
|
**SpexCode** is a spec-driven, self-developing dev tool. Every part of a project becomes a versioned
|
|
8
|
-
*spec node
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
*spec node*: a `.spec/**/spec.md` whose body states the part's present intent. Git is the database:
|
|
11
|
+
a node's version is its count of content commits, and "drift" is governed code that moved ahead of
|
|
12
|
+
its spec. The `spex` CLI and the live dashboard read everything straight from git; there is no
|
|
11
13
|
separate store.
|
|
12
14
|
|
|
13
15
|
- **[Using SpexCode](#using-spexcode)** — adopt the `spex` CLI and drive it through your coding agent to govern *your own* project.
|
|
14
16
|
- **[Contributing to SpexCode](#contributing-to-spexcode)** — hack on the tool itself, in this repo.
|
|
15
|
-
- **[Working with agents →](https://spexcode.net/working-with-agents/)** — the
|
|
17
|
+
- **[Working with agents →](https://spexcode.net/working-with-agents/)** — the longer write-up on the docs site.
|
|
16
18
|
|
|
17
19
|
---
|
|
18
20
|
|
|
19
21
|
## Using SpexCode
|
|
20
22
|
|
|
21
|
-
You set SpexCode up once
|
|
22
|
-
way
|
|
23
|
-
*"add a spec node for the auth flow", "extract specs for this package", "dispatch a worker to
|
|
24
|
-
Y"*
|
|
25
|
-
the substrate; your agent is the daily interface.
|
|
23
|
+
You set SpexCode up once (`npm i -g spexcode`, then `spex init` in your repo). After that, the main
|
|
24
|
+
way to use it is by talking to your coding agent. You describe what you want in plain language
|
|
25
|
+
(*"add a spec node for the auth flow", "extract specs for this package", "dispatch a worker to
|
|
26
|
+
implement Y"*) and the agent runs the `spex` CLI for you while you supervise on the board. The
|
|
27
|
+
manual CLI below is the substrate; your agent is the daily interface.
|
|
26
28
|
|
|
27
|
-
That works because a freshly
|
|
28
|
-
contract
|
|
29
|
+
That works because a freshly launched agent already knows SpexCode. `spex init` materializes the
|
|
30
|
+
contract (the spec-node ritual, the commit-before-declare gate, the merge style) into a
|
|
29
31
|
`<!-- spexcode -->` managed block in your repo's `CLAUDE.md`/`AGENTS.md`, which
|
|
30
|
-
**[Claude Code](https://www.anthropic.com/claude-code)** and **Codex**
|
|
31
|
-
context
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
the
|
|
41
|
-
|
|
42
|
-
>
|
|
43
|
-
|
|
44
|
-
>
|
|
45
|
-
|
|
46
|
-
> **
|
|
47
|
-
>
|
|
48
|
-
> — and those agents run commands on your machine, so read [`SECURITY.md`](./docs/SECURITY.md) before
|
|
49
|
-
> exposing the backend.
|
|
32
|
+
**[Claude Code](https://www.anthropic.com/claude-code)** and **Codex** auto-discover as always-on
|
|
33
|
+
context. From there the agent self-serves detail on demand from the built-in manual: `spex guide`
|
|
34
|
+
(the workflow), `spex guide spec` / `spex guide yatsu` (the file formats), and `spex guide config`
|
|
35
|
+
(every `spexcode.json` setting). You can tell it *"run `spex guide config` and set me up a
|
|
36
|
+
launcher"*.
|
|
37
|
+
|
|
38
|
+
The docs site covers this in full: **[working with agents](https://spexcode.net/working-with-agents/)**
|
|
39
|
+
for the agent-driven workflow, **[getting started](https://spexcode.net/getting-started/)** for the
|
|
40
|
+
setup end to end.
|
|
41
|
+
|
|
42
|
+
> **It's also just plain tooling.** Strip the agent away and the core is still useful on its own:
|
|
43
|
+
> spec files versioned by git, checked by `spex lint` and shown on a read-only dashboard. No AI,
|
|
44
|
+
> nothing to run but Node and git. The vibe-coding path sits on top of that; it doesn't replace it.
|
|
45
|
+
|
|
46
|
+
> **Requirements.** Core: **Node ≥ 22** and **git**. Driving SpexCode through an agent (or
|
|
47
|
+
> dispatching workers onto your nodes) also needs **tmux** and an authenticated **Claude Code or
|
|
48
|
+
> Codex** on your PATH. Those agents run commands on your machine, so read
|
|
49
|
+
> [`SECURITY.md`](./docs/SECURITY.md) before exposing the backend.
|
|
50
50
|
|
|
51
51
|
### Set it up
|
|
52
52
|
|
|
@@ -58,17 +58,18 @@ cd ~/my-app
|
|
|
58
58
|
spex init # additive — never restructures your code
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
`spex init` is additive: it seeds a starter **`.spec/`** tree (a root `project` node plus the
|
|
62
|
-
plugins that define the dev flow), a starter **`spexcode.json`**, and the per-clone
|
|
63
|
-
`pre-commit` hook that runs **spec-lint
|
|
64
|
-
which blocks direct commits to `main`, plus a `prepare-commit-msg` hook that stamps
|
|
65
|
-
session attribution). It also **materializes** the harness artifacts that make the
|
|
66
|
-
work: the `<!-- spexcode -->` contract block in `CLAUDE.md`/`AGENTS.md`, and the
|
|
67
|
-
shims (the `settings.json` hooks) a self-launched agent discovers. Those
|
|
68
|
-
gitignored
|
|
61
|
+
`spex init` is additive: it seeds a starter **`.spec/`** tree (a root `project` node plus the
|
|
62
|
+
`.config` plugins that define the dev flow), a starter **`spexcode.json`**, and the per-clone
|
|
63
|
+
**git hooks** (a `pre-commit` hook that runs **spec-lint**, blocking on broken spec↔code links, and
|
|
64
|
+
**main-guard**, which blocks direct commits to `main`, plus a `prepare-commit-msg` hook that stamps
|
|
65
|
+
each commit's session attribution). It also **materializes** the harness artifacts that make the
|
|
66
|
+
agent path work: the `<!-- spexcode -->` contract block in `CLAUDE.md`/`AGENTS.md`, and the
|
|
67
|
+
`.claude/` / `.codex/` shims (the `settings.json` hooks) a self-launched agent discovers. Those
|
|
68
|
+
artifacts are generated and gitignored; they are regenerated on each machine, never committed.
|
|
69
69
|
|
|
70
|
-
Then make it yours — either ask your agent to, or do it by hand: edit `.spec/project/spec.md` to
|
|
71
|
-
the project, point `spexcode.json`'s `lint.governedRoots` at your real source dir(s), and
|
|
70
|
+
Then make it yours — either ask your agent to, or do it by hand: edit `.spec/project/spec.md` to
|
|
71
|
+
describe the project, point `spexcode.json`'s `lint.governedRoots` at your real source dir(s), and
|
|
72
|
+
check the graph:
|
|
72
73
|
|
|
73
74
|
```sh
|
|
74
75
|
spex lint # the "coverage" warnings are your adoption TODO list
|
|
@@ -76,18 +77,18 @@ spex lint # the "coverage" warnings are your adoption TODO list
|
|
|
76
77
|
|
|
77
78
|
### Configure it
|
|
78
79
|
|
|
79
|
-
Two optional JSON files at the repo root hold every setting, split by portability
|
|
80
|
-
that's the whole discipline:
|
|
80
|
+
Two optional JSON files at the repo root hold every setting, split by portability:
|
|
81
81
|
|
|
82
|
-
- **`spexcode.json`** — *committed, portable*: layout, dashboard identity (`title` + `icon`), lint
|
|
83
|
-
and launcher **names**. Facts that are true for the project.
|
|
84
|
-
- **`spexcode.local.json`** — *gitignored, host-specific*: absolute launcher command paths,
|
|
85
|
-
paths, and the private-overlay switch (`private: true`, which makes `spex materialize`
|
|
86
|
-
in the tracked tree — ignores go to the local git exclude, not the committed
|
|
87
|
-
run SpexCode on a repo you share but don't own). Facts that are true for
|
|
82
|
+
- **`spexcode.json`** — *committed, portable*: layout, dashboard identity (`title` + `icon`), lint
|
|
83
|
+
budgets, and launcher **names**. Facts that are true for the project.
|
|
84
|
+
- **`spexcode.local.json`** — *gitignored, host-specific*: absolute launcher command paths,
|
|
85
|
+
cert/secret paths, and the private-overlay switch (`private: true`, which makes `spex materialize`
|
|
86
|
+
leave zero trace in the tracked tree — ignores go to the local git exclude, not the committed
|
|
87
|
+
`.gitignore` — so you can run SpexCode on a repo you share but don't own). Facts that are true for
|
|
88
|
+
one machine.
|
|
88
89
|
|
|
89
|
-
There is no `spex config set` — you (or your agent) edit the files directly. **`spex guide config`**
|
|
90
|
-
authoritative manual for every field and which of the two files it belongs in.
|
|
90
|
+
There is no `spex config set` — you (or your agent) edit the files directly. **`spex guide config`**
|
|
91
|
+
is the authoritative manual for every field and which of the two files it belongs in.
|
|
91
92
|
|
|
92
93
|
### Run it
|
|
93
94
|
|
|
@@ -100,11 +101,11 @@ spex dashboard # the board UI on :5173, proxying /api to the backend
|
|
|
100
101
|
|
|
101
102
|
Open <http://localhost:5173>.
|
|
102
103
|
|
|
103
|
-
Both ports are flags (`spex serve --port 8788`, `spex dashboard --port 5174 --api-port 8788`), so
|
|
104
|
-
run several projects' boards side by side
|
|
105
|
-
each tab its own identity in that project's `spexcode.json`: `dashboard.title` names it
|
|
106
|
-
`dashboard.icon` sets the favicon — an emoji (`"🔭"`), an Iconify name (`"mdi:rocket-launch"`),
|
|
107
|
-
|
|
104
|
+
Both ports are flags (`spex serve --port 8788`, `spex dashboard --port 5174 --api-port 8788`), so
|
|
105
|
+
you can run several projects' boards side by side; the working directory picks which project each
|
|
106
|
+
serves. Give each tab its own identity in that project's `spexcode.json`: `dashboard.title` names it
|
|
107
|
+
and `dashboard.icon` sets the favicon — an emoji (`"🔭"`), an Iconify name (`"mdi:rocket-launch"`),
|
|
108
|
+
or a URL.
|
|
108
109
|
|
|
109
110
|
Day to day (the commands your agent runs for you — and that you can run yourself):
|
|
110
111
|
|
|
@@ -115,15 +116,12 @@ Day to day (the commands your agent runs for you — and that you can run yourse
|
|
|
115
116
|
| `spex guide` | print the full workflow, plus the `spec.md` / `yatsu.md` / `config` manuals |
|
|
116
117
|
| `spex board` | dump the current board state as JSON |
|
|
117
118
|
|
|
118
|
-
The spec tree is ground truth and git is its database: every change is a `spec.md` node, **rewritten in
|
|
119
|
-
place** (never a `## vN` changelog) and versioned by its commits.
|
|
120
|
-
|
|
121
119
|
---
|
|
122
120
|
|
|
123
121
|
## Contributing to SpexCode
|
|
124
122
|
|
|
125
|
-
This repository *is* the SpexCode source, and it
|
|
126
|
-
spec node merged into `main`. Set up a checkout:
|
|
123
|
+
This repository *is* the SpexCode source, and it dogfoods itself: every change to the tool lands as
|
|
124
|
+
a spec node merged into `main`. Set up a checkout:
|
|
127
125
|
|
|
128
126
|
```sh
|
|
129
127
|
git clone https://github.com/shuxueshuxue/spexcode && cd spexcode
|
|
@@ -132,8 +130,8 @@ npm --prefix spec-dashboard install
|
|
|
132
130
|
npm run hooks # install the per-clone git hooks (main-guard + the session-stamp hook)
|
|
133
131
|
```
|
|
134
132
|
|
|
135
|
-
The development loop runs from source, with hot
|
|
136
|
-
|
|
133
|
+
The development loop runs from source, with hot reload (`npm run web`, as opposed to an installed
|
|
134
|
+
user's `spex dashboard`):
|
|
137
135
|
|
|
138
136
|
```sh
|
|
139
137
|
npm run api # backend on :8787, hot-reloads on spec-cli/src changes
|
|
@@ -142,6 +140,11 @@ npm run web # the dashboard via Vite (HMR), proxying /api → :8787
|
|
|
142
140
|
|
|
143
141
|
---
|
|
144
142
|
|
|
143
|
+
## Credits
|
|
144
|
+
|
|
145
|
+
SpexCode was first introduced publicly on [LINUX DO](https://linux.do) — thanks to the community
|
|
146
|
+
there for the first round of discussion and feedback.
|
|
147
|
+
|
|
145
148
|
## License
|
|
146
149
|
|
|
147
150
|
[MIT](./LICENSE).
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
<img src="docs/sdd-tuxedo-pooh.png" alt="写代码 vs. 维护一份活的、可执行的规格文档" width="420">
|
|
2
|
+
|
|
3
|
+
[English](./README.md) | 中文
|
|
4
|
+
|
|
5
|
+
> Spec 驱动开发通常死于两种方式:spec 和代码脱节,或者膨胀成一堆过时的仪式文档。SpexCode 让每个
|
|
6
|
+
> spec 保持简短、保持当前态:原地重写,由 git 记版本,不做累积式的 changelog。
|
|
7
|
+
|
|
8
|
+
**SpexCode** 是一个 spec 驱动、用自己开发自己的开发工具。项目的每个部分都是一个带版本的
|
|
9
|
+
*spec 节点*,即一个 `.spec/**/spec.md`,正文描述这个部分当前的意图。git 就是数据库:节点的版本号
|
|
10
|
+
是它的内容 commit 数,"drift" 指被管辖的代码走到了 spec 前面。`spex` CLI 和实时看板直接从 git
|
|
11
|
+
读取这一切,没有第二份存储。
|
|
12
|
+
|
|
13
|
+
- **[使用 SpexCode](#使用-spexcode)**:安装 `spex` CLI,通过你的 coding agent 治理*你自己的*项目。
|
|
14
|
+
- **[参与开发](#参与开发)**:在本仓库里改进工具本身。
|
|
15
|
+
- **[Working with agents →](https://spexcode.net/working-with-agents/)**:文档站上更完整的介绍。
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 使用 SpexCode
|
|
20
|
+
|
|
21
|
+
SpexCode 只需要设置一次(`npm i -g spexcode`,然后在你的仓库里 `spex init`)。之后的主要用法是和
|
|
22
|
+
你的 coding agent 对话:用自然语言说你要什么(*"给鉴权流程加一个 spec 节点""把这个包的 spec
|
|
23
|
+
提取出来""派一个 worker 去实现 Y"*),agent 替你执行 `spex` CLI,你在看板上盯进度。下面的手动
|
|
24
|
+
CLI 是底座,agent 才是日常界面。
|
|
25
|
+
|
|
26
|
+
这套之所以成立,是因为新启动的 agent 已经认识 SpexCode。`spex init` 会把整套约定(spec 节点流程、
|
|
27
|
+
先 commit 再声明完成、merge 方式)生成到仓库 `CLAUDE.md`/`AGENTS.md` 里的 `<!-- spexcode -->`
|
|
28
|
+
托管块中,**[Claude Code](https://www.anthropic.com/claude-code)** 和 **Codex** 启动时自动读到。
|
|
29
|
+
更细的内容 agent 自己按需查内置手册:`spex guide`(工作流)、`spex guide spec` /
|
|
30
|
+
`spex guide yatsu`(文件格式)、`spex guide config`(`spexcode.json` 的全部配置项)。你可以直接
|
|
31
|
+
对它说*"跑一下 `spex guide config`,帮我配好 launcher"*。
|
|
32
|
+
|
|
33
|
+
完整的说明在文档站:**[working with agents](https://spexcode.net/working-with-agents/)** 讲
|
|
34
|
+
agent 驱动的用法,**[getting started](https://spexcode.net/getting-started/)** 从头到尾走一遍安装。
|
|
35
|
+
|
|
36
|
+
> **抛开 agent,它也是一套普通工具。** 核心部分单独用也成立:git 记版本的 spec 文件,`spex lint`
|
|
37
|
+
> 检查,只读看板展示。不含 AI,除了 Node 和 git 什么都不用跑。vibe coding 的玩法建立在这层之上,
|
|
38
|
+
> 不是替代它。
|
|
39
|
+
|
|
40
|
+
> **环境要求。** 核心:**Node ≥ 22** 和 **git**。要通过 agent 驱动(或者往节点上派 worker),
|
|
41
|
+
> 还需要 **tmux** 和 PATH 里已登录的 **Claude Code 或 Codex**。这些 agent 会在你的机器上执行
|
|
42
|
+
> 命令,对外暴露后端之前请先读 [`SECURITY.md`](./docs/SECURITY.md)。
|
|
43
|
+
|
|
44
|
+
### 安装
|
|
45
|
+
|
|
46
|
+
装一次发布版 CLI,然后在任何项目里接入:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
npm i -g spexcode # 安装 spex 命令(需要 Node ≥ 22)
|
|
50
|
+
cd ~/my-app
|
|
51
|
+
spex init # 纯增量,不会动你的代码结构
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`spex init` 是增量式的:生成一棵起始 **`.spec/`** 树(根 `project` 节点,加上定义开发流程的
|
|
55
|
+
`.config` 插件)、一份起始 **`spexcode.json`**、每个 clone 各自的 **git hooks**(`pre-commit`
|
|
56
|
+
跑 **spec-lint**,spec↔code 链接坏了会拦下;**main-guard** 拦截直接向 `main` 的提交;
|
|
57
|
+
`prepare-commit-msg` 给每个 commit 盖 session 归属戳)。同时生成 agent 路径需要的产物:
|
|
58
|
+
`CLAUDE.md`/`AGENTS.md` 里的 `<!-- spexcode -->` 契约块,以及 `.claude/`、`.codex/` 的
|
|
59
|
+
settings hooks。这些产物是生成物,已被 gitignore,每台机器各自重新生成,不进版本库。
|
|
60
|
+
|
|
61
|
+
然后把它变成你的项目,让 agent 改或者自己动手都行:编辑 `.spec/project/spec.md` 描述项目,把
|
|
62
|
+
`spexcode.json` 的 `lint.governedRoots` 指向真实源码目录,再检查一遍:
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
spex lint # coverage 警告就是你的接入 TODO 清单
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### 配置
|
|
69
|
+
|
|
70
|
+
仓库根目录两个可选的 JSON 文件承载全部设置,按可移植性分开:
|
|
71
|
+
|
|
72
|
+
- **`spexcode.json`**:提交进仓库,可移植。布局、看板标识(`title` + `icon`)、lint 预算、
|
|
73
|
+
launcher 的**名字**。对整个项目成立的事实。
|
|
74
|
+
- **`spexcode.local.json`**:gitignore,单机。launcher 命令的绝对路径、证书和密钥路径、私有覆盖
|
|
75
|
+
开关(`private: true`,让 `spex materialize` 在被跟踪的文件里不留任何痕迹,ignore 写进本地
|
|
76
|
+
git exclude 而不是提交的 `.gitignore`,适合你参与但不拥有的仓库)。只对这台机器成立的事实。
|
|
77
|
+
|
|
78
|
+
没有 `spex config set`,你(或你的 agent)直接编辑文件。每个字段的含义、该放哪个文件,
|
|
79
|
+
**`spex guide config`** 是权威手册。
|
|
80
|
+
|
|
81
|
+
### 运行
|
|
82
|
+
|
|
83
|
+
启动后端和看板,打开页面:
|
|
84
|
+
|
|
85
|
+
```sh
|
|
86
|
+
spex serve # 后端(API + sessions),:8787
|
|
87
|
+
spex dashboard # 看板 UI,:5173,/api 代理到后端
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
打开 <http://localhost:5173>。
|
|
91
|
+
|
|
92
|
+
两个端口都是参数(`spex serve --port 8788`、`spex dashboard --port 5174 --api-port 8788`),
|
|
93
|
+
多个项目的看板可以并排跑,工作目录决定各自服务哪个项目。每个标签页的身份在各自项目的
|
|
94
|
+
`spexcode.json` 里配:`dashboard.title` 定名字,`dashboard.icon` 定 favicon,emoji(`"🔭"`)、
|
|
95
|
+
Iconify 名字(`"mdi:rocket-launch"`)或 URL 都行。
|
|
96
|
+
|
|
97
|
+
日常命令(agent 替你跑,你也可以自己跑):
|
|
98
|
+
|
|
99
|
+
| 命令 | 作用 |
|
|
100
|
+
| --- | --- |
|
|
101
|
+
| `spex lint` | 检查 spec↔code 图:coverage、drift、living 规则 |
|
|
102
|
+
| `spex watch` | 实时输出 session / 看板的状态变化 |
|
|
103
|
+
| `spex guide` | 打印完整工作流,以及 `spec.md` / `yatsu.md` / `config` 手册 |
|
|
104
|
+
| `spex board` | 以 JSON 输出当前看板状态 |
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## 参与开发
|
|
109
|
+
|
|
110
|
+
这个仓库就是 SpexCode 的源码,而且它 dogfood 自己:工具的每个改动都以 spec 节点的形式合入
|
|
111
|
+
`main`。搭一个开发环境:
|
|
112
|
+
|
|
113
|
+
```sh
|
|
114
|
+
git clone https://github.com/shuxueshuxue/spexcode && cd spexcode
|
|
115
|
+
npm --prefix spec-cli install
|
|
116
|
+
npm --prefix spec-dashboard install
|
|
117
|
+
npm run hooks # 安装每个 clone 各自的 git hooks(main-guard + session 戳)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
开发循环直接跑源码,带热重载(开发用 `npm run web`,区别于安装用户的 `spex dashboard`):
|
|
121
|
+
|
|
122
|
+
```sh
|
|
123
|
+
npm run api # 后端 :8787,spec-cli/src 改动即热重载
|
|
124
|
+
npm run web # Vite 起看板(HMR),/api 代理到 :8787
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## 致谢
|
|
130
|
+
|
|
131
|
+
SpexCode 的首次公开介绍发在 [LINUX DO](https://linux.do),感谢佬友们的第一轮讨论和反馈。
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
[MIT](./LICENSE)。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spexcode",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "SpexCode — a spec-driven, self-developing dev tool. The `spex` CLI + spec server reads the .spec tree and its git history, and serves the dashboard.",
|
|
6
6
|
"license": "MIT",
|
package/spec-cli/src/cli.ts
CHANGED
|
@@ -37,7 +37,7 @@ function flushExit(code = 0): Promise<never> {
|
|
|
37
37
|
}
|
|
38
38
|
const has = (name: string) => process.argv.includes(`--${name}`)
|
|
39
39
|
// bare positionals after argv index `from`, skipping flags and their values (selectors for ls/watch).
|
|
40
|
-
const VALUE_FLAGS = new Set(['--status', '--as', '--interval', '--propose', '--note', '--node', '--prompt', '--timeout', '--reason', '--out', '--password', '--tls-cert', '--tls-key', '--harness', '--launcher', '--harness-session', '--port', '--api-port', '--preset'])
|
|
40
|
+
const VALUE_FLAGS = new Set(['--status', '--as', '--interval', '--propose', '--note', '--node', '--prompt', '--timeout', '--reason', '--out', '--password', '--tls-cert', '--tls-key', '--harness', '--launcher', '--harness-session', '--port', '--api-port', '--host', '--preset'])
|
|
41
41
|
function positionals(from: number): string[] {
|
|
42
42
|
const out: string[] = []
|
|
43
43
|
for (let i = from; i < process.argv.length; i++) {
|
|
@@ -137,13 +137,15 @@ if (cmd === 'serve') {
|
|
|
137
137
|
}
|
|
138
138
|
await import('./supervise.js')
|
|
139
139
|
} else if (cmd === 'dashboard') {
|
|
140
|
-
// the natural post-install UI: serve the bundled dashboard on its OWN
|
|
141
|
-
// the
|
|
140
|
+
// the natural post-install UI: serve the bundled dashboard on its OWN port (loopback by default;
|
|
141
|
+
// --host widens the bind for LAN/tailnet viewing), proxying /api + the terminal socket to a
|
|
142
|
+
// separately-run `spex serve`. Replaces the dogfood-only `npm run web` (vite).
|
|
142
143
|
const { serveDashboardLocal } = await import('./gateway.js')
|
|
143
144
|
const port = Number(flag('port') ?? process.env.SPEXCODE_DASHBOARD_PORT ?? 5173)
|
|
144
145
|
const apiPort = Number(flag('api-port') ?? process.env.PORT ?? 8787)
|
|
146
|
+
const host = flag('host') ?? '127.0.0.1'
|
|
145
147
|
if (!Number.isInteger(port) || !Number.isInteger(apiPort)) { console.error('spex dashboard: --port and --api-port must be integers'); process.exit(2) }
|
|
146
|
-
serveDashboardLocal({ port, apiPort })
|
|
148
|
+
serveDashboardLocal({ port, apiPort, host })
|
|
147
149
|
} else if (cmd === undefined || cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
148
150
|
// `spex help <cmd>` drills into one command; bare help is the map. Both name the next layer down.
|
|
149
151
|
const { commandHelp, overviewHelp } = await import('./help.js')
|
|
@@ -421,7 +423,14 @@ if (cmd === 'serve') {
|
|
|
421
423
|
} else if (sub === 'done') {
|
|
422
424
|
// sugar for awaiting; --propose merge|nothing|close, optional --note
|
|
423
425
|
const p = (flag('propose') as any) || 'nothing'
|
|
424
|
-
|
|
426
|
+
let closeNote = p === 'close' ? CLOSE_CLEANUP : ''
|
|
427
|
+
if (p === 'close') {
|
|
428
|
+
// the DATA half of the close nudge ([[local-issues]] closeoutNudge): the still-open local issues this
|
|
429
|
+
// session touched, listed by id — empty/OFF/no-identity prints nothing. Loud on failure but never
|
|
430
|
+
// gating: the declaration must land whatever the issue store is doing.
|
|
431
|
+
try { closeNote += (await import('./localIssues.js')).closeoutNudge(sess ?? s.ownSessionId()) }
|
|
432
|
+
catch (e) { console.error(`issue closeout check failed (declaration unaffected): ${e instanceof Error ? e.message : e}`) }
|
|
433
|
+
}
|
|
425
434
|
console.log(s.markDone(p, sess) ? `done (${p})${DECLARED}${closeNote}` : 'no session record')
|
|
426
435
|
} else if (sub === 'park') {
|
|
427
436
|
// sugar: the agent is waiting on a background task; it will self-resume (NOT idle/awaiting)
|
package/spec-cli/src/gateway.ts
CHANGED
|
@@ -173,15 +173,17 @@ export function startGateway(opts: GatewayOpts): void {
|
|
|
173
173
|
socket.on('error', bail); up.on('error', bail)
|
|
174
174
|
})
|
|
175
175
|
|
|
176
|
-
// `spex dashboard` passes an explicit loopback host; `--public` passes
|
|
177
|
-
// original behaviour, IPv4+IPv6), so adding the local path never
|
|
178
|
-
|
|
176
|
+
// `spex dashboard` passes an explicit host (loopback by default, --host to widen); `--public` passes
|
|
177
|
+
// none → bind ALL interfaces (the original behaviour, IPv4+IPv6), so adding the local path never
|
|
178
|
+
// narrows the public gateway's reach. The gate note keys on LOOPBACK, not on host-being-explicit:
|
|
179
|
+
// an ungated loopback bind is normal, an ungated wide bind is announced — never silent.
|
|
180
|
+
const isLoopback = opts.host === '127.0.0.1' || opts.host === 'localhost' || opts.host === '::1'
|
|
179
181
|
const onListen = () => {
|
|
180
182
|
const scheme = secure ? 'https' : 'http'
|
|
181
183
|
const label = opts.label ?? 'public mode'
|
|
182
|
-
const gate =
|
|
183
|
-
console.log(`[gateway] ${label} on ${scheme}://${
|
|
184
|
-
if (!secure && !
|
|
184
|
+
const gate = isLoopback ? '' : ` — ${gated ? 'password-gated' : 'OPEN (no password)'}`
|
|
185
|
+
console.log(`[gateway] ${label} on ${scheme}://${isLoopback ? 'localhost' : (opts.host ?? '0.0.0.0')}:${opts.publicPort}${gate}, proxying /api to :${opts.upstreamPort}`)
|
|
186
|
+
if (!secure && !isLoopback && !opts.host) console.log('[gateway] (TLS off — --http)')
|
|
185
187
|
}
|
|
186
188
|
// a busy public port is a hard, loud, non-zero exit — the SAME contract as the supervisor's proxy
|
|
187
189
|
// (see [[spec-cli]] / listen.ts), so `spex serve` and `spex dashboard` fail a port clash identically.
|
|
@@ -270,13 +272,15 @@ export function ensureDashboardBuilt(repoRoot: string, distDir: string): void {
|
|
|
270
272
|
}
|
|
271
273
|
|
|
272
274
|
// @@@ serveDashboardLocal - the engine behind `spex dashboard`: the SAME gateway as public mode, bound to
|
|
273
|
-
// loopback with no TLS and no password
|
|
274
|
-
//
|
|
275
|
-
//
|
|
276
|
-
|
|
275
|
+
// loopback by default with no TLS and no password — `--host` widens the bind to a chosen interface
|
|
276
|
+
// (LAN/tailnet viewing) while staying plain HTTP; the internet face remains `spex serve --public`.
|
|
277
|
+
// It serves the bundled dist and proxies /api + the terminal socket to a separately-run `spex serve`.
|
|
278
|
+
// This is the post-install replacement for the dogfood-only `npm run web` (a vite dev server an
|
|
279
|
+
// installed user has no source tree for). See [[packaging]].
|
|
280
|
+
export function serveDashboardLocal(opts: { port: number; apiPort: number; host?: string }): void {
|
|
277
281
|
const pkgRoot = fileURLToPath(new URL('..', import.meta.url))
|
|
278
282
|
const distDir = resolveDistDir()
|
|
279
283
|
ensureDashboardBuilt(join(pkgRoot, '..'), distDir) // bundled dist already has index.html → returns at once
|
|
280
284
|
console.log(`[dashboard] serving ${distDir.endsWith('dashboard-dist') ? 'bundled' : 'monorepo'} build, /api → backend :${opts.apiPort}`)
|
|
281
|
-
startGateway({ host: '127.0.0.1', publicPort: opts.port, upstreamPort: opts.apiPort, password: '', tls: null, distDir, label: 'dashboard' })
|
|
285
|
+
startGateway({ host: opts.host ?? '127.0.0.1', publicPort: opts.port, upstreamPort: opts.apiPort, password: '', tls: null, distDir, label: 'dashboard' })
|
|
282
286
|
}
|
package/spec-cli/src/guide.ts
CHANGED
|
@@ -3,9 +3,13 @@ const SETUP = `spex guide — run SpexCode on your own repo
|
|
|
3
3
|
The product model: install SpexCode ONCE, then use it across all your projects — an agent drives
|
|
4
4
|
the rest, you don't hand-author the spec tree or wire the dashboard yourself.
|
|
5
5
|
|
|
6
|
-
1. Install the CLI (one-time, global —
|
|
7
|
-
|
|
6
|
+
1. Install the CLI (one-time, global — ONE install serves every project)
|
|
7
|
+
npm i -g spexcode # ONE command lands on PATH: \`spex\` (Node ≥ 22)
|
|
8
8
|
It always operates on the repo of your current directory — that cwd is the only "which repo" knob.
|
|
9
|
+
(Dogfooding an unpublished HEAD from a source checkout? \`npm link\` at the repo ROOT — that links
|
|
10
|
+
the \`spexcode\` package itself, never the internal @spexcode/spec-cli. Both paths own the same
|
|
11
|
+
\`spex\` bin, so uninstall one before switching (\`npm rm -g spexcode\`). A source link ships no
|
|
12
|
+
prebuilt dashboard dist — \`spex dashboard\` needs a manual dashboard build, or use the dev server.)
|
|
9
13
|
|
|
10
14
|
2. Adopt a repo
|
|
11
15
|
cd <your-repo> && spex init # seeds .spec/ + git hooks (additive, never overwrites)
|
|
@@ -17,11 +21,11 @@ the rest, you don't hand-author the spec tree or wire the dashboard yourself.
|
|
|
17
21
|
Serve a different repo by running it from there; two repos at once = two \`spex serve\` on two PORTs.
|
|
18
22
|
|
|
19
23
|
4. Open the dashboard — the SAME board for every project, pointed per project
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
The board is a viewer
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
spex dashboard # serves the bundled board on :5173, proxying /api
|
|
25
|
+
Point it at another backend with --api-port (pairs with \`spex serve --port\`); one dashboard per
|
|
26
|
+
project. The board is a viewer — which backend it proxies is the only "which project" knob.
|
|
27
|
+
(Dogfood/source alternative: API_URL=http://localhost:<port> npm run dev in spec-dashboard —
|
|
28
|
+
the dev server; "dashboard": { "apiUrl": "..." } in spexcode.json applies only to that layout.)
|
|
25
29
|
|
|
26
30
|
5. Govern your layout (optional)
|
|
27
31
|
spexcode.json sets lint's governedRoots/sourceExtensions and any non-default worktree layout.
|
package/spec-cli/src/help.ts
CHANGED
|
@@ -276,10 +276,12 @@ TLS (own cert via --tls-cert/--tls-key; --http drops TLS).`,
|
|
|
276
276
|
},
|
|
277
277
|
dashboard: {
|
|
278
278
|
line: 'dashboard serve the dashboard UI (default :5173), proxying /api to a running serve',
|
|
279
|
-
body: `Usage: spex dashboard [--port N=5173] [--api-port N=8787]
|
|
279
|
+
body: `Usage: spex dashboard [--port N=5173] [--api-port N=8787] [--host H=127.0.0.1]
|
|
280
280
|
|
|
281
281
|
Serves the bundled dashboard on its own port and proxies /api + the terminal socket to a running
|
|
282
|
-
\`spex serve\`. The installed replacement for the dev-only \`npm run web
|
|
282
|
+
\`spex serve\`. The installed replacement for the dev-only \`npm run web\`. Loopback-only by default;
|
|
283
|
+
--host 0.0.0.0 (or a specific interface) opens it to a LAN/tailnet — still plain HTTP with no gate,
|
|
284
|
+
so bind wide only on a network you trust (for the internet, use \`spex serve --public\`).`,
|
|
283
285
|
see: 'spex serve (must be running first)',
|
|
284
286
|
},
|
|
285
287
|
|
package/spec-cli/src/index.ts
CHANGED
|
@@ -4,8 +4,8 @@ import { cors } from 'hono/cors'
|
|
|
4
4
|
import { etag } from 'hono/etag'
|
|
5
5
|
import { createNodeWebSocket } from '@hono/node-ws'
|
|
6
6
|
import { loadSpecs, loadSpecsLite, specContent, specHistory, specDiffAt, loadConfig } from './specs.js'
|
|
7
|
-
import { issuesEnabled,
|
|
8
|
-
import { closeIssue, mergedIssues, replyIssue } from './issues.js'
|
|
7
|
+
import { issuesEnabled, remarkOnHost, resolveRemark, retractRemark } from './localIssues.js'
|
|
8
|
+
import { closeIssue, createIssue, issueStores, mergedIssues, replyIssue } from './issues.js'
|
|
9
9
|
import { residentForgeState, refreshForgeNow } from '../../spec-forge/src/resident.js'
|
|
10
10
|
import { summarize } from './mentions.js'
|
|
11
11
|
import { resolveLayout, mainBranch } from './layout.js'
|
|
@@ -159,6 +159,7 @@ app.get('/api/launchers', (c) => c.json({
|
|
|
159
159
|
app.get('/api/issues', etag(), (c) =>
|
|
160
160
|
c.json({
|
|
161
161
|
enabled: issuesEnabled(),
|
|
162
|
+
stores: issueStores(),
|
|
162
163
|
issues: mergedIssues({ host: 'github', state: residentForgeState() }, loadSpecsLite().map((s) => s.id)),
|
|
163
164
|
}))
|
|
164
165
|
// the WRITE surface ([[local-issues]] / [[issues-view]]) — the human reply path, STORE-ROUTED through the one
|
|
@@ -211,10 +212,16 @@ app.post('/api/issues', async (c) => {
|
|
|
211
212
|
if (!concern) return c.json({ error: 'empty concern' }, 400)
|
|
212
213
|
const nodes = Array.isArray(body?.nodes) ? (body.nodes as unknown[]).filter((n): n is string => typeof n === 'string') : []
|
|
213
214
|
const postBody = typeof body?.body === 'string' ? body.body : undefined
|
|
215
|
+
const store = typeof body?.store === 'string' && body.store.trim() ? body.store.trim() : 'local'
|
|
214
216
|
// typed evidence[] — yatsu content-addressed hashes (the annotator's clip reference rides here, not prose)
|
|
215
217
|
const evidence = Array.isArray(body?.evidence) ? (body.evidence as unknown[]).filter((h): h is string => typeof h === 'string' && /^[0-9a-f]{64}$/.test(h)) : []
|
|
216
|
-
|
|
217
|
-
|
|
218
|
+
try {
|
|
219
|
+
const r = await createIssue(concern, { store, nodes, body: postBody, evidence, author: 'human' })
|
|
220
|
+
if (r.store !== 'local') await refreshForgeNow()
|
|
221
|
+
return c.json({ ok: true, id: r.id, store: r.store, url: r.url, outcomes: summarize(r.outcomes) }, 201)
|
|
222
|
+
} catch (e) {
|
|
223
|
+
return c.json({ error: String((e as Error).message || e) }, store === 'local' ? 500 : 502)
|
|
224
|
+
}
|
|
218
225
|
})
|
|
219
226
|
|
|
220
227
|
// the REMARK write surface ([[remark-substrate]]) — server PARITY with the CLI: the dashboard can author /
|