From a190b72005555977599089e6881e897a2bdbe592 Mon Sep 17 00:00:00 2001 From: Sharjeel Yunus Date: Thu, 20 Aug 2026 01:37:54 +0500 Subject: [PATCH 1/3] feat(push): enhance push command to sync app manifest with CDN - Updated `ensemble push` command to also sync the app manifest to the CDN after pushing changes to the cloud. - Modified README to reflect the new behavior of the `ensemble push` command. - Added error handling for CDN sync failures, ensuring appropriate error messages are displayed. - Enhanced tests to cover new CDN sync functionality and error scenarios. --- README.md | 4 +- src/cloud/cdnClient.ts | 84 +++++++++++++++++++++++++++++++++ src/commands/push.ts | 15 ++++++ tests/cloud/cdnClient.test.ts | 77 ++++++++++++++++++++++++++++++ tests/commands/pushPull.test.ts | 72 +++++++++++++++++++++++++++- 5 files changed, 248 insertions(+), 4 deletions(-) create mode 100644 src/cloud/cdnClient.ts create mode 100644 tests/cloud/cdnClient.test.ts diff --git a/README.md b/README.md index edd100b..e392858 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ ensemble update | `ensemble logout` | Log out and clear local auth session | | `ensemble token` | Print token for CI (set as `ENSEMBLE_TOKEN`); run `ensemble login` first | | `ensemble init` | Initialize or update `ensemble.config.json` in the project | -| `ensemble push` | Scan the app directory and push changes to the cloud | +| `ensemble push` | Scan the app directory and push changes to the cloud (also syncs the app manifest to CDN) | | `ensemble pull` | Pull artifacts from the cloud and overwrite local files | | `ensemble release` | Manage releases (snapshots) of your app (interactive menu or subcommands) | | `ensemble add` | Add a new screen, widget, script, action, translation, or asset | @@ -154,7 +154,7 @@ Run YAML tests in a Flutter starter project (starter root or `ensemble/apps/ { + const res = await fetch(CREATE_APP_MANIFEST_URL, { + method: 'POST', + headers: { + Authorization: `Bearer ${idToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + data: { + appId, + }, + }), + }); + + const text = await res.text(); + let parsed: unknown = {}; + try { + parsed = text ? (JSON.parse(text) as unknown) : {}; + } catch { + parsed = { raw: text }; + } + + if (!res.ok) { + throw new CdnClientError({ + message: `CDN sync failed (${res.status}).`, + status: res.status, + hint: + res.status === 401 || res.status === 403 + ? 'Authentication/authorization failed for CDN sync. Run `ensemble login` and retry.' + : undefined, + cause: parsed, + }); + } + + parseCreateAppManifestResponse(parsed); +} diff --git a/src/commands/push.ts b/src/commands/push.ts index 52888cd..9758c55 100644 --- a/src/commands/push.ts +++ b/src/commands/push.ts @@ -2,6 +2,7 @@ import fs from 'fs/promises'; import path from 'path'; import prompts from 'prompts'; +import { createAppManifest, CdnClientError } from '../cloud/cdnClient.js'; import { checkAppAccess, fetchCloudApp, @@ -510,6 +511,8 @@ export async function pushCommand(options: PushOptions = {}): Promise { await writeEnvFile(root, envLocal.configWriteFile, pendingLocalEnvConfigWrite); } + let cloudWritten = false; + if (yamlChangeTotal > 0 || assetsToUpload.length > 0 || assetsToArchive.length > 0) { const { assetsUploaded } = await withSpinner('Pushing changes to cloud...', () => submitCliPush(appId, idToken, pushPayload, firestoreOptions, { @@ -517,6 +520,7 @@ export async function pushCommand(options: PushOptions = {}): Promise { ...(assetsToUpload.length > 0 && { assetFileNames: assetsToUpload }), }) ); + cloudWritten = true; if (assetsUploaded > 0) { ui.success(`Uploaded ${assetsUploaded} asset(s) and updated .env.config.`); } @@ -534,6 +538,11 @@ export async function pushCommand(options: PushOptions = {}): Promise { firestoreOptions ) ); + cloudWritten = true; + } + + if (cloudWritten) { + await withSpinner('Syncing to CDN...', () => createAppManifest(appId, idToken)); } if (manifestNeedsRefresh && bundle) { @@ -563,6 +572,12 @@ export async function pushCommand(options: PushOptions = {}): Promise { } else if (err.code === 'NETWORK_UNAVAILABLE') { console.error('Network error. Check your internet connection or proxy settings.'); } + } else if (err instanceof CdnClientError) { + console.error(err.message); + if (err.hint) { + console.error(err.hint); + } + console.error('Cloud push succeeded, but CDN sync failed.'); } else { const message = err instanceof Error ? err.message : String(err); console.error(message); diff --git a/tests/cloud/cdnClient.test.ts b/tests/cloud/cdnClient.test.ts new file mode 100644 index 0000000..0904634 --- /dev/null +++ b/tests/cloud/cdnClient.test.ts @@ -0,0 +1,77 @@ +import { afterEach, describe, expect, it } from 'vitest'; + +import { createAppManifest, CdnClientError } from '../../src/cloud/cdnClient.js'; + +describe('cdnClient', () => { + const originalFetch = globalThis.fetch; + + afterEach(() => { + globalThis.fetch = originalFetch; + }); + + it('posts expected payload and accepts direct success response', async () => { + let captured: { url: string; method?: string; headers?: HeadersInit; body?: string } | null = + null; + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const urlStr = + typeof input === 'string' + ? input + : input instanceof URL + ? input.toString() + : (input as Request).url; + captured = { + url: urlStr, + method: init?.method, + headers: init?.headers, + body: init?.body as string | undefined, + }; + return new Response(JSON.stringify({ success: true }), { status: 200 }); + }) as unknown as typeof fetch; + + await createAppManifest('app-1', 'id-token'); + + expect(captured).not.toBeNull(); + expect(captured!.url).toContain('createAppManifest'); + expect(captured!.method).toBe('POST'); + const headers = new Headers(captured!.headers); + expect(headers.get('Authorization')).toBe('Bearer id-token'); + expect(headers.get('Content-Type')).toBe('application/json'); + expect(captured!.body).toBe(JSON.stringify({ data: { appId: 'app-1' } })); + }); + + it('parses callable-style result wrapper', async () => { + globalThis.fetch = (async () => { + return new Response(JSON.stringify({ result: { success: true } }), { status: 200 }); + }) as unknown as typeof fetch; + + await expect(createAppManifest('app-1', 'id-token')).resolves.toBeUndefined(); + }); + + it('accepts empty 200 response body', async () => { + globalThis.fetch = (async () => { + return new Response('', { status: 200 }); + }) as unknown as typeof fetch; + + await expect(createAppManifest('app-1', 'id-token')).resolves.toBeUndefined(); + }); + + it('throws when success is false', async () => { + globalThis.fetch = (async () => { + return new Response(JSON.stringify({ success: false }), { status: 200 }); + }) as unknown as typeof fetch; + + await expect(createAppManifest('app-1', 'id-token')).rejects.toThrow(CdnClientError); + }); + + it('throws on non-2xx with auth hint for 403', async () => { + globalThis.fetch = (async () => { + return new Response(JSON.stringify({ error: 'forbidden' }), { status: 403 }); + }) as unknown as typeof fetch; + + await expect(createAppManifest('app-1', 'id-token')).rejects.toMatchObject({ + message: 'CDN sync failed (403).', + status: 403, + hint: expect.stringContaining('ensemble login'), + }); + }); +}); diff --git a/tests/commands/pushPull.test.ts b/tests/commands/pushPull.test.ts index 8bfe090..450000a 100644 --- a/tests/commands/pushPull.test.ts +++ b/tests/commands/pushPull.test.ts @@ -86,7 +86,16 @@ const cloudModuleMock = vi.hoisted(() => { }; }); -vi.mock('../../src/cloud/firestoreClient.js', () => cloudModuleMock); +vi.mock('../../src/cloud/firestoreClient.js', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + checkAppAccess: cloudModuleMock.checkAppAccess, + fetchCloudApp: cloudModuleMock.fetchCloudApp, + submitCliPush: cloudModuleMock.submitCliPush, + submitEnvDocumentsPush: cloudModuleMock.submitEnvDocumentsPush, + }; +}); const assetClientMock = vi.hoisted(() => ({ uploadAssetToStudio: vi.fn(async (_appId: string, fileName: string) => ({ @@ -102,6 +111,18 @@ const assetClientMock = vi.hoisted(() => ({ vi.mock('../../src/cloud/assetClient.js', () => assetClientMock); +const cdnClientMock = vi.hoisted(() => ({ + createAppManifest: vi.fn(async () => {}), +})); + +vi.mock('../../src/cloud/cdnClient.js', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + createAppManifest: cdnClientMock.createAppManifest, + }; +}); + const promptsModuleMock = vi.hoisted(() => ({ default: vi.fn(async () => ({ proceed: true })), })); @@ -165,6 +186,8 @@ describe('push/pull integration (commands)', () => { expect(submitCliPush).toHaveBeenCalledTimes(1); const [appId, , payload] = submitCliPush.mock.calls[0] as [string, string, unknown]; expect(appId).toBe('app1'); + expect(cdnClientMock.createAppManifest).toHaveBeenCalledTimes(1); + expect(cdnClientMock.createAppManifest).toHaveBeenCalledWith('app1', 'token'); const p = payload as { translations?: { operation: string; @@ -276,6 +299,7 @@ describe('push/pull integration (commands)', () => { submitCliPush: ReturnType; }; expect(submitCliPush).not.toHaveBeenCalled(); + expect(cdnClientMock.createAppManifest).not.toHaveBeenCalled(); // Dry run output should clearly indicate non-destructive behavior and how to apply. const lines = logSpy.mock.calls.map(([msg]) => String(msg)); @@ -544,6 +568,7 @@ describe('push/pull integration (commands)', () => { await pushCommand({ verbose: false, yes: true }); expect(submitCliPush).not.toHaveBeenCalled(); + expect(cdnClientMock.createAppManifest).not.toHaveBeenCalled(); const messages = logSpy.mock.calls.map((args) => args[0]); expect( messages.some( @@ -593,6 +618,7 @@ describe('push/pull integration (commands)', () => { await pushCommand({ verbose: false, yes: true }); expect(submitCliPush).not.toHaveBeenCalled(); + expect(cdnClientMock.createAppManifest).not.toHaveBeenCalled(); const messages = logSpy.mock.calls.map((args) => args[0]); expect( messages.some( @@ -602,7 +628,6 @@ describe('push/pull integration (commands)', () => { logSpy.mockRestore(); }); - it('pull respects app options and does not overwrite disabled artifact kinds', async () => { // Disable screens in app options appOptionsRef.value = { screens: false }; @@ -1039,6 +1064,49 @@ describe('push/pull integration (commands)', () => { )[2]; expect(payload.config?.envVariables?.API_URL).toBe('https://local.example.com'); expect(payload.secrets?.secrets?.S1).toBe('local-secret'); + expect(cdnClientMock.createAppManifest).toHaveBeenCalledTimes(1); + expect(cdnClientMock.createAppManifest).toHaveBeenCalledWith('app1', 'token'); + }); + + it('push fails when CDN sync fails after cloud push', async () => { + await fs.writeFile(path.join(projectRoot, 'screens', 'Home.yaml'), 'home: content', 'utf8'); + await fs.writeFile(path.join(projectRoot, 'translations', 'en.yaml'), 'en: content', 'utf8'); + + (cloudModuleMock.fetchCloudApp as ReturnType).mockResolvedValueOnce({ + id: 'app1', + name: 'App', + screens: [], + widgets: [], + scripts: [], + translations: [], + theme: undefined, + }); + + const { CdnClientError } = await import('../../src/cloud/cdnClient.js'); + cdnClientMock.createAppManifest.mockRejectedValueOnce( + new CdnClientError({ message: 'CDN sync failed (500).' }) + ); + + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + const originalExitCode = process.exitCode; + + await pushCommand({ yes: true }); + + expect(cloudModuleMock.submitCliPush).toHaveBeenCalledTimes(1); + expect(cdnClientMock.createAppManifest).toHaveBeenCalledTimes(1); + expect(process.exitCode).toBe(1); + const errors = errorSpy.mock.calls.map(([msg]) => String(msg)).join('\n'); + expect(errors).toContain('Cloud push succeeded, but CDN sync failed.'); + expect( + logSpy.mock.calls.some( + ([msg]) => typeof msg === 'string' && msg.includes('Pushed app "App" to environment "dev"') + ) + ).toBe(false); + + process.exitCode = originalExitCode; + errorSpy.mockRestore(); + logSpy.mockRestore(); }); it('push clears cloud secrets when .env.secrets is empty', async () => { From 38e5d396297c8a23e0bee69ea941a2b9ce40c100 Mon Sep 17 00:00:00 2001 From: Sharjeel Yunus Date: Thu, 20 Aug 2026 01:39:50 +0500 Subject: [PATCH 2/3] refactor: improve type definitions and formatting in multiple files - Updated type definitions for better clarity and consistency across various files, including `index.ts`, `firestoreClient.ts`, `applyToFs.ts`, `bundleDiff.ts`, and `sync.ts`. - Enhanced the formatting of command descriptions in `README.md` for improved readability. --- README.md | 24 ++++++++++++------------ src/cloud/firestoreClient.ts | 3 ++- src/core/applyToFs.ts | 9 ++++++--- src/core/bundleDiff.ts | 3 ++- src/core/sync.ts | 12 ++++++++---- src/index.ts | 8 +++++++- 6 files changed, 37 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index e392858..da20566 100644 --- a/README.md +++ b/README.md @@ -26,19 +26,19 @@ ensemble update ## Commands -| Command | Description | -| ------------------ | ----------------------------------------------------------------------------- | -| `ensemble login` | Log in to Ensemble (opens browser) | -| `ensemble logout` | Log out and clear local auth session | -| `ensemble token` | Print token for CI (set as `ENSEMBLE_TOKEN`); run `ensemble login` first | -| `ensemble init` | Initialize or update `ensemble.config.json` in the project | +| Command | Description | +| ------------------ | ----------------------------------------------------------------------------------------- | +| `ensemble login` | Log in to Ensemble (opens browser) | +| `ensemble logout` | Log out and clear local auth session | +| `ensemble token` | Print token for CI (set as `ENSEMBLE_TOKEN`); run `ensemble login` first | +| `ensemble init` | Initialize or update `ensemble.config.json` in the project | | `ensemble push` | Scan the app directory and push changes to the cloud (also syncs the app manifest to CDN) | -| `ensemble pull` | Pull artifacts from the cloud and overwrite local files | -| `ensemble release` | Manage releases (snapshots) of your app (interactive menu or subcommands) | -| `ensemble add` | Add a new screen, widget, script, action, translation, or asset | -| `ensemble enable` | Enable starter modules (camera, location, google_maps, etc.) in a Flutter app | -| `ensemble test` | Run declarative YAML tests in a Flutter starter project | -| `ensemble update` | Update the CLI to the latest version | +| `ensemble pull` | Pull artifacts from the cloud and overwrite local files | +| `ensemble release` | Manage releases (snapshots) of your app (interactive menu or subcommands) | +| `ensemble add` | Add a new screen, widget, script, action, translation, or asset | +| `ensemble enable` | Enable starter modules (camera, location, google_maps, etc.) in a Flutter app | +| `ensemble test` | Run declarative YAML tests in a Flutter starter project | +| `ensemble update` | Update the CLI to the latest version | ### Options diff --git a/src/cloud/firestoreClient.ts b/src/cloud/firestoreClient.ts index 30bc3b9..1c5f047 100644 --- a/src/cloud/firestoreClient.ts +++ b/src/cloud/firestoreClient.ts @@ -1056,7 +1056,8 @@ export async function submitEnvDocumentsPush( function getCollaboratorRole( collaboratorsField: - { mapValue?: { fields?: Record } } | undefined, + | { mapValue?: { fields?: Record } } + | undefined, userKey: string ): string | undefined { const mapFields = collaboratorsField?.mapValue?.fields; diff --git a/src/core/applyToFs.ts b/src/core/applyToFs.ts index 7393173..8c29722 100644 --- a/src/core/applyToFs.ts +++ b/src/core/applyToFs.ts @@ -26,7 +26,8 @@ export interface ApplyCloudStateToFsOptions { } type WriteTask = - { op: 'write'; filePath: string; content: string } | { op: 'delete'; filePath: string }; + | { op: 'write'; filePath: string; content: string } + | { op: 'delete'; filePath: string }; /** * Write local artifact files to match the given cloud/snapshot state. @@ -65,7 +66,8 @@ export async function applyCloudStateToFs( await ensureDir(baseDir); const cloudItems = (cloudApp as Record)[prop] as - { name: string; content?: string; isArchived?: boolean }[] | undefined; + | { name: string; content?: string; isArchived?: boolean }[] + | undefined; const expected: Record = {}; for (const item of cloudItems ?? []) { @@ -74,7 +76,8 @@ export async function applyCloudStateToFs( } const actual = (localFiles as unknown as Record)[prop] as - Record | undefined; + | Record + | undefined; const actualMap = actual ?? {}; const expectedKeys = new Set(Object.keys(expected)); diff --git a/src/core/bundleDiff.ts b/src/core/bundleDiff.ts index 0fa57f8..19ac3f2 100644 --- a/src/core/bundleDiff.ts +++ b/src/core/bundleDiff.ts @@ -330,7 +330,8 @@ function buildYamlPushItems( diff: { changed: ArtifactWithContent[]; new: ArtifactWithContent[] }, cloudItems: ArtifactWithContent[] | undefined, bundleItems: - (ArtifactWithContent & { type?: string; updatedAt?: string; updatedBy?: object })[] | undefined, + | (ArtifactWithContent & { type?: string; updatedAt?: string; updatedBy?: object })[] + | undefined, cloudById: Map< string, ArtifactWithContent & { type?: string; updatedAt?: string; updatedBy?: object } diff --git a/src/core/sync.ts b/src/core/sync.ts index 1d5ebf4..e6534ee 100644 --- a/src/core/sync.ts +++ b/src/core/sync.ts @@ -257,13 +257,15 @@ export function computePullPlan({ const expected: Record = {}; const cloudItems = (cloudApp as Record)[prop] as - { name: string; content?: string; isArchived?: boolean }[] | undefined; + | { name: string; content?: string; isArchived?: boolean }[] + | undefined; for (const item of cloudItems ?? []) { if (item.isArchived === true) continue; expected[`${item.name}${ext!}`] = item.content ?? ''; } const actual = (localFiles as unknown as Record)[prop] as - Record | undefined; + | Record + | undefined; const actualMap = actual ?? {}; const expectedKeys = Object.keys(expected).sort(); @@ -375,13 +377,15 @@ export function computePullPlan({ const kind = getArtifactConfig(prop as Exclude).label; const expected: Record = {}; const cloudItems = (cloudApp as Record)[prop] as - { name: string; content?: string; isArchived?: boolean }[] | undefined; + | { name: string; content?: string; isArchived?: boolean }[] + | undefined; for (const item of cloudItems ?? []) { if (item.isArchived === true) continue; expected[`${item.name}${ext!}`] = item.content ?? ''; } const actual = (localFiles as unknown as Record)[prop] as - Record | undefined; + | Record + | undefined; const actualMap = actual ?? {}; const expectedKeys = new Set(Object.keys(expected)); diff --git a/src/index.ts b/src/index.ts index 2a2aba1..4c5b9c9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -188,7 +188,13 @@ program .option('--overwrite', 'Overwrite existing file when adding (assets)', false) .action(async (kind?: string, name?: string, options?: { overwrite?: boolean }) => { let normalizedKind: - 'screen' | 'widget' | 'script' | 'action' | 'translation' | 'asset' | undefined; + | 'screen' + | 'widget' + | 'script' + | 'action' + | 'translation' + | 'asset' + | undefined; if (kind) { const k = kind.toLowerCase(); if ( From 30245dd00a826c1f4ce11d3f7db70d5060e5cd82 Mon Sep 17 00:00:00 2001 From: Sharjeel Yunus Date: Thu, 20 Aug 2026 01:45:06 +0500 Subject: [PATCH 3/3] refactor: streamline type definitions across multiple files - Consolidated type definitions for improved clarity and consistency in `index.ts`, `firestoreClient.ts`, `applyToFs.ts`, `bundleDiff.ts`, and `sync.ts`. - Enhanced formatting for better readability and maintainability. --- src/cloud/firestoreClient.ts | 3 +-- src/core/applyToFs.ts | 9 +++------ src/core/bundleDiff.ts | 3 +-- src/core/sync.ts | 12 ++++-------- src/index.ts | 8 +------- 5 files changed, 10 insertions(+), 25 deletions(-) diff --git a/src/cloud/firestoreClient.ts b/src/cloud/firestoreClient.ts index 1c5f047..30bc3b9 100644 --- a/src/cloud/firestoreClient.ts +++ b/src/cloud/firestoreClient.ts @@ -1056,8 +1056,7 @@ export async function submitEnvDocumentsPush( function getCollaboratorRole( collaboratorsField: - | { mapValue?: { fields?: Record } } - | undefined, + { mapValue?: { fields?: Record } } | undefined, userKey: string ): string | undefined { const mapFields = collaboratorsField?.mapValue?.fields; diff --git a/src/core/applyToFs.ts b/src/core/applyToFs.ts index 8c29722..7393173 100644 --- a/src/core/applyToFs.ts +++ b/src/core/applyToFs.ts @@ -26,8 +26,7 @@ export interface ApplyCloudStateToFsOptions { } type WriteTask = - | { op: 'write'; filePath: string; content: string } - | { op: 'delete'; filePath: string }; + { op: 'write'; filePath: string; content: string } | { op: 'delete'; filePath: string }; /** * Write local artifact files to match the given cloud/snapshot state. @@ -66,8 +65,7 @@ export async function applyCloudStateToFs( await ensureDir(baseDir); const cloudItems = (cloudApp as Record)[prop] as - | { name: string; content?: string; isArchived?: boolean }[] - | undefined; + { name: string; content?: string; isArchived?: boolean }[] | undefined; const expected: Record = {}; for (const item of cloudItems ?? []) { @@ -76,8 +74,7 @@ export async function applyCloudStateToFs( } const actual = (localFiles as unknown as Record)[prop] as - | Record - | undefined; + Record | undefined; const actualMap = actual ?? {}; const expectedKeys = new Set(Object.keys(expected)); diff --git a/src/core/bundleDiff.ts b/src/core/bundleDiff.ts index 19ac3f2..0fa57f8 100644 --- a/src/core/bundleDiff.ts +++ b/src/core/bundleDiff.ts @@ -330,8 +330,7 @@ function buildYamlPushItems( diff: { changed: ArtifactWithContent[]; new: ArtifactWithContent[] }, cloudItems: ArtifactWithContent[] | undefined, bundleItems: - | (ArtifactWithContent & { type?: string; updatedAt?: string; updatedBy?: object })[] - | undefined, + (ArtifactWithContent & { type?: string; updatedAt?: string; updatedBy?: object })[] | undefined, cloudById: Map< string, ArtifactWithContent & { type?: string; updatedAt?: string; updatedBy?: object } diff --git a/src/core/sync.ts b/src/core/sync.ts index e6534ee..1d5ebf4 100644 --- a/src/core/sync.ts +++ b/src/core/sync.ts @@ -257,15 +257,13 @@ export function computePullPlan({ const expected: Record = {}; const cloudItems = (cloudApp as Record)[prop] as - | { name: string; content?: string; isArchived?: boolean }[] - | undefined; + { name: string; content?: string; isArchived?: boolean }[] | undefined; for (const item of cloudItems ?? []) { if (item.isArchived === true) continue; expected[`${item.name}${ext!}`] = item.content ?? ''; } const actual = (localFiles as unknown as Record)[prop] as - | Record - | undefined; + Record | undefined; const actualMap = actual ?? {}; const expectedKeys = Object.keys(expected).sort(); @@ -377,15 +375,13 @@ export function computePullPlan({ const kind = getArtifactConfig(prop as Exclude).label; const expected: Record = {}; const cloudItems = (cloudApp as Record)[prop] as - | { name: string; content?: string; isArchived?: boolean }[] - | undefined; + { name: string; content?: string; isArchived?: boolean }[] | undefined; for (const item of cloudItems ?? []) { if (item.isArchived === true) continue; expected[`${item.name}${ext!}`] = item.content ?? ''; } const actual = (localFiles as unknown as Record)[prop] as - | Record - | undefined; + Record | undefined; const actualMap = actual ?? {}; const expectedKeys = new Set(Object.keys(expected)); diff --git a/src/index.ts b/src/index.ts index 4c5b9c9..2a2aba1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -188,13 +188,7 @@ program .option('--overwrite', 'Overwrite existing file when adding (assets)', false) .action(async (kind?: string, name?: string, options?: { overwrite?: boolean }) => { let normalizedKind: - | 'screen' - | 'widget' - | 'script' - | 'action' - | 'translation' - | 'asset' - | undefined; + 'screen' | 'widget' | 'script' | 'action' | 'translation' | 'asset' | undefined; if (kind) { const k = kind.toLowerCase(); if (