@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
package/src/commands/profile.ts
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import * as readline from 'readline';
|
|
2
|
-
import { Writable } from 'stream';
|
|
3
1
|
import type { ProfileManager } from '../lib/profile-manager';
|
|
4
2
|
import {
|
|
5
3
|
getProfileManager,
|
|
6
4
|
formatProfileBadge,
|
|
5
|
+
getDomainFromMatrixId,
|
|
7
6
|
getEnvironmentFromMatrixId,
|
|
8
|
-
getEnvironmentLabel,
|
|
9
7
|
getUsernameFromMatrixId,
|
|
10
8
|
} from '../lib/profile-manager';
|
|
9
|
+
import { prompt, promptPassword } from '../lib/prompt';
|
|
11
10
|
import {
|
|
12
11
|
FG_GREEN,
|
|
13
12
|
FG_YELLOW,
|
|
@@ -19,92 +18,94 @@ import {
|
|
|
19
18
|
RESET,
|
|
20
19
|
} from '../lib/colors';
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
return new Promise((resolve) => {
|
|
29
|
-
rl.question(question, (answer) => {
|
|
30
|
-
rl.close();
|
|
31
|
-
resolve(answer.trim());
|
|
32
|
-
});
|
|
33
|
-
});
|
|
21
|
+
export interface ProfileCommandOptions {
|
|
22
|
+
user?: string;
|
|
23
|
+
password?: string;
|
|
24
|
+
name?: string;
|
|
25
|
+
matrixUrl?: string;
|
|
26
|
+
realmServerUrl?: string;
|
|
34
27
|
}
|
|
35
28
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
input: process.stdin,
|
|
42
|
-
output: mutableOutput,
|
|
43
|
-
terminal: true,
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
return new Promise((resolve, reject) => {
|
|
47
|
-
const stdin = process.stdin;
|
|
48
|
-
const wasFlowing = stdin.readableFlowing;
|
|
49
|
-
|
|
50
|
-
if (stdin.isTTY) {
|
|
51
|
-
stdin.setRawMode(true);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const cleanup = () => {
|
|
55
|
-
stdin.removeListener('data', onData);
|
|
56
|
-
if (stdin.isTTY) {
|
|
57
|
-
stdin.setRawMode(false);
|
|
58
|
-
}
|
|
59
|
-
rl.close();
|
|
60
|
-
if (!wasFlowing) {
|
|
61
|
-
stdin.pause();
|
|
62
|
-
}
|
|
63
|
-
};
|
|
29
|
+
interface EnvironmentDefaults {
|
|
30
|
+
domain: string;
|
|
31
|
+
matrixUrl: string;
|
|
32
|
+
realmServerUrl: string;
|
|
33
|
+
}
|
|
64
34
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
35
|
+
const MENU_ENVIRONMENTS: Record<
|
|
36
|
+
'staging' | 'production' | 'local',
|
|
37
|
+
EnvironmentDefaults
|
|
38
|
+
> = {
|
|
39
|
+
staging: {
|
|
40
|
+
domain: 'stack.cards',
|
|
41
|
+
matrixUrl: 'https://matrix-staging.stack.cards',
|
|
42
|
+
realmServerUrl: 'https://realms-staging.stack.cards/',
|
|
43
|
+
},
|
|
44
|
+
production: {
|
|
45
|
+
domain: 'boxel.ai',
|
|
46
|
+
matrixUrl: 'https://matrix.boxel.ai',
|
|
47
|
+
realmServerUrl: 'https://app.boxel.ai/',
|
|
48
|
+
},
|
|
49
|
+
local: {
|
|
50
|
+
domain: 'localhost',
|
|
51
|
+
matrixUrl: 'http://localhost:8008',
|
|
52
|
+
realmServerUrl: 'http://localhost:4201/',
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Validate and normalize a Matrix or realm-server URL provided by the user
|
|
57
|
+
// (via --matrix-url / --realm-server-url or the interactive Custom prompt).
|
|
58
|
+
// Returns the trimmed input on success; exits 1 with a clear message
|
|
59
|
+
// otherwise. Without this, downstream code (fetch, realm auth, etc.) would
|
|
60
|
+
// throw on invalid input far away from where the value was entered.
|
|
61
|
+
function validateUrl(input: string, label: string): string {
|
|
62
|
+
const trimmed = input.trim();
|
|
63
|
+
let parsed: URL;
|
|
64
|
+
try {
|
|
65
|
+
parsed = new URL(trimmed);
|
|
66
|
+
} catch {
|
|
67
|
+
console.error(
|
|
68
|
+
`${FG_RED}Error:${RESET} ${label} "${input}" is not a valid URL.`,
|
|
69
|
+
);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
73
|
+
console.error(
|
|
74
|
+
`${FG_RED}Error:${RESET} ${label} "${input}" must use http:// or https://.`,
|
|
75
|
+
);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
return trimmed;
|
|
79
|
+
}
|
|
91
80
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
});
|
|
81
|
+
// Matches scripts/env-slug.sh: lowercase, "/" -> "-", strip chars outside
|
|
82
|
+
// [a-z0-9-], collapse runs of "-", trim leading/trailing "-".
|
|
83
|
+
function computeEnvSlug(name: string): string {
|
|
84
|
+
return name
|
|
85
|
+
.toLowerCase()
|
|
86
|
+
.replace(/\//g, '-')
|
|
87
|
+
.replace(/[^a-z0-9-]/g, '')
|
|
88
|
+
.replace(/-+/g, '-')
|
|
89
|
+
.replace(/^-+|-+$/g, '');
|
|
102
90
|
}
|
|
103
91
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
92
|
+
// Derive URLs from BOXEL_ENVIRONMENT using the same ".${slug}.localhost"
|
|
93
|
+
// pattern that mise-tasks/lib/env-vars.sh produces for env-mode local dev.
|
|
94
|
+
function resolveBoxelEnvironment(): EnvironmentDefaults | null {
|
|
95
|
+
const raw = process.env.BOXEL_ENVIRONMENT;
|
|
96
|
+
if (!raw || !raw.trim()) return null;
|
|
97
|
+
const slug = computeEnvSlug(raw);
|
|
98
|
+
if (!slug) {
|
|
99
|
+
console.error(
|
|
100
|
+
`${FG_RED}Error:${RESET} BOXEL_ENVIRONMENT="${raw}" contains no slug characters (expected letters, digits, or "-").`,
|
|
101
|
+
);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
domain: `${slug}.localhost`,
|
|
106
|
+
matrixUrl: `http://matrix.${slug}.localhost`,
|
|
107
|
+
realmServerUrl: `http://realm-server.${slug}.localhost/`,
|
|
108
|
+
};
|
|
108
109
|
}
|
|
109
110
|
|
|
110
111
|
export async function profileCommand(
|
|
@@ -122,14 +123,39 @@ export async function profileCommand(
|
|
|
122
123
|
case 'add': {
|
|
123
124
|
const password = options?.password || process.env.BOXEL_PASSWORD;
|
|
124
125
|
if (options?.user && password) {
|
|
126
|
+
const matrixUrl = options.matrixUrl
|
|
127
|
+
? validateUrl(options.matrixUrl, '--matrix-url')
|
|
128
|
+
: undefined;
|
|
129
|
+
const realmServerUrl = options.realmServerUrl
|
|
130
|
+
? validateUrl(options.realmServerUrl, '--realm-server-url')
|
|
131
|
+
: undefined;
|
|
132
|
+
// BOXEL_ENVIRONMENT only fills in URLs when (a) at least one flag is
|
|
133
|
+
// missing, AND (b) the Matrix ID's domain isn't a known standard
|
|
134
|
+
// (stack.cards / boxel.ai / localhost). Otherwise an unrelated
|
|
135
|
+
// BOXEL_ENVIRONMENT in the shell would silently produce a profile
|
|
136
|
+
// whose Matrix ID and URLs disagree — and an invalid env value
|
|
137
|
+
// (e.g. one that slugs to empty) would kill a fully-specified
|
|
138
|
+
// invocation where the env was meant to be overridden anyway.
|
|
139
|
+
const matrixIdEnv = getEnvironmentFromMatrixId(options.user);
|
|
140
|
+
const isStandardDomain = matrixIdEnv !== 'unknown';
|
|
141
|
+
const needsEnvDefaults =
|
|
142
|
+
!isStandardDomain && (!matrixUrl || !realmServerUrl);
|
|
143
|
+
const envDefaults = needsEnvDefaults ? resolveBoxelEnvironment() : null;
|
|
144
|
+
if (envDefaults) {
|
|
145
|
+
console.log(
|
|
146
|
+
`${DIM}Using BOXEL_ENVIRONMENT=${process.env.BOXEL_ENVIRONMENT}${RESET}`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
125
149
|
await addProfileNonInteractive(
|
|
126
150
|
manager,
|
|
127
151
|
options.user,
|
|
128
152
|
password,
|
|
129
153
|
options.name,
|
|
154
|
+
matrixUrl ?? envDefaults?.matrixUrl,
|
|
155
|
+
realmServerUrl ?? envDefaults?.realmServerUrl,
|
|
130
156
|
);
|
|
131
157
|
} else {
|
|
132
|
-
await addProfile(manager);
|
|
158
|
+
await addProfile(manager, resolveBoxelEnvironment());
|
|
133
159
|
}
|
|
134
160
|
break;
|
|
135
161
|
}
|
|
@@ -200,14 +226,12 @@ async function listProfiles(manager: ProfileManager): Promise<void> {
|
|
|
200
226
|
const env = getEnvironmentFromMatrixId(id);
|
|
201
227
|
|
|
202
228
|
const marker = isActive ? `${FG_GREEN}\u2605${RESET} ` : ' ';
|
|
203
|
-
const
|
|
229
|
+
const domain = getDomainFromMatrixId(id);
|
|
204
230
|
const envColor = env === 'production' ? FG_MAGENTA : FG_CYAN;
|
|
205
231
|
|
|
206
232
|
console.log(`${marker}${BOLD}${id}${RESET}`);
|
|
207
233
|
console.log(` ${DIM}Name:${RESET} ${profile.displayName}`);
|
|
208
|
-
console.log(
|
|
209
|
-
` ${DIM}Environment:${RESET} ${envColor}${envLabel}${RESET}`,
|
|
210
|
-
);
|
|
234
|
+
console.log(` ${DIM}Environment:${RESET} ${envColor}${domain}${RESET}`);
|
|
211
235
|
console.log(` ${DIM}Realm Server:${RESET} ${profile.realmServerUrl}`);
|
|
212
236
|
console.log('');
|
|
213
237
|
}
|
|
@@ -217,34 +241,77 @@ async function listProfiles(manager: ProfileManager): Promise<void> {
|
|
|
217
241
|
}
|
|
218
242
|
}
|
|
219
243
|
|
|
220
|
-
async function
|
|
221
|
-
|
|
222
|
-
|
|
244
|
+
async function promptEnvironmentMenu(): Promise<{
|
|
245
|
+
domain: string;
|
|
246
|
+
matrixUrl: string;
|
|
247
|
+
realmServerUrl: string;
|
|
248
|
+
}> {
|
|
223
249
|
console.log(`Which environment?`);
|
|
224
250
|
console.log(` ${FG_CYAN}1${RESET}) Staging (realms-staging.stack.cards)`);
|
|
225
251
|
console.log(` ${FG_MAGENTA}2${RESET}) Production (app.boxel.ai)`);
|
|
226
252
|
console.log(` ${FG_GREEN}3${RESET}) Local (localhost:4201)`);
|
|
253
|
+
console.log(` ${FG_YELLOW}4${RESET}) Custom (enter your own URLs)`);
|
|
254
|
+
|
|
255
|
+
const envChoice = await prompt('\nChoice [1/2/3/4]: ');
|
|
256
|
+
|
|
257
|
+
if (envChoice === '4') {
|
|
258
|
+
const matrixUrlInput = await prompt('Matrix server URL: ');
|
|
259
|
+
if (!matrixUrlInput) {
|
|
260
|
+
console.error(`${FG_RED}Error:${RESET} Matrix server URL is required.`);
|
|
261
|
+
process.exit(1);
|
|
262
|
+
}
|
|
263
|
+
const matrixUrl = validateUrl(matrixUrlInput, 'Matrix server URL');
|
|
264
|
+
const realmServerUrlInput = await prompt('Realm server URL: ');
|
|
265
|
+
if (!realmServerUrlInput) {
|
|
266
|
+
console.error(`${FG_RED}Error:${RESET} Realm server URL is required.`);
|
|
267
|
+
process.exit(1);
|
|
268
|
+
}
|
|
269
|
+
const realmServerUrl = validateUrl(realmServerUrlInput, 'Realm server URL');
|
|
270
|
+
// matrixUrl is already validated by validateUrl above, so new URL won't
|
|
271
|
+
// throw — the hostname fallback is just for the unlikely edge case of
|
|
272
|
+
// a parseable URL with empty hostname (e.g. "http:///path").
|
|
273
|
+
const defaultDomain = new URL(matrixUrl).hostname || 'custom';
|
|
274
|
+
const domainInput = await prompt(
|
|
275
|
+
`Domain for Matrix ID [${defaultDomain}]: `,
|
|
276
|
+
);
|
|
277
|
+
return {
|
|
278
|
+
domain: domainInput || defaultDomain,
|
|
279
|
+
matrixUrl,
|
|
280
|
+
realmServerUrl,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (envChoice === '3') {
|
|
285
|
+
return { ...MENU_ENVIRONMENTS.local };
|
|
286
|
+
}
|
|
287
|
+
if (envChoice === '2') {
|
|
288
|
+
return { ...MENU_ENVIRONMENTS.production };
|
|
289
|
+
}
|
|
290
|
+
return { ...MENU_ENVIRONMENTS.staging };
|
|
291
|
+
}
|
|
227
292
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
293
|
+
async function addProfile(
|
|
294
|
+
manager: ProfileManager,
|
|
295
|
+
envDefaults?: EnvironmentDefaults | null,
|
|
296
|
+
): Promise<void> {
|
|
297
|
+
console.log(`\n${BOLD}Add New Profile${RESET}\n`);
|
|
231
298
|
|
|
232
299
|
let domain: string;
|
|
233
300
|
let defaultMatrixUrl: string;
|
|
234
301
|
let defaultRealmUrl: string;
|
|
235
302
|
|
|
236
|
-
if (
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
defaultRealmUrl = 'https://app.boxel.ai/';
|
|
303
|
+
if (envDefaults) {
|
|
304
|
+
console.log(
|
|
305
|
+
`${DIM}Using BOXEL_ENVIRONMENT=${process.env.BOXEL_ENVIRONMENT}${RESET}`,
|
|
306
|
+
);
|
|
307
|
+
domain = envDefaults.domain;
|
|
308
|
+
defaultMatrixUrl = envDefaults.matrixUrl;
|
|
309
|
+
defaultRealmUrl = envDefaults.realmServerUrl;
|
|
244
310
|
} else {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
311
|
+
const menuResult = await promptEnvironmentMenu();
|
|
312
|
+
domain = menuResult.domain;
|
|
313
|
+
defaultMatrixUrl = menuResult.matrixUrl;
|
|
314
|
+
defaultRealmUrl = menuResult.realmServerUrl;
|
|
248
315
|
}
|
|
249
316
|
|
|
250
317
|
console.log(`\nEnter your Boxel username (without @ or domain)`);
|
|
@@ -381,6 +448,8 @@ async function addProfileNonInteractive(
|
|
|
381
448
|
matrixId: string,
|
|
382
449
|
password: string,
|
|
383
450
|
displayName?: string,
|
|
451
|
+
matrixUrl?: string,
|
|
452
|
+
realmServerUrl?: string,
|
|
384
453
|
): Promise<void> {
|
|
385
454
|
if (!matrixId.startsWith('@') || !matrixId.includes(':')) {
|
|
386
455
|
console.error(
|
|
@@ -397,13 +466,37 @@ async function addProfileNonInteractive(
|
|
|
397
466
|
if (displayName) {
|
|
398
467
|
manager.updateDisplayName(matrixId, displayName);
|
|
399
468
|
}
|
|
469
|
+
if (matrixUrl || realmServerUrl) {
|
|
470
|
+
const urlsChanged = manager.updateUrls(matrixId, {
|
|
471
|
+
matrixUrl,
|
|
472
|
+
realmServerUrl,
|
|
473
|
+
});
|
|
474
|
+
if (urlsChanged) {
|
|
475
|
+
console.log(
|
|
476
|
+
`${DIM}Updated server URLs and cleared cached realm tokens.${RESET}`,
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
400
480
|
console.log(
|
|
401
481
|
`${FG_GREEN}\u2713${RESET} Profile updated: ${formatProfileBadge(matrixId)}`,
|
|
402
482
|
);
|
|
403
483
|
return;
|
|
404
484
|
}
|
|
405
485
|
|
|
406
|
-
|
|
486
|
+
try {
|
|
487
|
+
await manager.addProfile(
|
|
488
|
+
matrixId,
|
|
489
|
+
password,
|
|
490
|
+
displayName,
|
|
491
|
+
matrixUrl,
|
|
492
|
+
realmServerUrl,
|
|
493
|
+
);
|
|
494
|
+
} catch (err) {
|
|
495
|
+
console.error(
|
|
496
|
+
`${FG_RED}Error:${RESET} ${err instanceof Error ? err.message : String(err)}`,
|
|
497
|
+
);
|
|
498
|
+
process.exit(1);
|
|
499
|
+
}
|
|
407
500
|
console.log(
|
|
408
501
|
`${FG_GREEN}\u2713${RESET} Profile created: ${formatProfileBadge(matrixId)}`,
|
|
409
502
|
);
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import { getProfileManager, type ProfileManager } from '../lib/profile-manager';
|
|
3
|
+
import { ensureTrailingSlash } from '@cardstack/runtime-common/paths';
|
|
4
|
+
import { FG_RED, DIM, RESET } from '../lib/colors';
|
|
5
|
+
import { cliLog } from '../lib/cli-log';
|
|
6
|
+
|
|
7
|
+
export interface ReadTranspiledResult {
|
|
8
|
+
ok: boolean;
|
|
9
|
+
status?: number;
|
|
10
|
+
/** Transpiled JavaScript output as text. */
|
|
11
|
+
content?: string;
|
|
12
|
+
error?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ReadTranspiledOptions {
|
|
16
|
+
profileManager?: ProfileManager;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface ReadTranspiledCliOptions {
|
|
20
|
+
realm: string;
|
|
21
|
+
json?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Fetch the TRANSPILED JavaScript output for a realm module.
|
|
26
|
+
*
|
|
27
|
+
* Runtime evaluation errors carry line/column references that point to
|
|
28
|
+
* the transpiled output, not the raw .gts source — this lets callers
|
|
29
|
+
* inspect what the realm actually compiled. The realm accepts the
|
|
30
|
+
* module path either with or without the `.gts` extension and returns
|
|
31
|
+
* the compiled JS when fetched with `Accept: *\/*`.
|
|
32
|
+
*
|
|
33
|
+
* Uses the per-realm JWT via `ProfileManager.authedRealmFetch`.
|
|
34
|
+
*/
|
|
35
|
+
export async function readTranspiledModule(
|
|
36
|
+
realmUrl: string,
|
|
37
|
+
modulePath: string,
|
|
38
|
+
options?: ReadTranspiledOptions,
|
|
39
|
+
): Promise<ReadTranspiledResult> {
|
|
40
|
+
let pm = options?.profileManager ?? getProfileManager();
|
|
41
|
+
let active = pm.getActiveProfile();
|
|
42
|
+
if (!active) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
'No active profile. Run `boxel profile add` to create one.',
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let url = new URL(modulePath, ensureTrailingSlash(realmUrl)).href;
|
|
49
|
+
|
|
50
|
+
let response: Response;
|
|
51
|
+
try {
|
|
52
|
+
response = await pm.authedRealmFetch(url, {
|
|
53
|
+
method: 'GET',
|
|
54
|
+
headers: { Accept: '*/*' },
|
|
55
|
+
});
|
|
56
|
+
} catch (err) {
|
|
57
|
+
return {
|
|
58
|
+
ok: false,
|
|
59
|
+
error: err instanceof Error ? err.message : String(err),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!response.ok) {
|
|
64
|
+
let body = await response.text().catch(() => '(no body)');
|
|
65
|
+
return {
|
|
66
|
+
ok: false,
|
|
67
|
+
status: response.status,
|
|
68
|
+
error: `HTTP ${response.status}: ${body.slice(0, 300)}`,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let text = await response.text();
|
|
73
|
+
return { ok: true, status: response.status, content: text };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function registerReadTranspiledCommand(program: Command): void {
|
|
77
|
+
program
|
|
78
|
+
.command('read-transpiled')
|
|
79
|
+
.description(
|
|
80
|
+
"Debugging tool ONLY for investigating runtime errors in .gts modules you've written. " +
|
|
81
|
+
'Use when an eval or instantiate error reports a line/column number — those line ' +
|
|
82
|
+
'numbers refer to the transpiled output, not your .gts source, so fetching the ' +
|
|
83
|
+
'transpiled output is how you locate the offending source construct. Never use the ' +
|
|
84
|
+
'transpiled output as a reference for how to write code: do not copy its patterns ' +
|
|
85
|
+
'(setComponentTemplate, precompileTemplate, wire-format templates, base64 CSS ' +
|
|
86
|
+
'imports) into source. Always write idiomatic Ember / <template>-tag / CardDef source.',
|
|
87
|
+
)
|
|
88
|
+
.argument(
|
|
89
|
+
'<path>',
|
|
90
|
+
'Realm-relative module path. The .gts extension is optional — the realm accepts either form.',
|
|
91
|
+
)
|
|
92
|
+
.requiredOption('--realm <realm-url>', 'The realm URL to fetch from')
|
|
93
|
+
.option('--json', 'Output raw JSON response')
|
|
94
|
+
.action(async (modulePath: string, opts: ReadTranspiledCliOptions) => {
|
|
95
|
+
let result: ReadTranspiledResult;
|
|
96
|
+
try {
|
|
97
|
+
result = await readTranspiledModule(opts.realm, modulePath);
|
|
98
|
+
} catch (err) {
|
|
99
|
+
console.error(
|
|
100
|
+
`${FG_RED}Error:${RESET} ${err instanceof Error ? err.message : String(err)}`,
|
|
101
|
+
);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (opts.json) {
|
|
106
|
+
cliLog.output(JSON.stringify(result, null, 2));
|
|
107
|
+
} else if (result.ok) {
|
|
108
|
+
cliLog.output(result.content ?? '');
|
|
109
|
+
} else {
|
|
110
|
+
console.error(
|
|
111
|
+
`${DIM}Status:${RESET} ${result.status ?? '(no status)'}`,
|
|
112
|
+
);
|
|
113
|
+
console.error(`${FG_RED}Error:${RESET} ${result.error}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!result.ok) {
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import {
|
|
3
|
+
getProfileManager,
|
|
4
|
+
NO_ACTIVE_PROFILE_ERROR,
|
|
5
|
+
type ProfileManager,
|
|
6
|
+
} from '../../lib/profile-manager';
|
|
7
|
+
import { ensureTrailingSlash } from '@cardstack/runtime-common/paths';
|
|
8
|
+
import { FG_GREEN, FG_RED, RESET } from '../../lib/colors';
|
|
9
|
+
import { cliLog } from '../../lib/cli-log';
|
|
10
|
+
|
|
11
|
+
export interface CancelIndexingCommandOptions {
|
|
12
|
+
profileManager?: ProfileManager;
|
|
13
|
+
/** Also cancel queued/pending jobs. Defaults to false (running-only). */
|
|
14
|
+
cancelPending?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface CancelIndexingResult {
|
|
18
|
+
ok: boolean;
|
|
19
|
+
error?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface CancelIndexingCliOptions {
|
|
23
|
+
realm: string;
|
|
24
|
+
cancelPending?: boolean;
|
|
25
|
+
json?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Cancel indexing jobs for a realm.
|
|
30
|
+
*
|
|
31
|
+
* Sends a POST to `<realmUrl>/_cancel-indexing-job` with `{ cancelPending }`.
|
|
32
|
+
* By default cancels only running jobs; pass `cancelPending: true` to also
|
|
33
|
+
* cancel queued/pending jobs.
|
|
34
|
+
*/
|
|
35
|
+
export async function cancelIndexing(
|
|
36
|
+
realmUrl: string,
|
|
37
|
+
options?: CancelIndexingCommandOptions,
|
|
38
|
+
): Promise<CancelIndexingResult> {
|
|
39
|
+
let pm = options?.profileManager ?? getProfileManager();
|
|
40
|
+
let active = pm.getActiveProfile();
|
|
41
|
+
if (!active) {
|
|
42
|
+
return {
|
|
43
|
+
ok: false,
|
|
44
|
+
error: NO_ACTIVE_PROFILE_ERROR,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let cancelPending = options?.cancelPending ?? false;
|
|
49
|
+
let cancelUrl = `${ensureTrailingSlash(realmUrl)}_cancel-indexing-job`;
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
let response = await pm.authedRealmFetch(cancelUrl, {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
headers: {
|
|
55
|
+
Accept: 'application/json',
|
|
56
|
+
'Content-Type': 'application/json',
|
|
57
|
+
},
|
|
58
|
+
body: JSON.stringify({ cancelPending }),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (!response.ok) {
|
|
62
|
+
let body = await response.text().catch(() => '(no body)');
|
|
63
|
+
return {
|
|
64
|
+
ok: false,
|
|
65
|
+
error: `HTTP ${response.status}: ${body.slice(0, 300)}`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { ok: true };
|
|
70
|
+
} catch (err) {
|
|
71
|
+
return {
|
|
72
|
+
ok: false,
|
|
73
|
+
error: err instanceof Error ? err.message : String(err),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function registerCancelIndexingCommand(realm: Command): void {
|
|
79
|
+
realm
|
|
80
|
+
.command('cancel-indexing')
|
|
81
|
+
.description(
|
|
82
|
+
'Cancel running indexing jobs for a realm (use --cancel-pending to also cancel queued jobs)',
|
|
83
|
+
)
|
|
84
|
+
.requiredOption(
|
|
85
|
+
'--realm <realm-url>',
|
|
86
|
+
'URL of the realm to cancel indexing for',
|
|
87
|
+
)
|
|
88
|
+
.option(
|
|
89
|
+
'--cancel-pending',
|
|
90
|
+
'Also cancel queued/pending indexing jobs (default: cancel running only)',
|
|
91
|
+
)
|
|
92
|
+
.option('--json', 'Output raw JSON response')
|
|
93
|
+
.action(async (opts: CancelIndexingCliOptions) => {
|
|
94
|
+
let result = await cancelIndexing(opts.realm, {
|
|
95
|
+
cancelPending: opts.cancelPending,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (opts.json) {
|
|
99
|
+
cliLog.output(JSON.stringify(result, null, 2));
|
|
100
|
+
if (!result.ok) {
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
} else if (result.ok) {
|
|
104
|
+
let scope = opts.cancelPending ? 'running and pending' : 'running';
|
|
105
|
+
console.log(
|
|
106
|
+
`${FG_GREEN}Cancelled ${scope} indexing jobs for ${opts.realm}${RESET}`,
|
|
107
|
+
);
|
|
108
|
+
} else {
|
|
109
|
+
console.error(`${FG_RED}Error:${RESET} ${result.error}`);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
iconURLFor,
|
|
4
4
|
getRandomBackgroundURL,
|
|
5
5
|
} from '@cardstack/runtime-common/realm-display-defaults';
|
|
6
|
+
import { ensureTrailingSlash } from '@cardstack/runtime-common/paths';
|
|
6
7
|
import {
|
|
7
8
|
getProfileManager,
|
|
8
9
|
type ProfileManager,
|
|
@@ -239,7 +240,3 @@ function extractRealmUrlFromError(
|
|
|
239
240
|
`Could not determine realm URL from server error response for endpoint "${endpoint}" on "${realmServerUrl}". The response did not include an explicit realm URL.`,
|
|
240
241
|
);
|
|
241
242
|
}
|
|
242
|
-
|
|
243
|
-
function ensureTrailingSlash(url: string): string {
|
|
244
|
-
return url.endsWith('/') ? url : `${url}/`;
|
|
245
|
-
}
|