Skip to content

Commit 6f652df

Browse files
committed
feat: Added ability to import into an existing codify file without any effort (codify import with no arguments). Fixed bug with memory access by limiting how many sub-progresses are shown. Fixed file bug with one resource updates
1 parent 7e4db19 commit 6f652df

File tree

8 files changed

+221
-60
lines changed

8 files changed

+221
-60
lines changed

src/orchestrators/import.ts

Lines changed: 103 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ import { DependencyMap, PluginManager } from '../plugins/plugin-manager.js';
99
import { PromptType, Reporter } from '../ui/reporters/reporter.js';
1010
import { FileUtils } from '../utils/file.js';
1111
import { FileModificationCalculator, ModificationType } from '../utils/file-modification-calculator.js';
12-
import { sleep } from '../utils/index.js';
12+
import { groupBy, sleep } from '../utils/index.js';
1313
import { wildCardMatch } from '../utils/wild-card-match.js';
14-
import { InitializeOrchestrator } from './initialize.js';
14+
import { InitializationResult, InitializeOrchestrator } from './initialize.js';
1515

1616
export type RequiredParameters = Map<string, RequiredParameter[]>;
1717
export type UserSuppliedParameters = Map<string, Record<string, unknown>>;
@@ -46,34 +46,70 @@ export class ImportOrchestrator {
4646
reporter: Reporter
4747
) {
4848
const { typeIds } = args
49-
if (typeIds.length === 0) {
50-
throw new Error('At least one resource <type> must be specified. Ex: "codify import homebrew"')
51-
}
52-
5349
ctx.processStarted(ProcessName.IMPORT)
5450

55-
const { typeIdsToDependenciesMap, pluginManager, project } = await InitializeOrchestrator.run(
51+
const initializationResult = await InitializeOrchestrator.run(
5652
{ ...args, allowEmptyProject: true },
5753
reporter
5854
);
59-
55+
const { project } = initializationResult;
56+
57+
if ((!typeIds || typeIds.length === 0) && project.isEmpty()) {
58+
throw new Error('At least one resource [type] must be specified. Ex: "codify import homebrew". Or the import command must be run in a directory with a valid codify file')
59+
}
60+
61+
if (!typeIds || typeIds.length === 0) {
62+
await ImportOrchestrator.runExistingProject(reporter, initializationResult)
63+
} else {
64+
await ImportOrchestrator.runNewImport(typeIds, reporter, initializationResult)
65+
}
66+
}
67+
68+
/** Import new resources. Type ids supplied. This will ask for any required parameters */
69+
static async runNewImport(typeIds: string[], reporter: Reporter, initializeResult: InitializationResult): Promise<ResourceConfig[]> {
70+
const { project, pluginManager, typeIdsToDependenciesMap } = initializeResult;
71+
6072
const matchedTypes = this.matchTypeIds(typeIds, [...typeIdsToDependenciesMap.keys()])
6173
await ImportOrchestrator.validate(matchedTypes, project, pluginManager, typeIdsToDependenciesMap);
6274

6375
const resourceInfoList = await pluginManager.getMultipleResourceInfo(matchedTypes);
64-
65-
const importParameters = await ImportOrchestrator.getImportParameters(reporter, project, resourceInfoList);
66-
const importResult = await ImportOrchestrator.import(pluginManager, importParameters);
76+
const resourcesToImport = await ImportOrchestrator.getImportParameters(reporter, project, resourceInfoList);
77+
const importResult = await ImportOrchestrator.import(pluginManager, resourcesToImport);
6778

6879
ctx.processFinished(ProcessName.IMPORT)
69-
reporter.displayImportResult(importResult, false);
7080

71-
const additionalResourceInfo = await pluginManager.getMultipleResourceInfo(project.resourceConfigs.map((r) => r.type));
72-
resourceInfoList.push(...additionalResourceInfo);
81+
reporter.displayImportResult(importResult, false);
7382

83+
resourceInfoList.push(...(await pluginManager.getMultipleResourceInfo(
84+
project.resourceConfigs.map((r) => r.type)
85+
)));
7486
await ImportOrchestrator.saveResults(reporter, importResult, project, resourceInfoList)
7587
}
7688

89+
/** Update an existing project. This will use the existing resources as the parameters (no user input required). */
90+
static async runExistingProject(reporter: Reporter, initializeResult: InitializationResult): Promise<ResourceConfig[]> {
91+
const { pluginManager, project } = initializeResult;
92+
93+
await pluginManager.validate(project);
94+
const importResult = await ImportOrchestrator.import(pluginManager, project.resourceConfigs);
95+
96+
ctx.processFinished(ProcessName.IMPORT);
97+
98+
const resourceInfoList = await pluginManager.getMultipleResourceInfo(
99+
project.resourceConfigs.map((r) => r.type),
100+
);
101+
102+
await ImportOrchestrator.updateExistingFiles(
103+
reporter,
104+
project,
105+
importResult,
106+
resourceInfoList,
107+
project.codifyFiles[0],
108+
);
109+
110+
return project.resourceConfigs;
111+
}
112+
77113
static async import(
78114
pluginManager: PluginManager,
79115
resources: ResourceConfig[],
@@ -90,7 +126,10 @@ export class ImportOrchestrator {
90126
if (response.result !== null && response.result.length > 0) {
91127
importedConfigs.push(...response
92128
?.result
93-
?.map((r) => ResourceConfig.fromJson(r)) ?? []
129+
?.map((r) =>
130+
// Keep the name on the resource if possible, this makes it easier to identify where the import came from
131+
ResourceConfig.fromJson({ ...r, core: { ...r.core, name: resource.name } })
132+
) ?? []
94133
);
95134
} else {
96135
errors.push(`Unable to import resource '${resource.type}', resource not found`);
@@ -183,18 +222,20 @@ ${JSON.stringify(unsupportedTypeIds)}`);
183222

184223
const promptResult = await reporter.promptOptions(
185224
'\nDo you want to save the results?',
186-
[projectExists ? multipleCodifyFiles ? 'Update existing file (multiple found)' : `Update existing file (${project.codifyFiles})` : undefined, 'In a new file', 'No'].filter(Boolean) as string[]
225+
[
226+
projectExists ?
227+
multipleCodifyFiles ? `Update existing files (${project.codifyFiles})` : `Update existing file (${project.codifyFiles})`
228+
: undefined,
229+
'In a new file',
230+
'No'
231+
].filter(Boolean) as string[]
187232
)
188233

189-
if (promptResult === 'Update existing file (multiple found)') {
190-
const file = await reporter.promptOptions(
191-
'\nWhich file would you like to update?',
192-
project.codifyFiles,
193-
)
194-
await ImportOrchestrator.updateExistingFile(reporter, file, importResult, resourceInfoList);
195-
196-
} else if (promptResult.startsWith('Update existing file')) {
197-
await ImportOrchestrator.updateExistingFile(reporter, project.codifyFiles[0], importResult, resourceInfoList);
234+
if (promptResult.startsWith('Update existing file')) {
235+
const file = multipleCodifyFiles
236+
? await reporter.promptOptions('\nIf new resources are added, where to write them?', project.codifyFiles)
237+
: project.codifyFiles[0];
238+
await ImportOrchestrator.updateExistingFiles(reporter, project, importResult, resourceInfoList, file);
198239

199240
} else if (promptResult === 'In a new file') {
200241
const newFileName = await ImportOrchestrator.generateNewImportFileName();
@@ -209,42 +250,62 @@ ${JSON.stringify(unsupportedTypeIds)}`);
209250
}
210251
}
211252

212-
private static async updateExistingFile(
253+
private static async updateExistingFiles(
213254
reporter: Reporter,
214-
filePath: string,
255+
existingProject: Project,
215256
importResult: ImportResult,
216-
resourceInfoList: ResourceInfo[]
257+
resourceInfoList: ResourceInfo[],
258+
preferredFile: string, // File to write any new resources (unknown file path)
217259
): Promise<void> {
218-
const existing = await CodifyParser.parse(filePath);
219-
ImportOrchestrator.attachResourceInfo(importResult.result, resourceInfoList);
220-
ImportOrchestrator.attachResourceInfo(existing.resourceConfigs, resourceInfoList);
260+
const groupedResults = groupBy(importResult.result, (r) =>
261+
existingProject.findSpecific(r.type, r.name)?.sourceMapKey?.split('#')?.[0] ?? 'unknown'
262+
)
221263

222-
const modificationCalculator = new FileModificationCalculator(existing);
223-
const result = modificationCalculator.calculate(importResult.result.map((resource) => ({
224-
modification: ModificationType.INSERT_OR_UPDATE,
225-
resource
226-
})));
264+
// New resources exists (they don't belong to any existing files)
265+
if (groupedResults.unknown) {
266+
groupedResults[preferredFile] = [
267+
...groupedResults.unknown,
268+
...groupedResults[preferredFile],
269+
]
270+
delete groupedResults.unknown;
271+
}
272+
273+
const diffs = await Promise.all(Object.entries(groupedResults).map(async ([filePath, imported]) => {
274+
const existing = await CodifyParser.parse(filePath!);
275+
ImportOrchestrator.attachResourceInfo(imported, resourceInfoList);
276+
ImportOrchestrator.attachResourceInfo(existing.resourceConfigs, resourceInfoList);
277+
278+
const modificationCalculator = new FileModificationCalculator(existing);
279+
const modification = modificationCalculator.calculate(imported.map((resource) => ({
280+
modification: ModificationType.INSERT_OR_UPDATE,
281+
resource
282+
})));
283+
284+
return { file: filePath!, modification };
285+
}));
227286

228287
// No changes to be made
229-
if (result.diff === '') {
288+
if (diffs.every((d) => d.modification.diff === '')) {
230289
reporter.displayMessage('\nNo changes are needed! Exiting...')
231290

232291
// Wait for the message to display before we exit
233292
await sleep(100);
234-
process.exit(0);
293+
return;
235294
}
236295

237-
reporter.displayFileModification(result.diff);
238-
const shouldSave = await reporter.promptConfirmation(`Save to file (${filePath})?`);
296+
reporter.displayFileModifications(diffs);
297+
const shouldSave = await reporter.promptConfirmation('Save the changes?');
239298
if (!shouldSave) {
240299
reporter.displayMessage('\nSkipping save! Exiting...');
241300

242301
// Wait for the message to display before we exit
243302
await sleep(100);
244-
process.exit(0);
303+
return;
245304
}
246305

247-
await FileUtils.writeFile(filePath, result.newFile);
306+
for (const diff of diffs) {
307+
await FileUtils.writeFile(diff.file, diff.modification.newFile);
308+
}
248309

249310
reporter.displayMessage('\n🎉 Imported completed and saved to file 🎉');
250311

src/ui/components/default-component.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import React, { useLayoutEffect, useState } from 'react';
88

99
import { Plan } from '../../entities/plan.js';
1010
import { ImportResult } from '../../orchestrators/import.js';
11+
import { FileModificationResult } from '../../utils/file-modification-calculator.js';
1112
import { RenderEvent } from '../reporters/reporter.js';
1213
import { RenderStatus, store } from '../store/index.js';
1314
import { FileModificationDisplay } from './file-modification/FileModification.js';
@@ -102,8 +103,8 @@ export function DefaultComponent(props: {
102103
}
103104
{
104105
renderStatus === RenderStatus.DISPLAY_FILE_MODIFICATION && (
105-
<Static items={[renderData as string]}>{
106-
(diff, idx) => <FileModificationDisplay diff={diff} key={idx}/>
106+
<Static items={[renderData as Array<{ file: string; modification: FileModificationResult }>]}>{
107+
(data, idx) => <FileModificationDisplay data={data} key={idx}/>
107108
}</Static>
108109
)
109110
}
Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,26 @@
11
import { Box, Text } from 'ink';
22
import React from 'react';
33

4+
import { FileModificationResult } from '../../../utils/file-modification-calculator.js';
5+
46
export function FileModificationDisplay(props: {
5-
diff: string,
7+
data: Array<{ file: string; modification: FileModificationResult }>,
68
}) {
79
return <Box flexDirection="column">
810
<Box borderColor="green" borderStyle="round">
9-
<Text>File Modification</Text>
11+
<Text>File Modifications</Text>
1012
</Box>
11-
<Text>The following changes will be made</Text>
12-
<Text> </Text>
13-
<Text>{props.diff}</Text>
13+
{
14+
props.data
15+
.filter(({ modification }) => modification.diff)
16+
.map(({ file, modification }, idx) =>
17+
<Box flexDirection="column" key={idx}>
18+
<Text backgroundColor='yellow' bold>File {file}</Text>
19+
<Text> </Text>
20+
<Text>{modification.diff}</Text>
21+
<Text> </Text>
22+
</Box>
23+
)
24+
}
1425
</Box>
1526
}

src/ui/components/progress/progress-display.tsx

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export function ProgressDisplay(
4444
: <StatusMessage variant="success">{label}</StatusMessage>
4545
}
4646
<Box flexDirection="column" marginLeft={2}>
47-
<SubProgressDisplay emitter={props.emitter} eventType={props.eventType} subProgresses={subProgresses} />
47+
<SubProgressDisplay emitter={props.emitter} eventType={props.eventType} subProgresses={subProgresses}/>
4848
</Box>
4949
</Box>
5050
}
@@ -59,10 +59,15 @@ export function SubProgressDisplay(
5959
const { subProgresses, emitter, eventType } = props;
6060

6161
return <>{
62-
subProgresses && subProgresses.map((s, idx) =>
63-
s.status === ProgressStatus.IN_PROGRESS
64-
? <Spinner eventEmitter={emitter} eventType={eventType} key={idx} label={s.label} type="circleHalves"/>
65-
: <StatusMessage key={idx} variant="success">{s.label}</StatusMessage>
66-
)
62+
subProgresses && subProgresses
63+
// Sort the subprocesses so that in progress ones are always at the bottom
64+
.sort((a, b) => a.status === ProgressStatus.IN_PROGRESS ? 1 : -1)
65+
// Limit the max number of subprocesses to 7. Too many doesn't look good and causes a wasm memory access error (yoga)
66+
.slice(Math.max(0, subProgresses.length - 7), subProgresses.length)
67+
.map((s, idx) =>
68+
s.status === ProgressStatus.IN_PROGRESS
69+
? <Spinner eventEmitter={emitter} eventType={eventType} key={idx} label={s.label} type="circleHalves"/>
70+
: <StatusMessage key={idx} variant="success">{s.label}</StatusMessage>
71+
)
6772
}</>
6873
}

src/ui/reporters/default-reporter.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { ResourceConfig } from '../../entities/resource-config.js';
1010
import { ResourceInfo } from '../../entities/resource-info.js';
1111
import { Event, ProcessName, SubProcessName, ctx } from '../../events/context.js';
1212
import { ImportResult } from '../../orchestrators/import.js';
13+
import { FileModificationResult } from '../../utils/file-modification-calculator.js';
1314
import { SudoUtils } from '../../utils/sudo.js';
1415
import { DefaultComponent } from '../components/default-component.js';
1516
import { ProgressState, ProgressStatus } from '../components/progress/progress-display.js';
@@ -154,7 +155,7 @@ export class DefaultReporter implements Reporter {
154155
return result
155156
}
156157

157-
displayFileModification(diff: string) {
158+
displayFileModifications(diff: Array<{ file: string; modification: FileModificationResult}>) {
158159
this.updateRenderState(RenderStatus.DISPLAY_FILE_MODIFICATION, diff);
159160
}
160161

src/ui/reporters/reporter.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { ImportResult } from '../../orchestrators/import.js';
55
import { DefaultReporter } from './default-reporter.js';
66
import { ResourceInfo } from '../../entities/resource-info.js';
77
import { ResourceConfig } from '../../entities/resource-config.js';
8+
import { FileModificationResult } from '../../utils/file-modification-calculator.js';
89

910
export enum RenderEvent {
1011
LOG = 'log',
@@ -51,7 +52,7 @@ export interface Reporter {
5152

5253
displayImportResult(importResult: ImportResult, showConfigs: boolean): void;
5354

54-
displayFileModification(diff: string): void
55+
displayFileModifications(diff: Array<{ file: string, modification: FileModificationResult }>): void
5556

5657
displayMessage(message: string): void
5758
}

0 commit comments

Comments
 (0)