@lark-apaas/miaoda-cli 0.1.48 → 0.1.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/cli/commands/app/index.js +83 -0
- package/dist/cli/handlers/app/import.js +191 -0
- package/dist/cli/handlers/app/index.js +7 -1
- package/dist/services/app/import/download.js +141 -0
- package/dist/services/app/import/index.js +39 -0
- package/dist/services/app/import/manifest.js +97 -0
- package/dist/services/app/import/place.js +327 -0
- package/dist/services/app/import/rules.js +293 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -48,15 +48,16 @@ miaoda file ls --output json
|
|
|
48
48
|
| `miaoda observability ...` | 线上日志、链路、前端源码堆栈反查、监控指标、运营指标 |
|
|
49
49
|
| `miaoda comment ...` | 应用评论:查看评论列表、解决评论(仅 design-html 场景开放) |
|
|
50
50
|
| `miaoda registry ...` | 组件 registry:列出 / copy-in SDK 文件(本地执行) |
|
|
51
|
-
| `miaoda app ...` |
|
|
51
|
+
| `miaoda app ...` | 应用生命周期:元数据、初始化、外部项目导入、平台同步、源码导出、静态产物导出 |
|
|
52
52
|
|
|
53
53
|
完整命令通过 `miaoda --help` 或 `miaoda <domain> --help` 查看。
|
|
54
54
|
|
|
55
55
|
## 工程初始化、同步与转换
|
|
56
56
|
|
|
57
|
-
`miaoda app init / sync / migrate` 管理本地应用工程(平台调用方与本地开发共用):
|
|
57
|
+
`miaoda app init / import / sync / migrate` 管理本地应用工程(平台调用方与本地开发共用):
|
|
58
58
|
|
|
59
59
|
- `init`:按 `--template` 或 `--app-type` 渲染模板(`full_stack → nestjs-react-fullstack`,`frontend / jspage → vite-react`)。工程已存在时不重新渲染、不覆盖业务代码,只输出就绪核验结果 `ready / problems`;`--strict` 下未就绪(meta 损坏、stack 不符、必要文件缺失、依赖未安装、上次操作失败)退出码 1。同步安装失败输出 `status=failed`、`failedStage=install`;退出码只在 `--strict` 时为 1,不带 `--strict` 时为 0(依赖由后续 `sync` 补装)。
|
|
60
|
+
- `import`(不在 help 列表中展示,仅供平台调用):外部项目导入(平台在 `init` 渲染模板之后、首次 git 提交之前于工程根目录调用)。`--zip <path>` 或 `--zip-url <预签名 URL>` 二选一,`--platform lovable|replit|gemini|zip`(默认 `zip`)。按平台规则把用户代码叠加到 `client/src`、`client/public`、`shared`,其余原样落到 `source_package/`;从不新建 / 覆盖模板托管文件(根 `package.json`、`package-lock.json`、`.npmrc`、`tsconfig*.json`、`vite.config.*`、`eslint.config.*`、`AGENTS.md`、`.githooks/`、`scripts/`、`.spark/`、`node_modules/`、`server/main.ts`、`client/src/index.tsx`),命中时改放 `source_package/` 并记入 `protectedConflicts`。**不合并 package.json、不装依赖**:用户 `package.json` 保留为 `source_package/package.json`,依赖静态分类(`platform_managed` / `present` / `build_chain` / `missing`)写进 `.spark/import/manifest.json`,由 Coding Agent 后续按需安装。先算完整计划、整体暂存、预检移入目标再逐个移入,任一阶段失败都会回滚,工程恢复原样(`WRITE_FAILED`,只可能留下新建的空目录);同时写待迁移标记 `.spark/import/MIGRATION_PENDING`(沙箱 skill `import-migration` 以它为加载条件,Coding Agent 迁移完成后删除);manifest 最后写,存在即视为已导入(`alreadyImported=true`,不再改动)。`--dry-run` 只输出计划;`--rules-json` 覆盖默认排除路径。错误码:`ARGS_INVALID`(退出码 2)、`NOT_INITIALIZED`、`DOWNLOAD_FAILED`、`INVALID_ZIP`(非 zip / 超 50MB / 解压失败)、`WRITE_FAILED`。
|
|
60
61
|
- `sync`:按 stack 的同步规则更新平台托管内容。全栈工程的托管文件是明确清单(`scripts/dev.sh`、`dev.js`、`dev-local.js`、`build.sh`、`run.sh`、`lint.js`、`prune-smart.js`、`hooks/run-precommit.js` 与 `.githooks/pre-commit`),`scripts/` 下其他用户文件保留;连续执行无漂移。依赖安装失败输出 `status=failed`、`failedStage=install`;退出码只在 `--strict`(平台严格调用)时为 1,不带 `--strict` 时为 0,`dev.sh` 与 lark-cli `+init` 不带它。
|
|
61
62
|
- `migrate`(不在 help 列表中展示):工程转换原语,在原工程目录内执行。目标模板先渲染到临时模板目录(不复制用户工程),再只读预检冲突(同路径不同内容、重复插件 ID、自定义同名 script、无法确定的 package.json / tsconfig / vite.config / index.html 形态等),随后在原目录定向转换(`src/ public/ → client/`、插件迁 `server/capabilities`、配置定向适配)并安装依赖;迁移配置声明了验证步骤时再执行验证(vite-react → 全栈不做完整构建)。冲突按 `--purpose` 决定的策略处理,结果字段 `conflictPolicy` 给出本次策略:
|
|
62
63
|
- `stop`(`--purpose upgrade` / `history_restore`,平台调用):预检发现冲突返回 `status=conflict` 与具体路径,工程不写入;执行中才出现的冲突即停、不覆盖用户不同内容。
|
|
@@ -25,6 +25,7 @@ function registerAppCommands(program, opts = {}) {
|
|
|
25
25
|
registerAppPack(appCmd);
|
|
26
26
|
if (opts.includeInit) {
|
|
27
27
|
registerAppInit(appCmd);
|
|
28
|
+
registerAppImport(appCmd);
|
|
28
29
|
registerAppSync(appCmd);
|
|
29
30
|
registerAppMigrate(appCmd);
|
|
30
31
|
}
|
|
@@ -462,6 +463,88 @@ JSON 输出
|
|
|
462
463
|
});
|
|
463
464
|
}));
|
|
464
465
|
}
|
|
466
|
+
function registerAppImport(parent) {
|
|
467
|
+
// 与 migrate 同理:import 只由平台(sandbox_console 建应用时)调用,**不暴露给 Agent**。
|
|
468
|
+
// hidden 后 `miaoda app -h` 不列出,避免 Agent 在已有应用里自行导入 zip 覆盖业务代码;
|
|
469
|
+
// 显式调用 `miaoda app import ...` 行为不变。
|
|
470
|
+
const cmd = new commander_1.Command('import')
|
|
471
|
+
.description('外部项目导入:把 zip 包(lovable / replit / gemini / 普通 zip)按平台规则叠加进已初始化的工程;不碰模板托管文件、不合并 package.json、不装依赖')
|
|
472
|
+
.option('--zip <path>', '本地 zip 路径(与 --zip-url 二选一)')
|
|
473
|
+
.option('--zip-url <url>', 'zip 下载地址(预签名 GET URL,与 --zip 二选一)')
|
|
474
|
+
.option('--platform <platform>', `来源平台(${index_1.IMPORT_PLATFORMS.join(' / ')})`, index_1.DEFAULT_IMPORT_PLATFORM)
|
|
475
|
+
.option('--rules-json <json>', '覆盖默认排除路径:{"excludedPaths": [...]},提供后整体替换该平台默认值')
|
|
476
|
+
.option('--dry-run', '只计算并输出放置计划,不写任何文件', false)
|
|
477
|
+
.option('--dir <path>', '项目目录,默认 cwd(需含 .spark/meta.json)')
|
|
478
|
+
.addHelpText('after', `
|
|
479
|
+
前置条件
|
|
480
|
+
工程已经 'miaoda app init' 过(.spark/meta.json 存在),否则报 NOT_INITIALIZED。
|
|
481
|
+
平台在模板渲染完成、首次 git 提交之前于工程根目录调用;依赖由 init 的后台安装负责,
|
|
482
|
+
本命令不读写 package.json / package-lock.json / node_modules。
|
|
483
|
+
|
|
484
|
+
幂等
|
|
485
|
+
.spark/import/manifest.json 已存在 → 不做任何改动,输出 alreadyImported=true。
|
|
486
|
+
manifest 最后写入,是导入完成的唯一标记。
|
|
487
|
+
|
|
488
|
+
导入包处理
|
|
489
|
+
大小上限 50MB(超出 / 非 zip / 解压失败 → INVALID_ZIP;下载失败 → DOWNLOAD_FAILED)。
|
|
490
|
+
丢弃 __MACOSX/、.DS_Store、含 ".." 的条目、.git 元数据(.gitignore / .github 保留)、
|
|
491
|
+
node_modules / dist / build 目录、符号链接。
|
|
492
|
+
所有条目共享一个不含 "." 的顶层目录时剥掉该目录(gemini 保留原始路径)。
|
|
493
|
+
超过 60 字节的路径段改名为 truncated-<hash><ext>。
|
|
494
|
+
|
|
495
|
+
放置规则(未命中的一律放 source_package/<原路径>)
|
|
496
|
+
lovable src/** → client/src/**(命中排除路径 → source_package/src/**);public/** → client/public/**
|
|
497
|
+
replit client/src/** → client/src/**(命中排除路径 → source_package/);shared/** → shared/**;
|
|
498
|
+
attached_assets/** → client/public/assets/**
|
|
499
|
+
gemini components/** → client/src/components/**;services/** → client/src/services/**;
|
|
500
|
+
types.ts / constants.ts → client/src/
|
|
501
|
+
zip src/** → client/src/**(命中排除路径 → source_package/src/**);public/** → client/public/**;
|
|
502
|
+
src/components/ui/<name>.<ext> 与工程已有 client/src/components/ui/<name>.* 同名时,
|
|
503
|
+
删除模板文件、写入用户文件(记入 replacedTemplateFiles);
|
|
504
|
+
例外:用户文件是 .js / .jsx 而模板有 .ts / .tsx 时保留模板版本,
|
|
505
|
+
用户文件改放 source_package/ 并记入 protectedConflicts
|
|
506
|
+
受保护目标(根 package.json / package-lock.json / .npmrc / tsconfig*.json / vite.config.* /
|
|
507
|
+
eslint.config.* / AGENTS.md / .githooks/ / scripts/ / .spark/ / node_modules/ / server/main.ts /
|
|
508
|
+
client/src/index.tsx)从不新建或覆盖,命中时改放 source_package/ 并记入 protectedConflicts。
|
|
509
|
+
用户的 package.json 保留在 source_package/package.json。
|
|
510
|
+
|
|
511
|
+
原子性
|
|
512
|
+
先算完整计划、整体暂存,再预检移入目标(目标已是目录 / 父路径是文件),最后逐个移入工程。
|
|
513
|
+
任一阶段失败都抛 WRITE_FAILED:移入中途失败会按逆序回滚已移入、已覆盖、已删除的文件,
|
|
514
|
+
工程恢复原样(只可能留下新建的空目录)。
|
|
515
|
+
|
|
516
|
+
依赖清单(.spark/import/manifest.json 的 dependencies,只做静态分析,不安装、不联网)
|
|
517
|
+
status 优先级:platform_managed(@lark-apaas/*)> present(工程已有,附 templateRange)>
|
|
518
|
+
build_chain(构建链 / 工具链)> missing
|
|
519
|
+
|
|
520
|
+
JSON 输出
|
|
521
|
+
{"data": {"imported": true, "alreadyImported": false, "platform": "zip",
|
|
522
|
+
"fileCounts": {"client/src": 0, "client/public": 0, "shared": 0, "source_package": 0},
|
|
523
|
+
"manifestPath": ".spark/import/manifest.json", "warnings": [...]}}
|
|
524
|
+
--dry-run 额外带 "dryRun": true 与 "plan": {placements, deletions, overwrites, excludedPaths,
|
|
525
|
+
protectedConflicts, replacedTemplateFiles, dependencies}
|
|
526
|
+
|
|
527
|
+
错误码
|
|
528
|
+
ARGS_INVALID(退出码 2)/ NOT_INITIALIZED / DOWNLOAD_FAILED / INVALID_ZIP / WRITE_FAILED(退出码 1)
|
|
529
|
+
|
|
530
|
+
示例
|
|
531
|
+
$ miaoda app import --zip-url 'https://tos.example.com/demo.zip?sign=...' --platform lovable --json
|
|
532
|
+
$ miaoda app import --zip ./demo.zip
|
|
533
|
+
$ miaoda app import --zip ./demo.zip --platform replit --dry-run
|
|
534
|
+
$ miaoda app import --zip ./demo.zip --rules-json '{"excludedPaths": ["src/legacy/"]}'
|
|
535
|
+
`);
|
|
536
|
+
cmd.action((0, shared_1.withHelp)(cmd, async (rawOpts) => {
|
|
537
|
+
await (0, index_1.handleAppImport)({
|
|
538
|
+
zip: rawOpts.zip,
|
|
539
|
+
zipUrl: rawOpts.zipUrl,
|
|
540
|
+
platform: rawOpts.platform,
|
|
541
|
+
rulesJson: rawOpts.rulesJson,
|
|
542
|
+
dryRun: rawOpts.dryRun,
|
|
543
|
+
dir: rawOpts.dir,
|
|
544
|
+
});
|
|
545
|
+
}));
|
|
546
|
+
parent.addCommand(cmd, { hidden: true });
|
|
547
|
+
}
|
|
465
548
|
function parseInitConf(raw) {
|
|
466
549
|
// --conf 为可选值:未传(undefined)、裸 --conf(Commander 给 true)、空/纯空白字符串
|
|
467
550
|
// 一律视为不配置,回退默认(latest)。
|
|
@@ -0,0 +1,191 @@
|
|
|
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.handleAppImport = handleAppImport;
|
|
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 index_1 = require("../../../services/app/import/index");
|
|
11
|
+
const error_1 = require("../../../utils/error");
|
|
12
|
+
const logger_1 = require("../../../utils/logger");
|
|
13
|
+
const output_1 = require("../../../utils/output");
|
|
14
|
+
function readPackageJson(file, label, warnings) {
|
|
15
|
+
try {
|
|
16
|
+
return (0, index_1.readJsonFile)(file);
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
warnings.push(`${label} 不是合法 JSON,依赖清单按空处理:${err instanceof Error ? err.message : String(err)}`);
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function readPrevFileCounts(manifestFile) {
|
|
24
|
+
try {
|
|
25
|
+
const prev = JSON.parse(node_fs_1.default.readFileSync(manifestFile, 'utf-8'));
|
|
26
|
+
return prev.fileCounts;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* miaoda app import --zip <path> | --zip-url <url> [--platform <p>] [--rules-json <json>] [--dry-run]
|
|
34
|
+
*
|
|
35
|
+
* 外部平台(lovable / replit / gemini / 普通 zip)项目导入:在已 `app init` 的工程里,
|
|
36
|
+
* 把用户代码按平台规则叠加到 client/ shared/,其余原样落到 source_package/。
|
|
37
|
+
* 不碰模板托管文件、不合并 package.json、不装依赖 —— 用户 package.json 保留在
|
|
38
|
+
* source_package/package.json,依赖分析写进 manifest 交给 Coding Agent 后续按需安装。
|
|
39
|
+
*
|
|
40
|
+
* 落盘两阶段(先暂存、再 rename),manifest 最后写,作为幂等标记。
|
|
41
|
+
*/
|
|
42
|
+
async function handleAppImport(opts) {
|
|
43
|
+
const hasZip = Boolean(opts.zip);
|
|
44
|
+
const hasUrl = Boolean(opts.zipUrl);
|
|
45
|
+
if (hasZip === hasUrl) {
|
|
46
|
+
throw new error_1.AppError('ARGS_INVALID', '--zip 与 --zip-url 必须且只能传一个');
|
|
47
|
+
}
|
|
48
|
+
const platform = (0, index_1.parsePlatform)(opts.platform);
|
|
49
|
+
const rules = (0, index_1.resolveImportRules)(platform, opts.rulesJson);
|
|
50
|
+
const projectDir = node_path_1.default.resolve(opts.dir ?? process.cwd());
|
|
51
|
+
if (!node_fs_1.default.existsSync(node_path_1.default.join(projectDir, '.spark', 'meta.json'))) {
|
|
52
|
+
throw new error_1.AppError('NOT_INITIALIZED', `工程未初始化(缺少 .spark/meta.json):${projectDir}`, {
|
|
53
|
+
next_actions: ['先执行 miaoda app init --template <stack> 再导入'],
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
const manifestFile = (0, index_1.importManifestPath)(projectDir);
|
|
57
|
+
if (node_fs_1.default.existsSync(manifestFile)) {
|
|
58
|
+
(0, logger_1.log)('import', `Already imported (${index_1.IMPORT_MANIFEST_REL} exists), skip`);
|
|
59
|
+
if (!(0, output_1.isJsonMode)())
|
|
60
|
+
process.stdout.write(`Already imported: ${manifestFile}\n`);
|
|
61
|
+
(0, output_1.emit)({
|
|
62
|
+
data: {
|
|
63
|
+
imported: false,
|
|
64
|
+
alreadyImported: true,
|
|
65
|
+
platform,
|
|
66
|
+
fileCounts: readPrevFileCounts(manifestFile),
|
|
67
|
+
manifestPath: index_1.IMPORT_MANIFEST_REL,
|
|
68
|
+
warnings: [],
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const tmpDir = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), 'miaoda-import-'));
|
|
74
|
+
try {
|
|
75
|
+
const warnings = [];
|
|
76
|
+
let zipPath;
|
|
77
|
+
if (opts.zipUrl) {
|
|
78
|
+
zipPath = node_path_1.default.join(tmpDir, 'import.zip');
|
|
79
|
+
await (0, index_1.downloadZip)(opts.zipUrl, zipPath);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
zipPath = node_path_1.default.resolve(opts.zip ?? '');
|
|
83
|
+
(0, index_1.assertLocalZip)(zipPath);
|
|
84
|
+
}
|
|
85
|
+
const extractDir = node_path_1.default.join(tmpDir, 'extracted');
|
|
86
|
+
warnings.push(...(0, index_1.extractImportZip)(zipPath, extractDir).warnings);
|
|
87
|
+
const { entries, renamed } = (0, index_1.normalizeEntries)((0, index_1.collectFiles)(extractDir), platform);
|
|
88
|
+
if (entries.length === 0) {
|
|
89
|
+
throw new error_1.AppError('INVALID_ZIP', '导入包内没有可导入的文件');
|
|
90
|
+
}
|
|
91
|
+
for (const r of renamed)
|
|
92
|
+
warnings.push(`路径段过长已重命名:${r.from} → ${r.to}`);
|
|
93
|
+
const plan = (0, index_1.buildImportPlan)({ entries, platform, rules, projectDir });
|
|
94
|
+
warnings.push(...plan.warnings);
|
|
95
|
+
for (const c of plan.protectedConflicts) {
|
|
96
|
+
warnings.push(`目标为模板受保护路径,已改放 ${c.placedAt}:${c.target}`);
|
|
97
|
+
}
|
|
98
|
+
if (plan.overwrites.length > 0) {
|
|
99
|
+
warnings.push(`覆盖了工程中已有的 ${String(plan.overwrites.length)} 个文件`);
|
|
100
|
+
}
|
|
101
|
+
const dependencies = analyzeDependencies({
|
|
102
|
+
entries,
|
|
103
|
+
extractDir,
|
|
104
|
+
projectDir,
|
|
105
|
+
warnings,
|
|
106
|
+
});
|
|
107
|
+
if (opts.dryRun) {
|
|
108
|
+
if (!(0, output_1.isJsonMode)()) {
|
|
109
|
+
process.stdout.write(`Dry run: ${String(plan.placements.length)} file(s) planned\n`);
|
|
110
|
+
}
|
|
111
|
+
(0, output_1.emit)({
|
|
112
|
+
data: {
|
|
113
|
+
imported: false,
|
|
114
|
+
alreadyImported: false,
|
|
115
|
+
dryRun: true,
|
|
116
|
+
platform,
|
|
117
|
+
fileCounts: plan.fileCounts,
|
|
118
|
+
manifestPath: index_1.IMPORT_MANIFEST_REL,
|
|
119
|
+
warnings,
|
|
120
|
+
plan: {
|
|
121
|
+
placements: plan.placements.map((p) => ({ source: p.source, target: p.target })),
|
|
122
|
+
deletions: plan.deletions,
|
|
123
|
+
overwrites: plan.overwrites,
|
|
124
|
+
excludedPaths: plan.excludedPaths,
|
|
125
|
+
protectedConflicts: plan.protectedConflicts,
|
|
126
|
+
replacedTemplateFiles: plan.replacedTemplateFiles,
|
|
127
|
+
dependencies,
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
applyWithStaging({ plan, extractDir, projectDir });
|
|
134
|
+
const manifest = (0, index_1.buildImportManifest)({ platform, plan, dependencies });
|
|
135
|
+
try {
|
|
136
|
+
(0, index_1.writeImportPendingMarker)(projectDir);
|
|
137
|
+
(0, index_1.writeImportManifest)(projectDir, manifest);
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
throw new error_1.AppError('WRITE_FAILED', `写 ${index_1.IMPORT_MANIFEST_REL} 失败:${err instanceof Error ? err.message : String(err)}`);
|
|
141
|
+
}
|
|
142
|
+
(0, logger_1.log)('import', `Imported ${String(plan.placements.length)} file(s) (${platform})`);
|
|
143
|
+
if (!(0, output_1.isJsonMode)()) {
|
|
144
|
+
process.stdout.write(`✓ Imported ${String(plan.placements.length)} file(s) from ${platform} package into ${projectDir}\n`);
|
|
145
|
+
}
|
|
146
|
+
(0, output_1.emit)({
|
|
147
|
+
data: {
|
|
148
|
+
imported: true,
|
|
149
|
+
alreadyImported: false,
|
|
150
|
+
platform,
|
|
151
|
+
fileCounts: plan.fileCounts,
|
|
152
|
+
manifestPath: index_1.IMPORT_MANIFEST_REL,
|
|
153
|
+
warnings,
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
node_fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function analyzeDependencies(opts) {
|
|
162
|
+
const pkgEntry = opts.entries.find((e) => e.normalized === 'package.json');
|
|
163
|
+
if (!pkgEntry)
|
|
164
|
+
return [];
|
|
165
|
+
const sourcePkg = readPackageJson(node_path_1.default.join(opts.extractDir, ...pkgEntry.raw.split('/')), '导入包 package.json', opts.warnings);
|
|
166
|
+
const projectPkg = readPackageJson(node_path_1.default.join(opts.projectDir, 'package.json'), '工程 package.json', opts.warnings);
|
|
167
|
+
return (0, index_1.classifyDependencies)({ sourcePkg, projectPkg });
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* 暂存目录放在工程 .spark/import/ 下:与工程同一文件系统,rename 不会 EXDEV。
|
|
171
|
+
* 若 .spark/import/ 是本次新建的且导入失败,连目录一起清掉,保证失败时工程原样。
|
|
172
|
+
*/
|
|
173
|
+
function applyWithStaging(opts) {
|
|
174
|
+
const importDir = node_path_1.default.join(opts.projectDir, '.spark', 'import');
|
|
175
|
+
const createdImportDir = !node_fs_1.default.existsSync(importDir);
|
|
176
|
+
const stagingDir = node_path_1.default.join(importDir, `.staging-${String(process.pid)}-${String(Date.now())}`);
|
|
177
|
+
try {
|
|
178
|
+
node_fs_1.default.mkdirSync(stagingDir, { recursive: true });
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
throw new error_1.AppError('WRITE_FAILED', `创建暂存目录失败:${err instanceof Error ? err.message : String(err)}`);
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
(0, index_1.applyImportPlan)({ ...opts, stagingDir });
|
|
185
|
+
}
|
|
186
|
+
catch (err) {
|
|
187
|
+
if (createdImportDir)
|
|
188
|
+
node_fs_1.default.rmSync(importDir, { recursive: true, force: true });
|
|
189
|
+
throw err;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.PACK_MODES = exports.DEFAULT_PACK_MODE = exports.DEFAULT_PACK_OUT = exports.handleAppPack = exports.SUPPORTED_STACKS = exports.handleAppMigrate = exports.UPGRADE_MANIFEST_FILENAME = exports.DEFAULT_EXPORT_OUT = exports.handleAppExport = exports.handleAppUpgrade = exports.handleAppSync = exports.handleAppInit = exports.handleAppUpdate = exports.handleAppGet = void 0;
|
|
3
|
+
exports.DEFAULT_IMPORT_PLATFORM = exports.IMPORT_PLATFORMS = exports.handleAppImport = exports.PACK_MODES = exports.DEFAULT_PACK_MODE = exports.DEFAULT_PACK_OUT = exports.handleAppPack = exports.SUPPORTED_STACKS = exports.handleAppMigrate = exports.UPGRADE_MANIFEST_FILENAME = exports.DEFAULT_EXPORT_OUT = exports.handleAppExport = exports.handleAppUpgrade = 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");
|
|
@@ -24,3 +24,9 @@ Object.defineProperty(exports, "handleAppPack", { enumerable: true, get: functio
|
|
|
24
24
|
Object.defineProperty(exports, "DEFAULT_PACK_OUT", { enumerable: true, get: function () { return pack_1.DEFAULT_PACK_OUT; } });
|
|
25
25
|
Object.defineProperty(exports, "DEFAULT_PACK_MODE", { enumerable: true, get: function () { return pack_1.DEFAULT_PACK_MODE; } });
|
|
26
26
|
Object.defineProperty(exports, "PACK_MODES", { enumerable: true, get: function () { return pack_1.PACK_MODES; } });
|
|
27
|
+
var import_1 = require("./import");
|
|
28
|
+
Object.defineProperty(exports, "handleAppImport", { enumerable: true, get: function () { return import_1.handleAppImport; } });
|
|
29
|
+
// commands 层渲染 help 用的枚举,同 SUPPORTED_STACKS 走 handler barrel 转发
|
|
30
|
+
var index_2 = require("../../../services/app/import/index");
|
|
31
|
+
Object.defineProperty(exports, "IMPORT_PLATFORMS", { enumerable: true, get: function () { return index_2.IMPORT_PLATFORMS; } });
|
|
32
|
+
Object.defineProperty(exports, "DEFAULT_IMPORT_PLATFORM", { enumerable: true, get: function () { return index_2.DEFAULT_IMPORT_PLATFORM; } });
|
|
@@ -0,0 +1,141 @@
|
|
|
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.MAX_IMPORT_ZIP_BYTES = void 0;
|
|
7
|
+
exports.downloadZip = downloadZip;
|
|
8
|
+
exports.assertLocalZip = assertLocalZip;
|
|
9
|
+
exports.extractImportZip = extractImportZip;
|
|
10
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
11
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
12
|
+
const archive_1 = require("../../../utils/archive");
|
|
13
|
+
const error_1 = require("../../../utils/error");
|
|
14
|
+
const logger_1 = require("../../../utils/logger");
|
|
15
|
+
const upload_1 = require("../../../services/app/pack/upload");
|
|
16
|
+
/** 导入包大小上限(对齐服务端导入上传口径)。 */
|
|
17
|
+
exports.MAX_IMPORT_ZIP_BYTES = 50 * 1024 * 1024;
|
|
18
|
+
/** 下载超时:50MB 包在沙箱内网拉 TOS 远用不满,给足余量。 */
|
|
19
|
+
const DOWNLOAD_TIMEOUT_MS = 120_000;
|
|
20
|
+
function tooLarge(size) {
|
|
21
|
+
return new error_1.AppError('INVALID_ZIP', `导入包超过大小上限(${String(size)} > ${String(exports.MAX_IMPORT_ZIP_BYTES)} bytes)`);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* 把预签名 URL 指向的 zip 下载到 destFile。
|
|
25
|
+
*
|
|
26
|
+
* 预签名 URL 等同凭证:错误信息与日志一律脱敏(只在 --verbose 的 debug 里给完整地址)。
|
|
27
|
+
* 超过 50MB 判 INVALID_ZIP(先看 content-length,再在读流时累计兜底);
|
|
28
|
+
* 网络失败 / 非 2xx 判 DOWNLOAD_FAILED。
|
|
29
|
+
*/
|
|
30
|
+
async function downloadZip(url, destFile) {
|
|
31
|
+
let parsed;
|
|
32
|
+
try {
|
|
33
|
+
parsed = new URL(url);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
throw new error_1.AppError('ARGS_INVALID', `--zip-url 不是合法 URL:${(0, upload_1.redactUrl)(url)}`);
|
|
37
|
+
}
|
|
38
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
39
|
+
throw new error_1.AppError('ARGS_INVALID', `--zip-url 只支持 http/https,收到 ${parsed.protocol}`);
|
|
40
|
+
}
|
|
41
|
+
(0, logger_1.log)('import', 'Downloading import package...');
|
|
42
|
+
(0, logger_1.debug)(`import: GET ${url}`);
|
|
43
|
+
let resp;
|
|
44
|
+
try {
|
|
45
|
+
resp = await fetch(url, { signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) });
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
throw new error_1.AppError('DOWNLOAD_FAILED', `下载导入包失败(${(0, upload_1.redactUrl)(url)}):${err instanceof Error ? err.message : String(err)}`, { retryable: true });
|
|
49
|
+
}
|
|
50
|
+
if (!resp.ok) {
|
|
51
|
+
throw new error_1.AppError('DOWNLOAD_FAILED', `下载导入包失败(${(0, upload_1.redactUrl)(url)}):HTTP ${String(resp.status)}`, {
|
|
52
|
+
retryable: resp.status >= 500 || resp.status === 429,
|
|
53
|
+
next_actions: ['预签名 URL 可能已过期,重新生成后重试'],
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
const declared = Number(resp.headers.get('content-length') ?? '');
|
|
57
|
+
if (Number.isFinite(declared) && declared > exports.MAX_IMPORT_ZIP_BYTES) {
|
|
58
|
+
throw tooLarge(declared);
|
|
59
|
+
}
|
|
60
|
+
const chunks = [];
|
|
61
|
+
let total = 0;
|
|
62
|
+
try {
|
|
63
|
+
if (resp.body) {
|
|
64
|
+
const reader = resp.body.getReader();
|
|
65
|
+
for (;;) {
|
|
66
|
+
const { done, value } = await reader.read();
|
|
67
|
+
if (done)
|
|
68
|
+
break;
|
|
69
|
+
total += value.byteLength;
|
|
70
|
+
if (total > exports.MAX_IMPORT_ZIP_BYTES) {
|
|
71
|
+
await reader.cancel();
|
|
72
|
+
throw tooLarge(total);
|
|
73
|
+
}
|
|
74
|
+
chunks.push(Buffer.from(value));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
if (err instanceof error_1.AppError)
|
|
80
|
+
throw err;
|
|
81
|
+
throw new error_1.AppError('DOWNLOAD_FAILED', `下载导入包失败(${(0, upload_1.redactUrl)(url)}):${err instanceof Error ? err.message : String(err)}`, { retryable: true });
|
|
82
|
+
}
|
|
83
|
+
node_fs_1.default.writeFileSync(destFile, Buffer.concat(chunks, total));
|
|
84
|
+
return total;
|
|
85
|
+
}
|
|
86
|
+
/** 校验本地 zip:存在、大小不超限。 */
|
|
87
|
+
function assertLocalZip(zipPath) {
|
|
88
|
+
let stat;
|
|
89
|
+
try {
|
|
90
|
+
stat = node_fs_1.default.statSync(zipPath);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
throw new error_1.AppError('ARGS_INVALID', `--zip 文件不存在:${zipPath}`);
|
|
94
|
+
}
|
|
95
|
+
if (!stat.isFile()) {
|
|
96
|
+
throw new error_1.AppError('ARGS_INVALID', `--zip 不是文件:${zipPath}`);
|
|
97
|
+
}
|
|
98
|
+
if (stat.size > exports.MAX_IMPORT_ZIP_BYTES)
|
|
99
|
+
throw tooLarge(stat.size);
|
|
100
|
+
}
|
|
101
|
+
function isZipMagic(head) {
|
|
102
|
+
// PK\x03\x04(常规)/ PK\x05\x06(空归档)
|
|
103
|
+
return (head.length >= 4 &&
|
|
104
|
+
head[0] === 0x50 &&
|
|
105
|
+
head[1] === 0x4b &&
|
|
106
|
+
((head[2] === 0x03 && head[3] === 0x04) || (head[2] === 0x05 && head[3] === 0x06)));
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* 解压导入包到 destDir(工程外的临时目录)。非 zip / 解压失败 → INVALID_ZIP。
|
|
110
|
+
*
|
|
111
|
+
* unzip 对含 "../" 段等可疑条目会剥掉危险段继续解、并以退出码 1(warning)结束;
|
|
112
|
+
* 这种情况视为成功并记 warning —— 解出的文件仍落在 destDir 内,随后按规则过滤。
|
|
113
|
+
*/
|
|
114
|
+
function extractImportZip(zipPath, destDir) {
|
|
115
|
+
const fd = node_fs_1.default.openSync(zipPath, 'r');
|
|
116
|
+
const head = Buffer.alloc(4);
|
|
117
|
+
try {
|
|
118
|
+
node_fs_1.default.readSync(fd, head, 0, 4, 0);
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
node_fs_1.default.closeSync(fd);
|
|
122
|
+
}
|
|
123
|
+
if (!isZipMagic(head)) {
|
|
124
|
+
throw new error_1.AppError('INVALID_ZIP', `导入包不是合法 zip:${node_path_1.default.basename(zipPath)}`);
|
|
125
|
+
}
|
|
126
|
+
node_fs_1.default.mkdirSync(destDir, { recursive: true });
|
|
127
|
+
try {
|
|
128
|
+
(0, archive_1.extractZip)(zipPath, destDir);
|
|
129
|
+
return { warnings: [] };
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
if (err instanceof error_1.AppError) {
|
|
133
|
+
throw new error_1.AppError('INVALID_ZIP', `解压导入包失败:${err.message}`);
|
|
134
|
+
}
|
|
135
|
+
const status = err.status;
|
|
136
|
+
if (status === 1 && node_fs_1.default.readdirSync(destDir).length > 0) {
|
|
137
|
+
return { warnings: ['解压时 unzip 报告了 warning(可疑条目已被剥离 / 跳过)'] };
|
|
138
|
+
}
|
|
139
|
+
throw new error_1.AppError('INVALID_ZIP', `解压导入包失败:${err instanceof Error ? err.message : String(err)}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.readJsonFile = exports.importManifestPath = exports.writeImportManifest = exports.buildImportManifest = exports.classifyDependencies = exports.IMPORT_MANIFEST_VERSION = exports.writeImportPendingMarker = exports.IMPORT_PENDING_MARKER_REL = exports.IMPORT_MANIFEST_REL = exports.FILE_COUNT_KEYS = exports.mapSourcePath = exports.applyImportPlan = exports.buildImportPlan = exports.normalizeEntries = exports.collectFiles = exports.MAX_IMPORT_ZIP_BYTES = exports.extractImportZip = exports.assertLocalZip = exports.downloadZip = exports.isGitRepoDir = exports.getImportedPrefix = exports.sanitizePathSegments = exports.isProtectedTarget = exports.isBuildChainDependency = exports.resolveImportRules = exports.parsePlatform = exports.BUILD_CHAIN_PATTERNS = exports.DEFAULT_IMPORT_RULES = exports.PLATFORM_IMPORT_RULES = exports.SOURCE_PACKAGE_DIR = exports.DEFAULT_IMPORT_PLATFORM = exports.IMPORT_PLATFORMS = void 0;
|
|
4
|
+
var rules_1 = require("./rules");
|
|
5
|
+
Object.defineProperty(exports, "IMPORT_PLATFORMS", { enumerable: true, get: function () { return rules_1.IMPORT_PLATFORMS; } });
|
|
6
|
+
Object.defineProperty(exports, "DEFAULT_IMPORT_PLATFORM", { enumerable: true, get: function () { return rules_1.DEFAULT_IMPORT_PLATFORM; } });
|
|
7
|
+
Object.defineProperty(exports, "SOURCE_PACKAGE_DIR", { enumerable: true, get: function () { return rules_1.SOURCE_PACKAGE_DIR; } });
|
|
8
|
+
Object.defineProperty(exports, "PLATFORM_IMPORT_RULES", { enumerable: true, get: function () { return rules_1.PLATFORM_IMPORT_RULES; } });
|
|
9
|
+
Object.defineProperty(exports, "DEFAULT_IMPORT_RULES", { enumerable: true, get: function () { return rules_1.DEFAULT_IMPORT_RULES; } });
|
|
10
|
+
Object.defineProperty(exports, "BUILD_CHAIN_PATTERNS", { enumerable: true, get: function () { return rules_1.BUILD_CHAIN_PATTERNS; } });
|
|
11
|
+
Object.defineProperty(exports, "parsePlatform", { enumerable: true, get: function () { return rules_1.parsePlatform; } });
|
|
12
|
+
Object.defineProperty(exports, "resolveImportRules", { enumerable: true, get: function () { return rules_1.resolveImportRules; } });
|
|
13
|
+
Object.defineProperty(exports, "isBuildChainDependency", { enumerable: true, get: function () { return rules_1.isBuildChainDependency; } });
|
|
14
|
+
Object.defineProperty(exports, "isProtectedTarget", { enumerable: true, get: function () { return rules_1.isProtectedTarget; } });
|
|
15
|
+
Object.defineProperty(exports, "sanitizePathSegments", { enumerable: true, get: function () { return rules_1.sanitizePathSegments; } });
|
|
16
|
+
Object.defineProperty(exports, "getImportedPrefix", { enumerable: true, get: function () { return rules_1.getImportedPrefix; } });
|
|
17
|
+
Object.defineProperty(exports, "isGitRepoDir", { enumerable: true, get: function () { return rules_1.isGitRepoDir; } });
|
|
18
|
+
var download_1 = require("./download");
|
|
19
|
+
Object.defineProperty(exports, "downloadZip", { enumerable: true, get: function () { return download_1.downloadZip; } });
|
|
20
|
+
Object.defineProperty(exports, "assertLocalZip", { enumerable: true, get: function () { return download_1.assertLocalZip; } });
|
|
21
|
+
Object.defineProperty(exports, "extractImportZip", { enumerable: true, get: function () { return download_1.extractImportZip; } });
|
|
22
|
+
Object.defineProperty(exports, "MAX_IMPORT_ZIP_BYTES", { enumerable: true, get: function () { return download_1.MAX_IMPORT_ZIP_BYTES; } });
|
|
23
|
+
var place_1 = require("./place");
|
|
24
|
+
Object.defineProperty(exports, "collectFiles", { enumerable: true, get: function () { return place_1.collectFiles; } });
|
|
25
|
+
Object.defineProperty(exports, "normalizeEntries", { enumerable: true, get: function () { return place_1.normalizeEntries; } });
|
|
26
|
+
Object.defineProperty(exports, "buildImportPlan", { enumerable: true, get: function () { return place_1.buildImportPlan; } });
|
|
27
|
+
Object.defineProperty(exports, "applyImportPlan", { enumerable: true, get: function () { return place_1.applyImportPlan; } });
|
|
28
|
+
Object.defineProperty(exports, "mapSourcePath", { enumerable: true, get: function () { return place_1.mapSourcePath; } });
|
|
29
|
+
Object.defineProperty(exports, "FILE_COUNT_KEYS", { enumerable: true, get: function () { return place_1.FILE_COUNT_KEYS; } });
|
|
30
|
+
var manifest_1 = require("./manifest");
|
|
31
|
+
Object.defineProperty(exports, "IMPORT_MANIFEST_REL", { enumerable: true, get: function () { return manifest_1.IMPORT_MANIFEST_REL; } });
|
|
32
|
+
Object.defineProperty(exports, "IMPORT_PENDING_MARKER_REL", { enumerable: true, get: function () { return manifest_1.IMPORT_PENDING_MARKER_REL; } });
|
|
33
|
+
Object.defineProperty(exports, "writeImportPendingMarker", { enumerable: true, get: function () { return manifest_1.writeImportPendingMarker; } });
|
|
34
|
+
Object.defineProperty(exports, "IMPORT_MANIFEST_VERSION", { enumerable: true, get: function () { return manifest_1.IMPORT_MANIFEST_VERSION; } });
|
|
35
|
+
Object.defineProperty(exports, "classifyDependencies", { enumerable: true, get: function () { return manifest_1.classifyDependencies; } });
|
|
36
|
+
Object.defineProperty(exports, "buildImportManifest", { enumerable: true, get: function () { return manifest_1.buildImportManifest; } });
|
|
37
|
+
Object.defineProperty(exports, "writeImportManifest", { enumerable: true, get: function () { return manifest_1.writeImportManifest; } });
|
|
38
|
+
Object.defineProperty(exports, "importManifestPath", { enumerable: true, get: function () { return manifest_1.importManifestPath; } });
|
|
39
|
+
Object.defineProperty(exports, "readJsonFile", { enumerable: true, get: function () { return manifest_1.readJsonFile; } });
|
|
@@ -0,0 +1,97 @@
|
|
|
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.IMPORT_PENDING_MARKER_REL = exports.IMPORT_MANIFEST_VERSION = exports.IMPORT_MANIFEST_REL = void 0;
|
|
7
|
+
exports.classifyDependencies = classifyDependencies;
|
|
8
|
+
exports.readJsonFile = readJsonFile;
|
|
9
|
+
exports.buildImportManifest = buildImportManifest;
|
|
10
|
+
exports.importManifestPath = importManifestPath;
|
|
11
|
+
exports.writeImportPendingMarker = writeImportPendingMarker;
|
|
12
|
+
exports.writeImportManifest = writeImportManifest;
|
|
13
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
14
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
15
|
+
const rules_1 = require("./rules");
|
|
16
|
+
/** manifest 相对工程根的位置。它是导入的幂等标记,必须最后写。 */
|
|
17
|
+
exports.IMPORT_MANIFEST_REL = '.spark/import/manifest.json';
|
|
18
|
+
exports.IMPORT_MANIFEST_VERSION = 1;
|
|
19
|
+
/**
|
|
20
|
+
* 「待迁移」标记:导入时与 manifest 一起写入,Coding Agent 完成迁移后删除。
|
|
21
|
+
* 沙箱 skill `import-migration` 以它的存在为加载条件,删除后该 skill 不再注入。
|
|
22
|
+
* manifest 则长期保留(导入幂等标记 + 依赖分析记录),两者职责分开。
|
|
23
|
+
*/
|
|
24
|
+
exports.IMPORT_PENDING_MARKER_REL = '.spark/import/MIGRATION_PENDING';
|
|
25
|
+
function readDepField(pkg, field) {
|
|
26
|
+
if (typeof pkg !== 'object' || pkg === null)
|
|
27
|
+
return {};
|
|
28
|
+
const v = pkg[field];
|
|
29
|
+
if (typeof v !== 'object' || v === null || Array.isArray(v))
|
|
30
|
+
return {};
|
|
31
|
+
const out = {};
|
|
32
|
+
for (const [k, ver] of Object.entries(v)) {
|
|
33
|
+
out[k] = typeof ver === 'string' ? ver : JSON.stringify(ver);
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* 依赖清单分类(只做静态分析,不装、不联网)。优先级:
|
|
39
|
+
* platform_managed(@lark-apaas/*)> present(工程已有,带 templateRange)> build_chain > missing
|
|
40
|
+
*/
|
|
41
|
+
function classifyDependencies(opts) {
|
|
42
|
+
const projectDeps = {
|
|
43
|
+
...readDepField(opts.projectPkg, 'devDependencies'),
|
|
44
|
+
...readDepField(opts.projectPkg, 'dependencies'),
|
|
45
|
+
};
|
|
46
|
+
const result = [];
|
|
47
|
+
for (const field of ['dependencies', 'devDependencies']) {
|
|
48
|
+
for (const [name, requested] of Object.entries(readDepField(opts.sourcePkg, field))) {
|
|
49
|
+
const dep = { name, requested, field, status: 'missing' };
|
|
50
|
+
if ((0, rules_1.isPlatformManagedDependency)(name))
|
|
51
|
+
dep.status = 'platform_managed';
|
|
52
|
+
else if (Object.prototype.hasOwnProperty.call(projectDeps, name)) {
|
|
53
|
+
dep.status = 'present';
|
|
54
|
+
dep.templateRange = projectDeps[name];
|
|
55
|
+
}
|
|
56
|
+
else if ((0, rules_1.isBuildChainDependency)(name))
|
|
57
|
+
dep.status = 'build_chain';
|
|
58
|
+
result.push(dep);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
/** 读 JSON 文件;不存在返回 undefined,解析失败抛出原始错误由调用方决定降级。 */
|
|
64
|
+
function readJsonFile(file) {
|
|
65
|
+
if (!node_fs_1.default.existsSync(file))
|
|
66
|
+
return undefined;
|
|
67
|
+
return JSON.parse(node_fs_1.default.readFileSync(file, 'utf-8'));
|
|
68
|
+
}
|
|
69
|
+
function buildImportManifest(opts) {
|
|
70
|
+
return {
|
|
71
|
+
version: exports.IMPORT_MANIFEST_VERSION,
|
|
72
|
+
platform: opts.platform,
|
|
73
|
+
importedAt: (opts.now ?? new Date()).toISOString(),
|
|
74
|
+
sourcePackageJson: opts.plan.hasSourcePackageJson ? `${rules_1.SOURCE_PACKAGE_DIR}/package.json` : null,
|
|
75
|
+
fileCounts: opts.plan.fileCounts,
|
|
76
|
+
dependencies: opts.dependencies,
|
|
77
|
+
excludedPaths: opts.plan.excludedPaths,
|
|
78
|
+
protectedConflicts: opts.plan.protectedConflicts,
|
|
79
|
+
replacedTemplateFiles: opts.plan.replacedTemplateFiles,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function importManifestPath(projectDir) {
|
|
83
|
+
return node_path_1.default.join(projectDir, ...exports.IMPORT_MANIFEST_REL.split('/'));
|
|
84
|
+
}
|
|
85
|
+
/** 写「待迁移」标记。必须在 manifest 之前写:manifest 是幂等标记,先写标记再写 manifest,中途失败重跑仍会补齐两者。 */
|
|
86
|
+
function writeImportPendingMarker(projectDir) {
|
|
87
|
+
const file = node_path_1.default.join(projectDir, ...exports.IMPORT_PENDING_MARKER_REL.split('/'));
|
|
88
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(file), { recursive: true });
|
|
89
|
+
node_fs_1.default.writeFileSync(file, '本工程由外部项目导入,用户代码尚未迁移完成。迁移全部完成后删除本文件。\n', 'utf-8');
|
|
90
|
+
}
|
|
91
|
+
function writeImportManifest(projectDir, manifest) {
|
|
92
|
+
const file = importManifestPath(projectDir);
|
|
93
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(file), { recursive: true });
|
|
94
|
+
const tmp = `${file}.tmp-${String(process.pid)}`;
|
|
95
|
+
node_fs_1.default.writeFileSync(tmp, JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
|
|
96
|
+
node_fs_1.default.renameSync(tmp, file);
|
|
97
|
+
}
|
|
@@ -0,0 +1,327 @@
|
|
|
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.FILE_COUNT_KEYS = void 0;
|
|
7
|
+
exports.collectFiles = collectFiles;
|
|
8
|
+
exports.normalizeEntries = normalizeEntries;
|
|
9
|
+
exports.listProjectUiComponents = listProjectUiComponents;
|
|
10
|
+
exports.mapSourcePath = mapSourcePath;
|
|
11
|
+
exports.buildImportPlan = buildImportPlan;
|
|
12
|
+
exports.applyImportPlan = applyImportPlan;
|
|
13
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
14
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
15
|
+
const error_1 = require("../../../utils/error");
|
|
16
|
+
const rules_1 = require("./rules");
|
|
17
|
+
exports.FILE_COUNT_KEYS = [
|
|
18
|
+
'client/src',
|
|
19
|
+
'client/public',
|
|
20
|
+
'shared',
|
|
21
|
+
rules_1.SOURCE_PACKAGE_DIR,
|
|
22
|
+
];
|
|
23
|
+
/** 递归列出目录下的普通文件(posix 相对路径)。符号链接一律跳过,防止 zip 里的链接把工程外文件带进来。 */
|
|
24
|
+
function collectFiles(rootDir) {
|
|
25
|
+
const out = [];
|
|
26
|
+
const walk = (rel) => {
|
|
27
|
+
for (const entry of node_fs_1.default.readdirSync(node_path_1.default.join(rootDir, rel), { withFileTypes: true })) {
|
|
28
|
+
const childRel = rel ? `${rel}/${entry.name}` : entry.name;
|
|
29
|
+
if (entry.isDirectory())
|
|
30
|
+
walk(childRel);
|
|
31
|
+
else if (entry.isFile())
|
|
32
|
+
out.push(childRel);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
walk('');
|
|
36
|
+
return out.sort();
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* 规范化导入条目(对齐 merger.go extractZip + normalizeImportedFiles):
|
|
40
|
+
* 1. 丢弃含 ".."、__MACOSX/、.DS_Store
|
|
41
|
+
* 2. 非 gemini:剥公共顶层目录(getImportedPrefix);gemini 保留原始路径
|
|
42
|
+
* 3. 超长路径段 hash 截断
|
|
43
|
+
* 4. 丢弃 .git 元数据与 node_modules / dist / build 目录
|
|
44
|
+
*/
|
|
45
|
+
function normalizeEntries(rawPaths, platform) {
|
|
46
|
+
const kept = rawPaths.filter((p) => !(0, rules_1.isRawEntrySkipped)(p));
|
|
47
|
+
const prefix = platform === 'gemini' ? '' : (0, rules_1.getImportedPrefix)(kept);
|
|
48
|
+
const entries = [];
|
|
49
|
+
const renamed = [];
|
|
50
|
+
for (const raw of kept) {
|
|
51
|
+
if ((0, rules_1.isGitRepoDir)(raw))
|
|
52
|
+
continue;
|
|
53
|
+
const stripped = (0, rules_1.removePrefix)(prefix, raw);
|
|
54
|
+
if (!stripped)
|
|
55
|
+
continue;
|
|
56
|
+
const normalized = (0, rules_1.sanitizePathSegments)(stripped);
|
|
57
|
+
if (normalized !== stripped)
|
|
58
|
+
renamed.push({ from: stripped, to: normalized });
|
|
59
|
+
if ((0, rules_1.isGitRepoDir)(normalized) || (0, rules_1.hasSkippedDirSegment)(normalized))
|
|
60
|
+
continue;
|
|
61
|
+
entries.push({ raw, normalized });
|
|
62
|
+
}
|
|
63
|
+
return { entries, renamed };
|
|
64
|
+
}
|
|
65
|
+
const UI_PREFIX_SRC = 'src/components/ui/';
|
|
66
|
+
const UI_PREFIX_PROJECT = 'client/src/components/ui/';
|
|
67
|
+
const JS_EXTS = new Set(['.js', '.jsx']);
|
|
68
|
+
const TS_EXTS = new Set(['.ts', '.tsx']);
|
|
69
|
+
function stripExt(name) {
|
|
70
|
+
const ext = node_path_1.default.posix.extname(name);
|
|
71
|
+
return ext ? name.slice(0, -ext.length) : name;
|
|
72
|
+
}
|
|
73
|
+
/** 工程已有的 UI 组件:basename(去扩展名)→ 相对工程根路径(仅 ui/ 直接子文件)。 */
|
|
74
|
+
function listProjectUiComponents(projectDir) {
|
|
75
|
+
const result = new Map();
|
|
76
|
+
const uiDir = node_path_1.default.join(projectDir, ...UI_PREFIX_PROJECT.split('/'));
|
|
77
|
+
let names;
|
|
78
|
+
try {
|
|
79
|
+
names = node_fs_1.default.readdirSync(uiDir, { withFileTypes: true });
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
for (const d of names) {
|
|
85
|
+
if (!d.isFile())
|
|
86
|
+
continue;
|
|
87
|
+
const base = stripExt(d.name);
|
|
88
|
+
const list = result.get(base) ?? [];
|
|
89
|
+
list.push(`${UI_PREFIX_PROJECT}${d.name}`);
|
|
90
|
+
result.set(base, list);
|
|
91
|
+
}
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
function mapByPrefix(src, from, to) {
|
|
95
|
+
return src.startsWith(from) ? to + src.slice(from.length) : undefined;
|
|
96
|
+
}
|
|
97
|
+
/** 按平台把规范化源路径映射到工程目标路径(对齐 merger.go 各 merge* 分支)。 */
|
|
98
|
+
function mapSourcePath(src, platform, rules, uiComponents) {
|
|
99
|
+
const fallback = `${rules_1.SOURCE_PACKAGE_DIR}/${src}`;
|
|
100
|
+
switch (platform) {
|
|
101
|
+
case 'lovable': {
|
|
102
|
+
if (src.startsWith('src/')) {
|
|
103
|
+
if ((0, rules_1.isExcludedPath)(src, rules.excludedPaths))
|
|
104
|
+
return { target: fallback, excluded: true };
|
|
105
|
+
return { target: `client/${src}`, excluded: false };
|
|
106
|
+
}
|
|
107
|
+
const pub = mapByPrefix(src, 'public/', 'client/public/');
|
|
108
|
+
return { target: pub ?? fallback, excluded: false };
|
|
109
|
+
}
|
|
110
|
+
case 'replit': {
|
|
111
|
+
if (src.startsWith('client/src/')) {
|
|
112
|
+
if ((0, rules_1.isExcludedPath)(src, rules.excludedPaths))
|
|
113
|
+
return { target: fallback, excluded: true };
|
|
114
|
+
return { target: src, excluded: false };
|
|
115
|
+
}
|
|
116
|
+
if (src.startsWith('shared/'))
|
|
117
|
+
return { target: src, excluded: false };
|
|
118
|
+
const assets = mapByPrefix(src, 'attached_assets/', 'client/public/assets/');
|
|
119
|
+
return { target: assets ?? fallback, excluded: false };
|
|
120
|
+
}
|
|
121
|
+
case 'gemini': {
|
|
122
|
+
const mapped = mapByPrefix(src, 'components/', 'client/src/components/') ??
|
|
123
|
+
mapByPrefix(src, 'services/', 'client/src/services/') ??
|
|
124
|
+
(src === 'types.ts' ? 'client/src/types.ts' : undefined) ??
|
|
125
|
+
(src === 'constants.ts' ? 'client/src/constants.ts' : undefined);
|
|
126
|
+
return { target: mapped ?? fallback, excluded: false };
|
|
127
|
+
}
|
|
128
|
+
case 'zip': {
|
|
129
|
+
if (src.startsWith('src/')) {
|
|
130
|
+
if (src.startsWith(UI_PREFIX_SRC)) {
|
|
131
|
+
const rest = src.slice(UI_PREFIX_SRC.length);
|
|
132
|
+
if (!rest.includes('/')) {
|
|
133
|
+
const existing = uiComponents.get(stripExt(rest));
|
|
134
|
+
if (existing && existing.length > 0) {
|
|
135
|
+
// 模板业务组件依赖 TS 版 UI 组件的类型,JS 版替换后会类型报错,保留模板版本
|
|
136
|
+
const tsVersion = existing.find((p) => TS_EXTS.has(node_path_1.default.posix.extname(p)));
|
|
137
|
+
if (tsVersion && JS_EXTS.has(node_path_1.default.posix.extname(rest))) {
|
|
138
|
+
return { target: fallback, excluded: false, uiKept: tsVersion };
|
|
139
|
+
}
|
|
140
|
+
return { target: `client/${src}`, excluded: false, uiReplaced: existing };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if ((0, rules_1.isExcludedPath)(src, rules.excludedPaths))
|
|
145
|
+
return { target: fallback, excluded: true };
|
|
146
|
+
return { target: `client/${src}`, excluded: false };
|
|
147
|
+
}
|
|
148
|
+
const pub = mapByPrefix(src, 'public/', 'client/public/');
|
|
149
|
+
return { target: pub ?? fallback, excluded: false };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function countKey(target) {
|
|
154
|
+
for (const key of exports.FILE_COUNT_KEYS) {
|
|
155
|
+
if (target === key || target.startsWith(`${key}/`))
|
|
156
|
+
return key;
|
|
157
|
+
}
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
/** 计算完整放置计划(纯计算 + 只读工程目录,不写任何文件)。 */
|
|
161
|
+
function buildImportPlan(opts) {
|
|
162
|
+
const uiComponents = opts.platform === 'zip'
|
|
163
|
+
? listProjectUiComponents(opts.projectDir)
|
|
164
|
+
: new Map();
|
|
165
|
+
const byTarget = new Map();
|
|
166
|
+
const excludedPaths = [];
|
|
167
|
+
const protectedConflicts = [];
|
|
168
|
+
const replaced = new Set();
|
|
169
|
+
const warnings = [];
|
|
170
|
+
for (const entry of opts.entries) {
|
|
171
|
+
const src = entry.normalized;
|
|
172
|
+
const mapped = mapSourcePath(src, opts.platform, opts.rules, uiComponents);
|
|
173
|
+
let target = mapped.target;
|
|
174
|
+
if (mapped.excluded)
|
|
175
|
+
excludedPaths.push(src);
|
|
176
|
+
if ((0, rules_1.isProtectedTarget)(target)) {
|
|
177
|
+
const placedAt = `${rules_1.SOURCE_PACKAGE_DIR}/${src}`;
|
|
178
|
+
protectedConflicts.push({ source: src, target, placedAt });
|
|
179
|
+
target = placedAt;
|
|
180
|
+
}
|
|
181
|
+
else if (mapped.uiKept) {
|
|
182
|
+
protectedConflicts.push({ source: src, target: mapped.uiKept, placedAt: target });
|
|
183
|
+
}
|
|
184
|
+
else if (mapped.uiReplaced) {
|
|
185
|
+
for (const p of mapped.uiReplaced)
|
|
186
|
+
replaced.add(p);
|
|
187
|
+
}
|
|
188
|
+
if (byTarget.has(target)) {
|
|
189
|
+
warnings.push(`多个导入文件映射到同一目标,后者覆盖前者:${target}`);
|
|
190
|
+
}
|
|
191
|
+
byTarget.set(target, { source: src, raw: entry.raw, target });
|
|
192
|
+
}
|
|
193
|
+
const placements = [...byTarget.values()];
|
|
194
|
+
const fileCounts = {
|
|
195
|
+
'client/src': 0,
|
|
196
|
+
'client/public': 0,
|
|
197
|
+
shared: 0,
|
|
198
|
+
source_package: 0,
|
|
199
|
+
};
|
|
200
|
+
for (const p of placements) {
|
|
201
|
+
const key = countKey(p.target);
|
|
202
|
+
if (key)
|
|
203
|
+
fileCounts[key] += 1;
|
|
204
|
+
}
|
|
205
|
+
const replacedTemplateFiles = [...replaced].sort();
|
|
206
|
+
const deletions = replacedTemplateFiles.filter((p) => !byTarget.has(p));
|
|
207
|
+
const hasSourcePackageJson = byTarget.has(`${rules_1.SOURCE_PACKAGE_DIR}/package.json`);
|
|
208
|
+
const overwrites = placements
|
|
209
|
+
.map((p) => p.target)
|
|
210
|
+
.filter((t) => !replaced.has(t) && node_fs_1.default.existsSync(node_path_1.default.join(opts.projectDir, ...t.split('/'))))
|
|
211
|
+
.sort();
|
|
212
|
+
return {
|
|
213
|
+
placements,
|
|
214
|
+
replacedTemplateFiles,
|
|
215
|
+
deletions,
|
|
216
|
+
excludedPaths,
|
|
217
|
+
protectedConflicts,
|
|
218
|
+
fileCounts,
|
|
219
|
+
hasSourcePackageJson,
|
|
220
|
+
overwrites,
|
|
221
|
+
warnings,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function writeFailed(stage, err) {
|
|
225
|
+
return new error_1.AppError('WRITE_FAILED', `导入写入失败(${stage}):${err instanceof Error ? err.message : String(err)}`, { next_actions: ['检查磁盘空间 / 目录权限后重试 miaoda app import'] });
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* 预检移入目标:rename 遇到「目标已是目录」或「父路径上有普通文件」必然失败,
|
|
229
|
+
* 在动工程之前拦下,避免移入到一半才报错。
|
|
230
|
+
*/
|
|
231
|
+
function preflightTargets(plan, projectDir) {
|
|
232
|
+
for (const p of plan.placements) {
|
|
233
|
+
const segs = p.target.split('/');
|
|
234
|
+
for (let i = 1; i < segs.length; i += 1) {
|
|
235
|
+
const parent = node_path_1.default.join(projectDir, ...segs.slice(0, i));
|
|
236
|
+
if (node_fs_1.default.existsSync(parent) && !node_fs_1.default.statSync(parent).isDirectory()) {
|
|
237
|
+
throw writeFailed('preflight', new Error(`目标父路径是文件:${segs.slice(0, i).join('/')}(${p.target})`));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const dest = node_path_1.default.join(projectDir, ...segs);
|
|
241
|
+
if (node_fs_1.default.existsSync(dest) && node_fs_1.default.statSync(dest).isDirectory()) {
|
|
242
|
+
throw writeFailed('preflight', new Error(`目标路径已是目录:${p.target}`));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/** 把工程里已有的 rel 挪进 backupDir,返回暂存位置;不存在则返回 undefined。 */
|
|
247
|
+
function backupExisting(projectDir, backupDir, rel) {
|
|
248
|
+
const dest = node_path_1.default.join(projectDir, ...rel.split('/'));
|
|
249
|
+
if (!node_fs_1.default.existsSync(dest))
|
|
250
|
+
return undefined;
|
|
251
|
+
const backup = node_path_1.default.join(backupDir, ...rel.split('/'));
|
|
252
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(backup), { recursive: true });
|
|
253
|
+
node_fs_1.default.renameSync(dest, backup);
|
|
254
|
+
return backup;
|
|
255
|
+
}
|
|
256
|
+
/** 逆序撤回已执行的移入 / 删除;返回撤回失败的路径(尽力而为,不中断)。 */
|
|
257
|
+
function rollbackMoves(records) {
|
|
258
|
+
const failed = [];
|
|
259
|
+
for (const r of [...records].reverse()) {
|
|
260
|
+
try {
|
|
261
|
+
node_fs_1.default.rmSync(r.dest, { force: true });
|
|
262
|
+
if (r.backup) {
|
|
263
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(r.dest), { recursive: true });
|
|
264
|
+
node_fs_1.default.renameSync(r.backup, r.dest);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
failed.push(r.dest);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return failed;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* 三阶段落盘:
|
|
275
|
+
* 1. 全部文件先拷进 stagingDir(工程不动);任一失败 → 清理暂存,工程保持原样
|
|
276
|
+
* 2. 预检所有移入目标(目标已是目录 / 父路径是文件);不通过 → 工程保持原样
|
|
277
|
+
* 3. 逐个移入工程、删被替换的模板 UI 组件;被覆盖或删除的原文件先挪进暂存目录下的备份,
|
|
278
|
+
* 中途失败按逆序回滚,工程恢复原样(只可能留下新建的空目录)
|
|
279
|
+
* 失败一律抛 WRITE_FAILED,调用方不写 manifest,重跑会重新导入。
|
|
280
|
+
*/
|
|
281
|
+
function applyImportPlan(opts) {
|
|
282
|
+
const { plan, extractDir, projectDir, stagingDir } = opts;
|
|
283
|
+
// 备份放暂存目录下(同一文件系统,rename 不跨设备),finally 一并清理;
|
|
284
|
+
// 放置目标都以 client/ shared/ source_package/ 开头,不会与它重名。
|
|
285
|
+
const backupDir = node_path_1.default.join(stagingDir, '.backup');
|
|
286
|
+
try {
|
|
287
|
+
try {
|
|
288
|
+
for (const p of plan.placements) {
|
|
289
|
+
const staged = node_path_1.default.join(stagingDir, ...p.target.split('/'));
|
|
290
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(staged), { recursive: true });
|
|
291
|
+
node_fs_1.default.copyFileSync(node_path_1.default.join(extractDir, ...p.raw.split('/')), staged);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
catch (err) {
|
|
295
|
+
throw writeFailed('stage', err);
|
|
296
|
+
}
|
|
297
|
+
preflightTargets(plan, projectDir);
|
|
298
|
+
const records = [];
|
|
299
|
+
try {
|
|
300
|
+
for (const p of plan.placements) {
|
|
301
|
+
const staged = node_path_1.default.join(stagingDir, ...p.target.split('/'));
|
|
302
|
+
const dest = node_path_1.default.join(projectDir, ...p.target.split('/'));
|
|
303
|
+
const backup = backupExisting(projectDir, backupDir, p.target);
|
|
304
|
+
records.push({ dest, backup });
|
|
305
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(dest), { recursive: true });
|
|
306
|
+
node_fs_1.default.renameSync(staged, dest);
|
|
307
|
+
}
|
|
308
|
+
for (const rel of plan.deletions) {
|
|
309
|
+
const backup = backupExisting(projectDir, backupDir, rel);
|
|
310
|
+
if (backup)
|
|
311
|
+
records.push({ dest: node_path_1.default.join(projectDir, ...rel.split('/')), backup });
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
catch (err) {
|
|
315
|
+
const notRestored = rollbackMoves(records);
|
|
316
|
+
if (notRestored.length > 0) {
|
|
317
|
+
throw writeFailed('move', new Error(`${err instanceof Error ? err.message : String(err)};回滚未完成:${notRestored
|
|
318
|
+
.map((d) => node_path_1.default.relative(projectDir, d))
|
|
319
|
+
.join(', ')}`));
|
|
320
|
+
}
|
|
321
|
+
throw writeFailed('move', err);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
finally {
|
|
325
|
+
node_fs_1.default.rmSync(stagingDir, { recursive: true, force: true });
|
|
326
|
+
}
|
|
327
|
+
}
|
|
@@ -0,0 +1,293 @@
|
|
|
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.PROJECT_ROOT_DIRS = exports.BUILD_CHAIN_PATTERNS = exports.DEFAULT_IMPORT_RULES = exports.PLATFORM_IMPORT_RULES = exports.MAX_PATH_SEGMENT_BYTES = exports.SOURCE_PACKAGE_DIR = exports.DEFAULT_IMPORT_PLATFORM = exports.IMPORT_PLATFORMS = void 0;
|
|
7
|
+
exports.isBuildChainDependency = isBuildChainDependency;
|
|
8
|
+
exports.isPlatformManagedDependency = isPlatformManagedDependency;
|
|
9
|
+
exports.parsePlatform = parsePlatform;
|
|
10
|
+
exports.resolveImportRules = resolveImportRules;
|
|
11
|
+
exports.isGitRepoDir = isGitRepoDir;
|
|
12
|
+
exports.isRawEntrySkipped = isRawEntrySkipped;
|
|
13
|
+
exports.hasSkippedDirSegment = hasSkippedDirSegment;
|
|
14
|
+
exports.getImportedPrefix = getImportedPrefix;
|
|
15
|
+
exports.removePrefix = removePrefix;
|
|
16
|
+
exports.sanitizePathSegments = sanitizePathSegments;
|
|
17
|
+
exports.isExcludedPath = isExcludedPath;
|
|
18
|
+
exports.isProtectedTarget = isProtectedTarget;
|
|
19
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
20
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
21
|
+
const error_1 = require("../../../utils/error");
|
|
22
|
+
const import_source_1 = require("../../../services/app/init/import-source");
|
|
23
|
+
/**
|
|
24
|
+
* 外部平台导入规则(移植自 studio_server import_svc/merger.go + tcc/import_merger_config.go)。
|
|
25
|
+
*
|
|
26
|
+
* 本文件只放纯函数与常量:路径规范化、平台排除清单、受保护目标、构建链依赖识别。
|
|
27
|
+
* 不做 IO,便于逐条对齐服务端 merger 的单测口径。
|
|
28
|
+
*/
|
|
29
|
+
exports.IMPORT_PLATFORMS = ['lovable', 'replit', 'gemini', 'zip'];
|
|
30
|
+
exports.DEFAULT_IMPORT_PLATFORM = 'zip';
|
|
31
|
+
/** 不进模板工程的文件统一落到这里(用户原 package.json 也在此:source_package/package.json)。 */
|
|
32
|
+
exports.SOURCE_PACKAGE_DIR = 'source_package';
|
|
33
|
+
/** 单个路径段字节上限;超过则 hash 截断重命名,避免沙箱解压 ENAMETOOLONG(对齐 merger.go)。 */
|
|
34
|
+
exports.MAX_PATH_SEGMENT_BYTES = 60;
|
|
35
|
+
/**
|
|
36
|
+
* 平台排除路径(取线上 TCC ImportMergerConfig 的 excluded_paths,而非代码兜底值)。
|
|
37
|
+
* 实际生效 = 平台清单 + DEFAULT 清单(对齐 tcc.GetImportExcludedPaths)。
|
|
38
|
+
*
|
|
39
|
+
* 不再有排除依赖清单:导入不合并 package.json,依赖由 Coding Agent 参考 manifest 自行安装、
|
|
40
|
+
* 遇到冲突自行处理。
|
|
41
|
+
*/
|
|
42
|
+
exports.PLATFORM_IMPORT_RULES = {
|
|
43
|
+
lovable: {
|
|
44
|
+
excludedPaths: [
|
|
45
|
+
'src/App.tsx',
|
|
46
|
+
'src/main.tsx',
|
|
47
|
+
'src/index.css',
|
|
48
|
+
'src/hooks/use-mobile.tsx',
|
|
49
|
+
'src/integrations/supabase',
|
|
50
|
+
'src/lib/supabase.ts',
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
replit: {
|
|
54
|
+
excludedPaths: [
|
|
55
|
+
'client/src/App.tsx',
|
|
56
|
+
'client/src/main.tsx',
|
|
57
|
+
'client/src/index.css',
|
|
58
|
+
'client/src/hooks/use-mobile.tsx',
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
gemini: {
|
|
62
|
+
excludedPaths: [],
|
|
63
|
+
},
|
|
64
|
+
zip: {
|
|
65
|
+
excludedPaths: [
|
|
66
|
+
'src/db/',
|
|
67
|
+
'src/integrations/',
|
|
68
|
+
'src/hooks/use-supabase-upload.ts',
|
|
69
|
+
'src/hooks/use-mobile.tsx',
|
|
70
|
+
'src/App.tsx',
|
|
71
|
+
'src/main.tsx',
|
|
72
|
+
'src/index.css',
|
|
73
|
+
'src/App.jsx',
|
|
74
|
+
'src/main.jsx',
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
exports.DEFAULT_IMPORT_RULES = {
|
|
79
|
+
excludedPaths: [],
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* 构建链 / 工具链依赖:模板已有自己的构建体系,用户包里的这类依赖通常不需要装。
|
|
83
|
+
* 支持精确名与 `*` 通配(只在末尾或 scope 内使用),集中在此一处便于扩展。
|
|
84
|
+
*/
|
|
85
|
+
exports.BUILD_CHAIN_PATTERNS = [
|
|
86
|
+
'vite',
|
|
87
|
+
'@vitejs/*',
|
|
88
|
+
'vite-plugin-*',
|
|
89
|
+
'@vite-pwa/*',
|
|
90
|
+
'rollup',
|
|
91
|
+
'rollup-plugin-*',
|
|
92
|
+
'@rollup/*',
|
|
93
|
+
'esbuild',
|
|
94
|
+
'webpack',
|
|
95
|
+
'webpack-*',
|
|
96
|
+
'@babel/*',
|
|
97
|
+
'babel-*',
|
|
98
|
+
'eslint',
|
|
99
|
+
'eslint-*',
|
|
100
|
+
'@eslint/*',
|
|
101
|
+
'@typescript-eslint/*',
|
|
102
|
+
'typescript-eslint',
|
|
103
|
+
'typescript',
|
|
104
|
+
'tailwindcss',
|
|
105
|
+
'@tailwindcss/*',
|
|
106
|
+
'postcss',
|
|
107
|
+
'postcss-*',
|
|
108
|
+
'autoprefixer',
|
|
109
|
+
'prettier',
|
|
110
|
+
'prettier-*',
|
|
111
|
+
'jest',
|
|
112
|
+
'@jest/*',
|
|
113
|
+
'ts-jest',
|
|
114
|
+
'vitest',
|
|
115
|
+
'@vitest/*',
|
|
116
|
+
'react-scripts',
|
|
117
|
+
'@vue/cli-*',
|
|
118
|
+
'@types/*',
|
|
119
|
+
'lovable-tagger',
|
|
120
|
+
'miaoda-sc-plugin',
|
|
121
|
+
'@builder.io/vite-plugin-jsx-loc',
|
|
122
|
+
];
|
|
123
|
+
function globToRegExp(pattern) {
|
|
124
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\/]/g, '\\$&').replace(/\*/g, '.*');
|
|
125
|
+
return new RegExp(`^${escaped}$`);
|
|
126
|
+
}
|
|
127
|
+
const BUILD_CHAIN_REGEXPS = exports.BUILD_CHAIN_PATTERNS.map(globToRegExp);
|
|
128
|
+
function isBuildChainDependency(name) {
|
|
129
|
+
return BUILD_CHAIN_REGEXPS.some((re) => re.test(name));
|
|
130
|
+
}
|
|
131
|
+
/** @lark-apaas/* 是平台托管依赖,版本由模板 / sync 管,不能按用户包装。 */
|
|
132
|
+
function isPlatformManagedDependency(name) {
|
|
133
|
+
return name.startsWith('@lark-apaas/');
|
|
134
|
+
}
|
|
135
|
+
function parsePlatform(raw) {
|
|
136
|
+
const value = (raw ?? exports.DEFAULT_IMPORT_PLATFORM).trim().toLowerCase();
|
|
137
|
+
if (exports.IMPORT_PLATFORMS.includes(value))
|
|
138
|
+
return value;
|
|
139
|
+
throw new error_1.AppError('ARGS_INVALID', `--platform 取值非法:${raw ?? ''}(可选 ${exports.IMPORT_PLATFORMS.join(' / ')})`);
|
|
140
|
+
}
|
|
141
|
+
function isStringArray(v) {
|
|
142
|
+
return Array.isArray(v) && v.every((x) => typeof x === 'string');
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* 解析生效规则:平台清单 + DEFAULT 清单;`--rules-json` 提供的字段整体替换对应默认值
|
|
146
|
+
* (未提供的字段保留默认)。
|
|
147
|
+
*/
|
|
148
|
+
function resolveImportRules(platform, rulesJson) {
|
|
149
|
+
const rules = {
|
|
150
|
+
excludedPaths: [
|
|
151
|
+
...exports.PLATFORM_IMPORT_RULES[platform].excludedPaths,
|
|
152
|
+
...exports.DEFAULT_IMPORT_RULES.excludedPaths,
|
|
153
|
+
],
|
|
154
|
+
};
|
|
155
|
+
if (rulesJson === undefined || rulesJson.trim() === '')
|
|
156
|
+
return rules;
|
|
157
|
+
let parsed;
|
|
158
|
+
try {
|
|
159
|
+
parsed = JSON.parse(rulesJson);
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
163
|
+
throw new error_1.AppError('ARGS_INVALID', `--rules-json 必须是合法 JSON:${msg}`);
|
|
164
|
+
}
|
|
165
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
166
|
+
throw new error_1.AppError('ARGS_INVALID', '--rules-json 必须是 JSON 对象');
|
|
167
|
+
}
|
|
168
|
+
const obj = parsed;
|
|
169
|
+
for (const key of ['excludedPaths']) {
|
|
170
|
+
const v = obj[key];
|
|
171
|
+
if (v === undefined)
|
|
172
|
+
continue;
|
|
173
|
+
if (!isStringArray(v)) {
|
|
174
|
+
throw new error_1.AppError('ARGS_INVALID', `--rules-json.${key} 必须是字符串数组`);
|
|
175
|
+
}
|
|
176
|
+
rules[key] = [...v];
|
|
177
|
+
}
|
|
178
|
+
return rules;
|
|
179
|
+
}
|
|
180
|
+
// ── 路径规范化(对齐 merger.go) ────────────
|
|
181
|
+
/** .git 元数据:精确 .git、.git/ 前缀、嵌套 /.git/。不误伤 .gitignore / .github / .gitattributes。 */
|
|
182
|
+
function isGitRepoDir(p) {
|
|
183
|
+
return p === '.git' || p.startsWith('.git/') || p.includes('/.git/');
|
|
184
|
+
}
|
|
185
|
+
/** zip 解出来的原始条目里需要直接丢弃的(merger.go extractZip 的过滤口径)。 */
|
|
186
|
+
function isRawEntrySkipped(p) {
|
|
187
|
+
if (p.includes('..'))
|
|
188
|
+
return true;
|
|
189
|
+
if (p.startsWith('__MACOSX/'))
|
|
190
|
+
return true;
|
|
191
|
+
if (p.endsWith('.DS_Store'))
|
|
192
|
+
return true;
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
/** 目录段命中 node_modules / dist / build(任意层级)即跳过;与 `app init --source-path` 同口径。 */
|
|
196
|
+
function hasSkippedDirSegment(p) {
|
|
197
|
+
const segments = p.split('/');
|
|
198
|
+
segments.pop();
|
|
199
|
+
return segments.some((seg) => seg !== '.git' && import_source_1.IMPORT_SKIP_DIRS.has(seg));
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* 放置规则本身认识的工程目录名。用户只压了这些目录(如只打包 src/)时,公共首段就是工程目录本身,
|
|
203
|
+
* 不是外层包裹目录,不能剥——否则 src/** 会整体落进 source_package/(服务端 merger.go 的同款缺陷,此处修正)。
|
|
204
|
+
*/
|
|
205
|
+
exports.PROJECT_ROOT_DIRS = new Set([
|
|
206
|
+
'src',
|
|
207
|
+
'public',
|
|
208
|
+
'client',
|
|
209
|
+
'server',
|
|
210
|
+
'shared',
|
|
211
|
+
'components',
|
|
212
|
+
'services',
|
|
213
|
+
'attached_assets',
|
|
214
|
+
]);
|
|
215
|
+
/**
|
|
216
|
+
* 导入包的公共顶层目录:所有条目都至少两段、首段一致且首段不含 ".",并且首段不是工程目录名
|
|
217
|
+
* (PROJECT_ROOT_DIRS),才认为是包裹目录。否则返回 ""(不剥前缀)。
|
|
218
|
+
*/
|
|
219
|
+
function getImportedPrefix(paths) {
|
|
220
|
+
let common = '';
|
|
221
|
+
let first = true;
|
|
222
|
+
for (const p of paths) {
|
|
223
|
+
const idx = p.indexOf('/');
|
|
224
|
+
if (idx < 0)
|
|
225
|
+
return '';
|
|
226
|
+
const head = p.slice(0, idx);
|
|
227
|
+
if (head.includes('.'))
|
|
228
|
+
return '';
|
|
229
|
+
if (first) {
|
|
230
|
+
common = head;
|
|
231
|
+
first = false;
|
|
232
|
+
}
|
|
233
|
+
else if (head !== common) {
|
|
234
|
+
return '';
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return exports.PROJECT_ROOT_DIRS.has(common) ? '' : common;
|
|
238
|
+
}
|
|
239
|
+
function removePrefix(prefix, p) {
|
|
240
|
+
if (!prefix)
|
|
241
|
+
return p;
|
|
242
|
+
return p.startsWith(`${prefix}/`) ? p.slice(prefix.length + 1) : p;
|
|
243
|
+
}
|
|
244
|
+
/** 超过 60 字节的路径段改名为 truncated-<sha256 前 8 字节 hex><ext>(ext 长度 ≤ 20 才保留)。 */
|
|
245
|
+
function sanitizePathSegments(p) {
|
|
246
|
+
const segments = p.split('/');
|
|
247
|
+
let rewritten = false;
|
|
248
|
+
for (let i = 0; i < segments.length; i += 1) {
|
|
249
|
+
const seg = segments[i];
|
|
250
|
+
if (Buffer.byteLength(seg, 'utf-8') <= exports.MAX_PATH_SEGMENT_BYTES)
|
|
251
|
+
continue;
|
|
252
|
+
let ext = node_path_1.default.posix.extname(seg);
|
|
253
|
+
if (Buffer.byteLength(ext, 'utf-8') > 20)
|
|
254
|
+
ext = '';
|
|
255
|
+
const sum = node_crypto_1.default.createHash('sha256').update(seg, 'utf-8').digest('hex');
|
|
256
|
+
segments[i] = `truncated-${sum.slice(0, 16)}${ext}`;
|
|
257
|
+
rewritten = true;
|
|
258
|
+
}
|
|
259
|
+
return rewritten ? segments.join('/') : p;
|
|
260
|
+
}
|
|
261
|
+
function isExcludedPath(p, excludedPaths) {
|
|
262
|
+
return excludedPaths.some((prefix) => prefix !== '' && p.startsWith(prefix));
|
|
263
|
+
}
|
|
264
|
+
// ── 受保护目标 ────────────
|
|
265
|
+
const PROTECTED_ROOT_FILES = new Set([
|
|
266
|
+
'package.json',
|
|
267
|
+
'package-lock.json',
|
|
268
|
+
'.npmrc',
|
|
269
|
+
'AGENTS.md',
|
|
270
|
+
'server/main.ts',
|
|
271
|
+
'client/src/index.tsx',
|
|
272
|
+
]);
|
|
273
|
+
const PROTECTED_ROOT_DIRS = ['.githooks/', 'scripts/', '.spark/', 'node_modules/'];
|
|
274
|
+
const PROTECTED_ROOT_PATTERNS = [
|
|
275
|
+
/^tsconfig.*\.json$/,
|
|
276
|
+
/^vite\.config\..+$/,
|
|
277
|
+
/^eslint\.config\..+$/,
|
|
278
|
+
];
|
|
279
|
+
/**
|
|
280
|
+
* 工程内模板托管、导入绝不能新建 / 覆盖的目标路径(相对工程根)。
|
|
281
|
+
* source_package/ 是导入的分流目录,本身不受保护。
|
|
282
|
+
*/
|
|
283
|
+
function isProtectedTarget(target) {
|
|
284
|
+
if (target.startsWith(`${exports.SOURCE_PACKAGE_DIR}/`))
|
|
285
|
+
return false;
|
|
286
|
+
if (PROTECTED_ROOT_FILES.has(target))
|
|
287
|
+
return true;
|
|
288
|
+
if (PROTECTED_ROOT_DIRS.some((d) => target.startsWith(d)))
|
|
289
|
+
return true;
|
|
290
|
+
if (!target.includes('/') && PROTECTED_ROOT_PATTERNS.some((re) => re.test(target)))
|
|
291
|
+
return true;
|
|
292
|
+
return false;
|
|
293
|
+
}
|