@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.
Files changed (61) hide show
  1. package/.claude/settings.local.json +13 -0
  2. package/bin/build.js +0 -0
  3. package/dist/bin/build.d.ts +1 -0
  4. package/dist/bin/build.js +80 -0
  5. package/dist/bin/deploy-plugin.d.ts +2 -0
  6. package/dist/bin/deploy-plugin.js +8 -0
  7. package/dist/entities/change-set.d.ts +24 -0
  8. package/dist/entities/change-set.js +152 -0
  9. package/dist/entities/errors.d.ts +4 -0
  10. package/dist/entities/errors.js +7 -0
  11. package/dist/entities/plan-types.d.ts +25 -0
  12. package/dist/entities/plan-types.js +1 -0
  13. package/dist/entities/plan.d.ts +15 -0
  14. package/dist/entities/plan.js +127 -0
  15. package/dist/entities/plugin.d.ts +16 -0
  16. package/dist/entities/plugin.js +80 -0
  17. package/dist/entities/resource-options.d.ts +31 -0
  18. package/dist/entities/resource-options.js +76 -0
  19. package/dist/entities/resource-types.d.ts +11 -0
  20. package/dist/entities/resource-types.js +1 -0
  21. package/dist/entities/resource.d.ts +42 -0
  22. package/dist/entities/resource.js +303 -0
  23. package/dist/entities/stateful-parameter.d.ts +29 -0
  24. package/dist/entities/stateful-parameter.js +46 -0
  25. package/dist/entities/transform-parameter.d.ts +4 -0
  26. package/dist/entities/transform-parameter.js +2 -0
  27. package/dist/plan/change-set.d.ts +1 -1
  28. package/dist/plan/plan.js +12 -1
  29. package/dist/pty/background-pty.js +5 -2
  30. package/dist/pty/seqeuntial-pty.js +6 -2
  31. package/dist/pty/vitest.config.d.ts +2 -0
  32. package/dist/pty/vitest.config.js +11 -0
  33. package/dist/resource/stateful-parameter.d.ts +165 -0
  34. package/dist/resource/stateful-parameter.js +94 -0
  35. package/dist/scripts/deploy.d.ts +1 -0
  36. package/dist/scripts/deploy.js +2 -0
  37. package/dist/test.d.ts +1 -0
  38. package/dist/test.js +5 -0
  39. package/dist/utils/codify-spawn.d.ts +29 -0
  40. package/dist/utils/codify-spawn.js +136 -0
  41. package/dist/utils/index.d.ts +11 -0
  42. package/dist/utils/index.js +18 -2
  43. package/dist/utils/internal-utils.d.ts +12 -0
  44. package/dist/utils/internal-utils.js +74 -0
  45. package/dist/utils/load-resources.d.ts +1 -0
  46. package/dist/utils/load-resources.js +46 -0
  47. package/dist/utils/package-json-utils.d.ts +12 -0
  48. package/dist/utils/package-json-utils.js +34 -0
  49. package/dist/utils/spawn-2.d.ts +5 -0
  50. package/dist/utils/spawn-2.js +7 -0
  51. package/dist/utils/spawn.d.ts +29 -0
  52. package/dist/utils/spawn.js +124 -0
  53. package/dist/utils/utils.d.ts +18 -0
  54. package/dist/utils/utils.js +86 -0
  55. package/package.json +1 -1
  56. package/src/plan/change-set.ts +1 -1
  57. package/src/plan/plan.test.ts +33 -0
  58. package/src/plan/plan.ts +13 -1
  59. package/src/pty/background-pty.ts +5 -2
  60. package/src/pty/seqeuntial-pty.ts +6 -2
  61. 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,5 @@
1
+ import { Shell } from 'zx';
2
+ export declare class ShellContext implements Shell {
3
+ zx: Shell;
4
+ static create(): ShellContext;
5
+ }
@@ -0,0 +1,7 @@
1
+ import { $ } from 'zx';
2
+ export class ShellContext {
3
+ zx = $({ shell: true });
4
+ static create() {
5
+ return new ShellContext();
6
+ }
7
+ }
@@ -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",
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",
@@ -227,7 +227,7 @@ export class ChangeSet<T extends StringIndexedObject> {
227
227
  return orderOfOperations[Math.max(indexPrev, indexNext)];
228
228
  }
229
229
 
230
- private static isAbsent(v: unknown): boolean {
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;
@@ -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
- const filteredCurrentParameters = Plan.filterCurrentParams<T>({
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
- private basePty = pty.spawn(this.getDefaultShell(), ['-i'], {
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 process.env.SHELL!;
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
- const args = options?.interactive ? ['-i', '-c', wrappedCmd] : ['-c', wrappedCmd]
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 process.env.SHELL!;
182
+ return Utils.getDefaultShell();
179
183
  }
180
184
  }
@@ -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
- await $.spawn(`brew install ${flagStr}${packageName}`, { interactive: true, env: { HOMEBREW_NO_AUTO_UPDATE: 1, HOMEBREW_NO_ASK: 1 } });
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, HOMEBREW_NO_ASK: 1 }
370
+ env: { HOMEBREW_NO_AUTO_UPDATE: 1, NONINTERACTIVE: 1 }
354
371
  });
355
372
  return status === SpawnStatus.SUCCESS;
356
373
  }