From 029b62dab268b1f4ed0363226af594bc00f0cd15 Mon Sep 17 00:00:00 2001 From: kevinwang Date: Fri, 3 May 2024 21:44:23 -0400 Subject: [PATCH 01/14] Initial POC with ink installed and working --- package.json | 5 ++++- src/commands/plan/{index.ts => index.tsx} | 5 +++++ tsconfig.json | 1 + 3 files changed, 10 insertions(+), 1 deletion(-) rename src/commands/plan/{index.ts => index.tsx} (91%) diff --git a/package.json b/package.json index 7841df60..f5986f09 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,9 @@ "codify-schemas": "1.0.32", "ajv": "^8.12.0", "ajv-formats": "^3.0.1", - "tsx": "^4.7.3" + "tsx": "^4.7.3", + "ink": "^4.4.1", + "react": "^18.3.1" }, "description": "Codify is a set up as code tool for developers", "devDependencies": { @@ -23,6 +25,7 @@ "@types/mock-fs": "^4.13.3", "@types/node": "^18", "@types/semver": "^7.5.4", + "@types/react": "^18.3.1", "eslint-config-prettier": "^9.0.0", "chai": "^4", "chai-as-promised": "^7.1.1", diff --git a/src/commands/plan/index.ts b/src/commands/plan/index.tsx similarity index 91% rename from src/commands/plan/index.ts rename to src/commands/plan/index.tsx index 8193635a..40b89a9b 100644 --- a/src/commands/plan/index.ts +++ b/src/commands/plan/index.tsx @@ -1,5 +1,8 @@ import { Args, Command, Flags } from '@oclif/core' +import { render, Text } from 'ink'; import * as path from 'node:path'; +import * as React from 'react'; + import { PlanOrchestrator } from '../../orchestrators/plan.js'; export default class Plan extends Command { @@ -25,6 +28,8 @@ export default class Plan extends Command { public async run(): Promise { const { args, flags } = await this.parse(Plan) + render(Test Text); + const name = flags.name ?? 'world' this.log(`hello ${name} from /Users/kevinwang/Projects/codify/codify-core/src/commands/plan.ts`) if (args.file && flags.force) { diff --git a/tsconfig.json b/tsconfig.json index cda29783..69915ca9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,6 +5,7 @@ "moduleResolution": "Node16", "esModuleInterop": true, "resolveJsonModule": true, + "jsx": "react", "outDir": "dist", "rootDir": "src", "strict": true, From 52a5b4542b5f39e7439500be87f3527ab965e381 Mon Sep 17 00:00:00 2001 From: kevinwang Date: Sat, 4 May 2024 13:02:01 -0400 Subject: [PATCH 02/14] Updated to node 18.20. Removed warning messages from codify pkg using https://github.com/nodejs/node/issues/10802 --- README.md | 2 +- bin/run.js | 3 +++ codify.json | 15 +++++++++++++++ package.json | 2 +- 4 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 codify.json diff --git a/README.md b/README.md index dc2841a1..30b23c18 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ $ npm install -g codify $ codify COMMAND running command... $ codify (--version) -codify/0.0.0 darwin-arm64 node-v18.15.0 +codify/0.0.0 darwin-arm64 node-v18.20.2 $ codify --help [COMMAND] USAGE $ codify COMMAND diff --git a/bin/run.js b/bin/run.js index b54398a5..8a5e6fae 100755 --- a/bin/run.js +++ b/bin/run.js @@ -1,5 +1,8 @@ #!/usr/bin/env node +// This removes any Node Experimental warnings from being printed to the CLI +process.removeAllListeners('warning') + import { flush, handle, run } from '@oclif/core' await run(process.argv.slice(2), import.meta.url) diff --git a/codify.json b/codify.json new file mode 100644 index 00000000..569f73f5 --- /dev/null +++ b/codify.json @@ -0,0 +1,15 @@ +[ + { + "type": "project", + "plugins": { + "default": "/Users/kevinwang/Projects/codify2/homebrew-plugin/src/index.ts" + } + }, + { + "type": "nvm", + "global": "18.20", + "nodeVersions": [ + "18.20.2" + ] + } +] diff --git a/package.json b/package.json index f5986f09..fb163dc3 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "build": "shx rm -rf dist && tsc -b", "lint": "eslint . --ext .ts", "postpack": "shx rm -f oclif.manifest.json", - "build:release:macos": "oclif pack macos -r .", + "pkg": "oclif pack macos -r .", "posttest": "npm run lint", "prepack": "npm run build && oclif manifest && oclif readme", "test": "mocha --forbid-only \"test/**/*.test.ts\"", From 013dd23eb867dd9d51c7cb46bb219bd6856f50d3 Mon Sep 17 00:00:00 2001 From: kevinwang Date: Mon, 6 May 2024 08:01:15 -0400 Subject: [PATCH 03/14] Added reporters and ink components --- codify.json | 7 ++ package.json | 4 +- src/commands/apply/index.ts | 1 + src/commands/plan/{index.tsx => index.ts} | 6 +- src/orchestrators/context.ts | 81 ++++++++++++++++ src/orchestrators/plan.ts | 25 ++++- src/plugins/plugin-process.ts | 9 +- src/ui/components/plan-component.tsx | 56 +++++++++++ src/ui/reporters/default-reporter.tsx | 109 ++++++++++++++++++++++ src/ui/reporters/plain-reporter.ts | 18 ++++ src/ui/reporters/reporter.ts | 3 + 11 files changed, 309 insertions(+), 10 deletions(-) rename src/commands/plan/{index.tsx => index.ts} (91%) create mode 100644 src/orchestrators/context.ts create mode 100644 src/ui/components/plan-component.tsx create mode 100644 src/ui/reporters/default-reporter.tsx create mode 100644 src/ui/reporters/plain-reporter.ts create mode 100644 src/ui/reporters/reporter.ts diff --git a/codify.json b/codify.json index 569f73f5..811bf36e 100644 --- a/codify.json +++ b/codify.json @@ -11,5 +11,12 @@ "nodeVersions": [ "18.20.2" ] + }, + { + "type": "homebrew", + "formulae": [ + "cirruslabs/cli/cirrus", + "cirruslabs/cli/tart" + ] } ] diff --git a/package.json b/package.json index fb163dc3..533b4384 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,9 @@ "ajv-formats": "^3.0.1", "tsx": "^4.7.3", "ink": "^4.4.1", - "react": "^18.3.1" + "@inkjs/ui": "^1.0.0", + "react": "^18.3.1", + "eventemitter2": "^6.4.9" }, "description": "Codify is a set up as code tool for developers", "devDependencies": { diff --git a/src/commands/apply/index.ts b/src/commands/apply/index.ts index 1714949c..b271b361 100644 --- a/src/commands/apply/index.ts +++ b/src/commands/apply/index.ts @@ -1,5 +1,6 @@ import { Args, Command, Flags } from '@oclif/core' import path from 'node:path'; + import { ApplyOrchestrator } from '../../orchestrators/apply.js'; export default class Apply extends Command { diff --git a/src/commands/plan/index.tsx b/src/commands/plan/index.ts similarity index 91% rename from src/commands/plan/index.tsx rename to src/commands/plan/index.ts index 40b89a9b..3eab8d85 100644 --- a/src/commands/plan/index.tsx +++ b/src/commands/plan/index.ts @@ -1,9 +1,8 @@ import { Args, Command, Flags } from '@oclif/core' -import { render, Text } from 'ink'; import * as path from 'node:path'; -import * as React from 'react'; import { PlanOrchestrator } from '../../orchestrators/plan.js'; +import { DefaultReporter } from '../../ui/reporters/default-reporter.js'; export default class Plan extends Command { static args = { @@ -27,8 +26,7 @@ export default class Plan extends Command { public async run(): Promise { const { args, flags } = await this.parse(Plan) - - render(Test Text); + const reporter = new DefaultReporter() const name = flags.name ?? 'world' this.log(`hello ${name} from /Users/kevinwang/Projects/codify/codify-core/src/commands/plan.ts`) diff --git a/src/orchestrators/context.ts b/src/orchestrators/context.ts new file mode 100644 index 00000000..b309093b --- /dev/null +++ b/src/orchestrators/context.ts @@ -0,0 +1,81 @@ +import { EventEmitter } from 'node:events'; + + +export enum Event { + STDOUT = 'stdout', + STDERR = 'stderr', + PLUGIN_STDOUT = 'plugin_stdout', + PLUGIN_STDERR = 'plugin_stderr', + DEBUG = 'debug', + OUTPUT = 'output', + PROCESS_START = 'process_start', + PROCESS_FINISH = 'process_finish', + SUB_PROCESS_START = 'sub_process_start', + SUB_PROCESS_FINISH = 'sub_process_finish', +} + +export const ctx = new class { + emitter: EventEmitter; + + constructor() { + this.emitter = new EventEmitter(); + this.attachOutputEmitters(); + } + + on(eventName: string | symbol, listener: (...args: any[]) => void): EventEmitter { + return this.emitter.on(eventName, listener); + } + + log(...args: unknown[]) { + this.emitter.emit(Event.STDOUT, ...args); + } + + pluginStdout(...args: unknown[]) { + this.emitter.emit(Event.PLUGIN_STDOUT, ...args); + } + + pluginStderr(...args: unknown[]) { + this.emitter.emit(Event.PLUGIN_STDERR, ...args); + } + + debug(...args: unknown[]) { + // Add filtering here to only allow debug events when in debug mode + this.emitter.emit(Event.DEBUG, ...args); + } + + + processStarted(name: string) { + this.emitter.emit(Event.PROCESS_START, name); + } + + processFinished(name: string) { + this.emitter.emit(Event.PROCESS_FINISH, name); + } + + subprocessStarted(name: string, processName: string) { + this.emitter.emit(Event.SUB_PROCESS_START, name, processName); + } + + subprocessFinished(name: string, processName: string) { + this.emitter.emit(Event.SUB_PROCESS_FINISH, name, processName); + } + + async subprocess(name: string, run: () => Promise): Promise { + this.emitter.emit(Event.SUB_PROCESS_START, name); + const result = await run(); + this.emitter.emit(Event.SUB_PROCESS_FINISH, name); + return result; + } + + attachOutputEmitters() { + this.emitter.prependListener(Event.STDOUT, (...args) => this.onOutputEvent(...args)); + this.emitter.prependListener(Event.STDERR, (...args) => this.onOutputEvent(...args)); + this.emitter.prependListener(Event.PLUGIN_STDOUT, (...args) => this.onOutputEvent(...args)); + this.emitter.prependListener(Event.PLUGIN_STDERR, (...args) => this.onOutputEvent(...args)); + this.emitter.prependListener(Event.DEBUG, (...args) => this.onOutputEvent(...args)); + } + + onOutputEvent(...args: unknown[]) { + this.emitter.emit(Event.OUTPUT, ...args); + } +} diff --git a/src/orchestrators/plan.ts b/src/orchestrators/plan.ts index d2aa2cef..6821332c 100644 --- a/src/orchestrators/plan.ts +++ b/src/orchestrators/plan.ts @@ -3,6 +3,7 @@ import { PlanResponseData } from 'codify-schemas'; import { Project } from '../entities/project.js'; import { Parser } from '../parser/index.js'; import { PluginCollection } from '../plugins/plugin-collection.js'; +import { ctx } from './context.js'; interface PlanOchestratorResponse { plan: PlanResponseData[], @@ -10,26 +11,48 @@ interface PlanOchestratorResponse { project: Project; } +export enum PlanStatus { + PLAN = 'plan', + PARSE = 'parse', + INITIALIZE_PLUGINS = 'initalize_plugins', + VALIDATE = 'validate', + GENERATE_PLAN = 'generate_plan', +} + export const PlanOrchestrator = { async run(path: string, destroyPlugins = true): Promise { + ctx.processStarted(PlanStatus.PLAN) + + ctx.subprocessStarted(PlanStatus.PARSE, PlanStatus.PLAN); const project = await Parser.parseProject(path); + ctx.subprocessFinished(PlanStatus.PARSE, PlanStatus.PLAN); + ctx.subprocessStarted(PlanStatus.INITIALIZE_PLUGINS, PlanStatus.PLAN) const pluginCollection = new PluginCollection(); const dependencyMap = await pluginCollection.initialize(project); + ctx.subprocessFinished(PlanStatus.INITIALIZE_PLUGINS, PlanStatus.PLAN) + + ctx.subprocessStarted(PlanStatus.VALIDATE, PlanStatus.PLAN) project.validateWithResourceMap(dependencyMap); project.resolveResourceDependencies(dependencyMap); const validationResults = await pluginCollection.validate(project); project.handlePluginResourceValidationResults(validationResults); project.calculateEvaluationOrder(); + ctx.subprocessFinished(PlanStatus.VALIDATE, PlanStatus.PLAN) + + ctx.subprocessStarted(PlanStatus.GENERATE_PLAN, PlanStatus.PLAN) const plan = await pluginCollection.getPlan(project); - console.log(JSON.stringify(plan, null, 2)); + ctx.subprocessFinished(PlanStatus.GENERATE_PLAN, PlanStatus.PLAN) + if (destroyPlugins) { await pluginCollection.destroy(); } + ctx.processFinished(PlanStatus.PLAN) + return { plan, pluginCollection, diff --git a/src/plugins/plugin-process.ts b/src/plugins/plugin-process.ts index c39ca364..e59f8c7b 100644 --- a/src/plugins/plugin-process.ts +++ b/src/plugins/plugin-process.ts @@ -1,6 +1,7 @@ import { IpcMessage, IpcMessageSchema } from 'codify-schemas'; import { ChildProcess, fork } from 'node:child_process'; +import { ctx } from '../orchestrators/context.js'; import { ajv } from '../utils/ajv.js'; import { PluginMessage } from './message.js'; @@ -23,14 +24,14 @@ export class PluginProcess { jsFileDir, [], { - execArgv: ['--import', 'tsx'], env: { ...process.env, FORCE_COLOR: '1' }, + execArgv: ['--import', 'tsx'], silent: true }, ); - _process.stdout!.on('data', (message) => console.log(message.toString())); - _process.stderr!.on('data', (message) => console.log(message.toString())); + _process.stdout!.on('data', (message) => ctx.pluginStdout(message.toString('utf8'))); + _process.stderr!.on('data', (message) => ctx.pluginStderr(message.toString('utf8'))); return new PluginProcess(_process); @@ -76,7 +77,7 @@ class SendMessageForResultHandler { } messageListener = (incomingMessage: unknown) => { - console.log(JSON.stringify(incomingMessage, null, 2)); + ctx.debug(JSON.stringify(incomingMessage, null, 2)); if (!this.validateIpcMessage(incomingMessage)) { return this.reject(new Error(`Bad message from plugin. ${JSON.stringify(incomingMessage, null, 2)}`)) diff --git a/src/ui/components/plan-component.tsx b/src/ui/components/plan-component.tsx new file mode 100644 index 00000000..0d2c81d6 --- /dev/null +++ b/src/ui/components/plan-component.tsx @@ -0,0 +1,56 @@ +import { Spinner, StatusMessage } from '@inkjs/ui'; +import { Box, Static, Text } from 'ink'; +import { EventEmitter } from 'node:events'; +import React, { useEffect, useState } from 'react'; + +import { ProcessState, ProcessStatus } from '../reporters/default-reporter.js'; + +export function PlanComponent({ eventTarget }: { eventTarget: EventEmitter }) { + const [staticOutput, setStaticOutput] = useState([] as Array); + const [processState, setProcessState] = useState({ + process: [], + } as ProcessState); + + useEffect(() => { + eventTarget.on('static_output', (newValue: any) => { + setStaticOutput([...newValue]); + }); + + eventTarget.on('process', (state: ProcessState) => { + setProcessState(structuredClone(state)); + }); + }, []); + + return + + { + (text, idx) => {text} + } + + { + processState.process?.map((item, i) => + + { + item.status === ProcessStatus.IN_PROGRESS + ? + : {item.name} + } + + { + item.subprocess?.map((subItem, i) => + subItem.status === ProcessStatus.IN_PROGRESS + ? + : {subItem.name} + ) ?? [] + } + + + ) ?? [] + } + { + staticOutput.flatMap((arr) => arr.split('\n')).slice(-5).map((item, i) => + {item} + ) + } + +} diff --git a/src/ui/reporters/default-reporter.tsx b/src/ui/reporters/default-reporter.tsx new file mode 100644 index 00000000..c69f6c80 --- /dev/null +++ b/src/ui/reporters/default-reporter.tsx @@ -0,0 +1,109 @@ +import { render } from 'ink'; +import { EventEmitter } from 'node:events'; +import React from 'react'; + +import { ctx, Event } from '../../orchestrators/context.js'; +import { PlanComponent } from '../components/plan-component.js'; +import { Reporter } from './reporter.js'; + +export enum ProcessStatus { + NOT_STARTED, + IN_PROGRESS, + FINISHED, +} + +export interface ProcessState { + process: Array<{ + name: string; + status: ProcessStatus; + subprocess: Array<{ + name: string; + status: ProcessStatus; + }> + }> +} + +export class DefaultReporter implements Reporter { + + private renderEmitter = new EventEmitter(); + private staticOutput = new Array() + private processState = { + process: [], + } as ProcessState + + constructor() { + ctx.on(Event.OUTPUT, (...args) => this.onOutputEvent(...args)); + ctx.on(Event.PROCESS_START, (name) => this.onProcessStartEvent(name)) + ctx.on(Event.PROCESS_FINISH, (name) => this.onProcessFinishEvent(name)) + ctx.on(Event.SUB_PROCESS_START, (name, processName) => this.onSubprocessStartEvent(name, processName)); + ctx.on(Event.SUB_PROCESS_FINISH, (name, processName) => this.onSubprocessFinishEvent(name, processName)) + + render() + + } + + async promptConfirmation(): Promise { + return true; + } + + private onOutputEvent(...args: unknown[]) { + this.staticOutput.push(...args) + this.renderEmitter.emit('static_output', this.staticOutput); + } + + private onProcessStartEvent(name: string): void { + this.processState.process.push({ + name, + status: ProcessStatus.IN_PROGRESS, + subprocess: [], + }) + + this.renderEmitter.emit('process', this.processState); + } + + private onProcessFinishEvent(name: string): void { + const process = this.processState.process + .find((process) => process.name === name); + if (!process) { + return; + } + + process.status = ProcessStatus.FINISHED; + + this.renderEmitter.emit('process', this.processState.process); + + } + + private onSubprocessStartEvent(name: string, processName: string): void { + const process = this.processState.process + .find((process) => process.name === processName); + + if (!process) return; + + process.subprocess.push({ + name, + status: ProcessStatus.IN_PROGRESS, + }) + + this.renderEmitter.emit('process', this.processState); + } + + private onSubprocessFinishEvent(name: string, processName: string): void { + const process = this.processState.process + .find((process) => process.name === processName); + if (!process) { + return; + } + + const subprocess = process.subprocess.find((subprocess) => subprocess.name === name) + if (!subprocess) { + return; + } + + subprocess.status = ProcessStatus.FINISHED; + + this.onOutputEvent(`${name} finished processing`) + this.renderEmitter.emit('process', this.processState); + } + +} diff --git a/src/ui/reporters/plain-reporter.ts b/src/ui/reporters/plain-reporter.ts new file mode 100644 index 00000000..c8d1e1ca --- /dev/null +++ b/src/ui/reporters/plain-reporter.ts @@ -0,0 +1,18 @@ +import { ctx, Event } from '../../orchestrators/context.js'; +import { Reporter } from './reporter.js'; + +export class PlainReporter implements Reporter { + + constructor() { + ctx.on(Event.OUTPUT, (...args) => console.log(...args)) + ctx.on(Event.PROCESS_START, (name) => console.log(name)) + ctx.on(Event.PROCESS_FINISH, (name) => console.log(name)) + ctx.on(Event.SUB_PROCESS_START, (name) => console.log(name)) + ctx.on(Event.SUB_PROCESS_FINISH, (name) => console.log(name)) + } + + async promptConfirmation(): Promise { + return true; + } + +} diff --git a/src/ui/reporters/reporter.ts b/src/ui/reporters/reporter.ts new file mode 100644 index 00000000..131bfd55 --- /dev/null +++ b/src/ui/reporters/reporter.ts @@ -0,0 +1,3 @@ +export interface Reporter { + promptConfirmation(): Promise +} From 00b079874e75db14f583d5200a9a8d7fe0154e66 Mon Sep 17 00:00:00 2001 From: kevinwang Date: Mon, 6 May 2024 21:09:08 -0400 Subject: [PATCH 04/14] Moved context to be it's own global object. Improved events --- package.json | 3 +-- src/{orchestrators => events}/context.ts | 7 +++++-- src/orchestrators/plan.ts | 8 ++++---- src/plugins/plugin-process.ts | 2 +- src/ui/components/plan-component.tsx | 5 ----- src/ui/reporters/default-reporter.tsx | 2 +- src/ui/reporters/plain-reporter.ts | 2 +- 7 files changed, 13 insertions(+), 16 deletions(-) rename src/{orchestrators => events}/context.ts (94%) diff --git a/package.json b/package.json index 533b4384..a7c72935 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,7 @@ "tsx": "^4.7.3", "ink": "^4.4.1", "@inkjs/ui": "^1.0.0", - "react": "^18.3.1", - "eventemitter2": "^6.4.9" + "react": "^18.3.1" }, "description": "Codify is a set up as code tool for developers", "devDependencies": { diff --git a/src/orchestrators/context.ts b/src/events/context.ts similarity index 94% rename from src/orchestrators/context.ts rename to src/events/context.ts index b309093b..0216616a 100644 --- a/src/orchestrators/context.ts +++ b/src/events/context.ts @@ -1,6 +1,5 @@ import { EventEmitter } from 'node:events'; - export enum Event { STDOUT = 'stdout', STDERR = 'stderr', @@ -39,7 +38,11 @@ export const ctx = new class { } debug(...args: unknown[]) { - // Add filtering here to only allow debug events when in debug mode + const debug = process.env.DEBUG; + if (!debug?.toLowerCase().includes('codify') && !debug?.includes('*')) { + return; + } + this.emitter.emit(Event.DEBUG, ...args); } diff --git a/src/orchestrators/plan.ts b/src/orchestrators/plan.ts index 6821332c..3b1d44f9 100644 --- a/src/orchestrators/plan.ts +++ b/src/orchestrators/plan.ts @@ -1,9 +1,9 @@ import { PlanResponseData } from 'codify-schemas'; import { Project } from '../entities/project.js'; +import { ctx } from '../events/context.js'; import { Parser } from '../parser/index.js'; import { PluginCollection } from '../plugins/plugin-collection.js'; -import { ctx } from './context.js'; interface PlanOchestratorResponse { plan: PlanResponseData[], @@ -12,11 +12,11 @@ interface PlanOchestratorResponse { } export enum PlanStatus { - PLAN = 'plan', - PARSE = 'parse', + GENERATE_PLAN = 'generate_plan', INITIALIZE_PLUGINS = 'initalize_plugins', + PARSE = 'parse', + PLAN = 'plan', VALIDATE = 'validate', - GENERATE_PLAN = 'generate_plan', } export const PlanOrchestrator = { diff --git a/src/plugins/plugin-process.ts b/src/plugins/plugin-process.ts index e59f8c7b..184907bb 100644 --- a/src/plugins/plugin-process.ts +++ b/src/plugins/plugin-process.ts @@ -1,7 +1,7 @@ import { IpcMessage, IpcMessageSchema } from 'codify-schemas'; import { ChildProcess, fork } from 'node:child_process'; -import { ctx } from '../orchestrators/context.js'; +import { ctx } from '../events/context.js'; import { ajv } from '../utils/ajv.js'; import { PluginMessage } from './message.js'; diff --git a/src/ui/components/plan-component.tsx b/src/ui/components/plan-component.tsx index 0d2c81d6..c5aac02a 100644 --- a/src/ui/components/plan-component.tsx +++ b/src/ui/components/plan-component.tsx @@ -47,10 +47,5 @@ export function PlanComponent({ eventTarget }: { eventTarget: EventEmitter }) { ) ?? [] } - { - staticOutput.flatMap((arr) => arr.split('\n')).slice(-5).map((item, i) => - {item} - ) - } } diff --git a/src/ui/reporters/default-reporter.tsx b/src/ui/reporters/default-reporter.tsx index c69f6c80..0b96de97 100644 --- a/src/ui/reporters/default-reporter.tsx +++ b/src/ui/reporters/default-reporter.tsx @@ -2,7 +2,7 @@ import { render } from 'ink'; import { EventEmitter } from 'node:events'; import React from 'react'; -import { ctx, Event } from '../../orchestrators/context.js'; +import { ctx, Event } from '../../events/context.js'; import { PlanComponent } from '../components/plan-component.js'; import { Reporter } from './reporter.js'; diff --git a/src/ui/reporters/plain-reporter.ts b/src/ui/reporters/plain-reporter.ts index c8d1e1ca..78853efd 100644 --- a/src/ui/reporters/plain-reporter.ts +++ b/src/ui/reporters/plain-reporter.ts @@ -1,4 +1,4 @@ -import { ctx, Event } from '../../orchestrators/context.js'; +import { ctx, Event } from '../../events/context.js'; import { Reporter } from './reporter.js'; export class PlainReporter implements Reporter { From 69cd91aace619f13045c96f66ffff5509ea60ae3 Mon Sep 17 00:00:00 2001 From: kevinwang Date: Mon, 6 May 2024 22:47:03 -0400 Subject: [PATCH 05/14] Added plan rendering --- codify.json | 9 +++ src/commands/plan/index.ts | 3 +- ...an-component.tsx => default-component.tsx} | 24 +++++++- src/ui/components/plan/operation-symbol.tsx | 53 +++++++++++++++++ src/ui/components/plan/plan.tsx | 50 ++++++++++++++++ src/ui/components/plan/resource-text.tsx | 57 +++++++++++++++++++ src/ui/reporters/default-reporter.tsx | 15 ++++- src/ui/reporters/plain-reporter.ts | 6 ++ src/ui/reporters/reporter.ts | 4 ++ 9 files changed, 214 insertions(+), 7 deletions(-) rename src/ui/components/{plan-component.tsx => default-component.tsx} (69%) create mode 100644 src/ui/components/plan/operation-symbol.tsx create mode 100644 src/ui/components/plan/plan.tsx create mode 100644 src/ui/components/plan/resource-text.tsx diff --git a/codify.json b/codify.json index 811bf36e..cdac3e12 100644 --- a/codify.json +++ b/codify.json @@ -18,5 +18,14 @@ "cirruslabs/cli/cirrus", "cirruslabs/cli/tart" ] + }, + { + "type": "git-lfs" + }, + { + "type": "pyenv", + "pythonVersions": [ + "3.9" + ] } ] diff --git a/src/commands/plan/index.ts b/src/commands/plan/index.ts index 3eab8d85..3a736a39 100644 --- a/src/commands/plan/index.ts +++ b/src/commands/plan/index.ts @@ -40,7 +40,8 @@ export default class Plan extends Command { const resolvedPath = path.resolve(flags.path ?? '.'); - await PlanOrchestrator.run(resolvedPath); + const { plan } = await PlanOrchestrator.run(resolvedPath); + reporter.displayPlan(plan); this.exit(0); } diff --git a/src/ui/components/plan-component.tsx b/src/ui/components/default-component.tsx similarity index 69% rename from src/ui/components/plan-component.tsx rename to src/ui/components/default-component.tsx index c5aac02a..5eefd398 100644 --- a/src/ui/components/plan-component.tsx +++ b/src/ui/components/default-component.tsx @@ -4,21 +4,32 @@ import { EventEmitter } from 'node:events'; import React, { useEffect, useState } from 'react'; import { ProcessState, ProcessStatus } from '../reporters/default-reporter.js'; +import { PlanResponseData } from 'codify-schemas'; +import { PlanComponent } from './plan/plan.js'; + +export function DefaultComponent(props: { + emitter: EventEmitter +}) { + const { emitter } = props; -export function PlanComponent({ eventTarget }: { eventTarget: EventEmitter }) { const [staticOutput, setStaticOutput] = useState([] as Array); const [processState, setProcessState] = useState({ process: [], } as ProcessState); + const [planState, setPlanState] = useState(null as PlanResponseData[] | null); useEffect(() => { - eventTarget.on('static_output', (newValue: any) => { + emitter.on('static_output', (newValue: any) => { setStaticOutput([...newValue]); }); - eventTarget.on('process', (state: ProcessState) => { + emitter.on('process', (state: ProcessState) => { setProcessState(structuredClone(state)); }); + + emitter.on('plan', (plan: PlanResponseData[]) => { + setPlanState(plan); + }); }, []); return @@ -47,5 +58,12 @@ export function PlanComponent({ eventTarget }: { eventTarget: EventEmitter }) { ) ?? [] } + { + planState + ? { + (plan, idx) => + } + : <> + } } diff --git a/src/ui/components/plan/operation-symbol.tsx b/src/ui/components/plan/operation-symbol.tsx new file mode 100644 index 00000000..fda88dc4 --- /dev/null +++ b/src/ui/components/plan/operation-symbol.tsx @@ -0,0 +1,53 @@ +import { ParameterOperation, ResourceOperation } from 'codify-schemas'; +import { Box, Text } from 'ink'; +import React from 'react'; + +export function ResourceOperationSymbol(props: { + resourceOperation: ResourceOperation +}) { + switch (props.resourceOperation) { + case ResourceOperation.NOOP: { + return + } + + case ResourceOperation.CREATE: { + return + + } + + case ResourceOperation.DESTROY: { + return - + } + + case ResourceOperation.RECREATE: { + return + -+ + + } + + case ResourceOperation.MODIFY: { + return ~ + } + } +} + +export function ParameterOperationSymbol(props: { + parameterOperation: ParameterOperation +}) { + switch (props.parameterOperation) { + case ParameterOperation.NOOP: { + return + } + + case ParameterOperation.ADD: { + return + + } + + case ParameterOperation.REMOVE: { + return - + } + + case ParameterOperation.MODIFY: { + return ~ + } + } +} diff --git a/src/ui/components/plan/plan.tsx b/src/ui/components/plan/plan.tsx new file mode 100644 index 00000000..114971eb --- /dev/null +++ b/src/ui/components/plan/plan.tsx @@ -0,0 +1,50 @@ +import { OrderedList } from '@inkjs/ui'; +import { PlanResponseData } from 'codify-schemas'; +import { Box, Text } from 'ink'; +import React from 'react'; + +import { ResourceText } from './resource-text.js'; + +export function PlanComponent(props: { + plan: PlanResponseData[] +}) { + // console.log(JSON.stringify(props.plan, null, 2)); + + return + + Codify Plan + + The following actions will be performed: + + + { + props.plan.map((p, idx) => + + + + + Parameters: + {JSON.stringify(p.parameters, null, 2)} + {/* { */} + {/* p.parameters.map((parameter, idx2) => */} + {/* */} + {/* /!* *!/ */} + {/* /!* {parameter.name} *!/ */} + {/* /!* *!/ */} + {/* /!* *!/ */} + {/* /!* {String(parameter.previousValue)} *!/ */} + {/* /!* {' -> '} *!/ */} + {/* /!* {String(parameter.newValue)} *!/ */} + {/* /!* *!/ */} + {/* {JSON.stringify(parameter, null, 2)} */} + {/* */} + {/* ) */} + {/* } */} + + + + ) + } + + +} diff --git a/src/ui/components/plan/resource-text.tsx b/src/ui/components/plan/resource-text.tsx new file mode 100644 index 00000000..626855e7 --- /dev/null +++ b/src/ui/components/plan/resource-text.tsx @@ -0,0 +1,57 @@ +import { PlanResponseData, ResourceOperation } from 'codify-schemas'; +import { Box, Text } from 'ink'; +import React from 'react'; + +import { ResourceOperationSymbol } from './operation-symbol.js'; + +export function ResourceText(props: { + plan: PlanResponseData +}) { + const { plan } = props; + const { operation, resourceName, resourceType } = plan; + + const fullyQualifiedName = resourceType + (resourceName ? `.${resourceName}` : ''); + let backgroundColor = ''; + let operationName = ''; + + switch (operation) { + case ResourceOperation.NOOP: { + backgroundColor = '#D3D3D3'; + operationName = 'not be modified' + break; + } + + case ResourceOperation.CREATE: { + backgroundColor = 'green' + operationName = 'be created' + break; + } + + case ResourceOperation.DESTROY: { + backgroundColor = 'red' + operationName = 'be destroyed' + break; + } + + case ResourceOperation.MODIFY: { + backgroundColor = 'yellow' + operationName = 'be modified' + break; + } + + case ResourceOperation.RECREATE: { + backgroundColor = 'yellow' + operationName = 'be re-created' + break; + } + } + + return + + {fullyQualifiedName} + resource will {operationName} + + + + +} diff --git a/src/ui/reporters/default-reporter.tsx b/src/ui/reporters/default-reporter.tsx index 0b96de97..58188ce4 100644 --- a/src/ui/reporters/default-reporter.tsx +++ b/src/ui/reporters/default-reporter.tsx @@ -1,9 +1,10 @@ +import { PlanResponseData } from 'codify-schemas'; import { render } from 'ink'; import { EventEmitter } from 'node:events'; import React from 'react'; import { ctx, Event } from '../../events/context.js'; -import { PlanComponent } from '../components/plan-component.js'; +import { DefaultComponent } from '../components/default-component.js'; import { Reporter } from './reporter.js'; export enum ProcessStatus { @@ -38,7 +39,7 @@ export class DefaultReporter implements Reporter { ctx.on(Event.SUB_PROCESS_START, (name, processName) => this.onSubprocessStartEvent(name, processName)); ctx.on(Event.SUB_PROCESS_FINISH, (name, processName) => this.onSubprocessFinishEvent(name, processName)) - render() + render() } @@ -46,6 +47,11 @@ export class DefaultReporter implements Reporter { return true; } + displayPlan(plan: PlanResponseData[]): void { + this.renderEmitter.emit('process', []); + this.renderEmitter.emit('plan', plan); + } + private onOutputEvent(...args: unknown[]) { this.staticOutput.push(...args) this.renderEmitter.emit('static_output', this.staticOutput); @@ -58,6 +64,7 @@ export class DefaultReporter implements Reporter { subprocess: [], }) + this.onOutputEvent(`${name} started`) this.renderEmitter.emit('process', this.processState); } @@ -70,6 +77,7 @@ export class DefaultReporter implements Reporter { process.status = ProcessStatus.FINISHED; + this.onOutputEvent(`${name} finished successfully`) this.renderEmitter.emit('process', this.processState.process); } @@ -85,6 +93,7 @@ export class DefaultReporter implements Reporter { status: ProcessStatus.IN_PROGRESS, }) + this.onOutputEvent(`${name} started`) this.renderEmitter.emit('process', this.processState); } @@ -102,7 +111,7 @@ export class DefaultReporter implements Reporter { subprocess.status = ProcessStatus.FINISHED; - this.onOutputEvent(`${name} finished processing`) + this.onOutputEvent(`${name} finished successfully`) this.renderEmitter.emit('process', this.processState); } diff --git a/src/ui/reporters/plain-reporter.ts b/src/ui/reporters/plain-reporter.ts index 78853efd..6a6d7f22 100644 --- a/src/ui/reporters/plain-reporter.ts +++ b/src/ui/reporters/plain-reporter.ts @@ -1,3 +1,5 @@ +import { PlanResponseData } from 'codify-schemas'; + import { ctx, Event } from '../../events/context.js'; import { Reporter } from './reporter.js'; @@ -15,4 +17,8 @@ export class PlainReporter implements Reporter { return true; } + displayPlan(plan: PlanResponseData[]): void { + console.log(JSON.stringify(plan)); + } + } diff --git a/src/ui/reporters/reporter.ts b/src/ui/reporters/reporter.ts index 131bfd55..9c0a493a 100644 --- a/src/ui/reporters/reporter.ts +++ b/src/ui/reporters/reporter.ts @@ -1,3 +1,7 @@ +import { PlanResponseData } from 'codify-schemas'; + export interface Reporter { promptConfirmation(): Promise + + displayPlan(plan: PlanResponseData[]): void } From 7e88f785102fefa24169905b17cfd7afa4e71327 Mon Sep 17 00:00:00 2001 From: kevinwang Date: Tue, 7 May 2024 19:15:41 -0400 Subject: [PATCH 06/14] Added prompts for apply command --- src/commands/apply/index.ts | 18 ++++++++++++--- src/orchestrators/apply.ts | 10 --------- src/ui/components/default-component.tsx | 30 ++++++++++++++++++++++++- src/ui/components/plan/plan.tsx | 26 ++++++++++----------- src/ui/reporters/default-reporter.tsx | 10 ++++++++- src/ui/reporters/plain-reporter.ts | 9 +++++++- 6 files changed, 74 insertions(+), 29 deletions(-) diff --git a/src/commands/apply/index.ts b/src/commands/apply/index.ts index b271b361..a90eceb6 100644 --- a/src/commands/apply/index.ts +++ b/src/commands/apply/index.ts @@ -1,7 +1,8 @@ import { Args, Command, Flags } from '@oclif/core' import path from 'node:path'; -import { ApplyOrchestrator } from '../../orchestrators/apply.js'; +import { DefaultReporter } from '../../ui/reporters/default-reporter.js'; +import { PlanOrchestrator } from '../../orchestrators/plan.js'; export default class Apply extends Command { static args = { @@ -25,6 +26,7 @@ export default class Apply extends Command { public async run(): Promise { const { args, flags } = await this.parse(Apply) + const reporter = new DefaultReporter() const name = flags.name ?? 'world' this.log(`hello ${name} from /Users/kevinwang/Projects/codify/codify-core/src/commands/apply.ts`) @@ -37,8 +39,18 @@ export default class Apply extends Command { } const resolvedPath = path.resolve(flags.path ?? '.'); - await ApplyOrchestrator.run(resolvedPath); - this.exit(0); + const { plan } = await PlanOrchestrator.run(resolvedPath); + reporter.displayPlan(plan); + + const confirm = await reporter.promptConfirmation() + + if (!confirm) { + return this.exit(0); + } + + setTimeout(() => console.log('Confirmed!'), 500) + + // this.exit(0); } } diff --git a/src/orchestrators/apply.ts b/src/orchestrators/apply.ts index 83ecaa3c..5d7a9868 100644 --- a/src/orchestrators/apply.ts +++ b/src/orchestrators/apply.ts @@ -1,10 +1,7 @@ import { ResourceOperation } from 'codify-schemas'; -import readline from 'node:readline'; import { PlanOrchestrator } from './plan.js'; -const rl = readline.createInterface(process.stdin, process.stdout); - export const ApplyOrchestrator = { async run(rootDirectory: string): Promise { const { plan, pluginCollection } = await PlanOrchestrator.run(rootDirectory, false); @@ -16,13 +13,6 @@ export const ApplyOrchestrator = { return; } - const response = await new Promise((resolve) => { - rl.question('Is this okay?\n', (answer) => resolve(answer)); - }); - if (response !== 'yes') { - return; - } - await pluginCollection.apply(plan); await pluginCollection.destroy(); }, diff --git a/src/ui/components/default-component.tsx b/src/ui/components/default-component.tsx index 5eefd398..2e324a6e 100644 --- a/src/ui/components/default-component.tsx +++ b/src/ui/components/default-component.tsx @@ -1,4 +1,4 @@ -import { Spinner, StatusMessage } from '@inkjs/ui'; +import { Select, Spinner, StatusMessage } from '@inkjs/ui'; import { Box, Static, Text } from 'ink'; import { EventEmitter } from 'node:events'; import React, { useEffect, useState } from 'react'; @@ -17,6 +17,8 @@ export function DefaultComponent(props: { process: [], } as ProcessState); const [planState, setPlanState] = useState(null as PlanResponseData[] | null); + const [showConfirm, setShowConfirm] = useState(false); + const [confirmValue, setConfirmValue] = useState(null as boolean | null) useEffect(() => { emitter.on('static_output', (newValue: any) => { @@ -30,6 +32,9 @@ export function DefaultComponent(props: { emitter.on('plan', (plan: PlanResponseData[]) => { setPlanState(plan); }); + emitter.on('promptConfirmation', () => { + setShowConfirm(true); + }) }, []); return @@ -65,5 +70,28 @@ export function DefaultComponent(props: { } : <> } + { + showConfirm && ( + confirmValue === null + ? + Do you want to apply the above changes? + + + ) + } } diff --git a/src/ui/components/plan/plan.tsx b/src/ui/components/plan/plan.tsx index 114971eb..525418ec 100644 --- a/src/ui/components/plan/plan.tsx +++ b/src/ui/components/plan/plan.tsx @@ -1,5 +1,5 @@ import { OrderedList } from '@inkjs/ui'; -import { PlanResponseData } from 'codify-schemas'; +import { PlanResponseData, ResourceOperation } from 'codify-schemas'; import { Box, Text } from 'ink'; import React from 'react'; @@ -8,6 +8,7 @@ import { ResourceText } from './resource-text.js'; export function PlanComponent(props: { plan: PlanResponseData[] }) { + const filteredPlan = props.plan.filter((p) => p.operation !== ResourceOperation.NOOP); // console.log(JSON.stringify(props.plan, null, 2)); return @@ -18,25 +19,24 @@ export function PlanComponent(props: { { - props.plan.map((p, idx) => + filteredPlan.map((p, idx) => Parameters: {JSON.stringify(p.parameters, null, 2)} - {/* { */} + {/* { */} {/* p.parameters.map((parameter, idx2) => */} - {/* */} - {/* /!* *!/ */} - {/* /!* {parameter.name} *!/ */} - {/* /!* *!/ */} - {/* /!* *!/ */} - {/* /!* {String(parameter.previousValue)} *!/ */} - {/* /!* {' -> '} *!/ */} - {/* /!* {String(parameter.newValue)} *!/ */} - {/* /!* *!/ */} - {/* {JSON.stringify(parameter, null, 2)} */} + {/* */} + {/* */} + {/* {parameter.name} */} + {/* */} + {/* {JSON.stringify(parameter.previousValue, null, 2)} */} + {/* {' -> '} */} + {/* {JSON.stringify(parameter.newValue, null, 2)} */} + {/* */} + {/* /!* {JSON.stringify(parameter, null, 2)} *!/ */} {/* */} {/* ) */} {/* } */} diff --git a/src/ui/reporters/default-reporter.tsx b/src/ui/reporters/default-reporter.tsx index 58188ce4..1d4241e2 100644 --- a/src/ui/reporters/default-reporter.tsx +++ b/src/ui/reporters/default-reporter.tsx @@ -44,7 +44,15 @@ export class DefaultReporter implements Reporter { } async promptConfirmation(): Promise { - return true; + const result = await Promise.all([ + new Promise((resolve) => { + this.renderEmitter.once('promptConfirmation_Result', (isConfirmed) => resolve(isConfirmed as boolean)); + }), + this.renderEmitter.emit('promptConfirmation'), + ]) + + + return result[0]; } displayPlan(plan: PlanResponseData[]): void { diff --git a/src/ui/reporters/plain-reporter.ts b/src/ui/reporters/plain-reporter.ts index 6a6d7f22..b29db630 100644 --- a/src/ui/reporters/plain-reporter.ts +++ b/src/ui/reporters/plain-reporter.ts @@ -1,9 +1,12 @@ import { PlanResponseData } from 'codify-schemas'; +import readline from 'node:readline'; import { ctx, Event } from '../../events/context.js'; import { Reporter } from './reporter.js'; export class PlainReporter implements Reporter { + private readonly rl = readline.createInterface(process.stdin, process.stdout); + constructor() { ctx.on(Event.OUTPUT, (...args) => console.log(...args)) @@ -14,7 +17,11 @@ export class PlainReporter implements Reporter { } async promptConfirmation(): Promise { - return true; + const response = await new Promise((resolve) => { + this.rl.question('Is this okay?\n', (answer) => resolve(answer)); + }); + + return response === 'yes'; } displayPlan(plan: PlanResponseData[]): void { From c23e3284b43ac3610c9f1170d388a7d7744c5819 Mon Sep 17 00:00:00 2001 From: kevinwang Date: Tue, 7 May 2024 21:25:45 -0400 Subject: [PATCH 07/14] Added proper progress display component. Added proper state machine events to organize component --- src/ui/components/default-component.tsx | 102 +++++++++--------- .../components/progress/progress-display.tsx | 58 ++++++++++ src/ui/reporters/default-reporter.tsx | 62 ++++------- src/ui/reporters/reporter.ts | 25 +++++ 4 files changed, 156 insertions(+), 91 deletions(-) create mode 100644 src/ui/components/progress/progress-display.tsx diff --git a/src/ui/components/default-component.tsx b/src/ui/components/default-component.tsx index 2e324a6e..7e4d5ccc 100644 --- a/src/ui/components/default-component.tsx +++ b/src/ui/components/default-component.tsx @@ -1,95 +1,93 @@ -import { Select, Spinner, StatusMessage } from '@inkjs/ui'; +import { Select } from '@inkjs/ui'; +import { PlanResponseData } from 'codify-schemas'; import { Box, Static, Text } from 'ink'; import { EventEmitter } from 'node:events'; import React, { useEffect, useState } from 'react'; -import { ProcessState, ProcessStatus } from '../reporters/default-reporter.js'; -import { PlanResponseData } from 'codify-schemas'; +import { RenderEvent, RenderState } from '../reporters/reporter.js'; import { PlanComponent } from './plan/plan.js'; +import { ProgressDisplay, ProgressState } from './progress/progress-display.js'; export function DefaultComponent(props: { emitter: EventEmitter }) { const { emitter } = props; - const [staticOutput, setStaticOutput] = useState([] as Array); - const [processState, setProcessState] = useState({ - process: [], - } as ProcessState); - const [planState, setPlanState] = useState(null as PlanResponseData[] | null); - const [showConfirm, setShowConfirm] = useState(false); + const [state, setState] = useState(RenderState.GENERATING_PLAN); + const [staticOutput, setStaticOutput] = useState([] as Array | string>); + const [progressState, setProgressState] = useState(null as ProgressState | null); + const [plan, setPlan] = useState(null as PlanResponseData[] | null); const [confirmValue, setConfirmValue] = useState(null as boolean | null) useEffect(() => { - emitter.on('static_output', (newValue: any) => { - setStaticOutput([...newValue]); - }); + emitter.on(RenderEvent.STATE_TRANSITION, (obj) => { + switch (obj.nextState) { + case RenderState.GENERATING_PLAN: { + setProgressState(obj.progressState); + setState(obj.nextState); + break; + } + + case RenderState.DISPLAY_PLAN: { + setPlan(obj.plan); + setState(obj.nextState); + break; + } + + case RenderState.ASK_CONFIRMATION: { + setState(obj.nextState); + break; + } + + case RenderState.APPLYING: { + break; + } + } + }) - emitter.on('process', (state: ProcessState) => { - setProcessState(structuredClone(state)); + emitter.once(RenderEvent.LOG, (newValue: string) => { + setStaticOutput([...newValue]); }); - emitter.on('plan', (plan: PlanResponseData[]) => { - setPlanState(plan); + emitter.on(RenderEvent.PROCESS_UPDATE, (state: ProgressState) => { + setProgressState(structuredClone(state)); }); - emitter.on('promptConfirmation', () => { - setShowConfirm(true); - }) }, []); return { - (text, idx) => {text} + (text, idx) => {text.toString()} } { - processState.process?.map((item, i) => - - { - item.status === ProcessStatus.IN_PROGRESS - ? - : {item.name} - } - - { - item.subprocess?.map((subItem, i) => - subItem.status === ProcessStatus.IN_PROGRESS - ? - : {subItem.name} - ) ?? [] - } - - - ) ?? [] + state >= RenderState.DISPLAY_PLAN && plan && { + (plan, idx) => + } } { - planState - ? { - (plan, idx) => - } - : <> + (state === RenderState.GENERATING_PLAN || state === RenderState.APPLYING) && progressState && + } { - showConfirm && ( + state === RenderState.ASK_CONFIRMATION && ( confirmValue === null ? Do you want to apply the above changes? - + ]}/> ) } diff --git a/src/ui/components/progress/progress-display.tsx b/src/ui/components/progress/progress-display.tsx new file mode 100644 index 00000000..64930548 --- /dev/null +++ b/src/ui/components/progress/progress-display.tsx @@ -0,0 +1,58 @@ +import { Spinner, StatusMessage } from '@inkjs/ui'; +import { Box } from 'ink'; +import React from 'react'; + +export enum ProgressStatus { + IN_PROGRESS, + FINISHED, +} + +export interface ProgressState { + label: string, + status: ProgressStatus; + subProgress: ProgressState[] | null, +} + +export function ProgressDisplay( + props: { + progress: ProgressState, + } +) { + const { label, status, subProgress } = props.progress; + + return + { + status === ProgressStatus.IN_PROGRESS + ? + : {label} + } + { + subProgress && + + + } + +} + +export function SubProgressDisplay( + props: { + subProgresses: ProgressState[], + } +) { + const { subProgresses } = props; + + return <>{ + subProgresses.map((s, idx) => + { + s.status === ProgressStatus.IN_PROGRESS + ? + : {s.label} + } + { + s.subProgress && + + + } + ) + } +} diff --git a/src/ui/reporters/default-reporter.tsx b/src/ui/reporters/default-reporter.tsx index 1d4241e2..e6d8db98 100644 --- a/src/ui/reporters/default-reporter.tsx +++ b/src/ui/reporters/default-reporter.tsx @@ -5,36 +5,16 @@ import React from 'react'; import { ctx, Event } from '../../events/context.js'; import { DefaultComponent } from '../components/default-component.js'; -import { Reporter } from './reporter.js'; - -export enum ProcessStatus { - NOT_STARTED, - IN_PROGRESS, - FINISHED, -} - -export interface ProcessState { - process: Array<{ - name: string; - status: ProcessStatus; - subprocess: Array<{ - name: string; - status: ProcessStatus; - }> - }> -} +import { DisplayPlanStateTransition, RenderEvent, RenderState, Reporter } from './reporter.js'; export class DefaultReporter implements Reporter { private renderEmitter = new EventEmitter(); private staticOutput = new Array() - private processState = { - process: [], - } as ProcessState constructor() { - ctx.on(Event.OUTPUT, (...args) => this.onOutputEvent(...args)); - ctx.on(Event.PROCESS_START, (name) => this.onProcessStartEvent(name)) + ctx.on(Event.OUTPUT, (...args) => this.renderLog(...args)); + ctx.on(Event.PROCESS_START, (name) => this.onProcessEvent(name)) ctx.on(Event.PROCESS_FINISH, (name) => this.onProcessFinishEvent(name)) ctx.on(Event.SUB_PROCESS_START, (name, processName) => this.onSubprocessStartEvent(name, processName)); ctx.on(Event.SUB_PROCESS_FINISH, (name, processName) => this.onSubprocessFinishEvent(name, processName)) @@ -46,9 +26,11 @@ export class DefaultReporter implements Reporter { async promptConfirmation(): Promise { const result = await Promise.all([ new Promise((resolve) => { - this.renderEmitter.once('promptConfirmation_Result', (isConfirmed) => resolve(isConfirmed as boolean)); + this.renderEmitter.once(RenderEvent.PROMPT_RESULT, (isConfirmed) => resolve(isConfirmed as boolean)); + }), + this.renderEmitter.emit(RenderEvent.STATE_TRANSITION, { + nextState: RenderState.ASK_CONFIRMATION, }), - this.renderEmitter.emit('promptConfirmation'), ]) @@ -56,24 +38,26 @@ export class DefaultReporter implements Reporter { } displayPlan(plan: PlanResponseData[]): void { - this.renderEmitter.emit('process', []); - this.renderEmitter.emit('plan', plan); + this.renderEmitter.emit(RenderEvent.STATE_TRANSITION, { + nextState: RenderState.DISPLAY_PLAN, + plan, + } as DisplayPlanStateTransition); } - private onOutputEvent(...args: unknown[]) { - this.staticOutput.push(...args) - this.renderEmitter.emit('static_output', this.staticOutput); + private renderLog(...args: unknown[]) { + this.staticOutput.push(...args); + this.renderEmitter.emit(RenderEvent.LOG, this.staticOutput); } - private onProcessStartEvent(name: string): void { + private onProcessEvent(name: string): void { this.processState.process.push({ name, status: ProcessStatus.IN_PROGRESS, subprocess: [], }) - this.onOutputEvent(`${name} started`) - this.renderEmitter.emit('process', this.processState); + this.renderLog(`${name} started`) + this.renderEmitter.emit(RenderEvent.PROCESS_UPDATE, this.processState); } private onProcessFinishEvent(name: string): void { @@ -85,8 +69,8 @@ export class DefaultReporter implements Reporter { process.status = ProcessStatus.FINISHED; - this.onOutputEvent(`${name} finished successfully`) - this.renderEmitter.emit('process', this.processState.process); + this.renderLog(`${name} finished successfully`) + this.renderEmitter.emit(RenderEvent.PROCESS_UPDATE, this.processState.process); } @@ -101,8 +85,8 @@ export class DefaultReporter implements Reporter { status: ProcessStatus.IN_PROGRESS, }) - this.onOutputEvent(`${name} started`) - this.renderEmitter.emit('process', this.processState); + this.renderLog(`${name} started`) + this.renderEmitter.emit(RenderEvent.PROCESS_UPDATE, this.processState); } private onSubprocessFinishEvent(name: string, processName: string): void { @@ -119,8 +103,8 @@ export class DefaultReporter implements Reporter { subprocess.status = ProcessStatus.FINISHED; - this.onOutputEvent(`${name} finished successfully`) - this.renderEmitter.emit('process', this.processState); + this.renderLog(`${name} finished successfully`) + this.renderEmitter.emit(RenderEvent.PROCESS_UPDATE, this.processState); } } diff --git a/src/ui/reporters/reporter.ts b/src/ui/reporters/reporter.ts index 9c0a493a..8c4fd91c 100644 --- a/src/ui/reporters/reporter.ts +++ b/src/ui/reporters/reporter.ts @@ -1,5 +1,30 @@ import { PlanResponseData } from 'codify-schemas'; +export enum RenderEvent { + STATE_TRANSITION = 'stateTransition', + LOG = 'log', + PROCESS_UPDATE = 'processUpdate', + PROMPT_RESULT = 'promptResult' +} + +/** + * Reporter to component (ink) communication is designed to be a state machine. + */ +export enum RenderState { + GENERATING_PLAN, + DISPLAY_PLAN, + ASK_CONFIRMATION, + APPLYING, +} + +export interface StateTransition { + nextState: RenderState; +} + +export interface DisplayPlanStateTransition extends StateTransition { + plan: PlanResponseData[]; +} + export interface Reporter { promptConfirmation(): Promise From 8bfe1941752d651bbed611c50ad028a5f237bc87 Mon Sep 17 00:00:00 2001 From: kevinwang Date: Tue, 7 May 2024 22:30:08 -0400 Subject: [PATCH 08/14] Re-worked how the process to progress display translation works. Fixed bugs. --- src/events/context.ts | 21 +++- src/orchestrators/plan.ts | 31 +++--- src/ui/components/default-component.tsx | 10 +- .../components/progress/progress-display.tsx | 40 ++++---- src/ui/reporters/default-reporter.tsx | 95 ++++++++++--------- src/ui/reporters/reporter.ts | 2 +- 6 files changed, 100 insertions(+), 99 deletions(-) diff --git a/src/events/context.ts b/src/events/context.ts index 0216616a..0a249d00 100644 --- a/src/events/context.ts +++ b/src/events/context.ts @@ -13,6 +13,19 @@ export enum Event { SUB_PROCESS_FINISH = 'sub_process_finish', } +export enum ProcessName { + PLAN = 'plan', + APPLY = 'apply' +} + +export enum SubProcessName { + PARSE = 'parse', + INITIALIZE_PLUGINS = 'initialize_plugins', + VALIDATE = 'validate', + GENERATE_PLAN = 'generate_plan', + APPLY_RESOURCE = 'apply_resource', +} + export const ctx = new class { emitter: EventEmitter; @@ -55,12 +68,12 @@ export const ctx = new class { this.emitter.emit(Event.PROCESS_FINISH, name); } - subprocessStarted(name: string, processName: string) { - this.emitter.emit(Event.SUB_PROCESS_START, name, processName); + subprocessStarted(name: string) { + this.emitter.emit(Event.SUB_PROCESS_START, name); } - subprocessFinished(name: string, processName: string) { - this.emitter.emit(Event.SUB_PROCESS_FINISH, name, processName); + subprocessFinished(name: string) { + this.emitter.emit(Event.SUB_PROCESS_FINISH, name); } async subprocess(name: string, run: () => Promise): Promise { diff --git a/src/orchestrators/plan.ts b/src/orchestrators/plan.ts index 3b1d44f9..85745765 100644 --- a/src/orchestrators/plan.ts +++ b/src/orchestrators/plan.ts @@ -1,7 +1,7 @@ import { PlanResponseData } from 'codify-schemas'; import { Project } from '../entities/project.js'; -import { ctx } from '../events/context.js'; +import { ctx, ProcessName, SubProcessName } from '../events/context.js'; import { Parser } from '../parser/index.js'; import { PluginCollection } from '../plugins/plugin-collection.js'; @@ -11,47 +11,38 @@ interface PlanOchestratorResponse { project: Project; } -export enum PlanStatus { - GENERATE_PLAN = 'generate_plan', - INITIALIZE_PLUGINS = 'initalize_plugins', - PARSE = 'parse', - PLAN = 'plan', - VALIDATE = 'validate', -} - export const PlanOrchestrator = { async run(path: string, destroyPlugins = true): Promise { - ctx.processStarted(PlanStatus.PLAN) + ctx.processStarted(ProcessName.PLAN) - ctx.subprocessStarted(PlanStatus.PARSE, PlanStatus.PLAN); + ctx.subprocessStarted(SubProcessName.PARSE); const project = await Parser.parseProject(path); - ctx.subprocessFinished(PlanStatus.PARSE, PlanStatus.PLAN); + ctx.subprocessFinished(SubProcessName.PARSE); - ctx.subprocessStarted(PlanStatus.INITIALIZE_PLUGINS, PlanStatus.PLAN) + ctx.subprocessStarted(SubProcessName.INITIALIZE_PLUGINS) const pluginCollection = new PluginCollection(); const dependencyMap = await pluginCollection.initialize(project); - ctx.subprocessFinished(PlanStatus.INITIALIZE_PLUGINS, PlanStatus.PLAN) + ctx.subprocessFinished(SubProcessName.INITIALIZE_PLUGINS) - ctx.subprocessStarted(PlanStatus.VALIDATE, PlanStatus.PLAN) + ctx.subprocessStarted(SubProcessName.VALIDATE) project.validateWithResourceMap(dependencyMap); project.resolveResourceDependencies(dependencyMap); const validationResults = await pluginCollection.validate(project); project.handlePluginResourceValidationResults(validationResults); project.calculateEvaluationOrder(); - ctx.subprocessFinished(PlanStatus.VALIDATE, PlanStatus.PLAN) + ctx.subprocessFinished(SubProcessName.VALIDATE) - ctx.subprocessStarted(PlanStatus.GENERATE_PLAN, PlanStatus.PLAN) + ctx.subprocessStarted(SubProcessName.GENERATE_PLAN) const plan = await pluginCollection.getPlan(project); - ctx.subprocessFinished(PlanStatus.GENERATE_PLAN, PlanStatus.PLAN) - + ctx.subprocessFinished(SubProcessName.GENERATE_PLAN) if (destroyPlugins) { await pluginCollection.destroy(); } - ctx.processFinished(PlanStatus.PLAN) + ctx.processFinished(ProcessName.PLAN) return { plan, diff --git a/src/ui/components/default-component.tsx b/src/ui/components/default-component.tsx index 7e4d5ccc..cfd48bf0 100644 --- a/src/ui/components/default-component.tsx +++ b/src/ui/components/default-component.tsx @@ -22,12 +22,6 @@ export function DefaultComponent(props: { useEffect(() => { emitter.on(RenderEvent.STATE_TRANSITION, (obj) => { switch (obj.nextState) { - case RenderState.GENERATING_PLAN: { - setProgressState(obj.progressState); - setState(obj.nextState); - break; - } - case RenderState.DISPLAY_PLAN: { setPlan(obj.plan); setState(obj.nextState); @@ -45,11 +39,11 @@ export function DefaultComponent(props: { } }) - emitter.once(RenderEvent.LOG, (newValue: string) => { + emitter.on(RenderEvent.LOG, (newValue: string) => { setStaticOutput([...newValue]); }); - emitter.on(RenderEvent.PROCESS_UPDATE, (state: ProgressState) => { + emitter.on(RenderEvent.PROGRESS_UPDATE, (state: ProgressState) => { setProgressState(structuredClone(state)); }); }, []); diff --git a/src/ui/components/progress/progress-display.tsx b/src/ui/components/progress/progress-display.tsx index 64930548..ee681505 100644 --- a/src/ui/components/progress/progress-display.tsx +++ b/src/ui/components/progress/progress-display.tsx @@ -8,9 +8,14 @@ export enum ProgressStatus { } export interface ProgressState { - label: string, + name: string, + label: string; status: ProgressStatus; - subProgress: ProgressState[] | null, + subProgresses: Array<{ + name: string, + label: string; + status: ProgressStatus; + }> | null; } export function ProgressDisplay( @@ -18,7 +23,7 @@ export function ProgressDisplay( progress: ProgressState, } ) { - const { label, status, subProgress } = props.progress; + const { label, status, subProgresses } = props.progress; return { @@ -26,33 +31,24 @@ export function ProgressDisplay( ? : {label} } - { - subProgress && - - - } + + + } export function SubProgressDisplay( props: { - subProgresses: ProgressState[], + subProgresses: ProgressState['subProgresses'], } ) { const { subProgresses } = props; return <>{ - subProgresses.map((s, idx) => - { - s.status === ProgressStatus.IN_PROGRESS - ? - : {s.label} - } - { - s.subProgress && - - - } - ) - } + subProgresses && subProgresses.map((s, idx) => + s.status === ProgressStatus.IN_PROGRESS + ? + : {s.label} + ) + } } diff --git a/src/ui/reporters/default-reporter.tsx b/src/ui/reporters/default-reporter.tsx index e6d8db98..7e44cf2f 100644 --- a/src/ui/reporters/default-reporter.tsx +++ b/src/ui/reporters/default-reporter.tsx @@ -3,24 +3,35 @@ import { render } from 'ink'; import { EventEmitter } from 'node:events'; import React from 'react'; -import { ctx, Event } from '../../events/context.js'; +import { ctx, Event, ProcessName, SubProcessName } from '../../events/context.js'; import { DefaultComponent } from '../components/default-component.js'; +import { ProgressState, ProgressStatus } from '../components/progress/progress-display.js'; import { DisplayPlanStateTransition, RenderEvent, RenderState, Reporter } from './reporter.js'; +const ProgressLabelMapping = { + [ProcessName.APPLY]: 'Applying plan...', + [ProcessName.PLAN]: 'Generating plan...', + [SubProcessName.APPLY_RESOURCE]: 'Applying resource', + [SubProcessName.GENERATE_PLAN]: 'Generating plan', + [SubProcessName.INITIALIZE_PLUGINS]: 'Initializing plugins', + [SubProcessName.PARSE]: 'Parsing configs', + [SubProcessName.VALIDATE]: 'Validating configs', +} + export class DefaultReporter implements Reporter { private renderEmitter = new EventEmitter(); - private staticOutput = new Array() + private staticOutput = new Array() + private progressState: ProgressState | null = null constructor() { ctx.on(Event.OUTPUT, (...args) => this.renderLog(...args)); - ctx.on(Event.PROCESS_START, (name) => this.onProcessEvent(name)) + ctx.on(Event.PROCESS_START, (name) => this.onProcessStartEvent(name)) ctx.on(Event.PROCESS_FINISH, (name) => this.onProcessFinishEvent(name)) - ctx.on(Event.SUB_PROCESS_START, (name, processName) => this.onSubprocessStartEvent(name, processName)); - ctx.on(Event.SUB_PROCESS_FINISH, (name, processName) => this.onSubprocessFinishEvent(name, processName)) + ctx.on(Event.SUB_PROCESS_START, (name) => this.onSubprocessStartEvent(name)); + ctx.on(Event.SUB_PROCESS_FINISH, (name) => this.onSubprocessFinishEvent(name)) render() - } async promptConfirmation(): Promise { @@ -44,67 +55,63 @@ export class DefaultReporter implements Reporter { } as DisplayPlanStateTransition); } - private renderLog(...args: unknown[]) { + private renderLog(...args: string[]) { this.staticOutput.push(...args); this.renderEmitter.emit(RenderEvent.LOG, this.staticOutput); } - private onProcessEvent(name: string): void { - this.processState.process.push({ + private onProcessStartEvent(name: ProcessName): void { + const label = ProgressLabelMapping[name]; + + this.progressState = { name, - status: ProcessStatus.IN_PROGRESS, - subprocess: [], - }) + label, + status: ProgressStatus.IN_PROGRESS, + subProgresses: [], + }; - this.renderLog(`${name} started`) - this.renderEmitter.emit(RenderEvent.PROCESS_UPDATE, this.processState); + this.renderLog(`${label} started`) + this.renderEmitter.emit(RenderEvent.PROGRESS_UPDATE, this.progressState); } - private onProcessFinishEvent(name: string): void { - const process = this.processState.process - .find((process) => process.name === name); - if (!process) { - return; - } + private onProcessFinishEvent(name: ProcessName): void { + const label = ProgressLabelMapping[name]; - process.status = ProcessStatus.FINISHED; + this.progressState!.status = ProgressStatus.FINISHED; - this.renderLog(`${name} finished successfully`) - this.renderEmitter.emit(RenderEvent.PROCESS_UPDATE, this.processState.process); + this.renderLog(`${label} finished successfully`) + this.renderEmitter.emit(RenderEvent.PROGRESS_UPDATE, this.progressState); } - private onSubprocessStartEvent(name: string, processName: string): void { - const process = this.processState.process - .find((process) => process.name === processName); - - if (!process) return; + private onSubprocessStartEvent(name: SubProcessName): void { + const label = ProgressLabelMapping[name]; - process.subprocess.push({ + this.progressState?.subProgresses?.push({ name, - status: ProcessStatus.IN_PROGRESS, - }) + label, + status: ProgressStatus.IN_PROGRESS, + }); - this.renderLog(`${name} started`) - this.renderEmitter.emit(RenderEvent.PROCESS_UPDATE, this.processState); + this.renderLog(`${label} started`) + this.renderEmitter.emit(RenderEvent.PROGRESS_UPDATE, this.progressState); } - private onSubprocessFinishEvent(name: string, processName: string): void { - const process = this.processState.process - .find((process) => process.name === processName); - if (!process) { - return; - } + private onSubprocessFinishEvent(name: SubProcessName): void { + const label = ProgressLabelMapping[name]; + + const subProgress = this.progressState + ?.subProgresses + ?.find((p) => p.name === name); - const subprocess = process.subprocess.find((subprocess) => subprocess.name === name) - if (!subprocess) { + if (!subProgress) { return; } - subprocess.status = ProcessStatus.FINISHED; + subProgress.status = ProgressStatus.FINISHED; - this.renderLog(`${name} finished successfully`) - this.renderEmitter.emit(RenderEvent.PROCESS_UPDATE, this.processState); + this.renderLog(`${label} finished successfully`) + this.renderEmitter.emit(RenderEvent.PROGRESS_UPDATE, this.progressState); } } diff --git a/src/ui/reporters/reporter.ts b/src/ui/reporters/reporter.ts index 8c4fd91c..2f60774e 100644 --- a/src/ui/reporters/reporter.ts +++ b/src/ui/reporters/reporter.ts @@ -3,7 +3,7 @@ import { PlanResponseData } from 'codify-schemas'; export enum RenderEvent { STATE_TRANSITION = 'stateTransition', LOG = 'log', - PROCESS_UPDATE = 'processUpdate', + PROGRESS_UPDATE = 'progressUpdate', PROMPT_RESULT = 'promptResult' } From 21c641bd2a073a811489b98c89ed3a4698ba9e5f Mon Sep 17 00:00:00 2001 From: kevinwang Date: Tue, 7 May 2024 23:20:28 -0400 Subject: [PATCH 09/14] Moved all logs to use console.log instead. Static are meant for single items tied to state. Changed useEffect to useLayoutEffect --- package.json | 4 +- src/commands/apply/index.ts | 5 +- src/ui/components/default-component.tsx | 66 +++++++++---------------- src/ui/reporters/default-reporter.tsx | 56 ++++++++++++--------- src/ui/reporters/plain-reporter.ts | 2 +- src/ui/reporters/reporter.ts | 4 +- 6 files changed, 62 insertions(+), 75 deletions(-) diff --git a/package.json b/package.json index a7c72935..727ca6a6 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "tsx": "^4.7.3", "ink": "^4.4.1", "@inkjs/ui": "^1.0.0", - "react": "^18.3.1" + "react": "^18.3.1", + "chalk": "^5.3.0" }, "description": "Codify is a set up as code tool for developers", "devDependencies": { @@ -27,6 +28,7 @@ "@types/node": "^18", "@types/semver": "^7.5.4", "@types/react": "^18.3.1", + "@types/chalk": "^2.2.0", "eslint-config-prettier": "^9.0.0", "chai": "^4", "chai-as-promised": "^7.1.1", diff --git a/src/commands/apply/index.ts b/src/commands/apply/index.ts index a90eceb6..13bdb4f8 100644 --- a/src/commands/apply/index.ts +++ b/src/commands/apply/index.ts @@ -43,14 +43,13 @@ export default class Apply extends Command { const { plan } = await PlanOrchestrator.run(resolvedPath); reporter.displayPlan(plan); - const confirm = await reporter.promptConfirmation() + const confirm = await reporter.promptApplyConfirmation() if (!confirm) { return this.exit(0); } - setTimeout(() => console.log('Confirmed!'), 500) - // this.exit(0); + } } diff --git a/src/ui/components/default-component.tsx b/src/ui/components/default-component.tsx index cfd48bf0..bf34c39c 100644 --- a/src/ui/components/default-component.tsx +++ b/src/ui/components/default-component.tsx @@ -1,8 +1,9 @@ import { Select } from '@inkjs/ui'; +import chalk from 'chalk'; import { PlanResponseData } from 'codify-schemas'; import { Box, Static, Text } from 'ink'; import { EventEmitter } from 'node:events'; -import React, { useEffect, useState } from 'react'; +import React, { useLayoutEffect, useState } from 'react'; import { RenderEvent, RenderState } from '../reporters/reporter.js'; import { PlanComponent } from './plan/plan.js'; @@ -14,33 +15,25 @@ export function DefaultComponent(props: { const { emitter } = props; const [state, setState] = useState(RenderState.GENERATING_PLAN); - const [staticOutput, setStaticOutput] = useState([] as Array | string>); const [progressState, setProgressState] = useState(null as ProgressState | null); const [plan, setPlan] = useState(null as PlanResponseData[] | null); - const [confirmValue, setConfirmValue] = useState(null as boolean | null) - useEffect(() => { + // Use layoutEffect runs before the first render, whereas useEffect runs after + useLayoutEffect(() => { emitter.on(RenderEvent.STATE_TRANSITION, (obj) => { switch (obj.nextState) { case RenderState.DISPLAY_PLAN: { + setProgressState(null); setPlan(obj.plan); - setState(obj.nextState); - break; - } - - case RenderState.ASK_CONFIRMATION: { - setState(obj.nextState); - break; - } - - case RenderState.APPLYING: { break; } } + + setState(obj.nextState); }) - emitter.on(RenderEvent.LOG, (newValue: string) => { - setStaticOutput([...newValue]); + emitter.on(RenderEvent.LOG, (log: string) => { + console.log(chalk.cyan(log)) }); emitter.on(RenderEvent.PROGRESS_UPDATE, (state: ProgressState) => { @@ -49,40 +42,25 @@ export function DefaultComponent(props: { }, []); return - - { - (text, idx) => {text.toString()} - } - + { + (state === RenderState.GENERATING_PLAN || state === RenderState.APPLYING) && progressState && ( + + ) + } { state >= RenderState.DISPLAY_PLAN && plan && { (plan, idx) => } } { - (state === RenderState.GENERATING_PLAN || state === RenderState.APPLYING) && progressState && - - } - { - state === RenderState.ASK_CONFIRMATION && ( - confirmValue === null - ? - Do you want to apply the above changes? - - + state === RenderState.PROMPT_APPLY_CONFIRMATION && ( + + Do you want to apply the above changes? +