@vocoder/cli 0.10.0 → 0.11.0
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/bin.mjs +143 -864
- package/dist/bin.mjs.map +1 -1
- package/dist/{chunk-73U4VZYP.mjs → chunk-XF3KGGYQ.mjs} +742 -37
- package/dist/chunk-XF3KGGYQ.mjs.map +1 -0
- package/dist/lib.d.mts +309 -3
- package/dist/lib.mjs +13 -3
- package/dist/lib.mjs.map +1 -1
- package/package.json +3 -3
- package/dist/chunk-73U4VZYP.mjs.map +0 -1
package/dist/bin.mjs
CHANGED
|
@@ -2,759 +2,36 @@
|
|
|
2
2
|
import { createRequire as __createRequire } from 'module'; const require = __createRequire(import.meta.url);
|
|
3
3
|
import {
|
|
4
4
|
StringExtractor,
|
|
5
|
+
VocoderAPI,
|
|
6
|
+
VocoderAPIError,
|
|
5
7
|
buildInstallCommand,
|
|
8
|
+
clearAuthData,
|
|
6
9
|
detectLocalEcosystem,
|
|
7
10
|
getPackagesToInstall,
|
|
8
11
|
getSetupSnippets,
|
|
9
|
-
loadVocoderConfig
|
|
10
|
-
|
|
12
|
+
loadVocoderConfig,
|
|
13
|
+
readAuthData,
|
|
14
|
+
writeAuthData
|
|
15
|
+
} from "./chunk-XF3KGGYQ.mjs";
|
|
11
16
|
|
|
12
17
|
// src/bin.ts
|
|
13
18
|
import { Command } from "commander";
|
|
14
19
|
|
|
15
20
|
// src/commands/init.ts
|
|
16
|
-
import { execSync as execSync3, spawn as spawn2 } from "child_process";
|
|
17
|
-
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
18
|
-
import { join as join3 } from "path";
|
|
19
21
|
import * as p5 from "@clack/prompts";
|
|
20
|
-
import
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
// src/utils/api.ts
|
|
24
|
-
function isLimitErrorResponse(value) {
|
|
25
|
-
if (!value || typeof value !== "object") {
|
|
26
|
-
return false;
|
|
27
|
-
}
|
|
28
|
-
const candidate = value;
|
|
29
|
-
return typeof candidate.errorCode === "string" && typeof candidate.limitType === "string" && typeof candidate.planId === "string" && typeof candidate.current === "number" && typeof candidate.required === "number" && typeof candidate.upgradeUrl === "string" && typeof candidate.message === "string";
|
|
30
|
-
}
|
|
31
|
-
function isSyncPolicyErrorResponse(value) {
|
|
32
|
-
if (!value || typeof value !== "object") {
|
|
33
|
-
return false;
|
|
34
|
-
}
|
|
35
|
-
const candidate = value;
|
|
36
|
-
return (candidate.errorCode === "BRANCH_NOT_ALLOWED" || candidate.errorCode === "PROJECT_REPOSITORY_MISMATCH") && typeof candidate.message === "string";
|
|
37
|
-
}
|
|
38
|
-
function extractErrorMessage(payload, fallback) {
|
|
39
|
-
if (!payload || typeof payload !== "object") {
|
|
40
|
-
return fallback;
|
|
41
|
-
}
|
|
42
|
-
const candidate = payload;
|
|
43
|
-
if (typeof candidate.message === "string") {
|
|
44
|
-
return candidate.message;
|
|
45
|
-
}
|
|
46
|
-
if (typeof candidate.error === "string") {
|
|
47
|
-
return candidate.error;
|
|
48
|
-
}
|
|
49
|
-
return fallback;
|
|
50
|
-
}
|
|
51
|
-
function parsePayload(raw) {
|
|
52
|
-
if (raw.length === 0) {
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
const trimmed = raw.trimStart();
|
|
56
|
-
if (trimmed.startsWith("<!DOCTYPE") || trimmed.startsWith("<html")) {
|
|
57
|
-
return {
|
|
58
|
-
message: "Unexpected response from server (received HTML). Check your network connection or try again."
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
try {
|
|
62
|
-
return JSON.parse(raw);
|
|
63
|
-
} catch {
|
|
64
|
-
return { message: raw };
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
async function readPayload(response) {
|
|
68
|
-
if (typeof response.text === "function") {
|
|
69
|
-
const raw = await response.text();
|
|
70
|
-
return parsePayload(raw);
|
|
71
|
-
}
|
|
72
|
-
if (typeof response.json === "function") {
|
|
73
|
-
return response.json();
|
|
74
|
-
}
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
var VocoderAPIError = class extends Error {
|
|
78
|
-
constructor(params) {
|
|
79
|
-
super(params.message);
|
|
80
|
-
this.name = "VocoderAPIError";
|
|
81
|
-
this.status = params.status;
|
|
82
|
-
this.payload = params.payload;
|
|
83
|
-
this.limitError = params.limitError ?? null;
|
|
84
|
-
this.syncPolicyError = params.syncPolicyError ?? null;
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
|
-
var VocoderAPI = class {
|
|
88
|
-
constructor(config) {
|
|
89
|
-
this.apiUrl = config.apiUrl;
|
|
90
|
-
this.apiKey = config.apiKey;
|
|
91
|
-
}
|
|
92
|
-
async request(path, init2 = {}, errorPrefix) {
|
|
93
|
-
const response = await fetch(`${this.apiUrl}${path}`, {
|
|
94
|
-
...init2,
|
|
95
|
-
headers: {
|
|
96
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
97
|
-
...init2.headers ?? {}
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
const payload = await readPayload(response);
|
|
101
|
-
if (!response.ok) {
|
|
102
|
-
const limitError = isLimitErrorResponse(payload) ? payload : null;
|
|
103
|
-
const syncPolicyError = isSyncPolicyErrorResponse(payload) ? payload : null;
|
|
104
|
-
const baseMessage = extractErrorMessage(
|
|
105
|
-
payload,
|
|
106
|
-
`Request failed with status ${response.status}`
|
|
107
|
-
);
|
|
108
|
-
throw new VocoderAPIError({
|
|
109
|
-
message: errorPrefix ? `${errorPrefix}: ${baseMessage}` : baseMessage,
|
|
110
|
-
status: response.status,
|
|
111
|
-
payload,
|
|
112
|
-
limitError,
|
|
113
|
-
syncPolicyError
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
return payload;
|
|
117
|
-
}
|
|
118
|
-
/**
|
|
119
|
-
* Fetch project configuration from API
|
|
120
|
-
* Project is determined from the API key
|
|
121
|
-
*/
|
|
122
|
-
async getProjectConfig() {
|
|
123
|
-
const data = await this.request("/api/cli/config", {}, "Failed to fetch project config");
|
|
124
|
-
return {
|
|
125
|
-
projectName: data.projectName,
|
|
126
|
-
organizationName: data.organizationName,
|
|
127
|
-
shortCode: data.shortCode,
|
|
128
|
-
sourceLocale: data.sourceLocale,
|
|
129
|
-
targetLocales: data.targetLocales,
|
|
130
|
-
targetBranches: data.targetBranches ?? ["main"],
|
|
131
|
-
primaryBranch: data.primaryBranch,
|
|
132
|
-
syncPolicy: {
|
|
133
|
-
blockingBranches: data.syncPolicy?.blockingBranches ?? [
|
|
134
|
-
"main",
|
|
135
|
-
"master"
|
|
136
|
-
],
|
|
137
|
-
blockingMode: data.syncPolicy?.blockingMode ?? "required",
|
|
138
|
-
nonBlockingMode: data.syncPolicy?.nonBlockingMode ?? "best-effort",
|
|
139
|
-
defaultMaxWaitMs: data.syncPolicy?.defaultMaxWaitMs ?? 6e4
|
|
140
|
-
}
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
/**
|
|
144
|
-
* Submit strings for translation
|
|
145
|
-
* Project is determined from the API key
|
|
146
|
-
*/
|
|
147
|
-
stableTextKey(text2) {
|
|
148
|
-
let hash = 2166136261;
|
|
149
|
-
for (let i = 0; i < text2.length; i++) {
|
|
150
|
-
hash ^= text2.charCodeAt(i);
|
|
151
|
-
hash = Math.imul(hash, 16777619);
|
|
152
|
-
}
|
|
153
|
-
return `SK_TEXT_${(hash >>> 0).toString(16).toUpperCase().padStart(8, "0")}`;
|
|
154
|
-
}
|
|
155
|
-
normalizeStringEntries(entries) {
|
|
156
|
-
if (entries.length === 0) {
|
|
157
|
-
return [];
|
|
158
|
-
}
|
|
159
|
-
const first = entries[0];
|
|
160
|
-
if (typeof first === "string") {
|
|
161
|
-
return entries.map((text2) => ({
|
|
162
|
-
key: this.stableTextKey(text2),
|
|
163
|
-
text: text2
|
|
164
|
-
}));
|
|
165
|
-
}
|
|
166
|
-
return entries.map((entry, index) => ({
|
|
167
|
-
key: entry.key || this.stableTextKey(`${entry.text}:${index}`),
|
|
168
|
-
text: entry.text,
|
|
169
|
-
...entry.context ? { context: entry.context } : {},
|
|
170
|
-
...entry.formality ? { formality: entry.formality } : {},
|
|
171
|
-
...entry.uiRole ? { uiRole: entry.uiRole } : {},
|
|
172
|
-
...entry.featureArea ? { featureArea: entry.featureArea } : {}
|
|
173
|
-
}));
|
|
174
|
-
}
|
|
175
|
-
async submitTranslation(branch, entries, targetLocales, options, repoIdentity) {
|
|
176
|
-
const stringEntries = this.normalizeStringEntries(entries);
|
|
177
|
-
const strings = stringEntries.map((entry) => entry.text);
|
|
178
|
-
const crypto = await import("crypto");
|
|
179
|
-
const sortedStrings = [...strings].sort();
|
|
180
|
-
const stringsHash = crypto.createHash("sha256").update(JSON.stringify(sortedStrings)).digest("hex");
|
|
181
|
-
return this.request(
|
|
182
|
-
"/api/cli/sync",
|
|
183
|
-
{
|
|
184
|
-
method: "POST",
|
|
185
|
-
headers: {
|
|
186
|
-
"Content-Type": "application/json"
|
|
187
|
-
},
|
|
188
|
-
body: JSON.stringify({
|
|
189
|
-
branch,
|
|
190
|
-
stringEntries,
|
|
191
|
-
targetLocales,
|
|
192
|
-
...options?.force ? {} : { stringsHash },
|
|
193
|
-
...options?.requestedMode ? { requestedMode: options.requestedMode } : {},
|
|
194
|
-
...typeof options?.requestedMaxWaitMs === "number" ? { requestedMaxWaitMs: options.requestedMaxWaitMs } : {},
|
|
195
|
-
...options?.clientRunId ? { clientRunId: options.clientRunId } : {},
|
|
196
|
-
...repoIdentity?.repoCanonical ? { repoCanonical: repoIdentity.repoCanonical } : {},
|
|
197
|
-
...repoIdentity?.repoAppDir !== void 0 ? { repoAppDir: repoIdentity.repoAppDir } : {},
|
|
198
|
-
...repoIdentity?.commitSha ? { commitSha: repoIdentity.commitSha } : {},
|
|
199
|
-
...options?.appIndustry ? { appIndustry: options.appIndustry } : {}
|
|
200
|
-
})
|
|
201
|
-
},
|
|
202
|
-
"Translation submission failed"
|
|
203
|
-
);
|
|
204
|
-
}
|
|
205
|
-
/**
|
|
206
|
-
* Check translation status
|
|
207
|
-
*/
|
|
208
|
-
async getTranslationStatus(batchId) {
|
|
209
|
-
return this.request(
|
|
210
|
-
`/api/cli/sync/status/${batchId}`,
|
|
211
|
-
{},
|
|
212
|
-
"Failed to check translation status"
|
|
213
|
-
);
|
|
214
|
-
}
|
|
215
|
-
async getTranslationSnapshot(params) {
|
|
216
|
-
const search = new URLSearchParams();
|
|
217
|
-
search.set("branch", params.branch);
|
|
218
|
-
for (const locale of params.targetLocales) {
|
|
219
|
-
search.append("targetLocale", locale);
|
|
220
|
-
}
|
|
221
|
-
return this.request(
|
|
222
|
-
`/api/cli/sync/snapshot?${search.toString()}`,
|
|
223
|
-
{},
|
|
224
|
-
"Failed to fetch translation snapshot"
|
|
225
|
-
);
|
|
226
|
-
}
|
|
227
|
-
/**
|
|
228
|
-
* Wait for translation to complete with polling
|
|
229
|
-
*/
|
|
230
|
-
async waitForCompletion(batchId, timeout = 6e4, onProgress) {
|
|
231
|
-
const startTime = Date.now();
|
|
232
|
-
const pollInterval = 1e3;
|
|
233
|
-
while (Date.now() - startTime < timeout) {
|
|
234
|
-
const status = await this.getTranslationStatus(batchId);
|
|
235
|
-
if (onProgress) {
|
|
236
|
-
onProgress(status.progress);
|
|
237
|
-
}
|
|
238
|
-
if (status.status === "COMPLETED") {
|
|
239
|
-
if (!status.translations) {
|
|
240
|
-
throw new Error("Translation completed but no translations returned");
|
|
241
|
-
}
|
|
242
|
-
return {
|
|
243
|
-
translations: status.translations,
|
|
244
|
-
localeMetadata: status.localeMetadata
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
if (status.status === "FAILED") {
|
|
248
|
-
throw new Error(
|
|
249
|
-
`Translation failed: ${status.errorMessage || "Unknown error"}`
|
|
250
|
-
);
|
|
251
|
-
}
|
|
252
|
-
await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
|
|
253
|
-
}
|
|
254
|
-
throw new Error(`Translation timeout after ${timeout}ms`);
|
|
255
|
-
}
|
|
256
|
-
async startInitSession(input) {
|
|
257
|
-
const response = await fetch(`${this.apiUrl}/api/cli/init/start`, {
|
|
258
|
-
method: "POST",
|
|
259
|
-
headers: {
|
|
260
|
-
"Content-Type": "application/json"
|
|
261
|
-
},
|
|
262
|
-
body: JSON.stringify(input)
|
|
263
|
-
});
|
|
264
|
-
const payload = await readPayload(response);
|
|
265
|
-
if (!response.ok) {
|
|
266
|
-
throw new VocoderAPIError({
|
|
267
|
-
message: extractErrorMessage(
|
|
268
|
-
payload,
|
|
269
|
-
`Failed to start init session (${response.status})`
|
|
270
|
-
),
|
|
271
|
-
status: response.status,
|
|
272
|
-
payload
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
return payload;
|
|
276
|
-
}
|
|
277
|
-
async getInitSessionStatus(params) {
|
|
278
|
-
const response = await fetch(
|
|
279
|
-
`${this.apiUrl}/api/cli/init/status/${params.sessionId}`,
|
|
280
|
-
{
|
|
281
|
-
headers: {
|
|
282
|
-
Authorization: `Bearer ${params.pollToken}`
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
);
|
|
286
|
-
const payload = await readPayload(response);
|
|
287
|
-
if (!response.ok) {
|
|
288
|
-
throw new VocoderAPIError({
|
|
289
|
-
message: extractErrorMessage(
|
|
290
|
-
payload,
|
|
291
|
-
`Failed to get init status (${response.status})`
|
|
292
|
-
),
|
|
293
|
-
status: response.status,
|
|
294
|
-
payload
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
return payload;
|
|
298
|
-
}
|
|
299
|
-
// ── CLI Auth endpoints (no project API key needed) ──────────────────────────
|
|
300
|
-
/**
|
|
301
|
-
* Start a CLI auth session. Returns `{ sessionId, verificationUrl, expiresAt }`.
|
|
302
|
-
* `sessionId` is the raw poll token — keep it secret, used for polling.
|
|
303
|
-
*/
|
|
304
|
-
async startCliAuthSession(callbackPort, repoCanonical) {
|
|
305
|
-
const response = await fetch(`${this.apiUrl}/api/cli/auth/start`, {
|
|
306
|
-
method: "POST",
|
|
307
|
-
headers: { "Content-Type": "application/json" },
|
|
308
|
-
body: JSON.stringify({
|
|
309
|
-
...callbackPort != null ? { callbackPort } : {},
|
|
310
|
-
...repoCanonical ? { repoCanonical } : {}
|
|
311
|
-
})
|
|
312
|
-
});
|
|
313
|
-
const payload = await readPayload(response);
|
|
314
|
-
if (!response.ok) {
|
|
315
|
-
throw new VocoderAPIError({
|
|
316
|
-
message: extractErrorMessage(
|
|
317
|
-
payload,
|
|
318
|
-
`Failed to start auth session (${response.status})`
|
|
319
|
-
),
|
|
320
|
-
status: response.status,
|
|
321
|
-
payload
|
|
322
|
-
});
|
|
323
|
-
}
|
|
324
|
-
return payload;
|
|
325
|
-
}
|
|
326
|
-
/**
|
|
327
|
-
* Poll for CLI auth session completion.
|
|
328
|
-
* Returns `{ token }` on success, throws on failure/expiry.
|
|
329
|
-
* The server returns HTTP 202 while still pending.
|
|
330
|
-
*/
|
|
331
|
-
async pollCliAuthSession(pollToken) {
|
|
332
|
-
const response = await fetch(
|
|
333
|
-
`${this.apiUrl}/api/cli/auth/session?session=${encodeURIComponent(pollToken)}`
|
|
334
|
-
);
|
|
335
|
-
const payload = await readPayload(response);
|
|
336
|
-
if (response.status === 202) {
|
|
337
|
-
return { status: "pending" };
|
|
338
|
-
}
|
|
339
|
-
if (response.status === 410) {
|
|
340
|
-
return {
|
|
341
|
-
status: "failed",
|
|
342
|
-
reason: extractErrorMessage(payload, "Auth session expired or failed")
|
|
343
|
-
};
|
|
344
|
-
}
|
|
345
|
-
if (!response.ok) {
|
|
346
|
-
return {
|
|
347
|
-
status: "failed",
|
|
348
|
-
reason: extractErrorMessage(
|
|
349
|
-
payload,
|
|
350
|
-
`Auth session error (${response.status})`
|
|
351
|
-
)
|
|
352
|
-
};
|
|
353
|
-
}
|
|
354
|
-
const result = payload;
|
|
355
|
-
if (!result.token) {
|
|
356
|
-
return { status: "failed", reason: "No token in response" };
|
|
357
|
-
}
|
|
358
|
-
return {
|
|
359
|
-
status: "complete",
|
|
360
|
-
token: result.token,
|
|
361
|
-
...result.organizationId ? { organizationId: result.organizationId } : {}
|
|
362
|
-
};
|
|
363
|
-
}
|
|
364
|
-
/**
|
|
365
|
-
* Validate a CLI user token and return the authenticated user's info.
|
|
366
|
-
* Used by the CLI to verify stored credentials on startup.
|
|
367
|
-
*/
|
|
368
|
-
async getCliUserInfo(userToken) {
|
|
369
|
-
const response = await fetch(`${this.apiUrl}/api/cli/auth/me`, {
|
|
370
|
-
headers: { Authorization: `Bearer ${userToken}` }
|
|
371
|
-
});
|
|
372
|
-
const payload = await readPayload(response);
|
|
373
|
-
if (!response.ok) {
|
|
374
|
-
throw new VocoderAPIError({
|
|
375
|
-
message: extractErrorMessage(
|
|
376
|
-
payload,
|
|
377
|
-
`Token validation failed (${response.status})`
|
|
378
|
-
),
|
|
379
|
-
status: response.status,
|
|
380
|
-
payload
|
|
381
|
-
});
|
|
382
|
-
}
|
|
383
|
-
return payload;
|
|
384
|
-
}
|
|
385
|
-
/**
|
|
386
|
-
* Revoke the given CLI user token server-side.
|
|
387
|
-
*/
|
|
388
|
-
async revokeCliToken(userToken) {
|
|
389
|
-
const response = await fetch(`${this.apiUrl}/api/cli/auth/token`, {
|
|
390
|
-
method: "DELETE",
|
|
391
|
-
headers: { Authorization: `Bearer ${userToken}` }
|
|
392
|
-
});
|
|
393
|
-
if (!response.ok) {
|
|
394
|
-
const payload = await readPayload(response);
|
|
395
|
-
throw new VocoderAPIError({
|
|
396
|
-
message: extractErrorMessage(
|
|
397
|
-
payload,
|
|
398
|
-
`Token revocation failed (${response.status})`
|
|
399
|
-
),
|
|
400
|
-
status: response.status,
|
|
401
|
-
payload
|
|
402
|
-
});
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
// ── Workspaces ────────────────────────────────────────────────────────────────
|
|
406
|
-
async listWorkspaces(userToken, params) {
|
|
407
|
-
const url = new URL(`${this.apiUrl}/api/cli/workspaces`);
|
|
408
|
-
if (params?.repo) url.searchParams.set("repo", params.repo);
|
|
409
|
-
const response = await fetch(url.toString(), {
|
|
410
|
-
headers: { Authorization: `Bearer ${userToken}` }
|
|
411
|
-
});
|
|
412
|
-
const payload = await readPayload(response);
|
|
413
|
-
if (!response.ok) {
|
|
414
|
-
throw new VocoderAPIError({
|
|
415
|
-
message: extractErrorMessage(
|
|
416
|
-
payload,
|
|
417
|
-
`Failed to list workspaces (${response.status})`
|
|
418
|
-
),
|
|
419
|
-
status: response.status,
|
|
420
|
-
payload
|
|
421
|
-
});
|
|
422
|
-
}
|
|
423
|
-
return payload;
|
|
424
|
-
}
|
|
425
|
-
async listProjects(userToken, organizationId) {
|
|
426
|
-
const url = new URL(`${this.apiUrl}/api/cli/projects`);
|
|
427
|
-
url.searchParams.set("organizationId", organizationId);
|
|
428
|
-
const response = await fetch(url.toString(), {
|
|
429
|
-
headers: { Authorization: `Bearer ${userToken}` }
|
|
430
|
-
});
|
|
431
|
-
const payload = await readPayload(response);
|
|
432
|
-
if (!response.ok) {
|
|
433
|
-
throw new VocoderAPIError({
|
|
434
|
-
message: extractErrorMessage(
|
|
435
|
-
payload,
|
|
436
|
-
`Failed to list projects (${response.status})`
|
|
437
|
-
),
|
|
438
|
-
status: response.status,
|
|
439
|
-
payload
|
|
440
|
-
});
|
|
441
|
-
}
|
|
442
|
-
const result = payload;
|
|
443
|
-
return result.projects;
|
|
444
|
-
}
|
|
445
|
-
async regenerateProjectApiKey(userToken, projectId) {
|
|
446
|
-
const response = await fetch(
|
|
447
|
-
`${this.apiUrl}/api/cli/project/regenerate-key`,
|
|
448
|
-
{
|
|
449
|
-
method: "POST",
|
|
450
|
-
headers: {
|
|
451
|
-
"Content-Type": "application/json",
|
|
452
|
-
Authorization: `Bearer ${userToken}`
|
|
453
|
-
},
|
|
454
|
-
body: JSON.stringify({ projectId })
|
|
455
|
-
}
|
|
456
|
-
);
|
|
457
|
-
const payload = await readPayload(response);
|
|
458
|
-
if (!response.ok) {
|
|
459
|
-
throw new VocoderAPIError({
|
|
460
|
-
message: extractErrorMessage(
|
|
461
|
-
payload,
|
|
462
|
-
`Failed to regenerate API key (${response.status})`
|
|
463
|
-
),
|
|
464
|
-
status: response.status,
|
|
465
|
-
payload
|
|
466
|
-
});
|
|
467
|
-
}
|
|
468
|
-
return payload;
|
|
469
|
-
}
|
|
470
|
-
// ── CLI GitHub endpoints ──────────────────────────────────────────────────────
|
|
471
|
-
async startCliGitHubInstall(userToken, params) {
|
|
472
|
-
const response = await fetch(
|
|
473
|
-
`${this.apiUrl}/api/cli/github/install/start`,
|
|
474
|
-
{
|
|
475
|
-
method: "POST",
|
|
476
|
-
headers: {
|
|
477
|
-
Authorization: `Bearer ${userToken}`,
|
|
478
|
-
"Content-Type": "application/json"
|
|
479
|
-
},
|
|
480
|
-
body: JSON.stringify(params)
|
|
481
|
-
}
|
|
482
|
-
);
|
|
483
|
-
const payload = await readPayload(response);
|
|
484
|
-
if (!response.ok) {
|
|
485
|
-
throw new VocoderAPIError({
|
|
486
|
-
message: extractErrorMessage(
|
|
487
|
-
payload,
|
|
488
|
-
`Failed to start GitHub install (${response.status})`
|
|
489
|
-
),
|
|
490
|
-
status: response.status,
|
|
491
|
-
payload
|
|
492
|
-
});
|
|
493
|
-
}
|
|
494
|
-
return payload;
|
|
495
|
-
}
|
|
496
|
-
/**
|
|
497
|
-
* Start the "link existing installation" discovery flow.
|
|
498
|
-
* Unlike startCliGitHubOAuth, this requires no bearer token — the Vocoder
|
|
499
|
-
* account is created from the OAuth code in the callback.
|
|
500
|
-
*/
|
|
501
|
-
async startCliGitHubLinkSession(sessionId, callbackPort) {
|
|
502
|
-
const response = await fetch(
|
|
503
|
-
`${this.apiUrl}/api/cli/github/oauth/link-start`,
|
|
504
|
-
{
|
|
505
|
-
method: "POST",
|
|
506
|
-
headers: { "Content-Type": "application/json" },
|
|
507
|
-
body: JSON.stringify({
|
|
508
|
-
sessionId,
|
|
509
|
-
...callbackPort != null ? { callbackPort } : {}
|
|
510
|
-
})
|
|
511
|
-
}
|
|
512
|
-
);
|
|
513
|
-
const payload = await readPayload(response);
|
|
514
|
-
if (!response.ok) {
|
|
515
|
-
throw new VocoderAPIError({
|
|
516
|
-
message: extractErrorMessage(
|
|
517
|
-
payload,
|
|
518
|
-
`Failed to start GitHub link session (${response.status})`
|
|
519
|
-
),
|
|
520
|
-
status: response.status,
|
|
521
|
-
payload
|
|
522
|
-
});
|
|
523
|
-
}
|
|
524
|
-
return payload;
|
|
525
|
-
}
|
|
526
|
-
async startCliGitHubOAuth(userToken, params) {
|
|
527
|
-
const response = await fetch(`${this.apiUrl}/api/cli/github/oauth/start`, {
|
|
528
|
-
method: "POST",
|
|
529
|
-
headers: {
|
|
530
|
-
Authorization: `Bearer ${userToken}`,
|
|
531
|
-
"Content-Type": "application/json"
|
|
532
|
-
},
|
|
533
|
-
body: JSON.stringify(params)
|
|
534
|
-
});
|
|
535
|
-
const payload = await readPayload(response);
|
|
536
|
-
if (!response.ok) {
|
|
537
|
-
throw new VocoderAPIError({
|
|
538
|
-
message: extractErrorMessage(
|
|
539
|
-
payload,
|
|
540
|
-
`Failed to start GitHub OAuth (${response.status})`
|
|
541
|
-
),
|
|
542
|
-
status: response.status,
|
|
543
|
-
payload
|
|
544
|
-
});
|
|
545
|
-
}
|
|
546
|
-
return payload;
|
|
547
|
-
}
|
|
548
|
-
async getCliGitHubDiscovery(userToken) {
|
|
549
|
-
const response = await fetch(`${this.apiUrl}/api/cli/github/discovery`, {
|
|
550
|
-
headers: { Authorization: `Bearer ${userToken}` }
|
|
551
|
-
});
|
|
552
|
-
const payload = await readPayload(response);
|
|
553
|
-
if (!response.ok) {
|
|
554
|
-
throw new VocoderAPIError({
|
|
555
|
-
message: extractErrorMessage(
|
|
556
|
-
payload,
|
|
557
|
-
`Failed to fetch GitHub discovery (${response.status})`
|
|
558
|
-
),
|
|
559
|
-
status: response.status,
|
|
560
|
-
payload
|
|
561
|
-
});
|
|
562
|
-
}
|
|
563
|
-
return payload;
|
|
564
|
-
}
|
|
565
|
-
async claimCliGitHubInstallation(userToken, params) {
|
|
566
|
-
const response = await fetch(`${this.apiUrl}/api/cli/github/claim`, {
|
|
567
|
-
method: "POST",
|
|
568
|
-
headers: {
|
|
569
|
-
Authorization: `Bearer ${userToken}`,
|
|
570
|
-
"Content-Type": "application/json"
|
|
571
|
-
},
|
|
572
|
-
body: JSON.stringify(params)
|
|
573
|
-
});
|
|
574
|
-
const payload = await readPayload(response);
|
|
575
|
-
if (!response.ok) {
|
|
576
|
-
throw new VocoderAPIError({
|
|
577
|
-
message: extractErrorMessage(
|
|
578
|
-
payload,
|
|
579
|
-
`Failed to claim GitHub installation (${response.status})`
|
|
580
|
-
),
|
|
581
|
-
status: response.status,
|
|
582
|
-
payload
|
|
583
|
-
});
|
|
584
|
-
}
|
|
585
|
-
return payload;
|
|
586
|
-
}
|
|
587
|
-
// ── Locales ───────────────────────────────────────────────────────────────────
|
|
588
|
-
async listLocales(userToken) {
|
|
589
|
-
const response = await fetch(`${this.apiUrl}/api/cli/locales`, {
|
|
590
|
-
headers: { Authorization: `Bearer ${userToken}` }
|
|
591
|
-
});
|
|
592
|
-
const payload = await readPayload(response);
|
|
593
|
-
if (!response.ok) {
|
|
594
|
-
throw new VocoderAPIError({
|
|
595
|
-
message: extractErrorMessage(
|
|
596
|
-
payload,
|
|
597
|
-
`Failed to list locales (${response.status})`
|
|
598
|
-
),
|
|
599
|
-
status: response.status,
|
|
600
|
-
payload
|
|
601
|
-
});
|
|
602
|
-
}
|
|
603
|
-
const result = payload;
|
|
604
|
-
return result;
|
|
605
|
-
}
|
|
606
|
-
async listCompatibleLocales(userToken, sourceLocale) {
|
|
607
|
-
const url = `${this.apiUrl}/api/cli/locales/compatible?source=${encodeURIComponent(sourceLocale)}`;
|
|
608
|
-
const response = await fetch(url, {
|
|
609
|
-
headers: { Authorization: `Bearer ${userToken}` }
|
|
610
|
-
});
|
|
611
|
-
const payload = await readPayload(response);
|
|
612
|
-
if (!response.ok) {
|
|
613
|
-
throw new VocoderAPIError({
|
|
614
|
-
message: extractErrorMessage(
|
|
615
|
-
payload,
|
|
616
|
-
`Failed to list compatible locales (${response.status})`
|
|
617
|
-
),
|
|
618
|
-
status: response.status,
|
|
619
|
-
payload
|
|
620
|
-
});
|
|
621
|
-
}
|
|
622
|
-
const result = payload;
|
|
623
|
-
return result.locales;
|
|
624
|
-
}
|
|
625
|
-
// ── Project creation ──────────────────────────────────────────────────────────
|
|
626
|
-
async createProject(userToken, params) {
|
|
627
|
-
const response = await fetch(`${this.apiUrl}/api/cli/projects`, {
|
|
628
|
-
method: "POST",
|
|
629
|
-
headers: {
|
|
630
|
-
"Content-Type": "application/json",
|
|
631
|
-
Authorization: `Bearer ${userToken}`
|
|
632
|
-
},
|
|
633
|
-
body: JSON.stringify(params)
|
|
634
|
-
});
|
|
635
|
-
const payload = await readPayload(response);
|
|
636
|
-
if (!response.ok) {
|
|
637
|
-
throw new VocoderAPIError({
|
|
638
|
-
message: extractErrorMessage(
|
|
639
|
-
payload,
|
|
640
|
-
`Failed to create project (${response.status})`
|
|
641
|
-
),
|
|
642
|
-
status: response.status,
|
|
643
|
-
payload
|
|
644
|
-
});
|
|
645
|
-
}
|
|
646
|
-
return payload;
|
|
647
|
-
}
|
|
648
|
-
// ── Project lookup ────────────────────────────────────────────────────────────
|
|
649
|
-
/**
|
|
650
|
-
* Look up all project apps for a given repo. Returns info about exact matches,
|
|
651
|
-
* existing apps in other scopes, and whether a whole-repo app exists.
|
|
652
|
-
* No auth required.
|
|
653
|
-
*/
|
|
654
|
-
async lookupProjectByRepo(params) {
|
|
655
|
-
try {
|
|
656
|
-
const response = await fetch(`${this.apiUrl}/api/cli/init/lookup`, {
|
|
657
|
-
method: "POST",
|
|
658
|
-
headers: { "Content-Type": "application/json" },
|
|
659
|
-
body: JSON.stringify({
|
|
660
|
-
repo: params.repoCanonical,
|
|
661
|
-
appDir: params.appDir
|
|
662
|
-
})
|
|
663
|
-
});
|
|
664
|
-
if (!response.ok) {
|
|
665
|
-
return { exactMatch: null, existingApps: [], hasWholeRepoApp: false };
|
|
666
|
-
}
|
|
667
|
-
const data = await response.json();
|
|
668
|
-
return {
|
|
669
|
-
exactMatch: data.exactMatch ?? null,
|
|
670
|
-
existingApps: data.existingApps ?? [],
|
|
671
|
-
hasWholeRepoApp: data.hasWholeRepoApp ?? false
|
|
672
|
-
};
|
|
673
|
-
} catch {
|
|
674
|
-
return { exactMatch: null, existingApps: [], hasWholeRepoApp: false };
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
/**
|
|
678
|
-
* Add a new ProjectApp to an existing project (monorepo: new app directory).
|
|
679
|
-
* Does not check plan limits — no new project is created.
|
|
680
|
-
*/
|
|
681
|
-
async createProjectApp(userToken, params) {
|
|
682
|
-
const response = await fetch(`${this.apiUrl}/api/cli/project/apps`, {
|
|
683
|
-
method: "POST",
|
|
684
|
-
headers: {
|
|
685
|
-
"Content-Type": "application/json",
|
|
686
|
-
Authorization: `Bearer ${userToken}`
|
|
687
|
-
},
|
|
688
|
-
body: JSON.stringify(params)
|
|
689
|
-
});
|
|
690
|
-
const payload = await readPayload(response);
|
|
691
|
-
if (!response.ok) {
|
|
692
|
-
throw new VocoderAPIError({
|
|
693
|
-
message: extractErrorMessage(
|
|
694
|
-
payload,
|
|
695
|
-
`Failed to create project app (${response.status})`
|
|
696
|
-
),
|
|
697
|
-
status: response.status,
|
|
698
|
-
payload
|
|
699
|
-
});
|
|
700
|
-
}
|
|
701
|
-
return payload;
|
|
702
|
-
}
|
|
703
|
-
};
|
|
704
|
-
|
|
705
|
-
// src/utils/auth-store.ts
|
|
706
|
-
import { mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
|
|
707
|
-
import { homedir } from "os";
|
|
708
|
-
import { dirname, join } from "path";
|
|
709
|
-
function getAuthFilePath() {
|
|
710
|
-
return join(homedir(), ".config", "vocoder", "auth.json");
|
|
711
|
-
}
|
|
712
|
-
function readAuthData() {
|
|
713
|
-
const filePath = getAuthFilePath();
|
|
714
|
-
try {
|
|
715
|
-
const raw = readFileSync(filePath, "utf8");
|
|
716
|
-
const parsed = JSON.parse(raw);
|
|
717
|
-
if (!parsed || typeof parsed !== "object") return null;
|
|
718
|
-
const data = parsed;
|
|
719
|
-
if (typeof data.token !== "string" || typeof data.apiUrl !== "string" || typeof data.userId !== "string" || typeof data.email !== "string" || typeof data.createdAt !== "string") {
|
|
720
|
-
return null;
|
|
721
|
-
}
|
|
722
|
-
return {
|
|
723
|
-
token: data.token,
|
|
724
|
-
apiUrl: data.apiUrl,
|
|
725
|
-
userId: data.userId,
|
|
726
|
-
email: data.email,
|
|
727
|
-
name: typeof data.name === "string" ? data.name : null,
|
|
728
|
-
createdAt: data.createdAt
|
|
729
|
-
};
|
|
730
|
-
} catch {
|
|
731
|
-
return null;
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
function writeAuthData(data) {
|
|
735
|
-
const filePath = getAuthFilePath();
|
|
736
|
-
const dir = dirname(filePath);
|
|
737
|
-
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
738
|
-
writeFileSync(filePath, JSON.stringify(data, null, 2), { mode: 384 });
|
|
739
|
-
}
|
|
740
|
-
function clearAuthData() {
|
|
741
|
-
const filePath = getAuthFilePath();
|
|
742
|
-
try {
|
|
743
|
-
unlinkSync(filePath);
|
|
744
|
-
} catch {
|
|
745
|
-
}
|
|
746
|
-
}
|
|
22
|
+
import { execSync as execSync3, spawn as spawn2 } from "child_process";
|
|
23
|
+
import { existsSync as existsSync2, readFileSync, writeFileSync as writeFileSync2 } from "fs";
|
|
747
24
|
|
|
748
25
|
// src/utils/write-config.ts
|
|
749
|
-
import { existsSync, writeFileSync
|
|
750
|
-
import { join
|
|
26
|
+
import { existsSync, writeFileSync } from "fs";
|
|
27
|
+
import { join } from "path";
|
|
751
28
|
function findExistingConfig(cwd = process.cwd()) {
|
|
752
29
|
for (const name of [
|
|
753
30
|
"vocoder.config.ts",
|
|
754
31
|
"vocoder.config.js",
|
|
755
32
|
"vocoder.config.json"
|
|
756
33
|
]) {
|
|
757
|
-
const candidate =
|
|
34
|
+
const candidate = join(cwd, name);
|
|
758
35
|
if (existsSync(candidate)) return candidate;
|
|
759
36
|
}
|
|
760
37
|
return null;
|
|
@@ -767,7 +44,7 @@ function writeVocoderConfig(options) {
|
|
|
767
44
|
} = options;
|
|
768
45
|
if (findExistingConfig(cwd)) return null;
|
|
769
46
|
const ext = useTypeScript ? "ts" : "js";
|
|
770
|
-
const configPath =
|
|
47
|
+
const configPath = join(cwd, `vocoder.config.${ext}`);
|
|
771
48
|
const branchesStr = targetBranches.map((b) => `'${b}'`).join(", ");
|
|
772
49
|
const content = `import { defineConfig } from '@vocoder/config'
|
|
773
50
|
|
|
@@ -785,121 +62,13 @@ export default defineConfig({
|
|
|
785
62
|
})
|
|
786
63
|
`;
|
|
787
64
|
try {
|
|
788
|
-
|
|
65
|
+
writeFileSync(configPath, content, "utf-8");
|
|
789
66
|
return `vocoder.config.${ext}`;
|
|
790
67
|
} catch {
|
|
791
68
|
return null;
|
|
792
69
|
}
|
|
793
70
|
}
|
|
794
71
|
|
|
795
|
-
// src/utils/git-identity.ts
|
|
796
|
-
import { execSync } from "child_process";
|
|
797
|
-
import { relative, resolve } from "path";
|
|
798
|
-
var SHA_REGEX = /^[0-9a-f]{40}$/i;
|
|
799
|
-
function detectCommitSha() {
|
|
800
|
-
if (process.env.VOCODER_COMMIT_SHA && SHA_REGEX.test(process.env.VOCODER_COMMIT_SHA)) {
|
|
801
|
-
return process.env.VOCODER_COMMIT_SHA;
|
|
802
|
-
}
|
|
803
|
-
const knownSha = process.env.GITHUB_SHA || process.env.VERCEL_GIT_COMMIT_SHA || process.env.CI_COMMIT_SHA || process.env.BITBUCKET_COMMIT || process.env.CIRCLE_SHA1 || process.env.RENDER_GIT_COMMIT;
|
|
804
|
-
if (knownSha && SHA_REGEX.test(knownSha)) return knownSha;
|
|
805
|
-
return safeExec("git rev-parse HEAD");
|
|
806
|
-
}
|
|
807
|
-
function safeExec(command) {
|
|
808
|
-
try {
|
|
809
|
-
const output = execSync(command, {
|
|
810
|
-
encoding: "utf-8",
|
|
811
|
-
stdio: ["pipe", "pipe", "ignore"]
|
|
812
|
-
}).trim();
|
|
813
|
-
return output.length > 0 ? output : null;
|
|
814
|
-
} catch {
|
|
815
|
-
return null;
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
function normalizePath(pathname) {
|
|
819
|
-
const cleaned = pathname.replace(/^\/+/, "").replace(/\.git$/i, "").trim();
|
|
820
|
-
if (!cleaned || !cleaned.includes("/")) {
|
|
821
|
-
return null;
|
|
822
|
-
}
|
|
823
|
-
return cleaned;
|
|
824
|
-
}
|
|
825
|
-
function parseRemoteUrl(remoteUrl) {
|
|
826
|
-
const trimmed = remoteUrl.trim();
|
|
827
|
-
if (!trimmed) {
|
|
828
|
-
return null;
|
|
829
|
-
}
|
|
830
|
-
if (!trimmed.includes("://")) {
|
|
831
|
-
const scpMatch = trimmed.match(/^(?:.+@)?([^:]+):(.+)$/);
|
|
832
|
-
if (scpMatch) {
|
|
833
|
-
const host = (scpMatch[1] || "").toLowerCase();
|
|
834
|
-
const ownerRepoPath = normalizePath(scpMatch[2] || "");
|
|
835
|
-
if (!host || !ownerRepoPath) {
|
|
836
|
-
return null;
|
|
837
|
-
}
|
|
838
|
-
return { host, ownerRepoPath };
|
|
839
|
-
}
|
|
840
|
-
return null;
|
|
841
|
-
}
|
|
842
|
-
try {
|
|
843
|
-
const parsed = new URL(trimmed);
|
|
844
|
-
const host = parsed.hostname.toLowerCase();
|
|
845
|
-
const ownerRepoPath = normalizePath(decodeURIComponent(parsed.pathname));
|
|
846
|
-
if (!host || !ownerRepoPath) {
|
|
847
|
-
return null;
|
|
848
|
-
}
|
|
849
|
-
return { host, ownerRepoPath };
|
|
850
|
-
} catch {
|
|
851
|
-
return null;
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
function toCanonical(host, ownerRepoPath) {
|
|
855
|
-
if (host.includes("github.com")) {
|
|
856
|
-
return `github:${ownerRepoPath.toLowerCase()}`;
|
|
857
|
-
}
|
|
858
|
-
if (host.includes("gitlab.com")) {
|
|
859
|
-
return `gitlab:${ownerRepoPath.toLowerCase()}`;
|
|
860
|
-
}
|
|
861
|
-
if (host.includes("bitbucket.org")) {
|
|
862
|
-
return `bitbucket:${ownerRepoPath.toLowerCase()}`;
|
|
863
|
-
}
|
|
864
|
-
return `git:${host}/${ownerRepoPath.toLowerCase()}`;
|
|
865
|
-
}
|
|
866
|
-
function resolveGitRepositoryIdentity() {
|
|
867
|
-
const remoteUrl = safeExec("git config --get remote.origin.url");
|
|
868
|
-
if (!remoteUrl) {
|
|
869
|
-
return null;
|
|
870
|
-
}
|
|
871
|
-
const parsed = parseRemoteUrl(remoteUrl);
|
|
872
|
-
if (!parsed) {
|
|
873
|
-
return null;
|
|
874
|
-
}
|
|
875
|
-
const repositoryRoot = safeExec("git rev-parse --show-toplevel");
|
|
876
|
-
const currentDirectory = process.cwd();
|
|
877
|
-
let repoAppDir = "";
|
|
878
|
-
if (repositoryRoot) {
|
|
879
|
-
const relativePath = relative(
|
|
880
|
-
resolve(repositoryRoot),
|
|
881
|
-
resolve(currentDirectory)
|
|
882
|
-
).replace(/\\/g, "/").trim();
|
|
883
|
-
if (relativePath && relativePath !== "." && !relativePath.startsWith("..")) {
|
|
884
|
-
repoAppDir = relativePath;
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
return {
|
|
888
|
-
repoCanonical: toCanonical(parsed.host, parsed.ownerRepoPath),
|
|
889
|
-
repoAppDir
|
|
890
|
-
};
|
|
891
|
-
}
|
|
892
|
-
function resolveGitContext() {
|
|
893
|
-
const warnings = [];
|
|
894
|
-
const identity = resolveGitRepositoryIdentity();
|
|
895
|
-
if (!identity) {
|
|
896
|
-
warnings.push(
|
|
897
|
-
"Could not detect git remote origin. Repo binding will be skipped until sync can detect it."
|
|
898
|
-
);
|
|
899
|
-
}
|
|
900
|
-
return { identity, warnings };
|
|
901
|
-
}
|
|
902
|
-
|
|
903
72
|
// src/utils/github-connect.ts
|
|
904
73
|
import { spawn } from "child_process";
|
|
905
74
|
import * as p from "@clack/prompts";
|
|
@@ -1185,7 +354,7 @@ import * as p3 from "@clack/prompts";
|
|
|
1185
354
|
import chalk4 from "chalk";
|
|
1186
355
|
|
|
1187
356
|
// src/utils/branch-select.ts
|
|
1188
|
-
import { execSync
|
|
357
|
+
import { execSync } from "child_process";
|
|
1189
358
|
import { isCancel as isCancel2, Prompt } from "@clack/core";
|
|
1190
359
|
import chalk2 from "chalk";
|
|
1191
360
|
var S_BAR = "\u2502";
|
|
@@ -1216,14 +385,14 @@ function symbol(state) {
|
|
|
1216
385
|
function detectGitBranches(cwd) {
|
|
1217
386
|
const workDir = cwd ?? process.cwd();
|
|
1218
387
|
try {
|
|
1219
|
-
const localOut =
|
|
388
|
+
const localOut = execSync("git branch", {
|
|
1220
389
|
cwd: workDir,
|
|
1221
390
|
stdio: "pipe"
|
|
1222
391
|
}).toString();
|
|
1223
392
|
const localBranches = localOut.split("\n").filter(Boolean).map((b) => b.replace(/^\*?\s*/, "").trim()).filter(Boolean);
|
|
1224
393
|
let remoteBranches = [];
|
|
1225
394
|
try {
|
|
1226
|
-
const remoteOut =
|
|
395
|
+
const remoteOut = execSync("git branch -r", {
|
|
1227
396
|
cwd: workDir,
|
|
1228
397
|
stdio: "pipe"
|
|
1229
398
|
}).toString();
|
|
@@ -1233,7 +402,7 @@ function detectGitBranches(cwd) {
|
|
|
1233
402
|
const branches = [.../* @__PURE__ */ new Set([...localBranches, ...remoteBranches])].sort();
|
|
1234
403
|
let defaultBranch = "main";
|
|
1235
404
|
try {
|
|
1236
|
-
const ref =
|
|
405
|
+
const ref = execSync("git symbolic-ref refs/remotes/origin/HEAD", {
|
|
1237
406
|
cwd: workDir,
|
|
1238
407
|
stdio: "pipe"
|
|
1239
408
|
}).toString().trim();
|
|
@@ -1919,6 +1088,119 @@ async function runProjectAppCreate(params) {
|
|
|
1919
1088
|
}
|
|
1920
1089
|
}
|
|
1921
1090
|
|
|
1091
|
+
// src/commands/init.ts
|
|
1092
|
+
import chalk6 from "chalk";
|
|
1093
|
+
import { join as join2 } from "path";
|
|
1094
|
+
import { config as loadEnv } from "dotenv";
|
|
1095
|
+
|
|
1096
|
+
// src/utils/git-identity.ts
|
|
1097
|
+
import { execSync as execSync2 } from "child_process";
|
|
1098
|
+
import { relative, resolve } from "path";
|
|
1099
|
+
var SHA_REGEX = /^[0-9a-f]{40}$/i;
|
|
1100
|
+
function detectCommitSha() {
|
|
1101
|
+
if (process.env.VOCODER_COMMIT_SHA && SHA_REGEX.test(process.env.VOCODER_COMMIT_SHA)) {
|
|
1102
|
+
return process.env.VOCODER_COMMIT_SHA;
|
|
1103
|
+
}
|
|
1104
|
+
const knownSha = process.env.GITHUB_SHA || process.env.VERCEL_GIT_COMMIT_SHA || process.env.CI_COMMIT_SHA || process.env.BITBUCKET_COMMIT || process.env.CIRCLE_SHA1 || process.env.RENDER_GIT_COMMIT;
|
|
1105
|
+
if (knownSha && SHA_REGEX.test(knownSha)) return knownSha;
|
|
1106
|
+
return safeExec("git rev-parse HEAD");
|
|
1107
|
+
}
|
|
1108
|
+
function safeExec(command) {
|
|
1109
|
+
try {
|
|
1110
|
+
const output = execSync2(command, {
|
|
1111
|
+
encoding: "utf-8",
|
|
1112
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
1113
|
+
}).trim();
|
|
1114
|
+
return output.length > 0 ? output : null;
|
|
1115
|
+
} catch {
|
|
1116
|
+
return null;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
function normalizePath(pathname) {
|
|
1120
|
+
const cleaned = pathname.replace(/^\/+/, "").replace(/\.git$/i, "").trim();
|
|
1121
|
+
if (!cleaned || !cleaned.includes("/")) {
|
|
1122
|
+
return null;
|
|
1123
|
+
}
|
|
1124
|
+
return cleaned;
|
|
1125
|
+
}
|
|
1126
|
+
function parseRemoteUrl(remoteUrl) {
|
|
1127
|
+
const trimmed = remoteUrl.trim();
|
|
1128
|
+
if (!trimmed) {
|
|
1129
|
+
return null;
|
|
1130
|
+
}
|
|
1131
|
+
if (!trimmed.includes("://")) {
|
|
1132
|
+
const scpMatch = trimmed.match(/^(?:.+@)?([^:]+):(.+)$/);
|
|
1133
|
+
if (scpMatch) {
|
|
1134
|
+
const host = (scpMatch[1] || "").toLowerCase();
|
|
1135
|
+
const ownerRepoPath = normalizePath(scpMatch[2] || "");
|
|
1136
|
+
if (!host || !ownerRepoPath) {
|
|
1137
|
+
return null;
|
|
1138
|
+
}
|
|
1139
|
+
return { host, ownerRepoPath };
|
|
1140
|
+
}
|
|
1141
|
+
return null;
|
|
1142
|
+
}
|
|
1143
|
+
try {
|
|
1144
|
+
const parsed = new URL(trimmed);
|
|
1145
|
+
const host = parsed.hostname.toLowerCase();
|
|
1146
|
+
const ownerRepoPath = normalizePath(decodeURIComponent(parsed.pathname));
|
|
1147
|
+
if (!host || !ownerRepoPath) {
|
|
1148
|
+
return null;
|
|
1149
|
+
}
|
|
1150
|
+
return { host, ownerRepoPath };
|
|
1151
|
+
} catch {
|
|
1152
|
+
return null;
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
function toCanonical(host, ownerRepoPath) {
|
|
1156
|
+
if (host.includes("github.com")) {
|
|
1157
|
+
return `github:${ownerRepoPath.toLowerCase()}`;
|
|
1158
|
+
}
|
|
1159
|
+
if (host.includes("gitlab.com")) {
|
|
1160
|
+
return `gitlab:${ownerRepoPath.toLowerCase()}`;
|
|
1161
|
+
}
|
|
1162
|
+
if (host.includes("bitbucket.org")) {
|
|
1163
|
+
return `bitbucket:${ownerRepoPath.toLowerCase()}`;
|
|
1164
|
+
}
|
|
1165
|
+
return `git:${host}/${ownerRepoPath.toLowerCase()}`;
|
|
1166
|
+
}
|
|
1167
|
+
function resolveGitRepositoryIdentity() {
|
|
1168
|
+
const remoteUrl = safeExec("git config --get remote.origin.url");
|
|
1169
|
+
if (!remoteUrl) {
|
|
1170
|
+
return null;
|
|
1171
|
+
}
|
|
1172
|
+
const parsed = parseRemoteUrl(remoteUrl);
|
|
1173
|
+
if (!parsed) {
|
|
1174
|
+
return null;
|
|
1175
|
+
}
|
|
1176
|
+
const repositoryRoot = safeExec("git rev-parse --show-toplevel");
|
|
1177
|
+
const currentDirectory = process.cwd();
|
|
1178
|
+
let repoAppDir = "";
|
|
1179
|
+
if (repositoryRoot) {
|
|
1180
|
+
const relativePath = relative(
|
|
1181
|
+
resolve(repositoryRoot),
|
|
1182
|
+
resolve(currentDirectory)
|
|
1183
|
+
).replace(/\\/g, "/").trim();
|
|
1184
|
+
if (relativePath && relativePath !== "." && !relativePath.startsWith("..")) {
|
|
1185
|
+
repoAppDir = relativePath;
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
return {
|
|
1189
|
+
repoCanonical: toCanonical(parsed.host, parsed.ownerRepoPath),
|
|
1190
|
+
repoAppDir
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
function resolveGitContext() {
|
|
1194
|
+
const warnings = [];
|
|
1195
|
+
const identity = resolveGitRepositoryIdentity();
|
|
1196
|
+
if (!identity) {
|
|
1197
|
+
warnings.push(
|
|
1198
|
+
"Could not detect git remote origin. Repo binding will be skipped until sync can detect it."
|
|
1199
|
+
);
|
|
1200
|
+
}
|
|
1201
|
+
return { identity, warnings };
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1922
1204
|
// src/utils/workspace.ts
|
|
1923
1205
|
import * as p4 from "@clack/prompts";
|
|
1924
1206
|
import chalk5 from "chalk";
|
|
@@ -2112,10 +1394,10 @@ function runScaffold(params) {
|
|
|
2112
1394
|
p5.log.message(chalk6.gray(" Docs: https://vocoder.app/docs/getting-started"));
|
|
2113
1395
|
}
|
|
2114
1396
|
function writeApiKeyToEnv(apiKey) {
|
|
2115
|
-
const envPath =
|
|
1397
|
+
const envPath = join2(process.cwd(), ".env");
|
|
2116
1398
|
if (!existsSync2(envPath)) return false;
|
|
2117
1399
|
try {
|
|
2118
|
-
const content =
|
|
1400
|
+
const content = readFileSync(envPath, "utf-8");
|
|
2119
1401
|
const keyLine = `VOCODER_API_KEY=${apiKey}`;
|
|
2120
1402
|
let updated;
|
|
2121
1403
|
if (/^VOCODER_API_KEY=/m.test(content)) {
|
|
@@ -2125,7 +1407,7 @@ function writeApiKeyToEnv(apiKey) {
|
|
|
2125
1407
|
updated = `${content}${sep}${keyLine}
|
|
2126
1408
|
`;
|
|
2127
1409
|
}
|
|
2128
|
-
|
|
1410
|
+
writeFileSync2(envPath, updated);
|
|
2129
1411
|
return true;
|
|
2130
1412
|
} catch {
|
|
2131
1413
|
return false;
|
|
@@ -2402,7 +1684,7 @@ async function init(options = {}) {
|
|
|
2402
1684
|
let userName;
|
|
2403
1685
|
let authOrganizationId;
|
|
2404
1686
|
const stored = readAuthData();
|
|
2405
|
-
if (stored
|
|
1687
|
+
if (stored) {
|
|
2406
1688
|
const verified = await verifyStoredToken(api, stored.token);
|
|
2407
1689
|
if (verified && !("userGone" in verified)) {
|
|
2408
1690
|
p5.log.success(`Authenticated as ${chalk6.bold(verified.email)}`);
|
|
@@ -2430,7 +1712,6 @@ async function init(options = {}) {
|
|
|
2430
1712
|
authOrganizationId = authResult.organizationId;
|
|
2431
1713
|
writeAuthData({
|
|
2432
1714
|
token: userToken,
|
|
2433
|
-
apiUrl,
|
|
2434
1715
|
userId: authResult.userId,
|
|
2435
1716
|
email: userEmail,
|
|
2436
1717
|
name: userName,
|
|
@@ -2451,7 +1732,6 @@ async function init(options = {}) {
|
|
|
2451
1732
|
authOrganizationId = authResult.organizationId;
|
|
2452
1733
|
writeAuthData({
|
|
2453
1734
|
token: userToken,
|
|
2454
|
-
apiUrl,
|
|
2455
1735
|
userId: authResult.userId,
|
|
2456
1736
|
email: userEmail,
|
|
2457
1737
|
name: userName,
|
|
@@ -2883,8 +2163,8 @@ async function logout(options = {}) {
|
|
|
2883
2163
|
|
|
2884
2164
|
// src/commands/sync.ts
|
|
2885
2165
|
import { createHash, randomUUID } from "crypto";
|
|
2886
|
-
import { existsSync as existsSync3, mkdirSync
|
|
2887
|
-
import { join as
|
|
2166
|
+
import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
2167
|
+
import { join as join3 } from "path";
|
|
2888
2168
|
import * as p8 from "@clack/prompts";
|
|
2889
2169
|
import chalk8 from "chalk";
|
|
2890
2170
|
|
|
@@ -3165,7 +2445,7 @@ function parseTranslations(value) {
|
|
|
3165
2445
|
return Object.keys(translations).length > 0 ? translations : null;
|
|
3166
2446
|
}
|
|
3167
2447
|
function getCacheFilePath(projectRoot, fingerprint) {
|
|
3168
|
-
return
|
|
2448
|
+
return join3(projectRoot, "node_modules", ".vocoder", "cache", `${fingerprint}.json`);
|
|
3169
2449
|
}
|
|
3170
2450
|
function buildTranslationData(params) {
|
|
3171
2451
|
const textToHash = new Map(params.stringEntries.map((e) => [e.text, e.key]));
|
|
@@ -3192,7 +2472,7 @@ function readLocalCache(params) {
|
|
|
3192
2472
|
const cacheFilePath = getCacheFilePath(params.projectRoot, params.fingerprint);
|
|
3193
2473
|
if (!existsSync3(cacheFilePath)) return null;
|
|
3194
2474
|
try {
|
|
3195
|
-
const raw =
|
|
2475
|
+
const raw = readFileSync2(cacheFilePath, "utf-8");
|
|
3196
2476
|
const parsed = JSON.parse(raw);
|
|
3197
2477
|
if (!isRecord(parsed)) return null;
|
|
3198
2478
|
const inner = isRecord(parsed.config) ? parsed : null;
|
|
@@ -3206,10 +2486,10 @@ function readLocalCache(params) {
|
|
|
3206
2486
|
}
|
|
3207
2487
|
}
|
|
3208
2488
|
function writeCache(params) {
|
|
3209
|
-
const cacheDir =
|
|
3210
|
-
|
|
2489
|
+
const cacheDir = join3(params.projectRoot, "node_modules", ".vocoder", "cache");
|
|
2490
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
3211
2491
|
const cacheFilePath = getCacheFilePath(params.projectRoot, params.fingerprint);
|
|
3212
|
-
|
|
2492
|
+
writeFileSync3(cacheFilePath, JSON.stringify(params.data), "utf-8");
|
|
3213
2493
|
return cacheFilePath;
|
|
3214
2494
|
}
|
|
3215
2495
|
function resolveEffectiveModeFromPolicy(params) {
|
|
@@ -3329,8 +2609,7 @@ function buildStringEntries(extractedStrings) {
|
|
|
3329
2609
|
text: str.text,
|
|
3330
2610
|
...str.context ? { context: str.context } : {},
|
|
3331
2611
|
...str.formality ? { formality: str.formality } : {},
|
|
3332
|
-
...str.uiRole ? { uiRole: str.uiRole } : {}
|
|
3333
|
-
...str.featureArea ? { featureArea: str.featureArea } : {}
|
|
2612
|
+
...str.uiRole ? { uiRole: str.uiRole } : {}
|
|
3334
2613
|
});
|
|
3335
2614
|
continue;
|
|
3336
2615
|
}
|