gis.ph-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/.env.example ADDED
@@ -0,0 +1,6 @@
1
+ # API Configuration
2
+ API_URL=https://api.gis.ph
3
+ API_KEY=your-api-key-here
4
+
5
+ # Optional: Set log level
6
+ # LOG_LEVEL=debug
package/README.md ADDED
@@ -0,0 +1,279 @@
1
+ # My API CLI
2
+
3
+ A command-line interface for interacting with your API.
4
+
5
+ ## Features
6
+
7
+ - šŸš€ Simple and intuitive commands
8
+ - šŸ” Secure credential management
9
+ - šŸ“Š Multiple output formats (table, JSON)
10
+ - ⚔ Fast and efficient API interactions
11
+ - šŸŽØ Colorful and user-friendly output
12
+ - šŸ’¾ Persistent configuration storage
13
+
14
+ ## Installation
15
+
16
+ ### Quick Install (Recommended)
17
+
18
+ Install with a single command:
19
+
20
+ ```bash
21
+ curl -fsSL https://gis.ph/install.sh | bash
22
+ ```
23
+
24
+ Then configure:
25
+ ```bash
26
+ gis.ph config set apiUrl https://your-api.com
27
+ gis.ph config set apiKey your-api-key-here
28
+ ```
29
+
30
+ ### Manual Installation
31
+
32
+ #### Prerequisites
33
+
34
+ - Node.js >= 14.0.0
35
+ - npm or yarn
36
+
37
+ #### Setup
38
+
39
+ 1. **Clone or navigate to the project directory:**
40
+ ```bash
41
+ cd my-api-cli
42
+ ```
43
+
44
+ 2. **Install dependencies:**
45
+ ```bash
46
+ npm install
47
+ ```
48
+
49
+ 3. **Link the CLI globally (optional):**
50
+ ```bash
51
+ npm link
52
+ ```
53
+ After linking, you can use `gis.ph` command from anywhere.
54
+
55
+ 4. **Configure your API:**
56
+ ```bash
57
+ # Set your API URL
58
+ gis.ph config set apiUrl https://api.gis.ph
59
+
60
+ # Set your API key (if required)
61
+ gis.ph config set apiKey your-api-key-here
62
+ ```
63
+
64
+ Alternatively, create a `.env` file:
65
+ ```bash
66
+ cp .env.example .env
67
+ # Edit .env with your values
68
+ ```
69
+
70
+ ### Uninstall
71
+
72
+ ```bash
73
+ # Using the uninstall script
74
+ curl -fsSL https://yourusername.github.io/my-api-cli/uninstall.sh | bash
75
+
76
+ # Or manually
77
+ rm -rf ~/.gis.ph
78
+ rm ~/.local/bin/gis.ph
79
+ ```
80
+
81
+ ## Usage
82
+
83
+ ### Configuration Commands
84
+
85
+ ```bash
86
+ # Set configuration
87
+ gis.ph config set <key> <value>
88
+
89
+ # Get configuration value
90
+ gis.ph config get <key>
91
+
92
+ # List all configuration
93
+ gis.ph config list
94
+
95
+ # Delete configuration
96
+ gis.ph config delete <key>
97
+
98
+ # Enable/disable automatic update checks
99
+ gis.ph config auto-update enable
100
+ gis.ph config auto-update disable
101
+ ```
102
+
103
+ ### Update Commands
104
+
105
+ ```bash
106
+ # Update to latest version
107
+ gis.ph update
108
+
109
+ # Check for updates without installing
110
+ gis.ph update --check
111
+
112
+ # Force update even if on latest version
113
+ gis.ph update --force
114
+ ```
115
+
116
+ The CLI automatically checks for updates once per day and notifies you if a new version is available. You can disable this with `gis.ph config auto-update disable`.
117
+
118
+ ### Regions Commands
119
+
120
+ ```bash
121
+ # List all regions (table format)
122
+ gis.ph regions list
123
+
124
+ # List regions in JSON format
125
+ gis.ph regions list --format json
126
+
127
+ # List with limit
128
+ gis.ph regions list --limit 10
129
+
130
+ # Filter regions
131
+ gis.ph regions list --filter status:active
132
+
133
+ # Get specific region by ID
134
+ gis.ph regions get <region-id>
135
+
136
+ # Get region in table format
137
+ gis.ph regions get <region-id> --format table
138
+ ```
139
+
140
+ ### Examples
141
+
142
+ ```bash
143
+ # Configure the CLI
144
+ gis.ph config set apiUrl https://api.gis.ph
145
+ gis.ph config set apiKey sk_live_abc123
146
+
147
+ # List all regions
148
+ gis.ph regions list
149
+
150
+ # Get specific region
151
+ gis.ph regions get us-east-1
152
+
153
+ # List regions as JSON for scripting
154
+ gis.ph regions list --format json | jq '.regions[0]'
155
+
156
+ # View help
157
+ gis.ph --help
158
+ gis.ph regions --help
159
+ gis.ph regions list --help
160
+ ```
161
+
162
+ ## Development
163
+
164
+ ### Running Locally
165
+
166
+ Without installing globally:
167
+ ```bash
168
+ npm start -- regions list
169
+ # or
170
+ node src/index.js regions list
171
+ ```
172
+
173
+ ### Project Structure
174
+
175
+ ```
176
+ my-api-cli/
177
+ ā”œā”€ā”€ src/
178
+ │ ā”œā”€ā”€ index.js # Main CLI entry point
179
+ │ ā”œā”€ā”€ commands/ # Command implementations
180
+ │ │ ā”œā”€ā”€ regions.js # Regions commands
181
+ │ │ └── config.js # Config commands
182
+ │ ā”œā”€ā”€ lib/ # Core libraries
183
+ │ │ └── api-client.js # API HTTP client
184
+ │ └── utils/ # Utility functions
185
+ │ ā”œā”€ā”€ config.js # Configuration management
186
+ │ └── formatter.js # Output formatting
187
+ ā”œā”€ā”€ package.json
188
+ ā”œā”€ā”€ .env.example
189
+ └── README.md
190
+ ```
191
+
192
+ ### Adding New Commands
193
+
194
+ 1. Create a new command file in `src/commands/`:
195
+ ```javascript
196
+ const { Command } = require('commander');
197
+
198
+ const myCommand = new Command('mycommand');
199
+
200
+ myCommand
201
+ .description('My command description')
202
+ .action(async () => {
203
+ // Your logic here
204
+ });
205
+
206
+ module.exports = myCommand;
207
+ ```
208
+
209
+ 2. Register it in `src/index.js`:
210
+ ```javascript
211
+ const myCommand = require('./commands/mycommand');
212
+ program.addCommand(myCommand);
213
+ ```
214
+
215
+ 3. Add corresponding API methods in `src/lib/api-client.js`:
216
+ ```javascript
217
+ async getMyData() {
218
+ return this.get('/v1/mydata');
219
+ }
220
+ ```
221
+
222
+ ## Configuration Storage
223
+
224
+ Configuration is stored in your system's config directory:
225
+ - **Linux:** `~/.config/my-api-cli/config.json`
226
+ - **macOS:** `~/Library/Preferences/my-api-cli/config.json`
227
+ - **Windows:** `%APPDATA%\my-api-cli\config.json`
228
+
229
+ ## Environment Variables
230
+
231
+ The CLI supports the following environment variables:
232
+
233
+ - `API_URL` - Base URL for the API
234
+ - `API_KEY` - API authentication key
235
+
236
+ These can be set in a `.env` file or exported in your shell.
237
+
238
+ ## Error Handling
239
+
240
+ The CLI provides helpful error messages:
241
+ - Network errors (API unreachable)
242
+ - Authentication errors (invalid API key)
243
+ - Validation errors (invalid parameters)
244
+ - API errors (with status codes and messages)
245
+
246
+ ## Troubleshooting
247
+
248
+ ### Command not found
249
+
250
+ If `gis.ph` command is not found after `npm link`:
251
+ ```bash
252
+ # Unlink and relink
253
+ npm unlink -g
254
+ npm link
255
+ ```
256
+
257
+ ### API connection errors
258
+
259
+ Check your configuration:
260
+ ```bash
261
+ gis.ph config list
262
+ ```
263
+
264
+ Verify your API URL is correct and accessible.
265
+
266
+ ### Debug mode
267
+
268
+ For more verbose output, use Node's debug mode:
269
+ ```bash
270
+ NODE_DEBUG=* gis.ph regions list
271
+ ```
272
+
273
+ ## License
274
+
275
+ MIT
276
+
277
+ ## Contributing
278
+
279
+ Contributions are welcome! Please feel free to submit a Pull Request.
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "gis.ph-cli",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "description": "CLI tool for GIS.ph API",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "gis.ph": "./dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "start": "node dist/index.js",
13
+ "dev": "ts-node src/index.ts",
14
+ "link": "npm run build && npm link",
15
+ "unlink": "npm unlink -g",
16
+ "release": "npm run build && bash scripts/release.sh",
17
+ "version": "npm version"
18
+ },
19
+ "keywords": [
20
+ "cli",
21
+ "api",
22
+ "tool"
23
+ ],
24
+ "author": "",
25
+ "license": "MIT",
26
+ "dependencies": {
27
+ "axios": "^1.6.2",
28
+ "chalk": "^4.1.2",
29
+ "cli-table3": "^0.6.3",
30
+ "commander": "^11.1.0",
31
+ "conf": "^10.2.0",
32
+ "dotenv": "^16.3.1",
33
+ "ora": "^5.4.1"
34
+ },
35
+ "devDependencies": {
36
+ "@types/chalk": "^0.4.31",
37
+ "@types/commander": "^2.12.0",
38
+ "@types/node": "^25.2.3",
39
+ "@types/ora": "^3.1.0",
40
+ "eslint": "^8.55.0",
41
+ "ts-node": "^10.9.2",
42
+ "tsc-alias": "^1.8.16",
43
+ "typescript": "^5.9.3"
44
+ },
45
+ "engines": {
46
+ "node": ">=14.0.0"
47
+ }
48
+ }
@@ -0,0 +1,88 @@
1
+ #!/bin/bash
2
+
3
+ # Release Script for My API CLI
4
+ # This script helps you create a release package
5
+
6
+ set -e
7
+
8
+ # Colors
9
+ GREEN='\033[0;32m'
10
+ BLUE='\033[0;34m'
11
+ YELLOW='\033[1;33m'
12
+ NC='\033[0m'
13
+
14
+ # Get version from package.json
15
+ VERSION=$(node -p "require('./package.json').version")
16
+
17
+ echo -e "${BLUE}Creating release package for v${VERSION}...${NC}"
18
+ echo ""
19
+
20
+ # Clean up old builds
21
+ if [ -d "dist" ]; then
22
+ echo -e "${YELLOW}Cleaning old builds...${NC}"
23
+ rm -rf dist
24
+ fi
25
+
26
+ mkdir -p dist
27
+
28
+ # Create tarball
29
+ echo -e "${BLUE}Creating tarball...${NC}"
30
+ tar -czf "dist/my-api-cli-v${VERSION}.tar.gz" \
31
+ --exclude='node_modules' \
32
+ --exclude='dist' \
33
+ --exclude='.git' \
34
+ --exclude='.env' \
35
+ --exclude='*.log' \
36
+ src/ \
37
+ package.json \
38
+ README.md \
39
+ .env.example
40
+
41
+ echo -e "${GREEN}āœ“ Created: dist/my-api-cli-v${VERSION}.tar.gz${NC}"
42
+
43
+ # Create checksum
44
+ echo -e "${BLUE}Generating SHA256 checksum...${NC}"
45
+ if command -v sha256sum &> /dev/null; then
46
+ sha256sum "dist/my-api-cli-v${VERSION}.tar.gz" > "dist/my-api-cli-v${VERSION}.tar.gz.sha256"
47
+ CHECKSUM=$(cat "dist/my-api-cli-v${VERSION}.tar.gz.sha256" | cut -d' ' -f1)
48
+ elif command -v shasum &> /dev/null; then
49
+ shasum -a 256 "dist/my-api-cli-v${VERSION}.tar.gz" > "dist/my-api-cli-v${VERSION}.tar.gz.sha256"
50
+ CHECKSUM=$(cat "dist/my-api-cli-v${VERSION}.tar.gz.sha256" | cut -d' ' -f1)
51
+ else
52
+ echo -e "${YELLOW}⚠ sha256sum not found, skipping checksum${NC}"
53
+ CHECKSUM="N/A"
54
+ fi
55
+
56
+ if [ "$CHECKSUM" != "N/A" ]; then
57
+ echo -e "${GREEN}āœ“ SHA256: $CHECKSUM${NC}"
58
+ fi
59
+
60
+ # Create npm package
61
+ echo -e "${BLUE}Creating npm package...${NC}"
62
+ npm pack --pack-destination=dist
63
+
64
+ echo -e "${GREEN}āœ“ Created: dist/my-api-cli-${VERSION}.tgz${NC}"
65
+
66
+ # List created files
67
+ echo ""
68
+ echo -e "${GREEN}Release files created:${NC}"
69
+ ls -lh dist/
70
+
71
+ echo ""
72
+ echo -e "${BLUE}Next steps:${NC}"
73
+ echo "1. Test the package:"
74
+ echo -e " ${YELLOW}cd /tmp && tar -xzf $(pwd)/dist/my-api-cli-v${VERSION}.tar.gz${NC}"
75
+ echo ""
76
+ echo "2. Create a git tag:"
77
+ echo -e " ${YELLOW}git tag -a v${VERSION} -m 'Release v${VERSION}'${NC}"
78
+ echo -e " ${YELLOW}git push origin v${VERSION}${NC}"
79
+ echo ""
80
+ echo "3. Create GitHub release and upload:"
81
+ echo -e " ${YELLOW}dist/my-api-cli-v${VERSION}.tar.gz${NC}"
82
+ echo -e " ${YELLOW}dist/my-api-cli-v${VERSION}.tar.gz.sha256${NC}"
83
+ echo ""
84
+ echo "4. Update install.sh with new version and checksum"
85
+ if [ "$CHECKSUM" != "N/A" ]; then
86
+ echo -e " ${YELLOW}EXPECTED_SHA='$CHECKSUM'${NC}"
87
+ fi
88
+ echo ""
@@ -0,0 +1,113 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import { getConfig } from '../utils/config.js';
4
+ import { enableAutoUpdateCheck, disableAutoUpdateCheck, isAutoUpdateDisabled } from '../utils/auto-update.js';
5
+
6
+ const configCommand = new Command('config');
7
+
8
+ configCommand
9
+ .description('Manage CLI configuration')
10
+ .addCommand(createSetCommand())
11
+ .addCommand(createGetCommand())
12
+ .addCommand(createListCommand())
13
+ .addCommand(createDeleteCommand())
14
+ .addCommand(createAutoUpdateCommand());
15
+
16
+ function createSetCommand() {
17
+ return new Command('set')
18
+ .description('Set a configuration value')
19
+ .argument('<key>', 'Configuration key (apiUrl, apiKey)')
20
+ .argument('<value>', 'Configuration value')
21
+ .action((key: string, value: string) => {
22
+ const config = getConfig();
23
+ config.set(key, value);
24
+ console.log(chalk.green(`āœ“ Configuration updated: ${key} = ${maskSensitive(key, value)}`));
25
+ });
26
+ }
27
+
28
+ function createGetCommand() {
29
+ return new Command('get')
30
+ .description('Get a configuration value')
31
+ .argument('<key>', 'Configuration key')
32
+ .action((key: string) => {
33
+ const config = getConfig();
34
+ const value = config.get(key);
35
+
36
+ if (value === undefined) {
37
+ console.log(chalk.yellow(`Configuration key "${key}" not found`));
38
+ } else {
39
+ console.log(chalk.cyan(`${key}:`), maskSensitive(key, value as string));
40
+ }
41
+ });
42
+ }
43
+
44
+ function createListCommand() {
45
+ return new Command('list')
46
+ .description('List all configuration values')
47
+ .action(() => {
48
+ const config = getConfig();
49
+ const store = config.store;
50
+
51
+ if (Object.keys(store).length === 0) {
52
+ console.log(chalk.yellow('No configuration set'));
53
+ return;
54
+ }
55
+
56
+ console.log(chalk.bold('\nCurrent Configuration:'));
57
+ Object.entries(store).forEach(([key, value]) => {
58
+ // Skip internal keys
59
+ if (key.startsWith('last') || key === 'autoUpdateCheckDisabled') {
60
+ return;
61
+ }
62
+ console.log(chalk.cyan(` ${key}:`), maskSensitive(key, value as string));
63
+ });
64
+
65
+ // Show auto-update status
66
+ const autoUpdateDisabled = isAutoUpdateDisabled();
67
+ console.log(chalk.cyan(` Auto-update checks:`), autoUpdateDisabled ? chalk.red('disabled') : chalk.green('enabled'));
68
+
69
+ console.log();
70
+ });
71
+ }
72
+
73
+ function createDeleteCommand() {
74
+ return new Command('delete')
75
+ .description('Delete a configuration value')
76
+ .argument('<key>', 'Configuration key')
77
+ .action((key: string) => {
78
+ const config = getConfig();
79
+ config.delete(key);
80
+ console.log(chalk.green(`āœ“ Configuration deleted: ${key}`));
81
+ });
82
+ }
83
+
84
+ function createAutoUpdateCommand() {
85
+ return new Command('auto-update')
86
+ .description('Enable or disable automatic update checks')
87
+ .argument('<action>', 'Action: enable or disable')
88
+ .action((action: string) => {
89
+ if (action === 'enable') {
90
+ enableAutoUpdateCheck();
91
+ console.log(chalk.green('āœ“ Automatic update checks enabled'));
92
+ console.log(chalk.gray(' The CLI will check for updates once per day\n'));
93
+ } else if (action === 'disable') {
94
+ disableAutoUpdateCheck();
95
+ console.log(chalk.yellow('āœ“ Automatic update checks disabled'));
96
+ console.log(chalk.gray(' You can still manually check with: gis.ph update --check\n'));
97
+ } else {
98
+ console.log(chalk.red(`Invalid action: ${action}`));
99
+ console.log(chalk.yellow('Usage: gis.ph config auto-update <enable|disable>\n'));
100
+ }
101
+ });
102
+ }
103
+
104
+ function maskSensitive(key: string, value: string): string {
105
+ if (!value) return '(not set)';
106
+ const sensitiveKeys = ['apiKey', 'apikey', 'api_key', 'token', 'password', 'secret'];
107
+ if (sensitiveKeys.some(k => key.toLowerCase().includes(k))) {
108
+ return value ? '***' + value.slice(-4) : '(not set)';
109
+ }
110
+ return value;
111
+ }
112
+
113
+ export default configCommand;
@@ -0,0 +1,99 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import apiClient from '../lib/api-client.js';
5
+ import { formatTable, formatJson } from '../utils/formatter.js';
6
+
7
+ const regionsCommand = new Command('regions');
8
+
9
+ regionsCommand
10
+ .description('Manage and view regions')
11
+ .addCommand(createListCommand())
12
+ .addCommand(createGetCommand());
13
+
14
+ function createListCommand() {
15
+ return new Command('list')
16
+ .description('List all regions')
17
+ .option('-f, --format <type>', 'Output format (table|json)', 'table')
18
+ .option('-l, --limit <number>', 'Limit number of results')
19
+ .option('--filter <field:value>', 'Filter results (e.g., status:active)')
20
+ .action(async (options) => {
21
+ const spinner = ora('Fetching regions...').start();
22
+
23
+ try {
24
+ // Build query parameters
25
+ const params: any = {};
26
+ if (options.limit) {
27
+ params.limit = parseInt(options.limit);
28
+ }
29
+ if (options.filter) {
30
+ const [field, value] = options.filter.split(':');
31
+ params[field] = value;
32
+ }
33
+
34
+ const data = await apiClient.getRegions(params);
35
+ spinner.succeed('Regions fetched successfully');
36
+
37
+ // Display results
38
+ if (options.format === 'json') {
39
+ console.log(formatJson(data));
40
+ } else {
41
+ // The API now returns { data: [...], error: null }
42
+ const regions = Array.isArray(data) ? data : ((data as any).data || (data as any).regions || []);
43
+
44
+ if (regions.length === 0) {
45
+ console.log(chalk.yellow('\nNo regions found.\n'));
46
+ return;
47
+ }
48
+
49
+ const tableData = regions.map((region: any) => ({
50
+ ID: region.id || 'N/A',
51
+ Name: region.name || 'N/A',
52
+ Title: region.title || 'N/A',
53
+ Code: region.code || 'N/A',
54
+ }));
55
+
56
+ console.log(formatTable(tableData));
57
+ console.log(chalk.gray(`\nTotal: ${regions.length} region(s)\n`));
58
+ }
59
+ } catch (error: any) {
60
+ spinner.fail('Failed to fetch regions');
61
+ console.error(chalk.red(`\nError: ${error.message}\n`));
62
+ process.exit(1);
63
+ }
64
+ });
65
+ }
66
+
67
+ function createGetCommand() {
68
+ return new Command('get')
69
+ .description('Get details of a specific region')
70
+ .argument('<id>', 'Region ID')
71
+ .option('-f, --format <type>', 'Output format (table|json)', 'json')
72
+ .action(async (id: string, options) => {
73
+ const spinner = ora(`Fetching region ${id}...`).start();
74
+
75
+ try {
76
+ const data = await apiClient.getRegionById(id);
77
+ spinner.succeed(`Region ${id} fetched successfully`);
78
+
79
+ if (options.format === 'json') {
80
+ console.log(formatJson(data));
81
+ } else {
82
+ // The API now returns { data: {...}, error: null }
83
+ const region = (data as any).data || (data as any).region || data;
84
+ const tableData = Object.entries(region).map(([key, value]) => ({
85
+ Field: key,
86
+ Value: typeof value === 'object' ? JSON.stringify(value) : value,
87
+ }));
88
+
89
+ console.log(formatTable(tableData));
90
+ }
91
+ } catch (error: any) {
92
+ spinner.fail(`Failed to fetch region ${id}`);
93
+ console.error(chalk.red(`\nError: ${error.message}\n`));
94
+ process.exit(1);
95
+ }
96
+ });
97
+ }
98
+
99
+ export default regionsCommand;
@@ -0,0 +1,321 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import https from 'https';
5
+ import { execSync } from 'child_process';
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+
9
+ // Load package.json manually to avoid issues with JSON modules in some environments
10
+ const pkgPath = new URL('../../package.json', import.meta.url);
11
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
12
+
13
+ const updateCommand = new Command('update');
14
+
15
+ updateCommand
16
+ .description('Update the CLI to the latest version')
17
+ .option('--check', 'Check for updates without installing')
18
+ .option('--force', 'Force update even if already on latest version')
19
+ .action(async (options: any) => {
20
+ if (options.check) {
21
+ await checkForUpdates(true);
22
+ } else {
23
+ await performUpdate(options.force);
24
+ }
25
+ });
26
+
27
+ /**
28
+ * Check for available updates
29
+ */
30
+ export async function checkForUpdates(verbose = false): Promise<any> {
31
+ const spinner = verbose ? ora('Checking for updates...').start() : null;
32
+
33
+ try {
34
+ const latestVersion = await getLatestVersion();
35
+ const currentVersion = pkg.version;
36
+
37
+ if (spinner) spinner.succeed('Update check complete');
38
+
39
+ const isNewer = compareVersions(latestVersion, currentVersion) > 0;
40
+
41
+ if (isNewer) {
42
+ if (verbose) {
43
+ console.log(chalk.yellow(`\nšŸŽ‰ Update available!`));
44
+ console.log(chalk.gray(` Current: ${currentVersion}`));
45
+ console.log(chalk.green(` Latest: ${latestVersion}`));
46
+ console.log(chalk.cyan(`\n Run ${chalk.bold('gis.ph update')} to upgrade\n`));
47
+ }
48
+ return { updateAvailable: true, currentVersion, latestVersion };
49
+ } else {
50
+ if (verbose) {
51
+ console.log(chalk.green(`\nāœ“ You're on the latest version (${currentVersion})\n`));
52
+ }
53
+ return { updateAvailable: false, currentVersion, latestVersion };
54
+ }
55
+ } catch (error: any) {
56
+ if (spinner) spinner.fail('Failed to check for updates');
57
+ if (verbose) {
58
+ console.error(chalk.red(`\nError: ${error.message}\n`));
59
+ }
60
+ return { updateAvailable: false, error: error.message };
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Get latest version from GitHub
66
+ */
67
+ export function getLatestVersion(): Promise<string> {
68
+ return new Promise((resolve, reject) => {
69
+ // Update this URL to match your GitHub repo
70
+ const GITHUB_REPO = process.env.GITHUB_REPO || 'yourusername/my-api-cli';
71
+ const url = `https://api.github.com/repos/${GITHUB_REPO}/releases/latest`;
72
+
73
+ https.get(url, {
74
+ headers: {
75
+ 'User-Agent': 'gis.ph-CLI'
76
+ }
77
+ }, (res: any) => {
78
+ let data = '';
79
+
80
+ res.on('data', (chunk: any) => {
81
+ data += chunk;
82
+ });
83
+
84
+ res.on('end', () => {
85
+ try {
86
+ if (res.statusCode === 404) {
87
+ // No releases, try to get from package.json on main branch
88
+ getVersionFromMain(GITHUB_REPO)
89
+ .then(resolve)
90
+ .catch(reject);
91
+ return;
92
+ }
93
+
94
+ if (res.statusCode !== 200) {
95
+ reject(new Error(`GitHub API returned status ${res.statusCode}`));
96
+ return;
97
+ }
98
+
99
+ const release = JSON.parse(data);
100
+ const version = release.tag_name.replace(/^v/, '');
101
+ resolve(version);
102
+ } catch (error) {
103
+ reject(new Error('Failed to parse GitHub response'));
104
+ }
105
+ });
106
+ }).on('error', (error: any) => {
107
+ reject(new Error(`Failed to check version: ${error.message}`));
108
+ });
109
+ });
110
+ }
111
+
112
+ /**
113
+ * Get version from package.json on main branch (fallback)
114
+ */
115
+ function getVersionFromMain(repo: string): Promise<string> {
116
+ return new Promise((resolve, reject) => {
117
+ const url = `https://raw.githubusercontent.com/${repo}/main/package.json`;
118
+
119
+ https.get(url, {
120
+ headers: {
121
+ 'User-Agent': 'gis.ph-CLI'
122
+ }
123
+ }, (res: any) => {
124
+ let data = '';
125
+
126
+ res.on('data', (chunk: any) => {
127
+ data += chunk;
128
+ });
129
+
130
+ res.on('end', () => {
131
+ try {
132
+ if (res.statusCode !== 200) {
133
+ reject(new Error('Could not fetch version from GitHub'));
134
+ return;
135
+ }
136
+
137
+ const packageJson = JSON.parse(data);
138
+ resolve(packageJson.version);
139
+ } catch (error) {
140
+ reject(new Error('Failed to parse package.json from GitHub'));
141
+ }
142
+ });
143
+ }).on('error', (error: any) => {
144
+ reject(error);
145
+ });
146
+ });
147
+ }
148
+
149
+ /**
150
+ * Compare semantic versions
151
+ * Returns: 1 if v1 > v2, -1 if v1 < v2, 0 if equal
152
+ */
153
+ export function compareVersions(v1: string, v2: string): number {
154
+ const parts1 = v1.split('.').map(Number);
155
+ const parts2 = v2.split('.').map(Number);
156
+
157
+ for (let i = 0; i < 3; i++) {
158
+ const part1 = parts1[i] || 0;
159
+ const part2 = parts2[i] || 0;
160
+
161
+ if (part1 > part2) return 1;
162
+ if (part1 < part2) return -1;
163
+ }
164
+
165
+ return 0;
166
+ }
167
+
168
+ /**
169
+ * Perform the update
170
+ */
171
+ async function performUpdate(force = false) {
172
+ console.log(chalk.blue('\nšŸ”„ Updating CLI...\n'));
173
+
174
+ // Check for updates first
175
+ if (!force) {
176
+ const updateInfo = await checkForUpdates(false);
177
+
178
+ if (!updateInfo.updateAvailable) {
179
+ console.log(chalk.green(`āœ“ Already on the latest version (${updateInfo.currentVersion})\n`));
180
+ console.log(chalk.gray(`Use --force to reinstall anyway\n`));
181
+ return;
182
+ }
183
+ }
184
+
185
+ const spinner = ora('Downloading latest version...').start();
186
+
187
+ try {
188
+ // Get installation directory
189
+ const installDir = getInstallDir();
190
+
191
+ if (!installDir) {
192
+ spinner.fail('Update failed');
193
+ console.error(chalk.red('\nError: Could not determine installation directory'));
194
+ console.log(chalk.yellow('\nPlease reinstall using:'));
195
+ console.log(chalk.cyan('curl -fsSL https://gis.ph/install.sh | bash\n'));
196
+ process.exit(1);
197
+ }
198
+
199
+ spinner.text = 'Updating...';
200
+
201
+ // Run the update script
202
+ const GITHUB_REPO = process.env.GITHUB_REPO || 'yourusername/my-api-cli';
203
+ const DOWNLOAD_URL = `https://github.com/${GITHUB_REPO}/archive/refs/heads/main.tar.gz`;
204
+
205
+ // Use the same update logic as install script
206
+ const updateScript = `
207
+ set -e
208
+ TEMP_DIR=$(mktemp -d)
209
+ TEMP_FILE="$TEMP_DIR/cli.tar.gz"
210
+ INSTALL_DIR="${installDir}"
211
+
212
+ # Download
213
+ if command -v curl &> /dev/null; then
214
+ curl -fsSL "${DOWNLOAD_URL}" -o "$TEMP_FILE"
215
+ else
216
+ wget -q "${DOWNLOAD_URL}" -O "$TEMP_FILE"
217
+ fi
218
+
219
+ # Backup current installation
220
+ if [ -d "$INSTALL_DIR.backup" ]; then
221
+ rm -rf "$INSTALL_DIR.backup"
222
+ fi
223
+ cp -r "$INSTALL_DIR" "$INSTALL_DIR.backup"
224
+
225
+ # Extract to temp location
226
+ EXTRACT_DIR="$TEMP_DIR/extract"
227
+ mkdir -p "$EXTRACT_DIR"
228
+ tar -xzf "$TEMP_FILE" -C "$EXTRACT_DIR"
229
+
230
+ # Find extracted directory
231
+ EXTRACTED_DIR=$(find "$EXTRACT_DIR" -mindepth 1 -maxdepth 1 -type d | head -n 1)
232
+
233
+ # Move files
234
+ rm -rf "$INSTALL_DIR"
235
+ mv "$EXTRACTED_DIR" "$INSTALL_DIR"
236
+
237
+ # Install dependencies
238
+ cd "$INSTALL_DIR"
239
+ npm install --production --silent
240
+
241
+ # Clean up
242
+ rm -rf "$TEMP_DIR"
243
+ rm -rf "$INSTALL_DIR.backup"
244
+ `;
245
+
246
+ execSync(updateScript, {
247
+ stdio: 'pipe',
248
+ shell: '/bin/bash'
249
+ });
250
+
251
+ spinner.succeed('Update complete!');
252
+
253
+ // Get new version
254
+ const newPackageJson = JSON.parse(
255
+ fs.readFileSync(path.join(installDir, 'package.json'), 'utf8')
256
+ );
257
+
258
+ console.log(chalk.green(`\nāœ“ Successfully updated to version ${newPackageJson.version}\n`));
259
+ console.log(chalk.gray('Changes will take effect immediately.\n'));
260
+
261
+ } catch (error: any) {
262
+ spinner.fail('Update failed');
263
+ console.error(chalk.red(`\nError: ${error.message}`));
264
+ console.log(chalk.yellow('\nIf the problem persists, try reinstalling:'));
265
+ console.log(chalk.cyan('curl -fsSL https://gis.ph/install.sh | bash\n'));
266
+ process.exit(1);
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Get CLI installation directory
272
+ */
273
+ function getInstallDir(): string | null {
274
+ // Method 1: Try from config file (set during installation)
275
+ const homeDir = process.env.HOME || process.env.USERPROFILE || '';
276
+ const configPath = path.join(homeDir, '.config', 'gis.ph', 'install_dir');
277
+ if (fs.existsSync(configPath)) {
278
+ const installDir = fs.readFileSync(configPath, 'utf8').trim();
279
+ if (installDir && fs.existsSync(installDir)) {
280
+ return installDir;
281
+ }
282
+ }
283
+
284
+ // Method 2: Try environment variable
285
+ const envInstallDir = process.env['GIS_PH_INSTALL_DIR'] || process.env['GIS.PH_INSTALL_DIR'];
286
+ if (envInstallDir && fs.existsSync(envInstallDir)) {
287
+ return envInstallDir;
288
+ }
289
+
290
+ // Method 3: Try common installation paths
291
+ const possiblePaths = [
292
+ path.join(homeDir, '.gis.ph'),
293
+ path.join(homeDir, '.my-api-cli'),
294
+ ];
295
+
296
+ for (const dir of possiblePaths) {
297
+ if (dir && fs.existsSync(dir) && fs.existsSync(path.join(dir, 'package.json'))) {
298
+ return dir;
299
+ }
300
+ }
301
+
302
+ // Method 4: Try to get from the current executable path
303
+ const binPath = process.argv[1];
304
+ if (binPath && fs.existsSync(binPath)) {
305
+ try {
306
+ if (fs.lstatSync(binPath).isSymbolicLink()) {
307
+ const realPath = fs.realpathSync(binPath);
308
+ const installDir = path.dirname(path.dirname(realPath));
309
+ if (fs.existsSync(path.join(installDir, 'package.json'))) {
310
+ return installDir;
311
+ }
312
+ }
313
+ } catch (e) {
314
+ // Ignore errors
315
+ }
316
+ }
317
+
318
+ return null;
319
+ }
320
+
321
+ export default updateCommand;
package/src/index.ts ADDED
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import chalk from 'chalk';
5
+ import fs from 'fs';
6
+ import path from 'path';
7
+ import { autoCheckForUpdates, isAutoUpdateDisabled } from './utils/auto-update.js';
8
+
9
+ // Import commands
10
+ import regionsCommand from './commands/regions.js';
11
+ import configCommand from './commands/config.js';
12
+ import updateCommand from './commands/update.js';
13
+
14
+ // Load package.json manually
15
+ const pkgPath = new URL('../package.json', import.meta.url);
16
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
17
+
18
+ const program = new Command();
19
+
20
+ program
21
+ .name('gis.ph')
22
+ .description('CLI tool for GIS.ph API')
23
+ .version(pkg.version);
24
+
25
+ // Add commands
26
+ program.addCommand(regionsCommand);
27
+ program.addCommand(configCommand);
28
+ program.addCommand(updateCommand);
29
+
30
+ // Handle unknown commands
31
+ program.on('command:*', () => {
32
+ console.error(chalk.red(`\nInvalid command: ${program.args.join(' ')}`));
33
+ console.log(chalk.yellow(`See --help for a list of available commands.\n`));
34
+ process.exit(1);
35
+ });
36
+
37
+ // Check for updates in background (non-blocking)
38
+ if (!isAutoUpdateDisabled()) {
39
+ autoCheckForUpdates().catch(() => {
40
+ // Silently fail - don't interrupt user
41
+ });
42
+ }
43
+
44
+ // Parse arguments
45
+ program.parse(process.argv);
46
+
47
+ // Show help if no arguments provided
48
+ if (!process.argv.slice(2).length) {
49
+ program.outputHelp();
50
+ }
@@ -0,0 +1,85 @@
1
+ import axios, { AxiosInstance } from 'axios';
2
+ import { getConfig } from '../utils/config.js';
3
+
4
+ class ApiClient {
5
+ private client: AxiosInstance;
6
+ private baseURL: string;
7
+ private apiKey?: string;
8
+
9
+ constructor() {
10
+ const config = getConfig();
11
+
12
+ this.baseURL = (config.get('apiUrl') as string) || process.env.API_URL || 'http://localhost:3000';
13
+ this.apiKey = (config.get('apiKey') as string) || process.env.API_KEY;
14
+
15
+ this.client = axios.create({
16
+ baseURL: this.baseURL,
17
+ timeout: 30000,
18
+ headers: {
19
+ 'Content-Type': 'application/json',
20
+ },
21
+ });
22
+
23
+ // Add request interceptor for authentication
24
+ this.client.interceptors.request.use(
25
+ (conf) => {
26
+ if (this.apiKey) {
27
+ conf.headers['Authorization'] = `Bearer ${this.apiKey}`;
28
+ }
29
+ return conf;
30
+ },
31
+ (error) => {
32
+ return Promise.reject(error);
33
+ }
34
+ );
35
+
36
+ // Add response interceptor for error handling
37
+ this.client.interceptors.response.use(
38
+ (response) => response,
39
+ (error) => {
40
+ if (error.response) {
41
+ // Server responded with error status
42
+ const message = error.response.data?.message || error.response.statusText;
43
+ throw new Error(`API Error (${error.response.status}): ${message}`);
44
+ } else if (error.request) {
45
+ // Request made but no response
46
+ throw new Error(`Network Error: Unable to reach API at ${this.baseURL}`);
47
+ } else {
48
+ // Something else happened
49
+ throw new Error(`Request Error: ${error.message}`);
50
+ }
51
+ }
52
+ );
53
+ }
54
+
55
+ async get<T = any>(endpoint: string, params: any = {}): Promise<T> {
56
+ const response = await this.client.get(endpoint, { params });
57
+ return response.data;
58
+ }
59
+
60
+ async post<T = any>(endpoint: string, data: any = {}): Promise<T> {
61
+ const response = await this.client.post(endpoint, data);
62
+ return response.data;
63
+ }
64
+
65
+ async put<T = any>(endpoint: string, data: any = {}): Promise<T> {
66
+ const response = await this.client.put(endpoint, data);
67
+ return response.data;
68
+ }
69
+
70
+ async delete<T = any>(endpoint: string): Promise<T> {
71
+ const response = await this.client.delete(endpoint);
72
+ return response.data;
73
+ }
74
+
75
+ // Specific API methods
76
+ async getRegions(params: any = {}) {
77
+ return this.get('/v1/regions', params);
78
+ }
79
+
80
+ async getRegionById(id: string | number) {
81
+ return this.get(`/v1/regions/${id}`);
82
+ }
83
+ }
84
+
85
+ export default new ApiClient();
@@ -0,0 +1,59 @@
1
+ import chalk from 'chalk';
2
+ import { getConfig } from './config.js';
3
+ // We'll import type if needed, but for now we'll use any or dynamic import if there's a circular dependency
4
+ // Actually, it's better to move common types to a separate file.
5
+
6
+ export async function autoCheckForUpdates() {
7
+ try {
8
+ const config = getConfig();
9
+ const lastCheck = config.get('lastUpdateCheck') as number;
10
+ const now = Date.now();
11
+
12
+ // Check once per day (24 hours)
13
+ const ONE_DAY = 24 * 60 * 60 * 1000;
14
+
15
+ if (lastCheck && (now - lastCheck) < ONE_DAY) {
16
+ return; // Too soon, skip check
17
+ }
18
+
19
+ // Update last check time
20
+ config.set('lastUpdateCheck', now);
21
+
22
+ // Lazy import to avoid circular dependency if update.ts imports this
23
+ const { checkForUpdates } = await import('../commands/update.js');
24
+
25
+ // Check for updates (non-verbose, don't show spinner)
26
+ const updateInfo = await checkForUpdates(false);
27
+
28
+ if (updateInfo.updateAvailable) {
29
+ // Show notification at the end of command output
30
+ setTimeout(() => {
31
+ console.log(chalk.yellow('\nā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”'));
32
+ console.log(chalk.yellow('│ šŸŽ‰ Update Available! │'));
33
+ console.log(chalk.yellow('│ │'));
34
+ console.log(chalk.gray(`│ Current: ${updateInfo.currentVersion.padEnd(28)} │`));
35
+ console.log(chalk.green(`│ Latest: ${updateInfo.latestVersion.padEnd(28)} │`));
36
+ console.log(chalk.yellow('│ │'));
37
+ console.log(chalk.cyan(`│ Run ${chalk.bold('gis.ph update')} to upgrade │`));
38
+ console.log(chalk.yellow('ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜\n'));
39
+ }, 100);
40
+ }
41
+ } catch (error) {
42
+ // Silently fail - don't interrupt user's workflow
43
+ }
44
+ }
45
+
46
+ export function disableAutoUpdateCheck() {
47
+ const config = getConfig();
48
+ config.set('autoUpdateCheckDisabled', true);
49
+ }
50
+
51
+ export function enableAutoUpdateCheck() {
52
+ const config = getConfig();
53
+ config.set('autoUpdateCheckDisabled', false);
54
+ }
55
+
56
+ export function isAutoUpdateDisabled() {
57
+ const config = getConfig();
58
+ return config.get('autoUpdateCheckDisabled') === true;
59
+ }
@@ -0,0 +1,23 @@
1
+ import Conf from 'conf';
2
+
3
+ interface ConfigSchema {
4
+ apiUrl: string;
5
+ apiKey?: string;
6
+ lastUpdateCheck?: number;
7
+ [key: string]: any;
8
+ }
9
+
10
+ let config: any;
11
+
12
+ export function getConfig() {
13
+ if (!config) {
14
+ // @ts-ignore
15
+ config = new Conf<ConfigSchema>({
16
+ projectName: 'gis.ph',
17
+ defaults: {
18
+ apiUrl: 'https://api.gis.ph',
19
+ },
20
+ });
21
+ }
22
+ return config;
23
+ }
@@ -0,0 +1,61 @@
1
+ import Table from 'cli-table3';
2
+ import chalk from 'chalk';
3
+
4
+ /**
5
+ * Format data as a table
6
+ * @param {Array<any>} data - Array of objects to display
7
+ * @returns {string} Formatted table
8
+ */
9
+ export function formatTable(data: any[]): string {
10
+ if (!Array.isArray(data) || data.length === 0) {
11
+ return chalk.yellow('No data to display');
12
+ }
13
+
14
+ const headers = Object.keys(data[0]);
15
+
16
+ const table = new Table({
17
+ head: headers.map(h => chalk.cyan(h)),
18
+ style: {
19
+ head: [],
20
+ border: ['gray'],
21
+ },
22
+ // @ts-ignore - cli-table3 types might be strict about colWidths
23
+ colWidths: headers.map(() => null), // Auto-width
24
+ });
25
+
26
+ data.forEach(row => {
27
+ table.push(headers.map(h => row[h] ?? ''));
28
+ });
29
+
30
+ return '\n' + table.toString() + '\n';
31
+ }
32
+
33
+ /**
34
+ * Format data as pretty JSON
35
+ * @param {*} data - Data to format
36
+ * @returns {string} Formatted JSON
37
+ */
38
+ export function formatJson(data: any): string {
39
+ return '\n' + JSON.stringify(data, null, 2) + '\n';
40
+ }
41
+
42
+ /**
43
+ * Format data as simple key-value pairs
44
+ * @param {Object} data - Object to display
45
+ * @returns {string} Formatted output
46
+ */
47
+ export function formatKeyValue(data: any): string {
48
+ if (typeof data !== 'object' || data === null) {
49
+ return String(data);
50
+ }
51
+
52
+ let output = '\n';
53
+ Object.entries(data).forEach(([key, value]) => {
54
+ const formattedValue = typeof value === 'object'
55
+ ? JSON.stringify(value, null, 2)
56
+ : value;
57
+ output += `${chalk.cyan(key + ':')} ${formattedValue}\n`;
58
+ });
59
+
60
+ return output;
61
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "declaration": true,
13
+ "sourceMap": true,
14
+ "resolveJsonModule": true
15
+ },
16
+ "include": [
17
+ "src/**/*"
18
+ ],
19
+ "exclude": [
20
+ "node_modules",
21
+ "dist"
22
+ ]
23
+ }