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/ide.js
ADDED
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
// dsh-ris-ide integration (user requirement 4): check / update the workspace
|
|
2
|
+
// run config `.dsh-ris-ide/config.json` so the YMS program appears in the
|
|
3
|
+
// ris-ide panel and is start/stop/JDWP-debuggable through ris_dev_* tools.
|
|
4
|
+
//
|
|
5
|
+
// We only UPSERT our own entry by name; every other entry the user keeps is
|
|
6
|
+
// preserved verbatim. JSONC comments are tolerated on read (ris-ide parses
|
|
7
|
+
// JSONC); our write serializes plain JSON with a short header comment — the
|
|
8
|
+
// same thing the ris-ide panel does when it generates the file.
|
|
9
|
+
//
|
|
10
|
+
// ALSO: `.vscode/launch.json` — YonCode generates a VS Code java launch
|
|
11
|
+
// entry ("yms-middleware-cloudnative-jdk-21") on startup via its
|
|
12
|
+
// ensureLaunchConfig so F5 starts/debugs the YMS middleware inside YonCode.
|
|
13
|
+
// We generate the byte-equivalent entry (same name → no duplicate when both
|
|
14
|
+
// sides write) so a workspace shared with YonCode keeps a working F5 path
|
|
15
|
+
// that either side can (re)create. ${command:yds.*}/${config:yds.*}
|
|
16
|
+
// placeholders are resolved by YonCode itself — that is intentional.
|
|
17
|
+
|
|
18
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
19
|
+
import { basename, dirname, join, resolve } from 'node:path'
|
|
20
|
+
import { resolveConfig } from './config.js'
|
|
21
|
+
import { fileURLToPath } from 'node:url'
|
|
22
|
+
|
|
23
|
+
export const IDE_CONFIG = join('.dsh-ris-ide', 'config.json')
|
|
24
|
+
export const LAUNCH_CONFIG = join('.vscode', 'launch.json')
|
|
25
|
+
export const LAUNCH_ENTRY_NAME = 'yms-middleware-cloudnative-jdk-21'
|
|
26
|
+
// Sibling of yms-home/config holding the UNTOUCHED confcenter copy, used by
|
|
27
|
+
// the direct launch mode while dsh itself still goes through SOCKS.
|
|
28
|
+
export const DIRECT_CONFIG_DIRNAME = 'config-direct'
|
|
29
|
+
const HEADER = '// dsh-ris-ide 程序配置(JSONC:允许注释与尾逗号)\n'
|
|
30
|
+
+ '// dsh-ris-yds 维护 YMS 程序条目(名字=项目 parent.artifactId;startCommand 内含 ${JDWP_ARGS} 占位符,ris_dev_debug 会替换为 JDWP 参数)。\n'
|
|
31
|
+
'// 其余条目由用户/面板维护,此工具不会触碰。\n'
|
|
32
|
+
const LAUNCH_HEADER = '// dsh-ris-yds 维护 yms-middleware 运行配置(与 YonCode ensureLaunchConfig 生成的条目同名同构,任一侧写入均有效)。\n'
|
|
33
|
+
'// 其余 configuration 条目由用户/YonCode 维护,此工具不会触碰。\n'
|
|
34
|
+
|
|
35
|
+
/** First match of one XML attribute in the whole document, `''` when absent. */
|
|
36
|
+
export function readXmlAttribute(text, tag, attribute) {
|
|
37
|
+
const match = new RegExp(`<${tag}\\b[^>]*\\s${attribute}\\s*=\\s*"([^"]*)"`).exec(text)
|
|
38
|
+
return match === null ? '' : match[1].trim()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** `<parent><artifactId>` of a pom, `''` when the pom has no parent block. */
|
|
42
|
+
export function readPomParentArtifactId(pomPath) {
|
|
43
|
+
try {
|
|
44
|
+
const text = readFileSync(pomPath, 'utf8')
|
|
45
|
+
const parent = /<parent>([\s\S]*?)<\/parent>/.exec(text)
|
|
46
|
+
if (parent === null) return ''
|
|
47
|
+
const artifact = /<artifactId>\s*([^<\s]+)\s*<\/artifactId>/.exec(parent[1])
|
|
48
|
+
return artifact === null ? '' : artifact[1]
|
|
49
|
+
} catch {
|
|
50
|
+
return ''
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The bootstrap module's pom (`*bootstrap/pom.xml`) under a yds project root. */
|
|
55
|
+
export function findBootstrapPom(projectDir) {
|
|
56
|
+
if (typeof projectDir !== 'string' || projectDir === '') return ''
|
|
57
|
+
try {
|
|
58
|
+
for (const entry of readdirSync(projectDir, { withFileTypes: true })) {
|
|
59
|
+
if (entry.isDirectory() !== true) continue
|
|
60
|
+
if (/bootstrap$/.test(entry.name) !== true) continue
|
|
61
|
+
const pom = join(projectDir, entry.name, 'pom.xml')
|
|
62
|
+
if (existsSync(pom)) return pom
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
// A missing/unreadable project root just falls back to the module name.
|
|
66
|
+
}
|
|
67
|
+
return ''
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Name of the java program, as the user asked: `parent.artifactId` of the
|
|
72
|
+
* bootstrap pom (the aggregator's artifactId, e.g. `c-ass-kk-hans`) — the same
|
|
73
|
+
* value the YMS module is deployed under, so the panel row reads like the
|
|
74
|
+
* project rather than like our own bookkeeping.
|
|
75
|
+
* @param {object} cfg - resolved plugin config.
|
|
76
|
+
* @returns {string} project name, falling back to the YMS module name.
|
|
77
|
+
*/
|
|
78
|
+
export function javaProjectName(cfg) {
|
|
79
|
+
const pom = findBootstrapPom(cfg?.projectDir)
|
|
80
|
+
const name = pom === '' ? '' : readPomParentArtifactId(pom)
|
|
81
|
+
if (name !== '') return name
|
|
82
|
+
return cfg?.module ?? 'local'
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Name of a frontend program: `module.xml`'s `<module name="…">` that sits
|
|
87
|
+
* beside `package.json` (the MDF convention), not the directory name.
|
|
88
|
+
* @param {object} cfg - resolved plugin config.
|
|
89
|
+
* @param {string} [dir] - frontend directory; defaults to the configured one.
|
|
90
|
+
* @returns {string} project name, `''` when no such module.xml exists.
|
|
91
|
+
*/
|
|
92
|
+
export function frontendProjectName(cfg, dir) {
|
|
93
|
+
const explicit = typeof dir === 'string' && dir !== '' ? dir : ''
|
|
94
|
+
if (explicit !== '') {
|
|
95
|
+
const file = join(explicit, 'module.xml')
|
|
96
|
+
if (existsSync(file)) {
|
|
97
|
+
const name = readXmlAttribute(readFileSync(file, 'utf8'), 'module', 'name')
|
|
98
|
+
if (name !== '') return name
|
|
99
|
+
}
|
|
100
|
+
return ''
|
|
101
|
+
}
|
|
102
|
+
const root = cfg?.projectDir
|
|
103
|
+
if (typeof root !== 'string' || root === '') return ''
|
|
104
|
+
const preferred = cfg?.frontend?.code
|
|
105
|
+
const candidates = []
|
|
106
|
+
try {
|
|
107
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
108
|
+
if (entry.isDirectory() !== true || entry.name.startsWith('.') === true) continue
|
|
109
|
+
const dirPath = join(root, entry.name)
|
|
110
|
+
const moduleXml = join(dirPath, 'module.xml')
|
|
111
|
+
if (!existsSync(moduleXml) || !existsSync(join(dirPath, 'package.json'))) continue
|
|
112
|
+
const name = readXmlAttribute(readFileSync(moduleXml, 'utf8'), 'module', 'name')
|
|
113
|
+
if (name !== '') candidates.push({ dir: dirPath, name })
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
return ''
|
|
117
|
+
}
|
|
118
|
+
if (candidates.length === 0) return ''
|
|
119
|
+
const match = candidates.find((item) => item.name === preferred || item.dir.endsWith(`/${preferred}`) === true)
|
|
120
|
+
return (match ?? candidates[0]).name
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function entryName(cfg) {
|
|
124
|
+
return javaProjectName(cfg)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* A row is ours when it runs our CLI, or when it is a spring row named after the
|
|
129
|
+
* project (the legacy name counts too, so apply/remove stay idempotent).
|
|
130
|
+
*
|
|
131
|
+
* The `kind` guard matters: a *frontend* row can legitimately carry the same name
|
|
132
|
+
* (a yds project's package.json name is the backend domainKey), and treating that
|
|
133
|
+
* row as ours would delete it on the next apply.
|
|
134
|
+
*/
|
|
135
|
+
export function isOurs(entry, cfg) {
|
|
136
|
+
if (entry === null || typeof entry !== 'object') return false
|
|
137
|
+
// Our launcher is always a spring row. Checking the kind *first* keeps a frontend
|
|
138
|
+
// row from claiming ours-ness just because an old update copied the command onto it.
|
|
139
|
+
if (entry.kind !== undefined && entry.kind !== 'spring' && entry.kind !== 'manual') return false
|
|
140
|
+
const command = typeof entry.startCommand === 'string' ? entry.startCommand : ''
|
|
141
|
+
if (command.includes('bin/yds.js')) return true
|
|
142
|
+
if (entry.name === entryName(cfg)) return true
|
|
143
|
+
return typeof cfg?.module === 'string' && entry.name === `yds-yms-${cfg.module}`
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The program entry we manage. startCommand runs our cross-OS node CLI in
|
|
148
|
+
* FOREGROUND mode — dsh-ris-ide's run-with-log wrapper owns the lifecycle.
|
|
149
|
+
*/
|
|
150
|
+
export function ourEntry(cfg) {
|
|
151
|
+
const bin = fileURLToPath(new URL('../bin/yds.js', import.meta.url))
|
|
152
|
+
// The startCommand is executed through a shell by ris-ide wrappers (bash /
|
|
153
|
+
// PowerShell). Inside double quotes both treat $, backtick and " as live
|
|
154
|
+
// metacharacters — a plugin path containing them would be command
|
|
155
|
+
// injection. Refuse up front instead of trying to out-quote two shells.
|
|
156
|
+
if (/[$`"]/.test(bin)) {
|
|
157
|
+
throw new Error(`插件路径含 shell 元字符(\$ \` "),无法安全生成 startCommand: ${bin}——请将插件安装到不含这些字符的路径`)
|
|
158
|
+
}
|
|
159
|
+
const esc = bin
|
|
160
|
+
return {
|
|
161
|
+
// A stable id of our own: the config update matches by id first, and without one
|
|
162
|
+
// it fell back to *name* — which hijacked a discovered frontend row that carried
|
|
163
|
+
// the same name and dropped this launcher from the config.
|
|
164
|
+
id: `yds:${entryName(cfg)}`,
|
|
165
|
+
name: entryName(cfg),
|
|
166
|
+
// kind=spring is required for ris_dev_debug (JDWP) eligibility: the panel
|
|
167
|
+
// only debugs spring rows. Our startCommand still wins over the default
|
|
168
|
+
// spring launch (custom startCommand bypasses mvn/mainClass), so this only
|
|
169
|
+
// unlocks the debug button without changing the start path.
|
|
170
|
+
kind: 'spring',
|
|
171
|
+
dir: cfg.workspace,
|
|
172
|
+
startCommand: `node "${esc}" start --foreground --jvm-args "${'${JDWP_ARGS}'}"`,
|
|
173
|
+
logName: entryName(cfg),
|
|
174
|
+
stopGraceMs: 60000,
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Read the current file: { exists, parsed, raw, path }. */
|
|
179
|
+
export function readIdeConfig(workspace) {
|
|
180
|
+
const path = join(workspace, IDE_CONFIG)
|
|
181
|
+
if (!existsSync(path)) return { exists: false, path, parsed: null }
|
|
182
|
+
const raw = readFileSync(path, 'utf8')
|
|
183
|
+
let parsed = null
|
|
184
|
+
try {
|
|
185
|
+
parsed = JSON.parse(stripJsonc(raw))
|
|
186
|
+
} catch { parsed = null }
|
|
187
|
+
return { exists: true, path, parsed, raw }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* The yds project a program directory belongs to, judged from the **parent** dir up.
|
|
192
|
+
*
|
|
193
|
+
* A program row only gives us a directory. The yds project root always sits *between*
|
|
194
|
+
* that directory and the workspace: a module dir (or the project dir itself) never
|
|
195
|
+
* carries the IDE settings, one of its ancestors does. So the walk starts at `dir` and
|
|
196
|
+
* climbs to the workspace; the first level whose `.vscode/settings.json` contains
|
|
197
|
+
* `yds.`-prefixed settings is the project root, and everything beneath it belongs to
|
|
198
|
+
* that project.
|
|
199
|
+
* @param {string} workspace - session workspace; the walk stops there.
|
|
200
|
+
* @param {string} dir - absolute directory of a program (e.g. `…/c-ass-kk-hans-fe`).
|
|
201
|
+
* @returns {string} the project root, `''` when the program is not in a yds project.
|
|
202
|
+
*/
|
|
203
|
+
export function ydsProjectRootOf(workspace, dir) {
|
|
204
|
+
if (typeof dir !== 'string' || dir === '') return ''
|
|
205
|
+
const stop = typeof workspace === 'string' && workspace !== '' ? resolve(workspace) : ''
|
|
206
|
+
let current = resolve(dir)
|
|
207
|
+
for (let depth = 0; depth < 8; depth += 1) {
|
|
208
|
+
const settings = join(current, '.vscode', 'settings.json')
|
|
209
|
+
if (existsSync(settings)) {
|
|
210
|
+
try {
|
|
211
|
+
// JSONC: the key prefix is what matters, a comment cannot fake it.
|
|
212
|
+
if (/"yds\.[A-Za-z]/.test(readFileSync(settings, 'utf8'))) return current
|
|
213
|
+
} catch {
|
|
214
|
+
// unreadable settings: keep walking up
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const parent = dirname(current)
|
|
218
|
+
if (parent === current) return ''
|
|
219
|
+
if (stop !== '' && current === stop) return ''
|
|
220
|
+
current = parent
|
|
221
|
+
}
|
|
222
|
+
return ''
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* The name a yds program should carry, taken from the project's own files.
|
|
227
|
+
*
|
|
228
|
+
* `module.xml`'s `<module name>` for a frontend (falling back to the folder name),
|
|
229
|
+
* and for a java module the pom's `parent.artifactId` — the aggregator's identity —
|
|
230
|
+
* or, for the aggregator itself, its own artifactId.
|
|
231
|
+
* @param {string} dir - absolute program directory.
|
|
232
|
+
* @returns {string} the intended name, `''` when the files say nothing.
|
|
233
|
+
*/
|
|
234
|
+
export function ydsProgramName(dir) {
|
|
235
|
+
if (typeof dir !== 'string' || dir === '') return ''
|
|
236
|
+
const moduleXml = join(dir, 'module.xml')
|
|
237
|
+
if (existsSync(moduleXml)) {
|
|
238
|
+
const named = readXmlAttribute(readFileSync(moduleXml, 'utf8'), 'module', 'name')
|
|
239
|
+
return named !== '' ? named : basename(dir)
|
|
240
|
+
}
|
|
241
|
+
const pom = join(dir, 'pom.xml')
|
|
242
|
+
if (!existsSync(pom)) return ''
|
|
243
|
+
const text = readFileSync(pom, 'utf8')
|
|
244
|
+
if (/<modules>/.test(text) === true) return readPomArtifactId(text)
|
|
245
|
+
return readPomParentArtifactId(pom)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** The pom's own artifactId: the first one outside the `<parent>` block. */
|
|
249
|
+
export function readPomArtifactId(text) {
|
|
250
|
+
if (typeof text !== 'string') return ''
|
|
251
|
+
const own = text.replace(/<parent>[\s\S]*?<\/parent>/, '')
|
|
252
|
+
const match = /<artifactId>([^<]*)<\/artifactId>/.exec(own)
|
|
253
|
+
return match === null ? '' : match[1].trim()
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Whether a config row belongs to a yds project (decided by `dir`, never by name). */
|
|
257
|
+
export function belongsToYdsProject(cfg, row) {
|
|
258
|
+
if (row === null || typeof row !== 'object') return false
|
|
259
|
+
const dir = typeof row.dir === 'string' && row.dir !== '' ? resolve(cfg.workspace, row.dir) : ''
|
|
260
|
+
return dir !== '' && ydsProjectRootOf(cfg.workspace, dir) !== ''
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Rename every row that belongs to a yds project, using that project's own files.
|
|
265
|
+
*
|
|
266
|
+
* A row is judged by `dir` alone: walk up to the nearest `.vscode/settings.json` with
|
|
267
|
+
* `yds.` settings, then name the row after `module.xml` (frontend) or the aggregator
|
|
268
|
+
* pom's `parent.artifactId` (java). Rows outside a yds project are never touched, and
|
|
269
|
+
* a rename that would duplicate another row's name is skipped instead of applied.
|
|
270
|
+
* @param {object} cfg - resolved plugin config (workspace is the walk bound).
|
|
271
|
+
* @param {object[]} programs - config rows.
|
|
272
|
+
* @returns {{programs: object[], renamed: object[], skipped: object[]}} the outcome.
|
|
273
|
+
*/
|
|
274
|
+
export function repairFrontendNames(cfg, programs) {
|
|
275
|
+
const renamed = []
|
|
276
|
+
const skipped = []
|
|
277
|
+
if (!Array.isArray(programs)) return { programs, renamed, skipped }
|
|
278
|
+
const rows = programs.map((row) => (row === null || typeof row !== 'object' ? row : { ...row }))
|
|
279
|
+
// Names of rows that will not be renamed stay reserved, so we never create a clash.
|
|
280
|
+
const taken = new Set()
|
|
281
|
+
const targets = []
|
|
282
|
+
for (const row of rows) {
|
|
283
|
+
if (row === null || typeof row !== 'object') continue
|
|
284
|
+
const dir = typeof row.dir === 'string' && row.dir !== '' ? resolve(cfg.workspace, row.dir) : ''
|
|
285
|
+
if (dir === '' || ydsProjectRootOf(cfg.workspace, dir) === '') { taken.add(row.name); continue }
|
|
286
|
+
targets.push({ row, dir })
|
|
287
|
+
}
|
|
288
|
+
for (const { row, dir } of targets) {
|
|
289
|
+
const name = ydsProgramName(dir)
|
|
290
|
+
if (name === '' || name === row.name) { taken.add(row.name); continue }
|
|
291
|
+
if (taken.has(name) === true) { skipped.push({ from: row.name, to: name, reason: '名字已被另一条目占用' }); taken.add(row.name); continue }
|
|
292
|
+
renamed.push({ from: row.name, to: name })
|
|
293
|
+
row.name = name
|
|
294
|
+
taken.add(name)
|
|
295
|
+
}
|
|
296
|
+
return { programs: rows, renamed, skipped }
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Reconcile every row that belongs to a yds project with what that project *is*.
|
|
301
|
+
*
|
|
302
|
+
* Three things happen to rows inside a yds project (judged by `dir` → the nearest
|
|
303
|
+
* ancestor `.vscode/settings.json` with `yds.` settings):
|
|
304
|
+
* 1. rename after the project's own files (`module.xml`, pom parent artifactId);
|
|
305
|
+
* 2. disable the discovered backend rows — the yds launcher starts that same app
|
|
306
|
+
* against the same port, so leaving both enabled creates two rival starters;
|
|
307
|
+
* 3. strip a `bin/yds.js` startCommand from any row that is not ours — an older
|
|
308
|
+
* update could copy the launcher's command onto a frontend row.
|
|
309
|
+
* @param {object} cfg - resolved plugin config.
|
|
310
|
+
* @param {object[]} programs - config rows.
|
|
311
|
+
* @returns {{programs: object[], renamed: object[], skipped: object[], disabled: object[], cleaned: object[]}}
|
|
312
|
+
*/
|
|
313
|
+
export function repairYdsRows(cfg, programs) {
|
|
314
|
+
const repaired = repairFrontendNames(cfg, programs)
|
|
315
|
+
const { renamed, skipped } = repaired
|
|
316
|
+
const disabled = []
|
|
317
|
+
const cleaned = []
|
|
318
|
+
const rows = repaired.programs.map((row) => {
|
|
319
|
+
if (row === null || typeof row !== 'object') return row
|
|
320
|
+
const ours = isOurs(row, cfg) === true
|
|
321
|
+
if (ours !== true && typeof row.startCommand === 'string' && row.startCommand.includes('bin/yds.js')) {
|
|
322
|
+
cleaned.push({ name: row.name, startCommand: row.startCommand })
|
|
323
|
+
const { startCommand, ...rest } = row
|
|
324
|
+
row = rest
|
|
325
|
+
}
|
|
326
|
+
if (ours !== true && row.kind === 'spring' && row.disabled !== true && belongsToYdsProject(cfg, row) === true) {
|
|
327
|
+
disabled.push({ name: row.name, dir: row.dir })
|
|
328
|
+
row = { ...row, disabled: true }
|
|
329
|
+
}
|
|
330
|
+
return row
|
|
331
|
+
})
|
|
332
|
+
return { programs: rows, renamed, skipped, disabled, cleaned }
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Apply our entry: add or refresh it, preserving all others.
|
|
337
|
+
* @returns { changed, action: 'created'|'added'|'updated'|'unchanged', path }
|
|
338
|
+
*/
|
|
339
|
+
/** The document ris-ide itself generates, so an initialised file looks native. */
|
|
340
|
+
export function emptyIdeConfigDocument() {
|
|
341
|
+
return { version: 1, defaults: { jvmArgs: [] }, programs: [] }
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Create the config file when it does not exist yet.
|
|
346
|
+
*
|
|
347
|
+
* Writing our row into a file ris-ide never generated would leave it without the
|
|
348
|
+
* `version`/`defaults` keys the panel writes and expects; initialising first keeps
|
|
349
|
+
* one shape whether the panel or this plugin created it.
|
|
350
|
+
* @returns {{created: boolean, path: string}} what happened.
|
|
351
|
+
*/
|
|
352
|
+
export function initializeIdeConfig(workspace) {
|
|
353
|
+
const path = join(workspace, IDE_CONFIG)
|
|
354
|
+
if (existsSync(path)) return { created: false, path }
|
|
355
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
356
|
+
writeIdeConfig(path, emptyIdeConfigDocument())
|
|
357
|
+
return { created: true, path }
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export function applyIdeConfig(cfg) {
|
|
361
|
+
const initialized = initializeIdeConfig(cfg.workspace)
|
|
362
|
+
const current = readIdeConfig(cfg.workspace)
|
|
363
|
+
const entry = ourEntry(cfg)
|
|
364
|
+
if (current.exists === false) {
|
|
365
|
+
mkdirSync(dirname(current.path), { recursive: true })
|
|
366
|
+
writeIdeConfig(current.path, { ...emptyIdeConfigDocument(), programs: [entry] })
|
|
367
|
+
return { changed: true, action: 'created', initialized: initialized.created, path: current.path, entry }
|
|
368
|
+
}
|
|
369
|
+
if (current.parsed === null) {
|
|
370
|
+
return { changed: false, action: 'error', error: `${current.path} 不是合法 JSON,为避免破坏用户配置未写入`, path: current.path }
|
|
371
|
+
}
|
|
372
|
+
const programs = Array.isArray(current.parsed.programs) ? current.parsed.programs : []
|
|
373
|
+
const others = programs.filter((p) => isOurs(p, cfg) !== true)
|
|
374
|
+
const idx = programs.findIndex((p) => isOurs(p, cfg) === true)
|
|
375
|
+
if (idx >= 0 && JSON.stringify(programs[idx]) === JSON.stringify(entry)) {
|
|
376
|
+
return { changed: false, action: 'unchanged', initialized: initialized.created, path: current.path, entry }
|
|
377
|
+
}
|
|
378
|
+
const repaired = repairYdsRows(cfg, [...others, entry])
|
|
379
|
+
const next = { ...current.parsed }
|
|
380
|
+
next.programs = repaired.programs
|
|
381
|
+
writeIdeConfig(current.path, next)
|
|
382
|
+
const action = initialized.created ? 'created' : (idx >= 0 ? 'updated' : 'added')
|
|
383
|
+
return {
|
|
384
|
+
changed: true, action, initialized: initialized.created, path: current.path, entry,
|
|
385
|
+
renamed: repaired.renamed, skipped: repaired.skipped, disabled: repaired.disabled, cleaned: repaired.cleaned,
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** Remove our entry if present. */
|
|
390
|
+
export function removeIdeConfig(cfg) {
|
|
391
|
+
const current = readIdeConfig(cfg.workspace)
|
|
392
|
+
if (current.exists === false) return { changed: false, action: 'missing' }
|
|
393
|
+
if (current.parsed === null) return { changed: false, action: 'error', error: '配置文件不是合法 JSON' }
|
|
394
|
+
const programs = Array.isArray(current.parsed.programs) ? current.parsed.programs : []
|
|
395
|
+
const rest = programs.filter((p) => isOurs(p, cfg) !== true)
|
|
396
|
+
if (rest.length === programs.length) return { changed: false, action: 'not-present' }
|
|
397
|
+
const next = { ...current.parsed, programs: rest }
|
|
398
|
+
writeIdeConfig(current.path, next)
|
|
399
|
+
return { changed: true, action: 'removed' }
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function writeIdeConfig(path, doc) {
|
|
403
|
+
writeFileSync(path, `${HEADER}${JSON.stringify(doc, null, 2)}\n`, 'utf8')
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* JVM flags that make a YonCode/VS Code-launched java use the SOCKS tunnel.
|
|
408
|
+
*
|
|
409
|
+
* This is the only way to proxy a process we do not spawn ourselves: YonCode
|
|
410
|
+
* reads vmArgs from launch.json, so the flags travel with the configuration.
|
|
411
|
+
* JDBC is not affected (drivers ignore SOCKS — those need the 127.0.0.1 map).
|
|
412
|
+
* @param cfg - resolved plugin config (uses `cfg.socks`).
|
|
413
|
+
* @returns the vmArgs to append (`[]` when no tunnel is configured).
|
|
414
|
+
*/
|
|
415
|
+
export function proxyVmArgs(cfg) {
|
|
416
|
+
const socks = cfg?.socks ?? null
|
|
417
|
+
if (socks === null || typeof socks !== 'object') return []
|
|
418
|
+
return [
|
|
419
|
+
`-DsocksProxyHost=${socks.host}`,
|
|
420
|
+
`-DsocksProxyPort=${socks.port}`,
|
|
421
|
+
'-Djava.net.useSystemProxies=false',
|
|
422
|
+
]
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* The JVM flags that select the launch mode — the ONLY place proxy flags may
|
|
427
|
+
* come from, so the two modes can never be mixed:
|
|
428
|
+
* - proxy (default): {@link proxyVmArgs} (the SOCKS tunnel flags);
|
|
429
|
+
* - direct: nothing, except `-Dlocal_conf_path=<ymsHome>/config-direct/` when
|
|
430
|
+
* SOCKS is still configured (dsh needs the tunnel, YonCode must not).
|
|
431
|
+
* @param cfg - resolved plugin config (uses `cfg.launchMode`, `cfg.socks`).
|
|
432
|
+
* @returns the vmArgs to append.
|
|
433
|
+
*/
|
|
434
|
+
export function launchModeVmArgs(cfg) {
|
|
435
|
+
const sep = process.platform === 'win32' ? '\\' : '/'
|
|
436
|
+
if ((cfg?.launchMode ?? 'proxy') === 'direct') {
|
|
437
|
+
const socks = cfg?.socks ?? null
|
|
438
|
+
if (socks === null || typeof socks !== 'object') return []
|
|
439
|
+
return [
|
|
440
|
+
`-Dlocal_conf_path=\${config:yds.ymsHome}${sep}${DIRECT_CONFIG_DIRNAME}${sep}`,
|
|
441
|
+
'-Dyms_use_local_conf=true',
|
|
442
|
+
]
|
|
443
|
+
}
|
|
444
|
+
return proxyVmArgs(cfg)
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/** True for the flags {@link proxyVmArgs} writes — used when taking them back out. */
|
|
448
|
+
function isProxyVmArg(value) {
|
|
449
|
+
return typeof value === 'string'
|
|
450
|
+
&& (/^-DsocksProxy(Host|Port)=/.test(value) || value === '-Djava.net.useSystemProxies=false')
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* The launch.json configuration we manage — byte-equivalent to what YonCode's
|
|
455
|
+
* ensureLaunchConfig writes (verified against the generated file), so YonCode
|
|
456
|
+
* F5 and a regenerated file stay interchangeable. Windows path separators on
|
|
457
|
+
* win32, forward slashes elsewhere.
|
|
458
|
+
* @param cfg - resolved plugin config; the mode-dependent flags come from
|
|
459
|
+
* {@link launchModeVmArgs} (`ide.launch=proxy|direct`).
|
|
460
|
+
*/
|
|
461
|
+
export function ourLaunchEntry(cfg) {
|
|
462
|
+
const win = process.platform === 'win32'
|
|
463
|
+
const sep = win ? '\\' : '/'
|
|
464
|
+
const javaExec = `\${config:yds.ymsHome}${sep}${win ? 'win32-x64-java.exe' : 'linux-x64-java'}`
|
|
465
|
+
const javaHome = `\${command:yds.javaHome}${sep}bin${sep}java${win ? '.exe' : ''}`
|
|
466
|
+
const exports = [
|
|
467
|
+
['--add-exports', 'java.base/sun.security.action=ALL-UNNAMED'],
|
|
468
|
+
['--add-exports', 'java.naming/com.sun.jndi.ldap=ALL-UNNAMED'],
|
|
469
|
+
['--add-exports', 'java.naming/com.sun.jndi.url.ldap=ALL-UNNAMED'],
|
|
470
|
+
['--add-exports', 'jdk.naming.dns/com.sun.jndi.dns=ALL-UNNAMED'],
|
|
471
|
+
['--add-exports', 'jdk.naming.dns/com.sun.jndi.url.dns=ALL-UNNAMED'],
|
|
472
|
+
['--add-exports', 'java.security.jgss/sun.security.krb5.internal=ALL-UNNAMED'],
|
|
473
|
+
['--add-exports', 'jdk.attach/sun.tools.attach=ALL-UNNAMED'],
|
|
474
|
+
]
|
|
475
|
+
const opens = [
|
|
476
|
+
['--add-opens', 'java.base/java.util=ALL-UNNAMED'],
|
|
477
|
+
['--add-opens', 'java.base/java.lang=ALL-UNNAMED'],
|
|
478
|
+
['--add-opens', 'java.base/java.util.concurrent=ALL-UNNAMED'],
|
|
479
|
+
['--add-opens', 'java.base/java.io=ALL-UNNAMED'],
|
|
480
|
+
['--add-opens', 'java.base/java.nio=ALL-UNNAMED'],
|
|
481
|
+
['--add-opens', 'java.base/sun.nio.ch=ALL-UNNAMED'],
|
|
482
|
+
['--add-opens', 'java.naming/javax.naming.spi=ALL-UNNAMED'],
|
|
483
|
+
['--add-opens', 'java.naming/com.sun.naming.internal=ALL-UNNAMED'],
|
|
484
|
+
['--add-opens', 'jdk.naming.rmi/com.sun.jndi.url.rmi=ALL-UNNAMED'],
|
|
485
|
+
['--add-opens', 'java.naming/javax.naming=ALL-UNNAMED'],
|
|
486
|
+
['--add-opens', 'java.rmi/java.rmi=ALL-UNNAMED'],
|
|
487
|
+
['--add-opens', 'java.sql/java.sql=ALL-UNNAMED'],
|
|
488
|
+
['--add-opens', 'java.management/javax.management=ALL-UNNAMED'],
|
|
489
|
+
['--add-opens', 'java.base/java.lang.reflect=ALL-UNNAMED'],
|
|
490
|
+
['--add-opens', 'java.desktop/java.awt.image=ALL-UNNAMED'],
|
|
491
|
+
['--add-opens', 'java.base/java.security=ALL-UNNAMED'],
|
|
492
|
+
['--add-opens', 'java.base/java.net=ALL-UNNAMED'],
|
|
493
|
+
['--add-opens', 'java.base/java.text=ALL-UNNAMED'],
|
|
494
|
+
['--add-opens', 'java.base/sun.net.www.protocol.https=ALL-UNNAMED'],
|
|
495
|
+
]
|
|
496
|
+
return {
|
|
497
|
+
type: 'java',
|
|
498
|
+
name: LAUNCH_ENTRY_NAME,
|
|
499
|
+
javaExec,
|
|
500
|
+
request: 'launch',
|
|
501
|
+
mainClass: 'iuap.yms.YMSLauncher',
|
|
502
|
+
cwd: '${config:yds.ymsHome}',
|
|
503
|
+
classPaths: [`\${config:yds.ymsHome}${sep}yms.jar`],
|
|
504
|
+
args: [],
|
|
505
|
+
vmArgs: [
|
|
506
|
+
`-Djavax.net.ssl.trustStore=\${config:yds.ymsHome}${sep}cacerts`,
|
|
507
|
+
'-Dapp.metainfo.ping_connect_timeout=200',
|
|
508
|
+
`-DYMS_WORK_PATH=\${config:yds.ymsHome}${sep}workarea`,
|
|
509
|
+
'-DYMS-DEV=true',
|
|
510
|
+
'-DYMS-DEV-MODE=debug',
|
|
511
|
+
'-Dyms.log.path=${config:yds.ymsHome}' + sep + 'logs',
|
|
512
|
+
'-Dyms.dev.libProjectsFile=${command:yds.libraryProjectsFile}',
|
|
513
|
+
'-Dapp.metainfo.preferred-networks=${command:yds.serverBindAddress}',
|
|
514
|
+
'-Dserver.port=${config:yds.serverPort}',
|
|
515
|
+
...exports.flat(),
|
|
516
|
+
...opens.flat(),
|
|
517
|
+
// YonCode appends these two exports AFTER the opens block — same flags,
|
|
518
|
+
// order kept identical for byte-parity with ensureLaunchConfig output.
|
|
519
|
+
'--add-exports', 'jdk.management.agent/jdk.internal.agent=ALL-UNNAMED',
|
|
520
|
+
'--add-exports', 'java.base/jdk.internal.vm=ALL-UNNAMED',
|
|
521
|
+
'-Dyms_fetch_config_version=2.0.0',
|
|
522
|
+
'-Dinotify_enable=false',
|
|
523
|
+
'-Dapp.metainfo.start_mode=static',
|
|
524
|
+
'-Dyms.bootstrap.enableNewConfig=true',
|
|
525
|
+
'-Drdbga=${command:yds.getEncryptedRDBGA}',
|
|
526
|
+
// Mode switch: proxy → SOCKS flags (removed again by `yds proxy stop`);
|
|
527
|
+
// direct → no proxy flag, and (when dsh still tunnels) the untouched
|
|
528
|
+
// config-direct via local_conf_path. See launchModeVmArgs.
|
|
529
|
+
...launchModeVmArgs(cfg),
|
|
530
|
+
],
|
|
531
|
+
env: {
|
|
532
|
+
JAVA_EXECUTABLE: javaHome,
|
|
533
|
+
mw_profiles_active: '${command:yds.env}',
|
|
534
|
+
YDS_STATE: '${command:yds.preLaunch}',
|
|
535
|
+
'access.key': '${config:yds.accessKey}',
|
|
536
|
+
'access.secret': '${config:yds.accessSecret}',
|
|
537
|
+
enable_cloud_debug: 'true',
|
|
538
|
+
YMS_ENABLE_MODULE: '${command:yds.getSelectedYmsModules}',
|
|
539
|
+
yms_enable_module: '${command:yds.getSelectedYmsModules}',
|
|
540
|
+
},
|
|
541
|
+
projectName: 'yds',
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Where .vscode/launch.json lives: the PROJECT folder the IDE actually opens
|
|
547
|
+
* (`cfg.projectDir`, i.e. the dir holding .vscode/settings.json — e.g.
|
|
548
|
+
* yonasset/), NOT the DSH session workspace root. Falling back to the
|
|
549
|
+
* session workspace when no project dir was detected keeps this usable in a
|
|
550
|
+
* bare checkout.
|
|
551
|
+
*/
|
|
552
|
+
export function launchRoot(cfg) {
|
|
553
|
+
return cfg.projectDir ?? cfg.workspace
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/** Read .vscode/launch.json: { exists, parsed, path }. */
|
|
557
|
+
export function readLaunchConfig(workspace) {
|
|
558
|
+
const path = join(workspace, LAUNCH_CONFIG)
|
|
559
|
+
if (!existsSync(path)) return { exists: false, path, parsed: null }
|
|
560
|
+
const raw = readFileSync(path, 'utf8')
|
|
561
|
+
let parsed = null
|
|
562
|
+
try {
|
|
563
|
+
parsed = JSON.parse(stripJsonc(raw))
|
|
564
|
+
} catch { parsed = null }
|
|
565
|
+
return { exists: true, path, parsed, raw }
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Upsert our launch.json configuration, preserving all others (Current File /
|
|
570
|
+
* MDFApplication / workbench / anything YonCode wrote).
|
|
571
|
+
*/
|
|
572
|
+
export function applyLaunchConfig(cfg) {
|
|
573
|
+
const current = readLaunchConfig(launchRoot(cfg))
|
|
574
|
+
const entry = ourLaunchEntry(cfg)
|
|
575
|
+
if (current.exists === false) {
|
|
576
|
+
mkdirSync(dirname(current.path), { recursive: true })
|
|
577
|
+
writeLaunchConfig(current.path, { version: '0.2.0', configurations: [entry] })
|
|
578
|
+
return { changed: true, action: 'created', path: current.path, entry }
|
|
579
|
+
}
|
|
580
|
+
if (current.parsed === null) {
|
|
581
|
+
return { changed: false, action: 'error', error: `${current.path} 不是合法 JSON,为避免破坏用户配置未写入`, path: current.path }
|
|
582
|
+
}
|
|
583
|
+
const configs = Array.isArray(current.parsed.configurations) ? current.parsed.configurations : []
|
|
584
|
+
const others = configs.filter((c) => c?.name !== entry.name)
|
|
585
|
+
const idx = configs.findIndex((c) => c?.name === entry.name)
|
|
586
|
+
if (idx >= 0 && JSON.stringify(configs[idx]) === JSON.stringify(entry)) {
|
|
587
|
+
return { changed: false, action: 'unchanged', path: current.path, entry }
|
|
588
|
+
}
|
|
589
|
+
const next = { ...current.parsed }
|
|
590
|
+
next.configurations = [...others, entry]
|
|
591
|
+
writeLaunchConfig(current.path, next)
|
|
592
|
+
return { changed: true, action: idx >= 0 ? 'updated' : 'added', path: current.path, entry }
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Take the tunnel flags back out of our launch entry (everything else stays).
|
|
597
|
+
*
|
|
598
|
+
* Called when the proxy engine stops: leaving `-DsocksProxyHost` behind would
|
|
599
|
+
* point YonCode's java at a dead port on the next F5.
|
|
600
|
+
* @param cfg - resolved plugin config.
|
|
601
|
+
* @returns `{ changed, action }`.
|
|
602
|
+
*/
|
|
603
|
+
export function stripLaunchProxy(cfg) {
|
|
604
|
+
const current = readLaunchConfig(launchRoot(cfg))
|
|
605
|
+
if (current.exists === false) return { changed: false, action: 'missing' }
|
|
606
|
+
if (current.parsed === null) return { changed: false, action: 'error', error: 'launch.json 不是合法 JSON' }
|
|
607
|
+
const configs = Array.isArray(current.parsed.configurations) ? current.parsed.configurations : []
|
|
608
|
+
const idx = configs.findIndex((c) => c?.name === LAUNCH_ENTRY_NAME)
|
|
609
|
+
if (idx < 0) return { changed: false, action: 'not-present' }
|
|
610
|
+
const vmArgs = Array.isArray(configs[idx].vmArgs) ? configs[idx].vmArgs : []
|
|
611
|
+
const kept = vmArgs.filter((value) => !isProxyVmArg(value))
|
|
612
|
+
if (kept.length === vmArgs.length) return { changed: false, action: 'no-proxy-args' }
|
|
613
|
+
const next = { ...current.parsed }
|
|
614
|
+
next.configurations = configs.map((entry, index) => (index === idx ? { ...entry, vmArgs: kept } : entry))
|
|
615
|
+
writeLaunchConfig(current.path, next)
|
|
616
|
+
return { changed: true, action: 'stripped' }
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/** Remove our launch.json configuration if present. */
|
|
620
|
+
export function removeLaunchConfig(cfg) {
|
|
621
|
+
const current = readLaunchConfig(launchRoot(cfg))
|
|
622
|
+
if (current.exists === false) return { changed: false, action: 'missing' }
|
|
623
|
+
if (current.parsed === null) return { changed: false, action: 'error', error: 'launch.json 不是合法 JSON' }
|
|
624
|
+
const configs = Array.isArray(current.parsed.configurations) ? current.parsed.configurations : []
|
|
625
|
+
const rest = configs.filter((c) => c?.name !== LAUNCH_ENTRY_NAME)
|
|
626
|
+
if (rest.length === configs.length) return { changed: false, action: 'not-present' }
|
|
627
|
+
const next = { ...current.parsed, configurations: rest }
|
|
628
|
+
writeLaunchConfig(current.path, next)
|
|
629
|
+
return { changed: true, action: 'removed' }
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function writeLaunchConfig(path, doc) {
|
|
633
|
+
writeFileSync(path, `${LAUNCH_HEADER}${JSON.stringify(doc, null, 2)}\n`, 'utf8')
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* String-aware JSONC strip in a single pass: removes // and /* *\/ comments
|
|
638
|
+
* and trailing commas ONLY outside string literals. The old regex version
|
|
639
|
+
* ate "http://" inside strings and ",]" sequences in startCommands —
|
|
640
|
+
* silently corrupting user config on round-trip.
|
|
641
|
+
*/
|
|
642
|
+
function stripJsonc(text) {
|
|
643
|
+
let outText = ''
|
|
644
|
+
let i = 0
|
|
645
|
+
const n = text.length
|
|
646
|
+
const skipWsAndComments = (pos) => {
|
|
647
|
+
let j = pos
|
|
648
|
+
for (;;) {
|
|
649
|
+
const ch = text[j]
|
|
650
|
+
if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') { j += 1; continue }
|
|
651
|
+
if (ch === '/' && text[j + 1] === '/') { while (j < n && text[j] !== '\n' && text[j] !== '\r') j += 1; continue }
|
|
652
|
+
if (ch === '/' && text[j + 1] === '*') { const close = text.indexOf('*/', j + 2); j = close === -1 ? n : close + 2; continue }
|
|
653
|
+
return j
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
while (i < n) {
|
|
657
|
+
const c = text[i]
|
|
658
|
+
if (c === '"') {
|
|
659
|
+
let j = i + 1
|
|
660
|
+
while (j < n) {
|
|
661
|
+
if (text[j] === '\\') { j += 2; continue }
|
|
662
|
+
if (text[j] === '"') break
|
|
663
|
+
j += 1
|
|
664
|
+
}
|
|
665
|
+
const stop = Math.min(j + 1, n)
|
|
666
|
+
outText += text.slice(i, stop)
|
|
667
|
+
i = stop
|
|
668
|
+
continue
|
|
669
|
+
}
|
|
670
|
+
if (c === '/' && text[i + 1] === '/') { i = skipWsAndComments(i); continue }
|
|
671
|
+
if (c === '/' && text[i + 1] === '*') { i = skipWsAndComments(i); continue }
|
|
672
|
+
if (c === ',') {
|
|
673
|
+
const j = skipWsAndComments(i + 1)
|
|
674
|
+
if (text[j] === '}' || text[j] === ']') { i = j; continue } // trailing comma — drop
|
|
675
|
+
outText += c
|
|
676
|
+
i += 1
|
|
677
|
+
continue
|
|
678
|
+
}
|
|
679
|
+
outText += c
|
|
680
|
+
i += 1
|
|
681
|
+
}
|
|
682
|
+
return outText
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Reconcile the panel config of one workspace with what its yds project is.
|
|
687
|
+
*
|
|
688
|
+
* Used by the config watcher (see `lib/ide-watch.js`): ris-ide rewrites the file on
|
|
689
|
+
* init/refresh, we put our launcher row and the yds names back. Nothing happens when
|
|
690
|
+
* the workspace has no yds project or no config file — the add-on never creates one.
|
|
691
|
+
* @param workspace - absolute workspace root.
|
|
692
|
+
* @returns {Promise<{changed: boolean, reason: string}>} outcome for logging.
|
|
693
|
+
*/
|
|
694
|
+
export async function reconcileWorkspaceConfig(workspace) {
|
|
695
|
+
const { config: cfg } = resolveConfig(workspace)
|
|
696
|
+
if (typeof cfg.projectDir !== 'string' || cfg.projectDir === '') return { changed: false, reason: 'no-project' }
|
|
697
|
+
if (existsSync(join(cfg.workspace, '.dsh-ris-ide', 'config.json')) !== true) {
|
|
698
|
+
return { changed: false, reason: 'no-config' }
|
|
699
|
+
}
|
|
700
|
+
const result = applyIdeConfig(cfg)
|
|
701
|
+
return { changed: result.changed === true, reason: result.action }
|
|
702
|
+
}
|