@codifycli/plugin-core 1.2.5-beta.3 → 1.2.5-beta.4
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/.claude/settings.local.json +13 -0
- package/bin/build.js +0 -0
- package/dist/bin/build.d.ts +1 -0
- package/dist/bin/build.js +80 -0
- package/dist/bin/deploy-plugin.d.ts +2 -0
- package/dist/bin/deploy-plugin.js +8 -0
- package/dist/entities/change-set.d.ts +24 -0
- package/dist/entities/change-set.js +152 -0
- package/dist/entities/errors.d.ts +4 -0
- package/dist/entities/errors.js +7 -0
- package/dist/entities/plan-types.d.ts +25 -0
- package/dist/entities/plan-types.js +1 -0
- package/dist/entities/plan.d.ts +15 -0
- package/dist/entities/plan.js +127 -0
- package/dist/entities/plugin.d.ts +16 -0
- package/dist/entities/plugin.js +80 -0
- package/dist/entities/resource-options.d.ts +31 -0
- package/dist/entities/resource-options.js +76 -0
- package/dist/entities/resource-types.d.ts +11 -0
- package/dist/entities/resource-types.js +1 -0
- package/dist/entities/resource.d.ts +42 -0
- package/dist/entities/resource.js +303 -0
- package/dist/entities/stateful-parameter.d.ts +29 -0
- package/dist/entities/stateful-parameter.js +46 -0
- package/dist/entities/transform-parameter.d.ts +4 -0
- package/dist/entities/transform-parameter.js +2 -0
- package/dist/plan/change-set.d.ts +1 -1
- package/dist/plan/plan.js +12 -1
- package/dist/pty/background-pty.js +5 -2
- package/dist/pty/seqeuntial-pty.js +6 -2
- package/dist/pty/vitest.config.d.ts +2 -0
- package/dist/pty/vitest.config.js +11 -0
- package/dist/resource/stateful-parameter.d.ts +165 -0
- package/dist/resource/stateful-parameter.js +94 -0
- package/dist/scripts/deploy.d.ts +1 -0
- package/dist/scripts/deploy.js +2 -0
- package/dist/test.d.ts +1 -0
- package/dist/test.js +5 -0
- package/dist/utils/codify-spawn.d.ts +29 -0
- package/dist/utils/codify-spawn.js +136 -0
- package/dist/utils/index.d.ts +11 -0
- package/dist/utils/index.js +18 -2
- package/dist/utils/internal-utils.d.ts +12 -0
- package/dist/utils/internal-utils.js +74 -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/dist/utils/spawn-2.d.ts +5 -0
- package/dist/utils/spawn-2.js +7 -0
- package/dist/utils/spawn.d.ts +29 -0
- package/dist/utils/spawn.js +124 -0
- package/dist/utils/utils.d.ts +18 -0
- package/dist/utils/utils.js +86 -0
- package/package.json +1 -1
- package/src/plan/change-set.ts +1 -1
- package/src/plan/plan.test.ts +33 -0
- package/src/plan/plan.ts +13 -1
- package/src/pty/background-pty.ts +5 -2
- package/src/pty/seqeuntial-pty.ts +6 -2
- package/src/utils/index.ts +19 -2
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { StringIndexedObject } from 'codify-schemas';
|
|
2
|
+
import { Plan } from '../plan/plan.js';
|
|
3
|
+
import { ArrayParameterSetting, ParameterSetting } from './resource-settings.js';
|
|
4
|
+
/**
|
|
5
|
+
* A stateful parameter represents a parameter that holds state on the system (can be created, destroyed) but
|
|
6
|
+
* is still tied to the overall lifecycle of a resource.
|
|
7
|
+
*
|
|
8
|
+
* **Examples include:**
|
|
9
|
+
* 1. Homebrew formulas are stateful parameters. They can be installed and uninstalled but they are still tied to the
|
|
10
|
+
* overall lifecycle of homebrew
|
|
11
|
+
* 2. Nvm installed node versions are stateful parameters. Nvm can install and uninstall different versions of Node but
|
|
12
|
+
* these versions are tied to the lifecycle of nvm. If nvm is uninstalled then so are the Node versions.
|
|
13
|
+
*/
|
|
14
|
+
export declare abstract class StatefulParameter<T extends StringIndexedObject, V extends T[keyof T]> {
|
|
15
|
+
/**
|
|
16
|
+
* Parameter settings for the stateful parameter. Stateful parameters share the same parameter settings as
|
|
17
|
+
* regular parameters except that they cannot be of type 'stateful'. See {@link ParameterSetting} for more
|
|
18
|
+
* information on available settings.
|
|
19
|
+
*
|
|
20
|
+
* @return The parameter settings
|
|
21
|
+
*/
|
|
22
|
+
getSettings(): ParameterSetting;
|
|
23
|
+
/**
|
|
24
|
+
* Refresh the status of the stateful parameter on the system. This method works similarly to {@link Resource.refresh}.
|
|
25
|
+
* Return the value of the stateful parameter or null if not found.
|
|
26
|
+
*
|
|
27
|
+
* @param desired The desired value of the user.
|
|
28
|
+
* @param config The desired config
|
|
29
|
+
*
|
|
30
|
+
* @return The value of the stateful parameter currently on the system or null if not found
|
|
31
|
+
*/
|
|
32
|
+
abstract refresh(desired: V | null, config: Partial<T>): Promise<V | null>;
|
|
33
|
+
/**
|
|
34
|
+
* Create the stateful parameter on the system. This method is similar {@link Resource.create} except that its only
|
|
35
|
+
* applicable to the stateful parameter. For resource `CREATE` operations, this method will be called after the
|
|
36
|
+
* resource is successfully created. The add method is called when a ParameterChange is ADD in a plan. The add
|
|
37
|
+
* method is only called when the stateful parameter does not currently exist.
|
|
38
|
+
*
|
|
39
|
+
* **Example (Homebrew formula):**
|
|
40
|
+
* 1. Add is called with a value of:
|
|
41
|
+
* ```
|
|
42
|
+
* ['jq', 'jenv']
|
|
43
|
+
* ```
|
|
44
|
+
* 2. Add handles the request by calling `brew install --formulae jq jenv`
|
|
45
|
+
*
|
|
46
|
+
* @param valueToAdd The desired value of the stateful parameter.
|
|
47
|
+
* @param plan The overall plan that contains the ADD
|
|
48
|
+
*/
|
|
49
|
+
abstract add(valueToAdd: V, plan: Plan<T>): Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* Modify the state of a stateful parameter on the system. This method is similar to {@link Resource.modify} except that its only
|
|
52
|
+
* applicable to the stateful parameter.
|
|
53
|
+
*
|
|
54
|
+
* **Example (Git email parameter):**
|
|
55
|
+
* 1. Add is called with a value of:
|
|
56
|
+
* ```
|
|
57
|
+
* newValue: 'email+new@gmail.com', previousValue: 'email+old@gmail.com'
|
|
58
|
+
* ```
|
|
59
|
+
* 2. Modify handles the request by calling `git config --global user.email email+new@gmail.com`
|
|
60
|
+
*
|
|
61
|
+
* @param newValue The desired value of the stateful parameter
|
|
62
|
+
* @param previousValue The current value of the stateful parameter
|
|
63
|
+
* @param plan The overall plan
|
|
64
|
+
*/
|
|
65
|
+
abstract modify(newValue: V, previousValue: V, plan: Plan<T>): Promise<void>;
|
|
66
|
+
/**
|
|
67
|
+
* Create the stateful parameter on the system. This method is similar {@link Resource.destroy} except that its only
|
|
68
|
+
* applicable to the stateful parameter. The remove method is only called when the stateful parameter already currently exist.
|
|
69
|
+
* This method corresponds to REMOVE parameter operations in a plan.
|
|
70
|
+
* For resource `DESTORY`, this method is only called if the {@link ResourceSettings.removeStatefulParametersBeforeDestroy}
|
|
71
|
+
* is set to true. This method will be called before the resource is destroyed.
|
|
72
|
+
*
|
|
73
|
+
* **Example (Homebrew formula):**
|
|
74
|
+
* 1. Remove is called with a value of:
|
|
75
|
+
* ```
|
|
76
|
+
* ['jq', 'jenv']
|
|
77
|
+
* ```
|
|
78
|
+
* 2. Remove handles the request by calling `brew uninstall --formulae jq jenv`
|
|
79
|
+
*
|
|
80
|
+
* @param valueToRemove The value to remove from the stateful parameter.
|
|
81
|
+
* @param plan The overall plan that contains the REMOVE
|
|
82
|
+
*/
|
|
83
|
+
abstract remove(valueToRemove: V, plan: Plan<T>): Promise<void>;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* A specialized version of {@link StatefulParameter } that is used for stateful parameters which are arrays.
|
|
87
|
+
* A stateful parameter represents a parameter that holds state on the system (can be created, destroyed) but
|
|
88
|
+
* is still tied to the overall lifecycle of a resource.
|
|
89
|
+
*
|
|
90
|
+
* **Examples:**
|
|
91
|
+
* - Homebrew formulas are arrays
|
|
92
|
+
* - Pyenv python versions are arrays
|
|
93
|
+
*/
|
|
94
|
+
export declare abstract class ArrayStatefulParameter<T extends StringIndexedObject, V> extends StatefulParameter<T, any> {
|
|
95
|
+
/**
|
|
96
|
+
* Parameter level settings. Type must be 'array'.
|
|
97
|
+
*/
|
|
98
|
+
getSettings(): ArrayParameterSetting;
|
|
99
|
+
/**
|
|
100
|
+
* It is not recommended to override the `add` method. A addItem helper method is available to operate on
|
|
101
|
+
* individual elements of the desired array. See {@link StatefulParameter.add} for more info.
|
|
102
|
+
*
|
|
103
|
+
* @param valuesToAdd The array of values to add
|
|
104
|
+
* @param plan The overall plan
|
|
105
|
+
*
|
|
106
|
+
*/
|
|
107
|
+
add(valuesToAdd: V[], plan: Plan<T>): Promise<void>;
|
|
108
|
+
/**
|
|
109
|
+
* It is not recommended to override the `modify` method. `addItem` and `removeItem` will be called accordingly based
|
|
110
|
+
* on the modifications. See {@link StatefulParameter.modify} for more info.
|
|
111
|
+
*
|
|
112
|
+
* @param newValues The new array value
|
|
113
|
+
* @param previousValues The previous array value
|
|
114
|
+
* @param plan The overall plan
|
|
115
|
+
*/
|
|
116
|
+
modify(newValues: V[], previousValues: V[], plan: Plan<T>): Promise<void>;
|
|
117
|
+
/**
|
|
118
|
+
* It is not recommended to override the `remove` method. A removeItem helper method is available to operate on
|
|
119
|
+
* individual elements of the desired array. See {@link StatefulParameter.remove} for more info.
|
|
120
|
+
*
|
|
121
|
+
* @param valuesToAdd The array of values to add
|
|
122
|
+
* @param plan The overall plan
|
|
123
|
+
*
|
|
124
|
+
*/
|
|
125
|
+
remove(valuesToRemove: V[], plan: Plan<T>): Promise<void>;
|
|
126
|
+
/**
|
|
127
|
+
* See {@link StatefulParameter.refresh} for more info.
|
|
128
|
+
*
|
|
129
|
+
* @param desired The desired value to refresh
|
|
130
|
+
* @return The current value on the system or null if not found.
|
|
131
|
+
*/
|
|
132
|
+
abstract refresh(desired: V[] | null, config: Partial<T>): Promise<V[] | null>;
|
|
133
|
+
/**
|
|
134
|
+
* Helper method that gets called when individual elements of the array need to be added. See {@link StatefulParameter.add}
|
|
135
|
+
* for more information.
|
|
136
|
+
*
|
|
137
|
+
* Example (Homebrew formula):
|
|
138
|
+
* 1. The stateful parameter receives an input of:
|
|
139
|
+
* ```
|
|
140
|
+
* ['jq', 'jenv', 'docker']
|
|
141
|
+
* ```
|
|
142
|
+
* 2. Internally the stateful parameter will iterate the array and call `addItem` for each element
|
|
143
|
+
* 3. Override addItem and install each formula using `brew install --formula jq`
|
|
144
|
+
*
|
|
145
|
+
* @param item The item to add (install)
|
|
146
|
+
* @param plan The overall plan
|
|
147
|
+
*/
|
|
148
|
+
abstract addItem(item: V, plan: Plan<T>): Promise<void>;
|
|
149
|
+
/**
|
|
150
|
+
* Helper method that gets called when individual elements of the array need to be removed. See {@link StatefulParameter.remove}
|
|
151
|
+
* for more information.
|
|
152
|
+
*
|
|
153
|
+
* Example (Homebrew formula):
|
|
154
|
+
* 1. The stateful parameter receives an input of:
|
|
155
|
+
* ```
|
|
156
|
+
* ['jq', 'jenv', 'docker']
|
|
157
|
+
* ```
|
|
158
|
+
* 2. Internally the stateful parameter will iterate the array and call `removeItem` for each element
|
|
159
|
+
* 3. Override removeItem and uninstall each formula using `brew uninstall --formula jq`
|
|
160
|
+
*
|
|
161
|
+
* @param item The item to remove (uninstall)
|
|
162
|
+
* @param plan The overall plan
|
|
163
|
+
*/
|
|
164
|
+
abstract removeItem(item: V, plan: Plan<T>): Promise<void>;
|
|
165
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A stateful parameter represents a parameter that holds state on the system (can be created, destroyed) but
|
|
3
|
+
* is still tied to the overall lifecycle of a resource.
|
|
4
|
+
*
|
|
5
|
+
* **Examples include:**
|
|
6
|
+
* 1. Homebrew formulas are stateful parameters. They can be installed and uninstalled but they are still tied to the
|
|
7
|
+
* overall lifecycle of homebrew
|
|
8
|
+
* 2. Nvm installed node versions are stateful parameters. Nvm can install and uninstall different versions of Node but
|
|
9
|
+
* these versions are tied to the lifecycle of nvm. If nvm is uninstalled then so are the Node versions.
|
|
10
|
+
*/
|
|
11
|
+
export class StatefulParameter {
|
|
12
|
+
/**
|
|
13
|
+
* Parameter settings for the stateful parameter. Stateful parameters share the same parameter settings as
|
|
14
|
+
* regular parameters except that they cannot be of type 'stateful'. See {@link ParameterSetting} for more
|
|
15
|
+
* information on available settings.
|
|
16
|
+
*
|
|
17
|
+
* @return The parameter settings
|
|
18
|
+
*/
|
|
19
|
+
getSettings() {
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A specialized version of {@link StatefulParameter } that is used for stateful parameters which are arrays.
|
|
25
|
+
* A stateful parameter represents a parameter that holds state on the system (can be created, destroyed) but
|
|
26
|
+
* is still tied to the overall lifecycle of a resource.
|
|
27
|
+
*
|
|
28
|
+
* **Examples:**
|
|
29
|
+
* - Homebrew formulas are arrays
|
|
30
|
+
* - Pyenv python versions are arrays
|
|
31
|
+
*/
|
|
32
|
+
export class ArrayStatefulParameter extends StatefulParameter {
|
|
33
|
+
/**
|
|
34
|
+
* Parameter level settings. Type must be 'array'.
|
|
35
|
+
*/
|
|
36
|
+
getSettings() {
|
|
37
|
+
return { type: 'array' };
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* It is not recommended to override the `add` method. A addItem helper method is available to operate on
|
|
41
|
+
* individual elements of the desired array. See {@link StatefulParameter.add} for more info.
|
|
42
|
+
*
|
|
43
|
+
* @param valuesToAdd The array of values to add
|
|
44
|
+
* @param plan The overall plan
|
|
45
|
+
*
|
|
46
|
+
*/
|
|
47
|
+
async add(valuesToAdd, plan) {
|
|
48
|
+
for (const value of valuesToAdd) {
|
|
49
|
+
await this.addItem(value, plan);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* It is not recommended to override the `modify` method. `addItem` and `removeItem` will be called accordingly based
|
|
54
|
+
* on the modifications. See {@link StatefulParameter.modify} for more info.
|
|
55
|
+
*
|
|
56
|
+
* @param newValues The new array value
|
|
57
|
+
* @param previousValues The previous array value
|
|
58
|
+
* @param plan The overall plan
|
|
59
|
+
*/
|
|
60
|
+
async modify(newValues, previousValues, plan) {
|
|
61
|
+
// TODO: I don't think this works with duplicate elements. Solve at another time
|
|
62
|
+
const valuesToAdd = newValues.filter((n) => !previousValues.some((p) => {
|
|
63
|
+
if (this.getSettings()?.isElementEqual) {
|
|
64
|
+
return this.getSettings().isElementEqual(n, p);
|
|
65
|
+
}
|
|
66
|
+
return n === p;
|
|
67
|
+
}));
|
|
68
|
+
const valuesToRemove = previousValues.filter((p) => !newValues.some((n) => {
|
|
69
|
+
if (this.getSettings().isElementEqual) {
|
|
70
|
+
return this.getSettings().isElementEqual(n, p);
|
|
71
|
+
}
|
|
72
|
+
return n === p;
|
|
73
|
+
}));
|
|
74
|
+
for (const value of valuesToAdd) {
|
|
75
|
+
await this.addItem(value, plan);
|
|
76
|
+
}
|
|
77
|
+
for (const value of valuesToRemove) {
|
|
78
|
+
await this.removeItem(value, plan);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* It is not recommended to override the `remove` method. A removeItem helper method is available to operate on
|
|
83
|
+
* individual elements of the desired array. See {@link StatefulParameter.remove} for more info.
|
|
84
|
+
*
|
|
85
|
+
* @param valuesToAdd The array of values to add
|
|
86
|
+
* @param plan The overall plan
|
|
87
|
+
*
|
|
88
|
+
*/
|
|
89
|
+
async remove(valuesToRemove, plan) {
|
|
90
|
+
for (const value of valuesToRemove) {
|
|
91
|
+
await this.removeItem(value, plan);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/test.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/test.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { SpawnOptions } from 'node:child_process';
|
|
2
|
+
export declare enum SpawnStatus {
|
|
3
|
+
SUCCESS = "success",
|
|
4
|
+
ERROR = "error"
|
|
5
|
+
}
|
|
6
|
+
export interface SpawnResult {
|
|
7
|
+
status: SpawnStatus;
|
|
8
|
+
data: string;
|
|
9
|
+
}
|
|
10
|
+
type CodifySpawnOptions = {
|
|
11
|
+
cwd?: string;
|
|
12
|
+
throws?: boolean;
|
|
13
|
+
requiresRoot?: boolean;
|
|
14
|
+
requestsTTY?: boolean;
|
|
15
|
+
} & Omit<SpawnOptions, 'detached' | 'shell' | 'stdio'>;
|
|
16
|
+
/**
|
|
17
|
+
*
|
|
18
|
+
* @param cmd Command to run. Ex: `rm -rf`
|
|
19
|
+
* @param opts Standard options for node spawn. Additional argument:
|
|
20
|
+
* throws determines if a shell will throw a JS error. Defaults to true
|
|
21
|
+
*
|
|
22
|
+
* @see promiseSpawn
|
|
23
|
+
* @see spawn
|
|
24
|
+
*
|
|
25
|
+
* @returns SpawnResult { status: SUCCESS | ERROR; data: string }
|
|
26
|
+
*/
|
|
27
|
+
export declare function codifySpawn(cmd: string, opts?: CodifySpawnOptions): Promise<SpawnResult>;
|
|
28
|
+
export declare function isDebug(): boolean;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { Ajv } from 'ajv';
|
|
2
|
+
import { MessageCmd, SudoRequestResponseDataSchema } from 'codify-schemas';
|
|
3
|
+
import { nanoid } from 'nanoid';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import stripAnsi from 'strip-ansi';
|
|
6
|
+
import { VerbosityLevel } from './utils.js';
|
|
7
|
+
import { SudoError } from '../errors.js';
|
|
8
|
+
const ajv = new Ajv({
|
|
9
|
+
strict: true,
|
|
10
|
+
});
|
|
11
|
+
const validateSudoRequestResponse = ajv.compile(SudoRequestResponseDataSchema);
|
|
12
|
+
export var SpawnStatus;
|
|
13
|
+
(function (SpawnStatus) {
|
|
14
|
+
SpawnStatus["SUCCESS"] = "success";
|
|
15
|
+
SpawnStatus["ERROR"] = "error";
|
|
16
|
+
})(SpawnStatus || (SpawnStatus = {}));
|
|
17
|
+
/**
|
|
18
|
+
*
|
|
19
|
+
* @param cmd Command to run. Ex: `rm -rf`
|
|
20
|
+
* @param opts Standard options for node spawn. Additional argument:
|
|
21
|
+
* throws determines if a shell will throw a JS error. Defaults to true
|
|
22
|
+
*
|
|
23
|
+
* @see promiseSpawn
|
|
24
|
+
* @see spawn
|
|
25
|
+
*
|
|
26
|
+
* @returns SpawnResult { status: SUCCESS | ERROR; data: string }
|
|
27
|
+
*/
|
|
28
|
+
export async function codifySpawn(cmd, opts) {
|
|
29
|
+
const throws = opts?.throws ?? true;
|
|
30
|
+
console.log(`Running command: ${cmd}` + (opts?.cwd ? `(${opts?.cwd})` : ''));
|
|
31
|
+
try {
|
|
32
|
+
// TODO: Need to benchmark the effects of using sh vs zsh for shell.
|
|
33
|
+
// Seems like zsh shells run slower
|
|
34
|
+
const result = await (opts?.requiresRoot
|
|
35
|
+
? externalSpawnWithSudo(cmd, opts)
|
|
36
|
+
: internalSpawn(cmd, opts ?? {}));
|
|
37
|
+
if (result.status !== SpawnStatus.SUCCESS) {
|
|
38
|
+
throw new Error(result.data);
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (isDebug()) {
|
|
44
|
+
console.error(`CodifySpawn error for command ${cmd}`, error);
|
|
45
|
+
}
|
|
46
|
+
// @ts-ignore
|
|
47
|
+
if (error.message?.startsWith('sudo:')) {
|
|
48
|
+
throw new SudoError(cmd);
|
|
49
|
+
}
|
|
50
|
+
if (throws) {
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
if (error instanceof Error) {
|
|
54
|
+
return {
|
|
55
|
+
status: SpawnStatus.ERROR,
|
|
56
|
+
data: error.message,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
status: SpawnStatus.ERROR,
|
|
61
|
+
data: String(error),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async function internalSpawn(cmd, opts) {
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
const output = [];
|
|
68
|
+
// If TERM_PROGRAM=Apple_Terminal is set then ANSI escape characters may be included
|
|
69
|
+
// in the response.
|
|
70
|
+
const env = { ...process.env, ...opts.env, TERM_PROGRAM: 'codify', COMMAND_MODE: 'unix2003', COLORTERM: 'truecolor' };
|
|
71
|
+
const shell = getDefaultShell();
|
|
72
|
+
const rcFile = shell === 'zsh' ? '~/.zshrc' : '~/.bashrc';
|
|
73
|
+
// Source start up shells to emulate a users environment vs. a non-interactive non-login shell script
|
|
74
|
+
// Ignore all stdin
|
|
75
|
+
// If tty is requested then we'll need to sleep 1 to avoid race conditions. This is because if the terminal updates async after the tty message is
|
|
76
|
+
// displayed then it'll disappear. By adding sleep 1 it'll allow ink.js to finish all the updates before the tty message is shown
|
|
77
|
+
const _process = spawn(`source ${rcFile}; ${opts.requestsTTY ? 'sleep 1;' : ''}${cmd}`, [], {
|
|
78
|
+
...opts,
|
|
79
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
80
|
+
shell,
|
|
81
|
+
env
|
|
82
|
+
});
|
|
83
|
+
const { stdout, stderr, stdin } = _process;
|
|
84
|
+
stdout.setEncoding('utf8');
|
|
85
|
+
stderr.setEncoding('utf8');
|
|
86
|
+
stdout.on('data', (data) => {
|
|
87
|
+
output.push(data.toString());
|
|
88
|
+
});
|
|
89
|
+
stderr.on('data', (data) => {
|
|
90
|
+
output.push(data.toString());
|
|
91
|
+
});
|
|
92
|
+
_process.on('error', (data) => { });
|
|
93
|
+
// please node that this is not a full replacement for 'inherit'
|
|
94
|
+
// the child process can and will detect if stdout is a pty and change output based on it
|
|
95
|
+
// the terminal context is lost & ansi information (coloring) etc will be lost
|
|
96
|
+
if (stdout && stderr && VerbosityLevel.get() > 0) {
|
|
97
|
+
stdout.pipe(process.stdout);
|
|
98
|
+
stderr.pipe(process.stderr);
|
|
99
|
+
}
|
|
100
|
+
_process.on('close', (code) => {
|
|
101
|
+
resolve({
|
|
102
|
+
status: code === 0 ? SpawnStatus.SUCCESS : SpawnStatus.ERROR,
|
|
103
|
+
data: stripAnsi(output.join('\n')),
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
async function externalSpawnWithSudo(cmd, opts) {
|
|
109
|
+
return new Promise((resolve) => {
|
|
110
|
+
const requestId = nanoid(8);
|
|
111
|
+
const listener = (data) => {
|
|
112
|
+
if (data.requestId === requestId) {
|
|
113
|
+
process.removeListener('message', listener);
|
|
114
|
+
if (!validateSudoRequestResponse(data.data)) {
|
|
115
|
+
throw new Error(`Invalid response for sudo request: ${JSON.stringify(validateSudoRequestResponse.errors, null, 2)}`);
|
|
116
|
+
}
|
|
117
|
+
resolve(data.data);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
process.on('message', listener);
|
|
121
|
+
process.send({
|
|
122
|
+
cmd: MessageCmd.SUDO_REQUEST,
|
|
123
|
+
data: {
|
|
124
|
+
command: cmd,
|
|
125
|
+
options: opts ?? {},
|
|
126
|
+
},
|
|
127
|
+
requestId
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
export function isDebug() {
|
|
132
|
+
return process.env.DEBUG != null && process.env.DEBUG.includes('codify'); // TODO: replace with debug library
|
|
133
|
+
}
|
|
134
|
+
function getDefaultShell() {
|
|
135
|
+
return process.platform === 'darwin' ? 'zsh' : 'bash';
|
|
136
|
+
}
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -53,6 +53,17 @@ export declare const Utils: {
|
|
|
53
53
|
isHomebrewInstalled(): Promise<boolean>;
|
|
54
54
|
isRosetta2Installed(): Promise<boolean>;
|
|
55
55
|
getShell(): Shell | undefined;
|
|
56
|
+
/**
|
|
57
|
+
* Resolves the shell binary to launch commands with. `process.env.SHELL` is
|
|
58
|
+
* only set when Codify is launched from a terminal — GUI launches (e.g. the
|
|
59
|
+
* Codify desktop app via launchd) do not export it, in which case
|
|
60
|
+
* `node-pty` would fall back to `/bin/sh`, which does not source the user's
|
|
61
|
+
* `~/.zshrc`/`~/.bash_profile`. That drops user env (e.g. TART_HOME, PATH
|
|
62
|
+
* additions), breaking resource refresh. Fall back to the login shell from
|
|
63
|
+
* the passwd database (os.userInfo().shell) so we always use the user's real
|
|
64
|
+
* shell regardless of how Codify was started.
|
|
65
|
+
*/
|
|
66
|
+
getDefaultShell(): string;
|
|
56
67
|
getPrimaryShellRc(): string;
|
|
57
68
|
getShellRcFiles(): string[];
|
|
58
69
|
isDirectoryOnPath(directory: string): Promise<boolean>;
|
package/dist/utils/index.js
CHANGED
|
@@ -85,6 +85,19 @@ export const Utils = {
|
|
|
85
85
|
}
|
|
86
86
|
return undefined;
|
|
87
87
|
},
|
|
88
|
+
/**
|
|
89
|
+
* Resolves the shell binary to launch commands with. `process.env.SHELL` is
|
|
90
|
+
* only set when Codify is launched from a terminal — GUI launches (e.g. the
|
|
91
|
+
* Codify desktop app via launchd) do not export it, in which case
|
|
92
|
+
* `node-pty` would fall back to `/bin/sh`, which does not source the user's
|
|
93
|
+
* `~/.zshrc`/`~/.bash_profile`. That drops user env (e.g. TART_HOME, PATH
|
|
94
|
+
* additions), breaking resource refresh. Fall back to the login shell from
|
|
95
|
+
* the passwd database (os.userInfo().shell) so we always use the user's real
|
|
96
|
+
* shell regardless of how Codify was started.
|
|
97
|
+
*/
|
|
98
|
+
getDefaultShell() {
|
|
99
|
+
return process.env.SHELL || os.userInfo().shell || '/bin/zsh';
|
|
100
|
+
},
|
|
88
101
|
getPrimaryShellRc() {
|
|
89
102
|
return this.getShellRcFiles()[0];
|
|
90
103
|
},
|
|
@@ -180,7 +193,10 @@ Brew can be installed using Codify:
|
|
|
180
193
|
if (brewOpts?.flags)
|
|
181
194
|
flags.push(...brewOpts.flags);
|
|
182
195
|
const flagStr = flags.length > 0 ? `${flags.join(' ')} ` : '';
|
|
183
|
-
|
|
196
|
+
// Redirect stdin from /dev/null so Homebrew's ask.rb detects a non-TTY stdin
|
|
197
|
+
// and skips any [y/n] confirmation prompts (e.g. "Do you want to proceed with the installation?").
|
|
198
|
+
// NONINTERACTIVE=1 alone is not sufficient — Homebrew's prompt only checks tty?, not that var.
|
|
199
|
+
await $.spawn(`brew install ${flagStr}${packageName} < /dev/null`, { interactive: true, env: { HOMEBREW_NO_AUTO_UPDATE: 1, NONINTERACTIVE: 1 } });
|
|
184
200
|
return;
|
|
185
201
|
}
|
|
186
202
|
const useApt = forcePackageManager === PackageManager.APT || (!forcePackageManager && Utils.isLinux());
|
|
@@ -272,7 +288,7 @@ Brew can be installed using Codify:
|
|
|
272
288
|
const flagStr = flags.length > 0 ? `${flags.join(' ')} ` : '';
|
|
273
289
|
const { status } = await $.spawnSafe(`brew uninstall ${flagStr}${packageName}`, {
|
|
274
290
|
interactive: true,
|
|
275
|
-
env: { HOMEBREW_NO_AUTO_UPDATE: 1,
|
|
291
|
+
env: { HOMEBREW_NO_AUTO_UPDATE: 1, NONINTERACTIVE: 1 }
|
|
276
292
|
});
|
|
277
293
|
return status === SpawnStatus.SUCCESS;
|
|
278
294
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ResourceConfig, StringIndexedObject } from 'codify-schemas';
|
|
2
|
+
export declare function splitUserConfig<T extends StringIndexedObject>(config: ResourceConfig & T): {
|
|
3
|
+
parameters: T;
|
|
4
|
+
coreParameters: ResourceConfig;
|
|
5
|
+
};
|
|
6
|
+
export declare function setsEqual(set1: Set<unknown>, set2: Set<unknown>): boolean;
|
|
7
|
+
export declare function untildify(pathWithTilde: string): string;
|
|
8
|
+
export declare function tildify(pathWithTilde: string): string;
|
|
9
|
+
export declare function resolvePathWithVariables(pathWithVariables: string): string;
|
|
10
|
+
export declare function addVariablesToPath(pathWithoutVariables: string): string;
|
|
11
|
+
export declare function unhome(pathWithHome: string): string;
|
|
12
|
+
export declare function areArraysEqual(isElementEqual: ((desired: unknown, current: unknown) => boolean) | undefined, desired: unknown, current: unknown): boolean;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export function splitUserConfig(config) {
|
|
4
|
+
const coreParameters = {
|
|
5
|
+
type: config.type,
|
|
6
|
+
...(config.name ? { name: config.name } : {}),
|
|
7
|
+
...(config.dependsOn ? { dependsOn: config.dependsOn } : {}),
|
|
8
|
+
};
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
10
|
+
const { type, name, dependsOn, ...parameters } = config;
|
|
11
|
+
return {
|
|
12
|
+
parameters: parameters,
|
|
13
|
+
coreParameters,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export function setsEqual(set1, set2) {
|
|
17
|
+
return set1.size === set2.size && [...set1].every((v) => set2.has(v));
|
|
18
|
+
}
|
|
19
|
+
const homeDirectory = os.homedir();
|
|
20
|
+
export function untildify(pathWithTilde) {
|
|
21
|
+
return homeDirectory ? pathWithTilde.replace(/^~(?=$|\/|\\)/, homeDirectory) : pathWithTilde;
|
|
22
|
+
}
|
|
23
|
+
export function tildify(pathWithTilde) {
|
|
24
|
+
return homeDirectory ? pathWithTilde.replace(homeDirectory, '~') : pathWithTilde;
|
|
25
|
+
}
|
|
26
|
+
export function resolvePathWithVariables(pathWithVariables) {
|
|
27
|
+
// @ts-expect-error Ignore this for now
|
|
28
|
+
return pathWithVariables.replace(/\$([A-Z_]+[A-Z0-9_]*)|\${([A-Z0-9_]*)}/ig, (_, a, b) => process.env[a || b]);
|
|
29
|
+
}
|
|
30
|
+
export function addVariablesToPath(pathWithoutVariables) {
|
|
31
|
+
let result = pathWithoutVariables;
|
|
32
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
33
|
+
if (!value || !path.isAbsolute(value) || value === '/' || key === 'HOME' || key === 'PATH' || key === 'SHELL' || key === 'PWD') {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
result = result.replaceAll(value, `$${key}`);
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
export function unhome(pathWithHome) {
|
|
41
|
+
return pathWithHome.includes('$HOME') ? pathWithHome.replaceAll('$HOME', os.homedir()) : pathWithHome;
|
|
42
|
+
}
|
|
43
|
+
export function areArraysEqual(isElementEqual, desired, current) {
|
|
44
|
+
if (!desired || !current) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
if (!Array.isArray(desired) || !Array.isArray(current)) {
|
|
48
|
+
throw new Error(`A non-array value:
|
|
49
|
+
|
|
50
|
+
Desired: ${JSON.stringify(desired, null, 2)}
|
|
51
|
+
|
|
52
|
+
Current: ${JSON.stringify(desired, null, 2)}
|
|
53
|
+
|
|
54
|
+
Was provided even though type array was specified.
|
|
55
|
+
`);
|
|
56
|
+
}
|
|
57
|
+
if (desired.length !== current.length) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
const desiredCopy = [...desired];
|
|
61
|
+
const currentCopy = [...current];
|
|
62
|
+
// Algorithm for to check equality between two un-ordered; un-hashable arrays using
|
|
63
|
+
// an isElementEqual method. Time: O(n^2)
|
|
64
|
+
for (let counter = desiredCopy.length - 1; counter >= 0; counter--) {
|
|
65
|
+
const idx = currentCopy.findIndex((e2) => (isElementEqual
|
|
66
|
+
?? ((a, b) => a === b))(desiredCopy[counter], e2));
|
|
67
|
+
if (idx === -1) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
desiredCopy.splice(counter, 1);
|
|
71
|
+
currentCopy.splice(idx, 1);
|
|
72
|
+
}
|
|
73
|
+
return currentCopy.length === 0;
|
|
74
|
+
}
|
|
@@ -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;
|