diff --git a/src/commands/components/actions.ts b/src/commands/components/actions.ts index cb3d386..aa90f16 100644 --- a/src/commands/components/actions.ts +++ b/src/commands/components/actions.ts @@ -75,6 +75,25 @@ export const fetchComponentPresets = async (space: string, token: string, region } } +export const pushComponent = async (space: string, component: SpaceComponent, token: string, region: RegionCode): Promise => { + try { + const url = getStoryblokUrl(region) + const response = await customFetch<{ + component: SpaceComponent + }>(`${url}/spaces/${space}/components`, { + method: 'POST', + headers: { + Authorization: token, + }, + body: JSON.stringify(component), + }) + return response.component + } + catch (error) { + handleAPIError('push_component', error as Error) + } +} + export const saveComponentsToFiles = async ( space: string, spaceData: SpaceData, @@ -97,12 +116,9 @@ export const saveComponentsToFiles = async ( const presetsFilePath = join(resolvedPath, suffix ? `${component.name}.preset.${suffix}.json` : `${component.name}.preset.json`) await saveToFile(presetsFilePath, JSON.stringify(componentPresets, null, 2)) } - // Find and save associated groups - const componentGroups = groups.filter(group => group.uuid === component.component_group_uuid) - if (componentGroups.length > 0) { - const groupsFilePath = join(resolvedPath, suffix ? `${component.name}.group.${suffix}.json` : `${component.name}.group.json`) - await saveToFile(groupsFilePath, JSON.stringify(componentGroups, null, 2)) - } + // Save groups + const groupsFilePath = join(resolvedPath, suffix ? `groups.${suffix}.json` : `groups.json`) + await saveToFile(groupsFilePath, JSON.stringify(groups, null, 2)) } return } @@ -137,17 +153,16 @@ export const readComponentsFiles = async ( groups: [], presets: [], } + // Add regex patterns to match file structures + const componentsPattern = /^components(?:\..+)?\.json$/ + const groupsPattern = /^groups(?:\..+)?\.json$/ + const presetsPattern = /^presets(?:\..+)?\.json$/ try { if (!separateFiles) { // Read from consolidated files const files = await readdir(resolvedPath, { recursive: !separateFiles }) - // Add regex patterns to match file structures - const componentsPattern = /^components(?:\..+)?\.json$/ - const groupsPattern = /^groups(?:\..+)?\.json$/ - const presetsPattern = /^presets(?:\..+)?\.json$/ - for (const file of files) { if (!file.endsWith('.json') || !componentsPattern.test(file) && !groupsPattern.test(file) && !presetsPattern.test(file)) { continue } @@ -187,12 +202,11 @@ export const readComponentsFiles = async ( if (!file.endsWith('.json')) { continue } // Skip consolidated files in separate files mode - if (/^(?:components|groups|presets)\.json$/.test(file)) { continue } + if (/^(?:components|presets)\.json$/.test(file)) { continue } const { dir, name } = parse(file) const isPreset = /\.preset\.json$/.test(file) - const isGroup = /\.group\.json$/.test(file) - const baseName = name.replace(/\.preset$/, '').replace(/\.group$/, '').split('.')[0] + const baseName = name.replace(/\.preset$/, '').split('.')[0] // Skip if filter is set and doesn't match the base component name if (regex && !regex.test(baseName)) { continue } @@ -203,8 +217,8 @@ export const readComponentsFiles = async ( if (isPreset) { spaceData.presets.push(...data) } - else if (isGroup) { - spaceData.groups.push(...data) + else if (groupsPattern.test(file)) { + spaceData.groups = data } else { // Regular component file diff --git a/src/commands/components/index.ts b/src/commands/components/index.ts index 5e7231f..33441fe 100644 --- a/src/commands/components/index.ts +++ b/src/commands/components/index.ts @@ -3,7 +3,7 @@ import { colorPalette, commands } from '../../constants' import { session } from '../../session' import { getProgram } from '../../program' import { CommandError, handleError, konsola } from '../../utils' -import { fetchComponent, fetchComponentGroups, fetchComponentPresets, fetchComponents, readComponentsFiles, saveComponentsToFiles } from './actions' +import { fetchComponent, fetchComponentGroups, fetchComponentPresets, fetchComponents, pushComponent, readComponentsFiles, saveComponentsToFiles } from './actions' import type { PullComponentsOptions, PushComponentsOptions } from './constants' const program = getProgram() // Get the shared singleton instance @@ -124,19 +124,55 @@ componentsCommand path, }) - console.log('Components:', spaceData.components.length) - console.log('Groups:', spaceData.groups.length) - console.log('Presets:', spaceData.presets.length) + const results = { + successful: [] as string[], + failed: [] as Array<{ name: string, error: unknown }>, + } + + try { + await pushComponent(space, spaceData.components[0], state.password, state.region) + results.successful.push(spaceData.components[0].name) + } + catch (error) { + results.failed.push({ + name: spaceData.components[0].name, + error, + }) + } + // Process all components sequentially + /* for (const component of spaceData.components) { + try { + await pushComponent(space, component, state.password, state.region) + results.successful.push(component.name) + } + catch (error) { + results.failed.push({ + name: component.name, + error, + }) + } + } */ + + console.log(results) - console.log('Components:', spaceData.components.map(c => c.name)) - console.log('Groups:', spaceData.groups.map(g => g.name)) - console.log('Presets:', spaceData.presets.map(p => p.name)) + // Display summary + konsola.ok(`Successfully pushed ${results.successful.length} components:`) + if (results.successful.length > 0) { + results.successful.forEach(name => konsola.info(`✓ ${name}`)) + } + + if (results.failed.length > 0) { + konsola.error('', null, { + header: true, + }) + konsola.error(`Failed to push ${results.failed.length} components:`) + results.failed.forEach(({ name, error }) => { + konsola.error(`✗ ${name}`, error) + }) + } if (filter) { - console.log('Applied filter:', filter) - console.log('Filtered components:', spaceData.components.map(c => c.name)) - console.log('Filtered groups:', spaceData.groups.map(g => g.name)) - console.log('Filtered presets:', spaceData.presets.map(p => p.name)) + konsola.info('Filter applied:', filter) } } catch (error) { diff --git a/src/utils/error/api-error.ts b/src/utils/error/api-error.ts index 779d0cd..b8e75a0 100644 --- a/src/utils/error/api-error.ts +++ b/src/utils/error/api-error.ts @@ -10,6 +10,7 @@ export const API_ACTIONS = { pull_components: 'Failed to pull components', pull_component_groups: 'Failed to pull component groups', pull_component_presets: 'Failed to pull component presets', + push_component: 'Failed to push component', } as const export const API_ERRORS = { @@ -19,6 +20,7 @@ export const API_ERRORS = { timeout: 'The API request timed out', generic: 'Error fetching data from the API', not_found: 'The requested resource was not found', + unprocessable_entity: 'The request was well-formed but was unable to be followed due to semantic errors', } as const export function handleAPIError(action: keyof typeof API_ACTIONS, error: unknown): void { @@ -31,7 +33,7 @@ export function handleAPIError(action: keyof typeof API_ACTIONS, error: unknown) case 404: throw new APIError('not_found', action, error) case 422: - throw new APIError('invalid_credentials', action, error) + throw new APIError('unprocessable_entity', action, error) default: throw new APIError('network_error', action, error) } @@ -64,6 +66,7 @@ export class APIError extends Error { cause: this.cause, errorId: this.errorId, stack: this.stack, + data: this.error?.response?.data, } } }