initial transfer

This commit is contained in:
Benjamin Diedrichsen
2026-07-27 13:09:00 +02:00
parent 9f25d48dc2
commit 736c01216a
191 changed files with 17622 additions and 136 deletions
+46
View File
@@ -0,0 +1,46 @@
/**
* Dynamic dependency resolution for cubes
* @module cubes/dependencies
*/
import type { Variables } from '../nopy.common.js';
import type { NopyConfig } from '../nopy.config.js';
import type { DeployCall } from '../nopy.executor.js';
import { type CubeSession, type NopySession } from '../nopy.session.js';
import type { Cube, CubeVariables } from './types.js';
/**
* Context for the resolution process
*/
export declare class BuildContext {
readonly allCubes: Record<string, Cube>;
readonly variables: Variables;
readonly session: NopySession;
readonly config: NopyConfig;
readonly auth: {
method: string;
username?: string;
password?: string;
};
readonly options: {
useDefaults?: boolean;
isSessionReplay?: boolean;
};
readonly deployCalls: DeployCall[];
readonly cubeSessions: CubeSession[];
private readonly resolvedCubes;
constructor(allCubes: Record<string, Cube>, variables: Variables, session: NopySession, config: NopyConfig, auth: {
method: string;
username?: string;
password?: string;
}, options?: {
useDefaults?: boolean;
isSessionReplay?: boolean;
});
/**
* Resolves a cube, its dependencies, and hooks recursively
*/
resolveCube(cubeId: string, host: string, overrides?: CubeVariables): Promise<void>;
/**
* Builds and stores a deployment call for a resolved cube
*/
private buildDeployCall;
}
+114
View File
@@ -0,0 +1,114 @@
/**
* Dynamic dependency resolution for cubes
* @module cubes/dependencies
*/
import { getLogger } from '@logtape/logtape';
import { VariableAssignment } from '../nopy.prompts.js';
const log = getLogger(['nopy', 'resolution']);
/**
* Context for the resolution process
*/
export class BuildContext {
allCubes;
variables;
session;
config;
auth;
options;
deployCalls = [];
cubeSessions = [];
resolvedCubes = new Set();
constructor(allCubes, variables, session, config, auth, options = {}) {
this.allCubes = allCubes;
this.variables = variables;
this.session = session;
this.config = config;
this.auth = auth;
this.options = options;
}
/**
* Resolves a cube, its dependencies, and hooks recursively
*/
async resolveCube(cubeId, host, overrides = {}) {
const cube = this.allCubes[cubeId];
if (!cube) {
throw new Error(`Cube not found: ${cubeId}`);
}
log.debug('Resolving cube', { cubeId, host });
// 1. Assign overrides and defaults
if (Object.keys(overrides).length > 0) {
this.variables.assign(cubeId, 'params', overrides);
}
this.variables.assign(cubeId, 'defaults', cube.getDefaults());
// 2. Variable collection
if (this.options.isSessionReplay) {
const sessionCube = this.session.cubes.find(c => c.key === cubeId);
if (sessionCube) {
this.variables.assign(cubeId, 'defaults', sessionCube.variables);
}
}
else {
await VariableAssignment(cube, this.variables);
}
const currentVars = this.variables.get(cubeId);
const hookCtx = {
exec: (id, vars) => this.resolveCube(id, host, vars),
};
// 3. Execute 'before' hooks
if (cube.manifest.before) {
for (const hook of cube.manifest.before) {
await hook(hookCtx, currentVars);
}
}
// 4. Resolve dynamic dependencies
const depSpecs = cube.manifest.dependencies?.(currentVars) ?? [];
for (const spec of depSpecs) {
const depId = typeof spec === 'string' ? spec : spec[0];
const depVars = typeof spec === 'string' ? {} : (spec[1] ?? {});
await this.resolveCube(depId, host, depVars);
}
// 5. Build the deployment call
this.buildDeployCall(cube, host);
// 6. Execute 'after' hooks
if (cube.manifest.after) {
for (const hook of cube.manifest.after) {
await hook(hookCtx, currentVars);
}
}
}
/**
* Builds and stores a deployment call for a resolved cube
*/
buildDeployCall(cube, host) {
const cubeId = cube.id;
const callKey = `${cubeId}:${host}`;
if (this.resolvedCubes.has(callKey))
return;
const parts = [];
if (this.auth.method === 'password' && this.auth.username && this.auth.password) {
parts.push(`--user ${this.auth.username} --password ${this.auth.password}`);
}
const cubeVars = this.variables.get(cubeId);
Object.entries(cubeVars).forEach(([key, value]) => {
parts.push(`--data "${key}=${value}"`);
});
parts.push(`--chdir ${cube.dir}`);
parts.push(`${cube.dir}/${cube.deployScript}`);
const command = ['pyinfra', host, '-y', ...parts];
this.deployCalls.push({
cube: cubeId,
host,
cwd: cube.dir,
command,
env: cubeVars,
dependencies: [],
});
if (!this.cubeSessions.some(s => s.key === cubeId)) {
this.cubeSessions.push({
key: cubeId,
variables: this.variables.get(cubeId, 'prompts'),
});
}
this.resolvedCubes.add(callKey);
}
}
+21
View File
@@ -0,0 +1,21 @@
/**
* Factory functions for creating cube configurations
* @module cubes/factories
*/
import { Manifest } from './types.js';
/**
* Creates a manifest configuration for a cube
*
* @param opts - Manifest options including name, schema, dependencies, and hooks
* @returns Manifest configuration object
*/
export declare function createManifest<Schema extends import('zod').z.AnyZodObject>(opts: Pick<Manifest<Schema>, 'name'> & Partial<Omit<Manifest<Schema>, 'name'>>): Manifest<Schema>;
/**
* Alias for createManifest - for backwards compatibility with existing manifests
*/
export declare const manifest: typeof createManifest;
/**
* @deprecated Use createManifest or manifest instead
*/
export declare const ManifestFactory: typeof createManifest;
export { Manifest } from './types.js';
+23
View File
@@ -0,0 +1,23 @@
/**
* Factory functions for creating cube configurations
* @module cubes/factories
*/
import { Manifest } from './types.js';
/**
* Creates a manifest configuration for a cube
*
* @param opts - Manifest options including name, schema, dependencies, and hooks
* @returns Manifest configuration object
*/
export function createManifest(opts) {
return Manifest(opts);
}
/**
* Alias for createManifest - for backwards compatibility with existing manifests
*/
export const manifest = createManifest;
/**
* @deprecated Use createManifest or manifest instead
*/
export const ManifestFactory = createManifest;
export { Manifest } from './types.js';
+13
View File
@@ -0,0 +1,13 @@
/**
* Nopy Cubes Module
*
* Self-contained deployment units for pyinfra automation.
*
* @module cubes
*/
export { Cube, Manifest, } from './types.js';
export type { Hook, HookContext, LoadResult, CubeVariables, DependencySpec, } from './types.js';
export { createManifest, manifest, } from './factories.js';
export { loadCubes, findCubeDirectories, getCube, } from './loader.js';
export { BuildContext, } from './dependencies.js';
export { uniqid } from './utils.js';
+17
View File
@@ -0,0 +1,17 @@
/**
* Nopy Cubes Module
*
* Self-contained deployment units for pyinfra automation.
*
* @module cubes
*/
// Types
export { Cube, Manifest, } from './types.js';
// Factory functions
export { createManifest, manifest, } from './factories.js';
// Loader
export { loadCubes, findCubeDirectories, getCube, } from './loader.js';
// Dependencies
export { BuildContext, } from './dependencies.js';
// Utilities
export { uniqid } from './utils.js';
+22
View File
@@ -0,0 +1,22 @@
/**
* Cube discovery and loading from the filesystem
* @module cubes/loader
*/
import { Cube, type LoadResult } from './types.js';
/**
* Traverses upwards from the current working directory to the root
* and collects all directories that contain a `.npcubes` marker file.
*
* Also includes directories specified in the `.nopyrc.json` configuration.
*
* @returns Array of absolute paths to directories containing cubes
*/
export declare function findCubeDirectories(): string[];
/**
* Loads all cubes from discovered cube directories.
*/
export declare function loadCubes(): Promise<LoadResult>;
/**
* Gets information about a single cube by name.
*/
export declare function getCube(cubeName: string): Promise<Cube | undefined>;
+102
View File
@@ -0,0 +1,102 @@
/**
* Cube discovery and loading from the filesystem
* @module cubes/loader
*/
import path from 'node:path';
import { z } from 'zod';
import { fs } from 'zx';
import { loadConfig } from '../nopy.config.js';
import { Cube } from './types.js';
/**
* Traverses upwards from the current working directory to the root
* and collects all directories that contain a `.npcubes` marker file.
*
* Also includes directories specified in the `.nopyrc.json` configuration.
*
* @returns Array of absolute paths to directories containing cubes
*/
export function findCubeDirectories() {
let currentDir = process.cwd();
const config = loadConfig();
const dirSet = new Set(config.cubeDirs.map((dir) => path.resolve(process.cwd(), dir)));
while (true) {
const targetFile = path.join(currentDir, '.npcubes');
if (fs.existsSync(targetFile) && fs.statSync(targetFile).isFile()) {
dirSet.add(currentDir);
}
const parentDir = path.dirname(currentDir);
if (parentDir === currentDir) {
break; // Stop when reaching the root
}
currentDir = parentDir;
}
return [...dirSet];
}
/**
* Extracts cube ID from name pattern [id] or explicit id field
*/
function extractCubeId(manifest) {
if (manifest.id)
return manifest.id;
const match = manifest.name.match(/^\[([^\]]+)\]/);
return match ? match[1] : undefined;
}
/**
* Loads all cubes from discovered cube directories.
*/
export async function loadCubes() {
const cubesFolders = findCubeDirectories();
const cubes = {};
const errors = [];
async function scanDirectory(currentDir, baseDir) {
const entries = await fs.readdir(currentDir, { withFileTypes: true });
const files = entries.filter((e) => e.isFile());
const manifestFile = files.find((f) => f.name === 'manifest.mjs' || f.name.endsWith('.manifest.mjs'));
const deployFile = files.find((f) => f.name === 'deploy.py' || f.name.endsWith('.deploy.py'));
if (manifestFile && deployFile) {
const cubePath = currentDir;
const manifestPath = path.join(cubePath, manifestFile.name);
try {
const manifest = (await import(manifestPath)).default;
if (!manifest || typeof manifest !== 'object') {
errors.push(`Invalid manifest export in ${manifestPath}`);
}
else if (!manifest.name) {
errors.push(`Invalid manifest format in ${manifestPath}: missing 'name'`);
}
else {
const cubeId = extractCubeId(manifest) || path.basename(cubePath);
if (cubes[cubeId]) {
errors.push(`Duplicate cube id '${cubeId}'`);
return;
}
// Ensure basic properties
manifest.id = cubeId;
manifest.schema = manifest.schema ?? z.object({});
cubes[cubeId] = new Cube(manifest, cubePath, deployFile.name);
}
}
catch (err) {
errors.push(`Failed to load manifest ${manifestPath}: ${err}`);
}
}
for (const entry of entries) {
if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
await scanDirectory(path.join(currentDir, entry.name), baseDir);
}
}
}
await Promise.all(cubesFolders.map(async (folder) => {
if (fs.existsSync(folder)) {
await scanDirectory(folder, folder);
}
}));
return { cubes, errors };
}
/**
* Gets information about a single cube by name.
*/
export async function getCube(cubeName) {
const { cubes } = await loadCubes();
return cubes[cubeName];
}
+74
View File
@@ -0,0 +1,74 @@
/**
* Type definitions for Nopy cubes
* @module cubes/types
*/
import { z } from 'zod';
/**
* Variables that can be passed to a cube
*/
export type CubeVariables = Record<string, string | number | boolean>;
/**
* A dependency specification
*/
export type DependencySpec = string | [id: string, variables?: CubeVariables];
/**
* Context passed to cube hooks for executing other cubes
*/
export interface HookContext {
exec: (key: string, variables: CubeVariables) => Promise<void> | void;
}
/**
* Hook function type for before/after cube execution
*/
export type Hook<Schema extends z.AnyZodObject = z.AnyZodObject> = (ctx: HookContext, variables: z.infer<Schema>) => void | Promise<void>;
/**
* User-defined specification for a cube
*/
export interface Manifest<Schema extends z.AnyZodObject = z.AnyZodObject> {
/** Unique identifier for the cube (used for dependency references) */
id: string;
/** Human-readable name of the cube */
name: string;
/** Zod schema for validating cube variables */
schema: Schema;
/** Dynamic dependency resolver based on collected variables */
dependencies?: (variables: z.infer<Schema>) => DependencySpec[];
/** Hooks to run before cube execution */
before?: Hook<Schema>[];
/** Hooks to run after cube execution */
after?: Hook<Schema>[];
}
/**
* Factory function and namespace for Manifest
*/
export declare function Manifest<Schema extends z.AnyZodObject>(opts: Pick<Manifest<Schema>, 'name'> & Partial<Omit<Manifest<Schema>, 'name'>>): Manifest<Schema>;
export declare namespace Manifest {
/**
* Internal create helper
*/
function create<Schema extends z.AnyZodObject>(opts: Pick<Manifest<Schema>, 'name'> & Partial<Omit<Manifest<Schema>, 'name'>>): Manifest<Schema>;
}
/**
* A fully loaded cube with its filesystem location and runtime state
*/
export declare class Cube<Schema extends z.AnyZodObject = z.AnyZodObject> {
readonly manifest: Manifest<Schema>;
readonly dir: string;
readonly deployScript: string;
constructor(manifest: Manifest<Schema>, dir: string, deployScript: string);
get id(): string;
get name(): string;
/**
* Returns default values for the cube's schema
*/
getDefaults(): z.infer<Schema>;
}
/**
* Result of loading cubes from the filesystem
*/
export interface LoadResult {
/** Map of cube key to Cube object */
cubes: Record<string, Cube>;
/** List of errors encountered during loading */
errors: string[];
}
+57
View File
@@ -0,0 +1,57 @@
/**
* Type definitions for Nopy cubes
* @module cubes/types
*/
import { z } from 'zod';
/**
* Factory function and namespace for Manifest
*/
export function Manifest(opts) {
return {
id: opts.id ?? '',
name: opts.name,
schema: opts.schema ?? z.object({}),
dependencies: opts.dependencies,
before: opts.before ?? [],
after: opts.after ?? [],
};
}
(function (Manifest) {
/**
* Internal create helper
*/
function create(opts) {
return Manifest(opts);
}
Manifest.create = create;
})(Manifest || (Manifest = {}));
/**
* A fully loaded cube with its filesystem location and runtime state
*/
export class Cube {
manifest;
dir;
deployScript;
constructor(manifest, dir, deployScript) {
this.manifest = manifest;
this.dir = dir;
this.deployScript = deployScript;
}
get id() {
return this.manifest.id;
}
get name() {
return this.manifest.name;
}
/**
* Returns default values for the cube's schema
*/
getDefaults() {
try {
return this.manifest.schema.parse({});
}
catch {
return {};
}
}
}
+20
View File
@@ -0,0 +1,20 @@
/**
* Utility functions for cubes
* @module cubes/utils
*/
/**
* Generates a random string of the specified length using the current nanotime as a seed.
*
* Uses a simple Linear Congruential Generator (LCG) seeded with high-resolution time.
* Suitable for generating unique identifiers, not for cryptographic purposes.
*
* @param length - The desired length of the random string (default: 5)
* @returns A random alphanumeric string of the specified length
*
* @example
* ```typescript
* const id = uniqid(); // e.g., "Kx7Pm"
* const longId = uniqid(10); // e.g., "Kx7PmQr2Yw"
* ```
*/
export declare function uniqid(length?: number): string;
+33
View File
@@ -0,0 +1,33 @@
/**
* Utility functions for cubes
* @module cubes/utils
*/
/**
* Generates a random string of the specified length using the current nanotime as a seed.
*
* Uses a simple Linear Congruential Generator (LCG) seeded with high-resolution time.
* Suitable for generating unique identifiers, not for cryptographic purposes.
*
* @param length - The desired length of the random string (default: 5)
* @returns A random alphanumeric string of the specified length
*
* @example
* ```typescript
* const id = uniqid(); // e.g., "Kx7Pm"
* const longId = uniqid(10); // e.g., "Kx7PmQr2Yw"
* ```
*/
export function uniqid(length = 5) {
const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const charsetLength = charset.length;
// Use process.hrtime.bigint() for high-resolution time in nanoseconds
let seed = Number(process.hrtime.bigint() % BigInt(Number.MAX_SAFE_INTEGER));
const randomString = [];
for (let i = 0; i < length; i++) {
// Simple linear congruential generator (LCG) for pseudo-randomness
seed = (seed * 48271) % 2147483647;
const index = seed % charsetLength;
randomString.push(charset[index]);
}
return randomString.join('');
}