nano-pow 3.2.1 → 4.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/dist/bin/cli.js CHANGED
@@ -4,6 +4,7 @@
4
4
  import * as fs from "node:fs/promises";
5
5
  import * as readline from "node:readline/promises";
6
6
  import * as puppeteer from "puppeteer";
7
+ import { cliHelp } from "../../docs/index.js";
7
8
  const hashes = [];
8
9
  const stdinErrors = [];
9
10
  if (!process.stdin.isTTY) {
@@ -20,25 +21,7 @@ if (!process.stdin.isTTY) {
20
21
  }
21
22
  const args = process.argv.slice(2);
22
23
  if (hashes.length === 0 && args.length === 0 || args.some((v) => v === "--help" || v === "-h")) {
23
- console.log(`Usage: nano-pow [OPTION]... BLOCKHASH...
24
- Generate work for BLOCKHASH, or multiple work values for BLOCKHASH(es)
25
- BLOCKHASH is a 64-character hexadecimal string. Multiple blockhashes must be separated by whitespace or line breaks.
26
- Prints a 16-character hexadecimal work value to standard output. If using --validate, prints 'true' or 'false' to standard output instead.
27
-
28
- -h, --help show this dialog
29
- -d, --debug enable additional logging output
30
- -j, --json format output as JSON
31
- -e, --effort=<value> increase demand on GPU processing
32
- -t, --threshold=<value> override the minimum threshold value
33
- -v, --validate=<value> check an existing work value instead of searching for one
34
-
35
- If validating a nonce, it must be a 16-character hexadecimal value.
36
- Effort must be a decimal number between 1-32.
37
- Threshold must be a hexadecimal string between 0-FFFFFFFF.
38
-
39
- Report bugs: <bug-nano-pow@zoso.dev>
40
- Full documentation: <https://www.npmjs.com/package/nano-pow>
41
- `);
24
+ console.log(cliHelp);
42
25
  process.exit();
43
26
  }
44
27
  const inArgs = [];
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env bash
2
+ # SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ SCRIPT_LINK=$(readlink -f $0);
6
+ SCRIPT_DIR=$(dirname $SCRIPT_LINK);
7
+ NANO_POW_HOME=$HOME/.nano-pow;
8
+ NANO_POW_LOGS=$NANO_POW_HOME/logs;
9
+
10
+ mkdir -p $NANO_POW_LOGS;
11
+ if [ $1 = '--server' ]; then
12
+ shift;
13
+ node $SCRIPT_DIR/server.js $@ > $NANO_POW_LOGS/nano-pow-server-$(date +%s).log 2>&1 & echo $! > $NANO_POW_HOME/server.pid;
14
+ else
15
+ node $SCRIPT_DIR/cli.js $@;
16
+ fi;
@@ -0,0 +1,175 @@
1
+ #!/usr/bin/env node
2
+ //! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
3
+ //! SPDX-License-Identifier: GPL-3.0-or-later
4
+ import * as http from "node:http";
5
+ import * as fs from "node:fs/promises";
6
+ import * as puppeteer from "puppeteer";
7
+ import { serverHelp } from "../../docs/index.js";
8
+ const PORT = process.env.PORT || 3e3;
9
+ function log(...args) {
10
+ console.log(new Date(Date.now()).toLocaleString(), "NanoPow", args);
11
+ }
12
+ log("Starting server");
13
+ const NanoPow = await fs.readFile(new URL("../main.min.js", import.meta.url), "utf-8");
14
+ let browser;
15
+ let page;
16
+ async function work_generate(res, json) {
17
+ if (!/^[0-9A-Fa-f]{64}$/.test(json.hash ?? "")) {
18
+ res.writeHead(400, { "Content-Type": "application/json" });
19
+ res.end(JSON.stringify({ error: "Invalid hash. Must be a 64-character hex string." }));
20
+ return;
21
+ }
22
+ if (json.difficulty && !/^[1-9A-Fa-f][0-9A-Fa-f]{0,15}$/.test(json.difficulty)) {
23
+ res.writeHead(400, { "Content-Type": "application/json" });
24
+ res.end(JSON.stringify({ error: "Invalid difficulty. Must be a hexadecimal string between 1-FFFFFFFFFFFFFFFF." }));
25
+ return;
26
+ }
27
+ try {
28
+ const result = await page.evaluate(async (args) => {
29
+ const options = {
30
+ debug: true
31
+ };
32
+ if (args.difficulty) options.threshold = BigInt(`0x${args.difficulty}`);
33
+ return await window.NanoPow.work_generate(args.hash, options);
34
+ }, json);
35
+ res.writeHead(200, { "Content-Type": "application/json" });
36
+ res.end(JSON.stringify(result));
37
+ } catch (err) {
38
+ log("work_generate error:", err);
39
+ res.writeHead(500, { "Content-Type": "application/json" });
40
+ res.end(JSON.stringify({ error: "work_generate failed" }));
41
+ }
42
+ }
43
+ async function work_validate(res, json) {
44
+ if (!/^[0-9A-Fa-f]{64}$/.test(json.hash ?? "")) {
45
+ res.writeHead(400, { "Content-Type": "application/json" });
46
+ res.end(JSON.stringify({ error: "Invalid hash. Must be a 64-character hex string." }));
47
+ return;
48
+ }
49
+ if (json.difficulty && !/^[1-9A-Fa-f][0-9A-Fa-f]{0,15}$/.test(json.difficulty)) {
50
+ res.writeHead(400, { "Content-Type": "application/json" });
51
+ res.end(JSON.stringify({ error: "Invalid difficulty. Must be a hexadecimal string between 1-FFFFFFFFFFFFFFFF." }));
52
+ return;
53
+ }
54
+ if (!/^[0-9A-Fa-f]{16}$/.test(json.work ?? "")) {
55
+ res.writeHead(400, { "Content-Type": "application/json" });
56
+ res.end(JSON.stringify({ error: "Invalid work. Must be a 16-character hex string." }));
57
+ return;
58
+ }
59
+ try {
60
+ const result = await page.evaluate(async (args) => {
61
+ const options = {
62
+ debug: true
63
+ };
64
+ if (args.difficulty) options.threshold = BigInt(`0x${args.difficulty}`);
65
+ return await window.NanoPow.work_validate(args.work, args.hash, options);
66
+ }, json);
67
+ res.writeHead(200, { "Content-Type": "application/json" });
68
+ res.end(JSON.stringify(result));
69
+ } catch (err) {
70
+ log("work_validate error:", err);
71
+ res.writeHead(500, { "Content-Type": "application/json" });
72
+ res.end(JSON.stringify({ error: "work_validate failed" }));
73
+ }
74
+ }
75
+ (async () => {
76
+ browser = await puppeteer.launch({
77
+ headless: true,
78
+ args: [
79
+ "--headless=new",
80
+ "--use-angle=vulkan",
81
+ "--enable-features=Vulkan",
82
+ "--disable-vulkan-surface",
83
+ "--enable-unsafe-webgpu"
84
+ ]
85
+ });
86
+ page = await browser.newPage();
87
+ page.on("console", (msg) => {
88
+ log(msg.text());
89
+ });
90
+ await fs.writeFile(`${import.meta.dirname}/server.html`, "");
91
+ await page.goto(import.meta.resolve("./server.html"));
92
+ await page.waitForFunction(async () => {
93
+ return await navigator.gpu.requestAdapter();
94
+ });
95
+ const inject = `${NanoPow};window.NanoPow=NanoPow;`;
96
+ const hash = await crypto.subtle.digest("SHA-256", Buffer.from(inject, "utf-8"));
97
+ const src = `sha256-${Buffer.from(hash).toString("base64")}`;
98
+ await page.setContent(`
99
+ <!DOCTYPE html>
100
+ <head>
101
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'; base-uri 'none'; form-action 'none'; script-src '${src}';">
102
+ <script type="module">${inject}</script>
103
+ </head>
104
+ </html>
105
+ `);
106
+ await fs.unlink(`${import.meta.dirname}/server.html`);
107
+ log("Puppeteer initialized");
108
+ const server = http.createServer(async (req, res) => {
109
+ let data = [];
110
+ if (req.method === "POST") {
111
+ req.on("data", (chunk) => {
112
+ data.push(chunk);
113
+ });
114
+ req.on("end", async () => {
115
+ let json;
116
+ try {
117
+ json = JSON.parse(Buffer.concat(data).toString());
118
+ } catch (err) {
119
+ log("JSON.parse error:", err);
120
+ log("Failed JSON:", json);
121
+ res.writeHead(400, { "Content-Type": "application/json" });
122
+ res.end(JSON.stringify({ error: "Invalid data." }));
123
+ return;
124
+ }
125
+ switch (json.action) {
126
+ case "work_generate": {
127
+ await work_generate(res, json);
128
+ break;
129
+ }
130
+ case "work_validate": {
131
+ await work_validate(res, json);
132
+ break;
133
+ }
134
+ default: {
135
+ res.writeHead(400, { "Content-Type": "application/json" });
136
+ res.end(JSON.stringify({ error: `Invalid data.` }));
137
+ return;
138
+ }
139
+ }
140
+ });
141
+ } else {
142
+ res.writeHead(200, { "Content-Type": "text/plain" });
143
+ res.end(serverHelp);
144
+ }
145
+ });
146
+ server.on("error", (e) => {
147
+ log("Server error", e);
148
+ try {
149
+ shutdown();
150
+ } catch (err) {
151
+ log("Failed to shut down", err);
152
+ process.exit(1);
153
+ }
154
+ });
155
+ server.listen(PORT, () => {
156
+ process.title = "NanoPow Server";
157
+ log(`Server process ${process.pid} running at http://localhost:${PORT}/`);
158
+ });
159
+ function shutdown() {
160
+ log("Shutdown signal received");
161
+ const kill = setTimeout(() => {
162
+ log("Server unresponsive, forcefully stopped");
163
+ process.exit(1);
164
+ }, 1e4);
165
+ server.close(async () => {
166
+ await page?.close();
167
+ await browser?.close();
168
+ clearTimeout(kill);
169
+ log("Server stopped");
170
+ process.exit(0);
171
+ });
172
+ }
173
+ process.on("SIGINT", shutdown);
174
+ process.on("SIGTERM", shutdown);
175
+ })();
package/dist/main.min.js CHANGED
@@ -345,22 +345,10 @@ var NanoPowGl = class _NanoPowGl {
345
345
  * Finds a nonce that satisfies the Nano proof-of-work requirements.
346
346
  *
347
347
  * @param {string} hash - Hexadecimal hash of previous block, or public key for new accounts
348
- * @param {NanoPowOptions} options - Used to configure search execution
349
- */
350
- static async search(hash, options) {
351
- if (options?.threshold != null) {
352
- options.threshold = BigInt(`0x${options.threshold.toString(16) ?? "0"}00000000`);
353
- }
354
- const result = await this.work_generate(hash, options);
355
- return result.work;
356
- }
357
- /**
358
- * Finds a nonce that satisfies the Nano proof-of-work requirements.
359
- *
360
- * @param {string} hash - Hexadecimal hash of previous block, or public key for new accounts
361
348
  * @param {NanoPowOptions} options - Options used to configure search execution
362
349
  */
363
350
  static async work_generate(hash, options) {
351
+ if (!/^[A-Fa-f0-9]{64}$/.test(hash)) throw new Error(`Invalid hash ${hash}`);
364
352
  if (this.#busy) {
365
353
  console.log("NanoPowGl is busy. Retrying search...");
366
354
  return new Promise((resolve) => {
@@ -371,11 +359,17 @@ var NanoPowGl = class _NanoPowGl {
371
359
  });
372
360
  }
373
361
  this.#busy = true;
374
- if (!/^[A-Fa-f0-9]{64}$/.test(hash)) throw new Error(`Invalid hash ${hash}`);
362
+ if (typeof options?.threshold === "string") {
363
+ try {
364
+ options.threshold = BigInt(`0x${options.threshold}`);
365
+ } catch (err) {
366
+ throw new TypeError(`Invalid threshold ${options.threshold}`);
367
+ }
368
+ }
375
369
  const threshold = typeof options?.threshold !== "bigint" || options.threshold < 1n || options.threshold > 0xffffffffffffffffn ? 0xfffffff800000000n : options.threshold;
376
370
  const effort = typeof options?.effort !== "number" || options.effort < 1 || options.effort > 32 ? this.#cores : options.effort;
377
371
  this.#debug = !!options?.debug;
378
- if (this.#debug) console.log("NanoPowGl.search()");
372
+ if (this.#debug) console.log("NanoPowGl.work_generate()");
379
373
  if (this.#debug) console.log("blockhash", hash);
380
374
  if (this.#debug) console.log("search options", JSON.stringify(options, (k, v) => typeof v === "bigint" ? v.toString(16) : v));
381
375
  if (this.#WORKLOAD !== 256 * effort) {
@@ -436,21 +430,9 @@ var NanoPowGl = class _NanoPowGl {
436
430
  * @param {string} hash - Hexadecimal hash of previous block, or public key for new accounts
437
431
  * @param {NanoPowOptions} options - Options used to configure search execution
438
432
  */
439
- static async validate(work, hash, options) {
440
- if (options?.threshold != null) {
441
- options.threshold = BigInt(`0x${options.threshold.toString(16) ?? "0"}00000000`);
442
- }
443
- const result = await this.work_validate(work, hash, options);
444
- return options?.threshold != null ? result.valid === "1" : result.valid_all === "1";
445
- }
446
- /**
447
- * Validates that a nonce satisfies Nano proof-of-work requirements.
448
- *
449
- * @param {string} work - Hexadecimal proof-of-work value to validate
450
- * @param {string} hash - Hexadecimal hash of previous block, or public key for new accounts
451
- * @param {NanoPowOptions} options - Options used to configure search execution
452
- */
453
433
  static async work_validate(work, hash, options) {
434
+ if (!/^[A-Fa-f0-9]{16}$/.test(work)) throw new Error(`Invalid work ${work}`);
435
+ if (!/^[A-Fa-f0-9]{64}$/.test(hash)) throw new Error(`Invalid hash ${hash}`);
454
436
  if (this.#busy) {
455
437
  console.log("NanoPowGl is busy. Retrying validate...");
456
438
  return new Promise((resolve) => {
@@ -461,11 +443,16 @@ var NanoPowGl = class _NanoPowGl {
461
443
  });
462
444
  }
463
445
  this.#busy = true;
464
- if (!/^[A-Fa-f0-9]{16}$/.test(work)) throw new Error(`Invalid work ${work}`);
465
- if (!/^[A-Fa-f0-9]{64}$/.test(hash)) throw new Error(`Invalid hash ${hash}`);
446
+ if (typeof options?.threshold === "string") {
447
+ try {
448
+ options.threshold = BigInt(`0x${options.threshold}`);
449
+ } catch (err) {
450
+ throw new TypeError(`Invalid threshold ${options.threshold}`);
451
+ }
452
+ }
466
453
  const threshold = typeof options?.threshold !== "bigint" || options.threshold < 1n || options.threshold > 0xffffffffffffffffn ? 0xfffffff800000000n : options.threshold;
467
454
  this.#debug = !!options?.debug;
468
- if (this.#debug) console.log("NanoPowGl.validate()");
455
+ if (this.#debug) console.log("NanoPowGl.work_validate()");
469
456
  if (this.#debug) console.log("blockhash", hash);
470
457
  if (this.#debug) console.log("validate options", JSON.stringify(options, (k, v) => typeof v === "bigint" ? v.toString(16) : v));
471
458
  if (_NanoPowGl.#gl == null) throw new Error("WebGL 2 is required");
@@ -706,19 +693,6 @@ var NanoPowGpu = class _NanoPowGpu {
706
693
  * @param {string} hash - Hexadecimal hash of previous block, or public key for new accounts
707
694
  * @param {NanoPowOptions} options - Used to configure search execution
708
695
  */
709
- static async search(hash, options) {
710
- if (options?.threshold != null) {
711
- options.threshold = BigInt(`0x${options.threshold.toString(16) ?? "0"}00000000`);
712
- }
713
- const result = await this.work_generate(hash, options);
714
- return result.work;
715
- }
716
- /**
717
- * Finds a nonce that satisfies the Nano proof-of-work requirements.
718
- *
719
- * @param {string} hash - Hexadecimal hash of previous block, or public key for new accounts
720
- * @param {NanoPowOptions} options - Used to configure search execution
721
- */
722
696
  static async work_generate(hash, options) {
723
697
  if (!/^[A-Fa-f0-9]{64}$/.test(hash)) throw new TypeError(`Invalid hash ${hash}`);
724
698
  if (this.#busy) {
@@ -731,10 +705,17 @@ var NanoPowGpu = class _NanoPowGpu {
731
705
  });
732
706
  }
733
707
  this.#busy = true;
708
+ if (typeof options?.threshold === "string") {
709
+ try {
710
+ options.threshold = BigInt(`0x${options.threshold}`);
711
+ } catch (err) {
712
+ throw new TypeError(`Invalid threshold ${options.threshold}`);
713
+ }
714
+ }
734
715
  const threshold = typeof options?.threshold !== "bigint" || options.threshold < 1n || options.threshold > 0xffffffffffffffffn ? 0xfffffff800000000n : options.threshold;
735
716
  const effort = typeof options?.effort !== "number" || options.effort < 1 || options.effort > 32 ? 2048 : options.effort * 256;
736
717
  this.#debug = !!options?.debug;
737
- if (this.#debug) console.log("NanoPowGpu.search()");
718
+ if (this.#debug) console.log("NanoPowGpu.work_generate()");
738
719
  if (this.#debug) console.log("blockhash", hash);
739
720
  if (this.#debug) console.log("search options", JSON.stringify(options, (k, v) => typeof v === "bigint" ? v.toString(16) : v));
740
721
  let loads = 0;
@@ -780,20 +761,6 @@ var NanoPowGpu = class _NanoPowGpu {
780
761
  * @param {string} hash - Hexadecimal hash of previous block, or public key for new accounts
781
762
  * @param {NanoPowOptions} options - Options used to configure search execution
782
763
  */
783
- static async validate(work, hash, options) {
784
- if (options?.threshold != null) {
785
- options.threshold = BigInt(`0x${options.threshold.toString(16) ?? "0"}00000000`);
786
- }
787
- const result = await this.work_validate(work, hash, options);
788
- return options?.threshold != null ? result.valid === "1" : result.valid_all === "1";
789
- }
790
- /**
791
- * Validates that a nonce satisfies Nano proof-of-work requirements.
792
- *
793
- * @param {string} work - Hexadecimal proof-of-work value to validate
794
- * @param {string} hash - Hexadecimal hash of previous block, or public key for new accounts
795
- * @param {NanoPowOptions} options - Options used to configure search execution
796
- */
797
764
  static async work_validate(work, hash, options) {
798
765
  if (!/^[A-Fa-f0-9]{16}$/.test(work)) throw new TypeError(`Invalid work ${work}`);
799
766
  if (!/^[A-Fa-f0-9]{64}$/.test(hash)) throw new TypeError(`Invalid hash ${hash}`);
@@ -807,9 +774,16 @@ var NanoPowGpu = class _NanoPowGpu {
807
774
  });
808
775
  }
809
776
  this.#busy = true;
777
+ if (typeof options?.threshold === "string") {
778
+ try {
779
+ options.threshold = BigInt(`0x${options.threshold}`);
780
+ } catch (err) {
781
+ throw new TypeError(`Invalid threshold ${options.threshold}`);
782
+ }
783
+ }
810
784
  const threshold = typeof options?.threshold !== "bigint" || options.threshold < 1n || options.threshold > 0xffffffffffffffffn ? 0xfffffff800000000n : options.threshold;
811
785
  this.#debug = !!options?.debug;
812
- if (this.#debug) console.log("NanoPowGpu.validate()");
786
+ if (this.#debug) console.log("NanoPowGpu.work_validate()");
813
787
  if (this.#debug) console.log("blockhash", hash);
814
788
  if (this.#debug) console.log("validate options", JSON.stringify(options, (k, v) => typeof v === "bigint" ? v.toString(16) : v));
815
789
  let loads = 0;
package/dist/types.d.ts CHANGED
@@ -91,12 +91,12 @@ export type FBO = {
91
91
  *
92
92
  * @param {boolean} [debug=false] - Enables additional debug logging to the console. Default: false
93
93
  * @param {number} [effort=0x8] - Multiplier for dispatching work search. Larger values are not necessarily better since they can quickly overwhelm the GPU. Ignored when validating. Default: 0x8
94
- * @param {number} [threshold=0xfffffff8] - Minimum value result of `BLAKE2b(nonce||blockhash) << 0x32`. Default: 0xFFFFFFF8
94
+ * @param {bigint|string} [threshold=0xfffffff800000000] - Minimum value result of `BLAKE2b(nonce||blockhash)`. Default: 0xFFFFFFF800000000
95
95
  */
96
96
  export type NanoPowOptions = {
97
97
  debug?: boolean
98
98
  effort?: number
99
- threshold?: bigint | number
99
+ threshold?: bigint | string
100
100
  }
101
101
 
102
102
  /**
@@ -105,7 +105,7 @@ export type NanoPowOptions = {
105
105
  export declare class NanoPowGl {
106
106
  #private
107
107
  /** Drawing buffer width in pixels. */
108
- static get size (): number | undefined
108
+ static get size (): number
109
109
  /**
110
110
  * Constructs canvas, gets WebGL context, initializes buffers, and compiles
111
111
  * shaders.
package/docs/index.js ADDED
@@ -0,0 +1,38 @@
1
+ //! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
2
+ //! SPDX-License-Identifier: GPL-3.0-or-later
3
+
4
+ export const cliHelp = `Usage: nano-pow [OPTION]... BLOCKHASH...
5
+ Generate work for BLOCKHASH, or multiple work values for BLOCKHASH(es)
6
+ BLOCKHASH is a 64-character hexadecimal string. Multiple blockhashes must be separated by whitespace or line breaks.
7
+ Prints a 16-character hexadecimal work value to standard output. If using --validate, prints 'true' or 'false' to standard output instead.
8
+
9
+ -h, --help show this dialog
10
+ -d, --debug enable additional logging output
11
+ -j, --json format output as JSON
12
+ -e, --effort=<value> increase demand on GPU processing
13
+ -t, --threshold=<value> override the minimum threshold value
14
+ -v, --validate=<value> check an existing work value instead of searching for one
15
+
16
+ If validating a nonce, it must be a 16-character hexadecimal value.
17
+ Effort must be a decimal number between 1-32.
18
+ Threshold must be a hexadecimal string between 0-FFFFFFFF.
19
+
20
+ Report bugs: <bug-nano-pow@zoso.dev>
21
+ Full documentation: <https://www.npmjs.com/package/nano-pow>
22
+ `
23
+
24
+ export const serverHelp = `Usage: Send POST request to server URL to generate or validate Nano proof-of-work
25
+
26
+ Generate work for a BLOCKHASH with an optional DIFFICULTY:
27
+ curl -d '{ action: "work_generate", hash: BLOCKHASH, difficulty?: DIFFICULTY }'
28
+
29
+ Validate WORK previously calculated for a BLOCKHASH with an optional DIFFICULTY:
30
+ curl -d '{ action: "work_validate", work: WORK, hash: BLOCKHASH, difficulty?: DIFFICULTY }'
31
+
32
+ BLOCKHASH is a 64-character hexadecimal string.
33
+ WORK is 16-character hexadecimal string.
34
+ DIFFICULTY is a 16-character hexadecimal string (default: FFFFFFF800000000)
35
+
36
+ Report bugs: <bug-nano-pow@zoso.dev>
37
+ Full documentation: <https://www.npmjs.com/package/nano-pow>
38
+ `
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nano-pow",
3
- "version": "3.2.1",
3
+ "version": "4.0.1",
4
4
  "description": "Proof-of-work generation and validation with WebGPU/WebGL for Nano cryptocurrency.",
5
5
  "keywords": [
6
6
  "nemo",
@@ -37,7 +37,7 @@
37
37
  "./dist/main.min.js": true
38
38
  },
39
39
  "bin": {
40
- "nano-pow": "dist/bin/cli.js"
40
+ "nano-pow": "dist/bin/nano-pow.sh"
41
41
  },
42
42
  "man": "./docs/nano-pow.1",
43
43
  "repository": {
@@ -45,7 +45,9 @@
45
45
  "url": "git+https://zoso.dev/nano-pow.git"
46
46
  },
47
47
  "scripts": {
48
- "build": "rm -rf {dist,types} && tsc && node esbuild.mjs"
48
+ "build": "rm -rf {dist,types} && tsc && node esbuild.mjs && cp -p src/bin/nano-pow.sh dist/bin",
49
+ "start": "./dist/bin/nano-pow.sh --server",
50
+ "test": "./test/script.sh"
49
51
  },
50
52
  "devDependencies": {
51
53
  "@types/node": "^22.13.11",