@epic-web/workshop-utils 6.71.4 → 6.72.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2101,7 +2101,7 @@ export declare function getWorkshopInstructions({ request, }?: {
2101
2101
  readonly status: "success";
2102
2102
  readonly code: string;
2103
2103
  readonly title: string | null;
2104
- readonly epicVideoEmbeds: Array<string>;
2104
+ readonly epicVideoEmbeds: string[];
2105
2105
  } | {
2106
2106
  readonly status: "error";
2107
2107
  readonly error: string;
@@ -2116,7 +2116,7 @@ export declare function getExtrasInstructions({ request, }?: {
2116
2116
  readonly status: "success";
2117
2117
  readonly code: string;
2118
2118
  readonly title: string | null;
2119
- readonly epicVideoEmbeds: Array<string>;
2119
+ readonly epicVideoEmbeds: string[];
2120
2120
  } | {
2121
2121
  readonly status: "error";
2122
2122
  readonly error: string;
@@ -2131,7 +2131,7 @@ export declare function getWorkshopFinished({ request, }?: {
2131
2131
  readonly status: "success";
2132
2132
  readonly code: string;
2133
2133
  readonly title: string | null;
2134
- readonly epicVideoEmbeds: Array<string>;
2134
+ readonly epicVideoEmbeds: string[];
2135
2135
  } | {
2136
2136
  readonly status: "error";
2137
2137
  readonly error: string;
@@ -217,6 +217,7 @@ async function isDirectoryEmpty(dirPath) {
217
217
  cache: directoryEmptyCache,
218
218
  ttl: 1000 * 60 * 5,
219
219
  swr: 1000 * 60 * 20,
220
+ checkValue: z.boolean(),
220
221
  forceFresh: await getForceFreshForDir(directoryEmptyCache.get(dirPath), dirPath),
221
222
  getFreshValue: async () => {
222
223
  try {
@@ -767,6 +768,7 @@ export async function getPlaygroundApp({ timings, request, } = {}) {
767
768
  cache: playgroundAppCache,
768
769
  ttl: 1000 * 60 * 5,
769
770
  swr: 1000 * 60 * 60 * 24 * 30,
771
+ checkValue: PlaygroundAppSchema.nullable(),
770
772
  timings,
771
773
  timingKey: playgroundDir.replace(`${playgroundDir}${path.sep}`, ''),
772
774
  request,
@@ -892,6 +894,7 @@ async function getExtraApps({ timings, request, } = {}) {
892
894
  cache: extraAppCache,
893
895
  ttl: 1000 * 60 * 5,
894
896
  swr: 1000 * 60 * 60 * 24 * 30,
897
+ checkValue: ExtraAppSchema.nullable(),
895
898
  timings,
896
899
  timingKey: extraDir.replace(`${extraDirInfo.fullPath}${path.sep}`, ''),
897
900
  request,
@@ -964,6 +967,7 @@ async function getSolutionApps({ timings, request, } = {}) {
964
967
  request,
965
968
  ttl: 1000 * 60 * 5,
966
969
  swr: 1000 * 60 * 60 * 24 * 30,
970
+ checkValue: SolutionAppSchema.nullable(),
967
971
  forceFresh: await getForceFreshForDir(solutionAppCache.get(solutionDir), solutionDir),
968
972
  getFreshValue: async () => {
969
973
  return getSolutionAppFromPath(solutionDir, request).catch((error) => {
@@ -1034,6 +1038,7 @@ async function getProblemApps({ timings, request, } = {}) {
1034
1038
  request,
1035
1039
  ttl: 1000 * 60 * 5,
1036
1040
  swr: 1000 * 60 * 60 * 24 * 30,
1041
+ checkValue: ProblemAppSchema.nullable(),
1037
1042
  forceFresh: await getForceFreshForDir(problemAppCache.get(problemDir), problemDir, solutionDir),
1038
1043
  getFreshValue: async () => {
1039
1044
  return getProblemAppFromPath(problemDir).catch((error) => {
@@ -2,6 +2,18 @@ import * as C from '@epic-web/cachified';
2
2
  import { type CreateReporter } from '@epic-web/cachified';
3
3
  import z from 'zod';
4
4
  import { type Timings } from "./timing.server.js";
5
+ type DiffStatus = 'renamed' | 'modified' | 'deleted' | 'added' | 'unknown';
6
+ type DiffFile = {
7
+ status: DiffStatus;
8
+ path: string;
9
+ line: number;
10
+ };
11
+ type CompiledCodeResult = {
12
+ outputFiles?: Array<unknown>;
13
+ errors: Array<unknown>;
14
+ warnings: Array<unknown>;
15
+ };
16
+ type OgCacheValue = string | Uint8Array;
5
17
  /**
6
18
  * Creates a cachified reporter that integrates with the Epic Workshop logger system.
7
19
  * Uses the pattern `epic:cache:{name-of-cache}` for logger namespaces.
@@ -156,24 +168,24 @@ export declare const playgroundAppCache: C.Cache<{
156
168
  epicVideoEmbeds?: string[] | undefined;
157
169
  }>;
158
170
  export declare const diffCodeCache: C.Cache<string>;
159
- export declare const diffFilesCache: C.Cache<string>;
171
+ export declare const diffFilesCache: C.Cache<DiffFile[]>;
160
172
  export declare const copyUnignoredFilesCache: {
161
173
  name: string;
162
- set: (key: string, value: C.CacheEntry<string>) => C.CacheEntry<string>;
163
- get: (key: string) => C.CacheEntry<string> | undefined;
174
+ set: (key: string, value: C.CacheEntry<boolean>) => C.CacheEntry<boolean>;
175
+ get: (key: string) => C.CacheEntry<boolean> | undefined;
164
176
  delete: (key: string) => boolean;
165
177
  };
166
178
  export declare const compiledMarkdownCache: C.Cache<string>;
167
179
  export declare const compiledCodeCache: {
168
180
  name: string;
169
- set: (key: string, value: C.CacheEntry<string>) => C.CacheEntry<string>;
170
- get: (key: string) => C.CacheEntry<string> | undefined;
181
+ set: (key: string, value: C.CacheEntry<CompiledCodeResult>) => C.CacheEntry<CompiledCodeResult>;
182
+ get: (key: string) => C.CacheEntry<CompiledCodeResult> | undefined;
171
183
  delete: (key: string) => boolean;
172
184
  };
173
185
  export declare const ogCache: {
174
186
  name: string;
175
- set: (key: string, value: C.CacheEntry<string>) => C.CacheEntry<string>;
176
- get: (key: string) => C.CacheEntry<string> | undefined;
187
+ set: (key: string, value: C.CacheEntry<OgCacheValue>) => C.CacheEntry<OgCacheValue>;
188
+ get: (key: string) => C.CacheEntry<OgCacheValue> | undefined;
177
189
  delete: (key: string) => boolean;
178
190
  };
179
191
  export declare const compiledInstructionMarkdownCache: C.Cache<{
@@ -197,20 +209,38 @@ export declare const checkForUpdatesCache: {
197
209
  name: string;
198
210
  set: (key: string, value: C.CacheEntry<{
199
211
  updatesAvailable: boolean;
200
- localCommit: string;
201
- remoteCommit: string;
212
+ repoUpdatesAvailable: boolean;
213
+ dependenciesNeedInstall: boolean;
214
+ updateNotificationId: string | null;
215
+ commitsAhead: number | null;
216
+ commitsBehind: number | null;
217
+ localCommit: string | null;
218
+ remoteCommit: string | null;
202
219
  diffLink: string | null;
220
+ message: string | null;
203
221
  }>) => C.CacheEntry<{
204
222
  updatesAvailable: boolean;
205
- localCommit: string;
206
- remoteCommit: string;
223
+ repoUpdatesAvailable: boolean;
224
+ dependenciesNeedInstall: boolean;
225
+ updateNotificationId: string | null;
226
+ commitsAhead: number | null;
227
+ commitsBehind: number | null;
228
+ localCommit: string | null;
229
+ remoteCommit: string | null;
207
230
  diffLink: string | null;
231
+ message: string | null;
208
232
  }>;
209
233
  get: (key: string) => C.CacheEntry<{
210
234
  updatesAvailable: boolean;
211
- localCommit: string;
212
- remoteCommit: string;
235
+ repoUpdatesAvailable: boolean;
236
+ dependenciesNeedInstall: boolean;
237
+ updateNotificationId: string | null;
238
+ commitsAhead: number | null;
239
+ commitsBehind: number | null;
240
+ localCommit: string | null;
241
+ remoteCommit: string | null;
213
242
  diffLink: string | null;
243
+ message: string | null;
214
244
  }> | undefined;
215
245
  delete: (key: string) => boolean;
216
246
  };
@@ -412,3 +442,4 @@ export declare function shouldForceFresh({ forceFresh, request, key, }: {
412
442
  request?: Request;
413
443
  key?: string;
414
444
  }): Promise<boolean>;
445
+ export {};
@@ -7,6 +7,6 @@ export declare function compileMdx(file: string, { request, timings, forceFresh,
7
7
  }): Promise<{
8
8
  code: string;
9
9
  title: string | null;
10
- epicVideoEmbeds: Array<string>;
10
+ epicVideoEmbeds: string[];
11
11
  }>;
12
12
  export declare function compileMarkdownString(markdownString: string): Promise<string>;
@@ -11,10 +11,16 @@ import rehypeAutolinkHeadings from 'rehype-autolink-headings';
11
11
  import emoji from 'remark-emoji';
12
12
  import gfm from 'remark-gfm';
13
13
  import { visit } from 'unist-util-visit';
14
+ import { z } from 'zod';
14
15
  import { cachified, compiledInstructionMarkdownCache, compiledMarkdownCache, shouldForceFresh, } from "./cache.server.js";
15
16
  import { checkConnection } from "./utils.server.js";
16
17
  const themeCookieName = 'EpicShop_theme';
17
18
  const themeHintCookieName = 'EpicShop_CH-prefers-color-scheme';
19
+ const CompiledInstructionMarkdownSchema = z.object({
20
+ code: z.string(),
21
+ title: z.string().nullable(),
22
+ epicVideoEmbeds: z.array(z.string()),
23
+ });
18
24
  function getMermaidTheme(request) {
19
25
  if (!request)
20
26
  return 'default';
@@ -182,6 +188,7 @@ export async function compileMdx(file, { request, timings, forceFresh, } = {}) {
182
188
  request,
183
189
  timings,
184
190
  forceFresh,
191
+ checkValue: CompiledInstructionMarkdownSchema,
185
192
  getFreshValue: () => compileMdxImpl(file, { mermaidTheme }),
186
193
  });
187
194
  }
@@ -277,6 +284,7 @@ export async function compileMarkdownString(markdownString) {
277
284
  key: md5(markdownString),
278
285
  cache: compiledMarkdownCache,
279
286
  ttl: 1000 * 60 * 60 * 24,
287
+ checkValue: z.string(),
280
288
  getFreshValue: async () => {
281
289
  try {
282
290
  verboseLog(`Compiling string`, markdownString);
@@ -5,7 +5,7 @@ export declare function getDiffFiles(app1: App, app2: App, { forceFresh, timings
5
5
  timings?: Timings;
6
6
  request?: Request;
7
7
  }): Promise<{
8
- status: "renamed" | "modified" | "deleted" | "added" | "unknown";
8
+ status: "unknown" | "renamed" | "modified" | "deleted" | "added";
9
9
  path: string;
10
10
  line: number;
11
11
  }[]>;
@@ -7,6 +7,7 @@ import fsExtra from 'fs-extra';
7
7
  import ignore from 'ignore';
8
8
  import parseGitDiff from 'parse-git-diff';
9
9
  import { bundledLanguagesInfo } from 'shiki/langs';
10
+ import { z } from 'zod';
10
11
  import { getForceFreshForDir, getRelativePath, getWorkshopRoot, modifiedTimes, } from "./apps.server.js";
11
12
  import { cachified, copyUnignoredFilesCache, diffCodeCache, diffFilesCache, } from "./cache.server.js";
12
13
  import { compileMarkdownString } from "./compile-mdx.server.js";
@@ -14,6 +15,37 @@ import { modifiedMoreRecentlyThan } from "./modified-time.server.js";
14
15
  const epicshopTempDir = path.join(os.tmpdir(), 'epicshop');
15
16
  const isDeployed = getEnv().EPICSHOP_DEPLOYED;
16
17
  const diffTmpDir = path.join(epicshopTempDir, 'diff');
18
+ const DiffStatusSchema = z.enum([
19
+ 'renamed',
20
+ 'modified',
21
+ 'deleted',
22
+ 'added',
23
+ 'unknown',
24
+ ]);
25
+ const DiffFileSchema = z.object({
26
+ status: DiffStatusSchema,
27
+ path: z.string(),
28
+ line: z.number(),
29
+ });
30
+ function getDiffStatus(fileType) {
31
+ switch (fileType) {
32
+ case 'ChangedFile': {
33
+ return 'modified';
34
+ }
35
+ case 'AddedFile': {
36
+ return 'added';
37
+ }
38
+ case 'DeletedFile': {
39
+ return 'deleted';
40
+ }
41
+ case 'RenamedFile': {
42
+ return 'renamed';
43
+ }
44
+ default: {
45
+ return 'unknown';
46
+ }
47
+ }
48
+ }
17
49
  /**
18
50
  * Converts a diff file path to a relative path for display and lookup.
19
51
  * - Removes leading/trailing quotes.
@@ -165,6 +197,7 @@ async function copyUnignoredFiles(srcDir, destDir, ignoreList) {
165
197
  await cachified({
166
198
  key,
167
199
  cache: copyUnignoredFilesCache,
200
+ checkValue: z.boolean(),
168
201
  forceFresh: await getForceFreshForDir(copyUnignoredFilesCache.get(key), srcDir),
169
202
  async getFreshValue() {
170
203
  // @ts-ignore 🤷‍♂️ weird module stuff
@@ -177,6 +210,7 @@ async function copyUnignoredFiles(srcDir, destDir, ignoreList) {
177
210
  return !ig.ignores(path.relative(srcDir, file));
178
211
  },
179
212
  });
213
+ return true;
180
214
  },
181
215
  });
182
216
  }
@@ -266,6 +300,7 @@ export async function getDiffFiles(app1, app2, { forceFresh, timings, request, }
266
300
  forceFresh: forceFresh || (await getForceFreshForDiff(app1, app2, cacheEntry)),
267
301
  timings,
268
302
  request,
303
+ checkValue: DiffFileSchema.array(),
269
304
  getFreshValue: () => getDiffFilesImpl(app1, app2),
270
305
  });
271
306
  return result;
@@ -288,12 +323,6 @@ async function getDiffFilesImpl(app1, app2) {
288
323
  ], { cwd: diffTmpDir }).catch((e) => e);
289
324
  void fsExtra.remove(app1CopyPath).catch(() => { });
290
325
  void fsExtra.remove(app2CopyPath).catch(() => { });
291
- const typesMap = {
292
- ChangedFile: 'modified',
293
- AddedFile: 'added',
294
- DeletedFile: 'deleted',
295
- RenamedFile: 'renamed',
296
- };
297
326
  const parsed = parseGitDiff(diffOutput, { noPrefix: true });
298
327
  const testFiles = Array.from(new Set([...getAppTestFiles(app1), ...getAppTestFiles(app2)]));
299
328
  const startLine = (file) => {
@@ -309,8 +338,7 @@ async function getDiffFilesImpl(app1, app2) {
309
338
  };
310
339
  return parsed.files
311
340
  .map((file) => ({
312
- // prettier-ignore
313
- status: (typesMap[file.type] ?? 'unknown'),
341
+ status: getDiffStatus(file.type),
314
342
  path: diffPathToRelative(file.type === 'RenamedFile' ? file.pathBefore : file.path),
315
343
  line: startLine(file),
316
344
  }))
@@ -325,6 +353,7 @@ export async function getDiffCode(app1, app2, { forceFresh, timings, request, }
325
353
  forceFresh: forceFresh || (await getForceFreshForDiff(app1, app2, cacheEntry)),
326
354
  timings,
327
355
  request,
356
+ checkValue: z.string(),
328
357
  getFreshValue: () => getDiffCodeImpl(app1, app2),
329
358
  });
330
359
  return result;
@@ -1,38 +1,53 @@
1
1
  import "./init-env.js";
2
2
  export declare function checkForUpdates(): Promise<{
3
3
  readonly updatesAvailable: false;
4
+ readonly dependenciesNeedInstall: false;
5
+ readonly updateNotificationId: null;
4
6
  readonly message: "The app is deployed";
5
- readonly commitsAhead?: undefined;
6
- readonly commitsBehind?: undefined;
7
- readonly localCommit?: undefined;
8
- readonly remoteCommit?: undefined;
9
- readonly diffLink?: undefined;
7
+ readonly repoUpdatesAvailable: boolean;
8
+ readonly commitsAhead: null;
9
+ readonly commitsBehind: null;
10
+ readonly localCommit: null;
11
+ readonly remoteCommit: null;
12
+ readonly diffLink: null;
10
13
  } | {
11
- readonly updatesAvailable: false;
12
14
  readonly message: "You are offline";
13
- readonly commitsAhead?: undefined;
14
- readonly commitsBehind?: undefined;
15
- readonly localCommit?: undefined;
16
- readonly remoteCommit?: undefined;
17
- readonly diffLink?: undefined;
15
+ readonly updatesAvailable: boolean;
16
+ readonly repoUpdatesAvailable: boolean;
17
+ readonly dependenciesNeedInstall: boolean;
18
+ readonly updateNotificationId: string | null;
19
+ readonly commitsAhead: null;
20
+ readonly commitsBehind: null;
21
+ readonly localCommit: null;
22
+ readonly remoteCommit: null;
23
+ readonly diffLink: null;
18
24
  } | {
19
- readonly updatesAvailable: false;
20
25
  readonly message: "Not in a git repo";
21
- readonly commitsAhead?: undefined;
22
- readonly commitsBehind?: undefined;
23
- readonly localCommit?: undefined;
24
- readonly remoteCommit?: undefined;
25
- readonly diffLink?: undefined;
26
+ readonly updatesAvailable: boolean;
27
+ readonly repoUpdatesAvailable: boolean;
28
+ readonly dependenciesNeedInstall: boolean;
29
+ readonly updateNotificationId: string | null;
30
+ readonly commitsAhead: null;
31
+ readonly commitsBehind: null;
32
+ readonly localCommit: null;
33
+ readonly remoteCommit: null;
34
+ readonly diffLink: null;
26
35
  } | {
27
- readonly updatesAvailable: false;
28
36
  readonly message: "Cannot find remote";
29
- readonly commitsAhead?: undefined;
30
- readonly commitsBehind?: undefined;
31
- readonly localCommit?: undefined;
32
- readonly remoteCommit?: undefined;
33
- readonly diffLink?: undefined;
37
+ readonly updatesAvailable: boolean;
38
+ readonly repoUpdatesAvailable: boolean;
39
+ readonly dependenciesNeedInstall: boolean;
40
+ readonly updateNotificationId: string | null;
41
+ readonly commitsAhead: null;
42
+ readonly commitsBehind: null;
43
+ readonly localCommit: null;
44
+ readonly remoteCommit: null;
45
+ readonly diffLink: null;
34
46
  } | {
35
47
  readonly updatesAvailable: boolean;
48
+ readonly repoUpdatesAvailable: boolean;
49
+ readonly dependenciesNeedInstall: boolean;
50
+ readonly updateNotificationId: string | null;
36
51
  readonly commitsAhead: number;
37
52
  readonly commitsBehind: number;
38
53
  readonly localCommit: string;
@@ -40,64 +55,39 @@ export declare function checkForUpdates(): Promise<{
40
55
  readonly diffLink: string | null;
41
56
  readonly message: null;
42
57
  } | {
43
- readonly updatesAvailable: false;
44
- readonly localCommit: string | undefined;
45
- readonly remoteCommit: string | undefined;
58
+ readonly localCommit: string | null;
59
+ readonly remoteCommit: string | null;
46
60
  readonly diffLink: string | null;
47
- readonly message?: undefined;
48
- readonly commitsAhead?: undefined;
49
- readonly commitsBehind?: undefined;
50
- }>;
51
- export declare function checkForUpdatesCached(): Promise<{
52
- readonly updatesAvailable: false;
53
- readonly message: "The app is deployed";
54
- readonly commitsAhead?: undefined;
55
- readonly commitsBehind?: undefined;
56
- readonly localCommit?: undefined;
57
- readonly remoteCommit?: undefined;
58
- readonly diffLink?: undefined;
59
- } | {
60
- readonly updatesAvailable: false;
61
- readonly message: "You are offline";
62
- readonly commitsAhead?: undefined;
63
- readonly commitsBehind?: undefined;
64
- readonly localCommit?: undefined;
65
- readonly remoteCommit?: undefined;
66
- readonly diffLink?: undefined;
67
- } | {
68
- readonly updatesAvailable: false;
69
- readonly message: "Not in a git repo";
70
- readonly commitsAhead?: undefined;
71
- readonly commitsBehind?: undefined;
72
- readonly localCommit?: undefined;
73
- readonly remoteCommit?: undefined;
74
- readonly diffLink?: undefined;
75
- } | {
76
- readonly updatesAvailable: false;
77
- readonly message: "Cannot find remote";
78
- readonly commitsAhead?: undefined;
79
- readonly commitsBehind?: undefined;
80
- readonly localCommit?: undefined;
81
- readonly remoteCommit?: undefined;
82
- readonly diffLink?: undefined;
83
- } | {
84
61
  readonly updatesAvailable: boolean;
85
- readonly commitsAhead: number;
86
- readonly commitsBehind: number;
87
- readonly localCommit: string;
88
- readonly remoteCommit: string;
89
- readonly diffLink: string | null;
62
+ readonly repoUpdatesAvailable: boolean;
63
+ readonly dependenciesNeedInstall: boolean;
64
+ readonly updateNotificationId: string | null;
65
+ readonly commitsAhead: null;
66
+ readonly commitsBehind: null;
90
67
  readonly message: null;
68
+ }>;
69
+ export declare function checkForUpdatesCached(): Promise<{
70
+ updatesAvailable: boolean;
71
+ repoUpdatesAvailable: boolean;
72
+ dependenciesNeedInstall: boolean;
73
+ updateNotificationId: string | null;
74
+ commitsAhead: number | null;
75
+ commitsBehind: number | null;
76
+ localCommit: string | null;
77
+ remoteCommit: string | null;
78
+ diffLink: string | null;
79
+ message: string | null;
91
80
  } | {
92
81
  readonly updatesAvailable: false;
93
- readonly localCommit: string | undefined;
94
- readonly remoteCommit: string | undefined;
95
- readonly diffLink: string | null;
96
- readonly message?: undefined;
97
- readonly commitsAhead?: undefined;
98
- readonly commitsBehind?: undefined;
99
- } | {
100
- readonly updatesAvailable: false;
82
+ readonly repoUpdatesAvailable: false;
83
+ readonly dependenciesNeedInstall: false;
84
+ readonly updateNotificationId: null;
85
+ readonly commitsAhead: null;
86
+ readonly commitsBehind: null;
87
+ readonly localCommit: null;
88
+ readonly remoteCommit: null;
89
+ readonly diffLink: null;
90
+ readonly message: "The app is deployed";
101
91
  }>;
102
92
  export declare function updateLocalRepo(): Promise<{
103
93
  readonly status: "success";
@@ -107,7 +97,7 @@ export declare function updateLocalRepo(): Promise<{
107
97
  readonly message: string;
108
98
  } | {
109
99
  readonly status: "success";
110
- readonly message: "Updated successfully.";
100
+ readonly message: "Updated successfully." | "Dependencies updated successfully.";
111
101
  }>;
112
102
  export declare function getCommitInfo(): Promise<{
113
103
  hash: string;
@@ -2,20 +2,39 @@ import "./init-env.js";
2
2
  import fs from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import { execa, execaCommand } from 'execa';
5
+ import { z } from 'zod';
5
6
  import { getWorkshopRoot } from "./apps.server.js";
6
7
  import { cachified, checkForUpdatesCache } from "./cache.server.js";
7
8
  import { getWorkshopConfig } from "./config.server.js";
8
9
  import { getEnv } from "./env.server.js";
9
10
  import { logger } from "./logger.js";
11
+ import { getInstallCommand, getWorkspaceInstallStatus, } from "./package-install/package-install-check.server.js";
10
12
  import { checkConnection } from "./utils.server.js";
11
13
  import { getErrorMessage } from "./utils.js";
12
14
  const gitLog = logger('epic:git');
15
+ const CheckForUpdatesSchema = z.object({
16
+ updatesAvailable: z.boolean(),
17
+ repoUpdatesAvailable: z.boolean(),
18
+ dependenciesNeedInstall: z.boolean(),
19
+ updateNotificationId: z.string().nullable(),
20
+ commitsAhead: z.number().nullable(),
21
+ commitsBehind: z.number().nullable(),
22
+ localCommit: z.string().nullable(),
23
+ remoteCommit: z.string().nullable(),
24
+ diffLink: z.string().nullable(),
25
+ message: z.string().nullable(),
26
+ });
13
27
  function dirHasTrackedFiles(cwd, dirPath) {
14
28
  return execa('git', ['ls-files', dirPath], { cwd }).then((s) => s.stdout.trim().length > 0, () => true);
15
29
  }
16
30
  function isDirectory(dirPath) {
17
31
  return fs.stat(dirPath).then((s) => s.isDirectory(), () => false);
18
32
  }
33
+ function getDependencyNotificationId(dependencyHash) {
34
+ if (!dependencyHash)
35
+ return null;
36
+ return `update-deps-${dependencyHash}`;
37
+ }
19
38
  async function cleanupEmptyExerciseDirectories(cwd) {
20
39
  console.log('🧹 Cleaning up empty exercise directories...');
21
40
  try {
@@ -62,25 +81,48 @@ async function getDiffUrl(commitBefore, commitAfter) {
62
81
  }
63
82
  export async function checkForUpdates() {
64
83
  const ENV = getEnv();
84
+ const cwd = getWorkshopRoot();
85
+ const dependencyStatus = await getWorkspaceInstallStatus(cwd);
86
+ const dependencyNotificationId = dependencyStatus.dependenciesNeedInstall
87
+ ? getDependencyNotificationId(dependencyStatus.dependencyHash)
88
+ : null;
89
+ const baseResult = {
90
+ updatesAvailable: dependencyStatus.dependenciesNeedInstall,
91
+ repoUpdatesAvailable: false,
92
+ dependenciesNeedInstall: dependencyStatus.dependenciesNeedInstall,
93
+ updateNotificationId: dependencyNotificationId,
94
+ commitsAhead: null,
95
+ commitsBehind: null,
96
+ localCommit: null,
97
+ remoteCommit: null,
98
+ diffLink: null,
99
+ message: null,
100
+ };
65
101
  if (ENV.EPICSHOP_DEPLOYED) {
66
- return { updatesAvailable: false, message: 'The app is deployed' };
102
+ return {
103
+ ...baseResult,
104
+ updatesAvailable: false,
105
+ dependenciesNeedInstall: false,
106
+ updateNotificationId: null,
107
+ message: 'The app is deployed',
108
+ };
67
109
  }
68
- const cwd = getWorkshopRoot();
69
110
  const online = await checkConnection();
70
111
  if (!online) {
71
- return { updatesAvailable: false, message: 'You are offline' };
112
+ return { ...baseResult, message: 'You are offline' };
72
113
  }
73
114
  const isInRepo = await execaCommand('git rev-parse --is-inside-work-tree', {
74
115
  cwd,
75
116
  }).then(() => true, () => false);
76
117
  if (!isInRepo) {
77
- return { updatesAvailable: false, message: 'Not in a git repo' };
118
+ return { ...baseResult, message: 'Not in a git repo' };
78
119
  }
79
120
  const { stdout: remote } = await execaCommand('git remote', { cwd });
80
121
  if (!remote) {
81
- return { updatesAvailable: false, message: 'Cannot find remote' };
122
+ return { ...baseResult, message: 'Cannot find remote' };
82
123
  }
83
- let localCommit, remoteCommit;
124
+ let localCommit = null;
125
+ let remoteCommit = null;
84
126
  try {
85
127
  const currentBranch = (await execaCommand('git rev-parse --abbrev-ref HEAD', { cwd })).stdout.trim();
86
128
  localCommit = (await execaCommand('git rev-parse --short HEAD', { cwd })).stdout.trim();
@@ -90,21 +132,28 @@ export async function checkForUpdates() {
90
132
  })).stdout.trim();
91
133
  const { stdout } = await execa('git', ['rev-list', '--count', '--left-right', 'HEAD...@{upstream}'], { cwd });
92
134
  const [ahead = 0, behind = 0] = stdout.trim().split(/\s+/).map(Number);
93
- const updatesAvailable = behind > 0;
135
+ const repoUpdatesAvailable = behind > 0;
136
+ const updatesAvailable = repoUpdatesAvailable || dependencyStatus.dependenciesNeedInstall;
137
+ const updateNotificationId = repoUpdatesAvailable
138
+ ? `update-repo-${remoteCommit}`
139
+ : dependencyNotificationId;
94
140
  return {
95
141
  updatesAvailable,
142
+ repoUpdatesAvailable,
143
+ dependenciesNeedInstall: dependencyStatus.dependenciesNeedInstall,
144
+ updateNotificationId,
96
145
  commitsAhead: ahead,
97
146
  commitsBehind: behind,
98
147
  localCommit,
99
148
  remoteCommit,
100
149
  diffLink: await getDiffUrl(localCommit, remoteCommit),
101
- message: null,
150
+ message: baseResult.message,
102
151
  };
103
152
  }
104
153
  catch (error) {
105
154
  console.error('Unable to check for updates', getErrorMessage(error));
106
155
  return {
107
- updatesAvailable: false,
156
+ ...baseResult,
108
157
  localCommit,
109
158
  remoteCommit,
110
159
  diffLink: localCommit && remoteCommit
@@ -116,21 +165,33 @@ export async function checkForUpdates() {
116
165
  export async function checkForUpdatesCached() {
117
166
  const ENV = getEnv();
118
167
  if (ENV.EPICSHOP_DEPLOYED) {
119
- return { updatesAvailable: false };
168
+ return {
169
+ updatesAvailable: false,
170
+ repoUpdatesAvailable: false,
171
+ dependenciesNeedInstall: false,
172
+ updateNotificationId: null,
173
+ commitsAhead: null,
174
+ commitsBehind: null,
175
+ localCommit: null,
176
+ remoteCommit: null,
177
+ diffLink: null,
178
+ message: 'The app is deployed',
179
+ };
120
180
  }
121
181
  const key = 'checkForUpdates';
122
182
  return cachified({
123
183
  ttl: 1000 * 60,
124
184
  swr: 1000 * 60 * 60 * 24,
125
185
  key,
186
+ checkValue: CheckForUpdatesSchema,
126
187
  getFreshValue: checkForUpdates,
127
188
  cache: checkForUpdatesCache,
128
189
  });
129
190
  }
130
- async function runNpmInstallWithRetry(cwd, maxRetries = 3, baseDelayMs = 1000) {
191
+ async function runInstallWithRetry(cwd, command, args, maxRetries = 3, baseDelayMs = 1000) {
131
192
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
132
193
  try {
133
- await execaCommand('npm install', { cwd, stdio: 'inherit' });
194
+ await execa(command, args, { cwd, stdio: 'inherit' });
134
195
  return;
135
196
  }
136
197
  catch (error) {
@@ -160,49 +221,77 @@ export async function updateLocalRepo() {
160
221
  const cwd = getWorkshopRoot();
161
222
  try {
162
223
  const updates = await checkForUpdates();
163
- if (!updates.updatesAvailable) {
224
+ const repoUpdatesAvailable = updates.repoUpdatesAvailable;
225
+ let dependencyStatus = await getWorkspaceInstallStatus(cwd);
226
+ let rootsNeedingInstall = dependencyStatus.roots.filter((status) => status.dependenciesNeedInstall);
227
+ if (!repoUpdatesAvailable && rootsNeedingInstall.length === 0) {
164
228
  return {
165
229
  status: 'success',
166
230
  message: updates.message ?? 'No updates available.',
167
231
  };
168
232
  }
169
- const uncommittedChanges = (await execaCommand('git status --porcelain', { cwd })).stdout.trim()
170
- .length > 0;
171
- if (uncommittedChanges) {
172
- console.log('👜 Stashing uncommitted changes...');
173
- await execaCommand('git stash --include-untracked', { cwd });
233
+ let didPull = false;
234
+ let didInstall = false;
235
+ if (repoUpdatesAvailable) {
236
+ const uncommittedChanges = (await execaCommand('git status --porcelain', { cwd })).stdout.trim()
237
+ .length > 0;
238
+ if (uncommittedChanges) {
239
+ console.log('👜 Stashing uncommitted changes...');
240
+ await execaCommand('git stash --include-untracked', { cwd });
241
+ }
242
+ console.log('⬇️ Pulling latest changes...');
243
+ await execaCommand('git pull origin HEAD', { cwd });
244
+ if (uncommittedChanges) {
245
+ console.log('👜 re-applying stashed changes...');
246
+ await execaCommand('git stash pop', { cwd });
247
+ }
248
+ didPull = true;
249
+ dependencyStatus = await getWorkspaceInstallStatus(cwd);
250
+ rootsNeedingInstall = dependencyStatus.roots.filter((status) => status.dependenciesNeedInstall);
174
251
  }
175
- console.log('⬇️ Pulling latest changes...');
176
- await execaCommand('git pull origin HEAD', { cwd });
177
- if (uncommittedChanges) {
178
- console.log('👜 re-applying stashed changes...');
179
- await execaCommand('git stash pop', { cwd });
252
+ if (rootsNeedingInstall.length > 0) {
253
+ for (const root of rootsNeedingInstall) {
254
+ const rootLabel = path.relative(cwd, root.rootDir).replace(/\\/g, '/') || '.';
255
+ const { command, args } = getInstallCommand(root.packageManager);
256
+ const commandLabel = `${command} ${args.join(' ')}`.trim();
257
+ console.log(`📦 Installing dependencies in ${rootLabel} using ${commandLabel}...`);
258
+ try {
259
+ await runInstallWithRetry(root.rootDir, command, args);
260
+ didInstall = true;
261
+ }
262
+ catch (error) {
263
+ const isEbusy = error instanceof Error &&
264
+ (error.message.includes('EBUSY') ||
265
+ error.code === 'EBUSY');
266
+ if (isEbusy) {
267
+ return {
268
+ status: 'error',
269
+ message: `${commandLabel} failed: files are locked. ` +
270
+ 'Please close any editors or terminals using this directory, ' +
271
+ `then run: ${commandLabel}`,
272
+ };
273
+ }
274
+ throw error;
275
+ }
276
+ }
180
277
  }
181
- console.log('📦 Re-installing dependencies...');
182
- try {
183
- await runNpmInstallWithRetry(cwd);
278
+ else if (repoUpdatesAvailable) {
279
+ console.log('📦 Dependencies already match package.json. Skipping install.');
184
280
  }
185
- catch (error) {
186
- const isEbusy = error instanceof Error &&
187
- (error.message.includes('EBUSY') ||
188
- error.code === 'EBUSY');
189
- if (isEbusy) {
190
- return {
191
- status: 'error',
192
- message: 'npm install failed: files are locked. ' +
193
- 'Please close any editors or terminals using this directory, ' +
194
- 'then run: npm install',
195
- };
281
+ if (didPull || didInstall) {
282
+ await cleanupEmptyExerciseDirectories(cwd);
283
+ const postUpdateScript = getWorkshopConfig().scripts?.postupdate;
284
+ if (postUpdateScript) {
285
+ console.log('🏃 Running post update script...');
286
+ await execaCommand(postUpdateScript, { cwd, stdio: 'inherit' });
196
287
  }
197
- throw error;
198
288
  }
199
- await cleanupEmptyExerciseDirectories(cwd);
200
- const postUpdateScript = getWorkshopConfig().scripts?.postupdate;
201
- if (postUpdateScript) {
202
- console.log('🏃 Running post update script...');
203
- await execaCommand(postUpdateScript, { cwd, stdio: 'inherit' });
204
- }
205
- return { status: 'success', message: 'Updated successfully.' };
289
+ return {
290
+ status: 'success',
291
+ message: repoUpdatesAvailable
292
+ ? 'Updated successfully.'
293
+ : 'Dependencies updated successfully.',
294
+ };
206
295
  }
207
296
  catch (error) {
208
297
  return { status: 'error', message: getErrorMessage(error) };
@@ -3,6 +3,7 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { isGitIgnored } from 'globby';
5
5
  import PQueue from 'p-queue';
6
+ import { z } from 'zod';
6
7
  import { cachified, dirModifiedTimeCache } from "./cache.server.js";
7
8
  async function getDirModifiedTime(dir, { forceFresh = false } = {}) {
8
9
  const result = await cachified({
@@ -10,6 +11,7 @@ async function getDirModifiedTime(dir, { forceFresh = false } = {}) {
10
11
  cache: dirModifiedTimeCache,
11
12
  ttl: 200,
12
13
  forceFresh,
14
+ checkValue: z.number(),
13
15
  getFreshValue: () => getDirModifiedTimeImpl(dir),
14
16
  });
15
17
  return result;
@@ -22,6 +22,21 @@ const NotificationSchema = z.object({
22
22
  .nullable()
23
23
  .transform((val) => (val ? new Date(val) : null)),
24
24
  });
25
+ // Schema for validating cached notifications (post-transform, with Date objects)
26
+ const CachedNotificationSchema = z.object({
27
+ id: z.string(),
28
+ title: z.string(),
29
+ message: z.string(),
30
+ link: z.string().optional(),
31
+ type: z.enum(['info', 'warning', 'danger']),
32
+ products: z
33
+ .array(z.object({
34
+ host: z.string(),
35
+ slug: z.string().optional(),
36
+ }))
37
+ .optional(),
38
+ expiresAt: z.instanceof(Date).nullable(),
39
+ });
25
40
  async function getRemoteNotifications() {
26
41
  return cachified({
27
42
  key: 'notifications',
@@ -29,6 +44,7 @@ async function getRemoteNotifications() {
29
44
  ttl: 1000 * 60 * 60 * 6,
30
45
  swr: 1000 * 60 * 60 * 24,
31
46
  offlineFallbackValue: [],
47
+ checkValue: CachedNotificationSchema.array(),
32
48
  async getFreshValue() {
33
49
  const URL = 'https://gist.github.com/kentcdodds/c3aaa5141f591cdbb0e6bfcacd361f39';
34
50
  const filename = 'notifications.json5';
@@ -0,0 +1,23 @@
1
+ export type RootPackageInstallStatus = {
2
+ rootDir: string;
3
+ packageJsonPath: string;
4
+ packageManager: string | null;
5
+ dependencyHash: string | null;
6
+ dependenciesNeedInstall: boolean;
7
+ missingDependencies: Array<string>;
8
+ missingDevDependencies: Array<string>;
9
+ missingOptionalDependencies: Array<string>;
10
+ reason: 'missing-node-modules' | 'missing-dependencies' | 'package-json-unreadable' | 'up-to-date';
11
+ };
12
+ export type WorkspaceInstallStatus = {
13
+ roots: Array<RootPackageInstallStatus>;
14
+ dependenciesNeedInstall: boolean;
15
+ dependencyHash: string | null;
16
+ };
17
+ export declare function getRootPackageJsonPaths(cwd: string): Promise<string[]>;
18
+ export declare function getRootPackageInstallStatus(packageJsonPath: string): Promise<RootPackageInstallStatus>;
19
+ export declare function getWorkspaceInstallStatus(cwd: string): Promise<WorkspaceInstallStatus>;
20
+ export declare function getInstallCommand(packageManager: string | null): {
21
+ command: string;
22
+ args: string[];
23
+ };
@@ -0,0 +1,229 @@
1
+ import { createHash } from 'node:crypto';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { globby } from 'globby';
5
+ import { getErrorMessage } from "../utils.js";
6
+ const workspaceIgnorePatterns = [
7
+ '**/node_modules/**',
8
+ '**/.git/**',
9
+ '**/.cache/**',
10
+ '**/dist/**',
11
+ '**/build/**',
12
+ '**/coverage/**',
13
+ ];
14
+ function hashString(value) {
15
+ return createHash('sha256').update(value).digest('hex').slice(0, 8);
16
+ }
17
+ function normalizeDependencyMap(dependencies) {
18
+ const entries = Object.entries(dependencies ?? {}).sort(([a], [b]) => a.localeCompare(b));
19
+ return Object.fromEntries(entries);
20
+ }
21
+ function getDependencySnapshot(packageJson) {
22
+ return {
23
+ dependencies: normalizeDependencyMap(packageJson.dependencies),
24
+ devDependencies: normalizeDependencyMap(packageJson.devDependencies),
25
+ optionalDependencies: normalizeDependencyMap(packageJson.optionalDependencies),
26
+ };
27
+ }
28
+ function parsePackageManager(value) {
29
+ if (!value)
30
+ return null;
31
+ const [name] = value.split('@');
32
+ return name || null;
33
+ }
34
+ function normalizeWorkspacePattern(pattern) {
35
+ const trimmed = pattern.trim();
36
+ if (!trimmed)
37
+ return trimmed;
38
+ const isNegated = trimmed.startsWith('!');
39
+ const raw = isNegated ? trimmed.slice(1) : trimmed;
40
+ const normalized = raw.replace(/\\/g, '/');
41
+ const withPackageJson = normalized.endsWith('package.json')
42
+ ? normalized
43
+ : path.posix.join(normalized, 'package.json');
44
+ return isNegated ? `!${withPackageJson}` : withPackageJson;
45
+ }
46
+ async function readPackageJson(filePath) {
47
+ try {
48
+ const contents = await fs.readFile(filePath, 'utf8');
49
+ return JSON.parse(contents);
50
+ }
51
+ catch (error) {
52
+ console.warn(`⚠️ Failed to read package.json at ${filePath}:`, getErrorMessage(error));
53
+ return null;
54
+ }
55
+ }
56
+ async function getWorkspacePackageJsonPaths(packageJsonPath) {
57
+ const packageJson = await readPackageJson(packageJsonPath);
58
+ if (!packageJson)
59
+ return [];
60
+ const workspaces = Array.isArray(packageJson.workspaces)
61
+ ? packageJson.workspaces
62
+ : (packageJson.workspaces?.packages ?? []);
63
+ if (!workspaces.length)
64
+ return [];
65
+ const workspacePatterns = workspaces.map(normalizeWorkspacePattern);
66
+ return globby(workspacePatterns, {
67
+ cwd: path.dirname(packageJsonPath),
68
+ absolute: true,
69
+ ignore: workspaceIgnorePatterns,
70
+ });
71
+ }
72
+ async function listPackageJsonPaths(cwd) {
73
+ return globby('**/package.json', {
74
+ cwd,
75
+ absolute: true,
76
+ ignore: workspaceIgnorePatterns,
77
+ });
78
+ }
79
+ async function listInstalledPackages(nodeModulesPath) {
80
+ try {
81
+ const entries = await fs.readdir(nodeModulesPath, { withFileTypes: true });
82
+ const packages = new Set();
83
+ for (const entry of entries) {
84
+ if (entry.name.startsWith('.'))
85
+ continue;
86
+ if (entry.name.startsWith('@')) {
87
+ if (!entry.isDirectory())
88
+ continue;
89
+ const scopePath = path.join(nodeModulesPath, entry.name);
90
+ const scopeEntries = await fs.readdir(scopePath, {
91
+ withFileTypes: true,
92
+ });
93
+ for (const scopedEntry of scopeEntries) {
94
+ if (scopedEntry.name.startsWith('.'))
95
+ continue;
96
+ if (scopedEntry.isDirectory() || scopedEntry.isSymbolicLink()) {
97
+ packages.add(`${entry.name}/${scopedEntry.name}`);
98
+ }
99
+ }
100
+ continue;
101
+ }
102
+ if (entry.isDirectory() || entry.isSymbolicLink()) {
103
+ packages.add(entry.name);
104
+ }
105
+ }
106
+ return packages;
107
+ }
108
+ catch {
109
+ return null;
110
+ }
111
+ }
112
+ function getExpectedDependencies(dependencies) {
113
+ return Object.keys(dependencies ?? {}).sort();
114
+ }
115
+ export async function getRootPackageJsonPaths(cwd) {
116
+ const allPackageJsonPaths = await listPackageJsonPaths(cwd);
117
+ const workspacePackageJsonPaths = new Set();
118
+ for (const packageJsonPath of allPackageJsonPaths) {
119
+ const workspacePaths = await getWorkspacePackageJsonPaths(packageJsonPath);
120
+ for (const workspacePath of workspacePaths) {
121
+ workspacePackageJsonPaths.add(path.resolve(workspacePath));
122
+ }
123
+ }
124
+ return allPackageJsonPaths
125
+ .map((packageJsonPath) => path.resolve(packageJsonPath))
126
+ .filter((packageJsonPath) => !workspacePackageJsonPaths.has(packageJsonPath))
127
+ .sort();
128
+ }
129
+ export async function getRootPackageInstallStatus(packageJsonPath) {
130
+ const rootDir = path.dirname(packageJsonPath);
131
+ const packageJson = await readPackageJson(packageJsonPath);
132
+ if (!packageJson) {
133
+ return {
134
+ rootDir,
135
+ packageJsonPath,
136
+ packageManager: null,
137
+ dependencyHash: null,
138
+ dependenciesNeedInstall: false,
139
+ missingDependencies: [],
140
+ missingDevDependencies: [],
141
+ missingOptionalDependencies: [],
142
+ reason: 'package-json-unreadable',
143
+ };
144
+ }
145
+ const dependencySnapshot = getDependencySnapshot(packageJson);
146
+ const dependencyHash = hashString(JSON.stringify(dependencySnapshot));
147
+ const packageManager = parsePackageManager(packageJson.packageManager);
148
+ const dependencies = getExpectedDependencies(packageJson.dependencies);
149
+ const devDependencies = getExpectedDependencies(packageJson.devDependencies);
150
+ const optionalDependencies = getExpectedDependencies(packageJson.optionalDependencies);
151
+ const expectedDependencies = [
152
+ ...dependencies,
153
+ ...devDependencies,
154
+ ...optionalDependencies,
155
+ ];
156
+ if (expectedDependencies.length === 0) {
157
+ return {
158
+ rootDir,
159
+ packageJsonPath,
160
+ packageManager,
161
+ dependencyHash,
162
+ dependenciesNeedInstall: false,
163
+ missingDependencies: [],
164
+ missingDevDependencies: [],
165
+ missingOptionalDependencies: [],
166
+ reason: 'up-to-date',
167
+ };
168
+ }
169
+ const installedPackages = await listInstalledPackages(path.join(rootDir, 'node_modules'));
170
+ if (!installedPackages) {
171
+ return {
172
+ rootDir,
173
+ packageJsonPath,
174
+ packageManager,
175
+ dependencyHash,
176
+ dependenciesNeedInstall: true,
177
+ missingDependencies: dependencies,
178
+ missingDevDependencies: devDependencies,
179
+ missingOptionalDependencies: optionalDependencies,
180
+ reason: 'missing-node-modules',
181
+ };
182
+ }
183
+ const missingDependencies = dependencies.filter((dep) => !installedPackages.has(dep));
184
+ const missingDevDependencies = devDependencies.filter((dep) => !installedPackages.has(dep));
185
+ const missingOptionalDependencies = optionalDependencies.filter((dep) => !installedPackages.has(dep));
186
+ const dependenciesNeedInstall = missingDependencies.length > 0 || missingDevDependencies.length > 0;
187
+ return {
188
+ rootDir,
189
+ packageJsonPath,
190
+ packageManager,
191
+ dependencyHash,
192
+ dependenciesNeedInstall,
193
+ missingDependencies,
194
+ missingDevDependencies,
195
+ missingOptionalDependencies,
196
+ reason: dependenciesNeedInstall ? 'missing-dependencies' : 'up-to-date',
197
+ };
198
+ }
199
+ export async function getWorkspaceInstallStatus(cwd) {
200
+ const rootPackageJsonPaths = await getRootPackageJsonPaths(cwd);
201
+ const rootStatuses = await Promise.all(rootPackageJsonPaths.map(getRootPackageInstallStatus));
202
+ const dependenciesNeedInstall = rootStatuses.some((status) => status.dependenciesNeedInstall);
203
+ const dependencyHash = rootStatuses.length > 0
204
+ ? hashString(JSON.stringify(rootStatuses
205
+ .map((status) => ({
206
+ path: path.relative(cwd, status.packageJsonPath),
207
+ hash: status.dependencyHash,
208
+ }))
209
+ .sort((a, b) => a.path.localeCompare(b.path))))
210
+ : null;
211
+ return {
212
+ roots: rootStatuses,
213
+ dependenciesNeedInstall,
214
+ dependencyHash,
215
+ };
216
+ }
217
+ export function getInstallCommand(packageManager) {
218
+ switch (packageManager) {
219
+ case 'pnpm':
220
+ return { command: 'pnpm', args: ['install'] };
221
+ case 'yarn':
222
+ return { command: 'yarn', args: ['install'] };
223
+ case 'bun':
224
+ return { command: 'bun', args: ['install'] };
225
+ case 'npm':
226
+ default:
227
+ return { command: 'npm', args: ['install'] };
228
+ }
229
+ }
@@ -1,4 +1,5 @@
1
1
  import "./init-env.js";
2
+ import { z } from 'zod';
2
3
  import { cachified, connectionCache } from "./cache.server.js";
3
4
  import { logger } from "./logger.js";
4
5
  export { dayjs } from "./utils.js";
@@ -42,6 +43,7 @@ export async function checkConnection({ request, timings, } = {}) {
42
43
  timings,
43
44
  key: 'connected',
44
45
  ttl: 1000 * 10,
46
+ checkValue: z.boolean(),
45
47
  async getFreshValue(context) {
46
48
  connectionLog('getting fresh connection value');
47
49
  const isOnline = await raceConnectivity();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@epic-web/workshop-utils",
3
- "version": "6.71.4",
3
+ "version": "6.72.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -191,7 +191,7 @@
191
191
  "@mdx-js/mdx": "^3.1.1",
192
192
  "@playwright/test": "^1.57.0",
193
193
  "@react-router/node": "^7.12.0",
194
- "@sentry/react-router": "^10.35.0",
194
+ "@sentry/react-router": "^10.36.0",
195
195
  "@testing-library/dom": "^10.4.1",
196
196
  "@testing-library/jest-dom": "^6.9.1",
197
197
  "@total-typescript/ts-reset": "^0.6.1",
@@ -221,7 +221,7 @@
221
221
  "mdx-bundler": "^10.1.1",
222
222
  "p-queue": "^9.1.0",
223
223
  "parse-git-diff": "^0.0.19",
224
- "pkgmgr": "^1.0.0",
224
+ "pkgmgr": "^1.1.1",
225
225
  "react": "^19.2.3",
226
226
  "react-dom": "^19.2.3",
227
227
  "react-router": "^7.12.0",
@@ -241,7 +241,7 @@
241
241
  "@types/hast": "^3.0.4",
242
242
  "@types/mdast": "^4.0.4",
243
243
  "@types/node": "^25.0.9",
244
- "@types/react": "^19.2.8",
244
+ "@types/react": "^19.2.9",
245
245
  "@types/react-dom": "^19.2.3",
246
246
  "@types/shell-quote": "^1.7.5",
247
247
  "vitest": "^4.0.17",