@runek/cli 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +60 -0
- package/dist/index.js +137 -0
- package/dist/lib.js +107 -0
- package/package.json +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 nullorder
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# runek
|
|
2
|
+
|
|
3
|
+
**Procedural 3D components for React Three Fiber — pull the source, own the world.**
|
|
4
|
+
|
|
5
|
+
[Runek](https://runek.nullorder.org) is a source registry of procedural,
|
|
6
|
+
seeded R3F components ("shadcn for 3D worlds"). Every component — a
|
|
7
|
+
bookshelf, a lake, a whole house — generates its geometry from props and a
|
|
8
|
+
`seed`. No models, no textures, no CDN. This CLI copies editable component
|
|
9
|
+
**source** into your project; you own the files.
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npx @runek/cli init # write runek.config.json + the install dir
|
|
15
|
+
npx @runek/cli add player terrain bookshelf # pull source + deps into your project
|
|
16
|
+
npx @runek/cli list # browse the catalog
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`add` resolves registry dependencies recursively (every component pulls
|
|
20
|
+
`core`), rewrites the `@runek/core` import to your local copy, and installs
|
|
21
|
+
the npm packages the components need via your detected package manager.
|
|
22
|
+
|
|
23
|
+
Then compose a world:
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
import { World } from './runek/core'
|
|
27
|
+
import { Bookshelf } from './runek/Bookshelf'
|
|
28
|
+
import { Player } from './runek/Player'
|
|
29
|
+
import { Terrain } from './runek/Terrain'
|
|
30
|
+
|
|
31
|
+
export function FirstWorld() {
|
|
32
|
+
return (
|
|
33
|
+
<World>
|
|
34
|
+
<Terrain size={[40, 40]} />
|
|
35
|
+
<Bookshelf position={[0, 1, 0]} seed={42} fill={0.8} />
|
|
36
|
+
<Player />
|
|
37
|
+
</World>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Same `seed` → same world, every time.
|
|
43
|
+
|
|
44
|
+
## Options
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
--registry <url|path> Registry base (default: https://runek.nullorder.org/r)
|
|
48
|
+
--dir <path> Install directory (default: src/runek)
|
|
49
|
+
--overwrite Overwrite files that already exist
|
|
50
|
+
--no-install Print the dependency install command instead of running it
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Links
|
|
54
|
+
|
|
55
|
+
[Docs](https://runek.nullorder.org/docs) ·
|
|
56
|
+
[Component gallery](https://runek.nullorder.org/gallery) ·
|
|
57
|
+
[Walk the library](https://runek.nullorder.org/library) ·
|
|
58
|
+
[GitHub](https://github.com/nullorder/runek)
|
|
59
|
+
|
|
60
|
+
MIT © nullorder
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdirSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { parseArgs } from 'node:util';
|
|
5
|
+
import { collectDependencies, configExists, DEFAULT_CONFIG, detectPackageManager, fetchIndex, installCommand, installDependencies, readConfig, resolveItems, writeConfig, writeFiles, } from "./lib.js";
|
|
6
|
+
const color = (code) => (s) => `\x1b[${code}m${s}\x1b[0m`;
|
|
7
|
+
const bold = color('1');
|
|
8
|
+
const dim = color('2');
|
|
9
|
+
const green = color('32');
|
|
10
|
+
const yellow = color('33');
|
|
11
|
+
const red = color('31');
|
|
12
|
+
const cyan = color('36');
|
|
13
|
+
const CONFIG_HINT = cyan('runek.config.json');
|
|
14
|
+
const HELP = `${bold('runek')} — pull procedural 3D component source into your project
|
|
15
|
+
|
|
16
|
+
${bold('Usage')}
|
|
17
|
+
runek init [options] Create ${CONFIG_HINT} and the component directory
|
|
18
|
+
runek add <name...> [options] Add components (and their dependencies)
|
|
19
|
+
runek list [options] List everything in the registry
|
|
20
|
+
|
|
21
|
+
${bold('Options')}
|
|
22
|
+
--registry <url|path> Registry base (default: ${DEFAULT_CONFIG.registry})
|
|
23
|
+
--dir <path> Install directory (default: ${DEFAULT_CONFIG.dir})
|
|
24
|
+
--overwrite Overwrite files that already exist
|
|
25
|
+
--no-install Print the dependency install command instead of running it
|
|
26
|
+
--force (init) overwrite an existing config
|
|
27
|
+
-h, --help Show this help
|
|
28
|
+
|
|
29
|
+
${bold('Examples')}
|
|
30
|
+
runek init
|
|
31
|
+
runek add player terrain bookshelf
|
|
32
|
+
runek list --registry ./registry
|
|
33
|
+
`;
|
|
34
|
+
async function main() {
|
|
35
|
+
const { values, positionals } = parseArgs({
|
|
36
|
+
allowPositionals: true,
|
|
37
|
+
options: {
|
|
38
|
+
registry: { type: 'string' },
|
|
39
|
+
dir: { type: 'string' },
|
|
40
|
+
'no-install': { type: 'boolean' },
|
|
41
|
+
overwrite: { type: 'boolean' },
|
|
42
|
+
force: { type: 'boolean' },
|
|
43
|
+
help: { type: 'boolean', short: 'h' },
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
const [command, ...names] = positionals;
|
|
47
|
+
const opts = values;
|
|
48
|
+
if (opts.help || command === 'help' || !command) {
|
|
49
|
+
process.stdout.write(HELP);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
switch (command) {
|
|
53
|
+
case 'init':
|
|
54
|
+
return init(opts);
|
|
55
|
+
case 'add':
|
|
56
|
+
return add(names, opts);
|
|
57
|
+
case 'list':
|
|
58
|
+
case 'ls':
|
|
59
|
+
return list(opts);
|
|
60
|
+
default:
|
|
61
|
+
throw new Error(`unknown command "${command}" — run "runek --help"`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function init(opts) {
|
|
65
|
+
const cwd = process.cwd();
|
|
66
|
+
if (configExists(cwd) && !opts.force) {
|
|
67
|
+
throw new Error(`${CONFIG_HINT} already exists — pass --force to overwrite`);
|
|
68
|
+
}
|
|
69
|
+
const config = { ...DEFAULT_CONFIG };
|
|
70
|
+
if (opts.registry)
|
|
71
|
+
config.registry = opts.registry;
|
|
72
|
+
if (opts.dir)
|
|
73
|
+
config.dir = opts.dir;
|
|
74
|
+
writeConfig(cwd, config);
|
|
75
|
+
mkdirSync(join(cwd, config.dir), { recursive: true });
|
|
76
|
+
console.log(`${green('✓')} wrote ${CONFIG_HINT}`);
|
|
77
|
+
console.log(`${green('✓')} created ${cyan(config.dir)}`);
|
|
78
|
+
console.log(`\n${bold('Next:')} runek add player terrain bookshelf`);
|
|
79
|
+
}
|
|
80
|
+
async function add(names, opts) {
|
|
81
|
+
if (names.length === 0)
|
|
82
|
+
throw new Error('specify at least one component, e.g. "runek add bookshelf"');
|
|
83
|
+
const cwd = process.cwd();
|
|
84
|
+
const config = readConfig(cwd);
|
|
85
|
+
if (opts.registry)
|
|
86
|
+
config.registry = opts.registry;
|
|
87
|
+
if (opts.dir)
|
|
88
|
+
config.dir = opts.dir;
|
|
89
|
+
const manifests = await resolveItems(config.registry, names);
|
|
90
|
+
const requested = new Set(names);
|
|
91
|
+
for (const m of manifests) {
|
|
92
|
+
const tag = requested.has(m.name) ? '' : dim(' (dependency)');
|
|
93
|
+
console.log(`${cyan('•')} ${m.name}${tag}`);
|
|
94
|
+
}
|
|
95
|
+
const files = manifests.flatMap((m) => m.files);
|
|
96
|
+
const { written, skipped } = writeFiles(cwd, config.dir, files, !!opts.overwrite);
|
|
97
|
+
for (const path of written)
|
|
98
|
+
console.log(` ${green('+')} ${config.dir}/${path}`);
|
|
99
|
+
for (const path of skipped)
|
|
100
|
+
console.log(` ${yellow('•')} ${config.dir}/${path} ${dim('(exists, skipped)')}`);
|
|
101
|
+
if (skipped.length > 0)
|
|
102
|
+
console.log(dim(' pass --overwrite to replace skipped files'));
|
|
103
|
+
const deps = collectDependencies(manifests);
|
|
104
|
+
if (deps.length === 0) {
|
|
105
|
+
console.log(`\n${green('✓')} done`);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const pm = detectPackageManager(cwd);
|
|
109
|
+
if (opts['no-install']) {
|
|
110
|
+
console.log(`\n${bold('Install dependencies:')}\n ${installCommand(pm, deps)}`);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
console.log(`\n${bold('Installing')} ${deps.join(', ')} ${dim(`(${pm})`)}`);
|
|
114
|
+
installDependencies(cwd, pm, deps);
|
|
115
|
+
console.log(`\n${green('✓')} done`);
|
|
116
|
+
}
|
|
117
|
+
async function list(opts) {
|
|
118
|
+
const base = opts.registry ?? readConfig(process.cwd()).registry;
|
|
119
|
+
const index = await fetchIndex(base);
|
|
120
|
+
const byCategory = new Map();
|
|
121
|
+
for (const item of index.items) {
|
|
122
|
+
const key = item.category ?? 'other';
|
|
123
|
+
const group = byCategory.get(key) ?? [];
|
|
124
|
+
group.push(item);
|
|
125
|
+
byCategory.set(key, group);
|
|
126
|
+
}
|
|
127
|
+
for (const [category, items] of [...byCategory].sort()) {
|
|
128
|
+
console.log(`\n${bold(category)}`);
|
|
129
|
+
for (const item of items) {
|
|
130
|
+
console.log(` ${cyan(item.name.padEnd(12))} ${dim(item.description ?? '')}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
main().catch((err) => {
|
|
135
|
+
console.error(red(`✖ ${err instanceof Error ? err.message : String(err)}`));
|
|
136
|
+
process.exit(1);
|
|
137
|
+
});
|
package/dist/lib.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Core logic for the Runek CLI, kept free of argument parsing so it can be unit
|
|
2
|
+
// tested and reused. The registry is "just data": an index plus one manifest per
|
|
3
|
+
// item, served over HTTP or read from a local directory.
|
|
4
|
+
import { execFileSync } from 'node:child_process';
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
7
|
+
export const CONFIG_FILE = 'runek.config.json';
|
|
8
|
+
export const DEFAULT_CONFIG = {
|
|
9
|
+
registry: 'https://runek.nullorder.org/r',
|
|
10
|
+
dir: 'src/runek',
|
|
11
|
+
};
|
|
12
|
+
// --- config ----------------------------------------------------------------
|
|
13
|
+
export function configExists(cwd) {
|
|
14
|
+
return existsSync(join(cwd, CONFIG_FILE));
|
|
15
|
+
}
|
|
16
|
+
export function readConfig(cwd) {
|
|
17
|
+
const path = join(cwd, CONFIG_FILE);
|
|
18
|
+
if (!existsSync(path))
|
|
19
|
+
return { ...DEFAULT_CONFIG };
|
|
20
|
+
return { ...DEFAULT_CONFIG, ...JSON.parse(readFileSync(path, 'utf8')) };
|
|
21
|
+
}
|
|
22
|
+
export function writeConfig(cwd, config) {
|
|
23
|
+
const body = { $schema: 'https://runek.nullorder.org/registry/config-schema.json', ...config };
|
|
24
|
+
writeFileSync(join(cwd, CONFIG_FILE), `${JSON.stringify(body, null, 2)}\n`);
|
|
25
|
+
}
|
|
26
|
+
// --- registry client -------------------------------------------------------
|
|
27
|
+
function isHttp(base) {
|
|
28
|
+
return /^https?:\/\//.test(base);
|
|
29
|
+
}
|
|
30
|
+
async function readJson(base, ...segments) {
|
|
31
|
+
if (isHttp(base)) {
|
|
32
|
+
const url = [base.replace(/\/+$/, ''), ...segments].join('/');
|
|
33
|
+
const res = await fetch(url);
|
|
34
|
+
if (!res.ok)
|
|
35
|
+
throw new Error(`registry request failed (${res.status}) for ${url}`);
|
|
36
|
+
return (await res.json());
|
|
37
|
+
}
|
|
38
|
+
const path = join(isAbsolute(base) ? base : resolve(base), ...segments);
|
|
39
|
+
if (!existsSync(path))
|
|
40
|
+
throw new Error(`registry file not found: ${path}`);
|
|
41
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
42
|
+
}
|
|
43
|
+
export function fetchIndex(base) {
|
|
44
|
+
return readJson(base, 'registry.json');
|
|
45
|
+
}
|
|
46
|
+
export function fetchManifest(base, name) {
|
|
47
|
+
return readJson(base, 'components', `${name}.json`);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Resolve the given item names plus their registry dependencies, returning
|
|
51
|
+
* manifests with every dependency ordered before the item that needs it.
|
|
52
|
+
*/
|
|
53
|
+
export async function resolveItems(base, names) {
|
|
54
|
+
const seen = new Set();
|
|
55
|
+
const ordered = [];
|
|
56
|
+
async function visit(name) {
|
|
57
|
+
if (seen.has(name))
|
|
58
|
+
return;
|
|
59
|
+
seen.add(name);
|
|
60
|
+
const manifest = await fetchManifest(base, name);
|
|
61
|
+
for (const dep of manifest.registryDependencies ?? [])
|
|
62
|
+
await visit(dep);
|
|
63
|
+
ordered.push(manifest);
|
|
64
|
+
}
|
|
65
|
+
for (const name of names)
|
|
66
|
+
await visit(name);
|
|
67
|
+
return ordered;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Write component source verbatim. Components import `@runek/core` from npm
|
|
71
|
+
* (installed as a dependency), so there's no import to rewrite.
|
|
72
|
+
*/
|
|
73
|
+
export function writeFiles(cwd, dir, files, overwrite) {
|
|
74
|
+
const result = { written: [], skipped: [] };
|
|
75
|
+
for (const file of files) {
|
|
76
|
+
const dest = join(cwd, dir, file.path);
|
|
77
|
+
if (existsSync(dest) && !overwrite) {
|
|
78
|
+
result.skipped.push(file.path);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
82
|
+
writeFileSync(dest, file.content);
|
|
83
|
+
result.written.push(file.path);
|
|
84
|
+
}
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
/** Union of npm dependencies across resolved manifests, sorted and deduped. */
|
|
88
|
+
export function collectDependencies(manifests) {
|
|
89
|
+
return [...new Set(manifests.flatMap((m) => m.dependencies ?? []))].sort();
|
|
90
|
+
}
|
|
91
|
+
export function detectPackageManager(cwd) {
|
|
92
|
+
if (existsSync(join(cwd, 'pnpm-lock.yaml')))
|
|
93
|
+
return 'pnpm';
|
|
94
|
+
if (existsSync(join(cwd, 'yarn.lock')))
|
|
95
|
+
return 'yarn';
|
|
96
|
+
if (existsSync(join(cwd, 'bun.lockb')) || existsSync(join(cwd, 'bun.lock')))
|
|
97
|
+
return 'bun';
|
|
98
|
+
return 'npm';
|
|
99
|
+
}
|
|
100
|
+
export function installCommand(pm, deps) {
|
|
101
|
+
const verb = pm === 'npm' ? 'install' : 'add';
|
|
102
|
+
return `${pm} ${verb} ${deps.join(' ')}`;
|
|
103
|
+
}
|
|
104
|
+
export function installDependencies(cwd, pm, deps) {
|
|
105
|
+
const verb = pm === 'npm' ? 'install' : 'add';
|
|
106
|
+
execFileSync(pm, [verb, ...deps], { cwd, stdio: 'inherit' });
|
|
107
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@runek/cli",
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"description": "Runek CLI — pull procedural 3D component source into your project (source registry).",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"type": "module",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/nullorder/runek.git",
|
|
13
|
+
"directory": "packages/cli"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://runek.nullorder.org",
|
|
16
|
+
"bugs": "https://github.com/nullorder/runek/issues",
|
|
17
|
+
"keywords": [
|
|
18
|
+
"react-three-fiber",
|
|
19
|
+
"threejs",
|
|
20
|
+
"3d",
|
|
21
|
+
"procedural-generation",
|
|
22
|
+
"components",
|
|
23
|
+
"registry",
|
|
24
|
+
"cli"
|
|
25
|
+
],
|
|
26
|
+
"bin": {
|
|
27
|
+
"runek": "./dist/index.js"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist"
|
|
31
|
+
],
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=20"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^24.0.0",
|
|
37
|
+
"typescript": "^6.0.3",
|
|
38
|
+
"vitest": "^4.1.8"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsc -p tsconfig.build.json",
|
|
42
|
+
"typecheck": "tsc --noEmit",
|
|
43
|
+
"test": "vitest run"
|
|
44
|
+
}
|
|
45
|
+
}
|