@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,76 @@
1
+ export class ResourceOptionsParser {
2
+ options;
3
+ constructor(options) {
4
+ this.options = options;
5
+ }
6
+ get statefulParameters() {
7
+ const statefulParameters = Object.entries(this.options.parameterOptions ?? {})
8
+ .filter(([, p]) => p?.hasOwnProperty('statefulParameter'))
9
+ .map(([k, v]) => [k, v])
10
+ .map(([k, v]) => [k, v.statefulParameter]);
11
+ return new Map(statefulParameters);
12
+ }
13
+ get transformParameters() {
14
+ const transformParameters = Object.entries(this.options.parameterOptions ?? {})
15
+ .filter(([, p]) => p?.hasOwnProperty('transformParameter'))
16
+ .map(([k, v]) => [k, v])
17
+ .map(([k, v]) => [k, v.transformParameter]);
18
+ return new Map(transformParameters);
19
+ }
20
+ get resourceParameters() {
21
+ const resourceParameters = Object.entries(this.options.parameterOptions ?? {})
22
+ .filter(([, p]) => !(p?.hasOwnProperty('statefulParameter') || p?.hasOwnProperty('transformParameter')))
23
+ .map(([k, v]) => [k, v]);
24
+ return new Map(resourceParameters);
25
+ }
26
+ get changeSetParameterOptions() {
27
+ const resourceParameters = Object.fromEntries([...this.resourceParameters.entries()]
28
+ .map(([name, value]) => ([name, { ...value, isStatefulParameter: false }])));
29
+ const statefulParameters = [...this.statefulParameters.entries()]
30
+ ?.reduce((obj, sp) => ({
31
+ ...obj,
32
+ [sp[0]]: {
33
+ ...sp[1].options,
34
+ isStatefulParameter: true,
35
+ }
36
+ }), {});
37
+ return {
38
+ ...resourceParameters,
39
+ ...statefulParameters,
40
+ };
41
+ }
42
+ get defaultValues() {
43
+ if (!this.options.parameterOptions) {
44
+ return {};
45
+ }
46
+ return Object.fromEntries([...this.resourceParameters.entries()]
47
+ .filter(([, rp]) => rp.default !== undefined)
48
+ .map(([name, rp]) => [name, rp.default]));
49
+ }
50
+ get statefulParameterOrder() {
51
+ const entries = Object.entries(this.options.parameterOptions ?? {})
52
+ .filter(([, v]) => v?.hasOwnProperty('statefulParameter'))
53
+ .map(([k, v]) => [k, v]);
54
+ const orderedEntries = entries.filter(([, v]) => v.order !== undefined);
55
+ const unorderedEntries = entries.filter(([, v]) => v.order === undefined);
56
+ orderedEntries.sort((a, b) => a[1].order - b[1].order);
57
+ const resultArray = [
58
+ ...orderedEntries.map(([k]) => k),
59
+ ...unorderedEntries.map(([k]) => k)
60
+ ];
61
+ return new Map(resultArray.map((key, idx) => [key, idx]));
62
+ }
63
+ get transformParameterOrder() {
64
+ const entries = Object.entries(this.options.parameterOptions ?? {})
65
+ .filter(([, v]) => v?.hasOwnProperty('transformParameter'))
66
+ .map(([k, v]) => [k, v]);
67
+ const orderedEntries = entries.filter(([, v]) => v.order !== undefined);
68
+ const unorderedEntries = entries.filter(([, v]) => v.order === undefined);
69
+ orderedEntries.sort((a, b) => a[1].order - b[1].order);
70
+ const resultArray = [
71
+ ...orderedEntries.map(([k]) => k),
72
+ ...unorderedEntries.map(([k]) => k)
73
+ ];
74
+ return new Map(resultArray.map((key, idx) => [key, idx]));
75
+ }
76
+ }
@@ -0,0 +1,11 @@
1
+ export type ErrorMessage = string;
2
+ export interface ResourceParameterOptions {
3
+ default?: unknown;
4
+ isEqual?: (desired: any, current: any) => boolean;
5
+ modifyOnChange?: boolean;
6
+ }
7
+ export interface ResourceDefinition {
8
+ [x: string]: {
9
+ type: string;
10
+ };
11
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,42 @@
1
+ import { Ajv, ValidateFunction } from 'ajv';
2
+ import { ResourceConfig, StringIndexedObject, ValidateResponseData } from 'codify-schemas';
3
+ import { ParameterChange } from './change-set.js';
4
+ import { Plan } from './plan.js';
5
+ import { CreatePlan, DestroyPlan, ModifyPlan, ParameterOptions } from './plan-types.js';
6
+ import { ResourceOptions } from './resource-options.js';
7
+ import { ResourceParameterOptions } from './resource-types.js';
8
+ import { StatefulParameter } from './stateful-parameter.js';
9
+ import { TransformParameter } from './transform-parameter.js';
10
+ export declare abstract class Resource<T extends StringIndexedObject> {
11
+ readonly typeId: string;
12
+ readonly statefulParameters: Map<keyof T, StatefulParameter<T, T[keyof T]>>;
13
+ readonly transformParameters: Map<keyof T, TransformParameter<T>>;
14
+ readonly resourceParameters: Map<keyof T, ResourceParameterOptions>;
15
+ readonly statefulParameterOrder: Map<keyof T, number>;
16
+ readonly transformParameterOrder: Map<keyof T, number>;
17
+ readonly dependencies: string[];
18
+ readonly parameterOptions: Record<keyof T, ParameterOptions>;
19
+ readonly options: ResourceOptions<T>;
20
+ readonly defaultValues: Partial<Record<keyof T, unknown>>;
21
+ protected ajv?: Ajv;
22
+ protected schemaValidator?: ValidateFunction;
23
+ protected constructor(options: ResourceOptions<T>);
24
+ onInitialize(): Promise<void>;
25
+ validate(parameters: Partial<T>, resourceMetaData: ResourceConfig): Promise<ValidateResponseData['resourceValidations'][0]>;
26
+ plan(desiredConfig: Partial<T> & ResourceConfig | null, currentConfig?: Partial<T> & ResourceConfig | null, statefulMode?: boolean): Promise<Plan<T>>;
27
+ apply(plan: Plan<T>): Promise<void>;
28
+ private _applyCreate;
29
+ private _applyModify;
30
+ private _applyDestroy;
31
+ private validateRefreshResults;
32
+ private applyTransformParameters;
33
+ private addDefaultValues;
34
+ private refreshNonStatefulParameters;
35
+ private refreshStatefulParameters;
36
+ private validatePlanInputs;
37
+ customValidation(parameters: Partial<T>): Promise<void>;
38
+ abstract refresh(parameters: Partial<T>): Promise<Partial<T> | null>;
39
+ abstract applyCreate(plan: CreatePlan<T>): Promise<void>;
40
+ applyModify(pc: ParameterChange<T>, plan: ModifyPlan<T>): Promise<void>;
41
+ abstract applyDestroy(plan: DestroyPlan<T>): Promise<void>;
42
+ }
@@ -0,0 +1,303 @@
1
+ import { Ajv } from 'ajv';
2
+ import { ParameterOperation, ResourceOperation, } from 'codify-schemas';
3
+ import { setsEqual, splitUserConfig } from '../utils/utils.js';
4
+ import { Plan } from './plan.js';
5
+ import { ResourceOptionsParser } from './resource-options.js';
6
+ export class Resource {
7
+ typeId;
8
+ statefulParameters;
9
+ transformParameters;
10
+ resourceParameters;
11
+ statefulParameterOrder;
12
+ transformParameterOrder;
13
+ dependencies;
14
+ parameterOptions;
15
+ options;
16
+ defaultValues;
17
+ ajv;
18
+ schemaValidator;
19
+ constructor(options) {
20
+ this.typeId = options.type;
21
+ this.dependencies = options.dependencies ?? [];
22
+ this.options = options;
23
+ if (this.options.schema) {
24
+ this.ajv = new Ajv({
25
+ allErrors: true,
26
+ strict: true,
27
+ strictRequired: false,
28
+ });
29
+ this.schemaValidator = this.ajv.compile(this.options.schema);
30
+ }
31
+ const parser = new ResourceOptionsParser(options);
32
+ this.statefulParameters = parser.statefulParameters;
33
+ this.transformParameters = parser.transformParameters;
34
+ this.resourceParameters = parser.resourceParameters;
35
+ this.parameterOptions = parser.changeSetParameterOptions;
36
+ this.defaultValues = parser.defaultValues;
37
+ this.statefulParameterOrder = parser.statefulParameterOrder;
38
+ this.transformParameterOrder = parser.transformParameterOrder;
39
+ }
40
+ async onInitialize() { }
41
+ async validate(parameters, resourceMetaData) {
42
+ if (this.schemaValidator) {
43
+ const isValid = this.schemaValidator(parameters);
44
+ if (!isValid) {
45
+ return {
46
+ isValid: false,
47
+ resourceName: resourceMetaData.name,
48
+ resourceType: resourceMetaData.type,
49
+ schemaValidationErrors: this.schemaValidator?.errors ?? [],
50
+ };
51
+ }
52
+ }
53
+ let isValid = true;
54
+ let customValidationErrorMessage;
55
+ try {
56
+ await this.customValidation(parameters);
57
+ }
58
+ catch (error) {
59
+ isValid = false;
60
+ customValidationErrorMessage = error.message;
61
+ }
62
+ if (!isValid) {
63
+ return {
64
+ customValidationErrorMessage,
65
+ isValid: false,
66
+ resourceName: resourceMetaData.name,
67
+ resourceType: resourceMetaData.type,
68
+ schemaValidationErrors: this.schemaValidator?.errors ?? [],
69
+ };
70
+ }
71
+ return {
72
+ isValid: true,
73
+ resourceName: resourceMetaData.name,
74
+ resourceType: resourceMetaData.type,
75
+ schemaValidationErrors: [],
76
+ };
77
+ }
78
+ async plan(desiredConfig, currentConfig = null, statefulMode = false) {
79
+ this.validatePlanInputs(desiredConfig, currentConfig, statefulMode);
80
+ const planOptions = {
81
+ parameterOptions: this.parameterOptions,
82
+ statefulMode,
83
+ };
84
+ this.addDefaultValues(desiredConfig);
85
+ await this.applyTransformParameters(desiredConfig);
86
+ const parsedConfig = new ConfigParser(desiredConfig, currentConfig, this.statefulParameters, this.transformParameters);
87
+ const { desiredParameters, nonStatefulParameters, resourceMetadata, statefulParameters, } = parsedConfig;
88
+ const currentParameters = await this.refreshNonStatefulParameters(nonStatefulParameters);
89
+ if (currentParameters === null || currentParameters === undefined) {
90
+ return Plan.create(desiredParameters, null, resourceMetadata, planOptions);
91
+ }
92
+ const statefulCurrentParameters = await this.refreshStatefulParameters(statefulParameters, planOptions.statefulMode);
93
+ return Plan.create(desiredParameters, { ...currentParameters, ...statefulCurrentParameters }, resourceMetadata, planOptions);
94
+ }
95
+ async apply(plan) {
96
+ if (plan.getResourceType() !== this.typeId) {
97
+ throw new Error(`Internal error: Plan set to wrong resource during apply. Expected ${this.typeId} but got: ${plan.getResourceType()}`);
98
+ }
99
+ switch (plan.changeSet.operation) {
100
+ case ResourceOperation.CREATE: {
101
+ return this._applyCreate(plan);
102
+ }
103
+ case ResourceOperation.MODIFY: {
104
+ return this._applyModify(plan);
105
+ }
106
+ case ResourceOperation.RECREATE: {
107
+ await this._applyDestroy(plan);
108
+ return this._applyCreate(plan);
109
+ }
110
+ case ResourceOperation.DESTROY: {
111
+ return this._applyDestroy(plan);
112
+ }
113
+ }
114
+ }
115
+ async _applyCreate(plan) {
116
+ await this.applyCreate(plan);
117
+ const statefulParameterChanges = plan.changeSet.parameterChanges
118
+ .filter((pc) => this.statefulParameters.has(pc.name))
119
+ .sort((a, b) => this.statefulParameterOrder.get(a.name) - this.statefulParameterOrder.get(b.name));
120
+ for (const parameterChange of statefulParameterChanges) {
121
+ const statefulParameter = this.statefulParameters.get(parameterChange.name);
122
+ await statefulParameter.applyAdd(parameterChange.newValue, plan);
123
+ }
124
+ }
125
+ async _applyModify(plan) {
126
+ const parameterChanges = plan
127
+ .changeSet
128
+ .parameterChanges
129
+ .filter((c) => c.operation !== ParameterOperation.NOOP);
130
+ const statelessParameterChanges = parameterChanges
131
+ .filter((pc) => !this.statefulParameters.has(pc.name));
132
+ for (const pc of statelessParameterChanges) {
133
+ await this.applyModify(pc, plan);
134
+ }
135
+ const statefulParameterChanges = parameterChanges
136
+ .filter((pc) => this.statefulParameters.has(pc.name))
137
+ .sort((a, b) => this.statefulParameterOrder.get(a.name) - this.statefulParameterOrder.get(b.name));
138
+ for (const parameterChange of statefulParameterChanges) {
139
+ const statefulParameter = this.statefulParameters.get(parameterChange.name);
140
+ switch (parameterChange.operation) {
141
+ case ParameterOperation.ADD: {
142
+ await statefulParameter.applyAdd(parameterChange.newValue, plan);
143
+ break;
144
+ }
145
+ case ParameterOperation.MODIFY: {
146
+ await statefulParameter.applyModify(parameterChange.newValue, parameterChange.previousValue, false, plan);
147
+ break;
148
+ }
149
+ case ParameterOperation.REMOVE: {
150
+ await statefulParameter.applyRemove(parameterChange.previousValue, plan);
151
+ break;
152
+ }
153
+ }
154
+ }
155
+ }
156
+ async _applyDestroy(plan) {
157
+ if (this.options.callStatefulParameterRemoveOnDestroy) {
158
+ const statefulParameterChanges = plan.changeSet.parameterChanges
159
+ .filter((pc) => this.statefulParameters.has(pc.name))
160
+ .sort((a, b) => this.statefulParameterOrder.get(a.name) - this.statefulParameterOrder.get(b.name));
161
+ for (const parameterChange of statefulParameterChanges) {
162
+ const statefulParameter = this.statefulParameters.get(parameterChange.name);
163
+ await statefulParameter.applyRemove(parameterChange.previousValue, plan);
164
+ }
165
+ }
166
+ await this.applyDestroy(plan);
167
+ }
168
+ validateRefreshResults(refresh, desired) {
169
+ if (!refresh) {
170
+ return;
171
+ }
172
+ const desiredKeys = new Set(Object.keys(refresh));
173
+ const refreshKeys = new Set(Object.keys(refresh));
174
+ if (!setsEqual(desiredKeys, refreshKeys)) {
175
+ throw new Error(`Resource ${this.typeId}
176
+ refresh() must return back exactly the keys that were provided
177
+ Missing: ${[...desiredKeys].filter((k) => !refreshKeys.has(k))};
178
+ Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`);
179
+ }
180
+ }
181
+ async applyTransformParameters(desired) {
182
+ if (!desired) {
183
+ return;
184
+ }
185
+ const transformParameters = [...this.transformParameters.entries()]
186
+ .sort(([keyA], [keyB]) => this.transformParameterOrder.get(keyA) - this.transformParameterOrder.get(keyB));
187
+ for (const [key, transformParameter] of transformParameters) {
188
+ if (desired[key] === undefined) {
189
+ continue;
190
+ }
191
+ const transformedValue = await transformParameter.transform(desired[key]);
192
+ if (Object.keys(transformedValue).some((k) => desired[k] !== undefined)) {
193
+ throw new Error(`Transform parameter ${key} is attempting to override existing values ${JSON.stringify(transformedValue, null, 2)}`);
194
+ }
195
+ delete desired[key];
196
+ for (const [tvKey, tvValue] of Object.entries(transformedValue)) {
197
+ desired[tvKey] = tvValue;
198
+ }
199
+ }
200
+ }
201
+ addDefaultValues(desired) {
202
+ if (!desired) {
203
+ return;
204
+ }
205
+ for (const [key, defaultValue] of Object.entries(this.defaultValues)) {
206
+ if (defaultValue !== undefined && desired[key] === undefined) {
207
+ desired[key] = defaultValue;
208
+ }
209
+ }
210
+ }
211
+ async refreshNonStatefulParameters(resourceParameters) {
212
+ const currentParameters = await this.refresh(resourceParameters);
213
+ this.validateRefreshResults(currentParameters, resourceParameters);
214
+ return currentParameters;
215
+ }
216
+ async refreshStatefulParameters(statefulParametersConfig, isStatefulMode) {
217
+ const currentParameters = {};
218
+ const sortedEntries = Object.entries(statefulParametersConfig)
219
+ .sort(([key1], [key2]) => this.statefulParameterOrder.get(key1) - this.statefulParameterOrder.get(key2));
220
+ for (const [key, desiredValue] of sortedEntries) {
221
+ const statefulParameter = this.statefulParameters.get(key);
222
+ if (!statefulParameter) {
223
+ throw new Error(`Stateful parameter ${key} was not found`);
224
+ }
225
+ let currentValue = await statefulParameter.refresh(desiredValue ?? null);
226
+ if (Array.isArray(currentValue)
227
+ && Array.isArray(desiredValue)
228
+ && !isStatefulMode
229
+ && !statefulParameter.options.disableStatelessModeArrayFiltering) {
230
+ currentValue = currentValue.filter((c) => desiredValue?.some((d) => {
231
+ const parameterOptions = statefulParameter.options;
232
+ if (parameterOptions && parameterOptions.isElementEqual) {
233
+ return parameterOptions.isElementEqual(d, c);
234
+ }
235
+ return d === c;
236
+ }));
237
+ }
238
+ currentParameters[key] = currentValue;
239
+ }
240
+ return currentParameters;
241
+ }
242
+ validatePlanInputs(desired, current, statefulMode) {
243
+ if (!desired && !current) {
244
+ throw new Error('Desired config and current config cannot both be missing');
245
+ }
246
+ if (!statefulMode && !desired) {
247
+ throw new Error('Desired config must be provided in non-stateful mode');
248
+ }
249
+ }
250
+ async customValidation(parameters) { }
251
+ ;
252
+ async applyModify(pc, plan) { }
253
+ ;
254
+ }
255
+ class ConfigParser {
256
+ desiredConfig;
257
+ currentConfig;
258
+ statefulParametersMap;
259
+ transformParametersMap;
260
+ constructor(desiredConfig, currentConfig, statefulParameters, transformParameters) {
261
+ this.desiredConfig = desiredConfig;
262
+ this.currentConfig = currentConfig;
263
+ this.statefulParametersMap = statefulParameters;
264
+ this.transformParametersMap = transformParameters;
265
+ }
266
+ get resourceMetadata() {
267
+ const desiredMetadata = this.desiredConfig ? splitUserConfig(this.desiredConfig).resourceMetadata : undefined;
268
+ const currentMetadata = this.currentConfig ? splitUserConfig(this.currentConfig).resourceMetadata : undefined;
269
+ if (!desiredMetadata && !currentMetadata) {
270
+ throw new Error(`Unable to parse resource metadata from ${this.desiredConfig}, ${this.currentConfig}`);
271
+ }
272
+ if (currentMetadata && desiredMetadata && (Object.keys(desiredMetadata).length !== Object.keys(currentMetadata).length
273
+ || Object.entries(desiredMetadata).some(([key, value]) => currentMetadata[key] !== value))) {
274
+ throw new Error(`The metadata for the current config does not match the desired config.
275
+ Desired metadata:
276
+ ${JSON.stringify(desiredMetadata, null, 2)}
277
+
278
+ Current metadata:
279
+ ${JSON.stringify(currentMetadata, null, 2)}`);
280
+ }
281
+ return desiredMetadata ?? currentMetadata;
282
+ }
283
+ get desiredParameters() {
284
+ if (!this.desiredConfig) {
285
+ return null;
286
+ }
287
+ const { parameters } = splitUserConfig(this.desiredConfig);
288
+ return parameters;
289
+ }
290
+ get parameters() {
291
+ const desiredParameters = this.desiredConfig ? splitUserConfig(this.desiredConfig).parameters : undefined;
292
+ const currentParameters = this.currentConfig ? splitUserConfig(this.currentConfig).parameters : undefined;
293
+ return { ...desiredParameters, ...currentParameters };
294
+ }
295
+ get nonStatefulParameters() {
296
+ const { parameters } = this;
297
+ return Object.fromEntries(Object.entries(parameters).filter(([key]) => !(this.statefulParametersMap.has(key) || this.transformParametersMap.has(key))));
298
+ }
299
+ get statefulParameters() {
300
+ const { parameters } = this;
301
+ return Object.fromEntries(Object.entries(parameters).filter(([key]) => this.statefulParametersMap.has(key)));
302
+ }
303
+ }
@@ -0,0 +1,29 @@
1
+ import { Plan } from './plan.js';
2
+ import { StringIndexedObject } from 'codify-schemas';
3
+ export interface StatefulParameterOptions<V> {
4
+ isEqual?: (desired: any, current: any) => boolean;
5
+ disableStatelessModeArrayFiltering?: boolean;
6
+ default?: V;
7
+ }
8
+ export interface ArrayStatefulParameterOptions<V> extends StatefulParameterOptions<V> {
9
+ isEqual?: (desired: any[], current: any[]) => boolean;
10
+ isElementEqual?: (desired: any, current: any) => boolean;
11
+ }
12
+ export declare abstract class StatefulParameter<T extends StringIndexedObject, V extends T[keyof T]> {
13
+ readonly options: StatefulParameterOptions<V>;
14
+ constructor(options?: StatefulParameterOptions<V>);
15
+ abstract refresh(desired: V | null): Promise<V | null>;
16
+ abstract applyAdd(valueToAdd: V, plan: Plan<T>): Promise<void>;
17
+ abstract applyModify(newValue: V, previousValue: V, allowDeletes: boolean, plan: Plan<T>): Promise<void>;
18
+ abstract applyRemove(valueToRemove: V, plan: Plan<T>): Promise<void>;
19
+ }
20
+ export declare abstract class ArrayStatefulParameter<T extends StringIndexedObject, V> extends StatefulParameter<T, any> {
21
+ options: ArrayStatefulParameterOptions<V>;
22
+ constructor(options?: ArrayStatefulParameterOptions<V>);
23
+ applyAdd(valuesToAdd: V[], plan: Plan<T>): Promise<void>;
24
+ applyModify(newValues: V[], previousValues: V[], allowDeletes: boolean, plan: Plan<T>): Promise<void>;
25
+ applyRemove(valuesToRemove: V[], plan: Plan<T>): Promise<void>;
26
+ abstract refresh(desired: V[] | null): Promise<V[] | null>;
27
+ abstract applyAddItem(item: V, plan: Plan<T>): Promise<void>;
28
+ abstract applyRemoveItem(item: V, plan: Plan<T>): Promise<void>;
29
+ }
@@ -0,0 +1,46 @@
1
+ export class StatefulParameter {
2
+ options;
3
+ constructor(options = {}) {
4
+ this.options = options;
5
+ }
6
+ }
7
+ export class ArrayStatefulParameter extends StatefulParameter {
8
+ options;
9
+ constructor(options = {}) {
10
+ super(options);
11
+ this.options = options;
12
+ }
13
+ async applyAdd(valuesToAdd, plan) {
14
+ for (const value of valuesToAdd) {
15
+ await this.applyAddItem(value, plan);
16
+ }
17
+ }
18
+ async applyModify(newValues, previousValues, allowDeletes, plan) {
19
+ const options = this.options;
20
+ const valuesToAdd = newValues.filter((n) => !previousValues.some((p) => {
21
+ if (options.isElementEqual) {
22
+ return options.isElementEqual(n, p);
23
+ }
24
+ return n === p;
25
+ }));
26
+ const valuesToRemove = previousValues.filter((p) => !newValues.some((n) => {
27
+ if (options.isElementEqual) {
28
+ return options.isElementEqual(n, p);
29
+ }
30
+ return n === p;
31
+ }));
32
+ for (const value of valuesToAdd) {
33
+ await this.applyAddItem(value, plan);
34
+ }
35
+ if (allowDeletes) {
36
+ for (const value of valuesToRemove) {
37
+ await this.applyRemoveItem(value, plan);
38
+ }
39
+ }
40
+ }
41
+ async applyRemove(valuesToRemove, plan) {
42
+ for (const value of valuesToRemove) {
43
+ await this.applyRemoveItem(value, plan);
44
+ }
45
+ }
46
+ }
@@ -0,0 +1,4 @@
1
+ import { StringIndexedObject } from 'codify-schemas';
2
+ export declare abstract class TransformParameter<T extends StringIndexedObject> {
3
+ abstract transform(value: any): Promise<Partial<T>>;
4
+ }
@@ -0,0 +1,2 @@
1
+ export class TransformParameter {
2
+ }
@@ -49,6 +49,6 @@ export declare class ChangeSet<T extends StringIndexedObject> {
49
49
  */
50
50
  private static calculateParameterChanges;
51
51
  private static combineResourceOperations;
52
- private static isAbsent;
52
+ static isAbsent(v: unknown): boolean;
53
53
  private static isSame;
54
54
  }
package/dist/plan/plan.js CHANGED
@@ -55,13 +55,24 @@ export class Plan {
55
55
  settings,
56
56
  isStateful
57
57
  });
58
- const filteredCurrentParameters = Plan.filterCurrentParams({
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
- basePty = pty.spawn(this.getDefaultShell(), ['-i'], {
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 process.env.SHELL;
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
- const args = options?.interactive ? ['-i', '-c', wrappedCmd] : ['-c', wrappedCmd];
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 process.env.SHELL;
141
+ return Utils.getDefaultShell();
138
142
  }
139
143
  }
@@ -0,0 +1,2 @@
1
+ declare const _default: import("vite").UserConfig;
2
+ export default _default;
@@ -0,0 +1,11 @@
1
+ import { defaultExclude, defineConfig } from 'vitest/config';
2
+ export default defineConfig({
3
+ test: {
4
+ pool: 'forks',
5
+ fileParallelism: false,
6
+ exclude: [
7
+ ...defaultExclude,
8
+ './src/utils/test-utils.test.ts',
9
+ ]
10
+ },
11
+ });