openxiangda 1.0.134 → 1.0.136
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 +6 -0
- package/lib/cli.js +947 -16
- package/lib/design-gates.js +40 -10
- package/lib/sdd.js +777 -0
- package/lib/workspace-init.js +3 -0
- package/openxiangda-skills/SKILL.md +11 -1
- package/openxiangda-skills/references/best-practices.md +32 -5
- package/openxiangda-skills/references/openxiangda-api.md +32 -6
- package/openxiangda-skills/references/pages/page-sdk.md +3 -0
- package/openxiangda-skills/references/pages/publish-flow.md +16 -0
- package/openxiangda-skills/references/permission-design-patterns.md +216 -0
- package/openxiangda-skills/references/permissions-settings.md +58 -0
- package/openxiangda-skills/references/platform-data-model.md +19 -4
- package/openxiangda-skills/references/resource-manifest-cheatsheet.md +32 -1
- package/openxiangda-skills/skills/openxiangda-architecture-design/SKILL.md +8 -0
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +2 -0
- package/openxiangda-skills/skills/openxiangda-permission-settings/SKILL.md +38 -1
- package/package.json +2 -1
- package/packages/sdk/dist/{ProcessPreview-Dfc4-wIq.d.mts → ProcessPreview-Ci8_UsbN.d.mts} +4 -0
- package/packages/sdk/dist/{ProcessPreview-Dfc4-wIq.d.ts → ProcessPreview-Ci8_UsbN.d.ts} +4 -0
- package/packages/sdk/dist/build/index.cjs.map +1 -1
- package/packages/sdk/dist/build/index.d.mts +7 -0
- package/packages/sdk/dist/build/index.d.ts +7 -0
- package/packages/sdk/dist/build/index.mjs.map +1 -1
- package/packages/sdk/dist/components/index.cjs +32 -4
- package/packages/sdk/dist/components/index.cjs.map +1 -1
- package/packages/sdk/dist/components/index.d.mts +8 -2
- package/packages/sdk/dist/components/index.d.ts +8 -2
- package/packages/sdk/dist/components/index.mjs +32 -4
- package/packages/sdk/dist/components/index.mjs.map +1 -1
- package/packages/sdk/dist/runtime/index.cjs +181 -28
- package/packages/sdk/dist/runtime/index.cjs.map +1 -1
- package/packages/sdk/dist/runtime/index.d.mts +2 -2
- package/packages/sdk/dist/runtime/index.d.ts +2 -2
- package/packages/sdk/dist/runtime/index.mjs +181 -28
- package/packages/sdk/dist/runtime/index.mjs.map +1 -1
- package/packages/sdk/dist/runtime/react.cjs +152 -25
- package/packages/sdk/dist/runtime/react.cjs.map +1 -1
- package/packages/sdk/dist/runtime/react.d.mts +57 -4
- package/packages/sdk/dist/runtime/react.d.ts +57 -4
- package/packages/sdk/dist/runtime/react.mjs +152 -25
- package/packages/sdk/dist/runtime/react.mjs.map +1 -1
- package/templates/openxiangda-react-spa/.cursor/rules/openxiangda.mdc +8 -1
- package/templates/openxiangda-react-spa/.qoder/rules/openxiangda.md +8 -1
- package/templates/openxiangda-react-spa/AGENTS.md +9 -1
- package/templates/openxiangda-react-spa/app-workspace.config.ts +14 -0
- package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda-resources.mdc +5 -1
- package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda.mdc +7 -1
- package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda-resources.md +6 -0
- package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda.md +7 -1
- package/templates/sy-lowcode-app-workspace/AGENTS.md +10 -1
- package/templates/sy-lowcode-app-workspace/app-workspace.config.ts +14 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/README.md +3 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/access-governance.md +239 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/catalog.json +6 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/decision-guide.md +25 -1
- package/templates/sy-lowcode-app-workspace/src/types/app-workspace.types.ts +12 -0
package/lib/cli.js
CHANGED
|
@@ -37,6 +37,17 @@ const {
|
|
|
37
37
|
renderResourceExplain,
|
|
38
38
|
} = require('./design-gates');
|
|
39
39
|
const { buildDesignReview, renderDesignReview } = require('./design-review');
|
|
40
|
+
const {
|
|
41
|
+
appendBypassLog,
|
|
42
|
+
archiveSddChange,
|
|
43
|
+
approveSddChange,
|
|
44
|
+
buildSddGovernanceReport,
|
|
45
|
+
buildSddStatus,
|
|
46
|
+
initSddWorkspace,
|
|
47
|
+
proposeSddChange,
|
|
48
|
+
syncSddChange,
|
|
49
|
+
verifySddChange,
|
|
50
|
+
} = require('./sdd');
|
|
40
51
|
const {
|
|
41
52
|
fail,
|
|
42
53
|
formatFetchError,
|
|
@@ -71,7 +82,9 @@ async function main(argv) {
|
|
|
71
82
|
if (command === 'auth') return auth(rest);
|
|
72
83
|
if (command === 'platform') return platform(rest);
|
|
73
84
|
if (command === 'doctor') return doctor(rest);
|
|
85
|
+
if (command === 'contract') return contract(rest);
|
|
74
86
|
if (command === 'design') return design(rest);
|
|
87
|
+
if (command === 'sdd') return sdd(rest);
|
|
75
88
|
if (command === 'workspace') return workspace(rest);
|
|
76
89
|
if (command === 'app') return app(rest);
|
|
77
90
|
if (command === 'form') return form(rest);
|
|
@@ -110,13 +123,16 @@ Usage:
|
|
|
110
123
|
openxiangda platform list
|
|
111
124
|
openxiangda platform use <name>
|
|
112
125
|
openxiangda auth status|refresh|logout [--profile name]
|
|
113
|
-
openxiangda doctor [--profile name] [--app-type APP_XXX] [--json]
|
|
126
|
+
openxiangda doctor [--profile name] [--app-type APP_XXX] [--release] [--json]
|
|
127
|
+
openxiangda contract list|explain <contract-name> [--json]
|
|
114
128
|
openxiangda design gates|template|review [--topic code] [--json]
|
|
129
|
+
openxiangda sdd init|propose|approve|status|context|verify|sync|archive [change] [--json]
|
|
115
130
|
openxiangda env [--profile name]
|
|
116
131
|
openxiangda workspace init [dir] [--name package-name] [--runtime legacy|react-spa] [--install] [--profile name --app-type APP_XXX]
|
|
117
132
|
openxiangda workspace init [dir] --profile <name> --app-name <app-name> [--runtime legacy|react-spa] [--install]
|
|
118
133
|
openxiangda workspace bind --profile <name> --app-type <APP_XXX>
|
|
119
|
-
openxiangda workspace
|
|
134
|
+
openxiangda workspace plan|check [--changed] [--json]
|
|
135
|
+
openxiangda workspace publish --profile <name> [--changed [--since ref]|--form code|--page code|--only list] [--dry-run] [--force] [--resources|--skip-resources] [--prune] [--sdd-bypass --reason text]
|
|
120
136
|
openxiangda app list [--profile name] [--json]
|
|
121
137
|
openxiangda app create <name> [--profile name] [--runtime legacy|react-spa] [--description text]
|
|
122
138
|
openxiangda app snapshot <APP_XXX> [--profile name] [--json]
|
|
@@ -125,6 +141,7 @@ Usage:
|
|
|
125
141
|
openxiangda form bind <formCode> --form-uuid <FORM_XXX>
|
|
126
142
|
openxiangda form pull <formCode|formUuid> [--json]
|
|
127
143
|
openxiangda form publish <formCode|formUuid> --bundle-url <url> # repair only
|
|
144
|
+
openxiangda form schema-plan <formCode|formUuid> --schema-json <file>|--components-json <file>
|
|
128
145
|
openxiangda page list [--profile name] [--json]
|
|
129
146
|
openxiangda page publish <pageCode> --entry-url <url> --version <v> --build-id <id> # repair only
|
|
130
147
|
openxiangda page bind <pageCode> --page-id <id>
|
|
@@ -156,8 +173,8 @@ Usage:
|
|
|
156
173
|
openxiangda permission page-group-list|page-group-create|page-group-update|page-group-delete|page-group-bind
|
|
157
174
|
openxiangda permission form-group-list|form-group-create|form-group-update|form-group-delete|form-group-bind
|
|
158
175
|
openxiangda settings get|save|indexes|indexes-save|data-management|data-management-save|public-access
|
|
159
|
-
openxiangda resource validate|plan|publish|pull|typegen|explain [--profile name] [--json] [--dry-run]
|
|
160
|
-
openxiangda runtime deploy [--profile name] [--dist dist] [--build-id id] [--upload-mode auto|staged|oss-direct|legacy-json] [--upload-timeout-ms ms] [--no-build] [--no-activate] [--json]
|
|
176
|
+
openxiangda resource validate|plan|publish|pull|typegen|explain [--profile name] [--json] [--dry-run] [--sdd-bypass --reason text]
|
|
177
|
+
openxiangda runtime deploy [--profile name] [--dist dist] [--build-id id] [--upload-mode auto|staged|oss-direct|legacy-json] [--upload-timeout-ms ms] [--no-build] [--no-activate] [--json] [--sdd-bypass --reason text]
|
|
161
178
|
openxiangda runtime releases [--profile name] [--json]
|
|
162
179
|
openxiangda runtime activate <releaseId> [--profile name] [--json]
|
|
163
180
|
openxiangda inspect app|form|workflow|automation|permissions
|
|
@@ -188,9 +205,11 @@ const SUBCOMMAND_BOOLEAN_FLAGS = new Set([
|
|
|
188
205
|
'--publish',
|
|
189
206
|
'--published',
|
|
190
207
|
'--redact',
|
|
208
|
+
'--release',
|
|
191
209
|
'--resources',
|
|
192
210
|
'--skip-resources',
|
|
193
211
|
'--strict',
|
|
212
|
+
'--sdd-bypass',
|
|
194
213
|
'--summary',
|
|
195
214
|
'--unpublish',
|
|
196
215
|
'--write-manifest',
|
|
@@ -505,10 +524,213 @@ async function design(args) {
|
|
|
505
524
|
fail('用法: openxiangda design gates|template|review [--topic code] [--json]');
|
|
506
525
|
}
|
|
507
526
|
|
|
527
|
+
async function sdd(args) {
|
|
528
|
+
const { subcommand, rest } = parseSubcommandArgs(args);
|
|
529
|
+
const { flags, positional } = parseArgs(rest);
|
|
530
|
+
if (wantsSubcommandHelp(subcommand, flags)) {
|
|
531
|
+
print([
|
|
532
|
+
'用法: openxiangda sdd init|propose|approve|status|context|verify|sync|archive [change] [--json]',
|
|
533
|
+
'常用流程:',
|
|
534
|
+
' openxiangda sdd init',
|
|
535
|
+
' openxiangda sdd propose add-customer-list --title "客户列表" --forms customer --pages customer_list',
|
|
536
|
+
' openxiangda sdd approve add-customer-list --summary "用户确认设计"',
|
|
537
|
+
' openxiangda sdd context --json',
|
|
538
|
+
' openxiangda sdd verify add-customer-list --changed --json',
|
|
539
|
+
' openxiangda sdd archive add-customer-list',
|
|
540
|
+
'说明:',
|
|
541
|
+
' - SDD 记录保存在 openspec/,不依赖官方 OpenSpec CLI。',
|
|
542
|
+
' - 高风险发布需要 approved change 覆盖本次 forms/pages/resources/runtime 目标。',
|
|
543
|
+
].join('\n'));
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (subcommand === 'init') {
|
|
548
|
+
const result = initSddWorkspace({
|
|
549
|
+
cwd: process.cwd(),
|
|
550
|
+
path: flags.path,
|
|
551
|
+
force: Boolean(flags.force),
|
|
552
|
+
});
|
|
553
|
+
if (flags.json) return writeJson(result);
|
|
554
|
+
printSddInitReport(result);
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (subcommand === 'propose' || subcommand === 'new') {
|
|
559
|
+
const changeId = positional[0] || flags.change;
|
|
560
|
+
const result = proposeSddChange({
|
|
561
|
+
cwd: process.cwd(),
|
|
562
|
+
changeId,
|
|
563
|
+
title: flags.title || flags.name,
|
|
564
|
+
riskLevel: flags.risk || flags['risk-level'],
|
|
565
|
+
domain: flags.domain,
|
|
566
|
+
force: Boolean(flags.force),
|
|
567
|
+
affected: parseSddAffectedFlags(flags),
|
|
568
|
+
});
|
|
569
|
+
if (flags.json) return writeJson(result);
|
|
570
|
+
print(`已创建 SDD change: ${result.change.id}\n目录: ${result.dir}`);
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (subcommand === 'approve') {
|
|
575
|
+
const changeId = positional[0] || flags.change;
|
|
576
|
+
const result = approveSddChange({
|
|
577
|
+
cwd: process.cwd(),
|
|
578
|
+
changeId,
|
|
579
|
+
summary: flags.summary || flags.note,
|
|
580
|
+
});
|
|
581
|
+
if (flags.json) return writeJson(result);
|
|
582
|
+
print(`SDD change 已确认: ${result.change.id} approvedAt=${result.change.approvedAt}`);
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (subcommand === 'status' || subcommand === 'context') {
|
|
587
|
+
const status = buildSddStatus({ cwd: process.cwd(), configText: readWorkspaceConfigText() });
|
|
588
|
+
const releasePlan = buildWorkspaceReleasePlan({
|
|
589
|
+
changed: Boolean(flags.changed || flags['changed-only']),
|
|
590
|
+
since: flags.since,
|
|
591
|
+
profile: flags.profile || '<profile>',
|
|
592
|
+
skipSdd: true,
|
|
593
|
+
});
|
|
594
|
+
const result = {
|
|
595
|
+
...status,
|
|
596
|
+
releasePlan,
|
|
597
|
+
contracts:
|
|
598
|
+
subcommand === 'context'
|
|
599
|
+
? Object.fromEntries(
|
|
600
|
+
Object.entries(API_CONTRACTS).map(([name, contract]) => [
|
|
601
|
+
name,
|
|
602
|
+
{
|
|
603
|
+
mode: contract.mode,
|
|
604
|
+
endpoint: contract.endpoint,
|
|
605
|
+
response: contract.response,
|
|
606
|
+
guarantees: contract.guarantees,
|
|
607
|
+
},
|
|
608
|
+
])
|
|
609
|
+
)
|
|
610
|
+
: undefined,
|
|
611
|
+
};
|
|
612
|
+
if (flags.json || subcommand === 'context') return writeJson(result);
|
|
613
|
+
printSddStatusReport(result);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
if (subcommand === 'verify') {
|
|
618
|
+
const changeId = positional[0] || flags.change;
|
|
619
|
+
const releasePlan = buildWorkspaceReleasePlan({
|
|
620
|
+
changed: flags.changed !== false,
|
|
621
|
+
since: flags.since,
|
|
622
|
+
profile: flags.profile || '<profile>',
|
|
623
|
+
skipSdd: true,
|
|
624
|
+
});
|
|
625
|
+
const result = verifySddChange({
|
|
626
|
+
cwd: process.cwd(),
|
|
627
|
+
changeId,
|
|
628
|
+
releasePlan,
|
|
629
|
+
writeMetadata: true,
|
|
630
|
+
});
|
|
631
|
+
if (flags.json) {
|
|
632
|
+
writeJson(result);
|
|
633
|
+
if (!result.passed) process.exitCode = 1;
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
printSddVerifyReport(result);
|
|
637
|
+
if (!result.passed) fail(`SDD verify 失败: ${result.errors[0].message}`);
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
if (subcommand === 'sync') {
|
|
642
|
+
const changeId = positional[0] || flags.change;
|
|
643
|
+
const result = syncSddChange({ cwd: process.cwd(), changeId });
|
|
644
|
+
if (flags.json) return writeJson(result);
|
|
645
|
+
print(`SDD specs 已同步: ${result.change.id}\n${result.synced.map(item => `- ${item}`).join('\n')}`);
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (subcommand === 'archive') {
|
|
650
|
+
const changeId = positional[0] || flags.change;
|
|
651
|
+
const releasePlan = buildWorkspaceReleasePlan({
|
|
652
|
+
changed: flags.changed !== false,
|
|
653
|
+
since: flags.since,
|
|
654
|
+
profile: flags.profile || '<profile>',
|
|
655
|
+
skipSdd: true,
|
|
656
|
+
});
|
|
657
|
+
const result = archiveSddChange({
|
|
658
|
+
cwd: process.cwd(),
|
|
659
|
+
changeId,
|
|
660
|
+
releasePlan,
|
|
661
|
+
force: Boolean(flags.force),
|
|
662
|
+
});
|
|
663
|
+
if (flags.json) return writeJson(result);
|
|
664
|
+
print(`SDD change 已归档: ${result.change.id}\n目录: ${result.archiveDir}`);
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
fail('用法: openxiangda sdd init|propose|approve|status|context|verify|sync|archive [change] [--json]');
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function parseSddAffectedFlags(flags) {
|
|
672
|
+
const split = value =>
|
|
673
|
+
value === undefined || value === null || value === false
|
|
674
|
+
? []
|
|
675
|
+
: String(value)
|
|
676
|
+
.split(',')
|
|
677
|
+
.map(item => item.trim())
|
|
678
|
+
.filter(Boolean);
|
|
679
|
+
return {
|
|
680
|
+
forms: split(flags.forms || flags.form),
|
|
681
|
+
pages: split(flags.pages || flags.page),
|
|
682
|
+
resources: Boolean(flags.resources),
|
|
683
|
+
runtime: Boolean(flags.runtime),
|
|
684
|
+
backend: Boolean(flags.backend),
|
|
685
|
+
contracts: Boolean(flags.contracts),
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function printSddInitReport(result) {
|
|
690
|
+
const lines = [`OpenXiangda SDD initialized: ${result.path}`];
|
|
691
|
+
for (const op of result.operations) {
|
|
692
|
+
lines.push(`- ${op.action}: ${op.path}`);
|
|
693
|
+
}
|
|
694
|
+
print(lines.join('\n'));
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function printSddStatusReport(result) {
|
|
698
|
+
const lines = [
|
|
699
|
+
`OpenXiangda SDD status: ${result.governance.initialized ? 'initialized' : 'not initialized'}`,
|
|
700
|
+
`path: ${result.governance.path}`,
|
|
701
|
+
`strictHighRisk: ${result.governance.strictHighRisk ? 'true' : 'false'}`,
|
|
702
|
+
`active changes: ${result.activeChanges.length}`,
|
|
703
|
+
];
|
|
704
|
+
for (const change of result.activeChanges) {
|
|
705
|
+
lines.push(`- ${change.id}: ${change.status}${change.approvedAt ? ` approvedAt=${change.approvedAt}` : ''}`);
|
|
706
|
+
}
|
|
707
|
+
if (result.bypassLog) lines.push(`bypass log: ${result.bypassLog}`);
|
|
708
|
+
print(lines.join('\n'));
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function printSddVerifyReport(result) {
|
|
712
|
+
const lines = [
|
|
713
|
+
`OpenXiangda SDD verify: ${result.passed ? 'passed' : 'failed'}`,
|
|
714
|
+
`change: ${result.change.id}`,
|
|
715
|
+
];
|
|
716
|
+
if (result.errors.length > 0) {
|
|
717
|
+
lines.push('Errors:');
|
|
718
|
+
for (const error of result.errors) {
|
|
719
|
+
lines.push(`- ${error.name}: ${error.message}`);
|
|
720
|
+
for (const detail of error.details || []) lines.push(` ${detail}`);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
if (result.warnings.length > 0) {
|
|
724
|
+
lines.push('Warnings:');
|
|
725
|
+
for (const warning of result.warnings) lines.push(`- ${warning.name}: ${warning.message}`);
|
|
726
|
+
}
|
|
727
|
+
print(lines.join('\n'));
|
|
728
|
+
}
|
|
729
|
+
|
|
508
730
|
async function doctor(args) {
|
|
509
731
|
const { flags } = parseArgs(args);
|
|
510
732
|
if (flags.help || flags.h) {
|
|
511
|
-
print('用法: openxiangda doctor [--profile name] [--app-type APP_XXX] [--agent codex] [--json]');
|
|
733
|
+
print('用法: openxiangda doctor [--profile name] [--app-type APP_XXX] [--agent codex] [--release] [--json]');
|
|
512
734
|
return;
|
|
513
735
|
}
|
|
514
736
|
const config = loadConfig();
|
|
@@ -537,6 +759,8 @@ async function doctor(args) {
|
|
|
537
759
|
hasDirectory: fs.existsSync(path.join(process.cwd(), 'src', 'resources')),
|
|
538
760
|
validation: null,
|
|
539
761
|
},
|
|
762
|
+
sdd: null,
|
|
763
|
+
release: null,
|
|
540
764
|
skills: null,
|
|
541
765
|
checks: [],
|
|
542
766
|
};
|
|
@@ -603,6 +827,23 @@ async function doctor(args) {
|
|
|
603
827
|
message: outdatedSkills.length > 0 ? `${outdatedSkills.length} 个 skill 未安装或过期` : undefined,
|
|
604
828
|
});
|
|
605
829
|
|
|
830
|
+
if (flags.release) {
|
|
831
|
+
result.release = buildWorkspaceReleasePlan({
|
|
832
|
+
changed: true,
|
|
833
|
+
profile: profileName || config.currentProfile || '<profile>',
|
|
834
|
+
check: true,
|
|
835
|
+
});
|
|
836
|
+
result.sdd = result.release.sdd || buildSddStatus({ cwd: process.cwd(), configText: readWorkspaceConfigText() });
|
|
837
|
+
result.checks.push({
|
|
838
|
+
name: 'workspace-release',
|
|
839
|
+
status: result.release.errors.length > 0 ? 'error' : 'ok',
|
|
840
|
+
message:
|
|
841
|
+
result.release.errors.length > 0
|
|
842
|
+
? `${result.release.errors.length} 个发布前错误`
|
|
843
|
+
: undefined,
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
|
|
606
847
|
if (flags.json) return writeJson(result);
|
|
607
848
|
printDoctorReport(result);
|
|
608
849
|
}
|
|
@@ -638,6 +879,17 @@ function printDoctorReport(result) {
|
|
|
638
879
|
`resource validation: ${result.resources.validation.errors.length} errors, ${result.resources.validation.warnings.length} warnings`
|
|
639
880
|
);
|
|
640
881
|
}
|
|
882
|
+
if (result.release) {
|
|
883
|
+
lines.push(
|
|
884
|
+
`release plan: forms=${result.release.targets.forms.length}, pages=${result.release.targets.pages.length}, resources=${result.release.targets.resources ? 'yes' : 'no'}, runtime=${result.release.targets.runtime ? 'yes' : 'no'}`
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
if (result.sdd) {
|
|
888
|
+
const governance = result.sdd.governance || result.sdd.governanceConfig || {};
|
|
889
|
+
lines.push(
|
|
890
|
+
`sdd: ${governance.initialized ? 'initialized' : 'not initialized'} path=${governance.path || 'openspec'} strict=${governance.strictHighRisk ? 'true' : 'false'}`
|
|
891
|
+
);
|
|
892
|
+
}
|
|
641
893
|
for (const check of result.checks) {
|
|
642
894
|
const suffix = check.message ? ` - ${check.message}` : '';
|
|
643
895
|
lines.push(`- ${check.name}: ${check.status}${suffix}`);
|
|
@@ -646,6 +898,131 @@ function printDoctorReport(result) {
|
|
|
646
898
|
print(lines.join('\n'));
|
|
647
899
|
}
|
|
648
900
|
|
|
901
|
+
const API_CONTRACTS = {
|
|
902
|
+
'sdk.form.create': {
|
|
903
|
+
name: 'sdk.form.create',
|
|
904
|
+
mode: 'strict-by-default',
|
|
905
|
+
endpoint: '/:appType/v1/form/saveFormData.json',
|
|
906
|
+
method: 'POST',
|
|
907
|
+
request: {
|
|
908
|
+
formUuid: 'string',
|
|
909
|
+
formDataJson: 'JSON.stringify(data)',
|
|
910
|
+
saveAsDraft: 'boolean | undefined',
|
|
911
|
+
startProcess: 'boolean | undefined',
|
|
912
|
+
processStartMode: 'auto | manual | draft | delayed | none | string | undefined',
|
|
913
|
+
},
|
|
914
|
+
response: {
|
|
915
|
+
code: '200 | 0 | success code',
|
|
916
|
+
result: 'normalized FormCreateResult object containing formInstId/formInstanceId',
|
|
917
|
+
data: 'legacy payload containing formInstId/formInstanceId',
|
|
918
|
+
formInstId: 'string',
|
|
919
|
+
formInstanceId: 'string',
|
|
920
|
+
processInstanceId: 'string | undefined',
|
|
921
|
+
serialNumber: 'string | undefined, first generated SerialNumberField value when present',
|
|
922
|
+
serialNumbers: 'Record<fieldId, string> | undefined',
|
|
923
|
+
},
|
|
924
|
+
guarantees: [
|
|
925
|
+
'保存接口只承诺返回实例标识、流程标识,以及保存时已生成的流水号字段。',
|
|
926
|
+
'保存接口不返回完整业务数据、公式回填或其他服务端格式化后的行数据。',
|
|
927
|
+
'缺少 formInstId/formInstanceId 时 strict 模式报错。',
|
|
928
|
+
],
|
|
929
|
+
followUp: '需要完整行数据、公式回填或非流水号服务端字段时,保存成功后调用 sdk.form.getDetail({ formUuid, formInstId })。',
|
|
930
|
+
forbidden: [
|
|
931
|
+
'不要从 response.data/response.result 猜完整行数据。',
|
|
932
|
+
'不要生成 inst_${Date.now()} 这类假实例 ID。',
|
|
933
|
+
'不要默认 lookupAfterCreate;仅旧项目显式启用。',
|
|
934
|
+
],
|
|
935
|
+
},
|
|
936
|
+
'sdk.form.update': {
|
|
937
|
+
name: 'sdk.form.update',
|
|
938
|
+
endpoint: '/:appType/v1/form/updateFormData.json',
|
|
939
|
+
method: 'POST',
|
|
940
|
+
response: {
|
|
941
|
+
code: '200 | 0 | success code',
|
|
942
|
+
result: 'formInstId string',
|
|
943
|
+
formInstId: 'string',
|
|
944
|
+
formInstanceId: 'string',
|
|
945
|
+
},
|
|
946
|
+
guarantees: [
|
|
947
|
+
'更新接口只承诺返回被更新的实例标识。',
|
|
948
|
+
'更新接口不返回完整业务数据。',
|
|
949
|
+
],
|
|
950
|
+
},
|
|
951
|
+
'sdk.form.getDetail': {
|
|
952
|
+
name: 'sdk.form.getDetail',
|
|
953
|
+
endpoint: '/:appType/v1/form/getFormDataById.json',
|
|
954
|
+
method: 'GET',
|
|
955
|
+
response: {
|
|
956
|
+
result: '完整表单实例详情,包含平台可返回的业务字段和系统字段',
|
|
957
|
+
},
|
|
958
|
+
guarantees: [
|
|
959
|
+
'需要读取保存后的流水号、服务端生成字段或完整行数据时使用该接口。',
|
|
960
|
+
],
|
|
961
|
+
},
|
|
962
|
+
'sdk.form.advancedSearch': {
|
|
963
|
+
name: 'sdk.form.advancedSearch',
|
|
964
|
+
endpoint: '/:appType/v1/form/advancedSearch.json',
|
|
965
|
+
method: 'GET',
|
|
966
|
+
response: {
|
|
967
|
+
currentPage: 'number',
|
|
968
|
+
data: 'array',
|
|
969
|
+
totalCount: 'number',
|
|
970
|
+
},
|
|
971
|
+
guarantees: [
|
|
972
|
+
'列表页必须使用分页查询和服务端过滤。',
|
|
973
|
+
'不要拉取大页数据后在本地过滤。',
|
|
974
|
+
],
|
|
975
|
+
},
|
|
976
|
+
};
|
|
977
|
+
|
|
978
|
+
async function contract(args) {
|
|
979
|
+
const [subcommand, ...rest] = args;
|
|
980
|
+
const { flags, positional } = parseArgs(rest);
|
|
981
|
+
if (wantsSubcommandHelp(subcommand, flags)) {
|
|
982
|
+
print('用法: openxiangda contract list|explain <contract-name> [--json]');
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
if (subcommand === 'list') {
|
|
987
|
+
const result = Object.keys(API_CONTRACTS).sort();
|
|
988
|
+
if (flags.json) return writeJson({ contracts: result });
|
|
989
|
+
print(result.join('\n'));
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
if (subcommand === 'explain') {
|
|
994
|
+
const name = positional[0];
|
|
995
|
+
if (!name) {
|
|
996
|
+
fail('用法: openxiangda contract explain <contract-name>');
|
|
997
|
+
}
|
|
998
|
+
const contractInfo = API_CONTRACTS[name];
|
|
999
|
+
if (!contractInfo) {
|
|
1000
|
+
fail(`未知契约: ${name}。可用契约: ${Object.keys(API_CONTRACTS).sort().join(', ')}`);
|
|
1001
|
+
}
|
|
1002
|
+
if (flags.json) return writeJson(contractInfo);
|
|
1003
|
+
const lines = [
|
|
1004
|
+
`${contractInfo.name}`,
|
|
1005
|
+
`${contractInfo.method || 'GET'} ${contractInfo.endpoint}`,
|
|
1006
|
+
'',
|
|
1007
|
+
'Response:',
|
|
1008
|
+
JSON.stringify(contractInfo.response || {}, null, 2),
|
|
1009
|
+
];
|
|
1010
|
+
if (contractInfo.guarantees?.length) {
|
|
1011
|
+
lines.push('', 'Guarantees:', ...contractInfo.guarantees.map(item => `- ${item}`));
|
|
1012
|
+
}
|
|
1013
|
+
if (contractInfo.followUp) {
|
|
1014
|
+
lines.push('', `Follow-up: ${contractInfo.followUp}`);
|
|
1015
|
+
}
|
|
1016
|
+
if (contractInfo.forbidden?.length) {
|
|
1017
|
+
lines.push('', 'Forbidden:', ...contractInfo.forbidden.map(item => `- ${item}`));
|
|
1018
|
+
}
|
|
1019
|
+
print(lines.join('\n'));
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
fail('用法: openxiangda contract list|explain <contract-name> [--json]');
|
|
1024
|
+
}
|
|
1025
|
+
|
|
649
1026
|
async function platform(args) {
|
|
650
1027
|
const [subcommand, ...rest] = args;
|
|
651
1028
|
const { flags, positional } = parseArgs(rest);
|
|
@@ -1437,6 +1814,28 @@ async function workspace(args) {
|
|
|
1437
1814
|
return;
|
|
1438
1815
|
}
|
|
1439
1816
|
|
|
1817
|
+
if (subcommand === 'plan' || subcommand === 'check') {
|
|
1818
|
+
const changed = Boolean(flags.changed || flags['changed-only'] || flags.since);
|
|
1819
|
+
const plan = buildWorkspaceReleasePlan({
|
|
1820
|
+
changed,
|
|
1821
|
+
since: flags.since,
|
|
1822
|
+
profile: flags.profile || config.currentProfile || '<profile>',
|
|
1823
|
+
check: subcommand === 'check',
|
|
1824
|
+
});
|
|
1825
|
+
if (flags.json) {
|
|
1826
|
+
writeJson(plan);
|
|
1827
|
+
if (subcommand === 'check' && plan.errors.length > 0) {
|
|
1828
|
+
process.exitCode = 1;
|
|
1829
|
+
}
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1832
|
+
printWorkspaceReleasePlan(plan);
|
|
1833
|
+
if (subcommand === 'check' && plan.errors.length > 0) {
|
|
1834
|
+
fail(`workspace check 失败: ${plan.errors[0].message}`);
|
|
1835
|
+
}
|
|
1836
|
+
return;
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1440
1839
|
const { profileName, profile } = getProfile(config, flags.profile);
|
|
1441
1840
|
|
|
1442
1841
|
if (subcommand === 'bind') {
|
|
@@ -1482,12 +1881,22 @@ async function workspace(args) {
|
|
|
1482
1881
|
`profile ${profileName} 未绑定应用。先执行 openxiangda workspace bind --profile ${profileName} --app-type APP_XXX`
|
|
1483
1882
|
);
|
|
1484
1883
|
}
|
|
1884
|
+
const reactSpaWorkspace = isReactSpaWorkspace();
|
|
1885
|
+
const sddPlan = buildWorkspacePublishSddPlan(
|
|
1886
|
+
publishOptions,
|
|
1887
|
+
reactSpaWorkspace ? 'react-spa' : getWorkspaceRuntimeModeFromConfig(readWorkspaceConfigText()),
|
|
1888
|
+
profileName
|
|
1889
|
+
);
|
|
1890
|
+
if (publishOptions.dryRun) {
|
|
1891
|
+
printWorkspaceReleasePlan(sddPlan);
|
|
1892
|
+
} else {
|
|
1893
|
+
assertSddReleaseGate(sddPlan, flags, 'openxiangda workspace publish');
|
|
1894
|
+
}
|
|
1485
1895
|
await requestWithAuth(
|
|
1486
1896
|
config,
|
|
1487
1897
|
profileName,
|
|
1488
1898
|
`/openxiangda-api/v1/apps/${encodeURIComponent(bound.appType)}/snapshot`
|
|
1489
1899
|
);
|
|
1490
|
-
const reactSpaWorkspace = isReactSpaWorkspace();
|
|
1491
1900
|
if (reactSpaWorkspace && !publishOptions.legacyFormBundle) {
|
|
1492
1901
|
if (publishOptions.targetForm && !publishOptions.targetPage && !publishOptions.only && !publishOptions.changed) {
|
|
1493
1902
|
runReactSpaFormSchemaPublish(profileName, profile, bound.appType, publishOptions);
|
|
@@ -1528,7 +1937,7 @@ async function workspace(args) {
|
|
|
1528
1937
|
return;
|
|
1529
1938
|
}
|
|
1530
1939
|
|
|
1531
|
-
fail('用法: openxiangda workspace init|bind|publish [--changed [--since ref]|--form code|--page code|--only list] [--dry-run] [--force] [--resources|--skip-resources]');
|
|
1940
|
+
fail('用法: openxiangda workspace init|bind|plan|check|publish [--changed [--since ref]|--form code|--page code|--only list] [--dry-run] [--force] [--resources|--skip-resources]');
|
|
1532
1941
|
}
|
|
1533
1942
|
|
|
1534
1943
|
function printWorkspaceHelp() {
|
|
@@ -1539,7 +1948,9 @@ Usage:
|
|
|
1539
1948
|
openxiangda workspace init [dir] --profile <name> --app-name <app-name> [--runtime legacy|react-spa] [--description text] [--install] [--force]
|
|
1540
1949
|
openxiangda workspace init [dir] --profile <name> --app-type <APP_XXX> [--runtime legacy|react-spa] [--install] [--force]
|
|
1541
1950
|
openxiangda workspace bind --profile <name> --app-type <APP_XXX>
|
|
1542
|
-
openxiangda workspace
|
|
1951
|
+
openxiangda workspace plan [--changed] [--json]
|
|
1952
|
+
openxiangda workspace check [--changed] [--json]
|
|
1953
|
+
openxiangda workspace publish --profile <name> [--changed [--since ref]|--form code|--page code|--only list] [--dry-run] [--force] [--resources|--skip-resources] [--prune] [--sdd-bypass --reason text]
|
|
1543
1954
|
|
|
1544
1955
|
React SPA:
|
|
1545
1956
|
openxiangda workspace publish --profile <name> --form <formCode> # sync form schema only
|
|
@@ -1556,7 +1967,8 @@ Options:
|
|
|
1556
1967
|
--changed [--since ref] Publish changed classic resources.
|
|
1557
1968
|
--dry-run Show planned work without writing.
|
|
1558
1969
|
--resources / --skip-resources Include or skip src/resources publish.
|
|
1559
|
-
--legacy-form-bundle Force legacy workspace publish in React SPA workspaces
|
|
1970
|
+
--legacy-form-bundle Force legacy workspace publish in React SPA workspaces.
|
|
1971
|
+
plan/check Show release order and fail on risky AI fallback patterns.`);
|
|
1560
1972
|
}
|
|
1561
1973
|
|
|
1562
1974
|
async function app(args) {
|
|
@@ -1637,7 +2049,7 @@ async function form(args) {
|
|
|
1637
2049
|
const [subcommand, ...rest] = args;
|
|
1638
2050
|
const { flags, positional } = parseArgs(rest);
|
|
1639
2051
|
if (wantsSubcommandHelp(subcommand, flags)) {
|
|
1640
|
-
print('用法: openxiangda form list|create|bind|pull|publish [--profile name] [--json]');
|
|
2052
|
+
print('用法: openxiangda form list|create|bind|pull|publish|schema-plan [--profile name] [--json]');
|
|
1641
2053
|
return;
|
|
1642
2054
|
}
|
|
1643
2055
|
const config = loadConfig();
|
|
@@ -1742,7 +2154,30 @@ async function form(args) {
|
|
|
1742
2154
|
return;
|
|
1743
2155
|
}
|
|
1744
2156
|
|
|
1745
|
-
|
|
2157
|
+
if (subcommand === 'schema-plan') {
|
|
2158
|
+
const [formKey] = positional;
|
|
2159
|
+
if (!formKey) {
|
|
2160
|
+
fail('用法: openxiangda form schema-plan <formCode|formUuid> --schema-json <file>|--components-json <file>');
|
|
2161
|
+
}
|
|
2162
|
+
const target = getWorkspaceTarget(config, profileName, flags);
|
|
2163
|
+
const formUuid = resolveFormUuid(target.bound, formKey, flags);
|
|
2164
|
+
const body = readFormSchemaPlanBody(flags);
|
|
2165
|
+
const data = await requestWithAuth(
|
|
2166
|
+
config,
|
|
2167
|
+
target.profileName,
|
|
2168
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/forms/${encodeURIComponent(formUuid)}/schema-storage-plan`,
|
|
2169
|
+
{
|
|
2170
|
+
method: 'POST',
|
|
2171
|
+
body,
|
|
2172
|
+
strictEnvelope: true,
|
|
2173
|
+
}
|
|
2174
|
+
);
|
|
2175
|
+
if (flags.json) return writeJson(data);
|
|
2176
|
+
print(JSON.stringify(data, null, 2));
|
|
2177
|
+
return;
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
fail('用法: openxiangda form list|create|bind|pull|publish|schema-plan');
|
|
1746
2181
|
}
|
|
1747
2182
|
|
|
1748
2183
|
async function page(args) {
|
|
@@ -4046,9 +4481,12 @@ async function resource(args) {
|
|
|
4046
4481
|
return;
|
|
4047
4482
|
}
|
|
4048
4483
|
|
|
4049
|
-
const target = getWorkspaceTarget(config, profileName, flags);
|
|
4050
4484
|
const validation = validateWorkspaceResources(manifest);
|
|
4051
|
-
|
|
4485
|
+
let target = null;
|
|
4486
|
+
if (!isManifestEmpty(manifest)) {
|
|
4487
|
+
target = getWorkspaceTarget(config, profileName, flags);
|
|
4488
|
+
validateWorkspaceResourceBindings(manifest, target.bound, validation);
|
|
4489
|
+
}
|
|
4052
4490
|
await validateCompiledWorkflowResources(manifest, validation);
|
|
4053
4491
|
if (subcommand === 'validate') {
|
|
4054
4492
|
if (flags.json) return writeJson(validation);
|
|
@@ -4063,6 +4501,7 @@ async function resource(args) {
|
|
|
4063
4501
|
}
|
|
4064
4502
|
|
|
4065
4503
|
const dryRun = subcommand === 'publish' && Boolean(flags['dry-run']);
|
|
4504
|
+
target = target || getWorkspaceTarget(config, profileName, flags);
|
|
4066
4505
|
if (subcommand === 'plan' || dryRun) {
|
|
4067
4506
|
const plan = await buildResourcePlan(config, target, manifest);
|
|
4068
4507
|
if (dryRun) {
|
|
@@ -4075,6 +4514,16 @@ async function resource(args) {
|
|
|
4075
4514
|
}
|
|
4076
4515
|
|
|
4077
4516
|
const publishPlan = await buildResourcePlan(config, target, manifest);
|
|
4517
|
+
if ((publishPlan.summary.create || 0) + (publishPlan.summary.update || 0) + (publishPlan.summary.delete || 0) > 0) {
|
|
4518
|
+
assertSddReleaseGate(
|
|
4519
|
+
buildExplicitSddReleasePlan(
|
|
4520
|
+
{ forms: [], pages: [], resources: true, runtime: false, other: [] },
|
|
4521
|
+
{ files: getWorkspaceChangedFiles() }
|
|
4522
|
+
),
|
|
4523
|
+
flags,
|
|
4524
|
+
'openxiangda resource publish'
|
|
4525
|
+
);
|
|
4526
|
+
}
|
|
4078
4527
|
const result = await publishResourceManifest(config, target, manifest, {
|
|
4079
4528
|
planActions: publishPlan.actions,
|
|
4080
4529
|
skipNoop: !Boolean(flags.force),
|
|
@@ -4133,6 +4582,14 @@ async function runtime(args) {
|
|
|
4133
4582
|
}
|
|
4134
4583
|
|
|
4135
4584
|
if (subcommand === 'deploy') {
|
|
4585
|
+
assertSddReleaseGate(
|
|
4586
|
+
buildExplicitSddReleasePlan(
|
|
4587
|
+
{ forms: [], pages: [], resources: false, runtime: true, other: [] },
|
|
4588
|
+
{ files: getWorkspaceChangedFiles() }
|
|
4589
|
+
),
|
|
4590
|
+
flags,
|
|
4591
|
+
'openxiangda runtime deploy'
|
|
4592
|
+
);
|
|
4136
4593
|
const buildId = normalizeRuntimeBuildId(flags['build-id'] || createRuntimeBuildId());
|
|
4137
4594
|
const distDir = path.resolve(process.cwd(), flags.dist || 'dist');
|
|
4138
4595
|
const uploadMode = normalizeRuntimeUploadMode(flags['upload-mode']);
|
|
@@ -4742,12 +5199,14 @@ async function commands(args) {
|
|
|
4742
5199
|
'update check|install',
|
|
4743
5200
|
'platform add|list|use|remove',
|
|
4744
5201
|
'auth status|refresh|logout',
|
|
4745
|
-
'doctor [--profile name] [--app-type APP_XXX]',
|
|
5202
|
+
'doctor [--profile name] [--app-type APP_XXX] [--release]',
|
|
5203
|
+
'contract list|explain <contract-name>',
|
|
4746
5204
|
'design gates|template|review [--topic code]',
|
|
5205
|
+
'sdd init|propose|approve|status|context|verify|sync|archive',
|
|
4747
5206
|
'env',
|
|
4748
|
-
'workspace init|bind|publish [--app-name] [--changed|--since|--form|--page|--only|--dry-run|--force|--resources|--skip-resources|--prune]',
|
|
5207
|
+
'workspace init|bind|plan|check|publish [--app-name] [--changed|--since|--form|--page|--only|--dry-run|--force|--resources|--skip-resources|--prune]',
|
|
4749
5208
|
'app list|create|snapshot',
|
|
4750
|
-
'form list|create|bind|pull|publish',
|
|
5209
|
+
'form list|create|bind|pull|publish|schema-plan',
|
|
4751
5210
|
'page list|publish|bind|releases|activate',
|
|
4752
5211
|
'menu list|create|update|sort|bind|delete',
|
|
4753
5212
|
'workflow compile|list|create|bind|pull|publish|delete|validate',
|
|
@@ -4777,6 +5236,7 @@ async function commands(args) {
|
|
|
4777
5236
|
'public-access is the new React SPA public policy resource for /view/:appType/public/* routes; page code should use PublicAccessGate, and Page SDK hooks must stay inside OpenXiangdaProvider + OpenXiangdaPageProvider.',
|
|
4778
5237
|
'settings public-access is legacy form public-access compatibility/repair and should not be used for new React SPA apps.',
|
|
4779
5238
|
'Direct live mutation commands should use --dry-run first and --write-manifest when the repository should remain source of truth.',
|
|
5239
|
+
'OpenXiangda native SDD uses openspec/ records; high-risk publishes require an approved change unless --sdd-bypass --reason is used.',
|
|
4780
5240
|
],
|
|
4781
5241
|
};
|
|
4782
5242
|
if (flags.json) return writeJson(manifest);
|
|
@@ -4794,6 +5254,9 @@ function printWorkspaceInitReport(result) {
|
|
|
4794
5254
|
if (result.bound) {
|
|
4795
5255
|
lines.push(`已绑定 ${result.bound.profile}: ${result.bound.appType}`);
|
|
4796
5256
|
}
|
|
5257
|
+
if (result.sdd?.initialized) {
|
|
5258
|
+
lines.push(`SDD: ${result.sdd.path}`);
|
|
5259
|
+
}
|
|
4797
5260
|
if (result.installedDependencies) {
|
|
4798
5261
|
lines.push('依赖已安装');
|
|
4799
5262
|
}
|
|
@@ -5357,6 +5820,19 @@ function resolveFormUuid(bound, formKey, flags = {}) {
|
|
|
5357
5820
|
return mapped || formKey;
|
|
5358
5821
|
}
|
|
5359
5822
|
|
|
5823
|
+
function readFormSchemaPlanBody(flags = {}) {
|
|
5824
|
+
if (flags['schema-json']) {
|
|
5825
|
+
return { schema: readJsonArg(flags['schema-json'], 'schema-json') };
|
|
5826
|
+
}
|
|
5827
|
+
if (flags['components-json']) {
|
|
5828
|
+
return { components: readJsonArg(flags['components-json'], 'components-json') };
|
|
5829
|
+
}
|
|
5830
|
+
if (flags['json-file'] || flags['body-json']) {
|
|
5831
|
+
return readDirectJsonBody(flags, 'form schema-plan');
|
|
5832
|
+
}
|
|
5833
|
+
fail('form schema-plan 需要 --schema-json <file> 或 --components-json <file>');
|
|
5834
|
+
}
|
|
5835
|
+
|
|
5360
5836
|
function resolveOptionalFormUuid(bound, formCode) {
|
|
5361
5837
|
if (!formCode) return undefined;
|
|
5362
5838
|
return bound.resources?.forms?.[formCode]?.formUuid || formCode;
|
|
@@ -10926,6 +11402,459 @@ function unwrapStrictApi(payload) {
|
|
|
10926
11402
|
return 'data' in payload ? payload.data : payload;
|
|
10927
11403
|
}
|
|
10928
11404
|
|
|
11405
|
+
function listDirectoriesIfExists(dir) {
|
|
11406
|
+
if (!fs.existsSync(dir)) return [];
|
|
11407
|
+
return fs
|
|
11408
|
+
.readdirSync(dir, { withFileTypes: true })
|
|
11409
|
+
.filter(entry => entry.isDirectory())
|
|
11410
|
+
.map(entry => entry.name)
|
|
11411
|
+
.sort();
|
|
11412
|
+
}
|
|
11413
|
+
|
|
11414
|
+
function normalizeWorkspaceRelativePath(value) {
|
|
11415
|
+
return String(value || '')
|
|
11416
|
+
.trim()
|
|
11417
|
+
.replace(/^"|"$/g, '')
|
|
11418
|
+
.split(path.sep)
|
|
11419
|
+
.join('/');
|
|
11420
|
+
}
|
|
11421
|
+
|
|
11422
|
+
function parseGitStatusPath(line) {
|
|
11423
|
+
const raw = line.slice(3).trim();
|
|
11424
|
+
const renamed = raw.includes(' -> ') ? raw.split(' -> ').pop() : raw;
|
|
11425
|
+
return normalizeWorkspaceRelativePath(renamed);
|
|
11426
|
+
}
|
|
11427
|
+
|
|
11428
|
+
function readGitNameList(args, options = {}) {
|
|
11429
|
+
const result = spawnSync('git', args, {
|
|
11430
|
+
cwd: process.cwd(),
|
|
11431
|
+
encoding: 'utf8',
|
|
11432
|
+
});
|
|
11433
|
+
if (result.status !== 0) {
|
|
11434
|
+
if (options.required) {
|
|
11435
|
+
fail(`git ${args.join(' ')} 失败: ${(result.stderr || result.stdout || '').trim()}`);
|
|
11436
|
+
}
|
|
11437
|
+
return [];
|
|
11438
|
+
}
|
|
11439
|
+
return result.stdout
|
|
11440
|
+
.split(/\r?\n/)
|
|
11441
|
+
.map(line => line.trimEnd())
|
|
11442
|
+
.filter(Boolean)
|
|
11443
|
+
.map(normalizeWorkspaceRelativePath)
|
|
11444
|
+
.filter(Boolean);
|
|
11445
|
+
}
|
|
11446
|
+
|
|
11447
|
+
function getWorkspaceChangedFiles(options = {}) {
|
|
11448
|
+
const since = String(options.since || '').trim();
|
|
11449
|
+
if (since) {
|
|
11450
|
+
return unique([
|
|
11451
|
+
...readGitNameList(
|
|
11452
|
+
['diff', '--name-only', '--diff-filter=ACMRTUXB', `${since}...HEAD`],
|
|
11453
|
+
{ required: true }
|
|
11454
|
+
),
|
|
11455
|
+
...readGitNameList(['diff', '--name-only', '--diff-filter=ACMRTUXB']),
|
|
11456
|
+
...readGitNameList(['diff', '--name-only', '--cached', '--diff-filter=ACMRTUXB']),
|
|
11457
|
+
...readGitNameList(['ls-files', '--others', '--exclude-standard']),
|
|
11458
|
+
]).sort();
|
|
11459
|
+
}
|
|
11460
|
+
|
|
11461
|
+
const result = spawnSync('git', ['status', '--short', '--untracked-files=all'], {
|
|
11462
|
+
cwd: process.cwd(),
|
|
11463
|
+
encoding: 'utf8',
|
|
11464
|
+
});
|
|
11465
|
+
if (result.status !== 0) {
|
|
11466
|
+
return [];
|
|
11467
|
+
}
|
|
11468
|
+
return result.stdout
|
|
11469
|
+
.split(/\r?\n/)
|
|
11470
|
+
.map(line => line.trimEnd())
|
|
11471
|
+
.filter(Boolean)
|
|
11472
|
+
.map(parseGitStatusPath)
|
|
11473
|
+
.filter(Boolean);
|
|
11474
|
+
}
|
|
11475
|
+
|
|
11476
|
+
function getAllWorkspaceSourceFiles() {
|
|
11477
|
+
const files = [];
|
|
11478
|
+
const visit = dir => {
|
|
11479
|
+
if (!fs.existsSync(dir)) return;
|
|
11480
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
11481
|
+
if (['node_modules', 'dist', '.vite', '.cache', 'coverage'].includes(entry.name)) continue;
|
|
11482
|
+
const entryPath = path.join(dir, entry.name);
|
|
11483
|
+
if (entry.isDirectory()) {
|
|
11484
|
+
visit(entryPath);
|
|
11485
|
+
continue;
|
|
11486
|
+
}
|
|
11487
|
+
if (!entry.isFile()) continue;
|
|
11488
|
+
files.push(normalizeWorkspaceRelativePath(path.relative(process.cwd(), entryPath)));
|
|
11489
|
+
}
|
|
11490
|
+
};
|
|
11491
|
+
['src', 'app-workspace.config.ts', 'package.json', 'vite.config.ts'].forEach(item => {
|
|
11492
|
+
const target = path.join(process.cwd(), item);
|
|
11493
|
+
if (fs.existsSync(target) && fs.statSync(target).isDirectory()) {
|
|
11494
|
+
visit(target);
|
|
11495
|
+
} else if (fs.existsSync(target)) {
|
|
11496
|
+
files.push(item);
|
|
11497
|
+
}
|
|
11498
|
+
});
|
|
11499
|
+
return files.sort();
|
|
11500
|
+
}
|
|
11501
|
+
|
|
11502
|
+
function classifyWorkspaceFiles(files, runtimeMode) {
|
|
11503
|
+
const forms = new Set();
|
|
11504
|
+
const pages = new Set();
|
|
11505
|
+
let resources = false;
|
|
11506
|
+
let runtime = false;
|
|
11507
|
+
const other = [];
|
|
11508
|
+
|
|
11509
|
+
for (const file of files) {
|
|
11510
|
+
const formMatch = file.match(/^src\/forms\/([^/]+)/);
|
|
11511
|
+
if (formMatch) {
|
|
11512
|
+
forms.add(formMatch[1]);
|
|
11513
|
+
continue;
|
|
11514
|
+
}
|
|
11515
|
+
const pageMatch = file.match(/^src\/pages\/([^/]+)/);
|
|
11516
|
+
if (pageMatch) {
|
|
11517
|
+
if (runtimeMode === 'react-spa') {
|
|
11518
|
+
runtime = true;
|
|
11519
|
+
} else {
|
|
11520
|
+
pages.add(pageMatch[1]);
|
|
11521
|
+
}
|
|
11522
|
+
continue;
|
|
11523
|
+
}
|
|
11524
|
+
if (file.startsWith('src/resources/')) {
|
|
11525
|
+
resources = true;
|
|
11526
|
+
continue;
|
|
11527
|
+
}
|
|
11528
|
+
if (
|
|
11529
|
+
file === 'package.json' ||
|
|
11530
|
+
file === 'vite.config.ts' ||
|
|
11531
|
+
file === 'index.html' ||
|
|
11532
|
+
file.startsWith('src/')
|
|
11533
|
+
) {
|
|
11534
|
+
runtime = runtimeMode === 'react-spa' || runtime;
|
|
11535
|
+
continue;
|
|
11536
|
+
}
|
|
11537
|
+
other.push(file);
|
|
11538
|
+
}
|
|
11539
|
+
|
|
11540
|
+
return {
|
|
11541
|
+
forms: Array.from(forms).sort(),
|
|
11542
|
+
pages: Array.from(pages).sort(),
|
|
11543
|
+
resources,
|
|
11544
|
+
runtime,
|
|
11545
|
+
other,
|
|
11546
|
+
};
|
|
11547
|
+
}
|
|
11548
|
+
|
|
11549
|
+
function readWorkspaceConfigText() {
|
|
11550
|
+
const file = path.join(process.cwd(), 'app-workspace.config.ts');
|
|
11551
|
+
return fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
|
|
11552
|
+
}
|
|
11553
|
+
|
|
11554
|
+
function getWorkspaceRuntimeModeFromConfig(configText) {
|
|
11555
|
+
return /runtimeMode\s*:\s*['"]react-spa['"]/.test(configText) ? 'react-spa' : 'legacy';
|
|
11556
|
+
}
|
|
11557
|
+
|
|
11558
|
+
function getWorkspaceCompatibilityFromConfig(configText) {
|
|
11559
|
+
return {
|
|
11560
|
+
apiContracts: /apiContracts\s*:\s*['"]legacy['"]/.test(configText) ? 'legacy' : 'strict',
|
|
11561
|
+
legacyFallbacks: /legacyFallbacks\s*:\s*true/.test(configText),
|
|
11562
|
+
requestTrace: /requestTrace\s*:\s*true/.test(configText),
|
|
11563
|
+
};
|
|
11564
|
+
}
|
|
11565
|
+
|
|
11566
|
+
const AI_RELIABILITY_RULES = [
|
|
11567
|
+
{
|
|
11568
|
+
id: 'fake-form-instance-id',
|
|
11569
|
+
severity: 'error',
|
|
11570
|
+
pattern: /inst_\$\{Date\.now\(\)\}|inst_['"]?\s*\+\s*Date\.now\(/,
|
|
11571
|
+
message: '禁止生成假 formInstanceId;保存接口缺少实例 ID 必须报错。',
|
|
11572
|
+
},
|
|
11573
|
+
{
|
|
11574
|
+
id: 'implicit-response-guessing',
|
|
11575
|
+
severity: 'warn',
|
|
11576
|
+
pattern: /response\??\.(data|result)\s*(\?\?|\|\|)\s*response\??\.(result|data)/,
|
|
11577
|
+
message: '避免在业务代码中猜 data/result;应使用 SDK 契约或 endpoint normalizer。',
|
|
11578
|
+
},
|
|
11579
|
+
{
|
|
11580
|
+
id: 'legacy-lookup-after-create',
|
|
11581
|
+
severity: 'warn',
|
|
11582
|
+
pattern: /lookupAfterCreate\s*:\s*true/,
|
|
11583
|
+
message: 'lookupAfterCreate 只能用于旧项目兼容;新代码保存后应显式 getDetail。',
|
|
11584
|
+
},
|
|
11585
|
+
{
|
|
11586
|
+
id: 'direct-openxiangda-api',
|
|
11587
|
+
severity: 'error',
|
|
11588
|
+
pattern: /fetch\s*\([^)]*openxiangda-api|axios\.[a-z]+\([^)]*openxiangda-api/,
|
|
11589
|
+
message: '页面代码不要直接请求 openxiangda-api;使用 Page SDK、connector 或 App Function。',
|
|
11590
|
+
},
|
|
11591
|
+
{
|
|
11592
|
+
id: 'mock-or-fallback-data',
|
|
11593
|
+
severity: 'warn',
|
|
11594
|
+
pattern: /\b(mockData|fallbackData|fallbackRows|demoRows)\b/,
|
|
11595
|
+
message: '不要用 mock/fallback 数据掩盖真实接口失败;开发态应暴露错误。',
|
|
11596
|
+
},
|
|
11597
|
+
];
|
|
11598
|
+
|
|
11599
|
+
function scanWorkspaceReliabilityIssues(files) {
|
|
11600
|
+
const issues = [];
|
|
11601
|
+
for (const file of files) {
|
|
11602
|
+
if (!file.startsWith('src/')) continue;
|
|
11603
|
+
if (/\.(test|spec)\.(ts|tsx|js|jsx)$/.test(file)) continue;
|
|
11604
|
+
if (!/\.(ts|tsx|js|jsx|mjs|cjs)$/.test(file)) continue;
|
|
11605
|
+
const abs = path.join(process.cwd(), file);
|
|
11606
|
+
if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) continue;
|
|
11607
|
+
const content = fs.readFileSync(abs, 'utf8');
|
|
11608
|
+
const lines = content.split(/\r?\n/);
|
|
11609
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
11610
|
+
const line = lines[index];
|
|
11611
|
+
for (const rule of AI_RELIABILITY_RULES) {
|
|
11612
|
+
if (rule.pattern.test(line)) {
|
|
11613
|
+
issues.push({
|
|
11614
|
+
rule: rule.id,
|
|
11615
|
+
severity: rule.severity,
|
|
11616
|
+
file,
|
|
11617
|
+
line: index + 1,
|
|
11618
|
+
message: rule.message,
|
|
11619
|
+
});
|
|
11620
|
+
}
|
|
11621
|
+
}
|
|
11622
|
+
}
|
|
11623
|
+
}
|
|
11624
|
+
return issues;
|
|
11625
|
+
}
|
|
11626
|
+
|
|
11627
|
+
function buildWorkspaceReleaseCommands(targets, runtimeMode, profile) {
|
|
11628
|
+
const profileArg = profile || '<profile>';
|
|
11629
|
+
const commands = [];
|
|
11630
|
+
if (runtimeMode === 'react-spa') {
|
|
11631
|
+
for (const formCode of targets.forms) {
|
|
11632
|
+
commands.push(`openxiangda workspace publish --profile ${profileArg} --form ${formCode}`);
|
|
11633
|
+
}
|
|
11634
|
+
if (targets.resources) {
|
|
11635
|
+
commands.push(`openxiangda resource publish --profile ${profileArg}`);
|
|
11636
|
+
}
|
|
11637
|
+
if (targets.runtime) {
|
|
11638
|
+
commands.push(`openxiangda runtime deploy --profile ${profileArg}`);
|
|
11639
|
+
}
|
|
11640
|
+
return commands;
|
|
11641
|
+
}
|
|
11642
|
+
|
|
11643
|
+
const onlyTargets = [
|
|
11644
|
+
...targets.forms.map(code => `forms/${code}`),
|
|
11645
|
+
...targets.pages.map(code => `pages/${code}`),
|
|
11646
|
+
];
|
|
11647
|
+
if (onlyTargets.length > 0) {
|
|
11648
|
+
commands.push(
|
|
11649
|
+
`openxiangda workspace publish --profile ${profileArg} --only ${onlyTargets.join(',')}${
|
|
11650
|
+
targets.resources ? ' --resources' : ''
|
|
11651
|
+
}`
|
|
11652
|
+
);
|
|
11653
|
+
} else if (targets.resources) {
|
|
11654
|
+
commands.push(`openxiangda resource publish --profile ${profileArg}`);
|
|
11655
|
+
}
|
|
11656
|
+
return commands;
|
|
11657
|
+
}
|
|
11658
|
+
|
|
11659
|
+
function buildWorkspaceReleasePlan(options = {}) {
|
|
11660
|
+
const configText = readWorkspaceConfigText();
|
|
11661
|
+
const runtimeMode = getWorkspaceRuntimeModeFromConfig(configText);
|
|
11662
|
+
const compatibility = getWorkspaceCompatibilityFromConfig(configText);
|
|
11663
|
+
const files = options.changed
|
|
11664
|
+
? getWorkspaceChangedFiles({ since: options.since })
|
|
11665
|
+
: getAllWorkspaceSourceFiles();
|
|
11666
|
+
const targets = classifyWorkspaceFiles(files, runtimeMode);
|
|
11667
|
+
const issues = scanWorkspaceReliabilityIssues(files);
|
|
11668
|
+
const errors = issues
|
|
11669
|
+
.filter(issue => issue.severity === 'error')
|
|
11670
|
+
.map(issue => ({
|
|
11671
|
+
name: issue.rule,
|
|
11672
|
+
message: `${issue.file}:${issue.line} ${issue.message}`,
|
|
11673
|
+
}));
|
|
11674
|
+
const commands = buildWorkspaceReleaseCommands(targets, runtimeMode, options.profile);
|
|
11675
|
+
const sdd = options.skipSdd
|
|
11676
|
+
? null
|
|
11677
|
+
: buildSddGovernanceReport({
|
|
11678
|
+
cwd: process.cwd(),
|
|
11679
|
+
configText,
|
|
11680
|
+
targets,
|
|
11681
|
+
files,
|
|
11682
|
+
});
|
|
11683
|
+
const sddErrors = sdd ? sdd.errors.map(error => ({ name: error.name, message: error.message })) : [];
|
|
11684
|
+
const checks = [
|
|
11685
|
+
{
|
|
11686
|
+
name: 'workspace-config',
|
|
11687
|
+
status: configText ? 'ok' : 'warn',
|
|
11688
|
+
message: configText ? undefined : '未发现 app-workspace.config.ts',
|
|
11689
|
+
},
|
|
11690
|
+
{
|
|
11691
|
+
name: 'api-contracts',
|
|
11692
|
+
status: compatibility.apiContracts === 'strict' ? 'ok' : 'warn',
|
|
11693
|
+
message:
|
|
11694
|
+
compatibility.apiContracts === 'strict'
|
|
11695
|
+
? undefined
|
|
11696
|
+
: '当前工作区处于 legacy API 契约模式',
|
|
11697
|
+
},
|
|
11698
|
+
{
|
|
11699
|
+
name: 'legacy-fallbacks',
|
|
11700
|
+
status: compatibility.legacyFallbacks ? 'warn' : 'ok',
|
|
11701
|
+
message: compatibility.legacyFallbacks ? 'legacyFallbacks 已开启,可能掩盖接口/schema 问题' : undefined,
|
|
11702
|
+
},
|
|
11703
|
+
{
|
|
11704
|
+
name: 'ai-reliability',
|
|
11705
|
+
status: errors.length > 0 ? 'error' : issues.length > 0 ? 'warn' : 'ok',
|
|
11706
|
+
message: issues.length > 0 ? `${issues.length} 个风险项` : undefined,
|
|
11707
|
+
},
|
|
11708
|
+
...(sdd ? sdd.checks : []),
|
|
11709
|
+
];
|
|
11710
|
+
|
|
11711
|
+
return {
|
|
11712
|
+
workspace: {
|
|
11713
|
+
cwd: process.cwd(),
|
|
11714
|
+
runtimeMode,
|
|
11715
|
+
configFile: path.join(process.cwd(), 'app-workspace.config.ts'),
|
|
11716
|
+
compatibility,
|
|
11717
|
+
},
|
|
11718
|
+
mode: options.changed ? 'changed' : 'all',
|
|
11719
|
+
since: options.changed ? options.since || undefined : undefined,
|
|
11720
|
+
changedFiles: options.changed ? files : undefined,
|
|
11721
|
+
targets,
|
|
11722
|
+
publishOrder:
|
|
11723
|
+
runtimeMode === 'react-spa'
|
|
11724
|
+
? ['forms', 'resources', 'runtime']
|
|
11725
|
+
: ['forms/pages', 'resources'],
|
|
11726
|
+
commands,
|
|
11727
|
+
checks,
|
|
11728
|
+
sdd,
|
|
11729
|
+
issues,
|
|
11730
|
+
errors: [...errors, ...sddErrors],
|
|
11731
|
+
};
|
|
11732
|
+
}
|
|
11733
|
+
|
|
11734
|
+
function printWorkspaceReleasePlan(plan) {
|
|
11735
|
+
const lines = [
|
|
11736
|
+
`OpenXiangda workspace ${plan.mode} plan`,
|
|
11737
|
+
`runtime: ${plan.workspace.runtimeMode}`,
|
|
11738
|
+
`compatibility: apiContracts=${plan.workspace.compatibility.apiContracts}, legacyFallbacks=${plan.workspace.compatibility.legacyFallbacks}`,
|
|
11739
|
+
`forms: ${plan.targets.forms.length ? plan.targets.forms.join(', ') : '(none)'}`,
|
|
11740
|
+
`pages: ${plan.targets.pages.length ? plan.targets.pages.join(', ') : '(none)'}`,
|
|
11741
|
+
`resources: ${plan.targets.resources ? 'yes' : 'no'}`,
|
|
11742
|
+
`runtime deploy: ${plan.targets.runtime ? 'yes' : 'no'}`,
|
|
11743
|
+
];
|
|
11744
|
+
if (plan.commands.length > 0) {
|
|
11745
|
+
lines.push('', 'Commands:', ...plan.commands.map(command => ` ${command}`));
|
|
11746
|
+
}
|
|
11747
|
+
if (plan.issues.length > 0) {
|
|
11748
|
+
lines.push('', 'Reliability issues:');
|
|
11749
|
+
for (const issue of plan.issues) {
|
|
11750
|
+
lines.push(`- [${issue.severity}] ${issue.file}:${issue.line} ${issue.message}`);
|
|
11751
|
+
}
|
|
11752
|
+
}
|
|
11753
|
+
if (plan.sdd) {
|
|
11754
|
+
lines.push(
|
|
11755
|
+
'',
|
|
11756
|
+
`SDD: enabled=${plan.sdd.governance.enabled ? 'true' : 'false'}, initialized=${plan.sdd.governance.initialized ? 'true' : 'false'}, strictHighRisk=${plan.sdd.governance.strictHighRisk ? 'true' : 'false'}, activeChanges=${plan.sdd.activeChanges.length}`
|
|
11757
|
+
);
|
|
11758
|
+
if (plan.sdd.highRiskFiles.length > 0) {
|
|
11759
|
+
lines.push(`high-risk files: ${plan.sdd.highRiskFiles.join(', ')}`);
|
|
11760
|
+
}
|
|
11761
|
+
}
|
|
11762
|
+
for (const check of plan.checks) {
|
|
11763
|
+
const suffix = check.message ? ` - ${check.message}` : '';
|
|
11764
|
+
lines.push(`- ${check.name}: ${check.status}${suffix}`);
|
|
11765
|
+
}
|
|
11766
|
+
print(lines.join('\n'));
|
|
11767
|
+
}
|
|
11768
|
+
|
|
11769
|
+
function buildExplicitSddReleasePlan(targets, options = {}) {
|
|
11770
|
+
const configText = readWorkspaceConfigText();
|
|
11771
|
+
const sdd = buildSddGovernanceReport({
|
|
11772
|
+
cwd: process.cwd(),
|
|
11773
|
+
configText,
|
|
11774
|
+
targets,
|
|
11775
|
+
files: options.files || [],
|
|
11776
|
+
});
|
|
11777
|
+
return {
|
|
11778
|
+
targets,
|
|
11779
|
+
sdd,
|
|
11780
|
+
errors: sdd.errors.map(error => ({ name: error.name, message: error.message })),
|
|
11781
|
+
};
|
|
11782
|
+
}
|
|
11783
|
+
|
|
11784
|
+
function assertSddReleaseGate(plan, flags, command) {
|
|
11785
|
+
if (!plan?.sdd || plan.sdd.errors.length === 0) return;
|
|
11786
|
+
if (flags['sdd-bypass']) {
|
|
11787
|
+
const bypass = appendBypassLog({
|
|
11788
|
+
cwd: process.cwd(),
|
|
11789
|
+
configText: readWorkspaceConfigText(),
|
|
11790
|
+
command,
|
|
11791
|
+
reason: flags.reason,
|
|
11792
|
+
targets: plan.targets,
|
|
11793
|
+
});
|
|
11794
|
+
warn(`SDD gate 已通过 bypass 放行: ${bypass.file}`);
|
|
11795
|
+
return;
|
|
11796
|
+
}
|
|
11797
|
+
const first = plan.sdd.errors[0];
|
|
11798
|
+
fail(`${first.message}\n如为紧急修复,可使用 --sdd-bypass --reason "..." 并在发布后补齐 SDD 归档。`);
|
|
11799
|
+
}
|
|
11800
|
+
|
|
11801
|
+
function buildWorkspacePublishSddPlan(publishOptions, runtimeMode, profile) {
|
|
11802
|
+
if (publishOptions.changed || (!publishOptions.targetForm && !publishOptions.targetPage && !publishOptions.only)) {
|
|
11803
|
+
const plan = buildWorkspaceReleasePlan({
|
|
11804
|
+
changed: publishOptions.changed,
|
|
11805
|
+
since: publishOptions.since,
|
|
11806
|
+
profile,
|
|
11807
|
+
});
|
|
11808
|
+
if (publishOptions.includeResources) {
|
|
11809
|
+
plan.targets.resources = true;
|
|
11810
|
+
}
|
|
11811
|
+
return plan;
|
|
11812
|
+
}
|
|
11813
|
+
const targets = {
|
|
11814
|
+
forms: [],
|
|
11815
|
+
pages: [],
|
|
11816
|
+
resources: Boolean(publishOptions.includeResources),
|
|
11817
|
+
runtime: false,
|
|
11818
|
+
other: [],
|
|
11819
|
+
};
|
|
11820
|
+
if (publishOptions.targetForm) targets.forms.push(publishOptions.targetForm);
|
|
11821
|
+
if (publishOptions.targetPage) {
|
|
11822
|
+
if (runtimeMode === 'react-spa') targets.runtime = true;
|
|
11823
|
+
else targets.pages.push(publishOptions.targetPage);
|
|
11824
|
+
}
|
|
11825
|
+
if (publishOptions.only) {
|
|
11826
|
+
for (const item of publishOptions.only.split(',').map(value => value.trim()).filter(Boolean)) {
|
|
11827
|
+
const formMatch = item.match(/^forms\/([^/]+)$/);
|
|
11828
|
+
const pageMatch = item.match(/^pages\/([^/]+)$/);
|
|
11829
|
+
if (formMatch) targets.forms.push(formMatch[1]);
|
|
11830
|
+
else if (pageMatch) targets.pages.push(pageMatch[1]);
|
|
11831
|
+
}
|
|
11832
|
+
}
|
|
11833
|
+
targets.forms = unique(targets.forms).sort();
|
|
11834
|
+
targets.pages = unique(targets.pages).sort();
|
|
11835
|
+
const commands = buildWorkspaceReleaseCommands(targets, runtimeMode, profile);
|
|
11836
|
+
const sddPlan = buildExplicitSddReleasePlan(targets, { files: [] });
|
|
11837
|
+
return {
|
|
11838
|
+
workspace: {
|
|
11839
|
+
cwd: process.cwd(),
|
|
11840
|
+
runtimeMode,
|
|
11841
|
+
configFile: path.join(process.cwd(), 'app-workspace.config.ts'),
|
|
11842
|
+
compatibility: getWorkspaceCompatibilityFromConfig(readWorkspaceConfigText()),
|
|
11843
|
+
},
|
|
11844
|
+
mode: 'targeted',
|
|
11845
|
+
targets,
|
|
11846
|
+
publishOrder:
|
|
11847
|
+
runtimeMode === 'react-spa'
|
|
11848
|
+
? ['forms', 'resources', 'runtime']
|
|
11849
|
+
: ['forms/pages', 'resources'],
|
|
11850
|
+
commands,
|
|
11851
|
+
checks: sddPlan.sdd.checks,
|
|
11852
|
+
sdd: sddPlan.sdd,
|
|
11853
|
+
issues: [],
|
|
11854
|
+
errors: sddPlan.errors,
|
|
11855
|
+
};
|
|
11856
|
+
}
|
|
11857
|
+
|
|
10929
11858
|
function isUnauthorized(error) {
|
|
10930
11859
|
return (
|
|
10931
11860
|
Number(error?.apiCode) === 401 ||
|
|
@@ -11155,8 +12084,10 @@ function normalizeWorkspacePublishOptions(flags) {
|
|
|
11155
12084
|
targetPage,
|
|
11156
12085
|
only,
|
|
11157
12086
|
changed,
|
|
12087
|
+
since,
|
|
11158
12088
|
legacyFormBundle: Boolean(flags['legacy-form-bundle']),
|
|
11159
12089
|
includeResources,
|
|
12090
|
+
prune,
|
|
11160
12091
|
quietResourceSkip: !targeted || skipResources,
|
|
11161
12092
|
};
|
|
11162
12093
|
}
|