Skip to content

Commit 4886daa

Browse files
committed
feat: added superseded types and separated out find resource into a dedicated call
1 parent 538888f commit 4886daa

14 files changed

Lines changed: 133 additions & 25 deletions

File tree

‎CLAUDE.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ npm run posttest # Runs lint after tests
2525
```bash
2626
./bin/dev.js <command> # Run CLI in development mode
2727
./bin/dev.js apply # Example: run apply command
28+
CODIFY_BETA=1 ./bin/dev.js destroy alias # Use the latest beta plugins instead of the latest stable
29+
npm run start:dev:beta -- destroy alias # Same as above
2830
```
2931

3032
### Test Command (VM Testing)

‎package-lock.json‎

Lines changed: 11 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
},
66
"dependencies": {
77
"@codifycli/ink-form": "0.0.12",
8-
"@codifycli/schemas": "1.2.0",
8+
"@codifycli/schemas": "1.4.0",
99
"@homebridge/node-pty-prebuilt-multiarch": "npm:node-pty@1.2.0-beta.15",
1010
"@mischnic/json-sourcemap": "^0.1.1",
1111
"@oclif/core": "^4.0.8",
@@ -45,7 +45,7 @@
4545
},
4646
"description": "Codify is a configuration-as-code tool that declaratively installs and manages developer tools and applications. Check out https://dashboard.codifycli.com for an editor.",
4747
"devDependencies": {
48-
"@codifycli/plugin-core": "^1.2.11",
48+
"@codifycli/plugin-core": "^1.3.0",
4949
"@oclif/prettier-config": "^0.2.1",
5050
"@types/chalk": "^2.2.0",
5151
"@types/cors": "^2.8.19",
@@ -143,6 +143,7 @@
143143
"test": "vitest",
144144
"version": "oclif readme && git add README.md",
145145
"start:dev": "./bin/dev.js",
146+
"start:dev:beta": "CODIFY_BETA=1 ./bin/dev.js",
146147
"start:vm": "npm run build && npm run pack:macos && npm run start:vm",
147148
"deploy": "npm run pkg && npm run notarize && npm run upload",
148149
"build:npm": "tsx ./scripts/build-npm.ts",

‎src/config.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,6 @@ export const config = {
1414
apiUrl: 'https://api.codifycli.com',
1515
supabaseUrl: 'https://kdctbvqvqjfquplxhqrm.supabase.co',
1616

17-
isBeta: VERSION.includes('beta'),
17+
// Beta CLI versions use beta plugins. Set CODIFY_BETA=1 to use beta plugins from a non-beta CLI (ex: local development)
18+
isBeta: VERSION.includes('beta') || ['1', 'true'].includes(process.env.CODIFY_BETA?.toLowerCase() ?? ''),
1819
}

‎src/orchestrators/destroy.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -253,8 +253,9 @@ ${JSON.stringify(unsupportedTypeIds)}`);
253253
}
254254

255255
/**
256-
* Find the instances of a resource that exist on the system using the plugin's import (the same call as import all).
257-
* Each instance is labelled by the values required to identify it. Returns an empty list if no instances are found.
256+
* Find the instances of a resource that exist on the system using the plugin's findInstances (discovery only, separate
257+
* from import so resources that can't be imported can still be discovered). Each instance is labelled by the values
258+
* required to identify it. Returns an empty list if no instances are found or the plugin doesn't support discovery.
258259
*/
259260
private static async findExistingInstances(
260261
pluginManager: PluginManager,
@@ -268,7 +269,7 @@ ${JSON.stringify(unsupportedTypeIds)}`);
268269

269270
let imported: ResourceConfig[];
270271
try {
271-
const response = await pluginManager.importResource({ core: { type: info.type }, parameters: {} }, true);
272+
const response = await pluginManager.findInstances(info.type);
272273
imported = (response.result ?? []).map((r) => ResourceConfig.fromJson(r));
273274
} catch (error) {
274275
ctx.debug(`Unable to find existing instances of ${info.type}: ${error}`);

‎src/orchestrators/import.ts‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { prettyFormatFileDiff } from '../ui/file-diff-pretty-printer.js';
1818
import { PromptType, Reporter } from '../ui/reporters/reporter.js';
1919
import { FileUtils } from '../utils/file.js';
2020
import { groupBy, sleep } from '../utils/index.js';
21+
import { removeSupersededTypes } from '../utils/superseded-types.js';
2122
import { wildCardMatch } from '../utils/wild-card-match.js';
2223
import { LoginOrchestrator } from './login.js';
2324

@@ -60,9 +61,12 @@ export class ImportOrchestrator {
6061

6162
ctx.subprocessStarted(SubProcessName.IMPORT_RESOURCE)
6263

63-
// Omit sensitive resources if not included
64-
const typeIdsToImport = [...resourceDefinitions.keys()]
65-
.filter((typeId) => args.includeSensitive || (!args.includeSensitive && (resourceDefinitions.get(typeId)?.sensitiveParameters ?? []).length === 0))
64+
// Omit sensitive resources if not included, and resources whose state is already imported by another resource
65+
const typeIdsToImport = removeSupersededTypes(
66+
[...resourceDefinitions.keys()]
67+
.filter((typeId) => args.includeSensitive || (!args.includeSensitive && (resourceDefinitions.get(typeId)?.sensitiveParameters ?? []).length === 0)),
68+
resourceDefinitions,
69+
);
6670

6771
const importResults = await Promise.all(typeIdsToImport.map(async (typeId) => {
6872
try {

‎src/orchestrators/init.ts‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { Reporter } from '../ui/reporters/reporter.js';
1111
import { FileUtils } from '../utils/file.js';
1212
import { resolvePathWithVariables, tildify, untildify } from '../utils/index.js';
1313
import { ShellUtils } from '../utils/shell.js';
14+
import { removeSupersededTypes } from '../utils/superseded-types.js';
1415

1516
export interface InitArgs {
1617
path?: string;
@@ -38,9 +39,12 @@ export const InitializeOrchestrator = {
3839
const currentDistro = os.type() === OS.Linux ? await ShellUtils.getLinuxDistro() : undefined;
3940

4041
// Omit sensitive resources and resources not supported on the current OS
41-
const typeIdsToImport = [...resourceDefinitions.keys()]
42-
.filter((typeId) => args.includeSensitive || (!args.includeSensitive && (resourceDefinitions.get(typeId)?.sensitiveParameters ?? []).length === 0))
43-
.filter((typeId) => this.filterByOperatingSystemAndDistro(typeId, resourceDefinitions, currentDistro))
42+
const typeIdsToImport = removeSupersededTypes(
43+
[...resourceDefinitions.keys()]
44+
.filter((typeId) => args.includeSensitive || (!args.includeSensitive && (resourceDefinitions.get(typeId)?.sensitiveParameters ?? []).length === 0))
45+
.filter((typeId) => this.filterByOperatingSystemAndDistro(typeId, resourceDefinitions, currentDistro)),
46+
resourceDefinitions,
47+
);
4448

4549
const importResults = await Promise.all(typeIdsToImport.map(async (typeId) => {
4650
try {

‎src/plugins/plugin-manager.ts‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
ApplyNoteRequestData,
3+
FindInstancesResponseData,
34
ImportResponseData, ResourceDefinition,
45
ResourceJson,
56
ValidateResponseData,
@@ -120,6 +121,24 @@ export class PluginManager {
120121
return plugin.import(config, autoImportAll);
121122
}
122123

124+
/**
125+
* Find every instance of a resource that currently exists on the system. This is discovery only (ex: used by destroy
126+
* to let the user pick which instance to remove) and is separate from import.
127+
*/
128+
async findInstances(type: string): Promise<FindInstancesResponseData> {
129+
const pluginName = this.resourceToPluginMapping.get(type);
130+
if (!pluginName) {
131+
throw new Error(`Unable to find plugin for resource: ${type}`);
132+
}
133+
134+
const plugin = this.plugins.get(pluginName)
135+
if (!plugin) {
136+
throw new Error(`Unable to find plugin for resource ${type}`);
137+
}
138+
139+
return plugin.findInstances(type);
140+
}
141+
123142
async plan(project: Project): Promise<Plan> {
124143
const result = new Array<ResourcePlan>();
125144
await Promise.all(

‎src/plugins/plugin.ts‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import {
22
ErrorResponseDataSchema,
3+
FindInstancesRequestData,
4+
FindInstancesResponseData,
5+
FindInstancesResponseDataSchema,
36
GetResourceInfoResponseData,
47
GetResourceInfoResponseDataSchema,
58
ImportRequestData,
@@ -30,6 +33,7 @@ const validateResponseValidator = ajv.compile(ValidateResponseDataSchema);
3033
const getResourceInfoResponseValidator = ajv.compile(GetResourceInfoResponseDataSchema);
3134
const matchResponseValidator = ajv.compile(MatchResponseDataSchema);
3235
const importResponseValidator = ajv.compile(ImportResponseDataSchema);
36+
const findInstancesResponseValidator = ajv.compile(FindInstancesResponseDataSchema);
3337
const planResponseValidator = ajv.compile(PlanResponseDataSchema);
3438

3539
export interface IPlugin {
@@ -38,6 +42,7 @@ export interface IPlugin {
3842
getResourceInfo(type: string): Promise<GetResourceInfoResponseData>;
3943
match(resource: ResourceConfig, array: ResourceConfig[]): Promise<MatchResponseData>;
4044
import(config: ResourceJson): Promise<ImportResponseData>;
45+
findInstances(type: string): Promise<FindInstancesResponseData>;
4146
plan(request: PlanRequestData): Promise<ResourcePlan>;
4247
apply(plan: ResourcePlan): Promise<void>;
4348
kill(): void;
@@ -128,6 +133,20 @@ export class Plugin implements IPlugin {
128133
return result.data;
129134
}
130135

136+
async findInstances(type: string): Promise<FindInstancesResponseData> {
137+
const result = await this.process!.sendMessageForResult('findInstances', <FindInstancesRequestData>{ core: { type } });
138+
139+
if (!result.isSuccessful()) {
140+
throw new PluginError(this.name, type, this.toErrorData(result.data));
141+
}
142+
143+
if (!this.validateFindInstancesResponse(result.data)) {
144+
throw new Error(`Plugin error: Invalid find instances response from plugin: ${this.name}`);
145+
}
146+
147+
return result.data;
148+
}
149+
131150
async plan(request: PlanRequestData): Promise<ResourcePlan> {
132151
const result = await this.process!.sendMessageForResult('plan', request);
133152

@@ -189,6 +208,10 @@ export class Plugin implements IPlugin {
189208
return importResponseValidator(response)
190209
}
191210

211+
private validateFindInstancesResponse(response: unknown): response is FindInstancesResponseData {
212+
return findInstancesResponseValidator(response)
213+
}
214+
192215
private validatePlanResponse(response: unknown): response is PlanResponseData {
193216
return planResponseValidator(response);
194217
}

‎src/utils/superseded-types.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { ResourceDefinitionMap } from '../plugins/plugin-manager.js';
2+
3+
/**
4+
* Remove the types that are superseded by another type in the same list (ex: `alias` is superseded by `aliases`).
5+
* Both resources manage the same system state, so importing both automatically (init / import all) would import the
6+
* same state twice. A superseded type is kept if the type that supersedes it isn't in the list.
7+
*/
8+
export function removeSupersededTypes(typeIds: string[], resourceDefinitions: ResourceDefinitionMap): string[] {
9+
const typeIdSet = new Set(typeIds);
10+
11+
return typeIds.filter((typeId) => {
12+
const supersededBy = resourceDefinitions.get(typeId)?.supersededBy;
13+
return !supersededBy || !typeIdSet.has(supersededBy);
14+
});
15+
}

0 commit comments

Comments
 (0)