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 +162 -13
- package/lib/developer-center.js +6 -3
- package/package.json +2 -1
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
|
|
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
|
|
2274
|
+
const remoteStatus = await requestWithAuth(
|
|
2169
2275
|
config,
|
|
2170
2276
|
selected.binding.profile,
|
|
2171
2277
|
environmentSetApiPath(state.logicalApp.code, '/status')
|
|
2172
2278
|
);
|
|
2173
|
-
const
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
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
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
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,
|
package/lib/developer-center.js
CHANGED
|
@@ -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=>({"&":"&","<":"<",">":">","\\"":""","'":"'"}[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.
|
|
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",
|