-
Notifications
You must be signed in to change notification settings - Fork 12
Replace pullRealmFiles with BoxelCLIClient.pull() #4422
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -17,6 +17,7 @@ interface PullOptions extends SyncOptions { | |||||||
|
|
||||||||
| class RealmPuller extends RealmSyncBase { | ||||||||
| hasError = false; | ||||||||
| downloadedFiles: string[] = []; | ||||||||
|
|
||||||||
| constructor( | ||||||||
| private pullOptions: PullOptions, | ||||||||
|
|
@@ -106,7 +107,7 @@ class RealmPuller extends RealmSyncBase { | |||||||
| }), | ||||||||
| ), | ||||||||
| ); | ||||||||
| const downloadedFiles = downloadResults.filter( | ||||||||
| this.downloadedFiles = downloadResults.filter( | ||||||||
| (f): f is string => f !== null, | ||||||||
| ); | ||||||||
|
|
||||||||
|
|
@@ -138,10 +139,10 @@ class RealmPuller extends RealmSyncBase { | |||||||
|
|
||||||||
| if ( | ||||||||
| !this.options.dryRun && | ||||||||
| downloadedFiles.length + deletedFiles.length > 0 | ||||||||
| this.downloadedFiles.length + deletedFiles.length > 0 | ||||||||
| ) { | ||||||||
| const pullChanges: CheckpointChange[] = [ | ||||||||
| ...downloadedFiles.map((f) => ({ | ||||||||
| ...this.downloadedFiles.map((f) => ({ | ||||||||
| file: f, | ||||||||
| status: 'modified' as const, | ||||||||
| })), | ||||||||
|
|
@@ -232,3 +233,46 @@ export async function pullCommand( | |||||||
| process.exit(1); | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| export async function pull( | ||||||||
| realmUrl: string, | ||||||||
| localDir: string, | ||||||||
| options: PullCommandOptions, | ||||||||
| ): Promise<{ files: string[]; error?: string }> { | ||||||||
| let pm = options.profileManager ?? getProfileManager(); | ||||||||
| let active = pm.getActiveProfile(); | ||||||||
| if (!active) { | ||||||||
| return { | ||||||||
| files: [], | ||||||||
| error: 'No active profile. Run `boxel profile add` to create one.', | ||||||||
| }; | ||||||||
|
Comment on lines
+237
to
+248
|
||||||||
| } | ||||||||
|
|
||||||||
| try { | ||||||||
| const puller = new RealmPuller( | ||||||||
| { | ||||||||
| realmUrl, | ||||||||
| localDir, | ||||||||
| deleteLocal: options.delete, | ||||||||
|
||||||||
| deleteLocal: options.delete, | |
| deleteLocal: options.delete, | |
| dryRun: options.dryRun, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Respect dry-run option in programmatic pull helper
The new exported pull() API accepts PullCommandOptions (which includes dryRun), but it does not pass dryRun into RealmPuller. As a result, pull(..., { dryRun: true }) will still write files and perform real mutations, which violates the expected no-side-effects behavior used by the CLI path.
Useful? React with 👍 / 👎.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -5,9 +5,7 @@ | |||||||||||||||||||
| * refactor to boxel-cli tool calls (CS-10529). | ||||||||||||||||||||
| */ | ||||||||||||||||||||
|
|
||||||||||||||||||||
| import { mkdirSync, writeFileSync } from 'node:fs'; | ||||||||||||||||||||
| import { dirname, join } from 'node:path'; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| import { BoxelCLIClient } from '@cardstack/boxel-cli/api'; | ||||||||||||||||||||
| import type { LooseSingleCardDocument } from '@cardstack/runtime-common'; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| import { SupportedMimeType } from '@cardstack/runtime-common/supported-mime-type'; | ||||||||||||||||||||
|
|
@@ -877,91 +875,17 @@ export async function fetchRealmFilenames( | |||||||||||||||||||
| // --------------------------------------------------------------------------- | ||||||||||||||||||||
|
|
||||||||||||||||||||
| /** | ||||||||||||||||||||
| * Download all files from a remote realm to a local directory using the | ||||||||||||||||||||
| * `_mtimes` endpoint to discover file paths. | ||||||||||||||||||||
| * | ||||||||||||||||||||
| * TODO: Replace with `boxel pull` once CS-10529 is implemented. | ||||||||||||||||||||
| * Download all files from a remote realm to a local directory. | ||||||||||||||||||||
| * Delegates to boxel-cli's pull implementation which handles auth | ||||||||||||||||||||
| * via the active profile. | ||||||||||||||||||||
| * | ||||||||||||||||||||
| * Returns the list of relative file paths that were downloaded. | ||||||||||||||||||||
| */ | ||||||||||||||||||||
| export async function pullRealmFiles( | ||||||||||||||||||||
| realmUrl: string, | ||||||||||||||||||||
| localDir: string, | ||||||||||||||||||||
| options?: RealmFetchOptions, | ||||||||||||||||||||
| _options?: RealmFetchOptions, | ||||||||||||||||||||
| ): Promise<{ files: string[]; error?: string }> { | ||||||||||||||||||||
|
||||||||||||||||||||
| ): Promise<{ files: string[]; error?: string }> { | |
| ): Promise<{ files: string[]; error?: string }> { | |
| if (_options && Object.keys(_options).length > 0) { | |
| return { | |
| files: [], | |
| error: | |
| 'pullRealmFiles no longer supports RealmFetchOptions such as authorization or injected fetch; it now uses boxel-cli pull with active-profile authentication.', | |
| }; | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Forward pullRealmFiles options to delegated client pull
pullRealmFiles still accepts RealmFetchOptions, but the new delegation ignores _options entirely and always uses a fresh BoxelCLIClient profile. Any caller that passes authorization (or a mocked fetch for controlled execution) now gets silently different behavior—most critically, private realm pulls can fail with "No active profile" even when a valid bearer token was provided.
Useful? React with 👍 / 👎.
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -12,7 +12,6 @@ import { | |||||||
| resolveTestRun, | ||||||||
| type TestRunAttributes, | ||||||||
| } from '../src/factory-test-realm'; | ||||||||
| import { pullRealmFiles } from '../src/realm-operations'; | ||||||||
|
|
||||||||
| // --------------------------------------------------------------------------- | ||||||||
| // Shared helpers | ||||||||
|
|
@@ -837,128 +836,8 @@ module('factory-test-realm > resolveTestRun', function () { | |||||||
| }); | ||||||||
| }); | ||||||||
|
|
||||||||
| // --------------------------------------------------------------------------- | ||||||||
| // pullRealmFiles | ||||||||
| // --------------------------------------------------------------------------- | ||||||||
|
|
||||||||
| module('factory-test-realm > pullRealmFiles', function () { | ||||||||
| test('downloads files listed by _mtimes', async function (assert) { | ||||||||
| let realmUrl = 'https://realms.example.test/user/personal/'; | ||||||||
| let capturedUrls: string[] = []; | ||||||||
|
|
||||||||
| let mockFetch = (async (url: string | URL | Request) => { | ||||||||
| let urlStr = String(url); | ||||||||
| capturedUrls.push(urlStr); | ||||||||
|
|
||||||||
| if (urlStr.includes('_mtimes')) { | ||||||||
| return new Response( | ||||||||
| JSON.stringify({ | ||||||||
| [`${realmUrl}hello.gts`]: 1000, | ||||||||
| [`${realmUrl}HelloCard/sample.json`]: 2000, | ||||||||
| }), | ||||||||
| { status: 200, headers: { 'Content-Type': SupportedMimeType.JSON } }, | ||||||||
| ); | ||||||||
| } | ||||||||
|
|
||||||||
| // File downloads | ||||||||
| return new Response('file-content', { status: 200 }); | ||||||||
| }) as typeof globalThis.fetch; | ||||||||
|
|
||||||||
| let tmpDir = `/tmp/sf-test-pull-${Date.now()}`; | ||||||||
| let result = await pullRealmFiles(realmUrl, tmpDir, { fetch: mockFetch }); | ||||||||
|
|
||||||||
| assert.strictEqual(result.error, undefined); | ||||||||
| assert.strictEqual(result.files.length, 2); | ||||||||
| assert.true(result.files.includes('hello.gts')); | ||||||||
| assert.true(result.files.includes('HelloCard/sample.json')); | ||||||||
|
|
||||||||
| // Should have fetched _mtimes + 2 files = 3 requests | ||||||||
| assert.strictEqual(capturedUrls.length, 3); | ||||||||
| assert.true(capturedUrls[0].includes('_mtimes')); | ||||||||
| }); | ||||||||
|
|
||||||||
| test('passes authorization header', async function (assert) { | ||||||||
| let capturedHeaders: Record<string, string>[] = []; | ||||||||
|
|
||||||||
| let mockFetch = (async ( | ||||||||
| _url: string | URL | Request, | ||||||||
| init?: RequestInit, | ||||||||
| ) => { | ||||||||
| capturedHeaders.push((init?.headers as Record<string, string>) ?? {}); | ||||||||
| // Return empty mtimes so no file downloads happen | ||||||||
| return new Response(JSON.stringify({}), { | ||||||||
| status: 200, | ||||||||
| headers: { 'Content-Type': SupportedMimeType.JSON }, | ||||||||
| }); | ||||||||
| }) as typeof globalThis.fetch; | ||||||||
|
|
||||||||
| await pullRealmFiles('https://example.test/realm/', '/tmp/unused', { | ||||||||
| fetch: mockFetch, | ||||||||
| authorization: 'Bearer my-token', | ||||||||
| }); | ||||||||
|
|
||||||||
| assert.strictEqual(capturedHeaders[0]['Authorization'], 'Bearer my-token'); | ||||||||
| }); | ||||||||
|
|
||||||||
| test('returns error on _mtimes HTTP failure', async function (assert) { | ||||||||
| let mockFetch = (async () => { | ||||||||
| return new Response('Forbidden', { status: 403 }); | ||||||||
| }) as typeof globalThis.fetch; | ||||||||
|
|
||||||||
| let result = await pullRealmFiles( | ||||||||
| 'https://example.test/realm/', | ||||||||
| '/tmp/unused', | ||||||||
| { fetch: mockFetch }, | ||||||||
| ); | ||||||||
|
|
||||||||
| assert.strictEqual(result.files.length, 0); | ||||||||
| assert.true(result.error?.includes('403')); | ||||||||
| }); | ||||||||
|
|
||||||||
| test('returns error on network failure', async function (assert) { | ||||||||
| let mockFetch = (async () => { | ||||||||
| throw new Error('ECONNREFUSED'); | ||||||||
| }) as typeof globalThis.fetch; | ||||||||
|
|
||||||||
| let result = await pullRealmFiles( | ||||||||
| 'https://example.test/realm/', | ||||||||
| '/tmp/unused', | ||||||||
| { fetch: mockFetch }, | ||||||||
| ); | ||||||||
|
|
||||||||
| assert.strictEqual(result.files.length, 0); | ||||||||
| assert.true(result.error?.includes('ECONNREFUSED')); | ||||||||
| }); | ||||||||
|
|
||||||||
| test('skips files outside the realm URL', async function (assert) { | ||||||||
| let realmUrl = 'https://realms.example.test/user/personal/'; | ||||||||
|
|
||||||||
| let mockFetch = (async (url: string | URL | Request) => { | ||||||||
| let urlStr = String(url); | ||||||||
| if (urlStr.includes('_mtimes')) { | ||||||||
| return new Response( | ||||||||
| JSON.stringify({ | ||||||||
| [`${realmUrl}hello.gts`]: 1000, | ||||||||
| ['https://other.test/evil.gts']: 2000, // outside realm | ||||||||
| }), | ||||||||
| { status: 200, headers: { 'Content-Type': SupportedMimeType.JSON } }, | ||||||||
| ); | ||||||||
| } | ||||||||
| return new Response('content', { status: 200 }); | ||||||||
| }) as typeof globalThis.fetch; | ||||||||
|
|
||||||||
| let result = await pullRealmFiles( | ||||||||
| realmUrl, | ||||||||
| `/tmp/sf-test-pull-${Date.now()}`, | ||||||||
| { | ||||||||
| fetch: mockFetch, | ||||||||
| }, | ||||||||
| ); | ||||||||
|
|
||||||||
| assert.strictEqual(result.files.length, 1); | ||||||||
| assert.strictEqual(result.files[0], 'hello.gts'); | ||||||||
| }); | ||||||||
| }); | ||||||||
| // pullRealmFiles tests removed — pullRealmFiles now delegates to | ||||||||
| // BoxelCLIClient.pull() which is tested in the boxel-cli package. | ||||||||
|
||||||||
| // BoxelCLIClient.pull() which is tested in the boxel-cli package. | |
| // BoxelCLIClient.pull(), and pull behavior is covered in the boxel-cli | |
| // package. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can drop the
pullCommandfunction and use this function inregisterPullCommandand move theconsole.logandprocess.exitthat previously handled in pullCommand toregisterPullCommand.