codex-team 0.0.4 → 0.0.6
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 +1 -0
- package/dist/cli.cjs +72 -27
- package/dist/main.cjs +72 -27
- package/dist/main.js +72 -27
- package/package.json +9 -10
package/README.md
CHANGED
package/dist/cli.cjs
CHANGED
|
@@ -12,6 +12,10 @@ var __webpack_modules__ = {
|
|
|
12
12
|
var timezone_js_default = /*#__PURE__*/ __webpack_require__.n(timezone_js_namespaceObject);
|
|
13
13
|
const utc_js_namespaceObject = require("dayjs/plugin/utc.js");
|
|
14
14
|
var utc_js_default = /*#__PURE__*/ __webpack_require__.n(utc_js_namespaceObject);
|
|
15
|
+
var package_namespaceObject = {
|
|
16
|
+
rE: "0.0.6"
|
|
17
|
+
};
|
|
18
|
+
const external_node_crypto_namespaceObject = require("node:crypto");
|
|
15
19
|
const promises_namespaceObject = require("node:fs/promises");
|
|
16
20
|
function isRecord(value) {
|
|
17
21
|
return "object" == typeof value && null !== value && !Array.isArray(value);
|
|
@@ -34,6 +38,29 @@ var __webpack_modules__ = {
|
|
|
34
38
|
if ("number" != typeof value || Number.isNaN(value)) throw new Error(`Field "${fieldName}" must be a number.`);
|
|
35
39
|
return value;
|
|
36
40
|
}
|
|
41
|
+
function normalizeAuthMode(authMode) {
|
|
42
|
+
return authMode.trim().toLowerCase();
|
|
43
|
+
}
|
|
44
|
+
function isApiKeyAuthMode(authMode) {
|
|
45
|
+
return "apikey" === normalizeAuthMode(authMode);
|
|
46
|
+
}
|
|
47
|
+
function isSupportedChatGPTAuthMode(authMode) {
|
|
48
|
+
const normalized = normalizeAuthMode(authMode);
|
|
49
|
+
return "chatgpt" === normalized || "chatgpt_auth_tokens" === normalized;
|
|
50
|
+
}
|
|
51
|
+
function fingerprintApiKey(apiKey) {
|
|
52
|
+
return (0, external_node_crypto_namespaceObject.createHash)("sha256").update(apiKey).digest("hex").slice(0, 16);
|
|
53
|
+
}
|
|
54
|
+
function getSnapshotIdentity(snapshot) {
|
|
55
|
+
if (isApiKeyAuthMode(snapshot.auth_mode)) {
|
|
56
|
+
const apiKey = snapshot.OPENAI_API_KEY;
|
|
57
|
+
if ("string" != typeof apiKey || "" === apiKey.trim()) throw new Error('Field "OPENAI_API_KEY" must be a non-empty string for apikey auth.');
|
|
58
|
+
return `key_${fingerprintApiKey(apiKey)}`;
|
|
59
|
+
}
|
|
60
|
+
const accountId = snapshot.tokens?.account_id;
|
|
61
|
+
if ("string" != typeof accountId || "" === accountId.trim()) throw new Error('Field "tokens.account_id" must be a non-empty string.');
|
|
62
|
+
return accountId;
|
|
63
|
+
}
|
|
37
64
|
function defaultQuotaSnapshot() {
|
|
38
65
|
return {
|
|
39
66
|
status: "stale"
|
|
@@ -78,15 +105,28 @@ var __webpack_modules__ = {
|
|
|
78
105
|
}
|
|
79
106
|
if (!isRecord(parsed)) throw new Error("Auth snapshot must be a JSON object.");
|
|
80
107
|
const authMode = asNonEmptyString(parsed.auth_mode, "auth_mode");
|
|
81
|
-
|
|
82
|
-
|
|
108
|
+
const tokens = parsed.tokens;
|
|
109
|
+
if (null != tokens && !isRecord(tokens)) throw new Error('Field "tokens" must be an object.');
|
|
110
|
+
const apiKeyMode = isApiKeyAuthMode(authMode);
|
|
111
|
+
const normalizedApiKey = null === parsed.OPENAI_API_KEY || void 0 === parsed.OPENAI_API_KEY ? parsed.OPENAI_API_KEY : asNonEmptyString(parsed.OPENAI_API_KEY, "OPENAI_API_KEY");
|
|
112
|
+
if (apiKeyMode) {
|
|
113
|
+
if ("string" != typeof normalizedApiKey || "" === normalizedApiKey.trim()) throw new Error('Field "OPENAI_API_KEY" must be a non-empty string for apikey auth.');
|
|
114
|
+
} else {
|
|
115
|
+
if (!isRecord(tokens)) throw new Error('Field "tokens" must be an object.');
|
|
116
|
+
asNonEmptyString(tokens.account_id, "tokens.account_id");
|
|
117
|
+
}
|
|
83
118
|
return {
|
|
84
119
|
...parsed,
|
|
85
120
|
auth_mode: authMode,
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
121
|
+
OPENAI_API_KEY: normalizedApiKey,
|
|
122
|
+
...isRecord(tokens) ? {
|
|
123
|
+
tokens: {
|
|
124
|
+
...tokens,
|
|
125
|
+
..."string" == typeof tokens.account_id && "" !== tokens.account_id.trim() ? {
|
|
126
|
+
account_id: tokens.account_id
|
|
127
|
+
} : {}
|
|
128
|
+
}
|
|
129
|
+
} : {}
|
|
90
130
|
};
|
|
91
131
|
}
|
|
92
132
|
async function readAuthSnapshotFile(filePath) {
|
|
@@ -98,7 +138,7 @@ var __webpack_modules__ = {
|
|
|
98
138
|
return {
|
|
99
139
|
name,
|
|
100
140
|
auth_mode: snapshot.auth_mode,
|
|
101
|
-
account_id: snapshot
|
|
141
|
+
account_id: getSnapshotIdentity(snapshot),
|
|
102
142
|
created_at: existingCreatedAt ?? timestamp,
|
|
103
143
|
updated_at: timestamp,
|
|
104
144
|
last_switched_at: null,
|
|
@@ -155,16 +195,13 @@ var __webpack_modules__ = {
|
|
|
155
195
|
const value = payload[key];
|
|
156
196
|
return "string" == typeof value && "" !== value.trim() ? value : void 0;
|
|
157
197
|
}
|
|
158
|
-
function isSupportedChatGPTMode(authMode) {
|
|
159
|
-
const normalized = authMode.trim().toLowerCase();
|
|
160
|
-
return "chatgpt" === normalized || "chatgpt_auth_tokens" === normalized;
|
|
161
|
-
}
|
|
162
198
|
function parsePlanType(snapshot) {
|
|
199
|
+
const tokens = snapshot.tokens ?? {};
|
|
163
200
|
for (const tokenName of [
|
|
164
201
|
"id_token",
|
|
165
202
|
"access_token"
|
|
166
203
|
]){
|
|
167
|
-
const token =
|
|
204
|
+
const token = tokens[tokenName];
|
|
168
205
|
if ("string" == typeof token && "" !== token.trim()) try {
|
|
169
206
|
const payload = decodeJwtPayload(token);
|
|
170
207
|
const authClaim = extractAuthClaim(payload);
|
|
@@ -175,10 +212,11 @@ var __webpack_modules__ = {
|
|
|
175
212
|
}
|
|
176
213
|
function extractChatGPTAuth(snapshot) {
|
|
177
214
|
const authMode = snapshot.auth_mode ?? "";
|
|
178
|
-
const supported =
|
|
179
|
-
const
|
|
180
|
-
const
|
|
181
|
-
const
|
|
215
|
+
const supported = isSupportedChatGPTAuthMode(authMode);
|
|
216
|
+
const tokens = snapshot.tokens ?? {};
|
|
217
|
+
const accessTokenValue = tokens.access_token;
|
|
218
|
+
const refreshTokenValue = tokens.refresh_token;
|
|
219
|
+
const directAccountId = tokens.account_id;
|
|
182
220
|
let accountId = "string" == typeof directAccountId && "" !== directAccountId.trim() ? directAccountId : void 0;
|
|
183
221
|
let planType;
|
|
184
222
|
let issuer;
|
|
@@ -187,7 +225,7 @@ var __webpack_modules__ = {
|
|
|
187
225
|
"id_token",
|
|
188
226
|
"access_token"
|
|
189
227
|
]){
|
|
190
|
-
const token =
|
|
228
|
+
const token = tokens[tokenName];
|
|
191
229
|
if ("string" == typeof token && "" !== token.trim()) try {
|
|
192
230
|
const payload = decodeJwtPayload(token);
|
|
193
231
|
const authClaim = extractAuthClaim(payload);
|
|
@@ -369,7 +407,7 @@ var __webpack_modules__ = {
|
|
|
369
407
|
...snapshot,
|
|
370
408
|
last_refresh: (options.now ?? new Date()).toISOString(),
|
|
371
409
|
tokens: {
|
|
372
|
-
...snapshot.tokens,
|
|
410
|
+
...snapshot.tokens ?? {},
|
|
373
411
|
access_token: payload.access_token,
|
|
374
412
|
id_token: payload.id_token,
|
|
375
413
|
refresh_token: payload.refresh_token ?? extracted.refreshToken,
|
|
@@ -520,7 +558,7 @@ var __webpack_modules__ = {
|
|
|
520
558
|
if (!await pathExists(this.paths.currentAuthPath)) return;
|
|
521
559
|
try {
|
|
522
560
|
const currentSnapshot = await readAuthSnapshotFile(this.paths.currentAuthPath);
|
|
523
|
-
if (currentSnapshot
|
|
561
|
+
if (getSnapshotIdentity(currentSnapshot) !== getSnapshotIdentity(snapshot)) return;
|
|
524
562
|
await atomicWriteFile(this.paths.currentAuthPath, stringifyJson(snapshot));
|
|
525
563
|
} catch {}
|
|
526
564
|
}
|
|
@@ -576,7 +614,7 @@ var __webpack_modules__ = {
|
|
|
576
614
|
]);
|
|
577
615
|
const meta = parseSnapshotMeta(rawMeta);
|
|
578
616
|
if (meta.name !== name) throw new Error(`Account metadata name mismatch for "${name}".`);
|
|
579
|
-
if (meta.account_id !== snapshot
|
|
617
|
+
if (meta.account_id !== getSnapshotIdentity(snapshot)) throw new Error(`Account metadata account_id mismatch for "${name}".`);
|
|
580
618
|
return {
|
|
581
619
|
...meta,
|
|
582
620
|
authPath,
|
|
@@ -619,11 +657,12 @@ var __webpack_modules__ = {
|
|
|
619
657
|
warnings
|
|
620
658
|
};
|
|
621
659
|
const snapshot = await readAuthSnapshotFile(this.paths.currentAuthPath);
|
|
622
|
-
const
|
|
660
|
+
const currentIdentity = getSnapshotIdentity(snapshot);
|
|
661
|
+
const matchedAccounts = accounts.filter((account)=>account.account_id === currentIdentity).map((account)=>account.name);
|
|
623
662
|
return {
|
|
624
663
|
exists: true,
|
|
625
664
|
auth_mode: snapshot.auth_mode,
|
|
626
|
-
account_id:
|
|
665
|
+
account_id: currentIdentity,
|
|
627
666
|
matched_accounts: matchedAccounts,
|
|
628
667
|
managed: matchedAccounts.length > 0,
|
|
629
668
|
duplicate_match: matchedAccounts.length > 1,
|
|
@@ -686,7 +725,7 @@ var __webpack_modules__ = {
|
|
|
686
725
|
const rawAuth = await readJsonFile(account.authPath);
|
|
687
726
|
await atomicWriteFile(this.paths.currentAuthPath, `${rawAuth.trimEnd()}\n`);
|
|
688
727
|
const writtenSnapshot = await readAuthSnapshotFile(this.paths.currentAuthPath);
|
|
689
|
-
if (writtenSnapshot
|
|
728
|
+
if (getSnapshotIdentity(writtenSnapshot) !== account.account_id) throw new Error(`Switch verification failed for account "${name}".`);
|
|
690
729
|
const meta = parseSnapshotMeta(await readJsonFile(account.metaPath));
|
|
691
730
|
meta.last_switched_at = new Date().toISOString();
|
|
692
731
|
meta.updated_at = meta.last_switched_at;
|
|
@@ -730,7 +769,7 @@ var __webpack_modules__ = {
|
|
|
730
769
|
await this.syncCurrentAuthIfMatching(result.authSnapshot);
|
|
731
770
|
}
|
|
732
771
|
meta.auth_mode = result.authSnapshot.auth_mode;
|
|
733
|
-
meta.account_id = result.authSnapshot
|
|
772
|
+
meta.account_id = getSnapshotIdentity(result.authSnapshot);
|
|
734
773
|
meta.updated_at = now.toISOString();
|
|
735
774
|
meta.quota = result.quota;
|
|
736
775
|
await this.writeAccountMeta(name, meta);
|
|
@@ -842,7 +881,7 @@ var __webpack_modules__ = {
|
|
|
842
881
|
const metaStat = await (0, promises_namespaceObject.stat)(account.metaPath);
|
|
843
882
|
if ((511 & authStat.mode) !== FILE_MODE) issues.push(`Account "${account.name}" auth permissions must be 600.`);
|
|
844
883
|
if ((511 & metaStat.mode) !== FILE_MODE) issues.push(`Account "${account.name}" metadata permissions must be 600.`);
|
|
845
|
-
if (account.duplicateAccountId) warnings.push(`Account "${account.name}" shares
|
|
884
|
+
if (account.duplicateAccountId) warnings.push(`Account "${account.name}" shares identity ${account.account_id} with another saved account.`);
|
|
846
885
|
}
|
|
847
886
|
let currentAuthPresent = false;
|
|
848
887
|
if (await pathExists(this.paths.currentAuthPath)) {
|
|
@@ -904,6 +943,8 @@ var __webpack_modules__ = {
|
|
|
904
943
|
stream.write(`codexm - manage multiple Codex ChatGPT auth snapshots
|
|
905
944
|
|
|
906
945
|
Usage:
|
|
946
|
+
codexm --version
|
|
947
|
+
codexm --help
|
|
907
948
|
codexm current [--json]
|
|
908
949
|
codexm list [name] [--json]
|
|
909
950
|
codexm save <name> [--force] [--json]
|
|
@@ -923,7 +964,7 @@ Account names must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.
|
|
|
923
964
|
if (status.exists) {
|
|
924
965
|
lines.push("Current auth: present");
|
|
925
966
|
lines.push(`Auth mode: ${status.auth_mode}`);
|
|
926
|
-
lines.push(`
|
|
967
|
+
lines.push(`Identity: ${maskAccountId(status.account_id ?? "")}`);
|
|
927
968
|
if (0 === status.matched_accounts.length) lines.push("Managed account: no (unmanaged)");
|
|
928
969
|
else if (1 === status.matched_accounts.length) lines.push(`Managed account: ${status.matched_accounts[0]}`);
|
|
929
970
|
else lines.push(`Managed account: multiple (${status.matched_accounts.join(", ")})`);
|
|
@@ -1056,7 +1097,7 @@ Account names must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.
|
|
|
1056
1097
|
},
|
|
1057
1098
|
{
|
|
1058
1099
|
key: "account_id",
|
|
1059
|
-
label: "
|
|
1100
|
+
label: "IDENTITY"
|
|
1060
1101
|
},
|
|
1061
1102
|
{
|
|
1062
1103
|
key: "plan_type",
|
|
@@ -1131,6 +1172,10 @@ Account names must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.
|
|
|
1131
1172
|
const parsed = parseArgs(argv);
|
|
1132
1173
|
const json = parsed.flags.has("--json");
|
|
1133
1174
|
try {
|
|
1175
|
+
if (parsed.flags.has("--version")) {
|
|
1176
|
+
streams.stdout.write(`${package_namespaceObject.rE}\n`);
|
|
1177
|
+
return 0;
|
|
1178
|
+
}
|
|
1134
1179
|
if (!parsed.command || parsed.flags.has("--help")) {
|
|
1135
1180
|
printHelp(streams.stdout);
|
|
1136
1181
|
return 0;
|
package/dist/main.cjs
CHANGED
|
@@ -42,6 +42,10 @@ const timezone_js_namespaceObject = require("dayjs/plugin/timezone.js");
|
|
|
42
42
|
var timezone_js_default = /*#__PURE__*/ __webpack_require__.n(timezone_js_namespaceObject);
|
|
43
43
|
const utc_js_namespaceObject = require("dayjs/plugin/utc.js");
|
|
44
44
|
var utc_js_default = /*#__PURE__*/ __webpack_require__.n(utc_js_namespaceObject);
|
|
45
|
+
var package_namespaceObject = {
|
|
46
|
+
rE: "0.0.6"
|
|
47
|
+
};
|
|
48
|
+
const external_node_crypto_namespaceObject = require("node:crypto");
|
|
45
49
|
const promises_namespaceObject = require("node:fs/promises");
|
|
46
50
|
function isRecord(value) {
|
|
47
51
|
return "object" == typeof value && null !== value && !Array.isArray(value);
|
|
@@ -64,6 +68,29 @@ function asOptionalNumber(value, fieldName) {
|
|
|
64
68
|
if ("number" != typeof value || Number.isNaN(value)) throw new Error(`Field "${fieldName}" must be a number.`);
|
|
65
69
|
return value;
|
|
66
70
|
}
|
|
71
|
+
function normalizeAuthMode(authMode) {
|
|
72
|
+
return authMode.trim().toLowerCase();
|
|
73
|
+
}
|
|
74
|
+
function isApiKeyAuthMode(authMode) {
|
|
75
|
+
return "apikey" === normalizeAuthMode(authMode);
|
|
76
|
+
}
|
|
77
|
+
function isSupportedChatGPTAuthMode(authMode) {
|
|
78
|
+
const normalized = normalizeAuthMode(authMode);
|
|
79
|
+
return "chatgpt" === normalized || "chatgpt_auth_tokens" === normalized;
|
|
80
|
+
}
|
|
81
|
+
function fingerprintApiKey(apiKey) {
|
|
82
|
+
return (0, external_node_crypto_namespaceObject.createHash)("sha256").update(apiKey).digest("hex").slice(0, 16);
|
|
83
|
+
}
|
|
84
|
+
function getSnapshotIdentity(snapshot) {
|
|
85
|
+
if (isApiKeyAuthMode(snapshot.auth_mode)) {
|
|
86
|
+
const apiKey = snapshot.OPENAI_API_KEY;
|
|
87
|
+
if ("string" != typeof apiKey || "" === apiKey.trim()) throw new Error('Field "OPENAI_API_KEY" must be a non-empty string for apikey auth.');
|
|
88
|
+
return `key_${fingerprintApiKey(apiKey)}`;
|
|
89
|
+
}
|
|
90
|
+
const accountId = snapshot.tokens?.account_id;
|
|
91
|
+
if ("string" != typeof accountId || "" === accountId.trim()) throw new Error('Field "tokens.account_id" must be a non-empty string.');
|
|
92
|
+
return accountId;
|
|
93
|
+
}
|
|
67
94
|
function defaultQuotaSnapshot() {
|
|
68
95
|
return {
|
|
69
96
|
status: "stale"
|
|
@@ -108,15 +135,28 @@ function parseAuthSnapshot(raw) {
|
|
|
108
135
|
}
|
|
109
136
|
if (!isRecord(parsed)) throw new Error("Auth snapshot must be a JSON object.");
|
|
110
137
|
const authMode = asNonEmptyString(parsed.auth_mode, "auth_mode");
|
|
111
|
-
|
|
112
|
-
|
|
138
|
+
const tokens = parsed.tokens;
|
|
139
|
+
if (null != tokens && !isRecord(tokens)) throw new Error('Field "tokens" must be an object.');
|
|
140
|
+
const apiKeyMode = isApiKeyAuthMode(authMode);
|
|
141
|
+
const normalizedApiKey = null === parsed.OPENAI_API_KEY || void 0 === parsed.OPENAI_API_KEY ? parsed.OPENAI_API_KEY : asNonEmptyString(parsed.OPENAI_API_KEY, "OPENAI_API_KEY");
|
|
142
|
+
if (apiKeyMode) {
|
|
143
|
+
if ("string" != typeof normalizedApiKey || "" === normalizedApiKey.trim()) throw new Error('Field "OPENAI_API_KEY" must be a non-empty string for apikey auth.');
|
|
144
|
+
} else {
|
|
145
|
+
if (!isRecord(tokens)) throw new Error('Field "tokens" must be an object.');
|
|
146
|
+
asNonEmptyString(tokens.account_id, "tokens.account_id");
|
|
147
|
+
}
|
|
113
148
|
return {
|
|
114
149
|
...parsed,
|
|
115
150
|
auth_mode: authMode,
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
151
|
+
OPENAI_API_KEY: normalizedApiKey,
|
|
152
|
+
...isRecord(tokens) ? {
|
|
153
|
+
tokens: {
|
|
154
|
+
...tokens,
|
|
155
|
+
..."string" == typeof tokens.account_id && "" !== tokens.account_id.trim() ? {
|
|
156
|
+
account_id: tokens.account_id
|
|
157
|
+
} : {}
|
|
158
|
+
}
|
|
159
|
+
} : {}
|
|
120
160
|
};
|
|
121
161
|
}
|
|
122
162
|
async function readAuthSnapshotFile(filePath) {
|
|
@@ -128,7 +168,7 @@ function createSnapshotMeta(name, snapshot, now, existingCreatedAt) {
|
|
|
128
168
|
return {
|
|
129
169
|
name,
|
|
130
170
|
auth_mode: snapshot.auth_mode,
|
|
131
|
-
account_id: snapshot
|
|
171
|
+
account_id: getSnapshotIdentity(snapshot),
|
|
132
172
|
created_at: existingCreatedAt ?? timestamp,
|
|
133
173
|
updated_at: timestamp,
|
|
134
174
|
last_switched_at: null,
|
|
@@ -185,16 +225,13 @@ function extractStringClaim(payload, key) {
|
|
|
185
225
|
const value = payload[key];
|
|
186
226
|
return "string" == typeof value && "" !== value.trim() ? value : void 0;
|
|
187
227
|
}
|
|
188
|
-
function isSupportedChatGPTMode(authMode) {
|
|
189
|
-
const normalized = authMode.trim().toLowerCase();
|
|
190
|
-
return "chatgpt" === normalized || "chatgpt_auth_tokens" === normalized;
|
|
191
|
-
}
|
|
192
228
|
function parsePlanType(snapshot) {
|
|
229
|
+
const tokens = snapshot.tokens ?? {};
|
|
193
230
|
for (const tokenName of [
|
|
194
231
|
"id_token",
|
|
195
232
|
"access_token"
|
|
196
233
|
]){
|
|
197
|
-
const token =
|
|
234
|
+
const token = tokens[tokenName];
|
|
198
235
|
if ("string" == typeof token && "" !== token.trim()) try {
|
|
199
236
|
const payload = decodeJwtPayload(token);
|
|
200
237
|
const authClaim = extractAuthClaim(payload);
|
|
@@ -205,10 +242,11 @@ function parsePlanType(snapshot) {
|
|
|
205
242
|
}
|
|
206
243
|
function extractChatGPTAuth(snapshot) {
|
|
207
244
|
const authMode = snapshot.auth_mode ?? "";
|
|
208
|
-
const supported =
|
|
209
|
-
const
|
|
210
|
-
const
|
|
211
|
-
const
|
|
245
|
+
const supported = isSupportedChatGPTAuthMode(authMode);
|
|
246
|
+
const tokens = snapshot.tokens ?? {};
|
|
247
|
+
const accessTokenValue = tokens.access_token;
|
|
248
|
+
const refreshTokenValue = tokens.refresh_token;
|
|
249
|
+
const directAccountId = tokens.account_id;
|
|
212
250
|
let accountId = "string" == typeof directAccountId && "" !== directAccountId.trim() ? directAccountId : void 0;
|
|
213
251
|
let planType;
|
|
214
252
|
let issuer;
|
|
@@ -217,7 +255,7 @@ function extractChatGPTAuth(snapshot) {
|
|
|
217
255
|
"id_token",
|
|
218
256
|
"access_token"
|
|
219
257
|
]){
|
|
220
|
-
const token =
|
|
258
|
+
const token = tokens[tokenName];
|
|
221
259
|
if ("string" == typeof token && "" !== token.trim()) try {
|
|
222
260
|
const payload = decodeJwtPayload(token);
|
|
223
261
|
const authClaim = extractAuthClaim(payload);
|
|
@@ -399,7 +437,7 @@ async function refreshChatGPTAuthTokens(snapshot, options) {
|
|
|
399
437
|
...snapshot,
|
|
400
438
|
last_refresh: (options.now ?? new Date()).toISOString(),
|
|
401
439
|
tokens: {
|
|
402
|
-
...snapshot.tokens,
|
|
440
|
+
...snapshot.tokens ?? {},
|
|
403
441
|
access_token: payload.access_token,
|
|
404
442
|
id_token: payload.id_token,
|
|
405
443
|
refresh_token: payload.refresh_token ?? extracted.refreshToken,
|
|
@@ -550,7 +588,7 @@ class AccountStore {
|
|
|
550
588
|
if (!await pathExists(this.paths.currentAuthPath)) return;
|
|
551
589
|
try {
|
|
552
590
|
const currentSnapshot = await readAuthSnapshotFile(this.paths.currentAuthPath);
|
|
553
|
-
if (currentSnapshot
|
|
591
|
+
if (getSnapshotIdentity(currentSnapshot) !== getSnapshotIdentity(snapshot)) return;
|
|
554
592
|
await atomicWriteFile(this.paths.currentAuthPath, stringifyJson(snapshot));
|
|
555
593
|
} catch {}
|
|
556
594
|
}
|
|
@@ -606,7 +644,7 @@ class AccountStore {
|
|
|
606
644
|
]);
|
|
607
645
|
const meta = parseSnapshotMeta(rawMeta);
|
|
608
646
|
if (meta.name !== name) throw new Error(`Account metadata name mismatch for "${name}".`);
|
|
609
|
-
if (meta.account_id !== snapshot
|
|
647
|
+
if (meta.account_id !== getSnapshotIdentity(snapshot)) throw new Error(`Account metadata account_id mismatch for "${name}".`);
|
|
610
648
|
return {
|
|
611
649
|
...meta,
|
|
612
650
|
authPath,
|
|
@@ -649,11 +687,12 @@ class AccountStore {
|
|
|
649
687
|
warnings
|
|
650
688
|
};
|
|
651
689
|
const snapshot = await readAuthSnapshotFile(this.paths.currentAuthPath);
|
|
652
|
-
const
|
|
690
|
+
const currentIdentity = getSnapshotIdentity(snapshot);
|
|
691
|
+
const matchedAccounts = accounts.filter((account)=>account.account_id === currentIdentity).map((account)=>account.name);
|
|
653
692
|
return {
|
|
654
693
|
exists: true,
|
|
655
694
|
auth_mode: snapshot.auth_mode,
|
|
656
|
-
account_id:
|
|
695
|
+
account_id: currentIdentity,
|
|
657
696
|
matched_accounts: matchedAccounts,
|
|
658
697
|
managed: matchedAccounts.length > 0,
|
|
659
698
|
duplicate_match: matchedAccounts.length > 1,
|
|
@@ -716,7 +755,7 @@ class AccountStore {
|
|
|
716
755
|
const rawAuth = await readJsonFile(account.authPath);
|
|
717
756
|
await atomicWriteFile(this.paths.currentAuthPath, `${rawAuth.trimEnd()}\n`);
|
|
718
757
|
const writtenSnapshot = await readAuthSnapshotFile(this.paths.currentAuthPath);
|
|
719
|
-
if (writtenSnapshot
|
|
758
|
+
if (getSnapshotIdentity(writtenSnapshot) !== account.account_id) throw new Error(`Switch verification failed for account "${name}".`);
|
|
720
759
|
const meta = parseSnapshotMeta(await readJsonFile(account.metaPath));
|
|
721
760
|
meta.last_switched_at = new Date().toISOString();
|
|
722
761
|
meta.updated_at = meta.last_switched_at;
|
|
@@ -760,7 +799,7 @@ class AccountStore {
|
|
|
760
799
|
await this.syncCurrentAuthIfMatching(result.authSnapshot);
|
|
761
800
|
}
|
|
762
801
|
meta.auth_mode = result.authSnapshot.auth_mode;
|
|
763
|
-
meta.account_id = result.authSnapshot
|
|
802
|
+
meta.account_id = getSnapshotIdentity(result.authSnapshot);
|
|
764
803
|
meta.updated_at = now.toISOString();
|
|
765
804
|
meta.quota = result.quota;
|
|
766
805
|
await this.writeAccountMeta(name, meta);
|
|
@@ -872,7 +911,7 @@ class AccountStore {
|
|
|
872
911
|
const metaStat = await (0, promises_namespaceObject.stat)(account.metaPath);
|
|
873
912
|
if ((511 & authStat.mode) !== FILE_MODE) issues.push(`Account "${account.name}" auth permissions must be 600.`);
|
|
874
913
|
if ((511 & metaStat.mode) !== FILE_MODE) issues.push(`Account "${account.name}" metadata permissions must be 600.`);
|
|
875
|
-
if (account.duplicateAccountId) warnings.push(`Account "${account.name}" shares
|
|
914
|
+
if (account.duplicateAccountId) warnings.push(`Account "${account.name}" shares identity ${account.account_id} with another saved account.`);
|
|
876
915
|
}
|
|
877
916
|
let currentAuthPresent = false;
|
|
878
917
|
if (await pathExists(this.paths.currentAuthPath)) {
|
|
@@ -934,6 +973,8 @@ function printHelp(stream) {
|
|
|
934
973
|
stream.write(`codexm - manage multiple Codex ChatGPT auth snapshots
|
|
935
974
|
|
|
936
975
|
Usage:
|
|
976
|
+
codexm --version
|
|
977
|
+
codexm --help
|
|
937
978
|
codexm current [--json]
|
|
938
979
|
codexm list [name] [--json]
|
|
939
980
|
codexm save <name> [--force] [--json]
|
|
@@ -953,7 +994,7 @@ function describeCurrentStatus(status) {
|
|
|
953
994
|
if (status.exists) {
|
|
954
995
|
lines.push("Current auth: present");
|
|
955
996
|
lines.push(`Auth mode: ${status.auth_mode}`);
|
|
956
|
-
lines.push(`
|
|
997
|
+
lines.push(`Identity: ${maskAccountId(status.account_id ?? "")}`);
|
|
957
998
|
if (0 === status.matched_accounts.length) lines.push("Managed account: no (unmanaged)");
|
|
958
999
|
else if (1 === status.matched_accounts.length) lines.push(`Managed account: ${status.matched_accounts[0]}`);
|
|
959
1000
|
else lines.push(`Managed account: multiple (${status.matched_accounts.join(", ")})`);
|
|
@@ -1086,7 +1127,7 @@ function describeQuotaAccounts(accounts, warnings) {
|
|
|
1086
1127
|
},
|
|
1087
1128
|
{
|
|
1088
1129
|
key: "account_id",
|
|
1089
|
-
label: "
|
|
1130
|
+
label: "IDENTITY"
|
|
1090
1131
|
},
|
|
1091
1132
|
{
|
|
1092
1133
|
key: "plan_type",
|
|
@@ -1161,6 +1202,10 @@ async function runCli(argv, options = {}) {
|
|
|
1161
1202
|
const parsed = parseArgs(argv);
|
|
1162
1203
|
const json = parsed.flags.has("--json");
|
|
1163
1204
|
try {
|
|
1205
|
+
if (parsed.flags.has("--version")) {
|
|
1206
|
+
streams.stdout.write(`${package_namespaceObject.rE}\n`);
|
|
1207
|
+
return 0;
|
|
1208
|
+
}
|
|
1164
1209
|
if (!parsed.command || parsed.flags.has("--help")) {
|
|
1165
1210
|
printHelp(streams.stdout);
|
|
1166
1211
|
return 0;
|
package/dist/main.js
CHANGED
|
@@ -2,11 +2,15 @@ import { stderr, stdin, stdout as external_node_process_stdout } from "node:proc
|
|
|
2
2
|
import dayjs from "dayjs";
|
|
3
3
|
import timezone from "dayjs/plugin/timezone.js";
|
|
4
4
|
import utc from "dayjs/plugin/utc.js";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
5
6
|
import { chmod, copyFile, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
6
7
|
import { homedir } from "node:os";
|
|
7
8
|
import { basename, dirname, join } from "node:path";
|
|
8
9
|
import { execFile } from "node:child_process";
|
|
9
10
|
import { promisify } from "node:util";
|
|
11
|
+
var package_namespaceObject = {
|
|
12
|
+
rE: "0.0.6"
|
|
13
|
+
};
|
|
10
14
|
function isRecord(value) {
|
|
11
15
|
return "object" == typeof value && null !== value && !Array.isArray(value);
|
|
12
16
|
}
|
|
@@ -28,6 +32,29 @@ function asOptionalNumber(value, fieldName) {
|
|
|
28
32
|
if ("number" != typeof value || Number.isNaN(value)) throw new Error(`Field "${fieldName}" must be a number.`);
|
|
29
33
|
return value;
|
|
30
34
|
}
|
|
35
|
+
function normalizeAuthMode(authMode) {
|
|
36
|
+
return authMode.trim().toLowerCase();
|
|
37
|
+
}
|
|
38
|
+
function isApiKeyAuthMode(authMode) {
|
|
39
|
+
return "apikey" === normalizeAuthMode(authMode);
|
|
40
|
+
}
|
|
41
|
+
function isSupportedChatGPTAuthMode(authMode) {
|
|
42
|
+
const normalized = normalizeAuthMode(authMode);
|
|
43
|
+
return "chatgpt" === normalized || "chatgpt_auth_tokens" === normalized;
|
|
44
|
+
}
|
|
45
|
+
function fingerprintApiKey(apiKey) {
|
|
46
|
+
return createHash("sha256").update(apiKey).digest("hex").slice(0, 16);
|
|
47
|
+
}
|
|
48
|
+
function getSnapshotIdentity(snapshot) {
|
|
49
|
+
if (isApiKeyAuthMode(snapshot.auth_mode)) {
|
|
50
|
+
const apiKey = snapshot.OPENAI_API_KEY;
|
|
51
|
+
if ("string" != typeof apiKey || "" === apiKey.trim()) throw new Error('Field "OPENAI_API_KEY" must be a non-empty string for apikey auth.');
|
|
52
|
+
return `key_${fingerprintApiKey(apiKey)}`;
|
|
53
|
+
}
|
|
54
|
+
const accountId = snapshot.tokens?.account_id;
|
|
55
|
+
if ("string" != typeof accountId || "" === accountId.trim()) throw new Error('Field "tokens.account_id" must be a non-empty string.');
|
|
56
|
+
return accountId;
|
|
57
|
+
}
|
|
31
58
|
function defaultQuotaSnapshot() {
|
|
32
59
|
return {
|
|
33
60
|
status: "stale"
|
|
@@ -72,15 +99,28 @@ function parseAuthSnapshot(raw) {
|
|
|
72
99
|
}
|
|
73
100
|
if (!isRecord(parsed)) throw new Error("Auth snapshot must be a JSON object.");
|
|
74
101
|
const authMode = asNonEmptyString(parsed.auth_mode, "auth_mode");
|
|
75
|
-
|
|
76
|
-
|
|
102
|
+
const tokens = parsed.tokens;
|
|
103
|
+
if (null != tokens && !isRecord(tokens)) throw new Error('Field "tokens" must be an object.');
|
|
104
|
+
const apiKeyMode = isApiKeyAuthMode(authMode);
|
|
105
|
+
const normalizedApiKey = null === parsed.OPENAI_API_KEY || void 0 === parsed.OPENAI_API_KEY ? parsed.OPENAI_API_KEY : asNonEmptyString(parsed.OPENAI_API_KEY, "OPENAI_API_KEY");
|
|
106
|
+
if (apiKeyMode) {
|
|
107
|
+
if ("string" != typeof normalizedApiKey || "" === normalizedApiKey.trim()) throw new Error('Field "OPENAI_API_KEY" must be a non-empty string for apikey auth.');
|
|
108
|
+
} else {
|
|
109
|
+
if (!isRecord(tokens)) throw new Error('Field "tokens" must be an object.');
|
|
110
|
+
asNonEmptyString(tokens.account_id, "tokens.account_id");
|
|
111
|
+
}
|
|
77
112
|
return {
|
|
78
113
|
...parsed,
|
|
79
114
|
auth_mode: authMode,
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
115
|
+
OPENAI_API_KEY: normalizedApiKey,
|
|
116
|
+
...isRecord(tokens) ? {
|
|
117
|
+
tokens: {
|
|
118
|
+
...tokens,
|
|
119
|
+
..."string" == typeof tokens.account_id && "" !== tokens.account_id.trim() ? {
|
|
120
|
+
account_id: tokens.account_id
|
|
121
|
+
} : {}
|
|
122
|
+
}
|
|
123
|
+
} : {}
|
|
84
124
|
};
|
|
85
125
|
}
|
|
86
126
|
async function readAuthSnapshotFile(filePath) {
|
|
@@ -92,7 +132,7 @@ function createSnapshotMeta(name, snapshot, now, existingCreatedAt) {
|
|
|
92
132
|
return {
|
|
93
133
|
name,
|
|
94
134
|
auth_mode: snapshot.auth_mode,
|
|
95
|
-
account_id: snapshot
|
|
135
|
+
account_id: getSnapshotIdentity(snapshot),
|
|
96
136
|
created_at: existingCreatedAt ?? timestamp,
|
|
97
137
|
updated_at: timestamp,
|
|
98
138
|
last_switched_at: null,
|
|
@@ -145,16 +185,13 @@ function extractStringClaim(payload, key) {
|
|
|
145
185
|
const value = payload[key];
|
|
146
186
|
return "string" == typeof value && "" !== value.trim() ? value : void 0;
|
|
147
187
|
}
|
|
148
|
-
function isSupportedChatGPTMode(authMode) {
|
|
149
|
-
const normalized = authMode.trim().toLowerCase();
|
|
150
|
-
return "chatgpt" === normalized || "chatgpt_auth_tokens" === normalized;
|
|
151
|
-
}
|
|
152
188
|
function parsePlanType(snapshot) {
|
|
189
|
+
const tokens = snapshot.tokens ?? {};
|
|
153
190
|
for (const tokenName of [
|
|
154
191
|
"id_token",
|
|
155
192
|
"access_token"
|
|
156
193
|
]){
|
|
157
|
-
const token =
|
|
194
|
+
const token = tokens[tokenName];
|
|
158
195
|
if ("string" == typeof token && "" !== token.trim()) try {
|
|
159
196
|
const payload = decodeJwtPayload(token);
|
|
160
197
|
const authClaim = extractAuthClaim(payload);
|
|
@@ -165,10 +202,11 @@ function parsePlanType(snapshot) {
|
|
|
165
202
|
}
|
|
166
203
|
function extractChatGPTAuth(snapshot) {
|
|
167
204
|
const authMode = snapshot.auth_mode ?? "";
|
|
168
|
-
const supported =
|
|
169
|
-
const
|
|
170
|
-
const
|
|
171
|
-
const
|
|
205
|
+
const supported = isSupportedChatGPTAuthMode(authMode);
|
|
206
|
+
const tokens = snapshot.tokens ?? {};
|
|
207
|
+
const accessTokenValue = tokens.access_token;
|
|
208
|
+
const refreshTokenValue = tokens.refresh_token;
|
|
209
|
+
const directAccountId = tokens.account_id;
|
|
172
210
|
let accountId = "string" == typeof directAccountId && "" !== directAccountId.trim() ? directAccountId : void 0;
|
|
173
211
|
let planType;
|
|
174
212
|
let issuer;
|
|
@@ -177,7 +215,7 @@ function extractChatGPTAuth(snapshot) {
|
|
|
177
215
|
"id_token",
|
|
178
216
|
"access_token"
|
|
179
217
|
]){
|
|
180
|
-
const token =
|
|
218
|
+
const token = tokens[tokenName];
|
|
181
219
|
if ("string" == typeof token && "" !== token.trim()) try {
|
|
182
220
|
const payload = decodeJwtPayload(token);
|
|
183
221
|
const authClaim = extractAuthClaim(payload);
|
|
@@ -359,7 +397,7 @@ async function refreshChatGPTAuthTokens(snapshot, options) {
|
|
|
359
397
|
...snapshot,
|
|
360
398
|
last_refresh: (options.now ?? new Date()).toISOString(),
|
|
361
399
|
tokens: {
|
|
362
|
-
...snapshot.tokens,
|
|
400
|
+
...snapshot.tokens ?? {},
|
|
363
401
|
access_token: payload.access_token,
|
|
364
402
|
id_token: payload.id_token,
|
|
365
403
|
refresh_token: payload.refresh_token ?? extracted.refreshToken,
|
|
@@ -510,7 +548,7 @@ class AccountStore {
|
|
|
510
548
|
if (!await pathExists(this.paths.currentAuthPath)) return;
|
|
511
549
|
try {
|
|
512
550
|
const currentSnapshot = await readAuthSnapshotFile(this.paths.currentAuthPath);
|
|
513
|
-
if (currentSnapshot
|
|
551
|
+
if (getSnapshotIdentity(currentSnapshot) !== getSnapshotIdentity(snapshot)) return;
|
|
514
552
|
await atomicWriteFile(this.paths.currentAuthPath, stringifyJson(snapshot));
|
|
515
553
|
} catch {}
|
|
516
554
|
}
|
|
@@ -566,7 +604,7 @@ class AccountStore {
|
|
|
566
604
|
]);
|
|
567
605
|
const meta = parseSnapshotMeta(rawMeta);
|
|
568
606
|
if (meta.name !== name) throw new Error(`Account metadata name mismatch for "${name}".`);
|
|
569
|
-
if (meta.account_id !== snapshot
|
|
607
|
+
if (meta.account_id !== getSnapshotIdentity(snapshot)) throw new Error(`Account metadata account_id mismatch for "${name}".`);
|
|
570
608
|
return {
|
|
571
609
|
...meta,
|
|
572
610
|
authPath,
|
|
@@ -609,11 +647,12 @@ class AccountStore {
|
|
|
609
647
|
warnings
|
|
610
648
|
};
|
|
611
649
|
const snapshot = await readAuthSnapshotFile(this.paths.currentAuthPath);
|
|
612
|
-
const
|
|
650
|
+
const currentIdentity = getSnapshotIdentity(snapshot);
|
|
651
|
+
const matchedAccounts = accounts.filter((account)=>account.account_id === currentIdentity).map((account)=>account.name);
|
|
613
652
|
return {
|
|
614
653
|
exists: true,
|
|
615
654
|
auth_mode: snapshot.auth_mode,
|
|
616
|
-
account_id:
|
|
655
|
+
account_id: currentIdentity,
|
|
617
656
|
matched_accounts: matchedAccounts,
|
|
618
657
|
managed: matchedAccounts.length > 0,
|
|
619
658
|
duplicate_match: matchedAccounts.length > 1,
|
|
@@ -676,7 +715,7 @@ class AccountStore {
|
|
|
676
715
|
const rawAuth = await readJsonFile(account.authPath);
|
|
677
716
|
await atomicWriteFile(this.paths.currentAuthPath, `${rawAuth.trimEnd()}\n`);
|
|
678
717
|
const writtenSnapshot = await readAuthSnapshotFile(this.paths.currentAuthPath);
|
|
679
|
-
if (writtenSnapshot
|
|
718
|
+
if (getSnapshotIdentity(writtenSnapshot) !== account.account_id) throw new Error(`Switch verification failed for account "${name}".`);
|
|
680
719
|
const meta = parseSnapshotMeta(await readJsonFile(account.metaPath));
|
|
681
720
|
meta.last_switched_at = new Date().toISOString();
|
|
682
721
|
meta.updated_at = meta.last_switched_at;
|
|
@@ -720,7 +759,7 @@ class AccountStore {
|
|
|
720
759
|
await this.syncCurrentAuthIfMatching(result.authSnapshot);
|
|
721
760
|
}
|
|
722
761
|
meta.auth_mode = result.authSnapshot.auth_mode;
|
|
723
|
-
meta.account_id = result.authSnapshot
|
|
762
|
+
meta.account_id = getSnapshotIdentity(result.authSnapshot);
|
|
724
763
|
meta.updated_at = now.toISOString();
|
|
725
764
|
meta.quota = result.quota;
|
|
726
765
|
await this.writeAccountMeta(name, meta);
|
|
@@ -832,7 +871,7 @@ class AccountStore {
|
|
|
832
871
|
const metaStat = await stat(account.metaPath);
|
|
833
872
|
if ((511 & authStat.mode) !== FILE_MODE) issues.push(`Account "${account.name}" auth permissions must be 600.`);
|
|
834
873
|
if ((511 & metaStat.mode) !== FILE_MODE) issues.push(`Account "${account.name}" metadata permissions must be 600.`);
|
|
835
|
-
if (account.duplicateAccountId) warnings.push(`Account "${account.name}" shares
|
|
874
|
+
if (account.duplicateAccountId) warnings.push(`Account "${account.name}" shares identity ${account.account_id} with another saved account.`);
|
|
836
875
|
}
|
|
837
876
|
let currentAuthPresent = false;
|
|
838
877
|
if (await pathExists(this.paths.currentAuthPath)) {
|
|
@@ -894,6 +933,8 @@ function printHelp(stream) {
|
|
|
894
933
|
stream.write(`codexm - manage multiple Codex ChatGPT auth snapshots
|
|
895
934
|
|
|
896
935
|
Usage:
|
|
936
|
+
codexm --version
|
|
937
|
+
codexm --help
|
|
897
938
|
codexm current [--json]
|
|
898
939
|
codexm list [name] [--json]
|
|
899
940
|
codexm save <name> [--force] [--json]
|
|
@@ -913,7 +954,7 @@ function describeCurrentStatus(status) {
|
|
|
913
954
|
if (status.exists) {
|
|
914
955
|
lines.push("Current auth: present");
|
|
915
956
|
lines.push(`Auth mode: ${status.auth_mode}`);
|
|
916
|
-
lines.push(`
|
|
957
|
+
lines.push(`Identity: ${maskAccountId(status.account_id ?? "")}`);
|
|
917
958
|
if (0 === status.matched_accounts.length) lines.push("Managed account: no (unmanaged)");
|
|
918
959
|
else if (1 === status.matched_accounts.length) lines.push(`Managed account: ${status.matched_accounts[0]}`);
|
|
919
960
|
else lines.push(`Managed account: multiple (${status.matched_accounts.join(", ")})`);
|
|
@@ -1046,7 +1087,7 @@ function describeQuotaAccounts(accounts, warnings) {
|
|
|
1046
1087
|
},
|
|
1047
1088
|
{
|
|
1048
1089
|
key: "account_id",
|
|
1049
|
-
label: "
|
|
1090
|
+
label: "IDENTITY"
|
|
1050
1091
|
},
|
|
1051
1092
|
{
|
|
1052
1093
|
key: "plan_type",
|
|
@@ -1121,6 +1162,10 @@ async function runCli(argv, options = {}) {
|
|
|
1121
1162
|
const parsed = parseArgs(argv);
|
|
1122
1163
|
const json = parsed.flags.has("--json");
|
|
1123
1164
|
try {
|
|
1165
|
+
if (parsed.flags.has("--version")) {
|
|
1166
|
+
streams.stdout.write(`${package_namespaceObject.rE}\n`);
|
|
1167
|
+
return 0;
|
|
1168
|
+
}
|
|
1124
1169
|
if (!parsed.command || parsed.flags.has("--help")) {
|
|
1125
1170
|
printHelp(streams.stdout);
|
|
1126
1171
|
return 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-team",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "Manage multiple Codex ChatGPT auth snapshots and quota usage from the command line.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -36,14 +36,6 @@
|
|
|
36
36
|
"LICENSE",
|
|
37
37
|
"README.md"
|
|
38
38
|
],
|
|
39
|
-
"scripts": {
|
|
40
|
-
"build": "rslib build",
|
|
41
|
-
"dev": "rslib build --watch",
|
|
42
|
-
"prepack": "pnpm build",
|
|
43
|
-
"test": "rstest run",
|
|
44
|
-
"test:watch": "rstest watch",
|
|
45
|
-
"typecheck": "tsc --noEmit"
|
|
46
|
-
},
|
|
47
39
|
"engines": {
|
|
48
40
|
"node": ">=20"
|
|
49
41
|
},
|
|
@@ -59,5 +51,12 @@
|
|
|
59
51
|
},
|
|
60
52
|
"dependencies": {
|
|
61
53
|
"dayjs": "^1.11.20"
|
|
54
|
+
},
|
|
55
|
+
"scripts": {
|
|
56
|
+
"build": "rslib build",
|
|
57
|
+
"dev": "rslib build --watch",
|
|
58
|
+
"test": "rstest run",
|
|
59
|
+
"test:watch": "rstest watch",
|
|
60
|
+
"typecheck": "tsc --noEmit"
|
|
62
61
|
}
|
|
63
|
-
}
|
|
62
|
+
}
|