create-three-blocks-starter 0.0.1 → 0.0.3
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 +4 -4
- package/bin/index.js +96 -27
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# create-three-blocks-starter
|
|
2
2
|
|
|
3
3
|
Private CLI to scaffold a new project from the Three Blocks starter.
|
|
4
4
|
|
|
@@ -15,7 +15,7 @@ Then scaffold a new app:
|
|
|
15
15
|
```bash
|
|
16
16
|
npm create @three-blocks/starter@latest my-app
|
|
17
17
|
# or
|
|
18
|
-
pnpm dlx
|
|
18
|
+
pnpm dlx create-three-blocks-starter my-app
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
Next steps:
|
|
@@ -32,12 +32,12 @@ pnpm dev
|
|
|
32
32
|
- To sync locally without publishing:
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
|
-
pnpm --filter
|
|
35
|
+
pnpm --filter create-three-blocks-starter run dev:sync
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
## Publish
|
|
39
39
|
|
|
40
40
|
```bash
|
|
41
41
|
npx -y three-blocks-login --mode project --scope @three-blocks
|
|
42
|
-
pnpm --filter
|
|
42
|
+
pnpm --filter create-three-blocks-starter publish --access restricted
|
|
43
43
|
```
|
package/bin/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Node 18+
|
|
3
3
|
// Usage:
|
|
4
|
-
//
|
|
5
|
-
// npx three-blocks-
|
|
4
|
+
// THREE_BLOCKS_SECRET_KEY=sk_live_... npx create-three-blocks-starter my-app
|
|
5
|
+
// npx create-three-blocks-starter my-app (will prompt for key)
|
|
6
6
|
|
|
7
7
|
import fs from 'node:fs';
|
|
8
8
|
import fsp from 'node:fs/promises';
|
|
@@ -10,6 +10,7 @@ import path from 'node:path';
|
|
|
10
10
|
import os from 'node:os';
|
|
11
11
|
import { spawnSync } from 'node:child_process';
|
|
12
12
|
import readline from 'node:readline';
|
|
13
|
+
import { bold, cyan, dim, gray, green, yellow } from 'kolorist';
|
|
13
14
|
|
|
14
15
|
const STARTER_PKG = '@three-blocks/starter'; // private starter (lives in CodeArtifact)
|
|
15
16
|
const LOGIN_CLI = 'three-blocks-login'; // public login CLI
|
|
@@ -21,6 +22,13 @@ const run = (cmd, args, opts = {}) => {
|
|
|
21
22
|
if (r.status !== 0) process.exit(r.status ?? 1);
|
|
22
23
|
};
|
|
23
24
|
|
|
25
|
+
// capture stdout (used for npm pack)
|
|
26
|
+
const exec = (cmd, args, opts = {}) => {
|
|
27
|
+
const r = spawnSync(cmd, args, { stdio: ['ignore', 'pipe', 'inherit'], ...opts });
|
|
28
|
+
if (r.status !== 0) process.exit(r.status ?? 1);
|
|
29
|
+
return (r.stdout || Buffer.alloc(0)).toString();
|
|
30
|
+
};
|
|
31
|
+
|
|
24
32
|
async function ensureEmptyDir(dir) {
|
|
25
33
|
try { await fsp.mkdir(dir, { recursive: true }); } catch {}
|
|
26
34
|
const files = await fsp.readdir(dir).catch(() => []);
|
|
@@ -28,20 +36,44 @@ async function ensureEmptyDir(dir) {
|
|
|
28
36
|
}
|
|
29
37
|
|
|
30
38
|
async function promptHidden(label) {
|
|
31
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
32
39
|
return await new Promise((resolve) => {
|
|
33
|
-
process.
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
const stdin = process.stdin;
|
|
41
|
+
const stdout = process.stdout;
|
|
42
|
+
let value = '';
|
|
43
|
+
const cleanup = () => {
|
|
44
|
+
stdin.removeListener('data', onData);
|
|
45
|
+
if (stdin.isTTY) stdin.setRawMode(false);
|
|
46
|
+
stdin.pause();
|
|
38
47
|
};
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
48
|
+
const onData = (chunk) => {
|
|
49
|
+
const str = String(chunk);
|
|
50
|
+
for (const ch of str) {
|
|
51
|
+
if (ch === '\u0003') { cleanup(); process.exit(1); }
|
|
52
|
+
if (ch === '\r' || ch === '\n' || ch === '\u0004') {
|
|
53
|
+
stdout.write('\n');
|
|
54
|
+
cleanup();
|
|
55
|
+
return resolve(value.trim());
|
|
56
|
+
}
|
|
57
|
+
if (ch === '\u0008' || ch === '\u007f') {
|
|
58
|
+
if (value.length) {
|
|
59
|
+
value = value.slice(0, -1);
|
|
60
|
+
readline.moveCursor(stdout, -1, 0);
|
|
61
|
+
stdout.write(' ');
|
|
62
|
+
readline.moveCursor(stdout, -1, 0);
|
|
63
|
+
}
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
// Ignore ESC start of control sequences
|
|
67
|
+
if (ch === '\u001b') continue;
|
|
68
|
+
value += ch;
|
|
69
|
+
stdout.write('•');
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
stdout.write(label);
|
|
73
|
+
stdin.setEncoding('utf8');
|
|
74
|
+
if (stdin.isTTY) stdin.setRawMode(true);
|
|
75
|
+
stdin.resume();
|
|
76
|
+
stdin.on('data', onData);
|
|
45
77
|
});
|
|
46
78
|
}
|
|
47
79
|
|
|
@@ -49,6 +81,13 @@ function mkTmpDir(prefix = 'three-blocks-') {
|
|
|
49
81
|
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
50
82
|
}
|
|
51
83
|
|
|
84
|
+
function mask(s) {
|
|
85
|
+
if (!s) return '';
|
|
86
|
+
const v = String(s);
|
|
87
|
+
if (v.length <= 6) return '••••';
|
|
88
|
+
return v.slice(0, 3) + '••••' + v.slice(-4);
|
|
89
|
+
}
|
|
90
|
+
|
|
52
91
|
async function main() {
|
|
53
92
|
const args = process.argv.slice(2);
|
|
54
93
|
let appName = '';
|
|
@@ -64,7 +103,7 @@ async function main() {
|
|
|
64
103
|
if (!appName) {
|
|
65
104
|
die(
|
|
66
105
|
`Usage:\n` +
|
|
67
|
-
`
|
|
106
|
+
` THREE_BLOCKS_SECRET_KEY=sk_live_... npx three-blocks-create-app <directory>\n` +
|
|
68
107
|
` (or: npx three-blocks-create-app <directory> and enter the key when prompted)`
|
|
69
108
|
);
|
|
70
109
|
}
|
|
@@ -73,9 +112,15 @@ async function main() {
|
|
|
73
112
|
await ensureEmptyDir(targetDir);
|
|
74
113
|
|
|
75
114
|
// 1) License key (env or prompt)
|
|
76
|
-
let license = process.env.
|
|
77
|
-
if (
|
|
78
|
-
|
|
115
|
+
let license = process.env.THREE_BLOCKS_SECRET_KEY;
|
|
116
|
+
if (license) {
|
|
117
|
+
console.log(`${gray('›')} Using ${bold('THREE_BLOCKS_SECRET_KEY')} from environment ${dim(`(${mask(license)})`)}`);
|
|
118
|
+
} else {
|
|
119
|
+
console.log('');
|
|
120
|
+
console.log(bold(cyan('Three Blocks Starter')) + ' ' + dim(`[channel: ${channel}]`));
|
|
121
|
+
console.log(dim('Enter your license key to continue.'));
|
|
122
|
+
console.log(dim('Tip: paste it here; input is hidden. Press Enter to submit.'));
|
|
123
|
+
license = await promptHidden(`${gray('›')} ${bold('License key')} ${dim('(tb_…)')}: `);
|
|
79
124
|
if (!license) die('License key is required to install private packages.');
|
|
80
125
|
}
|
|
81
126
|
|
|
@@ -85,24 +130,44 @@ async function main() {
|
|
|
85
130
|
console.log(`> Fetching short-lived token (temp .npmrc) [channel: ${channel}] ...`);
|
|
86
131
|
run('npx', ['-y', LOGIN_CLI, '--mode', 'project', '--scope', SCOPE, '--channel', channel], {
|
|
87
132
|
cwd: tmp,
|
|
88
|
-
env: { ...process.env,
|
|
133
|
+
env: { ...process.env, THREE_BLOCKS_SECRET_KEY: license, THREE_BLOCKS_CHANNEL: channel },
|
|
89
134
|
});
|
|
90
135
|
if (!fs.existsSync(tmpNpmrc)) die('Login failed: temp .npmrc not created.');
|
|
136
|
+
// Extract registry URL from temp .npmrc so we can pass it explicitly to npm create
|
|
137
|
+
let registryUrl = '';
|
|
138
|
+
try {
|
|
139
|
+
const txt = fs.readFileSync(tmpNpmrc, 'utf8');
|
|
140
|
+
const m = txt.match(/^@[^:]+:registry=(.+)$/m);
|
|
141
|
+
if (m && m[1]) registryUrl = m[1].trim();
|
|
142
|
+
} catch {}
|
|
91
143
|
|
|
92
|
-
// 3) Scaffold the private starter
|
|
144
|
+
// 3) Scaffold the private starter by packing and extracting the tarball (avoids npm create naming transform)
|
|
93
145
|
const starterSpec = `${STARTER_PKG}@${channel === 'stable' ? 'latest' : channel}`;
|
|
94
|
-
console.log(`>
|
|
146
|
+
console.log(`> Fetching starter tarball ${starterSpec} ...`);
|
|
95
147
|
const createEnv = {
|
|
96
148
|
...process.env,
|
|
97
|
-
|
|
149
|
+
THREE_BLOCKS_SECRET_KEY: license,
|
|
98
150
|
NPM_CONFIG_USERCONFIG: tmpNpmrc,
|
|
99
151
|
npm_config_userconfig: tmpNpmrc,
|
|
100
152
|
};
|
|
101
|
-
|
|
153
|
+
let packedOut = exec('npm', ['pack', starterSpec, '--json', '--silent'], { cwd: tmp, env: createEnv });
|
|
154
|
+
let tarName = '';
|
|
155
|
+
try {
|
|
156
|
+
const info = JSON.parse(packedOut);
|
|
157
|
+
if (Array.isArray(info)) tarName = info[0]?.filename || '';
|
|
158
|
+
else tarName = info.filename || '';
|
|
159
|
+
} catch {
|
|
160
|
+
const lines = String(packedOut || '').trim().split(/\r?\n/);
|
|
161
|
+
tarName = lines[lines.length - 1] || '';
|
|
162
|
+
}
|
|
163
|
+
const tarPath = path.join(tmp, tarName);
|
|
164
|
+
if (!tarName || !fs.existsSync(tarPath)) die('Failed to fetch starter tarball.');
|
|
165
|
+
console.log('> Extracting files ...');
|
|
166
|
+
run('tar', ['-xzf', tarPath, '-C', targetDir, '--strip-components=1']);
|
|
102
167
|
|
|
103
168
|
// 4) Write .env.local and .gitignore entries
|
|
104
169
|
await fsp.writeFile(path.join(targetDir, '.env.local'),
|
|
105
|
-
`
|
|
170
|
+
`THREE_BLOCKS_SECRET_KEY=${license}\nTHREE_BLOCKS_CHANNEL=${channel}\n`, { mode: 0o600 }).catch(() => {});
|
|
106
171
|
const giPath = path.join(targetDir, '.gitignore');
|
|
107
172
|
let gi = '';
|
|
108
173
|
try { gi = await fsp.readFile(giPath, 'utf8'); } catch {}
|
|
@@ -123,16 +188,20 @@ async function main() {
|
|
|
123
188
|
pkg.scripts.preinstall = `npx -y ${LOGIN_CLI} --mode project --scope ${SCOPE} --channel ${channel}`;
|
|
124
189
|
}
|
|
125
190
|
await fsp.writeFile(pkgPath, JSON.stringify(pkg, null, 2));
|
|
126
|
-
console.log('
|
|
191
|
+
console.log(`${green('✔')} Added preinstall to generated package.json to refresh token on installs.`);
|
|
127
192
|
} catch (e) {
|
|
128
|
-
console.log('
|
|
193
|
+
console.log(`${yellow('⚠')} Warning: could not add preinstall to package.json:`, e?.message || String(e));
|
|
129
194
|
}
|
|
130
195
|
|
|
131
196
|
console.log('> Installing dependencies (preinstall will refresh token) ...');
|
|
132
197
|
const hasPnpm = spawnSync('pnpm', ['-v'], { stdio: 'ignore' }).status === 0;
|
|
133
198
|
run(hasPnpm ? 'pnpm' : 'npm', [hasPnpm ? 'install' : 'ci'], {
|
|
134
199
|
cwd: targetDir,
|
|
135
|
-
env: {
|
|
200
|
+
env: {
|
|
201
|
+
...process.env,
|
|
202
|
+
THREE_BLOCKS_SECRET_KEY: license,
|
|
203
|
+
THREE_BLOCKS_CHANNEL: channel,
|
|
204
|
+
},
|
|
136
205
|
});
|
|
137
206
|
|
|
138
207
|
// 6) Cleanup temp dir
|