dsh-shutdown-guard 1.6.1-beta.1 → 1.6.1-beta.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## v1.6.1-beta.2 — 2026-11-08
4
+
5
+ > 测试版(beta):`latest` 仍指向 1.6.0,`npm i dsh-shutdown-guard` 不会装到它。装这个版本请用 `npm i dsh-shutdown-guard@1.6.1-beta.2`。
6
+
7
+ - ⚠️ `~` **默认配置下,锁屏 / 注销 / 自定义脚本不再可用**:这三件事会让人离开机器、或让插件去执行一段你未必盯着的代码,所以现在要先开启「自定义配置」。设置页会写明原因「开启「自定义配置」后才能用这个动作」;即使绕过界面直接调接口武装,判定闸也同样拦住,不会进倒计时。
8
+ - `~` **守夜页的行序换了**:倒计时挪到最上面,动作紧随其后;「会话都停下来」移到「后台任务跑完」与「有会话报错就跳过」之前,与判定实际执行的顺序一致。功能没少,位置变了。
9
+ - `+` **不再限制安装平台**:移除了包里的平台限制,macOS 与 Linux 现在能正常安装(此前 npm 会直接以 `EBADPLATFORM` 拒绝);Windows 用户不受影响。
10
+ - 修复:自定义脚本还没上传文件时,下拉里那一项以前是灰的、点不动也没有说明 → 现在可以先选中再挑文件,不能武装时会说清原因。
11
+ - 修复:关闭「自定义配置」后,脚本上传行与确认框整体收起;如果此前选的就是自定义脚本,动作下拉显示一个禁用的「自定义脚本(不可用)」占位,而不是空白。
12
+ - **测试与体积**:宿主断言 **66 项**、客户端 **15 项**全绿;本版 npm 包约 363 kB(未压缩解包约 763 kB),仍是 11 个文件。
13
+
3
14
  ## v1.6.1-beta.1 — 2026-11-06
4
15
 
5
16
  > 测试版(beta):`latest` 仍指向 1.6.0,`npm i dsh-shutdown-guard` 不会装到它。装这个版本请用 `npm i dsh-shutdown-guard@1.6.1-beta.1`。
package/README.en.md CHANGED
@@ -7,7 +7,7 @@ Stable `v1.6.0` · beta `v1.6.1-beta.1` (closed beta) · [Changelog](https://git
7
7
 
8
8
  [![version](https://img.shields.io/badge/version-1.6.0-2ea44f)](https://github.com/ltayshark0637/dsh-Shutdown-Guard/blob/main/CHANGELOG.md) [![DSH](https://img.shields.io/badge/DSH-%E2%89%A50.1.5--rc.1-4a90d9)](https://github.com/deepseek-ai)
9
9
  [![demo](https://img.shields.io/badge/%E2%96%B6-interactive%20demo-d97757)](https://ltayshark0637.github.io/dsh-Shutdown-Guard/demo.html) [![license](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
10
- [![dshbase install-tested](https://dshbase.com/badges/dsh-shutdown-guard.svg)](https://dshbase.com/plugins/dsh-shutdown-guard/) [![awesome-dsh-plugin listed](https://awesome-dsh-plugin.com/badge.svg)](https://awesome-dsh-plugin.com/p/ltayshark0637/dsh-Shutdown-Guard/)
10
+ [![dshbase install-tested](https://dshbase.com/badges/dsh-shutdown-guard.svg)](https://dshbase.com/plugins/dsh-shutdown-guard/) [![dsh.so runtime-tested](https://cdn.dsh.so/badge/dsh-shutdown-guard.svg)](https://www.dsh.so/artifact/dsh-shutdown-guard/?ref=badge) [![dsh-plugin.org listed](https://dsh-plugin.org/badges/listed.svg)](https://dsh-plugin.org/plugins/ltayshark0637/dsh-shutdown-guard) [![awesome-dsh-plugin listed](https://awesome-dsh-plugin.com/badge.svg)](https://awesome-dsh-plugin.com/p/ltayshark0637/dsh-Shutdown-Guard/) [![dpharness listed](https://img.shields.io/badge/dpharness-listed-6f42c1)](https://dpharness.com/plugin/ltayshark0637/dsh-Shutdown-Guard)
11
11
 
12
12
  Adds a "shut down when every task is done" card to the DeepSeek Harness sidebar — right under Skill Centre and above the session list, following the task state live.
13
13
 
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  [![version](https://img.shields.io/badge/version-1.6.0-2ea44f)](https://github.com/ltayshark0637/dsh-Shutdown-Guard/blob/main/CHANGELOG.md) [![DSH](https://img.shields.io/badge/DSH-%E2%89%A50.1.5--rc.1-4a90d9)](https://github.com/deepseek-ai)
9
9
  [![demo](https://img.shields.io/badge/%E2%96%B6-%E4%BA%A4%E4%BA%92%E6%BC%94%E7%A4%BA-d97757)](https://ltayshark0637.github.io/dsh-Shutdown-Guard/demo.html) [![license](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
10
- [![dshbase 实装验证](https://dshbase.com/badges/dsh-shutdown-guard.svg)](https://dshbase.com/plugins/dsh-shutdown-guard/) [![awesome-dsh-plugin 收录](https://awesome-dsh-plugin.com/badge.svg)](https://awesome-dsh-plugin.com/p/ltayshark0637/dsh-Shutdown-Guard/)
10
+ [![dshbase 实装验证](https://dshbase.com/badges/dsh-shutdown-guard.svg)](https://dshbase.com/plugins/dsh-shutdown-guard/) [![dsh.so 运行验证](https://cdn.dsh.so/badge/dsh-shutdown-guard.svg)](https://www.dsh.so/artifact/dsh-shutdown-guard/?ref=badge) [![dsh-plugin.org 收录](https://dsh-plugin.org/badges/listed.svg)](https://dsh-plugin.org/plugins/ltayshark0637/dsh-shutdown-guard) [![awesome-dsh-plugin 收录](https://awesome-dsh-plugin.com/badge.svg)](https://awesome-dsh-plugin.com/p/ltayshark0637/dsh-Shutdown-Guard/) [![dpharness 收录](https://img.shields.io/badge/dpharness-%E6%94%B6%E5%BD%95-6f42c1)](https://dpharness.com/plugin/ltayshark0637/dsh-Shutdown-Guard)
11
11
 
12
12
  为 DeepSeek Harness 的侧边栏加一张「所有任务完成后自动关机」的卡片,就在「技能中心」下方、会话列表上方,随任务状态实时变化。
13
13
 
package/README.ru.md CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  [![version](https://img.shields.io/badge/version-1.6.0-2ea44f)](https://github.com/ltayshark0637/dsh-Shutdown-Guard/blob/main/CHANGELOG.md) [![DSH](https://img.shields.io/badge/DSH-%E2%89%A50.1.5--rc.1-4a90d9)](https://github.com/deepseek-ai)
9
9
  [![demo](https://img.shields.io/badge/%E2%96%B6-%D0%B4%D0%B5%D0%BC%D0%BE-d97757)](https://ltayshark0637.github.io/dsh-Shutdown-Guard/demo.html) [![license](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
10
- [![dshbase: проверено установкой](https://dshbase.com/badges/dsh-shutdown-guard.svg)](https://dshbase.com/plugins/dsh-shutdown-guard/) [![awesome-dsh-plugin: в каталоге](https://awesome-dsh-plugin.com/badge.svg)](https://awesome-dsh-plugin.com/p/ltayshark0637/dsh-Shutdown-Guard/)
10
+ [![dshbase: проверено установкой](https://dshbase.com/badges/dsh-shutdown-guard.svg)](https://dshbase.com/plugins/dsh-shutdown-guard/) [![dsh.so: проверено запуском](https://cdn.dsh.so/badge/dsh-shutdown-guard.svg)](https://www.dsh.so/artifact/dsh-shutdown-guard/?ref=badge) [![dsh-plugin.org: в каталоге](https://dsh-plugin.org/badges/listed.svg)](https://dsh-plugin.org/plugins/ltayshark0637/dsh-shutdown-guard) [![awesome-dsh-plugin: в каталоге](https://awesome-dsh-plugin.com/badge.svg)](https://awesome-dsh-plugin.com/p/ltayshark0637/dsh-Shutdown-Guard/) [![dpharness: в каталоге](https://img.shields.io/badge/dpharness-%D0%B2%20%D0%BA%D0%B0%D1%82%D0%B0%D0%BB%D0%BE%D0%B3%D0%B5-6f42c1)](https://dpharness.com/plugin/ltayshark0637/dsh-Shutdown-Guard)
11
11
 
12
12
  Добавляет в боковую панель DeepSeek Harness карточку «выключить, когда все задачи готовы» — сразу под «Центром навыков» и над списком сессий, в реальном времени следуя за состоянием задач.
13
13
 
package/lib/client.js CHANGED
@@ -198,6 +198,7 @@
198
198
  'action.signout': '注销',
199
199
  'action.restart-dsh': '重启 DSH',
200
200
  'action.custom': '自定义脚本',
201
+ 'action.custom.off': '自定义脚本(不可用)',
201
202
  'action.unknown': '可能需要授权;如果失败,会告诉你原因。',
202
203
  'action.reason.no-cli': '没有找到这个操作需要的系统命令。',
203
204
  'action.reason.may-need-permissions': '可能需要管理员权限;执行失败时会说明原因。',
@@ -205,6 +206,7 @@
205
206
  'action.reason.unsupported-on-platform': '当前系统不支持这个操作。',
206
207
  'action.reason.no-host-restart': 'DSH 不支持自重启,请手动退出后重开。',
207
208
  'action.reason.no-script': '还没有选择脚本文件。',
209
+ 'action.reason.no-custom-profile': '开启「自定义配置」后才能用这个动作。',
208
210
  'action.reason.missing-interpreter': '找不到运行这个脚本所需的解释器。',
209
211
  'action.reason.script-not-acknowledged': '脚本还没有确认,先在下面勾选确认。',
210
212
  'set.halt': '熔断后关机',
@@ -377,6 +379,7 @@
377
379
  'action.signout': 'Sign out',
378
380
  'action.restart-dsh': 'Restart DSH',
379
381
  'action.custom': 'Custom script',
382
+ 'action.custom.off': 'Custom script (unavailable)',
380
383
  'action.unknown': 'It may need permission; if it fails, you will see why.',
381
384
  'action.reason.no-cli': 'The system command this action needs was not found.',
382
385
  'action.reason.may-need-permissions': 'It may need administrator rights; if it fails, you will see why.',
@@ -384,6 +387,7 @@
384
387
  'action.reason.unsupported-on-platform': 'This system does not support this action.',
385
388
  'action.reason.no-host-restart': 'DSH cannot restart itself; quit and open it again manually.',
386
389
  'action.reason.no-script': 'No script file has been chosen yet.',
390
+ 'action.reason.no-custom-profile': 'Turn on custom profiles to use this action.',
387
391
  'action.reason.missing-interpreter': 'The interpreter needed to run this script was not found.',
388
392
  'action.reason.script-not-acknowledged': 'The script is not confirmed yet; tick the box below first.',
389
393
  'set.halt': 'Shut down after a halt',
@@ -556,6 +560,7 @@
556
560
  'action.signout': 'Выйти из системы',
557
561
  'action.restart-dsh': 'Перезапустить DSH',
558
562
  'action.custom': 'Свой скрипт',
563
+ 'action.custom.off': 'Свой скрипт (недоступен)',
559
564
  'action.unknown': 'Может потребоваться разрешение; при ошибке будет указана причина.',
560
565
  'action.reason.no-cli': 'Системная команда для этого действия не найдена.',
561
566
  'action.reason.may-need-permissions': 'Может потребоваться разрешение администратора; при ошибке будет указана причина.',
@@ -563,6 +568,7 @@
563
568
  'action.reason.unsupported-on-platform': 'Эта система не поддерживает такое действие.',
564
569
  'action.reason.no-host-restart': 'DSH не может перезапустить себя; закройте и откройте его вручную.',
565
570
  'action.reason.no-script': 'Файл скрипта ещё не выбран.',
571
+ 'action.reason.no-custom-profile': 'Включите собственные профили, чтобы использовать это действие.',
566
572
  'action.reason.missing-interpreter': 'Не найден интерпретатор для запуска этого скрипта.',
567
573
  'action.reason.script-not-acknowledged': 'Скрипт ещё не подтверждён; сначала отметьте поле ниже.',
568
574
  'set.halt': 'Выключить после сбоя',
@@ -2386,19 +2392,36 @@
2386
2392
  }
2387
2393
 
2388
2394
  function actionPicker(snap, current, onPick) {
2395
+ var showCustom = snap.customEnabled === true
2396
+ var ids = showCustom
2397
+ ? ACTION_IDS
2398
+ : ACTION_IDS.filter(function (id) { return id !== 'custom' })
2399
+ // When custom profiles were switched off after someone had picked the
2400
+ // script action, the stored value is no longer one of the visible
2401
+ // options. A <select> would silently render blank, so keep a disabled
2402
+ // placeholder at the top that tells the truth; it disappears as soon
2403
+ // as the value changes to a listed action.
2404
+ var hiddenCustom = !showCustom && current === 'custom'
2405
+ ? h('option', { key: 'custom-off', value: 'custom', disabled: true }, tr('action.custom.off'))
2406
+ : null
2389
2407
  return h('select', {
2390
2408
  className: 'dsp-sel',
2391
2409
  value: current,
2392
2410
  'aria-label': tr('set.action'),
2393
2411
  onChange: function (ev) { onPick(ev.target.value) },
2394
- }, ACTION_IDS.map(function (id) {
2412
+ }, hiddenCustom, ids.map(function (id) {
2395
2413
  var info = actionInfo(snap, id)
2414
+ // Picking an action is a saved choice, not an arming promise: the host
2415
+ // stores `custom` even when no script is uploaded yet, and refuses to
2416
+ // arm instead. So the two "set this up first" reasons stay selectable,
2417
+ // and the note under the picker says what is still missing. Anything
2418
+ // else is a hard no from the host and stays grayed out.
2419
+ var pending = info.reason === 'no-script'
2420
+ || info.reason === 'script-not-acknowledged'
2421
+ || info.reason === 'missing-interpreter'
2396
2422
  return h('option', {
2397
2423
  key: id, value: id,
2398
- // Disabled is the host's call: `available:false` always means the
2399
- // action cannot run, including a custom script we have no file for.
2400
- // The reason below the picker explains it in words.
2401
- disabled: info.available === false,
2424
+ disabled: info.available === false && !pending,
2402
2425
  }, tr('action.' + id))
2403
2426
  }))
2404
2427
  }
@@ -2884,14 +2907,8 @@
2884
2907
 
2885
2908
  // 3) 守夜行为:四行判定闸不再自成一组的成员,收进这里排在 AFK 前面。
2886
2909
  var watch = [settingsGroup('watch-g', tr('set.watch'), null, 'watch')]
2887
- watch.push(settingsRow('c3', tr('set.bottom'), tr('set.bottom.desc'),
2888
- h('span', { className: 'dsp-sval' }, tr('set.always')), 'dsp-srow-ro'))
2889
- watch.push(settingsRow('c2', tr('set.jobs'), tr('set.jobs.desc') + hint,
2890
- toggle(cur.blockOnJobs, function (next) { profileWrite('jobs', 'blockOnJobs', next) },
2891
- locked, tr('set.jobs')), null, errFor('jobs')))
2892
- watch.push(settingsRow('c1', tr('set.err'), tr('set.err.desc') + hint,
2893
- toggle(cur.onlyOnSuccess, function (next) { profileWrite('err', 'onlyOnSuccess', next) },
2894
- locked, tr('set.err')), null, errFor('err')))
2910
+ // The countdown comes first: it is the number most people open this pane
2911
+ // to change, and it reads better before what happens when it hits zero.
2895
2912
  watch.push(settingsRow('cd', tr('set.cd'),
2896
2913
  snap.countdownLocked ? tr('set.cd.locked') : tr('set.cd.desc'),
2897
2914
  snap.countdownLocked
@@ -2899,10 +2916,10 @@
2899
2916
  : seg([[30, tr('cd.30')], [60, tr('cd.60')], [120, tr('cd.120')], [300, tr('cd.300')]], cur.countdownSec, function (v) {
2900
2917
  profileWrite('cd', 'countdownSec', v)
2901
2918
  }), null, errFor('cd')))
2902
- // 3b) Action layer: the picker, then the script block. The script row is
2903
- // rendered for every action on purpose: uploading a file is the only
2904
- // way to make 'custom' available, so it must stay reachable while that
2905
- // option is still grayed out.
2919
+ // 3b) Action layer: the picker, then the script block. The script block
2920
+ // only exists while custom profiles are on: with the feature off the
2921
+ // action picker does not offer 'custom' at all, so there is nothing to
2922
+ // upload for.
2906
2923
  var actionId = cur.action && ACTION_IDS.indexOf(cur.action) >= 0 ? cur.action : 'shutdown'
2907
2924
  var scr = cur.script && cur.script.ext ? cur.script : null
2908
2925
  var scrAck = !!(scr && cur.scriptAck && cur.scriptAck.sha256 === scr.sha256)
@@ -2919,7 +2936,7 @@
2919
2936
  profileWrite('action', 'action', next)
2920
2937
  }),
2921
2938
  actionNote(snap, actionId))))
2922
- watch.push(h('div', { key: 'script', className: 'dsp-srow dsp-srow-stack' },
2939
+ if (customOn) watch.push(h('div', { key: 'script', className: 'dsp-srow dsp-srow-stack' },
2923
2940
  h('div', { className: 'dsp-srow-text' },
2924
2941
  h('span', { className: 'dsp-stitle' }, tr('set.script')),
2925
2942
  h('span', { className: 'dsp-sdesc' }, tr('set.script.desc')),
@@ -2953,7 +2970,7 @@
2953
2970
  if (ev.key === 'Enter') { ev.preventDefault(); commitScriptName(ev.target.value) }
2954
2971
  },
2955
2972
  })) : null)))
2956
- if (scr) {
2973
+ if (customOn && scr) {
2957
2974
  // The checkbox lives inside the warning block, not under it: the
2958
2975
  // disclaimer and the confirmation must be impossible to separate.
2959
2976
  watch.push(h('div', { key: 'ack', className: 'dsp-srow dsp-srow-stack dsp-ackrow' },
@@ -2973,6 +2990,20 @@
2973
2990
  !scrAck ? h('span', { className: 'dsp-sreason' },
2974
2991
  tr(resetHint ? 'set.script.ack.reset' : 'set.script.ack.needed')) : null)))
2975
2992
  }
2993
+ // "Sessions have stopped" leads the group of three: no running session
2994
+ // first, then background jobs, then this round's errors — the order the
2995
+ // gate itself evaluates them in.
2996
+ watch.push(settingsRow('c3', tr('set.bottom'), tr('set.bottom.desc'),
2997
+ h('span', { className: 'dsp-sval' }, tr('set.always')), 'dsp-srow-ro'))
2998
+ // The gate rows continue: "what runs" reads above, the conditions that
2999
+ // hold it back below. The halt switch stays directly under its parent
3000
+ // ("skip this round on errors") because it is its child.
3001
+ watch.push(settingsRow('c2', tr('set.jobs'), tr('set.jobs.desc') + hint,
3002
+ toggle(cur.blockOnJobs, function (next) { profileWrite('jobs', 'blockOnJobs', next) },
3003
+ locked, tr('set.jobs')), null, errFor('jobs')))
3004
+ watch.push(settingsRow('c1', tr('set.err'), tr('set.err.desc') + hint,
3005
+ toggle(cur.onlyOnSuccess, function (next) { profileWrite('err', 'onlyOnSuccess', next) },
3006
+ locked, tr('set.err')), null, errFor('err')))
2976
3007
  // "Shut down after a halt" is the child of "skip this round on errors":
2977
3008
  // the stored profile value is never rewritten, so turning the parent
2978
3009
  // back on restores what the user picked. /state already folded the
package/lib/index.mjs CHANGED
@@ -400,8 +400,14 @@ const ACTION_REASON = {
400
400
  noScript: 'no-script',
401
401
  missingInterpreter: 'missing-interpreter',
402
402
  scriptNotAcknowledged: 'script-not-acknowledged',
403
+ noCustomProfile: 'no-custom-profile',
403
404
  }
404
405
 
406
+ // Actions outside the plain system-power set are gated behind custom profiles:
407
+ // locking a machine or running a script is not something an unattended default
408
+ // profile should be able to do. Restart-dsh keeps its own permanent reason.
409
+ const CUSTOM_PROFILE_ONLY = ['lock', 'signout', 'custom']
410
+
405
411
  /** True for an absolute/relative path, which needs no PATH lookup. */
406
412
  function looksLikePath(value) {
407
413
  return typeof value === 'string' && value.length > 0
@@ -1979,6 +1985,10 @@ export function apply(ctx, rawConfig) {
1979
1985
  // The script's interpreter is part of the answer, so resolve it first.
1980
1986
  await resolveCurrentScript()
1981
1987
  const next = {}
1988
+ // Raw per-platform answers only. The "custom profile required" gate is applied
1989
+ // on the way out (`gatedActions`), not stored here: a stored gate would go
1990
+ // stale the moment the user enables custom profiles, and /state would keep
1991
+ // reporting `no-custom-profile` until the async probe finished.
1982
1992
  for (const id of ACTION_IDS) {
1983
1993
  // `custom` has no table row: it runs the profile's script, so "no file" and
1984
1994
  // "no interpreter" are different answers and both are worth saying.
@@ -2024,6 +2034,9 @@ export function apply(ctx, rawConfig) {
2024
2034
  /** The active action is armed only when the probe did not mark it unavailable. */
2025
2035
  function actionReadyNow() {
2026
2036
  const id = resolveAction()
2037
+ // Same gate as the probe, but read from the store: a stale map (or no probe
2038
+ // yet) must not let a default profile arm lock/signout/custom.
2039
+ if (store.customEnabled !== true && CUSTOM_PROFILE_ONLY.includes(id)) return false
2027
2040
  const entry = actionAvailability[id]
2028
2041
  if (id === 'custom') {
2029
2042
  // The script lives in the profile, so this answer must not lag behind the
@@ -2038,6 +2051,20 @@ export function apply(ctx, rawConfig) {
2038
2051
  return entry.available !== false
2039
2052
  }
2040
2053
 
2054
+ /**
2055
+ * The capability map as the settings page should see it. The probe stores the
2056
+ * raw per-platform answer; the custom-profile gate is applied on the way out so
2057
+ * a snapshot taken right after the toggle cannot show the old permission.
2058
+ */
2059
+ function gatedActions() {
2060
+ if (store.customEnabled === true) return actionAvailability
2061
+ const out = Object.assign({}, actionAvailability)
2062
+ for (const id of CUSTOM_PROFILE_ONLY) {
2063
+ out[id] = { available: false, reason: ACTION_REASON.noCustomProfile }
2064
+ }
2065
+ return out
2066
+ }
2067
+
2041
2068
  /** True when the user acknowledged exactly the file that is stored now. */
2042
2069
  function scriptAckedNow() {
2043
2070
  const script = cfg && cfg.script
@@ -2822,7 +2849,7 @@ export function apply(ctx, rawConfig) {
2822
2849
  afkAutoAnswer: cfg.afkAutoAnswer !== false,
2823
2850
  // Capability map, keyed by the same ids `profile.action` uses (hyphen
2824
2851
  // included). `{}` until the first probe settles; `recheck` refreshes it.
2825
- actions: actionAvailability,
2852
+ actions: gatedActions(),
2826
2853
  actionReady: actionReadyNow(),
2827
2854
  // The active profile's script, metadata only, plus whether the stored
2828
2855
  // acknowledgement still covers exactly these bytes.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-shutdown-guard",
3
- "version": "1.6.1-beta.1",
3
+ "version": "1.6.1-beta.2",
4
4
  "description": "任务结束后自动关机 —— 侧边栏守夜卡片:五道判定闸、后台任务守卫、AFK 接管、阻断态分级",
5
5
  "author": {
6
6
  "name": "ltayshark0637",
@@ -65,8 +65,5 @@
65
65
  "engines": {
66
66
  "node": ">=22.19"
67
67
  },
68
- "os": [
69
- "win32"
70
- ],
71
68
  "license": "MIT"
72
69
  }