vocabflow 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/dist/__tests__/api-client.test.d.ts +2 -0
- package/dist/__tests__/api-client.test.d.ts.map +1 -0
- package/dist/__tests__/api-client.test.js +105 -0
- package/dist/__tests__/api-client.test.js.map +1 -0
- package/dist/__tests__/config.test.d.ts +2 -0
- package/dist/__tests__/config.test.d.ts.map +1 -0
- package/dist/__tests__/config.test.js +54 -0
- package/dist/__tests__/config.test.js.map +1 -0
- package/dist/__tests__/oauth-callback.test.d.ts +2 -0
- package/dist/__tests__/oauth-callback.test.d.ts.map +1 -0
- package/dist/__tests__/oauth-callback.test.js +46 -0
- package/dist/__tests__/oauth-callback.test.js.map +1 -0
- package/dist/__tests__/review.test.d.ts +2 -0
- package/dist/__tests__/review.test.d.ts.map +1 -0
- package/dist/__tests__/review.test.js +37 -0
- package/dist/__tests__/review.test.js.map +1 -0
- package/dist/__tests__/session.test.d.ts +2 -0
- package/dist/__tests__/session.test.d.ts.map +1 -0
- package/dist/__tests__/session.test.js +41 -0
- package/dist/__tests__/session.test.js.map +1 -0
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +51 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/logout.d.ts +2 -0
- package/dist/commands/logout.d.ts.map +1 -0
- package/dist/commands/logout.js +28 -0
- package/dist/commands/logout.js.map +1 -0
- package/dist/commands/review.d.ts +2 -0
- package/dist/commands/review.d.ts.map +1 -0
- package/dist/commands/review.js +449 -0
- package/dist/commands/review.js.map +1 -0
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +29 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +42 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/api-client.d.ts +55 -0
- package/dist/lib/api-client.d.ts.map +1 -0
- package/dist/lib/api-client.js +93 -0
- package/dist/lib/api-client.js.map +1 -0
- package/dist/lib/config.d.ts +20 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +39 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/oauth-callback.d.ts +11 -0
- package/dist/lib/oauth-callback.d.ts.map +1 -0
- package/dist/lib/oauth-callback.js +83 -0
- package/dist/lib/oauth-callback.js.map +1 -0
- package/dist/lib/tty.d.ts +28 -0
- package/dist/lib/tty.d.ts.map +1 -0
- package/dist/lib/tty.js +59 -0
- package/dist/lib/tty.js.map +1 -0
- package/dist/types/review.d.ts +77 -0
- package/dist/types/review.d.ts.map +1 -0
- package/dist/types/review.js +2 -0
- package/dist/types/review.js.map +1 -0
- package/package.json +31 -0
- package/src/__tests__/api-client.test.ts +123 -0
- package/src/__tests__/config.test.ts +61 -0
- package/src/__tests__/oauth-callback.test.ts +54 -0
- package/src/__tests__/review.test.ts +41 -0
- package/src/__tests__/session.test.ts +53 -0
- package/src/commands/login.ts +60 -0
- package/src/commands/logout.ts +31 -0
- package/src/commands/review.ts +544 -0
- package/src/commands/status.ts +32 -0
- package/src/index.ts +45 -0
- package/src/lib/api-client.ts +126 -0
- package/src/lib/config.ts +53 -0
- package/src/lib/oauth-callback.ts +102 -0
- package/src/lib/tty.ts +64 -0
- package/src/types/review.ts +87 -0
- package/tsconfig.json +10 -0
- package/vitest.config.ts +24 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import ky from 'ky';
|
|
2
|
+
import { getToken } from './config.js';
|
|
3
|
+
// API base URL without /api prefix, endpoints include /api path
|
|
4
|
+
const API_BASE_URL = process.env.VOCAB_API_BASE_URL || 'https://api.enbook.site';
|
|
5
|
+
// Webapp URL for login flow
|
|
6
|
+
const WEBAPP_URL = process.env.VOCAB_WEBAPP_URL || 'https://vocab.now';
|
|
7
|
+
// Create Ky instance with Bearer token injection
|
|
8
|
+
const cliApiClient = ky.create({
|
|
9
|
+
prefixUrl: API_BASE_URL,
|
|
10
|
+
timeout: 30000,
|
|
11
|
+
hooks: {
|
|
12
|
+
beforeRequest: [
|
|
13
|
+
(request) => {
|
|
14
|
+
const token = getToken();
|
|
15
|
+
if (token) {
|
|
16
|
+
request.headers.set('Authorization', `Bearer ${token}`);
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
// Type-safe API methods
|
|
23
|
+
export const cliApi = {
|
|
24
|
+
get: (url) => cliApiClient.get(url).json(),
|
|
25
|
+
post: (url, data) => cliApiClient.post(url, { json: data }).json(),
|
|
26
|
+
put: (url, data) => cliApiClient.put(url, { json: data }).json(),
|
|
27
|
+
delete: (url) => cliApiClient.delete(url).json(),
|
|
28
|
+
};
|
|
29
|
+
export class ApiError extends Error {
|
|
30
|
+
status;
|
|
31
|
+
response;
|
|
32
|
+
constructor(status, message, response) {
|
|
33
|
+
super(message);
|
|
34
|
+
this.status = status;
|
|
35
|
+
this.response = response;
|
|
36
|
+
this.name = 'ApiError';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export { WEBAPP_URL };
|
|
40
|
+
export function isApiSuccess(code) {
|
|
41
|
+
return code === 0 || code === 200;
|
|
42
|
+
}
|
|
43
|
+
export async function validateSession() {
|
|
44
|
+
try {
|
|
45
|
+
const response = await cliApi.get('api/users/me');
|
|
46
|
+
if (isApiSuccess(response.code) && response.data) {
|
|
47
|
+
return { valid: true, user: response.data };
|
|
48
|
+
}
|
|
49
|
+
return { valid: false, error: 'Invalid response from server' };
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
if (err?.response?.status === 401) {
|
|
53
|
+
return { valid: false, error: 'Session expired. Please run `vocab login`.' };
|
|
54
|
+
}
|
|
55
|
+
if (err?.cause?.code === 'ECONNREFUSED' || err?.name === 'FetchError') {
|
|
56
|
+
return { valid: false, error: 'Unable to connect. Check your network.' };
|
|
57
|
+
}
|
|
58
|
+
return { valid: false, error: 'Unable to connect. Check your network.' };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Require authenticated session - throws and exits if invalid
|
|
62
|
+
export async function requireAuth() {
|
|
63
|
+
const { valid, user, error } = await validateSession();
|
|
64
|
+
if (!valid) {
|
|
65
|
+
console.error(error);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
return user;
|
|
69
|
+
}
|
|
70
|
+
// Review API endpoints
|
|
71
|
+
export const reviewApi = {
|
|
72
|
+
/**
|
|
73
|
+
* GET /api/review/today
|
|
74
|
+
* Fetch all words for today's review session
|
|
75
|
+
*/
|
|
76
|
+
getTodayReview: () => cliApi.get('api/review/today'),
|
|
77
|
+
/**
|
|
78
|
+
* GET /api/review/questions/{wordId}?mode=FLASHCARD_FORWARD
|
|
79
|
+
* Fetch detailed word info for reveal
|
|
80
|
+
*/
|
|
81
|
+
getReviewQuestion: (wordId) => cliApi.get(`api/review/questions/${wordId}?mode=FLASHCARD_FORWARD`),
|
|
82
|
+
/**
|
|
83
|
+
* POST /api/review/complete
|
|
84
|
+
* Submit rating for a word
|
|
85
|
+
*/
|
|
86
|
+
submitReview: (data) => cliApi.post('api/review/complete', data),
|
|
87
|
+
/**
|
|
88
|
+
* DELETE /api/words/{wordId}
|
|
89
|
+
* Delete a word from user's wordbook
|
|
90
|
+
*/
|
|
91
|
+
deleteWord: (wordId) => cliApi.delete(`api/words/${wordId}`),
|
|
92
|
+
};
|
|
93
|
+
//# sourceMappingURL=api-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-client.js","sourceRoot":"","sources":["../../src/lib/api-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAQvC,gEAAgE;AAChE,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,yBAAyB,CAAC;AAEjF,4BAA4B;AAC5B,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,mBAAmB,CAAC;AAEvE,iDAAiD;AACjD,MAAM,YAAY,GAAG,EAAE,CAAC,MAAM,CAAC;IAC7B,SAAS,EAAE,YAAY;IACvB,OAAO,EAAE,KAAK;IACd,KAAK,EAAE;QACL,aAAa,EAAE;YACb,CAAC,OAAO,EAAE,EAAE;gBACV,MAAM,KAAK,GAAG,QAAQ,EAAE,CAAC;gBACzB,IAAI,KAAK,EAAE,CAAC;oBACV,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,eAAe,EAAE,UAAU,KAAK,EAAE,CAAC,CAAC;gBAC1D,CAAC;YACH,CAAC;SACF;KACF;CACF,CAAC,CAAC;AAEH,wBAAwB;AACxB,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,GAAG,EAAE,CAAI,GAAW,EAAE,EAAE,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,IAAI,EAAK;IACxD,IAAI,EAAE,CAAI,GAAW,EAAE,IAAc,EAAE,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAK;IAC1F,GAAG,EAAE,CAAI,GAAW,EAAE,IAAc,EAAE,EAAE,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAK;IACxF,MAAM,EAAE,CAAI,GAAW,EAAE,EAAE,CAAC,YAAY,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAK;CAC/D,CAAC;AAUF,MAAM,OAAO,QAAS,SAAQ,KAAK;IAEf;IAEA;IAHlB,YACkB,MAAc,EAC9B,OAAe,EACC,QAAmB;QAEnC,KAAK,CAAC,OAAO,CAAC,CAAC;QAJC,WAAM,GAAN,MAAM,CAAQ;QAEd,aAAQ,GAAR,QAAQ,CAAW;QAGnC,IAAI,CAAC,IAAI,GAAG,UAAU,CAAC;IACzB,CAAC;CACF;AAED,OAAO,EAAE,UAAU,EAAE,CAAC;AAEtB,MAAM,UAAU,YAAY,CAAC,IAA+B;IAC1D,OAAO,IAAI,KAAK,CAAC,IAAI,IAAI,KAAK,GAAG,CAAC;AACpC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,eAAe;IACnC,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,GAAG,CAAuD,cAAc,CAAC,CAAC;QACxG,IAAI,YAAY,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,QAAQ,CAAC,IAAI,EAAE,CAAC;YACjD,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC;QAC9C,CAAC;QACD,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,8BAA8B,EAAE,CAAC;IACjE,CAAC;IAAC,OAAO,GAAQ,EAAE,CAAC;QAClB,IAAI,GAAG,EAAE,QAAQ,EAAE,MAAM,KAAK,GAAG,EAAE,CAAC;YAClC,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,4CAA4C,EAAE,CAAC;QAC/E,CAAC;QACD,IAAI,GAAG,EAAE,KAAK,EAAE,IAAI,KAAK,cAAc,IAAI,GAAG,EAAE,IAAI,KAAK,YAAY,EAAE,CAAC;YACtE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,wCAAwC,EAAE,CAAC;QAC3E,CAAC;QACD,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,wCAAwC,EAAE,CAAC;IAC3E,CAAC;AACH,CAAC;AAED,8DAA8D;AAC9D,MAAM,CAAC,KAAK,UAAU,WAAW;IAC/B,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,eAAe,EAAE,CAAC;IACvD,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QACrB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IACD,OAAO,IAAK,CAAC;AACf,CAAC;AAED,uBAAuB;AACvB,MAAM,CAAC,MAAM,SAAS,GAAG;IACvB;;;OAGG;IACH,cAAc,EAAE,GAAG,EAAE,CACnB,MAAM,CAAC,GAAG,CAAgC,kBAAkB,CAAC;IAE/D;;;OAGG;IACH,iBAAiB,EAAE,CAAC,MAAc,EAAE,EAAE,CACpC,MAAM,CAAC,GAAG,CACR,wBAAwB,MAAM,yBAAyB,CACxD;IAEH;;;OAGG;IACH,YAAY,EAAE,CAAC,IAAyB,EAAE,EAAE,CAC1C,MAAM,CAAC,IAAI,CAA+B,qBAAqB,EAAE,IAAI,CAAC;IAExE;;;OAGG;IACH,UAAU,EAAE,CAAC,MAAc,EAAE,EAAE,CAC7B,MAAM,CAAC,MAAM,CACX,aAAa,MAAM,EAAE,CACtB;CACJ,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import Conf from 'conf';
|
|
2
|
+
export interface ConfigSchema {
|
|
3
|
+
token: string | null;
|
|
4
|
+
email: string | null;
|
|
5
|
+
expiresAt: number | null;
|
|
6
|
+
}
|
|
7
|
+
declare const config: Conf<ConfigSchema>;
|
|
8
|
+
export declare function getToken(): string | null;
|
|
9
|
+
export declare function getEmail(): string | null;
|
|
10
|
+
export declare function getExpiresAt(): number | null;
|
|
11
|
+
export declare function setAuth(data: {
|
|
12
|
+
token: string;
|
|
13
|
+
email: string;
|
|
14
|
+
expiresAt: number | null;
|
|
15
|
+
}): void;
|
|
16
|
+
export declare function clearAuth(): void;
|
|
17
|
+
export declare function hasToken(): boolean;
|
|
18
|
+
export declare function isTokenExpired(): boolean;
|
|
19
|
+
export { config };
|
|
20
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,QAAA,MAAM,MAAM,oBAOV,CAAC;AAEH,wBAAgB,QAAQ,IAAI,MAAM,GAAG,IAAI,CAExC;AAED,wBAAgB,QAAQ,IAAI,MAAM,GAAG,IAAI,CAExC;AAED,wBAAgB,YAAY,IAAI,MAAM,GAAG,IAAI,CAE5C;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAAG,IAAI,CAI9F;AAED,wBAAgB,SAAS,IAAI,IAAI,CAEhC;AAED,wBAAgB,QAAQ,IAAI,OAAO,CAGlC;AAED,wBAAgB,cAAc,IAAI,OAAO,CAMxC;AAED,OAAO,EAAE,MAAM,EAAE,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import Conf from 'conf';
|
|
2
|
+
const config = new Conf({
|
|
3
|
+
projectName: 'vocab-cli',
|
|
4
|
+
defaults: {
|
|
5
|
+
token: null,
|
|
6
|
+
email: null,
|
|
7
|
+
expiresAt: null,
|
|
8
|
+
},
|
|
9
|
+
});
|
|
10
|
+
export function getToken() {
|
|
11
|
+
return config.get('token');
|
|
12
|
+
}
|
|
13
|
+
export function getEmail() {
|
|
14
|
+
return config.get('email');
|
|
15
|
+
}
|
|
16
|
+
export function getExpiresAt() {
|
|
17
|
+
return config.get('expiresAt');
|
|
18
|
+
}
|
|
19
|
+
export function setAuth(data) {
|
|
20
|
+
config.set('token', data.token);
|
|
21
|
+
config.set('email', data.email);
|
|
22
|
+
config.set('expiresAt', data.expiresAt);
|
|
23
|
+
}
|
|
24
|
+
export function clearAuth() {
|
|
25
|
+
config.clear();
|
|
26
|
+
}
|
|
27
|
+
export function hasToken() {
|
|
28
|
+
const token = config.get('token');
|
|
29
|
+
return token !== null && token !== undefined;
|
|
30
|
+
}
|
|
31
|
+
export function isTokenExpired() {
|
|
32
|
+
const expiresAt = config.get('expiresAt');
|
|
33
|
+
if (expiresAt === null || expiresAt === undefined) {
|
|
34
|
+
return false; // No expiry stored, assume valid until server says otherwise
|
|
35
|
+
}
|
|
36
|
+
return Date.now() > expiresAt;
|
|
37
|
+
}
|
|
38
|
+
export { config };
|
|
39
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAC;AAQxB,MAAM,MAAM,GAAG,IAAI,IAAI,CAAe;IACpC,WAAW,EAAE,WAAW;IACxB,QAAQ,EAAE;QACR,KAAK,EAAE,IAAI;QACX,KAAK,EAAE,IAAI;QACX,SAAS,EAAE,IAAI;KAChB;CACF,CAAC,CAAC;AAEH,MAAM,UAAU,QAAQ;IACtB,OAAO,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;AAC7B,CAAC;AAED,MAAM,UAAU,QAAQ;IACtB,OAAO,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;AAC7B,CAAC;AAED,MAAM,UAAU,YAAY;IAC1B,OAAO,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;AACjC,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,IAAgE;IACtF,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;IAChC,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;IAChC,MAAM,CAAC,GAAG,CAAC,WAAW,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;AAC1C,CAAC;AAED,MAAM,UAAU,SAAS;IACvB,MAAM,CAAC,KAAK,EAAE,CAAC;AACjB,CAAC;AAED,MAAM,UAAU,QAAQ;IACtB,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAClC,OAAO,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,CAAC;AAC/C,CAAC;AAED,MAAM,UAAU,cAAc;IAC5B,MAAM,SAAS,GAAG,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IAC1C,IAAI,SAAS,KAAK,IAAI,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;QAClD,OAAO,KAAK,CAAC,CAAC,6DAA6D;IAC7E,CAAC;IACD,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC;AAChC,CAAC;AAED,OAAO,EAAE,MAAM,EAAE,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface CallbackSuccess {
|
|
2
|
+
type: 'success';
|
|
3
|
+
token: string;
|
|
4
|
+
}
|
|
5
|
+
export interface CallbackError {
|
|
6
|
+
type: 'error';
|
|
7
|
+
message: string;
|
|
8
|
+
}
|
|
9
|
+
export type CallbackResult = CallbackSuccess | CallbackError;
|
|
10
|
+
export declare function startCallbackServer(): Promise<CallbackResult>;
|
|
11
|
+
//# sourceMappingURL=oauth-callback.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth-callback.d.ts","sourceRoot":"","sources":["../../src/lib/oauth-callback.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,cAAc,GAAG,eAAe,GAAG,aAAa,CAAC;AAE7D,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,cAAc,CAAC,CAmFnE"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import { URL } from 'node:url';
|
|
3
|
+
const CALLBACK_PORT = 58421;
|
|
4
|
+
const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
5
|
+
export async function startCallbackServer() {
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
const timer = setTimeout(() => {
|
|
8
|
+
server.close();
|
|
9
|
+
reject(new Error('Authentication timed out. Please try again.'));
|
|
10
|
+
}, CALLBACK_TIMEOUT_MS);
|
|
11
|
+
const server = http.createServer((req, res) => {
|
|
12
|
+
// Handle CORS preflight
|
|
13
|
+
if (req.method === 'OPTIONS') {
|
|
14
|
+
res.writeHead(200, {
|
|
15
|
+
'Access-Control-Allow-Origin': '*',
|
|
16
|
+
'Access-Control-Allow-Methods': 'GET, OPTIONS',
|
|
17
|
+
'Access-Control-Allow-Headers': 'Content-Type',
|
|
18
|
+
});
|
|
19
|
+
res.end();
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const url = new URL(req.url ?? '/', `http://localhost:${CALLBACK_PORT}`);
|
|
23
|
+
const token = url.searchParams.get('token');
|
|
24
|
+
const error = url.searchParams.get('error');
|
|
25
|
+
const errorDescription = url.searchParams.get('error_description');
|
|
26
|
+
if (error || errorDescription) {
|
|
27
|
+
const msg = errorDescription ?? error ?? 'Unknown error';
|
|
28
|
+
res.writeHead(400, {
|
|
29
|
+
'Content-Type': 'text/html',
|
|
30
|
+
'Access-Control-Allow-Origin': '*',
|
|
31
|
+
});
|
|
32
|
+
res.end(`
|
|
33
|
+
<html>
|
|
34
|
+
<head><title>Authentication Failed</title></head>
|
|
35
|
+
<body style="font-family: sans-serif; padding: 40px; text-align: center;">
|
|
36
|
+
<h1 style="color: #d97757;">Authentication Failed</h1>
|
|
37
|
+
<p>${msg}</p>
|
|
38
|
+
<p>You can close this window.</p>
|
|
39
|
+
</body>
|
|
40
|
+
</html>
|
|
41
|
+
`);
|
|
42
|
+
clearTimeout(timer);
|
|
43
|
+
server.close();
|
|
44
|
+
resolve({ type: 'error', message: msg });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (token) {
|
|
48
|
+
res.writeHead(200, {
|
|
49
|
+
'Content-Type': 'text/html',
|
|
50
|
+
'Access-Control-Allow-Origin': '*',
|
|
51
|
+
});
|
|
52
|
+
res.end(`
|
|
53
|
+
<html>
|
|
54
|
+
<head><title>Authentication Successful</title></head>
|
|
55
|
+
<body style="font-family: sans-serif; padding: 40px; text-align: center;">
|
|
56
|
+
<h1 style="color: #22c55e;">Authentication Successful</h1>
|
|
57
|
+
<p>You can close this window and return to the CLI.</p>
|
|
58
|
+
</body>
|
|
59
|
+
</html>
|
|
60
|
+
`);
|
|
61
|
+
clearTimeout(timer);
|
|
62
|
+
server.close();
|
|
63
|
+
resolve({ type: 'success', token });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
// Unknown route
|
|
67
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
68
|
+
res.end('Not found. Expected callback with ?token=xxx or ?error=xxx');
|
|
69
|
+
});
|
|
70
|
+
server.on('error', (err) => {
|
|
71
|
+
if (err.code === 'EADDRINUSE') {
|
|
72
|
+
reject(new Error(`Port ${CALLBACK_PORT} is already in use. Please close other vocab processes and try again.`));
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
reject(err);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
server.listen(CALLBACK_PORT, () => {
|
|
79
|
+
// Server started successfully
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=oauth-callback.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth-callback.js","sourceRoot":"","sources":["../../src/lib/oauth-callback.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check if stdin is a TTY (interactive terminal)
|
|
3
|
+
*/
|
|
4
|
+
export declare function isInteractive(): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* Setup raw mode for keypress capture.
|
|
7
|
+
* MUST check isInteractive() first - setRawMode only works with real TTY.
|
|
8
|
+
* Call cleanup() when done to restore normal terminal behavior.
|
|
9
|
+
*/
|
|
10
|
+
export declare function setupRawMode(): void;
|
|
11
|
+
/**
|
|
12
|
+
* Cleanup raw mode - restore normal terminal behavior.
|
|
13
|
+
* Call this on exit (success, error, or quit).
|
|
14
|
+
*/
|
|
15
|
+
export declare function cleanup(): void;
|
|
16
|
+
/**
|
|
17
|
+
* Clear the current line in terminal
|
|
18
|
+
*/
|
|
19
|
+
export declare function clearLine(): void;
|
|
20
|
+
/**
|
|
21
|
+
* Move cursor to top-left of terminal
|
|
22
|
+
*/
|
|
23
|
+
export declare function moveCursorTop(): void;
|
|
24
|
+
/**
|
|
25
|
+
* Clear screen from cursor position to bottom
|
|
26
|
+
*/
|
|
27
|
+
export declare function clearScreen(): void;
|
|
28
|
+
//# sourceMappingURL=tty.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tty.d.ts","sourceRoot":"","sources":["../../src/lib/tty.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,wBAAgB,aAAa,IAAI,OAAO,CAEvC;AAED;;;;GAIG;AACH,wBAAgB,YAAY,IAAI,IAAI,CAOnC;AAED;;;GAGG;AACH,wBAAgB,OAAO,IAAI,IAAI,CAM9B;AAED;;GAEG;AACH,wBAAgB,SAAS,IAAI,IAAI,CAGhC;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,IAAI,CAMpC;AAED;;GAEG;AACH,wBAAgB,WAAW,IAAI,IAAI,CAMlC"}
|
package/dist/lib/tty.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import * as readline from 'readline';
|
|
2
|
+
/**
|
|
3
|
+
* Check if stdin is a TTY (interactive terminal)
|
|
4
|
+
*/
|
|
5
|
+
export function isInteractive() {
|
|
6
|
+
return process.stdin.isTTY === true;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Setup raw mode for keypress capture.
|
|
10
|
+
* MUST check isInteractive() first - setRawMode only works with real TTY.
|
|
11
|
+
* Call cleanup() when done to restore normal terminal behavior.
|
|
12
|
+
*/
|
|
13
|
+
export function setupRawMode() {
|
|
14
|
+
if (!isInteractive()) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
readline.emitKeypressEvents(process.stdin);
|
|
18
|
+
process.stdin.setRawMode?.(true);
|
|
19
|
+
process.stdin.resume?.();
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Cleanup raw mode - restore normal terminal behavior.
|
|
23
|
+
* Call this on exit (success, error, or quit).
|
|
24
|
+
*/
|
|
25
|
+
export function cleanup() {
|
|
26
|
+
if (!isInteractive()) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
process.stdin.setRawMode?.(false);
|
|
30
|
+
process.stdin.pause?.();
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Clear the current line in terminal
|
|
34
|
+
*/
|
|
35
|
+
export function clearLine() {
|
|
36
|
+
readline.cursorTo(process.stdout, 0);
|
|
37
|
+
readline.clearLine(process.stdout, 0);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Move cursor to top-left of terminal
|
|
41
|
+
*/
|
|
42
|
+
export function moveCursorTop() {
|
|
43
|
+
if (!process.stdout.isTTY) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
// ANSI cursor-home is more consistent across Windows + Git Bash terminals.
|
|
47
|
+
process.stdout.write('\u001B[H');
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Clear screen from cursor position to bottom
|
|
51
|
+
*/
|
|
52
|
+
export function clearScreen() {
|
|
53
|
+
if (!process.stdout.isTTY) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
// ANSI full-screen clear avoids stale lines in some TTY implementations.
|
|
57
|
+
process.stdout.write('\u001B[2J');
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=tty.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tty.js","sourceRoot":"","sources":["../../src/lib/tty.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,UAAU,CAAC;AAErC;;GAEG;AACH,MAAM,UAAU,aAAa;IAC3B,OAAO,OAAO,CAAC,KAAK,CAAC,KAAK,KAAK,IAAI,CAAC;AACtC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,YAAY;IAC1B,IAAI,CAAC,aAAa,EAAE,EAAE,CAAC;QACrB,OAAO;IACT,CAAC;IACD,QAAQ,CAAC,kBAAkB,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IAC3C,OAAO,CAAC,KAAK,CAAC,UAAU,EAAE,CAAC,IAAI,CAAC,CAAC;IACjC,OAAO,CAAC,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC;AAC3B,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,OAAO;IACrB,IAAI,CAAC,aAAa,EAAE,EAAE,CAAC;QACrB,OAAO;IACT,CAAC;IACD,OAAO,CAAC,KAAK,CAAC,UAAU,EAAE,CAAC,KAAK,CAAC,CAAC;IAClC,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC;AAC1B,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,SAAS;IACvB,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;IACrC,QAAQ,CAAC,SAAS,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;AACxC,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,aAAa;IAC3B,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;QAC1B,OAAO;IACT,CAAC;IACD,2EAA2E;IAC3E,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;AACnC,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,WAAW;IACzB,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;QAC1B,OAAO;IACT,CAAC;IACD,yEAAyE;IACzE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;AACpC,CAAC"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response from GET /api/review/today
|
|
3
|
+
*/
|
|
4
|
+
export interface TodayReviewVO {
|
|
5
|
+
id: number;
|
|
6
|
+
wordText: string;
|
|
7
|
+
phoneticUs?: string;
|
|
8
|
+
briefExplanation: string;
|
|
9
|
+
nextReviewTime: string;
|
|
10
|
+
reviewCount: number;
|
|
11
|
+
intervalDays: number;
|
|
12
|
+
easeFactor: number;
|
|
13
|
+
}
|
|
14
|
+
export interface PagedResponseTodayReviewVO {
|
|
15
|
+
datas?: TodayReviewVO[];
|
|
16
|
+
totalElements?: number;
|
|
17
|
+
page?: number;
|
|
18
|
+
size?: number;
|
|
19
|
+
totalPages?: number;
|
|
20
|
+
first?: boolean;
|
|
21
|
+
last?: boolean;
|
|
22
|
+
words?: TodayReviewVO[];
|
|
23
|
+
total?: number;
|
|
24
|
+
}
|
|
25
|
+
export interface ApiResponsePagedTodayReviewVO {
|
|
26
|
+
code: number;
|
|
27
|
+
message: string;
|
|
28
|
+
data: PagedResponseTodayReviewVO;
|
|
29
|
+
timestamp: number;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Response from GET /api/review/questions/{wordId}
|
|
33
|
+
*/
|
|
34
|
+
export interface ReviewQuestionVO {
|
|
35
|
+
wordId: number;
|
|
36
|
+
mode: string;
|
|
37
|
+
question: Record<string, unknown>;
|
|
38
|
+
answer: Record<string, unknown>;
|
|
39
|
+
}
|
|
40
|
+
export interface ApiResponseReviewQuestionVO {
|
|
41
|
+
code: number;
|
|
42
|
+
message: string;
|
|
43
|
+
data: ReviewQuestionVO;
|
|
44
|
+
timestamp: number;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Request body for POST /api/review/complete
|
|
48
|
+
*/
|
|
49
|
+
export type ReviewResult = 'UNKNOWN' | 'VAGUE' | 'KNOWN' | 'CORRECT' | 'INCORRECT';
|
|
50
|
+
export type ReviewMode = 'FLASHCARD_FORWARD' | 'FLASHCARD_BACKWARD' | 'SPELLING' | 'CHOICE_EN2CN' | 'CHOICE_CN2EN';
|
|
51
|
+
export interface SubmitReviewRequest {
|
|
52
|
+
wordId: number;
|
|
53
|
+
result: ReviewResult;
|
|
54
|
+
mode: ReviewMode;
|
|
55
|
+
userAnswer?: string;
|
|
56
|
+
timeCost?: number;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Response from POST /api/review/complete
|
|
60
|
+
*/
|
|
61
|
+
export interface ReviewResponseDto {
|
|
62
|
+
wordId: number;
|
|
63
|
+
wordText: string;
|
|
64
|
+
easeFactor: number;
|
|
65
|
+
intervalDays: number;
|
|
66
|
+
nextReviewTime: string;
|
|
67
|
+
reviewCount: number;
|
|
68
|
+
correctCount: number;
|
|
69
|
+
lastReviewResult: ReviewResult;
|
|
70
|
+
}
|
|
71
|
+
export interface ApiResponseReviewResponseDto {
|
|
72
|
+
code: number;
|
|
73
|
+
message: string;
|
|
74
|
+
data: ReviewResponseDto;
|
|
75
|
+
timestamp: number;
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=review.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"review.d.ts","sourceRoot":"","sources":["../../src/types/review.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,MAAM,CAAC;IACzB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,0BAA0B;IAEzC,KAAK,CAAC,EAAE,aAAa,EAAE,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,IAAI,CAAC,EAAE,OAAO,CAAC;IAGf,KAAK,CAAC,EAAE,aAAa,EAAE,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,6BAA6B;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,0BAA0B,CAAC;IACjC,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,2BAA2B;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,gBAAgB,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,OAAO,GAAG,OAAO,GAAG,SAAS,GAAG,WAAW,CAAC;AACnF,MAAM,MAAM,UAAU,GAAG,mBAAmB,GAAG,oBAAoB,GAAG,UAAU,GAAG,cAAc,GAAG,cAAc,CAAC;AAEnH,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,YAAY,CAAC;IACrB,IAAI,EAAE,UAAU,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,YAAY,CAAC;CAChC;AAED,MAAM,WAAW,4BAA4B;IAC3C,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,iBAAiB,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;CACnB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"review.js","sourceRoot":"","sources":["../../src/types/review.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vocabflow",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"vocabflow": "./dist/index.js",
|
|
8
|
+
"vocab": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"dev": "tsx watch src/index.ts",
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"type-check": "tsc --noEmit",
|
|
14
|
+
"test": "vitest run",
|
|
15
|
+
"test:watch": "vitest"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"conf": "^15.1.0",
|
|
19
|
+
"ky": "^1.14.3",
|
|
20
|
+
"open": "^11.0.0",
|
|
21
|
+
"picocolors": "^1.1.1"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@repo/api-client": "workspace:*",
|
|
25
|
+
"@repo/utils": "workspace:*",
|
|
26
|
+
"@types/node": "^20.11.0",
|
|
27
|
+
"tsx": "^4.7.0",
|
|
28
|
+
"typescript": "^5.3.0",
|
|
29
|
+
"vitest": "^1.2.0"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const mockGetToken = vi.fn();
|
|
4
|
+
const mockGet = vi.fn();
|
|
5
|
+
const mockPost = vi.fn();
|
|
6
|
+
const mockPut = vi.fn();
|
|
7
|
+
const mockDelete = vi.fn();
|
|
8
|
+
const mockKyCreate = vi.fn();
|
|
9
|
+
|
|
10
|
+
vi.mock('../lib/config.js', () => ({
|
|
11
|
+
getToken: mockGetToken,
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
vi.mock('ky', () => ({
|
|
15
|
+
default: {
|
|
16
|
+
create: mockKyCreate,
|
|
17
|
+
},
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
describe('CLI API client endpoint compatibility', () => {
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
vi.resetModules();
|
|
23
|
+
vi.clearAllMocks();
|
|
24
|
+
|
|
25
|
+
mockGetToken.mockReturnValue('test-token');
|
|
26
|
+
mockGet.mockImplementation((_url: string) => ({
|
|
27
|
+
json: vi.fn(),
|
|
28
|
+
}));
|
|
29
|
+
mockPost.mockImplementation((_url: string, _opts?: unknown) => ({
|
|
30
|
+
json: vi.fn(),
|
|
31
|
+
}));
|
|
32
|
+
mockPut.mockImplementation((_url: string, _opts?: unknown) => ({
|
|
33
|
+
json: vi.fn(),
|
|
34
|
+
}));
|
|
35
|
+
mockDelete.mockImplementation((_url: string) => ({
|
|
36
|
+
json: vi.fn(),
|
|
37
|
+
}));
|
|
38
|
+
mockKyCreate.mockReturnValue({
|
|
39
|
+
get: mockGet,
|
|
40
|
+
post: mockPost,
|
|
41
|
+
put: mockPut,
|
|
42
|
+
delete: mockDelete,
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('validateSession success should call relative endpoint without leading slash', async () => {
|
|
47
|
+
const jsonMock = vi.fn().mockResolvedValue({
|
|
48
|
+
code: 0,
|
|
49
|
+
message: 'ok',
|
|
50
|
+
data: { id: 'u1', email: 'test@example.com', name: 'Test User' },
|
|
51
|
+
});
|
|
52
|
+
mockGet.mockReturnValueOnce({ json: jsonMock });
|
|
53
|
+
|
|
54
|
+
const { validateSession } = await import('../lib/api-client.js');
|
|
55
|
+
const result = await validateSession();
|
|
56
|
+
|
|
57
|
+
expect(mockGet).toHaveBeenCalledWith('api/users/me');
|
|
58
|
+
expect(mockGet).not.toHaveBeenCalledWith('/api/users/me');
|
|
59
|
+
expect(result).toEqual({
|
|
60
|
+
valid: true,
|
|
61
|
+
user: { id: 'u1', email: 'test@example.com', name: 'Test User' },
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('isApiSuccess should accept both 0 and 200', async () => {
|
|
66
|
+
const { isApiSuccess } = await import('../lib/api-client.js');
|
|
67
|
+
expect(isApiSuccess(0)).toBe(true);
|
|
68
|
+
expect(isApiSuccess(200)).toBe(true);
|
|
69
|
+
expect(isApiSuccess(401)).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('validateSession failure should return session-expired message for 401', async () => {
|
|
73
|
+
const jsonMock = vi.fn().mockRejectedValue({ response: { status: 401 } });
|
|
74
|
+
mockGet.mockReturnValueOnce({ json: jsonMock });
|
|
75
|
+
|
|
76
|
+
const { validateSession } = await import('../lib/api-client.js');
|
|
77
|
+
const result = await validateSession();
|
|
78
|
+
|
|
79
|
+
expect(mockGet).toHaveBeenCalledWith('api/users/me');
|
|
80
|
+
expect(result).toEqual({
|
|
81
|
+
valid: false,
|
|
82
|
+
error: 'Session expired. Please run `vocab login`.',
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('reviewApi should keep all endpoints relative to prefixUrl', async () => {
|
|
87
|
+
const getJsonMock = vi.fn().mockResolvedValue({ code: 200, data: {} });
|
|
88
|
+
const postJsonMock = vi.fn().mockResolvedValue({ code: 200, data: {} });
|
|
89
|
+
const deleteJsonMock = vi.fn().mockResolvedValue({ code: 200, data: {} });
|
|
90
|
+
|
|
91
|
+
mockGet.mockReturnValue({ json: getJsonMock });
|
|
92
|
+
mockPost.mockReturnValue({ json: postJsonMock });
|
|
93
|
+
mockDelete.mockReturnValue({ json: deleteJsonMock });
|
|
94
|
+
|
|
95
|
+
const { reviewApi } = await import('../lib/api-client.js');
|
|
96
|
+
await reviewApi.getTodayReview();
|
|
97
|
+
await reviewApi.getReviewQuestion(123);
|
|
98
|
+
await reviewApi.submitReview({
|
|
99
|
+
wordId: 123,
|
|
100
|
+
result: 'KNOWN',
|
|
101
|
+
mode: 'FLASHCARD_FORWARD',
|
|
102
|
+
});
|
|
103
|
+
await reviewApi.deleteWord(123);
|
|
104
|
+
|
|
105
|
+
expect(mockGet).toHaveBeenCalledWith('api/review/today');
|
|
106
|
+
expect(mockGet).toHaveBeenCalledWith('api/review/questions/123?mode=FLASHCARD_FORWARD');
|
|
107
|
+
expect(mockPost).toHaveBeenCalledWith(
|
|
108
|
+
'api/review/complete',
|
|
109
|
+
expect.objectContaining({
|
|
110
|
+
json: {
|
|
111
|
+
wordId: 123,
|
|
112
|
+
result: 'KNOWN',
|
|
113
|
+
mode: 'FLASHCARD_FORWARD',
|
|
114
|
+
},
|
|
115
|
+
})
|
|
116
|
+
);
|
|
117
|
+
expect(mockDelete).toHaveBeenCalledWith('api/words/123');
|
|
118
|
+
expect(mockGet).not.toHaveBeenCalledWith('/api/review/today');
|
|
119
|
+
expect(mockGet).not.toHaveBeenCalledWith('/api/review/questions/123?mode=FLASHCARD_FORWARD');
|
|
120
|
+
expect(mockPost).not.toHaveBeenCalledWith('/api/review/complete', expect.anything());
|
|
121
|
+
expect(mockDelete).not.toHaveBeenCalledWith('/api/words/123');
|
|
122
|
+
});
|
|
123
|
+
});
|