@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,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
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
+
import { SpawnOptions } from 'node:child_process';
|
|
3
|
+
export declare enum SpawnStatus {
|
|
4
|
+
SUCCESS = "success",
|
|
5
|
+
ERROR = "error"
|
|
6
|
+
}
|
|
7
|
+
export interface SpawnResult {
|
|
8
|
+
status: SpawnStatus;
|
|
9
|
+
data: string;
|
|
10
|
+
}
|
|
11
|
+
type CodifySpawnOptions = {
|
|
12
|
+
cwd?: string;
|
|
13
|
+
throws?: boolean;
|
|
14
|
+
requiresRoot?: 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 $(cmd: string, opts?: CodifySpawnOptions): Promise<SpawnResult>;
|
|
28
|
+
export declare function isDebug(): boolean;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { Ajv } from 'ajv';
|
|
2
|
+
import { MessageCmd, SudoRequestResponseDataSchema } from 'codify-schemas';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { SudoError } from '../errors.js';
|
|
5
|
+
const ajv = new Ajv({
|
|
6
|
+
strict: true,
|
|
7
|
+
});
|
|
8
|
+
const validateSudoRequestResponse = ajv.compile(SudoRequestResponseDataSchema);
|
|
9
|
+
export var SpawnStatus;
|
|
10
|
+
(function (SpawnStatus) {
|
|
11
|
+
SpawnStatus["SUCCESS"] = "success";
|
|
12
|
+
SpawnStatus["ERROR"] = "error";
|
|
13
|
+
})(SpawnStatus || (SpawnStatus = {}));
|
|
14
|
+
/**
|
|
15
|
+
*
|
|
16
|
+
* @param cmd Command to run. Ex: `rm -rf`
|
|
17
|
+
* @param opts Standard options for node spawn. Additional argument:
|
|
18
|
+
* throws determines if a shell will throw a JS error. Defaults to true
|
|
19
|
+
*
|
|
20
|
+
* @see promiseSpawn
|
|
21
|
+
* @see spawn
|
|
22
|
+
*
|
|
23
|
+
* @returns SpawnResult { status: SUCCESS | ERROR; data: string }
|
|
24
|
+
*/
|
|
25
|
+
export async function $(cmd, opts) {
|
|
26
|
+
const throws = opts?.throws ?? true;
|
|
27
|
+
console.log(`Running command: ${cmd}`);
|
|
28
|
+
try {
|
|
29
|
+
// TODO: Need to benchmark the effects of using sh vs zsh for shell.
|
|
30
|
+
// Seems like zsh shells run slower
|
|
31
|
+
let result;
|
|
32
|
+
if (!opts?.requiresRoot) {
|
|
33
|
+
result = await internalSpawn(cmd, opts ?? {});
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
result = await externalSpawnWithSudo(cmd, opts);
|
|
37
|
+
}
|
|
38
|
+
if (result.status !== SpawnStatus.SUCCESS) {
|
|
39
|
+
throw new Error(result.data);
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
if (isDebug()) {
|
|
45
|
+
console.error(`CodifySpawn error for command ${cmd}`, error);
|
|
46
|
+
}
|
|
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: error + '',
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async function internalSpawn(cmd, opts) {
|
|
66
|
+
return new Promise((resolve, reject) => {
|
|
67
|
+
const output = [];
|
|
68
|
+
// Source start up shells to emulate a users environment vs. a non-interactive non-login shell script
|
|
69
|
+
// Ignore all stdin
|
|
70
|
+
const _process = spawn(`source ~/.zshrc; ${cmd}`, [], {
|
|
71
|
+
...opts,
|
|
72
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
73
|
+
shell: 'zsh',
|
|
74
|
+
});
|
|
75
|
+
const { stdout, stderr, stdin } = _process;
|
|
76
|
+
stdout.setEncoding('utf8');
|
|
77
|
+
stderr.setEncoding('utf8');
|
|
78
|
+
stdout.on('data', (data) => {
|
|
79
|
+
output.push(data.toString());
|
|
80
|
+
});
|
|
81
|
+
stderr.on('data', (data) => {
|
|
82
|
+
output.push(data.toString());
|
|
83
|
+
});
|
|
84
|
+
_process.on('error', (data) => {
|
|
85
|
+
});
|
|
86
|
+
// please node that this is not a full replacement for 'inherit'
|
|
87
|
+
// the child process can and will detect if stdout is a pty and change output based on it
|
|
88
|
+
// the terminal context is lost & ansi information (coloring) etc will be lost
|
|
89
|
+
if (stdout && stderr) {
|
|
90
|
+
stdout.pipe(process.stdout);
|
|
91
|
+
stderr.pipe(process.stderr);
|
|
92
|
+
}
|
|
93
|
+
_process.on('close', (code) => {
|
|
94
|
+
resolve({
|
|
95
|
+
status: code === 0 ? SpawnStatus.SUCCESS : SpawnStatus.ERROR,
|
|
96
|
+
data: output.join('\n'),
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
async function externalSpawnWithSudo(cmd, opts) {
|
|
102
|
+
return await new Promise((resolve) => {
|
|
103
|
+
const listener = (data) => {
|
|
104
|
+
if (data.cmd === MessageCmd.SUDO_REQUEST + '_Response') {
|
|
105
|
+
process.removeListener('message', listener);
|
|
106
|
+
if (!validateSudoRequestResponse(data.data)) {
|
|
107
|
+
throw new Error(`Invalid response for sudo request: ${JSON.stringify(validateSudoRequestResponse.errors, null, 2)}`);
|
|
108
|
+
}
|
|
109
|
+
resolve(data.data);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
process.on('message', listener);
|
|
113
|
+
process.send({
|
|
114
|
+
cmd: MessageCmd.SUDO_REQUEST,
|
|
115
|
+
data: {
|
|
116
|
+
command: cmd,
|
|
117
|
+
options: opts ?? {},
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
export function isDebug() {
|
|
123
|
+
return process.env.DEBUG != null && process.env.DEBUG.includes('codify'); // TODO: replace with debug library
|
|
124
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ResourceConfig, StringIndexedObject } from 'codify-schemas';
|
|
2
|
+
export declare const VerbosityLevel: {
|
|
3
|
+
level: number;
|
|
4
|
+
get(): number;
|
|
5
|
+
set(level: number): void;
|
|
6
|
+
};
|
|
7
|
+
export declare function isDebug(): boolean;
|
|
8
|
+
export declare function splitUserConfig<T extends StringIndexedObject>(config: ResourceConfig & T): {
|
|
9
|
+
parameters: T;
|
|
10
|
+
coreParameters: ResourceConfig;
|
|
11
|
+
};
|
|
12
|
+
export declare function setsEqual(set1: Set<unknown>, set2: Set<unknown>): boolean;
|
|
13
|
+
export declare function untildify(pathWithTilde: string): string;
|
|
14
|
+
export declare function tildify(pathWithTilde: string): string;
|
|
15
|
+
export declare function resolvePathWithVariables(pathWithVariables: string): string;
|
|
16
|
+
export declare function addVariablesToPath(pathWithoutVariables: string): string;
|
|
17
|
+
export declare function unhome(pathWithHome: string): string;
|
|
18
|
+
export declare function areArraysEqual(isElementEqual: ((desired: unknown, current: unknown) => boolean) | undefined, desired: unknown, current: unknown): boolean;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export const VerbosityLevel = new class {
|
|
4
|
+
level = 0;
|
|
5
|
+
get() {
|
|
6
|
+
return this.level;
|
|
7
|
+
}
|
|
8
|
+
set(level) {
|
|
9
|
+
this.level = level;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
export function isDebug() {
|
|
13
|
+
return process.env.DEBUG != null && process.env.DEBUG.includes('codify'); // TODO: replace with debug library
|
|
14
|
+
}
|
|
15
|
+
export function splitUserConfig(config) {
|
|
16
|
+
const coreParameters = {
|
|
17
|
+
type: config.type,
|
|
18
|
+
...(config.name ? { name: config.name } : {}),
|
|
19
|
+
...(config.dependsOn ? { dependsOn: config.dependsOn } : {}),
|
|
20
|
+
};
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
22
|
+
const { type, name, dependsOn, ...parameters } = config;
|
|
23
|
+
return {
|
|
24
|
+
parameters: parameters,
|
|
25
|
+
coreParameters,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function setsEqual(set1, set2) {
|
|
29
|
+
return set1.size === set2.size && [...set1].every((v) => set2.has(v));
|
|
30
|
+
}
|
|
31
|
+
const homeDirectory = os.homedir();
|
|
32
|
+
export function untildify(pathWithTilde) {
|
|
33
|
+
return homeDirectory ? pathWithTilde.replace(/^~(?=$|\/|\\)/, homeDirectory) : pathWithTilde;
|
|
34
|
+
}
|
|
35
|
+
export function tildify(pathWithTilde) {
|
|
36
|
+
return homeDirectory ? pathWithTilde.replace(homeDirectory, '~') : pathWithTilde;
|
|
37
|
+
}
|
|
38
|
+
export function resolvePathWithVariables(pathWithVariables) {
|
|
39
|
+
// @ts-expect-error Ignore this for now
|
|
40
|
+
return pathWithVariables.replace(/\$([A-Z_]+[A-Z0-9_]*)|\${([A-Z0-9_]*)}/ig, (_, a, b) => process.env[a || b]);
|
|
41
|
+
}
|
|
42
|
+
export function addVariablesToPath(pathWithoutVariables) {
|
|
43
|
+
let result = pathWithoutVariables;
|
|
44
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
45
|
+
if (!value || !path.isAbsolute(value) || value === '/' || key === 'HOME' || key === 'PATH' || key === 'SHELL' || key === 'PWD') {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
result = result.replaceAll(value, `$${key}`);
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
export function unhome(pathWithHome) {
|
|
53
|
+
return pathWithHome.includes('$HOME') ? pathWithHome.replaceAll('$HOME', os.homedir()) : pathWithHome;
|
|
54
|
+
}
|
|
55
|
+
export function areArraysEqual(isElementEqual, desired, current) {
|
|
56
|
+
if (!desired || !current) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
if (!Array.isArray(desired) || !Array.isArray(current)) {
|
|
60
|
+
throw new Error(`A non-array value:
|
|
61
|
+
|
|
62
|
+
Desired: ${JSON.stringify(desired, null, 2)}
|
|
63
|
+
|
|
64
|
+
Current: ${JSON.stringify(desired, null, 2)}
|
|
65
|
+
|
|
66
|
+
Was provided even though type array was specified.
|
|
67
|
+
`);
|
|
68
|
+
}
|
|
69
|
+
if (desired.length !== current.length) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
const desiredCopy = [...desired];
|
|
73
|
+
const currentCopy = [...current];
|
|
74
|
+
// Algorithm for to check equality between two un-ordered; un-hashable arrays using
|
|
75
|
+
// an isElementEqual method. Time: O(n^2)
|
|
76
|
+
for (let counter = desiredCopy.length - 1; counter >= 0; counter--) {
|
|
77
|
+
const idx = currentCopy.findIndex((e2) => (isElementEqual
|
|
78
|
+
?? ((a, b) => a === b))(desiredCopy[counter], e2));
|
|
79
|
+
if (idx === -1) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
desiredCopy.splice(counter, 1);
|
|
83
|
+
currentCopy.splice(idx, 1);
|
|
84
|
+
}
|
|
85
|
+
return currentCopy.length === 0;
|
|
86
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codifycli/plugin-core",
|
|
3
|
-
"version": "1.2.5-beta.
|
|
3
|
+
"version": "1.2.5-beta.4",
|
|
4
4
|
"description": "TypeScript library for building Codify plugins to manage system resources (applications, CLI tools, settings) through infrastructure-as-code",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"typings": "dist/index.d.ts",
|
package/src/plan/change-set.ts
CHANGED
|
@@ -227,7 +227,7 @@ export class ChangeSet<T extends StringIndexedObject> {
|
|
|
227
227
|
return orderOfOperations[Math.max(indexPrev, indexNext)];
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
-
|
|
230
|
+
static isAbsent(v: unknown): boolean {
|
|
231
231
|
if (v === null || v === undefined) return true;
|
|
232
232
|
if (Array.isArray(v)) return v.length === 0;
|
|
233
233
|
if (typeof v === 'object') return Object.keys(v as object).length === 0;
|
package/src/plan/plan.test.ts
CHANGED
|
@@ -226,6 +226,39 @@ describe('Plan entity tests', () => {
|
|
|
226
226
|
})
|
|
227
227
|
})
|
|
228
228
|
|
|
229
|
+
it('Treats an empty-after-filtering array as no current state (already destroyed)', async () => {
|
|
230
|
+
const resource = new class extends TestResource {
|
|
231
|
+
getSettings(): ResourceSettings<any> {
|
|
232
|
+
return {
|
|
233
|
+
id: 'type',
|
|
234
|
+
operatingSystems: [OS.Darwin],
|
|
235
|
+
parameterSettings: {
|
|
236
|
+
propZ: { type: 'array', isElementEqual: (a, b) => a === b }
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async refresh(): Promise<Partial<any> | null> {
|
|
242
|
+
// The declared item is already gone from the system, but other unrelated items remain —
|
|
243
|
+
// refresh() can't know which items belong to this resource's declared config, so it still
|
|
244
|
+
// returns a non-empty object overall.
|
|
245
|
+
return {
|
|
246
|
+
propZ: ['some-other-unmanaged-item']
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const controller = new ResourceController(resource);
|
|
252
|
+
const plan = await controller.plan(
|
|
253
|
+
{ type: 'type' },
|
|
254
|
+
null,
|
|
255
|
+
{ propZ: ['20.15.0'] } as any,
|
|
256
|
+
true
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
expect(plan.changeSet.operation).to.eq(ResourceOperation.NOOP);
|
|
260
|
+
})
|
|
261
|
+
|
|
229
262
|
it('Doesn\'t filters array parameters if filtering is disabled', async () => {
|
|
230
263
|
const resource = new class extends TestResource {
|
|
231
264
|
getSettings(): ResourceSettings<any> {
|
package/src/plan/plan.ts
CHANGED
|
@@ -96,7 +96,7 @@ export class Plan<T extends StringIndexedObject> {
|
|
|
96
96
|
isStateful
|
|
97
97
|
});
|
|
98
98
|
|
|
99
|
-
|
|
99
|
+
let filteredCurrentParameters = Plan.filterCurrentParams<T>({
|
|
100
100
|
desired,
|
|
101
101
|
current,
|
|
102
102
|
state,
|
|
@@ -104,6 +104,18 @@ export class Plan<T extends StringIndexedObject> {
|
|
|
104
104
|
isStateful
|
|
105
105
|
});
|
|
106
106
|
|
|
107
|
+
// A filtered result with at least one key, where every value is absent (empty array/object, null,
|
|
108
|
+
// undefined), carries no actionable state — treat it the same as no current parameters at all.
|
|
109
|
+
// This matters for resources managing an internal array (e.g. multiple declarations in one
|
|
110
|
+
// resource instance) where destroy-time filtering can legitimately narrow the array down to []
|
|
111
|
+
// once every declared item has been removed, without the resource itself disappearing from
|
|
112
|
+
// filteredCurrentParams. An empty object ({}, zero keys) is intentionally excluded — per the
|
|
113
|
+
// refresh() contract, {} means "exists with no state to track", distinct from "doesn't exist".
|
|
114
|
+
const filteredEntries = filteredCurrentParameters ? Object.values(filteredCurrentParameters) : [];
|
|
115
|
+
if (filteredCurrentParameters && filteredEntries.length > 0 && filteredEntries.every((v) => ChangeSet.isAbsent(v))) {
|
|
116
|
+
filteredCurrentParameters = null;
|
|
117
|
+
}
|
|
118
|
+
|
|
107
119
|
// Empty
|
|
108
120
|
if (!filteredCurrentParameters && !desired) {
|
|
109
121
|
return new Plan(
|
|
@@ -21,7 +21,10 @@ EventEmitter.defaultMaxListeners = 1000;
|
|
|
21
21
|
*/
|
|
22
22
|
export class BackgroundPty implements IPty {
|
|
23
23
|
private historyIgnore = Utils.getShell() === Shell.ZSH ? { HISTORY_IGNORE: '*' } : { HISTIGNORE: '*' };
|
|
24
|
-
|
|
24
|
+
// Login + interactive (-l -i) so the user's rc files are sourced. GUI launches
|
|
25
|
+
// (e.g. the Codify desktop app) do not inherit env like TART_HOME / PATH additions
|
|
26
|
+
// from the parent process; these live in ~/.zshrc / ~/.zprofile.
|
|
27
|
+
private basePty = pty.spawn(this.getDefaultShell(), ['-l', '-i'], {
|
|
25
28
|
env: { ...process.env, ...this.historyIgnore },
|
|
26
29
|
cols: 10_000, // Set to a really large value to prevent wrapping
|
|
27
30
|
name: nanoid(6),
|
|
@@ -156,6 +159,6 @@ export class BackgroundPty implements IPty {
|
|
|
156
159
|
}
|
|
157
160
|
|
|
158
161
|
private getDefaultShell(): string {
|
|
159
|
-
return
|
|
162
|
+
return Utils.getDefaultShell();
|
|
160
163
|
}
|
|
161
164
|
}
|
|
@@ -92,7 +92,11 @@ export class SequentialPty implements IPty {
|
|
|
92
92
|
// the pty waiting for interactive input. Can't be disabled via env var; must unset the options explicitly.
|
|
93
93
|
const disableAutocorrect = Utils.getShell() === Shell.ZSH ? 'unsetopt CORRECT CORRECT_ALL 2>/dev/null; ' : '';
|
|
94
94
|
const wrappedCmd = `${disableAutocorrect}${cmd}`;
|
|
95
|
-
|
|
95
|
+
// Use a login + interactive shell (-l -i) so the user's rc files are sourced.
|
|
96
|
+
// This matters for GUI launches (e.g. the Codify desktop app) where env vars
|
|
97
|
+
// like TART_HOME and PATH additions live in ~/.zshrc/~/.zprofile and are not
|
|
98
|
+
// inherited from the parent process.
|
|
99
|
+
const args = options?.interactive ? ['-l', '-i', '-c', wrappedCmd] : ['-c', wrappedCmd]
|
|
96
100
|
|
|
97
101
|
// Run the command in a pty for interactivity
|
|
98
102
|
const mPty = pty.spawn(this.getDefaultShell(), args, {
|
|
@@ -175,6 +179,6 @@ export class SequentialPty implements IPty {
|
|
|
175
179
|
}
|
|
176
180
|
|
|
177
181
|
private getDefaultShell(): string {
|
|
178
|
-
return
|
|
182
|
+
return Utils.getDefaultShell();
|
|
179
183
|
}
|
|
180
184
|
}
|
package/src/utils/index.ts
CHANGED
|
@@ -132,6 +132,20 @@ export const Utils = {
|
|
|
132
132
|
return undefined;
|
|
133
133
|
},
|
|
134
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Resolves the shell binary to launch commands with. `process.env.SHELL` is
|
|
137
|
+
* only set when Codify is launched from a terminal — GUI launches (e.g. the
|
|
138
|
+
* Codify desktop app via launchd) do not export it, in which case
|
|
139
|
+
* `node-pty` would fall back to `/bin/sh`, which does not source the user's
|
|
140
|
+
* `~/.zshrc`/`~/.bash_profile`. That drops user env (e.g. TART_HOME, PATH
|
|
141
|
+
* additions), breaking resource refresh. Fall back to the login shell from
|
|
142
|
+
* the passwd database (os.userInfo().shell) so we always use the user's real
|
|
143
|
+
* shell regardless of how Codify was started.
|
|
144
|
+
*/
|
|
145
|
+
getDefaultShell(): string {
|
|
146
|
+
return process.env.SHELL || os.userInfo().shell || '/bin/zsh';
|
|
147
|
+
},
|
|
148
|
+
|
|
135
149
|
|
|
136
150
|
getPrimaryShellRc(): string {
|
|
137
151
|
return this.getShellRcFiles()[0];
|
|
@@ -244,7 +258,10 @@ Brew can be installed using Codify:
|
|
|
244
258
|
if (brewOpts?.adopt) flags.push('--adopt');
|
|
245
259
|
if (brewOpts?.flags) flags.push(...brewOpts.flags);
|
|
246
260
|
const flagStr = flags.length > 0 ? `${flags.join(' ')} ` : '';
|
|
247
|
-
|
|
261
|
+
// Redirect stdin from /dev/null so Homebrew's ask.rb detects a non-TTY stdin
|
|
262
|
+
// and skips any [y/n] confirmation prompts (e.g. "Do you want to proceed with the installation?").
|
|
263
|
+
// NONINTERACTIVE=1 alone is not sufficient — Homebrew's prompt only checks tty?, not that var.
|
|
264
|
+
await $.spawn(`brew install ${flagStr}${packageName} < /dev/null`, { interactive: true, env: { HOMEBREW_NO_AUTO_UPDATE: 1, NONINTERACTIVE: 1 } });
|
|
248
265
|
return;
|
|
249
266
|
}
|
|
250
267
|
|
|
@@ -350,7 +367,7 @@ Brew can be installed using Codify:
|
|
|
350
367
|
const flagStr = flags.length > 0 ? `${flags.join(' ')} ` : '';
|
|
351
368
|
const { status } = await $.spawnSafe(`brew uninstall ${flagStr}${packageName}`, {
|
|
352
369
|
interactive: true,
|
|
353
|
-
env: { HOMEBREW_NO_AUTO_UPDATE: 1,
|
|
370
|
+
env: { HOMEBREW_NO_AUTO_UPDATE: 1, NONINTERACTIVE: 1 }
|
|
354
371
|
});
|
|
355
372
|
return status === SpawnStatus.SUCCESS;
|
|
356
373
|
}
|