@tutorflow/cli 0.1.2 → 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 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
- - **Version:** `0.1.0`
7
- - **Node:** `>= 18`
8
- - **Package:** `@tutorflow/cli`
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 (other machines)
13
+ ### A. End users (recommended)
13
14
 
14
- Once published to npm, install globally with any package manager:
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
- > **One-shot use without installing** (npm 5.2+):
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
- npm config set @tutorflow:registry https://npm.pkg.github.com
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
- ### B. Local development (this monorepo)
37
+ This is the recommended path for AI agents and CI environments — no install step, always pulls the latest published version.
45
38
 
46
- From the monorepo root:
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 --filter @tutorflow/cli build # tsc → ./dist
51
- pnpm --filter @tutorflow/cli link # npm link → exposes `tutorflow` globally
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 # 0.1.0
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
- ## Publishing
58
+ Run from source without building:
64
59
 
65
- The package is **scoped** (`@tutorflow/...`), so the first publish needs `--access public` (or a paid org plan for private packages).
60
+ ```bash
61
+ pnpm dev -- <command> [options]
62
+ # e.g. pnpm dev -- slide list --limit 5
63
+ ```
66
64
 
67
- ### One-time setup
65
+ ## Releasing
68
66
 
69
- 1. Make sure you're a member of the `@tutorflow` npm org with publish rights.
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
- ```bash
73
- npm login # public registry
74
- # or:
75
- npm login --scope=@tutorflow --registry=<your-private-registry>
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
- 3. Confirm `apps/cli/package.json` has the right metadata. Recommended fields to add before the first release:
77
+ The [`Release` workflow](.github/workflows/release.yml) runs on tags matching `v*.*.*` and:
79
78
 
80
- ```json
81
- {
82
- "files": ["dist", "README.md"],
83
- "publishConfig": { "access": "public" },
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
- The `files` allowlist keeps `src/`, `node_modules/`, and `tsconfig.json` out of the tarball.
84
+ For full setup (npm org, Trusted Publisher config) see [RELEASING.md](RELEASING.md).
90
85
 
91
- ### Release flow
86
+ ### Pre-release / beta
92
87
 
93
88
  ```bash
94
- cd apps/cli
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
- # 1. bump version (creates a git tag)
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
- # 2. clean build
101
- rm -rf dist
102
- pnpm build
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
- # 3. (optional) inspect what will be published
105
- npm pack --dry-run
137
+ Run with `tsx` (no build step):
106
138
 
107
- # 4. publish
108
- npm publish --access public
139
+ ```bash
140
+ pnpm dev -- <command> [options]
141
+ # e.g. pnpm dev -- slide list --limit 5
109
142
  ```
110
143
 
111
- For pre-release channels (so end users don't auto-pull untested versions):
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
- npm version prerelease --preid=beta # 0.1.1 → 0.1.2-beta.0
115
- npm publish --tag beta --access public
116
- # users install with: npm install -g @tutorflow/cli@beta
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
- ### CI publishing (sketch)
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
- In GitHub Actions, after `pnpm build`:
165
+ `--json` (or `TUTORFLOW_JSON=1`) makes every command emit a single JSON line on stdout. Errors emit JSON to stderr.
122
166
 
123
- ```yaml
124
- - run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc
125
- - run: npm publish --access public
126
- working-directory: apps/cli
127
- env:
128
- NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
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
- Trigger on tags matching `cli-v*` or use `changesets` for monorepo-aware versioning.
177
+ List commands return raw arrays:
132
178
 
133
- ## Development
179
+ ```bash
180
+ $ tutorflow --json slide list --limit 3
181
+ [{"id":"...","title":"...","status":"ready","createdAt":"..."},...]
182
+ ```
134
183
 
135
- Run with `tsx` (no build step):
184
+ Pipe into `jq` for downstream automation:
136
185
 
137
186
  ```bash
138
- pnpm --filter @tutorflow/cli dev -- <command> [options]
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
- Build artifacts land in `apps/cli/dist/` (entry: `dist/index.js`, declared in `package.json#bin`).
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
- apps/cli/
407
- ├── package.json # bin: { tutorflow: ./dist/index.js }
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 # commander entrypoint, registers all commands
411
- ├── client.ts # undici-based ApiClient
412
- ├── config.ts # ~/.tutorflow/config.json read/write/update/delete
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 # login / logout / whoami
415
- │ ├── config.ts # get / set
416
- │ ├── course.ts # create / list / open
417
- │ ├── evaluate.ts # interactive evaluation
418
- │ ├── slide.ts # create / list / open
419
- │ └── video.ts # create / list / open
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 # formatTable, formatJson
522
+ ├── output.ts # formatTable, formatJson
422
523
  └── spinner.ts
423
524
  ```
424
525
 
425
526
  ## npm scripts
426
527
 
427
- | Script | Command | Purpose |
428
- | ------- | ------------------- | ---------------------------- |
429
- | `dev` | `tsx src/index.ts` | Run from source |
430
- | `build` | `tsc --outDir dist` | Emit JS + `.d.ts` to `dist/` |
431
- | `link` | `npm link` | Symlink `tutorflow` globally |
432
- | `test` | `jest` | (No tests committed yet) |
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;
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,SAAS;IACpB,OAAO,CAAC,MAAM,CAAgB;gBAElB,MAAM,CAAC,EAAE,aAAa;WAOrB,MAAM,IAAI,OAAO,CAAC,SAAS,CAAC;IAQnC,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;IA0Bb,SAAS,CAAC,MAAM,EAAE,MAAM;IAIxB,UAAU,CAAC,OAAO,EAAE,MAAM;CAG3B"}
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
- const config = await (0, config_1.readConfig)();
15
- return new ApiClient({
16
- apiKey: config.apiKey || '',
17
- baseUrl: config.baseUrl || 'https://api.tutorflow.io',
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 Error(`API Error: ${response.status} ${JSON.stringify(data)}`);
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
  }
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":";;;AAAA,mCAAyC;AACzC,qCAAsC;AAOtC,MAAa,SAAS;IAGpB,YAAY,MAAsB;QAChC,IAAI,CAAC,MAAM,GAAG,MAAM,IAAI;YACtB,MAAM,EAAE,EAAE;YACV,OAAO,EAAE,0BAA0B;SACpC,CAAC;IACJ,CAAC;IAED,MAAM,CAAC,KAAK,CAAC,MAAM;QACjB,MAAM,MAAM,GAAG,MAAM,IAAA,mBAAU,GAAE,CAAC;QAClC,OAAO,IAAI,SAAS,CAAC;YACnB,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,EAAE;YAC3B,OAAO,EAAE,MAAM,CAAC,OAAO,IAAI,0BAA0B;SACtD,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,OAAO,CACX,MAAc,EACd,QAAgB,EAChB,IAA8B;QAE9B,MAAM,GAAG,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,OAAO,GAAG,QAAQ,EAAE,CAAC;QAChD,MAAM,OAAO,GAA2B;YACtC,cAAc,EAAE,kBAAkB;YAClC,WAAW,EAAE,IAAI,CAAC,MAAM,CAAC,MAAM;SAChC,CAAC;QAEF,MAAM,IAAI,GAAuE;YAC/E,MAAM;YACN,OAAO;SACR,CAAC;QAEF,IAAI,IAAI,EAAE,CAAC;YACT,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;QACnC,CAAC;QAED,MAAM,QAAQ,GAAG,MAAM,IAAA,cAAM,EAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QACzC,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;QAEnC,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,MAAM,IAAI,KAAK,CAAC,cAAc,QAAQ,CAAC,MAAM,IAAI,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC3E,CAAC;QAED,OAAO,IAAS,CAAC;IACnB,CAAC;IAED,SAAS,CAAC,MAAc;QACtB,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,MAAM,CAAC;IAC9B,CAAC;IAED,UAAU,CAAC,OAAe;QACxB,IAAI,CAAC,MAAM,CAAC,OAAO,GAAG,OAAO,CAAC;IAChC,CAAC;CACF;AAvDD,8BAuDC"}
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;AAKpC,wBAAgB,WAAW,IAAI,OAAO,CAoErC"}
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"}
@@ -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
- .action(async () => {
17
- try {
18
- const apiKey = await (0, prompts_1.password)({
19
- message: 'Enter your TutorFlow API key:',
20
- });
21
- if (typeof apiKey === 'symbol') {
22
- console.log(chalk_1.default.red('Authentication cancelled'));
23
- return;
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
- await (0, config_1.updateConfig)({ apiKey });
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
- catch (error) {
30
- console.error(chalk_1.default.red('✗ Authentication failed'), error);
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
- .action(async () => {
37
- try {
38
- const confirmed = await (0, prompts_1.confirm)({
39
- message: 'Remove stored credentials?',
40
- });
41
- if (!confirmed) {
42
- console.log(chalk_1.default.gray('Cancelled'));
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
- try {
57
- const config = await (0, config_1.readConfig)();
58
- if (!config.apiKey) {
59
- console.log(chalk_1.default.yellow('Not authenticated'));
60
- return;
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