openxiangda 1.0.231 → 1.0.232

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: 4 -->
1
+ <!-- OpenXiangda-Policy-Version: 5 -->
2
2
  # OpenXiangda
3
3
 
4
4
  OpenXiangda is a lightweight CLI and skill package for private low-code platforms.
@@ -7,6 +7,30 @@ Normal OpenXiangda app development uses platform-user login tokens through `/ope
7
7
 
8
8
  Private platform routing is fixed: backend APIs are under `/service`, platform management is under `/platform`, and app runtime access is under `/view`. Passing a root domain such as `https://yida.wisejob.cn/` to the CLI is supported; OpenXiangda stores the API base as `https://yida.wisejob.cn/service`.
9
9
 
10
+ ## Delivery V2
11
+
12
+ New workspaces declare `deliveryVersion: 2`. The release flow is intentionally
13
+ small and deterministic:
14
+
15
+ ```bash
16
+ openxiangda check --environment preproduction
17
+ openxiangda deploy preproduction --json
18
+ openxiangda deploy production --package <packageDigest> --json
19
+ openxiangda status <runId> --json
20
+ openxiangda retry <runId> --json
21
+ openxiangda rollback production --to <appReleaseId> --json
22
+ ```
23
+
24
+ The preproduction deploy compiles one content-addressed App Package containing
25
+ source, form, configuration, backend, workflow, and Runtime layers. Production
26
+ downloads that exact package and does not rebuild it. ReleaseRun state and
27
+ checkpoints live on the platform, so a CLI/AI interruption does not discard
28
+ completed work. V2 does not require SDD, a clean/pushed Git mainline,
29
+ `--change`, `--only`, or candidate/ship orchestration.
30
+
31
+ The lower-level commands later in this README remain available for V1
32
+ compatibility and diagnostics; do not use them to assemble a normal V2 release.
33
+
10
34
  ```bash
11
35
  npm install -g openxiangda@latest --registry=https://registry.npmjs.org
12
36
  openxiangda update check
@@ -22,7 +46,8 @@ openxiangda env
22
46
  openxiangda workspace init ./my-app-workspace --profile dev --app-name "示例应用"
23
47
  cd ./my-app-workspace
24
48
  pnpm install
25
- openxiangda workspace publish --profile dev
49
+ openxiangda check --environment preproduction
50
+ openxiangda deploy preproduction --json
26
51
  openxiangda menu list --profile dev
27
52
  openxiangda workflow list --profile dev
28
53
  openxiangda automation list --profile dev
package/lib/cli.js CHANGED
@@ -166,6 +166,7 @@ const {
166
166
  withReleaseClientSessionArgs,
167
167
  } = require('./application-environments');
168
168
  const { startDeveloperCenter } = require('./developer-center');
169
+ const { createDeliveryV2Cli } = require('./delivery-v2-cli');
169
170
  const {
170
171
  describeOpenApiOperation,
171
172
  listOpenApiOperations,
@@ -219,6 +220,8 @@ let readOnlyHttpGuardLabel = null;
219
220
  let activePublishRequestContext = null;
220
221
  let activePublishLeaseHeartbeat = null;
221
222
  let activeCommandScopedPublishLease = null;
223
+ let activeDeliveryV2Context = null;
224
+ let deliveryV2Cli = null;
222
225
  const genericGitBasePlanCache = new Map();
223
226
 
224
227
  async function main(argv) {
@@ -270,6 +273,9 @@ async function mainImpl(argv) {
270
273
  if (command === 'design') return design(rest);
271
274
  if (command === 'sdd') return sdd(rest);
272
275
  if (command === 'environment') return environment(rest);
276
+ if (['check', 'deploy', 'status', 'retry', 'rollback'].includes(command)) {
277
+ return getDeliveryV2Cli().run(command, rest);
278
+ }
273
279
  if (command === 'release') return release(rest);
274
280
  if (command === 'studio') return studio(rest);
275
281
  if (command === 'task') return task(rest);
@@ -321,6 +327,11 @@ Usage:
321
327
  openxiangda design gates|template|review [--topic code] [--json]
322
328
  openxiangda sdd init|migrate|propose|quick|bundle|approve|status|context|verify|sync|archive [change] [--change id] [--json]
323
329
  openxiangda environment init|attach|bind|swap|policy update|list|status|diff|use [preproduction|production] [--logical-app code] [--environment target] [--json]
330
+ openxiangda check [--environment preproduction|production] [--json]
331
+ openxiangda deploy <preproduction|production> [--package <sha256>] [--profile name] [--json]
332
+ openxiangda status <runId> [--environment preproduction|production] [--json]
333
+ openxiangda retry <runId> [--environment preproduction|production] [--json]
334
+ openxiangda rollback <preproduction|production> --to <appReleaseId> [--json]
324
335
  openxiangda release publish|begin|status|integration-status|renew|end [--change id] [--profile name] [--watch] [--json]
325
336
  openxiangda release ship|candidate|deploy|test|promote|rollback [--candidate id] [--environment target] [--replace-manifest --reason text] [--adopt-online-baseline --adoption-reason text] [--json]
326
337
  openxiangda task status --change <id> [--profile name] [--watch] [--json]
@@ -1451,6 +1462,120 @@ async function runOpenXiangdaInProcess(args, options = {}) {
1451
1462
  }
1452
1463
  }
1453
1464
 
1465
+ function getDeliveryV2Cli() {
1466
+ if (deliveryV2Cli) return deliveryV2Cli;
1467
+ deliveryV2Cli = createDeliveryV2Cli({
1468
+ clearDeliveryContext: clearDeliveryV2Context,
1469
+ downloadWithAuth,
1470
+ getWorkspaceTarget,
1471
+ installDeliveryContext: installDeliveryV2Context,
1472
+ loadConfig,
1473
+ requestFormWithAuth,
1474
+ requestWithAuth,
1475
+ runOpenXiangdaInProcess,
1476
+ });
1477
+ return deliveryV2Cli;
1478
+ }
1479
+
1480
+ function deliveryV2SourceRevision(packageDigest) {
1481
+ return {
1482
+ repo: `sha256:${packageDigest}`,
1483
+ repositoryId: `sha256:${packageDigest}`,
1484
+ repoAliases: [`sha256:${packageDigest}`],
1485
+ baseCommit: packageDigest,
1486
+ treeHash: packageDigest,
1487
+ branch: 'delivery-v2',
1488
+ mainBranch: null,
1489
+ remoteName: null,
1490
+ remoteUrlHash: null,
1491
+ };
1492
+ }
1493
+
1494
+ function installDeliveryV2Context(target, run, targets = {}) {
1495
+ const control = run?.control || {};
1496
+ const publishLeaseId = String(control.publishLeaseId || '').trim();
1497
+ const baselineId = String(control.baselineId || '').trim();
1498
+ const changeId = String(control.changeId || '').trim();
1499
+ const clientSessionId = String(control.clientSessionId || '').trim();
1500
+ const packageDigest = String(run?.packageDigest || '').trim();
1501
+ if (
1502
+ !publishLeaseId ||
1503
+ !baselineId ||
1504
+ !changeId ||
1505
+ !clientSessionId ||
1506
+ !/^[a-f0-9]{64}$/.test(packageDigest)
1507
+ ) {
1508
+ fail(
1509
+ 'DELIVERY_CONTROL_INVALID: ReleaseRun 缺少 publishLease/baseline/change/session/package 控制信息'
1510
+ );
1511
+ }
1512
+ const sourceRevision = deliveryV2SourceRevision(packageDigest);
1513
+ saveChangeBaseline(target, {
1514
+ baselineId,
1515
+ appType: target.appType,
1516
+ profile: target.profileName,
1517
+ changeId,
1518
+ clientSessionId,
1519
+ sourceBase: {
1520
+ repo: sourceRevision.repo,
1521
+ baseCommit: packageDigest,
1522
+ treeHash: packageDigest,
1523
+ },
1524
+ releaseSourceRevision: sourceRevision,
1525
+ resourceHeads: {},
1526
+ lifecycleState: 'active',
1527
+ });
1528
+ savePublishLease(target, {
1529
+ leaseId: publishLeaseId,
1530
+ expiresAt:
1531
+ control.leaseExpiresAt ||
1532
+ new Date(Date.now() + 60 * 60 * 1000).toISOString(),
1533
+ appType: target.appType,
1534
+ profile: target.profileName,
1535
+ changeId,
1536
+ clientSessionId,
1537
+ baseRevision: packageDigest,
1538
+ holder: 'self',
1539
+ workspace: {
1540
+ branch: 'delivery-v2',
1541
+ commit: packageDigest,
1542
+ treeHash: packageDigest,
1543
+ },
1544
+ });
1545
+ activeDeliveryV2Context = {
1546
+ runId: run.id,
1547
+ appType: target.appType,
1548
+ profileName: target.profileName,
1549
+ packageDigest,
1550
+ publishLeaseId,
1551
+ baselineId,
1552
+ changeId,
1553
+ clientSessionId,
1554
+ leaseExpiresAt: control.leaseExpiresAt,
1555
+ sourceRevision,
1556
+ targets,
1557
+ };
1558
+ activePublishRequestContext = normalizePublishContext({
1559
+ appType: target.appType,
1560
+ profileName: target.profileName,
1561
+ publishLeaseId,
1562
+ changeBaselineId: baselineId,
1563
+ changeId,
1564
+ clientSessionId,
1565
+ });
1566
+ return activeDeliveryV2Context;
1567
+ }
1568
+
1569
+ function clearDeliveryV2Context(target, run) {
1570
+ const context = activeDeliveryV2Context;
1571
+ if (!context) return;
1572
+ if (run?.id && context.runId !== run.id) return;
1573
+ clearPublishLease(target, context.publishLeaseId);
1574
+ clearChangeBaseline(target, context.baselineId);
1575
+ activePublishRequestContext = null;
1576
+ activeDeliveryV2Context = null;
1577
+ }
1578
+
1454
1579
  function normalizeReleaseWaitSeconds(flags) {
1455
1580
  const raw =
1456
1581
  readStringFlag(flags, 'wait-seconds') ||
@@ -6478,22 +6603,35 @@ function assertStagedAppReleaseScope(target, flags, resources) {
6478
6603
  }
6479
6604
  const stagedContext = assertStagedResourcesContext(target, changeId);
6480
6605
  let scope;
6481
- try {
6482
- scope = getSddChangeScope({
6483
- cwd: process.cwd(),
6484
- configText: readWorkspaceConfigText(),
6606
+ if (activeDeliveryV2Context?.changeId === changeId) {
6607
+ scope = {
6485
6608
  changeId,
6486
- });
6487
- } catch (error) {
6488
- if (/SDD 未初始化|change 不存在|ENOENT/i.test(String(error?.message || ''))) {
6489
- return {
6609
+ targets: activeDeliveryV2Context.targets || {},
6610
+ changedFiles: [],
6611
+ deliveryRunId: activeDeliveryV2Context.runId,
6612
+ };
6613
+ } else {
6614
+ try {
6615
+ scope = getSddChangeScope({
6616
+ cwd: process.cwd(),
6617
+ configText: readWorkspaceConfigText(),
6490
6618
  changeId,
6491
- scope: null,
6492
- resources: normalizeStagedAppReleaseResources(resources),
6493
- compatibility: 'unmanaged_sdd_scope',
6494
- };
6619
+ });
6620
+ } catch (error) {
6621
+ if (
6622
+ /SDD 未初始化|change 不存在|ENOENT/i.test(
6623
+ String(error?.message || '')
6624
+ )
6625
+ ) {
6626
+ return {
6627
+ changeId,
6628
+ scope: null,
6629
+ resources: normalizeStagedAppReleaseResources(resources),
6630
+ compatibility: 'unmanaged_sdd_scope',
6631
+ };
6632
+ }
6633
+ throw error;
6495
6634
  }
6496
- throw error;
6497
6635
  }
6498
6636
  const runtimeMode = getWorkspaceRuntimeModeFromConfig(
6499
6637
  readWorkspaceConfigText()
@@ -6799,6 +6937,7 @@ function appReleaseEnvironmentControl(flags = {}) {
6799
6937
  environmentId: readStringFlag(flags, 'environment-id') || undefined,
6800
6938
  candidateId: readStringFlag(flags, 'candidate-id') || undefined,
6801
6939
  deploymentId: readStringFlag(flags, 'deployment-id') || undefined,
6940
+ deliveryRunId: readStringFlag(flags, 'delivery-run-id') || undefined,
6802
6941
  });
6803
6942
  }
6804
6943
 
@@ -7528,6 +7667,30 @@ async function renewPublishLease(config, target, lease, flags = {}) {
7528
7667
  }
7529
7668
 
7530
7669
  async function resolvePublishLeaseForWrite(config, target, flags = {}) {
7670
+ if (
7671
+ activeDeliveryV2Context?.appType === target.appType &&
7672
+ activeDeliveryV2Context?.profileName === target.profileName
7673
+ ) {
7674
+ activePublishRequestContext = normalizePublishContext({
7675
+ appType: target.appType,
7676
+ profileName: target.profileName,
7677
+ publishLeaseId: activeDeliveryV2Context.publishLeaseId,
7678
+ changeBaselineId: activeDeliveryV2Context.baselineId,
7679
+ changeId: activeDeliveryV2Context.changeId,
7680
+ clientSessionId: activeDeliveryV2Context.clientSessionId,
7681
+ });
7682
+ return {
7683
+ leaseId: activeDeliveryV2Context.publishLeaseId,
7684
+ expiresAt: activeDeliveryV2Context.leaseExpiresAt,
7685
+ appType: target.appType,
7686
+ profile: target.profileName,
7687
+ changeId: activeDeliveryV2Context.changeId,
7688
+ clientSessionId: activeDeliveryV2Context.clientSessionId,
7689
+ baseRevision: activeDeliveryV2Context.packageDigest,
7690
+ holder: 'self',
7691
+ explicit: true,
7692
+ };
7693
+ }
7531
7694
  const explicitLeaseId = readStringFlag(flags, 'publish-lease-id');
7532
7695
  const changeId = readStringFlag(flags, 'change');
7533
7696
  const storedLease = getStoredPublishLease(target);
@@ -14807,7 +14970,9 @@ async function runtime(args) {
14807
14970
  // Preserve Runtime's dedicated dirty-source contract before the generic
14808
14971
  // frozen-release revision check runs inside lease resolution. The release
14809
14972
  // freeze and clean-HEAD gates still run immediately afterwards.
14810
- assertRuntimeSourceClean(process.cwd());
14973
+ if (!activeDeliveryV2Context) {
14974
+ assertRuntimeSourceClean(process.cwd());
14975
+ }
14811
14976
  // Keep build after lease acquisition. In direct-storage modes the build
14812
14977
  // embeds the provider-specific assetBaseUrl returned by the protected
14813
14978
  // upload plan; substituting the gateway URL would change chunk/publicPath
@@ -14819,13 +14984,26 @@ async function runtime(args) {
14819
14984
  const runtimeHead = await telemetry.runPhase('head', async () =>
14820
14985
  loadRuntimeHeadApp(config, target)
14821
14986
  );
14822
- const runtimeLineage = prepareRuntimeDeployLineage({
14823
- cwd: process.cwd(),
14824
- app: runtimeHead,
14825
- activate: !flags['no-activate'],
14826
- allowRollback: flags['allow-runtime-rollback'],
14827
- reason: readStringFlag(flags, 'reason'),
14828
- });
14987
+ const runtimeLineage = activeDeliveryV2Context
14988
+ ? {
14989
+ sourceRevision: activeDeliveryV2Context.sourceRevision,
14990
+ parentReleaseId: runtimeHead?.activeRuntimeReleaseId || null,
14991
+ parentSourceRevision:
14992
+ runtimeHead?.activeRuntimeSourceRevision || null,
14993
+ lineageBypassReason: null,
14994
+ lineageStatus: 'delivery-v2-package',
14995
+ legacyBootstrap: Boolean(
14996
+ runtimeHead?.activeRuntimeReleaseId &&
14997
+ !runtimeHead?.activeRuntimeSourceRevision
14998
+ ),
14999
+ }
15000
+ : prepareRuntimeDeployLineage({
15001
+ cwd: process.cwd(),
15002
+ app: runtimeHead,
15003
+ activate: !flags['no-activate'],
15004
+ allowRollback: flags['allow-runtime-rollback'],
15005
+ reason: readStringFlag(flags, 'reason'),
15006
+ });
14829
15007
  if (runtimeLineage.lineageStatus === 'rollback-bypass') {
14830
15008
  warn(
14831
15009
  `[RUNTIME_LINEAGE_BYPASS] 已允许审计式 Runtime 源码回退: reason=${runtimeLineage.lineageBypassReason}`
@@ -14904,13 +15082,15 @@ async function runtime(args) {
14904
15082
  jsonOutput: Boolean(flags.json),
14905
15083
  })
14906
15084
  );
14907
- const postBuildLineage = prepareRuntimeDeployLineage({
14908
- cwd: process.cwd(),
14909
- app: runtimeHead,
14910
- activate: !flags['no-activate'],
14911
- allowRollback: flags['allow-runtime-rollback'],
14912
- reason: readStringFlag(flags, 'reason'),
14913
- });
15085
+ const postBuildLineage = activeDeliveryV2Context
15086
+ ? runtimeLineage
15087
+ : prepareRuntimeDeployLineage({
15088
+ cwd: process.cwd(),
15089
+ app: runtimeHead,
15090
+ activate: !flags['no-activate'],
15091
+ allowRollback: flags['allow-runtime-rollback'],
15092
+ reason: readStringFlag(flags, 'reason'),
15093
+ });
14914
15094
  assertRuntimeSourceRevisionStable(
14915
15095
  runtimeLineage.sourceRevision,
14916
15096
  postBuildLineage.sourceRevision
@@ -21427,6 +21607,14 @@ async function runResourceChangeBaselinePreflight(
21427
21607
  flags = {},
21428
21608
  publishLease = null
21429
21609
  ) {
21610
+ if (activeDeliveryV2Context) {
21611
+ return {
21612
+ ok: true,
21613
+ baselineId: activeDeliveryV2Context.baselineId,
21614
+ checkedTargets: [],
21615
+ skipped: 'trusted-delivery-v2-package',
21616
+ };
21617
+ }
21430
21618
  const onlineBaselineAdoption = resolveOnlineBaselineAdoption(
21431
21619
  flags,
21432
21620
  planActions,
@@ -28690,6 +28878,7 @@ function buildExplicitSddReleasePlan(targets, options = {}) {
28690
28878
  }
28691
28879
 
28692
28880
  function assertSddReleaseGate(plan, flags, command) {
28881
+ if (activeDeliveryV2Context) return;
28693
28882
  if (!plan?.sdd || plan.sdd.errors.length === 0) return;
28694
28883
  const nonBypassable = new Set([
28695
28884
  'sdd-release-direct-form-schema-forbidden',
@@ -28980,7 +29169,8 @@ function isPublishMutationRequest(apiPath, options = {}) {
28980
29169
  if (
28981
29170
  normalizedPath.includes('/publish-lease/') ||
28982
29171
  normalizedPath.endsWith('/publish-lease/acquire') ||
28983
- normalizedPath.includes('/change-baselines')
29172
+ normalizedPath.includes('/change-baselines') ||
29173
+ normalizedPath.includes('/delivery/')
28984
29174
  ) {
28985
29175
  return false;
28986
29176
  }
@@ -0,0 +1,216 @@
1
+ const crypto = require('crypto');
2
+ const { createDeliveryV2Executor } = require('./delivery-v2-executor');
3
+ const { parseArgs, print, warn, writeJson } = require('./utils');
4
+
5
+ function createDeliveryV2Cli(dependencies) {
6
+ const deps = assertDependencies(dependencies);
7
+ const executor = createDeliveryV2Executor(deps);
8
+
9
+ async function run(command, argv = []) {
10
+ const { flags, positional } = parseArgs(argv);
11
+ if (flags.help || flags.h) {
12
+ printHelp(command);
13
+ return;
14
+ }
15
+ const config = deps.loadConfig();
16
+ if (command === 'check') {
17
+ const target = resolveTarget(config, flags, positional, false);
18
+ const inspected = await executor.inspect({
19
+ config,
20
+ target,
21
+ workspaceRoot: process.cwd(),
22
+ skipBuild: flags.build !== true,
23
+ buildCommand: stringFlag(flags, 'build-command'),
24
+ distDir: stringFlag(flags, 'dist'),
25
+ onBuildOutput: buildOutput(flags),
26
+ });
27
+ return output(
28
+ flags,
29
+ {
30
+ ok: true,
31
+ appType: target.appType,
32
+ environment: target.targetName || target.environmentKind || null,
33
+ packageDigest: inspected.compiled.packageDigest,
34
+ summary: inspected.compiled.summary,
35
+ targets: inspected.targets,
36
+ previousPackageDigest: inspected.previous?.packageDigest || null,
37
+ },
38
+ `Delivery V2 检查通过: ${inspected.compiled.packageDigest}`
39
+ );
40
+ }
41
+ if (command === 'deploy') {
42
+ const target = resolveTarget(config, flags, positional, true);
43
+ const packageDigest = stringFlag(flags, 'package');
44
+ const result = await executor.deploy({
45
+ config,
46
+ target,
47
+ workspaceRoot: process.cwd(),
48
+ packageDigest,
49
+ buildCommand: stringFlag(flags, 'build-command'),
50
+ distDir: stringFlag(flags, 'dist'),
51
+ clientSessionId: createClientSessionId(),
52
+ onBuildOutput: buildOutput(flags),
53
+ });
54
+ return output(
55
+ flags,
56
+ result,
57
+ `Delivery V2 发布成功: run=${result.run.id} package=${result.packageDigest}`
58
+ );
59
+ }
60
+ if (command === 'status') {
61
+ const runId = positional[0] || stringFlag(flags, 'run');
62
+ if (!runId) {
63
+ throw usageError('用法: openxiangda status <runId> [--json]');
64
+ }
65
+ const target = resolveTarget(config, flags, [], false);
66
+ const result = await executor.status({ config, target, runId });
67
+ return output(
68
+ flags,
69
+ result,
70
+ `Delivery V2: run=${result.id} status=${result.status} stage=${result.stage}`
71
+ );
72
+ }
73
+ if (command === 'retry') {
74
+ const runId = positional[0] || stringFlag(flags, 'run');
75
+ if (!runId) {
76
+ throw usageError('用法: openxiangda retry <runId> [--json]');
77
+ }
78
+ const target = resolveTarget(config, flags, [], false);
79
+ const result = await executor.retry({
80
+ config,
81
+ target,
82
+ runId,
83
+ workspaceRoot: process.cwd(),
84
+ });
85
+ return output(
86
+ flags,
87
+ result,
88
+ `Delivery V2 重试成功: run=${result.run.id} status=${result.run.status}`
89
+ );
90
+ }
91
+ if (command === 'rollback') {
92
+ const target = resolveTarget(config, flags, positional, true);
93
+ const releaseId = stringFlag(flags, 'to');
94
+ if (!releaseId) {
95
+ throw usageError(
96
+ '用法: openxiangda rollback <environment> --to <appReleaseId>'
97
+ );
98
+ }
99
+ const result = await executor.rollback({
100
+ config,
101
+ target,
102
+ releaseId,
103
+ workspaceRoot: process.cwd(),
104
+ clientSessionId: createClientSessionId(),
105
+ });
106
+ return output(
107
+ flags,
108
+ result,
109
+ `Delivery V2 回滚成功: run=${result.run.id} package=${result.packageDigest}`
110
+ );
111
+ }
112
+ throw usageError(`未知 Delivery V2 命令: ${command}`);
113
+ }
114
+
115
+ function resolveTarget(config, flags, positional, environmentRequired) {
116
+ const environment =
117
+ positional[0] ||
118
+ stringFlag(flags, 'environment') ||
119
+ stringFlag(flags, 'target');
120
+ if (environmentRequired && !environment) {
121
+ throw usageError(
122
+ '必须指定环境,例如 openxiangda deploy preproduction'
123
+ );
124
+ }
125
+ return deps.getWorkspaceTarget(
126
+ config,
127
+ stringFlag(flags, 'profile') || config.currentProfile,
128
+ {
129
+ ...flags,
130
+ ...(environment ? { environment } : {}),
131
+ }
132
+ );
133
+ }
134
+
135
+ return { run };
136
+ }
137
+
138
+ function output(flags, value, message) {
139
+ if (flags.json) return writeJson(value);
140
+ print(message);
141
+ if (value?.run?.failure?.code) {
142
+ warn(
143
+ `${value.run.failure.code}: ${value.run.failure.message || '发布失败'}`
144
+ );
145
+ }
146
+ return value;
147
+ }
148
+
149
+ function buildOutput(flags) {
150
+ if (flags.json || flags.quiet) return undefined;
151
+ return (stream, text) => {
152
+ if (stream === 'stderr') warn(text.trimEnd());
153
+ else print(text.trimEnd());
154
+ };
155
+ }
156
+
157
+ function stringFlag(flags, name) {
158
+ const value = flags?.[name];
159
+ if (value === undefined || value === null || value === false) return '';
160
+ return String(value).trim();
161
+ }
162
+
163
+ function createClientSessionId() {
164
+ return `delivery-v2-${process.pid}-${crypto
165
+ .randomBytes(10)
166
+ .toString('hex')}`;
167
+ }
168
+
169
+ function usageError(message) {
170
+ const error = new Error(message);
171
+ error.code = 'DELIVERY_USAGE_ERROR';
172
+ error.retryable = false;
173
+ return error;
174
+ }
175
+
176
+ function printHelp(command) {
177
+ const lines = {
178
+ check: [
179
+ '用法: openxiangda check [--environment <name>] [--build] [--json]',
180
+ '编译并校验 App Package V2;默认复用现有 dist,不执行 Runtime 构建。',
181
+ ],
182
+ deploy: [
183
+ '用法: openxiangda deploy <environment> [--package <sha256>] [--json]',
184
+ '不带 --package 时从当前工作区构建;带 --package 时直接部署已封存制品。',
185
+ ],
186
+ status: ['用法: openxiangda status <runId> [--json]'],
187
+ retry: ['用法: openxiangda retry <runId> [--json]'],
188
+ rollback: [
189
+ '用法: openxiangda rollback <environment> --to <appReleaseId> [--json]',
190
+ ],
191
+ };
192
+ print((lines[command] || ['Delivery V2']).join('\n'));
193
+ }
194
+
195
+ function assertDependencies(value) {
196
+ const required = [
197
+ 'clearDeliveryContext',
198
+ 'downloadWithAuth',
199
+ 'getWorkspaceTarget',
200
+ 'installDeliveryContext',
201
+ 'loadConfig',
202
+ 'requestFormWithAuth',
203
+ 'requestWithAuth',
204
+ 'runOpenXiangdaInProcess',
205
+ ];
206
+ for (const key of required) {
207
+ if (typeof value?.[key] !== 'function') {
208
+ throw new Error(`Delivery V2 CLI dependency 缺失: ${key}`);
209
+ }
210
+ }
211
+ return value;
212
+ }
213
+
214
+ module.exports = {
215
+ createDeliveryV2Cli,
216
+ };