Skip to content
Open
49 changes: 19 additions & 30 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
},
"dependencies": {
"@codifycli/ink-form": "0.0.12",
"@codifycli/schemas": "1.1.0-beta8",
"@homebridge/node-pty-prebuilt-multiarch": "^0.12.0-beta.5",
"@codifycli/schemas": "1.2.0",
"@homebridge/node-pty-prebuilt-multiarch": "^0.13.1",
"@mischnic/json-sourcemap": "^0.1.1",
"@oclif/core": "^4.0.8",
"@oclif/plugin-autocomplete": "^3.2.24",
Expand Down Expand Up @@ -43,7 +43,7 @@
},
"description": "Codify is a configuration-as-code tool that declaratively installs and manages developer tools and applications. Check out https://dashboard.codifycli.com for an editor.",
"devDependencies": {
"@codifycli/plugin-core": "^1.1.0-beta19",
"@codifycli/plugin-core": "^1.2.0",
"@oclif/prettier-config": "^0.2.1",
"@types/chalk": "^2.2.0",
"@types/cors": "^2.8.19",
Expand Down Expand Up @@ -145,7 +145,7 @@
"deploy": "npm run pkg && npm run notarize && npm run upload",
"prepublishOnly": "npm run build"
},
"version": "1.1.0",
"version": "1.2.0-beta.3",
"bugs": "https://github.com/codifycli/codify/issues",
"keywords": [
"oclif",
Expand Down
63 changes: 63 additions & 0 deletions src/common/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,69 @@ export class PluginError extends CodifyError {
}
}

export class ShellValidationError extends CodifyError {
name = 'ShellValidationError';
timedOut: boolean;
capturedOutput: string;
rcFiles: string[];

constructor(timedOut: boolean, capturedOutput: string, rcFiles: string[]) {
super(timedOut
? 'Shell initialization timed out — a shell rc file may be waiting for input'
: 'Shell initialization produced unexpected output'
);
this.timedOut = timedOut;
this.capturedOutput = capturedOutput;
this.rcFiles = rcFiles;
}

formattedMessage(): string {
const rcFileList = this.rcFiles.map((f) => chalk.white(` • ${f}`)).join('\n');
const indentedOutput = (text: string) => (text || '(none)')
.split('\n')
.map((l) => chalk.red('│ ') + chalk.white(l))
.join('\n');

if (this.timedOut) {
return [
chalk.bold('Shell Validation Error: Shell timed out after 10 seconds'),
'',
'Something in your shell initialization is waiting for interactive input',
chalk.white('(e.g. a password prompt, a `read` call, or an SSH key passphrase)'),
'',
chalk.white('Codify sources your interactive shell to install tools exactly as you would.') +
' A shell that hangs on startup will prevent Codify from running.',
'',
chalk.bold('Output captured before timeout:'),
indentedOutput(this.capturedOutput),
'',
chalk.bold('Check the following shell rc files for interactive prompts:'),
rcFileList,
].join('\n');
}

return [
chalk.bold('Shell Validation Error: Unexpected output detected on shell startup'),
'',
chalk.bold('Unexpected output:'),
indentedOutput(this.capturedOutput),
'',
'Your shell initialization is printing extra output',
chalk.white('(e.g. a greeting, banner, or debug message)'),
chalk.white('Codify sources your interactive shell to install tools exactly as you would.') +
' Any extra output from your rc files will break plugin commands that parse shell output.',
'',
chalk.bold('Check the following rc files and wrap output-producing lines in an interactive guard:'),
rcFileList,
'',
chalk.bold('Example fix for .zshrc / .bashrc:'),
chalk.white(' if [[ "$TERM_PROGRAM" != "codify" ]]; then'),
chalk.white(' echo "your greeting here" # skipped when Codify sources your shell'),
chalk.white(' fi'),
].join('\n');
}
}

export function prettyPrintError(error: unknown): void {
if (error instanceof CodifyError) {
return console.error(chalk.red(error.formattedMessage()));
Expand Down
3 changes: 3 additions & 0 deletions src/common/initialize-plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { CODIFY_FILE_REGEX, CodifyParser } from '../parser/index.js';
import { PluginManager, ResourceDefinitionMap } from '../plugins/plugin-manager.js';
import { Reporter } from '../ui/reporters/reporter.js';
import { FileUtils } from '../utils/file.js';
import { ShellUtils } from '../utils/shell.js';

export interface InitializeArgs {
path?: string;
Expand All @@ -34,6 +35,8 @@ export class PluginInitOrchestrator {
args: InitializeArgs,
reporter: Reporter,
): Promise<InitializationResult> {
await ShellUtils.validateShell();

const project = await PluginInitOrchestrator.parseProject(
args,
reporter
Expand Down
4 changes: 4 additions & 0 deletions src/entities/apply-note.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface ApplyNote {
message: string;
resourceType?: string;
}
6 changes: 4 additions & 2 deletions src/entities/apply-result.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ResourceOperation } from '@codifycli/schemas';

import { PluginError } from '../common/errors.js';
import { ApplyNote } from './apply-note.js';
import { ResourcePlan } from './plan.js';

export interface ApplyResultEntry {
Expand All @@ -13,6 +14,7 @@ export interface ApplyResultEntry {
export interface ApplyResult {
entries: ApplyResultEntry[];
errors: PluginError[];
notes: ApplyNote[];

isPartialFailure(): boolean;
}
Expand All @@ -21,9 +23,8 @@ export function createApplyResult(
succeededPlans: ResourcePlan[],
failedErrors: PluginError[],
skippedIds: Set<string>,
notes: ApplyNote[] = [],
): ApplyResult {
const failedByType = new Map(failedErrors.map((e) => [e.resourceType, e]));

const entries: ApplyResultEntry[] = [
...succeededPlans.map((p) => ({
id: p.id,
Expand All @@ -46,6 +47,7 @@ export function createApplyResult(
return {
entries,
errors: failedErrors,
notes,
isPartialFailure() {
return failedErrors.length > 0;
},
Expand Down
7 changes: 6 additions & 1 deletion src/events/context.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CommandRequestData, CommandRequestResponseData } from '@codifycli/schemas';
import { ApplyNoteRequestData, CommandRequestData, CommandRequestResponseData } from '@codifycli/schemas';
import { EventEmitter } from 'node:events';

export enum Event {
Expand All @@ -18,6 +18,7 @@ export enum Event {
PRESS_KEY_TO_CONTINUE_COMPLETED = 'press_key_to_continue_completed',
CODIFY_LOGIN_CREDENTIALS_REQUEST = 'codify_login_credentials_request',
CODIFY_LOGIN_CREDENTIALS_COMPLETED = 'codify_login_credentials_completed',
APPLY_NOTE_REQUEST = 'apply_note_request',
}

export enum ProcessName {
Expand Down Expand Up @@ -141,6 +142,10 @@ export const ctx = new class {
this.emitter.emit(Event.CODIFY_LOGIN_CREDENTIALS_COMPLETED, pluginName, credentials);
}

applyNoteRequested(pluginName: string, data: ApplyNoteRequestData) {
this.emitter.emit(Event.APPLY_NOTE_REQUEST, pluginName, data);
}

async subprocess<T>(name: string, run: () => Promise<T>): Promise<T> {
this.emitter.emit(Event.SUB_PROCESS_START, name);
const result = await run();
Expand Down
13 changes: 11 additions & 2 deletions src/plugins/plugin-manager.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import {
ApplyNoteRequestData,
ImportResponseData, ResourceDefinition,
ResourceJson,
ValidateResponseData,
} from '@codifycli/schemas';

import { InternalError, PluginError } from '../common/errors.js';
import { config } from '../config.js';
import { ApplyNote } from '../entities/apply-note.js';
import { ApplyResult, createApplyResult } from '../entities/apply-result.js';
import { Plan, ResourcePlan } from '../entities/plan.js';
import { Project } from '../entities/project.js';
import { ResourceConfig } from '../entities/resource-config.js';
import { ResourceInfo } from '../entities/resource-info.js';
import { SubProcessName, SubprocessFinishStatus, ctx } from '../events/context.js';
import { Event, SubProcessName, SubprocessFinishStatus, ctx } from '../events/context.js';
import { groupBy } from '../utils/index.js';
import { registerKillListeners } from '../utils/register-kill-listeners.js';
import { Plugin } from './plugin.js';
Expand Down Expand Up @@ -142,6 +144,12 @@ export class PluginManager {
const collectedErrors: PluginError[] = [];
const skippedIds = new Set<string>();
const succeededPlans: ResourcePlan[] = [];
const collectedNotes: ApplyNote[] = [];

const noteListener = (_pluginName: string, data: ApplyNoteRequestData) => {
collectedNotes.push({ message: data.message, resourceType: data.resourceType });
};
ctx.on(Event.APPLY_NOTE_REQUEST, noteListener);

for (const id of project.evaluationOrder ?? []) {
if (skippedIds.has(id)) {
Expand Down Expand Up @@ -179,7 +187,8 @@ export class PluginManager {
}
}

return createApplyResult(succeededPlans, collectedErrors, skippedIds);
ctx.emitter.removeListener(Event.APPLY_NOTE_REQUEST, noteListener);
return createApplyResult(succeededPlans, collectedErrors, skippedIds, collectedNotes);
}

async setVerbosityLevel(verbosityLevel: number): Promise<void> {
Expand Down
18 changes: 18 additions & 0 deletions src/plugins/plugin-process.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import {
ApplyNoteRequestData,
ApplyNoteRequestDataSchema,
CommandRequestData,
CommandRequestDataSchema,
CommandRequestResponseData,
Expand All @@ -19,6 +21,7 @@ import { PluginMessage } from './plugin-message.js';
export const ipcMessageValidator = ajv.compile(IpcMessageV2Schema);
export const commandRequestValidator = ajv.compile(CommandRequestDataSchema);
export const pressKeyToContinueRequestValidator = ajv.compile(PressKeyToContinueRequestDataSchema);
export const applyNoteRequestValidator = ajv.compile(ApplyNoteRequestDataSchema);

const DEFAULT_NODE_MODULES_DIR = '/usr/local/lib/codify/node_modules/'

Expand Down Expand Up @@ -122,6 +125,21 @@ export class PluginProcess {
}


if (message.cmd === MessageCmd.APPLY_NOTE_REQUEST) {
const { data, requestId } = message;
if (!applyNoteRequestValidator(data)) {
throw new Error(`Invalid apply note request from plugin ${pluginName}. ${JSON.stringify(applyNoteRequestValidator.errors, null, 2)}`);
}

process.send({
cmd: returnMessageCmd(MessageCmd.APPLY_NOTE_REQUEST),
requestId,
data: {},
});

return ctx.applyNoteRequested(pluginName, data as unknown as ApplyNoteRequestData);
}

if (message.cmd === MessageCmd.CODIFY_CREDENTIALS_REQUEST) {
if (pluginName !== 'default') {
throw new Error(`Only the default Codify plugin is able to request Codify credentials. ${pluginName}`);
Expand Down
Loading
Loading