@cardstack/boxel-cli 0.1.0 → 0.1.1
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/dist/index.js +48 -48
- package/package.json +5 -3
- package/src/build-program.ts +91 -0
- package/src/commands/realm/push.ts +27 -11
- package/src/commands/realm/sync.ts +7 -0
- package/src/index.ts +3 -83
- package/src/lib/boxel-cli-client.ts +11 -0
- package/src/lib/realm-sync-base.ts +76 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cardstack/boxel-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "CLI tools for Boxel workspace management",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -57,6 +57,8 @@
|
|
|
57
57
|
},
|
|
58
58
|
"scripts": {
|
|
59
59
|
"build": "pnpm clean && NODE_NO_WARNINGS=1 ts-node --transpileOnly scripts/build.ts",
|
|
60
|
+
"build:plugin": "NODE_NO_WARNINGS=1 ts-node --transpileOnly scripts/build-plugin.ts",
|
|
61
|
+
"build:skills": "NODE_NO_WARNINGS=1 ts-node --transpileOnly scripts/build-skills.ts",
|
|
60
62
|
"clean": "rm -rf dist/*",
|
|
61
63
|
"start": "NODE_NO_WARNINGS=1 ts-node --transpileOnly src/index.ts",
|
|
62
64
|
"lint": "concurrently \"pnpm:lint:*(!fix)\" --names \"lint:\"",
|
|
@@ -72,8 +74,8 @@
|
|
|
72
74
|
"version:patch": "npm version patch",
|
|
73
75
|
"version:minor": "npm version minor",
|
|
74
76
|
"version:major": "npm version major",
|
|
75
|
-
"publish:npm": "
|
|
76
|
-
"publish:dry": "
|
|
77
|
+
"publish:npm": "pnpm publish --no-git-checks",
|
|
78
|
+
"publish:dry": "pnpm publish --dry-run --no-git-checks"
|
|
77
79
|
},
|
|
78
80
|
"publishConfig": {
|
|
79
81
|
"access": "public",
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { profileCommand } from './commands/profile';
|
|
3
|
+
import { registerReadTranspiledCommand } from './commands/read-transpiled';
|
|
4
|
+
import { registerRealmCommand } from './commands/realm/index';
|
|
5
|
+
import { registerFileCommand } from './commands/file/index';
|
|
6
|
+
import { registerRunCommand } from './commands/run-command';
|
|
7
|
+
import { registerSearchCommand } from './commands/search';
|
|
8
|
+
import { setQuiet } from './lib/cli-log';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Construct the boxel CLI program with every command registered. Pure builder
|
|
12
|
+
* — does not call `program.parse()` and has no side effects on argv. Both the
|
|
13
|
+
* runtime entry point (`src/index.ts`) and the plugin generator
|
|
14
|
+
* (`scripts/build-plugin.ts`) call this so the Commander tree is one source of
|
|
15
|
+
* truth.
|
|
16
|
+
*/
|
|
17
|
+
export function buildBoxelProgram(version: string): Command {
|
|
18
|
+
const program = new Command();
|
|
19
|
+
|
|
20
|
+
program
|
|
21
|
+
.name('boxel')
|
|
22
|
+
.description('CLI tools for Boxel workspace management')
|
|
23
|
+
.version(version)
|
|
24
|
+
.option(
|
|
25
|
+
'-q, --quiet',
|
|
26
|
+
'Suppress informational progress logs (info/log/debug). Errors and warnings, plus command result payloads (JSON, file contents), are still emitted. Use this when invoking the CLI from automation (e.g. the software factory test harness) to keep stdout focused on the result.',
|
|
27
|
+
)
|
|
28
|
+
.hook('preAction', (thisCommand) => {
|
|
29
|
+
let opts = thisCommand.optsWithGlobals?.() ?? thisCommand.opts();
|
|
30
|
+
if (opts.quiet) {
|
|
31
|
+
setQuiet(true);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
program
|
|
36
|
+
.command('profile')
|
|
37
|
+
.description('Manage saved profiles for different users/environments')
|
|
38
|
+
.argument('[subcommand]', 'list | add | switch | remove | migrate')
|
|
39
|
+
.argument('[arg]', 'Profile ID (for switch/remove)')
|
|
40
|
+
.option('-u, --user <matrixId>', 'Matrix user ID (e.g., @user:boxel.ai)')
|
|
41
|
+
.option('-p, --password <password>', 'Password (for add command)')
|
|
42
|
+
.option('-n, --name <displayName>', 'Display name (for add command)')
|
|
43
|
+
.option(
|
|
44
|
+
'-m, --matrix-url <url>',
|
|
45
|
+
'Matrix server URL (for add command with non-standard domains)',
|
|
46
|
+
)
|
|
47
|
+
.option(
|
|
48
|
+
'-r, --realm-server-url <url>',
|
|
49
|
+
'Realm server URL (for add command with non-standard domains)',
|
|
50
|
+
)
|
|
51
|
+
.addHelpText(
|
|
52
|
+
'after',
|
|
53
|
+
`
|
|
54
|
+
Environment variables (for 'add'):
|
|
55
|
+
BOXEL_PASSWORD Password; preferred over -p to avoid shell history.
|
|
56
|
+
BOXEL_ENVIRONMENT An env-mode slug (e.g. a branch name), interpreted
|
|
57
|
+
like scripts/env-slug.sh: URLs are derived as
|
|
58
|
+
http://matrix.<slug>.localhost and
|
|
59
|
+
http://realm-server.<slug>.localhost/. Overridden
|
|
60
|
+
by --matrix-url / --realm-server-url if provided.`,
|
|
61
|
+
)
|
|
62
|
+
.action(
|
|
63
|
+
async (
|
|
64
|
+
subcommand?: string,
|
|
65
|
+
arg?: string,
|
|
66
|
+
options?: {
|
|
67
|
+
user?: string;
|
|
68
|
+
password?: string;
|
|
69
|
+
name?: string;
|
|
70
|
+
matrixUrl?: string;
|
|
71
|
+
realmServerUrl?: string;
|
|
72
|
+
},
|
|
73
|
+
) => {
|
|
74
|
+
if (options?.password) {
|
|
75
|
+
console.warn(
|
|
76
|
+
'Warning: Supplying a password via -p/--password may expose it in shell history and process listings. ' +
|
|
77
|
+
'For non-interactive usage, prefer the BOXEL_PASSWORD environment variable or use "boxel profile add" interactively.',
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
await profileCommand(subcommand, arg, options);
|
|
81
|
+
},
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
registerFileCommand(program);
|
|
85
|
+
registerRealmCommand(program);
|
|
86
|
+
registerRunCommand(program);
|
|
87
|
+
registerSearchCommand(program);
|
|
88
|
+
registerReadTranspiledCommand(program);
|
|
89
|
+
|
|
90
|
+
return program;
|
|
91
|
+
}
|
|
@@ -25,6 +25,11 @@ interface PushOptions extends SyncOptions {
|
|
|
25
25
|
force?: boolean;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
// Fresh realms always include these server-managed cards even when the local
|
|
29
|
+
// workspace has never pulled them. Treat them as realm artifacts, not user
|
|
30
|
+
// drift, so `push --delete` only removes genuine remote-only user files.
|
|
31
|
+
const REMOTE_DELETE_EXCLUSIONS = new Set(['index.json', 'realm.json']);
|
|
32
|
+
|
|
28
33
|
class RealmPusher extends RealmSyncBase {
|
|
29
34
|
hasError = false;
|
|
30
35
|
|
|
@@ -225,10 +230,16 @@ class RealmPusher extends RealmSyncBase {
|
|
|
225
230
|
|
|
226
231
|
if (this.pushOptions.deleteRemote) {
|
|
227
232
|
const filesToDelete = new Set(initialRemoteFiles.keys());
|
|
233
|
+
const skippedDeleteArtifacts: string[] = [];
|
|
228
234
|
|
|
229
235
|
for (const relativePath of filesToDelete) {
|
|
230
236
|
if (isProtectedFile(relativePath)) {
|
|
231
237
|
filesToDelete.delete(relativePath);
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (REMOTE_DELETE_EXCLUSIONS.has(relativePath)) {
|
|
241
|
+
filesToDelete.delete(relativePath);
|
|
242
|
+
skippedDeleteArtifacts.push(relativePath);
|
|
232
243
|
}
|
|
233
244
|
}
|
|
234
245
|
|
|
@@ -236,21 +247,26 @@ class RealmPusher extends RealmSyncBase {
|
|
|
236
247
|
filesToDelete.delete(relativePath);
|
|
237
248
|
}
|
|
238
249
|
|
|
239
|
-
if (
|
|
250
|
+
if (skippedDeleteArtifacts.length > 0) {
|
|
240
251
|
console.log(
|
|
241
|
-
`
|
|
252
|
+
`Skipping ${skippedDeleteArtifacts.length} realm-managed remote artifact(s): ${skippedDeleteArtifacts.join(', ')}`,
|
|
242
253
|
);
|
|
254
|
+
}
|
|
243
255
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
} catch (error) {
|
|
249
|
-
this.hasError = true;
|
|
250
|
-
console.error(`Error deleting ${relativePath}:`, error);
|
|
251
|
-
}
|
|
252
|
-
}),
|
|
256
|
+
if (filesToDelete.size > 0) {
|
|
257
|
+
const deletePlan = Array.from(filesToDelete).sort();
|
|
258
|
+
console.log(
|
|
259
|
+
`Deleting ${deletePlan.length} remote files that don't exist locally: ${deletePlan.join(', ')}`,
|
|
253
260
|
);
|
|
261
|
+
|
|
262
|
+
for (const relativePath of deletePlan) {
|
|
263
|
+
try {
|
|
264
|
+
await this.deleteFile(relativePath);
|
|
265
|
+
} catch (error) {
|
|
266
|
+
this.hasError = true;
|
|
267
|
+
console.error(`Error deleting ${relativePath}:`, error);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
254
270
|
}
|
|
255
271
|
}
|
|
256
272
|
|
|
@@ -489,6 +489,12 @@ export interface SyncCommandOptions {
|
|
|
489
489
|
preferNewest?: boolean;
|
|
490
490
|
delete?: boolean;
|
|
491
491
|
dryRun?: boolean;
|
|
492
|
+
/**
|
|
493
|
+
* Append `?waitForIndex=true` to the `_atomic` upload so the
|
|
494
|
+
* realm-server returns only after the indexer has processed the
|
|
495
|
+
* batch. See `SyncOptions.waitForIndex` for the rationale.
|
|
496
|
+
*/
|
|
497
|
+
waitForIndex?: boolean;
|
|
492
498
|
profileManager?: ProfileManager;
|
|
493
499
|
/**
|
|
494
500
|
* Pre-resolved realm secret seed for administrative access. When set, the
|
|
@@ -629,6 +635,7 @@ export async function sync(
|
|
|
629
635
|
preferNewest: options.preferNewest,
|
|
630
636
|
deleteSync: options.delete,
|
|
631
637
|
dryRun: options.dryRun,
|
|
638
|
+
waitForIndex: options.waitForIndex,
|
|
632
639
|
},
|
|
633
640
|
authenticator,
|
|
634
641
|
);
|
package/src/index.ts
CHANGED
|
@@ -1,44 +1,19 @@
|
|
|
1
1
|
import 'dotenv/config';
|
|
2
|
-
import { Command } from 'commander';
|
|
3
2
|
import { readFileSync } from 'fs';
|
|
4
3
|
import { resolve } from 'path';
|
|
5
|
-
import {
|
|
6
|
-
import { registerReadTranspiledCommand } from './commands/read-transpiled';
|
|
7
|
-
import { registerRealmCommand } from './commands/realm/index';
|
|
8
|
-
import { registerFileCommand } from './commands/file/index';
|
|
9
|
-
import { registerRunCommand } from './commands/run-command';
|
|
10
|
-
import { registerSearchCommand } from './commands/search';
|
|
4
|
+
import { buildBoxelProgram } from './build-program';
|
|
11
5
|
import { setQuiet } from './lib/cli-log';
|
|
12
6
|
|
|
13
7
|
const pkg = JSON.parse(
|
|
14
8
|
readFileSync(resolve(__dirname, '../package.json'), 'utf-8'),
|
|
15
9
|
);
|
|
16
10
|
|
|
17
|
-
const program = new Command();
|
|
18
|
-
|
|
19
11
|
// `--quiet` is implemented by intercepting `console.log/info/debug`.
|
|
20
12
|
// New commands: write decorative output (status, confirmations, colored
|
|
21
13
|
// lines) with `console.log` — it's silenced for free under `--quiet`.
|
|
22
14
|
// For programmatic output (`--json` payloads, raw file bytes), use
|
|
23
15
|
// `cliLog.output(...)`. Full guidance: see `lib/cli-log.ts`.
|
|
24
|
-
|
|
25
|
-
.name('boxel')
|
|
26
|
-
.description('CLI tools for Boxel workspace management')
|
|
27
|
-
.version(pkg.version)
|
|
28
|
-
.option(
|
|
29
|
-
'-q, --quiet',
|
|
30
|
-
'Suppress informational progress logs (info/log/debug). Errors and warnings, plus command result payloads (JSON, file contents), are still emitted. Use this when invoking the CLI from automation (e.g. the software factory test harness) to keep stdout focused on the result.',
|
|
31
|
-
)
|
|
32
|
-
// Toggle quiet mode as soon as the global option is parsed, so that any
|
|
33
|
-
// module-level setup happening inside command actions sees the right
|
|
34
|
-
// state. Commander invokes this hook before any subcommand action.
|
|
35
|
-
.hook('preAction', (thisCommand) => {
|
|
36
|
-
let opts = thisCommand.optsWithGlobals?.() ?? thisCommand.opts();
|
|
37
|
-
if (opts.quiet) {
|
|
38
|
-
setQuiet(true);
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
|
|
16
|
+
//
|
|
42
17
|
// Belt-and-suspenders: also flip quiet mode based on a raw scan of argv,
|
|
43
18
|
// so any code that runs between Commander's option parsing and the
|
|
44
19
|
// `preAction` hook sees the right state. We scan for the long form only;
|
|
@@ -47,59 +22,4 @@ if (process.argv.includes('--quiet')) {
|
|
|
47
22
|
setQuiet(true);
|
|
48
23
|
}
|
|
49
24
|
|
|
50
|
-
|
|
51
|
-
.command('profile')
|
|
52
|
-
.description('Manage saved profiles for different users/environments')
|
|
53
|
-
.argument('[subcommand]', 'list | add | switch | remove | migrate')
|
|
54
|
-
.argument('[arg]', 'Profile ID (for switch/remove)')
|
|
55
|
-
.option('-u, --user <matrixId>', 'Matrix user ID (e.g., @user:boxel.ai)')
|
|
56
|
-
.option('-p, --password <password>', 'Password (for add command)')
|
|
57
|
-
.option('-n, --name <displayName>', 'Display name (for add command)')
|
|
58
|
-
.option(
|
|
59
|
-
'-m, --matrix-url <url>',
|
|
60
|
-
'Matrix server URL (for add command with non-standard domains)',
|
|
61
|
-
)
|
|
62
|
-
.option(
|
|
63
|
-
'-r, --realm-server-url <url>',
|
|
64
|
-
'Realm server URL (for add command with non-standard domains)',
|
|
65
|
-
)
|
|
66
|
-
.addHelpText(
|
|
67
|
-
'after',
|
|
68
|
-
`
|
|
69
|
-
Environment variables (for 'add'):
|
|
70
|
-
BOXEL_PASSWORD Password; preferred over -p to avoid shell history.
|
|
71
|
-
BOXEL_ENVIRONMENT An env-mode slug (e.g. a branch name), interpreted
|
|
72
|
-
like scripts/env-slug.sh: URLs are derived as
|
|
73
|
-
http://matrix.<slug>.localhost and
|
|
74
|
-
http://realm-server.<slug>.localhost/. Overridden
|
|
75
|
-
by --matrix-url / --realm-server-url if provided.`,
|
|
76
|
-
)
|
|
77
|
-
.action(
|
|
78
|
-
async (
|
|
79
|
-
subcommand?: string,
|
|
80
|
-
arg?: string,
|
|
81
|
-
options?: {
|
|
82
|
-
user?: string;
|
|
83
|
-
password?: string;
|
|
84
|
-
name?: string;
|
|
85
|
-
matrixUrl?: string;
|
|
86
|
-
realmServerUrl?: string;
|
|
87
|
-
},
|
|
88
|
-
) => {
|
|
89
|
-
if (options?.password) {
|
|
90
|
-
console.warn(
|
|
91
|
-
'Warning: Supplying a password via -p/--password may expose it in shell history and process listings. ' +
|
|
92
|
-
'For non-interactive usage, prefer the BOXEL_PASSWORD environment variable or use "boxel profile add" interactively.',
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
await profileCommand(subcommand, arg, options);
|
|
96
|
-
},
|
|
97
|
-
);
|
|
98
|
-
|
|
99
|
-
registerFileCommand(program);
|
|
100
|
-
registerRealmCommand(program);
|
|
101
|
-
registerRunCommand(program);
|
|
102
|
-
registerSearchCommand(program);
|
|
103
|
-
registerReadTranspiledCommand(program);
|
|
104
|
-
|
|
105
|
-
program.parse();
|
|
25
|
+
buildBoxelProgram(pkg.version).parse();
|
|
@@ -90,6 +90,16 @@ export interface SyncOptions {
|
|
|
90
90
|
delete?: boolean;
|
|
91
91
|
/** Preview without making changes. */
|
|
92
92
|
dryRun?: boolean;
|
|
93
|
+
/**
|
|
94
|
+
* Block on the realm-server until uploaded cards have been indexed,
|
|
95
|
+
* not just durably written. Appends `?waitForIndex=true` to the
|
|
96
|
+
* `_atomic` POST. Trades upload latency for read-after-write
|
|
97
|
+
* consistency — useful when the next step queries the realm's index
|
|
98
|
+
* (search / list) and can't tolerate the indexer lag introduced by
|
|
99
|
+
* CS-11003 PR 2's deferred `+source` POST. Off by default; flip on
|
|
100
|
+
* for hand-off boundaries like the factory's post-seed sync.
|
|
101
|
+
*/
|
|
102
|
+
waitForIndex?: boolean;
|
|
93
103
|
}
|
|
94
104
|
|
|
95
105
|
export type { DeleteResult };
|
|
@@ -467,6 +477,7 @@ export class BoxelCLIClient {
|
|
|
467
477
|
preferNewest: options?.preferNewest,
|
|
468
478
|
delete: options?.delete,
|
|
469
479
|
dryRun: options?.dryRun,
|
|
480
|
+
waitForIndex: options?.waitForIndex,
|
|
470
481
|
profileManager: this.pm,
|
|
471
482
|
});
|
|
472
483
|
}
|
|
@@ -9,6 +9,8 @@ type Ignore = ReturnType<typeof ignoreModule>;
|
|
|
9
9
|
|
|
10
10
|
// Files that must never be pushed, deleted, or overwritten on the server via CLI.
|
|
11
11
|
export const PROTECTED_FILES = new Set(['.realm.json']);
|
|
12
|
+
const DELETE_TIMEOUT_MS = 10_000;
|
|
13
|
+
const DELETE_TIMEOUT_PROBE_MS = 3_000;
|
|
12
14
|
|
|
13
15
|
export function isProtectedFile(relativePath: string): boolean {
|
|
14
16
|
const normalizedPath = relativePath.replace(/\\/g, '/').replace(/^\/+/, '');
|
|
@@ -50,6 +52,15 @@ export interface SyncOptions {
|
|
|
50
52
|
realmUrl: string;
|
|
51
53
|
localDir: string;
|
|
52
54
|
dryRun?: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Append `?waitForIndex=true` to the `_atomic` POST so the realm-server
|
|
57
|
+
* returns only after the indexer has processed the batch. The
|
|
58
|
+
* `_atomic` handler hardcoded `waitForIndex: false` after CS-11003
|
|
59
|
+
* PR 2 (deferred `+source` POST), so callers that read indexed state
|
|
60
|
+
* (search / list) immediately after a sync race the indexer. Off by
|
|
61
|
+
* default.
|
|
62
|
+
*/
|
|
63
|
+
waitForIndex?: boolean;
|
|
53
64
|
}
|
|
54
65
|
|
|
55
66
|
const REMOTE_CONCURRENCY = 10;
|
|
@@ -453,7 +464,9 @@ export abstract class RealmSyncBase {
|
|
|
453
464
|
}),
|
|
454
465
|
);
|
|
455
466
|
|
|
456
|
-
const url =
|
|
467
|
+
const url = this.options.waitForIndex
|
|
468
|
+
? `${this.normalizedRealmUrl}_atomic?waitForIndex=true`
|
|
469
|
+
: `${this.normalizedRealmUrl}_atomic`;
|
|
457
470
|
const response = await this.authenticator.authedRealmFetch(url, {
|
|
458
471
|
method: 'POST',
|
|
459
472
|
headers: {
|
|
@@ -567,13 +580,45 @@ export abstract class RealmSyncBase {
|
|
|
567
580
|
}
|
|
568
581
|
|
|
569
582
|
const url = this.buildFileUrl(relativePath);
|
|
583
|
+
const startedAt = Date.now();
|
|
570
584
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
585
|
+
let response: Response;
|
|
586
|
+
try {
|
|
587
|
+
response = await this.authenticator.authedRealmFetch(url, {
|
|
588
|
+
method: 'DELETE',
|
|
589
|
+
headers: {
|
|
590
|
+
Accept: SupportedMimeType.CardSource,
|
|
591
|
+
},
|
|
592
|
+
signal: AbortSignal.timeout(DELETE_TIMEOUT_MS),
|
|
593
|
+
});
|
|
594
|
+
} catch (error) {
|
|
595
|
+
let elapsedMs = Date.now() - startedAt;
|
|
596
|
+
console.error(
|
|
597
|
+
` Delete request failed after ${elapsedMs}ms: ${relativePath}`,
|
|
598
|
+
);
|
|
599
|
+
if (
|
|
600
|
+
error instanceof Error &&
|
|
601
|
+
(error.name === 'TimeoutError' || error.name === 'AbortError')
|
|
602
|
+
) {
|
|
603
|
+
let deleteApplied = await this.verifyDeleteApplied(relativePath);
|
|
604
|
+
if (deleteApplied === true) {
|
|
605
|
+
console.warn(
|
|
606
|
+
` Delete response timed out after ${DELETE_TIMEOUT_MS}ms, but ${relativePath} is already gone on the realm; continuing`,
|
|
607
|
+
);
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
throw new Error(
|
|
611
|
+
`Timed out deleting ${relativePath} after ${DELETE_TIMEOUT_MS}ms`,
|
|
612
|
+
{ cause: error },
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
throw error;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
let elapsedMs = Date.now() - startedAt;
|
|
619
|
+
console.log(
|
|
620
|
+
` Delete response for ${relativePath}: ${response.status} ${response.statusText} (${elapsedMs}ms)`,
|
|
621
|
+
);
|
|
577
622
|
|
|
578
623
|
if (!response.ok && response.status !== 404) {
|
|
579
624
|
throw new Error(
|
|
@@ -584,6 +629,30 @@ export abstract class RealmSyncBase {
|
|
|
584
629
|
console.log(` Deleted: ${relativePath}`);
|
|
585
630
|
}
|
|
586
631
|
|
|
632
|
+
private async verifyDeleteApplied(
|
|
633
|
+
relativePath: string,
|
|
634
|
+
): Promise<boolean | 'unknown'> {
|
|
635
|
+
const url = this.buildFileUrl(relativePath);
|
|
636
|
+
try {
|
|
637
|
+
const response = await this.authenticator.authedRealmFetch(url, {
|
|
638
|
+
headers: {
|
|
639
|
+
Accept: SupportedMimeType.CardSource,
|
|
640
|
+
},
|
|
641
|
+
signal: AbortSignal.timeout(DELETE_TIMEOUT_PROBE_MS),
|
|
642
|
+
});
|
|
643
|
+
console.warn(
|
|
644
|
+
` Delete-timeout probe for ${relativePath}: ${response.status} ${response.statusText}`,
|
|
645
|
+
);
|
|
646
|
+
return response.status === 404 ? true : false;
|
|
647
|
+
} catch (probeError) {
|
|
648
|
+
console.warn(
|
|
649
|
+
` Delete-timeout probe failed for ${relativePath}:`,
|
|
650
|
+
probeError,
|
|
651
|
+
);
|
|
652
|
+
return 'unknown';
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
587
656
|
protected async deleteLocalFile(localPath: string): Promise<void> {
|
|
588
657
|
console.log(`Deleting local: ${localPath}`);
|
|
589
658
|
|