@weotro/dx 0.2.2 → 0.3.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/README.md +47 -28
- package/bin/dx.js +59 -53
- package/example/README.md +26 -11
- package/example/dx/contract.json +106 -0
- package/example/dx/lib/arguments.mjs +6 -0
- package/example/dx/lib/arguments.test.mjs +20 -0
- package/example/dx/lib/commands.mjs +48 -0
- package/example/dx/scripts/build +1 -1
- package/example/dx/scripts/db +1 -1
- package/example/dx/scripts/deploy +1 -1
- package/example/dx/scripts/lint +2 -0
- package/example/dx/scripts/setup +1 -1
- package/example/dx/scripts/start +2 -12
- package/example/dx/scripts/test +1 -1
- package/lib/contract.js +135 -0
- package/lib/env.js +2 -11
- package/lib/invocation.js +79 -0
- package/package.json +8 -1
- package/skills/dx-adopt/SKILL.md +33 -66
- package/skills/dx-adopt/references/adoption-regressions.md +61 -8
- package/skills/dx-adopt/references/dx-0.3-contract.md +99 -0
- package/skills/dx-adopt/references/switch-to-0.3.md +61 -0
- package/skills/git-release/SKILL.md +4 -4
- package/skills/git-release/references/post-release-follow-up.md +2 -2
- package/skills/project-agent-standards/SKILL.md +14 -4
- package/skills/dx-adopt/references/dx-0.2-contract.md +0 -95
- package/skills/dx-adopt/references/migrate-0.1.md +0 -50
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ dx initial --help
|
|
|
23
23
|
|
|
24
24
|
`initial` 用于安装和更新技能,`setup` 用于当前项目的准备;两者独立,不互相自动调用。自定义应用命令不得使用保留名称 `initial`。技能中的外部工具需另行安装和认证,initial 只同步技能文件。
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
首次接入或升级的应用可使用随包的 [dx-adopt](skills/dx-adopt/SKILL.md) 技能,例如“使用 $dx-adopt 为当前项目接入 dx 0.3.0,并完成验证”。技能会生成项目自己的脚本及契约声明,适配环境、setup 和调用方。
|
|
27
27
|
|
|
28
28
|
## 应用提供脚本
|
|
29
29
|
|
|
@@ -35,14 +35,15 @@ dx/scripts/
|
|
|
35
35
|
test
|
|
36
36
|
db
|
|
37
37
|
deploy
|
|
38
|
+
lint
|
|
38
39
|
```
|
|
39
40
|
|
|
40
|
-
|
|
41
|
+
七个入口全部必选。每个入口都是无扩展名的可执行文件,用 shebang 指定解释器绝对路径;首行不超过 255 字节(不含换行),避免系统截断或回退执行。
|
|
41
42
|
|
|
42
43
|
```sh
|
|
43
44
|
#!/bin/sh
|
|
44
|
-
# dx/
|
|
45
|
-
exec
|
|
45
|
+
# dx/lib/build.mjs 由应用实现目标和工具参数路由。
|
|
46
|
+
exec node dx/lib/build.mjs "$@"
|
|
46
47
|
```
|
|
47
48
|
|
|
48
49
|
```bash
|
|
@@ -50,7 +51,7 @@ chmod +x dx/scripts/build
|
|
|
50
51
|
dx build admin
|
|
51
52
|
```
|
|
52
53
|
|
|
53
|
-
Node、Shell
|
|
54
|
+
Node、Shell 或其他语言均可使用同一接口。新增合法名称的脚本即可增加扩展命令,例如 `dx/scripts/integration`;名称须匹配 `[a-z][a-z0-9-]*`。`check` 和 `initial` 是保留名称。
|
|
54
55
|
|
|
55
56
|
## 标准命令语义
|
|
56
57
|
|
|
@@ -59,12 +60,23 @@ Node、Shell 或其他语言均可使用同一接口。新增合法名称的脚
|
|
|
59
60
|
| `dx setup` | dx 基础 setup 完成后,执行应用准备,例如安装依赖、生成文件 |
|
|
60
61
|
| `dx start stack` | 启动 monorepo 定义的全部服务,默认 dev 环境 |
|
|
61
62
|
| `dx start admin` | 只启动指定应用,默认 dev 环境 |
|
|
62
|
-
| `dx build
|
|
63
|
-
| `dx
|
|
64
|
-
| `dx
|
|
65
|
-
| `dx
|
|
63
|
+
| `dx build <目标>` | 构建;all 表示全部构建目标 |
|
|
64
|
+
| `dx lint <目标>` | 静态检查;all 表示全部检查目标 |
|
|
65
|
+
| `dx test <unit\|e2e> <目标>` | 执行指定类型测试;all 表示该类型全部测试 |
|
|
66
|
+
| `dx db <migrate\|seed\|reset\|status> <目标>` | 数据库操作,只接受单目标 |
|
|
67
|
+
| `dx deploy <目标>` | 部署;all 表示全部部署目标 |
|
|
66
68
|
|
|
67
|
-
|
|
69
|
+
除 setup 无位置参数外,表中类型、动作和目标都必填;多余位置参数也报错。start 使用 stack 聚合,不接受 all;build/lint/deploy/test 使用 all,不接受 stack;db 禁止 all/stack。目标由应用声明,聚合行为由应用实现,dx 不自动编排。setup 需要显式执行,其他命令不会自动触发它。
|
|
70
|
+
|
|
71
|
+
## 应用契约与检查
|
|
72
|
+
|
|
73
|
+
工程必须提供 `dx/contract.json`,使用 `schemaVersion: 1`,按完整调用组合声明七个标准命令的目标及 `implemented` / `mock` 状态。完整字段与可用 JSON 示例见[契约参考](skills/dx-adopt/references/dx-0.3-contract.md#契约声明)。扩展命令不写入声明。
|
|
74
|
+
|
|
75
|
+
不适用能力可提供 mock:声明非空原因,应用实际输出命令名称和原因并返回 0,表示明确跳过。需要但尚未完成的功能不能冒充不适用;unit 所有目标必须真实实现,E2E 可 mock。start stack、build/lint/deploy all、test unit/e2e all 必须显式声明;db 四个动作各至少一个目标,无数据库时可分别声明 main mock。
|
|
76
|
+
|
|
77
|
+
`dx check` 聚合静态契约错误,通过返回 0,违规返回 1;不加载环境、不执行 setup 或应用脚本。它检查声明结构、必需组合、mock 规则及全部公开入口(含扩展)的名称、文件、执行权限和 shebang。真实应用调用(含扩展帮助)也必须先通过全量检查及本次语法、目标和环境校验,失败发生在环境加载或 setup 复制之前。没有兼容模式或跳过检查开关。
|
|
78
|
+
|
|
79
|
+
检查通过只证明静态接口合规;dx 不扫描脚本文本猜测实现,也不代替应用执行 mock。接入验收分别记录真实功能运行、mock 日志与退出码、未验证项,声明 implemented 不能代替实际测试证据。
|
|
68
80
|
|
|
69
81
|
## 基础 setup
|
|
70
82
|
|
|
@@ -86,31 +98,32 @@ Node、Shell 或其他语言均可使用同一接口。新增合法名称的脚
|
|
|
86
98
|
调用者环境变量 < .env.<环境> < .env.<环境>.local
|
|
87
99
|
```
|
|
88
100
|
|
|
89
|
-
不加载通用 `.env`、`.env.local` 或旧 `dx/config/env-layers.json
|
|
90
|
-
|
|
91
|
-
环境选择依次使用:
|
|
101
|
+
不加载通用 `.env`、`.env.local` 或旧 `dx/config/env-layers.json`。文件缺失时跳过,读取失败时停止执行并提示文件路径;顶层及标准命令帮助和版本查询不加载文件,扩展命令帮助仍加载环境。
|
|
92
102
|
|
|
93
|
-
|
|
94
|
-
2. 调用者的 `APP_ENV`,其次 `NODE_ENV`;都未设置时为 `development`。`dev` / `local` 归一为 `development`。
|
|
103
|
+
命令决定环境,环境变量只传递结果:`test unit` 固定 test,`test e2e` 固定 e2e,首个 `--` 前禁止任何环境标志(包括冗余的 --test/--e2e)。其他应用命令含扩展默认 development,可显式选择 `--dev`、`--staging`、`--prod`、`--test`、`--e2e` 中一个;重复或多个标志均报错。
|
|
95
104
|
|
|
96
|
-
|
|
105
|
+
调用者 APP_ENV/NODE_ENV 和环境文件不参与环境选择。应用收到的 `APP_ENV` 固定为 development / staging / production / test / e2e,`NODE_ENV` 派生为 development / production / test(staging → production,e2e → test),两者在展开变量前确定且不被文件覆盖。
|
|
97
106
|
|
|
98
107
|
```bash
|
|
99
108
|
dx start stack # 默认 development
|
|
100
109
|
dx start admin --prod # 加载 .env.production 和 .env.production.local
|
|
101
|
-
APP_ENV=staging dx
|
|
110
|
+
APP_ENV=staging dx build all # 仍为 development
|
|
111
|
+
dx deploy all --staging # 加载 staging 两层
|
|
112
|
+
dx test unit all # 自动 test,无需 --test
|
|
113
|
+
dx test e2e all # 自动 e2e,无需 --e2e
|
|
102
114
|
dx start admin -- --prod # -- 后的标志仅交给应用
|
|
103
115
|
```
|
|
104
116
|
|
|
105
117
|
`dx start admin --prod` 表示在当前机器加载项目根的 `.env.production` 和 `.env.production.local`,并在当前机器启动 admin;环境标志选择配置及 APP_ENV / NODE_ENV,不会把执行位置切换到生产服务器,也不会自动部署。`--staging` 等同理。应用 start 应保持本机启动语义,远程部署由应用 deploy 等明确入口实现;连接的数据库或 API 在哪里,则由配置中的地址决定。
|
|
106
118
|
|
|
107
|
-
|
|
119
|
+
dx 消费并删除首个 `--` 前的环境标志,应用直接读取注入的环境结果。只有子进程收到合并环境,dx 自身环境不被修改。变量展开遵循 dotenv-expand 语法,可引用同层之前定义的变量、前层和调用者变量;文件中的引号、注释和多行值由 dotenv 解析。
|
|
108
120
|
|
|
109
121
|
## 调用契约
|
|
110
122
|
|
|
111
123
|
- 从当前目录逐级向上寻找最近的 `dx/scripts` 目录,以对应工程根目录作为脚本 cwd。
|
|
112
124
|
- 最近目录缺少所请求脚本时直接报错,不运行外层同名脚本。内层完全没有这个标记时,仍会继续向上查找。
|
|
113
|
-
-
|
|
125
|
+
- 首个 `--` 前只接受规定的位置参数、环境标志及 -h/--help,未知选项报错;扩展命令的位置参数由应用定义。工具专属选项放在 `--` 后。
|
|
126
|
+
- dx 移除已消费的环境标志,保留类型、动作、目标、首个 `--` 和其后所有原始参数(含空字符串和空格),不拼接 shell 命令。例如 `dx build admin --prod -- --mode fast` 传给 build 的参数为 `admin`、`--`、`--mode`、`fast`。
|
|
114
127
|
- 将按环境契约合并的变量传给应用,标准输入输出直接继承;只有显式 `setup` 运行前述基础文件复制,安装和生成由应用负责。
|
|
115
128
|
- 返回脚本退出码。脚本保留前台终端;dx 转发收到的 SIGINT/SIGTERM,并传递脚本最终退出状态。
|
|
116
129
|
- 终端 Ctrl-C 会向前台组广播,应用应幂等处理信号并清理自己的子进程;dx 不监督任意后代或后台服务。Shell 入口可以使用 `exec` 将进程交给实际程序。
|
|
@@ -118,22 +131,28 @@ dx start admin -- --prod # -- 后的标志仅交给应用
|
|
|
118
131
|
```bash
|
|
119
132
|
dx --help # 查看入口和实际可用脚本
|
|
120
133
|
dx --version # 无需目标工程
|
|
121
|
-
dx deploy --help #
|
|
134
|
+
dx deploy --help # dx 统一帮助,允许省略目标
|
|
135
|
+
dx build all -- --help # 运行应用,请求工具帮助
|
|
122
136
|
```
|
|
123
137
|
|
|
124
|
-
|
|
138
|
+
无命令时显示帮助。顶层和标准命令帮助、版本及 initial 不受缺失或损坏的应用契约拦截;标准帮助不加载环境、复制文件或运行脚本。扩展命令帮助仍执行应用,须先通过检查,不承诺无副作用。
|
|
139
|
+
|
|
140
|
+
## 切换到 0.3.0
|
|
141
|
+
|
|
142
|
+
0.3.0 使用本文的强制应用契约。应用须补齐七个入口和声明,按标准类型/动作/目标调用,将工具选项放到 `--` 后,由命令决定环境。先使用实际 0.3.0 包完成 `dx check` 与行为验证,再统一使用方和 CI。完整步骤见 [0.3.0 接入与切换向导](skills/dx-adopt/references/switch-to-0.3.md),经验与常见坑见[适配回归检查](skills/dx-adopt/references/adoption-regressions.md)。
|
|
125
143
|
|
|
126
|
-
|
|
144
|
+
应用将确认、启动编排和部署安全等逻辑放入自身脚本,直接使用 dx 注入的环境,入口及声明检查通过后显式执行 `dx setup`。dx 不自动转换旧配置。
|
|
127
145
|
|
|
128
|
-
|
|
146
|
+
## 随包文档与示例
|
|
129
147
|
|
|
130
|
-
|
|
148
|
+
`dx initial` 会将本版接入向导、完整契约和经验资料同步到 `~/.agents/skills/dx-adopt/`,应用可直接读取。无需访问 dx 源码仓库。
|
|
131
149
|
|
|
132
|
-
|
|
150
|
+
安装包包含本 README 及[可运行示例](example/README.md)。全局包位于 `npm root -g` 输出下的 `@weotro/dx`,本地包位于项目 `node_modules/@weotro/dx`。在包内 example 目录可以验证:
|
|
133
151
|
|
|
134
152
|
```bash
|
|
135
|
-
|
|
136
|
-
|
|
153
|
+
node ../bin/dx.js check
|
|
154
|
+
node ../bin/dx.js test unit all
|
|
155
|
+
node ../bin/dx.js lint all
|
|
137
156
|
```
|
|
138
157
|
|
|
139
|
-
|
|
158
|
+
示例提供真实单测和静态检查,其余不适用能力明确输出 mock 日志,不实际启动服务或部署。
|
package/bin/dx.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { statSync, lstatSync, readdirSync,
|
|
2
|
+
import { statSync, lstatSync, readdirSync, accessSync, constants } from 'node:fs'
|
|
3
3
|
import { dirname, join } from 'node:path'
|
|
4
4
|
import { spawn } from 'node:child_process'
|
|
5
5
|
import { fileURLToPath } from 'node:url'
|
|
@@ -7,27 +7,53 @@ import { logger } from '../lib/logger.js'
|
|
|
7
7
|
import { getPackageVersion } from '../lib/version.js'
|
|
8
8
|
import { loadEnvironment } from '../lib/env.js'
|
|
9
9
|
import { runBaseSetup } from '../lib/setup.js'
|
|
10
|
+
import { inspectContract, inspectScript, assertTarget, standardCommands } from '../lib/contract.js'
|
|
11
|
+
import { parseInvocation } from '../lib/invocation.js'
|
|
10
12
|
|
|
11
13
|
const [command, ...args] = process.argv.slice(2)
|
|
12
|
-
function
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
14
|
+
function findProject() {
|
|
15
|
+
let root = process.cwd()
|
|
16
|
+
for (;;) {
|
|
17
|
+
const marker = join(root, 'dx/scripts')
|
|
18
|
+
let markerExists = false
|
|
19
|
+
try {
|
|
20
|
+
lstatSync(marker)
|
|
21
|
+
markerExists = true
|
|
22
|
+
if (!statSync(marker).isDirectory()) throw new Error(`工程标记 ${marker} 不是目录;请改为脚本目录。`)
|
|
23
|
+
accessSync(marker, constants.R_OK | constants.X_OK)
|
|
24
|
+
break
|
|
25
|
+
} catch (error) {
|
|
26
|
+
if (markerExists || error.code !== 'ENOENT') throw new Error(`无法读取工程标记 ${marker}:${error.message};请检查目录类型、链接目标与读取、搜索权限。`)
|
|
27
|
+
}
|
|
28
|
+
const parent = dirname(root)
|
|
29
|
+
if (parent === root) {
|
|
30
|
+
throw new Error('未找到工程:请按 dx-adopt 创建 dx/scripts 七个标准入口及 dx/contract.json。')
|
|
31
|
+
}
|
|
32
|
+
root = parent
|
|
22
33
|
}
|
|
34
|
+
return root
|
|
23
35
|
}
|
|
36
|
+
|
|
37
|
+
function topLevelHelp() {
|
|
38
|
+
let entries = []
|
|
39
|
+
try {
|
|
40
|
+
const root = findProject()
|
|
41
|
+
entries = readdirSync(join(root, 'dx/scripts')).filter(name =>
|
|
42
|
+
!['check', 'initial'].includes(name) && /^[a-z][a-z0-9-]*$/.test(name)
|
|
43
|
+
&& !inspectScript(join(root, 'dx/scripts', name)),
|
|
44
|
+
).sort()
|
|
45
|
+
} catch { /* Help must remain available while an application is being repaired. */ }
|
|
46
|
+
return `用法:dx <command> [标准参数] [-- 应用参数...]\n内置命令:check(静态契约检查) initial(同步包内 skills,无需应用工程)\n必选标准入口:${standardCommands.join(' ')}\n当前工程入口:${entries.join(' ') || '无'}\n入口:dx/scripts/<command>,需可执行权限和 shebang;声明:dx/contract.json(schemaVersion: 1)。\n-h / --help 显示帮助;--version 显示版本;dx <标准命令> --help 查看统一语法。\n命令决定环境,APP_ENV/NODE_ENV 只传递结果。工具参数放在 -- 后。\n`
|
|
47
|
+
}
|
|
48
|
+
|
|
24
49
|
try {
|
|
25
50
|
if (command === '--version') {
|
|
51
|
+
if (args.length) throw new Error('--version 不接受其他参数。')
|
|
26
52
|
process.stdout.write(`${getPackageVersion()}\n`)
|
|
27
53
|
process.exit(0)
|
|
28
54
|
}
|
|
29
55
|
if (command === 'initial') {
|
|
30
|
-
if (args.length === 1 &&
|
|
56
|
+
if (args.length === 1 && ['--help', '-h'].includes(args[0])) {
|
|
31
57
|
process.stdout.write('用法:dx initial\n将包内 skills 同步到 ~/.agents/skills,并在 ~/.claude/skills 建立链接;更新托管技能并清理历史旧副本。无需应用工程。\n')
|
|
32
58
|
} else {
|
|
33
59
|
if (args.length) throw new Error('initial 不接受应用参数;请使用 dx initial 或 dx initial --help。')
|
|
@@ -36,57 +62,37 @@ try {
|
|
|
36
62
|
}
|
|
37
63
|
process.exit(0)
|
|
38
64
|
}
|
|
39
|
-
|
|
40
|
-
|
|
65
|
+
if (command === undefined || command === '--help' || command === '-h') {
|
|
66
|
+
process.stdout.write(topLevelHelp())
|
|
67
|
+
process.exit(0)
|
|
68
|
+
}
|
|
69
|
+
if (!/^[a-z][a-z0-9-]*$/.test(command)) {
|
|
41
70
|
throw new Error('非法命令名:仅接受小写字母开头,后接小写字母、数字或连字符。')
|
|
42
71
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
break
|
|
53
|
-
} catch (error) {
|
|
54
|
-
if (markerExists || error.code !== 'ENOENT') throw new Error(`无法读取工程标记 ${marker}:${error.message};请检查目录类型、链接目标与读取、搜索权限。`)
|
|
55
|
-
}
|
|
56
|
-
const parent = dirname(root)
|
|
57
|
-
if (parent === root) {
|
|
58
|
-
if (help) { root = null; break }
|
|
59
|
-
throw new Error('未找到工程:请创建 dx/scripts/<command> 可执行脚本。')
|
|
60
|
-
}
|
|
61
|
-
root = parent
|
|
72
|
+
const invocation = parseInvocation(command, args)
|
|
73
|
+
if (invocation.help) {
|
|
74
|
+
process.stdout.write(invocation.help)
|
|
75
|
+
process.exit(0)
|
|
76
|
+
}
|
|
77
|
+
const root = findProject()
|
|
78
|
+
const { contract, errors } = inspectContract(root)
|
|
79
|
+
if (errors.length) {
|
|
80
|
+
throw new Error(`契约检查失败:\n${errors.map(error => `- ${error}`).join('\n')}\n请按 dx-adopt 修复应用契约,再运行 dx check。`)
|
|
62
81
|
}
|
|
63
|
-
if (
|
|
64
|
-
|
|
65
|
-
if (name === 'initial' || !/^[a-z][a-z0-9-]*$/.test(name)) return false
|
|
66
|
-
const entry = join(root, 'dx/scripts', name)
|
|
67
|
-
try {
|
|
68
|
-
if (!statSync(entry).isFile()) return false
|
|
69
|
-
accessSync(entry, constants.R_OK | constants.X_OK)
|
|
70
|
-
return hasShebang(entry)
|
|
71
|
-
} catch { return false }
|
|
72
|
-
}).sort() : []
|
|
73
|
-
process.stdout.write(`用法:dx <command> [args...]\n内置命令:initial(同步包内 skills,无需应用工程)\n标准入口:setup start build test db deploy\n当前工程入口:${entries.join(' ') || '无'}\n入口:dx/scripts/<command>,需可执行权限和 shebang。\n--help 显示帮助;--version 显示版本。应用命令后的参数原样传给应用脚本。\n`)
|
|
82
|
+
if (command === 'check') {
|
|
83
|
+
logger.success('契约检查通过:入口与声明静态合规;真实功能需另行验证。')
|
|
74
84
|
process.exit(0)
|
|
75
85
|
}
|
|
86
|
+
assertTarget(contract, command, invocation.positionals)
|
|
76
87
|
const entry = join(root, 'dx/scripts', command)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
accessSync(entry, constants.R_OK | constants.X_OK)
|
|
80
|
-
} catch (error) {
|
|
81
|
-
throw new Error(`命令 ${command} 无法使用入口 ${entry}:${error.message};请创建带 shebang 的脚本并执行 chmod +x "${entry}",检查读取与执行权限。`)
|
|
82
|
-
}
|
|
83
|
-
if (!hasShebang(entry)) throw new Error(`命令 ${command} 的入口 ${entry} 缺少有效 shebang;请在首行指定解释器绝对路径,例如 #!/bin/sh,首行不得超过 255 字节(不含换行)。`)
|
|
88
|
+
const scriptError = inspectScript(entry)
|
|
89
|
+
if (scriptError) throw new Error(`命令 ${command} 无法使用入口 ${entry}:${scriptError}`)
|
|
84
90
|
if (command === 'setup') runBaseSetup(root)
|
|
85
|
-
const env = loadEnvironment(root,
|
|
91
|
+
const env = loadEnvironment(root, invocation.environment)
|
|
86
92
|
const startupError = error => `命令 ${command} 启动失败,入口 ${entry}:${error.message};请检查 shebang 解释器已安装、PATH 与执行权限。`
|
|
87
93
|
let child
|
|
88
94
|
try {
|
|
89
|
-
child = spawn(entry, args, { cwd: root, env, stdio: 'inherit' })
|
|
95
|
+
child = spawn(entry, invocation.args, { cwd: root, env, stdio: 'inherit' })
|
|
90
96
|
} catch (error) {
|
|
91
97
|
throw new Error(startupError(error))
|
|
92
98
|
}
|
package/example/README.md
CHANGED
|
@@ -1,24 +1,39 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 应用契约最小示例
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
本目录使用 dx 0.3.0 的七个必选入口和 `dx/contract.json`(`schemaVersion: 1`)。所有标准调用的目标及能力状态均显式声明;`dx check` 只证明静态接口合规。
|
|
4
4
|
|
|
5
5
|
在本目录执行:
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
node ../bin/dx.js
|
|
8
|
+
node ../bin/dx.js check
|
|
9
9
|
node ../bin/dx.js setup
|
|
10
10
|
node ../bin/dx.js start stack
|
|
11
11
|
node ../bin/dx.js start admin
|
|
12
|
-
node ../bin/dx.js build
|
|
13
|
-
node ../bin/dx.js
|
|
14
|
-
node ../bin/dx.js
|
|
15
|
-
node ../bin/dx.js
|
|
12
|
+
node ../bin/dx.js build all
|
|
13
|
+
node ../bin/dx.js lint all
|
|
14
|
+
node ../bin/dx.js test unit all
|
|
15
|
+
node ../bin/dx.js test unit all -- --test-name-pattern=preserves
|
|
16
|
+
node ../bin/dx.js test e2e all
|
|
17
|
+
node ../bin/dx.js db status main
|
|
18
|
+
node ../bin/dx.js deploy all
|
|
19
|
+
node ../bin/dx.js test --help
|
|
16
20
|
```
|
|
17
21
|
|
|
18
|
-
|
|
22
|
+
## 实际行为
|
|
19
23
|
|
|
20
|
-
|
|
24
|
+
- `setup` 检查示例资源可用,无需安装外部依赖。
|
|
25
|
+
- `lint all` 使用 Node.js 对应用模块执行真实语法检查。
|
|
26
|
+
- `test unit all` 使用 `node --test` 验证应用实际使用的参数分隔函数,覆盖工具参数边界、空格和空参数。
|
|
27
|
+
- start、build、deploy、db 和 E2E 在本示例中不适用:应用输出包含命令名称和原因的 `[mock]` 跳过日志并返回 0,没有启动服务、构建产物、部署、数据库操作或真实 E2E 验证。接入实际工程时按真实能力替换,不得将待实现功能声明为不适用。
|
|
21
28
|
|
|
22
|
-
|
|
29
|
+
## 参数与环境
|
|
23
30
|
|
|
24
|
-
|
|
31
|
+
命令决定环境。除测试外默认 development,可用 `node ../bin/dx.js build all --prod` 显式选择 production;unit 固定 test,E2E 固定 e2e,测试调用禁止额外环境标志。继承的 `APP_ENV` / `NODE_ENV` 不参与选择,应用只消费 dx 注入的结果,不重新解析环境标志或加载环境文件。
|
|
32
|
+
|
|
33
|
+
标准命令目标必填,setup 无目标。start 的聚合目标为 stack;build/lint/deploy/test 的聚合目标为 all;db 使用 migrate/seed/reset/status 和单个目标 main。工具参数放在首个 `--` 后;dx 将分隔符及其后参数原样交给应用,应用示例去掉该分隔符再调用测试工具。标准帮助由 dx 提供,工具帮助使用合法调用后的 `-- --help`。
|
|
34
|
+
|
|
35
|
+
## 工程准备与分发
|
|
36
|
+
|
|
37
|
+
所有应用调用前强制检查全部入口和契约声明,setup 也不例外。显式 setup 在 linked worktree 中先由 dx 从主 Git 工作目录复制缺失的 `.local` 文件,再加载环境并调用应用脚本;已有文件不覆盖。本示例位于 Git 根的 example 子目录,其本地覆盖文件为 `example/.env.development.local`。
|
|
38
|
+
|
|
39
|
+
环境文件仅含示例值,真实本地配置不得提交。脚本入口必须保留 shebang 和可执行权限;共享实现与真实单测位于 `dx/lib/`。复制本示例时同时复制 `dx/contract.json`、全部 `dx/scripts/` 和 `dx/lib/`。
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"commands": {
|
|
4
|
+
"setup": {
|
|
5
|
+
"status": "implemented"
|
|
6
|
+
},
|
|
7
|
+
"start": {
|
|
8
|
+
"targets": {
|
|
9
|
+
"stack": {
|
|
10
|
+
"status": "mock",
|
|
11
|
+
"reason": "This example has no running application services."
|
|
12
|
+
},
|
|
13
|
+
"admin": {
|
|
14
|
+
"status": "mock",
|
|
15
|
+
"reason": "This example has no running application services."
|
|
16
|
+
},
|
|
17
|
+
"backend": {
|
|
18
|
+
"status": "mock",
|
|
19
|
+
"reason": "This example has no running application services."
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"build": {
|
|
24
|
+
"targets": {
|
|
25
|
+
"all": {
|
|
26
|
+
"status": "mock",
|
|
27
|
+
"reason": "This Node.js script example requires no build artifacts."
|
|
28
|
+
},
|
|
29
|
+
"admin": {
|
|
30
|
+
"status": "mock",
|
|
31
|
+
"reason": "This Node.js script example requires no build artifacts."
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"lint": {
|
|
36
|
+
"targets": {
|
|
37
|
+
"all": {
|
|
38
|
+
"status": "implemented"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"deploy": {
|
|
43
|
+
"targets": {
|
|
44
|
+
"all": {
|
|
45
|
+
"status": "mock",
|
|
46
|
+
"reason": "This local example has no deployment destination."
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"test": {
|
|
51
|
+
"types": {
|
|
52
|
+
"unit": {
|
|
53
|
+
"targets": {
|
|
54
|
+
"all": {
|
|
55
|
+
"status": "implemented"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"e2e": {
|
|
60
|
+
"targets": {
|
|
61
|
+
"all": {
|
|
62
|
+
"status": "mock",
|
|
63
|
+
"reason": "This script example has no end-to-end application flow."
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"db": {
|
|
70
|
+
"actions": {
|
|
71
|
+
"migrate": {
|
|
72
|
+
"targets": {
|
|
73
|
+
"main": {
|
|
74
|
+
"status": "mock",
|
|
75
|
+
"reason": "This example does not use a database."
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
"seed": {
|
|
80
|
+
"targets": {
|
|
81
|
+
"main": {
|
|
82
|
+
"status": "mock",
|
|
83
|
+
"reason": "This example does not use a database."
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
"reset": {
|
|
88
|
+
"targets": {
|
|
89
|
+
"main": {
|
|
90
|
+
"status": "mock",
|
|
91
|
+
"reason": "This example does not use a database."
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
"status": {
|
|
96
|
+
"targets": {
|
|
97
|
+
"main": {
|
|
98
|
+
"status": "mock",
|
|
99
|
+
"reason": "This example does not use a database."
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import test from 'node:test'
|
|
3
|
+
import { splitArguments } from './arguments.mjs'
|
|
4
|
+
|
|
5
|
+
test('keeps positional arguments without a tool separator', () => {
|
|
6
|
+
assert.deepEqual(splitArguments(['unit', 'all']), { positional: ['unit', 'all'], tool: [] })
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
test('preserves spaces, empty arguments and subsequent separators for the tool', () => {
|
|
10
|
+
const input = ['unit', 'my target', '--', '--test-name-pattern', 'a b', '', '--']
|
|
11
|
+
assert.deepEqual(splitArguments(input), {
|
|
12
|
+
positional: ['unit', 'my target'],
|
|
13
|
+
tool: ['--test-name-pattern', 'a b', '', '--'],
|
|
14
|
+
})
|
|
15
|
+
assert.equal(input.length, 7)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('accepts an empty tool argument list', () => {
|
|
19
|
+
assert.deepEqual(splitArguments(['all', '--']), { positional: ['all'], tool: [] })
|
|
20
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { access, readFile } from 'node:fs/promises'
|
|
2
|
+
import { spawnSync } from 'node:child_process'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { splitArguments } from './arguments.mjs'
|
|
5
|
+
|
|
6
|
+
const [command, ...args] = process.argv.slice(2)
|
|
7
|
+
const { positional, tool } = splitArguments(args)
|
|
8
|
+
const contract = JSON.parse(await readFile(new URL('../contract.json', import.meta.url), 'utf8'))
|
|
9
|
+
const entry = contract.commands[command]
|
|
10
|
+
const capability = command === 'setup' ? entry
|
|
11
|
+
: command === 'test' ? entry.types[positional[0]]?.targets[positional[1]]
|
|
12
|
+
: command === 'db' ? entry.actions[positional[0]]?.targets[positional[1]]
|
|
13
|
+
: entry?.targets[positional[0]]
|
|
14
|
+
|
|
15
|
+
if (!capability) {
|
|
16
|
+
console.error('Example: unsupported invocation; run the command through dx.')
|
|
17
|
+
process.exit(1)
|
|
18
|
+
}
|
|
19
|
+
if (capability.status === 'mock') {
|
|
20
|
+
console.log(`[mock] ${command}: skipped; ${capability.reason}`)
|
|
21
|
+
process.exit(0)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function runNode(nodeArgs) {
|
|
25
|
+
const result = spawnSync(process.execPath, nodeArgs, { stdio: 'inherit', env: process.env })
|
|
26
|
+
if (result.error || result.signal) process.exit(1)
|
|
27
|
+
if (result.status !== 0) process.exit(result.status ?? 1)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (command === 'test') {
|
|
31
|
+
runNode(['--test', ...tool, fileURLToPath(new URL('./arguments.test.mjs', import.meta.url))])
|
|
32
|
+
} else if (command === 'lint') {
|
|
33
|
+
if (tool.length > 0) {
|
|
34
|
+
console.error('Example lint does not accept tool options.')
|
|
35
|
+
process.exit(1)
|
|
36
|
+
}
|
|
37
|
+
for (const name of ['arguments.mjs', 'arguments.test.mjs', 'commands.mjs']) {
|
|
38
|
+
runNode(['--check', fileURLToPath(new URL(name, import.meta.url))])
|
|
39
|
+
}
|
|
40
|
+
console.log('lint: JavaScript syntax checks passed.')
|
|
41
|
+
} else if (command === 'setup') {
|
|
42
|
+
if (tool.length > 0) {
|
|
43
|
+
console.error('Example setup does not accept tool options.')
|
|
44
|
+
process.exit(1)
|
|
45
|
+
}
|
|
46
|
+
await access(new URL('./arguments.test.mjs', import.meta.url))
|
|
47
|
+
console.log('setup: example resources are ready; no external dependencies required.')
|
|
48
|
+
}
|
package/example/dx/scripts/build
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
|
-
|
|
2
|
+
exec node "$(dirname "$0")/../lib/commands.mjs" build "$@"
|
package/example/dx/scripts/db
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
|
-
|
|
2
|
+
exec node "$(dirname "$0")/../lib/commands.mjs" db "$@"
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
|
-
|
|
2
|
+
exec node "$(dirname "$0")/../lib/commands.mjs" deploy "$@"
|
package/example/dx/scripts/setup
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
|
-
|
|
2
|
+
exec node "$(dirname "$0")/../lib/commands.mjs" setup "$@"
|
package/example/dx/scripts/start
CHANGED
|
@@ -1,12 +1,2 @@
|
|
|
1
|
-
#!/
|
|
2
|
-
|
|
3
|
-
const [target = 'stack'] = process.argv.slice(2)
|
|
4
|
-
const environment = process.env.APP_ENV
|
|
5
|
-
const applications = ['backend', 'admin']
|
|
6
|
-
if (target !== 'stack' && !applications.includes(target)) {
|
|
7
|
-
console.error(`未知目标 ${target},可选 stack、backend、admin。`)
|
|
8
|
-
process.exit(1)
|
|
9
|
-
}
|
|
10
|
-
const selected = target === 'stack' ? applications : [target]
|
|
11
|
-
console.log(`${process.env.EXAMPLE_GREETING || '启动演示'}:environment=${environment} target=${target}`)
|
|
12
|
-
for (const application of selected) console.log(`演示启动 ${application}`)
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
exec node "$(dirname "$0")/../lib/commands.mjs" start "$@"
|
package/example/dx/scripts/test
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
|
-
|
|
2
|
+
exec node "$(dirname "$0")/../lib/commands.mjs" test "$@"
|