diff --git a/.gitignore b/.gitignore index bc6bf7e..d23ca86 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,8 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json # Finder (MacOS) folder config .DS_Store + + +# infernod + +infernod.db* diff --git a/bun.lock b/bun.lock index b838215..f9386be 100644 --- a/bun.lock +++ b/bun.lock @@ -3,21 +3,50 @@ "workspaces": { "": { "name": "infernod", + "dependencies": { + "@duckdb/node-api": "^1.3.1-alpha.23", + "@sharkitek/core": "^4.1.0", + "commander": "^14.0.0", + }, "devDependencies": { "@types/bun": "latest", + "@types/commander": "^2.12.5", + "prettier": "^3.6.2", }, "peerDependencies": { - "typescript": "^5", + "typescript": "^5.8.3", }, }, }, "packages": { + "@duckdb/node-api": ["@duckdb/node-api@1.3.1-alpha.23", "", { "dependencies": { "@duckdb/node-bindings": "1.3.1-alpha.23" } }, "sha512-D2dug8GMCC/PNkrB37gik0Wm74J3/JFQxIrEk7c02nK74BmLrZsxTRG5cLc6fqJuqj+AZ31jjgWroUtAaHSN/A=="], + + "@duckdb/node-bindings": ["@duckdb/node-bindings@1.3.1-alpha.23", "", { "optionalDependencies": { "@duckdb/node-bindings-darwin-arm64": "1.3.1-alpha.23", "@duckdb/node-bindings-darwin-x64": "1.3.1-alpha.23", "@duckdb/node-bindings-linux-arm64": "1.3.1-alpha.23", "@duckdb/node-bindings-linux-x64": "1.3.1-alpha.23", "@duckdb/node-bindings-win32-x64": "1.3.1-alpha.23" } }, "sha512-uVFuy0bfV/XHRtMKZMDjaONbZplGXBOep20DZqA8BbWwWBnc37XMZoXZcykLRXdMVvzUll13mwfL1InDa9k1gw=="], + + "@duckdb/node-bindings-darwin-arm64": ["@duckdb/node-bindings-darwin-arm64@1.3.1-alpha.23", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4UbDqkp+vOACjkOp6kQiTtrkzZw9CFb1GIEcDLZ+iySka0fz5sNNldJUNPuCbWm0Racu17OBzoSXQqF5Q0KUmQ=="], + + "@duckdb/node-bindings-darwin-x64": ["@duckdb/node-bindings-darwin-x64@1.3.1-alpha.23", "", { "os": "darwin", "cpu": "x64" }, "sha512-qdBsiFR5dPswAwDiCqE227cP18618uKX5E3U5qEk0bsb9C8f+I+k3aHl8nLjE0vXOCxBUeI6xr3U9KskjyixpA=="], + + "@duckdb/node-bindings-linux-arm64": ["@duckdb/node-bindings-linux-arm64@1.3.1-alpha.23", "", { "os": "linux", "cpu": "arm64" }, "sha512-kqWaYd6AkpLkJbIPd4UqujQE2G/QCriLIgFkpUgWVqP3yyCL/Oq+5oRvRSdUsuFJQqHztj8L3ZOy76xd5q12Ww=="], + + "@duckdb/node-bindings-linux-x64": ["@duckdb/node-bindings-linux-x64@1.3.1-alpha.23", "", { "os": "linux", "cpu": "x64" }, "sha512-lANN+JYoBTqyOF0dGPiXz8r9+GGs6mm5nwB08boWYpQCjdoG2gFpK6XvT56HO+t01wbKIngDNrdThjqdZW9+Aw=="], + + "@duckdb/node-bindings-win32-x64": ["@duckdb/node-bindings-win32-x64@1.3.1-alpha.23", "", { "os": "win32", "cpu": "x64" }, "sha512-bYBLYPAIs4KTSnTtqsRnjBH1Hgu9QVa3Yxio4TYktneRG7rHeX89nKcfszLLIkSUt6xhRNwYoeSh/HbXmx46fA=="], + + "@sharkitek/core": ["@sharkitek/core@4.1.0", "", {}, "sha512-z+4YPJlH3/7EHiGY9WsuPhsWJsUDlf5V2D4nK398rVshYQP+fGUvJsGGtIH+YW3K3iyDft8cseWQrH67cix+lA=="], + "@types/bun": ["@types/bun@1.2.17", "", { "dependencies": { "bun-types": "1.2.17" } }, "sha512-l/BYs/JYt+cXA/0+wUhulYJB6a6p//GTPiJ7nV+QHa8iiId4HZmnu/3J/SowP5g0rTiERY2kfGKXEK5Ehltx4Q=="], + "@types/commander": ["@types/commander@2.12.5", "", { "dependencies": { "commander": "*" } }, "sha512-YXGZ/rz+s57VbzcvEV9fUoXeJlBt5HaKu5iUheiIWNsJs23bz6AnRuRiZBRVBLYyPnixNvVnuzM5pSaxr8Yp/g=="], + "@types/node": ["@types/node@24.0.3", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg=="], "bun-types": ["bun-types@1.2.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-ElC7ItwT3SCQwYZDYoAH+q6KT4Fxjl8DtZ6qDulUFBmXA8YB4xo+l54J9ZJN+k2pphfn9vk7kfubeSd5QfTVJQ=="], + "commander": ["commander@14.0.0", "", {}, "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA=="], + + "prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="], + "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], diff --git a/cli/cli.ts b/cli/cli.ts new file mode 100644 index 0000000..d3c4f0b --- /dev/null +++ b/cli/cli.ts @@ -0,0 +1,66 @@ +import { Command } from "commander"; +import { Manager } from "../core/manager.ts"; +import { listSoftwares } from "./commands/softwares.ts"; + +export class Cli { + /** + * CLI instance. + * @protected + */ + protected command: Command; + + /** + * Infernod manager instance. + * @protected + */ + protected manager!: Manager; + + constructor() { + this.command = new Command(); + this.setup(); + } + + /** + * Run the currently passed command using the defined CLI. + */ + async run(): Promise { + // Parse the provided command. + await this.command.parseAsync(); + } + + /** + * Setup CLI commands, args and flags. + * @protected + */ + protected setup(): void { + this.command + .name("infernod") + .description("Infernod Manager CLI") + .version("0.1.0") + + .option("-d, --database ", "set database path") + + // Boot the CLI using provided global options. + .hook("preAction", async () => { + await this.boot(); + }); + + const softwares = this.command + .command("softwares") + .description("Softwares management commands."); + softwares + .command("list") + .description("List all registered softwares.") + .action(() => listSoftwares(this.manager)); + } + + /** + * Boot the CLI internal requirements. + * Mainly, initialize the manager. + * @protected + */ + protected boot(): Promise { + this.manager = new Manager(); + return this.manager.waitForInitialized(); + } +} diff --git a/cli/commands/softwares.ts b/cli/commands/softwares.ts new file mode 100644 index 0000000..6091e0c --- /dev/null +++ b/cli/commands/softwares.ts @@ -0,0 +1,3 @@ +import type { Manager } from "../../core/manager.ts"; + +export function listSoftwares(manager: Manager) {} diff --git a/core/manager.ts b/core/manager.ts new file mode 100644 index 0000000..fc5ed0c --- /dev/null +++ b/core/manager.ts @@ -0,0 +1,53 @@ +import { DuckDBConnection, DuckDBInstance } from "@duckdb/node-api"; +import { Migrations } from "./migrations/migrations.ts"; + +export class Manager { + /** + * The internal database connection, available when the manager is initialized. + * @protected + */ + protected database?: DuckDBInstance; + + /** + * Main initialization promise. + * @protected + */ + protected initialization: Promise; + + constructor(public readonly options: { databasePath?: string } = {}) { + this.initialization = this.initialize(); + } + + /** + * Initialize the infernod manager. + */ + async initialize(): Promise { + this.database = await DuckDBInstance.create(this.databasePath); + + // Setup database. + const migrations = new Migrations(this); + await migrations.execute(); + await migrations.close(); + } + + /** + * Return the promise to wait for initialization. + */ + async waitForInitialized(): Promise { + return this.initialization; + } + + /** + * Get database path option, using default value if undefined. + */ + get databasePath(): string { + return this.options?.databasePath ?? "infernod.db"; + } + + /** + * Get a new database connection. + */ + newDatabaseConnection(): Promise { + return this.database!.connect(); + } +} diff --git a/core/migrations/migration.ts b/core/migrations/migration.ts new file mode 100644 index 0000000..225c3b9 --- /dev/null +++ b/core/migrations/migration.ts @@ -0,0 +1,7 @@ +import type { DuckDBConnection } from "@duckdb/node-api"; + +export interface Migration { + getIdentifier(): string; + + execute(connection: DuckDBConnection): Promise; +} diff --git a/core/migrations/migrations.ts b/core/migrations/migrations.ts new file mode 100644 index 0000000..2ddb2de --- /dev/null +++ b/core/migrations/migrations.ts @@ -0,0 +1,74 @@ +import type { DuckDBConnection } from "@duckdb/node-api"; + +import type { Manager } from "../manager.ts"; +import type { Migration } from "./migration.ts"; + +import { V001_AddAgents } from "./migrations/V001_AddAgents.ts"; +import { V002_AddApplicationWorkers } from "./migrations/V002_AddApplicationWorkers.ts"; +import { V003_AddServiceProviders } from "./migrations/V003_AddServiceProviders.ts"; +import { V004_AddSoftwares } from "./migrations/V004_AddSoftwares.ts"; + +export class Migrations { + protected connection: Promise; + + constructor(protected manager: Manager) { + this.connection = this.manager.newDatabaseConnection(); + } + + /** + * Create migrations table if it does not exist. + * @protected + */ + protected async setupMigrationsTable() { + await ( + await this.connection + ).run(` + CREATE OR REPLACE TABLE migrations( + id VARCHAR(255) PRIMARY KEY, + migrated_at TIMESTAMPTZ DEFAULT NOW() + ); + `); + } + + /** + * Check if the provided migration is executed or not. + * @param migrationIdentifier The migration identifier to check for. + * @protected + */ + protected async isExecuted(migrationIdentifier: string): Promise { + return ( + ( + await ( + await this.connection + ).runAndRead("SELECT id FROM migrations WHERE id = $id", { + id: migrationIdentifier, + }) + ).columnCount > 0 + ); + } + + /** + * Register the provided migration. + * @param MigrationClass Migration class to register. + * @protected + */ + protected async register(MigrationClass: { new (): Migration }) { + const migration = new MigrationClass(); + if (!(await this.isExecuted(migration.getIdentifier()))) { + await migration.execute(await this.connection); + } + } + + async execute() { + await this.setupMigrationsTable(); + + await this.register(V001_AddAgents); + await this.register(V002_AddApplicationWorkers); + await this.register(V003_AddServiceProviders); + await this.register(V004_AddSoftwares); + } + + async close() { + (await this.connection).closeSync(); + } +} diff --git a/core/migrations/migrations/V001_AddAgents.ts b/core/migrations/migrations/V001_AddAgents.ts new file mode 100644 index 0000000..af5f34a --- /dev/null +++ b/core/migrations/migrations/V001_AddAgents.ts @@ -0,0 +1,18 @@ +import type { Migration } from "../migration.ts"; +import type { DuckDBConnection } from "@duckdb/node-api"; + +export class V001_AddAgents implements Migration { + getIdentifier(): string { + return "V001_AddAgents"; + } + + async execute(connection: DuckDBConnection): Promise { + await connection.run(` + CREATE TABLE agents + ( + id uuid default gen_random_uuid() primary key, + name varchar(255) not null unique + ); + `); + } +} diff --git a/core/migrations/migrations/V002_AddApplicationWorkers.ts b/core/migrations/migrations/V002_AddApplicationWorkers.ts new file mode 100644 index 0000000..57ec3cc --- /dev/null +++ b/core/migrations/migrations/V002_AddApplicationWorkers.ts @@ -0,0 +1,17 @@ +import type { Migration } from "../migration.ts"; +import type { DuckDBConnection } from "@duckdb/node-api"; + +export class V002_AddApplicationWorkers implements Migration { + getIdentifier(): string { + return "V002_AddApplicationWorkers"; + } + + async execute(connection: DuckDBConnection): Promise { + await connection.run(` + CREATE TABLE application_workers + ( + agent_id uuid primary key references agents (id) + ); + `); + } +} diff --git a/core/migrations/migrations/V003_AddServiceProviders.ts b/core/migrations/migrations/V003_AddServiceProviders.ts new file mode 100644 index 0000000..b471778 --- /dev/null +++ b/core/migrations/migrations/V003_AddServiceProviders.ts @@ -0,0 +1,17 @@ +import type { Migration } from "../migration.ts"; +import type { DuckDBConnection } from "@duckdb/node-api"; + +export class V003_AddServiceProviders implements Migration { + getIdentifier(): string { + return "V003_AddServiceProviders"; + } + + async execute(connection: DuckDBConnection): Promise { + await connection.run(` + CREATE TABLE service_providers + ( + agent_id uuid primary key references agents (id) + ); + `); + } +} diff --git a/core/migrations/migrations/V004_AddSoftwares.ts b/core/migrations/migrations/V004_AddSoftwares.ts new file mode 100644 index 0000000..a024094 --- /dev/null +++ b/core/migrations/migrations/V004_AddSoftwares.ts @@ -0,0 +1,19 @@ +import type { Migration } from "../migration.ts"; +import type { DuckDBConnection } from "@duckdb/node-api"; + +export class V004_AddSoftwares implements Migration { + getIdentifier(): string { + return "V004_AddSoftwares"; + } + + async execute(connection: DuckDBConnection): Promise { + await connection.run(` + CREATE TABLE softwares + ( + id uuid default gen_random_uuid(), + name varchar(255) not null unique, + variants jsonb not null + ); + `); + } +} diff --git a/core/model/agent.ts b/core/model/agent.ts new file mode 100644 index 0000000..e224c2c --- /dev/null +++ b/core/model/agent.ts @@ -0,0 +1,26 @@ +import s from "@sharkitek/core"; + +/** + * An **Agent** is a generic node object, which can carry out assigned tasks. + */ +export class Agent { + /** + * Unique agent UUID. + */ + id!: string; + + /** + * The name of the agent must be unique. + * It is a human-understandable identifier. + */ + name!: string; +} + +export const AgentModel = s.defineModel({ + Class: Agent, + identifier: "id", + properties: { + id: s.property.string(), + name: s.property.string(), + }, +}); diff --git a/core/model/application-worker.ts b/core/model/application-worker.ts new file mode 100644 index 0000000..4e585b5 --- /dev/null +++ b/core/model/application-worker.ts @@ -0,0 +1,14 @@ +import s from "@sharkitek/core"; + +import { Agent, AgentModel } from "./agent.ts"; + +/** + * An **ApplicationWorker** agent is responsible for running assigned instances of an application, + * using their specific environment. + */ +export class ApplicationWorker extends Agent {} + +export const ApplicationWorkerModel = s.extend(AgentModel, { + Class: ApplicationWorker, + properties: {}, +}); diff --git a/core/model/service-provider.ts b/core/model/service-provider.ts new file mode 100644 index 0000000..33da49e --- /dev/null +++ b/core/model/service-provider.ts @@ -0,0 +1,14 @@ +import s from "@sharkitek/core"; + +import { Agent, AgentModel } from "./agent.ts"; + +/** + * The **Service Provider** is responsible for providing a database, a storage solution, + * or a way to execute a specific thing when an application needs it. + */ +export class ServiceProvider extends Agent {} + +export const ServiceProviderModel = s.extend(AgentModel, { + Class: ServiceProvider, + properties: {}, +}); diff --git a/core/model/software.ts b/core/model/software.ts new file mode 100644 index 0000000..74c69aa --- /dev/null +++ b/core/model/software.ts @@ -0,0 +1,32 @@ +import s from "@sharkitek/core"; + +/** + * A **Software** describes the required *services* and *application environment* to run an application. + */ +export class Software { + /** + * Unique software UUID. + */ + id!: string; + + /** + * The name of the software must be unique. + * It is a human-understandable identifier. + */ + name!: string; + + /** + * Software variants identifiers, with their associated metadata. + */ + variants!: Map; +} + +export const SoftwareModel = s.defineModel({ + Class: Software, + identifier: "id", + properties: { + id: s.property.string(), + name: s.property.string(), + variants: s.property.stringMap(s.property.object({})), + }, +}); diff --git a/infernod.ts b/infernod.ts new file mode 100644 index 0000000..950a7ea --- /dev/null +++ b/infernod.ts @@ -0,0 +1,4 @@ +import { Cli } from "./cli/cli.ts"; + +const infernod = new Cli(); +await infernod.run(); diff --git a/package.json b/package.json index 953ebe2..40f808c 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,22 @@ { "name": "infernod", - "version": "0.1", + "version": "0.1.0", "type": "module", "scripts": { + "infernod": "bun run infernod.ts", "format": "prettier . --write" }, "devDependencies": { - "@types/bun": "latest" + "@types/bun": "latest", + "@types/commander": "^2.12.5", + "prettier": "^3.6.2" }, "peerDependencies": { "typescript": "^5.8.3" + }, + "dependencies": { + "@duckdb/node-api": "^1.3.1-alpha.23", + "@sharkitek/core": "^4.1.0", + "commander": "^14.0.0" } }