@codifycli/plugin-core 1.1.0-beta9 → 1.2.0

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 (40) hide show
  1. package/.claude/settings.local.json +9 -1
  2. package/dist/common/apply-notes.d.ts +5 -0
  3. package/dist/common/apply-notes.js +10 -0
  4. package/dist/common/errors.d.ts +2 -1
  5. package/dist/common/errors.js +3 -1
  6. package/dist/index.d.ts +1 -0
  7. package/dist/index.js +1 -0
  8. package/dist/messages/handlers.js +24 -15
  9. package/dist/messages/sender.d.ts +1 -0
  10. package/dist/messages/sender.js +12 -0
  11. package/dist/plan/change-set.js +1 -0
  12. package/dist/plugin/plugin.d.ts +7 -2
  13. package/dist/plugin/plugin.js +14 -7
  14. package/dist/pty/background-pty.d.ts +1 -0
  15. package/dist/pty/background-pty.js +3 -0
  16. package/dist/pty/index.d.ts +3 -1
  17. package/dist/pty/index.js +5 -2
  18. package/dist/pty/seqeuntial-pty.d.ts +4 -0
  19. package/dist/pty/seqeuntial-pty.js +18 -3
  20. package/dist/resource/parsed-resource-settings.js +1 -1
  21. package/dist/utils/file-utils.js +8 -2
  22. package/dist/utils/functions.js +2 -1
  23. package/dist/utils/index.js +15 -1
  24. package/package.json +3 -3
  25. package/src/common/apply-notes.ts +12 -0
  26. package/src/common/errors.ts +3 -1
  27. package/src/index.ts +1 -0
  28. package/src/messages/handlers.ts +24 -16
  29. package/src/messages/sender.ts +21 -1
  30. package/src/plan/change-set.test.ts +46 -0
  31. package/src/plan/change-set.ts +1 -0
  32. package/src/plugin/plugin.ts +22 -8
  33. package/src/pty/background-pty.ts +4 -0
  34. package/src/pty/index.ts +8 -2
  35. package/src/pty/seqeuntial-pty.ts +21 -3
  36. package/src/resource/parsed-resource-settings.ts +1 -1
  37. package/src/utils/file-utils.ts +7 -2
  38. package/src/utils/functions.ts +2 -1
  39. package/src/utils/index.ts +18 -1
  40. package/src/utils/internal-utils.test.ts +1 -0
@@ -1,5 +1,13 @@
1
1
  {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(grep -r \"COMMAND_REQUEST\\\\|PRESS_KEY_TO_CONTINUE_REQUEST\\\\|CODIFY_CREDENTIALS_REQUEST\" /Users/kevinwang/Projects/codify-parent/codify/src --include=\"*.ts\" ! -name \"*.test.ts\")",
5
+ "Read(//Users/kevinwang/Projects/codify-parent/codify-schemas/**)",
6
+ "Bash(npm run *)"
7
+ ]
8
+ },
2
9
  "disabledMcpjsonServers": [
3
- "supabase"
10
+ "supabase",
11
+ "codify"
4
12
  ]
5
13
  }
@@ -0,0 +1,5 @@
1
+ export declare const ApplyNotes: {
2
+ readonly RESTART_REQUIRED: "A system restart is required for changes to take effect.";
3
+ readonly NEW_SHELL_REQUIRED: "Open a new terminal session for the changes to be reflected.";
4
+ readonly sourceShellRc: () => string;
5
+ };
@@ -0,0 +1,10 @@
1
+ import path from 'node:path';
2
+ import { Utils } from '../utils/index.js';
3
+ export const ApplyNotes = {
4
+ RESTART_REQUIRED: 'A system restart is required for changes to take effect.',
5
+ NEW_SHELL_REQUIRED: 'Open a new terminal session for the changes to be reflected.',
6
+ sourceShellRc() {
7
+ const rc = path.basename(Utils.getPrimaryShellRc());
8
+ return `Source '~/${rc}' for the changes to be reflected.`;
9
+ },
10
+ };
@@ -3,6 +3,7 @@ export declare class ApplyValidationError extends Error {
3
3
  resourceType: string;
4
4
  resourceName?: string;
5
5
  plan: Plan<any>;
6
- constructor(plan: Plan<any>);
6
+ logs: string[];
7
+ constructor(plan: Plan<any>, logs?: string[]);
7
8
  private static prettyPrintPlan;
8
9
  }
@@ -2,11 +2,13 @@ export class ApplyValidationError extends Error {
2
2
  resourceType;
3
3
  resourceName;
4
4
  plan;
5
- constructor(plan) {
5
+ logs;
6
+ constructor(plan, logs = []) {
6
7
  super(`Failed to apply changes to resource: "${plan.resourceId}". Additional changes are needed to complete apply.\nChanges remaining:\n${ApplyValidationError.prettyPrintPlan(plan)}`);
7
8
  this.resourceType = plan.coreParameters.type;
8
9
  this.resourceName = plan.coreParameters.name;
9
10
  this.plan = plan;
11
+ this.logs = logs;
10
12
  }
11
13
  static prettyPrintPlan(plan) {
12
14
  const { operation, parameters } = plan.toResponse();
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Plugin } from './plugin/plugin.js';
2
+ export * from './common/apply-notes.js';
2
3
  export * from './errors.js';
3
4
  export * from './messages/sender.js';
4
5
  export * from './plan/change-set.js';
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { MessageHandler } from './messages/handlers.js';
2
+ export * from './common/apply-notes.js';
2
3
  export * from './errors.js';
3
4
  export * from './messages/sender.js';
4
5
  export * from './plan/change-set.js';
@@ -1,6 +1,7 @@
1
1
  import { ApplyRequestDataSchema, EmptyResponseDataSchema, GetResourceInfoRequestDataSchema, GetResourceInfoResponseDataSchema, ImportRequestDataSchema, ImportResponseDataSchema, InitializeRequestDataSchema, InitializeResponseDataSchema, IpcMessageSchema, IpcMessageV2Schema, MatchRequestDataSchema, MatchResponseDataSchema, MessageStatus, PlanRequestDataSchema, PlanResponseDataSchema, ResourceSchema, SetVerbosityRequestDataSchema, ValidateRequestDataSchema, ValidateResponseDataSchema } from '@codifycli/schemas';
2
2
  import { Ajv } from 'ajv';
3
3
  import addFormats from 'ajv-formats';
4
+ import { ApplyValidationError } from '../common/errors.js';
4
5
  import { SudoError } from '../errors.js';
5
6
  const SupportedRequests = {
6
7
  'initialize': {
@@ -113,22 +114,30 @@ export class MessageHandler {
113
114
  }
114
115
  // @ts-expect-error TS2239
115
116
  const cmd = message.cmd + '_Response';
117
+ // @ts-expect-error TS2239
118
+ const requestId = message.requestId || undefined;
119
+ let errorPayload;
116
120
  if (e instanceof SudoError) {
117
- return process.send?.({
118
- cmd,
119
- // @ts-expect-error TS2239
120
- requestId: message.requestId || undefined,
121
- data: `Plugin: '${this.plugin.name}'. Forbidden usage of sudo for command '${e.command}'. Please contact the plugin developer to fix this.`,
122
- status: MessageStatus.ERROR,
123
- });
121
+ errorPayload = {
122
+ errorType: 'sudo_error',
123
+ message: `Plugin: '${this.plugin.name}'. Forbidden usage of sudo for command '${e.command}'. Please contact the plugin developer to fix this.`,
124
+ data: { command: e.command, pluginName: this.plugin.name },
125
+ };
126
+ }
127
+ else if (e instanceof ApplyValidationError) {
128
+ errorPayload = {
129
+ errorType: 'apply_validation',
130
+ message: e.message,
131
+ data: { plan: e.plan.toResponse(), logs: e.logs },
132
+ };
133
+ }
134
+ else {
135
+ const isDebug = process.env.DEBUG?.includes('*') ?? false;
136
+ errorPayload = {
137
+ errorType: 'unknown',
138
+ message: isDebug ? (e.stack ?? e.message) : e.message,
139
+ };
124
140
  }
125
- const isDebug = process.env.DEBUG?.includes('*') ?? false;
126
- process.send?.({
127
- cmd,
128
- // @ts-expect-error TS2239
129
- requestId: message.requestId || undefined,
130
- data: isDebug ? e.stack : e.message,
131
- status: MessageStatus.ERROR,
132
- });
141
+ process.send?.({ cmd, requestId, data: errorPayload, status: MessageStatus.ERROR });
133
142
  }
134
143
  }
@@ -4,6 +4,7 @@
4
4
  declare class CodifyCliSenderImpl {
5
5
  private readonly validateIpcMessageV2;
6
6
  requestPressKeyToContinuePrompt(message?: string): Promise<void>;
7
+ sendApplyNote(message: string, resourceType?: string): Promise<void>;
7
8
  getCodifyCliCredentials(): Promise<string>;
8
9
  private sendAndWaitForResponse;
9
10
  }
@@ -17,6 +17,18 @@ class CodifyCliSenderImpl {
17
17
  }
18
18
  });
19
19
  }
20
+ async sendApplyNote(message, resourceType) {
21
+ if (!process.send || !process.connected) {
22
+ return;
23
+ }
24
+ await this.sendAndWaitForResponse({
25
+ cmd: MessageCmd.APPLY_NOTE_REQUEST,
26
+ data: {
27
+ message,
28
+ ...(resourceType && { resourceType }),
29
+ }
30
+ });
31
+ }
20
32
  async getCodifyCliCredentials() {
21
33
  const data = await this.sendAndWaitForResponse({
22
34
  cmd: MessageCmd.CODIFY_CREDENTIALS_REQUEST,
@@ -48,6 +48,7 @@ export class ChangeSet {
48
48
  }
49
49
  static destroy(current, settings) {
50
50
  const parameterChanges = Object.entries(current)
51
+ .filter(([k]) => !settings?.parameterSettings?.[k]?.setting)
51
52
  .map(([k, v]) => ({
52
53
  name: k,
53
54
  operation: ParameterOperation.REMOVE,
@@ -8,8 +8,13 @@ export declare class Plugin {
8
8
  resourceControllers: Map<string, ResourceController<ResourceConfig>>;
9
9
  planStorage: Map<string, Plan<any>>;
10
10
  planPty: BackgroundPty;
11
- constructor(name: string, resourceControllers: Map<string, ResourceController<ResourceConfig>>);
12
- static create(name: string, resources: Resource<any>[]): Plugin;
11
+ minSupportedCliVersion: string | undefined;
12
+ constructor(name: string, resourceControllers: Map<string, ResourceController<ResourceConfig>>, options?: {
13
+ minSupportedCliVersion?: string;
14
+ });
15
+ static create(name: string, resources: Resource<any>[], options?: {
16
+ minSupportedCliVersion?: string;
17
+ }): Plugin;
13
18
  initialize(data: InitializeRequestData): Promise<InitializeResponseData>;
14
19
  getResourceInfo(data: GetResourceInfoRequestData): GetResourceInfoResponseData;
15
20
  match(data: MatchRequestData): Promise<MatchResponseData>;
@@ -11,16 +11,18 @@ export class Plugin {
11
11
  resourceControllers;
12
12
  planStorage;
13
13
  planPty = new BackgroundPty();
14
- constructor(name, resourceControllers) {
14
+ minSupportedCliVersion;
15
+ constructor(name, resourceControllers, options) {
15
16
  this.name = name;
16
17
  this.resourceControllers = resourceControllers;
17
18
  this.planStorage = new Map();
19
+ this.minSupportedCliVersion = options?.minSupportedCliVersion;
18
20
  }
19
- static create(name, resources) {
21
+ static create(name, resources, options) {
20
22
  const controllers = resources
21
23
  .map((resource) => new ResourceController(resource));
22
24
  const controllersMap = new Map(controllers.map((r) => [r.typeId, r]));
23
- return new Plugin(name, controllersMap);
25
+ return new Plugin(name, controllersMap, options);
24
26
  }
25
27
  async initialize(data) {
26
28
  if (data.verbosityLevel) {
@@ -30,6 +32,7 @@ export class Plugin {
30
32
  await controller.initialize();
31
33
  }
32
34
  return {
35
+ minSupportedCliVersion: this.minSupportedCliVersion,
33
36
  resourceDefinitions: [...this.resourceControllers.values()]
34
37
  .map((r) => {
35
38
  const sensitiveParameters = Object.entries(r.settings.parameterSettings ?? {})
@@ -121,9 +124,9 @@ export class Plugin {
121
124
  if (!this.resourceControllers.has(core.type)) {
122
125
  throw new Error(`Resource type not found: ${core.type}`);
123
126
  }
124
- const validation = await this.resourceControllers
127
+ const validation = await ptyLocalStorage.run(this.planPty, () => this.resourceControllers
125
128
  .get(core.type)
126
- .validate(core, parameters);
129
+ .validate(core, parameters));
127
130
  validationResults.push(validation);
128
131
  }
129
132
  // Validate that if allow multiple is false, then only 1 of each resource exists
@@ -165,7 +168,11 @@ export class Plugin {
165
168
  if (!resource) {
166
169
  throw new Error('Malformed plan with resource that cannot be found');
167
170
  }
168
- await ptyLocalStorage.run(new SequentialPty(), async () => resource.apply(plan));
171
+ let applyLogs = [];
172
+ await ptyLocalStorage.run(new SequentialPty(), async () => {
173
+ await resource.apply(plan);
174
+ applyLogs = getPty().getLogs();
175
+ });
169
176
  // Validate using desired/desired. If the apply was successful, no changes should be reported back.
170
177
  // Default back desired back to current if it is not defined (for destroys only)
171
178
  const validationPlan = await ptyLocalStorage.run(new BackgroundPty(), async () => {
@@ -174,7 +181,7 @@ export class Plugin {
174
181
  return result;
175
182
  });
176
183
  if (validationPlan.requiresChanges()) {
177
- throw new ApplyValidationError(plan);
184
+ throw new ApplyValidationError(validationPlan, applyLogs);
178
185
  }
179
186
  }
180
187
  async setVerbosityLevel(data) {
@@ -17,5 +17,6 @@ export declare class BackgroundPty implements IPty {
17
17
  signal?: number | undefined;
18
18
  }>;
19
19
  private initialize;
20
+ getLogs(): string[];
20
21
  private getDefaultShell;
21
22
  }
@@ -121,6 +121,9 @@ export class BackgroundPty {
121
121
  });
122
122
  });
123
123
  }
124
+ getLogs() {
125
+ return [];
126
+ }
124
127
  getDefaultShell() {
125
128
  return process.env.SHELL;
126
129
  }
@@ -30,6 +30,7 @@ export interface SpawnOptions {
30
30
  env?: Record<string, unknown>;
31
31
  interactive?: boolean;
32
32
  requiresRoot?: boolean;
33
+ requiresSudoAskpass?: boolean;
33
34
  stdin?: boolean;
34
35
  disableWrapping?: boolean;
35
36
  }
@@ -37,7 +38,7 @@ export declare class SpawnError extends Error {
37
38
  data: string;
38
39
  cmd: string;
39
40
  exitCode: number;
40
- constructor(cmd: string, exitCode: number, data: string);
41
+ constructor(cmd: string, exitCode: number, data: string, logs?: string[]);
41
42
  }
42
43
  export interface IPty {
43
44
  spawn(cmd: string | string[], options?: SpawnOptions): Promise<SpawnResult>;
@@ -46,5 +47,6 @@ export interface IPty {
46
47
  exitCode: number;
47
48
  signal?: number | undefined;
48
49
  }>;
50
+ getLogs(): string[];
49
51
  }
50
52
  export declare function getPty(): IPty;
package/dist/pty/index.js CHANGED
@@ -8,8 +8,11 @@ export class SpawnError extends Error {
8
8
  data;
9
9
  cmd;
10
10
  exitCode;
11
- constructor(cmd, exitCode, data) {
12
- super(`Spawn Error: on command "${cmd}" with exit code: ${exitCode}\nOutput:\n${data}`);
11
+ constructor(cmd, exitCode, data, logs) {
12
+ const logSection = logs?.length
13
+ ? `\nLast logs:\n${logs.join('\n')}`
14
+ : '';
15
+ super(`Spawn Error: on command "${cmd}" with exit code: ${exitCode}\nOutput:\n${data}${logSection}`);
13
16
  this.data = data;
14
17
  this.cmd = cmd;
15
18
  this.exitCode = exitCode;
@@ -6,6 +6,10 @@ import { IPty, SpawnOptions, SpawnResult } from './index.js';
6
6
  * without a tty (or even a stdin) attached so interactive commands will not work.
7
7
  */
8
8
  export declare class SequentialPty implements IPty {
9
+ private logBuffer;
10
+ private static readonly MAX_LOG_LINES;
11
+ getLogs(): string[];
12
+ private appendLog;
9
13
  spawn(cmd: string | string[], options?: SpawnOptions): Promise<SpawnResult>;
10
14
  spawnSafe(cmd: string | string[], options?: SpawnOptions): Promise<SpawnResult>;
11
15
  kill(): Promise<{
@@ -19,10 +19,22 @@ const validateSudoRequestResponse = ajv.compile(CommandRequestResponseDataSchema
19
19
  * without a tty (or even a stdin) attached so interactive commands will not work.
20
20
  */
21
21
  export class SequentialPty {
22
+ logBuffer = [];
23
+ static MAX_LOG_LINES = 30;
24
+ getLogs() {
25
+ return [...this.logBuffer];
26
+ }
27
+ appendLog(data) {
28
+ const lines = data.split('\n');
29
+ this.logBuffer.push(...lines);
30
+ if (this.logBuffer.length > SequentialPty.MAX_LOG_LINES) {
31
+ this.logBuffer = this.logBuffer.slice(-SequentialPty.MAX_LOG_LINES);
32
+ }
33
+ }
22
34
  async spawn(cmd, options) {
23
35
  const spawnResult = await this.spawnSafe(cmd, options);
24
36
  if (spawnResult.status !== 'success') {
25
- throw new SpawnError(Array.isArray(cmd) ? cmd.join('\n') : cmd, spawnResult.exitCode, spawnResult.data);
37
+ throw new SpawnError(Array.isArray(cmd) ? cmd.join('\n') : cmd, spawnResult.exitCode, spawnResult.data, this.logBuffer);
26
38
  }
27
39
  return spawnResult;
28
40
  }
@@ -32,10 +44,12 @@ export class SequentialPty {
32
44
  throw new Error('Do not directly use sudo. Use the option { requiresRoot: true } instead');
33
45
  }
34
46
  // If sudo is required, we must delegate to the main codify process.
35
- if (options?.stdin || options?.requiresRoot) {
47
+ if (options?.stdin || options?.requiresRoot || options?.requiresSudoAskpass) {
36
48
  return this.externalSpawn(cmd, options);
37
49
  }
38
- console.log(`Running command: ${Array.isArray(cmd) ? cmd.join('\\\n') : cmd}` + (options?.cwd ? `(${options?.cwd})` : ''));
50
+ const cmdLine = `Running command: ${Array.isArray(cmd) ? cmd.join('\\\n') : cmd}` + (options?.cwd ? `(${options?.cwd})` : '');
51
+ console.log(cmdLine);
52
+ this.appendLog(cmdLine);
39
53
  return new Promise((resolve) => {
40
54
  const output = [];
41
55
  const historyIgnore = Utils.getShell() === Shell.ZSH ? { HISTORY_IGNORE: '*' } : { HISTIGNORE: '*' };
@@ -65,6 +79,7 @@ export class SequentialPty {
65
79
  process.stdout.write(data);
66
80
  }
67
81
  output.push(data.toString());
82
+ this.appendLog(data.toString());
68
83
  });
69
84
  const resizeListener = () => {
70
85
  const { columns, rows } = process.stdout;
@@ -28,7 +28,7 @@ export class ParsedResourceSettings {
28
28
  if (ctx.path.length === 0) {
29
29
  ctx.jsonSchema.title = settings.id;
30
30
  ctx.jsonSchema.description = schema.description ?? settings.description ?? `${settings.id} resource. Can be used to manage ${settings.id}`;
31
- ctx.jsonSchema.$comment = schema.meta().$comment;
31
+ ctx.jsonSchema.$comment = schema.meta()?.$comment;
32
32
  }
33
33
  }
34
34
  })
@@ -3,6 +3,8 @@ import * as fs from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import { Readable } from 'node:stream';
5
5
  import { finished } from 'node:stream/promises';
6
+ import { ApplyNotes } from '../common/apply-notes.js';
7
+ import { CodifyCliSender } from '../messages/sender.js';
6
8
  import { Utils } from './index.js';
7
9
  const SPACE_REGEX = /^\s*$/;
8
10
  export class FileUtils {
@@ -22,6 +24,7 @@ export class FileUtils {
22
24
  await FileUtils.createShellRcIfNotExists();
23
25
  const lineToInsert = addLeadingSpacer(addTrailingSpacer(line));
24
26
  await fs.appendFile(Utils.getPrimaryShellRc(), lineToInsert);
27
+ await CodifyCliSender.sendApplyNote(ApplyNotes.sourceShellRc());
25
28
  function addLeadingSpacer(line) {
26
29
  return line.startsWith('\n')
27
30
  ? line
@@ -40,6 +43,7 @@ export class FileUtils {
40
43
  console.log(`Adding to ${path.basename(shellRc)}:
41
44
  ${lines.join('\n')}`);
42
45
  await fs.appendFile(shellRc, formattedLines);
46
+ await CodifyCliSender.sendApplyNote(ApplyNotes.sourceShellRc());
43
47
  }
44
48
  /**
45
49
  * This method adds a directory path to the shell rc file if it doesn't already exist.
@@ -56,9 +60,11 @@ ${lines.join('\n')}`);
56
60
  console.log(`Saving path: ${value} to ${shellRc}`);
57
61
  if (prepend) {
58
62
  await fs.appendFile(shellRc, `\nexport PATH=$PATH:${value};`, { encoding: 'utf8' });
59
- return;
60
63
  }
61
- await fs.appendFile(shellRc, `\nexport PATH=${value}:$PATH;`, { encoding: 'utf8' });
64
+ else {
65
+ await fs.appendFile(shellRc, `\nexport PATH=${value}:$PATH;`, { encoding: 'utf8' });
66
+ }
67
+ await CodifyCliSender.sendApplyNote(ApplyNotes.sourceShellRc());
62
68
  }
63
69
  static async removeFromFile(filePath, search) {
64
70
  const contents = await fs.readFile(filePath, 'utf8');
@@ -6,9 +6,10 @@ export function splitUserConfig(config) {
6
6
  ...(config.name ? { name: config.name } : {}),
7
7
  ...(config.dependsOn ? { dependsOn: config.dependsOn } : {}),
8
8
  ...(config.os ? { os: config.os } : {}),
9
+ ...(config.distro ? { distro: config.distro } : {})
9
10
  };
10
11
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
11
- const { type, name, dependsOn, os, ...parameters } = config;
12
+ const { type, name, dependsOn, os, distro, ...parameters } = config;
12
13
  return {
13
14
  parameters: parameters,
14
15
  coreParameters,
@@ -180,7 +180,21 @@ Brew can be installed using Codify:
180
180
  return;
181
181
  }
182
182
  if (status === SpawnStatus.ERROR) {
183
- throw new Error(`Failed to install package ${packageName} via apt: ${data}`);
183
+ // Attempt to fix broken dependencies then retry
184
+ const fixResult = await $.spawnSafe('apt-get install -f -y -o Dpkg::Use-Pty=0 -o Dpkg::Progress-Fancy=0', {
185
+ requiresRoot: true,
186
+ env: { DEBIAN_FRONTEND: 'noninteractive', NEEDRESTART_MODE: 'a' }
187
+ });
188
+ if (fixResult.status === SpawnStatus.ERROR) {
189
+ throw new Error(`Failed to install package ${packageName} via apt: ${data}`);
190
+ }
191
+ const retryResult = await $.spawnSafe(`apt-get -y -qq install -o Dpkg::Use-Pty=0 -o Dpkg::Progress-Fancy=0 ${packageName}`, {
192
+ requiresRoot: true,
193
+ env: { DEBIAN_FRONTEND: 'noninteractive', NEEDRESTART_MODE: 'a' }
194
+ });
195
+ if (retryResult.status === SpawnStatus.ERROR) {
196
+ throw new Error(`Failed to install package ${packageName} via apt after fixing dependencies: ${retryResult.data}`);
197
+ }
184
198
  }
185
199
  }
186
200
  const isDnfInstalled = await $.spawnSafe('which dnf');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codifycli/plugin-core",
3
- "version": "1.1.0-beta9",
3
+ "version": "1.2.0",
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",
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "license": "ISC",
37
37
  "dependencies": {
38
- "@codifycli/schemas": "1.1.0-beta3",
38
+ "@codifycli/schemas": "^1.2.0",
39
39
  "@homebridge/node-pty-prebuilt-multiarch": "^0.13.1",
40
40
  "ajv": "^8.18.0",
41
41
  "ajv-formats": "^2.1.1",
@@ -43,7 +43,7 @@
43
43
  "lodash.isequal": "^4.5.0",
44
44
  "nanoid": "^5.0.9",
45
45
  "strip-ansi": "^7.1.0",
46
- "uuid": "^10.0.0",
46
+ "uuid": "^14.0.0",
47
47
  "zod": "4.1.13"
48
48
  },
49
49
  "devDependencies": {
@@ -0,0 +1,12 @@
1
+ import path from 'node:path';
2
+
3
+ import { Utils } from '../utils/index.js';
4
+
5
+ export const ApplyNotes = {
6
+ RESTART_REQUIRED: 'A system restart is required for changes to take effect.',
7
+ NEW_SHELL_REQUIRED: 'Open a new terminal session for the changes to be reflected.',
8
+ sourceShellRc(): string {
9
+ const rc = path.basename(Utils.getPrimaryShellRc());
10
+ return `Source '~/${rc}' for the changes to be reflected.`;
11
+ },
12
+ } as const;
@@ -4,13 +4,15 @@ export class ApplyValidationError extends Error {
4
4
  resourceType: string;
5
5
  resourceName?: string;
6
6
  plan: Plan<any>;
7
+ logs: string[];
7
8
 
8
- constructor(plan: Plan<any>) {
9
+ constructor(plan: Plan<any>, logs: string[] = []) {
9
10
  super(`Failed to apply changes to resource: "${plan.resourceId}". Additional changes are needed to complete apply.\nChanges remaining:\n${ApplyValidationError.prettyPrintPlan(plan)}`);
10
11
 
11
12
  this.resourceType = plan.coreParameters.type;
12
13
  this.resourceName = plan.coreParameters.name;
13
14
  this.plan = plan;
15
+ this.logs = logs;
14
16
  }
15
17
 
16
18
  private static prettyPrintPlan(plan: Plan<any>): string {
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { MessageHandler } from './messages/handlers.js';
2
2
  import { Plugin } from './plugin/plugin.js';
3
3
 
4
+ export * from './common/apply-notes.js'
4
5
  export * from './errors.js'
5
6
  export * from './messages/sender.js'
6
7
  export * from './plan/change-set.js'
@@ -16,6 +16,7 @@ import {
16
16
  MessageStatus,
17
17
  PlanRequestDataSchema,
18
18
  PlanResponseDataSchema,
19
+ PluginErrorData,
19
20
  ResourceSchema,
20
21
  SetVerbosityRequestDataSchema,
21
22
  ValidateRequestDataSchema,
@@ -24,6 +25,7 @@ import {
24
25
  import { Ajv, SchemaObject, ValidateFunction } from 'ajv';
25
26
  import addFormats from 'ajv-formats';
26
27
 
28
+ import { ApplyValidationError } from '../common/errors.js';
27
29
  import { SudoError } from '../errors.js';
28
30
  import { Plugin } from '../plugin/plugin.js';
29
31
 
@@ -157,25 +159,31 @@ export class MessageHandler {
157
159
 
158
160
  // @ts-expect-error TS2239
159
161
  const cmd = message.cmd + '_Response';
162
+ // @ts-expect-error TS2239
163
+ const requestId = message.requestId || undefined;
164
+
165
+ let errorPayload: PluginErrorData;
160
166
 
161
167
  if (e instanceof SudoError) {
162
- return process.send?.({
163
- cmd,
164
- // @ts-expect-error TS2239
165
- requestId: message.requestId || undefined,
166
- data: `Plugin: '${this.plugin.name}'. Forbidden usage of sudo for command '${e.command}'. Please contact the plugin developer to fix this.`,
167
- status: MessageStatus.ERROR,
168
- })
168
+ errorPayload = {
169
+ errorType: 'sudo_error',
170
+ message: `Plugin: '${this.plugin.name}'. Forbidden usage of sudo for command '${e.command}'. Please contact the plugin developer to fix this.`,
171
+ data: { command: e.command, pluginName: this.plugin.name },
172
+ };
173
+ } else if (e instanceof ApplyValidationError) {
174
+ errorPayload = {
175
+ errorType: 'apply_validation',
176
+ message: e.message,
177
+ data: { plan: e.plan.toResponse(), logs: e.logs },
178
+ };
179
+ } else {
180
+ const isDebug = process.env.DEBUG?.includes('*') ?? false;
181
+ errorPayload = {
182
+ errorType: 'unknown',
183
+ message: isDebug ? (e.stack ?? e.message) : e.message,
184
+ };
169
185
  }
170
186
 
171
- const isDebug = process.env.DEBUG?.includes('*') ?? false;
172
-
173
- process.send?.({
174
- cmd,
175
- // @ts-expect-error TS2239
176
- requestId: message.requestId || undefined,
177
- data: isDebug ? e.stack : e.message,
178
- status: MessageStatus.ERROR,
179
- })
187
+ process.send?.({ cmd, requestId, data: errorPayload, status: MessageStatus.ERROR });
180
188
  }
181
189
  }
@@ -1,4 +1,10 @@
1
- import { IpcMessageV2, IpcMessageV2Schema, MessageCmd, PressKeyToContinueRequestData } from '@codifycli/schemas';
1
+ import {
2
+ ApplyNoteRequestData,
3
+ IpcMessageV2,
4
+ IpcMessageV2Schema,
5
+ MessageCmd,
6
+ PressKeyToContinueRequestData
7
+ } from '@codifycli/schemas';
2
8
  import { Ajv } from 'ajv';
3
9
  import { nanoid } from 'nanoid';
4
10
 
@@ -21,6 +27,20 @@ class CodifyCliSenderImpl {
21
27
  })
22
28
  }
23
29
 
30
+ async sendApplyNote(message: string, resourceType?: string): Promise<void> {
31
+ if (!process.send || !process.connected) {
32
+ return;
33
+ }
34
+
35
+ await this.sendAndWaitForResponse(<IpcMessageV2>{
36
+ cmd: MessageCmd.APPLY_NOTE_REQUEST,
37
+ data: <ApplyNoteRequestData>{
38
+ message,
39
+ ...(resourceType && { resourceType }),
40
+ }
41
+ });
42
+ }
43
+
24
44
  async getCodifyCliCredentials(): Promise<string> {
25
45
  const data = await this.sendAndWaitForResponse(<IpcMessageV2>{
26
46
  cmd: MessageCmd.CODIFY_CREDENTIALS_REQUEST,
@@ -107,6 +107,7 @@ describe('Change set tests', () => {
107
107
 
108
108
  const parameterSettings = new ParsedResourceSettings({
109
109
  id: 'type',
110
+ operatingSystems: [],
110
111
  parameterSettings: {
111
112
  propA: { type: 'array' }
112
113
  }
@@ -129,6 +130,7 @@ describe('Change set tests', () => {
129
130
 
130
131
  const parameterSettings = new ParsedResourceSettings({
131
132
  id: 'type',
133
+ operatingSystems: [],
132
134
  parameterSettings: {
133
135
  propA: { type: 'array' }
134
136
  }
@@ -152,6 +154,7 @@ describe('Change set tests', () => {
152
154
 
153
155
  const parameterSettings = new ParsedResourceSettings({
154
156
  id: 'type',
157
+ operatingSystems: [],
155
158
  parameterSettings: {
156
159
  propA: { canModify: true }
157
160
  }
@@ -176,6 +179,7 @@ describe('Change set tests', () => {
176
179
 
177
180
  const parameterSettings = new ParsedResourceSettings({
178
181
  id: 'type',
182
+ operatingSystems: [],
179
183
  parameterSettings: {
180
184
  propA: { canModify: true },
181
185
  propB: { canModify: true }
@@ -196,6 +200,7 @@ describe('Change set tests', () => {
196
200
 
197
201
  const parameterSettings = new ParsedResourceSettings({
198
202
  id: 'type',
203
+ operatingSystems: [],
199
204
  parameterSettings: {
200
205
  propA: { type: 'array' }
201
206
  },
@@ -213,6 +218,7 @@ describe('Change set tests', () => {
213
218
 
214
219
  const parameterSettings = new ParsedResourceSettings({
215
220
  id: 'type',
221
+ operatingSystems: [],
216
222
  parameterSettings: {
217
223
  propA: { type: 'array' }
218
224
  },
@@ -229,6 +235,7 @@ describe('Change set tests', () => {
229
235
 
230
236
  const parameterSettings = new ParsedResourceSettings({
231
237
  id: 'type',
238
+ operatingSystems: [],
232
239
  parameterSettings: {
233
240
  propA: { type: 'array' }
234
241
  },
@@ -245,6 +252,7 @@ describe('Change set tests', () => {
245
252
 
246
253
  const parameterSettings = new ParsedResourceSettings({
247
254
  id: 'type',
255
+ operatingSystems: [],
248
256
  parameterSettings: {
249
257
  propA: {
250
258
  type: 'array',
@@ -259,12 +267,50 @@ describe('Change set tests', () => {
259
267
  expect(result.parameterChanges[0].operation).to.eq(ParameterOperation.MODIFY);
260
268
  })
261
269
 
270
+ it('excludes setting parameters from destroy change set', () => {
271
+ const settings = {
272
+ id: 'type',
273
+ operatingSystems: [],
274
+ parameterSettings: {
275
+ propA: {},
276
+ propB: { setting: true },
277
+ },
278
+ };
279
+
280
+ const cs = ChangeSet.destroy({ propA: 'val', propB: true }, settings as any);
281
+ expect(cs.parameterChanges.length).to.eq(1);
282
+ expect(cs.parameterChanges[0].name).to.eq('propA');
283
+ expect(cs.parameterChanges[0].operation).to.eq(ParameterOperation.REMOVE);
284
+ expect(cs.operation).to.eq(ResourceOperation.DESTROY);
285
+ })
286
+
287
+ it('excludes multiple setting parameters from destroy change set', () => {
288
+ const settings = {
289
+ id: 'type',
290
+ operatingSystems: [],
291
+ parameterSettings: {
292
+ skipAlreadyInstalledCasks: { type: 'boolean', default: true, setting: true },
293
+ onlyPlanUserInstalled: { type: 'boolean', default: true, setting: true },
294
+ directory: { type: 'directory' },
295
+ },
296
+ };
297
+
298
+ const cs = ChangeSet.destroy(
299
+ { skipAlreadyInstalledCasks: true, onlyPlanUserInstalled: true, directory: '/opt/homebrew' },
300
+ settings as any
301
+ );
302
+ expect(cs.parameterChanges.length).to.eq(1);
303
+ expect(cs.parameterChanges[0].name).to.eq('directory');
304
+ expect(cs.operation).to.eq(ResourceOperation.DESTROY);
305
+ })
306
+
262
307
  it('correctly determines array equality 5', () => {
263
308
  const arrA = [{ key1: 'b' }, { key1: 'a' }, { key1: 'a' }];
264
309
  const arrB = [{ key1: 'a' }, { key1: 'a' }, { key1: 'b' }];
265
310
 
266
311
  const parameterSettings = new ParsedResourceSettings({
267
312
  id: 'type',
313
+ operatingSystems: [],
268
314
  parameterSettings: {
269
315
  propA: {
270
316
  type: 'array',
@@ -94,6 +94,7 @@ export class ChangeSet<T extends StringIndexedObject> {
94
94
 
95
95
  static destroy<T extends StringIndexedObject>(current: Partial<T>, settings?: ResourceSettings<T>): ChangeSet<T> {
96
96
  const parameterChanges = Object.entries(current)
97
+ .filter(([k]) => !settings?.parameterSettings?.[k]?.setting)
97
98
  .map(([k, v]) => ({
98
99
  name: k,
99
100
  operation: ParameterOperation.REMOVE,
@@ -31,15 +31,22 @@ import { VerbosityLevel } from '../utils/verbosity-level.js';
31
31
  export class Plugin {
32
32
  planStorage: Map<string, Plan<any>>;
33
33
  planPty = new BackgroundPty();
34
+ minSupportedCliVersion: string | undefined;
34
35
 
35
36
  constructor(
36
37
  public name: string,
37
- public resourceControllers: Map<string, ResourceController<ResourceConfig>>
38
+ public resourceControllers: Map<string, ResourceController<ResourceConfig>>,
39
+ options?: { minSupportedCliVersion?: string }
38
40
  ) {
39
41
  this.planStorage = new Map();
42
+ this.minSupportedCliVersion = options?.minSupportedCliVersion;
40
43
  }
41
44
 
42
- static create(name: string, resources: Resource<any>[]) {
45
+ static create(
46
+ name: string,
47
+ resources: Resource<any>[],
48
+ options?: { minSupportedCliVersion?: string }
49
+ ) {
43
50
  const controllers = resources
44
51
  .map((resource) => new ResourceController(resource))
45
52
 
@@ -47,7 +54,7 @@ export class Plugin {
47
54
  controllers.map((r) => [r.typeId, r] as const)
48
55
  );
49
56
 
50
- return new Plugin(name, controllersMap);
57
+ return new Plugin(name, controllersMap, options);
51
58
  }
52
59
 
53
60
  async initialize(data: InitializeRequestData): Promise<InitializeResponseData> {
@@ -60,6 +67,7 @@ export class Plugin {
60
67
  }
61
68
 
62
69
  return {
70
+ minSupportedCliVersion: this.minSupportedCliVersion,
63
71
  resourceDefinitions: [...this.resourceControllers.values()]
64
72
  .map((r) => {
65
73
  const sensitiveParameters = Object.entries(r.settings.parameterSettings ?? {})
@@ -174,9 +182,11 @@ export class Plugin {
174
182
  throw new Error(`Resource type not found: ${core.type}`);
175
183
  }
176
184
 
177
- const validation = await this.resourceControllers
178
- .get(core.type)!
179
- .validate(core, parameters);
185
+ const validation = await ptyLocalStorage.run(this.planPty, () =>
186
+ this.resourceControllers
187
+ .get(core.type)!
188
+ .validate(core, parameters)
189
+ );
180
190
 
181
191
  validationResults.push(validation);
182
192
  }
@@ -240,7 +250,11 @@ export class Plugin {
240
250
  throw new Error('Malformed plan with resource that cannot be found');
241
251
  }
242
252
 
243
- await ptyLocalStorage.run(new SequentialPty(), async () => resource.apply(plan))
253
+ let applyLogs: string[] = [];
254
+ await ptyLocalStorage.run(new SequentialPty(), async () => {
255
+ await resource.apply(plan);
256
+ applyLogs = getPty().getLogs();
257
+ });
244
258
 
245
259
  // Validate using desired/desired. If the apply was successful, no changes should be reported back.
246
260
  // Default back desired back to current if it is not defined (for destroys only)
@@ -258,7 +272,7 @@ export class Plugin {
258
272
  })
259
273
 
260
274
  if (validationPlan.requiresChanges()) {
261
- throw new ApplyValidationError(plan);
275
+ throw new ApplyValidationError(validationPlan, applyLogs);
262
276
  }
263
277
  }
264
278
 
@@ -148,6 +148,10 @@ export class BackgroundPty implements IPty {
148
148
  })
149
149
  }
150
150
 
151
+ getLogs(): string[] {
152
+ return [];
153
+ }
154
+
151
155
  private getDefaultShell(): string {
152
156
  return process.env.SHELL!;
153
157
  }
package/src/pty/index.ts CHANGED
@@ -34,6 +34,7 @@ export interface SpawnOptions {
34
34
  env?: Record<string, unknown>;
35
35
  interactive?: boolean;
36
36
  requiresRoot?: boolean;
37
+ requiresSudoAskpass?: boolean;
37
38
  stdin?: boolean;
38
39
  disableWrapping?: boolean;
39
40
  }
@@ -43,8 +44,11 @@ export class SpawnError extends Error {
43
44
  cmd: string;
44
45
  exitCode: number;
45
46
 
46
- constructor(cmd: string, exitCode: number, data: string) {
47
- super(`Spawn Error: on command "${cmd}" with exit code: ${exitCode}\nOutput:\n${data}`);
47
+ constructor(cmd: string, exitCode: number, data: string, logs?: string[]) {
48
+ const logSection = logs?.length
49
+ ? `\nLast logs:\n${logs.join('\n')}`
50
+ : '';
51
+ super(`Spawn Error: on command "${cmd}" with exit code: ${exitCode}\nOutput:\n${data}${logSection}`);
48
52
 
49
53
  this.data = data;
50
54
  this.cmd = cmd;
@@ -59,6 +63,8 @@ export interface IPty {
59
63
  spawnSafe(cmd: string | string[], options?: SpawnOptions): Promise<SpawnResult>
60
64
 
61
65
  kill(): Promise<{ exitCode: number, signal?: number | undefined }>
66
+
67
+ getLogs(): string[]
62
68
  }
63
69
 
64
70
  export function getPty(): IPty {
@@ -28,11 +28,26 @@ const validateSudoRequestResponse = ajv.compile(CommandRequestResponseDataSchema
28
28
  * without a tty (or even a stdin) attached so interactive commands will not work.
29
29
  */
30
30
  export class SequentialPty implements IPty {
31
+ private logBuffer: string[] = [];
32
+ private static readonly MAX_LOG_LINES = 30;
33
+
34
+ getLogs(): string[] {
35
+ return [...this.logBuffer];
36
+ }
37
+
38
+ private appendLog(data: string): void {
39
+ const lines = data.split('\n');
40
+ this.logBuffer.push(...lines);
41
+ if (this.logBuffer.length > SequentialPty.MAX_LOG_LINES) {
42
+ this.logBuffer = this.logBuffer.slice(-SequentialPty.MAX_LOG_LINES);
43
+ }
44
+ }
45
+
31
46
  async spawn(cmd: string | string[], options?: SpawnOptions): Promise<SpawnResult> {
32
47
  const spawnResult = await this.spawnSafe(cmd, options);
33
48
 
34
49
  if (spawnResult.status !== 'success') {
35
- throw new SpawnError(Array.isArray(cmd) ? cmd.join('\n') : cmd, spawnResult.exitCode, spawnResult.data);
50
+ throw new SpawnError(Array.isArray(cmd) ? cmd.join('\n') : cmd, spawnResult.exitCode, spawnResult.data, this.logBuffer);
36
51
  }
37
52
 
38
53
  return spawnResult;
@@ -46,11 +61,13 @@ export class SequentialPty implements IPty {
46
61
  }
47
62
 
48
63
  // If sudo is required, we must delegate to the main codify process.
49
- if (options?.stdin || options?.requiresRoot) {
64
+ if (options?.stdin || options?.requiresRoot || options?.requiresSudoAskpass) {
50
65
  return this.externalSpawn(cmd, options);
51
66
  }
52
67
 
53
- console.log(`Running command: ${Array.isArray(cmd) ? cmd.join('\\\n') : cmd}` + (options?.cwd ? `(${options?.cwd})` : ''))
68
+ const cmdLine = `Running command: ${Array.isArray(cmd) ? cmd.join('\\\n') : cmd}` + (options?.cwd ? `(${options?.cwd})` : '');
69
+ console.log(cmdLine);
70
+ this.appendLog(cmdLine);
54
71
 
55
72
  return new Promise((resolve) => {
56
73
  const output: string[] = [];
@@ -87,6 +104,7 @@ export class SequentialPty implements IPty {
87
104
  }
88
105
 
89
106
  output.push(data.toString());
107
+ this.appendLog(data.toString());
90
108
  })
91
109
 
92
110
  const resizeListener = () => {
@@ -76,7 +76,7 @@ export class ParsedResourceSettings<T extends StringIndexedObject> implements Re
76
76
  if (ctx.path.length === 0) {
77
77
  ctx.jsonSchema.title = settings.id;
78
78
  ctx.jsonSchema.description = schema.description ?? settings.description ?? `${settings.id} resource. Can be used to manage ${settings.id}`;
79
- ctx.jsonSchema.$comment = (schema.meta() as Record<string, string | undefined>).$comment;
79
+ ctx.jsonSchema.$comment = (schema.meta() as Record<string, string | undefined> | undefined)?.$comment;
80
80
  }
81
81
  }
82
82
  }) as JSONSchemaType<T>
@@ -4,6 +4,8 @@ import path from 'node:path';
4
4
  import { Readable } from 'node:stream';
5
5
  import { finished } from 'node:stream/promises';
6
6
 
7
+ import { ApplyNotes } from '../common/apply-notes.js';
8
+ import { CodifyCliSender } from '../messages/sender.js';
7
9
  import { Utils } from './index.js';
8
10
 
9
11
  const SPACE_REGEX = /^\s*$/
@@ -33,6 +35,7 @@ export class FileUtils {
33
35
  );
34
36
 
35
37
  await fs.appendFile(Utils.getPrimaryShellRc(), lineToInsert)
38
+ await CodifyCliSender.sendApplyNote(ApplyNotes.sourceShellRc());
36
39
 
37
40
  function addLeadingSpacer(line: string): string {
38
41
  return line.startsWith('\n')
@@ -57,6 +60,7 @@ export class FileUtils {
57
60
  ${lines.join('\n')}`)
58
61
 
59
62
  await fs.appendFile(shellRc, formattedLines)
63
+ await CodifyCliSender.sendApplyNote(ApplyNotes.sourceShellRc());
60
64
  }
61
65
 
62
66
  /**
@@ -77,10 +81,11 @@ ${lines.join('\n')}`)
77
81
 
78
82
  if (prepend) {
79
83
  await fs.appendFile(shellRc, `\nexport PATH=$PATH:${value};`, { encoding: 'utf8' });
80
- return;
84
+ } else {
85
+ await fs.appendFile(shellRc, `\nexport PATH=${value}:$PATH;`, { encoding: 'utf8' });
81
86
  }
82
87
 
83
- await fs.appendFile(shellRc, `\nexport PATH=${value}:$PATH;`, { encoding: 'utf8' });
88
+ await CodifyCliSender.sendApplyNote(ApplyNotes.sourceShellRc());
84
89
  }
85
90
 
86
91
  static async removeFromFile(filePath: string, search: string): Promise<void> {
@@ -10,10 +10,11 @@ export function splitUserConfig<T extends StringIndexedObject>(
10
10
  ...(config.name ? { name: config.name } : {}),
11
11
  ...(config.dependsOn ? { dependsOn: config.dependsOn } : {}),
12
12
  ...(config.os ? { os: config.os } : {}),
13
+ ...(config.distro ? { distro: config.distro } : {})
13
14
  };
14
15
 
15
16
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
16
- const { type, name, dependsOn, os, ...parameters } = config;
17
+ const { type, name, dependsOn, os, distro, ...parameters } = config;
17
18
 
18
19
  return {
19
20
  parameters: parameters as T,
@@ -225,7 +225,24 @@ Brew can be installed using Codify:
225
225
  }
226
226
 
227
227
  if (status === SpawnStatus.ERROR) {
228
- throw new Error(`Failed to install package ${packageName} via apt: ${data}`);
228
+ // Attempt to fix broken dependencies then retry
229
+ const fixResult = await $.spawnSafe('apt-get install -f -y -o Dpkg::Use-Pty=0 -o Dpkg::Progress-Fancy=0', {
230
+ requiresRoot: true,
231
+ env: { DEBIAN_FRONTEND: 'noninteractive', NEEDRESTART_MODE: 'a' }
232
+ });
233
+
234
+ if (fixResult.status === SpawnStatus.ERROR) {
235
+ throw new Error(`Failed to install package ${packageName} via apt: ${data}`);
236
+ }
237
+
238
+ const retryResult = await $.spawnSafe(`apt-get -y -qq install -o Dpkg::Use-Pty=0 -o Dpkg::Progress-Fancy=0 ${packageName}`, {
239
+ requiresRoot: true,
240
+ env: { DEBIAN_FRONTEND: 'noninteractive', NEEDRESTART_MODE: 'a' }
241
+ });
242
+
243
+ if (retryResult.status === SpawnStatus.ERROR) {
244
+ throw new Error(`Failed to install package ${packageName} via apt after fixing dependencies: ${retryResult.data}`);
245
+ }
229
246
  }
230
247
  }
231
248
 
@@ -9,6 +9,7 @@ describe('Utils tests', () => {
9
9
  name: 'name',
10
10
  dependsOn: ['a', 'b', 'c'],
11
11
  os: ['linux'],
12
+ distro: ['debian-based'],
12
13
  propA: 'propA',
13
14
  propB: 'propB',
14
15
  propC: 'propC',