@goodandready/dsh-time-machine 0.1.2 → 0.1.4
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/README.md +113 -14
- package/README.ru.md +115 -0
- package/README.zh.md +73 -0
- package/lib/client.js +104 -38
- package/lib/index.js +74 -13
- package/lib/snapshot.js +28 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,20 +1,119 @@
|
|
|
1
|
-
# dsh-time-machine
|
|
1
|
+
# 📦 @goodandready/dsh-time-machine
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<div align="center">
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
- `time_machine_checkpoint_create`
|
|
7
|
-
- `time_machine_checkpoint_list`
|
|
8
|
-
- `time_machine_checkpoint_rollback`
|
|
9
|
-
- `time_machine_diff`
|
|
5
|
+
<h3>Automated Shadow Git Snapshots, Workspace Time-Travel & Instant Rollback for DeepSeek Harness</h3>
|
|
10
6
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
-
|
|
15
|
-
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://www.npmjs.com/package/@goodandready/dsh-time-machine"><img src="https://img.shields.io/npm/v/@goodandready/dsh-time-machine.svg?style=for-the-badge&color=6366f1&labelColor=1e1b4b" alt="npm version"></a>
|
|
9
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-10b981.svg?style=for-the-badge&color=10b981&labelColor=064e3b" alt="license"></a>
|
|
10
|
+
<a href="https://github.com/topics/dsh-plugin"><img src="https://img.shields.io/badge/DSH-Plugin-8b5cf6.svg?style=for-the-badge&labelColor=2e1065" alt="DSH Plugin"></a>
|
|
11
|
+
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/Node-20%2B-f59e0b.svg?style=for-the-badge&labelColor=451a03" alt="Node version"></a>
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
<!-- Showcase Catalog Button -->
|
|
15
|
+
<p align="center">
|
|
16
|
+
<a href="https://goodandready.app/"><img src="https://img.shields.io/badge/All_Author_Projects-goodandready.app-ff4500.svg?style=for-the-badge&logo=rocket&logoColor=white&labelColor=1a1a2e" alt="All Author Projects"></a>
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
<p align="center">
|
|
20
|
+
<a href="README.md"><b>🇬🇧 English</b></a> •
|
|
21
|
+
<a href="README.ru.md"><b>🇷🇺 Русский</b></a> •
|
|
22
|
+
<a href="README.zh.md"><b>🇨🇳 中文说明</b></a>
|
|
23
|
+
</p>
|
|
24
|
+
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## ⚡ Overview
|
|
30
|
+
|
|
31
|
+
**`dsh-time-machine`** provides an automated safety net and instantaneous rollback engine for **DeepSeek Harness** workspaces.
|
|
32
|
+
|
|
33
|
+
Autonomous agents frequently execute complex multi-file refactorings, run mutating shell commands, or install dependencies. When an unexpected regression or broken state occurs, manual git reversion can be messy and risk losing untracked files or user git history.
|
|
34
|
+
|
|
35
|
+
`dsh-time-machine` creates lightweight **shadow git snapshots** in the background without modifying user git commits or branches, providing **1-click interactive rollback, visual file diffs, and automatic recovery prompts on command failure**.
|
|
36
|
+
|
|
37
|
+
```mermaid
|
|
38
|
+
graph LR
|
|
39
|
+
subgraph AgentAction [DSH Agent Mutating Operations]
|
|
40
|
+
Agent[🤖 Agent: Edits Files / Runs Commands] --> Trigger{Pre-Action Hook}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
subgraph TimeMachine [dsh-time-machine Core Engine]
|
|
44
|
+
Trigger --> ShadowGit[Shadow Git Snapshot Engine]
|
|
45
|
+
ShadowGit --> Snapshots[(In-Memory Checkpoint Timeline)]
|
|
46
|
+
Snapshots --> DiffEngine[Visual Workspace Diff Calculator]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
subgraph SafetyNet [Restoration & Web UI Studio]
|
|
50
|
+
DiffEngine --> Sidebar[🕒 Time Machine Sidebar Tab]
|
|
51
|
+
Snapshots --> Rollback[⏪ 1-Click Instant Workspace Rollback]
|
|
52
|
+
Rollback --> CleanState[Restored Pristine Working Tree]
|
|
53
|
+
Trigger -.->|Command Error| AutoHeal[🩹 Auto-Heal Rollback Prompt]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
style AgentAction fill:#1e1e2e,stroke:#89b4fa,stroke-width:2px,color:#cdd6f4
|
|
57
|
+
style TimeMachine fill:#181825,stroke:#cba6f7,stroke-width:2px,color:#cdd6f4
|
|
58
|
+
style SafetyNet fill:#11111b,stroke:#a6e3a1,stroke-width:2px,color:#cdd6f4
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## ✨ Key Capabilities & Studio Features
|
|
64
|
+
|
|
65
|
+
### 1. 🛡️ Non-Intrusive Shadow Git Snapshots (`lib/snapshot.js`)
|
|
66
|
+
* Captures complete working tree state, staged files, and untracked assets using shadow git refs;
|
|
67
|
+
* Zero pollution of user git commit history, tags, or active branches;
|
|
68
|
+
* Retains a rolling history of the last $N$ checkpoints with human-readable labels and timestamps.
|
|
69
|
+
|
|
70
|
+
### 2. ⏪ Instant Safe Rollback (`time_machine_checkpoint_rollback`)
|
|
71
|
+
* Restores the entire workspace or specific files to any previous checkpoint in milliseconds;
|
|
72
|
+
* Can be triggered programmatically by the agent or interactively by the user in the UI.
|
|
73
|
+
|
|
74
|
+
### 3. 🔍 Visual Snapshot Diff Inspector (`time_machine_diff`)
|
|
75
|
+
* Computes file-by-file visual diffs comparing current workspace state against any checkpoint;
|
|
76
|
+
* Highlights added, deleted, and modified lines with clean line numbers.
|
|
77
|
+
|
|
78
|
+
### 4. 🕒 Interactive Sidebar Timeline (`lib/client.js`)
|
|
79
|
+
* Seamlessly integrates into DSH Web UI sidebar;
|
|
80
|
+
* Displays a chronological timeline of all session checkpoints with file change counters and 1-click "Restore Checkpoint" and "Inspect Diff" buttons.
|
|
81
|
+
|
|
82
|
+
### 5. 🩹 Auto-Heal on Command Failure
|
|
83
|
+
* Automatically prompts the user/agent with a 1-click restore proposal when a destructive command exits with an error.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## 🛠️ Agent Tools Reference (4 Tools)
|
|
88
|
+
|
|
89
|
+
| Tool Name | Parameters | Description |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| `time_machine_checkpoint_create` | `label?: string` | Creates a shadow git workspace checkpoint before risky edits or operations |
|
|
92
|
+
| `time_machine_checkpoint_list` | *(none)* | Lists all recent workspace checkpoints newest first |
|
|
93
|
+
| `time_machine_checkpoint_rollback` | `id: string` | Reverts the entire workspace back to the specified checkpoint state |
|
|
94
|
+
| `time_machine_diff` | `id?: string` | Returns unified file diff between current workspace and target checkpoint |
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## 📦 Quick Installation
|
|
16
99
|
|
|
17
|
-
## Verification
|
|
18
100
|
```bash
|
|
19
|
-
|
|
101
|
+
dsh plugin --profile web add @goodandready/dsh-time-machine
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## ⚙️ Configuration Reference (`settings.yaml`)
|
|
107
|
+
|
|
108
|
+
```yaml
|
|
109
|
+
dsh-time-machine:
|
|
110
|
+
autoSnapshotEnabled: true # Automatically take checkpoints before file modifications
|
|
111
|
+
maxSnapshots: 20 # Maximum rolling checkpoints retained in memory
|
|
112
|
+
autoHealPrompt: true # Prompt for rollback when a command fails
|
|
20
113
|
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## 📄 License
|
|
118
|
+
|
|
119
|
+
MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
package/README.ru.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# 📦 @goodandready/dsh-time-machine
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+
<h3>Автоматические теневые снапшоты, путешествие во времени по кодовой базе и мгновенный откат изменений для DeepSeek Harness</h3>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://www.npmjs.com/package/@goodandready/dsh-time-machine"><img src="https://img.shields.io/npm/v/@goodandready/dsh-time-machine.svg?style=for-the-badge&color=6366f1&labelColor=1e1b4b" alt="npm version"></a>
|
|
9
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-10b981.svg?style=for-the-badge&color=10b981&labelColor=064e3b" alt="license"></a>
|
|
10
|
+
<a href="https://github.com/topics/dsh-plugin"><img src="https://img.shields.io/badge/DSH-Plugin-8b5cf6.svg?style=for-the-badge&labelColor=2e1065" alt="DSH Plugin"></a>
|
|
11
|
+
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/Node-20%2B-f59e0b.svg?style=for-the-badge&labelColor=451a03" alt="Node version"></a>
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
<!-- Обязательная кнопка перехода на витрину всех проектов -->
|
|
15
|
+
<p align="center">
|
|
16
|
+
<a href="https://goodandready.app/"><img src="https://img.shields.io/badge/Все_проекты_автора-goodandready.app-ff4500.svg?style=for-the-badge&logo=rocket&logoColor=white&labelColor=1a1a2e" alt="Все проекты автора"></a>
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
<p align="center">
|
|
20
|
+
<a href="README.md"><b>🇬🇧 English</b></a> •
|
|
21
|
+
<a href="README.ru.md"><b>🇷🇺 Русский</b></a> •
|
|
22
|
+
<a href="README.zh.md"><b>🇨🇳 中文说明</b></a>
|
|
23
|
+
</p>
|
|
24
|
+
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## ⚡ Обзор
|
|
30
|
+
|
|
31
|
+
**`dsh-time-machine`** предоставляет автоматическую систему безопасности и мгновенного отката для рабочего пространства агентов **DeepSeek Harness**.
|
|
32
|
+
|
|
33
|
+
Автономные агенты часто выполняют сложные рефакторинги множества файлов, выполняют терминальные команды или устанавливают пакеты. В случае ошибок или поломки кода ручной откат через git может быть затруднён и грозит потерей неотслеживаемых файлов или порчей истории коммитов.
|
|
34
|
+
|
|
35
|
+
`dsh-time-machine` создаёт легковесные **теневые снапшоты (shadow git snapshots)** в фоновом режиме без изменения пользовательских веток и коммитов, обеспечивая **откат в 1 клик, визуальный просмотр Diff и предложение авто-восстановления при сбоях**.
|
|
36
|
+
|
|
37
|
+
```mermaid
|
|
38
|
+
graph LR
|
|
39
|
+
subgraph AgentAction [Действия агента DSH]
|
|
40
|
+
Agent[🤖 Агент: Правка файлов / Запуск команд] --> Trigger{Хук перед действием}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
subgraph TimeMachine [Ядро dsh-time-machine]
|
|
44
|
+
Trigger --> ShadowGit[Движок теневых Git-снапшотов]
|
|
45
|
+
ShadowGit --> Snapshots[(Хронологическая лента чекпоинтов)]
|
|
46
|
+
Snapshots --> DiffEngine[Калькулятор визуальных Diff]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
subgraph SafetyNet [Безопасность и Web UI]
|
|
50
|
+
DiffEngine --> Sidebar[🕒 Вкладка Time Machine в сайдбаре]
|
|
51
|
+
Snapshots --> Rollback[⏪ Мгновенный откат рабочего каталога]
|
|
52
|
+
Rollback --> CleanState[Восстановленное чистое состояние]
|
|
53
|
+
Trigger -.->|Ошибка команды| AutoHeal[🩹 Предложение авто-отката]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
style AgentAction fill:#1e1e2e,stroke:#89b4fa,stroke-width:2px,color:#cdd6f4
|
|
57
|
+
style TimeMachine fill:#181825,stroke:#cba6f7,stroke-width:2px,color:#cdd6f4
|
|
58
|
+
style SafetyNet fill:#11111b,stroke:#a6e3a1,stroke-width:2px,color:#cdd6f4
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## ✨ Ключевые возможности
|
|
64
|
+
|
|
65
|
+
### 1. 🛡️ Теневые снапшоты без засорения Git (`lib/snapshot.js`)
|
|
66
|
+
* Фиксирует всё рабочее дерево, индексированные и новые файлы через изолированные shadow-ссылки Git;
|
|
67
|
+
* Не создаёт лишних коммитов в пользовательской истории и не переключает активные ветки;
|
|
68
|
+
* Хранит скользящую историю последних $N$ чекпоинтов с понятными метками и метками времени.
|
|
69
|
+
|
|
70
|
+
### 2. ⏪ Мгновенный безопасный откат (`time_machine_checkpoint_rollback`)
|
|
71
|
+
* Возвращает всё рабочее пространство к любому предыдущему состоянию за миллисекунды;
|
|
72
|
+
* Может вызываться как программно агентом, так и пользователем через кнопку в интерфейсе.
|
|
73
|
+
|
|
74
|
+
### 3. 🔍 Визуальный инспектор Diff (`time_machine_diff`)
|
|
75
|
+
* Сравнивает текущие файлы со снапшотом и формирует наглядный пофайловый Diff.
|
|
76
|
+
|
|
77
|
+
### 4. 🕒 Интерактивная хроника в сайдбаре (`lib/client.js`)
|
|
78
|
+
* Встраивается в боковую панель Web UI DSH;
|
|
79
|
+
* Показывает список чекпоинтов со счётчиками изменённых файлов и кнопками «Откатить» и «Сравнить Diff».
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## 🛠️ Инструменты агента (4 инструмента)
|
|
84
|
+
|
|
85
|
+
| Имя инструмента | Параметры | Описание |
|
|
86
|
+
|---|---|---|
|
|
87
|
+
| `time_machine_checkpoint_create` | `label?: string` | Создаёт теневой чекпоинт перед рискованными правками |
|
|
88
|
+
| `time_machine_checkpoint_list` | *(нет)* | Возвращает список недавних снапшотов от новых к старым |
|
|
89
|
+
| `time_machine_checkpoint_rollback` | `id: string` | Откатывает файлы рабочего пространства к указанному снапшоту |
|
|
90
|
+
| `time_machine_diff` | `id?: string` | Возвращает пофайловый Diff между текущим состоянием и чекпоинтом |
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## 📦 Быстрая установка
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
dsh plugin --profile web add @goodandready/dsh-time-machine
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## ⚙️ Пример конфигурации (`settings.yaml`)
|
|
103
|
+
|
|
104
|
+
```yaml
|
|
105
|
+
dsh-time-machine:
|
|
106
|
+
autoSnapshotEnabled: true # Создавать чекпоинты перед изменением файлов
|
|
107
|
+
maxSnapshots: 20 # Максимальное количество чекпоинтов в памяти
|
|
108
|
+
autoHealPrompt: true # Предлагать откат при падении терминальной команды
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## 📄 Лицензия
|
|
114
|
+
|
|
115
|
+
MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# 📦 @goodandready/dsh-time-machine
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+
<h3>DeepSeek Harness 自动化影子 Git 快照、工作区时光机与一键即时回滚插件</h3>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://www.npmjs.com/package/@goodandready/dsh-time-machine"><img src="https://img.shields.io/npm/v/@goodandready/dsh-time-machine.svg?style=for-the-badge&color=6366f1&labelColor=1e1b4b" alt="npm version"></a>
|
|
9
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-10b981.svg?style=for-the-badge&color=10b981&labelColor=064e3b" alt="license"></a>
|
|
10
|
+
<a href="https://github.com/topics/dsh-plugin"><img src="https://img.shields.io/badge/DSH-Plugin-8b5cf6.svg?style=for-the-badge&labelColor=2e1065" alt="DSH Plugin"></a>
|
|
11
|
+
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/Node-20%2B-f59e0b.svg?style=for-the-badge&labelColor=451a03" alt="Node version"></a>
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
<!-- 官方展示中心跳转按钮 -->
|
|
15
|
+
<p align="center">
|
|
16
|
+
<a href="https://goodandready.app/"><img src="https://img.shields.io/badge/作者全部项目-goodandready.app-ff4500.svg?style=for-the-badge&logo=rocket&logoColor=white&labelColor=1a1a2e" alt="作者全部项目"></a>
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
<p align="center">
|
|
20
|
+
<a href="README.md"><b>🇬🇧 English</b></a> •
|
|
21
|
+
<a href="README.ru.md"><b>🇷🇺 Русский</b></a> •
|
|
22
|
+
<a href="README.zh.md"><b>🇨🇳 中文说明</b></a>
|
|
23
|
+
</p>
|
|
24
|
+
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## ⚡ 插件概览
|
|
30
|
+
|
|
31
|
+
**`dsh-time-machine`** 为 **DeepSeek Harness** 智能体提供全自动工作区安全护栏与即时状态回滚引擎。
|
|
32
|
+
|
|
33
|
+
智能体在执行多文件批量重构、依赖安装或高危终端命令时,一旦发生代码损坏或逻辑回归,手动 Git 回退不仅繁琐,还容易丢失未跟踪的新建文件。
|
|
34
|
+
|
|
35
|
+
本插件在后台利用**影子 Git 快照技术(Shadow Git Snapshots)**自动捕获工作区状态,不污染用户 Git 提交树与分支,支持**一键即时回退、可视化文件 Diff 对比以及命令失败时的主动自愈挽救**。
|
|
36
|
+
|
|
37
|
+
```mermaid
|
|
38
|
+
graph LR
|
|
39
|
+
subgraph AgentAction [智能体执行操作]
|
|
40
|
+
Agent[🤖 智能体: 批量修改文件 / 执行终端命令] --> Trigger{执行前置拦截}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
subgraph TimeMachine [dsh-time-machine 引擎核心]
|
|
44
|
+
Trigger --> ShadowGit[影子 Git 快照引擎]
|
|
45
|
+
ShadowGit --> Snapshots[(时序检查点历史队列)]
|
|
46
|
+
Snapshots --> DiffEngine[可视化工作区 Diff 计算器]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
subgraph SafetyNet [安全防护与界面集成]
|
|
50
|
+
DiffEngine --> Sidebar[🕒 侧边栏 Time Machine 时间轴]
|
|
51
|
+
Snapshots --> Rollback[⏪ 一键毫秒级即时回滚]
|
|
52
|
+
Rollback --> CleanState[恢复纯净安全状态]
|
|
53
|
+
Trigger -.->|命令报错| AutoHeal[🩹 自动弹出回滚修复建议]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
style AgentAction fill:#1e1e2e,stroke:#89b4fa,stroke-width:2px,color:#cdd6f4
|
|
57
|
+
style TimeMachine fill:#181825,stroke:#cba6f7,stroke-width:2px,color:#cdd6f4
|
|
58
|
+
style SafetyNet fill:#11111b,stroke:#a6e3a1,stroke-width:2px,color:#cdd6f4
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## 📦 安装指南
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
dsh plugin --profile web add @goodandready/dsh-time-machine
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 📄 开源协议
|
|
72
|
+
|
|
73
|
+
MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
package/lib/client.js
CHANGED
|
@@ -104,7 +104,24 @@ window.__ModuleLoader__.load({
|
|
|
104
104
|
return lang;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
function
|
|
107
|
+
function sessionIdOf(ctx) {
|
|
108
|
+
try {
|
|
109
|
+
if (!ctx) return '';
|
|
110
|
+
if (ctx.scope) {
|
|
111
|
+
if (ctx.scope.session) return String(ctx.scope.session.id || ctx.scope.session.header?.id || '');
|
|
112
|
+
if (typeof ctx.scope.id === 'string') return ctx.scope.id;
|
|
113
|
+
}
|
|
114
|
+
if (ctx.session) return String(ctx.session.id || ctx.session.header?.id || '');
|
|
115
|
+
if (ctx.get && typeof ctx.get === 'function') {
|
|
116
|
+
const s = ctx.get('session');
|
|
117
|
+
if (s) return String(s.id || s.header?.id || '');
|
|
118
|
+
}
|
|
119
|
+
} catch {}
|
|
120
|
+
return '';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function Timeline({ t, ctx }) {
|
|
124
|
+
const sid = sessionIdOf(ctx);
|
|
108
125
|
const [snapshots, setSnapshots] = React.useState([]);
|
|
109
126
|
const [loading, setLoading] = React.useState(false);
|
|
110
127
|
const [label, setLabel] = React.useState('');
|
|
@@ -114,17 +131,18 @@ window.__ModuleLoader__.load({
|
|
|
114
131
|
|
|
115
132
|
const fetchList = React.useCallback(() => {
|
|
116
133
|
setLoading(true); setErr('');
|
|
117
|
-
|
|
134
|
+
const q = sid ? '?sessionId=' + encodeURIComponent(sid) : '';
|
|
135
|
+
fetch('/dsh-time-machine/snapshots' + q).then(r => r.json()).then(j => {
|
|
118
136
|
if (j.success) setSnapshots(j.snapshots || []);
|
|
119
137
|
else setErr(j.error || 'load failed');
|
|
120
138
|
}).catch(e => setErr(String(e.message || e))).finally(() => setLoading(false));
|
|
121
|
-
}, []);
|
|
139
|
+
}, [sid]);
|
|
122
140
|
|
|
123
141
|
React.useEffect(() => { fetchList(); }, [fetchList]);
|
|
124
142
|
|
|
125
143
|
const onCreate = () => {
|
|
126
144
|
setBusy(true); setErr('');
|
|
127
|
-
fetch('/dsh-time-machine/create', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ label }) })
|
|
145
|
+
fetch('/dsh-time-machine/create', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ label, sessionId: sid }) })
|
|
128
146
|
.then(r => r.json()).then(j => {
|
|
129
147
|
if (!j.success) throw new Error(j.error || 'create failed');
|
|
130
148
|
setLabel(''); fetchList();
|
|
@@ -179,6 +197,7 @@ window.__ModuleLoader__.load({
|
|
|
179
197
|
|
|
180
198
|
function PluginCard(props) {
|
|
181
199
|
const ctx = props.ctx;
|
|
200
|
+
const sid = sessionIdOf(ctx);
|
|
182
201
|
const [expanded, setExpanded] = React.useState(false);
|
|
183
202
|
const lang = useLocale(ctx);
|
|
184
203
|
const t = lang === 'ru' ? ru : en;
|
|
@@ -225,17 +244,18 @@ window.__ModuleLoader__.load({
|
|
|
225
244
|
|
|
226
245
|
const fetchList = React.useCallback(() => {
|
|
227
246
|
setLoading(true); setErr('');
|
|
228
|
-
|
|
247
|
+
const q = sid ? '?sessionId=' + encodeURIComponent(sid) : '';
|
|
248
|
+
fetch('/dsh-time-machine/snapshots' + q).then(r => r.json()).then(j => {
|
|
229
249
|
if (j.success) setSnapshots(j.snapshots || []);
|
|
230
250
|
else setErr(j.error || 'load failed');
|
|
231
251
|
}).catch(e => setErr(String(e.message || e))).finally(() => setLoading(false));
|
|
232
|
-
}, []);
|
|
252
|
+
}, [sid]);
|
|
233
253
|
|
|
234
254
|
React.useEffect(() => { if (expanded) fetchList(); }, [expanded, fetchList]);
|
|
235
255
|
|
|
236
256
|
const onCreate = () => {
|
|
237
257
|
setBusy(true); setErr('');
|
|
238
|
-
fetch('/dsh-time-machine/create', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ label }) })
|
|
258
|
+
fetch('/dsh-time-machine/create', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ label, sessionId: sid }) })
|
|
239
259
|
.then(r => r.json()).then(j => {
|
|
240
260
|
if (!j.success) throw new Error(j.error || 'create failed');
|
|
241
261
|
setLabel(''); fetchList();
|
|
@@ -342,46 +362,92 @@ window.__ModuleLoader__.load({
|
|
|
342
362
|
return React.createElement('div', { className: 'tm-tab' },
|
|
343
363
|
React.createElement('div', { style: { fontSize: 13, fontWeight: 600, color: 'var(--dsw-alias-label-primary)' } }, t.tabTitle),
|
|
344
364
|
React.createElement('div', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginBottom: 4 } }, t.sub),
|
|
345
|
-
React.createElement(Timeline, { t })
|
|
365
|
+
React.createElement(Timeline, { t, ctx })
|
|
346
366
|
);
|
|
347
367
|
}
|
|
348
368
|
|
|
349
|
-
function registerBetterSidebar(ctx) {
|
|
350
|
-
const svc = (ctx.get && (()=>{ try{return ctx.get('betterSidebar')}catch{return null}})()) || ctx.betterSidebar;
|
|
351
|
-
if (!svc || typeof svc.registerTab !== 'function') return;
|
|
352
|
-
try {
|
|
353
|
-
svc.registerTab({
|
|
354
|
-
id: 'time-machine',
|
|
355
|
-
title: () => {
|
|
356
|
-
const a = ctx.locale && ctx.locale.getSnapshot && ctx.locale.getSnapshot().active || 'en';
|
|
357
|
-
return a.startsWith('ru') ? 'Машина времени' : 'Time Machine';
|
|
358
|
-
},
|
|
359
|
-
order: 30,
|
|
360
|
-
single: true,
|
|
361
|
-
icon: (size) => React.createElement('svg', { width: size||16, height: size||16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: '2', strokeLinecap: 'round', strokeLinejoin: 'round' },
|
|
362
|
-
React.createElement('circle', { cx:12, cy:12, r:10 }),
|
|
363
|
-
React.createElement('polyline', { points:'12 6 12 12 16 14' })
|
|
364
|
-
),
|
|
365
|
-
component: (p) => React.createElement(TimeMachineTab, { ctx, ...p })
|
|
366
|
-
});
|
|
367
|
-
} catch (e) { console.warn('[dsh-time-machine] betterSidebar tab failed', e); }
|
|
368
|
-
}
|
|
369
|
-
|
|
370
369
|
module.exports.inject = ['slots', 'locale'];
|
|
371
370
|
module.exports.apply = function apply(ctx) {
|
|
372
371
|
ensureStyles();
|
|
373
372
|
try { ctx.locale && ctx.locale.register && ctx.locale.register(NS, { en, ru }); } catch {}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
373
|
+
// declaration-safe settings registration (alpha2 SlotCore requires inject)
|
|
374
|
+
let settingsRegistered = false;
|
|
375
|
+
const doRegisterSettings = () => {
|
|
376
|
+
if (settingsRegistered) return;
|
|
377
|
+
try {
|
|
378
|
+
ctx.slots.register({ name: 'settings.plugin.item', key: NS, locale: NS, inject: () => ({ ctx }) }, PluginCard);
|
|
379
|
+
settingsRegistered = true;
|
|
380
|
+
} catch (e) {
|
|
381
|
+
const msg = String(e && e.message || e);
|
|
382
|
+
if (msg.includes('not declared') || msg.includes('is not declared')) {
|
|
383
|
+
// fallback to section if plugin.item not declared in this host build
|
|
384
|
+
try {
|
|
385
|
+
const t = (ctx.locale && ctx.locale.bind) ? ctx.locale.bind(NS) : (k) => k;
|
|
386
|
+
ctx.slots.register({ name: 'settings.section', id: NS, order: 30, label: () => t('title'), inject: () => ({ ctx }) }, PluginCard);
|
|
387
|
+
settingsRegistered = true;
|
|
388
|
+
} catch (e2) {
|
|
389
|
+
console.error('[dsh-time-machine] settings registration failed', e2);
|
|
390
|
+
}
|
|
391
|
+
} else {
|
|
392
|
+
console.error('[dsh-time-machine] settings.plugin.item registration failed', e);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
// wait for host declaration; if inject not available, try direct (older DSH)
|
|
397
|
+
if (ctx.slots && typeof ctx.slots.inject === 'function') {
|
|
398
|
+
try {
|
|
399
|
+
ctx.slots.inject('settings.plugin.item', () => {
|
|
400
|
+
doRegisterSettings();
|
|
401
|
+
});
|
|
402
|
+
// also handle delayed BetterSidebar via inject
|
|
403
|
+
} catch (e) {
|
|
404
|
+
console.error('[dsh-time-machine] slots.inject failed', e);
|
|
405
|
+
doRegisterSettings();
|
|
406
|
+
}
|
|
407
|
+
} else {
|
|
408
|
+
doRegisterSettings();
|
|
409
|
+
}
|
|
410
|
+
// BetterSidebar: single declaration-aware path via ctx.inject
|
|
411
|
+
let tabRegistered = false;
|
|
412
|
+
const doRegisterTab = (bctx) => {
|
|
413
|
+
if (tabRegistered) return;
|
|
414
|
+
const svc = (bctx && bctx.betterSidebar) || (bctx && bctx.get && (()=>{ try{return bctx.get('betterSidebar')}catch{return null}})()) || ctx.betterSidebar;
|
|
415
|
+
if (!svc || typeof svc.registerTab !== 'function') return;
|
|
378
416
|
try {
|
|
379
|
-
|
|
380
|
-
|
|
417
|
+
svc.registerTab({
|
|
418
|
+
id: 'time-machine',
|
|
419
|
+
title: () => {
|
|
420
|
+
const a = ctx.locale && ctx.locale.getSnapshot && ctx.locale.getSnapshot().active || 'en';
|
|
421
|
+
return a.startsWith('ru') ? 'Машина времени' : 'Time Machine';
|
|
422
|
+
},
|
|
423
|
+
order: 30,
|
|
424
|
+
single: true,
|
|
425
|
+
icon: (size) => React.createElement('svg', { width: size||16, height: size||16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: '2', strokeLinecap: 'round', strokeLinejoin: 'round' },
|
|
426
|
+
React.createElement('circle', { cx:12, cy:12, r:10 }),
|
|
427
|
+
React.createElement('polyline', { points:'12 6 12 12 16 14' })
|
|
428
|
+
),
|
|
429
|
+
component: (p) => React.createElement(TimeMachineTab, { ctx, ...p })
|
|
430
|
+
});
|
|
431
|
+
tabRegistered = true;
|
|
432
|
+
} catch (e) {
|
|
433
|
+
console.error('[dsh-time-machine] betterSidebar registerTab failed', e);
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
if (ctx.inject) {
|
|
437
|
+
try {
|
|
438
|
+
ctx.inject(['betterSidebar'], (bctx) => {
|
|
439
|
+
doRegisterTab(bctx);
|
|
440
|
+
return () => {};
|
|
441
|
+
});
|
|
442
|
+
} catch (e) {
|
|
443
|
+
// betterSidebar is optional, do not hide loader failure
|
|
444
|
+
if (String(e && e.message || '').includes('not declared')) {
|
|
445
|
+
// service not declared in this host build - silently skip tab
|
|
446
|
+
} else {
|
|
447
|
+
console.error('[dsh-time-machine] betterSidebar inject failed', e);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
381
450
|
}
|
|
382
|
-
// betterSidebar tab — immediate + inject fallback
|
|
383
|
-
registerBetterSidebar(ctx);
|
|
384
|
-
try { ctx.inject(['betterSidebar'], () => { registerBetterSidebar(ctx); }); } catch {}
|
|
385
451
|
};
|
|
386
452
|
return module.exports;
|
|
387
453
|
},
|
package/lib/index.js
CHANGED
|
@@ -12,6 +12,20 @@ export const Config = Schema.object({
|
|
|
12
12
|
|
|
13
13
|
const NS = '@goodandready/dsh-time-machine';
|
|
14
14
|
|
|
15
|
+
function sessionIdOf(execution, ctx) {
|
|
16
|
+
try {
|
|
17
|
+
if (execution && execution.sessionId) return String(execution.sessionId);
|
|
18
|
+
if (execution && execution.agent && execution.agent.session) {
|
|
19
|
+
return String(execution.agent.session.id || execution.agent.session.header?.id || '');
|
|
20
|
+
}
|
|
21
|
+
if (ctx && ctx.session) return String(ctx.session.id || ctx.session.header?.id || '');
|
|
22
|
+
if (ctx && ctx.get && typeof ctx.get === 'function') {
|
|
23
|
+
try { const s = ctx.get('session'); if (s) return String(s.id || s.header?.id || ''); } catch {}
|
|
24
|
+
}
|
|
25
|
+
} catch {}
|
|
26
|
+
return '';
|
|
27
|
+
}
|
|
28
|
+
|
|
15
29
|
export function apply(ctx, config) {
|
|
16
30
|
let getConfig = () => config;
|
|
17
31
|
let engine = new ShadowSnapshotEngine({ maxSnapshots: config?.maxSnapshots ?? 20 });
|
|
@@ -25,30 +39,74 @@ export function apply(ctx, config) {
|
|
|
25
39
|
sctx.effect(() => () => { try { stop(); } catch {} }, 'dsh-time-machine: settings watch');
|
|
26
40
|
});
|
|
27
41
|
|
|
28
|
-
//
|
|
42
|
+
// auto-snapshot on turn/start and approval/asked (session-scoped)
|
|
43
|
+
const autoSnap = async (label, sessId) => {
|
|
44
|
+
if (!getConfig().autoSnapshotEnabled) return;
|
|
45
|
+
try {
|
|
46
|
+
const sid = String(sessId || '').trim();
|
|
47
|
+
await engine.createSnapshot(label, { sessionId: sid });
|
|
48
|
+
} catch {}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// listen to DSH events if available
|
|
52
|
+
try {
|
|
53
|
+
if (ctx.events && typeof ctx.events.on === 'function') {
|
|
54
|
+
ctx.effect(() => {
|
|
55
|
+
const offs = [];
|
|
56
|
+
try {
|
|
57
|
+
offs.push(ctx.events.on('turn/start', (ev) => {
|
|
58
|
+
const sid = sessionIdOf(ev, ctx);
|
|
59
|
+
autoSnap(`auto:turn:${ev?.turnId || Date.now()}`, sid);
|
|
60
|
+
}));
|
|
61
|
+
} catch {}
|
|
62
|
+
try {
|
|
63
|
+
offs.push(ctx.events.on('approval/asked', (ev) => {
|
|
64
|
+
const sid = sessionIdOf(ev, ctx);
|
|
65
|
+
const tool = ev?.tool || ev?.name || 'approval';
|
|
66
|
+
autoSnap(`auto:approval:${tool}`, sid);
|
|
67
|
+
}));
|
|
68
|
+
} catch {}
|
|
69
|
+
return () => { for (const off of offs) try { typeof off === 'function' && off(); } catch {} };
|
|
70
|
+
}, 'dsh-time-machine: auto snapshot events');
|
|
71
|
+
}
|
|
72
|
+
} catch {}
|
|
73
|
+
|
|
74
|
+
// tools (session-aware)
|
|
29
75
|
const registerTools = (tctx) => {
|
|
30
76
|
tctx.tools.register({
|
|
31
77
|
name: 'time_machine_checkpoint_create',
|
|
32
|
-
description: 'Create a workspace checkpoint (shadow git snapshot) before risky changes. Returns id and label.',
|
|
78
|
+
description: 'Create a workspace checkpoint (shadow git snapshot) before risky changes. Returns id and label. Auto-creates per session if autoSnapshotEnabled.',
|
|
33
79
|
parameters: {
|
|
34
80
|
type: 'object',
|
|
35
81
|
properties: {
|
|
36
82
|
label: { type: 'string', description: 'Human label for checkpoint' },
|
|
83
|
+
sessionId: { type: 'string', description: 'Session id to scope checkpoint (auto-detected if omitted)' },
|
|
37
84
|
},
|
|
38
85
|
},
|
|
39
|
-
execute: async (
|
|
40
|
-
const
|
|
86
|
+
execute: async (args = {}, execution) => {
|
|
87
|
+
const sid = String(args.sessionId || sessionIdOf(execution, tctx) || sessionIdOf(args, tctx) || '').trim();
|
|
88
|
+
const snap = await engine.createSnapshot(args.label, { sessionId: sid });
|
|
41
89
|
return { success: true, snapshot: snap };
|
|
42
90
|
},
|
|
43
91
|
});
|
|
44
92
|
|
|
45
93
|
tctx.tools.register({
|
|
46
94
|
name: 'time_machine_checkpoint_list',
|
|
47
|
-
description: 'List recent workspace checkpoints newest first.',
|
|
48
|
-
parameters: {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
95
|
+
description: 'List recent workspace checkpoints newest first. Filter by sessionId if given.',
|
|
96
|
+
parameters: {
|
|
97
|
+
type: 'object',
|
|
98
|
+
properties: {
|
|
99
|
+
sessionId: { type: 'string', description: 'Filter by session id' },
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
execute: async (args = {}, execution) => {
|
|
103
|
+
const sid = args.sessionId != null ? String(args.sessionId).trim() : (sessionIdOf(execution, tctx) ? String(sessionIdOf(execution, tctx)).trim() : undefined);
|
|
104
|
+
const list = sid !== undefined ? engine.listSnapshots(sid) : engine.listSnapshots();
|
|
105
|
+
// if sid undefined and we want all, return all; but if sessionId was auto-detected, filter
|
|
106
|
+
// For backward compat, if no sid provided and engine has sessionIds, return all
|
|
107
|
+
// If sid was auto-detected (execution has session), use it
|
|
108
|
+
const finalList = (args.sessionId == null && sid !== undefined && sid !== '') ? engine.listSnapshots(sid) : (args.sessionId != null ? engine.listSnapshots(String(args.sessionId)) : engine.listSnapshots());
|
|
109
|
+
return { success: true, snapshots: finalList, sessionId: sid || '' };
|
|
52
110
|
},
|
|
53
111
|
});
|
|
54
112
|
|
|
@@ -92,7 +150,7 @@ export function apply(ctx, config) {
|
|
|
92
150
|
else ctx.inject(['tools'], (tctx) => registerTools(tctx));
|
|
93
151
|
} catch {}
|
|
94
152
|
|
|
95
|
-
// web routes for UI
|
|
153
|
+
// web routes for UI (session-aware via ?sessionId=)
|
|
96
154
|
ctx.effect(() => {
|
|
97
155
|
const disposals = [];
|
|
98
156
|
try {
|
|
@@ -100,8 +158,11 @@ export function apply(ctx, config) {
|
|
|
100
158
|
kind: 'exact',
|
|
101
159
|
path: '/dsh-time-machine/snapshots',
|
|
102
160
|
handler: (req, res) => {
|
|
161
|
+
const url = new URL(req.url, 'http://localhost');
|
|
162
|
+
const sid = url.searchParams.get('sessionId');
|
|
163
|
+
const list = sid != null ? engine.listSnapshots(String(sid)) : engine.listSnapshots();
|
|
103
164
|
res.setHeader('content-type', 'application/json');
|
|
104
|
-
res.end(JSON.stringify({ success: true, snapshots:
|
|
165
|
+
res.end(JSON.stringify({ success: true, snapshots: list }));
|
|
105
166
|
},
|
|
106
167
|
}));
|
|
107
168
|
disposals.push(ctx.webServer.register({
|
|
@@ -131,7 +192,8 @@ export function apply(ctx, config) {
|
|
|
131
192
|
req.on('end', async () => {
|
|
132
193
|
try {
|
|
133
194
|
const parsed = body ? JSON.parse(body) : {};
|
|
134
|
-
const
|
|
195
|
+
const sid = String(parsed.sessionId || new URL(req.url, 'http://localhost').searchParams.get('sessionId') || '').trim();
|
|
196
|
+
const snap = await engine.createSnapshot(parsed.label, { sessionId: sid });
|
|
135
197
|
res.setHeader('content-type', 'application/json');
|
|
136
198
|
res.end(JSON.stringify({ success: true, snapshot: snap }));
|
|
137
199
|
} catch (e) {
|
|
@@ -166,6 +228,5 @@ export function apply(ctx, config) {
|
|
|
166
228
|
return () => { for (const d of disposals) try { typeof d === 'function' && d(); } catch {} };
|
|
167
229
|
}, 'dsh-time-machine: web routes');
|
|
168
230
|
|
|
169
|
-
// expose engine for tests
|
|
170
231
|
ctx.provide?.('timeMachineEngine', engine);
|
|
171
232
|
}
|
package/lib/snapshot.js
CHANGED
|
@@ -16,19 +16,28 @@ async function defaultExec(cmd, args, opts = {}) {
|
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
// ponytail: in-memory engine, git shadow refs if repo present; O(n) scan,
|
|
19
|
+
// ponytail: in-memory engine, git shadow refs if repo present; O(n) scan, per-session trim
|
|
20
20
|
export class ShadowSnapshotEngine {
|
|
21
21
|
constructor({ exec = defaultExec, maxSnapshots = 20, cwd } = {}) {
|
|
22
22
|
this.exec = exec;
|
|
23
23
|
this.maxSnapshots = maxSnapshots;
|
|
24
24
|
this.cwd = cwd;
|
|
25
|
-
this.snapshots = []; //
|
|
25
|
+
this.snapshots = []; // {id, label, sessionId, createdAt, commit, ref}
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
_now() { return Date.now(); }
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
_trimFor(sessionId) {
|
|
31
|
+
if (!sessionId) {
|
|
32
|
+
while (this.snapshots.length > this.maxSnapshots) this.snapshots.shift();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const perSession = this.snapshots.filter(s => s.sessionId === sessionId);
|
|
36
|
+
while (perSession.length > this.maxSnapshots) {
|
|
37
|
+
const oldest = perSession.shift();
|
|
38
|
+
const idx = this.snapshots.indexOf(oldest);
|
|
39
|
+
if (idx !== -1) this.snapshots.splice(idx, 1);
|
|
40
|
+
}
|
|
32
41
|
}
|
|
33
42
|
|
|
34
43
|
async _isGitRepo() {
|
|
@@ -38,36 +47,38 @@ export class ShadowSnapshotEngine {
|
|
|
38
47
|
} catch { return false; }
|
|
39
48
|
}
|
|
40
49
|
|
|
41
|
-
async createSnapshot(label = '') {
|
|
50
|
+
async createSnapshot(label = '', { sessionId = '' } = {}) {
|
|
42
51
|
const id = crypto.randomUUID().slice(0, 8);
|
|
43
52
|
const createdAt = this._now();
|
|
44
53
|
const safeLabel = String(label ?? '').trim() || `checkpoint-${id}`;
|
|
54
|
+
const sid = String(sessionId || '').trim();
|
|
45
55
|
let commit = null;
|
|
46
56
|
let ref = null;
|
|
47
57
|
const inGit = await this._isGitRepo();
|
|
48
58
|
if (inGit) {
|
|
49
59
|
try {
|
|
50
|
-
// stage working tree without committing to main branch
|
|
51
60
|
await this.exec('git', ['add', '-A'], this.cwd ? { cwd: this.cwd } : {});
|
|
52
61
|
const { stdout: tree } = await this.exec('git', ['write-tree'], this.cwd ? { cwd: this.cwd } : {});
|
|
53
62
|
const treeHash = String(tree).trim();
|
|
54
63
|
const { stdout: commitHash } = await this.exec('git', ['commit-tree', treeHash, '-m', safeLabel], this.cwd ? { cwd: this.cwd } : {});
|
|
55
64
|
commit = String(commitHash).trim();
|
|
56
|
-
ref = `refs/dsh-time-machine/${id}`;
|
|
65
|
+
ref = sid ? `refs/dsh-time-machine/${sid}/${id}` : `refs/dsh-time-machine/${id}`;
|
|
57
66
|
await this.exec('git', ['update-ref', ref, commit], this.cwd ? { cwd: this.cwd } : {});
|
|
58
67
|
} catch {
|
|
59
68
|
commit = null;
|
|
60
69
|
ref = null;
|
|
61
70
|
}
|
|
62
71
|
}
|
|
63
|
-
const snap = { id, label: safeLabel, createdAt, commit, ref };
|
|
72
|
+
const snap = { id, label: safeLabel, sessionId: sid, createdAt, commit, ref };
|
|
64
73
|
this.snapshots.push(snap);
|
|
65
|
-
this.
|
|
74
|
+
this._trimFor(sid);
|
|
66
75
|
return snap;
|
|
67
76
|
}
|
|
68
77
|
|
|
69
|
-
listSnapshots() {
|
|
70
|
-
|
|
78
|
+
listSnapshots(sessionId) {
|
|
79
|
+
const sid = sessionId != null ? String(sessionId).trim() : undefined;
|
|
80
|
+
const list = sid !== undefined ? this.snapshots.filter(s => String(s.sessionId) === sid) : [...this.snapshots];
|
|
81
|
+
return [...list].sort((a, b) => b.createdAt - a.createdAt);
|
|
71
82
|
}
|
|
72
83
|
|
|
73
84
|
getSnapshot(id) {
|
|
@@ -91,7 +102,6 @@ export class ShadowSnapshotEngine {
|
|
|
91
102
|
try {
|
|
92
103
|
await this.exec('git', ['reset', '--hard', snap.commit], this.cwd ? { cwd: this.cwd } : {});
|
|
93
104
|
} catch (e) {
|
|
94
|
-
// fallback to checkout of commit tree
|
|
95
105
|
await this.exec('git', ['read-tree', snap.commit], this.cwd ? { cwd: this.cwd } : {}).catch(()=>{});
|
|
96
106
|
await this.exec('git', ['checkout-index', '-a', '-f'], this.cwd ? { cwd: this.cwd } : {}).catch(()=>{});
|
|
97
107
|
}
|
|
@@ -107,7 +117,6 @@ export class ShadowSnapshotEngine {
|
|
|
107
117
|
throw err;
|
|
108
118
|
}
|
|
109
119
|
let to = toId ? this.getSnapshot(toId) : null;
|
|
110
|
-
// if to not given, diff against current HEAD/working tree
|
|
111
120
|
const inGit = await this._isGitRepo();
|
|
112
121
|
if (inGit && from.commit) {
|
|
113
122
|
try {
|
|
@@ -116,7 +125,6 @@ export class ShadowSnapshotEngine {
|
|
|
116
125
|
const diffStat = String(stdout ?? '').trim();
|
|
117
126
|
if (diffStat) return { from: fromId, to: toId || 'HEAD', diff: diffStat };
|
|
118
127
|
} catch {}
|
|
119
|
-
// fallback to name-only
|
|
120
128
|
try {
|
|
121
129
|
const range = to?.commit ? `${from.commit}..${to.commit}` : from.commit;
|
|
122
130
|
const { stdout } = await this.exec('git', ['diff', '--name-only', range], this.cwd ? { cwd: this.cwd } : {});
|
|
@@ -129,6 +137,11 @@ export class ShadowSnapshotEngine {
|
|
|
129
137
|
|
|
130
138
|
setMax(n) {
|
|
131
139
|
this.maxSnapshots = Math.max(1, Number(n) || 20);
|
|
132
|
-
|
|
140
|
+
// trim all sessions
|
|
141
|
+
const sessions = [...new Set(this.snapshots.map(s => s.sessionId))];
|
|
142
|
+
for (const sid of sessions) this._trimFor(sid);
|
|
143
|
+
if (sessions.length === 0) {
|
|
144
|
+
while (this.snapshots.length > this.maxSnapshots) this.snapshots.shift();
|
|
145
|
+
}
|
|
133
146
|
}
|
|
134
147
|
}
|