streamline package naming

This commit is contained in:
Benjamin Diedrichsen
2026-07-29 13:07:34 +02:00
parent 1ba1c2a32a
commit 7e703c93b1
100 changed files with 141 additions and 139 deletions
+30
View File
@@ -0,0 +1,30 @@
/**
* Factory functions for creating cube configurations
* @module factories
*/
import { type AnyObjectSchema, 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<Schema extends AnyObjectSchema>(
opts: Pick<Manifest<Schema>, 'name'> & Partial<Omit<Manifest<Schema>, 'name'>>
): Manifest<Schema> {
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';
+31
View File
@@ -0,0 +1,31 @@
/**
* @bitsquare/nopy-cubes — the authoring surface for nopy cubes.
*
* Everything a `manifest.mjs` needs and nothing else: no CLI, no prompts, no
* process spawning. `@bitsquare/nopy` re-exports all of it, so a manifest can
* import from either package.
*
* @packageDocumentation
*/
export {
createManifest,
ManifestFactory,
manifest,
} from './factories.js';
export type {
AnyObjectSchema,
CubeSource,
CubeVariables,
DependencySpec,
Hook,
HookContext,
LoadResult,
} from './types.js';
export {
Cube,
Manifest,
zodInner,
zodKind,
} from './types.js';
export { uniqid } from './utils.js';
+224
View File
@@ -0,0 +1,224 @@
/**
* Type definitions for Nopy cubes
* @module types
*/
import { z } from 'zod';
/**
* Any object schema, whatever its shape.
*
* Stands in for zod 3's `z.AnyZodObject`, which zod 4 removed.
*/
export type AnyObjectSchema = z.ZodObject<Record<string, z.ZodType<any>>>;
/**
* 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 AnyObjectSchema = AnyObjectSchema> = (
ctx: HookContext,
variables: z.infer<Schema>
) => void | Promise<void>;
/**
* User-defined specification for a cube
*/
export interface Manifest<Schema extends AnyObjectSchema = AnyObjectSchema> {
/** 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;
/**
* Schema keys holding secrets. Their values are never written to a session
* file, and are masked wherever a command or a variable would be printed.
*
* A plain array rather than schema-level metadata on purpose: `.meta()` and
* `.describe()` both store into zod's global registry, which is per-copy — a
* manifest that builds its schema with its own zod writes the marker into a
* registry this process cannot read. A missed `.describe()` costs an ugly
* prompt label; a missed secret marker writes a password to disk, so this one
* cannot be allowed to fail open. See {@link zodKind} for the same hazard.
*/
secrets?: string[];
/** 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 function Manifest<Schema extends AnyObjectSchema>(
opts: Pick<Manifest<Schema>, 'name'> & Partial<Omit<Manifest<Schema>, 'name'>>
): Manifest<Schema> {
return {
id: opts.id ?? '',
name: opts.name,
schema: opts.schema ?? (z.object({}) as unknown as Schema),
secrets: opts.secrets ?? [],
dependencies: opts.dependencies,
before: opts.before ?? [],
after: opts.after ?? [],
};
}
export namespace Manifest {
/**
* Internal create helper
*/
export function create<Schema extends AnyObjectSchema>(
opts: Pick<Manifest<Schema>, 'name'> & Partial<Omit<Manifest<Schema>, 'name'>>
): Manifest<Schema> {
return Manifest(opts);
}
}
/**
* zod's runtime discriminant for a schema node, as a plain string.
*
* `instanceof z.ZodDefault` compares against the *running* copy of zod. A cube
* manifest is free to build its schema with a different copy — its own
* dependency, or one shipped inside a bundle — and then every `instanceof`
* quietly returns false and the caller falls through to a wrong answer instead
* of failing. `def.type` holds across instances, so nothing here may go back to
* `instanceof`.
*/
export function zodKind(zodType: unknown): string {
return (zodType as { def: { type: string } }).def.type;
}
/**
* The type a wrapper wraps — `.default()`, `.optional()`, `.nullable()`.
* Only call this for a node whose {@link zodKind} is one of those.
*/
export function zodInner(zodType: unknown): z.ZodType {
return (zodType as { def: { innerType: z.ZodType } }).def.innerType;
}
/**
* Reads the `.default()` off a schema field, unwrapping the wrappers that may
* sit above it (`.default().optional()`, `.default().nullable()`).
*
* Returns `undefined` for a field that declares no default — which is also how
* `requiredKeys()` recognises a field the user has to supply.
*/
function defaultValueOf(zodType: z.ZodType): unknown {
const kind = zodKind(zodType);
if (kind === 'default') {
// zod 4 exposes `defaultValue` as a getter that already invokes a lazily
// declared default; the function branch is insurance against that changing.
const { defaultValue } = (zodType as unknown as { def: { defaultValue: unknown } }).def;
return typeof defaultValue === 'function' ? defaultValue() : defaultValue;
}
if (kind === 'optional' || kind === 'nullable') {
return defaultValueOf(zodInner(zodType));
}
return undefined;
}
/**
* Where a cube was discovered.
*
* Worth carrying because a cube's own directory does not say how it got into
* the run: `/…/node_modules/@acme/cubes-net/cubes/x` could equally have come
* from a `cubeDirs` entry pointing straight at it.
*/
export type CubeSource =
/** Found under a `cubeDirs` entry or a `.npcubes` marker, at `dir`. */
| { type: 'dir'; dir: string }
/** Contributed by a package named in `cubePackages`. */
| { type: 'package'; packageName: string; dir: string };
/**
* A fully loaded cube with its filesystem location and runtime state
*/
export class Cube<Schema extends AnyObjectSchema = AnyObjectSchema> {
constructor(
public readonly manifest: Manifest<Schema>,
public readonly dir: string,
public readonly deployScript: string,
/** Defaults to the cube's own directory, for cubes built by hand. */
public readonly source: CubeSource = { type: 'dir', dir }
) {}
get id(): string {
return this.manifest.id;
}
get name(): string {
return this.manifest.name;
}
/**
* Returns default values for the cube's schema.
*
* Parsing an empty object resolves every default in one go, but it fails
* outright as soon as one field has no `.default()`. Falling back to a
* per-field read keeps the defaults that *are* declared instead of dropping
* the whole set — a single required field used to leave the cube with no
* variables at all.
*/
getDefaults(): z.infer<Schema> {
const parsed = this.manifest.schema.safeParse({});
if (parsed.success) return parsed.data as z.infer<Schema>;
const defaults: Record<string, unknown> = {};
for (const [key, zodType] of Object.entries(this.manifest.schema.shape)) {
const value = defaultValueOf(zodType);
if (value !== undefined) defaults[key] = value;
}
return defaults as z.infer<Schema>;
}
/**
* Schema keys that have to be supplied from somewhere: no `.default()`, and
* not optional. Nothing else can fill them in, so a run that cannot prompt
* has to fail rather than deploy a cube with the value missing.
*/
requiredKeys(): string[] {
return Object.entries(this.manifest.schema.shape)
.filter(([, zodType]) => !zodType.safeParse(undefined).success)
.map(([key]) => key);
}
/** Schema keys the manifest declared as secrets. */
get secrets(): string[] {
return this.manifest.secrets ?? [];
}
isSecret(key: string): boolean {
return this.secrets.includes(key);
}
}
/**
* 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[];
}
+38
View File
@@ -0,0 +1,38 @@
/**
* Utility functions for cubes
* @module 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): string {
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: string[] = [];
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('');
}