@lark-apaas/miaoda-cli 0.1.32 → 0.1.33-alpha.07f9284
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/dist/cli/commands/deploy/modern.js +6 -5
- package/dist/cli/handlers/app/init.js +10 -2
- package/dist/cli/handlers/app/migrate.js +2 -4
- package/dist/cli/handlers/skills/sync.js +75 -37
- package/dist/config/sync-configs/design-stack.js +1 -1
- package/dist/config/sync-configs/nestjs-react-fullstack.js +4 -4
- 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 +168 -6
- package/dist/utils/dir-lock.js +103 -0
- package/dist/utils/env.js +3 -3
- package/dist/utils/npm-pack.js +30 -0
- package/dist/utils/sandbox-skills.js +65 -61
- package/package.json +1 -1
- package/upgrade/templates/nestjs-react-fullstack/templates/scripts/lint.js +43 -7
|
@@ -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>}}
|
|
@@ -7,6 +7,7 @@ exports.handleAppInit = handleAppInit;
|
|
|
7
7
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
8
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
9
|
const index_1 = require("../../../services/app/init/index");
|
|
10
|
+
const dir_lock_1 = require("../../../utils/dir-lock");
|
|
10
11
|
const coding_steering_1 = require("../../../utils/coding-steering");
|
|
11
12
|
const logger_1 = require("../../../utils/logger");
|
|
12
13
|
const githooks_1 = require("../../../utils/githooks");
|
|
@@ -86,16 +87,23 @@ async function handleAppInit(opts) {
|
|
|
86
87
|
version: 'unknown',
|
|
87
88
|
syncedSkills: [],
|
|
88
89
|
techSynced: false,
|
|
90
|
+
prunedStacks: [],
|
|
89
91
|
};
|
|
90
92
|
let steeringError;
|
|
91
93
|
if (scaffoldSteering) {
|
|
92
94
|
try {
|
|
93
|
-
|
|
95
|
+
// 与会话期的 `skills sync` 互斥:两者都写 `.agent/skills`,而写入策略是按目录
|
|
96
|
+
// 整清再拷。线上实测过这条竞态 —— init 的 steering 同步撞上 sync 收尾的
|
|
97
|
+
// `chmod -R a=rX`,报 `EACCES: mkdir '.agent/skills/steering/<stack>/skills'`。
|
|
98
|
+
steeringResult = (0, dir_lock_1.withDirLock)(targetDir, () => (0, coding_steering_1.syncCodingSteering)({
|
|
94
99
|
stack,
|
|
95
100
|
targetDir,
|
|
96
101
|
logPrefix: 'init',
|
|
97
102
|
outputLayout,
|
|
98
|
-
|
|
103
|
+
// match-template-version 门禁的比对基准:用刚渲染的 template 实际版本,
|
|
104
|
+
// 与随后写入 .spark/meta.json 的 version 同源(见下方 writeSparkMeta)
|
|
105
|
+
templateVersion: tplResult.version,
|
|
106
|
+
}));
|
|
99
107
|
}
|
|
100
108
|
catch (err) {
|
|
101
109
|
steeringError = err instanceof Error ? err.message : String(err);
|
|
@@ -144,12 +144,10 @@ async function handleAppMigrate(opts) {
|
|
|
144
144
|
// ignoreOutdatedRequests fix 注释)。直接 rm -rf 让新 vite 从干净状态 cold
|
|
145
145
|
// optimize, 一次写到位 lockfile 一致的 metadata, 不再触发中途 re-optimize。
|
|
146
146
|
//
|
|
147
|
-
// -
|
|
147
|
+
// - 只在沙箱内做(isSandboxEnv;本地一般不会撞这条链路)
|
|
148
148
|
// - 只在 install 成功时做(install 挂了 node_modules 状态不对, 清 cache 也救不回来)
|
|
149
149
|
// - 软失败:.vite 不存在 / 删失败都不阻断
|
|
150
|
-
if (installError === undefined &&
|
|
151
|
-
process.env.SANDBOX_ID !== undefined &&
|
|
152
|
-
process.env.SANDBOX_ID !== '') {
|
|
150
|
+
if (installError === undefined && (0, env_1.isSandboxEnv)()) {
|
|
153
151
|
const viteCacheDir = node_path_1.default.join(targetDir, 'node_modules', '.vite');
|
|
154
152
|
if (node_fs_1.default.existsSync(viteCacheDir)) {
|
|
155
153
|
(0, logger_1.log)('migrate', '清理 node_modules/.vite (vite cache 跟新 lockfile 错位会 504)...');
|
|
@@ -9,6 +9,7 @@ const coding_steering_1 = require("../../../utils/coding-steering");
|
|
|
9
9
|
const sandbox_skills_1 = require("../../../utils/sandbox-skills");
|
|
10
10
|
const file_ops_1 = require("../../../utils/file-ops");
|
|
11
11
|
const spark_meta_1 = require("../../../utils/spark-meta");
|
|
12
|
+
const dir_lock_1 = require("../../../utils/dir-lock");
|
|
12
13
|
const error_1 = require("../../../utils/error");
|
|
13
14
|
const output_1 = require("../../../utils/output");
|
|
14
15
|
/**
|
|
@@ -24,24 +25,35 @@ const output_1 = require("../../../utils/output");
|
|
|
24
25
|
*/
|
|
25
26
|
function syncSteeringForSandbox(params) {
|
|
26
27
|
let stack;
|
|
28
|
+
// 提到 try 外:下面 syncCodingSteering 要用它做 match-template-version 门禁
|
|
29
|
+
let templateVersion;
|
|
27
30
|
try {
|
|
28
31
|
const meta = (0, spark_meta_1.readSparkMeta)(params.targetDir);
|
|
29
32
|
if (meta.stack === undefined || meta.stack === '' || meta.stack === 'null') {
|
|
30
33
|
return { status: 'skipped', reason: '.spark/meta.json missing stack' };
|
|
31
34
|
}
|
|
32
35
|
stack = meta.stack;
|
|
36
|
+
templateVersion = meta.version;
|
|
33
37
|
}
|
|
34
38
|
catch (err) {
|
|
35
39
|
return { status: 'skipped', reason: `read .spark/meta.json failed: ${err.message}` };
|
|
36
40
|
}
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
41
|
+
// 版本口径:--version > lane(TCC tgzKey)> BOE 的 dist-tag dev > latest。
|
|
42
|
+
//
|
|
43
|
+
// **这里不预先把 BOE 解成 'dev'**(下方本地分支为了回报 versionSource 才那么做):
|
|
44
|
+
// 一旦提前填进 version,就会盖住 lane —— version 在优先级里高于 lane,BOE 沙箱上
|
|
45
|
+
// 配了 tgzKey 也永远命中不了。把 version 原样透传(未指定就是 undefined),让
|
|
46
|
+
// syncCodingSteering 一处算全链:version > lane > dev > latest。
|
|
40
47
|
try {
|
|
41
48
|
const r = (0, coding_steering_1.syncCodingSteering)({
|
|
42
49
|
stack,
|
|
43
50
|
targetDir: params.targetDir,
|
|
44
|
-
version:
|
|
51
|
+
version: params.version,
|
|
52
|
+
// TCC 的 X 与平铺 skill 共用:pickLaneVersion 只匹配 `-alpha.<X>` 后缀,
|
|
53
|
+
// 不看 base 版本,所以一个 key 同时定向 coding-steering 与 sandbox-skills。
|
|
54
|
+
tgzKey: params.tgzKey,
|
|
55
|
+
// match-template-version 门禁的比对基准(老 update-skills.sh 读的同一个字段)
|
|
56
|
+
templateVersion,
|
|
45
57
|
// 必须显式传 mode,不能落到 inferMode():它靠 env MIAODA_DEP_CACHE_DIR 嗅探,
|
|
46
58
|
// 而 mode 决定读包内哪个 layer(sandbox→`skills/`,local→`skills_local/`)。
|
|
47
59
|
// `--type` 分支本身就是沙箱语义,嗅探失手会去读不存在的 skills_local/、
|
|
@@ -56,6 +68,7 @@ function syncSteeringForSandbox(params) {
|
|
|
56
68
|
version: r.version,
|
|
57
69
|
syncedSkills: r.syncedSkills,
|
|
58
70
|
techSynced: r.techSynced,
|
|
71
|
+
prunedStacks: r.prunedStacks,
|
|
59
72
|
};
|
|
60
73
|
}
|
|
61
74
|
catch (err) {
|
|
@@ -70,9 +83,16 @@ function syncSteeringForSandbox(params) {
|
|
|
70
83
|
*
|
|
71
84
|
* 版本来源优先级(从高到低):
|
|
72
85
|
* 1. CLI `--version` flag(显式指定)
|
|
73
|
-
* 2. `
|
|
86
|
+
* 2. lane:`--type` 分支下由 TCC `skillsUpdateTgzKey` 透传 `--tgz-key`,反推 X 后
|
|
87
|
+
* 解析出 `-alpha.<X>` 版本。**与平铺 skill 共用同一个 X** —— 匹配只看后缀、
|
|
88
|
+
* 不看 base 版本,所以 TCC 配一个 key 就同时定向 coding-steering 与 sandbox-skills。
|
|
89
|
+
* 本地分支(不带 `--type`)没有 tgzKey,这一档天然不参与。
|
|
90
|
+
* 3. `FORCE_FRAMEWORK_ENVIRONMENT=boe` → dist-tag `dev`(BOE 测试灰度,跟随
|
|
74
91
|
* publish-alpha.sh 在 develop 分支打的 dev tag,便于在 BOE 沙箱里验证预发 steering)
|
|
75
|
-
*
|
|
92
|
+
* 4. 默认 `latest`
|
|
93
|
+
*
|
|
94
|
+
* 2/3/4 档统一在 `syncCodingSteering` 里算(见那边注释),handler 只负责透传,避免
|
|
95
|
+
* 提前把 `dev` 填进 version 把 lane 挤掉。
|
|
76
96
|
*
|
|
77
97
|
* **不读 meta.json**——steering 版本不在 meta 里钉。
|
|
78
98
|
*/
|
|
@@ -95,41 +115,57 @@ async function handleSkillsSync(opts) {
|
|
|
95
115
|
// 冻结只冻结了一半。
|
|
96
116
|
if (opts.type !== undefined && opts.type !== '') {
|
|
97
117
|
const { type, tgzKey } = (0, sandbox_skills_1.validateSandboxSyncOpts)({ type: opts.type, tgzKey: opts.tgzKey });
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
// 锁上就写不进去了。`.keep` 冻结时不锁 —— 老链路那条路径是 exit 0 直接走、
|
|
104
|
-
// 末尾的 chmod 压根不执行,保持一致。
|
|
105
|
-
//
|
|
106
|
-
// **只锁沙箱侧**:这里的 result.targetDir 只会是 `.agent/skills` 或 `skills`
|
|
107
|
-
// (见 sandbox-skills 的 skillsRoot),都是平台下发的资产根。本地 `--local` 走
|
|
108
|
-
// flat layout 输出到 `.agents/`,压根不经过本分支,权限不会被碰。
|
|
109
|
-
//
|
|
110
|
-
// 回退风险由解锁侧兜:权限是持久化在磁盘的,代码能回退、磁盘状态不会。所以
|
|
111
|
-
// file-ops 的 unlockDirForWrite 在**两处写入前**都调用了(syncSandboxSkills 的
|
|
112
|
-
// 写入前 + syncCodingSteering 的 nested 分支写入前),任一入口都不会因为
|
|
113
|
-
// 上一轮的锁而 EACCES。改动这里之前先确认那两处解锁还在。
|
|
114
|
-
const lockedReadonly = result.skippedByKeepFile ? false : (0, file_ops_1.lockDirReadonly)(result.targetDir);
|
|
115
|
-
(0, output_1.emit)({
|
|
116
|
-
data: {
|
|
117
|
-
mode: 'sandbox',
|
|
118
|
-
type,
|
|
119
|
-
packageName: result.packageName,
|
|
120
|
-
version: result.version,
|
|
121
|
-
lane: result.lane,
|
|
122
|
-
syncedTop: result.syncedTop,
|
|
123
|
-
deleted: result.deleted,
|
|
124
|
-
skippedByKeepFile: result.skippedByKeepFile,
|
|
125
|
-
lockedReadonly,
|
|
126
|
-
targetDir: result.targetDir,
|
|
127
|
-
steering,
|
|
128
|
-
},
|
|
118
|
+
// 并发锁覆盖「平铺同步 + steering 同步 + 收尾 chmod」整段:写入策略是按目录整清再拷,
|
|
119
|
+
// 两个实例并发会互相看到半清空状态,而 feida-ai 的 scanAllSkills 是会话期按需扫盘。
|
|
120
|
+
// 对齐老 update-skills.sh:157-159 的 flock。
|
|
121
|
+
(0, dir_lock_1.withDirLock)(targetDir, () => {
|
|
122
|
+
syncSandboxAndSteering({ opts, targetDir, type, tgzKey });
|
|
129
123
|
});
|
|
130
124
|
return;
|
|
131
125
|
}
|
|
132
126
|
// 原有 coding-steering 同步逻辑(本地 dev / miaoda app init 用)
|
|
127
|
+
(0, dir_lock_1.withDirLock)(targetDir, () => {
|
|
128
|
+
syncSteeringOnly({ opts, targetDir });
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
function syncSandboxAndSteering(params) {
|
|
132
|
+
const { opts, targetDir, type, tgzKey } = params;
|
|
133
|
+
const result = (0, sandbox_skills_1.syncSandboxSkills)({ type, tgzKey, targetDir });
|
|
134
|
+
const steering = result.skippedByKeepFile
|
|
135
|
+
? { status: 'skipped', reason: '.keep file found, sync frozen' }
|
|
136
|
+
: syncSteeringForSandbox({ targetDir, version: opts.version, tgzKey });
|
|
137
|
+
// ③ 收尾锁只读(update-skills.sh:241 的 chmod -R a=rX)。必须在 ①② 全部写完之后,
|
|
138
|
+
// 锁上就写不进去了。`.keep` 冻结时不锁 —— 老链路那条路径是 exit 0 直接走、
|
|
139
|
+
// 末尾的 chmod 压根不执行,保持一致。
|
|
140
|
+
//
|
|
141
|
+
// **只锁沙箱侧**:这里的 result.targetDir 只会是 `.agent/skills` 或 `skills`
|
|
142
|
+
// (见 sandbox-skills 的 skillsRoot),都是平台下发的资产根。本地 `--local` 走
|
|
143
|
+
// flat layout 输出到 `.agents/`,压根不经过本分支,权限不会被碰。
|
|
144
|
+
//
|
|
145
|
+
// 回退风险由解锁侧兜:权限是持久化在磁盘的,代码能回退、磁盘状态不会。所以
|
|
146
|
+
// file-ops 的 unlockDirForWrite 在**两处写入前**都调用了(syncSandboxSkills 的
|
|
147
|
+
// 写入前 + syncCodingSteering 的 nested 分支写入前),任一入口都不会因为
|
|
148
|
+
// 上一轮的锁而 EACCES。改动这里之前先确认那两处解锁还在。
|
|
149
|
+
const lockedReadonly = result.skippedByKeepFile ? false : (0, file_ops_1.lockDirReadonly)(result.targetDir);
|
|
150
|
+
(0, output_1.emit)({
|
|
151
|
+
data: {
|
|
152
|
+
mode: 'sandbox',
|
|
153
|
+
type,
|
|
154
|
+
packageName: result.packageName,
|
|
155
|
+
version: result.version,
|
|
156
|
+
lane: result.lane,
|
|
157
|
+
syncedTop: result.syncedTop,
|
|
158
|
+
deleted: result.deleted,
|
|
159
|
+
skippedByKeepFile: result.skippedByKeepFile,
|
|
160
|
+
lockedReadonly,
|
|
161
|
+
targetDir: result.targetDir,
|
|
162
|
+
steering,
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
/** 不带 --type 的本地 / init 形态:只同步 coding-steering。 */
|
|
167
|
+
function syncSteeringOnly(params) {
|
|
168
|
+
const { opts, targetDir } = params;
|
|
133
169
|
const meta = (0, spark_meta_1.readSparkMeta)(targetDir);
|
|
134
170
|
if (meta.stack === undefined || meta.stack === '') {
|
|
135
171
|
throw new error_1.AppError('SKILLS_META_INCOMPLETE', '.spark/meta.json missing stack — run `miaoda app init` first');
|
|
@@ -141,6 +177,7 @@ async function handleSkillsSync(opts) {
|
|
|
141
177
|
stack: meta.stack,
|
|
142
178
|
targetDir,
|
|
143
179
|
version: effectiveVersion,
|
|
180
|
+
templateVersion: meta.version,
|
|
144
181
|
outputLayout: opts.local === true ? 'flat' : 'nested',
|
|
145
182
|
});
|
|
146
183
|
(0, output_1.emit)({
|
|
@@ -150,6 +187,7 @@ async function handleSkillsSync(opts) {
|
|
|
150
187
|
versionSource,
|
|
151
188
|
syncedSkills: result.syncedSkills,
|
|
152
189
|
techSynced: result.techSynced,
|
|
190
|
+
prunedStacks: result.prunedStacks,
|
|
153
191
|
...(result.claudeSkillsLink !== undefined
|
|
154
192
|
? { claudeSkillsLink: result.claudeSkillsLink }
|
|
155
193
|
: {}),
|
|
@@ -91,7 +91,7 @@ exports.SYNC_CONFIG = {
|
|
|
91
91
|
ifStartsWith: 'npm run upgrade && ',
|
|
92
92
|
},
|
|
93
93
|
// ===== miaoda-cli 本地开发新增规则 =====
|
|
94
|
-
// M1. scripts.dev:local ——
|
|
94
|
+
// M1. scripts.dev:local —— 本地用户绕过沙箱判定直接跑本地链路(npm run dev:local)。
|
|
95
95
|
{
|
|
96
96
|
type: 'add-script',
|
|
97
97
|
name: 'dev:local',
|
|
@@ -135,8 +135,8 @@ exports.SYNC_CONFIG = {
|
|
|
135
135
|
ifStartsWith: 'concurrently ',
|
|
136
136
|
},
|
|
137
137
|
// 10. 把老 `npm run upgrade && ./scripts/dev.sh` 形式迁移到 `./scripts/dev.sh`,
|
|
138
|
-
// 让本地 `npm run dev` 不再前置跑 fullstack-cli sync。新版 dev.sh 按
|
|
139
|
-
//
|
|
138
|
+
// 让本地 `npm run dev` 不再前置跑 fullstack-cli sync。新版 dev.sh 按 MIAODA_DEP_CACHE_DIR
|
|
139
|
+
// 是否非空判分支:沙箱直接 exec dev.js(脚本同步由平台 pod 启动时做过,dev 入口不再 upgrade);
|
|
140
140
|
// 本地走 miaoda app sync 兜底 + dev-local.js。
|
|
141
141
|
{
|
|
142
142
|
type: 'patch-script',
|
|
@@ -160,8 +160,8 @@ exports.SYNC_CONFIG = {
|
|
|
160
160
|
ifStartsWith: 'npx -y @lark-apaas/fullstack-cli sync',
|
|
161
161
|
},
|
|
162
162
|
// ===== miaoda-cli 本地开发新增规则(fullstack-cli sync 不会执行) =====
|
|
163
|
-
// M1. scripts.dev:local ——
|
|
164
|
-
// dev.sh 在
|
|
163
|
+
// M1. scripts.dev:local —— 本地用户绕过沙箱判定直接跑本地链路(npm run dev:local)。
|
|
164
|
+
// dev.sh 在 MIAODA_DEP_CACHE_DIR 非空时跑 dev.js(沙箱保活)、否则 exec dev-local.js;显式 dev:local
|
|
165
165
|
// 用于 agent 在本地稳定调试,不受任何 env 影响。
|
|
166
166
|
{
|
|
167
167
|
type: 'add-script',
|
|
@@ -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) {
|
|
@@ -7,7 +7,9 @@ exports.syncCodingSteering = syncCodingSteering;
|
|
|
7
7
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
8
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
9
|
const npm_pack_1 = require("../utils/npm-pack");
|
|
10
|
+
const file_ops_1 = require("../utils/file-ops");
|
|
10
11
|
const logger_1 = require("../utils/logger");
|
|
12
|
+
const logger_2 = require("../utils/logger");
|
|
11
13
|
const env_1 = require("../utils/env");
|
|
12
14
|
const STEERING_PACKAGE = '@lark-apaas/coding-steering';
|
|
13
15
|
function inferMode() {
|
|
@@ -40,10 +42,30 @@ function inferMode() {
|
|
|
40
42
|
*/
|
|
41
43
|
function syncCodingSteering(opts) {
|
|
42
44
|
const logPrefix = opts.logPrefix ?? 'skills';
|
|
43
|
-
|
|
45
|
+
// 版本优先级:--version(显式 pin / 止血)> lane(TCC 定向灰度)> BOE 默认 @dev > @latest
|
|
46
|
+
//
|
|
47
|
+
// 与 sandbox-skills 的 syncSandboxSkills 完全同一套口径,只是包名不同。BOE 的 @dev
|
|
48
|
+
// 由 miaoda-coding develop 分支的自动发布维护;lane 必须压过 dev —— TCC 配了 tgzKey
|
|
49
|
+
// 就是定向到某个 feat 分支的快照,不该被"BOE 跟 develop"这条默认规则盖掉。
|
|
50
|
+
//
|
|
51
|
+
// BOE→dev 这一档在本函数里算,不由调用方预填:`skills sync --type` 分支会带 tgzKey
|
|
52
|
+
// 进来,调用方若先把 'dev' 塞进 version,version 档位更高,lane 就永远命不中。
|
|
53
|
+
// 本地分支 / app init 为了回报 versionSource 仍会自己算 dev 再传 version —— 那两条路
|
|
54
|
+
// 没有 tgzKey,lane 档天然为空,两处口径一致不会打架。
|
|
55
|
+
//
|
|
56
|
+
// lane 解析与 sandbox-skills 同一套:`extractLaneFromTgzKey` 反推 X,`pickLaneVersion`
|
|
57
|
+
// 过滤后缀 `-alpha.<X>`。**匹配只看后缀、不看 base 版本**,所以 coding-steering
|
|
58
|
+
// (`0.1.3x-alpha.<X>`)和 sandbox-skills(`0.1.x-alpha.<X>`)能共用 TCC 里的同一个
|
|
59
|
+
// key —— 前提是 lane 发布脚本用同一个 X 把两边一起发出去。这个包没发 `-alpha.<X>`
|
|
60
|
+
// 时 pickLaneVersion 返回 null(它对报错 / 无匹配都返回 null,绝不 throw),静默
|
|
61
|
+
// 回落到 dev / latest,不影响现有行为。
|
|
62
|
+
const lane = (0, npm_pack_1.extractLaneFromTgzKey)(opts.tgzKey);
|
|
63
|
+
const laneVersion = lane === null ? null : (0, npm_pack_1.pickLaneVersion)(STEERING_PACKAGE, lane);
|
|
64
|
+
const isBoe = process.env.FORCE_FRAMEWORK_ENVIRONMENT === 'boe';
|
|
65
|
+
const effectiveVersion = opts.version ?? laneVersion ?? (isBoe ? 'dev' : 'latest');
|
|
44
66
|
const mode = opts.mode ?? inferMode();
|
|
45
67
|
const layout = opts.outputLayout ?? 'nested';
|
|
46
|
-
(0,
|
|
68
|
+
(0, logger_2.log)(logPrefix, `Fetching ${STEERING_PACKAGE}@${effectiveVersion} (mode=${mode}, layout=${layout})...`);
|
|
47
69
|
const fetched = (0, npm_pack_1.fetchNpmPackage)({
|
|
48
70
|
packageName: STEERING_PACKAGE,
|
|
49
71
|
version: effectiveVersion,
|
|
@@ -73,6 +95,17 @@ function syncCodingSteering(opts) {
|
|
|
73
95
|
// (老脚本 :179/:208 写明 `steering/ 子树由 miaoda CLI 独占,update.sh 不写不删`),
|
|
74
96
|
// 锁它没收益;而权限逻辑散在写入方会跟收尾的 lockDirReadonly 形成隐式耦合,
|
|
75
97
|
// 范围一旦不对称就会出「跑一次 skills sync 把整个 skills 目录解封」这种事。
|
|
98
|
+
// nested:把 `steering/<stack>/` 整棵清掉再重建。
|
|
99
|
+
//
|
|
100
|
+
// 这一刀同时覆盖了三件事,省掉了此前一堆对账逻辑:源包删掉的 skill、被门禁判否的
|
|
101
|
+
// skill、源包这版不再提供的 tech.md —— 全都因为「先清后拷」自然消失。
|
|
102
|
+
//
|
|
103
|
+
// flat(`.agents/skills`)**刻意不清**:那是本地 dev 目录,开发者会往里放自己手写的
|
|
104
|
+
// skill,「不在本轮同步清单里就删」等于替用户删东西。老脚本压根没有 flat 概念。
|
|
105
|
+
if (layout === 'nested') {
|
|
106
|
+
const stackDst = node_path_1.default.join(opts.targetDir, '.agent', 'skills', 'steering', opts.stack);
|
|
107
|
+
(0, file_ops_1.forceRemove)(stackDst);
|
|
108
|
+
}
|
|
76
109
|
node_fs_1.default.mkdirSync(dstSkillsDir, { recursive: true });
|
|
77
110
|
let techSynced = false;
|
|
78
111
|
const techSrc = node_path_1.default.join(stackDir, 'tech.md');
|
|
@@ -81,7 +114,17 @@ function syncCodingSteering(opts) {
|
|
|
81
114
|
node_fs_1.default.copyFileSync(techSrc, dstTechPath);
|
|
82
115
|
techSynced = true;
|
|
83
116
|
}
|
|
117
|
+
else if (layout === 'flat' && node_fs_1.default.existsSync(dstTechPath)) {
|
|
118
|
+
// flat 没有整清,源包不再提供 tech.md 时需显式删掉残留;nested 已被整清带走
|
|
119
|
+
try {
|
|
120
|
+
(0, file_ops_1.forceRemove)(dstTechPath);
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
(0, logger_1.debug)(`coding-steering: remove stale tech.md failed: ${err.message}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
84
126
|
const synced = [];
|
|
127
|
+
const filteredOut = [];
|
|
85
128
|
// 顺序拷贝,后者覆盖前者:skills_common 铺底,专有层(skills 或 skills_local)覆盖同名
|
|
86
129
|
const exclusiveLayer = mode === 'sandbox' ? 'skills' : 'skills_local';
|
|
87
130
|
const layers = [
|
|
@@ -95,22 +138,55 @@ function syncCodingSteering(opts) {
|
|
|
95
138
|
const src = node_path_1.default.join(layer.dir, name);
|
|
96
139
|
if (!node_fs_1.default.statSync(src).isDirectory())
|
|
97
140
|
continue;
|
|
141
|
+
if (!matchesTemplate(src, opts.stack, opts.templateVersion, logPrefix)) {
|
|
142
|
+
filteredOut.push(`${layer.label}/${name}`);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
98
145
|
const dst = node_path_1.default.join(dstSkillsDir, name);
|
|
146
|
+
// flat 下目录没被整清,同名仍需先删再拷(skills_common 铺底、专有层覆盖同名)
|
|
99
147
|
if (node_fs_1.default.existsSync(dst))
|
|
100
148
|
node_fs_1.default.rmSync(dst, { recursive: true, force: true });
|
|
101
149
|
copyDir(src, dst);
|
|
102
150
|
synced.push(`${layer.label}/${name}`);
|
|
103
151
|
}
|
|
104
152
|
}
|
|
153
|
+
// 收尾(仅 nested):删掉同级其它 stack 的整棵 steering 目录。
|
|
154
|
+
// meta.json 的 stack 唯一,切栈后旧 stack 的 steering 不该再留 —— 修的正是
|
|
155
|
+
// 「vite-react 升级到 nestjs-react-fullstack 后 steering/vite-react/ 永久残留」。
|
|
156
|
+
// 老脚本靠 should_sync 里 `path_stack != STACK → return 1` + 逐文件删除达到同样效果。
|
|
157
|
+
// flat layout 没有 stack 命名空间,天然不涉及。
|
|
158
|
+
const prunedStacks = [];
|
|
159
|
+
if (layout === 'nested') {
|
|
160
|
+
const steeringDst = node_path_1.default.join(opts.targetDir, '.agent', 'skills', 'steering');
|
|
161
|
+
if (node_fs_1.default.existsSync(steeringDst)) {
|
|
162
|
+
for (const entry of node_fs_1.default.readdirSync(steeringDst)) {
|
|
163
|
+
if (entry === opts.stack)
|
|
164
|
+
continue;
|
|
165
|
+
try {
|
|
166
|
+
(0, file_ops_1.forceRemove)(node_path_1.default.join(steeringDst, entry));
|
|
167
|
+
prunedStacks.push(entry);
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
(0, logger_1.debug)(`coding-steering: prune stale stack ${entry} failed: ${err.message}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
105
175
|
let claudeSkillsLink;
|
|
106
176
|
if (layout === 'flat') {
|
|
107
177
|
claudeSkillsLink = ensureClaudeSkillsSymlink(opts.targetDir, logPrefix);
|
|
108
178
|
}
|
|
109
|
-
|
|
179
|
+
const extras = [
|
|
180
|
+
prunedStacks.length > 0 ? `pruned stack(s) ${prunedStacks.join(',')}` : '',
|
|
181
|
+
filteredOut.length > 0 ? `filtered ${String(filteredOut.length)} by version` : '',
|
|
182
|
+
].filter((x) => x !== '');
|
|
183
|
+
(0, logger_2.log)(logPrefix, `Synced ${String(synced.length)} skill(s), tech.md ${techSynced ? 'yes' : 'no'} ` +
|
|
184
|
+
`(mode=${mode}, layout=${layout})${extras.length > 0 ? ` [${extras.join('; ')}]` : ''}`);
|
|
110
185
|
return {
|
|
111
186
|
version: fetched.version,
|
|
112
187
|
syncedSkills: synced,
|
|
113
188
|
techSynced,
|
|
189
|
+
prunedStacks,
|
|
114
190
|
...(claudeSkillsLink !== undefined ? { claudeSkillsLink } : {}),
|
|
115
191
|
};
|
|
116
192
|
}
|
|
@@ -144,7 +220,7 @@ function ensureClaudeSkillsSymlink(targetDir, logPrefix) {
|
|
|
144
220
|
}
|
|
145
221
|
if (existing === null) {
|
|
146
222
|
node_fs_1.default.symlinkSync(linkTarget, linkPath, 'dir');
|
|
147
|
-
(0,
|
|
223
|
+
(0, logger_2.log)(logPrefix, ` ✓ .claude/skills → ${linkTarget} (symlink created)`);
|
|
148
224
|
return 'created';
|
|
149
225
|
}
|
|
150
226
|
if (existing.isSymbolicLink()) {
|
|
@@ -154,12 +230,98 @@ function ensureClaudeSkillsSymlink(targetDir, logPrefix) {
|
|
|
154
230
|
}
|
|
155
231
|
node_fs_1.default.unlinkSync(linkPath);
|
|
156
232
|
node_fs_1.default.symlinkSync(linkTarget, linkPath, 'dir');
|
|
157
|
-
(0,
|
|
233
|
+
(0, logger_2.log)(logPrefix, ` ✓ .claude/skills → ${linkTarget} (symlink updated, was → ${current})`);
|
|
158
234
|
return 'updated';
|
|
159
235
|
}
|
|
160
|
-
(0,
|
|
236
|
+
(0, logger_2.log)(logPrefix, ` ⚠ .claude/skills 已是普通目录/文件,跳过软链创建`);
|
|
161
237
|
return 'conflict';
|
|
162
238
|
}
|
|
239
|
+
/**
|
|
240
|
+
* 读 SKILL.md frontmatter 里的 `match-template-version`。逐字对齐老脚本的取值方式
|
|
241
|
+
* (`update-skills.sh:80-90`):只看第一段 `---` 围起来的 frontmatter,去掉引号与全部空白。
|
|
242
|
+
* 没有该字段(或没有 SKILL.md)返回 undefined = 不设版本门禁。
|
|
243
|
+
*/
|
|
244
|
+
function readMatchTemplateVersion(skillDir) {
|
|
245
|
+
const skillMd = node_path_1.default.join(skillDir, 'SKILL.md');
|
|
246
|
+
if (!node_fs_1.default.existsSync(skillMd))
|
|
247
|
+
return undefined;
|
|
248
|
+
let content;
|
|
249
|
+
try {
|
|
250
|
+
content = node_fs_1.default.readFileSync(skillMd, 'utf-8');
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
return undefined;
|
|
254
|
+
}
|
|
255
|
+
const fm = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content);
|
|
256
|
+
if (fm === null)
|
|
257
|
+
return undefined;
|
|
258
|
+
const line = /^match-template-version:(.*)$/m.exec(fm[1]);
|
|
259
|
+
if (line === null)
|
|
260
|
+
return undefined;
|
|
261
|
+
const value = line[1].replace(/["']/g, '').replace(/\s/g, '');
|
|
262
|
+
return value === '' ? undefined : value;
|
|
263
|
+
}
|
|
264
|
+
/** 读 frontmatter 里的 `match-template-name`(同上,取不到返回 undefined)。 */
|
|
265
|
+
function readMatchTemplateName(skillDir) {
|
|
266
|
+
const skillMd = node_path_1.default.join(skillDir, 'SKILL.md');
|
|
267
|
+
if (!node_fs_1.default.existsSync(skillMd))
|
|
268
|
+
return undefined;
|
|
269
|
+
let content;
|
|
270
|
+
try {
|
|
271
|
+
content = node_fs_1.default.readFileSync(skillMd, 'utf-8');
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
const fm = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content);
|
|
277
|
+
if (fm === null)
|
|
278
|
+
return undefined;
|
|
279
|
+
const line = /^match-template-name:(.*)$/m.exec(fm[1]);
|
|
280
|
+
if (line === null)
|
|
281
|
+
return undefined;
|
|
282
|
+
const value = line[1].replace(/["']/g, '').trim();
|
|
283
|
+
return value === '' ? undefined : value;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* `match-template-version` 门禁,语义逐字对齐老 `check_version_match`
|
|
287
|
+
* (`update-skills.sh:76-108`):
|
|
288
|
+
* - skill 没声明该字段 → 放行
|
|
289
|
+
* - 声明值是纯整数 → 与用户版本的 **major** 比(`3` 匹配 `3.2.1`)
|
|
290
|
+
* - 其它形态(如 `2.2.6`) → 与用户版本**全等**
|
|
291
|
+
* - 用户版本未知(meta 里没 version)→ 放行(见 opts.templateVersion 注释)
|
|
292
|
+
*/
|
|
293
|
+
function matchesTemplateVersion(skillDir, templateVersion) {
|
|
294
|
+
const required = readMatchTemplateVersion(skillDir);
|
|
295
|
+
if (required === undefined)
|
|
296
|
+
return true;
|
|
297
|
+
if (templateVersion === undefined || templateVersion === '') {
|
|
298
|
+
(0, logger_1.debug)(`coding-steering: ${node_path_1.default.basename(skillDir)} 声明 match-template-version=${required},` +
|
|
299
|
+
`但 .spark/meta.json 无 version,放行不过滤`);
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
if (/^\d+$/.test(required)) {
|
|
303
|
+
return templateVersion.split('.')[0] === required;
|
|
304
|
+
}
|
|
305
|
+
return templateVersion === required;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* 组合门禁:`match-template-name` + `match-template-version`。
|
|
309
|
+
*
|
|
310
|
+
* `match-template-name` 在这里是**防御性校验**而非过滤主力:包内 skill 本来就住在
|
|
311
|
+
* `steering/<stack>/` 下、目录结构已经按 stack 隔离,声明值理应等于所在 stack
|
|
312
|
+
* (实测包里 23 处全是 `nestjs-react-fullstack`)。真正跨 stack 的池化过滤在 feida-ai
|
|
313
|
+
* 加载时做(`skills.ts:1209` 的 `isTemplateMatched`)。所以这里对不上就是**包结构出问题**,
|
|
314
|
+
* 用 log(常显)而不是 debug 报出来,别静默下发到错误的 stack。
|
|
315
|
+
*/
|
|
316
|
+
function matchesTemplate(skillDir, stack, templateVersion, logPrefix) {
|
|
317
|
+
const declaredName = readMatchTemplateName(skillDir);
|
|
318
|
+
if (declaredName !== undefined && declaredName !== stack) {
|
|
319
|
+
(0, logger_2.log)(logPrefix, `⚠ ${node_path_1.default.basename(skillDir)} 声明 match-template-name=${declaredName},` +
|
|
320
|
+
`但它位于 steering/${stack}/ 下 —— 包结构异常,跳过该 skill`);
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
return matchesTemplateVersion(skillDir, templateVersion);
|
|
324
|
+
}
|
|
163
325
|
function copyDir(src, dest) {
|
|
164
326
|
node_fs_1.default.mkdirSync(dest, { recursive: true });
|
|
165
327
|
for (const entry of node_fs_1.default.readdirSync(src, { withFileTypes: true })) {
|
|
@@ -0,0 +1,103 @@
|
|
|
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.withDirLock = withDirLock;
|
|
7
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
8
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
10
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
+
const error_1 = require("../utils/error");
|
|
12
|
+
const logger_1 = require("../utils/logger");
|
|
13
|
+
/**
|
|
14
|
+
* 跨进程互斥锁,对齐老 `update-skills.sh:157-159` 的 `flock`:
|
|
15
|
+
*
|
|
16
|
+
* ```bash
|
|
17
|
+
* LOCK_FILE="/tmp/update-skills-$(echo "$TARGET_DIR" | md5sum | cut -d' ' -f1).lock"
|
|
18
|
+
* exec 9>"$LOCK_FILE"; flock 9
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* 为什么必须有:skills 同步现在是「按目录整清再拷」,两个实例并发时会互相看到半清空
|
|
22
|
+
* 状态;而 feida-ai 的 `scanAllSkills`(skills.ts:533)是会话期按需扫盘,正好能撞上这个
|
|
23
|
+
* 窗口 —— 历史上出过 skill 集合从 59 掉到 34 的事故。触发并发的现实路径:
|
|
24
|
+
* sandbox_updater 自带重试 + `app init` 与会话期 sync 同时跑。
|
|
25
|
+
*
|
|
26
|
+
* 用 `mkdir` 而不是写文件:`mkdirSync` 在 POSIX 与 Windows 上都是原子的「存在即失败」,
|
|
27
|
+
* 不需要 O_EXCL 之类的平台差异处理,也不依赖第三方库。
|
|
28
|
+
*/
|
|
29
|
+
/** 轮询间隔 */
|
|
30
|
+
const POLL_INTERVAL_MS = 200;
|
|
31
|
+
/** 默认最长等待:另一个实例正常跑完是秒级,给足余量 */
|
|
32
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
33
|
+
/**
|
|
34
|
+
* 陈旧锁接管阈值。进程被 SIGKILL(沙箱重建 / OOM)时 finally 不会执行、锁目录会留下,
|
|
35
|
+
* 没有接管机制就会把后续所有同步永久堵死。10 分钟远大于一次正常同步的耗时。
|
|
36
|
+
*/
|
|
37
|
+
const STALE_TAKEOVER_MS = 600_000;
|
|
38
|
+
function lockDirFor(key) {
|
|
39
|
+
const hash = node_crypto_1.default.createHash('md5').update(node_path_1.default.resolve(key)).digest('hex');
|
|
40
|
+
return node_path_1.default.join(node_os_1.default.tmpdir(), `miaoda-skills-sync-${hash}.lock`);
|
|
41
|
+
}
|
|
42
|
+
/** 锁目录存在且已超过陈旧阈值 → 接管(删掉重建)。返回是否接管过。 */
|
|
43
|
+
function takeoverIfStale(lockDir) {
|
|
44
|
+
try {
|
|
45
|
+
const ageMs = Date.now() - node_fs_1.default.statSync(lockDir).mtimeMs;
|
|
46
|
+
if (ageMs < STALE_TAKEOVER_MS)
|
|
47
|
+
return false;
|
|
48
|
+
node_fs_1.default.rmSync(lockDir, { recursive: true, force: true });
|
|
49
|
+
(0, logger_1.log)('skills', `接管陈旧锁(持有 ${String(Math.round(ageMs / 1000))}s,超过阈值):${lockDir}`);
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// stat 失败通常是刚好被别人释放了,交回上层重试
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 以 `key`(一般传目标目录)为粒度加锁执行 `fn`。
|
|
59
|
+
*
|
|
60
|
+
* 拿不到锁时**抛 AppError 而不是无锁硬跑**:并发方此刻正在把同一棵树刷成最新,硬跑只会
|
|
61
|
+
* 互相覆盖。抛错让上层(sandbox_updater 有重试)稍后再来,磁盘状态始终是完整的一份。
|
|
62
|
+
*
|
|
63
|
+
* 注意不可重入:同进程内嵌套调用同一 key 会等到超时。调用点都在各自的「一次完整同步」
|
|
64
|
+
* 外层,不存在嵌套。
|
|
65
|
+
*/
|
|
66
|
+
function withDirLock(key, fn, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
67
|
+
const lockDir = lockDirFor(key);
|
|
68
|
+
const deadline = Date.now() + timeoutMs;
|
|
69
|
+
let acquired = false;
|
|
70
|
+
while (!acquired) {
|
|
71
|
+
try {
|
|
72
|
+
node_fs_1.default.mkdirSync(lockDir, { recursive: false });
|
|
73
|
+
acquired = true;
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
if (err.code !== 'EEXIST')
|
|
77
|
+
throw err;
|
|
78
|
+
if (takeoverIfStale(lockDir))
|
|
79
|
+
continue;
|
|
80
|
+
if (Date.now() >= deadline) {
|
|
81
|
+
throw new error_1.AppError('SKILLS_SYNC_LOCKED', `另一个 skills 同步正在进行(锁:${lockDir}),等待 ${String(Math.round(timeoutMs / 1000))}s 后仍未释放`);
|
|
82
|
+
}
|
|
83
|
+
// 忙等:同步本身是重 IO 的短任务,简单轮询比引入 async 更贴合现有同步调用链
|
|
84
|
+
const until = Date.now() + POLL_INTERVAL_MS;
|
|
85
|
+
while (Date.now() < until) {
|
|
86
|
+
/* spin */
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
(0, logger_1.debug)(`dir-lock: acquired ${lockDir}`);
|
|
91
|
+
try {
|
|
92
|
+
return fn();
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
try {
|
|
96
|
+
node_fs_1.default.rmSync(lockDir, { recursive: true, force: true });
|
|
97
|
+
(0, logger_1.debug)(`dir-lock: released ${lockDir}`);
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
(0, logger_1.debug)(`dir-lock: release failed ${lockDir}: ${err.message}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
package/dist/utils/env.js
CHANGED
|
@@ -7,9 +7,9 @@ exports.isSandboxEnv = isSandboxEnv;
|
|
|
7
7
|
* 口径:`process.env.MIAODA_DEP_CACHE_DIR` 非空(既非 undefined 也非空串)即为沙箱。
|
|
8
8
|
* 沙箱平台运行时会给应用进程注入依赖缓存目录;本地 dev / agent 环境拿不到该变量。
|
|
9
9
|
*
|
|
10
|
-
* 历史上用 `SANDBOX_ID`
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* 历史上用 `SANDBOX_ID` 判断,但它由 sandbox_console 在绑定沙箱时才写入
|
|
11
|
+
* `.force/environment/env`,预热池实例在绑定前拿不到,故换成 `MIAODA_DEP_CACHE_DIR`。
|
|
12
|
+
* 下发给 user app 的 `scripts/dev.sh` 是独立 bash 链路(不复用本函数),现已同口径。
|
|
13
13
|
*
|
|
14
14
|
* 这是 CLI 运行时沙箱判定的 SSOT —— init outputLayout 分流、migrate 重启 dev process、
|
|
15
15
|
* coding-steering 同步 mode 推断、async-install 写 marker 都走这里,别再各处内联读环境变量。
|
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
|
}
|
|
@@ -177,60 +168,73 @@ function syncSandboxSkills(opts) {
|
|
|
177
168
|
(0, file_ops_1.unlockDirForWrite)(skillsRoot);
|
|
178
169
|
node_fs_1.default.mkdirSync(skillsRoot, { recursive: true });
|
|
179
170
|
const syncedTop = [];
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
//
|
|
171
|
+
// ---- 第一步:只收集「哪个顶层条目由哪些源目录贡献」,先不落盘 ----
|
|
172
|
+
//
|
|
173
|
+
// 为什么要分两步:写入策略是**按顶层条目整清再拷**(见第二步),而合并包里业务线与
|
|
174
|
+
// shared 可能提供同名条目(运行时 union、同名 shared 赢,逐字对齐老 build.sh:33/47)。
|
|
175
|
+
// 如果边遍历边「清+拷」,shared 那一轮会把业务线刚写进去的独有文件一起清掉。
|
|
176
|
+
// 先把 union 收集齐、每个条目只清一次,才能既做到镜像又保住 union 语义。
|
|
177
|
+
//
|
|
178
|
+
// sourceTop 同时是 pruneStaleEntries 的保留集:它是「源包提供了什么」,
|
|
179
|
+
// 不能用 syncedTop(本轮实际写入)—— 否则 shared 条目会被反复删了又写、来回抖动。
|
|
184
180
|
const sourceTop = new Set();
|
|
181
|
+
const plan = new Map();
|
|
182
|
+
const addSource = (entry, dir) => {
|
|
183
|
+
sourceTop.add(entry);
|
|
184
|
+
const list = plan.get(entry);
|
|
185
|
+
if (list === undefined)
|
|
186
|
+
plan.set(entry, [dir]);
|
|
187
|
+
else
|
|
188
|
+
list.push(dir);
|
|
189
|
+
};
|
|
185
190
|
if (businessLine !== null) {
|
|
186
|
-
// 合并包 miaoda/miaoda-modern/miaoda-design:cp <extractDir>/<business-line>/* 到 target
|
|
187
191
|
const sourceDir = node_path_1.default.join(fetched.extractDir, businessLine);
|
|
188
192
|
if (node_fs_1.default.existsSync(sourceDir)) {
|
|
189
193
|
for (const entry of node_fs_1.default.readdirSync(sourceDir)) {
|
|
190
|
-
|
|
191
|
-
const to = node_path_1.default.join(skillsRoot, entry);
|
|
192
|
-
sourceTop.add(entry);
|
|
193
|
-
node_fs_1.default.cpSync(from, to, { recursive: true });
|
|
194
|
-
syncedTop.push(entry);
|
|
194
|
+
addSource(entry, node_path_1.default.join(sourceDir, entry));
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
197
|
else {
|
|
198
198
|
(0, logger_1.debug)(`sandbox-skills: source dir ${sourceDir} not found in package (skipped)`);
|
|
199
199
|
}
|
|
200
|
-
// shared
|
|
201
|
-
// 那里业务线先 `cp -r`(:33-34)、shared 后 `cp -r`(:47),后写覆盖前写,
|
|
202
|
-
// 即同名时 **shared 赢**。当前包里两边无同名交集,但语义得跟老链路一致。
|
|
203
|
-
//
|
|
204
|
-
// 早期这里写成「`fs.existsSync(to)` 就 continue」,有两个问题:
|
|
205
|
-
// 1. 同名优先级反了(变成业务线赢);
|
|
206
|
-
// 2. 更严重 —— existsSync 分不清「本轮业务线刚写的」和「上一轮 shared 自己写的」,
|
|
207
|
-
// 于是 shared skill 首次落盘后就永久冻结,包里再更新也同步不到已有沙箱
|
|
208
|
-
// (沙箱磁盘跨会话保留,每次 update-skills 都会命中 existsSync)。
|
|
200
|
+
// shared 排在业务线之后 → 同名时 shared 赢(老 build.sh:47 后写覆盖前写)
|
|
209
201
|
const sharedDir = node_path_1.default.join(fetched.extractDir, 'shared');
|
|
210
202
|
if (node_fs_1.default.existsSync(sharedDir)) {
|
|
211
203
|
for (const entry of node_fs_1.default.readdirSync(sharedDir)) {
|
|
212
|
-
|
|
213
|
-
const to = node_path_1.default.join(skillsRoot, entry);
|
|
214
|
-
sourceTop.add(entry);
|
|
215
|
-
node_fs_1.default.cpSync(from, to, { recursive: true });
|
|
216
|
-
syncedTop.push(entry);
|
|
204
|
+
addSource(entry, node_path_1.default.join(sharedDir, entry));
|
|
217
205
|
}
|
|
218
206
|
}
|
|
219
207
|
}
|
|
220
208
|
else {
|
|
221
|
-
// openclaw
|
|
209
|
+
// openclaw 独立包:包内 skill 目录直接平铺(跳过 package.json 等元文件)
|
|
222
210
|
for (const entry of node_fs_1.default.readdirSync(fetched.extractDir)) {
|
|
223
211
|
if (entry === 'package.json' || entry === 'README.md' || entry === 'LICENSE')
|
|
224
212
|
continue;
|
|
225
213
|
const from = node_path_1.default.join(fetched.extractDir, entry);
|
|
226
|
-
|
|
227
|
-
if (!stat.isDirectory())
|
|
214
|
+
if (!node_fs_1.default.statSync(from).isDirectory())
|
|
228
215
|
continue;
|
|
229
|
-
|
|
230
|
-
|
|
216
|
+
addSource(entry, from);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// ---- 第二步:逐条目「整清再拷」----
|
|
220
|
+
//
|
|
221
|
+
// 整清而非 `cpSync` 覆盖:cpSync 是覆盖不是镜像,源包删掉某个 skill *内部* 的文件
|
|
222
|
+
// (如 `references/old.md`)时目标那份会永久残留。老脚本靠逐文件比对 + `rm` 达到镜像
|
|
223
|
+
// (update-skills.sh:196-215),这里用「目录重建」达到同样效果,实现上更简单。
|
|
224
|
+
//
|
|
225
|
+
// 清理粒度刻意收在**单个条目**而不是整个 skillsRoot:
|
|
226
|
+
// - `.keep` / `.config` / `.last-update` 是控制文件,整清会连它们一起删
|
|
227
|
+
// (`.config` 是沙箱级 tgzKey 覆盖,feida-ai `sandbox-updater.ts:30` 读它,删了版本钉死失效)
|
|
228
|
+
// - `steering/` 归 syncCodingSteering 管,本函数不该碰
|
|
229
|
+
// - 单条目重建是毫秒级、一次只影响一个 skill;整清则会出现「整棵 skills 为空」的窗口,
|
|
230
|
+
// 而 feida-ai 的 scanAllSkills 会话期按需扫盘,正好能撞上
|
|
231
|
+
for (const [entry, srcDirs] of plan) {
|
|
232
|
+
const to = node_path_1.default.join(skillsRoot, entry);
|
|
233
|
+
(0, file_ops_1.forceRemove)(to);
|
|
234
|
+
for (const from of srcDirs) {
|
|
231
235
|
node_fs_1.default.cpSync(from, to, { recursive: true });
|
|
232
|
-
syncedTop.push(entry);
|
|
233
236
|
}
|
|
237
|
+
syncedTop.push(entry);
|
|
234
238
|
}
|
|
235
239
|
// rsync 式收尾:删掉源包已不提供的顶层条目(对齐 update-skills.sh 的 deleted)。
|
|
236
240
|
// 保留集用 sourceTop(源包提供的全部)而非 syncedTop(本轮实际写入的)——原因见
|
package/package.json
CHANGED
|
@@ -63,13 +63,47 @@ function isStylelintTarget(filePath) {
|
|
|
63
63
|
return filePath.endsWith('.css');
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
const STYLELINT_CONFIG_FILES = [
|
|
67
|
+
'.stylelintrc',
|
|
68
|
+
'.stylelintrc.js',
|
|
69
|
+
'.stylelintrc.cjs',
|
|
70
|
+
'.stylelintrc.json',
|
|
71
|
+
'stylelint.config.js',
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 老模板(fullstack-nestjs-template 1.x 时代)的应用跑不了 stylelint,跑了就挂、把 pre-commit
|
|
76
|
+
* 卡死。两种形态都实测过:
|
|
77
|
+
*
|
|
78
|
+
* - 缺 scripts.stylelint → `npm error Missing script: "stylelint"`
|
|
79
|
+
* - 缺 .stylelintrc.* → `ConfigurationError: No configuration provided`
|
|
80
|
+
*
|
|
81
|
+
* 注意光判断 stylelint 装没装拦不住:@lark-apaas/fullstack-presets 的 dependencies 里有
|
|
82
|
+
* stylelint,只要 dep 了 presets 二进制就在。缺任一件就跳过这一步。
|
|
83
|
+
*/
|
|
84
|
+
function canRunStylelint() {
|
|
85
|
+
let pkg;
|
|
86
|
+
try {
|
|
87
|
+
pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
88
|
+
} catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!pkg.scripts || !pkg.scripts.stylelint) return false;
|
|
93
|
+
|
|
94
|
+
return STYLELINT_CONFIG_FILES.some(file => fs.existsSync(path.join(cwd, file)));
|
|
95
|
+
}
|
|
96
|
+
|
|
66
97
|
async function runDefaultLint() {
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
'npm run
|
|
71
|
-
|
|
72
|
-
|
|
98
|
+
const commands = ['npm run eslint', 'npm run type:check'];
|
|
99
|
+
|
|
100
|
+
if (canRunStylelint()) {
|
|
101
|
+
commands.push('npm run stylelint');
|
|
102
|
+
} else {
|
|
103
|
+
console.warn('[lint] Skip stylelint: missing scripts.stylelint or stylelint config');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const code = await runCommand(getBinName('npx'), ['concurrently', ...commands]);
|
|
73
107
|
process.exit(code);
|
|
74
108
|
}
|
|
75
109
|
|
|
@@ -107,7 +141,9 @@ async function runSelectiveLint(inputFiles) {
|
|
|
107
141
|
tasks.push(runCommand(getBinName('npx'), ['eslint', '--quiet', ...eslintFiles]));
|
|
108
142
|
}
|
|
109
143
|
|
|
110
|
-
if (stylelintFiles.length > 0) {
|
|
144
|
+
if (stylelintFiles.length > 0 && !canRunStylelint()) {
|
|
145
|
+
console.warn('[lint] Skip stylelint: missing scripts.stylelint or stylelint config');
|
|
146
|
+
} else if (stylelintFiles.length > 0) {
|
|
111
147
|
tasks.push(runCommand(getBinName('npx'), ['stylelint', '--quiet', ...stylelintFiles]));
|
|
112
148
|
}
|
|
113
149
|
|