openxiangda 1.0.267 → 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 (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +19 -2
  3. package/lib/cli.js +70 -37
  4. package/lib/config.js +60 -56
  5. package/lib/generation-update.js +42 -0
  6. package/lib/migration-advice.js +19 -0
  7. package/lib/skills.js +8 -16
  8. package/lib/workspace-init.js +11 -4
  9. package/openxiangda-skills/SKILL.md +11 -3
  10. package/openxiangda-skills/references/architecture-patterns.md +218 -218
  11. package/openxiangda-skills/references/component-guide.md +122 -122
  12. package/openxiangda-skills/references/openxiangda-api.md +1 -1
  13. package/openxiangda-skills/references/platform-data-model.md +285 -285
  14. package/openxiangda-skills/references/resource-manifest-cheatsheet.md +1 -1
  15. package/openxiangda-skills/references/troubleshooting.md +224 -224
  16. package/openxiangda-skills/references/workspace-state.md +3 -3
  17. package/openxiangda-skills/skills/openxiangda-core/SKILL.md +1 -1
  18. package/package.json +54 -3
  19. package/packages/sdk/dist/{ProcessPreview-Cyk6uv-w.d.mts → ProcessPreview-DSUIJi5V.d.mts} +2 -3
  20. package/packages/sdk/dist/{ProcessPreview-Cyk6uv-w.d.ts → ProcessPreview-DSUIJi5V.d.ts} +2 -3
  21. package/packages/sdk/dist/components/index.d.mts +41 -42
  22. package/packages/sdk/dist/components/index.d.ts +41 -42
  23. package/packages/sdk/dist/{dataManagementApi-CE8Zyj3a.d.ts → dataManagementApi-Ro9itwm8.d.ts} +2 -2
  24. package/packages/sdk/dist/{dataManagementApi-4fSaCA5t.d.mts → dataManagementApi-p_HOhQnA.d.mts} +2 -2
  25. package/packages/sdk/dist/runtime/index.d.mts +3 -4
  26. package/packages/sdk/dist/runtime/index.d.ts +3 -4
  27. package/packages/sdk/dist/runtime/react.d.mts +5 -5
  28. package/packages/sdk/dist/runtime/react.d.ts +5 -5
  29. package/packages/sdk/src/build-source/scripts/utils/mime-types.mjs +70 -70
  30. package/packages/sdk/src/build-source/scripts/utils/oss-client.mjs +128 -128
  31. package/packages/sdk/src/build-source/scripts/utils/progress.mjs +57 -57
  32. package/templates/openxiangda-react-spa/.cursor/rules/openxiangda.mdc +6 -0
  33. package/templates/openxiangda-react-spa/.qoder/rules/openxiangda.md +6 -0
  34. package/templates/openxiangda-react-spa/AGENTS.md +6 -0
  35. package/templates/openxiangda-react-spa/app-workspace.config.ts +1 -1
  36. package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda.mdc +7 -1
  37. package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda.md +7 -1
  38. package/templates/sy-lowcode-app-workspace/AGENTS.md +8 -2
  39. package/templates/sy-lowcode-app-workspace/app-workspace.config.ts +1 -1
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 OpenXiangda contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -20,7 +20,22 @@ packages, unified AI skill, templates, and documentation. It must not use the
20
20
 
21
21
  Normal OpenXiangda app development uses platform-user login tokens through `/openxiangda-api/v1`; it does not use AK/SK. External backend and third-party integrations use the separate `openxiangda-open-api` skill, `/dingtalk-api/v1.0`, and a platform-managed AK/SK credential.
22
22
 
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://yida.wisejob.cn/` to the CLI is supported; OpenXiangda stores the API base as `https://yida.wisejob.cn/service`.
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
+
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)。工具更新与应用迁移分别执行。
24
39
 
25
40
  ## OpenXiangda 1.x Delivery V2 (maintenance)
26
41
 
@@ -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,
@@ -114,6 +114,8 @@ const {
114
114
  summarizeBindingDifferences,
115
115
  } = require('./resource-binding-contract');
116
116
  const { getSkillStatusReport, installSkills } = require('./skills');
117
+ const { resolveUpdateTarget, buildGenerationUpdateCommand } = require('./generation-update');
118
+ const { migrationAdvice, printMigrationAdvice } = require('./migration-advice');
117
119
  const {
118
120
  assertOrClaimWorktreeOwner,
119
121
  claimWorktreeOwner,
@@ -232,7 +234,12 @@ const genericGitBasePlanCache = new Map();
232
234
  async function main(argv) {
233
235
  let commandError = null;
234
236
  try {
235
- 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;
236
243
  } catch (error) {
237
244
  commandError = error;
238
245
  throw error;
@@ -318,12 +325,12 @@ function printHelp() {
318
325
 
319
326
  Usage:
320
327
  openxiangda version [--json]
321
- openxiangda login <platform-url> [--profile name]
322
- 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]
323
330
  openxiangda platform add <name> <platform-url>
324
331
  openxiangda platform list
325
332
  openxiangda platform use <name>
326
- openxiangda auth status|refresh|logout [--profile name]
333
+ openxiangda auth status|refresh|logout [--profile name] [--cwd directory]
327
334
  openxiangda doctor [--profile name] [--app-type APP_XXX] [--release] [--json]
328
335
  openxiangda contract list|explain <contract-name> [--json]
329
336
  openxiangda policy show|check|render [--json]
@@ -518,6 +525,7 @@ async function version(args) {
518
525
  atomicActivation: APP_FUNCTION_SECRET_CAPABILITIES.atomicActivation,
519
526
  },
520
527
  updateCheckCommand: 'openxiangda update check --json',
528
+ migrationAdvice: migrationAdvice(),
521
529
  };
522
530
  if (flags.json) return writeJson(result);
523
531
  print(CURRENT_VERSION);
@@ -528,13 +536,14 @@ async function update(args) {
528
536
  const parsedArgs = requestedSubcommand === args[0] ? args.slice(1) : args;
529
537
  const { flags } = parseArgs(parsedArgs);
530
538
  if (wantsSubcommandHelp(requestedSubcommand, flags)) {
531
- 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]');
532
540
  return;
533
541
  }
534
542
  const registry = normalizeNpmRegistry(flags.registry || OFFICIAL_NPM_REGISTRY);
543
+ const selection = resolveUpdateTarget({ target: flags.target });
535
544
 
536
545
  if (requestedSubcommand === 'check') {
537
- const result = checkOpenXiangdaUpdate(registry);
546
+ const result = checkOpenXiangdaUpdate(registry, selection);
538
547
  if (flags.json) return writeJson(result);
539
548
  printUpdateCheck(result);
540
549
  return;
@@ -545,6 +554,7 @@ async function update(args) {
545
554
  json: Boolean(flags.json),
546
555
  skipSkills: Boolean(flags['no-skills']),
547
556
  timeoutSeconds: flags['timeout-seconds'],
557
+ ...selection,
548
558
  });
549
559
  if (flags.json) return writeJson(result);
550
560
  print('OpenXiangda 已更新。');
@@ -554,7 +564,7 @@ async function update(args) {
554
564
  return;
555
565
  }
556
566
 
557
- 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]');
558
568
  }
559
569
 
560
570
  function normalizeNpmRegistry(value) {
@@ -562,8 +572,8 @@ function normalizeNpmRegistry(value) {
562
572
  return registry.replace(/\/+$/, '') || OFFICIAL_NPM_REGISTRY;
563
573
  }
564
574
 
565
- function checkOpenXiangdaUpdate(registry) {
566
- const latestVersion = fetchLatestOpenXiangdaVersion(registry);
575
+ function checkOpenXiangdaUpdate(registry, selection = resolveUpdateTarget()) {
576
+ const latestVersion = fetchLatestOpenXiangdaVersion(registry, selection.channel);
567
577
  const versionComparison = compareVersions(latestVersion, CURRENT_VERSION);
568
578
  const updateAvailable = versionComparison > 0;
569
579
  return {
@@ -577,10 +587,13 @@ function checkOpenXiangdaUpdate(registry) {
577
587
  : versionComparison < 0
578
588
  ? 'local_newer_than_registry'
579
589
  : 'latest',
580
- installCommand: `npm ${buildOpenXiangdaUpdateInstallPlan(registry).args.join(' ')}`,
581
- skillInstallCommand: 'openxiangda skill install --force',
590
+ target: selection.target,
591
+ channel: selection.channel,
592
+ installCommand: `openxiangda update install --target ${selection.target}`,
593
+ skillInstallCommand: 'openxiangda skill install',
582
594
  compatibilityCheckCommand: 'openxiangda commands --json',
583
595
  checkedAt: new Date().toISOString(),
596
+ migrationAdvice: migrationAdvice(),
584
597
  notes: [
585
598
  'Use the official npm registry for OpenXiangda updates; domestic mirrors may lag.',
586
599
  'After updating the package, refresh OpenXiangda skills so AI guidance matches the CLI version.',
@@ -588,10 +601,10 @@ function checkOpenXiangdaUpdate(registry) {
588
601
  };
589
602
  }
590
603
 
591
- function fetchLatestOpenXiangdaVersion(registry) {
604
+ function fetchLatestOpenXiangdaVersion(registry, channel = 'v1') {
592
605
  const result = spawnSync(
593
606
  'npm',
594
- ['view', `${NPM_PACKAGE_NAME}@latest`, 'version', `--registry=${registry}`, '--json'],
607
+ ['view', `${NPM_PACKAGE_NAME}@${channel}`, 'version', `--registry=${registry}`, '--json'],
595
608
  {
596
609
  encoding: 'utf8',
597
610
  timeout: 30000,
@@ -630,17 +643,22 @@ function normalizeUpdateInstallTimeoutSeconds(value) {
630
643
 
631
644
  function buildOpenXiangdaUpdateInstallPlan(registryInput, options = {}) {
632
645
  const registry = normalizeNpmRegistry(registryInput);
646
+ const selection = resolveUpdateTarget(options);
647
+ const version = options.version || fetchLatestOpenXiangdaVersion(registry, selection.channel);
648
+ const execution = buildGenerationUpdateCommand(selection, version);
633
649
  const timeoutSeconds = normalizeUpdateInstallTimeoutSeconds(
634
650
  options.timeoutSeconds
635
651
  );
636
652
  return {
637
653
  registry,
638
654
  timeoutSeconds,
655
+ ...selection,
656
+ command: execution.command,
657
+ version,
639
658
  args: [
640
- 'install',
641
- '-g',
642
- `${NPM_PACKAGE_NAME}@latest`,
659
+ ...execution.args,
643
660
  `--registry=${registry}`,
661
+ ...(execution.command === 'npm' ? [
644
662
  '--prefer-offline',
645
663
  '--legacy-peer-deps',
646
664
  '--no-audit',
@@ -650,6 +668,7 @@ function buildOpenXiangdaUpdateInstallPlan(registryInput, options = {}) {
650
668
  '--fetch-timeout=60000',
651
669
  '--fetch-retry-mintimeout=1000',
652
670
  '--fetch-retry-maxtimeout=10000',
671
+ ] : []),
653
672
  ],
654
673
  };
655
674
  }
@@ -662,6 +681,7 @@ async function runCommandWithHeartbeat(command, args, options = {}) {
662
681
  let child;
663
682
  try {
664
683
  child = spawn(command, args, {
684
+ cwd: options.cwd || process.cwd(),
665
685
  encoding: 'utf8',
666
686
  stdio: quiet ? ['ignore', 'pipe', 'pipe'] : 'inherit',
667
687
  env: options.env || process.env,
@@ -731,7 +751,8 @@ async function installOpenXiangdaUpdate(registry, options = {}) {
731
751
  if (!quiet) {
732
752
  print(`执行: npm ${npmArgs.join(' ')}`);
733
753
  }
734
- const installResult = await runCommandWithHeartbeat('npm', npmArgs, {
754
+ const installResult = await runCommandWithHeartbeat(plan.command, npmArgs, {
755
+ cwd: plan.cwd,
735
756
  quiet,
736
757
  timeoutMs: plan.timeoutSeconds * 1000,
737
758
  env: { ...process.env, npm_config_registry: plan.registry },
@@ -753,6 +774,7 @@ async function installOpenXiangdaUpdate(registry, options = {}) {
753
774
  registry: plan.registry,
754
775
  installCommand: `npm ${npmArgs.join(' ')}`,
755
776
  installed: true,
777
+ migrationAdvice: migrationAdvice(),
756
778
  durationMs: installResult.durationMs,
757
779
  cacheMode: 'prefer-offline',
758
780
  timeoutSeconds: plan.timeoutSeconds,
@@ -763,11 +785,15 @@ async function installOpenXiangdaUpdate(registry, options = {}) {
763
785
  },
764
786
  };
765
787
 
766
- if (!options.skipSkills) {
788
+ if (!options.skipSkills && plan.target === 'workspace') {
767
789
  if (!quiet) {
768
790
  print('刷新 OpenXiangda skills: openxiangda skill install --force');
769
791
  }
770
- const skillResult = spawnSync('openxiangda', ['skill', 'install', '--force'], {
792
+ const localManifest = path.join(plan.cwd, 'node_modules/openxiangda/package.json');
793
+ const localPackage = JSON.parse(fs.readFileSync(localManifest, 'utf8'));
794
+ const localBin = path.resolve(path.dirname(localManifest), typeof localPackage.bin === 'string' ? localPackage.bin : localPackage.bin.openxiangda);
795
+ const skillResult = spawnSync(process.execPath, [localBin, 'skill', 'install'], {
796
+ cwd: plan.cwd,
771
797
  encoding: 'utf8',
772
798
  stdio: quiet ? 'pipe' : 'inherit',
773
799
  timeout: 60000,
@@ -9427,7 +9453,7 @@ async function platform(args) {
9427
9453
  print('用法: openxiangda platform add|list|use|remove');
9428
9454
  return;
9429
9455
  }
9430
- const config = loadConfig();
9456
+ const config = loadConfig(flags.cwd || process.cwd(), { exact: Boolean(flags.cwd) });
9431
9457
 
9432
9458
  if (subcommand === 'prepare') {
9433
9459
  const result = prepareDependencyCapsule({
@@ -9532,10 +9558,10 @@ async function login(args) {
9532
9558
  positional[0] === '--help' ||
9533
9559
  positional[0] === '-h'
9534
9560
  ) {
9535
- print('用法: openxiangda login <platform-url> [--profile name] [--no-open] [--json]');
9561
+ print('用法: openxiangda login <platform-url> [--profile name] [--cwd directory] [--no-open] [--json]');
9536
9562
  return;
9537
9563
  }
9538
- const config = loadConfig();
9564
+ const config = loadConfig(flags.cwd || process.cwd(), { exact: Boolean(flags.cwd) });
9539
9565
  const rawUrl = positional[0];
9540
9566
  const profileName = flags.profile || config.currentProfile || 'default';
9541
9567
  let profile = config.profiles[profileName] || { name: profileName };
@@ -9550,7 +9576,7 @@ async function login(args) {
9550
9576
  }
9551
9577
 
9552
9578
  if (!profile.baseUrl) {
9553
- fail('缺少平台地址。用法: openxiangda login <platform-url> [--profile name]');
9579
+ fail('缺少平台地址。用法: openxiangda login <platform-url> [--profile name] [--cwd directory]');
9554
9580
  }
9555
9581
 
9556
9582
  const sessionPayload = await requestJson(
@@ -9649,12 +9675,12 @@ async function auth(args) {
9649
9675
  const [subcommand, ...rest] = args;
9650
9676
  const { flags } = parseArgs(rest);
9651
9677
  if (wantsSubcommandHelp(subcommand, flags)) {
9652
- print('用法: openxiangda auth status|refresh|logout [--profile name] [--json]');
9678
+ print('用法: openxiangda auth status|refresh|logout [--profile name] [--cwd directory] [--json]');
9653
9679
  return;
9654
9680
  }
9655
9681
 
9656
9682
  if (subcommand === 'status') {
9657
- const config = loadConfig();
9683
+ const config = loadConfig(flags.cwd || process.cwd(), { exact: Boolean(flags.cwd) });
9658
9684
  const { profileName, profile } = getProfile(config, flags.profile);
9659
9685
  if (!profile.token?.accessToken) {
9660
9686
  print(`未登录: ${profileName} (${profile.baseUrl})`);
@@ -9672,7 +9698,7 @@ async function auth(args) {
9672
9698
  }
9673
9699
 
9674
9700
  if (subcommand === 'refresh') {
9675
- const config = loadConfig();
9701
+ const config = loadConfig(flags.cwd || process.cwd(), { exact: Boolean(flags.cwd) });
9676
9702
  const profileName = flags.profile || config.currentProfile;
9677
9703
  if (!profileName) fail('未选择平台 profile');
9678
9704
  const data = await refreshProfile(config, profileName);
@@ -9683,7 +9709,7 @@ async function auth(args) {
9683
9709
  }
9684
9710
 
9685
9711
  if (subcommand === 'logout') {
9686
- const config = loadConfig();
9712
+ const config = loadConfig(flags.cwd || process.cwd(), { exact: Boolean(flags.cwd) });
9687
9713
  const { profileName, profile } = getProfile(config, flags.profile);
9688
9714
  if (profile.token?.accessToken) {
9689
9715
  try {
@@ -9701,7 +9727,7 @@ async function auth(args) {
9701
9727
  return;
9702
9728
  }
9703
9729
 
9704
- fail('用法: openxiangda auth status|refresh|logout [--profile name]');
9730
+ fail('用法: openxiangda auth status|refresh|logout [--profile name] [--cwd directory]');
9705
9731
  }
9706
9732
 
9707
9733
  async function env(args) {
@@ -9718,7 +9744,8 @@ async function env(args) {
9718
9744
  const currentState = profileName ? state.profiles?.[profileName] : null;
9719
9745
  const oss = summarizeOssEnv(globalEnv);
9720
9746
  const data = {
9721
- configFile: CONFIG_FILE,
9747
+ configFile: getConfigFile(config),
9748
+ sessionScope: 'workspace',
9722
9749
  globalEnvFile: GLOBAL_ENV_FILE,
9723
9750
  globalEnvExists: fs.existsSync(GLOBAL_ENV_FILE),
9724
9751
  projectStateFile: path.join(process.cwd(), PROJECT_STATE_FILE),
@@ -10181,7 +10208,9 @@ async function workspace(args) {
10181
10208
  printWorkspaceHelp();
10182
10209
  return;
10183
10210
  }
10184
- const config = loadConfig();
10211
+ const config = subcommand === 'init'
10212
+ ? loadConfig(path.resolve(positional[0] || process.cwd()), { exact: true })
10213
+ : loadConfig();
10185
10214
 
10186
10215
  if (subcommand === 'ownership' || subcommand === 'owner') {
10187
10216
  const action = positional[0] || 'status';
@@ -17047,7 +17076,7 @@ async function commands(args) {
17047
17076
  commands: [
17048
17077
  'version [--json]',
17049
17078
  'login <platform-url> [--profile name]',
17050
- 'update check|install',
17079
+ 'update check|install [--target workspace|launcher] [--json]',
17051
17080
  'platform add|list|use|remove',
17052
17081
  'auth status|refresh|logout',
17053
17082
  'doctor [--profile name] [--app-type APP_XXX] [--release]',
@@ -17143,6 +17172,7 @@ async function skill(args) {
17143
17172
 
17144
17173
  if (subcommand === 'install') {
17145
17174
  const result = installSkills(options);
17175
+ result.migrationAdvice = migrationAdvice();
17146
17176
  if (flags.json) return writeJson(result);
17147
17177
  printSkillInstallReport(result);
17148
17178
  return;
@@ -17161,6 +17191,7 @@ async function skill(args) {
17161
17191
  force: Boolean(flags.force),
17162
17192
  dryRun: Boolean(flags['dry-run']),
17163
17193
  });
17194
+ result.migrationAdvice = migrationAdvice();
17164
17195
  if (flags.json) return writeJson(result);
17165
17196
  printSkillBootstrapReport(result);
17166
17197
  return;
@@ -22475,7 +22506,7 @@ function runGenericResourceGitBasePreflight({
22475
22506
  copyProjectState: true,
22476
22507
  },
22477
22508
  ({ buildRoot, buildEnv, tempRoot }) => {
22478
- const isolatedHome = prepareReadonlyGitBasePlanHome(tempRoot);
22509
+ const isolatedHome = prepareReadonlyGitBasePlanHome(tempRoot, buildRoot);
22479
22510
  const childEnv = {
22480
22511
  ...buildEnv,
22481
22512
  HOME: isolatedHome,
@@ -22611,8 +22642,9 @@ function runGenericResourceGitBasePreflight({
22611
22642
  };
22612
22643
  }
22613
22644
 
22614
- function prepareReadonlyGitBasePlanHome(tempRoot) {
22615
- if (!fs.existsSync(CONFIG_FILE)) {
22645
+ function prepareReadonlyGitBasePlanHome(tempRoot, buildRoot) {
22646
+ const configSource = getConfigFile();
22647
+ if (!fs.existsSync(configSource)) {
22616
22648
  const error = new Error(
22617
22649
  'SOURCE_BASE_PLAN_FAILED: 当前 profile 配置不存在,无法执行隔离 Git 基线 plan'
22618
22650
  );
@@ -22620,10 +22652,11 @@ function prepareReadonlyGitBasePlanHome(tempRoot) {
22620
22652
  throw error;
22621
22653
  }
22622
22654
  const home = path.join(tempRoot, 'plan-home');
22623
- const configDir = path.join(home, '.openxiangda');
22655
+ const configDir = path.join(buildRoot, '.openxiangda');
22656
+ fs.mkdirSync(home, { recursive: true, mode: 0o700 });
22624
22657
  const configFile = path.join(configDir, 'profiles.json');
22625
22658
  fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
22626
- fs.copyFileSync(CONFIG_FILE, configFile);
22659
+ fs.copyFileSync(configSource, configFile);
22627
22660
  try {
22628
22661
  fs.chmodSync(configFile, 0o400);
22629
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,42 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ function resolveUpdateTarget(options = {}) {
5
+ let root = path.resolve(options.cwd || process.cwd());
6
+ let workspace = null;
7
+ while (true) {
8
+ const v1 = fs.existsSync(path.join(root, 'app-workspace.config.ts')) || fs.existsSync(path.join(root, '.openxiangda/state.json'));
9
+ const v2 = fs.existsSync(path.join(root, 'openxiangda.config.ts')) || fs.existsSync(path.join(root, 'openxiangda-app.config.ts'));
10
+ if (v1 && v2) throw new Error('WORKSPACE_GENERATION_CONFLICT: 同一目录存在 V1/V2 工作区标记');
11
+ if (v2) throw new Error('WORKSPACE_ENGINE_GENERATION_MISMATCH: V1 CLI 不能升级 V2 项目;请使用统一入口');
12
+ if (v1) { workspace = root; break; }
13
+ const parent = path.dirname(root);
14
+ if (parent === root) break;
15
+ root = parent;
16
+ }
17
+ const target = options.target || (workspace ? 'workspace' : 'launcher');
18
+ if (!['workspace', 'launcher'].includes(target)) throw new Error('DISTRIBUTION_UPDATE_TARGET_INVALID: 使用 workspace 或 launcher');
19
+ if (target === 'workspace' && !workspace) throw new Error('DISTRIBUTION_WORKSPACE_REQUIRED: 未找到 V1 工作区');
20
+ return { target, channel: target === 'workspace' ? 'legacy-v1' : 'latest', cwd: workspace || process.cwd() };
21
+ }
22
+
23
+ function buildGenerationUpdateCommand(selection, version) {
24
+ if (!/^[12]\.\d+\.\d+$/.test(version)) throw new Error('DISTRIBUTION_UPDATE_VERSION_INVALID: 升级目标必须是已发布的稳定版本');
25
+ if (selection.target === 'launcher') {
26
+ if (!version.startsWith('2.')) throw new Error('DISTRIBUTION_UPDATE_GENERATION_MISMATCH: 全局入口需要 V2');
27
+ if (Number(process.versions.node.split('.')[0]) < 24) throw new Error('DISTRIBUTION_NODE_VERSION_REQUIRED: 全局统一入口需要 Node.js 24;当前 V1 项目环境不会自动改变');
28
+ return { command: 'npm', args: ['install', '-g', `openxiangda@${version}`] };
29
+ }
30
+ if (!version.startsWith('1.')) throw new Error('DISTRIBUTION_UPDATE_GENERATION_MISMATCH: V1 项目只允许安装 V1');
31
+ const file = path.join(selection.cwd, 'package.json');
32
+ if (!fs.existsSync(file)) throw new Error('DISTRIBUTION_PACKAGE_MANIFEST_REQUIRED: 工作区缺少 package.json');
33
+ const manifest = JSON.parse(fs.readFileSync(file, 'utf8'));
34
+ const pnpm = fs.existsSync(path.join(selection.cwd, 'pnpm-lock.yaml')) || manifest.packageManager?.startsWith('pnpm@');
35
+ if (pnpm && fs.existsSync(path.join(selection.cwd, 'package-lock.json'))) throw new Error('DISTRIBUTION_PACKAGE_MANAGER_CONFLICT: 多个包管理器锁文件');
36
+ if (fs.existsSync(path.join(selection.cwd, 'yarn.lock'))) throw new Error('DISTRIBUTION_PACKAGE_MANAGER_UNSUPPORTED: 请通过项目 Yarn 命令显式安装 openxiangda@legacy-v1');
37
+ return { command: pnpm ? 'pnpm' : 'npm', args: pnpm
38
+ ? ['add', ...(manifest.dependencies?.openxiangda ? [] : ['--save-dev']), '--save-exact', `openxiangda@${version}`, ...(fs.existsSync(path.join(selection.cwd, 'pnpm-workspace.yaml')) ? ['--workspace-root'] : [])]
39
+ : ['install', manifest.dependencies?.openxiangda ? '--save-prod' : '--save-dev', '--save-exact', `openxiangda@${version}`] };
40
+ }
41
+
42
+ module.exports = { resolveUpdateTarget, buildGenerationUpdateCommand };
@@ -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 };