@lark-apaas/miaoda-cli 0.1.17 → 0.1.18-alpha.5fd4656

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 (30) hide show
  1. package/dist/cli/commands/app/index.js +16 -0
  2. package/dist/cli/commands/index.js +10 -0
  3. package/dist/cli/handlers/app/init.js +45 -12
  4. package/dist/cli/handlers/app/migrate.js +5 -1
  5. package/dist/cli/handlers/app/sync.js +5 -0
  6. package/dist/config/sync-configs/design-html.js +25 -0
  7. package/dist/config/sync-configs/index.js +2 -0
  8. package/dist/services/app/init/async-install.js +116 -0
  9. package/dist/services/app/init/index.js +6 -1
  10. package/dist/services/app/init/template.js +1 -0
  11. package/dist/services/deploy/modern/atoms/design-build.js +20 -0
  12. package/dist/services/deploy/modern/atoms/design-upload.js +73 -0
  13. package/dist/services/deploy/modern/atoms/index.js +5 -1
  14. package/dist/services/deploy/modern/atoms/tosutil.js +234 -0
  15. package/dist/services/deploy/modern/atoms/upload.js +4 -127
  16. package/dist/services/deploy/modern/pipelines/design-local.js +47 -0
  17. package/dist/services/deploy/modern/pipelines/index.js +3 -1
  18. package/dist/services/deploy/modern/protocol.js +7 -0
  19. package/dist/services/deploy/modern/run.js +10 -4
  20. package/dist/services/deploy/modern/template-key-map.js +4 -0
  21. package/dist/utils/logs-dir.js +19 -0
  22. package/package.json +1 -1
  23. package/upgrade/templates/design-html/templates/scripts/build.sh +70 -0
  24. package/upgrade/templates/design-stack/templates/.githooks/pre-commit +1 -0
  25. package/upgrade/templates/design-stack/templates/scripts/dev-local.js +69 -10
  26. package/upgrade/templates/design-stack/templates/scripts/hooks/run-precommit.js +36 -0
  27. package/upgrade/templates/nestjs-react-fullstack/templates/.githooks/pre-commit +1 -0
  28. package/upgrade/templates/nestjs-react-fullstack/templates/.spark_project +2 -2
  29. package/upgrade/templates/nestjs-react-fullstack/templates/scripts/dev-local.js +71 -13
  30. package/upgrade/templates/nestjs-react-fullstack/templates/scripts/hooks/run-precommit.js +36 -0
@@ -0,0 +1,234 @@
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
+ const entries = node_fs_1.default.readdirSync(sourceDir);
87
+ if (entries.length === 0) {
88
+ (0, logger_1.debug)(`upload: skip empty dir ${sourceDir}`);
89
+ return;
90
+ }
91
+ const dest = normalizeTosDest(cred.bucket, cred.prefix);
92
+ (0, logger_1.debug)(`upload: ${sourceDir} -> ${dest} (${String(entries.length)} entries)`);
93
+ const confPath = writeTosutilConfig(tosutilPath, cred, label);
94
+ let output;
95
+ try {
96
+ output = (0, node_child_process_1.execFileSync)(tosutilPath, [
97
+ 'cp',
98
+ sourceDir,
99
+ dest,
100
+ '-r',
101
+ '-flat',
102
+ '-j',
103
+ '5',
104
+ '-p',
105
+ '3',
106
+ '-ps',
107
+ '10485760',
108
+ '-f',
109
+ '-conf',
110
+ confPath,
111
+ ], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
112
+ streamTosutilOutput(output);
113
+ }
114
+ catch (err) {
115
+ const e = err;
116
+ const stderr = (e.stderr ?? '').trim();
117
+ const stdout = (e.stdout ?? '').trim();
118
+ streamTosutilOutput(stdout);
119
+ streamTosutilOutput(stderr);
120
+ throw new error_1.AppError('DEPLOY_UPLOAD_FAILED', `tosutil cp exited with status ${String(e.status ?? -1)} for ${sourceDir}\n` +
121
+ ` stderr: ${stderr || '(empty)'}\n` +
122
+ ` stdout: ${stdout.slice(0, 500) || '(empty)'}`);
123
+ }
124
+ finally {
125
+ try {
126
+ node_fs_1.default.unlinkSync(confPath);
127
+ }
128
+ catch {
129
+ /* ignore */
130
+ }
131
+ }
132
+ const succeedMatch = /Succeed count is:\s*(\d+)/.exec(output);
133
+ const failedMatch = /Failed count is:\s*(\d+)/.exec(output);
134
+ const succeedCount = succeedMatch ? Number.parseInt(succeedMatch[1], 10) : 0;
135
+ const failedCount = failedMatch ? Number.parseInt(failedMatch[1], 10) : 0;
136
+ if (failedCount > 0) {
137
+ throw new error_1.AppError('DEPLOY_UPLOAD_FAILED', `Upload failed: ${String(failedCount)} files failed, ${String(succeedCount)} succeeded. source=${sourceDir}`);
138
+ }
139
+ if (succeedCount === 0) {
140
+ throw new error_1.AppError('DEPLOY_UPLOAD_EMPTY', `Upload failed: 0 files uploaded. source=${sourceDir}. Check tosutil credentials and bucket permissions.`);
141
+ }
142
+ }
143
+ /** version→latest 同桶同凭证服务端复制:tosutil cp tos://src tos://dst -r -flat -f */
144
+ function copyTosToTos(tosutilPath, cred, srcPrefix, dstPrefix) {
145
+ const src = normalizeTosDest(cred.bucket, srcPrefix);
146
+ const dst = normalizeTosDest(cred.bucket, dstPrefix);
147
+ const confPath = writeTosutilConfig(tosutilPath, cred, 'tos2tos');
148
+ try {
149
+ 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'] });
150
+ streamTosutilOutput(out);
151
+ }
152
+ catch (err) {
153
+ const e = err;
154
+ streamTosutilOutput((e.stdout ?? '').trim());
155
+ streamTosutilOutput((e.stderr ?? '').trim());
156
+ throw new error_1.AppError('DEPLOY_UPLOAD_FAILED', `tosutil cp(tos→tos) exited status ${String(e.status ?? -1)}: ${src} → ${dst}`);
157
+ }
158
+ finally {
159
+ try {
160
+ node_fs_1.default.unlinkSync(confPath);
161
+ }
162
+ catch {
163
+ /* ignore */
164
+ }
165
+ }
166
+ }
167
+ /** 列 tos://bucket/prefix 下所有对象,返回相对 prefix 的 key 集合(用 ls -r -s 精简格式)。 */
168
+ function listRemoteKeys(tosutilPath, cred) {
169
+ const base = normalizeTosDest(cred.bucket, cred.prefix); // tos://bucket/prefix
170
+ const confPath = writeTosutilConfig(tosutilPath, cred, 'ls');
171
+ let out;
172
+ try {
173
+ out = (0, node_child_process_1.execFileSync)(tosutilPath, ['ls', base, '-r', '-s', '-conf', confPath], {
174
+ encoding: 'utf-8',
175
+ stdio: ['pipe', 'pipe', 'pipe'],
176
+ });
177
+ }
178
+ catch (err) {
179
+ const e = err;
180
+ throw new error_1.AppError('DEPLOY_UPLOAD_FAILED', `tosutil ls exited status ${String(e.status ?? -1)} for ${base}: ${(e.stderr ?? '').trim()}`);
181
+ }
182
+ finally {
183
+ try {
184
+ node_fs_1.default.unlinkSync(confPath);
185
+ }
186
+ catch {
187
+ /* ignore */
188
+ }
189
+ }
190
+ const prefixUri = base.replace(/\/+$/, '') + '/';
191
+ const keys = [];
192
+ for (const line of out.split('\n')) {
193
+ const t = line.trim();
194
+ if (!t.startsWith(prefixUri))
195
+ continue;
196
+ const rel = t.slice(prefixUri.length);
197
+ if (rel.length > 0 && !rel.endsWith('/'))
198
+ keys.push(rel);
199
+ }
200
+ return keys;
201
+ }
202
+ /** 删除 tos://bucket/prefix 下指定相对 key(逐个 rm -f)。 */
203
+ function removeRemoteKeys(tosutilPath, cred, relKeys) {
204
+ if (relKeys.length === 0)
205
+ return;
206
+ const confPath = writeTosutilConfig(tosutilPath, cred, 'rm');
207
+ const prefix = cred.prefix.replace(/\/+$/, '');
208
+ const failed = [];
209
+ try {
210
+ for (const rel of relKeys) {
211
+ // 用与 cp/ls 同一份 normalizeTosDest 拼目标 URI,避免删除路径与其它操作漂移。
212
+ const dest = normalizeTosDest(cred.bucket, `${prefix}/${rel}`);
213
+ try {
214
+ (0, node_child_process_1.execFileSync)(tosutilPath, ['rm', dest, '-f', '-conf', confPath], {
215
+ stdio: ['pipe', 'pipe', 'pipe'],
216
+ });
217
+ }
218
+ catch {
219
+ failed.push(rel);
220
+ }
221
+ }
222
+ }
223
+ finally {
224
+ try {
225
+ node_fs_1.default.unlinkSync(confPath);
226
+ }
227
+ catch {
228
+ /* ignore */
229
+ }
230
+ }
231
+ if (failed.length > 0) {
232
+ throw new error_1.AppError('DEPLOY_UPLOAD_FAILED', `prune latest failed for: ${failed.join(', ')}`);
233
+ }
234
+ }
@@ -5,14 +5,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.uploadArtifacts = uploadArtifacts;
7
7
  const node_fs_1 = __importDefault(require("node:fs"));
8
- const node_os_1 = __importDefault(require("node:os"));
9
8
  const node_path_1 = __importDefault(require("node:path"));
10
- const node_child_process_1 = require("node:child_process");
11
9
  const index_1 = require("../../../../api/deploy/index");
12
10
  const error_1 = require("../../../../utils/error");
13
11
  const logger_1 = require("../../../../utils/logger");
14
12
  const constants_1 = require("../constants");
15
13
  const protocol_1 = require("../protocol");
14
+ const tosutil_1 = require("./tosutil");
16
15
  const UPLOAD_PLAN = [
17
16
  {
18
17
  kind: 'tos',
@@ -32,28 +31,6 @@ const UPLOAD_PLAN = [
32
31
  dataKey: protocol_1.DataKey.OUTPUT_STATIC_PAAS_STORAGE_CREDENTIAL,
33
32
  },
34
33
  ];
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
34
  function tosutilUploadFromPaasStorage(cred, dataKey) {
58
35
  const { bucket, prefix } = (0, protocol_1.parseTosUploadPrefix)(cred.uploadPrefix, dataKey);
59
36
  return {
@@ -66,106 +43,6 @@ function tosutilUploadFromPaasStorage(cred, dataKey) {
66
43
  prefix,
67
44
  };
68
45
  }
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
46
  function uploadTosTarget(tosutilPath, target, localPath, data) {
170
47
  const credRaw = target.required
171
48
  ? (0, protocol_1.requireDataKey)(data, target.dataKey)
@@ -176,7 +53,7 @@ function uploadTosTarget(tosutilPath, target, localPath, data) {
176
53
  }
177
54
  const cred = (0, protocol_1.parseTosUploadCredential)(credRaw, target.dataKey);
178
55
  (0, logger_1.log)('deploy', `Uploading ${target.dir}...`);
179
- uploadDirWithCredential(tosutilPath, localPath, tosutilUploadFromTos(cred), target.dir);
56
+ (0, tosutil_1.uploadDirWithCredential)(tosutilPath, localPath, (0, tosutil_1.tosutilUploadFromTos)(cred), target.dir);
180
57
  return true;
181
58
  }
182
59
  function uploadPaasStorageTarget(tosutilPath, target, localPath, data) {
@@ -188,7 +65,7 @@ function uploadPaasStorageTarget(tosutilPath, target, localPath, data) {
188
65
  }
189
66
  const cred = (0, protocol_1.parsePaasStorageCredential)((0, protocol_1.requireDataKey)(data, target.dataKey), target.dataKey);
190
67
  (0, logger_1.log)('deploy', `Uploading ${target.dir}...`);
191
- uploadDirWithCredential(tosutilPath, localPath, tosutilUploadFromPaasStorage(cred, target.dataKey), target.dir);
68
+ (0, tosutil_1.uploadDirWithCredential)(tosutilPath, localPath, tosutilUploadFromPaasStorage(cred, target.dataKey), target.dir);
192
69
  return cred;
193
70
  }
194
71
  /**
@@ -202,7 +79,7 @@ function uploadPaasStorageTarget(tosutilPath, target, localPath, data) {
202
79
  * 目录不存在的可选项跳过;output 不存在或凭证缺失抛错。
203
80
  */
204
81
  async function uploadArtifacts(opts) {
205
- const tosutilPath = resolveTosutilPath();
82
+ const tosutilPath = (0, tosutil_1.resolveTosutilPath)();
206
83
  const distDir = node_path_1.default.join(opts.projectDir, constants_1.DIST_DIR);
207
84
  const outcome = { uploaded: 0 };
208
85
  for (const target of UPLOAD_PLAN) {
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.designLocalPublishPipeline = designLocalPublishPipeline;
4
+ const spark_meta_1 = require("../../../../utils/spark-meta");
5
+ const error_1 = require("../../../../utils/error");
6
+ const logger_1 = require("../../../../utils/logger");
7
+ const index_1 = require("../atoms/index");
8
+ /**
9
+ * design(design_local_deploy)本地构建 + 本地部署 pipeline。
10
+ *
11
+ * 与 client 版差异:build 走 runDesignBuild、upload 走 uploadDesignArtifacts(version+latest
12
+ * 双写、latest 精确镜像)、不注册插件能力(design-html 无 capability)。其余复用同一套 atom。
13
+ */
14
+ async function designLocalPublishPipeline(opts) {
15
+ const ctx = (0, index_1.prepareDeployContext)(opts.projectDir, opts.appId);
16
+ (0, logger_1.log)('deploy', 'Pre-releasing...');
17
+ const pre = await (0, index_1.preRelease)(ctx.appId, ctx.templateKey);
18
+ (0, logger_1.log)('deploy', `pre_release_id=${pre.preReleaseID} version=${pre.version}`);
19
+ if (pre.data === undefined) {
20
+ throw new error_1.AppError('DEPLOY_PRE_RELEASE_DATA_MISSING', 'preRelease did not return a `data` map — backend tcc may not be configured for current template_key');
21
+ }
22
+ const data = pre.data;
23
+ if (!opts.skipBuild) {
24
+ (0, index_1.runDesignBuild)({ projectDir: ctx.projectDir });
25
+ }
26
+ (0, logger_1.log)('deploy', 'Creating local release...');
27
+ const release = await (0, index_1.createLocalRelease)(ctx.appId, pre.version);
28
+ try {
29
+ await (0, index_1.uploadDesignArtifacts)({ projectDir: ctx.projectDir, data });
30
+ await (0, index_1.finalizeLocalRelease)(ctx.appId, release.releaseID, index_1.LocalReleaseStatus.Finished);
31
+ }
32
+ catch (err) {
33
+ await (0, index_1.finalizeLocalRelease)(ctx.appId, release.releaseID, index_1.LocalReleaseStatus.Failed);
34
+ throw err;
35
+ }
36
+ if (release.onlineUrl !== undefined && release.onlineUrl !== '') {
37
+ (0, spark_meta_1.writeSparkMeta)(ctx.projectDir, { appUrl: release.onlineUrl });
38
+ }
39
+ (0, logger_1.log)('deploy', 'Deployed successfully');
40
+ return {
41
+ appId: ctx.appId,
42
+ version: pre.version,
43
+ url: release.onlineUrl ?? '',
44
+ releaseID: release.releaseID,
45
+ preReleaseID: pre.preReleaseID,
46
+ };
47
+ }
@@ -1,5 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.localBuildLocalPublishPipeline = void 0;
3
+ exports.designLocalPublishPipeline = exports.localBuildLocalPublishPipeline = void 0;
4
4
  var local_1 = require("./local");
5
5
  Object.defineProperty(exports, "localBuildLocalPublishPipeline", { enumerable: true, get: function () { return local_1.localBuildLocalPublishPipeline; } });
6
+ var design_local_1 = require("./design-local");
7
+ Object.defineProperty(exports, "designLocalPublishPipeline", { enumerable: true, get: function () { return design_local_1.designLocalPublishPipeline; } });
@@ -33,6 +33,13 @@ exports.DataKey = {
33
33
  // downloadURLPrefix 替代原 static_cdn_prefix 注入 build env。
34
34
  /** dist/output_static 上传凭证(仅当目录存在时使用) */
35
35
  OUTPUT_STATIC_PAAS_STORAGE_CREDENTIAL: 'output_static_paas_storage_credential',
36
+ // ── design 部署专用(design_local_deploy) ──
37
+ /** 覆盖整个 {app_id} 文件夹的上传凭证(形态同 TosUploadCredential) */
38
+ OUTPUT_ALL_TOS_UPLOAD_CREDENTIAL: 'output_all_tos_upload_credential',
39
+ /** version/备份份真实路径(后端已替换 {app_id}/{version}) */
40
+ OUTPUT_BACKUP_PATH: 'output_backup_path',
41
+ /** latest 份真实路径(后端已替换 {app_id}/latest) */
42
+ OUTPUT_LATEST_PATH: 'output_latest_path',
36
43
  };
37
44
  /** 取必传 key,缺失抛 AppError(带 key 名,便于后端 tcc 配置排查) */
38
45
  function requireDataKey(data, key) {
@@ -1,13 +1,19 @@
1
1
  "use strict";
2
2
  // modern deploy 主入口(dispatch 层)。
3
3
  //
4
- // 当前阶段(Scope A)只接入"本地构建 + 本地部署"一条 pipeline
5
- // 后续接入"本地构建 + 远端部署 / 远端构建 + 远端部署"时,在此处按 template_key
6
- // 或显式 flag 选不同 pipeline。pipeline 之间彼此独立、不共享中间状态。
4
+ // templateKey pipeline:design_local_deploy → design pipeline,其余 → client pipeline。
5
+ // 仅为路由轻量解析 templateKey(读 meta.stack + 映射),不跑前置检查——前置检查在各
6
+ // pipeline 内部的 prepareDeployContext 里做。client 分支与原行为完全一致。
7
+ // pipeline 之间彼此独立、不共享中间状态。
7
8
  Object.defineProperty(exports, "__esModule", { value: true });
8
9
  exports.runModernDeploy = runModernDeploy;
10
+ const spark_meta_1 = require("../../../utils/spark-meta");
11
+ const template_key_map_1 = require("./template-key-map");
9
12
  const index_1 = require("./pipelines/index");
10
13
  async function runModernDeploy(opts) {
11
- // 暂只支持本地构建 + 本地部署
14
+ const stack = (0, spark_meta_1.readSparkMeta)(opts.projectDir).stack ?? '';
15
+ if ((0, template_key_map_1.resolveTemplateKey)(stack) === template_key_map_1.DESIGN_LOCAL_DEPLOY) {
16
+ return (0, index_1.designLocalPublishPipeline)(opts);
17
+ }
12
18
  return (0, index_1.localBuildLocalPublishPipeline)(opts);
13
19
  }
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DESIGN_LOCAL_DEPLOY = void 0;
3
4
  exports.resolveTemplateKey = resolveTemplateKey;
4
5
  /**
5
6
  * 本地 stack 短名 → 后端发布模板 key 的映射。
@@ -10,9 +11,12 @@ exports.resolveTemplateKey = resolveTemplateKey;
10
11
  * 未来出现需要走别的 templateKey 的 stack,再在表里加一行覆盖默认。
11
12
  */
12
13
  const DEFAULT_TEMPLATE_KEY = 'client_local_deploy';
14
+ /** design 部署 templateKey(run.ts 据此分流到 design pipeline) */
15
+ exports.DESIGN_LOCAL_DEPLOY = 'design_local_deploy';
13
16
  const TEMPLATE_KEY_MAP = {
14
17
  'vite-react': DEFAULT_TEMPLATE_KEY,
15
18
  html: DEFAULT_TEMPLATE_KEY,
19
+ 'design-html': exports.DESIGN_LOCAL_DEPLOY,
16
20
  };
17
21
  /**
18
22
  * 把本地 stack 短名映射到后端 templateKey;未配置的 stack 走默认 client_local_deploy。
@@ -0,0 +1,19 @@
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.ensureLogsDir = ensureLogsDir;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ /**
10
+ * 确保 user app 根下存在 `logs/` 目录(幂等)。
11
+ *
12
+ * 模板的 dev.js / dev-local.js 自己也会 mkdir LOG_DIR,但只在进程启动后才执行 —— AI / 用户
13
+ * 在 dev 启动前跑 `npm run dev > logs/dev.log 2>&1` 这种 shell redirect 时,shell 先 open
14
+ * 目标文件,父目录不存在直接 ENOENT,子进程根本不会启动。init / sync 落地阶段先把空目录建出来,
15
+ * 避免这种「目录不存在导致 redirect 失败」的低级问题。
16
+ */
17
+ function ensureLogsDir(targetDir) {
18
+ node_fs_1.default.mkdirSync(node_path_1.default.join(targetDir, 'logs'), { recursive: true });
19
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/miaoda-cli",
3
- "version": "0.1.17",
3
+ "version": "0.1.18-alpha.5fd4656",
4
4
  "description": "Miaoda 平台命令行工具,面向 Agent 调用",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -0,0 +1,70 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ # design-html 的「构建」= 打包,不是编译。
5
+ # 1. 把源文件原样拷进 dist/output/(保留目录层级),排除工具/公共文件;
6
+ # 2. 扫描 .html 生成 dist/output/routes.json(schema 与 vite-react 对齐:[{ path }])。
7
+ # 无 bundler、无 transpile —— 源码即产物。
8
+
9
+ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
10
+ OUTPUT="$ROOT/dist/output"
11
+
12
+ # basePath:平台注入 CLIENT_BASE_PATH 作为路由前缀(如 /app/<id>);
13
+ # 本地裸跑不设则为空(routes 退到 '/')。
14
+ BASE_PATH="${CLIENT_BASE_PATH:-}"
15
+
16
+ # 不进产物的公共 / 工具文件(rsync 模式,相对 ROOT)
17
+ EXCLUDES=(
18
+ ".git" "node_modules" "dist" "scripts"
19
+ "package.json" "package-lock.json" "pnpm-lock.yaml" "yarn.lock"
20
+ ".gitignore" ".npmrc" ".agent" ".env" ".env.local"
21
+ "README.md" ".DS_Store" ".spark"
22
+ )
23
+
24
+ # 清理 & 拷贝
25
+ rm -rf "$ROOT/dist"
26
+ mkdir -p "$OUTPUT"
27
+
28
+ rsync_args=(-a)
29
+ for e in "${EXCLUDES[@]}"; do rsync_args+=(--exclude "$e"); done
30
+ rsync "${rsync_args[@]}" "$ROOT/" "$OUTPUT/"
31
+
32
+ # 生成 routes.json
33
+ OUTPUT="$OUTPUT" BASE_PATH="$BASE_PATH" node --input-type=module <<'NODE'
34
+ import { readdirSync, statSync, writeFileSync } from 'node:fs';
35
+ import { join, relative, sep } from 'node:path';
36
+
37
+ const out = process.env.OUTPUT;
38
+ const base = (process.env.BASE_PATH || '').replace(/\/+$/, '');
39
+
40
+ const htmls = [];
41
+ (function walk(dir) {
42
+ for (const name of readdirSync(dir)) {
43
+ const p = join(dir, name);
44
+ if (statSync(p).isDirectory()) walk(p);
45
+ else if (name.toLowerCase().endsWith('.html')) htmls.push(relative(out, p));
46
+ }
47
+ })(out);
48
+
49
+ function toRoute(rel) {
50
+ let r = '/' + rel.split(sep).join('/'); // '/about.html' | '/blog/index.html'
51
+ r = r.replace(/\.html$/i, ''); // '/about' | '/blog/index'
52
+ r = r.replace(/(^|\/)index$/, '$1'); // '/about' | '/blog/'
53
+ if (r.length > 1) r = r.replace(/\/$/, ''); // 去掉非根的尾斜杠 → '/blog'
54
+ const full = base + r;
55
+ return full === '' ? '/' : full;
56
+ }
57
+
58
+ const routes = [...new Set(htmls.map(toRoute))]
59
+ .sort()
60
+ .map((path) => ({ path }));
61
+
62
+ // 空白画布或无 html 时给一个默认根路由,保证 routes.json 始终可用
63
+ if (routes.length === 0) routes.push({ path: base ? `${base}/` : '/' });
64
+
65
+ writeFileSync(join(out, 'routes.json'), JSON.stringify(routes, null, 2) + '\n');
66
+ console.log(`Generated routes.json (${routes.length} routes)`);
67
+ NODE
68
+
69
+ echo "Build complete"
70
+ echo " Output → dist/output/ (+ routes.json)"
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env sh
2
2
  [ "$SKIP_GIT_HOOKS" = "1" ] && exit 0
3
+
3
4
  export PATH="node_modules/.bin:$PATH"
4
5
  npm run precommit