@devbrevity/note-cli 1.0.0 → 1.1.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 +27 -2
- package/bin/note.mjs +4 -2
- package/package.json +2 -2
- package/src/auth/cognitoLogin.mjs +210 -170
- package/src/commands/import.mjs +80 -0
- package/src/commands/list.mjs +1 -1
package/README.md
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
# @devbrevity/note-cli
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@devbrevity/note-cli)
|
|
4
|
+
|
|
3
5
|
A terminal-based CLI for [LifeBrevity](https://www.lifebrevity.com) journal entries. Create, list, search, and manage your notes from the command line.
|
|
4
6
|
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- [Node.js](https://nodejs.org/) 18 or later
|
|
10
|
+
- An active [LifeBrevity](https://www.lifebrevity.com) subscription
|
|
11
|
+
- Register for an account at [lifebrevity.com/register](https://www.lifebrevity.com/register)
|
|
12
|
+
|
|
5
13
|
## Install
|
|
6
14
|
|
|
7
15
|
```bash
|
|
8
16
|
npm install -g @devbrevity/note-cli
|
|
9
17
|
```
|
|
10
18
|
|
|
11
|
-
Requires Node.js 18+ and an active [LifeBrevity](https://www.lifebrevity.com) subscription.
|
|
12
|
-
|
|
13
19
|
## Quick Start
|
|
14
20
|
|
|
15
21
|
```bash
|
|
@@ -93,6 +99,14 @@ Show current user, token expiry, and config location.
|
|
|
93
99
|
|
|
94
100
|
Clear all stored credentials.
|
|
95
101
|
|
|
102
|
+
### `note --help`
|
|
103
|
+
|
|
104
|
+
Show all available commands and options.
|
|
105
|
+
|
|
106
|
+
### `note --version`
|
|
107
|
+
|
|
108
|
+
Show the installed version.
|
|
109
|
+
|
|
96
110
|
## Authentication
|
|
97
111
|
|
|
98
112
|
The CLI uses browser-based OAuth (same as the LifeBrevity web app). When you run `note login`, your browser opens to the LifeBrevity sign-in page. After authenticating, you're redirected back and the CLI stores your session locally.
|
|
@@ -101,6 +115,17 @@ The CLI uses browser-based OAuth (same as the LifeBrevity web app). When you run
|
|
|
101
115
|
- **Session duration:** 7 days
|
|
102
116
|
- **API key:** Provided automatically at login
|
|
103
117
|
|
|
118
|
+
## Uninstall
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
npm uninstall -g @devbrevity/note-cli
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Links
|
|
125
|
+
|
|
126
|
+
- [npm package](https://www.npmjs.com/package/@devbrevity/note-cli)
|
|
127
|
+
- [LifeBrevity](https://www.lifebrevity.com)
|
|
128
|
+
|
|
104
129
|
## License
|
|
105
130
|
|
|
106
131
|
MIT
|
package/bin/note.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { program } from 'commander';
|
|
4
4
|
import { loginCommand } from '../src/commands/login.mjs';
|
|
@@ -8,11 +8,12 @@ import { getCommand } from '../src/commands/get.mjs';
|
|
|
8
8
|
import { searchCommand } from '../src/commands/search.mjs';
|
|
9
9
|
import { logoutCommand } from '../src/commands/logout.mjs';
|
|
10
10
|
import { whoamiCommand } from '../src/commands/whoami.mjs';
|
|
11
|
+
import { importCommand } from '../src/commands/import.mjs';
|
|
11
12
|
|
|
12
13
|
program
|
|
13
14
|
.name('note')
|
|
14
15
|
.description('notesBrevity - Journal from your terminal')
|
|
15
|
-
.version('1.
|
|
16
|
+
.version('1.1.0');
|
|
16
17
|
|
|
17
18
|
program.addCommand(loginCommand);
|
|
18
19
|
program.addCommand(createCommand);
|
|
@@ -21,5 +22,6 @@ program.addCommand(getCommand);
|
|
|
21
22
|
program.addCommand(searchCommand);
|
|
22
23
|
program.addCommand(logoutCommand);
|
|
23
24
|
program.addCommand(whoamiCommand);
|
|
25
|
+
program.addCommand(importCommand);
|
|
24
26
|
|
|
25
27
|
program.parse();
|
package/package.json
CHANGED
|
@@ -1,170 +1,210 @@
|
|
|
1
|
-
import { createServer } from 'http';
|
|
2
|
-
import { randomBytes } from 'crypto';
|
|
3
|
-
import { URL } from 'url';
|
|
4
|
-
import open from 'open';
|
|
5
|
-
import { CONFIG } from '../config.mjs';
|
|
6
|
-
import { saveAuth } from './tokenStore.mjs';
|
|
7
|
-
|
|
8
|
-
const TIMEOUT_MS = 120_000; // 2 minutes
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Browser-based Cognito login flow.
|
|
12
|
-
* Opens the Cognito Hosted UI, catches the callback on a local server,
|
|
13
|
-
* exchanges the auth code via LifeBrevity backend for a local JWT.
|
|
14
|
-
*/
|
|
15
|
-
export async function cognitoLogin() {
|
|
16
|
-
const state = randomBytes(32).toString('hex');
|
|
17
|
-
const redirectUri = `http://localhost:${CONFIG.CALLBACK_PORT}${CONFIG.CALLBACK_PATH}`;
|
|
18
|
-
|
|
19
|
-
const authorizeUrl = new URL(`https://${CONFIG.COGNITO_DOMAIN}/oauth2/authorize`);
|
|
20
|
-
authorizeUrl.searchParams.set('response_type', 'code');
|
|
21
|
-
authorizeUrl.searchParams.set('client_id', CONFIG.COGNITO_CLIENT_ID);
|
|
22
|
-
authorizeUrl.searchParams.set('redirect_uri', redirectUri);
|
|
23
|
-
authorizeUrl.searchParams.set('scope', CONFIG.COGNITO_SCOPES);
|
|
24
|
-
authorizeUrl.searchParams.set('state', state);
|
|
25
|
-
|
|
26
|
-
return new Promise((resolve, reject) => {
|
|
27
|
-
let timeout;
|
|
28
|
-
|
|
29
|
-
const server = createServer(async (req, res) => {
|
|
30
|
-
try {
|
|
31
|
-
const url = new URL(req.url, `http://localhost:${CONFIG.CALLBACK_PORT}`);
|
|
32
|
-
|
|
33
|
-
if (url.pathname !== CONFIG.CALLBACK_PATH) {
|
|
34
|
-
res.writeHead(404);
|
|
35
|
-
res.end('Not found');
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const code = url.searchParams.get('code');
|
|
40
|
-
const returnedState = url.searchParams.get('state');
|
|
41
|
-
const error = url.searchParams.get('error');
|
|
42
|
-
|
|
43
|
-
if (error) {
|
|
44
|
-
const errorDesc = url.searchParams.get('error_description') || error;
|
|
45
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
46
|
-
res.end(errorPage(errorDesc));
|
|
47
|
-
cleanup();
|
|
48
|
-
reject(new Error(`Login failed: ${errorDesc}`));
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (returnedState !== state) {
|
|
53
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
54
|
-
res.end(errorPage('Security validation failed (state mismatch). Please try again.'));
|
|
55
|
-
cleanup();
|
|
56
|
-
reject(new Error('State mismatch - possible CSRF attack'));
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (!code) {
|
|
61
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
62
|
-
res.end(errorPage('No authorization code received.'));
|
|
63
|
-
cleanup();
|
|
64
|
-
reject(new Error('No authorization code in callback'));
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Exchange the code for a local JWT via LifeBrevity backend
|
|
69
|
-
const tokenResult = await exchangeCodeForToken(code, redirectUri);
|
|
70
|
-
|
|
71
|
-
// Save the token and API key (if returned by backend)
|
|
72
|
-
saveAuth({
|
|
73
|
-
token: tokenResult.token,
|
|
74
|
-
email: tokenResult.user.email,
|
|
75
|
-
apiKey: tokenResult.apiKeys?.notesBrevity,
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
79
|
-
res.end(successPage(tokenResult.user.email));
|
|
80
|
-
cleanup();
|
|
81
|
-
resolve(tokenResult);
|
|
82
|
-
} catch (err) {
|
|
83
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
84
|
-
res.end(errorPage(err.message));
|
|
85
|
-
cleanup();
|
|
86
|
-
reject(err);
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
function cleanup() {
|
|
91
|
-
clearTimeout(timeout);
|
|
92
|
-
server.close();
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
server.on('error', (err) => {
|
|
96
|
-
if (err.code === 'EADDRINUSE') {
|
|
97
|
-
reject(new Error(
|
|
98
|
-
`Port ${CONFIG.CALLBACK_PORT} is already in use. ` +
|
|
99
|
-
`Close the process using it and try again.`
|
|
100
|
-
));
|
|
101
|
-
} else {
|
|
102
|
-
reject(err);
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
server.listen(CONFIG.CALLBACK_PORT, async () => {
|
|
107
|
-
// Open the browser
|
|
108
|
-
await open(authorizeUrl.toString());
|
|
109
|
-
|
|
110
|
-
timeout = setTimeout(() => {
|
|
111
|
-
cleanup();
|
|
112
|
-
reject(new Error('Login timed out after 2 minutes. Please try again.'));
|
|
113
|
-
}, TIMEOUT_MS);
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Exchange the Cognito authorization code for a local JWT
|
|
120
|
-
* via the LifeBrevity backend's /api/oidc/callback endpoint.
|
|
121
|
-
*/
|
|
122
|
-
async function exchangeCodeForToken(code, redirectUri) {
|
|
123
|
-
const response = await fetch(`${CONFIG.LIFEBREVITY_BACKEND}/api/oidc/callback`, {
|
|
124
|
-
method: 'POST',
|
|
125
|
-
headers: { 'Content-Type': 'application/json' },
|
|
126
|
-
body: JSON.stringify({ code, redirectUri }),
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
const data = await response.json();
|
|
130
|
-
|
|
131
|
-
if (!response.ok || !data.success) {
|
|
132
|
-
if (data.code === 'SubscriptionRequired') {
|
|
133
|
-
throw new Error(
|
|
134
|
-
'Active LifeBrevity subscription required.\n' +
|
|
135
|
-
'Subscribe at: https://www.lifebrevity.com/subscribe'
|
|
136
|
-
);
|
|
137
|
-
}
|
|
138
|
-
throw new Error(data.message || `Authentication failed (HTTP ${response.status})`);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return data;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
<
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
1
|
+
import { createServer } from 'http';
|
|
2
|
+
import { randomBytes } from 'crypto';
|
|
3
|
+
import { URL } from 'url';
|
|
4
|
+
import open from 'open';
|
|
5
|
+
import { CONFIG } from '../config.mjs';
|
|
6
|
+
import { saveAuth } from './tokenStore.mjs';
|
|
7
|
+
|
|
8
|
+
const TIMEOUT_MS = 120_000; // 2 minutes
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Browser-based Cognito login flow.
|
|
12
|
+
* Opens the Cognito Hosted UI, catches the callback on a local server,
|
|
13
|
+
* exchanges the auth code via LifeBrevity backend for a local JWT.
|
|
14
|
+
*/
|
|
15
|
+
export async function cognitoLogin() {
|
|
16
|
+
const state = randomBytes(32).toString('hex');
|
|
17
|
+
const redirectUri = `http://localhost:${CONFIG.CALLBACK_PORT}${CONFIG.CALLBACK_PATH}`;
|
|
18
|
+
|
|
19
|
+
const authorizeUrl = new URL(`https://${CONFIG.COGNITO_DOMAIN}/oauth2/authorize`);
|
|
20
|
+
authorizeUrl.searchParams.set('response_type', 'code');
|
|
21
|
+
authorizeUrl.searchParams.set('client_id', CONFIG.COGNITO_CLIENT_ID);
|
|
22
|
+
authorizeUrl.searchParams.set('redirect_uri', redirectUri);
|
|
23
|
+
authorizeUrl.searchParams.set('scope', CONFIG.COGNITO_SCOPES);
|
|
24
|
+
authorizeUrl.searchParams.set('state', state);
|
|
25
|
+
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
let timeout;
|
|
28
|
+
|
|
29
|
+
const server = createServer(async (req, res) => {
|
|
30
|
+
try {
|
|
31
|
+
const url = new URL(req.url, `http://localhost:${CONFIG.CALLBACK_PORT}`);
|
|
32
|
+
|
|
33
|
+
if (url.pathname !== CONFIG.CALLBACK_PATH) {
|
|
34
|
+
res.writeHead(404);
|
|
35
|
+
res.end('Not found');
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const code = url.searchParams.get('code');
|
|
40
|
+
const returnedState = url.searchParams.get('state');
|
|
41
|
+
const error = url.searchParams.get('error');
|
|
42
|
+
|
|
43
|
+
if (error) {
|
|
44
|
+
const errorDesc = url.searchParams.get('error_description') || error;
|
|
45
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
46
|
+
res.end(errorPage(errorDesc));
|
|
47
|
+
cleanup();
|
|
48
|
+
reject(new Error(`Login failed: ${errorDesc}`));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (returnedState !== state) {
|
|
53
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
54
|
+
res.end(errorPage('Security validation failed (state mismatch). Please try again.'));
|
|
55
|
+
cleanup();
|
|
56
|
+
reject(new Error('State mismatch - possible CSRF attack'));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!code) {
|
|
61
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
62
|
+
res.end(errorPage('No authorization code received.'));
|
|
63
|
+
cleanup();
|
|
64
|
+
reject(new Error('No authorization code in callback'));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Exchange the code for a local JWT via LifeBrevity backend
|
|
69
|
+
const tokenResult = await exchangeCodeForToken(code, redirectUri);
|
|
70
|
+
|
|
71
|
+
// Save the token and API key (if returned by backend)
|
|
72
|
+
saveAuth({
|
|
73
|
+
token: tokenResult.token,
|
|
74
|
+
email: tokenResult.user.email,
|
|
75
|
+
apiKey: tokenResult.apiKeys?.notesBrevity,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
79
|
+
res.end(successPage(tokenResult.user.email));
|
|
80
|
+
cleanup();
|
|
81
|
+
resolve(tokenResult);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
84
|
+
res.end(errorPage(err.message));
|
|
85
|
+
cleanup();
|
|
86
|
+
reject(err);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
function cleanup() {
|
|
91
|
+
clearTimeout(timeout);
|
|
92
|
+
server.close();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
server.on('error', (err) => {
|
|
96
|
+
if (err.code === 'EADDRINUSE') {
|
|
97
|
+
reject(new Error(
|
|
98
|
+
`Port ${CONFIG.CALLBACK_PORT} is already in use. ` +
|
|
99
|
+
`Close the process using it and try again.`
|
|
100
|
+
));
|
|
101
|
+
} else {
|
|
102
|
+
reject(err);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
server.listen(CONFIG.CALLBACK_PORT, async () => {
|
|
107
|
+
// Open the browser
|
|
108
|
+
await open(authorizeUrl.toString());
|
|
109
|
+
|
|
110
|
+
timeout = setTimeout(() => {
|
|
111
|
+
cleanup();
|
|
112
|
+
reject(new Error('Login timed out after 2 minutes. Please try again.'));
|
|
113
|
+
}, TIMEOUT_MS);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Exchange the Cognito authorization code for a local JWT
|
|
120
|
+
* via the LifeBrevity backend's /api/oidc/callback endpoint.
|
|
121
|
+
*/
|
|
122
|
+
async function exchangeCodeForToken(code, redirectUri) {
|
|
123
|
+
const response = await fetch(`${CONFIG.LIFEBREVITY_BACKEND}/api/oidc/callback`, {
|
|
124
|
+
method: 'POST',
|
|
125
|
+
headers: { 'Content-Type': 'application/json' },
|
|
126
|
+
body: JSON.stringify({ code, redirectUri }),
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const data = await response.json();
|
|
130
|
+
|
|
131
|
+
if (!response.ok || !data.success) {
|
|
132
|
+
if (data.code === 'SubscriptionRequired') {
|
|
133
|
+
throw new Error(
|
|
134
|
+
'Active LifeBrevity subscription required.\n' +
|
|
135
|
+
'Subscribe at: https://www.lifebrevity.com/subscribe'
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
throw new Error(data.message || `Authentication failed (HTTP ${response.status})`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return data;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const PAGE_STYLES = `
|
|
145
|
+
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
|
|
146
|
+
<style>
|
|
147
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
148
|
+
body {
|
|
149
|
+
font-family: 'Roboto', 'Segoe UI', -apple-system, sans-serif;
|
|
150
|
+
display: flex; justify-content: center; align-items: center;
|
|
151
|
+
height: 100vh; background: #f5f5f5;
|
|
152
|
+
}
|
|
153
|
+
.card {
|
|
154
|
+
text-align: center; padding: 48px 40px; background: white;
|
|
155
|
+
border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
|
156
|
+
max-width: 420px; width: 90%;
|
|
157
|
+
animation: fadeInUp 0.8s ease-out;
|
|
158
|
+
}
|
|
159
|
+
.subtitle { color: #999; font-size: 12px; font-weight: 300; letter-spacing: 1px; text-transform: uppercase; margin-bottom: 24px; }
|
|
160
|
+
.divider { width: 40px; height: 2px; background: #2A6A88; margin: 0 auto 24px; }
|
|
161
|
+
.status { font-size: 22px; font-weight: 500; margin-bottom: 8px; }
|
|
162
|
+
.status.success { color: #2A6A88; }
|
|
163
|
+
.status.error { color: #FF5252; }
|
|
164
|
+
.detail { color: #666; font-size: 16px; margin-bottom: 4px; }
|
|
165
|
+
.hint { color: #999; font-size: 14px; margin-top: 20px; }
|
|
166
|
+
.hint code { background: #f0f0f0; padding: 2px 6px; border-radius: 3px; font-size: 13px; }
|
|
167
|
+
.countdown { color: #bbb; font-size: 12px; margin-top: 12px; }
|
|
168
|
+
@keyframes fadeInUp {
|
|
169
|
+
from { opacity: 0; transform: translateY(20px); }
|
|
170
|
+
to { opacity: 1; transform: translateY(0); }
|
|
171
|
+
}
|
|
172
|
+
</style>`;
|
|
173
|
+
|
|
174
|
+
function successPage(email) {
|
|
175
|
+
return `<!DOCTYPE html>
|
|
176
|
+
<html>
|
|
177
|
+
<head><title>LifeBrevity - Login Successful</title><link rel="icon" href="data:,">${PAGE_STYLES}</head>
|
|
178
|
+
<body>
|
|
179
|
+
<div class="card">
|
|
180
|
+
<div class="subtitle">note CLI</div>
|
|
181
|
+
<div class="divider"></div>
|
|
182
|
+
<div class="status success">Login Successful</div>
|
|
183
|
+
<p class="detail">Signed in as <strong>${email}</strong></p>
|
|
184
|
+
<p class="hint">You can close this window and return to your terminal.</p>
|
|
185
|
+
<p class="countdown" id="countdown">This window will close in 3 seconds...</p>
|
|
186
|
+
</div>
|
|
187
|
+
<script>
|
|
188
|
+
let s = 3;
|
|
189
|
+
const el = document.getElementById('countdown');
|
|
190
|
+
const t = setInterval(() => { if (--s <= 0) { clearInterval(t); window.close(); } el.textContent = 'This window will close in ' + s + ' seconds...'; }, 1000);
|
|
191
|
+
</script>
|
|
192
|
+
</body>
|
|
193
|
+
</html>`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function errorPage(message) {
|
|
197
|
+
return `<!DOCTYPE html>
|
|
198
|
+
<html>
|
|
199
|
+
<head><title>LifeBrevity - Login Failed</title><link rel="icon" href="data:,">${PAGE_STYLES}</head>
|
|
200
|
+
<body>
|
|
201
|
+
<div class="card">
|
|
202
|
+
<div class="subtitle">note CLI</div>
|
|
203
|
+
<div class="divider"></div>
|
|
204
|
+
<div class="status error">Login Failed</div>
|
|
205
|
+
<p class="detail">${message}</p>
|
|
206
|
+
<p class="hint">Close this window and try <code>note login</code> again.</p>
|
|
207
|
+
</div>
|
|
208
|
+
</body>
|
|
209
|
+
</html>`;
|
|
210
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { readFileSync, existsSync } from 'fs';
|
|
5
|
+
import { resolve, basename, extname } from 'path';
|
|
6
|
+
import { apiRequest } from '../auth/apiClient.mjs';
|
|
7
|
+
|
|
8
|
+
const SUPPORTED_EXTENSIONS = ['.md', '.txt'];
|
|
9
|
+
|
|
10
|
+
export const importCommand = new Command('import')
|
|
11
|
+
.description('Import a markdown or text file as a journal entry')
|
|
12
|
+
.argument('<file>', 'Path to a .md or .txt file')
|
|
13
|
+
.option('-t, --title <title>', 'Entry title (defaults to filename)')
|
|
14
|
+
.option('-s, --summarize', 'Generate AI summary')
|
|
15
|
+
.option('--tags <tags>', 'Comma-separated tags (e.g. "work,ideas")')
|
|
16
|
+
.action(async (file, options) => {
|
|
17
|
+
const filePath = resolve(file);
|
|
18
|
+
const ext = extname(filePath).toLowerCase();
|
|
19
|
+
|
|
20
|
+
if (!existsSync(filePath)) {
|
|
21
|
+
console.error(chalk.red(`File not found: ${filePath}`));
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!SUPPORTED_EXTENSIONS.includes(ext)) {
|
|
26
|
+
console.error(chalk.red(`Unsupported file type "${ext}". Use .md or .txt files.`));
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let body;
|
|
31
|
+
try {
|
|
32
|
+
body = readFileSync(filePath, 'utf8');
|
|
33
|
+
} catch (err) {
|
|
34
|
+
console.error(chalk.red(`Could not read file: ${err.message}`));
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!body.trim()) {
|
|
39
|
+
console.error(chalk.red('File is empty.'));
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const title = options.title || basename(filePath, ext);
|
|
44
|
+
|
|
45
|
+
const spinner = ora('Importing entry...').start();
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const payload = {
|
|
49
|
+
title,
|
|
50
|
+
body,
|
|
51
|
+
generateSummary: !!options.summarize,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
if (options.tags) {
|
|
55
|
+
payload.tags = options.tags.split(',').map(t => t.trim());
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const data = await apiRequest('POST', '/entries', payload);
|
|
59
|
+
const entry = data.entry;
|
|
60
|
+
|
|
61
|
+
spinner.succeed(chalk.green('Entry imported!'));
|
|
62
|
+
console.log('');
|
|
63
|
+
console.log(` ${chalk.dim('ID:')} ${entry.entryId}`);
|
|
64
|
+
console.log(` ${chalk.dim('Title:')} ${entry.title}`);
|
|
65
|
+
console.log(` ${chalk.dim('Created:')} ${new Date(entry.createdAt).toLocaleString()}`);
|
|
66
|
+
|
|
67
|
+
if (entry.tags?.length > 0) {
|
|
68
|
+
console.log(` ${chalk.dim('Tags:')} ${entry.tags.join(', ')}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (entry.summary) {
|
|
72
|
+
console.log('');
|
|
73
|
+
console.log(` ${chalk.dim('Summary:')}`);
|
|
74
|
+
console.log(` ${entry.summary}`);
|
|
75
|
+
}
|
|
76
|
+
} catch (err) {
|
|
77
|
+
spinner.fail(chalk.red(err.message));
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
});
|
package/src/commands/list.mjs
CHANGED
|
@@ -32,7 +32,7 @@ export const listCommand = new Command('list')
|
|
|
32
32
|
for (const entry of data.entries) {
|
|
33
33
|
const date = new Date(entry.createdAt).toLocaleDateString();
|
|
34
34
|
const tags = entry.tags?.length ? chalk.dim(` [${entry.tags.join(', ')}]`) : '';
|
|
35
|
-
console.log(` ${chalk.cyan(entry.entryId
|
|
35
|
+
console.log(` ${chalk.cyan(entry.entryId)} ${date} ${chalk.bold(entry.title)}${tags}`);
|
|
36
36
|
if (entry.summary) {
|
|
37
37
|
console.log(` ${chalk.dim(entry.summary.slice(0, 80))}${entry.summary.length > 80 ? '...' : ''}`);
|
|
38
38
|
}
|