openxiangda 1.0.190 → 1.0.192

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.
@@ -401,10 +401,34 @@ function withReleaseClientSessionArgs(args = [], flags = {}) {
401
401
  : [...args];
402
402
  }
403
403
 
404
+ function isRecoverablePostActivationDeploymentError(error, deployment) {
405
+ if (
406
+ deployment?.status !== 'deployed' ||
407
+ !String(deployment?.targetAppReleaseId || '').trim()
408
+ ) {
409
+ return false;
410
+ }
411
+ const detail = [
412
+ error?.code,
413
+ error?.message,
414
+ error?.publishLeaseCleanupError?.code,
415
+ error?.publishLeaseCleanupError?.message,
416
+ ]
417
+ .filter(Boolean)
418
+ .join(' ');
419
+ return (
420
+ /(publish[\s_-]*lease|publishLease|发布租约)/i.test(detail) &&
421
+ /(concurr|conflict|cleanup|release[\s_-]*end|并发冲突|清理|释放)/i.test(
422
+ detail
423
+ )
424
+ );
425
+ }
426
+
404
427
  module.exports = {
405
428
  bindEnvironmentTarget,
406
429
  buildCandidateBundle,
407
430
  canonicalJson,
431
+ isRecoverablePostActivationDeploymentError,
408
432
  normalizeEnvironmentKind,
409
433
  normalizeManagedChangeSourceBase,
410
434
  normalizeManagedReleaseSourceRevision,
package/lib/cli.js CHANGED
@@ -130,6 +130,7 @@ const { buildDesignReview, renderDesignReview } = require('./design-review');
130
130
  const {
131
131
  bindEnvironmentTarget,
132
132
  buildCandidateBundle,
133
+ isRecoverablePostActivationDeploymentError,
133
134
  normalizeEnvironmentKind,
134
135
  normalizeManagedChangeSourceBase,
135
136
  normalizeManagedReleaseSourceRevision,
@@ -1300,9 +1301,9 @@ async function waitForPublishLeaseAvailability(config, target, flags = {}) {
1300
1301
  }
1301
1302
  }
1302
1303
 
1303
- function readReleaseRecoveryContext(target, changeId) {
1304
+ function readReleaseRecoveryContext(target, changeId, deploymentId) {
1304
1305
  if (!changeId) return null;
1305
- const execution = readReleaseExecution(changeId);
1306
+ const execution = readReleaseExecution(changeId, deploymentId);
1306
1307
  const context = execution?.releaseContext;
1307
1308
  if (!context || typeof context !== 'object' || Array.isArray(context)) {
1308
1309
  return null;
@@ -2142,14 +2143,120 @@ function buildEnvironmentStatusDiff(status) {
2142
2143
  };
2143
2144
  }
2144
2145
 
2145
- function readStudioGit(args) {
2146
+ function readStudioGit(args, cwd = process.cwd()) {
2146
2147
  const result = spawnSync('git', args, {
2147
- cwd: process.cwd(),
2148
+ cwd,
2148
2149
  encoding: 'utf8',
2150
+ timeout: 5000,
2149
2151
  });
2150
2152
  return result.status === 0 ? String(result.stdout || '').trim() : '';
2151
2153
  }
2152
2154
 
2155
+ function studioGitSnapshot() {
2156
+ const porcelain = readStudioGit([
2157
+ 'status',
2158
+ '--porcelain',
2159
+ '--untracked-files=all',
2160
+ ]);
2161
+ const branch = readStudioGit(['branch', '--show-current']);
2162
+ const commit = readStudioGit(['rev-parse', 'HEAD']);
2163
+ const upstream = readStudioGit([
2164
+ 'rev-parse',
2165
+ '--abbrev-ref',
2166
+ '--symbolic-full-name',
2167
+ '@{upstream}',
2168
+ ]);
2169
+ const upstreamCommit = upstream
2170
+ ? readStudioGit(['rev-parse', upstream])
2171
+ : '';
2172
+ const upstreamParts = upstream.split('/');
2173
+ const remoteName = upstreamParts.length > 1 ? upstreamParts[0] : '';
2174
+ const upstreamBranch = upstreamParts.slice(1).join('/');
2175
+ const remoteHeadCommit =
2176
+ remoteName && upstreamBranch
2177
+ ? readStudioGit([
2178
+ 'ls-remote',
2179
+ '--heads',
2180
+ remoteName,
2181
+ `refs/heads/${upstreamBranch}`,
2182
+ ]).split(/\s+/)[0] || ''
2183
+ : '';
2184
+ const aheadBehind = upstream
2185
+ ? readStudioGit([
2186
+ 'rev-list',
2187
+ '--left-right',
2188
+ '--count',
2189
+ `${upstream}...HEAD`,
2190
+ ])
2191
+ .split(/\s+/)
2192
+ .map(value => Number(value))
2193
+ : [];
2194
+ const worktreePaths = readStudioGit(['worktree', 'list', '--porcelain'])
2195
+ .split(/\r?\n/)
2196
+ .filter(line => line.startsWith('worktree '))
2197
+ .map(line => line.slice('worktree '.length))
2198
+ .filter(Boolean);
2199
+ const dirtyWorktrees = worktreePaths
2200
+ .filter(worktreePath => path.resolve(worktreePath) !== process.cwd())
2201
+ .filter(worktreePath =>
2202
+ Boolean(
2203
+ readStudioGit(
2204
+ ['status', '--porcelain', '--untracked-files=all'],
2205
+ worktreePath
2206
+ )
2207
+ )
2208
+ );
2209
+ const ahead = Number.isFinite(aheadBehind[1]) ? aheadBehind[1] : null;
2210
+ const behind = Number.isFinite(aheadBehind[0]) ? aheadBehind[0] : null;
2211
+ const authoritativeBranch = ['main', 'master'].includes(branch);
2212
+ const upstreamMainline = /\/(?:main|master)$/.test(upstream);
2213
+ const clean = !porcelain;
2214
+ const mainlineAligned =
2215
+ clean &&
2216
+ authoritativeBranch &&
2217
+ upstreamMainline &&
2218
+ commit === upstreamCommit &&
2219
+ commit === remoteHeadCommit &&
2220
+ ahead === 0 &&
2221
+ behind === 0 &&
2222
+ dirtyWorktrees.length === 0;
2223
+ return {
2224
+ branch,
2225
+ commit,
2226
+ upstream: upstream || null,
2227
+ upstreamCommit: upstreamCommit || null,
2228
+ remoteHeadCommit: remoteHeadCommit || null,
2229
+ ahead,
2230
+ behind,
2231
+ clean,
2232
+ mainlineAligned,
2233
+ dirtyWorktrees,
2234
+ worktreeCount: worktreePaths.length,
2235
+ changes: porcelain ? porcelain.split(/\r?\n/).slice(0, 50) : [],
2236
+ };
2237
+ }
2238
+
2239
+ function isProductionCommissioning(environment) {
2240
+ const policy = environment?.sideEffectPolicy || {};
2241
+ return (
2242
+ policy.payments === 'deny' &&
2243
+ policy.notifications === 'tester_allowlist' &&
2244
+ policy.externalWrites === 'deny' &&
2245
+ policy.publicIndexing === 'deny' &&
2246
+ policy.organizationWrites === 'deny' &&
2247
+ policy.scheduledAutomations === 'disabled' &&
2248
+ policy.environmentBanner === true
2249
+ );
2250
+ }
2251
+
2252
+ function isDeploymentEvidenceValid(deployment, now = Date.now()) {
2253
+ if (!deployment?.evidenceHash) return false;
2254
+ const evidence = deployment.evidenceSummary;
2255
+ if (!evidence || evidence.outcome !== 'passed') return false;
2256
+ const validUntil = Date.parse(evidence.validUntil || '');
2257
+ return Number.isFinite(validUntil) && validUntil > now;
2258
+ }
2259
+
2153
2260
  async function studio(args) {
2154
2261
  const { flags } = parseArgs(args);
2155
2262
  const config = loadConfig();
@@ -2165,26 +2272,69 @@ async function studio(args) {
2165
2272
  state,
2166
2273
  state.currentTarget || 'preproduction'
2167
2274
  );
2168
- const remote = await requestWithAuth(
2275
+ const remoteStatus = await requestWithAuth(
2169
2276
  config,
2170
2277
  selected.binding.profile,
2171
2278
  environmentSetApiPath(state.logicalApp.code, '/status')
2172
2279
  );
2173
- const porcelain = readStudioGit([
2174
- 'status',
2175
- '--porcelain',
2176
- '--untracked-files=all',
2177
- ]);
2280
+ const environments = await Promise.all(
2281
+ (remoteStatus?.environments || []).map(async environment => {
2282
+ const deploymentId = environment?.latestDeployment?.id;
2283
+ if (!deploymentId) return environment;
2284
+ try {
2285
+ const latestDeployment = await requestWithAuth(
2286
+ config,
2287
+ selected.binding.profile,
2288
+ applicationDeploymentApiPath(
2289
+ state.logicalApp.code,
2290
+ deploymentId
2291
+ )
2292
+ );
2293
+ return { ...environment, latestDeployment };
2294
+ } catch {
2295
+ return environment;
2296
+ }
2297
+ })
2298
+ );
2299
+ const remote = { ...remoteStatus, environments };
2300
+ const git = studioGitSnapshot();
2301
+ const preproduction = environments.find(
2302
+ environment => environment.kind === 'preproduction'
2303
+ );
2304
+ const production = environments.find(
2305
+ environment => environment.kind === 'production'
2306
+ );
2307
+ const drift = buildEnvironmentStatusDiff(remote).drift;
2308
+ const evidenceValid = isDeploymentEvidenceValid(
2309
+ preproduction?.latestDeployment
2310
+ );
2311
+ const productionCommissioning =
2312
+ isProductionCommissioning(production);
2313
+ const candidateReady = git.mainlineAligned;
2314
+ const testRegistrationReady = Boolean(
2315
+ preproduction?.latestDeployment?.id &&
2316
+ preproduction.latestDeployment.status !== 'failed'
2317
+ );
2318
+ const promotionReady =
2319
+ candidateReady &&
2320
+ productionCommissioning &&
2321
+ evidenceValid &&
2322
+ preproduction?.latestDeployment?.status === 'succeeded' &&
2323
+ Boolean(preproduction.latestDeployment.candidateId);
2178
2324
  return {
2179
2325
  workspace: process.cwd(),
2180
2326
  logicalApp: state.logicalApp,
2181
2327
  currentTarget: state.currentTarget,
2182
2328
  targets: state.targets,
2183
- git: {
2184
- branch: readStudioGit(['branch', '--show-current']),
2185
- commit: readStudioGit(['rev-parse', 'HEAD']),
2186
- clean: !porcelain,
2187
- changes: porcelain ? porcelain.split(/\r?\n/).slice(0, 50) : [],
2329
+ git,
2330
+ drift,
2331
+ deliveryGates: {
2332
+ candidateReady,
2333
+ evidenceValid,
2334
+ productionCommissioning,
2335
+ promotionReady,
2336
+ rollbackReady: Boolean(production?.heads?.appReleaseId),
2337
+ testRegistrationReady,
2188
2338
  },
2189
2339
  openxiangdaVersion: CURRENT_VERSION,
2190
2340
  remote,
@@ -2493,6 +2643,33 @@ async function deployApplicationCandidate(
2493
2643
  rememberTargetDeployment(target, detail);
2494
2644
  return { deployment: detail, published };
2495
2645
  } catch (error) {
2646
+ let activatedDeployment = null;
2647
+ try {
2648
+ activatedDeployment = await requestWithAuth(
2649
+ config,
2650
+ target.profileName,
2651
+ applicationDeploymentApiPath(logicalApp.code, deployment.id)
2652
+ );
2653
+ } catch {
2654
+ // Preserve the primary deployment error when the read-only check fails.
2655
+ }
2656
+ if (
2657
+ isRecoverablePostActivationDeploymentError(error, activatedDeployment)
2658
+ ) {
2659
+ rememberTargetDeployment(target, activatedDeployment);
2660
+ warn(
2661
+ `AppRelease 已激活,发布租约清理发生并发冲突;部署 ${deployment.id} 保持 deployed,请执行 release status/end 完成只读回合清理。`
2662
+ );
2663
+ return {
2664
+ deployment: activatedDeployment,
2665
+ published: null,
2666
+ recoveredAfterActivation: true,
2667
+ postActivationError: {
2668
+ code: error?.code || 'PUBLISH_LEASE_CLEANUP_CONFLICT',
2669
+ message: maskText(error?.message || String(error)),
2670
+ },
2671
+ };
2672
+ }
2496
2673
  await markApplicationDeploymentFailed(
2497
2674
  config,
2498
2675
  target,
@@ -2549,6 +2726,35 @@ async function runApplicationEnvironmentReleaseCommand(
2549
2726
  );
2550
2727
  }
2551
2728
 
2729
+ if (subcommand === 'reconcile') {
2730
+ const deploymentId =
2731
+ readStringFlag(flags, 'deployment') ||
2732
+ readStringFlag(flags, 'deployment-id') ||
2733
+ positional[0] ||
2734
+ target.bound.lastDeploymentId;
2735
+ const reason = readStringFlag(flags, 'reason');
2736
+ if (!deploymentId || reason.length < 8) {
2737
+ fail(
2738
+ 'release reconcile 必须提供 --deployment <id> 和至少 8 个字符的 --reason'
2739
+ );
2740
+ }
2741
+ const reconciled = await requestWithAuth(
2742
+ config,
2743
+ target.profileName,
2744
+ applicationDeploymentApiPath(
2745
+ logicalApp.code,
2746
+ deploymentId,
2747
+ 'reconcile'
2748
+ ),
2749
+ {
2750
+ method: 'POST',
2751
+ body: { reason },
2752
+ }
2753
+ );
2754
+ rememberTargetDeployment(target, reconciled);
2755
+ return { deployment: reconciled, reconciled: true };
2756
+ }
2757
+
2552
2758
  const candidateId =
2553
2759
  readStringFlag(flags, 'candidate') ||
2554
2760
  readStringFlag(flags, 'candidate-id') ||
@@ -2682,7 +2888,7 @@ async function release(args) {
2682
2888
  if (wantsSubcommandHelp(subcommand, flags)) {
2683
2889
  print([
2684
2890
  '用法: openxiangda release publish|begin|status|explain|integration-status|renew|end [--change id] [--profile name] [--json]',
2685
- ' openxiangda release candidate|deploy|test|promote|rollback [--candidate id] [--environment target] [--confirm-production] [--json]',
2891
+ ' openxiangda release candidate|deploy|reconcile|test|promote|rollback [--candidate id] [--environment target] [--confirm-production] [--json]',
2686
2892
  ' openxiangda release backend-head|backend-list|backend-detail|backend-diff|backend-rollback|backend-abort|backend-retry [releaseId] [--profile name] [--json]',
2687
2893
  ' openxiangda release app-capture|app-head|app-list|app-detail|app-diff|app-post-commit|app-retry|app-prepare|app-verify|app-activate|app-finalize|app-rollback|app-abort [releaseId] [--staged-resources-json <JSON|file>] [--activate-staged-children] [--profile name] [--json]',
2688
2894
  '常用流程:',
@@ -2720,7 +2926,7 @@ async function release(args) {
2720
2926
  flags
2721
2927
  );
2722
2928
  if (
2723
- ['candidate', 'deploy', 'test', 'promote', 'rollback'].includes(
2929
+ ['candidate', 'deploy', 'reconcile', 'test', 'promote', 'rollback'].includes(
2724
2930
  subcommand
2725
2931
  )
2726
2932
  ) {
@@ -2853,7 +3059,11 @@ async function release(args) {
2853
3059
  access: 'reconciliation-read',
2854
3060
  });
2855
3061
  const changeId = positional[0] || readStringFlag(flags, 'change');
2856
- const recovered = readReleaseRecoveryContext(target, changeId);
3062
+ const recovered = readReleaseRecoveryContext(
3063
+ target,
3064
+ changeId,
3065
+ readStringFlag(flags, 'deployment-id')
3066
+ );
2857
3067
  const sourceRevision =
2858
3068
  baseline?.releaseSourceRevision ||
2859
3069
  recovered?.context?.releaseSourceRevision;
@@ -60,7 +60,7 @@ function studioHtml(sessionToken) {
60
60
  .pre{--accent:var(--pre)}.prod{--accent:var(--prod)}.card-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:22px}
61
61
  h2{font-size:19px;margin:0}.badge{border:1px solid color-mix(in srgb,var(--accent) 45%,var(--line));color:var(--accent);padding:5px 9px;border-radius:999px;font-size:11px;letter-spacing:.08em}
62
62
  dl{display:grid;grid-template-columns:130px 1fr;gap:11px;margin:0;font-size:13px}dt{color:var(--muted)}dd{margin:0;font-family:ui-monospace,SFMono-Regular,monospace;overflow-wrap:anywhere}
63
- .wide{grid-column:1/-1}.toolbar{display:flex;flex-wrap:wrap;gap:10px;margin-top:20px}button{appearance:none;border:1px solid #3a3d36;background:#22241f;color:#f4f4ef;padding:10px 14px;border-radius:10px;font-weight:600;cursor:pointer}
63
+ .wide{grid-column:1/-1}.toolbar{display:flex;flex-wrap:wrap;gap:10px;margin-top:20px}.gate-ok{color:var(--prod)}.gate-bad{color:var(--bad)}button{appearance:none;border:1px solid #3a3d36;background:#22241f;color:#f4f4ef;padding:10px 14px;border-radius:10px;font-weight:600;cursor:pointer}
64
64
  button:hover{border-color:#777c70}button.primary{background:#e9efe9;color:#111;border-color:#e9efe9}button.danger{color:#ffaaa3;border-color:#70423e}
65
65
  button:disabled{opacity:.4;cursor:not-allowed}.status{display:flex;gap:8px;align-items:center;color:var(--muted);font-size:13px}.dot{width:8px;height:8px;border-radius:50%;background:var(--prod);box-shadow:0 0 14px var(--prod)}
66
66
  pre{white-space:pre-wrap;word-break:break-word;background:#10110f;border:1px solid #252722;border-radius:12px;padding:16px;color:#c7cbc2;max-height:300px;overflow:auto;font:12px/1.55 ui-monospace,SFMono-Regular,monospace}
@@ -76,6 +76,7 @@ function studioHtml(sessionToken) {
76
76
  <section class="grid">
77
77
  <article class="card env pre"><div class="card-head"><h2>预发环境</h2><span class="badge">PREPRODUCTION</span></div><dl id="pre"></dl></article>
78
78
  <article class="card env prod"><div class="card-head"><h2>正式环境</h2><span class="badge">PRODUCTION</span></div><dl id="prod"></dl></article>
79
+ <article class="card wide"><div class="card-head"><h2>交付门禁与环境差异</h2><span class="badge" id="gate-badge">CHECKING</span></div><dl id="gates"></dl></article>
79
80
  <article class="card wide"><div class="card-head"><h2>交付状态</h2><div class="status"><span class="dot"></span><span id="health">连接中</span></div></div><div class="toolbar">
80
81
  <button data-action="candidate">生成候选</button><button data-action="deploy">部署预发</button><button data-action="test">登记测试证据</button><button class="primary" data-action="promote">晋级正式</button><button class="danger" data-action="rollback">准备回退</button><button data-action="refresh">刷新</button>
81
82
  </div><pre id="output">Developer Center 只监听 127.0.0.1;所有动作复用 OpenXiangda CLI 门禁。</pre></article>
@@ -87,9 +88,11 @@ const token=${token}; history.replaceState(null,"",location.pathname);
87
88
  const out=document.querySelector("#output"), dialog=document.querySelector("#dialog");
88
89
  let status=null, pending=null;
89
90
  const esc=v=>String(v??"-").replace(/[&<>"']/g,c=>({"&":"&amp;","<":"&lt;",">":"&gt;","\\"":"&quot;","'":"&#39;"}[c]));
90
- const rows=e=>[["appType",e?.appType],["公开地址",e?.publicOrigin],["AppRelease",e?.heads?.appReleaseId],["候选版本",e?.latestDeployment?.candidateId],["部署状态",e?.latestDeployment?.status],["副作用策略",JSON.stringify(e?.sideEffectPolicy||{})]].map(([k,v])=>\`<dt>\${esc(k)}</dt><dd>\${esc(v)}</dd>\`).join("");
91
+ const rows=e=>[["appType",e?.appType],["环境修订",e?.revision],["公开地址",e?.publicOrigin],["AppRelease",e?.heads?.appReleaseId],["RuntimeRelease",e?.heads?.runtimeReleaseId],["BackendRelease",e?.heads?.backendReleaseId],["PageRelease",e?.heads?.pageReleaseId],["WorkflowRelease",e?.heads?.workflowReleaseId],["候选版本",e?.latestDeployment?.candidateId],["Deployment",e?.latestDeployment?.id],["部署状态",e?.latestDeployment?.status],["测试证据",e?.latestDeployment?.evidenceHash],["证据有效期",e?.latestDeployment?.evidenceSummary?.validUntil],["副作用策略",JSON.stringify(e?.sideEffectPolicy||{})]].map(([k,v])=>\`<dt>\${esc(k)}</dt><dd>\${esc(v)}</dd>\`).join("");
92
+ const bool=v=>\`<span class="\${v?"gate-ok":"gate-bad"}">\${v?"通过":"阻断"}</span>\`;
93
+ const gateRows=s=>[["权威主线",s?.git?.upstream||"-"],["远端主线提交",s?.git?.remoteHeadCommit],["ahead / behind",\`\${s?.git?.ahead??"-"} / \${s?.git?.behind??"-"}\`],["工作区干净",bool(Boolean(s?.git?.clean))],["其他脏 worktree",s?.git?.dirtyWorktrees?.length?s.git.dirtyWorktrees.join(", "):bool(true)],["Candidate 门禁",bool(Boolean(s?.deliveryGates?.candidateReady))],["预发测试证据",bool(Boolean(s?.deliveryGates?.evidenceValid))],["正式 commissioning",bool(Boolean(s?.deliveryGates?.productionCommissioning))],["同一 AppRelease",bool(Boolean(s?.drift?.sameAppRelease))],["资源差异",s?.drift?.sameAppRelease?"无":"存在(预发/正式 Release Head 不同)"],["OpenXiangda",s?.openxiangdaVersion]].map(([k,v])=>\`<dt>\${esc(k)}</dt><dd>\${typeof v==="string"&&v.startsWith("<span")?v:esc(v)}</dd>\`).join("");
91
94
  async function api(path,options={}){const r=await fetch(path,{...options,headers:{"content-type":"application/json","x-openxiangda-studio-token":token,...options.headers}});const j=await r.json();if(!r.ok)throw new Error(j.message||"request failed");return j}
92
- async function refresh(){try{status=await api("/api/status");document.querySelector("#repo").textContent=\`\${status.git?.branch||"-"} @ \${(status.git?.commit||"-").slice(0,12)} · \${status.git?.clean?"clean":"dirty"}\`;document.querySelector("#pre").innerHTML=rows(status.remote?.environments?.find(e=>e.kind==="preproduction"));document.querySelector("#prod").innerHTML=rows(status.remote?.environments?.find(e=>e.kind==="production"));document.querySelector("#health").textContent="状态已同步";}catch(e){document.querySelector("#health").textContent="读取失败";out.textContent=e.message}}
95
+ async function refresh(){try{status=await api("/api/status");document.querySelector("#repo").textContent=\`\${status.git?.branch||"-"} @ \${(status.git?.commit||"-").slice(0,12)} · \${status.git?.mainlineAligned?"mainline ready":status.git?.clean?"clean / not aligned":"dirty"}\`;document.querySelector("#pre").innerHTML=rows(status.remote?.environments?.find(e=>e.kind==="preproduction"));document.querySelector("#prod").innerHTML=rows(status.remote?.environments?.find(e=>e.kind==="production"));document.querySelector("#gates").innerHTML=gateRows(status);const ready=Boolean(status.deliveryGates?.candidateReady);const badge=document.querySelector("#gate-badge");badge.textContent=ready?"READY":"BLOCKED";badge.className=\`badge \${ready?"gate-ok":"gate-bad"}\`;document.querySelector('[data-action="candidate"]').disabled=!ready;document.querySelector('[data-action="deploy"]').disabled=!ready;document.querySelector('[data-action="test"]').disabled=!status.deliveryGates?.testRegistrationReady;document.querySelector('[data-action="promote"]').disabled=!status.deliveryGates?.promotionReady;document.querySelector('[data-action="rollback"]').disabled=!status.deliveryGates?.rollbackReady;document.querySelector("#health").textContent="状态已同步";}catch(e){document.querySelector("#health").textContent="读取失败";out.textContent=e.message}}
93
96
  document.querySelectorAll("[data-action]").forEach(b=>b.onclick=()=>{pending=b.dataset.action;if(pending==="refresh")return refresh();document.querySelector("#dialog-title").textContent=b.textContent;document.querySelector("#value").value="";document.querySelector("#details").value=pending==="test"?JSON.stringify({outcome:"passed",requiredGates:["static","schema","appFunctions","roles","browser","lifecycle","cleanup"],results:{static:{status:"passed"},schema:{status:"passed"},appFunctions:{status:"passed"},roles:{status:"passed"},browser:{status:"passed"},lifecycle:{status:"passed"},cleanup:{status:"passed"}},cleanup:{passed:true,residueCount:0}},null,2):"";dialog.showModal()});
94
97
  document.querySelector("#confirm").onclick=async e=>{e.preventDefault();const action=pending;dialog.close();out.textContent="执行中…";try{const result=await api("/api/action",{method:"POST",body:JSON.stringify({action,value:document.querySelector("#value").value,details:document.querySelector("#details").value,confirmProduction:["promote","rollback"].includes(action)})});out.textContent=JSON.stringify(result,null,2);await refresh()}catch(error){out.textContent=error.message}};
95
98
  refresh();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openxiangda",
3
- "version": "1.0.190",
3
+ "version": "1.0.192",
4
4
  "description": "OpenXiangda CLI, workspace build tools, runtime SDK, and form components.",
5
5
  "private": false,
6
6
  "bin": {
@@ -111,6 +111,7 @@
111
111
  "test:page-release-cli": "node scripts/page-release-cli-smoke.mjs",
112
112
  "test:app-release-cli": "node scripts/app-release-cli-smoke.mjs",
113
113
  "test:application-environments": "node scripts/application-environments-smoke.mjs",
114
+ "test:developer-center": "node scripts/developer-center-smoke.mjs",
114
115
  "test:form-release-cas": "node scripts/form-release-cas-smoke.mjs",
115
116
  "test:source-dependencies": "node scripts/source-dependencies-smoke.mjs",
116
117
  "test:form-field-contract": "node scripts/form-field-contract-smoke.mjs",