clawkeep 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,276 @@
1
+ 'use strict';
2
+
3
+ const chalk = require('chalk');
4
+ const ora = require('ora');
5
+ const path = require('path');
6
+ const ClawGit = require('../core/git');
7
+ const BackupManager = require('../core/backup');
8
+
9
+ module.exports = async function backup(subcommand, args, opts) {
10
+ // Restore doesn't need an initialized repo — it creates a new one
11
+ if (subcommand === 'restore') {
12
+ return doRestore(args, opts);
13
+ }
14
+
15
+ const dir = path.resolve(opts.dir || '.');
16
+
17
+ const claw = new ClawGit(dir);
18
+ if (!(await claw.isInitialized())) {
19
+ console.error(chalk.red('Not initialized. Run `clawkeep init` first.'));
20
+ process.exit(1);
21
+ }
22
+
23
+ const bm = new BackupManager(claw);
24
+
25
+ if (!subcommand || subcommand === 'status') {
26
+ return showStatus(bm);
27
+ } else if (subcommand === 'set-target') {
28
+ return setTarget(bm, args, opts);
29
+ } else if (subcommand === 'set-password') {
30
+ return doSetPassword(bm, opts);
31
+ } else if (subcommand === 'sync') {
32
+ return doSync(bm, opts);
33
+ } else if (subcommand === 'pull') {
34
+ return doPull(bm);
35
+ } else if (subcommand === 'test') {
36
+ return doTest(bm);
37
+ } else if (subcommand === 'compact') {
38
+ return doCompact(bm, opts);
39
+ } else {
40
+ // Treat subcommand as target type shorthand: `clawkeep backup local /path`
41
+ return setTarget(bm, subcommand, opts);
42
+ }
43
+ };
44
+
45
+ function getPassword(opts) {
46
+ return opts.password || process.env.CLAWKEEP_PASSWORD || null;
47
+ }
48
+
49
+ async function showStatus(bm) {
50
+ const cfg = bm.getConfig();
51
+ console.log('');
52
+ console.log(chalk.bold(' Backup Status'));
53
+ console.log('');
54
+
55
+ if (!cfg.target) {
56
+ console.log(` ${chalk.yellow('\u25cf')} No backup target configured`);
57
+ console.log('');
58
+ console.log(chalk.dim(' Set one up:'));
59
+ console.log(chalk.dim(' $ clawkeep backup local /path/to/backup'));
60
+ console.log(chalk.dim(' $ clawkeep backup git <remote-url>'));
61
+ } else {
62
+ const icon = { local: '\ud83d\udcc1', cloud: '\ud83d\udc3e', s3: '\ud83e\udea3', git: '\ud83d\udd17' }[cfg.target] || '?';
63
+ console.log(` Target: ${icon} ${chalk.white(cfg.target)} \u2014 ${cfg.targetLabel}`);
64
+ console.log(` Auto-sync: ${cfg.autoSync ? chalk.green('on') : chalk.dim('off')}`);
65
+ if (cfg.lastSync) {
66
+ console.log(` Last sync: ${chalk.dim(cfg.lastSync)}`);
67
+ } else {
68
+ console.log(` Last sync: ${chalk.dim('never')}`);
69
+ }
70
+
71
+ // Encryption status
72
+ if (cfg.target === 'local') {
73
+ console.log(` Encrypted: ${cfg.passwordSet ? chalk.green('\u2713 yes') : chalk.yellow('\u26a0 password not set')}`);
74
+ if (cfg.chunkCount > 0) {
75
+ console.log(` Chunks: ${cfg.chunkCount}`);
76
+ }
77
+ if (cfg.workspaceId) {
78
+ console.log(` Workspace: ${chalk.dim(cfg.workspaceId)}`);
79
+ }
80
+ }
81
+ }
82
+ console.log('');
83
+ }
84
+
85
+ async function setTarget(bm, typeOrArgs, opts) {
86
+ let type, options = {};
87
+
88
+ if (typeof typeOrArgs === 'string') {
89
+ type = typeOrArgs;
90
+ } else {
91
+ type = typeOrArgs;
92
+ }
93
+
94
+ // Handle: `clawkeep backup local /path`
95
+ if (type === 'local') {
96
+ const targetPath = opts.path || opts.args?.[0];
97
+ if (!targetPath) {
98
+ console.error(chalk.red(' Usage: clawkeep backup local <path>'));
99
+ process.exit(1);
100
+ }
101
+ options.path = targetPath;
102
+ } else if (type === 'git') {
103
+ const url = opts.path || opts.args?.[0];
104
+ if (!url) {
105
+ console.error(chalk.red(' Usage: clawkeep backup git <remote-url>'));
106
+ process.exit(1);
107
+ }
108
+ options.url = url;
109
+ }
110
+
111
+ const spinner = ora('Setting up backup target...').start();
112
+ try {
113
+ const cfg = await bm.setTarget(type, options);
114
+ spinner.succeed('Backup target configured');
115
+ console.log(` Target: ${cfg.target} \u2014 ${cfg.targetLabel}`);
116
+ console.log('');
117
+
118
+ // Auto-test
119
+ const test = await bm.test();
120
+ if (test.ok) {
121
+ console.log(` ${chalk.green('\u2713')} ${test.message}${test.latencyMs ? ` (${test.latencyMs}ms)` : ''}`);
122
+ } else {
123
+ console.log(` ${chalk.yellow('\u26a0')} ${test.message}`);
124
+ }
125
+
126
+ // Remind about password for local targets
127
+ if (type === 'local' && !bm.hasPassword()) {
128
+ console.log('');
129
+ console.log(chalk.yellow(' \u26a0 Set a password before syncing:'));
130
+ console.log(chalk.dim(' $ clawkeep backup set-password'));
131
+ console.log(chalk.dim(' or: CLAWKEEP_PASSWORD=xxx clawkeep backup set-password'));
132
+ }
133
+ console.log('');
134
+ } catch (err) {
135
+ spinner.fail('Failed to set target');
136
+ console.error(chalk.red(' ' + err.message));
137
+ process.exit(1);
138
+ }
139
+ }
140
+
141
+ async function doSetPassword(bm, opts) {
142
+ const password = getPassword(opts);
143
+ if (!password) {
144
+ console.error(chalk.red(' Password required.'));
145
+ console.error(chalk.dim(' Use: CLAWKEEP_PASSWORD=xxx clawkeep backup set-password'));
146
+ console.error(chalk.dim(' Or: clawkeep backup set-password -p <password>'));
147
+ process.exit(1);
148
+ }
149
+
150
+ const spinner = ora('Setting encryption password...').start();
151
+ try {
152
+ bm.setPassword(password);
153
+ spinner.succeed('Encryption password set');
154
+ console.log(chalk.dim(' Password hash stored (password itself is never saved)'));
155
+ } catch (err) {
156
+ spinner.fail('Failed to set password');
157
+ console.error(chalk.red(' ' + err.message));
158
+ process.exit(1);
159
+ }
160
+ }
161
+
162
+ async function doSync(bm, opts) {
163
+ const cfg = bm.getConfig();
164
+ const password = cfg.target === 'local' ? getPassword(opts) : null;
165
+
166
+ if (cfg.target === 'local' && !password) {
167
+ console.error(chalk.red(' Password required for encrypted sync.'));
168
+ console.error(chalk.dim(' Use: CLAWKEEP_PASSWORD=xxx clawkeep backup sync'));
169
+ process.exit(1);
170
+ }
171
+
172
+ const spinner = ora('Syncing to backup target...').start();
173
+ try {
174
+ const result = await bm.sync(password);
175
+ if (result.synced === false) {
176
+ spinner.succeed(result.message || 'Already up to date');
177
+ } else {
178
+ spinner.succeed('Synced to backup target');
179
+ if (result.chunkCount) {
180
+ console.log(chalk.dim(` Chunks: ${result.chunkCount} \u2022 Size: ${fmtSize(result.totalSize || 0)}`));
181
+ }
182
+ }
183
+ if (result.lastSync) {
184
+ console.log(chalk.dim(` Last sync: ${result.lastSync}`));
185
+ }
186
+ } catch (err) {
187
+ spinner.fail('Sync failed');
188
+ console.error(chalk.red(' ' + err.message));
189
+ process.exit(1);
190
+ }
191
+ }
192
+
193
+ async function doPull(bm) {
194
+ const spinner = ora('Pulling from backup target...').start();
195
+ try {
196
+ await bm.pull();
197
+ spinner.succeed('Pulled from backup target');
198
+ } catch (err) {
199
+ spinner.fail('Pull failed');
200
+ console.error(chalk.red(' ' + err.message));
201
+ process.exit(1);
202
+ }
203
+ }
204
+
205
+ async function doTest(bm) {
206
+ const spinner = ora('Testing connection...').start();
207
+ try {
208
+ const result = await bm.test();
209
+ if (result.ok) {
210
+ spinner.succeed(`${result.message}${result.latencyMs ? ` (${result.latencyMs}ms)` : ''}`);
211
+ } else {
212
+ spinner.fail(result.message);
213
+ }
214
+ } catch (err) {
215
+ spinner.fail(err.message);
216
+ process.exit(1);
217
+ }
218
+ }
219
+
220
+ async function doCompact(bm, opts) {
221
+ const password = getPassword(opts);
222
+ if (!password) {
223
+ console.error(chalk.red(' Password required for compact.'));
224
+ console.error(chalk.dim(' Use: CLAWKEEP_PASSWORD=xxx clawkeep backup compact'));
225
+ process.exit(1);
226
+ }
227
+
228
+ const spinner = ora('Compacting backup chunks...').start();
229
+ try {
230
+ const result = await bm.compact(password);
231
+ if (result.compacted === false) {
232
+ spinner.succeed(result.message || 'Nothing to compact');
233
+ } else {
234
+ spinner.succeed(`Compacted ${result.oldChunks} chunks into 1`);
235
+ console.log(chalk.dim(` New size: ${fmtSize(result.newSize || 0)}`));
236
+ }
237
+ } catch (err) {
238
+ spinner.fail('Compact failed');
239
+ console.error(chalk.red(' ' + err.message));
240
+ process.exit(1);
241
+ }
242
+ }
243
+
244
+ async function doRestore(args, opts) {
245
+ const sourcePath = opts.path || (args && args[0]);
246
+ if (!sourcePath) {
247
+ console.error(chalk.red(' Usage: clawkeep backup restore <backup-path> -d <dest>'));
248
+ process.exit(1);
249
+ }
250
+
251
+ const password = getPassword(opts);
252
+ if (!password) {
253
+ console.error(chalk.red(' Password required for restore.'));
254
+ console.error(chalk.dim(' Use: CLAWKEEP_PASSWORD=xxx clawkeep backup restore <path>'));
255
+ process.exit(1);
256
+ }
257
+
258
+ const destDir = path.resolve(opts.dir || '.');
259
+ const spinner = ora('Restoring from encrypted backup...').start();
260
+ try {
261
+ const result = await BackupManager.restoreFromBackup(sourcePath, destDir, password);
262
+ spinner.succeed('Restored from backup');
263
+ console.log(chalk.dim(` Chunks: ${result.chunks} \u2022 Commits: ${result.totalCommits}`));
264
+ console.log(chalk.dim(` Restored to: ${destDir}`));
265
+ } catch (err) {
266
+ spinner.fail('Restore failed');
267
+ console.error(chalk.red(' ' + err.message));
268
+ process.exit(1);
269
+ }
270
+ }
271
+
272
+ function fmtSize(b) {
273
+ if (b < 1024) return b + ' B';
274
+ if (b < 1048576) return (b / 1024).toFixed(1) + ' KB';
275
+ return (b / 1048576).toFixed(1) + ' MB';
276
+ }
@@ -0,0 +1,44 @@
1
+ 'use strict';
2
+
3
+ const chalk = require('chalk');
4
+ const path = require('path');
5
+ const ClawGit = require('../core/git');
6
+
7
+ module.exports = async function diff(opts) {
8
+ const dir = path.resolve(opts.dir || '.');
9
+
10
+ try {
11
+ const claw = new ClawGit(dir);
12
+
13
+ if (!(await claw.isInitialized())) {
14
+ console.error(chalk.red('ClawKeep not initialized. Run `clawkeep init` first.'));
15
+ process.exit(1);
16
+ }
17
+
18
+ const result = await claw.diff(opts.stat);
19
+
20
+ if (!result || result.trim() === '') {
21
+ console.log(chalk.dim('No changes since last backup.'));
22
+ return;
23
+ }
24
+
25
+ // Colorize diff output
26
+ const lines = result.split('\n');
27
+ for (const line of lines) {
28
+ if (line.startsWith('+') && !line.startsWith('+++')) {
29
+ console.log(chalk.green(line));
30
+ } else if (line.startsWith('-') && !line.startsWith('---')) {
31
+ console.log(chalk.red(line));
32
+ } else if (line.startsWith('@@')) {
33
+ console.log(chalk.cyan(line));
34
+ } else if (line.startsWith('diff')) {
35
+ console.log(chalk.bold(line));
36
+ } else {
37
+ console.log(line);
38
+ }
39
+ }
40
+ } catch (err) {
41
+ console.error(chalk.red(err.message));
42
+ process.exit(1);
43
+ }
44
+ };
@@ -0,0 +1,55 @@
1
+ 'use strict';
2
+
3
+ const chalk = require('chalk');
4
+ const ora = require('ora');
5
+ const path = require('path');
6
+ const ClawGit = require('../core/git');
7
+ const { exportEncrypted } = require('../core/crypto');
8
+
9
+ module.exports = async function exportCmd(opts) {
10
+ const dir = path.resolve(opts.dir || '.');
11
+ const spinner = ora('Exporting encrypted archive...').start();
12
+
13
+ try {
14
+ const claw = new ClawGit(dir);
15
+
16
+ if (!(await claw.isInitialized())) {
17
+ spinner.fail('ClawKeep not initialized. Run `clawkeep init` first.');
18
+ process.exit(1);
19
+ }
20
+
21
+ // Get password
22
+ const password = opts.password || process.env.CLAWKEEP_PASSWORD;
23
+ if (!password) {
24
+ spinner.fail('Password required. Use -p <password> or set CLAWKEEP_PASSWORD');
25
+ process.exit(1);
26
+ }
27
+
28
+ // Default output path
29
+ const config = claw.loadConfig();
30
+ const agentName = config.agentName || 'agent';
31
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);
32
+ const outputPath = opts.output || path.join(process.cwd(), `${agentName}-${timestamp}.clawkeep.enc`);
33
+
34
+ // Take a snap first to capture latest state
35
+ spinner.text = 'Capturing latest state...';
36
+ await claw.snap('export: pre-export backup');
37
+
38
+ // Export
39
+ spinner.text = 'Encrypting archive...';
40
+ const result = await exportEncrypted(dir, outputPath, password);
41
+
42
+ const sizeMB = (result.size / 1024 / 1024).toFixed(2);
43
+ spinner.succeed('Encrypted archive exported!');
44
+ console.log('');
45
+ console.log(` 📦 ${chalk.cyan(result.path)}`);
46
+ console.log(` Size: ${chalk.yellow(sizeMB + ' MB')}`);
47
+ console.log(` Encryption: ${chalk.green('AES-256-CTR + scrypt')}`);
48
+ console.log('');
49
+ console.log(chalk.yellow(' ⚠️ Don\'t lose the password. We can\'t recover it.'));
50
+ } catch (err) {
51
+ spinner.fail('Export failed');
52
+ console.error(chalk.red(err.message));
53
+ process.exit(1);
54
+ }
55
+ };
@@ -0,0 +1,39 @@
1
+ 'use strict';
2
+
3
+ const chalk = require('chalk');
4
+ const ora = require('ora');
5
+ const path = require('path');
6
+ const { importEncrypted } = require('../core/crypto');
7
+
8
+ module.exports = async function importCmd(file, opts) {
9
+ const targetDir = path.resolve(opts.dir || '.');
10
+ const spinner = ora('Importing from encrypted archive...').start();
11
+
12
+ try {
13
+ const archivePath = path.resolve(file);
14
+
15
+ // Get password
16
+ const password = opts.password || process.env.CLAWKEEP_PASSWORD;
17
+ if (!password) {
18
+ spinner.fail('Password required. Use -p <password> or set CLAWKEEP_PASSWORD');
19
+ process.exit(1);
20
+ }
21
+
22
+ spinner.text = 'Decrypting and extracting...';
23
+ const result = await importEncrypted(archivePath, targetDir, password);
24
+
25
+ spinner.succeed('Import complete!');
26
+ console.log('');
27
+ console.log(` 📂 Restored to: ${chalk.cyan(result.path)}`);
28
+ console.log(chalk.dim(' Your full history and memory are restored.'));
29
+ console.log(chalk.dim(' Run `clawkeep log` to see backup history.'));
30
+ } catch (err) {
31
+ spinner.fail('Import failed');
32
+ if (err.message.includes('incorrect header check') || err.message.includes('bad decrypt')) {
33
+ console.error(chalk.red(' Wrong password or corrupted archive.'));
34
+ } else {
35
+ console.error(chalk.red(err.message));
36
+ }
37
+ process.exit(1);
38
+ }
39
+ };
@@ -0,0 +1,46 @@
1
+ 'use strict';
2
+
3
+ const chalk = require('chalk');
4
+ const ora = require('ora');
5
+ const path = require('path');
6
+ const ClawGit = require('../core/git');
7
+
8
+ module.exports = async function init(opts) {
9
+ const dir = path.resolve(opts.dir || '.');
10
+ const spinner = ora('Initializing ClawKeep...').start();
11
+
12
+ try {
13
+ const claw = new ClawGit(dir);
14
+
15
+ if (await claw.isInitialized()) {
16
+ spinner.warn('ClawKeep is already initialized here.');
17
+ console.log(chalk.dim(' Run `clawkeep status` for details.'));
18
+ return;
19
+ }
20
+
21
+ const config = await claw.init();
22
+
23
+ // Take initial backup
24
+ spinner.text = 'Taking initial backup...';
25
+ const result = await claw.snap('initial backup');
26
+
27
+ spinner.succeed(chalk.bold('ClawKeep initialized!'));
28
+ console.log('');
29
+ console.log(chalk.bold.cyan(' 🐾 Directory is now backed up'));
30
+ console.log('');
31
+ if (result) {
32
+ console.log(` ${chalk.dim('Tracked')} ${chalk.white(result.summary.changed + ' files')}`);
33
+ console.log(` ${chalk.dim('Backup')} ${chalk.yellow(result.hash.substring(0, 8))}`);
34
+ }
35
+ console.log('');
36
+ console.log(chalk.dim(' Next steps:'));
37
+ console.log(chalk.dim(' $ clawkeep watch Auto-backup on file changes'));
38
+ console.log(chalk.dim(' $ clawkeep backup local Set up backup target'));
39
+ console.log(chalk.dim(' $ clawkeep snap -m "..." Manual backup'));
40
+ console.log('');
41
+ } catch (err) {
42
+ spinner.fail('Failed to initialize');
43
+ console.error(chalk.red(' ' + err.message));
44
+ process.exit(1);
45
+ }
46
+ };
@@ -0,0 +1,77 @@
1
+ 'use strict';
2
+
3
+ const chalk = require('chalk');
4
+ const path = require('path');
5
+ const ClawGit = require('../core/git');
6
+
7
+ module.exports = async function log(opts) {
8
+ const dir = path.resolve(opts.dir || '.');
9
+
10
+ try {
11
+ const claw = new ClawGit(dir);
12
+
13
+ if (!(await claw.isInitialized())) {
14
+ console.error(chalk.red('Not initialized. Run `clawkeep init` first.'));
15
+ process.exit(1);
16
+ }
17
+
18
+ const entries = await claw.log(parseInt(opts.limit) || 20);
19
+
20
+ if (entries.length === 0) {
21
+ console.log(chalk.dim('No backups yet.'));
22
+ return;
23
+ }
24
+
25
+ // JSON output
26
+ if (opts.json) {
27
+ console.log(JSON.stringify(entries, null, 2));
28
+ return;
29
+ }
30
+
31
+ console.log('');
32
+ console.log(chalk.bold(` 📋 ${entries.length} backup${entries.length > 1 ? 's' : ''}`));
33
+ console.log('');
34
+
35
+ for (let i = 0; i < entries.length; i++) {
36
+ const entry = entries[i];
37
+ const hash = chalk.yellow(entry.hash.substring(0, 8));
38
+ const date = _formatDate(entry.date);
39
+ const msg = entry.message;
40
+ const isFirst = i === 0;
41
+
42
+ if (opts.oneline) {
43
+ console.log(` ${hash} ${msg} ${chalk.dim(date)}`);
44
+ } else {
45
+ const marker = isFirst ? chalk.green('●') : chalk.dim('○');
46
+ console.log(` ${marker} ${hash} — ${msg}`);
47
+ console.log(` ${chalk.dim('│')} ${chalk.dim(date)}`);
48
+ if (i < entries.length - 1) {
49
+ console.log(` ${chalk.dim('│')}`);
50
+ }
51
+ }
52
+ }
53
+ console.log('');
54
+ } catch (err) {
55
+ console.error(chalk.red(err.message));
56
+ process.exit(1);
57
+ }
58
+ };
59
+
60
+ function _formatDate(dateStr) {
61
+ try {
62
+ const d = new Date(dateStr);
63
+ const now = new Date();
64
+ const diffMs = now - d;
65
+ const diffMins = Math.floor(diffMs / 60000);
66
+ const diffHours = Math.floor(diffMs / 3600000);
67
+ const diffDays = Math.floor(diffMs / 86400000);
68
+
69
+ if (diffMins < 1) return 'just now';
70
+ if (diffMins < 60) return `${diffMins}m ago`;
71
+ if (diffHours < 24) return `${diffHours}h ago`;
72
+ if (diffDays < 7) return `${diffDays}d ago`;
73
+ return d.toISOString().substring(0, 16).replace('T', ' ');
74
+ } catch {
75
+ return dateStr;
76
+ }
77
+ }
@@ -0,0 +1,34 @@
1
+ 'use strict';
2
+
3
+ const chalk = require('chalk');
4
+ const ora = require('ora');
5
+ const path = require('path');
6
+ const ClawGit = require('../core/git');
7
+
8
+ module.exports = async function pull(opts) {
9
+ const dir = path.resolve(opts.dir || '.');
10
+ const spinner = ora('Pulling from remote...').start();
11
+
12
+ try {
13
+ const claw = new ClawGit(dir);
14
+
15
+ if (!(await claw.isInitialized())) {
16
+ spinner.fail('ClawKeep not initialized. Run `clawkeep init` first.');
17
+ process.exit(1);
18
+ }
19
+
20
+ const config = claw.loadConfig();
21
+ if (!config.remote) {
22
+ spinner.fail('No remote configured. Use `clawkeep push -r <url>` first.');
23
+ process.exit(1);
24
+ }
25
+
26
+ await claw.pull();
27
+
28
+ spinner.succeed('Pulled from remote!');
29
+ } catch (err) {
30
+ spinner.fail('Pull failed');
31
+ console.error(chalk.red(err.message));
32
+ process.exit(1);
33
+ }
34
+ };
@@ -0,0 +1,45 @@
1
+ 'use strict';
2
+
3
+ const chalk = require('chalk');
4
+ const ora = require('ora');
5
+ const path = require('path');
6
+ const ClawGit = require('../core/git');
7
+
8
+ module.exports = async function push(opts) {
9
+ const dir = path.resolve(opts.dir || '.');
10
+ const spinner = ora('Pushing to remote...').start();
11
+
12
+ try {
13
+ const claw = new ClawGit(dir);
14
+
15
+ if (!(await claw.isInitialized())) {
16
+ spinner.fail('ClawKeep not initialized. Run `clawkeep init` first.');
17
+ process.exit(1);
18
+ }
19
+
20
+ // Set remote if provided
21
+ if (opts.remote) {
22
+ spinner.text = 'Setting remote...';
23
+ await claw.setRemote(opts.remote);
24
+ }
25
+
26
+ const config = claw.loadConfig();
27
+ if (!config.remote && !opts.remote) {
28
+ spinner.fail('No remote configured. Use `clawkeep push -r <url>`');
29
+ process.exit(1);
30
+ }
31
+
32
+ spinner.text = 'Pushing backups...';
33
+ await claw.push();
34
+
35
+ spinner.succeed('Pushed to remote!');
36
+ console.log(chalk.dim(` Remote: ${config.remote || opts.remote}`));
37
+ } catch (err) {
38
+ spinner.fail('Push failed');
39
+ console.error(chalk.red(err.message));
40
+ if (err.message.includes('Authentication')) {
41
+ console.log(chalk.dim(' Hint: Make sure your remote URL includes credentials or SSH key is set up.'));
42
+ }
43
+ process.exit(1);
44
+ }
45
+ };
@@ -0,0 +1,42 @@
1
+ 'use strict';
2
+
3
+ const chalk = require('chalk');
4
+ const ora = require('ora');
5
+ const path = require('path');
6
+ const ClawGit = require('../core/git');
7
+
8
+ module.exports = async function restore(ref, opts) {
9
+ const dir = path.resolve(opts.dir || '.');
10
+ const spinner = ora('Restoring...').start();
11
+
12
+ try {
13
+ const claw = new ClawGit(dir);
14
+
15
+ if (!(await claw.isInitialized())) {
16
+ spinner.fail('ClawKeep not initialized. Run `clawkeep init` first.');
17
+ process.exit(1);
18
+ }
19
+
20
+ if (!ref) {
21
+ spinner.fail('Please specify a backup reference (hash or HEAD~N).');
22
+ console.log(chalk.dim(' Example: clawkeep restore abc123'));
23
+ console.log(chalk.dim(' Example: clawkeep restore HEAD~3'));
24
+ console.log(chalk.dim(' Run `clawkeep log` to see available backups.'));
25
+ process.exit(1);
26
+ }
27
+
28
+ await claw.restore(ref, opts.hard);
29
+
30
+ if (opts.hard) {
31
+ spinner.succeed(`Hard restore to ${chalk.cyan(ref.substring(0, 8))}`);
32
+ console.log(chalk.yellow(' ⚠️ Current changes were discarded.'));
33
+ } else {
34
+ spinner.succeed(`Restored to ${chalk.cyan(ref.substring(0, 8))} (new backup created)`);
35
+ console.log(chalk.dim(' Your history is preserved. The restore is a new backup.'));
36
+ }
37
+ } catch (err) {
38
+ spinner.fail('Restore failed');
39
+ console.error(chalk.red(err.message));
40
+ process.exit(1);
41
+ }
42
+ };