@cardstack/boxel-cli 0.0.1 → 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/README.md +124 -0
- package/api.ts +3 -0
- package/bin/boxel.js +15 -0
- package/dist/index.js +107 -66
- package/package.json +35 -26
- package/src/build-program.ts +91 -0
- package/src/commands/file/delete.ts +110 -0
- package/src/commands/file/index.ts +20 -0
- package/src/commands/file/lint.ts +235 -0
- package/src/commands/file/list.ts +121 -0
- package/src/commands/file/read.ts +113 -0
- package/src/commands/file/touch.ts +222 -0
- package/src/commands/file/write.ts +152 -0
- package/src/commands/profile.ts +199 -106
- package/src/commands/read-transpiled.ts +120 -0
- package/src/commands/realm/cancel-indexing.ts +113 -0
- package/src/commands/realm/create.ts +1 -4
- package/src/commands/realm/history.ts +388 -0
- package/src/commands/realm/index.ts +12 -0
- package/src/commands/realm/list.ts +156 -0
- package/src/commands/realm/pull.ts +51 -17
- package/src/commands/realm/push.ts +79 -27
- package/src/commands/realm/remove.ts +281 -0
- package/src/commands/realm/sync.ts +160 -60
- package/src/commands/realm/wait-for-ready.ts +120 -0
- package/src/commands/realm/watch.ts +626 -0
- package/src/commands/run-command.ts +4 -3
- package/src/commands/search.ts +160 -0
- package/src/index.ts +16 -38
- package/src/lib/auth-resolver.ts +58 -0
- package/src/lib/auth.ts +56 -12
- package/src/lib/boxel-cli-client.ts +146 -279
- package/src/lib/cli-log.ts +132 -0
- package/src/lib/colors.ts +14 -9
- package/src/lib/find-checkpoint.ts +65 -0
- package/src/lib/profile-manager.ts +49 -4
- package/src/lib/prompt.ts +133 -0
- package/src/lib/realm-authenticator.ts +12 -0
- package/src/lib/realm-sync-base.ts +122 -16
- package/src/lib/seed-auth.ts +214 -0
- package/src/lib/watch-lock.ts +81 -0
- package/LICENSE +0 -21
|
@@ -1,6 +1,49 @@
|
|
|
1
|
+
import { deleteFile, type DeleteResult } from '../commands/file/delete';
|
|
2
|
+
import { read as fileRead, type ReadResult } from '../commands/file/read';
|
|
3
|
+
import {
|
|
4
|
+
lint as coreLint,
|
|
5
|
+
type LintResult,
|
|
6
|
+
type LintMessage,
|
|
7
|
+
} from '../commands/file/lint';
|
|
8
|
+
import {
|
|
9
|
+
listFiles as coreListFiles,
|
|
10
|
+
type ListFilesResult,
|
|
11
|
+
} from '../commands/file/list';
|
|
12
|
+
import {
|
|
13
|
+
search as fileSearch,
|
|
14
|
+
type SearchResult,
|
|
15
|
+
type SearchCommandOptions,
|
|
16
|
+
} from '../commands/search';
|
|
17
|
+
import {
|
|
18
|
+
readTranspiledModule,
|
|
19
|
+
type ReadTranspiledResult,
|
|
20
|
+
} from '../commands/read-transpiled';
|
|
21
|
+
import {
|
|
22
|
+
touchFiles as coreTouchFiles,
|
|
23
|
+
type TouchResult,
|
|
24
|
+
type TouchCommandOptions,
|
|
25
|
+
} from '../commands/file/touch';
|
|
26
|
+
import { write as coreWrite, type WriteResult } from '../commands/file/write';
|
|
27
|
+
import {
|
|
28
|
+
cancelIndexing as coreCancelIndexing,
|
|
29
|
+
type CancelIndexingResult,
|
|
30
|
+
} from '../commands/realm/cancel-indexing';
|
|
1
31
|
import { createRealm as coreCreateRealm } from '../commands/realm/create';
|
|
2
32
|
import { pull as realmPull } from '../commands/realm/pull';
|
|
33
|
+
import { sync as realmSync, type SyncResult } from '../commands/realm/sync';
|
|
34
|
+
import { waitForReady as coreWaitForReady } from '../commands/realm/wait-for-ready';
|
|
3
35
|
import { getProfileManager, type ProfileManager } from './profile-manager';
|
|
36
|
+
import { ensureTrailingSlash } from '@cardstack/runtime-common/paths';
|
|
37
|
+
|
|
38
|
+
export type {
|
|
39
|
+
ReadResult,
|
|
40
|
+
ListFilesResult,
|
|
41
|
+
ReadTranspiledResult,
|
|
42
|
+
SyncResult,
|
|
43
|
+
SearchResult,
|
|
44
|
+
SearchCommandOptions,
|
|
45
|
+
TouchResult,
|
|
46
|
+
};
|
|
4
47
|
|
|
5
48
|
const MIME = {
|
|
6
49
|
CardSource: 'application/vnd.card+source',
|
|
@@ -9,10 +52,6 @@ const MIME = {
|
|
|
9
52
|
JSONAPI: 'application/vnd.api+json',
|
|
10
53
|
} as const;
|
|
11
54
|
|
|
12
|
-
function ensureTrailingSlash(url: string): string {
|
|
13
|
-
return url.endsWith('/') ? url : `${url}/`;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
55
|
export interface CreateRealmOptions {
|
|
17
56
|
/** URL slug for the realm (lowercase, numbers, hyphens). */
|
|
18
57
|
realmName: string;
|
|
@@ -40,37 +79,31 @@ export interface PullResult {
|
|
|
40
79
|
error?: string;
|
|
41
80
|
}
|
|
42
81
|
|
|
43
|
-
export interface
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
ok: boolean;
|
|
65
|
-
status?: number;
|
|
66
|
-
data?: Record<string, unknown>[];
|
|
67
|
-
error?: string;
|
|
82
|
+
export interface SyncOptions {
|
|
83
|
+
/** Resolve conflicts by keeping the local version. */
|
|
84
|
+
preferLocal?: boolean;
|
|
85
|
+
/** Resolve conflicts by keeping the remote version. */
|
|
86
|
+
preferRemote?: boolean;
|
|
87
|
+
/** Resolve conflicts by keeping the newest version. */
|
|
88
|
+
preferNewest?: boolean;
|
|
89
|
+
/** Propagate deletions in both directions. */
|
|
90
|
+
delete?: boolean;
|
|
91
|
+
/** Preview without making changes. */
|
|
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;
|
|
68
103
|
}
|
|
69
104
|
|
|
70
|
-
export
|
|
71
|
-
|
|
72
|
-
error?: string;
|
|
73
|
-
}
|
|
105
|
+
export type { DeleteResult };
|
|
106
|
+
export type { WriteResult };
|
|
74
107
|
|
|
75
108
|
export interface RunCommandResult {
|
|
76
109
|
status: 'ready' | 'error' | 'unusable';
|
|
@@ -79,21 +112,7 @@ export interface RunCommandResult {
|
|
|
79
112
|
error?: string | null;
|
|
80
113
|
}
|
|
81
114
|
|
|
82
|
-
export
|
|
83
|
-
ruleId: string | null;
|
|
84
|
-
severity: 1 | 2;
|
|
85
|
-
message: string;
|
|
86
|
-
line: number;
|
|
87
|
-
column: number;
|
|
88
|
-
endLine?: number;
|
|
89
|
-
endColumn?: number;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export interface LintResult {
|
|
93
|
-
fixed: boolean;
|
|
94
|
-
output: string;
|
|
95
|
-
messages: LintMessage[];
|
|
96
|
-
}
|
|
115
|
+
export type { LintMessage, LintResult };
|
|
97
116
|
|
|
98
117
|
export interface WaitForReadyResult {
|
|
99
118
|
ready: boolean;
|
|
@@ -111,10 +130,7 @@ export interface AtomicResult {
|
|
|
111
130
|
error?: string;
|
|
112
131
|
}
|
|
113
132
|
|
|
114
|
-
export
|
|
115
|
-
ok: boolean;
|
|
116
|
-
error?: string;
|
|
117
|
-
}
|
|
133
|
+
export type { CancelIndexingResult };
|
|
118
134
|
|
|
119
135
|
export class BoxelCLIClient {
|
|
120
136
|
private pm: ProfileManager;
|
|
@@ -155,148 +171,79 @@ export class BoxelCLIClient {
|
|
|
155
171
|
}
|
|
156
172
|
|
|
157
173
|
/**
|
|
158
|
-
* Read a file from a realm.
|
|
159
|
-
*
|
|
174
|
+
* Read a file from a realm. Always returns raw text content.
|
|
175
|
+
* Callers should parse the content themselves if needed (e.g. JSON).
|
|
176
|
+
*
|
|
177
|
+
* Delegates to the standalone `read()` in `commands/file/read.ts`
|
|
178
|
+
* so the CLI and programmatic API share one implementation.
|
|
160
179
|
*/
|
|
161
180
|
async read(realmUrl: string, path: string): Promise<ReadResult> {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
try {
|
|
165
|
-
let response = await this.pm.authedRealmFetch(url, {
|
|
166
|
-
method: 'GET',
|
|
167
|
-
headers: { Accept: MIME.CardSource },
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
if (!response.ok) {
|
|
171
|
-
let body = await response.text();
|
|
172
|
-
return {
|
|
173
|
-
ok: false,
|
|
174
|
-
status: response.status,
|
|
175
|
-
error: `HTTP ${response.status}: ${body.slice(0, 300)}`,
|
|
176
|
-
};
|
|
177
|
-
}
|
|
181
|
+
return fileRead(realmUrl, path, { profileManager: this.pm });
|
|
182
|
+
}
|
|
178
183
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
};
|
|
191
|
-
}
|
|
184
|
+
/**
|
|
185
|
+
* Fetch the TRANSPILED JavaScript output for a realm module. Thin
|
|
186
|
+
* wrapper around the `read-transpiled` CLI command — delegates to
|
|
187
|
+
* `readTranspiledModule()` in `commands/read-transpiled.ts` so the
|
|
188
|
+
* CLI and programmatic API share one implementation.
|
|
189
|
+
*/
|
|
190
|
+
async readTranspiled(
|
|
191
|
+
realmUrl: string,
|
|
192
|
+
path: string,
|
|
193
|
+
): Promise<ReadTranspiledResult> {
|
|
194
|
+
return readTranspiledModule(realmUrl, path, { profileManager: this.pm });
|
|
192
195
|
}
|
|
193
196
|
|
|
194
197
|
/**
|
|
195
198
|
* Write a file to a realm. Content is sent as-is with card+source MIME type.
|
|
196
199
|
* Path should include the file extension.
|
|
200
|
+
*
|
|
201
|
+
* Delegates to `write()` in `commands/file/write.ts` so the CLI and
|
|
202
|
+
* programmatic API share one implementation.
|
|
197
203
|
*/
|
|
198
204
|
async write(
|
|
199
205
|
realmUrl: string,
|
|
200
206
|
path: string,
|
|
201
207
|
content: string,
|
|
202
208
|
): Promise<WriteResult> {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
try {
|
|
206
|
-
let response = await this.pm.authedRealmFetch(url, {
|
|
207
|
-
method: 'POST',
|
|
208
|
-
headers: {
|
|
209
|
-
Accept: MIME.CardSource,
|
|
210
|
-
'Content-Type': MIME.CardSource,
|
|
211
|
-
},
|
|
212
|
-
body: content,
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
if (!response.ok) {
|
|
216
|
-
let body = await response.text();
|
|
217
|
-
return {
|
|
218
|
-
ok: false,
|
|
219
|
-
error: `HTTP ${response.status}: ${body.slice(0, 300)}`,
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
return { ok: true };
|
|
224
|
-
} catch (err) {
|
|
225
|
-
return {
|
|
226
|
-
ok: false,
|
|
227
|
-
error: err instanceof Error ? err.message : String(err),
|
|
228
|
-
};
|
|
229
|
-
}
|
|
209
|
+
return coreWrite(realmUrl, path, content, { profileManager: this.pm });
|
|
230
210
|
}
|
|
231
211
|
|
|
232
212
|
/**
|
|
233
|
-
* Delete a file from a realm.
|
|
213
|
+
* Delete a file from a realm. Delegates to the standalone
|
|
214
|
+
* `deleteFile()` command in `commands/file/delete.ts` so the CLI
|
|
215
|
+
* and programmatic API share one implementation.
|
|
234
216
|
*/
|
|
235
217
|
async delete(realmUrl: string, path: string): Promise<DeleteResult> {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
method: 'DELETE',
|
|
241
|
-
headers: { Accept: MIME.CardSource },
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
if (!response.ok) {
|
|
245
|
-
let body = await response.text();
|
|
246
|
-
return {
|
|
247
|
-
ok: false,
|
|
248
|
-
error: `HTTP ${response.status}: ${body.slice(0, 300)}`,
|
|
249
|
-
};
|
|
250
|
-
}
|
|
218
|
+
return deleteFile(realmUrl, path, {
|
|
219
|
+
profileManager: this.pm,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
251
222
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
223
|
+
/**
|
|
224
|
+
* Touch one or more files in a realm to force re-indexing. Delegates to
|
|
225
|
+
* `touchFiles()` in `commands/file/touch.ts`.
|
|
226
|
+
*/
|
|
227
|
+
async touch(
|
|
228
|
+
realmUrl: string,
|
|
229
|
+
paths: string[],
|
|
230
|
+
options?: Pick<TouchCommandOptions, 'all' | 'dryRun'>,
|
|
231
|
+
): Promise<TouchResult> {
|
|
232
|
+
return coreTouchFiles(realmUrl, paths, {
|
|
233
|
+
...options,
|
|
234
|
+
profileManager: this.pm,
|
|
235
|
+
});
|
|
259
236
|
}
|
|
260
237
|
|
|
261
238
|
/**
|
|
262
|
-
*
|
|
239
|
+
* Federated search across one or more realms via `_federated-search`.
|
|
240
|
+
* Delegates to the standalone `search()` in `commands/search.ts`.
|
|
263
241
|
*/
|
|
264
242
|
async search(
|
|
265
|
-
|
|
243
|
+
realmUrls: string | string[],
|
|
266
244
|
query: Record<string, unknown>,
|
|
267
245
|
): Promise<SearchResult> {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
try {
|
|
271
|
-
let response = await this.pm.authedRealmFetch(searchUrl, {
|
|
272
|
-
method: 'QUERY',
|
|
273
|
-
headers: {
|
|
274
|
-
Accept: MIME.CardJson,
|
|
275
|
-
'Content-Type': MIME.JSON,
|
|
276
|
-
},
|
|
277
|
-
body: JSON.stringify(query),
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
if (!response.ok) {
|
|
281
|
-
let body = await response.text();
|
|
282
|
-
return {
|
|
283
|
-
ok: false,
|
|
284
|
-
status: response.status,
|
|
285
|
-
error: `HTTP ${response.status}: ${body.slice(0, 300)}`,
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
let result = (await response.json()) as {
|
|
290
|
-
data?: Record<string, unknown>[];
|
|
291
|
-
};
|
|
292
|
-
return { ok: true, data: result.data };
|
|
293
|
-
} catch (err) {
|
|
294
|
-
return {
|
|
295
|
-
ok: false,
|
|
296
|
-
status: 0,
|
|
297
|
-
error: err instanceof Error ? err.message : String(err),
|
|
298
|
-
};
|
|
299
|
-
}
|
|
246
|
+
return fileSearch(realmUrls, query, { profileManager: this.pm });
|
|
300
247
|
}
|
|
301
248
|
|
|
302
249
|
/**
|
|
@@ -304,49 +251,7 @@ export class BoxelCLIClient {
|
|
|
304
251
|
* Returns relative paths (e.g., `hello.gts`, `Cards/my-card.json`).
|
|
305
252
|
*/
|
|
306
253
|
async listFiles(realmUrl: string): Promise<ListFilesResult> {
|
|
307
|
-
|
|
308
|
-
let mtimesUrl = `${normalizedRealmUrl}_mtimes`;
|
|
309
|
-
|
|
310
|
-
try {
|
|
311
|
-
let response = await this.pm.authedRealmFetch(mtimesUrl, {
|
|
312
|
-
method: 'GET',
|
|
313
|
-
headers: { Accept: MIME.JSONAPI },
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
if (!response.ok) {
|
|
317
|
-
let body = await response.text();
|
|
318
|
-
return {
|
|
319
|
-
filenames: [],
|
|
320
|
-
error: `_mtimes returned HTTP ${response.status}: ${body.slice(0, 300)}`,
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
let json = (await response.json()) as {
|
|
325
|
-
data?: { attributes?: { mtimes?: Record<string, number> } };
|
|
326
|
-
};
|
|
327
|
-
let mtimes =
|
|
328
|
-
json?.data?.attributes?.mtimes ??
|
|
329
|
-
(json as unknown as Record<string, number>);
|
|
330
|
-
|
|
331
|
-
let filenames: string[] = [];
|
|
332
|
-
for (let fullUrl of Object.keys(mtimes)) {
|
|
333
|
-
if (!fullUrl.startsWith(normalizedRealmUrl)) {
|
|
334
|
-
continue;
|
|
335
|
-
}
|
|
336
|
-
let relativePath = fullUrl.slice(normalizedRealmUrl.length);
|
|
337
|
-
if (!relativePath || relativePath.endsWith('/')) {
|
|
338
|
-
continue;
|
|
339
|
-
}
|
|
340
|
-
filenames.push(relativePath);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
return { filenames: filenames.sort() };
|
|
344
|
-
} catch (err) {
|
|
345
|
-
return {
|
|
346
|
-
filenames: [],
|
|
347
|
-
error: err instanceof Error ? err.message : String(err),
|
|
348
|
-
};
|
|
349
|
-
}
|
|
254
|
+
return coreListFiles(realmUrl, { profileManager: this.pm });
|
|
350
255
|
}
|
|
351
256
|
|
|
352
257
|
/**
|
|
@@ -414,32 +319,14 @@ export class BoxelCLIClient {
|
|
|
414
319
|
|
|
415
320
|
/**
|
|
416
321
|
* Lint a single file's source code via the realm's `_lint` endpoint.
|
|
322
|
+
* Delegates to the standalone `lint()` in `commands/file/lint.ts`.
|
|
417
323
|
*/
|
|
418
324
|
async lint(
|
|
419
325
|
realmUrl: string,
|
|
420
326
|
source: string,
|
|
421
327
|
filename: string,
|
|
422
328
|
): Promise<LintResult> {
|
|
423
|
-
|
|
424
|
-
let response = await this.pm.authedRealmFetch(lintUrl, {
|
|
425
|
-
method: 'POST',
|
|
426
|
-
headers: {
|
|
427
|
-
Accept: MIME.JSON,
|
|
428
|
-
'Content-Type': MIME.CardSource,
|
|
429
|
-
'X-Filename': filename,
|
|
430
|
-
'X-HTTP-Method-Override': 'QUERY',
|
|
431
|
-
},
|
|
432
|
-
body: source,
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
if (!response.ok) {
|
|
436
|
-
let body = await response.text().catch(() => '(no body)');
|
|
437
|
-
throw new Error(
|
|
438
|
-
`_lint returned HTTP ${response.status}: ${body.slice(0, 300)}`,
|
|
439
|
-
);
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
return (await response.json()) as LintResult;
|
|
329
|
+
return coreLint(realmUrl, source, filename, { profileManager: this.pm });
|
|
443
330
|
}
|
|
444
331
|
|
|
445
332
|
/**
|
|
@@ -449,28 +336,10 @@ export class BoxelCLIClient {
|
|
|
449
336
|
realmUrl: string,
|
|
450
337
|
timeoutMs = 30_000,
|
|
451
338
|
): Promise<WaitForReadyResult> {
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
try {
|
|
457
|
-
let response = await this.pm.authedRealmFetch(readinessUrl, {
|
|
458
|
-
method: 'GET',
|
|
459
|
-
headers: { Accept: MIME.JSON },
|
|
460
|
-
});
|
|
461
|
-
if (response.ok) {
|
|
462
|
-
return { ready: true };
|
|
463
|
-
}
|
|
464
|
-
} catch {
|
|
465
|
-
// retry
|
|
466
|
-
}
|
|
467
|
-
await new Promise((r) => setTimeout(r, 1000));
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
return {
|
|
471
|
-
ready: false,
|
|
472
|
-
error: `Realm not ready after ${timeoutMs}ms: ${readinessUrl}`,
|
|
473
|
-
};
|
|
339
|
+
return coreWaitForReady(realmUrl, {
|
|
340
|
+
timeoutMs,
|
|
341
|
+
profileManager: this.pm,
|
|
342
|
+
});
|
|
474
343
|
}
|
|
475
344
|
|
|
476
345
|
/**
|
|
@@ -540,33 +409,10 @@ export class BoxelCLIClient {
|
|
|
540
409
|
* Cancel all indexing jobs (running + pending) for a realm.
|
|
541
410
|
*/
|
|
542
411
|
async cancelAllIndexingJobs(realmUrl: string): Promise<CancelIndexingResult> {
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
method: 'POST',
|
|
548
|
-
headers: {
|
|
549
|
-
Accept: MIME.JSON,
|
|
550
|
-
'Content-Type': MIME.JSON,
|
|
551
|
-
},
|
|
552
|
-
body: JSON.stringify({ cancelPending: true }),
|
|
553
|
-
});
|
|
554
|
-
|
|
555
|
-
if (!response.ok) {
|
|
556
|
-
let body = await response.text();
|
|
557
|
-
return {
|
|
558
|
-
ok: false,
|
|
559
|
-
error: `HTTP ${response.status}: ${body.slice(0, 300)}`,
|
|
560
|
-
};
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
return { ok: true };
|
|
564
|
-
} catch (err) {
|
|
565
|
-
return {
|
|
566
|
-
ok: false,
|
|
567
|
-
error: err instanceof Error ? err.message : String(err),
|
|
568
|
-
};
|
|
569
|
-
}
|
|
412
|
+
return coreCancelIndexing(realmUrl, {
|
|
413
|
+
profileManager: this.pm,
|
|
414
|
+
cancelPending: true,
|
|
415
|
+
});
|
|
570
416
|
}
|
|
571
417
|
|
|
572
418
|
/**
|
|
@@ -615,6 +461,27 @@ export class BoxelCLIClient {
|
|
|
615
461
|
});
|
|
616
462
|
}
|
|
617
463
|
|
|
464
|
+
/**
|
|
465
|
+
* Bidirectional sync between a local workspace and a realm. Thin wrapper
|
|
466
|
+
* around the `realm sync` command's programmatic `sync()` function so the
|
|
467
|
+
* CLI and programmatic API share one implementation.
|
|
468
|
+
*/
|
|
469
|
+
async sync(
|
|
470
|
+
realmUrl: string,
|
|
471
|
+
localDir: string,
|
|
472
|
+
options?: SyncOptions,
|
|
473
|
+
): Promise<SyncResult> {
|
|
474
|
+
return realmSync(localDir, realmUrl, {
|
|
475
|
+
preferLocal: options?.preferLocal,
|
|
476
|
+
preferRemote: options?.preferRemote,
|
|
477
|
+
preferNewest: options?.preferNewest,
|
|
478
|
+
delete: options?.delete,
|
|
479
|
+
dryRun: options?.dryRun,
|
|
480
|
+
waitForIndex: options?.waitForIndex,
|
|
481
|
+
profileManager: this.pm,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
618
485
|
async createRealm(options: CreateRealmOptions): Promise<CreateRealmResult> {
|
|
619
486
|
let result = await coreCreateRealm(options.realmName, options.displayName, {
|
|
620
487
|
background: options.backgroundURL,
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized logger for the boxel CLI.
|
|
3
|
+
*
|
|
4
|
+
* # Guidance for command authors
|
|
5
|
+
*
|
|
6
|
+
* Pick where output goes by asking: **does anything ever parse this
|
|
7
|
+
* line programmatically (`--json` consumer, shell pipeline, the
|
|
8
|
+
* software factory tool executor)?**
|
|
9
|
+
*
|
|
10
|
+
* - **No → `console.log` / `console.info` / `console.debug`.** This is
|
|
11
|
+
* the default for interactive/decorative output: progress messages,
|
|
12
|
+
* colored confirmations ("Fixed: foo.gts"), summaries, anything you'd
|
|
13
|
+
* write with ANSI escapes from `lib/colors`. Under `--quiet` these are
|
|
14
|
+
* intercepted and silenced — a fresh command author doesn't need to
|
|
15
|
+
* know quiet mode exists.
|
|
16
|
+
* - **Yes → `cliLog.output(...)`.** This is for raw payloads a caller
|
|
17
|
+
* parses: the `--json` branch (`cliLog.output(JSON.stringify(result, null, 2))`),
|
|
18
|
+
* raw file content from `read`/`read-transpiled`, and any other
|
|
19
|
+
* stdout-as-contract output. `cliLog.output` writes to stdout
|
|
20
|
+
* regardless of `--quiet`.
|
|
21
|
+
* - **Errors / warnings → `console.error` / `console.warn`.** Never
|
|
22
|
+
* silenced; goes to stderr.
|
|
23
|
+
*
|
|
24
|
+
* Quick heuristic: if the string you're about to print contains
|
|
25
|
+
* `FG_GREEN`/`DIM`/`RESET`/etc., it's decorative — use `console.log`.
|
|
26
|
+
* If it's `JSON.stringify(...)` or raw bytes, use `cliLog.output`.
|
|
27
|
+
*
|
|
28
|
+
* # How it works
|
|
29
|
+
*
|
|
30
|
+
* `--quiet` is a global flag in `src/index.ts`. When set, `setQuiet(true)`
|
|
31
|
+
* replaces `console.log`/`info`/`debug` with no-ops; `cliLog.output`
|
|
32
|
+
* writes directly to `process.stdout` and bypasses the interceptor.
|
|
33
|
+
* Software factory's tool executor passes `--quiet` by default so chatty
|
|
34
|
+
* progress lines don't pollute CI logs (see `factory-tool-executor.ts`).
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
let quiet = false;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Original references to the console methods we intercept. Captured at
|
|
41
|
+
* module load so `setQuiet(false)` can put them back if quiet mode is
|
|
42
|
+
* toggled off (the unit tests rely on this).
|
|
43
|
+
*/
|
|
44
|
+
const originalConsoleLog = console.log.bind(console);
|
|
45
|
+
const originalConsoleInfo = console.info.bind(console);
|
|
46
|
+
const originalConsoleDebug = console.debug.bind(console);
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Toggle quiet mode. When true, `console.log` / `console.info` /
|
|
50
|
+
* `console.debug` are replaced with no-ops for the lifetime of the
|
|
51
|
+
* process. `console.warn` and `console.error` are untouched.
|
|
52
|
+
*
|
|
53
|
+
* Idempotent: calling with the same value is a no-op.
|
|
54
|
+
*/
|
|
55
|
+
export function setQuiet(value: boolean): void {
|
|
56
|
+
if (value === quiet) return;
|
|
57
|
+
quiet = value;
|
|
58
|
+
if (quiet) {
|
|
59
|
+
const noop = () => {
|
|
60
|
+
/* intentionally empty: silence info/log/debug under --quiet */
|
|
61
|
+
};
|
|
62
|
+
console.log = noop;
|
|
63
|
+
console.info = noop;
|
|
64
|
+
console.debug = noop;
|
|
65
|
+
} else {
|
|
66
|
+
console.log = originalConsoleLog;
|
|
67
|
+
console.info = originalConsoleInfo;
|
|
68
|
+
console.debug = originalConsoleDebug;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function isQuiet(): boolean {
|
|
73
|
+
return quiet;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const cliLog = {
|
|
77
|
+
/**
|
|
78
|
+
* Informational progress message. Silenced under `--quiet`. Goes to
|
|
79
|
+
* stderr (not stdout) so that it never contaminates stdout-payload
|
|
80
|
+
* commands even when they're inadvertently mixed in.
|
|
81
|
+
*/
|
|
82
|
+
info(...args: unknown[]): void {
|
|
83
|
+
if (quiet) return;
|
|
84
|
+
process.stderr.write(args.map(stringifyArg).join(' ') + '\n');
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Warning. Always written (even under quiet) — warnings should reach
|
|
89
|
+
* the operator. Goes to stderr.
|
|
90
|
+
*/
|
|
91
|
+
warn(...args: unknown[]): void {
|
|
92
|
+
process.stderr.write(args.map(stringifyArg).join(' ') + '\n');
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Error. Always written (even under quiet). Goes to stderr.
|
|
97
|
+
*/
|
|
98
|
+
error(...args: unknown[]): void {
|
|
99
|
+
process.stderr.write(args.map(stringifyArg).join(' ') + '\n');
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Programmatic command output — the "result payload" a caller parses.
|
|
104
|
+
* Writes to stdout and is **never** silenced by `--quiet`.
|
|
105
|
+
*
|
|
106
|
+
* Use this **only** for output where stdout is the contract:
|
|
107
|
+
* - `--json` branches: `cliLog.output(JSON.stringify(result, null, 2))`
|
|
108
|
+
* - raw file content: `read` / `read-transpiled` print bytes via
|
|
109
|
+
* `cliLog.output(result.content ?? '')`
|
|
110
|
+
* - any other stdout-as-API output a script or the software factory
|
|
111
|
+
* pipes/parses
|
|
112
|
+
*
|
|
113
|
+
* Do **not** use this for human-facing decorative lines (colored
|
|
114
|
+
* confirmations, status, summaries, progress). Those go to
|
|
115
|
+
* `console.log` so the `--quiet` interceptor can silence them.
|
|
116
|
+
* If you're tempted to wrap the string in ANSI escapes from
|
|
117
|
+
* `lib/colors`, you want `console.log`, not `cliLog.output`.
|
|
118
|
+
*/
|
|
119
|
+
output(...args: unknown[]): void {
|
|
120
|
+
process.stdout.write(args.map(stringifyArg).join(' ') + '\n');
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
function stringifyArg(arg: unknown): string {
|
|
125
|
+
if (typeof arg === 'string') return arg;
|
|
126
|
+
if (arg instanceof Error) return arg.stack ?? arg.message;
|
|
127
|
+
try {
|
|
128
|
+
return JSON.stringify(arg);
|
|
129
|
+
} catch {
|
|
130
|
+
return String(arg);
|
|
131
|
+
}
|
|
132
|
+
}
|
package/src/lib/colors.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
// ANSI
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export const
|
|
8
|
-
export const
|
|
9
|
-
export const
|
|
1
|
+
// Disable ANSI escapes when stdout isn't a TTY (piped to file/pager) or
|
|
2
|
+
// NO_COLOR is set — see https://no-color.org. Evaluated at module load.
|
|
3
|
+
const COLOR_ENABLED = process.stdout.isTTY === true && !process.env.NO_COLOR;
|
|
4
|
+
|
|
5
|
+
const c = (code: string): string => (COLOR_ENABLED ? code : '');
|
|
6
|
+
|
|
7
|
+
export const FG_GREEN = c('\x1b[32m');
|
|
8
|
+
export const FG_YELLOW = c('\x1b[33m');
|
|
9
|
+
export const FG_CYAN = c('\x1b[36m');
|
|
10
|
+
export const FG_MAGENTA = c('\x1b[35m');
|
|
11
|
+
export const FG_RED = c('\x1b[31m');
|
|
12
|
+
export const DIM = c('\x1b[2m');
|
|
13
|
+
export const BOLD = c('\x1b[1m');
|
|
14
|
+
export const RESET = c('\x1b[0m');
|