@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 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,3 @@
1
+ import { Command } from "commander";
2
+ export declare function makeAuthCommand(): Command;
3
+ //# sourceMappingURL=auth.d.ts.map
@@ -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,3 @@
1
+ import { Command } from "commander";
2
+ export declare function makeConfigCommand(): Command;
3
+ //# sourceMappingURL=config.d.ts.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,3 @@
1
+ import { Command } from "commander";
2
+ export declare function makeContextCommand(): Command;
3
+ //# sourceMappingURL=context.d.ts.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
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ export declare function makeDocsCommand(): Command;
3
+ //# sourceMappingURL=docs.d.ts.map