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 +6 -0
- package/README.md +279 -0
- package/package.json +48 -0
- package/scripts/release.sh +88 -0
- package/src/commands/config.ts +113 -0
- package/src/commands/regions.ts +99 -0
- package/src/commands/update.ts +321 -0
- package/src/index.ts +50 -0
- package/src/lib/api-client.ts +85 -0
- package/src/utils/auto-update.ts +59 -0
- package/src/utils/config.ts +23 -0
- package/src/utils/formatter.ts +61 -0
- package/tsconfig.json +23 -0
package/.env.example
ADDED
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
|
+
}
|