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 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,2 @@
1
+ import { type ParsedArgs } from "../lib/args.js";
2
+ export declare function deployCommand(args: ParsedArgs): Promise<void>;
@@ -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,2 @@
1
+ import { type ParsedArgs } from "../lib/args.js";
2
+ export declare function loginCommand(args: ParsedArgs): Promise<void>;
@@ -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,2 @@
1
+ import { type ParsedArgs } from "../lib/args.js";
2
+ export declare function projectsCommand(args: ParsedArgs): Promise<void>;
@@ -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,2 @@
1
+ import { type ParsedArgs } from "../lib/args.js";
2
+ export declare function whoamiCommand(args: ParsedArgs): Promise<void>;
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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;
@@ -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>;
@@ -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
+ }
@@ -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
+ }