dsh-ris-yds 0.0.0-stage → 1.1.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 +160 -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 +1130 -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 +906 -0
- package/lib/ide-watch.js +188 -0
- package/lib/ide.js +728 -0
- package/lib/index.js +267 -0
- package/lib/proc.js +270 -0
- package/lib/risd-peer.js +353 -0
- package/lib/rules.js +193 -0
- package/lib/socks.js +279 -0
- package/lib/token.js +36 -0
- package/lib/yms.js +336 -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-stop.test.mjs +40 -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/hints.test.mjs +13 -0
- package/test/ide-config.test.mjs +155 -0
- package/test/ide-proxy.test.mjs +256 -0
- package/test/ide-watch.test.mjs +79 -0
- package/test/launch-proxy.test.mjs +83 -0
- package/test/lease-release.test.mjs +76 -0
- package/test/mode-separation.test.mjs +147 -0
- package/test/pidfile.test.mjs +105 -0
- package/test/proc.test.mjs +172 -0
- package/test/proxy-standalone.test.mjs +121 -0
- package/test/risd-leases.test.mjs +111 -0
- package/test/risd-tier.test.mjs +361 -0
- package/test/rules-e2e.test.mjs +122 -0
- package/test/rules.test.mjs +154 -0
- package/test/yoncode.test.mjs +159 -0
package/lib/ide-watch.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Watch ris-ide's program config and keep our launcher row in it.
|
|
3
|
+
*
|
|
4
|
+
* ris-ide rewrites `.dsh-ris-ide/config.json` when the panel is initialised or the
|
|
5
|
+
* user clicks "generate/update config". That rewrite is derived from discovery, so
|
|
6
|
+
* the yds launcher row — which ris-ide cannot discover — disappears, and the names
|
|
7
|
+
* of the rows it does generate follow whatever discovery thinks they are. Our row
|
|
8
|
+
* has to be re-injected afterwards.
|
|
9
|
+
*
|
|
10
|
+
* The re-injection lives here, in the yds plugin, as an add-on: ris-ide is never
|
|
11
|
+
* modified and never learns that we exist. We only watch the file it owns and
|
|
12
|
+
* rewrite it *after* it changed, keeping every foreign row byte-for-byte.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { existsSync, readFileSync, statSync, watch } from 'node:fs'
|
|
16
|
+
import { mkdirSync, rmSync, writeFileSync } from 'node:fs'
|
|
17
|
+
import { homedir } from 'node:os'
|
|
18
|
+
import { join, resolve } from 'node:path'
|
|
19
|
+
|
|
20
|
+
/** Where the list of watched workspaces lives (machine level, survives restarts). */
|
|
21
|
+
export const WATCH_STATE_FILENAME = '.dsh-ris-yds/ide-watch.json'
|
|
22
|
+
|
|
23
|
+
/** Debounce after a config write before we reconcile (ris-ide writes once). */
|
|
24
|
+
const DEBOUNCE_MS = 400
|
|
25
|
+
|
|
26
|
+
/** Polling fallback — covers editors/FS where `watch` misses events (network mounts). */
|
|
27
|
+
const POLL_MS = 2000
|
|
28
|
+
|
|
29
|
+
/** @returns {string} machine-level state file. */
|
|
30
|
+
export function watchStatePath(home = homedir()) {
|
|
31
|
+
return join(home, WATCH_STATE_FILENAME)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Register a workspace so the running plugin keeps re-injecting our launcher there.
|
|
36
|
+
* @param workspace - absolute workspace root.
|
|
37
|
+
* @returns {{ok: boolean, workspaces: string[]}} the updated list.
|
|
38
|
+
*/
|
|
39
|
+
export function registerWorkspace(workspace, home = homedir()) {
|
|
40
|
+
const path = watchStatePath(home)
|
|
41
|
+
const current = listWatched(home)
|
|
42
|
+
const target = resolve(workspace)
|
|
43
|
+
const next = current.includes(target) ? current : [...current, target]
|
|
44
|
+
try {
|
|
45
|
+
mkdirSync(join(path, '..'), { recursive: true })
|
|
46
|
+
writeFileSync(path, `${JSON.stringify({ workspaces: next }, null, 2)}\n`, { mode: 0o600 })
|
|
47
|
+
return { ok: true, workspaces: next }
|
|
48
|
+
} catch (error) {
|
|
49
|
+
return { ok: false, workspaces: current, error: String(error) }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Stop watching a workspace (used by `yds ide remove`).
|
|
55
|
+
* @param workspace - absolute workspace root.
|
|
56
|
+
* @returns {{ok: boolean, workspaces: string[]}} the updated list.
|
|
57
|
+
*/
|
|
58
|
+
export function unregisterWorkspace(workspace, home = homedir()) {
|
|
59
|
+
const path = watchStatePath(home)
|
|
60
|
+
const target = resolve(workspace)
|
|
61
|
+
const next = listWatched(home).filter((item) => item !== target)
|
|
62
|
+
try {
|
|
63
|
+
if (next.length === 0) rmSync(path, { force: true })
|
|
64
|
+
else writeFileSync(path, `${JSON.stringify({ workspaces: next }, null, 2)}\n`, { mode: 0o600 })
|
|
65
|
+
return { ok: true, workspaces: next }
|
|
66
|
+
} catch (error) {
|
|
67
|
+
return { ok: false, workspaces: next, error: String(error) }
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @returns {string[]} workspaces the plugin should keep reconciled.
|
|
73
|
+
*/
|
|
74
|
+
export function listWatched(home = homedir()) {
|
|
75
|
+
try {
|
|
76
|
+
const parsed = JSON.parse(readFileSync(watchStatePath(home), 'utf8'))
|
|
77
|
+
const list = Array.isArray(parsed?.workspaces) ? parsed.workspaces : []
|
|
78
|
+
return list.filter((item) => typeof item === 'string' && item !== '').map((item) => resolve(item))
|
|
79
|
+
} catch {
|
|
80
|
+
return []
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Reconcile one workspace's config if (and only if) we already own a row in it.
|
|
86
|
+
*
|
|
87
|
+
* Two refusals keep the add-on from fighting the user or ris-ide:
|
|
88
|
+
* - a workspace where our row was deliberately removed (`yds ide remove`) is left
|
|
89
|
+
* alone — {@link syncWorkspace} only acts while our row is present or absent
|
|
90
|
+
* *because ris-ide rewrote the file*;
|
|
91
|
+
* - a config that does not exist is not created (ris-ide owns the file's birth).
|
|
92
|
+
* @param workspace - absolute workspace root.
|
|
93
|
+
* @param deps - `{readConfig, writeConfig, hasOurRow, repair}` injected to avoid cycles.
|
|
94
|
+
* @returns {Promise<{changed: boolean, reason: string}>} the outcome.
|
|
95
|
+
*/
|
|
96
|
+
export async function reconcileOnce(workspace, deps, options = {}) {
|
|
97
|
+
const configPath = join(workspace, '.dsh-ris-ide', 'config.json')
|
|
98
|
+
if (existsSync(configPath) !== true) return { changed: false, reason: 'no-config' }
|
|
99
|
+
if (deps === undefined || typeof deps.repair !== 'function') return { changed: false, reason: 'no-repair' }
|
|
100
|
+
let before
|
|
101
|
+
try {
|
|
102
|
+
before = readFileSync(configPath, 'utf8')
|
|
103
|
+
} catch {
|
|
104
|
+
return { changed: false, reason: 'unreadable' }
|
|
105
|
+
}
|
|
106
|
+
const restored = await deps.repair(workspace)
|
|
107
|
+
if (restored?.changed !== true) return { changed: false, reason: restored?.reason ?? 'unchanged' }
|
|
108
|
+
let after
|
|
109
|
+
try {
|
|
110
|
+
after = readFileSync(configPath, 'utf8')
|
|
111
|
+
} catch {
|
|
112
|
+
after = ''
|
|
113
|
+
}
|
|
114
|
+
if (after === before) return { changed: false, reason: 'no-op' }
|
|
115
|
+
if (options.log !== undefined) options.log(`[yds] 已把启动器条目重新注入 ${configPath}`)
|
|
116
|
+
return { changed: true, reason: restored.reason ?? 'injected' }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Keep one workspace reconciled: `fs.watch` on `.dsh-ris-ide` plus a polling
|
|
121
|
+
* fallback, both funnelled through a debounce. Returns a disposer.
|
|
122
|
+
* @param workspace - absolute workspace root.
|
|
123
|
+
* @param deps - see {@link reconcileOnce}.
|
|
124
|
+
* @param options - `{log}` optional logger.
|
|
125
|
+
* @returns {() => void} disposer.
|
|
126
|
+
*/
|
|
127
|
+
export function watchWorkspace(workspace, deps, options = {}) {
|
|
128
|
+
const root = resolve(workspace)
|
|
129
|
+
const dir = join(root, '.dsh-ris-ide')
|
|
130
|
+
const configPath = join(dir, 'config.json')
|
|
131
|
+
let timer
|
|
132
|
+
let lastMtime = 0
|
|
133
|
+
let stopped = false
|
|
134
|
+
const run = () => {
|
|
135
|
+
if (stopped) return
|
|
136
|
+
void reconcileOnce(root, deps, options).catch(() => {})
|
|
137
|
+
}
|
|
138
|
+
const schedule = () => {
|
|
139
|
+
if (stopped) return
|
|
140
|
+
clearTimeout(timer)
|
|
141
|
+
timer = setTimeout(run, DEBOUNCE_MS)
|
|
142
|
+
timer.unref?.()
|
|
143
|
+
}
|
|
144
|
+
let watcher = null
|
|
145
|
+
try {
|
|
146
|
+
watcher = watch(dir, { persistent: false }, (event, filename) => {
|
|
147
|
+
if (typeof filename === 'string' && filename !== 'config.json') return
|
|
148
|
+
if (event !== undefined) schedule()
|
|
149
|
+
})
|
|
150
|
+
watcher.on?.('error', () => {})
|
|
151
|
+
} catch {
|
|
152
|
+
watcher = null // the directory may not exist yet; the poller below covers us
|
|
153
|
+
}
|
|
154
|
+
const poll = setInterval(() => {
|
|
155
|
+
if (stopped) return
|
|
156
|
+
try {
|
|
157
|
+
const { mtimeMs } = statSync(configPath)
|
|
158
|
+
if (mtimeMs !== lastMtime) {
|
|
159
|
+
lastMtime = mtimeMs
|
|
160
|
+
schedule()
|
|
161
|
+
}
|
|
162
|
+
} catch {
|
|
163
|
+
// no config yet — nothing to reconcile
|
|
164
|
+
}
|
|
165
|
+
}, POLL_MS)
|
|
166
|
+
poll.unref?.()
|
|
167
|
+
return () => {
|
|
168
|
+
stopped = true
|
|
169
|
+
clearTimeout(timer)
|
|
170
|
+
clearInterval(poll)
|
|
171
|
+
try {
|
|
172
|
+
watcher?.close?.()
|
|
173
|
+
} catch {
|
|
174
|
+
// ignore
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Start watchers for every registered workspace.
|
|
181
|
+
* @param deps - see {@link reconcileOnce}.
|
|
182
|
+
* @param options - `{log, home}` optional.
|
|
183
|
+
* @returns {() => void} disposer stopping all watchers.
|
|
184
|
+
*/
|
|
185
|
+
export function watchAll(deps, options = {}) {
|
|
186
|
+
const disposers = listWatched(options.home).map((workspace) => watchWorkspace(workspace, deps, options))
|
|
187
|
+
return () => disposers.forEach((dispose) => { try { dispose() } catch { /* ignore */ } })
|
|
188
|
+
}
|