Skip to content

Commit 0fab861

Browse files
committed
Added reporters and ink components
1 parent a22f087 commit 0fab861

10 files changed

Lines changed: 302 additions & 10 deletions

File tree

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
"ajv-formats": "^3.0.1",
1414
"tsx": "^4.7.3",
1515
"ink": "^4.4.1",
16-
"react": "^18.3.1"
16+
"@inkjs/ui": "^1.0.0",
17+
"react": "^18.3.1",
18+
"eventemitter2": "^6.4.9"
1719
},
1820
"description": "Codify is a set up as code tool for developers",
1921
"devDependencies": {

src/commands/apply/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Args, Command, Flags } from '@oclif/core'
22
import path from 'node:path';
3+
34
import { ApplyOrchestrator } from '../../orchestrators/apply.js';
45

56
export default class Apply extends Command {
Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import { Args, Command, Flags } from '@oclif/core'
2-
import { render, Text } from 'ink';
32
import * as path from 'node:path';
4-
import * as React from 'react';
53

64
import { PlanOrchestrator } from '../../orchestrators/plan.js';
5+
import { DefaultReporter } from '../../ui/reporters/default-reporter.js';
76

87
export default class Plan extends Command {
98
static args = {
@@ -27,8 +26,7 @@ export default class Plan extends Command {
2726

2827
public async run(): Promise<void> {
2928
const { args, flags } = await this.parse(Plan)
30-
31-
render(<Text color="blue">Test Text</Text>);
29+
const reporter = new DefaultReporter()
3230

3331
const name = flags.name ?? 'world'
3432
this.log(`hello ${name} from /Users/kevinwang/Projects/codify/codify-core/src/commands/plan.ts`)

src/orchestrators/context.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { EventEmitter } from 'node:events';
2+
3+
4+
export enum Event {
5+
STDOUT = 'stdout',
6+
STDERR = 'stderr',
7+
PLUGIN_STDOUT = 'plugin_stdout',
8+
PLUGIN_STDERR = 'plugin_stderr',
9+
DEBUG = 'debug',
10+
OUTPUT = 'output',
11+
PROCESS_START = 'process_start',
12+
PROCESS_FINISH = 'process_finish',
13+
SUB_PROCESS_START = 'sub_process_start',
14+
SUB_PROCESS_FINISH = 'sub_process_finish',
15+
}
16+
17+
export const ctx = new class {
18+
emitter: EventEmitter;
19+
20+
constructor() {
21+
this.emitter = new EventEmitter();
22+
this.attachOutputEmitters();
23+
}
24+
25+
on(eventName: string | symbol, listener: (...args: any[]) => void): EventEmitter {
26+
return this.emitter.on(eventName, listener);
27+
}
28+
29+
log(...args: unknown[]) {
30+
this.emitter.emit(Event.STDOUT, ...args);
31+
}
32+
33+
pluginStdout(...args: unknown[]) {
34+
this.emitter.emit(Event.PLUGIN_STDOUT, ...args);
35+
}
36+
37+
pluginStderr(...args: unknown[]) {
38+
this.emitter.emit(Event.PLUGIN_STDERR, ...args);
39+
}
40+
41+
debug(...args: unknown[]) {
42+
// Add filtering here to only allow debug events when in debug mode
43+
this.emitter.emit(Event.DEBUG, ...args);
44+
}
45+
46+
47+
processStarted(name: string) {
48+
this.emitter.emit(Event.PROCESS_START, name);
49+
}
50+
51+
processFinished(name: string) {
52+
this.emitter.emit(Event.PROCESS_FINISH, name);
53+
}
54+
55+
subprocessStarted(name: string, processName: string) {
56+
this.emitter.emit(Event.SUB_PROCESS_START, name, processName);
57+
}
58+
59+
subprocessFinished(name: string, processName: string) {
60+
this.emitter.emit(Event.SUB_PROCESS_FINISH, name, processName);
61+
}
62+
63+
async subprocess<T>(name: string, run: () => Promise<T>): Promise<T> {
64+
this.emitter.emit(Event.SUB_PROCESS_START, name);
65+
const result = await run();
66+
this.emitter.emit(Event.SUB_PROCESS_FINISH, name);
67+
return result;
68+
}
69+
70+
attachOutputEmitters() {
71+
this.emitter.prependListener(Event.STDOUT, (...args) => this.onOutputEvent(...args));
72+
this.emitter.prependListener(Event.STDERR, (...args) => this.onOutputEvent(...args));
73+
this.emitter.prependListener(Event.PLUGIN_STDOUT, (...args) => this.onOutputEvent(...args));
74+
this.emitter.prependListener(Event.PLUGIN_STDERR, (...args) => this.onOutputEvent(...args));
75+
this.emitter.prependListener(Event.DEBUG, (...args) => this.onOutputEvent(...args));
76+
}
77+
78+
onOutputEvent(...args: unknown[]) {
79+
this.emitter.emit(Event.OUTPUT, ...args);
80+
}
81+
}

src/orchestrators/plan.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,56 @@ import { PlanResponseData } from 'codify-schemas';
33
import { Project } from '../entities/project.js';
44
import { Parser } from '../parser/index.js';
55
import { PluginCollection } from '../plugins/plugin-collection.js';
6+
import { ctx } from './context.js';
67

78
interface PlanOchestratorResponse {
89
plan: PlanResponseData[],
910
pluginCollection: PluginCollection;
1011
project: Project;
1112
}
1213

14+
export enum PlanStatus {
15+
PLAN = 'plan',
16+
PARSE = 'parse',
17+
INITIALIZE_PLUGINS = 'initalize_plugins',
18+
VALIDATE = 'validate',
19+
GENERATE_PLAN = 'generate_plan',
20+
}
21+
1322
export const PlanOrchestrator = {
1423
async run(path: string, destroyPlugins = true): Promise<PlanOchestratorResponse> {
24+
ctx.processStarted(PlanStatus.PLAN)
25+
26+
ctx.subprocessStarted(PlanStatus.PARSE, PlanStatus.PLAN);
1527
const project = await Parser.parseProject(path);
28+
ctx.subprocessFinished(PlanStatus.PARSE, PlanStatus.PLAN);
1629

30+
ctx.subprocessStarted(PlanStatus.INITIALIZE_PLUGINS, PlanStatus.PLAN)
1731
const pluginCollection = new PluginCollection();
1832
const dependencyMap = await pluginCollection.initialize(project);
33+
ctx.subprocessFinished(PlanStatus.INITIALIZE_PLUGINS, PlanStatus.PLAN)
34+
35+
ctx.subprocessStarted(PlanStatus.VALIDATE, PlanStatus.PLAN)
1936
project.validateWithResourceMap(dependencyMap);
2037
project.resolveResourceDependencies(dependencyMap);
2138

2239
const validationResults = await pluginCollection.validate(project);
2340
project.handlePluginResourceValidationResults(validationResults);
2441
project.calculateEvaluationOrder();
42+
ctx.subprocessFinished(PlanStatus.VALIDATE, PlanStatus.PLAN)
2543

44+
45+
ctx.subprocessStarted(PlanStatus.GENERATE_PLAN, PlanStatus.PLAN)
2646
const plan = await pluginCollection.getPlan(project);
27-
console.log(JSON.stringify(plan, null, 2));
47+
ctx.subprocessFinished(PlanStatus.GENERATE_PLAN, PlanStatus.PLAN)
48+
2849

2950
if (destroyPlugins) {
3051
await pluginCollection.destroy();
3152
}
3253

54+
ctx.processFinished(PlanStatus.PLAN)
55+
3356
return {
3457
plan,
3558
pluginCollection,

src/plugins/plugin-process.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { IpcMessage, IpcMessageSchema } from 'codify-schemas';
22
import { ChildProcess, fork } from 'node:child_process';
33

4+
import { ctx } from '../orchestrators/context.js';
45
import { ajv } from '../utils/ajv.js';
56
import { PluginMessage } from './message.js';
67

@@ -23,14 +24,14 @@ export class PluginProcess {
2324
jsFileDir,
2425
[],
2526
{
26-
execArgv: ['--import', 'tsx'],
2727
env: { ...process.env, FORCE_COLOR: '1' },
28+
execArgv: ['--import', 'tsx'],
2829
silent: true
2930
},
3031
);
3132

32-
_process.stdout!.on('data', (message) => console.log(message.toString()));
33-
_process.stderr!.on('data', (message) => console.log(message.toString()));
33+
_process.stdout!.on('data', (message) => ctx.pluginStdout(message.toString('utf8')));
34+
_process.stderr!.on('data', (message) => ctx.pluginStderr(message.toString('utf8')));
3435

3536

3637
return new PluginProcess(_process);
@@ -76,7 +77,7 @@ class SendMessageForResultHandler {
7677
}
7778

7879
messageListener = (incomingMessage: unknown) => {
79-
console.log(JSON.stringify(incomingMessage, null, 2));
80+
ctx.debug(JSON.stringify(incomingMessage, null, 2));
8081

8182
if (!this.validateIpcMessage(incomingMessage)) {
8283
return this.reject(new Error(`Bad message from plugin. ${JSON.stringify(incomingMessage, null, 2)}`))
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { Spinner, StatusMessage } from '@inkjs/ui';
2+
import { Box, Static, Text } from 'ink';
3+
import { EventEmitter } from 'node:events';
4+
import React, { useEffect, useState } from 'react';
5+
6+
import { ProcessState, ProcessStatus } from '../reporters/default-reporter.js';
7+
8+
export function PlanComponent({ eventTarget }: { eventTarget: EventEmitter }) {
9+
const [staticOutput, setStaticOutput] = useState([] as Array<string>);
10+
const [processState, setProcessState] = useState({
11+
process: [],
12+
} as ProcessState);
13+
14+
useEffect(() => {
15+
eventTarget.on('static_output', (newValue: any) => {
16+
setStaticOutput([...newValue]);
17+
});
18+
19+
eventTarget.on('process', (state: ProcessState) => {
20+
setProcessState(structuredClone(state));
21+
});
22+
}, []);
23+
24+
return <Box flexDirection="column">
25+
<Static items={staticOutput}>
26+
{
27+
(text, idx) => <Text color="cyan" key={idx}>{text}</Text>
28+
}
29+
</Static>
30+
{
31+
processState.process?.map((item, i) =>
32+
<Box flexDirection="column" key={i}>
33+
{
34+
item.status === ProcessStatus.IN_PROGRESS
35+
? <Spinner label={item.name}/>
36+
: <StatusMessage variant="success">{item.name}</StatusMessage>
37+
}
38+
<Box flexDirection="column" marginLeft={2}>
39+
{
40+
item.subprocess?.map((subItem, i) =>
41+
subItem.status === ProcessStatus.IN_PROGRESS
42+
? <Spinner key={i} label={subItem.name}/>
43+
: <StatusMessage key={i} variant="success">{subItem.name}</StatusMessage>
44+
) ?? []
45+
}
46+
</Box>
47+
</Box>
48+
) ?? []
49+
}
50+
{
51+
staticOutput.flatMap((arr) => arr.split('\n')).slice(-5).map((item, i) =>
52+
<Text key={i}>{item}</Text>
53+
)
54+
}
55+
</Box>
56+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { render } from 'ink';
2+
import { EventEmitter } from 'node:events';
3+
import React from 'react';
4+
5+
import { ctx, Event } from '../../orchestrators/context.js';
6+
import { PlanComponent } from '../components/plan-component.js';
7+
import { Reporter } from './reporter.js';
8+
9+
export enum ProcessStatus {
10+
NOT_STARTED,
11+
IN_PROGRESS,
12+
FINISHED,
13+
}
14+
15+
export interface ProcessState {
16+
process: Array<{
17+
name: string;
18+
status: ProcessStatus;
19+
subprocess: Array<{
20+
name: string;
21+
status: ProcessStatus;
22+
}>
23+
}>
24+
}
25+
26+
export class DefaultReporter implements Reporter {
27+
28+
private renderEmitter = new EventEmitter();
29+
private staticOutput = new Array<any>()
30+
private processState = {
31+
process: [],
32+
} as ProcessState
33+
34+
constructor() {
35+
ctx.on(Event.OUTPUT, (...args) => this.onOutputEvent(...args));
36+
ctx.on(Event.PROCESS_START, (name) => this.onProcessStartEvent(name))
37+
ctx.on(Event.PROCESS_FINISH, (name) => this.onProcessFinishEvent(name))
38+
ctx.on(Event.SUB_PROCESS_START, (name, processName) => this.onSubprocessStartEvent(name, processName));
39+
ctx.on(Event.SUB_PROCESS_FINISH, (name, processName) => this.onSubprocessFinishEvent(name, processName))
40+
41+
render(<PlanComponent eventTarget={this.renderEmitter}/>)
42+
43+
}
44+
45+
async promptConfirmation(): Promise<boolean> {
46+
return true;
47+
}
48+
49+
private onOutputEvent(...args: unknown[]) {
50+
this.staticOutput.push(...args)
51+
this.renderEmitter.emit('static_output', this.staticOutput);
52+
}
53+
54+
private onProcessStartEvent(name: string): void {
55+
this.processState.process.push({
56+
name,
57+
status: ProcessStatus.IN_PROGRESS,
58+
subprocess: [],
59+
})
60+
61+
this.renderEmitter.emit('process', this.processState);
62+
}
63+
64+
private onProcessFinishEvent(name: string): void {
65+
const process = this.processState.process
66+
.find((process) => process.name === name);
67+
if (!process) {
68+
return;
69+
}
70+
71+
process.status = ProcessStatus.FINISHED;
72+
73+
this.renderEmitter.emit('process', this.processState.process);
74+
75+
}
76+
77+
private onSubprocessStartEvent(name: string, processName: string): void {
78+
const process = this.processState.process
79+
.find((process) => process.name === processName);
80+
81+
if (!process) return;
82+
83+
process.subprocess.push({
84+
name,
85+
status: ProcessStatus.IN_PROGRESS,
86+
})
87+
88+
this.renderEmitter.emit('process', this.processState);
89+
}
90+
91+
private onSubprocessFinishEvent(name: string, processName: string): void {
92+
const process = this.processState.process
93+
.find((process) => process.name === processName);
94+
if (!process) {
95+
return;
96+
}
97+
98+
const subprocess = process.subprocess.find((subprocess) => subprocess.name === name)
99+
if (!subprocess) {
100+
return;
101+
}
102+
103+
subprocess.status = ProcessStatus.FINISHED;
104+
105+
this.onOutputEvent(`${name} finished processing`)
106+
this.renderEmitter.emit('process', this.processState);
107+
}
108+
109+
}

src/ui/reporters/plain-reporter.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { ctx, Event } from '../../orchestrators/context.js';
2+
import { Reporter } from './reporter.js';
3+
4+
export class PlainReporter implements Reporter {
5+
6+
constructor() {
7+
ctx.on(Event.OUTPUT, (...args) => console.log(...args))
8+
ctx.on(Event.PROCESS_START, (name) => console.log(name))
9+
ctx.on(Event.PROCESS_FINISH, (name) => console.log(name))
10+
ctx.on(Event.SUB_PROCESS_START, (name) => console.log(name))
11+
ctx.on(Event.SUB_PROCESS_FINISH, (name) => console.log(name))
12+
}
13+
14+
async promptConfirmation(): Promise<boolean> {
15+
return true;
16+
}
17+
18+
}

0 commit comments

Comments
 (0)