@weotro/dx 0.2.2 → 0.3.0

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 CHANGED
@@ -23,7 +23,7 @@ dx initial --help
23
23
 
24
24
  `initial` 用于安装和更新技能,`setup` 用于当前项目的准备;两者独立,不互相自动调用。自定义应用命令不得使用保留名称 `initial`。技能中的外部工具需另行安装和认证,initial 只同步技能文件。
25
25
 
26
- 首次接入或从 0.1.x 升级的应用可使用随包的 [dx-adopt](skills/dx-adopt/SKILL.md) 技能,例如“使用 $dx-adopt 为当前项目接入 dx 0.2.1,并完成验证”。技能会区分新项目与旧版迁移,生成项目自己的脚本并适配环境、setup 和调用方。
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
- 每个入口都是无扩展名的可执行文件,用 shebang 指定解释器绝对路径;首行不超过 255 字节(不含换行),避免系统截断或回退执行。按需提供入口,不要求实现全部命令。
41
+ 七个入口全部必选。每个入口都是无扩展名的可执行文件,用 shebang 指定解释器绝对路径;首行不超过 255 字节(不含换行),避免系统截断或回退执行。
41
42
 
42
43
  ```sh
43
44
  #!/bin/sh
44
- # dx/scripts/build:具体构建策略属于应用。
45
- exec pnpm exec nx build "$@"
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 或其他语言均可使用同一接口。新增合法名称的脚本即可增加命令,例如 `dx/scripts/lint` 对应 `dx lint`;名称须匹配 `[a-z][a-z0-9-]*`。
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 test ...` | 执行测试 |
64
- | `dx db ...` | 数据库操作 |
65
- | `dx deploy ...` | 部署 |
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
- `start` 的目标清单由应用脚本定义,dx 不解析 `stack` 或 `admin`。环境文件由 dx 统一加载。`setup` 需要显式执行,其他命令不会自动触发它。
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
- 1. 命令后、首个 `--` 前的 `--prod`、`--staging`、`--dev`、`--test`、`--e2e`。同时出现多个时,按此顺序取第一个,沿用旧版优先级。
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
- 支持 development、staging、production、test、e2e;其他名称明确报错。应用收到的 `APP_ENV` 固定为选定环境,`NODE_ENV` 映射为 development / production / test(staging → production,e2e → test),这两个选择变量不被文件覆盖。dx 不根据 build/test 等命令名强制切换环境。
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 deploy # 加载 staging 两层
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
- 参数仍逐项原样传给脚本,包含用于选择环境的标志。只有子进程收到合并环境,dx 自身环境不被修改。变量展开遵循 dotenv-expand 语法,可引用同层之前定义的变量、前层和调用者变量;文件中的引号、注释和多行值由 dotenv 解析。
119
+ dx 消费并删除首个 `--` 前的环境标志,应用直接读取注入的环境结果。只有子进程收到合并环境,dx 自身环境不被修改。变量展开遵循 dotenv-expand 语法,可引用同层之前定义的变量、前层和调用者变量;文件中的引号、注释和多行值由 dotenv 解析。
108
120
 
109
121
  ## 调用契约
110
122
 
111
123
  - 从当前目录逐级向上寻找最近的 `dx/scripts` 目录,以对应工程根目录作为脚本 cwd。
112
124
  - 最近目录缺少所请求脚本时直接报错,不运行外层同名脚本。内层完全没有这个标记时,仍会继续向上查找。
113
- - 命令后的参数逐项原样传递,包括空字符串、`--`、`--prod`、`--help`。dx 仅识别约定的环境标志,不拼接 shell 命令。
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 # 由应用 deploy 脚本提供帮助
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
- 0.2.0 删除旧工程实现。旧 `dx/config/*`、internal runners、环境策略校验、部署/回滚、SDK、PM2、worktree 管理命令和 `dx-with-version-env` 不再提供,也不读取 `DX_CONFIG_DIR` 或顶层 `--config-dir`。技能资源与 `dx initial` 保留;公共两层环境加载和 setup 的 worktree 文件准备由 dx 提供。
146
+ ## 随包文档与示例
129
147
 
130
- 应用在 `dx/scripts/` 实现需要的入口,将确认、启动编排和部署安全等逻辑放入自身脚本,直接使用 dx 注入的环境,然后显式执行 `dx setup`。dx 不自动把旧配置转换为脚本。
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
- pnpm install --frozen-lockfile
136
- pnpm test
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
- [最小示例](example/README.md)仅展示调用契约,不实际安装、启动服务或部署。[开发约定](ruler/development.md)说明本仓库的验证与发布流程。
158
+ 示例提供真实单测和静态检查,其余不适用能力明确输出 mock 日志,不实际启动服务或部署。
package/bin/dx.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { statSync, lstatSync, readdirSync, openSync, readSync, closeSync, accessSync, constants } from 'node:fs'
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 hasShebang(entry) {
13
- const descriptor = openSync(entry, 'r')
14
- try {
15
- const prefix = Buffer.alloc(4096)
16
- const length = readSync(descriptor, prefix, 0, prefix.length, 0)
17
- const firstLine = prefix.subarray(0, length).toString().split('\n', 1)[0]
18
- // Keep the header within Linux's exec limit to prevent shell fallback or truncation.
19
- return Buffer.byteLength(firstLine) <= 255 && /^#![ \t]*\/[^\s\0]+(?:[ \t]|$)/.test(firstLine)
20
- } finally {
21
- closeSync(descriptor)
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 && args[0] === '--help') {
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
- const help = command === undefined || command === '--help'
40
- if (!help && !/^[a-z][a-z0-9-]*$/.test(command)) {
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
- let root = process.cwd()
44
- for (;;) {
45
- const marker = join(root, 'dx/scripts')
46
- let markerExists = false
47
- try {
48
- lstatSync(marker)
49
- markerExists = true
50
- if (!statSync(marker).isDirectory()) throw new Error(`工程标记 ${marker} 不是目录;请改为脚本目录。`)
51
- accessSync(marker, constants.R_OK | constants.X_OK)
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 (help) {
64
- const entries = root ? readdirSync(join(root, 'dx/scripts')).filter(name => {
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
- try {
78
- if (!statSync(entry).isFile()) throw new Error('入口不是普通文件')
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, args)
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
- 本目录展示目标 monorepo 如何提供 `dx/scripts/` 入口。脚本仅演示接口,不执行真实依赖安装、数据库写入或部署。
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 --help
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 admin
13
- node ../bin/dx.js test
14
- node ../bin/dx.js db --help
15
- node ../bin/dx.js deploy --help
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
- `setup` 演示 clone 或新 worktree 后显式进行准备。dx 默认加载工程根 `.env.development` 和可选的 `.env.development.local`;`start` 直接使用注入的环境。`stack` 对应示例定义的全部服务,`admin` 对应单个应用。
22
+ ## 实际行为
19
23
 
20
- 在 linked worktree 中运行 setup 时,dx 先从主 Git 工作目录按相对路径复制所有缺失的 .local 文件,再加载环境并调用示例脚本;已有目标不覆盖。该公共步骤无需应用手动调用。这里的示例位于 Git 根的 example 子目录,因此其本地覆盖文件为 example/.env.development.local。
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
- 实际工程应替换这些示例的应用逻辑。入口使用 shebang 和可执行权限,内部实现可以拆分到工程自己的其他文件。多服务并发启动、失败处理和子进程清理由工程自行决定。
29
+ ## 参数与环境
23
30
 
24
- 环境文件中的值只用于示例,不包含真实凭据。本地真实值放入被 Git 忽略的 `.env.development.local`。标准流和参数保留原样,应用自行提供 `--help`。可用 `node ../bin/dx.js start admin --prod` 选择 production 文件。
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,6 @@
1
+ export function splitArguments(args) {
2
+ const separator = args.indexOf('--')
3
+ return separator < 0
4
+ ? { positional: args.slice(), tool: [] }
5
+ : { positional: args.slice(0, separator), tool: args.slice(separator + 1) }
6
+ }
@@ -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
+ }
@@ -1,2 +1,2 @@
1
1
  #!/bin/sh
2
- printf '演示构建,收到 %s 个参数。\n' "$#"
2
+ exec node "$(dirname "$0")/../lib/commands.mjs" build "$@"
@@ -1,2 +1,2 @@
1
1
  #!/bin/sh
2
- printf '演示数据库入口,收到 %s 个参数。\n' "$#"
2
+ exec node "$(dirname "$0")/../lib/commands.mjs" db "$@"
@@ -1,2 +1,2 @@
1
1
  #!/bin/sh
2
- printf '演示部署入口,实际确认、安全策略和恢复由应用负责。\n'
2
+ exec node "$(dirname "$0")/../lib/commands.mjs" deploy "$@"
@@ -0,0 +1,2 @@
1
+ #!/bin/sh
2
+ exec node "$(dirname "$0")/../lib/commands.mjs" lint "$@"
@@ -1,2 +1,2 @@
1
1
  #!/bin/sh
2
- printf '演示工程准备,具体安装和生成步骤由应用维护。\n'
2
+ exec node "$(dirname "$0")/../lib/commands.mjs" setup "$@"
@@ -1,12 +1,2 @@
1
- #!/usr/bin/env node
2
- // dx 注入环境;应用定义启动目标及具体启动方式。
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 "$@"
@@ -1,2 +1,2 @@
1
1
  #!/bin/sh
2
- printf '演示测试,收到 %s 个参数。\n' "$#"
2
+ exec node "$(dirname "$0")/../lib/commands.mjs" test "$@"