openxiangda 1.0.214 → 1.0.216
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 +5 -1
- package/lib/application-environments.js +227 -3
- package/lib/cli.js +1521 -136
- package/lib/policy.js +1 -0
- package/lib/release-mainline.js +67 -8
- package/lib/resource-binding-contract.js +210 -0
- package/lib/sdd.js +216 -74
- package/lib/workspace-init.js +9 -3
- package/lib/worktree-cleanup.js +631 -0
- package/lib/worktree-owner.js +121 -5
- package/openxiangda-skills/SKILL.md +11 -4
- package/openxiangda-skills/references/openxiangda-api.md +1 -1
- package/openxiangda-skills/references/workspace-state.md +1 -1
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +9 -1
- package/package.json +8 -2
- package/packages/sdk/dist/runtime/index.cjs +3305 -1752
- package/packages/sdk/dist/runtime/index.cjs.map +1 -1
- package/packages/sdk/dist/runtime/index.d.mts +1 -1
- package/packages/sdk/dist/runtime/index.d.ts +1 -1
- package/packages/sdk/dist/runtime/index.mjs +3275 -1699
- package/packages/sdk/dist/runtime/index.mjs.map +1 -1
- package/packages/sdk/dist/runtime/react.cjs +1572 -19
- package/packages/sdk/dist/runtime/react.cjs.map +1 -1
- package/packages/sdk/dist/runtime/react.d.mts +219 -2
- package/packages/sdk/dist/runtime/react.d.ts +219 -2
- package/packages/sdk/dist/runtime/react.mjs +1595 -19
- package/packages/sdk/dist/runtime/react.mjs.map +1 -1
- package/packages/sdk/source/admin-list/AdminList.tsx +1525 -0
- package/packages/sdk/source/admin-list/README.md +12 -0
- package/packages/sdk/source/admin-list/dataSources.ts +220 -0
- package/packages/sdk/source/admin-list/index.ts +41 -0
- package/packages/sdk/source/admin-list/preferences.ts +174 -0
- package/packages/sdk/source/admin-list/types.ts +251 -0
- package/templates/openxiangda-react-spa/.cursor/rules/openxiangda.mdc +6 -0
- package/templates/openxiangda-react-spa/.qoder/rules/openxiangda.md +6 -0
- package/templates/openxiangda-react-spa/AGENTS.md +8 -1
- package/templates/openxiangda-react-spa/scripts/deploy.mjs +1 -1
- package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda.mdc +6 -1
- package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda.md +6 -0
- package/templates/sy-lowcode-app-workspace/AGENTS.md +9 -1
package/README.md
CHANGED
|
@@ -82,7 +82,7 @@ openxiangda app snapshot APP_XXXX --profile dev --json
|
|
|
82
82
|
|
|
83
83
|
User tokens are stored in `~/.openxiangda/profiles.json` with `0600` permissions. Shared workspace environment values, including `APP_OSS_*`, can live in `~/.openxiangda/.env` and are inherited by new workspaces. Project `.env` files still work and override the global defaults. Project state is stored in `.openxiangda/state.json` and contains only durable profile-specific resource IDs and environment bindings; volatile candidate/deployment progress lives in the private `.openxiangda/releases/` journal so a release does not dirty the reviewed Git state. Durable CLI writes use a workspace lock plus atomic merge/rename so concurrent processes do not truncate another profile's state.
|
|
84
84
|
|
|
85
|
-
An environment-managed workspace keeps one logical application with independent `preproduction` and `production` targets. Each target owns its own `appType`, resource IDs, release heads, data, and side-effect policy; IDs must never be copied across targets. `release ship`
|
|
85
|
+
An environment-managed workspace keeps one logical application with independent `preproduction` and `production` targets. Each target owns its own `appType`, resource IDs, release heads, data, and side-effect policy; IDs must never be copied across targets. `release ship` always executes the same ordered candidate → preproduction → production protocol. The normal first invocation seals the candidate, deploys only to preproduction, and stops at `awaiting_production_confirmation`; a later invocation with `--confirm-production` promotes it. For an explicitly authorized emergency, supplying `--confirm-production` on the first invocation runs both phases in one command without bypassing preproduction, CAS, evidence, or production confirmation. Candidate sealing covers source, `public/`, build controls/scripts, stable environment/resource bindings, and target-specific hashed Runtime artifacts. Both deployments upload those artifacts with `--no-build`; each deployment is completed with evidence and reaches terminal `succeeded`, so it cannot leave the target slot blocked. Unrelated commits may land on authoritative mainline between phases only while the sealed commit remains an ancestor and every sealed input/binding/artifact still validates. Real human acceptance remains the recommended default and `--acceptance-note` records it. For audited historical-lineage adoption or reviewed Backend manifest replacement, the existing paired flags and exact-scope gates remain mandatory. Supported configuration resources use exact `resourceSelectors`; unknown, wildcard, destructive, and genuinely unscoped generic resources remain blocked. Lower-level candidate/deploy/test/promote commands are recovery primitives. Direct `release publish` is retained only for legacy unmanaged workspaces.
|
|
86
86
|
|
|
87
87
|
Exact, non-destructive configuration selectors such as Data Views and permission groups are now sequenced automatically inside the same ship journal instead of requiring separate SDD changes. New forms are idempotently ensured per environment before their immutable FormRelease is staged. Unscoped resources and destructive configuration deletes remain fail-closed.
|
|
88
88
|
|
|
@@ -108,6 +108,8 @@ Parallel tasks develop and test in isolated worktrees, but feature worktrees do
|
|
|
108
108
|
|
|
109
109
|
Because promotion starts from an already-pushed authoritative mainline commit, `openxiangda release integration-status --change <change> --profile <name>` should pass immediately after activation. After a managed ship has ended, `--change` recovers lineage directly from the private `ship.json` or follows its production/preproduction deployment IDs to the matching `execution.json`; a recovery failure names the missing file or field. Run it and `release end`; there is no post-release merge step.
|
|
110
110
|
|
|
111
|
+
Task worktrees are disposable only after the release lifecycle is closed. From the synchronized canonical main checkout, run `openxiangda workspace cleanup` for a dry-run inventory. A worktree is `SAFE` only when it is OpenXiangda/Codex-managed, its commit is contained in live remote mainline, it is clean and unowned, and it has no private candidate/release journal or promotion lease/baseline. `--apply` is bound to the exact hashed dry-run plan, revalidates under the worktree owner lock, and removes only those reviewed entries plus their unchanged local branches. Newly-safe worktrees are left for the next review. Malformed owner state fails closed. Stale records are reported for explicit manual review because the CLI never runs repository-wide `git worktree prune`. Remote branches are retained.
|
|
112
|
+
|
|
111
113
|
`openxiangda task status --change <id>` is the compact, read-only answer to
|
|
112
114
|
“现在到哪一步”:it combines SDD、TaskResult、IntegrationBundle、execution
|
|
113
115
|
journal、lease 和 post-commit outbox,输出进度、ETA、是否已经发生平台写入、
|
|
@@ -175,6 +177,8 @@ L1 quick records are generated in a compact form and should not be expanded into
|
|
|
175
177
|
|
|
176
178
|
Every concurrent task uses its own Git worktree/branch and development change. The canonical main checkout is reserved for integration and release; tasks never stash/restore each other's files to make it publishable. The mainline release coordinator bundles selected approved changes after merge; dependency impact outside the approved scope is reported as a warning rather than silently widening a small release. Live commands still require canonical exact selectors, and app-wide/delete operations need explicit authority.
|
|
177
179
|
|
|
180
|
+
After the mainline release and `release end`, the coordinator runs `workspace cleanup` from canonical main and applies only `SAFE` entries. Dirty/unmerged/owned worktrees and worktrees with private release or promotion state remain blocked for recovery.
|
|
181
|
+
|
|
178
182
|
Agent investigation is bounded to one complete CodeGraph survey plus at most one focused follow-up, and ordinary work loads one domain skill. Unchanged lease/build waits use `task status --watch` and produce updates only on material transitions.
|
|
179
183
|
|
|
180
184
|
Before confirmation, agents may read, inspect, snapshot, dry-run, ask questions, and output/write the architecture document. They must not edit source files, mutate platform resources, publish, deploy, send notifications, or call live write/delete endpoints.
|
|
@@ -570,20 +570,163 @@ function hashWorkspaceFile(cwd, relativePath) {
|
|
|
570
570
|
if (
|
|
571
571
|
!normalized ||
|
|
572
572
|
relative.startsWith('..') ||
|
|
573
|
-
path.isAbsolute(relative)
|
|
574
|
-
!fs.existsSync(absolute) ||
|
|
575
|
-
!fs.statSync(absolute).isFile()
|
|
573
|
+
path.isAbsolute(relative)
|
|
576
574
|
) {
|
|
577
575
|
return null;
|
|
578
576
|
}
|
|
577
|
+
if (!fs.existsSync(absolute)) {
|
|
578
|
+
return {
|
|
579
|
+
path: normalized,
|
|
580
|
+
exists: false,
|
|
581
|
+
size: 0,
|
|
582
|
+
sha256: null,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
if (!fs.statSync(absolute).isFile()) return null;
|
|
579
586
|
const buffer = fs.readFileSync(absolute);
|
|
580
587
|
return {
|
|
581
588
|
path: normalized,
|
|
589
|
+
exists: true,
|
|
582
590
|
size: buffer.length,
|
|
583
591
|
sha256: crypto.createHash('sha256').update(buffer).digest('hex'),
|
|
584
592
|
};
|
|
585
593
|
}
|
|
586
594
|
|
|
595
|
+
function buildCandidateEnvironmentBindings(state) {
|
|
596
|
+
const targets =
|
|
597
|
+
state?.targets && typeof state.targets === 'object'
|
|
598
|
+
? state.targets
|
|
599
|
+
: {};
|
|
600
|
+
return Object.fromEntries(
|
|
601
|
+
Object.entries(targets)
|
|
602
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
603
|
+
.map(([targetName, binding]) => [
|
|
604
|
+
targetName,
|
|
605
|
+
{
|
|
606
|
+
targetName,
|
|
607
|
+
profile: binding?.profile || null,
|
|
608
|
+
environmentId: binding?.environmentId || null,
|
|
609
|
+
kind: binding?.kind || null,
|
|
610
|
+
appType: binding?.appType || null,
|
|
611
|
+
publicOrigin: binding?.publicOrigin || null,
|
|
612
|
+
sideEffectPolicy:
|
|
613
|
+
cloneJsonValue(binding?.sideEffectPolicy || {}) || {},
|
|
614
|
+
revision:
|
|
615
|
+
binding?.revision === undefined
|
|
616
|
+
? null
|
|
617
|
+
: binding.revision,
|
|
618
|
+
resources: cloneJsonValue(binding?.resources || {}) || {},
|
|
619
|
+
},
|
|
620
|
+
])
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function candidateBindingValueContains(expected, current) {
|
|
625
|
+
if (Array.isArray(expected)) {
|
|
626
|
+
return (
|
|
627
|
+
Array.isArray(current) &&
|
|
628
|
+
expected.length === current.length &&
|
|
629
|
+
expected.every((item, index) =>
|
|
630
|
+
candidateBindingValueContains(item, current[index])
|
|
631
|
+
)
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
if (expected && typeof expected === 'object') {
|
|
635
|
+
if (!current || typeof current !== 'object' || Array.isArray(current)) {
|
|
636
|
+
return false;
|
|
637
|
+
}
|
|
638
|
+
return Object.entries(expected).every(([key, value]) =>
|
|
639
|
+
candidateBindingValueContains(value, current[key])
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
return expected === current;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function inspectCandidateEnvironmentBindings(candidate, state) {
|
|
646
|
+
const expected =
|
|
647
|
+
candidate?.sourceBundleManifest?.environmentBindings ||
|
|
648
|
+
candidate?.environmentBindings ||
|
|
649
|
+
{};
|
|
650
|
+
const current = buildCandidateEnvironmentBindings(state);
|
|
651
|
+
const mismatches = [];
|
|
652
|
+
const expectedNames = Object.keys(expected).sort();
|
|
653
|
+
const currentNames = Object.keys(current).sort();
|
|
654
|
+
if (canonicalJson(expectedNames) !== canonicalJson(currentNames)) {
|
|
655
|
+
mismatches.push({
|
|
656
|
+
targetName: '(targets)',
|
|
657
|
+
reason: 'target-set-changed',
|
|
658
|
+
expected: expectedNames,
|
|
659
|
+
current: currentNames,
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
for (const targetName of expectedNames) {
|
|
663
|
+
const expectedBinding = expected[targetName];
|
|
664
|
+
const currentBinding = current[targetName];
|
|
665
|
+
if (!currentBinding) continue;
|
|
666
|
+
for (const field of [
|
|
667
|
+
'targetName',
|
|
668
|
+
'profile',
|
|
669
|
+
'environmentId',
|
|
670
|
+
'kind',
|
|
671
|
+
'appType',
|
|
672
|
+
'publicOrigin',
|
|
673
|
+
'sideEffectPolicy',
|
|
674
|
+
'revision',
|
|
675
|
+
]) {
|
|
676
|
+
if (
|
|
677
|
+
canonicalJson(expectedBinding?.[field]) !==
|
|
678
|
+
canonicalJson(currentBinding?.[field])
|
|
679
|
+
) {
|
|
680
|
+
mismatches.push({
|
|
681
|
+
targetName,
|
|
682
|
+
field,
|
|
683
|
+
reason: 'changed',
|
|
684
|
+
expected: expectedBinding?.[field],
|
|
685
|
+
current: currentBinding?.[field],
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
if (
|
|
690
|
+
!candidateBindingValueContains(
|
|
691
|
+
expectedBinding?.resources || {},
|
|
692
|
+
currentBinding.resources || {}
|
|
693
|
+
)
|
|
694
|
+
) {
|
|
695
|
+
mismatches.push({
|
|
696
|
+
targetName,
|
|
697
|
+
field: 'resources',
|
|
698
|
+
reason: 'sealed-resource-binding-changed',
|
|
699
|
+
expected: expectedBinding?.resources || {},
|
|
700
|
+
current: currentBinding.resources || {},
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return {
|
|
705
|
+
valid: mismatches.length === 0,
|
|
706
|
+
checked: expectedNames.length,
|
|
707
|
+
mismatches,
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function assertCandidateEnvironmentBindingsCurrent(candidate, state) {
|
|
712
|
+
const inspection = inspectCandidateEnvironmentBindings(candidate, state);
|
|
713
|
+
if (inspection.valid) return inspection;
|
|
714
|
+
const error = new Error(
|
|
715
|
+
`CANDIDATE_ENVIRONMENT_BINDINGS_CHANGED: sealed candidate 的环境或资源绑定已变化: ${inspection.mismatches
|
|
716
|
+
.slice(0, 8)
|
|
717
|
+
.map(
|
|
718
|
+
item =>
|
|
719
|
+
`${item.targetName}${
|
|
720
|
+
item.field ? `.${item.field}` : ''
|
|
721
|
+
}(${item.reason})`
|
|
722
|
+
)
|
|
723
|
+
.join(', ')}`
|
|
724
|
+
);
|
|
725
|
+
error.code = 'CANDIDATE_ENVIRONMENT_BINDINGS_CHANGED';
|
|
726
|
+
error.details = inspection;
|
|
727
|
+
throw error;
|
|
728
|
+
}
|
|
729
|
+
|
|
587
730
|
function buildCandidateBundle(input) {
|
|
588
731
|
const cwd = path.resolve(input.cwd || process.cwd());
|
|
589
732
|
const files = Array.from(new Set(input.files || []))
|
|
@@ -610,10 +753,17 @@ function buildCandidateBundle(input) {
|
|
|
610
753
|
sourceRevision: input.sourceRevision,
|
|
611
754
|
runtimeMode: input.runtimeMode,
|
|
612
755
|
targets: input.targets,
|
|
756
|
+
inputPolicy: 'candidate-inputs-v2',
|
|
613
757
|
files,
|
|
758
|
+
environmentBindings:
|
|
759
|
+
cloneJsonValue(input.environmentBindings || {}) || {},
|
|
760
|
+
runtimeArtifacts:
|
|
761
|
+
cloneJsonValue(input.runtimeArtifacts || []) || [],
|
|
614
762
|
testPlanHash: sha256Canonical(testPlan),
|
|
615
763
|
prepublishVerificationHash:
|
|
616
764
|
input.prepublishVerificationHash || null,
|
|
765
|
+
candidatePreflightHash:
|
|
766
|
+
input.candidatePreflightHash || null,
|
|
617
767
|
openxiangdaVersion: input.openxiangdaVersion,
|
|
618
768
|
};
|
|
619
769
|
return {
|
|
@@ -624,6 +774,75 @@ function buildCandidateBundle(input) {
|
|
|
624
774
|
};
|
|
625
775
|
}
|
|
626
776
|
|
|
777
|
+
function inspectCandidateBundleFiles(candidate, cwd = process.cwd()) {
|
|
778
|
+
const manifest =
|
|
779
|
+
candidate?.sourceBundleManifest &&
|
|
780
|
+
typeof candidate.sourceBundleManifest === 'object'
|
|
781
|
+
? candidate.sourceBundleManifest
|
|
782
|
+
: candidate;
|
|
783
|
+
const expectedFiles = Array.isArray(manifest?.files)
|
|
784
|
+
? manifest.files
|
|
785
|
+
: [];
|
|
786
|
+
const mismatches = [];
|
|
787
|
+
const seen = new Set();
|
|
788
|
+
for (const expected of expectedFiles) {
|
|
789
|
+
const file = String(expected?.path || '').replace(/\\/g, '/');
|
|
790
|
+
if (!file || seen.has(file)) {
|
|
791
|
+
mismatches.push({
|
|
792
|
+
path: file || '(missing path)',
|
|
793
|
+
reason: seen.has(file) ? 'duplicate' : 'invalid',
|
|
794
|
+
});
|
|
795
|
+
continue;
|
|
796
|
+
}
|
|
797
|
+
seen.add(file);
|
|
798
|
+
const current = hashWorkspaceFile(path.resolve(cwd), file);
|
|
799
|
+
const expectedExists = expected.exists !== false;
|
|
800
|
+
if (!current || (expectedExists && current.exists === false)) {
|
|
801
|
+
mismatches.push({ path: file, reason: 'missing' });
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
if (!expectedExists) {
|
|
805
|
+
if (current.exists !== false) {
|
|
806
|
+
mismatches.push({ path: file, reason: 'reappeared', current });
|
|
807
|
+
}
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
if (
|
|
811
|
+
current.size !== Number(expected.size) ||
|
|
812
|
+
current.sha256 !== String(expected.sha256 || '').toLowerCase()
|
|
813
|
+
) {
|
|
814
|
+
mismatches.push({
|
|
815
|
+
path: file,
|
|
816
|
+
reason: 'changed',
|
|
817
|
+
expected: {
|
|
818
|
+
size: Number(expected.size),
|
|
819
|
+
sha256: String(expected.sha256 || '').toLowerCase(),
|
|
820
|
+
},
|
|
821
|
+
current,
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
return {
|
|
826
|
+
valid: mismatches.length === 0,
|
|
827
|
+
checked: expectedFiles.length,
|
|
828
|
+
mismatches,
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function assertCandidateBundleFilesCurrent(candidate, cwd = process.cwd()) {
|
|
833
|
+
const inspection = inspectCandidateBundleFiles(candidate, cwd);
|
|
834
|
+
if (inspection.valid) return inspection;
|
|
835
|
+
const error = new Error(
|
|
836
|
+
`CANDIDATE_INPUTS_CHANGED: sealed candidate 的发布输入已变化: ${inspection.mismatches
|
|
837
|
+
.slice(0, 8)
|
|
838
|
+
.map(item => `${item.path}(${item.reason})`)
|
|
839
|
+
.join(', ')}`
|
|
840
|
+
);
|
|
841
|
+
error.code = 'CANDIDATE_INPUTS_CHANGED';
|
|
842
|
+
error.details = inspection;
|
|
843
|
+
throw error;
|
|
844
|
+
}
|
|
845
|
+
|
|
627
846
|
function releaseExecutionPath(changeId, deploymentId, cwd = process.cwd()) {
|
|
628
847
|
const base = path.join(
|
|
629
848
|
cwd,
|
|
@@ -706,7 +925,10 @@ function isRecoverablePostActivationDeploymentError(error, deployment) {
|
|
|
706
925
|
}
|
|
707
926
|
|
|
708
927
|
module.exports = {
|
|
928
|
+
assertCandidateBundleFilesCurrent,
|
|
929
|
+
assertCandidateEnvironmentBindingsCurrent,
|
|
709
930
|
bindEnvironmentTarget,
|
|
931
|
+
buildCandidateEnvironmentBindings,
|
|
710
932
|
buildSideEffectPolicyDiff,
|
|
711
933
|
buildCandidateBundle,
|
|
712
934
|
canonicalJson,
|
|
@@ -718,6 +940,8 @@ module.exports = {
|
|
|
718
940
|
normalizeSideEffectPolicyInput,
|
|
719
941
|
normalizeTargetName,
|
|
720
942
|
hasStateResourceMappings,
|
|
943
|
+
inspectCandidateBundleFiles,
|
|
944
|
+
inspectCandidateEnvironmentBindings,
|
|
721
945
|
managedTargetDeploymentFile,
|
|
722
946
|
readCandidate,
|
|
723
947
|
readJsonInput,
|