initial transfer
This commit is contained in:
+46
@@ -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
@@ -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
@@ -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
@@ -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';
|
||||
Vendored
+13
@@ -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';
|
||||
Vendored
+17
@@ -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';
|
||||
Vendored
+22
@@ -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>;
|
||||
Vendored
+102
@@ -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];
|
||||
}
|
||||
Vendored
+74
@@ -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[];
|
||||
}
|
||||
Vendored
+57
@@ -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 {};
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+20
@@ -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;
|
||||
Vendored
+33
@@ -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('');
|
||||
}
|
||||
Reference in New Issue
Block a user