openxiangda 1.0.190 → 1.0.191

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/lib/cli.js CHANGED
@@ -2142,14 +2142,120 @@ function buildEnvironmentStatusDiff(status) {
2142
2142
  };
2143
2143
  }
2144
2144
 
2145
- function readStudioGit(args) {
2145
+ function readStudioGit(args, cwd = process.cwd()) {
2146
2146
  const result = spawnSync('git', args, {
2147
- cwd: process.cwd(),
2147
+ cwd,
2148
2148
  encoding: 'utf8',
2149
+ timeout: 5000,
2149
2150
  });
2150
2151
  return result.status === 0 ? String(result.stdout || '').trim() : '';
2151
2152
  }
2152
2153
 
2154
+ function studioGitSnapshot() {
2155
+ const porcelain = readStudioGit([
2156
+ 'status',
2157
+ '--porcelain',
2158
+ '--untracked-files=all',
2159
+ ]);
2160
+ const branch = readStudioGit(['branch', '--show-current']);
2161
+ const commit = readStudioGit(['rev-parse', 'HEAD']);
2162
+ const upstream = readStudioGit([
2163
+ 'rev-parse',
2164
+ '--abbrev-ref',
2165
+ '--symbolic-full-name',
2166
+ '@{upstream}',
2167
+ ]);
2168
+ const upstreamCommit = upstream
2169
+ ? readStudioGit(['rev-parse', upstream])
2170
+ : '';
2171
+ const upstreamParts = upstream.split('/');
2172
+ const remoteName = upstreamParts.length > 1 ? upstreamParts[0] : '';
2173
+ const upstreamBranch = upstreamParts.slice(1).join('/');
2174
+ const remoteHeadCommit =
2175
+ remoteName && upstreamBranch
2176
+ ? readStudioGit([
2177
+ 'ls-remote',
2178
+ '--heads',
2179
+ remoteName,
2180
+ `refs/heads/${upstreamBranch}`,
2181
+ ]).split(/\s+/)[0] || ''
2182
+ : '';
2183
+ const aheadBehind = upstream
2184
+ ? readStudioGit([
2185
+ 'rev-list',
2186
+ '--left-right',
2187
+ '--count',
2188
+ `${upstream}...HEAD`,
2189
+ ])
2190
+ .split(/\s+/)
2191
+ .map(value => Number(value))
2192
+ : [];
2193
+ const worktreePaths = readStudioGit(['worktree', 'list', '--porcelain'])
2194
+ .split(/\r?\n/)
2195
+ .filter(line => line.startsWith('worktree '))
2196
+ .map(line => line.slice('worktree '.length))
2197
+ .filter(Boolean);
2198
+ const dirtyWorktrees = worktreePaths
2199
+ .filter(worktreePath => path.resolve(worktreePath) !== process.cwd())
2200
+ .filter(worktreePath =>
2201
+ Boolean(
2202
+ readStudioGit(
2203
+ ['status', '--porcelain', '--untracked-files=all'],
2204
+ worktreePath
2205
+ )
2206
+ )
2207
+ );
2208
+ const ahead = Number.isFinite(aheadBehind[1]) ? aheadBehind[1] : null;
2209
+ const behind = Number.isFinite(aheadBehind[0]) ? aheadBehind[0] : null;
2210
+ const authoritativeBranch = ['main', 'master'].includes(branch);
2211
+ const upstreamMainline = /\/(?:main|master)$/.test(upstream);
2212
+ const clean = !porcelain;
2213
+ const mainlineAligned =
2214
+ clean &&
2215
+ authoritativeBranch &&
2216
+ upstreamMainline &&
2217
+ commit === upstreamCommit &&
2218
+ commit === remoteHeadCommit &&
2219
+ ahead === 0 &&
2220
+ behind === 0 &&
2221
+ dirtyWorktrees.length === 0;
2222
+ return {
2223
+ branch,
2224
+ commit,
2225
+ upstream: upstream || null,
2226
+ upstreamCommit: upstreamCommit || null,
2227
+ remoteHeadCommit: remoteHeadCommit || null,
2228
+ ahead,
2229
+ behind,
2230
+ clean,
2231
+ mainlineAligned,
2232
+ dirtyWorktrees,
2233
+ worktreeCount: worktreePaths.length,
2234
+ changes: porcelain ? porcelain.split(/\r?\n/).slice(0, 50) : [],
2235
+ };
2236
+ }
2237
+
2238
+ function isProductionCommissioning(environment) {
2239
+ const policy = environment?.sideEffectPolicy || {};
2240
+ return (
2241
+ policy.payments === 'deny' &&
2242
+ policy.notifications === 'tester_allowlist' &&
2243
+ policy.externalWrites === 'deny' &&
2244
+ policy.publicIndexing === 'deny' &&
2245
+ policy.organizationWrites === 'deny' &&
2246
+ policy.scheduledAutomations === 'disabled' &&
2247
+ policy.environmentBanner === true
2248
+ );
2249
+ }
2250
+
2251
+ function isDeploymentEvidenceValid(deployment, now = Date.now()) {
2252
+ if (!deployment?.evidenceHash) return false;
2253
+ const evidence = deployment.evidenceSummary;
2254
+ if (!evidence || evidence.outcome !== 'passed') return false;
2255
+ const validUntil = Date.parse(evidence.validUntil || '');
2256
+ return Number.isFinite(validUntil) && validUntil > now;
2257
+ }
2258
+
2153
2259
  async function studio(args) {
2154
2260
  const { flags } = parseArgs(args);
2155
2261
  const config = loadConfig();
@@ -2165,26 +2271,69 @@ async function studio(args) {
2165
2271
  state,
2166
2272
  state.currentTarget || 'preproduction'
2167
2273
  );
2168
- const remote = await requestWithAuth(
2274
+ const remoteStatus = await requestWithAuth(
2169
2275
  config,
2170
2276
  selected.binding.profile,
2171
2277
  environmentSetApiPath(state.logicalApp.code, '/status')
2172
2278
  );
2173
- const porcelain = readStudioGit([
2174
- 'status',
2175
- '--porcelain',
2176
- '--untracked-files=all',
2177
- ]);
2279
+ const environments = await Promise.all(
2280
+ (remoteStatus?.environments || []).map(async environment => {
2281
+ const deploymentId = environment?.latestDeployment?.id;
2282
+ if (!deploymentId) return environment;
2283
+ try {
2284
+ const latestDeployment = await requestWithAuth(
2285
+ config,
2286
+ selected.binding.profile,
2287
+ applicationDeploymentApiPath(
2288
+ state.logicalApp.code,
2289
+ deploymentId
2290
+ )
2291
+ );
2292
+ return { ...environment, latestDeployment };
2293
+ } catch {
2294
+ return environment;
2295
+ }
2296
+ })
2297
+ );
2298
+ const remote = { ...remoteStatus, environments };
2299
+ const git = studioGitSnapshot();
2300
+ const preproduction = environments.find(
2301
+ environment => environment.kind === 'preproduction'
2302
+ );
2303
+ const production = environments.find(
2304
+ environment => environment.kind === 'production'
2305
+ );
2306
+ const drift = buildEnvironmentStatusDiff(remote).drift;
2307
+ const evidenceValid = isDeploymentEvidenceValid(
2308
+ preproduction?.latestDeployment
2309
+ );
2310
+ const productionCommissioning =
2311
+ isProductionCommissioning(production);
2312
+ const candidateReady = git.mainlineAligned;
2313
+ const testRegistrationReady = Boolean(
2314
+ preproduction?.latestDeployment?.id &&
2315
+ preproduction.latestDeployment.status !== 'failed'
2316
+ );
2317
+ const promotionReady =
2318
+ candidateReady &&
2319
+ productionCommissioning &&
2320
+ evidenceValid &&
2321
+ preproduction?.latestDeployment?.status === 'succeeded' &&
2322
+ Boolean(preproduction.latestDeployment.candidateId);
2178
2323
  return {
2179
2324
  workspace: process.cwd(),
2180
2325
  logicalApp: state.logicalApp,
2181
2326
  currentTarget: state.currentTarget,
2182
2327
  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) : [],
2328
+ git,
2329
+ drift,
2330
+ deliveryGates: {
2331
+ candidateReady,
2332
+ evidenceValid,
2333
+ productionCommissioning,
2334
+ promotionReady,
2335
+ rollbackReady: Boolean(production?.heads?.appReleaseId),
2336
+ testRegistrationReady,
2188
2337
  },
2189
2338
  openxiangdaVersion: CURRENT_VERSION,
2190
2339
  remote,
@@ -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.191",
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",