unoverse 0.1.13 → 0.1.15
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/lib/create.mjs +104 -22
- package/package.json +1 -1
package/lib/create.mjs
CHANGED
|
@@ -96,30 +96,108 @@ function download(dir, stripComponents = 1, filter = "") {
|
|
|
96
96
|
if (r.status !== 0) throw new Error("download failed");
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
const OPTIONS = [
|
|
100
|
+
["Studio", "Components, agents and workflows", "Most people start here"],
|
|
101
|
+
["Universe", "Run the platform yourself, on your own infrastructure"],
|
|
102
|
+
["Client", "A client accelerator that talks to unoverse"],
|
|
103
|
+
];
|
|
104
|
+
const NAME_W = Math.max(...OPTIONS.map(([n]) => n.length)) + 3;
|
|
105
|
+
// Lines the menu occupies, so a redraw knows how far up to go.
|
|
106
|
+
const MENU_LINES = OPTIONS.reduce((n, [, , note]) => n + (note ? 2 : 1), 0);
|
|
107
|
+
|
|
108
|
+
function renderMenu(active) {
|
|
109
|
+
OPTIONS.forEach(([name, desc, note], i) => {
|
|
110
|
+
const on = i === active;
|
|
111
|
+
process.stdout.write(
|
|
112
|
+
`\x1b[2K ${on ? cyan("❯") : " "} ${i + 1} ${on ? bold(name) : name}` +
|
|
113
|
+
`${" ".repeat(NAME_W - name.length)}${on ? desc : dim(desc)}\n`,
|
|
114
|
+
);
|
|
115
|
+
if (note) process.stdout.write(`\x1b[2K ${" ".repeat(NAME_W + 1)}${dim(note)}\n`);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Pick one, with the arrow keys.
|
|
121
|
+
*
|
|
122
|
+
* NO DEPENDENCY. Raw stdin is all a list picker needs, and `packages/cli` is deliberately
|
|
123
|
+
* zero-dependency (it is the first thing anyone installs). Up/down move, a digit jumps,
|
|
124
|
+
* Enter takes it, Ctrl-C leaves without doing anything.
|
|
125
|
+
*
|
|
126
|
+
* NOT A TTY means something is piping input, so fall back to reading a line. A picker
|
|
127
|
+
* that only works interactively would break every scripted install.
|
|
128
|
+
*/
|
|
129
|
+
function select() {
|
|
130
|
+
if (!process.stdin.isTTY) {
|
|
131
|
+
renderMenu(0);
|
|
132
|
+
process.stdout.write("\n Choose 1, 2 or 3 (Enter for 1): ");
|
|
133
|
+
return new Promise((resolve) => {
|
|
134
|
+
let buf = "";
|
|
135
|
+
process.stdin.on("data", (d) => {
|
|
136
|
+
buf += d;
|
|
137
|
+
if (buf.includes("\n")) {
|
|
138
|
+
const n = parseInt(buf.trim(), 10);
|
|
139
|
+
resolve(Number.isInteger(n) && n >= 1 && n <= OPTIONS.length ? n - 1 : 0);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
process.stdin.on("end", () => resolve(0));
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return new Promise((resolve) => {
|
|
147
|
+
let active = 0;
|
|
148
|
+
process.stdout.write("\x1b[?25l"); // hide cursor while the list is live
|
|
149
|
+
renderMenu(active);
|
|
150
|
+
process.stdout.write(`\n ${dim("↑↓ to move, Enter to choose")}`);
|
|
151
|
+
|
|
152
|
+
const redraw = () => {
|
|
153
|
+
process.stdout.write(`\x1b[${MENU_LINES + 1}A\r`);
|
|
154
|
+
renderMenu(active);
|
|
155
|
+
process.stdout.write(`\n ${dim("↑↓ to move, Enter to choose")}`);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
process.stdin.setRawMode(true);
|
|
159
|
+
process.stdin.resume();
|
|
160
|
+
// PARSE the chunk, do not compare it. Keystrokes batch: hold a key down, or paste,
|
|
161
|
+
// and "\x1b[B\x1b[B\r" arrives as ONE data event. Comparing the whole chunk to a
|
|
162
|
+
// single key silently ignores every one of them.
|
|
163
|
+
const onKey = (buf) => {
|
|
164
|
+
const k = buf.toString();
|
|
165
|
+
let moved = false;
|
|
166
|
+
for (let i = 0; i < k.length; i++) {
|
|
167
|
+
if (k[i] === "\u0003") { done(); process.stdout.write("\n"); process.exit(130); }
|
|
168
|
+
if (k[i] === "\x1b" && k[i + 1] === "[" && (k[i + 2] === "A" || k[i + 2] === "B")) {
|
|
169
|
+
const step = k[i + 2] === "A" ? -1 : 1;
|
|
170
|
+
active = (active + step + OPTIONS.length) % OPTIONS.length;
|
|
171
|
+
moved = true;
|
|
172
|
+
i += 2;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (k[i] === "\r" || k[i] === "\n") {
|
|
176
|
+
if (moved) redraw();
|
|
177
|
+
done();
|
|
178
|
+
process.stdout.write("\n\n");
|
|
179
|
+
return resolve(active);
|
|
180
|
+
}
|
|
181
|
+
if (/[1-9]/.test(k[i]) && +k[i] <= OPTIONS.length) { active = +k[i] - 1; moved = true; }
|
|
182
|
+
}
|
|
183
|
+
if (moved) redraw();
|
|
184
|
+
};
|
|
185
|
+
const done = () => {
|
|
186
|
+
process.stdin.setRawMode(false);
|
|
187
|
+
process.stdin.pause();
|
|
188
|
+
process.stdin.off("data", onKey);
|
|
189
|
+
process.stdout.write("\x1b[?25h"); // cursor back
|
|
190
|
+
};
|
|
191
|
+
process.stdin.on("data", onKey);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
99
195
|
export async function create(nameArg) {
|
|
196
|
+
console.log(`\n ${cyan("⬡ What are you building?")}\n`);
|
|
197
|
+
const choice = String((await select()) + 1);
|
|
198
|
+
|
|
100
199
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
101
200
|
try {
|
|
102
|
-
// THE SELECTION IS SHOWN IN THE LIST, not as "[1]" in the prompt. A bracketed
|
|
103
|
-
// default is a shell convention, not a selection: it puts the answer somewhere
|
|
104
|
-
// other than where the reader is looking. The marked row carries it instead.
|
|
105
|
-
const OPTIONS = [
|
|
106
|
-
["Studio", "Components, agents and workflows", "Most people start here"],
|
|
107
|
-
["Universe", "Run the platform yourself, on your own infrastructure"],
|
|
108
|
-
["Client", "A client accelerator that talks to unoverse"],
|
|
109
|
-
];
|
|
110
|
-
const NAME_W = Math.max(...OPTIONS.map(([n]) => n.length)) + 3;
|
|
111
|
-
|
|
112
|
-
console.log(`\n ${cyan("⬡ What are you building?")}\n`);
|
|
113
|
-
OPTIONS.forEach(([name, desc, note], i) => {
|
|
114
|
-
const selected = i === 0;
|
|
115
|
-
const mark = selected ? cyan("❯") : " ";
|
|
116
|
-
const label = selected ? bold(name) : name;
|
|
117
|
-
console.log(` ${mark} ${i + 1} ${label}${" ".repeat(NAME_W - name.length)}${desc}`);
|
|
118
|
-
if (note) console.log(` ${" ".repeat(NAME_W + 1)}${dim(note)}`);
|
|
119
|
-
});
|
|
120
|
-
console.log("");
|
|
121
|
-
|
|
122
|
-
const choice = (await ask(rl, `Enter to choose, or type 2 or 3: `)) || "1";
|
|
123
201
|
|
|
124
202
|
if (choice === "1") {
|
|
125
203
|
console.log(`\n ${dim("Launching Unoverse Studio. It creates and manages your projects.")}\n`);
|
|
@@ -129,6 +207,11 @@ export async function create(nameArg) {
|
|
|
129
207
|
}
|
|
130
208
|
|
|
131
209
|
if (choice === "2") {
|
|
210
|
+
// TARGET FIRST, credential second. This used to ask for the registry token, make a
|
|
211
|
+
// network round-trip to validate it, and only then discover the folder was not
|
|
212
|
+
// empty. Never ask for a credential you are about to throw away.
|
|
213
|
+
const name = resolveTarget(nameArg);
|
|
214
|
+
|
|
132
215
|
console.log(`\n ${cyan("A universe needs a registry access token.")}`);
|
|
133
216
|
console.log(` ${dim("It authorizes the platform's images. Your Unoverse admin issues it.")}\n`);
|
|
134
217
|
const token = await ask(rl, "Registry access token: ");
|
|
@@ -146,7 +229,6 @@ export async function create(nameArg) {
|
|
|
146
229
|
}
|
|
147
230
|
ok("token accepted");
|
|
148
231
|
|
|
149
|
-
const name = resolveTarget(nameArg);
|
|
150
232
|
download(name);
|
|
151
233
|
ok(`universe scaffolded in ${label(name)}`);
|
|
152
234
|
|
package/package.json
CHANGED