@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/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cardstack/boxel-cli",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "CLI tools for Boxel workspace management",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"bin": {
|
|
8
|
-
"boxel": "./
|
|
8
|
+
"boxel": "./bin/boxel.js"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"dist/**/*",
|
|
@@ -28,36 +28,35 @@
|
|
|
28
28
|
},
|
|
29
29
|
"author": "Cardstack",
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@aws-crypto/sha256-js": "
|
|
31
|
+
"@aws-crypto/sha256-js": "catalog:",
|
|
32
32
|
"commander": "^13.1.0",
|
|
33
33
|
"dotenv": "^16.4.7",
|
|
34
|
-
"ignore": "
|
|
34
|
+
"ignore": "catalog:",
|
|
35
|
+
"jsonwebtoken": "catalog:",
|
|
35
36
|
"p-limit": "^7.3.0"
|
|
36
37
|
},
|
|
37
38
|
"devDependencies": {
|
|
38
|
-
"@types
|
|
39
|
-
"@
|
|
40
|
-
"@
|
|
41
|
-
"
|
|
39
|
+
"@cardstack/local-types": "workspace:*",
|
|
40
|
+
"@cardstack/postgres": "workspace:*",
|
|
41
|
+
"@cardstack/runtime-common": "workspace:*",
|
|
42
|
+
"content-tag": "catalog:",
|
|
43
|
+
"@types/jsonwebtoken": "catalog:",
|
|
44
|
+
"@types/node": "catalog:",
|
|
45
|
+
"@typescript-eslint/eslint-plugin": "catalog:",
|
|
46
|
+
"@typescript-eslint/parser": "catalog:",
|
|
47
|
+
"concurrently": "catalog:",
|
|
42
48
|
"esbuild": "^0.19.0",
|
|
43
|
-
"eslint": "
|
|
44
|
-
"eslint-plugin-n": "
|
|
45
|
-
"eslint-plugin-prettier": "
|
|
46
|
-
"prettier": "
|
|
49
|
+
"eslint": "catalog:",
|
|
50
|
+
"eslint-plugin-n": "catalog:",
|
|
51
|
+
"eslint-plugin-prettier": "catalog:",
|
|
52
|
+
"prettier": "catalog:",
|
|
47
53
|
"ts-node": "^10.9.1",
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"vitest": "^2.1.9",
|
|
52
|
-
"@cardstack/local-types": "0.0.0",
|
|
53
|
-
"@cardstack/runtime-common": "1.0.0",
|
|
54
|
-
"@cardstack/postgres": "0.0.0"
|
|
55
|
-
},
|
|
56
|
-
"publishConfig": {
|
|
57
|
-
"access": "public"
|
|
54
|
+
"typescript": "catalog:",
|
|
55
|
+
"vite": "catalog:",
|
|
56
|
+
"vitest": "catalog:"
|
|
58
57
|
},
|
|
59
58
|
"scripts": {
|
|
60
|
-
"build": "pnpm clean &&
|
|
59
|
+
"build": "pnpm clean && NODE_NO_WARNINGS=1 ts-node --transpileOnly scripts/build.ts",
|
|
61
60
|
"clean": "rm -rf dist/*",
|
|
62
61
|
"start": "NODE_NO_WARNINGS=1 ts-node --transpileOnly src/index.ts",
|
|
63
62
|
"lint": "concurrently \"pnpm:lint:*(!fix)\" --names \"lint:\"",
|
|
@@ -67,6 +66,7 @@
|
|
|
67
66
|
"lint:types": "tsc --noEmit",
|
|
68
67
|
"test": "vitest run",
|
|
69
68
|
"test:unit": "vitest run --exclude 'tests/integration/**'",
|
|
69
|
+
"test:unit-exclude-smoke": "vitest run --exclude 'tests/integration/**' --exclude 'tests/smoke.test.ts'",
|
|
70
70
|
"test:integration": "./tests/scripts/run-integration-with-test-pg.sh",
|
|
71
71
|
"test:watch": "vitest",
|
|
72
72
|
"version:patch": "npm version patch",
|
|
@@ -74,5 +74,12 @@
|
|
|
74
74
|
"version:major": "npm version major",
|
|
75
75
|
"publish:npm": "npm publish",
|
|
76
76
|
"publish:dry": "npm publish --dry-run"
|
|
77
|
+
},
|
|
78
|
+
"publishConfig": {
|
|
79
|
+
"access": "public",
|
|
80
|
+
"provenance": true,
|
|
81
|
+
"bin": {
|
|
82
|
+
"boxel": "./dist/index.js"
|
|
83
|
+
}
|
|
77
84
|
}
|
|
78
|
-
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
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 { isProtectedFile } from '../../lib/realm-sync-base';
|
|
8
|
+
import { ensureTrailingSlash } from '@cardstack/runtime-common/paths';
|
|
9
|
+
import { SupportedMimeType } from '@cardstack/runtime-common/supported-mime-type';
|
|
10
|
+
import { FG_RED, DIM, RESET } from '../../lib/colors';
|
|
11
|
+
import { cliLog } from '../../lib/cli-log';
|
|
12
|
+
|
|
13
|
+
export interface DeleteResult {
|
|
14
|
+
ok: boolean;
|
|
15
|
+
error?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface DeleteCommandOptions {
|
|
19
|
+
profileManager?: ProfileManager;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface DeleteCliOptions {
|
|
23
|
+
realm: string;
|
|
24
|
+
json?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Delete a file from a realm.
|
|
29
|
+
*
|
|
30
|
+
* Sends an HTTP DELETE request for the given path within the realm.
|
|
31
|
+
* Uses the per-realm JWT via `ProfileManager.authedRealmFetch`.
|
|
32
|
+
*/
|
|
33
|
+
export async function deleteFile(
|
|
34
|
+
realmUrl: string,
|
|
35
|
+
path: string,
|
|
36
|
+
options?: DeleteCommandOptions,
|
|
37
|
+
): Promise<DeleteResult> {
|
|
38
|
+
let pm = options?.profileManager ?? getProfileManager();
|
|
39
|
+
let active = pm.getActiveProfile();
|
|
40
|
+
if (!active) {
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
error: NO_ACTIVE_PROFILE_ERROR,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (isProtectedFile(path)) {
|
|
48
|
+
return {
|
|
49
|
+
ok: false,
|
|
50
|
+
error: `Cannot delete protected file: ${path}`,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let url = new URL(path, ensureTrailingSlash(realmUrl)).href;
|
|
55
|
+
|
|
56
|
+
let response: Response;
|
|
57
|
+
try {
|
|
58
|
+
response = await pm.authedRealmFetch(url, {
|
|
59
|
+
method: 'DELETE',
|
|
60
|
+
headers: { Accept: SupportedMimeType.CardSource },
|
|
61
|
+
});
|
|
62
|
+
} catch (err) {
|
|
63
|
+
return {
|
|
64
|
+
ok: false,
|
|
65
|
+
error: err instanceof Error ? err.message : String(err),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!response.ok) {
|
|
70
|
+
let body = await response.text().catch(() => '(no body)');
|
|
71
|
+
return {
|
|
72
|
+
ok: false,
|
|
73
|
+
error: `HTTP ${response.status}: ${body.slice(0, 300)}`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { ok: true };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function registerDeleteCommand(parent: Command): void {
|
|
81
|
+
parent
|
|
82
|
+
.command('delete')
|
|
83
|
+
.description('Delete a file from a realm')
|
|
84
|
+
.argument('<path>', 'Realm-relative file path to delete')
|
|
85
|
+
.requiredOption('--realm <realm-url>', 'The realm URL to delete from')
|
|
86
|
+
.option('--json', 'Output raw JSON response')
|
|
87
|
+
.action(async (filePath: string, opts: DeleteCliOptions) => {
|
|
88
|
+
let result: DeleteResult;
|
|
89
|
+
try {
|
|
90
|
+
result = await deleteFile(opts.realm, filePath);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
console.error(
|
|
93
|
+
`${FG_RED}Error:${RESET} ${err instanceof Error ? err.message : String(err)}`,
|
|
94
|
+
);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (opts.json) {
|
|
99
|
+
cliLog.output(JSON.stringify(result, null, 2));
|
|
100
|
+
} else if (result.ok) {
|
|
101
|
+
console.log(`${DIM}Deleted:${RESET} ${filePath}`);
|
|
102
|
+
} else {
|
|
103
|
+
console.error(`${FG_RED}Error:${RESET} ${result.error}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!result.ok) {
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import { registerDeleteCommand } from './delete';
|
|
3
|
+
import { registerListCommand } from './list';
|
|
4
|
+
import { registerLintCommand } from './lint';
|
|
5
|
+
import { registerReadCommand } from './read';
|
|
6
|
+
import { registerTouchCommand } from './touch';
|
|
7
|
+
import { registerWriteCommand } from './write';
|
|
8
|
+
|
|
9
|
+
export function registerFileCommand(program: Command): void {
|
|
10
|
+
let file = program
|
|
11
|
+
.command('file')
|
|
12
|
+
.description('Read, write, search, and manage files in a realm');
|
|
13
|
+
|
|
14
|
+
registerDeleteCommand(file);
|
|
15
|
+
registerListCommand(file);
|
|
16
|
+
registerLintCommand(file);
|
|
17
|
+
registerReadCommand(file);
|
|
18
|
+
registerTouchCommand(file);
|
|
19
|
+
registerWriteCommand(file);
|
|
20
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import { readFileSync, writeFileSync } from 'fs';
|
|
3
|
+
import {
|
|
4
|
+
getProfileManager,
|
|
5
|
+
NO_ACTIVE_PROFILE_ERROR,
|
|
6
|
+
type ProfileManager,
|
|
7
|
+
} from '../../lib/profile-manager';
|
|
8
|
+
import { ensureTrailingSlash } from '@cardstack/runtime-common/paths';
|
|
9
|
+
import { SupportedMimeType } from '@cardstack/runtime-common/supported-mime-type';
|
|
10
|
+
import { FG_GREEN, FG_RED, FG_YELLOW, DIM, RESET } from '../../lib/colors';
|
|
11
|
+
import { cliLog } from '../../lib/cli-log';
|
|
12
|
+
import { write } from './write';
|
|
13
|
+
|
|
14
|
+
export interface LintMessage {
|
|
15
|
+
ruleId: string | null;
|
|
16
|
+
severity: 1 | 2;
|
|
17
|
+
message: string;
|
|
18
|
+
line: number;
|
|
19
|
+
column: number;
|
|
20
|
+
endLine?: number;
|
|
21
|
+
endColumn?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface LintResult {
|
|
25
|
+
ok: boolean;
|
|
26
|
+
error?: string;
|
|
27
|
+
fixed?: boolean;
|
|
28
|
+
output?: string;
|
|
29
|
+
messages?: LintMessage[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface LintCommandOptions {
|
|
33
|
+
profileManager?: ProfileManager;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Lint a single file's source code via the realm's `_lint` endpoint.
|
|
38
|
+
*
|
|
39
|
+
* Sends the source to `POST <realmUrl>/_lint` with `X-Filename` and
|
|
40
|
+
* `X-HTTP-Method-Override: QUERY` headers. Returns the lint result
|
|
41
|
+
* containing messages and optionally auto-fixed output.
|
|
42
|
+
*
|
|
43
|
+
* Uses the per-realm JWT via `ProfileManager.authedRealmFetch`.
|
|
44
|
+
*/
|
|
45
|
+
export async function lint(
|
|
46
|
+
realmUrl: string,
|
|
47
|
+
source: string,
|
|
48
|
+
filename: string,
|
|
49
|
+
options?: LintCommandOptions,
|
|
50
|
+
): Promise<LintResult> {
|
|
51
|
+
let pm = options?.profileManager ?? getProfileManager();
|
|
52
|
+
let active = pm.getActiveProfile();
|
|
53
|
+
if (!active) {
|
|
54
|
+
return {
|
|
55
|
+
ok: false,
|
|
56
|
+
error: NO_ACTIVE_PROFILE_ERROR,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let lintUrl = `${ensureTrailingSlash(realmUrl)}_lint`;
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
let response = await pm.authedRealmFetch(lintUrl, {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers: {
|
|
66
|
+
Accept: 'application/json',
|
|
67
|
+
'Content-Type': SupportedMimeType.CardSource,
|
|
68
|
+
'X-Filename': filename,
|
|
69
|
+
'X-HTTP-Method-Override': 'QUERY',
|
|
70
|
+
},
|
|
71
|
+
body: source,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (!response.ok) {
|
|
75
|
+
let body = await response.text().catch(() => '(no body)');
|
|
76
|
+
return {
|
|
77
|
+
ok: false,
|
|
78
|
+
error: `HTTP ${response.status}: ${body.slice(0, 300)}`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let json = (await response.json()) as {
|
|
83
|
+
fixed: boolean;
|
|
84
|
+
output: string;
|
|
85
|
+
messages: LintMessage[];
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
ok: true,
|
|
90
|
+
fixed: json.fixed,
|
|
91
|
+
output: json.output,
|
|
92
|
+
messages: json.messages,
|
|
93
|
+
};
|
|
94
|
+
} catch (err) {
|
|
95
|
+
return {
|
|
96
|
+
ok: false,
|
|
97
|
+
error: err instanceof Error ? err.message : String(err),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
interface LintCliOptions {
|
|
103
|
+
realm: string;
|
|
104
|
+
file?: string;
|
|
105
|
+
json?: boolean;
|
|
106
|
+
fix?: boolean;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function registerLintCommand(parent: Command): void {
|
|
110
|
+
parent
|
|
111
|
+
.command('lint')
|
|
112
|
+
.description('Lint a file in a realm using the realm lint endpoint')
|
|
113
|
+
.argument('<path>', 'Realm-relative file path to lint (e.g., my-card.gts)')
|
|
114
|
+
.requiredOption('--realm <realm-url>', 'The realm URL to lint against')
|
|
115
|
+
.option(
|
|
116
|
+
'--file <local-filepath>',
|
|
117
|
+
'Read source from a local file instead of fetching from the realm',
|
|
118
|
+
)
|
|
119
|
+
.option('--json', 'Output raw JSON response')
|
|
120
|
+
.option('--fix', 'Write auto-fixed output back to the source')
|
|
121
|
+
.action(async (filePath: string, opts: LintCliOptions) => {
|
|
122
|
+
let pm = getProfileManager();
|
|
123
|
+
let active = pm.getActiveProfile();
|
|
124
|
+
if (!active) {
|
|
125
|
+
console.error(`${FG_RED}Error:${RESET} ${NO_ACTIVE_PROFILE_ERROR}`);
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let source: string;
|
|
130
|
+
|
|
131
|
+
if (opts.file) {
|
|
132
|
+
try {
|
|
133
|
+
source = readFileSync(opts.file, 'utf-8');
|
|
134
|
+
} catch (err) {
|
|
135
|
+
console.error(
|
|
136
|
+
`${FG_RED}Error:${RESET} Could not read local file: ${err instanceof Error ? err.message : String(err)}`,
|
|
137
|
+
);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
let readUrl = new URL(filePath, ensureTrailingSlash(opts.realm)).href;
|
|
142
|
+
try {
|
|
143
|
+
let response = await pm.authedRealmFetch(readUrl, {
|
|
144
|
+
method: 'GET',
|
|
145
|
+
headers: { Accept: SupportedMimeType.CardSource },
|
|
146
|
+
});
|
|
147
|
+
if (!response.ok) {
|
|
148
|
+
let body = await response.text().catch(() => '(no body)');
|
|
149
|
+
console.error(
|
|
150
|
+
`${FG_RED}Error:${RESET} Could not read file from realm: HTTP ${response.status}: ${body.slice(0, 300)}`,
|
|
151
|
+
);
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
source = await response.text();
|
|
155
|
+
} catch (err) {
|
|
156
|
+
console.error(
|
|
157
|
+
`${FG_RED}Error:${RESET} ${err instanceof Error ? err.message : String(err)}`,
|
|
158
|
+
);
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let result: LintResult;
|
|
164
|
+
try {
|
|
165
|
+
result = await lint(opts.realm, source, filePath, {
|
|
166
|
+
profileManager: pm,
|
|
167
|
+
});
|
|
168
|
+
} catch (err) {
|
|
169
|
+
console.error(
|
|
170
|
+
`${FG_RED}Error:${RESET} ${err instanceof Error ? err.message : String(err)}`,
|
|
171
|
+
);
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (opts.json) {
|
|
176
|
+
cliLog.output(JSON.stringify(result, null, 2));
|
|
177
|
+
if (!result.ok) {
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!result.ok) {
|
|
184
|
+
console.error(`${FG_RED}Error:${RESET} ${result.error}`);
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Handle --fix: write fixed output back to the source
|
|
189
|
+
if (opts.fix && result.fixed && result.output) {
|
|
190
|
+
if (opts.file) {
|
|
191
|
+
writeFileSync(opts.file, result.output, 'utf-8');
|
|
192
|
+
console.log(`${FG_GREEN}Fixed:${RESET} ${opts.file}`);
|
|
193
|
+
} else {
|
|
194
|
+
let writeResult = await write(opts.realm, filePath, result.output, {
|
|
195
|
+
profileManager: pm,
|
|
196
|
+
});
|
|
197
|
+
if (!writeResult.ok) {
|
|
198
|
+
console.error(
|
|
199
|
+
`${FG_RED}Error:${RESET} Could not write fixed file: ${writeResult.error}`,
|
|
200
|
+
);
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
console.log(
|
|
204
|
+
`${FG_GREEN}Fixed:${RESET} ${filePath} ${DIM}→${RESET} ${opts.realm}`,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let messages = result.messages ?? [];
|
|
210
|
+
let errors = messages.filter((m) => m.severity === 2);
|
|
211
|
+
let warnings = messages.filter((m) => m.severity === 1);
|
|
212
|
+
|
|
213
|
+
if (messages.length === 0) {
|
|
214
|
+
console.log(`${DIM}No lint issues found.${RESET}`);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
for (let msg of messages) {
|
|
219
|
+
let color = msg.severity === 2 ? FG_RED : FG_YELLOW;
|
|
220
|
+
let level = msg.severity === 2 ? 'error' : 'warning';
|
|
221
|
+
let rule = msg.ruleId ? ` (${msg.ruleId})` : '';
|
|
222
|
+
console.log(
|
|
223
|
+
`${color}${level}${RESET} ${msg.line}:${msg.column} ${msg.message}${DIM}${rule}${RESET}`,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
console.log(
|
|
228
|
+
`\n${DIM}${errors.length} error(s), ${warnings.length} warning(s)${RESET}`,
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
if (errors.length > 0) {
|
|
232
|
+
process.exit(1);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
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 { SupportedMimeType } from '@cardstack/runtime-common/supported-mime-type';
|
|
9
|
+
import { FG_RED, DIM, RESET } from '../../lib/colors';
|
|
10
|
+
import { cliLog } from '../../lib/cli-log';
|
|
11
|
+
|
|
12
|
+
export interface ListFilesResult {
|
|
13
|
+
filenames: string[];
|
|
14
|
+
error?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ListFilesCommandOptions {
|
|
18
|
+
profileManager?: ProfileManager;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface ListFilesCliOptions {
|
|
22
|
+
realm: string;
|
|
23
|
+
json?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* List all file paths in a realm via the `_mtimes` endpoint.
|
|
28
|
+
* Returns relative paths (e.g., `hello.gts`, `Cards/my-card.json`).
|
|
29
|
+
*/
|
|
30
|
+
export async function listFiles(
|
|
31
|
+
realmUrl: string,
|
|
32
|
+
options?: ListFilesCommandOptions,
|
|
33
|
+
): Promise<ListFilesResult> {
|
|
34
|
+
let pm = options?.profileManager ?? getProfileManager();
|
|
35
|
+
let active = pm.getActiveProfile();
|
|
36
|
+
if (!active) {
|
|
37
|
+
return {
|
|
38
|
+
filenames: [],
|
|
39
|
+
error: NO_ACTIVE_PROFILE_ERROR,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let normalizedRealmUrl = ensureTrailingSlash(realmUrl);
|
|
44
|
+
let mtimesUrl = `${normalizedRealmUrl}_mtimes`;
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
let response = await pm.authedRealmFetch(mtimesUrl, {
|
|
48
|
+
method: 'GET',
|
|
49
|
+
headers: { Accept: SupportedMimeType.Mtimes },
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
if (!response.ok) {
|
|
53
|
+
let body = await response.text().catch(() => '(no body)');
|
|
54
|
+
return {
|
|
55
|
+
filenames: [],
|
|
56
|
+
error: `_mtimes returned HTTP ${response.status}: ${body.slice(0, 300)}`,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let json = (await response.json()) as {
|
|
61
|
+
data?: { attributes?: { mtimes?: Record<string, number> } };
|
|
62
|
+
};
|
|
63
|
+
let mtimes =
|
|
64
|
+
json?.data?.attributes?.mtimes ??
|
|
65
|
+
(json as unknown as Record<string, number>);
|
|
66
|
+
|
|
67
|
+
let filenames: string[] = [];
|
|
68
|
+
for (let fullUrl of Object.keys(mtimes)) {
|
|
69
|
+
if (!fullUrl.startsWith(normalizedRealmUrl)) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
let relativePath = fullUrl.slice(normalizedRealmUrl.length);
|
|
73
|
+
if (!relativePath || relativePath.endsWith('/')) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
filenames.push(relativePath);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { filenames: filenames.sort() };
|
|
80
|
+
} catch (err) {
|
|
81
|
+
return {
|
|
82
|
+
filenames: [],
|
|
83
|
+
error: err instanceof Error ? err.message : String(err),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function registerListCommand(file: Command): void {
|
|
89
|
+
file
|
|
90
|
+
.command('list')
|
|
91
|
+
.alias('ls')
|
|
92
|
+
.description('List all files in a realm')
|
|
93
|
+
.requiredOption('--realm <realm-url>', 'The realm URL to list files from')
|
|
94
|
+
.option('--json', 'Output raw JSON response')
|
|
95
|
+
.action(async (opts: ListFilesCliOptions) => {
|
|
96
|
+
let result: ListFilesResult;
|
|
97
|
+
try {
|
|
98
|
+
result = await listFiles(opts.realm);
|
|
99
|
+
} catch (err) {
|
|
100
|
+
console.error(
|
|
101
|
+
`${FG_RED}Error:${RESET} ${err instanceof Error ? err.message : String(err)}`,
|
|
102
|
+
);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (opts.json) {
|
|
107
|
+
cliLog.output(JSON.stringify(result, null, 2));
|
|
108
|
+
if (result.error) {
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
} else if (result.error) {
|
|
112
|
+
console.error(`${FG_RED}Error:${RESET} ${result.error}`);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
} else {
|
|
115
|
+
for (let filename of result.filenames) {
|
|
116
|
+
console.log(`${DIM}${filename}${RESET}`);
|
|
117
|
+
}
|
|
118
|
+
console.log(`\n${DIM}${result.filenames.length} file(s)${RESET}`);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
@@ -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 { SupportedMimeType } from '@cardstack/runtime-common/supported-mime-type';
|
|
9
|
+
import { FG_RED, DIM, RESET } from '../../lib/colors';
|
|
10
|
+
import { cliLog } from '../../lib/cli-log';
|
|
11
|
+
|
|
12
|
+
export interface ReadResult {
|
|
13
|
+
ok: boolean;
|
|
14
|
+
status?: number;
|
|
15
|
+
/** Raw text content of the file. */
|
|
16
|
+
content?: string;
|
|
17
|
+
error?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ReadCommandOptions {
|
|
21
|
+
profileManager?: ProfileManager;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface ReadCliOptions {
|
|
25
|
+
realm: string;
|
|
26
|
+
json?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Read a file from a realm. Always returns the raw text content.
|
|
31
|
+
* Callers should parse the content themselves if needed (e.g. JSON).
|
|
32
|
+
*
|
|
33
|
+
* Uses the per-realm JWT via `ProfileManager.authedRealmFetch`.
|
|
34
|
+
*/
|
|
35
|
+
export async function read(
|
|
36
|
+
realmUrl: string,
|
|
37
|
+
path: string,
|
|
38
|
+
options?: ReadCommandOptions,
|
|
39
|
+
): Promise<ReadResult> {
|
|
40
|
+
let pm = options?.profileManager ?? getProfileManager();
|
|
41
|
+
let active = pm.getActiveProfile();
|
|
42
|
+
if (!active) {
|
|
43
|
+
return {
|
|
44
|
+
ok: false,
|
|
45
|
+
error: NO_ACTIVE_PROFILE_ERROR,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let url = new URL(path, ensureTrailingSlash(realmUrl)).href;
|
|
50
|
+
|
|
51
|
+
let response: Response;
|
|
52
|
+
try {
|
|
53
|
+
response = await pm.authedRealmFetch(url, {
|
|
54
|
+
method: 'GET',
|
|
55
|
+
headers: { Accept: SupportedMimeType.CardSource },
|
|
56
|
+
});
|
|
57
|
+
} catch (err) {
|
|
58
|
+
return {
|
|
59
|
+
ok: false,
|
|
60
|
+
error: err instanceof Error ? err.message : String(err),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
let body = await response.text().catch(() => '(no body)');
|
|
66
|
+
return {
|
|
67
|
+
ok: false,
|
|
68
|
+
status: response.status,
|
|
69
|
+
error: `HTTP ${response.status}: ${body.slice(0, 300)}`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let text = await response.text();
|
|
74
|
+
return { ok: true, status: response.status, content: text };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function registerReadCommand(parent: Command): void {
|
|
78
|
+
parent
|
|
79
|
+
.command('read')
|
|
80
|
+
.description('Read a file from a realm')
|
|
81
|
+
.argument(
|
|
82
|
+
'<path>',
|
|
83
|
+
'Realm-relative file path (e.g., hello-world.json, Cards/my-card.gts)',
|
|
84
|
+
)
|
|
85
|
+
.requiredOption('--realm <realm-url>', 'The realm URL to read from')
|
|
86
|
+
.option('--json', 'Output raw JSON response')
|
|
87
|
+
.action(async (filePath: string, opts: ReadCliOptions) => {
|
|
88
|
+
let result: ReadResult;
|
|
89
|
+
try {
|
|
90
|
+
result = await read(opts.realm, filePath);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
console.error(
|
|
93
|
+
`${FG_RED}Error:${RESET} ${err instanceof Error ? err.message : String(err)}`,
|
|
94
|
+
);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (opts.json) {
|
|
99
|
+
cliLog.output(JSON.stringify(result, null, 2));
|
|
100
|
+
} else if (result.ok) {
|
|
101
|
+
cliLog.output(result.content ?? '');
|
|
102
|
+
} else {
|
|
103
|
+
console.error(
|
|
104
|
+
`${DIM}Status:${RESET} ${result.status ?? '(no status)'}`,
|
|
105
|
+
);
|
|
106
|
+
console.error(`${FG_RED}Error:${RESET} ${result.error}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!result.ok) {
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|