@lark-apaas/miaoda-cli 0.1.39-alpha.ba612c9 → 0.1.39-alpha.fd5a5fe
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 +11 -31
- package/dist/cli/commands/skills/index.js +1 -1
- package/dist/cli/handlers/app/export.js +3 -3
- package/dist/cli/handlers/app/migrate.js +4 -1
- package/dist/cli/handlers/app/sync.js +2 -2
- package/dist/cli/handlers/db/sql.js +8 -6
- package/dist/cli/handlers/deploy/modern.js +3 -32
- package/dist/cli/handlers/plugin/plugin-local.js +5 -4
- package/dist/services/app/init/async-install.js +13 -2
- package/dist/services/app/init/install.js +4 -3
- package/dist/services/deploy/modern/atoms/build.js +3 -3
- package/dist/services/deploy/modern/atoms/local-release.js +0 -2
- package/dist/services/deploy/modern/atoms/tosutil.js +6 -8
- package/dist/services/deploy/modern/patch/actions.js +1 -7
- package/dist/services/deploy/modern/patch/index.js +1 -2
- package/dist/services/deploy/modern/pipelines/design-local.js +1 -6
- package/dist/services/deploy/modern/pipelines/local.js +1 -6
- package/dist/utils/archive.js +103 -0
- package/dist/utils/coding-steering.js +30 -2
- package/dist/utils/file-ops.js +28 -8
- package/dist/utils/npm-pack.js +6 -7
- package/dist/utils/run-npm.js +102 -0
- package/dist/utils/which.js +27 -0
- package/package.json +6 -2
|
@@ -18,14 +18,7 @@ function registerDeployCommandsModern(program) {
|
|
|
18
18
|
.option('--dir <path>', '项目目录', '.')
|
|
19
19
|
.option('--skip-build', '跳过 build 步骤(已构建好时使用)', false)
|
|
20
20
|
.option('--skip-release', '不创建/更新本地发布单,发布单状态由外部托管', false)
|
|
21
|
-
.option('--conf <json>', '关联元信息 JSON,仅识别 checkPointVersion / commitID
|
|
22
|
-
// 这三个 flag 只在父命令声明一份,deploy 与 deploy patch 共用。
|
|
23
|
-
// 不能在 patch 上再声明一遍:commander 遇到父子同名 option 会把值判给父命令,
|
|
24
|
-
// 子命令拿到空数组,`deploy patch --create x` 会静默失效(deploy-patch-wiring 测试可复现)。
|
|
25
|
-
// patch 从 deployCmd.opts() 读,与它今天读 --dir 的方式一致。
|
|
26
|
-
.option('--create <relpath>', '新增文件(可重复)', shared_1.collectRepeatedOption, [])
|
|
27
|
-
.option('--update <relpath>', '修改文件(可重复)', shared_1.collectRepeatedOption, [])
|
|
28
|
-
.option('--delete <relpath>', '删除文件(可重复)', shared_1.collectRepeatedOption, [])
|
|
21
|
+
.option('--conf <json>', '关联元信息 JSON,仅识别 checkPointVersion / commitID 两字段')
|
|
29
22
|
.addHelpText('after', `
|
|
30
23
|
不要用异步模式或后台模式调用 deploy,否则调用可能提前结束,Agent 会误判发布已完成。
|
|
31
24
|
|
|
@@ -48,24 +41,14 @@ function registerDeployCommandsModern(program) {
|
|
|
48
41
|
外部据 stdout 返回的 version / preReleaseID 自行建单,并据本命令的
|
|
49
42
|
退出码决定把发布单翻成 Finished 还是 Failed —— 失败态由外部负责,CLI 不再兜底。
|
|
50
43
|
该模式下 releaseID / url 输出为 null(CLI 未建单,无从得知)。
|
|
51
|
-
注意:--conf
|
|
44
|
+
注意:--conf 的 checkPointVersion / commitID 唯一消费者是 createLocalRelease,
|
|
52
45
|
本模式下不生效(静默忽略,不报错),需要关联请在外部建单时自行传递。
|
|
53
46
|
|
|
54
|
-
--create / --update / --delete(变更文件清单)
|
|
55
|
-
可重复,值为工程根相对路径(禁绝对路径与 "..")。deploy patch 消费它做增量发布;
|
|
56
|
-
在 deploy 下当前是预留口子——只校验路径并记一行日志,不改变上传行为:
|
|
57
|
-
deploy 仍然全量上传产物,latest 每次被重建为工作区的精确镜像(prune 清理远端多余对象),
|
|
58
|
-
以此保证 git 仓库与 TOS 资源不会不一致。
|
|
59
|
-
后续若实现增量上传,从这里取「要 PUT 哪些文件」即可,调用方无需改动;
|
|
60
|
-
删除仍交给 prune,因为 prune 比对的是完整本地清单,与上传了哪些文件无关。
|
|
61
|
-
|
|
62
47
|
--conf(关联元信息透传)
|
|
63
|
-
值为 JSON string
|
|
48
|
+
值为 JSON string,只识别两个字段,其余 key 忽略:
|
|
64
49
|
checkPointVersion 关联的 checkpoint 版本
|
|
65
50
|
commitID 关联的代码 commit ID
|
|
66
|
-
|
|
67
|
-
marker 本次发布的变更说明,落发布单 description
|
|
68
|
-
四字段均可选;非法 JSON / 非对象 / 字段值非字符串会报错。
|
|
51
|
+
两字段均可选;非法 JSON / 非对象 / 字段值非字符串会报错。
|
|
69
52
|
|
|
70
53
|
JSON 输出(stdout)
|
|
71
54
|
{"data": {"appId": "...", "version": <n>, "url": "...", "releaseID": "...", "preReleaseID": "..."}}
|
|
@@ -77,8 +60,6 @@ JSON 输出(stdout)
|
|
|
77
60
|
$ miaoda deploy --skip-build
|
|
78
61
|
$ miaoda deploy --skip-release
|
|
79
62
|
$ miaoda deploy --conf '{"checkPointVersion":"v3","commitID":"a1b2c3d"}'
|
|
80
|
-
$ miaoda deploy --conf '{"commitID":"a1b2c3d","sessionID":"<uuid>","marker":"编辑保存:修改 index.html"}'
|
|
81
|
-
$ miaoda deploy --update index.html --create assets/new.js --delete old.html
|
|
82
63
|
`);
|
|
83
64
|
deployCmd.action((0, shared_1.withHelp)(deployCmd, async (rawOpts) => {
|
|
84
65
|
await (0, modern_1.handleDeployModern)({
|
|
@@ -87,18 +68,17 @@ JSON 输出(stdout)
|
|
|
87
68
|
skipBuild: rawOpts.skipBuild,
|
|
88
69
|
skipRelease: rawOpts.skipRelease,
|
|
89
70
|
conf: rawOpts.conf,
|
|
90
|
-
creates: rawOpts.create,
|
|
91
|
-
updates: rawOpts.update,
|
|
92
|
-
deletes: rawOpts.delete,
|
|
93
71
|
});
|
|
94
72
|
}));
|
|
95
73
|
const patchCmd = deployCmd
|
|
96
74
|
.command('patch')
|
|
97
75
|
.description('design-html 增量发布:按文件 create/update/delete,.html 变动时同步 routes.json')
|
|
76
|
+
.option('--create <relpath>', '新增文件(可重复)', shared_1.collectRepeatedOption, [])
|
|
77
|
+
.option('--update <relpath>', '覆盖已有文件(可重复)', shared_1.collectRepeatedOption, [])
|
|
78
|
+
.option('--delete <relpath>', '删除文件(可重复)', shared_1.collectRepeatedOption, [])
|
|
98
79
|
.option('--session-id <id>', '会话 ID,发布时透传给下游(可选)')
|
|
99
80
|
.addHelpText('after', `
|
|
100
|
-
--dir
|
|
101
|
-
--dir 为项目目录,默认当前目录。
|
|
81
|
+
--dir 为项目目录(与 deploy 同名参数,默认当前目录)。
|
|
102
82
|
仅支持 design-html(design_local_deploy)。路径为工程根相对路径(= 服务端 latest 下 key),
|
|
103
83
|
禁绝对路径与 ".."。--create/--update 的本地文件必须存在。改动里含任意 .html(增 / 改 / 删)
|
|
104
84
|
时自动重算并随包更新 routes.json;纯非 .html 改动不动 routes.json。
|
|
@@ -118,9 +98,9 @@ JSON 输出
|
|
|
118
98
|
await (0, index_1.handleDeployPatch)({
|
|
119
99
|
appId: (0, shared_1.resolveAppId)({}),
|
|
120
100
|
dir: deployCmd.opts().dir ?? '.',
|
|
121
|
-
creates:
|
|
122
|
-
updates:
|
|
123
|
-
deletes:
|
|
101
|
+
creates: rawOpts.create,
|
|
102
|
+
updates: rawOpts.update,
|
|
103
|
+
deletes: rawOpts.delete,
|
|
124
104
|
sessionID: rawOpts.sessionId,
|
|
125
105
|
});
|
|
126
106
|
}));
|
|
@@ -53,7 +53,7 @@ function registerSkillsSync(parent) {
|
|
|
53
53
|
|
|
54
54
|
JSON 输出
|
|
55
55
|
{"data": {"stack": "...", "version": "...", "syncedSkills": [...], "techSynced": true|false,
|
|
56
|
-
"claudeSkillsLink": "created|updated|noop|conflict" // 仅 --local 时}}
|
|
56
|
+
"claudeSkillsLink": "created|updated|noop|conflict|unsupported" // 仅 --local 时}}
|
|
57
57
|
|
|
58
58
|
传 --type 时:
|
|
59
59
|
{"data": {"mode": "sandbox", "type": "...", "packageName": "...", "version": "...",
|
|
@@ -38,11 +38,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.UPGRADE_MANIFEST_FILENAME = exports.DEFAULT_EXPORT_OUT = void 0;
|
|
40
40
|
exports.handleAppExport = handleAppExport;
|
|
41
|
-
const node_child_process_1 = require("node:child_process");
|
|
42
41
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
43
42
|
const node_os_1 = __importDefault(require("node:os"));
|
|
44
43
|
const node_path_1 = __importDefault(require("node:path"));
|
|
45
44
|
const api = __importStar(require("../../../api/index"));
|
|
45
|
+
const archive_1 = require("../../../utils/archive");
|
|
46
46
|
const error_1 = require("../../../utils/error");
|
|
47
47
|
const output_1 = require("../../../utils/output");
|
|
48
48
|
const logger_1 = require("../../../utils/logger");
|
|
@@ -95,11 +95,11 @@ function extractArchive(archivePath, destDir, head) {
|
|
|
95
95
|
const isZip = head.length >= 4 && head[0] === 0x50 && head[1] === 0x4b;
|
|
96
96
|
const isGzip = head.length >= 2 && head[0] === 0x1f && head[1] === 0x8b;
|
|
97
97
|
if (isZip) {
|
|
98
|
-
(0,
|
|
98
|
+
(0, archive_1.extractZip)(archivePath, destDir);
|
|
99
99
|
return;
|
|
100
100
|
}
|
|
101
101
|
if (isGzip) {
|
|
102
|
-
(0,
|
|
102
|
+
(0, archive_1.extractTarGz)(archivePath, destDir);
|
|
103
103
|
return;
|
|
104
104
|
}
|
|
105
105
|
throw new error_1.AppError('EXPORT_ARCHIVE_INVALID', '无法识别归档格式(非 zip / tar.gz)', {
|
|
@@ -16,6 +16,7 @@ const error_1 = require("../../../utils/error");
|
|
|
16
16
|
const output_1 = require("../../../utils/output");
|
|
17
17
|
const env_1 = require("../../../utils/env");
|
|
18
18
|
const logger_1 = require("../../../utils/logger");
|
|
19
|
+
const run_npm_1 = require("../../../utils/run-npm");
|
|
19
20
|
/**
|
|
20
21
|
* miaoda app migrate --to <stack> [--from <stack>] [--dir <path>]
|
|
21
22
|
*
|
|
@@ -126,7 +127,7 @@ async function handleAppMigrate(opts) {
|
|
|
126
127
|
(0, logger_1.log)('migrate', `Running npm ${installArgs.join(' ')}...`);
|
|
127
128
|
let installError;
|
|
128
129
|
try {
|
|
129
|
-
(0,
|
|
130
|
+
(0, run_npm_1.execNpmSync)(installArgs, {
|
|
130
131
|
cwd: targetDir,
|
|
131
132
|
stdio: (0, output_1.isJsonMode)() ? ['ignore', 'ignore', 'inherit'] : 'inherit',
|
|
132
133
|
});
|
|
@@ -167,6 +168,8 @@ async function handleAppMigrate(opts) {
|
|
|
167
168
|
// - install 挂了不做 —— 新依赖不全, 杀旧 dev 后新 dev 起不来反而更糟,
|
|
168
169
|
// 留着旧进程让用户先处理 installError
|
|
169
170
|
// - 软失败:pkill 无匹配进程退出 1,catch 吞掉
|
|
171
|
+
// - pkill 是 POSIX-only,但整段被 isSandboxEnv() 包着(沙箱恒为 Linux),
|
|
172
|
+
// Windows 本地永远走不到这里,故不需要跨平台封装
|
|
170
173
|
let devRestarted = false;
|
|
171
174
|
if (installError === undefined && (0, env_1.isSandboxEnv)()) {
|
|
172
175
|
(0, logger_1.log)('migrate', '沙箱环境,重启 dev process(平台 supervisor 会自动拉起)...');
|
|
@@ -9,7 +9,6 @@ exports.runStackSync = runStackSync;
|
|
|
9
9
|
exports.summarizeSyncResults = summarizeSyncResults;
|
|
10
10
|
exports.snapshotManagedDepSpecs = snapshotManagedDepSpecs;
|
|
11
11
|
exports.diffManagedSpecs = diffManagedSpecs;
|
|
12
|
-
const node_child_process_1 = require("node:child_process");
|
|
13
12
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
14
13
|
const node_path_1 = __importDefault(require("node:path"));
|
|
15
14
|
const sync_configs_1 = require("../../../config/sync-configs");
|
|
@@ -19,6 +18,7 @@ const githooks_1 = require("../../../utils/githooks");
|
|
|
19
18
|
const logs_dir_1 = require("../../../utils/logs-dir");
|
|
20
19
|
const install_1 = require("../../../services/app/init/install");
|
|
21
20
|
const spark_meta_1 = require("../../../utils/spark-meta");
|
|
21
|
+
const run_npm_1 = require("../../../utils/run-npm");
|
|
22
22
|
const error_1 = require("../../../utils/error");
|
|
23
23
|
const output_1 = require("../../../utils/output");
|
|
24
24
|
const logger_1 = require("../../../utils/logger");
|
|
@@ -127,7 +127,7 @@ async function handleAppSync(opts) {
|
|
|
127
127
|
(0, logger_1.log)('sync', `Running npm ${installArgs.join(' ')}...`);
|
|
128
128
|
let installError;
|
|
129
129
|
try {
|
|
130
|
-
(0,
|
|
130
|
+
(0, run_npm_1.execNpmSync)(installArgs, {
|
|
131
131
|
cwd: targetDir,
|
|
132
132
|
stdio: (0, output_1.isJsonMode)() ? ['ignore', 'ignore', 'inherit'] : 'inherit',
|
|
133
133
|
});
|
|
@@ -47,7 +47,7 @@ const colors_1 = require("../../../utils/colors");
|
|
|
47
47
|
const fuzzy_match_1 = require("../../../utils/fuzzy-match");
|
|
48
48
|
const index_1 = require("../../../api/db/index");
|
|
49
49
|
const sql_keywords_1 = require("../../../api/db/sql-keywords");
|
|
50
|
-
const
|
|
50
|
+
const run_npm_1 = require("../../../utils/run-npm");
|
|
51
51
|
const node_fs_1 = require("node:fs");
|
|
52
52
|
const node_path_1 = __importDefault(require("node:path"));
|
|
53
53
|
/**
|
|
@@ -140,20 +140,22 @@ async function maybeSyncAgentSchema(results) {
|
|
|
140
140
|
(0, logger_1.debug)(`[db sql] DDL detected, running \`npm run gen:db-schema\` in ${projectRoot}`);
|
|
141
141
|
try {
|
|
142
142
|
await new Promise((resolve) => {
|
|
143
|
-
const proc = (0,
|
|
143
|
+
const proc = (0, run_npm_1.spawnNpm)(['run', 'gen:db-schema'], {
|
|
144
144
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
145
145
|
cwd: projectRoot,
|
|
146
146
|
});
|
|
147
147
|
// --verbose:实时透传到 stderr 看进度;默认完全静默丢弃。
|
|
148
148
|
// 任何路径下子进程输出都不会进入当前 CLI 的 stdout,--json 模式安全。
|
|
149
149
|
// 注:默认路径必须 attach 监听器,否则 pipe 缓冲区写满会反压子进程。
|
|
150
|
+
// `?.`:stdio 走数组形式时 node 类型上 stdout/stderr 可为 null。这里 fd1/fd2
|
|
151
|
+
// 显式给了 'pipe',实际恒非 null;真为 null 也意味着不是 pipe、不存在缓冲反压。
|
|
150
152
|
if (verbose) {
|
|
151
|
-
proc.stdout
|
|
152
|
-
proc.stderr
|
|
153
|
+
proc.stdout?.pipe(process.stderr);
|
|
154
|
+
proc.stderr?.pipe(process.stderr);
|
|
153
155
|
}
|
|
154
156
|
else {
|
|
155
|
-
proc.stdout
|
|
156
|
-
proc.stderr
|
|
157
|
+
proc.stdout?.on('data', drainChunk);
|
|
158
|
+
proc.stderr?.on('data', drainChunk);
|
|
157
159
|
}
|
|
158
160
|
let timedOut = false;
|
|
159
161
|
const timer = setTimeout(() => {
|
|
@@ -4,17 +4,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.parseDeployConf = parseDeployConf;
|
|
7
|
-
exports.parseChangedFiles = parseChangedFiles;
|
|
8
7
|
exports.handleDeployModern = handleDeployModern;
|
|
9
8
|
const node_path_1 = __importDefault(require("node:path"));
|
|
10
9
|
const output_1 = require("../../../utils/output");
|
|
11
10
|
const error_1 = require("../../../utils/error");
|
|
12
11
|
const index_1 = require("../../../services/deploy/modern/index");
|
|
13
|
-
const
|
|
14
|
-
const logger_1 = require("../../../utils/logger");
|
|
15
|
-
const CONF_EXPECTED = '期望形如 {"checkPointVersion":"...","commitID":"...","sessionID":"...","marker":"..."} 的 JSON 对象';
|
|
12
|
+
const CONF_EXPECTED = '期望形如 {"checkPointVersion":"...","commitID":"..."} 的 JSON 对象';
|
|
16
13
|
/**
|
|
17
|
-
* 解析 `--conf` JSON string,只取 checkPointVersion / commitID
|
|
14
|
+
* 解析 `--conf` JSON string,只取 checkPointVersion / commitID 两个白名单字段。
|
|
18
15
|
* - 未传 → 返回 {}(行为不变)。
|
|
19
16
|
* - 非法 JSON / 非 object / 字段值非 string → 抛 DEPLOY_CONF_INVALID。
|
|
20
17
|
* - 空串字段视为未提供(不进 body,保持可选语义);未知 key 忽略。
|
|
@@ -34,7 +31,7 @@ function parseDeployConf(raw) {
|
|
|
34
31
|
}
|
|
35
32
|
const source = parsed;
|
|
36
33
|
const conf = {};
|
|
37
|
-
for (const key of ['checkPointVersion', 'commitID'
|
|
34
|
+
for (const key of ['checkPointVersion', 'commitID']) {
|
|
38
35
|
const value = source[key];
|
|
39
36
|
if (value === undefined)
|
|
40
37
|
continue;
|
|
@@ -46,29 +43,6 @@ function parseDeployConf(raw) {
|
|
|
46
43
|
}
|
|
47
44
|
return conf;
|
|
48
45
|
}
|
|
49
|
-
/**
|
|
50
|
-
* 解析 `--create` / `--update` / `--delete`,校验路径后组成 PatchChangeSet。
|
|
51
|
-
*
|
|
52
|
-
* 复用 deploy patch 的 normalizeRel(同一套校验:禁绝对路径、禁 ".."、统一为 posix 相对路径),
|
|
53
|
-
* 保证两条链路对「变更文件」的接受范围完全一致,调用方从 patch 迁移不会遇到收紧或放宽。
|
|
54
|
-
*
|
|
55
|
-
* 当前是预留口子:deploy 仍然全量上传,这里只校验并记一行日志,不参与上传决策。
|
|
56
|
-
* 三个参数都没传 → 返回 undefined(与不传时行为完全一致,不产生日志)。
|
|
57
|
-
*/
|
|
58
|
-
function parseChangedFiles(opts) {
|
|
59
|
-
const cs = {
|
|
60
|
-
creates: (opts.creates ?? []).map((rel) => (0, index_2.normalizeRel)(rel)),
|
|
61
|
-
updates: (opts.updates ?? []).map((rel) => (0, index_2.normalizeRel)(rel)),
|
|
62
|
-
deletes: (opts.deletes ?? []).map((rel) => (0, index_2.normalizeRel)(rel)),
|
|
63
|
-
};
|
|
64
|
-
const total = cs.creates.length + cs.updates.length + cs.deletes.length;
|
|
65
|
-
if (total === 0)
|
|
66
|
-
return undefined;
|
|
67
|
-
(0, logger_1.log)('deploy', `收到变更文件提示 ${String(total)} 个(create=${String(cs.creates.length)} ` +
|
|
68
|
-
`update=${String(cs.updates.length)} delete=${String(cs.deletes.length)}):` +
|
|
69
|
-
'当前仍为全量上传,未启用增量');
|
|
70
|
-
return cs;
|
|
71
|
-
}
|
|
72
46
|
/**
|
|
73
47
|
* miaoda deploy(modern scene 专用,CLI 表面对齐 openclaw-cli)
|
|
74
48
|
*
|
|
@@ -84,9 +58,6 @@ async function handleDeployModern(opts) {
|
|
|
84
58
|
skipRelease: opts.skipRelease ?? false,
|
|
85
59
|
checkPointVersion: conf.checkPointVersion,
|
|
86
60
|
commitID: conf.commitID,
|
|
87
|
-
sessionID: conf.sessionID,
|
|
88
|
-
marker: conf.marker,
|
|
89
|
-
changedFiles: parseChangedFiles(opts),
|
|
90
61
|
});
|
|
91
62
|
(0, output_1.emit)({
|
|
92
63
|
data: {
|
|
@@ -56,10 +56,11 @@ exports.readAllCapabilities = readAllCapabilities;
|
|
|
56
56
|
exports.hydrateCapability = hydrateCapability;
|
|
57
57
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
58
58
|
const node_path_1 = __importDefault(require("node:path"));
|
|
59
|
-
const node_child_process_1 = require("node:child_process");
|
|
60
59
|
const node_module_1 = require("node:module");
|
|
60
|
+
const archive_1 = require("../../../utils/archive");
|
|
61
61
|
const error_1 = require("../../../utils/error");
|
|
62
62
|
const logger_1 = require("../../../utils/logger");
|
|
63
|
+
const run_npm_1 = require("../../../utils/run-npm");
|
|
63
64
|
// ── Project paths ──
|
|
64
65
|
function getProjectRoot() {
|
|
65
66
|
return process.cwd();
|
|
@@ -152,7 +153,7 @@ function extractTgzToNodeModules(tgzPath, pluginName) {
|
|
|
152
153
|
}
|
|
153
154
|
node_fs_1.default.mkdirSync(tempDir, { recursive: true });
|
|
154
155
|
try {
|
|
155
|
-
(0,
|
|
156
|
+
(0, archive_1.extractTarGz)(tgzPath, tempDir);
|
|
156
157
|
const extractedDir = node_path_1.default.join(tempDir, 'package');
|
|
157
158
|
if (node_fs_1.default.existsSync(extractedDir)) {
|
|
158
159
|
node_fs_1.default.renameSync(extractedDir, targetDir);
|
|
@@ -191,7 +192,7 @@ function installMissingDeps(deps) {
|
|
|
191
192
|
if (deps.length === 0)
|
|
192
193
|
return;
|
|
193
194
|
(0, logger_1.log)('plugin', `Installing missing dependencies: ${deps.join(', ')}`);
|
|
194
|
-
const result = (0,
|
|
195
|
+
const result = (0, run_npm_1.runNpmSync)(['install', ...deps, '--no-save', '--no-package-lock'], {
|
|
195
196
|
cwd: getProjectRoot(),
|
|
196
197
|
stdio: 'inherit',
|
|
197
198
|
});
|
|
@@ -205,7 +206,7 @@ function installMissingDeps(deps) {
|
|
|
205
206
|
}
|
|
206
207
|
}
|
|
207
208
|
function npmInstall(tgzPath) {
|
|
208
|
-
const result = (0,
|
|
209
|
+
const result = (0, run_npm_1.runNpmSync)(['install', tgzPath, '--no-save', '--no-package-lock', '--ignore-scripts'], { cwd: getProjectRoot(), stdio: 'inherit' });
|
|
209
210
|
if (result.error) {
|
|
210
211
|
throw new error_1.AppError('INTERNAL_NPM_FAILED', `npm install failed: ${result.error.message}`, {
|
|
211
212
|
next_actions: ['确认本机已安装 npm,可 --verbose 查看执行详情'],
|
|
@@ -11,13 +11,24 @@ const node_fs_1 = __importDefault(require("node:fs"));
|
|
|
11
11
|
const node_path_1 = __importDefault(require("node:path"));
|
|
12
12
|
const env_1 = require("../../../utils/env");
|
|
13
13
|
const install_1 = require("./install");
|
|
14
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* event marker 目录,写死 `/tmp/event`;写前 mkdir -p。沙箱里 code server 轮询此目录,
|
|
16
|
+
* 路径是与平台约定的契约,**不能换成 os.tmpdir()**。所有读写它的代码路径都被
|
|
17
|
+
* isSandboxEnv() 守着(沙箱恒为 Linux),非沙箱环境不会碰。
|
|
18
|
+
*/
|
|
15
19
|
const EVENT_DIR = '/tmp/event';
|
|
16
20
|
/** 安装成功 marker 名 */
|
|
17
21
|
const MARKER_READY = 'WORKSPACE_READY';
|
|
18
22
|
/** 安装失败 marker 名 */
|
|
19
23
|
const MARKER_FAILED = 'WORKSPACE_FAILED';
|
|
20
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* 后台 worker stdout/stderr 落盘路径,写死 `/tmp/...`。
|
|
26
|
+
*
|
|
27
|
+
* 刻意**不**换成 `os.tmpdir()`:这个路径已经是对外契约 —— 写在 async-install 设计
|
|
28
|
+
* spec 里、`app init` 的 help 文案里、失败 marker 的 `logPath` 字段里,用户与平台
|
|
29
|
+
* 都按它 cat 日志。Windows 上 Node 会把 `/tmp` 解析到当前盘符根下的 `\tmp\`,
|
|
30
|
+
* 目录可写、内容可读,功能不受影响,不值得为此破坏契约。
|
|
31
|
+
*/
|
|
21
32
|
exports.ASYNC_INSTALL_LOG = '/tmp/async_install_dep.std.log';
|
|
22
33
|
function nowIso() {
|
|
23
34
|
return new Date().toISOString();
|
|
@@ -5,12 +5,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.installDependencies = installDependencies;
|
|
7
7
|
exports.resolveNpmInstallRegistry = resolveNpmInstallRegistry;
|
|
8
|
-
const node_child_process_1 = require("node:child_process");
|
|
9
8
|
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
10
9
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
11
10
|
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
+
const archive_1 = require("../../../utils/archive");
|
|
12
12
|
const error_1 = require("../../../utils/error");
|
|
13
13
|
const logger_1 = require("../../../utils/logger");
|
|
14
|
+
const run_npm_1 = require("../../../utils/run-npm");
|
|
14
15
|
const DEP_CACHE_ENV = 'MIAODA_DEP_CACHE_DIR';
|
|
15
16
|
/**
|
|
16
17
|
* 依赖安装。
|
|
@@ -94,7 +95,7 @@ function md5File(filePath) {
|
|
|
94
95
|
}
|
|
95
96
|
function extractZip(zipPath, targetDir, stdio) {
|
|
96
97
|
try {
|
|
97
|
-
(0,
|
|
98
|
+
(0, archive_1.extractZip)(zipPath, targetDir, { stdio });
|
|
98
99
|
}
|
|
99
100
|
catch (err) {
|
|
100
101
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -129,5 +130,5 @@ function runNpmInstall(targetDir, stdio, extraPackages) {
|
|
|
129
130
|
args.push(...extraPackages);
|
|
130
131
|
}
|
|
131
132
|
(0, logger_1.log)('init', `npm ${args.join(' ')} in ${targetDir}...`);
|
|
132
|
-
(0,
|
|
133
|
+
(0, run_npm_1.execNpmSync)(args, { cwd: targetDir, stdio });
|
|
133
134
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.runBuild = runBuild;
|
|
4
|
-
const node_child_process_1 = require("node:child_process");
|
|
5
4
|
const error_1 = require("../../../../utils/error");
|
|
6
5
|
const logger_1 = require("../../../../utils/logger");
|
|
6
|
+
const run_npm_1 = require("../../../../utils/run-npm");
|
|
7
7
|
const protocol_1 = require("../protocol");
|
|
8
8
|
/**
|
|
9
9
|
* 给 RESOURCE_CDN_PREFIX 补 https:// 协议头:
|
|
@@ -33,7 +33,7 @@ function ensureHttpsScheme(v) {
|
|
|
33
33
|
* 是 runtime 同域相对路径(例如 /app/<appId>/runtime/api/v1/storage/object/<bucket>/),
|
|
34
34
|
* 不补 https scheme,原样注入。顶层 static_cdn_prefix 不再下发。
|
|
35
35
|
*
|
|
36
|
-
* build 失败抛 AppError(
|
|
36
|
+
* build 失败抛 AppError(execNpmSync 自身会 throw,捕获后包一层加错误码)。
|
|
37
37
|
*/
|
|
38
38
|
function runBuild(opts) {
|
|
39
39
|
const staticCred = (0, protocol_1.parsePaasStorageCredential)((0, protocol_1.requireDataKey)(opts.data, protocol_1.DataKey.OUTPUT_STATIC_PAAS_STORAGE_CREDENTIAL), protocol_1.DataKey.OUTPUT_STATIC_PAAS_STORAGE_CREDENTIAL);
|
|
@@ -47,7 +47,7 @@ function runBuild(opts) {
|
|
|
47
47
|
};
|
|
48
48
|
(0, logger_1.log)('deploy', 'Building...');
|
|
49
49
|
try {
|
|
50
|
-
(0,
|
|
50
|
+
(0, run_npm_1.execNpmSync)(['run', 'build'], {
|
|
51
51
|
cwd: opts.projectDir,
|
|
52
52
|
stdio: 'inherit',
|
|
53
53
|
env: buildEnv,
|
|
@@ -18,15 +18,13 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
18
18
|
const node_child_process_1 = require("node:child_process");
|
|
19
19
|
const error_1 = require("../../../../utils/error");
|
|
20
20
|
const logger_1 = require("../../../../utils/logger");
|
|
21
|
+
const which_1 = require("../../../../utils/which");
|
|
21
22
|
function resolveTosutilPath() {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
catch {
|
|
28
|
-
/* fallthrough */
|
|
29
|
-
}
|
|
23
|
+
// 用 whichSync 而不是 shell out `which`:Windows 上没有 `which`(对应 `where`),
|
|
24
|
+
// 且查 PATH 本身不需要子进程。
|
|
25
|
+
const resolved = (0, which_1.whichSync)('tosutil');
|
|
26
|
+
if (resolved !== undefined)
|
|
27
|
+
return resolved;
|
|
30
28
|
throw new error_1.AppError('DEPLOY_TOSUTIL_MISSING', 'tosutil is not installed or not in PATH. modern deploy requires sandbox preinstalled tosutil.');
|
|
31
29
|
}
|
|
32
30
|
function tosutilUploadFromTos(cred) {
|
|
@@ -3,7 +3,6 @@ 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.normalizeRel = normalizeRel;
|
|
7
6
|
exports.buildTosActions = buildTosActions;
|
|
8
7
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
8
|
const node_path_1 = __importDefault(require("node:path"));
|
|
@@ -11,12 +10,7 @@ const error_1 = require("../../../../utils/error");
|
|
|
11
10
|
const index_1 = require("../../../../api/deploy/index");
|
|
12
11
|
const content_1 = require("./content");
|
|
13
12
|
const routes_1 = require("./routes");
|
|
14
|
-
/**
|
|
15
|
-
* 校验相对路径:禁绝对路径、禁含 ".." 段;normalize 成 posix 相对。
|
|
16
|
-
*
|
|
17
|
-
* 导出供 `deploy` 的 `--create` / `--update` / `--delete` 复用同一套校验,
|
|
18
|
-
* 让两条链路对「变更文件」的接受范围一致,避免两处实现漂移。
|
|
19
|
-
*/
|
|
13
|
+
/** 校验相对路径:禁绝对路径、禁含 ".." 段;normalize 成 posix 相对。 */
|
|
20
14
|
function normalizeRel(rel) {
|
|
21
15
|
if (node_path_1.default.isAbsolute(rel) || rel.startsWith('/')) {
|
|
22
16
|
throw new error_1.AppError('ARGS_INVALID', `文件路径不能是绝对路径:${rel}`);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.buildTosActions = void 0;
|
|
4
4
|
exports.patchDesignDeploy = patchDesignDeploy;
|
|
5
5
|
const spark_meta_1 = require("../../../../utils/spark-meta");
|
|
6
6
|
const error_1 = require("../../../../utils/error");
|
|
@@ -10,7 +10,6 @@ const template_key_map_1 = require("../template-key-map");
|
|
|
10
10
|
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
|
-
Object.defineProperty(exports, "normalizeRel", { enumerable: true, get: function () { return actions_2.normalizeRel; } });
|
|
14
13
|
/**
|
|
15
14
|
* design-html 增量发布:读本地文件组 TosFileAction(改动含 .html 时带重算的
|
|
16
15
|
* routes.json),调后端 applyTosDiff 应用到 latest。scope 限 design_local_deploy。
|
|
@@ -28,12 +28,7 @@ async function designLocalPublishPipeline(opts) {
|
|
|
28
28
|
skipRelease: opts.skipRelease ?? false,
|
|
29
29
|
appId: ctx.appId,
|
|
30
30
|
version: pre.version,
|
|
31
|
-
extra: {
|
|
32
|
-
checkPointVersion: opts.checkPointVersion,
|
|
33
|
-
commitID: opts.commitID,
|
|
34
|
-
sessionID: opts.sessionID,
|
|
35
|
-
marker: opts.marker,
|
|
36
|
-
},
|
|
31
|
+
extra: { checkPointVersion: opts.checkPointVersion, commitID: opts.commitID },
|
|
37
32
|
});
|
|
38
33
|
try {
|
|
39
34
|
await (0, index_1.uploadDesignArtifacts)({ projectDir: ctx.projectDir, data });
|
|
@@ -45,12 +45,7 @@ async function localBuildLocalPublishPipeline(opts) {
|
|
|
45
45
|
skipRelease: opts.skipRelease ?? false,
|
|
46
46
|
appId: ctx.appId,
|
|
47
47
|
version: pre.version,
|
|
48
|
-
extra: {
|
|
49
|
-
checkPointVersion: opts.checkPointVersion,
|
|
50
|
-
commitID: opts.commitID,
|
|
51
|
-
sessionID: opts.sessionID,
|
|
52
|
-
marker: opts.marker,
|
|
53
|
-
},
|
|
48
|
+
extra: { checkPointVersion: opts.checkPointVersion, commitID: opts.commitID },
|
|
54
49
|
});
|
|
55
50
|
try {
|
|
56
51
|
(0, logger_1.log)('deploy', 'Uploading artifacts...');
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resetArchiveProbeCache = resetArchiveProbeCache;
|
|
4
|
+
exports.extractTarGz = extractTarGz;
|
|
5
|
+
exports.extractZip = extractZip;
|
|
6
|
+
const node_child_process_1 = require("node:child_process");
|
|
7
|
+
const error_1 = require("../utils/error");
|
|
8
|
+
const logger_1 = require("../utils/logger");
|
|
9
|
+
const which_1 = require("../utils/which");
|
|
10
|
+
/**
|
|
11
|
+
* 归档解压的跨平台入口 —— 仓库内所有解压必须走这里,禁止再裸写
|
|
12
|
+
* `execFileSync('unzip', ...)` / `execSync('tar -xzf ...')`。
|
|
13
|
+
*
|
|
14
|
+
* 平台差异:
|
|
15
|
+
* - `tar`:Linux(GNU tar)、macOS(bsdtar)、Windows 10 1803+(内置 bsdtar)都有,
|
|
16
|
+
* 且都支持 `-xzf` 解 .tar.gz,所以 tar.gz 一条路走到底。
|
|
17
|
+
* - `unzip`:Linux / macOS 常见,**Windows 完全没有**。但 bsdtar 能解 zip,
|
|
18
|
+
* 所以 zip 走 `unzip → tar` 两级探测:Linux 命中 unzip,Windows 命中 tar。
|
|
19
|
+
* (GNU tar 不支持 zip,所以顺序不能反 —— Linux 上必须优先 unzip。)
|
|
20
|
+
*
|
|
21
|
+
* 一律用 execFileSync 传参数数组、不过 shell:路径含空格 / 中文 / `&` 时,
|
|
22
|
+
* 拼字符串交给 cmd.exe 或 sh 重新分词的行为在各平台不一致,且是注入面。
|
|
23
|
+
*
|
|
24
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
25
|
+
* 为什么**不**换成纯 JS 解压库(adm-zip / yauzl / node-tar)—— 别再尝试了:
|
|
26
|
+
*
|
|
27
|
+
* `extractZip` 的主要用途是解依赖缓存 zip(`MIAODA_DEP_CACHE_DIR`),内容是整个
|
|
28
|
+
* `node_modules/`。实测对比同一个 zip:
|
|
29
|
+
*
|
|
30
|
+
* | | Unix mode | 软链 |
|
|
31
|
+
* | unzip | 755 保留 | 保留 |
|
|
32
|
+
* | adm-zip | 666(执行位丢) | 被当普通文件解开 |
|
|
33
|
+
*
|
|
34
|
+
* `node_modules/.bin/*` 既需要执行位、又大量是软链 —— 用 adm-zip 解出来的缓存,
|
|
35
|
+
* 沙箱里 `npm run dev` 会直接挂。yauzl 更低层,mode 得自己从
|
|
36
|
+
* `externalFileAttributes` 还原、软链也要自己判,等于把 unzip 重写一遍。
|
|
37
|
+
*
|
|
38
|
+
* node-tar 倒是保权限 + 软链(npm 自己用它装包),但装出来 2.9MB、连传递依赖
|
|
39
|
+
* +3.7MB —— 而本仓生产依赖总共才 1.7MB,翻 3 倍多。CLI 是 `npx -y` 拉起的
|
|
40
|
+
* (模板 `scripts/dev.sh` 每次本地启动都会跑 `app sync`),冷启动下载量直接可感。
|
|
41
|
+
* 外部 tar 在 Linux / macOS / Windows 10 1803+ 都有,不值得为此付这个代价。
|
|
42
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
43
|
+
*/
|
|
44
|
+
/** tar 可用性探测结果缓存(单次 CLI 执行期内 PATH 不变) */
|
|
45
|
+
let tarAvailable;
|
|
46
|
+
let unzipAvailable;
|
|
47
|
+
function hasTar() {
|
|
48
|
+
tarAvailable ??= (0, which_1.whichSync)('tar') !== undefined;
|
|
49
|
+
return tarAvailable;
|
|
50
|
+
}
|
|
51
|
+
function hasUnzip() {
|
|
52
|
+
unzipAvailable ??= (0, which_1.whichSync)('unzip') !== undefined;
|
|
53
|
+
return unzipAvailable;
|
|
54
|
+
}
|
|
55
|
+
/** 仅测试用:清掉可用性探测缓存 */
|
|
56
|
+
function resetArchiveProbeCache() {
|
|
57
|
+
tarAvailable = undefined;
|
|
58
|
+
unzipAvailable = undefined;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* 解 .tar.gz / .tgz 到 `destDir`(须已存在)。失败抛 `INTERNAL_EXTRACT_FAILED`。
|
|
62
|
+
*/
|
|
63
|
+
function extractTarGz(archivePath, destDir, options = {}) {
|
|
64
|
+
if (!hasTar()) {
|
|
65
|
+
throw new error_1.AppError('INTERNAL_EXTRACT_FAILED', `解压 ${archivePath} 需要 tar,但当前环境 PATH 中找不到`, {
|
|
66
|
+
next_actions: [
|
|
67
|
+
'Linux/macOS:安装 tar(通常已内置)',
|
|
68
|
+
'Windows:升级到 Windows 10 1803+(内置 tar),或安装 Git for Windows 并把其 usr/bin 加入 PATH',
|
|
69
|
+
],
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
(0, logger_1.debug)(`archive: tar -xzf ${archivePath} -C ${destDir}`);
|
|
73
|
+
(0, node_child_process_1.execFileSync)('tar', ['-xzf', archivePath, '-C', destDir], {
|
|
74
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
75
|
+
...options,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* 解 .zip 到 `destDir`(须已存在),同名文件覆盖。失败抛 `INTERNAL_EXTRACT_FAILED`。
|
|
80
|
+
*
|
|
81
|
+
* unzip 优先(Linux 上唯一可用项),退化到 bsdtar(Windows 上唯一可用项)。
|
|
82
|
+
*/
|
|
83
|
+
function extractZip(archivePath, destDir, options = {}) {
|
|
84
|
+
const stdio = ['ignore', 'pipe', 'pipe'];
|
|
85
|
+
if (hasUnzip()) {
|
|
86
|
+
(0, logger_1.debug)(`archive: unzip -q -o ${archivePath} -d ${destDir}`);
|
|
87
|
+
(0, node_child_process_1.execFileSync)('unzip', ['-q', '-o', archivePath, '-d', destDir], { stdio, ...options });
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (hasTar()) {
|
|
91
|
+
// bsdtar 按后缀自动识别 zip,故用 -xf 而非 -xzf(zip 不是 gzip 流)。
|
|
92
|
+
// 覆盖行为与 `unzip -o` 一致:tar 默认覆盖已存在文件。
|
|
93
|
+
(0, logger_1.debug)(`archive: tar -xf ${archivePath} -C ${destDir}(unzip 不可用,走 bsdtar)`);
|
|
94
|
+
(0, node_child_process_1.execFileSync)('tar', ['-xf', archivePath, '-C', destDir], { stdio, ...options });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
throw new error_1.AppError('INTERNAL_EXTRACT_FAILED', `解压 ${archivePath} 需要 unzip 或 tar,但当前环境 PATH 中两者都找不到`, {
|
|
98
|
+
next_actions: [
|
|
99
|
+
'Linux:apt-get install unzip / yum install unzip',
|
|
100
|
+
'Windows:升级到 Windows 10 1803+(内置 tar 可解 zip)',
|
|
101
|
+
],
|
|
102
|
+
});
|
|
103
|
+
}
|
|
@@ -237,7 +237,33 @@ function syncCodingSteering(opts) {
|
|
|
237
237
|
* - 已是软链但指错地方:删旧的,建新的(updated)
|
|
238
238
|
* - 已是普通目录 / 文件:不覆盖,警告并跳过(conflict)—— 用户可能手动放了别的 skills
|
|
239
239
|
* - 不存在:建新的(created)
|
|
240
|
+
*
|
|
241
|
+
* 软链建不出来时返回 unsupported 而不是抛错:Windows 上非管理员且未开「开发者模式」
|
|
242
|
+
* 时 symlinkSync 报 EPERM,这不该让整条 steering 同步(进而 app init / app sync)失败。
|
|
243
|
+
* 退化后果是 Claude Code 默认路径读不到 skills —— 功能降级但项目可用,且日志里给了
|
|
244
|
+
* 明确的开启方式。不做「复制代替软链」的兜底:那会让两份内容各自漂移,比读不到更糟。
|
|
245
|
+
*/
|
|
246
|
+
/**
|
|
247
|
+
* 建目录软链,失败只 warn 不抛。返回是否建成。
|
|
248
|
+
*
|
|
249
|
+
* 主要挡的是 Windows 的 EPERM(创建软链需要 SeCreateSymbolicLinkPrivilege,
|
|
250
|
+
* 默认只给管理员;开启「开发者模式」后普通用户也可以)。其它 errno 一样按降级处理 ——
|
|
251
|
+
* 这一步是锦上添花,不值得中断整次同步。
|
|
240
252
|
*/
|
|
253
|
+
function trySymlink(linkTarget, linkPath, logPrefix) {
|
|
254
|
+
try {
|
|
255
|
+
node_fs_1.default.symlinkSync(linkTarget, linkPath, 'dir');
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
catch (err) {
|
|
259
|
+
const code = err.code ?? 'UNKNOWN';
|
|
260
|
+
const hint = process.platform === 'win32'
|
|
261
|
+
? '(Windows 需以管理员运行,或在「设置 → 隐私和安全性 → 开发者选项」开启开发者模式)'
|
|
262
|
+
: '';
|
|
263
|
+
(0, logger_2.log)(logPrefix, ` ⚠ .claude/skills 软链创建失败 [${code}],跳过${hint};skills 仍在 .agents/skills 下可用`);
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
241
267
|
function ensureClaudeSkillsSymlink(targetDir, logPrefix) {
|
|
242
268
|
const linkPath = node_path_1.default.join(targetDir, '.claude', 'skills');
|
|
243
269
|
const linkTarget = node_path_1.default.join('..', '.agents', 'skills');
|
|
@@ -251,7 +277,8 @@ function ensureClaudeSkillsSymlink(targetDir, logPrefix) {
|
|
|
251
277
|
throw err;
|
|
252
278
|
}
|
|
253
279
|
if (existing === null) {
|
|
254
|
-
|
|
280
|
+
if (!trySymlink(linkTarget, linkPath, logPrefix))
|
|
281
|
+
return 'unsupported';
|
|
255
282
|
(0, logger_2.log)(logPrefix, ` ✓ .claude/skills → ${linkTarget} (symlink created)`);
|
|
256
283
|
return 'created';
|
|
257
284
|
}
|
|
@@ -261,7 +288,8 @@ function ensureClaudeSkillsSymlink(targetDir, logPrefix) {
|
|
|
261
288
|
return 'noop';
|
|
262
289
|
}
|
|
263
290
|
node_fs_1.default.unlinkSync(linkPath);
|
|
264
|
-
|
|
291
|
+
if (!trySymlink(linkTarget, linkPath, logPrefix))
|
|
292
|
+
return 'unsupported';
|
|
265
293
|
(0, logger_2.log)(logPrefix, ` ✓ .claude/skills → ${linkTarget} (symlink updated, was → ${current})`);
|
|
266
294
|
return 'updated';
|
|
267
295
|
}
|
package/dist/utils/file-ops.js
CHANGED
|
@@ -63,7 +63,15 @@ function removeLineFromContent(content, pattern) {
|
|
|
63
63
|
// 不碰权限 —— 它跑在同一次调用里、紧随 syncSandboxSkills 之后,目录已经可写;
|
|
64
64
|
// 各写入方各自 chmod 会与收尾的 lockDirReadonly 形成隐式耦合,范围一旦不对称就会
|
|
65
65
|
// 出「跑一次 skills sync 把整个 skills 目录解封」这种事。
|
|
66
|
+
//
|
|
67
|
+
// **平台前提**:整套只读锁语义是 POSIX mode 位的,靠 shell `chmod -R` 实现。Windows
|
|
68
|
+
// 没有对应概念(NTFS 用 ACL,mode 位不映射),所以那里直接跳过加解锁 —— 锁和解锁
|
|
69
|
+
// 成对失效,语义仍自洽:目录始终可写,只是少了「防手改」这层保护。这是有意的降级,
|
|
70
|
+
// 不是遗漏;不用 fs.chmodSync 模拟,因为它在 Windows 上对目录基本是 no-op,
|
|
71
|
+
// 只会制造「以为锁上了」的假象。
|
|
66
72
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
73
|
+
/** POSIX mode 位语义是否可用;Windows 上整套只读锁降级为 no-op */
|
|
74
|
+
const SUPPORTS_POSIX_MODE = process.platform !== 'win32';
|
|
67
75
|
/**
|
|
68
76
|
* 解锁目录使其可写(`chmod -R u+w`)。
|
|
69
77
|
*
|
|
@@ -71,8 +79,12 @@ function removeLineFromContent(content, pattern) {
|
|
|
71
79
|
* 只加 owner 写位、不动 g/o,与老链路一致。目录不存在直接返回 false(调用方
|
|
72
80
|
* 通常紧接着 mkdir)。失败只 debug 不抛:解锁是尽力而为,真写不进去让后续
|
|
73
81
|
* 写操作自己报错,错误信息更具体。
|
|
82
|
+
*
|
|
83
|
+
* Windows 返回 false(没上过锁,也就无需解锁)。
|
|
74
84
|
*/
|
|
75
85
|
function unlockDirForWrite(dir) {
|
|
86
|
+
if (!SUPPORTS_POSIX_MODE)
|
|
87
|
+
return false;
|
|
76
88
|
if (!node_fs_1.default.existsSync(dir))
|
|
77
89
|
return false;
|
|
78
90
|
try {
|
|
@@ -92,8 +104,12 @@ function unlockDirForWrite(dir) {
|
|
|
92
104
|
* {@link unlockDirForWrite}。
|
|
93
105
|
*
|
|
94
106
|
* **调用时机必须是全部写入完成之后**——锁上就写不进去了。
|
|
107
|
+
*
|
|
108
|
+
* Windows 直接返回 false(不加锁,对应 unlockDirForWrite 也不解锁)。
|
|
95
109
|
*/
|
|
96
110
|
function lockDirReadonly(dir) {
|
|
111
|
+
if (!SUPPORTS_POSIX_MODE)
|
|
112
|
+
return false;
|
|
97
113
|
if (!node_fs_1.default.existsSync(dir))
|
|
98
114
|
return false;
|
|
99
115
|
try {
|
|
@@ -112,18 +128,22 @@ function lockDirReadonly(dir) {
|
|
|
112
128
|
* 只读锁下;另外老 update.sh 曾以 root 跑过,可能留下异主文件(如 root-owned 的
|
|
113
129
|
* `.last-update`),`rmSync{force:true}` 只忽略 ENOENT、遇 EACCES 仍抛。
|
|
114
130
|
* shell `rm -rf` 对异主文件 / 怪符号链接比 rmSync 宽容,故作主档。
|
|
131
|
+
*
|
|
132
|
+
* Windows 上没有 `rm`(也没有异主/只读锁那套前提),直接走 `fs.rmSync` 这一层。
|
|
115
133
|
*/
|
|
116
134
|
function forceRemove(target) {
|
|
117
135
|
if (!node_fs_1.default.existsSync(target))
|
|
118
136
|
return;
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
(
|
|
137
|
+
if (SUPPORTS_POSIX_MODE) {
|
|
138
|
+
unlockDirForWrite(target);
|
|
139
|
+
try {
|
|
140
|
+
(0, node_child_process_1.execFileSync)('rm', ['-rf', target], { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
141
|
+
if (!node_fs_1.default.existsSync(target))
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
(0, logger_1.debug)(`file-ops: rm -rf ${target} failed: ${err.message}`);
|
|
146
|
+
}
|
|
127
147
|
}
|
|
128
148
|
node_fs_1.default.rmSync(target, { recursive: true, force: true });
|
|
129
149
|
}
|
package/dist/utils/npm-pack.js
CHANGED
|
@@ -6,12 +6,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.pickLaneVersion = pickLaneVersion;
|
|
7
7
|
exports.extractLaneFromTgzKey = extractLaneFromTgzKey;
|
|
8
8
|
exports.fetchNpmPackage = fetchNpmPackage;
|
|
9
|
-
const node_child_process_1 = require("node:child_process");
|
|
10
9
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
11
10
|
const node_os_1 = __importDefault(require("node:os"));
|
|
12
11
|
const node_path_1 = __importDefault(require("node:path"));
|
|
12
|
+
const archive_1 = require("./archive");
|
|
13
13
|
const error_1 = require("./error");
|
|
14
14
|
const logger_1 = require("./logger");
|
|
15
|
+
const run_npm_1 = require("./run-npm");
|
|
15
16
|
const DEFAULT_REGISTRY = 'https://registry.npmmirror.com/';
|
|
16
17
|
/**
|
|
17
18
|
* 从 `npm view <pkg> versions --json` 里过滤出后缀 `-alpha.<X>` 的版本。
|
|
@@ -29,7 +30,7 @@ function pickLaneVersion(packageName, X, registry) {
|
|
|
29
30
|
const reg = registry ?? process.env.MIAODA_NPM_REGISTRY ?? DEFAULT_REGISTRY;
|
|
30
31
|
const suffix = `-alpha.${X}`;
|
|
31
32
|
try {
|
|
32
|
-
const stdout = (0,
|
|
33
|
+
const stdout = (0, run_npm_1.execNpmSyncCapture)(['view', packageName, 'versions', '--json', '--registry', reg], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
33
34
|
const versions = JSON.parse(stdout);
|
|
34
35
|
if (!Array.isArray(versions))
|
|
35
36
|
return null;
|
|
@@ -75,7 +76,7 @@ function extractLaneFromTgzKey(tgzKey) {
|
|
|
75
76
|
return lane;
|
|
76
77
|
}
|
|
77
78
|
/**
|
|
78
|
-
* `npm pack <pkg>@<ver> --registry <r>` +
|
|
79
|
+
* `npm pack <pkg>@<ver> --registry <r>` + 解压(走 archive 跨平台封装),解到临时目录。
|
|
79
80
|
* extractDir 是包内容根(含 package.json)。使用完必须 cleanup()。
|
|
80
81
|
*/
|
|
81
82
|
function fetchNpmPackage(opts) {
|
|
@@ -85,7 +86,7 @@ function fetchNpmPackage(opts) {
|
|
|
85
86
|
const tmpDir = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), 'miaoda-pack-'));
|
|
86
87
|
try {
|
|
87
88
|
(0, logger_1.debug)(`npm pack ${pkgSpec} --registry ${registry} → ${tmpDir}`);
|
|
88
|
-
const stdout = (0,
|
|
89
|
+
const stdout = (0, run_npm_1.execNpmSyncCapture)(['pack', pkgSpec, '--pack-destination', tmpDir, '--json', '--registry', registry], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
89
90
|
const packInfo = JSON.parse(stdout);
|
|
90
91
|
const tgzFilename = packInfo[0]?.filename;
|
|
91
92
|
const pkgVersion = packInfo[0]?.version ?? version;
|
|
@@ -95,9 +96,7 @@ function fetchNpmPackage(opts) {
|
|
|
95
96
|
const tgzPath = node_path_1.default.join(tmpDir, tgzFilename);
|
|
96
97
|
const extractDir = node_path_1.default.join(tmpDir, 'extracted');
|
|
97
98
|
node_fs_1.default.mkdirSync(extractDir, { recursive: true });
|
|
98
|
-
(0,
|
|
99
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
100
|
-
});
|
|
99
|
+
(0, archive_1.extractTarGz)(tgzPath, extractDir);
|
|
101
100
|
return {
|
|
102
101
|
extractDir: node_path_1.default.join(extractDir, 'package'),
|
|
103
102
|
version: pkgVersion,
|
|
@@ -0,0 +1,102 @@
|
|
|
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.execNpmSync = execNpmSync;
|
|
7
|
+
exports.execNpmSyncCapture = execNpmSyncCapture;
|
|
8
|
+
exports.runNpmSync = runNpmSync;
|
|
9
|
+
exports.spawnNpm = spawnNpm;
|
|
10
|
+
const cross_spawn_1 = __importDefault(require("cross-spawn"));
|
|
11
|
+
const error_1 = require("../utils/error");
|
|
12
|
+
const logger_1 = require("../utils/logger");
|
|
13
|
+
/**
|
|
14
|
+
* 跨平台 npm 执行入口 —— 仓库内**所有** npm 调用必须走这里,禁止再裸写
|
|
15
|
+
* `execFileSync('npm', ...)` / `spawnSync('npm', ...)` / `spawn('npm', ...)`,
|
|
16
|
+
* 也禁止直接 import `cross-spawn` 绕过这层(两条都由 ESLint 强制)。
|
|
17
|
+
*
|
|
18
|
+
* 为什么必须封装:Windows 上 npm 不是可执行文件而是 `npm.cmd` 批处理包装。
|
|
19
|
+
* - `execFile` / `spawn` 不带 shell 时按可执行文件解析,命中不到 `npm` → ENOENT;
|
|
20
|
+
* - 自 Node 18.20.2 / 20.12.2(CVE-2024-27980)起,不带 `shell: true` 执行
|
|
21
|
+
* `.cmd` / `.bat` 会直接抛 EINVAL,连"碰巧能跑"都不再成立。
|
|
22
|
+
*
|
|
23
|
+
* 为什么用 `cross-spawn` 而不是自己处理:正确的做法不只是"加 shell"。cross-spawn
|
|
24
|
+
* 在 Windows 上先判断目标是否真可执行文件,只在需要时切到 cmd.exe,并对参数做完整的
|
|
25
|
+
* 元字符转义(还专门处理了 `node_modules/.bin/*.cmd` 这类 cmd-shim 需要**双重**转义
|
|
26
|
+
* 的情况)。这些边角自己写必定漏,而它是 5000 万周下载量级的生态基建。
|
|
27
|
+
*
|
|
28
|
+
* 这层封装自身只做三件事:钉死命令名为 `npm`、把 spawn 结果的失败语义统一成
|
|
29
|
+
* `AppError`、给调用点一个稳定的收口位置。
|
|
30
|
+
*/
|
|
31
|
+
/** npm 子进程失败时统一抛的错误码 */
|
|
32
|
+
const NPM_FAILED_CODE = 'NPM_COMMAND_FAILED';
|
|
33
|
+
/** stderr 被 pipe 时,附进错误信息的尾部长度上限(避免把整篇 npm 日志灌进 message) */
|
|
34
|
+
const STDERR_TAIL_LIMIT = 800;
|
|
35
|
+
/**
|
|
36
|
+
* 取 stderr 尾部拼进错误信息。
|
|
37
|
+
*
|
|
38
|
+
* 参数刻意收窄成结构类型并显式带 `null`:node 的 `SpawnSyncReturns.stderr` 类型上是
|
|
39
|
+
* 非空的,但 `stdio: 'inherit' | 'ignore'` 时运行时实际为 null(输出已直出或丢弃)。
|
|
40
|
+
* 照类型写会漏掉这种情况,照运行时写又会被 no-unnecessary-condition 判成多余判断 ——
|
|
41
|
+
* 所以在这里把真实契约写清楚。
|
|
42
|
+
*/
|
|
43
|
+
function stderrTail(result) {
|
|
44
|
+
const raw = result.stderr;
|
|
45
|
+
if (raw === null || raw === undefined)
|
|
46
|
+
return '';
|
|
47
|
+
const text = typeof raw === 'string' ? raw : raw.toString('utf-8');
|
|
48
|
+
const trimmed = text.trim();
|
|
49
|
+
if (trimmed === '')
|
|
50
|
+
return '';
|
|
51
|
+
const tail = trimmed.length > STDERR_TAIL_LIMIT ? trimmed.slice(-STDERR_TAIL_LIMIT) : trimmed;
|
|
52
|
+
return `\n${tail}`;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 把 spawnSync 结果里的失败翻译成 AppError。
|
|
56
|
+
*
|
|
57
|
+
* 两类失败要分开报,因为处置动作不同:
|
|
58
|
+
* - `error` 非空 = 进程根本没起来(npm 不在 PATH、权限问题);
|
|
59
|
+
* - `status !== 0` = npm 起来了但命令失败(装包失败、脚本报错),此时 stderr 才有内容。
|
|
60
|
+
*/
|
|
61
|
+
function assertNpmOk(args, result) {
|
|
62
|
+
if (result.error) {
|
|
63
|
+
throw new error_1.AppError(NPM_FAILED_CODE, `npm 启动失败(npm ${args.join(' ')}):${result.error.message}`, {
|
|
64
|
+
next_actions: ['确认本机已安装 npm 且在 PATH 中', '可加 --verbose 查看执行详情'],
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
if (result.status !== 0) {
|
|
68
|
+
const code = result.status === null ? `signal ${String(result.signal)}` : `exit ${String(result.status)}`;
|
|
69
|
+
throw new error_1.AppError(NPM_FAILED_CODE, `npm ${args.join(' ')} 失败(${code})${stderrTail(result)}`, { next_actions: ['检查上方 npm 输出定位具体错误'] });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* 同步跑 npm,失败抛 `NPM_COMMAND_FAILED`(对齐原先 `execFileSync('npm', args)` 的
|
|
74
|
+
* "失败即抛"语义)。`stdio` 等选项原样透传。
|
|
75
|
+
*/
|
|
76
|
+
function execNpmSync(args, options = {}) {
|
|
77
|
+
(0, logger_1.debug)(`run-npm: npm ${args.join(' ')}`);
|
|
78
|
+
const result = cross_spawn_1.default.sync('npm', args, options);
|
|
79
|
+
assertNpmOk(args, result);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* 同步跑 npm 并拿回 stdout 字符串,失败抛 `NPM_COMMAND_FAILED`。
|
|
83
|
+
* 用于需要解析输出的场景(如 `npm pack --json`)。
|
|
84
|
+
*/
|
|
85
|
+
function execNpmSyncCapture(args, options = {}) {
|
|
86
|
+
(0, logger_1.debug)(`run-npm: npm ${args.join(' ')} (capture)`);
|
|
87
|
+
const result = cross_spawn_1.default.sync('npm', args, { ...options, encoding: 'utf-8' });
|
|
88
|
+
assertNpmOk(args, result);
|
|
89
|
+
return result.stdout;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* 同步跑 npm,**不抛错**,返回 spawnSync 结果(对齐 `spawnSync('npm', args)` 的语义)。
|
|
93
|
+
* 调用方自行判 `error` / `status` —— 用于需要把失败当正常分支处理的场景。
|
|
94
|
+
*/
|
|
95
|
+
function runNpmSync(args, options = {}) {
|
|
96
|
+
(0, logger_1.debug)(`run-npm: npm ${args.join(' ')} (no-throw)`);
|
|
97
|
+
return cross_spawn_1.default.sync('npm', args, options);
|
|
98
|
+
}
|
|
99
|
+
function spawnNpm(args, options = {}) {
|
|
100
|
+
(0, logger_1.debug)(`run-npm: npm ${args.join(' ')} (async)`);
|
|
101
|
+
return (0, cross_spawn_1.default)('npm', args, options);
|
|
102
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
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.whichSync = whichSync;
|
|
7
|
+
const which_1 = __importDefault(require("which"));
|
|
8
|
+
/**
|
|
9
|
+
* 跨平台可执行文件查找 —— 仓库内查 PATH 的唯一入口,禁止 shell out `which`。
|
|
10
|
+
*
|
|
11
|
+
* 为什么不 shell out:`which` 在 Windows 上不存在(对应的是 `where`),而 `where`
|
|
12
|
+
* 的输出格式与退出码语义又跟 `which` 不同。
|
|
13
|
+
*
|
|
14
|
+
* 为什么用社区 `which` 包而不是自己遍历 PATH:Windows 要按 PATHEXT 逐个后缀试探
|
|
15
|
+
* (`npm` 实际是 `npm.cmd`),POSIX 要看执行位,还有 all / delimiter 这些边角。
|
|
16
|
+
* 这些是 npm 官方维护的 `which` 已经做完并被全生态验证过的事,自己写只会漏。
|
|
17
|
+
*
|
|
18
|
+
* 保留这层薄封装而不是让各处直接 `import which`,是为了:
|
|
19
|
+
* 1. 把 `null`(社区库的「没找到」)归一成仓库内统一的 `undefined` optional 语义;
|
|
20
|
+
* 2. 留一个收口点 —— 换实现、加缓存、加平台特例都只改这里;
|
|
21
|
+
* 3. 配合 ESLint no-restricted-imports,保证不会有人绕过去直接用。
|
|
22
|
+
*/
|
|
23
|
+
function whichSync(name) {
|
|
24
|
+
if (name === '')
|
|
25
|
+
return undefined;
|
|
26
|
+
return which_1.default.sync(name, { nothrow: true }) ?? undefined;
|
|
27
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lark-apaas/miaoda-cli",
|
|
3
|
-
"version": "0.1.39-alpha.
|
|
3
|
+
"version": "0.1.39-alpha.fd5a5fe",
|
|
4
4
|
"description": "Miaoda 平台命令行工具,面向 Agent 调用",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"bin": {
|
|
@@ -29,12 +29,16 @@
|
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@lark-apaas/http-client": "^0.1.5",
|
|
31
31
|
"commander": "^13.1.0",
|
|
32
|
+
"cross-spawn": "^7.0.6",
|
|
32
33
|
"jsonc-parser": "^3.3.1",
|
|
33
34
|
"ora": "^5.4.1",
|
|
34
|
-
"picocolors": "^1.1.1"
|
|
35
|
+
"picocolors": "^1.1.1",
|
|
36
|
+
"which": "^7.0.0"
|
|
35
37
|
},
|
|
36
38
|
"devDependencies": {
|
|
39
|
+
"@types/cross-spawn": "^6.0.6",
|
|
37
40
|
"@types/node": "^22.15.3",
|
|
41
|
+
"@types/which": "^3.0.4",
|
|
38
42
|
"@typescript-eslint/eslint-plugin": "^8.58.2",
|
|
39
43
|
"@typescript-eslint/parser": "^8.58.2",
|
|
40
44
|
"@vitest/coverage-v8": "^4.1.4",
|