Cavnas: Implement basic service discovery logic

Change-Id: I71b25076dba94d6491ad4db748b259870991c526
diff --git a/apps/canvas/back/src/lib/nodejs.test.ts b/apps/canvas/back/src/lib/nodejs.test.ts
new file mode 100644
index 0000000..7d406b1
--- /dev/null
+++ b/apps/canvas/back/src/lib/nodejs.test.ts
@@ -0,0 +1,83 @@
+import { NodeJSAnalyzer } from "./nodejs";
+import { FileSystem, RealFileSystem } from "./fs";
+import { Volume, IFs, createFsFromVolume } from "memfs";
+import { test, expect } from "@jest/globals";
+import { expandValue } from "./env";
+import shell from "shelljs";
+
+class InMemoryFileSystem implements FileSystem {
+	constructor(private readonly fs: IFs) {}
+
+	exists(path: string): boolean {
+		return this.fs.existsSync(path);
+	}
+
+	// TODO(gio): add encoding
+	async readFile(path: string, encoding?: BufferEncoding): Promise<string> {
+		const contents = await this.fs.promises.readFile(path);
+		return contents.toString(encoding);
+	}
+}
+
+test("canvas", async () => {
+	const fs: FileSystem = new RealFileSystem("/home/gio/code/apps/canvas/back");
+	const analyzer = new NodeJSAnalyzer();
+	expect(analyzer.detect(fs, "/")).toBe(true);
+	const info = await analyzer.analyze(fs, "/");
+	console.log(info);
+});
+
+test("nodejs", async () => {
+	return;
+	const root = "/";
+	const vol = Volume.fromNestedJSON(
+		{
+			"package.json": JSON.stringify({
+				name: "test",
+				version: "1.0.0",
+				dependencies: {
+					prisma: "6.6.0",
+				},
+			}),
+			"package-lock.json": "fake",
+			"prisma/schema.prisma": `
+				datasource db {
+					provider = "sqlite"
+					url = env("DB_URL")
+				}
+			`,
+		},
+		root,
+	);
+	const fs: FileSystem = new InMemoryFileSystem(createFsFromVolume(vol));
+	const analyzer = new NodeJSAnalyzer();
+	expect(analyzer.detect(fs, root)).toBe(true);
+	const info = await analyzer.analyze(fs, root);
+	console.log(info);
+});
+
+test("env", () => {
+	console.log(expandValue("${PORT} ${DODO_VOLUME_DB}"));
+	console.log(expandValue("$PORT $DODO_VOLUME_DB"));
+	console.log(expandValue("${UNDEFINED:-${MACHINE}${UNDEFINED:-default}}"));
+});
+
+test("clone", async () => {
+	expect(shell.which("ssh-agent")).toBeTruthy();
+	expect(shell.which("ssh-add")).toBeTruthy();
+	expect(shell.which("git")).toBeTruthy();
+	expect(
+		shell.exec(
+			"GIT_SSH_COMMAND='ssh -i /home/gio/.ssh/key -o IdentitiesOnly=yes' git clone git@github.com:giolekva/dodo-blog.git /tmp/dodo-blog",
+		).code,
+	).toBe(0);
+	const fs: FileSystem = new RealFileSystem("/tmp/dodo-blog");
+	const analyzer = new NodeJSAnalyzer();
+	expect(analyzer.detect(fs, "/")).toBe(true);
+	const info = await analyzer.analyze(fs, "/");
+	console.log(info);
+});
+
+test("keygen", () => {
+	expect(shell.exec(`ssh-keygen -y -t ed25519 -f /tmp/key`).code).toBe(0);
+});