@cardstack/boxel-cli 0.0.1 → 0.1.0
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 +31 -24
- 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 +52 -16
- package/src/commands/realm/remove.ts +281 -0
- package/src/commands/realm/sync.ts +153 -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 +60 -2
- package/src/lib/auth-resolver.ts +58 -0
- package/src/lib/auth.ts +56 -12
- package/src/lib/boxel-cli-client.ts +135 -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 +47 -10
- 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,21 @@ 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
|
-
ok: boolean;
|
|
55
|
-
error?: string;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export interface DeleteResult {
|
|
59
|
-
ok: boolean;
|
|
60
|
-
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;
|
|
61
93
|
}
|
|
62
94
|
|
|
63
|
-
export
|
|
64
|
-
|
|
65
|
-
status?: number;
|
|
66
|
-
data?: Record<string, unknown>[];
|
|
67
|
-
error?: string;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export interface ListFilesResult {
|
|
71
|
-
filenames: string[];
|
|
72
|
-
error?: string;
|
|
73
|
-
}
|
|
95
|
+
export type { DeleteResult };
|
|
96
|
+
export type { WriteResult };
|
|
74
97
|
|
|
75
98
|
export interface RunCommandResult {
|
|
76
99
|
status: 'ready' | 'error' | 'unusable';
|
|
@@ -79,21 +102,7 @@ export interface RunCommandResult {
|
|
|
79
102
|
error?: string | null;
|
|
80
103
|
}
|
|
81
104
|
|
|
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
|
-
}
|
|
105
|
+
export type { LintMessage, LintResult };
|
|
97
106
|
|
|
98
107
|
export interface WaitForReadyResult {
|
|
99
108
|
ready: boolean;
|
|
@@ -111,10 +120,7 @@ export interface AtomicResult {
|
|
|
111
120
|
error?: string;
|
|
112
121
|
}
|
|
113
122
|
|
|
114
|
-
export
|
|
115
|
-
ok: boolean;
|
|
116
|
-
error?: string;
|
|
117
|
-
}
|
|
123
|
+
export type { CancelIndexingResult };
|
|
118
124
|
|
|
119
125
|
export class BoxelCLIClient {
|
|
120
126
|
private pm: ProfileManager;
|
|
@@ -155,148 +161,79 @@ export class BoxelCLIClient {
|
|
|
155
161
|
}
|
|
156
162
|
|
|
157
163
|
/**
|
|
158
|
-
* Read a file from a realm.
|
|
159
|
-
*
|
|
164
|
+
* Read a file from a realm. Always returns raw text content.
|
|
165
|
+
* Callers should parse the content themselves if needed (e.g. JSON).
|
|
166
|
+
*
|
|
167
|
+
* Delegates to the standalone `read()` in `commands/file/read.ts`
|
|
168
|
+
* so the CLI and programmatic API share one implementation.
|
|
160
169
|
*/
|
|
161
170
|
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
|
-
}
|
|
171
|
+
return fileRead(realmUrl, path, { profileManager: this.pm });
|
|
172
|
+
}
|
|
178
173
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
};
|
|
191
|
-
}
|
|
174
|
+
/**
|
|
175
|
+
* Fetch the TRANSPILED JavaScript output for a realm module. Thin
|
|
176
|
+
* wrapper around the `read-transpiled` CLI command — delegates to
|
|
177
|
+
* `readTranspiledModule()` in `commands/read-transpiled.ts` so the
|
|
178
|
+
* CLI and programmatic API share one implementation.
|
|
179
|
+
*/
|
|
180
|
+
async readTranspiled(
|
|
181
|
+
realmUrl: string,
|
|
182
|
+
path: string,
|
|
183
|
+
): Promise<ReadTranspiledResult> {
|
|
184
|
+
return readTranspiledModule(realmUrl, path, { profileManager: this.pm });
|
|
192
185
|
}
|
|
193
186
|
|
|
194
187
|
/**
|
|
195
188
|
* Write a file to a realm. Content is sent as-is with card+source MIME type.
|
|
196
189
|
* Path should include the file extension.
|
|
190
|
+
*
|
|
191
|
+
* Delegates to `write()` in `commands/file/write.ts` so the CLI and
|
|
192
|
+
* programmatic API share one implementation.
|
|
197
193
|
*/
|
|
198
194
|
async write(
|
|
199
195
|
realmUrl: string,
|
|
200
196
|
path: string,
|
|
201
197
|
content: string,
|
|
202
198
|
): 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
|
-
}
|
|
199
|
+
return coreWrite(realmUrl, path, content, { profileManager: this.pm });
|
|
230
200
|
}
|
|
231
201
|
|
|
232
202
|
/**
|
|
233
|
-
* Delete a file from a realm.
|
|
203
|
+
* Delete a file from a realm. Delegates to the standalone
|
|
204
|
+
* `deleteFile()` command in `commands/file/delete.ts` so the CLI
|
|
205
|
+
* and programmatic API share one implementation.
|
|
234
206
|
*/
|
|
235
207
|
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
|
-
}
|
|
208
|
+
return deleteFile(realmUrl, path, {
|
|
209
|
+
profileManager: this.pm,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
251
212
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
213
|
+
/**
|
|
214
|
+
* Touch one or more files in a realm to force re-indexing. Delegates to
|
|
215
|
+
* `touchFiles()` in `commands/file/touch.ts`.
|
|
216
|
+
*/
|
|
217
|
+
async touch(
|
|
218
|
+
realmUrl: string,
|
|
219
|
+
paths: string[],
|
|
220
|
+
options?: Pick<TouchCommandOptions, 'all' | 'dryRun'>,
|
|
221
|
+
): Promise<TouchResult> {
|
|
222
|
+
return coreTouchFiles(realmUrl, paths, {
|
|
223
|
+
...options,
|
|
224
|
+
profileManager: this.pm,
|
|
225
|
+
});
|
|
259
226
|
}
|
|
260
227
|
|
|
261
228
|
/**
|
|
262
|
-
*
|
|
229
|
+
* Federated search across one or more realms via `_federated-search`.
|
|
230
|
+
* Delegates to the standalone `search()` in `commands/search.ts`.
|
|
263
231
|
*/
|
|
264
232
|
async search(
|
|
265
|
-
|
|
233
|
+
realmUrls: string | string[],
|
|
266
234
|
query: Record<string, unknown>,
|
|
267
235
|
): 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
|
-
}
|
|
236
|
+
return fileSearch(realmUrls, query, { profileManager: this.pm });
|
|
300
237
|
}
|
|
301
238
|
|
|
302
239
|
/**
|
|
@@ -304,49 +241,7 @@ export class BoxelCLIClient {
|
|
|
304
241
|
* Returns relative paths (e.g., `hello.gts`, `Cards/my-card.json`).
|
|
305
242
|
*/
|
|
306
243
|
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
|
-
}
|
|
244
|
+
return coreListFiles(realmUrl, { profileManager: this.pm });
|
|
350
245
|
}
|
|
351
246
|
|
|
352
247
|
/**
|
|
@@ -414,32 +309,14 @@ export class BoxelCLIClient {
|
|
|
414
309
|
|
|
415
310
|
/**
|
|
416
311
|
* Lint a single file's source code via the realm's `_lint` endpoint.
|
|
312
|
+
* Delegates to the standalone `lint()` in `commands/file/lint.ts`.
|
|
417
313
|
*/
|
|
418
314
|
async lint(
|
|
419
315
|
realmUrl: string,
|
|
420
316
|
source: string,
|
|
421
317
|
filename: string,
|
|
422
318
|
): 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;
|
|
319
|
+
return coreLint(realmUrl, source, filename, { profileManager: this.pm });
|
|
443
320
|
}
|
|
444
321
|
|
|
445
322
|
/**
|
|
@@ -449,28 +326,10 @@ export class BoxelCLIClient {
|
|
|
449
326
|
realmUrl: string,
|
|
450
327
|
timeoutMs = 30_000,
|
|
451
328
|
): 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
|
-
};
|
|
329
|
+
return coreWaitForReady(realmUrl, {
|
|
330
|
+
timeoutMs,
|
|
331
|
+
profileManager: this.pm,
|
|
332
|
+
});
|
|
474
333
|
}
|
|
475
334
|
|
|
476
335
|
/**
|
|
@@ -540,33 +399,10 @@ export class BoxelCLIClient {
|
|
|
540
399
|
* Cancel all indexing jobs (running + pending) for a realm.
|
|
541
400
|
*/
|
|
542
401
|
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
|
-
}
|
|
402
|
+
return coreCancelIndexing(realmUrl, {
|
|
403
|
+
profileManager: this.pm,
|
|
404
|
+
cancelPending: true,
|
|
405
|
+
});
|
|
570
406
|
}
|
|
571
407
|
|
|
572
408
|
/**
|
|
@@ -615,6 +451,26 @@ export class BoxelCLIClient {
|
|
|
615
451
|
});
|
|
616
452
|
}
|
|
617
453
|
|
|
454
|
+
/**
|
|
455
|
+
* Bidirectional sync between a local workspace and a realm. Thin wrapper
|
|
456
|
+
* around the `realm sync` command's programmatic `sync()` function so the
|
|
457
|
+
* CLI and programmatic API share one implementation.
|
|
458
|
+
*/
|
|
459
|
+
async sync(
|
|
460
|
+
realmUrl: string,
|
|
461
|
+
localDir: string,
|
|
462
|
+
options?: SyncOptions,
|
|
463
|
+
): Promise<SyncResult> {
|
|
464
|
+
return realmSync(localDir, realmUrl, {
|
|
465
|
+
preferLocal: options?.preferLocal,
|
|
466
|
+
preferRemote: options?.preferRemote,
|
|
467
|
+
preferNewest: options?.preferNewest,
|
|
468
|
+
delete: options?.delete,
|
|
469
|
+
dryRun: options?.dryRun,
|
|
470
|
+
profileManager: this.pm,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
618
474
|
async createRealm(options: CreateRealmOptions): Promise<CreateRealmResult> {
|
|
619
475
|
let result = await coreCreateRealm(options.realmName, options.displayName, {
|
|
620
476
|
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');
|