alexandr 0.1.1 → 0.1.2
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/bin.js +15 -2
- package/package.json +2 -2
- package/src/commands.js +6 -0
- package/src/link.js +18 -2
- package/templates/env.example +2 -0
package/bin.js
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Public entrypoint for the `alexandr` CLI. Keep this a thin shim; logic lives
|
|
3
|
-
// in ./src. Plain Node (no Bun, no deps) so it runs anywhere Node
|
|
4
|
-
|
|
3
|
+
// in ./src. Plain Node (no Bun, no deps) so it runs anywhere Node 18+ is.
|
|
4
|
+
|
|
5
|
+
// Our own version guard, so nobody meets npm's EBADENGINE wall: package.json
|
|
6
|
+
// declares >=18 (what Ubuntu LTS's apt ships, and provably sufficient — the CLI
|
|
7
|
+
// is dependency-free), and anything older gets ONE styled line instead of a trace.
|
|
8
|
+
const major = Number(process.versions.node.split(".")[0]);
|
|
9
|
+
if (major < 18) {
|
|
10
|
+
process.stderr.write(
|
|
11
|
+
`\x1b[31m✗\x1b[0m alexandr needs Node 18 or newer — this is Node ${process.versions.node}.\n` +
|
|
12
|
+
` On Ubuntu/Debian: apt install -y npm (or use nodesource for a current LTS)\n`,
|
|
13
|
+
);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const { run } = await import("./src/cli.js");
|
|
5
18
|
|
|
6
19
|
run().catch((err) => {
|
|
7
20
|
process.stderr.write(`${err?.stack || err}\n`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "alexandr",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Run the alexandr workspace runtime locally — a thin Docker front door (npx alexandr up). Pulls + boots the published kernel image.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"LICENSE"
|
|
18
18
|
],
|
|
19
19
|
"engines": {
|
|
20
|
-
"node": ">=
|
|
20
|
+
"node": ">=18"
|
|
21
21
|
},
|
|
22
22
|
"license": "Apache-2.0",
|
|
23
23
|
"repository": {
|
package/src/commands.js
CHANGED
|
@@ -173,6 +173,12 @@ export async function up(flags) {
|
|
|
173
173
|
|
|
174
174
|
if (flags.port) setEnv(inst.dir, "ALEXANDR_KERNEL_PORT", String(flags.port));
|
|
175
175
|
if (flags.domain) setEnv(inst.dir, "ALEXANDR_DOMAIN", String(flags.domain));
|
|
176
|
+
// Persist the display name so a RETRIED ceremony reuses the wizard's answer — the
|
|
177
|
+
// first attempt on the aos box lost "AOS" to the default because it lived only in
|
|
178
|
+
// that run's memory (the ceremony it fed timed out).
|
|
179
|
+
if (typeof flags.name === "string" && flags.name.trim()) {
|
|
180
|
+
setEnv(inst.dir, "ALEXANDR_WORKSPACE_NAME", flags.name.trim());
|
|
181
|
+
}
|
|
176
182
|
|
|
177
183
|
// Sign-in comes FIRST (account-required-runtimes D3) — now literally: the entitlement
|
|
178
184
|
// gate fires before anything system-mutating, so a refused account walks away from a
|
package/src/link.js
CHANGED
|
@@ -101,7 +101,9 @@ export async function runLinkCeremony(inst, flags) {
|
|
|
101
101
|
? `https://${String(domain).replace(/^https?:\/\//, "").replace(/\/+$/, "")}`
|
|
102
102
|
: kernelUrl(kernelPort(inst.dir));
|
|
103
103
|
const name =
|
|
104
|
-
typeof flags.name === "string" && flags.name.trim()
|
|
104
|
+
(typeof flags.name === "string" && flags.name.trim()) ||
|
|
105
|
+
(readEnv(inst.dir).ALEXANDR_WORKSPACE_NAME || "").trim() ||
|
|
106
|
+
"My self-hosted workspace";
|
|
105
107
|
|
|
106
108
|
// PKCE (S256) + a CSRF state for the loopback redirect.
|
|
107
109
|
const verifier = b64url(crypto.randomBytes(32));
|
|
@@ -309,7 +311,7 @@ export async function presentAuthUrl(authUrl, { port, domain, headless = isHeadl
|
|
|
309
311
|
}
|
|
310
312
|
|
|
311
313
|
/** Start a loopback listener for the OAuth redirect. Resolves {code,state} when /callback is hit. */
|
|
312
|
-
function startLoopback() {
|
|
314
|
+
export function startLoopback() {
|
|
313
315
|
let resolveFn, rejectFn;
|
|
314
316
|
const done = new Promise((res, rej) => {
|
|
315
317
|
resolveFn = res;
|
|
@@ -317,6 +319,20 @@ function startLoopback() {
|
|
|
317
319
|
});
|
|
318
320
|
const server = http.createServer((req, res) => {
|
|
319
321
|
const u = new URL(req.url, "http://127.0.0.1");
|
|
322
|
+
// Reachability probe for the consent page: /cli-auth pings this before the user
|
|
323
|
+
// clicks Link, so a missing ssh tunnel becomes a guided "start the tunnel" notice
|
|
324
|
+
// instead of a dead browser error page after the click. The PNA header answers
|
|
325
|
+
// Chrome's public→loopback preflight; ACAO lets the page read the success.
|
|
326
|
+
if (u.pathname === "/ping") {
|
|
327
|
+
res.writeHead(204, {
|
|
328
|
+
"access-control-allow-origin": "*",
|
|
329
|
+
"access-control-allow-methods": "GET, OPTIONS",
|
|
330
|
+
"access-control-allow-headers": "*",
|
|
331
|
+
"access-control-allow-private-network": "true",
|
|
332
|
+
});
|
|
333
|
+
res.end();
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
320
336
|
if (u.pathname !== "/callback") {
|
|
321
337
|
res.writeHead(404);
|
|
322
338
|
res.end();
|
package/templates/env.example
CHANGED
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
# ALEXANDR_INSTANCE_ID=
|
|
22
22
|
# ALEXANDR_RUNTIME_SECRET=
|
|
23
23
|
# ALEXANDR_WORKSPACE_ID= # account-side record id (used by `destroy --unlink`)
|
|
24
|
+
# ALEXANDR_WORKSPACE_NAME= # display name for registration (the wizard writes it,
|
|
25
|
+
# # so a retried sign-in reuses your answer)
|
|
24
26
|
|
|
25
27
|
# Public domain for Caddy auto-HTTPS. Setting this starts the "public" profile — the
|
|
26
28
|
# supported way to reach the runtime from another machine. Without a domain the
|