exb-community-cli 1.0.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/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # ExB Community CLI
2
+
3
+ A simple, lightweight command line interface to make using Experience Builder Developer Edition easier for new developers, looking to add widgets to their collection.
@@ -0,0 +1,51 @@
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.installWidget = installWidget;
7
+ const pacote_1 = __importDefault(require("pacote"));
8
+ const fs_extra_1 = __importDefault(require("fs-extra"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const os_1 = __importDefault(require("os"));
11
+ const npm_1 = require("../utils/npm");
12
+ async function installWidget(packageName, options = {}) {
13
+ const rootDir = process.cwd();
14
+ const tempDir = path_1.default.join(os_1.default.tmpdir(), `exb-install-${Date.now()}`);
15
+ const extensionsDir = 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
+ try {
21
+ console.log(`Fetching ${packageName}...`);
22
+ await pacote_1.default.extract(packageName, tempDir);
23
+ const { pkgContentPath, manifestPath } = await resolveManifest(tempDir);
24
+ // Using our interface for type safety
25
+ const manifest = await fs_extra_1.default.readJson(manifestPath);
26
+ const finalDestination = path_1.default.join(extensionsDir, manifest.name);
27
+ if (fs_extra_1.default.existsSync(finalDestination)) {
28
+ await fs_extra_1.default.remove(finalDestination);
29
+ }
30
+ await fs_extra_1.default.move(pkgContentPath, finalDestination);
31
+ console.log(`Installed ${manifest.name} successfully!`);
32
+ await (0, npm_1.runNpmCi)(finalDestination, { skip: options.widgetOnly });
33
+ }
34
+ catch (err) {
35
+ console.error(`Error: Failed to install widget: ${err.message}`);
36
+ }
37
+ finally {
38
+ await fs_extra_1.default.remove(tempDir);
39
+ }
40
+ }
41
+ async function resolveManifest(tempDir) {
42
+ const directManifest = path_1.default.join(tempDir, 'manifest.json');
43
+ const nestedManifest = path_1.default.join(tempDir, 'package', 'manifest.json');
44
+ if (await fs_extra_1.default.pathExists(directManifest)) {
45
+ return { pkgContentPath: tempDir, manifestPath: directManifest };
46
+ }
47
+ if (await fs_extra_1.default.pathExists(nestedManifest)) {
48
+ return { pkgContentPath: path_1.default.join(tempDir, 'package'), manifestPath: nestedManifest };
49
+ }
50
+ throw new Error('Invalid ExB widget: missing manifest.json');
51
+ }
@@ -0,0 +1,116 @@
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.searchWidgets = searchWidgets;
7
+ const https_1 = __importDefault(require("https"));
8
+ const pacote_1 = __importDefault(require("pacote"));
9
+ const fs_extra_1 = __importDefault(require("fs-extra"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const os_1 = __importDefault(require("os"));
12
+ const DEFAULT_KEYWORDS = ['arcgis-exb-widget', 'experience-builder-widget'];
13
+ const DEFAULT_SIZE = 15;
14
+ async function searchWidgets(options = {}) {
15
+ if (options.githubList) {
16
+ console.log('GitHub list search is not yet implemented; falling back to npm search.');
17
+ }
18
+ const keywords = options.keyword ? [options.keyword] : DEFAULT_KEYWORDS;
19
+ const size = options.size ?? DEFAULT_SIZE;
20
+ let results = [];
21
+ try {
22
+ results = await searchNpm(keywords, size);
23
+ }
24
+ catch (err) {
25
+ console.error(`Failed to search npm: ${err.message}`);
26
+ return;
27
+ }
28
+ if (!results.length) {
29
+ console.log('No npm results found for the provided keywords.');
30
+ return;
31
+ }
32
+ const validated = [];
33
+ for (const pkg of results) {
34
+ const validatedPkg = await validateExbWidget(pkg.name, pkg.version);
35
+ if (validatedPkg) {
36
+ validated.push({ ...pkg, ...validatedPkg });
37
+ }
38
+ }
39
+ if (!validated.length) {
40
+ console.log('No valid Experience Builder widgets found in npm search results.');
41
+ return;
42
+ }
43
+ console.log(`Found ${validated.length} widget(s):`);
44
+ validated.forEach((pkg, idx) => {
45
+ const parts = [
46
+ `${idx + 1}. ${pkg.name}@${pkg.version}`,
47
+ pkg.label ? `label: ${pkg.label}` : undefined,
48
+ pkg.exbVersion ? `exbVersion: ${pkg.exbVersion}` : undefined,
49
+ pkg.description
50
+ ].filter(Boolean);
51
+ console.log(parts.join(' | '));
52
+ });
53
+ }
54
+ async function searchNpm(keywords, size) {
55
+ const text = keywords.map((k) => `keywords:${k}`).join(' ');
56
+ const url = `https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(text)}&size=${size}`;
57
+ const payload = await fetchJson(url);
58
+ if (!payload || !Array.isArray(payload.objects)) {
59
+ return [];
60
+ }
61
+ return payload.objects.map((obj) => ({
62
+ name: obj?.package?.name,
63
+ version: obj?.package?.version,
64
+ description: obj?.package?.description
65
+ })).filter((p) => Boolean(p.name && p.version));
66
+ }
67
+ async function validateExbWidget(pkgName, pkgVersion) {
68
+ const tempDir = path_1.default.join(os_1.default.tmpdir(), `exb-search-${pkgName.replace('/', '-')}-${Date.now()}`);
69
+ try {
70
+ await pacote_1.default.extract(`${pkgName}@${pkgVersion}`, tempDir);
71
+ const { manifestPath } = await resolveManifest(tempDir);
72
+ const manifest = await fs_extra_1.default.readJson(manifestPath);
73
+ if (manifest.type !== 'widget') {
74
+ return null;
75
+ }
76
+ return { exbVersion: manifest.exbVersion, label: manifest.label ?? manifest.name };
77
+ }
78
+ catch (err) {
79
+ return null;
80
+ }
81
+ finally {
82
+ await fs_extra_1.default.remove(tempDir);
83
+ }
84
+ }
85
+ async function resolveManifest(tempDir) {
86
+ const directManifest = path_1.default.join(tempDir, 'manifest.json');
87
+ const nestedManifest = path_1.default.join(tempDir, 'package', 'manifest.json');
88
+ if (await fs_extra_1.default.pathExists(directManifest)) {
89
+ return { pkgContentPath: tempDir, manifestPath: directManifest };
90
+ }
91
+ if (await fs_extra_1.default.pathExists(nestedManifest)) {
92
+ return { pkgContentPath: path_1.default.join(tempDir, 'package'), manifestPath: nestedManifest };
93
+ }
94
+ throw new Error('Invalid ExB widget: missing manifest.json');
95
+ }
96
+ async function fetchJson(url) {
97
+ return new Promise((resolve, reject) => {
98
+ const req = https_1.default.get(url, { headers: { 'User-Agent': 'exb-community-cli' } }, (res) => {
99
+ if (res.statusCode && res.statusCode >= 400) {
100
+ reject(new Error(`HTTP ${res.statusCode}`));
101
+ return;
102
+ }
103
+ let data = '';
104
+ res.on('data', (chunk) => { data += chunk; });
105
+ res.on('end', () => {
106
+ try {
107
+ resolve(JSON.parse(data));
108
+ }
109
+ catch (err) {
110
+ reject(err);
111
+ }
112
+ });
113
+ });
114
+ req.on('error', reject);
115
+ });
116
+ }
@@ -0,0 +1,71 @@
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.updateWidget = updateWidget;
7
+ const pacote_1 = __importDefault(require("pacote"));
8
+ const fs_extra_1 = __importDefault(require("fs-extra"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const os_1 = __importDefault(require("os"));
11
+ const semver_1 = __importDefault(require("semver"));
12
+ const npm_1 = require("../utils/npm");
13
+ async function updateWidget(packageName, options = {}) {
14
+ const rootDir = process.cwd();
15
+ const tempDir = path_1.default.join(os_1.default.tmpdir(), `exb-update-${Date.now()}`);
16
+ const extensionsDir = path_1.default.join(rootDir, 'your-extensions', 'widgets');
17
+ if (!fs_extra_1.default.existsSync(path_1.default.join(rootDir, 'your-extensions'))) {
18
+ console.error('Error: Run this from the ExB client folder.');
19
+ return;
20
+ }
21
+ const spec = options.version ? `${packageName}@${options.version}` : packageName;
22
+ try {
23
+ console.log(`Fetching ${spec}...`);
24
+ const incomingPkg = await pacote_1.default.manifest(spec);
25
+ await pacote_1.default.extract(spec, tempDir);
26
+ const { pkgContentPath, manifestPath: newManifestPath } = await resolveManifest(tempDir);
27
+ const newManifest = await fs_extra_1.default.readJson(newManifestPath);
28
+ const installedDir = path_1.default.join(extensionsDir, newManifest.name);
29
+ const installedManifestPath = path_1.default.join(installedDir, 'manifest.json');
30
+ const installedPkgJsonPath = path_1.default.join(installedDir, 'package.json');
31
+ if (!(await fs_extra_1.default.pathExists(installedManifestPath))) {
32
+ throw new Error(`Widget ${newManifest.name} is not installed; cannot update.`);
33
+ }
34
+ const currentManifest = await fs_extra_1.default.readJson(installedManifestPath);
35
+ const currentPkgJson = (await fs_extra_1.default.pathExists(installedPkgJsonPath)) ? await fs_extra_1.default.readJson(installedPkgJsonPath) : null;
36
+ const currentVersionRaw = currentPkgJson?.version ?? currentManifest.version;
37
+ const incomingVersionRaw = incomingPkg.version ?? newManifest.version;
38
+ const currentVersion = semver_1.default.valid(currentVersionRaw);
39
+ const incomingVersion = semver_1.default.valid(incomingVersionRaw);
40
+ if (!currentVersion || !incomingVersion) {
41
+ throw new Error('Unable to compare versions; expected valid semver from npm package metadata');
42
+ }
43
+ if (semver_1.default.eq(incomingVersion, currentVersion)) {
44
+ throw new Error(`Widget ${newManifest.name} is already at version ${currentVersionRaw}.`);
45
+ }
46
+ if (semver_1.default.lte(incomingVersion, currentVersion)) {
47
+ throw new Error(`New version ${incomingVersionRaw} is older than installed ${currentVersionRaw}.`);
48
+ }
49
+ await fs_extra_1.default.remove(installedDir);
50
+ await fs_extra_1.default.move(pkgContentPath, installedDir);
51
+ console.log(`Updated ${newManifest.name} to ${incomingVersionRaw}.`);
52
+ await (0, npm_1.runNpmCi)(installedDir, { skip: options.widgetOnly });
53
+ }
54
+ catch (err) {
55
+ console.error(`Error: Failed to update widget: ${err.message}`);
56
+ }
57
+ finally {
58
+ await fs_extra_1.default.remove(tempDir);
59
+ }
60
+ }
61
+ async function resolveManifest(tempDir) {
62
+ const directManifest = path_1.default.join(tempDir, 'manifest.json');
63
+ const nestedManifest = path_1.default.join(tempDir, 'package', 'manifest.json');
64
+ if (await fs_extra_1.default.pathExists(directManifest)) {
65
+ return { pkgContentPath: tempDir, manifestPath: directManifest };
66
+ }
67
+ if (await fs_extra_1.default.pathExists(nestedManifest)) {
68
+ return { pkgContentPath: path_1.default.join(tempDir, 'package'), manifestPath: nestedManifest };
69
+ }
70
+ throw new Error('Invalid ExB widget: missing manifest.json');
71
+ }
package/dist/index.js ADDED
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ // The line above is the "shebang". It MUST be the very first line of the file.
4
+ // It tells the user's operating system to execute this file using Node.js.
5
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ var desc = Object.getOwnPropertyDescriptor(m, k);
8
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
+ desc = { enumerable: true, get: function() { return m[k]; } };
10
+ }
11
+ Object.defineProperty(o, k2, desc);
12
+ }) : (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ o[k2] = m[k];
15
+ }));
16
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
17
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
18
+ }) : function(o, v) {
19
+ o["default"] = v;
20
+ });
21
+ var __importStar = (this && this.__importStar) || (function () {
22
+ var ownKeys = function(o) {
23
+ ownKeys = Object.getOwnPropertyNames || function (o) {
24
+ var ar = [];
25
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
26
+ return ar;
27
+ };
28
+ return ownKeys(o);
29
+ };
30
+ return function (mod) {
31
+ if (mod && mod.__esModule) return mod;
32
+ var result = {};
33
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
34
+ __setModuleDefault(result, mod);
35
+ return result;
36
+ };
37
+ })();
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ const commander_1 = require("commander");
40
+ const install_1 = require("./commands/install");
41
+ const update_1 = require("./commands/update");
42
+ const search_1 = require("./commands/search");
43
+ const fs = __importStar(require("fs-extra"));
44
+ const path = __importStar(require("path"));
45
+ const packageJsonPath = path.join(__dirname, '../package.json');
46
+ const packageJson = fs.readJsonSync(packageJsonPath, { throws: false }) || { version: '1.0.0' };
47
+ const program = new commander_1.Command();
48
+ program
49
+ .name('exb-cli')
50
+ .description('The community-led widget manager for ArcGIS Experience Builder')
51
+ .version(packageJson.version);
52
+ // --- COMMAND: INSTALL ---
53
+ program
54
+ .command('install <package>')
55
+ .alias('i') // Allows users to type `exb-cli i widget-name`
56
+ .description('Install a widget from NPM into your Experience Builder project')
57
+ .option('--widget-only', 'Skip running npm ci in the installed widget directory')
58
+ .action(async (pkg, options) => {
59
+ await (0, install_1.installWidget)(pkg, { widgetOnly: options.widgetOnly });
60
+ });
61
+ // --- COMMAND: UPDATE ---
62
+ program
63
+ .command('update <package>')
64
+ .alias('u')
65
+ .description('Update an installed widget to the latest version (or a specified version)')
66
+ .option('--widget-only', 'Skip running npm ci in the updated widget directory')
67
+ .option('--version <version>', 'Specify a version or dist-tag to update to')
68
+ .action(async (pkg, options) => {
69
+ await (0, update_1.updateWidget)(pkg, { widgetOnly: options.widgetOnly, version: options.version });
70
+ });
71
+ // --- COMMAND: LIST (Example for future expansion) ---
72
+ program
73
+ .command('list')
74
+ .alias('ls')
75
+ .description('List all community widgets currently installed in your project')
76
+ .action(() => {
77
+ console.log('Listing widgets is not yet implemented, but coming soon!');
78
+ // Future logic: scan the client/your-extensions/widgets folder and print the names
79
+ });
80
+ // --- COMMAND: SEARCH ---
81
+ program
82
+ .command('search')
83
+ .alias('s')
84
+ .description('Search npm for Experience Builder widgets (by keyword)')
85
+ .option('-k, --keyword <keyword>', 'Additional keyword to include in search')
86
+ .option('-n, --size <size>', 'Maximum number of npm results to fetch (default: 15)')
87
+ .option('--github-list', 'Include results from a curated GitHub list when available')
88
+ .action(async (options) => {
89
+ const size = options.size ? Number(options.size) : undefined;
90
+ await (0, search_1.searchWidgets)({ keyword: options.keyword, size, githubList: options.githubList });
91
+ });
92
+ // Parse the arguments passed by the user in the terminal
93
+ program.parse(process.argv);
94
+ // If the user runs the CLI with no arguments, show the help menu
95
+ if (!process.argv.slice(2).length) {
96
+ program.outputHelp();
97
+ }
@@ -0,0 +1,50 @@
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.runNpmCi = runNpmCi;
7
+ const fs_extra_1 = __importDefault(require("fs-extra"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const child_process_1 = require("child_process");
10
+ async function runNpmCi(widgetDir, options = {}) {
11
+ if (options.skip) {
12
+ console.log('Skipping npm ci because --widget-only was provided.');
13
+ return;
14
+ }
15
+ const shrinkwrap = path_1.default.join(widgetDir, 'npm-shrinkwrap.json');
16
+ const lockFile = path_1.default.join(widgetDir, 'package-lock.json');
17
+ const packageJson = path_1.default.join(widgetDir, 'package.json');
18
+ if (await fs_extra_1.default.pathExists(shrinkwrap)) {
19
+ await runNpm(widgetDir, ['ci'], 'npm-shrinkwrap.json found; running npm ci in widget directory...');
20
+ return;
21
+ }
22
+ if (await fs_extra_1.default.pathExists(lockFile)) {
23
+ await runNpm(widgetDir, ['ci'], 'Running npm ci in widget directory...');
24
+ return;
25
+ }
26
+ if (await fs_extra_1.default.pathExists(packageJson)) {
27
+ await runNpm(widgetDir, ['install'], 'No lockfile found; running npm install in widget directory...');
28
+ return;
29
+ }
30
+ console.log('No package.json found in widget; skipping dependency install.');
31
+ }
32
+ async function runNpm(cwd, args, startMessage) {
33
+ console.log(startMessage);
34
+ await new Promise((resolve, reject) => {
35
+ const proc = (0, child_process_1.spawn)('npm', args, {
36
+ cwd,
37
+ stdio: 'inherit',
38
+ shell: process.platform === 'win32'
39
+ });
40
+ proc.on('close', (code) => {
41
+ if (code === 0) {
42
+ resolve();
43
+ }
44
+ else {
45
+ reject(new Error(`npm ${args.join(' ')} exited with code ${code}`));
46
+ }
47
+ });
48
+ proc.on('error', reject);
49
+ });
50
+ }
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "exb-community-cli",
3
+ "version": "1.0.0",
4
+ "description": "A lightweight command line interface to allow Experience Builder developers to quickly import widgets published as NPM pacakges.",
5
+ "keywords": [
6
+ "exb-community",
7
+ "cli",
8
+ "experience-builder",
9
+ "widget"
10
+ ],
11
+ "bin": {
12
+ "exb-cli": "dist/index.js"
13
+ },
14
+ "preferGlobal": true,
15
+ "license": "MIT",
16
+ "author": "Lucius Creamer",
17
+ "scripts": {
18
+ "build": "tsc",
19
+ "prepublishOnly": "npm run build"
20
+ },
21
+ "files": [
22
+ "dist/**/*"
23
+ ],
24
+ "dependencies": {
25
+ "commander": "^14.0.3",
26
+ "fs-extra": "^11.3.3",
27
+ "pacote": "^21.3.1",
28
+ "semver": "^7.6.3"
29
+ },
30
+ "devDependencies": {
31
+ "@types/fs-extra": "^11.0.4",
32
+ "@types/node": "^25.2.3",
33
+ "@types/pacote": "^11.1.8",
34
+ "@types/semver": "^7.5.8",
35
+ "typescript": "^5.9.3"
36
+ }
37
+ }