@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.
- package/.claude/settings.local.json +9 -1
- package/dist/common/apply-notes.d.ts +5 -0
- package/dist/common/apply-notes.js +10 -0
- package/dist/common/errors.d.ts +2 -1
- package/dist/common/errors.js +3 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/messages/handlers.js +24 -15
- package/dist/messages/sender.d.ts +1 -0
- package/dist/messages/sender.js +12 -0
- package/dist/plan/change-set.js +1 -0
- package/dist/plugin/plugin.d.ts +7 -2
- package/dist/plugin/plugin.js +14 -7
- package/dist/pty/background-pty.d.ts +1 -0
- package/dist/pty/background-pty.js +3 -0
- package/dist/pty/index.d.ts +3 -1
- package/dist/pty/index.js +5 -2
- package/dist/pty/seqeuntial-pty.d.ts +4 -0
- package/dist/pty/seqeuntial-pty.js +18 -3
- package/dist/resource/parsed-resource-settings.js +1 -1
- package/dist/utils/file-utils.js +8 -2
- package/dist/utils/functions.js +2 -1
- package/dist/utils/index.js +15 -1
- package/package.json +3 -3
- package/src/common/apply-notes.ts +12 -0
- package/src/common/errors.ts +3 -1
- package/src/index.ts +1 -0
- package/src/messages/handlers.ts +24 -16
- package/src/messages/sender.ts +21 -1
- package/src/plan/change-set.test.ts +46 -0
- package/src/plan/change-set.ts +1 -0
- package/src/plugin/plugin.ts +22 -8
- package/src/pty/background-pty.ts +4 -0
- package/src/pty/index.ts +8 -2
- package/src/pty/seqeuntial-pty.ts +21 -3
- package/src/resource/parsed-resource-settings.ts +1 -1
- package/src/utils/file-utils.ts +7 -2
- package/src/utils/functions.ts +2 -1
- package/src/utils/index.ts +18 -1
- 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,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
|
+
};
|
package/dist/common/errors.d.ts
CHANGED
package/dist/common/errors.js
CHANGED
|
@@ -2,11 +2,13 @@ export class ApplyValidationError extends Error {
|
|
|
2
2
|
resourceType;
|
|
3
3
|
resourceName;
|
|
4
4
|
plan;
|
|
5
|
-
|
|
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
package/dist/index.js
CHANGED
|
@@ -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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/messages/sender.js
CHANGED
|
@@ -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,
|
package/dist/plan/change-set.js
CHANGED
package/dist/plugin/plugin.d.ts
CHANGED
|
@@ -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
|
-
|
|
12
|
-
|
|
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>;
|
package/dist/plugin/plugin.js
CHANGED
|
@@ -11,16 +11,18 @@ export class Plugin {
|
|
|
11
11
|
resourceControllers;
|
|
12
12
|
planStorage;
|
|
13
13
|
planPty = new BackgroundPty();
|
|
14
|
-
|
|
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
|
-
|
|
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(
|
|
184
|
+
throw new ApplyValidationError(validationPlan, applyLogs);
|
|
178
185
|
}
|
|
179
186
|
}
|
|
180
187
|
async setVerbosityLevel(data) {
|
package/dist/pty/index.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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()
|
|
31
|
+
ctx.jsonSchema.$comment = schema.meta()?.$comment;
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
})
|
package/dist/utils/file-utils.js
CHANGED
|
@@ -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
|
-
|
|
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');
|
package/dist/utils/functions.js
CHANGED
|
@@ -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,
|
package/dist/utils/index.js
CHANGED
|
@@ -180,7 +180,21 @@ Brew can be installed using Codify:
|
|
|
180
180
|
return;
|
|
181
181
|
}
|
|
182
182
|
if (status === SpawnStatus.ERROR) {
|
|
183
|
-
|
|
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.
|
|
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.
|
|
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": "^
|
|
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;
|
package/src/common/errors.ts
CHANGED
|
@@ -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
package/src/messages/handlers.ts
CHANGED
|
@@ -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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/messages/sender.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
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',
|
package/src/plan/change-set.ts
CHANGED
|
@@ -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,
|
package/src/plugin/plugin.ts
CHANGED
|
@@ -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(
|
|
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.
|
|
178
|
-
.
|
|
179
|
-
|
|
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
|
-
|
|
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(
|
|
275
|
+
throw new ApplyValidationError(validationPlan, applyLogs);
|
|
262
276
|
}
|
|
263
277
|
}
|
|
264
278
|
|
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
|
-
|
|
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
|
-
|
|
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>)
|
|
79
|
+
ctx.jsonSchema.$comment = (schema.meta() as Record<string, string | undefined> | undefined)?.$comment;
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
}) as JSONSchemaType<T>
|
package/src/utils/file-utils.ts
CHANGED
|
@@ -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
|
-
|
|
84
|
+
} else {
|
|
85
|
+
await fs.appendFile(shellRc, `\nexport PATH=${value}:$PATH;`, { encoding: 'utf8' });
|
|
81
86
|
}
|
|
82
87
|
|
|
83
|
-
await
|
|
88
|
+
await CodifyCliSender.sendApplyNote(ApplyNotes.sourceShellRc());
|
|
84
89
|
}
|
|
85
90
|
|
|
86
91
|
static async removeFromFile(filePath: string, search: string): Promise<void> {
|
package/src/utils/functions.ts
CHANGED
|
@@ -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,
|
package/src/utils/index.ts
CHANGED
|
@@ -225,7 +225,24 @@ Brew can be installed using Codify:
|
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
if (status === SpawnStatus.ERROR) {
|
|
228
|
-
|
|
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
|
|