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.
- package/LICENSE +21 -0
- package/README.md +19 -2
- package/lib/cli.js +70 -37
- package/lib/config.js +60 -56
- package/lib/generation-update.js +42 -0
- package/lib/migration-advice.js +19 -0
- package/lib/skills.js +8 -16
- package/lib/workspace-init.js +11 -4
- package/openxiangda-skills/SKILL.md +11 -3
- package/openxiangda-skills/references/architecture-patterns.md +218 -218
- package/openxiangda-skills/references/component-guide.md +122 -122
- package/openxiangda-skills/references/openxiangda-api.md +1 -1
- package/openxiangda-skills/references/platform-data-model.md +285 -285
- package/openxiangda-skills/references/resource-manifest-cheatsheet.md +1 -1
- package/openxiangda-skills/references/troubleshooting.md +224 -224
- package/openxiangda-skills/references/workspace-state.md +3 -3
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +1 -1
- package/package.json +54 -3
- 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/openxiangda-react-spa/app-workspace.config.ts +1 -1
- 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 +8 -2
- 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://
|
|
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
|
|
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,
|
|
@@ -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
|
-
|
|
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
|
-
|
|
581
|
-
|
|
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}
|
|
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
|
-
|
|
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(
|
|
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
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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,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 };
|