@ktmcp-cli/adafruit 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/AGENT.md ADDED
@@ -0,0 +1,46 @@
1
+ # Adafruit IO CLI - AI Agent Guide
2
+
3
+ This CLI provides programmatic access to the Adafruit IO IoT Platform API.
4
+
5
+ ## Quick Start for AI Agents
6
+
7
+ ```bash
8
+ adafruit config set --api-key YOUR_AIO_KEY
9
+ adafruit config set --username YOUR_USERNAME
10
+ adafruit feeds list
11
+ ```
12
+
13
+ ## Available Commands
14
+
15
+ ### config
16
+ - `adafruit config set --api-key <key>` - Set Adafruit IO API key
17
+ - `adafruit config set --username <user>` - Set Adafruit IO username
18
+ - `adafruit config get <key>` - Get a config value
19
+ - `adafruit config list` - List all config values
20
+
21
+ ### feeds
22
+ - `adafruit feeds list` - List all feeds
23
+ - `adafruit feeds get <feed-key>` - Get feed details
24
+ - `adafruit feeds create --name <name>` - Create a new feed
25
+
26
+ ### data
27
+ - `adafruit data send <feed-key> <value>` - Send data to a feed
28
+ - `adafruit data send <feed-key> <value> --lat <lat> --lon <lon>` - Send with GPS
29
+ - `adafruit data get <feed-key> <data-id>` - Get a specific data point
30
+ - `adafruit data list <feed-key>` - List recent data points
31
+ - `adafruit data list <feed-key> --limit <n>` - List with limit
32
+ - `adafruit data list <feed-key> --start <iso> --end <iso>` - List by date range
33
+
34
+ ### dashboards
35
+ - `adafruit dashboards list` - List all dashboards
36
+ - `adafruit dashboards get <dashboard-key>` - Get dashboard details
37
+ - `adafruit dashboards create --name <name>` - Create a new dashboard
38
+
39
+ ## Output Format
40
+
41
+ All commands output formatted tables by default. Use `--json` flag for machine-readable JSON output.
42
+
43
+ ## Authentication
44
+
45
+ This CLI uses your Adafruit IO AIO Key for authentication. Get your key at https://io.adafruit.com/api/docs/
46
+ Rate limit: 30 data points/minute on free plan.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 KTMCP
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,96 @@
1
+ > "Six months ago, everyone was talking about MCPs. And I was like, screw MCPs. Every MCP would be better as a CLI."
2
+ >
3
+ > — [Peter Steinberger](https://twitter.com/steipete), Founder of OpenClaw
4
+ > [Watch on YouTube (~2:39:00)](https://www.youtube.com/@lexfridman) | [Lex Fridman Podcast #491](https://lexfridman.com/peter-steinberger/)
5
+
6
+ # Adafruit IO CLI
7
+
8
+ Production-ready CLI for Adafruit IO IoT Platform API.
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ npm install -g @ktmcp-cli/adafruit
14
+ ```
15
+
16
+ ## Configuration
17
+
18
+ ```bash
19
+ adafruit config set --api-key YOUR_AIO_KEY
20
+ adafruit config set --username YOUR_USERNAME
21
+ ```
22
+
23
+ Get your AIO key from: https://io.adafruit.com/api/docs/
24
+
25
+ ## Usage
26
+
27
+ ### Feeds
28
+
29
+ ```bash
30
+ # List all feeds
31
+ adafruit feeds list
32
+
33
+ # Get feed details
34
+ adafruit feeds get temperature-sensor
35
+
36
+ # Create a new feed
37
+ adafruit feeds create --name "Temperature" --description "Indoor temp" --visibility private
38
+ adafruit feeds create --name "GPS Location" --visibility public
39
+ ```
40
+
41
+ ### Data
42
+
43
+ ```bash
44
+ # Send a data point to a feed
45
+ adafruit data send temperature-sensor 72.5
46
+
47
+ # Send with GPS location
48
+ adafruit data send gps-tracker 1 --lat 40.7128 --lon -74.0060
49
+
50
+ # Get a specific data point
51
+ adafruit data get temperature-sensor <data-id>
52
+
53
+ # List recent data points
54
+ adafruit data list temperature-sensor --limit 50
55
+ adafruit data list temperature-sensor --start 2024-01-01T00:00:00Z --end 2024-01-31T23:59:59Z
56
+ ```
57
+
58
+ ### Dashboards
59
+
60
+ ```bash
61
+ # List all dashboards
62
+ adafruit dashboards list
63
+
64
+ # Get dashboard details
65
+ adafruit dashboards get my-home-dashboard
66
+
67
+ # Create a dashboard
68
+ adafruit dashboards create --name "Home Monitoring" --description "Temperature and humidity"
69
+ ```
70
+
71
+ ### Configuration
72
+
73
+ ```bash
74
+ adafruit config set --api-key YOUR_KEY
75
+ adafruit config set --username YOUR_USERNAME
76
+ adafruit config get username
77
+ adafruit config list
78
+ ```
79
+
80
+ ## JSON Output
81
+
82
+ All commands support `--json` flag for machine-readable output:
83
+
84
+ ```bash
85
+ adafruit feeds list --json
86
+ adafruit data list temperature-sensor --json
87
+ ```
88
+
89
+ ## Rate Limits
90
+
91
+ - Free plan: 30 data points per minute
92
+ - Plus plan: 60 data points per minute
93
+
94
+ ## License
95
+
96
+ MIT
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../src/index.js';
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@ktmcp-cli/adafruit",
3
+ "version": "1.0.0",
4
+ "description": "Production-ready CLI for Adafruit IO IoT Platform API - Kill The MCP",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "bin": {
8
+ "adafruit": "bin/adafruit.js"
9
+ },
10
+ "keywords": ["adafruit", "iot", "cli", "api", "ktmcp", "feeds", "dashboards", "hardware"],
11
+ "author": "KTMCP",
12
+ "license": "MIT",
13
+ "dependencies": {
14
+ "commander": "^12.0.0",
15
+ "axios": "^1.6.7",
16
+ "chalk": "^5.3.0",
17
+ "ora": "^8.0.1",
18
+ "conf": "^12.0.0"
19
+ },
20
+ "engines": { "node": ">=18.0.0" },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/ktmcp-cli/adafruit.git"
24
+ },
25
+ "homepage": "https://killthemcp.com/adafruit-cli",
26
+ "bugs": { "url": "https://github.com/ktmcp-cli/adafruit/issues" }
27
+ }
package/src/api.js ADDED
@@ -0,0 +1,169 @@
1
+ import axios from 'axios';
2
+ import { getConfig } from './config.js';
3
+
4
+ const BASE_URL = 'https://io.adafruit.com/api/v2';
5
+
6
+ function getClient() {
7
+ const apiKey = getConfig('apiKey');
8
+ const username = getConfig('username');
9
+
10
+ if (!apiKey || !username) {
11
+ throw new Error('Adafruit IO credentials not configured. Run: adafruit config set --api-key YOUR_KEY --username YOUR_USERNAME');
12
+ }
13
+
14
+ return axios.create({
15
+ baseURL: BASE_URL,
16
+ headers: {
17
+ 'X-AIO-Key': apiKey,
18
+ 'Content-Type': 'application/json',
19
+ 'Accept': 'application/json'
20
+ }
21
+ });
22
+ }
23
+
24
+ function getUsername() {
25
+ return getConfig('username');
26
+ }
27
+
28
+ function handleApiError(error) {
29
+ if (error.response) {
30
+ const status = error.response.status;
31
+ const data = error.response.data;
32
+ if (status === 401) throw new Error('Authentication failed. Check your Adafruit IO API key.');
33
+ if (status === 403) throw new Error('Access forbidden. Check your API key permissions.');
34
+ if (status === 404) throw new Error('Resource not found on Adafruit IO.');
35
+ if (status === 429) throw new Error('Rate limit exceeded. Free plan allows 30 data points/minute.');
36
+ const message = data?.error || data?.message || JSON.stringify(data);
37
+ throw new Error(`API Error (${status}): ${message}`);
38
+ } else if (error.request) {
39
+ throw new Error('No response from Adafruit IO API. Check your internet connection.');
40
+ } else {
41
+ throw error;
42
+ }
43
+ }
44
+
45
+ // ============================================================
46
+ // FEEDS
47
+ // ============================================================
48
+
49
+ export async function listFeeds() {
50
+ try {
51
+ const client = getClient();
52
+ const username = getUsername();
53
+ const response = await client.get(`/${username}/feeds`);
54
+ return response.data;
55
+ } catch (error) {
56
+ handleApiError(error);
57
+ }
58
+ }
59
+
60
+ export async function getFeed(feedKey) {
61
+ try {
62
+ const client = getClient();
63
+ const username = getUsername();
64
+ const response = await client.get(`/${username}/feeds/${feedKey}`);
65
+ return response.data;
66
+ } catch (error) {
67
+ handleApiError(error);
68
+ }
69
+ }
70
+
71
+ export async function createFeed({ name, description, visibility = 'private' }) {
72
+ try {
73
+ const client = getClient();
74
+ const username = getUsername();
75
+ const response = await client.post(`/${username}/feeds`, {
76
+ feed: { name, description, visibility }
77
+ });
78
+ return response.data;
79
+ } catch (error) {
80
+ handleApiError(error);
81
+ }
82
+ }
83
+
84
+ // ============================================================
85
+ // DATA
86
+ // ============================================================
87
+
88
+ export async function sendData(feedKey, value, { lat, lon, ele, createdAt } = {}) {
89
+ try {
90
+ const client = getClient();
91
+ const username = getUsername();
92
+ const body = { value: String(value) };
93
+ if (lat !== undefined) body.lat = lat;
94
+ if (lon !== undefined) body.lon = lon;
95
+ if (ele !== undefined) body.ele = ele;
96
+ if (createdAt) body.created_at = createdAt;
97
+
98
+ const response = await client.post(`/${username}/feeds/${feedKey}/data`, body);
99
+ return response.data;
100
+ } catch (error) {
101
+ handleApiError(error);
102
+ }
103
+ }
104
+
105
+ export async function getData(feedKey, dataId) {
106
+ try {
107
+ const client = getClient();
108
+ const username = getUsername();
109
+ const response = await client.get(`/${username}/feeds/${feedKey}/data/${dataId}`);
110
+ return response.data;
111
+ } catch (error) {
112
+ handleApiError(error);
113
+ }
114
+ }
115
+
116
+ export async function listData(feedKey, { limit = 100, startTime, endTime } = {}) {
117
+ try {
118
+ const client = getClient();
119
+ const username = getUsername();
120
+ const params = { limit };
121
+ if (startTime) params.start_time = startTime;
122
+ if (endTime) params.end_time = endTime;
123
+
124
+ const response = await client.get(`/${username}/feeds/${feedKey}/data`, { params });
125
+ return response.data;
126
+ } catch (error) {
127
+ handleApiError(error);
128
+ }
129
+ }
130
+
131
+ // ============================================================
132
+ // DASHBOARDS
133
+ // ============================================================
134
+
135
+ export async function listDashboards() {
136
+ try {
137
+ const client = getClient();
138
+ const username = getUsername();
139
+ const response = await client.get(`/${username}/dashboards`);
140
+ return response.data;
141
+ } catch (error) {
142
+ handleApiError(error);
143
+ }
144
+ }
145
+
146
+ export async function getDashboard(dashboardKey) {
147
+ try {
148
+ const client = getClient();
149
+ const username = getUsername();
150
+ const response = await client.get(`/${username}/dashboards/${dashboardKey}`);
151
+ return response.data;
152
+ } catch (error) {
153
+ handleApiError(error);
154
+ }
155
+ }
156
+
157
+ export async function createDashboard({ name, description }) {
158
+ try {
159
+ const client = getClient();
160
+ const username = getUsername();
161
+ const response = await client.post(`/${username}/dashboards`, {
162
+ name,
163
+ description
164
+ });
165
+ return response.data;
166
+ } catch (error) {
167
+ handleApiError(error);
168
+ }
169
+ }
package/src/config.js ADDED
@@ -0,0 +1,19 @@
1
+ import Conf from 'conf';
2
+
3
+ const config = new Conf({ projectName: '@ktmcp-cli/adafruit' });
4
+
5
+ export function getConfig(key) {
6
+ return config.get(key);
7
+ }
8
+
9
+ export function setConfig(key, value) {
10
+ config.set(key, value);
11
+ }
12
+
13
+ export function isConfigured() {
14
+ return !!config.get('apiKey') && !!config.get('username');
15
+ }
16
+
17
+ export function getAllConfig() {
18
+ return config.store;
19
+ }
package/src/index.js ADDED
@@ -0,0 +1,373 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import { getConfig, setConfig, isConfigured, getAllConfig } from './config.js';
5
+ import {
6
+ listFeeds, getFeed, createFeed,
7
+ sendData, getData, listData,
8
+ listDashboards, getDashboard, createDashboard
9
+ } from './api.js';
10
+
11
+ const program = new Command();
12
+
13
+ // ============================================================
14
+ // Helpers
15
+ // ============================================================
16
+
17
+ function printSuccess(message) {
18
+ console.log(chalk.green('✓') + ' ' + message);
19
+ }
20
+
21
+ function printError(message) {
22
+ console.error(chalk.red('✗') + ' ' + message);
23
+ }
24
+
25
+ function printTable(data, columns) {
26
+ if (!data || data.length === 0) {
27
+ console.log(chalk.yellow('No results found.'));
28
+ return;
29
+ }
30
+ const widths = {};
31
+ columns.forEach(col => {
32
+ widths[col.key] = col.label.length;
33
+ data.forEach(row => {
34
+ const val = String(col.format ? col.format(row[col.key], row) : (row[col.key] ?? ''));
35
+ if (val.length > widths[col.key]) widths[col.key] = val.length;
36
+ });
37
+ widths[col.key] = Math.min(widths[col.key], 40);
38
+ });
39
+ const header = columns.map(col => col.label.padEnd(widths[col.key])).join(' ');
40
+ console.log(chalk.bold(chalk.cyan(header)));
41
+ console.log(chalk.dim('─'.repeat(header.length)));
42
+ data.forEach(row => {
43
+ const line = columns.map(col => {
44
+ const val = String(col.format ? col.format(row[col.key], row) : (row[col.key] ?? ''));
45
+ return val.substring(0, widths[col.key]).padEnd(widths[col.key]);
46
+ }).join(' ');
47
+ console.log(line);
48
+ });
49
+ console.log(chalk.dim(`\n${data.length} result(s)`));
50
+ }
51
+
52
+ function printJson(data) {
53
+ console.log(JSON.stringify(data, null, 2));
54
+ }
55
+
56
+ async function withSpinner(message, fn) {
57
+ const spinner = ora(message).start();
58
+ try {
59
+ const result = await fn();
60
+ spinner.stop();
61
+ return result;
62
+ } catch (error) {
63
+ spinner.stop();
64
+ throw error;
65
+ }
66
+ }
67
+
68
+ function requireAuth() {
69
+ if (!isConfigured()) {
70
+ printError('Adafruit IO credentials not configured.');
71
+ console.log('\nRun the following to configure:');
72
+ console.log(chalk.cyan(' adafruit config set --api-key YOUR_AIO_KEY --username YOUR_USERNAME'));
73
+ process.exit(1);
74
+ }
75
+ }
76
+
77
+ // ============================================================
78
+ // Program metadata
79
+ // ============================================================
80
+
81
+ program
82
+ .name('adafruit')
83
+ .description(chalk.bold('Adafruit IO CLI') + ' - IoT feeds and dashboards from your terminal')
84
+ .version('1.0.0');
85
+
86
+ // ============================================================
87
+ // CONFIG
88
+ // ============================================================
89
+
90
+ const configCmd = program.command('config').description('Manage CLI configuration');
91
+
92
+ configCmd
93
+ .command('set')
94
+ .description('Set configuration values')
95
+ .option('--api-key <key>', 'Adafruit IO API key (AIO Key)')
96
+ .option('--username <user>', 'Adafruit IO username')
97
+ .action((options) => {
98
+ if (options.apiKey) { setConfig('apiKey', options.apiKey); printSuccess('API key set'); }
99
+ if (options.username) { setConfig('username', options.username); printSuccess(`Username set: ${options.username}`); }
100
+ if (!options.apiKey && !options.username) {
101
+ printError('No options provided. Use --api-key or --username');
102
+ }
103
+ });
104
+
105
+ configCmd
106
+ .command('get <key>')
107
+ .description('Get a configuration value')
108
+ .action((key) => {
109
+ const value = getConfig(key);
110
+ if (value === undefined) {
111
+ printError(`Key "${key}" not found`);
112
+ } else {
113
+ console.log(value);
114
+ }
115
+ });
116
+
117
+ configCmd
118
+ .command('list')
119
+ .description('List all configuration values')
120
+ .action(() => {
121
+ const all = getAllConfig();
122
+ console.log(chalk.bold('\nAdafruit IO CLI Configuration\n'));
123
+ console.log('API Key: ', all.apiKey ? chalk.green('aio_' + '*'.repeat(20)) : chalk.red('not set'));
124
+ console.log('Username: ', all.username ? chalk.green(all.username) : chalk.red('not set'));
125
+ console.log('');
126
+ });
127
+
128
+ // ============================================================
129
+ // FEEDS
130
+ // ============================================================
131
+
132
+ const feedsCmd = program.command('feeds').description('Manage Adafruit IO feeds');
133
+
134
+ feedsCmd
135
+ .command('list')
136
+ .description('List all feeds')
137
+ .option('--json', 'Output as JSON')
138
+ .action(async (options) => {
139
+ requireAuth();
140
+ try {
141
+ const feeds = await withSpinner('Fetching feeds...', () => listFeeds());
142
+ if (options.json) { printJson(feeds); return; }
143
+ printTable(feeds, [
144
+ { key: 'id', label: 'ID' },
145
+ { key: 'name', label: 'Name' },
146
+ { key: 'key', label: 'Key' },
147
+ { key: 'visibility', label: 'Visibility' },
148
+ { key: 'last_value', label: 'Last Value' },
149
+ { key: 'status', label: 'Status' }
150
+ ]);
151
+ } catch (error) {
152
+ printError(error.message);
153
+ process.exit(1);
154
+ }
155
+ });
156
+
157
+ feedsCmd
158
+ .command('get <feed-key>')
159
+ .description('Get details of a specific feed')
160
+ .option('--json', 'Output as JSON')
161
+ .action(async (feedKey, options) => {
162
+ requireAuth();
163
+ try {
164
+ const feed = await withSpinner('Fetching feed...', () => getFeed(feedKey));
165
+ if (options.json) { printJson(feed); return; }
166
+ console.log(chalk.bold('\nFeed Details\n'));
167
+ console.log('ID: ', chalk.cyan(feed.id));
168
+ console.log('Name: ', chalk.bold(feed.name));
169
+ console.log('Key: ', feed.key);
170
+ console.log('Visibility: ', feed.visibility);
171
+ console.log('Last Value: ', feed.last_value !== null ? chalk.green(feed.last_value) : chalk.dim('no data'));
172
+ console.log('Unit Symbol: ', feed.unit_symbol || 'N/A');
173
+ console.log('Unit Type: ', feed.unit_type || 'N/A');
174
+ console.log('Status: ', feed.status || 'N/A');
175
+ console.log('Created: ', feed.created_at || 'N/A');
176
+ console.log('Updated: ', feed.updated_at || 'N/A');
177
+ console.log('');
178
+ } catch (error) {
179
+ printError(error.message);
180
+ process.exit(1);
181
+ }
182
+ });
183
+
184
+ feedsCmd
185
+ .command('create')
186
+ .description('Create a new feed')
187
+ .requiredOption('--name <name>', 'Feed name')
188
+ .option('--description <desc>', 'Feed description')
189
+ .option('--visibility <vis>', 'Feed visibility (public|private)', 'private')
190
+ .option('--json', 'Output as JSON')
191
+ .action(async (options) => {
192
+ requireAuth();
193
+ try {
194
+ const feed = await withSpinner('Creating feed...', () =>
195
+ createFeed({ name: options.name, description: options.description, visibility: options.visibility })
196
+ );
197
+ if (options.json) { printJson(feed); return; }
198
+ printSuccess(`Feed created: ${chalk.bold(feed.name)}`);
199
+ console.log('Key: ', feed.key);
200
+ console.log('Visibility: ', feed.visibility);
201
+ } catch (error) {
202
+ printError(error.message);
203
+ process.exit(1);
204
+ }
205
+ });
206
+
207
+ // ============================================================
208
+ // DATA
209
+ // ============================================================
210
+
211
+ const dataCmd = program.command('data').description('Send and retrieve feed data');
212
+
213
+ dataCmd
214
+ .command('send <feed-key> <value>')
215
+ .description('Send a data point to a feed')
216
+ .option('--lat <lat>', 'Latitude for location data')
217
+ .option('--lon <lon>', 'Longitude for location data')
218
+ .option('--ele <ele>', 'Elevation for location data')
219
+ .option('--json', 'Output as JSON')
220
+ .action(async (feedKey, value, options) => {
221
+ requireAuth();
222
+ try {
223
+ const dataPoint = await withSpinner(`Sending value "${value}" to feed "${feedKey}"...`, () =>
224
+ sendData(feedKey, value, {
225
+ lat: options.lat,
226
+ lon: options.lon,
227
+ ele: options.ele
228
+ })
229
+ );
230
+ if (options.json) { printJson(dataPoint); return; }
231
+ printSuccess(`Data sent to ${chalk.bold(feedKey)}`);
232
+ console.log('ID: ', dataPoint.id);
233
+ console.log('Value: ', chalk.green(dataPoint.value));
234
+ console.log('Created At: ', dataPoint.created_at);
235
+ } catch (error) {
236
+ printError(error.message);
237
+ process.exit(1);
238
+ }
239
+ });
240
+
241
+ dataCmd
242
+ .command('get <feed-key> <data-id>')
243
+ .description('Get a specific data point from a feed')
244
+ .option('--json', 'Output as JSON')
245
+ .action(async (feedKey, dataId, options) => {
246
+ requireAuth();
247
+ try {
248
+ const dataPoint = await withSpinner('Fetching data point...', () => getData(feedKey, dataId));
249
+ if (options.json) { printJson(dataPoint); return; }
250
+ console.log(chalk.bold('\nData Point\n'));
251
+ console.log('ID: ', chalk.cyan(dataPoint.id));
252
+ console.log('Value: ', chalk.green(dataPoint.value));
253
+ console.log('Created At: ', dataPoint.created_at);
254
+ if (dataPoint.lat) console.log('Location: ', `${dataPoint.lat}, ${dataPoint.lon}`);
255
+ console.log('');
256
+ } catch (error) {
257
+ printError(error.message);
258
+ process.exit(1);
259
+ }
260
+ });
261
+
262
+ dataCmd
263
+ .command('list <feed-key>')
264
+ .description('List data points from a feed')
265
+ .option('--limit <n>', 'Maximum number of points', '100')
266
+ .option('--start <time>', 'Start time (ISO 8601)')
267
+ .option('--end <time>', 'End time (ISO 8601)')
268
+ .option('--json', 'Output as JSON')
269
+ .action(async (feedKey, options) => {
270
+ requireAuth();
271
+ try {
272
+ const data = await withSpinner('Fetching data...', () =>
273
+ listData(feedKey, {
274
+ limit: parseInt(options.limit),
275
+ startTime: options.start,
276
+ endTime: options.end
277
+ })
278
+ );
279
+ if (options.json) { printJson(data); return; }
280
+ printTable(data, [
281
+ { key: 'id', label: 'ID' },
282
+ { key: 'value', label: 'Value' },
283
+ { key: 'created_at', label: 'Created At' },
284
+ { key: 'lat', label: 'Lat', format: (v) => v || 'N/A' },
285
+ { key: 'lon', label: 'Lon', format: (v) => v || 'N/A' }
286
+ ]);
287
+ } catch (error) {
288
+ printError(error.message);
289
+ process.exit(1);
290
+ }
291
+ });
292
+
293
+ // ============================================================
294
+ // DASHBOARDS
295
+ // ============================================================
296
+
297
+ const dashboardsCmd = program.command('dashboards').description('Manage Adafruit IO dashboards');
298
+
299
+ dashboardsCmd
300
+ .command('list')
301
+ .description('List all dashboards')
302
+ .option('--json', 'Output as JSON')
303
+ .action(async (options) => {
304
+ requireAuth();
305
+ try {
306
+ const dashboards = await withSpinner('Fetching dashboards...', () => listDashboards());
307
+ if (options.json) { printJson(dashboards); return; }
308
+ printTable(dashboards, [
309
+ { key: 'id', label: 'ID' },
310
+ { key: 'name', label: 'Name' },
311
+ { key: 'key', label: 'Key' },
312
+ { key: 'description', label: 'Description' }
313
+ ]);
314
+ } catch (error) {
315
+ printError(error.message);
316
+ process.exit(1);
317
+ }
318
+ });
319
+
320
+ dashboardsCmd
321
+ .command('get <dashboard-key>')
322
+ .description('Get details of a specific dashboard')
323
+ .option('--json', 'Output as JSON')
324
+ .action(async (dashboardKey, options) => {
325
+ requireAuth();
326
+ try {
327
+ const dashboard = await withSpinner('Fetching dashboard...', () => getDashboard(dashboardKey));
328
+ if (options.json) { printJson(dashboard); return; }
329
+ console.log(chalk.bold('\nDashboard Details\n'));
330
+ console.log('ID: ', chalk.cyan(dashboard.id));
331
+ console.log('Name: ', chalk.bold(dashboard.name));
332
+ console.log('Key: ', dashboard.key);
333
+ console.log('Description: ', dashboard.description || 'N/A');
334
+ console.log('Created: ', dashboard.created_at || 'N/A');
335
+ console.log('Updated: ', dashboard.updated_at || 'N/A');
336
+ console.log('');
337
+ } catch (error) {
338
+ printError(error.message);
339
+ process.exit(1);
340
+ }
341
+ });
342
+
343
+ dashboardsCmd
344
+ .command('create')
345
+ .description('Create a new dashboard')
346
+ .requiredOption('--name <name>', 'Dashboard name')
347
+ .option('--description <desc>', 'Dashboard description')
348
+ .option('--json', 'Output as JSON')
349
+ .action(async (options) => {
350
+ requireAuth();
351
+ try {
352
+ const dashboard = await withSpinner('Creating dashboard...', () =>
353
+ createDashboard({ name: options.name, description: options.description })
354
+ );
355
+ if (options.json) { printJson(dashboard); return; }
356
+ printSuccess(`Dashboard created: ${chalk.bold(dashboard.name)}`);
357
+ console.log('ID: ', dashboard.id);
358
+ console.log('Key: ', dashboard.key);
359
+ } catch (error) {
360
+ printError(error.message);
361
+ process.exit(1);
362
+ }
363
+ });
364
+
365
+ // ============================================================
366
+ // Parse
367
+ // ============================================================
368
+
369
+ program.parse(process.argv);
370
+
371
+ if (process.argv.length <= 2) {
372
+ program.help();
373
+ }