@buoy-gg/time-machine 7.0.17

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.
Files changed (60) hide show
  1. package/LICENSE +58 -0
  2. package/README.md +55 -0
  3. package/lib/commonjs/components/TimeMachineBody.js +420 -0
  4. package/lib/commonjs/components/TimeMachineBody.js.map +1 -0
  5. package/lib/commonjs/components/TimeMachineModal.js +74 -0
  6. package/lib/commonjs/components/TimeMachineModal.js.map +1 -0
  7. package/lib/commonjs/hooks/useTimeMachine.js +24 -0
  8. package/lib/commonjs/hooks/useTimeMachine.js.map +1 -0
  9. package/lib/commonjs/index.js +64 -0
  10. package/lib/commonjs/index.js.map +1 -0
  11. package/lib/commonjs/package.json +1 -0
  12. package/lib/commonjs/preset.js +45 -0
  13. package/lib/commonjs/preset.js.map +1 -0
  14. package/lib/commonjs/store/snapshotVault.js +83 -0
  15. package/lib/commonjs/store/snapshotVault.js.map +1 -0
  16. package/lib/commonjs/store/timeMachineStore.js +335 -0
  17. package/lib/commonjs/store/timeMachineStore.js.map +1 -0
  18. package/lib/commonjs/sync/timeMachineSyncAdapter.js +139 -0
  19. package/lib/commonjs/sync/timeMachineSyncAdapter.js.map +1 -0
  20. package/lib/module/components/TimeMachineBody.js +415 -0
  21. package/lib/module/components/TimeMachineBody.js.map +1 -0
  22. package/lib/module/components/TimeMachineModal.js +68 -0
  23. package/lib/module/components/TimeMachineModal.js.map +1 -0
  24. package/lib/module/hooks/useTimeMachine.js +19 -0
  25. package/lib/module/hooks/useTimeMachine.js.map +1 -0
  26. package/lib/module/index.js +29 -0
  27. package/lib/module/index.js.map +1 -0
  28. package/lib/module/preset.js +40 -0
  29. package/lib/module/preset.js.map +1 -0
  30. package/lib/module/store/snapshotVault.js +74 -0
  31. package/lib/module/store/snapshotVault.js.map +1 -0
  32. package/lib/module/store/timeMachineStore.js +327 -0
  33. package/lib/module/store/timeMachineStore.js.map +1 -0
  34. package/lib/module/sync/timeMachineSyncAdapter.js +134 -0
  35. package/lib/module/sync/timeMachineSyncAdapter.js.map +1 -0
  36. package/lib/typescript/components/TimeMachineBody.d.ts +2 -0
  37. package/lib/typescript/components/TimeMachineBody.d.ts.map +1 -0
  38. package/lib/typescript/components/TimeMachineModal.d.ts +12 -0
  39. package/lib/typescript/components/TimeMachineModal.d.ts.map +1 -0
  40. package/lib/typescript/hooks/useTimeMachine.d.ts +6 -0
  41. package/lib/typescript/hooks/useTimeMachine.d.ts.map +1 -0
  42. package/lib/typescript/index.d.ts +18 -0
  43. package/lib/typescript/index.d.ts.map +1 -0
  44. package/lib/typescript/preset.d.ts +38 -0
  45. package/lib/typescript/preset.d.ts.map +1 -0
  46. package/lib/typescript/store/snapshotVault.d.ts +46 -0
  47. package/lib/typescript/store/snapshotVault.d.ts.map +1 -0
  48. package/lib/typescript/store/timeMachineStore.d.ts +97 -0
  49. package/lib/typescript/store/timeMachineStore.d.ts.map +1 -0
  50. package/lib/typescript/sync/timeMachineSyncAdapter.d.ts +59 -0
  51. package/lib/typescript/sync/timeMachineSyncAdapter.d.ts.map +1 -0
  52. package/package.json +59 -0
  53. package/src/components/TimeMachineBody.tsx +455 -0
  54. package/src/components/TimeMachineModal.tsx +75 -0
  55. package/src/hooks/useTimeMachine.ts +26 -0
  56. package/src/index.tsx +45 -0
  57. package/src/preset.tsx +38 -0
  58. package/src/store/snapshotVault.ts +117 -0
  59. package/src/store/timeMachineStore.ts +368 -0
  60. package/src/sync/timeMachineSyncAdapter.ts +128 -0
@@ -0,0 +1,455 @@
1
+ /**
2
+ * Time Machine body — capture bar, provider status strip, snapshot list and
3
+ * the last-restore results panel. Shared by the RN modal and the inline
4
+ * (desktop) tool view.
5
+ */
6
+ import { useState } from "react";
7
+ import {
8
+ Alert,
9
+ Platform,
10
+ StyleSheet,
11
+ Text,
12
+ TextInput,
13
+ TouchableOpacity,
14
+ View,
15
+ } from "react-native";
16
+ import { buoyColors } from "@buoy-gg/shared-ui";
17
+ import { useIsPro } from "@buoy-gg/license";
18
+ import { useSnapshotProviders, useTimeMachineState } from "../hooks/useTimeMachine";
19
+ import { timeMachineStore } from "../store/timeMachineStore";
20
+ import type { RestoreOutcome } from "../store/timeMachineStore";
21
+ import type { SnapshotMeta } from "../store/snapshotVault";
22
+
23
+ const FREE_SNAPSHOT_LIMIT = 2;
24
+
25
+ function timeAgo(ts: number): string {
26
+ const s = Math.max(0, Math.round((Date.now() - ts) / 1000));
27
+ if (s < 60) return `${s}s ago`;
28
+ const m = Math.round(s / 60);
29
+ if (m < 60) return `${m}m ago`;
30
+ const h = Math.round(m / 60);
31
+ if (h < 24) return `${h}h ago`;
32
+ return `${Math.round(h / 24)}d ago`;
33
+ }
34
+
35
+ function formatBytes(bytes: number): string {
36
+ if (bytes < 1024) return `${bytes} B`;
37
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
38
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
39
+ }
40
+
41
+ function warningCount(meta: SnapshotMeta): number {
42
+ return Object.values(meta.sources).reduce(
43
+ (sum, s) => sum + s.warnings.length,
44
+ 0
45
+ );
46
+ }
47
+
48
+ /** RN-web's Alert renders no buttons — fall back to the browser dialog there. */
49
+ function confirmDestructive(
50
+ title: string,
51
+ message: string,
52
+ confirmLabel: string,
53
+ onConfirm: () => void
54
+ ) {
55
+ if (Platform.OS === "web") {
56
+ const confirmFn = (globalThis as { confirm?: (msg: string) => boolean }).confirm;
57
+ if (!confirmFn || confirmFn(`${title}\n\n${message}`)) onConfirm();
58
+ return;
59
+ }
60
+ Alert.alert(title, message, [
61
+ { text: "Cancel", style: "cancel" },
62
+ { text: confirmLabel, style: "destructive", onPress: onConfirm },
63
+ ]);
64
+ }
65
+
66
+ function OutcomePanel({ outcome }: { outcome: RestoreOutcome }) {
67
+ const entries = Object.entries(outcome.results);
68
+ return (
69
+ <View style={styles.outcomePanel}>
70
+ <View style={styles.outcomeHeaderRow}>
71
+ <Text style={styles.outcomeTitle}>
72
+ Restored “{outcome.snapshotName}”
73
+ {outcome.mode === "reload" || outcome.willReload ? " · reloading" : ""}
74
+ </Text>
75
+ <TouchableOpacity
76
+ onPress={() => timeMachineStore.clearLastOutcome()}
77
+ hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
78
+ >
79
+ <Text style={styles.outcomeDismiss}>✕</Text>
80
+ </TouchableOpacity>
81
+ </View>
82
+ {entries.length === 0 && (
83
+ <Text style={styles.outcomeLine}>no sources restored</Text>
84
+ )}
85
+ {entries.map(([sourceId, r]) => (
86
+ <View key={sourceId} style={styles.outcomeSource}>
87
+ <Text style={[styles.outcomeLine, !r.ok && styles.outcomeLineBad]}>
88
+ {r.ok ? "✓" : "✕"} {sourceId} — applied {r.applied}
89
+ {r.skipped.length > 0 ? `, skipped ${r.skipped.length}` : ""}
90
+ </Text>
91
+ {r.skipped.slice(0, 3).map((skip) => (
92
+ <Text key={skip.id} style={styles.outcomeDetail}>
93
+ · {skip.id}: {skip.reason}
94
+ </Text>
95
+ ))}
96
+ {r.warnings.slice(0, 3).map((warning) => (
97
+ <Text key={warning} style={styles.outcomeDetail}>
98
+ ⚠ {warning}
99
+ </Text>
100
+ ))}
101
+ </View>
102
+ ))}
103
+ </View>
104
+ );
105
+ }
106
+
107
+ export function TimeMachineBody() {
108
+ const { snapshots, hasLoaded, busy, lastOutcome, remoteProviders } =
109
+ useTimeMachineState();
110
+ const localProviders = useSnapshotProviders();
111
+ const isPro = useIsPro();
112
+ const [name, setName] = useState("");
113
+
114
+ // On-device: live registry probes. Buoy Desktop: statuses mirrored from
115
+ // the device's sync payload.
116
+ const providers =
117
+ remoteProviders ??
118
+ localProviders.map((p) => ({ id: p.id, label: p.label, ...p.status() }));
119
+
120
+ const atFreeLimit = !isPro && snapshots.length >= FREE_SNAPSHOT_LIMIT;
121
+ const captureDisabled = busy !== null || atFreeLimit;
122
+
123
+ const capture = () => {
124
+ if (captureDisabled) return;
125
+ void timeMachineStore.capture(name);
126
+ setName("");
127
+ };
128
+
129
+ const captureBaseline = () => {
130
+ if (captureDisabled) return;
131
+ void timeMachineStore.captureBaseline();
132
+ };
133
+
134
+ const restore = (meta: SnapshotMeta, mode: "live" | "reload") => {
135
+ if (busy) return;
136
+ if (meta.baseline) {
137
+ confirmDestructive(
138
+ "Restore fresh install?",
139
+ "This wipes all app storage and reloads the app.",
140
+ "Wipe & reload",
141
+ () => void timeMachineStore.restore(meta.id, "reload")
142
+ );
143
+ return;
144
+ }
145
+ void timeMachineStore.restore(meta.id, mode);
146
+ };
147
+
148
+ const remove = (meta: SnapshotMeta) => {
149
+ confirmDestructive(
150
+ "Delete snapshot?",
151
+ `“${meta.name}” cannot be recovered.`,
152
+ "Delete",
153
+ () => void timeMachineStore.delete(meta.id)
154
+ );
155
+ };
156
+
157
+ const unavailable = providers.filter(
158
+ (p) => p.detail && (!p.canCapture || !p.canRestore)
159
+ );
160
+
161
+ return (
162
+ <View style={styles.container}>
163
+ {lastOutcome && <OutcomePanel outcome={lastOutcome} />}
164
+
165
+ {/* Capture bar */}
166
+ <View style={styles.captureRow}>
167
+ <TextInput
168
+ style={styles.nameInput}
169
+ value={name}
170
+ onChangeText={setName}
171
+ placeholder="Snapshot name…"
172
+ placeholderTextColor={buoyColors.textMuted}
173
+ editable={!captureDisabled}
174
+ onSubmitEditing={capture}
175
+ returnKeyType="done"
176
+ testID="time-machine-name-input"
177
+ />
178
+ <TouchableOpacity
179
+ style={[styles.captureButton, captureDisabled && styles.buttonDisabled]}
180
+ onPress={capture}
181
+ disabled={captureDisabled}
182
+ testID="time-machine-capture"
183
+ >
184
+ <Text style={styles.captureButtonText}>
185
+ {busy === "capture" ? "…" : "CAPTURE"}
186
+ </Text>
187
+ </TouchableOpacity>
188
+ </View>
189
+ <TouchableOpacity
190
+ style={[styles.baselineButton, captureDisabled && styles.buttonDisabled]}
191
+ onPress={captureBaseline}
192
+ disabled={captureDisabled}
193
+ testID="time-machine-capture-baseline"
194
+ >
195
+ <Text style={styles.baselineButtonText}>
196
+ ⌂ CAPTURE FRESH-INSTALL BASELINE
197
+ </Text>
198
+ </TouchableOpacity>
199
+ {atFreeLimit && (
200
+ <Text style={styles.freeLimitNote}>
201
+ Free plan: {FREE_SNAPSHOT_LIMIT} restore points. Upgrade to Pro for
202
+ unlimited snapshots.
203
+ </Text>
204
+ )}
205
+
206
+ {/* Provider status */}
207
+ <View style={styles.providerStrip}>
208
+ {providers.map((p) => {
209
+ const dotColor = !p.canCapture
210
+ ? buoyColors.textMuted
211
+ : p.canRestore
212
+ ? buoyColors.success
213
+ : buoyColors.warning;
214
+ return (
215
+ <View key={p.id} style={styles.providerChip}>
216
+ <View style={[styles.providerDot, { backgroundColor: dotColor }]} />
217
+ <Text style={styles.providerLabel}>{p.label}</Text>
218
+ </View>
219
+ );
220
+ })}
221
+ {providers.length === 0 && (
222
+ <Text style={styles.emptyText}>No snapshot providers registered</Text>
223
+ )}
224
+ </View>
225
+ {unavailable.map((p) => (
226
+ <Text key={p.id} style={styles.providerDetail}>
227
+ {p.label}: {p.detail}
228
+ </Text>
229
+ ))}
230
+
231
+ {/* Snapshot list */}
232
+ {hasLoaded && snapshots.length === 0 && (
233
+ <Text style={styles.emptyText}>
234
+ No snapshots yet. Capture one, test your flow, then restore to jump
235
+ right back.
236
+ </Text>
237
+ )}
238
+ {snapshots.map((meta) => {
239
+ const warnings = warningCount(meta);
240
+ return (
241
+ <View key={meta.id} style={styles.snapshotRow} testID={`time-machine-snap-${meta.id}`}>
242
+ <View style={styles.snapshotInfo}>
243
+ <Text style={styles.snapshotName} numberOfLines={1}>
244
+ {meta.baseline ? "⌂ " : ""}
245
+ {meta.name}
246
+ </Text>
247
+ <Text style={styles.snapshotMeta}>
248
+ {timeAgo(meta.createdAt)}
249
+ {meta.baseline
250
+ ? " · wipes to fresh install"
251
+ : ` · ${formatBytes(meta.sizeBytes)} · ${Object.keys(meta.sources).join(", ") || "empty"}`}
252
+ {warnings > 0 ? ` · ⚠ ${warnings}` : ""}
253
+ </Text>
254
+ </View>
255
+ {!meta.baseline && (
256
+ <TouchableOpacity
257
+ style={[styles.rowButton, busy && styles.buttonDisabled]}
258
+ onPress={() => restore(meta, "live")}
259
+ disabled={busy !== null}
260
+ >
261
+ <Text style={styles.rowButtonText}>RESTORE</Text>
262
+ </TouchableOpacity>
263
+ )}
264
+ <TouchableOpacity
265
+ style={[styles.rowButton, busy && styles.buttonDisabled]}
266
+ onPress={() => restore(meta, "reload")}
267
+ disabled={busy !== null}
268
+ >
269
+ <Text style={styles.rowButtonText}>
270
+ {meta.baseline ? "RESTORE" : "↻"}
271
+ </Text>
272
+ </TouchableOpacity>
273
+ <TouchableOpacity
274
+ style={styles.deleteButton}
275
+ onPress={() => remove(meta)}
276
+ hitSlop={{ top: 8, bottom: 8, left: 4, right: 8 }}
277
+ >
278
+ <Text style={styles.deleteButtonText}>✕</Text>
279
+ </TouchableOpacity>
280
+ </View>
281
+ );
282
+ })}
283
+ </View>
284
+ );
285
+ }
286
+
287
+ const styles = StyleSheet.create({
288
+ container: {
289
+ padding: 12,
290
+ gap: 8,
291
+ },
292
+ captureRow: {
293
+ flexDirection: "row",
294
+ gap: 8,
295
+ },
296
+ nameInput: {
297
+ flex: 1,
298
+ backgroundColor: buoyColors.input,
299
+ borderColor: buoyColors.border,
300
+ borderWidth: 1,
301
+ borderRadius: 6,
302
+ color: buoyColors.text,
303
+ paddingHorizontal: 10,
304
+ paddingVertical: 8,
305
+ fontSize: 13,
306
+ },
307
+ captureButton: {
308
+ backgroundColor: buoyColors.primary,
309
+ borderRadius: 6,
310
+ paddingHorizontal: 14,
311
+ justifyContent: "center",
312
+ },
313
+ captureButtonText: {
314
+ color: "#0A0A0A",
315
+ fontSize: 12,
316
+ fontWeight: "700",
317
+ letterSpacing: 0.5,
318
+ },
319
+ baselineButton: {
320
+ borderColor: buoyColors.border,
321
+ borderWidth: 1,
322
+ borderRadius: 6,
323
+ paddingVertical: 8,
324
+ alignItems: "center",
325
+ },
326
+ baselineButtonText: {
327
+ color: buoyColors.textSecondary,
328
+ fontSize: 11,
329
+ fontWeight: "600",
330
+ letterSpacing: 0.5,
331
+ },
332
+ buttonDisabled: {
333
+ opacity: 0.4,
334
+ },
335
+ freeLimitNote: {
336
+ color: buoyColors.warning,
337
+ fontSize: 11,
338
+ },
339
+ providerStrip: {
340
+ flexDirection: "row",
341
+ flexWrap: "wrap",
342
+ gap: 6,
343
+ marginTop: 4,
344
+ },
345
+ providerChip: {
346
+ flexDirection: "row",
347
+ alignItems: "center",
348
+ gap: 5,
349
+ backgroundColor: buoyColors.card,
350
+ borderColor: buoyColors.border,
351
+ borderWidth: 1,
352
+ borderRadius: 12,
353
+ paddingHorizontal: 8,
354
+ paddingVertical: 3,
355
+ },
356
+ providerDot: {
357
+ width: 6,
358
+ height: 6,
359
+ borderRadius: 3,
360
+ },
361
+ providerLabel: {
362
+ color: buoyColors.textSecondary,
363
+ fontSize: 11,
364
+ },
365
+ providerDetail: {
366
+ color: buoyColors.textMuted,
367
+ fontSize: 10,
368
+ },
369
+ emptyText: {
370
+ color: buoyColors.textMuted,
371
+ fontSize: 12,
372
+ marginTop: 8,
373
+ },
374
+ snapshotRow: {
375
+ flexDirection: "row",
376
+ alignItems: "center",
377
+ backgroundColor: buoyColors.card,
378
+ borderColor: buoyColors.border,
379
+ borderWidth: 1,
380
+ borderRadius: 8,
381
+ padding: 10,
382
+ gap: 8,
383
+ },
384
+ snapshotInfo: {
385
+ flex: 1,
386
+ minWidth: 0,
387
+ },
388
+ snapshotName: {
389
+ color: buoyColors.text,
390
+ fontSize: 13,
391
+ fontWeight: "600",
392
+ },
393
+ snapshotMeta: {
394
+ color: buoyColors.textMuted,
395
+ fontSize: 10,
396
+ marginTop: 2,
397
+ },
398
+ rowButton: {
399
+ borderColor: buoyColors.primary,
400
+ borderWidth: 1,
401
+ borderRadius: 6,
402
+ paddingHorizontal: 8,
403
+ paddingVertical: 6,
404
+ },
405
+ rowButtonText: {
406
+ color: buoyColors.primary,
407
+ fontSize: 10,
408
+ fontWeight: "700",
409
+ },
410
+ deleteButton: {
411
+ paddingHorizontal: 2,
412
+ },
413
+ deleteButtonText: {
414
+ color: buoyColors.textMuted,
415
+ fontSize: 13,
416
+ },
417
+ outcomePanel: {
418
+ backgroundColor: buoyColors.card,
419
+ borderColor: buoyColors.borderStrong,
420
+ borderWidth: 1,
421
+ borderRadius: 8,
422
+ padding: 10,
423
+ gap: 4,
424
+ },
425
+ outcomeHeaderRow: {
426
+ flexDirection: "row",
427
+ justifyContent: "space-between",
428
+ alignItems: "center",
429
+ },
430
+ outcomeTitle: {
431
+ color: buoyColors.text,
432
+ fontSize: 12,
433
+ fontWeight: "700",
434
+ flex: 1,
435
+ },
436
+ outcomeDismiss: {
437
+ color: buoyColors.textMuted,
438
+ fontSize: 13,
439
+ },
440
+ outcomeSource: {
441
+ gap: 2,
442
+ },
443
+ outcomeLine: {
444
+ color: buoyColors.success,
445
+ fontSize: 11,
446
+ },
447
+ outcomeLineBad: {
448
+ color: buoyColors.warning,
449
+ },
450
+ outcomeDetail: {
451
+ color: buoyColors.textMuted,
452
+ fontSize: 10,
453
+ paddingLeft: 12,
454
+ },
455
+ });
@@ -0,0 +1,75 @@
1
+ /**
2
+ * TimeMachineModal — bottom-sheet wrapper rendered by FloatingDevTools when
3
+ * the Time Machine tool is opened. TimeMachineToolView is the inline variant
4
+ * for Buoy Desktop.
5
+ */
6
+ import { StyleSheet, Text, View } from "react-native";
7
+ import { JsModal, buoyColors } from "@buoy-gg/shared-ui";
8
+ import type { ModalState } from "@buoy-gg/shared-ui";
9
+ import { TimeMachineBody } from "./TimeMachineBody";
10
+
11
+ export interface TimeMachineModalProps {
12
+ visible: boolean;
13
+ onClose: () => void;
14
+ onBack?: () => void;
15
+ onMinimize?: (modalState: ModalState) => void;
16
+ persistenceKey?: string;
17
+ }
18
+
19
+ function Header() {
20
+ return (
21
+ <View style={styles.header}>
22
+ <Text style={styles.headerTitle}>TIME MACHINE</Text>
23
+ <Text style={styles.headerSubtitle}>snapshot & restore app state</Text>
24
+ </View>
25
+ );
26
+ }
27
+
28
+ export function TimeMachineModal({
29
+ visible,
30
+ onClose,
31
+ onBack: _onBack,
32
+ onMinimize,
33
+ persistenceKey,
34
+ }: TimeMachineModalProps) {
35
+ return (
36
+ <JsModal
37
+ visible={visible}
38
+ onClose={onClose}
39
+ onMinimize={onMinimize}
40
+ persistenceKey={persistenceKey}
41
+ header={{
42
+ showToggleButton: true,
43
+ customContent: <Header />,
44
+ }}
45
+ enablePersistence
46
+ initialMode="bottomSheet"
47
+ >
48
+ <TimeMachineBody />
49
+ </JsModal>
50
+ );
51
+ }
52
+
53
+ /** Inline variant (no modal chrome) — used by Buoy Desktop. */
54
+ export function TimeMachineToolView() {
55
+ return <TimeMachineBody />;
56
+ }
57
+
58
+ const styles = StyleSheet.create({
59
+ header: {
60
+ flexDirection: "row",
61
+ alignItems: "baseline",
62
+ gap: 8,
63
+ paddingHorizontal: 4,
64
+ },
65
+ headerTitle: {
66
+ color: buoyColors.text,
67
+ fontSize: 13,
68
+ fontWeight: "700",
69
+ letterSpacing: 1,
70
+ },
71
+ headerSubtitle: {
72
+ color: buoyColors.textMuted,
73
+ fontSize: 10,
74
+ },
75
+ });
@@ -0,0 +1,26 @@
1
+ import { useEffect, useState, useSyncExternalStore } from "react";
2
+ import {
3
+ getStateSnapshotProviders,
4
+ subscribeToStateSnapshotProviders,
5
+ type StateSnapshotProvider,
6
+ } from "@buoy-gg/shared-ui";
7
+ import { timeMachineStore } from "../store/timeMachineStore";
8
+ import type { TimeMachineState } from "../store/timeMachineStore";
9
+
10
+ export function useTimeMachineState(): TimeMachineState {
11
+ return useSyncExternalStore(timeMachineStore.subscribe, () =>
12
+ timeMachineStore.getState()
13
+ );
14
+ }
15
+
16
+ /** Registered snapshot providers (query registers late, hence the subscribe). */
17
+ export function useSnapshotProviders(): StateSnapshotProvider[] {
18
+ const [providers, setProviders] = useState(getStateSnapshotProviders);
19
+ useEffect(() => {
20
+ setProviders(getStateSnapshotProviders());
21
+ return subscribeToStateSnapshotProviders(() =>
22
+ setProviders(getStateSnapshotProviders())
23
+ );
24
+ }, []);
25
+ return providers;
26
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,45 @@
1
+ /**
2
+ * @buoy-gg/time-machine
3
+ *
4
+ * Time Machine — snapshot & restore app state for React Native DevTools.
5
+ *
6
+ * Capture the complete client-side state of the app (storage, Redux,
7
+ * Zustand, Jotai, React Query) as a named snapshot; restore any snapshot in
8
+ * one tap to iterate on multi-step test flows without rebuilding state.
9
+ *
10
+ * PUBLIC API - Only these exports are supported for external use.
11
+ */
12
+
13
+ // Tool preset for FloatingDevTools
14
+ export { timeMachineToolPreset, createTimeMachineTool } from "./preset";
15
+
16
+ // Components
17
+ export {
18
+ TimeMachineModal,
19
+ TimeMachineToolView,
20
+ type TimeMachineModalProps,
21
+ } from "./components/TimeMachineModal";
22
+
23
+ // External-sync adapter (auto-registered by @buoy-gg/core)
24
+ export { timeMachineSyncAdapter } from "./sync/timeMachineSyncAdapter";
25
+
26
+ // Programmatic snapshot control (also used by tests / power users)
27
+ export {
28
+ timeMachineStore,
29
+ RESTORE_ORDER,
30
+ type RestoreMode,
31
+ type RestoreOutcome,
32
+ type TimeMachineState,
33
+ type RemoteProviderStatus,
34
+ } from "./store/timeMachineStore";
35
+
36
+ // Buoy Desktop remote-mirror seam
37
+ export {
38
+ setRemoteTimeMachineHandler,
39
+ replaceTimeMachineFromSnapshot,
40
+ } from "./store/timeMachineStore";
41
+ export type {
42
+ SnapshotMeta,
43
+ SnapshotSourceMeta,
44
+ StoredSnapshot,
45
+ } from "./store/snapshotVault";
package/src/preset.tsx ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Pre-configured Time Machine tool for FloatingDevTools.
3
+ *
4
+ * @example
5
+ * ```tsx
6
+ * import { timeMachineToolPreset } from '@buoy-gg/time-machine';
7
+ *
8
+ * <FloatingDevTools apps={[timeMachineToolPreset]} />
9
+ * ```
10
+ */
11
+
12
+ import {
13
+ TimeMachineIcon,
14
+ TIME_MACHINE_ICON_COLOR,
15
+ } from "@buoy-gg/shared-ui";
16
+ import { TimeMachineModal } from "./components/TimeMachineModal";
17
+
18
+ export const timeMachineToolPreset = {
19
+ id: "time-machine",
20
+ name: "TIME MACHINE",
21
+ description: "Snapshot & restore app state (storage, stores, query cache)",
22
+ slot: "both" as const,
23
+ icon: ({ size, color }: { size: number; color?: string }) => (
24
+ <TimeMachineIcon size={size} color={color || TIME_MACHINE_ICON_COLOR} />
25
+ ),
26
+ component: TimeMachineModal,
27
+ };
28
+
29
+ /** Create a custom Time Machine tool configuration. */
30
+ export function createTimeMachineTool(
31
+ options: { name?: string; description?: string } = {}
32
+ ) {
33
+ return {
34
+ ...timeMachineToolPreset,
35
+ name: options.name ?? timeMachineToolPreset.name,
36
+ description: options.description ?? timeMachineToolPreset.description,
37
+ };
38
+ }