openyida 2026.7.18 → 2026.7.21

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 (200) hide show
  1. package/README.md +37 -2
  2. package/bin/yida.js +113 -18
  3. package/docs/capabilities.md +202 -0
  4. package/lib/agent-center/agent-center.js +2 -3
  5. package/lib/app/app-list.js +3 -4
  6. package/lib/app/build-page.js +13 -6
  7. package/lib/app/check-page.js +16 -6
  8. package/lib/app/compile.js +17 -7
  9. package/lib/app/create-app.js +14 -2
  10. package/lib/app/create-form/api-path.js +25 -0
  11. package/lib/app/create-form/args.js +13 -0
  12. package/lib/app/create-form/commands.js +4 -0
  13. package/lib/app/create-form/definition-reader.js +23 -23
  14. package/lib/app/create-form/field-normalizers.js +26 -7
  15. package/lib/app/create-form/schema-patch.js +4 -1
  16. package/lib/app/create-form.js +155 -68
  17. package/lib/app/create-page.js +47 -10
  18. package/lib/app/generate-page.js +20 -1
  19. package/lib/app/get-form-config.js +2 -1
  20. package/lib/app/get-schema.js +67 -15
  21. package/lib/app/import-app.js +94 -42
  22. package/lib/app/list-forms.js +4 -8
  23. package/lib/app/page-ir.js +140 -0
  24. package/lib/app/publish.js +289 -556
  25. package/lib/app/schema-field-resolution.js +370 -0
  26. package/lib/app/services/app-reader.js +85 -0
  27. package/lib/app/services/app-service.js +235 -0
  28. package/lib/app/services/canvas-page-compiler.js +91 -0
  29. package/lib/app/services/canvas-page-schema-builder.js +149 -0
  30. package/lib/app/services/field-bindings.js +68 -0
  31. package/lib/app/services/form-compiler.js +1922 -0
  32. package/lib/app/services/form-mode-service.js +182 -0
  33. package/lib/app/services/form-schema-patcher.js +333 -0
  34. package/lib/app/services/form-schema-reader.js +59 -0
  35. package/lib/app/services/form-service.js +379 -0
  36. package/lib/app/services/form-validation.js +562 -0
  37. package/lib/app/services/native-page-compiler.js +92 -0
  38. package/lib/app/services/native-page-schema-builder.js +428 -0
  39. package/lib/app/services/page-resource-service.js +185 -0
  40. package/lib/app/update-app.js +22 -9
  41. package/lib/app/update-form-config.js +4 -2
  42. package/lib/asset/asset-cmd.js +19 -6
  43. package/lib/auth/oauth-loopback.js +376 -15
  44. package/lib/auth/token-auth.js +34 -3
  45. package/lib/auth/token-store.js +22 -4
  46. package/lib/basic-info/basic-info.js +2 -1
  47. package/lib/bridge/bridge.js +43 -18
  48. package/lib/core/agent-capabilities.js +5 -3
  49. package/lib/core/command-errors.js +38 -0
  50. package/lib/core/command-manifest.js +59 -1
  51. package/lib/core/copy.js +12 -0
  52. package/lib/core/doctor.js +15 -4
  53. package/lib/core/env-cmd.js +1 -1
  54. package/lib/core/env.js +7 -6
  55. package/lib/core/i18n.js +66 -8
  56. package/lib/core/legacy-schema-guard.js +408 -0
  57. package/lib/core/locales/en.js +38 -46
  58. package/lib/core/locales/zh.js +30 -46
  59. package/lib/core/sample.js +12 -2
  60. package/lib/core/utils.js +334 -66
  61. package/lib/core/yida-client.js +108 -5
  62. package/lib/corp-efficiency/corp-efficiency.js +2 -2
  63. package/lib/corp-manager/corp-manager.js +3 -3
  64. package/lib/formula/evaluate.js +11 -68
  65. package/lib/formula/field-refs.js +83 -0
  66. package/lib/i18n-management/i18n-management.js +2 -3
  67. package/lib/process/configure-process.js +44 -1992
  68. package/lib/process/create-process.js +54 -87
  69. package/lib/process/preview-process.js +20 -8
  70. package/lib/process/services/process-compiler.js +2056 -0
  71. package/lib/process/services/process-reader.js +232 -0
  72. package/lib/process/services/process-resource-service.js +571 -0
  73. package/lib/process/services/process-service.js +122 -0
  74. package/lib/process/services/process-stage-checkpoint.js +246 -0
  75. package/lib/report/append.js +7 -26
  76. package/lib/report/http.js +31 -5
  77. package/lib/report/index.js +10 -2
  78. package/lib/samples/yida-canvas-custom-page/business-list.canvas.jsx +63 -14
  79. package/lib/samples/yida-canvas-custom-page/dashboard-overview.canvas.jsx +176 -32
  80. package/lib/samples/yida-canvas-custom-page/dashboard-starter.canvas.jsx +5 -2
  81. package/lib/samples/yida-canvas-custom-page/data-management.canvas.jsx +228 -14
  82. package/lib/samples/yida-canvas-custom-page/portal-shell-home.canvas.jsx +2 -2
  83. package/lib/samples/yida-canvas-custom-page/split-pane-detail.canvas.jsx +206 -10
  84. package/lib/samples/yida-custom-page/data-management.oyd.jsx +14 -3
  85. package/lib/samples/yida-custom-page/design-tokens.js +2 -2
  86. package/lib/samples/yida-custom-page/detail-profile.oyd.jsx +1 -1
  87. package/lib/samples/yida-custom-page/portal-shell-home.oyd.jsx +7 -2
  88. package/lib/samples/yida-custom-page/split-pane-detail.oyd.jsx +2 -1
  89. package/lib/schema/adapters/app-adapter.js +210 -0
  90. package/lib/schema/adapters/form-adapter.js +1362 -0
  91. package/lib/schema/adapters/keys.js +19 -0
  92. package/lib/schema/adapters/page-adapter.js +566 -0
  93. package/lib/schema/adapters/process-adapter.js +519 -0
  94. package/lib/schema/applier.js +1878 -0
  95. package/lib/schema/apply-store.js +1083 -0
  96. package/lib/schema/command.js +645 -0
  97. package/lib/schema/dependency-graph.js +83 -0
  98. package/lib/schema/errors.js +481 -0
  99. package/lib/schema/hash.js +9 -0
  100. package/lib/schema/manifest-limits.js +177 -0
  101. package/lib/schema/manifest-loader.js +309 -0
  102. package/lib/schema/manifest-schema-v1.json +193 -0
  103. package/lib/schema/normalize-manifest.js +281 -0
  104. package/lib/schema/page-canvas-foundation.js +447 -0
  105. package/lib/schema/page-data-source-builder.js +561 -0
  106. package/lib/schema/page-foundation.js +556 -0
  107. package/lib/schema/page-source-loader.js +314 -0
  108. package/lib/schema/planner.js +691 -0
  109. package/lib/schema/remote-dispatch-boundary.js +39 -0
  110. package/lib/schema/remote-missing.js +21 -0
  111. package/lib/schema/remote-reader.js +189 -0
  112. package/lib/schema/resource-registry.js +157 -0
  113. package/lib/schema/server-revision.js +71 -0
  114. package/lib/schema/sort.js +28 -0
  115. package/lib/schema/state-store.js +589 -0
  116. package/package.json +10 -8
  117. package/scripts/check-release-risks.js +254 -0
  118. package/scripts/generate-command-docs.js +6 -1
  119. package/scripts/i18n-baseline.json +11 -11
  120. package/scripts/postinstall.js +8 -12
  121. package/scripts/validate-ci.sh +15 -11
  122. package/scripts/validate-i18n.js +30 -9
  123. package/scripts/validate-package-size.js +5 -2
  124. package/scripts/validate-skills.js +4 -0
  125. package/scripts/validate-structure.js +15 -0
  126. package/yida-skills/SKILL.md +117 -30
  127. package/yida-skills/references/schema-as-code-phase1.md +99 -0
  128. package/yida-skills/references/setup-and-env.md +59 -59
  129. package/yida-skills/references/task-retrospective.md +6 -6
  130. package/yida-skills/skills/yida-app/SKILL.md +78 -17
  131. package/yida-skills/skills/yida-canvas-custom-page/SKILL.md +18 -7
  132. package/yida-skills/skills/yida-canvas-custom-page/references/data-bridge-guide.md +3 -3
  133. package/yida-skills/skills/yida-canvas-custom-page/references/page-generation-guide.md +15 -0
  134. package/yida-skills/skills/yida-create-app/SKILL.md +21 -3
  135. package/yida-skills/skills/yida-create-form-page/SKILL.md +19 -1
  136. package/yida-skills/skills/yida-create-page/SKILL.md +16 -3
  137. package/yida-skills/skills/yida-create-process/SKILL.md +100 -4
  138. package/yida-skills/skills/yida-custom-page/SKILL.md +19 -4
  139. package/yida-skills/skills/yida-dashboard/SKILL.md +1 -1
  140. package/yida-skills/skills/yida-dashboard/references/pitfalls.md +1 -1
  141. package/yida-skills/skills/yida-get-schema/SKILL.md +49 -5
  142. package/yida-skills/skills/yida-i18n/SKILL.md +1 -1
  143. package/yida-skills/skills/yida-integration/SKILL.md +7 -1
  144. package/yida-skills/skills/yida-login/SKILL.md +50 -99
  145. package/yida-skills/skills/yida-page-config/SKILL.md +7 -1
  146. package/yida-skills/skills/yida-page-uiux/references/app/blueprint.md +0 -1
  147. package/yida-skills/skills/yida-page-uiux/references/app/navigation-patterns.md +0 -1
  148. package/yida-skills/skills/yida-page-uiux/references/app/role-journey.md +0 -1
  149. package/yida-skills/skills/yida-page-uiux/references/dashboard/app-blueprint.md +0 -1
  150. package/yida-skills/skills/yida-page-uiux/references/dashboard/chart-contracts.md +0 -1
  151. package/yida-skills/skills/yida-page-uiux/references/dashboard/component-contracts.md +0 -1
  152. package/yida-skills/skills/yida-page-uiux/references/dashboard/layout-patterns.md +0 -1
  153. package/yida-skills/skills/yida-page-uiux/references/dashboard/theme-recipes.md +0 -1
  154. package/yida-skills/skills/yida-page-uiux/references/detail/object-narrative.md +0 -1
  155. package/yida-skills/skills/yida-page-uiux/references/detail/section-patterns.md +0 -1
  156. package/yida-skills/skills/yida-page-uiux/references/detail/timeline-and-related.md +0 -1
  157. package/yida-skills/skills/yida-page-uiux/references/landing/assets-workflow.md +0 -1
  158. package/yida-skills/skills/yida-page-uiux/references/landing/industry-playbooks.md +0 -1
  159. package/yida-skills/skills/yida-page-uiux/references/landing/research-levels.md +0 -1
  160. package/yida-skills/skills/yida-page-uiux/references/landing/section-patterns.md +0 -1
  161. package/yida-skills/skills/yida-page-uiux/references/list/drawer-detail.md +0 -1
  162. package/yida-skills/skills/yida-page-uiux/references/list/empty-loading-error.md +0 -1
  163. package/yida-skills/skills/yida-page-uiux/references/list/filter-patterns.md +0 -1
  164. package/yida-skills/skills/yida-page-uiux/references/list/table-patterns.md +0 -1
  165. package/yida-skills/skills/yida-page-uiux/references/scenes/screen.md +0 -1
  166. package/yida-skills/skills/yida-page-uiux/references/workbench/entry-patterns.md +0 -1
  167. package/yida-skills/skills/yida-page-uiux/references/workbench/portal-layouts.md +0 -1
  168. package/yida-skills/skills/yida-page-uiux/references/workbench/task-feed.md +0 -1
  169. package/yida-skills/skills/yida-page-uiux/workflow/output-decision-block.md +1 -1
  170. package/yida-skills/skills/yida-process-rule/SKILL.md +35 -1
  171. package/yida-skills/skills/yida-publish-page/SKILL.md +46 -4
  172. package/yida-skills/skills/yida-report/SKILL.md +9 -235
  173. package/yida-skills/skills/yida-report/references/schema-builder-details.md +239 -0
  174. package/yida-skills/skills/yida-skill-evaluator/SKILL.md +170 -0
  175. package/yida-skills/skills-index.json +238 -54
  176. package/lib/core/locales/ar.js +0 -834
  177. package/lib/core/locales/de.js +0 -834
  178. package/lib/core/locales/es.js +0 -834
  179. package/lib/core/locales/fr.js +0 -834
  180. package/lib/core/locales/hi.js +0 -834
  181. package/lib/core/locales/ja.js +0 -1227
  182. package/lib/core/locales/ko.js +0 -836
  183. package/lib/core/locales/pt.js +0 -834
  184. package/lib/core/locales/vi.js +0 -834
  185. package/lib/core/locales/zh-HK.js +0 -1263
  186. package/project/pages/src/demo-birthday-game.oyd.jsx +0 -832
  187. package/project/pages/src/demo-chip-insight.oyd.jsx +0 -983
  188. package/project/pages/src/demo-compat-smoke.oyd.jsx +0 -58
  189. package/project/pages/src/demo-crm-batch-entry.oyd.jsx +0 -805
  190. package/project/pages/src/demo-crm-dashboard.oyd.jsx +0 -677
  191. package/project/pages/src/demo-future-vision-2026.oyd.jsx +0 -1102
  192. package/project/pages/src/demo-ppt.oyd.jsx +0 -1192
  193. package/project/pages/src/demo-salary-calculator.oyd.jsx +0 -904
  194. package/project/pages/src/openyida-knowledge-doc.oyd.jsx +0 -1714
  195. package/yida-skills/skills/large-file-write/SKILL.md +0 -93
  196. package/yida-skills/skills/large-file-write/references/write-patterns.md +0 -147
  197. package/yida-skills/skills/large-file-write/scripts/write.js +0 -157
  198. package/yida-skills/skills/sls-log-workbench/SKILL.md +0 -134
  199. package/yida-skills/skills/sls-log-workbench/references/workbench-reference.md +0 -130
  200. package/yida-skills/skills/sls-log-workbench/sls-query.js +0 -295
@@ -4,6 +4,7 @@ const { loadAuthData, triggerLogin, resolveBaseUrl, httpPost, requestWithAutoLog
4
4
  const { t } = require('../core/i18n');
5
5
  const { buildYidaTitleI18n, normalizeYidaLocale, resolveContentLocale } = require('../core/yida-i18n');
6
6
  const { banner, step, label, success, fail, warn, info, error, result, usage } = require('../core/chalk');
7
+ const { throwCommandError, throwUsage } = require('../core/command-errors');
7
8
 
8
9
  function parseArgs() {
9
10
  const args = process.argv.slice(2);
@@ -24,7 +25,7 @@ function parseArgs() {
24
25
  info(t('update_form_config.params_label'));
25
26
  label(t('update_form_config.param_is_render_nav'), '');
26
27
  label(t('update_form_config.param_title'), '');
27
- process.exit(1);
28
+ throwUsage(t('update_form_config.usage'), t('update_form_config.example'));
28
29
  }
29
30
  return {
30
31
  appType: args[0],
@@ -218,10 +219,11 @@ async function main() {
218
219
  }
219
220
  } else {
220
221
  fail(t('common.request_failed_label'));
221
- process.exit(1);
222
+ throwCommandError(t('common.request_failed_label'));
222
223
  }
223
224
  }
224
225
 
225
226
  main().catch((err) => {
226
227
  error(t('common.exception', err.message));
228
+ process.exitCode = err && err.exitCode ? err.exitCode : 1;
227
229
  });
@@ -24,6 +24,7 @@ const {
24
24
  detectImageGenerator,
25
25
  getMaterialSourcingGuidance,
26
26
  } = require('./ai-image');
27
+ const { throwCommandError, throwStatus } = require('../core/command-errors');
27
28
 
28
29
  function printHelp() {
29
30
  console.log(`
@@ -110,8 +111,9 @@ function runStatus(options) {
110
111
 
111
112
  async function runVerifyUrl(options) {
112
113
  if (!options.urls.length) {
113
- console.error('错误:请至少提供一个图片 URL。用法:openyida asset verify-url <url> [<url> ...]');
114
- process.exit(1);
114
+ throwCommandError('错误:请至少提供一个图片 URL。用法:openyida asset verify-url <url> [<url> ...]', {
115
+ code: 'ASSET_VERIFY_URL_INVALID_ARGUMENTS',
116
+ });
115
117
  }
116
118
  const results = await verifyImageUrls(options.urls, {
117
119
  minBytes: options.minBytes,
@@ -132,7 +134,12 @@ async function runVerifyUrl(options) {
132
134
  ' reason=' + r.reason
133
135
  );
134
136
  });
135
- if (!allOk) {process.exit(1);}
137
+ if (!allOk) {
138
+ throwCommandError('图片 URL 校验失败', {
139
+ code: 'ASSET_VERIFY_URL_FAILED',
140
+ details: { results },
141
+ });
142
+ }
136
143
  }
137
144
 
138
145
  async function runResolve(options) {
@@ -164,7 +171,12 @@ async function runResolve(options) {
164
171
  result.materialGaps.forEach((g) => console.log(' - ' + g));
165
172
  }
166
173
  // 非 final 时以非零退出码提示调用方(生成链路可据此拦截「最终版」)
167
- if (result.materialStatus !== 'final') {process.exit(2);}
174
+ if (result.materialStatus !== 'final') {
175
+ throwStatus('素材未达到 final 状态', 2, {
176
+ code: 'ASSET_MATERIAL_NOT_FINAL',
177
+ details: result,
178
+ });
179
+ }
168
180
  }
169
181
 
170
182
  function runGenerate(options) {
@@ -208,9 +220,10 @@ async function run(args) {
208
220
  runGenerate(options);
209
221
  break;
210
222
  default:
211
- console.error('未知子命令: ' + options.subCommand);
212
223
  printHelp();
213
- process.exit(1);
224
+ throwCommandError('未知子命令: ' + options.subCommand, {
225
+ code: 'ASSET_UNKNOWN_SUBCOMMAND',
226
+ });
214
227
  }
215
228
  }
216
229
 
@@ -2,7 +2,10 @@
2
2
 
3
3
  const crypto = require('crypto');
4
4
  const http = require('http');
5
- const { spawn } = require('child_process');
5
+ const fs = require('fs');
6
+ const os = require('os');
7
+ const path = require('path');
8
+ const { spawn, spawnSync } = require('child_process');
6
9
 
7
10
  const CALLBACK_PATH = '/oauth/callback';
8
11
  const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
@@ -11,30 +14,336 @@ function randomToken(bytes = 24) {
11
14
  return crypto.randomBytes(bytes).toString('base64url');
12
15
  }
13
16
 
14
- function openBrowser(url) {
15
- if (process.env.OPENYIDA_NO_BROWSER === '1') {
16
- return false;
17
+ const CHROMIUM_MAC_BUNDLES = {
18
+ 'com.google.chrome': 'com.google.Chrome',
19
+ 'com.microsoft.edgemac': 'com.microsoft.edgemac',
20
+ 'com.brave.browser': 'com.brave.Browser',
21
+ 'com.operasoftware.opera': 'com.operasoftware.Opera',
22
+ 'com.vivaldi.vivaldi': 'com.vivaldi.Vivaldi',
23
+ 'org.chromium.chromium': 'org.chromium.Chromium',
24
+ };
25
+
26
+ // Only these browser families have a known reliable new-window launch path.
27
+ // Anything else (unknown/unsupported default browser) must fall back.
28
+ const SUPPORTED_BROWSER_FAMILIES = new Set(['chromium', 'firefox', 'safari']);
29
+
30
+ // Chromium accepts `--new-window`; Firefox uses the single-dash `-new-window`.
31
+ function newWindowFlag(family) {
32
+ return family === 'firefox' ? '-new-window' : '--new-window';
33
+ }
34
+
35
+ function appleScriptSafe(value) {
36
+ return String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
37
+ }
38
+
39
+ function classifyMacBundleId(bundleId) {
40
+ if (!bundleId) {
41
+ return null;
42
+ }
43
+ const id = String(bundleId).toLowerCase();
44
+ if (id === 'com.apple.safari') {
45
+ return { family: 'safari' };
46
+ }
47
+ if (id === 'org.mozilla.firefox') {
48
+ return { family: 'firefox', bundleId: 'org.mozilla.firefox' };
49
+ }
50
+ if (CHROMIUM_MAC_BUNDLES[id]) {
51
+ return { family: 'chromium', bundleId: CHROMIUM_MAC_BUNDLES[id] };
52
+ }
53
+ return null;
54
+ }
55
+
56
+ function classifyWindowsProgId(progId) {
57
+ if (!progId) {
58
+ return null;
59
+ }
60
+ const id = String(progId).toLowerCase();
61
+ if (id.includes('firefox')) {
62
+ return 'firefox';
63
+ }
64
+ if (
65
+ id.startsWith('chromehtml') ||
66
+ id.startsWith('msedge') ||
67
+ id.startsWith('bravehtml') ||
68
+ id.startsWith('opera') ||
69
+ id.includes('vivaldi') ||
70
+ id.includes('chromium')
71
+ ) {
72
+ return 'chromium';
73
+ }
74
+ return null;
75
+ }
76
+
77
+ function classifyLinuxDesktop(name) {
78
+ if (!name) {
79
+ return null;
80
+ }
81
+ const id = String(name).toLowerCase();
82
+ if (id.includes('firefox')) {
83
+ return 'firefox';
84
+ }
85
+ if (
86
+ id.includes('chrome') ||
87
+ id.includes('chromium') ||
88
+ id.includes('brave') ||
89
+ id.includes('edge') ||
90
+ id.includes('opera') ||
91
+ id.includes('vivaldi')
92
+ ) {
93
+ return 'chromium';
94
+ }
95
+ return null;
96
+ }
97
+
98
+ function parseMacDefaultBrowser(defaultsOutput) {
99
+ if (!defaultsOutput) {
100
+ return null;
101
+ }
102
+ // The secure plist is an array of dicts; each `}` closes one handler entry.
103
+ const blocks = String(defaultsOutput).split('}');
104
+ for (const block of blocks) {
105
+ if (/LSHandlerURLScheme\s*=\s*https\b/i.test(block)) {
106
+ const match = block.match(/LSHandlerRole(?:All|Viewer)\s*=\s*"?([\w.-]+)"?/i);
107
+ if (match) {
108
+ return match[1].toLowerCase();
109
+ }
110
+ }
111
+ }
112
+ return null;
113
+ }
114
+
115
+ function parseWindowsProgId(regOutput) {
116
+ if (!regOutput) {
117
+ return null;
118
+ }
119
+ const match = String(regOutput).match(/ProgId\s+REG_\w+\s+(\S+)/i);
120
+ return match ? match[1] : null;
121
+ }
122
+
123
+ function parseWindowsCommandExec(regOutput) {
124
+ if (!regOutput) {
125
+ return null;
126
+ }
127
+ const match = String(regOutput).match(/REG_\w+\s+(.+)$/m);
128
+ if (!match) {
129
+ return null;
130
+ }
131
+ const value = match[1].trim();
132
+ if (value.startsWith('"')) {
133
+ const end = value.indexOf('"', 1);
134
+ if (end > 1) {
135
+ return value.slice(1, end);
136
+ }
137
+ }
138
+ return value.split(/\s+/)[0] || null;
139
+ }
140
+
141
+ function parseLinuxExec(desktopContent) {
142
+ if (!desktopContent) {
143
+ return null;
144
+ }
145
+ const match = String(desktopContent).match(/^Exec=(.+)$/m);
146
+ if (!match) {
147
+ return null;
148
+ }
149
+ // Drop desktop-entry field codes (%u %U %f %F ...) then take the executable.
150
+ const withoutCodes = match[1].replace(/%[a-zA-Z]/g, '').trim();
151
+ const first = withoutCodes.match(/^"([^"]+)"|^(\S+)/);
152
+ if (!first) {
153
+ return null;
154
+ }
155
+ return first[1] || first[2] || null;
156
+ }
157
+
158
+ function runCapture(command, args) {
159
+ try {
160
+ const result = spawnSync(command, args, {
161
+ encoding: 'utf8',
162
+ timeout: 2500,
163
+ windowsHide: true,
164
+ });
165
+ if (result && typeof result.stdout === 'string' && result.stdout.length > 0) {
166
+ return result.stdout;
167
+ }
168
+ } catch {
169
+ return null;
170
+ }
171
+ return null;
172
+ }
173
+
174
+ // Detect the system default browser so we can open a real new window.
175
+ // Returns { family, bundleId? , execPath? } or null when detection fails or
176
+ // the default browser is not a supported family (unknown ProgId/desktop/bundle).
177
+ // `deps` allows injecting command runner / file reader for deterministic tests.
178
+ function detectDefaultBrowser(platform = process.platform, deps = {}) {
179
+ const run = deps.run || runCapture;
180
+ const readFile = deps.readFile || ((filePath) => fs.readFileSync(filePath, 'utf8'));
181
+ const homedir = deps.homedir || os.homedir;
182
+ try {
183
+ if (platform === 'darwin') {
184
+ const output = run('defaults', [
185
+ 'read',
186
+ 'com.apple.LaunchServices/com.apple.launchservices.secure',
187
+ ]);
188
+ return classifyMacBundleId(parseMacDefaultBrowser(output));
189
+ }
190
+ if (platform === 'win32') {
191
+ const progOutput = run('reg', [
192
+ 'query',
193
+ 'HKCU\\Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\https\\UserChoice',
194
+ '/v',
195
+ 'ProgId',
196
+ ]);
197
+ const progId = parseWindowsProgId(progOutput);
198
+ const family = classifyWindowsProgId(progId);
199
+ // Unknown ProgId (e.g. IE.HTTP or any browser we don't map) -> fall back.
200
+ if (!family) {
201
+ return null;
202
+ }
203
+ const commandOutput =
204
+ run('reg', ['query', `HKCU\\Software\\Classes\\${progId}\\shell\\open\\command`, '/ve']) ||
205
+ run('reg', ['query', `HKEY_CLASSES_ROOT\\${progId}\\shell\\open\\command`, '/ve']);
206
+ const execPath = parseWindowsCommandExec(commandOutput);
207
+ if (!execPath) {
208
+ return null;
209
+ }
210
+ return { family, execPath };
211
+ }
212
+ // linux and other unix-like desktops
213
+ const desktop = run('xdg-settings', ['get', 'default-web-browser']);
214
+ const name = desktop ? desktop.trim() : '';
215
+ if (!name) {
216
+ return null;
217
+ }
218
+ const family = classifyLinuxDesktop(name);
219
+ // Unknown .desktop (e.g. some non-browser or unmapped browser) -> fall back.
220
+ if (!family) {
221
+ return null;
222
+ }
223
+ const candidates = [
224
+ path.join(homedir(), '.local/share/applications', name),
225
+ path.join('/usr/share/applications', name),
226
+ path.join('/usr/local/share/applications', name),
227
+ ];
228
+ for (const candidate of candidates) {
229
+ try {
230
+ const content = readFile(candidate);
231
+ const execPath = parseLinuxExec(content);
232
+ if (execPath) {
233
+ return { family, execPath };
234
+ }
235
+ } catch {
236
+ // try the next candidate location
237
+ }
238
+ }
239
+ return null;
240
+ } catch {
241
+ return null;
17
242
  }
243
+ }
18
244
 
19
- let command = 'xdg-open';
20
- let args = [url];
21
- if (process.platform === 'darwin') {
22
- command = 'open';
23
- } else if (process.platform === 'win32') {
24
- command = 'cmd';
25
- args = ['/c', 'start', '', url];
245
+ function resolveBrowserLauncher(url, platform = process.platform, openMode = 'legacy', browser = null) {
246
+ if (openMode === 'newWindow' && browser && SUPPORTED_BROWSER_FAMILIES.has(browser.family)) {
247
+ if (platform === 'darwin') {
248
+ if (browser.family === 'safari') {
249
+ // Safari has no CLI new-window flag; AppleScript opens a new window.
250
+ const safeUrl = appleScriptSafe(url);
251
+ return {
252
+ command: 'osascript',
253
+ args: [
254
+ '-e',
255
+ `tell application "Safari" to make new document with properties {URL:"${safeUrl}"}`,
256
+ '-e',
257
+ 'tell application "Safari" to activate',
258
+ ],
259
+ };
260
+ }
261
+ if (browser.bundleId) {
262
+ // `open -b <bundle> -n --args --new-window <url>` forces a new window
263
+ // in the specific Chromium/Firefox app instead of a new tab.
264
+ return {
265
+ command: 'open',
266
+ args: ['-b', browser.bundleId, '-n', '--args', newWindowFlag(browser.family), url],
267
+ };
268
+ }
269
+ } else if (browser.execPath) {
270
+ return {
271
+ command: browser.execPath,
272
+ args: [newWindowFlag(browser.family), url],
273
+ };
274
+ }
26
275
  }
27
276
 
277
+ if (platform === 'darwin') {
278
+ return { command: 'open', args: [url] };
279
+ }
280
+ if (platform === 'win32') {
281
+ // rundll32 hands the URL to the default browser as a single argument.
282
+ // Going through `cmd /c start` would let cmd.exe treat the `&` in the
283
+ // OAuth URL as command separators and truncate the query string after
284
+ // redirect_uri, dropping client_id/state and breaking login.
285
+ return { command: 'rundll32', args: ['url.dll,FileProtocolHandler', url] };
286
+ }
287
+ return { command: 'xdg-open', args: [url] };
288
+ }
289
+
290
+ function launchBrowserLauncher(launcher, onError) {
28
291
  try {
29
- const child = spawn(command, args, {
292
+ const child = spawn(launcher.command, launcher.args, {
30
293
  detached: true,
31
294
  stdio: 'ignore',
32
295
  });
33
296
  child.unref();
297
+ if (onError) {
298
+ child.once('error', onError);
299
+ }
34
300
  return true;
35
301
  } catch {
302
+ if (onError) {
303
+ onError();
304
+ }
305
+ return false;
306
+ }
307
+ }
308
+
309
+ function openBrowser(url) {
310
+ if (process.env.OPENYIDA_NO_BROWSER === '1') {
36
311
  return false;
37
312
  }
313
+
314
+ const platform = process.platform;
315
+ const legacyLauncher = resolveBrowserLauncher(url, platform, 'legacy');
316
+ const runLegacy = () => launchBrowserLauncher(legacyLauncher);
317
+
318
+ let browser = null;
319
+ try {
320
+ browser = detectDefaultBrowser(platform);
321
+ } catch {
322
+ browser = null;
323
+ }
324
+
325
+ // Default browser detected and supported → open a real new window in that
326
+ // browser, falling back to the plain default-browser open if launch errors.
327
+ if (browser && SUPPORTED_BROWSER_FAMILIES.has(browser.family)) {
328
+ const newWindowLauncher = resolveBrowserLauncher(url, platform, 'newWindow', browser);
329
+ let fallbackUsed = false;
330
+ const runFallback = () => {
331
+ if (fallbackUsed) {
332
+ return false;
333
+ }
334
+ fallbackUsed = true;
335
+ return runLegacy();
336
+ };
337
+
338
+ const launched = launchBrowserLauncher(newWindowLauncher, runFallback);
339
+ if (!launched) {
340
+ return runFallback();
341
+ }
342
+ return true;
343
+ }
344
+
345
+ // Detection failed → fall back to the system default browser (new tab).
346
+ return runLegacy();
38
347
  }
39
348
 
40
349
  function buildDingtalkOAuthUrl(options) {
@@ -59,8 +368,45 @@ function buildDingtalkOAuthUrl(options) {
59
368
  return url.toString();
60
369
  }
61
370
 
62
- function responseHtml(title, body) {
63
- return `<!doctype html><html><head><meta charset="utf-8"><title>${title}</title></head><body><h3>${title}</h3><p>${body}</p></body></html>`;
371
+ function responseHtml(title, body, options = {}) {
372
+ const {
373
+ autoClose = false,
374
+ autoCloseDelayMs = 1200,
375
+ staticHint = 'You can close this page and return to the terminal.',
376
+ } = options;
377
+
378
+ const closeScript = autoClose ? `
379
+ <script>
380
+ (function() {
381
+ const delay = Math.max(0, ${Number(autoCloseDelayMs)});
382
+ function forceClose() {
383
+ try {
384
+ if (window.opener && !window.opener.closed && window.opener.focus) {
385
+ window.opener.focus();
386
+ }
387
+ } catch (err) {}
388
+
389
+ try {
390
+ window.close();
391
+ } catch (err) {}
392
+
393
+ if (!window.closed) {
394
+ try {
395
+ window.open('', '_self').close();
396
+ } catch (err) {}
397
+ }
398
+ }
399
+
400
+ setTimeout(forceClose, delay);
401
+ })();
402
+ </script>
403
+ ` : '';
404
+
405
+ const closeHint = autoClose
406
+ ? ''
407
+ : `<p>${staticHint}</p>`;
408
+
409
+ return `<!doctype html><html><head><meta charset="utf-8"><title>${title}</title></head><body><h3>${title}</h3><p>${body}</p>${closeHint}${closeScript}</body></html>`;
64
410
  }
65
411
 
66
412
  function runDingtalkLoopback(options = {}) {
@@ -115,7 +461,13 @@ function runDingtalkLoopback(options = {}) {
115
461
 
116
462
  res.statusCode = 200;
117
463
  res.setHeader('content-type', 'text/html; charset=utf-8');
118
- res.end(responseHtml('OpenYida login completed', 'You can close this page and return to the terminal.'));
464
+ res.end(responseHtml(
465
+ 'OpenYida login completed',
466
+ 'Login callback received. Returning to terminal.',
467
+ {
468
+ autoClose: true,
469
+ }
470
+ ));
119
471
  finish(null, {
120
472
  code,
121
473
  authCode,
@@ -152,5 +504,14 @@ function runDingtalkLoopback(options = {}) {
152
504
  module.exports = {
153
505
  runDingtalkLoopback,
154
506
  buildDingtalkOAuthUrl,
507
+ resolveBrowserLauncher,
508
+ detectDefaultBrowser,
509
+ classifyMacBundleId,
510
+ classifyWindowsProgId,
511
+ classifyLinuxDesktop,
512
+ parseMacDefaultBrowser,
513
+ parseWindowsProgId,
514
+ parseWindowsCommandExec,
515
+ parseLinuxExec,
155
516
  CALLBACK_PATH,
156
517
  };
@@ -13,8 +13,10 @@ const {
13
13
  const { runDingtalkLoopback } = require('./oauth-loopback');
14
14
  const {
15
15
  clearTokenSession,
16
+ isHostInjectedTokenMode,
16
17
  loadTokenSession,
17
18
  maskToken,
19
+ normalizeTokenSession,
18
20
  saveTokenSession,
19
21
  } = require('./token-store');
20
22
 
@@ -35,6 +37,7 @@ function appendPath(baseUrl, path) {
35
37
 
36
38
  function getArgEndpoint(options = {}) {
37
39
  return options.endpoint ||
40
+ (options.env && options.env.OPENYIDA_ENDPOINT) ||
38
41
  null;
39
42
  }
40
43
 
@@ -49,7 +52,8 @@ function resolveTokenBaseUrl(options = {}) {
49
52
  }
50
53
 
51
54
  const { config } = getCurrentEnvConfig(options.projectRoot);
52
- const baseUrl = normalizeBaseUrl(process.env.OPENYIDA_ENDPOINT || config.baseUrl, null);
55
+ const env = options.env || process.env;
56
+ const baseUrl = normalizeBaseUrl(env.OPENYIDA_ENDPOINT || config.baseUrl, null);
53
57
  if (!baseUrl) {
54
58
  throw new Error('missing base_url: pass --endpoint or set OPENYIDA_ENDPOINT');
55
59
  }
@@ -299,18 +303,29 @@ async function tokenLogin(options = {}) {
299
303
  function tokenStatus(options = {}) {
300
304
  const session = loadTokenSession(options);
301
305
  if (!session || (!session.access_token && !session.refresh_token)) {
302
- return {
306
+ const env = options.env || process.env;
307
+ const hostInjected = isHostInjectedTokenMode(env);
308
+ const status = {
303
309
  ok: false,
304
310
  auth_mode: 'token',
311
+ auth_source: hostInjected ? 'env' : undefined,
305
312
  status: 'not_logged_in',
306
313
  can_auto_use: false,
307
- token_file: require('./token-store').getTokenFilePath(options),
314
+ failure_reason: hostInjected ? 'env_token_missing' : undefined,
315
+ message: hostInjected
316
+ ? 'host-injected token is missing. Ask the host to inject OPENYIDA_ACCESS_TOKEN or OPENYIDA_REFRESH_TOKEN.'
317
+ : undefined,
308
318
  };
319
+ if (!hostInjected) {
320
+ status.token_file = require('./token-store').getTokenFilePath(options);
321
+ }
322
+ return status;
309
323
  }
310
324
  if (!session.access_token && session.refresh_token) {
311
325
  return {
312
326
  ok: true,
313
327
  auth_mode: 'token',
328
+ auth_source: session.auth_source,
314
329
  status: 'refresh_required',
315
330
  can_auto_use: true,
316
331
  refresh_token: maskToken(session.refresh_token),
@@ -324,6 +339,7 @@ function tokenStatus(options = {}) {
324
339
  // whether this access token must be refreshed.
325
340
  ok: true,
326
341
  auth_mode: 'token',
342
+ auth_source: session.auth_source,
327
343
  status: 'ok',
328
344
  can_auto_use: true,
329
345
  token_type: session.token_type,
@@ -343,6 +359,7 @@ async function tokenRefresh(options = {}) {
343
359
  return {
344
360
  ok: false,
345
361
  auth_mode: 'token',
362
+ auth_source: session && session.auth_source,
346
363
  status: 'missing_refresh_token',
347
364
  can_auto_use: false,
348
365
  };
@@ -380,6 +397,20 @@ async function tokenRefresh(options = {}) {
380
397
  message: normalized.message || 'auth service did not return access_token',
381
398
  };
382
399
  }
400
+ const env = options.env || process.env;
401
+ if (isHostInjectedTokenMode(env) && session.auth_source === 'env') {
402
+ env.OPENYIDA_ACCESS_TOKEN = normalized.access_token;
403
+ if (normalized.refresh_token) {
404
+ env.OPENYIDA_REFRESH_TOKEN = normalized.refresh_token;
405
+ }
406
+ if (normalized.expires_at) {
407
+ env.OPENYIDA_ACCESS_TOKEN_EXPIRES_AT = String(normalized.expires_at);
408
+ }
409
+ return normalizeTokenSession({
410
+ ...normalized,
411
+ auth_source: 'env',
412
+ });
413
+ }
383
414
  return saveTokenSession(normalized, options);
384
415
  }
385
416
 
@@ -119,17 +119,27 @@ function saveTokenSession(session, options = {}) {
119
119
  return normalized;
120
120
  }
121
121
 
122
+ function isHostInjectedTokenMode(env = process.env) {
123
+ const authEnabled = String(env.YIDA_AUTH_ENABLED || '').trim().toLowerCase();
124
+ return ['1', 'true', 'yes', 'on'].includes(authEnabled);
125
+ }
126
+
122
127
  function loadEnvTokenSession(env = process.env) {
128
+ const accessToken = String(env.OPENYIDA_ACCESS_TOKEN || '').trim();
123
129
  const refreshToken = String(env.OPENYIDA_REFRESH_TOKEN || '').trim();
124
- if (!refreshToken) {
130
+ if (!accessToken && !refreshToken) {
125
131
  return null;
126
132
  }
127
133
  return normalizeTokenSession({
134
+ access_token: accessToken,
128
135
  refresh_token: refreshToken,
129
136
  base_url: env.OPENYIDA_ENDPOINT,
130
137
  client_id: env.OPENYIDA_TOKEN_CLIENT_ID,
131
138
  corp_id: env.OPENYIDA_TOKEN_CORP_ID,
132
139
  user_id: env.OPENYIDA_TOKEN_USER_ID,
140
+ token_type: env.OPENYIDA_TOKEN_TYPE,
141
+ expires_at: env.OPENYIDA_ACCESS_TOKEN_EXPIRES_AT,
142
+ expires_in: env.OPENYIDA_ACCESS_TOKEN_EXPIRES_IN,
133
143
  auth_source: 'env',
134
144
  });
135
145
  }
@@ -147,24 +157,31 @@ function loadLocalTokenSession(options = {}) {
147
157
  }
148
158
 
149
159
  function loadTokenSession(options = {}) {
160
+ const env = options.env || process.env;
161
+ const envSession = loadEnvTokenSession(env);
162
+ if (isHostInjectedTokenMode(env)) {
163
+ return envSession;
164
+ }
165
+
150
166
  const localSession = loadLocalTokenSession(options);
151
- const envSession = loadEnvTokenSession(options.env || process.env);
152
167
  if (!localSession && !envSession) {
153
168
  return null;
154
169
  }
155
170
 
156
171
  const localAccessToken = localSession && localSession.access_token;
157
172
  const localRefreshToken = localSession && localSession.refresh_token;
173
+ const envAccessToken = envSession && envSession.access_token;
158
174
  const envRefreshToken = envSession && envSession.refresh_token;
159
175
  const usesLocal = Boolean(localAccessToken || localRefreshToken);
160
176
  const usesEnv = Boolean(
161
- !localRefreshToken && envRefreshToken
177
+ (!localAccessToken && !localRefreshToken && envAccessToken) ||
178
+ (!localRefreshToken && envRefreshToken)
162
179
  );
163
180
 
164
181
  return normalizeTokenSession({
165
182
  ...(envSession || {}),
166
183
  ...(localSession || {}),
167
- access_token: localAccessToken,
184
+ access_token: localAccessToken || (!localSession && envAccessToken),
168
185
  refresh_token: localRefreshToken || envRefreshToken,
169
186
  auth_source: usesLocal && usesEnv ? 'mixed' : (usesLocal ? 'local' : 'env'),
170
187
  });
@@ -210,6 +227,7 @@ module.exports = {
210
227
  normalizeTokenSession,
211
228
  loadEnvTokenSession,
212
229
  loadLocalTokenSession,
230
+ isHostInjectedTokenMode,
213
231
  isAccessTokenUsable,
214
232
  maskToken,
215
233
  resolveEnvName,