novence 0.1.0 → 0.1.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 +39 -14
- package/dist/commands/claim.d.ts +2 -0
- package/dist/commands/claim.js +16 -0
- package/dist/commands/deploy.js +39 -20
- package/dist/commands/login.js +16 -9
- package/dist/commands/whoami.js +10 -1
- package/dist/index.js +6 -1
- package/dist/lib/auth.d.ts +9 -5
- package/dist/lib/auth.js +45 -21
- package/dist/lib/ui.d.ts +11 -0
- package/dist/lib/ui.js +91 -0
- package/package.json +15 -11
package/README.md
CHANGED
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
# novence
|
|
2
2
|
|
|
3
|
-
Deploy a static site to the edge in one command.
|
|
3
|
+
Deploy a static site to the edge in one command. Prints the live URL. No email
|
|
4
|
+
until you claim.
|
|
4
5
|
|
|
5
6
|
```bash
|
|
6
7
|
npx novence deploy ./dist
|
|
7
8
|
```
|
|
8
9
|
|
|
10
|
+
```text
|
|
11
|
+
Created project site (abcd1234)
|
|
12
|
+
Uploaded 12 files
|
|
13
|
+
https://abcd1234.novence.ai/
|
|
14
|
+
```
|
|
15
|
+
|
|
9
16
|
Uploads the directory, runs quality checks (Lighthouse, axe, links), publishes to
|
|
10
17
|
`https://{suffix}.novence.ai/`, and prints the live URL.
|
|
11
18
|
|
|
12
19
|
Part of [Novence](https://novence.ai) — static site hosting built for AI agents.
|
|
20
|
+
Same loop as the homepage kit: Copy Prompt · email OTP · or `npx novence deploy`.
|
|
13
21
|
|
|
14
22
|
## Install
|
|
15
23
|
|
|
@@ -28,8 +36,18 @@ novence deploy # auto-detects dist, build, out, public, _site
|
|
|
28
36
|
novence deploy ./site # or point at a directory
|
|
29
37
|
```
|
|
30
38
|
|
|
31
|
-
First run
|
|
32
|
-
`~/.novence/credentials` (mode 0600)
|
|
39
|
+
First run with no stored key publishes anonymously (no email), stores
|
|
40
|
+
`~/.novence/credentials` (mode 0600), and prints the live URL. Claim afterwards:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
novence claim --email you@example.com
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Or pass `--email` on first deploy to bootstrap instead of the demo path:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
novence deploy ./dist --email you@example.com
|
|
50
|
+
```
|
|
33
51
|
|
|
34
52
|
The live URL is the last line on stdout and progress goes to stderr, so it pipes:
|
|
35
53
|
|
|
@@ -43,7 +61,7 @@ URL=$(novence deploy ./dist)
|
|
|
43
61
|
| --- | --- |
|
|
44
62
|
| `--name <name>` | Project name (default `site`) |
|
|
45
63
|
| `--project <id>` | Redeploy an existing project instead of creating one |
|
|
46
|
-
| `--email <email>` |
|
|
64
|
+
| `--email <email>` | Bootstrap with email on first deploy instead of anonymous |
|
|
47
65
|
| `--json` | Machine-readable output; implies `--no-input` |
|
|
48
66
|
| `--no-input` | Never prompt — fail instead. For CI |
|
|
49
67
|
| `--api <url>` | Control plane (default `https://api.novence.ai`) |
|
|
@@ -59,6 +77,7 @@ novence keys list # API keys on the account
|
|
|
59
77
|
novence keys revoke <id> # revoke a key (--force to revoke the one in use)
|
|
60
78
|
novence projects # list projects and their URLs
|
|
61
79
|
novence login # sign in, verify email, or --recover a key
|
|
80
|
+
novence claim # attach an email to an anonymous deploy
|
|
62
81
|
novence whoami # plan, verification, usage against quota
|
|
63
82
|
```
|
|
64
83
|
|
|
@@ -66,7 +85,19 @@ Every command accepts `--json`. Unknown flags are rejected rather than ignored,
|
|
|
66
85
|
so a typo like `--nmae` fails loudly instead of silently deploying under the
|
|
67
86
|
default name.
|
|
68
87
|
|
|
69
|
-
## For agents
|
|
88
|
+
## For agents
|
|
89
|
+
|
|
90
|
+
When a shell is available, prefer the CLI over MCP or raw REST:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
npx novence deploy ./dist
|
|
94
|
+
novence claim --email you@example.com
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Copy Prompt kit (same loop, pasteable): [novence.ai](https://novence.ai).
|
|
98
|
+
Full agent brief: [novence.ai/llms-full.txt](https://novence.ai/llms-full.txt).
|
|
99
|
+
|
|
100
|
+
## For CI
|
|
70
101
|
|
|
71
102
|
`--json` gives a stable envelope and exits non-zero on failure:
|
|
72
103
|
|
|
@@ -82,7 +113,7 @@ default name.
|
|
|
82
113
|
}
|
|
83
114
|
```
|
|
84
115
|
|
|
85
|
-
|
|
116
|
+
Set `NOVENCE_API_KEY` and pass `--no-input`.
|
|
86
117
|
|
|
87
118
|
Key lookup order: `NOVENCE_API_KEY`, then `./.novence-key`, then
|
|
88
119
|
`~/.novence/credentials`. Never commit a key.
|
|
@@ -91,17 +122,11 @@ Key lookup order: `NOVENCE_API_KEY`, then `./.novence-key`, then
|
|
|
91
122
|
|
|
92
123
|
Deploys run Lighthouse, axe, and link checks. On unverified accounts checks are
|
|
93
124
|
skipped — `checks.skipped` is `true` and `checks.passed` is `null`, which is not
|
|
94
|
-
a pass. Verify your email with `novence login` to enable them.
|
|
95
|
-
|
|
96
|
-
## Roadmap
|
|
97
|
-
|
|
98
|
-
Today the first deploy requires an email up front. Once anonymous deploys land in
|
|
99
|
-
the API, `novence deploy` will publish first and let you claim the site
|
|
100
|
-
afterwards — no signup before you see a URL.
|
|
125
|
+
a pass. Verify your email with `novence login` (or claim) to enable them.
|
|
101
126
|
|
|
102
127
|
## Docs
|
|
103
128
|
|
|
104
|
-
[Documentation](https://novence.ai/docs) · [Pricing](https://novence.ai/pricing) · [TypeScript SDK](https://www.npmjs.com/package/@novence/sdk) · [MCP server](https://novence.ai/mcp)
|
|
129
|
+
[CLI reference](https://novence.ai/cli) · [Documentation](https://novence.ai/docs) · [Pricing](https://novence.ai/pricing) · [TypeScript SDK](https://www.npmjs.com/package/@novence/sdk) · [MCP server](https://novence.ai/mcp)
|
|
105
130
|
|
|
106
131
|
## License
|
|
107
132
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { loadApiKey, resolveApiBase } from "../lib/config.js";
|
|
2
|
+
import { claimAndMaybeVerify } from "../lib/auth.js";
|
|
3
|
+
import { flagBool, flagString } from "../lib/args.js";
|
|
4
|
+
import { fail, isJsonMode, prompt, result } from "../lib/ui.js";
|
|
5
|
+
export async function claimCommand(args) {
|
|
6
|
+
const apiBase = resolveApiBase(flagString(args.flags, "api"));
|
|
7
|
+
const apiKey = loadApiKey();
|
|
8
|
+
if (!apiKey)
|
|
9
|
+
fail("No API key. Deploy once (`novence deploy`) or set NOVENCE_API_KEY.");
|
|
10
|
+
const interactive = !isJsonMode() && !flagBool(args.flags, "no-input");
|
|
11
|
+
const email = flagString(args.flags, "email") ?? (interactive ? await prompt(" Email: ") : "");
|
|
12
|
+
if (!email)
|
|
13
|
+
fail("Email required. Use `--email you@example.com`.");
|
|
14
|
+
await claimAndMaybeVerify({ apiKey, apiBase, email, interactive });
|
|
15
|
+
result(`Claimed ${email}. Verify if you have not already.`);
|
|
16
|
+
}
|
package/dist/commands/deploy.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { existsSync, statSync } from "node:fs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { Novence } from "@novence/sdk";
|
|
4
|
-
import { ensureApiKey } from "../lib/auth.js";
|
|
4
|
+
import { claimAndMaybeVerify, ensureApiKey } from "../lib/auth.js";
|
|
5
5
|
import { resolveApiBase } from "../lib/config.js";
|
|
6
6
|
import { flagBool, flagString } from "../lib/args.js";
|
|
7
|
-
import { fail, isJsonMode, json, result, siteUrl, step, warn } from "../lib/ui.js";
|
|
7
|
+
import { fail, isJsonMode, json, prompt, result, siteUrl, spin, step, warn, withSpin } from "../lib/ui.js";
|
|
8
8
|
import { explain } from "../lib/errors.js";
|
|
9
9
|
const POLL_INTERVAL_MS = 2000;
|
|
10
10
|
const POLL_TIMEOUT_MS = 5 * 60 * 1000;
|
|
@@ -26,19 +26,34 @@ function resolveSiteDir(explicit) {
|
|
|
26
26
|
return fail(`No build output found. Looked for ${CANDIDATE_DIRS.join(", ")} in ${process.cwd()}.\n` +
|
|
27
27
|
" Pass one explicitly: novence deploy ./your-dir");
|
|
28
28
|
}
|
|
29
|
-
async function
|
|
29
|
+
async function publish(client, projectId) {
|
|
30
|
+
const spinner = spin("Publishing…");
|
|
30
31
|
const startedAt = Date.now();
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
try {
|
|
33
|
+
await client.deployments.create(projectId);
|
|
34
|
+
let lastStatus = "";
|
|
35
|
+
while (Date.now() - startedAt < POLL_TIMEOUT_MS) {
|
|
36
|
+
const { deployment } = await client.deployments.latest(projectId);
|
|
37
|
+
if (deployment.status !== lastStatus) {
|
|
38
|
+
spinner.update(`${deployment.status}…`);
|
|
39
|
+
lastStatus = deployment.status;
|
|
40
|
+
}
|
|
41
|
+
if (deployment.status === "live") {
|
|
42
|
+
spinner.stop("Live");
|
|
43
|
+
return deployment;
|
|
44
|
+
}
|
|
45
|
+
if (deployment.status === "failed") {
|
|
46
|
+
spinner.stop();
|
|
47
|
+
return deployment;
|
|
48
|
+
}
|
|
49
|
+
await sleep(POLL_INTERVAL_MS);
|
|
37
50
|
}
|
|
38
|
-
if (deployment.status === "live" || deployment.status === "failed")
|
|
39
|
-
return deployment;
|
|
40
|
-
await sleep(POLL_INTERVAL_MS);
|
|
41
51
|
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
spinner.stop();
|
|
54
|
+
throw err;
|
|
55
|
+
}
|
|
56
|
+
spinner.stop();
|
|
42
57
|
return fail("Timed out after 5 minutes waiting for the deploy to go live.");
|
|
43
58
|
}
|
|
44
59
|
function reportChecks(deployment) {
|
|
@@ -70,12 +85,13 @@ export async function deployCommand(args) {
|
|
|
70
85
|
}
|
|
71
86
|
}
|
|
72
87
|
async function runDeploy(args, apiBase, siteDir) {
|
|
73
|
-
const
|
|
88
|
+
const interactive = !isJsonMode() && !flagBool(args.flags, "no-input");
|
|
89
|
+
const creds = await ensureApiKey({
|
|
74
90
|
apiBase,
|
|
75
91
|
email: flagString(args.flags, "email"),
|
|
76
|
-
interactive
|
|
92
|
+
interactive,
|
|
77
93
|
});
|
|
78
|
-
const client = new Novence({ apiKey, baseUrl: apiBase });
|
|
94
|
+
const client = new Novence({ apiKey: creds.apiKey, baseUrl: apiBase });
|
|
79
95
|
let projectId = flagString(args.flags, "project");
|
|
80
96
|
let projectUrl;
|
|
81
97
|
if (projectId) {
|
|
@@ -90,11 +106,8 @@ async function runDeploy(args, apiBase, siteDir) {
|
|
|
90
106
|
projectUrl = created.url;
|
|
91
107
|
step(`Created project ${name} (${created.project.suffix})`);
|
|
92
108
|
}
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
step(`Uploaded ${uploaded.length} file${uploaded.length === 1 ? "" : "s"}`);
|
|
96
|
-
await client.deployments.create(projectId);
|
|
97
|
-
const deployment = await waitForLive(client, projectId);
|
|
109
|
+
const { uploaded } = await withSpin(`Uploading ${siteDir}…`, () => client.files.uploadDir(projectId, siteDir), ({ uploaded: files }) => `Uploaded ${files.length} file${files.length === 1 ? "" : "s"}`);
|
|
110
|
+
const deployment = await publish(client, projectId);
|
|
98
111
|
if (deployment.status === "failed") {
|
|
99
112
|
if (isJsonMode()) {
|
|
100
113
|
json({ ok: false, projectId, status: deployment.status, error: deployment.errorMessage });
|
|
@@ -118,4 +131,10 @@ async function runDeploy(args, apiBase, siteDir) {
|
|
|
118
131
|
}
|
|
119
132
|
// Last line, alone, so it pipes cleanly.
|
|
120
133
|
result(liveUrl);
|
|
134
|
+
if (creds.anonymous && interactive) {
|
|
135
|
+
warn("Claim this site to keep it past 30 days and unlock checks.");
|
|
136
|
+
const email = await prompt(" Email (Enter to skip): ");
|
|
137
|
+
if (email)
|
|
138
|
+
await claimAndMaybeVerify({ apiKey: creds.apiKey, apiBase, email, interactive: true });
|
|
139
|
+
}
|
|
121
140
|
}
|
package/dist/commands/login.js
CHANGED
|
@@ -1,23 +1,30 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { ensureApiKey } from "../lib/auth.js";
|
|
1
|
+
import { Novence } from "@novence/sdk";
|
|
2
|
+
import { claimAndMaybeVerify, ensureApiKey } from "../lib/auth.js";
|
|
3
3
|
import { credentialsPath, loadApiKey, resolveApiBase, saveApiKey } from "../lib/config.js";
|
|
4
|
+
import { reissueApiKey, resendVerification, verifyEmail } from "@novence/sdk";
|
|
4
5
|
import { flagString } from "../lib/args.js";
|
|
5
|
-
import { fail, prompt, result,
|
|
6
|
+
import { fail, prompt, result, withSpin } from "../lib/ui.js";
|
|
6
7
|
export async function loginCommand(args) {
|
|
7
8
|
const apiBase = resolveApiBase(flagString(args.flags, "api"));
|
|
8
|
-
const
|
|
9
|
-
// No key yet — the normal bootstrap path handles account creation + OTP.
|
|
9
|
+
const emailFlag = flagString(args.flags, "email");
|
|
10
10
|
if (!loadApiKey()) {
|
|
11
|
+
const email = emailFlag ?? (await prompt(" Email: "));
|
|
11
12
|
await ensureApiKey({ apiBase, email, interactive: true });
|
|
12
13
|
result(`Signed in. Key stored at ${credentialsPath()}`);
|
|
13
14
|
return;
|
|
14
15
|
}
|
|
15
|
-
|
|
16
|
-
const address = email ?? (await prompt(" Email: "));
|
|
16
|
+
const address = emailFlag ?? (await prompt(" Email: "));
|
|
17
17
|
if (!address.includes("@"))
|
|
18
18
|
fail(`Not a valid email: ${address}`);
|
|
19
|
-
|
|
20
|
-
const
|
|
19
|
+
const apiKey = loadApiKey();
|
|
20
|
+
const client = new Novence({ apiKey, baseUrl: apiBase });
|
|
21
|
+
const snap = await client.account.get();
|
|
22
|
+
if (!snap.email) {
|
|
23
|
+
await claimAndMaybeVerify({ apiKey, apiBase, email: address, interactive: true });
|
|
24
|
+
result(`${address} claimed.`);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const sent = await withSpin(`Sending a code to ${address}…`, () => resendVerification(address, apiBase));
|
|
21
28
|
const code = sent.verificationCode ?? (await prompt(" 6-digit code from your email: "));
|
|
22
29
|
if (!code)
|
|
23
30
|
fail("No code entered.");
|
package/dist/commands/whoami.js
CHANGED
|
@@ -8,14 +8,23 @@ export async function whoamiCommand(args) {
|
|
|
8
8
|
if (!apiKey)
|
|
9
9
|
fail("Not signed in. Run `novence login`.");
|
|
10
10
|
const client = new Novence({ apiKey, baseUrl: apiBase });
|
|
11
|
+
const snap = await client.account.get();
|
|
11
12
|
const quotas = await client.billing.quotas();
|
|
12
13
|
// Project count is not part of the usage payload — count them directly.
|
|
13
14
|
const { projects } = await client.projects.list();
|
|
14
15
|
if (isJsonMode()) {
|
|
15
|
-
json({
|
|
16
|
+
json({
|
|
17
|
+
ok: true,
|
|
18
|
+
...quotas,
|
|
19
|
+
email: snap.email ?? null,
|
|
20
|
+
anonymous: !snap.email,
|
|
21
|
+
projectCount: projects.length,
|
|
22
|
+
apiBase,
|
|
23
|
+
});
|
|
16
24
|
return;
|
|
17
25
|
}
|
|
18
26
|
result(`plan ${quotas.plan}`);
|
|
27
|
+
result(`email ${typeof snap.email === "string" ? snap.email : "(unclaimed)"}`);
|
|
19
28
|
result(`email verified ${quotas.emailVerified ? "yes" : "no"}`);
|
|
20
29
|
result(`api ${apiBase}`);
|
|
21
30
|
result(`key ${keySource()}`);
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import { explain } from "./lib/errors.js";
|
|
|
8
8
|
import { fail, isJsonMode, result, setJsonMode } from "./lib/ui.js";
|
|
9
9
|
import { deployCommand } from "./commands/deploy.js";
|
|
10
10
|
import { loginCommand } from "./commands/login.js";
|
|
11
|
+
import { claimCommand } from "./commands/claim.js";
|
|
11
12
|
import { projectsCommand } from "./commands/projects.js";
|
|
12
13
|
import { whoamiCommand } from "./commands/whoami.js";
|
|
13
14
|
import { openCommand } from "./commands/open.js";
|
|
@@ -27,6 +28,7 @@ const HELP = `
|
|
|
27
28
|
novence keys revoke <id> Revoke a key
|
|
28
29
|
novence projects List projects and their URLs
|
|
29
30
|
novence login Sign in, verify email, or recover a key
|
|
31
|
+
novence claim Attach an email to an anonymous deploy
|
|
30
32
|
novence whoami Plan, verification, and usage
|
|
31
33
|
|
|
32
34
|
Deploy
|
|
@@ -34,7 +36,7 @@ const HELP = `
|
|
|
34
36
|
novence deploy ./site Deploy a specific directory
|
|
35
37
|
--name <name> Project name (default: "site")
|
|
36
38
|
--project <id> Redeploy an existing project
|
|
37
|
-
--email <email> Skip the email prompt on first run
|
|
39
|
+
--email <email> Skip the email prompt on first run (bootstrap instead of demo)
|
|
38
40
|
|
|
39
41
|
Global
|
|
40
42
|
--json Machine-readable output (implies --no-input)
|
|
@@ -72,6 +74,7 @@ async function main() {
|
|
|
72
74
|
// change behaviour.
|
|
73
75
|
const ALLOWED = {
|
|
74
76
|
deploy: ["name", "project", "email"],
|
|
77
|
+
claim: ["email"],
|
|
75
78
|
open: [],
|
|
76
79
|
checks: [],
|
|
77
80
|
domains: ["project", "primary", "redirect"],
|
|
@@ -100,6 +103,8 @@ async function main() {
|
|
|
100
103
|
return keysCommand(args);
|
|
101
104
|
case "login":
|
|
102
105
|
return loginCommand(args);
|
|
106
|
+
case "claim":
|
|
107
|
+
return claimCommand(args);
|
|
103
108
|
case "projects":
|
|
104
109
|
case "ls":
|
|
105
110
|
return projectsCommand(args);
|
package/dist/lib/auth.d.ts
CHANGED
|
@@ -2,16 +2,20 @@ export interface Credentials {
|
|
|
2
2
|
apiKey: string;
|
|
3
3
|
/** True when this run created the account, so callers can tailor next steps. */
|
|
4
4
|
fresh: boolean;
|
|
5
|
+
anonymous: boolean;
|
|
5
6
|
}
|
|
6
7
|
/**
|
|
7
|
-
* Returns a usable API key
|
|
8
|
-
*
|
|
9
|
-
* The API currently requires an email at bootstrap. Once anonymous deploys land
|
|
10
|
-
* (`POST /v1/demo`), deploy should publish first and claim afterwards — see the
|
|
11
|
-
* deploy-first note in the README.
|
|
8
|
+
* Returns a usable API key. With no stored key and no `--email`, creates an
|
|
9
|
+
* anonymous demo account so deploy can print a live URL first.
|
|
12
10
|
*/
|
|
13
11
|
export declare function ensureApiKey(opts: {
|
|
14
12
|
apiBase: string;
|
|
15
13
|
email?: string;
|
|
16
14
|
interactive: boolean;
|
|
17
15
|
}): Promise<Credentials>;
|
|
16
|
+
export declare function claimAndMaybeVerify(opts: {
|
|
17
|
+
apiKey: string;
|
|
18
|
+
apiBase: string;
|
|
19
|
+
email: string;
|
|
20
|
+
interactive: boolean;
|
|
21
|
+
}): Promise<void>;
|
package/dist/lib/auth.js
CHANGED
|
@@ -1,34 +1,35 @@
|
|
|
1
|
-
import { bootstrapAccount, verifyEmail } from "@novence/sdk";
|
|
1
|
+
import { bootstrapAccount, claimAccount, createDemoAccount, verifyEmail } from "@novence/sdk";
|
|
2
2
|
import { loadApiKey, saveApiKey } from "./config.js";
|
|
3
|
-
import { fail, prompt, step, warn } from "./ui.js";
|
|
3
|
+
import { fail, prompt, step, warn, withSpin } from "./ui.js";
|
|
4
4
|
/**
|
|
5
|
-
* Returns a usable API key
|
|
6
|
-
*
|
|
7
|
-
* The API currently requires an email at bootstrap. Once anonymous deploys land
|
|
8
|
-
* (`POST /v1/demo`), deploy should publish first and claim afterwards — see the
|
|
9
|
-
* deploy-first note in the README.
|
|
5
|
+
* Returns a usable API key. With no stored key and no `--email`, creates an
|
|
6
|
+
* anonymous demo account so deploy can print a live URL first.
|
|
10
7
|
*/
|
|
11
8
|
export async function ensureApiKey(opts) {
|
|
12
9
|
const existing = loadApiKey();
|
|
13
10
|
if (existing)
|
|
14
|
-
return { apiKey: existing, fresh: false };
|
|
15
|
-
if (
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
11
|
+
return { apiKey: existing, fresh: false, anonymous: false };
|
|
12
|
+
if (opts.email)
|
|
13
|
+
return bootstrapWithEmail(opts.email, opts.apiBase, opts.interactive);
|
|
14
|
+
const demo = await withSpin("Creating a temporary account…", () => createDemoAccount(opts.apiBase));
|
|
15
|
+
const path = saveApiKey(demo.apiKey);
|
|
16
|
+
step(`Anonymous account ${demo.accountId} · key saved to ${path}`);
|
|
17
|
+
warn("Unclaimed site — run `novence claim --email you@example.com` to keep it.");
|
|
18
|
+
return { apiKey: demo.apiKey, fresh: true, anonymous: true };
|
|
19
|
+
}
|
|
20
|
+
async function bootstrapWithEmail(email, apiBase, interactive) {
|
|
20
21
|
if (!email.includes("@"))
|
|
21
22
|
fail(`Not a valid email: ${email}`);
|
|
22
|
-
step(`Creating your account (${email})…`);
|
|
23
23
|
let boot;
|
|
24
24
|
try {
|
|
25
|
-
boot = await bootstrapAccount(email,
|
|
25
|
+
boot = await withSpin(`Creating your account (${email})…`, () => bootstrapAccount(email, apiBase));
|
|
26
26
|
}
|
|
27
27
|
catch (err) {
|
|
28
28
|
const message = err instanceof Error ? err.message : String(err);
|
|
29
29
|
if (/exists|already|409/i.test(message)) {
|
|
30
30
|
fail(`${email} already has an account. Recover the key with an OTP:\n` +
|
|
31
|
-
" novence login --email " +
|
|
31
|
+
" novence login --email " +
|
|
32
|
+
email);
|
|
32
33
|
}
|
|
33
34
|
throw err;
|
|
34
35
|
}
|
|
@@ -36,19 +37,42 @@ export async function ensureApiKey(opts) {
|
|
|
36
37
|
step(`Account ${boot.accountId} · plan ${boot.plan}`);
|
|
37
38
|
step(`Key saved to ${path}`);
|
|
38
39
|
if (boot.verificationCode) {
|
|
39
|
-
|
|
40
|
-
await verifyEmail(email, boot.verificationCode, opts.apiBase);
|
|
40
|
+
await verifyEmail(email, boot.verificationCode, apiBase);
|
|
41
41
|
step("Email verified");
|
|
42
42
|
}
|
|
43
|
-
else if (boot.emailVerificationRequired) {
|
|
43
|
+
else if (boot.emailVerificationRequired && interactive) {
|
|
44
44
|
const code = await prompt(" 6-digit code from your email (Enter to skip): ");
|
|
45
45
|
if (code) {
|
|
46
|
-
await verifyEmail(email, code,
|
|
46
|
+
await verifyEmail(email, code, apiBase);
|
|
47
47
|
step("Email verified");
|
|
48
48
|
}
|
|
49
49
|
else {
|
|
50
50
|
warn("Unverified: 1 project, 3 deploys/mo, checks skipped. Run `novence login` to verify.");
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
|
-
|
|
53
|
+
else if (boot.emailVerificationRequired) {
|
|
54
|
+
warn("Unverified: run `novence login --email " + email + "` to unlock checks.");
|
|
55
|
+
}
|
|
56
|
+
return { apiKey: boot.apiKey, fresh: true, anonymous: false };
|
|
57
|
+
}
|
|
58
|
+
export async function claimAndMaybeVerify(opts) {
|
|
59
|
+
if (!opts.email.includes("@"))
|
|
60
|
+
fail(`Not a valid email: ${opts.email}`);
|
|
61
|
+
const claimed = await withSpin(`Claiming account for ${opts.email}…`, () => claimAccount(opts.apiKey, opts.email, opts.apiBase));
|
|
62
|
+
if (claimed.verificationCode) {
|
|
63
|
+
await verifyEmail(opts.email, claimed.verificationCode, opts.apiBase);
|
|
64
|
+
step("Email verified");
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (!opts.interactive) {
|
|
68
|
+
warn("OTP emailed. Run `novence login --email " + opts.email + "` to verify.");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const code = await prompt(" 6-digit code from your email: ");
|
|
72
|
+
if (!code) {
|
|
73
|
+
warn("Skipped verify. Run `novence login --email " + opts.email + "` later.");
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
await verifyEmail(opts.email, code, opts.apiBase);
|
|
77
|
+
step("Email verified");
|
|
54
78
|
}
|
package/dist/lib/ui.d.ts
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
|
+
export interface Spinner {
|
|
2
|
+
update(message: string): void;
|
|
3
|
+
stop(done?: string): void;
|
|
4
|
+
}
|
|
1
5
|
export declare function setJsonMode(on: boolean): void;
|
|
2
6
|
export declare function isJsonMode(): boolean;
|
|
7
|
+
export declare function isInteractive(): boolean;
|
|
3
8
|
/** Progress goes to stderr so `novence deploy … > url.txt` stays clean. */
|
|
4
9
|
export declare function step(message: string): void;
|
|
10
|
+
/**
|
|
11
|
+
* Animate a packet-on-a-wire spinner on stderr while work runs.
|
|
12
|
+
* No-ops the animation in --json, CI, or when stderr is not a TTY.
|
|
13
|
+
*/
|
|
14
|
+
export declare function spin(label: string): Spinner;
|
|
15
|
+
export declare function withSpin<T>(label: string, work: (spinner: Spinner) => Promise<T>, done?: string | ((value: T) => string)): Promise<T>;
|
|
5
16
|
export declare function warn(message: string): void;
|
|
6
17
|
/** The one line callers pipe. Always stdout. */
|
|
7
18
|
export declare function result(value: string): void;
|
package/dist/lib/ui.js
CHANGED
|
@@ -1,18 +1,107 @@
|
|
|
1
1
|
import { createInterface } from "node:readline/promises";
|
|
2
2
|
import { stdin, stdout } from "node:process";
|
|
3
3
|
let jsonMode = false;
|
|
4
|
+
let cursorHidden = false;
|
|
5
|
+
let activeSpinner = null;
|
|
6
|
+
/** Packet traveling along a wire. Same width every frame so the line does not jump. */
|
|
7
|
+
const PACKET_FRAMES = ["──·────", "───·───", "────·──", "─────·─", "──────·", "─────·─", "────·──", "───·───"];
|
|
8
|
+
const PACKET_INTERVAL_MS = 100;
|
|
4
9
|
export function setJsonMode(on) {
|
|
5
10
|
jsonMode = on;
|
|
6
11
|
}
|
|
7
12
|
export function isJsonMode() {
|
|
8
13
|
return jsonMode;
|
|
9
14
|
}
|
|
15
|
+
export function isInteractive() {
|
|
16
|
+
return Boolean(process.stderr.isTTY) && !jsonMode && !process.env.CI;
|
|
17
|
+
}
|
|
18
|
+
function hideCursor() {
|
|
19
|
+
if (cursorHidden || !process.stderr.isTTY)
|
|
20
|
+
return;
|
|
21
|
+
process.stderr.write("\x1b[?25l");
|
|
22
|
+
cursorHidden = true;
|
|
23
|
+
}
|
|
24
|
+
function showCursor() {
|
|
25
|
+
if (!cursorHidden)
|
|
26
|
+
return;
|
|
27
|
+
process.stderr.write("\x1b[?25h");
|
|
28
|
+
cursorHidden = false;
|
|
29
|
+
}
|
|
30
|
+
process.on("exit", showCursor);
|
|
31
|
+
function stopActiveSpinner() {
|
|
32
|
+
activeSpinner?.stop();
|
|
33
|
+
}
|
|
10
34
|
/** Progress goes to stderr so `novence deploy … > url.txt` stays clean. */
|
|
11
35
|
export function step(message) {
|
|
12
36
|
if (!jsonMode)
|
|
13
37
|
process.stderr.write(` ${message}\n`);
|
|
14
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Animate a packet-on-a-wire spinner on stderr while work runs.
|
|
41
|
+
* No-ops the animation in --json, CI, or when stderr is not a TTY.
|
|
42
|
+
*/
|
|
43
|
+
export function spin(label) {
|
|
44
|
+
stopActiveSpinner();
|
|
45
|
+
if (!isInteractive()) {
|
|
46
|
+
step(label);
|
|
47
|
+
const spinner = {
|
|
48
|
+
update() { },
|
|
49
|
+
stop(done) {
|
|
50
|
+
if (activeSpinner !== spinner)
|
|
51
|
+
return;
|
|
52
|
+
activeSpinner = null;
|
|
53
|
+
if (done)
|
|
54
|
+
step(done);
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
activeSpinner = spinner;
|
|
58
|
+
return spinner;
|
|
59
|
+
}
|
|
60
|
+
let i = 0;
|
|
61
|
+
let text = label;
|
|
62
|
+
let stopped = false;
|
|
63
|
+
hideCursor();
|
|
64
|
+
const draw = () => {
|
|
65
|
+
process.stderr.write(`\r ${PACKET_FRAMES[i % PACKET_FRAMES.length]} ${text}\x1b[K`);
|
|
66
|
+
i += 1;
|
|
67
|
+
};
|
|
68
|
+
draw();
|
|
69
|
+
const id = setInterval(draw, PACKET_INTERVAL_MS);
|
|
70
|
+
const spinner = {
|
|
71
|
+
update(message) {
|
|
72
|
+
text = message;
|
|
73
|
+
},
|
|
74
|
+
stop(done) {
|
|
75
|
+
if (stopped)
|
|
76
|
+
return;
|
|
77
|
+
stopped = true;
|
|
78
|
+
clearInterval(id);
|
|
79
|
+
if (activeSpinner === spinner)
|
|
80
|
+
activeSpinner = null;
|
|
81
|
+
if (done)
|
|
82
|
+
process.stderr.write(`\r ${done}\x1b[K\n`);
|
|
83
|
+
else
|
|
84
|
+
process.stderr.write(`\r\x1b[K`);
|
|
85
|
+
showCursor();
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
activeSpinner = spinner;
|
|
89
|
+
return spinner;
|
|
90
|
+
}
|
|
91
|
+
export async function withSpin(label, work, done) {
|
|
92
|
+
const spinner = spin(label);
|
|
93
|
+
try {
|
|
94
|
+
const value = await work(spinner);
|
|
95
|
+
spinner.stop(typeof done === "function" ? done(value) : done);
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
spinner.stop();
|
|
100
|
+
throw err;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
15
103
|
export function warn(message) {
|
|
104
|
+
stopActiveSpinner();
|
|
16
105
|
process.stderr.write(` ! ${message}\n`);
|
|
17
106
|
}
|
|
18
107
|
/** The one line callers pipe. Always stdout. */
|
|
@@ -23,6 +112,7 @@ export function json(value) {
|
|
|
23
112
|
stdout.write(JSON.stringify(value, null, 2) + "\n");
|
|
24
113
|
}
|
|
25
114
|
export function fail(message) {
|
|
115
|
+
stopActiveSpinner();
|
|
26
116
|
if (jsonMode) {
|
|
27
117
|
stdout.write(JSON.stringify({ ok: false, error: message }, null, 2) + "\n");
|
|
28
118
|
}
|
|
@@ -32,6 +122,7 @@ export function fail(message) {
|
|
|
32
122
|
process.exit(1);
|
|
33
123
|
}
|
|
34
124
|
export async function prompt(question) {
|
|
125
|
+
stopActiveSpinner();
|
|
35
126
|
if (!stdin.isTTY) {
|
|
36
127
|
fail(`Cannot prompt for "${question.trim()}" — stdin is not a TTY. Pass the value as a flag.`);
|
|
37
128
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "novence",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "CLI for Novence
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "CLI for Novence — deploy a static site to the edge in one command.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"engines": {
|
|
18
18
|
"node": ">=18"
|
|
19
19
|
},
|
|
20
|
-
"homepage": "https://novence.ai/
|
|
20
|
+
"homepage": "https://novence.ai/cli",
|
|
21
21
|
"bugs": {
|
|
22
22
|
"url": "https://novence.ai/support"
|
|
23
23
|
},
|
|
@@ -29,21 +29,25 @@
|
|
|
29
29
|
"hosting",
|
|
30
30
|
"edge",
|
|
31
31
|
"static-site",
|
|
32
|
-
"ai-agents"
|
|
32
|
+
"ai-agents",
|
|
33
|
+
"claude",
|
|
34
|
+
"cursor",
|
|
35
|
+
"mcp-server",
|
|
36
|
+
"static-hosting",
|
|
37
|
+
"netlify-alternative"
|
|
33
38
|
],
|
|
34
39
|
"publishConfig": {
|
|
35
40
|
"access": "public"
|
|
36
41
|
},
|
|
37
|
-
"scripts": {
|
|
38
|
-
"build": "tsc -p tsconfig.json",
|
|
39
|
-
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
40
|
-
"prepublishOnly": "pnpm run build"
|
|
41
|
-
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@novence/sdk": "^0.2.
|
|
43
|
+
"@novence/sdk": "^0.2.3"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@types/node": "^22.10.2",
|
|
47
47
|
"typescript": "^5.7.2"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsc -p tsconfig.json",
|
|
51
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
48
52
|
}
|
|
49
|
-
}
|
|
53
|
+
}
|