nopy: add create-cube command scaffolding a cube from bundled templates
Publish snapshot / snapshot (push) Successful in 1m17s

Gathers id, name and directory from flags or prompts (only what the flags
do not supply), then writes manifest.mjs + deploy.py from templates under
src/templates/cube. Templates are named *.example.* so the template
directory itself can never match the loader's manifest+deploy pair rule.

The scaffold refuses a directory that already holds cube files by the
loader's own patterns, checks the id against the loaded cube set (best
effort, exempting the target directory so --force re-scaffolds work), and
warns when the target lands outside every configured cube directory.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ApuW1MMGK2pUQ9mTVRTxc5
This commit is contained in:
Benjamin Diedrichsen
2026-09-02 13:57:51 +02:00
co-authored by Claude Fable 5
parent 568d4c83ff
commit 8973ff7113
11 changed files with 645 additions and 2 deletions
+48
View File
@@ -41,6 +41,7 @@ import { Cube, Manifest } from '@bitsquare/nopy-cubes';
import { Variables } from '../src/nopy.common.js';
import {
AuthSelection,
CubeScaffoldPrompts,
CubeSelection,
HostSelection,
PasswordSelection,
@@ -532,3 +533,50 @@ describe('VariableAssignment', () => {
expect(variables.get('svc')).toEqual({ port: 9090, enabled: true, maybe: null });
});
});
describe('CubeScaffoldPrompts', () => {
const suggestDir = vi.fn((id: string) => `cubes/${id}`);
it('passes fully-given answers through without asking anything', async () => {
inquirerPrompt.mockResolvedValue({});
const given = { id: 'net:x', name: 'X', dir: 'cubes/net/x' };
await expect(CubeScaffoldPrompts(given, suggestDir)).resolves.toEqual(given);
for (const q of questions()) {
expect(q.when()).toBe(false);
}
});
it('asks only for what is missing and merges the answers', async () => {
inquirerPrompt.mockResolvedValue({ name: 'Typed name', dir: 'typed/dir' });
const result = await CubeScaffoldPrompts({ id: 'net:x' }, suggestDir);
expect(result).toEqual({ id: 'net:x', name: 'Typed name', dir: 'typed/dir' });
expect(question('id')?.when()).toBe(false);
expect(question('name')?.when()).toBe(true);
expect(question('dir')?.when()).toBe(true);
});
it('wires the id validation into the prompt', async () => {
inquirerPrompt.mockResolvedValue({ id: 'ok', name: 'n', dir: 'd' });
await CubeScaffoldPrompts({}, suggestDir);
const validate = question('id')?.validate;
expect(validate('net:x')).toBe(true);
expect(validate('has space')).toBeTypeOf('string');
expect(question('name')?.validate(' ')).toBeTypeOf('string');
});
it('derives the directory default from the id, typed or given', async () => {
inquirerPrompt.mockResolvedValue({ id: 'typed:id', name: 'n', dir: 'd' });
await CubeScaffoldPrompts({}, suggestDir);
expect(question('dir')?.default({ id: 'typed:id' })).toBe('cubes/typed:id');
await CubeScaffoldPrompts({ id: 'given:id' }, suggestDir);
expect(question('dir')?.default({})).toBe('cubes/given:id');
});
});