novence 0.1.1 → 0.1.2

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 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
@@ -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 resolveSiteDir(explicit) {
14
+ function resolveSiteSource(explicit) {
15
15
  if (explicit) {
16
- const dir = resolve(explicit);
17
- if (!existsSync(dir) || !statSync(dir).isDirectory())
18
- fail(`Not a directory: ${dir}`);
19
- return dir;
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 one explicitly: novence deploy ./your-dir");
36
+ " Pass a directory or an HTML file: novence deploy ./your-dir or novence deploy report.html");
28
37
  }
29
- async function publish(client, projectId) {
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 siteDir = resolveSiteDir(args.positionals[0]);
76
- if (!existsSync(resolve(siteDir, "index.html"))) {
77
- fail(`${siteDir} has no index.html — a static site needs one at its root.`);
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, siteDir);
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, siteDir) {
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
- if (projectId) {
98
- const { project } = await client.projects.get(projectId);
99
- projectUrl = project.url;
100
- step(`Reusing project ${project.name} (${project.suffix})`);
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
- const name = flagString(args.flags, "name") ?? "site";
104
- const created = await client.projects.create({ name });
105
- projectId = created.project.id;
106
- projectUrl = created.url;
107
- step(`Created project ${name} (${created.project.suffix})`);
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
- files: uploaded.length,
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,2 @@
1
+ import { type ParsedArgs } from "../lib/args.js";
2
+ export declare function rollbackCommand(args: ParsedArgs): Promise<void>;
@@ -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] Upload a directory and publish it live
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.1",
3
+ "version": "0.1.2",
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.3"
43
+ "@novence/sdk": "^0.2.4"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@types/node": "^22.10.2",