@kiipu/cli 0.0.4 → 0.0.5
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 +70 -49
- package/dist/commands/auth.js +119 -32
- package/dist/commands/doctor.js +3 -7
- package/dist/commands/help.js +11 -3
- package/dist/config/config.js +15 -0
- package/dist/index.js +4 -1
- package/dist/lib/browser-auth.js +319 -0
- package/dist/lib/kiipu-user-client.js +13 -1
- package/dist/lib/post-actions.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
# Kiipu CLI
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Publish to Kiipu from your terminal.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
`@kiipu/cli` is the official command line interface for Kiipu. It is the best place to start if you want to authenticate locally and post directly from the command line.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- publish posts directly from the terminal
|
|
9
|
-
- delete, restore, or permanently purge posts by id
|
|
10
|
-
- verify local setup and API reachability with `kiipu doctor`
|
|
11
|
-
- point developers to the separate Claude Code plugin and skills packages
|
|
7
|
+
Use it to:
|
|
12
8
|
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
- sign in on the current device
|
|
10
|
+
- publish posts from the command line
|
|
11
|
+
- delete, restore, or permanently remove posts by id
|
|
12
|
+
- verify local authentication and API access with `kiipu doctor`
|
|
13
|
+
|
|
14
|
+
If you want Claude Code integration on top of the CLI, use `@kiipu/claude-plugin`.
|
|
15
15
|
|
|
16
16
|
## Install
|
|
17
17
|
|
|
@@ -21,85 +21,106 @@ npm install -g @kiipu/cli
|
|
|
21
21
|
|
|
22
22
|
## Quick Start
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
1. Sign in:
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
|
-
kiipu auth login
|
|
27
|
+
kiipu auth login
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
2. Publish a post:
|
|
31
31
|
|
|
32
32
|
```bash
|
|
33
|
-
kiipu post create "
|
|
33
|
+
kiipu post create "Hello Kiipu"
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
3. Confirm local setup:
|
|
37
37
|
|
|
38
38
|
```bash
|
|
39
39
|
kiipu doctor
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
##
|
|
42
|
+
## Example Workflow
|
|
43
43
|
|
|
44
44
|
```bash
|
|
45
|
-
kiipu auth login
|
|
45
|
+
kiipu auth login
|
|
46
|
+
kiipu post create "Ship the beta today"
|
|
46
47
|
kiipu auth status
|
|
47
|
-
kiipu auth logout
|
|
48
|
-
|
|
49
|
-
kiipu post create "Hello Kiipu"
|
|
50
|
-
kiipu post delete --id post_123
|
|
51
|
-
kiipu post restore --id post_123
|
|
52
|
-
kiipu post purge --id post_123
|
|
53
|
-
|
|
54
|
-
kiipu doctor
|
|
55
|
-
kiipu skills
|
|
56
48
|
```
|
|
57
49
|
|
|
58
|
-
##
|
|
50
|
+
## Authentication
|
|
59
51
|
|
|
60
|
-
|
|
52
|
+
By default, `kiipu auth login` opens your browser and connects the current device to your Kiipu account.
|
|
61
53
|
|
|
62
|
-
```
|
|
63
|
-
|
|
54
|
+
```bash
|
|
55
|
+
kiipu auth login
|
|
64
56
|
```
|
|
65
57
|
|
|
66
|
-
|
|
58
|
+
Useful authentication commands:
|
|
67
59
|
|
|
68
60
|
```bash
|
|
69
|
-
|
|
61
|
+
kiipu auth login --device-name "MacBook Pro"
|
|
62
|
+
kiipu auth login --no-browser
|
|
63
|
+
kiipu auth login --api-key <cpk_...>
|
|
64
|
+
kiipu auth status
|
|
65
|
+
kiipu auth logout
|
|
70
66
|
```
|
|
71
67
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
## Claude Code Packages
|
|
75
|
-
|
|
76
|
-
The Claude Code integration ships separately:
|
|
68
|
+
## Posting
|
|
77
69
|
|
|
78
|
-
|
|
79
|
-
- skill assets package: `@kiipu/skills`
|
|
80
|
-
|
|
81
|
-
In this monorepo, you can test the plugin locally with:
|
|
70
|
+
Create a post:
|
|
82
71
|
|
|
83
72
|
```bash
|
|
84
|
-
|
|
73
|
+
kiipu post create "Ship the beta today"
|
|
74
|
+
kiipu post create --content "Ship the beta today"
|
|
85
75
|
```
|
|
86
76
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
Export a clean standalone CLI repository snapshot from the monorepo root:
|
|
77
|
+
Delete, restore, or permanently remove a post by id:
|
|
90
78
|
|
|
91
79
|
```bash
|
|
92
|
-
|
|
80
|
+
kiipu post delete --id post_123
|
|
81
|
+
kiipu post restore --id post_123
|
|
82
|
+
kiipu post purge --id post_123
|
|
93
83
|
```
|
|
94
84
|
|
|
95
|
-
|
|
85
|
+
## Core Commands
|
|
96
86
|
|
|
97
87
|
```bash
|
|
98
|
-
|
|
88
|
+
kiipu auth login
|
|
89
|
+
kiipu auth status
|
|
90
|
+
kiipu auth logout
|
|
91
|
+
|
|
92
|
+
kiipu post create "Hello Kiipu"
|
|
93
|
+
kiipu post delete --id post_123
|
|
94
|
+
kiipu post restore --id post_123
|
|
95
|
+
kiipu post purge --id post_123
|
|
96
|
+
|
|
97
|
+
kiipu doctor
|
|
98
|
+
kiipu --help
|
|
99
99
|
```
|
|
100
100
|
|
|
101
|
-
|
|
101
|
+
## Troubleshooting
|
|
102
|
+
|
|
103
|
+
If browser login does not finish:
|
|
104
|
+
|
|
105
|
+
- Complete sign-in in the browser tab opened by `kiipu auth login`.
|
|
106
|
+
- If the browser does not open automatically, run `kiipu auth login --no-browser` and open the printed URL yourself.
|
|
107
|
+
|
|
108
|
+
If a command fails with an authentication error:
|
|
109
|
+
|
|
110
|
+
- Run `kiipu auth status` to confirm the current device is still signed in.
|
|
111
|
+
- Re-run `kiipu auth login` if you need to refresh local credentials.
|
|
112
|
+
|
|
113
|
+
If `kiipu doctor` reports a problem:
|
|
114
|
+
|
|
115
|
+
- Re-run `kiipu auth login`.
|
|
116
|
+
- Confirm you can reach Kiipu from the same machine in your browser.
|
|
117
|
+
|
|
118
|
+
## Help
|
|
119
|
+
|
|
120
|
+
See the full command reference in the terminal:
|
|
102
121
|
|
|
103
122
|
```bash
|
|
104
|
-
|
|
123
|
+
kiipu --help
|
|
124
|
+
kiipu auth --help
|
|
125
|
+
kiipu post --help
|
|
105
126
|
```
|
package/dist/commands/auth.js
CHANGED
|
@@ -1,5 +1,110 @@
|
|
|
1
|
-
import { KiipuUserApiClient } from '../lib/kiipu-user-client.js';
|
|
2
1
|
import { saveKiipuConfig } from '../config/config.js';
|
|
2
|
+
import { createAuthState, createLoopbackServer, createPkcePair, getDefaultDeviceName, openBrowser, waitForEnterBeforeOpeningBrowser } from '../lib/browser-auth.js';
|
|
3
|
+
import { KiipuUserApiClient } from '../lib/kiipu-user-client.js';
|
|
4
|
+
import { logCliEvent } from '../logger/cli-logger.js';
|
|
5
|
+
async function storeAuthenticatedConfig(config, payload) {
|
|
6
|
+
config.apiKey = payload.apiKey;
|
|
7
|
+
config.keyPrefix = payload.keyPrefix ?? undefined;
|
|
8
|
+
config.authUserId = payload.userId;
|
|
9
|
+
config.authUsername = payload.username;
|
|
10
|
+
await saveKiipuConfig(config);
|
|
11
|
+
}
|
|
12
|
+
async function loginWithApiKey(config, apiKey) {
|
|
13
|
+
const client = new KiipuUserApiClient({
|
|
14
|
+
apiBaseUrl: config.apiBaseUrl,
|
|
15
|
+
apiKey,
|
|
16
|
+
});
|
|
17
|
+
const response = await client.getApiKeyMe();
|
|
18
|
+
if (!response.ok) {
|
|
19
|
+
return {
|
|
20
|
+
ok: false,
|
|
21
|
+
message: response.error.message,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
await storeAuthenticatedConfig(config, {
|
|
25
|
+
apiKey,
|
|
26
|
+
keyPrefix: response.data.keyPrefix,
|
|
27
|
+
userId: response.data.userId,
|
|
28
|
+
username: response.data.username,
|
|
29
|
+
});
|
|
30
|
+
return {
|
|
31
|
+
ok: true,
|
|
32
|
+
message: `Authenticated as ${response.data.username} (${response.data.displayName}). Key ${response.data.keyPrefix ?? 'unknown'} is now stored locally.`,
|
|
33
|
+
data: response.data,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
async function loginWithBrowser(config, input) {
|
|
37
|
+
const deviceName = input.deviceName?.trim() || getDefaultDeviceName();
|
|
38
|
+
const state = createAuthState();
|
|
39
|
+
const { verifier, challenge } = createPkcePair();
|
|
40
|
+
const server = await createLoopbackServer(state);
|
|
41
|
+
const client = new KiipuUserApiClient({
|
|
42
|
+
apiBaseUrl: config.apiBaseUrl,
|
|
43
|
+
});
|
|
44
|
+
try {
|
|
45
|
+
const session = await client.createCliAuthSession({
|
|
46
|
+
deviceName,
|
|
47
|
+
redirectUri: server.redirectUri,
|
|
48
|
+
state,
|
|
49
|
+
codeChallenge: challenge,
|
|
50
|
+
});
|
|
51
|
+
if (!session.ok) {
|
|
52
|
+
return {
|
|
53
|
+
ok: false,
|
|
54
|
+
message: session.error.message,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
console.log(`Kiipu CLI will connect this device as "${deviceName}".`);
|
|
58
|
+
console.log(`Waiting for browser login at ${session.data.authorizeUrl}`);
|
|
59
|
+
if (input.noBrowser) {
|
|
60
|
+
console.log('Open the URL above in your browser to continue.');
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
await waitForEnterBeforeOpeningBrowser();
|
|
64
|
+
const opened = openBrowser(session.data.authorizeUrl);
|
|
65
|
+
if (!opened) {
|
|
66
|
+
console.log('Could not open the browser automatically. Open this URL manually:');
|
|
67
|
+
console.log(session.data.authorizeUrl);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const callback = await server.waitForCallback(new Date(session.data.expiresAt).getTime() - Date.now());
|
|
71
|
+
const exchange = await client.exchangeCliAuthSession({
|
|
72
|
+
sessionId: session.data.sessionId,
|
|
73
|
+
authorizationCode: callback.code,
|
|
74
|
+
codeVerifier: verifier,
|
|
75
|
+
});
|
|
76
|
+
if (!exchange.ok) {
|
|
77
|
+
return {
|
|
78
|
+
ok: false,
|
|
79
|
+
message: exchange.error.message,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
await storeAuthenticatedConfig(config, {
|
|
83
|
+
apiKey: exchange.data.apiKey,
|
|
84
|
+
keyPrefix: exchange.data.keyPrefix,
|
|
85
|
+
userId: exchange.data.userId,
|
|
86
|
+
username: exchange.data.username,
|
|
87
|
+
});
|
|
88
|
+
logCliEvent('auth_browser_login_complete', {
|
|
89
|
+
username: exchange.data.username,
|
|
90
|
+
deviceName,
|
|
91
|
+
});
|
|
92
|
+
return {
|
|
93
|
+
ok: true,
|
|
94
|
+
message: `Authenticated as ${exchange.data.username} (${exchange.data.displayName}). This device is now connected to Kiipu and stored locally.`,
|
|
95
|
+
data: exchange.data,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
return {
|
|
100
|
+
ok: false,
|
|
101
|
+
message: error instanceof Error ? error.message : 'Kiipu browser login failed.',
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
await server.close().catch(() => undefined);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
3
108
|
export async function runAuthCommand(config, input) {
|
|
4
109
|
if (input.action === 'logout') {
|
|
5
110
|
delete config.apiKey;
|
|
@@ -9,14 +114,14 @@ export async function runAuthCommand(config, input) {
|
|
|
9
114
|
await saveKiipuConfig(config);
|
|
10
115
|
return {
|
|
11
116
|
ok: true,
|
|
12
|
-
message: 'Kiipu
|
|
117
|
+
message: 'Kiipu authentication was cleared from the local config. Connected devices stay revocable from the web settings page.',
|
|
13
118
|
};
|
|
14
119
|
}
|
|
15
120
|
if (input.action === 'status') {
|
|
16
121
|
if (!config.apiKey) {
|
|
17
122
|
return {
|
|
18
123
|
ok: true,
|
|
19
|
-
message: 'Kiipu CLI is not authenticated yet. Run `kiipu auth login
|
|
124
|
+
message: 'Kiipu CLI is not authenticated yet. Run `kiipu auth login` first.',
|
|
20
125
|
};
|
|
21
126
|
}
|
|
22
127
|
const client = new KiipuUserApiClient({
|
|
@@ -30,41 +135,23 @@ export async function runAuthCommand(config, input) {
|
|
|
30
135
|
message: response.error.message,
|
|
31
136
|
};
|
|
32
137
|
}
|
|
33
|
-
config
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
138
|
+
await storeAuthenticatedConfig(config, {
|
|
139
|
+
apiKey: config.apiKey,
|
|
140
|
+
keyPrefix: response.data.keyPrefix,
|
|
141
|
+
userId: response.data.userId,
|
|
142
|
+
username: response.data.username,
|
|
143
|
+
});
|
|
37
144
|
return {
|
|
38
145
|
ok: true,
|
|
39
146
|
message: `Authenticated as ${response.data.username} (${response.data.displayName}) with key ${response.data.keyPrefix ?? 'unknown'}.`,
|
|
40
147
|
data: response.data,
|
|
41
148
|
};
|
|
42
149
|
}
|
|
43
|
-
if (
|
|
44
|
-
return
|
|
45
|
-
ok: false,
|
|
46
|
-
message: 'Usage: kiipu auth login --api-key <cpk_...>',
|
|
47
|
-
};
|
|
150
|
+
if (input.apiKey) {
|
|
151
|
+
return loginWithApiKey(config, input.apiKey);
|
|
48
152
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
153
|
+
return loginWithBrowser(config, {
|
|
154
|
+
deviceName: input.deviceName,
|
|
155
|
+
noBrowser: input.noBrowser,
|
|
52
156
|
});
|
|
53
|
-
const response = await client.getApiKeyMe();
|
|
54
|
-
if (!response.ok) {
|
|
55
|
-
return {
|
|
56
|
-
ok: false,
|
|
57
|
-
message: response.error.message,
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
config.apiKey = input.apiKey;
|
|
61
|
-
config.keyPrefix = response.data.keyPrefix ?? undefined;
|
|
62
|
-
config.authUserId = response.data.userId;
|
|
63
|
-
config.authUsername = response.data.username;
|
|
64
|
-
await saveKiipuConfig(config);
|
|
65
|
-
return {
|
|
66
|
-
ok: true,
|
|
67
|
-
message: `Authenticated as ${response.data.username} (${response.data.displayName}). Key ${response.data.keyPrefix ?? 'unknown'} is now stored locally.`,
|
|
68
|
-
data: response.data,
|
|
69
|
-
};
|
|
70
157
|
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { KiipuUserApiClient } from '../lib/kiipu-user-client.js';
|
|
2
2
|
import { access } from 'node:fs/promises';
|
|
3
|
-
import {
|
|
3
|
+
import { getConfiguredApiBaseUrl, getDefaultConfigPath } from '../config/config.js';
|
|
4
4
|
import { logCliEvent } from '../logger/cli-logger.js';
|
|
5
5
|
export async function runDoctorCommand(config) {
|
|
6
6
|
logCliEvent('doctor_start');
|
|
@@ -8,13 +8,13 @@ export async function runDoctorCommand(config) {
|
|
|
8
8
|
if (!config) {
|
|
9
9
|
return {
|
|
10
10
|
ok: false,
|
|
11
|
-
message: `Missing config at ${configPath}. Run \`kiipu auth login
|
|
11
|
+
message: `Missing config at ${configPath}. Run \`kiipu auth login\` first.`,
|
|
12
12
|
};
|
|
13
13
|
}
|
|
14
14
|
const checks = [];
|
|
15
15
|
let ok = true;
|
|
16
16
|
const apiBaseUrlConfig = getConfiguredApiBaseUrl();
|
|
17
|
-
checks.push(config.apiKey ? `OK API key: ${config.keyPrefix ?? 'configured'}` : 'Missing API key. Run `kiipu auth login
|
|
17
|
+
checks.push(config.apiKey ? `OK API key: ${config.keyPrefix ?? 'configured'}` : 'Missing API key. Run `kiipu auth login`.');
|
|
18
18
|
if (!(config.apiKey || process.env.KIIPU_API_KEY)) {
|
|
19
19
|
ok = false;
|
|
20
20
|
}
|
|
@@ -22,10 +22,6 @@ export async function runDoctorCommand(config) {
|
|
|
22
22
|
checks.push(apiBaseUrlConfig.source === 'env'
|
|
23
23
|
? `WARN API base URL override: ${config.apiBaseUrl}`
|
|
24
24
|
: `OK API base URL: ${config.apiBaseUrl}`);
|
|
25
|
-
if (apiBaseUrlConfig.source === 'default' && config.apiBaseUrl !== DEFAULT_KIIPU_API_BASE_URL) {
|
|
26
|
-
ok = false;
|
|
27
|
-
checks.push(`Config mismatch: expected ${DEFAULT_KIIPU_API_BASE_URL} but loaded ${config.apiBaseUrl}. Re-run auth to refresh local config.`);
|
|
28
|
-
}
|
|
29
25
|
let apiStatus = `API unreachable at ${config.apiBaseUrl}`;
|
|
30
26
|
try {
|
|
31
27
|
const response = await fetch(`${config.apiBaseUrl}/health`);
|
package/dist/commands/help.js
CHANGED
|
@@ -59,17 +59,25 @@ export function getHelpResult(command) {
|
|
|
59
59
|
'Kiipu CLI',
|
|
60
60
|
'',
|
|
61
61
|
'Usage:',
|
|
62
|
+
' kiipu auth login',
|
|
63
|
+
' kiipu auth login --device-name "<name>"',
|
|
64
|
+
' kiipu auth login --no-browser',
|
|
62
65
|
' kiipu auth login --api-key <cpk_...>',
|
|
63
66
|
' kiipu auth status',
|
|
64
67
|
' kiipu auth logout',
|
|
65
68
|
'',
|
|
66
69
|
'Description:',
|
|
67
|
-
' Manage local Kiipu
|
|
70
|
+
' Manage local Kiipu authentication. Browser login is the default flow.',
|
|
68
71
|
'',
|
|
69
72
|
'Options:',
|
|
70
|
-
' --
|
|
73
|
+
' --device-name "<name>" Name shown under Connected Devices for browser login.',
|
|
74
|
+
' --no-browser Print the browser login URL instead of opening it automatically.',
|
|
75
|
+
' --api-key <cpk_...> Manually store an existing API key for compatibility or CI.',
|
|
71
76
|
'',
|
|
72
77
|
'Examples:',
|
|
78
|
+
' kiipu auth login',
|
|
79
|
+
' kiipu auth login --device-name "MacBook Pro"',
|
|
80
|
+
' kiipu auth login --no-browser',
|
|
73
81
|
' kiipu auth login --api-key cpk_abc123...',
|
|
74
82
|
' kiipu auth status',
|
|
75
83
|
' kiipu auth logout',
|
|
@@ -112,7 +120,7 @@ export function getHelpResult(command) {
|
|
|
112
120
|
'Core examples:',
|
|
113
121
|
' kiipu post create "Hello Kiipu"',
|
|
114
122
|
' kiipu post delete --id 123',
|
|
115
|
-
' kiipu auth login
|
|
123
|
+
' kiipu auth login',
|
|
116
124
|
' kiipu doctor',
|
|
117
125
|
' claude --plugin-dir ./packages/claude-plugin',
|
|
118
126
|
' KIIPU_API_URL=http://localhost:3001 kiipu doctor',
|
package/dist/config/config.js
CHANGED
|
@@ -25,6 +25,16 @@ export function getConfiguredApiBaseUrl() {
|
|
|
25
25
|
source: 'default',
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
+
function getFileConfiguredApiBaseUrl(raw) {
|
|
29
|
+
const value = typeof raw.apiBaseUrl === 'string' ? raw.apiBaseUrl.trim() : '';
|
|
30
|
+
if (!value) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
value,
|
|
35
|
+
source: 'file',
|
|
36
|
+
};
|
|
37
|
+
}
|
|
28
38
|
export function createDefaultConfig() {
|
|
29
39
|
return {
|
|
30
40
|
apiBaseUrl: getConfiguredApiBaseUrl().value,
|
|
@@ -37,6 +47,11 @@ export async function loadKiipuConfig(configPath = getDefaultConfigPath()) {
|
|
|
37
47
|
const content = await readFile(configPath, 'utf8');
|
|
38
48
|
const raw = JSON.parse(content);
|
|
39
49
|
const nextConfig = createDefaultConfig();
|
|
50
|
+
const envConfiguredApiBaseUrl = process.env.KIIPU_API_URL?.trim();
|
|
51
|
+
const fileConfiguredApiBaseUrl = getFileConfiguredApiBaseUrl(raw);
|
|
52
|
+
if (!envConfiguredApiBaseUrl && fileConfiguredApiBaseUrl) {
|
|
53
|
+
nextConfig.apiBaseUrl = fileConfiguredApiBaseUrl.value;
|
|
54
|
+
}
|
|
40
55
|
if (typeof raw.apiKey === 'string') {
|
|
41
56
|
nextConfig.apiKey = raw.apiKey;
|
|
42
57
|
}
|
package/dist/index.js
CHANGED
|
@@ -26,7 +26,7 @@ async function main() {
|
|
|
26
26
|
const wantsHelp = hasFlag(normalizedArgs, '--help') || hasFlag(normalizedArgs, '-h');
|
|
27
27
|
const wantsVersion = hasFlag(normalizedArgs, '--version') || hasFlag(normalizedArgs, '-v');
|
|
28
28
|
const positionalArgs = normalizedArgs.filter((arg, index, all) => {
|
|
29
|
-
if (arg === '--json' || arg === '--help' || arg === '-h' || arg === '--version' || arg === '-v')
|
|
29
|
+
if (arg === '--json' || arg === '--help' || arg === '-h' || arg === '--version' || arg === '-v' || arg === '--no-browser')
|
|
30
30
|
return false;
|
|
31
31
|
const prev = all[index - 1];
|
|
32
32
|
return (prev !== '--scheduled-at' &&
|
|
@@ -36,6 +36,7 @@ async function main() {
|
|
|
36
36
|
prev !== '--content' &&
|
|
37
37
|
prev !== '--id' &&
|
|
38
38
|
prev !== '--api-key' &&
|
|
39
|
+
prev !== '--device-name' &&
|
|
39
40
|
prev !== '--config-path' &&
|
|
40
41
|
prev !== '--wrapper-path' &&
|
|
41
42
|
prev !== '--conversation-id' &&
|
|
@@ -88,6 +89,8 @@ async function main() {
|
|
|
88
89
|
result = await runAuthCommand(config, {
|
|
89
90
|
action,
|
|
90
91
|
apiKey: readFlag(normalizedArgs, '--api-key'),
|
|
92
|
+
deviceName: readFlag(normalizedArgs, '--device-name'),
|
|
93
|
+
noBrowser: hasFlag(normalizedArgs, '--no-browser'),
|
|
91
94
|
});
|
|
92
95
|
return printResult(result, asJson);
|
|
93
96
|
}
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
+
import { createServer } from 'node:http';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import readline from 'node:readline/promises';
|
|
6
|
+
import { logCliEvent } from '../logger/cli-logger.js';
|
|
7
|
+
function toBase64Url(buffer) {
|
|
8
|
+
return buffer.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
|
9
|
+
}
|
|
10
|
+
function createRandomToken(size = 32) {
|
|
11
|
+
return toBase64Url(randomBytes(size));
|
|
12
|
+
}
|
|
13
|
+
export function createPkcePair() {
|
|
14
|
+
const verifier = createRandomToken(48);
|
|
15
|
+
const challenge = toBase64Url(createHash('sha256').update(verifier).digest());
|
|
16
|
+
return {
|
|
17
|
+
verifier,
|
|
18
|
+
challenge,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function createAuthState() {
|
|
22
|
+
return createRandomToken(24);
|
|
23
|
+
}
|
|
24
|
+
export function getDefaultDeviceName() {
|
|
25
|
+
return os.hostname();
|
|
26
|
+
}
|
|
27
|
+
export async function createLoopbackServer(expectedState) {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
let timeout = null;
|
|
30
|
+
let callbackResolver = null;
|
|
31
|
+
let callbackRejecter = null;
|
|
32
|
+
let pendingValue = null;
|
|
33
|
+
let pendingError = null;
|
|
34
|
+
function renderPage(input) {
|
|
35
|
+
const tone = input.tone ?? 'success';
|
|
36
|
+
const accent = tone === 'success' ? '#d56c47' : '#c2410c';
|
|
37
|
+
const accentSoft = tone === 'success' ? 'rgba(213, 108, 71, 0.16)' : 'rgba(194, 65, 12, 0.14)';
|
|
38
|
+
const badgeText = tone === 'success' ? 'CLI Connected' : 'Connection Error';
|
|
39
|
+
return `<!doctype html>
|
|
40
|
+
<html lang="en">
|
|
41
|
+
<head>
|
|
42
|
+
<meta charset="utf-8" />
|
|
43
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
44
|
+
<title>${input.title}</title>
|
|
45
|
+
<style>
|
|
46
|
+
:root {
|
|
47
|
+
color-scheme: light;
|
|
48
|
+
--bg: #f7f1ec;
|
|
49
|
+
--card: rgba(255, 255, 255, 0.88);
|
|
50
|
+
--text: #201714;
|
|
51
|
+
--muted: rgba(32, 23, 20, 0.64);
|
|
52
|
+
--border: rgba(32, 23, 20, 0.08);
|
|
53
|
+
--accent: ${accent};
|
|
54
|
+
--accent-soft: ${accentSoft};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
* {
|
|
58
|
+
box-sizing: border-box;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
body {
|
|
62
|
+
margin: 0;
|
|
63
|
+
min-height: 100vh;
|
|
64
|
+
display: flex;
|
|
65
|
+
align-items: center;
|
|
66
|
+
justify-content: center;
|
|
67
|
+
padding: 24px;
|
|
68
|
+
font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", sans-serif;
|
|
69
|
+
background:
|
|
70
|
+
radial-gradient(circle at top left, rgba(213, 108, 71, 0.18), transparent 30%),
|
|
71
|
+
linear-gradient(135deg, #fcf8f5 0%, #f5ece5 100%);
|
|
72
|
+
color: var(--text);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.shell {
|
|
76
|
+
width: min(100%, 560px);
|
|
77
|
+
border: 1px solid var(--border);
|
|
78
|
+
border-radius: 28px;
|
|
79
|
+
background: var(--card);
|
|
80
|
+
backdrop-filter: blur(14px);
|
|
81
|
+
box-shadow:
|
|
82
|
+
0 20px 60px rgba(61, 33, 20, 0.10),
|
|
83
|
+
inset 0 1px 0 rgba(255, 255, 255, 0.55);
|
|
84
|
+
overflow: hidden;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.hero {
|
|
88
|
+
padding: 28px 28px 18px;
|
|
89
|
+
background:
|
|
90
|
+
radial-gradient(circle at top left, var(--accent-soft), transparent 42%),
|
|
91
|
+
linear-gradient(180deg, rgba(255,255,255,0.75), rgba(255,255,255,0.4));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.badge {
|
|
95
|
+
display: inline-flex;
|
|
96
|
+
align-items: center;
|
|
97
|
+
gap: 8px;
|
|
98
|
+
padding: 8px 12px;
|
|
99
|
+
border-radius: 999px;
|
|
100
|
+
background: var(--accent-soft);
|
|
101
|
+
color: var(--accent);
|
|
102
|
+
font-size: 12px;
|
|
103
|
+
font-weight: 700;
|
|
104
|
+
letter-spacing: 0.08em;
|
|
105
|
+
text-transform: uppercase;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.dot {
|
|
109
|
+
width: 8px;
|
|
110
|
+
height: 8px;
|
|
111
|
+
border-radius: 999px;
|
|
112
|
+
background: currentColor;
|
|
113
|
+
box-shadow: 0 0 0 6px rgba(255, 255, 255, 0.55);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.content {
|
|
117
|
+
padding: 8px 28px 28px;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
h1 {
|
|
121
|
+
margin: 18px 0 10px;
|
|
122
|
+
font-size: clamp(28px, 5vw, 38px);
|
|
123
|
+
line-height: 1.04;
|
|
124
|
+
letter-spacing: -0.04em;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
p {
|
|
128
|
+
margin: 0;
|
|
129
|
+
color: var(--muted);
|
|
130
|
+
font-size: 15px;
|
|
131
|
+
line-height: 1.7;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.panel {
|
|
135
|
+
margin-top: 22px;
|
|
136
|
+
display: grid;
|
|
137
|
+
gap: 12px;
|
|
138
|
+
border: 1px solid var(--border);
|
|
139
|
+
border-radius: 22px;
|
|
140
|
+
background: rgba(255, 255, 255, 0.72);
|
|
141
|
+
padding: 16px 18px;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.panel strong {
|
|
145
|
+
display: block;
|
|
146
|
+
font-size: 14px;
|
|
147
|
+
color: var(--text);
|
|
148
|
+
margin-bottom: 4px;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.footer {
|
|
152
|
+
margin-top: 16px;
|
|
153
|
+
font-size: 13px;
|
|
154
|
+
color: rgba(32, 23, 20, 0.48);
|
|
155
|
+
}
|
|
156
|
+
</style>
|
|
157
|
+
</head>
|
|
158
|
+
<body>
|
|
159
|
+
<main class="shell">
|
|
160
|
+
<section class="hero">
|
|
161
|
+
<div class="badge"><span class="dot"></span>${badgeText}</div>
|
|
162
|
+
<h1>${input.title}</h1>
|
|
163
|
+
</section>
|
|
164
|
+
<section class="content">
|
|
165
|
+
<p>${input.body}</p>
|
|
166
|
+
<div class="panel">
|
|
167
|
+
<div>
|
|
168
|
+
<strong>What happens next</strong>
|
|
169
|
+
Return to your terminal and Kiipu will finish connecting this device automatically.
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
<p class="footer">This tab is only used to complete the local Kiipu CLI sign-in flow.</p>
|
|
173
|
+
</section>
|
|
174
|
+
</main>
|
|
175
|
+
</body>
|
|
176
|
+
</html>`;
|
|
177
|
+
}
|
|
178
|
+
function respond(response, status, body) {
|
|
179
|
+
response.writeHead(status, {
|
|
180
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
181
|
+
});
|
|
182
|
+
response.end(body);
|
|
183
|
+
}
|
|
184
|
+
function done(error, value) {
|
|
185
|
+
if (timeout) {
|
|
186
|
+
clearTimeout(timeout);
|
|
187
|
+
timeout = null;
|
|
188
|
+
}
|
|
189
|
+
if (error) {
|
|
190
|
+
if (callbackRejecter) {
|
|
191
|
+
callbackRejecter(error);
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
pendingError = error;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
else if (value) {
|
|
198
|
+
if (callbackResolver) {
|
|
199
|
+
callbackResolver(value);
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
pendingValue = value;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const server = createServer((request, response) => {
|
|
207
|
+
const url = new URL(request.url ?? '/', 'http://127.0.0.1');
|
|
208
|
+
if (url.pathname !== '/callback') {
|
|
209
|
+
respond(response, 404, '<h1>Not found</h1>');
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const code = url.searchParams.get('code')?.trim() ?? '';
|
|
213
|
+
const state = url.searchParams.get('state')?.trim() ?? '';
|
|
214
|
+
if (!code || !state) {
|
|
215
|
+
respond(response, 400, renderPage({
|
|
216
|
+
title: 'Missing login parameters',
|
|
217
|
+
body: 'Kiipu could not complete this local sign-in because the callback was missing required details. Close this tab and run `kiipu auth login` again.',
|
|
218
|
+
tone: 'error',
|
|
219
|
+
}));
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (state !== expectedState) {
|
|
223
|
+
respond(response, 400, renderPage({
|
|
224
|
+
title: 'This login link is no longer valid',
|
|
225
|
+
body: 'The browser callback did not match the active Kiipu CLI session. Start a fresh `kiipu auth login` command and try again.',
|
|
226
|
+
tone: 'error',
|
|
227
|
+
}));
|
|
228
|
+
done(new Error('CLI login callback state did not match the expected request.'));
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
respond(response, 200, renderPage({
|
|
232
|
+
title: 'Kiipu CLI connected',
|
|
233
|
+
body: 'This browser step is complete. You can close this tab and return to your terminal.',
|
|
234
|
+
tone: 'success',
|
|
235
|
+
}));
|
|
236
|
+
done(undefined, { code, state });
|
|
237
|
+
});
|
|
238
|
+
server.once('error', reject);
|
|
239
|
+
server.listen(0, '127.0.0.1', () => {
|
|
240
|
+
const address = server.address();
|
|
241
|
+
if (!address || typeof address === 'string') {
|
|
242
|
+
reject(new Error('Failed to determine the local CLI callback port.'));
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
resolve({
|
|
246
|
+
redirectUri: `http://127.0.0.1:${address.port}/callback`,
|
|
247
|
+
waitForCallback(timeoutMs) {
|
|
248
|
+
return new Promise((innerResolve, innerReject) => {
|
|
249
|
+
if (pendingError) {
|
|
250
|
+
const error = pendingError;
|
|
251
|
+
pendingError = null;
|
|
252
|
+
innerReject(error);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (pendingValue) {
|
|
256
|
+
const value = pendingValue;
|
|
257
|
+
pendingValue = null;
|
|
258
|
+
innerResolve(value);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
callbackResolver = innerResolve;
|
|
262
|
+
callbackRejecter = innerReject;
|
|
263
|
+
timeout = setTimeout(() => {
|
|
264
|
+
innerReject(new Error('Timed out waiting for browser login to complete.'));
|
|
265
|
+
}, timeoutMs);
|
|
266
|
+
});
|
|
267
|
+
},
|
|
268
|
+
close() {
|
|
269
|
+
return new Promise((innerResolve, innerReject) => {
|
|
270
|
+
server.close((error) => {
|
|
271
|
+
if (error) {
|
|
272
|
+
innerReject(error);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
innerResolve();
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
export async function waitForEnterBeforeOpeningBrowser() {
|
|
284
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const rl = readline.createInterface({
|
|
288
|
+
input: process.stdin,
|
|
289
|
+
output: process.stdout,
|
|
290
|
+
});
|
|
291
|
+
try {
|
|
292
|
+
await rl.question('Press Enter to open the browser and continue login...');
|
|
293
|
+
}
|
|
294
|
+
finally {
|
|
295
|
+
rl.close();
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
export function openBrowser(url) {
|
|
299
|
+
const platform = process.platform;
|
|
300
|
+
const command = platform === 'darwin'
|
|
301
|
+
? { file: 'open', args: [url] }
|
|
302
|
+
: platform === 'win32'
|
|
303
|
+
? { file: 'cmd', args: ['/c', 'start', '', url] }
|
|
304
|
+
: { file: 'xdg-open', args: [url] };
|
|
305
|
+
try {
|
|
306
|
+
const child = spawn(command.file, command.args, {
|
|
307
|
+
detached: true,
|
|
308
|
+
stdio: 'ignore',
|
|
309
|
+
});
|
|
310
|
+
child.unref();
|
|
311
|
+
logCliEvent('auth_browser_opened', {
|
|
312
|
+
platform,
|
|
313
|
+
});
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
@@ -19,7 +19,7 @@ export class KiipuUserApiClient {
|
|
|
19
19
|
...init,
|
|
20
20
|
headers: {
|
|
21
21
|
'Content-Type': 'application/json',
|
|
22
|
-
Authorization: `Bearer ${this.config.apiKey}
|
|
22
|
+
...(this.config.apiKey ? { Authorization: `Bearer ${this.config.apiKey}` } : {}),
|
|
23
23
|
...(init?.headers ?? {}),
|
|
24
24
|
},
|
|
25
25
|
});
|
|
@@ -47,4 +47,16 @@ export class KiipuUserApiClient {
|
|
|
47
47
|
getApiKeyMe() {
|
|
48
48
|
return this.request('/auth/api-key/me');
|
|
49
49
|
}
|
|
50
|
+
createCliAuthSession(input) {
|
|
51
|
+
return this.request('/auth/cli/sessions', {
|
|
52
|
+
method: 'POST',
|
|
53
|
+
body: JSON.stringify(input),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
exchangeCliAuthSession(input) {
|
|
57
|
+
return this.request('/auth/cli/exchange', {
|
|
58
|
+
method: 'POST',
|
|
59
|
+
body: JSON.stringify(input),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
50
62
|
}
|
package/dist/lib/post-actions.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kiipu/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "Kiipu CLI for local authentication, doctor checks, and direct post actions.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"release:major": "npm version major --no-git-tag-version",
|
|
41
41
|
"pack:local": "node scripts/pack-local.mjs",
|
|
42
42
|
"verify:package": "node scripts/verify-package.mjs",
|
|
43
|
-
"publish:dry-run": "npm publish --dry-run"
|
|
43
|
+
"publish:dry-run": "node ../../infra/scripts/prepare-cli-release.mjs && npm publish ../../.release/cli-package --dry-run"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {},
|
|
46
46
|
"devDependencies": {
|