From 5c62756aef119f9567a85f1917fa8f4a2cc4f08f Mon Sep 17 00:00:00 2001 From: Fadhlan Ridhwanallah Date: Mon, 18 May 2026 15:06:47 +0700 Subject: [PATCH 1/3] CS-11047: Consolidate workspace-sync-cli integration tests into boxel-cli CI job MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Folds workspace-sync-cli's qunit integration suite into the boxel-cli vitest suite and removes the standalone `workspace-sync-cli-test` CI job. The `workspace-sync-cli-build` job and the package source stay (out of scope per the ticket). Audit of the seven qunit cases: - Cases 1-4 (pull, push, pull --delete, push --dry-run): already covered by existing boxel-cli specs — dropped to avoid duplicate coverage. - Case 5 (.realm.json bidirectional): deliberately inverted by design. boxel-cli treats .realm.json as a protected file and never syncs it; CS-11131 phases the sidecar out entirely. - Case 6 (REALM_SECRET_SEED password derivation): N/A. boxel-cli captures credentials via `boxel profile add`; no equivalent code path. - Case 7 (.boxelignore patterns): ported as a new `it(...)` in `realm-push.test.ts` — boxel-cli supports .boxelignore (realm-sync-base.ts) but had no integration coverage for it. The spawn-based `start-test-realm.ts` helper is replaced by boxel-cli's in-process `startTestRealmServer` (stronger cleanup, no IPC handshake). Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/ci.yaml | 53 +- ...nsolidate-workspace-sync-cli-tests-plan.md | 146 ++++ .../tests/integration/realm-push.test.ts | 29 + packages/workspace-sync-cli/.eslintrc.js | 8 - packages/workspace-sync-cli/package.json | 5 +- packages/workspace-sync-cli/tests/README.md | 79 --- .../tests/helpers/start-test-realm.ts | 343 ---------- packages/workspace-sync-cli/tests/index.ts | 1 - .../tests/integration-test.ts | 634 ------------------ pnpm-lock.yaml | 6 - 10 files changed, 177 insertions(+), 1127 deletions(-) create mode 100644 docs/cs-11047-consolidate-workspace-sync-cli-tests-plan.md delete mode 100644 packages/workspace-sync-cli/tests/README.md delete mode 100644 packages/workspace-sync-cli/tests/helpers/start-test-realm.ts delete mode 100644 packages/workspace-sync-cli/tests/index.ts delete mode 100644 packages/workspace-sync-cli/tests/integration-test.ts diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5b3f3b72c91..af892059c37 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -157,7 +157,7 @@ jobs: test-web-assets: name: Build test web assets needs: change-check - if: needs.change-check.outputs.boxel == 'true' || needs.change-check.outputs.boxel-ui == 'true' || needs.change-check.outputs.matrix == 'true' || needs.change-check.outputs.realm-server == 'true' || needs.change-check.outputs.vscode-boxel-tools == 'true' || needs.change-check.outputs.workspace-sync-cli == 'true' || needs.change-check.outputs.boxel-cli == 'true' || github.ref == 'refs/heads/main' || needs.change-check.outputs.run_all == 'true' + if: needs.change-check.outputs.boxel == 'true' || needs.change-check.outputs.boxel-ui == 'true' || needs.change-check.outputs.matrix == 'true' || needs.change-check.outputs.realm-server == 'true' || needs.change-check.outputs.vscode-boxel-tools == 'true' || needs.change-check.outputs.boxel-cli == 'true' || github.ref == 'refs/heads/main' || needs.change-check.outputs.run_all == 'true' uses: ./.github/workflows/test-web-assets.yaml with: caller: ci @@ -878,57 +878,6 @@ jobs: run: pnpm build working-directory: packages/workspace-sync-cli - workspace-sync-cli-test: - name: Workspace Sync CLI Integration Tests - needs: [change-check, test-web-assets] - if: needs.change-check.outputs.workspace-sync-cli == 'true' || github.ref == 'refs/heads/main' || needs.change-check.outputs.run_all == 'true' - runs-on: ubuntu-latest - timeout-minutes: 15 - concurrency: - group: workspace-sync-cli-test-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: ./.github/actions/init - - name: Download test web assets - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: ${{ needs.test-web-assets.outputs.artifact_name }} - path: .test-web-assets-artifact - - name: Restore test web assets into workspace - shell: bash - run: | - shopt -s dotglob - cp -a .test-web-assets-artifact/. ./ - - name: Build workspace-sync-cli - run: pnpm build - working-directory: packages/workspace-sync-cli - - name: Serve test assets (icons + host dist) - run: | - mise run ci:serve-test-assets & - timeout 180 bash -c 'until curl -ksf https://localhost:4200 > /dev/null && curl -sf http://localhost:4206 > /dev/null; do sleep 2; done' - - name: Start PostgreSQL for tests - run: pnpm start:pg | tee -a /tmp/test-services.log & - working-directory: packages/realm-server - - name: Start Matrix services for tests - run: pnpm start:matrix | tee -a /tmp/test-services.log & - working-directory: packages/realm-server - - name: Wait for PostgreSQL to accept connections - run: timeout 60 bash -c 'until (echo > /dev/tcp/127.0.0.1/5435) >/dev/null 2>&1; do sleep 1; done' - - name: Register realm users for tests - run: pnpm register-realm-users - working-directory: packages/matrix - - name: Run integration tests - run: pnpm test - working-directory: packages/workspace-sync-cli - - name: Upload test services log - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - if: ${{ !cancelled() }} - with: - name: workspace-sync-cli-test-services-log - path: /tmp/test-services.log - retention-days: 30 - boxel-cli-build: name: Boxel CLI Build needs: change-check diff --git a/docs/cs-11047-consolidate-workspace-sync-cli-tests-plan.md b/docs/cs-11047-consolidate-workspace-sync-cli-tests-plan.md new file mode 100644 index 00000000000..f43631bcdd1 --- /dev/null +++ b/docs/cs-11047-consolidate-workspace-sync-cli-tests-plan.md @@ -0,0 +1,146 @@ +# CS-11047 — Consolidate workspace-sync-cli integration tests into boxel-cli CI job + +Linear: +Branch: `cs-11047-consolidate-workspace-sync-cli-integration-tests-into-boxel` + +## Context + +`packages/workspace-sync-cli` and `packages/boxel-cli` each ship their own integration test suite hitting a real realm-server, with their own CI job. The split is historical — workspace-sync-cli predates boxel-cli. The boxel-cli job is meaningfully more robust: dedicated pre-seeded PG on port `55436`, a readiness gate, and `mise run test-services:matrix` for the surrounding stack. The workspace-sync-cli job manually spawns PG on port `5435` without a readiness gate (the cause of the flake fixed in PR #4666, commit `2950dfd256`). + +This ticket folds all workspace-sync-cli **integration coverage** into the `boxel-cli-test` job and deletes the standalone `workspace-sync-cli-test` job. The package source and its build/publish pipeline are **explicitly out of scope** per the ticket body — `workspace-sync-cli-build` stays. + +Outcome: one CI job covers both CLIs' integration suites; every retained qunit case has a vitest counterpart; no regression in coverage; the `workspace-sync-cli-test` job is gone from `ci.yaml`. + +## What the source actually covers + +`packages/workspace-sync-cli/tests/integration-test.ts` (lines 1–634, qunit) — single `module('Workspace Sync CLI Integration Tests')` with seven cases. **Case 6 is intentionally dropped** — it tests workspace-sync-cli's behavior of deriving a Matrix password from `REALM_SECRET_SEED` when `MATRIX_PASSWORD` is unset. boxel-cli does not replicate that flow: credentials are captured up-front through `boxel profile add` and stored in the profile, so there is no seed-derivation code path to test. + +| # | Line | Case | Verdict after audit | +| - | - | - | - | +| 1 | 227 | Pull files from realm to local directory | **DROP** — covered by `realm-pull.test.ts:56` ("pulls seeded files into an empty local directory") | +| 2 | 267 | Push modified files from local to realm | **DROP** — covered by `realm-push.test.ts:153, 194` (push + incremental push) | +| 3 | 336 | Pull with `--delete` removes extra local files | **DROP** — covered by `realm-pull.test.ts:123` ("removes local files missing from the realm when --delete is set") | +| 4 | 379 | Push with `--dry-run` does not modify realm | **DROP** — covered by `realm-push.test.ts:328` ("push with --dry-run makes no changes...") | +| 5 | 430 | Syncs `.realm.json` files in both directions | **DROP** — deliberate design change. boxel-cli treats `.realm.json` as a protected file (`realm-sync.test.ts:441` enforces "protected files (.realm.json) are never synced"), and CS-11131 is phasing the sidecar out entirely. | +| 6 | 519 | `REALM_SECRET_SEED` password derivation | **DROP** — boxel-cli uses `boxel profile add` for credentials; no equivalent code path. | +| 7 | 563 | Respects `.boxelignore` patterns | **PORT** — boxel-cli supports `.boxelignore` (`src/lib/realm-sync-base.ts:697`) but has no integration test for it. | + +Setup model: `hooks.before` spawns one realm server on port `4205` via `tests/helpers/start-test-realm.ts`, which itself spawns mock prerender, worker-manager, and realm-server as child processes talking over IPC. CLI is invoked via `spawn('node', [dist/push.js])` and assertions check exit code + stdout. + +## What the destination already has + +`packages/boxel-cli/tests/integration/` — 24 vitest specs. Setup pattern: + +```ts +import '../helpers/setup-realm-server'; +import { startTestRealmServer } from '../helpers/integration'; + +beforeAll(async () => { + ({ realms, testRealmHttpServer } = await startTestRealmServer({ fileSystem: { … } })); +}); +afterAll(async () => { await testRealmHttpServer.close(); }); +``` + +`startTestRealmServer` (`tests/helpers/integration.ts`) wraps `packages/realm-server/tests/helpers/index.ts → runTestRealmServerWithRealms(...)`. In-process Realm API, optional embedded worker — no spawned ts-node subprocesses, no IPC handshake. Tests call boxel-cli command functions directly and assert on return value + filesystem state. + +`tests/scripts/run-integration-with-test-pg.sh` runs: + +```bash +"${REALM_SERVER_SCRIPTS}/prepare-test-pg.sh" +trap '"${REALM_SERVER_SCRIPTS}/stop-test-pg.sh"' EXIT INT TERM +NODE_NO_WARNINGS=1 PGPORT=55436 vitest run \ + --pool=forks --poolOptions.forks.singleFork tests/integration/** +``` + +`singleFork` keeps the shared-realm pattern compatible. + +## The `start-test-realm.ts` decision + +**Replace.** Reasons: + +- The spawn+IPC approach is the same shape that caused the bug fixed in `2950dfd256`. +- boxel-cli's in-process helper has stronger cleanup: `testRealmHttpServer.close()` is awaited; no orphaned `ts-node`. +- We get free reuse of `fileSystem: { … }` seeding — fixtures become JS objects, not `fs.writeFile` calls. + +`start-test-realm.ts` is not migrated. It dies with the rest of `packages/workspace-sync-cli/tests/`. + +## Implementation steps + +### Step 1 — Audit existing coverage (done) + +See the case table above. Cases 1–6 each map to existing coverage or a deliberate design decision. Only case 7 (`.boxelignore`) needs porting. + +### Step 2 — Port case 7 to vitest + +Add a `it('respects .boxelignore patterns', …)` block inside the existing `describe('realm push (integration)', …)` in `packages/boxel-cli/tests/integration/realm-push.test.ts`. Use the existing helpers (`makeLocalDir`, `writeLocalFile`, `createTestRealm`, `push(...)`) and assert that: + +- A file listed in `.boxelignore` is not uploaded to the realm. +- Files not matched by the pattern are uploaded normally. +- The `.boxelignore` file itself is not uploaded. + +Mechanical translations: +- `module('…', hooks)` → `describe('…', () => { … })` +- `hooks.before/after` → `beforeAll/afterAll` +- `hooks.beforeEach/afterEach` → `beforeEach/afterEach` +- `test('…', async (assert) => { … })` → `it('…', async () => { … })` +- `assert.strictEqual(a, b)` → `expect(a).toBe(b)` +- `assert.deepEqual(a, b)` → `expect(a).toEqual(b)` +- `assert.ok(x)` → `expect(x).toBeTruthy()` + +**Big semantic change:** replace `spawn('node', [dist/push.js])` with direct in-process function calls. Qunit asserts on exit code + stdout regex; vitest asserts on return value + filesystem state. + +### Step 3 — Delete the moved source + +```bash +rm packages/workspace-sync-cli/tests/integration-test.ts +rm packages/workspace-sync-cli/tests/index.ts +rm -rf packages/workspace-sync-cli/tests/helpers/ +rmdir packages/workspace-sync-cli/tests/ +``` + +In `packages/workspace-sync-cli/package.json`: remove the `test` script and `qunit` + `@types/qunit` devDeps. Run `pnpm install` to update the lockfile. + +### Step 4 — Remove the standalone CI job + +Edit `.github/workflows/ci.yaml`: + +- Delete the `workspace-sync-cli-test` job block (lines ~881–930). +- Drop the dead `needs.change-check.outputs.workspace-sync-cli == 'true'` clause from the `test-web-assets` consumers' `if:` at line ~160. The `workspace-sync-cli-build` job (lines ~866–880) does not consume test web assets. +- Keep change-check outputs and the filter — `workspace-sync-cli-build` still uses them. + +### Step 5 — Local verification + +```bash +pnpm install +pnpm --filter @cardstack/boxel-cli build +pnpm --filter @cardstack/boxel-cli test:unit +pnpm --filter @cardstack/boxel-cli test:integration +``` + +All four must pass. + +## Critical files + +- `packages/workspace-sync-cli/tests/integration-test.ts` — source of truth for the 7 cases; deleted in Step 3. +- `packages/workspace-sync-cli/tests/helpers/start-test-realm.ts` — not migrated; deleted in Step 3. +- `packages/workspace-sync-cli/package.json` — Step 3 edit (remove `test` script + qunit devDeps). +- `packages/boxel-cli/tests/integration/realm-pull.test.ts` — destination for case 3. +- `packages/boxel-cli/tests/integration/realm-push.test.ts` — destination for cases 4, 7. +- `packages/boxel-cli/tests/integration/realm-sync.test.ts` — destination for case 5. +- `packages/boxel-cli/tests/helpers/integration.ts` — `startTestRealmServer` wrapper; reused. +- `packages/boxel-cli/tests/scripts/run-integration-with-test-pg.sh` — reused per the ticket. +- `.github/workflows/ci.yaml` — Step 4 edits. + +## Acceptance + +- [ ] `Boxel CLI Tests` is the single CI job covering both suites. +- [ ] Cases 1–5, 7 each map to a vitest spec or an explicit "covered by existing test X" note. Case 6 documented as deliberately dropped. +- [ ] `workspace-sync-cli-test` job removed from `ci.yaml`. +- [ ] `workspace-sync-cli-build` job still present and green. +- [ ] `packages/workspace-sync-cli/src/` untouched. + +## Verification + +1. CI Checks page: `Boxel CLI Tests` and `Workspace Sync CLI Build` pass; `Workspace Sync CLI Integration Tests` is gone. +2. Search the integration test log for the names of cases 3, 4, 5, 7 (and 1, 2 if ported). +3. After merge, `grep -rn '4205\|:5435' packages/boxel-cli/` returns nothing. diff --git a/packages/boxel-cli/tests/integration/realm-push.test.ts b/packages/boxel-cli/tests/integration/realm-push.test.ts index f46e09b0008..07ca07370d5 100644 --- a/packages/boxel-cli/tests/integration/realm-push.test.ts +++ b/packages/boxel-cli/tests/integration/realm-push.test.ts @@ -361,6 +361,35 @@ describe('realm push (integration)', () => { expect(manifest.files['.boxel-sync.json']).toBeUndefined(); }); + it('respects .boxelignore patterns', async () => { + let realmUrl = await createTestRealm(); + let localDir = makeLocalDir(); + + writeLocalFile(localDir, '.boxelignore', '*.ignore\nignore-dir/\n'); + writeLocalFile(localDir, 'card.gts', 'export const card = true;\n'); + writeLocalFile(localDir, 'test.ignore', 'should not be uploaded'); + writeLocalFile( + localDir, + 'ignore-dir/ignored.json', + '{"ignored":true}\n', + ); + + await pushCommand(localDir, realmUrl, { profileManager }); + + // Non-ignored file is uploaded + expect(await remoteFileExists(realmUrl, 'card.gts')).toBe(true); + // Files matching .boxelignore patterns are not uploaded + expect(await remoteFileExists(realmUrl, 'test.ignore')).toBe(false); + expect(await remoteFileExists(realmUrl, 'ignore-dir/ignored.json')).toBe( + false, + ); + // The .boxelignore file itself is also not uploaded (dotfile rule) + expect(await remoteFileExists(realmUrl, '.boxelignore')).toBe(false); + + let manifest = readManifest(localDir); + expect(Object.keys(manifest.files).sort()).toEqual(['card.gts']); + }); + it('pushes nested subdirectories recursively', async () => { let realmUrl = await createTestRealm(); let localDir = makeLocalDir(); diff --git a/packages/workspace-sync-cli/.eslintrc.js b/packages/workspace-sync-cli/.eslintrc.js index 835a43d5790..fc4ed8d3279 100644 --- a/packages/workspace-sync-cli/.eslintrc.js +++ b/packages/workspace-sync-cli/.eslintrc.js @@ -23,13 +23,5 @@ module.exports = { '@typescript-eslint/no-import-type-side-effects': 'error', }, }, - { - files: ['tests/**/*-test.{js,ts}'], - extends: ['plugin:qunit/recommended'], - rules: { - 'qunit/require-expect': 'off', - 'qunit/no-conditional-assertions': 'off', - }, - }, ], }; diff --git a/packages/workspace-sync-cli/package.json b/packages/workspace-sync-cli/package.json index ea0582938e6..77bce607dcb 100644 --- a/packages/workspace-sync-cli/package.json +++ b/packages/workspace-sync-cli/package.json @@ -33,14 +33,12 @@ "@cardstack/local-types": "workspace:*", "@cardstack/runtime-common": "workspace:*", "@types/node": "catalog:", - "@types/qunit": "catalog:", "esbuild": "^0.19.0", "tsx": "^4.0.0", "ts-node": "^10.9.1", "typescript": "catalog:", "concurrently": "catalog:", - "ignore": "^5.3.0", - "qunit": "^2.24.1" + "ignore": "^5.3.0" }, "scripts": { "build": "pnpm clean && tsx scripts/build.ts", @@ -51,7 +49,6 @@ "lint:fix": "concurrently \"pnpm:lint:*:fix\" --names \"fix:\"", "lint:js": "eslint . --report-unused-disable-directives --cache", "lint:js:fix": "eslint . --report-unused-disable-directives --fix", - "test": "NODE_NO_WARNINGS=1 qunit --require ts-node/register/transpile-only tests/index.ts", "version:patch": "npm version patch", "version:minor": "npm version minor", "version:major": "npm version major", diff --git a/packages/workspace-sync-cli/tests/README.md b/packages/workspace-sync-cli/tests/README.md deleted file mode 100644 index 556205234fb..00000000000 --- a/packages/workspace-sync-cli/tests/README.md +++ /dev/null @@ -1,79 +0,0 @@ -# Workspace Sync CLI Integration Tests - -This directory contains integration tests for the workspace-sync-cli that verify the push/pull functionality between local directories and Boxel workspaces. - -## Prerequisites - -Before running the tests, ensure the following services are running: - -1. **PostgreSQL Test Instance** (port 5435) - ```bash - # From the root directory - pnpm start:pg:test - ``` - Note: The tests use a separate PostgreSQL instance on port 5435 to avoid conflicts with development databases. - -2. **Matrix Server** (port 8008) - ```bash - # From the root directory - pnpm start:matrix - ``` - - Or start both test services together: - ```bash - # From the root directory - pnpm start:test:prereqs - ``` - -3. **Build the CLI** - ```bash - cd workspace-sync-cli - pnpm build - ``` - -## Running the Tests - -```bash -cd workspace-sync-cli -pnpm test -``` - -For debugging output: -```bash -DEBUG=1 pnpm test -``` - -## What the Tests Cover - -1. **Pull Operations** - - Downloading files from realm to local directory - - Respecting file ignore patterns (dotfiles) - - Preserving directory structure - -2. **Push Operations** - - Uploading files from local to realm - - Modifying existing files - - Adding new files - -3. **Command Options** - - `--delete`: Removes extra files in destination - - `--dry-run`: Preview changes without applying them - -4. **Ignore Patterns** - - `.boxelignore` file support - - Pattern matching for files and directories - -## Test Architecture - -The tests work by: -1. Creating temporary directories for testing -2. Starting an isolated realm server instance with both a worker manager and realm server -3. Running push/pull commands against the test realm -4. Verifying the expected file operations occurred -5. Cleaning up all temporary resources - -The test infrastructure: -- Realm server runs on port 4205 -- Worker manager runs on port 4212 -- Uses a unique test database for each run -- Mimics the isolated-realm-server setup used in other tests \ No newline at end of file diff --git a/packages/workspace-sync-cli/tests/helpers/start-test-realm.ts b/packages/workspace-sync-cli/tests/helpers/start-test-realm.ts deleted file mode 100644 index 1fd44dfcb13..00000000000 --- a/packages/workspace-sync-cli/tests/helpers/start-test-realm.ts +++ /dev/null @@ -1,343 +0,0 @@ -#!/usr/bin/env tsx -import type { ChildProcess } from 'child_process'; -import { spawn } from 'child_process'; -import http from 'http'; -import * as path from 'path'; -import { readFileSync } from 'fs'; - -export interface TestRealmServer { - realmProcess: ChildProcess; - workerProcess: ChildProcess; - stop: () => Promise; - executeSQL: (sql: string) => Promise[]>; -} - -export async function startTestRealmServer( - realmPath: string, - realmsRootPath: string, -): Promise { - const realmServerDir = path.join(__dirname, '..', '..', '..', 'realm-server'); - const matrixDir = path.join(__dirname, '..', '..', '..', 'matrix'); - let prerenderPort: number; - - // Use unique test database name like isolated-realm-server - const testDbName = `test_db_${Math.floor(10000000 * Math.random())}`; - - // Inherit REALM_SERVER_TLS_CERT_FILE / _KEY_FILE so the spawned - // realm-server speaks HTTPS+HTTP/2 on :4205, matching the production - // wire and the matrix harness's isolated stack. The integration tests - // below now drive `https://localhost:4205/test/` URLs end-to-end; the - // mkcert leaf (provisioned by infra:ensure-dev-cert) covers - // `localhost` so cert validation works for the canonical realm URL, - // and the WSC CLI gets `NODE_TLS_REJECT_UNAUTHORIZED=0` via the - // process env below to keep this harness independent of the global - // NODE_EXTRA_CA_CERTS chain. - const env = { - ...process.env, - PGHOST: 'localhost', - PGPORT: '5435', // Test port, not 5432 - PGUSER: 'postgres', - PGDATABASE: testDbName, - REALM_SERVER_SECRET_SEED: "mum's the word", - REALM_SECRET_SEED: "shhh! it's a secret", - GRAFANA_SECRET: "shhh! it's a secret", - MATRIX_URL: 'http://localhost:8008', - REALM_SERVER_MATRIX_USERNAME: 'realm_server', - NODE_ENV: 'test', - NODE_NO_WARNINGS: '1', - LOW_CREDIT_THRESHOLD: '2000', - // The WSC CLI under test makes its own Node-side fetches against - // the spawned realm-server. Disable strict TLS on this harness so - // the integration suite doesn't depend on whether mise's - // env-vars.sh has populated NODE_EXTRA_CA_CERTS in the test shell. - NODE_TLS_REJECT_UNAUTHORIZED: '0', - }; - - // Minimal stub prerender server to satisfy required args without needing full prerender stack - const prerenderServer = http.createServer((req, res) => { - let isModule = req.url?.includes('prerender-module'); - let payload = isModule - ? { - id: 'test-module', - status: 'ready', - nonce: 'nonce', - isShimmed: false, - lastModified: Date.now(), - createdAt: Date.now(), - deps: [], - definitions: {}, - } - : { - serialized: null, - searchDoc: null, - displayNames: null, - deps: [], - types: [], - isolatedHTML: null, - headHTML: null, - atomHTML: null, - embeddedHTML: {}, - fittedHTML: {}, - iconHTML: null, - markdown: null, - }; - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ data: { attributes: payload } })); - }); - // bind to an available port to avoid clashes with dev prerender or stale runs - await new Promise((resolve, reject) => { - prerenderServer.on('error', reject); - prerenderServer.listen(0, '127.0.0.1', () => { - let address = prerenderServer.address(); - if (address && typeof address === 'object') { - prerenderPort = address.port; - resolve(); - } else { - reject(new Error('Failed to determine prerender server port')); - } - }); - }); - // Start worker manager first - const workerArgs = [ - '--transpileOnly', - 'worker-manager', - '--port=4212', - '--matrixURL=http://localhost:8008', - `--distURL=${process.env.HOST_URL ?? 'https://localhost:4200'}`, - `--prerendererUrl=http://localhost:${prerenderPort}`, - '--migrateDB', - '--fromUrl=https://localhost:4205/test/', - '--toUrl=https://localhost:4205/test/', - '--fromUrl=https://cardstack.com/base/', - '--toUrl=https://localhost:4201/base/', - ]; - - const workerProcess = spawn('ts-node', workerArgs, { - cwd: realmServerDir, - env, - stdio: ['pipe', 'pipe', 'pipe', 'ipc'], - }); - workerProcess.on('error', (err) => { - console.error( - `[worker-error] failed to start worker-manager: ${err.message}`, - ); - }); - - if (workerProcess.stdout) { - workerProcess.stdout.on('data', (data: Buffer) => { - if (process.env.DEBUG) { - console.log(`[worker] ${data.toString().trim()}`); - } - }); - } - - if (workerProcess.stderr) { - workerProcess.stderr.on('data', (data: Buffer) => { - console.error(`[worker-error] ${data.toString().trim()}`); - }); - } - - // Now start realm server - const serverArgs = [ - '--transpileOnly', - 'main', - '--port=4205', - '--matrixURL=http://localhost:8008', - `--realmsRootPath=${realmsRootPath}`, - '--workerManagerPort=4212', - `--prerendererUrl=http://localhost:${prerenderPort}`, - '--migrateDB', - '--useRegistrationSecretFunction', - `--path=${realmPath}`, - '--username=test_realm', - '--fromUrl=https://localhost:4205/test/', - '--toUrl=https://localhost:4205/test/', - '--fromUrl=https://cardstack.com/base/', - '--toUrl=https://localhost:4201/base/', - ]; - - const realmProcess = spawn('ts-node', serverArgs, { - cwd: realmServerDir, - env, - stdio: ['pipe', 'pipe', 'pipe', 'ipc'], - }); - realmProcess.on('error', (err) => { - console.error( - `[realm-server-error] failed to start realm server: ${err.message}`, - ); - }); - - // Handle registration secret requests - realmProcess.on('message', (message) => { - if (message === 'get-registration-secret' && realmProcess.send) { - try { - const secret = readFileSync( - path.join(matrixDir, 'registration_secret.txt'), - 'utf8', - ); - realmProcess.send(`registration-secret:${secret}`); - } catch (err) { - console.error('Failed to read registration secret:', err); - realmProcess.send(`registration-secret:registration`); - } - } - }); - - if (realmProcess.stdout) { - realmProcess.stdout.on('data', (data: Buffer) => { - const output = data.toString().trim(); - if ( - process.env.DEBUG || - output.includes('error') || - output.includes('Error') - ) { - console.log(`[realm-server] ${output}`); - } - }); - } - - if (realmProcess.stderr) { - realmProcess.stderr.on('data', (data: Buffer) => { - console.error(`[realm-server-error] ${data.toString().trim()}`); - }); - } - - // Wait for ready message - try { - await new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - reject(new Error('Realm server failed to start within 60 seconds')); - }, 60000); - - const handleError = (err: Error) => { - clearTimeout(timeout); - reject(err); - }; - - workerProcess.on('error', handleError); - realmProcess.on('error', handleError); - - realmProcess.on('message', (message) => { - if (message === 'ready') { - clearTimeout(timeout); - resolve(); - } - }); - - realmProcess.on('exit', (code) => { - clearTimeout(timeout); - reject(new Error(`Realm server exited with code ${code}`)); - }); - }); - } catch (err) { - // Kill any spawned child processes that survived the startup failure - // (e.g., realm server died but worker manager is still running). They - // hold open IPC channels that would otherwise keep the qunit process - // alive after every test in the suite has reported failure. - for (let proc of [workerProcess, realmProcess]) { - if (proc.exitCode === null && proc.signalCode === null) { - try { - proc.kill('SIGKILL'); - } catch { - // process already gone - } - } - } - await new Promise((resolve) => - prerenderServer.close(() => resolve()), - ); - throw err; - } - - let sqlResults: ((results: string) => void) | undefined; - let sqlError: ((error: string) => void) | undefined; - - realmProcess.on('message', (message) => { - if (typeof message === 'string' && message.startsWith('sql-results:')) { - let results = message.substring('sql-results:'.length); - if (!sqlResults) { - console.error(`received unprompted SQL: ${results}`); - return; - } - sqlResults(results); - } else if ( - typeof message === 'string' && - message.startsWith('sql-error:') - ) { - let error = message.substring('sql-error:'.length); - if (!sqlError) { - console.error(`received unprompted SQL error: ${error}`); - return; - } - sqlError(error); - } - }); - - // Create stop function - const stop = async () => { - const realmServerStopped = new Promise((resolve) => { - realmProcess.on('message', (message) => { - if (message === 'stopped') { - resolve(); - } - }); - }); - - const workerManagerStopped = new Promise((resolve) => { - workerProcess.on('message', (message) => { - if (message === 'stopped') { - resolve(); - } - }); - }); - - realmProcess.send('stop'); - await realmServerStopped; - realmProcess.send('kill'); - - workerProcess.send('stop'); - await workerManagerStopped; - workerProcess.send('kill'); - - await new Promise((resolve) => - prerenderServer.close(() => resolve()), - ); - }; - const executeSQL = async (sql: string): Promise[]> => { - let execute = new Promise( - (resolve, reject: (reason: string) => void) => { - sqlResults = resolve; - sqlError = reject; - }, - ); - console.log('Executing SQL in realm server:', sql); - realmProcess.send(`execute-sql:${sql}`); - let resultsStr = await execute; - sqlResults = undefined; - sqlError = undefined; - return JSON.parse(resultsStr); - }; - - return { realmProcess, workerProcess, stop, executeSQL }; -} - -export async function waitForServer( - url: string, - maxAttempts = 30, -): Promise { - for (let i = 0; i < maxAttempts; i++) { - try { - const response = await fetch(url); - if (response.ok) { - return; - } - } catch (e) { - // Server not ready yet - } - await new Promise((resolve) => setTimeout(resolve, 1000)); - } - - throw new Error( - `Server at ${url} failed to start after ${maxAttempts} attempts`, - ); -} diff --git a/packages/workspace-sync-cli/tests/index.ts b/packages/workspace-sync-cli/tests/index.ts deleted file mode 100644 index 20565b9bc05..00000000000 --- a/packages/workspace-sync-cli/tests/index.ts +++ /dev/null @@ -1 +0,0 @@ -import './integration-test'; diff --git a/packages/workspace-sync-cli/tests/integration-test.ts b/packages/workspace-sync-cli/tests/integration-test.ts deleted file mode 100644 index a0370523b2e..00000000000 --- a/packages/workspace-sync-cli/tests/integration-test.ts +++ /dev/null @@ -1,634 +0,0 @@ -import { spawn } from 'child_process'; -import { promises as fs } from 'fs'; -import * as path from 'path'; -import { tmpdir } from 'os'; -import QUnit, { module, test } from 'qunit'; -import { realmPassword } from '../../matrix/helpers/realm-credentials'; -import type { TestRealmServer } from './helpers/start-test-realm'; -import { startTestRealmServer } from './helpers/start-test-realm'; - -// Ceiling for any single test. The realm server's own 60s startup budget -// lives inside `startTestRealmServer`; the test bodies are short CLI -// invocations. Without this, a hung fetch would keep qunit alive until -// the workflow timeout fires. -QUnit.config.testTimeout = 60_000; - -const REALM_PORT = 4205; // Using isolated realm server port -const MATRIX_URL = 'http://localhost:8008'; -const TEST_USERNAME = 'test_realm'; // Using test_realm username -const REALM_SECRET_SEED = "shhh! it's a secret"; - -interface TestContext { - tempDir: string; - localDir: string; - realmDir: string; - realmServer?: TestRealmServer; -} - -// Global context for shared realm server -let sharedRealmServer: TestRealmServer; -let sharedRealmDir: string; -let sharedTempDir: string; - -let context: TestContext; - -// Check if required services are running -async function checkDependencies() { - console.log('🔍 Checking dependencies...\n'); - - // Check if Matrix is running - try { - const response = await fetch(MATRIX_URL); - if (!response.ok) { - throw new Error('Matrix server is not responding'); - } - console.log('✅ Matrix server is running'); - } catch (error) { - console.error('❌ Matrix server is not running at', MATRIX_URL); - console.error(' Please start Matrix. You can use one of these methods:'); - console.error(' 1. From the root directory: pnpm start:all'); - console.error(' 2. Or just Matrix: cd matrix && docker-compose up'); - process.exit(1); - } - - // Check if CLI is built - const pushCmd = path.join(__dirname, '..', 'dist', 'push.js'); - try { - await fs.access(pushCmd); - console.log('✅ workspace-sync-cli is built'); - } catch (error) { - console.error('❌ workspace-sync-cli is not built'); - console.error( - ' Please build it using: cd workspace-sync-cli && pnpm build', - ); - process.exit(1); - } - - console.log('\n✅ All dependencies are ready\n'); -} - -async function createRealmContent(realmDir: string) { - // Create some test files in the realm directory - await fs.writeFile( - path.join(realmDir, 'card1.json'), - JSON.stringify({ title: 'Test Card 1', type: 'card' }, null, 2), - ); - - await fs.writeFile( - path.join(realmDir, '.realm.json'), - JSON.stringify({ name: 'Test Realm', version: '1.0.0' }, null, 2), - ); - - await fs.mkdir(path.join(realmDir, 'nested'), { - recursive: true, - }); - await fs.writeFile( - path.join(realmDir, 'nested', 'card2.json'), - JSON.stringify({ title: 'Test Card 2', type: 'card' }, null, 2), - ); - - await fs.writeFile( - path.join(realmDir, 'module.gts'), - `import { Component } from '@glimmer/component'; -export default class TestComponent extends Component { - message = 'Hello from test'; -}`, - ); - - // Create a file that should be ignored - await fs.writeFile(path.join(realmDir, '.hidden'), 'This should be ignored'); -} - -async function clearRealmContent(realmDir: string) { - // Safely clear realm content without deleting the directory itself - const items = await fs.readdir(realmDir); - for (const item of items) { - const itemPath = path.join(realmDir, item); - const stat = await fs.stat(itemPath); - if (stat.isDirectory()) { - await fs.rm(itemPath, { recursive: true, force: true }); - } else { - await fs.unlink(itemPath); - } - } -} - -async function runCommand( - command: string, - args: string[], - cwd: string, - customEnv?: Record, -): Promise<{ stdout: string; stderr: string; code: number }> { - const testPassword = await realmPassword(TEST_USERNAME, REALM_SECRET_SEED); - return new Promise((resolve) => { - const defaultEnv = { - ...process.env, - MATRIX_URL, - MATRIX_USERNAME: TEST_USERNAME, - MATRIX_PASSWORD: testPassword, - }; - - // If custom env is provided, use it instead of defaults - const env = customEnv ? { ...process.env, ...customEnv } : defaultEnv; - - const proc = spawn(command, args, { - cwd, - env, - }); - - let stdout = ''; - let stderr = ''; - - proc.stdout.on('data', (data) => { - stdout += data.toString(); - }); - - proc.stderr.on('data', (data) => { - stderr += data.toString(); - }); - - proc.on('close', (code) => { - resolve({ stdout, stderr, code: code || 0 }); - }); - }); -} - -module('Workspace Sync CLI Integration Tests', function (hooks) { - hooks.before(async function () { - console.log('🧪 Workspace Sync CLI Integration Tests\n'); - await checkDependencies(); - - // Create shared realm server once for all tests - console.log('🚀 Starting shared realm server...'); - sharedTempDir = await fs.mkdtemp( - path.join(tmpdir(), 'workspace-sync-shared-'), - ); - sharedRealmDir = path.join(sharedTempDir, 'realm'); - - await fs.mkdir(sharedRealmDir, { recursive: true }); - await createRealmContent(sharedRealmDir); - - try { - sharedRealmServer = await startTestRealmServer( - sharedRealmDir, - path.join(sharedTempDir, 'realms'), - ); - console.log('✅ Shared realm server is ready!\n'); - } catch (error) { - console.error('❌ Failed to start shared realm server:', error); - throw error; - } - await sharedRealmServer.executeSQL( - `INSERT INTO users (matrix_user_id) VALUES ('@test_realm:localhost') ON CONFLICT (matrix_user_id) DO NOTHING`, - ); - }); - - hooks.after(async function () { - console.log('\n🧹 Cleaning up shared realm server...'); - - if (sharedRealmServer) { - await sharedRealmServer.stop(); - } - - if (sharedTempDir) { - await fs.rm(sharedTempDir, { recursive: true, force: true }); - } - }); - - hooks.beforeEach(async function () { - console.log('🔧 Setting up test environment...\n'); - - // Create temp directories for this test - context = { - tempDir: await fs.mkdtemp(path.join(tmpdir(), 'workspace-sync-test-')), - localDir: '', - realmDir: sharedRealmDir, // Use shared realm directory - realmServer: sharedRealmServer, // Use shared realm server - }; - - context.localDir = path.join(context.tempDir, 'local'); - await fs.mkdir(context.localDir, { recursive: true }); - - // Reset realm content between tests (safely) - await clearRealmContent(sharedRealmDir); - await createRealmContent(sharedRealmDir); - console.log('✅ Test environment is ready!\n'); - }); - - hooks.afterEach(async function () { - console.log('\n🧹 Cleaning up test environment...'); - - // Only clean up test-specific temp directory - if (context?.tempDir) { - await fs.rm(context.tempDir, { recursive: true, force: true }); - } - }); - - test('Pull files from realm to local directory', async function (assert) { - const pullCmd = path.join(__dirname, '..', 'dist', 'pull.js'); - - const result = await runCommand( - 'node', - [pullCmd, `https://localhost:${REALM_PORT}/test/`, context.localDir], - process.cwd(), - ); - - assert.strictEqual( - result.code, - 0, - `Pull command should succeed: ${result.stderr}`, - ); - - // Verify files were pulled - const card1 = await fs.readFile( - path.join(context.localDir, 'card1.json'), - 'utf-8', - ); - const parsed = JSON.parse(card1); - assert.strictEqual( - parsed.title, - 'Test Card 1', - 'card1.json content should match', - ); - - const card2Exists = await fs - .access(path.join(context.localDir, 'nested', 'card2.json')) - .then(() => true) - .catch(() => false); - assert.true(card2Exists, 'nested/card2.json should be pulled'); - - const hiddenExists = await fs - .access(path.join(context.localDir, '.hidden')) - .then(() => true) - .catch(() => false); - assert.false(hiddenExists, '.hidden file should not have been pulled'); - }); - - test('Push modified files from local to realm', async function (assert) { - const pushCmd = path.join(__dirname, '..', 'dist', 'push.js'); - const pullCmd = path.join(__dirname, '..', 'dist', 'pull.js'); - - // First pull to get initial files - await runCommand( - 'node', - [pullCmd, `https://localhost:${REALM_PORT}/test/`, context.localDir], - process.cwd(), - ); - - // Modify existing file - await fs.writeFile( - path.join(context.localDir, 'card1.json'), - JSON.stringify( - { title: 'Modified Card 1', type: 'card', modified: true }, - null, - 2, - ), - ); - - // Add new file - await fs.writeFile( - path.join(context.localDir, 'new-card.json'), - JSON.stringify({ title: 'New Card', type: 'card' }, null, 2), - ); - - const result = await runCommand( - 'node', - [pushCmd, context.localDir, `https://localhost:${REALM_PORT}/test/`], - process.cwd(), - ); - - assert.strictEqual( - result.code, - 0, - `Push command should succeed: ${result.stderr}`, - ); - - // Pull again to verify changes - const verifyDir = path.join(context.tempDir, 'verify'); - await fs.mkdir(verifyDir, { recursive: true }); - - const pullResult = await runCommand( - 'node', - [pullCmd, `https://localhost:${REALM_PORT}/test/`, verifyDir], - process.cwd(), - ); - - assert.strictEqual( - pullResult.code, - 0, - `Verification pull should succeed: ${pullResult.stderr}`, - ); - - const modifiedCard = await fs.readFile( - path.join(verifyDir, 'card1.json'), - 'utf-8', - ); - const parsed = JSON.parse(modifiedCard); - assert.true(parsed.modified, 'card1.json should be properly updated'); - - const newCardExists = await fs - .access(path.join(verifyDir, 'new-card.json')) - .then(() => true) - .catch(() => false); - assert.true(newCardExists, 'new-card.json should be pushed'); - }); - - test('Pull with --delete removes extra local files', async function (assert) { - const pullCmd = path.join(__dirname, '..', 'dist', 'pull.js'); - - // First pull to get initial files - await runCommand( - 'node', - [pullCmd, `https://localhost:${REALM_PORT}/test/`, context.localDir], - process.cwd(), - ); - - // Add an extra file locally - await fs.writeFile( - path.join(context.localDir, 'should-be-deleted.json'), - JSON.stringify({ delete: 'me' }), - ); - - const result = await runCommand( - 'node', - [ - pullCmd, - `https://localhost:${REALM_PORT}/test/`, - context.localDir, - '--delete', - ], - process.cwd(), - ); - - assert.strictEqual( - result.code, - 0, - `Pull with --delete should succeed: ${result.stderr}`, - ); - - const deletedExists = await fs - .access(path.join(context.localDir, 'should-be-deleted.json')) - .then(() => true) - .catch(() => false); - assert.false( - deletedExists, - 'Extra file should be deleted with --delete option', - ); - }); - - test('Push with --dry-run does not modify realm', async function (assert) { - const pushCmd = path.join(__dirname, '..', 'dist', 'push.js'); - const pullCmd = path.join(__dirname, '..', 'dist', 'pull.js'); - - // First pull to get initial files - await runCommand( - 'node', - [pullCmd, `https://localhost:${REALM_PORT}/test/`, context.localDir], - process.cwd(), - ); - - // Create a file that would be pushed - await fs.writeFile( - path.join(context.localDir, 'dry-run-test.json'), - JSON.stringify({ title: 'Should not be pushed' }), - ); - - const result = await runCommand( - 'node', - [ - pushCmd, - context.localDir, - `https://localhost:${REALM_PORT}/test/`, - '--dry-run', - ], - process.cwd(), - ); - - assert.strictEqual( - result.code, - 0, - `Push --dry-run should succeed: ${result.stderr}`, - ); - - // Verify the file was not actually pushed - const checkDir = path.join(context.tempDir, 'dry-run-check'); - await fs.mkdir(checkDir, { recursive: true }); - - await runCommand( - 'node', - [pullCmd, `https://localhost:${REALM_PORT}/test/`, checkDir], - process.cwd(), - ); - - const dryRunExists = await fs - .access(path.join(checkDir, 'dry-run-test.json')) - .then(() => true) - .catch(() => false); - assert.false(dryRunExists, '--dry-run should not have pushed the file'); - }); - - test('Syncs .realm.json files in both directions', async function (assert) { - const pushCmd = path.join(__dirname, '..', 'dist', 'push.js'); - const pullCmd = path.join(__dirname, '..', 'dist', 'pull.js'); - - // Test pulling .realm.json - const pullResult = await runCommand( - 'node', - [pullCmd, `https://localhost:${REALM_PORT}/test/`, context.localDir], - process.cwd(), - ); - - assert.strictEqual( - pullResult.code, - 0, - `Pull .realm.json should succeed: ${pullResult.stderr}`, - ); - - // Verify .realm.json was pulled - const realmJsonExists = await fs - .access(path.join(context.localDir, '.realm.json')) - .then(() => true) - .catch(() => false); - assert.true(realmJsonExists, '.realm.json should be pulled from realm'); - - const realmJsonContent = await fs.readFile( - path.join(context.localDir, '.realm.json'), - 'utf-8', - ); - const realmConfig = JSON.parse(realmJsonContent); - assert.strictEqual( - realmConfig.name, - 'Test Realm', - '.realm.json content should match after pull', - ); - - // Modify .realm.json locally and push it back - await fs.writeFile( - path.join(context.localDir, '.realm.json'), - JSON.stringify( - { name: 'Modified Test Realm', version: '1.1.0', modified: true }, - null, - 2, - ), - ); - - const pushResult = await runCommand( - 'node', - [pushCmd, context.localDir, `https://localhost:${REALM_PORT}/test/`], - process.cwd(), - ); - - assert.strictEqual( - pushResult.code, - 0, - `Push .realm.json should succeed: ${pushResult.stderr}`, - ); - - // Verify the modification was pushed by pulling to a new directory - const verifyDir = path.join(context.tempDir, 'realm-json-verify'); - await fs.mkdir(verifyDir, { recursive: true }); - - const verifyPullResult = await runCommand( - 'node', - [pullCmd, `https://localhost:${REALM_PORT}/test/`, verifyDir], - process.cwd(), - ); - - assert.strictEqual( - verifyPullResult.code, - 0, - `Verification pull for .realm.json should succeed: ${verifyPullResult.stderr}`, - ); - - const verifyRealmJsonContent = await fs.readFile( - path.join(verifyDir, '.realm.json'), - 'utf-8', - ); - const verifyRealmConfig = JSON.parse(verifyRealmJsonContent); - assert.true( - verifyRealmConfig.modified, - '.realm.json modifications should be properly pushed', - ); - assert.strictEqual( - verifyRealmConfig.version, - '1.1.0', - '.realm.json version should be updated', - ); - }); - - test('Generates password from REALM_SECRET_SEED when MATRIX_PASSWORD not provided', async function (assert) { - const pullCmd = path.join(__dirname, '..', 'dist', 'pull.js'); - - // Verify files are not there before pulling - const card1ExistsBefore = await fs - .access(path.join(context.localDir, 'card1.json')) - .then(() => true) - .catch(() => false); - assert.false( - card1ExistsBefore, - 'Authentication with generated password should work - card1.json should be pulled', - ); - - // Test using only REALM_SECRET_SEED instead of MATRIX_PASSWORD - const pullResult = await runCommand( - 'node', - [pullCmd, `https://localhost:${REALM_PORT}/test/`, context.localDir], - process.cwd(), - { - // Remove MATRIX_PASSWORD and provide REALM_SECRET_SEED instead - MATRIX_URL, - MATRIX_USERNAME: TEST_USERNAME, - REALM_SECRET_SEED, - // Don't provide MATRIX_PASSWORD to test the fallback - }, - ); - - assert.strictEqual( - pullResult.code, - 0, - `Pull with realm secret should succeed: ${pullResult.stderr}`, - ); - - // Verify files were pulled successfully (basic smoke test) - const card1ExistsAfter = await fs - .access(path.join(context.localDir, 'card1.json')) - .then(() => true) - .catch(() => false); - assert.true( - card1ExistsAfter, - 'Authentication with generated password should work - card1.json should be pulled', - ); - }); - - test('Respects .boxelignore patterns', async function (assert) { - const pushCmd = path.join(__dirname, '..', 'dist', 'push.js'); - const pullCmd = path.join(__dirname, '..', 'dist', 'pull.js'); - - // First pull to get initial files - await runCommand( - 'node', - [pullCmd, `https://localhost:${REALM_PORT}/test/`, context.localDir], - process.cwd(), - ); - - // Create .boxelignore - await fs.writeFile( - path.join(context.localDir, '.boxelignore'), - '*.ignore\nignore-dir/\n', - ); - - // Create files that should be ignored - await fs.writeFile( - path.join(context.localDir, 'test.ignore'), - 'Should be ignored', - ); - - await fs.mkdir(path.join(context.localDir, 'ignore-dir'), { - recursive: true, - }); - await fs.writeFile( - path.join(context.localDir, 'ignore-dir', 'ignored.json'), - JSON.stringify({ ignored: true }), - ); - - const result = await runCommand( - 'node', - [pushCmd, context.localDir, `https://localhost:${REALM_PORT}/test/`], - process.cwd(), - ); - - assert.strictEqual( - result.code, - 0, - `Push with .boxelignore should succeed: ${result.stderr}`, - ); - - // Verify ignored files were not pushed - const checkDir = path.join(context.tempDir, 'ignore-check'); - await fs.mkdir(checkDir, { recursive: true }); - - await runCommand( - 'node', - [pullCmd, `https://localhost:${REALM_PORT}/test/`, checkDir], - process.cwd(), - ); - - const ignoredFileExists = await fs - .access(path.join(checkDir, 'test.ignore')) - .then(() => true) - .catch(() => false); - assert.false( - ignoredFileExists, - '.boxelignore pattern *.ignore should be respected', - ); - - const ignoredDirExists = await fs - .access(path.join(checkDir, 'ignore-dir')) - .then(() => true) - .catch(() => false); - assert.false( - ignoredDirExists, - '.boxelignore pattern ignore-dir/ should be respected', - ); - }); -}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ba51f78d440..4bf39f91987 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3346,9 +3346,6 @@ importers: '@types/node': specifier: 'catalog:' version: 24.12.4 - '@types/qunit': - specifier: 'catalog:' - version: 2.19.14 concurrently: specifier: 'catalog:' version: 8.2.2 @@ -3358,9 +3355,6 @@ importers: ignore: specifier: ^5.3.0 version: 5.3.2 - qunit: - specifier: ^2.24.1 - version: 2.25.0 ts-node: specifier: ^10.9.1 version: 10.9.2(@types/node@24.12.4)(typescript@5.9.3) From b534880e89b59232523c5ac1a754ce04704e8ac6 Mon Sep 17 00:00:00 2001 From: Fadhlan Ridhwanallah Date: Tue, 19 May 2026 16:32:58 +0700 Subject: [PATCH 2/3] CS-11047: Fix prettier lint and drop plan doc Collapse the multi-line writeLocalFile call in the new .boxelignore test to satisfy prettier's 80-col rule. Also remove the planning doc from the merged branch. Co-Authored-By: Claude Opus 4.7 (1M context) --- ...nsolidate-workspace-sync-cli-tests-plan.md | 146 ------------------ .../tests/integration/realm-push.test.ts | 6 +- 2 files changed, 1 insertion(+), 151 deletions(-) delete mode 100644 docs/cs-11047-consolidate-workspace-sync-cli-tests-plan.md diff --git a/docs/cs-11047-consolidate-workspace-sync-cli-tests-plan.md b/docs/cs-11047-consolidate-workspace-sync-cli-tests-plan.md deleted file mode 100644 index f43631bcdd1..00000000000 --- a/docs/cs-11047-consolidate-workspace-sync-cli-tests-plan.md +++ /dev/null @@ -1,146 +0,0 @@ -# CS-11047 — Consolidate workspace-sync-cli integration tests into boxel-cli CI job - -Linear: -Branch: `cs-11047-consolidate-workspace-sync-cli-integration-tests-into-boxel` - -## Context - -`packages/workspace-sync-cli` and `packages/boxel-cli` each ship their own integration test suite hitting a real realm-server, with their own CI job. The split is historical — workspace-sync-cli predates boxel-cli. The boxel-cli job is meaningfully more robust: dedicated pre-seeded PG on port `55436`, a readiness gate, and `mise run test-services:matrix` for the surrounding stack. The workspace-sync-cli job manually spawns PG on port `5435` without a readiness gate (the cause of the flake fixed in PR #4666, commit `2950dfd256`). - -This ticket folds all workspace-sync-cli **integration coverage** into the `boxel-cli-test` job and deletes the standalone `workspace-sync-cli-test` job. The package source and its build/publish pipeline are **explicitly out of scope** per the ticket body — `workspace-sync-cli-build` stays. - -Outcome: one CI job covers both CLIs' integration suites; every retained qunit case has a vitest counterpart; no regression in coverage; the `workspace-sync-cli-test` job is gone from `ci.yaml`. - -## What the source actually covers - -`packages/workspace-sync-cli/tests/integration-test.ts` (lines 1–634, qunit) — single `module('Workspace Sync CLI Integration Tests')` with seven cases. **Case 6 is intentionally dropped** — it tests workspace-sync-cli's behavior of deriving a Matrix password from `REALM_SECRET_SEED` when `MATRIX_PASSWORD` is unset. boxel-cli does not replicate that flow: credentials are captured up-front through `boxel profile add` and stored in the profile, so there is no seed-derivation code path to test. - -| # | Line | Case | Verdict after audit | -| - | - | - | - | -| 1 | 227 | Pull files from realm to local directory | **DROP** — covered by `realm-pull.test.ts:56` ("pulls seeded files into an empty local directory") | -| 2 | 267 | Push modified files from local to realm | **DROP** — covered by `realm-push.test.ts:153, 194` (push + incremental push) | -| 3 | 336 | Pull with `--delete` removes extra local files | **DROP** — covered by `realm-pull.test.ts:123` ("removes local files missing from the realm when --delete is set") | -| 4 | 379 | Push with `--dry-run` does not modify realm | **DROP** — covered by `realm-push.test.ts:328` ("push with --dry-run makes no changes...") | -| 5 | 430 | Syncs `.realm.json` files in both directions | **DROP** — deliberate design change. boxel-cli treats `.realm.json` as a protected file (`realm-sync.test.ts:441` enforces "protected files (.realm.json) are never synced"), and CS-11131 is phasing the sidecar out entirely. | -| 6 | 519 | `REALM_SECRET_SEED` password derivation | **DROP** — boxel-cli uses `boxel profile add` for credentials; no equivalent code path. | -| 7 | 563 | Respects `.boxelignore` patterns | **PORT** — boxel-cli supports `.boxelignore` (`src/lib/realm-sync-base.ts:697`) but has no integration test for it. | - -Setup model: `hooks.before` spawns one realm server on port `4205` via `tests/helpers/start-test-realm.ts`, which itself spawns mock prerender, worker-manager, and realm-server as child processes talking over IPC. CLI is invoked via `spawn('node', [dist/push.js])` and assertions check exit code + stdout. - -## What the destination already has - -`packages/boxel-cli/tests/integration/` — 24 vitest specs. Setup pattern: - -```ts -import '../helpers/setup-realm-server'; -import { startTestRealmServer } from '../helpers/integration'; - -beforeAll(async () => { - ({ realms, testRealmHttpServer } = await startTestRealmServer({ fileSystem: { … } })); -}); -afterAll(async () => { await testRealmHttpServer.close(); }); -``` - -`startTestRealmServer` (`tests/helpers/integration.ts`) wraps `packages/realm-server/tests/helpers/index.ts → runTestRealmServerWithRealms(...)`. In-process Realm API, optional embedded worker — no spawned ts-node subprocesses, no IPC handshake. Tests call boxel-cli command functions directly and assert on return value + filesystem state. - -`tests/scripts/run-integration-with-test-pg.sh` runs: - -```bash -"${REALM_SERVER_SCRIPTS}/prepare-test-pg.sh" -trap '"${REALM_SERVER_SCRIPTS}/stop-test-pg.sh"' EXIT INT TERM -NODE_NO_WARNINGS=1 PGPORT=55436 vitest run \ - --pool=forks --poolOptions.forks.singleFork tests/integration/** -``` - -`singleFork` keeps the shared-realm pattern compatible. - -## The `start-test-realm.ts` decision - -**Replace.** Reasons: - -- The spawn+IPC approach is the same shape that caused the bug fixed in `2950dfd256`. -- boxel-cli's in-process helper has stronger cleanup: `testRealmHttpServer.close()` is awaited; no orphaned `ts-node`. -- We get free reuse of `fileSystem: { … }` seeding — fixtures become JS objects, not `fs.writeFile` calls. - -`start-test-realm.ts` is not migrated. It dies with the rest of `packages/workspace-sync-cli/tests/`. - -## Implementation steps - -### Step 1 — Audit existing coverage (done) - -See the case table above. Cases 1–6 each map to existing coverage or a deliberate design decision. Only case 7 (`.boxelignore`) needs porting. - -### Step 2 — Port case 7 to vitest - -Add a `it('respects .boxelignore patterns', …)` block inside the existing `describe('realm push (integration)', …)` in `packages/boxel-cli/tests/integration/realm-push.test.ts`. Use the existing helpers (`makeLocalDir`, `writeLocalFile`, `createTestRealm`, `push(...)`) and assert that: - -- A file listed in `.boxelignore` is not uploaded to the realm. -- Files not matched by the pattern are uploaded normally. -- The `.boxelignore` file itself is not uploaded. - -Mechanical translations: -- `module('…', hooks)` → `describe('…', () => { … })` -- `hooks.before/after` → `beforeAll/afterAll` -- `hooks.beforeEach/afterEach` → `beforeEach/afterEach` -- `test('…', async (assert) => { … })` → `it('…', async () => { … })` -- `assert.strictEqual(a, b)` → `expect(a).toBe(b)` -- `assert.deepEqual(a, b)` → `expect(a).toEqual(b)` -- `assert.ok(x)` → `expect(x).toBeTruthy()` - -**Big semantic change:** replace `spawn('node', [dist/push.js])` with direct in-process function calls. Qunit asserts on exit code + stdout regex; vitest asserts on return value + filesystem state. - -### Step 3 — Delete the moved source - -```bash -rm packages/workspace-sync-cli/tests/integration-test.ts -rm packages/workspace-sync-cli/tests/index.ts -rm -rf packages/workspace-sync-cli/tests/helpers/ -rmdir packages/workspace-sync-cli/tests/ -``` - -In `packages/workspace-sync-cli/package.json`: remove the `test` script and `qunit` + `@types/qunit` devDeps. Run `pnpm install` to update the lockfile. - -### Step 4 — Remove the standalone CI job - -Edit `.github/workflows/ci.yaml`: - -- Delete the `workspace-sync-cli-test` job block (lines ~881–930). -- Drop the dead `needs.change-check.outputs.workspace-sync-cli == 'true'` clause from the `test-web-assets` consumers' `if:` at line ~160. The `workspace-sync-cli-build` job (lines ~866–880) does not consume test web assets. -- Keep change-check outputs and the filter — `workspace-sync-cli-build` still uses them. - -### Step 5 — Local verification - -```bash -pnpm install -pnpm --filter @cardstack/boxel-cli build -pnpm --filter @cardstack/boxel-cli test:unit -pnpm --filter @cardstack/boxel-cli test:integration -``` - -All four must pass. - -## Critical files - -- `packages/workspace-sync-cli/tests/integration-test.ts` — source of truth for the 7 cases; deleted in Step 3. -- `packages/workspace-sync-cli/tests/helpers/start-test-realm.ts` — not migrated; deleted in Step 3. -- `packages/workspace-sync-cli/package.json` — Step 3 edit (remove `test` script + qunit devDeps). -- `packages/boxel-cli/tests/integration/realm-pull.test.ts` — destination for case 3. -- `packages/boxel-cli/tests/integration/realm-push.test.ts` — destination for cases 4, 7. -- `packages/boxel-cli/tests/integration/realm-sync.test.ts` — destination for case 5. -- `packages/boxel-cli/tests/helpers/integration.ts` — `startTestRealmServer` wrapper; reused. -- `packages/boxel-cli/tests/scripts/run-integration-with-test-pg.sh` — reused per the ticket. -- `.github/workflows/ci.yaml` — Step 4 edits. - -## Acceptance - -- [ ] `Boxel CLI Tests` is the single CI job covering both suites. -- [ ] Cases 1–5, 7 each map to a vitest spec or an explicit "covered by existing test X" note. Case 6 documented as deliberately dropped. -- [ ] `workspace-sync-cli-test` job removed from `ci.yaml`. -- [ ] `workspace-sync-cli-build` job still present and green. -- [ ] `packages/workspace-sync-cli/src/` untouched. - -## Verification - -1. CI Checks page: `Boxel CLI Tests` and `Workspace Sync CLI Build` pass; `Workspace Sync CLI Integration Tests` is gone. -2. Search the integration test log for the names of cases 3, 4, 5, 7 (and 1, 2 if ported). -3. After merge, `grep -rn '4205\|:5435' packages/boxel-cli/` returns nothing. diff --git a/packages/boxel-cli/tests/integration/realm-push.test.ts b/packages/boxel-cli/tests/integration/realm-push.test.ts index 07ca07370d5..43f7ffd8ed7 100644 --- a/packages/boxel-cli/tests/integration/realm-push.test.ts +++ b/packages/boxel-cli/tests/integration/realm-push.test.ts @@ -368,11 +368,7 @@ describe('realm push (integration)', () => { writeLocalFile(localDir, '.boxelignore', '*.ignore\nignore-dir/\n'); writeLocalFile(localDir, 'card.gts', 'export const card = true;\n'); writeLocalFile(localDir, 'test.ignore', 'should not be uploaded'); - writeLocalFile( - localDir, - 'ignore-dir/ignored.json', - '{"ignored":true}\n', - ); + writeLocalFile(localDir, 'ignore-dir/ignored.json', '{"ignored":true}\n'); await pushCommand(localDir, realmUrl, { profileManager }); From 83d1c87c9dacebf7f347ad8dcd6a26822b3811e9 Mon Sep 17 00:00:00 2001 From: Fadhlan Ridhwanallah Date: Tue, 19 May 2026 17:54:19 +0700 Subject: [PATCH 3/3] CS-11162: Remove packages/workspace-sync-cli from monorepo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every command the package shipped has a `boxel realm`-namespaced replacement on boxel-cli (push, pull, sync). CS-11047 already moved the test suite into the boxel-cli vitest job; this commit finishes the sunset by deleting the package source, the `workspace-sync-cli-build` CI job, the `change-check.workspace-sync-cli` filter, and the `Lint Workspace Sync CLI` step. Zero in-repo dependents — verified via `git grep '"@cardstack/workspace-sync-cli"' -- '**/package.json'`. The `npm deprecate` action is handled in a sibling ticket. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/ci-lint.yaml | 4 - .github/workflows/ci.yaml | 19 - packages/workspace-sync-cli/.eslintignore | 11 - packages/workspace-sync-cli/.eslintrc.js | 27 - packages/workspace-sync-cli/.gitignore | 53 -- packages/workspace-sync-cli/README.md | 224 -------- packages/workspace-sync-cli/package.json | 61 --- packages/workspace-sync-cli/scripts/build.ts | 147 ------ packages/workspace-sync-cli/src/index.ts | 2 - packages/workspace-sync-cli/src/pull.ts | 187 ------- packages/workspace-sync-cli/src/push.ts | 182 ------- .../workspace-sync-cli/src/realm-sync-base.ts | 492 ------------------ packages/workspace-sync-cli/tsconfig.json | 38 -- pnpm-lock.yaml | 462 +++------------- 14 files changed, 61 insertions(+), 1848 deletions(-) delete mode 100644 packages/workspace-sync-cli/.eslintignore delete mode 100644 packages/workspace-sync-cli/.eslintrc.js delete mode 100644 packages/workspace-sync-cli/.gitignore delete mode 100644 packages/workspace-sync-cli/README.md delete mode 100644 packages/workspace-sync-cli/package.json delete mode 100644 packages/workspace-sync-cli/scripts/build.ts delete mode 100644 packages/workspace-sync-cli/src/index.ts delete mode 100644 packages/workspace-sync-cli/src/pull.ts delete mode 100644 packages/workspace-sync-cli/src/push.ts delete mode 100644 packages/workspace-sync-cli/src/realm-sync-base.ts delete mode 100644 packages/workspace-sync-cli/tsconfig.json diff --git a/.github/workflows/ci-lint.yaml b/.github/workflows/ci-lint.yaml index 52fe7c857fc..6af0eb8fd89 100644 --- a/.github/workflows/ci-lint.yaml +++ b/.github/workflows/ci-lint.yaml @@ -117,10 +117,6 @@ jobs: if: ${{ !cancelled() }} run: pnpm run lint working-directory: packages/bot-runner - - name: Lint Workspace Sync CLI - if: ${{ !cancelled() }} - run: pnpm run lint - working-directory: packages/workspace-sync-cli - name: Lint Boxel CLI if: ${{ !cancelled() }} run: pnpm run lint diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index af892059c37..94717b191de 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -31,7 +31,6 @@ jobs: matrix: ${{ steps.filter.outputs.matrix }} realm-server: ${{ steps.filter.outputs.realm-server }} vscode-boxel-tools: ${{ steps.filter.outputs.vscode-boxel-tools }} - workspace-sync-cli: ${{ steps.filter.outputs.workspace-sync-cli }} boxel-cli: ${{ steps.filter.outputs.boxel-cli }} bench-amd: ${{ steps.filter.outputs.bench-amd }} bench-realm: ${{ steps.filter.outputs.bench-realm }} @@ -117,9 +116,6 @@ jobs: vscode-boxel-tools: - *shared - 'packages/vscode-boxel-tools/**' - workspace-sync-cli: - - *shared - - 'packages/workspace-sync-cli/**' boxel-cli: - *shared - 'packages/boxel-cli/**' @@ -863,21 +859,6 @@ jobs: name: vscode-boxel-tools path: packages/vscode-boxel-tools/boxel-tools*vsix - workspace-sync-cli-build: - name: Workspace Sync CLI Build - needs: change-check - if: needs.change-check.outputs.workspace-sync-cli == 'true' || github.ref == 'refs/heads/main' || needs.change-check.outputs.run_all == 'true' - runs-on: ubuntu-latest - concurrency: - group: workspace-sync-cli-build-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: ./.github/actions/init - - name: Build workspace-sync-cli - run: pnpm build - working-directory: packages/workspace-sync-cli - boxel-cli-build: name: Boxel CLI Build needs: change-check diff --git a/packages/workspace-sync-cli/.eslintignore b/packages/workspace-sync-cli/.eslintignore deleted file mode 100644 index ca06c10dcad..00000000000 --- a/packages/workspace-sync-cli/.eslintignore +++ /dev/null @@ -1,11 +0,0 @@ -# Build output -dist/ - -# Dependencies -node_modules/ - -# Package files -*.tgz - -# Logs -*.log \ No newline at end of file diff --git a/packages/workspace-sync-cli/.eslintrc.js b/packages/workspace-sync-cli/.eslintrc.js deleted file mode 100644 index fc4ed8d3279..00000000000 --- a/packages/workspace-sync-cli/.eslintrc.js +++ /dev/null @@ -1,27 +0,0 @@ -'use strict'; - -module.exports = { - overrides: [ - { - files: ['./.eslintrc.js'], - parserOptions: { - sourceType: 'script', - }, - env: { - browser: false, - node: true, - }, - extends: ['plugin:n/recommended'], - rules: { - '@typescript-eslint/no-var-requires': 'off', - '@typescript-eslint/consistent-type-imports': [ - 'error', - { - disallowTypeAnnotations: false, - }, - ], - '@typescript-eslint/no-import-type-side-effects': 'error', - }, - }, - ], -}; diff --git a/packages/workspace-sync-cli/.gitignore b/packages/workspace-sync-cli/.gitignore deleted file mode 100644 index 3736389a7eb..00000000000 --- a/packages/workspace-sync-cli/.gitignore +++ /dev/null @@ -1,53 +0,0 @@ -# Build output -dist/ - -# Dependencies -node_modules/ - -# Logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Coverage directory used by tools like istanbul -coverage/ - -# nyc test coverage -.nyc_output - -# Compiled binary addons -build/Release - -# Dependency directories -node_modules/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# OS generated files -.DS_Store -.DS_Store? -._* -.Spotlight-V100 -.Trashes -ehthumbs.db -Thumbs.db \ No newline at end of file diff --git a/packages/workspace-sync-cli/README.md b/packages/workspace-sync-cli/README.md deleted file mode 100644 index 362ee8ad701..00000000000 --- a/packages/workspace-sync-cli/README.md +++ /dev/null @@ -1,224 +0,0 @@ -# Workspace Sync CLI - -CLI tools for syncing files between local directories and Boxel workspaces. - -## Installation - -### Global Installation (Recommended) - -```bash -npm install -g @cardstack/workspace-sync-cli -``` - -### Per-project Installation - -```bash -npm install @cardstack/workspace-sync-cli -npx workspace-push --help -npx workspace-pull --help -``` - -### Development Installation - -```bash -git clone https://github.com/cardstack/boxel.git -cd boxel/packages/realm-sync-cli -pnpm install -pnpm package -npm install -g ... -``` - -## Authentication - -Both commands require Matrix credentials to authenticate with the workspace: - -```bash -export MATRIX_URL="https://matrix.boxel.ai" -export MATRIX_USERNAME="your-username" -export MATRIX_PASSWORD="your-password" - -# Optionally you can provide the realm's secret seed and the CLI will derive -# the matrix credentials from the workspace URL: -# /// -> realm/_ -# /base/, /skills/, ... -> _realm -# /published// -> realm/published_ -export REALM_SECRET_SEED="super-secret-seed" -``` - -## Usage - -### Push (Local → Workspace) - -Uploads files from a local directory to a workspace. - -```bash -workspace-push [OPTIONS] -``` - -**Arguments:** - -- `LOCAL_DIR` - The local directory containing files to sync -- `WORKSPACE_URL` - The URL of the target workspace (e.g., https://app.boxel.ai/demo/) - -**Options:** - -- `--delete` - Delete remote files that don't exist locally -- `--dry-run` - Show what would be done without making changes -- `--help, -h` - Show help message - -**Examples:** - -```bash -workspace-push ./my-cards https://app.boxel.ai/demo/ -workspace-push ./my-cards https://app.boxel.ai/demo/ --delete --dry-run -``` - -### Pull (Workspace → Local) - -Downloads files from a workspace to a local directory. - -```bash -workspace-pull [OPTIONS] -``` - -**Arguments:** - -- `WORKSPACE_URL` - The URL of the source workspace (e.g., https://app.boxel.ai/demo/) -- `LOCAL_DIR` - The local directory to sync files to - -**Options:** - -- `--delete` - Delete local files that don't exist in the workspace -- `--dry-run` - Show what would be done without making changes -- `--help, -h` - Show help message - -**Examples:** - -```bash -workspace-pull https://app.boxel.ai/demo/ ./my-cards -workspace-pull https://app.boxel.ai/demo/ ./my-cards --delete --dry-run -``` - -## File Filtering - -The sync commands automatically filter files based on several criteria to avoid syncing unwanted content: - -### Automatic Filtering - -- **Dotfiles**: All files and directories starting with a dot (`.`) are automatically ignored - - Examples: `.DS_Store`, `.env`, `.git/`, `.vscode/` -- **`.gitignore` files**: Standard gitignore patterns are respected -- **Hierarchical**: Checks for `.gitignore` files in current directory and all parent directories -- **Standard patterns**: Supports all gitignore pattern syntax (wildcards, negation, etc.) - -### Boxelignore Support - -- **`.boxelignore` files**: Workspace-specific ignore patterns (same syntax as `.gitignore`) -- **Use case**: Exclude files from workspace sync while keeping them in git -- **Priority**: Applied in addition to `.gitignore` patterns -- **Hierarchical**: Works the same way as `.gitignore` files - -### Example Ignore Files - -**`.gitignore`** (standard git ignoring): - -``` -node_modules/ -*.log -.env -``` - -**`.boxelignore`** (workspace-specific ignoring): - -``` -# Keep in git but exclude from workspace -docs/ -test-data/ -*.draft.gts -development-cards/ -``` - -## Development - -### Building - -```bash -# Clean and build bundled executables -pnpm build -``` - -### Development Scripts - -```bash -pnpm push [OPTIONS] -pnpm pull [OPTIONS] -``` - -### Code Quality - -```bash -# Linting -pnpm lint -pnpm lint:fix -``` - -### Publishing - -```bash -# Version bumping -pnpm version:patch # 0.1.0 -> 0.1.1 -pnpm version:minor # 0.1.0 -> 0.2.0 -pnpm version:major # 0.1.0 -> 1.0.0 - -# Publishing -pnpm publish:dry # Dry run to see what would be published -pnpm publish:npm # Publish to npm registry -``` - -### Testing Built Version - -```bash -# Build and test locally -pnpm build -node dist/push.js --help -node dist/pull.js --help - -# Test as installed package -npm pack -npm install -g ./cardstack-realm-sync-cli-0.1.0.tgz -workspace-push --help -``` - -## Features - -- **Bundled executables** - Single-file binaries with all dependencies included -- **Recursive directory syncing** - Handles nested folder structures -- **Smart file filtering** - Respects `.gitignore` and `.boxelignore` patterns, skips dotfiles -- **Safe authentication** - Tests workspace access before destructive operations -- **Detailed logging** - Clear feedback on all operations -- **Dry-run mode** - Preview changes without making them - -## Architecture - -The package uses esbuild to create standalone executables that bundle all dependencies: - -- **`workspace-push`** - Standalone executable for uploading files to workspaces -- **`workspace-pull`** - Standalone executable for downloading files from workspaces -- **Library API** - Programmatic access via `@cardstack/workspace-sync-cli` - -### Components - -- `WorkspaceSyncBase` - Abstract base class with common sync functionality -- `WorkspacePusher` - Implements push (local → workspace) synchronization -- `WorkspacePuller` - Implements pull (workspace → local) synchronization - -Authentication is handled through the bundled `@cardstack/runtime-common` package using Matrix credentials. - -## Requirements - -- Node.js 18 or higher -- Matrix account with workspace access permissions - -## License - -MIT diff --git a/packages/workspace-sync-cli/package.json b/packages/workspace-sync-cli/package.json deleted file mode 100644 index 77bce607dcb..00000000000 --- a/packages/workspace-sync-cli/package.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "name": "@cardstack/workspace-sync-cli", - "version": "1.0.1", - "license": "MIT", - "description": "CLI tools for syncing files between local directories and Boxel workspaces", - "main": "./dist/index.js", - "bin": { - "workspace-push": "./dist/push.js", - "workspace-pull": "./dist/pull.js" - }, - "files": [ - "dist/**/*", - "README.md" - ], - "engines": { - "node": ">=18.0.0" - }, - "keywords": [ - "boxel", - "workspace", - "sync", - "cli", - "cardstack" - ], - "repository": { - "type": "git", - "url": "https://github.com/cardstack/boxel.git", - "directory": "packages/realm-sync-cli" - }, - "author": "Cardstack", - "dependencies": {}, - "devDependencies": { - "@cardstack/local-types": "workspace:*", - "@cardstack/runtime-common": "workspace:*", - "@types/node": "catalog:", - "esbuild": "^0.19.0", - "tsx": "^4.0.0", - "ts-node": "^10.9.1", - "typescript": "catalog:", - "concurrently": "catalog:", - "ignore": "^5.3.0" - }, - "scripts": { - "build": "pnpm clean && tsx scripts/build.ts", - "clean": "rm -rf dist/*", - "push": "NODE_NO_WARNINGS=1 ts-node --transpileOnly src/push.ts", - "pull": "NODE_NO_WARNINGS=1 ts-node --transpileOnly src/pull.ts", - "lint": "concurrently \"pnpm:lint:*(!fix)\" --names \"lint:\"", - "lint:fix": "concurrently \"pnpm:lint:*:fix\" --names \"fix:\"", - "lint:js": "eslint . --report-unused-disable-directives --cache", - "lint:js:fix": "eslint . --report-unused-disable-directives --fix", - "version:patch": "npm version patch", - "version:minor": "npm version minor", - "version:major": "npm version major", - "publish:npm": "npm publish", - "publish:dry": "npm publish --dry-run" - }, - "publishConfig": { - "access": "public" - } -} diff --git a/packages/workspace-sync-cli/scripts/build.ts b/packages/workspace-sync-cli/scripts/build.ts deleted file mode 100644 index 5a587384e39..00000000000 --- a/packages/workspace-sync-cli/scripts/build.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { build } from 'esbuild'; -import { copyFileSync, mkdirSync, chmodSync } from 'fs'; -import { createRequire } from 'module'; -import { dirname, join } from 'path'; - -const require = createRequire(import.meta.url); - -// Node.js built-in modules that should remain external -const nodeBuiltins = [ - 'fs', - 'path', - 'url', - 'crypto', - 'os', - 'stream', - 'util', - 'events', - 'buffer', - 'string_decoder', - 'querystring', - 'http', - 'https', - 'net', - 'tls', - 'zlib', - 'worker_threads', - 'child_process', - 'cluster', - 'dgram', - 'dns', - 'domain', - 'readline', - 'repl', - 'tty', - 'v8', - 'vm', - 'assert', - 'constants', - 'module', - 'perf_hooks', - 'process', - 'punycode', - 'timers', - 'trace_events', -]; - -const commonConfig = { - bundle: true, - platform: 'node' as const, - target: 'node18', - format: 'cjs' as const, - external: nodeBuiltins, - sourcemap: false, // Disable source maps for production - minify: true, // Enable minification to reduce size - metafile: true, // For bundle analysis - logLevel: 'info' as const, - treeShaking: true, // Enable tree shaking - define: { - // Ensure NODE_ENV is defined - 'process.env.NODE_ENV': '"production"', - }, - // More aggressive bundling optimizations - mainFields: ['module', 'main'], - conditions: ['import', 'require'], -}; - -async function buildCLI() { - // Ensure dist directory exists - mkdirSync('dist', { recursive: true }); - - console.log('Building CLI executables...'); - - try { - // Build push command - console.log('Building realm-push...'); - const pushResult = await build({ - ...commonConfig, - entryPoints: ['src/push.ts'], - outfile: 'dist/push.js', - banner: { - js: '#!/usr/bin/env node', - }, - }); - - // Build pull command - console.log('Building realm-pull...'); - const pullResult = await build({ - ...commonConfig, - entryPoints: ['src/pull.ts'], - outfile: 'dist/pull.js', - banner: { - js: '#!/usr/bin/env node', - }, - }); - - // Build library entry point (for programmatic use) - console.log('Building library...'); - const libResult = await build({ - ...commonConfig, - entryPoints: ['src/index.ts'], - outfile: 'dist/index.js', - }); - - // Make CLI files executable - console.log('Making CLI files executable...'); - chmodSync('dist/push.js', 0o755); - chmodSync('dist/pull.js', 0o755); - - // content-tag (pulled in by runtime-common/transpile) embeds a - // `require('fs').readFileSync(path.join(__dirname, 'content_tag_bg.wasm'))` - // call that survives bundling. Copy the wasm next to dist/*.js so that - // call resolves at runtime. content-tag's default node entry is - // pkg/node.cjs; the wasm ships alongside the inner pkg/node/ module it - // re-exports. - const contentTagEntry = require.resolve('content-tag'); - copyFileSync( - join(dirname(contentTagEntry), 'node', 'content_tag_bg.wasm'), - 'dist/content_tag_bg.wasm', - ); - - console.log('✅ Build complete!'); - - // Log bundle sizes - const bundleInfo = [ - { name: 'realm-push', result: pushResult }, - { name: 'realm-pull', result: pullResult }, - { name: 'library', result: libResult }, - ]; - - console.log('\n📦 Bundle sizes:'); - for (const { name, result } of bundleInfo) { - if (result.metafile) { - const outputs = Object.values(result.metafile.outputs); - const totalSize = outputs.reduce( - (sum, output) => sum + output.bytes, - 0, - ); - console.log(` ${name}: ${(totalSize / 1024).toFixed(1)} KB`); - } - } - } catch (error) { - console.error('❌ Build failed:', error); - process.exit(1); - } -} - -buildCLI(); diff --git a/packages/workspace-sync-cli/src/index.ts b/packages/workspace-sync-cli/src/index.ts deleted file mode 100644 index d913b1d8112..00000000000 --- a/packages/workspace-sync-cli/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { RealmSyncBase, validateMatrixEnvVars } from './realm-sync-base'; -export type { SyncOptions } from './realm-sync-base'; diff --git a/packages/workspace-sync-cli/src/pull.ts b/packages/workspace-sync-cli/src/pull.ts deleted file mode 100644 index e51cf02c209..00000000000 --- a/packages/workspace-sync-cli/src/pull.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { RealmSyncBase, validateMatrixEnvVars } from './realm-sync-base'; -import * as fs from 'fs'; -import * as path from 'path'; - -interface PullOptions { - workspaceUrl: string; - localDir: string; - deleteLocal?: boolean; - dryRun?: boolean; -} - -class RealmPuller extends RealmSyncBase { - constructor( - private pullOptions: PullOptions, - matrixUrl: string, - username: string, - password: string, - ) { - super(pullOptions, matrixUrl, username, password); - } - - async sync() { - console.log( - `Starting pull from ${this.options.workspaceUrl} to ${this.options.localDir}`, - ); - - // Test authentication by trying to access the workspace root first - console.log('Testing workspace access...'); - try { - await this.getRemoteFileList(''); // Test with empty path (root) - } catch (error) { - console.error('Failed to access workspace:', error); - throw new Error( - 'Cannot proceed with pull: Authentication or access failed. ' + - 'Please check your Matrix credentials and workspace permissions.', - ); - } - console.log('Workspace access verified'); - - // Get current remote file listing - const remoteFiles = await this.getRemoteFileList(); - console.log(`Found ${remoteFiles.size} files in remote workspace`); - - // Get local file listing - const localFiles = await this.getLocalFileList(); - console.log(`Found ${localFiles.size} files in local directory`); - - // Create local directory if it doesn't exist - if (!fs.existsSync(this.options.localDir)) { - if (this.options.dryRun) { - console.log( - `[DRY RUN] Would create directory: ${this.options.localDir}`, - ); - } else { - fs.mkdirSync(this.options.localDir, { recursive: true }); - console.log(`Created directory: ${this.options.localDir}`); - } - } - - // Download remote files - for (const [relativePath] of remoteFiles) { - try { - const localPath = path.join(this.options.localDir, relativePath); - await this.downloadFile(relativePath, localPath); - } catch (error) { - console.error(`Error downloading ${relativePath}:`, error); - } - } - - // Delete local files that don't exist remotely (if requested) - if (this.pullOptions.deleteLocal) { - const filesToDelete = new Set(localFiles.keys()); - for (const relativePath of remoteFiles.keys()) { - filesToDelete.delete(relativePath); - } - - if (filesToDelete.size > 0) { - console.log( - `Will delete ${filesToDelete.size} local files that don't exist in workspace`, - ); - } - - for (const relativePath of filesToDelete) { - try { - const localPath = localFiles.get(relativePath); - if (localPath) { - await this.deleteLocalFile(localPath); - } - } catch (error) { - console.error(`Error deleting local file ${relativePath}:`, error); - } - } - } - - console.log('Pull completed'); - } -} - -function parseArgs(): { - workspaceUrl: string; - localDir: string; - deleteLocal: boolean; - dryRun: boolean; -} { - const args = process.argv.slice(2); - - if (args.includes('--help') || args.includes('-h')) { - console.log(` -Usage: workspace-pull [OPTIONS] - -Arguments: - WORKSPACE_URL The URL of the source workspace (e.g., https://demo.cardstack.com/demo/) - LOCAL_DIR The local directory to sync files to - -Options: - --delete Delete local files that don't exist in the workspace - --dry-run Show what would be done without making changes - --help, -h Show this help message - -Environment Variables (required): - MATRIX_URL The Matrix server URL - MATRIX_USERNAME Your Matrix username - MATRIX_PASSWORD Your Matrix password (or use REALM_SECRET_SEED for realm users) - -Environment Variables (optional): - REALM_SECRET_SEED Secret for generating realm user credentials. If MATRIX_USERNAME - is omitted, it will be derived from WORKSPACE_URL: - /// -> realm/_ - /base/, /skills/, ... -> _realm - /published// -> realm/published_ - -File Filtering: - - Files starting with a dot (.) are always ignored - - Files matching patterns in .gitignore are ignored - - Files matching patterns in .boxelignore are ignored (workspace-specific) - - .boxelignore allows you to exclude files from workspace sync while keeping them in git - -Examples: - workspace-pull https://demo.cardstack.com/demo/ ./my-cards - workspace-pull https://demo.cardstack.com/demo/ ./my-cards --delete --dry-run -`); - process.exit(0); - } - - if (args.length < 2) { - console.error('Error: WORKSPACE_URL and LOCAL_DIR are required arguments'); - console.error('Run with --help for usage information'); - process.exit(1); - } - - const workspaceUrl = args[0]; - const localDir = args[1]; - const deleteLocal = args.includes('--delete'); - const dryRun = args.includes('--dry-run'); - - return { workspaceUrl, localDir, deleteLocal, dryRun }; -} - -async function main() { - // Parse command line arguments - const { workspaceUrl, localDir, deleteLocal, dryRun } = parseArgs(); - - // Get environment variables for Matrix authentication - const { matrixUrl, username, password } = - await validateMatrixEnvVars(workspaceUrl); - - try { - const puller = new RealmPuller( - { workspaceUrl, localDir, deleteLocal, dryRun }, - matrixUrl, - username, - password, - ); - - await puller.initialize(); - await puller.sync(); - - console.log('Pull completed successfully'); - } catch (error) { - console.error('Pull failed:', error); - process.exit(1); - } -} - -if (require.main === module) { - main(); -} diff --git a/packages/workspace-sync-cli/src/push.ts b/packages/workspace-sync-cli/src/push.ts deleted file mode 100644 index 7817ee6712e..00000000000 --- a/packages/workspace-sync-cli/src/push.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { RealmSyncBase, validateMatrixEnvVars } from './realm-sync-base'; -import * as fs from 'fs'; - -interface PushOptions { - workspaceUrl: string; - localDir: string; - deleteRemote?: boolean; - dryRun?: boolean; -} - -class RealmPusher extends RealmSyncBase { - hasError = false; - constructor( - private pushOptions: PushOptions, - matrixUrl: string, - username: string, - password: string, - ) { - super(pushOptions, matrixUrl, username, password); - } - - async sync() { - console.log( - `Starting push from ${this.options.localDir} to ${this.options.workspaceUrl}`, - ); - - // Test authentication by trying to access the workspace root first - console.log('Testing workspace access...'); - try { - await this.getRemoteFileList(''); // Test with empty path (root) - } catch (error) { - console.error('Failed to access workspace:', error); - throw new Error( - 'Cannot proceed with push: Authentication or access failed. ' + - 'Please check your Matrix credentials and workspace permissions.', - ); - } - console.log('Workspace access verified'); - - // Get current remote file listing - const remoteFiles = await this.getRemoteFileList(); - console.log(`Found ${remoteFiles.size} files in remote workspace`); - - // Get local file listing - const localFiles = await this.getLocalFileList(); - console.log(`Found ${localFiles.size} files in local directory`); - - // Upload local files - for (const [relativePath, localPath] of localFiles) { - try { - await this.uploadFile(relativePath, localPath); - } catch (error) { - this.hasError = true; - console.error(`Error uploading ${relativePath}:`, error); - } - } - - // Delete remote files that don't exist locally (if requested) - if (this.pushOptions.deleteRemote) { - const filesToDelete = new Set(remoteFiles.keys()); - for (const relativePath of localFiles.keys()) { - filesToDelete.delete(relativePath); - } - - if (filesToDelete.size > 0) { - console.log( - `Will delete ${filesToDelete.size} remote files that don't exist locally`, - ); - } - - for (const relativePath of filesToDelete) { - try { - await this.deleteFile(relativePath); - } catch (error) { - console.error(`Error deleting ${relativePath}:`, error); - } - } - } - - console.log('Push completed'); - } -} - -function parseArgs(): { - workspaceUrl: string; - localDir: string; - deleteRemote: boolean; - dryRun: boolean; -} { - const args = process.argv.slice(2); - - if (args.includes('--help') || args.includes('-h')) { - console.log(` -Usage: workspace-push [OPTIONS] - -Arguments: - LOCAL_DIR The local directory containing files to sync - WORKSPACE_URL The URL of the target workspace (e.g., https://demo.cardstack.com/demo/) - -Options: - --delete Delete remote files that don't exist locally - --dry-run Show what would be done without making changes - --help, -h Show this help message - -Environment Variables (required): - MATRIX_URL The Matrix server URL - MATRIX_USERNAME Your Matrix username - MATRIX_PASSWORD Your Matrix password (or use REALM_SECRET_SEED for realm users) - -Environment Variables (optional): - REALM_SECRET_SEED Secret for generating realm user credentials. If MATRIX_USERNAME - is omitted, it will be derived from WORKSPACE_URL: - /// -> realm/_ - /base/, /skills/, ... -> _realm - /published// -> realm/published_ - -File Filtering: - - Files starting with a dot (.) are always ignored - - Files matching patterns in .gitignore are ignored - - Files matching patterns in .boxelignore are ignored (workspace-specific) - - .boxelignore allows you to exclude files from workspace sync while keeping them in git - -Examples: - workspace-push ./my-cards https://demo.cardstack.com/demo/ - workspace-push ./my-cards https://demo.cardstack.com/demo/ --delete --dry-run -`); - process.exit(0); - } - - if (args.length < 2) { - console.error('Error: LOCAL_DIR and WORKSPACE_URL are required arguments'); - console.error('Run with --help for usage information'); - process.exit(1); - } - - const localDir = args[0]; - const workspaceUrl = args[1]; - const deleteRemote = args.includes('--delete'); - const dryRun = args.includes('--dry-run'); - - return { workspaceUrl, localDir, deleteRemote, dryRun }; -} - -async function main() { - // Parse command line arguments - const { workspaceUrl, localDir, deleteRemote, dryRun } = parseArgs(); - - // Get environment variables for Matrix authentication - const { matrixUrl, username, password } = - await validateMatrixEnvVars(workspaceUrl); - - if (!fs.existsSync(localDir)) { - console.error(`Local directory does not exist: ${localDir}`); - process.exit(1); - } - - try { - const pusher = new RealmPusher( - { workspaceUrl, localDir, deleteRemote, dryRun }, - matrixUrl, - username, - password, - ); - - await pusher.initialize(); - await pusher.sync(); - - if (pusher.hasError) { - console.log('Push did not complete successfully. view logs for details'); - process.exit(2); - } else { - console.log('Push completed successfully'); - } - } catch (error) { - console.error('Push failed:', error); - process.exit(1); - } -} - -if (require.main === module) { - main(); -} diff --git a/packages/workspace-sync-cli/src/realm-sync-base.ts b/packages/workspace-sync-cli/src/realm-sync-base.ts deleted file mode 100644 index 20814e0e659..00000000000 --- a/packages/workspace-sync-cli/src/realm-sync-base.ts +++ /dev/null @@ -1,492 +0,0 @@ -import { - MatrixClient, - passwordFromSeed, -} from '@cardstack/runtime-common/matrix-client'; -import { RealmAuthClient } from '@cardstack/runtime-common/realm-auth-client'; -import * as fs from 'fs'; -import * as path from 'path'; -import { SupportedMimeType } from '@cardstack/runtime-common/router'; -import ignore, { type Ignore } from 'ignore'; - -export interface SyncOptions { - workspaceUrl: string; - localDir: string; - dryRun?: boolean; -} - -export abstract class RealmSyncBase { - protected matrixClient: MatrixClient; - protected realmAuthClient: RealmAuthClient; - protected normalizedRealmUrl: string; - private ignoreCache = new Map(); - - constructor( - protected options: SyncOptions, - matrixUrl: string, - username: string, - password: string, - ) { - this.matrixClient = new MatrixClient({ - matrixURL: new URL(matrixUrl), - username, - password, - }); - - // Normalize the realm URL once at construction - this.normalizedRealmUrl = this.normalizeRealmUrl(options.workspaceUrl); - - this.realmAuthClient = new RealmAuthClient( - new URL(this.normalizedRealmUrl), - this.matrixClient, - globalThis.fetch, - ); - } - - async initialize() { - console.log('Logging into Matrix...'); - await this.matrixClient.login(); - console.log('Matrix login successful'); - } - - private normalizeRealmUrl(url: string): string { - // Ensure the workspace URL is properly formatted - try { - const urlObj = new URL(url); - // Ensure it ends with a single slash for consistency - return urlObj.href.replace(/\/+$/, '') + '/'; - } catch (error) { - throw new Error(`Invalid workspace URL: ${url}`); - } - } - - protected buildDirectoryUrl(dir: string = ''): string { - // For directory listings, we need trailing slashes - if (!dir) { - return this.normalizedRealmUrl; // Already has trailing slash - } - - // Remove leading/trailing slashes from dir and add trailing slash - const cleanDir = dir.replace(/^\/+|\/+$/g, ''); - return `${this.normalizedRealmUrl}${cleanDir}/`; - } - - protected buildFileUrl(relativePath: string): string { - // For file operations, we don't want trailing slashes - const cleanPath = relativePath.replace(/^\/+/, ''); // Remove leading slashes only - return `${this.normalizedRealmUrl}${cleanPath}`; - } - - protected async getRemoteFileList(dir = ''): Promise> { - const files = new Map(); - - try { - const url = this.buildDirectoryUrl(dir); - const jwt = await this.realmAuthClient.getJWT(); - - const response = await fetch(url, { - headers: { - Accept: 'application/vnd.api+json', - Authorization: jwt, - }, - }); - - if (!response.ok) { - if (response.status === 404) { - return files; // Directory doesn't exist, return empty - } - if (response.status === 401 || response.status === 403) { - throw new Error( - `Authentication failed (${response.status}): Cannot access workspace. Check your Matrix credentials and workspace permissions.`, - ); - } - throw new Error( - `Failed to get directory listing: ${response.status} ${response.statusText}`, - ); - } - - const data = (await response.json()) as { - data?: { - relationships?: Record; - }; - }; - - if (data.data && data.data.relationships) { - for (const [name, info] of Object.entries(data.data.relationships)) { - const entry = info as { meta: { kind: string } }; - const isFile = entry.meta.kind === 'file'; - - // Use path.posix.join for consistent forward slashes in URLs - const entryPath = dir ? path.posix.join(dir, name) : name; - - if (isFile) { - // Apply the same filtering logic as local files - if (!this.shouldIgnoreRemoteFile(entryPath)) { - files.set(entryPath, true); - } - } else { - // Recursively get subdirectory files - const subdirFiles = await this.getRemoteFileList(entryPath); - for (const [subPath, isFileEntry] of subdirFiles) { - files.set(subPath, isFileEntry); - } - } - } - } - } catch (error) { - // Re-throw authentication and other critical errors instead of silently failing - if (error instanceof Error) { - if ( - error.message.includes('Authentication failed') || - error.message.includes('Cannot access workspace') || - error.message.includes('401') || - error.message.includes('403') - ) { - throw error; // Don't catch auth failures - let them bubble up - } - } - console.error(`Error reading remote directory ${dir}:`, error); - throw error; // Re-throw other errors too - don't silently continue - } - - // Special case: Check for .realm.json in the root directory - // The realm server doesn't include dotfiles in directory listings but serves them directly - if (!dir) { - // Only check in root directory - try { - const realmJsonUrl = this.buildFileUrl('.realm.json'); - const jwt = await this.realmAuthClient.getJWT(); - - const response = await fetch(realmJsonUrl, { - method: 'HEAD', // Just check if it exists - headers: { - Authorization: jwt, - }, - }); - - if (response.ok) { - files.set('.realm.json', true); - } - } catch (error) { - // .realm.json doesn't exist or can't be accessed, which is fine - console.log('Note: .realm.json not found in remote realm'); - } - } - - return files; - } - - protected async getLocalFileList(dir = ''): Promise> { - const files = new Map(); - const fullDir = path.join(this.options.localDir, dir); - - if (!fs.existsSync(fullDir)) { - return files; - } - - const entries = fs.readdirSync(fullDir); - - for (const entry of entries) { - const fullPath = path.join(fullDir, entry); - // Use path.posix.join for consistent forward slashes (URLs use forward slashes) - const relativePath = dir ? path.posix.join(dir, entry) : entry; - const stats = fs.statSync(fullPath); - - // Apply filtering for dotfiles and gitignore patterns - if (this.shouldIgnoreFile(relativePath, fullPath)) { - continue; - } - - if (stats.isFile()) { - files.set(relativePath, fullPath); - } else if (stats.isDirectory()) { - // Recursively get subdirectory files - const subdirFiles = await this.getLocalFileList(relativePath); - for (const [subPath, fullSubPath] of subdirFiles) { - files.set(subPath, fullSubPath); - } - } - } - - return files; - } - - protected async uploadFile(relativePath: string, localPath: string) { - console.log(`Uploading: ${relativePath}`); - - if (this.options.dryRun) { - console.log(`[DRY RUN] Would upload ${relativePath}`); - return; - } - - const content = fs.readFileSync(localPath, 'utf8'); - const url = this.buildFileUrl(relativePath); - const jwt = await this.realmAuthClient.getJWT(); - - const response = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'text/plain;charset=UTF-8', - Authorization: jwt, - Accept: SupportedMimeType.CardSource, - }, - body: content, - }); - - if (!response.ok) { - throw new Error( - `Failed to upload: ${response.status} ${response.statusText}`, - ); - } - - console.log(`✓ Uploaded: ${relativePath}`); - } - - protected async downloadFile(relativePath: string, localPath: string) { - console.log(`Downloading: ${relativePath}`); - - if (this.options.dryRun) { - console.log(`[DRY RUN] Would download ${relativePath}`); - return; - } - - const url = this.buildFileUrl(relativePath); - const jwt = await this.realmAuthClient.getJWT(); - - const response = await fetch(url, { - headers: { - Authorization: jwt, - Accept: SupportedMimeType.CardSource, - }, - }); - - if (!response.ok) { - throw new Error( - `Failed to download: ${response.status} ${response.statusText}`, - ); - } - - const content = await response.text(); - - // Ensure directory exists - const localDir = path.dirname(localPath); - if (!fs.existsSync(localDir)) { - fs.mkdirSync(localDir, { recursive: true }); - } - - fs.writeFileSync(localPath, content, 'utf8'); - console.log(`✓ Downloaded: ${relativePath}`); - } - - protected async deleteFile(relativePath: string) { - console.log(`Deleting: ${relativePath}`); - - if (this.options.dryRun) { - console.log(`[DRY RUN] Would delete ${relativePath}`); - return; - } - - const url = this.buildFileUrl(relativePath); - const jwt = await this.realmAuthClient.getJWT(); - - const response = await fetch(url, { - method: 'DELETE', - headers: { - Authorization: jwt, - Accept: SupportedMimeType.CardSource, - }, - }); - - if (!response.ok && response.status !== 404) { - throw new Error( - `Failed to delete: ${response.status} ${response.statusText}`, - ); - } - - console.log(`✓ Deleted: ${relativePath}`); - } - - protected async deleteLocalFile(localPath: string) { - console.log(`Deleting local file: ${localPath}`); - - if (this.options.dryRun) { - console.log(`[DRY RUN] Would delete local file ${localPath}`); - return; - } - - if (fs.existsSync(localPath)) { - fs.unlinkSync(localPath); - console.log(`✓ Deleted local file: ${localPath}`); - } - } - - private getIgnoreInstance(dirPath: string): Ignore { - if (this.ignoreCache.has(dirPath)) { - return this.ignoreCache.get(dirPath)!; - } - - const ig = ignore(); - - // Find all .gitignore and .boxelignore files in the path hierarchy - let currentPath = dirPath; - const rootPath = this.options.localDir; - - while (currentPath.startsWith(rootPath)) { - // Check for .gitignore file - const gitignorePath = path.join(currentPath, '.gitignore'); - if (fs.existsSync(gitignorePath)) { - try { - const gitignoreContent = fs.readFileSync(gitignorePath, 'utf8'); - ig.add(gitignoreContent); - } catch (error) { - console.warn( - `Warning: Could not read .gitignore file at ${gitignorePath}:`, - error, - ); - } - } - - // Check for .boxelignore file - const boxelignorePath = path.join(currentPath, '.boxelignore'); - if (fs.existsSync(boxelignorePath)) { - try { - const boxelignoreContent = fs.readFileSync(boxelignorePath, 'utf8'); - ig.add(boxelignoreContent); - } catch (error) { - console.warn( - `Warning: Could not read .boxelignore file at ${boxelignorePath}:`, - error, - ); - } - } - - // Move up one directory - const parentPath = path.dirname(currentPath); - if (parentPath === currentPath) break; // Reached filesystem root - currentPath = parentPath; - } - - this.ignoreCache.set(dirPath, ig); - return ig; - } - - private shouldIgnoreFile(relativePath: string, fullPath: string): boolean { - // Always ignore files that start with a dot, except for .realm.json - const fileName = path.basename(relativePath); - if (fileName.startsWith('.')) { - // Exception: allow .realm.json to be synced - if (fileName === '.realm.json') { - return false; - } - return true; - } - - // Check against gitignore patterns - const dirPath = path.dirname(fullPath); - const ig = this.getIgnoreInstance(dirPath); - - // Use forward slashes for ignore patterns (gitignore standard) - const normalizedPath = relativePath.replace(/\\/g, '/'); - - return ig.ignores(normalizedPath); - } - - private shouldIgnoreRemoteFile(relativePath: string): boolean { - // Apply the same dotfile filtering logic as local files - const fileName = path.basename(relativePath); - if (fileName.startsWith('.')) { - // Exception: allow .realm.json to be synced - if (fileName === '.realm.json') { - return false; - } - return true; - } - - // Note: We can't check gitignore patterns for remote files since we don't have - // access to the remote .gitignore/.boxelignore files, but dotfile filtering - // is the primary concern for security - return false; - } - - abstract sync(): Promise; -} - -function deriveRealmUsername(workspaceUrl: string): string { - let url: URL; - try { - url = new URL(workspaceUrl); - } catch (error) { - throw new Error(`Invalid workspace URL: ${workspaceUrl}`); - } - - let segments = url.pathname.split('/').filter(Boolean); - if (segments.length === 0) { - throw new Error( - `Cannot derive realm username from workspace URL (${workspaceUrl}). Please provide MATRIX_USERNAME`, - ); - } - - // Published realms live at /published// and use realm/published_ - if (segments[0] === 'published') { - if (!segments[1]) { - throw new Error( - `Cannot derive published realm username from workspace URL (${workspaceUrl}). Missing published realm id.`, - ); - } - return `realm/published_${segments[1]}`; - } - - // Realms created through the app live at /// and use realm/_ - if (segments.length >= 2) { - return `realm/${segments[0]}_${segments[1]}`; - } - - // Root realms like /base/, /skills/, or /experiments/ use _realm - return `${segments[0]}_realm`; -} - -export async function validateMatrixEnvVars(workspaceUrl: string): Promise<{ - matrixUrl: string; - username: string; - password: string; -}> { - const matrixUrl = process.env.MATRIX_URL; - const envUsername = process.env.MATRIX_USERNAME; - let password = process.env.MATRIX_PASSWORD; - const realmSecret = process.env.REALM_SECRET_SEED; - let username = envUsername; - - if (!matrixUrl) { - console.error('MATRIX_URL environment variable is required'); - process.exit(1); - } - - if (!username) { - if (!realmSecret) { - console.error( - 'Either MATRIX_USERNAME or REALM_SECRET_SEED environment variable is required', - ); - process.exit(1); - } - username = deriveRealmUsername(workspaceUrl); - console.log( - `Derived realm Matrix username '${username}' from workspace URL using REALM_SECRET_SEED`, - ); - } - - // If password is not provided but realm secret is, generate password from secret - if (!password && realmSecret) { - password = await passwordFromSeed(username, realmSecret); - console.log( - 'Generated password from REALM_SECRET_SEED for realm user authentication', - ); - } - - if (!password) { - console.error( - 'Either MATRIX_PASSWORD or REALM_SECRET_SEED environment variable is required', - ); - process.exit(1); - } - - return { matrixUrl, username, password: password! }; -} diff --git a/packages/workspace-sync-cli/tsconfig.json b/packages/workspace-sync-cli/tsconfig.json deleted file mode 100644 index 8e6c9862bf8..00000000000 --- a/packages/workspace-sync-cli/tsconfig.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "compilerOptions": { - "target": "es2020", - "lib": ["es2020"], - "allowJs": true, - "module": "NodeNext", - "moduleResolution": "nodenext", - "allowSyntheticDefaultImports": true, - "noImplicitAny": true, - "noImplicitThis": true, - "alwaysStrict": true, - "strictNullChecks": true, - "strictPropertyInitialization": true, - "noFallthroughCasesInSwitch": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noImplicitReturns": true, - "noEmitOnError": false, - "noEmit": true, - "declaration": true, - "declarationMap": false, - "sourceMap": false, - "inlineSourceMap": false, - "inlineSources": false, - "baseUrl": ".", - "esModuleInterop": true, - "experimentalDecorators": true, - "skipLibCheck": true, - "strict": true, - "paths": { - "https://cardstack.com/base/*": ["../base/*"], - "*": ["types/*"] - }, - "types": ["@cardstack/local-types"] - }, - "include": ["./**/*"], - "exclude": ["node_modules", "dist"] -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4bf39f91987..30c77546260 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1085,7 +1085,7 @@ importers: version: 5.9.3 vite: specifier: 'catalog:' - version: 6.4.2(@types/node@24.12.4)(lightningcss@1.32.0)(terser@5.47.1)(tsx@4.21.0)(yaml@2.9.0) + version: 6.4.2(@types/node@24.12.4)(lightningcss@1.32.0)(terser@5.47.1)(yaml@2.9.0) vitest: specifier: 'catalog:' version: 2.1.9(@types/node@24.12.4)(jsdom@26.1.0)(lightningcss@1.32.0)(terser@5.47.1) @@ -1480,10 +1480,10 @@ importers: version: 1.16.13(@glint/template@1.7.7) '@embroider/test-setup': specifier: ^4.0.0 - version: 4.0.0(@embroider/compat@3.9.4(patch_hash=db8df3cd3be93909d4ddbc1eace0a46dd23639f38332d9eb4c500c534687c7b2)(@embroider/core@3.5.10(@glint/template@1.7.7))(@glint/template@1.7.7))(@embroider/core@3.5.10(@glint/template@1.7.7))(@embroider/webpack@4.1.2(patch_hash=3575bbdd1074ff74a26adde4a25140c197c845679f6ad0941e00494f73c79eff)(@embroider/core@3.5.10(@glint/template@1.7.7))(webpack@5.106.2)) + version: 4.0.0(@embroider/compat@3.9.4(patch_hash=db8df3cd3be93909d4ddbc1eace0a46dd23639f38332d9eb4c500c534687c7b2)(@embroider/core@3.5.10(@glint/template@1.7.7))(@glint/template@1.7.7))(@embroider/core@3.5.10(@glint/template@1.7.7))(@embroider/webpack@4.1.2(patch_hash=3575bbdd1074ff74a26adde4a25140c197c845679f6ad0941e00494f73c79eff)(@embroider/core@3.5.10(@glint/template@1.7.7))(webpack@5.106.2(postcss@8.5.14))) '@embroider/webpack': specifier: ^4.0.4 - version: 4.1.2(patch_hash=3575bbdd1074ff74a26adde4a25140c197c845679f6ad0941e00494f73c79eff)(@embroider/core@3.5.10(@glint/template@1.7.7))(webpack@5.106.2) + version: 4.1.2(patch_hash=3575bbdd1074ff74a26adde4a25140c197c845679f6ad0941e00494f73c79eff)(@embroider/core@3.5.10(@glint/template@1.7.7))(webpack@5.106.2(postcss@8.5.14)) '@glimmer/component': specifier: ^2.0.0 version: 2.1.1 @@ -1534,7 +1534,7 @@ importers: version: 8.0.1(@babel/core@7.29.0)(@ember/test-helpers@5.4.2(@babel/core@7.29.0)(@glint/template@1.7.7))(@ember/test-waiters@4.1.1(@glint/template@1.7.7))(axe-core@4.11.4)(qunit@2.25.0) ember-auto-import: specifier: ^2.7.2 - version: 2.13.1(@glint/template@1.7.7)(webpack@5.106.2) + version: 2.13.1(@glint/template@1.7.7)(webpack@5.106.2(postcss@8.5.14)) ember-cli: specifier: ^5.4.1 version: 5.12.0(@babel/core@7.29.0)(@types/node@24.12.4) @@ -1585,7 +1585,7 @@ importers: version: 1.0.0(patch_hash=0e5253a008cc7bf02424d786e7a8fb2397bc48962f3cd1443f2c0fdd8200c96d) ember-freestyle: specifier: ^0.22.0 - version: 0.22.0(@babel/core@7.29.0)(@ember/string@4.0.1)(@glint/template@1.7.7)(ember-source@6.10.1(patch_hash=ea945024993105fb6cc4ae5cb5e9ea8e0eff6cd5fe0b0033c43dd0cf9453eb0d)(@glimmer/component@2.1.1)(rsvp@4.8.5))(webpack@5.106.2) + version: 0.22.0(@babel/core@7.29.0)(@ember/string@4.0.1)(@glint/template@1.7.7)(ember-source@6.10.1(patch_hash=ea945024993105fb6cc4ae5cb5e9ea8e0eff6cd5fe0b0033c43dd0cf9453eb0d)(@glimmer/component@2.1.1)(rsvp@4.8.5))(webpack@5.106.2(postcss@8.5.14)) ember-load-initializers: specifier: ^2.1.2 version: 2.1.2(@babel/core@7.29.0) @@ -1663,7 +1663,7 @@ importers: version: 5.9.3 webpack: specifier: 'catalog:' - version: 5.106.2 + version: 5.106.2(postcss@8.5.14) packages/catalog: dependencies: @@ -1876,7 +1876,7 @@ importers: version: 5.9.3 vite: specifier: 'catalog:' - version: 6.4.2(@types/node@25.7.0)(lightningcss@1.32.0)(terser@5.47.1)(tsx@4.21.0)(yaml@2.9.0) + version: 6.4.2(@types/node@25.7.0)(lightningcss@1.32.0)(terser@5.47.1)(yaml@2.9.0) vitest: specifier: 'catalog:' version: 2.1.9(@types/node@25.7.0)(jsdom@26.1.0)(lightningcss@1.32.0)(terser@5.47.1) @@ -1895,7 +1895,7 @@ importers: version: 1.7.1(eslint@8.57.1)(typescript@5.9.3) vite: specifier: 'catalog:' - version: 6.4.2(@types/node@25.7.0)(lightningcss@1.32.0)(terser@5.47.1)(tsx@4.21.0)(yaml@2.9.0) + version: 6.4.2(@types/node@25.7.0)(lightningcss@1.32.0)(terser@5.47.1)(yaml@2.9.0) vitest: specifier: 'catalog:' version: 2.1.9(@types/node@25.7.0)(jsdom@26.1.0)(lightningcss@1.32.0)(terser@5.47.1) @@ -2056,7 +2056,7 @@ importers: version: 1.16.13(@glint/template@1.7.7) '@embroider/vite': specifier: ^1.1.1 - version: 1.7.2(@embroider/core@4.4.7(@glint/template@1.7.7))(@glint/template@1.7.7)(vite@8.0.12(@types/node@25.7.0)(esbuild@0.27.7)(terser@5.47.1)(tsx@4.21.0)(yaml@2.9.0)) + version: 1.7.2(@embroider/core@4.4.7(@glint/template@1.7.7))(@glint/template@1.7.7)(vite@8.0.12(@types/node@25.7.0)(terser@5.47.1)(yaml@2.9.0)) '@floating-ui/dom': specifier: 'catalog:' version: 1.7.6 @@ -2083,7 +2083,7 @@ importers: version: 1.31.13(typescript@5.9.3) '@percy/ember': specifier: 'catalog:' - version: 5.0.1(@babel/core@7.29.0)(@glint/template@1.7.7)(webpack@5.106.2(esbuild@0.27.7)(postcss@8.5.14)) + version: 5.0.1(@babel/core@7.29.0)(@glint/template@1.7.7)(webpack@5.106.2) '@rollup/plugin-babel': specifier: ^6.0.4 version: 6.1.0(@babel/core@7.29.0)(@types/babel__core@7.20.5)(rollup@4.60.3) @@ -2200,7 +2200,7 @@ importers: version: 1.0.3(ember-source@6.10.1(patch_hash=ea945024993105fb6cc4ae5cb5e9ea8e0eff6cd5fe0b0033c43dd0cf9453eb0d)(@glimmer/component@2.1.1)(rsvp@4.8.5)) ember-auto-import: specifier: ^2.7.2 - version: 2.13.1(@glint/template@1.7.7)(webpack@5.106.2(esbuild@0.27.7)(postcss@8.5.14)) + version: 2.13.1(@glint/template@1.7.7)(webpack@5.106.2) ember-basic-dropdown: specifier: 8.0.4 version: 8.0.4(patch_hash=19b0fc5d4bd8b9aa296c4065fa5e33bdbb965db0b277810b596eacd0b9e2f428)(@ember/test-helpers@5.4.2(@babel/core@7.29.0)(@glint/template@1.7.7))(@glimmer/component@2.1.1)(@glimmer/tracking@1.1.2)(@glint/template@1.7.7)(ember-source@6.10.1(patch_hash=ea945024993105fb6cc4ae5cb5e9ea8e0eff6cd5fe0b0033c43dd0cf9453eb0d)(@glimmer/component@2.1.1)(rsvp@4.8.5)) @@ -2251,13 +2251,13 @@ importers: version: 2.0.0(@glint/template@1.7.7)(ember-source@6.10.1(patch_hash=ea945024993105fb6cc4ae5cb5e9ea8e0eff6cd5fe0b0033c43dd0cf9453eb0d)(@glimmer/component@2.1.1)(rsvp@4.8.5)) ember-exam: specifier: ^10.1.0 - version: 10.1.0(@glint/template@1.7.7)(ember-qunit@9.0.4(@ember/test-helpers@5.4.2(@babel/core@7.29.0)(@glint/template@1.7.7))(@glint/template@1.7.7)(qunit@2.25.0))(ember-source@6.10.1(patch_hash=ea945024993105fb6cc4ae5cb5e9ea8e0eff6cd5fe0b0033c43dd0cf9453eb0d)(@glimmer/component@2.1.1)(rsvp@4.8.5))(qunit@2.25.0)(webpack@5.106.2(esbuild@0.27.7)(postcss@8.5.14)) + version: 10.1.0(@glint/template@1.7.7)(ember-qunit@9.0.4(@ember/test-helpers@5.4.2(@babel/core@7.29.0)(@glint/template@1.7.7))(@glint/template@1.7.7)(qunit@2.25.0))(ember-source@6.10.1(patch_hash=ea945024993105fb6cc4ae5cb5e9ea8e0eff6cd5fe0b0033c43dd0cf9453eb0d)(@glimmer/component@2.1.1)(rsvp@4.8.5))(qunit@2.25.0)(webpack@5.106.2) ember-focus-trap: specifier: ^1.0.1 version: 1.2.0(@babel/core@7.29.0) ember-freestyle: specifier: ^0.20.0 - version: 0.20.0(@babel/core@7.29.0)(@ember/string@4.0.1)(@glint/template@1.7.7)(ember-source@6.10.1(patch_hash=ea945024993105fb6cc4ae5cb5e9ea8e0eff6cd5fe0b0033c43dd0cf9453eb0d)(@glimmer/component@2.1.1)(rsvp@4.8.5))(webpack@5.106.2(esbuild@0.27.7)(postcss@8.5.14)) + version: 0.20.0(@babel/core@7.29.0)(@ember/string@4.0.1)(@glint/template@1.7.7)(ember-source@6.10.1(patch_hash=ea945024993105fb6cc4ae5cb5e9ea8e0eff6cd5fe0b0033c43dd0cf9453eb0d)(@glimmer/component@2.1.1)(rsvp@4.8.5))(webpack@5.106.2) ember-keyboard: specifier: ^8.2.1 version: 8.2.1(@babel/core@7.29.0)(@ember/test-helpers@5.4.2(@babel/core@7.29.0)(@glint/template@1.7.7)) @@ -2440,7 +2440,7 @@ importers: version: 9.0.1 vite: specifier: ^8.0.8 - version: 8.0.12(@types/node@25.7.0)(esbuild@0.27.7)(terser@5.47.1)(tsx@4.21.0)(yaml@2.9.0) + version: 8.0.12(@types/node@25.7.0)(terser@5.47.1)(yaml@2.9.0) wait-for-localhost-cli: specifier: 'catalog:' version: 3.2.0 @@ -3335,36 +3335,6 @@ importers: specifier: 'catalog:' version: 5.9.3 - packages/workspace-sync-cli: - devDependencies: - '@cardstack/local-types': - specifier: workspace:* - version: link:../local-types - '@cardstack/runtime-common': - specifier: workspace:* - version: link:../runtime-common - '@types/node': - specifier: 'catalog:' - version: 24.12.4 - concurrently: - specifier: 'catalog:' - version: 8.2.2 - esbuild: - specifier: ^0.19.0 - version: 0.19.12 - ignore: - specifier: ^5.3.0 - version: 5.3.2 - ts-node: - specifier: ^10.9.1 - version: 10.9.2(@types/node@24.12.4)(typescript@5.9.3) - tsx: - specifier: ^4.0.0 - version: 4.21.0 - typescript: - specifier: 'catalog:' - version: 5.9.3 - vendor/ember-concurrency-async-plugin: dependencies: '@babel/helper-module-imports': @@ -4466,12 +4436,6 @@ packages: cpu: [ppc64] os: [aix] - '@esbuild/aix-ppc64@0.27.7': - resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - '@esbuild/android-arm64@0.19.12': resolution: {integrity: sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==} engines: {node: '>=12'} @@ -4496,12 +4460,6 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm64@0.27.7': - resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - '@esbuild/android-arm@0.19.12': resolution: {integrity: sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==} engines: {node: '>=12'} @@ -4526,12 +4484,6 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-arm@0.27.7': - resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - '@esbuild/android-x64@0.19.12': resolution: {integrity: sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==} engines: {node: '>=12'} @@ -4556,12 +4508,6 @@ packages: cpu: [x64] os: [android] - '@esbuild/android-x64@0.27.7': - resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - '@esbuild/darwin-arm64@0.19.12': resolution: {integrity: sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==} engines: {node: '>=12'} @@ -4586,12 +4532,6 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-arm64@0.27.7': - resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - '@esbuild/darwin-x64@0.19.12': resolution: {integrity: sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==} engines: {node: '>=12'} @@ -4616,12 +4556,6 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/darwin-x64@0.27.7': - resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - '@esbuild/freebsd-arm64@0.19.12': resolution: {integrity: sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==} engines: {node: '>=12'} @@ -4646,12 +4580,6 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-arm64@0.27.7': - resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - '@esbuild/freebsd-x64@0.19.12': resolution: {integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==} engines: {node: '>=12'} @@ -4676,12 +4604,6 @@ packages: cpu: [x64] os: [freebsd] - '@esbuild/freebsd-x64@0.27.7': - resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - '@esbuild/linux-arm64@0.19.12': resolution: {integrity: sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==} engines: {node: '>=12'} @@ -4706,12 +4628,6 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm64@0.27.7': - resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - '@esbuild/linux-arm@0.19.12': resolution: {integrity: sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==} engines: {node: '>=12'} @@ -4736,12 +4652,6 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-arm@0.27.7': - resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - '@esbuild/linux-ia32@0.19.12': resolution: {integrity: sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==} engines: {node: '>=12'} @@ -4766,12 +4676,6 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-ia32@0.27.7': - resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - '@esbuild/linux-loong64@0.19.12': resolution: {integrity: sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==} engines: {node: '>=12'} @@ -4796,12 +4700,6 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-loong64@0.27.7': - resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - '@esbuild/linux-mips64el@0.19.12': resolution: {integrity: sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==} engines: {node: '>=12'} @@ -4826,12 +4724,6 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-mips64el@0.27.7': - resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - '@esbuild/linux-ppc64@0.19.12': resolution: {integrity: sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==} engines: {node: '>=12'} @@ -4856,12 +4748,6 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-ppc64@0.27.7': - resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - '@esbuild/linux-riscv64@0.19.12': resolution: {integrity: sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==} engines: {node: '>=12'} @@ -4886,12 +4772,6 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-riscv64@0.27.7': - resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - '@esbuild/linux-s390x@0.19.12': resolution: {integrity: sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==} engines: {node: '>=12'} @@ -4916,12 +4796,6 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-s390x@0.27.7': - resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - '@esbuild/linux-x64@0.19.12': resolution: {integrity: sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==} engines: {node: '>=12'} @@ -4946,12 +4820,6 @@ packages: cpu: [x64] os: [linux] - '@esbuild/linux-x64@0.27.7': - resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - '@esbuild/netbsd-arm64@0.24.2': resolution: {integrity: sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==} engines: {node: '>=18'} @@ -4964,12 +4832,6 @@ packages: cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-arm64@0.27.7': - resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - '@esbuild/netbsd-x64@0.19.12': resolution: {integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==} engines: {node: '>=12'} @@ -4994,12 +4856,6 @@ packages: cpu: [x64] os: [netbsd] - '@esbuild/netbsd-x64@0.27.7': - resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - '@esbuild/openbsd-arm64@0.24.2': resolution: {integrity: sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==} engines: {node: '>=18'} @@ -5012,12 +4868,6 @@ packages: cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-arm64@0.27.7': - resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - '@esbuild/openbsd-x64@0.19.12': resolution: {integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==} engines: {node: '>=12'} @@ -5042,24 +4892,12 @@ packages: cpu: [x64] os: [openbsd] - '@esbuild/openbsd-x64@0.27.7': - resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - '@esbuild/openharmony-arm64@0.25.12': resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/openharmony-arm64@0.27.7': - resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - '@esbuild/sunos-x64@0.19.12': resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==} engines: {node: '>=12'} @@ -5084,12 +4922,6 @@ packages: cpu: [x64] os: [sunos] - '@esbuild/sunos-x64@0.27.7': - resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - '@esbuild/win32-arm64@0.19.12': resolution: {integrity: sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==} engines: {node: '>=12'} @@ -5114,12 +4946,6 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-arm64@0.27.7': - resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - '@esbuild/win32-ia32@0.19.12': resolution: {integrity: sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==} engines: {node: '>=12'} @@ -5144,12 +4970,6 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-ia32@0.27.7': - resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - '@esbuild/win32-x64@0.19.12': resolution: {integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==} engines: {node: '>=12'} @@ -5174,12 +4994,6 @@ packages: cpu: [x64] os: [win32] - '@esbuild/win32-x64@0.27.7': - resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - '@eslint-community/eslint-utils@4.9.1': resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -9997,11 +9811,6 @@ packages: engines: {node: '>=18'} hasBin: true - esbuild@0.27.7: - resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} - engines: {node: '>=18'} - hasBin: true - escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -14754,11 +14563,6 @@ packages: peerDependencies: typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' - tsx@4.21.0: - resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} - engines: {node: '>=18.0.0'} - hasBin: true - tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} @@ -16933,11 +16737,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@embroider/babel-loader-9@3.1.3(@embroider/core@3.5.10(@glint/template@1.7.7))(supports-color@8.1.1)(webpack@5.106.2)': + '@embroider/babel-loader-9@3.1.3(@embroider/core@3.5.10(@glint/template@1.7.7))(supports-color@8.1.1)(webpack@5.106.2(postcss@8.5.14))': dependencies: '@babel/core': 7.29.0(supports-color@8.1.1) '@embroider/core': 3.5.10(@glint/template@1.7.7) - babel-loader: 9.2.1(@babel/core@7.29.0)(webpack@5.106.2) + babel-loader: 9.2.1(@babel/core@7.29.0)(webpack@5.106.2(postcss@8.5.14)) transitivePeerDependencies: - supports-color - webpack @@ -17120,10 +16924,10 @@ snapshots: - supports-color - utf-8-validate - '@embroider/hbs-loader@3.0.5(@embroider/core@3.5.10(@glint/template@1.7.7))(webpack@5.106.2)': + '@embroider/hbs-loader@3.0.5(@embroider/core@3.5.10(@glint/template@1.7.7))(webpack@5.106.2(postcss@8.5.14))': dependencies: '@embroider/core': 3.5.10(@glint/template@1.7.7) - webpack: 5.106.2 + webpack: 5.106.2(postcss@8.5.14) '@embroider/macros@1.16.13(@glint/template@1.7.7)': dependencies: @@ -17213,14 +17017,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@embroider/test-setup@4.0.0(@embroider/compat@3.9.4(patch_hash=db8df3cd3be93909d4ddbc1eace0a46dd23639f38332d9eb4c500c534687c7b2)(@embroider/core@3.5.10(@glint/template@1.7.7))(@glint/template@1.7.7))(@embroider/core@3.5.10(@glint/template@1.7.7))(@embroider/webpack@4.1.2(patch_hash=3575bbdd1074ff74a26adde4a25140c197c845679f6ad0941e00494f73c79eff)(@embroider/core@3.5.10(@glint/template@1.7.7))(webpack@5.106.2))': + '@embroider/test-setup@4.0.0(@embroider/compat@3.9.4(patch_hash=db8df3cd3be93909d4ddbc1eace0a46dd23639f38332d9eb4c500c534687c7b2)(@embroider/core@3.5.10(@glint/template@1.7.7))(@glint/template@1.7.7))(@embroider/core@3.5.10(@glint/template@1.7.7))(@embroider/webpack@4.1.2(patch_hash=3575bbdd1074ff74a26adde4a25140c197c845679f6ad0941e00494f73c79eff)(@embroider/core@3.5.10(@glint/template@1.7.7))(webpack@5.106.2(postcss@8.5.14)))': dependencies: lodash: 4.18.1 resolve: 1.22.12 optionalDependencies: '@embroider/compat': 3.9.4(patch_hash=db8df3cd3be93909d4ddbc1eace0a46dd23639f38332d9eb4c500c534687c7b2)(@embroider/core@3.5.10(@glint/template@1.7.7))(@glint/template@1.7.7) '@embroider/core': 3.5.10(@glint/template@1.7.7) - '@embroider/webpack': 4.1.2(patch_hash=3575bbdd1074ff74a26adde4a25140c197c845679f6ad0941e00494f73c79eff)(@embroider/core@3.5.10(@glint/template@1.7.7))(webpack@5.106.2) + '@embroider/webpack': 4.1.2(patch_hash=3575bbdd1074ff74a26adde4a25140c197c845679f6ad0941e00494f73c79eff)(@embroider/core@3.5.10(@glint/template@1.7.7))(webpack@5.106.2(postcss@8.5.14)) '@embroider/util@1.13.1(@glint/template@1.7.7)(ember-source@6.10.1(patch_hash=ea945024993105fb6cc4ae5cb5e9ea8e0eff6cd5fe0b0033c43dd0cf9453eb0d)(@glimmer/component@2.1.1)(rsvp@4.8.5))': dependencies: @@ -17233,7 +17037,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@embroider/vite@1.7.2(@embroider/core@4.4.7(@glint/template@1.7.7))(@glint/template@1.7.7)(vite@8.0.12(@types/node@25.7.0)(esbuild@0.27.7)(terser@5.47.1)(tsx@4.21.0)(yaml@2.9.0))': + '@embroider/vite@1.7.2(@embroider/core@4.4.7(@glint/template@1.7.7))(@glint/template@1.7.7)(vite@8.0.12(@types/node@25.7.0)(terser@5.47.1)(yaml@2.9.0))': dependencies: '@babel/core': 7.29.0(supports-color@8.1.1) '@embroider/core': 4.4.7(@glint/template@1.7.7) @@ -17251,7 +17055,7 @@ snapshots: send: 0.18.0 source-map-url: 0.4.1 terser: 5.47.1 - vite: 8.0.12(@types/node@25.7.0)(esbuild@0.27.7)(terser@5.47.1)(tsx@4.21.0)(yaml@2.9.0) + vite: 8.0.12(@types/node@25.7.0)(terser@5.47.1)(yaml@2.9.0) transitivePeerDependencies: - '@glint/template' - bufferutil @@ -17259,32 +17063,32 @@ snapshots: - supports-color - utf-8-validate - '@embroider/webpack@4.1.2(patch_hash=3575bbdd1074ff74a26adde4a25140c197c845679f6ad0941e00494f73c79eff)(@embroider/core@3.5.10(@glint/template@1.7.7))(webpack@5.106.2)': + '@embroider/webpack@4.1.2(patch_hash=3575bbdd1074ff74a26adde4a25140c197c845679f6ad0941e00494f73c79eff)(@embroider/core@3.5.10(@glint/template@1.7.7))(webpack@5.106.2(postcss@8.5.14))': dependencies: '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/preset-env': 7.29.5(@babel/core@7.29.0)(supports-color@8.1.1) - '@embroider/babel-loader-9': 3.1.3(@embroider/core@3.5.10(@glint/template@1.7.7))(supports-color@8.1.1)(webpack@5.106.2) + '@embroider/babel-loader-9': 3.1.3(@embroider/core@3.5.10(@glint/template@1.7.7))(supports-color@8.1.1)(webpack@5.106.2(postcss@8.5.14)) '@embroider/core': 3.5.10(@glint/template@1.7.7) - '@embroider/hbs-loader': 3.0.5(@embroider/core@3.5.10(@glint/template@1.7.7))(webpack@5.106.2) + '@embroider/hbs-loader': 3.0.5(@embroider/core@3.5.10(@glint/template@1.7.7))(webpack@5.106.2(postcss@8.5.14)) '@embroider/shared-internals': 2.9.2(supports-color@8.1.1) '@types/supports-color': 8.1.3 assert-never: 1.4.0 - babel-loader: 8.4.1(@babel/core@7.29.0)(webpack@5.106.2) - css-loader: 5.2.7(webpack@5.106.2) + babel-loader: 8.4.1(@babel/core@7.29.0)(webpack@5.106.2(postcss@8.5.14)) + css-loader: 5.2.7(webpack@5.106.2(postcss@8.5.14)) csso: 4.2.0 debug: 4.4.3(supports-color@8.1.1) escape-string-regexp: 4.0.0 fs-extra: 9.1.0 jsdom: 25.0.1(supports-color@8.1.1) lodash: 4.18.1 - mini-css-extract-plugin: 2.10.2(webpack@5.106.2) + mini-css-extract-plugin: 2.10.2(webpack@5.106.2(postcss@8.5.14)) semver: 7.8.0 source-map-url: 0.4.1 - style-loader: 2.0.0(patch_hash=579dd92e6adabd45669f9a99a01c6c28c97488c7bf4ee0d6c1c622a14592e4c8)(webpack@5.106.2) + style-loader: 2.0.0(patch_hash=579dd92e6adabd45669f9a99a01c6c28c97488c7bf4ee0d6c1c622a14592e4c8)(webpack@5.106.2(postcss@8.5.14)) supports-color: 8.1.1 terser: 5.47.1 - thread-loader: 3.0.4(webpack@5.106.2) - webpack: 5.106.2 + thread-loader: 3.0.4(webpack@5.106.2(postcss@8.5.14)) + webpack: 5.106.2(postcss@8.5.14) transitivePeerDependencies: - bufferutil - canvas @@ -17318,9 +17122,6 @@ snapshots: '@esbuild/aix-ppc64@0.25.12': optional: true - '@esbuild/aix-ppc64@0.27.7': - optional: true - '@esbuild/android-arm64@0.19.12': optional: true @@ -17333,9 +17134,6 @@ snapshots: '@esbuild/android-arm64@0.25.12': optional: true - '@esbuild/android-arm64@0.27.7': - optional: true - '@esbuild/android-arm@0.19.12': optional: true @@ -17348,9 +17146,6 @@ snapshots: '@esbuild/android-arm@0.25.12': optional: true - '@esbuild/android-arm@0.27.7': - optional: true - '@esbuild/android-x64@0.19.12': optional: true @@ -17363,9 +17158,6 @@ snapshots: '@esbuild/android-x64@0.25.12': optional: true - '@esbuild/android-x64@0.27.7': - optional: true - '@esbuild/darwin-arm64@0.19.12': optional: true @@ -17378,9 +17170,6 @@ snapshots: '@esbuild/darwin-arm64@0.25.12': optional: true - '@esbuild/darwin-arm64@0.27.7': - optional: true - '@esbuild/darwin-x64@0.19.12': optional: true @@ -17393,9 +17182,6 @@ snapshots: '@esbuild/darwin-x64@0.25.12': optional: true - '@esbuild/darwin-x64@0.27.7': - optional: true - '@esbuild/freebsd-arm64@0.19.12': optional: true @@ -17408,9 +17194,6 @@ snapshots: '@esbuild/freebsd-arm64@0.25.12': optional: true - '@esbuild/freebsd-arm64@0.27.7': - optional: true - '@esbuild/freebsd-x64@0.19.12': optional: true @@ -17423,9 +17206,6 @@ snapshots: '@esbuild/freebsd-x64@0.25.12': optional: true - '@esbuild/freebsd-x64@0.27.7': - optional: true - '@esbuild/linux-arm64@0.19.12': optional: true @@ -17438,9 +17218,6 @@ snapshots: '@esbuild/linux-arm64@0.25.12': optional: true - '@esbuild/linux-arm64@0.27.7': - optional: true - '@esbuild/linux-arm@0.19.12': optional: true @@ -17453,9 +17230,6 @@ snapshots: '@esbuild/linux-arm@0.25.12': optional: true - '@esbuild/linux-arm@0.27.7': - optional: true - '@esbuild/linux-ia32@0.19.12': optional: true @@ -17468,9 +17242,6 @@ snapshots: '@esbuild/linux-ia32@0.25.12': optional: true - '@esbuild/linux-ia32@0.27.7': - optional: true - '@esbuild/linux-loong64@0.19.12': optional: true @@ -17483,9 +17254,6 @@ snapshots: '@esbuild/linux-loong64@0.25.12': optional: true - '@esbuild/linux-loong64@0.27.7': - optional: true - '@esbuild/linux-mips64el@0.19.12': optional: true @@ -17498,9 +17266,6 @@ snapshots: '@esbuild/linux-mips64el@0.25.12': optional: true - '@esbuild/linux-mips64el@0.27.7': - optional: true - '@esbuild/linux-ppc64@0.19.12': optional: true @@ -17513,9 +17278,6 @@ snapshots: '@esbuild/linux-ppc64@0.25.12': optional: true - '@esbuild/linux-ppc64@0.27.7': - optional: true - '@esbuild/linux-riscv64@0.19.12': optional: true @@ -17528,9 +17290,6 @@ snapshots: '@esbuild/linux-riscv64@0.25.12': optional: true - '@esbuild/linux-riscv64@0.27.7': - optional: true - '@esbuild/linux-s390x@0.19.12': optional: true @@ -17543,9 +17302,6 @@ snapshots: '@esbuild/linux-s390x@0.25.12': optional: true - '@esbuild/linux-s390x@0.27.7': - optional: true - '@esbuild/linux-x64@0.19.12': optional: true @@ -17558,18 +17314,12 @@ snapshots: '@esbuild/linux-x64@0.25.12': optional: true - '@esbuild/linux-x64@0.27.7': - optional: true - '@esbuild/netbsd-arm64@0.24.2': optional: true '@esbuild/netbsd-arm64@0.25.12': optional: true - '@esbuild/netbsd-arm64@0.27.7': - optional: true - '@esbuild/netbsd-x64@0.19.12': optional: true @@ -17582,18 +17332,12 @@ snapshots: '@esbuild/netbsd-x64@0.25.12': optional: true - '@esbuild/netbsd-x64@0.27.7': - optional: true - '@esbuild/openbsd-arm64@0.24.2': optional: true '@esbuild/openbsd-arm64@0.25.12': optional: true - '@esbuild/openbsd-arm64@0.27.7': - optional: true - '@esbuild/openbsd-x64@0.19.12': optional: true @@ -17606,15 +17350,9 @@ snapshots: '@esbuild/openbsd-x64@0.25.12': optional: true - '@esbuild/openbsd-x64@0.27.7': - optional: true - '@esbuild/openharmony-arm64@0.25.12': optional: true - '@esbuild/openharmony-arm64@0.27.7': - optional: true - '@esbuild/sunos-x64@0.19.12': optional: true @@ -17627,9 +17365,6 @@ snapshots: '@esbuild/sunos-x64@0.25.12': optional: true - '@esbuild/sunos-x64@0.27.7': - optional: true - '@esbuild/win32-arm64@0.19.12': optional: true @@ -17642,9 +17377,6 @@ snapshots: '@esbuild/win32-arm64@0.25.12': optional: true - '@esbuild/win32-arm64@0.27.7': - optional: true - '@esbuild/win32-ia32@0.19.12': optional: true @@ -17657,9 +17389,6 @@ snapshots: '@esbuild/win32-ia32@0.25.12': optional: true - '@esbuild/win32-ia32@0.27.7': - optional: true - '@esbuild/win32-x64@0.19.12': optional: true @@ -17672,9 +17401,6 @@ snapshots: '@esbuild/win32-x64@0.25.12': optional: true - '@esbuild/win32-x64@0.27.7': - optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@8.57.1)': dependencies: eslint: 8.57.1 @@ -18863,10 +18589,10 @@ snapshots: '@percy/dom@1.31.13': {} - '@percy/ember@5.0.1(@babel/core@7.29.0)(@glint/template@1.7.7)(webpack@5.106.2(esbuild@0.27.7)(postcss@8.5.14))': + '@percy/ember@5.0.1(@babel/core@7.29.0)(@glint/template@1.7.7)(webpack@5.106.2)': dependencies: '@percy/sdk-utils': 1.31.13 - ember-auto-import: 2.13.1(@glint/template@1.7.7)(webpack@5.106.2(esbuild@0.27.7)(postcss@8.5.14)) + ember-auto-import: 2.13.1(@glint/template@1.7.7)(webpack@5.106.2) ember-cli-babel: 8.3.1(@babel/core@7.29.0) transitivePeerDependencies: - '@babel/core' @@ -20692,14 +20418,14 @@ snapshots: babel-import-util@3.0.1: {} - babel-loader@8.4.1(@babel/core@7.29.0)(webpack@5.106.2(esbuild@0.27.7)(postcss@8.5.14)): + babel-loader@8.4.1(@babel/core@7.29.0)(webpack@5.106.2(postcss@8.5.14)): dependencies: '@babel/core': 7.29.0(supports-color@8.1.1) find-cache-dir: 3.3.2 loader-utils: 2.0.4 make-dir: 3.1.0 schema-utils: 2.7.1 - webpack: 5.106.2(esbuild@0.27.7)(postcss@8.5.14) + webpack: 5.106.2(postcss@8.5.14) babel-loader@8.4.1(@babel/core@7.29.0)(webpack@5.106.2): dependencies: @@ -20710,12 +20436,12 @@ snapshots: schema-utils: 2.7.1 webpack: 5.106.2 - babel-loader@9.2.1(@babel/core@7.29.0)(webpack@5.106.2): + babel-loader@9.2.1(@babel/core@7.29.0)(webpack@5.106.2(postcss@8.5.14)): dependencies: '@babel/core': 7.29.0(supports-color@8.1.1) find-cache-dir: 4.0.0 schema-utils: 4.3.3 - webpack: 5.106.2 + webpack: 5.106.2(postcss@8.5.14) babel-plugin-debug-macros@0.2.0(@babel/core@7.29.0): dependencies: @@ -22089,7 +21815,7 @@ snapshots: crypto-random-string@2.0.0: {} - css-loader@5.2.7(webpack@5.106.2(esbuild@0.27.7)(postcss@8.5.14)): + css-loader@5.2.7(webpack@5.106.2(postcss@8.5.14)): dependencies: icss-utils: 5.1.0(postcss@8.5.14) loader-utils: 2.0.4 @@ -22101,7 +21827,7 @@ snapshots: postcss-value-parser: 4.2.0 schema-utils: 3.3.0 semver: 7.8.0 - webpack: 5.106.2(esbuild@0.27.7)(postcss@8.5.14) + webpack: 5.106.2(postcss@8.5.14) css-loader@5.2.7(webpack@5.106.2): dependencies: @@ -22724,7 +22450,7 @@ snapshots: transitivePeerDependencies: - supports-color - ember-auto-import@2.13.1(@glint/template@1.7.7)(webpack@5.106.2(esbuild@0.27.7)(postcss@8.5.14)): + ember-auto-import@2.13.1(@glint/template@1.7.7)(webpack@5.106.2(postcss@8.5.14)): dependencies: '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.29.0) @@ -22735,7 +22461,7 @@ snapshots: '@embroider/macros': 1.16.13(@glint/template@1.7.7) '@embroider/reverse-exports': 0.2.0 '@embroider/shared-internals': 2.9.2(supports-color@8.1.1) - babel-loader: 8.4.1(@babel/core@7.29.0)(webpack@5.106.2(esbuild@0.27.7)(postcss@8.5.14)) + babel-loader: 8.4.1(@babel/core@7.29.0)(webpack@5.106.2(postcss@8.5.14)) babel-plugin-ember-modules-api-polyfill: 3.5.0 babel-plugin-ember-template-compilation: 2.4.1 babel-plugin-htmlbars-inline-precompile: 5.3.1 @@ -22745,7 +22471,7 @@ snapshots: broccoli-merge-trees: 4.2.0 broccoli-plugin: 4.0.7 broccoli-source: 3.0.1 - css-loader: 5.2.7(webpack@5.106.2(esbuild@0.27.7)(postcss@8.5.14)) + css-loader: 5.2.7(webpack@5.106.2(postcss@8.5.14)) debug: 4.4.3(supports-color@8.1.1) fs-extra: 10.1.0 fs-tree-diff: 2.0.1 @@ -22753,14 +22479,14 @@ snapshots: is-subdir: 1.2.0 js-string-escape: 1.0.1 lodash: 4.18.1 - mini-css-extract-plugin: 2.10.2(webpack@5.106.2(esbuild@0.27.7)(postcss@8.5.14)) + mini-css-extract-plugin: 2.10.2(webpack@5.106.2(postcss@8.5.14)) minimatch: 3.1.5 parse5: 6.0.1 pkg-entry-points: 1.1.1 resolve: 1.22.12 resolve-package-path: 4.0.3 semver: 7.8.0 - style-loader: 2.0.0(patch_hash=579dd92e6adabd45669f9a99a01c6c28c97488c7bf4ee0d6c1c622a14592e4c8)(webpack@5.106.2(esbuild@0.27.7)(postcss@8.5.14)) + style-loader: 2.0.0(patch_hash=579dd92e6adabd45669f9a99a01c6c28c97488c7bf4ee0d6c1c622a14592e4c8)(webpack@5.106.2(postcss@8.5.14)) typescript-memoize: 1.1.1 walk-sync: 3.0.0 transitivePeerDependencies: @@ -23549,13 +23275,13 @@ snapshots: transitivePeerDependencies: - eslint - ember-exam@10.1.0(@glint/template@1.7.7)(ember-qunit@9.0.4(@ember/test-helpers@5.4.2(@babel/core@7.29.0)(@glint/template@1.7.7))(@glint/template@1.7.7)(qunit@2.25.0))(ember-source@6.10.1(patch_hash=ea945024993105fb6cc4ae5cb5e9ea8e0eff6cd5fe0b0033c43dd0cf9453eb0d)(@glimmer/component@2.1.1)(rsvp@4.8.5))(qunit@2.25.0)(webpack@5.106.2(esbuild@0.27.7)(postcss@8.5.14)): + ember-exam@10.1.0(@glint/template@1.7.7)(ember-qunit@9.0.4(@ember/test-helpers@5.4.2(@babel/core@7.29.0)(@glint/template@1.7.7))(@glint/template@1.7.7)(qunit@2.25.0))(ember-source@6.10.1(patch_hash=ea945024993105fb6cc4ae5cb5e9ea8e0eff6cd5fe0b0033c43dd0cf9453eb0d)(@glimmer/component@2.1.1)(rsvp@4.8.5))(qunit@2.25.0)(webpack@5.106.2): dependencies: '@babel/core': 7.29.0(supports-color@8.1.1) chalk: 5.6.2 cli-table3: 0.6.5 debug: 4.4.3(supports-color@8.1.1) - ember-auto-import: 2.13.1(@glint/template@1.7.7)(webpack@5.106.2(esbuild@0.27.7)(postcss@8.5.14)) + ember-auto-import: 2.13.1(@glint/template@1.7.7)(webpack@5.106.2) ember-cli-babel: 8.3.1(@babel/core@7.29.0) ember-qunit: 9.0.4(@ember/test-helpers@5.4.2(@babel/core@7.29.0)(@glint/template@1.7.7))(@glint/template@1.7.7)(qunit@2.25.0) ember-source: 6.10.1(patch_hash=ea945024993105fb6cc4ae5cb5e9ea8e0eff6cd5fe0b0033c43dd0cf9453eb0d)(@glimmer/component@2.1.1)(rsvp@4.8.5) @@ -23581,31 +23307,6 @@ snapshots: - '@babel/core' - supports-color - ember-freestyle@0.20.0(@babel/core@7.29.0)(@ember/string@4.0.1)(@glint/template@1.7.7)(ember-source@6.10.1(patch_hash=ea945024993105fb6cc4ae5cb5e9ea8e0eff6cd5fe0b0033c43dd0cf9453eb0d)(@glimmer/component@2.1.1)(rsvp@4.8.5))(webpack@5.106.2(esbuild@0.27.7)(postcss@8.5.14)): - dependencies: - '@ember/render-modifiers': 2.1.0(@babel/core@7.29.0)(@glint/template@1.7.7)(ember-source@6.10.1(patch_hash=ea945024993105fb6cc4ae5cb5e9ea8e0eff6cd5fe0b0033c43dd0cf9453eb0d)(@glimmer/component@2.1.1)(rsvp@4.8.5)) - '@ember/string': 4.0.1 - '@glimmer/component': 2.1.1 - '@glimmer/tracking': 1.1.2 - ember-auto-import: 2.13.1(@glint/template@1.7.7)(webpack@5.106.2(esbuild@0.27.7)(postcss@8.5.14)) - ember-cli-babel: 7.26.11 - ember-cli-htmlbars: 6.3.0 - ember-cli-typescript: 5.3.0 - ember-focus-trap: 1.2.0(@babel/core@7.29.0) - ember-modifier: 4.3.0(@babel/core@7.29.0) - ember-named-blocks-polyfill: 0.2.5 - ember-truth-helpers: 4.0.3(ember-source@6.10.1(patch_hash=ea945024993105fb6cc4ae5cb5e9ea8e0eff6cd5fe0b0033c43dd0cf9453eb0d)(@glimmer/component@2.1.1)(rsvp@4.8.5)) - json-formatter-js: 2.5.23 - macro-decorators: 0.1.2 - strip-indent: 3.0.0 - tracked-built-ins: 4.1.2(@babel/core@7.29.0) - transitivePeerDependencies: - - '@babel/core' - - '@glint/template' - - ember-source - - supports-color - - webpack - ember-freestyle@0.20.0(@babel/core@7.29.0)(@ember/string@4.0.1)(@glint/template@1.7.7)(ember-source@6.10.1(patch_hash=ea945024993105fb6cc4ae5cb5e9ea8e0eff6cd5fe0b0033c43dd0cf9453eb0d)(@glimmer/component@2.1.1)(rsvp@4.8.5))(webpack@5.106.2): dependencies: '@ember/render-modifiers': 2.1.0(@babel/core@7.29.0)(@glint/template@1.7.7)(ember-source@6.10.1(patch_hash=ea945024993105fb6cc4ae5cb5e9ea8e0eff6cd5fe0b0033c43dd0cf9453eb0d)(@glimmer/component@2.1.1)(rsvp@4.8.5)) @@ -23631,12 +23332,12 @@ snapshots: - supports-color - webpack - ember-freestyle@0.22.0(@babel/core@7.29.0)(@ember/string@4.0.1)(@glint/template@1.7.7)(ember-source@6.10.1(patch_hash=ea945024993105fb6cc4ae5cb5e9ea8e0eff6cd5fe0b0033c43dd0cf9453eb0d)(@glimmer/component@2.1.1)(rsvp@4.8.5))(webpack@5.106.2): + ember-freestyle@0.22.0(@babel/core@7.29.0)(@ember/string@4.0.1)(@glint/template@1.7.7)(ember-source@6.10.1(patch_hash=ea945024993105fb6cc4ae5cb5e9ea8e0eff6cd5fe0b0033c43dd0cf9453eb0d)(@glimmer/component@2.1.1)(rsvp@4.8.5))(webpack@5.106.2(postcss@8.5.14)): dependencies: '@ember/string': 4.0.1 '@glimmer/component': 2.1.1 '@glimmer/tracking': 1.1.2 - ember-auto-import: 2.13.1(@glint/template@1.7.7)(webpack@5.106.2) + ember-auto-import: 2.13.1(@glint/template@1.7.7)(webpack@5.106.2(postcss@8.5.14)) ember-cli-babel: 8.3.1(@babel/core@7.29.0) ember-cli-htmlbars: 6.3.0 ember-cli-typescript: 5.3.0 @@ -24316,35 +24017,6 @@ snapshots: '@esbuild/win32-ia32': 0.25.12 '@esbuild/win32-x64': 0.25.12 - esbuild@0.27.7: - optionalDependencies: - '@esbuild/aix-ppc64': 0.27.7 - '@esbuild/android-arm': 0.27.7 - '@esbuild/android-arm64': 0.27.7 - '@esbuild/android-x64': 0.27.7 - '@esbuild/darwin-arm64': 0.27.7 - '@esbuild/darwin-x64': 0.27.7 - '@esbuild/freebsd-arm64': 0.27.7 - '@esbuild/freebsd-x64': 0.27.7 - '@esbuild/linux-arm': 0.27.7 - '@esbuild/linux-arm64': 0.27.7 - '@esbuild/linux-ia32': 0.27.7 - '@esbuild/linux-loong64': 0.27.7 - '@esbuild/linux-mips64el': 0.27.7 - '@esbuild/linux-ppc64': 0.27.7 - '@esbuild/linux-riscv64': 0.27.7 - '@esbuild/linux-s390x': 0.27.7 - '@esbuild/linux-x64': 0.27.7 - '@esbuild/netbsd-arm64': 0.27.7 - '@esbuild/netbsd-x64': 0.27.7 - '@esbuild/openbsd-arm64': 0.27.7 - '@esbuild/openbsd-x64': 0.27.7 - '@esbuild/openharmony-arm64': 0.27.7 - '@esbuild/sunos-x64': 0.27.7 - '@esbuild/win32-arm64': 0.27.7 - '@esbuild/win32-ia32': 0.27.7 - '@esbuild/win32-x64': 0.27.7 - escalade@3.2.0: {} escape-html@1.0.3: {} @@ -27160,11 +26832,11 @@ snapshots: min-indent@1.0.1: {} - mini-css-extract-plugin@2.10.2(webpack@5.106.2(esbuild@0.27.7)(postcss@8.5.14)): + mini-css-extract-plugin@2.10.2(webpack@5.106.2(postcss@8.5.14)): dependencies: schema-utils: 4.3.3 tapable: 2.3.3 - webpack: 5.106.2(esbuild@0.27.7)(postcss@8.5.14) + webpack: 5.106.2(postcss@8.5.14) mini-css-extract-plugin@2.10.2(webpack@5.106.2): dependencies: @@ -29487,11 +29159,11 @@ snapshots: stubborn-utils@1.0.2: {} - style-loader@2.0.0(patch_hash=579dd92e6adabd45669f9a99a01c6c28c97488c7bf4ee0d6c1c622a14592e4c8)(webpack@5.106.2(esbuild@0.27.7)(postcss@8.5.14)): + style-loader@2.0.0(patch_hash=579dd92e6adabd45669f9a99a01c6c28c97488c7bf4ee0d6c1c622a14592e4c8)(webpack@5.106.2(postcss@8.5.14)): dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.106.2(esbuild@0.27.7)(postcss@8.5.14) + webpack: 5.106.2(postcss@8.5.14) style-loader@2.0.0(patch_hash=579dd92e6adabd45669f9a99a01c6c28c97488c7bf4ee0d6c1c622a14592e4c8)(webpack@5.106.2): dependencies: @@ -29673,15 +29345,14 @@ snapshots: ansi-escapes: 7.3.0 supports-hyperlinks: 3.2.0 - terser-webpack-plugin@5.6.0(esbuild@0.27.7)(postcss@8.5.14)(webpack@5.106.2(esbuild@0.27.7)(postcss@8.5.14)): + terser-webpack-plugin@5.6.0(postcss@8.5.14)(webpack@5.106.2(postcss@8.5.14)): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 terser: 5.47.1 - webpack: 5.106.2(esbuild@0.27.7)(postcss@8.5.14) + webpack: 5.106.2(postcss@8.5.14) optionalDependencies: - esbuild: 0.27.7 postcss: 8.5.14 terser-webpack-plugin@5.6.0(webpack@5.106.2): @@ -29793,14 +29464,14 @@ snapshots: dependencies: editions: 6.22.0 - thread-loader@3.0.4(webpack@5.106.2): + thread-loader@3.0.4(webpack@5.106.2(postcss@8.5.14)): dependencies: json-parse-better-errors: 1.0.2 loader-runner: 4.3.2 loader-utils: 2.0.4 neo-async: 2.6.2 schema-utils: 3.3.0 - webpack: 5.106.2 + webpack: 5.106.2(postcss@8.5.14) through2@3.0.2: dependencies: @@ -30018,13 +29689,6 @@ snapshots: tslib: 1.14.1 typescript: 5.9.3 - tsx@4.21.0: - dependencies: - esbuild: 0.27.7 - get-tsconfig: 4.14.0 - optionalDependencies: - fsevents: 2.3.3 - tunnel-agent@0.6.0: dependencies: safe-buffer: 5.2.1 @@ -30351,7 +30015,7 @@ snapshots: lightningcss: 1.32.0 terser: 5.47.1 - vite@6.4.2(@types/node@24.12.4)(lightningcss@1.32.0)(terser@5.47.1)(tsx@4.21.0)(yaml@2.9.0): + vite@6.4.2(@types/node@24.12.4)(lightningcss@1.32.0)(terser@5.47.1)(yaml@2.9.0): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.4) @@ -30364,10 +30028,9 @@ snapshots: fsevents: 2.3.3 lightningcss: 1.32.0 terser: 5.47.1 - tsx: 4.21.0 yaml: 2.9.0 - vite@6.4.2(@types/node@25.7.0)(lightningcss@1.32.0)(terser@5.47.1)(tsx@4.21.0)(yaml@2.9.0): + vite@6.4.2(@types/node@25.7.0)(lightningcss@1.32.0)(terser@5.47.1)(yaml@2.9.0): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.4) @@ -30380,10 +30043,9 @@ snapshots: fsevents: 2.3.3 lightningcss: 1.32.0 terser: 5.47.1 - tsx: 4.21.0 yaml: 2.9.0 - vite@8.0.12(@types/node@25.7.0)(esbuild@0.27.7)(terser@5.47.1)(tsx@4.21.0)(yaml@2.9.0): + vite@8.0.12(@types/node@25.7.0)(terser@5.47.1)(yaml@2.9.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 @@ -30392,10 +30054,8 @@ snapshots: tinyglobby: 0.2.16 optionalDependencies: '@types/node': 25.7.0 - esbuild: 0.27.7 fsevents: 2.3.3 terser: 5.47.1 - tsx: 4.21.0 yaml: 2.9.0 vitest@2.1.9(@types/node@24.12.4)(jsdom@26.1.0)(lightningcss@1.32.0)(terser@5.47.1): @@ -30663,7 +30323,7 @@ snapshots: - postcss - uglify-js - webpack@5.106.2(esbuild@0.27.7)(postcss@8.5.14): + webpack@5.106.2(postcss@8.5.14): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.9 @@ -30686,7 +30346,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.3 - terser-webpack-plugin: 5.6.0(esbuild@0.27.7)(postcss@8.5.14)(webpack@5.106.2(esbuild@0.27.7)(postcss@8.5.14)) + terser-webpack-plugin: 5.6.0(postcss@8.5.14)(webpack@5.106.2(postcss@8.5.14)) watchpack: 2.5.1 webpack-sources: 3.4.1 transitivePeerDependencies: