|
| 1 | +import { |
| 2 | + CreatePlan, |
| 3 | + DestroyPlan, |
| 4 | + ModifyPlan, |
| 5 | + ParameterChange, |
| 6 | + RefreshContext, |
| 7 | + Resource, |
| 8 | + ResourceSettings, |
| 9 | + SpawnStatus, |
| 10 | + getPty, |
| 11 | + z |
| 12 | +} from 'codify-plugin-lib'; |
| 13 | +import { OS } from 'codify-schemas'; |
| 14 | +import fs from 'node:fs/promises'; |
| 15 | + |
| 16 | +import { FileUtils } from '../../../utils/file-utils.js'; |
| 17 | +import { Utils } from '../../../utils/index.js'; |
| 18 | + |
| 19 | +const ALIAS_REGEX = /^'?([^=]+?)'?='?(.*?)'?$/ |
| 20 | + |
| 21 | +export const schema = z.object({ |
| 22 | + aliases: z |
| 23 | + .array(z.object({ |
| 24 | + alias: z.string(), |
| 25 | + value: z.string(), |
| 26 | + })) |
| 27 | + .describe('Aliases to create') |
| 28 | + .optional(), |
| 29 | +}) |
| 30 | + |
| 31 | +export type AliasesConfig = z.infer<typeof schema>; |
| 32 | +export class AliasesResource extends Resource<AliasesConfig> { |
| 33 | + getSettings(): ResourceSettings<AliasesConfig> { |
| 34 | + return { |
| 35 | + id: 'aliases', |
| 36 | + operatingSystems: [OS.Darwin, OS.Linux], |
| 37 | + schema, |
| 38 | + parameterSettings: { |
| 39 | + aliases: { |
| 40 | + type: 'array', |
| 41 | + itemType: 'object', |
| 42 | + isElementEqual: (a, b) => a.alias === b.alias && a.value === b.value, |
| 43 | + filterInStatelessMode: (desired, current) => |
| 44 | + current.filter((c) => desired.some((d) => d.alias === c.alias)), |
| 45 | + canModify: true, |
| 46 | + } |
| 47 | + }, |
| 48 | + importAndDestroy: { |
| 49 | + requiredParameters: ['aliases'], |
| 50 | + }, |
| 51 | + allowMultiple: { |
| 52 | + identifyingParameters: ['alias'], |
| 53 | + }, |
| 54 | + } |
| 55 | + } |
| 56 | + |
| 57 | + override async refresh(parameters: any, context: RefreshContext<AliasesConfig>): Promise<Partial<AliasesConfig> | null> { |
| 58 | + const $ = getPty(); |
| 59 | + |
| 60 | + const { data, status } = await $.spawnSafe('alias', { interactive: true }); |
| 61 | + |
| 62 | + console.log('Data', data); |
| 63 | + |
| 64 | + if (status === SpawnStatus.ERROR) { |
| 65 | + return null; |
| 66 | + } |
| 67 | + |
| 68 | + const aliases = data.split(/\n/g) |
| 69 | + .map((l) => l.trim()) |
| 70 | + .map((l) => l.match(ALIAS_REGEX)) |
| 71 | + .filter(Boolean) |
| 72 | + .map((m) => (m ? { alias: m[1], value: m[2] } : null)) |
| 73 | + .filter(Boolean) as Array<{ alias: string; value: string }>; |
| 74 | + |
| 75 | + console.log('Command type', context.commandType); |
| 76 | + console.log('Aliases', aliases); |
| 77 | + |
| 78 | + // If validation plan and no aliases match, return null |
| 79 | + if (context.commandType === 'validationPlan' |
| 80 | + && aliases.filter((a) => |
| 81 | + context.originalDesiredConfig?.aliases?.some((d) => d.alias === a.alias)).length === 0 |
| 82 | + ) { |
| 83 | + return null; |
| 84 | + } |
| 85 | + |
| 86 | + if (!aliases || aliases.length === 0) { |
| 87 | + return null; |
| 88 | + } |
| 89 | + |
| 90 | + return { |
| 91 | + aliases, |
| 92 | + } |
| 93 | + } |
| 94 | + |
| 95 | + override async create(plan: CreatePlan<AliasesConfig>): Promise<void> { |
| 96 | + const shellRcPath = Utils.getPrimaryShellRc(); |
| 97 | + |
| 98 | + if (!(await FileUtils.fileExists(shellRcPath))) { |
| 99 | + await fs.writeFile(shellRcPath, '', { encoding: 'utf8' }); |
| 100 | + } |
| 101 | + |
| 102 | + await this.addAliases(plan.desiredConfig.aliases ?? []); |
| 103 | + } |
| 104 | + |
| 105 | + async modify(pc: ParameterChange<AliasesConfig>, plan: ModifyPlan<AliasesConfig>): Promise<void> { |
| 106 | + const shellRcPath = Utils.getPrimaryShellRc(); |
| 107 | + |
| 108 | + if (!(await FileUtils.fileExists(shellRcPath))) { |
| 109 | + await fs.writeFile(shellRcPath, '', { encoding: 'utf8' }); |
| 110 | + } |
| 111 | + |
| 112 | + const { isStateful } = plan; |
| 113 | + if (isStateful) { |
| 114 | + const aliasesToRemove = pc.previousValue |
| 115 | + ?.filter((a) => !pc.newValue?.some((c) => c.alias === a.alias) |
| 116 | + || pc.newValue?.some((c) => c.alias === a.alias && c.value !== a.value) |
| 117 | + ); |
| 118 | + const aliasesToAdd = pc.newValue |
| 119 | + ?.filter((a) => !pc.previousValue?.some((c) => c.alias === a.alias)); |
| 120 | + |
| 121 | + await this.removeAliases(aliasesToRemove); |
| 122 | + await this.addAliases(aliasesToAdd); |
| 123 | + } else { |
| 124 | + const aliasesToRemove = pc.previousValue |
| 125 | + ?.filter((a) => pc.newValue?.some((c) => c.alias === a.alias && c.value !== a.value)); |
| 126 | + |
| 127 | + const aliasesToAdd = pc.newValue |
| 128 | + ?.filter((a) => !pc.previousValue?.some((c) => c.alias === a.alias) |
| 129 | + || pc.previousValue?.some((c) => c.alias === a.alias && c.value !== a.value)); |
| 130 | + |
| 131 | + console.log('Aliases to add', aliasesToAdd); |
| 132 | + |
| 133 | + await this.removeAliases(aliasesToRemove); |
| 134 | + await this.addAliases(aliasesToAdd); |
| 135 | + } |
| 136 | + } |
| 137 | + |
| 138 | + async destroy(plan: DestroyPlan<AliasesConfig>): Promise<void> { |
| 139 | + console.log(plan.currentConfig.aliases); |
| 140 | + await this.removeAliases(plan.currentConfig.aliases ?? []); |
| 141 | + } |
| 142 | + |
| 143 | + private async findAlias(alias: string, value: string): Promise<{ path: string; contents: string; } | null> { |
| 144 | + const paths = Utils.getShellRcFiles(); |
| 145 | + |
| 146 | + const aliasString = this.aliasString(alias, value); |
| 147 | + const aliasStringShort = this.aliasStringShort(alias, value); |
| 148 | + |
| 149 | + for (const path of paths) { |
| 150 | + if (await FileUtils.fileExists(path)) { |
| 151 | + const fileContents = await fs.readFile(path, 'utf8'); |
| 152 | + |
| 153 | + if (fileContents.includes(aliasString) || fileContents.includes(aliasStringShort)) { |
| 154 | + return { |
| 155 | + path, |
| 156 | + contents: fileContents, |
| 157 | + } |
| 158 | + } |
| 159 | + } |
| 160 | + } |
| 161 | + |
| 162 | + return null; |
| 163 | + } |
| 164 | + |
| 165 | + private aliasString(alias: string, value: string): string { |
| 166 | + return `alias ${alias}='${value}'` |
| 167 | + } |
| 168 | + |
| 169 | + private aliasStringShort(alias: string, value: string): string { |
| 170 | + return `alias ${alias}=${value}` |
| 171 | + } |
| 172 | + |
| 173 | + private async removeAliases(aliasesToRemove: Array<{ alias: string; value: string }>): Promise<void> { |
| 174 | + for (const { alias, value } of aliasesToRemove ?? []) { |
| 175 | + const aliasInfo = await this.findAlias(alias, value); |
| 176 | + if (!aliasInfo) { |
| 177 | + console.warn(`Unable to find alias: ${alias} on the system. Codify isn't able to search all locations on the system. Please delete the alias manually and re-run Codify.`); |
| 178 | + continue; |
| 179 | + } |
| 180 | + |
| 181 | + const aliasString = this.aliasString(alias, value); |
| 182 | + const aliasStringShort = this.aliasStringShort(alias, value); |
| 183 | + |
| 184 | + await FileUtils.removeLineFromFile(aliasInfo.path, aliasString); |
| 185 | + await FileUtils.removeLineFromFile(aliasInfo.path, aliasStringShort); |
| 186 | + } |
| 187 | + } |
| 188 | + |
| 189 | + private async addAliases(aliasesToAdd: Array<{ alias: string; value: string }>): Promise<void> { |
| 190 | + for (const { alias, value } of aliasesToAdd ?? []) { |
| 191 | + const aliasString = this.aliasString(alias, value); |
| 192 | + await FileUtils.addToStartupFile(aliasString); |
| 193 | + } |
| 194 | + } |
| 195 | +} |
| 196 | + |
| 197 | + |
0 commit comments