create-three-blocks-starter 0.0.1
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/README.md +43 -0
- package/bin/index.js +150 -0
- package/package.json +28 -0
package/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# @three-blocks/create-starter
|
|
2
|
+
|
|
3
|
+
Private CLI to scaffold a new project from the Three Blocks starter.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
Authenticate to the private registry first (one time per project):
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx -y three-blocks-login --mode project --scope @three-blocks
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Then scaffold a new app:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm create @three-blocks/starter@latest my-app
|
|
17
|
+
# or
|
|
18
|
+
pnpm dlx @three-blocks/create-starter my-app
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Next steps:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
cd my-app
|
|
25
|
+
pnpm i
|
|
26
|
+
pnpm dev
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Development (monorepo)
|
|
30
|
+
|
|
31
|
+
- The CLI pulls the template from `packages/three-blocks-starter/` during `prepack`.
|
|
32
|
+
- To sync locally without publishing:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pnpm --filter @three-blocks/create-starter run dev:sync
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Publish
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npx -y three-blocks-login --mode project --scope @three-blocks
|
|
42
|
+
pnpm --filter @three-blocks/create-starter publish --access restricted
|
|
43
|
+
```
|
package/bin/index.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Node 18+
|
|
3
|
+
// Usage:
|
|
4
|
+
// THREE_BLOCKS_LICENSE_KEY=sk_live_... npx three-blocks-create-app my-app
|
|
5
|
+
// npx three-blocks-create-app my-app (will prompt for key)
|
|
6
|
+
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import fsp from 'node:fs/promises';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import os from 'node:os';
|
|
11
|
+
import { spawnSync } from 'node:child_process';
|
|
12
|
+
import readline from 'node:readline';
|
|
13
|
+
|
|
14
|
+
const STARTER_PKG = '@three-blocks/starter'; // private starter (lives in CodeArtifact)
|
|
15
|
+
const LOGIN_CLI = 'three-blocks-login'; // public login CLI
|
|
16
|
+
const SCOPE = '@three-blocks';
|
|
17
|
+
|
|
18
|
+
const die = (m) => { console.error(m); process.exit(1); };
|
|
19
|
+
const run = (cmd, args, opts = {}) => {
|
|
20
|
+
const r = spawnSync(cmd, args, { stdio: 'inherit', ...opts });
|
|
21
|
+
if (r.status !== 0) process.exit(r.status ?? 1);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
async function ensureEmptyDir(dir) {
|
|
25
|
+
try { await fsp.mkdir(dir, { recursive: true }); } catch {}
|
|
26
|
+
const files = await fsp.readdir(dir).catch(() => []);
|
|
27
|
+
if (files.length) die(`Target directory '${path.basename(dir)}' is not empty.`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function promptHidden(label) {
|
|
31
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
32
|
+
return await new Promise((resolve) => {
|
|
33
|
+
process.stdout.write(label);
|
|
34
|
+
const onData = (char) => {
|
|
35
|
+
char = String(char);
|
|
36
|
+
if (char === '\n' || char === '\r' || char === '\u0004') process.stdout.write('\n');
|
|
37
|
+
else process.stdout.write('*');
|
|
38
|
+
};
|
|
39
|
+
process.stdin.on('data', onData);
|
|
40
|
+
rl.question('', (v) => {
|
|
41
|
+
process.stdin.removeListener('data', onData);
|
|
42
|
+
rl.close();
|
|
43
|
+
resolve(v.trim());
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function mkTmpDir(prefix = 'three-blocks-') {
|
|
49
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function main() {
|
|
53
|
+
const args = process.argv.slice(2);
|
|
54
|
+
let appName = '';
|
|
55
|
+
let channel = String(process.env.THREE_BLOCKS_CHANNEL || 'stable').toLowerCase();
|
|
56
|
+
for (let i = 0; i < args.length; i++) {
|
|
57
|
+
const a = args[i];
|
|
58
|
+
if (!a.startsWith('-') && !appName) { appName = a; continue; }
|
|
59
|
+
if (a === '--channel' || a === '-c') { const v = args[i + 1]; if (v) { channel = String(v).toLowerCase(); i++; } continue; }
|
|
60
|
+
const m = a.match(/^--channel=(.+)$/);
|
|
61
|
+
if (m) { channel = String(m[1]).toLowerCase(); continue; }
|
|
62
|
+
}
|
|
63
|
+
if (!['stable', 'beta', 'alpha'].includes(channel)) channel = 'stable';
|
|
64
|
+
if (!appName) {
|
|
65
|
+
die(
|
|
66
|
+
`Usage:\n` +
|
|
67
|
+
` THREE_BLOCKS_LICENSE_KEY=sk_live_... npx three-blocks-create-app <directory>\n` +
|
|
68
|
+
` (or: npx three-blocks-create-app <directory> and enter the key when prompted)`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const targetDir = path.resolve(process.cwd(), appName);
|
|
73
|
+
await ensureEmptyDir(targetDir);
|
|
74
|
+
|
|
75
|
+
// 1) License key (env or prompt)
|
|
76
|
+
let license = process.env.THREE_BLOCKS_LICENSE_KEY;
|
|
77
|
+
if (!license) {
|
|
78
|
+
license = await promptHidden('Enter your THREE_BLOCKS_LICENSE_KEY: ');
|
|
79
|
+
if (!license) die('License key is required to install private packages.');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 2) Pre-login in a TEMP dir to generate a temp .npmrc (no always-auth)
|
|
83
|
+
const tmp = mkTmpDir();
|
|
84
|
+
const tmpNpmrc = path.join(tmp, '.npmrc');
|
|
85
|
+
console.log(`> Fetching short-lived token (temp .npmrc) [channel: ${channel}] ...`);
|
|
86
|
+
run('npx', ['-y', LOGIN_CLI, '--mode', 'project', '--scope', SCOPE, '--channel', channel], {
|
|
87
|
+
cwd: tmp,
|
|
88
|
+
env: { ...process.env, THREE_BLOCKS_LICENSE_KEY: license, THREE_BLOCKS_CHANNEL: channel },
|
|
89
|
+
});
|
|
90
|
+
if (!fs.existsSync(tmpNpmrc)) die('Login failed: temp .npmrc not created.');
|
|
91
|
+
|
|
92
|
+
// 3) Scaffold the private starter using the temp .npmrc
|
|
93
|
+
const starterSpec = `${STARTER_PKG}@${channel === 'stable' ? 'latest' : channel}`;
|
|
94
|
+
console.log(`> Creating ${appName} from ${starterSpec} ...`);
|
|
95
|
+
const createEnv = {
|
|
96
|
+
...process.env,
|
|
97
|
+
THREE_BLOCKS_LICENSE_KEY: license,
|
|
98
|
+
NPM_CONFIG_USERCONFIG: tmpNpmrc,
|
|
99
|
+
npm_config_userconfig: tmpNpmrc,
|
|
100
|
+
};
|
|
101
|
+
run('npm', ['create', starterSpec, appName], { env: createEnv });
|
|
102
|
+
|
|
103
|
+
// 4) Write .env.local and .gitignore entries
|
|
104
|
+
await fsp.writeFile(path.join(targetDir, '.env.local'),
|
|
105
|
+
`THREE_BLOCKS_LICENSE_KEY=${license}\nTHREE_BLOCKS_CHANNEL=${channel}\n`, { mode: 0o600 }).catch(() => {});
|
|
106
|
+
const giPath = path.join(targetDir, '.gitignore');
|
|
107
|
+
let gi = '';
|
|
108
|
+
try { gi = await fsp.readFile(giPath, 'utf8'); } catch {}
|
|
109
|
+
for (const line of ['.env.local', '.npmrc']) {
|
|
110
|
+
if (!gi.includes(line)) gi += (gi.endsWith('\n') || gi === '' ? '' : '\n') + line + '\n';
|
|
111
|
+
}
|
|
112
|
+
await fsp.writeFile(giPath, gi);
|
|
113
|
+
|
|
114
|
+
// 5) First install — the starter already has:
|
|
115
|
+
// "preinstall": "npx -y three-blocks-login --mode project --scope @three-blocks"
|
|
116
|
+
// so it will mint a fresh token and write a project .npmrc.
|
|
117
|
+
try {
|
|
118
|
+
const pkgPath = path.join(targetDir, 'package.json');
|
|
119
|
+
const pkgRaw = await fsp.readFile(pkgPath, 'utf8');
|
|
120
|
+
const pkg = JSON.parse(pkgRaw);
|
|
121
|
+
pkg.scripts = pkg.scripts || {};
|
|
122
|
+
if (!pkg.scripts.preinstall) {
|
|
123
|
+
pkg.scripts.preinstall = `npx -y ${LOGIN_CLI} --mode project --scope ${SCOPE} --channel ${channel}`;
|
|
124
|
+
}
|
|
125
|
+
await fsp.writeFile(pkgPath, JSON.stringify(pkg, null, 2));
|
|
126
|
+
console.log('> Added preinstall to generated package.json to refresh token on installs.');
|
|
127
|
+
} catch (e) {
|
|
128
|
+
console.log('> Warning: could not add preinstall to package.json:', e?.message || String(e));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
console.log('> Installing dependencies (preinstall will refresh token) ...');
|
|
132
|
+
const hasPnpm = spawnSync('pnpm', ['-v'], { stdio: 'ignore' }).status === 0;
|
|
133
|
+
run(hasPnpm ? 'pnpm' : 'npm', [hasPnpm ? 'install' : 'ci'], {
|
|
134
|
+
cwd: targetDir,
|
|
135
|
+
env: { ...process.env, THREE_BLOCKS_LICENSE_KEY: license, THREE_BLOCKS_CHANNEL: channel },
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// 6) Cleanup temp dir
|
|
139
|
+
try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {}
|
|
140
|
+
|
|
141
|
+
console.log(`\nSuccess! ${appName} is ready.`);
|
|
142
|
+
console.log(`\nNext:`);
|
|
143
|
+
console.log(` cd ${appName}`);
|
|
144
|
+
console.log(` ${hasPnpm ? 'pnpm dev' : 'npm run dev'}`);
|
|
145
|
+
console.log(`\nNotes:`);
|
|
146
|
+
console.log(` • Your license key is stored in .env.local (gitignored).`);
|
|
147
|
+
console.log(` • ${SCOPE} packages are private; each install refreshes a short-lived token via preinstall.`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
main().catch((e) => { console.error(e); process.exit(1); });
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-three-blocks-starter",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Create a new Three Blocks starter app",
|
|
7
|
+
"bin": {
|
|
8
|
+
"create-three-blocks-starter": "bin/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"three",
|
|
19
|
+
"threejs",
|
|
20
|
+
"webgpu",
|
|
21
|
+
"starter",
|
|
22
|
+
"scaffold"
|
|
23
|
+
],
|
|
24
|
+
"license": "UNLICENSED",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"kolorist": "^1.8.0"
|
|
27
|
+
}
|
|
28
|
+
}
|