@serviceme/devtools-core 2.0.5 → 2.0.7

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.
@@ -1,614 +0,0 @@
1
- const require_rolldown_runtime = require("./rolldown-runtime-VH7oDXx4.js");
2
- const require_userHome = require("./userHome-CLlsXCvW.js");
3
- let node_fs_promises = require("node:fs/promises");
4
- node_fs_promises = require_rolldown_runtime.__toESM(node_fs_promises);
5
- let node_path = require("node:path");
6
- node_path = require_rolldown_runtime.__toESM(node_path);
7
- let node_timers_promises = require("node:timers/promises");
8
- //#region src/toolbox/sort.ts
9
- /**
10
- * Parsed timestamp from `lastUsedAt`. Returns 0 when the field is
11
- * missing or unparseable (so the entry falls below entries with a
12
- * real timestamp).
13
- */
14
- function lastUsedTimestamp(tool) {
15
- if (!tool.lastUsedAt) return 0;
16
- const ms = Date.parse(tool.lastUsedAt);
17
- return Number.isFinite(ms) ? ms : 0;
18
- }
19
- /**
20
- * Sort by "recently used" — entries with newer `lastUsedAt` float to
21
- * the top, entries with no `lastUsedAt` sort to the bottom (preserving
22
- * their relative `order` index when both are absent).
23
- *
24
- * Ties (same `lastUsedAt` or both missing) are broken by `order`, then
25
- * by `id` for determinism.
26
- */
27
- function sortByRecentFirst(tools) {
28
- return [...tools].sort((a, b) => {
29
- const tsA = lastUsedTimestamp(a);
30
- const tsB = lastUsedTimestamp(b);
31
- if (tsA !== tsB) return tsB - tsA;
32
- const orderA = a.order ?? Number.MAX_SAFE_INTEGER;
33
- const orderB = b.order ?? Number.MAX_SAFE_INTEGER;
34
- if (orderA !== orderB) return orderA - orderB;
35
- return a.id.localeCompare(b.id);
36
- });
37
- }
38
- /**
39
- * Sort by user-defined `order` field. Entries without `order` are
40
- * appended in their input order (stable sort via `id` tiebreaker).
41
- */
42
- function sortByUserOrder(tools) {
43
- return [...tools].sort((a, b) => {
44
- const orderA = a.order ?? Number.MAX_SAFE_INTEGER;
45
- const orderB = b.order ?? Number.MAX_SAFE_INTEGER;
46
- if (orderA !== orderB) return orderA - orderB;
47
- return a.id.localeCompare(b.id);
48
- });
49
- }
50
- /**
51
- * Merge built-in defaults with user-stored tools. Defaults keep
52
- * `isDefault: true` and their `order`; user tools are appended with
53
- * their stored metadata. Output is sorted by user order.
54
- */
55
- function mergeWithDefaults(defaults, userTools) {
56
- const defaultIds = new Set(defaults.map((d) => d.id));
57
- const uniqueUserTools = userTools.filter((t) => !defaultIds.has(t.id));
58
- return sortByUserOrder([...defaults, ...uniqueUserTools]);
59
- }
60
- /**
61
- * Re-number `order` for a list of tool ids. Used by
62
- * `toolbox.update` when the user drags-and-drops entries in the UI.
63
- */
64
- function reindexOrder(tools, newOrderIds) {
65
- const orderMap = /* @__PURE__ */ new Map();
66
- for (const [index, id] of newOrderIds.entries()) orderMap.set(id, index);
67
- for (const tool of tools) {
68
- const next = orderMap.get(tool.id);
69
- if (next !== void 0) tool.order = next;
70
- }
71
- return tools;
72
- }
73
- /**
74
- * Stamp `lastUsedAt` on the targeted tool (clones the array). Returns
75
- * a new array; the input is left untouched.
76
- */
77
- function touchLastUsedAt(tools, id, when = /* @__PURE__ */ new Date()) {
78
- return tools.map((tool) => tool.id === id ? {
79
- ...tool,
80
- lastUsedAt: when.toISOString()
81
- } : tool);
82
- }
83
- //#endregion
84
- //#region src/toolbox/types.ts
85
- /** Schema version of the on-disk toolbox JSON files. Bumped on breaking changes. */
86
- const TOOLBOX_JSON_SCHEMA_VERSION = 1;
87
- /** Built-in tools rendered when the JSON file is missing or empty. */
88
- const BUILTIN_DEFAULT_TOOLS = [
89
- {
90
- id: "builtin-docs",
91
- name: "SERVICEME Docs",
92
- description: "Official documentation portal",
93
- icon: "book",
94
- url: "https://docs.medalsoft.com/serviceme"
95
- },
96
- {
97
- id: "builtin-issues",
98
- name: "Issue Tracker",
99
- description: "Report bugs and feature requests",
100
- icon: "bug",
101
- url: "https://github.com/medalsoftchina/ms-devtools-vscode/issues"
102
- },
103
- {
104
- id: "builtin-changelog",
105
- name: "Changelog",
106
- description: "Release notes for every published version",
107
- icon: "history",
108
- url: "https://github.com/medalsoftchina/ms-devtools-vscode/releases"
109
- }
110
- ];
111
- //#endregion
112
- //#region src/toolbox/ToolboxStore.ts
113
- /**
114
- * ToolboxStore — JSON persistence for the toolbox entries.
115
- *
116
- * Two scopes:
117
- * - `user` → `~/.serviceme/toolbox.json`
118
- * - `workspace` → `<cwd>/.github/.serviceme-toolbox.json`
119
- *
120
- * Both files share the `PersistedToolbox` shape and the same atomic
121
- * write pattern (tmp + rename + fsync) as `IdentityStore`. Concurrent
122
- * writers are serialized via a mkdir-based file lock (Phase 6+ may
123
- * upgrade to `proper-lockfile`).
124
- *
125
- * Refs:
126
- * - 4.功能规划.md §2.3 — `ToolboxStore.ts JSON 持久化(user + workspace scope)`
127
- * - `3.功能拆分.md` §3 — toolbox wire shape
128
- */
129
- const FILE_MODE = 384;
130
- const LOCK_DIR_MODE = 448;
131
- const DEFAULT_LOCK_TIMEOUT_MS = 5e3;
132
- const DEFAULT_LOCK_RETRY_MS = 25;
133
- const LOCK_STALE_GRACE_MS = 200;
134
- const TMP_SUFFIX = ".tmp";
135
- const WORKSPACE_TOOLBOX_RELATIVE_PATH = node_path.join(".github", ".serviceme-toolbox.json");
136
- const LEGACY_WORKSPACE_TOOLBOX_FILENAME = ".ms-devtools-toolbox.json";
137
- /**
138
- * One-time migration: the workspace-scope toolbox file used to be named
139
- * `.ms-devtools-toolbox.json`. If the new `.serviceme-toolbox.json` doesn't
140
- * exist yet but the legacy file does (in the same directory), rename it
141
- * forward so existing toolbox entries aren't silently lost.
142
- */
143
- async function migrateLegacyWorkspaceToolboxFile(filePath) {
144
- if (!filePath) return;
145
- const legacyPath = node_path.join(node_path.dirname(filePath), LEGACY_WORKSPACE_TOOLBOX_FILENAME);
146
- if (legacyPath === filePath) return;
147
- try {
148
- await node_fs_promises.access(filePath);
149
- return;
150
- } catch {}
151
- try {
152
- await node_fs_promises.rename(legacyPath, filePath);
153
- } catch {}
154
- }
155
- var FsToolboxFileBackend = class {
156
- constructor() {
157
- this.maxBackupCount = 5;
158
- }
159
- async exists(filePath) {
160
- try {
161
- await node_fs_promises.access(filePath);
162
- return true;
163
- } catch {
164
- return false;
165
- }
166
- }
167
- async read(filePath) {
168
- let buf;
169
- try {
170
- buf = await node_fs_promises.readFile(filePath, "utf8");
171
- } catch (err) {
172
- if (isNodeError(err) && err.code === "ENOENT") return null;
173
- throw err;
174
- }
175
- try {
176
- return coercePersistedToolbox(JSON.parse(buf));
177
- } catch {
178
- await this.backupCorruptedFile(filePath);
179
- return null;
180
- }
181
- }
182
- async backupCorruptedFile(filePath) {
183
- try {
184
- const backupPath = `${filePath}.corrupted.${Date.now()}.bak`;
185
- await node_fs_promises.copyFile(filePath, backupPath);
186
- await this.purgeExcessBackups(filePath);
187
- } catch {}
188
- }
189
- async purgeExcessBackups(filePath) {
190
- const dir = node_path.dirname(filePath);
191
- const base = node_path.basename(filePath);
192
- let entries;
193
- try {
194
- entries = await node_fs_promises.readdir(dir);
195
- } catch {
196
- return;
197
- }
198
- const backups = entries.filter((n) => n.startsWith(base) && n.endsWith(".bak")).map((n) => ({
199
- name: n,
200
- filePath: node_path.join(dir, n)
201
- })).sort((a, b) => {
202
- return a.name.localeCompare(b.name);
203
- });
204
- const excess = backups.length - this.maxBackupCount;
205
- if (excess <= 0) return;
206
- await Promise.all(backups.slice(0, excess).map((b) => node_fs_promises.rm(b.filePath).catch(() => void 0)));
207
- }
208
- async write(filePath, payload) {
209
- await node_fs_promises.mkdir(node_path.dirname(filePath), { recursive: true });
210
- const tmpPath = `${filePath}${TMP_SUFFIX}`;
211
- const bytes = Buffer.from(JSON.stringify(payload, null, " "), "utf8");
212
- await node_fs_promises.rm(tmpPath, { force: true });
213
- const handle = await node_fs_promises.open(tmpPath, "w", FILE_MODE);
214
- try {
215
- await handle.writeFile(bytes);
216
- await handle.sync();
217
- } finally {
218
- await handle.close();
219
- }
220
- await node_fs_promises.rename(tmpPath, filePath);
221
- await node_fs_promises.chmod(filePath, FILE_MODE).catch(() => void 0);
222
- }
223
- };
224
- function coercePersistedToolbox(parsed) {
225
- if (typeof parsed !== "object" || parsed === null) throw new Error("toolbox.json: top-level must be an object");
226
- const obj = parsed;
227
- const version = obj.version;
228
- if (version !== 1) throw new Error(`toolbox.json: unsupported schema version ${String(version)}`);
229
- if (!Array.isArray(obj.tools)) throw new Error("toolbox.json: 'tools' must be an array");
230
- return {
231
- version,
232
- tools: obj.tools
233
- };
234
- }
235
- function isNodeError(value) {
236
- return value instanceof Error && typeof value.code === "string";
237
- }
238
- function isProcessAlive(pid) {
239
- try {
240
- process.kill(pid, 0);
241
- return true;
242
- } catch {
243
- return false;
244
- }
245
- }
246
- var ToolboxFileLock = class {
247
- constructor(filePath, timeoutMs, retryMs) {
248
- this.acquired = false;
249
- this.dirPath = `${filePath}.lock`;
250
- this.pidFilePath = node_path.join(this.dirPath, "pid");
251
- this.timeoutMs = timeoutMs;
252
- this.retryMs = retryMs;
253
- }
254
- async acquire() {
255
- const start = Date.now();
256
- while (true) try {
257
- await node_fs_promises.mkdir(this.dirPath, { mode: LOCK_DIR_MODE });
258
- await node_fs_promises.writeFile(this.pidFilePath, String(process.pid), "utf8").catch(() => void 0);
259
- this.acquired = true;
260
- return;
261
- } catch (err) {
262
- if (!isNodeError(err) || err.code !== "EEXIST") throw err;
263
- if (await this.isStaleLock()) {
264
- await node_fs_promises.rm(this.dirPath, {
265
- recursive: true,
266
- force: true
267
- });
268
- continue;
269
- }
270
- if (Date.now() - start >= this.timeoutMs) throw new Error(`ToolboxStore lock acquisition timed out for ${this.dirPath}`);
271
- await (0, node_timers_promises.setTimeout)(this.retryMs);
272
- }
273
- }
274
- async isStaleLock() {
275
- let pidStr;
276
- try {
277
- pidStr = await node_fs_promises.readFile(this.pidFilePath, "utf8");
278
- } catch {
279
- try {
280
- const stat = await node_fs_promises.stat(this.dirPath);
281
- return Date.now() - stat.mtimeMs > LOCK_STALE_GRACE_MS;
282
- } catch {
283
- return false;
284
- }
285
- }
286
- const pid = Number.parseInt(pidStr.trim(), 10);
287
- if (!Number.isFinite(pid) || pid <= 0) return true;
288
- return !isProcessAlive(pid);
289
- }
290
- async release() {
291
- if (!this.acquired) return;
292
- this.acquired = false;
293
- await node_fs_promises.rm(this.dirPath, {
294
- recursive: true,
295
- force: true
296
- });
297
- }
298
- };
299
- function defaultWorkspacePath() {
300
- if (process.env.SERVICEME_NO_WORKSPACE_TOOLBOX === "1") return null;
301
- return node_path.join(process.cwd(), WORKSPACE_TOOLBOX_RELATIVE_PATH);
302
- }
303
- var ToolboxStore = class {
304
- constructor(opts = {}) {
305
- this.userFilePath = opts.userFilePath ?? require_userHome.getToolboxJsonPath();
306
- this.resolveWorkspacePath = opts.resolveWorkspacePath ?? defaultWorkspacePath;
307
- this.defaults = opts.defaultTools ?? BUILTIN_DEFAULT_TOOLS;
308
- this.hooks = opts.hooks ?? {};
309
- this.backend = opts.backend ?? new FsToolboxFileBackend();
310
- this.lockTimeoutMs = opts.lockTimeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS;
311
- this.lockRetryMs = opts.lockRetryMs ?? DEFAULT_LOCK_RETRY_MS;
312
- }
313
- /** Read a scope; returns the resolved toolbox (with defaults merged when empty). */
314
- async read(scope) {
315
- const filePath = this.filePathFor(scope);
316
- if (scope === "workspace") await migrateLegacyWorkspaceToolboxFile(filePath);
317
- const storedTools = (filePath ? await this.backend.read(filePath) : null)?.tools ?? [];
318
- const merged = mergeWithDefaults(this.defaults.map((seed, index) => toDefaultTool(seed, index)), storedTools);
319
- storedTools.filter((t) => !this.defaults.some((d) => d.id === t.id));
320
- return {
321
- scope,
322
- tools: merged
323
- };
324
- }
325
- /**
326
- * Atomic write under a file lock. Replaces the entire `tools` array
327
- * with the provided snapshot.
328
- */
329
- async write(scope, tools) {
330
- const filePath = this.filePathFor(scope);
331
- if (!filePath) throw new Error(`Cannot write toolbox scope '${scope}': file path is unavailable`);
332
- if (scope === "workspace") await migrateLegacyWorkspaceToolboxFile(filePath);
333
- const payload = {
334
- version: 1,
335
- tools: sortByUserOrder([...tools])
336
- };
337
- await this.hooks.beforeWrite?.(scope, payload);
338
- const lock = new ToolboxFileLock(filePath, this.lockTimeoutMs, this.lockRetryMs);
339
- await lock.acquire();
340
- try {
341
- await this.backend.write(filePath, payload);
342
- } finally {
343
- await lock.release();
344
- }
345
- await this.hooks.afterWrite?.(scope, payload);
346
- }
347
- /**
348
- * Read-modify-write under the file lock. The mutator receives the
349
- * current user-only list (defaults not included) and returns the
350
- * replacement list. Throwing inside the mutator aborts the write.
351
- */
352
- async mutate(scope, mutator) {
353
- const filePath = this.filePathFor(scope);
354
- if (!filePath) throw new Error(`Cannot mutate toolbox scope '${scope}': file path is unavailable`);
355
- if (scope === "workspace") await migrateLegacyWorkspaceToolboxFile(filePath);
356
- const lock = new ToolboxFileLock(filePath, this.lockTimeoutMs, this.lockRetryMs);
357
- await lock.acquire();
358
- try {
359
- const storedTools = (await this.backend.read(filePath))?.tools ?? [];
360
- const defaultIds = new Set(this.defaults.map((d) => d.id));
361
- const next = await mutator(storedTools.filter((t) => !defaultIds.has(t.id)));
362
- const payload = {
363
- version: 1,
364
- tools: sortByUserOrder([...next])
365
- };
366
- await this.hooks.beforeWrite?.(scope, payload);
367
- await this.backend.write(filePath, payload);
368
- await this.hooks.afterWrite?.(scope, payload);
369
- return next;
370
- } finally {
371
- await lock.release();
372
- }
373
- }
374
- /** Wipe a scope entirely (used by `toolbox.remove --all` extensions). */
375
- async clear(scope) {
376
- const filePath = this.filePathFor(scope);
377
- if (!filePath) return;
378
- await node_fs_promises.rm(filePath, { force: true });
379
- }
380
- /** Test seam — resolve the user-scope file path. */
381
- getUserFilePath() {
382
- return this.userFilePath;
383
- }
384
- /** Test seam — resolve the workspace-scope file path (or null when disabled). */
385
- getWorkspaceFilePath() {
386
- return this.resolveWorkspacePath();
387
- }
388
- filePathFor(scope) {
389
- return scope === "user" ? this.userFilePath : this.resolveWorkspacePath();
390
- }
391
- };
392
- function toDefaultTool(seed, order) {
393
- return {
394
- id: seed.id,
395
- name: seed.name,
396
- description: seed.description,
397
- icon: seed.icon,
398
- url: seed.url,
399
- isDefault: true,
400
- order,
401
- scope: "user"
402
- };
403
- }
404
- //#endregion
405
- //#region src/toolbox/ToolboxCore.ts
406
- /** Sentinel — caller tried to remove a built-in (immutable) tool. */
407
- var DefaultToolImmutableError = class extends Error {
408
- constructor(toolId) {
409
- super(`Cannot remove default toolbox entry: ${toolId}`);
410
- this.toolId = toolId;
411
- this.name = "DefaultToolImmutableError";
412
- }
413
- };
414
- var ToolboxCore = class {
415
- constructor(opts = {}) {
416
- this.store = opts.store ?? new ToolboxStore();
417
- this.defaults = opts.defaultTools ?? BUILTIN_DEFAULT_TOOLS;
418
- this.listSort = opts.listSort ?? sortByUserOrder;
419
- }
420
- /** List all tools in a scope. Built-in defaults are merged in. */
421
- async list(scope = "user") {
422
- const resolved = await this.store.read(scope);
423
- return {
424
- scope,
425
- tools: this.listSort(resolved.tools)
426
- };
427
- }
428
- /**
429
- * Append a new tool to the requested scope. Default tools are
430
- * rejected (they are seeds, not user entries).
431
- */
432
- async add(tool, scope = "user") {
433
- if (this.isDefaultId(tool.id)) throw new DefaultToolImmutableError(tool.id);
434
- return {
435
- scope,
436
- tools: await this.store.mutate(scope, async (current) => {
437
- const filtered = current.filter((t) => t.id !== tool.id);
438
- return [...filtered, {
439
- ...tool,
440
- scope,
441
- isDefault: false,
442
- order: tool.order ?? filtered.length
443
- }];
444
- })
445
- };
446
- }
447
- /**
448
- * Remove a tool by id. Returns `success: false` when the id is a
449
- * built-in default (idempotent, never throws on missing entries).
450
- */
451
- async remove(id, scope = "user") {
452
- if (this.isDefaultId(id)) throw new DefaultToolImmutableError(id);
453
- return {
454
- scope,
455
- toolId: id,
456
- success: !(await this.store.mutate(scope, async (current) => current.filter((t) => t.id !== id))).some((t) => t.id === id)
457
- };
458
- }
459
- /**
460
- * Patch a tool by id. Default tools can only have their `order`
461
- * updated; other patches are silently ignored for default entries
462
- * (callers can compare before/after to detect the ignore).
463
- */
464
- async update(id, patch, scope = "user") {
465
- const isDefault = this.isDefaultId(id);
466
- return {
467
- scope,
468
- tools: await this.store.mutate(scope, async (current) => {
469
- if (isDefault) return current;
470
- return current.map((tool) => tool.id === id ? {
471
- ...tool,
472
- ...patch,
473
- scope,
474
- isDefault: false
475
- } : tool);
476
- })
477
- };
478
- }
479
- /**
480
- * Stamp `lastUsedAt` on the targeted tool. This is the "recently
481
- * used" hook the Extension's webview uses when a user clicks a
482
- * toolbox entry.
483
- */
484
- async recordUsage(id, scope = "user", when = /* @__PURE__ */ new Date()) {
485
- if (this.isDefaultId(id)) {
486
- const seed = this.defaults.find((d) => d.id === id);
487
- if (!seed) return null;
488
- return {
489
- ...seed,
490
- scope,
491
- isDefault: true,
492
- order: 0,
493
- lastUsedAt: when.toISOString()
494
- };
495
- }
496
- let updated = null;
497
- await this.store.mutate(scope, async (current) => {
498
- const next = touchLastUsedAt(current, id, when);
499
- updated = next.find((t) => t.id === id) ?? null;
500
- return next;
501
- });
502
- return updated;
503
- }
504
- /**
505
- * Combined view: user + workspace scopes merged, sorted by recent
506
- * usage. Workspace tools overlay user tools (workspace entries win
507
- * on `id` collision).
508
- */
509
- async listMerged() {
510
- const [user, workspace] = await Promise.all([this.store.read("user"), this.store.read("workspace")]);
511
- const seen = /* @__PURE__ */ new Set();
512
- const merged = [];
513
- for (const tool of workspace.tools) {
514
- seen.add(tool.id);
515
- merged.push({
516
- ...tool,
517
- scope: "workspace"
518
- });
519
- }
520
- for (const tool of user.tools) {
521
- if (seen.has(tool.id)) continue;
522
- merged.push({
523
- ...tool,
524
- scope: "user"
525
- });
526
- }
527
- return {
528
- scope: "user",
529
- tools: sortByRecentFirst(merged)
530
- };
531
- }
532
- /** Expose the underlying store (CLI / Bridge use it for path-level access). */
533
- getStore() {
534
- return this.store;
535
- }
536
- isDefaultId(id) {
537
- return this.defaults.some((d) => d.id === id);
538
- }
539
- };
540
- //#endregion
541
- Object.defineProperty(exports, "BUILTIN_DEFAULT_TOOLS", {
542
- enumerable: true,
543
- get: function() {
544
- return BUILTIN_DEFAULT_TOOLS;
545
- }
546
- });
547
- Object.defineProperty(exports, "DefaultToolImmutableError", {
548
- enumerable: true,
549
- get: function() {
550
- return DefaultToolImmutableError;
551
- }
552
- });
553
- Object.defineProperty(exports, "FsToolboxFileBackend", {
554
- enumerable: true,
555
- get: function() {
556
- return FsToolboxFileBackend;
557
- }
558
- });
559
- Object.defineProperty(exports, "TOOLBOX_JSON_SCHEMA_VERSION", {
560
- enumerable: true,
561
- get: function() {
562
- return TOOLBOX_JSON_SCHEMA_VERSION;
563
- }
564
- });
565
- Object.defineProperty(exports, "ToolboxCore", {
566
- enumerable: true,
567
- get: function() {
568
- return ToolboxCore;
569
- }
570
- });
571
- Object.defineProperty(exports, "ToolboxStore", {
572
- enumerable: true,
573
- get: function() {
574
- return ToolboxStore;
575
- }
576
- });
577
- Object.defineProperty(exports, "WORKSPACE_TOOLBOX_RELATIVE_PATH", {
578
- enumerable: true,
579
- get: function() {
580
- return WORKSPACE_TOOLBOX_RELATIVE_PATH;
581
- }
582
- });
583
- Object.defineProperty(exports, "mergeWithDefaults", {
584
- enumerable: true,
585
- get: function() {
586
- return mergeWithDefaults;
587
- }
588
- });
589
- Object.defineProperty(exports, "reindexOrder", {
590
- enumerable: true,
591
- get: function() {
592
- return reindexOrder;
593
- }
594
- });
595
- Object.defineProperty(exports, "sortByRecentFirst", {
596
- enumerable: true,
597
- get: function() {
598
- return sortByRecentFirst;
599
- }
600
- });
601
- Object.defineProperty(exports, "sortByUserOrder", {
602
- enumerable: true,
603
- get: function() {
604
- return sortByUserOrder;
605
- }
606
- });
607
- Object.defineProperty(exports, "touchLastUsedAt", {
608
- enumerable: true,
609
- get: function() {
610
- return touchLastUsedAt;
611
- }
612
- });
613
-
614
- //# sourceMappingURL=toolbox-BV7vLGXZ.js.map