@fastino-ai/pioneer-cli 0.1.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/README.md ADDED
@@ -0,0 +1,116 @@
1
+ # Pioneer CLI
2
+
3
+ Command-line interface for the Pioneer AI training platform.
4
+
5
+ ```
6
+ ██████╗ ██╗ ██████╗ ███╗ ██╗███████╗███████╗██████╗
7
+ ██╔══██╗██║██╔═══██╗████╗ ██║██╔════╝██╔════╝██╔══██╗
8
+ ██████╔╝██║██║ ██║██╔██╗ ██║█████╗ █████╗ ██████╔╝
9
+ ██╔═══╝ ██║██║ ██║██║╚██╗██║██╔══╝ ██╔══╝ ██╔══██╗
10
+ ██║ ██║╚██████╔╝██║ ╚████║███████╗███████╗██║ ██║
11
+ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚══════╝╚══════╝╚═╝ ╚═╝
12
+ ```
13
+
14
+ ## Installation
15
+
16
+ ### Quick Install (requires Bun)
17
+
18
+ ```bash
19
+ curl -fsSL https://pioneer.ai/install.sh | sh
20
+ ```
21
+
22
+ ### Manual Install
23
+
24
+ ```bash
25
+ # Install Bun if needed
26
+ curl -fsSL https://bun.sh/install | bash
27
+
28
+ # Clone and install
29
+ git clone https://github.com/fastino-ai/pioneer-cli.git
30
+ cd pioneer-cli/
31
+ bun install
32
+
33
+ # Run directly
34
+ bun run src/index.tsx --help
35
+ ```
36
+
37
+ ### npm (coming soon)
38
+
39
+ ```bash
40
+ npm install -g @fastino-ai/pioneer-cli
41
+ ```
42
+
43
+ ## Usage
44
+
45
+ ```bash
46
+ # Show help
47
+ pioneer --help
48
+
49
+ # Authentication
50
+ pioneer auth login # Enter API key interactively
51
+ pioneer auth logout # Clear stored API key
52
+ pioneer auth status # Check if logged in
53
+
54
+ # Datasets
55
+ pioneer dataset list
56
+ pioneer dataset get <id>
57
+ pioneer dataset delete <id>
58
+ pioneer dataset download <id>
59
+ pioneer dataset analyze <id>
60
+
61
+ # Training Jobs
62
+ pioneer job list
63
+ pioneer job get <id>
64
+ pioneer job logs <id>
65
+ pioneer job create --model-name "My Model" --dataset-ids ds_123,ds_456
66
+
67
+ # Models
68
+ pioneer model list # List deployed models
69
+ pioneer model trained # List trained models
70
+ pioneer model delete <id>
71
+ pioneer model download <id>
72
+ ```
73
+
74
+ ## Configuration
75
+
76
+ The CLI stores configuration in `~/.pioneer/config.json`.
77
+
78
+ ### Environment Variables
79
+
80
+ | Variable | Description |
81
+ |----------|-------------|
82
+ | `PIONEER_API_URL` | API base URL (default: `https://agent.pioneer.ai`) |
83
+ | `PIONEER_API_KEY` | API key (overrides saved key) |
84
+
85
+ ## Authentication
86
+
87
+ The CLI validates your API key by making a test request to `/felix/datasets`. If successful, the key is saved locally for future use.
88
+
89
+ ```bash
90
+ # Login with API key
91
+ pioneer auth login
92
+ # Enter your API key when prompted
93
+
94
+ # Or set via environment
95
+ export PIONEER_API_KEY="your-api-key"
96
+ pioneer dataset list
97
+ ```
98
+
99
+ ## Development
100
+
101
+ ```bash
102
+ cd pioneer
103
+ bun install
104
+ bun run dev # Hot reload
105
+ bun run typecheck # Type checking
106
+ ```
107
+
108
+ ## Tech Stack
109
+
110
+ - **Runtime**: [Bun](https://bun.sh)
111
+ - **UI**: [Ink](https://github.com/vadimdemedes/ink) (React for CLI)
112
+ - **Language**: TypeScript
113
+
114
+ ## License
115
+
116
+ MIT
package/bun.lock ADDED
@@ -0,0 +1,127 @@
1
+ {
2
+ "lockfileVersion": 1,
3
+ "configVersion": 1,
4
+ "workspaces": {
5
+ "": {
6
+ "name": "pioneer",
7
+ "dependencies": {
8
+ "ink": "^4.4.1",
9
+ "ink-spinner": "^5.0.0",
10
+ "ink-text-input": "^5.0.1",
11
+ "react": "^18.3.1",
12
+ },
13
+ "devDependencies": {
14
+ "@types/node": "^22.0.0",
15
+ "@types/react": "^18.3.0",
16
+ "typescript": "^5.6.3",
17
+ },
18
+ },
19
+ },
20
+ "packages": {
21
+ "@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.1.3", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-3yWxPTq3UQ/FY9p1ErPxIyfT64elWaMvM9lIHnaqpyft63tkxodF5aUElYHrdisWve5cETkh1+KBw1yJuW0aRw=="],
22
+
23
+ "@types/node": ["@types/node@22.19.3", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA=="],
24
+
25
+ "@types/prop-types": ["@types/prop-types@15.7.15", "", {}, "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="],
26
+
27
+ "@types/react": ["@types/react@18.3.27", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" } }, "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w=="],
28
+
29
+ "ansi-escapes": ["ansi-escapes@6.2.1", "", {}, "sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig=="],
30
+
31
+ "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
32
+
33
+ "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
34
+
35
+ "auto-bind": ["auto-bind@5.0.1", "", {}, "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg=="],
36
+
37
+ "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
38
+
39
+ "ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="],
40
+
41
+ "cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="],
42
+
43
+ "cli-cursor": ["cli-cursor@4.0.0", "", { "dependencies": { "restore-cursor": "^4.0.0" } }, "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg=="],
44
+
45
+ "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="],
46
+
47
+ "cli-truncate": ["cli-truncate@3.1.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^5.0.0" } }, "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA=="],
48
+
49
+ "code-excerpt": ["code-excerpt@4.0.0", "", { "dependencies": { "convert-to-spaces": "^2.0.1" } }, "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA=="],
50
+
51
+ "convert-to-spaces": ["convert-to-spaces@2.0.1", "", {}, "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ=="],
52
+
53
+ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
54
+
55
+ "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
56
+
57
+ "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
58
+
59
+ "escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="],
60
+
61
+ "indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="],
62
+
63
+ "ink": ["ink@4.4.1", "", { "dependencies": { "@alcalzone/ansi-tokenize": "^0.1.3", "ansi-escapes": "^6.0.0", "auto-bind": "^5.0.1", "chalk": "^5.2.0", "cli-boxes": "^3.0.0", "cli-cursor": "^4.0.0", "cli-truncate": "^3.1.0", "code-excerpt": "^4.0.0", "indent-string": "^5.0.0", "is-ci": "^3.0.1", "is-lower-case": "^2.0.2", "is-upper-case": "^2.0.2", "lodash": "^4.17.21", "patch-console": "^2.0.0", "react-reconciler": "^0.29.0", "scheduler": "^0.23.0", "signal-exit": "^3.0.7", "slice-ansi": "^6.0.0", "stack-utils": "^2.0.6", "string-width": "^5.1.2", "type-fest": "^0.12.0", "widest-line": "^4.0.1", "wrap-ansi": "^8.1.0", "ws": "^8.12.0", "yoga-wasm-web": "~0.3.3" }, "peerDependencies": { "@types/react": ">=18.0.0", "react": ">=18.0.0", "react-devtools-core": "^4.19.1" }, "optionalPeers": ["@types/react", "react-devtools-core"] }, "sha512-rXckvqPBB0Krifk5rn/5LvQGmyXwCUpBfmTwbkQNBY9JY8RSl3b8OftBNEYxg4+SWUhEKcPifgope28uL9inlA=="],
64
+
65
+ "ink-spinner": ["ink-spinner@5.0.0", "", { "dependencies": { "cli-spinners": "^2.7.0" }, "peerDependencies": { "ink": ">=4.0.0", "react": ">=18.0.0" } }, "sha512-EYEasbEjkqLGyPOUc8hBJZNuC5GvXGMLu0w5gdTNskPc7Izc5vO3tdQEYnzvshucyGCBXc86ig0ujXPMWaQCdA=="],
66
+
67
+ "ink-text-input": ["ink-text-input@5.0.1", "", { "dependencies": { "chalk": "^5.2.0", "type-fest": "^3.6.1" }, "peerDependencies": { "ink": "^4.0.0", "react": "^18.0.0" } }, "sha512-crnsYJalG4EhneOFnr/q+Kzw1RgmXI2KsBaLFE6mpiIKxAtJLUnvygOF2IUKO8z4nwkSkveGRBMd81RoYdRSag=="],
68
+
69
+ "is-ci": ["is-ci@3.0.1", "", { "dependencies": { "ci-info": "^3.2.0" }, "bin": { "is-ci": "bin.js" } }, "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ=="],
70
+
71
+ "is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="],
72
+
73
+ "is-lower-case": ["is-lower-case@2.0.2", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-bVcMJy4X5Og6VZfdOZstSexlEy20Sr0k/p/b2IlQJlfdKAQuMpiv5w2Ccxb8sKdRUNAG1PnHVHjFSdRDVS6NlQ=="],
74
+
75
+ "is-upper-case": ["is-upper-case@2.0.2", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-44pxmxAvnnAOwBg4tHPnkfvgjPwbc5QIsSstNU+YcJ1ovxVzCWpSGosPJOZh/a1tdl81fbgnLc9LLv+x2ywbPQ=="],
76
+
77
+ "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
78
+
79
+ "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
80
+
81
+ "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
82
+
83
+ "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
84
+
85
+ "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
86
+
87
+ "patch-console": ["patch-console@2.0.0", "", {}, "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA=="],
88
+
89
+ "react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="],
90
+
91
+ "react-reconciler": ["react-reconciler@0.29.2", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-zZQqIiYgDCTP/f1N/mAR10nJGrPD2ZR+jDSEsKWJHYC7Cm2wodlwbR3upZRdC3cjIjSlTLNVyO7Iu0Yy7t2AYg=="],
92
+
93
+ "restore-cursor": ["restore-cursor@4.0.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg=="],
94
+
95
+ "scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
96
+
97
+ "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
98
+
99
+ "slice-ansi": ["slice-ansi@6.0.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-6bn4hRfkTvDfUoEQYkERg0BVF1D0vrX9HEkMl08uDiNWvVvjylLHvZFZWkDo6wjT8tUctbYl1nCOuE66ZTaUtA=="],
100
+
101
+ "stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="],
102
+
103
+ "string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
104
+
105
+ "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
106
+
107
+ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
108
+
109
+ "type-fest": ["type-fest@0.12.0", "", {}, "sha512-53RyidyjvkGpnWPMF9bQgFtWp+Sl8O2Rp13VavmJgfAP9WWG6q6TkrKU8iyJdnwnfgHI6k2hTlgqH4aSdjoTbg=="],
110
+
111
+ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
112
+
113
+ "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
114
+
115
+ "widest-line": ["widest-line@4.0.1", "", { "dependencies": { "string-width": "^5.0.1" } }, "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig=="],
116
+
117
+ "wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
118
+
119
+ "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
120
+
121
+ "yoga-wasm-web": ["yoga-wasm-web@0.3.3", "", {}, "sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA=="],
122
+
123
+ "cli-truncate/slice-ansi": ["slice-ansi@5.0.0", "", { "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ=="],
124
+
125
+ "ink-text-input/type-fest": ["type-fest@3.13.1", "", {}, "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g=="],
126
+ }
127
+ }
package/install.sh ADDED
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env sh
2
+ # Pioneer CLI installer
3
+ # Usage: curl -fsSL https://pioneer.ai/install.sh | sh
4
+ set -e
5
+
6
+ cat <<'EOF'
7
+ ██████╗ ██╗ ██████╗ ███╗ ██╗███████╗███████╗██████╗
8
+ ██╔══██╗██║██╔═══██╗████╗ ██║██╔════╝██╔════╝██╔══██╗
9
+ ██████╔╝██║██║ ██║██╔██╗ ██║█████╗ █████╗ ██████╔╝
10
+ ██╔═══╝ ██║██║ ██║██║╚██╗██║██╔══╝ ██╔══╝ ██╔══██╗
11
+ ██║ ██║╚██████╔╝██║ ╚████║███████╗███████╗██║ ██║
12
+ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚══════╝╚══════╝╚═╝ ╚═╝
13
+ EOF
14
+
15
+ echo ""
16
+ echo "Installing Pioneer CLI..."
17
+ echo ""
18
+
19
+ # Check for Bun
20
+ if ! command -v bun >/dev/null 2>&1; then
21
+ echo "Bun is required but not installed."
22
+ echo "Install Bun first: curl -fsSL https://bun.sh/install | bash"
23
+ exit 1
24
+ fi
25
+
26
+ # Clone or update repo
27
+ INSTALL_DIR="${PIONEER_INSTALL_DIR:-$HOME/.pioneer-cli}"
28
+
29
+ if [ -d "$INSTALL_DIR" ]; then
30
+ echo "Updating existing installation..."
31
+ cd "$INSTALL_DIR"
32
+ git pull --quiet origin main 2>/dev/null || true
33
+ else
34
+ echo "Cloning Pioneer CLI..."
35
+ git clone --quiet --depth 1 https://github.com/fastino-ai/pioneer-cli.git "$INSTALL_DIR" 2>/dev/null || {
36
+ echo "Git clone failed. Creating local installation..."
37
+ mkdir -p "$INSTALL_DIR"
38
+ }
39
+ fi
40
+
41
+ # Install dependencies
42
+ cd "$INSTALL_DIR/pioneer" 2>/dev/null || cd "$INSTALL_DIR"
43
+ if [ -f "package.json" ]; then
44
+ echo "Installing dependencies..."
45
+ bun install --silent
46
+ fi
47
+
48
+ # Create symlink
49
+ BIN_DIR="${BIN_DIR:-$HOME/.local/bin}"
50
+ mkdir -p "$BIN_DIR"
51
+
52
+ cat > "$BIN_DIR/pioneer" << 'SCRIPT'
53
+ #!/usr/bin/env sh
54
+ PIONEER_DIR="${PIONEER_INSTALL_DIR:-$HOME/.pioneer-cli}"
55
+ exec bun run "$PIONEER_DIR/pioneer/src/index.tsx" "$@"
56
+ SCRIPT
57
+ chmod +x "$BIN_DIR/pioneer"
58
+
59
+ echo ""
60
+ echo "Pioneer CLI installed successfully!"
61
+ echo ""
62
+ echo "Add to PATH if needed:"
63
+ echo " export PATH=\"\$HOME/.local/bin:\$PATH\""
64
+ echo ""
65
+ echo "Get started:"
66
+ echo " pioneer --help"
67
+ echo " pioneer auth login"
68
+ echo ""
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@fastino-ai/pioneer-cli",
3
+ "version": "0.1.0",
4
+ "description": "Pioneer CLI - AI training platform",
5
+ "type": "module",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "bin": {
10
+ "pioneer": "./src/index.tsx"
11
+ },
12
+ "scripts": {
13
+ "start": "bun run src/index.tsx",
14
+ "dev": "bun run --hot src/index.tsx",
15
+ "typecheck": "tsc --noEmit"
16
+ },
17
+ "dependencies": {
18
+ "ink": "^4.4.1",
19
+ "ink-spinner": "^5.0.0",
20
+ "ink-text-input": "^5.0.1",
21
+ "react": "^18.3.1"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "^22.0.0",
25
+ "@types/react": "^18.3.0",
26
+ "typescript": "^5.6.3"
27
+ },
28
+ "engines": {
29
+ "bun": ">=1.1.0"
30
+ }
31
+ }
package/src/api.ts ADDED
@@ -0,0 +1,348 @@
1
+ /**
2
+ * Felix API client for Pioneer CLI.
3
+ * Calls the real /felix/* endpoints.
4
+ */
5
+
6
+ import { getApiKey, getBaseUrl } from "./config.js";
7
+
8
+ export interface ApiResult<T = unknown> {
9
+ ok: boolean;
10
+ status: number;
11
+ data?: T;
12
+ error?: string;
13
+ }
14
+
15
+ async function request<T = unknown>(
16
+ method: string,
17
+ path: string,
18
+ body?: unknown
19
+ ): Promise<ApiResult<T>> {
20
+ const baseUrl = getBaseUrl().replace(/\/$/, "");
21
+ const apiKey = getApiKey();
22
+ const url = `${baseUrl}${path}`;
23
+
24
+ const headers: Record<string, string> = {
25
+ "Content-Type": "application/json",
26
+ "User-Agent": "pioneer-cli/0.1.0",
27
+ };
28
+
29
+ if (apiKey) {
30
+ headers["X-API-Key"] = apiKey;
31
+ }
32
+
33
+ try {
34
+ const res = await fetch(url, {
35
+ method,
36
+ headers,
37
+ body: body ? JSON.stringify(body) : undefined,
38
+ });
39
+
40
+ const text = await res.text();
41
+ let data: T | undefined;
42
+ try {
43
+ data = JSON.parse(text) as T;
44
+ } catch {
45
+ // Not JSON
46
+ }
47
+
48
+ if (!res.ok) {
49
+ return {
50
+ ok: false,
51
+ status: res.status,
52
+ error: data ? JSON.stringify(data) : text || `HTTP ${res.status}`,
53
+ };
54
+ }
55
+
56
+ return { ok: true, status: res.status, data };
57
+ } catch (err) {
58
+ return {
59
+ ok: false,
60
+ status: 0,
61
+ error: err instanceof Error ? err.message : String(err),
62
+ };
63
+ }
64
+ }
65
+
66
+ // ─────────────────────────────────────────────────────────────────────────────
67
+ // Auth - validate API key by calling /felix/datasets
68
+ // ─────────────────────────────────────────────────────────────────────────────
69
+
70
+ export async function validateApiKey(apiKey: string): Promise<ApiResult> {
71
+ const baseUrl = getBaseUrl().replace(/\/$/, "");
72
+ const url = `${baseUrl}/felix/datasets`;
73
+
74
+ try {
75
+ const res = await fetch(url, {
76
+ method: "GET",
77
+ headers: {
78
+ "Content-Type": "application/json",
79
+ "User-Agent": "pioneer-cli/0.1.0",
80
+ "X-API-Key": apiKey,
81
+ },
82
+ });
83
+
84
+ if (res.ok) {
85
+ return { ok: true, status: res.status };
86
+ }
87
+
88
+ if (res.status === 401 || res.status === 403) {
89
+ return { ok: false, status: res.status, error: "Invalid API key" };
90
+ }
91
+
92
+ const text = await res.text();
93
+ return { ok: false, status: res.status, error: text || `HTTP ${res.status}` };
94
+ } catch (err) {
95
+ return {
96
+ ok: false,
97
+ status: 0,
98
+ error: err instanceof Error ? err.message : String(err),
99
+ };
100
+ }
101
+ }
102
+
103
+ // ─────────────────────────────────────────────────────────────────────────────
104
+ // Datasets
105
+ // ─────────────────────────────────────────────────────────────────────────────
106
+
107
+ export interface Dataset {
108
+ id: string;
109
+ user_id: string;
110
+ dataset_name: string;
111
+ dataset_path: string;
112
+ dataset_type: string;
113
+ size?: number;
114
+ sample_size?: number;
115
+ created_at: string;
116
+ updated_at: string;
117
+ }
118
+
119
+ export interface DatasetListResponse {
120
+ success: boolean;
121
+ datasets: Dataset[];
122
+ count: number;
123
+ }
124
+
125
+ export async function listDatasets(): Promise<ApiResult<DatasetListResponse>> {
126
+ return request<DatasetListResponse>("GET", "/felix/datasets");
127
+ }
128
+
129
+ export async function getDataset(datasetId: string): Promise<ApiResult<Dataset>> {
130
+ return request<Dataset>("GET", `/felix/datasets/${datasetId}`);
131
+ }
132
+
133
+ export interface DatasetCreateRequest {
134
+ dataset_name: string;
135
+ dataset_path: string;
136
+ dataset_type?: "classification" | "ner" | "custom";
137
+ size?: number;
138
+ sample_size?: number;
139
+ }
140
+
141
+ export async function createDataset(
142
+ req: DatasetCreateRequest
143
+ ): Promise<ApiResult<Dataset>> {
144
+ return request<Dataset>("POST", "/felix/datasets", req);
145
+ }
146
+
147
+ export async function deleteDataset(datasetId: string): Promise<ApiResult> {
148
+ return request("DELETE", `/felix/datasets/${datasetId}`);
149
+ }
150
+
151
+ export interface DatasetDownloadResponse {
152
+ download_url: string;
153
+ }
154
+
155
+ export async function downloadDataset(
156
+ datasetId: string
157
+ ): Promise<ApiResult<DatasetDownloadResponse>> {
158
+ return request<DatasetDownloadResponse>(
159
+ "GET",
160
+ `/felix/datasets/${datasetId}/download`
161
+ );
162
+ }
163
+
164
+ export interface DatasetAnalysisRequest {
165
+ dataset_id: string;
166
+ }
167
+
168
+ export interface DatasetAnalysisResponse {
169
+ success: boolean;
170
+ analysis: unknown;
171
+ }
172
+
173
+ export async function analyzeDataset(
174
+ datasetId: string
175
+ ): Promise<ApiResult<DatasetAnalysisResponse>> {
176
+ return request<DatasetAnalysisResponse>("POST", "/felix/dataset/analyze", {
177
+ dataset_id: datasetId,
178
+ });
179
+ }
180
+
181
+ // ─────────────────────────────────────────────────────────────────────────────
182
+ // Training Jobs
183
+ // ─────────────────────────────────────────────────────────────────────────────
184
+
185
+ export interface TrainingJob {
186
+ id: string;
187
+ user_id: string;
188
+ model_name?: string;
189
+ dataset_ids: string[];
190
+ train_dataset_paths: string[];
191
+ base_model: string;
192
+ validation_data_percentage: number;
193
+ nr_epochs: number;
194
+ learning_rate: number;
195
+ batch_size: number;
196
+ trained_model_path?: string;
197
+ sagemaker_job_name?: string;
198
+ status: string;
199
+ error_message?: string;
200
+ created_at: string;
201
+ updated_at: string;
202
+ started_at?: string;
203
+ completed_at?: string;
204
+ }
205
+
206
+ export interface TrainingJobListResponse {
207
+ success: boolean;
208
+ training_jobs: TrainingJob[];
209
+ count: number;
210
+ }
211
+
212
+ export async function listJobs(): Promise<ApiResult<TrainingJobListResponse>> {
213
+ return request<TrainingJobListResponse>("GET", "/felix/training-jobs");
214
+ }
215
+
216
+ export async function getJob(jobId: string): Promise<ApiResult<TrainingJob>> {
217
+ return request<TrainingJob>("GET", `/felix/training-jobs/${jobId}`);
218
+ }
219
+
220
+ export interface TrainingJobCreateRequest {
221
+ model_name: string;
222
+ dataset_ids: string[];
223
+ base_model?: string;
224
+ validation_data_percentage?: number;
225
+ nr_epochs?: number;
226
+ learning_rate?: number;
227
+ batch_size?: number;
228
+ }
229
+
230
+ export async function createJob(
231
+ req: TrainingJobCreateRequest
232
+ ): Promise<ApiResult<TrainingJob>> {
233
+ return request<TrainingJob>("POST", "/felix/training-jobs", req);
234
+ }
235
+
236
+ export interface TrainingLog {
237
+ id: string;
238
+ job_id: string;
239
+ epoch: number;
240
+ step?: number;
241
+ training_loss?: number;
242
+ validation_loss?: number;
243
+ accuracy?: number;
244
+ learning_rate?: number;
245
+ gpu_memory_used?: number;
246
+ gpu_memory_total?: number;
247
+ created_at: string;
248
+ }
249
+
250
+ export interface TrainingLogsResponse {
251
+ success: boolean;
252
+ logs: TrainingLog[];
253
+ count: number;
254
+ }
255
+
256
+ export async function getJobLogs(
257
+ jobId: string
258
+ ): Promise<ApiResult<TrainingLogsResponse>> {
259
+ return request<TrainingLogsResponse>("GET", `/felix/training-jobs/${jobId}/logs`);
260
+ }
261
+
262
+ // ─────────────────────────────────────────────────────────────────────────────
263
+ // Models
264
+ // ─────────────────────────────────────────────────────────────────────────────
265
+
266
+ export interface DeployedModel {
267
+ job_id: string;
268
+ model_name?: string;
269
+ endpoint_name?: string;
270
+ status: string;
271
+ }
272
+
273
+ export interface DeployedModelsListResponse {
274
+ success: boolean;
275
+ models: DeployedModel[];
276
+ count: number;
277
+ }
278
+
279
+ export async function listModels(): Promise<ApiResult<DeployedModelsListResponse>> {
280
+ return request<DeployedModelsListResponse>("GET", "/felix/models");
281
+ }
282
+
283
+ export async function deleteModel(jobId: string): Promise<ApiResult> {
284
+ return request("DELETE", `/felix/models/${jobId}`);
285
+ }
286
+
287
+ export interface TrainedModel {
288
+ job_id: string;
289
+ model_name?: string;
290
+ trained_model_path?: string;
291
+ status: string;
292
+ }
293
+
294
+ export interface TrainedModelsListResponse {
295
+ success: boolean;
296
+ models: TrainedModel[];
297
+ count: number;
298
+ }
299
+
300
+ export async function listTrainedModels(): Promise<
301
+ ApiResult<TrainedModelsListResponse>
302
+ > {
303
+ return request<TrainedModelsListResponse>("GET", "/felix/trained-models");
304
+ }
305
+
306
+ export interface ModelDownloadResponse {
307
+ download_url: string;
308
+ }
309
+
310
+ export async function downloadModel(
311
+ jobId: string
312
+ ): Promise<ApiResult<ModelDownloadResponse>> {
313
+ return request<ModelDownloadResponse>(
314
+ "GET",
315
+ `/felix/training/jobs/${jobId}/download`
316
+ );
317
+ }
318
+
319
+ // ─────────────────────────────────────────────────────────────────────────────
320
+ // Evaluations
321
+ // ─────────────────────────────────────────────────────────────────────────────
322
+
323
+ export interface Evaluation {
324
+ id: string;
325
+ user_id: string;
326
+ dataset_id: string;
327
+ model_id?: string;
328
+ status: string;
329
+ results?: unknown;
330
+ created_at: string;
331
+ }
332
+
333
+ export async function getEvaluation(
334
+ evalId: string
335
+ ): Promise<ApiResult<Evaluation>> {
336
+ return request<Evaluation>("GET", `/felix/evaluations/${evalId}`);
337
+ }
338
+
339
+ export interface EvaluationCreateRequest {
340
+ dataset_id: string;
341
+ model_id?: string;
342
+ }
343
+
344
+ export async function createEvaluation(
345
+ req: EvaluationCreateRequest
346
+ ): Promise<ApiResult<Evaluation>> {
347
+ return request<Evaluation>("POST", "/felix/evaluations", req);
348
+ }
package/src/config.ts ADDED
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Configuration management for Pioneer CLI.
3
+ * Stores API key and base URL in ~/.pioneer/config.json
4
+ */
5
+
6
+ import fs from "fs";
7
+ import os from "os";
8
+ import path from "path";
9
+
10
+ export interface Config {
11
+ apiKey?: string;
12
+ baseUrl?: string;
13
+ }
14
+
15
+ const CONFIG_DIR = path.join(os.homedir(), ".pioneer");
16
+ const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
17
+
18
+ export const DEFAULT_BASE_URL =
19
+ process.env.PIONEER_API_URL ?? "http://localhost:5001";
20
+
21
+ function ensureConfigDir(): void {
22
+ if (!fs.existsSync(CONFIG_DIR)) {
23
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
24
+ }
25
+ }
26
+
27
+ export function loadConfig(): Config {
28
+ try {
29
+ if (fs.existsSync(CONFIG_FILE)) {
30
+ const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
31
+ return JSON.parse(raw) as Config;
32
+ }
33
+ } catch {
34
+ // Ignore parse errors
35
+ }
36
+ return {};
37
+ }
38
+
39
+ export function saveConfig(config: Config): void {
40
+ ensureConfigDir();
41
+ const existing = loadConfig();
42
+ const merged = { ...existing, ...config };
43
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2));
44
+ }
45
+
46
+ export function clearApiKey(): void {
47
+ const config = loadConfig();
48
+ delete config.apiKey;
49
+ ensureConfigDir();
50
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
51
+ }
52
+
53
+ export function getApiKey(): string | undefined {
54
+ // Environment variable takes precedence
55
+ if (process.env.PIONEER_API_KEY) {
56
+ return process.env.PIONEER_API_KEY;
57
+ }
58
+ return loadConfig().apiKey;
59
+ }
60
+
61
+ export function getBaseUrl(): string {
62
+ const config = loadConfig();
63
+ return config.baseUrl ?? DEFAULT_BASE_URL;
64
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,417 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Pioneer CLI - main entry point.
4
+ * Uses Ink (React) for terminal UI.
5
+ */
6
+
7
+ import React, { useState, useEffect } from "react";
8
+ import { render, Box, Text, useApp, useInput } from "ink";
9
+ import Spinner from "ink-spinner";
10
+ import TextInput from "ink-text-input";
11
+
12
+ import {
13
+ getApiKey,
14
+ getBaseUrl,
15
+ saveConfig,
16
+ clearApiKey,
17
+ } from "./config.js";
18
+ import * as api from "./api.js";
19
+
20
+ // ─────────────────────────────────────────────────────────────────────────────
21
+ // ASCII Banner
22
+ // ─────────────────────────────────────────────────────────────────────────────
23
+
24
+ const BANNER = `
25
+ ██████╗ ██╗ ██████╗ ███╗ ██╗███████╗███████╗██████╗
26
+ ██╔══██╗██║██╔═══██╗████╗ ██║██╔════╝██╔════╝██╔══██╗
27
+ ██████╔╝██║██║ ██║██╔██╗ ██║█████╗ █████╗ ██████╔╝
28
+ ██╔═══╝ ██║██║ ██║██║╚██╗██║██╔══╝ ██╔══╝ ██╔══██╗
29
+ ██║ ██║╚██████╔╝██║ ╚████║███████╗███████╗██║ ██║
30
+ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚══════╝╚══════╝╚═╝ ╚═╝
31
+ `.trim();
32
+
33
+ // ─────────────────────────────────────────────────────────────────────────────
34
+ // Helpers
35
+ // ─────────────────────────────────────────────────────────────────────────────
36
+
37
+ function formatJson(data: unknown): string {
38
+ return JSON.stringify(data, null, 2);
39
+ }
40
+
41
+ function parseArgs(argv: string[]): { command: string[]; flags: Record<string, string> } {
42
+ const command: string[] = [];
43
+ const flags: Record<string, string> = {};
44
+
45
+ for (let i = 0; i < argv.length; i++) {
46
+ const arg = argv[i];
47
+ if (arg.startsWith("--")) {
48
+ const key = arg.slice(2);
49
+ const next = argv[i + 1];
50
+ if (next && !next.startsWith("--")) {
51
+ flags[key] = next;
52
+ i++;
53
+ } else {
54
+ flags[key] = "true";
55
+ }
56
+ } else {
57
+ command.push(arg);
58
+ }
59
+ }
60
+
61
+ return { command, flags };
62
+ }
63
+
64
+ // ─────────────────────────────────────────────────────────────────────────────
65
+ // Components
66
+ // ─────────────────────────────────────────────────────────────────────────────
67
+
68
+ const Loading: React.FC<{ message?: string }> = ({ message }) => (
69
+ <Box>
70
+ <Text color="yellow">
71
+ <Spinner type="dots" />
72
+ </Text>
73
+ <Text> {message ?? "Loading..."}</Text>
74
+ </Box>
75
+ );
76
+
77
+ const ErrorMessage: React.FC<{ error: string }> = ({ error }) => (
78
+ <Box>
79
+ <Text color="red">Error: {error}</Text>
80
+ </Box>
81
+ );
82
+
83
+ const Success: React.FC<{ message: string }> = ({ message }) => (
84
+ <Box>
85
+ <Text color="green">✓ {message}</Text>
86
+ </Box>
87
+ );
88
+
89
+ const JsonOutput: React.FC<{ data: unknown }> = ({ data }) => (
90
+ <Box>
91
+ <Text>{formatJson(data)}</Text>
92
+ </Box>
93
+ );
94
+
95
+ // ─────────────────────────────────────────────────────────────────────────────
96
+ // Auth Login Component
97
+ // ─────────────────────────────────────────────────────────────────────────────
98
+
99
+ const AuthLogin: React.FC = () => {
100
+ const { exit } = useApp();
101
+ const [apiKey, setApiKey] = useState("");
102
+ const [state, setState] = useState<"input" | "validating" | "done" | "error">("input");
103
+ const [error, setError] = useState("");
104
+
105
+ const handleSubmit = async (value: string) => {
106
+ if (!value.trim()) {
107
+ setError("API key cannot be empty");
108
+ setState("error");
109
+ setTimeout(() => exit(), 1500);
110
+ return;
111
+ }
112
+
113
+ setState("validating");
114
+ const result = await api.validateApiKey(value.trim());
115
+
116
+ if (result.ok) {
117
+ saveConfig({ apiKey: value.trim() });
118
+ setState("done");
119
+ } else if (result.status === 401 || result.status === 403) {
120
+ setError(`Invalid API key (HTTP ${result.status}). Please check and try again.`);
121
+ setState("error");
122
+ } else {
123
+ setError(`Connection failed (HTTP ${result.status}): ${result.error ?? "Unknown error"}`);
124
+ setState("error");
125
+ }
126
+
127
+ setTimeout(() => exit(), 1500);
128
+ };
129
+
130
+ if (state === "validating") {
131
+ return <Loading message="Validating API key..." />;
132
+ }
133
+
134
+ if (state === "done") {
135
+ return <Success message="Logged in successfully. API key saved." />;
136
+ }
137
+
138
+ if (state === "error") {
139
+ return <ErrorMessage error={error} />;
140
+ }
141
+
142
+ return (
143
+ <Box flexDirection="column">
144
+ <Text>Enter your Pioneer API key:</Text>
145
+ <Box>
146
+ <Text color="cyan">&gt; </Text>
147
+ <TextInput value={apiKey} onChange={setApiKey} onSubmit={handleSubmit} mask="*" />
148
+ </Box>
149
+ </Box>
150
+ );
151
+ };
152
+
153
+ // ─────────────────────────────────────────────────────────────────────────────
154
+ // Auth Logout Component
155
+ // ─────────────────────────────────────────────────────────────────────────────
156
+
157
+ const AuthLogout: React.FC = () => {
158
+ const { exit } = useApp();
159
+
160
+ useEffect(() => {
161
+ clearApiKey();
162
+ setTimeout(() => exit(), 500);
163
+ }, [exit]);
164
+
165
+ return <Success message="Logged out. API key cleared." />;
166
+ };
167
+
168
+ // ─────────────────────────────────────────────────────────────────────────────
169
+ // Auth Status Component
170
+ // ─────────────────────────────────────────────────────────────────────────────
171
+
172
+ const AuthStatus: React.FC = () => {
173
+ const { exit } = useApp();
174
+ const apiKey = getApiKey();
175
+ const baseUrl = getBaseUrl();
176
+
177
+ useEffect(() => {
178
+ setTimeout(() => exit(), 500);
179
+ }, [exit]);
180
+
181
+ return (
182
+ <Box flexDirection="column">
183
+ <Text>Base URL: {baseUrl}</Text>
184
+ <Text>
185
+ Logged in:{" "}
186
+ <Text color={apiKey ? "green" : "red"}>{apiKey ? "Yes" : "No"}</Text>
187
+ </Text>
188
+ </Box>
189
+ );
190
+ };
191
+
192
+ // ─────────────────────────────────────────────────────────────────────────────
193
+ // Generic API Command Component
194
+ // ─────────────────────────────────────────────────────────────────────────────
195
+
196
+ interface ApiCommandProps<T> {
197
+ action: () => Promise<api.ApiResult<T>>;
198
+ successMessage?: string;
199
+ }
200
+
201
+ function ApiCommand<T>({ action, successMessage }: ApiCommandProps<T>) {
202
+ const { exit } = useApp();
203
+ const [state, setState] = useState<"loading" | "done" | "error">("loading");
204
+ const [data, setData] = useState<T | null>(null);
205
+ const [error, setError] = useState("");
206
+
207
+ useEffect(() => {
208
+ (async () => {
209
+ const result = await action();
210
+ if (result.ok) {
211
+ setData(result.data ?? null);
212
+ setState("done");
213
+ } else {
214
+ setError(result.error ?? "Unknown error");
215
+ setState("error");
216
+ }
217
+ setTimeout(() => exit(), 500);
218
+ })();
219
+ }, [action, exit]);
220
+
221
+ if (state === "loading") {
222
+ return <Loading />;
223
+ }
224
+
225
+ if (state === "error") {
226
+ return <ErrorMessage error={error} />;
227
+ }
228
+
229
+ return (
230
+ <Box flexDirection="column">
231
+ {successMessage && <Success message={successMessage} />}
232
+ {data && <JsonOutput data={data} />}
233
+ </Box>
234
+ );
235
+ }
236
+
237
+ // ─────────────────────────────────────────────────────────────────────────────
238
+ // Help Component
239
+ // ─────────────────────────────────────────────────────────────────────────────
240
+
241
+ const Help: React.FC = () => {
242
+ const { exit } = useApp();
243
+
244
+ useEffect(() => {
245
+ setTimeout(() => exit(), 100);
246
+ }, [exit]);
247
+
248
+ return (
249
+ <Box flexDirection="column">
250
+ <Text color="cyan">{BANNER}</Text>
251
+ <Text> </Text>
252
+ <Text bold>Usage:</Text>
253
+ <Text> pioneer {"<command>"} {"[options]"}</Text>
254
+ <Text> </Text>
255
+ <Text bold>Auth Commands:</Text>
256
+ <Text> auth login Login with API key</Text>
257
+ <Text> auth logout Clear stored API key</Text>
258
+ <Text> auth status Show auth status</Text>
259
+ <Text> </Text>
260
+ <Text bold>Dataset Commands:</Text>
261
+ <Text> dataset list List all datasets</Text>
262
+ <Text> dataset get {"<id>"} Get dataset details</Text>
263
+ <Text> dataset delete {"<id>"} Delete a dataset</Text>
264
+ <Text> dataset download {"<id>"} Get download URL</Text>
265
+ <Text> dataset analyze {"<id>"} Analyze a dataset</Text>
266
+ <Text> </Text>
267
+ <Text bold>Job Commands:</Text>
268
+ <Text> job list List training jobs</Text>
269
+ <Text> job get {"<id>"} Get job details</Text>
270
+ <Text> job logs {"<id>"} Get job logs</Text>
271
+ <Text> job create Create training job</Text>
272
+ <Text> --model-name {"<name>"} Model name (required)</Text>
273
+ <Text> --dataset-ids {"<ids>"} Comma-separated dataset IDs</Text>
274
+ <Text> --base-model {"<model>"} Base model (default: fastino/gliner2-base-v1)</Text>
275
+ <Text> --epochs {"<n>"} Number of epochs (default: 5)</Text>
276
+ <Text> </Text>
277
+ <Text bold>Model Commands:</Text>
278
+ <Text> model list List deployed models</Text>
279
+ <Text> model trained List trained models</Text>
280
+ <Text> model delete {"<id>"} Undeploy a model</Text>
281
+ <Text> model download {"<id>"} Get model download URL</Text>
282
+ <Text> </Text>
283
+ <Text bold>Options:</Text>
284
+ <Text> --help Show this help</Text>
285
+ <Text> </Text>
286
+ <Text dimColor>Environment:</Text>
287
+ <Text dimColor> PIONEER_API_URL API base URL (default: http://localhost:5001)</Text>
288
+ <Text dimColor> PIONEER_API_KEY API key (overrides saved key)</Text>
289
+ </Box>
290
+ );
291
+ };
292
+
293
+ // ─────────────────────────────────────────────────────────────────────────────
294
+ // Main Router
295
+ // ─────────────────────────────────────────────────────────────────────────────
296
+
297
+ interface AppProps {
298
+ command: string[];
299
+ flags: Record<string, string>;
300
+ }
301
+
302
+ const App: React.FC<AppProps> = ({ command, flags }) => {
303
+ const [group, action, ...rest] = command;
304
+
305
+ // Help
306
+ if (!group || flags.help === "true") {
307
+ return <Help />;
308
+ }
309
+
310
+ // Auth commands
311
+ if (group === "auth") {
312
+ if (action === "login") return <AuthLogin />;
313
+ if (action === "logout") return <AuthLogout />;
314
+ if (action === "status") return <AuthStatus />;
315
+ return <Help />;
316
+ }
317
+
318
+ // Dataset commands
319
+ if (group === "dataset") {
320
+ if (action === "list") {
321
+ return <ApiCommand action={api.listDatasets} />;
322
+ }
323
+ if (action === "get" && rest[0]) {
324
+ return <ApiCommand action={() => api.getDataset(rest[0])} />;
325
+ }
326
+ if (action === "delete" && rest[0]) {
327
+ return (
328
+ <ApiCommand
329
+ action={() => api.deleteDataset(rest[0])}
330
+ successMessage={`Dataset ${rest[0]} deleted`}
331
+ />
332
+ );
333
+ }
334
+ if (action === "download" && rest[0]) {
335
+ return <ApiCommand action={() => api.downloadDataset(rest[0])} />;
336
+ }
337
+ if (action === "analyze" && rest[0]) {
338
+ return <ApiCommand action={() => api.analyzeDataset(rest[0])} />;
339
+ }
340
+ return <Help />;
341
+ }
342
+
343
+ // Job commands
344
+ if (group === "job") {
345
+ if (action === "list") {
346
+ return <ApiCommand action={api.listJobs} />;
347
+ }
348
+ if (action === "get" && rest[0]) {
349
+ return <ApiCommand action={() => api.getJob(rest[0])} />;
350
+ }
351
+ if (action === "logs" && rest[0]) {
352
+ return <ApiCommand action={() => api.getJobLogs(rest[0])} />;
353
+ }
354
+ if (action === "create") {
355
+ const modelName = flags["model-name"];
356
+ const datasetIds = flags["dataset-ids"]?.split(",") ?? [];
357
+ const baseModel = flags["base-model"];
358
+ const epochs = flags["epochs"] ? parseInt(flags["epochs"], 10) : undefined;
359
+
360
+ if (!modelName || datasetIds.length === 0) {
361
+ return <ErrorMessage error="--model-name and --dataset-ids are required" />;
362
+ }
363
+
364
+ return (
365
+ <ApiCommand
366
+ action={() =>
367
+ api.createJob({
368
+ model_name: modelName,
369
+ dataset_ids: datasetIds,
370
+ base_model: baseModel,
371
+ nr_epochs: epochs,
372
+ })
373
+ }
374
+ successMessage="Training job created"
375
+ />
376
+ );
377
+ }
378
+ return <Help />;
379
+ }
380
+
381
+ // Model commands
382
+ if (group === "model") {
383
+ if (action === "list") {
384
+ return <ApiCommand action={api.listModels} />;
385
+ }
386
+ if (action === "trained") {
387
+ return <ApiCommand action={api.listTrainedModels} />;
388
+ }
389
+ if (action === "delete" && rest[0]) {
390
+ return (
391
+ <ApiCommand
392
+ action={() => api.deleteModel(rest[0])}
393
+ successMessage={`Model ${rest[0]} deleted`}
394
+ />
395
+ );
396
+ }
397
+ if (action === "download" && rest[0]) {
398
+ return <ApiCommand action={() => api.downloadModel(rest[0])} />;
399
+ }
400
+ return <Help />;
401
+ }
402
+
403
+ return <Help />;
404
+ };
405
+
406
+ // ─────────────────────────────────────────────────────────────────────────────
407
+ // Entry point
408
+ // ─────────────────────────────────────────────────────────────────────────────
409
+
410
+ function main() {
411
+ const argv = process.argv.slice(2);
412
+ const { command, flags } = parseArgs(argv);
413
+
414
+ render(<App command={command} flags={flags} />);
415
+ }
416
+
417
+ main();
package/tsconfig.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "jsx": "react-jsx",
7
+ "jsxImportSource": "react",
8
+ "allowJs": false,
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "skipLibCheck": true,
13
+ "resolveJsonModule": true,
14
+ "types": [
15
+ "node"
16
+ ]
17
+ },
18
+ "include": [
19
+ "src"
20
+ ]
21
+ }