@bobfrankston/mailx 1.0.392 → 1.0.394
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/#build-apk.cmd.asv# +4 -0
- package/build-apk.cmd +3 -0
- package/client/app.js +6 -5
- package/client/components/message-list.js +29 -7
- package/client/compose/compose.js +12 -5
- package/package.json +1 -1
- package/packages/mailx-service/index.js +3 -1
- package/packages/mailx-store/db.js +17 -1
- package/packages/mailx-store-web/android-bootstrap.js +28 -10
- package/packages/mailx-store-web/sync-manager.js +39 -10
- package/rebuild.cmd +23 -0
- package/unbash.cmd +55 -0
package/build-apk.cmd
ADDED
package/client/app.js
CHANGED
|
@@ -1573,11 +1573,12 @@ onWsEvent((event) => {
|
|
|
1573
1573
|
}
|
|
1574
1574
|
break;
|
|
1575
1575
|
case "folderCountsChanged": {
|
|
1576
|
-
//
|
|
1577
|
-
//
|
|
1578
|
-
//
|
|
1579
|
-
//
|
|
1580
|
-
|
|
1576
|
+
// Incremental update only — updateFolderCounts patches badge counts
|
|
1577
|
+
// in-place and falls back to a full refreshFolderTree() when the
|
|
1578
|
+
// folder structure has actually changed. Calling both was doing a
|
|
1579
|
+
// 300 ms debounced rebuild on every sync tick even when just the
|
|
1580
|
+
// unread count moved — visible as folder-tree flicker on Dovecot
|
|
1581
|
+
// accounts where STATUS polls fire frequently.
|
|
1581
1582
|
updateFolderCounts();
|
|
1582
1583
|
updateNewMessageCount();
|
|
1583
1584
|
// Debounced silent reload — preserves scroll position, selection, and viewer
|
|
@@ -24,6 +24,16 @@ let touchWasScroll = false;
|
|
|
24
24
|
// (text columns default asc, date defaults desc).
|
|
25
25
|
let currentSort = "date";
|
|
26
26
|
let currentSortDir = "desc";
|
|
27
|
+
/** Atomic focus: update shared state + notify viewer in one call.
|
|
28
|
+
* First slice of S56 (row-objects-own-preview) — consolidates the two
|
|
29
|
+
* parallel selection paths (state.select + onMessageSelect) so the eventual
|
|
30
|
+
* Row-class migration touches exactly one call site. The viewer's `gen`
|
|
31
|
+
* token still cancels stale fetches; this just makes the transition
|
|
32
|
+
* indivisible at the caller level. */
|
|
33
|
+
function focusMessage(accountId, msg) {
|
|
34
|
+
state.select(msg);
|
|
35
|
+
onMessageSelect(accountId, msg.uid, msg.folderId);
|
|
36
|
+
}
|
|
27
37
|
/** Flip the "not-downloaded" indicator off for rows whose bodies just cached.
|
|
28
38
|
* Called from the bodyCached service event — covers both background prefetch
|
|
29
39
|
* and on-demand fetch. No-op for rows not currently rendered. */
|
|
@@ -473,8 +483,7 @@ export async function showThreadPopup(pillEl, headMsg) {
|
|
|
473
483
|
item.appendChild(date);
|
|
474
484
|
item.appendChild(subject);
|
|
475
485
|
item.addEventListener("click", async () => {
|
|
476
|
-
|
|
477
|
-
onMessageSelect(msg.accountId, msg.uid, msg.folderId);
|
|
486
|
+
focusMessage(msg.accountId, { accountId: msg.accountId, uid: msg.uid, folderId: msg.folderId, subject: msg.subject, from: msg.from, to: msg.to, cc: msg.cc, date: msg.date, flags: msg.flags, size: msg.size, preview: msg.preview, hasAttachments: msg.hasAttachments });
|
|
478
487
|
popup.remove();
|
|
479
488
|
});
|
|
480
489
|
popup.appendChild(item);
|
|
@@ -640,9 +649,7 @@ function appendMessages(body, accountId, items) {
|
|
|
640
649
|
}
|
|
641
650
|
lastClickedRow = row;
|
|
642
651
|
row.classList.remove("unread");
|
|
643
|
-
|
|
644
|
-
state.select(msg);
|
|
645
|
-
onMessageSelect(msgAccountId, msg.uid, msg.folderId);
|
|
652
|
+
focusMessage(msgAccountId, msg);
|
|
646
653
|
});
|
|
647
654
|
// Q64: double-click → pop out the message in a floating overlay so
|
|
648
655
|
// the user can read it without losing the selected list context.
|
|
@@ -718,8 +725,7 @@ function appendMessages(body, accountId, items) {
|
|
|
718
725
|
clearSelection();
|
|
719
726
|
row.classList.add("selected");
|
|
720
727
|
lastClickedRow = row;
|
|
721
|
-
|
|
722
|
-
onMessageSelect(msgAccountId, msg.uid, msg.folderId);
|
|
728
|
+
focusMessage(msgAccountId, msg);
|
|
723
729
|
}
|
|
724
730
|
const isSeen = msg.flags.includes("\\Seen");
|
|
725
731
|
const isFlagged = msg.flags.includes("\\Flagged");
|
|
@@ -793,6 +799,22 @@ function appendMessages(body, accountId, items) {
|
|
|
793
799
|
label: "Delete",
|
|
794
800
|
action: () => document.dispatchEvent(new CustomEvent("mailx-delete")),
|
|
795
801
|
},
|
|
802
|
+
{ label: "", action: () => { }, separator: true },
|
|
803
|
+
{
|
|
804
|
+
label: "Copy Message-ID",
|
|
805
|
+
action: async () => {
|
|
806
|
+
// Useful when asking "where did my letter go?" — pair the
|
|
807
|
+
// Message-ID with the reconcile-delete log line.
|
|
808
|
+
if (!msg.messageId) {
|
|
809
|
+
alert("No Message-ID on this row.");
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
try {
|
|
813
|
+
await navigator.clipboard.writeText(msg.messageId);
|
|
814
|
+
}
|
|
815
|
+
catch { /* */ }
|
|
816
|
+
},
|
|
817
|
+
},
|
|
796
818
|
];
|
|
797
819
|
showContextMenu(e.clientX, e.clientY, items);
|
|
798
820
|
});
|
|
@@ -888,23 +888,30 @@ fileInput?.addEventListener("change", async () => {
|
|
|
888
888
|
const root = document.body;
|
|
889
889
|
const overlay = document.createElement("div");
|
|
890
890
|
overlay.id = "compose-drop-overlay";
|
|
891
|
-
|
|
892
|
-
|
|
891
|
+
// Toggle `display` directly — can't use the `hidden` attribute here
|
|
892
|
+
// because the inline `display` property in cssText outranks it, which is
|
|
893
|
+
// why the overlay showed permanently when I used `overlay.hidden = true`
|
|
894
|
+
// (user-reported 2026-04-24 with screenshot — blue tint + dashed border
|
|
895
|
+
// were visible before any drag started).
|
|
896
|
+
const baseStyle = "position:fixed;inset:0;background:oklch(0.6 0.18 250 / 0.15);border:3px dashed oklch(0.55 0.2 250);z-index:9999;pointer-events:none;align-items:center;justify-content:center;font-size:1.5rem;font-weight:500;color:oklch(0.35 0.2 250)";
|
|
897
|
+
overlay.style.cssText = baseStyle + ";display:none";
|
|
893
898
|
overlay.textContent = "Drop files to attach";
|
|
894
899
|
root.appendChild(overlay);
|
|
900
|
+
const show = () => { overlay.style.display = "flex"; };
|
|
901
|
+
const hide = () => { overlay.style.display = "none"; };
|
|
895
902
|
const hasFiles = (e) => Array.from(e.dataTransfer?.types || []).includes("Files");
|
|
896
903
|
root.addEventListener("dragenter", (e) => {
|
|
897
904
|
if (!hasFiles(e))
|
|
898
905
|
return;
|
|
899
906
|
dragDepth++;
|
|
900
|
-
|
|
907
|
+
show();
|
|
901
908
|
});
|
|
902
909
|
root.addEventListener("dragleave", (e) => {
|
|
903
910
|
if (!hasFiles(e))
|
|
904
911
|
return;
|
|
905
912
|
dragDepth = Math.max(0, dragDepth - 1);
|
|
906
913
|
if (dragDepth === 0)
|
|
907
|
-
|
|
914
|
+
hide();
|
|
908
915
|
});
|
|
909
916
|
root.addEventListener("dragover", (e) => {
|
|
910
917
|
if (!hasFiles(e))
|
|
@@ -918,7 +925,7 @@ fileInput?.addEventListener("change", async () => {
|
|
|
918
925
|
return;
|
|
919
926
|
e.preventDefault();
|
|
920
927
|
dragDepth = 0;
|
|
921
|
-
|
|
928
|
+
hide();
|
|
922
929
|
const files = e.dataTransfer?.files;
|
|
923
930
|
if (files && files.length > 0)
|
|
924
931
|
await ingestFiles(files);
|
package/package.json
CHANGED
|
@@ -351,7 +351,8 @@ export class MailxService {
|
|
|
351
351
|
}
|
|
352
352
|
// ── Search ──
|
|
353
353
|
async search(q, page = 1, pageSize = 50, scope = "all", accountId, folderId) {
|
|
354
|
-
|
|
354
|
+
q = (q || "").trim();
|
|
355
|
+
if (!q)
|
|
355
356
|
return { items: [], total: 0, page, pageSize };
|
|
356
357
|
if (scope === "server") {
|
|
357
358
|
// Parse qualifiers once; SEARCH runs per folder.
|
|
@@ -1345,6 +1346,7 @@ export class MailxService {
|
|
|
1345
1346
|
}
|
|
1346
1347
|
// ── Contacts ──
|
|
1347
1348
|
searchContacts(query) {
|
|
1349
|
+
query = (query || "").trim();
|
|
1348
1350
|
if (query.length < 1)
|
|
1349
1351
|
return [];
|
|
1350
1352
|
return this.db.searchContacts(query);
|
|
@@ -168,6 +168,9 @@ const SCHEMA = `
|
|
|
168
168
|
);
|
|
169
169
|
CREATE INDEX IF NOT EXISTS idx_calendar_events_start ON calendar_events(account_id, start_ms);
|
|
170
170
|
CREATE INDEX IF NOT EXISTS idx_calendar_events_dirty ON calendar_events(dirty) WHERE dirty = 1;
|
|
171
|
+
-- getCalendarEventByProviderId runs once per event on every Google refresh;
|
|
172
|
+
-- without this index each lookup is a full table scan over calendar_events.
|
|
173
|
+
CREATE INDEX IF NOT EXISTS idx_calendar_events_provider ON calendar_events(account_id, provider_id);
|
|
171
174
|
|
|
172
175
|
-- Tasks: two-way cache of Google Tasks / local tasks. Same shape as
|
|
173
176
|
-- calendar_events minus the time range.
|
|
@@ -188,6 +191,10 @@ const SCHEMA = `
|
|
|
188
191
|
);
|
|
189
192
|
CREATE INDEX IF NOT EXISTS idx_tasks_account ON tasks(account_id);
|
|
190
193
|
CREATE INDEX IF NOT EXISTS idx_tasks_dirty ON tasks(dirty) WHERE dirty = 1;
|
|
194
|
+
-- Mirror calendar_events: any provider_id lookup path needs a proper index,
|
|
195
|
+
-- even if today's reconcile does the dedup in memory — prevents a future
|
|
196
|
+
-- refactor from accidentally introducing an O(N) scan.
|
|
197
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_provider ON tasks(account_id, provider_id);
|
|
191
198
|
|
|
192
199
|
-- Generic store-sync queue for domains OTHER than messages. Messages
|
|
193
200
|
-- use sync_actions above. This table queues push-to-server actions
|
|
@@ -208,6 +215,10 @@ const SCHEMA = `
|
|
|
208
215
|
UNIQUE(kind, target_uuid, op)
|
|
209
216
|
);
|
|
210
217
|
CREATE INDEX IF NOT EXISTS idx_store_sync_account ON store_sync(account_id, kind);
|
|
218
|
+
-- UNIQUE(kind, target_uuid, op) covers queries that start with kind; lookups
|
|
219
|
+
-- by target_uuid alone ("is this uuid queued for any op?") would otherwise
|
|
220
|
+
-- table-scan. Cheap; store_sync is tiny and write-heavy.
|
|
221
|
+
CREATE INDEX IF NOT EXISTS idx_store_sync_target_uuid ON store_sync(target_uuid);
|
|
211
222
|
`;
|
|
212
223
|
export class MailxDB {
|
|
213
224
|
db;
|
|
@@ -1023,6 +1034,9 @@ export class MailxDB {
|
|
|
1023
1034
|
}
|
|
1024
1035
|
/** Search contacts by name or email prefix */
|
|
1025
1036
|
searchContacts(query, limit = 10) {
|
|
1037
|
+
query = (query || "").trim();
|
|
1038
|
+
if (!query)
|
|
1039
|
+
return [];
|
|
1026
1040
|
// Ranking: prefix matches beat substring matches, then recency-weighted
|
|
1027
1041
|
// use_count within a tier. Recency decay: half-life of 30 days, so a
|
|
1028
1042
|
// contact used today edges out one from months ago even with a lower
|
|
@@ -1064,7 +1078,8 @@ export class MailxDB {
|
|
|
1064
1078
|
}
|
|
1065
1079
|
/** List all contacts (address-book view) with pagination + optional filter. */
|
|
1066
1080
|
listContacts(query, page = 1, pageSize = 100) {
|
|
1067
|
-
|
|
1081
|
+
query = (query || "").trim();
|
|
1082
|
+
const hasQuery = !!query;
|
|
1068
1083
|
const q = `%${query}%`;
|
|
1069
1084
|
const whereClause = hasQuery ? "WHERE email LIKE ? OR name LIKE ?" : "";
|
|
1070
1085
|
const params = hasQuery ? [q, q] : [];
|
|
@@ -1105,6 +1120,7 @@ export class MailxDB {
|
|
|
1105
1120
|
// ── Search ──
|
|
1106
1121
|
/** Full-text search across all messages. Supports qualifiers: from:, to:, subject: */
|
|
1107
1122
|
searchMessages(query, page = 1, pageSize = 50, accountId, folderId) {
|
|
1123
|
+
query = (query || "").trim();
|
|
1108
1124
|
// Parse qualifiers (C45: extended set — date:, has:, is:, folder:).
|
|
1109
1125
|
let ftsQuery = "";
|
|
1110
1126
|
const parts = query.match(/(?:[^\s"]+|"[^"]*")+/g) || [];
|
|
@@ -365,9 +365,13 @@ class AndroidSyncManager {
|
|
|
365
365
|
}
|
|
366
366
|
}
|
|
367
367
|
async fetchMessageBody(accountId, folderId, uid) {
|
|
368
|
+
const t0 = Date.now();
|
|
368
369
|
if (await this.bodyStore.hasMessage(accountId, folderId, uid)) {
|
|
369
|
-
|
|
370
|
+
const cached = await this.bodyStore.getMessage(accountId, folderId, uid);
|
|
371
|
+
console.log(`[fetchBody] cache hit ${accountId}/${folderId}/${uid} (${Date.now() - t0}ms)`);
|
|
372
|
+
return cached;
|
|
370
373
|
}
|
|
374
|
+
console.log(`[fetchBody] cache miss ${accountId}/${folderId}/${uid} — fetching`);
|
|
371
375
|
const provider = this.getProvider(accountId);
|
|
372
376
|
if (!provider) {
|
|
373
377
|
console.warn(`[fetchBody] No provider for ${accountId}`);
|
|
@@ -376,27 +380,41 @@ class AndroidSyncManager {
|
|
|
376
380
|
// Look up the Gmail providerId stored in body_path during sync
|
|
377
381
|
const envelope = this.db.getMessageByUid(accountId, uid, folderId);
|
|
378
382
|
const bp = envelope?.bodyPath || "";
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
383
|
+
// 60 s wall-clock cap — infinite hang was the user-reported symptom
|
|
384
|
+
// ("fetch message body on android is infinite"). A dead BridgeTransport
|
|
385
|
+
// socket won't recover by waiting. Legit fetches finish in seconds.
|
|
386
|
+
const FETCH_TIMEOUT_MS = 60_000;
|
|
387
|
+
const fetchPromise = (async () => {
|
|
388
|
+
if (bp.startsWith("gmail:") && provider.fetchById) {
|
|
389
|
+
const providerId = bp.substring(6);
|
|
390
|
+
return provider.fetchById(providerId, { source: true });
|
|
391
|
+
}
|
|
386
392
|
const folders = this.db.getFolders(accountId);
|
|
387
393
|
const folder = folders.find(f => f.id === folderId);
|
|
388
394
|
if (!folder)
|
|
389
395
|
return null;
|
|
390
|
-
|
|
396
|
+
return provider.fetchOne(folder.path, uid, { source: true });
|
|
397
|
+
})();
|
|
398
|
+
let msg = null;
|
|
399
|
+
try {
|
|
400
|
+
msg = await Promise.race([
|
|
401
|
+
fetchPromise,
|
|
402
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`body-fetch timeout ${FETCH_TIMEOUT_MS / 1000}s (${accountId}/${folderId}/${uid})`)), FETCH_TIMEOUT_MS)),
|
|
403
|
+
]);
|
|
404
|
+
}
|
|
405
|
+
catch (e) {
|
|
406
|
+
console.error(`[fetchBody] failed ${accountId}/${folderId}/${uid} after ${Date.now() - t0}ms: ${e?.message || e}`);
|
|
407
|
+
throw e;
|
|
391
408
|
}
|
|
392
409
|
if (!msg?.source) {
|
|
393
|
-
console.warn(`[fetchBody] No source returned for ${accountId}/${folderId}/${uid} (bp=${bp})`);
|
|
410
|
+
console.warn(`[fetchBody] No source returned for ${accountId}/${folderId}/${uid} (bp=${bp}, ${Date.now() - t0}ms)`);
|
|
394
411
|
return null;
|
|
395
412
|
}
|
|
396
413
|
// Encode the UTF-8 string back to bytes for storage
|
|
397
414
|
const raw = new TextEncoder().encode(msg.source);
|
|
398
415
|
await this.bodyStore.putMessage(accountId, folderId, uid, raw);
|
|
399
416
|
this.db.updateBodyPath(accountId, uid, `idb:${accountId}/${folderId}/${uid}`);
|
|
417
|
+
console.log(`[fetchBody] fetched + cached ${accountId}/${folderId}/${uid} (${raw.byteLength} bytes, ${Date.now() - t0}ms)`);
|
|
400
418
|
return raw;
|
|
401
419
|
}
|
|
402
420
|
async updateFlagsLocal(accountId, uid, folderId, flags) {
|
|
@@ -241,33 +241,62 @@ export class SyncManager {
|
|
|
241
241
|
}
|
|
242
242
|
}
|
|
243
243
|
async fetchMessageBody(accountId, folderId, uid) {
|
|
244
|
+
const t0 = Date.now();
|
|
245
|
+
// Cache first — IndexedDB lookup is O(1) and should always win on a
|
|
246
|
+
// previously-fetched body. If this path misses on something the user
|
|
247
|
+
// clearly fetched before, the cache is broken (wrong key shape, wiped
|
|
248
|
+
// IndexedDB, account-id change) and needs investigation — log so it's
|
|
249
|
+
// visible.
|
|
244
250
|
if (await this.bodyStore.hasMessage(accountId, folderId, uid)) {
|
|
245
|
-
|
|
251
|
+
const cached = await this.bodyStore.getMessage(accountId, folderId, uid);
|
|
252
|
+
console.log(`[fetchBody] cache hit ${accountId}/${folderId}/${uid} (${Date.now() - t0}ms)`);
|
|
253
|
+
return cached;
|
|
246
254
|
}
|
|
255
|
+
console.log(`[fetchBody] cache miss ${accountId}/${folderId}/${uid} — fetching from provider`);
|
|
247
256
|
const provider = this.getProvider(accountId);
|
|
248
|
-
if (!provider)
|
|
257
|
+
if (!provider) {
|
|
258
|
+
console.warn(`[fetchBody] no provider for ${accountId}`);
|
|
249
259
|
return null;
|
|
260
|
+
}
|
|
250
261
|
const envelope = this.db.getMessageByUid(accountId, uid, folderId);
|
|
251
262
|
const bp = envelope?.bodyPath || "";
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
263
|
+
// Wall-clock timeout — without this, an IMAP provider that dangles
|
|
264
|
+
// (Dovecot silently dropped the socket, BridgeTransport stalled) hung
|
|
265
|
+
// the viewer forever. 60 s is generous for a single-message BODY[]
|
|
266
|
+
// fetch on a phone connection; legit large bodies finish in seconds,
|
|
267
|
+
// and anything longer means the socket is dead and retrying on a fresh
|
|
268
|
+
// one is faster than waiting.
|
|
269
|
+
const FETCH_TIMEOUT_MS = 60_000;
|
|
270
|
+
const fetchPromise = (async () => {
|
|
271
|
+
if (bp.startsWith("gmail:") && provider.fetchById) {
|
|
272
|
+
const providerId = bp.substring(6);
|
|
273
|
+
return provider.fetchById(providerId, { source: true });
|
|
274
|
+
}
|
|
258
275
|
const folders = this.db.getFolders(accountId);
|
|
259
276
|
const folder = folders.find((f) => f.id === folderId);
|
|
260
277
|
if (!folder)
|
|
261
278
|
return null;
|
|
262
|
-
|
|
279
|
+
return provider.fetchOne(folder.path, uid, { source: true });
|
|
280
|
+
})();
|
|
281
|
+
let msg = null;
|
|
282
|
+
try {
|
|
283
|
+
msg = await Promise.race([
|
|
284
|
+
fetchPromise,
|
|
285
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`body-fetch timeout ${FETCH_TIMEOUT_MS / 1000}s (${accountId}/${folderId}/${uid})`)), FETCH_TIMEOUT_MS)),
|
|
286
|
+
]);
|
|
287
|
+
}
|
|
288
|
+
catch (e) {
|
|
289
|
+
console.error(`[fetchBody] failed ${accountId}/${folderId}/${uid} after ${Date.now() - t0}ms: ${e?.message || e}`);
|
|
290
|
+
throw e;
|
|
263
291
|
}
|
|
264
292
|
if (!msg?.source) {
|
|
265
|
-
console.warn(`[fetchBody] No source returned for ${accountId}/${folderId}/${uid} (bp=${bp})`);
|
|
293
|
+
console.warn(`[fetchBody] No source returned for ${accountId}/${folderId}/${uid} (bp=${bp}, ${Date.now() - t0}ms)`);
|
|
266
294
|
return null;
|
|
267
295
|
}
|
|
268
296
|
const raw = new TextEncoder().encode(msg.source);
|
|
269
297
|
await this.bodyStore.putMessage(accountId, folderId, uid, raw);
|
|
270
298
|
this.db.updateBodyPath(accountId, uid, `idb:${accountId}/${folderId}/${uid}`);
|
|
299
|
+
console.log(`[fetchBody] fetched + cached ${accountId}/${folderId}/${uid} (${raw.byteLength} bytes, ${Date.now() - t0}ms)`);
|
|
271
300
|
return raw;
|
|
272
301
|
}
|
|
273
302
|
async updateFlagsLocal(accountId, uid, folderId, flags) {
|
package/rebuild.cmd
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
@echo off
|
|
2
|
+
REM ─────────────────────────────────────────────────────────────────────────
|
|
3
|
+
REM rebuild.cmd — one-shot release: npmglobalize + APK.
|
|
4
|
+
REM Runs the desktop release workflow (npmglobalize commits, tags, bumps,
|
|
5
|
+
REM publishes the npm package) and, only if that succeeds, rebuilds the
|
|
6
|
+
REM Android MAUI APK + copies it into download/apks/ + updates versions.json.
|
|
7
|
+
REM Anything non-zero from npmglobalize aborts before the APK stage — no
|
|
8
|
+
REM point building an APK against a failed publish.
|
|
9
|
+
REM ─────────────────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
cls
|
|
12
|
+
setlocal
|
|
13
|
+
cd /d %~dp0
|
|
14
|
+
|
|
15
|
+
call npmglobalize
|
|
16
|
+
if errorlevel 1 (
|
|
17
|
+
echo [rebuild] npmglobalize failed with errorlevel %ERRORLEVEL% — skipping build-apk
|
|
18
|
+
endlocal
|
|
19
|
+
exit /b %ERRORLEVEL%
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
call "%~dp0build-apk.cmd"
|
|
23
|
+
endlocal
|
package/unbash.cmd
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
@echo off
|
|
2
|
+
REM ─────────────────────────────────────────────────────────────────────────
|
|
3
|
+
REM unbash.cmd — surgically remove a stuck session-env subdir so Claude
|
|
4
|
+
REM Code's Bash tool can recreate it. One subdir per concurrent Claude
|
|
5
|
+
REM session; only nuke the stuck one, not all of them.
|
|
6
|
+
REM
|
|
7
|
+
REM Usage:
|
|
8
|
+
REM unbash.cmd List existing session-env subdirs.
|
|
9
|
+
REM unbash.cmd <uuid> Remove that specific subdir.
|
|
10
|
+
REM unbash.cmd stale Remove subdirs with no writes in >60 min
|
|
11
|
+
REM (abandoned from crashed sessions).
|
|
12
|
+
REM unbash.cmd all Remove every subdir (nuclear — kills
|
|
13
|
+
REM state of live concurrent sessions too).
|
|
14
|
+
REM ─────────────────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
setlocal ENABLEEXTENSIONS
|
|
17
|
+
set BASE=%USERPROFILE%\.claude\session-env
|
|
18
|
+
|
|
19
|
+
if not exist "%BASE%" (
|
|
20
|
+
echo [unbash] No session-env directory at %BASE% — nothing to do.
|
|
21
|
+
endlocal
|
|
22
|
+
exit /b 0
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
if "%~1"=="" (
|
|
26
|
+
echo [unbash] Subdirs under %BASE%:
|
|
27
|
+
dir /b /ad "%BASE%" 2>nul
|
|
28
|
+
echo.
|
|
29
|
+
echo [unbash] Pass a UUID to remove just one, 'stale' to remove old ones,
|
|
30
|
+
echo or 'all' to nuke everything ^(kills live sessions too^).
|
|
31
|
+
endlocal
|
|
32
|
+
exit /b 0
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
if /I "%~1"=="all" (
|
|
36
|
+
rmdir /s /q "%BASE%"
|
|
37
|
+
echo [unbash] Removed all session-env subdirs.
|
|
38
|
+
endlocal
|
|
39
|
+
exit /b 0
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
if /I "%~1"=="stale" (
|
|
43
|
+
powershell -NoProfile -Command "Get-ChildItem -Directory '%BASE%' | Where-Object { $_.LastWriteTime -lt (Get-Date).AddMinutes(-60) } | ForEach-Object { Write-Host ('[unbash] removing stale ' + $_.Name); Remove-Item -Recurse -Force $_.FullName }"
|
|
44
|
+
endlocal
|
|
45
|
+
exit /b 0
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
if not exist "%BASE%\%~1" (
|
|
49
|
+
echo [unbash] No subdir %~1 under %BASE%.
|
|
50
|
+
endlocal
|
|
51
|
+
exit /b 1
|
|
52
|
+
)
|
|
53
|
+
rmdir /s /q "%BASE%\%~1"
|
|
54
|
+
echo [unbash] Removed %BASE%\%~1
|
|
55
|
+
endlocal
|