@alwith/module-drive 0.0.0-stage → 0.1.0-rc.22

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 (107) hide show
  1. package/LICENSE +94 -0
  2. package/README.md +50 -2
  3. package/THIRD_PARTY_NOTICES.md +7 -0
  4. package/desktop-provenance.json +34 -0
  5. package/dist/badge-react.d.ts +9 -0
  6. package/dist/badge-react.js +1 -0
  7. package/dist/badge.d.ts +10 -0
  8. package/dist/badge.js +1 -0
  9. package/dist/browse.d.ts +33 -0
  10. package/dist/browse.js +1 -0
  11. package/dist/content/chat-condense.d.ts +22 -0
  12. package/dist/content/chat-condense.js +1 -0
  13. package/dist/content/collab.d.ts +34 -0
  14. package/dist/content/collab.js +1 -0
  15. package/dist/content/comment-anchors.d.ts +23 -0
  16. package/dist/content/comment-anchors.js +2 -0
  17. package/dist/content/comment-decorations.d.ts +23 -0
  18. package/dist/content/comment-decorations.js +3 -0
  19. package/dist/content/comment-drawer.d.ts +44 -0
  20. package/dist/content/comment-drawer.js +1 -0
  21. package/dist/content/comment-mentions.d.ts +18 -0
  22. package/dist/content/comment-mentions.js +1 -0
  23. package/dist/content/comment-surface.d.ts +25 -0
  24. package/dist/content/comment-surface.js +1 -0
  25. package/dist/content/condense-task.d.ts +44 -0
  26. package/dist/content/condense-task.js +1 -0
  27. package/dist/content/condense.d.ts +20 -0
  28. package/dist/content/condense.js +1 -0
  29. package/dist/content/editor.d.ts +5 -0
  30. package/dist/content/editor.js +1 -0
  31. package/dist/content/file-actions.d.ts +43 -0
  32. package/dist/content/file-actions.js +1 -0
  33. package/dist/content/file-link.d.ts +4 -0
  34. package/dist/content/file-link.js +1 -0
  35. package/dist/content/frontmatter-panel.d.ts +7 -0
  36. package/dist/content/frontmatter-panel.js +1 -0
  37. package/dist/content/frontmatter.d.ts +9 -0
  38. package/dist/content/frontmatter.js +7 -0
  39. package/dist/content/host.d.ts +49 -0
  40. package/dist/content/host.js +1 -0
  41. package/dist/content/html-bridge.d.ts +1 -0
  42. package/dist/content/html-bridge.js +187 -0
  43. package/dist/content/html-comments.d.ts +5 -0
  44. package/dist/content/html-comments.js +1 -0
  45. package/dist/content/markdown-comments.d.ts +4 -0
  46. package/dist/content/markdown-comments.js +2 -0
  47. package/dist/content/mention-picker.d.ts +10 -0
  48. package/dist/content/mention-picker.js +1 -0
  49. package/dist/content/save-version.d.ts +2 -0
  50. package/dist/content/save-version.js +1 -0
  51. package/dist/content/translations.d.ts +2 -0
  52. package/dist/content/translations.js +1 -0
  53. package/dist/content/use-file-comments.d.ts +12 -0
  54. package/dist/content/use-file-comments.js +1 -0
  55. package/dist/content/yuplink.d.ts +11 -0
  56. package/dist/content/yuplink.js +2 -0
  57. package/dist/content.css +64 -0
  58. package/dist/content.d.ts +10 -0
  59. package/dist/content.js +1 -0
  60. package/dist/contract.d.ts +780 -0
  61. package/dist/contract.js +0 -0
  62. package/dist/core.d.ts +22 -0
  63. package/dist/core.js +1 -0
  64. package/dist/files.d.ts +7 -0
  65. package/dist/files.js +1 -0
  66. package/dist/global-search.d.ts +34 -0
  67. package/dist/global-search.js +1 -0
  68. package/dist/icons.d.ts +5 -0
  69. package/dist/icons.js +1 -0
  70. package/dist/launcher.d.ts +15 -0
  71. package/dist/launcher.js +2 -0
  72. package/dist/locales.d.ts +983 -0
  73. package/dist/locales.js +1 -0
  74. package/dist/mass-delete.d.ts +9 -0
  75. package/dist/mass-delete.js +1 -0
  76. package/dist/notifications.d.ts +14 -0
  77. package/dist/notifications.js +1 -0
  78. package/dist/pinned-docs.d.ts +14 -0
  79. package/dist/pinned-docs.js +1 -0
  80. package/dist/pinned.d.ts +12 -0
  81. package/dist/pinned.js +1 -0
  82. package/dist/react.d.ts +29 -0
  83. package/dist/react.js +1 -0
  84. package/dist/roots.d.ts +10 -0
  85. package/dist/roots.js +1 -0
  86. package/dist/settings.d.ts +14 -0
  87. package/dist/settings.js +1 -0
  88. package/dist/sharing.d.ts +12 -0
  89. package/dist/sharing.js +1 -0
  90. package/dist/status.d.ts +16 -0
  91. package/dist/status.js +1 -0
  92. package/dist/styles.css +458 -0
  93. package/dist/summary.d.ts +10 -0
  94. package/dist/summary.js +1 -0
  95. package/dist/tauri.d.ts +6 -0
  96. package/dist/tauri.js +1 -0
  97. package/dist/transfer-feed.d.ts +36 -0
  98. package/dist/transfer-feed.js +1 -0
  99. package/dist/ui.d.ts +96 -0
  100. package/dist/ui.js +1 -0
  101. package/package.json +117 -4
  102. package/rust/Cargo.toml +14 -0
  103. package/rust/src/contract.rs +366 -0
  104. package/rust/src/lib.rs +5 -0
  105. package/rust/src/ownership.rs +4 -0
  106. package/rust/src/service.rs +140 -0
  107. package/rust/src/sync.rs +1602 -0
package/package.json CHANGED
@@ -1,6 +1,119 @@
1
1
  {
2
2
  "name": "@alwith/module-drive",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0-rc.22",
4
+ "type": "module",
5
+ "license": "SEE LICENSE IN LICENSE",
6
+ "files": [
7
+ "dist",
8
+ "LICENSE",
9
+ "THIRD_PARTY_NOTICES.md",
10
+ "README.md",
11
+ "desktop-provenance.json",
12
+ "rust/Cargo.toml",
13
+ "rust/src"
14
+ ],
15
+ "sideEffects": [
16
+ "**/*.css"
17
+ ],
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/core.d.ts",
21
+ "import": "./dist/core.js"
22
+ },
23
+ "./core": {
24
+ "types": "./dist/core.d.ts",
25
+ "import": "./dist/core.js"
26
+ },
27
+ "./react": {
28
+ "types": "./dist/react.d.ts",
29
+ "import": "./dist/react.js"
30
+ },
31
+ "./tauri": {
32
+ "types": "./dist/tauri.d.ts",
33
+ "import": "./dist/tauri.js"
34
+ },
35
+ "./styles.css": "./dist/styles.css",
36
+ "./content": {
37
+ "types": "./dist/content.d.ts",
38
+ "import": "./dist/content.js"
39
+ }
40
+ },
41
+ "scripts": {
42
+ "build": "bun ../../scripts/build-npm.ts",
43
+ "test": "bun test src",
44
+ "bindings": "cargo run --manifest-path ../../Cargo.toml -p alwith-drive-impl --bin export -- src/contract.ts",
45
+ "prepack": "bun run build && bun ../../scripts/verify-module-package.ts drive"
46
+ },
47
+ "peerDependencies": {
48
+ "react": "^19.0.0",
49
+ "react-dom": "^19.0.0",
50
+ "@milkdown/core": "^7.22.2",
51
+ "@milkdown/ctx": "^7.22.2",
52
+ "@milkdown/prose": "^7.22.2",
53
+ "@milkdown/utils": "^7.22.2",
54
+ "@milkdown/plugin-collab": "^7.22.2",
55
+ "yjs": "^13.6.33",
56
+ "y-prosemirror": "^1.3.7",
57
+ "y-protocols": "^1.0.7",
58
+ "y-websocket": "^3.1.0",
59
+ "lucide-react": "^1.54.0",
60
+ "@alwith/module-editor": "^0.1.0-rc.8"
61
+ },
62
+ "devDependencies": {
63
+ "@types/react": "^19.3.0",
64
+ "@types/react-dom": "^19.3.0",
65
+ "@milkdown/core": "^7.22.2",
66
+ "@milkdown/ctx": "^7.22.2",
67
+ "@milkdown/prose": "^7.22.2",
68
+ "@milkdown/utils": "^7.22.2",
69
+ "@milkdown/plugin-collab": "^7.22.2",
70
+ "yjs": "^13.6.33",
71
+ "y-prosemirror": "^1.3.7",
72
+ "y-protocols": "^1.0.7",
73
+ "y-websocket": "^3.1.0",
74
+ "lucide-react": "^1.54.0",
75
+ "@alwith/module-editor": "workspace:*"
76
+ },
77
+ "peerDependenciesMeta": {
78
+ "@milkdown/core": {
79
+ "optional": true
80
+ },
81
+ "@milkdown/ctx": {
82
+ "optional": true
83
+ },
84
+ "@milkdown/prose": {
85
+ "optional": true
86
+ },
87
+ "@milkdown/utils": {
88
+ "optional": true
89
+ },
90
+ "@milkdown/plugin-collab": {
91
+ "optional": true
92
+ },
93
+ "yjs": {
94
+ "optional": true
95
+ },
96
+ "y-prosemirror": {
97
+ "optional": true
98
+ },
99
+ "y-protocols": {
100
+ "optional": true
101
+ },
102
+ "y-websocket": {
103
+ "optional": true
104
+ },
105
+ "lucide-react": {
106
+ "optional": true
107
+ },
108
+ "@alwith/module-editor": {
109
+ "optional": true
110
+ }
111
+ },
112
+ "publishConfig": {
113
+ "access": "public",
114
+ "registry": "https://registry.npmjs.org"
115
+ },
116
+ "dependencies": {
117
+ "@alwith/native-kit": "0.1.0"
118
+ }
119
+ }
@@ -0,0 +1,14 @@
1
+ [package]
2
+ autobins = false
3
+ autotests = false
4
+ name = "alwith-drive"
5
+ version = "0.1.0-rc.22"
6
+ edition = "2024"
7
+ publish = false
8
+ [workspace]
9
+ [dependencies]
10
+ alwith-native-client = { path = "../../native-kit/rust" }
11
+ serde = { version = "1", features = ["derive"] }
12
+ serde_json = "1"
13
+ specta = { version = "2.0.0-rc.27", features = ["derive"] }
14
+ tokio = { version = "1", features = ["sync"] }
@@ -0,0 +1,366 @@
1
+ use crate::sync::*;
2
+ use serde::{Deserialize, Serialize};
3
+ use specta::Type;
4
+
5
+ pub const CONTRACT_VERSION: u32 = 1;
6
+
7
+ #[derive(Deserialize, Type, Serialize)]
8
+ #[serde(rename_all = "SCREAMING_SNAKE_CASE")]
9
+ pub enum AccessRequestType {
10
+ FileAccess,
11
+ ProjectJoin,
12
+ }
13
+
14
+ #[derive(Serialize, Type, Deserialize)]
15
+ #[serde(rename_all = "camelCase")]
16
+ pub enum AccessRequestOutcome {
17
+ Submitted,
18
+ AlreadyPending,
19
+ }
20
+
21
+ #[derive(Clone, Serialize, Deserialize, Type)]
22
+ #[serde(rename_all = "camelCase")]
23
+ pub struct Snapshot {
24
+ pub version: u32,
25
+ pub generation: u32,
26
+ pub sequence: u32,
27
+ pub configured: bool,
28
+ pub running: bool,
29
+ pub base_url: String,
30
+ pub local_root: String,
31
+ pub web_base_url: Option<String>,
32
+ pub status: Option<StatusSnapshot>,
33
+ pub roots: Vec<RootInfo>,
34
+ pub pending_deletes: Vec<PendingDeletes>,
35
+ pub files: Vec<FileStateEvent>,
36
+ pub projects: Vec<ProjectProgress>,
37
+ pub error: Option<String>,
38
+ }
39
+
40
+ #[derive(Clone, Serialize, Deserialize, Type)]
41
+ #[serde(tag = "type", content = "data", rename_all = "camelCase")]
42
+ pub enum Payload {
43
+ Snapshot(Snapshot),
44
+ Status(StatusSnapshot),
45
+ File(FileStateEvent),
46
+ Project(ProjectProgress),
47
+ Conflict(ConflictRecord),
48
+ ReadOnly(ReadOnlyChange),
49
+ Changed(Vec<String>),
50
+ Reconcile,
51
+ Comment(CommentEvent),
52
+ Error(String),
53
+ }
54
+
55
+ #[derive(Clone, Serialize, Deserialize, Type)]
56
+ #[serde(rename_all = "camelCase")]
57
+ pub struct DriveEvent {
58
+ pub version: u32,
59
+ pub generation: u32,
60
+ pub sequence: u32,
61
+ pub payload: Payload,
62
+ }
63
+
64
+ #[derive(Clone, Serialize, Deserialize, Type)]
65
+ #[serde(rename_all = "camelCase")]
66
+ pub struct CommentEvent {
67
+ pub file_id: RemoteId,
68
+ pub comment_id: RemoteId,
69
+ pub action: String,
70
+ pub actor_id: Option<RemoteId>,
71
+ pub at: Option<String>,
72
+ }
73
+
74
+ #[derive(Serialize, Type, Deserialize)]
75
+ #[serde(rename_all = "camelCase")]
76
+ pub struct Configuration {
77
+ pub device_id: Option<String>,
78
+ pub user_name: Option<String>,
79
+ pub user_id: Option<String>,
80
+ pub client_config: Option<ClientConfig>,
81
+ }
82
+
83
+ #[derive(Serialize, Type, Deserialize)]
84
+ #[serde(rename_all = "camelCase")]
85
+ pub struct CollabSession {
86
+ pub file_id: RemoteId,
87
+ pub server_url: String,
88
+ pub room: String,
89
+ pub token: String,
90
+ pub can_write: bool,
91
+ }
92
+
93
+ #[derive(Serialize, Type, Deserialize)]
94
+ pub struct McpHeader {
95
+ pub name: String,
96
+ pub value: String,
97
+ }
98
+
99
+ #[derive(Serialize, Type, Deserialize)]
100
+ pub struct McpSessionConfig {
101
+ pub url: String,
102
+ pub headers: Vec<McpHeader>,
103
+ }
104
+
105
+ #[derive(Deserialize, Type, Serialize)]
106
+ #[serde(tag = "type", rename_all = "camelCase", rename_all_fields = "camelCase")]
107
+ pub enum DriveRequest {
108
+ Preferences,
109
+ ArchivePrivateSessions,
110
+ SetArchivePrivate {
111
+ session_id: String,
112
+ private: bool,
113
+ },
114
+ SetPreferences {
115
+ session_archive: bool,
116
+ knowledge_inject: bool,
117
+ },
118
+ ArchiveSession {
119
+ expected_generation: u32,
120
+ session_id: String,
121
+ cwd: String,
122
+ title: String,
123
+ updated_at: String,
124
+ content: String,
125
+ },
126
+ Configuration,
127
+ Reconfigure {
128
+ local_root: String,
129
+ user_name: String,
130
+ },
131
+ RefreshClientConfig,
132
+ McpSessionConfig,
133
+ RetryUpload {
134
+ project_id: RemoteId,
135
+ path: String,
136
+ },
137
+ CloudFolders {
138
+ project_id: RemoteId,
139
+ },
140
+ FileRemoteId {
141
+ path: String,
142
+ },
143
+ CommentContext {
144
+ path: String,
145
+ },
146
+ Comments {
147
+ file_id: RemoteId,
148
+ },
149
+ CommentCreate {
150
+ file_id: RemoteId,
151
+ request: CreateCommentRequest,
152
+ },
153
+ CommentUpdate {
154
+ comment_id: RemoteId,
155
+ body: String,
156
+ },
157
+ CommentDelete {
158
+ comment_id: RemoteId,
159
+ },
160
+ CommentApply {
161
+ comment_id: RemoteId,
162
+ },
163
+ CommentWatch {
164
+ owner_id: String,
165
+ file_ids: Vec<RemoteId>,
166
+ },
167
+ CollabSession {
168
+ path: String,
169
+ owner_id: String,
170
+ },
171
+ CollabEnd {
172
+ path: String,
173
+ owner_id: String,
174
+ },
175
+ CondensePreflight {
176
+ project_id: RemoteId,
177
+ scope_path: Option<String>,
178
+ },
179
+ CondenseStart {
180
+ project_id: RemoteId,
181
+ scope_path: Option<String>,
182
+ },
183
+ Job {
184
+ job_id: RemoteId,
185
+ },
186
+ ProposedCondense {
187
+ project_id: RemoteId,
188
+ },
189
+ ApproveJob {
190
+ job_id: RemoteId,
191
+ },
192
+ CancelJob {
193
+ job_id: RemoteId,
194
+ },
195
+ KnowledgeContext {
196
+ cwd: String,
197
+ },
198
+ SkillMirrors,
199
+ ArchiveStatus,
200
+ SyncProject {
201
+ project_id: RemoteId,
202
+ },
203
+ Snapshot,
204
+ MoveBetweenProjects {
205
+ source: String,
206
+ destination: String,
207
+ },
208
+ Login {
209
+ base_url: String,
210
+ token: String,
211
+ local_root: String,
212
+ },
213
+ SsoConfig {
214
+ base_url: String,
215
+ },
216
+ SsoLogin {
217
+ base_url: String,
218
+ local_root: String,
219
+ },
220
+ ResolveRootOwnership {
221
+ generation: u32,
222
+ accept: bool,
223
+ },
224
+ Logout,
225
+ Start,
226
+ Stop,
227
+ MaxAutoDownloadMb,
228
+ SetMaxAutoDownloadMb {
229
+ megabytes: u64,
230
+ },
231
+ Pause {
232
+ paused: bool,
233
+ },
234
+ Sync,
235
+ Roots,
236
+ Pin {
237
+ project_id: RemoteId,
238
+ pinned: bool,
239
+ },
240
+ Land {
241
+ project_id: RemoteId,
242
+ },
243
+ File {
244
+ path: String,
245
+ },
246
+ Download {
247
+ project_id: RemoteId,
248
+ path: String,
249
+ },
250
+ ConfirmDeletes {
251
+ project_id: RemoteId,
252
+ },
253
+ CancelDeletes {
254
+ project_id: RemoteId,
255
+ },
256
+ Templates,
257
+ Departments,
258
+ CreateProject {
259
+ request: CreateProjectFromTemplateRequest,
260
+ },
261
+ Shared,
262
+ FailedUploads,
263
+ RetryUploads,
264
+ Reconcile {
265
+ project_id: Option<RemoteId>,
266
+ },
267
+ RecheckCloudOnly,
268
+ DiskUsage {
269
+ limit: u32,
270
+ },
271
+ CleanupScan,
272
+ Cleanup {
273
+ paths: Vec<String>,
274
+ },
275
+ OwnershipGuard,
276
+ SyncAllPersonal {
277
+ enabled: bool,
278
+ },
279
+ GrantTarget {
280
+ path: String,
281
+ },
282
+ Grants {
283
+ target: GrantTarget,
284
+ },
285
+ SetGrant {
286
+ target: GrantTarget,
287
+ request: SetGrantRequest,
288
+ },
289
+ RemoveGrant {
290
+ target: GrantTarget,
291
+ subject_type: String,
292
+ subject_id: RemoteId,
293
+ },
294
+ SearchUsers {
295
+ query: String,
296
+ },
297
+ SearchDepartments {
298
+ query: String,
299
+ },
300
+ RequestAccess {
301
+ request_type: AccessRequestType,
302
+ target_id: RemoteId,
303
+ message: Option<String>,
304
+ },
305
+ SaveVersion {
306
+ path: String,
307
+ },
308
+ LocalPath {
309
+ file_id: RemoteId,
310
+ },
311
+ }
312
+
313
+ #[derive(Serialize, Type, Deserialize)]
314
+ #[serde(tag = "type", content = "data", rename_all = "camelCase")]
315
+ pub enum DriveResponse {
316
+ RootOwnershipConfirmation {
317
+ generation: u32,
318
+ #[serde(rename = "localRoot")]
319
+ local_root: String,
320
+ },
321
+ ArchivePrivateSessions(Vec<String>),
322
+ Preferences(Preferences),
323
+ Configuration(Configuration),
324
+ ClientConfig(ClientConfig),
325
+ McpSessionConfig(Option<McpSessionConfig>),
326
+ CloudFolders(Vec<crate::sync::CloudFolder>),
327
+ CommentContext(Option<CommentContext>),
328
+ Comments(Vec<FileComment>),
329
+ Comment(FileComment),
330
+ CollabSession(Option<CollabSession>),
331
+ Unsynced(UnsyncedSummary),
332
+ Job(Job),
333
+ Jobs(Vec<Job>),
334
+ Text(Option<String>),
335
+ SkillMirrors(Vec<SkillMirror>),
336
+ ArchiveStatus(ArchiveStatus),
337
+ Count(u32),
338
+ MaxAutoDownloadMb(u64),
339
+ Snapshot(Snapshot),
340
+ SsoConfig(SsoConfig),
341
+ Roots(Vec<RootInfo>),
342
+ Root(RootInfo),
343
+ File(Option<FileStateEvent>),
344
+ Templates(Vec<ProjectTemplate>),
345
+ Departments(Vec<Department>),
346
+ Shared(Vec<SharedItem>),
347
+ FailedUploads(Vec<FailedUpload>),
348
+ DiskUsage(DiskUsage),
349
+ Cleanup(CleanupReport),
350
+ OwnershipGuard(OwnershipGuard),
351
+ GrantTarget(Option<GrantTarget>),
352
+ Grants(GrantList),
353
+ GrantWrite(GrantWriteOutcome),
354
+ Users(Vec<UserBrief>),
355
+ AccessRequest(AccessRequestOutcome),
356
+ Id(Option<RemoteId>),
357
+ Path(Option<String>),
358
+ Done,
359
+ }
360
+
361
+ #[derive(Clone, Default, Serialize, Deserialize, Type)]
362
+ #[serde(rename_all = "camelCase")]
363
+ pub struct Preferences {
364
+ pub session_archive: bool,
365
+ pub knowledge_inject: bool,
366
+ }
@@ -0,0 +1,5 @@
1
+ //! Public Drive wire types and native service transport.
2
+ pub mod contract;
3
+ pub mod ownership;
4
+ pub mod service;
5
+ pub mod sync;
@@ -0,0 +1,4 @@
1
+ use std::path::{Path, PathBuf};
2
+ pub fn canonical_target(path: &Path) -> Result<PathBuf, String> {
3
+ alwith_native_client::Library::module("drive")?.call("drive.canonical_target", serde_json::json!({"path":path}))
4
+ }
@@ -0,0 +1,140 @@
1
+ use crate::contract::{DriveEvent, DriveRequest, DriveResponse, Snapshot};
2
+ use alwith_native_client::{EventReader, Handle};
3
+ use serde_json::{Value, json};
4
+ use std::{
5
+ path::{Path, PathBuf},
6
+ sync::Arc,
7
+ };
8
+ use tokio::sync::broadcast;
9
+ pub type UrlOpener = Arc<dyn Fn(&str) -> Result<(), String> + Send + Sync>;
10
+ pub struct ServiceOptions {
11
+ pub data_directory: PathBuf,
12
+ pub ownership_directory: PathBuf,
13
+ pub default_root: PathBuf,
14
+ pub reserved_roots: Vec<PathBuf>,
15
+ pub device_name: String,
16
+ pub client_version: String,
17
+ pub open_url: UrlOpener,
18
+ }
19
+ pub struct DriveService {
20
+ handle: Handle,
21
+ events: broadcast::Sender<DriveEvent>,
22
+ _reader: EventReader,
23
+ }
24
+ struct Transaction {
25
+ handle: Handle,
26
+ id: String,
27
+ finished: bool,
28
+ }
29
+ impl Drop for Transaction {
30
+ fn drop(&mut self) {
31
+ if !self.finished {
32
+ self.handle
33
+ .call::<()>("transaction_cancel", json!({"transaction_id":self.id}))
34
+ .expect("Native Drive transaction cancellation failed");
35
+ }
36
+ }
37
+ }
38
+ impl DriveService {
39
+ pub fn new(options: ServiceOptions) -> Result<Self, String> {
40
+ let handle = Handle::open(
41
+ "drive",
42
+ json!({"data_directory":options.data_directory,"ownership_directory":options.ownership_directory,"default_root":options.default_root,"reserved_roots":options.reserved_roots,"device_name":options.device_name,"client_version":options.client_version}),
43
+ )?;
44
+ let (events, _) = broadcast::channel(1024);
45
+ let sender = events.clone();
46
+ let replies = handle.clone();
47
+ let reader = EventReader::start::<Value>(handle.clone(), "next", "shutdown", move |event| {
48
+ let event = event.expect("Native Drive event stream failed");
49
+ match event["type"].as_str() {
50
+ Some("event") => {
51
+ let _ = sender
52
+ .send(serde_json::from_value(event["event"].clone()).expect("Invalid Drive event contract"));
53
+ }
54
+ Some("open_url") => {
55
+ let url = event["url"].as_str().expect("Invalid Drive URL request");
56
+ let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| (options.open_url)(url)))
57
+ .unwrap_or_else(|_| Err("Host URL opener panicked".into()));
58
+ if let Err(error) =
59
+ replies.call::<()>("reply", json!({"request_id":event["request_id"],"result":result}))
60
+ {
61
+ eprintln!("Native Drive URL response failed: {error}");
62
+ }
63
+ }
64
+ _ => panic!("Unknown Drive host event"),
65
+ }
66
+ })?;
67
+ Ok(Self { handle, events, _reader: reader })
68
+ }
69
+ pub fn with_existing_default_root(self) -> Self {
70
+ self.handle.call::<()>("adopt_existing_default_root", json!({})).expect("Native Drive root adoption failed");
71
+ self
72
+ }
73
+ pub async fn set_skills_directory(&self, directory: PathBuf) -> Result<(), String> {
74
+ self.handle.call_async("set_skills_directory", json!({"directory":directory})).await
75
+ }
76
+ pub fn snapshot(&self) -> Snapshot {
77
+ self.handle.call("snapshot", json!({})).expect("Native Drive snapshot failed")
78
+ }
79
+ pub fn subscribe(&self) -> broadcast::Receiver<DriveEvent> {
80
+ self.events.subscribe()
81
+ }
82
+ pub async fn execute(&self, request: DriveRequest) -> Result<DriveResponse, String> {
83
+ self.handle.call_async("execute", json!({"request":request})).await
84
+ }
85
+ pub async fn move_between_projects(&self, source: &Path, destination: &Path) -> Result<bool, String> {
86
+ self.handle.call_async("move_between_projects", json!({"source":source,"destination":destination})).await
87
+ }
88
+ pub async fn with_move_to_filesystem<T, F>(
89
+ &self, sources: &[PathBuf], directory: &Path, operation: F,
90
+ ) -> Result<T, String>
91
+ where
92
+ F: std::future::Future<Output = Result<T, String>>,
93
+ {
94
+ self.transaction(
95
+ json!({"writes":[],"removed":[],"collaborative":null,"move_target":[sources,directory]}),
96
+ operation,
97
+ )
98
+ .await
99
+ }
100
+ pub async fn move_to_cloud_directory(
101
+ &self, sources: &[PathBuf], directory: &Path,
102
+ ) -> Result<(usize, Option<String>), String> {
103
+ self.handle.call_async("move_to_cloud_directory", json!({"sources":sources,"directory":directory})).await
104
+ }
105
+ pub async fn shutdown(&self) -> Result<(), String> {
106
+ self.handle.call_async("shutdown", json!({})).await
107
+ }
108
+ async fn transaction<T>(
109
+ &self, params: Value, operation: impl std::future::Future<Output = Result<T, String>>,
110
+ ) -> Result<T, String> {
111
+ // Allocate synchronously before the first await so cancellation always owns a revocable handle.
112
+ let id = self.handle.call("transaction_open", params)?;
113
+ let mut transaction = Transaction { handle: self.handle.clone(), id, finished: false };
114
+ self.handle.call_async::<()>("transaction_wait", json!({"transaction_id":transaction.id})).await?;
115
+ let result = operation.await;
116
+ let outcome = result.as_ref().map(|_| ()).map_err(Clone::clone);
117
+ // Finishing synchronously prevents cancellation between commit and handle retirement.
118
+ let finish =
119
+ self.handle.call::<()>("transaction_finish", json!({"transaction_id":transaction.id,"result":outcome}));
120
+ transaction.finished = true;
121
+ finish?;
122
+ result
123
+ }
124
+ pub async fn with_filesystem<T, F>(
125
+ &self, writes: &[PathBuf], removed: &[PathBuf], operation: F,
126
+ ) -> Result<T, String>
127
+ where
128
+ F: std::future::Future<Output = Result<T, String>>,
129
+ {
130
+ self.transaction(json!({"writes":writes,"removed":removed,"collaborative":null}), operation).await
131
+ }
132
+ pub async fn with_collaborative_filesystem<T, F>(
133
+ &self, path: &Path, owner_id: &str, operation: F,
134
+ ) -> Result<T, String>
135
+ where
136
+ F: std::future::Future<Output = Result<T, String>>,
137
+ {
138
+ self.transaction(json!({"writes":[],"removed":[],"collaborative":[path,owner_id]}), operation).await
139
+ }
140
+ }