epicshop 6.75.1 → 6.76.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/cli.js +14 -3
- package/dist/commands/cleanup.js +84 -22
- package/dist/commands/workshops.d.ts +2 -1
- package/dist/commands/workshops.js +271 -34
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -280,11 +280,15 @@ const cli = yargs(args)
|
|
|
280
280
|
.positional('subcommand', {
|
|
281
281
|
describe: 'Config subcommand (reset)',
|
|
282
282
|
type: 'string',
|
|
283
|
-
choices: ['reset'],
|
|
283
|
+
choices: ['reset', 'editor'],
|
|
284
284
|
})
|
|
285
285
|
.option('repos-dir', {
|
|
286
286
|
type: 'string',
|
|
287
287
|
description: 'Set the default directory for workshop repos',
|
|
288
|
+
})
|
|
289
|
+
.option('editor', {
|
|
290
|
+
type: 'string',
|
|
291
|
+
description: 'Set the preferred editor command',
|
|
288
292
|
})
|
|
289
293
|
.option('silent', {
|
|
290
294
|
alias: 's',
|
|
@@ -294,12 +298,19 @@ const cli = yargs(args)
|
|
|
294
298
|
})
|
|
295
299
|
.example('$0 config', 'View current configuration')
|
|
296
300
|
.example('$0 config reset', 'Delete config file and reset to defaults')
|
|
297
|
-
.example('$0 config --repos-dir ~/epicweb', 'Set the repos directory')
|
|
301
|
+
.example('$0 config --repos-dir ~/epicweb', 'Set the repos directory')
|
|
302
|
+
.example('$0 config editor', 'Choose a preferred editor')
|
|
303
|
+
.example('$0 config --editor code', 'Set preferred editor to VS Code');
|
|
298
304
|
}, async (argv) => {
|
|
299
305
|
const { config } = await import("./commands/workshops.js");
|
|
300
306
|
const result = await config({
|
|
301
|
-
subcommand: argv.subcommand === 'reset'
|
|
307
|
+
subcommand: argv.subcommand === 'reset'
|
|
308
|
+
? 'reset'
|
|
309
|
+
: argv.subcommand === 'editor'
|
|
310
|
+
? 'editor'
|
|
311
|
+
: undefined,
|
|
302
312
|
reposDir: argv.reposDir,
|
|
313
|
+
preferredEditor: argv.editor,
|
|
303
314
|
silent: argv.silent,
|
|
304
315
|
});
|
|
305
316
|
if (!result.success) {
|
package/dist/commands/cleanup.js
CHANGED
|
@@ -6,6 +6,7 @@ import path from 'node:path';
|
|
|
6
6
|
import { resolveCacheDir, resolveFallbackPath, resolvePrimaryDir, resolvePrimaryPath, } from '@epic-web/workshop-utils/data-storage.server';
|
|
7
7
|
import { deleteWorkshop, getReposDirectory, getUnpushedChanges, } from '@epic-web/workshop-utils/workshops.server';
|
|
8
8
|
import chalk from 'chalk';
|
|
9
|
+
import ora from 'ora';
|
|
9
10
|
import { assertCanPrompt } from "../utils/cli-runtime.js";
|
|
10
11
|
const CLEANUP_TARGETS = [
|
|
11
12
|
{
|
|
@@ -63,6 +64,19 @@ function resolveWorkshopCleanupTargets(targets) {
|
|
|
63
64
|
const allowed = new Set(WORKSHOP_CLEANUP_TARGETS.map((target) => target.value));
|
|
64
65
|
return Array.from(new Set(targets.filter((target) => allowed.has(target))));
|
|
65
66
|
}
|
|
67
|
+
function startSpinner(text, silent) {
|
|
68
|
+
if (silent)
|
|
69
|
+
return null;
|
|
70
|
+
return ora(text).start();
|
|
71
|
+
}
|
|
72
|
+
function updateSpinner(spinner, text) {
|
|
73
|
+
if (spinner)
|
|
74
|
+
spinner.text = text;
|
|
75
|
+
}
|
|
76
|
+
function stopSpinner(spinner) {
|
|
77
|
+
if (spinner?.isSpinning)
|
|
78
|
+
spinner.stop();
|
|
79
|
+
}
|
|
66
80
|
async function resolveCleanupPaths(paths = {}) {
|
|
67
81
|
const reposDir = paths.reposDir ?? (await getReposDirectory());
|
|
68
82
|
const cacheDir = paths.cacheDir ?? resolveCacheDir();
|
|
@@ -403,9 +417,11 @@ async function getDataCleanupSizeSummary(dataPaths) {
|
|
|
403
417
|
}
|
|
404
418
|
return { preferencesBytes, authBytes };
|
|
405
419
|
}
|
|
406
|
-
async function getWorkshopSummaries({ workshops, cacheDir, }) {
|
|
420
|
+
async function getWorkshopSummaries({ workshops, cacheDir, onProgress, }) {
|
|
407
421
|
const summaries = [];
|
|
408
|
-
|
|
422
|
+
const total = workshops.length;
|
|
423
|
+
for (const [index, workshop] of workshops.entries()) {
|
|
424
|
+
onProgress?.({ current: index + 1, total, workshop });
|
|
409
425
|
const id = getWorkshopInstanceId(workshop.path);
|
|
410
426
|
const sizeBytes = await getPathSize(workshop.path);
|
|
411
427
|
const cacheBytes = await getPathSize(path.join(cacheDir, id));
|
|
@@ -520,17 +536,47 @@ export async function cleanup({ silent = false, force = false, targets, workshop
|
|
|
520
536
|
selectedTargets.push('workshops');
|
|
521
537
|
}
|
|
522
538
|
}
|
|
523
|
-
const
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
539
|
+
const analysisSpinner = startSpinner('Scanning local epicshop data...', silent);
|
|
540
|
+
let reposDir = '';
|
|
541
|
+
let cacheDir = '';
|
|
542
|
+
let legacyCacheDir = '';
|
|
543
|
+
let dataPaths = [];
|
|
544
|
+
let offlineVideosDir = '';
|
|
545
|
+
let workshopSummaries = [];
|
|
546
|
+
let workshopBytes = 0;
|
|
547
|
+
let legacyCacheBytes = 0;
|
|
548
|
+
let cacheBytes = 0;
|
|
549
|
+
let offlineVideosBytes = 0;
|
|
550
|
+
let preferencesBytes = 0;
|
|
551
|
+
let authBytes = 0;
|
|
552
|
+
try {
|
|
553
|
+
updateSpinner(analysisSpinner, 'Resolving cleanup locations...');
|
|
554
|
+
({ reposDir, cacheDir, legacyCacheDir, dataPaths, offlineVideosDir } =
|
|
555
|
+
await resolveCleanupPaths(paths));
|
|
556
|
+
updateSpinner(analysisSpinner, 'Finding installed workshops...');
|
|
557
|
+
const allWorkshops = await listWorkshopsInDirectory(reposDir);
|
|
558
|
+
updateSpinner(analysisSpinner, 'Calculating workshop sizes...');
|
|
559
|
+
workshopSummaries = await getWorkshopSummaries({
|
|
560
|
+
workshops: allWorkshops,
|
|
561
|
+
cacheDir,
|
|
562
|
+
onProgress: (progress) => {
|
|
563
|
+
updateSpinner(analysisSpinner, `Calculating workshop sizes (${progress.current}/${progress.total}): ${progress.workshop.repoName}`);
|
|
564
|
+
},
|
|
565
|
+
});
|
|
566
|
+
workshopBytes = workshopSummaries.reduce((total, workshop) => total + workshop.sizeBytes, 0);
|
|
567
|
+
updateSpinner(analysisSpinner, 'Calculating cache sizes...');
|
|
568
|
+
legacyCacheBytes = await getPathSize(legacyCacheDir);
|
|
569
|
+
const cacheDirBytes = await getPathSize(cacheDir);
|
|
570
|
+
cacheBytes = cacheDirBytes + legacyCacheBytes;
|
|
571
|
+
updateSpinner(analysisSpinner, 'Calculating offline video sizes...');
|
|
572
|
+
offlineVideosBytes = await getPathSize(offlineVideosDir);
|
|
573
|
+
updateSpinner(analysisSpinner, 'Scanning preferences and auth data...');
|
|
574
|
+
({ preferencesBytes, authBytes } =
|
|
575
|
+
await getDataCleanupSizeSummary(dataPaths));
|
|
576
|
+
}
|
|
577
|
+
finally {
|
|
578
|
+
stopSpinner(analysisSpinner);
|
|
579
|
+
}
|
|
534
580
|
const cleanupChoices = CLEANUP_TARGETS.map((target) => {
|
|
535
581
|
const sizeByTarget = {
|
|
536
582
|
workshops: workshopBytes,
|
|
@@ -580,8 +626,17 @@ export async function cleanup({ silent = false, force = false, targets, workshop
|
|
|
580
626
|
const selectedWorkshopIds = new Set(selectedWorkshops.map((workshop) => workshop.id));
|
|
581
627
|
const workshopFileBytes = selectedWorkshops.reduce((total, workshop) => total + workshop.sizeBytes, 0);
|
|
582
628
|
const workshopCacheBytes = selectedWorkshops.reduce((total, workshop) => total + workshop.cacheBytes, 0);
|
|
583
|
-
const
|
|
584
|
-
|
|
629
|
+
const selectionSpinner = startSpinner('Calculating workshop cleanup sizes...', silent);
|
|
630
|
+
let workshopOfflineBytes = 0;
|
|
631
|
+
try {
|
|
632
|
+
updateSpinner(selectionSpinner, 'Loading offline video index...');
|
|
633
|
+
const offlineVideoIndex = await readOfflineVideoIndex(offlineVideosDir);
|
|
634
|
+
updateSpinner(selectionSpinner, 'Calculating workshop offline video sizes...');
|
|
635
|
+
workshopOfflineBytes = await estimateOfflineVideoBytesForWorkshops(offlineVideosDir, offlineVideoIndex, selectedWorkshopIds);
|
|
636
|
+
}
|
|
637
|
+
finally {
|
|
638
|
+
stopSpinner(selectionSpinner);
|
|
639
|
+
}
|
|
585
640
|
const workshopChoices = WORKSHOP_CLEANUP_TARGETS.map((target) => {
|
|
586
641
|
const sizeByTarget = {
|
|
587
642
|
files: workshopFileBytes,
|
|
@@ -618,14 +673,21 @@ export async function cleanup({ silent = false, force = false, targets, workshop
|
|
|
618
673
|
console.log(chalk.gray(message));
|
|
619
674
|
return { success: true, message, selectedTargets };
|
|
620
675
|
}
|
|
621
|
-
|
|
676
|
+
let unpushedSummaries = [];
|
|
677
|
+
if (!silent &&
|
|
622
678
|
selectedWorkshopTargets.includes('files') &&
|
|
623
|
-
selectedWorkshops.length > 0
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
679
|
+
selectedWorkshops.length > 0) {
|
|
680
|
+
const unpushedSpinner = startSpinner('Checking for unpushed workshop changes...', silent);
|
|
681
|
+
try {
|
|
682
|
+
unpushedSummaries = await Promise.all(selectedWorkshops.map(async (workshop) => ({
|
|
683
|
+
workshop,
|
|
684
|
+
unpushedChanges: await getUnpushedChanges(workshop.path),
|
|
685
|
+
})));
|
|
686
|
+
}
|
|
687
|
+
finally {
|
|
688
|
+
stopSpinner(unpushedSpinner);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
629
691
|
if (!silent) {
|
|
630
692
|
console.log(chalk.yellow('This will clean up the following:'));
|
|
631
693
|
if (selectedWorkshopTargets.includes('files')) {
|
|
@@ -28,8 +28,9 @@ export type StartOptions = {
|
|
|
28
28
|
};
|
|
29
29
|
export type ConfigOptions = {
|
|
30
30
|
reposDir?: string;
|
|
31
|
+
preferredEditor?: string;
|
|
31
32
|
silent?: boolean;
|
|
32
|
-
subcommand?: 'reset' | 'delete';
|
|
33
|
+
subcommand?: 'reset' | 'delete' | 'editor';
|
|
33
34
|
};
|
|
34
35
|
/**
|
|
35
36
|
* Add a workshop by cloning from epicweb-dev GitHub org and running setup
|
|
@@ -78,6 +78,7 @@ const GitHubRepoSchema = z.object({
|
|
|
78
78
|
stargazers_count: z.number(),
|
|
79
79
|
topics: z.array(z.string()).default([]),
|
|
80
80
|
archived: z.boolean(),
|
|
81
|
+
default_branch: z.string().optional(),
|
|
81
82
|
});
|
|
82
83
|
const GitHubSearchResponseSchema = z.object({
|
|
83
84
|
total_count: z.number(),
|
|
@@ -99,6 +100,20 @@ function resolvePathWithTilde(inputPath) {
|
|
|
99
100
|
}
|
|
100
101
|
return trimmed;
|
|
101
102
|
}
|
|
103
|
+
function formatEditorChoiceName(editor) {
|
|
104
|
+
return editor.label === editor.command
|
|
105
|
+
? editor.label
|
|
106
|
+
: `${editor.label} (${editor.command})`;
|
|
107
|
+
}
|
|
108
|
+
async function getInstalledEditorChoices() {
|
|
109
|
+
const { getAvailableEditors } = await import('@epic-web/workshop-utils/launch-editor.server');
|
|
110
|
+
const editors = getAvailableEditors();
|
|
111
|
+
return editors.map((editor) => ({
|
|
112
|
+
name: formatEditorChoiceName(editor),
|
|
113
|
+
value: editor.command,
|
|
114
|
+
description: editor.label === editor.command ? undefined : editor.command,
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
102
117
|
function parseRepoSpecifier(value) {
|
|
103
118
|
const trimmed = value.trim();
|
|
104
119
|
const hashIndex = trimmed.indexOf('#');
|
|
@@ -113,8 +128,11 @@ function parseRepoSpecifier(value) {
|
|
|
113
128
|
return { repoName, repoRef };
|
|
114
129
|
}
|
|
115
130
|
function getGitHubHeaders() {
|
|
131
|
+
return getGitHubHeadersWithAccept('application/vnd.github.v3+json');
|
|
132
|
+
}
|
|
133
|
+
function getGitHubHeadersWithAccept(accept) {
|
|
116
134
|
const headers = {
|
|
117
|
-
Accept:
|
|
135
|
+
Accept: accept,
|
|
118
136
|
'User-Agent': 'epicshop-cli',
|
|
119
137
|
};
|
|
120
138
|
if (GITHUB_TOKEN) {
|
|
@@ -122,6 +140,47 @@ function getGitHubHeaders() {
|
|
|
122
140
|
}
|
|
123
141
|
return headers;
|
|
124
142
|
}
|
|
143
|
+
const DEFAULT_BRANCHES = ['main', 'master'];
|
|
144
|
+
function buildRawPackageJsonUrls(repoName, defaultBranch) {
|
|
145
|
+
const branches = [defaultBranch, ...DEFAULT_BRANCHES].filter((branch) => Boolean(branch));
|
|
146
|
+
const uniqueBranches = Array.from(new Set(branches));
|
|
147
|
+
return uniqueBranches.map((branch) => `https://raw.githubusercontent.com/${GITHUB_ORG}/${repoName}/${branch}/package.json`);
|
|
148
|
+
}
|
|
149
|
+
async function parsePackageJsonResponse(response) {
|
|
150
|
+
try {
|
|
151
|
+
const parsed = PackageJsonSchema.safeParse(await response.json());
|
|
152
|
+
return parsed.success ? parsed.data : null;
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async function fetchPackageJsonFromUrl(url, headers) {
|
|
159
|
+
try {
|
|
160
|
+
const response = await fetch(url, { headers });
|
|
161
|
+
if (!response.ok)
|
|
162
|
+
return null;
|
|
163
|
+
return await parsePackageJsonResponse(response);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function normalizeProductHost(host) {
|
|
170
|
+
if (!host)
|
|
171
|
+
return undefined;
|
|
172
|
+
const normalized = host
|
|
173
|
+
.replace(/^https?:\/\//, '')
|
|
174
|
+
.replace(/\/$/, '')
|
|
175
|
+
.toLowerCase();
|
|
176
|
+
if (normalized === 'epicweb.dev')
|
|
177
|
+
return 'www.epicweb.dev';
|
|
178
|
+
if (normalized === 'epicreact.dev')
|
|
179
|
+
return 'www.epicreact.dev';
|
|
180
|
+
if (normalized === 'epicai.pro')
|
|
181
|
+
return 'www.epicai.pro';
|
|
182
|
+
return normalized;
|
|
183
|
+
}
|
|
125
184
|
/**
|
|
126
185
|
* Fetch available workshops from GitHub (epicweb-dev org with 'workshop' topic)
|
|
127
186
|
*/
|
|
@@ -175,31 +234,30 @@ async function fetchAvailableWorkshops() {
|
|
|
175
234
|
/**
|
|
176
235
|
* Fetch a workshop's package.json from GitHub raw content
|
|
177
236
|
*/
|
|
178
|
-
async function fetchWorkshopPackageJson(
|
|
237
|
+
async function fetchWorkshopPackageJson(repo) {
|
|
179
238
|
return cachified({
|
|
180
|
-
key: `github-package-json:${
|
|
239
|
+
key: `github-package-json:${repo.name}`,
|
|
181
240
|
cache: githubCache,
|
|
182
241
|
ttl: 1000 * 60 * 60 * 6, // 6 hours
|
|
183
242
|
swr: 1000 * 60 * 60 * 24 * 30, // 30 days stale-while-revalidate
|
|
184
243
|
checkValue: PackageJsonSchema.nullable(),
|
|
185
|
-
async getFreshValue() {
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if (!response.ok) {
|
|
194
|
-
return null;
|
|
195
|
-
}
|
|
196
|
-
try {
|
|
197
|
-
const parsed = PackageJsonSchema.safeParse(await response.json());
|
|
198
|
-
return parsed.success ? parsed.data : null;
|
|
244
|
+
async getFreshValue(context) {
|
|
245
|
+
const rawHeaders = getGitHubHeadersWithAccept('application/vnd.github.raw');
|
|
246
|
+
const rawUrls = buildRawPackageJsonUrls(repo.name, repo.default_branch);
|
|
247
|
+
for (const url of rawUrls) {
|
|
248
|
+
const packageJson = await fetchPackageJsonFromUrl(url, rawHeaders);
|
|
249
|
+
if (packageJson) {
|
|
250
|
+
return packageJson;
|
|
251
|
+
}
|
|
199
252
|
}
|
|
200
|
-
|
|
201
|
-
|
|
253
|
+
const apiUrl = `https://api.github.com/repos/${GITHUB_ORG}/${repo.name}/contents/package.json`;
|
|
254
|
+
const apiPackageJson = await fetchPackageJsonFromUrl(apiUrl, getGitHubHeadersWithAccept('application/vnd.github.raw'));
|
|
255
|
+
if (apiPackageJson) {
|
|
256
|
+
return apiPackageJson;
|
|
202
257
|
}
|
|
258
|
+
context.metadata.ttl = 1000 * 60;
|
|
259
|
+
context.metadata.swr = 0;
|
|
260
|
+
return null;
|
|
203
261
|
},
|
|
204
262
|
});
|
|
205
263
|
}
|
|
@@ -207,13 +265,17 @@ async function fetchWorkshopPackageJson(repoName) {
|
|
|
207
265
|
* Enrich workshops with metadata from their package.json files
|
|
208
266
|
*/
|
|
209
267
|
async function enrichWorkshopsWithMetadata(workshops) {
|
|
210
|
-
const packageJsons = await Promise.all(workshops.map((w) => fetchWorkshopPackageJson(
|
|
268
|
+
const packageJsons = await Promise.all(workshops.map((w) => fetchWorkshopPackageJson({
|
|
269
|
+
name: w.name,
|
|
270
|
+
default_branch: w.default_branch,
|
|
271
|
+
})));
|
|
211
272
|
return workshops.map((workshop, index) => {
|
|
212
273
|
const packageJson = packageJsons[index];
|
|
213
274
|
const config = packageJson ? parseEpicshopConfig(packageJson) : null;
|
|
275
|
+
const productHost = normalizeProductHost(config?.product?.host);
|
|
214
276
|
return {
|
|
215
277
|
...workshop,
|
|
216
|
-
productHost
|
|
278
|
+
productHost,
|
|
217
279
|
productSlug: config?.product?.slug,
|
|
218
280
|
productDisplayName: config?.product?.displayName,
|
|
219
281
|
instructorName: config?.instructor?.name,
|
|
@@ -251,11 +313,14 @@ async function checkWorkshopDownloadStatus(workshops) {
|
|
|
251
313
|
/**
|
|
252
314
|
* Check access for workshops in parallel
|
|
253
315
|
*/
|
|
254
|
-
async function checkWorkshopAccess(workshops) {
|
|
316
|
+
async function checkWorkshopAccess(workshops, authStatusMap) {
|
|
255
317
|
const accessResults = await Promise.all(workshops.map(async (workshop) => {
|
|
256
318
|
if (!workshop.productHost || !workshop.productSlug) {
|
|
257
319
|
return undefined;
|
|
258
320
|
}
|
|
321
|
+
if (authStatusMap?.get(workshop.productHost) === false) {
|
|
322
|
+
return undefined;
|
|
323
|
+
}
|
|
259
324
|
return userHasAccessToWorkshop({
|
|
260
325
|
productHost: workshop.productHost,
|
|
261
326
|
workshopSlug: workshop.productSlug,
|
|
@@ -266,6 +331,57 @@ async function checkWorkshopAccess(workshops) {
|
|
|
266
331
|
hasAccess: accessResults[index],
|
|
267
332
|
}));
|
|
268
333
|
}
|
|
334
|
+
async function resolvePreferredEditor({ silent, }) {
|
|
335
|
+
const { getPreferredEditor, setPreferredEditor } = await import('@epic-web/workshop-utils/workshops.server');
|
|
336
|
+
const { getDefaultEditorCommand, formatEditorLabel } = await import('@epic-web/workshop-utils/launch-editor.server');
|
|
337
|
+
const preferredEditor = await getPreferredEditor();
|
|
338
|
+
if (preferredEditor)
|
|
339
|
+
return preferredEditor;
|
|
340
|
+
const defaultEditor = getDefaultEditorCommand();
|
|
341
|
+
if (silent)
|
|
342
|
+
return defaultEditor;
|
|
343
|
+
assertCanPrompt({
|
|
344
|
+
reason: 'choose a preferred editor',
|
|
345
|
+
hints: ['Set it later with: npx epicshop config editor'],
|
|
346
|
+
});
|
|
347
|
+
const { select, confirm } = await import('@inquirer/prompts');
|
|
348
|
+
const availableEditors = await getInstalledEditorChoices();
|
|
349
|
+
if (defaultEditor) {
|
|
350
|
+
const defaultLabel = formatEditorLabel(defaultEditor);
|
|
351
|
+
if (availableEditors.length === 0) {
|
|
352
|
+
const useDefault = await confirm({
|
|
353
|
+
message: `Use ${defaultLabel} to open workshops?`,
|
|
354
|
+
default: true,
|
|
355
|
+
});
|
|
356
|
+
if (useDefault) {
|
|
357
|
+
await setPreferredEditor(defaultEditor);
|
|
358
|
+
return defaultEditor;
|
|
359
|
+
}
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
const decision = await select({
|
|
363
|
+
message: `Open workshops with ${defaultLabel}?`,
|
|
364
|
+
choices: [
|
|
365
|
+
{ name: `Use ${defaultLabel}`, value: 'use' },
|
|
366
|
+
{ name: 'Choose a different editor', value: 'choose' },
|
|
367
|
+
],
|
|
368
|
+
});
|
|
369
|
+
if (decision === 'use') {
|
|
370
|
+
await setPreferredEditor(defaultEditor);
|
|
371
|
+
return defaultEditor;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (availableEditors.length === 0) {
|
|
375
|
+
console.log(chalk.yellow('⚠️ No supported editors detected. Set EPICSHOP_EDITOR or install a supported editor.'));
|
|
376
|
+
return defaultEditor;
|
|
377
|
+
}
|
|
378
|
+
const selectedEditor = await select({
|
|
379
|
+
message: 'Select your preferred editor:',
|
|
380
|
+
choices: availableEditors,
|
|
381
|
+
});
|
|
382
|
+
await setPreferredEditor(selectedEditor);
|
|
383
|
+
return selectedEditor;
|
|
384
|
+
}
|
|
269
385
|
/**
|
|
270
386
|
* Helper function to add a single workshop by repo name
|
|
271
387
|
* This handles the actual cloning and setup logic
|
|
@@ -497,7 +613,7 @@ export async function add(options) {
|
|
|
497
613
|
console.log();
|
|
498
614
|
}
|
|
499
615
|
spinner.start('Checking access...');
|
|
500
|
-
enrichedWorkshops = await checkWorkshopAccess(enrichedWorkshops);
|
|
616
|
+
enrichedWorkshops = await checkWorkshopAccess(enrichedWorkshops, authStatusMap);
|
|
501
617
|
enrichedWorkshops.sort((a, b) => {
|
|
502
618
|
const aHasAccess = a.hasAccess === true;
|
|
503
619
|
const aIsDownloaded = a.isDownloaded === true;
|
|
@@ -1294,6 +1410,10 @@ export async function openWorkshop(options = {}) {
|
|
|
1294
1410
|
console.log(chalk.red(`❌ ${message}`));
|
|
1295
1411
|
return { success: false, message };
|
|
1296
1412
|
}
|
|
1413
|
+
const preferredEditor = await resolvePreferredEditor({ silent });
|
|
1414
|
+
if (preferredEditor) {
|
|
1415
|
+
process.env.EPICSHOP_EDITOR = preferredEditor;
|
|
1416
|
+
}
|
|
1297
1417
|
if (!silent) {
|
|
1298
1418
|
console.log(chalk.cyan(`📂 Opening ${chalk.bold(workshopToOpen.title)} in your editor...`));
|
|
1299
1419
|
console.log(chalk.gray(` Path: ${workshopToOpen.path}\n`));
|
|
@@ -1329,7 +1449,7 @@ export async function openWorkshop(options = {}) {
|
|
|
1329
1449
|
export async function config(options = {}) {
|
|
1330
1450
|
const { silent = false } = options;
|
|
1331
1451
|
try {
|
|
1332
|
-
const { getReposDirectory, setReposDirectory, isReposDirectoryConfigured, loadConfig, saveConfig, getDefaultReposDir, deleteConfig, } = await import('@epic-web/workshop-utils/workshops.server');
|
|
1452
|
+
const { getReposDirectory, setReposDirectory, isReposDirectoryConfigured, loadConfig, saveConfig, getDefaultReposDir, getPreferredEditor, setPreferredEditor, clearPreferredEditor, deleteConfig, } = await import('@epic-web/workshop-utils/workshops.server');
|
|
1333
1453
|
// Handle reset subcommand
|
|
1334
1454
|
if (options.subcommand === 'reset' || options.subcommand === 'delete') {
|
|
1335
1455
|
if (silent) {
|
|
@@ -1369,6 +1489,13 @@ export async function config(options = {}) {
|
|
|
1369
1489
|
if (!silent)
|
|
1370
1490
|
console.log(chalk.green(`✅ ${message}`));
|
|
1371
1491
|
}
|
|
1492
|
+
if (options.preferredEditor) {
|
|
1493
|
+
await setPreferredEditor(options.preferredEditor);
|
|
1494
|
+
const message = `Preferred editor set to: ${options.preferredEditor}`;
|
|
1495
|
+
messages.push(message);
|
|
1496
|
+
if (!silent)
|
|
1497
|
+
console.log(chalk.green(`✅ ${message}`));
|
|
1498
|
+
}
|
|
1372
1499
|
// If either option was set, return now
|
|
1373
1500
|
if (messages.length > 0) {
|
|
1374
1501
|
return { success: true, message: messages.join('; ') };
|
|
@@ -1376,20 +1503,36 @@ export async function config(options = {}) {
|
|
|
1376
1503
|
if (silent) {
|
|
1377
1504
|
// In silent mode, just return current config
|
|
1378
1505
|
const reposDir = await getReposDirectory();
|
|
1379
|
-
|
|
1506
|
+
const preferredEditor = await getPreferredEditor();
|
|
1507
|
+
const editorMessage = preferredEditor
|
|
1508
|
+
? `Preferred editor: ${preferredEditor}`
|
|
1509
|
+
: 'Preferred editor: not set';
|
|
1510
|
+
return {
|
|
1511
|
+
success: true,
|
|
1512
|
+
message: `Repos directory: ${reposDir}; ${editorMessage}`,
|
|
1513
|
+
};
|
|
1380
1514
|
}
|
|
1381
1515
|
// Interactive config selection
|
|
1382
1516
|
assertCanPrompt({
|
|
1383
1517
|
reason: 'select a configuration option',
|
|
1384
1518
|
hints: [
|
|
1385
1519
|
'Set repos dir directly: npx epicshop config --repos-dir <path>',
|
|
1520
|
+
'Set preferred editor: npx epicshop config --editor <command>',
|
|
1386
1521
|
'Delete config non-interactively: npx epicshop config reset --silent',
|
|
1387
1522
|
],
|
|
1388
1523
|
});
|
|
1389
|
-
const { search, confirm } = await import('@inquirer/prompts');
|
|
1524
|
+
const { search, confirm, select } = await import('@inquirer/prompts');
|
|
1525
|
+
const { formatEditorLabel } = await import('@epic-web/workshop-utils/launch-editor.server');
|
|
1390
1526
|
const reposDir = await getReposDirectory();
|
|
1391
1527
|
const isConfigured = await isReposDirectoryConfigured();
|
|
1392
1528
|
const defaultDir = getDefaultReposDir();
|
|
1529
|
+
const preferredEditor = await getPreferredEditor();
|
|
1530
|
+
const preferredEditorDescription = preferredEditor
|
|
1531
|
+
? formatEditorChoiceName({
|
|
1532
|
+
label: formatEditorLabel(preferredEditor),
|
|
1533
|
+
command: preferredEditor,
|
|
1534
|
+
})
|
|
1535
|
+
: 'Not set';
|
|
1393
1536
|
// Build config options
|
|
1394
1537
|
const configOptions = [
|
|
1395
1538
|
{
|
|
@@ -1397,13 +1540,92 @@ export async function config(options = {}) {
|
|
|
1397
1540
|
value: 'repos-dir',
|
|
1398
1541
|
description: isConfigured ? reposDir : `${reposDir} (default)`,
|
|
1399
1542
|
},
|
|
1543
|
+
{
|
|
1544
|
+
name: 'Preferred editor',
|
|
1545
|
+
value: 'preferred-editor',
|
|
1546
|
+
description: preferredEditorDescription,
|
|
1547
|
+
},
|
|
1400
1548
|
{
|
|
1401
1549
|
name: `Reset config file`,
|
|
1402
1550
|
value: 'reset',
|
|
1403
1551
|
description: 'Delete config file and reset all settings to defaults',
|
|
1404
1552
|
},
|
|
1405
1553
|
];
|
|
1554
|
+
const handlePreferredEditorConfig = async () => {
|
|
1555
|
+
console.log();
|
|
1556
|
+
console.log(chalk.bold(' Current value:'));
|
|
1557
|
+
if (preferredEditor) {
|
|
1558
|
+
console.log(chalk.white(` ${preferredEditorDescription}`));
|
|
1559
|
+
}
|
|
1560
|
+
else {
|
|
1561
|
+
console.log(chalk.gray(' Not set'));
|
|
1562
|
+
}
|
|
1563
|
+
console.log();
|
|
1564
|
+
const actionChoices = [
|
|
1565
|
+
{
|
|
1566
|
+
name: 'Edit',
|
|
1567
|
+
value: 'edit',
|
|
1568
|
+
description: 'Choose a preferred editor',
|
|
1569
|
+
},
|
|
1570
|
+
...(preferredEditor
|
|
1571
|
+
? [
|
|
1572
|
+
{
|
|
1573
|
+
name: 'Remove',
|
|
1574
|
+
value: 'remove',
|
|
1575
|
+
description: 'Clear the preferred editor',
|
|
1576
|
+
},
|
|
1577
|
+
]
|
|
1578
|
+
: []),
|
|
1579
|
+
{
|
|
1580
|
+
name: 'Cancel',
|
|
1581
|
+
value: 'cancel',
|
|
1582
|
+
description: 'Go back without changes',
|
|
1583
|
+
},
|
|
1584
|
+
];
|
|
1585
|
+
const action = await search({
|
|
1586
|
+
message: 'What would you like to do?',
|
|
1587
|
+
source: async (input) => {
|
|
1588
|
+
if (!input)
|
|
1589
|
+
return actionChoices;
|
|
1590
|
+
return matchSorter(actionChoices, input, {
|
|
1591
|
+
keys: ['name', 'value', 'description'],
|
|
1592
|
+
});
|
|
1593
|
+
},
|
|
1594
|
+
});
|
|
1595
|
+
if (action === 'edit') {
|
|
1596
|
+
const editorChoices = await getInstalledEditorChoices();
|
|
1597
|
+
if (editorChoices.length === 0) {
|
|
1598
|
+
console.log(chalk.yellow('⚠️ No supported editors detected. Set EPICSHOP_EDITOR or install a supported editor.'));
|
|
1599
|
+
return {
|
|
1600
|
+
success: true,
|
|
1601
|
+
message: 'No supported editors detected',
|
|
1602
|
+
};
|
|
1603
|
+
}
|
|
1604
|
+
const selectedEditor = await select({
|
|
1605
|
+
message: 'Select your preferred editor:',
|
|
1606
|
+
choices: editorChoices,
|
|
1607
|
+
});
|
|
1608
|
+
await setPreferredEditor(selectedEditor);
|
|
1609
|
+
console.log();
|
|
1610
|
+
console.log(chalk.green(`✅ Preferred editor set to: ${chalk.bold(selectedEditor)}`));
|
|
1611
|
+
return {
|
|
1612
|
+
success: true,
|
|
1613
|
+
message: `Preferred editor set to: ${selectedEditor}`,
|
|
1614
|
+
};
|
|
1615
|
+
}
|
|
1616
|
+
if (action === 'remove') {
|
|
1617
|
+
await clearPreferredEditor();
|
|
1618
|
+
console.log();
|
|
1619
|
+
console.log(chalk.green('✅ Preferred editor cleared.'));
|
|
1620
|
+
return { success: true, message: 'Preferred editor cleared' };
|
|
1621
|
+
}
|
|
1622
|
+
console.log(chalk.gray('\nNo changes made.'));
|
|
1623
|
+
return { success: true, message: 'Cancelled' };
|
|
1624
|
+
};
|
|
1406
1625
|
console.log(chalk.bold.cyan('\n⚙️ Workshop Configuration\n'));
|
|
1626
|
+
if (options.subcommand === 'editor') {
|
|
1627
|
+
return await handlePreferredEditorConfig();
|
|
1628
|
+
}
|
|
1407
1629
|
const selectedConfig = await search({
|
|
1408
1630
|
message: 'Select a setting to configure:',
|
|
1409
1631
|
source: async (input) => {
|
|
@@ -1433,6 +1655,9 @@ export async function config(options = {}) {
|
|
|
1433
1655
|
return { success: true, message: 'Cancelled' };
|
|
1434
1656
|
}
|
|
1435
1657
|
}
|
|
1658
|
+
if (selectedConfig === 'preferred-editor') {
|
|
1659
|
+
return await handlePreferredEditorConfig();
|
|
1660
|
+
}
|
|
1436
1661
|
if (selectedConfig === 'repos-dir') {
|
|
1437
1662
|
// Show current value and actions
|
|
1438
1663
|
console.log();
|
|
@@ -1842,7 +2067,7 @@ async function promptAndSetupAccessibleWorkshops() {
|
|
|
1842
2067
|
else {
|
|
1843
2068
|
spinner.start('Checking access...');
|
|
1844
2069
|
}
|
|
1845
|
-
enrichedWorkshops = await checkWorkshopAccess(enrichedWorkshops);
|
|
2070
|
+
enrichedWorkshops = await checkWorkshopAccess(enrichedWorkshops, authStatusMap);
|
|
1846
2071
|
spinner.succeed(`Found ${enrichedWorkshops.length} available workshops`);
|
|
1847
2072
|
}
|
|
1848
2073
|
catch (error) {
|
|
@@ -1851,21 +2076,33 @@ async function promptAndSetupAccessibleWorkshops() {
|
|
|
1851
2076
|
console.log(chalk.yellow(`⚠️ Could not load workshops right now. Skipping this step.\n`));
|
|
1852
2077
|
return;
|
|
1853
2078
|
}
|
|
1854
|
-
const
|
|
1855
|
-
|
|
2079
|
+
const availableWorkshops = enrichedWorkshops.filter((w) => w.name !== TUTORIAL_REPO && !w.isDownloaded);
|
|
2080
|
+
const accessibleWorkshops = availableWorkshops.filter((w) => w.hasAccess === true);
|
|
2081
|
+
const selectableWorkshops = accessibleWorkshops.length > 0
|
|
2082
|
+
? accessibleWorkshops
|
|
2083
|
+
: availableWorkshops.filter((w) => w.hasAccess !== false);
|
|
2084
|
+
if (selectableWorkshops.length === 0) {
|
|
1856
2085
|
console.log(chalk.gray('No additional workshops to set up right now (either none found, none accessible, or already downloaded).\n'));
|
|
1857
2086
|
return;
|
|
1858
2087
|
}
|
|
1859
2088
|
console.log();
|
|
1860
|
-
|
|
2089
|
+
const header = accessibleWorkshops.length > 0
|
|
2090
|
+
? 'Available Workshops You Have Access To\n'
|
|
2091
|
+
: 'Available Workshops\n';
|
|
2092
|
+
console.log(chalk.bold.cyan(header));
|
|
1861
2093
|
console.log(chalk.gray('Icon Key:'));
|
|
1862
2094
|
console.log(chalk.gray(` 🚀 EpicReact.dev`));
|
|
1863
2095
|
console.log(chalk.gray(` 🌌 EpicWeb.dev`));
|
|
1864
2096
|
console.log(chalk.gray(` ⚡ EpicAI.pro`));
|
|
1865
2097
|
console.log(chalk.gray(` 🔑 You have access to this workshop`));
|
|
1866
2098
|
console.log();
|
|
2099
|
+
if (accessibleWorkshops.length === 0) {
|
|
2100
|
+
console.log(chalk.yellow('💡 We could not confirm access for available workshops. You can still select them to try setup.'));
|
|
2101
|
+
console.log(chalk.gray(` To verify access, log in with: ${chalk.cyan('npx epicshop auth')}`));
|
|
2102
|
+
console.log();
|
|
2103
|
+
}
|
|
1867
2104
|
// Filter workshops that are part of a product (for "All My Workshops" option)
|
|
1868
|
-
const workshopsWithProduct =
|
|
2105
|
+
const workshopsWithProduct = accessibleWorkshops.filter((w) => w.productSlug);
|
|
1869
2106
|
// Group workshops by product for quick-select options
|
|
1870
2107
|
const workshopsByProduct = new Map();
|
|
1871
2108
|
for (const w of workshopsWithProduct) {
|
|
@@ -1934,11 +2171,11 @@ async function promptAndSetupAccessibleWorkshops() {
|
|
|
1934
2171
|
}
|
|
1935
2172
|
else {
|
|
1936
2173
|
// Show checkbox for individual selection
|
|
1937
|
-
const individualChoices =
|
|
2174
|
+
const individualChoices = selectableWorkshops.map((w) => {
|
|
1938
2175
|
const productIcon = w.productHost
|
|
1939
2176
|
? PRODUCT_ICONS[w.productHost] || ''
|
|
1940
2177
|
: '';
|
|
1941
|
-
const accessIcon = chalk.yellow('🔑');
|
|
2178
|
+
const accessIcon = w.hasAccess === true ? chalk.yellow('🔑') : '';
|
|
1942
2179
|
const name = [productIcon, w.title || w.name, accessIcon]
|
|
1943
2180
|
.filter(Boolean)
|
|
1944
2181
|
.join(' ');
|
|
@@ -1966,7 +2203,7 @@ async function promptAndSetupAccessibleWorkshops() {
|
|
|
1966
2203
|
}
|
|
1967
2204
|
// Create a map from repo name to workshop title for nice display
|
|
1968
2205
|
const repoToTitle = new Map();
|
|
1969
|
-
for (const w of
|
|
2206
|
+
for (const w of selectableWorkshops) {
|
|
1970
2207
|
repoToTitle.set(w.name, w.title || w.name);
|
|
1971
2208
|
}
|
|
1972
2209
|
const getDisplayName = (repo) => repoToTitle.get(repo) || repo;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "epicshop",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.76.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -99,7 +99,7 @@
|
|
|
99
99
|
"build:watch": "nx watch --projects=epicshop -- nx run \\$NX_PROJECT_NAME:build"
|
|
100
100
|
},
|
|
101
101
|
"dependencies": {
|
|
102
|
-
"@epic-web/workshop-utils": "6.
|
|
102
|
+
"@epic-web/workshop-utils": "6.76.0",
|
|
103
103
|
"@inquirer/prompts": "^8.2.0",
|
|
104
104
|
"@sentry/node": "^10.36.0",
|
|
105
105
|
"chalk": "^5.6.2",
|