Add core models, setup basic CLI, add internal database and migrations to store models.

This commit is contained in:
Madeorsk 2025-06-29 12:58:31 +02:00
parent e018f3443a
commit ea4f691864
Signed by: Madeorsk
GPG key ID: 677E51CA765BB79F
17 changed files with 409 additions and 3 deletions

5
.gitignore vendored
View file

@ -33,3 +33,8 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Finder (MacOS) folder config
.DS_Store
# infernod
infernod.db*

View file

@ -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=="],

66
cli/cli.ts Normal file
View file

@ -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<void> {
// 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 <string>", "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<void> {
this.manager = new Manager();
return this.manager.waitForInitialized();
}
}

View file

@ -0,0 +1,3 @@
import type { Manager } from "../../core/manager.ts";
export function listSoftwares(manager: Manager) {}

53
core/manager.ts Normal file
View file

@ -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<void>;
constructor(public readonly options: { databasePath?: string } = {}) {
this.initialization = this.initialize();
}
/**
* Initialize the infernod manager.
*/
async initialize(): Promise<void> {
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<void> {
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<DuckDBConnection> {
return this.database!.connect();
}
}

View file

@ -0,0 +1,7 @@
import type { DuckDBConnection } from "@duckdb/node-api";
export interface Migration {
getIdentifier(): string;
execute(connection: DuckDBConnection): Promise<void>;
}

View file

@ -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<DuckDBConnection>;
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<boolean> {
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();
}
}

View file

@ -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<void> {
await connection.run(`
CREATE TABLE agents
(
id uuid default gen_random_uuid() primary key,
name varchar(255) not null unique
);
`);
}
}

View file

@ -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<void> {
await connection.run(`
CREATE TABLE application_workers
(
agent_id uuid primary key references agents (id)
);
`);
}
}

View file

@ -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<void> {
await connection.run(`
CREATE TABLE service_providers
(
agent_id uuid primary key references agents (id)
);
`);
}
}

View file

@ -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<void> {
await connection.run(`
CREATE TABLE softwares
(
id uuid default gen_random_uuid(),
name varchar(255) not null unique,
variants jsonb not null
);
`);
}
}

26
core/model/agent.ts Normal file
View file

@ -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(),
},
});

View file

@ -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: {},
});

View file

@ -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: {},
});

32
core/model/software.ts Normal file
View file

@ -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<string, any>;
}
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({})),
},
});

4
infernod.ts Normal file
View file

@ -0,0 +1,4 @@
import { Cli } from "./cli/cli.ts";
const infernod = new Cli();
await infernod.run();

View file

@ -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"
}
}