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.
Files changed (32) hide show
  1. package/README.md +18 -1
  2. package/lib/cli.js +40 -24
  3. package/lib/config.js +60 -56
  4. package/lib/migration-advice.js +19 -0
  5. package/lib/workspace-init.js +11 -4
  6. package/openxiangda-skills/SKILL.md +6 -0
  7. package/openxiangda-skills/references/architecture-patterns.md +218 -218
  8. package/openxiangda-skills/references/component-guide.md +122 -122
  9. package/openxiangda-skills/references/platform-data-model.md +285 -285
  10. package/openxiangda-skills/references/troubleshooting.md +224 -224
  11. package/openxiangda-skills/references/workspace-state.md +1 -1
  12. package/openxiangda-skills/skills/openxiangda-core/SKILL.md +1 -1
  13. package/package.json +23 -22
  14. package/packages/sdk/dist/{ProcessPreview-Cyk6uv-w.d.mts → ProcessPreview-DSUIJi5V.d.mts} +2 -3
  15. package/packages/sdk/dist/{ProcessPreview-Cyk6uv-w.d.ts → ProcessPreview-DSUIJi5V.d.ts} +2 -3
  16. package/packages/sdk/dist/components/index.d.mts +41 -42
  17. package/packages/sdk/dist/components/index.d.ts +41 -42
  18. package/packages/sdk/dist/{dataManagementApi-CE8Zyj3a.d.ts → dataManagementApi-Ro9itwm8.d.ts} +2 -2
  19. package/packages/sdk/dist/{dataManagementApi-4fSaCA5t.d.mts → dataManagementApi-p_HOhQnA.d.mts} +2 -2
  20. package/packages/sdk/dist/runtime/index.d.mts +3 -4
  21. package/packages/sdk/dist/runtime/index.d.ts +3 -4
  22. package/packages/sdk/dist/runtime/react.d.mts +5 -5
  23. package/packages/sdk/dist/runtime/react.d.ts +5 -5
  24. package/packages/sdk/src/build-source/scripts/utils/mime-types.mjs +70 -70
  25. package/packages/sdk/src/build-source/scripts/utils/oss-client.mjs +128 -128
  26. package/packages/sdk/src/build-source/scripts/utils/progress.mjs +57 -57
  27. package/templates/openxiangda-react-spa/.cursor/rules/openxiangda.mdc +6 -0
  28. package/templates/openxiangda-react-spa/.qoder/rules/openxiangda.md +6 -0
  29. package/templates/openxiangda-react-spa/AGENTS.md +6 -0
  30. package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda.mdc +7 -1
  31. package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda.md +7 -1
  32. 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 `~/.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.
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
- CONFIG_FILE,
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
- return await mainImpl(argv);
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: CONFIG_FILE,
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 = loadConfig();
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
- if (!fs.existsSync(CONFIG_FILE)) {
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(home, '.openxiangda');
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(CONFIG_FILE, configFile);
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
- function mergeConfigs(...configs) {
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 mergeProfile(previous = {}, next = {}) {
67
- const merged = {
68
- ...previous,
69
- ...next,
70
- };
71
- const baseUrlChanged =
72
- previous.baseUrl && next.baseUrl && previous.baseUrl !== next.baseUrl;
73
- for (const key of ['token', 'user', 'tenant']) {
74
- if (
75
- !baseUrlChanged &&
76
- !Object.prototype.hasOwnProperty.call(next, key) &&
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 merged;
63
+ return start;
83
64
  }
84
65
 
85
- function isConfigEqual(left, right) {
86
- return JSON.stringify(normalizeConfig(left)) === JSON.stringify(normalizeConfig(right));
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 loadConfig(cwd = process.cwd()) {
90
- ensureUserConfigDir();
91
- const globalConfig = readJson(CONFIG_FILE, null);
92
- const legacyConfig = readJson(path.join(cwd, LEGACY_PROJECT_CONFIG_FILE), null);
93
-
94
- if (legacyConfig?.profiles && Object.keys(legacyConfig.profiles).length > 0) {
95
- const migrated = mergeConfigs(legacyConfig, globalConfig);
96
- if (!globalConfig || !isConfigEqual(globalConfig, migrated)) {
97
- saveConfig(migrated);
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
- return normalizeConfig(globalConfig);
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
- ensureUserConfigDir();
107
- const normalized = normalizeConfig(config);
108
- const tempFile = `${CONFIG_FILE}.${process.pid}.tmp`;
109
- fs.writeFileSync(tempFile, `${JSON.stringify(normalized, null, 2)}\n`, {
110
- mode: 0o600,
111
- });
112
- fs.renameSync(tempFile, CONFIG_FILE);
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.chmodSync(CONFIG_FILE, 0o600);
115
- } catch {
116
- // chmod is best-effort on non-POSIX filesystems.
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 };
@@ -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 config = loadConfig();
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 忽略规则,不得提交或打包。