@xrxs-ai/dsh-xrxs-plugins-store 0.2.5 → 0.2.6

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/lib/client.js CHANGED
@@ -462,6 +462,13 @@ function formatDate(text) {
462
462
  if (d === null) return text;
463
463
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
464
464
  }
465
+ function formatShortDate(text) {
466
+ const d = parseTime(text);
467
+ if (d === null) return text;
468
+ const now = /* @__PURE__ */ new Date();
469
+ const yearPrefix = d.getFullYear() === now.getFullYear() ? "" : `${String(d.getFullYear() % 100).padStart(2, "0")}-`;
470
+ return `${yearPrefix}${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
471
+ }
465
472
  function formatRelative(text) {
466
473
  const parsed = parseTime(text);
467
474
  if (parsed === null) return text;
@@ -1042,30 +1049,8 @@ var MARKET_CSS = `
1042
1049
  font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-primary, #1f2329);
1043
1050
  text-align: right; word-break: break-all;
1044
1051
  }
1045
- .xps-mk-version {
1046
- display: flex; justify-content: space-between; align-items: center; gap: 8px;
1047
- width: 100%; box-sizing: border-box; padding: 4px 8px;
1048
- border: none; border-radius: 8px; background: transparent; cursor: pointer;
1049
- color: var(--dsw-alias-label-primary, #1f2329);
1050
- font-family: var(--dsw-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
1051
- font-size: 12px; line-height: 18px; text-align: left;
1052
- transition: background-color 0.15s ease;
1053
- }
1054
- .xps-mk-version:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(31, 35, 41, 0.06)); }
1055
- .xps-mk-version:active { background: var(--dsw-alias-interactive-bg-active, rgba(38, 49, 72, 0.1)); }
1056
- .xps-mk-version:focus-visible {
1057
- outline: 2px solid var(--dsw-alias-brand-primary, #3370ff);
1058
- outline-offset: -2px;
1059
- }
1060
- .xps-mk-version-active, .xps-mk-version-active:hover {
1061
- background: var(--dsw-alias-interactive-bg-active, rgba(38, 49, 72, 0.1));
1062
- color: var(--dsw-alias-label-primary, #1f2329);
1063
- }
1064
- .xps-mk-version-date {
1065
- flex: none; font-size: 11px; line-height: 17px;
1066
- font-family: inherit;
1067
- color: var(--dsw-alias-label-tertiary, #8f959e);
1068
- }
1052
+ /* \u8BE6\u60C5\u9875\u7248\u672C\u5386\u53F2\u4E0B\u62C9:\u5360\u6EE1\u5361\u7247\u5BBD(.xps-mk-select \u9ED8\u8BA4 flex:none \u53EA\u9002\u914D\u5DE5\u5177\u680F) */
1053
+ .xps-mk-version-select { display: block; width: 100%; margin-top: 8px; }
1069
1054
 
1070
1055
  /* README markdown \u767D\u540D\u5355\u6E32\u67D3 */
1071
1056
  .xps-md {
@@ -1125,8 +1110,7 @@ var MARKET_CSS = `
1125
1110
  .xps-mk-chip,
1126
1111
  .xps-mk-row,
1127
1112
  .xps-mk-page,
1128
- .xps-mk-select,
1129
- .xps-mk-version {
1113
+ .xps-mk-select {
1130
1114
  transition: none;
1131
1115
  }
1132
1116
  .xps-mk-page:active:not(:disabled) {
@@ -2267,6 +2251,12 @@ function ConfirmDialog3(props) {
2267
2251
  ] })
2268
2252
  ] }) });
2269
2253
  }
2254
+ function byInstalledAtDesc(a, b) {
2255
+ const ta = a.installedAt ?? "";
2256
+ const tb = b.installedAt ?? "";
2257
+ if (ta !== tb) return ta < tb ? 1 : -1;
2258
+ return a.name.localeCompare(b.name);
2259
+ }
2270
2260
  function InstalledPage() {
2271
2261
  ensureMarketStyles();
2272
2262
  const { items, push, dismiss } = useToasts();
@@ -2280,7 +2270,7 @@ function InstalledPage() {
2280
2270
  const load = (0, import_react5.useCallback)(() => {
2281
2271
  setList(null);
2282
2272
  setError(false);
2283
- void getInstalled().then((installed) => setList(installed)).catch(() => {
2273
+ void getInstalled().then((installed) => setList([...installed].sort(byInstalledAtDesc))).catch(() => {
2284
2274
  setList([]);
2285
2275
  setError(true);
2286
2276
  });
@@ -2549,6 +2539,24 @@ function satisfies(version, range) {
2549
2539
 
2550
2540
  // src/client/pages/market/InstallButton.tsx
2551
2541
  var import_react6 = require("react");
2542
+
2543
+ // src/client/pages/market/version.ts
2544
+ function compareVersions(a, b) {
2545
+ const parse = (text) => text.trim().replace(/^v/, "").split(".").map((part) => {
2546
+ const numeric = /^\d+/.exec(part);
2547
+ return numeric === null ? 0 : Number(numeric[0]);
2548
+ });
2549
+ const left = parse(a);
2550
+ const right = parse(b);
2551
+ const length = Math.max(left.length, right.length);
2552
+ for (let index = 0; index < length; index += 1) {
2553
+ const diff = (left[index] ?? 0) - (right[index] ?? 0);
2554
+ if (diff !== 0) return diff > 0 ? 1 : -1;
2555
+ }
2556
+ return 0;
2557
+ }
2558
+
2559
+ // src/client/pages/market/InstallButton.tsx
2552
2560
  var import_jsx_runtime15 = require("react/jsx-runtime");
2553
2561
  function InstallButton(props) {
2554
2562
  const [phase, setPhase] = (0, import_react6.useState)("idle");
@@ -2556,10 +2564,11 @@ function InstallButton(props) {
2556
2564
  const [buildConfirm, setBuildConfirm] = (0, import_react6.useState)(null);
2557
2565
  const [approving, setApproving] = (0, import_react6.useState)(false);
2558
2566
  const compat = checkCompat(props.compatRange, props.dshVersion);
2559
- const verb = props.installed?.updateAvailable === true ? "\u66F4\u65B0" : "\u5B89\u88C5";
2560
- const variant = verb === "\u66F4\u65B0" ? "update" : "primary";
2561
- if (props.installed !== void 0 && !props.installed.updateAvailable) {
2562
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "xps-mk-install", onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Button, { size: props.size ?? "medium", variant: "primary", disabled: true, title: "\u5DF2\u5B89\u88C5\u6700\u65B0\u7248\u672C,\u91CD\u88C5\u540C\u7248\u672C\u4E0D\u4F1A\u5237\u65B0", children: "\u5DF2\u5B89\u88C5" }) });
2567
+ const sameAsInstalled = props.installed !== void 0 && props.installed.version === props.version;
2568
+ const verb = props.installed === void 0 || sameAsInstalled ? "\u5B89\u88C5" : compareVersions(props.version, props.installed.version) >= 0 ? "\u66F4\u65B0" : "\u964D\u7EA7";
2569
+ const variant = verb === "\u5B89\u88C5" ? "primary" : "update";
2570
+ if (sameAsInstalled) {
2571
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "xps-mk-install", onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Button, { size: props.size ?? "medium", variant: "primary", disabled: true, title: "\u5DF2\u5B89\u88C5\u6B64\u7248\u672C,\u91CD\u88C5\u540C\u7248\u672C\u4E0D\u4F1A\u5237\u65B0", children: "\u5DF2\u5B89\u88C5" }) });
2563
2572
  }
2564
2573
  if (compat.ok === false) {
2565
2574
  return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "xps-mk-install", onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
@@ -3233,12 +3242,32 @@ function PluginDetailPage(props) {
3233
3242
  const [dshVersion, setDshVersion] = (0, import_react8.useState)(null);
3234
3243
  const [installed, setInstalled] = (0, import_react8.useState)(null);
3235
3244
  const toasts = useToasts();
3245
+ const installedSeq = (0, import_react8.useRef)(0);
3236
3246
  const loadInstalled = (0, import_react8.useCallback)(
3237
- (name) => {
3238
- getInstalled().then((list) => setInstalled(list.find((item) => item.name === name) ?? null)).catch(() => setInstalled(null));
3247
+ (...names) => {
3248
+ const seq = ++installedSeq.current;
3249
+ getInstalled().then((list) => {
3250
+ if (seq === installedSeq.current) {
3251
+ setInstalled(list.find((item) => names.includes(item.name)) ?? null);
3252
+ }
3253
+ }).catch(() => {
3254
+ if (seq === installedSeq.current) setInstalled(null);
3255
+ });
3239
3256
  },
3240
3257
  []
3241
3258
  );
3259
+ const selectVersion = (version) => {
3260
+ setSelectedVersion(version);
3261
+ if (detail !== null) loadInstalled(pluginId, detail.name);
3262
+ };
3263
+ (0, import_react8.useEffect)(() => {
3264
+ if (detail === null) return;
3265
+ const onVisible = () => {
3266
+ if (document.visibilityState === "visible") loadInstalled(pluginId, detail.name);
3267
+ };
3268
+ document.addEventListener("visibilitychange", onVisible);
3269
+ return () => document.removeEventListener("visibilitychange", onVisible);
3270
+ }, [detail, pluginId, loadInstalled]);
3242
3271
  (0, import_react8.useEffect)(() => {
3243
3272
  let alive = true;
3244
3273
  getHostInfo().then((info) => {
@@ -3255,7 +3284,6 @@ function PluginDetailPage(props) {
3255
3284
  setDetail(null);
3256
3285
  setSelectedVersion(null);
3257
3286
  setInstalled(null);
3258
- if (pluginId !== "") loadInstalled(pluginId);
3259
3287
  getPluginDetail(pluginId).then((d) => {
3260
3288
  if (!alive) return;
3261
3289
  if (d === null) {
@@ -3264,6 +3292,7 @@ function PluginDetailPage(props) {
3264
3292
  setDetail(d);
3265
3293
  setSelectedVersion(d.version);
3266
3294
  setStatus("ready");
3295
+ loadInstalled(pluginId, d.name);
3267
3296
  }
3268
3297
  }).catch(() => {
3269
3298
  if (alive) setStatus("error");
@@ -3351,7 +3380,7 @@ function PluginDetailPage(props) {
3351
3380
  sourceType: detail.sourceType,
3352
3381
  dshVersion,
3353
3382
  installed: installed ?? void 0,
3354
- onInstalled: () => loadInstalled(detail.name),
3383
+ onInstalled: () => loadInstalled(pluginId, detail.name),
3355
3384
  onToast: toasts.push
3356
3385
  }
3357
3386
  )
@@ -3380,24 +3409,22 @@ function PluginDetailPage(props) {
3380
3409
  ] }),
3381
3410
  /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "xps-mk-card", children: [
3382
3411
  /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "xps-mk-card-title", children: "\u7248\u672C\u5386\u53F2" }),
3383
- detail.versions.map((v) => /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
3384
- "button",
3412
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
3413
+ "select",
3385
3414
  {
3386
- type: "button",
3387
- className: `xps-mk-version${v.version === displayVersion ? " xps-mk-version-active" : ""}`,
3388
- title: `\u5207\u6362\u5230 v${v.version}`,
3389
- onClick: () => setSelectedVersion(v.version),
3390
- children: [
3391
- /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("span", { children: [
3392
- "v",
3393
- v.version,
3394
- v.version === detail.version ? " \xB7 \u6700\u65B0" : ""
3395
- ] }),
3396
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "xps-mk-version-date", children: formatDate(v.publishedAt) })
3397
- ]
3398
- },
3399
- v.version
3400
- ))
3415
+ className: "xps-mk-select xps-mk-version-select",
3416
+ value: displayVersion,
3417
+ title: "\u5207\u6362\u7248\u672C",
3418
+ onChange: (e) => selectVersion(e.target.value),
3419
+ children: detail.versions.map((v) => /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("option", { value: v.version, children: [
3420
+ "v",
3421
+ v.version,
3422
+ " \xB7 ",
3423
+ formatShortDate(v.publishedAt),
3424
+ v.version === detail.version ? " \xB7 \u6700\u65B0" : ""
3425
+ ] }, v.version))
3426
+ }
3427
+ )
3401
3428
  ] })
3402
3429
  ] })
3403
3430
  ] })
@@ -88,6 +88,9 @@ export interface InstalledEntry {
88
88
  managed: boolean;
89
89
  /** 已装包自身 package.json 的 description(file: 本地装的包不在市场里,简介只能从这里来);读不到为 null。 */
90
90
  description: string | null;
91
+ /** 安装时间(ISO 8601);取 node_modules/<name> 的创建时间(pnpm 装包时建立,更新重装会刷新)。
92
+ * 目录缺失/文件系统不支持 birthtime 时为 null,页面把它排在最后。 */
93
+ installedAt: string | null;
91
94
  }
92
95
  /** 已装列表:读 <DSH_HOME>/profiles/<profile>/package.json 依赖 + dsh.profile.bundles 层栈
93
96
  * (desktop profile 被 CLI 独占时读文件不受影响;设计文档 §3.3)。
@@ -1,7 +1,7 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { spawn } from 'node:child_process';
3
3
  import { existsSync, readdirSync, statSync } from 'node:fs';
4
- import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
4
+ import { lstat, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
5
5
  import { homedir } from 'node:os';
6
6
  import { basename, delimiter, dirname, join } from 'node:path';
7
7
  import { ApiError } from "./api-client.js";
@@ -437,7 +437,18 @@ export async function listInstalledFromProfile(host) {
437
437
  }
438
438
  if (version === '')
439
439
  version = versionFromFileSpec(spec) ?? '';
440
- return { name, version, managed: bundles.has(name), description };
440
+ // 安装时间:lstat 链接本身(pnpm 的 node_modules 项是指向 .pnpm 的符号链接,
441
+ // 链接创建时间 = 装包时间);birthtime 不可用(≤0/非法)退 mtime,再不行置 null
442
+ let installedAt = null;
443
+ try {
444
+ const link = await lstat(join(profileDir, 'node_modules', ...name.split('/')));
445
+ const time = link.birthtimeMs > 0 ? link.birthtime : link.mtime;
446
+ installedAt = Number.isNaN(time.getTime()) || time.getTime() <= 0 ? null : time.toISOString();
447
+ }
448
+ catch {
449
+ // 路径不存在:installedAt 保持 null
450
+ }
451
+ return { name, version, managed: bundles.has(name), description, installedAt };
441
452
  }));
442
453
  }
443
454
  const OUTPUT_CAP = 512 * 1024;
@@ -301,7 +301,11 @@ async function fetchLatestVersionMap(deps) {
301
301
  const version = optionalString(item.version);
302
302
  if (name === undefined || version === undefined)
303
303
  continue;
304
- map.set(name, { version, summary: optionalString(item.summary) ?? null });
304
+ map.set(name, {
305
+ version,
306
+ displayName: optionalString(item.displayName) ?? null,
307
+ summary: optionalString(item.summary) ?? null,
308
+ });
305
309
  }
306
310
  }
307
311
  catch (error) {
@@ -321,6 +325,9 @@ async function handleInstalled(rc) {
321
325
  latestVersion,
322
326
  updateAvailable: latestVersion !== null && latestVersion !== entry.version,
323
327
  managed: entry.managed,
328
+ installedAt: entry.installedAt,
329
+ // 展示名来自市场;本机手装/离线的包没有,client 半自行回退包名
330
+ displayName: meta?.displayName ?? null,
324
331
  // 市场简介优先;file: 本地装的包不在市场里,回退该包自己 package.json 的 description
325
332
  summary: meta?.summary ?? entry.description,
326
333
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xrxs-ai/dsh-xrxs-plugins-store",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "DSH 私有插件市场插件(静态持久 bundle,双 face)",
5
5
  "type": "module",
6
6
  "license": "MIT",