@semaphore-protocol/cli 3.0.0-beta.8 → 3.1.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/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  /**
4
4
  * @module @semaphore-protocol/cli
5
- * @version 3.0.0-beta.8
5
+ * @version 3.1.0
6
6
  * @file A command line tool to set up your Semaphore project and get group data.
7
7
  * @copyright Ethereum Foundation 2022
8
8
  * @license MIT
@@ -18,8 +18,41 @@ import { readFileSync, existsSync, renameSync } from 'fs';
18
18
  import logSymbols from 'log-symbols';
19
19
  import { dirname } from 'path';
20
20
  import { fileURLToPath } from 'url';
21
+ import inquirer from 'inquirer';
22
+ import axios from 'axios';
23
+ import boxen from 'boxen';
24
+ import { execSync } from 'child_process';
25
+ import { lt } from 'semver';
21
26
  import ora from 'ora';
22
27
 
28
+ const cliRegistryURL = "https://registry.npmjs.org/-/package/@semaphore-protocol/cli/dist-tags";
29
+ /**
30
+ * Checks the registry directly via the API, if that fails, tries the slower `npm view [package] version` command.
31
+ * This is important for users in environments where direct access to npm is blocked by a firewall, and packages are
32
+ * provided exclusively via a private registry.
33
+ * @param currentVersion The current version of the CLI.
34
+ */
35
+ async function checkLatestVersion(currentVersion) {
36
+ let latestVersion;
37
+ try {
38
+ const { data } = await axios.get(cliRegistryURL);
39
+ latestVersion = data.latest;
40
+ }
41
+ catch {
42
+ try {
43
+ latestVersion = execSync("npm view @semaphore-protocol/cli version").toString().trim();
44
+ }
45
+ catch {
46
+ latestVersion = null;
47
+ }
48
+ }
49
+ if (latestVersion && lt(currentVersion, latestVersion)) {
50
+ console.info("\n");
51
+ console.info(boxen(chalk.white(`Update available ${chalk.gray(currentVersion)} -> ${chalk.green(latestVersion)} \n\n You are currently using @semaphore-protocol/cli ${chalk.gray(currentVersion)} which is behind the latest release ${chalk.green(latestVersion)} \n\n Run ${chalk.cyan("npm install -g @semaphore-protocol/cli@latest")} to get the latest version`), { padding: 1, borderColor: "yellow", textAlignment: "center" }));
52
+ console.info("");
53
+ }
54
+ }
55
+
23
56
  class Spinner {
24
57
  constructor(text) {
25
58
  this.text = text;
@@ -33,12 +66,13 @@ class Spinner {
33
66
  }
34
67
  stop() {
35
68
  this.ora.stop();
36
- process.stdout.cursorTo(0);
69
+ process.stdout.moveCursor(0, -1);
37
70
  }
38
71
  }
39
72
 
40
73
  const packagePath = `${dirname(fileURLToPath(import.meta.url))}/..`;
41
74
  const { description, version } = JSON.parse(readFileSync(`${packagePath}/package.json`, "utf8"));
75
+ const supportedNetworks = ["goerli", "arbitrum"];
42
76
  program
43
77
  .name("semaphore")
44
78
  .description(description)
@@ -55,9 +89,19 @@ program
55
89
  program
56
90
  .command("create")
57
91
  .description("Create a Semaphore project with a supported template.")
58
- .argument("<project-directory>", "Directory of the project.")
92
+ .argument("[project-directory]", "Directory of the project.")
59
93
  // .option("-t, --template <template-name>", "Supported Semaphore template.", "hardhat")
94
+ .allowExcessArguments(false)
60
95
  .action(async (projectDirectory) => {
96
+ if (!projectDirectory) {
97
+ const { projectName } = await inquirer.prompt({
98
+ name: "projectName",
99
+ type: "input",
100
+ message: "What is your project name?",
101
+ default: "my-app"
102
+ });
103
+ projectDirectory = projectName;
104
+ }
61
105
  const currentDirectory = process.cwd();
62
106
  const spinner = new Spinner(`Creating your project in ${chalk.green(`./${projectDirectory}`)}`);
63
107
  const templateURL = `https://registry.npmjs.org/@semaphore-protocol/cli-template-hardhat/-/cli-template-hardhat-${version}.tgz`;
@@ -66,10 +110,11 @@ program
66
110
  return;
67
111
  }
68
112
  spinner.start();
113
+ await checkLatestVersion(version);
69
114
  await download(templateURL, currentDirectory, { extract: true });
70
115
  renameSync(`${currentDirectory}/package`, `${currentDirectory}/${projectDirectory}`);
71
116
  spinner.stop();
72
- console.info(` ${logSymbols.success}`, `Your project is ready!\n`);
117
+ console.info(`\n ${logSymbols.success}`, `Your project is ready!\n`);
73
118
  console.info(` Please, install your dependencies by running:\n`);
74
119
  console.info(` ${chalk.cyan("cd")} ${projectDirectory}`);
75
120
  console.info(` ${chalk.cyan("npm i")}\n`);
@@ -85,9 +130,20 @@ program
85
130
  program
86
131
  .command("get-groups")
87
132
  .description("Get the list of groups from a supported network (goerli or arbitrum).")
88
- .option("-n, --network <network-name>", "Supported Ethereum network.", "goerli")
133
+ .option("-n, --network <network-name>", "Supported Ethereum network.")
134
+ .allowExcessArguments(false)
89
135
  .action(async ({ network }) => {
90
- if (!["goerli", "arbitrum"].includes(network)) {
136
+ if (!network) {
137
+ const { selectedNetwork } = await inquirer.prompt({
138
+ name: "selectedNetwork",
139
+ type: "list",
140
+ message: "Select one of our supported networks:",
141
+ default: supportedNetworks[0],
142
+ choices: supportedNetworks
143
+ });
144
+ network = selectedNetwork;
145
+ }
146
+ if (!supportedNetworks.includes(network)) {
91
147
  console.info(`\n ${logSymbols.error}`, `error: the network '${network}' is not supported\n`);
92
148
  return;
93
149
  }
@@ -98,26 +154,79 @@ program
98
154
  const groups = await subgraph.getGroups();
99
155
  spinner.stop();
100
156
  if (groups.length === 0) {
101
- console.info(` ${logSymbols.error}`, "error: there are no groups in this network\n");
157
+ console.info(`\n ${logSymbols.info}`, "info: there are no groups in this network\n");
102
158
  return;
103
159
  }
104
- const content = `${groups.map(({ id }) => ` - ${id}`).join("\n")}`;
160
+ const content = `\n${groups.map(({ id }) => ` - ${id}`).join("\n")}`;
105
161
  console.info(`${content}\n`);
106
162
  }
107
163
  catch (error) {
108
164
  spinner.stop();
109
- console.info(` ${logSymbols.error}`, "error: unexpected error with the Semaphore subgraph");
165
+ console.info(`\n ${logSymbols.error}`, "error: unexpected error with the Semaphore subgraph");
110
166
  }
111
167
  });
112
168
  program
113
169
  .command("get-group")
114
170
  .description("Get the data of a group from a supported network (Goerli or Arbitrum).")
115
- .argument("<group-id>", "Identifier of the group.")
116
- .option("-n, --network <network-name>", "Supported Ethereum network.", "goerli")
117
- .option("--members", "Show group members.")
118
- .option("--signals", "Show group signals.")
171
+ .argument("[group-id]", "Identifier of the group.")
172
+ .option("-n, --network <network-name>", "Supported Ethereum network.")
173
+ .option("-m, --members", "Show group members.")
174
+ .option("-s, --signals", "Show group signals.")
175
+ .allowExcessArguments(false)
119
176
  .action(async (groupId, { network, members, signals }) => {
120
- if (!["goerli", "arbitrum"].includes(network)) {
177
+ if (!network) {
178
+ const { selectedNetwork } = await inquirer.prompt({
179
+ name: "selectedNetwork",
180
+ type: "list",
181
+ message: "Select one of our supported networks:",
182
+ default: supportedNetworks[0],
183
+ choices: supportedNetworks
184
+ });
185
+ network = selectedNetwork;
186
+ }
187
+ if (!groupId) {
188
+ const subgraphGroups = new Subgraph(network);
189
+ const spinnerGroups = new Spinner("Fetching groups");
190
+ spinnerGroups.start();
191
+ try {
192
+ const groups = await subgraphGroups.getGroups();
193
+ spinnerGroups.stop();
194
+ if (groups.length === 0) {
195
+ console.info(`\n ${logSymbols.info}`, "info: there are no groups in this network\n");
196
+ return;
197
+ }
198
+ const groupIds = groups.map(({ id }) => id);
199
+ const { selectedGroupId } = await inquirer.prompt({
200
+ name: "selectedGroupId",
201
+ type: "list",
202
+ message: "Select one of the following existing group ids:",
203
+ choices: groupIds
204
+ });
205
+ groupId = selectedGroupId;
206
+ }
207
+ catch (error) {
208
+ spinnerGroups.stop();
209
+ console.info(`\n ${logSymbols.error}`, "error: unexpected error with the Semaphore subgraph");
210
+ return;
211
+ }
212
+ }
213
+ if (!members && !signals) {
214
+ const { showMembers } = await inquirer.prompt({
215
+ name: "showMembers",
216
+ type: "confirm",
217
+ message: "Do you want to show members?",
218
+ default: false
219
+ });
220
+ members = showMembers;
221
+ const { showSignals } = await inquirer.prompt({
222
+ name: "showSignals",
223
+ type: "confirm",
224
+ message: "Do you want to show signals?",
225
+ default: false
226
+ });
227
+ signals = showSignals;
228
+ }
229
+ if (!supportedNetworks.includes(network)) {
121
230
  console.info(`\n ${logSymbols.error}`, `error: the network '${network}' is not supported\n`);
122
231
  return;
123
232
  }
@@ -128,7 +237,7 @@ program
128
237
  const group = await subgraph.getGroup(groupId, { members, verifiedProofs: signals });
129
238
  spinner.stop();
130
239
  if (!group) {
131
- console.info(` ${logSymbols.error}`, "error: the group does not exist\n");
240
+ console.info(`\n ${logSymbols.error}`, "error: the group does not exist\n");
132
241
  return;
133
242
  }
134
243
  let content = ` ${chalk.bold("Id")}: ${group.id}\n`;
@@ -148,11 +257,11 @@ program
148
257
  .map(({ signal }) => ` - ${signal}`)
149
258
  .join("\n")}`;
150
259
  }
151
- console.info(`${content}\n`);
260
+ console.info(`\n${content}\n`);
152
261
  }
153
262
  catch (error) {
154
263
  spinner.stop();
155
- console.info(` ${logSymbols.error}`, "error: unexpected error with the Semaphore subgraph");
264
+ console.info(`\n ${logSymbols.error}`, "error: unexpected error with the Semaphore subgraph");
156
265
  }
157
266
  });
158
- program.parse();
267
+ program.parse(process.argv);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@semaphore-protocol/cli",
3
3
  "type": "module",
4
- "version": "3.0.0-beta.8",
4
+ "version": "3.1.0",
5
5
  "description": "A command line tool to set up your Semaphore project and get group data.",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -34,18 +34,23 @@
34
34
  "devDependencies": {
35
35
  "@types/clear": "^0.1.2",
36
36
  "@types/figlet": "^1.5.5",
37
+ "@types/semver": "^7.3.13",
37
38
  "rollup-plugin-cleanup": "^3.2.1",
38
39
  "rollup-plugin-typescript2": "^0.31.2",
39
40
  "ts-node": "^10.9.1",
40
41
  "typedoc": "^0.22.11"
41
42
  },
42
43
  "dependencies": {
43
- "@semaphore-protocol/subgraph": "3.0.0-beta.8",
44
+ "@semaphore-protocol/subgraph": "3.1.0",
45
+ "axios": "^1.3.2",
46
+ "boxen": "^7.0.1",
44
47
  "chalk": "^5.1.2",
45
48
  "commander": "^9.4.1",
46
49
  "download": "^8.0.0",
47
50
  "figlet": "^1.5.2",
51
+ "inquirer": "^9.1.4",
48
52
  "log-symbols": "^5.1.0",
49
- "ora": "^6.1.2"
53
+ "ora": "^6.1.2",
54
+ "semver": "^7.3.8"
50
55
  }
51
56
  }
@@ -0,0 +1,48 @@
1
+ import axios from "axios"
2
+ import boxen from "boxen"
3
+ import chalk from "chalk"
4
+ import { execSync } from "child_process"
5
+ import { lt as semverLt } from "semver"
6
+
7
+ const cliRegistryURL = "https://registry.npmjs.org/-/package/@semaphore-protocol/cli/dist-tags"
8
+
9
+ /**
10
+ * Checks the registry directly via the API, if that fails, tries the slower `npm view [package] version` command.
11
+ * This is important for users in environments where direct access to npm is blocked by a firewall, and packages are
12
+ * provided exclusively via a private registry.
13
+ * @param currentVersion The current version of the CLI.
14
+ */
15
+ export default async function checkLatestVersion(currentVersion: string) {
16
+ let latestVersion: string
17
+
18
+ try {
19
+ const { data } = await axios.get(cliRegistryURL)
20
+
21
+ latestVersion = data.latest
22
+ } catch {
23
+ try {
24
+ latestVersion = execSync("npm view @semaphore-protocol/cli version").toString().trim()
25
+ } catch {
26
+ latestVersion = null
27
+ }
28
+ }
29
+
30
+ if (latestVersion && semverLt(currentVersion, latestVersion)) {
31
+ console.info("\n")
32
+ console.info(
33
+ boxen(
34
+ chalk.white(
35
+ `Update available ${chalk.gray(currentVersion)} -> ${chalk.green(
36
+ latestVersion
37
+ )} \n\n You are currently using @semaphore-protocol/cli ${chalk.gray(
38
+ currentVersion
39
+ )} which is behind the latest release ${chalk.green(latestVersion)} \n\n Run ${chalk.cyan(
40
+ "npm install -g @semaphore-protocol/cli@latest"
41
+ )} to get the latest version`
42
+ ),
43
+ { padding: 1, borderColor: "yellow", textAlignment: "center" }
44
+ )
45
+ )
46
+ console.info("")
47
+ }
48
+ }
package/src/index.ts CHANGED
@@ -7,11 +7,15 @@ import { existsSync, readFileSync, renameSync } from "fs"
7
7
  import logSymbols from "log-symbols"
8
8
  import { dirname } from "path"
9
9
  import { fileURLToPath } from "url"
10
+ import inquirer from "inquirer"
11
+ import checkLatestVersion from "./checkLatestVersion.js"
10
12
  import Spinner from "./spinner.js"
11
13
 
12
14
  const packagePath = `${dirname(fileURLToPath(import.meta.url))}/..`
13
15
  const { description, version } = JSON.parse(readFileSync(`${packagePath}/package.json`, "utf8"))
14
16
 
17
+ const supportedNetworks = ["goerli", "arbitrum"]
18
+
15
19
  program
16
20
  .name("semaphore")
17
21
  .description(description)
@@ -29,9 +33,20 @@ program
29
33
  program
30
34
  .command("create")
31
35
  .description("Create a Semaphore project with a supported template.")
32
- .argument("<project-directory>", "Directory of the project.")
36
+ .argument("[project-directory]", "Directory of the project.")
33
37
  // .option("-t, --template <template-name>", "Supported Semaphore template.", "hardhat")
38
+ .allowExcessArguments(false)
34
39
  .action(async (projectDirectory) => {
40
+ if (!projectDirectory) {
41
+ const { projectName } = await inquirer.prompt({
42
+ name: "projectName",
43
+ type: "input",
44
+ message: "What is your project name?",
45
+ default: "my-app"
46
+ })
47
+ projectDirectory = projectName
48
+ }
49
+
35
50
  const currentDirectory = process.cwd()
36
51
  const spinner = new Spinner(`Creating your project in ${chalk.green(`./${projectDirectory}`)}`)
37
52
  const templateURL = `https://registry.npmjs.org/@semaphore-protocol/cli-template-hardhat/-/cli-template-hardhat-${version}.tgz`
@@ -43,13 +58,15 @@ program
43
58
 
44
59
  spinner.start()
45
60
 
61
+ await checkLatestVersion(version)
62
+
46
63
  await download(templateURL, currentDirectory, { extract: true })
47
64
 
48
65
  renameSync(`${currentDirectory}/package`, `${currentDirectory}/${projectDirectory}`)
49
66
 
50
67
  spinner.stop()
51
68
 
52
- console.info(` ${logSymbols.success}`, `Your project is ready!\n`)
69
+ console.info(`\n ${logSymbols.success}`, `Your project is ready!\n`)
53
70
  console.info(` Please, install your dependencies by running:\n`)
54
71
  console.info(` ${chalk.cyan("cd")} ${projectDirectory}`)
55
72
  console.info(` ${chalk.cyan("npm i")}\n`)
@@ -72,9 +89,21 @@ program
72
89
  program
73
90
  .command("get-groups")
74
91
  .description("Get the list of groups from a supported network (goerli or arbitrum).")
75
- .option("-n, --network <network-name>", "Supported Ethereum network.", "goerli")
92
+ .option("-n, --network <network-name>", "Supported Ethereum network.")
93
+ .allowExcessArguments(false)
76
94
  .action(async ({ network }) => {
77
- if (!["goerli", "arbitrum"].includes(network)) {
95
+ if (!network) {
96
+ const { selectedNetwork } = await inquirer.prompt({
97
+ name: "selectedNetwork",
98
+ type: "list",
99
+ message: "Select one of our supported networks:",
100
+ default: supportedNetworks[0],
101
+ choices: supportedNetworks
102
+ })
103
+ network = selectedNetwork
104
+ }
105
+
106
+ if (!supportedNetworks.includes(network)) {
78
107
  console.info(`\n ${logSymbols.error}`, `error: the network '${network}' is not supported\n`)
79
108
  return
80
109
  }
@@ -90,29 +119,91 @@ program
90
119
  spinner.stop()
91
120
 
92
121
  if (groups.length === 0) {
93
- console.info(` ${logSymbols.error}`, "error: there are no groups in this network\n")
122
+ console.info(`\n ${logSymbols.info}`, "info: there are no groups in this network\n")
94
123
  return
95
124
  }
96
125
 
97
- const content = `${groups.map(({ id }: any) => ` - ${id}`).join("\n")}`
126
+ const content = `\n${groups.map(({ id }: any) => ` - ${id}`).join("\n")}`
98
127
 
99
128
  console.info(`${content}\n`)
100
129
  } catch (error) {
101
130
  spinner.stop()
102
131
 
103
- console.info(` ${logSymbols.error}`, "error: unexpected error with the Semaphore subgraph")
132
+ console.info(`\n ${logSymbols.error}`, "error: unexpected error with the Semaphore subgraph")
104
133
  }
105
134
  })
106
135
 
107
136
  program
108
137
  .command("get-group")
109
138
  .description("Get the data of a group from a supported network (Goerli or Arbitrum).")
110
- .argument("<group-id>", "Identifier of the group.")
111
- .option("-n, --network <network-name>", "Supported Ethereum network.", "goerli")
112
- .option("--members", "Show group members.")
113
- .option("--signals", "Show group signals.")
139
+ .argument("[group-id]", "Identifier of the group.")
140
+ .option("-n, --network <network-name>", "Supported Ethereum network.")
141
+ .option("-m, --members", "Show group members.")
142
+ .option("-s, --signals", "Show group signals.")
143
+ .allowExcessArguments(false)
114
144
  .action(async (groupId, { network, members, signals }) => {
115
- if (!["goerli", "arbitrum"].includes(network)) {
145
+ if (!network) {
146
+ const { selectedNetwork } = await inquirer.prompt({
147
+ name: "selectedNetwork",
148
+ type: "list",
149
+ message: "Select one of our supported networks:",
150
+ default: supportedNetworks[0],
151
+ choices: supportedNetworks
152
+ })
153
+ network = selectedNetwork
154
+ }
155
+
156
+ if (!groupId) {
157
+ const subgraphGroups = new Subgraph(network)
158
+ const spinnerGroups = new Spinner("Fetching groups")
159
+ spinnerGroups.start()
160
+ try {
161
+ const groups = await subgraphGroups.getGroups()
162
+
163
+ spinnerGroups.stop()
164
+
165
+ if (groups.length === 0) {
166
+ console.info(`\n ${logSymbols.info}`, "info: there are no groups in this network\n")
167
+ return
168
+ }
169
+
170
+ const groupIds = groups.map(({ id }: any) => id)
171
+
172
+ const { selectedGroupId } = await inquirer.prompt({
173
+ name: "selectedGroupId",
174
+ type: "list",
175
+ message: "Select one of the following existing group ids:",
176
+ choices: groupIds
177
+ })
178
+ groupId = selectedGroupId
179
+ } catch (error) {
180
+ spinnerGroups.stop()
181
+ console.info(`\n ${logSymbols.error}`, "error: unexpected error with the Semaphore subgraph")
182
+ return
183
+ }
184
+ }
185
+
186
+ if (!members && !signals) {
187
+ const { showMembers } = await inquirer.prompt({
188
+ name: "showMembers",
189
+ type: "confirm",
190
+ message: "Do you want to show members?",
191
+ default: false
192
+ })
193
+
194
+ members = showMembers
195
+
196
+ const { showSignals } = await inquirer.prompt({
197
+ name: "showSignals",
198
+ type: "confirm",
199
+ message: "Do you want to show signals?",
200
+ default: false
201
+ })
202
+
203
+ signals = showSignals
204
+ }
205
+
206
+ if (!supportedNetworks.includes(network)) {
116
207
  console.info(`\n ${logSymbols.error}`, `error: the network '${network}' is not supported\n`)
117
208
  return
118
209
  }
@@ -128,7 +219,7 @@ program
128
219
  spinner.stop()
129
220
 
130
221
  if (!group) {
131
- console.info(` ${logSymbols.error}`, "error: the group does not exist\n")
222
+ console.info(`\n ${logSymbols.error}`, "error: the group does not exist\n")
132
223
 
133
224
  return
134
225
  }
@@ -153,12 +244,12 @@ program
153
244
  .join("\n")}`
154
245
  }
155
246
 
156
- console.info(`${content}\n`)
247
+ console.info(`\n${content}\n`)
157
248
  } catch (error) {
158
249
  spinner.stop()
159
250
 
160
- console.info(` ${logSymbols.error}`, "error: unexpected error with the Semaphore subgraph")
251
+ console.info(`\n ${logSymbols.error}`, "error: unexpected error with the Semaphore subgraph")
161
252
  }
162
253
  })
163
254
 
164
- program.parse()
255
+ program.parse(process.argv)
package/src/spinner.ts CHANGED
@@ -20,6 +20,6 @@ export default class Spinner {
20
20
  stop() {
21
21
  this.ora.stop()
22
22
 
23
- process.stdout.cursorTo(0)
23
+ process.stdout.moveCursor(0, -1)
24
24
  }
25
25
  }