epicshop 6.50.1 → 6.50.3

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 CHANGED
@@ -47,6 +47,10 @@ epicshop start <workshop>
47
47
  - `epicshop open`: open a workshop in your editor
48
48
  - `epicshop update`: pull the latest workshop changes
49
49
  - `epicshop warm`: warm caches for faster workshop startup
50
+ - `epicshop exercises`: list exercises with progress (context-aware)
51
+ - `epicshop playground`: view or set the current playground (context-aware)
52
+ - `epicshop progress`: view or update your progress (context-aware)
53
+ - `epicshop diff`: show diff between playground and solution (context-aware)
50
54
 
51
55
  ## Environment variables
52
56
 
@@ -62,6 +66,14 @@ This package also exports ESM entrypoints:
62
66
  import { start } from 'epicshop/start'
63
67
  import { update } from 'epicshop/update'
64
68
  import { warm } from 'epicshop/warm'
69
+ import { show, set } from 'epicshop/playground'
70
+ import {
71
+ show as showProgress,
72
+ update as updateProgress,
73
+ } from 'epicshop/progress'
74
+ import { showProgressDiff, showDiffBetweenApps } from 'epicshop/diff'
75
+ import { list, showExercise } from 'epicshop/exercises'
76
+ import { status, login, logout } from 'epicshop/auth'
65
77
  ```
66
78
 
67
79
  ## Documentation
package/dist/cli.js CHANGED
@@ -636,6 +636,309 @@ const cli = yargs(args)
636
636
  if (!result.success) {
637
637
  process.exit(1);
638
638
  }
639
+ })
640
+ .command('playground [subcommand] [target]', 'Manage the playground environment (context-aware)', (yargs) => {
641
+ return yargs
642
+ .positional('subcommand', {
643
+ describe: 'Playground subcommand (show, set)',
644
+ type: 'string',
645
+ choices: ['show', 'set'],
646
+ })
647
+ .positional('target', {
648
+ describe: 'Target exercise step (e.g., 1.2.problem, 2.3.solution)',
649
+ type: 'string',
650
+ })
651
+ .option('exercise', {
652
+ alias: 'e',
653
+ type: 'number',
654
+ description: 'Exercise number',
655
+ })
656
+ .option('step', {
657
+ type: 'number',
658
+ description: 'Step number',
659
+ })
660
+ .option('type', {
661
+ alias: 't',
662
+ type: 'string',
663
+ choices: ['problem', 'solution'],
664
+ description: 'App type (problem or solution)',
665
+ })
666
+ .option('silent', {
667
+ alias: 's',
668
+ type: 'boolean',
669
+ description: 'Run without output logs',
670
+ default: false,
671
+ })
672
+ .example('$0 playground', 'Show current playground status')
673
+ .example('$0 playground show', 'Show current playground status')
674
+ .example('$0 playground set', 'Set playground (auto-detect next)')
675
+ .example('$0 playground set 1.2.problem', 'Set to specific step')
676
+ .example('$0 playground set --exercise 1 --step 2', 'Set with options');
677
+ }, async (argv) => {
678
+ const { findWorkshopRoot } = await import("./commands/workshops.js");
679
+ const workshopRoot = await findWorkshopRoot();
680
+ if (!workshopRoot) {
681
+ console.error(chalk.red('❌ Not inside a workshop directory. Please cd into a workshop first.'));
682
+ process.exit(1);
683
+ }
684
+ const originalCwd = process.cwd();
685
+ process.chdir(workshopRoot);
686
+ try {
687
+ const { show, set, selectAndSet, parseAppIdentifier } = await import("./commands/playground.js");
688
+ const subcommand = argv.subcommand || 'show';
689
+ if (subcommand === 'show') {
690
+ const result = await show({ silent: argv.silent });
691
+ if (!result.success)
692
+ process.exit(1);
693
+ }
694
+ else if (subcommand === 'set') {
695
+ // Parse target if provided (e.g., "1.2.problem")
696
+ let exerciseNumber = argv.exercise;
697
+ let stepNumber = argv.step;
698
+ let type = argv.type;
699
+ if (argv.target) {
700
+ const parsed = parseAppIdentifier(argv.target);
701
+ // Validate that the target was parseable - at least exercise number should be found
702
+ if (parsed.exerciseNumber === undefined) {
703
+ console.error(chalk.red(`❌ Invalid target format: "${argv.target}". Expected format like "1.2.problem" or "01.02.solution"`));
704
+ process.exit(1);
705
+ }
706
+ exerciseNumber = parsed.exerciseNumber ?? exerciseNumber;
707
+ stepNumber = parsed.stepNumber ?? stepNumber;
708
+ type = parsed.type ?? type;
709
+ }
710
+ // If no specific target, check if we should show interactive or auto
711
+ if (!exerciseNumber && !stepNumber && !type) {
712
+ // Auto-detect next step or show interactive
713
+ const result = await set({ silent: argv.silent });
714
+ if (!result.success) {
715
+ // If auto-detect fails, try interactive
716
+ const interactiveResult = await selectAndSet({
717
+ silent: argv.silent,
718
+ });
719
+ if (!interactiveResult.success)
720
+ process.exit(1);
721
+ }
722
+ }
723
+ else {
724
+ const result = await set({
725
+ exerciseNumber,
726
+ stepNumber,
727
+ type,
728
+ silent: argv.silent,
729
+ });
730
+ if (!result.success)
731
+ process.exit(1);
732
+ }
733
+ }
734
+ else {
735
+ console.error(chalk.red(`❌ Unknown subcommand: ${subcommand}`));
736
+ process.exit(1);
737
+ }
738
+ }
739
+ finally {
740
+ process.chdir(originalCwd);
741
+ }
742
+ })
743
+ .command('progress [subcommand] [lesson-slug]', 'View and manage your progress (context-aware)', (yargs) => {
744
+ return yargs
745
+ .positional('subcommand', {
746
+ describe: 'Progress subcommand (show, update)',
747
+ type: 'string',
748
+ choices: ['show', 'update'],
749
+ })
750
+ .positional('lesson-slug', {
751
+ describe: 'Lesson slug to update (for update subcommand)',
752
+ type: 'string',
753
+ })
754
+ .option('complete', {
755
+ alias: 'c',
756
+ type: 'boolean',
757
+ description: 'Mark as complete (default: true)',
758
+ default: true,
759
+ })
760
+ .option('incomplete', {
761
+ alias: 'i',
762
+ type: 'boolean',
763
+ description: 'Mark as incomplete',
764
+ default: false,
765
+ })
766
+ .option('json', {
767
+ type: 'boolean',
768
+ description: 'Output as JSON',
769
+ default: false,
770
+ })
771
+ .option('silent', {
772
+ alias: 's',
773
+ type: 'boolean',
774
+ description: 'Run without output logs',
775
+ default: false,
776
+ })
777
+ .example('$0 progress', 'Show progress for current workshop')
778
+ .example('$0 progress show', 'Show progress for current workshop')
779
+ .example('$0 progress show --json', 'Output progress as JSON')
780
+ .example('$0 progress update 01-01-problem', 'Mark lesson as complete')
781
+ .example('$0 progress update 01-01-problem --incomplete', 'Mark lesson as incomplete');
782
+ }, async (argv) => {
783
+ const { findWorkshopRoot } = await import("./commands/workshops.js");
784
+ const workshopRoot = await findWorkshopRoot();
785
+ if (!workshopRoot) {
786
+ console.error(chalk.red('❌ Not inside a workshop directory. Please cd into a workshop first.'));
787
+ process.exit(1);
788
+ }
789
+ const originalCwd = process.cwd();
790
+ process.chdir(workshopRoot);
791
+ try {
792
+ const { show, update } = await import("./commands/progress.js");
793
+ const subcommand = argv.subcommand || 'show';
794
+ if (subcommand === 'show') {
795
+ const result = await show({
796
+ silent: argv.silent,
797
+ json: argv.json,
798
+ });
799
+ if (!result.success)
800
+ process.exit(1);
801
+ }
802
+ else if (subcommand === 'update') {
803
+ // --incomplete takes precedence, otherwise use --complete value
804
+ const complete = argv.incomplete ? false : argv.complete;
805
+ const result = await update({
806
+ lessonSlug: argv.lessonSlug,
807
+ complete,
808
+ silent: argv.silent,
809
+ });
810
+ if (!result.success)
811
+ process.exit(1);
812
+ }
813
+ else {
814
+ console.error(chalk.red(`❌ Unknown subcommand: ${subcommand}`));
815
+ process.exit(1);
816
+ }
817
+ }
818
+ finally {
819
+ process.chdir(originalCwd);
820
+ }
821
+ })
822
+ .command('diff [app1] [app2]', 'Show differences between apps or playground vs solution (context-aware)', (yargs) => {
823
+ return yargs
824
+ .positional('app1', {
825
+ describe: 'First app identifier (e.g., 01.02.problem). If omitted, shows playground vs solution.',
826
+ type: 'string',
827
+ })
828
+ .positional('app2', {
829
+ describe: 'Second app identifier (e.g., 01.02.solution)',
830
+ type: 'string',
831
+ })
832
+ .option('silent', {
833
+ alias: 's',
834
+ type: 'boolean',
835
+ description: 'Run without output logs',
836
+ default: false,
837
+ })
838
+ .example('$0 diff', 'Show diff between playground and solution')
839
+ .example('$0 diff 01.02.problem 01.02.solution', 'Show diff between two apps');
840
+ }, async (argv) => {
841
+ const { findWorkshopRoot } = await import("./commands/workshops.js");
842
+ const workshopRoot = await findWorkshopRoot();
843
+ if (!workshopRoot) {
844
+ console.error(chalk.red('❌ Not inside a workshop directory. Please cd into a workshop first.'));
845
+ process.exit(1);
846
+ }
847
+ const originalCwd = process.cwd();
848
+ process.chdir(workshopRoot);
849
+ try {
850
+ const { showProgressDiff, showDiffBetweenApps } = await import("./commands/diff.js");
851
+ if (argv.app1 && argv.app2) {
852
+ const result = await showDiffBetweenApps({
853
+ app1: argv.app1,
854
+ app2: argv.app2,
855
+ silent: argv.silent,
856
+ });
857
+ if (!result.success)
858
+ process.exit(1);
859
+ }
860
+ else if (argv.app1 && !argv.app2) {
861
+ console.error(chalk.red('❌ When providing app1, app2 is also required'));
862
+ process.exit(1);
863
+ }
864
+ else {
865
+ const result = await showProgressDiff({ silent: argv.silent });
866
+ if (!result.success)
867
+ process.exit(1);
868
+ }
869
+ }
870
+ finally {
871
+ process.chdir(originalCwd);
872
+ }
873
+ })
874
+ .command('exercises [exercise] [step]', 'List exercises or show exercise details (context-aware)', (yargs) => {
875
+ return yargs
876
+ .positional('exercise', {
877
+ describe: 'Exercise number to show details for (e.g., 1 or 01)',
878
+ type: 'string',
879
+ })
880
+ .positional('step', {
881
+ describe: 'Step number to show details for (e.g., 2 or 02)',
882
+ type: 'string',
883
+ })
884
+ .option('json', {
885
+ type: 'boolean',
886
+ description: 'Output as JSON',
887
+ default: false,
888
+ })
889
+ .option('silent', {
890
+ alias: 's',
891
+ type: 'boolean',
892
+ description: 'Run without output logs',
893
+ default: false,
894
+ })
895
+ .example('$0 exercises', 'List all exercises with progress')
896
+ .example('$0 exercises 1', 'Show details for exercise 1')
897
+ .example('$0 exercises 1 2', 'Show details for exercise 1 step 2')
898
+ .example('$0 exercises --json', 'Output exercises as JSON');
899
+ }, async (argv) => {
900
+ const { findWorkshopRoot } = await import("./commands/workshops.js");
901
+ const workshopRoot = await findWorkshopRoot();
902
+ if (!workshopRoot) {
903
+ console.error(chalk.red('❌ Not inside a workshop directory. Please cd into a workshop first.'));
904
+ process.exit(1);
905
+ }
906
+ const originalCwd = process.cwd();
907
+ process.chdir(workshopRoot);
908
+ try {
909
+ const { list, showExercise } = await import("./commands/exercises.js");
910
+ if (argv.exercise) {
911
+ const exerciseNumber = parseInt(argv.exercise, 10);
912
+ if (isNaN(exerciseNumber)) {
913
+ console.error(chalk.red(`❌ Invalid exercise number: "${argv.exercise}". Expected a number.`));
914
+ process.exit(1);
915
+ }
916
+ const stepNumber = argv.step ? parseInt(argv.step, 10) : undefined;
917
+ if (stepNumber !== undefined && isNaN(stepNumber)) {
918
+ console.error(chalk.red(`❌ Invalid step number: "${argv.step}". Expected a number.`));
919
+ process.exit(1);
920
+ }
921
+ const result = await showExercise({
922
+ exerciseNumber,
923
+ stepNumber,
924
+ json: argv.json,
925
+ silent: argv.silent,
926
+ });
927
+ if (!result.success)
928
+ process.exit(1);
929
+ }
930
+ else {
931
+ const result = await list({
932
+ json: argv.json,
933
+ silent: argv.silent,
934
+ });
935
+ if (!result.success)
936
+ process.exit(1);
937
+ }
938
+ }
939
+ finally {
940
+ process.chdir(originalCwd);
941
+ }
639
942
  })
640
943
  .epilogue(`For more information, visit: ${chalk.cyan('https://github.com/epicweb-dev/epicshop')}`)
641
944
  .strict()
@@ -674,73 +977,86 @@ try {
674
977
  ],
675
978
  });
676
979
  const { search } = await import('@inquirer/prompts');
677
- const baseChoices = [
678
- {
679
- name: `${chalk.green('start')} - Start a workshop`,
680
- value: 'start',
681
- description: workshopTitle
682
- ? `Start ${workshopTitle}`
683
- : 'Select a workshop to start',
684
- },
685
- {
686
- name: `${chalk.green('open')} - Open a workshop in editor`,
687
- value: 'open',
688
- description: workshopTitle
689
- ? `Open ${workshopTitle}`
690
- : 'Select a workshop to open',
691
- },
692
- {
693
- name: `${chalk.green('list')} - List all workshops`,
694
- value: 'list',
695
- description: 'List all added workshops',
696
- },
697
- {
698
- name: `${chalk.green('add')} - Add a workshop`,
699
- value: 'add',
700
- description: 'Clone a workshop from epicweb-dev GitHub org',
701
- },
702
- {
703
- name: `${chalk.green('remove')} - Remove a workshop`,
704
- value: 'remove',
705
- description: workshopTitle
706
- ? `Remove ${workshopTitle}`
707
- : 'Select a workshop to remove',
708
- },
709
- {
710
- name: `${chalk.green('update')} - Update workshop`,
711
- value: 'update',
712
- description: workshopTitle
713
- ? `Update ${workshopTitle}`
714
- : 'Select a workshop to update',
715
- },
716
- {
717
- name: `${chalk.green('warm')} - Warm caches`,
718
- value: 'warm',
719
- description: workshopTitle
720
- ? `Warm the cache for ${workshopTitle}`
721
- : 'Select a workshop to warm the cache for',
722
- },
723
- {
724
- name: `${chalk.green('config')} - View/update configuration`,
725
- value: 'config',
726
- description: 'View or update workshop configuration',
727
- },
728
- {
729
- name: `${chalk.green('init')} - First-time setup`,
730
- value: 'init',
731
- description: 'Initialize epicshop and start the tutorial',
732
- },
733
- {
734
- name: `${chalk.green('auth')} - Manage authentication`,
735
- value: 'auth',
736
- description: 'Login/logout from EpicWeb.dev, EpicReact.dev, EpicAI.pro',
737
- },
738
- {
739
- name: `${chalk.green('help')} - Show help`,
740
- value: 'help',
741
- description: 'Display CLI help information',
742
- },
743
- ];
980
+ // Build choices - workshop-specific commands only show when inside a workshop
981
+ const baseChoices = [];
982
+ // Always-available commands
983
+ baseChoices.push({
984
+ name: `${chalk.green('start')} - Start a workshop`,
985
+ value: 'start',
986
+ description: workshopTitle
987
+ ? `Start ${workshopTitle}`
988
+ : 'Select a workshop to start',
989
+ }, {
990
+ name: `${chalk.green('open')} - Open a workshop in editor`,
991
+ value: 'open',
992
+ description: workshopTitle
993
+ ? `Open ${workshopTitle}`
994
+ : 'Select a workshop to open',
995
+ });
996
+ // Workshop-specific commands (only when inside a workshop)
997
+ if (workshopTitle) {
998
+ baseChoices.push({
999
+ name: `${chalk.green('exercises')} - List exercises`,
1000
+ value: 'exercises',
1001
+ description: `View exercises and progress in ${workshopTitle}`,
1002
+ }, {
1003
+ name: `${chalk.green('playground')} - Manage playground`,
1004
+ value: 'playground',
1005
+ description: 'View or set the current playground',
1006
+ }, {
1007
+ name: `${chalk.green('progress')} - View/update progress`,
1008
+ value: 'progress',
1009
+ description: 'View or update your learning progress',
1010
+ }, {
1011
+ name: `${chalk.green('diff')} - Show differences`,
1012
+ value: 'diff',
1013
+ description: 'Show diff between playground and solution',
1014
+ });
1015
+ }
1016
+ // More always-available commands
1017
+ baseChoices.push({
1018
+ name: `${chalk.green('list')} - List all workshops`,
1019
+ value: 'list',
1020
+ description: 'List all added workshops',
1021
+ }, {
1022
+ name: `${chalk.green('add')} - Add a workshop`,
1023
+ value: 'add',
1024
+ description: 'Clone a workshop from epicweb-dev GitHub org',
1025
+ }, {
1026
+ name: `${chalk.green('remove')} - Remove a workshop`,
1027
+ value: 'remove',
1028
+ description: workshopTitle
1029
+ ? `Remove ${workshopTitle}`
1030
+ : 'Select a workshop to remove',
1031
+ }, {
1032
+ name: `${chalk.green('update')} - Update workshop`,
1033
+ value: 'update',
1034
+ description: workshopTitle
1035
+ ? `Update ${workshopTitle}`
1036
+ : 'Select a workshop to update',
1037
+ }, {
1038
+ name: `${chalk.green('warm')} - Warm caches`,
1039
+ value: 'warm',
1040
+ description: workshopTitle
1041
+ ? `Warm the cache for ${workshopTitle}`
1042
+ : 'Select a workshop to warm the cache for',
1043
+ }, {
1044
+ name: `${chalk.green('config')} - View/update configuration`,
1045
+ value: 'config',
1046
+ description: 'View or update workshop configuration',
1047
+ }, {
1048
+ name: `${chalk.green('init')} - First-time setup`,
1049
+ value: 'init',
1050
+ description: 'Initialize epicshop and start the tutorial',
1051
+ }, {
1052
+ name: `${chalk.green('auth')} - Manage authentication`,
1053
+ value: 'auth',
1054
+ description: 'Login/logout from EpicWeb.dev, EpicReact.dev, EpicAI.pro',
1055
+ }, {
1056
+ name: `${chalk.green('help')} - Show help`,
1057
+ value: 'help',
1058
+ description: 'Display CLI help information',
1059
+ });
744
1060
  const subcommand = await search({
745
1061
  message: workshopTitle
746
1062
  ? `What would you like to do? ${chalk.gray(`(in ${workshopTitle})`)}`
@@ -1006,6 +1322,86 @@ try {
1006
1322
  process.exit(1);
1007
1323
  break;
1008
1324
  }
1325
+ case 'exercises': {
1326
+ const { findWorkshopRoot } = await import("./commands/workshops.js");
1327
+ const wsRoot = await findWorkshopRoot();
1328
+ if (!wsRoot) {
1329
+ console.error(chalk.red('❌ Not inside a workshop directory'));
1330
+ process.exit(1);
1331
+ }
1332
+ const originalCwd = process.cwd();
1333
+ process.chdir(wsRoot);
1334
+ try {
1335
+ const { list: listExercises } = await import("./commands/exercises.js");
1336
+ const result = await listExercises({});
1337
+ if (!result.success)
1338
+ process.exit(1);
1339
+ }
1340
+ finally {
1341
+ process.chdir(originalCwd);
1342
+ }
1343
+ break;
1344
+ }
1345
+ case 'playground': {
1346
+ const { findWorkshopRoot } = await import("./commands/workshops.js");
1347
+ const wsRoot = await findWorkshopRoot();
1348
+ if (!wsRoot) {
1349
+ console.error(chalk.red('❌ Not inside a workshop directory'));
1350
+ process.exit(1);
1351
+ }
1352
+ const originalCwd = process.cwd();
1353
+ process.chdir(wsRoot);
1354
+ try {
1355
+ const { show: showPlayground } = await import("./commands/playground.js");
1356
+ const result = await showPlayground({});
1357
+ if (!result.success)
1358
+ process.exit(1);
1359
+ }
1360
+ finally {
1361
+ process.chdir(originalCwd);
1362
+ }
1363
+ break;
1364
+ }
1365
+ case 'progress': {
1366
+ const { findWorkshopRoot } = await import("./commands/workshops.js");
1367
+ const wsRoot = await findWorkshopRoot();
1368
+ if (!wsRoot) {
1369
+ console.error(chalk.red('❌ Not inside a workshop directory'));
1370
+ process.exit(1);
1371
+ }
1372
+ const originalCwd = process.cwd();
1373
+ process.chdir(wsRoot);
1374
+ try {
1375
+ const { show: showProgress } = await import("./commands/progress.js");
1376
+ const result = await showProgress({});
1377
+ if (!result.success)
1378
+ process.exit(1);
1379
+ }
1380
+ finally {
1381
+ process.chdir(originalCwd);
1382
+ }
1383
+ break;
1384
+ }
1385
+ case 'diff': {
1386
+ const { findWorkshopRoot } = await import("./commands/workshops.js");
1387
+ const wsRoot = await findWorkshopRoot();
1388
+ if (!wsRoot) {
1389
+ console.error(chalk.red('❌ Not inside a workshop directory'));
1390
+ process.exit(1);
1391
+ }
1392
+ const originalCwd = process.cwd();
1393
+ process.chdir(wsRoot);
1394
+ try {
1395
+ const { showProgressDiff } = await import("./commands/diff.js");
1396
+ const result = await showProgressDiff({});
1397
+ if (!result.success)
1398
+ process.exit(1);
1399
+ }
1400
+ finally {
1401
+ process.chdir(originalCwd);
1402
+ }
1403
+ break;
1404
+ }
1009
1405
  case 'help': {
1010
1406
  cli.showHelp((helpText) => {
1011
1407
  console.log(formatHelp(helpText));
@@ -0,0 +1,21 @@
1
+ export type DiffResult = {
2
+ success: boolean;
3
+ message?: string;
4
+ diff?: string;
5
+ error?: Error;
6
+ };
7
+ export type DiffOptions = {
8
+ app1?: string;
9
+ app2?: string;
10
+ silent?: boolean;
11
+ };
12
+ /**
13
+ * Show diff between the current playground and its solution
14
+ */
15
+ export declare function showProgressDiff(options?: {
16
+ silent?: boolean;
17
+ }): Promise<DiffResult>;
18
+ /**
19
+ * Show diff between two specific apps
20
+ */
21
+ export declare function showDiffBetweenApps(options?: DiffOptions): Promise<DiffResult>;