@codifycli/plugin-core 1.2.5-beta.2 → 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/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/utils/index.d.ts +11 -0
- package/dist/utils/index.js +17 -1
- 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 +18 -1
package/dist/plan/plan.js
CHANGED
|
@@ -55,13 +55,24 @@ export class Plan {
|
|
|
55
55
|
settings,
|
|
56
56
|
isStateful
|
|
57
57
|
});
|
|
58
|
-
|
|
58
|
+
let filteredCurrentParameters = Plan.filterCurrentParams({
|
|
59
59
|
desired,
|
|
60
60
|
current,
|
|
61
61
|
state,
|
|
62
62
|
settings,
|
|
63
63
|
isStateful
|
|
64
64
|
});
|
|
65
|
+
// A filtered result with at least one key, where every value is absent (empty array/object, null,
|
|
66
|
+
// undefined), carries no actionable state — treat it the same as no current parameters at all.
|
|
67
|
+
// This matters for resources managing an internal array (e.g. multiple declarations in one
|
|
68
|
+
// resource instance) where destroy-time filtering can legitimately narrow the array down to []
|
|
69
|
+
// once every declared item has been removed, without the resource itself disappearing from
|
|
70
|
+
// filteredCurrentParams. An empty object ({}, zero keys) is intentionally excluded — per the
|
|
71
|
+
// refresh() contract, {} means "exists with no state to track", distinct from "doesn't exist".
|
|
72
|
+
const filteredEntries = filteredCurrentParameters ? Object.values(filteredCurrentParameters) : [];
|
|
73
|
+
if (filteredCurrentParameters && filteredEntries.length > 0 && filteredEntries.every((v) => ChangeSet.isAbsent(v))) {
|
|
74
|
+
filteredCurrentParameters = null;
|
|
75
|
+
}
|
|
65
76
|
// Empty
|
|
66
77
|
if (!filteredCurrentParameters && !desired) {
|
|
67
78
|
return new Plan(uuidV4(), ChangeSet.empty(), core, isStateful);
|
|
@@ -18,7 +18,10 @@ EventEmitter.defaultMaxListeners = 1000;
|
|
|
18
18
|
*/
|
|
19
19
|
export class BackgroundPty {
|
|
20
20
|
historyIgnore = Utils.getShell() === Shell.ZSH ? { HISTORY_IGNORE: '*' } : { HISTIGNORE: '*' };
|
|
21
|
-
|
|
21
|
+
// Login + interactive (-l -i) so the user's rc files are sourced. GUI launches
|
|
22
|
+
// (e.g. the Codify desktop app) do not inherit env like TART_HOME / PATH additions
|
|
23
|
+
// from the parent process; these live in ~/.zshrc / ~/.zprofile.
|
|
24
|
+
basePty = pty.spawn(this.getDefaultShell(), ['-l', '-i'], {
|
|
22
25
|
env: { ...process.env, ...this.historyIgnore },
|
|
23
26
|
cols: 10_000, // Set to a really large value to prevent wrapping
|
|
24
27
|
name: nanoid(6),
|
|
@@ -128,6 +131,6 @@ export class BackgroundPty {
|
|
|
128
131
|
return [];
|
|
129
132
|
}
|
|
130
133
|
getDefaultShell() {
|
|
131
|
-
return
|
|
134
|
+
return Utils.getDefaultShell();
|
|
132
135
|
}
|
|
133
136
|
}
|
|
@@ -70,7 +70,11 @@ export class SequentialPty {
|
|
|
70
70
|
// the pty waiting for interactive input. Can't be disabled via env var; must unset the options explicitly.
|
|
71
71
|
const disableAutocorrect = Utils.getShell() === Shell.ZSH ? 'unsetopt CORRECT CORRECT_ALL 2>/dev/null; ' : '';
|
|
72
72
|
const wrappedCmd = `${disableAutocorrect}${cmd}`;
|
|
73
|
-
|
|
73
|
+
// Use a login + interactive shell (-l -i) so the user's rc files are sourced.
|
|
74
|
+
// This matters for GUI launches (e.g. the Codify desktop app) where env vars
|
|
75
|
+
// like TART_HOME and PATH additions live in ~/.zshrc/~/.zprofile and are not
|
|
76
|
+
// inherited from the parent process.
|
|
77
|
+
const args = options?.interactive ? ['-l', '-i', '-c', wrappedCmd] : ['-c', wrappedCmd];
|
|
74
78
|
// Run the command in a pty for interactivity
|
|
75
79
|
const mPty = pty.spawn(this.getDefaultShell(), args, {
|
|
76
80
|
...options,
|
|
@@ -134,6 +138,6 @@ export class SequentialPty {
|
|
|
134
138
|
});
|
|
135
139
|
}
|
|
136
140
|
getDefaultShell() {
|
|
137
|
-
return
|
|
141
|
+
return Utils.getDefaultShell();
|
|
138
142
|
}
|
|
139
143
|
}
|
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());
|
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
|
|