@manfred-kunze-dev/iot-cli 3.0.0-dev.1
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/LICENSE +19 -0
- package/README.md +140 -0
- package/dist/commands/auth.d.ts +3 -0
- package/dist/commands/auth.js +204 -0
- package/dist/commands/config.d.ts +3 -0
- package/dist/commands/config.js +94 -0
- package/dist/commands/context.d.ts +3 -0
- package/dist/commands/context.js +137 -0
- package/dist/commands/docs.d.ts +3 -0
- package/dist/commands/docs.js +183 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +38 -0
- package/dist/lib/client.d.ts +15 -0
- package/dist/lib/client.js +55 -0
- package/dist/lib/config.d.ts +63 -0
- package/dist/lib/config.js +188 -0
- package/dist/lib/errors.d.ts +73 -0
- package/dist/lib/errors.js +160 -0
- package/dist/lib/output.d.ts +45 -0
- package/dist/lib/output.js +104 -0
- package/dist/lib/update-notifier.d.ts +18 -0
- package/dist/lib/update-notifier.js +118 -0
- package/openapi/openapi.json +65 -0
- package/package.json +60 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2026-present Manfred Kunze Dev
|
|
2
|
+
|
|
3
|
+
All rights reserved.
|
|
4
|
+
|
|
5
|
+
This software and associated documentation files (the "Software") are proprietary
|
|
6
|
+
and confidential. No part of the Software may be reproduced, distributed, or
|
|
7
|
+
transmitted in any form or by any means, including photocopying, recording, or
|
|
8
|
+
other electronic or mechanical methods, without the prior written permission of
|
|
9
|
+
the copyright holder.
|
|
10
|
+
|
|
11
|
+
The Software is provided "as is", without warranty of any kind, express or implied,
|
|
12
|
+
including but not limited to the warranties of merchantability, fitness for a
|
|
13
|
+
particular purpose, and noninfringement. In no event shall the authors or copyright
|
|
14
|
+
holders be liable for any claim, damages, or other liability, whether in an action
|
|
15
|
+
of contract, tort, or otherwise, arising from, out of, or in connection with the
|
|
16
|
+
Software or the use or other dealings in the Software.
|
|
17
|
+
|
|
18
|
+
Usage of this Software is permitted only in accordance with the terms and conditions
|
|
19
|
+
set forth by Manfred Kunze Dev.
|
package/README.md
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# iot CLI
|
|
2
|
+
|
|
3
|
+
Command-line interface for the [iot platform](https://iot.manfred-kunze.dev) by Manfred Kunze Development.
|
|
4
|
+
|
|
5
|
+
Manage devices, sensors, sites, readings, events and more — straight from your terminal.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Latest stable release
|
|
11
|
+
npm install -g @manfred-kunze-dev/iot-cli
|
|
12
|
+
|
|
13
|
+
# Pre-release (dev channel)
|
|
14
|
+
npm install -g @manfred-kunze-dev/iot-cli@dev
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The CLI is installed as `iot`, `mkd-iot`, and `mki` (short alias) — pick whichever you prefer:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
iot --version
|
|
21
|
+
mki --version
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The CLI will notify you when a newer version is available.
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Authenticate
|
|
30
|
+
iot auth login
|
|
31
|
+
|
|
32
|
+
# Verify
|
|
33
|
+
iot auth status
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Authentication
|
|
37
|
+
|
|
38
|
+
The CLI supports multiple authentication methods (highest priority first):
|
|
39
|
+
|
|
40
|
+
| Method | Example |
|
|
41
|
+
|--------|---------|
|
|
42
|
+
| CLI flags | `--api-key sk_... --base-url https://...` |
|
|
43
|
+
| Environment variables | `IOT_API_KEY`, `IOT_BASE_URL` |
|
|
44
|
+
| Local `.iot` file | JSON file in the current directory or any ancestor |
|
|
45
|
+
| Config store | `~/.config/iot-cli/config.json` (set via `iot auth login`) |
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
iot auth login # Interactive setup
|
|
49
|
+
iot auth status # Verify credentials
|
|
50
|
+
iot auth logout # Clear stored credentials
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Contexts
|
|
54
|
+
|
|
55
|
+
The CLI supports kubectl-style contexts for switching between organizations and environments:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# Contexts are created automatically via `iot auth login`
|
|
59
|
+
iot context list # List all contexts
|
|
60
|
+
iot context use prod # Switch active context
|
|
61
|
+
iot context create staging # Create a new context
|
|
62
|
+
iot context rename old new # Rename a context
|
|
63
|
+
iot context delete old # Remove a context
|
|
64
|
+
iot context current # Print just the active name (script-friendly)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
You can also switch contexts for a single invocation:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
iot --context staging auth status
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Commands (foundation — v1)
|
|
74
|
+
|
|
75
|
+
| Command | Description |
|
|
76
|
+
|---------|-------------|
|
|
77
|
+
| `auth` | Login, logout, and check auth status |
|
|
78
|
+
| `config` | Get, set, and list global configuration values |
|
|
79
|
+
| `context` | Manage CLI contexts for multiple environments |
|
|
80
|
+
| `docs` | Browse API documentation from the terminal |
|
|
81
|
+
|
|
82
|
+
Resource commands (`devices`, `sensors`, `sites`, `readings`, `events`, …) ship in later releases.
|
|
83
|
+
|
|
84
|
+
### Global Options
|
|
85
|
+
|
|
86
|
+
| Flag | Description |
|
|
87
|
+
|------|-------------|
|
|
88
|
+
| `--api-key <key>` | Override the API key |
|
|
89
|
+
| `--base-url <url>` | Override the base URL |
|
|
90
|
+
| `--context <name>` | Use a named context for this invocation without switching the active one |
|
|
91
|
+
| `--json` | Output raw JSON instead of formatted tables |
|
|
92
|
+
| `--no-color` | Disable colored output |
|
|
93
|
+
| `--verbose` | Print resolved base URL, masked key, and request method+path on stderr before each call |
|
|
94
|
+
|
|
95
|
+
## Output
|
|
96
|
+
|
|
97
|
+
Stdout carries the command's data payload (table, JSON, created resource ID). Everything else — spinners, errors, the update banner — goes to stderr. This makes pipelines like `iot devices list --json | jq` clean.
|
|
98
|
+
|
|
99
|
+
Exit codes:
|
|
100
|
+
|
|
101
|
+
| Code | Meaning |
|
|
102
|
+
|------|---------|
|
|
103
|
+
| 0 | Success |
|
|
104
|
+
| 1 | Generic error |
|
|
105
|
+
| 2 | Not authenticated (401) |
|
|
106
|
+
| 3 | Forbidden (403) |
|
|
107
|
+
| 4 | Not found (404) |
|
|
108
|
+
| 5 | Conflict (409) |
|
|
109
|
+
| 6 | Validation error (422) |
|
|
110
|
+
| 7 | Server error (5xx) |
|
|
111
|
+
| 8 | Network error |
|
|
112
|
+
|
|
113
|
+
## Development
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
# Install deps
|
|
117
|
+
npm ci
|
|
118
|
+
|
|
119
|
+
# Build
|
|
120
|
+
npm run build
|
|
121
|
+
|
|
122
|
+
# Watch + rebuild
|
|
123
|
+
npm run dev
|
|
124
|
+
|
|
125
|
+
# Type-check only
|
|
126
|
+
npm run typecheck
|
|
127
|
+
|
|
128
|
+
# Run tests
|
|
129
|
+
npm test
|
|
130
|
+
|
|
131
|
+
# Regenerate typed client from a running dev backend
|
|
132
|
+
SPRING_PROFILES_ACTIVE=spec ./mvnw.cmd -pl backend spring-boot:run # in backend/
|
|
133
|
+
npm run generate # in cli/
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
See [`docs/superpowers/specs/2026-05-25-iot-cli-foundation-design.md`](../docs/superpowers/specs/2026-05-25-iot-cli-foundation-design.md) for the full design.
|
|
137
|
+
|
|
138
|
+
## License
|
|
139
|
+
|
|
140
|
+
Proprietary — see [LICENSE](./LICENSE) for details.
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { createInterface } from "node:readline/promises";
|
|
3
|
+
import { DEFAULT_BASE_URL, getActiveContextName, getActiveContext, getContextCount, setContext, setActiveContext, deleteContext, isJsonOutput, resolveConfig, } from "../lib/config.js";
|
|
4
|
+
import { getClient } from "../lib/client.js";
|
|
5
|
+
import { printJson, printKeyValue, printSuccess, maskKey } from "../lib/output.js";
|
|
6
|
+
import { handleError } from "../lib/errors.js";
|
|
7
|
+
export function makeAuthCommand() {
|
|
8
|
+
const cmd = new Command("auth").description("Manage authentication");
|
|
9
|
+
cmd
|
|
10
|
+
.command("login")
|
|
11
|
+
.description("Configure API credentials for the current context")
|
|
12
|
+
.option("--api-key <key>", "API key (skips the prompt)")
|
|
13
|
+
.option("--base-url <url>", "Base URL (skips the prompt)")
|
|
14
|
+
.action(async (opts, command) => {
|
|
15
|
+
try {
|
|
16
|
+
let { apiKey, baseUrl } = opts;
|
|
17
|
+
const contextName = getActiveContextName();
|
|
18
|
+
const currentCtx = getActiveContext();
|
|
19
|
+
if (!apiKey || !baseUrl) {
|
|
20
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
21
|
+
try {
|
|
22
|
+
if (!baseUrl) {
|
|
23
|
+
const defaultUrl = currentCtx?.baseUrl ?? DEFAULT_BASE_URL;
|
|
24
|
+
const answer = (await rl.question(`Base URL [${defaultUrl}]: `)).trim();
|
|
25
|
+
baseUrl = answer || defaultUrl;
|
|
26
|
+
}
|
|
27
|
+
if (!apiKey) {
|
|
28
|
+
apiKey = (await promptHidden(rl, "API Key: ")).trim();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
rl.close();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (!apiKey) {
|
|
36
|
+
throw new Error("API key is required.");
|
|
37
|
+
}
|
|
38
|
+
setContext(contextName, { apiKey, baseUrl: baseUrl });
|
|
39
|
+
setActiveContext(contextName);
|
|
40
|
+
// Validate by probing /v1/devices/summary so the user knows the
|
|
41
|
+
// creds work before they exit the prompt.
|
|
42
|
+
const { client } = getClient(command);
|
|
43
|
+
const { data, error } = await client.GET("/v1/devices/summary");
|
|
44
|
+
if (error) {
|
|
45
|
+
throw new Error("Saved credentials, but the validation probe failed. " +
|
|
46
|
+
"Double-check the API key and base URL with `iot auth status`.");
|
|
47
|
+
}
|
|
48
|
+
if (isJsonOutput(command)) {
|
|
49
|
+
printJson({
|
|
50
|
+
success: true,
|
|
51
|
+
context: contextName,
|
|
52
|
+
baseUrl,
|
|
53
|
+
devices: data?.total ?? null,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
printSuccess(`Credentials saved (context: ${contextName}).`, "stderr");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
handleError(err, { json: isJsonOutput(command) });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
cmd
|
|
65
|
+
.command("status")
|
|
66
|
+
.description("Show authentication status for the active context")
|
|
67
|
+
.action(async (_opts, command) => {
|
|
68
|
+
try {
|
|
69
|
+
const json = isJsonOutput(command);
|
|
70
|
+
let config;
|
|
71
|
+
try {
|
|
72
|
+
config = resolveConfig(command);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
if (json) {
|
|
76
|
+
printJson({ authenticated: false });
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
process.stderr.write("Not authenticated.\n");
|
|
80
|
+
process.stderr.write('Run "iot auth login" to configure.\n');
|
|
81
|
+
}
|
|
82
|
+
process.exitCode = 2;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const { apiKey, baseUrl } = config;
|
|
86
|
+
const multiContext = getContextCount() > 1;
|
|
87
|
+
const contextName = getActiveContextName();
|
|
88
|
+
try {
|
|
89
|
+
const { client } = getClient(command);
|
|
90
|
+
const { data, error } = await client.GET("/v1/devices/summary");
|
|
91
|
+
if (error) {
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
const devices = data?.total ?? 0;
|
|
95
|
+
if (json) {
|
|
96
|
+
printJson({
|
|
97
|
+
authenticated: true,
|
|
98
|
+
...(multiContext ? { context: contextName } : {}),
|
|
99
|
+
baseUrl,
|
|
100
|
+
apiKeyPreview: maskKey(apiKey),
|
|
101
|
+
devices,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
printKeyValue({
|
|
106
|
+
Status: "Authenticated",
|
|
107
|
+
...(multiContext ? { Context: contextName } : {}),
|
|
108
|
+
"Base URL": baseUrl,
|
|
109
|
+
"API Key": maskKey(apiKey),
|
|
110
|
+
Devices: devices,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
if (json) {
|
|
116
|
+
printJson({
|
|
117
|
+
authenticated: false,
|
|
118
|
+
...(multiContext ? { context: contextName } : {}),
|
|
119
|
+
baseUrl,
|
|
120
|
+
apiKeyPreview: maskKey(apiKey),
|
|
121
|
+
error: "validation_failed",
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
process.stderr.write("Credentials configured but validation failed.\n");
|
|
126
|
+
printKeyValue({
|
|
127
|
+
...(multiContext ? { Context: contextName } : {}),
|
|
128
|
+
"Base URL": baseUrl,
|
|
129
|
+
"API Key": maskKey(apiKey),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
throw err;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
handleError(err, { json: isJsonOutput(command) });
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
cmd
|
|
140
|
+
.command("logout")
|
|
141
|
+
.description("Clear the API key from the active context (keeps the base URL)")
|
|
142
|
+
.action(async (_opts, command) => {
|
|
143
|
+
try {
|
|
144
|
+
const contextName = getActiveContextName();
|
|
145
|
+
const count = getContextCount();
|
|
146
|
+
if (count === 0) {
|
|
147
|
+
// Nothing configured — silent success.
|
|
148
|
+
printSuccess("No credentials to clear.", "stderr");
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const ctx = getActiveContext();
|
|
152
|
+
if (count <= 1) {
|
|
153
|
+
// Last context — keep the entry, just blank the key. This matches
|
|
154
|
+
// the spec: "logout clears the active context's apiKey, keeps baseUrl
|
|
155
|
+
// so re-login is one prompt."
|
|
156
|
+
setContext(contextName, { apiKey: "", baseUrl: ctx?.baseUrl ?? DEFAULT_BASE_URL });
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
deleteContext(contextName);
|
|
160
|
+
}
|
|
161
|
+
if (isJsonOutput(command)) {
|
|
162
|
+
printJson({ success: true, context: contextName });
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
printSuccess("Credentials cleared.", "stderr");
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
handleError(err, { json: isJsonOutput(command) });
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
return cmd;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Tiny hidden-input helper for API keys. readline.question doesn't natively
|
|
176
|
+
* support hiding input on Windows/POSIX consistently, so we mute the muxed
|
|
177
|
+
* output stream while the user types.
|
|
178
|
+
*/
|
|
179
|
+
function promptHidden(rl, prompt) {
|
|
180
|
+
return new Promise((resolveAnswer, rejectAnswer) => {
|
|
181
|
+
const mutableStdout = rl.output;
|
|
182
|
+
const original = mutableStdout.write.bind(mutableStdout);
|
|
183
|
+
let muted = false;
|
|
184
|
+
mutableStdout.write = ((chunk, enc, cb) => {
|
|
185
|
+
if (muted && typeof chunk === "string") {
|
|
186
|
+
return original("", enc, cb);
|
|
187
|
+
}
|
|
188
|
+
return original(chunk, enc, cb);
|
|
189
|
+
});
|
|
190
|
+
process.stderr.write(prompt);
|
|
191
|
+
muted = true;
|
|
192
|
+
rl.question("")
|
|
193
|
+
.then((answer) => {
|
|
194
|
+
mutableStdout.write = original;
|
|
195
|
+
process.stderr.write("\n");
|
|
196
|
+
resolveAnswer(answer);
|
|
197
|
+
})
|
|
198
|
+
.catch((err) => {
|
|
199
|
+
mutableStdout.write = original;
|
|
200
|
+
rejectAnswer(err);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
//# sourceMappingURL=auth.js.map
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { isPreferenceKey, getPreference, setPreference, unsetPreference, listPreferences, listPreferenceKeys, isJsonOutput, } from "../lib/config.js";
|
|
3
|
+
import { printTable, printJson, printSuccess } from "../lib/output.js";
|
|
4
|
+
import { handleError, CLIError, EXIT } from "../lib/errors.js";
|
|
5
|
+
export function makeConfigCommand() {
|
|
6
|
+
const cmd = new Command("config").description("Manage global CLI preferences (not per-context)");
|
|
7
|
+
cmd
|
|
8
|
+
.command("get <key>")
|
|
9
|
+
.description("Get a preference value")
|
|
10
|
+
.action((key, _opts, command) => {
|
|
11
|
+
try {
|
|
12
|
+
ensureValidKey(key);
|
|
13
|
+
const value = getPreference(key);
|
|
14
|
+
if (isJsonOutput(command)) {
|
|
15
|
+
printJson({ key, value: value ?? null });
|
|
16
|
+
}
|
|
17
|
+
else if (value === undefined) {
|
|
18
|
+
process.stderr.write(`(unset)\n`);
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
process.stdout.write(String(value) + "\n");
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
handleError(err, { json: isJsonOutput(command) });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
cmd
|
|
29
|
+
.command("set <key> <value>")
|
|
30
|
+
.description("Set a preference value")
|
|
31
|
+
.action((key, value, _opts, command) => {
|
|
32
|
+
try {
|
|
33
|
+
ensureValidKey(key);
|
|
34
|
+
setPreference(key, value);
|
|
35
|
+
if (isJsonOutput(command)) {
|
|
36
|
+
printJson({ success: true, key, value });
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
printSuccess(`Set ${key}=${value}`, "stderr");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
handleError(err, { json: isJsonOutput(command) });
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
cmd
|
|
47
|
+
.command("unset <key>")
|
|
48
|
+
.description("Remove a preference")
|
|
49
|
+
.action((key, _opts, command) => {
|
|
50
|
+
try {
|
|
51
|
+
ensureValidKey(key);
|
|
52
|
+
unsetPreference(key);
|
|
53
|
+
if (isJsonOutput(command)) {
|
|
54
|
+
printJson({ success: true, key });
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
printSuccess(`Unset ${key}`, "stderr");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
handleError(err, { json: isJsonOutput(command) });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
cmd
|
|
65
|
+
.command("list")
|
|
66
|
+
.description("List all preferences and their valid keys")
|
|
67
|
+
.action((_opts, command) => {
|
|
68
|
+
try {
|
|
69
|
+
const prefs = listPreferences();
|
|
70
|
+
if (isJsonOutput(command)) {
|
|
71
|
+
printJson({ preferences: prefs, validKeys: listPreferenceKeys() });
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const rows = listPreferenceKeys().map((key) => ({
|
|
75
|
+
key,
|
|
76
|
+
value: prefs[key] ?? "(unset)",
|
|
77
|
+
}));
|
|
78
|
+
printTable(rows, [
|
|
79
|
+
{ key: "key", header: "KEY" },
|
|
80
|
+
{ key: "value", header: "VALUE" },
|
|
81
|
+
]);
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
handleError(err, { json: isJsonOutput(command) });
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
return cmd;
|
|
88
|
+
}
|
|
89
|
+
function ensureValidKey(key) {
|
|
90
|
+
if (!isPreferenceKey(key)) {
|
|
91
|
+
throw new CLIError(`Unknown preference key: ${key}. Valid keys: ${listPreferenceKeys().join(", ")}`, EXIT.VALIDATION);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { getAllContexts, getActiveContextName, setActiveContext, setContext, deleteContext, renameContext, isJsonOutput, DEFAULT_BASE_URL, validateContextName, } from "../lib/config.js";
|
|
3
|
+
import { printTable, printJson, printSuccess, maskKey } from "../lib/output.js";
|
|
4
|
+
import { handleError } from "../lib/errors.js";
|
|
5
|
+
export function makeContextCommand() {
|
|
6
|
+
const cmd = new Command("context").description("Manage CLI contexts (environments)");
|
|
7
|
+
cmd
|
|
8
|
+
.command("list")
|
|
9
|
+
.description("List all contexts")
|
|
10
|
+
.action((_opts, command) => {
|
|
11
|
+
try {
|
|
12
|
+
const contexts = getAllContexts();
|
|
13
|
+
const active = getActiveContextName();
|
|
14
|
+
const rows = Object.entries(contexts).map(([name, ctx]) => ({
|
|
15
|
+
name,
|
|
16
|
+
baseUrl: ctx.baseUrl,
|
|
17
|
+
apiKey: ctx.apiKey ? maskKey(ctx.apiKey) : "(unset)",
|
|
18
|
+
active: name === active ? "*" : "",
|
|
19
|
+
}));
|
|
20
|
+
if (isJsonOutput(command)) {
|
|
21
|
+
printJson({ active, contexts });
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (rows.length === 0) {
|
|
25
|
+
process.stderr.write('No contexts configured. Run "iot auth login" to create one.\n');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
printTable(rows, [
|
|
29
|
+
{ key: "active", header: "" },
|
|
30
|
+
{ key: "name", header: "NAME" },
|
|
31
|
+
{ key: "baseUrl", header: "BASE URL" },
|
|
32
|
+
{ key: "apiKey", header: "API KEY" },
|
|
33
|
+
]);
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
handleError(err, { json: isJsonOutput(command) });
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
cmd
|
|
40
|
+
.command("use <name>")
|
|
41
|
+
.description("Switch the active context")
|
|
42
|
+
.action((name, _opts, command) => {
|
|
43
|
+
try {
|
|
44
|
+
setActiveContext(name);
|
|
45
|
+
if (isJsonOutput(command)) {
|
|
46
|
+
printJson({ success: true, currentContext: name });
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
printSuccess(`Switched to context "${name}".`, "stderr");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
handleError(err, { json: isJsonOutput(command) });
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
cmd
|
|
57
|
+
.command("create <name>")
|
|
58
|
+
.description("Create a new empty context")
|
|
59
|
+
.option("--base-url <url>", "Base URL for the new context", DEFAULT_BASE_URL)
|
|
60
|
+
.action((name, opts, command) => {
|
|
61
|
+
try {
|
|
62
|
+
validateContextName(name);
|
|
63
|
+
if (getAllContexts()[name]) {
|
|
64
|
+
throw new Error(`Context "${name}" already exists.`);
|
|
65
|
+
}
|
|
66
|
+
setContext(name, { apiKey: "", baseUrl: opts.baseUrl });
|
|
67
|
+
if (isJsonOutput(command)) {
|
|
68
|
+
printJson({ success: true, name, baseUrl: opts.baseUrl });
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
printSuccess(`Created context "${name}". Run "iot auth login" to set its API key, or "iot context use ${name}".`, "stderr");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
handleError(err, { json: isJsonOutput(command) });
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
cmd
|
|
79
|
+
.command("rename <old> <new>")
|
|
80
|
+
.description("Rename a context")
|
|
81
|
+
.action((oldName, newName, _opts, command) => {
|
|
82
|
+
try {
|
|
83
|
+
renameContext(oldName, newName);
|
|
84
|
+
if (isJsonOutput(command)) {
|
|
85
|
+
printJson({ success: true, from: oldName, to: newName });
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
printSuccess(`Renamed "${oldName}" to "${newName}".`, "stderr");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
handleError(err, { json: isJsonOutput(command) });
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
cmd
|
|
96
|
+
.command("delete <name>")
|
|
97
|
+
.description("Delete a context (refuses if it's the only one)")
|
|
98
|
+
.action((name, _opts, command) => {
|
|
99
|
+
try {
|
|
100
|
+
const wasActive = getActiveContextName() === name;
|
|
101
|
+
deleteContext(name);
|
|
102
|
+
if (isJsonOutput(command)) {
|
|
103
|
+
printJson({ success: true, deleted: name, wasActive });
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
if (wasActive) {
|
|
107
|
+
process.stderr.write(`Deleted active context "${name}". Switched to "${getActiveContextName()}".\n`);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
printSuccess(`Deleted context "${name}".`, "stderr");
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
handleError(err, { json: isJsonOutput(command) });
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
cmd
|
|
119
|
+
.command("current")
|
|
120
|
+
.description("Print the active context name (script-friendly)")
|
|
121
|
+
.action((_opts, command) => {
|
|
122
|
+
try {
|
|
123
|
+
const name = getActiveContextName();
|
|
124
|
+
if (isJsonOutput(command)) {
|
|
125
|
+
printJson({ currentContext: name });
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
process.stdout.write(name + "\n");
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
handleError(err, { json: isJsonOutput(command) });
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
return cmd;
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=context.js.map
|