@lark-apaas/miaoda-cli 0.1.5 → 0.1.6

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.
Files changed (42) hide show
  1. package/README.md +1 -1
  2. package/dist/api/deploy/index.js +13 -1
  3. package/dist/api/deploy/modern-types.js +23 -0
  4. package/dist/api/deploy/modern.js +78 -0
  5. package/dist/api/deploy/plugin-instances-types.js +6 -0
  6. package/dist/api/deploy/plugin-instances.js +22 -0
  7. package/dist/cli/commands/app/index.js +84 -2
  8. package/dist/cli/commands/deploy/modern.js +50 -0
  9. package/dist/cli/commands/index.js +33 -5
  10. package/dist/cli/commands/skills/index.js +63 -0
  11. package/dist/cli/handlers/app/index.js +6 -1
  12. package/dist/cli/handlers/app/init.js +88 -0
  13. package/dist/cli/handlers/deploy/modern.js +33 -0
  14. package/dist/cli/handlers/skills/index.js +7 -0
  15. package/dist/cli/handlers/skills/status.js +31 -0
  16. package/dist/cli/handlers/skills/sync.js +38 -0
  17. package/dist/services/app/init/index.js +12 -0
  18. package/dist/services/app/init/install.js +101 -0
  19. package/dist/services/app/init/template.js +87 -0
  20. package/dist/services/deploy/modern/atoms/build.js +59 -0
  21. package/dist/services/deploy/modern/atoms/context.js +27 -0
  22. package/dist/services/deploy/modern/atoms/index.js +17 -0
  23. package/dist/services/deploy/modern/atoms/local-release.js +27 -0
  24. package/dist/services/deploy/modern/atoms/pre-release.js +13 -0
  25. package/dist/services/deploy/modern/atoms/save-plugin-instances.js +72 -0
  26. package/dist/services/deploy/modern/atoms/upload.js +246 -0
  27. package/dist/services/deploy/modern/check.js +53 -0
  28. package/dist/services/deploy/modern/constants.js +13 -0
  29. package/dist/services/deploy/modern/index.js +16 -0
  30. package/dist/services/deploy/modern/pipelines/index.js +5 -0
  31. package/dist/services/deploy/modern/pipelines/local.js +75 -0
  32. package/dist/services/deploy/modern/protocol.js +122 -0
  33. package/dist/services/deploy/modern/run-types.js +4 -0
  34. package/dist/services/deploy/modern/run.js +13 -0
  35. package/dist/services/deploy/modern/template-key-map.js +22 -0
  36. package/dist/services/skills/index.js +5 -0
  37. package/dist/services/skills/status.js +37 -0
  38. package/dist/utils/coding-steering.js +85 -0
  39. package/dist/utils/http.js +21 -11
  40. package/dist/utils/npm-pack.js +55 -0
  41. package/dist/utils/spark-meta.js +42 -0
  42. package/package.json +1 -1
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.installDependencies = exports.writeSparkMeta = exports.readSparkMeta = exports.TEMPLATE_PACKAGE_BY_STACK = exports.SUPPORTED_STACKS = exports.renderTemplate = void 0;
4
+ var template_1 = require("./template");
5
+ Object.defineProperty(exports, "renderTemplate", { enumerable: true, get: function () { return template_1.renderTemplate; } });
6
+ Object.defineProperty(exports, "SUPPORTED_STACKS", { enumerable: true, get: function () { return template_1.SUPPORTED_STACKS; } });
7
+ Object.defineProperty(exports, "TEMPLATE_PACKAGE_BY_STACK", { enumerable: true, get: function () { return template_1.TEMPLATE_PACKAGE_BY_STACK; } });
8
+ var spark_meta_1 = require("../../../utils/spark-meta");
9
+ Object.defineProperty(exports, "readSparkMeta", { enumerable: true, get: function () { return spark_meta_1.readSparkMeta; } });
10
+ Object.defineProperty(exports, "writeSparkMeta", { enumerable: true, get: function () { return spark_meta_1.writeSparkMeta; } });
11
+ var install_1 = require("./install");
12
+ Object.defineProperty(exports, "installDependencies", { enumerable: true, get: function () { return install_1.installDependencies; } });
@@ -0,0 +1,101 @@
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.installDependencies = installDependencies;
7
+ const node_child_process_1 = require("node:child_process");
8
+ const node_crypto_1 = __importDefault(require("node:crypto"));
9
+ const node_fs_1 = __importDefault(require("node:fs"));
10
+ const node_path_1 = __importDefault(require("node:path"));
11
+ const error_1 = require("../../../utils/error");
12
+ const logger_1 = require("../../../utils/logger");
13
+ const DEP_CACHE_ENV = 'MIAODA_DEP_CACHE_DIR';
14
+ /**
15
+ * 依赖安装。
16
+ *
17
+ * 优先级:
18
+ * 1. skip=true → 直接返回 source=skipped
19
+ * 2. MIAODA_DEP_CACHE_DIR 设了 + 目标目录有 package.json:
20
+ * - 算 md5(package.json)(与 CI 生产端 dep-cache pipeline 一致)
21
+ * - 若 ${cache}/${hash}.zip 存在 → unzip 到目标目录
22
+ * - 校验 node_modules/ 非空:通过则 source=cache,否则 fallback npm install
23
+ * - zip 不存在 → fallback npm install,但 hash 仍记录
24
+ * 3. `npm install --no-audit --no-fund`
25
+ *
26
+ * 不做 cache 回写(read-only)。
27
+ */
28
+ function installDependencies(opts) {
29
+ if (opts.skip) {
30
+ (0, logger_1.log)('init', 'Skipping dependency install (--skip-install)');
31
+ return { installed: false, source: 'skipped' };
32
+ }
33
+ const childStdio = stdioFor(opts.quietStdout);
34
+ const cacheDir = process.env[DEP_CACHE_ENV];
35
+ const pkgJsonPath = node_path_1.default.join(opts.targetDir, 'package.json');
36
+ const hasPkg = node_fs_1.default.existsSync(pkgJsonPath);
37
+ if (cacheDir && hasPkg) {
38
+ const hash = md5File(pkgJsonPath);
39
+ const cacheZip = node_path_1.default.join(cacheDir, `${hash}.zip`);
40
+ if (node_fs_1.default.existsSync(cacheZip)) {
41
+ (0, logger_1.log)('init', `Cache hit: ${cacheZip}`);
42
+ extractZip(cacheZip, opts.targetDir, childStdio);
43
+ if (nodeModulesUsable(opts.targetDir)) {
44
+ return { installed: true, source: 'cache', hash, cacheZip };
45
+ }
46
+ (0, logger_1.log)('init', 'Cache zip extracted but node_modules/ missing or empty; falling back to npm install');
47
+ runNpmInstall(opts.targetDir, childStdio);
48
+ return { installed: true, source: 'npm', hash };
49
+ }
50
+ (0, logger_1.log)('init', `Cache miss for ${hash}.zip, falling back to npm install`);
51
+ runNpmInstall(opts.targetDir, childStdio);
52
+ return { installed: true, source: 'npm', hash };
53
+ }
54
+ if (cacheDir && !hasPkg) {
55
+ (0, logger_1.log)('init', `${DEP_CACHE_ENV} set but no package.json in target; running npm install`);
56
+ }
57
+ runNpmInstall(opts.targetDir, childStdio);
58
+ return { installed: true, source: 'npm' };
59
+ }
60
+ function nodeModulesUsable(targetDir) {
61
+ const nm = node_path_1.default.join(targetDir, 'node_modules');
62
+ if (!node_fs_1.default.existsSync(nm))
63
+ return false;
64
+ try {
65
+ return node_fs_1.default.readdirSync(nm).length > 0;
66
+ }
67
+ catch {
68
+ return false;
69
+ }
70
+ }
71
+ function stdioFor(quietStdout) {
72
+ // quietStdout=true:子进程 stdout 重定向到 fd 2(父进程 stderr),
73
+ // 保留 stderr 直出 → 终端仍能看到 npm install 进度,但父进程 stdout 干净,
74
+ // 后续 emit(JSON) 不会被污染。
75
+ return quietStdout ? ['ignore', 2, 'inherit'] : ['ignore', 'inherit', 'inherit'];
76
+ }
77
+ function md5File(filePath) {
78
+ const buf = node_fs_1.default.readFileSync(filePath);
79
+ return node_crypto_1.default.createHash('md5').update(buf).digest('hex');
80
+ }
81
+ function extractZip(zipPath, targetDir, stdio) {
82
+ try {
83
+ (0, node_child_process_1.execFileSync)('unzip', ['-q', '-o', zipPath, '-d', targetDir], { stdio });
84
+ }
85
+ catch (err) {
86
+ const msg = err instanceof Error ? err.message : String(err);
87
+ throw new error_1.AppError('DEP_CACHE_EXTRACT_FAILED', `解压依赖缓存失败 (${zipPath}): ${msg}`, {
88
+ next_actions: [`确认 ${zipPath} 是合法 zip`, `或 unset ${DEP_CACHE_ENV} 走 npm install`],
89
+ });
90
+ }
91
+ }
92
+ function runNpmInstall(targetDir, stdio) {
93
+ (0, logger_1.log)('init', `npm install in ${targetDir}...`);
94
+ try {
95
+ (0, node_child_process_1.execFileSync)('npm', ['install', '--no-audit', '--no-fund'], { cwd: targetDir, stdio });
96
+ }
97
+ catch (err) {
98
+ const msg = err instanceof Error ? err.message : String(err);
99
+ throw new error_1.AppError('NPM_INSTALL_FAILED', `npm install 失败: ${msg}`);
100
+ }
101
+ }
@@ -0,0 +1,87 @@
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.SUPPORTED_STACKS = exports.TEMPLATE_PACKAGE_BY_STACK = void 0;
7
+ exports.renderTemplate = renderTemplate;
8
+ const node_fs_1 = __importDefault(require("node:fs"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const npm_pack_1 = require("../../../utils/npm-pack");
11
+ const error_1 = require("../../../utils/error");
12
+ const logger_1 = require("../../../utils/logger");
13
+ /** 短名 → 包名映射。新增 stack 时改这里。archType 由 template 包自报,CLI 不维护。 */
14
+ exports.TEMPLATE_PACKAGE_BY_STACK = {
15
+ 'vite-react': '@lark-apaas/coding-template-vite-react',
16
+ html: '@lark-apaas/coding-template-html',
17
+ };
18
+ exports.SUPPORTED_STACKS = Object.keys(exports.TEMPLATE_PACKAGE_BY_STACK);
19
+ // template/ 内以下划线开头的占位文件名在渲染时改名(npm publish 会把 .gitignore / .npmrc 吞掉,
20
+ // 模板里用 _gitignore / _npmrc 提交,渲染时再改回)。
21
+ const RENAME_FILES = {
22
+ _gitignore: '.gitignore',
23
+ '_env.local': '.env.local',
24
+ _npmrc: '.npmrc',
25
+ };
26
+ function renderTemplate(opts) {
27
+ const packageName = exports.TEMPLATE_PACKAGE_BY_STACK[opts.stack];
28
+ if (!packageName) {
29
+ throw new error_1.AppError('ARGS_INVALID', `不支持的 template: ${opts.stack}`, {
30
+ next_actions: [`可用 stack:${exports.SUPPORTED_STACKS.join(', ')}`],
31
+ });
32
+ }
33
+ (0, logger_1.log)('init', `Fetching ${packageName}@${opts.version ?? 'latest'}...`);
34
+ const fetched = (0, npm_pack_1.fetchNpmPackage)({ packageName, version: opts.version });
35
+ try {
36
+ const templateDir = node_path_1.default.join(fetched.extractDir, 'template');
37
+ if (!node_fs_1.default.existsSync(templateDir)) {
38
+ throw new error_1.AppError('TEMPLATE_INVALID', `包 ${packageName}@${fetched.version} 缺少 template/ 目录`);
39
+ }
40
+ const pkgJsonPath = node_path_1.default.join(fetched.extractDir, 'package.json');
41
+ let archType;
42
+ if (node_fs_1.default.existsSync(pkgJsonPath)) {
43
+ const pkgJson = JSON.parse(node_fs_1.default.readFileSync(pkgJsonPath, 'utf-8'));
44
+ archType = pkgJson.miaodaTemplate?.archType;
45
+ }
46
+ // 用显式 null/undefined/'' 判,避免把合法值 0 / false 当成"缺失"误抛
47
+ if (archType === undefined || archType === null || archType === '') {
48
+ throw new error_1.AppError('TEMPLATE_INVALID', `包 ${packageName}@${fetched.version} 缺少 package.json.miaodaTemplate.archType`);
49
+ }
50
+ (0, logger_1.log)('init', `Rendering template to ${opts.targetDir}...`);
51
+ copyDir(templateDir, opts.targetDir);
52
+ for (const [from, to] of Object.entries(RENAME_FILES)) {
53
+ const fromPath = node_path_1.default.join(opts.targetDir, from);
54
+ const toPath = node_path_1.default.join(opts.targetDir, to);
55
+ if (node_fs_1.default.existsSync(fromPath))
56
+ node_fs_1.default.renameSync(fromPath, toPath);
57
+ }
58
+ // 仅替换展示性文件;package.json 写固定 name,保证 md5(package.json) 跨渲染稳定,
59
+ // 可被 install 阶段直接当 MIAODA_DEP_CACHE_DIR 的 cache key。
60
+ for (const rel of ['index.html', 'README.md']) {
61
+ const p = node_path_1.default.join(opts.targetDir, rel);
62
+ if (node_fs_1.default.existsSync(p))
63
+ replaceInFile(p, '{{projectName}}', opts.projectName);
64
+ }
65
+ return { version: fetched.version, packageName, archType };
66
+ }
67
+ finally {
68
+ fetched.cleanup();
69
+ }
70
+ }
71
+ function copyDir(src, dest) {
72
+ node_fs_1.default.mkdirSync(dest, { recursive: true });
73
+ for (const entry of node_fs_1.default.readdirSync(src, { withFileTypes: true })) {
74
+ const srcPath = node_path_1.default.join(src, entry.name);
75
+ const destPath = node_path_1.default.join(dest, entry.name);
76
+ if (entry.isDirectory()) {
77
+ copyDir(srcPath, destPath);
78
+ }
79
+ else {
80
+ node_fs_1.default.copyFileSync(srcPath, destPath);
81
+ }
82
+ }
83
+ }
84
+ function replaceInFile(filePath, search, replace) {
85
+ const content = node_fs_1.default.readFileSync(filePath, 'utf-8');
86
+ node_fs_1.default.writeFileSync(filePath, content.replaceAll(search, replace), 'utf-8');
87
+ }
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runBuild = runBuild;
4
+ const node_child_process_1 = require("node:child_process");
5
+ const error_1 = require("../../../../utils/error");
6
+ const logger_1 = require("../../../../utils/logger");
7
+ const protocol_1 = require("../protocol");
8
+ /**
9
+ * 给 RESOURCE_CDN_PREFIX 补 https:// 协议头:
10
+ * - 已带 http:// 或 https:// → 原样返回
11
+ * - 以 / 开头(绝对路径)→ 原样返回
12
+ * - 不含 dot(看起来不像域名,比如纯路径 app_xxx/2)→ 原样返回
13
+ * - 其它(典型:lf-xxx.bytetos.com/...)→ 前缀 https://
14
+ *
15
+ * template 的 build script 直接拿来当 vite base / asset URL 用,必须是合法 URL。
16
+ * STATIC_CDN_PREFIX 由 runtime 同域提供(相对路径),不走这里。
17
+ */
18
+ function ensureHttpsScheme(v) {
19
+ if (/^https?:\/\//i.test(v))
20
+ return v;
21
+ if (v.startsWith('/'))
22
+ return v;
23
+ if (!v.includes('.'))
24
+ return v;
25
+ return `https://${v}`;
26
+ }
27
+ /**
28
+ * 跑 `npm run build`,把 preRelease 下发的动态配置注入 env:
29
+ * MIAODA_APP_ID / MIAODA_VERSION / MIAODA_STACK
30
+ * MIAODA_RESOURCE_CDN_PREFIX / MIAODA_STATIC_CDN_PREFIX
31
+ *
32
+ * MIAODA_STATIC_CDN_PREFIX 取自 output_static_paas_storage_credential.downloadURLPrefix,
33
+ * 是 runtime 同域相对路径(例如 /app/<appId>/runtime/api/v1/storage/object/<bucket>/),
34
+ * 不补 https scheme,原样注入。顶层 static_cdn_prefix 不再下发。
35
+ *
36
+ * build 失败抛 AppError(execSync 自身会 throw,捕获后包一层加错误码)。
37
+ */
38
+ function runBuild(opts) {
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);
40
+ const buildEnv = {
41
+ ...process.env,
42
+ MIAODA_APP_ID: opts.appId,
43
+ MIAODA_VERSION: opts.version,
44
+ MIAODA_STACK: opts.templateKey,
45
+ MIAODA_RESOURCE_CDN_PREFIX: ensureHttpsScheme((0, protocol_1.requireDataKey)(opts.data, protocol_1.DataKey.RESOURCE_CDN_PREFIX)),
46
+ MIAODA_STATIC_CDN_PREFIX: staticCred.downloadURLPrefix,
47
+ };
48
+ (0, logger_1.log)('deploy', 'Building...');
49
+ try {
50
+ (0, node_child_process_1.execSync)('npm run build', {
51
+ cwd: opts.projectDir,
52
+ stdio: 'inherit',
53
+ env: buildEnv,
54
+ });
55
+ }
56
+ catch (err) {
57
+ throw new error_1.AppError('DEPLOY_BUILD_FAILED', `npm run build failed: ${err.message}`);
58
+ }
59
+ }
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.prepareDeployContext = prepareDeployContext;
4
+ const error_1 = require("../../../../utils/error");
5
+ const spark_meta_1 = require("../../../../utils/spark-meta");
6
+ const check_1 = require("../check");
7
+ const template_key_map_1 = require("../template-key-map");
8
+ /**
9
+ * 准备 deploy 上下文:跑前置检查、读 .spark/meta.json 拿 stack,
10
+ * 并把 stack 短名映射成后端 templateKey。appId 由调用方从 env 解析后传入。
11
+ *
12
+ * 任何前置失败统一抛 AppError。返回值是后续 atom 唯一信任的入参源。
13
+ */
14
+ function prepareDeployContext(projectDir, appId) {
15
+ (0, check_1.runDeployChecks)(projectDir);
16
+ const meta = (0, spark_meta_1.readSparkMeta)(projectDir);
17
+ if (meta.stack === undefined || meta.stack === '') {
18
+ throw new error_1.AppError('DEPLOY_META_INCOMPLETE', '.spark/meta.json missing stack — run `miaoda app init` first');
19
+ }
20
+ return {
21
+ projectDir,
22
+ appId,
23
+ stack: meta.stack,
24
+ templateKey: (0, template_key_map_1.resolveTemplateKey)(meta.stack),
25
+ meta,
26
+ };
27
+ }
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.savePluginInstances = exports.LocalReleaseStatus = exports.finalizeLocalRelease = exports.createLocalRelease = exports.uploadArtifacts = exports.runBuild = exports.preRelease = exports.prepareDeployContext = void 0;
4
+ var context_1 = require("./context");
5
+ Object.defineProperty(exports, "prepareDeployContext", { enumerable: true, get: function () { return context_1.prepareDeployContext; } });
6
+ var pre_release_1 = require("./pre-release");
7
+ Object.defineProperty(exports, "preRelease", { enumerable: true, get: function () { return pre_release_1.preRelease; } });
8
+ var build_1 = require("./build");
9
+ Object.defineProperty(exports, "runBuild", { enumerable: true, get: function () { return build_1.runBuild; } });
10
+ var upload_1 = require("./upload");
11
+ Object.defineProperty(exports, "uploadArtifacts", { enumerable: true, get: function () { return upload_1.uploadArtifacts; } });
12
+ var local_release_1 = require("./local-release");
13
+ Object.defineProperty(exports, "createLocalRelease", { enumerable: true, get: function () { return local_release_1.createLocalRelease; } });
14
+ Object.defineProperty(exports, "finalizeLocalRelease", { enumerable: true, get: function () { return local_release_1.finalizeLocalRelease; } });
15
+ Object.defineProperty(exports, "LocalReleaseStatus", { enumerable: true, get: function () { return local_release_1.LocalReleaseStatus; } });
16
+ var save_plugin_instances_1 = require("./save-plugin-instances");
17
+ Object.defineProperty(exports, "savePluginInstances", { enumerable: true, get: function () { return save_plugin_instances_1.savePluginInstances; } });
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LocalReleaseStatus = void 0;
4
+ exports.createLocalRelease = createLocalRelease;
5
+ exports.finalizeLocalRelease = finalizeLocalRelease;
6
+ const index_1 = require("../../../../api/deploy/index");
7
+ Object.defineProperty(exports, "LocalReleaseStatus", { enumerable: true, get: function () { return index_1.LocalReleaseStatus; } });
8
+ const logger_1 = require("../../../../utils/logger");
9
+ /**
10
+ * 创建本地发布单(加锁;不挂 pipeline)。
11
+ * 返回的 releaseId 必须由 finalizeLocalRelease 翻为终态,否则发布单一直挂着。
12
+ */
13
+ async function createLocalRelease(appId, version) {
14
+ return (0, index_1.createLocalRelease)({ appID: appId, version });
15
+ }
16
+ /**
17
+ * 把本地发布单翻为终态。Finished / Failed 由 pipeline 视上下游结果决定。
18
+ * 失败不抛(避免覆盖上游真错误),但记一行 stderr 让排查可定位。
19
+ */
20
+ async function finalizeLocalRelease(appId, releaseId, status) {
21
+ try {
22
+ await (0, index_1.updateLocalRelease)({ appID: appId, releaseID: releaseId, status });
23
+ }
24
+ catch (err) {
25
+ (0, logger_1.log)('deploy', `finalize release(${releaseId}, status=${String(status)}) failed: ${err.message}`);
26
+ }
27
+ }
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.preRelease = preRelease;
4
+ const index_1 = require("../../../../api/deploy/index");
5
+ /**
6
+ * 调用 preRelease,拿到 pre_release_id、version 和动态配置 data。
7
+ *
8
+ * 失败由 api 层抛 HttpError / AppError 透传上去;atom 本身不再做语义包装,
9
+ * 让 pipeline 层决定怎么向 handler 报告。
10
+ */
11
+ async function preRelease(appId, templateKey) {
12
+ return (0, index_1.preRelease)({ appID: appId, templateKey });
13
+ }
@@ -0,0 +1,72 @@
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.savePluginInstances = savePluginInstances;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const index_1 = require("../../../../api/deploy/index");
10
+ const logger_1 = require("../../../../utils/logger");
11
+ const constants_1 = require("../constants");
12
+ function isValidCapability(o) {
13
+ if (typeof o !== 'object' || o === null)
14
+ return false;
15
+ const id = o.id;
16
+ return typeof id === 'string' && id.length > 0;
17
+ }
18
+ /**
19
+ * 扫描 dist/output_capabilities/*.json,过滤出合法 capability,批量上报到 studio_server。
20
+ *
21
+ * 合法判定:能 JSON.parse + 顶层 id 是非空字符串。其它字段后端校验。
22
+ * 目录不存在 / 没有合法 capability → 直接返回 0,pipeline 当作 no-op。
23
+ * batchSave 调用失败 → 透传抛 AppError,pipeline 兜底 finalizeLocalRelease(Failed)。
24
+ */
25
+ async function savePluginInstances(opts) {
26
+ const dir = node_path_1.default.join(opts.projectDir, constants_1.DIST_DIR, constants_1.OUTPUT_CAPABILITIES_DIR);
27
+ if (!node_fs_1.default.existsSync(dir)) {
28
+ (0, logger_1.debug)(`save-plugin-instances: ${dir} not present, skipping`);
29
+ return { saved: 0, skipped: 0 };
30
+ }
31
+ const files = node_fs_1.default.readdirSync(dir).filter((f) => f.endsWith('.json'));
32
+ const instances = [];
33
+ let skipped = 0;
34
+ for (const f of files) {
35
+ const full = node_path_1.default.join(dir, f);
36
+ let raw;
37
+ try {
38
+ raw = node_fs_1.default.readFileSync(full, 'utf-8');
39
+ }
40
+ catch (err) {
41
+ (0, logger_1.debug)(`save-plugin-instances: read ${f} failed: ${err.message}`);
42
+ skipped++;
43
+ continue;
44
+ }
45
+ let parsed;
46
+ try {
47
+ parsed = JSON.parse(raw);
48
+ }
49
+ catch {
50
+ (0, logger_1.debug)(`save-plugin-instances: ${f} is not valid JSON, skipping`);
51
+ skipped++;
52
+ continue;
53
+ }
54
+ if (!isValidCapability(parsed)) {
55
+ (0, logger_1.debug)(`save-plugin-instances: ${f} missing 'id' string, skipping`);
56
+ skipped++;
57
+ continue;
58
+ }
59
+ instances.push({ id: parsed.id, content: JSON.stringify(parsed) });
60
+ }
61
+ if (instances.length === 0) {
62
+ (0, logger_1.log)('deploy', `No capability to register (${String(files.length)} files scanned, all skipped)`);
63
+ return { saved: 0, skipped };
64
+ }
65
+ (0, logger_1.log)('deploy', `Registering ${String(instances.length)} capability instance(s)...`);
66
+ await (0, index_1.batchSavePluginInstances)({
67
+ appID: opts.appId,
68
+ appVersion: opts.version,
69
+ pluginInstances: instances,
70
+ });
71
+ return { saved: instances.length, skipped };
72
+ }
@@ -0,0 +1,246 @@
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.uploadArtifacts = uploadArtifacts;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_os_1 = __importDefault(require("node:os"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const node_child_process_1 = require("node:child_process");
11
+ const index_1 = require("../../../../api/deploy/index");
12
+ const error_1 = require("../../../../utils/error");
13
+ const logger_1 = require("../../../../utils/logger");
14
+ const constants_1 = require("../constants");
15
+ const protocol_1 = require("../protocol");
16
+ const UPLOAD_PLAN = [
17
+ {
18
+ kind: 'tos',
19
+ dir: constants_1.OUTPUT_DIR,
20
+ dataKey: protocol_1.DataKey.OUTPUT_TOS_UPLOAD_CREDENTIAL,
21
+ required: true,
22
+ },
23
+ {
24
+ kind: 'tos',
25
+ dir: constants_1.OUTPUT_RESOURCE_DIR,
26
+ dataKey: protocol_1.DataKey.OUTPUT_RESOURCE_TOS_UPLOAD_CREDENTIAL,
27
+ required: false,
28
+ },
29
+ {
30
+ kind: 'paas_storage',
31
+ dir: constants_1.OUTPUT_STATIC_DIR,
32
+ dataKey: protocol_1.DataKey.OUTPUT_STATIC_PAAS_STORAGE_CREDENTIAL,
33
+ },
34
+ ];
35
+ function resolveTosutilPath() {
36
+ try {
37
+ const resolved = (0, node_child_process_1.execFileSync)('which', ['tosutil'], { encoding: 'utf-8' }).trim();
38
+ if (resolved)
39
+ return resolved;
40
+ }
41
+ catch {
42
+ /* fallthrough */
43
+ }
44
+ throw new error_1.AppError('DEPLOY_TOSUTIL_MISSING', 'tosutil is not installed or not in PATH. modern deploy requires sandbox preinstalled tosutil.');
45
+ }
46
+ function tosutilUploadFromTos(cred) {
47
+ return {
48
+ accessKeyID: cred.accessKeyID,
49
+ secretAccessKey: cred.secretAccessKey,
50
+ sessionToken: cred.sessionToken,
51
+ endpoint: cred.endpoint,
52
+ region: cred.region,
53
+ bucket: cred.bucket,
54
+ prefix: cred.prefix,
55
+ };
56
+ }
57
+ function tosutilUploadFromPaasStorage(cred, dataKey) {
58
+ const { bucket, prefix } = (0, protocol_1.parseTosUploadPrefix)(cred.uploadPrefix, dataKey);
59
+ return {
60
+ accessKeyID: cred.accessKeyID,
61
+ secretAccessKey: cred.secretAccessKey,
62
+ sessionToken: cred.sessionToken,
63
+ endpoint: cred.endpoint,
64
+ region: cred.region,
65
+ bucket,
66
+ prefix,
67
+ };
68
+ }
69
+ /**
70
+ * 写一个 tosutil 临时 config 文件,注入 STS 凭证 + endpoint + region。
71
+ * 返回 config 路径,调用方负责清理。
72
+ */
73
+ function writeTosutilConfig(tosutilPath, cred, label) {
74
+ const confPath = node_path_1.default.join(node_os_1.default.tmpdir(), `.tosutilconfig-miaoda-${label}-${String(process.pid)}-${String(Date.now())}`);
75
+ node_fs_1.default.writeFileSync(confPath, '');
76
+ (0, node_child_process_1.execFileSync)(tosutilPath, [
77
+ 'config',
78
+ '-conf',
79
+ confPath,
80
+ '-e',
81
+ cred.endpoint,
82
+ '-i',
83
+ cred.accessKeyID,
84
+ '-k',
85
+ cred.secretAccessKey,
86
+ '-t',
87
+ cred.sessionToken,
88
+ '-re',
89
+ cred.region,
90
+ ], { stdio: 'pipe' });
91
+ return confPath;
92
+ }
93
+ /** 把 tosutil 进程的输出按行回放到 stderr,便于线上排查(沙箱里无法 attach 进程)。 */
94
+ function streamTosutilOutput(output) {
95
+ if (!output)
96
+ return;
97
+ for (const line of output.split('\n')) {
98
+ if (line.length === 0)
99
+ continue;
100
+ process.stderr.write(`[tosutil] ${line}\n`);
101
+ }
102
+ }
103
+ function normalizeTosDest(bucket, prefix) {
104
+ const bucketPart = bucket.startsWith('tos://') ? bucket : `tos://${bucket}`;
105
+ const normalized = prefix.replace(/^\/+/, '');
106
+ return `${bucketPart.replace(/\/+$/, '')}/${normalized}`;
107
+ }
108
+ /**
109
+ * 用 tosutil 把目录批量上传到 tos://bucket/prefix(STS 凭证从 config 文件读取)。
110
+ */
111
+ function uploadDirWithCredential(tosutilPath, sourceDir, cred, label) {
112
+ const entries = node_fs_1.default.readdirSync(sourceDir);
113
+ if (entries.length === 0) {
114
+ (0, logger_1.debug)(`upload: skip empty dir ${sourceDir}`);
115
+ return;
116
+ }
117
+ const dest = normalizeTosDest(cred.bucket, cred.prefix);
118
+ (0, logger_1.debug)(`upload: ${sourceDir} -> ${dest} (${String(entries.length)} entries)`);
119
+ const confPath = writeTosutilConfig(tosutilPath, cred, label);
120
+ let output;
121
+ try {
122
+ output = (0, node_child_process_1.execFileSync)(tosutilPath, [
123
+ 'cp',
124
+ sourceDir,
125
+ dest,
126
+ '-r',
127
+ '-flat',
128
+ '-j',
129
+ '5',
130
+ '-p',
131
+ '3',
132
+ '-ps',
133
+ '10485760',
134
+ '-f',
135
+ '-conf',
136
+ confPath,
137
+ ], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
138
+ streamTosutilOutput(output);
139
+ }
140
+ catch (err) {
141
+ const e = err;
142
+ const stderr = (e.stderr ?? '').trim();
143
+ const stdout = (e.stdout ?? '').trim();
144
+ streamTosutilOutput(stdout);
145
+ streamTosutilOutput(stderr);
146
+ throw new error_1.AppError('DEPLOY_UPLOAD_FAILED', `tosutil cp exited with status ${String(e.status ?? -1)} for ${sourceDir}\n` +
147
+ ` stderr: ${stderr || '(empty)'}\n` +
148
+ ` stdout: ${stdout.slice(0, 500) || '(empty)'}`);
149
+ }
150
+ finally {
151
+ try {
152
+ node_fs_1.default.unlinkSync(confPath);
153
+ }
154
+ catch {
155
+ /* ignore */
156
+ }
157
+ }
158
+ const succeedMatch = /Succeed count is:\s*(\d+)/.exec(output);
159
+ const failedMatch = /Failed count is:\s*(\d+)/.exec(output);
160
+ const succeedCount = succeedMatch ? Number.parseInt(succeedMatch[1], 10) : 0;
161
+ const failedCount = failedMatch ? Number.parseInt(failedMatch[1], 10) : 0;
162
+ if (failedCount > 0) {
163
+ throw new error_1.AppError('DEPLOY_UPLOAD_FAILED', `Upload failed: ${String(failedCount)} files failed, ${String(succeedCount)} succeeded. source=${sourceDir}`);
164
+ }
165
+ if (succeedCount === 0) {
166
+ throw new error_1.AppError('DEPLOY_UPLOAD_EMPTY', `Upload failed: 0 files uploaded. source=${sourceDir}. Check tosutil credentials and bucket permissions.`);
167
+ }
168
+ }
169
+ function uploadTosTarget(tosutilPath, target, localPath, data) {
170
+ const credRaw = target.required
171
+ ? (0, protocol_1.requireDataKey)(data, target.dataKey)
172
+ : (0, protocol_1.optionalDataKey)(data, target.dataKey);
173
+ if (credRaw === undefined) {
174
+ (0, logger_1.debug)(`upload: ${target.dir} exists but no credential configured, skipping`);
175
+ return false;
176
+ }
177
+ const cred = (0, protocol_1.parseTosUploadCredential)(credRaw, target.dataKey);
178
+ (0, logger_1.log)('deploy', `Uploading ${target.dir}...`);
179
+ uploadDirWithCredential(tosutilPath, localPath, tosutilUploadFromTos(cred), target.dir);
180
+ return true;
181
+ }
182
+ function uploadPaasStorageTarget(tosutilPath, target, localPath, data) {
183
+ // 空目录:直接跳过;callbackStatic 也不能调,否则会让后端把"未发生的上传"
184
+ // 误以为已完成。凭证本身 build 阶段已经消费过 downloadURLPrefix,这里不重复要求。
185
+ if (node_fs_1.default.readdirSync(localPath).length === 0) {
186
+ (0, logger_1.debug)(`upload: ${target.dir} is empty, skipping`);
187
+ return undefined;
188
+ }
189
+ const cred = (0, protocol_1.parsePaasStorageCredential)((0, protocol_1.requireDataKey)(data, target.dataKey), target.dataKey);
190
+ (0, logger_1.log)('deploy', `Uploading ${target.dir}...`);
191
+ uploadDirWithCredential(tosutilPath, localPath, tosutilUploadFromPaasStorage(cred, target.dataKey), target.dir);
192
+ return cred;
193
+ }
194
+ /**
195
+ * 上传三类产物。output / output_resource 走 TOS STS 凭证;output_static 走 PaaS Storage
196
+ * 凭证(uploadPrefix 解出 tosutil cp 目的,上传完后再调 callbackStatic 把 uploadID
197
+ * 提交给后端核销):
198
+ * - dist/output (必传) → OUTPUT_TOS_UPLOAD_CREDENTIAL
199
+ * - dist/output_resource (可选) → OUTPUT_RESOURCE_TOS_UPLOAD_CREDENTIAL
200
+ * - dist/output_static (可选) → OUTPUT_STATIC_PAAS_STORAGE_CREDENTIAL + callbackStatic
201
+ *
202
+ * 目录不存在的可选项跳过;output 不存在或凭证缺失抛错。
203
+ */
204
+ async function uploadArtifacts(opts) {
205
+ const tosutilPath = resolveTosutilPath();
206
+ const distDir = node_path_1.default.join(opts.projectDir, constants_1.DIST_DIR);
207
+ const outcome = { uploaded: 0 };
208
+ for (const target of UPLOAD_PLAN) {
209
+ const localPath = node_path_1.default.join(distDir, target.dir);
210
+ const exists = node_fs_1.default.existsSync(localPath);
211
+ if (target.kind === 'tos') {
212
+ if (!exists) {
213
+ if (target.required) {
214
+ throw new error_1.AppError('DEPLOY_NO_BUILD_OUTPUT', `Required directory missing: ${constants_1.DIST_DIR}/${target.dir}`);
215
+ }
216
+ (0, logger_1.debug)(`upload: dir not present, skipping ${target.dir}`);
217
+ continue;
218
+ }
219
+ if (uploadTosTarget(tosutilPath, target, localPath, opts.data)) {
220
+ outcome.uploaded++;
221
+ }
222
+ continue;
223
+ }
224
+ // PaaS Storage 分支
225
+ if (!exists) {
226
+ (0, logger_1.debug)(`upload: dir not present, skipping ${target.dir}`);
227
+ continue;
228
+ }
229
+ const cred = uploadPaasStorageTarget(tosutilPath, target, localPath, opts.data);
230
+ if (cred) {
231
+ outcome.staticUploaded = cred;
232
+ outcome.uploaded++;
233
+ }
234
+ }
235
+ if (outcome.uploaded === 0) {
236
+ throw new error_1.AppError('DEPLOY_UPLOAD_EMPTY', 'No artifacts uploaded — check build output.');
237
+ }
238
+ if (outcome.staticUploaded) {
239
+ (0, logger_1.log)('deploy', 'Callbacking static upload...');
240
+ await (0, index_1.callbackStatic)({
241
+ appID: opts.appId,
242
+ bucketID: outcome.staticUploaded.bucketID,
243
+ uploadID: outcome.staticUploaded.uploadID,
244
+ });
245
+ }
246
+ }