@lark-apaas/miaoda-cli 0.1.31 → 0.1.33-alpha.414062
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 +2 -2
- package/dist/cli/commands/deploy/modern.js +6 -5
- package/dist/cli/commands/registry/index.js +6 -6
- package/dist/cli/handlers/registry/add.js +3 -2
- package/dist/cli/handlers/registry/list.js +3 -8
- package/dist/cli/handlers/registry/shared.js +8 -4
- package/dist/cli/handlers/skills/sync.js +20 -7
- package/dist/services/deploy/modern/patch/actions.js +6 -4
- package/dist/services/deploy/modern/patch/html-title.js +75 -0
- package/dist/services/deploy/modern/patch/index.js +1 -1
- package/dist/services/deploy/modern/patch/routes.js +13 -3
- package/dist/utils/coding-steering.js +21 -1
- package/dist/utils/npm-pack.js +30 -0
- package/dist/utils/sandbox-skills.js +22 -31
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -55,7 +55,7 @@ miaoda file ls --output json
|
|
|
55
55
|
|
|
56
56
|
`miaoda registry` 提供 shadcn 式的「组件 registry」:把 SDK 文件**拷进**当前项目(copy-in),而非作为依赖安装。registry 是独立 npm 包,按运行时分发(design-html / html → buildless runtime → `@lark-apaas/coding-registry-buildless`)。
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
前置:当前工作目录已走过 `miaoda app init`,且 `.spark/meta.json` 的 `stack` 有对应 registry 包(当前支持 `design-html` / `html`);其余 stack 执行时报 `REGISTRY_STACK_UNSUPPORTED` 并提示支持列表。meta 一律从 cwd 读取,不随 `--dir` 走。
|
|
59
59
|
|
|
60
60
|
```bash
|
|
61
61
|
# 列出可用 SDK 条目(输出各文件 USAGE 注释块原文,便于 Agent 直接读用法)
|
|
@@ -76,7 +76,7 @@ miaoda registry add deck-slide --overwrite # 目标已存在时覆盖(
|
|
|
76
76
|
- **拍平落地**:所有文件**拍平**到项目根(`scripts/deck-slide.js` → 项目 `deck-slide.js`,不保留子目录);闭包内同名 basename 撞车时后者覆盖前者。
|
|
77
77
|
- **skip-by-disk**:项目已有同名(拍平后的 basename)文件默认跳过,`--overwrite` 才覆盖;不写任何安装记账文件,项目文件树即状态。
|
|
78
78
|
- **add 回执带 usage**:`add` 结果的 `entries` 只列**你请求的条目**(不含被 `Depends` 传递拉进来的依赖——依赖照常落地在 `added`/`skipped`,但那是"管道"),逐条给出 `usage`(USAGE 块原文)与 `dependsOn`,Agent `add` 完即可直接读用法,无需再 `registry list`。请求的条目即使已存在被跳过也回带 usage(`status` 标 `added`/`skipped`,据此判断磁盘上是 registry 版还是你改过的旧版)。`--json` 结构为 `{registryVersion, added, skipped, dryRun, entries}`;pretty(默认)输出 `registry@<ver>` + `added:`/`skipped:` 逗号列表 + 各请求条目的 USAGE 块,稳定 label、无 JSON 数组、无截断,对 Agent 与人同样可读。
|
|
79
|
-
- `--version <ver>` 指定 registry 包版本或 dist-tag(缺省 `latest
|
|
79
|
+
- `--version <ver>` 指定 registry 包版本或 dist-tag(缺省 `latest`);`add` 的 `--dir <path>` 指定 copy-in 落地目录(缺省当前目录),不影响 `.spark/meta.json` 读取。
|
|
80
80
|
|
|
81
81
|
## 全局参数
|
|
82
82
|
|
|
@@ -72,7 +72,7 @@ JSON 输出(stdout)
|
|
|
72
72
|
}));
|
|
73
73
|
const patchCmd = deployCmd
|
|
74
74
|
.command('patch')
|
|
75
|
-
.description('design-html 增量发布:按文件 create/update/delete,.html
|
|
75
|
+
.description('design-html 增量发布:按文件 create/update/delete,.html 变动时同步 routes.json')
|
|
76
76
|
.option('--create <relpath>', '新增文件(可重复)', shared_1.collectRepeatedOption, [])
|
|
77
77
|
.option('--update <relpath>', '覆盖已有文件(可重复)', shared_1.collectRepeatedOption, [])
|
|
78
78
|
.option('--delete <relpath>', '删除文件(可重复)', shared_1.collectRepeatedOption, [])
|
|
@@ -80,10 +80,11 @@ JSON 输出(stdout)
|
|
|
80
80
|
.addHelpText('after', `
|
|
81
81
|
--dir 为项目目录(与 deploy 同名参数,默认当前目录)。
|
|
82
82
|
仅支持 design-html(design_local_deploy)。路径为工程根相对路径(= 服务端 latest 下 key),
|
|
83
|
-
禁绝对路径与 ".."。--create/--update
|
|
84
|
-
时自动重算并随包更新 routes.json
|
|
85
|
-
routes.json 每项为 {path, file}:path 为相对路径(不含 base 前缀,由消费侧拼接),
|
|
86
|
-
|
|
83
|
+
禁绝对路径与 ".."。--create/--update 的本地文件必须存在。改动里含任意 .html(增 / 改 / 删)
|
|
84
|
+
时自动重算并随包更新 routes.json;纯非 .html 改动不动 routes.json。
|
|
85
|
+
routes.json 每项为 {path, file, name?}:path 为相对路径(不含 base 前缀,由消费侧拼接),
|
|
86
|
+
file 为对应源文件(如 / → index.html),name 为该 HTML 的 <title> 文本(方案名,可缺省,
|
|
87
|
+
缺省时由消费侧 fallback 到 path)。
|
|
87
88
|
|
|
88
89
|
JSON 输出
|
|
89
90
|
{"data": {"upsertCount": <n>, "deleteCount": <n>, "actionsSent": <n>, "routesRegenerated": <bool>}}
|
|
@@ -33,8 +33,9 @@ function registerRegistryCommands(program) {
|
|
|
33
33
|
- add 落地:拍平到项目根(包根 scripts/x.js → 项目 x.js);同名 basename 撞车时后者覆盖前者
|
|
34
34
|
|
|
35
35
|
前置
|
|
36
|
-
|
|
36
|
+
当前工作目录已走过 'miaoda app init',且 .spark/meta.json 的 stack 有对应
|
|
37
37
|
registry 包(当前支持 design-html / html);其余 stack 执行时报 REGISTRY_STACK_UNSUPPORTED
|
|
38
|
+
meta 一律从 cwd 读取,add 的 --dir 只影响 copy-in 落地目录
|
|
38
39
|
`);
|
|
39
40
|
registerRegistryList(registryCmd);
|
|
40
41
|
registerRegistryAdd(registryCmd);
|
|
@@ -43,7 +44,6 @@ function registerRegistryList(parent) {
|
|
|
43
44
|
const cmd = parent
|
|
44
45
|
.command('list')
|
|
45
46
|
.description('列出 registry 可用 SDK 条目,输出各文件的 USAGE 块原文')
|
|
46
|
-
.option('--dir <path>', '项目目录,默认当前目录', '.')
|
|
47
47
|
.option('--version <ver>', 'registry 包版本或 dist-tag,缺省 latest')
|
|
48
48
|
.addHelpText('after', `
|
|
49
49
|
输出
|
|
@@ -59,7 +59,6 @@ JSON 输出
|
|
|
59
59
|
`);
|
|
60
60
|
cmd.action((0, shared_1.withHelp)(cmd, async (rawOpts) => {
|
|
61
61
|
await (0, index_1.handleRegistryList)({
|
|
62
|
-
dir: rawOpts.dir,
|
|
63
62
|
version: rawOpts.version,
|
|
64
63
|
});
|
|
65
64
|
}));
|
|
@@ -69,7 +68,7 @@ function registerRegistryAdd(parent) {
|
|
|
69
68
|
.command('add')
|
|
70
69
|
.description('把一个或多个 SDK 文件 copy-in 到当前项目(拍平到项目根,含 Depends 闭包)')
|
|
71
70
|
.argument('<token...>', 'name / 完整文件名 / relPath,可传多个;同名多匹配全拉,Depends 自动传递解析')
|
|
72
|
-
.option('--dir <path>', '
|
|
71
|
+
.option('--dir <path>', 'copy-in 落地目录,默认当前目录(stack 一律读 cwd 的 .spark/meta.json)', '.')
|
|
73
72
|
.option('--version <ver>', 'registry 包版本或 dist-tag,缺省 latest')
|
|
74
73
|
.option('--overwrite', '目标文件已存在时覆盖(默认跳过以保护用户改动)', false)
|
|
75
74
|
.option('--dry-run', '只报告将写 / 将跳哪些文件,不落盘', false)
|
|
@@ -77,8 +76,9 @@ function registerRegistryAdd(parent) {
|
|
|
77
76
|
行为
|
|
78
77
|
- token 寻址:name(foo)/ 完整文件名(foo.css)/ relPath(stylesheets/foo.css)任一形式
|
|
79
78
|
- 多匹配全拉:token 命中多个同名条目时全部 copy-in;想只要其一就写完整文件名 / relPath
|
|
80
|
-
- 解析 USAGE 块 Depends 传递闭包,把 SDK
|
|
81
|
-
(包根 scripts/deck-stage.js →
|
|
79
|
+
- 解析 USAGE 块 Depends 传递闭包,把 SDK 文件**拍平**拷到落地目录(--dir,默认当前目录)
|
|
80
|
+
(包根 scripts/deck-stage.js → 落地 deck-stage.js;同名 basename 撞车时后者覆盖前者)
|
|
81
|
+
- stack 一律读当前工作目录的 .spark/meta.json,--dir 不影响 meta 读取
|
|
82
82
|
- skip-by-disk:目标文件已存在默认跳过;--overwrite 才覆盖
|
|
83
83
|
- 不写任何安装记账文件(项目文件树即状态)
|
|
84
84
|
|
|
@@ -13,7 +13,8 @@ const output_1 = require("../../../utils/output");
|
|
|
13
13
|
* miaoda registry add <name...> [--overwrite] [--dry-run] [--dir <path>] [--version <ver>]
|
|
14
14
|
*
|
|
15
15
|
* 解析 USAGE 块 Depends 传递闭包,把每个条目对应的 SDK 文件从 registry 包根的 <relPath>
|
|
16
|
-
*
|
|
16
|
+
* **保留相对路径**拷到落地目录(--dir,默认 cwd)。stack 一律从 <cwd>/.spark/meta.json 读,
|
|
17
|
+
* 不随 --dir 走。
|
|
17
18
|
* skip-by-disk:目标已存在默认跳过,--overwrite 才覆盖。不写任何安装记账文件。
|
|
18
19
|
*/
|
|
19
20
|
async function handleRegistryAdd(opts) {
|
|
@@ -22,7 +23,7 @@ async function handleRegistryAdd(opts) {
|
|
|
22
23
|
throw new error_1.AppError('ARGS_INVALID', 'registry add 至少需要一个条目名');
|
|
23
24
|
}
|
|
24
25
|
const targetDir = node_path_1.default.resolve(opts.dir ?? process.cwd());
|
|
25
|
-
const stack = (0, shared_1.resolveRegistryStack)(
|
|
26
|
+
const stack = (0, shared_1.resolveRegistryStack)();
|
|
26
27
|
const reg = (0, index_1.loadRegistry)({ stack, version: opts.version });
|
|
27
28
|
try {
|
|
28
29
|
const plan = (0, index_1.planAdd)({
|
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
3
|
exports.handleRegistryList = handleRegistryList;
|
|
7
|
-
const node_path_1 = __importDefault(require("node:path"));
|
|
8
4
|
const index_1 = require("../../../services/registry/index");
|
|
9
5
|
const shared_1 = require("./shared");
|
|
10
6
|
const output_1 = require("../../../utils/output");
|
|
11
7
|
/**
|
|
12
|
-
* miaoda registry list [--
|
|
8
|
+
* miaoda registry list [--version <ver>]
|
|
13
9
|
*
|
|
14
|
-
* 从
|
|
10
|
+
* 从 <cwd>/.spark/meta.json 读 stack → runtime → 包名,拉对应 registry 包,从包根递归扫含 USAGE 块的 SDK 文件。
|
|
15
11
|
*
|
|
16
12
|
* - 默认(pretty):输出各条目 USAGE 块原文,带文件名分隔,给 Agent 直接读注释。
|
|
17
13
|
* - --json:输出 [{name, file, usage(原文字符串), dependsOn[]}],usage 保持原文不解析成字段。
|
|
@@ -19,8 +15,7 @@ const output_1 = require("../../../utils/output");
|
|
|
19
15
|
*/
|
|
20
16
|
async function handleRegistryList(opts) {
|
|
21
17
|
await Promise.resolve();
|
|
22
|
-
const
|
|
23
|
-
const stack = (0, shared_1.resolveRegistryStack)(targetDir);
|
|
18
|
+
const stack = (0, shared_1.resolveRegistryStack)();
|
|
24
19
|
const reg = (0, index_1.loadRegistry)({ stack, version: opts.version });
|
|
25
20
|
try {
|
|
26
21
|
if ((0, output_1.isJsonMode)()) {
|
|
@@ -5,16 +5,20 @@ const spark_meta_1 = require("../../../utils/spark-meta");
|
|
|
5
5
|
const index_1 = require("../../../services/registry/index");
|
|
6
6
|
const error_1 = require("../../../utils/error");
|
|
7
7
|
/**
|
|
8
|
-
* 从 <
|
|
8
|
+
* 从 <cwd>/.spark/meta.json 读 stack,校验该 stack 有组件 registry。
|
|
9
|
+
* meta 只与当前工作目录相关(CLI 必须在 user app 根目录下运行),不随 --dir 走——
|
|
10
|
+
* --dir 只影响 add 的 copy-in 落地目录。
|
|
9
11
|
* 缺 stack → 提示先 init;stack 无 registry(无对应 runtime / runtime 无 registry 包)→ 提示支持哪些 stack。
|
|
10
12
|
*
|
|
11
13
|
* 这里只做「stack 是否在支持列表」的早校验给用户友好提示;包名的两跳解析
|
|
12
14
|
* (stack → runtime → 包)由 loadRegistry/resolveRegistryPackage 负责。
|
|
13
15
|
*/
|
|
14
|
-
function resolveRegistryStack(
|
|
15
|
-
const meta = (0, spark_meta_1.readSparkMeta)(
|
|
16
|
+
function resolveRegistryStack() {
|
|
17
|
+
const meta = (0, spark_meta_1.readSparkMeta)(process.cwd());
|
|
16
18
|
if (meta.stack === undefined || meta.stack === '') {
|
|
17
|
-
throw new error_1.AppError('REGISTRY_META_INCOMPLETE', '.spark/meta.json missing stack — run `miaoda app init` first'
|
|
19
|
+
throw new error_1.AppError('REGISTRY_META_INCOMPLETE', '.spark/meta.json missing stack — run `miaoda app init` first', {
|
|
20
|
+
next_actions: ['meta 一律读当前工作目录(不随 --dir),请 cd 到 user app 根目录后重试'],
|
|
21
|
+
});
|
|
18
22
|
}
|
|
19
23
|
if (!index_1.REGISTRY_SUPPORTED_STACKS.includes(meta.stack)) {
|
|
20
24
|
throw new error_1.AppError('REGISTRY_STACK_UNSUPPORTED', `stack "${meta.stack}" 没有组件 registry`, {
|
|
@@ -34,14 +34,20 @@ function syncSteeringForSandbox(params) {
|
|
|
34
34
|
catch (err) {
|
|
35
35
|
return { status: 'skipped', reason: `read .spark/meta.json failed: ${err.message}` };
|
|
36
36
|
}
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
// 版本口径:--version > lane(TCC tgzKey)> BOE 的 dist-tag dev > latest。
|
|
38
|
+
//
|
|
39
|
+
// **这里不预先把 BOE 解成 'dev'**(下方本地分支为了回报 versionSource 才那么做):
|
|
40
|
+
// 一旦提前填进 version,就会盖住 lane —— version 在优先级里高于 lane,BOE 沙箱上
|
|
41
|
+
// 配了 tgzKey 也永远命中不了。把 version 原样透传(未指定就是 undefined),让
|
|
42
|
+
// syncCodingSteering 一处算全链:version > lane > dev > latest。
|
|
40
43
|
try {
|
|
41
44
|
const r = (0, coding_steering_1.syncCodingSteering)({
|
|
42
45
|
stack,
|
|
43
46
|
targetDir: params.targetDir,
|
|
44
|
-
version:
|
|
47
|
+
version: params.version,
|
|
48
|
+
// TCC 的 X 与平铺 skill 共用:pickLaneVersion 只匹配 `-alpha.<X>` 后缀,
|
|
49
|
+
// 不看 base 版本,所以一个 key 同时定向 coding-steering 与 sandbox-skills。
|
|
50
|
+
tgzKey: params.tgzKey,
|
|
45
51
|
// 必须显式传 mode,不能落到 inferMode():它靠 env MIAODA_DEP_CACHE_DIR 嗅探,
|
|
46
52
|
// 而 mode 决定读包内哪个 layer(sandbox→`skills/`,local→`skills_local/`)。
|
|
47
53
|
// `--type` 分支本身就是沙箱语义,嗅探失手会去读不存在的 skills_local/、
|
|
@@ -70,9 +76,16 @@ function syncSteeringForSandbox(params) {
|
|
|
70
76
|
*
|
|
71
77
|
* 版本来源优先级(从高到低):
|
|
72
78
|
* 1. CLI `--version` flag(显式指定)
|
|
73
|
-
* 2. `
|
|
79
|
+
* 2. lane:`--type` 分支下由 TCC `skillsUpdateTgzKey` 透传 `--tgz-key`,反推 X 后
|
|
80
|
+
* 解析出 `-alpha.<X>` 版本。**与平铺 skill 共用同一个 X** —— 匹配只看后缀、
|
|
81
|
+
* 不看 base 版本,所以 TCC 配一个 key 就同时定向 coding-steering 与 sandbox-skills。
|
|
82
|
+
* 本地分支(不带 `--type`)没有 tgzKey,这一档天然不参与。
|
|
83
|
+
* 3. `FORCE_FRAMEWORK_ENVIRONMENT=boe` → dist-tag `dev`(BOE 测试灰度,跟随
|
|
74
84
|
* publish-alpha.sh 在 develop 分支打的 dev tag,便于在 BOE 沙箱里验证预发 steering)
|
|
75
|
-
*
|
|
85
|
+
* 4. 默认 `latest`
|
|
86
|
+
*
|
|
87
|
+
* 2/3/4 档统一在 `syncCodingSteering` 里算(见那边注释),handler 只负责透传,避免
|
|
88
|
+
* 提前把 `dev` 填进 version 把 lane 挤掉。
|
|
76
89
|
*
|
|
77
90
|
* **不读 meta.json**——steering 版本不在 meta 里钉。
|
|
78
91
|
*/
|
|
@@ -98,7 +111,7 @@ async function handleSkillsSync(opts) {
|
|
|
98
111
|
const result = (0, sandbox_skills_1.syncSandboxSkills)({ type, tgzKey, targetDir });
|
|
99
112
|
const steering = result.skippedByKeepFile
|
|
100
113
|
? { status: 'skipped', reason: '.keep file found, sync frozen' }
|
|
101
|
-
: syncSteeringForSandbox({ targetDir, version: opts.version });
|
|
114
|
+
: syncSteeringForSandbox({ targetDir, version: opts.version, tgzKey });
|
|
102
115
|
// ③ 收尾锁只读(update-skills.sh:241 的 chmod -R a=rX)。必须在 ①② 全部写完之后,
|
|
103
116
|
// 锁上就写不进去了。`.keep` 冻结时不锁 —— 老链路那条路径是 exit 0 直接走、
|
|
104
117
|
// 末尾的 chmod 压根不执行,保持一致。
|
|
@@ -36,8 +36,10 @@ function isHtml(rel) {
|
|
|
36
36
|
/**
|
|
37
37
|
* 把 changeset 组成 TosFileAction[]:
|
|
38
38
|
* - create/update 读本地文件、自动编码;delete 仅 FilePath。
|
|
39
|
-
* - .html 被 create / delete
|
|
40
|
-
*
|
|
39
|
+
* - 任一 .html 被 create / update / delete 时,追加重算 routes.json 的 UPDATE action。
|
|
40
|
+
* update 也算:routes.json 的 name 取自 HTML 的 `<title>`,改方案名就是一次纯 update,
|
|
41
|
+
* 只按 create/delete 判定会让改名永远进不了 routes.json。
|
|
42
|
+
* - 纯非 .html 改动(js / css / 图片)不动 routes.json。
|
|
41
43
|
*/
|
|
42
44
|
function buildTosActions(projectDir, cs) {
|
|
43
45
|
const actions = [];
|
|
@@ -48,8 +50,8 @@ function buildTosActions(projectDir, cs) {
|
|
|
48
50
|
for (const rel of cs.deletes) {
|
|
49
51
|
actions.push({ actionType: index_1.TosActionType.DELETE, filePath: normalizeRel(rel) });
|
|
50
52
|
}
|
|
51
|
-
const
|
|
52
|
-
if (
|
|
53
|
+
const routesMayChange = [...cs.creates, ...cs.updates, ...cs.deletes].some(isHtml);
|
|
54
|
+
if (routesMayChange) {
|
|
53
55
|
actions.push({
|
|
54
56
|
actionType: index_1.TosActionType.UPDATE,
|
|
55
57
|
filePath: 'routes.json',
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.extractTitle = extractTitle;
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
/**
|
|
9
|
+
* 读 `<title>` 时的扫描窗口上限(字节)。title 规范上在 `<head>` 里、正常紧贴文档开头,
|
|
10
|
+
* 64KB 足以覆盖 head 里塞了内联 style / script 的情况,同时给大 HTML 兜住 IO 上限
|
|
11
|
+
* (generateRoutes 会遍历工程内所有 .html,不能整文件读)。
|
|
12
|
+
*/
|
|
13
|
+
const HEAD_SCAN_BYTES = 64 * 1024;
|
|
14
|
+
const TITLE_RE = /<title[^>]*>([^<]*)<\/title>/i;
|
|
15
|
+
const HEAD_CLOSE_RE = /<\/head\s*>/i;
|
|
16
|
+
const COMMENT_RE = /<!--[\s\S]*?-->/g;
|
|
17
|
+
const ENTITY_RE = /&(#x[0-9a-f]+|#[0-9]+|[a-z]+);/gi;
|
|
18
|
+
/** HTML 里可能出现在 title 文本中的命名实体。未列出的原样保留,不做全量实体表。 */
|
|
19
|
+
const NAMED_ENTITIES = {
|
|
20
|
+
amp: '&',
|
|
21
|
+
lt: '<',
|
|
22
|
+
gt: '>',
|
|
23
|
+
quot: '"',
|
|
24
|
+
apos: "'",
|
|
25
|
+
nbsp: ' ',
|
|
26
|
+
};
|
|
27
|
+
/** 把 title 文本里的实体还原成明文——name 是给前端直接当文本渲染的,不该带 `&`。 */
|
|
28
|
+
function decodeEntities(s) {
|
|
29
|
+
return s.replace(ENTITY_RE, (matched, body) => {
|
|
30
|
+
if (!body.startsWith('#')) {
|
|
31
|
+
return NAMED_ENTITIES[body.toLowerCase()] ?? matched;
|
|
32
|
+
}
|
|
33
|
+
const code = /^#x/i.test(body) ? parseInt(body.slice(2), 16) : parseInt(body.slice(1), 10);
|
|
34
|
+
if (!Number.isInteger(code) || code <= 0 || code > 0x10ffff)
|
|
35
|
+
return matched;
|
|
36
|
+
return String.fromCodePoint(code);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/** 只读文件头部 HEAD_SCAN_BYTES 字节。尾部可能截断多字节字符,对 title 提取无影响。 */
|
|
40
|
+
function readHead(absPath) {
|
|
41
|
+
const fd = node_fs_1.default.openSync(absPath, 'r');
|
|
42
|
+
try {
|
|
43
|
+
const buf = Buffer.alloc(HEAD_SCAN_BYTES);
|
|
44
|
+
const bytes = node_fs_1.default.readSync(fd, buf, 0, HEAD_SCAN_BYTES, 0);
|
|
45
|
+
return buf.subarray(0, bytes).toString('utf-8');
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
node_fs_1.default.closeSync(fd);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 取 HTML 的 `<title>` 文本作为该页面的方案名。
|
|
53
|
+
*
|
|
54
|
+
* - 扫描范围截到第一个 `</head>`:body 里的 `<title>` 不算——内联 SVG 图标的 a11y
|
|
55
|
+
* `<title>` 很常见,抓到就会把图标描述当成方案名。找不到 `</head>` 时退回整个窗口。
|
|
56
|
+
* - 先剔 HTML 注释,避免抓到被注释掉的 title。
|
|
57
|
+
* - 实体还原 + 空白折叠 + trim;空 title / 无 title / 读失败一律返回 undefined,
|
|
58
|
+
* 由消费侧 fallback 到 path(旧应用没有 title 时保持显示页面地址)。
|
|
59
|
+
*/
|
|
60
|
+
function extractTitle(absPath) {
|
|
61
|
+
let head;
|
|
62
|
+
try {
|
|
63
|
+
head = readHead(absPath);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
const headClose = HEAD_CLOSE_RE.exec(head)?.index ?? -1;
|
|
69
|
+
const scope = (headClose >= 0 ? head.slice(0, headClose) : head).replace(COMMENT_RE, '');
|
|
70
|
+
const raw = TITLE_RE.exec(scope)?.[1];
|
|
71
|
+
if (raw === undefined)
|
|
72
|
+
return undefined;
|
|
73
|
+
const text = decodeEntities(raw).replace(/\s+/g, ' ').trim();
|
|
74
|
+
return text === '' ? undefined : text;
|
|
75
|
+
}
|
|
@@ -11,7 +11,7 @@ const actions_1 = require("./actions");
|
|
|
11
11
|
var actions_2 = require("./actions");
|
|
12
12
|
Object.defineProperty(exports, "buildTosActions", { enumerable: true, get: function () { return actions_2.buildTosActions; } });
|
|
13
13
|
/**
|
|
14
|
-
* design-html 增量发布:读本地文件组 TosFileAction
|
|
14
|
+
* design-html 增量发布:读本地文件组 TosFileAction(改动含 .html 时带重算的
|
|
15
15
|
* routes.json),调后端 applyTosDiff 应用到 latest。scope 限 design_local_deploy。
|
|
16
16
|
*/
|
|
17
17
|
async function patchDesignDeploy(opts) {
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.generateRoutes = generateRoutes;
|
|
7
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
8
|
+
const html_title_1 = require("./html-title");
|
|
4
9
|
const source_scan_1 = require("./source-scan");
|
|
5
10
|
/**
|
|
6
11
|
* .html → route path 推导(相对路径,不带 base 前缀)。rel 为 posix 相对路径。
|
|
@@ -16,8 +21,9 @@ function toRoute(rel) {
|
|
|
16
21
|
}
|
|
17
22
|
/**
|
|
18
23
|
* 扫描 projectDir 下所有 .html(套 source-scan 的 EXCLUDES),生成 routes.json 文本(含尾换行)。
|
|
19
|
-
* 每项为 { path, file }:path 为相对路径(不带 CLIENT_BASE_PATH 前缀,由消费侧拼接),
|
|
20
|
-
* file 为该路由对应的原始 .html
|
|
24
|
+
* 每项为 { path, file, name? }:path 为相对路径(不带 CLIENT_BASE_PATH 前缀,由消费侧拼接),
|
|
25
|
+
* file 为该路由对应的原始 .html 源文件,name 为该文件 `<title>` 解析出的方案名(可缺省)。
|
|
26
|
+
* 按 path 排序、同 path 取字典序靠前的 file 去重。
|
|
21
27
|
* 零 html 时输出空数组——不伪造解析不到文件的根路由。
|
|
22
28
|
*/
|
|
23
29
|
function generateRoutes(projectDir) {
|
|
@@ -27,7 +33,11 @@ function generateRoutes(projectDir) {
|
|
|
27
33
|
if (!byPath.has(entry.path))
|
|
28
34
|
byPath.set(entry.path, entry);
|
|
29
35
|
}
|
|
30
|
-
|
|
36
|
+
// title 只读去重后胜出的 file:同 path 多源(foo.html / foo/index.html)时不做无用 IO。
|
|
37
|
+
const routes = [...byPath.values()].map((entry) => {
|
|
38
|
+
const name = (0, html_title_1.extractTitle)(node_path_1.default.join(projectDir, entry.file));
|
|
39
|
+
return name === undefined ? entry : { ...entry, name };
|
|
40
|
+
});
|
|
31
41
|
return JSON.stringify(routes, null, 2) + '\n';
|
|
32
42
|
}
|
|
33
43
|
function byPathThenFile(a, b) {
|
|
@@ -40,7 +40,27 @@ function inferMode() {
|
|
|
40
40
|
*/
|
|
41
41
|
function syncCodingSteering(opts) {
|
|
42
42
|
const logPrefix = opts.logPrefix ?? 'skills';
|
|
43
|
-
|
|
43
|
+
// 版本优先级:--version(显式 pin / 止血)> lane(TCC 定向灰度)> BOE 默认 @dev > @latest
|
|
44
|
+
//
|
|
45
|
+
// 与 sandbox-skills 的 syncSandboxSkills 完全同一套口径,只是包名不同。BOE 的 @dev
|
|
46
|
+
// 由 miaoda-coding develop 分支的自动发布维护;lane 必须压过 dev —— TCC 配了 tgzKey
|
|
47
|
+
// 就是定向到某个 feat 分支的快照,不该被"BOE 跟 develop"这条默认规则盖掉。
|
|
48
|
+
//
|
|
49
|
+
// BOE→dev 这一档在本函数里算,不由调用方预填:`skills sync --type` 分支会带 tgzKey
|
|
50
|
+
// 进来,调用方若先把 'dev' 塞进 version,version 档位更高,lane 就永远命不中。
|
|
51
|
+
// 本地分支 / app init 为了回报 versionSource 仍会自己算 dev 再传 version —— 那两条路
|
|
52
|
+
// 没有 tgzKey,lane 档天然为空,两处口径一致不会打架。
|
|
53
|
+
//
|
|
54
|
+
// lane 解析与 sandbox-skills 同一套:`extractLaneFromTgzKey` 反推 X,`pickLaneVersion`
|
|
55
|
+
// 过滤后缀 `-alpha.<X>`。**匹配只看后缀、不看 base 版本**,所以 coding-steering
|
|
56
|
+
// (`0.1.3x-alpha.<X>`)和 sandbox-skills(`0.1.x-alpha.<X>`)能共用 TCC 里的同一个
|
|
57
|
+
// key —— 前提是 lane 发布脚本用同一个 X 把两边一起发出去。这个包没发 `-alpha.<X>`
|
|
58
|
+
// 时 pickLaneVersion 返回 null(它对报错 / 无匹配都返回 null,绝不 throw),静默
|
|
59
|
+
// 回落到 dev / latest,不影响现有行为。
|
|
60
|
+
const lane = (0, npm_pack_1.extractLaneFromTgzKey)(opts.tgzKey);
|
|
61
|
+
const laneVersion = lane === null ? null : (0, npm_pack_1.pickLaneVersion)(STEERING_PACKAGE, lane);
|
|
62
|
+
const isBoe = process.env.FORCE_FRAMEWORK_ENVIRONMENT === 'boe';
|
|
63
|
+
const effectiveVersion = opts.version ?? laneVersion ?? (isBoe ? 'dev' : 'latest');
|
|
44
64
|
const mode = opts.mode ?? inferMode();
|
|
45
65
|
const layout = opts.outputLayout ?? 'nested';
|
|
46
66
|
(0, logger_1.log)(logPrefix, `Fetching ${STEERING_PACKAGE}@${effectiveVersion} (mode=${mode}, layout=${layout})...`);
|
package/dist/utils/npm-pack.js
CHANGED
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.pickLaneVersion = pickLaneVersion;
|
|
7
|
+
exports.extractLaneFromTgzKey = extractLaneFromTgzKey;
|
|
7
8
|
exports.fetchNpmPackage = fetchNpmPackage;
|
|
8
9
|
const node_child_process_1 = require("node:child_process");
|
|
9
10
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
@@ -44,6 +45,35 @@ function pickLaneVersion(packageName, X, registry) {
|
|
|
44
45
|
return null;
|
|
45
46
|
}
|
|
46
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* 从 TCC skillsUpdateTgzKey 反推 lane 名。
|
|
50
|
+
* skills_versions/all_feat-xxx.tgz → feat-xxx
|
|
51
|
+
* skills_versions/all_feat_xxx.tgz → feat-xxx (_ sanitize → -)
|
|
52
|
+
* skills_versions/all_latest.tgz → null(走各自的默认版本口径)
|
|
53
|
+
* 任何 all_<X>.tgz → <X> 里 _ 全部转 -
|
|
54
|
+
*
|
|
55
|
+
* **宽进严出**:正则允许 `_` 匹配(兼容老 miaoda-skills 时代 TOS key 含 `_` 的存量 TCC
|
|
56
|
+
* 命名),但返回时 sanitize 成 `-` —— 对齐 publish-sandbox-skills.sh 侧只发 `-` 版
|
|
57
|
+
* lane 的现实(npm SemVer pre-release 只允许 `[A-Za-z0-9-]`,不允许 `_`)。
|
|
58
|
+
* 存量 TCC 里如果有 `all_feat_xxx.tgz` 这种命名,反推出 `feat-xxx` 后能正确命中
|
|
59
|
+
* npm 上实际发出的 `-alpha.feat-xxx-<ts>` 包,而不是静默 fallback。
|
|
60
|
+
*
|
|
61
|
+
* 用 nullable 而不是空字符串 —— 让调用方能判断"没 lane 就走默认版本"。
|
|
62
|
+
*/
|
|
63
|
+
function extractLaneFromTgzKey(tgzKey) {
|
|
64
|
+
if (tgzKey === undefined || tgzKey === '') {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
const match = /(?:^|\/)all_([A-Za-z0-9][A-Za-z0-9_-]*)\.tgz$/.exec(tgzKey);
|
|
68
|
+
if (match === null) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const lane = match[1].replace(/_/g, '-');
|
|
72
|
+
if (lane === 'latest') {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
return lane;
|
|
76
|
+
}
|
|
47
77
|
/**
|
|
48
78
|
* `npm pack <pkg>@<ver> --registry <r>` + `tar -xzf`,解到临时目录。
|
|
49
79
|
* extractDir 是包内容根(含 package.json)。使用完必须 cleanup()。
|
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.extractLaneFromTgzKey =
|
|
6
|
+
exports.extractLaneFromTgzKey = void 0;
|
|
7
7
|
exports.syncSandboxSkills = syncSandboxSkills;
|
|
8
8
|
exports.validateSandboxSyncOpts = validateSandboxSyncOpts;
|
|
9
9
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
@@ -68,34 +68,11 @@ const BUSINESS_LINE_BY_TYPE = {
|
|
|
68
68
|
openclaw: null, // openclaw 独立包,直接展平
|
|
69
69
|
};
|
|
70
70
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* skills_versions/all_feat_xxx.tgz → feat-xxx (_ sanitize → -)
|
|
74
|
-
* skills_versions/all_latest.tgz → null(走 @latest)
|
|
75
|
-
* 任何 all_<X>.tgz → <X> 里 _ 全部转 -
|
|
76
|
-
*
|
|
77
|
-
* **宽进严出**:正则允许 `_` 匹配(兼容老 miaoda-skills 时代 TOS key 含 `_` 的存量 TCC
|
|
78
|
-
* 命名),但返回时 sanitize 成 `-` —— 对齐 publish-sandbox-skills.sh 侧只发 `-` 版
|
|
79
|
-
* lane 的现实(npm SemVer pre-release 只允许 `[A-Za-z0-9-]`,不允许 `_`)。
|
|
80
|
-
* 存量 TCC 里如果有 `all_feat_xxx.tgz` 这种命名,反推出 `feat-xxx` 后能正确命中
|
|
81
|
-
* npm 上实际发出的 `@lane-feat-xxx` 包,而不是静默 fallback 到 @latest。
|
|
82
|
-
*
|
|
83
|
-
* 用 nullable 而不是空字符串 —— 让调用方能判断"没 lane 就走 latest"。
|
|
71
|
+
* lane 解析已下沉到 `utils/npm-pack`(与 `pickLaneVersion` 同处一地,供
|
|
72
|
+
* sandbox-skills 与 coding-steering 共用)。这里保留再导出,维持既有引用面。
|
|
84
73
|
*/
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return null;
|
|
88
|
-
}
|
|
89
|
-
const match = /(?:^|\/)all_([A-Za-z0-9][A-Za-z0-9_-]*)\.tgz$/.exec(tgzKey);
|
|
90
|
-
if (match === null) {
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
const lane = match[1].replace(/_/g, '-');
|
|
94
|
-
if (lane === 'latest') {
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
return lane;
|
|
98
|
-
}
|
|
74
|
+
var npm_pack_2 = require("./npm-pack");
|
|
75
|
+
Object.defineProperty(exports, "extractLaneFromTgzKey", { enumerable: true, get: function () { return npm_pack_2.extractLaneFromTgzKey; } });
|
|
99
76
|
/**
|
|
100
77
|
* 主流程:按 type 拉包 + sync 到目标目录。
|
|
101
78
|
*
|
|
@@ -113,7 +90,7 @@ function syncSandboxSkills(opts) {
|
|
|
113
90
|
const targetDir = node_path_1.default.resolve(opts.targetDir ?? process.cwd());
|
|
114
91
|
const packageName = PACKAGE_BY_TYPE[opts.type];
|
|
115
92
|
const businessLine = BUSINESS_LINE_BY_TYPE[opts.type];
|
|
116
|
-
const lane = extractLaneFromTgzKey(opts.tgzKey);
|
|
93
|
+
const lane = (0, npm_pack_1.extractLaneFromTgzKey)(opts.tgzKey);
|
|
117
94
|
// ---- lane 拉包 ----
|
|
118
95
|
// 新姿势(2026-08 迁离 lane-<X> dist-tag 之后):
|
|
119
96
|
// TCC 里的 X(比如 `feat-migrate-skills-from-miaoda-skills-20260805143512`)
|
|
@@ -121,14 +98,25 @@ function syncSandboxSkills(opts) {
|
|
|
121
98
|
// 过滤 `endsWith('-alpha.' + X)`(因 X 含 timestamp 全局唯一,唯一命中或 0 命中)。
|
|
122
99
|
// 0 命中 → fallback @latest(对齐旧 TCC 值找不到、灰度切换期间的兜底语义)。
|
|
123
100
|
// `pickLaneVersion` 内部对 `npm view` 报错 / 无匹配都返回 null,绝不 throw ——
|
|
124
|
-
// 目标就是"不挂掉"
|
|
101
|
+
// 目标就是"不挂掉"。**同一个 X 也被 coding-steering 侧用同样方式解析**(见
|
|
102
|
+
// `syncCodingSteering`):匹配只看 `-alpha.<X>` 后缀、不看 base 版本,所以两个包
|
|
103
|
+
// base 版本不同也能共用一个 TCC key。
|
|
125
104
|
let fetched;
|
|
126
105
|
const laneVersion = lane === null ? null : (0, npm_pack_1.pickLaneVersion)(packageName, lane, opts.registry);
|
|
127
106
|
if (lane !== null && laneVersion === null) {
|
|
128
107
|
(0, logger_1.debug)(`sandbox-skills: no npm version matched -alpha.${lane}, fallback to @latest ` +
|
|
129
108
|
`(TCC 里的 X 可能是老 lane dist-tag 命名 / 已 unpublish / npm view 挂 / 网络断)`);
|
|
130
109
|
}
|
|
131
|
-
|
|
110
|
+
// 版本优先级:lane(TCC 显式定向)> BOE 默认 @dev > @latest
|
|
111
|
+
//
|
|
112
|
+
// `dev` 通道由 miaoda-coding 的 publish-sandbox-skills-dev.sh 在 develop 分支自动发布
|
|
113
|
+
// (`<base>-dev.<sha>` + dist-tag `dev`),口径与 coding-steering 完全一致 ——
|
|
114
|
+
// 后者在本仓 coding-steering 侧也是 `FORCE_FRAMEWORK_ENVIRONMENT=boe` 时默认取 `dev`。
|
|
115
|
+
//
|
|
116
|
+
// lane 必须压过 dev:TCC 里配了 tgz-key 就说明是定向灰度(钉某个 feat 分支的快照),
|
|
117
|
+
// 不能被"BOE 就跟 develop"这条默认规则覆盖掉。
|
|
118
|
+
const isBoe = process.env.FORCE_FRAMEWORK_ENVIRONMENT === 'boe';
|
|
119
|
+
const primaryVersion = laneVersion ?? (isBoe ? 'dev' : 'latest');
|
|
132
120
|
try {
|
|
133
121
|
(0, logger_1.debug)(`sandbox-skills: fetching ${packageName}@${primaryVersion}`);
|
|
134
122
|
fetched = (0, npm_pack_1.fetchNpmPackage)({
|
|
@@ -138,6 +126,9 @@ function syncSandboxSkills(opts) {
|
|
|
138
126
|
});
|
|
139
127
|
}
|
|
140
128
|
catch (err) {
|
|
129
|
+
// @latest 是最后一档,它再挂就没得降了,直接抛。
|
|
130
|
+
// lane / dev 拉失败都降到 @latest:dev 通道可能因为 develop 还没发过包而不存在
|
|
131
|
+
// (dist-tag 缺失 → npm pack 404),这时候不该让整条 sync 挂掉。
|
|
141
132
|
if (primaryVersion === 'latest') {
|
|
142
133
|
throw err;
|
|
143
134
|
}
|