@kittl/cli 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +129 -0
- package/bin/bootstrap.js +16 -0
- package/bin/run.cmd +3 -0
- package/bin/run.js +6 -0
- package/dist/chunk-4ISWSLZ5.js +29 -0
- package/dist/chunk-4ISWSLZ5.js.map +1 -0
- package/dist/chunk-JGD3QFQS.js +529 -0
- package/dist/chunk-JGD3QFQS.js.map +1 -0
- package/dist/commands/app/create.js +16 -0
- package/dist/commands/app/create.js.map +1 -0
- package/dist/commands/app/upload.js +16 -0
- package/dist/commands/app/upload.js.map +1 -0
- package/dist/commands/auth/login.js +186 -0
- package/dist/commands/auth/login.js.map +1 -0
- package/dist/commands/auth/logout.js +18 -0
- package/dist/commands/auth/logout.js.map +1 -0
- package/dist/commands/auth/whoami.js +8 -0
- package/dist/commands/auth/whoami.js.map +1 -0
- package/dist/commands/whoami.js +8 -0
- package/dist/commands/whoami.js.map +1 -0
- package/package.json +60 -0
package/README.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Kittl CLI
|
|
2
|
+
|
|
3
|
+
Official command-line interface for Kittl.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
From repo root:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm install
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Build
|
|
14
|
+
|
|
15
|
+
From repo root:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pnpm --filter ./packages/cli build
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Or from `packages/cli`:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pnpm build
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Use locally
|
|
28
|
+
|
|
29
|
+
From `packages/cli` after build:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
node bin/run.js --help
|
|
33
|
+
node bin/run.js app --help
|
|
34
|
+
node bin/run.js auth login
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Subcommands are space-separated (for example `app init`, `app upload`, `auth login`). `whoami` is available as `auth whoami` or as a top-level shortcut. Command names, flags, and descriptions live in the tool: run `kittl --help`, then `kittl <topic> --help`. This README only covers install, build, and dev workflow, not a duplicate of the help text.
|
|
38
|
+
|
|
39
|
+
Note: run **`pnpm build`** before using `bin/run.js`.
|
|
40
|
+
|
|
41
|
+
Using pnpm exec from repo root:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pnpm --filter ./packages/cli exec kittl --help
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Published usage
|
|
48
|
+
|
|
49
|
+
After publishing:
|
|
50
|
+
|
|
51
|
+
- package name is `@kittl/cli`
|
|
52
|
+
- executable command is `kittl`
|
|
53
|
+
|
|
54
|
+
Examples:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# Global install
|
|
58
|
+
npm i -g @kittl/cli
|
|
59
|
+
kittl --help
|
|
60
|
+
kittl app --help
|
|
61
|
+
|
|
62
|
+
# Without global install
|
|
63
|
+
npx @kittl/cli --help
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Most commands that call the API expect you to be signed in (`kittl auth login`).
|
|
67
|
+
|
|
68
|
+
## Environment
|
|
69
|
+
|
|
70
|
+
Copy and edit:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
cp .env.example .env
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Supported vars are documented in `.env.example`.
|
|
77
|
+
They are optional overrides for local development and testing.
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
## Developer docs
|
|
81
|
+
|
|
82
|
+
Internal architecture and implementation notes live in `docs/internals.md`.
|
|
83
|
+
|
|
84
|
+
## Local development scripts
|
|
85
|
+
|
|
86
|
+
Run from `packages/cli`:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
# Run CLI once in development mode (no build required)
|
|
90
|
+
pnpm dev app --help
|
|
91
|
+
|
|
92
|
+
# Re-run the CLI when source changes
|
|
93
|
+
pnpm dev:watch app --help
|
|
94
|
+
|
|
95
|
+
# Watch + rebuild dist and regenerate oclif manifest
|
|
96
|
+
pnpm build:watch
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Global CLI on your PATH (linked)
|
|
100
|
+
|
|
101
|
+
From `packages/cli` (after `pnpm install` at the repo root):
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
cd packages/cli
|
|
105
|
+
pnpm link --global
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
This puts **`kittl`** and **`kittl-dev`** on your global `PATH`, pointing at your local clone:
|
|
109
|
+
|
|
110
|
+
| Command | When to use it |
|
|
111
|
+
| ------------- | -------------- |
|
|
112
|
+
| `kittl` | Production entry. Run **`pnpm build`** first so `dist/` is up to date. |
|
|
113
|
+
| `kittl-dev` | Dev entry (`bin/dev.js` + `tsx`), same as **`pnpm dev`**, no build required. |
|
|
114
|
+
|
|
115
|
+
To remove the global install later:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
pnpm uninstall --global @kittl/cli
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The npm package only ships the **`kittl`** binary; **`kittl-dev`** exists in this repo for contributors (`publishConfig` strips it from the published manifest).
|
|
122
|
+
|
|
123
|
+
## Debug logging
|
|
124
|
+
|
|
125
|
+
Enable debug output with `DEBUG`:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
DEBUG=oclif:kittl:* kittl app --help #TODO: need to get rid of oclif prefix here
|
|
129
|
+
```
|
package/bin/bootstrap.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ensure Ctrl+C / SIGTERM exit the process, and uncaught exceptions print a stack then exit
|
|
3
|
+
*/
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import process from 'node:process';
|
|
6
|
+
|
|
7
|
+
process.on('uncaughtException', (err) => {
|
|
8
|
+
fs.writeSync(process.stderr.fd, `${err.stack ?? err.message ?? err}\n`);
|
|
9
|
+
process.exit(1);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
for (const sig of ['SIGINT', 'SIGTERM', 'SIGQUIT']) {
|
|
13
|
+
process.on(sig, () => {
|
|
14
|
+
process.exit(1);
|
|
15
|
+
});
|
|
16
|
+
}
|
package/bin/run.cmd
ADDED
package/bin/run.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BaseCommand
|
|
3
|
+
} from "./chunk-JGD3QFQS.js";
|
|
4
|
+
|
|
5
|
+
// src/commands/auth/whoami.ts
|
|
6
|
+
import { jwtDecode } from "jwt-decode";
|
|
7
|
+
var AuthWhoAmI = class _AuthWhoAmI extends BaseCommand {
|
|
8
|
+
static description = "Show authenticated Kittl account details";
|
|
9
|
+
async run() {
|
|
10
|
+
await this.parse(_AuthWhoAmI);
|
|
11
|
+
const session = await this.ensureAuthenticated();
|
|
12
|
+
if (!session.idToken) {
|
|
13
|
+
this.error(
|
|
14
|
+
`No ID token found in session. Run \`${this.config.bin} auth:login\` again.`,
|
|
15
|
+
{ exit: 2 }
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
const claims = jwtDecode(session.idToken);
|
|
19
|
+
const email = claims.email ?? "unknown";
|
|
20
|
+
const name = claims.name ?? claims.preferred_username ?? claims.sub ?? "unknown";
|
|
21
|
+
this.log(`Name: ${name}`);
|
|
22
|
+
this.log(`Email: ${email}`);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export {
|
|
27
|
+
AuthWhoAmI
|
|
28
|
+
};
|
|
29
|
+
//# sourceMappingURL=chunk-4ISWSLZ5.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/auth/whoami.ts"],"sourcesContent":["import { jwtDecode } from 'jwt-decode';\nimport type { IDToken } from 'openid-client';\nimport { BaseCommand } from '../../base-command';\n\nexport default class AuthWhoAmI extends BaseCommand {\n public static override description =\n 'Show authenticated Kittl account details';\n\n public async run(): Promise<void> {\n await this.parse(AuthWhoAmI);\n\n const session = await this.ensureAuthenticated();\n\n if (!session.idToken) {\n this.error(\n `No ID token found in session. Run \\`${this.config.bin} auth:login\\` again.`,\n { exit: 2 },\n );\n }\n\n const claims = jwtDecode<IDToken>(session.idToken);\n const email = claims.email ?? 'unknown';\n const name =\n claims.name ?? claims.preferred_username ?? claims.sub ?? 'unknown';\n\n this.log(`Name: ${name}`);\n this.log(`Email: ${email}`);\n }\n}\n"],"mappings":";;;;;AAAA,SAAS,iBAAiB;AAI1B,IAAqB,aAArB,MAAqB,oBAAmB,YAAY;AAAA,EAClD,OAAuB,cACrB;AAAA,EAEF,MAAa,MAAqB;AAChC,UAAM,KAAK,MAAM,WAAU;AAE3B,UAAM,UAAU,MAAM,KAAK,oBAAoB;AAE/C,QAAI,CAAC,QAAQ,SAAS;AACpB,WAAK;AAAA,QACH,uCAAuC,KAAK,OAAO,GAAG;AAAA,QACtD,EAAE,MAAM,EAAE;AAAA,MACZ;AAAA,IACF;AAEA,UAAM,SAAS,UAAmB,QAAQ,OAAO;AACjD,UAAM,QAAQ,OAAO,SAAS;AAC9B,UAAM,OACJ,OAAO,QAAQ,OAAO,sBAAsB,OAAO,OAAO;AAE5D,SAAK,IAAI,SAAS,IAAI,EAAE;AACxB,SAAK,IAAI,UAAU,KAAK,EAAE;AAAA,EAC5B;AACF;","names":[]}
|
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
// src/services/auth.service.ts
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
import { URL } from "node:url";
|
|
4
|
+
import open from "open";
|
|
5
|
+
import * as oidc from "openid-client";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
|
|
8
|
+
// package.json
|
|
9
|
+
var package_default = {
|
|
10
|
+
name: "@kittl/cli",
|
|
11
|
+
version: "0.0.1",
|
|
12
|
+
private: true,
|
|
13
|
+
type: "module",
|
|
14
|
+
bin: {
|
|
15
|
+
kittl: "./bin/run.js",
|
|
16
|
+
"kittl-dev": "./bin/dev.js"
|
|
17
|
+
},
|
|
18
|
+
publishConfig: {
|
|
19
|
+
bin: {
|
|
20
|
+
kittl: "./bin/run.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
files: [
|
|
24
|
+
"bin/bootstrap.js",
|
|
25
|
+
"bin/run.cmd",
|
|
26
|
+
"bin/run.js",
|
|
27
|
+
"dist"
|
|
28
|
+
],
|
|
29
|
+
scripts: {
|
|
30
|
+
build: "tsup && oclif manifest",
|
|
31
|
+
"build:watch": 'tsup --watch --onSuccess "oclif manifest"',
|
|
32
|
+
dev: "node ./bin/dev.js",
|
|
33
|
+
"dev:watch": "node --watch --watch-path=./src --watch-path=./bin ./bin/dev.js",
|
|
34
|
+
typecheck: "tsc --noEmit",
|
|
35
|
+
test: "vitest run",
|
|
36
|
+
"test:watch": "vitest"
|
|
37
|
+
},
|
|
38
|
+
dependencies: {
|
|
39
|
+
"@oclif/core": "^4.10.2",
|
|
40
|
+
axios: "^1.13.6",
|
|
41
|
+
"cross-keychain": "^1.1.0",
|
|
42
|
+
ink: "^6.8.0",
|
|
43
|
+
"jwt-decode": "^4.0.0",
|
|
44
|
+
open: "^11.0.0",
|
|
45
|
+
"openid-client": "^6.8.2",
|
|
46
|
+
react: "catalog:",
|
|
47
|
+
zod: "catalog:"
|
|
48
|
+
},
|
|
49
|
+
devDependencies: {
|
|
50
|
+
"@types/node": "^25.5.0",
|
|
51
|
+
"@types/react": "catalog:",
|
|
52
|
+
"ink-testing-library": "^4.0.0",
|
|
53
|
+
oclif: "^4.22.96",
|
|
54
|
+
tinyglobby: "^0.2.15",
|
|
55
|
+
tsup: "catalog:",
|
|
56
|
+
tsx: "catalog:",
|
|
57
|
+
typescript: "catalog:",
|
|
58
|
+
vitest: "^4.1.1"
|
|
59
|
+
},
|
|
60
|
+
oclif: {
|
|
61
|
+
bin: "kittl",
|
|
62
|
+
commands: "./dist/commands",
|
|
63
|
+
dirname: "kittl",
|
|
64
|
+
topicSeparator: " "
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// src/utils.ts
|
|
69
|
+
var INK_VIEW_UNMOUNT_REASON = Symbol("INK_VIEW_UNMOUNT_REASON");
|
|
70
|
+
function parseRedirectPortFromEnv(defaultPort) {
|
|
71
|
+
const raw = process.env.KITTL_REDIRECT_PORT ?? String(defaultPort);
|
|
72
|
+
const n = Number(raw);
|
|
73
|
+
if (!Number.isInteger(n) || n < 1 || n > 65535) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`KITTL_REDIRECT_PORT must be an integer between 1 and 65535 (got ${JSON.stringify(raw)}).`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return n;
|
|
79
|
+
}
|
|
80
|
+
function localhostOAuthRedirectUri(port) {
|
|
81
|
+
return `http://localhost:${port}/callback`;
|
|
82
|
+
}
|
|
83
|
+
function getKittlEnvEntries() {
|
|
84
|
+
return Object.entries(process.env).filter(([key]) => key.startsWith("KITTL_")).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => `${key}=${value ?? ""}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/constants.ts
|
|
88
|
+
var { version } = package_default;
|
|
89
|
+
var PRODUCTION = {
|
|
90
|
+
issuer: "https://auth.kittl.com/realms/kittl",
|
|
91
|
+
//TODO: TBD
|
|
92
|
+
apiBaseUrl: "https://api.kittl.com",
|
|
93
|
+
clientId: "kittl-cli",
|
|
94
|
+
redirectPort: 51771
|
|
95
|
+
};
|
|
96
|
+
var AUTH_CONFIG = {
|
|
97
|
+
issuer: process.env.KITTL_ISSUER_URL ?? PRODUCTION.issuer,
|
|
98
|
+
clientId: process.env.KITTL_CLIENT_ID ?? PRODUCTION.clientId,
|
|
99
|
+
redirectPort: parseRedirectPortFromEnv(PRODUCTION.redirectPort),
|
|
100
|
+
redirectUri: localhostOAuthRedirectUri(
|
|
101
|
+
parseRedirectPortFromEnv(PRODUCTION.redirectPort)
|
|
102
|
+
),
|
|
103
|
+
scope: process.env.KITTL_AUTH_SCOPE ?? "openid profile email offline_access",
|
|
104
|
+
oauthCallbackTimeoutMs: Number(
|
|
105
|
+
process.env.KITTL_OAUTH_CALLBACK_TIMEOUT_MS ?? 12e4
|
|
106
|
+
),
|
|
107
|
+
/** Optional 302 after OAuth; if undefined it renders a fallback HTML. */
|
|
108
|
+
oauthSuccessRedirectUrl: process.env.KITTL_OAUTH_SUCCESS_REDIRECT_URL || void 0,
|
|
109
|
+
serviceName: "kittl-cli",
|
|
110
|
+
accountName: "oauth-session"
|
|
111
|
+
};
|
|
112
|
+
var API_CONFIG = {
|
|
113
|
+
baseUrl: process.env.KITTL_API_BASE_URL ?? PRODUCTION.apiBaseUrl,
|
|
114
|
+
timeoutMs: 3e4,
|
|
115
|
+
userAgent: `kittl-cli/${version}`
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// src/services/session-vault.service.ts
|
|
119
|
+
import { deletePassword, getPassword, setPassword } from "cross-keychain";
|
|
120
|
+
var SessionVault = class {
|
|
121
|
+
keychainService = AUTH_CONFIG.serviceName;
|
|
122
|
+
keychainAccount = AUTH_CONFIG.accountName;
|
|
123
|
+
async getSession() {
|
|
124
|
+
const raw = await getPassword(this.keychainService, this.keychainAccount);
|
|
125
|
+
if (!raw)
|
|
126
|
+
return null;
|
|
127
|
+
try {
|
|
128
|
+
return JSON.parse(raw);
|
|
129
|
+
} catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async saveSession(session) {
|
|
134
|
+
await setPassword(
|
|
135
|
+
this.keychainService,
|
|
136
|
+
this.keychainAccount,
|
|
137
|
+
JSON.stringify(session)
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
async clear() {
|
|
141
|
+
await deletePassword(this.keychainService, this.keychainAccount);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
var sessionVault = new SessionVault();
|
|
145
|
+
|
|
146
|
+
// src/services/auth.service.ts
|
|
147
|
+
var ACCESS_TOKEN_REFRESH_BUFFER_SEC = 60;
|
|
148
|
+
var OAUTH2_TOKEN_ERROR_INVALID_GRANT = "invalid_grant";
|
|
149
|
+
var authConfigSchema = z.object({
|
|
150
|
+
issuer: z.url(),
|
|
151
|
+
clientId: z.string().min(1),
|
|
152
|
+
redirectUri: z.url(),
|
|
153
|
+
redirectPort: z.number().int().min(1).max(65535),
|
|
154
|
+
scope: z.string().min(1),
|
|
155
|
+
oauthCallbackTimeoutMs: z.number().int().min(5e3).max(36e5),
|
|
156
|
+
oauthSuccessRedirectUrl: z.url().optional()
|
|
157
|
+
});
|
|
158
|
+
var LoginCancelledError = class extends Error {
|
|
159
|
+
constructor() {
|
|
160
|
+
super("Login cancelled.");
|
|
161
|
+
this.name = "LoginCancelledError";
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
var AuthService = class {
|
|
165
|
+
config = authConfigSchema.parse(AUTH_CONFIG);
|
|
166
|
+
// concurrent refresh attempts are merged into a single promise
|
|
167
|
+
refreshSessionPromise = null;
|
|
168
|
+
/**
|
|
169
|
+
* raw vault read.
|
|
170
|
+
*/
|
|
171
|
+
async getStoredSession() {
|
|
172
|
+
return sessionVault.getSession();
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* ensures token freshness (by silently refresh if needed)
|
|
176
|
+
*/
|
|
177
|
+
async getSession() {
|
|
178
|
+
await this.getAccessToken();
|
|
179
|
+
return this.getStoredSession();
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Returns a usable access token, silently refreshing when `expiresAt` is within {@link ACCESS_TOKEN_REFRESH_BUFFER_SEC}
|
|
183
|
+
* seconds (or in the past). based on Session.expiresAt.
|
|
184
|
+
*/
|
|
185
|
+
async getAccessToken() {
|
|
186
|
+
const session = await this.getStoredSession();
|
|
187
|
+
if (!session?.accessToken)
|
|
188
|
+
return void 0;
|
|
189
|
+
if (!this.shouldRefreshAccessToken(session)) {
|
|
190
|
+
return session.accessToken;
|
|
191
|
+
}
|
|
192
|
+
if (!session.refreshToken) {
|
|
193
|
+
await sessionVault.clear();
|
|
194
|
+
return void 0;
|
|
195
|
+
}
|
|
196
|
+
const refreshed = await this.refreshSession();
|
|
197
|
+
return refreshed?.accessToken;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Refresh tokens via the OIDC token endpoint. On hard failure (e.g. `invalid_grant`), clears the vault.
|
|
201
|
+
*/
|
|
202
|
+
async refreshSession() {
|
|
203
|
+
if (this.refreshSessionPromise) {
|
|
204
|
+
return this.refreshSessionPromise;
|
|
205
|
+
}
|
|
206
|
+
this.refreshSessionPromise = this.performRefreshSession().finally(() => {
|
|
207
|
+
this.refreshSessionPromise = null;
|
|
208
|
+
});
|
|
209
|
+
return this.refreshSessionPromise;
|
|
210
|
+
}
|
|
211
|
+
shouldRefreshAccessToken(session) {
|
|
212
|
+
if (session.expiresAt === void 0)
|
|
213
|
+
return false;
|
|
214
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
215
|
+
return session.expiresAt <= now + ACCESS_TOKEN_REFRESH_BUFFER_SEC;
|
|
216
|
+
}
|
|
217
|
+
async performRefreshSession() {
|
|
218
|
+
const session = await this.getStoredSession();
|
|
219
|
+
if (!session?.refreshToken) {
|
|
220
|
+
await sessionVault.clear();
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
const oidcConfig = await this.discoverOidcConfiguration();
|
|
225
|
+
const tokenSet = await oidc.refreshTokenGrant(
|
|
226
|
+
oidcConfig,
|
|
227
|
+
session.refreshToken,
|
|
228
|
+
{ scope: this.config.scope },
|
|
229
|
+
void 0
|
|
230
|
+
);
|
|
231
|
+
const newSession = this.mapTokenSetToSession(tokenSet, session);
|
|
232
|
+
await sessionVault.saveSession(newSession);
|
|
233
|
+
return newSession;
|
|
234
|
+
} catch (error) {
|
|
235
|
+
if (error instanceof oidc.ResponseBodyError && error.error === OAUTH2_TOKEN_ERROR_INVALID_GRANT) {
|
|
236
|
+
await sessionVault.clear();
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
async discoverOidcConfiguration() {
|
|
243
|
+
const issuerUrl = new URL(this.config.issuer);
|
|
244
|
+
return oidc.discovery(
|
|
245
|
+
issuerUrl,
|
|
246
|
+
this.config.clientId,
|
|
247
|
+
void 0,
|
|
248
|
+
void 0,
|
|
249
|
+
{
|
|
250
|
+
...issuerUrl.protocol === "http:" ? { execute: [oidc.allowInsecureRequests] } : {}
|
|
251
|
+
}
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
async login(options) {
|
|
255
|
+
const { signal } = options ?? {};
|
|
256
|
+
const oidcConfig = await this.discoverOidcConfiguration();
|
|
257
|
+
signal?.throwIfAborted();
|
|
258
|
+
const codeVerifier = oidc.randomPKCECodeVerifier();
|
|
259
|
+
const codeChallenge = await oidc.calculatePKCECodeChallenge(codeVerifier);
|
|
260
|
+
const state = oidc.randomState();
|
|
261
|
+
const nonce = oidc.randomNonce();
|
|
262
|
+
const authorizationUrl = oidc.buildAuthorizationUrl(oidcConfig, {
|
|
263
|
+
redirect_uri: this.config.redirectUri,
|
|
264
|
+
response_type: "code",
|
|
265
|
+
scope: this.config.scope,
|
|
266
|
+
code_challenge: codeChallenge,
|
|
267
|
+
code_challenge_method: "S256",
|
|
268
|
+
state,
|
|
269
|
+
nonce
|
|
270
|
+
});
|
|
271
|
+
signal?.throwIfAborted();
|
|
272
|
+
await this.openBrowser(authorizationUrl.toString());
|
|
273
|
+
const callback = await this.waitForCallback(signal);
|
|
274
|
+
const tokenSet = await oidc.authorizationCodeGrant(
|
|
275
|
+
oidcConfig,
|
|
276
|
+
callback.callbackUrl,
|
|
277
|
+
{
|
|
278
|
+
pkceCodeVerifier: codeVerifier,
|
|
279
|
+
expectedState: state,
|
|
280
|
+
expectedNonce: nonce
|
|
281
|
+
}
|
|
282
|
+
);
|
|
283
|
+
const session = this.mapTokenSetToSession(tokenSet);
|
|
284
|
+
await sessionVault.saveSession(session);
|
|
285
|
+
return session;
|
|
286
|
+
}
|
|
287
|
+
async logout() {
|
|
288
|
+
await sessionVault.clear();
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Starts a short-lived `http` server on the host/port from the configured `redirectUri` so the IdP can
|
|
292
|
+
* redirect the browser to `…/callback?code=…&state=…` (Authorization Code + PKCE). The first matching request
|
|
293
|
+
* stops the server and resolves with that URL for the token exchange (`authorizationCodeGrant`).
|
|
294
|
+
*
|
|
295
|
+
* - **Success response:** HTTP 302 to {@link AUTH_CONFIG.oauthSuccessRedirectUrl} when set; otherwise a minimal inline HTML page.
|
|
296
|
+
*/
|
|
297
|
+
async waitForCallback(signal) {
|
|
298
|
+
const redirectUrl = new URL(this.config.redirectUri);
|
|
299
|
+
const hostname = redirectUrl.hostname;
|
|
300
|
+
const port = Number(redirectUrl.port);
|
|
301
|
+
const pathname = redirectUrl.pathname;
|
|
302
|
+
const timeoutSignal = AbortSignal.timeout(
|
|
303
|
+
this.config.oauthCallbackTimeoutMs
|
|
304
|
+
);
|
|
305
|
+
const combinedSignal = signal !== void 0 ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
306
|
+
return new Promise((resolve, reject) => {
|
|
307
|
+
let closed = false;
|
|
308
|
+
const server = createServer((req, res) => {
|
|
309
|
+
try {
|
|
310
|
+
const requestUrl = new URL(req.url ?? "", this.config.redirectUri);
|
|
311
|
+
if (requestUrl.pathname !== pathname) {
|
|
312
|
+
res.statusCode = 404;
|
|
313
|
+
res.end("Not Found");
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
const callbackUrl = new URL(this.config.redirectUri);
|
|
317
|
+
callbackUrl.search = requestUrl.search;
|
|
318
|
+
const brandUrl = this.config.oauthSuccessRedirectUrl;
|
|
319
|
+
if (brandUrl) {
|
|
320
|
+
res.writeHead(302, { Location: brandUrl });
|
|
321
|
+
res.end();
|
|
322
|
+
} else {
|
|
323
|
+
res.statusCode = 200;
|
|
324
|
+
res.setHeader("content-type", "text/html; charset=utf-8");
|
|
325
|
+
res.end(
|
|
326
|
+
"<!doctype html><html><body><h2>Authentication complete.</h2><p>You can close this tab and return to the terminal.</p></body></html>"
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
closeServer((closeErr) => {
|
|
330
|
+
if (closeErr) {
|
|
331
|
+
reject(closeErr);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
resolve({ callbackUrl });
|
|
335
|
+
});
|
|
336
|
+
} catch (error) {
|
|
337
|
+
closeServer(() => reject(error));
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
const closeServer = (onClosed) => {
|
|
341
|
+
if (closed)
|
|
342
|
+
return;
|
|
343
|
+
closed = true;
|
|
344
|
+
combinedSignal.removeEventListener("abort", onCombinedAbort);
|
|
345
|
+
if (!server.listening) {
|
|
346
|
+
onClosed();
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
server.closeAllConnections();
|
|
350
|
+
server.close((closeErr) => {
|
|
351
|
+
onClosed(closeErr ?? void 0);
|
|
352
|
+
});
|
|
353
|
+
};
|
|
354
|
+
function onCombinedAbort() {
|
|
355
|
+
closeServer((closeErr) => {
|
|
356
|
+
if (closeErr) {
|
|
357
|
+
reject(closeErr);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (signal?.aborted) {
|
|
361
|
+
reject(signal.reason ?? new LoginCancelledError());
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
if (timeoutSignal.aborted) {
|
|
365
|
+
reject(new Error("Login timed out. Please try again."));
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
reject(new LoginCancelledError());
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
server.on("error", (error) => {
|
|
372
|
+
closeServer(() => {
|
|
373
|
+
if (error.code === "EADDRINUSE") {
|
|
374
|
+
const inUsePort = error.port ?? port;
|
|
375
|
+
reject(
|
|
376
|
+
new Error(
|
|
377
|
+
`Port ${String(inUsePort)} is already in use. Close the other app using it or set KITTL_REDIRECT_PORT to a free port, then try again.`
|
|
378
|
+
)
|
|
379
|
+
);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
reject(error);
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
combinedSignal.addEventListener("abort", onCombinedAbort, { once: true });
|
|
386
|
+
if (combinedSignal.aborted) {
|
|
387
|
+
onCombinedAbort();
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
server.listen(port, hostname);
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
async openBrowser(url) {
|
|
394
|
+
await open(url);
|
|
395
|
+
}
|
|
396
|
+
mapTokenSetToSession(tokenSet, currentSession) {
|
|
397
|
+
return {
|
|
398
|
+
accessToken: tokenSet.access_token,
|
|
399
|
+
refreshToken: tokenSet.refresh_token ?? currentSession?.refreshToken,
|
|
400
|
+
idToken: tokenSet.id_token ?? currentSession?.idToken,
|
|
401
|
+
tokenType: tokenSet.token_type ?? currentSession?.tokenType,
|
|
402
|
+
expiresAt: tokenSet.expires_in ? Math.floor(Date.now() / 1e3) + tokenSet.expires_in : void 0
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
var authService = new AuthService();
|
|
407
|
+
|
|
408
|
+
// src/base-command.ts
|
|
409
|
+
import { Command } from "@oclif/core";
|
|
410
|
+
import { render } from "ink";
|
|
411
|
+
import React from "react";
|
|
412
|
+
|
|
413
|
+
// src/services/api.service.ts
|
|
414
|
+
import axios, {
|
|
415
|
+
AxiosHeaders
|
|
416
|
+
} from "axios";
|
|
417
|
+
var KittlApiService = class {
|
|
418
|
+
client;
|
|
419
|
+
accessTokenProvider;
|
|
420
|
+
constructor() {
|
|
421
|
+
this.client = axios.create({
|
|
422
|
+
baseURL: API_CONFIG.baseUrl,
|
|
423
|
+
timeout: API_CONFIG.timeoutMs,
|
|
424
|
+
headers: {
|
|
425
|
+
"User-Agent": API_CONFIG.userAgent
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
this.client.interceptors.request.use(
|
|
429
|
+
async (config) => {
|
|
430
|
+
if (config.skipAuth) {
|
|
431
|
+
return config;
|
|
432
|
+
}
|
|
433
|
+
const token = await this.accessTokenProvider?.();
|
|
434
|
+
if (token) {
|
|
435
|
+
this.setAuthorizationHeader(config, token);
|
|
436
|
+
}
|
|
437
|
+
return config;
|
|
438
|
+
}
|
|
439
|
+
);
|
|
440
|
+
this.client.interceptors.response.use(
|
|
441
|
+
(response) => response,
|
|
442
|
+
async (error) => {
|
|
443
|
+
const originalRequest = error.config;
|
|
444
|
+
if (error.response?.status !== 401 || !originalRequest || originalRequest.skipAuth || originalRequest._retry) {
|
|
445
|
+
throw error;
|
|
446
|
+
}
|
|
447
|
+
originalRequest._retry = true;
|
|
448
|
+
const refreshedSession = await authService.refreshSession();
|
|
449
|
+
const refreshedToken = refreshedSession?.accessToken;
|
|
450
|
+
if (!refreshedToken) {
|
|
451
|
+
throw error;
|
|
452
|
+
}
|
|
453
|
+
this.setAuthorizationHeader(originalRequest, refreshedToken);
|
|
454
|
+
return this.client.request(originalRequest);
|
|
455
|
+
}
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
setAccessTokenProvider(provider) {
|
|
459
|
+
this.accessTokenProvider = provider;
|
|
460
|
+
}
|
|
461
|
+
getClient() {
|
|
462
|
+
return this.client;
|
|
463
|
+
}
|
|
464
|
+
setAuthorizationHeader(config, token) {
|
|
465
|
+
const headers = AxiosHeaders.from(config.headers);
|
|
466
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
467
|
+
config.headers = headers;
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
var kittlApiService = new KittlApiService();
|
|
471
|
+
|
|
472
|
+
// src/base-command.ts
|
|
473
|
+
var BaseCommand = class extends Command {
|
|
474
|
+
session = null;
|
|
475
|
+
async init() {
|
|
476
|
+
await super.init();
|
|
477
|
+
this.session = await authService.getSession();
|
|
478
|
+
kittlApiService.setAccessTokenProvider(
|
|
479
|
+
async () => authService.getAccessToken()
|
|
480
|
+
);
|
|
481
|
+
const envEntries = getKittlEnvEntries();
|
|
482
|
+
this.debug(`API base URL: ${API_CONFIG.baseUrl}`);
|
|
483
|
+
this.debug(`KITTL_* variables:
|
|
484
|
+
${envEntries.join("\n") || "(none)"}`);
|
|
485
|
+
}
|
|
486
|
+
getKittlApiClient() {
|
|
487
|
+
return kittlApiService.getClient();
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Ensures a valid access token (+ refreshes when the accessToken is near expiry).
|
|
491
|
+
*/
|
|
492
|
+
async ensureAuthenticated() {
|
|
493
|
+
const session = await authService.getSession();
|
|
494
|
+
if (!session?.accessToken) {
|
|
495
|
+
this.error(
|
|
496
|
+
`Session expired. Run \`${this.config.bin} auth:login\` first.`,
|
|
497
|
+
{ exit: 2 }
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
this.session = session;
|
|
501
|
+
return session;
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Run an Ink view that reports a result via `onDone`, then unmount and return.
|
|
505
|
+
* PRO TIP: use `this.log` / `this.error` after this resolves, not inside the view!! for final line stays on stdout.
|
|
506
|
+
*/
|
|
507
|
+
async renderView(View) {
|
|
508
|
+
return new Promise((resolve, reject) => {
|
|
509
|
+
const app = render(
|
|
510
|
+
React.createElement(View, {
|
|
511
|
+
onDone: (result) => {
|
|
512
|
+
void (async () => {
|
|
513
|
+
app.unmount();
|
|
514
|
+
await app.waitUntilExit();
|
|
515
|
+
resolve(result);
|
|
516
|
+
})().catch(reject);
|
|
517
|
+
}
|
|
518
|
+
})
|
|
519
|
+
);
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
export {
|
|
525
|
+
INK_VIEW_UNMOUNT_REASON,
|
|
526
|
+
authService,
|
|
527
|
+
BaseCommand
|
|
528
|
+
};
|
|
529
|
+
//# sourceMappingURL=chunk-JGD3QFQS.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/services/auth.service.ts","../package.json","../src/utils.ts","../src/constants.ts","../src/services/session-vault.service.ts","../src/base-command.ts","../src/services/api.service.ts"],"sourcesContent":["import { createServer } from 'node:http';\nimport { URL } from 'node:url';\nimport open from 'open';\nimport * as oidc from 'openid-client';\nimport { z } from 'zod';\nimport { AUTH_CONFIG } from '../constants';\nimport type { Session } from '../types/session';\nimport { sessionVault } from './session-vault.service';\n\n// Seconds before access token expiry when we proactively refresh.\nconst ACCESS_TOKEN_REFRESH_BUFFER_SEC = 60;\n\nconst OAUTH2_TOKEN_ERROR_INVALID_GRANT = 'invalid_grant';\n\nexport type LoginOptions = {\n signal?: AbortSignal;\n};\n\nexport type CallbackResult = {\n callbackUrl: URL;\n};\n\nconst authConfigSchema = z.object({\n issuer: z.url(),\n clientId: z.string().min(1),\n redirectUri: z.url(),\n redirectPort: z.number().int().min(1).max(65_535),\n scope: z.string().min(1),\n oauthCallbackTimeoutMs: z.number().int().min(5_000).max(3_600_000),\n oauthSuccessRedirectUrl: z.url().optional(),\n});\n\nexport class LoginCancelledError extends Error {\n public constructor() {\n super('Login cancelled.');\n this.name = 'LoginCancelledError';\n }\n}\n\nexport class AuthService {\n private readonly config = authConfigSchema.parse(AUTH_CONFIG);\n\n // concurrent refresh attempts are merged into a single promise\n private refreshSessionPromise: Promise<Session | null> | null = null;\n\n /**\n * raw vault read.\n */\n public async getStoredSession(): Promise<Session | null> {\n return sessionVault.getSession();\n }\n\n /**\n * ensures token freshness (by silently refresh if needed)\n */\n public async getSession(): Promise<Session | null> {\n await this.getAccessToken();\n return this.getStoredSession();\n }\n\n /**\n * Returns a usable access token, silently refreshing when `expiresAt` is within {@link ACCESS_TOKEN_REFRESH_BUFFER_SEC}\n * seconds (or in the past). based on Session.expiresAt.\n */\n public async getAccessToken(): Promise<string | undefined> {\n const session = await this.getStoredSession();\n if (!session?.accessToken) return undefined;\n\n if (!this.shouldRefreshAccessToken(session)) {\n return session.accessToken;\n }\n\n if (!session.refreshToken) {\n await sessionVault.clear();\n return undefined;\n }\n\n const refreshed = await this.refreshSession();\n return refreshed?.accessToken;\n }\n\n /**\n * Refresh tokens via the OIDC token endpoint. On hard failure (e.g. `invalid_grant`), clears the vault.\n */\n public async refreshSession(): Promise<Session | null> {\n if (this.refreshSessionPromise) {\n return this.refreshSessionPromise;\n }\n this.refreshSessionPromise = this.performRefreshSession().finally(() => {\n this.refreshSessionPromise = null;\n });\n return this.refreshSessionPromise;\n }\n\n private shouldRefreshAccessToken(session: Session): boolean {\n if (session.expiresAt === undefined) return false;\n const now = Math.floor(Date.now() / 1000);\n return session.expiresAt <= now + ACCESS_TOKEN_REFRESH_BUFFER_SEC;\n }\n\n private async performRefreshSession(): Promise<Session | null> {\n const session = await this.getStoredSession();\n if (!session?.refreshToken) {\n await sessionVault.clear();\n return null;\n }\n\n try {\n const oidcConfig = await this.discoverOidcConfiguration();\n\n const tokenSet = await oidc.refreshTokenGrant(\n oidcConfig,\n session.refreshToken,\n { scope: this.config.scope },\n undefined,\n );\n\n const newSession = this.mapTokenSetToSession(tokenSet, session);\n\n await sessionVault.saveSession(newSession);\n return newSession;\n } catch (error) {\n if (\n error instanceof oidc.ResponseBodyError &&\n error.error === OAUTH2_TOKEN_ERROR_INVALID_GRANT\n ) {\n await sessionVault.clear();\n return null;\n }\n return null;\n }\n }\n\n private async discoverOidcConfiguration(): Promise<oidc.Configuration> {\n const issuerUrl = new URL(this.config.issuer);\n return oidc.discovery(\n issuerUrl,\n this.config.clientId,\n undefined,\n undefined,\n {\n ...(issuerUrl.protocol === 'http:'\n ? { execute: [oidc.allowInsecureRequests] as const }\n : {}),\n } as Parameters<typeof oidc.discovery>[4],\n );\n }\n\n public async login(options?: LoginOptions): Promise<Session> {\n const { signal } = options ?? {};\n\n const oidcConfig = await this.discoverOidcConfiguration();\n signal?.throwIfAborted();\n\n const codeVerifier = oidc.randomPKCECodeVerifier();\n const codeChallenge = await oidc.calculatePKCECodeChallenge(codeVerifier);\n const state = oidc.randomState();\n const nonce = oidc.randomNonce();\n\n const authorizationUrl = oidc.buildAuthorizationUrl(oidcConfig, {\n redirect_uri: this.config.redirectUri,\n response_type: 'code',\n scope: this.config.scope,\n code_challenge: codeChallenge,\n code_challenge_method: 'S256',\n state,\n nonce,\n });\n\n signal?.throwIfAborted();\n await this.openBrowser(authorizationUrl.toString());\n\n const callback = await this.waitForCallback(signal);\n\n const tokenSet = await oidc.authorizationCodeGrant(\n oidcConfig,\n callback.callbackUrl,\n {\n pkceCodeVerifier: codeVerifier,\n expectedState: state,\n expectedNonce: nonce,\n },\n );\n\n const session = this.mapTokenSetToSession(tokenSet);\n\n await sessionVault.saveSession(session);\n return session;\n }\n\n public async logout(): Promise<void> {\n await sessionVault.clear();\n }\n\n /**\n * Starts a short-lived `http` server on the host/port from the configured `redirectUri` so the IdP can\n * redirect the browser to `…/callback?code=…&state=…` (Authorization Code + PKCE). The first matching request\n * stops the server and resolves with that URL for the token exchange (`authorizationCodeGrant`).\n *\n * - **Success response:** HTTP 302 to {@link AUTH_CONFIG.oauthSuccessRedirectUrl} when set; otherwise a minimal inline HTML page.\n */\n private async waitForCallback(signal?: AbortSignal): Promise<CallbackResult> {\n const redirectUrl = new URL(this.config.redirectUri);\n const hostname = redirectUrl.hostname;\n const port = Number(redirectUrl.port);\n const pathname = redirectUrl.pathname;\n\n const timeoutSignal = AbortSignal.timeout(\n this.config.oauthCallbackTimeoutMs,\n );\n const combinedSignal =\n signal !== undefined\n ? AbortSignal.any([signal, timeoutSignal])\n : timeoutSignal;\n\n return new Promise((resolve, reject) => {\n let closed = false;\n\n const server = createServer((req, res) => {\n try {\n const requestUrl = new URL(req.url ?? '', this.config.redirectUri);\n if (requestUrl.pathname !== pathname) {\n res.statusCode = 404;\n res.end('Not Found');\n return;\n }\n\n const callbackUrl = new URL(this.config.redirectUri);\n callbackUrl.search = requestUrl.search;\n\n const brandUrl = this.config.oauthSuccessRedirectUrl;\n if (brandUrl) {\n res.writeHead(302, { Location: brandUrl });\n res.end();\n } else {\n res.statusCode = 200;\n res.setHeader('content-type', 'text/html; charset=utf-8');\n res.end(\n '<!doctype html><html><body><h2>Authentication complete.</h2><p>You can close this tab and return to the terminal.</p></body></html>',\n );\n }\n\n closeServer((closeErr?: Error) => {\n if (closeErr) {\n reject(closeErr);\n return;\n }\n resolve({ callbackUrl });\n });\n } catch (error) {\n closeServer(() => reject(error));\n }\n });\n\n const closeServer = (onClosed: (closeErr?: Error) => void): void => {\n if (closed) return;\n closed = true;\n combinedSignal.removeEventListener('abort', onCombinedAbort);\n if (!server.listening) {\n onClosed();\n return;\n }\n server.closeAllConnections();\n server.close((closeErr) => {\n onClosed(closeErr ?? undefined);\n });\n };\n\n function onCombinedAbort(): void {\n closeServer((closeErr?: Error) => {\n if (closeErr) {\n reject(closeErr);\n return;\n }\n if (signal?.aborted) {\n reject(signal.reason ?? new LoginCancelledError());\n return;\n }\n if (timeoutSignal.aborted) {\n reject(new Error('Login timed out. Please try again.'));\n return;\n }\n reject(new LoginCancelledError());\n });\n }\n\n server.on('error', (error: NodeJS.ErrnoException) => {\n closeServer(() => {\n if (error.code === 'EADDRINUSE') {\n const inUsePort =\n (error as NodeJS.ErrnoException & { port?: number }).port ?? port;\n reject(\n new Error(\n `Port ${String(inUsePort)} is already in use. Close the other app using it or set KITTL_REDIRECT_PORT to a free port, then try again.`,\n ),\n );\n return;\n }\n reject(error);\n });\n });\n\n // 1. Abort before any listen — avoids a half-started server; early exit skips `listen` entirely.\n combinedSignal.addEventListener('abort', onCombinedAbort, { once: true });\n if (combinedSignal.aborted) {\n onCombinedAbort();\n return;\n }\n\n server.listen(port, hostname);\n });\n }\n\n private async openBrowser(url: string): Promise<void> {\n await open(url);\n }\n\n private mapTokenSetToSession(\n tokenSet: oidc.TokenEndpointResponse,\n currentSession?: Session,\n ): Session {\n return {\n accessToken: tokenSet.access_token,\n refreshToken: tokenSet.refresh_token ?? currentSession?.refreshToken,\n idToken: tokenSet.id_token ?? currentSession?.idToken,\n tokenType: tokenSet.token_type ?? currentSession?.tokenType,\n expiresAt: tokenSet.expires_in\n ? Math.floor(Date.now() / 1000) + tokenSet.expires_in\n : undefined,\n };\n }\n}\n\nexport const authService = new AuthService();\n","{\n \"name\": \"@kittl/cli\",\n \"version\": \"0.0.1\",\n \"private\": true,\n \"type\": \"module\",\n \"bin\": {\n \"kittl\": \"./bin/run.js\",\n \"kittl-dev\": \"./bin/dev.js\"\n },\n \"publishConfig\": {\n \"bin\": {\n \"kittl\": \"./bin/run.js\"\n }\n },\n \"files\": [\n \"bin/bootstrap.js\",\n \"bin/run.cmd\",\n \"bin/run.js\",\n \"dist\"\n ],\n \"scripts\": {\n \"build\": \"tsup && oclif manifest\",\n \"build:watch\": \"tsup --watch --onSuccess \\\"oclif manifest\\\"\",\n \"dev\": \"node ./bin/dev.js\",\n \"dev:watch\": \"node --watch --watch-path=./src --watch-path=./bin ./bin/dev.js\",\n \"typecheck\": \"tsc --noEmit\",\n \"test\": \"vitest run\",\n \"test:watch\": \"vitest\"\n },\n \"dependencies\": {\n \"@oclif/core\": \"^4.10.2\",\n \"axios\": \"^1.13.6\",\n \"cross-keychain\": \"^1.1.0\",\n \"ink\": \"^6.8.0\",\n \"jwt-decode\": \"^4.0.0\",\n \"open\": \"^11.0.0\",\n \"openid-client\": \"^6.8.2\",\n \"react\": \"catalog:\",\n \"zod\": \"catalog:\"\n },\n \"devDependencies\": {\n \"@types/node\": \"^25.5.0\",\n \"@types/react\": \"catalog:\",\n \"ink-testing-library\": \"^4.0.0\",\n \"oclif\": \"^4.22.96\",\n \"tinyglobby\": \"^0.2.15\",\n \"tsup\": \"catalog:\",\n \"tsx\": \"catalog:\",\n \"typescript\": \"catalog:\",\n \"vitest\": \"^4.1.1\"\n },\n \"oclif\": {\n \"bin\": \"kittl\",\n \"commands\": \"./dist/commands\",\n \"dirname\": \"kittl\",\n \"topicSeparator\": \" \"\n }\n}\n","/** Silent UI teardown (e.g. Ink unmount) — not user-initiated cancel. */\nexport const INK_VIEW_UNMOUNT_REASON = Symbol('INK_VIEW_UNMOUNT_REASON');\n\n/**\n * Reads `KITTL_REDIRECT_PORT` (or {@link defaultPort}) and validates it is an integer in 1–65535.\n */\nexport function parseRedirectPortFromEnv(defaultPort: number): number {\n const raw = process.env.KITTL_REDIRECT_PORT ?? String(defaultPort);\n const n = Number(raw);\n if (!Number.isInteger(n) || n < 1 || n > 65_535) {\n throw new Error(\n `KITTL_REDIRECT_PORT must be an integer between 1 and 65535 (got ${JSON.stringify(raw)}).`,\n );\n }\n return n;\n}\n\n/** Fixed localhost OAuth callback path for Authorization Code + PKCE. */\nexport function localhostOAuthRedirectUri(port: number): string {\n return `http://localhost:${port}/callback`;\n}\n\nexport function getKittlEnvEntries(): string[] {\n return Object.entries(process.env)\n .filter(([key]) => key.startsWith('KITTL_'))\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([key, value]) => `${key}=${value ?? ''}`);\n}\n","import pkg from '../package.json' with { type: 'json' };\nimport { localhostOAuthRedirectUri, parseRedirectPortFromEnv } from './utils';\n\nconst { version } = pkg;\n\nexport const PRODUCTION = {\n issuer: 'https://auth.kittl.com/realms/kittl', //TODO: TBD\n apiBaseUrl: 'https://api.kittl.com',\n clientId: 'kittl-cli',\n redirectPort: 51771,\n} as const;\n\n// -----------------------------------------------------------------------------\n// OAuth / OIDC (PKCE login, keychain session)\n// -----------------------------------------------------------------------------\n\nexport const AUTH_CONFIG = {\n issuer: process.env.KITTL_ISSUER_URL ?? PRODUCTION.issuer,\n clientId: process.env.KITTL_CLIENT_ID ?? PRODUCTION.clientId,\n redirectPort: parseRedirectPortFromEnv(PRODUCTION.redirectPort),\n redirectUri: localhostOAuthRedirectUri(\n parseRedirectPortFromEnv(PRODUCTION.redirectPort),\n ),\n scope: process.env.KITTL_AUTH_SCOPE ?? 'openid profile email offline_access',\n oauthCallbackTimeoutMs: Number(\n process.env.KITTL_OAUTH_CALLBACK_TIMEOUT_MS ?? 120_000,\n ),\n /** Optional 302 after OAuth; if undefined it renders a fallback HTML. */\n oauthSuccessRedirectUrl:\n process.env.KITTL_OAUTH_SUCCESS_REDIRECT_URL || undefined,\n serviceName: 'kittl-cli',\n accountName: 'oauth-session',\n} as const;\n\n// -----------------------------------------------------------------------------\n// HTTP API (Axios base URL for Kittl APIs)\n// -----------------------------------------------------------------------------\n\nexport const API_CONFIG = {\n baseUrl: process.env.KITTL_API_BASE_URL ?? PRODUCTION.apiBaseUrl,\n timeoutMs: 30_000,\n userAgent: `kittl-cli/${version}`,\n} as const;\n","import { deletePassword, getPassword, setPassword } from 'cross-keychain';\nimport { AUTH_CONFIG } from '../constants';\nimport type { Session } from '../types/session';\n\n/**\n * Persists OIDC session (tokens) in the OS credential store (e.g. Keychain).\n */\nexport class SessionVault {\n private readonly keychainService = AUTH_CONFIG.serviceName;\n private readonly keychainAccount = AUTH_CONFIG.accountName;\n\n public async getSession(): Promise<Session | null> {\n const raw = await getPassword(this.keychainService, this.keychainAccount);\n if (!raw) return null;\n\n try {\n return JSON.parse(raw) as Session;\n } catch {\n return null;\n }\n }\n\n public async saveSession(session: Session): Promise<void> {\n await setPassword(\n this.keychainService,\n this.keychainAccount,\n JSON.stringify(session),\n );\n }\n\n public async clear(): Promise<void> {\n await deletePassword(this.keychainService, this.keychainAccount);\n }\n}\n\nexport const sessionVault = new SessionVault();\n","import { Command } from '@oclif/core';\nimport type { AxiosInstance } from 'axios';\nimport { render } from 'ink';\nimport type { FC } from 'react';\nimport React from 'react';\nimport { API_CONFIG } from './constants';\nimport { kittlApiService } from './services/api.service';\nimport { authService } from './services/auth.service';\nimport type { Session } from './types/session';\nimport { getKittlEnvEntries } from './utils';\n\nexport abstract class BaseCommand extends Command {\n protected session: Session | null = null;\n\n public override async init(): Promise<void> {\n await super.init();\n this.session = await authService.getSession();\n kittlApiService.setAccessTokenProvider(async () =>\n authService.getAccessToken(),\n );\n\n const envEntries = getKittlEnvEntries();\n this.debug(`API base URL: ${API_CONFIG.baseUrl}`);\n this.debug(`KITTL_* variables:\\n${envEntries.join('\\n') || '(none)'}`);\n }\n\n protected getKittlApiClient(): AxiosInstance {\n return kittlApiService.getClient();\n }\n\n /**\n * Ensures a valid access token (+ refreshes when the accessToken is near expiry).\n */\n protected async ensureAuthenticated(): Promise<Session> {\n const session = await authService.getSession();\n if (!session?.accessToken) {\n this.error(\n `Session expired. Run \\`${this.config.bin} auth:login\\` first.`,\n { exit: 2 },\n );\n }\n\n this.session = session;\n return session;\n }\n\n /**\n * Run an Ink view that reports a result via `onDone`, then unmount and return.\n * PRO TIP: use `this.log` / `this.error` after this resolves, not inside the view!! for final line stays on stdout.\n */\n protected async renderView<R>(\n View: FC<{ onDone: (result: R) => void }>,\n ): Promise<R> {\n return new Promise<R>((resolve, reject) => {\n const app = render(\n React.createElement(View, {\n onDone: (result: R) => {\n void (async () => {\n app.unmount();\n await app.waitUntilExit();\n resolve(result);\n })().catch(reject);\n },\n }),\n );\n });\n }\n}\n","import axios, {\n type AxiosError,\n AxiosHeaders,\n type AxiosInstance,\n type InternalAxiosRequestConfig,\n} from 'axios';\nimport { API_CONFIG } from '../constants';\nimport { authService } from './auth.service';\n\ntype AccessTokenProvider = () => Promise<string | undefined>;\ntype RetriableRequestConfig = InternalAxiosRequestConfig & {\n _retry?: boolean;\n};\n\nexport class KittlApiService {\n private readonly client: AxiosInstance;\n private accessTokenProvider?: AccessTokenProvider;\n\n public constructor() {\n this.client = axios.create({\n baseURL: API_CONFIG.baseUrl,\n timeout: API_CONFIG.timeoutMs,\n headers: {\n 'User-Agent': API_CONFIG.userAgent,\n },\n });\n\n this.client.interceptors.request.use(\n async (config: RetriableRequestConfig) => {\n if (config.skipAuth) {\n return config;\n }\n const token = await this.accessTokenProvider?.();\n if (token) {\n this.setAuthorizationHeader(config, token);\n }\n return config;\n },\n );\n\n this.client.interceptors.response.use(\n (response) => response,\n async (error: AxiosError) => {\n const originalRequest = error.config as\n | RetriableRequestConfig\n | undefined;\n // Retry only first-time 401 responses with a valid original request.\n if (\n error.response?.status !== 401 ||\n !originalRequest ||\n originalRequest.skipAuth ||\n originalRequest._retry\n ) {\n throw error;\n }\n\n // Mark as retried to avoid infinite retry loops.\n originalRequest._retry = true;\n // Force-refresh the token before replaying the request.\n const refreshedSession = await authService.refreshSession();\n const refreshedToken = refreshedSession?.accessToken;\n if (!refreshedToken) {\n throw error;\n }\n\n // Re-run the original request with updated Authorization header.\n this.setAuthorizationHeader(originalRequest, refreshedToken);\n return this.client.request(originalRequest);\n },\n );\n }\n\n public setAccessTokenProvider(provider: AccessTokenProvider): void {\n this.accessTokenProvider = provider;\n }\n\n public getClient(): AxiosInstance {\n return this.client;\n }\n\n private setAuthorizationHeader(\n config: InternalAxiosRequestConfig,\n token: string,\n ): void {\n const headers = AxiosHeaders.from(config.headers);\n headers.set('Authorization', `Bearer ${token}`);\n config.headers = headers;\n }\n}\n\nexport const kittlApiService = new KittlApiService();\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BaseCommand
|
|
3
|
+
} from "../../chunk-JGD3QFQS.js";
|
|
4
|
+
|
|
5
|
+
// src/commands/app/create.ts
|
|
6
|
+
var AppCreate = class _AppCreate extends BaseCommand {
|
|
7
|
+
static description = "Create a Kittl app (coming soon)";
|
|
8
|
+
async run() {
|
|
9
|
+
await this.parse(_AppCreate);
|
|
10
|
+
this.log("Not implemented yet.");
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
export {
|
|
14
|
+
AppCreate as default
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=create.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/commands/app/create.ts"],"sourcesContent":["import { BaseCommand } from '../../base-command';\n\nexport default class AppCreate extends BaseCommand {\n public static override description = 'Create a Kittl app (coming soon)';\n\n public async run(): Promise<void> {\n await this.parse(AppCreate);\n\n this.log('Not implemented yet.');\n }\n}\n"],"mappings":";;;;;AAEA,IAAqB,YAArB,MAAqB,mBAAkB,YAAY;AAAA,EACjD,OAAuB,cAAc;AAAA,EAErC,MAAa,MAAqB;AAChC,UAAM,KAAK,MAAM,UAAS;AAE1B,SAAK,IAAI,sBAAsB;AAAA,EACjC;AACF;","names":[]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BaseCommand
|
|
3
|
+
} from "../../chunk-JGD3QFQS.js";
|
|
4
|
+
|
|
5
|
+
// src/commands/app/upload.ts
|
|
6
|
+
var AppUpload = class _AppUpload extends BaseCommand {
|
|
7
|
+
static description = "Upload app files for review (coming soon)";
|
|
8
|
+
async run() {
|
|
9
|
+
await this.parse(_AppUpload);
|
|
10
|
+
this.log("Not implemented yet.");
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
export {
|
|
14
|
+
AppUpload as default
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=upload.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/commands/app/upload.ts"],"sourcesContent":["import { BaseCommand } from '../../base-command';\n\nexport default class AppUpload extends BaseCommand {\n public static override description =\n 'Upload app files for review (coming soon)';\n\n public async run(): Promise<void> {\n await this.parse(AppUpload);\n\n this.log('Not implemented yet.');\n }\n}\n"],"mappings":";;;;;AAEA,IAAqB,YAArB,MAAqB,mBAAkB,YAAY;AAAA,EACjD,OAAuB,cACrB;AAAA,EAEF,MAAa,MAAqB;AAChC,UAAM,KAAK,MAAM,UAAS;AAE1B,SAAK,IAAI,sBAAsB;AAAA,EACjC;AACF;","names":[]}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BaseCommand,
|
|
3
|
+
INK_VIEW_UNMOUNT_REASON,
|
|
4
|
+
authService
|
|
5
|
+
} from "../../chunk-JGD3QFQS.js";
|
|
6
|
+
|
|
7
|
+
// src/ui/views/LoginView.tsx
|
|
8
|
+
import { Box, Text as Text2 } from "ink";
|
|
9
|
+
import { useCallback, useEffect as useEffect3, useRef, useState as useState3 } from "react";
|
|
10
|
+
|
|
11
|
+
// src/ui/components/InlineSpinner.tsx
|
|
12
|
+
import { Text } from "ink";
|
|
13
|
+
import { useEffect, useState } from "react";
|
|
14
|
+
|
|
15
|
+
// src/ui/theme/tokens.ts
|
|
16
|
+
var colors = {
|
|
17
|
+
textPrimary: "white",
|
|
18
|
+
textMuted: "gray",
|
|
19
|
+
success: "green",
|
|
20
|
+
warning: "yellow",
|
|
21
|
+
danger: "red",
|
|
22
|
+
accent: "cyan"
|
|
23
|
+
};
|
|
24
|
+
var spacing = {
|
|
25
|
+
xs: 0,
|
|
26
|
+
sm: 1,
|
|
27
|
+
md: 2
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// src/ui/theme/styles.ts
|
|
31
|
+
var layoutStyles = {
|
|
32
|
+
viewColumn: {
|
|
33
|
+
flexDirection: "column",
|
|
34
|
+
padding: spacing.sm
|
|
35
|
+
},
|
|
36
|
+
section: {
|
|
37
|
+
marginTop: spacing.sm
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
var textStyles = {
|
|
41
|
+
title: {
|
|
42
|
+
bold: true,
|
|
43
|
+
color: colors.accent
|
|
44
|
+
},
|
|
45
|
+
muted: {
|
|
46
|
+
color: colors.textMuted
|
|
47
|
+
},
|
|
48
|
+
success: {
|
|
49
|
+
color: colors.success
|
|
50
|
+
},
|
|
51
|
+
warning: {
|
|
52
|
+
color: colors.warning
|
|
53
|
+
},
|
|
54
|
+
error: {
|
|
55
|
+
color: colors.danger
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// src/ui/components/InlineSpinner.tsx
|
|
60
|
+
import { jsxs } from "react/jsx-runtime";
|
|
61
|
+
var frames = ["-", "\\", "|", "/"];
|
|
62
|
+
function InlineSpinner({ label }) {
|
|
63
|
+
const [frameIndex, setFrameIndex] = useState(0);
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
const timer = setInterval(() => {
|
|
66
|
+
setFrameIndex((current) => (current + 1) % frames.length);
|
|
67
|
+
}, 80);
|
|
68
|
+
return () => clearInterval(timer);
|
|
69
|
+
}, []);
|
|
70
|
+
return /* @__PURE__ */ jsxs(Text, { ...textStyles.muted, children: [
|
|
71
|
+
frames[frameIndex],
|
|
72
|
+
" ",
|
|
73
|
+
label
|
|
74
|
+
] });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// src/ui/hooks/useTerminalWidth.ts
|
|
78
|
+
import { useStdout } from "ink";
|
|
79
|
+
import { useEffect as useEffect2, useState as useState2 } from "react";
|
|
80
|
+
function useTerminalWidth() {
|
|
81
|
+
const { stdout } = useStdout();
|
|
82
|
+
const [width, setWidth] = useState2(() => stdout.columns ?? 80);
|
|
83
|
+
useEffect2(() => {
|
|
84
|
+
const onResize = () => setWidth(stdout.columns ?? 80);
|
|
85
|
+
stdout.on("resize", onResize);
|
|
86
|
+
return () => {
|
|
87
|
+
stdout.off("resize", onResize);
|
|
88
|
+
};
|
|
89
|
+
}, [stdout]);
|
|
90
|
+
return width;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// src/ui/views/LoginView.tsx
|
|
94
|
+
import { jsx, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
95
|
+
var CHECKING_SESSION = "Checking session\u2026";
|
|
96
|
+
var SIGNING_IN = "Signing in\u2026";
|
|
97
|
+
function LoginView({ onDone }) {
|
|
98
|
+
const width = useTerminalWidth();
|
|
99
|
+
const doneRef = useRef(false);
|
|
100
|
+
const [phase, setPhase] = useState3("checking");
|
|
101
|
+
const finish = useCallback(
|
|
102
|
+
(result, force = false) => {
|
|
103
|
+
if (doneRef.current && !force)
|
|
104
|
+
return;
|
|
105
|
+
doneRef.current = true;
|
|
106
|
+
onDone(result);
|
|
107
|
+
},
|
|
108
|
+
[onDone]
|
|
109
|
+
);
|
|
110
|
+
useEffect3(() => {
|
|
111
|
+
doneRef.current = false;
|
|
112
|
+
const ac = new AbortController();
|
|
113
|
+
void (async () => {
|
|
114
|
+
try {
|
|
115
|
+
const existing = await authService.getSession();
|
|
116
|
+
if (ac.signal.aborted) {
|
|
117
|
+
finish({ kind: "cancelled" }, true);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (existing?.accessToken) {
|
|
121
|
+
finish({ kind: "success", reusedSession: true });
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
setPhase("signing-in");
|
|
125
|
+
await authService.login({ signal: ac.signal });
|
|
126
|
+
finish({ kind: "success" });
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (error === INK_VIEW_UNMOUNT_REASON) {
|
|
129
|
+
finish({ kind: "cancelled" }, true);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (doneRef.current)
|
|
133
|
+
return;
|
|
134
|
+
finish({
|
|
135
|
+
kind: "error",
|
|
136
|
+
error: error instanceof Error ? error : new Error("Auth failed")
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
})();
|
|
140
|
+
return () => {
|
|
141
|
+
doneRef.current = true;
|
|
142
|
+
ac.abort(INK_VIEW_UNMOUNT_REASON);
|
|
143
|
+
};
|
|
144
|
+
}, [finish]);
|
|
145
|
+
return /* @__PURE__ */ jsxs2(
|
|
146
|
+
Box,
|
|
147
|
+
{
|
|
148
|
+
...layoutStyles.viewColumn,
|
|
149
|
+
width: width > 0 ? width : "100%",
|
|
150
|
+
minWidth: 0,
|
|
151
|
+
children: [
|
|
152
|
+
/* @__PURE__ */ jsx(Text2, { ...textStyles.title, children: "Kittl CLI Authentication" }),
|
|
153
|
+
/* @__PURE__ */ jsx(Box, { ...layoutStyles.section, children: /* @__PURE__ */ jsx(
|
|
154
|
+
InlineSpinner,
|
|
155
|
+
{
|
|
156
|
+
label: phase === "checking" ? CHECKING_SESSION : SIGNING_IN
|
|
157
|
+
}
|
|
158
|
+
) })
|
|
159
|
+
]
|
|
160
|
+
}
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// src/commands/auth/login.ts
|
|
165
|
+
var AuthLogin = class _AuthLogin extends BaseCommand {
|
|
166
|
+
static description = "Authenticate with your Kittl account";
|
|
167
|
+
async run() {
|
|
168
|
+
await this.parse(_AuthLogin);
|
|
169
|
+
const result = await this.renderView(LoginView);
|
|
170
|
+
if (result.kind === "cancelled") {
|
|
171
|
+
this.exit(130);
|
|
172
|
+
}
|
|
173
|
+
if (result.kind === "error") {
|
|
174
|
+
throw result.error;
|
|
175
|
+
}
|
|
176
|
+
if (result.reusedSession) {
|
|
177
|
+
this.log("Already signed in.");
|
|
178
|
+
} else {
|
|
179
|
+
this.log("Signed in successfully.");
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
export {
|
|
184
|
+
AuthLogin as default
|
|
185
|
+
};
|
|
186
|
+
//# sourceMappingURL=login.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/ui/views/LoginView.tsx","../../../src/ui/components/InlineSpinner.tsx","../../../src/ui/theme/tokens.ts","../../../src/ui/theme/styles.ts","../../../src/ui/hooks/useTerminalWidth.ts","../../../src/commands/auth/login.ts"],"sourcesContent":["import { Box, Text } from 'ink';\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { authService } from '../../services/auth.service';\nimport { INK_VIEW_UNMOUNT_REASON } from '../../utils';\nimport { InlineSpinner } from '../components/InlineSpinner';\nimport { useTerminalWidth } from '../hooks';\nimport { layoutStyles, textStyles } from '../theme/styles';\n\nconst CHECKING_SESSION = 'Checking session…';\nconst SIGNING_IN = 'Signing in…';\n\nexport type LoginResult =\n | { kind: 'success'; reusedSession?: boolean }\n | { kind: 'cancelled' }\n | { kind: 'error'; error: Error };\n\nexport type LoginViewProps = {\n onDone: (result: LoginResult) => void;\n};\n\n/**\n * OAuth login TUI — only live feedback (spinner). Final outcome is owned by the command via `onDone` + `this.log`.\n * **Unmount:** aborts the local `AbortController` so discovery + the callback server stop cleanly.\n */\nexport function LoginView({ onDone }: LoginViewProps) {\n const width = useTerminalWidth();\n const doneRef = useRef(false);\n const [phase, setPhase] = useState<'checking' | 'signing-in'>('checking');\n\n const finish = useCallback(\n (result: LoginResult, force = false) => {\n if (doneRef.current && !force) return;\n doneRef.current = true;\n onDone(result);\n },\n [onDone],\n );\n\n useEffect(() => {\n doneRef.current = false;\n const ac = new AbortController();\n\n void (async () => {\n try {\n const existing = await authService.getSession();\n if (ac.signal.aborted) {\n finish({ kind: 'cancelled' }, true);\n return;\n }\n if (existing?.accessToken) {\n finish({ kind: 'success', reusedSession: true });\n return;\n }\n\n setPhase('signing-in');\n await authService.login({ signal: ac.signal });\n finish({ kind: 'success' });\n } catch (error) {\n if (error === INK_VIEW_UNMOUNT_REASON) {\n finish({ kind: 'cancelled' }, true);\n return;\n }\n if (doneRef.current) return;\n finish({\n kind: 'error',\n error: error instanceof Error ? error : new Error('Auth failed'),\n });\n }\n })();\n\n return () => {\n doneRef.current = true;\n ac.abort(INK_VIEW_UNMOUNT_REASON);\n };\n }, [finish]);\n\n return (\n <Box\n {...layoutStyles.viewColumn}\n width={width > 0 ? width : '100%'}\n minWidth={0}\n >\n <Text {...textStyles.title}>Kittl CLI Authentication</Text>\n <Box {...layoutStyles.section}>\n <InlineSpinner\n label={phase === 'checking' ? CHECKING_SESSION : SIGNING_IN}\n />\n </Box>\n </Box>\n );\n}\n","import { Text } from 'ink';\nimport { useEffect, useState } from 'react';\nimport { textStyles } from '../theme/styles';\n\nexport type InlineSpinnerProps = {\n label: string;\n};\nconst frames = ['-', '\\\\', '|', '/'];\nexport function InlineSpinner({ label }: InlineSpinnerProps) {\n const [frameIndex, setFrameIndex] = useState(0);\n\n useEffect(() => {\n const timer = setInterval(() => {\n setFrameIndex((current) => (current + 1) % frames.length);\n }, 80);\n return () => clearInterval(timer);\n }, []);\n\n return (\n <Text {...textStyles.muted}>\n {frames[frameIndex]} {label}\n </Text>\n );\n}\n","export const colors = {\n textPrimary: 'white',\n textMuted: 'gray',\n success: 'green',\n warning: 'yellow',\n danger: 'red',\n accent: 'cyan',\n} as const;\n\nexport const spacing = {\n xs: 0,\n sm: 1,\n md: 2,\n} as const;\n","import { colors, spacing } from './tokens';\n\nexport const layoutStyles = {\n viewColumn: {\n flexDirection: 'column' as const,\n padding: spacing.sm,\n },\n section: {\n marginTop: spacing.sm,\n },\n};\n\nexport const textStyles = {\n title: {\n bold: true,\n color: colors.accent,\n },\n muted: {\n color: colors.textMuted,\n },\n success: {\n color: colors.success,\n },\n warning: {\n color: colors.warning,\n },\n error: {\n color: colors.danger,\n },\n};\n","import { useStdout } from 'ink';\nimport { useEffect, useState } from 'react';\n\nexport function useTerminalWidth(): number {\n const { stdout } = useStdout();\n const [width, setWidth] = useState(() => stdout.columns ?? 80);\n\n useEffect(() => {\n const onResize = () => setWidth(stdout.columns ?? 80);\n stdout.on('resize', onResize);\n return () => {\n stdout.off('resize', onResize);\n };\n }, [stdout]);\n\n return width;\n}\n","import { BaseCommand } from '../../base-command';\nimport { type LoginResult, LoginView } from '../../ui/views/LoginView';\n\nexport default class AuthLogin extends BaseCommand {\n public static description = 'Authenticate with your Kittl account';\n\n public async run(): Promise<void> {\n await this.parse(AuthLogin);\n\n const result = await this.renderView<LoginResult>(LoginView);\n\n if (result.kind === 'cancelled') {\n this.exit(130);\n }\n if (result.kind === 'error') {\n throw result.error;\n }\n\n if (result.reusedSession) {\n this.log('Already signed in.');\n } else {\n this.log('Signed in successfully.');\n }\n }\n}\n"],"mappings":"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","names":["Text","useEffect","useState","useEffect","useState","jsxs","useState","useEffect","Text"]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BaseCommand,
|
|
3
|
+
authService
|
|
4
|
+
} from "../../chunk-JGD3QFQS.js";
|
|
5
|
+
|
|
6
|
+
// src/commands/auth/logout.ts
|
|
7
|
+
var AuthLogout = class _AuthLogout extends BaseCommand {
|
|
8
|
+
static description = "Clear local Kittl CLI session";
|
|
9
|
+
async run() {
|
|
10
|
+
await this.parse(_AuthLogout);
|
|
11
|
+
await authService.logout();
|
|
12
|
+
this.log("Logged out.");
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
export {
|
|
16
|
+
AuthLogout as default
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=logout.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/commands/auth/logout.ts"],"sourcesContent":["import { BaseCommand } from '../../base-command';\nimport { authService } from '../../services/auth.service';\n\nexport default class AuthLogout extends BaseCommand {\n public static override description = 'Clear local Kittl CLI session';\n\n public async run(): Promise<void> {\n await this.parse(AuthLogout);\n\n await authService.logout();\n this.log('Logged out.');\n }\n}\n"],"mappings":";;;;;;AAGA,IAAqB,aAArB,MAAqB,oBAAmB,YAAY;AAAA,EAClD,OAAuB,cAAc;AAAA,EAErC,MAAa,MAAqB;AAChC,UAAM,KAAK,MAAM,WAAU;AAE3B,UAAM,YAAY,OAAO;AACzB,SAAK,IAAI,aAAa;AAAA,EACxB;AACF;","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kittl/cli",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"engines": {
|
|
6
|
+
"node": ">=18"
|
|
7
|
+
},
|
|
8
|
+
"bin": {
|
|
9
|
+
"kittl": "./bin/run.js"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"bin/bootstrap.js",
|
|
16
|
+
"bin/run.cmd",
|
|
17
|
+
"bin/run.js",
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@oclif/core": "^4.10.2",
|
|
22
|
+
"axios": "^1.13.6",
|
|
23
|
+
"cross-keychain": "^1.1.0",
|
|
24
|
+
"tinyglobby": "^0.2.15",
|
|
25
|
+
"ink": "^6.8.0",
|
|
26
|
+
"ink-text-input": "^6.0.0",
|
|
27
|
+
"jwt-decode": "^4.0.0",
|
|
28
|
+
"mime-types": "^3.0.1",
|
|
29
|
+
"open": "^11.0.0",
|
|
30
|
+
"openid-client": "^6.8.2",
|
|
31
|
+
"react": "19.0.0",
|
|
32
|
+
"zod": "4.3.6"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/mime-types": "^3.0.1",
|
|
36
|
+
"@types/node": "^25.5.0",
|
|
37
|
+
"@types/react": "19.0.0",
|
|
38
|
+
"ink-testing-library": "^4.0.0",
|
|
39
|
+
"oclif": "^4.22.96",
|
|
40
|
+
"tsup": "8.0.2",
|
|
41
|
+
"tsx": "4.21.0",
|
|
42
|
+
"typescript": "5.9.3",
|
|
43
|
+
"vitest": "^4.1.1"
|
|
44
|
+
},
|
|
45
|
+
"oclif": {
|
|
46
|
+
"bin": "kittl",
|
|
47
|
+
"commands": "./dist/commands",
|
|
48
|
+
"dirname": "kittl",
|
|
49
|
+
"topicSeparator": " "
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "tsup && oclif manifest",
|
|
53
|
+
"build:watch": "tsup --watch --onSuccess \"oclif manifest\"",
|
|
54
|
+
"dev": "node ./bin/dev.js",
|
|
55
|
+
"dev:watch": "node --watch --watch-path=./src --watch-path=./bin ./bin/dev.js",
|
|
56
|
+
"typecheck": "tsc --noEmit",
|
|
57
|
+
"test": "vitest run",
|
|
58
|
+
"test:watch": "vitest"
|
|
59
|
+
}
|
|
60
|
+
}
|