@lark-apaas/miaoda-cli 0.1.19-alpha.4bf5458 → 0.1.19-alpha.6dad9cd
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/index.js +3 -0
- package/dist/cli/commands/registry/index.js +97 -0
- package/dist/cli/handlers/app/init.js +6 -2
- package/dist/cli/handlers/registry/add.js +54 -0
- package/dist/cli/handlers/registry/index.js +7 -0
- package/dist/cli/handlers/registry/list.js +48 -0
- package/dist/cli/handlers/registry/shared.js +25 -0
- package/dist/services/registry/index.js +263 -0
- package/package.json +1 -1
|
@@ -9,6 +9,7 @@ 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
|
+
const index_7 = require("../../cli/commands/registry/index");
|
|
12
13
|
// scene 跟 dispatcher(MIAODA_APP_TYPE)同语义层级 —— app 业务类型维度,
|
|
13
14
|
// 对齐后端 devops app_common.AppType 枚举:
|
|
14
15
|
// 3 = AppType_APPLICATION(全栈应用,当前仅 nestjs-react-fullstack 一个 stack)
|
|
@@ -67,10 +68,12 @@ const SCENE_REGISTRARS = {
|
|
|
67
68
|
},
|
|
68
69
|
// design-html scene(AppType_DESIGN_HTML=8):buildless 静态 HTML,
|
|
69
70
|
// 命令集与 modern 一致(app init/sync + modern 拆分版 deploy + skills),不挂 db/file/observability。
|
|
71
|
+
// 额外挂 registry(shadcn 式 copy-in SDK,design-html scene 专属)。
|
|
70
72
|
'design-html': (p) => {
|
|
71
73
|
(0, index_4.registerAppCommands)(p, { includeInit: true });
|
|
72
74
|
(0, modern_1.registerDeployCommandsModern)(p);
|
|
73
75
|
(0, index_6.registerSkillsCommands)(p);
|
|
76
|
+
(0, index_7.registerRegistryCommands)(p);
|
|
74
77
|
},
|
|
75
78
|
};
|
|
76
79
|
function readEnv(name) {
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerRegistryCommands = registerRegistryCommands;
|
|
4
|
+
const shared_1 = require("../../../cli/commands/shared");
|
|
5
|
+
const index_1 = require("../../../cli/handlers/registry/index");
|
|
6
|
+
/**
|
|
7
|
+
* miaoda registry:design-html 技术栈的「组件 registry」(shadcn 式 copy-in)。
|
|
8
|
+
*
|
|
9
|
+
* registry 是独立 npm 包,按 runtime 命名分发(buildless runtime → @lark-apaas/coding-registry-buildless)。
|
|
10
|
+
* SDK 文件按目录组织**平铺在包根**(scripts/ · components/ · stylesheets/),每个文件以 USAGE 注释块
|
|
11
|
+
* 开头(声明描述 / 用法 / Depends)。CLI 抓 tarball、按 .spark/meta.json.stack → runtime → 包名 选包,
|
|
12
|
+
* 把 SDK 文件**保留相对包根的路径** copy-in 到用户项目根。
|
|
13
|
+
* 不写安装记账文件——项目文件树即状态(skip-by-disk)。
|
|
14
|
+
*/
|
|
15
|
+
function registerRegistryCommands(program) {
|
|
16
|
+
const registryCmd = program
|
|
17
|
+
.command('registry')
|
|
18
|
+
.description('组件 registry:列出 / copy-in design-html SDK 文件')
|
|
19
|
+
.usage('<command> [flags]');
|
|
20
|
+
registryCmd.action(() => {
|
|
21
|
+
registryCmd.outputHelp();
|
|
22
|
+
});
|
|
23
|
+
registryCmd.addHelpText('after', `
|
|
24
|
+
概念
|
|
25
|
+
- registry 包:独立 npm 包(按 runtime 命名),SDK 文件平铺在包根(如 scripts/deck-stage.js)
|
|
26
|
+
- 条目 (entry):name = 文件名去扩展名(全树唯一);每个文件以 USAGE 注释块开头
|
|
27
|
+
- USAGE 块:声明描述 / Exports / Usage / Depends(可选,依赖其他 SDK 的 name)
|
|
28
|
+
- Depends:从 USAGE 块解析的跨条目依赖,add 时传递解析、一并 copy-in
|
|
29
|
+
- add 落地:保留文件相对包根的路径(包根 scripts/x.js → 项目 scripts/x.js)
|
|
30
|
+
|
|
31
|
+
前置
|
|
32
|
+
当前目录(或 --dir)已走过 'miaoda app init'(.spark/meta.json 含 stack=design-html)
|
|
33
|
+
`);
|
|
34
|
+
registerRegistryList(registryCmd);
|
|
35
|
+
registerRegistryAdd(registryCmd);
|
|
36
|
+
}
|
|
37
|
+
function registerRegistryList(parent) {
|
|
38
|
+
const cmd = parent
|
|
39
|
+
.command('list')
|
|
40
|
+
.description('列出 registry 可用 SDK 条目,输出各文件的 USAGE 块原文')
|
|
41
|
+
.option('--dir <path>', '项目目录,默认当前目录', '.')
|
|
42
|
+
.option('--version <ver>', 'registry 包版本或 dist-tag,缺省 latest')
|
|
43
|
+
.addHelpText('after', `
|
|
44
|
+
输出
|
|
45
|
+
默认(pretty):各条目 USAGE 块原文拼接,带文件名分隔,给 Agent 直接读注释
|
|
46
|
+
|
|
47
|
+
JSON 输出
|
|
48
|
+
{"data": [{"name": "...", "file": "...", "usage": "<原文>", "dependsOn": [...]}, ...]}
|
|
49
|
+
(usage 保持 USAGE 块原文,不解析成结构化字段)
|
|
50
|
+
|
|
51
|
+
示例
|
|
52
|
+
$ miaoda registry list
|
|
53
|
+
$ miaoda registry list --json
|
|
54
|
+
`);
|
|
55
|
+
cmd.action((0, shared_1.withHelp)(cmd, async (rawOpts) => {
|
|
56
|
+
await (0, index_1.handleRegistryList)({
|
|
57
|
+
dir: rawOpts.dir,
|
|
58
|
+
version: rawOpts.version,
|
|
59
|
+
});
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
function registerRegistryAdd(parent) {
|
|
63
|
+
const cmd = parent
|
|
64
|
+
.command('add')
|
|
65
|
+
.description('把一个或多个 SDK 文件 copy-in 到当前项目(保留目录结构,含 Depends 闭包)')
|
|
66
|
+
.argument('<name...>', '条目名(文件名去扩展名),可传多个;Depends 会自动传递解析')
|
|
67
|
+
.option('--dir <path>', '项目目录,默认当前目录', '.')
|
|
68
|
+
.option('--version <ver>', 'registry 包版本或 dist-tag,缺省 latest')
|
|
69
|
+
.option('--overwrite', '目标文件已存在时覆盖(默认跳过以保护用户改动)', false)
|
|
70
|
+
.option('--dry-run', '只报告将写 / 将跳哪些文件,不落盘', false)
|
|
71
|
+
.addHelpText('after', `
|
|
72
|
+
行为
|
|
73
|
+
- 解析 USAGE 块 Depends 传递闭包,把 SDK 文件从包根 <rel> 按相对路径拷到项目根
|
|
74
|
+
(包根 scripts/deck-stage.js → 项目 scripts/deck-stage.js,1:1 保留路径)
|
|
75
|
+
- skip-by-disk:目标文件已存在默认跳过;--overwrite 才覆盖
|
|
76
|
+
- 不写任何安装记账文件(项目文件树即状态)
|
|
77
|
+
|
|
78
|
+
JSON 输出
|
|
79
|
+
{"data": {"registryVersion": "...", "requested": [...], "resolved": [...],
|
|
80
|
+
"added": [...], "skipped": [...], "dryRun": false}}
|
|
81
|
+
(added / skipped 是项目内相对路径,如 scripts/deck-stage.js)
|
|
82
|
+
|
|
83
|
+
示例
|
|
84
|
+
$ miaoda registry add deck-slide
|
|
85
|
+
$ miaoda registry add deck-slide theme --dry-run
|
|
86
|
+
$ miaoda registry add deck-slide --overwrite --json
|
|
87
|
+
`);
|
|
88
|
+
cmd.action((0, shared_1.withHelp)(cmd, async (names, rawOpts) => {
|
|
89
|
+
await (0, index_1.handleRegistryAdd)({
|
|
90
|
+
names,
|
|
91
|
+
dir: rawOpts.dir,
|
|
92
|
+
version: rawOpts.version,
|
|
93
|
+
overwrite: rawOpts.overwrite,
|
|
94
|
+
dryRun: rawOpts.dryRun,
|
|
95
|
+
});
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
@@ -69,8 +69,12 @@ async function handleAppInit(opts) {
|
|
|
69
69
|
const projectName = node_path_1.default.basename(targetDir);
|
|
70
70
|
const tplResult = (0, index_1.renderTemplate)({ stack, version, targetDir, projectName });
|
|
71
71
|
// 先建 logs/,防止 user 跑 dev 之前 AI/工具 redirect 到 logs/*.log 因为父目录不存在直接挂
|
|
72
|
-
// (dev.js / dev-local.js 自己也建 logs/,但只在它启动后;启动前的 shell redirect 会先于此)
|
|
73
|
-
|
|
72
|
+
// (dev.js / dev-local.js 自己也建 logs/,但只在它启动后;启动前的 shell redirect 会先于此)。
|
|
73
|
+
// 仅本地开发需要:沙箱环境由平台 supervisor 拉起 dev,无 init 前的 shell redirect 场景,
|
|
74
|
+
// 不预建 logs/ 以免在沙箱工作区留空目录。
|
|
75
|
+
if (!(0, env_1.isSandboxEnv)()) {
|
|
76
|
+
(0, logs_dir_1.ensureLogsDir)(targetDir);
|
|
77
|
+
}
|
|
74
78
|
// skills 同步软失败:拉不到 coding-steering 包不该阻断 writeSparkMeta /
|
|
75
79
|
// activateGitHooks(之前会让 .spark/meta.json 没写,下次 init 半渲染状态又得重跑全套)。
|
|
76
80
|
// 按运行环境(isSandboxEnv:MIAODA_DEP_CACHE_DIR 非空)分流 outputLayout,不绑 stack:
|
|
@@ -0,0 +1,54 @@
|
|
|
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.handleRegistryAdd = handleRegistryAdd;
|
|
7
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
8
|
+
const index_1 = require("../../../services/registry/index");
|
|
9
|
+
const shared_1 = require("./shared");
|
|
10
|
+
const error_1 = require("../../../utils/error");
|
|
11
|
+
const output_1 = require("../../../utils/output");
|
|
12
|
+
const logger_1 = require("../../../utils/logger");
|
|
13
|
+
/**
|
|
14
|
+
* miaoda registry add <name...> [--overwrite] [--dry-run] [--dir <path>] [--version <ver>]
|
|
15
|
+
*
|
|
16
|
+
* 解析 USAGE 块 Depends 传递闭包,把每个条目对应的 SDK 文件从 registry 包根的 <relPath>
|
|
17
|
+
* **保留相对路径**拷到项目根(--dir 指定目录的根)。
|
|
18
|
+
* skip-by-disk:目标已存在默认跳过,--overwrite 才覆盖。不写任何安装记账文件。
|
|
19
|
+
*/
|
|
20
|
+
async function handleRegistryAdd(opts) {
|
|
21
|
+
await Promise.resolve();
|
|
22
|
+
if (opts.names.length === 0) {
|
|
23
|
+
throw new error_1.AppError('ARGS_INVALID', 'registry add 至少需要一个条目名');
|
|
24
|
+
}
|
|
25
|
+
const targetDir = node_path_1.default.resolve(opts.dir ?? process.cwd());
|
|
26
|
+
const stack = (0, shared_1.resolveRegistryStack)(targetDir);
|
|
27
|
+
const reg = (0, index_1.loadRegistry)({ stack, version: opts.version });
|
|
28
|
+
try {
|
|
29
|
+
const plan = (0, index_1.planAdd)({
|
|
30
|
+
entries: reg.entries,
|
|
31
|
+
targetDir,
|
|
32
|
+
names: opts.names,
|
|
33
|
+
});
|
|
34
|
+
const result = (0, index_1.applyAdd)(plan.files, {
|
|
35
|
+
overwrite: opts.overwrite === true,
|
|
36
|
+
dryRun: opts.dryRun === true,
|
|
37
|
+
});
|
|
38
|
+
const verb = opts.dryRun === true ? 'Would add' : 'Added';
|
|
39
|
+
(0, logger_1.log)('registry', `${verb} ${String(result.added.length)} file(s), skipped ${String(result.skipped.length)} (registry@${reg.version})`);
|
|
40
|
+
(0, output_1.emit)({
|
|
41
|
+
data: {
|
|
42
|
+
registryVersion: reg.version,
|
|
43
|
+
requested: opts.names,
|
|
44
|
+
resolved: plan.entries.map((e) => e.name),
|
|
45
|
+
added: result.added,
|
|
46
|
+
skipped: result.skipped,
|
|
47
|
+
dryRun: opts.dryRun === true,
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
reg.cleanup();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handleRegistryAdd = exports.handleRegistryList = void 0;
|
|
4
|
+
var list_1 = require("./list");
|
|
5
|
+
Object.defineProperty(exports, "handleRegistryList", { enumerable: true, get: function () { return list_1.handleRegistryList; } });
|
|
6
|
+
var add_1 = require("./add");
|
|
7
|
+
Object.defineProperty(exports, "handleRegistryAdd", { enumerable: true, get: function () { return add_1.handleRegistryAdd; } });
|
|
@@ -0,0 +1,48 @@
|
|
|
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.handleRegistryList = handleRegistryList;
|
|
7
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
8
|
+
const index_1 = require("../../../services/registry/index");
|
|
9
|
+
const shared_1 = require("./shared");
|
|
10
|
+
const output_1 = require("../../../utils/output");
|
|
11
|
+
/**
|
|
12
|
+
* miaoda registry list [--dir <path>] [--version <ver>]
|
|
13
|
+
*
|
|
14
|
+
* 从 .spark/meta.json 读 stack → runtime → 包名,拉对应 registry 包,从包根递归扫含 USAGE 块的 SDK 文件。
|
|
15
|
+
*
|
|
16
|
+
* - 默认(pretty):输出各条目 USAGE 块原文,带文件名分隔,给 Agent 直接读注释。
|
|
17
|
+
* - --json:输出 [{name, file, usage(原文字符串), dependsOn[]}],usage 保持原文不解析成字段。
|
|
18
|
+
* file 是相对包根的路径(如 scripts/deck-stage.js),即 add 后的落地相对路径。
|
|
19
|
+
*/
|
|
20
|
+
async function handleRegistryList(opts) {
|
|
21
|
+
await Promise.resolve();
|
|
22
|
+
const targetDir = node_path_1.default.resolve(opts.dir ?? process.cwd());
|
|
23
|
+
const stack = (0, shared_1.resolveRegistryStack)(targetDir);
|
|
24
|
+
const reg = (0, index_1.loadRegistry)({ stack, version: opts.version });
|
|
25
|
+
try {
|
|
26
|
+
if ((0, output_1.isJsonMode)()) {
|
|
27
|
+
(0, output_1.emit)({
|
|
28
|
+
data: reg.entries.map((e) => ({
|
|
29
|
+
name: e.name,
|
|
30
|
+
file: e.relPath,
|
|
31
|
+
usage: e.usage,
|
|
32
|
+
dependsOn: e.dependsOn,
|
|
33
|
+
})),
|
|
34
|
+
});
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
// pretty:各条目 USAGE 块原文拼接,带文件路径分隔(让 Agent 直接读注释)
|
|
38
|
+
if (reg.entries.length === 0) {
|
|
39
|
+
(0, output_1.emit)('(no registry entries)');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const blocks = reg.entries.map((e) => `# ${e.relPath}\n${e.usage}`);
|
|
43
|
+
(0, output_1.emit)(blocks.join('\n\n'));
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
reg.cleanup();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
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.REGISTRY_SUPPORTED_STACKS = exports.REGISTRY_BY_RUNTIME = exports.STACK_RUNTIME = void 0;
|
|
7
|
+
exports.resolveRegistryPackage = resolveRegistryPackage;
|
|
8
|
+
exports.loadRegistry = loadRegistry;
|
|
9
|
+
exports.extractUsageBlock = extractUsageBlock;
|
|
10
|
+
exports.parseDependsOn = parseDependsOn;
|
|
11
|
+
exports.indexEntries = indexEntries;
|
|
12
|
+
exports.resolveDependencyClosure = resolveDependencyClosure;
|
|
13
|
+
exports.planAdd = planAdd;
|
|
14
|
+
exports.applyAdd = applyAdd;
|
|
15
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
16
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
17
|
+
const npm_pack_1 = require("../../utils/npm-pack");
|
|
18
|
+
const error_1 = require("../../utils/error");
|
|
19
|
+
const logger_1 = require("../../utils/logger");
|
|
20
|
+
/**
|
|
21
|
+
* 解析链:stack → runtime → registry 包名。
|
|
22
|
+
*
|
|
23
|
+
* registry 包按 **运行时(runtime)** 命名分发(而非 stack):多个 stack 可共享同一 runtime
|
|
24
|
+
* 的同一份 registry。runtime 不引入新的 meta 字段,从已有的 `.spark/meta.json.stack` 推得。
|
|
25
|
+
* STACK_RUNTIME: stack → runtime(design-html 跑在 buildless 运行时)
|
|
26
|
+
* REGISTRY_BY_RUNTIME: runtime → registry 包名
|
|
27
|
+
* 任一步查不到都明确报错(stack 无对应 runtime / runtime 无对应 registry)。
|
|
28
|
+
*/
|
|
29
|
+
exports.STACK_RUNTIME = {
|
|
30
|
+
'design-html': 'buildless',
|
|
31
|
+
};
|
|
32
|
+
exports.REGISTRY_BY_RUNTIME = {
|
|
33
|
+
buildless: '@lark-apaas/coding-registry-buildless',
|
|
34
|
+
};
|
|
35
|
+
/** 有对应 registry 的 stack 列表(stack 有 runtime 且该 runtime 有 registry 包)。 */
|
|
36
|
+
exports.REGISTRY_SUPPORTED_STACKS = Object.keys(exports.STACK_RUNTIME).filter((stack) => Boolean(exports.REGISTRY_BY_RUNTIME[exports.STACK_RUNTIME[stack]]));
|
|
37
|
+
/**
|
|
38
|
+
* stack → registry 包名。走 STACK_RUNTIME → REGISTRY_BY_RUNTIME 两跳。
|
|
39
|
+
* stack 无对应 runtime,或 runtime 无对应 registry,均抛 AppError。
|
|
40
|
+
*/
|
|
41
|
+
function resolveRegistryPackage(stack) {
|
|
42
|
+
const runtime = exports.STACK_RUNTIME[stack];
|
|
43
|
+
if (!runtime) {
|
|
44
|
+
throw new error_1.AppError('REGISTRY_STACK_UNSUPPORTED', `stack "${stack}" 没有对应的 runtime`, {
|
|
45
|
+
next_actions: [`支持 registry 的 stack:${exports.REGISTRY_SUPPORTED_STACKS.join(', ') || '(none)'}`],
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
const packageName = exports.REGISTRY_BY_RUNTIME[runtime];
|
|
49
|
+
if (!packageName) {
|
|
50
|
+
throw new error_1.AppError('REGISTRY_RUNTIME_UNSUPPORTED', `runtime "${runtime}"(来自 stack "${stack}")没有对应的 registry 包`);
|
|
51
|
+
}
|
|
52
|
+
return packageName;
|
|
53
|
+
}
|
|
54
|
+
const USAGE_BEGIN = '/* BEGIN USAGE */';
|
|
55
|
+
const USAGE_END = '/* END USAGE */';
|
|
56
|
+
/**
|
|
57
|
+
* 拉取并解析某 stack 的 registry 包。
|
|
58
|
+
*
|
|
59
|
+
* 复用 fetchNpmPackage(npm pack + tar -xzf + 临时目录),与 template / steering 同一套
|
|
60
|
+
* tarball 基建。包内布局约定(SDK 文件**平铺在包根**,无外层 registry/ 目录):
|
|
61
|
+
* package/<subdir>/<sdk-file> ← 按目录组织的 SDK 文件,每个以 USAGE 块开头
|
|
62
|
+
* (scripts/ · components/ · stylesheets/ 等;CLI 不约束子目录名)
|
|
63
|
+
*
|
|
64
|
+
* 从包根递归扫整棵树,判定含 USAGE 块的为 SDK 条目(package.json / README / LICENSE 等无
|
|
65
|
+
* USAGE 块的文件自然跳过),解析其 USAGE 原文 + Depends。
|
|
66
|
+
* 调用方用完 **必须** 调 result.cleanup() 清理临时目录。
|
|
67
|
+
*/
|
|
68
|
+
function loadRegistry(opts) {
|
|
69
|
+
const packageName = resolveRegistryPackage(opts.stack);
|
|
70
|
+
const effectiveVersion = opts.version ?? 'latest';
|
|
71
|
+
(0, logger_1.log)('registry', `Fetching ${packageName}@${effectiveVersion}...`);
|
|
72
|
+
const fetched = (0, npm_pack_1.fetchNpmPackage)({ packageName, version: effectiveVersion });
|
|
73
|
+
try {
|
|
74
|
+
// 扫描根 = 包根(fetchNpmPackage 的 extractDir 即 tarball 内 package/ 目录)
|
|
75
|
+
const registryRoot = fetched.extractDir;
|
|
76
|
+
const entries = scanEntries(registryRoot);
|
|
77
|
+
return {
|
|
78
|
+
entries,
|
|
79
|
+
registryRoot,
|
|
80
|
+
version: fetched.version,
|
|
81
|
+
cleanup: () => {
|
|
82
|
+
fetched.cleanup();
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
fetched.cleanup();
|
|
88
|
+
throw err;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* 可作为 SDK 条目的代码文件扩展名白名单。
|
|
93
|
+
* 只扫代码文件 —— README.md / LICENSE / package.json 等不在此列,避免文档里出现的
|
|
94
|
+
* USAGE 字面量(如 README 解释 `/* BEGIN USAGE */` 格式)被误判为 SDK 条目。
|
|
95
|
+
*/
|
|
96
|
+
const SDK_FILE_EXTENSIONS = new Set(['.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx', '.css']);
|
|
97
|
+
/**
|
|
98
|
+
* 从包根递归扫整棵树,提取含 USAGE 块的 SDK 条目(任意子目录深度)。
|
|
99
|
+
* 只看代码文件(SDK_FILE_EXTENSIONS);条目 name 全树唯一,撞名抛错。
|
|
100
|
+
*/
|
|
101
|
+
function scanEntries(registryRoot) {
|
|
102
|
+
const entries = [];
|
|
103
|
+
const byName = new Map(); // name → relPath(撞名诊断)
|
|
104
|
+
const walk = (dir) => {
|
|
105
|
+
for (const dirent of node_fs_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
106
|
+
const absPath = node_path_1.default.join(dir, dirent.name);
|
|
107
|
+
if (dirent.isDirectory()) {
|
|
108
|
+
walk(absPath);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (!dirent.isFile())
|
|
112
|
+
continue;
|
|
113
|
+
// 只扫代码文件,排除 README/LICENSE/package.json 等(它们正文可能含 USAGE 字面量)
|
|
114
|
+
if (!SDK_FILE_EXTENSIONS.has(node_path_1.default.extname(dirent.name).toLowerCase()))
|
|
115
|
+
continue;
|
|
116
|
+
const content = node_fs_1.default.readFileSync(absPath, 'utf-8');
|
|
117
|
+
const usage = extractUsageBlock(content);
|
|
118
|
+
if (usage === null)
|
|
119
|
+
continue; // 非 SDK 文件(无 USAGE 块)
|
|
120
|
+
// 相对包根的路径,统一用 '/' 分隔(落地时再按平台 join)
|
|
121
|
+
const relPath = node_path_1.default.relative(registryRoot, absPath).split(node_path_1.default.sep).join('/');
|
|
122
|
+
const name = stripExt(dirent.name);
|
|
123
|
+
const prev = byName.get(name);
|
|
124
|
+
if (prev !== undefined) {
|
|
125
|
+
throw new error_1.AppError('REGISTRY_DUPLICATE_NAME', `registry 条目名 "${name}" 重复:${prev} 与 ${relPath}`);
|
|
126
|
+
}
|
|
127
|
+
byName.set(name, relPath);
|
|
128
|
+
entries.push({
|
|
129
|
+
name,
|
|
130
|
+
relPath,
|
|
131
|
+
usage,
|
|
132
|
+
dependsOn: parseDependsOn(usage),
|
|
133
|
+
absPath,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
walk(registryRoot);
|
|
138
|
+
// 稳定排序,便于 list 输出可预期
|
|
139
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
140
|
+
return entries;
|
|
141
|
+
}
|
|
142
|
+
/** 提取 BEGIN/END USAGE 标记之间的原文(含标记行)。无 USAGE 块返回 null。 */
|
|
143
|
+
function extractUsageBlock(content) {
|
|
144
|
+
const begin = content.indexOf(USAGE_BEGIN);
|
|
145
|
+
if (begin === -1)
|
|
146
|
+
return null;
|
|
147
|
+
const end = content.indexOf(USAGE_END, begin);
|
|
148
|
+
if (end === -1)
|
|
149
|
+
return null;
|
|
150
|
+
return content.slice(begin, end + USAGE_END.length);
|
|
151
|
+
}
|
|
152
|
+
/** 从 USAGE 块文本里解析 `// Depends: <name>` 行(可多行),返回去重后的依赖名列表。 */
|
|
153
|
+
function parseDependsOn(usage) {
|
|
154
|
+
const deps = [];
|
|
155
|
+
const seen = new Set();
|
|
156
|
+
for (const line of usage.split(/\r?\n/)) {
|
|
157
|
+
const m = /^\s*\/\/\s*Depends:\s*(.+?)\s*$/.exec(line);
|
|
158
|
+
if (m === null)
|
|
159
|
+
continue;
|
|
160
|
+
// 一行内允许逗号 / 空白分隔多个依赖
|
|
161
|
+
for (const raw of m[1].split(/[\s,]+/)) {
|
|
162
|
+
const name = raw.trim();
|
|
163
|
+
if (name !== '' && !seen.has(name)) {
|
|
164
|
+
seen.add(name);
|
|
165
|
+
deps.push(name);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return deps;
|
|
170
|
+
}
|
|
171
|
+
function stripExt(file) {
|
|
172
|
+
const ext = node_path_1.default.extname(file);
|
|
173
|
+
return ext === '' ? file : file.slice(0, -ext.length);
|
|
174
|
+
}
|
|
175
|
+
/** name → entry 索引,便于依赖解析。 */
|
|
176
|
+
function indexEntries(entries) {
|
|
177
|
+
const map = new Map();
|
|
178
|
+
for (const e of entries)
|
|
179
|
+
map.set(e.name, e);
|
|
180
|
+
return map;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* 解析 Depends 的传递闭包:从 requested 出发,递归把所有依赖一并纳入。
|
|
184
|
+
*
|
|
185
|
+
* 返回拓扑序(依赖在前、被依赖在后),便于按序拷贝。检测循环依赖并抛错;
|
|
186
|
+
* 引用了不存在的条目名也抛错(fail-fast,避免拷半套)。
|
|
187
|
+
*/
|
|
188
|
+
function resolveDependencyClosure(entries, requested) {
|
|
189
|
+
const index = indexEntries(entries);
|
|
190
|
+
const ordered = [];
|
|
191
|
+
const visited = new Set();
|
|
192
|
+
const onStack = new Set();
|
|
193
|
+
const visit = (name, trail) => {
|
|
194
|
+
if (visited.has(name))
|
|
195
|
+
return;
|
|
196
|
+
if (onStack.has(name)) {
|
|
197
|
+
throw new error_1.AppError('REGISTRY_CYCLE', `registry 条目依赖成环:${[...trail, name].join(' -> ')}`);
|
|
198
|
+
}
|
|
199
|
+
const entry = index.get(name);
|
|
200
|
+
if (entry === undefined) {
|
|
201
|
+
// requested 顶层缺失 vs 依赖链里缺失,都报清楚是谁引的
|
|
202
|
+
const via = trail.length > 0 ? `(被 "${trail[trail.length - 1]}" 依赖)` : '';
|
|
203
|
+
throw new error_1.AppError('REGISTRY_ENTRY_NOT_FOUND', `registry 没有条目 "${name}"${via}`, {
|
|
204
|
+
next_actions: ['用 `miaoda registry list` 查看可用条目'],
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
onStack.add(name);
|
|
208
|
+
for (const dep of entry.dependsOn) {
|
|
209
|
+
visit(dep, [...trail, name]);
|
|
210
|
+
}
|
|
211
|
+
onStack.delete(name);
|
|
212
|
+
visited.add(name);
|
|
213
|
+
ordered.push(entry);
|
|
214
|
+
};
|
|
215
|
+
for (const name of requested)
|
|
216
|
+
visit(name, []);
|
|
217
|
+
return ordered;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* 规划 add:解析依赖闭包 → 每个条目按其相对包根的路径落地到项目根 → 标记是否已存在。
|
|
221
|
+
* 纯计算,不落盘(dry-run 与真实 add 共用)。
|
|
222
|
+
*/
|
|
223
|
+
function planAdd(opts) {
|
|
224
|
+
const entries = resolveDependencyClosure(opts.entries, opts.names);
|
|
225
|
+
const files = entries.map((entry) => {
|
|
226
|
+
// relPath 用 '/' 存,落地时按平台拆分 join,保证 Windows 也正确
|
|
227
|
+
const destAbs = node_path_1.default.join(opts.targetDir, ...entry.relPath.split('/'));
|
|
228
|
+
return {
|
|
229
|
+
entry: entry.name,
|
|
230
|
+
relPath: entry.relPath,
|
|
231
|
+
destAbs,
|
|
232
|
+
srcAbs: entry.absPath,
|
|
233
|
+
exists: node_fs_1.default.existsSync(destAbs),
|
|
234
|
+
};
|
|
235
|
+
});
|
|
236
|
+
return { entries, files };
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* 执行 add 计划:按 skip-by-disk 把 SDK 文件保留相对结构拷到项目根。
|
|
240
|
+
* - 目标已存在且未 overwrite → skip(保护用户改动)
|
|
241
|
+
* - 目标已存在且 overwrite → 覆盖写
|
|
242
|
+
* - 目标不存在 → 写
|
|
243
|
+
* dryRun=true 时只分类不落盘。不写任何安装记账文件(项目文件树即状态)。
|
|
244
|
+
*/
|
|
245
|
+
function applyAdd(files, opts) {
|
|
246
|
+
const added = [];
|
|
247
|
+
const skipped = [];
|
|
248
|
+
for (const f of files) {
|
|
249
|
+
if (f.exists && !opts.overwrite) {
|
|
250
|
+
skipped.push(f.relPath);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (!opts.dryRun) {
|
|
254
|
+
if (!node_fs_1.default.existsSync(f.srcAbs)) {
|
|
255
|
+
throw new error_1.AppError('REGISTRY_FILE_MISSING', `registry 包内缺少文件 ${f.relPath}(条目 "${f.entry}")`);
|
|
256
|
+
}
|
|
257
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(f.destAbs), { recursive: true });
|
|
258
|
+
node_fs_1.default.copyFileSync(f.srcAbs, f.destAbs);
|
|
259
|
+
}
|
|
260
|
+
added.push(f.relPath);
|
|
261
|
+
}
|
|
262
|
+
return { added, skipped };
|
|
263
|
+
}
|