From 4e8b611d4c71e94bcae752a863cd0fd381d5a73f Mon Sep 17 00:00:00 2001 From: kevinwang Date: Mon, 1 Jun 2026 23:08:13 -0400 Subject: [PATCH 1/6] fix: fixed codify edit desktop installation. Fixed apply message --- package-lock.json | 31 +++----- package.json | 4 +- src/ui/components/widgets/ApplyComplete.tsx | 4 +- src/utils/desktop-installer.ts | 20 ++++-- src/utils/shell.ts | 78 +++++++++++++++++++++ 5 files changed, 108 insertions(+), 29 deletions(-) diff --git a/package-lock.json b/package-lock.json index ab8a6b8..de48d96 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,18 @@ { "name": "codify", - "version": "1.1.0-beta6", + "version": "1.1.1-beta.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codify", - "version": "1.1.0-beta6", + "version": "1.1.1-beta.2", + "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "@codifycli/ink-form": "0.0.12", "@codifycli/schemas": "1.1.0-beta8", - "@homebridge/node-pty-prebuilt-multiarch": "^0.12.0-beta.5", + "@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", @@ -1113,21 +1114,6 @@ "node": ">=22.0.0" } }, - "node_modules/@codifycli/plugin-core/node_modules/@homebridge/node-pty-prebuilt-multiarch": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/@homebridge/node-pty-prebuilt-multiarch/-/node-pty-prebuilt-multiarch-0.13.1.tgz", - "integrity": "sha512-ccQ60nMcbEGrQh0U9E6x0ajW9qJNeazpcM/9CH6J8leyNtJgb+gu24WTBAfBUVeO486ZhscnaxLEITI2HXwhow==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-addon-api": "^7.1.0", - "prebuild-install": "^7.1.2" - }, - "engines": { - "node": ">=18.0.0 <25.0.0" - } - }, "node_modules/@codifycli/plugin-core/node_modules/ajv-formats": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", @@ -2002,14 +1988,17 @@ } }, "node_modules/@homebridge/node-pty-prebuilt-multiarch": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@homebridge/node-pty-prebuilt-multiarch/-/node-pty-prebuilt-multiarch-0.12.0.tgz", - "integrity": "sha512-hJCGcfOnMeRh2KUdWPlVN/1egnfqI4yxgpDhqHSkF2DLn5fiJNdjEHHlcM1K2w9+QBmRE2D/wfmM4zUOb8aMyQ==", + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/@homebridge/node-pty-prebuilt-multiarch/-/node-pty-prebuilt-multiarch-0.13.1.tgz", + "integrity": "sha512-ccQ60nMcbEGrQh0U9E6x0ajW9qJNeazpcM/9CH6J8leyNtJgb+gu24WTBAfBUVeO486ZhscnaxLEITI2HXwhow==", "hasInstallScript": true, "license": "MIT", "dependencies": { "node-addon-api": "^7.1.0", "prebuild-install": "^7.1.2" + }, + "engines": { + "node": ">=18.0.0 <25.0.0" } }, "node_modules/@humanfs/core": { diff --git a/package.json b/package.json index 75e6443..86c6fde 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "dependencies": { "@codifycli/ink-form": "0.0.12", "@codifycli/schemas": "1.1.0-beta8", - "@homebridge/node-pty-prebuilt-multiarch": "^0.12.0-beta.5", + "@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", @@ -145,7 +145,7 @@ "deploy": "npm run pkg && npm run notarize && npm run upload", "prepublishOnly": "npm run build" }, - "version": "1.1.0", + "version": "1.1.1-beta.6", "bugs": "https://github.com/codifycli/codify/issues", "keywords": [ "oclif", diff --git a/src/ui/components/widgets/ApplyComplete.tsx b/src/ui/components/widgets/ApplyComplete.tsx index 8ec2d79..a3331d1 100644 --- a/src/ui/components/widgets/ApplyComplete.tsx +++ b/src/ui/components/widgets/ApplyComplete.tsx @@ -1,11 +1,13 @@ import { ResourceOperation } from '@codifycli/schemas'; import { Box, Text } from 'ink'; +import path from 'node:path'; import React from 'react'; import { ApplyResult } from '../../../entities/apply-result.js'; import { ResourcePlan } from '../../../entities/plan.js'; import { applyEntryInkColor, applyEntryLabel } from '../../apply-result-formatter.js'; import { prettyFormatResourcePlan } from '../../plan-pretty-printer.js'; +import { ShellUtils } from '../../../utils/shell.js'; export function ApplyComplete({ result }: { result: ApplyResult }) { const isPartial = result.isPartialFailure(); @@ -93,7 +95,7 @@ export function ApplyComplete({ result }: { result: ApplyResult }) { {!isPartial && ( - Open a new terminal or source '.zshrc' for the new changes to be reflected + Open a new terminal or source '{path.basename(ShellUtils.getPrimaryShellRc())}' for the new changes to be reflected )} diff --git a/src/utils/desktop-installer.ts b/src/utils/desktop-installer.ts index c3f790c..6208d6d 100644 --- a/src/utils/desktop-installer.ts +++ b/src/utils/desktop-installer.ts @@ -9,7 +9,7 @@ import { spawn } from './spawn.js'; const DESKTOP_APP_PATHS = { darwin: '/Applications/Codify.app', - linux: '/usr/bin/codify', + linux: '/usr/bin/codify-desktop', }; const DOWNLOAD_URLS: Record> = { @@ -86,7 +86,7 @@ export async function installDesktopApp(reporter: Reporter, url: string, platfor } } else { const password = await reporter.promptSudo('codify-installer', { - command: platform === 'linux_deb' ? `dpkg -i ${tmpFile}` : `rpm -i ${tmpFile}`, + command: platform === 'linux_deb' ? `apt install -y ${tmpFile}` : `rpm -i ${tmpFile}`, options: { requiresRoot: true }, }); @@ -97,12 +97,22 @@ export async function installDesktopApp(reporter: Reporter, url: string, platfor try { console.log('Installing Codify desktop app...'); - const cmd = platform === 'linux_deb' ? `dpkg -i ${tmpFile}` : `rpm -i ${tmpFile}`; - await spawn(cmd, { requiresRoot: true }, undefined, password); + const cmd = platform === 'linux_deb' ? `apt install -y ${tmpFile}` : `rpm -i ${tmpFile}`; + try { + await spawn(cmd, { requiresRoot: true }, undefined, password); + } catch (e) { + if (platform === 'linux_deb') { + console.log('Fixing broken dependencies...'); + await spawn('apt-get install -f -y', { requiresRoot: true }, undefined, password); + await spawn(cmd, { requiresRoot: true }, undefined, password); + } else { + throw e; + } + } } finally { await fs.unlink(tmpFile).catch(() => {}); } } console.log('Codify desktop app installed successfully.'); -} \ No newline at end of file +} diff --git a/src/utils/shell.ts b/src/utils/shell.ts index 29709d3..d194970 100644 --- a/src/utils/shell.ts +++ b/src/utils/shell.ts @@ -2,6 +2,8 @@ import { LinuxDistro } from '@codifycli/schemas'; import cp from 'node:child_process'; import * as fs from 'node:fs/promises'; import util from 'node:util'; +import os from 'node:os'; +import path from 'node:path'; const exec = util.promisify(cp.exec); @@ -50,6 +52,82 @@ export const ShellUtils = { return process.env.SHELL!; }, + getPrimaryShellRc(): string { + return this.getShellRcFiles()[0]; + }, + + getShellRcFiles(): string[] { + const shell = process.env.SHELL || os.userInfo().shell || ''; + const homeDir = os.homedir(); + + if (shell.endsWith('bash')) { + // Linux typically uses .bashrc, macOS uses .bash_profile + if (ShellUtils.isLinux()) { + return [ + path.join(homeDir, '.bashrc'), + path.join(homeDir, '.bash_profile'), + path.join(homeDir, '.profile'), + ]; + } + + return [ + path.join(homeDir, '.bash_profile'), + path.join(homeDir, '.bashrc'), + path.join(homeDir, '.profile'), + ]; + } + + if (shell.endsWith('zsh')) { + return [ + path.join(homeDir, '.zshrc'), + path.join(homeDir, '.zprofile'), + path.join(homeDir, '.zshenv'), + ]; + } + + if (shell.endsWith('sh')) { + return [ + path.join(homeDir, '.profile'), + ] + } + + if (shell.endsWith('ksh')) { + return [ + path.join(homeDir, '.profile'), + path.join(homeDir, '.kshrc'), + ] + } + + if (shell.endsWith('csh')) { + return [ + path.join(homeDir, '.cshrc'), + path.join(homeDir, '.login'), + path.join(homeDir, '.logout'), + ] + } + + if (shell.endsWith('fish')) { + return [ + path.join(homeDir, '.config/fish/config.fish'), + ] + } + + // Default to bash-style files + return [ + path.join(homeDir, '.bashrc'), + path.join(homeDir, '.bash_profile'), + path.join(homeDir, '.profile'), + ]; + }, + + isMacOS(): boolean { + return os.platform() === 'darwin'; + }, + + isLinux(): boolean { + return os.platform() === 'linux'; + }, + async getLinuxDistro(): Promise { for (const candidate of ['/etc/os-release', '/usr/lib/os-release']) { let osRelease: string; From 793cb91eedfbfd1af7b6756c36819cba2da93175 Mon Sep 17 00:00:00 2001 From: kevinwang Date: Thu, 4 Jun 2026 21:01:57 -0400 Subject: [PATCH 2/6] chore: update download links for desktop app --- src/utils/desktop-installer.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/utils/desktop-installer.ts b/src/utils/desktop-installer.ts index 6208d6d..b90aa86 100644 --- a/src/utils/desktop-installer.ts +++ b/src/utils/desktop-installer.ts @@ -8,22 +8,22 @@ import { OsUtils } from './os-utils.js'; import { spawn } from './spawn.js'; const DESKTOP_APP_PATHS = { - darwin: '/Applications/Codify.app', + darwin: '/Applications/CodifyApp.app', linux: '/usr/bin/codify-desktop', }; const DOWNLOAD_URLS: Record> = { darwin: { - arm64: 'https://releases-desktop.codifycli.com/channels/stable/Codify_aarch64.dmg', - x64: 'https://releases-desktop.codifycli.com/channels/stable/Codify_x64.dmg', + arm64: 'https://releases-desktop.codifycli.com/channels/stable/CodifyApp_aarch64.dmg', + x64: 'https://releases-desktop.codifycli.com/channels/stable/CodifyApp_x64.dmg', }, linux_deb: { - arm64: 'https://releases-desktop.codifycli.com/channels/stable/Codify_arm64.deb', - x64: 'https://releases-desktop.codifycli.com/channels/stable/Codify_amd64.deb', + arm64: 'https://releases-desktop.codifycli.com/channels/stable/CodifyApp_arm64.deb', + x64: 'https://releases-desktop.codifycli.com/channels/stable/CodifyApp_amd64.deb', }, linux_rpm: { - aarch64: 'https://releases-desktop.codifycli.com/channels/stable/Codify_aarch64.rpm', - x64: 'https://releases-desktop.codifycli.com/channels/stable/Codify_x86_64.rpm', + aarch64: 'https://releases-desktop.codifycli.com/channels/stable/CodifyApp_aarch64.rpm', + x64: 'https://releases-desktop.codifycli.com/channels/stable/CodifyApp_x86_64.rpm', }, }; @@ -79,7 +79,7 @@ export async function installDesktopApp(reporter: Reporter, url: string, platfor try { console.log('Installing Codify desktop app...'); await spawn(`hdiutil attach ${tmpFile} -mountpoint ${mountPoint} -nobrowse -quiet`); - await spawn(`cp -R ${mountPoint}/Codify.app /Applications/Codify.app`); + await spawn(`cp -R ${mountPoint}/CodifyApp.app /Applications/CodifyApp.app`); } finally { await spawn(`hdiutil detach ${mountPoint} -quiet`).catch(() => {}); await fs.unlink(tmpFile).catch(() => {}); From 3999b9f45186510307f219d71c980538a3a3f0ac Mon Sep 17 00:00:00 2001 From: kevinwang Date: Thu, 4 Jun 2026 21:47:47 -0400 Subject: [PATCH 3/6] feat: Added apply notes system --- package-lock.json | 22 ++++++++--------- package.json | 4 +-- src/entities/apply-note.ts | 4 +++ src/entities/apply-result.ts | 6 +++-- src/events/context.ts | 7 +++++- src/plugins/plugin-manager.ts | 13 ++++++++-- src/plugins/plugin-process.ts | 18 ++++++++++++++ src/ui/components/widgets/ApplyComplete.tsx | 27 +++++++++++++++++++++ src/ui/reporters/json-reporter.ts | 1 + src/ui/reporters/plain-reporter.ts | 23 ++++++++++++++++++ 10 files changed, 107 insertions(+), 18 deletions(-) create mode 100644 src/entities/apply-note.ts diff --git a/package-lock.json b/package-lock.json index de48d96..aeff3cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,17 @@ { "name": "codify", - "version": "1.1.1-beta.2", + "version": "1.1.1-beta.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codify", - "version": "1.1.1-beta.2", + "version": "1.1.1-beta.6", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "@codifycli/ink-form": "0.0.12", - "@codifycli/schemas": "1.1.0-beta8", + "@codifycli/schemas": "1.1.0-beta.9", "@homebridge/node-pty-prebuilt-multiarch": "^0.13.1", "@mischnic/json-sourcemap": "^0.1.1", "@oclif/core": "^4.0.8", @@ -51,7 +51,7 @@ "codify": "bin/run.js" }, "devDependencies": { - "@codifycli/plugin-core": "^1.1.0-beta19", + "@codifycli/plugin-core": "^1.1.0-beta.25", "@oclif/prettier-config": "^0.2.1", "@types/chalk": "^2.2.0", "@types/cors": "^2.8.19", @@ -1090,13 +1090,13 @@ } }, "node_modules/@codifycli/plugin-core": { - "version": "1.1.0-beta19", - "resolved": "https://registry.npmjs.org/@codifycli/plugin-core/-/plugin-core-1.1.0-beta19.tgz", - "integrity": "sha512-ci8QU2xn3Zl50EdCA1ymi2KiwDQO43t27fG7cRqBnbCpQZgVtlSyV18xLd3td6rzigVVDNtCSY3a6ZayM7zhpg==", + "version": "1.1.0-beta.25", + "resolved": "https://npm.pkg.github.com/download/@codifycli/plugin-core/1.1.0-beta.25/d79d0b334c36141e72195380e7dcf965f1fdbd7c", + "integrity": "sha512-VhrteXubY/+jRCsvoDPYTot50lvOTJS4xGjgos4/A3ZSYcIxG1Nv5erBKGaQ0dhiFGRzG72qhS7XhlYnh4q+Lg==", "dev": true, "license": "ISC", "dependencies": { - "@codifycli/schemas": "^1.1.0-beta8", + "@codifycli/schemas": "^1.1.0-beta.9", "@homebridge/node-pty-prebuilt-multiarch": "^0.13.1", "ajv": "^8.18.0", "ajv-formats": "^2.1.1", @@ -1147,9 +1147,9 @@ } }, "node_modules/@codifycli/schemas": { - "version": "1.1.0-beta8", - "resolved": "https://registry.npmjs.org/@codifycli/schemas/-/schemas-1.1.0-beta8.tgz", - "integrity": "sha512-2PLCPmU2mtDilqx71uQIjpZLnvqSkdSR+BgImN6eRbRWKJcfltBEONPAlRhRU74kAyURpqCfDSLKTYa1MqLxZw==", + "version": "1.1.0-beta.9", + "resolved": "https://npm.pkg.github.com/download/@codifycli/schemas/1.1.0-beta.9/bbca84aee2e36bf4c8dec31492751214d617373c", + "integrity": "sha512-y5IGCN3ZvxkQS2b4WUcmdBVXSN/2qY/rYVtkjtPNcaNiwwg36bfxiClcPpc1AE5IfQ4VRGekniKOA3BGKw52FQ==", "license": "ISC", "dependencies": { "ajv": "^8.18.0" diff --git a/package.json b/package.json index 86c6fde..52e0ea8 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ }, "dependencies": { "@codifycli/ink-form": "0.0.12", - "@codifycli/schemas": "1.1.0-beta8", + "@codifycli/schemas": "1.1.0-beta.9", "@homebridge/node-pty-prebuilt-multiarch": "^0.13.1", "@mischnic/json-sourcemap": "^0.1.1", "@oclif/core": "^4.0.8", @@ -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.1.0-beta.25", "@oclif/prettier-config": "^0.2.1", "@types/chalk": "^2.2.0", "@types/cors": "^2.8.19", diff --git a/src/entities/apply-note.ts b/src/entities/apply-note.ts new file mode 100644 index 0000000..365d454 --- /dev/null +++ b/src/entities/apply-note.ts @@ -0,0 +1,4 @@ +export interface ApplyNote { + message: string; + resourceType: string; +} diff --git a/src/entities/apply-result.ts b/src/entities/apply-result.ts index 656424d..c65f7ab 100644 --- a/src/entities/apply-result.ts +++ b/src/entities/apply-result.ts @@ -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 { @@ -13,6 +14,7 @@ export interface ApplyResultEntry { export interface ApplyResult { entries: ApplyResultEntry[]; errors: PluginError[]; + notes: ApplyNote[]; isPartialFailure(): boolean; } @@ -21,9 +23,8 @@ export function createApplyResult( succeededPlans: ResourcePlan[], failedErrors: PluginError[], skippedIds: Set, + notes: ApplyNote[] = [], ): ApplyResult { - const failedByType = new Map(failedErrors.map((e) => [e.resourceType, e])); - const entries: ApplyResultEntry[] = [ ...succeededPlans.map((p) => ({ id: p.id, @@ -46,6 +47,7 @@ export function createApplyResult( return { entries, errors: failedErrors, + notes, isPartialFailure() { return failedErrors.length > 0; }, diff --git a/src/events/context.ts b/src/events/context.ts index c3b0dfa..b634927 100644 --- a/src/events/context.ts +++ b/src/events/context.ts @@ -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 { @@ -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 { @@ -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(name: string, run: () => Promise): Promise { this.emitter.emit(Event.SUB_PROCESS_START, name); const result = await run(); diff --git a/src/plugins/plugin-manager.ts b/src/plugins/plugin-manager.ts index 9435bfd..bb5f07a 100644 --- a/src/plugins/plugin-manager.ts +++ b/src/plugins/plugin-manager.ts @@ -1,4 +1,5 @@ import { + ApplyNoteRequestData, ImportResponseData, ResourceDefinition, ResourceJson, ValidateResponseData, @@ -6,12 +7,13 @@ import { 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'; @@ -142,6 +144,12 @@ export class PluginManager { const collectedErrors: PluginError[] = []; const skippedIds = new Set(); 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)) { @@ -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 { diff --git a/src/plugins/plugin-process.ts b/src/plugins/plugin-process.ts index 63e6f8f..813c337 100644 --- a/src/plugins/plugin-process.ts +++ b/src/plugins/plugin-process.ts @@ -1,4 +1,6 @@ import { + ApplyNoteRequestData, + ApplyNoteRequestDataSchema, CommandRequestData, CommandRequestDataSchema, CommandRequestResponseData, @@ -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/' @@ -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}`); diff --git a/src/ui/components/widgets/ApplyComplete.tsx b/src/ui/components/widgets/ApplyComplete.tsx index a3331d1..da0f76e 100644 --- a/src/ui/components/widgets/ApplyComplete.tsx +++ b/src/ui/components/widgets/ApplyComplete.tsx @@ -93,6 +93,18 @@ export function ApplyComplete({ result }: { result: ApplyResult }) { )} + {result.notes.length > 0 && ( + + {groupNotesByMessage(result.notes).map(({ message, resourceTypes }) => ( + + {'⚠ '} + {resourceTypes.join(', ')}: + {message} + + ))} + + )} + {!isPartial && ( Open a new terminal or source '{path.basename(ShellUtils.getPrimaryShellRc())}' for the new changes to be reflected @@ -102,3 +114,18 @@ export function ApplyComplete({ result }: { result: ApplyResult }) { ); } + +function groupNotesByMessage(notes: ApplyResult['notes']): { message: string; resourceTypes: string[] }[] { + const map = new Map(); + for (const note of notes) { + const existing = map.get(note.message); + if (existing) { + if (!existing.includes(note.resourceType)) { + existing.push(note.resourceType); + } + } else { + map.set(note.message, [note.resourceType]); + } + } + return [...map.entries()].map(([message, resourceTypes]) => ({ message, resourceTypes })); +} diff --git a/src/ui/reporters/json-reporter.ts b/src/ui/reporters/json-reporter.ts index 84533a6..d1bcd16 100644 --- a/src/ui/reporters/json-reporter.ts +++ b/src/ui/reporters/json-reporter.ts @@ -99,6 +99,7 @@ export class JsonReporter implements Reporter { resourceType: error.resourceType, data: error.errorData.data, })), + notes: result.notes, }, null, 2)); } } diff --git a/src/ui/reporters/plain-reporter.ts b/src/ui/reporters/plain-reporter.ts index c988cf9..87b973c 100644 --- a/src/ui/reporters/plain-reporter.ts +++ b/src/ui/reporters/plain-reporter.ts @@ -200,5 +200,28 @@ Use this init flow to get started quickly with Codify. ctx.log(''); ctx.log('Open a new terminal or source \'.zshrc\' for the new changes to be reflected'); } + + if (result.notes.length > 0) { + ctx.log(''); + const grouped = groupNotesByMessage(result.notes); + for (const { message, resourceTypes } of grouped) { + ctx.log(chalk.yellow(`⚠ ${resourceTypes.join(', ')}: ${message}`)); + } + } + } +} + +function groupNotesByMessage(notes: ApplyResult['notes']): { message: string; resourceTypes: string[] }[] { + const map = new Map(); + for (const note of notes) { + const existing = map.get(note.message); + if (existing) { + if (!existing.includes(note.resourceType)) { + existing.push(note.resourceType); + } + } else { + map.set(note.message, [note.resourceType]); + } } + return [...map.entries()].map(([message, resourceTypes]) => ({ message, resourceTypes })); } From 21288ccfc2e2602c537b5931a7ddf655bea9c4a9 Mon Sep 17 00:00:00 2001 From: kevinwang Date: Thu, 4 Jun 2026 22:27:04 -0400 Subject: [PATCH 4/6] feat: Made modifications to apply note. Improved the pretty printing of object plans --- package-lock.json | 8 +- package.json | 2 +- src/entities/apply-note.ts | 2 +- src/ui/components/widgets/ApplyComplete.tsx | 14 +-- src/ui/plan-pretty-printer.ts | 111 ++++++++++++++++++-- src/ui/reporters/plain-reporter.ts | 10 +- test/utils/plan-pretty-printer.test.ts | 39 +++++++ 7 files changed, 154 insertions(+), 32 deletions(-) diff --git a/package-lock.json b/package-lock.json index aeff3cc..91c0c0e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "license": "Apache-2.0", "dependencies": { "@codifycli/ink-form": "0.0.12", - "@codifycli/schemas": "1.1.0-beta.9", + "@codifycli/schemas": "1.1.0-beta.10", "@homebridge/node-pty-prebuilt-multiarch": "^0.13.1", "@mischnic/json-sourcemap": "^0.1.1", "@oclif/core": "^4.0.8", @@ -1147,9 +1147,9 @@ } }, "node_modules/@codifycli/schemas": { - "version": "1.1.0-beta.9", - "resolved": "https://npm.pkg.github.com/download/@codifycli/schemas/1.1.0-beta.9/bbca84aee2e36bf4c8dec31492751214d617373c", - "integrity": "sha512-y5IGCN3ZvxkQS2b4WUcmdBVXSN/2qY/rYVtkjtPNcaNiwwg36bfxiClcPpc1AE5IfQ4VRGekniKOA3BGKw52FQ==", + "version": "1.1.0-beta.10", + "resolved": "https://npm.pkg.github.com/download/@codifycli/schemas/1.1.0-beta.10/bdf6998e4c3442c6348509a17d87ab41b8ca769c", + "integrity": "sha512-gmYKVM56EqeltOwjiO5xNGanbbxadUFhAJDCCCmTPsVVUMXxApfDOy5NdSYqSzHrbyTmH4KGZW4EOb9uY4rwFw==", "license": "ISC", "dependencies": { "ajv": "^8.18.0" diff --git a/package.json b/package.json index 52e0ea8..01cffbe 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ }, "dependencies": { "@codifycli/ink-form": "0.0.12", - "@codifycli/schemas": "1.1.0-beta.9", + "@codifycli/schemas": "1.1.0-beta.10", "@homebridge/node-pty-prebuilt-multiarch": "^0.13.1", "@mischnic/json-sourcemap": "^0.1.1", "@oclif/core": "^4.0.8", diff --git a/src/entities/apply-note.ts b/src/entities/apply-note.ts index 365d454..47ac871 100644 --- a/src/entities/apply-note.ts +++ b/src/entities/apply-note.ts @@ -1,4 +1,4 @@ export interface ApplyNote { message: string; - resourceType: string; + resourceType?: string; } diff --git a/src/ui/components/widgets/ApplyComplete.tsx b/src/ui/components/widgets/ApplyComplete.tsx index da0f76e..d146f24 100644 --- a/src/ui/components/widgets/ApplyComplete.tsx +++ b/src/ui/components/widgets/ApplyComplete.tsx @@ -1,13 +1,11 @@ import { ResourceOperation } from '@codifycli/schemas'; import { Box, Text } from 'ink'; -import path from 'node:path'; import React from 'react'; import { ApplyResult } from '../../../entities/apply-result.js'; import { ResourcePlan } from '../../../entities/plan.js'; import { applyEntryInkColor, applyEntryLabel } from '../../apply-result-formatter.js'; import { prettyFormatResourcePlan } from '../../plan-pretty-printer.js'; -import { ShellUtils } from '../../../utils/shell.js'; export function ApplyComplete({ result }: { result: ApplyResult }) { const isPartial = result.isPartialFailure(); @@ -98,19 +96,13 @@ export function ApplyComplete({ result }: { result: ApplyResult }) { {groupNotesByMessage(result.notes).map(({ message, resourceTypes }) => ( {'⚠ '} - {resourceTypes.join(', ')}: + {resourceTypes.length > 0 && {resourceTypes.join(', ')}: } {message} ))} )} - {!isPartial && ( - - Open a new terminal or source '{path.basename(ShellUtils.getPrimaryShellRc())}' for the new changes to be reflected - - )} - ); } @@ -120,11 +112,11 @@ function groupNotesByMessage(notes: ApplyResult['notes']): { message: string; re for (const note of notes) { const existing = map.get(note.message); if (existing) { - if (!existing.includes(note.resourceType)) { + if (note.resourceType && !existing.includes(note.resourceType)) { existing.push(note.resourceType); } } else { - map.set(note.message, [note.resourceType]); + map.set(note.message, note.resourceType ? [note.resourceType] : []); } } return [...map.entries()].map(([message, resourceTypes]) => ({ message, resourceTypes })); diff --git a/src/ui/plan-pretty-printer.ts b/src/ui/plan-pretty-printer.ts index 4d05a93..410dc5b 100644 --- a/src/ui/plan-pretty-printer.ts +++ b/src/ui/plan-pretty-printer.ts @@ -1,4 +1,5 @@ import chalk from 'chalk'; +import * as Diff from 'diff'; import { ParameterOperation, PlanResponseData, ResourceOperation } from '@codifycli/schemas'; import { Plan, ResourcePlan } from '../entities/plan.js'; @@ -91,14 +92,31 @@ function prettyFormatModifyPlan(plan: ResourcePlan): string { ]; for (const parameter of plan.parameters) { - // TODO: Add support for object types as well in the future if ((Array.isArray(parameter.previousValue) || parameter.previousValue === null) && (Array.isArray(parameter.newValue) || parameter.newValue === null) && !(parameter.previousValue === null && parameter.newValue === null) && !parameter.isSensitive ) { - const line = formatArray(parameter); - builder.push(line); + builder.push(formatArray(parameter)); + } else if ( + !parameter.isSensitive + && isPlainObject(parameter.previousValue) + && isPlainObject(parameter.newValue) + && parameter.operation === ParameterOperation.MODIFY + ) { + builder.push(formatObjectDiff(parameter.name, parameter.previousValue, parameter.newValue)); + } else if ( + !parameter.isSensitive + && isPlainObject(parameter.newValue) + && (parameter.operation === ParameterOperation.ADD || parameter.operation === ParameterOperation.NOOP) + ) { + builder.push(formatObjectSingleSide(parameter.name, parameter.newValue, parameter.operation)); + } else if ( + !parameter.isSensitive + && isPlainObject(parameter.previousValue) + && parameter.operation === ParameterOperation.REMOVE + ) { + builder.push(formatObjectSingleSide(parameter.name, parameter.previousValue, parameter.operation)); } else { const formattedParameter = formatParameter(parameter); @@ -127,7 +145,7 @@ function formatParameter(parameter: PlanResponseData['parameters'][0]): string { return typeof parameter.newValue === 'string' ? `"${parameter.name}": "${escapeNewlines(value as string)}",` - : `"${parameter.name}": ${value},` + : `"${parameter.name}": ${typeof value === 'object' ? JSON.stringify(value) : value},` } case ParameterOperation.ADD: { @@ -135,7 +153,7 @@ function formatParameter(parameter: PlanResponseData['parameters'][0]): string { return typeof parameter.newValue === 'string' ? chalk.green(`"${parameter.name}": "${escapeNewlines(value as string)}",`) - : chalk.green(`"${parameter.name}": ${value},`) + : chalk.green(`"${parameter.name}": ${typeof value === 'object' ? JSON.stringify(value) : value},`) } case ParameterOperation.REMOVE: { @@ -143,16 +161,20 @@ function formatParameter(parameter: PlanResponseData['parameters'][0]): string { return typeof parameter.previousValue === 'string' ? chalk.red(`"${parameter.name}": "${escapeNewlines(value as string)}",`) - : chalk.red(`"${parameter.name}": ${value},`) + : chalk.red(`"${parameter.name}": ${typeof value === 'object' ? JSON.stringify(value) : value},`) } case ParameterOperation.MODIFY: { const newValue = parameter.isSensitive ? '[Sensitive]' : parameter.newValue; const previousValue = parameter.isSensitive ? '[Sensitive]' : parameter.previousValue; - return typeof parameter.newValue === 'string' && typeof parameter.previousValue === 'string' - ? `"${parameter.name}": "${escapeNewlines(previousValue as string)}" -> "${escapeNewlines(newValue as string)}",` - : `"${parameter.name}": ${previousValue} -> ${newValue},` + if (typeof parameter.newValue === 'string' && typeof parameter.previousValue === 'string') { + return `"${parameter.name}": "${escapeNewlines(previousValue as string)}" -> "${escapeNewlines(newValue as string)}",`; + } + + const prevFormatted = typeof previousValue === 'object' ? JSON.stringify(previousValue) : previousValue; + const newFormatted = typeof newValue === 'object' ? JSON.stringify(newValue) : newValue; + return `"${parameter.name}": ${prevFormatted} -> ${newFormatted},`; } } } @@ -201,6 +223,77 @@ function operationSymbol(operation: ParameterOperation): string { } } +function isPlainObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function formatObjectDiff(name: string, previousValue: object, newValue: object): string { + const prevJson = JSON.stringify(previousValue, null, 2); + const newJson = JSON.stringify(newValue, null, 2); + const diff = Diff.diffLines(prevJson, newJson); + + const coloredLines: Array<{ text: string; added: boolean; removed: boolean }> = []; + for (const part of diff) { + const lines = part.value.split('\n').filter((l) => l.length > 0); + for (const line of lines) { + // Skip the outer { } braces — we render those as the header/footer + if (line === '{' || line === '}') continue; + coloredLines.push({ + text: part.added ? chalk.green(line) : part.removed ? chalk.red(line) : line, + added: part.added ?? false, + removed: part.removed ?? false, + }); + } + } + + const CONTEXT = 2; + const included = new Set(); + for (let i = 0; i < coloredLines.length; i++) { + if (coloredLines[i].added || coloredLines[i].removed) { + for (let j = Math.max(0, i - CONTEXT); j <= Math.min(coloredLines.length - 1, i + CONTEXT); j++) { + included.add(j); + } + } + } + + const resultLines: string[] = [`${chalk.yellow('~')} "${name}": {`]; + let lastIncluded = -1; + + for (let i = 0; i < coloredLines.length; i++) { + if (!included.has(i)) continue; + if (lastIncluded !== -1 && i > lastIncluded + 1) { + resultLines.push(' ...'); + } + const { text, added, removed } = coloredLines[i]; + const symbol = added ? chalk.green('+') : removed ? chalk.red('-') : ' '; + resultLines.push(` ${symbol} ${text}`); + lastIncluded = i; + } + + resultLines.push(' },'); + return resultLines.join('\n'); +} + +const OBJECT_SINGLE_SIDE_MAX_LINES = 20; + +function formatObjectSingleSide(name: string, value: object, operation: ParameterOperation): string { + const json = JSON.stringify(value, null, 2); + const lines = json.split('\n'); + const truncated = lines.length > OBJECT_SINGLE_SIDE_MAX_LINES; + const visibleLines = truncated ? lines.slice(0, OBJECT_SINGLE_SIDE_MAX_LINES) : lines; + + const colorFn = operation === ParameterOperation.REMOVE ? chalk.red : chalk.green; + const sym = operationSymbol(operation); + + const formatted = visibleLines + .map((l, idx) => idx === 0 ? `"${name}": ${l}` : l) + .map((l) => ` ${colorFn(l)}`) + .map((l, idx) => idx === 0 ? sym + l : ` ${l}`) + .join('\n'); + + return truncated ? formatted + '\n ...' : formatted + ','; +} + function formatArray(parameter: PlanResponseData['parameters'][0]): string { const { name, newValue, operation, previousValue } = parameter; const a = previousValue as null | unknown[]; diff --git a/src/ui/reporters/plain-reporter.ts b/src/ui/reporters/plain-reporter.ts index 87b973c..cedad43 100644 --- a/src/ui/reporters/plain-reporter.ts +++ b/src/ui/reporters/plain-reporter.ts @@ -196,16 +196,14 @@ Use this init flow to get started quickly with Codify. for (const error of result.errors) { await this.displayPluginError(error); } - } else { - ctx.log(''); - ctx.log('Open a new terminal or source \'.zshrc\' for the new changes to be reflected'); } if (result.notes.length > 0) { ctx.log(''); const grouped = groupNotesByMessage(result.notes); for (const { message, resourceTypes } of grouped) { - ctx.log(chalk.yellow(`⚠ ${resourceTypes.join(', ')}: ${message}`)); + const prefix = resourceTypes.length > 0 ? `${resourceTypes.join(', ')}: ` : ''; + ctx.log(chalk.yellow(`⚠ ${prefix}${message}`)); } } } @@ -216,11 +214,11 @@ function groupNotesByMessage(notes: ApplyResult['notes']): { message: string; re for (const note of notes) { const existing = map.get(note.message); if (existing) { - if (!existing.includes(note.resourceType)) { + if (note.resourceType && !existing.includes(note.resourceType)) { existing.push(note.resourceType); } } else { - map.set(note.message, [note.resourceType]); + map.set(note.message, note.resourceType ? [note.resourceType] : []); } } return [...map.entries()].map(([message, resourceTypes]) => ({ message, resourceTypes })); diff --git a/test/utils/plan-pretty-printer.test.ts b/test/utils/plan-pretty-printer.test.ts index 6047aa4..8b5225a 100644 --- a/test/utils/plan-pretty-printer.test.ts +++ b/test/utils/plan-pretty-printer.test.ts @@ -48,6 +48,45 @@ describe('Plan pretty printer', () => { console.log(prettyFormatResourcePlan(new ResourcePlan(plan))) }) + it('Can diff nested objects in modify plans', () => { + const plan: PlanResponseData = { + planId: 'id', + resourceType: 'macos-settings', + operation: ResourceOperation.MODIFY, + isStateful: false, + parameters: [ + { + name: 'mouse', + previousValue: { naturalScrolling: false }, + newValue: { naturalScrolling: true }, + operation: ParameterOperation.MODIFY, + isSensitive: false, + }, + { + name: 'trackpad', + previousValue: { + tapToClick: true, + scrollSpeed: 1, + pointerSpeed: 3, + naturalScrolling: false, + twoFingerSwipe: true, + }, + newValue: { + tapToClick: true, + scrollSpeed: 1, + pointerSpeed: 5, + naturalScrolling: false, + twoFingerSwipe: true, + }, + operation: ParameterOperation.MODIFY, + isSensitive: false, + }, + ] + } + + console.log(prettyFormatResourcePlan(new ResourcePlan(plan))) + }) + it('Can print modify and re-create plans', () => { const plan: PlanResponseData = { planId: 'id', From 4f81fa4eec6077be4bf4b94f53428e705ab6127a Mon Sep 17 00:00:00 2001 From: kevinwang Date: Fri, 5 Jun 2026 14:01:44 -0400 Subject: [PATCH 5/6] feat: Changed the diff display to be a structural diff --- package-lock.json | 18 +++++----- package.json | 6 ++-- src/ui/plan-pretty-printer.ts | 65 ++++++++++++++++++++--------------- 3 files changed, 49 insertions(+), 40 deletions(-) diff --git a/package-lock.json b/package-lock.json index 91c0c0e..82a1a79 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "license": "Apache-2.0", "dependencies": { "@codifycli/ink-form": "0.0.12", - "@codifycli/schemas": "1.1.0-beta.10", + "@codifycli/schemas": "1.2.0", "@homebridge/node-pty-prebuilt-multiarch": "^0.13.1", "@mischnic/json-sourcemap": "^0.1.1", "@oclif/core": "^4.0.8", @@ -51,7 +51,7 @@ "codify": "bin/run.js" }, "devDependencies": { - "@codifycli/plugin-core": "^1.1.0-beta.25", + "@codifycli/plugin-core": "^1.2.0", "@oclif/prettier-config": "^0.2.1", "@types/chalk": "^2.2.0", "@types/cors": "^2.8.19", @@ -1090,13 +1090,13 @@ } }, "node_modules/@codifycli/plugin-core": { - "version": "1.1.0-beta.25", - "resolved": "https://npm.pkg.github.com/download/@codifycli/plugin-core/1.1.0-beta.25/d79d0b334c36141e72195380e7dcf965f1fdbd7c", - "integrity": "sha512-VhrteXubY/+jRCsvoDPYTot50lvOTJS4xGjgos4/A3ZSYcIxG1Nv5erBKGaQ0dhiFGRzG72qhS7XhlYnh4q+Lg==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@codifycli/plugin-core/-/plugin-core-1.2.0.tgz", + "integrity": "sha512-GvGRSZ1xtwF5TiiauV/VUGNnJPQ6TUhtGZfXqnIwCozdPgTFp3AYH49q7Pbd7AYAG+5pnFUa9J4yO6WNUfDeWA==", "dev": true, "license": "ISC", "dependencies": { - "@codifycli/schemas": "^1.1.0-beta.9", + "@codifycli/schemas": "^1.2.0", "@homebridge/node-pty-prebuilt-multiarch": "^0.13.1", "ajv": "^8.18.0", "ajv-formats": "^2.1.1", @@ -1147,9 +1147,9 @@ } }, "node_modules/@codifycli/schemas": { - "version": "1.1.0-beta.10", - "resolved": "https://npm.pkg.github.com/download/@codifycli/schemas/1.1.0-beta.10/bdf6998e4c3442c6348509a17d87ab41b8ca769c", - "integrity": "sha512-gmYKVM56EqeltOwjiO5xNGanbbxadUFhAJDCCCmTPsVVUMXxApfDOy5NdSYqSzHrbyTmH4KGZW4EOb9uY4rwFw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@codifycli/schemas/-/schemas-1.2.0.tgz", + "integrity": "sha512-ZUx8+IsW8ZvBWu+ilbUsK+vE1oMaiDvsDlgQoe92scRZUh1pFMPw6303N1T9BTep+sooRE5X4Y9IRloPQOjRjQ==", "license": "ISC", "dependencies": { "ajv": "^8.18.0" diff --git a/package.json b/package.json index 01cffbe..7142632 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ }, "dependencies": { "@codifycli/ink-form": "0.0.12", - "@codifycli/schemas": "1.1.0-beta.10", + "@codifycli/schemas": "1.2.0", "@homebridge/node-pty-prebuilt-multiarch": "^0.13.1", "@mischnic/json-sourcemap": "^0.1.1", "@oclif/core": "^4.0.8", @@ -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-beta.25", + "@codifycli/plugin-core": "^1.2.0", "@oclif/prettier-config": "^0.2.1", "@types/chalk": "^2.2.0", "@types/cors": "^2.8.19", @@ -145,7 +145,7 @@ "deploy": "npm run pkg && npm run notarize && npm run upload", "prepublishOnly": "npm run build" }, - "version": "1.1.1-beta.6", + "version": "1.2.0-beta.1", "bugs": "https://github.com/codifycli/codify/issues", "keywords": [ "oclif", diff --git a/src/ui/plan-pretty-printer.ts b/src/ui/plan-pretty-printer.ts index 410dc5b..d95521b 100644 --- a/src/ui/plan-pretty-printer.ts +++ b/src/ui/plan-pretty-printer.ts @@ -1,5 +1,4 @@ import chalk from 'chalk'; -import * as Diff from 'diff'; import { ParameterOperation, PlanResponseData, ResourceOperation } from '@codifycli/schemas'; import { Plan, ResourcePlan } from '../entities/plan.js'; @@ -227,31 +226,27 @@ function isPlainObject(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value); } -function formatObjectDiff(name: string, previousValue: object, newValue: object): string { - const prevJson = JSON.stringify(previousValue, null, 2); - const newJson = JSON.stringify(newValue, null, 2); - const diff = Diff.diffLines(prevJson, newJson); - - const coloredLines: Array<{ text: string; added: boolean; removed: boolean }> = []; - for (const part of diff) { - const lines = part.value.split('\n').filter((l) => l.length > 0); - for (const line of lines) { - // Skip the outer { } braces — we render those as the header/footer - if (line === '{' || line === '}') continue; - coloredLines.push({ - text: part.added ? chalk.green(line) : part.removed ? chalk.red(line) : line, - added: part.added ?? false, - removed: part.removed ?? false, - }); +function formatObjectDiff(name: string, previousValue: Record, newValue: Record): string { + const allKeys = Array.from(new Set([...Object.keys(previousValue), ...Object.keys(newValue)])); + + type Entry = { op: 'noop' | 'add' | 'remove' | 'modify'; key: string; prev?: unknown; next?: unknown }; + const entries: Entry[] = allKeys.map((key) => { + const inPrev = Object.hasOwn(previousValue, key); + const inNext = Object.hasOwn(newValue, key); + if (!inPrev) return { op: 'add', key, next: newValue[key] }; + if (!inNext) return { op: 'remove', key, prev: previousValue[key] }; + if (JSON.stringify(previousValue[key]) === JSON.stringify(newValue[key])) { + return { op: 'noop', key, next: newValue[key] }; } - } + return { op: 'modify', key, prev: previousValue[key], next: newValue[key] }; + }); const CONTEXT = 2; - const included = new Set(); - for (let i = 0; i < coloredLines.length; i++) { - if (coloredLines[i].added || coloredLines[i].removed) { - for (let j = Math.max(0, i - CONTEXT); j <= Math.min(coloredLines.length - 1, i + CONTEXT); j++) { - included.add(j); + const includedIndices = new Set(); + for (let i = 0; i < entries.length; i++) { + if (entries[i].op !== 'noop') { + for (let j = Math.max(0, i - CONTEXT); j <= Math.min(entries.length - 1, i + CONTEXT); j++) { + includedIndices.add(j); } } } @@ -259,21 +254,35 @@ function formatObjectDiff(name: string, previousValue: object, newValue: object) const resultLines: string[] = [`${chalk.yellow('~')} "${name}": {`]; let lastIncluded = -1; - for (let i = 0; i < coloredLines.length; i++) { - if (!included.has(i)) continue; + for (let i = 0; i < entries.length; i++) { + if (!includedIndices.has(i)) continue; if (lastIncluded !== -1 && i > lastIncluded + 1) { resultLines.push(' ...'); } - const { text, added, removed } = coloredLines[i]; - const symbol = added ? chalk.green('+') : removed ? chalk.red('-') : ' '; - resultLines.push(` ${symbol} ${text}`); lastIncluded = i; + const { op, key, prev, next } = entries[i]; + + if (op === 'noop') { + resultLines.push(` "${key}": ${formatValue(next)},`); + } else if (op === 'add') { + resultLines.push(` ${chalk.green('+')} ${chalk.green(`"${key}": ${formatValue(next)},`)}`); + } else if (op === 'remove') { + resultLines.push(` ${chalk.red('-')} ${chalk.red(`"${key}": ${formatValue(prev)},`)}`); + } else { + resultLines.push(` ${chalk.yellow('~')} "${key}": ${formatValue(prev)} -> ${formatValue(next)},`); + } } resultLines.push(' },'); return resultLines.join('\n'); } +function formatValue(value: unknown): string { + if (typeof value === 'string') return `"${value}"`; + if (value === null || value === undefined) return String(value); + return JSON.stringify(value); +} + const OBJECT_SINGLE_SIDE_MAX_LINES = 20; function formatObjectSingleSide(name: string, value: object, operation: ParameterOperation): string { From fdd62a795253fb83466f755e78eea25cbc234a24 Mon Sep 17 00:00:00 2001 From: kevinwang Date: Fri, 5 Jun 2026 14:12:52 -0400 Subject: [PATCH 6/6] feat: Improved plan printing more --- package.json | 2 +- src/ui/plan-pretty-printer.ts | 14 +++++++---- test/utils/plan-pretty-printer.test.ts | 34 ++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 7142632..6b690e8 100644 --- a/package.json +++ b/package.json @@ -145,7 +145,7 @@ "deploy": "npm run pkg && npm run notarize && npm run upload", "prepublishOnly": "npm run build" }, - "version": "1.2.0-beta.1", + "version": "1.2.0-beta.2", "bugs": "https://github.com/codifycli/codify/issues", "keywords": [ "oclif", diff --git a/src/ui/plan-pretty-printer.ts b/src/ui/plan-pretty-printer.ts index d95521b..421b3c8 100644 --- a/src/ui/plan-pretty-printer.ts +++ b/src/ui/plan-pretty-printer.ts @@ -251,29 +251,33 @@ function formatObjectDiff(name: string, previousValue: Record, } } + // Layout: every line uses [3 spaces][sym][2 spaces (JSON indent)][content] + // sym col=3, content col=6 — matches the outer block's "~ content" (sym=0, content=5) + 1 level deeper const resultLines: string[] = [`${chalk.yellow('~')} "${name}": {`]; let lastIncluded = -1; for (let i = 0; i < entries.length; i++) { if (!includedIndices.has(i)) continue; if (lastIncluded !== -1 && i > lastIncluded + 1) { - resultLines.push(' ...'); + resultLines.push(' ...'); } lastIncluded = i; const { op, key, prev, next } = entries[i]; + // All inner lines: sym at col 4, content at col 7 (2-space JSON indent relative to { at col 5). + // Noop uses a space for sym so content stays at col 7. if (op === 'noop') { resultLines.push(` "${key}": ${formatValue(next)},`); } else if (op === 'add') { - resultLines.push(` ${chalk.green('+')} ${chalk.green(`"${key}": ${formatValue(next)},`)}`); + resultLines.push(` ${chalk.green('+')} ${chalk.green(`"${key}": ${formatValue(next)},`)}`); } else if (op === 'remove') { - resultLines.push(` ${chalk.red('-')} ${chalk.red(`"${key}": ${formatValue(prev)},`)}`); + resultLines.push(` ${chalk.red('-')} ${chalk.red(`"${key}": ${formatValue(prev)},`)}`); } else { - resultLines.push(` ${chalk.yellow('~')} "${key}": ${formatValue(prev)} -> ${formatValue(next)},`); + resultLines.push(` ${chalk.yellow('~')} ${chalk.yellow(`"${key}": ${formatValue(prev)} -> ${formatValue(next)},`)}`); } } - resultLines.push(' },'); + resultLines.push(' },'); return resultLines.join('\n'); } diff --git a/test/utils/plan-pretty-printer.test.ts b/test/utils/plan-pretty-printer.test.ts index 8b5225a..97dcd53 100644 --- a/test/utils/plan-pretty-printer.test.ts +++ b/test/utils/plan-pretty-printer.test.ts @@ -87,6 +87,40 @@ describe('Plan pretty printer', () => { console.log(prettyFormatResourcePlan(new ResourcePlan(plan))) }) + it('Can diff nested objects with adds, removes, and modifies', () => { + const plan: PlanResponseData = { + planId: '18d9dbbc-9dd1-4581-9a6a-db146d44c829', + resourceType: 'macos-settings', + operation: ResourceOperation.MODIFY, + isStateful: false, + parameters: [ + { + name: 'mouse', + previousValue: { naturalScrolling: true, speed: 1.5 }, + newValue: { naturalScrolling: false, speed: 1.5 }, + operation: ParameterOperation.MODIFY, + isSensitive: false, + }, + { + name: 'keyboard', + previousValue: { pressAndHold: false, fnKeysAsStandardKeys: true }, + newValue: { keyRepeat: 6, initialKeyRepeat: 68, pressAndHold: true, fnKeysAsStandardKeys: true }, + operation: ParameterOperation.MODIFY, + isSensitive: false, + }, + { + name: 'dock', + previousValue: { position: 'bottom', autohide: true, minimizeEffect: 'scale' }, + newValue: { position: 'bottom', autohide: false, showRecents: true, minimizeEffect: 'genie' }, + operation: ParameterOperation.MODIFY, + isSensitive: false, + }, + ] + } + + console.log(prettyFormatResourcePlan(new ResourcePlan(plan))) + }) + it('Can print modify and re-create plans', () => { const plan: PlanResponseData = { planId: 'id',