@lark-apaas/miaoda-cli 0.1.22-alpha.2ecdffe → 0.1.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -36
- package/dist/api/deploy/index.js +1 -3
- package/dist/api/deploy/modern-types.js +1 -4
- package/dist/api/deploy/modern.js +0 -8
- package/dist/api/index.js +1 -3
- package/dist/cli/commands/app/index.js +2 -4
- package/dist/cli/commands/deploy/modern.js +0 -34
- package/dist/cli/commands/index.js +0 -19
- package/dist/cli/handlers/app/init.js +2 -6
- package/dist/cli/handlers/deploy/index.js +1 -3
- package/dist/cli/handlers/skills/sync.js +4 -12
- package/dist/services/app/init/template.js +0 -1
- package/dist/services/deploy/modern/atoms/index.js +1 -6
- package/dist/services/deploy/modern/atoms/upload.js +127 -4
- package/dist/services/deploy/modern/check.js +16 -28
- package/dist/services/deploy/modern/pipelines/index.js +1 -3
- package/dist/services/deploy/modern/protocol.js +0 -7
- package/dist/services/deploy/modern/run.js +4 -10
- package/dist/services/deploy/modern/template-key-map.js +0 -4
- package/dist/utils/http.js +1 -1
- package/package.json +1 -1
- package/dist/api/comment/api.js +0 -24
- package/dist/api/comment/index.js +0 -6
- package/dist/api/comment/types.js +0 -6
- package/dist/cli/commands/comment/index.js +0 -55
- package/dist/cli/commands/registry/index.js +0 -114
- package/dist/cli/handlers/comment/index.js +0 -7
- package/dist/cli/handlers/comment/list.js +0 -132
- package/dist/cli/handlers/comment/resolve.js +0 -44
- package/dist/cli/handlers/deploy/patch.js +0 -17
- package/dist/cli/handlers/registry/add.js +0 -97
- package/dist/cli/handlers/registry/index.js +0 -7
- package/dist/cli/handlers/registry/list.js +0 -48
- package/dist/cli/handlers/registry/shared.js +0 -25
- package/dist/services/deploy/modern/atoms/design-build.js +0 -49
- package/dist/services/deploy/modern/atoms/design-upload.js +0 -74
- package/dist/services/deploy/modern/atoms/tosutil.js +0 -246
- package/dist/services/deploy/modern/patch/actions.js +0 -61
- package/dist/services/deploy/modern/patch/content.js +0 -18
- package/dist/services/deploy/modern/patch/index.js +0 -46
- package/dist/services/deploy/modern/patch/routes.js +0 -37
- package/dist/services/deploy/modern/patch/source-scan.js +0 -58
- package/dist/services/deploy/modern/pipelines/design-local.js +0 -56
- package/dist/services/registry/index.js +0 -293
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.resolveRegistryStack = resolveRegistryStack;
|
|
4
|
-
const spark_meta_1 = require("../../../utils/spark-meta");
|
|
5
|
-
const index_1 = require("../../../services/registry/index");
|
|
6
|
-
const error_1 = require("../../../utils/error");
|
|
7
|
-
/**
|
|
8
|
-
* 从 <targetDir>/.spark/meta.json 读 stack,校验该 stack 有组件 registry。
|
|
9
|
-
* 缺 stack → 提示先 init;stack 无 registry(无对应 runtime / runtime 无 registry 包)→ 提示支持哪些 stack。
|
|
10
|
-
*
|
|
11
|
-
* 这里只做「stack 是否在支持列表」的早校验给用户友好提示;包名的两跳解析
|
|
12
|
-
* (stack → runtime → 包)由 loadRegistry/resolveRegistryPackage 负责。
|
|
13
|
-
*/
|
|
14
|
-
function resolveRegistryStack(targetDir) {
|
|
15
|
-
const meta = (0, spark_meta_1.readSparkMeta)(targetDir);
|
|
16
|
-
if (meta.stack === undefined || meta.stack === '') {
|
|
17
|
-
throw new error_1.AppError('REGISTRY_META_INCOMPLETE', '.spark/meta.json missing stack — run `miaoda app init` first');
|
|
18
|
-
}
|
|
19
|
-
if (!index_1.REGISTRY_SUPPORTED_STACKS.includes(meta.stack)) {
|
|
20
|
-
throw new error_1.AppError('REGISTRY_STACK_UNSUPPORTED', `stack "${meta.stack}" 没有组件 registry`, {
|
|
21
|
-
next_actions: [`支持 registry 的 stack:${index_1.REGISTRY_SUPPORTED_STACKS.join(', ') || '(none)'}`],
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
return meta.stack;
|
|
25
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
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.buildDesignOutput = buildDesignOutput;
|
|
7
|
-
exports.cleanDesignDist = cleanDesignDist;
|
|
8
|
-
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
-
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
-
const error_1 = require("../../../../utils/error");
|
|
11
|
-
const logger_1 = require("../../../../utils/logger");
|
|
12
|
-
const constants_1 = require("../constants");
|
|
13
|
-
const source_scan_1 = require("../patch/source-scan");
|
|
14
|
-
const routes_1 = require("../patch/routes");
|
|
15
|
-
/**
|
|
16
|
-
* design-html buildless 闭环 build(替代模板 scripts/build.sh):
|
|
17
|
-
* 1. rm -rf <projectDir>/dist
|
|
18
|
-
* 2. 拷源码 → dist/output(套 EXCLUDES,与 routes 同一份 listSourceFiles)
|
|
19
|
-
* 3. 写 dist/output/routes.json = generateRoutes(projectDir)
|
|
20
|
-
* 不跑 npm run build、不依赖技术栈内 build.sh。
|
|
21
|
-
* fs / scan / routes 任何失败统一包成 AppError('DEPLOY_BUILD_FAILED'),与旧 runDesignBuild 对齐。
|
|
22
|
-
*/
|
|
23
|
-
function buildDesignOutput(opts) {
|
|
24
|
-
(0, logger_1.log)('deploy', 'Building (design, CLI)...');
|
|
25
|
-
try {
|
|
26
|
-
const distDir = node_path_1.default.join(opts.projectDir, constants_1.DIST_DIR);
|
|
27
|
-
const outputDir = node_path_1.default.join(distDir, constants_1.OUTPUT_DIR);
|
|
28
|
-
node_fs_1.default.rmSync(distDir, { recursive: true, force: true });
|
|
29
|
-
node_fs_1.default.mkdirSync(outputDir, { recursive: true });
|
|
30
|
-
for (const rel of (0, source_scan_1.listSourceFiles)(opts.projectDir)) {
|
|
31
|
-
const dest = node_path_1.default.join(outputDir, rel);
|
|
32
|
-
node_fs_1.default.mkdirSync(node_path_1.default.dirname(dest), { recursive: true });
|
|
33
|
-
node_fs_1.default.copyFileSync(node_path_1.default.join(opts.projectDir, rel), dest);
|
|
34
|
-
}
|
|
35
|
-
node_fs_1.default.writeFileSync(node_path_1.default.join(outputDir, 'routes.json'), (0, routes_1.generateRoutes)(opts.projectDir), 'utf-8');
|
|
36
|
-
}
|
|
37
|
-
catch (err) {
|
|
38
|
-
if (err instanceof error_1.AppError)
|
|
39
|
-
throw err;
|
|
40
|
-
throw new error_1.AppError('DEPLOY_BUILD_FAILED', `design build failed: ${err.message}`);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* 删除 <projectDir>/dist(本地构建产物)。幂等:不存在也不报错(force)。
|
|
45
|
-
* design 部署收尾调用,部署动作结束后不在用户项目里留产物。
|
|
46
|
-
*/
|
|
47
|
-
function cleanDesignDist(projectDir) {
|
|
48
|
-
node_fs_1.default.rmSync(node_path_1.default.join(projectDir, constants_1.DIST_DIR), { recursive: true, force: true });
|
|
49
|
-
}
|
|
@@ -1,74 +0,0 @@
|
|
|
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.uploadDesignArtifacts = uploadDesignArtifacts;
|
|
7
|
-
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
-
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
-
const error_1 = require("../../../../utils/error");
|
|
10
|
-
const logger_1 = require("../../../../utils/logger");
|
|
11
|
-
const constants_1 = require("../constants");
|
|
12
|
-
const protocol_1 = require("../protocol");
|
|
13
|
-
const tosutil_1 = require("./tosutil");
|
|
14
|
-
/** 递归收集 dir 下相对路径(posix 分隔),与 -flat 上传后远端 key 对齐。 */
|
|
15
|
-
function listLocalRelKeys(dir) {
|
|
16
|
-
const out = new Set();
|
|
17
|
-
const walk = (cur, rel) => {
|
|
18
|
-
for (const entry of node_fs_1.default.readdirSync(cur, { withFileTypes: true })) {
|
|
19
|
-
const childRel = rel ? `${rel}/${entry.name}` : entry.name;
|
|
20
|
-
if (entry.isDirectory())
|
|
21
|
-
walk(node_path_1.default.join(cur, entry.name), childRel);
|
|
22
|
-
else
|
|
23
|
-
out.add(childRel);
|
|
24
|
-
}
|
|
25
|
-
};
|
|
26
|
-
walk(dir, '');
|
|
27
|
-
return out;
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* design 部署上传(省带宽):
|
|
31
|
-
* 1. 本地 dist/output ──cp(local→tos)──▶ output_backup_path/ (唯一一次上传)
|
|
32
|
-
* 2. output_backup_path ──cp(tos→tos,同凭证同桶)──▶ output_latest_path (服务端复制)
|
|
33
|
-
* 3. ls latest → 跟本地 diff → rm latest 里源端已无的对象 (精确镜像)
|
|
34
|
-
* design-html 只产 dist/output,无 output_resource / output_static / capability。
|
|
35
|
-
*/
|
|
36
|
-
async function uploadDesignArtifacts(opts) {
|
|
37
|
-
await Promise.resolve();
|
|
38
|
-
const tosutilPath = (0, tosutil_1.resolveTosutilPath)();
|
|
39
|
-
const outputDir = node_path_1.default.join(opts.projectDir, constants_1.DIST_DIR, constants_1.OUTPUT_DIR);
|
|
40
|
-
if (!node_fs_1.default.existsSync(outputDir)) {
|
|
41
|
-
throw new error_1.AppError('DEPLOY_NO_BUILD_OUTPUT', `Required directory missing: ${constants_1.DIST_DIR}/${constants_1.OUTPUT_DIR}`);
|
|
42
|
-
}
|
|
43
|
-
// 空产物防呆:本地一个文件都没有时,绝不继续(否则 latest 会被整体 prune 清空)。
|
|
44
|
-
const localKeys = listLocalRelKeys(outputDir);
|
|
45
|
-
if (localKeys.size === 0) {
|
|
46
|
-
throw new error_1.AppError('DEPLOY_UPLOAD_EMPTY', `No files under ${constants_1.DIST_DIR}/${constants_1.OUTPUT_DIR} to upload — check build output.`);
|
|
47
|
-
}
|
|
48
|
-
const cred = (0, protocol_1.parseTosUploadCredential)((0, protocol_1.requireDataKey)(opts.data, protocol_1.DataKey.OUTPUT_ALL_TOS_UPLOAD_CREDENTIAL), protocol_1.DataKey.OUTPUT_ALL_TOS_UPLOAD_CREDENTIAL);
|
|
49
|
-
const backupPath = (0, protocol_1.requireDataKey)(opts.data, protocol_1.DataKey.OUTPUT_BACKUP_PATH);
|
|
50
|
-
const latestPath = (0, protocol_1.requireDataKey)(opts.data, protocol_1.DataKey.OUTPUT_LATEST_PATH);
|
|
51
|
-
const base = (0, tosutil_1.tosutilUploadFromTos)(cred); // 鉴权 + bucket
|
|
52
|
-
// 1. 本地 → backup(version):唯一一次本地上传。
|
|
53
|
-
// -ddo(noDirObjects):不上传文件夹占位对象,避免 latest 残留 `src/` 这类空目录。
|
|
54
|
-
(0, logger_1.log)('deploy', `Uploading ${constants_1.OUTPUT_DIR} → ${backupPath}...`);
|
|
55
|
-
(0, tosutil_1.uploadDirWithCredential)(tosutilPath, outputDir, { ...base, prefix: backupPath }, 'output_backup', true);
|
|
56
|
-
// 2. backup → latest:服务端 tos→tos cp(不占带宽)
|
|
57
|
-
(0, logger_1.log)('deploy', `Server-side copy ${backupPath} → ${latestPath}...`);
|
|
58
|
-
(0, tosutil_1.copyTosToTos)(tosutilPath, base, backupPath, latestPath);
|
|
59
|
-
// 3. latest 精确镜像:删源端已不存在的对象。
|
|
60
|
-
// 安全阀:远端有对象但跟本地零重合时,判定为 key 形态/ls 解析不匹配,跳过 prune——
|
|
61
|
-
// 宁可漏删(latest 残留旧文件)也绝不误删整目录(数据丢失)。design-html 的 build.sh
|
|
62
|
-
// 必产稳定名的 routes.json / *.html,正常重合不会为空,零重合即异常信号。
|
|
63
|
-
const remoteKeys = (0, tosutil_1.listRemoteKeys)(tosutilPath, { ...base, prefix: latestPath });
|
|
64
|
-
const overlap = remoteKeys.filter((k) => localKeys.has(k));
|
|
65
|
-
if (remoteKeys.length > 0 && overlap.length === 0) {
|
|
66
|
-
(0, logger_1.log)('deploy', `⚠ latest 远端 ${String(remoteKeys.length)} 个对象与本地零重合,疑似 key 形态不匹配,跳过 prune(避免误删)`);
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
const staleKeys = remoteKeys.filter((k) => !localKeys.has(k));
|
|
70
|
-
if (staleKeys.length > 0) {
|
|
71
|
-
(0, logger_1.log)('deploy', `Pruning ${String(staleKeys.length)} stale object(s) from latest...`);
|
|
72
|
-
(0, tosutil_1.removeRemoteKeys)(tosutilPath, { ...base, prefix: latestPath }, staleKeys);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
@@ -1,246 +0,0 @@
|
|
|
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.resolveTosutilPath = resolveTosutilPath;
|
|
7
|
-
exports.tosutilUploadFromTos = tosutilUploadFromTos;
|
|
8
|
-
exports.writeTosutilConfig = writeTosutilConfig;
|
|
9
|
-
exports.streamTosutilOutput = streamTosutilOutput;
|
|
10
|
-
exports.normalizeTosDest = normalizeTosDest;
|
|
11
|
-
exports.uploadDirWithCredential = uploadDirWithCredential;
|
|
12
|
-
exports.copyTosToTos = copyTosToTos;
|
|
13
|
-
exports.listRemoteKeys = listRemoteKeys;
|
|
14
|
-
exports.removeRemoteKeys = removeRemoteKeys;
|
|
15
|
-
const node_fs_1 = __importDefault(require("node:fs"));
|
|
16
|
-
const node_os_1 = __importDefault(require("node:os"));
|
|
17
|
-
const node_path_1 = __importDefault(require("node:path"));
|
|
18
|
-
const node_child_process_1 = require("node:child_process");
|
|
19
|
-
const error_1 = require("../../../../utils/error");
|
|
20
|
-
const logger_1 = require("../../../../utils/logger");
|
|
21
|
-
function resolveTosutilPath() {
|
|
22
|
-
try {
|
|
23
|
-
const resolved = (0, node_child_process_1.execFileSync)('which', ['tosutil'], { encoding: 'utf-8' }).trim();
|
|
24
|
-
if (resolved)
|
|
25
|
-
return resolved;
|
|
26
|
-
}
|
|
27
|
-
catch {
|
|
28
|
-
/* fallthrough */
|
|
29
|
-
}
|
|
30
|
-
throw new error_1.AppError('DEPLOY_TOSUTIL_MISSING', 'tosutil is not installed or not in PATH. modern deploy requires sandbox preinstalled tosutil.');
|
|
31
|
-
}
|
|
32
|
-
function tosutilUploadFromTos(cred) {
|
|
33
|
-
return {
|
|
34
|
-
accessKeyID: cred.accessKeyID,
|
|
35
|
-
secretAccessKey: cred.secretAccessKey,
|
|
36
|
-
sessionToken: cred.sessionToken,
|
|
37
|
-
endpoint: cred.endpoint,
|
|
38
|
-
region: cred.region,
|
|
39
|
-
bucket: cred.bucket,
|
|
40
|
-
prefix: cred.prefix,
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* 写一个 tosutil 临时 config 文件,注入 STS 凭证 + endpoint + region。
|
|
45
|
-
* 返回 config 路径,调用方负责清理。
|
|
46
|
-
*/
|
|
47
|
-
function writeTosutilConfig(tosutilPath, cred, label) {
|
|
48
|
-
const confPath = node_path_1.default.join(node_os_1.default.tmpdir(), `.tosutilconfig-miaoda-${label}-${String(process.pid)}-${String(Date.now())}`);
|
|
49
|
-
node_fs_1.default.writeFileSync(confPath, '');
|
|
50
|
-
(0, node_child_process_1.execFileSync)(tosutilPath, [
|
|
51
|
-
'config',
|
|
52
|
-
'-conf',
|
|
53
|
-
confPath,
|
|
54
|
-
'-e',
|
|
55
|
-
cred.endpoint,
|
|
56
|
-
'-i',
|
|
57
|
-
cred.accessKeyID,
|
|
58
|
-
'-k',
|
|
59
|
-
cred.secretAccessKey,
|
|
60
|
-
'-t',
|
|
61
|
-
cred.sessionToken,
|
|
62
|
-
'-re',
|
|
63
|
-
cred.region,
|
|
64
|
-
], { stdio: 'pipe' });
|
|
65
|
-
return confPath;
|
|
66
|
-
}
|
|
67
|
-
/** 把 tosutil 进程的输出按行回放到 stderr,便于线上排查(沙箱里无法 attach 进程)。 */
|
|
68
|
-
function streamTosutilOutput(output) {
|
|
69
|
-
if (!output)
|
|
70
|
-
return;
|
|
71
|
-
for (const line of output.split('\n')) {
|
|
72
|
-
if (line.length === 0)
|
|
73
|
-
continue;
|
|
74
|
-
process.stderr.write(`[tosutil] ${line}\n`);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
function normalizeTosDest(bucket, prefix) {
|
|
78
|
-
const bucketPart = bucket.startsWith('tos://') ? bucket : `tos://${bucket}`;
|
|
79
|
-
const normalized = prefix.replace(/^\/+/, '');
|
|
80
|
-
return `${bucketPart.replace(/\/+$/, '')}/${normalized}`;
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* 用 tosutil 把目录批量上传到 tos://bucket/prefix(STS 凭证从 config 文件读取)。
|
|
84
|
-
*/
|
|
85
|
-
function uploadDirWithCredential(tosutilPath, sourceDir, cred, label,
|
|
86
|
-
/**
|
|
87
|
-
* 传 true 时加 tosutil `-ddo`:上传时不把文件夹本身作为单独对象上传,
|
|
88
|
-
* 避免在 TOS 留下 `src/` 这类零字节目录占位对象(删空目录下最后一个文件后“文件夹”仍残留)。
|
|
89
|
-
* 仅 design 链路传 true;client/nrf 上传保持原行为(不传)。
|
|
90
|
-
*/
|
|
91
|
-
noDirObjects = false) {
|
|
92
|
-
const entries = node_fs_1.default.readdirSync(sourceDir);
|
|
93
|
-
if (entries.length === 0) {
|
|
94
|
-
(0, logger_1.debug)(`upload: skip empty dir ${sourceDir}`);
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
const dest = normalizeTosDest(cred.bucket, cred.prefix);
|
|
98
|
-
(0, logger_1.debug)(`upload: ${sourceDir} -> ${dest} (${String(entries.length)} entries)`);
|
|
99
|
-
const confPath = writeTosutilConfig(tosutilPath, cred, label);
|
|
100
|
-
let output;
|
|
101
|
-
try {
|
|
102
|
-
output = (0, node_child_process_1.execFileSync)(tosutilPath, [
|
|
103
|
-
'cp',
|
|
104
|
-
sourceDir,
|
|
105
|
-
dest,
|
|
106
|
-
'-r',
|
|
107
|
-
'-flat',
|
|
108
|
-
'-j',
|
|
109
|
-
'5',
|
|
110
|
-
'-p',
|
|
111
|
-
'3',
|
|
112
|
-
'-ps',
|
|
113
|
-
'10485760',
|
|
114
|
-
'-f',
|
|
115
|
-
...(noDirObjects ? ['-ddo'] : []),
|
|
116
|
-
'-conf',
|
|
117
|
-
confPath,
|
|
118
|
-
], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
119
|
-
streamTosutilOutput(output);
|
|
120
|
-
}
|
|
121
|
-
catch (err) {
|
|
122
|
-
const e = err;
|
|
123
|
-
const stderr = (e.stderr ?? '').trim();
|
|
124
|
-
const stdout = (e.stdout ?? '').trim();
|
|
125
|
-
streamTosutilOutput(stdout);
|
|
126
|
-
streamTosutilOutput(stderr);
|
|
127
|
-
throw new error_1.AppError('DEPLOY_UPLOAD_FAILED', `tosutil cp exited with status ${String(e.status ?? -1)} for ${sourceDir}\n` +
|
|
128
|
-
` stderr: ${stderr || '(empty)'}\n` +
|
|
129
|
-
` stdout: ${stdout.slice(0, 500) || '(empty)'}`);
|
|
130
|
-
}
|
|
131
|
-
finally {
|
|
132
|
-
try {
|
|
133
|
-
node_fs_1.default.unlinkSync(confPath);
|
|
134
|
-
}
|
|
135
|
-
catch {
|
|
136
|
-
/* ignore */
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
const succeedMatch = /Succeed count is:\s*(\d+)/.exec(output);
|
|
140
|
-
const failedMatch = /Failed count is:\s*(\d+)/.exec(output);
|
|
141
|
-
const succeedCount = succeedMatch ? Number.parseInt(succeedMatch[1], 10) : 0;
|
|
142
|
-
const failedCount = failedMatch ? Number.parseInt(failedMatch[1], 10) : 0;
|
|
143
|
-
if (failedCount > 0) {
|
|
144
|
-
throw new error_1.AppError('DEPLOY_UPLOAD_FAILED', `Upload failed: ${String(failedCount)} files failed, ${String(succeedCount)} succeeded. source=${sourceDir}`);
|
|
145
|
-
}
|
|
146
|
-
if (succeedCount === 0) {
|
|
147
|
-
throw new error_1.AppError('DEPLOY_UPLOAD_EMPTY', `Upload failed: 0 files uploaded. source=${sourceDir}. Check tosutil credentials and bucket permissions.`);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
/** version→latest 同桶同凭证服务端复制:tosutil cp tos://src tos://dst -r -flat -f */
|
|
151
|
-
function copyTosToTos(tosutilPath, cred, srcPrefix, dstPrefix) {
|
|
152
|
-
const src = normalizeTosDest(cred.bucket, srcPrefix);
|
|
153
|
-
const dst = normalizeTosDest(cred.bucket, dstPrefix);
|
|
154
|
-
const confPath = writeTosutilConfig(tosutilPath, cred, 'tos2tos');
|
|
155
|
-
try {
|
|
156
|
-
const out = (0, node_child_process_1.execFileSync)(tosutilPath, ['cp', src, dst, '-r', '-flat', '-f', '-j', '5', '-conf', confPath], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
157
|
-
streamTosutilOutput(out);
|
|
158
|
-
}
|
|
159
|
-
catch (err) {
|
|
160
|
-
const e = err;
|
|
161
|
-
streamTosutilOutput((e.stdout ?? '').trim());
|
|
162
|
-
streamTosutilOutput((e.stderr ?? '').trim());
|
|
163
|
-
throw new error_1.AppError('DEPLOY_UPLOAD_FAILED', `tosutil cp(tos→tos) exited status ${String(e.status ?? -1)}: ${src} → ${dst}`);
|
|
164
|
-
}
|
|
165
|
-
finally {
|
|
166
|
-
try {
|
|
167
|
-
node_fs_1.default.unlinkSync(confPath);
|
|
168
|
-
}
|
|
169
|
-
catch {
|
|
170
|
-
/* ignore */
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
/** 列 tos://bucket/prefix 下所有对象,返回相对 prefix 的 key 集合(用 ls -s 精简格式,ls 默认递归)。 */
|
|
175
|
-
function listRemoteKeys(tosutilPath, cred) {
|
|
176
|
-
const base = normalizeTosDest(cred.bucket, cred.prefix); // tos://bucket/prefix
|
|
177
|
-
const confPath = writeTosutilConfig(tosutilPath, cred, 'ls');
|
|
178
|
-
let out;
|
|
179
|
-
try {
|
|
180
|
-
// 注意:tosutil ls 没有 -r(默认就递归,-d 才是只列一层),别套 cp/rm 的 -r。
|
|
181
|
-
out = (0, node_child_process_1.execFileSync)(tosutilPath, ['ls', base, '-s', '-conf', confPath], {
|
|
182
|
-
encoding: 'utf-8',
|
|
183
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
catch (err) {
|
|
187
|
-
const e = err;
|
|
188
|
-
// tosutil 把参数类错误打到 stdout,故 stderr/stdout 都带上,避免错误信息为空难排查。
|
|
189
|
-
const detail = [(e.stderr ?? '').trim(), (e.stdout ?? '').trim()].filter(Boolean).join(' | ');
|
|
190
|
-
throw new error_1.AppError('DEPLOY_UPLOAD_FAILED', `tosutil ls exited status ${String(e.status ?? -1)} for ${base}: ${detail || '(no output)'}`);
|
|
191
|
-
}
|
|
192
|
-
finally {
|
|
193
|
-
try {
|
|
194
|
-
node_fs_1.default.unlinkSync(confPath);
|
|
195
|
-
}
|
|
196
|
-
catch {
|
|
197
|
-
/* ignore */
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
const prefixUri = base.replace(/\/+$/, '') + '/';
|
|
201
|
-
const keys = [];
|
|
202
|
-
for (const line of out.split('\n')) {
|
|
203
|
-
const t = line.trim();
|
|
204
|
-
if (!t.startsWith(prefixUri))
|
|
205
|
-
continue;
|
|
206
|
-
const rel = t.slice(prefixUri.length);
|
|
207
|
-
// 含以 / 结尾的目录占位对象(如 `src/`):本地文件集永远不含此类 key,
|
|
208
|
-
// 故它们必落入镜像 prune 的“stale”集合被删除——清掉历史 -ddo 之前留下的空目录残留。
|
|
209
|
-
if (rel.length > 0)
|
|
210
|
-
keys.push(rel);
|
|
211
|
-
}
|
|
212
|
-
return keys;
|
|
213
|
-
}
|
|
214
|
-
/** 删除 tos://bucket/prefix 下指定相对 key(逐个 rm -f)。 */
|
|
215
|
-
function removeRemoteKeys(tosutilPath, cred, relKeys) {
|
|
216
|
-
if (relKeys.length === 0)
|
|
217
|
-
return;
|
|
218
|
-
const confPath = writeTosutilConfig(tosutilPath, cred, 'rm');
|
|
219
|
-
const prefix = cred.prefix.replace(/\/+$/, '');
|
|
220
|
-
const failed = [];
|
|
221
|
-
try {
|
|
222
|
-
for (const rel of relKeys) {
|
|
223
|
-
// 用与 cp/ls 同一份 normalizeTosDest 拼目标 URI,避免删除路径与其它操作漂移。
|
|
224
|
-
const dest = normalizeTosDest(cred.bucket, `${prefix}/${rel}`);
|
|
225
|
-
try {
|
|
226
|
-
(0, node_child_process_1.execFileSync)(tosutilPath, ['rm', dest, '-f', '-conf', confPath], {
|
|
227
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
catch {
|
|
231
|
-
failed.push(rel);
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
finally {
|
|
236
|
-
try {
|
|
237
|
-
node_fs_1.default.unlinkSync(confPath);
|
|
238
|
-
}
|
|
239
|
-
catch {
|
|
240
|
-
/* ignore */
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
if (failed.length > 0) {
|
|
244
|
-
throw new error_1.AppError('DEPLOY_UPLOAD_FAILED', `prune latest failed for: ${failed.join(', ')}`);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
@@ -1,61 +0,0 @@
|
|
|
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.buildTosActions = buildTosActions;
|
|
7
|
-
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
-
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
-
const error_1 = require("../../../../utils/error");
|
|
10
|
-
const index_1 = require("../../../../api/deploy/index");
|
|
11
|
-
const content_1 = require("./content");
|
|
12
|
-
const routes_1 = require("./routes");
|
|
13
|
-
/** 校验相对路径:禁绝对路径、禁含 ".." 段;normalize 成 posix 相对。 */
|
|
14
|
-
function normalizeRel(rel) {
|
|
15
|
-
if (node_path_1.default.isAbsolute(rel) || rel.startsWith('/')) {
|
|
16
|
-
throw new error_1.AppError('ARGS_INVALID', `文件路径不能是绝对路径:${rel}`);
|
|
17
|
-
}
|
|
18
|
-
const posix = rel.split(node_path_1.default.sep).join('/').replace(/^\.\//, '');
|
|
19
|
-
if (posix.split('/').includes('..')) {
|
|
20
|
-
throw new error_1.AppError('ARGS_INVALID', `文件路径不能包含 "..":${rel}`);
|
|
21
|
-
}
|
|
22
|
-
return posix;
|
|
23
|
-
}
|
|
24
|
-
function readAction(projectDir, rel, actionType) {
|
|
25
|
-
const filePath = normalizeRel(rel);
|
|
26
|
-
const abs = node_path_1.default.join(projectDir, filePath);
|
|
27
|
-
if (!node_fs_1.default.existsSync(abs) || !node_fs_1.default.statSync(abs).isFile()) {
|
|
28
|
-
throw new error_1.AppError('ARGS_INVALID', `文件不存在:${filePath}`);
|
|
29
|
-
}
|
|
30
|
-
const { content, base64Content } = (0, content_1.encodeContent)(node_fs_1.default.readFileSync(abs));
|
|
31
|
-
return { actionType, filePath, content, base64Content };
|
|
32
|
-
}
|
|
33
|
-
function isHtml(rel) {
|
|
34
|
-
return rel.toLowerCase().endsWith('.html');
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* 把 changeset 组成 TosFileAction[]:
|
|
38
|
-
* - create/update 读本地文件、自动编码;delete 仅 FilePath。
|
|
39
|
-
* - .html 被 create / delete(路由集合变化)时,追加重算 routes.json 的 UPDATE action;
|
|
40
|
-
* 纯 update 现有 .html 只改内容、不改路由集合,不重算 routes.json。
|
|
41
|
-
*/
|
|
42
|
-
function buildTosActions(projectDir, cs) {
|
|
43
|
-
const actions = [];
|
|
44
|
-
for (const rel of cs.creates)
|
|
45
|
-
actions.push(readAction(projectDir, rel, index_1.TosActionType.CREATE));
|
|
46
|
-
for (const rel of cs.updates)
|
|
47
|
-
actions.push(readAction(projectDir, rel, index_1.TosActionType.UPDATE));
|
|
48
|
-
for (const rel of cs.deletes) {
|
|
49
|
-
actions.push({ actionType: index_1.TosActionType.DELETE, filePath: normalizeRel(rel) });
|
|
50
|
-
}
|
|
51
|
-
const routeSetChanged = [...cs.creates, ...cs.deletes].some(isHtml);
|
|
52
|
-
if (routeSetChanged) {
|
|
53
|
-
actions.push({
|
|
54
|
-
actionType: index_1.TosActionType.UPDATE,
|
|
55
|
-
filePath: 'routes.json',
|
|
56
|
-
content: (0, routes_1.generateRoutes)(projectDir),
|
|
57
|
-
base64Content: false,
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
return actions;
|
|
61
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.encodeContent = encodeContent;
|
|
4
|
-
/**
|
|
5
|
-
* 自动判别文件内容编码:
|
|
6
|
-
* - 含 0x00 字节 → 二进制 → base64
|
|
7
|
-
* - 否则 round-trip 校验 UTF-8:能无损 decode→encode → 文本直传;否则 base64
|
|
8
|
-
*/
|
|
9
|
-
function encodeContent(buf) {
|
|
10
|
-
if (buf.includes(0x00)) {
|
|
11
|
-
return { content: buf.toString('base64'), base64Content: true };
|
|
12
|
-
}
|
|
13
|
-
const text = buf.toString('utf8');
|
|
14
|
-
if (Buffer.from(text, 'utf8').equals(buf)) {
|
|
15
|
-
return { content: text, base64Content: false };
|
|
16
|
-
}
|
|
17
|
-
return { content: buf.toString('base64'), base64Content: true };
|
|
18
|
-
}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.buildTosActions = void 0;
|
|
4
|
-
exports.patchDesignDeploy = patchDesignDeploy;
|
|
5
|
-
const spark_meta_1 = require("../../../../utils/spark-meta");
|
|
6
|
-
const error_1 = require("../../../../utils/error");
|
|
7
|
-
const logger_1 = require("../../../../utils/logger");
|
|
8
|
-
const index_1 = require("../../../../api/deploy/index");
|
|
9
|
-
const template_key_map_1 = require("../template-key-map");
|
|
10
|
-
const actions_1 = require("./actions");
|
|
11
|
-
var actions_2 = require("./actions");
|
|
12
|
-
Object.defineProperty(exports, "buildTosActions", { enumerable: true, get: function () { return actions_2.buildTosActions; } });
|
|
13
|
-
/**
|
|
14
|
-
* design-html 增量发布:读本地文件组 TosFileAction(create/delete .html 时带重算的
|
|
15
|
-
* routes.json),调后端 applyTosDiff 应用到 latest。scope 限 design_local_deploy。
|
|
16
|
-
*/
|
|
17
|
-
async function patchDesignDeploy(opts) {
|
|
18
|
-
if (opts.creates.length + opts.updates.length + opts.deletes.length === 0) {
|
|
19
|
-
throw new error_1.AppError('ARGS_INVALID', '至少指定一个 --create / --update / --delete');
|
|
20
|
-
}
|
|
21
|
-
const stack = (0, spark_meta_1.readSparkMeta)(opts.projectDir).stack ?? '';
|
|
22
|
-
const templateKey = (0, template_key_map_1.resolveTemplateKey)(stack);
|
|
23
|
-
if (templateKey !== template_key_map_1.DESIGN_LOCAL_DEPLOY) {
|
|
24
|
-
throw new error_1.AppError('ARGS_INVALID', `deploy patch 仅支持 design-html(design_local_deploy),当前 stack=${stack || '(空)'}`);
|
|
25
|
-
}
|
|
26
|
-
const actions = (0, actions_1.buildTosActions)(opts.projectDir, {
|
|
27
|
-
creates: opts.creates,
|
|
28
|
-
updates: opts.updates,
|
|
29
|
-
deletes: opts.deletes,
|
|
30
|
-
});
|
|
31
|
-
const routesRegenerated = actions.some((a) => a.filePath === 'routes.json');
|
|
32
|
-
(0, logger_1.log)('deploy', `Applying ${String(actions.length)} file action(s)...`);
|
|
33
|
-
const res = await (0, index_1.applyTosDiff)({
|
|
34
|
-
appID: opts.appId,
|
|
35
|
-
templateKey,
|
|
36
|
-
actions,
|
|
37
|
-
sessionId: opts.sessionId,
|
|
38
|
-
});
|
|
39
|
-
(0, logger_1.log)('deploy', `Patched: upsert=${String(res.upsertCount)} delete=${String(res.deleteCount)}`);
|
|
40
|
-
return {
|
|
41
|
-
upsertCount: res.upsertCount,
|
|
42
|
-
deleteCount: res.deleteCount,
|
|
43
|
-
actionsSent: actions.length,
|
|
44
|
-
routesRegenerated,
|
|
45
|
-
};
|
|
46
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.generateRoutes = generateRoutes;
|
|
4
|
-
const source_scan_1 = require("./source-scan");
|
|
5
|
-
/**
|
|
6
|
-
* .html → route path 推导(相对路径,不带 base 前缀)。rel 为 posix 相对路径。
|
|
7
|
-
* base 前缀交由消费侧(运行时/网关)自行拼接,design-html 链路不在 routes.json 内带前缀。
|
|
8
|
-
*/
|
|
9
|
-
function toRoute(rel) {
|
|
10
|
-
let r = '/' + rel;
|
|
11
|
-
r = r.replace(/\.html$/i, '');
|
|
12
|
-
r = r.replace(/(^|\/)index$/, '$1');
|
|
13
|
-
if (r.length > 1)
|
|
14
|
-
r = r.replace(/\/$/, '');
|
|
15
|
-
return r === '' ? '/' : r;
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* 扫描 projectDir 下所有 .html(套 source-scan 的 EXCLUDES),生成 routes.json 文本(含尾换行)。
|
|
19
|
-
* 每项为 { path, file }:path 为相对路径(不带 CLIENT_BASE_PATH 前缀,由消费侧拼接),
|
|
20
|
-
* file 为该路由对应的原始 .html 源文件。按 path 排序、同 path 取字典序靠前的 file 去重。
|
|
21
|
-
* 零 html 时输出空数组——不伪造解析不到文件的根路由。
|
|
22
|
-
*/
|
|
23
|
-
function generateRoutes(projectDir) {
|
|
24
|
-
const htmls = (0, source_scan_1.listSourceFiles)(projectDir).filter((f) => f.toLowerCase().endsWith('.html'));
|
|
25
|
-
const byPath = new Map();
|
|
26
|
-
for (const entry of htmls.map((file) => ({ path: toRoute(file), file })).sort(byPathThenFile)) {
|
|
27
|
-
if (!byPath.has(entry.path))
|
|
28
|
-
byPath.set(entry.path, entry);
|
|
29
|
-
}
|
|
30
|
-
const routes = [...byPath.values()];
|
|
31
|
-
return JSON.stringify(routes, null, 2) + '\n';
|
|
32
|
-
}
|
|
33
|
-
function byPathThenFile(a, b) {
|
|
34
|
-
return a.path === b.path
|
|
35
|
-
? (a.file ?? '').localeCompare(b.file ?? '')
|
|
36
|
-
: a.path.localeCompare(b.path);
|
|
37
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
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.EXCLUDES = void 0;
|
|
7
|
-
exports.listSourceFiles = listSourceFiles;
|
|
8
|
-
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
-
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
-
// 与模板 scripts/build.sh 的 rsync EXCLUDES 对齐:这些名字(任意层级 basename)不进产物/不计入路由。
|
|
11
|
-
exports.EXCLUDES = new Set([
|
|
12
|
-
'.git',
|
|
13
|
-
'node_modules',
|
|
14
|
-
'dist',
|
|
15
|
-
'scripts',
|
|
16
|
-
'package.json',
|
|
17
|
-
'package-lock.json',
|
|
18
|
-
'pnpm-lock.yaml',
|
|
19
|
-
'yarn.lock',
|
|
20
|
-
'.gitignore',
|
|
21
|
-
'.npmrc',
|
|
22
|
-
'.agent',
|
|
23
|
-
'.agents',
|
|
24
|
-
'skills',
|
|
25
|
-
'.env',
|
|
26
|
-
'.env.local',
|
|
27
|
-
'README.md',
|
|
28
|
-
'.DS_Store',
|
|
29
|
-
'.spark',
|
|
30
|
-
]);
|
|
31
|
-
/**
|
|
32
|
-
* 递归列出 root 下所有文件的相对 posix 路径,跳过 EXCLUDES(任意层级 basename),
|
|
33
|
-
* 用 statSync 判定目录/文件以 follow 符号链接(含指向目录的软链),断链等无法 stat 的条目跳过。
|
|
34
|
-
*/
|
|
35
|
-
function listSourceFiles(root) {
|
|
36
|
-
const out = [];
|
|
37
|
-
const walk = (dir, rel) => {
|
|
38
|
-
for (const entry of node_fs_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
39
|
-
if (exports.EXCLUDES.has(entry.name))
|
|
40
|
-
continue;
|
|
41
|
-
const childRel = rel ? `${rel}/${entry.name}` : entry.name;
|
|
42
|
-
const abs = node_path_1.default.join(dir, entry.name);
|
|
43
|
-
let isDir;
|
|
44
|
-
try {
|
|
45
|
-
isDir = node_fs_1.default.statSync(abs).isDirectory();
|
|
46
|
-
}
|
|
47
|
-
catch {
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
if (isDir)
|
|
51
|
-
walk(abs, childRel);
|
|
52
|
-
else
|
|
53
|
-
out.push(childRel);
|
|
54
|
-
}
|
|
55
|
-
};
|
|
56
|
-
walk(root, '');
|
|
57
|
-
return out;
|
|
58
|
-
}
|