novence 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/LICENSE +21 -0
- package/README.md +98 -0
- package/dist/commands/deploy.d.ts +2 -0
- package/dist/commands/deploy.js +111 -0
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.js +32 -0
- package/dist/commands/projects.d.ts +2 -0
- package/dist/commands/projects.js +24 -0
- package/dist/commands/whoami.d.ts +2 -0
- package/dist/commands/whoami.js +37 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +75 -0
- package/dist/lib/args.d.ts +9 -0
- package/dist/lib/args.js +42 -0
- package/dist/lib/auth.d.ts +17 -0
- package/dist/lib/auth.js +54 -0
- package/dist/lib/config.d.ts +13 -0
- package/dist/lib/config.js +54 -0
- package/dist/lib/ui.d.ts +10 -0
- package/dist/lib/ui.js +48 -0
- package/package.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Novence
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# novence
|
|
2
|
+
|
|
3
|
+
Deploy a static site to the edge in one command.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx novence deploy ./dist
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Uploads the directory, runs quality checks (Lighthouse, axe, links), publishes to
|
|
10
|
+
`https://{suffix}.novence.ai/`, and prints the live URL.
|
|
11
|
+
|
|
12
|
+
Part of [Novence](https://novence.ai) — static site hosting built for AI agents.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
Run it directly with `npx`, or install it:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install -g novence
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Requires Node 18+.
|
|
23
|
+
|
|
24
|
+
## Deploy
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
novence deploy # auto-detects dist, build, out, public, _site
|
|
28
|
+
novence deploy ./site # or point at a directory
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
First run asks for an email and a 6-digit code, then stores your API key in
|
|
32
|
+
`~/.novence/credentials` (mode 0600). After that, deploys are one command.
|
|
33
|
+
|
|
34
|
+
The live URL is the last line on stdout and progress goes to stderr, so it pipes:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
URL=$(novence deploy ./dist)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Flags
|
|
41
|
+
|
|
42
|
+
| Flag | |
|
|
43
|
+
| --- | --- |
|
|
44
|
+
| `--name <name>` | Project name (default `site`) |
|
|
45
|
+
| `--project <id>` | Redeploy an existing project instead of creating one |
|
|
46
|
+
| `--email <email>` | Skip the email prompt on first run |
|
|
47
|
+
| `--json` | Machine-readable output; implies `--no-input` |
|
|
48
|
+
| `--no-input` | Never prompt — fail instead. For CI |
|
|
49
|
+
| `--api <url>` | Control plane (default `https://api.novence.ai`) |
|
|
50
|
+
|
|
51
|
+
## Other commands
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
novence login # sign in, verify email, or recover a key with --recover
|
|
55
|
+
novence projects # list projects and their URLs
|
|
56
|
+
novence whoami # plan, verification, usage against quota
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## For agents and CI
|
|
60
|
+
|
|
61
|
+
`--json` gives a stable envelope and exits non-zero on failure:
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"ok": true,
|
|
66
|
+
"projectId": "…",
|
|
67
|
+
"deploymentId": "…",
|
|
68
|
+
"status": "live",
|
|
69
|
+
"url": "https://abcd1234.novence.ai/",
|
|
70
|
+
"files": 12,
|
|
71
|
+
"checks": { "passed": true }
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
In CI, set `NOVENCE_API_KEY` and pass `--no-input`.
|
|
76
|
+
|
|
77
|
+
Key lookup order: `NOVENCE_API_KEY`, then `./.novence-key`, then
|
|
78
|
+
`~/.novence/credentials`. Never commit a key.
|
|
79
|
+
|
|
80
|
+
## Checks
|
|
81
|
+
|
|
82
|
+
Deploys run Lighthouse, axe, and link checks. On unverified accounts checks are
|
|
83
|
+
skipped — `checks.skipped` is `true` and `checks.passed` is `null`, which is not
|
|
84
|
+
a pass. Verify your email with `novence login` to enable them.
|
|
85
|
+
|
|
86
|
+
## Roadmap
|
|
87
|
+
|
|
88
|
+
Today the first deploy requires an email up front. Once anonymous deploys land in
|
|
89
|
+
the API, `novence deploy` will publish first and let you claim the site
|
|
90
|
+
afterwards — no signup before you see a URL.
|
|
91
|
+
|
|
92
|
+
## Docs
|
|
93
|
+
|
|
94
|
+
[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)
|
|
95
|
+
|
|
96
|
+
## License
|
|
97
|
+
|
|
98
|
+
MIT
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { existsSync, statSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { Novence } from "@novence/sdk";
|
|
4
|
+
import { ensureApiKey } from "../lib/auth.js";
|
|
5
|
+
import { resolveApiBase } from "../lib/config.js";
|
|
6
|
+
import { flagBool, flagString } from "../lib/args.js";
|
|
7
|
+
import { fail, isJsonMode, json, result, step, warn } from "../lib/ui.js";
|
|
8
|
+
const POLL_INTERVAL_MS = 2000;
|
|
9
|
+
const POLL_TIMEOUT_MS = 5 * 60 * 1000;
|
|
10
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
11
|
+
/** Common build output directories, checked in order when no dir is given. */
|
|
12
|
+
const CANDIDATE_DIRS = ["dist", "build", "out", "public", "_site"];
|
|
13
|
+
function resolveSiteDir(explicit) {
|
|
14
|
+
if (explicit) {
|
|
15
|
+
const dir = resolve(explicit);
|
|
16
|
+
if (!existsSync(dir) || !statSync(dir).isDirectory())
|
|
17
|
+
fail(`Not a directory: ${dir}`);
|
|
18
|
+
return dir;
|
|
19
|
+
}
|
|
20
|
+
for (const candidate of CANDIDATE_DIRS) {
|
|
21
|
+
const dir = resolve(process.cwd(), candidate);
|
|
22
|
+
if (existsSync(dir) && statSync(dir).isDirectory())
|
|
23
|
+
return dir;
|
|
24
|
+
}
|
|
25
|
+
return fail(`No build output found. Looked for ${CANDIDATE_DIRS.join(", ")} in ${process.cwd()}.\n` +
|
|
26
|
+
" Pass one explicitly: novence deploy ./your-dir");
|
|
27
|
+
}
|
|
28
|
+
async function waitForLive(client, projectId) {
|
|
29
|
+
const startedAt = Date.now();
|
|
30
|
+
let lastStatus = "";
|
|
31
|
+
while (Date.now() - startedAt < POLL_TIMEOUT_MS) {
|
|
32
|
+
const { deployment } = await client.deployments.latest(projectId);
|
|
33
|
+
if (deployment.status !== lastStatus) {
|
|
34
|
+
step(`${deployment.status}…`);
|
|
35
|
+
lastStatus = deployment.status;
|
|
36
|
+
}
|
|
37
|
+
if (deployment.status === "live" || deployment.status === "failed")
|
|
38
|
+
return deployment;
|
|
39
|
+
await sleep(POLL_INTERVAL_MS);
|
|
40
|
+
}
|
|
41
|
+
return fail("Timed out after 5 minutes waiting for the deploy to go live.");
|
|
42
|
+
}
|
|
43
|
+
function reportChecks(deployment) {
|
|
44
|
+
const checks = deployment.checksResults;
|
|
45
|
+
if (!checks)
|
|
46
|
+
return;
|
|
47
|
+
if (checks.skipped) {
|
|
48
|
+
warn(`Checks skipped${checks.reason ? `: ${checks.reason}` : ""} (verify your email to enable)`);
|
|
49
|
+
}
|
|
50
|
+
else if (checks.passed === false) {
|
|
51
|
+
warn(`Quality checks failed${deployment.checksUrl ? ` — ${deployment.checksUrl}` : ""}`);
|
|
52
|
+
}
|
|
53
|
+
else if (checks.passed === true) {
|
|
54
|
+
step("Checks passed");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export async function deployCommand(args) {
|
|
58
|
+
const apiBase = resolveApiBase(flagString(args.flags, "api"));
|
|
59
|
+
const siteDir = resolveSiteDir(args.positionals[0]);
|
|
60
|
+
if (!existsSync(resolve(siteDir, "index.html"))) {
|
|
61
|
+
fail(`${siteDir} has no index.html — a static site needs one at its root.`);
|
|
62
|
+
}
|
|
63
|
+
const { apiKey } = await ensureApiKey({
|
|
64
|
+
apiBase,
|
|
65
|
+
email: flagString(args.flags, "email"),
|
|
66
|
+
interactive: !isJsonMode() && !flagBool(args.flags, "no-input"),
|
|
67
|
+
});
|
|
68
|
+
const client = new Novence({ apiKey, baseUrl: apiBase });
|
|
69
|
+
let projectId = flagString(args.flags, "project");
|
|
70
|
+
let siteUrl;
|
|
71
|
+
if (projectId) {
|
|
72
|
+
const { project } = await client.projects.get(projectId);
|
|
73
|
+
siteUrl = project.url;
|
|
74
|
+
step(`Reusing project ${project.name} (${project.suffix})`);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
const name = flagString(args.flags, "name") ?? "site";
|
|
78
|
+
const created = await client.projects.create({ name });
|
|
79
|
+
projectId = created.project.id;
|
|
80
|
+
siteUrl = created.url;
|
|
81
|
+
step(`Created project ${name} (${created.project.suffix})`);
|
|
82
|
+
}
|
|
83
|
+
step(`Uploading ${siteDir}…`);
|
|
84
|
+
const { uploaded } = await client.files.uploadDir(projectId, siteDir);
|
|
85
|
+
step(`Uploaded ${uploaded.length} file${uploaded.length === 1 ? "" : "s"}`);
|
|
86
|
+
await client.deployments.create(projectId);
|
|
87
|
+
const deployment = await waitForLive(client, projectId);
|
|
88
|
+
if (deployment.status === "failed") {
|
|
89
|
+
if (isJsonMode()) {
|
|
90
|
+
json({ ok: false, projectId, status: deployment.status, error: deployment.errorMessage });
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
fail(deployment.errorMessage ?? "Deploy failed.");
|
|
94
|
+
}
|
|
95
|
+
reportChecks(deployment);
|
|
96
|
+
const liveUrl = (deployment.liveUrl ?? siteUrl).replace(/\/?$/, "/");
|
|
97
|
+
if (isJsonMode()) {
|
|
98
|
+
json({
|
|
99
|
+
ok: true,
|
|
100
|
+
projectId,
|
|
101
|
+
deploymentId: deployment.id,
|
|
102
|
+
status: deployment.status,
|
|
103
|
+
url: liveUrl,
|
|
104
|
+
files: uploaded.length,
|
|
105
|
+
checks: deployment.checksResults ?? null,
|
|
106
|
+
});
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
// Last line, alone, so it pipes cleanly.
|
|
110
|
+
result(liveUrl);
|
|
111
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { reissueApiKey, resendVerification, verifyEmail } from "@novence/sdk";
|
|
2
|
+
import { ensureApiKey } from "../lib/auth.js";
|
|
3
|
+
import { credentialsPath, loadApiKey, resolveApiBase, saveApiKey } from "../lib/config.js";
|
|
4
|
+
import { flagString } from "../lib/args.js";
|
|
5
|
+
import { fail, prompt, result, step } from "../lib/ui.js";
|
|
6
|
+
export async function loginCommand(args) {
|
|
7
|
+
const apiBase = resolveApiBase(flagString(args.flags, "api"));
|
|
8
|
+
const email = flagString(args.flags, "email");
|
|
9
|
+
// No key yet — the normal bootstrap path handles account creation + OTP.
|
|
10
|
+
if (!loadApiKey()) {
|
|
11
|
+
await ensureApiKey({ apiBase, email, interactive: true });
|
|
12
|
+
result(`Signed in. Key stored at ${credentialsPath()}`);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
// Key exists. Either verify this email, or recover a lost key via OTP.
|
|
16
|
+
const address = email ?? (await prompt(" Email: "));
|
|
17
|
+
if (!address.includes("@"))
|
|
18
|
+
fail(`Not a valid email: ${address}`);
|
|
19
|
+
step(`Sending a code to ${address}…`);
|
|
20
|
+
const sent = await resendVerification(address, apiBase);
|
|
21
|
+
const code = sent.verificationCode ?? (await prompt(" 6-digit code from your email: "));
|
|
22
|
+
if (!code)
|
|
23
|
+
fail("No code entered.");
|
|
24
|
+
if (flagString(args.flags, "recover") !== undefined || args.flags.recover === true) {
|
|
25
|
+
const issued = await reissueApiKey(address, code, apiBase);
|
|
26
|
+
saveApiKey(issued.apiKey);
|
|
27
|
+
result(`Key reissued for account ${issued.accountId} → ${credentialsPath()}`);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
await verifyEmail(address, code, apiBase);
|
|
31
|
+
result(`${address} verified.`);
|
|
32
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Novence } from "@novence/sdk";
|
|
2
|
+
import { loadApiKey, resolveApiBase } from "../lib/config.js";
|
|
3
|
+
import { flagString } from "../lib/args.js";
|
|
4
|
+
import { fail, isJsonMode, json, result } from "../lib/ui.js";
|
|
5
|
+
export async function projectsCommand(args) {
|
|
6
|
+
const apiBase = resolveApiBase(flagString(args.flags, "api"));
|
|
7
|
+
const apiKey = loadApiKey();
|
|
8
|
+
if (!apiKey)
|
|
9
|
+
fail("Not signed in. Run `novence login`.");
|
|
10
|
+
const client = new Novence({ apiKey, baseUrl: apiBase });
|
|
11
|
+
const { projects } = await client.projects.list();
|
|
12
|
+
if (isJsonMode()) {
|
|
13
|
+
json({ ok: true, projects });
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
if (projects.length === 0) {
|
|
17
|
+
result("No projects yet. Run `novence deploy ./dist` to create one.");
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const width = Math.max(...projects.map((p) => p.name.length), 4);
|
|
21
|
+
for (const project of projects) {
|
|
22
|
+
result(`${project.name.padEnd(width)} ${project.suffix} ${project.url}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Novence } from "@novence/sdk";
|
|
2
|
+
import { keySource, loadApiKey, resolveApiBase } from "../lib/config.js";
|
|
3
|
+
import { flagString } from "../lib/args.js";
|
|
4
|
+
import { fail, isJsonMode, json, result } from "../lib/ui.js";
|
|
5
|
+
export async function whoamiCommand(args) {
|
|
6
|
+
const apiBase = resolveApiBase(flagString(args.flags, "api"));
|
|
7
|
+
const apiKey = loadApiKey();
|
|
8
|
+
if (!apiKey)
|
|
9
|
+
fail("Not signed in. Run `novence login`.");
|
|
10
|
+
const client = new Novence({ apiKey, baseUrl: apiBase });
|
|
11
|
+
const quotas = await client.billing.quotas();
|
|
12
|
+
// Project count is not part of the usage payload — count them directly.
|
|
13
|
+
const { projects } = await client.projects.list();
|
|
14
|
+
if (isJsonMode()) {
|
|
15
|
+
json({ ok: true, ...quotas, projectCount: projects.length, apiBase });
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
result(`plan ${quotas.plan}`);
|
|
19
|
+
result(`email verified ${quotas.emailVerified ? "yes" : "no"}`);
|
|
20
|
+
result(`api ${apiBase}`);
|
|
21
|
+
result(`key ${keySource()}`);
|
|
22
|
+
result(`projects ${projects.length} / ${quotas.quotas.projects}`);
|
|
23
|
+
result(`deploys (mo) ${quotas.usage.deploys_count ?? 0} / ${quotas.quotas.deploysPerMonth}`);
|
|
24
|
+
result(`storage ${formatBytes(quotas.usage.storage_bytes ?? 0)} / ${quotas.quotas.storageGb} GB`);
|
|
25
|
+
}
|
|
26
|
+
function formatBytes(bytes) {
|
|
27
|
+
if (bytes < 1024)
|
|
28
|
+
return `${bytes} B`;
|
|
29
|
+
const units = ["KB", "MB", "GB"];
|
|
30
|
+
let value = bytes / 1024;
|
|
31
|
+
let unit = 0;
|
|
32
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
33
|
+
value /= 1024;
|
|
34
|
+
unit += 1;
|
|
35
|
+
}
|
|
36
|
+
return `${value < 10 ? value.toFixed(1) : Math.round(value)} ${units[unit]}`;
|
|
37
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { flagBool, parseArgs } from "./lib/args.js";
|
|
6
|
+
import { fail, isJsonMode, result, setJsonMode } from "./lib/ui.js";
|
|
7
|
+
import { deployCommand } from "./commands/deploy.js";
|
|
8
|
+
import { loginCommand } from "./commands/login.js";
|
|
9
|
+
import { projectsCommand } from "./commands/projects.js";
|
|
10
|
+
import { whoamiCommand } from "./commands/whoami.js";
|
|
11
|
+
const HELP = `
|
|
12
|
+
novence — deploy a static site to the edge
|
|
13
|
+
|
|
14
|
+
Usage
|
|
15
|
+
novence deploy [dir] Upload a directory and publish it live
|
|
16
|
+
novence login Sign in, verify email, or recover a key
|
|
17
|
+
novence projects List your projects and their URLs
|
|
18
|
+
novence whoami Show plan, verification, and usage
|
|
19
|
+
|
|
20
|
+
Deploy
|
|
21
|
+
novence deploy Auto-detects dist, build, out, public, _site
|
|
22
|
+
novence deploy ./site Deploy a specific directory
|
|
23
|
+
--name <name> Project name (default: "site")
|
|
24
|
+
--project <id> Redeploy an existing project
|
|
25
|
+
--email <email> Skip the email prompt on first run
|
|
26
|
+
|
|
27
|
+
Global
|
|
28
|
+
--json Machine-readable output (implies --no-input)
|
|
29
|
+
--no-input Never prompt; fail instead
|
|
30
|
+
--api <url> Control plane (default https://api.novence.ai)
|
|
31
|
+
-h, --help Show this help
|
|
32
|
+
-v, --version Show the version
|
|
33
|
+
|
|
34
|
+
The live URL is the last line on stdout, so it pipes:
|
|
35
|
+
URL=$(novence deploy ./dist)
|
|
36
|
+
|
|
37
|
+
Auth
|
|
38
|
+
Reads NOVENCE_API_KEY, then ./.novence-key, then ~/.novence/credentials.
|
|
39
|
+
|
|
40
|
+
Docs https://novence.ai/docs
|
|
41
|
+
`;
|
|
42
|
+
function version() {
|
|
43
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
44
|
+
const pkg = JSON.parse(readFileSync(join(here, "..", "package.json"), "utf8"));
|
|
45
|
+
return pkg.version;
|
|
46
|
+
}
|
|
47
|
+
async function main() {
|
|
48
|
+
const args = parseArgs(process.argv.slice(2));
|
|
49
|
+
if (flagBool(args.flags, "json"))
|
|
50
|
+
setJsonMode(true);
|
|
51
|
+
if (flagBool(args.flags, "version"))
|
|
52
|
+
return result(version());
|
|
53
|
+
if (!args.command || flagBool(args.flags, "help")) {
|
|
54
|
+
if (isJsonMode())
|
|
55
|
+
return result(version());
|
|
56
|
+
process.stdout.write(HELP);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
switch (args.command) {
|
|
60
|
+
case "deploy":
|
|
61
|
+
return deployCommand(args);
|
|
62
|
+
case "login":
|
|
63
|
+
return loginCommand(args);
|
|
64
|
+
case "projects":
|
|
65
|
+
case "ls":
|
|
66
|
+
return projectsCommand(args);
|
|
67
|
+
case "whoami":
|
|
68
|
+
return whoamiCommand(args);
|
|
69
|
+
default:
|
|
70
|
+
fail(`Unknown command: ${args.command}\n Run \`novence --help\` for usage.`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
main().catch((err) => {
|
|
74
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
75
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface ParsedArgs {
|
|
2
|
+
command: string | null;
|
|
3
|
+
positionals: string[];
|
|
4
|
+
flags: Record<string, string | boolean>;
|
|
5
|
+
}
|
|
6
|
+
/** Supports --flag, --flag=value, --flag value, and -h/-v shorthands. */
|
|
7
|
+
export declare function parseArgs(argv: string[]): ParsedArgs;
|
|
8
|
+
export declare function flagString(flags: ParsedArgs["flags"], name: string): string | undefined;
|
|
9
|
+
export declare function flagBool(flags: ParsedArgs["flags"], name: string): boolean;
|
package/dist/lib/args.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** Supports --flag, --flag=value, --flag value, and -h/-v shorthands. */
|
|
2
|
+
export function parseArgs(argv) {
|
|
3
|
+
const flags = {};
|
|
4
|
+
const positionals = [];
|
|
5
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
6
|
+
const token = argv[i];
|
|
7
|
+
if (token === "-h") {
|
|
8
|
+
flags.help = true;
|
|
9
|
+
}
|
|
10
|
+
else if (token === "-v") {
|
|
11
|
+
flags.version = true;
|
|
12
|
+
}
|
|
13
|
+
else if (token.startsWith("--")) {
|
|
14
|
+
const body = token.slice(2);
|
|
15
|
+
const eq = body.indexOf("=");
|
|
16
|
+
if (eq !== -1) {
|
|
17
|
+
flags[body.slice(0, eq)] = body.slice(eq + 1);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
const next = argv[i + 1];
|
|
21
|
+
if (next && !next.startsWith("-")) {
|
|
22
|
+
flags[body] = next;
|
|
23
|
+
i += 1;
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
flags[body] = true;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
positionals.push(token);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return { command: positionals[0] ?? null, positionals: positionals.slice(1), flags };
|
|
35
|
+
}
|
|
36
|
+
export function flagString(flags, name) {
|
|
37
|
+
const value = flags[name];
|
|
38
|
+
return typeof value === "string" ? value : undefined;
|
|
39
|
+
}
|
|
40
|
+
export function flagBool(flags, name) {
|
|
41
|
+
return flags[name] === true || flags[name] === "true";
|
|
42
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface Credentials {
|
|
2
|
+
apiKey: string;
|
|
3
|
+
/** True when this run created the account, so callers can tailor next steps. */
|
|
4
|
+
fresh: boolean;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Returns a usable API key, bootstrapping interactively if none is stored.
|
|
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.
|
|
12
|
+
*/
|
|
13
|
+
export declare function ensureApiKey(opts: {
|
|
14
|
+
apiBase: string;
|
|
15
|
+
email?: string;
|
|
16
|
+
interactive: boolean;
|
|
17
|
+
}): Promise<Credentials>;
|
package/dist/lib/auth.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { bootstrapAccount, verifyEmail } from "@novence/sdk";
|
|
2
|
+
import { loadApiKey, saveApiKey } from "./config.js";
|
|
3
|
+
import { fail, prompt, step, warn } from "./ui.js";
|
|
4
|
+
/**
|
|
5
|
+
* Returns a usable API key, bootstrapping interactively if none is stored.
|
|
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.
|
|
10
|
+
*/
|
|
11
|
+
export async function ensureApiKey(opts) {
|
|
12
|
+
const existing = loadApiKey();
|
|
13
|
+
if (existing)
|
|
14
|
+
return { apiKey: existing, fresh: false };
|
|
15
|
+
if (!opts.interactive) {
|
|
16
|
+
fail("No API key found. Set NOVENCE_API_KEY, or run `novence login` first.\n" +
|
|
17
|
+
" Non-interactive runs cannot complete the email OTP step.");
|
|
18
|
+
}
|
|
19
|
+
const email = opts.email ?? (await prompt(" Email (for your account + site URL): "));
|
|
20
|
+
if (!email.includes("@"))
|
|
21
|
+
fail(`Not a valid email: ${email}`);
|
|
22
|
+
step(`Creating your account (${email})…`);
|
|
23
|
+
let boot;
|
|
24
|
+
try {
|
|
25
|
+
boot = await bootstrapAccount(email, opts.apiBase);
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
29
|
+
if (/exists|already|409/i.test(message)) {
|
|
30
|
+
fail(`${email} already has an account. Recover the key with an OTP:\n` +
|
|
31
|
+
" novence login --email " + email);
|
|
32
|
+
}
|
|
33
|
+
throw err;
|
|
34
|
+
}
|
|
35
|
+
const path = saveApiKey(boot.apiKey);
|
|
36
|
+
step(`Account ${boot.accountId} · plan ${boot.plan}`);
|
|
37
|
+
step(`Key saved to ${path}`);
|
|
38
|
+
if (boot.verificationCode) {
|
|
39
|
+
// Local/dev API returns the OTP inline.
|
|
40
|
+
await verifyEmail(email, boot.verificationCode, opts.apiBase);
|
|
41
|
+
step("Email verified");
|
|
42
|
+
}
|
|
43
|
+
else if (boot.emailVerificationRequired) {
|
|
44
|
+
const code = await prompt(" 6-digit code from your email (Enter to skip): ");
|
|
45
|
+
if (code) {
|
|
46
|
+
await verifyEmail(email, code, opts.apiBase);
|
|
47
|
+
step("Email verified");
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
warn("Unverified: 1 project, 3 deploys/mo, checks skipped. Run `novence login` to verify.");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return { apiKey: boot.apiKey, fresh: true };
|
|
54
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const DEFAULT_API = "https://api.novence.ai";
|
|
2
|
+
/** Explicit flag wins, then env, then the public control plane. */
|
|
3
|
+
export declare function resolveApiBase(flag?: string): string;
|
|
4
|
+
/**
|
|
5
|
+
* Key lookup order: NOVENCE_API_KEY, then a project-local .novence-key (what
|
|
6
|
+
* scripts/launch-site.ts writes), then ~/.novence/credentials.
|
|
7
|
+
*/
|
|
8
|
+
export declare function loadApiKey(): string | null;
|
|
9
|
+
/** Written 0600 — this is a live credential. */
|
|
10
|
+
export declare function saveApiKey(apiKey: string): string;
|
|
11
|
+
export declare function credentialsPath(): string;
|
|
12
|
+
/** Where loadApiKey() found the key, for display. */
|
|
13
|
+
export declare function keySource(): string;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
const CONFIG_DIR = join(homedir(), ".novence");
|
|
5
|
+
const CREDENTIALS_FILE = join(CONFIG_DIR, "credentials");
|
|
6
|
+
export const DEFAULT_API = "https://api.novence.ai";
|
|
7
|
+
/** Explicit flag wins, then env, then the public control plane. */
|
|
8
|
+
export function resolveApiBase(flag) {
|
|
9
|
+
const raw = flag ?? process.env.NOVENCE_BASE_URL ?? process.env.API_BASE_URL ?? DEFAULT_API;
|
|
10
|
+
return raw.replace(/\/$/, "");
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Key lookup order: NOVENCE_API_KEY, then a project-local .novence-key (what
|
|
14
|
+
* scripts/launch-site.ts writes), then ~/.novence/credentials.
|
|
15
|
+
*/
|
|
16
|
+
export function loadApiKey() {
|
|
17
|
+
const fromEnv = process.env.NOVENCE_API_KEY?.trim();
|
|
18
|
+
if (fromEnv)
|
|
19
|
+
return fromEnv;
|
|
20
|
+
const local = join(process.cwd(), ".novence-key");
|
|
21
|
+
if (existsSync(local)) {
|
|
22
|
+
const key = readFileSync(local, "utf8").trim();
|
|
23
|
+
if (key)
|
|
24
|
+
return key;
|
|
25
|
+
}
|
|
26
|
+
if (existsSync(CREDENTIALS_FILE)) {
|
|
27
|
+
const key = readFileSync(CREDENTIALS_FILE, "utf8").trim();
|
|
28
|
+
if (key)
|
|
29
|
+
return key;
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
/** Written 0600 — this is a live credential. */
|
|
34
|
+
export function saveApiKey(apiKey) {
|
|
35
|
+
mkdirSync(dirname(CREDENTIALS_FILE), { recursive: true, mode: 0o700 });
|
|
36
|
+
writeFileSync(CREDENTIALS_FILE, apiKey.trim() + "\n", { mode: 0o600 });
|
|
37
|
+
chmodSync(CREDENTIALS_FILE, 0o600);
|
|
38
|
+
return CREDENTIALS_FILE;
|
|
39
|
+
}
|
|
40
|
+
export function credentialsPath() {
|
|
41
|
+
return CREDENTIALS_FILE;
|
|
42
|
+
}
|
|
43
|
+
/** Where loadApiKey() found the key, for display. */
|
|
44
|
+
export function keySource() {
|
|
45
|
+
if (process.env.NOVENCE_API_KEY?.trim())
|
|
46
|
+
return "NOVENCE_API_KEY";
|
|
47
|
+
const local = join(process.cwd(), ".novence-key");
|
|
48
|
+
if (existsSync(local) && readFileSync(local, "utf8").trim())
|
|
49
|
+
return local;
|
|
50
|
+
if (existsSync(CREDENTIALS_FILE) && readFileSync(CREDENTIALS_FILE, "utf8").trim()) {
|
|
51
|
+
return CREDENTIALS_FILE;
|
|
52
|
+
}
|
|
53
|
+
return "(none)";
|
|
54
|
+
}
|
package/dist/lib/ui.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function setJsonMode(on: boolean): void;
|
|
2
|
+
export declare function isJsonMode(): boolean;
|
|
3
|
+
/** Progress goes to stderr so `novence deploy … > url.txt` stays clean. */
|
|
4
|
+
export declare function step(message: string): void;
|
|
5
|
+
export declare function warn(message: string): void;
|
|
6
|
+
/** The one line callers pipe. Always stdout. */
|
|
7
|
+
export declare function result(value: string): void;
|
|
8
|
+
export declare function json(value: unknown): void;
|
|
9
|
+
export declare function fail(message: string): never;
|
|
10
|
+
export declare function prompt(question: string): Promise<string>;
|
package/dist/lib/ui.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import { stdin, stdout } from "node:process";
|
|
3
|
+
let jsonMode = false;
|
|
4
|
+
export function setJsonMode(on) {
|
|
5
|
+
jsonMode = on;
|
|
6
|
+
}
|
|
7
|
+
export function isJsonMode() {
|
|
8
|
+
return jsonMode;
|
|
9
|
+
}
|
|
10
|
+
/** Progress goes to stderr so `novence deploy … > url.txt` stays clean. */
|
|
11
|
+
export function step(message) {
|
|
12
|
+
if (!jsonMode)
|
|
13
|
+
process.stderr.write(` ${message}\n`);
|
|
14
|
+
}
|
|
15
|
+
export function warn(message) {
|
|
16
|
+
process.stderr.write(` ! ${message}\n`);
|
|
17
|
+
}
|
|
18
|
+
/** The one line callers pipe. Always stdout. */
|
|
19
|
+
export function result(value) {
|
|
20
|
+
stdout.write(value + "\n");
|
|
21
|
+
}
|
|
22
|
+
export function json(value) {
|
|
23
|
+
stdout.write(JSON.stringify(value, null, 2) + "\n");
|
|
24
|
+
}
|
|
25
|
+
export function fail(message) {
|
|
26
|
+
if (jsonMode) {
|
|
27
|
+
stdout.write(JSON.stringify({ ok: false, error: message }, null, 2) + "\n");
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
process.stderr.write(`\n Error: ${message}\n`);
|
|
31
|
+
}
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
export async function prompt(question) {
|
|
35
|
+
if (!stdin.isTTY) {
|
|
36
|
+
fail(`Cannot prompt for "${question.trim()}" — stdin is not a TTY. Pass the value as a flag.`);
|
|
37
|
+
}
|
|
38
|
+
const rl = createInterface({ input: stdin, output: stderr() });
|
|
39
|
+
try {
|
|
40
|
+
return (await rl.question(question)).trim();
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
rl.close();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function stderr() {
|
|
47
|
+
return process.stderr;
|
|
48
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "novence",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "CLI for Novence — deploy a static site to the edge in one command.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"novence": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"main": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=18"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://novence.ai/docs",
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://novence.ai/support"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"novence",
|
|
26
|
+
"static-site-hosting",
|
|
27
|
+
"deploy",
|
|
28
|
+
"cli",
|
|
29
|
+
"hosting",
|
|
30
|
+
"edge",
|
|
31
|
+
"static-site",
|
|
32
|
+
"ai-agents"
|
|
33
|
+
],
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsc -p tsconfig.json",
|
|
39
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
40
|
+
"prepublishOnly": "pnpm run build"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@novence/sdk": "^0.1.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/node": "^22.10.2",
|
|
47
|
+
"typescript": "^5.7.2"
|
|
48
|
+
}
|
|
49
|
+
}
|