dsh-ris-yds 0.0.0-stage → 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.
- package/LICENSE +21 -0
- package/README.md +141 -2
- package/assets/yds-chrome-resource-override.crx +0 -0
- package/bin/check-listing.mjs +247 -0
- package/bin/yds.js +19 -0
- package/cordis.patch.yml +13 -0
- package/lib/browser.js +428 -0
- package/lib/build.js +115 -0
- package/lib/cli.js +1003 -0
- package/lib/confcenter.js +377 -0
- package/lib/config.js +552 -0
- package/lib/fe-dev.js +206 -0
- package/lib/ide-proxy.js +682 -0
- package/lib/ide-watch.js +188 -0
- package/lib/ide.js +702 -0
- package/lib/index.js +267 -0
- package/lib/socks.js +260 -0
- package/lib/token.js +36 -0
- package/lib/yms.js +302 -0
- package/lib/yoncode.js +249 -0
- package/native/yds-rust.darwin-arm64.node +0 -0
- package/native/yds-rust.darwin-x64.node +0 -0
- package/native/yds-rust.linux-arm64.node +0 -0
- package/native/yds-rust.linux-x64.node +0 -0
- package/native/yds-rust.win32-arm64.node +0 -0
- package/native/yds-rust.win32-x64.node +0 -0
- package/package.json +58 -4
- package/skills/dsh-ris-yds/SKILL.md +134 -0
- package/test/browser.test.mjs +442 -0
- package/test/cli.test.mjs +246 -0
- package/test/config.test.mjs +106 -0
- package/test/fe-dev.test.mjs +93 -0
- package/test/ide-config.test.mjs +151 -0
- package/test/ide-proxy.test.mjs +194 -0
- package/test/ide-watch.test.mjs +79 -0
- package/test/launch-proxy.test.mjs +83 -0
- package/test/mode-separation.test.mjs +147 -0
- package/test/proxy-standalone.test.mjs +118 -0
- package/test/yoncode.test.mjs +159 -0
package/lib/config.js
ADDED
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
// dsh-ris-yds config resolution.
|
|
2
|
+
//
|
|
3
|
+
// Requirements (user 2026-11-06):
|
|
4
|
+
// 1. No machine-specific content in project docs; every IP/port comes from
|
|
5
|
+
// settings.json (project, YonCode standard, READ-ONLY for us) or
|
|
6
|
+
// .env (workspace root, OPTIONAL, machine level; shared with dsh-ris-ide).
|
|
7
|
+
// 2. .env is not required: SOCKS configured -> proxy, else direct.
|
|
8
|
+
// 3. Never casually modify project files: yonasset/.vscode/settings.json is
|
|
9
|
+
// read-only; we write only dsh-owned state (.dsh-ris-yds/, yms-home in the
|
|
10
|
+
// same standard format YonCode itself deploys).
|
|
11
|
+
// 5. No dependence on local resources: a fresh machine + this plugin + the
|
|
12
|
+
// project git clone must be able to develop (confcenter config is pulled
|
|
13
|
+
// live; kernel comes from jfrog).
|
|
14
|
+
//
|
|
15
|
+
// Source priority (low -> high):
|
|
16
|
+
// defaults < .env < process env (YDS_*/YMS_PORT) < CLI overrides.
|
|
17
|
+
|
|
18
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
|
|
19
|
+
import { execFileSync } from 'node:child_process'
|
|
20
|
+
import { join, resolve, dirname } from 'node:path'
|
|
21
|
+
import { fileURLToPath } from 'node:url'
|
|
22
|
+
|
|
23
|
+
export const PLUGIN_DIR = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
24
|
+
|
|
25
|
+
export const IS_WINDOWS = process.platform === 'win32'
|
|
26
|
+
export const IS_MAC = process.platform === 'darwin'
|
|
27
|
+
export const IS_WSL = detectWsl()
|
|
28
|
+
|
|
29
|
+
function detectWsl() {
|
|
30
|
+
if (IS_WINDOWS) return false
|
|
31
|
+
if (process.env.WSL_DISTRO_NAME !== undefined) return true
|
|
32
|
+
try {
|
|
33
|
+
return /microsoft/i.test(readFileSync('/proc/version', 'utf8'))
|
|
34
|
+
} catch {
|
|
35
|
+
return false
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Parse an env-format file (KEY=VALUE, tolerates CRLF, comments, blanks). */
|
|
40
|
+
export function parseEnvFile(text) {
|
|
41
|
+
const out = {}
|
|
42
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
43
|
+
const line = rawLine.trim()
|
|
44
|
+
if (line === '' || line.startsWith('#')) continue
|
|
45
|
+
const eq = line.indexOf('=')
|
|
46
|
+
if (eq <= 0) continue
|
|
47
|
+
const key = line.slice(0, eq).trim()
|
|
48
|
+
let value = line.slice(eq + 1)
|
|
49
|
+
// tolerate quotes the user wrapped around values
|
|
50
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
51
|
+
value = value.slice(1, -1)
|
|
52
|
+
}
|
|
53
|
+
out[key] = value.trim()
|
|
54
|
+
}
|
|
55
|
+
return out
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Find the project root: the directory whose .vscode/settings.json carries
|
|
60
|
+
* yds.* keys. Search order: cwd itself -> up to 4 ancestor levels (agent
|
|
61
|
+
* sessions often start inside the project tree) -> one level of children
|
|
62
|
+
* (the workspace root usually holds the project dir).
|
|
63
|
+
*/
|
|
64
|
+
export function findProjectDir(workspace) {
|
|
65
|
+
const candidates = []
|
|
66
|
+
// ancestors (bounded)
|
|
67
|
+
{
|
|
68
|
+
let cur = workspace
|
|
69
|
+
for (let i = 0; i < 5 && cur !== undefined; i += 1) {
|
|
70
|
+
candidates.push(cur)
|
|
71
|
+
const parent = dirname(cur)
|
|
72
|
+
if (parent === cur) break
|
|
73
|
+
cur = parent
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
for (const name of readdirSync(workspace)) {
|
|
79
|
+
const full = join(workspace, name)
|
|
80
|
+
if (statSync(full).isDirectory() && !name.startsWith('.') && name !== 'node_modules') {
|
|
81
|
+
candidates.push(full)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
} catch { /* unreadable cwd: only the cwd itself */ }
|
|
85
|
+
for (const dir of candidates) {
|
|
86
|
+
const p = join(dir, '.vscode', 'settings.json')
|
|
87
|
+
if (!existsSync(p)) continue
|
|
88
|
+
try {
|
|
89
|
+
const json = JSON.parse(readFileSync(p, 'utf8'))
|
|
90
|
+
if (json !== null && typeof json === 'object' && Object.keys(json).some((k) => k.startsWith('yds.'))) {
|
|
91
|
+
return { dir, settings: json }
|
|
92
|
+
}
|
|
93
|
+
} catch { /* malformed: skip */ }
|
|
94
|
+
}
|
|
95
|
+
return null
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Read the optional machine-level `.env` (workspace root, shared with dsh-ris-ide). */
|
|
99
|
+
/**
|
|
100
|
+
* Find the shared `.env`.
|
|
101
|
+
*
|
|
102
|
+
* It is deliberately NOT inside the project (a clone must not carry the tunnel
|
|
103
|
+
* credentials), so the lookup walks up from the workspace: `<ws>/.env`,
|
|
104
|
+
* `<ws>/../.env`, … up to four levels. First hit wins.
|
|
105
|
+
* @param workspace - the workspace (or project) directory to start from.
|
|
106
|
+
* @returns `{ path, exists, values }` — `path` is where the file WOULD be.
|
|
107
|
+
*/
|
|
108
|
+
export function readSettingsEnv(workspace) {
|
|
109
|
+
let dir = workspace
|
|
110
|
+
for (let depth = 0; depth < 5; depth += 1) {
|
|
111
|
+
const path = join(dir, '.env')
|
|
112
|
+
if (existsSync(path)) return { path, exists: true, values: parseEnvFile(readFileSync(path, 'utf8')) }
|
|
113
|
+
const parent = dirname(dir)
|
|
114
|
+
if (parent === dir) break
|
|
115
|
+
dir = parent
|
|
116
|
+
}
|
|
117
|
+
return { path: join(workspace, '.env'), exists: false, values: {} }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Detect the YDS module name + context from the project tree. */
|
|
121
|
+
export function detectModule(projectDir, ymsHome) {
|
|
122
|
+
// Candidate A: yonbuilder.json in the project root or its direct children
|
|
123
|
+
// (the backend project, e.g. c-ass-kk-hans-be/yonbuilder.json: {code:"..."}).
|
|
124
|
+
// The root-level module.xml often describes the *scripts* module
|
|
125
|
+
// (name=...-scripts, service_type=sql) — wrong for the backend runtime — so
|
|
126
|
+
// it is only a last resort.
|
|
127
|
+
const yonbuilderCodes = []
|
|
128
|
+
const dirs = [projectDir]
|
|
129
|
+
try {
|
|
130
|
+
for (const name of readdirSync(projectDir)) {
|
|
131
|
+
const full = join(projectDir, name)
|
|
132
|
+
if (statSync(full).isDirectory() && !name.startsWith('.') && name !== 'node_modules') dirs.push(full)
|
|
133
|
+
}
|
|
134
|
+
} catch { /* unreadable project dir */ }
|
|
135
|
+
for (const dir of dirs) {
|
|
136
|
+
try {
|
|
137
|
+
const p = join(dir, 'yonbuilder.json')
|
|
138
|
+
if (!existsSync(p)) continue
|
|
139
|
+
const json = JSON.parse(readFileSync(p, 'utf8'))
|
|
140
|
+
if (typeof json?.code === 'string' && json.code !== '' && !yonbuilderCodes.includes(json.code)) {
|
|
141
|
+
yonbuilderCodes.push(json.code)
|
|
142
|
+
}
|
|
143
|
+
} catch { /* malformed: skip */ }
|
|
144
|
+
}
|
|
145
|
+
// Candidate B: modules already deployed into yms-home (deployed = intended).
|
|
146
|
+
const deployed = []
|
|
147
|
+
try {
|
|
148
|
+
for (const name of readdirSync(join(ymsHome, 'modules'))) {
|
|
149
|
+
const full = join(ymsHome, 'modules', name)
|
|
150
|
+
if (statSync(full).isDirectory()) deployed.push(name)
|
|
151
|
+
}
|
|
152
|
+
} catch { /* no yms-home yet */ }
|
|
153
|
+
|
|
154
|
+
let name = null
|
|
155
|
+
const both = yonbuilderCodes.find((code) => deployed.includes(code))
|
|
156
|
+
if (both !== undefined) name = both
|
|
157
|
+
else if (yonbuilderCodes.length > 0) name = yonbuilderCodes[0]
|
|
158
|
+
else if (deployed.length > 0) name = deployed[0]
|
|
159
|
+
if (name === null) {
|
|
160
|
+
// Last resort: module.xml at the project root.
|
|
161
|
+
try {
|
|
162
|
+
const p = join(projectDir, 'module.xml')
|
|
163
|
+
if (existsSync(p)) {
|
|
164
|
+
const m = /<module[^>]*\sname="([^"]+)"/.exec(readFileSync(p, 'utf8'))
|
|
165
|
+
if (m !== null) name = m[1]
|
|
166
|
+
}
|
|
167
|
+
} catch { /* ignore */ }
|
|
168
|
+
}
|
|
169
|
+
if (name === null) return null
|
|
170
|
+
// Context from the deployed module descriptor if available.
|
|
171
|
+
let context = '/'
|
|
172
|
+
try {
|
|
173
|
+
const p = join(ymsHome, 'modules', name, name, 'WEB-INF', 'classes', 'iuap-yms-module', 'module.xml')
|
|
174
|
+
if (existsSync(p)) {
|
|
175
|
+
const m = /<module[^>]*\scontext="([^"]*)"/.exec(readFileSync(p, 'utf8'))
|
|
176
|
+
if (m !== null && m[1] !== '') context = m[1]
|
|
177
|
+
}
|
|
178
|
+
} catch { /* ignore */ }
|
|
179
|
+
return { name, context }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Locate a JDK: settings/env hint, JAVA_HOME, PATH, yms-home hint, Windows devkit. */
|
|
183
|
+
export function detectJava(cfg) {
|
|
184
|
+
const candidates = []
|
|
185
|
+
const fromEnv = cfg.envValues?.['java.path'] ?? process.env.YDS_JAVA_PATH
|
|
186
|
+
if (typeof fromEnv === 'string' && fromEnv !== '') candidates.push(fromEnv)
|
|
187
|
+
if (process.env.JAVA_HOME !== undefined) {
|
|
188
|
+
candidates.push(join(process.env.JAVA_HOME, 'bin', IS_WINDOWS ? 'java.exe' : 'java'))
|
|
189
|
+
}
|
|
190
|
+
const fromHome = join(cfg.ymsHome ?? '', '.javaExecutable')
|
|
191
|
+
if (cfg.ymsHome !== undefined && existsSync(fromHome)) {
|
|
192
|
+
try {
|
|
193
|
+
const v = readFileSync(fromHome, 'utf8').trim()
|
|
194
|
+
if (v !== '') candidates.push(v)
|
|
195
|
+
} catch { /* ignore */ }
|
|
196
|
+
}
|
|
197
|
+
if (IS_WINDOWS) candidates.push('java.exe') // PATH lookup
|
|
198
|
+
candidates.push('java')
|
|
199
|
+
for (const candidate of candidates) {
|
|
200
|
+
if (candidate.includes('/') || candidate.includes('\\')) {
|
|
201
|
+
if (existsSync(candidate)) return candidate
|
|
202
|
+
} else {
|
|
203
|
+
// PATH lookup: existsSync on the bare name is unreliable cross-OS
|
|
204
|
+
try {
|
|
205
|
+
execFileSync(candidate, ['-version'], { stdio: 'ignore', timeout: 15000 })
|
|
206
|
+
return candidate
|
|
207
|
+
} catch { /* keep trying */ }
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return null
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Resolve the full effective configuration for a workspace.
|
|
215
|
+
* @param {string} workspace - absolute session workspace root
|
|
216
|
+
* @returns the config object + a `problems` list of human-readable gaps.
|
|
217
|
+
*/
|
|
218
|
+
/**
|
|
219
|
+
* Pick the front-end project entry that belongs to this module.
|
|
220
|
+
*
|
|
221
|
+
* `yds.webConfig` is a map keyed by front-end project (e.g. `c-ass-kk-hans-fe`);
|
|
222
|
+
* each value carries `yds.domainKey`, `yds.localDevUrl` (where its dev server
|
|
223
|
+
* listens) and the debug/build scripts. The entry whose domainKey matches the
|
|
224
|
+
* backend module is the one whose pages load this module's extender; failing that a
|
|
225
|
+
* single entry is unambiguous, and otherwise the first one wins.
|
|
226
|
+
* @param {object|undefined} webConfig - the raw `yds.webConfig` value.
|
|
227
|
+
* @param {string|null} module - the resolved backend module name.
|
|
228
|
+
* @returns {{code: string, domainKey: string, microFrontendType: string, port: number, devUrl: string, debugScript: string, buildScript: string, buildDistPath: string}} entry (port defaults to 3004).
|
|
229
|
+
*/
|
|
230
|
+
export function pickFrontend(webConfig, module) {
|
|
231
|
+
const fallback = { code: '', domainKey: module ?? '', microFrontendType: '', port: DEFAULT_FE_PORT, devUrl: '', debugScript: 'debug', buildScript: 'build', buildDistPath: '' }
|
|
232
|
+
if (webConfig === null || typeof webConfig !== 'object') return fallback
|
|
233
|
+
const entries = Object.entries(webConfig)
|
|
234
|
+
.filter(([, value]) => value !== null && typeof value === 'object')
|
|
235
|
+
.map(([code, value]) => ({ code, value }))
|
|
236
|
+
if (entries.length === 0) return fallback
|
|
237
|
+
const matched = entries.find(({ value }) => module !== null && value['yds.domainKey'] === module)
|
|
238
|
+
?? (entries.length === 1 ? entries[0] : entries.find(({ value }) => value['yds.domainKey'] !== undefined) ?? entries[0])
|
|
239
|
+
const value = matched.value
|
|
240
|
+
const devUrl = typeof value['yds.localDevUrl'] === 'string' ? value['yds.localDevUrl'] : ''
|
|
241
|
+
return {
|
|
242
|
+
code: matched.code,
|
|
243
|
+
domainKey: typeof value['yds.domainKey'] === 'string' && value['yds.domainKey'] !== '' ? value['yds.domainKey'] : (module ?? ''),
|
|
244
|
+
microFrontendType: typeof value['yds.microFrontendType'] === 'string' ? value['yds.microFrontendType'] : '',
|
|
245
|
+
port: portOfUrl(devUrl) || DEFAULT_FE_PORT,
|
|
246
|
+
devUrl,
|
|
247
|
+
debugScript: typeof value['yds.debugScript'] === 'string' && value['yds.debugScript'] !== '' ? value['yds.debugScript'] : 'debug',
|
|
248
|
+
buildScript: typeof value['yds.buildScript'] === 'string' && value['yds.buildScript'] !== '' ? value['yds.buildScript'] : 'build',
|
|
249
|
+
buildDistPath: typeof value['yds.buildDistPath'] === 'string' ? value['yds.buildDistPath'] : '',
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* The TCP port of a URL, or 0 when it has none / does not parse.
|
|
255
|
+
* @param {string} value - e.g. `http://127.0.0.1:3004`.
|
|
256
|
+
* @returns {number} port.
|
|
257
|
+
*/
|
|
258
|
+
export function portOfUrl(value) {
|
|
259
|
+
// Only an explicit port counts: a dev URL without one tells us nothing about
|
|
260
|
+
// where the dev server listens, and 80/443 would be a wrong answer.
|
|
261
|
+
try {
|
|
262
|
+
return Number(new URL(String(value)).port) || 0
|
|
263
|
+
} catch {
|
|
264
|
+
return 0
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Remember which port the last start used.
|
|
270
|
+
*
|
|
271
|
+
* The port is chosen from "the first free one", so while an instance runs that port
|
|
272
|
+
* is by definition taken — without the memory every `yds status` would report a
|
|
273
|
+
* different port than the running JVM. Kept in the state directory next to the pid.
|
|
274
|
+
* @param {string} stateDir - `<workspace>/.dsh-ris-yds`.
|
|
275
|
+
* @returns {number|null} the remembered port, or null when there is none.
|
|
276
|
+
*/
|
|
277
|
+
export function readRememberedPort(stateDir) {
|
|
278
|
+
try {
|
|
279
|
+
const value = Number(readFileSync(join(stateDir, 'yms-port'), 'utf8').trim())
|
|
280
|
+
return Number.isInteger(value) && value > 0 ? value : null
|
|
281
|
+
} catch {
|
|
282
|
+
return null
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Default webpack dev-server port of a yds front-end project. */
|
|
287
|
+
/** Default webpack dev-server port of a yds front-end project. */
|
|
288
|
+
export const DEFAULT_FE_PORT = 3004
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Find the front-end project by looking at what is on disk.
|
|
292
|
+
*
|
|
293
|
+
* A yds front end ships `resource_override_rules.json` naming the dev server it
|
|
294
|
+
* expects (`http://127.0.0.1:3004/static/…`), so the port can be read from the
|
|
295
|
+
* project instead of assumed. Used only when `yds.webConfig` is absent.
|
|
296
|
+
* @param {string} projectDir - the yds project root.
|
|
297
|
+
* @param {string|null} module - backend module, for the fallback domainKey.
|
|
298
|
+
* @returns {object} a frontend entry (see pickFrontend).
|
|
299
|
+
*/
|
|
300
|
+
export function discoverFrontend(projectDir, module) {
|
|
301
|
+
const empty = pickFrontend(undefined, module)
|
|
302
|
+
let entries = []
|
|
303
|
+
try {
|
|
304
|
+
entries = readdirSync(projectDir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules')
|
|
305
|
+
} catch {
|
|
306
|
+
return empty
|
|
307
|
+
}
|
|
308
|
+
for (const entry of entries) {
|
|
309
|
+
const file = join(projectDir, entry.name, 'resource_override_rules.json')
|
|
310
|
+
if (existsSync(file) === false) continue
|
|
311
|
+
let port = 0
|
|
312
|
+
try {
|
|
313
|
+
const text = readFileSync(file, 'utf8')
|
|
314
|
+
const found = /127\.0\.0\.1:(\d{2,5})/.exec(text)
|
|
315
|
+
port = found === null ? 0 : Number(found[1])
|
|
316
|
+
} catch {
|
|
317
|
+
port = 0
|
|
318
|
+
}
|
|
319
|
+
if (port <= 0) continue
|
|
320
|
+
return {
|
|
321
|
+
...empty,
|
|
322
|
+
code: entry.name,
|
|
323
|
+
// The front-end directory name minus the module suffix is the domainKey the
|
|
324
|
+
// platform uses (`c-ass-kk-hans-fe` → `c-ass-kk-hans`, which is also the module).
|
|
325
|
+
domainKey: module ?? entry.name,
|
|
326
|
+
port,
|
|
327
|
+
devUrl: `http://127.0.0.1:${port}`,
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return empty
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* First TCP port at or above `from` that nothing is listening on.
|
|
335
|
+
*
|
|
336
|
+
* Binding is the only reliable test (a pidfile or config can be stale), and it is
|
|
337
|
+
* released immediately — this runs before the JVM starts, never while it runs.
|
|
338
|
+
* @param {number} from - preferred port.
|
|
339
|
+
* @param {{attempts?: number}} [options] - how far to walk.
|
|
340
|
+
* @returns {number} the port chosen (falls back to `from` when all are taken).
|
|
341
|
+
*/
|
|
342
|
+
export function findFreePort(from, options = {}) {
|
|
343
|
+
const attempts = options.attempts ?? 50
|
|
344
|
+
for (let offset = 0; offset < attempts; offset += 1) {
|
|
345
|
+
const candidate = from + offset
|
|
346
|
+
if (candidate > 65535) break
|
|
347
|
+
if (isPortFree(candidate)) return candidate
|
|
348
|
+
}
|
|
349
|
+
return from
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Is a port free on the loopback interface?
|
|
354
|
+
* @param {number} port - the port to test.
|
|
355
|
+
* @returns {boolean} free when the bind succeeds.
|
|
356
|
+
*/
|
|
357
|
+
export function isPortFree(port) {
|
|
358
|
+
// A bind attempt is the only reliable test (pidfiles and configs go stale), and it
|
|
359
|
+
// has to happen synchronously inside a sync config builder — so it runs in a tiny
|
|
360
|
+
// child process that exits 0 when the bind succeeded.
|
|
361
|
+
const script = `const s=require('net').createServer();s.once('error',()=>process.exit(1));s.listen(${port},'127.0.0.1',()=>s.close(()=>process.exit(0)))`
|
|
362
|
+
try {
|
|
363
|
+
execFileSync(process.execPath, ['-e', script], { timeout: 4000, stdio: 'ignore' })
|
|
364
|
+
return true
|
|
365
|
+
} catch {
|
|
366
|
+
return false
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export function resolveConfig(workspace) {
|
|
371
|
+
const problems = []
|
|
372
|
+
const project = findProjectDir(workspace)
|
|
373
|
+
const settingsEnv = readSettingsEnv(workspace)
|
|
374
|
+
const envValues = settingsEnv.values
|
|
375
|
+
|
|
376
|
+
const projectSettings = project?.settings ?? {}
|
|
377
|
+
const pick = (key, fallback) => {
|
|
378
|
+
if (typeof projectSettings[key] === 'string' && projectSettings[key] !== '') return projectSettings[key]
|
|
379
|
+
return fallback
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const ymsHome = resolve(workspace, envValues['local.ymsHome'] ?? process.env.YMS_HOME ?? (project !== null ? join(project.dir, 'yms-home') : join(workspace, 'yms-home')))
|
|
383
|
+
|
|
384
|
+
const module = project !== null ? detectModule(project.dir, ymsHome) : null
|
|
385
|
+
// YonCode keeps the front-end project settings in `.vscode/settings.json` under
|
|
386
|
+
// `yds.webConfig`: one entry per front-end project with its OWN dev URL/port and
|
|
387
|
+
// domainKey. That is the project's declaration of where its dev server lives, so
|
|
388
|
+
// the override rules read it instead of assuming 3004.
|
|
389
|
+
// `yds.webConfig` is written by YonCode into the project it opened; a checkout
|
|
390
|
+
// that never saw YonCode (the WSL copy) has no such key, so the front-end project's
|
|
391
|
+
// own override file is the next witness of where its dev server listens.
|
|
392
|
+
const frontend = projectSettings['yds.webConfig'] !== undefined
|
|
393
|
+
? pickFrontend(projectSettings['yds.webConfig'], module)
|
|
394
|
+
: discoverFrontend(project !== null ? project.dir : workspace, module)
|
|
395
|
+
|
|
396
|
+
// SOCKS: optional by design (requirement 2). No host -> direct mode.
|
|
397
|
+
const socksHost = envValues['socks.host'] ?? process.env.YDS_SOCKS_HOST
|
|
398
|
+
const socksPortRaw = envValues['socks.port'] ?? process.env.YDS_SOCKS_PORT
|
|
399
|
+
const socks = socksHost !== undefined && socksHost !== ''
|
|
400
|
+
? { host: socksHost, port: Number(socksPortRaw ?? 1080) || 1080 }
|
|
401
|
+
: null
|
|
402
|
+
|
|
403
|
+
const workbenchUrl = (pick('yds.workbenchUrl') ?? process.env.YDS_WORKBENCH_URL ?? '').replace(/\/+$/, '')
|
|
404
|
+
const serverBindAddress = pick('yds.serverBindAddress') ?? envValues['local.bindAddress'] ?? process.env.YDS_BIND
|
|
405
|
+
const serverPort = Number(pick('yds.serverPort') ?? envValues['local.serverPort'] ?? process.env.YDS_SERVER_PORT ?? 31010) || 31010
|
|
406
|
+
const env = pick('yds.env') ?? 'dev'
|
|
407
|
+
const accessKey = pick('yds.accessKey') ?? process.env.YDS_ACCESS_KEY
|
|
408
|
+
const accessSecret = pick('yds.accessSecret') ?? process.env.YDS_ACCESS_SECRET
|
|
409
|
+
|
|
410
|
+
// Local YMS listen port: the callback port is taken by the host (or by a Windows
|
|
411
|
+
// YonCode that shares it), so start two above it and walk up to the first free
|
|
412
|
+
// one. No configuration to keep in sync, no "port already in use" on a machine
|
|
413
|
+
// that also runs YonCode. An explicit YMS_PORT still wins (tests, CI).
|
|
414
|
+
const ymsPort = Number(process.env.YMS_PORT ?? 0) > 0
|
|
415
|
+
? Number(process.env.YMS_PORT)
|
|
416
|
+
: (readRememberedPort(join(workspace, '.dsh-ris-yds')) ?? findFreePort(serverPort + 2))
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
// git (optional, requirement 2: proxy only when SOCKS configured)
|
|
420
|
+
const git = envValues['git.url'] !== undefined
|
|
421
|
+
? {
|
|
422
|
+
url: envValues['git.url'],
|
|
423
|
+
tokenName: envValues['git.token.name'] ?? '',
|
|
424
|
+
tokenPwd: envValues['git.token.pwd'] ?? '',
|
|
425
|
+
commit: envValues['git.commit'] ?? 'never',
|
|
426
|
+
proxy: socks !== null ? `socks5h://${socks.host}:${socks.port}` : undefined,
|
|
427
|
+
}
|
|
428
|
+
: null
|
|
429
|
+
|
|
430
|
+
// maven: local repo + settings.xml. Auto-detect a workspace-scoped repo
|
|
431
|
+
// (<workspace>/.m2 — used when the machine keeps a project-local repository);
|
|
432
|
+
// otherwise default ~/.m2 and generate jfrog settings at build time.
|
|
433
|
+
const workspaceMavenSettings = join(workspace, '.m2', 'settings.xml')
|
|
434
|
+
const workspaceMavenRepo = join(workspace, '.m2', 'repository')
|
|
435
|
+
const maven = {
|
|
436
|
+
repo: envValues['maven.repo'] ?? process.env.YDS_MAVEN_REPO
|
|
437
|
+
?? (existsSync(workspaceMavenRepo) ? workspaceMavenRepo : null),
|
|
438
|
+
settingsXml: envValues['maven.settings'] ?? process.env.YDS_MAVEN_SETTINGS
|
|
439
|
+
?? (existsSync(workspaceMavenSettings) ? workspaceMavenSettings : null),
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const rawModule = module?.name ?? null
|
|
443
|
+
// module flows into filesystem paths (logs/<module>, modules/<module>...) —
|
|
444
|
+
// reject anything that could traverse or escape (path traversal guard).
|
|
445
|
+
const moduleOk = rawModule !== null && /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(rawModule)
|
|
446
|
+
const cfg = {
|
|
447
|
+
workspace,
|
|
448
|
+
platform: process.platform,
|
|
449
|
+
arch: process.arch,
|
|
450
|
+
isWindows: IS_WINDOWS,
|
|
451
|
+
isWsl: IS_WSL,
|
|
452
|
+
projectDir: project?.dir ?? null,
|
|
453
|
+
projectSettingsPath: project !== null ? join(project.dir, '.vscode', 'settings.json') : null,
|
|
454
|
+
settingsEnv,
|
|
455
|
+
envValues,
|
|
456
|
+
module: moduleOk ? rawModule : null,
|
|
457
|
+
context: moduleOk ? (module?.context ?? '/') : '/',
|
|
458
|
+
env,
|
|
459
|
+
workbenchUrl,
|
|
460
|
+
accessKey: accessKey ?? null,
|
|
461
|
+
accessSecret: accessSecret ?? null,
|
|
462
|
+
serverBindAddress: serverBindAddress ?? null,
|
|
463
|
+
serverPort,
|
|
464
|
+
ymsPort,
|
|
465
|
+
ymsHome,
|
|
466
|
+
socks,
|
|
467
|
+
dbForwardPort: Number(envValues['socks.dbPort'] ?? 1521) || 1521,
|
|
468
|
+
redisForwardPort: Number(envValues['socks.redisPort'] ?? 6890) || 6890,
|
|
469
|
+
// Which tunnel engine to use: 'auto' borrows dsh-ris-ide's engine (one owner
|
|
470
|
+
// for the 127.0.0.1 ports, shared with the IDE dialog); 'self' forces the
|
|
471
|
+
// bundled forwarder (a machine without dsh-ris-ide, or a deliberate split).
|
|
472
|
+
// Optional: where YonCode is installed (`yds yoncode`). Empty means "search
|
|
473
|
+
// the platform defaults"; when set it is authoritative.
|
|
474
|
+
yoncodeHome: envValues['yoncode.home'] ?? '',
|
|
475
|
+
proxyEngine: envValues['proxy.engine'] ?? 'auto',
|
|
476
|
+
proxyListenPort: Number(envValues['proxy.listenPort'] ?? 0) || 0,
|
|
477
|
+
// Two mutually exclusive launch modes for the YonCode bundle:
|
|
478
|
+
// proxy - the JVM carries the SOCKS flags and reads yms-home/config,
|
|
479
|
+
// i.e. the copy rewritten to 127.0.0.1 for the local forwarder.
|
|
480
|
+
// direct - the JVM carries no proxy flag at all; when dsh still needs the
|
|
481
|
+
// tunnel, it reads the untouched copy in yms-home/config-direct.
|
|
482
|
+
// The two modes must never share a config directory or leak flags into each
|
|
483
|
+
// other: a 127.0.0.1 address in a direct launch (or a stale socks flag in a
|
|
484
|
+
// proxy-less launch) points the JVM at a port nobody listens on.
|
|
485
|
+
launchMode: String(envValues['ide.launch'] ?? 'proxy').trim() === 'direct' ? 'direct' : 'proxy',
|
|
486
|
+
configDir: join(ymsHome, 'config'),
|
|
487
|
+
configDirectDir: join(ymsHome, 'config-direct'),
|
|
488
|
+
// ProxyBridge rules for `*java*`. Off by default because the YMS JVM gets
|
|
489
|
+
// `-DsocksProxyHost` from the YonCode bundle patch (`yds yoncode patch`), and
|
|
490
|
+
// funnelling Oracle through WinDivert breaks the TNS handshake (reset after
|
|
491
|
+
// CONNECT). Set `proxy.javaRules=on` when you need system-level coverage for
|
|
492
|
+
// java processes that have no JVM proxy flag.
|
|
493
|
+
proxyJavaRules: (envValues['proxy.javaRules'] ?? '') === 'on',
|
|
494
|
+
// Whether `yds start` should also write the scoped git proxy entry (so plain
|
|
495
|
+
// git / dsh-git honour SOCKS without a -c flag). 'off' leaves git alone.
|
|
496
|
+
gitProxy: envValues['git.proxy'] ?? 'auto',
|
|
497
|
+
// Front-end dev server (webpack `debug:extend`): the resource-override rules
|
|
498
|
+
// point at it, so the port is config, not a literal in the rule text.
|
|
499
|
+
// Front-end dev server, as declared by the project (see pickFrontend).
|
|
500
|
+
frontend,
|
|
501
|
+
fePort: frontend.port,
|
|
502
|
+
git,
|
|
503
|
+
maven,
|
|
504
|
+
stateDir: join(workspace, '.dsh-ris-yds'),
|
|
505
|
+
}
|
|
506
|
+
cfg.java = detectJava(cfg)
|
|
507
|
+
|
|
508
|
+
if (project === null) problems.push('未找到含 yds.* 键的 .vscode/settings.json(项目配置缺失或项目未检出)')
|
|
509
|
+
if (cfg.accessKey == null || cfg.accessSecret == null) problems.push('缺少 yds.accessKey / yds.accessSecret(settings.json)——YYCToken 与 confcenter 拉取不可用')
|
|
510
|
+
if (cfg.serverBindAddress == null) problems.push('缺少 yds.serverBindAddress(settings.json)——rdbga 回调地址无法确定')
|
|
511
|
+
if (cfg.module == null) problems.push(rawModule !== null ? `模块名 "${rawModule}" 含非法字符(仅允许字母数字._-),已拒绝` : '未能从 yonbuilder.json / module.xml 识别模块名')
|
|
512
|
+
if (cfg.java == null) problems.push('未找到 JDK(可用 .env 的 java.path 或 JAVA_HOME 指定)')
|
|
513
|
+
const launchRaw = envValues['ide.launch']
|
|
514
|
+
if (launchRaw !== undefined && !['proxy', 'direct'].includes(String(launchRaw).trim())) {
|
|
515
|
+
problems.push(`ide.launch 只能是 proxy / direct(当前 "${launchRaw}")`)
|
|
516
|
+
}
|
|
517
|
+
return { config: cfg, problems }
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/** Human-readable dump of the effective config (secrets masked). */
|
|
521
|
+
export function describeConfig(cfg) {
|
|
522
|
+
const mask = (v) => (typeof v === 'string' && v !== '' ? `${v.slice(0, 4)}…${v.slice(-2)}(${v.length})` : '(缺失)')
|
|
523
|
+
// never echo user:pass@ / user:token@ embedded in a URL
|
|
524
|
+
const stripUrlUser = (u) => (typeof u === 'string' && u !== '' ? u.replace(/^([a-z][a-z0-9+.-]*:\/\/)[^/@]+@/i, '$1***@') : '(缺失)')
|
|
525
|
+
const lines = [
|
|
526
|
+
`workspace: ${cfg.workspace}`,
|
|
527
|
+
`platform: ${cfg.platform}-${cfg.arch}${cfg.isWsl ? ' (WSL)' : ''}${cfg.isWindows ? ' (Windows)' : ''}`,
|
|
528
|
+
`project: ${cfg.projectDir ?? '(未检出)'} module: ${cfg.module ?? '(未检出)'}`,
|
|
529
|
+
`workbenchUrl: ${cfg.workbenchUrl || '(缺失)'} env: ${cfg.env}`,
|
|
530
|
+
`accessKey: ${mask(cfg.accessKey)} accessSecret: ${mask(cfg.accessSecret)}`,
|
|
531
|
+
`serverBindAddress: ${cfg.serverBindAddress ?? '(缺失)'} serverPort: ${cfg.serverPort}`,
|
|
532
|
+
`ymsPort(本地监听): ${cfg.ymsPort} ymsHome: ${cfg.ymsHome}`,
|
|
533
|
+
`java: ${cfg.java ?? '(未找到)'}`,
|
|
534
|
+
`socks: ${cfg.socks !== null ? `${cfg.socks.host}:${cfg.socks.port} (db→127.0.0.1:${cfg.dbForwardPort}, redis→127.0.0.1:${cfg.redisForwardPort})` : '(未配置,直连)'}`,
|
|
535
|
+
`launch: ide.launch=${cfg.launchMode} config=${cfg.configDir}${cfg.socks !== null ? ` direct=${cfg.configDirectDir}` : ''}`,
|
|
536
|
+
`proxy engine: ${cfg.proxyEngine}${cfg.proxyListenPort > 0 ? ` listen=${cfg.proxyListenPort}` : ''} git.proxy=${cfg.gitProxy} fe.port=${cfg.fePort}(来自项目配置)`,
|
|
537
|
+
`.env: ${cfg.settingsEnv.exists ? cfg.settingsEnv.path : '(不存在,可选)'}`,
|
|
538
|
+
`git: ${cfg.git !== null ? `${stripUrlUser(cfg.git.url)} proxy=${cfg.git.proxy ?? '直连'} commit=${cfg.git.commit}` : '(未配置)'}`,
|
|
539
|
+
`maven: repo=${cfg.maven.repo ?? '默认 ~/.m2'} settings=${cfg.maven.settingsXml ?? '(生成 jfrog settings)'}`,
|
|
540
|
+
]
|
|
541
|
+
return lines.join('\n')
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/** jfrog repository base derived from the workbench URL (same host). */
|
|
545
|
+
export function artifactoryUrl(cfg) {
|
|
546
|
+
if (typeof cfg.workbenchUrl !== 'string' || cfg.workbenchUrl.trim() === '') {
|
|
547
|
+
throw new Error('workbenchUrl 缺失——在项目 .vscode/settings.json 配置 yds.workbenchUrl 后重试')
|
|
548
|
+
}
|
|
549
|
+
let u
|
|
550
|
+
try { u = new URL(cfg.workbenchUrl) } catch { throw new Error(`workbenchUrl 不是合法 URL: ${cfg.workbenchUrl}`) }
|
|
551
|
+
return `${u.protocol}//${u.host}/artifactory/repository/`
|
|
552
|
+
}
|