openxiangda 1.0.268 → 1.0.269
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 +18 -1
- package/lib/cli.js +40 -24
- package/lib/config.js +60 -56
- package/lib/migration-advice.js +19 -0
- package/lib/workspace-init.js +11 -4
- package/openxiangda-skills/SKILL.md +6 -0
- package/openxiangda-skills/references/architecture-patterns.md +218 -218
- package/openxiangda-skills/references/component-guide.md +122 -122
- package/openxiangda-skills/references/platform-data-model.md +285 -285
- package/openxiangda-skills/references/troubleshooting.md +224 -224
- package/openxiangda-skills/references/workspace-state.md +1 -1
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +1 -1
- package/package.json +23 -22
- package/packages/sdk/dist/{ProcessPreview-Cyk6uv-w.d.mts → ProcessPreview-DSUIJi5V.d.mts} +2 -3
- package/packages/sdk/dist/{ProcessPreview-Cyk6uv-w.d.ts → ProcessPreview-DSUIJi5V.d.ts} +2 -3
- package/packages/sdk/dist/components/index.d.mts +41 -42
- package/packages/sdk/dist/components/index.d.ts +41 -42
- package/packages/sdk/dist/{dataManagementApi-CE8Zyj3a.d.ts → dataManagementApi-Ro9itwm8.d.ts} +2 -2
- package/packages/sdk/dist/{dataManagementApi-4fSaCA5t.d.mts → dataManagementApi-p_HOhQnA.d.mts} +2 -2
- package/packages/sdk/dist/runtime/index.d.mts +3 -4
- package/packages/sdk/dist/runtime/index.d.ts +3 -4
- package/packages/sdk/dist/runtime/react.d.mts +5 -5
- package/packages/sdk/dist/runtime/react.d.ts +5 -5
- package/packages/sdk/src/build-source/scripts/utils/mime-types.mjs +70 -70
- package/packages/sdk/src/build-source/scripts/utils/oss-client.mjs +128 -128
- package/packages/sdk/src/build-source/scripts/utils/progress.mjs +57 -57
- 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 +6 -0
- package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda.mdc +7 -1
- package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda.md +7 -1
- package/templates/sy-lowcode-app-workspace/AGENTS.md +7 -1
package/README.md
CHANGED
|
@@ -22,6 +22,21 @@ Normal OpenXiangda app development uses platform-user login tokens through `/ope
|
|
|
22
22
|
|
|
23
23
|
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://platform.example.com/` to the CLI is supported; OpenXiangda stores the API base as `https://platform.example.com/service`.
|
|
24
24
|
|
|
25
|
+
## 升级到 OpenXiangda 2.0
|
|
26
|
+
|
|
27
|
+
建议评估升级到 OpenXiangda 2.0;新应用优先使用 V2,现有应用先确认能力覆盖、迁移成本与验收方案。准备 Node.js 24+,安装最新统一入口:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install -g openxiangda@latest --registry=https://registry.npmjs.org
|
|
31
|
+
# 进入原 V1 项目,使用新版全局入口
|
|
32
|
+
openxiangda version --json
|
|
33
|
+
openxiangda migrate assess --to v2 --json
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`latest` / `stable-v2` 是 V2 稳定渠道,`legacy-v1` 是 V1 维护渠道。统一入口继续用原项目的 V1 引擎;不会自动迁移配置、数据或流程。V1 项目用 `openxiangda update install --target workspace` 更新同代维护版;全局入口用 `--target launcher` 更新。项目锁定的 V1 CLI 不提供 `migrate` 命令,评估时使用新版全局命令。
|
|
37
|
+
|
|
38
|
+
版本查询、更新检查/安装、Skill 安装后会展示建议;JSON 调用通过 `migrationAdvice` 字段提供同样信息。CLI、Skill、MCP 如何安装、刷新以及配置客户端,见[统一安装升级说明](https://github.com/1377385356/openxiangda/blob/master/docs/getting-started.md#upgrade)。工具更新与应用迁移分别执行。
|
|
39
|
+
|
|
25
40
|
## OpenXiangda 1.x Delivery V2 (maintenance)
|
|
26
41
|
|
|
27
42
|
New workspaces declare `deliveryVersion: 2`. The release flow is intentionally
|
|
@@ -150,7 +165,7 @@ openxiangda inspect app --profile dev --json
|
|
|
150
165
|
openxiangda app snapshot APP_XXXX --profile dev --json
|
|
151
166
|
```
|
|
152
167
|
|
|
153
|
-
User tokens are stored in
|
|
168
|
+
User tokens are stored in the current workspace `.openxiangda/profiles.json` with `0600` permissions and automatic Git ignore rules. Global profiles are no longer read or merged; sign in again in each project after upgrading. Use `login --cwd <directory>` to select a new project explicitly. 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.
|
|
154
169
|
|
|
155
170
|
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`; Backend, Runtime, and Root children all retain the sealed candidate `sourceRevision`, even when promotion starts from a later clean descendant mainline commit. 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/fail/promote commands are recovery primitives. `release fail` requires an explicit preproduction target, deployment ID, and audit message; it verifies the deployment belongs to that preproduction environment before writing optional code/details to the platform failure audit. Direct `release publish` is retained only for legacy unmanaged workspaces.
|
|
156
171
|
|
|
@@ -685,3 +700,5 @@ Skill migration plan: [docs/skill-refactor-plan.md](docs/skill-refactor-plan.md)
|
|
|
685
700
|
Release isolation and speed roadmap: [docs/release-isolation-and-speed-roadmap.md](docs/release-isolation-and-speed-roadmap.md).
|
|
686
701
|
|
|
687
702
|
Reproduced concurrency incidents and regression invariants: [docs/release-concurrency-incident-matrix.md](docs/release-concurrency-incident-matrix.md).
|
|
703
|
+
|
|
704
|
+
平台登录态只使用当前工作区的 `.openxiangda/profiles.json`,不再读取或合并用户主目录的全局 profiles。升级后请进入每个项目运行 `openxiangda login <platform-url>`;子目录沿最近应用根目录定位,不跨嵌套应用或 Git 边界。登录文件及临时文件会自动加入忽略规则,请勿提交或打包。新项目先用 `openxiangda login <platform-url> --cwd <directory>` 在目标目录登录,再在该目录运行 `openxiangda workspace init`。
|
package/lib/cli.js
CHANGED
|
@@ -7,7 +7,7 @@ const { spawn, spawnSync } = require('child_process');
|
|
|
7
7
|
const { pathToFileURL } = require('url');
|
|
8
8
|
const esbuild = require('esbuild');
|
|
9
9
|
const {
|
|
10
|
-
|
|
10
|
+
getConfigFile,
|
|
11
11
|
GLOBAL_ENV_FILE,
|
|
12
12
|
PROJECT_STATE_FILE,
|
|
13
13
|
getProfile,
|
|
@@ -115,6 +115,7 @@ const {
|
|
|
115
115
|
} = require('./resource-binding-contract');
|
|
116
116
|
const { getSkillStatusReport, installSkills } = require('./skills');
|
|
117
117
|
const { resolveUpdateTarget, buildGenerationUpdateCommand } = require('./generation-update');
|
|
118
|
+
const { migrationAdvice, printMigrationAdvice } = require('./migration-advice');
|
|
118
119
|
const {
|
|
119
120
|
assertOrClaimWorktreeOwner,
|
|
120
121
|
claimWorktreeOwner,
|
|
@@ -233,7 +234,12 @@ const genericGitBasePlanCache = new Map();
|
|
|
233
234
|
async function main(argv) {
|
|
234
235
|
let commandError = null;
|
|
235
236
|
try {
|
|
236
|
-
|
|
237
|
+
const result = await mainImpl(argv);
|
|
238
|
+
const [command, action] = argv;
|
|
239
|
+
if (!argv.some(value => value === '--json' || value.startsWith('--json=')) && (!command || ['version', '--version', '-v', 'help', '--help', '-h', 'update'].includes(command) || command === 'skill' && ['install', 'bootstrap'].includes(action))) {
|
|
240
|
+
printMigrationAdvice();
|
|
241
|
+
}
|
|
242
|
+
return result;
|
|
237
243
|
} catch (error) {
|
|
238
244
|
commandError = error;
|
|
239
245
|
throw error;
|
|
@@ -319,12 +325,12 @@ function printHelp() {
|
|
|
319
325
|
|
|
320
326
|
Usage:
|
|
321
327
|
openxiangda version [--json]
|
|
322
|
-
openxiangda login <platform-url> [--profile name]
|
|
323
|
-
openxiangda update check|install [--json] [--registry https://registry.npmjs.org]
|
|
328
|
+
openxiangda login <platform-url> [--profile name] [--cwd directory]
|
|
329
|
+
openxiangda update check|install [--target workspace|launcher] [--json] [--registry https://registry.npmjs.org]
|
|
324
330
|
openxiangda platform add <name> <platform-url>
|
|
325
331
|
openxiangda platform list
|
|
326
332
|
openxiangda platform use <name>
|
|
327
|
-
openxiangda auth status|refresh|logout [--profile name]
|
|
333
|
+
openxiangda auth status|refresh|logout [--profile name] [--cwd directory]
|
|
328
334
|
openxiangda doctor [--profile name] [--app-type APP_XXX] [--release] [--json]
|
|
329
335
|
openxiangda contract list|explain <contract-name> [--json]
|
|
330
336
|
openxiangda policy show|check|render [--json]
|
|
@@ -519,6 +525,7 @@ async function version(args) {
|
|
|
519
525
|
atomicActivation: APP_FUNCTION_SECRET_CAPABILITIES.atomicActivation,
|
|
520
526
|
},
|
|
521
527
|
updateCheckCommand: 'openxiangda update check --json',
|
|
528
|
+
migrationAdvice: migrationAdvice(),
|
|
522
529
|
};
|
|
523
530
|
if (flags.json) return writeJson(result);
|
|
524
531
|
print(CURRENT_VERSION);
|
|
@@ -529,7 +536,7 @@ async function update(args) {
|
|
|
529
536
|
const parsedArgs = requestedSubcommand === args[0] ? args.slice(1) : args;
|
|
530
537
|
const { flags } = parseArgs(parsedArgs);
|
|
531
538
|
if (wantsSubcommandHelp(requestedSubcommand, flags)) {
|
|
532
|
-
print('用法: openxiangda update check|install [--json] [--registry https://registry.npmjs.org] [--timeout-seconds 300] [--no-skills]');
|
|
539
|
+
print('用法: openxiangda update check|install [--target workspace|launcher] [--json] [--registry https://registry.npmjs.org] [--timeout-seconds 300] [--no-skills]');
|
|
533
540
|
return;
|
|
534
541
|
}
|
|
535
542
|
const registry = normalizeNpmRegistry(flags.registry || OFFICIAL_NPM_REGISTRY);
|
|
@@ -557,7 +564,7 @@ async function update(args) {
|
|
|
557
564
|
return;
|
|
558
565
|
}
|
|
559
566
|
|
|
560
|
-
fail('用法: openxiangda update check|install [--json] [--registry https://registry.npmjs.org] [--timeout-seconds 300] [--no-skills]');
|
|
567
|
+
fail('用法: openxiangda update check|install [--target workspace|launcher] [--json] [--registry https://registry.npmjs.org] [--timeout-seconds 300] [--no-skills]');
|
|
561
568
|
}
|
|
562
569
|
|
|
563
570
|
function normalizeNpmRegistry(value) {
|
|
@@ -586,6 +593,7 @@ function checkOpenXiangdaUpdate(registry, selection = resolveUpdateTarget()) {
|
|
|
586
593
|
skillInstallCommand: 'openxiangda skill install',
|
|
587
594
|
compatibilityCheckCommand: 'openxiangda commands --json',
|
|
588
595
|
checkedAt: new Date().toISOString(),
|
|
596
|
+
migrationAdvice: migrationAdvice(),
|
|
589
597
|
notes: [
|
|
590
598
|
'Use the official npm registry for OpenXiangda updates; domestic mirrors may lag.',
|
|
591
599
|
'After updating the package, refresh OpenXiangda skills so AI guidance matches the CLI version.',
|
|
@@ -766,6 +774,7 @@ async function installOpenXiangdaUpdate(registry, options = {}) {
|
|
|
766
774
|
registry: plan.registry,
|
|
767
775
|
installCommand: `npm ${npmArgs.join(' ')}`,
|
|
768
776
|
installed: true,
|
|
777
|
+
migrationAdvice: migrationAdvice(),
|
|
769
778
|
durationMs: installResult.durationMs,
|
|
770
779
|
cacheMode: 'prefer-offline',
|
|
771
780
|
timeoutSeconds: plan.timeoutSeconds,
|
|
@@ -9444,7 +9453,7 @@ async function platform(args) {
|
|
|
9444
9453
|
print('用法: openxiangda platform add|list|use|remove');
|
|
9445
9454
|
return;
|
|
9446
9455
|
}
|
|
9447
|
-
const config = loadConfig();
|
|
9456
|
+
const config = loadConfig(flags.cwd || process.cwd(), { exact: Boolean(flags.cwd) });
|
|
9448
9457
|
|
|
9449
9458
|
if (subcommand === 'prepare') {
|
|
9450
9459
|
const result = prepareDependencyCapsule({
|
|
@@ -9549,10 +9558,10 @@ async function login(args) {
|
|
|
9549
9558
|
positional[0] === '--help' ||
|
|
9550
9559
|
positional[0] === '-h'
|
|
9551
9560
|
) {
|
|
9552
|
-
print('用法: openxiangda login <platform-url> [--profile name] [--no-open] [--json]');
|
|
9561
|
+
print('用法: openxiangda login <platform-url> [--profile name] [--cwd directory] [--no-open] [--json]');
|
|
9553
9562
|
return;
|
|
9554
9563
|
}
|
|
9555
|
-
const config = loadConfig();
|
|
9564
|
+
const config = loadConfig(flags.cwd || process.cwd(), { exact: Boolean(flags.cwd) });
|
|
9556
9565
|
const rawUrl = positional[0];
|
|
9557
9566
|
const profileName = flags.profile || config.currentProfile || 'default';
|
|
9558
9567
|
let profile = config.profiles[profileName] || { name: profileName };
|
|
@@ -9567,7 +9576,7 @@ async function login(args) {
|
|
|
9567
9576
|
}
|
|
9568
9577
|
|
|
9569
9578
|
if (!profile.baseUrl) {
|
|
9570
|
-
fail('缺少平台地址。用法: openxiangda login <platform-url> [--profile name]');
|
|
9579
|
+
fail('缺少平台地址。用法: openxiangda login <platform-url> [--profile name] [--cwd directory]');
|
|
9571
9580
|
}
|
|
9572
9581
|
|
|
9573
9582
|
const sessionPayload = await requestJson(
|
|
@@ -9666,12 +9675,12 @@ async function auth(args) {
|
|
|
9666
9675
|
const [subcommand, ...rest] = args;
|
|
9667
9676
|
const { flags } = parseArgs(rest);
|
|
9668
9677
|
if (wantsSubcommandHelp(subcommand, flags)) {
|
|
9669
|
-
print('用法: openxiangda auth status|refresh|logout [--profile name] [--json]');
|
|
9678
|
+
print('用法: openxiangda auth status|refresh|logout [--profile name] [--cwd directory] [--json]');
|
|
9670
9679
|
return;
|
|
9671
9680
|
}
|
|
9672
9681
|
|
|
9673
9682
|
if (subcommand === 'status') {
|
|
9674
|
-
const config = loadConfig();
|
|
9683
|
+
const config = loadConfig(flags.cwd || process.cwd(), { exact: Boolean(flags.cwd) });
|
|
9675
9684
|
const { profileName, profile } = getProfile(config, flags.profile);
|
|
9676
9685
|
if (!profile.token?.accessToken) {
|
|
9677
9686
|
print(`未登录: ${profileName} (${profile.baseUrl})`);
|
|
@@ -9689,7 +9698,7 @@ async function auth(args) {
|
|
|
9689
9698
|
}
|
|
9690
9699
|
|
|
9691
9700
|
if (subcommand === 'refresh') {
|
|
9692
|
-
const config = loadConfig();
|
|
9701
|
+
const config = loadConfig(flags.cwd || process.cwd(), { exact: Boolean(flags.cwd) });
|
|
9693
9702
|
const profileName = flags.profile || config.currentProfile;
|
|
9694
9703
|
if (!profileName) fail('未选择平台 profile');
|
|
9695
9704
|
const data = await refreshProfile(config, profileName);
|
|
@@ -9700,7 +9709,7 @@ async function auth(args) {
|
|
|
9700
9709
|
}
|
|
9701
9710
|
|
|
9702
9711
|
if (subcommand === 'logout') {
|
|
9703
|
-
const config = loadConfig();
|
|
9712
|
+
const config = loadConfig(flags.cwd || process.cwd(), { exact: Boolean(flags.cwd) });
|
|
9704
9713
|
const { profileName, profile } = getProfile(config, flags.profile);
|
|
9705
9714
|
if (profile.token?.accessToken) {
|
|
9706
9715
|
try {
|
|
@@ -9718,7 +9727,7 @@ async function auth(args) {
|
|
|
9718
9727
|
return;
|
|
9719
9728
|
}
|
|
9720
9729
|
|
|
9721
|
-
fail('用法: openxiangda auth status|refresh|logout [--profile name]');
|
|
9730
|
+
fail('用法: openxiangda auth status|refresh|logout [--profile name] [--cwd directory]');
|
|
9722
9731
|
}
|
|
9723
9732
|
|
|
9724
9733
|
async function env(args) {
|
|
@@ -9735,7 +9744,8 @@ async function env(args) {
|
|
|
9735
9744
|
const currentState = profileName ? state.profiles?.[profileName] : null;
|
|
9736
9745
|
const oss = summarizeOssEnv(globalEnv);
|
|
9737
9746
|
const data = {
|
|
9738
|
-
configFile:
|
|
9747
|
+
configFile: getConfigFile(config),
|
|
9748
|
+
sessionScope: 'workspace',
|
|
9739
9749
|
globalEnvFile: GLOBAL_ENV_FILE,
|
|
9740
9750
|
globalEnvExists: fs.existsSync(GLOBAL_ENV_FILE),
|
|
9741
9751
|
projectStateFile: path.join(process.cwd(), PROJECT_STATE_FILE),
|
|
@@ -10198,7 +10208,9 @@ async function workspace(args) {
|
|
|
10198
10208
|
printWorkspaceHelp();
|
|
10199
10209
|
return;
|
|
10200
10210
|
}
|
|
10201
|
-
const config =
|
|
10211
|
+
const config = subcommand === 'init'
|
|
10212
|
+
? loadConfig(path.resolve(positional[0] || process.cwd()), { exact: true })
|
|
10213
|
+
: loadConfig();
|
|
10202
10214
|
|
|
10203
10215
|
if (subcommand === 'ownership' || subcommand === 'owner') {
|
|
10204
10216
|
const action = positional[0] || 'status';
|
|
@@ -17064,7 +17076,7 @@ async function commands(args) {
|
|
|
17064
17076
|
commands: [
|
|
17065
17077
|
'version [--json]',
|
|
17066
17078
|
'login <platform-url> [--profile name]',
|
|
17067
|
-
'update check|install',
|
|
17079
|
+
'update check|install [--target workspace|launcher] [--json]',
|
|
17068
17080
|
'platform add|list|use|remove',
|
|
17069
17081
|
'auth status|refresh|logout',
|
|
17070
17082
|
'doctor [--profile name] [--app-type APP_XXX] [--release]',
|
|
@@ -17160,6 +17172,7 @@ async function skill(args) {
|
|
|
17160
17172
|
|
|
17161
17173
|
if (subcommand === 'install') {
|
|
17162
17174
|
const result = installSkills(options);
|
|
17175
|
+
result.migrationAdvice = migrationAdvice();
|
|
17163
17176
|
if (flags.json) return writeJson(result);
|
|
17164
17177
|
printSkillInstallReport(result);
|
|
17165
17178
|
return;
|
|
@@ -17178,6 +17191,7 @@ async function skill(args) {
|
|
|
17178
17191
|
force: Boolean(flags.force),
|
|
17179
17192
|
dryRun: Boolean(flags['dry-run']),
|
|
17180
17193
|
});
|
|
17194
|
+
result.migrationAdvice = migrationAdvice();
|
|
17181
17195
|
if (flags.json) return writeJson(result);
|
|
17182
17196
|
printSkillBootstrapReport(result);
|
|
17183
17197
|
return;
|
|
@@ -22492,7 +22506,7 @@ function runGenericResourceGitBasePreflight({
|
|
|
22492
22506
|
copyProjectState: true,
|
|
22493
22507
|
},
|
|
22494
22508
|
({ buildRoot, buildEnv, tempRoot }) => {
|
|
22495
|
-
const isolatedHome = prepareReadonlyGitBasePlanHome(tempRoot);
|
|
22509
|
+
const isolatedHome = prepareReadonlyGitBasePlanHome(tempRoot, buildRoot);
|
|
22496
22510
|
const childEnv = {
|
|
22497
22511
|
...buildEnv,
|
|
22498
22512
|
HOME: isolatedHome,
|
|
@@ -22628,8 +22642,9 @@ function runGenericResourceGitBasePreflight({
|
|
|
22628
22642
|
};
|
|
22629
22643
|
}
|
|
22630
22644
|
|
|
22631
|
-
function prepareReadonlyGitBasePlanHome(tempRoot) {
|
|
22632
|
-
|
|
22645
|
+
function prepareReadonlyGitBasePlanHome(tempRoot, buildRoot) {
|
|
22646
|
+
const configSource = getConfigFile();
|
|
22647
|
+
if (!fs.existsSync(configSource)) {
|
|
22633
22648
|
const error = new Error(
|
|
22634
22649
|
'SOURCE_BASE_PLAN_FAILED: 当前 profile 配置不存在,无法执行隔离 Git 基线 plan'
|
|
22635
22650
|
);
|
|
@@ -22637,10 +22652,11 @@ function prepareReadonlyGitBasePlanHome(tempRoot) {
|
|
|
22637
22652
|
throw error;
|
|
22638
22653
|
}
|
|
22639
22654
|
const home = path.join(tempRoot, 'plan-home');
|
|
22640
|
-
const configDir = path.join(
|
|
22655
|
+
const configDir = path.join(buildRoot, '.openxiangda');
|
|
22656
|
+
fs.mkdirSync(home, { recursive: true, mode: 0o700 });
|
|
22641
22657
|
const configFile = path.join(configDir, 'profiles.json');
|
|
22642
22658
|
fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
|
|
22643
|
-
fs.copyFileSync(
|
|
22659
|
+
fs.copyFileSync(configSource, configFile);
|
|
22644
22660
|
try {
|
|
22645
22661
|
fs.chmodSync(configFile, 0o400);
|
|
22646
22662
|
} catch {
|
package/lib/config.js
CHANGED
|
@@ -9,7 +9,6 @@ const CONFIG_FILE = path.join(CONFIG_DIR, 'profiles.json');
|
|
|
9
9
|
const GLOBAL_ENV_FILE = path.join(CONFIG_DIR, '.env');
|
|
10
10
|
const PROJECT_DIR = '.openxiangda';
|
|
11
11
|
const PROJECT_STATE_FILE = path.join(PROJECT_DIR, 'state.json');
|
|
12
|
-
const LEGACY_PROJECT_CONFIG_FILE = path.join(PROJECT_DIR, 'profiles.json');
|
|
13
12
|
const PROJECT_STATE_LOCK_TIMEOUT_MS = 15_000;
|
|
14
13
|
const PROJECT_STATE_STALE_LOCK_MS = 30_000;
|
|
15
14
|
const projectStateBaselines = new WeakMap();
|
|
@@ -47,74 +46,78 @@ function normalizeConfig(config) {
|
|
|
47
46
|
};
|
|
48
47
|
}
|
|
49
48
|
|
|
50
|
-
|
|
51
|
-
const merged = emptyConfig();
|
|
52
|
-
for (const config of configs) {
|
|
53
|
-
if (!config) continue;
|
|
54
|
-
const normalized = normalizeConfig(config);
|
|
55
|
-
merged.version = normalized.version || merged.version;
|
|
56
|
-
if (normalized.currentProfile) {
|
|
57
|
-
merged.currentProfile = normalized.currentProfile;
|
|
58
|
-
}
|
|
59
|
-
for (const [name, profile] of Object.entries(normalized.profiles)) {
|
|
60
|
-
merged.profiles[name] = mergeProfile(merged.profiles[name], profile);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
return normalizeConfig(merged);
|
|
64
|
-
}
|
|
49
|
+
const configPaths = new WeakMap();
|
|
65
50
|
|
|
66
|
-
function
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
previous[key] != null
|
|
78
|
-
) {
|
|
79
|
-
merged[key] = previous[key];
|
|
80
|
-
}
|
|
51
|
+
function workspaceRoot(cwd = process.cwd()) {
|
|
52
|
+
const start = path.resolve(cwd);
|
|
53
|
+
if (!fs.existsSync(start)) return start;
|
|
54
|
+
let directory = start;
|
|
55
|
+
while (directory !== os.homedir()) {
|
|
56
|
+
if (['app-workspace.config.ts', 'app-workspace.config.js', 'openxiangda.config.ts', '.openxiangda/profiles.json']
|
|
57
|
+
.some(file => fs.existsSync(path.join(directory, file)))) return directory;
|
|
58
|
+
if (fs.existsSync(path.join(directory, '.git'))) return start;
|
|
59
|
+
const parent = path.dirname(directory);
|
|
60
|
+
if (parent === directory) break;
|
|
61
|
+
directory = parent;
|
|
81
62
|
}
|
|
82
|
-
return
|
|
63
|
+
return start;
|
|
83
64
|
}
|
|
84
65
|
|
|
85
|
-
function
|
|
86
|
-
return
|
|
66
|
+
function getConfigFile(config, cwd = process.cwd()) {
|
|
67
|
+
return configPaths.get(config) || path.join(workspaceRoot(cwd), PROJECT_DIR, 'profiles.json');
|
|
87
68
|
}
|
|
88
69
|
|
|
89
|
-
function
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
70
|
+
function assertConfigFile(file) {
|
|
71
|
+
if (file === CONFIG_FILE) throw new Error('请进入项目工作区后登录;不再使用用户主目录的全局登录态');
|
|
72
|
+
for (const target of [path.dirname(file), file]) {
|
|
73
|
+
try {
|
|
74
|
+
const info = fs.lstatSync(target);
|
|
75
|
+
if (info.isSymbolicLink() || (target === file && (!info.isFile() || info.size > 1024 * 1024))) {
|
|
76
|
+
throw new Error(`工作区登录态必须是普通文件: ${file}`);
|
|
77
|
+
}
|
|
78
|
+
} catch (error) {
|
|
79
|
+
if (error.code !== 'ENOENT') throw error;
|
|
98
80
|
}
|
|
99
|
-
return migrated;
|
|
100
81
|
}
|
|
82
|
+
}
|
|
101
83
|
|
|
102
|
-
|
|
84
|
+
function loadConfig(cwd = process.cwd(), options = {}) {
|
|
85
|
+
const file = options.exact
|
|
86
|
+
? path.join(path.resolve(cwd), PROJECT_DIR, 'profiles.json')
|
|
87
|
+
: getConfigFile(null, cwd);
|
|
88
|
+
assertConfigFile(file);
|
|
89
|
+
let config;
|
|
90
|
+
try {
|
|
91
|
+
config = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
92
|
+
if (!config || typeof config !== 'object' || Array.isArray(config) ||
|
|
93
|
+
!config.profiles || typeof config.profiles !== 'object' || Array.isArray(config.profiles)) {
|
|
94
|
+
throw new Error('invalid configuration');
|
|
95
|
+
}
|
|
96
|
+
} catch (error) {
|
|
97
|
+
if (error.code !== 'ENOENT') throw new Error(`工作区登录态无法解析,请在当前工作区重新登录: ${file}`);
|
|
98
|
+
}
|
|
99
|
+
const result = normalizeConfig(config);
|
|
100
|
+
configPaths.set(result, file);
|
|
101
|
+
return result;
|
|
103
102
|
}
|
|
104
103
|
|
|
105
104
|
function saveConfig(config) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
fs.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
105
|
+
const file = getConfigFile(config);
|
|
106
|
+
assertConfigFile(file);
|
|
107
|
+
const directory = path.dirname(file);
|
|
108
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
109
|
+
fs.chmodSync(directory, 0o700);
|
|
110
|
+
const ignore = path.join(directory, '.gitignore');
|
|
111
|
+
let current = '';
|
|
113
112
|
try {
|
|
114
|
-
fs.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
113
|
+
if (!fs.lstatSync(ignore).isFile() || fs.lstatSync(ignore).isSymbolicLink()) throw new Error('登录态忽略文件必须是普通文件');
|
|
114
|
+
current = fs.readFileSync(ignore, 'utf8');
|
|
115
|
+
} catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
116
|
+
const missing = ['/profiles.json', '/profiles.json.*'].filter(entry => !current.split(/\r?\n/).includes(entry));
|
|
117
|
+
if (missing.length) fs.writeFileSync(ignore, `${current}${current && !current.endsWith('\n') ? '\n' : ''}${missing.join('\n')}\n`, { mode: 0o600 });
|
|
118
|
+
atomicWriteJson(file, normalizeConfig(config));
|
|
119
|
+
fs.chmodSync(file, 0o600);
|
|
120
|
+
configPaths.set(config, file);
|
|
118
121
|
}
|
|
119
122
|
|
|
120
123
|
function normalizeBaseUrl(value) {
|
|
@@ -441,6 +444,7 @@ module.exports = {
|
|
|
441
444
|
GLOBAL_ENV_FILE,
|
|
442
445
|
PROJECT_STATE_FILE,
|
|
443
446
|
getProfile,
|
|
447
|
+
getConfigFile,
|
|
444
448
|
loadGlobalEnv,
|
|
445
449
|
loadConfig,
|
|
446
450
|
loadProjectState,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Static guidance: no registry requests, installation or workspace mutations.
|
|
2
|
+
function migrationAdvice() {
|
|
3
|
+
return {
|
|
4
|
+
generation: 'v1',
|
|
5
|
+
message: '建议评估升级到 OpenXiangda 2.0;新应用优先使用 V2,现有应用先核实能力覆盖、迁移成本与验收方案。',
|
|
6
|
+
launcherInstallCommand: 'npm install -g openxiangda@latest --registry=https://registry.npmjs.org',
|
|
7
|
+
nodeRequirement: '>=24',
|
|
8
|
+
assessCommand: 'openxiangda migrate assess --to v2',
|
|
9
|
+
guide: 'https://github.com/1377385356/openxiangda/blob/master/docs/getting-started.md#upgrade',
|
|
10
|
+
workspacePolicy: '更新统一入口后旧项目仍使用 V1;不会自动转换应用、数据或流程。',
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function printMigrationAdvice() {
|
|
15
|
+
const advice = migrationAdvice();
|
|
16
|
+
process.stderr.write(`\n${advice.message}\nNode.js 24+:${advice.launcherInstallCommand}\n使用新版全局入口在旧项目评估:${advice.assessCommand}\n${advice.workspacePolicy}\nCLI / Skill / MCP 安装升级说明:${advice.guide}\n`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = { migrationAdvice, printMigrationAdvice };
|
package/lib/workspace-init.js
CHANGED
|
@@ -41,6 +41,7 @@ function initWorkspace(options = {}) {
|
|
|
41
41
|
throw new Error('workspace init 绑定应用时必须同时提供 --profile 和 --app-type');
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
const resolvedProfile = profileName ? getProfile(loadConfig(targetDir, { exact: true }), profileName) : null;
|
|
44
45
|
ensureCanInitialize(targetDir, force, templateDir);
|
|
45
46
|
copyTemplate(templateDir, targetDir, {
|
|
46
47
|
__WORKSPACE_PACKAGE_NAME__: packageName,
|
|
@@ -49,8 +50,7 @@ function initWorkspace(options = {}) {
|
|
|
49
50
|
|
|
50
51
|
let bound = null;
|
|
51
52
|
if (profileName && appType) {
|
|
52
|
-
const
|
|
53
|
-
const resolved = getProfile(config, profileName);
|
|
53
|
+
const resolved = resolvedProfile;
|
|
54
54
|
const state = {
|
|
55
55
|
version: 1,
|
|
56
56
|
profiles: {
|
|
@@ -125,11 +125,18 @@ function ensureCanInitialize(targetDir, force, templateDir) {
|
|
|
125
125
|
const entries = fs
|
|
126
126
|
.readdirSync(targetDir)
|
|
127
127
|
.filter(name => !['.DS_Store'].includes(name));
|
|
128
|
-
if (entries.length > 0 && !force) {
|
|
128
|
+
if (entries.length > 0 && !force && !isAuthOnlyWorkspace(targetDir, entries)) {
|
|
129
129
|
throw new Error(`目标目录非空: ${targetDir}。如需写入请传 --force`);
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
function isAuthOnlyWorkspace(targetDir, entries) {
|
|
134
|
+
if (entries.length !== 1 || entries[0] !== '.openxiangda') return false;
|
|
135
|
+
const directory = path.join(targetDir, '.openxiangda');
|
|
136
|
+
if (!fs.lstatSync(directory).isDirectory() || fs.lstatSync(directory).isSymbolicLink()) return false;
|
|
137
|
+
return fs.readdirSync(directory).every(name => ['profiles.json', '.gitignore'].includes(name) && fs.lstatSync(path.join(directory, name)).isFile() && !fs.lstatSync(path.join(directory, name)).isSymbolicLink());
|
|
138
|
+
}
|
|
139
|
+
|
|
133
140
|
function normalizeRuntime(value) {
|
|
134
141
|
return value === 'react-spa' || value === 'spa' ? 'react-spa' : 'legacy';
|
|
135
142
|
}
|
|
@@ -154,7 +161,7 @@ function copyTemplate(sourceDir, targetDir, replacements, baseDir = sourceDir) {
|
|
|
154
161
|
function shouldSkipTemplateEntry(baseDir, sourcePath, entryName) {
|
|
155
162
|
if (TEMPLATE_IGNORE_NAMES.has(entryName)) return true;
|
|
156
163
|
const relativePath = path.relative(baseDir, sourcePath).split(path.sep).join('/');
|
|
157
|
-
return TEMPLATE_IGNORE_PATHS.has(relativePath);
|
|
164
|
+
return TEMPLATE_IGNORE_PATHS.has(relativePath) || /^\.openxiangda\/(profiles\.json|session\.json)(\.|$)/.test(relativePath);
|
|
158
165
|
}
|
|
159
166
|
|
|
160
167
|
function applyReplacements(content, replacements) {
|
|
@@ -19,6 +19,10 @@ If the workspace contains `app-workspace.config.ts`, forms/pages/resource manife
|
|
|
19
19
|
|
|
20
20
|
For an existing V1 project, proactively check whether V2 covers its required capabilities, whether it is still in testing, and whether migration cost is manageable. When those conditions hold, recommend V2 first and explain the benefit, rebuild scope and verification cost. Use the unified entry's `openxiangda migrate assess --to v2` for source pointers; confirm the project design, data/workflow mapping, acceptance and rollback before implementing migration. Keep unknown conditions explicit and continue current maintenance with the matching engine until migration is authorized.
|
|
21
21
|
|
|
22
|
+
## Installation and upgrade
|
|
23
|
+
|
|
24
|
+
For CLI, Skill and MCP setup or upgrades, read the [unified installation guide](https://github.com/1377385356/openxiangda/blob/master/docs/getting-started.md#upgrade). Install the global launcher with Node.js 24+ using `npm install -g openxiangda@latest --registry=https://registry.npmjs.org`. Use that global command for `migrate assess`; a project-local V1 CLI does not implement it. Workspace updates remain on `legacy-v1`. Refresh matching skills after dependency updates and restart a configured V2 MCP client to load its new project version. Never automatically replace a V1 project dependency with V2.
|
|
25
|
+
|
|
22
26
|
## 1.x Delivery V2 is the normal 1.x release path
|
|
23
27
|
|
|
24
28
|
When `app-workspace.config.ts` declares `deliveryVersion: 2`, all later V1 SDD,
|
|
@@ -229,3 +233,5 @@ The lease is app-level promotion ownership, while worktree ownership prevents tw
|
|
|
229
233
|
- `../openxiangda-open-api/SKILL.md`
|
|
230
234
|
|
|
231
235
|
Load a reference only when the selected subskill explicitly requires it. Workspace `AGENTS.md` remains authoritative for project-specific constraints.
|
|
236
|
+
|
|
237
|
+
平台登录态只使用当前工作区的 `.openxiangda/profiles.json`,不再读取或合并全局 profiles。升级后请进入每个项目运行 `openxiangda login <platform-url>`。登录文件自动加入 Git 忽略规则,不得提交或打包。
|