codify-plugin-lib 1.0.182-beta7 → 1.0.182-beta71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/build.js +189 -0
- package/dist/bin/build.js +0 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/messages/handlers.js +10 -2
- package/dist/plan/plan.js +45 -3
- package/dist/plugin/plugin.d.ts +2 -1
- package/dist/plugin/plugin.js +6 -1
- package/dist/pty/background-pty.d.ts +3 -2
- package/dist/pty/background-pty.js +7 -14
- package/dist/pty/index.d.ts +8 -2
- package/dist/pty/seqeuntial-pty.d.ts +3 -2
- package/dist/pty/seqeuntial-pty.js +47 -12
- package/dist/resource/parsed-resource-settings.d.ts +5 -2
- package/dist/resource/parsed-resource-settings.js +16 -2
- package/dist/resource/resource-controller.js +5 -5
- package/dist/resource/resource-settings.d.ts +13 -3
- package/dist/resource/resource-settings.js +2 -2
- package/dist/test.d.ts +1 -0
- package/dist/test.js +5 -0
- package/dist/utils/file-utils.d.ts +14 -7
- package/dist/utils/file-utils.js +65 -51
- package/dist/utils/functions.js +2 -2
- package/dist/utils/index.d.ts +21 -1
- package/dist/utils/index.js +160 -0
- package/dist/utils/load-resources.d.ts +1 -0
- package/dist/utils/load-resources.js +46 -0
- package/dist/utils/package-json-utils.d.ts +12 -0
- package/dist/utils/package-json-utils.js +34 -0
- package/package.json +5 -4
- package/rollup.config.js +24 -0
- package/src/index.ts +3 -0
- package/src/messages/handlers.test.ts +23 -0
- package/src/messages/handlers.ts +11 -2
- package/src/plan/plan.test.ts +46 -0
- package/src/plan/plan.ts +65 -4
- package/src/plugin/plugin.test.ts +31 -0
- package/src/plugin/plugin.ts +8 -2
- package/src/pty/background-pty.ts +10 -18
- package/src/pty/index.ts +10 -4
- package/src/pty/seqeuntial-pty.ts +62 -16
- package/src/pty/sequential-pty.test.ts +137 -4
- package/src/resource/parsed-resource-settings.test.ts +24 -0
- package/src/resource/parsed-resource-settings.ts +26 -8
- package/src/resource/resource-controller.test.ts +126 -0
- package/src/resource/resource-controller.ts +5 -6
- package/src/resource/resource-settings.test.ts +36 -0
- package/src/resource/resource-settings.ts +17 -5
- package/src/utils/file-utils.test.ts +7 -0
- package/src/utils/file-utils.ts +70 -55
- package/src/utils/functions.ts +3 -3
- package/src/utils/index.ts +197 -1
- package/src/utils/internal-utils.test.ts +1 -0
|
@@ -1,20 +1,34 @@
|
|
|
1
|
+
import { ZodObject, z } from 'zod';
|
|
1
2
|
import { StatefulParameterController } from '../stateful-parameter/stateful-parameter-controller.js';
|
|
2
3
|
import { resolveElementEqualsFn, resolveEqualsFn, resolveMatcher, resolveParameterTransformFn } from './resource-settings.js';
|
|
3
4
|
export class ParsedResourceSettings {
|
|
4
5
|
cache = new Map();
|
|
5
6
|
id;
|
|
7
|
+
description;
|
|
6
8
|
schema;
|
|
7
9
|
allowMultiple;
|
|
8
10
|
removeStatefulParametersBeforeDestroy;
|
|
9
11
|
dependencies;
|
|
10
12
|
transformation;
|
|
11
13
|
operatingSystems;
|
|
14
|
+
linuxDistros;
|
|
12
15
|
isSensitive;
|
|
13
16
|
settings;
|
|
14
17
|
constructor(settings) {
|
|
15
18
|
this.settings = settings;
|
|
16
|
-
const { parameterSettings, ...rest } = settings;
|
|
19
|
+
const { parameterSettings, schema, ...rest } = settings;
|
|
17
20
|
Object.assign(this, rest);
|
|
21
|
+
if (schema) {
|
|
22
|
+
this.schema = schema instanceof ZodObject
|
|
23
|
+
? z.toJSONSchema(schema.strict(), {
|
|
24
|
+
target: 'draft-7',
|
|
25
|
+
override(ctx) {
|
|
26
|
+
ctx.jsonSchema.title = settings.id;
|
|
27
|
+
ctx.jsonSchema.description = settings.description ?? `${settings.id} resource. Can be used to manage ${settings.id}`;
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
: schema;
|
|
31
|
+
}
|
|
18
32
|
this.validateSettings();
|
|
19
33
|
}
|
|
20
34
|
get typeId() {
|
|
@@ -118,7 +132,7 @@ export class ParsedResourceSettings {
|
|
|
118
132
|
&& typeof this.settings.allowMultiple === 'object' && this.settings.allowMultiple?.identifyingParameters?.includes(k))) {
|
|
119
133
|
throw new Error(`Resource: ${this.id}. Stateful parameters are not allowed to be identifying parameters for allowMultiple.`);
|
|
120
134
|
}
|
|
121
|
-
const schema = this.
|
|
135
|
+
const schema = this.schema;
|
|
122
136
|
if (!this.settings.importAndDestroy && (schema?.oneOf
|
|
123
137
|
&& Array.isArray(schema.oneOf)
|
|
124
138
|
&& schema.oneOf.some((s) => s.required))
|
|
@@ -17,16 +17,16 @@ export class ResourceController {
|
|
|
17
17
|
this.settings = resource.getSettings();
|
|
18
18
|
this.typeId = this.settings.id;
|
|
19
19
|
this.dependencies = this.settings.dependencies ?? [];
|
|
20
|
-
|
|
20
|
+
this.parsedSettings = new ParsedResourceSettings(this.settings);
|
|
21
|
+
if (this.parsedSettings.schema) {
|
|
21
22
|
this.ajv = new Ajv({
|
|
22
23
|
allErrors: true,
|
|
23
24
|
strict: true,
|
|
24
25
|
strictRequired: false,
|
|
25
26
|
allowUnionTypes: true
|
|
26
27
|
});
|
|
27
|
-
this.schemaValidator = this.ajv.compile(this.
|
|
28
|
+
this.schemaValidator = this.ajv.compile(this.parsedSettings.schema);
|
|
28
29
|
}
|
|
29
|
-
this.parsedSettings = new ParsedResourceSettings(this.settings);
|
|
30
30
|
}
|
|
31
31
|
async initialize() {
|
|
32
32
|
return this.resource.initialize();
|
|
@@ -374,8 +374,8 @@ ${JSON.stringify(refresh, null, 2)}
|
|
|
374
374
|
.sort((a, b) => this.parsedSettings.statefulParameterOrder.get(a.name) - this.parsedSettings.statefulParameterOrder.get(b.name));
|
|
375
375
|
}
|
|
376
376
|
getAllParameterKeys() {
|
|
377
|
-
return this.
|
|
378
|
-
? Object.keys(this.
|
|
377
|
+
return this.parsedSettings.schema
|
|
378
|
+
? Object.keys(this.parsedSettings.schema?.properties)
|
|
379
379
|
: Object.keys(this.parsedSettings.parameterSettings);
|
|
380
380
|
}
|
|
381
381
|
getParametersToRefreshForImport(parameters, context) {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { JSONSchemaType } from 'ajv';
|
|
2
|
-
import { OS, StringIndexedObject } from 'codify-schemas';
|
|
2
|
+
import { LinuxDistro, OS, StringIndexedObject } from 'codify-schemas';
|
|
3
|
+
import { ZodObject } from 'zod';
|
|
3
4
|
import { ArrayStatefulParameter, StatefulParameter } from '../stateful-parameter/stateful-parameter.js';
|
|
5
|
+
import { ParsedResourceSettings } from './parsed-resource-settings.js';
|
|
4
6
|
import { RefreshContext } from './resource.js';
|
|
5
7
|
export interface InputTransformation {
|
|
6
8
|
to: (input: any) => Promise<any> | any;
|
|
@@ -18,15 +20,23 @@ export interface ResourceSettings<T extends StringIndexedObject> {
|
|
|
18
20
|
* List of supported operating systems
|
|
19
21
|
*/
|
|
20
22
|
operatingSystems: Array<OS>;
|
|
23
|
+
/**
|
|
24
|
+
* List of supported linux distros
|
|
25
|
+
*/
|
|
26
|
+
linuxDistros?: Array<LinuxDistro>;
|
|
21
27
|
/**
|
|
22
28
|
* Schema to validate user configs with. Must be in the format JSON Schema draft07
|
|
23
29
|
*/
|
|
24
|
-
schema?: Partial<JSONSchemaType<T | any
|
|
30
|
+
schema?: Partial<JSONSchemaType<T | any>> | ZodObject;
|
|
25
31
|
/**
|
|
26
32
|
* Mark the resource as sensitive. Defaults to false. This prevents the resource from automatically being imported by init and import.
|
|
27
33
|
* This differs from the parameter level sensitivity which also prevents the parameter value from being displayed in the plan.
|
|
28
34
|
*/
|
|
29
35
|
isSensitive?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* An optional description of the resource. This does not affect the behavior of the resource.
|
|
38
|
+
*/
|
|
39
|
+
description?: string;
|
|
30
40
|
/**
|
|
31
41
|
* Allow multiple of the same resource to unique. Set truthy if
|
|
32
42
|
* multiples are allowed, for example for applications, there can be multiple copy of the same application installed
|
|
@@ -290,4 +300,4 @@ export declare function resolveEqualsFn(parameter: ParameterSetting): (desired:
|
|
|
290
300
|
export declare function resolveElementEqualsFn(parameter: ArrayParameterSetting): (desired: unknown, current: unknown) => boolean;
|
|
291
301
|
export declare function resolveFnFromEqualsFnOrString(fnOrString: ((a: unknown, b: unknown) => boolean) | ParameterSettingType | undefined): ((a: unknown, b: unknown) => boolean) | undefined;
|
|
292
302
|
export declare function resolveParameterTransformFn(parameter: ParameterSetting): InputTransformation | undefined;
|
|
293
|
-
export declare function resolveMatcher<T extends StringIndexedObject>(settings:
|
|
303
|
+
export declare function resolveMatcher<T extends StringIndexedObject>(settings: ParsedResourceSettings<T>): (desired: Partial<T>, current: Partial<T>) => boolean;
|
|
@@ -3,7 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { addVariablesToPath, areArraysEqual, resolvePathWithVariables, tildify, untildify } from '../utils/functions.js';
|
|
4
4
|
const ParameterEqualsDefaults = {
|
|
5
5
|
'boolean': (a, b) => Boolean(a) === Boolean(b),
|
|
6
|
-
'directory'
|
|
6
|
+
'directory'(a, b) {
|
|
7
7
|
let transformedA = resolvePathWithVariables(untildify(String(a)));
|
|
8
8
|
let transformedB = resolvePathWithVariables(untildify(String(b)));
|
|
9
9
|
if (transformedA.startsWith('.')) { // Only relative paths start with '.'
|
|
@@ -65,7 +65,7 @@ export function resolveFnFromEqualsFnOrString(fnOrString) {
|
|
|
65
65
|
const ParameterTransformationDefaults = {
|
|
66
66
|
'directory': {
|
|
67
67
|
to: (a) => resolvePathWithVariables((untildify(String(a)))),
|
|
68
|
-
from
|
|
68
|
+
from(a, original) {
|
|
69
69
|
if (ParameterEqualsDefaults.directory(a, original)) {
|
|
70
70
|
return original;
|
|
71
71
|
}
|
package/dist/test.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/test.js
ADDED
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
export declare class FileUtils {
|
|
2
2
|
static downloadFile(url: string, destination: string): Promise<void>;
|
|
3
|
-
static
|
|
4
|
-
static
|
|
5
|
-
|
|
3
|
+
static addToShellRc(line: string): Promise<void>;
|
|
4
|
+
static addAllToShellRc(lines: string[]): Promise<void>;
|
|
5
|
+
/**
|
|
6
|
+
* This method adds a directory path to the shell rc file if it doesn't already exist.
|
|
7
|
+
*
|
|
8
|
+
* @param value - The directory path to add.
|
|
9
|
+
* @param prepend - Whether to prepend the path to the existing PATH variable.
|
|
10
|
+
*/
|
|
11
|
+
static addPathToShellRc(value: string, prepend: boolean): Promise<void>;
|
|
12
|
+
static removeFromFile(filePath: string, search: string): Promise<void>;
|
|
13
|
+
static removeLineFromFile(filePath: string, search: RegExp | string): Promise<void>;
|
|
14
|
+
static removeLineFromShellRc(search: RegExp | string): Promise<void>;
|
|
15
|
+
static removeAllLinesFromShellRc(searches: Array<RegExp | string>): Promise<void>;
|
|
16
|
+
static appendToFileWithSpacing(file: string, textToInsert: string): string;
|
|
6
17
|
static dirExists(path: string): Promise<boolean>;
|
|
7
18
|
static fileExists(path: string): Promise<boolean>;
|
|
8
19
|
static exists(path: string): Promise<boolean>;
|
|
9
20
|
static checkDirExistsOrThrowIfFile(path: string): Promise<boolean>;
|
|
10
21
|
static createDirIfNotExists(path: string): Promise<void>;
|
|
11
|
-
static removeFromFile(filePath: string, search: string): Promise<void>;
|
|
12
|
-
static removeLineFromFile(filePath: string, search: RegExp | string): Promise<void>;
|
|
13
|
-
static removeLineFromPrimaryShellRc(search: RegExp | string): Promise<void>;
|
|
14
|
-
static appendToFileWithSpacing(file: string, textToInsert: string): string;
|
|
15
22
|
private static calculateEndingNewLines;
|
|
16
23
|
}
|
package/dist/utils/file-utils.js
CHANGED
|
@@ -18,7 +18,7 @@ export class FileUtils {
|
|
|
18
18
|
await finished(Readable.fromWeb(body).pipe(ws));
|
|
19
19
|
console.log(`Finished downloading to ${destination}`);
|
|
20
20
|
}
|
|
21
|
-
static async
|
|
21
|
+
static async addToShellRc(line) {
|
|
22
22
|
const lineToInsert = addLeadingSpacer(addTrailingSpacer(line));
|
|
23
23
|
await fs.appendFile(Utils.getPrimaryShellRc(), lineToInsert);
|
|
24
24
|
function addLeadingSpacer(line) {
|
|
@@ -32,14 +32,23 @@ export class FileUtils {
|
|
|
32
32
|
: line + '\n';
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
-
static async
|
|
35
|
+
static async addAllToShellRc(lines) {
|
|
36
36
|
const formattedLines = '\n' + lines.join('\n') + '\n';
|
|
37
37
|
const shellRc = Utils.getPrimaryShellRc();
|
|
38
38
|
console.log(`Adding to ${path.basename(shellRc)}:
|
|
39
39
|
${lines.join('\n')}`);
|
|
40
40
|
await fs.appendFile(shellRc, formattedLines);
|
|
41
41
|
}
|
|
42
|
-
|
|
42
|
+
/**
|
|
43
|
+
* This method adds a directory path to the shell rc file if it doesn't already exist.
|
|
44
|
+
*
|
|
45
|
+
* @param value - The directory path to add.
|
|
46
|
+
* @param prepend - Whether to prepend the path to the existing PATH variable.
|
|
47
|
+
*/
|
|
48
|
+
static async addPathToShellRc(value, prepend) {
|
|
49
|
+
if (await Utils.isDirectoryOnPath(value)) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
43
52
|
const shellRc = Utils.getPrimaryShellRc();
|
|
44
53
|
console.log(`Saving path: ${value} to ${shellRc}`);
|
|
45
54
|
if (prepend) {
|
|
@@ -48,53 +57,6 @@ ${lines.join('\n')}`);
|
|
|
48
57
|
}
|
|
49
58
|
await fs.appendFile(shellRc, `\nexport PATH=${value}:$PATH;`, { encoding: 'utf8' });
|
|
50
59
|
}
|
|
51
|
-
static async dirExists(path) {
|
|
52
|
-
let stat;
|
|
53
|
-
try {
|
|
54
|
-
stat = await fs.stat(path);
|
|
55
|
-
return stat.isDirectory();
|
|
56
|
-
}
|
|
57
|
-
catch {
|
|
58
|
-
return false;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
static async fileExists(path) {
|
|
62
|
-
let stat;
|
|
63
|
-
try {
|
|
64
|
-
stat = await fs.stat(path);
|
|
65
|
-
return stat.isFile();
|
|
66
|
-
}
|
|
67
|
-
catch {
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
static async exists(path) {
|
|
72
|
-
try {
|
|
73
|
-
await fs.stat(path);
|
|
74
|
-
return true;
|
|
75
|
-
}
|
|
76
|
-
catch {
|
|
77
|
-
return false;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
static async checkDirExistsOrThrowIfFile(path) {
|
|
81
|
-
let stat;
|
|
82
|
-
try {
|
|
83
|
-
stat = await fs.stat(path);
|
|
84
|
-
}
|
|
85
|
-
catch {
|
|
86
|
-
return false;
|
|
87
|
-
}
|
|
88
|
-
if (stat.isDirectory()) {
|
|
89
|
-
return true;
|
|
90
|
-
}
|
|
91
|
-
throw new Error(`Directory ${path} already exists and is a file`);
|
|
92
|
-
}
|
|
93
|
-
static async createDirIfNotExists(path) {
|
|
94
|
-
if (!fsSync.existsSync(path)) {
|
|
95
|
-
await fs.mkdir(path, { recursive: true });
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
60
|
static async removeFromFile(filePath, search) {
|
|
99
61
|
const contents = await fs.readFile(filePath, 'utf8');
|
|
100
62
|
const newContents = contents.replaceAll(search, '');
|
|
@@ -131,9 +93,14 @@ ${lines.join('\n')}`);
|
|
|
131
93
|
await fs.writeFile(filePath, lines.join('\n'));
|
|
132
94
|
console.log(`Removed line: ${search} from ${filePath}`);
|
|
133
95
|
}
|
|
134
|
-
static async
|
|
96
|
+
static async removeLineFromShellRc(search) {
|
|
135
97
|
return FileUtils.removeLineFromFile(Utils.getPrimaryShellRc(), search);
|
|
136
98
|
}
|
|
99
|
+
static async removeAllLinesFromShellRc(searches) {
|
|
100
|
+
for (const search of searches) {
|
|
101
|
+
await FileUtils.removeLineFromFile(Utils.getPrimaryShellRc(), search);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
137
104
|
// Append the string to the end of a file ensuring at least 1 lines of space between.
|
|
138
105
|
// Ex result:
|
|
139
106
|
// something something;
|
|
@@ -150,6 +117,53 @@ ${lines.join('\n')}`);
|
|
|
150
117
|
: Math.max(0, 2 - endingNewLines);
|
|
151
118
|
return lines.join('\n') + '\n'.repeat(numNewLines) + textToInsert;
|
|
152
119
|
}
|
|
120
|
+
static async dirExists(path) {
|
|
121
|
+
let stat;
|
|
122
|
+
try {
|
|
123
|
+
stat = await fs.stat(path);
|
|
124
|
+
return stat.isDirectory();
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
static async fileExists(path) {
|
|
131
|
+
let stat;
|
|
132
|
+
try {
|
|
133
|
+
stat = await fs.stat(path);
|
|
134
|
+
return stat.isFile();
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
static async exists(path) {
|
|
141
|
+
try {
|
|
142
|
+
await fs.stat(path);
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
static async checkDirExistsOrThrowIfFile(path) {
|
|
150
|
+
let stat;
|
|
151
|
+
try {
|
|
152
|
+
stat = await fs.stat(path);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
if (stat.isDirectory()) {
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
throw new Error(`Directory ${path} already exists and is a file`);
|
|
161
|
+
}
|
|
162
|
+
static async createDirIfNotExists(path) {
|
|
163
|
+
if (!fsSync.existsSync(path)) {
|
|
164
|
+
await fs.mkdir(path, { recursive: true });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
153
167
|
// This is overly complicated but it can be used to insert into any
|
|
154
168
|
// position in the future
|
|
155
169
|
static calculateEndingNewLines(lines) {
|
package/dist/utils/functions.js
CHANGED
|
@@ -5,9 +5,10 @@ export function splitUserConfig(config) {
|
|
|
5
5
|
type: config.type,
|
|
6
6
|
...(config.name ? { name: config.name } : {}),
|
|
7
7
|
...(config.dependsOn ? { dependsOn: config.dependsOn } : {}),
|
|
8
|
+
...(config.os ? { os: config.os } : {}),
|
|
8
9
|
};
|
|
9
10
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
10
|
-
const { type, name, dependsOn, ...parameters } = config;
|
|
11
|
+
const { type, name, dependsOn, os, ...parameters } = config;
|
|
11
12
|
return {
|
|
12
13
|
parameters: parameters,
|
|
13
14
|
coreParameters,
|
|
@@ -24,7 +25,6 @@ export function tildify(pathWithTilde) {
|
|
|
24
25
|
return homeDirectory ? pathWithTilde.replace(homeDirectory, '~') : pathWithTilde;
|
|
25
26
|
}
|
|
26
27
|
export function resolvePathWithVariables(pathWithVariables) {
|
|
27
|
-
// @ts-expect-error Ignore this for now
|
|
28
28
|
return pathWithVariables.replace(/\$([A-Z_]+[A-Z0-9_]*)|\${([A-Z0-9_]*)}/ig, (_, a, b) => process.env[a || b]);
|
|
29
29
|
}
|
|
30
30
|
export function addVariablesToPath(pathWithoutVariables) {
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { OS } from 'codify-schemas';
|
|
1
|
+
import { LinuxDistro, OS } from 'codify-schemas';
|
|
2
2
|
export declare function isDebug(): boolean;
|
|
3
3
|
export declare enum Shell {
|
|
4
4
|
ZSH = "zsh",
|
|
@@ -20,7 +20,27 @@ export declare const Utils: {
|
|
|
20
20
|
};
|
|
21
21
|
isMacOS(): boolean;
|
|
22
22
|
isLinux(): boolean;
|
|
23
|
+
isArmArch(): Promise<boolean>;
|
|
24
|
+
isHomebrewInstalled(): Promise<boolean>;
|
|
25
|
+
isRosetta2Installed(): Promise<boolean>;
|
|
23
26
|
getShell(): Shell | undefined;
|
|
24
27
|
getPrimaryShellRc(): string;
|
|
25
28
|
getShellRcFiles(): string[];
|
|
29
|
+
isDirectoryOnPath(directory: string): Promise<boolean>;
|
|
30
|
+
assertBrewInstalled(): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Installs a package via the system package manager. This will use Homebrew on macOS and apt on Ubuntu/Debian or dnf on Fedora.
|
|
33
|
+
* @param packageName
|
|
34
|
+
*/
|
|
35
|
+
installViaPkgMgr(packageName: string): Promise<void>;
|
|
36
|
+
uninstallViaPkgMgr(packageName: string): Promise<boolean>;
|
|
37
|
+
getLinuxDistro(): Promise<LinuxDistro | undefined>;
|
|
38
|
+
isUbuntu(): Promise<boolean>;
|
|
39
|
+
isDebian(): Promise<boolean>;
|
|
40
|
+
isArch(): Promise<boolean>;
|
|
41
|
+
isCentOS(): Promise<boolean>;
|
|
42
|
+
isFedora(): Promise<boolean>;
|
|
43
|
+
isRHEL(): Promise<boolean>;
|
|
44
|
+
isDebianBased(): boolean;
|
|
45
|
+
isRedhatBased(): boolean;
|
|
26
46
|
};
|
package/dist/utils/index.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import { LinuxDistro } from 'codify-schemas';
|
|
2
|
+
import * as fsSync from 'node:fs';
|
|
3
|
+
import * as fs from 'node:fs/promises';
|
|
1
4
|
import os from 'node:os';
|
|
2
5
|
import path from 'node:path';
|
|
6
|
+
import { SpawnStatus, getPty } from '../pty/index.js';
|
|
3
7
|
export function isDebug() {
|
|
4
8
|
return process.env.DEBUG != null && process.env.DEBUG.includes('codify'); // TODO: replace with debug library
|
|
5
9
|
}
|
|
@@ -28,6 +32,29 @@ export const Utils = {
|
|
|
28
32
|
isLinux() {
|
|
29
33
|
return os.platform() === 'linux';
|
|
30
34
|
},
|
|
35
|
+
async isArmArch() {
|
|
36
|
+
const $ = getPty();
|
|
37
|
+
if (!Utils.isMacOS()) {
|
|
38
|
+
// On Linux, check uname -m
|
|
39
|
+
const query = await $.spawn('uname -m');
|
|
40
|
+
return query.data.trim() === 'aarch64' || query.data.trim() === 'arm64';
|
|
41
|
+
}
|
|
42
|
+
const query = await $.spawn('sysctl -n machdep.cpu.brand_string');
|
|
43
|
+
return /M(\d)/.test(query.data);
|
|
44
|
+
},
|
|
45
|
+
async isHomebrewInstalled() {
|
|
46
|
+
const $ = getPty();
|
|
47
|
+
const query = await $.spawnSafe('which brew', { interactive: true });
|
|
48
|
+
return query.status === SpawnStatus.SUCCESS;
|
|
49
|
+
},
|
|
50
|
+
async isRosetta2Installed() {
|
|
51
|
+
if (!Utils.isMacOS()) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
const $ = getPty();
|
|
55
|
+
const query = await $.spawnSafe('arch -x86_64 /usr/bin/true 2> /dev/null', { interactive: true });
|
|
56
|
+
return query.status === SpawnStatus.SUCCESS;
|
|
57
|
+
},
|
|
31
58
|
getShell() {
|
|
32
59
|
const shell = process.env.SHELL || '';
|
|
33
60
|
if (shell.endsWith('bash')) {
|
|
@@ -108,4 +135,137 @@ export const Utils = {
|
|
|
108
135
|
path.join(homeDir, '.profile'),
|
|
109
136
|
];
|
|
110
137
|
},
|
|
138
|
+
async isDirectoryOnPath(directory) {
|
|
139
|
+
const $ = getPty();
|
|
140
|
+
const { data: pathQuery } = await $.spawn('echo $PATH', { interactive: true });
|
|
141
|
+
const lines = pathQuery.split(':');
|
|
142
|
+
return lines.includes(directory);
|
|
143
|
+
},
|
|
144
|
+
async assertBrewInstalled() {
|
|
145
|
+
const $ = getPty();
|
|
146
|
+
const brewCheck = await $.spawnSafe('which brew', { interactive: true });
|
|
147
|
+
if (brewCheck.status === SpawnStatus.ERROR) {
|
|
148
|
+
throw new Error(`Homebrew is not installed. Cannot install git-lfs without Homebrew installed.
|
|
149
|
+
|
|
150
|
+
Brew can be installed using Codify:
|
|
151
|
+
{
|
|
152
|
+
"type": "homebrew",
|
|
153
|
+
}`);
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
/**
|
|
157
|
+
* Installs a package via the system package manager. This will use Homebrew on macOS and apt on Ubuntu/Debian or dnf on Fedora.
|
|
158
|
+
* @param packageName
|
|
159
|
+
*/
|
|
160
|
+
async installViaPkgMgr(packageName) {
|
|
161
|
+
const $ = getPty();
|
|
162
|
+
if (Utils.isMacOS()) {
|
|
163
|
+
await this.assertBrewInstalled();
|
|
164
|
+
await $.spawn(`brew install ${packageName}`, { interactive: true, env: { HOMEBREW_NO_AUTO_UPDATE: 1 } });
|
|
165
|
+
}
|
|
166
|
+
if (Utils.isLinux()) {
|
|
167
|
+
const isAptInstalled = await $.spawnSafe('which apt');
|
|
168
|
+
if (isAptInstalled.status === SpawnStatus.SUCCESS) {
|
|
169
|
+
await $.spawn('apt-get update', { requiresRoot: true });
|
|
170
|
+
const { status, data } = await $.spawnSafe(`apt-get -y install ${packageName}`, {
|
|
171
|
+
requiresRoot: true,
|
|
172
|
+
env: { DEBIAN_FRONTEND: 'noninteractive' }
|
|
173
|
+
});
|
|
174
|
+
if (status === SpawnStatus.ERROR && data.includes('E: dpkg was interrupted, you must manually run \'sudo dpkg --configure -a\' to correct the problem.')) {
|
|
175
|
+
await $.spawn('dpkg --configure -a', { requiresRoot: true });
|
|
176
|
+
await $.spawn(`apt-get -y install ${packageName}`, {
|
|
177
|
+
requiresRoot: true,
|
|
178
|
+
env: { DEBIAN_FRONTEND: 'noninteractive' }
|
|
179
|
+
});
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (status === SpawnStatus.ERROR) {
|
|
183
|
+
throw new Error(`Failed to install package ${packageName} via apt: ${data}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const isDnfInstalled = await $.spawnSafe('which dnf');
|
|
187
|
+
if (isDnfInstalled.status === SpawnStatus.SUCCESS) {
|
|
188
|
+
await $.spawn('dnf update', { requiresRoot: true });
|
|
189
|
+
await $.spawn(`dnf install ${packageName} -y`, { requiresRoot: true });
|
|
190
|
+
}
|
|
191
|
+
const isYumInstalled = await $.spawnSafe('which yum');
|
|
192
|
+
if (isYumInstalled.status === SpawnStatus.SUCCESS) {
|
|
193
|
+
await $.spawn('yum update', { requiresRoot: true });
|
|
194
|
+
await $.spawn(`yum install ${packageName} -y`, { requiresRoot: true });
|
|
195
|
+
}
|
|
196
|
+
const isPacmanInstalled = await $.spawnSafe('which pacman');
|
|
197
|
+
if (isPacmanInstalled.status === SpawnStatus.SUCCESS) {
|
|
198
|
+
await $.spawn('pacman -Syu', { requiresRoot: true });
|
|
199
|
+
await $.spawn(`pacman -S ${packageName} --noconfirm`, { requiresRoot: true });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
async uninstallViaPkgMgr(packageName) {
|
|
204
|
+
const $ = getPty();
|
|
205
|
+
if (Utils.isMacOS()) {
|
|
206
|
+
await this.assertBrewInstalled();
|
|
207
|
+
const { status } = await $.spawnSafe(`brew uninstall --zap ${packageName}`, {
|
|
208
|
+
interactive: true,
|
|
209
|
+
env: { HOMEBREW_NO_AUTO_UPDATE: 1 }
|
|
210
|
+
});
|
|
211
|
+
return status === SpawnStatus.SUCCESS;
|
|
212
|
+
}
|
|
213
|
+
if (Utils.isLinux()) {
|
|
214
|
+
const isAptInstalled = await $.spawnSafe('which apt');
|
|
215
|
+
if (isAptInstalled.status === SpawnStatus.SUCCESS) {
|
|
216
|
+
const { status } = await $.spawnSafe(`apt-get autoremove -y --purge ${packageName}`, {
|
|
217
|
+
requiresRoot: true,
|
|
218
|
+
env: { DEBIAN_FRONTEND: 'noninteractive' }
|
|
219
|
+
});
|
|
220
|
+
return status === SpawnStatus.SUCCESS;
|
|
221
|
+
}
|
|
222
|
+
const isDnfInstalled = await $.spawnSafe('which dnf');
|
|
223
|
+
if (isDnfInstalled.status === SpawnStatus.SUCCESS) {
|
|
224
|
+
const { status } = await $.spawnSafe(`dnf autoremove ${packageName} -y`, { requiresRoot: true });
|
|
225
|
+
return status === SpawnStatus.SUCCESS;
|
|
226
|
+
}
|
|
227
|
+
const isYumInstalled = await $.spawnSafe('which yum');
|
|
228
|
+
if (isYumInstalled.status === SpawnStatus.SUCCESS) {
|
|
229
|
+
const { status } = await $.spawnSafe(`yum autoremove ${packageName} -y`, { requiresRoot: true });
|
|
230
|
+
return status === SpawnStatus.SUCCESS;
|
|
231
|
+
}
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
return false;
|
|
235
|
+
},
|
|
236
|
+
async getLinuxDistro() {
|
|
237
|
+
const osRelease = await fs.readFile('/etc/os-release', 'utf8');
|
|
238
|
+
const lines = osRelease.split('\n');
|
|
239
|
+
for (const line of lines) {
|
|
240
|
+
if (line.startsWith('ID=')) {
|
|
241
|
+
const distroId = line.slice(3).trim().replaceAll('"', '');
|
|
242
|
+
return Object.values(LinuxDistro).includes(distroId) ? distroId : undefined;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return undefined;
|
|
246
|
+
},
|
|
247
|
+
async isUbuntu() {
|
|
248
|
+
return (await this.getLinuxDistro()) === LinuxDistro.UBUNTU;
|
|
249
|
+
},
|
|
250
|
+
async isDebian() {
|
|
251
|
+
return (await this.getLinuxDistro()) === LinuxDistro.DEBIAN;
|
|
252
|
+
},
|
|
253
|
+
async isArch() {
|
|
254
|
+
return (await this.getLinuxDistro()) === LinuxDistro.ARCH;
|
|
255
|
+
},
|
|
256
|
+
async isCentOS() {
|
|
257
|
+
return (await this.getLinuxDistro()) === LinuxDistro.CENTOS;
|
|
258
|
+
},
|
|
259
|
+
async isFedora() {
|
|
260
|
+
return (await this.getLinuxDistro()) === LinuxDistro.FEDORA;
|
|
261
|
+
},
|
|
262
|
+
async isRHEL() {
|
|
263
|
+
return (await this.getLinuxDistro()) === LinuxDistro.RHEL;
|
|
264
|
+
},
|
|
265
|
+
isDebianBased() {
|
|
266
|
+
return fsSync.existsSync('/etc/debian_version');
|
|
267
|
+
},
|
|
268
|
+
isRedhatBased() {
|
|
269
|
+
return fsSync.existsSync('/etc/redhat-release');
|
|
270
|
+
}
|
|
111
271
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const listAllResources: (root?: string) => Promise<string[]>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import * as url from 'node:url';
|
|
4
|
+
export const listAllResources = async (root = path.join(path.dirname(url.fileURLToPath(import.meta.url)), '..', '..', '..', '..')) => {
|
|
5
|
+
console.log('Dirname', root);
|
|
6
|
+
const resourcesPath = path.join(root, 'src', 'resources');
|
|
7
|
+
const resourceDir = await fs.readdir(resourcesPath);
|
|
8
|
+
const dedupSet = new Set();
|
|
9
|
+
const result = new Set();
|
|
10
|
+
for (const folder of resourceDir) {
|
|
11
|
+
if (await fs.stat(path.join(resourcesPath, folder)).then(s => s.isDirectory()).catch(() => false)) {
|
|
12
|
+
for (const folderContents of await fs.readdir(path.join(resourcesPath, folder))) {
|
|
13
|
+
const isDirectory = await fs.stat(path.join(resourcesPath, folder, folderContents)).then(s => s.isDirectory());
|
|
14
|
+
// console.log(folderContents, isDirectory);
|
|
15
|
+
if (isDirectory) {
|
|
16
|
+
for (const innerContents of await fs.readdir(path.join(resourcesPath, folder, folderContents))) {
|
|
17
|
+
if (!dedupSet.has(path.join(resourcesPath, folder, folderContents))) {
|
|
18
|
+
dedupSet.add(path.join(resourcesPath, folder, folderContents));
|
|
19
|
+
addResourceFromDir(path.join(resourcesPath, folder, folderContents), result);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
if (!dedupSet.has(path.join(resourcesPath, folder))) {
|
|
25
|
+
dedupSet.add(path.join(resourcesPath, folder));
|
|
26
|
+
addResourceFromDir(path.join(resourcesPath, folder), result);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
throw new Error('Only directories are allowed in resources folder');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return [...result];
|
|
36
|
+
};
|
|
37
|
+
function addResourceFromDir(dir, result) {
|
|
38
|
+
try {
|
|
39
|
+
const resourceFile = path.resolve(path.join(dir, 'resource.ts'));
|
|
40
|
+
if (!(fs.stat(resourceFile).then((s) => s.isFile())).catch(() => false)) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
result.add(resourceFile);
|
|
44
|
+
}
|
|
45
|
+
catch { }
|
|
46
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Find the nearest package.json starting from a directory and walking upward.
|
|
3
|
+
* @param {string} startDir - Directory to start searching from
|
|
4
|
+
* @returns {string|null} Absolute path to package.json or null if not found
|
|
5
|
+
*/
|
|
6
|
+
export declare function findNearestPackageJson(startDir?: string): string | null;
|
|
7
|
+
/**
|
|
8
|
+
* Read and parse the nearest package.json
|
|
9
|
+
* @param {string} startDir
|
|
10
|
+
* @returns {object|null}
|
|
11
|
+
*/
|
|
12
|
+
export declare function readNearestPackageJson(startDir?: string): any;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Find the nearest package.json starting from a directory and walking upward.
|
|
5
|
+
* @param {string} startDir - Directory to start searching from
|
|
6
|
+
* @returns {string|null} Absolute path to package.json or null if not found
|
|
7
|
+
*/
|
|
8
|
+
export function findNearestPackageJson(startDir = process.cwd()) {
|
|
9
|
+
let currentDir = path.resolve(startDir);
|
|
10
|
+
while (true) {
|
|
11
|
+
const pkgPath = path.join(currentDir, "package.json");
|
|
12
|
+
if (fs.existsSync(pkgPath)) {
|
|
13
|
+
return pkgPath;
|
|
14
|
+
}
|
|
15
|
+
const parentDir = path.dirname(currentDir);
|
|
16
|
+
if (parentDir === currentDir) {
|
|
17
|
+
// Reached filesystem root
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
currentDir = parentDir;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Read and parse the nearest package.json
|
|
25
|
+
* @param {string} startDir
|
|
26
|
+
* @returns {object|null}
|
|
27
|
+
*/
|
|
28
|
+
export function readNearestPackageJson(startDir = process.cwd()) {
|
|
29
|
+
const pkgPath = findNearestPackageJson(startDir);
|
|
30
|
+
if (!pkgPath)
|
|
31
|
+
return null;
|
|
32
|
+
const contents = fs.readFileSync(pkgPath, 'utf8');
|
|
33
|
+
return JSON.parse(contents);
|
|
34
|
+
}
|