@co-engram/openclaw 0.1.0
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/LICENSE +21 -0
- package/README.md +203 -0
- package/dist/adapter.d.ts +36 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +330 -0
- package/dist/adapter.js.map +1 -0
- package/dist/auto-onboard.d.ts +19 -0
- package/dist/auto-onboard.d.ts.map +1 -0
- package/dist/auto-onboard.js +35 -0
- package/dist/auto-onboard.js.map +1 -0
- package/dist/entry-example.d.ts +10 -0
- package/dist/entry-example.d.ts.map +1 -0
- package/dist/entry-example.js +10 -0
- package/dist/entry-example.js.map +1 -0
- package/dist/entry.d.ts +96 -0
- package/dist/entry.d.ts.map +1 -0
- package/dist/entry.js +228 -0
- package/dist/entry.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/json-schemas.d.ts +35 -0
- package/dist/json-schemas.d.ts.map +1 -0
- package/dist/json-schemas.js +432 -0
- package/dist/json-schemas.js.map +1 -0
- package/dist/list-intent-detector.d.ts +20 -0
- package/dist/list-intent-detector.d.ts.map +1 -0
- package/dist/list-intent-detector.js +50 -0
- package/dist/list-intent-detector.js.map +1 -0
- package/dist/llm-client.d.ts +29 -0
- package/dist/llm-client.d.ts.map +1 -0
- package/dist/llm-client.js +112 -0
- package/dist/llm-client.js.map +1 -0
- package/dist/maintenance-runtime.d.ts +31 -0
- package/dist/maintenance-runtime.d.ts.map +1 -0
- package/dist/maintenance-runtime.js +61 -0
- package/dist/maintenance-runtime.js.map +1 -0
- package/dist/memory-tools.d.ts +82 -0
- package/dist/memory-tools.d.ts.map +1 -0
- package/dist/memory-tools.js +237 -0
- package/dist/memory-tools.js.map +1 -0
- package/dist/plugin-entry.d.ts +98 -0
- package/dist/plugin-entry.d.ts.map +1 -0
- package/dist/plugin-entry.js +444 -0
- package/dist/plugin-entry.js.map +1 -0
- package/dist/prompt-builder.d.ts +48 -0
- package/dist/prompt-builder.d.ts.map +1 -0
- package/dist/prompt-builder.js +33 -0
- package/dist/prompt-builder.js.map +1 -0
- package/dist/types.d.ts +236 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +29 -0
- package/dist/types.js.map +1 -0
- package/dist/viewer-loader.d.ts +36 -0
- package/dist/viewer-loader.d.ts.map +1 -0
- package/dist/viewer-loader.js +45 -0
- package/dist/viewer-loader.js.map +1 -0
- package/openclaw.plugin.json +137 -0
- package/package.json +61 -0
- package/scripts/setup.mjs +231 -0
- package/scripts/sync-deps.mjs +247 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Co-Engram OpenClaw plugin 一键 setup
|
|
4
|
+
*
|
|
5
|
+
* 行为(idempotent,可重复跑):
|
|
6
|
+
* 1. 读取 ~/.openclaw/openclaw.json(不存在则报错并指引 `openclaw onboard`)
|
|
7
|
+
* 2. 在 plugins.entries.co-engram.config 下 merge 推荐默认值
|
|
8
|
+
* - 保留用户已显式设置的字段
|
|
9
|
+
* - 仅在缺失时填入推荐值
|
|
10
|
+
* 3. 把 memory exclusive slot 设为 "co-engram"(自动让 memory-core 失效)
|
|
11
|
+
* 4. 备份原文件到 <path>.bak.<timestamp>
|
|
12
|
+
* 5. 默认 dry-run,需 --yes 才落盘;--restart 顺带 openclaw gateway restart
|
|
13
|
+
*
|
|
14
|
+
* 用法:
|
|
15
|
+
* pnpm --filter @co-engram/openclaw setup # dry-run,只打印 diff
|
|
16
|
+
* pnpm --filter @co-engram/openclaw setup --yes # 落盘
|
|
17
|
+
* pnpm --filter @co-engram/openclaw setup --yes --restart # 落盘 + 重启
|
|
18
|
+
* npx @co-engram/openclaw setup --yes --restart # 通过 bin
|
|
19
|
+
*
|
|
20
|
+
* @module @co-engram/openclaw/scripts/setup
|
|
21
|
+
*/
|
|
22
|
+
import { readFileSync, writeFileSync, existsSync, copyFileSync } from "node:fs";
|
|
23
|
+
import { homedir } from "node:os";
|
|
24
|
+
import { join } from "node:path";
|
|
25
|
+
import { spawnSync } from "node:child_process";
|
|
26
|
+
import process from "node:process";
|
|
27
|
+
|
|
28
|
+
const OPENCLAW_CONFIG_PATH = join(homedir(), ".openclaw", "openclaw.json");
|
|
29
|
+
|
|
30
|
+
/** 推荐默认配置(仅在用户未显式设置时写入) */
|
|
31
|
+
const RECOMMENDED_CONFIG = {
|
|
32
|
+
language: "zh",
|
|
33
|
+
startMaintenance: true,
|
|
34
|
+
startViewer: true,
|
|
35
|
+
auditEnabled: true,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const PLUGIN_ID = "co-engram";
|
|
39
|
+
|
|
40
|
+
function parseArgs(argv) {
|
|
41
|
+
const flags = {
|
|
42
|
+
yes: false,
|
|
43
|
+
restart: false,
|
|
44
|
+
configPath: OPENCLAW_CONFIG_PATH,
|
|
45
|
+
};
|
|
46
|
+
for (const arg of argv.slice(2)) {
|
|
47
|
+
if (arg === "--yes" || arg === "-y") flags.yes = true;
|
|
48
|
+
else if (arg === "--restart") flags.restart = true;
|
|
49
|
+
else if (arg.startsWith("--config="))
|
|
50
|
+
flags.configPath = arg.slice("--config=".length);
|
|
51
|
+
else if (arg === "--help" || arg === "-h") {
|
|
52
|
+
printHelp();
|
|
53
|
+
process.exit(0);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return flags;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function printHelp() {
|
|
60
|
+
process.stdout.write(`co-engram openclaw setup
|
|
61
|
+
|
|
62
|
+
Usage:
|
|
63
|
+
co-engram-openclaw-setup [options]
|
|
64
|
+
|
|
65
|
+
Options:
|
|
66
|
+
--yes, -y Apply changes (default: dry-run, only print diff)
|
|
67
|
+
--restart Run \`openclaw gateway restart\` after applying
|
|
68
|
+
--config=<path> Override openclaw.json path (default: ~/.openclaw/openclaw.json)
|
|
69
|
+
--help, -h Show this help
|
|
70
|
+
|
|
71
|
+
Recommended config written to plugins.entries.co-engram.config (only when missing):
|
|
72
|
+
${JSON.stringify(RECOMMENDED_CONFIG, null, 2)
|
|
73
|
+
.split("\n")
|
|
74
|
+
.map((l) => " " + l)
|
|
75
|
+
.join("\n")}
|
|
76
|
+
`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function readConfig(path) {
|
|
80
|
+
if (!existsSync(path)) {
|
|
81
|
+
process.stderr.write(`[setup] Config not found: ${path}\n`);
|
|
82
|
+
process.stderr.write(
|
|
83
|
+
`[setup] Run \`openclaw onboard\` first to initialize OpenClaw.\n`,
|
|
84
|
+
);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function deepMergeMissing(target, defaults) {
|
|
91
|
+
const applied = {};
|
|
92
|
+
for (const [k, v] of Object.entries(defaults)) {
|
|
93
|
+
if (target[k] === undefined) {
|
|
94
|
+
target[k] = v;
|
|
95
|
+
applied[k] = { from: undefined, to: v };
|
|
96
|
+
}
|
|
97
|
+
// 已存在的值保留不动(包括 false / 0 / '')
|
|
98
|
+
}
|
|
99
|
+
return applied;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function applyChanges(config) {
|
|
103
|
+
config.plugins ??= {};
|
|
104
|
+
config.plugins.entries ??= {};
|
|
105
|
+
config.plugins.slots ??= {};
|
|
106
|
+
|
|
107
|
+
const entry = (config.plugins.entries[PLUGIN_ID] ??= { enabled: true });
|
|
108
|
+
entry.enabled = entry.enabled !== false; // 默认 true,但保留显式 false
|
|
109
|
+
const userConfig = (entry.config ??= {});
|
|
110
|
+
|
|
111
|
+
const applied = deepMergeMissing(userConfig, RECOMMENDED_CONFIG);
|
|
112
|
+
|
|
113
|
+
// memory exclusive slot → co-engram(让 memory-core 自动 disable)
|
|
114
|
+
let slotChanged = false;
|
|
115
|
+
if (config.plugins.slots.memory !== PLUGIN_ID) {
|
|
116
|
+
slotChanged = {
|
|
117
|
+
from: config.plugins.slots.memory,
|
|
118
|
+
to: PLUGIN_ID,
|
|
119
|
+
};
|
|
120
|
+
config.plugins.slots.memory = PLUGIN_ID;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { applied, slotChanged };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function findOpenclawCli() {
|
|
127
|
+
// 优先找 co-claw bundled CLI
|
|
128
|
+
const candidates = [
|
|
129
|
+
"/opt/Co-Claw/resources/app-data/coclaw/openclaw.mjs",
|
|
130
|
+
process.env.HOME + "/.coclaw/nodejs/bin/node", // 备用 node
|
|
131
|
+
];
|
|
132
|
+
for (const c of candidates) {
|
|
133
|
+
if (existsSync(c)) return c;
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function restartGateway() {
|
|
139
|
+
const cli = findOpenclawCli();
|
|
140
|
+
if (!cli) {
|
|
141
|
+
process.stderr.write(`[setup] openclaw CLI not found; skip restart\n`);
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
process.stdout.write(`[setup] Restarting gateway via ${cli}\n`);
|
|
145
|
+
const result = spawnSync("node", [cli, "gateway", "restart"], {
|
|
146
|
+
stdio: "inherit",
|
|
147
|
+
timeout: 30000,
|
|
148
|
+
});
|
|
149
|
+
return result.status === 0;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function main() {
|
|
153
|
+
const flags = parseArgs(process.argv);
|
|
154
|
+
const config = readConfig(flags.configPath);
|
|
155
|
+
const before = JSON.stringify(config, null, 2);
|
|
156
|
+
const { applied, slotChanged } = applyChanges(config);
|
|
157
|
+
const after = JSON.stringify(config, null, 2);
|
|
158
|
+
const changed = Object.keys(applied).length > 0 || slotChanged;
|
|
159
|
+
|
|
160
|
+
if (!changed) {
|
|
161
|
+
process.stdout.write(`[setup] Already up to date: ${flags.configPath}\n`);
|
|
162
|
+
if (flags.restart) restartGateway();
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
process.stdout.write(`[setup] Planned changes to ${flags.configPath}:\n`);
|
|
167
|
+
for (const [k, v] of Object.entries(applied)) {
|
|
168
|
+
process.stdout.write(
|
|
169
|
+
` plugins.entries.${PLUGIN_ID}.config.${k}: <missing> → ${JSON.stringify(v.to)}\n`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
if (slotChanged) {
|
|
173
|
+
process.stdout.write(
|
|
174
|
+
` plugins.slots.memory: ${JSON.stringify(slotChanged.from)} → ${JSON.stringify(slotChanged.to)}\n`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (!flags.yes) {
|
|
179
|
+
process.stdout.write(
|
|
180
|
+
`\n[setup] Dry-run only. Re-run with --yes to apply.\n`,
|
|
181
|
+
);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
186
|
+
const backup = `${flags.configPath}.bak.${ts}`;
|
|
187
|
+
copyFileSync(flags.configPath, backup);
|
|
188
|
+
process.stdout.write(`[setup] Backup: ${backup}\n`);
|
|
189
|
+
|
|
190
|
+
writeFileSync(flags.configPath, after + "\n", "utf8");
|
|
191
|
+
process.stdout.write(`[setup] Applied: ${flags.configPath}\n`);
|
|
192
|
+
|
|
193
|
+
syncWorkspaceDeps();
|
|
194
|
+
|
|
195
|
+
if (flags.restart) {
|
|
196
|
+
restartGateway();
|
|
197
|
+
} else {
|
|
198
|
+
process.stdout.write(
|
|
199
|
+
`\n[setup] Run \`openclaw gateway restart\` to load the new config.\n`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* 调用同目录的 sync-deps.mjs,把 workspace 依赖从 pnpm symlink 替换为实拷贝
|
|
206
|
+
*
|
|
207
|
+
* 失败不抛错,只 stderr 提示——setup 仍算成功(用户可能从已发布 npm tarball 安装,
|
|
208
|
+
* 此时无 symlink 需要处理)。
|
|
209
|
+
*/
|
|
210
|
+
function syncWorkspaceDeps() {
|
|
211
|
+
const scriptPath = join(
|
|
212
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
213
|
+
"sync-deps.mjs",
|
|
214
|
+
);
|
|
215
|
+
if (!existsSync(scriptPath)) {
|
|
216
|
+
process.stderr.write(
|
|
217
|
+
`[setup] sync-deps.mjs not found at ${scriptPath}; skipping workspace dep sync\n`,
|
|
218
|
+
);
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
process.stdout.write(
|
|
222
|
+
`[setup] Syncing workspace deps (pnpm symlink → real copy)\n`,
|
|
223
|
+
);
|
|
224
|
+
const result = spawnSync("node", [scriptPath], {
|
|
225
|
+
stdio: "inherit",
|
|
226
|
+
timeout: 60000,
|
|
227
|
+
});
|
|
228
|
+
return result.status === 0;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
main();
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 把 openclaw-plugin 的 workspace 依赖从 pnpm symlink 替换为实际拷贝。
|
|
4
|
+
*
|
|
5
|
+
* 背景:openclaw 安装插件时会扫描 node_modules,拒绝包含指向 install root 外的
|
|
6
|
+
* symlink(`manifest dependency scan found node_modules symlink target outside
|
|
7
|
+
* install root`)。pnpm workspace 默认用 symlink,所以需要这一步把
|
|
8
|
+
* @co-engram/core / @co-engram/viewer / yaml / zod / ulid 等运行时依赖"实拷贝"。
|
|
9
|
+
*
|
|
10
|
+
* 行为(idempotent,可重复跑):
|
|
11
|
+
* 1. 对每个目标包:resolve 真实位置(createRequire + require.resolve)
|
|
12
|
+
* 2. 删除 openclaw-plugin/node_modules/<pkg> 当前内容(可能是 symlink)
|
|
13
|
+
* 3. fs.cpSync 真实位置 → node_modules/<pkg>(排除嵌套 node_modules / .git / 测试文件)
|
|
14
|
+
* 4. 删除拷贝结果中的嵌套 node_modules(避免 openclaw 扫描器递归解析)
|
|
15
|
+
*
|
|
16
|
+
* 用法:
|
|
17
|
+
* pnpm --filter @co-engram/openclaw sync-deps # 标准用法
|
|
18
|
+
* node ./scripts/sync-deps.mjs --check # 只检查,不修改
|
|
19
|
+
* node ./scripts/sync-deps.mjs --force # 强制重做(即使已是实拷贝)
|
|
20
|
+
*
|
|
21
|
+
* @module @co-engram/openclaw/scripts/sync-deps
|
|
22
|
+
*/
|
|
23
|
+
import { createRequire } from "node:module";
|
|
24
|
+
import {
|
|
25
|
+
cpSync,
|
|
26
|
+
existsSync,
|
|
27
|
+
lstatSync,
|
|
28
|
+
readdirSync,
|
|
29
|
+
readFileSync,
|
|
30
|
+
readlinkSync,
|
|
31
|
+
rmSync,
|
|
32
|
+
statSync,
|
|
33
|
+
} from "node:fs";
|
|
34
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
35
|
+
import { fileURLToPath } from "node:url";
|
|
36
|
+
|
|
37
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
38
|
+
const __dirname = dirname(__filename);
|
|
39
|
+
const PKG_ROOT = resolve(__dirname, "..");
|
|
40
|
+
const NM = resolve(PKG_ROOT, "node_modules");
|
|
41
|
+
|
|
42
|
+
/** 需要实拷贝的运行时依赖(直接 + 传递) */
|
|
43
|
+
const TARGETS = ["@co-engram/core", "@co-engram/viewer", "yaml", "zod", "ulid"];
|
|
44
|
+
|
|
45
|
+
/** 拷贝时跳过的目录名 */
|
|
46
|
+
const SKIP_DIRS = new Set([
|
|
47
|
+
"node_modules",
|
|
48
|
+
".git",
|
|
49
|
+
".cache",
|
|
50
|
+
"test",
|
|
51
|
+
"tests",
|
|
52
|
+
"__tests__",
|
|
53
|
+
".tshy",
|
|
54
|
+
".tshy-build",
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
/** 拷贝时跳过的文件后缀 */
|
|
58
|
+
const SKIP_SUFFIXES = [
|
|
59
|
+
".test.ts",
|
|
60
|
+
".test.js",
|
|
61
|
+
".test.d.ts",
|
|
62
|
+
".spec.ts",
|
|
63
|
+
".spec.js",
|
|
64
|
+
".tsbuildinfo",
|
|
65
|
+
".map",
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
function parseArgs(argv) {
|
|
69
|
+
const flags = { check: false, force: false };
|
|
70
|
+
for (const arg of argv.slice(2)) {
|
|
71
|
+
if (arg === "--check") flags.check = true;
|
|
72
|
+
else if (arg === "--force") flags.force = true;
|
|
73
|
+
else if (arg === "--help" || arg === "-h") {
|
|
74
|
+
process.stdout.write(`co-engram openclaw sync-deps
|
|
75
|
+
|
|
76
|
+
Usage:
|
|
77
|
+
sync-deps [options]
|
|
78
|
+
|
|
79
|
+
Options:
|
|
80
|
+
--check Only print current state, do not modify anything
|
|
81
|
+
--force Re-copy even if target is already a real directory
|
|
82
|
+
--help Show this help
|
|
83
|
+
|
|
84
|
+
Targets (copied from pnpm symlink → real files):
|
|
85
|
+
${TARGETS.map((t) => ` - ${t}`).join("\n")}
|
|
86
|
+
`);
|
|
87
|
+
process.exit(0);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return flags;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** 通过读 node_modules/<pkg>/package.json 找到包根目录(穿透 pnpm symlink) */
|
|
94
|
+
function resolvePackageRoot(pkgName) {
|
|
95
|
+
const directPath = join(NM, pkgName);
|
|
96
|
+
if (!existsSync(directPath)) return null;
|
|
97
|
+
|
|
98
|
+
// 处理 symlink 情况:readlinksync 取 target
|
|
99
|
+
const stat = lstatSync(directPath);
|
|
100
|
+
let realPath;
|
|
101
|
+
if (stat.isSymbolicLink()) {
|
|
102
|
+
realPath = readlinkSync(directPath);
|
|
103
|
+
// 相对 symlink:相对于 symlink 父目录解析
|
|
104
|
+
if (!realPath.startsWith("/")) {
|
|
105
|
+
realPath = resolve(dirname(directPath), realPath);
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
// 已经是实拷贝,直接用
|
|
109
|
+
realPath = directPath;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 验证 realPath 下有 package.json 且 name 匹配
|
|
113
|
+
const pkgJsonPath = join(realPath, "package.json");
|
|
114
|
+
if (!existsSync(pkgJsonPath)) return null;
|
|
115
|
+
try {
|
|
116
|
+
const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
|
|
117
|
+
if (pkg.name === pkgName) return realPath;
|
|
118
|
+
} catch {}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** 判断 node_modules/<pkg> 当前是否已经是实拷贝(非 symlink 且非空) */
|
|
123
|
+
function isRealCopy(pkgName) {
|
|
124
|
+
const target = join(NM, pkgName);
|
|
125
|
+
if (!existsSync(target)) return false;
|
|
126
|
+
const stat = lstatSync(target);
|
|
127
|
+
return stat.isDirectory() && !stat.isSymbolicLink();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** 过滤掉测试文件和构建缓存 */
|
|
131
|
+
function filterCopy(src, dest) {
|
|
132
|
+
const base = src.split("/").pop();
|
|
133
|
+
if (SKIP_DIRS.has(base)) return false;
|
|
134
|
+
for (const suf of SKIP_SUFFIXES) {
|
|
135
|
+
if (src.endsWith(suf)) return false;
|
|
136
|
+
}
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** 拷贝后清理嵌套 node_modules(.pnpm 有时通过 dependencies 嵌入) */
|
|
141
|
+
function cleanNestedNodeModules(rootPath) {
|
|
142
|
+
if (!existsSync(rootPath)) return;
|
|
143
|
+
const stack = [rootPath];
|
|
144
|
+
while (stack.length > 0) {
|
|
145
|
+
const cur = stack.pop();
|
|
146
|
+
let entries;
|
|
147
|
+
try {
|
|
148
|
+
entries = readdirSync(cur, { withFileTypes: true });
|
|
149
|
+
} catch {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
for (const ent of entries) {
|
|
153
|
+
if (ent.name === "node_modules" && ent.isDirectory()) {
|
|
154
|
+
const nested = join(cur, ent.name);
|
|
155
|
+
rmSync(nested, { recursive: true, force: true });
|
|
156
|
+
process.stdout.write(
|
|
157
|
+
` cleaned nested: ${relative(PKG_ROOT, nested)}\n`,
|
|
158
|
+
);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (ent.isDirectory()) {
|
|
162
|
+
stack.push(join(cur, ent.name));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function syncOne(pkgName, { check, force }) {
|
|
169
|
+
const target = join(NM, pkgName);
|
|
170
|
+
const status = { pkg: pkgName, action: "noop", reason: "" };
|
|
171
|
+
|
|
172
|
+
const realPath = resolvePackageRoot(pkgName);
|
|
173
|
+
if (!realPath) {
|
|
174
|
+
status.action = "skip";
|
|
175
|
+
status.reason = "cannot resolve (not installed?)";
|
|
176
|
+
return status;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (check) {
|
|
180
|
+
status.action = isRealCopy(pkgName) ? "real-copy" : "symlink-or-missing";
|
|
181
|
+
status.reason = `real=${realPath}`;
|
|
182
|
+
return status;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (isRealCopy(pkgName)) {
|
|
186
|
+
// 已经是实拷贝:无需重做。
|
|
187
|
+
// --force 对实拷贝无效,因为 resolvePackageRoot 返回的 realPath 就是 target 自身,
|
|
188
|
+
// rm 后 cp 会 ENOENT。若需重做,先 `pnpm install` 恢复 symlink 再 sync。
|
|
189
|
+
status.action = "noop";
|
|
190
|
+
status.reason = force
|
|
191
|
+
? "already real-copy (force-noop; run `pnpm install` first to redo)"
|
|
192
|
+
: "already real-copy";
|
|
193
|
+
return status;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// 删除现有(symlink 或目录)
|
|
197
|
+
if (existsSync(target)) {
|
|
198
|
+
rmSync(target, { recursive: true, force: true });
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// 实拷贝
|
|
202
|
+
cpSync(realPath, target, {
|
|
203
|
+
recursive: true,
|
|
204
|
+
filter: filterCopy,
|
|
205
|
+
dereference: true,
|
|
206
|
+
});
|
|
207
|
+
// 清理嵌套 node_modules
|
|
208
|
+
cleanNestedNodeModules(target);
|
|
209
|
+
|
|
210
|
+
status.action = "copied";
|
|
211
|
+
status.reason = `from ${relative(PKG_ROOT, realPath)}`;
|
|
212
|
+
return status;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function main() {
|
|
216
|
+
const flags = parseArgs(process.argv);
|
|
217
|
+
process.stdout.write(
|
|
218
|
+
`[sync-deps] mode=${flags.check ? "check" : flags.force ? "force" : "sync"} pkg=${PKG_ROOT}\n`,
|
|
219
|
+
);
|
|
220
|
+
let copied = 0;
|
|
221
|
+
let skipped = 0;
|
|
222
|
+
let failed = 0;
|
|
223
|
+
for (const pkg of TARGETS) {
|
|
224
|
+
const r = syncOne(pkg, flags);
|
|
225
|
+
const tag =
|
|
226
|
+
r.action === "copied"
|
|
227
|
+
? "✓"
|
|
228
|
+
: r.action === "real-copy"
|
|
229
|
+
? "="
|
|
230
|
+
: r.action === "symlink-or-missing"
|
|
231
|
+
? "!"
|
|
232
|
+
: r.action === "skip"
|
|
233
|
+
? "x"
|
|
234
|
+
: "-";
|
|
235
|
+
process.stdout.write(
|
|
236
|
+
` ${tag} ${pkg.padEnd(20)} ${r.action} ${r.reason}\n`,
|
|
237
|
+
);
|
|
238
|
+
if (r.action === "copied") copied++;
|
|
239
|
+
else if (r.action === "skip") failed++;
|
|
240
|
+
else if (r.action === "symlink-or-missing") skipped++;
|
|
241
|
+
}
|
|
242
|
+
process.stdout.write(
|
|
243
|
+
`[sync-deps] done: ${copied} copied, ${skipped} symlink-or-missing, ${failed} skip\n`,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
main();
|