@tutorflow/cli 0.1.1 → 0.2.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 +195 -89
- package/dist/client.d.ts +6 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +51 -18
- package/dist/client.js.map +1 -1
- package/dist/commands/auth.d.ts.map +1 -1
- package/dist/commands/auth.js +48 -45
- package/dist/commands/auth.js.map +1 -1
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +34 -45
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/course.d.ts.map +1 -1
- package/dist/commands/course.js +61 -75
- package/dist/commands/course.js.map +1 -1
- package/dist/commands/evaluate.d.ts.map +1 -1
- package/dist/commands/evaluate.js +49 -55
- package/dist/commands/evaluate.js.map +1 -1
- package/dist/commands/slide.d.ts.map +1 -1
- package/dist/commands/slide.js +65 -76
- package/dist/commands/slide.js.map +1 -1
- package/dist/commands/video.d.ts.map +1 -1
- package/dist/commands/video.js +66 -78
- package/dist/commands/video.js.map +1 -1
- package/dist/index.js +17 -2
- package/dist/index.js.map +1 -1
- package/dist/utils/output.d.ts +15 -2
- package/dist/utils/output.d.ts.map +1 -1
- package/dist/utils/output.js +66 -22
- package/dist/utils/output.js.map +1 -1
- package/dist/utils/runtime.d.ts +39 -0
- package/dist/utils/runtime.d.ts.map +1 -0
- package/dist/utils/runtime.js +105 -0
- package/dist/utils/runtime.js.map +1 -0
- package/package.json +18 -2
package/README.md
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
# @tutorflow/cli
|
|
2
2
|
|
|
3
|
-
TutorFlow CLI for creating AI-powered educational content (slides, videos, courses) and evaluating student responses from the terminal. Wraps the TutorFlow Platform API (`/v1/platform/*`).
|
|
3
|
+
TutorFlow CLI for creating AI-powered educational content (slides, videos, courses) and evaluating student responses from the terminal or from AI agents. Wraps the TutorFlow Platform API (`/v1/platform/*`).
|
|
4
4
|
|
|
5
5
|
- **Binary:** `tutorflow`
|
|
6
|
-
- **
|
|
7
|
-
- **
|
|
8
|
-
- **
|
|
6
|
+
- **Package:** [`@tutorflow/cli`](https://www.npmjs.com/package/@tutorflow/cli) (current version in [`package.json`](package.json))
|
|
7
|
+
- **Source:** [tutorflow-io/cli](https://github.com/tutorflow-io/cli) (private)
|
|
8
|
+
- **Node:** `>= 18` for runtime, `>= 20` for development (Vitest 4 requirement)
|
|
9
|
+
- **Agent-friendly:** every command supports `--json` output, env-var auth (`TUTORFLOW_API_KEY`), and proper exit codes — see [Non-interactive / agent usage](#non-interactive--agent-usage)
|
|
9
10
|
|
|
10
11
|
## Installation
|
|
11
12
|
|
|
12
|
-
### A. End users (
|
|
13
|
+
### A. End users (recommended)
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
Install globally with any package manager:
|
|
15
16
|
|
|
16
17
|
```bash
|
|
17
18
|
npm install -g @tutorflow/cli
|
|
@@ -27,119 +28,211 @@ tutorflow auth login
|
|
|
27
28
|
tutorflow slide create "My first deck"
|
|
28
29
|
```
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
>
|
|
32
|
-
> ```bash
|
|
33
|
-
> npx @tutorflow/cli slide create "My first deck"
|
|
34
|
-
> ```
|
|
35
|
-
|
|
36
|
-
If you publish to a private registry (e.g. GitHub Packages or a self-hosted Verdaccio), users first authenticate to that registry and scope the `@tutorflow` namespace to it:
|
|
31
|
+
**One-shot use without installing**:
|
|
37
32
|
|
|
38
33
|
```bash
|
|
39
|
-
|
|
40
|
-
npm login --scope=@tutorflow --registry=https://npm.pkg.github.com
|
|
41
|
-
npm install -g @tutorflow/cli
|
|
34
|
+
npx @tutorflow/cli slide create "My first deck"
|
|
42
35
|
```
|
|
43
36
|
|
|
44
|
-
|
|
37
|
+
This is the recommended path for AI agents and CI environments — no install step, always pulls the latest published version.
|
|
45
38
|
|
|
46
|
-
|
|
39
|
+
### B. Local development (this repo)
|
|
47
40
|
|
|
48
41
|
```bash
|
|
42
|
+
git clone https://github.com/tutorflow-io/cli.git
|
|
43
|
+
cd cli
|
|
49
44
|
pnpm install
|
|
50
|
-
pnpm
|
|
51
|
-
pnpm
|
|
45
|
+
pnpm build
|
|
46
|
+
pnpm link # exposes the `tutorflow` binary globally
|
|
52
47
|
```
|
|
53
48
|
|
|
54
49
|
Verify:
|
|
55
50
|
|
|
56
51
|
```bash
|
|
57
|
-
tutorflow --version #
|
|
52
|
+
tutorflow --version # matches package.json
|
|
58
53
|
tutorflow --help
|
|
59
54
|
```
|
|
60
55
|
|
|
61
56
|
To unlink later: `npm unlink -g @tutorflow/cli`.
|
|
62
57
|
|
|
63
|
-
|
|
58
|
+
Run from source without building:
|
|
64
59
|
|
|
65
|
-
|
|
60
|
+
```bash
|
|
61
|
+
pnpm dev -- <command> [options]
|
|
62
|
+
# e.g. pnpm dev -- slide list --limit 5
|
|
63
|
+
```
|
|
66
64
|
|
|
67
|
-
|
|
65
|
+
## Releasing
|
|
68
66
|
|
|
69
|
-
|
|
70
|
-
2. Login from the CLI:
|
|
67
|
+
Releases are fully automated via tag-driven GitHub Actions. Auth uses **npm OIDC Trusted Publishing** — no `NPM_TOKEN` secret in CI.
|
|
71
68
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
69
|
+
```bash
|
|
70
|
+
git checkout master
|
|
71
|
+
git pull
|
|
72
|
+
|
|
73
|
+
npm version patch # 0.1.2 → 0.1.3 (or minor / major)
|
|
74
|
+
git push origin master --follow-tags # tag triggers the Release workflow
|
|
75
|
+
```
|
|
77
76
|
|
|
78
|
-
|
|
77
|
+
The [`Release` workflow](.github/workflows/release.yml) runs on tags matching `v*.*.*` and:
|
|
79
78
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
"repository": "https://github.com/<org>/tutorflow-monorepo",
|
|
85
|
-
"license": "UNLICENSED"
|
|
86
|
-
}
|
|
87
|
-
```
|
|
79
|
+
1. Verifies `package.json` version matches the git tag
|
|
80
|
+
2. `pnpm typecheck` + `pnpm build` + `pnpm test`
|
|
81
|
+
3. `npm publish --access public` (auth via OIDC)
|
|
82
|
+
4. Creates a GitHub Release with auto-generated notes
|
|
88
83
|
|
|
89
|
-
|
|
84
|
+
For full setup (npm org, Trusted Publisher config) see [RELEASING.md](RELEASING.md).
|
|
90
85
|
|
|
91
|
-
###
|
|
86
|
+
### Pre-release / beta
|
|
92
87
|
|
|
93
88
|
```bash
|
|
94
|
-
|
|
89
|
+
npm version prerelease --preid=beta # 0.1.3 → 0.1.4-beta.0
|
|
90
|
+
git push origin master --follow-tags
|
|
91
|
+
```
|
|
95
92
|
|
|
96
|
-
|
|
97
|
-
npm version patch # 0.1.0 → 0.1.1
|
|
98
|
-
# or: npm version minor / major / prerelease --preid=beta
|
|
93
|
+
**Note:** the workflow currently publishes everything to `latest`. To route beta tags to a separate dist-tag, see the snippet in [RELEASING.md](RELEASING.md#pre-release-channels).
|
|
99
94
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
95
|
+
## ⚠️ Caveats & gotchas
|
|
96
|
+
|
|
97
|
+
These tripped us up during initial setup. Read before changing release infrastructure.
|
|
98
|
+
|
|
99
|
+
### Versioning
|
|
100
|
+
|
|
101
|
+
- **Don't hardcode the version in `src/index.ts`.** It reads `version` from [`package.json`](package.json) via `resolveJsonModule`. Hardcoding will silently lie to users via `--version` after every `npm version` bump.
|
|
102
|
+
- The `prepublishOnly` script in `package.json` rebuilds `dist/` before publish. Don't skip it.
|
|
103
|
+
|
|
104
|
+
### npm Trusted Publishing (OIDC)
|
|
105
|
+
|
|
106
|
+
- **npm CLI must be 11.5.1+** for OIDC auth. Node 20.x ships with npm 10.x, so the workflow runs `npm install -g npm@latest` before publish. Removing that step will break publishes with a misleading `404 Not Found` (npm hides auth failures as 404s to prevent enumeration).
|
|
107
|
+
- The Trusted Publisher binding at [npmjs.com/package/@tutorflow/cli/access](https://www.npmjs.com/package/@tutorflow/cli/access) hardcodes:
|
|
108
|
+
- GitHub org: `tutorflow-io`
|
|
109
|
+
- Repo: `cli`
|
|
110
|
+
- Workflow filename: `release.yml`
|
|
111
|
+
- Environment: (none)
|
|
112
|
+
|
|
113
|
+
Renaming the repo, moving the workflow file, or adding an `environment:` to the publish job will break OIDC auth until the binding is updated.
|
|
114
|
+
- Local manual publishes still need a token (set `NPM_TOKEN` in `.env` or run `npm login`). Use a Granular Access Token with **"Bypass two-factor authentication (2FA)"** checked, otherwise CI/scripts get `EOTP` errors.
|
|
115
|
+
|
|
116
|
+
### Provenance
|
|
117
|
+
|
|
118
|
+
- `--provenance` is **disabled** in [release.yml](.github/workflows/release.yml). npm only accepts provenance attestations from **public** GitHub repos. Add `--provenance` back to the `npm publish` command when this repo goes public.
|
|
119
|
+
|
|
120
|
+
### Node + tooling
|
|
121
|
+
|
|
122
|
+
- **Vitest 4.x requires Node 20+** (uses `node:util.styleText`). The CI matrix dropped Node 18 for this reason. The runtime CLI itself still supports Node 18 (declared in `package.json#engines`).
|
|
123
|
+
- `actions/setup-node@v4` and friends emit Node 20 deprecation warnings. They'll be force-upgraded to Node 24 on June 2, 2026.
|
|
124
|
+
|
|
125
|
+
### Tarball hygiene
|
|
126
|
+
|
|
127
|
+
- `tsconfig.json` excludes `**/*.test.ts` so test files don't end up in `dist/` (and therefore not in the published tarball).
|
|
128
|
+
- `package.json#files` is an allowlist (`dist`, `README.md`, `LICENSE`). Anything not listed is excluded.
|
|
129
|
+
- Run `npm pack --dry-run` to inspect the tarball before publish.
|
|
130
|
+
|
|
131
|
+
### Source maps and a private repo
|
|
132
|
+
|
|
133
|
+
The published tarball currently includes `.js.map` and `.d.ts.map` files which embed paths from the build environment. While the source itself isn't shipped, this leaks file structure. If that matters, set `sourceMap: false` and `declarationMap: false` in [tsconfig.json](tsconfig.json) before going public.
|
|
134
|
+
|
|
135
|
+
## Development
|
|
103
136
|
|
|
104
|
-
|
|
105
|
-
npm pack --dry-run
|
|
137
|
+
Run with `tsx` (no build step):
|
|
106
138
|
|
|
107
|
-
|
|
108
|
-
|
|
139
|
+
```bash
|
|
140
|
+
pnpm dev -- <command> [options]
|
|
141
|
+
# e.g. pnpm dev -- slide list --limit 5
|
|
109
142
|
```
|
|
110
143
|
|
|
111
|
-
|
|
144
|
+
Build artifacts land in `dist/` (entry: `dist/index.js`, declared in `package.json#bin`).
|
|
145
|
+
|
|
146
|
+
## Non-interactive / agent usage
|
|
147
|
+
|
|
148
|
+
Every command supports the contract below so AI agents and CI scripts can use the CLI without a TTY.
|
|
149
|
+
|
|
150
|
+
### Auth without prompts
|
|
112
151
|
|
|
113
152
|
```bash
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
153
|
+
# Option 1: env var (preferred for one-shot calls)
|
|
154
|
+
TUTORFLOW_API_KEY=tf_xxx tutorflow --json slide list
|
|
155
|
+
|
|
156
|
+
# Option 2: persist to ~/.tutorflow/config.json once
|
|
157
|
+
tutorflow auth login --api-key tf_xxx
|
|
158
|
+
tutorflow --json slide create "Photosynthesis for 5th graders" -n 12
|
|
117
159
|
```
|
|
118
160
|
|
|
119
|
-
|
|
161
|
+
`TUTORFLOW_API_KEY` always wins over the config file. `tutorflow auth whoami --json` reports `source: "env" | "config"` so an agent can verify which credential is active.
|
|
162
|
+
|
|
163
|
+
### Machine-readable output
|
|
120
164
|
|
|
121
|
-
|
|
165
|
+
`--json` (or `TUTORFLOW_JSON=1`) makes every command emit a single JSON line on stdout. Errors emit JSON to stderr.
|
|
122
166
|
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
167
|
+
```bash
|
|
168
|
+
$ tutorflow --json auth whoami
|
|
169
|
+
{"authenticated":true,"apiKeyPreview":"tf_xxxxxxx...","baseUrl":"https://api.tutorflow.io","source":"env"}
|
|
170
|
+
|
|
171
|
+
$ tutorflow --json auth whoami # without auth
|
|
172
|
+
# stdout: (empty)
|
|
173
|
+
# stderr: {"error":{"message":"Not authenticated","code":"EAUTH"}}
|
|
174
|
+
# exit: 1
|
|
129
175
|
```
|
|
130
176
|
|
|
131
|
-
|
|
177
|
+
List commands return raw arrays:
|
|
132
178
|
|
|
133
|
-
|
|
179
|
+
```bash
|
|
180
|
+
$ tutorflow --json slide list --limit 3
|
|
181
|
+
[{"id":"...","title":"...","status":"ready","createdAt":"..."},...]
|
|
182
|
+
```
|
|
134
183
|
|
|
135
|
-
|
|
184
|
+
Pipe into `jq` for downstream automation:
|
|
136
185
|
|
|
137
186
|
```bash
|
|
138
|
-
|
|
139
|
-
# e.g. pnpm --filter @tutorflow/cli dev -- slide list --limit 5
|
|
187
|
+
tutorflow --json slide list --status ready | jq -r '.[].id' | xargs -I{} tutorflow --json slide get {}
|
|
140
188
|
```
|
|
141
189
|
|
|
142
|
-
|
|
190
|
+
### Exit codes
|
|
191
|
+
|
|
192
|
+
| Code | Meaning |
|
|
193
|
+
| ----- | ----------------------------------------------------------------- |
|
|
194
|
+
| `0` | Success |
|
|
195
|
+
| `1` | Generic failure (auth, network, API error, etc.) |
|
|
196
|
+
| `2` | Bad input (invalid flag value, missing required input in non-TTY) |
|
|
197
|
+
| `130` | User cancelled an interactive prompt (Ctrl+C) |
|
|
198
|
+
|
|
199
|
+
The error JSON includes a stable `code`: `EAUTH`, `EINVAL`, `ENOINTERACTIVE`, `ENETWORK`, `EHTTP_<status>`, `ENOPREVIEW`, `ENOTFOUND`, `ECANCELLED`.
|
|
200
|
+
|
|
201
|
+
### Required-input flags (no prompts)
|
|
202
|
+
|
|
203
|
+
Commands that have required input expose every prompt as a flag:
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
# slide / video / course create — accept positional [prompt]
|
|
207
|
+
tutorflow --json slide create "Photosynthesis" -n 12 -l en
|
|
208
|
+
tutorflow --json video create "Newton's laws" -s 6 -r 16:9 -d 90
|
|
209
|
+
tutorflow --json course create "Beginner Python" -l 10 -lang en
|
|
210
|
+
|
|
211
|
+
# evaluate — every input is a flag
|
|
212
|
+
tutorflow --json evaluate \
|
|
213
|
+
--type rubric \
|
|
214
|
+
--question "What is 2+2?" \
|
|
215
|
+
--student-answer "4" \
|
|
216
|
+
--expected-answer "4"
|
|
217
|
+
|
|
218
|
+
# logout (skip confirmation)
|
|
219
|
+
tutorflow auth logout --force
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
If a required input is missing **and** stdin is not a TTY, the command exits 2 with `ENOINTERACTIVE` and an error message naming the flag(s) to use. It never hangs waiting for input.
|
|
223
|
+
|
|
224
|
+
### Globals
|
|
225
|
+
|
|
226
|
+
| Flag | Env var | Effect |
|
|
227
|
+
| --------------------------------- | -------------------------------- | ----------------------------------------------- |
|
|
228
|
+
| `--json` | `TUTORFLOW_JSON=1` | JSON output to stdout, JSON errors to stderr |
|
|
229
|
+
| `--debug` | `DEBUG=1` or `TUTORFLOW_DEBUG=1` | Print stack traces on errors |
|
|
230
|
+
| `--api-key <k>` (auth login only) | `TUTORFLOW_API_KEY` | API key |
|
|
231
|
+
| (n/a) | `TUTORFLOW_BASE_URL` | Override API base URL (staging, self-hosted) |
|
|
232
|
+
|
|
233
|
+
### MCP server
|
|
234
|
+
|
|
235
|
+
A native MCP server (so agents can call tools directly without shelling out) is on the roadmap. For now, the `--json` shell contract is the recommended integration path.
|
|
143
236
|
|
|
144
237
|
## Authentication
|
|
145
238
|
|
|
@@ -403,33 +496,45 @@ class ApiClient {
|
|
|
403
496
|
## Project layout
|
|
404
497
|
|
|
405
498
|
```
|
|
406
|
-
|
|
407
|
-
├── package.json
|
|
499
|
+
.
|
|
500
|
+
├── package.json # bin: { tutorflow: ./dist/index.js }
|
|
408
501
|
├── tsconfig.json
|
|
502
|
+
├── vitest.config.ts
|
|
503
|
+
├── .github/workflows/
|
|
504
|
+
│ ├── ci.yml # typecheck + build + test on PRs and master
|
|
505
|
+
│ └── release.yml # OIDC publish on v*.*.* tags
|
|
506
|
+
├── RELEASING.md # full release procedure + Trusted Publisher setup
|
|
409
507
|
└── src/
|
|
410
|
-
├── index.ts
|
|
411
|
-
├── client.ts
|
|
412
|
-
├──
|
|
508
|
+
├── index.ts # commander entrypoint, registers all commands
|
|
509
|
+
├── client.ts # undici-based ApiClient
|
|
510
|
+
├── client.test.ts
|
|
511
|
+
├── config.ts # ~/.tutorflow/config.json read/write/update/delete
|
|
512
|
+
├── config.test.ts
|
|
513
|
+
├── types.ts # API response interfaces (SlideResource, etc.)
|
|
413
514
|
├── commands/
|
|
414
|
-
│ ├── auth.ts
|
|
415
|
-
│ ├── config.ts
|
|
416
|
-
│ ├── course.ts
|
|
417
|
-
│ ├── evaluate.ts
|
|
418
|
-
│ ├── slide.ts
|
|
419
|
-
│ └── video.ts
|
|
515
|
+
│ ├── auth.ts # login / logout / whoami
|
|
516
|
+
│ ├── config.ts # get / set
|
|
517
|
+
│ ├── course.ts # create / list / open
|
|
518
|
+
│ ├── evaluate.ts # interactive evaluation
|
|
519
|
+
│ ├── slide.ts # create / list / open
|
|
520
|
+
│ └── video.ts # create / list / open
|
|
420
521
|
└── utils/
|
|
421
|
-
├── output.ts
|
|
522
|
+
├── output.ts # formatTable, formatJson
|
|
422
523
|
└── spinner.ts
|
|
423
524
|
```
|
|
424
525
|
|
|
425
526
|
## npm scripts
|
|
426
527
|
|
|
427
|
-
| Script
|
|
428
|
-
|
|
|
429
|
-
| `dev`
|
|
430
|
-
| `build`
|
|
431
|
-
| `link`
|
|
432
|
-
| `
|
|
528
|
+
| Script | Command | Purpose |
|
|
529
|
+
| ---------------- | ----------------------- | --------------------------------------- |
|
|
530
|
+
| `dev` | `tsx src/index.ts` | Run from source |
|
|
531
|
+
| `build` | `tsc --outDir dist` | Emit JS + `.d.ts` to `dist/` |
|
|
532
|
+
| `link` | `npm link` | Symlink `tutorflow` globally |
|
|
533
|
+
| `typecheck` | `tsc --noEmit` | Type-check without emitting |
|
|
534
|
+
| `test` | `vitest run` | Run all tests once |
|
|
535
|
+
| `test:watch` | `vitest` | Watch-mode tests |
|
|
536
|
+
| `test:coverage` | `vitest run --coverage` | Generate coverage report |
|
|
537
|
+
| `prepublishOnly` | `rm -rf dist && build` | Auto-runs before `npm publish` |
|
|
433
538
|
|
|
434
539
|
## Stack
|
|
435
540
|
|
|
@@ -438,4 +543,5 @@ apps/cli/
|
|
|
438
543
|
- **HTTP:** [undici](https://github.com/nodejs/undici) `^5.27.0`
|
|
439
544
|
- **Color:** [chalk](https://github.com/chalk/chalk) `^5.3.0`
|
|
440
545
|
- **Browser open:** [open](https://github.com/sindresorhus/open) `^9.0.0`
|
|
546
|
+
- **Tests:** [Vitest](https://vitest.dev) `^4.1.5`
|
|
441
547
|
- **Dev:** TypeScript `^5.0.0`, tsx `^3.0.0`, `@types/node` `^20.0.0`
|
package/dist/client.d.ts
CHANGED
|
@@ -6,6 +6,12 @@ export declare class ApiClient {
|
|
|
6
6
|
private config;
|
|
7
7
|
constructor(config?: IClientConfig);
|
|
8
8
|
static create(): Promise<ApiClient>;
|
|
9
|
+
/**
|
|
10
|
+
* Like `create` but throws CliError if no API key is resolvable. Use this
|
|
11
|
+
* in any command that talks to the API so we get a single, consistent
|
|
12
|
+
* "not authenticated" path.
|
|
13
|
+
*/
|
|
14
|
+
static createAuthenticated(): Promise<ApiClient>;
|
|
9
15
|
request<T>(method: string, endpoint: string, body?: Record<string, unknown>): Promise<T>;
|
|
10
16
|
setApiKey(apiKey: string): void;
|
|
11
17
|
setBaseUrl(baseUrl: string): void;
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB;AAqBD,qBAAa,SAAS;IACpB,OAAO,CAAC,MAAM,CAAgB;gBAElB,MAAM,CAAC,EAAE,aAAa;WAIrB,MAAM,IAAI,OAAO,CAAC,SAAS,CAAC;IAIzC;;;;OAIG;WACU,mBAAmB,IAAI,OAAO,CAAC,SAAS,CAAC;IAWhD,OAAO,CAAC,CAAC,EACb,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,OAAO,CAAC,CAAC,CAAC;IAuCb,SAAS,CAAC,MAAM,EAAE,MAAM;IAIxB,UAAU,CAAC,OAAO,EAAE,MAAM;CAG3B"}
|
package/dist/client.js
CHANGED
|
@@ -3,37 +3,70 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.ApiClient = void 0;
|
|
4
4
|
const undici_1 = require("undici");
|
|
5
5
|
const config_1 = require("./config");
|
|
6
|
+
const runtime_1 = require("./utils/runtime");
|
|
7
|
+
const DEFAULT_BASE_URL = 'https://api.tutorflow.io';
|
|
8
|
+
/**
|
|
9
|
+
* Resolution order (env wins so CI/agents can override config without
|
|
10
|
+
* touching ~/.tutorflow/config.json):
|
|
11
|
+
*
|
|
12
|
+
* 1. TUTORFLOW_API_KEY / TUTORFLOW_BASE_URL env vars
|
|
13
|
+
* 2. ~/.tutorflow/config.json
|
|
14
|
+
* 3. baseUrl default
|
|
15
|
+
*/
|
|
16
|
+
async function resolveConfig() {
|
|
17
|
+
const fileConfig = await (0, config_1.readConfig)();
|
|
18
|
+
return {
|
|
19
|
+
apiKey: process.env.TUTORFLOW_API_KEY ?? fileConfig.apiKey ?? '',
|
|
20
|
+
baseUrl: process.env.TUTORFLOW_BASE_URL ?? fileConfig.baseUrl ?? DEFAULT_BASE_URL,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
6
23
|
class ApiClient {
|
|
7
24
|
constructor(config) {
|
|
8
|
-
this.config = config || {
|
|
9
|
-
apiKey: '',
|
|
10
|
-
baseUrl: 'https://api.tutorflow.io',
|
|
11
|
-
};
|
|
25
|
+
this.config = config || { apiKey: '', baseUrl: DEFAULT_BASE_URL };
|
|
12
26
|
}
|
|
13
27
|
static async create() {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
28
|
+
return new ApiClient(await resolveConfig());
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Like `create` but throws CliError if no API key is resolvable. Use this
|
|
32
|
+
* in any command that talks to the API so we get a single, consistent
|
|
33
|
+
* "not authenticated" path.
|
|
34
|
+
*/
|
|
35
|
+
static async createAuthenticated() {
|
|
36
|
+
const config = await resolveConfig();
|
|
37
|
+
if (!config.apiKey) {
|
|
38
|
+
throw new runtime_1.CliError('Not authenticated. Run "tutorflow auth login --api-key <key>" or set TUTORFLOW_API_KEY.', { code: 'EAUTH', exitCode: 1 });
|
|
39
|
+
}
|
|
40
|
+
return new ApiClient(config);
|
|
19
41
|
}
|
|
20
42
|
async request(method, endpoint, body) {
|
|
21
43
|
const url = `${this.config.baseUrl}${endpoint}`;
|
|
22
|
-
const headers = {
|
|
23
|
-
'Content-Type': 'application/json',
|
|
24
|
-
'X-API-Key': this.config.apiKey,
|
|
25
|
-
};
|
|
26
44
|
const init = {
|
|
27
45
|
method,
|
|
28
|
-
headers
|
|
46
|
+
headers: {
|
|
47
|
+
'Content-Type': 'application/json',
|
|
48
|
+
'X-API-Key': this.config.apiKey,
|
|
49
|
+
},
|
|
29
50
|
};
|
|
30
|
-
if (body)
|
|
51
|
+
if (body)
|
|
31
52
|
init.body = JSON.stringify(body);
|
|
53
|
+
let response;
|
|
54
|
+
try {
|
|
55
|
+
response = await (0, undici_1.fetch)(url, init);
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
throw new runtime_1.CliError(`Network error reaching ${url}: ${err instanceof Error ? err.message : String(err)}`, { code: 'ENETWORK' });
|
|
59
|
+
}
|
|
60
|
+
const text = await response.text();
|
|
61
|
+
let data = text;
|
|
62
|
+
try {
|
|
63
|
+
data = text.length ? JSON.parse(text) : {};
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// non-JSON response; keep as text
|
|
32
67
|
}
|
|
33
|
-
const response = await (0, undici_1.fetch)(url, init);
|
|
34
|
-
const data = await response.json();
|
|
35
68
|
if (!response.ok) {
|
|
36
|
-
throw new
|
|
69
|
+
throw new runtime_1.CliError(`API error ${response.status}: ${typeof data === 'string' ? data : JSON.stringify(data)}`, { code: `EHTTP_${response.status}` });
|
|
37
70
|
}
|
|
38
71
|
return data;
|
|
39
72
|
}
|
package/dist/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":";;;AAAA,mCAAyC;AACzC,qCAAsC;
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/commands/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/commands/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAUpC,wBAAgB,WAAW,IAAI,OAAO,CAiFrC"}
|
package/dist/commands/auth.js
CHANGED
|
@@ -1,72 +1,75 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
3
|
exports.authCommand = authCommand;
|
|
7
4
|
const commander_1 = require("commander");
|
|
8
5
|
const prompts_1 = require("@clack/prompts");
|
|
9
|
-
const chalk_1 = __importDefault(require("chalk"));
|
|
10
6
|
const config_1 = require("../config");
|
|
7
|
+
const runtime_1 = require("../utils/runtime");
|
|
8
|
+
const output_1 = require("../utils/output");
|
|
11
9
|
function authCommand() {
|
|
12
10
|
const auth = new commander_1.Command('auth').description('Manage authentication');
|
|
13
11
|
auth
|
|
14
12
|
.command('login')
|
|
15
13
|
.description('Authenticate with your API key')
|
|
16
|
-
.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
14
|
+
.option('--api-key <key>', 'API key (overrides interactive prompt; also reads TUTORFLOW_API_KEY)')
|
|
15
|
+
.action((0, runtime_1.command)(async (options) => {
|
|
16
|
+
let apiKey = options.apiKey ?? process.env.TUTORFLOW_API_KEY;
|
|
17
|
+
if (!apiKey) {
|
|
18
|
+
(0, runtime_1.requireInteractive)('Pass --api-key <key> or set TUTORFLOW_API_KEY.');
|
|
19
|
+
const result = await (0, prompts_1.password)({ message: 'Enter your TutorFlow API key:' });
|
|
20
|
+
if (typeof result === 'symbol') {
|
|
21
|
+
throw new runtime_1.CliError('Authentication cancelled', { code: 'ECANCELLED', exitCode: 130 });
|
|
24
22
|
}
|
|
25
|
-
|
|
26
|
-
console.log(chalk_1.default.green('✓ Authentication successful!'));
|
|
27
|
-
console.log(chalk_1.default.gray(`API key saved to ~/.tutorflow/config.json`));
|
|
23
|
+
apiKey = result;
|
|
28
24
|
}
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
if (!apiKey || !apiKey.trim()) {
|
|
26
|
+
throw new runtime_1.CliError('API key is empty', { code: 'EINVAL', exitCode: 2 });
|
|
31
27
|
}
|
|
32
|
-
|
|
28
|
+
await (0, config_1.updateConfig)({ apiKey });
|
|
29
|
+
(0, output_1.printStatus)('Authentication successful', 'success');
|
|
30
|
+
(0, output_1.printResult)({ ok: true, apiKeyPreview: `${apiKey.slice(0, 10)}...` });
|
|
31
|
+
}));
|
|
33
32
|
auth
|
|
34
33
|
.command('logout')
|
|
35
34
|
.description('Remove stored credentials')
|
|
36
|
-
.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
35
|
+
.option('-f, --force', 'Skip confirmation prompt')
|
|
36
|
+
.action((0, runtime_1.command)(async (options) => {
|
|
37
|
+
if (!options.force) {
|
|
38
|
+
// In non-interactive mode require --force so we never block silently.
|
|
39
|
+
if (!process.stdin.isTTY) {
|
|
40
|
+
throw new runtime_1.CliError('Pass --force to logout in non-interactive mode.', {
|
|
41
|
+
code: 'ENOINTERACTIVE',
|
|
42
|
+
exitCode: 2,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
const confirmed = await (0, prompts_1.confirm)({ message: 'Remove stored credentials?' });
|
|
46
|
+
if (!confirmed || typeof confirmed === 'symbol') {
|
|
47
|
+
(0, output_1.printStatus)('Cancelled');
|
|
43
48
|
return;
|
|
44
49
|
}
|
|
45
|
-
await (0, config_1.deleteConfig)();
|
|
46
|
-
console.log(chalk_1.default.green('✓ Credentials removed'));
|
|
47
|
-
}
|
|
48
|
-
catch (error) {
|
|
49
|
-
console.error(chalk_1.default.red('✗ Failed to logout'), error);
|
|
50
50
|
}
|
|
51
|
-
|
|
51
|
+
await (0, config_1.deleteConfig)();
|
|
52
|
+
(0, output_1.printStatus)('Credentials removed', 'success');
|
|
53
|
+
if ((0, runtime_1.isJsonMode)())
|
|
54
|
+
(0, output_1.printResult)({ ok: true });
|
|
55
|
+
}));
|
|
52
56
|
auth
|
|
53
57
|
.command('whoami')
|
|
54
58
|
.description('Show current authenticated workspace')
|
|
55
|
-
.action(async () => {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
console.log(chalk_1.default.green('✓ Authenticated as:'));
|
|
63
|
-
console.log(chalk_1.default.gray(` API Key: ${config.apiKey.slice(0, 10)}...`));
|
|
64
|
-
console.log(chalk_1.default.gray(` Base URL: ${config.baseUrl || 'https://api.tutorflow.io'}`));
|
|
65
|
-
}
|
|
66
|
-
catch (error) {
|
|
67
|
-
console.error(chalk_1.default.red('✗ Error:'), error);
|
|
59
|
+
.action((0, runtime_1.command)(async () => {
|
|
60
|
+
const fileConfig = await (0, config_1.readConfig)();
|
|
61
|
+
const apiKey = process.env.TUTORFLOW_API_KEY ?? fileConfig.apiKey;
|
|
62
|
+
const baseUrl = process.env.TUTORFLOW_BASE_URL ?? fileConfig.baseUrl ?? 'https://api.tutorflow.io';
|
|
63
|
+
if (!apiKey) {
|
|
64
|
+
throw new runtime_1.CliError('Not authenticated', { code: 'EAUTH', exitCode: 1 });
|
|
68
65
|
}
|
|
69
|
-
|
|
66
|
+
(0, output_1.printResult)({
|
|
67
|
+
authenticated: true,
|
|
68
|
+
apiKeyPreview: `${apiKey.slice(0, 10)}...`,
|
|
69
|
+
baseUrl,
|
|
70
|
+
source: process.env.TUTORFLOW_API_KEY ? 'env' : 'config',
|
|
71
|
+
});
|
|
72
|
+
}));
|
|
70
73
|
return auth;
|
|
71
74
|
}
|
|
72
75
|
//# sourceMappingURL=auth.js.map
|