Skip to content

Commit fc55291

Browse files
Merge pull request #15 from kevinwang5658/kevin/ink
Add ink to render UI
2 parents 7de344a + 04ac4ff commit fc55291

24 files changed

Lines changed: 712 additions & 75 deletions

README.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ $ npm install -g codify
1818
$ codify COMMAND
1919
running command...
2020
$ codify (--version)
21-
codify/0.0.0 darwin-arm64 node-v18.15.0
21+
codify/0.0.0 darwin-arm64 node-v18.20.2
2222
$ codify --help [COMMAND]
2323
USAGE
2424
$ codify COMMAND
@@ -48,14 +48,12 @@ describe the command here
4848

4949
```
5050
USAGE
51-
$ codify apply [FILE] [-f] [-n <value>] [-p <value>]
51+
$ codify apply [FILE] [-p <value>]
5252
5353
ARGUMENTS
5454
FILE file to read
5555
5656
FLAGS
57-
-f, --force
58-
-n, --name=<value> name to print
5957
-p, --path=<value> path to project
6058
6159
DESCRIPTION

bin/run.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
#!/usr/bin/env node
22

3+
// This removes any Node Experimental warnings from being printed to the CLI
4+
process.removeAllListeners('warning')
5+
36
import { flush, handle, run } from '@oclif/core'
47

58
await run(process.argv.slice(2), import.meta.url)

package.json

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@
1111
"codify-schemas": "1.0.32",
1212
"ajv": "^8.12.0",
1313
"ajv-formats": "^3.0.1",
14-
"tsx": "^4.7.3"
14+
"tsx": "^4.7.3",
15+
"ink": "^4.4.1",
16+
"@inkjs/ui": "^1.0.0",
17+
"react": "^18.3.1",
18+
"chalk": "^5.3.0",
19+
"parse-json": "^8.1.0"
1520
},
1621
"description": "Codify is a set up as code tool for developers",
1722
"devDependencies": {
@@ -23,6 +28,8 @@
2328
"@types/mock-fs": "^4.13.3",
2429
"@types/node": "^18",
2530
"@types/semver": "^7.5.4",
31+
"@types/react": "^18.3.1",
32+
"@types/chalk": "^2.2.0",
2633
"eslint-config-prettier": "^9.0.0",
2734
"chai": "^4",
2835
"chai-as-promised": "^7.1.1",
@@ -77,7 +84,7 @@
7784
"build": "shx rm -rf dist && tsc -b",
7885
"lint": "eslint . --ext .ts",
7986
"postpack": "shx rm -f oclif.manifest.json",
80-
"build:release:macos": "oclif pack macos -r .",
87+
"pkg": "oclif pack macos -r .",
8188
"posttest": "npm run lint",
8289
"prepack": "npm run build && oclif manifest && oclif readme",
8390
"test": "mocha --forbid-only \"test/**/*.test.ts\"",
File renamed without changes.
File renamed without changes.

src/commands/apply/index.ts

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { Args, Command, Flags } from '@oclif/core'
2+
import { ResourceOperation } from 'codify-schemas';
23
import path from 'node:path';
4+
35
import { ApplyOrchestrator } from '../../orchestrators/apply.js';
6+
import { PlanOrchestrator } from '../../orchestrators/plan.js';
7+
import { DefaultReporter } from '../../ui/reporters/default-reporter.js';
48

59
export default class Apply extends Command {
610
static args = {
@@ -14,30 +18,41 @@ export default class Apply extends Command {
1418
]
1519

1620
static flags = {
17-
// flag with no value (-f, --force)
18-
force: Flags.boolean({ char: 'f' }),
19-
// flag with a value (-n, --name=VALUE)
20-
name: Flags.string({ char: 'n', description: 'name to print' }),
2121
// flag with a value (-p, --path=VALUE)
2222
path: Flags.string({ char: 'p', description: 'path to project' }),
2323
}
2424

2525
public async run(): Promise<void> {
26-
const { args, flags } = await this.parse(Apply)
27-
28-
const name = flags.name ?? 'world'
29-
this.log(`hello ${name} from /Users/kevinwang/Projects/codify/codify-core/src/commands/apply.ts`)
30-
if (args.file && flags.force) {
31-
this.log(`you input --force and --file: ${args.file}`)
26+
const { flags } = await this.parse(Apply)
27+
const reporter = new DefaultReporter()
28+
29+
try {
30+
if (flags.path) {
31+
this.log(`Applying Codify from: ${flags.path}`);
32+
}
33+
34+
const resolvedPath = path.resolve(flags.path ?? '.');
35+
36+
const planResult = await PlanOrchestrator.run(resolvedPath, false);
37+
reporter.displayPlan(planResult.plan);
38+
39+
// Short circuit and exit if every change is NOOP
40+
if (planResult.plan.every((p) => p.operation === ResourceOperation.NOOP)) {
41+
console.log('No changes necessary. Exiting');
42+
await planResult.pluginCollection.destroy();
43+
return process.exit(0);
44+
}
45+
46+
const confirm = await reporter.promptApplyConfirmation()
47+
if (!confirm) {
48+
return process.exit(0);
49+
}
50+
51+
await ApplyOrchestrator.run(planResult);
52+
} catch (error: unknown) {
53+
console.error(error);
3254
}
3355

34-
if (flags.path) {
35-
this.log(`Applying Codify from: ${flags.path}`);
36-
}
37-
38-
const resolvedPath = path.resolve(flags.path ?? '.');
39-
await ApplyOrchestrator.run(resolvedPath);
40-
41-
this.exit(0);
56+
process.exit(0);
4257
}
4358
}

src/commands/plan/index.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { Args, Command, Flags } from '@oclif/core'
22
import * as path from 'node:path';
3+
34
import { PlanOrchestrator } from '../../orchestrators/plan.js';
5+
import { DefaultReporter } from '../../ui/reporters/default-reporter.js';
46

57
export default class Plan extends Command {
68
static args = {
@@ -23,22 +25,23 @@ export default class Plan extends Command {
2325
}
2426

2527
public async run(): Promise<void> {
26-
const { args, flags } = await this.parse(Plan)
28+
const { flags } = await this.parse(Plan)
29+
const reporter = new DefaultReporter()
2730

28-
const name = flags.name ?? 'world'
29-
this.log(`hello ${name} from /Users/kevinwang/Projects/codify/codify-core/src/commands/plan.ts`)
30-
if (args.file && flags.force) {
31-
this.log(`you input --force and --file: ${args.file}`)
32-
}
31+
try {
32+
if (flags.path) {
33+
this.log(`Applying Codify from: ${flags.path}`);
34+
}
3335

34-
if (flags.path) {
35-
this.log(`Applying Codify from: ${flags.path}`);
36-
}
36+
const resolvedPath = path.resolve(flags.path ?? '.');
3737

38-
const resolvedPath = path.resolve(flags.path ?? '.');
38+
const { plan } = await PlanOrchestrator.run(resolvedPath);
39+
reporter.displayPlan(plan);
3940

40-
await PlanOrchestrator.run(resolvedPath);
41+
} catch (error) {
42+
console.error(error);
43+
}
4144

42-
this.exit(0);
45+
process.exit(0);
4346
}
4447
}

src/events/context.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { EventEmitter } from 'node:events';
2+
3+
export enum Event {
4+
STDOUT = 'stdout',
5+
STDERR = 'stderr',
6+
PLUGIN_STDOUT = 'plugin_stdout',
7+
PLUGIN_STDERR = 'plugin_stderr',
8+
DEBUG = 'debug',
9+
OUTPUT = 'output',
10+
PROCESS_START = 'process_start',
11+
PROCESS_FINISH = 'process_finish',
12+
SUB_PROCESS_START = 'sub_process_start',
13+
SUB_PROCESS_FINISH = 'sub_process_finish',
14+
}
15+
16+
export enum ProcessName {
17+
PLAN = 'plan',
18+
APPLY = 'apply'
19+
}
20+
21+
export enum SubProcessName {
22+
PARSE = 'parse',
23+
INITIALIZE_PLUGINS = 'initialize_plugins',
24+
VALIDATE = 'validate',
25+
GENERATE_PLAN = 'generate_plan',
26+
APPLYING_RESOURCE = 'apply_resource',
27+
}
28+
29+
export const ctx = new class {
30+
emitter: EventEmitter;
31+
32+
constructor() {
33+
this.emitter = new EventEmitter();
34+
this.attachOutputEmitters();
35+
}
36+
37+
on(eventName: string | symbol, listener: (...args: any[]) => void): EventEmitter {
38+
return this.emitter.on(eventName, listener);
39+
}
40+
41+
log(...args: unknown[]) {
42+
this.emitter.emit(Event.STDOUT, ...args);
43+
}
44+
45+
pluginStdout(...args: unknown[]) {
46+
this.emitter.emit(Event.PLUGIN_STDOUT, ...args);
47+
}
48+
49+
pluginStderr(...args: unknown[]) {
50+
this.emitter.emit(Event.PLUGIN_STDERR, ...args);
51+
}
52+
53+
debug(...args: unknown[]) {
54+
const debug = process.env.DEBUG;
55+
if (!debug?.toLowerCase().includes('codify') && !debug?.includes('*')) {
56+
return;
57+
}
58+
59+
this.emitter.emit(Event.DEBUG, ...args);
60+
}
61+
62+
63+
processStarted(name: string) {
64+
this.emitter.emit(Event.PROCESS_START, name);
65+
}
66+
67+
processFinished(name: string) {
68+
this.emitter.emit(Event.PROCESS_FINISH, name);
69+
}
70+
71+
subprocessStarted(name: string, additionalName?: string) {
72+
this.emitter.emit(Event.SUB_PROCESS_START, name, additionalName);
73+
}
74+
75+
subprocessFinished(name: string, additionalName?: string) {
76+
this.emitter.emit(Event.SUB_PROCESS_FINISH, name, additionalName);
77+
}
78+
79+
async subprocess<T>(name: string, run: () => Promise<T>): Promise<T> {
80+
this.emitter.emit(Event.SUB_PROCESS_START, name);
81+
const result = await run();
82+
this.emitter.emit(Event.SUB_PROCESS_FINISH, name);
83+
return result;
84+
}
85+
86+
attachOutputEmitters() {
87+
this.emitter.prependListener(Event.STDOUT, (...args) => this.onOutputEvent(...args));
88+
this.emitter.prependListener(Event.STDERR, (...args) => this.onOutputEvent(...args));
89+
this.emitter.prependListener(Event.PLUGIN_STDOUT, (...args) => this.onOutputEvent(...args));
90+
this.emitter.prependListener(Event.PLUGIN_STDERR, (...args) => this.onOutputEvent(...args));
91+
this.emitter.prependListener(Event.DEBUG, (...args) => this.onOutputEvent(...args));
92+
}
93+
94+
onOutputEvent(...args: unknown[]) {
95+
this.emitter.emit(Event.OUTPUT, ...args);
96+
}
97+
}

src/orchestrators/apply.ts

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,17 @@
11
import { ResourceOperation } from 'codify-schemas';
2-
import readline from 'node:readline';
32

4-
import { PlanOrchestrator } from './plan.js';
5-
6-
const rl = readline.createInterface(process.stdin, process.stdout);
3+
import { ctx, ProcessName } from '../events/context.js';
4+
import { PlanOrchestratorResponse } from './plan.js';
75

86
export const ApplyOrchestrator = {
9-
async run(rootDirectory: string): Promise<void> {
10-
const { plan, pluginCollection } = await PlanOrchestrator.run(rootDirectory, false);
11-
12-
// Short circuit and exit if every change is NOOP
13-
if (plan.every((p) => p.operation === ResourceOperation.NOOP)) {
14-
console.log('No changes necessary. Exiting');
15-
await pluginCollection.destroy();
16-
return;
17-
}
18-
19-
const response = await new Promise((resolve) => {
20-
rl.question('Is this okay?\n', (answer) => resolve(answer));
21-
});
22-
if (response !== 'yes') {
23-
return;
24-
}
7+
async run(planResult: PlanOrchestratorResponse): Promise<void> {
8+
const { plan, pluginCollection } = planResult;
9+
const filteredPlan = plan
10+
.filter((p) => p.operation !== ResourceOperation.NOOP)
2511

26-
await pluginCollection.apply(plan);
12+
ctx.processStarted(ProcessName.APPLY);
13+
await pluginCollection.apply(filteredPlan);
2714
await pluginCollection.destroy();
15+
ctx.processFinished(ProcessName.APPLY);
2816
},
2917
};

src/orchestrators/plan.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,49 @@
11
import { PlanResponseData } from 'codify-schemas';
22

33
import { Project } from '../entities/project.js';
4+
import { ctx, ProcessName, SubProcessName } from '../events/context.js';
45
import { Parser } from '../parser/index.js';
56
import { PluginCollection } from '../plugins/plugin-collection.js';
67

7-
interface PlanOchestratorResponse {
8+
export interface PlanOrchestratorResponse {
89
plan: PlanResponseData[],
910
pluginCollection: PluginCollection;
1011
project: Project;
1112
}
1213

1314
export const PlanOrchestrator = {
14-
async run(path: string, destroyPlugins = true): Promise<PlanOchestratorResponse> {
15+
async run(path: string, destroyPlugins = true): Promise<PlanOrchestratorResponse> {
16+
ctx.processStarted(ProcessName.PLAN)
17+
18+
ctx.subprocessStarted(SubProcessName.PARSE);
1519
const project = await Parser.parseProject(path);
20+
ctx.subprocessFinished(SubProcessName.PARSE);
1621

22+
ctx.subprocessStarted(SubProcessName.INITIALIZE_PLUGINS)
1723
const pluginCollection = new PluginCollection();
1824
const dependencyMap = await pluginCollection.initialize(project);
25+
ctx.subprocessFinished(SubProcessName.INITIALIZE_PLUGINS)
26+
27+
ctx.subprocessStarted(SubProcessName.VALIDATE)
1928
project.validateWithResourceMap(dependencyMap);
2029
project.resolveResourceDependencies(dependencyMap);
2130

2231
const validationResults = await pluginCollection.validate(project);
2332
project.handlePluginResourceValidationResults(validationResults);
2433
project.calculateEvaluationOrder();
34+
ctx.subprocessFinished(SubProcessName.VALIDATE)
2535

36+
37+
ctx.subprocessStarted(SubProcessName.GENERATE_PLAN)
2638
const plan = await pluginCollection.getPlan(project);
27-
console.log(JSON.stringify(plan, null, 2));
39+
ctx.subprocessFinished(SubProcessName.GENERATE_PLAN)
2840

2941
if (destroyPlugins) {
3042
await pluginCollection.destroy();
3143
}
3244

45+
ctx.processFinished(ProcessName.PLAN)
46+
3347
return {
3448
plan,
3549
pluginCollection,

0 commit comments

Comments
 (0)