@lark-apaas/miaoda-cli 0.1.4-alpha.b2fdfff → 0.1.4-alpha.dbe45ea

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.
@@ -6,7 +6,7 @@ const index_1 = require("../../../cli/handlers/app/index");
6
6
  const error_1 = require("../../../utils/error");
7
7
  function registerAppCommands(program, opts = {}) {
8
8
  const description = opts.includeInit
9
- ? '应用元数据管理:查看 / 修改 / 初始化'
9
+ ? '应用元数据管理:查看 / 修改 / 初始化 / 同步平台脚本'
10
10
  : '应用元数据管理:查看 / 修改';
11
11
  const appCmd = program.command('app').description(description);
12
12
  appCmd.action(() => {
@@ -21,8 +21,35 @@ function registerAppCommands(program, opts = {}) {
21
21
  registerAppUpdate(appCmd);
22
22
  if (opts.includeInit) {
23
23
  registerAppInit(appCmd);
24
+ registerAppSync(appCmd);
24
25
  }
25
26
  }
27
+ function registerAppSync(parent) {
28
+ const cmd = parent
29
+ .command('sync')
30
+ .description('按 stack 同步 platform-controlled/ 下的平台管控内容(scripts / package.json 字段等)到项目')
31
+ .option('--dir <path>', '项目目录,默认 cwd(需含 .spark/meta.json)')
32
+ .addHelpText('after', `
33
+ 流程
34
+ 1. 读 .spark/meta.json.stack 决定从 platform-controlled/<stack>/ 拉取
35
+ 2. files/ → 按相对路径覆盖到 <dir>(.sh 自动 +x)
36
+ 3. patches/ → 每个 .json 文件按相对路径 deep merge 到 <dir>/<rel-path>
37
+ (支持任意 JSON:package.json / tsconfig.json / nest-cli.json / ...)
38
+
39
+ JSON 输出
40
+ {"data": {"stack": "...", "syncedFiles": [...],
41
+ "mergedJsonFiles": [{"path": "...", "keys": [...]}]}}
42
+
43
+ 示例
44
+ $ miaoda app sync
45
+ $ miaoda app sync --dir /path/to/app --json
46
+ `);
47
+ cmd.action((0, shared_1.withHelp)(cmd, async (rawOpts) => {
48
+ await (0, index_1.handleAppSync)({
49
+ dir: rawOpts.dir,
50
+ });
51
+ }));
52
+ }
26
53
  function registerAppGet(parent) {
27
54
  const cmd = parent
28
55
  .command('get')
@@ -71,7 +98,6 @@ function registerAppInit(parent) {
71
98
  const cmd = parent
72
99
  .command('init')
73
100
  .description('初始化应用代码:抓 template 渲染、装 .agent/steering/ skills、写 .spark/meta.json。.spark/meta.json 已存在则直接退出')
74
- .addOption((0, shared_1.appIdOption)().hideHelp())
75
101
  .option('--template <stack>', `技术栈短名(${index_1.SUPPORTED_STACKS.join(' / ')})`)
76
102
  .option('--conf <json>', 'init 配置 JSON。支持 {"version": "<template 版本>"} / {"steeringVersion": "<steering 版本>"},均默认 latest')
77
103
  .option('--skip-install', '跳过依赖安装', false)
@@ -82,6 +108,10 @@ function registerAppInit(parent) {
82
108
  meta.json 在 init 流程最后才写,写了 = 已完整成功一次;
83
109
  上一次失败留下半渲染状态时(package.json 在但 meta.json 没在)允许重跑。
84
110
 
111
+ 应用身份(appId)
112
+ init 不再持久化 appId。运行端命令(deploy / file / plugin 等)始终从 MIAODA_APP_ID env 读,
113
+ init 阶段也无需感知 appId。
114
+
85
115
  依赖安装
86
116
  默认:npm install --no-audit --no-fund
87
117
  --skip-install: 跳过
@@ -104,10 +134,8 @@ JSON 输出
104
134
  $ MIAODA_DEP_CACHE_DIR=/tmp/dep-cache miaoda app init --template vite-react
105
135
  `);
106
136
  cmd.action((0, shared_1.withHelp)(cmd, async (rawOpts) => {
107
- (0, shared_1.rejectCliOverride)(cmd, 'appId');
108
137
  const conf = parseInitConf(rawOpts.conf);
109
138
  await (0, index_1.handleAppInit)({
110
- appId: (0, shared_1.resolveAppId)({ appId: rawOpts.appId }),
111
139
  template: rawOpts.template,
112
140
  conf,
113
141
  skipInstall: rawOpts.skipInstall,
@@ -20,7 +20,8 @@ function registerDeployCommandsModern(program) {
20
20
  不要用异步模式或后台模式调用 deploy,否则调用可能提前结束,Agent 会误判发布已完成。
21
21
 
22
22
  前置要求
23
- - 项目已 init(.spark/meta.json 含 appId / template
23
+ - 项目已 init(.spark/meta.json 含 stack
24
+ - MIAODA_APP_ID 已设置(应用身份从 env 读,不再从 meta.json 读)
24
25
  - node_modules 已安装
25
26
  - package.json 含 build script
26
27
  - 沙箱内预装 tosutil(用于把构建产物上传到 TOS)
@@ -42,6 +43,7 @@ JSON 输出(stdout)
42
43
  deployCmd.action((0, shared_1.withHelp)(deployCmd, async (rawOpts) => {
43
44
  await (0, modern_1.handleDeployModern)({
44
45
  dir: rawOpts.dir ?? '.',
46
+ appId: (0, shared_1.resolveAppId)({}),
45
47
  skipBuild: rawOpts.skipBuild,
46
48
  });
47
49
  }));
@@ -9,9 +9,21 @@ const index_4 = require("../../cli/commands/app/index");
9
9
  const index_5 = require("../../cli/commands/deploy/index");
10
10
  const modern_1 = require("../../cli/commands/deploy/modern");
11
11
  const index_6 = require("../../cli/commands/skills/index");
12
+ // scene 跟 dispatcher(MIAODA_APP_TYPE)同语义层级 —— app 业务类型维度,
13
+ // 对齐后端 devops app_common.AppType 枚举:
14
+ // 3 = AppType_APPLICATION(全栈应用,当前仅 nestjs-react-fullstack 一个 stack)
15
+ // 4 = AppType_DESIGN(design-stack,SSR 渲染、无业务逻辑、无数据库)
16
+ // 7 = miaoda-cli 自定义 modern 占位(后端枚举无 7,沙箱目前不传)
17
+ // 其它(0/1/2/5/6 / 未设)→ default(命令全开,本地 dev / CI / 兼容回退)
18
+ // stack 维度(nestjs-react-fullstack / vite-react / ...)是正交的,
19
+ // skills sync 内部按 .spark/meta.json.stack 拉对应 coding-steering/steering/<stack>/ 子目录。
12
20
  function resolveScene(appType, _archType) {
13
21
  if (appType === '7')
14
22
  return 'modern';
23
+ if (appType === '3')
24
+ return 'application';
25
+ if (appType === '4')
26
+ return 'design';
15
27
  return 'default';
16
28
  }
17
29
  const SCENE_REGISTRARS = {
@@ -27,6 +39,25 @@ const SCENE_REGISTRARS = {
27
39
  (0, modern_1.registerDeployCommandsModern)(p);
28
40
  (0, index_6.registerSkillsCommands)(p);
29
41
  },
42
+ // application scene(AppType_APPLICATION=3):
43
+ // 在 default 命令集基础上加 skills(按 meta.json.stack 拉对应 stack 技能)
44
+ // + app init(新一代应用走 init 写 .spark/meta.json)
45
+ application: (p) => {
46
+ (0, index_4.registerAppCommands)(p, { includeInit: true });
47
+ (0, index_5.registerDeployCommands)(p);
48
+ (0, index_2.registerDbCommands)(p);
49
+ (0, index_1.registerFileCommands)(p);
50
+ (0, index_3.registerObservabilityCommands)(p);
51
+ (0, index_6.registerSkillsCommands)(p);
52
+ },
53
+ // design scene(AppType_DESIGN=4):design-stack 仅 SSR 渲染、无后端业务逻辑、
54
+ // 无数据库、无 UGC 文件。命令集裁掉 db / file,保留 deploy / observability / app(init) / skills。
55
+ design: (p) => {
56
+ (0, index_4.registerAppCommands)(p, { includeInit: true });
57
+ (0, index_5.registerDeployCommands)(p);
58
+ (0, index_3.registerObservabilityCommands)(p);
59
+ (0, index_6.registerSkillsCommands)(p);
60
+ },
30
61
  };
31
62
  function readEnv(name) {
32
63
  const v = process.env[name]?.trim();
@@ -1,12 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SUPPORTED_STACKS = exports.handleAppInit = exports.handleAppUpdate = exports.handleAppGet = void 0;
3
+ exports.SUPPORTED_STACKS = exports.handleAppSync = exports.handleAppInit = exports.handleAppUpdate = exports.handleAppGet = void 0;
4
4
  var get_1 = require("./get");
5
5
  Object.defineProperty(exports, "handleAppGet", { enumerable: true, get: function () { return get_1.handleAppGet; } });
6
6
  var update_1 = require("./update");
7
7
  Object.defineProperty(exports, "handleAppUpdate", { enumerable: true, get: function () { return update_1.handleAppUpdate; } });
8
8
  var init_1 = require("./init");
9
9
  Object.defineProperty(exports, "handleAppInit", { enumerable: true, get: function () { return init_1.handleAppInit; } });
10
+ var sync_1 = require("./sync");
11
+ Object.defineProperty(exports, "handleAppSync", { enumerable: true, get: function () { return sync_1.handleAppSync; } });
10
12
  // commands 层渲染 help 时需要这份枚举;从 handler barrel 转发,避免 commands → services 越界
11
13
  var index_1 = require("../../../services/app/init/index");
12
14
  Object.defineProperty(exports, "SUPPORTED_STACKS", { enumerable: true, get: function () { return index_1.SUPPORTED_STACKS; } });
@@ -10,13 +10,19 @@ const index_1 = require("../../../services/app/init/index");
10
10
  const coding_steering_1 = require("../../../utils/coding-steering");
11
11
  const error_1 = require("../../../utils/error");
12
12
  const output_1 = require("../../../utils/output");
13
- /** miaoda app init [--template <stack>] [--conf <json>] */
13
+ /**
14
+ * miaoda app init
15
+ *
16
+ * 只负责本地脚手架:render template、sync skills、install deps,把 {stack, version, archType}
17
+ * 写入 .spark/meta.json。appId 不在此处持久化——deploy 等运行端通过 MIAODA_APP_ID env 拿。
18
+ *
19
+ * 幂等:.spark/meta.json 存在即表示已完整成功一次,直接退出,不重跑任何子步骤。
20
+ * 半渲染状态(package.json 在但 meta.json 不在)会重新跑完整流程。
21
+ */
14
22
  async function handleAppInit(opts) {
15
23
  await Promise.resolve();
16
24
  const targetDir = opts.targetDir ?? process.cwd();
17
25
  const metaPath = node_path_1.default.join(targetDir, '.spark', 'meta.json');
18
- // 幂等:.spark/meta.json 是 init 流程最后才写的,存在即代表"已完整成功一次"。
19
- // 用 package.json 判幂等会让 install 失败、CWD 留下半渲染状态的应用永远 init 不了。
20
26
  if (node_fs_1.default.existsSync(metaPath)) {
21
27
  if (!(0, output_1.isJsonMode)()) {
22
28
  process.stdout.write(`Already initialized: ${metaPath}\n`);
@@ -56,7 +62,6 @@ async function handleAppInit(opts) {
56
62
  quietStdout: (0, output_1.isJsonMode)(),
57
63
  });
58
64
  (0, index_1.writeSparkMeta)(targetDir, {
59
- appId: opts.appId,
60
65
  stack,
61
66
  version: tplResult.version,
62
67
  archType: tplResult.archType,
@@ -67,7 +72,6 @@ async function handleAppInit(opts) {
67
72
  (0, output_1.emit)({
68
73
  data: {
69
74
  initialized: true,
70
- appId: opts.appId,
71
75
  template: stack,
72
76
  templatePackage: tplResult.packageName,
73
77
  templateVersion: tplResult.version,
@@ -0,0 +1,45 @@
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.handleAppSync = handleAppSync;
7
+ const node_path_1 = __importDefault(require("node:path"));
8
+ const platform_sync_1 = require("../../../utils/platform-sync");
9
+ const spark_meta_1 = require("../../../utils/spark-meta");
10
+ const error_1 = require("../../../utils/error");
11
+ const output_1 = require("../../../utils/output");
12
+ /**
13
+ * miaoda app sync [--dir <path>]
14
+ *
15
+ * 从 platform-controlled/<stack>/ 同步平台管控内容到项目:
16
+ * - files/ → 按相对路径覆盖
17
+ * - package.json → deep merge patch
18
+ *
19
+ * 需要 .spark/meta.json 已含 stack(走过 `miaoda app init`)。
20
+ * 不动 .agent/skills/(那是 `miaoda skills sync` 的事)。
21
+ */
22
+ async function handleAppSync(opts) {
23
+ await Promise.resolve();
24
+ const targetDir = node_path_1.default.resolve(opts.dir ?? process.cwd());
25
+ const meta = (0, spark_meta_1.readSparkMeta)(targetDir);
26
+ if (meta.stack === undefined || meta.stack === '') {
27
+ throw new error_1.AppError('SYNC_META_INCOMPLETE', '.spark/meta.json missing stack — run `miaoda app init` first');
28
+ }
29
+ const result = (0, platform_sync_1.syncPlatformControlled)({
30
+ stack: meta.stack,
31
+ targetDir,
32
+ });
33
+ if (!result.stackFound) {
34
+ throw new error_1.AppError('SYNC_STACK_NOT_SUPPORTED', `platform-controlled/${meta.stack}/ not found — 该 stack 暂未纳入 app sync 范围`, {
35
+ next_actions: ['在 miaoda-cli 仓 platform-controlled/ 下添加该 stack 目录,然后重新发版'],
36
+ });
37
+ }
38
+ (0, output_1.emit)({
39
+ data: {
40
+ stack: meta.stack,
41
+ syncedFiles: result.syncedFiles,
42
+ mergedJsonFiles: result.mergedJsonFiles,
43
+ },
44
+ });
45
+ }
@@ -16,6 +16,7 @@ async function handleDeployModern(opts) {
16
16
  const projectDir = node_path_1.default.resolve(opts.dir);
17
17
  const result = await (0, index_1.runModernDeploy)({
18
18
  projectDir,
19
+ appId: opts.appId,
19
20
  skipBuild: opts.skipBuild ?? false,
20
21
  });
21
22
  (0, output_1.emit)({
@@ -6,23 +6,20 @@ const spark_meta_1 = require("../../../../utils/spark-meta");
6
6
  const check_1 = require("../check");
7
7
  const template_key_map_1 = require("../template-key-map");
8
8
  /**
9
- * 准备 deploy 上下文:跑前置检查、读 .spark/meta.json、抽出 appId / stack,
10
- * 并把 stack 短名映射成后端 templateKey。
9
+ * 准备 deploy 上下文:跑前置检查、读 .spark/meta.json stack,
10
+ * 并把 stack 短名映射成后端 templateKey。appId 由调用方从 env 解析后传入。
11
11
  *
12
12
  * 任何前置失败统一抛 AppError。返回值是后续 atom 唯一信任的入参源。
13
13
  */
14
- function prepareDeployContext(projectDir) {
14
+ function prepareDeployContext(projectDir, appId) {
15
15
  (0, check_1.runDeployChecks)(projectDir);
16
16
  const meta = (0, spark_meta_1.readSparkMeta)(projectDir);
17
- if (meta.appId === undefined || meta.appId === '') {
18
- throw new error_1.AppError('DEPLOY_META_INCOMPLETE', '.spark/meta.json missing appId — run `miaoda app init` first');
19
- }
20
17
  if (meta.stack === undefined || meta.stack === '') {
21
18
  throw new error_1.AppError('DEPLOY_META_INCOMPLETE', '.spark/meta.json missing stack — run `miaoda app init` first');
22
19
  }
23
20
  return {
24
21
  projectDir,
25
- appId: meta.appId,
22
+ appId,
26
23
  stack: meta.stack,
27
24
  templateKey: (0, template_key_map_1.resolveTemplateKey)(meta.stack),
28
25
  meta,
@@ -24,13 +24,8 @@ function runDeployChecks(projectDir) {
24
24
  else {
25
25
  try {
26
26
  const meta = JSON.parse(node_fs_1.default.readFileSync(metaPath, 'utf-8'));
27
- const missing = [];
28
- if (!meta.appId)
29
- missing.push('appId');
30
- if (!meta.stack)
31
- missing.push('stack');
32
- if (missing.length > 0) {
33
- issues.push(`.spark/meta.json missing fields: ${missing.join(', ')}`);
27
+ if (!meta.stack) {
28
+ issues.push('.spark/meta.json missing fields: stack');
34
29
  }
35
30
  }
36
31
  catch {
@@ -22,7 +22,7 @@ const index_1 = require("../atoms/index");
22
22
  * 任一步失败都会精准把该发布单标记为 Failed,避免遗留挂态。
23
23
  */
24
24
  async function localBuildLocalPublishPipeline(opts) {
25
- const ctx = (0, index_1.prepareDeployContext)(opts.projectDir);
25
+ const ctx = (0, index_1.prepareDeployContext)(opts.projectDir, opts.appId);
26
26
  (0, logger_1.log)('deploy', 'Pre-releasing...');
27
27
  const pre = await (0, index_1.preRelease)(ctx.appId, ctx.templateKey);
28
28
  (0, logger_1.log)('deploy', `pre_release_id=${pre.preReleaseID} version=${pre.version}`);
@@ -62,7 +62,7 @@ async function localBuildLocalPublishPipeline(opts) {
62
62
  throw err;
63
63
  }
64
64
  if (release.onlineUrl !== undefined && release.onlineUrl !== '') {
65
- (0, spark_meta_1.writeSparkMeta)(ctx.projectDir, { ...ctx.meta, appUrl: release.onlineUrl });
65
+ (0, spark_meta_1.writeSparkMeta)(ctx.projectDir, { appUrl: release.onlineUrl });
66
66
  }
67
67
  (0, logger_1.log)('deploy', 'Deployed successfully');
68
68
  return {
@@ -9,6 +9,9 @@ const node_path_1 = __importDefault(require("node:path"));
9
9
  const npm_pack_1 = require("../utils/npm-pack");
10
10
  const logger_1 = require("../utils/logger");
11
11
  const STEERING_PACKAGE = '@lark-apaas/coding-steering';
12
+ // 临时:本地测试 alpha 链路用,默认 pin 到 0.1.6-alpha.0
13
+ // (用户显式传 version,例如 `miaoda skills sync latest`,仍会 override 这个默认值)
14
+ const DEFAULT_PINNED_VERSION = '0.1.6-alpha.0';
12
15
  /**
13
16
  * 把 coding-steering 包内 _common/skills + <stack>/skills 同步到 <targetDir>/.agent/steering/skills/,
14
17
  * 同名 stack 子目录覆盖 _common。tech.md 存在时也同步到 .agent/steering/tech.md。
@@ -18,10 +21,11 @@ const STEERING_PACKAGE = '@lark-apaas/coding-steering';
18
21
  */
19
22
  function syncCodingSteering(opts) {
20
23
  const logPrefix = opts.logPrefix ?? 'skills';
21
- (0, logger_1.log)(logPrefix, `Fetching ${STEERING_PACKAGE}@${opts.version ?? 'latest'}...`);
24
+ const effectiveVersion = opts.version ?? DEFAULT_PINNED_VERSION;
25
+ (0, logger_1.log)(logPrefix, `Fetching ${STEERING_PACKAGE}@${effectiveVersion}...`);
22
26
  const fetched = (0, npm_pack_1.fetchNpmPackage)({
23
27
  packageName: STEERING_PACKAGE,
24
- version: opts.version,
28
+ version: effectiveVersion,
25
29
  });
26
30
  try {
27
31
  const steeringRoot = node_path_1.default.join(fetched.extractDir, 'steering');
@@ -0,0 +1,114 @@
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.syncPlatformControlled = syncPlatformControlled;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const logger_1 = require("../utils/logger");
10
+ function syncPlatformControlled(opts) {
11
+ const logPrefix = opts.logPrefix ?? 'app-sync';
12
+ const cliRoot = opts.cliRoot ?? resolveCliRoot();
13
+ const stackDir = node_path_1.default.join(cliRoot, 'platform-controlled', opts.stack);
14
+ if (!node_fs_1.default.existsSync(stackDir)) {
15
+ return { stackFound: false, syncedFiles: [], mergedJsonFiles: [] };
16
+ }
17
+ (0, logger_1.log)(logPrefix, `Syncing platform-controlled/${opts.stack}/ → ${opts.targetDir}`);
18
+ const syncedFiles = syncFilesDir(node_path_1.default.join(stackDir, 'files'), opts.targetDir, logPrefix);
19
+ const mergedJsonFiles = applyJsonPatches(node_path_1.default.join(stackDir, 'patches'), opts.targetDir, logPrefix);
20
+ return {
21
+ stackFound: true,
22
+ syncedFiles,
23
+ mergedJsonFiles,
24
+ };
25
+ }
26
+ /** 推断 cli 安装根:dist/utils/platform-sync.js 上溯两级即仓根 */
27
+ function resolveCliRoot() {
28
+ return node_path_1.default.resolve(__dirname, '..', '..');
29
+ }
30
+ /** 递归同步 srcDir 下所有文件到 destDir,保持相对路径;.sh 文件 +x。返回 dest 相对路径列表 */
31
+ function syncFilesDir(srcDir, destDir, logPrefix) {
32
+ if (!node_fs_1.default.existsSync(srcDir))
33
+ return [];
34
+ const synced = [];
35
+ walk(srcDir, srcDir, destDir, (rel, srcPath, destPath) => {
36
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(destPath), { recursive: true });
37
+ node_fs_1.default.copyFileSync(srcPath, destPath);
38
+ if (rel.endsWith('.sh')) {
39
+ node_fs_1.default.chmodSync(destPath, 0o755);
40
+ }
41
+ synced.push(rel);
42
+ (0, logger_1.log)(logPrefix, ` + ${rel}`);
43
+ });
44
+ return synced;
45
+ }
46
+ /** 递归遍历 patchesDir 下每个 JSON 文件,deep merge 到 destDir 对应路径 */
47
+ function applyJsonPatches(patchesDir, destDir, logPrefix) {
48
+ if (!node_fs_1.default.existsSync(patchesDir))
49
+ return [];
50
+ const merged = [];
51
+ walk(patchesDir, patchesDir, destDir, (rel, patchPath, destPath) => {
52
+ if (!rel.endsWith('.json')) {
53
+ (0, logger_1.log)(logPrefix, ` (skip non-json patch: ${rel})`);
54
+ return;
55
+ }
56
+ if (!node_fs_1.default.existsSync(destPath)) {
57
+ throw new Error(`[${logPrefix}] target JSON not found: ${destPath} ` +
58
+ `(patches/${rel} 要求项目根含同路径文件;app init 应该先创建它)`);
59
+ }
60
+ const patch = readJson(patchPath);
61
+ const target = readJson(destPath);
62
+ if (!isPlainObject(patch) || !isPlainObject(target)) {
63
+ throw new Error(`[${logPrefix}] patch 与 target 必须都是 JSON object: ${rel}`);
64
+ }
65
+ const changed = [];
66
+ for (const key of Object.keys(patch)) {
67
+ const before = JSON.stringify(target[key]);
68
+ target[key] = deepMerge(target[key], patch[key]);
69
+ const after = JSON.stringify(target[key]);
70
+ if (before !== after)
71
+ changed.push(key);
72
+ }
73
+ if (changed.length > 0) {
74
+ node_fs_1.default.writeFileSync(destPath, JSON.stringify(target, null, 2) + '\n', 'utf-8');
75
+ merged.push({ path: rel, keys: changed });
76
+ (0, logger_1.log)(logPrefix, ` ~ ${rel} (keys: ${changed.join(', ')})`);
77
+ }
78
+ else {
79
+ (0, logger_1.log)(logPrefix, ` = ${rel} (unchanged)`);
80
+ }
81
+ });
82
+ return merged;
83
+ }
84
+ /** 通用递归遍历:对 rootSrc 下每个文件调 visit(rel, srcAbsPath, destAbsPath) */
85
+ function walk(rootSrc, curSrc, destDir, visit) {
86
+ for (const name of node_fs_1.default.readdirSync(curSrc)) {
87
+ const srcPath = node_path_1.default.join(curSrc, name);
88
+ const stat = node_fs_1.default.statSync(srcPath);
89
+ if (stat.isDirectory()) {
90
+ walk(rootSrc, srcPath, destDir, visit);
91
+ continue;
92
+ }
93
+ const rel = node_path_1.default.relative(rootSrc, srcPath);
94
+ const destPath = node_path_1.default.join(destDir, rel);
95
+ visit(rel, srcPath, destPath);
96
+ }
97
+ }
98
+ function readJson(filePath) {
99
+ return JSON.parse(node_fs_1.default.readFileSync(filePath, 'utf-8'));
100
+ }
101
+ function isPlainObject(v) {
102
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
103
+ }
104
+ /** value 是 object 时递归 merge,其它(包括数组)直接以 patch 覆盖 */
105
+ function deepMerge(target, patch) {
106
+ if (isPlainObject(patch) && isPlainObject(target)) {
107
+ const merged = { ...target };
108
+ for (const k of Object.keys(patch)) {
109
+ merged[k] = deepMerge(target[k], patch[k]);
110
+ }
111
+ return merged;
112
+ }
113
+ return patch;
114
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/miaoda-cli",
3
- "version": "0.1.4-alpha.b2fdfff",
3
+ "version": "0.1.4-alpha.dbe45ea",
4
4
  "description": "Miaoda 平台命令行工具,面向 Agent 调用",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -10,10 +10,11 @@
10
10
  "access": "public"
11
11
  },
12
12
  "files": [
13
+ "LICENSE",
14
+ "README.md",
13
15
  "bin",
14
16
  "dist",
15
- "README.md",
16
- "LICENSE"
17
+ "platform-controlled"
17
18
  ],
18
19
  "keywords": [
19
20
  "miaoda",
@@ -0,0 +1,33 @@
1
+ # platform-controlled/
2
+
3
+ 平台管控的脚手架增量内容,按 stack 分目录,由 `miaoda app sync` 同步到用户项目。
4
+
5
+ ## 目录约定
6
+
7
+ ```
8
+ platform-controlled/
9
+ └── <stack>/
10
+ ├── files/ ← 文件模式:按相对路径整文件覆盖
11
+ │ └── <rel-path> ← 例如 scripts/dev-local.sh → <app>/scripts/dev-local.sh
12
+ └── patches/ ← JSON merge 模式:按相对路径 deep merge(任意 JSON 文件)
13
+ └── <rel-path>.json ← 例如 package.json / tsconfig.json / nest-cli.json
14
+ ```
15
+
16
+ ## 同步行为
17
+
18
+ - **files/**:递归遍历,按相对路径全量覆盖 targetDir。`.sh` 后缀文件同步后自动 `chmod +x`。
19
+ - **patches/**:递归遍历,**每个 `.json` 文件** 都跟 targetDir 同路径文件做 deep merge:
20
+ - 两边都必须是 JSON object(否则报错)
21
+ - value 是 object → 递归 merge;value 是数组 / 标量 → 整体替换
22
+ - 目标文件不存在则报错(patches 是补丁性质,不负责创建文件;app init 应该先创建)
23
+ - patches 下非 `.json` 文件会被跳过(预留给将来其它 patch 类型)
24
+ - 不做用户改动保护(本期约定:`platform-controlled/` 下的文件路径由平台拥有,用户不应手改)。
25
+
26
+ ## 跟 `miaoda skills sync` 的区别
27
+
28
+ | 链路 | 源 | 内容 | 目标 |
29
+ |---|---|---|---|
30
+ | `miaoda skills sync` | NPM 包 `@lark-apaas/coding-steering` | agent skills(`.md`) | `<app>/.agent/skills/steering/<stack>/` |
31
+ | `miaoda app sync` | 本仓 `platform-controlled/<stack>/` | 启动脚本、package.json 字段等 | `<app>/` 项目根 |
32
+
33
+ 两者独立调用,互不依赖。
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env bash
2
+ # ============================================================================
3
+ # 本地 dev 启动脚本(平台控制,通过 `miaoda app sync` 同步,请勿手改)
4
+ # Stack: design-stack(SSR-only,无 server 业务逻辑,无数据库)
5
+ # 流程:env pull → skills sync → 起 dev server
6
+ # ============================================================================
7
+ set -euo pipefail
8
+ ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
9
+ cd "$ROOT_DIR"
10
+
11
+ # Stack 是 design-stack,对应 miaoda CLI 的 design scene
12
+ # 沙箱里这个 env 由平台注入,本地 dev 由本脚本兜底,确保 miaoda 子命令(skills/sync)能看见
13
+ export MIAODA_APP_TYPE="${MIAODA_APP_TYPE:-4}"
14
+
15
+ # 临时:绕 npmmirror/bnpm 内网到 npmjs 的同步延迟,等 coding-steering 正式版去掉
16
+ export MIAODA_NPM_REGISTRY="${MIAODA_NPM_REGISTRY:-https://registry.npmjs.org/}"
17
+
18
+ # 1. 拉本地 dev 所需 ENV(jwt / cookie 等,写入 .env.local)
19
+ echo "[dev-local] (1/3) env pull..."
20
+ if command -v lark-cli >/dev/null 2>&1; then
21
+ lark-cli apps env pull || echo " (env pull 失败,后续按 .env.local 现状继续)"
22
+ else
23
+ echo " (lark-cli 未装,跳过 env pull;请确保 .env.local 已就绪)"
24
+ fi
25
+
26
+ # 2. 同步 stack 对应 skills(coding-steering)
27
+ echo "[dev-local] (2/3) miaoda skills sync..."
28
+ if command -v miaoda >/dev/null 2>&1; then
29
+ miaoda skills sync || echo " (skills sync 失败,继续启动)"
30
+ else
31
+ echo " (miaoda 未装,跳过 skills sync)"
32
+ fi
33
+
34
+ # 3. 起 dev server(design-stack 只有 SSR 渲染,单进程即可;不走 scripts/dev.js 的保活)
35
+ # 如果模板的 npm run dev 本身就是裸进程(无保活)则直接调即可;
36
+ # 如果 design-stack 模板的 dev 也是 dev.js 包装,后续需要新增 dev:raw 之类的直通命令
37
+ echo "[dev-local] (3/3) npm run dev"
38
+ exec npm run dev
@@ -0,0 +1,5 @@
1
+ {
2
+ "scripts": {
3
+ "dev:local": "./scripts/dev-local.sh"
4
+ }
5
+ }
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env bash
2
+ # ============================================================================
3
+ # 本地 dev 启动脚本(平台控制,通过 `miaoda app sync` 同步,请勿手改)
4
+ # Stack: nestjs-react-fullstack
5
+ # 流程:env pull → skills sync → 起 server + client(用户访问 http://localhost:8001)
6
+ # ============================================================================
7
+ set -euo pipefail
8
+ ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
9
+ cd "$ROOT_DIR"
10
+
11
+ # Stack 是 nestjs-react-fullstack(全栈应用),对应 miaoda CLI 的 application scene
12
+ # 沙箱里这个 env 由平台注入,本地 dev 由本脚本兜底,确保 miaoda 子命令(skills/sync)能看见
13
+ export MIAODA_APP_TYPE="${MIAODA_APP_TYPE:-3}"
14
+
15
+ # 临时:绕 npmmirror/bnpm 内网到 npmjs 的同步延迟,等 coding-steering 正式版去掉
16
+ export MIAODA_NPM_REGISTRY="${MIAODA_NPM_REGISTRY:-https://registry.npmjs.org/}"
17
+
18
+ # 1. 拉本地 dev 所需 ENV(jwt / cookie / sandbox URL 等,写入 .env.local)
19
+ echo "[dev-local] (1/3) env pull..."
20
+ if command -v lark-cli >/dev/null 2>&1; then
21
+ lark-cli apps env pull || echo " (env pull 失败,后续按 .env.local 现状继续)"
22
+ else
23
+ echo " (lark-cli 未装,跳过 env pull;请确保 .env.local 已就绪)"
24
+ fi
25
+
26
+ # 2. 同步 stack 对应 skills(coding-steering)
27
+ echo "[dev-local] (2/3) miaoda skills sync..."
28
+ if command -v miaoda >/dev/null 2>&1; then
29
+ miaoda skills sync || echo " (skills sync 失败,继续启动)"
30
+ else
31
+ echo " (miaoda 未装,跳过 skills sync)"
32
+ fi
33
+
34
+ # 3. 并发起 dev:server(NestJS 3000) + dev:client(rspack 8001),用户访问 8001
35
+ # 不走 scripts/dev.js(那有保活 / restart 循环 / 日志写文件,本地 dev 不需要 ——
36
+ # 进程崩了就让它崩,失败立即可见;日志直接 stdout 让 Agent 抓得到)
37
+ echo "[dev-local] (3/3) 并发起 dev:server + dev:client → http://localhost:${CLIENT_DEV_PORT:-8001}/"
38
+ exec npx --no-install concurrently \
39
+ --names "server,client" --prefix-colors "blue,green" --kill-others-on-fail \
40
+ "npm run dev:server" "npm run dev:client"
@@ -0,0 +1,5 @@
1
+ {
2
+ "scripts": {
3
+ "dev:local": "./scripts/dev-local.sh"
4
+ }
5
+ }