openxiangda 1.0.159 → 1.0.161
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 +39 -13
- package/lib/change-baseline.js +1077 -0
- package/lib/cli.js +12002 -6723
- package/lib/config.js +272 -7
- package/lib/http.js +2 -0
- package/lib/js-code-build.js +61 -0
- package/lib/publish-context.js +98 -0
- package/lib/publish-lease-heartbeat.js +182 -0
- package/lib/publish-lease.js +190 -0
- package/lib/release-telemetry.js +182 -0
- package/lib/runtime-lineage.js +297 -0
- package/lib/sdd.js +567 -35
- package/lib/source-dependencies.js +742 -0
- package/lib/workspace-init.js +6 -1
- package/lib/worktree-owner.js +219 -0
- package/openxiangda-skills/SKILL.md +103 -228
- package/openxiangda-skills/references/architecture-design.md +2 -1
- package/openxiangda-skills/references/openxiangda-api.md +27 -0
- package/openxiangda-skills/references/pages/page-sdk.md +11 -1
- package/openxiangda-skills/references/resource-manifest-cheatsheet.md +12 -2
- package/openxiangda-skills/references/workspace-state.md +2 -0
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +77 -243
- package/openxiangda-skills/skills/openxiangda-form/SKILL.md +8 -0
- package/openxiangda-skills/skills/openxiangda-page/SKILL.md +18 -0
- package/package.json +20 -1
- package/packages/sdk/dist/{ProcessPreview-DMkzccq4.d.mts → ProcessPreview-Dux73lY_.d.mts} +5 -0
- package/packages/sdk/dist/{ProcessPreview-DMkzccq4.d.ts → ProcessPreview-Dux73lY_.d.ts} +5 -0
- package/packages/sdk/dist/components/index.cjs +29 -1
- package/packages/sdk/dist/components/index.cjs.map +1 -1
- package/packages/sdk/dist/components/index.d.mts +4 -4
- package/packages/sdk/dist/components/index.d.ts +4 -4
- package/packages/sdk/dist/components/index.mjs +29 -1
- package/packages/sdk/dist/components/index.mjs.map +1 -1
- package/packages/sdk/dist/{dataManagementApi-C9O-Bb0j.d.ts → dataManagementApi-BNLH2o7f.d.ts} +2 -1
- package/packages/sdk/dist/{dataManagementApi-gpZkgRDM.d.mts → dataManagementApi-BOcIx4U-.d.mts} +2 -1
- package/packages/sdk/dist/runtime/index.cjs +150 -22
- package/packages/sdk/dist/runtime/index.cjs.map +1 -1
- package/packages/sdk/dist/runtime/index.d.mts +3 -3
- package/packages/sdk/dist/runtime/index.d.ts +3 -3
- package/packages/sdk/dist/runtime/index.mjs +150 -22
- package/packages/sdk/dist/runtime/index.mjs.map +1 -1
- package/packages/sdk/dist/runtime/react.cjs +121 -21
- package/packages/sdk/dist/runtime/react.cjs.map +1 -1
- package/packages/sdk/dist/runtime/react.d.mts +15 -2
- package/packages/sdk/dist/runtime/react.d.ts +15 -2
- package/packages/sdk/dist/runtime/react.mjs +121 -21
- package/packages/sdk/dist/runtime/react.mjs.map +1 -1
- package/packages/sdk/src/build-source/scripts/register.mjs +54 -23
- package/packages/sdk/src/build-source/scripts/sync-schema.mjs +59 -19
- package/packages/sdk/src/build-source/scripts/utils/form-api.mjs +242 -2
- package/templates/openxiangda-react-spa/.cursor/rules/openxiangda-resources.mdc +7 -3
- package/templates/openxiangda-react-spa/.cursor/rules/openxiangda.mdc +11 -4
- package/templates/openxiangda-react-spa/.qoder/rules/openxiangda-resources.md +7 -3
- package/templates/openxiangda-react-spa/.qoder/rules/openxiangda.md +10 -4
- package/templates/openxiangda-react-spa/AGENTS.md +25 -12
- package/templates/openxiangda-react-spa/scripts/build-js-code.mjs +344 -60
- package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda-resources.mdc +5 -1
- package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda.mdc +8 -2
- package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda-resources.md +8 -4
- package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda.md +8 -2
- package/templates/sy-lowcode-app-workspace/AGENTS.md +14 -4
- package/templates/sy-lowcode-app-workspace/scripts/build-js-code.mjs +344 -60
|
@@ -0,0 +1,1077 @@
|
|
|
1
|
+
const { spawnSync } = require('child_process');
|
|
2
|
+
const crypto = require('crypto');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const { saveProjectState } = require('./config');
|
|
8
|
+
|
|
9
|
+
const SOURCE_KIND_BY_RESOURCE_KIND = Object.freeze({
|
|
10
|
+
Function: 'functions',
|
|
11
|
+
Automation: 'automations',
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
function getStoredChangeBaseline(target, options = {}) {
|
|
15
|
+
const baseline = target?.bound?.promotion?.changeBaseline;
|
|
16
|
+
if (!baseline || typeof baseline !== 'object' || Array.isArray(baseline)) return null;
|
|
17
|
+
assertStoredChangeBaseline(target, baseline, options);
|
|
18
|
+
return baseline;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function saveChangeBaseline(target, input, options = {}) {
|
|
22
|
+
const identity = assertTargetIdentity(target);
|
|
23
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
24
|
+
throw changeBaselineError(
|
|
25
|
+
'CHANGE_BASELINE_INVALID',
|
|
26
|
+
'change baseline 必须是对象'
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const existing = target.bound?.promotion?.changeBaseline;
|
|
31
|
+
if (existing && typeof existing === 'object' && !Array.isArray(existing)) {
|
|
32
|
+
assertStoredChangeBaseline(target, existing, options);
|
|
33
|
+
}
|
|
34
|
+
assertInputIdentity(input, identity);
|
|
35
|
+
|
|
36
|
+
const env = options.env || process.env;
|
|
37
|
+
const currentThreadId = currentCodexThreadId(env);
|
|
38
|
+
const inputOwnerThreadId = getChangeBaselineOwnerThreadId(input);
|
|
39
|
+
if (inputOwnerThreadId && inputOwnerThreadId !== currentThreadId) {
|
|
40
|
+
throw ownershipError(inputOwnerThreadId, currentThreadId);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const merged = {
|
|
44
|
+
...(existing ? cloneJson(existing) : {}),
|
|
45
|
+
...cloneJson(input),
|
|
46
|
+
};
|
|
47
|
+
const workspace = {
|
|
48
|
+
...(existing?.workspace && typeof existing.workspace === 'object'
|
|
49
|
+
? cloneJson(existing.workspace)
|
|
50
|
+
: {}),
|
|
51
|
+
...(input.workspace && typeof input.workspace === 'object'
|
|
52
|
+
? cloneJson(input.workspace)
|
|
53
|
+
: {}),
|
|
54
|
+
};
|
|
55
|
+
const ownerThreadId = currentThreadId || inputOwnerThreadId;
|
|
56
|
+
if (ownerThreadId) workspace.threadId = ownerThreadId;
|
|
57
|
+
else delete workspace.threadId;
|
|
58
|
+
|
|
59
|
+
const baseline = {
|
|
60
|
+
...merged,
|
|
61
|
+
appType: identity.appType,
|
|
62
|
+
profile: identity.profile,
|
|
63
|
+
resourceHeads: clonePreservedField(input, existing, 'resourceHeads', {}),
|
|
64
|
+
sourceBase: clonePreservedField(input, existing, 'sourceBase', null),
|
|
65
|
+
clientSessionId: normalizeOptionalString(
|
|
66
|
+
preservedField(input, existing, 'clientSessionId', null)
|
|
67
|
+
),
|
|
68
|
+
localBaseArtifactHashes: clonePreservedField(
|
|
69
|
+
input,
|
|
70
|
+
existing,
|
|
71
|
+
'localBaseArtifactHashes',
|
|
72
|
+
{}
|
|
73
|
+
),
|
|
74
|
+
workspace,
|
|
75
|
+
updatedAt: normalizeNow(options.now),
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
target.bound.promotion = {
|
|
79
|
+
...(target.bound.promotion || {}),
|
|
80
|
+
changeBaseline: baseline,
|
|
81
|
+
};
|
|
82
|
+
persistTargetState(target, options);
|
|
83
|
+
return baseline;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function clearChangeBaseline(target, baselineIdOrOptions, maybeOptions = {}) {
|
|
87
|
+
const hasOptionsAsSecondArgument =
|
|
88
|
+
baselineIdOrOptions &&
|
|
89
|
+
typeof baselineIdOrOptions === 'object' &&
|
|
90
|
+
!Array.isArray(baselineIdOrOptions);
|
|
91
|
+
const options = hasOptionsAsSecondArgument
|
|
92
|
+
? baselineIdOrOptions
|
|
93
|
+
: maybeOptions;
|
|
94
|
+
const baselineId = hasOptionsAsSecondArgument
|
|
95
|
+
? null
|
|
96
|
+
: normalizeOptionalString(baselineIdOrOptions);
|
|
97
|
+
|
|
98
|
+
const baseline = target?.bound?.promotion?.changeBaseline;
|
|
99
|
+
if (!baseline || typeof baseline !== 'object' || Array.isArray(baseline)) return false;
|
|
100
|
+
assertStoredChangeBaseline(target, baseline, options);
|
|
101
|
+
|
|
102
|
+
const storedId = normalizeOptionalString(baseline.baselineId || baseline.id);
|
|
103
|
+
if (baselineId && storedId !== baselineId) return false;
|
|
104
|
+
|
|
105
|
+
target.bound.promotion = { ...(target.bound.promotion || {}) };
|
|
106
|
+
delete target.bound.promotion.changeBaseline;
|
|
107
|
+
if (Object.keys(target.bound.promotion).length === 0) {
|
|
108
|
+
delete target.bound.promotion;
|
|
109
|
+
}
|
|
110
|
+
persistTargetState(target, options);
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function isChangeBaselineOwnedByCurrentSession(baseline, env = process.env) {
|
|
115
|
+
const ownerThreadId = getChangeBaselineOwnerThreadId(baseline);
|
|
116
|
+
if (!ownerThreadId) return true;
|
|
117
|
+
const threadId = currentCodexThreadId(env);
|
|
118
|
+
return Boolean(threadId) && ownerThreadId === threadId;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function readGitSourceBase(cwd = process.cwd(), baseRef = 'HEAD') {
|
|
122
|
+
const repository = resolveGitRepository(cwd);
|
|
123
|
+
const normalizedBaseRef = normalizeBaseRef(baseRef);
|
|
124
|
+
const baseCommit = resolveExactCommit(repository.root, normalizedBaseRef);
|
|
125
|
+
const treeHash = runGit(repository.root, [
|
|
126
|
+
'rev-parse',
|
|
127
|
+
'--verify',
|
|
128
|
+
`${baseCommit}^{tree}`,
|
|
129
|
+
]).trim();
|
|
130
|
+
assertGitObjectId(treeHash, 'treeHash');
|
|
131
|
+
|
|
132
|
+
const remote = tryRunGit(repository.root, [
|
|
133
|
+
'config',
|
|
134
|
+
'--get',
|
|
135
|
+
'remote.origin.url',
|
|
136
|
+
]).trim();
|
|
137
|
+
const commonGitDirValue = tryRunGit(repository.root, [
|
|
138
|
+
'rev-parse',
|
|
139
|
+
'--path-format=absolute',
|
|
140
|
+
'--git-common-dir',
|
|
141
|
+
]).trim() || tryRunGit(repository.root, [
|
|
142
|
+
'rev-parse',
|
|
143
|
+
'--git-common-dir',
|
|
144
|
+
]).trim();
|
|
145
|
+
const commonGitDir = commonGitDirValue
|
|
146
|
+
? fs.realpathSync(
|
|
147
|
+
path.isAbsolute(commonGitDirValue)
|
|
148
|
+
? commonGitDirValue
|
|
149
|
+
: path.resolve(repository.root, commonGitDirValue)
|
|
150
|
+
)
|
|
151
|
+
: repository.root;
|
|
152
|
+
const identitySeed = remote
|
|
153
|
+
? `remote:${normalizeRemoteIdentity(remote)}`
|
|
154
|
+
: `local:${commonGitDir}`;
|
|
155
|
+
const repositoryId = `sha256:${sha256Buffer(Buffer.from(identitySeed, 'utf8'))}`;
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
repo: repositoryId,
|
|
159
|
+
repositoryId,
|
|
160
|
+
baseCommit,
|
|
161
|
+
treeHash,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function buildGitBaseSourceArtifacts({
|
|
166
|
+
cwd = process.cwd(),
|
|
167
|
+
baseCommit,
|
|
168
|
+
targets,
|
|
169
|
+
env = process.env,
|
|
170
|
+
packageManager = 'pnpm',
|
|
171
|
+
} = {}) {
|
|
172
|
+
const normalizedTargets = normalizeArtifactTargets(targets);
|
|
173
|
+
return withGitBaseWorkspace({ cwd, baseCommit, env }, context => {
|
|
174
|
+
const {
|
|
175
|
+
buildRoot,
|
|
176
|
+
buildEnv,
|
|
177
|
+
resolvedCommit,
|
|
178
|
+
} = context;
|
|
179
|
+
prepareExactArtifactBuild(buildRoot, normalizedTargets);
|
|
180
|
+
const specs = normalizedTargets
|
|
181
|
+
.map(target => `${target.sourceKind}:${target.code}`)
|
|
182
|
+
.join(',');
|
|
183
|
+
const batch = runBuildCommand(
|
|
184
|
+
packageManager,
|
|
185
|
+
['build-js-code', '--scripts', specs],
|
|
186
|
+
buildRoot,
|
|
187
|
+
buildEnv
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
let buildMode = 'batch';
|
|
191
|
+
if (!batch.ok) {
|
|
192
|
+
buildMode = 'legacy-fallback';
|
|
193
|
+
for (const target of normalizedTargets) {
|
|
194
|
+
const legacy = runBuildCommand(
|
|
195
|
+
packageManager,
|
|
196
|
+
[
|
|
197
|
+
'build-js-code',
|
|
198
|
+
'--script',
|
|
199
|
+
target.code,
|
|
200
|
+
'--source',
|
|
201
|
+
target.sourceKind,
|
|
202
|
+
],
|
|
203
|
+
buildRoot,
|
|
204
|
+
buildEnv
|
|
205
|
+
);
|
|
206
|
+
if (!legacy.ok) {
|
|
207
|
+
throw buildFailureError(target, batch, legacy);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const artifacts = {};
|
|
213
|
+
const localBaseArtifactHashes = {};
|
|
214
|
+
for (const target of normalizedTargets) {
|
|
215
|
+
const relativePath = path.posix.join(
|
|
216
|
+
'dist',
|
|
217
|
+
target.sourceKind,
|
|
218
|
+
target.code,
|
|
219
|
+
'index.cjs'
|
|
220
|
+
);
|
|
221
|
+
const artifactPath = path.resolve(buildRoot, ...relativePath.split('/'));
|
|
222
|
+
assertArtifactFileSafe(buildRoot, artifactPath, target);
|
|
223
|
+
const artifactHash = sha256File(artifactPath);
|
|
224
|
+
const key = `${target.kind}:${target.code}`;
|
|
225
|
+
artifacts[key] = {
|
|
226
|
+
kind: target.kind,
|
|
227
|
+
code: target.code,
|
|
228
|
+
sourceKind: target.sourceKind,
|
|
229
|
+
relativePath,
|
|
230
|
+
artifactHash,
|
|
231
|
+
sha256: artifactHash,
|
|
232
|
+
};
|
|
233
|
+
localBaseArtifactHashes[key] = artifactHash;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
baseCommit: resolvedCommit,
|
|
238
|
+
buildMode,
|
|
239
|
+
artifacts,
|
|
240
|
+
localBaseArtifactHashes,
|
|
241
|
+
};
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function withGitBaseWorkspace(
|
|
246
|
+
{
|
|
247
|
+
cwd = process.cwd(),
|
|
248
|
+
baseCommit,
|
|
249
|
+
env = process.env,
|
|
250
|
+
copyProjectState = false,
|
|
251
|
+
} = {},
|
|
252
|
+
callback
|
|
253
|
+
) {
|
|
254
|
+
if (typeof callback !== 'function') {
|
|
255
|
+
throw changeBaselineError(
|
|
256
|
+
'CHANGE_BASELINE_CALLBACK_REQUIRED',
|
|
257
|
+
'Git 基线工作区必须提供同步 callback'
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
const workspace = createGitBaseWorkspace({
|
|
261
|
+
cwd,
|
|
262
|
+
baseCommit,
|
|
263
|
+
env,
|
|
264
|
+
copyProjectState,
|
|
265
|
+
});
|
|
266
|
+
let callbackError = null;
|
|
267
|
+
try {
|
|
268
|
+
return callback(workspace);
|
|
269
|
+
} catch (error) {
|
|
270
|
+
callbackError = error;
|
|
271
|
+
throw error;
|
|
272
|
+
} finally {
|
|
273
|
+
try {
|
|
274
|
+
assertCopiedProjectStateUnchanged(workspace);
|
|
275
|
+
} catch (stateError) {
|
|
276
|
+
if (!callbackError) throw stateError;
|
|
277
|
+
} finally {
|
|
278
|
+
fs.rmSync(workspace.tempRoot, { recursive: true, force: true });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function createGitBaseWorkspace({
|
|
284
|
+
cwd,
|
|
285
|
+
baseCommit,
|
|
286
|
+
env,
|
|
287
|
+
copyProjectState,
|
|
288
|
+
}) {
|
|
289
|
+
const repository = resolveGitRepository(cwd);
|
|
290
|
+
const immutableCommit = normalizeImmutableCommit(baseCommit);
|
|
291
|
+
const resolvedCommit = resolveExactCommit(repository.root, immutableCommit);
|
|
292
|
+
if (resolvedCommit.toLowerCase() !== immutableCommit.toLowerCase()) {
|
|
293
|
+
throw changeBaselineError(
|
|
294
|
+
'CHANGE_BASELINE_COMMIT_NOT_EXACT',
|
|
295
|
+
`baseCommit 必须是完整且不可变的 commit id: ${baseCommit}`
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const currentWorkspaceRoot = fs.realpathSync(path.resolve(cwd));
|
|
300
|
+
const workspaceRelative = path.relative(repository.root, currentWorkspaceRoot);
|
|
301
|
+
if (isOutsidePath(workspaceRelative)) {
|
|
302
|
+
throw changeBaselineError(
|
|
303
|
+
'CHANGE_BASELINE_WORKSPACE_OUTSIDE_REPOSITORY',
|
|
304
|
+
'cwd 必须位于 Git 仓库内'
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const currentNodeModules = path.join(currentWorkspaceRoot, 'node_modules');
|
|
309
|
+
let currentNodeModulesRealPath;
|
|
310
|
+
try {
|
|
311
|
+
currentNodeModulesRealPath = fs.realpathSync(currentNodeModules);
|
|
312
|
+
} catch {
|
|
313
|
+
throw changeBaselineError(
|
|
314
|
+
'CHANGE_BASELINE_NODE_MODULES_MISSING',
|
|
315
|
+
'构建 Git 基线前必须先在当前工作区安装 node_modules'
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
if (!fs.statSync(currentNodeModulesRealPath).isDirectory()) {
|
|
319
|
+
throw changeBaselineError(
|
|
320
|
+
'CHANGE_BASELINE_NODE_MODULES_INVALID',
|
|
321
|
+
'当前工作区 node_modules 不是目录'
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const tempRoot = fs.realpathSync(
|
|
326
|
+
fs.mkdtempSync(path.join(os.tmpdir(), 'openxiangda-change-baseline-'))
|
|
327
|
+
);
|
|
328
|
+
const archivePath = path.join(tempRoot, 'source.tar');
|
|
329
|
+
const extractRoot = path.join(tempRoot, 'source');
|
|
330
|
+
|
|
331
|
+
try {
|
|
332
|
+
fs.mkdirSync(extractRoot, { recursive: true });
|
|
333
|
+
validateGitTreeForSafeArchive(repository.root, resolvedCommit);
|
|
334
|
+
runGit(repository.root, [
|
|
335
|
+
'archive',
|
|
336
|
+
'--format=tar',
|
|
337
|
+
`--output=${archivePath}`,
|
|
338
|
+
resolvedCommit,
|
|
339
|
+
]);
|
|
340
|
+
validateTarEntryNames(archivePath);
|
|
341
|
+
runCommand('tar', ['-xf', archivePath, '-C', extractRoot], {
|
|
342
|
+
code: 'CHANGE_BASELINE_ARCHIVE_EXTRACT_FAILED',
|
|
343
|
+
message: '无法安全解包 Git 基线归档',
|
|
344
|
+
});
|
|
345
|
+
assertExtractedTreeSafe(extractRoot);
|
|
346
|
+
|
|
347
|
+
const buildRoot = path.resolve(extractRoot, workspaceRelative || '.');
|
|
348
|
+
if (!isPathInside(extractRoot, buildRoot) || !isDirectory(buildRoot)) {
|
|
349
|
+
throw changeBaselineError(
|
|
350
|
+
'CHANGE_BASELINE_WORKSPACE_NOT_IN_COMMIT',
|
|
351
|
+
'baseCommit 中不存在当前工作区目录'
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
if (!fs.existsSync(path.join(buildRoot, 'package.json'))) {
|
|
355
|
+
throw changeBaselineError(
|
|
356
|
+
'CHANGE_BASELINE_PACKAGE_MISSING',
|
|
357
|
+
'baseCommit 工作区缺少 package.json,不能执行基线校验'
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const archivedNodeModules = path.join(buildRoot, 'node_modules');
|
|
362
|
+
if (fs.existsSync(archivedNodeModules)) {
|
|
363
|
+
throw changeBaselineError(
|
|
364
|
+
'CHANGE_BASELINE_ARCHIVE_NODE_MODULES_CONFLICT',
|
|
365
|
+
'baseCommit 不应包含 node_modules'
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
fs.symlinkSync(
|
|
369
|
+
currentNodeModulesRealPath,
|
|
370
|
+
archivedNodeModules,
|
|
371
|
+
process.platform === 'win32' ? 'junction' : 'dir'
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
const stateSnapshot = copyProjectState
|
|
375
|
+
? copyReadonlyProjectState(currentWorkspaceRoot, buildRoot)
|
|
376
|
+
: null;
|
|
377
|
+
return {
|
|
378
|
+
tempRoot,
|
|
379
|
+
extractRoot,
|
|
380
|
+
buildRoot,
|
|
381
|
+
currentWorkspaceRoot,
|
|
382
|
+
repositoryRoot: repository.root,
|
|
383
|
+
resolvedCommit,
|
|
384
|
+
stateSnapshot,
|
|
385
|
+
buildEnv: buildIsolatedEnvironment(env, {
|
|
386
|
+
currentWorkspaceRoot,
|
|
387
|
+
buildRoot,
|
|
388
|
+
repositoryRoot: repository.root,
|
|
389
|
+
extractRoot,
|
|
390
|
+
}),
|
|
391
|
+
};
|
|
392
|
+
} catch (error) {
|
|
393
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
394
|
+
throw error;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function copyReadonlyProjectState(currentWorkspaceRoot, buildRoot) {
|
|
399
|
+
const sourcePath = path.join(
|
|
400
|
+
currentWorkspaceRoot,
|
|
401
|
+
'.openxiangda',
|
|
402
|
+
'state.json'
|
|
403
|
+
);
|
|
404
|
+
if (!fs.existsSync(sourcePath)) {
|
|
405
|
+
throw changeBaselineError(
|
|
406
|
+
'CHANGE_BASELINE_PROJECT_STATE_MISSING',
|
|
407
|
+
'当前工作区缺少 .openxiangda/state.json,不能在 Git 基线中解析应用绑定'
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
const destinationDir = path.join(buildRoot, '.openxiangda');
|
|
411
|
+
const destinationPath = path.join(destinationDir, 'state.json');
|
|
412
|
+
fs.mkdirSync(destinationDir, { recursive: true });
|
|
413
|
+
const content = fs.readFileSync(sourcePath);
|
|
414
|
+
fs.writeFileSync(destinationPath, content, { mode: 0o444 });
|
|
415
|
+
try {
|
|
416
|
+
fs.chmodSync(destinationPath, 0o444);
|
|
417
|
+
} catch {
|
|
418
|
+
// chmod is best-effort on non-POSIX filesystems; the before/after hash below
|
|
419
|
+
// still proves that the read-only child plan did not mutate project state.
|
|
420
|
+
}
|
|
421
|
+
return {
|
|
422
|
+
path: destinationPath,
|
|
423
|
+
sha256: sha256Buffer(content),
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function assertCopiedProjectStateUnchanged(workspace) {
|
|
428
|
+
const snapshot = workspace?.stateSnapshot;
|
|
429
|
+
if (!snapshot) return;
|
|
430
|
+
let currentHash = null;
|
|
431
|
+
try {
|
|
432
|
+
currentHash = sha256File(snapshot.path);
|
|
433
|
+
} catch {
|
|
434
|
+
// Missing/renamed state is also a mutation.
|
|
435
|
+
}
|
|
436
|
+
if (currentHash !== snapshot.sha256) {
|
|
437
|
+
throw changeBaselineError(
|
|
438
|
+
'CHANGE_BASELINE_PROJECT_STATE_MUTATED',
|
|
439
|
+
'Git 基线 resource plan 修改了只读 .openxiangda/state.json,已中止发布'
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function canonicalJsonSha256(value) {
|
|
445
|
+
const canonical = canonicalizeJson(value, new Set(), '$');
|
|
446
|
+
return sha256Buffer(Buffer.from(JSON.stringify(canonical), 'utf8'));
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function sha256File(filePath) {
|
|
450
|
+
return sha256Buffer(fs.readFileSync(filePath));
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function canonicalizeJson(value, ancestors, pathLabel) {
|
|
454
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean') {
|
|
455
|
+
return value;
|
|
456
|
+
}
|
|
457
|
+
if (typeof value === 'number') {
|
|
458
|
+
if (!Number.isFinite(value)) {
|
|
459
|
+
throw changeBaselineError(
|
|
460
|
+
'CHANGE_BASELINE_CANONICAL_JSON_INVALID',
|
|
461
|
+
`${pathLabel} 包含非有限数字`
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
return Object.is(value, -0) ? 0 : value;
|
|
465
|
+
}
|
|
466
|
+
if (Array.isArray(value)) {
|
|
467
|
+
if (ancestors.has(value)) return throwCircularJson(pathLabel);
|
|
468
|
+
ancestors.add(value);
|
|
469
|
+
const result = value.map((item, index) =>
|
|
470
|
+
canonicalizeJson(item, ancestors, `${pathLabel}[${index}]`)
|
|
471
|
+
);
|
|
472
|
+
ancestors.delete(value);
|
|
473
|
+
return result;
|
|
474
|
+
}
|
|
475
|
+
if (value && typeof value === 'object') {
|
|
476
|
+
const prototype = Object.getPrototypeOf(value);
|
|
477
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
478
|
+
throw changeBaselineError(
|
|
479
|
+
'CHANGE_BASELINE_CANONICAL_JSON_INVALID',
|
|
480
|
+
`${pathLabel} 不是普通 JSON 对象`
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
if (ancestors.has(value)) return throwCircularJson(pathLabel);
|
|
484
|
+
ancestors.add(value);
|
|
485
|
+
const result = {};
|
|
486
|
+
for (const key of Object.keys(value).sort()) {
|
|
487
|
+
const item = value[key];
|
|
488
|
+
if (
|
|
489
|
+
item === undefined ||
|
|
490
|
+
typeof item === 'function' ||
|
|
491
|
+
typeof item === 'symbol' ||
|
|
492
|
+
typeof item === 'bigint'
|
|
493
|
+
) {
|
|
494
|
+
throw changeBaselineError(
|
|
495
|
+
'CHANGE_BASELINE_CANONICAL_JSON_INVALID',
|
|
496
|
+
`${pathLabel}.${key} 不是有效 JSON 值`
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
result[key] = canonicalizeJson(item, ancestors, `${pathLabel}.${key}`);
|
|
500
|
+
}
|
|
501
|
+
ancestors.delete(value);
|
|
502
|
+
return result;
|
|
503
|
+
}
|
|
504
|
+
throw changeBaselineError(
|
|
505
|
+
'CHANGE_BASELINE_CANONICAL_JSON_INVALID',
|
|
506
|
+
`${pathLabel} 不是有效 JSON 值`
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function throwCircularJson(pathLabel) {
|
|
511
|
+
throw changeBaselineError(
|
|
512
|
+
'CHANGE_BASELINE_CANONICAL_JSON_INVALID',
|
|
513
|
+
`${pathLabel} 包含循环引用`
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function assertStoredChangeBaseline(target, baseline, options = {}) {
|
|
518
|
+
const identity = assertTargetIdentity(target);
|
|
519
|
+
const baselineAppType = normalizeOptionalString(baseline.appType);
|
|
520
|
+
const baselineProfile = normalizeOptionalString(baseline.profile);
|
|
521
|
+
if (baselineAppType !== identity.appType) {
|
|
522
|
+
throw changeBaselineError(
|
|
523
|
+
'CHANGE_BASELINE_APP_MISMATCH',
|
|
524
|
+
`change baseline 属于应用 ${baselineAppType || '<missing>'},不能用于 ${identity.appType}`
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
if (baselineProfile !== identity.profile) {
|
|
528
|
+
throw changeBaselineError(
|
|
529
|
+
'CHANGE_BASELINE_PROFILE_MISMATCH',
|
|
530
|
+
`change baseline 属于 profile ${baselineProfile || '<missing>'},不能用于 ${identity.profile}`
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
if (!isChangeBaselineOwnedByCurrentSession(baseline, options.env || process.env)) {
|
|
534
|
+
throw ownershipError(
|
|
535
|
+
getChangeBaselineOwnerThreadId(baseline),
|
|
536
|
+
currentCodexThreadId(options.env || process.env)
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function assertTargetIdentity(target) {
|
|
542
|
+
if (!target?.state || !target?.bound || !target?.profileName) {
|
|
543
|
+
throw changeBaselineError(
|
|
544
|
+
'CHANGE_BASELINE_TARGET_INVALID',
|
|
545
|
+
'change baseline target 缺少 state、bound 或 profileName'
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
const profile = String(target.profileName).trim();
|
|
549
|
+
const appType = String(target.appType || target.bound.appType || '').trim();
|
|
550
|
+
if (!profile || !appType) {
|
|
551
|
+
throw changeBaselineError(
|
|
552
|
+
'CHANGE_BASELINE_TARGET_INVALID',
|
|
553
|
+
'change baseline target 缺少 appType 或 profileName'
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
const boundAppType = normalizeOptionalString(target.bound.appType);
|
|
557
|
+
const stateBound = target.state.profiles?.[profile];
|
|
558
|
+
const stateAppType = normalizeOptionalString(stateBound?.appType);
|
|
559
|
+
if (
|
|
560
|
+
(boundAppType && boundAppType !== appType) ||
|
|
561
|
+
(stateAppType && stateAppType !== appType)
|
|
562
|
+
) {
|
|
563
|
+
throw changeBaselineError(
|
|
564
|
+
'CHANGE_BASELINE_TARGET_INVALID',
|
|
565
|
+
'change baseline target 的 appType 与 profile binding 不一致'
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
return { appType, profile };
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function assertInputIdentity(input, identity) {
|
|
572
|
+
const appType = normalizeOptionalString(input.appType);
|
|
573
|
+
const profile = normalizeOptionalString(input.profile);
|
|
574
|
+
if (appType && appType !== identity.appType) {
|
|
575
|
+
throw changeBaselineError(
|
|
576
|
+
'CHANGE_BASELINE_APP_MISMATCH',
|
|
577
|
+
`不能把应用 ${appType} 的 baseline 保存到 ${identity.appType}`
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
if (profile && profile !== identity.profile) {
|
|
581
|
+
throw changeBaselineError(
|
|
582
|
+
'CHANGE_BASELINE_PROFILE_MISMATCH',
|
|
583
|
+
`不能把 profile ${profile} 的 baseline 保存到 ${identity.profile}`
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function persistTargetState(target, options) {
|
|
589
|
+
target.bound.updatedAt = normalizeNow(options.now);
|
|
590
|
+
if (target.state.profiles?.[target.profileName]) {
|
|
591
|
+
target.state.profiles[target.profileName] = target.bound;
|
|
592
|
+
}
|
|
593
|
+
(options.saveState || saveProjectState)(
|
|
594
|
+
target.state,
|
|
595
|
+
options.cwd || target.cwd || process.cwd()
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function normalizeArtifactTargets(targets) {
|
|
600
|
+
if (!Array.isArray(targets) || targets.length === 0) {
|
|
601
|
+
throw changeBaselineError(
|
|
602
|
+
'CHANGE_BASELINE_TARGETS_REQUIRED',
|
|
603
|
+
'Git 基线构建至少需要一个精确 Function/Automation target'
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
const normalized = [];
|
|
607
|
+
const seen = new Set();
|
|
608
|
+
for (const target of targets) {
|
|
609
|
+
const kind = String(target?.kind || '').trim();
|
|
610
|
+
const code = String(target?.code || '').trim();
|
|
611
|
+
const expectedSourceKind = SOURCE_KIND_BY_RESOURCE_KIND[kind];
|
|
612
|
+
if (!expectedSourceKind) {
|
|
613
|
+
throw changeBaselineError(
|
|
614
|
+
'CHANGE_BASELINE_TARGET_INVALID',
|
|
615
|
+
`不支持的 Git 基线资源类型: ${kind || '<empty>'}`
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
if (!isSafeCode(code)) {
|
|
619
|
+
throw changeBaselineError(
|
|
620
|
+
'CHANGE_BASELINE_TARGET_INVALID',
|
|
621
|
+
`无效的 Git 基线资源 code: ${code || '<empty>'}`
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
const sourceKind = String(target.sourceKind || expectedSourceKind).trim();
|
|
625
|
+
if (sourceKind !== expectedSourceKind) {
|
|
626
|
+
throw changeBaselineError(
|
|
627
|
+
'CHANGE_BASELINE_TARGET_INVALID',
|
|
628
|
+
`${kind}:${code} 的 sourceKind 必须是 ${expectedSourceKind}`
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
const key = `${kind}:${code}`;
|
|
632
|
+
if (seen.has(key)) continue;
|
|
633
|
+
seen.add(key);
|
|
634
|
+
normalized.push({ kind, code, sourceKind });
|
|
635
|
+
}
|
|
636
|
+
return normalized;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function prepareExactArtifactBuild(buildRoot, targets) {
|
|
640
|
+
for (const target of targets) {
|
|
641
|
+
const outputDir = path.join(buildRoot, 'dist', target.sourceKind, target.code);
|
|
642
|
+
fs.rmSync(outputDir, { recursive: true, force: true });
|
|
643
|
+
}
|
|
644
|
+
fs.rmSync(path.join(buildRoot, '.openxiangda', 'build-cache.json'), {
|
|
645
|
+
force: true,
|
|
646
|
+
});
|
|
647
|
+
fs.rmSync(path.join(buildRoot, '.openxiangda', 'build-cache.json.lock'), {
|
|
648
|
+
force: true,
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function runBuildCommand(packageManager, args, cwd, env) {
|
|
653
|
+
const result = spawnSync(packageManager, args, {
|
|
654
|
+
cwd,
|
|
655
|
+
env,
|
|
656
|
+
encoding: 'utf8',
|
|
657
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
658
|
+
});
|
|
659
|
+
return {
|
|
660
|
+
ok: !result.error && result.status === 0,
|
|
661
|
+
status: result.status,
|
|
662
|
+
error: result.error || null,
|
|
663
|
+
stdout: String(result.stdout || ''),
|
|
664
|
+
stderr: String(result.stderr || ''),
|
|
665
|
+
command: [packageManager, ...args],
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function buildIsolatedEnvironment(
|
|
670
|
+
env,
|
|
671
|
+
{ currentWorkspaceRoot, buildRoot, repositoryRoot, extractRoot }
|
|
672
|
+
) {
|
|
673
|
+
const replacements = [
|
|
674
|
+
[currentWorkspaceRoot, buildRoot],
|
|
675
|
+
[repositoryRoot, extractRoot],
|
|
676
|
+
].sort((left, right) => right[0].length - left[0].length);
|
|
677
|
+
const isolated = {};
|
|
678
|
+
for (const [key, rawValue] of Object.entries(env || {})) {
|
|
679
|
+
if (rawValue === undefined || rawValue === null) continue;
|
|
680
|
+
let value = String(rawValue);
|
|
681
|
+
for (const [currentPath, archivedPath] of replacements) {
|
|
682
|
+
value = value.split(currentPath).join(archivedPath);
|
|
683
|
+
}
|
|
684
|
+
isolated[key] = value;
|
|
685
|
+
}
|
|
686
|
+
return {
|
|
687
|
+
...isolated,
|
|
688
|
+
INIT_CWD: buildRoot,
|
|
689
|
+
LOWCODE_WORKSPACE_ROOT: buildRoot,
|
|
690
|
+
OPENXIANGDA_WORKSPACE_ROOT: buildRoot,
|
|
691
|
+
PWD: buildRoot,
|
|
692
|
+
npm_config_local_prefix: buildRoot,
|
|
693
|
+
npm_package_json: path.join(buildRoot, 'package.json'),
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function buildFailureError(target, batch, legacy) {
|
|
698
|
+
const output = [
|
|
699
|
+
summarizeCommandFailure('batch', batch),
|
|
700
|
+
summarizeCommandFailure(`legacy ${target.kind}:${target.code}`, legacy),
|
|
701
|
+
].join('\n');
|
|
702
|
+
return changeBaselineError(
|
|
703
|
+
'CHANGE_BASELINE_BUILD_FAILED',
|
|
704
|
+
`无法从 Git baseCommit 构建精确源码产物\n${output}`
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function summarizeCommandFailure(label, result) {
|
|
709
|
+
const detail = [result.error?.message, result.stdout, result.stderr]
|
|
710
|
+
.filter(Boolean)
|
|
711
|
+
.join('\n')
|
|
712
|
+
.trim()
|
|
713
|
+
.slice(-4000);
|
|
714
|
+
return `${label} (${result.command.join(' ')}) status=${result.status ?? 'spawn-error'}${
|
|
715
|
+
detail ? `\n${detail}` : ''
|
|
716
|
+
}`;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function assertArtifactFileSafe(buildRoot, artifactPath, target) {
|
|
720
|
+
if (!isPathInside(buildRoot, artifactPath)) {
|
|
721
|
+
throw changeBaselineError(
|
|
722
|
+
'CHANGE_BASELINE_ARTIFACT_UNSAFE',
|
|
723
|
+
`${target.kind}:${target.code} 产物路径越界`
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
let stat;
|
|
727
|
+
try {
|
|
728
|
+
stat = fs.lstatSync(artifactPath);
|
|
729
|
+
} catch {
|
|
730
|
+
throw changeBaselineError(
|
|
731
|
+
'CHANGE_BASELINE_ARTIFACT_MISSING',
|
|
732
|
+
`Git baseCommit 构建未生成 ${target.kind}:${target.code} 的 index.cjs`
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
736
|
+
throw changeBaselineError(
|
|
737
|
+
'CHANGE_BASELINE_ARTIFACT_UNSAFE',
|
|
738
|
+
`${target.kind}:${target.code} 产物必须是普通文件`
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
const realPath = fs.realpathSync(artifactPath);
|
|
742
|
+
if (!isPathInside(buildRoot, realPath)) {
|
|
743
|
+
throw changeBaselineError(
|
|
744
|
+
'CHANGE_BASELINE_ARTIFACT_UNSAFE',
|
|
745
|
+
`${target.kind}:${target.code} 产物指向临时 Git 基线之外`
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function validateGitTreeForSafeArchive(repoRoot, commit) {
|
|
751
|
+
const result = runGitBuffer(repoRoot, [
|
|
752
|
+
'ls-tree',
|
|
753
|
+
'-r',
|
|
754
|
+
'-z',
|
|
755
|
+
'--full-tree',
|
|
756
|
+
commit,
|
|
757
|
+
]);
|
|
758
|
+
for (const recordBuffer of splitNullBuffer(result)) {
|
|
759
|
+
const record = recordBuffer.toString('utf8');
|
|
760
|
+
const tab = record.indexOf('\t');
|
|
761
|
+
if (tab < 0) {
|
|
762
|
+
throw changeBaselineError(
|
|
763
|
+
'CHANGE_BASELINE_ARCHIVE_UNSAFE',
|
|
764
|
+
'Git tree 包含无法解析的条目'
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
const metadata = record.slice(0, tab).split(' ');
|
|
768
|
+
const entryPath = record.slice(tab + 1);
|
|
769
|
+
const mode = metadata[0];
|
|
770
|
+
const type = metadata[1];
|
|
771
|
+
if (type !== 'blob' || mode === '120000') {
|
|
772
|
+
throw changeBaselineError(
|
|
773
|
+
'CHANGE_BASELINE_ARCHIVE_UNSAFE',
|
|
774
|
+
`Git baseCommit 包含不允许归档的 ${type || 'unknown'} 条目`
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
assertSafeArchivePath(entryPath);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function validateTarEntryNames(archivePath) {
|
|
782
|
+
const result = runCommand('tar', ['-tf', archivePath], {
|
|
783
|
+
code: 'CHANGE_BASELINE_ARCHIVE_LIST_FAILED',
|
|
784
|
+
message: '无法检查 Git 基线归档',
|
|
785
|
+
encoding: 'utf8',
|
|
786
|
+
});
|
|
787
|
+
for (const entry of String(result.stdout || '').split(/\r?\n/).filter(Boolean)) {
|
|
788
|
+
assertSafeArchivePath(entry.replace(/\/$/, ''));
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function assertSafeArchivePath(value) {
|
|
793
|
+
const entry = String(value || '');
|
|
794
|
+
const normalized = path.posix.normalize(entry);
|
|
795
|
+
const segments = entry.split('/');
|
|
796
|
+
if (
|
|
797
|
+
!entry ||
|
|
798
|
+
entry.includes('\\') ||
|
|
799
|
+
/[\0\r\n]/.test(entry) ||
|
|
800
|
+
path.posix.isAbsolute(entry) ||
|
|
801
|
+
normalized === '..' ||
|
|
802
|
+
normalized.startsWith('../') ||
|
|
803
|
+
normalized !== entry.replace(/\/$/, '') ||
|
|
804
|
+
segments.some(segment => segment === '..' || segment === '.git')
|
|
805
|
+
) {
|
|
806
|
+
throw changeBaselineError(
|
|
807
|
+
'CHANGE_BASELINE_ARCHIVE_UNSAFE',
|
|
808
|
+
'Git baseCommit 归档包含不安全路径'
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function assertExtractedTreeSafe(root) {
|
|
814
|
+
const visit = directory => {
|
|
815
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
816
|
+
const entryPath = path.join(directory, entry.name);
|
|
817
|
+
const stat = fs.lstatSync(entryPath);
|
|
818
|
+
if (stat.isSymbolicLink()) {
|
|
819
|
+
throw changeBaselineError(
|
|
820
|
+
'CHANGE_BASELINE_ARCHIVE_UNSAFE',
|
|
821
|
+
'Git baseCommit 归档不能包含符号链接'
|
|
822
|
+
);
|
|
823
|
+
}
|
|
824
|
+
if (!isPathInside(root, fs.realpathSync(entryPath))) {
|
|
825
|
+
throw changeBaselineError(
|
|
826
|
+
'CHANGE_BASELINE_ARCHIVE_UNSAFE',
|
|
827
|
+
'Git baseCommit 解包路径越界'
|
|
828
|
+
);
|
|
829
|
+
}
|
|
830
|
+
if (entry.isDirectory()) visit(entryPath);
|
|
831
|
+
}
|
|
832
|
+
};
|
|
833
|
+
visit(root);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function resolveGitRepository(cwd) {
|
|
837
|
+
const resolvedCwd = path.resolve(cwd || process.cwd());
|
|
838
|
+
if (!isDirectory(resolvedCwd)) {
|
|
839
|
+
throw changeBaselineError(
|
|
840
|
+
'CHANGE_BASELINE_GIT_REQUIRED',
|
|
841
|
+
'cwd 不是可用目录'
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
const root = runGit(resolvedCwd, ['rev-parse', '--show-toplevel']).trim();
|
|
845
|
+
if (!root) {
|
|
846
|
+
throw changeBaselineError(
|
|
847
|
+
'CHANGE_BASELINE_GIT_REQUIRED',
|
|
848
|
+
'当前工作区不是 Git 仓库'
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
return { root: fs.realpathSync(path.resolve(root)) };
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function resolveExactCommit(repoRoot, ref) {
|
|
855
|
+
const commit = runGit(repoRoot, [
|
|
856
|
+
'rev-parse',
|
|
857
|
+
'--verify',
|
|
858
|
+
`${ref}^{commit}`,
|
|
859
|
+
]).trim();
|
|
860
|
+
assertGitObjectId(commit, 'baseCommit');
|
|
861
|
+
return commit;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function normalizeBaseRef(value) {
|
|
865
|
+
const ref = String(value || 'HEAD').trim();
|
|
866
|
+
if (!ref || ref.startsWith('-') || /[\0\r\n]/.test(ref)) {
|
|
867
|
+
throw changeBaselineError(
|
|
868
|
+
'CHANGE_BASELINE_REF_INVALID',
|
|
869
|
+
'Git baseRef 无效'
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
return ref;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
function normalizeImmutableCommit(value) {
|
|
876
|
+
const commit = String(value || '').trim();
|
|
877
|
+
if (!/^[0-9a-f]{40,64}$/i.test(commit)) {
|
|
878
|
+
throw changeBaselineError(
|
|
879
|
+
'CHANGE_BASELINE_COMMIT_NOT_EXACT',
|
|
880
|
+
'baseCommit 必须是完整的 Git commit id,不能使用 HEAD、branch 或短 hash'
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
return commit;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function assertGitObjectId(value, label) {
|
|
887
|
+
if (!/^[0-9a-f]{40,64}$/i.test(String(value || ''))) {
|
|
888
|
+
throw changeBaselineError(
|
|
889
|
+
'CHANGE_BASELINE_GIT_OBJECT_INVALID',
|
|
890
|
+
`Git ${label} 无效`
|
|
891
|
+
);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
function runGit(cwd, args) {
|
|
896
|
+
const result = runCommand('git', ['-C', cwd, ...args], {
|
|
897
|
+
code: 'CHANGE_BASELINE_GIT_FAILED',
|
|
898
|
+
message: `Git 命令失败: git ${args.join(' ')}`,
|
|
899
|
+
encoding: 'utf8',
|
|
900
|
+
});
|
|
901
|
+
return String(result.stdout || '');
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
function tryRunGit(cwd, args) {
|
|
905
|
+
const result = spawnSync('git', ['-C', cwd, ...args], {
|
|
906
|
+
encoding: 'utf8',
|
|
907
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
908
|
+
});
|
|
909
|
+
return result.status === 0 && !result.error ? String(result.stdout || '') : '';
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
function runGitBuffer(cwd, args) {
|
|
913
|
+
const result = runCommand('git', ['-C', cwd, ...args], {
|
|
914
|
+
code: 'CHANGE_BASELINE_GIT_FAILED',
|
|
915
|
+
message: `Git 命令失败: git ${args.join(' ')}`,
|
|
916
|
+
});
|
|
917
|
+
return Buffer.isBuffer(result.stdout) ? result.stdout : Buffer.from(result.stdout || '');
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
function runCommand(command, args, options = {}) {
|
|
921
|
+
const result = spawnSync(command, args, {
|
|
922
|
+
encoding: options.encoding,
|
|
923
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
924
|
+
});
|
|
925
|
+
if (result.error || result.status !== 0) {
|
|
926
|
+
const detail = [
|
|
927
|
+
result.error?.message,
|
|
928
|
+
Buffer.isBuffer(result.stderr)
|
|
929
|
+
? result.stderr.toString('utf8')
|
|
930
|
+
: result.stderr,
|
|
931
|
+
Buffer.isBuffer(result.stdout)
|
|
932
|
+
? result.stdout.toString('utf8')
|
|
933
|
+
: result.stdout,
|
|
934
|
+
]
|
|
935
|
+
.filter(Boolean)
|
|
936
|
+
.join('\n')
|
|
937
|
+
.trim()
|
|
938
|
+
.slice(-4000);
|
|
939
|
+
throw changeBaselineError(
|
|
940
|
+
options.code || 'CHANGE_BASELINE_COMMAND_FAILED',
|
|
941
|
+
`${options.message || `${command} 执行失败`}${detail ? `\n${detail}` : ''}`
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
return result;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
function splitNullBuffer(buffer) {
|
|
948
|
+
const records = [];
|
|
949
|
+
let start = 0;
|
|
950
|
+
for (let index = 0; index < buffer.length; index += 1) {
|
|
951
|
+
if (buffer[index] !== 0) continue;
|
|
952
|
+
if (index > start) records.push(buffer.subarray(start, index));
|
|
953
|
+
start = index + 1;
|
|
954
|
+
}
|
|
955
|
+
if (start < buffer.length) records.push(buffer.subarray(start));
|
|
956
|
+
return records;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
function normalizeRemoteIdentity(value) {
|
|
960
|
+
const remote = String(value || '').trim();
|
|
961
|
+
try {
|
|
962
|
+
const url = new URL(remote);
|
|
963
|
+
url.username = '';
|
|
964
|
+
url.password = '';
|
|
965
|
+
url.search = '';
|
|
966
|
+
url.hash = '';
|
|
967
|
+
return url.toString();
|
|
968
|
+
} catch {
|
|
969
|
+
return remote
|
|
970
|
+
.replace(/^[^@/]+@([^:]+):/, '$1:')
|
|
971
|
+
.replace(/[?#].*$/, '');
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
function getChangeBaselineOwnerThreadId(baseline) {
|
|
976
|
+
return String(
|
|
977
|
+
baseline?.workspace?.threadId || baseline?.ownerThreadId || ''
|
|
978
|
+
).trim();
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
function currentCodexThreadId(env = process.env) {
|
|
982
|
+
return String(env?.CODEX_THREAD_ID || '').trim();
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
function ownershipError(ownerThreadId, currentThreadId) {
|
|
986
|
+
return changeBaselineError(
|
|
987
|
+
'CHANGE_BASELINE_OWNED_BY_ANOTHER_TASK',
|
|
988
|
+
`change baseline 由另一个任务持有 (${ownerThreadId || 'unknown'});当前任务 ${
|
|
989
|
+
currentThreadId || '<none>'
|
|
990
|
+
} 不能复用、覆盖或清除`
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function preservedField(input, existing, key, fallback) {
|
|
995
|
+
if (Object.prototype.hasOwnProperty.call(input, key)) return input[key];
|
|
996
|
+
if (existing && Object.prototype.hasOwnProperty.call(existing, key)) {
|
|
997
|
+
return existing[key];
|
|
998
|
+
}
|
|
999
|
+
return fallback;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
function clonePreservedField(input, existing, key, fallback) {
|
|
1003
|
+
return cloneJson(preservedField(input, existing, key, fallback));
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
function cloneJson(value) {
|
|
1007
|
+
if (value === undefined) return undefined;
|
|
1008
|
+
return JSON.parse(JSON.stringify(value));
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
function normalizeOptionalString(value) {
|
|
1012
|
+
const text = String(value === undefined || value === null ? '' : value).trim();
|
|
1013
|
+
return text || null;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function normalizeNow(value) {
|
|
1017
|
+
const date = value === undefined ? new Date() : new Date(value);
|
|
1018
|
+
if (Number.isNaN(date.getTime())) {
|
|
1019
|
+
throw changeBaselineError(
|
|
1020
|
+
'CHANGE_BASELINE_INVALID',
|
|
1021
|
+
'change baseline updatedAt 无效'
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
return date.toISOString();
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
function isSafeCode(code) {
|
|
1028
|
+
return Boolean(
|
|
1029
|
+
code &&
|
|
1030
|
+
code !== '.' &&
|
|
1031
|
+
code !== '..' &&
|
|
1032
|
+
!/[\\/,:\0\r\n]/.test(code)
|
|
1033
|
+
);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
function isDirectory(value) {
|
|
1037
|
+
try {
|
|
1038
|
+
return fs.statSync(value).isDirectory();
|
|
1039
|
+
} catch {
|
|
1040
|
+
return false;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
function isOutsidePath(relativePath) {
|
|
1045
|
+
return (
|
|
1046
|
+
relativePath === '..' ||
|
|
1047
|
+
relativePath.startsWith(`..${path.sep}`) ||
|
|
1048
|
+
path.isAbsolute(relativePath)
|
|
1049
|
+
);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
function isPathInside(root, candidate) {
|
|
1053
|
+
const relative = path.relative(path.resolve(root), path.resolve(candidate));
|
|
1054
|
+
return !isOutsidePath(relative);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
function sha256Buffer(buffer) {
|
|
1058
|
+
return crypto.createHash('sha256').update(buffer).digest('hex');
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
function changeBaselineError(code, message) {
|
|
1062
|
+
const error = new Error(message);
|
|
1063
|
+
error.code = code;
|
|
1064
|
+
return error;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
module.exports = {
|
|
1068
|
+
buildGitBaseSourceArtifacts,
|
|
1069
|
+
canonicalJsonSha256,
|
|
1070
|
+
clearChangeBaseline,
|
|
1071
|
+
getStoredChangeBaseline,
|
|
1072
|
+
isChangeBaselineOwnedByCurrentSession,
|
|
1073
|
+
readGitSourceBase,
|
|
1074
|
+
saveChangeBaseline,
|
|
1075
|
+
sha256File,
|
|
1076
|
+
withGitBaseWorkspace,
|
|
1077
|
+
};
|