@epic-web/workshop-utils 6.45.0 → 6.45.2

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.
@@ -0,0 +1,48 @@
1
+ import { z } from 'zod';
2
+ declare const ConfigSchema: z.ZodObject<{
3
+ reposDirectory: z.ZodOptional<z.ZodString>;
4
+ }, "strip", z.ZodTypeAny, {
5
+ reposDirectory?: string | undefined;
6
+ }, {
7
+ reposDirectory?: string | undefined;
8
+ }>;
9
+ export type Workshop = {
10
+ name: string;
11
+ title: string;
12
+ subtitle?: string;
13
+ repoName: string;
14
+ path: string;
15
+ };
16
+ export type WorkshopsConfig = z.infer<typeof ConfigSchema>;
17
+ export declare function loadConfig(): Promise<WorkshopsConfig>;
18
+ export declare function saveConfig(config: WorkshopsConfig): Promise<void>;
19
+ export declare function getReposDirectory(): Promise<string>;
20
+ export declare function isReposDirectoryConfigured(): Promise<boolean>;
21
+ export declare function getDefaultReposDir(): string;
22
+ export declare function setReposDirectory(directory: string): Promise<void>;
23
+ /**
24
+ * Scan a directory for workshops (directories with package.json containing "epicshop" property)
25
+ */
26
+ export declare function listWorkshops(): Promise<Workshop[]>;
27
+ export declare function getWorkshop(idOrName: string): Promise<Workshop | undefined>;
28
+ export declare function workshopExists(repoName: string): Promise<boolean>;
29
+ export declare function getWorkshopByPath(workshopPath: string): Promise<Workshop | undefined>;
30
+ /**
31
+ * Check for unpushed changes in a git repository
32
+ * Returns info about unpushed commits across all branches
33
+ */
34
+ export declare function getUnpushedChanges(repoPath: string): Promise<{
35
+ hasUnpushed: boolean;
36
+ branches: Array<{
37
+ name: string;
38
+ unpushedCount: number;
39
+ }>;
40
+ uncommittedChanges: boolean;
41
+ summary: string[];
42
+ }>;
43
+ /**
44
+ * Delete a workshop directory
45
+ */
46
+ export declare function deleteWorkshop(workshopPath: string): Promise<void>;
47
+ export {};
48
+ //# sourceMappingURL=workshops.server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workshops.server.d.ts","sourceRoot":"","sources":["../../src/workshops.server.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAqBvB,QAAA,MAAM,YAAY;;;;;;EAEhB,CAAA;AAEF,MAAM,MAAM,QAAQ,GAAG;IACtB,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;CACZ,CAAA;AAED,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAA;AA2B1D,wBAAsB,UAAU,IAAI,OAAO,CAAC,eAAe,CAAC,CAS3D;AAED,wBAAsB,UAAU,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAGvE;AAED,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,MAAM,CAAC,CAGzD;AAED,wBAAsB,0BAA0B,IAAI,OAAO,CAAC,OAAO,CAAC,CAGnE;AAED,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED,wBAAsB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAIxE;AAED;;GAEG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC,CA4CzD;AAED,wBAAsB,WAAW,CAChC,QAAQ,EAAE,MAAM,GACd,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC,CAQ/B;AAED,wBAAsB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAKvE;AAED,wBAAsB,iBAAiB,CACtC,YAAY,EAAE,MAAM,GAClB,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC,CAI/B;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;IACnE,WAAW,EAAE,OAAO,CAAA;IACpB,QAAQ,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACxD,kBAAkB,EAAE,OAAO,CAAA;IAC3B,OAAO,EAAE,MAAM,EAAE,CAAA;CACjB,CAAC,CA6HD;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAExE"}
@@ -0,0 +1,250 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { promises as fs } from 'node:fs';
3
+ import * as os from 'node:os';
4
+ import * as path from 'node:path';
5
+ import { z } from 'zod';
6
+ import { resolvePrimaryDir } from './data-storage.server.js';
7
+ const CONFIG_FILE = 'workshops-config.json';
8
+ // Schema for the epicshop property in package.json
9
+ const EpicshopConfigSchema = z.object({
10
+ title: z.string(),
11
+ subtitle: z.string().optional(),
12
+ product: z
13
+ .object({
14
+ host: z.string().optional(),
15
+ slug: z.string().optional(),
16
+ logo: z.string().optional(),
17
+ displayName: z.string().optional(),
18
+ displayNameShort: z.string().optional(),
19
+ })
20
+ .optional(),
21
+ });
22
+ // Schema for workshop configuration (stored settings only)
23
+ const ConfigSchema = z.object({
24
+ reposDirectory: z.string().optional(),
25
+ });
26
+ function getDefaultReposDirectory() {
27
+ return path.join(os.homedir(), 'epic-workshops');
28
+ }
29
+ function resolveConfigPath() {
30
+ return path.join(resolvePrimaryDir(), CONFIG_FILE);
31
+ }
32
+ async function ensureDir(dir) {
33
+ try {
34
+ await fs.mkdir(dir, { recursive: true, mode: 0o700 });
35
+ }
36
+ catch { }
37
+ try {
38
+ await fs.chmod(dir, 0o700);
39
+ }
40
+ catch { }
41
+ }
42
+ async function atomicWriteJSON(filePath, data) {
43
+ const dir = path.dirname(filePath);
44
+ await ensureDir(dir);
45
+ const tmp = path.join(dir, `.tmp-${randomUUID()}`);
46
+ await fs.writeFile(tmp, JSON.stringify(data, null, 2), { mode: 0o600 });
47
+ await fs.rename(tmp, filePath);
48
+ }
49
+ export async function loadConfig() {
50
+ const configPath = resolveConfigPath();
51
+ try {
52
+ const txt = await fs.readFile(configPath, 'utf8');
53
+ const data = JSON.parse(txt);
54
+ return ConfigSchema.parse(data);
55
+ }
56
+ catch {
57
+ return {};
58
+ }
59
+ }
60
+ export async function saveConfig(config) {
61
+ const configPath = resolveConfigPath();
62
+ await atomicWriteJSON(configPath, config);
63
+ }
64
+ export async function getReposDirectory() {
65
+ const config = await loadConfig();
66
+ return config.reposDirectory || getDefaultReposDirectory();
67
+ }
68
+ export async function isReposDirectoryConfigured() {
69
+ const config = await loadConfig();
70
+ return Boolean(config.reposDirectory);
71
+ }
72
+ export function getDefaultReposDir() {
73
+ return getDefaultReposDirectory();
74
+ }
75
+ export async function setReposDirectory(directory) {
76
+ const config = await loadConfig();
77
+ config.reposDirectory = path.resolve(directory);
78
+ await saveConfig(config);
79
+ }
80
+ /**
81
+ * Scan a directory for workshops (directories with package.json containing "epicshop" property)
82
+ */
83
+ export async function listWorkshops() {
84
+ const reposDir = await getReposDirectory();
85
+ // Check if directory exists
86
+ try {
87
+ await fs.access(reposDir);
88
+ }
89
+ catch {
90
+ return [];
91
+ }
92
+ const entries = await fs.readdir(reposDir, { withFileTypes: true });
93
+ const workshops = [];
94
+ for (const entry of entries) {
95
+ if (!entry.isDirectory())
96
+ continue;
97
+ const workshopPath = path.join(reposDir, entry.name);
98
+ const pkgPath = path.join(workshopPath, 'package.json');
99
+ try {
100
+ const pkgContent = await fs.readFile(pkgPath, 'utf8');
101
+ const pkg = JSON.parse(pkgContent);
102
+ if (pkg.epicshop) {
103
+ const epicshopConfig = EpicshopConfigSchema.safeParse(pkg.epicshop);
104
+ if (epicshopConfig.success) {
105
+ workshops.push({
106
+ name: pkg.name || entry.name,
107
+ title: epicshopConfig.data.title,
108
+ subtitle: epicshopConfig.data.subtitle,
109
+ repoName: entry.name,
110
+ path: workshopPath,
111
+ });
112
+ }
113
+ }
114
+ }
115
+ catch {
116
+ // Not a valid workshop directory, skip
117
+ }
118
+ }
119
+ return workshops;
120
+ }
121
+ export async function getWorkshop(idOrName) {
122
+ const workshops = await listWorkshops();
123
+ return workshops.find((w) => w.name.toLowerCase() === idOrName.toLowerCase() ||
124
+ w.repoName.toLowerCase() === idOrName.toLowerCase() ||
125
+ w.title.toLowerCase() === idOrName.toLowerCase());
126
+ }
127
+ export async function workshopExists(repoName) {
128
+ const workshops = await listWorkshops();
129
+ return workshops.some((w) => w.repoName.toLowerCase() === repoName.toLowerCase());
130
+ }
131
+ export async function getWorkshopByPath(workshopPath) {
132
+ const workshops = await listWorkshops();
133
+ const resolvedPath = path.resolve(workshopPath);
134
+ return workshops.find((w) => path.resolve(w.path) === resolvedPath);
135
+ }
136
+ /**
137
+ * Check for unpushed changes in a git repository
138
+ * Returns info about unpushed commits across all branches
139
+ */
140
+ export async function getUnpushedChanges(repoPath) {
141
+ const { execSync } = await import('node:child_process');
142
+ const result = {
143
+ hasUnpushed: false,
144
+ branches: [],
145
+ uncommittedChanges: false,
146
+ summary: [],
147
+ };
148
+ try {
149
+ // Check if it's a git repository
150
+ execSync('git rev-parse --git-dir', {
151
+ cwd: repoPath,
152
+ stdio: 'pipe',
153
+ });
154
+ }
155
+ catch {
156
+ // Not a git repository
157
+ return result;
158
+ }
159
+ try {
160
+ // Check for uncommitted changes
161
+ const status = execSync('git status --porcelain', {
162
+ cwd: repoPath,
163
+ encoding: 'utf8',
164
+ }).trim();
165
+ if (status) {
166
+ result.uncommittedChanges = true;
167
+ result.hasUnpushed = true;
168
+ const lines = status.split('\n');
169
+ const modified = lines.filter((l) => l.startsWith(' M') || l.startsWith('M ')).length;
170
+ const added = lines.filter((l) => l.startsWith('A ') || l.startsWith('??')).length;
171
+ const deleted = lines.filter((l) => l.startsWith(' D') || l.startsWith('D ')).length;
172
+ const parts = [];
173
+ if (modified > 0)
174
+ parts.push(`${modified} modified`);
175
+ if (added > 0)
176
+ parts.push(`${added} untracked/added`);
177
+ if (deleted > 0)
178
+ parts.push(`${deleted} deleted`);
179
+ if (parts.length > 0) {
180
+ result.summary.push(`Uncommitted changes: ${parts.join(', ')}`);
181
+ }
182
+ }
183
+ // Get all local branches
184
+ const branchOutput = execSync('git branch', {
185
+ cwd: repoPath,
186
+ encoding: 'utf8',
187
+ }).trim();
188
+ const branches = branchOutput
189
+ .split('\n')
190
+ .map((b) => b.replace(/^\*?\s*/, '').trim())
191
+ .filter(Boolean);
192
+ for (const branch of branches) {
193
+ try {
194
+ // Check if branch has an upstream
195
+ const upstream = execSync(`git rev-parse --abbrev-ref ${branch}@{upstream}`, {
196
+ cwd: repoPath,
197
+ encoding: 'utf8',
198
+ stdio: ['pipe', 'pipe', 'pipe'],
199
+ }).trim();
200
+ // Count unpushed commits
201
+ const unpushedOutput = execSync(`git rev-list --count ${upstream}..${branch}`, {
202
+ cwd: repoPath,
203
+ encoding: 'utf8',
204
+ }).trim();
205
+ const unpushedCount = parseInt(unpushedOutput, 10);
206
+ if (unpushedCount > 0) {
207
+ result.hasUnpushed = true;
208
+ result.branches.push({ name: branch, unpushedCount });
209
+ result.summary.push(`Branch "${branch}": ${unpushedCount} unpushed commit${unpushedCount > 1 ? 's' : ''}`);
210
+ }
211
+ }
212
+ catch {
213
+ // Branch has no upstream, check if it has any commits not in any remote
214
+ try {
215
+ const allRemotes = execSync('git remote', {
216
+ cwd: repoPath,
217
+ encoding: 'utf8',
218
+ }).trim();
219
+ if (allRemotes) {
220
+ // Branch exists but has no upstream tracking
221
+ const commitCount = execSync(`git rev-list --count ${branch}`, {
222
+ cwd: repoPath,
223
+ encoding: 'utf8',
224
+ }).trim();
225
+ const count = parseInt(commitCount, 10);
226
+ if (count > 0) {
227
+ result.hasUnpushed = true;
228
+ result.branches.push({ name: branch, unpushedCount: count });
229
+ result.summary.push(`Branch "${branch}": ${count} local commit${count > 1 ? 's' : ''} (no upstream)`);
230
+ }
231
+ }
232
+ }
233
+ catch {
234
+ // Ignore errors for individual branches
235
+ }
236
+ }
237
+ }
238
+ }
239
+ catch {
240
+ // Error checking git status, assume no unpushed changes
241
+ }
242
+ return result;
243
+ }
244
+ /**
245
+ * Delete a workshop directory
246
+ */
247
+ export async function deleteWorkshop(workshopPath) {
248
+ await fs.rm(workshopPath, { recursive: true, force: true });
249
+ }
250
+ //# sourceMappingURL=workshops.server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workshops.server.js","sourceRoot":"","sources":["../../src/workshops.server.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomUUID } from 'node:crypto'\nimport { promises as fs } from 'node:fs'\nimport * as os from 'node:os'\nimport * as path from 'node:path'\nimport { z } from 'zod'\nimport { resolvePrimaryDir } from './data-storage.server.js'\n\nconst CONFIG_FILE = 'workshops-config.json'\n\n// Schema for the epicshop property in package.json\nconst EpicshopConfigSchema = z.object({\n\ttitle: z.string(),\n\tsubtitle: z.string().optional(),\n\tproduct: z\n\t\t.object({\n\t\t\thost: z.string().optional(),\n\t\t\tslug: z.string().optional(),\n\t\t\tlogo: z.string().optional(),\n\t\t\tdisplayName: z.string().optional(),\n\t\t\tdisplayNameShort: z.string().optional(),\n\t\t})\n\t\t.optional(),\n})\n\n// Schema for workshop configuration (stored settings only)\nconst ConfigSchema = z.object({\n\treposDirectory: z.string().optional(),\n})\n\nexport type Workshop = {\n\tname: string\n\ttitle: string\n\tsubtitle?: string\n\trepoName: string\n\tpath: string\n}\n\nexport type WorkshopsConfig = z.infer<typeof ConfigSchema>\n\nfunction getDefaultReposDirectory(): string {\n\treturn path.join(os.homedir(), 'epic-workshops')\n}\n\nfunction resolveConfigPath(): string {\n\treturn path.join(resolvePrimaryDir(), CONFIG_FILE)\n}\n\nasync function ensureDir(dir: string) {\n\ttry {\n\t\tawait fs.mkdir(dir, { recursive: true, mode: 0o700 })\n\t} catch {}\n\ttry {\n\t\tawait fs.chmod(dir, 0o700)\n\t} catch {}\n}\n\nasync function atomicWriteJSON(filePath: string, data: unknown) {\n\tconst dir = path.dirname(filePath)\n\tawait ensureDir(dir)\n\tconst tmp = path.join(dir, `.tmp-${randomUUID()}`)\n\tawait fs.writeFile(tmp, JSON.stringify(data, null, 2), { mode: 0o600 })\n\tawait fs.rename(tmp, filePath)\n}\n\nexport async function loadConfig(): Promise<WorkshopsConfig> {\n\tconst configPath = resolveConfigPath()\n\ttry {\n\t\tconst txt = await fs.readFile(configPath, 'utf8')\n\t\tconst data = JSON.parse(txt)\n\t\treturn ConfigSchema.parse(data)\n\t} catch {\n\t\treturn {}\n\t}\n}\n\nexport async function saveConfig(config: WorkshopsConfig): Promise<void> {\n\tconst configPath = resolveConfigPath()\n\tawait atomicWriteJSON(configPath, config)\n}\n\nexport async function getReposDirectory(): Promise<string> {\n\tconst config = await loadConfig()\n\treturn config.reposDirectory || getDefaultReposDirectory()\n}\n\nexport async function isReposDirectoryConfigured(): Promise<boolean> {\n\tconst config = await loadConfig()\n\treturn Boolean(config.reposDirectory)\n}\n\nexport function getDefaultReposDir(): string {\n\treturn getDefaultReposDirectory()\n}\n\nexport async function setReposDirectory(directory: string): Promise<void> {\n\tconst config = await loadConfig()\n\tconfig.reposDirectory = path.resolve(directory)\n\tawait saveConfig(config)\n}\n\n/**\n * Scan a directory for workshops (directories with package.json containing \"epicshop\" property)\n */\nexport async function listWorkshops(): Promise<Workshop[]> {\n\tconst reposDir = await getReposDirectory()\n\n\t// Check if directory exists\n\ttry {\n\t\tawait fs.access(reposDir)\n\t} catch {\n\t\treturn []\n\t}\n\n\tconst entries = await fs.readdir(reposDir, { withFileTypes: true })\n\tconst workshops: Workshop[] = []\n\n\tfor (const entry of entries) {\n\t\tif (!entry.isDirectory()) continue\n\n\t\tconst workshopPath = path.join(reposDir, entry.name)\n\t\tconst pkgPath = path.join(workshopPath, 'package.json')\n\n\t\ttry {\n\t\t\tconst pkgContent = await fs.readFile(pkgPath, 'utf8')\n\t\t\tconst pkg = JSON.parse(pkgContent) as {\n\t\t\t\tname?: string\n\t\t\t\tepicshop?: unknown\n\t\t\t}\n\n\t\t\tif (pkg.epicshop) {\n\t\t\t\tconst epicshopConfig = EpicshopConfigSchema.safeParse(pkg.epicshop)\n\t\t\t\tif (epicshopConfig.success) {\n\t\t\t\t\tworkshops.push({\n\t\t\t\t\t\tname: pkg.name || entry.name,\n\t\t\t\t\t\ttitle: epicshopConfig.data.title,\n\t\t\t\t\t\tsubtitle: epicshopConfig.data.subtitle,\n\t\t\t\t\t\trepoName: entry.name,\n\t\t\t\t\t\tpath: workshopPath,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// Not a valid workshop directory, skip\n\t\t}\n\t}\n\n\treturn workshops\n}\n\nexport async function getWorkshop(\n\tidOrName: string,\n): Promise<Workshop | undefined> {\n\tconst workshops = await listWorkshops()\n\treturn workshops.find(\n\t\t(w) =>\n\t\t\tw.name.toLowerCase() === idOrName.toLowerCase() ||\n\t\t\tw.repoName.toLowerCase() === idOrName.toLowerCase() ||\n\t\t\tw.title.toLowerCase() === idOrName.toLowerCase(),\n\t)\n}\n\nexport async function workshopExists(repoName: string): Promise<boolean> {\n\tconst workshops = await listWorkshops()\n\treturn workshops.some(\n\t\t(w) => w.repoName.toLowerCase() === repoName.toLowerCase(),\n\t)\n}\n\nexport async function getWorkshopByPath(\n\tworkshopPath: string,\n): Promise<Workshop | undefined> {\n\tconst workshops = await listWorkshops()\n\tconst resolvedPath = path.resolve(workshopPath)\n\treturn workshops.find((w) => path.resolve(w.path) === resolvedPath)\n}\n\n/**\n * Check for unpushed changes in a git repository\n * Returns info about unpushed commits across all branches\n */\nexport async function getUnpushedChanges(repoPath: string): Promise<{\n\thasUnpushed: boolean\n\tbranches: Array<{ name: string; unpushedCount: number }>\n\tuncommittedChanges: boolean\n\tsummary: string[]\n}> {\n\tconst { execSync } = await import('node:child_process')\n\n\tconst result = {\n\t\thasUnpushed: false,\n\t\tbranches: [] as Array<{ name: string; unpushedCount: number }>,\n\t\tuncommittedChanges: false,\n\t\tsummary: [] as string[],\n\t}\n\n\ttry {\n\t\t// Check if it's a git repository\n\t\texecSync('git rev-parse --git-dir', {\n\t\t\tcwd: repoPath,\n\t\t\tstdio: 'pipe',\n\t\t})\n\t} catch {\n\t\t// Not a git repository\n\t\treturn result\n\t}\n\n\ttry {\n\t\t// Check for uncommitted changes\n\t\tconst status = execSync('git status --porcelain', {\n\t\t\tcwd: repoPath,\n\t\t\tencoding: 'utf8',\n\t\t}).trim()\n\n\t\tif (status) {\n\t\t\tresult.uncommittedChanges = true\n\t\t\tresult.hasUnpushed = true\n\t\t\tconst lines = status.split('\\n')\n\t\t\tconst modified = lines.filter(\n\t\t\t\t(l) => l.startsWith(' M') || l.startsWith('M '),\n\t\t\t).length\n\t\t\tconst added = lines.filter(\n\t\t\t\t(l) => l.startsWith('A ') || l.startsWith('??'),\n\t\t\t).length\n\t\t\tconst deleted = lines.filter(\n\t\t\t\t(l) => l.startsWith(' D') || l.startsWith('D '),\n\t\t\t).length\n\n\t\t\tconst parts = []\n\t\t\tif (modified > 0) parts.push(`${modified} modified`)\n\t\t\tif (added > 0) parts.push(`${added} untracked/added`)\n\t\t\tif (deleted > 0) parts.push(`${deleted} deleted`)\n\t\t\tif (parts.length > 0) {\n\t\t\t\tresult.summary.push(`Uncommitted changes: ${parts.join(', ')}`)\n\t\t\t}\n\t\t}\n\n\t\t// Get all local branches\n\t\tconst branchOutput = execSync('git branch', {\n\t\t\tcwd: repoPath,\n\t\t\tencoding: 'utf8',\n\t\t}).trim()\n\n\t\tconst branches = branchOutput\n\t\t\t.split('\\n')\n\t\t\t.map((b) => b.replace(/^\\*?\\s*/, '').trim())\n\t\t\t.filter(Boolean)\n\n\t\tfor (const branch of branches) {\n\t\t\ttry {\n\t\t\t\t// Check if branch has an upstream\n\t\t\t\tconst upstream = execSync(\n\t\t\t\t\t`git rev-parse --abbrev-ref ${branch}@{upstream}`,\n\t\t\t\t\t{\n\t\t\t\t\t\tcwd: repoPath,\n\t\t\t\t\t\tencoding: 'utf8',\n\t\t\t\t\t\tstdio: ['pipe', 'pipe', 'pipe'],\n\t\t\t\t\t},\n\t\t\t\t).trim()\n\n\t\t\t\t// Count unpushed commits\n\t\t\t\tconst unpushedOutput = execSync(\n\t\t\t\t\t`git rev-list --count ${upstream}..${branch}`,\n\t\t\t\t\t{\n\t\t\t\t\t\tcwd: repoPath,\n\t\t\t\t\t\tencoding: 'utf8',\n\t\t\t\t\t},\n\t\t\t\t).trim()\n\n\t\t\t\tconst unpushedCount = parseInt(unpushedOutput, 10)\n\t\t\t\tif (unpushedCount > 0) {\n\t\t\t\t\tresult.hasUnpushed = true\n\t\t\t\t\tresult.branches.push({ name: branch, unpushedCount })\n\t\t\t\t\tresult.summary.push(\n\t\t\t\t\t\t`Branch \"${branch}\": ${unpushedCount} unpushed commit${unpushedCount > 1 ? 's' : ''}`,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Branch has no upstream, check if it has any commits not in any remote\n\t\t\t\ttry {\n\t\t\t\t\tconst allRemotes = execSync('git remote', {\n\t\t\t\t\t\tcwd: repoPath,\n\t\t\t\t\t\tencoding: 'utf8',\n\t\t\t\t\t}).trim()\n\n\t\t\t\t\tif (allRemotes) {\n\t\t\t\t\t\t// Branch exists but has no upstream tracking\n\t\t\t\t\t\tconst commitCount = execSync(`git rev-list --count ${branch}`, {\n\t\t\t\t\t\t\tcwd: repoPath,\n\t\t\t\t\t\t\tencoding: 'utf8',\n\t\t\t\t\t\t}).trim()\n\n\t\t\t\t\t\tconst count = parseInt(commitCount, 10)\n\t\t\t\t\t\tif (count > 0) {\n\t\t\t\t\t\t\tresult.hasUnpushed = true\n\t\t\t\t\t\t\tresult.branches.push({ name: branch, unpushedCount: count })\n\t\t\t\t\t\t\tresult.summary.push(\n\t\t\t\t\t\t\t\t`Branch \"${branch}\": ${count} local commit${count > 1 ? 's' : ''} (no upstream)`,\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Ignore errors for individual branches\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t// Error checking git status, assume no unpushed changes\n\t}\n\n\treturn result\n}\n\n/**\n * Delete a workshop directory\n */\nexport async function deleteWorkshop(workshopPath: string): Promise<void> {\n\tawait fs.rm(workshopPath, { recursive: true, force: true })\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@epic-web/workshop-utils",
3
- "version": "6.45.0",
3
+ "version": "6.45.2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -35,7 +35,8 @@
35
35
  "./test": "./src/test.ts",
36
36
  "./request-context.server": "./src/request-context.server.ts",
37
37
  "./utils.server": "./src/utils.server.ts",
38
- "./utils": "./src/utils.ts"
38
+ "./utils": "./src/utils.ts",
39
+ "./workshops.server": "./src/workshops.server.ts"
39
40
  }
40
41
  },
41
42
  "exports": {
@@ -183,6 +184,12 @@
183
184
  "types": "./dist/esm/utils.d.ts",
184
185
  "default": "./dist/esm/utils.js"
185
186
  }
187
+ },
188
+ "./workshops.server": {
189
+ "import": {
190
+ "types": "./dist/esm/workshops.server.d.ts",
191
+ "default": "./dist/esm/workshops.server.js"
192
+ }
186
193
  }
187
194
  },
188
195
  "files": [