openxiangda 1.0.0

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 (121) hide show
  1. package/README.md +58 -0
  2. package/bin/openxiangda.js +11 -0
  3. package/lib/cli.js +2423 -0
  4. package/lib/config.js +121 -0
  5. package/lib/http.js +47 -0
  6. package/lib/skills.js +371 -0
  7. package/lib/utils.js +87 -0
  8. package/lib/workspace-init.js +139 -0
  9. package/openxiangda-skills/SKILL.md +128 -0
  10. package/openxiangda-skills/references/architecture-patterns.md +242 -0
  11. package/openxiangda-skills/references/automation-v3.md +129 -0
  12. package/openxiangda-skills/references/component-guide.md +198 -0
  13. package/openxiangda-skills/references/forms/component-registry.md +53 -0
  14. package/openxiangda-skills/references/forms/form-schema.md +109 -0
  15. package/openxiangda-skills/references/forms/layout-and-rules.md +24 -0
  16. package/openxiangda-skills/references/openxiangda-api.md +466 -0
  17. package/openxiangda-skills/references/pages/page-sdk.md +13 -0
  18. package/openxiangda-skills/references/pages/publish-flow.md +36 -0
  19. package/openxiangda-skills/references/pages/workspace-structure.md +38 -0
  20. package/openxiangda-skills/references/permissions-settings.md +147 -0
  21. package/openxiangda-skills/references/platform-data-model.md +305 -0
  22. package/openxiangda-skills/references/style-system.md +492 -0
  23. package/openxiangda-skills/references/troubleshooting.md +246 -0
  24. package/openxiangda-skills/references/workflow-v3.md +105 -0
  25. package/openxiangda-skills/references/workspace-state.md +45 -0
  26. package/openxiangda-skills/skills/openxiangda-app/SKILL.md +64 -0
  27. package/openxiangda-skills/skills/openxiangda-core/SKILL.md +143 -0
  28. package/openxiangda-skills/skills/openxiangda-form/SKILL.md +76 -0
  29. package/openxiangda-skills/skills/openxiangda-inspect/SKILL.md +40 -0
  30. package/openxiangda-skills/skills/openxiangda-page/SKILL.md +62 -0
  31. package/openxiangda-skills/skills/openxiangda-permission-settings/SKILL.md +95 -0
  32. package/openxiangda-skills/skills/openxiangda-workflow-automation/SKILL.md +97 -0
  33. package/package.json +126 -0
  34. package/packages/sdk/bin/lowcode-workspace.mjs +4 -0
  35. package/packages/sdk/dist/build/index.cjs +33 -0
  36. package/packages/sdk/dist/build/index.cjs.map +1 -0
  37. package/packages/sdk/dist/build/index.d.mts +40 -0
  38. package/packages/sdk/dist/build/index.d.ts +40 -0
  39. package/packages/sdk/dist/build/index.mjs +8 -0
  40. package/packages/sdk/dist/build/index.mjs.map +1 -0
  41. package/packages/sdk/dist/components/index.cjs +18700 -0
  42. package/packages/sdk/dist/components/index.cjs.map +1 -0
  43. package/packages/sdk/dist/components/index.d.mts +2094 -0
  44. package/packages/sdk/dist/components/index.d.ts +2094 -0
  45. package/packages/sdk/dist/components/index.mjs +18649 -0
  46. package/packages/sdk/dist/components/index.mjs.map +1 -0
  47. package/packages/sdk/dist/runtime/index.cjs +1469 -0
  48. package/packages/sdk/dist/runtime/index.cjs.map +1 -0
  49. package/packages/sdk/dist/runtime/index.d.mts +831 -0
  50. package/packages/sdk/dist/runtime/index.d.ts +831 -0
  51. package/packages/sdk/dist/runtime/index.mjs +1420 -0
  52. package/packages/sdk/dist/runtime/index.mjs.map +1 -0
  53. package/packages/sdk/dist/styles/antd-theme.cjs +60 -0
  54. package/packages/sdk/dist/styles/antd-theme.cjs.map +1 -0
  55. package/packages/sdk/dist/styles/antd-theme.d.mts +5 -0
  56. package/packages/sdk/dist/styles/antd-theme.d.ts +5 -0
  57. package/packages/sdk/dist/styles/antd-theme.mjs +35 -0
  58. package/packages/sdk/dist/styles/antd-theme.mjs.map +1 -0
  59. package/packages/sdk/dist/styles/tailwind-preset.cjs +2641 -0
  60. package/packages/sdk/dist/styles/tailwind-preset.cjs.map +1 -0
  61. package/packages/sdk/dist/styles/tailwind-preset.d.mts +75 -0
  62. package/packages/sdk/dist/styles/tailwind-preset.d.ts +75 -0
  63. package/packages/sdk/dist/styles/tailwind-preset.mjs +2618 -0
  64. package/packages/sdk/dist/styles/tailwind-preset.mjs.map +1 -0
  65. package/packages/sdk/dist/styles/tokens.css +73 -0
  66. package/packages/sdk/src/build-source/README.md +9 -0
  67. package/packages/sdk/src/build-source/bin/lowcode-workspace.mjs +7 -0
  68. package/packages/sdk/src/build-source/package.json +34 -0
  69. package/packages/sdk/src/build-source/scripts/build-forms.mjs +824 -0
  70. package/packages/sdk/src/build-source/scripts/build-forms.runtime-entry.test.ts +18 -0
  71. package/packages/sdk/src/build-source/scripts/build-pages.mjs +793 -0
  72. package/packages/sdk/src/build-source/scripts/build-workspace.mjs +64 -0
  73. package/packages/sdk/src/build-source/scripts/publish-all.mjs +127 -0
  74. package/packages/sdk/src/build-source/scripts/publish-oss.mjs +149 -0
  75. package/packages/sdk/src/build-source/scripts/register-bundle.mjs +1 -0
  76. package/packages/sdk/src/build-source/scripts/register.mjs +329 -0
  77. package/packages/sdk/src/build-source/scripts/sync-schema.mjs +301 -0
  78. package/packages/sdk/src/build-source/scripts/utils/form-api.mjs +639 -0
  79. package/packages/sdk/src/build-source/scripts/utils/form-api.test.ts +244 -0
  80. package/packages/sdk/src/build-source/scripts/utils/form-runtime-assets.mjs +57 -0
  81. package/packages/sdk/src/build-source/scripts/utils/form-runtime-assets.test.ts +135 -0
  82. package/packages/sdk/src/build-source/scripts/utils/incremental.mjs +210 -0
  83. package/packages/sdk/src/build-source/scripts/utils/load-config.mjs +257 -0
  84. package/packages/sdk/src/build-source/scripts/utils/load-config.test.ts +44 -0
  85. package/packages/sdk/src/build-source/scripts/utils/mime-types.mjs +70 -0
  86. package/packages/sdk/src/build-source/scripts/utils/namespace-css.mjs +61 -0
  87. package/packages/sdk/src/build-source/scripts/utils/oss-client.mjs +128 -0
  88. package/packages/sdk/src/build-source/scripts/utils/pages.mjs +80 -0
  89. package/packages/sdk/src/build-source/scripts/utils/progress.mjs +57 -0
  90. package/packages/sdk/src/build-source/scripts/utils/register-payload.mjs +89 -0
  91. package/packages/sdk/src/build-source/scripts/utils/register-payload.test.ts +76 -0
  92. package/packages/sdk/src/build-source/scripts/utils/runtime-css-check.mjs +44 -0
  93. package/packages/sdk/src/build-source/scripts/utils/runtime-css-check.test.ts +54 -0
  94. package/packages/sdk/src/build-source/scripts/utils/schema-transform.mjs +130 -0
  95. package/packages/sdk/src/build-source/scripts/utils/schema-transform.test.ts +141 -0
  96. package/packages/sdk/src/build-source/scripts/utils/tailwind-config.mjs +227 -0
  97. package/packages/sdk/src/build-source/scripts/utils/tailwind-config.test.ts +187 -0
  98. package/packages/sdk/src/build-source/src/cli.mjs +679 -0
  99. package/templates/sy-lowcode-app-workspace/app-workspace.config.ts +34 -0
  100. package/templates/sy-lowcode-app-workspace/examples/forms/customer/page.tsx +1 -0
  101. package/templates/sy-lowcode-app-workspace/examples/forms/customer/schema.ts +35 -0
  102. package/templates/sy-lowcode-app-workspace/index.html +12 -0
  103. package/templates/sy-lowcode-app-workspace/package.json +49 -0
  104. package/templates/sy-lowcode-app-workspace/postcss.config.cjs +6 -0
  105. package/templates/sy-lowcode-app-workspace/scripts/build-js-code.mjs +100 -0
  106. package/templates/sy-lowcode-app-workspace/src/dev/App.tsx +26 -0
  107. package/templates/sy-lowcode-app-workspace/src/forms/.gitkeep +1 -0
  108. package/templates/sy-lowcode-app-workspace/src/forms/README.md +48 -0
  109. package/templates/sy-lowcode-app-workspace/src/index.css +28 -0
  110. package/templates/sy-lowcode-app-workspace/src/js-code-nodes/.gitkeep +1 -0
  111. package/templates/sy-lowcode-app-workspace/src/js-code-nodes/types.d.ts +3 -0
  112. package/templates/sy-lowcode-app-workspace/src/main.tsx +36 -0
  113. package/templates/sy-lowcode-app-workspace/src/pages/.gitkeep +1 -0
  114. package/templates/sy-lowcode-app-workspace/src/shared/form-schema.ts +128 -0
  115. package/templates/sy-lowcode-app-workspace/src/types/app-workspace.types.ts +31 -0
  116. package/templates/sy-lowcode-app-workspace/tailwind.config.cjs +30 -0
  117. package/templates/sy-lowcode-app-workspace/tsconfig.app.json +24 -0
  118. package/templates/sy-lowcode-app-workspace/tsconfig.js-code-nodes.json +15 -0
  119. package/templates/sy-lowcode-app-workspace/tsconfig.json +7 -0
  120. package/templates/sy-lowcode-app-workspace/tsconfig.node.json +10 -0
  121. package/templates/sy-lowcode-app-workspace/vite.config.ts +32 -0
package/lib/config.js ADDED
@@ -0,0 +1,121 @@
1
+ const fs = require('fs');
2
+ const os = require('os');
3
+ const path = require('path');
4
+
5
+ const CONFIG_DIR = path.join(os.homedir(), '.openxiangda');
6
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'profiles.json');
7
+ const PROJECT_DIR = '.openxiangda';
8
+ const PROJECT_STATE_FILE = path.join(PROJECT_DIR, 'state.json');
9
+
10
+ function emptyConfig() {
11
+ return {
12
+ version: 1,
13
+ currentProfile: null,
14
+ profiles: {},
15
+ };
16
+ }
17
+
18
+ function ensureUserConfigDir() {
19
+ fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
20
+ try {
21
+ fs.chmodSync(CONFIG_DIR, 0o700);
22
+ } catch {
23
+ // chmod is best-effort on non-POSIX filesystems.
24
+ }
25
+ }
26
+
27
+ function readJson(file, fallback) {
28
+ try {
29
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
30
+ } catch {
31
+ return fallback;
32
+ }
33
+ }
34
+
35
+ function loadConfig() {
36
+ ensureUserConfigDir();
37
+ const config = readJson(CONFIG_FILE, emptyConfig());
38
+ return {
39
+ ...emptyConfig(),
40
+ ...config,
41
+ profiles: config.profiles || {},
42
+ };
43
+ }
44
+
45
+ function saveConfig(config) {
46
+ ensureUserConfigDir();
47
+ const normalized = {
48
+ ...emptyConfig(),
49
+ ...config,
50
+ profiles: config.profiles || {},
51
+ };
52
+ const tempFile = `${CONFIG_FILE}.${process.pid}.tmp`;
53
+ fs.writeFileSync(tempFile, `${JSON.stringify(normalized, null, 2)}\n`, {
54
+ mode: 0o600,
55
+ });
56
+ fs.renameSync(tempFile, CONFIG_FILE);
57
+ try {
58
+ fs.chmodSync(CONFIG_FILE, 0o600);
59
+ } catch {
60
+ // chmod is best-effort on non-POSIX filesystems.
61
+ }
62
+ }
63
+
64
+ function normalizeBaseUrl(value) {
65
+ const raw = String(value || '').trim();
66
+ if (!raw) throw new Error('平台地址不能为空');
67
+ const url = new URL(raw);
68
+ if (!/^https?:$/.test(url.protocol)) {
69
+ throw new Error('平台地址必须使用 http 或 https');
70
+ }
71
+ const cleanPath = url.pathname.replace(/\/+$/, '');
72
+ if (
73
+ !cleanPath ||
74
+ cleanPath === '/platform' ||
75
+ cleanPath.startsWith('/platform/') ||
76
+ cleanPath === '/view' ||
77
+ cleanPath.startsWith('/view/')
78
+ ) {
79
+ return `${url.origin}/service`;
80
+ }
81
+ return `${url.origin}${cleanPath}`.replace(/\/+$/, '');
82
+ }
83
+
84
+ function getProfile(config, name) {
85
+ const profileName = name || config.currentProfile;
86
+ if (!profileName) {
87
+ throw new Error('未选择平台 profile,请先执行 openxiangda platform add <name> <url>');
88
+ }
89
+ const profile = config.profiles[profileName];
90
+ if (!profile) {
91
+ throw new Error(`平台 profile 不存在: ${profileName}`);
92
+ }
93
+ return { profileName, profile };
94
+ }
95
+
96
+ function loadProjectState(cwd = process.cwd()) {
97
+ return readJson(path.join(cwd, PROJECT_STATE_FILE), {
98
+ version: 1,
99
+ profiles: {},
100
+ });
101
+ }
102
+
103
+ function saveProjectState(state, cwd = process.cwd()) {
104
+ const dir = path.join(cwd, PROJECT_DIR);
105
+ fs.mkdirSync(dir, { recursive: true });
106
+ fs.writeFileSync(
107
+ path.join(cwd, PROJECT_STATE_FILE),
108
+ `${JSON.stringify({ version: 1, ...state }, null, 2)}\n`
109
+ );
110
+ }
111
+
112
+ module.exports = {
113
+ CONFIG_FILE,
114
+ PROJECT_STATE_FILE,
115
+ getProfile,
116
+ loadConfig,
117
+ loadProjectState,
118
+ normalizeBaseUrl,
119
+ saveConfig,
120
+ saveProjectState,
121
+ };
package/lib/http.js ADDED
@@ -0,0 +1,47 @@
1
+ const { maskText } = require('./utils');
2
+
3
+ async function requestJson(baseUrl, apiPath, options = {}) {
4
+ if (typeof fetch !== 'function') {
5
+ throw new Error('当前 Node.js 版本不支持 fetch,请使用 Node.js 18 或更高版本');
6
+ }
7
+
8
+ const url = `${baseUrl.replace(/\/+$/, '')}${apiPath}`;
9
+ const headers = {
10
+ accept: 'application/json',
11
+ ...(options.headers || {}),
12
+ };
13
+ if (options.body !== undefined) {
14
+ headers['content-type'] = 'application/json';
15
+ }
16
+ if (options.accessToken) {
17
+ headers.authorization = `Bearer ${options.accessToken}`;
18
+ }
19
+
20
+ const response = await fetch(url, {
21
+ method: options.method || 'GET',
22
+ headers,
23
+ body:
24
+ options.body === undefined ? undefined : JSON.stringify(options.body),
25
+ });
26
+
27
+ const text = await response.text();
28
+ let payload = null;
29
+ if (text) {
30
+ try {
31
+ payload = JSON.parse(text);
32
+ } catch {
33
+ payload = { message: text };
34
+ }
35
+ }
36
+
37
+ if (!response.ok) {
38
+ const message = payload?.message || response.statusText || 'request failed';
39
+ throw new Error(maskText(`HTTP ${response.status}: ${message}`));
40
+ }
41
+
42
+ return payload;
43
+ }
44
+
45
+ module.exports = {
46
+ requestJson,
47
+ };
package/lib/skills.js ADDED
@@ -0,0 +1,371 @@
1
+ const fs = require('fs');
2
+ const os = require('os');
3
+ const path = require('path');
4
+ const { version: packageVersion } = require('../package.json');
5
+
6
+ const ROOT_DIR = path.join(__dirname, '..');
7
+ const SOURCE_SKILLS_DIR = path.join(ROOT_DIR, 'openxiangda-skills');
8
+ const SOURCE_REFERENCES_DIR = path.join(SOURCE_SKILLS_DIR, 'references');
9
+ const INSTALL_MANIFEST = '.openxiangda-skill-install.json';
10
+ const MANAGER = 'openxiangda';
11
+
12
+ const SKILL_SPECS = [
13
+ {
14
+ name: 'openxiangda',
15
+ displayName: 'OpenXiangda',
16
+ shortDescription: '私有化低代码平台 CLI 与 AI skill 入口。',
17
+ sourceRelativePath: 'openxiangda-skills',
18
+ type: 'root',
19
+ },
20
+ {
21
+ name: 'openxiangda-core',
22
+ displayName: 'OpenXiangda Core',
23
+ shortDescription: '登录、profile、workspace 绑定与发布上下文。',
24
+ sourceRelativePath: 'openxiangda-skills/skills/openxiangda-core',
25
+ type: 'subskill',
26
+ },
27
+ {
28
+ name: 'openxiangda-app',
29
+ displayName: 'OpenXiangda App',
30
+ shortDescription: '应用、菜单、快照和资源状态管理。',
31
+ sourceRelativePath: 'openxiangda-skills/skills/openxiangda-app',
32
+ type: 'subskill',
33
+ },
34
+ {
35
+ name: 'openxiangda-form',
36
+ displayName: 'OpenXiangda Form',
37
+ shortDescription: '表单页和流程表单页开发发布。',
38
+ sourceRelativePath: 'openxiangda-skills/skills/openxiangda-form',
39
+ type: 'subskill',
40
+ },
41
+ {
42
+ name: 'openxiangda-page',
43
+ displayName: 'OpenXiangda Page',
44
+ shortDescription: '自定义代码页开发、绑定和发布。',
45
+ sourceRelativePath: 'openxiangda-skills/skills/openxiangda-page',
46
+ type: 'subskill',
47
+ },
48
+ {
49
+ name: 'openxiangda-workflow-automation',
50
+ displayName: 'OpenXiangda Workflow Automation',
51
+ shortDescription: '流程、自动化、触发器和 JS_CODE 节点。',
52
+ sourceRelativePath: 'openxiangda-skills/skills/openxiangda-workflow-automation',
53
+ type: 'subskill',
54
+ },
55
+ {
56
+ name: 'openxiangda-permission-settings',
57
+ displayName: 'OpenXiangda Permission Settings',
58
+ shortDescription: '角色、权限组、表单设置和公开访问。',
59
+ sourceRelativePath: 'openxiangda-skills/skills/openxiangda-permission-settings',
60
+ type: 'subskill',
61
+ },
62
+ {
63
+ name: 'openxiangda-inspect',
64
+ displayName: 'OpenXiangda Inspect',
65
+ shortDescription: '只读快照、资源诊断和状态核对。',
66
+ sourceRelativePath: 'openxiangda-skills/skills/openxiangda-inspect',
67
+ type: 'subskill',
68
+ },
69
+ ];
70
+
71
+ function getDefaultCodexSkillsDir(env = process.env) {
72
+ const codexHome = env.CODEX_HOME || path.join(os.homedir(), '.codex');
73
+ return path.join(codexHome, 'skills');
74
+ }
75
+
76
+ function getDefaultClaudeSkillsDir(env = process.env) {
77
+ const claudeHome = env.CLAUDE_HOME || path.join(os.homedir(), '.claude');
78
+ return path.join(claudeHome, 'skills');
79
+ }
80
+
81
+ function getDefaultQoderSkillsDir(env = process.env) {
82
+ const qoderHome = env.QODER_HOME || path.join(os.homedir(), '.qoder');
83
+ return path.join(qoderHome, 'skills');
84
+ }
85
+
86
+ function resolveSkillsDir(dest, env = process.env) {
87
+ if (!dest) return getDefaultCodexSkillsDir(env);
88
+ if (typeof dest !== 'string') {
89
+ throw new Error('缺少 --dest 参数值');
90
+ }
91
+ if (dest === '~') return os.homedir();
92
+ if (dest.startsWith('~/')) return path.join(os.homedir(), dest.slice(2));
93
+ return path.resolve(dest);
94
+ }
95
+
96
+ function validateAgent(agent) {
97
+ const normalized = agent || 'dual';
98
+ if (normalized !== 'codex' && normalized !== 'claude' && normalized !== 'qoder' && normalized !== 'dual') {
99
+ throw new Error(`不支持的 skill agent: ${normalized}。支持的选项: codex, claude, qoder, dual`);
100
+ }
101
+ return normalized;
102
+ }
103
+
104
+ function getDualSkillsDirs(agent, env = process.env) {
105
+ if (agent === 'codex') {
106
+ return [getDefaultCodexSkillsDir(env)];
107
+ } else if (agent === 'claude') {
108
+ return [getDefaultClaudeSkillsDir(env)];
109
+ } else if (agent === 'qoder') {
110
+ return [getDefaultQoderSkillsDir(env)];
111
+ } else if (agent === 'dual') {
112
+ return [getDefaultCodexSkillsDir(env), getDefaultClaudeSkillsDir(env), getDefaultQoderSkillsDir(env)];
113
+ }
114
+ throw new Error(`不支持的 agent: ${agent}`);
115
+ }
116
+
117
+ function listLegacySkills(skillsDir) {
118
+ try {
119
+ return fs
120
+ .readdirSync(skillsDir, { withFileTypes: true })
121
+ .filter(entry => entry.isDirectory() && entry.name.startsWith('ai-lowcode-'))
122
+ .map(entry => entry.name)
123
+ .sort();
124
+ } catch {
125
+ return [];
126
+ }
127
+ }
128
+
129
+ function readManifest(targetDir) {
130
+ try {
131
+ return JSON.parse(
132
+ fs.readFileSync(path.join(targetDir, INSTALL_MANIFEST), 'utf8')
133
+ );
134
+ } catch {
135
+ return null;
136
+ }
137
+ }
138
+
139
+ function getSkillStatus(spec, skillsDir) {
140
+ const targetDir = path.join(skillsDir, spec.name);
141
+ if (!fs.existsSync(targetDir)) {
142
+ return {
143
+ name: spec.name,
144
+ status: 'missing',
145
+ targetDir,
146
+ sourceRelativePath: spec.sourceRelativePath,
147
+ };
148
+ }
149
+
150
+ const manifest = readManifest(targetDir);
151
+ if (!manifest || manifest.manager !== MANAGER) {
152
+ return {
153
+ name: spec.name,
154
+ status: 'foreign',
155
+ targetDir,
156
+ sourceRelativePath: spec.sourceRelativePath,
157
+ };
158
+ }
159
+
160
+ const isCurrent =
161
+ manifest.packageVersion === packageVersion &&
162
+ manifest.sourceRelativePath === spec.sourceRelativePath;
163
+ return {
164
+ name: spec.name,
165
+ status: isCurrent ? 'installed' : 'outdated',
166
+ targetDir,
167
+ sourceRelativePath: spec.sourceRelativePath,
168
+ installedVersion: manifest.packageVersion || null,
169
+ installedAt: manifest.installedAt || null,
170
+ };
171
+ }
172
+
173
+ function getSkillStatusReport(options = {}) {
174
+ const agent = validateAgent(options.agent);
175
+ const env = options.env || process.env;
176
+
177
+ // 获取目标目录列表
178
+ const skillsDirs = getDualSkillsDirs(agent, env);
179
+
180
+ const results = [];
181
+ for (const skillsDir of skillsDirs) {
182
+ const skills = SKILL_SPECS.map(spec => getSkillStatus(spec, skillsDir));
183
+ results.push({
184
+ agent,
185
+ skillsDir,
186
+ skills,
187
+ });
188
+ }
189
+
190
+ const legacySkills = listLegacySkills(skillsDirs[0]); // 只取第一个目录的遗留技能
191
+ const warnings = buildWarnings(legacySkills);
192
+
193
+ return {
194
+ agent,
195
+ skillsDirs,
196
+ packageVersion,
197
+ results,
198
+ legacySkills,
199
+ warnings,
200
+ };
201
+ }
202
+
203
+ function installSkills(options = {}) {
204
+ const agent = validateAgent(options.agent);
205
+ const dryRun = Boolean(options.dryRun);
206
+ const force = Boolean(options.force);
207
+
208
+ // 获取目标目录列表
209
+ const skillsDirs = getDualSkillsDirs(agent, options.env);
210
+
211
+ const results = [];
212
+ for (const skillsDir of skillsDirs) {
213
+ const before = SKILL_SPECS.map(spec => getSkillStatus(spec, skillsDir));
214
+ const conflicts = before.filter(item => item.status === 'foreign');
215
+
216
+ if (conflicts.length > 0 && !force && !dryRun) {
217
+ const names = conflicts.map(item => item.name).join(', ');
218
+ throw new Error(
219
+ `目标目录存在非 OpenXiangda 管理的同名 skill: ${names}。如需覆盖请传 --force`
220
+ );
221
+ }
222
+
223
+ const operations = before.map(item => ({
224
+ name: item.name,
225
+ operation: planOperation(item.status, force),
226
+ targetDir: item.targetDir,
227
+ sourceRelativePath: item.sourceRelativePath,
228
+ }));
229
+
230
+ if (!dryRun) {
231
+ fs.mkdirSync(skillsDir, { recursive: true });
232
+ for (const spec of SKILL_SPECS) {
233
+ installOneSkill(spec, skillsDir);
234
+ }
235
+ }
236
+
237
+ const after = dryRun
238
+ ? before
239
+ : SKILL_SPECS.map(spec => getSkillStatus(spec, skillsDir));
240
+
241
+ results.push({
242
+ agent,
243
+ skillsDir,
244
+ packageVersion,
245
+ dryRun,
246
+ force,
247
+ operations,
248
+ skills: after,
249
+ });
250
+ }
251
+
252
+ const legacySkills = listLegacySkills(skillsDirs[0]); // 只取第一个目录的遗留技能
253
+ const warnings = buildWarnings(legacySkills);
254
+
255
+ return {
256
+ agent,
257
+ skillsDirs,
258
+ packageVersion,
259
+ dryRun,
260
+ force,
261
+ results,
262
+ legacySkills,
263
+ warnings,
264
+ message: dryRun
265
+ ? 'dry-run completed; no files were changed'
266
+ : agent === 'dual'
267
+ ? 'OpenXiangda skills installed to Claude, Codex, and Qoder. Restart all to pick up new skills.'
268
+ : `OpenXiangda skills installed to ${agent}. Restart ${agent} to pick up new skills.`,
269
+ };
270
+ }
271
+
272
+ function planOperation(status, force) {
273
+ if (status === 'missing') return 'install';
274
+ if (status === 'foreign') return force ? 'overwrite-foreign' : 'blocked';
275
+ if (status === 'outdated') return 'update';
276
+ return 'refresh';
277
+ }
278
+
279
+ function installOneSkill(spec, skillsDir) {
280
+ const targetDir = path.join(skillsDir, spec.name);
281
+ const stagingDir = path.join(
282
+ skillsDir,
283
+ `.openxiangda-${spec.name}-${process.pid}-${Date.now()}.tmp`
284
+ );
285
+
286
+ fs.rmSync(stagingDir, { recursive: true, force: true });
287
+ try {
288
+ if (spec.type === 'root') {
289
+ copyRootSkill(stagingDir);
290
+ } else {
291
+ copySubskill(spec, stagingDir);
292
+ }
293
+ writeAgentMetadata(spec, stagingDir);
294
+ writeInstallManifest(spec, stagingDir);
295
+
296
+ fs.rmSync(targetDir, { recursive: true, force: true });
297
+ fs.renameSync(stagingDir, targetDir);
298
+ } catch (error) {
299
+ fs.rmSync(stagingDir, { recursive: true, force: true });
300
+ throw error;
301
+ }
302
+ }
303
+
304
+ function copyRootSkill(stagingDir) {
305
+ fs.cpSync(SOURCE_SKILLS_DIR, stagingDir, {
306
+ recursive: true,
307
+ dereference: false,
308
+ });
309
+ }
310
+
311
+ function copySubskill(spec, stagingDir) {
312
+ const sourceDir = path.join(ROOT_DIR, spec.sourceRelativePath);
313
+ fs.mkdirSync(stagingDir, { recursive: true });
314
+ const skillMarkdown = fs
315
+ .readFileSync(path.join(sourceDir, 'SKILL.md'), 'utf8')
316
+ .replace(/\.\.\/\.\.\/references\//g, 'references/');
317
+ fs.writeFileSync(path.join(stagingDir, 'SKILL.md'), skillMarkdown);
318
+ fs.cpSync(SOURCE_REFERENCES_DIR, path.join(stagingDir, 'references'), {
319
+ recursive: true,
320
+ dereference: false,
321
+ });
322
+ }
323
+
324
+ function writeAgentMetadata(spec, skillDir) {
325
+ const agentsDir = path.join(skillDir, 'agents');
326
+ fs.mkdirSync(agentsDir, { recursive: true });
327
+ const defaultPrompt =
328
+ spec.name === 'openxiangda'
329
+ ? '使用 $openxiangda 处理私有化低代码平台的登录、发布和诊断任务。'
330
+ : `使用 $${spec.name} 处理对应的 OpenXiangda 低代码平台任务。`;
331
+ const content = [
332
+ 'interface:',
333
+ ` display_name: ${yamlQuote(spec.displayName)}`,
334
+ ` short_description: ${yamlQuote(spec.shortDescription)}`,
335
+ ` default_prompt: ${yamlQuote(defaultPrompt)}`,
336
+ '',
337
+ ].join('\n');
338
+ fs.writeFileSync(path.join(agentsDir, 'openai.yaml'), content);
339
+ }
340
+
341
+ function writeInstallManifest(spec, skillDir) {
342
+ const manifest = {
343
+ manager: MANAGER,
344
+ packageVersion,
345
+ sourceRelativePath: spec.sourceRelativePath,
346
+ installedAt: new Date().toISOString(),
347
+ };
348
+ fs.writeFileSync(
349
+ path.join(skillDir, INSTALL_MANIFEST),
350
+ `${JSON.stringify(manifest, null, 2)}\n`
351
+ );
352
+ }
353
+
354
+ function yamlQuote(value) {
355
+ return JSON.stringify(String(value));
356
+ }
357
+
358
+ function buildWarnings(legacySkills) {
359
+ if (legacySkills.length === 0) return [];
360
+ return [
361
+ `检测到旧 ai-lowcode skills: ${legacySkills.join(', ')}。OpenXiangda 不会自动删除它们,请按需手动清理以避免混用。`,
362
+ ];
363
+ }
364
+
365
+ module.exports = {
366
+ INSTALL_MANIFEST,
367
+ SKILL_SPECS,
368
+ getSkillStatusReport,
369
+ installSkills,
370
+ resolveSkillsDir,
371
+ };
package/lib/utils.js ADDED
@@ -0,0 +1,87 @@
1
+ const { spawn } = require('child_process');
2
+ const os = require('os');
3
+
4
+ function maskText(value) {
5
+ return String(value || '')
6
+ .replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, '***token***')
7
+ .replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, match => {
8
+ const [name, domain] = match.split('@');
9
+ return `${name.slice(0, 2)}***@${domain}`;
10
+ })
11
+ .replace(/\b1[3-9]\d{9}\b/g, match => `${match.slice(0, 3)}****${match.slice(-4)}`);
12
+ }
13
+
14
+ function print(value) {
15
+ console.log(maskText(value));
16
+ }
17
+
18
+ function warn(value) {
19
+ console.warn(maskText(value));
20
+ }
21
+
22
+ function fail(value) {
23
+ throw new Error(maskText(value));
24
+ }
25
+
26
+ function parseArgs(argv) {
27
+ const flags = {};
28
+ const positional = [];
29
+ for (let index = 0; index < argv.length; index += 1) {
30
+ const item = argv[index];
31
+ if (!item.startsWith('--')) {
32
+ positional.push(item);
33
+ continue;
34
+ }
35
+
36
+ const eqIndex = item.indexOf('=');
37
+ if (eqIndex > 0) {
38
+ flags[item.slice(2, eqIndex)] = item.slice(eqIndex + 1);
39
+ continue;
40
+ }
41
+
42
+ const key = item.slice(2);
43
+ const next = argv[index + 1];
44
+ if (next && !next.startsWith('--')) {
45
+ flags[key] = next;
46
+ index += 1;
47
+ } else {
48
+ flags[key] = true;
49
+ }
50
+ }
51
+ return { flags, positional };
52
+ }
53
+
54
+ function openBrowser(url) {
55
+ const platform = os.platform();
56
+ const command =
57
+ platform === 'darwin'
58
+ ? 'open'
59
+ : platform === 'win32'
60
+ ? 'cmd'
61
+ : 'xdg-open';
62
+ const args = platform === 'win32' ? ['/c', 'start', '', url] : [url];
63
+ const child = spawn(command, args, {
64
+ detached: true,
65
+ stdio: 'ignore',
66
+ });
67
+ child.unref();
68
+ }
69
+
70
+ function sleep(ms) {
71
+ return new Promise(resolve => setTimeout(resolve, ms));
72
+ }
73
+
74
+ function writeJson(value) {
75
+ console.log(JSON.stringify(value, null, 2));
76
+ }
77
+
78
+ module.exports = {
79
+ fail,
80
+ maskText,
81
+ openBrowser,
82
+ parseArgs,
83
+ print,
84
+ sleep,
85
+ warn,
86
+ writeJson,
87
+ };