Skip to content

Commit 78d5dd6

Browse files
committed
feat: Added connect integration with codify-dashboard
1 parent 47baf27 commit 78d5dd6

File tree

8 files changed

+623
-10
lines changed

8 files changed

+623
-10
lines changed

package-lock.json

Lines changed: 305 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@
3535
"parse-json": "^8.1.0",
3636
"react": "^18.3.1",
3737
"semver": "^7.5.4",
38-
"supports-color": "^9.4.0"
38+
"socket.io": "^4.8.1",
39+
"supports-color": "^9.4.0",
40+
"ws": "^8.18.3"
3941
},
4042
"description": "Codify allows users to configure settings, install new packages, and automate their systems using code instead of the GUI. Get set up on a new laptop in one click, maintain a Codify file within your project so anyone can get started and never lose your cool apps or favourite settings again.",
4143
"devDependencies": {
@@ -52,6 +54,8 @@
5254
"@types/react": "^18.3.1",
5355
"@types/semver": "^7.5.4",
5456
"@types/strip-ansi": "^5.2.1",
57+
"@types/uuid": "^10.0.0",
58+
"@types/ws": "^8.18.1",
5559
"@typescript-eslint/eslint-plugin": "^8.16.0",
5660
"codify-plugin-lib": "^1.0.151",
5761
"esbuild": "^0.24.0",
@@ -69,7 +73,8 @@
6973
"strip-ansi": "^7.1.0",
7074
"tsx": "^4.7.3",
7175
"typescript": "5.3.3",
72-
"vitest": "^2.1.6"
76+
"vitest": "^2.1.6",
77+
"uuid": "^10.0.0"
7378
},
7479
"overrides": {
7580
"ink-form": {

src/commands/connect.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { BaseCommand } from '../common/base-command.js';
2+
import { LoginOrchestrator } from '../orchestrators/login.js';
3+
import { ConnectOrchestrator } from '../orchestrators/connect.js';
4+
5+
export default class Connect extends BaseCommand {
6+
static description =
7+
`Validate a codify.jsonc/codify.json/codify.yaml file.
8+
9+
For more information, visit: https://docs.codifycli.com/commands/validate
10+
`
11+
12+
static flags = {}
13+
14+
static examples = [
15+
'<%= config.bin %> <%= command.id %>',
16+
'<%= config.bin %> <%= command.id %> --path=../../import.codify.jsonc',
17+
]
18+
19+
public async run(): Promise<void> {
20+
const { flags } = await this.parse(Connect)
21+
22+
await ConnectOrchestrator.run();
23+
}
24+
}

src/connect/apply.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { spawn } from '@homebridge/node-pty-prebuilt-multiarch';
2+
import * as fs from 'node:fs/promises';
3+
import * as os from 'node:os';
4+
import * as path from 'node:path';
5+
import { v4 as uuid } from 'uuid';
6+
import { WebSocket } from 'ws';
7+
8+
import { WsServerManager } from './server.js';
9+
10+
export function connectApplyInitHandler(msg: any, initWs: WebSocket, manager: WsServerManager) {
11+
const sessionId = uuid();
12+
13+
manager.startAdhocWsServer(sessionId, async (ws) => {
14+
console.log('connected apply ws');
15+
16+
const { config } = msg;
17+
console.log('apply ws open', config);
18+
19+
const tmpDir = await fs.mkdtemp(os.tmpdir());
20+
const filePath = path.join(tmpDir, 'codify.json');
21+
22+
await fs.writeFile(filePath, JSON.stringify(config));
23+
24+
const pty = spawn('zsh', ['-c', 'codify apply'], {
25+
name: 'xterm-color',
26+
cols: 80,
27+
rows: 30,
28+
cwd: process.env.HOME,
29+
env: process.env
30+
});
31+
32+
pty.onData((data) => {
33+
ws.send(Buffer.from(data, 'utf8'));
34+
});
35+
36+
ws.on('message', (message) => {
37+
pty.write(message.toString('utf8'));
38+
})
39+
40+
pty.onExit(({ exitCode, signal }) => {
41+
console.log('pty exit', exitCode, signal);
42+
// ws.close(exitCode);
43+
ws.terminate();
44+
})
45+
});
46+
47+
initWs.send(JSON.stringify({
48+
cmd: 'apply_init_response',
49+
sessionId,
50+
}))
51+
}

src/connect/route-handler.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { WebSocket } from 'ws';
2+
3+
import { connectApplyInitHandler } from './apply.js';
4+
import { WsServerManager } from './server.js';
5+
6+
export async function defaultWsHandler(ws: WebSocket, manager: WsServerManager) {
7+
ws.on('message', (message) => {
8+
let msg;
9+
try {
10+
msg = JSON.parse(message.toString('utf8'));
11+
console.log(msg);
12+
} catch (error) {
13+
console.error(error);
14+
return;
15+
}
16+
17+
const { cmd } = msg;
18+
if (!cmd) {
19+
console.error('No cmd found');
20+
return;
21+
}
22+
23+
switch (cmd) {
24+
case 'apply_init': {
25+
connectApplyInitHandler(msg, ws, manager);
26+
break;
27+
}
28+
}
29+
30+
})
31+
}
32+
33+

src/connect/server.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { IncomingMessage, Server, createServer } from 'node:http';
2+
import { v4 as uuid } from 'uuid';
3+
import { WebSocket, WebSocketServer } from 'ws';
4+
5+
const DEFAULT_PORT = 51_040;
6+
7+
export class WsServerManager {
8+
9+
server: Server;
10+
port?: number;
11+
12+
private wsServerMap = new Map<string, WebSocketServer>();
13+
private handlerMap = new Map<string, (ws: WebSocket, manager: WsServerManager, request: IncomingMessage) => void>();
14+
15+
private connectionSecret;
16+
17+
constructor(connectionSecret?: string) {
18+
this.server = createServer();
19+
this.connectionSecret = connectionSecret;
20+
this.wsServerMap.set('default', this.createWssServer());
21+
22+
this.initServer();
23+
}
24+
25+
listen(cb?: () => void, port?: number, ) {
26+
this.port = port ?? DEFAULT_PORT
27+
this.server.listen(this.port, 'localhost', cb);
28+
}
29+
30+
setDefaultHandler(handler: (ws: WebSocket, manager: WsServerManager) => void): WsServerManager {
31+
const wss = this.createWssServer();
32+
this.wsServerMap.set('default', wss);
33+
this.handlerMap.set('default', handler);
34+
35+
return this;
36+
}
37+
38+
addAdditionalHandlers(path: string, handler: (ws: WebSocket) => void): WsServerManager {
39+
this.handlerMap.set(path, () => {
40+
const wss = this.addWebsocketServer();
41+
42+
});
43+
44+
return this;
45+
}
46+
47+
startAdhocWsServer(sessionId: string, handler: (ws: WebSocket, manager: WsServerManager) => void) {
48+
this.wsServerMap.set(sessionId, this.createWssServer());
49+
this.handlerMap.set(sessionId, handler);
50+
}
51+
52+
private addWebsocketServer(): string {
53+
const key = uuid();
54+
55+
const wss = new WebSocketServer({
56+
noServer: true
57+
})
58+
this.wsServerMap.set(key, wss);
59+
60+
wss.on('close', () => {
61+
this.wsServerMap.delete(key);
62+
})
63+
64+
return key;
65+
}
66+
67+
private initServer() {
68+
this.server.on('upgrade', (request, socket, head) => {
69+
console.log('upgrade')
70+
71+
const { pathname } = new URL(request.url!, 'ws://localhost:51040')
72+
console.log('Pathname:', pathname)
73+
74+
const code = request.headers['sec-websocket-protocol']
75+
if (this.connectionSecret && code !== this.connectionSecret) {
76+
console.log('Auth failed');
77+
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n')
78+
socket.destroy()
79+
return;
80+
}
81+
82+
if (pathname === '/' && this.handlerMap.has('default')) {
83+
const wss = this.wsServerMap.get('default');
84+
wss?.handleUpgrade(request, socket, head, (ws, request) => this.handlerMap.get('default')!(ws, this, request));
85+
return;
86+
}
87+
88+
const pathSections = pathname.split('/').filter(Boolean);
89+
console.log(pathSections);
90+
console.log('available sessions', this.handlerMap)
91+
92+
if (pathSections[0] === 'session'
93+
&& pathSections[1]
94+
&& this.handlerMap.has(pathSections[1])
95+
) {
96+
const sessionId = pathSections[1];
97+
console.log('session found, upgrading', sessionId);
98+
99+
const wss = this.wsServerMap.get(sessionId)!;
100+
101+
wss.handleUpgrade(request, socket, head, (ws, request) => this.handlerMap.get(sessionId)!(ws, this, request));
102+
return;
103+
}
104+
})
105+
}
106+
107+
private createWssServer(): WebSocketServer {
108+
return new WebSocketServer({
109+
noServer: true,
110+
})
111+
}
112+
}

src/orchestrators/connect.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { spawn } from '@homebridge/node-pty-prebuilt-multiarch';
2+
import { randomBytes } from 'node:crypto';
3+
import open from 'open';
4+
import { WebSocket } from 'ws';
5+
6+
import { defaultWsHandler } from '../connect/route-handler.js';
7+
import { WsServerManager } from '../connect/server.js';
8+
9+
export class ConnectOrchestrator {
10+
static async run() {
11+
const connectionSecret = ConnectOrchestrator.tokenGenerate()
12+
console.log(connectionSecret)
13+
14+
const server = new WsServerManager(connectionSecret)
15+
.setDefaultHandler(defaultWsHandler)
16+
.addAdditionalHandlers('/apply-logs', () => {})
17+
.addAdditionalHandlers('/import-logs', () => {})
18+
.addAdditionalHandlers('/terminal', () => {})
19+
20+
server.listen(() => {
21+
open(`http://localhost:3000/connection/success?code=${connectionSecret}`)
22+
});
23+
}
24+
25+
private static onConnection(ws: WebSocket) {
26+
console.log('[WS] Connection opened');
27+
28+
ws.on('apply', (message) => {
29+
let data;
30+
try {
31+
data = JSON.parse(message.toString('utf8'));
32+
console.log(data);
33+
} catch (error) {
34+
console.error(error);
35+
}
36+
37+
});
38+
39+
ws.on('close', () => {
40+
41+
});
42+
}
43+
44+
/*
45+
private static async onApply(ws: WebSocket, data: any) {
46+
const { config } = data;
47+
const tmpDir = await fs.mkdtemp(os.tmpdir());
48+
const filePath = path.join(tmpDir, 'codify.json');
49+
50+
await fs.writeFile(filePath, JSON.stringify(config));
51+
52+
const server = createServer()
53+
const wss = new WebSocketServer({
54+
55+
})
56+
57+
server.on('upgrade', (request, socket, head) => {
58+
wss.handleUpgrade(request, socket, head, (ws2) => {
59+
const pty = spawn('zsh', ['-c', '"codify apply"'], {
60+
name: 'xterm-color',
61+
cols: 80,
62+
rows: 30,
63+
cwd: process.env.HOME,
64+
env: process.env
65+
});
66+
67+
pty.onData((data) => {
68+
ws2.send(Buffer.from(data, 'utf8'));
69+
});
70+
71+
ws2.on('message', (message) => {
72+
pty.write(message.toString('utf8'));
73+
})
74+
75+
pty.onExit((code) => {
76+
ws2.close(code, code);
77+
})
78+
})
79+
});
80+
81+
server.listen(2123, () => {
82+
ws.emit('apply_Response', {
83+
wsPass: 'pass',
84+
})
85+
})
86+
} */
87+
88+
private static tokenGenerate(length = 20): string {
89+
return Buffer.from(randomBytes(length)).toString('hex')
90+
}
91+
}

src/orchestrators/login.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,6 @@ export class LoginOrchestrator {
1010
LoginOrchestrator.handleRequests(req, res);
1111
});
1212

13-
process.stdout.on('data', (data) => {
14-
console.log(data);
15-
})
16-
1713
server.listen(51_039, 'localhost', () => {
1814
console.log('Opening CLI auth page...')
1915
open('http://localhost:3000/auth/cli');

0 commit comments

Comments
 (0)