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 CHANGED
@@ -1,4 +1,4 @@
1
- <!-- OpenXiangda-Policy-Version: 5 -->
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.name || formCode,
10746
+ name: localSchema?.name || existingRemote?.name || formCode,
10722
10747
  });
10723
10748
  results.push({
10724
10749
  code: formCode,
10725
10750
  formUuid,
10726
- action: existingRemote ? 'bind' : 'create',
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
 
@@ -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 target = resolveTarget(config, flags, [], false);
66
- const result = await executor.status({ config, target, runId });
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 target = resolveTarget(config, flags, [], false);
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
- '编译并校验 App Package V2;默认复用现有 dist,不执行 Runtime 构建。',
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: ['用法: openxiangda status <runId> [--json]'],
216
- retry: ['用法: openxiangda retry <runId> [--json]'],
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
  ],