novence 0.2.0 → 0.3.0
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 +35 -6
- package/dist/commands/checks.js +2 -2
- package/dist/commands/data.js +2 -2
- package/dist/commands/domains.js +2 -2
- package/dist/commands/keys.js +58 -8
- package/dist/commands/login.js +92 -35
- package/dist/commands/open.js +2 -2
- package/dist/commands/projects.js +2 -2
- package/dist/commands/rollback.js +2 -2
- package/dist/commands/site-mcp.js +2 -2
- package/dist/commands/whoami.js +2 -2
- package/dist/index.js +20 -5
- package/dist/lib/args.js +1 -1
- package/dist/lib/auth.d.ts +3 -0
- package/dist/lib/auth.js +15 -5
- package/dist/lib/config.d.ts +29 -3
- package/dist/lib/config.js +120 -28
- package/dist/lib/errors.js +5 -2
- package/package.json +10 -7
package/README.md
CHANGED
|
@@ -7,6 +7,12 @@ until you claim.
|
|
|
7
7
|
npx novence deploy ./dist
|
|
8
8
|
```
|
|
9
9
|
|
|
10
|
+
> **Breaking change in 0.3.0:** `novence keys create <name>` no longer prints the
|
|
11
|
+
> secret. It saves it to `~/.novence/keys/<name>` (mode 0600) and prints only the
|
|
12
|
+
> prefix and path. `KEY=$(novence keys create ci)` now fails with a message —
|
|
13
|
+
> use `KEY=$(novence keys create ci --print)` or `--output <path>`. `--json`
|
|
14
|
+
> omits `apiKey` unless `--print` is passed.
|
|
15
|
+
|
|
10
16
|
```text
|
|
11
17
|
Created project site (abcd1234)
|
|
12
18
|
Uploaded 12 files
|
|
@@ -75,15 +81,38 @@ novence checks [project] # Lighthouse / axe / link results for the last dep
|
|
|
75
81
|
novence domains add <host> # attach a custom domain, prints the DNS records
|
|
76
82
|
novence domains status # custom domain state
|
|
77
83
|
novence keys list # API keys on the account
|
|
78
|
-
novence keys create <name> #
|
|
79
|
-
novence keys revoke <id> # revoke a key
|
|
84
|
+
novence keys create <name> # named key (e.g. ci), saved to ~/.novence/keys/<name>
|
|
85
|
+
novence keys revoke <id> # revoke a key and delete its ~/.novence/keys file
|
|
80
86
|
novence projects # list projects and their URLs
|
|
81
|
-
novence login
|
|
87
|
+
novence login --email <email> # sign in, or verify your email
|
|
82
88
|
novence claim # attach an email to an anonymous deploy
|
|
83
89
|
novence whoami # plan, verification, usage against quota
|
|
84
90
|
novence dashboard # local account console (alias: novence console)
|
|
85
91
|
```
|
|
86
92
|
|
|
93
|
+
## Lost or revoked key
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
novence login --recover --email you@example.com # emails a code, reissues the key
|
|
97
|
+
novence login --recover --email you@example.com --revoke-all # also revoke named keys
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Recovery never uses the old key, so a revoked `./.novence-key` can't block it, and
|
|
101
|
+
it rewrites `./.novence-key` if one exists. Without a terminal (agents, CI), run it
|
|
102
|
+
once to send the code, then again with `--code <6 digits>`.
|
|
103
|
+
|
|
104
|
+
## Named keys
|
|
105
|
+
|
|
106
|
+
`novence keys create ci` saves the key to `~/.novence/keys/ci` (0600) and prints
|
|
107
|
+
only its prefix — the secret never reaches your terminal. Use it with
|
|
108
|
+
`--key-name ci` (or `NOVENCE_KEY_NAME=ci`), or in CI with
|
|
109
|
+
`NOVENCE_API_KEY="$(cat ~/.novence/keys/ci)"`.
|
|
110
|
+
|
|
111
|
+
| Flag | |
|
|
112
|
+
| --- | --- |
|
|
113
|
+
| `--output <path>` | Save somewhere else (0600). Refuses to overwrite without `--force` |
|
|
114
|
+
| `--print` | Print the secret to stdout instead: `KEY=$(novence keys create ci --print)` |
|
|
115
|
+
|
|
87
116
|
## Dashboard
|
|
88
117
|
|
|
89
118
|
`novence dashboard` fetches your console-kit, writes `~/.novence/console/novence-console.html`
|
|
@@ -130,14 +159,14 @@ Full agent brief: [novence.ai/llms-full.txt](https://novence.ai/llms-full.txt).
|
|
|
130
159
|
|
|
131
160
|
Set `NOVENCE_API_KEY` and pass `--no-input`.
|
|
132
161
|
|
|
133
|
-
Key lookup order: `
|
|
134
|
-
`~/.novence/credentials`. Never commit a key.
|
|
162
|
+
Key lookup order: `--key-name <name>` / `NOVENCE_KEY_NAME`, then `NOVENCE_API_KEY`,
|
|
163
|
+
then `./.novence-key`, then `~/.novence/credentials`. Never commit a key.
|
|
135
164
|
|
|
136
165
|
## Checks
|
|
137
166
|
|
|
138
167
|
Deploys run Lighthouse, axe, and link checks. On unverified accounts checks are
|
|
139
168
|
skipped — `checks.skipped` is `true` and `checks.passed` is `null`, which is not
|
|
140
|
-
a pass. Verify your email with `novence login` (or claim) to enable them.
|
|
169
|
+
a pass. Verify your email with `novence login --email you@example.com` (or claim) to enable them.
|
|
141
170
|
|
|
142
171
|
## Docs
|
|
143
172
|
|
package/dist/commands/checks.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Novence } from "@novence/sdk";
|
|
2
|
-
import { loadApiKey, resolveApiBase } from "../lib/config.js";
|
|
2
|
+
import { loadApiKey, notSignedInMessage, resolveApiBase } from "../lib/config.js";
|
|
3
3
|
import { flagString } from "../lib/args.js";
|
|
4
4
|
import { explain } from "../lib/errors.js";
|
|
5
5
|
import { fail, isJsonMode, json, result } from "../lib/ui.js";
|
|
@@ -10,7 +10,7 @@ export async function checksCommand(args) {
|
|
|
10
10
|
const apiBase = resolveApiBase(flagString(args.flags, "api"));
|
|
11
11
|
const apiKey = loadApiKey();
|
|
12
12
|
if (!apiKey)
|
|
13
|
-
fail(
|
|
13
|
+
fail(notSignedInMessage());
|
|
14
14
|
const client = new Novence({ apiKey, baseUrl: apiBase });
|
|
15
15
|
const wanted = args.positionals[0];
|
|
16
16
|
let projectId = wanted;
|
package/dist/commands/data.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { Novence } from "@novence/sdk";
|
|
3
|
-
import { loadApiKey, resolveApiBase } from "../lib/config.js";
|
|
3
|
+
import { loadApiKey, notSignedInMessage, resolveApiBase } from "../lib/config.js";
|
|
4
4
|
import { flagString } from "../lib/args.js";
|
|
5
5
|
import { explain } from "../lib/errors.js";
|
|
6
6
|
import { fail, isJsonMode, json, result, step } from "../lib/ui.js";
|
|
@@ -35,7 +35,7 @@ export async function dataCommand(args) {
|
|
|
35
35
|
const apiBase = resolveApiBase(flagString(args.flags, "api"));
|
|
36
36
|
const apiKey = loadApiKey();
|
|
37
37
|
if (!apiKey)
|
|
38
|
-
fail(
|
|
38
|
+
fail(notSignedInMessage());
|
|
39
39
|
const client = new Novence({ apiKey, baseUrl: apiBase });
|
|
40
40
|
const sub = args.positionals[0] ?? "put";
|
|
41
41
|
const projectFlag = flagString(args.flags, "project");
|
package/dist/commands/domains.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Novence } from "@novence/sdk";
|
|
2
|
-
import { loadApiKey, resolveApiBase } from "../lib/config.js";
|
|
2
|
+
import { loadApiKey, notSignedInMessage, resolveApiBase } from "../lib/config.js";
|
|
3
3
|
import { flagBool, flagString } from "../lib/args.js";
|
|
4
4
|
import { explain } from "../lib/errors.js";
|
|
5
5
|
import { fail, isJsonMode, json, result, step } from "../lib/ui.js";
|
|
@@ -20,7 +20,7 @@ export async function domainsCommand(args) {
|
|
|
20
20
|
const apiBase = resolveApiBase(flagString(args.flags, "api"));
|
|
21
21
|
const apiKey = loadApiKey();
|
|
22
22
|
if (!apiKey)
|
|
23
|
-
fail(
|
|
23
|
+
fail(notSignedInMessage());
|
|
24
24
|
const client = new Novence({ apiKey, baseUrl: apiBase });
|
|
25
25
|
const sub = args.positionals[0] ?? "status";
|
|
26
26
|
const projectFlag = flagString(args.flags, "project");
|
package/dist/commands/keys.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
1
3
|
import { Novence } from "@novence/sdk";
|
|
2
|
-
import { loadApiKey, resolveApiBase } from "../lib/config.js";
|
|
4
|
+
import { loadApiKey, namedKeyPath, notSignedInMessage, removeNamedKeyFile, resolveApiBase, saveNamedKey, } from "../lib/config.js";
|
|
3
5
|
import { flagBool, flagString } from "../lib/args.js";
|
|
4
6
|
import { explain } from "../lib/errors.js";
|
|
5
|
-
import { fail, isJsonMode, json, result, step } from "../lib/ui.js";
|
|
7
|
+
import { fail, isJsonMode, json, result, step, warn } from "../lib/ui.js";
|
|
6
8
|
export async function keysCommand(args) {
|
|
7
9
|
const apiBase = resolveApiBase(flagString(args.flags, "api"));
|
|
8
10
|
const apiKey = loadApiKey();
|
|
9
11
|
if (!apiKey)
|
|
10
|
-
fail(
|
|
12
|
+
fail(notSignedInMessage());
|
|
11
13
|
const client = new Novence({ apiKey, baseUrl: apiBase });
|
|
12
14
|
const sub = args.positionals[0] ?? "list";
|
|
13
15
|
try {
|
|
@@ -37,14 +39,42 @@ export async function keysCommand(args) {
|
|
|
37
39
|
const name = args.positionals[1];
|
|
38
40
|
if (!name)
|
|
39
41
|
fail("Usage: novence keys create <name> [--project <id>]\n e.g. novence keys create ci");
|
|
42
|
+
const outputFlag = flagString(args.flags, "output");
|
|
43
|
+
const print = flagBool(args.flags, "print") || outputFlag === "-";
|
|
44
|
+
const outputPath = outputFlag === "-" ? undefined : outputFlag;
|
|
45
|
+
const force = flagBool(args.flags, "force");
|
|
46
|
+
// Checked before the API call so a refused run never mints an orphan key.
|
|
47
|
+
if (!print && !outputPath && !isJsonMode() && !process.stdout.isTTY) {
|
|
48
|
+
fail("keys create no longer prints the secret. Capture it with --print:\n" +
|
|
49
|
+
` KEY=$(novence keys create ${name} --print)\n` +
|
|
50
|
+
" or write it to a file: --output <path>");
|
|
51
|
+
}
|
|
52
|
+
let target;
|
|
53
|
+
if (!print) {
|
|
54
|
+
target = outputPath ? resolve(outputPath) : namedKeyPath(name);
|
|
55
|
+
if (existsSync(target) && !force)
|
|
56
|
+
fail(`${target} already exists. Pass --force to replace it.`);
|
|
57
|
+
}
|
|
40
58
|
const res = await client.apiKeys.create({ name, projectId: flagString(args.flags, "project") });
|
|
59
|
+
if (print) {
|
|
60
|
+
if (isJsonMode()) {
|
|
61
|
+
json({ ok: true, ...res });
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
result(res.apiKey);
|
|
65
|
+
step(`Key "${res.key.name}" (${res.key.scope}) — shown once. Store it in a secret manager.`);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const savedTo = saveNamedKey(name, res.apiKey, { outputPath, force });
|
|
41
69
|
if (isJsonMode()) {
|
|
42
|
-
json({ ok: true,
|
|
70
|
+
json({ ok: true, key: res.key, savedTo });
|
|
43
71
|
return;
|
|
44
72
|
}
|
|
45
|
-
|
|
46
|
-
step(`Key "${res.key.name}" (${res.key.scope})
|
|
73
|
+
const inCi = `NOVENCE_API_KEY="$(cat ${savedTo})"`;
|
|
74
|
+
step(`Key "${res.key.name}" (${res.key.scope}) · ${res.key.keyPrefix}… saved to ${savedTo} (0600).`);
|
|
75
|
+
step(outputPath ? `In CI: ${inCi}` : `Use it here with --key-name ${name}, or in CI with ${inCi}.`);
|
|
47
76
|
step("It keeps working when `novence login --recover` replaces your primary key.");
|
|
77
|
+
result(savedTo);
|
|
48
78
|
return;
|
|
49
79
|
}
|
|
50
80
|
if (sub === "revoke") {
|
|
@@ -52,14 +82,34 @@ export async function keysCommand(args) {
|
|
|
52
82
|
if (!id)
|
|
53
83
|
fail("Usage: novence keys revoke <id> [--force]\n Find ids with: novence keys list");
|
|
54
84
|
const force = flagBool(args.flags, "force");
|
|
85
|
+
// Only used to clean up ~/.novence/keys/<name>; never block the revoke on it.
|
|
86
|
+
const { keys } = await client.apiKeys
|
|
87
|
+
.list({ projectId: flagString(args.flags, "project"), includeRevoked: true })
|
|
88
|
+
.catch(() => ({ keys: [] }));
|
|
89
|
+
const revoked = keys.find((k) => k.id === id);
|
|
55
90
|
const res = await client.apiKeys.revoke(id, { force });
|
|
91
|
+
const fileOutcome = revoked ? removeNamedKeyFile(revoked.name, revoked.keyPrefix) : "absent";
|
|
56
92
|
if (isJsonMode()) {
|
|
57
|
-
json({ ok: true, ...res });
|
|
93
|
+
json({ ok: true, ...res, keyFile: fileOutcome });
|
|
58
94
|
return;
|
|
59
95
|
}
|
|
60
96
|
result(`Revoked ${id}`);
|
|
97
|
+
switch (fileOutcome) {
|
|
98
|
+
case "removed":
|
|
99
|
+
step(`Deleted ${namedKeyPath(revoked.name)}.`);
|
|
100
|
+
break;
|
|
101
|
+
case "mismatch":
|
|
102
|
+
warn(`Kept ${namedKeyPath(revoked.name)}: it holds a different key than the one revoked.`);
|
|
103
|
+
break;
|
|
104
|
+
case "absent":
|
|
105
|
+
break;
|
|
106
|
+
default: {
|
|
107
|
+
const unreachable = fileOutcome;
|
|
108
|
+
return unreachable;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
61
111
|
if (force)
|
|
62
|
-
step("That was the key this machine uses — run `novence login
|
|
112
|
+
step("That was the key this machine uses — run `novence login --recover --email you@example.com`.");
|
|
63
113
|
return;
|
|
64
114
|
}
|
|
65
115
|
fail(`Unknown subcommand: ${sub}\n Usage: novence keys list | novence keys create <name> | novence keys revoke <id>`);
|
package/dist/commands/login.js
CHANGED
|
@@ -1,45 +1,102 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { Novence, NovenceError, reissueApiKey, resendVerification, verifyEmail, } from "@novence/sdk";
|
|
3
|
+
import { claimAndMaybeVerify, ensureApiKey, isAccountExistsError, recoverHint } from "../lib/auth.js";
|
|
4
|
+
import { credentialsPath, keySource, loadApiKey, localKeyPath, resolveApiBase, saveApiKey, } from "../lib/config.js";
|
|
5
5
|
import { flagBool, flagString } from "../lib/args.js";
|
|
6
|
-
import { fail, prompt, result, step, withSpin } from "../lib/ui.js";
|
|
6
|
+
import { fail, isJsonMode, prompt, result, step, warn, withSpin } from "../lib/ui.js";
|
|
7
7
|
export async function loginCommand(args) {
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
const recoverFlag = args.flags.recover;
|
|
9
|
+
// `--recover you@x.com` parses as a value; treat it as the email.
|
|
10
|
+
const recoverEmail = typeof recoverFlag === "string" && recoverFlag.includes("@") ? recoverFlag : undefined;
|
|
11
|
+
const ctx = {
|
|
12
|
+
apiBase: resolveApiBase(flagString(args.flags, "api")),
|
|
13
|
+
email: flagString(args.flags, "email") ?? recoverEmail,
|
|
14
|
+
code: flagString(args.flags, "code"),
|
|
15
|
+
revokeAll: flagBool(args.flags, "revoke-all"),
|
|
16
|
+
canPrompt: Boolean(process.stdin.isTTY) && !isJsonMode() && !flagBool(args.flags, "no-input"),
|
|
17
|
+
};
|
|
18
|
+
if (recoverFlag !== undefined)
|
|
19
|
+
return recoverKey(ctx);
|
|
20
|
+
const storedKey = loadApiKey();
|
|
21
|
+
if (!storedKey)
|
|
22
|
+
return signInWithEmail(ctx);
|
|
23
|
+
let snap;
|
|
24
|
+
try {
|
|
25
|
+
snap = await new Novence({ apiKey: storedKey, baseUrl: ctx.apiBase }).account.get();
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
if (!(err instanceof NovenceError && err.status === 401))
|
|
29
|
+
throw err;
|
|
30
|
+
const lead = `The stored key (${keySource()}) was rejected — it may be revoked.`;
|
|
31
|
+
if (!ctx.canPrompt)
|
|
32
|
+
fail(recoverHint(ctx.email, lead));
|
|
33
|
+
warn(lead);
|
|
34
|
+
return recoverKey(ctx);
|
|
15
35
|
}
|
|
16
|
-
const address =
|
|
17
|
-
if (!address.includes("@"))
|
|
18
|
-
fail(`Not a valid email: ${address}`);
|
|
19
|
-
const apiKey = loadApiKey();
|
|
20
|
-
const client = new Novence({ apiKey, baseUrl: apiBase });
|
|
21
|
-
const snap = await client.account.get();
|
|
36
|
+
const address = await requireEmail(ctx);
|
|
22
37
|
if (!snap.email) {
|
|
23
|
-
await claimAndMaybeVerify({ apiKey, apiBase, email: address, interactive:
|
|
38
|
+
await claimAndMaybeVerify({ apiKey: storedKey, apiBase: ctx.apiBase, email: address, interactive: ctx.canPrompt });
|
|
24
39
|
result(`${address} claimed.`);
|
|
25
40
|
return;
|
|
26
41
|
}
|
|
27
|
-
const
|
|
28
|
-
|
|
42
|
+
const code = ctx.code ?? (await sendCode(address, ctx));
|
|
43
|
+
await verifyEmail(address, code, ctx.apiBase);
|
|
44
|
+
result(`${address} verified.`);
|
|
45
|
+
}
|
|
46
|
+
async function signInWithEmail(ctx) {
|
|
47
|
+
const email = await requireEmail(ctx);
|
|
48
|
+
try {
|
|
49
|
+
await ensureApiKey({ apiBase: ctx.apiBase, email, interactive: ctx.canPrompt });
|
|
50
|
+
result(`Signed in. Key stored at ${credentialsPath()}`);
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
if (!isAccountExistsError(err))
|
|
54
|
+
throw err;
|
|
55
|
+
if (!ctx.canPrompt)
|
|
56
|
+
fail(err.message);
|
|
57
|
+
step(`${email} already has an account — recovering its key with an email code.`);
|
|
58
|
+
await recoverKey({ ...ctx, email });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** Email OTP → POST /v1/auth/reissue-key. Never reads or sends the stored key. */
|
|
62
|
+
async function recoverKey(ctx) {
|
|
63
|
+
const email = await requireEmail(ctx);
|
|
64
|
+
const code = ctx.code ?? (await sendCode(email, ctx, true));
|
|
65
|
+
const issued = await withSpin("Reissuing your key…", () => reissueApiKey(email, code, ctx.apiBase, { revokeAll: ctx.revokeAll }));
|
|
66
|
+
const hadLocalKey = existsSync(localKeyPath());
|
|
67
|
+
const path = saveApiKey(issued.apiKey);
|
|
68
|
+
if (hadLocalKey)
|
|
69
|
+
step(`Also replaced ${localKeyPath()} (it would otherwise shadow the new key).`);
|
|
70
|
+
if (process.env.NOVENCE_API_KEY?.trim()) {
|
|
71
|
+
warn("NOVENCE_API_KEY is set and overrides the new key. Update or unset it.");
|
|
72
|
+
}
|
|
73
|
+
if (issued.remainingKeys.length) {
|
|
74
|
+
step(`Still active: ${issued.remainingKeys.map((k) => `${k.name} (${k.keyPrefix})`).join(", ")}. ` +
|
|
75
|
+
"Use --revoke-all if a key may have leaked.");
|
|
76
|
+
}
|
|
77
|
+
result(`Key reissued for account ${issued.accountId} → ${path}`);
|
|
78
|
+
}
|
|
79
|
+
/** Sends an OTP. Without a TTY, stops and says how to finish with --code. */
|
|
80
|
+
async function sendCode(email, ctx, recovering = false) {
|
|
81
|
+
const sent = await withSpin(`Sending a code to ${email}…`, () => resendVerification(email, ctx.apiBase));
|
|
82
|
+
if (sent.verificationCode)
|
|
83
|
+
return sent.verificationCode;
|
|
84
|
+
if (!ctx.canPrompt) {
|
|
85
|
+
const rerun = recovering
|
|
86
|
+
? `novence login --recover --email ${email}${ctx.revokeAll ? " --revoke-all" : ""} --code <6 digits>`
|
|
87
|
+
: `novence login --email ${email} --code <6 digits>`;
|
|
88
|
+
fail(`Code sent to ${email}. Re-run:\n ${rerun}`);
|
|
89
|
+
}
|
|
90
|
+
const code = await prompt(" 6-digit code from your email: ");
|
|
29
91
|
if (!code)
|
|
30
92
|
fail("No code entered.");
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
await verifyEmail(address, code, apiBase);
|
|
44
|
-
result(`${address} verified.`);
|
|
93
|
+
return code;
|
|
94
|
+
}
|
|
95
|
+
async function requireEmail(ctx) {
|
|
96
|
+
const email = ctx.email ?? (ctx.canPrompt ? await prompt(" Email: ") : undefined);
|
|
97
|
+
if (!email)
|
|
98
|
+
fail("Pass --email you@example.com.");
|
|
99
|
+
if (!email.includes("@"))
|
|
100
|
+
fail(`Not a valid email: ${email}`);
|
|
101
|
+
return email;
|
|
45
102
|
}
|
package/dist/commands/open.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Novence } from "@novence/sdk";
|
|
2
|
-
import { loadApiKey, resolveApiBase } from "../lib/config.js";
|
|
2
|
+
import { loadApiKey, notSignedInMessage, resolveApiBase } from "../lib/config.js";
|
|
3
3
|
import { flagString } from "../lib/args.js";
|
|
4
4
|
import { openInBrowser } from "../lib/browser.js";
|
|
5
5
|
import { explain } from "../lib/errors.js";
|
|
@@ -8,7 +8,7 @@ export async function openCommand(args) {
|
|
|
8
8
|
const apiBase = resolveApiBase(flagString(args.flags, "api"));
|
|
9
9
|
const apiKey = loadApiKey();
|
|
10
10
|
if (!apiKey)
|
|
11
|
-
fail(
|
|
11
|
+
fail(notSignedInMessage());
|
|
12
12
|
const client = new Novence({ apiKey, baseUrl: apiBase });
|
|
13
13
|
const wanted = args.positionals[0];
|
|
14
14
|
try {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { Novence } from "@novence/sdk";
|
|
2
|
-
import { loadApiKey, resolveApiBase } from "../lib/config.js";
|
|
2
|
+
import { loadApiKey, notSignedInMessage, resolveApiBase } from "../lib/config.js";
|
|
3
3
|
import { flagString } from "../lib/args.js";
|
|
4
4
|
import { fail, isJsonMode, json, result } from "../lib/ui.js";
|
|
5
5
|
export async function projectsCommand(args) {
|
|
6
6
|
const apiBase = resolveApiBase(flagString(args.flags, "api"));
|
|
7
7
|
const apiKey = loadApiKey();
|
|
8
8
|
if (!apiKey)
|
|
9
|
-
fail(
|
|
9
|
+
fail(notSignedInMessage());
|
|
10
10
|
const client = new Novence({ apiKey, baseUrl: apiBase });
|
|
11
11
|
const { projects } = await client.projects.list();
|
|
12
12
|
if (isJsonMode()) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Novence } from "@novence/sdk";
|
|
2
|
-
import { loadApiKey, resolveApiBase } from "../lib/config.js";
|
|
2
|
+
import { loadApiKey, notSignedInMessage, resolveApiBase } from "../lib/config.js";
|
|
3
3
|
import { flagString } from "../lib/args.js";
|
|
4
4
|
import { explain } from "../lib/errors.js";
|
|
5
5
|
import { fail, isJsonMode, json, result, siteUrl, step } from "../lib/ui.js";
|
|
@@ -7,7 +7,7 @@ export async function rollbackCommand(args) {
|
|
|
7
7
|
const apiBase = resolveApiBase(flagString(args.flags, "api"));
|
|
8
8
|
const apiKey = loadApiKey();
|
|
9
9
|
if (!apiKey)
|
|
10
|
-
fail(
|
|
10
|
+
fail(notSignedInMessage());
|
|
11
11
|
const client = new Novence({ apiKey, baseUrl: apiBase });
|
|
12
12
|
const wanted = flagString(args.flags, "project") ?? args.positionals[0];
|
|
13
13
|
const deploymentId = flagString(args.flags, "deployment");
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Novence } from "@novence/sdk";
|
|
2
|
-
import { loadApiKey, resolveApiBase } from "../lib/config.js";
|
|
2
|
+
import { loadApiKey, notSignedInMessage, resolveApiBase } from "../lib/config.js";
|
|
3
3
|
import { flagString } from "../lib/args.js";
|
|
4
4
|
import { explain } from "../lib/errors.js";
|
|
5
5
|
import { fail, isJsonMode, json, result, step } from "../lib/ui.js";
|
|
@@ -19,7 +19,7 @@ export async function siteMcpCommand(args) {
|
|
|
19
19
|
const apiBase = resolveApiBase(flagString(args.flags, "api"));
|
|
20
20
|
const apiKey = loadApiKey();
|
|
21
21
|
if (!apiKey)
|
|
22
|
-
fail(
|
|
22
|
+
fail(notSignedInMessage());
|
|
23
23
|
const client = new Novence({ apiKey, baseUrl: apiBase });
|
|
24
24
|
const sub = (args.positionals[0] ?? "status").toLowerCase();
|
|
25
25
|
const projectFlag = flagString(args.flags, "project");
|
package/dist/commands/whoami.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { Novence } from "@novence/sdk";
|
|
2
|
-
import { keySource, loadApiKey, resolveApiBase } from "../lib/config.js";
|
|
2
|
+
import { keySource, loadApiKey, notSignedInMessage, resolveApiBase } from "../lib/config.js";
|
|
3
3
|
import { flagString } from "../lib/args.js";
|
|
4
4
|
import { fail, isJsonMode, json, result } from "../lib/ui.js";
|
|
5
5
|
export async function whoamiCommand(args) {
|
|
6
6
|
const apiBase = resolveApiBase(flagString(args.flags, "api"));
|
|
7
7
|
const apiKey = loadApiKey();
|
|
8
8
|
if (!apiKey)
|
|
9
|
-
fail(
|
|
9
|
+
fail(notSignedInMessage());
|
|
10
10
|
const client = new Novence({ apiKey, baseUrl: apiBase });
|
|
11
11
|
const snap = await client.account.get();
|
|
12
12
|
const quotas = await client.billing.quotas();
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { readFileSync } from "node:fs";
|
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { assertKnownFlags, flagBool, flagString, parseArgs } from "./lib/args.js";
|
|
6
|
-
import { resolveApiBase } from "./lib/config.js";
|
|
6
|
+
import { resolveApiBase, setKeyName } from "./lib/config.js";
|
|
7
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";
|
|
@@ -32,7 +32,7 @@ const HELP = `
|
|
|
32
32
|
novence domains add <host> Attach a custom domain (prints DNS records)
|
|
33
33
|
novence domains status Custom domain state
|
|
34
34
|
novence keys list API keys on the account
|
|
35
|
-
novence keys create <name> Extra named key (e.g. ci)
|
|
35
|
+
novence keys create <name> Extra named key (e.g. ci), saved to ~/.novence/keys/<name>
|
|
36
36
|
novence keys revoke <id> Revoke a key
|
|
37
37
|
novence projects List projects and their URLs
|
|
38
38
|
novence login Sign in, verify email, or recover a key
|
|
@@ -40,6 +40,18 @@ const HELP = `
|
|
|
40
40
|
novence whoami Plan, verification, and usage
|
|
41
41
|
novence dashboard Open your local account console (alias: console)
|
|
42
42
|
|
|
43
|
+
Login
|
|
44
|
+
novence login --email <email> Sign in (new account) or verify your email
|
|
45
|
+
novence login --recover --email <email> Lost or revoked key: email code → new key
|
|
46
|
+
--revoke-all Also revoke named keys (use if a key may have leaked)
|
|
47
|
+
--code <6 digits> Finish without a prompt (agents, --no-input)
|
|
48
|
+
|
|
49
|
+
Keys
|
|
50
|
+
novence keys create ci Save to ~/.novence/keys/ci (0600); prints only the prefix
|
|
51
|
+
--output <path> Save somewhere else (0600; --force to replace)
|
|
52
|
+
--print Print the secret to stdout instead, e.g. KEY=$(novence keys create ci --print)
|
|
53
|
+
novence keys revoke <id> Also deletes the matching ~/.novence/keys/<name>
|
|
54
|
+
|
|
43
55
|
Dashboard
|
|
44
56
|
novence dashboard Serve the console on the first free 127.0.0.1 port from 8765
|
|
45
57
|
--port <n> First port to try (default 8765)
|
|
@@ -70,6 +82,7 @@ const HELP = `
|
|
|
70
82
|
--json Machine-readable output (implies --no-input)
|
|
71
83
|
--no-input Never prompt; fail instead
|
|
72
84
|
--api <url> Control plane (default https://api.novence.ai)
|
|
85
|
+
--key-name <name> Use ~/.novence/keys/<name> (or set NOVENCE_KEY_NAME)
|
|
73
86
|
-h, --help Show this help
|
|
74
87
|
-v, --version Show the version
|
|
75
88
|
|
|
@@ -77,7 +90,8 @@ const HELP = `
|
|
|
77
90
|
URL=$(novence deploy ./dist)
|
|
78
91
|
|
|
79
92
|
Auth
|
|
80
|
-
Reads NOVENCE_API_KEY, then ./.novence-key,
|
|
93
|
+
Reads --key-name / NOVENCE_KEY_NAME, then NOVENCE_API_KEY, then ./.novence-key,
|
|
94
|
+
then ~/.novence/credentials. Lost key: novence login --recover --email <email>
|
|
81
95
|
NOVENCE_ATTRIBUTION, when set to JSON, is sent with the first signup.
|
|
82
96
|
|
|
83
97
|
Docs https://novence.ai/docs
|
|
@@ -107,10 +121,10 @@ async function main() {
|
|
|
107
121
|
open: [],
|
|
108
122
|
checks: [],
|
|
109
123
|
domains: ["project", "primary", "redirect"],
|
|
110
|
-
keys: ["project", "include-revoked", "force"],
|
|
124
|
+
keys: ["project", "include-revoked", "force", "output", "print"],
|
|
111
125
|
projects: [],
|
|
112
126
|
ls: [],
|
|
113
|
-
login: ["email", "recover", "revoke-all"],
|
|
127
|
+
login: ["email", "recover", "revoke-all", "code"],
|
|
114
128
|
whoami: [],
|
|
115
129
|
rollback: ["project", "deployment"],
|
|
116
130
|
data: ["project"],
|
|
@@ -124,6 +138,7 @@ async function main() {
|
|
|
124
138
|
if (problem)
|
|
125
139
|
fail(problem);
|
|
126
140
|
}
|
|
141
|
+
setKeyName(flagString(args.flags, "key-name"));
|
|
127
142
|
switch (args.command) {
|
|
128
143
|
case "deploy":
|
|
129
144
|
return deployCommand(args);
|
package/dist/lib/args.js
CHANGED
|
@@ -41,7 +41,7 @@ export function flagBool(flags, name) {
|
|
|
41
41
|
return flags[name] === true || flags[name] === "true";
|
|
42
42
|
}
|
|
43
43
|
/** Flags every command accepts. */
|
|
44
|
-
export const GLOBAL_FLAGS = ["json", "no-input", "api", "help", "version", "quiet"];
|
|
44
|
+
export const GLOBAL_FLAGS = ["json", "no-input", "api", "help", "version", "quiet", "key-name"];
|
|
45
45
|
/**
|
|
46
46
|
* Reject unrecognised flags. Without this a typo like `--nmae "My Site"` is
|
|
47
47
|
* silently dropped and the user gets a differently-named project with no
|
package/dist/lib/auth.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
export declare function isAccountExistsError(err: unknown): boolean;
|
|
2
|
+
/** The one command that restores a lost or revoked key. */
|
|
3
|
+
export declare function recoverHint(email: string | undefined, lead: string): string;
|
|
1
4
|
export interface Credentials {
|
|
2
5
|
apiKey: string;
|
|
3
6
|
/** True when this run created the account, so callers can tailor next steps. */
|
package/dist/lib/auth.js
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
|
-
import { bootstrapAccount, claimAccount, createDemoAccount, verifyEmail, } from "@novence/sdk";
|
|
1
|
+
import { bootstrapAccount, claimAccount, createDemoAccount, NovenceError, verifyEmail, } from "@novence/sdk";
|
|
2
2
|
import { loadApiKey, saveApiKey } from "./config.js";
|
|
3
3
|
import { fail, prompt, step, warn, withSpin } from "./ui.js";
|
|
4
|
+
const ACCOUNT_EXISTS = "ACCOUNT_EXISTS";
|
|
5
|
+
export function isAccountExistsError(err) {
|
|
6
|
+
return err?.code === ACCOUNT_EXISTS;
|
|
7
|
+
}
|
|
8
|
+
/** The one command that restores a lost or revoked key. */
|
|
9
|
+
export function recoverHint(email, lead) {
|
|
10
|
+
const who = email ?? "you@example.com";
|
|
11
|
+
return `${email ? `${email} ` : ""}${lead}\n Recover the key with an email code: novence login --recover --email ${who}`;
|
|
12
|
+
}
|
|
4
13
|
function attributionFromEnv() {
|
|
5
14
|
const raw = process.env.NOVENCE_ATTRIBUTION?.trim();
|
|
6
15
|
let parsed = {};
|
|
@@ -42,10 +51,11 @@ async function bootstrapWithEmail(email, apiBase, interactive) {
|
|
|
42
51
|
}
|
|
43
52
|
catch (err) {
|
|
44
53
|
const message = err instanceof Error ? err.message : String(err);
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
54
|
+
const status = err instanceof NovenceError ? err.status : undefined;
|
|
55
|
+
if (status === 409 || /exists|already/i.test(message)) {
|
|
56
|
+
throw Object.assign(new Error(recoverHint(email, "already has an account.")), {
|
|
57
|
+
code: ACCOUNT_EXISTS,
|
|
58
|
+
});
|
|
49
59
|
}
|
|
50
60
|
throw err;
|
|
51
61
|
}
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -1,13 +1,39 @@
|
|
|
1
1
|
export declare const DEFAULT_API = "https://api.novence.ai";
|
|
2
|
+
/** Project-local key file; wins over ~/.novence/credentials. */
|
|
3
|
+
export declare function localKeyPath(): string;
|
|
2
4
|
/** Explicit flag wins, then env, then the public control plane. */
|
|
3
5
|
export declare function resolveApiBase(flag?: string): string;
|
|
6
|
+
/** Global `--key-name`; falls back to NOVENCE_KEY_NAME. */
|
|
7
|
+
export declare function setKeyName(name: string | undefined): void;
|
|
8
|
+
export declare function isValidKeyFileName(name: string): boolean;
|
|
9
|
+
export declare function namedKeyPath(name: string): string;
|
|
4
10
|
/**
|
|
5
|
-
* Key lookup order:
|
|
6
|
-
*
|
|
11
|
+
* Key lookup order: --key-name / NOVENCE_KEY_NAME (~/.novence/keys/<name>),
|
|
12
|
+
* NOVENCE_API_KEY, a project-local .novence-key, then ~/.novence/credentials.
|
|
7
13
|
*/
|
|
8
14
|
export declare function loadApiKey(): string | null;
|
|
9
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Write a credential at mode 0600 from the first byte. Without `overwrite`
|
|
17
|
+
* an existing file is an error; with it, a temp file is renamed over the
|
|
18
|
+
* target so a previously looser mode is not inherited.
|
|
19
|
+
*/
|
|
20
|
+
export declare function writeSecretFile(path: string, value: string, opts: {
|
|
21
|
+
overwrite: boolean;
|
|
22
|
+
}): void;
|
|
23
|
+
/**
|
|
24
|
+
* Store the primary key in ~/.novence/credentials. An existing ./.novence-key
|
|
25
|
+
* is replaced too, since it would otherwise shadow the new key.
|
|
26
|
+
*/
|
|
10
27
|
export declare function saveApiKey(apiKey: string): string;
|
|
28
|
+
/** Store a named key at ~/.novence/keys/<name> or `outputPath`. Returns the path written. */
|
|
29
|
+
export declare function saveNamedKey(name: string, apiKey: string, opts?: {
|
|
30
|
+
outputPath?: string;
|
|
31
|
+
force?: boolean;
|
|
32
|
+
}): string;
|
|
33
|
+
export type NamedKeyRemoval = "removed" | "mismatch" | "absent";
|
|
34
|
+
/** Delete ~/.novence/keys/<name> only if it holds the key with `keyPrefix`. */
|
|
35
|
+
export declare function removeNamedKeyFile(name: string, keyPrefix: string): NamedKeyRemoval;
|
|
11
36
|
export declare function credentialsPath(): string;
|
|
12
37
|
/** Where loadApiKey() found the key, for display. */
|
|
13
38
|
export declare function keySource(): string;
|
|
39
|
+
export declare function notSignedInMessage(): string;
|
package/dist/lib/config.js
CHANGED
|
@@ -1,54 +1,146 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
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");
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
6
4
|
export const DEFAULT_API = "https://api.novence.ai";
|
|
5
|
+
const KEY_FILE_NAME = /^[A-Za-z0-9_-]+$/;
|
|
6
|
+
let keyNameOverride;
|
|
7
|
+
/** Resolved per call so HOME changes (tests, sudo -E) are honoured. */
|
|
8
|
+
function configDir() {
|
|
9
|
+
return join(homedir(), ".novence");
|
|
10
|
+
}
|
|
11
|
+
function credentialsFile() {
|
|
12
|
+
return join(configDir(), "credentials");
|
|
13
|
+
}
|
|
14
|
+
function keysDir() {
|
|
15
|
+
return join(configDir(), "keys");
|
|
16
|
+
}
|
|
17
|
+
/** Project-local key file; wins over ~/.novence/credentials. */
|
|
18
|
+
export function localKeyPath() {
|
|
19
|
+
return join(process.cwd(), ".novence-key");
|
|
20
|
+
}
|
|
7
21
|
/** Explicit flag wins, then env, then the public control plane. */
|
|
8
22
|
export function resolveApiBase(flag) {
|
|
9
23
|
const raw = flag ?? process.env.NOVENCE_BASE_URL ?? process.env.API_BASE_URL ?? DEFAULT_API;
|
|
10
24
|
return raw.replace(/\/$/, "");
|
|
11
25
|
}
|
|
26
|
+
/** Global `--key-name`; falls back to NOVENCE_KEY_NAME. */
|
|
27
|
+
export function setKeyName(name) {
|
|
28
|
+
keyNameOverride = name?.trim() || undefined;
|
|
29
|
+
}
|
|
30
|
+
function selectedKeyName() {
|
|
31
|
+
return keyNameOverride ?? (process.env.NOVENCE_KEY_NAME?.trim() || undefined);
|
|
32
|
+
}
|
|
33
|
+
export function isValidKeyFileName(name) {
|
|
34
|
+
return KEY_FILE_NAME.test(name);
|
|
35
|
+
}
|
|
36
|
+
export function namedKeyPath(name) {
|
|
37
|
+
if (!isValidKeyFileName(name)) {
|
|
38
|
+
throw new Error(`Key name "${name}" can't be used as a file name (letters, digits, _ and - only). Pass --output <path>.`);
|
|
39
|
+
}
|
|
40
|
+
return join(keysDir(), name);
|
|
41
|
+
}
|
|
42
|
+
function readKeyFile(path) {
|
|
43
|
+
if (!existsSync(path))
|
|
44
|
+
return null;
|
|
45
|
+
return readFileSync(path, "utf8").trim() || null;
|
|
46
|
+
}
|
|
12
47
|
/**
|
|
13
|
-
* Key lookup order:
|
|
14
|
-
*
|
|
48
|
+
* Key lookup order: --key-name / NOVENCE_KEY_NAME (~/.novence/keys/<name>),
|
|
49
|
+
* NOVENCE_API_KEY, a project-local .novence-key, then ~/.novence/credentials.
|
|
15
50
|
*/
|
|
16
51
|
export function loadApiKey() {
|
|
52
|
+
const name = selectedKeyName();
|
|
53
|
+
if (name) {
|
|
54
|
+
const path = namedKeyPath(name);
|
|
55
|
+
const key = readKeyFile(path);
|
|
56
|
+
if (!key) {
|
|
57
|
+
throw new Error(`No key named "${name}" at ${path}.\n Create it with: novence keys create ${name}`);
|
|
58
|
+
}
|
|
59
|
+
return key;
|
|
60
|
+
}
|
|
17
61
|
const fromEnv = process.env.NOVENCE_API_KEY?.trim();
|
|
18
62
|
if (fromEnv)
|
|
19
63
|
return fromEnv;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
64
|
+
return readKeyFile(localKeyPath()) ?? readKeyFile(credentialsFile());
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Write a credential at mode 0600 from the first byte. Without `overwrite`
|
|
68
|
+
* an existing file is an error; with it, a temp file is renamed over the
|
|
69
|
+
* target so a previously looser mode is not inherited.
|
|
70
|
+
*/
|
|
71
|
+
export function writeSecretFile(path, value, opts) {
|
|
72
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
73
|
+
const body = value.trim() + "\n";
|
|
74
|
+
if (!opts.overwrite) {
|
|
75
|
+
try {
|
|
76
|
+
writeFileSync(path, body, { mode: 0o600, flag: "wx" });
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
if (err.code === "EEXIST") {
|
|
80
|
+
throw new Error(`${path} already exists. Pass --force to replace it.`);
|
|
81
|
+
}
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
25
85
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
86
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
87
|
+
writeFileSync(tmp, body, { mode: 0o600, flag: "wx" });
|
|
88
|
+
try {
|
|
89
|
+
renameSync(tmp, path);
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
rmSync(tmp, { force: true });
|
|
93
|
+
throw err;
|
|
30
94
|
}
|
|
31
|
-
return null;
|
|
32
95
|
}
|
|
33
|
-
/**
|
|
96
|
+
/**
|
|
97
|
+
* Store the primary key in ~/.novence/credentials. An existing ./.novence-key
|
|
98
|
+
* is replaced too, since it would otherwise shadow the new key.
|
|
99
|
+
*/
|
|
34
100
|
export function saveApiKey(apiKey) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
101
|
+
const path = credentialsFile();
|
|
102
|
+
writeSecretFile(path, apiKey, { overwrite: true });
|
|
103
|
+
if (existsSync(localKeyPath()))
|
|
104
|
+
writeSecretFile(localKeyPath(), apiKey, { overwrite: true });
|
|
105
|
+
return path;
|
|
106
|
+
}
|
|
107
|
+
/** Store a named key at ~/.novence/keys/<name> or `outputPath`. Returns the path written. */
|
|
108
|
+
export function saveNamedKey(name, apiKey, opts = {}) {
|
|
109
|
+
const path = opts.outputPath ? resolve(opts.outputPath) : namedKeyPath(name);
|
|
110
|
+
writeSecretFile(path, apiKey, { overwrite: Boolean(opts.force) });
|
|
111
|
+
return path;
|
|
112
|
+
}
|
|
113
|
+
/** Delete ~/.novence/keys/<name> only if it holds the key with `keyPrefix`. */
|
|
114
|
+
export function removeNamedKeyFile(name, keyPrefix) {
|
|
115
|
+
if (!isValidKeyFileName(name))
|
|
116
|
+
return "absent";
|
|
117
|
+
const path = namedKeyPath(name);
|
|
118
|
+
const key = readKeyFile(path);
|
|
119
|
+
if (!key)
|
|
120
|
+
return "absent";
|
|
121
|
+
if (!key.startsWith(keyPrefix))
|
|
122
|
+
return "mismatch";
|
|
123
|
+
unlinkSync(path);
|
|
124
|
+
return "removed";
|
|
39
125
|
}
|
|
40
126
|
export function credentialsPath() {
|
|
41
|
-
return
|
|
127
|
+
return credentialsFile();
|
|
42
128
|
}
|
|
43
129
|
/** Where loadApiKey() found the key, for display. */
|
|
44
130
|
export function keySource() {
|
|
131
|
+
const name = selectedKeyName();
|
|
132
|
+
if (name)
|
|
133
|
+
return namedKeyPath(name);
|
|
45
134
|
if (process.env.NOVENCE_API_KEY?.trim())
|
|
46
135
|
return "NOVENCE_API_KEY";
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
return CREDENTIALS_FILE;
|
|
52
|
-
}
|
|
136
|
+
if (readKeyFile(localKeyPath()))
|
|
137
|
+
return localKeyPath();
|
|
138
|
+
if (readKeyFile(credentialsFile()))
|
|
139
|
+
return credentialsFile();
|
|
53
140
|
return "(none)";
|
|
54
141
|
}
|
|
142
|
+
export function notSignedInMessage() {
|
|
143
|
+
return (`Not signed in. Looked in NOVENCE_API_KEY, ${localKeyPath()}, ${credentialsFile()}.\n` +
|
|
144
|
+
" New account: novence login --email you@example.com\n" +
|
|
145
|
+
" Lost key: novence login --recover --email you@example.com");
|
|
146
|
+
}
|
package/dist/lib/errors.js
CHANGED
|
@@ -27,10 +27,13 @@ export function explain(err, ctx) {
|
|
|
27
27
|
const { message: detail, status } = err;
|
|
28
28
|
switch (status) {
|
|
29
29
|
case 401:
|
|
30
|
-
case 403:
|
|
31
30
|
return (`${detail}\n` +
|
|
32
31
|
" Your API key was rejected. It may be revoked, or from another environment.\n" +
|
|
33
|
-
"
|
|
32
|
+
" Lost or revoked key: novence login --recover --email you@example.com");
|
|
33
|
+
case 403:
|
|
34
|
+
return (`${detail}\n` +
|
|
35
|
+
" This key lacks permission for that action (project or member keys are limited).\n" +
|
|
36
|
+
" Check which key is in use: novence whoami");
|
|
34
37
|
case 402:
|
|
35
38
|
return (`${detail}\n` +
|
|
36
39
|
" Redeploy an existing project instead: novence deploy --project <id>\n" +
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "novence",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "CLI for Novence
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "CLI for Novence \u2014 deploy a static site to the edge in one command.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -39,15 +39,18 @@
|
|
|
39
39
|
"publishConfig": {
|
|
40
40
|
"access": "public"
|
|
41
41
|
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsc -p tsconfig.json",
|
|
44
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
45
|
+
"test": "tsx --test src/commands/login.test.ts src/commands/keys.test.ts",
|
|
46
|
+
"prepublishOnly": "pnpm run build"
|
|
47
|
+
},
|
|
42
48
|
"dependencies": {
|
|
43
49
|
"@novence/sdk": "^0.3.0"
|
|
44
50
|
},
|
|
45
51
|
"devDependencies": {
|
|
46
52
|
"@types/node": "^22.10.2",
|
|
53
|
+
"tsx": "^4.19.2",
|
|
47
54
|
"typescript": "^5.7.2"
|
|
48
|
-
},
|
|
49
|
-
"scripts": {
|
|
50
|
-
"build": "tsc -p tsconfig.json",
|
|
51
|
-
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
52
55
|
}
|
|
53
|
-
}
|
|
56
|
+
}
|