novence 0.1.1 → 0.1.3
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 +1 -0
- package/dist/commands/deploy.js +69 -28
- package/dist/commands/rollback.d.ts +2 -0
- package/dist/commands/rollback.js +46 -0
- package/dist/index.js +12 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -34,6 +34,7 @@ Requires Node 18+.
|
|
|
34
34
|
```bash
|
|
35
35
|
novence deploy # auto-detects dist, build, out, public, _site
|
|
36
36
|
novence deploy ./site # or point at a directory
|
|
37
|
+
novence deploy report.html # one HTML file (dashboard, report, artifact)
|
|
37
38
|
```
|
|
38
39
|
|
|
39
40
|
First run with no stored key publishes anonymously (no email), stores
|
package/dist/commands/deploy.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { existsSync, statSync } from "node:fs";
|
|
2
|
-
import { resolve } from "node:path";
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { basename, resolve } from "node:path";
|
|
3
3
|
import { Novence } from "@novence/sdk";
|
|
4
4
|
import { claimAndMaybeVerify, ensureApiKey } from "../lib/auth.js";
|
|
5
5
|
import { resolveApiBase } from "../lib/config.js";
|
|
@@ -11,26 +11,34 @@ const POLL_TIMEOUT_MS = 5 * 60 * 1000;
|
|
|
11
11
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
12
12
|
/** Common build output directories, checked in order when no dir is given. */
|
|
13
13
|
const CANDIDATE_DIRS = ["dist", "build", "out", "public", "_site"];
|
|
14
|
-
function
|
|
14
|
+
function resolveSiteSource(explicit) {
|
|
15
15
|
if (explicit) {
|
|
16
|
-
const
|
|
17
|
-
if (!existsSync(
|
|
18
|
-
fail(`Not
|
|
19
|
-
|
|
16
|
+
const target = resolve(explicit);
|
|
17
|
+
if (!existsSync(target))
|
|
18
|
+
fail(`Not found: ${target}`);
|
|
19
|
+
const st = statSync(target);
|
|
20
|
+
if (st.isFile()) {
|
|
21
|
+
if (!target.toLowerCase().endsWith(".html")) {
|
|
22
|
+
fail(`Not a directory or .html file: ${target}`);
|
|
23
|
+
}
|
|
24
|
+
return { kind: "html", path: target };
|
|
25
|
+
}
|
|
26
|
+
if (!st.isDirectory())
|
|
27
|
+
fail(`Not a directory: ${target}`);
|
|
28
|
+
return { kind: "dir", path: target };
|
|
20
29
|
}
|
|
21
30
|
for (const candidate of CANDIDATE_DIRS) {
|
|
22
31
|
const dir = resolve(process.cwd(), candidate);
|
|
23
32
|
if (existsSync(dir) && statSync(dir).isDirectory())
|
|
24
|
-
return dir;
|
|
33
|
+
return { kind: "dir", path: dir };
|
|
25
34
|
}
|
|
26
35
|
return fail(`No build output found. Looked for ${CANDIDATE_DIRS.join(", ")} in ${process.cwd()}.\n` +
|
|
27
|
-
" Pass
|
|
36
|
+
" Pass a directory or an HTML file: novence deploy ./your-dir or novence deploy report.html");
|
|
28
37
|
}
|
|
29
|
-
async function
|
|
38
|
+
async function waitUntilLive(client, projectId) {
|
|
30
39
|
const spinner = spin("Publishing…");
|
|
31
40
|
const startedAt = Date.now();
|
|
32
41
|
try {
|
|
33
|
-
await client.deployments.create(projectId);
|
|
34
42
|
let lastStatus = "";
|
|
35
43
|
while (Date.now() - startedAt < POLL_TIMEOUT_MS) {
|
|
36
44
|
const { deployment } = await client.deployments.latest(projectId);
|
|
@@ -56,6 +64,10 @@ async function publish(client, projectId) {
|
|
|
56
64
|
spinner.stop();
|
|
57
65
|
return fail("Timed out after 5 minutes waiting for the deploy to go live.");
|
|
58
66
|
}
|
|
67
|
+
async function publish(client, projectId) {
|
|
68
|
+
await client.deployments.create(projectId);
|
|
69
|
+
return waitUntilLive(client, projectId);
|
|
70
|
+
}
|
|
59
71
|
function reportChecks(deployment) {
|
|
60
72
|
const checks = deployment.checksResults;
|
|
61
73
|
if (!checks)
|
|
@@ -72,19 +84,19 @@ function reportChecks(deployment) {
|
|
|
72
84
|
}
|
|
73
85
|
export async function deployCommand(args) {
|
|
74
86
|
const apiBase = resolveApiBase(flagString(args.flags, "api"));
|
|
75
|
-
const
|
|
76
|
-
if (!existsSync(resolve(
|
|
77
|
-
fail(`${
|
|
87
|
+
const source = resolveSiteSource(args.positionals[0]);
|
|
88
|
+
if (source.kind === "dir" && !existsSync(resolve(source.path, "index.html"))) {
|
|
89
|
+
fail(`${source.path} has no index.html — a static site needs one at its root.`);
|
|
78
90
|
}
|
|
79
91
|
let projectId = flagString(args.flags, "project");
|
|
80
92
|
try {
|
|
81
|
-
await runDeploy(args, apiBase,
|
|
93
|
+
await runDeploy(args, apiBase, source);
|
|
82
94
|
}
|
|
83
95
|
catch (err) {
|
|
84
96
|
fail(explain(err, { apiBase, projectId }));
|
|
85
97
|
}
|
|
86
98
|
}
|
|
87
|
-
async function runDeploy(args, apiBase,
|
|
99
|
+
async function runDeploy(args, apiBase, source) {
|
|
88
100
|
const interactive = !isJsonMode() && !flagBool(args.flags, "no-input");
|
|
89
101
|
const creds = await ensureApiKey({
|
|
90
102
|
apiBase,
|
|
@@ -94,20 +106,44 @@ async function runDeploy(args, apiBase, siteDir) {
|
|
|
94
106
|
const client = new Novence({ apiKey: creds.apiKey, baseUrl: apiBase });
|
|
95
107
|
let projectId = flagString(args.flags, "project");
|
|
96
108
|
let projectUrl;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
109
|
+
let fileCount = 1;
|
|
110
|
+
let deployment;
|
|
111
|
+
if (source.kind === "html") {
|
|
112
|
+
const html = readFileSync(source.path, "utf8");
|
|
113
|
+
const defaultName = basename(source.path).replace(/\.html?$/i, "") || "artifact";
|
|
114
|
+
const name = flagString(args.flags, "name") ?? defaultName;
|
|
115
|
+
const published = await withSpin(`Publishing ${source.path}…`, () => client.publishHtml({
|
|
116
|
+
html,
|
|
117
|
+
title: name,
|
|
118
|
+
projectId,
|
|
119
|
+
}), (result) => (result.created ? "Created project" : "Updated project"));
|
|
120
|
+
projectId = published.projectId;
|
|
121
|
+
projectUrl = published.url;
|
|
122
|
+
if (!projectId)
|
|
123
|
+
fail("Missing project id");
|
|
124
|
+
const htmlProjectId = projectId;
|
|
125
|
+
deployment = await waitUntilLive(client, htmlProjectId);
|
|
101
126
|
}
|
|
102
127
|
else {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
128
|
+
if (projectId) {
|
|
129
|
+
const { project } = await client.projects.get(projectId);
|
|
130
|
+
projectUrl = project.url;
|
|
131
|
+
step(`Reusing project ${project.name} (${project.suffix})`);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
const name = flagString(args.flags, "name") ?? "site";
|
|
135
|
+
const created = await client.projects.create({ name });
|
|
136
|
+
projectId = created.project.id;
|
|
137
|
+
projectUrl = created.url;
|
|
138
|
+
step(`Created project ${name} (${created.project.suffix})`);
|
|
139
|
+
}
|
|
140
|
+
if (!projectId)
|
|
141
|
+
fail("Missing project id");
|
|
142
|
+
const dirProjectId = projectId;
|
|
143
|
+
const { uploaded } = await withSpin(`Uploading ${source.path}…`, () => client.files.uploadDir(dirProjectId, source.path), ({ uploaded: files }) => `Uploaded ${files.length} file${files.length === 1 ? "" : "s"}`);
|
|
144
|
+
fileCount = uploaded.length;
|
|
145
|
+
deployment = await publish(client, dirProjectId);
|
|
108
146
|
}
|
|
109
|
-
const { uploaded } = await withSpin(`Uploading ${siteDir}…`, () => client.files.uploadDir(projectId, siteDir), ({ uploaded: files }) => `Uploaded ${files.length} file${files.length === 1 ? "" : "s"}`);
|
|
110
|
-
const deployment = await publish(client, projectId);
|
|
111
147
|
if (deployment.status === "failed") {
|
|
112
148
|
if (isJsonMode()) {
|
|
113
149
|
json({ ok: false, projectId, status: deployment.status, error: deployment.errorMessage });
|
|
@@ -117,6 +153,8 @@ async function runDeploy(args, apiBase, siteDir) {
|
|
|
117
153
|
}
|
|
118
154
|
reportChecks(deployment);
|
|
119
155
|
const liveUrl = siteUrl(deployment.liveUrl ?? projectUrl);
|
|
156
|
+
const previewUrl = deployment.previewUrl ? siteUrl(deployment.previewUrl) : null;
|
|
157
|
+
const distinctPreview = previewUrl && previewUrl !== liveUrl ? previewUrl : null;
|
|
120
158
|
if (isJsonMode()) {
|
|
121
159
|
json({
|
|
122
160
|
ok: true,
|
|
@@ -124,11 +162,14 @@ async function runDeploy(args, apiBase, siteDir) {
|
|
|
124
162
|
deploymentId: deployment.id,
|
|
125
163
|
status: deployment.status,
|
|
126
164
|
url: liveUrl,
|
|
127
|
-
|
|
165
|
+
...(distinctPreview ? { previewUrl: distinctPreview } : {}),
|
|
166
|
+
files: fileCount,
|
|
128
167
|
checks: deployment.checksResults ?? null,
|
|
129
168
|
});
|
|
130
169
|
return;
|
|
131
170
|
}
|
|
171
|
+
if (distinctPreview)
|
|
172
|
+
step(`Preview ${distinctPreview}`);
|
|
132
173
|
// Last line, alone, so it pipes cleanly.
|
|
133
174
|
result(liveUrl);
|
|
134
175
|
if (creds.anonymous && interactive) {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Novence } from "@novence/sdk";
|
|
2
|
+
import { loadApiKey, resolveApiBase } from "../lib/config.js";
|
|
3
|
+
import { flagString } from "../lib/args.js";
|
|
4
|
+
import { explain } from "../lib/errors.js";
|
|
5
|
+
import { fail, isJsonMode, json, result, siteUrl, step } from "../lib/ui.js";
|
|
6
|
+
export async function rollbackCommand(args) {
|
|
7
|
+
const apiBase = resolveApiBase(flagString(args.flags, "api"));
|
|
8
|
+
const apiKey = loadApiKey();
|
|
9
|
+
if (!apiKey)
|
|
10
|
+
fail("Not signed in. Run `novence login`.");
|
|
11
|
+
const client = new Novence({ apiKey, baseUrl: apiBase });
|
|
12
|
+
const wanted = flagString(args.flags, "project") ?? args.positionals[0];
|
|
13
|
+
const deploymentId = flagString(args.flags, "deployment");
|
|
14
|
+
let projectId = wanted;
|
|
15
|
+
try {
|
|
16
|
+
if (!projectId) {
|
|
17
|
+
const { projects } = await client.projects.list();
|
|
18
|
+
if (projects.length === 0)
|
|
19
|
+
fail("No projects yet. Run `novence deploy ./dist`.");
|
|
20
|
+
projectId = projects[0].id;
|
|
21
|
+
}
|
|
22
|
+
const { deployment } = await client.deployments.rollback(projectId, {
|
|
23
|
+
...(deploymentId ? { deploymentId } : {}),
|
|
24
|
+
});
|
|
25
|
+
const liveUrl = siteUrl(deployment.liveUrl ?? deployment.previewUrl ?? "");
|
|
26
|
+
const previewUrl = deployment.previewUrl ? siteUrl(deployment.previewUrl) : null;
|
|
27
|
+
if (isJsonMode()) {
|
|
28
|
+
json({
|
|
29
|
+
ok: true,
|
|
30
|
+
projectId,
|
|
31
|
+
deploymentId: deployment.id,
|
|
32
|
+
status: deployment.status,
|
|
33
|
+
url: liveUrl,
|
|
34
|
+
...(previewUrl && previewUrl !== liveUrl ? { previewUrl } : {}),
|
|
35
|
+
});
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
step("Restored previous snapshot (HTML cache ~60s; hard-refresh if the old page lingers).");
|
|
39
|
+
if (previewUrl && previewUrl !== liveUrl)
|
|
40
|
+
step(`Preview ${previewUrl}`);
|
|
41
|
+
result(liveUrl);
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
fail(explain(err, { apiBase, projectId: wanted }));
|
|
45
|
+
}
|
|
46
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -15,11 +15,13 @@ import { openCommand } from "./commands/open.js";
|
|
|
15
15
|
import { domainsCommand } from "./commands/domains.js";
|
|
16
16
|
import { keysCommand } from "./commands/keys.js";
|
|
17
17
|
import { checksCommand } from "./commands/checks.js";
|
|
18
|
+
import { rollbackCommand } from "./commands/rollback.js";
|
|
18
19
|
const HELP = `
|
|
19
20
|
novence — deploy a static site to the edge
|
|
20
21
|
|
|
21
22
|
Usage
|
|
22
|
-
novence deploy [dir]
|
|
23
|
+
novence deploy [dir|file] Upload a directory or one .html file and publish it live
|
|
24
|
+
novence rollback Restore the previous live snapshot (Pro/Scale)
|
|
23
25
|
novence open [project] Open a project's live URL
|
|
24
26
|
novence checks [project] Quality-gate results for the latest deploy
|
|
25
27
|
novence domains add <host> Attach a custom domain (prints DNS records)
|
|
@@ -34,10 +36,16 @@ const HELP = `
|
|
|
34
36
|
Deploy
|
|
35
37
|
novence deploy Auto-detects dist, build, out, public, _site
|
|
36
38
|
novence deploy ./site Deploy a specific directory
|
|
39
|
+
novence deploy report.html Publish a single HTML file as the site
|
|
37
40
|
--name <name> Project name (default: "site")
|
|
38
41
|
--project <id> Redeploy an existing project
|
|
39
42
|
--email <email> Skip the email prompt on first run (bootstrap instead of demo)
|
|
40
43
|
|
|
44
|
+
Rollback (Pro/Scale)
|
|
45
|
+
novence rollback Restore the previous live snapshot
|
|
46
|
+
--project <id> Project (default: first in the list)
|
|
47
|
+
--deployment <id> Specific retained deploy (last 5)
|
|
48
|
+
|
|
41
49
|
Global
|
|
42
50
|
--json Machine-readable output (implies --no-input)
|
|
43
51
|
--no-input Never prompt; fail instead
|
|
@@ -83,6 +91,7 @@ async function main() {
|
|
|
83
91
|
ls: [],
|
|
84
92
|
login: ["email", "recover"],
|
|
85
93
|
whoami: [],
|
|
94
|
+
rollback: ["project", "deployment"],
|
|
86
95
|
};
|
|
87
96
|
const allowed = ALLOWED[args.command];
|
|
88
97
|
if (allowed) {
|
|
@@ -110,6 +119,8 @@ async function main() {
|
|
|
110
119
|
return projectsCommand(args);
|
|
111
120
|
case "whoami":
|
|
112
121
|
return whoamiCommand(args);
|
|
122
|
+
case "rollback":
|
|
123
|
+
return rollbackCommand(args);
|
|
113
124
|
default:
|
|
114
125
|
fail(`Unknown command: ${args.command}\n Run \`novence --help\` for usage.`);
|
|
115
126
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "novence",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "CLI for Novence — deploy a static site to the edge in one command.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"access": "public"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@novence/sdk": "^0.2.
|
|
43
|
+
"@novence/sdk": "^0.2.5"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@types/node": "^22.10.2",
|