exb-community-cli 1.0.1 → 1.0.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
@@ -4,9 +4,11 @@ A simple, lightweight command line interface to make using Experience Builder De
4
4
 
5
5
  ## Using this CLI
6
6
 
7
- The ExB Community CLI has a few tools to make it easy to install, update, remove, and search for Experience Builder widgets that have been posted on NPM. Installing the CLI is as simple as running `npm i exb-community-cli` in your client directory of your Experience Builder application. To then run commands, the format is `npx exb-cli [command]`. If you don't want to prefix every command with npx, you can install the exb-community-cli globally, by running `npm i -g exb-community-cli`. For information on the commands that exist, run `npx exb-cli help`, and commands, arguments, and descriptions will be listed.
7
+ The ExB Community CLI has a few tools to make it easy to install, update, remove, and search for Experience Builder widgets that have been posted on NPM. Installing the CLI is as simple as running `npm i exb-community-cli` in your client directory of your Experience Builder application. To then run commands, the format is `npx exb-cli [command]`. If you don't want to prefix every command with npx, you can install the exb-community-cli globally, by running `npm i -g exb-community-cli` (Recommended). For information on the commands that exist, run `npx exb-cli help`, and commands, arguments, and descriptions will be listed.
8
8
 
9
- When using any of the ExB Community CLI commands, the command needs to be run in the client directory of your Experience builder application. If you don't, you will get the error `Error: Run this from the ExB client folder.`.
9
+ Additionally, this CLI makes it easy to get up and running with Experience Builder, requiring a single command to install and run Experience Builder Developer Edition, making it easier than ever to get started. Just install the CLI globally `npm-i -g exb-community-cli` and then run `exb-cli dev-setup`. The tool will give you a few prompts to get started, and you'll end up with a fully functional install.
10
+
11
+ When using any of the ExB Community CLI commands, the command needs to be run in the client directory of your Experience builder application (excluding dev-setup). If you don't, you will get the error `Error: Run this from the ExB client folder.`.
10
12
 
11
13
  ## Formatting widgets for NPM (And Best Practices)
12
14
 
@@ -0,0 +1,237 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.devSetup = devSetup;
7
+ const fs_extra_1 = __importDefault(require("fs-extra"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const promises_1 = require("stream/promises");
10
+ const fs_1 = require("fs");
11
+ const child_process_1 = require("child_process");
12
+ const readline_1 = require("readline");
13
+ const enquirer_1 = __importDefault(require("enquirer"));
14
+ const os_1 = __importDefault(require("os"));
15
+ async function devSetup(options) {
16
+ let version = options.version;
17
+ const response = await fetch("https://developers.arcgis.com/experience-builder/guide/downloads/");
18
+ const html = await response.text();
19
+ // Extract version numbers from the page using a regex
20
+ const versions = [...html.matchAll(/py-4 pr-3">v(\d+\.\d+)/g)].map((m) => m[1]);
21
+ if (options.version && options.version == "latest") {
22
+ console.log("Setting up development environment for latest version...");
23
+ const latestVersion = versions[0];
24
+ version = latestVersion;
25
+ }
26
+ else if (options.version === undefined) {
27
+ const prompt = new enquirer_1.default.Select({
28
+ name: "version",
29
+ message: "Choose an Experience Builder version",
30
+ choices: versions,
31
+ limit: 5
32
+ });
33
+ const chosenVersion = await prompt.run();
34
+ version = chosenVersion;
35
+ }
36
+ // Actually install and set up the chosen version of Experience Builder
37
+ await installExperienceBuilder(version);
38
+ // Ask user if they want to start the development servers now
39
+ const approveStart = await new enquirer_1.default.Confirm({
40
+ name: "start",
41
+ message: "Do you want to start the development server now?"
42
+ }).run();
43
+ if (!approveStart) {
44
+ console.log("You can start the development server later by running `npm start` in the client and server folders.");
45
+ return;
46
+ }
47
+ const clientDir = path_1.default.join(process.cwd(), `arcgis-experience-builder-${version}`, "client");
48
+ const serverDir = path_1.default.join(process.cwd(), `arcgis-experience-builder-${version}`, "server");
49
+ console.log("Starting development servers in new terminal windows...");
50
+ openInNewTerminal(clientDir, "client");
51
+ openInNewTerminal(serverDir, "server");
52
+ }
53
+ const ciWithSpinner = (message, cwd) => {
54
+ const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
55
+ let frameIndex = 0;
56
+ return new Promise((resolve, reject) => {
57
+ const interval = setInterval(() => {
58
+ frameIndex = (frameIndex + 1) % spinnerFrames.length;
59
+ process.stdout.write(`\r ${spinnerFrames[frameIndex]} ${message}`);
60
+ }, 80);
61
+ process.stdout.write(` ${spinnerFrames[0]} ${message}`);
62
+ const proc = (0, child_process_1.spawn)("npm", ["ci"], { cwd, stdio: "pipe" });
63
+ proc.on("close", (code) => {
64
+ clearInterval(interval);
65
+ if (code === 0) {
66
+ process.stdout.write(`\r ✔ ${message} done.\n`);
67
+ resolve();
68
+ }
69
+ else {
70
+ process.stdout.write(`\r ✖ ${message} failed.\n`);
71
+ reject(new Error(`npm ci exited with code ${code}`));
72
+ }
73
+ });
74
+ proc.on("error", (err) => {
75
+ clearInterval(interval);
76
+ reject(err);
77
+ });
78
+ });
79
+ };
80
+ const openInNewTerminal = (cwd, label) => {
81
+ const cmd = `cd "${cwd}" && npm start`;
82
+ if (process.platform === "darwin") {
83
+ const script = `tell application "Terminal"\nactivate\ndo script "${cmd.replace(/"/g, '\\"')}"\nend tell`;
84
+ (0, child_process_1.spawn)("osascript", ["-e", script], {
85
+ stdio: "ignore",
86
+ detached: true
87
+ }).unref();
88
+ }
89
+ else if (process.platform === "win32") {
90
+ (0, child_process_1.spawn)("cmd.exe", ["/c", "start", "cmd.exe", "/k", cmd], {
91
+ cwd,
92
+ stdio: "ignore",
93
+ detached: true,
94
+ shell: true
95
+ }).unref();
96
+ }
97
+ else {
98
+ // Linux: try common terminal emulators in order
99
+ const terminals = [
100
+ {
101
+ bin: "x-terminal-emulator",
102
+ args: ["-e", `bash -c '${cmd}; exec bash'`]
103
+ },
104
+ {
105
+ bin: "gnome-terminal",
106
+ args: ["--", "bash", "-c", `${cmd}; exec bash`]
107
+ },
108
+ { bin: "konsole", args: ["-e", "bash", "-c", `${cmd}; exec bash`] },
109
+ {
110
+ bin: "xfce4-terminal",
111
+ args: ["-e", `bash -c '${cmd}; exec bash'`]
112
+ },
113
+ { bin: "xterm", args: ["-e", `bash -c '${cmd}; exec bash'`] }
114
+ ];
115
+ let launched = false;
116
+ for (const t of terminals) {
117
+ try {
118
+ (0, child_process_1.execSync)(`which ${t.bin}`, { stdio: "pipe" });
119
+ (0, child_process_1.spawn)(t.bin, t.args, {
120
+ cwd,
121
+ stdio: "ignore",
122
+ detached: true
123
+ }).unref();
124
+ launched = true;
125
+ break;
126
+ }
127
+ catch {
128
+ // not found, try next
129
+ }
130
+ }
131
+ if (!launched) {
132
+ console.log(` ⚠ No supported terminal found. Run manually: cd "${cwd}" && npm start`);
133
+ return;
134
+ }
135
+ }
136
+ console.log(` ✔ Opened ${label} in a new terminal window.`);
137
+ };
138
+ const installExperienceBuilder = async (version) => {
139
+ const url = `https://downloads.arcgis.com/dms/rest/download/secured/arcgis-experience-builder-${version}.zip?f=json&folder=software%2FExperienceBuilder%2F${version}`;
140
+ const res = await fetch(url);
141
+ if (!res.ok) {
142
+ throw new Error(`Failed to fetch download info: ${res.status} ${res.statusText}`);
143
+ }
144
+ const json = (await res.json());
145
+ const downloadUrl = json.url;
146
+ if (!downloadUrl) {
147
+ throw new Error("No download URL found in response");
148
+ }
149
+ // Download the zip file to a temporary location
150
+ const zipPath = path_1.default.join(os_1.default.tmpdir(), `exb-install-${Date.now()}`);
151
+ const extractDir = path_1.default.resolve(process.cwd(), `arcgis-experience-builder-${version}`);
152
+ try {
153
+ console.log(`Downloading zip`);
154
+ const zipRes = await fetch(downloadUrl);
155
+ if (!zipRes.ok || !zipRes.body) {
156
+ throw new Error(`Failed to download zip: ${zipRes.status} ${zipRes.statusText}`);
157
+ }
158
+ await (0, promises_1.pipeline)(zipRes.body, (0, fs_1.createWriteStream)(zipPath));
159
+ // Extract the zip file to a folder in the current directory
160
+ console.log(`Extracting to ${extractDir}...`);
161
+ await fs_extra_1.default.ensureDir(extractDir);
162
+ // Get total file count for progress bar (pipe through tail to avoid buffer overflow on large zips)
163
+ const listSummary = (0, child_process_1.execSync)(`unzip -l "${zipPath}" | tail -1`, {
164
+ encoding: "utf-8"
165
+ });
166
+ const countMatch = listSummary.match(/(\d+)\s+files?/);
167
+ const totalFiles = countMatch ? parseInt(countMatch[1], 10) : 1;
168
+ await new Promise((resolve, reject) => {
169
+ let extracted = 0;
170
+ const proc = (0, child_process_1.spawn)("unzip", ["-o", zipPath, "-d", extractDir]);
171
+ const rl = (0, readline_1.createInterface)({ input: proc.stdout });
172
+ rl.on("line", (line) => {
173
+ if (/^\s*(extracting|inflating|creating):/.test(line)) {
174
+ extracted++;
175
+ const pct = Math.min(100, Math.round((extracted / totalFiles) * 100));
176
+ const filled = Math.round(pct / 4);
177
+ const bar = "█".repeat(filled) + "░".repeat(25 - filled);
178
+ process.stdout.write(`\r [${bar}] ${pct}% (${extracted}/${totalFiles})`);
179
+ }
180
+ });
181
+ proc.stderr.on("data", () => { }); // suppress stderr
182
+ proc.on("close", (code) => {
183
+ process.stdout.write("\n");
184
+ if (code === 0)
185
+ resolve();
186
+ else
187
+ reject(new Error(`unzip exited with code ${code}`));
188
+ });
189
+ proc.on("error", reject);
190
+ });
191
+ // Remove any directory nesting (some zip versions include an extra top-level folder)
192
+ const entries = await fs_extra_1.default.readdir(extractDir);
193
+ const hasClient = entries.includes("client");
194
+ const hasServer = entries.includes("server");
195
+ if (!hasClient || !hasServer) {
196
+ // Check if there's a single nested directory containing client/ and server/
197
+ const dirs = [];
198
+ for (const entry of entries) {
199
+ const stat = await fs_extra_1.default.stat(path_1.default.join(extractDir, entry));
200
+ if (stat.isDirectory())
201
+ dirs.push(entry);
202
+ }
203
+ if (dirs.length === 1) {
204
+ const nestedDir = path_1.default.join(extractDir, dirs[0]);
205
+ const nestedEntries = await fs_extra_1.default.readdir(nestedDir);
206
+ if (nestedEntries.includes("client") &&
207
+ nestedEntries.includes("server")) {
208
+ // Move all contents from the nested dir up to extractDir
209
+ for (const item of nestedEntries) {
210
+ await fs_extra_1.default.move(path_1.default.join(nestedDir, item), path_1.default.join(extractDir, item), { overwrite: true });
211
+ }
212
+ await fs_extra_1.default.remove(nestedDir);
213
+ }
214
+ else {
215
+ throw new Error("Extracted zip does not contain expected client/ and server/ folders");
216
+ }
217
+ }
218
+ else {
219
+ throw new Error("Extracted zip does not contain expected client/ and server/ folders");
220
+ }
221
+ }
222
+ // Install dependencies in client and server folders
223
+ const clientDir = path_1.default.join(extractDir, "client");
224
+ const serverDir = path_1.default.join(extractDir, "server");
225
+ await ciWithSpinner("Installing client dependencies...", clientDir);
226
+ await ciWithSpinner("Installing server dependencies...", serverDir);
227
+ return extractDir;
228
+ }
229
+ catch (err) {
230
+ console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
231
+ return "";
232
+ }
233
+ finally {
234
+ // Clean up the zip file
235
+ await fs_extra_1.default.remove(zipPath);
236
+ }
237
+ };
@@ -60,7 +60,7 @@ async function formatWidget(widgetNameOrFolder, options = {}) {
60
60
  // Collect package.json fields from manifest or prompt for missing values
61
61
  const pkg = {};
62
62
  const initialName = (manifest.name ?? (await askForValue('package name (npm-safe)'))) || '';
63
- pkg.name = await validateAndChooseName(initialName, options.force);
63
+ pkg.name = await validateAndChooseName(initialName.toLowerCase(), options.force);
64
64
  pkg.version = (await askForValue('version', '1.0.0')) || '1.0.0';
65
65
  pkg.description = manifest.description ?? (await askForValue('description')) ?? '';
66
66
  pkg.author = manifest.author ?? (await askForValue('author')) ?? '';
@@ -10,8 +10,17 @@ const os_1 = __importDefault(require("os"));
10
10
  const degit_1 = __importDefault(require("degit"));
11
11
  const enquirer_1 = __importDefault(require("enquirer"));
12
12
  const manifestCollector_1 = require("../utils/manifestCollector");
13
- const format_1 = require("./format");
14
13
  async function scaffoldWidget(name) {
14
+ const rootDir = process.cwd();
15
+ const widgetsDir = path_1.default.join(rootDir, 'your-extensions', 'widgets');
16
+ if (!fs_extra_1.default.existsSync(path_1.default.join(rootDir, 'your-extensions'))) {
17
+ console.error('Error: Run this from the ExB client folder.');
18
+ return;
19
+ }
20
+ if (!(await fs_extra_1.default.pathExists(widgetsDir))) {
21
+ console.error('Error: No widgets folder found (your-extensions/widgets).');
22
+ return;
23
+ }
15
24
  const repoSource = "Esri/arcgis-experience-builder-sdk-resources/widgets";
16
25
  const tmpDir = path_1.default.join(os_1.default.tmpdir(), `exb-scaffold-${Date.now()}`);
17
26
  await fs_extra_1.default.ensureDir(tmpDir);
@@ -45,7 +54,8 @@ async function scaffoldWidget(name) {
45
54
  await fs_extra_1.default.copy(widgetFolder, targetPath);
46
55
  const manifestPath = path_1.default.join(targetPath, 'manifest.json');
47
56
  const manifest = await fs_extra_1.default.readJson(manifestPath);
48
- manifest.name = await (0, format_1.validateAndChooseName)(name);
57
+ //manifest.name = await validateAndChooseName(name);
58
+ manifest.name = name;
49
59
  await fs_extra_1.default.writeJson(manifestPath, manifest, { spaces: 2 });
50
60
  console.log(`${chosen.manifest.label ?? chosen.manifest.name} scaffolded to ${targetPath}`);
51
61
  }
package/dist/index.js CHANGED
@@ -45,70 +45,90 @@ const format_1 = require("./commands/format");
45
45
  const scaffold_1 = require("./commands/scaffold");
46
46
  const fs = __importStar(require("fs-extra"));
47
47
  const path = __importStar(require("path"));
48
- const packageJsonPath = path.join(__dirname, '../package.json');
49
- const packageJson = fs.readJsonSync(packageJsonPath, { throws: false }) || { version: '1.0.0' };
48
+ const dev_setup_1 = require("./commands/dev-setup");
49
+ const packageJsonPath = path.join(__dirname, "../package.json");
50
+ const packageJson = fs.readJsonSync(packageJsonPath, { throws: false }) || {
51
+ version: "1.0.0"
52
+ };
50
53
  const program = new commander_1.Command();
51
54
  program
52
- .name('exb-cli')
53
- .description('The community-led widget manager for ArcGIS Experience Builder')
55
+ .name("exb-cli")
56
+ .description("The community-led widget manager for ArcGIS Experience Builder")
54
57
  .version(packageJson.version);
55
58
  // --- COMMAND: INSTALL ---
56
59
  program
57
- .command('install <package>')
58
- .alias('i') // Allows users to type `exb-cli i widget-name`
59
- .description('Install a widget from NPM into your Experience Builder project')
60
- .option('--widget-only', 'Skip running npm ci in the installed widget directory')
60
+ .command("install <package>")
61
+ .alias("i") // Allows users to type `exb-cli i widget-name`
62
+ .description("Install a widget from NPM into your Experience Builder project")
63
+ .option("--widget-only", "Skip running npm ci in the installed widget directory")
61
64
  .action(async (pkg, options) => {
62
65
  await (0, install_1.installWidget)(pkg, { widgetOnly: options.widgetOnly });
63
66
  });
64
67
  // --- COMMAND: UPDATE ---
65
68
  program
66
- .command('update <package>')
67
- .alias('u')
68
- .description('Update an installed widget to the latest version (or a specified version)')
69
- .option('--widget-only', 'Skip running npm ci in the updated widget directory')
70
- .option('--version <version>', 'Specify a version or dist-tag to update to')
69
+ .command("update <package>")
70
+ .alias("u")
71
+ .description("Update an installed widget to the latest version (or a specified version)")
72
+ .option("--widget-only", "Skip running npm ci in the updated widget directory")
73
+ .option("--version <version>", "Specify a version or dist-tag to update to")
71
74
  .action(async (pkg, options) => {
72
- await (0, update_1.updateWidget)(pkg, { widgetOnly: options.widgetOnly, version: options.version });
75
+ await (0, update_1.updateWidget)(pkg, {
76
+ widgetOnly: options.widgetOnly,
77
+ version: options.version
78
+ });
73
79
  });
74
80
  // --- COMMAND: REMOVE ---
75
81
  program
76
- .command('remove <package>')
77
- .alias('rm')
78
- .description('Remove an installed widget from your Experience Builder project')
79
- .option('-f, --force', 'Skip confirmation prompt')
82
+ .command("remove <package>")
83
+ .alias("rm")
84
+ .description("Remove an installed widget from your Experience Builder project")
85
+ .option("-f, --force", "Skip confirmation prompt")
80
86
  .action(async (pkg, options) => {
81
87
  await (0, remove_1.removeWidget)(pkg, { force: options.force });
82
88
  });
83
89
  // --- COMMAND: SEARCH ---
84
90
  program
85
- .command('search')
86
- .alias('s')
87
- .description('Search npm for Experience Builder widgets (by keyword)')
88
- .option('-k, --keyword <keyword>', 'Additional keyword to include in search')
89
- .option('-n, --size <size>', 'Maximum number of npm results to fetch (default: 15)')
90
- .option('--github-list', 'Include results from a curated GitHub list when available')
91
+ .command("search")
92
+ .alias("s")
93
+ .description("Search npm for Experience Builder widgets (by keyword)")
94
+ .option("-k, --keyword <keyword>", "Additional keyword to include in search")
95
+ .option("-n, --size <size>", "Maximum number of npm results to fetch (default: 15)")
96
+ .option("--github-list", "Include results from a curated GitHub list when available")
91
97
  .action(async (options) => {
92
98
  const size = options.size ? Number(options.size) : undefined;
93
- await (0, search_1.searchWidgets)({ keyword: options.keyword, size, githubList: options.githubList });
99
+ await (0, search_1.searchWidgets)({
100
+ keyword: options.keyword,
101
+ size,
102
+ githubList: options.githubList
103
+ });
94
104
  });
95
105
  // --- COMMAND: FORMAT ---
96
106
  program
97
- .command('format <widget>')
98
- .alias('fmt')
99
- .description('Format a widget package according to community standards, based on the manifest.json configuration.')
100
- .option('-f, --force', 'Skip prompts and overwrite package.json if present')
107
+ .command("format <widget>")
108
+ .alias("fmt")
109
+ .description("Format a widget package according to community standards, based on the manifest.json configuration.")
110
+ .option("-f, --force", "Skip prompts and overwrite package.json if present")
101
111
  .action(async (widget, options) => {
102
112
  await (0, format_1.formatWidget)(widget, { force: options.force });
103
113
  });
104
114
  // --- COMMAND: SCAFFOLD ---
105
115
  program
106
- .command('scaffold <name>')
107
- .alias('new')
108
- .description('Scaffold a new widget package with a manifest.json and package.json, using a template from the arcgis-experience-builder-sdk-resources repository.')
116
+ .command("scaffold <name>")
117
+ .alias("new")
118
+ .description("Scaffold a new widget package with a manifest.json, using an esri template")
109
119
  .action(async (name, options) => {
110
120
  await (0, scaffold_1.scaffoldWidget)(name);
111
121
  });
122
+ program
123
+ .command("dev-setup")
124
+ .description("Download and set up the local development environment for Experience Builder widget development")
125
+ .option("--latest", "Download the latest version of Experience Builder Developer Edition")
126
+ .option("-v, --version <version>", "Specify a version of Experience Builder Developer Edition to download")
127
+ .action(async (options) => {
128
+ const version = options.version || (options.latest ? "latest" : undefined);
129
+ (0, dev_setup_1.devSetup)({ version });
130
+ //await scaffoldWidget('dev-environment', { version });
131
+ });
112
132
  // Parse the arguments passed by the user in the terminal
113
133
  program.parse(process.argv);
114
134
  // If the user runs the CLI with no arguments, show the help menu
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "exb-community-cli",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "A lightweight command line interface to allow Experience Builder developers to quickly import widgets published as NPM pacakges.",
5
5
  "keywords": [
6
6
  "exb-community",
@@ -24,12 +24,12 @@
24
24
  ],
25
25
  "dependencies": {
26
26
  "commander": "^14.0.3",
27
+ "degit": "^2.8.0",
28
+ "enquirer": "^2.3.6",
27
29
  "fs-extra": "^11.3.3",
28
30
  "pacote": "^21.3.1",
29
31
  "semver": "^7.7.4",
30
- "validate-npm-package-name": "^7.0.2",
31
- "degit": "^2.8.0",
32
- "enquirer": "^2.3.6"
32
+ "validate-npm-package-name": "^7.0.2"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@types/fs-extra": "^11.0.4",
@@ -42,4 +42,4 @@
42
42
  "ts-jest": "^29.1.0",
43
43
  "typescript": "^5.9.3"
44
44
  }
45
- }
45
+ }