openxiangda 1.0.251 → 1.0.252
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 +13 -1
- package/lib/cli.js +50 -15
- package/lib/delivery-v2-cli.js +89 -6
- package/lib/delivery-v2-executor.js +424 -80
- package/lib/delivery-v2-package.js +445 -12
- package/lib/js-code-build.js +5 -3
- package/lib/policy.js +15 -0
- package/lib/release-plan.js +10 -3
- package/openxiangda-skills/SKILL.md +10 -2
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +8 -0
- package/package.json +2 -2
- package/policy/openxiangda-policy.json +8 -1
- package/templates/openxiangda-react-spa/.cursor/rules/openxiangda.mdc +1 -0
- package/templates/openxiangda-react-spa/.qoder/rules/openxiangda.md +1 -0
- package/templates/openxiangda-react-spa/AGENTS.md +2 -2
- package/templates/openxiangda-react-spa/DELIVERY.md +8 -2
- package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda.mdc +1 -0
- package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda.md +1 -0
- package/templates/sy-lowcode-app-workspace/AGENTS.md +2 -2
- package/templates/sy-lowcode-app-workspace/DELIVERY.md +8 -2
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- OpenXiangda-Policy-Version:
|
|
1
|
+
<!-- OpenXiangda-Policy-Version: 6 -->
|
|
2
2
|
# OpenXiangda
|
|
3
3
|
|
|
4
4
|
OpenXiangda is a lightweight CLI and skill package for private low-code platforms.
|
|
@@ -28,6 +28,18 @@ checkpoints live on the platform, so a CLI/AI interruption does not discard
|
|
|
28
28
|
completed work. V2 does not require SDD, a clean/pushed Git mainline,
|
|
29
29
|
`--change`, `--only`, or candidate/ship orchestration.
|
|
30
30
|
|
|
31
|
+
`check --json` reports the exact changed resources, dependency-only Form
|
|
32
|
+
bindings, and ordered execution plan. Authored Form/Backend/Workflow code is
|
|
33
|
+
built only with the CLI-sealed toolchain; a V2 package never links the caller's
|
|
34
|
+
`node_modules`, and undeclared third-party build dependencies fail before any
|
|
35
|
+
remote write. Resource removals also fail closed until a dedicated migration
|
|
36
|
+
path is used.
|
|
37
|
+
|
|
38
|
+
`status` and `retry` locate the run across preproduction and production when
|
|
39
|
+
`--environment` is omitted. Retry resumes only unfinished checkpoints, safely
|
|
40
|
+
replays local Form bindings on another machine, and uses a server-side attempt
|
|
41
|
+
fence so an older executor cannot continue writing after takeover.
|
|
42
|
+
|
|
31
43
|
The lower-level commands later in this README remain available for V1
|
|
32
44
|
compatibility and diagnostics; do not use them to assemble a normal V2 release.
|
|
33
45
|
|
package/lib/cli.js
CHANGED
|
@@ -10656,6 +10656,7 @@ async function form(args) {
|
|
|
10656
10656
|
fail('用法: openxiangda form ensure --only <formCode1,formCode2>');
|
|
10657
10657
|
}
|
|
10658
10658
|
const target = getWorkspaceTarget(config, profileName, flags);
|
|
10659
|
+
const replayLocal = Boolean(flags['replay-local']);
|
|
10659
10660
|
await ensureDirectMutationPublishContext(
|
|
10660
10661
|
config,
|
|
10661
10662
|
target,
|
|
@@ -10669,22 +10670,30 @@ async function form(args) {
|
|
|
10669
10670
|
target.bound.resources?.formSettings?.[formCode],
|
|
10670
10671
|
].find(entry => String(entry?.formUuid || '').trim());
|
|
10671
10672
|
if (existing?.formUuid) {
|
|
10673
|
+
if (replayLocal) {
|
|
10674
|
+
try {
|
|
10675
|
+
await requestWithAuth(
|
|
10676
|
+
config,
|
|
10677
|
+
target.profileName,
|
|
10678
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/forms/${encodeURIComponent(existing.formUuid)}`
|
|
10679
|
+
);
|
|
10680
|
+
} catch (error) {
|
|
10681
|
+
if (!isHttpNotFound(error)) throw error;
|
|
10682
|
+
const missing = new Error(
|
|
10683
|
+
`FORM_ENSURE_REPLAY_TARGET_MISSING: ${formCode} 的已绑定表单 ${existing.formUuid} 在目标环境不存在,不能以 replay-local 补写远端资源`
|
|
10684
|
+
);
|
|
10685
|
+
missing.code = 'FORM_ENSURE_REPLAY_TARGET_MISSING';
|
|
10686
|
+
missing.retryable = false;
|
|
10687
|
+
throw missing;
|
|
10688
|
+
}
|
|
10689
|
+
}
|
|
10672
10690
|
results.push({
|
|
10673
10691
|
code: formCode,
|
|
10674
10692
|
formUuid: existing.formUuid,
|
|
10675
|
-
action: 'noop',
|
|
10693
|
+
action: replayLocal ? 'verify-local' : 'noop',
|
|
10676
10694
|
});
|
|
10677
10695
|
continue;
|
|
10678
10696
|
}
|
|
10679
|
-
const localSchema = await loadLocalFormSchema(
|
|
10680
|
-
process.cwd(),
|
|
10681
|
-
formCode
|
|
10682
|
-
);
|
|
10683
|
-
if (!localSchema) {
|
|
10684
|
-
fail(
|
|
10685
|
-
`FORM_ENSURE_SCHEMA_REQUIRED: ${formCode} 缺少 src/forms/${formCode}/schema.ts`
|
|
10686
|
-
);
|
|
10687
|
-
}
|
|
10688
10697
|
const formUuid = managedFormUuidForCode(
|
|
10689
10698
|
target.appType,
|
|
10690
10699
|
formCode
|
|
@@ -10699,6 +10708,22 @@ async function form(args) {
|
|
|
10699
10708
|
} catch (error) {
|
|
10700
10709
|
if (!isHttpNotFound(error)) throw error;
|
|
10701
10710
|
}
|
|
10711
|
+
if (!existingRemote && replayLocal) {
|
|
10712
|
+
const missing = new Error(
|
|
10713
|
+
`FORM_ENSURE_REPLAY_TARGET_MISSING: ${formCode} 对应的确定性表单 ${formUuid} 在目标环境不存在;replay-local 禁止创建远端资源`
|
|
10714
|
+
);
|
|
10715
|
+
missing.code = 'FORM_ENSURE_REPLAY_TARGET_MISSING';
|
|
10716
|
+
missing.retryable = false;
|
|
10717
|
+
throw missing;
|
|
10718
|
+
}
|
|
10719
|
+
const localSchema = existingRemote
|
|
10720
|
+
? null
|
|
10721
|
+
: await loadLocalFormSchema(process.cwd(), formCode);
|
|
10722
|
+
if (!existingRemote && !localSchema) {
|
|
10723
|
+
fail(
|
|
10724
|
+
`FORM_ENSURE_SCHEMA_REQUIRED: ${formCode} 缺少 src/forms/${formCode}/schema.ts`
|
|
10725
|
+
);
|
|
10726
|
+
}
|
|
10702
10727
|
if (!existingRemote) {
|
|
10703
10728
|
await requestWithAuth(
|
|
10704
10729
|
config,
|
|
@@ -10718,12 +10743,16 @@ async function form(args) {
|
|
|
10718
10743
|
);
|
|
10719
10744
|
}
|
|
10720
10745
|
saveFormResource(target, formCode, formUuid, {
|
|
10721
|
-
name: localSchema
|
|
10746
|
+
name: localSchema?.name || existingRemote?.name || formCode,
|
|
10722
10747
|
});
|
|
10723
10748
|
results.push({
|
|
10724
10749
|
code: formCode,
|
|
10725
10750
|
formUuid,
|
|
10726
|
-
action:
|
|
10751
|
+
action: replayLocal
|
|
10752
|
+
? 'rebind-local'
|
|
10753
|
+
: existingRemote
|
|
10754
|
+
? 'bind'
|
|
10755
|
+
: 'create',
|
|
10727
10756
|
});
|
|
10728
10757
|
}
|
|
10729
10758
|
const result = {
|
|
@@ -10734,6 +10763,9 @@ async function form(args) {
|
|
|
10734
10763
|
create: results.filter(item => item.action === 'create').length,
|
|
10735
10764
|
bind: results.filter(item => item.action === 'bind').length,
|
|
10736
10765
|
noop: results.filter(item => item.action === 'noop').length,
|
|
10766
|
+
replayed: results.filter(item =>
|
|
10767
|
+
['verify-local', 'rebind-local'].includes(item.action)
|
|
10768
|
+
).length,
|
|
10737
10769
|
},
|
|
10738
10770
|
};
|
|
10739
10771
|
if (flags.json) return writeJson(result);
|
|
@@ -20068,7 +20100,8 @@ async function prepareManifestJsCodeBundlesForPlan(manifest) {
|
|
|
20068
20100
|
sourceInfos.map(item => ({
|
|
20069
20101
|
sourceKind: item.sourceKind,
|
|
20070
20102
|
scriptCode: item.scriptCode,
|
|
20071
|
-
}))
|
|
20103
|
+
})),
|
|
20104
|
+
{ forceCanonical: Boolean(activeDeliveryV2Context) }
|
|
20072
20105
|
);
|
|
20073
20106
|
if (result.status !== 0) {
|
|
20074
20107
|
const legacyFallback = result.openxiangdaBuildMode === 'workspace-script'
|
|
@@ -28056,7 +28089,8 @@ function ensureJsCodeBundleForPlan(sourceInfo) {
|
|
|
28056
28089
|
sourceKind: sourceInfo.sourceKind,
|
|
28057
28090
|
scriptCode: sourceInfo.scriptCode,
|
|
28058
28091
|
},
|
|
28059
|
-
]
|
|
28092
|
+
],
|
|
28093
|
+
{ forceCanonical: Boolean(activeDeliveryV2Context) }
|
|
28060
28094
|
);
|
|
28061
28095
|
if (result.stdout) process.stderr.write(result.stdout);
|
|
28062
28096
|
if (result.stderr) process.stderr.write(result.stderr);
|
|
@@ -28399,7 +28433,8 @@ async function resolveJsCodeBundlePath(localPath, scriptCode) {
|
|
|
28399
28433
|
function runWorkspaceJsCodeBuild(workspaceRoot, resolvedScriptCode, sourceKind) {
|
|
28400
28434
|
return runWorkspaceJsCodeBuildBatch(
|
|
28401
28435
|
workspaceRoot,
|
|
28402
|
-
[{ sourceKind, scriptCode: resolvedScriptCode }]
|
|
28436
|
+
[{ sourceKind, scriptCode: resolvedScriptCode }],
|
|
28437
|
+
{ forceCanonical: Boolean(activeDeliveryV2Context) }
|
|
28403
28438
|
);
|
|
28404
28439
|
}
|
|
28405
28440
|
|
package/lib/delivery-v2-cli.js
CHANGED
|
@@ -33,6 +33,7 @@ function createDeliveryV2Cli(dependencies) {
|
|
|
33
33
|
packageDigest: inspected.compiled.packageDigest,
|
|
34
34
|
summary: inspected.compiled.summary,
|
|
35
35
|
targets: inspected.targets,
|
|
36
|
+
plan: inspected.plan,
|
|
36
37
|
previousPackageDigest: inspected.previous?.packageDigest || null,
|
|
37
38
|
},
|
|
38
39
|
`Delivery V2 检查通过: ${inspected.compiled.packageDigest}`
|
|
@@ -62,8 +63,8 @@ function createDeliveryV2Cli(dependencies) {
|
|
|
62
63
|
if (!runId) {
|
|
63
64
|
throw usageError('用法: openxiangda status <runId> [--json]');
|
|
64
65
|
}
|
|
65
|
-
const
|
|
66
|
-
const result =
|
|
66
|
+
const resolved = await resolveRunTarget(config, flags, runId);
|
|
67
|
+
const result = resolved.run;
|
|
67
68
|
return output(
|
|
68
69
|
flags,
|
|
69
70
|
result,
|
|
@@ -75,7 +76,8 @@ function createDeliveryV2Cli(dependencies) {
|
|
|
75
76
|
if (!runId) {
|
|
76
77
|
throw usageError('用法: openxiangda retry <runId> [--json]');
|
|
77
78
|
}
|
|
78
|
-
const
|
|
79
|
+
const resolved = await resolveRunTarget(config, flags, runId);
|
|
80
|
+
const target = resolved.target;
|
|
79
81
|
const result = await executor.retry({
|
|
80
82
|
config,
|
|
81
83
|
target,
|
|
@@ -132,9 +134,83 @@ function createDeliveryV2Cli(dependencies) {
|
|
|
132
134
|
);
|
|
133
135
|
}
|
|
134
136
|
|
|
137
|
+
async function resolveRunTarget(config, flags, runId) {
|
|
138
|
+
const explicitEnvironment =
|
|
139
|
+
stringFlag(flags, 'environment') || stringFlag(flags, 'target');
|
|
140
|
+
if (explicitEnvironment) {
|
|
141
|
+
const target = resolveTarget(
|
|
142
|
+
config,
|
|
143
|
+
{ ...flags, environment: explicitEnvironment },
|
|
144
|
+
[],
|
|
145
|
+
false
|
|
146
|
+
);
|
|
147
|
+
return {
|
|
148
|
+
target,
|
|
149
|
+
run: await executor.status({ config, target, runId }),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const candidates = [];
|
|
154
|
+
const addCandidate = candidate => {
|
|
155
|
+
if (!candidate) return;
|
|
156
|
+
const key = [
|
|
157
|
+
candidate.profileName,
|
|
158
|
+
candidate.appType,
|
|
159
|
+
candidate.environmentId || 'direct',
|
|
160
|
+
].join(':');
|
|
161
|
+
if (candidates.some(item => item.key === key)) return;
|
|
162
|
+
candidates.push({ key, target: candidate });
|
|
163
|
+
};
|
|
164
|
+
try {
|
|
165
|
+
addCandidate(resolveTarget(config, flags, [], false));
|
|
166
|
+
} catch {
|
|
167
|
+
// A multi-environment workspace may require an explicit target; probe
|
|
168
|
+
// the canonical targets below before returning a not-found error.
|
|
169
|
+
}
|
|
170
|
+
for (const environment of ['preproduction', 'production']) {
|
|
171
|
+
try {
|
|
172
|
+
addCandidate(
|
|
173
|
+
deps.getWorkspaceTarget(
|
|
174
|
+
config,
|
|
175
|
+
stringFlag(flags, 'profile') || config.currentProfile,
|
|
176
|
+
{ ...flags, environment }
|
|
177
|
+
)
|
|
178
|
+
);
|
|
179
|
+
} catch {
|
|
180
|
+
// This workspace does not define the candidate environment.
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let notFound = null;
|
|
185
|
+
for (const candidate of candidates) {
|
|
186
|
+
try {
|
|
187
|
+
const run = await executor.status({
|
|
188
|
+
config,
|
|
189
|
+
target: candidate.target,
|
|
190
|
+
runId,
|
|
191
|
+
});
|
|
192
|
+
return { target: candidate.target, run };
|
|
193
|
+
} catch (error) {
|
|
194
|
+
if (!isRunNotFoundError(error)) throw error;
|
|
195
|
+
notFound = error;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (notFound) throw notFound;
|
|
199
|
+
throw usageError(
|
|
200
|
+
`无法解析 ReleaseRun ${runId} 所属环境;请显式添加 --environment preproduction|production`
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
135
204
|
return { run };
|
|
136
205
|
}
|
|
137
206
|
|
|
207
|
+
function isRunNotFoundError(error) {
|
|
208
|
+
return (
|
|
209
|
+
Number(error?.status || error?.statusCode) === 404 ||
|
|
210
|
+
/DELIVERY_RUN_NOT_FOUND|HTTP 404/i.test(String(error?.message || ''))
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
138
214
|
function output(flags, value, message) {
|
|
139
215
|
if (flags.json) return writeJson(value);
|
|
140
216
|
print(message);
|
|
@@ -206,14 +282,21 @@ function printHelp(command) {
|
|
|
206
282
|
const lines = {
|
|
207
283
|
check: [
|
|
208
284
|
'用法: openxiangda check [--environment <name>] [--build] [--json]',
|
|
209
|
-
'
|
|
285
|
+
'编译并校验密封 App Package,输出精确资源差异和执行计划;默认复用现有 dist。',
|
|
286
|
+
'删除或未进入密封工具链的第三方构建依赖会在远程写入前失败。',
|
|
210
287
|
],
|
|
211
288
|
deploy: [
|
|
212
289
|
'用法: openxiangda deploy <environment> [--package <sha256>] [--json]',
|
|
213
290
|
'不带 --package 时从当前工作区构建;带 --package 时直接部署已封存制品。',
|
|
214
291
|
],
|
|
215
|
-
status: [
|
|
216
|
-
|
|
292
|
+
status: [
|
|
293
|
+
'用法: openxiangda status <runId> [--environment <name>] [--json]',
|
|
294
|
+
'未指定环境时自动在预发和生产定位 ReleaseRun。',
|
|
295
|
+
],
|
|
296
|
+
retry: [
|
|
297
|
+
'用法: openxiangda retry <runId> [--environment <name>] [--json]',
|
|
298
|
+
'从服务端检查点继续;隔离旧 attempt,并在跨机器场景重建本地 Form 绑定。',
|
|
299
|
+
],
|
|
217
300
|
rollback: [
|
|
218
301
|
'用法: openxiangda rollback <environment> --to <appReleaseId> [--json]',
|
|
219
302
|
],
|