supaslidev 0.1.4 → 0.2.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.
Files changed (67) hide show
  1. package/app/app.config.ts +9 -0
  2. package/app/assets/css/main.css +90 -0
  3. package/app/components/AppHeader.vue +429 -0
  4. package/app/components/CreatePresentationDialog.vue +236 -0
  5. package/app/components/EmptyState.vue +37 -0
  6. package/app/components/ImportPresentationDialog.vue +865 -0
  7. package/app/components/PresentationCard.vue +343 -0
  8. package/app/components/PresentationListItem.vue +242 -0
  9. package/app/composables/useServers.ts +148 -0
  10. package/app/layouts/default.vue +49 -0
  11. package/app/pages/index.vue +542 -0
  12. package/dist/cli/index.js +183751 -137
  13. package/dist/config.d.ts +8 -0
  14. package/dist/config.js +16 -0
  15. package/dist/index.d.ts +21 -0
  16. package/dist/index.js +3 -0
  17. package/dist/module.d.ts +6 -0
  18. package/dist/module.js +9168 -0
  19. package/dist/prompt.js +847 -0
  20. package/nuxt.config.ts +53 -0
  21. package/package.json +26 -19
  22. package/server/api/export/[id].post.ts +67 -0
  23. package/server/api/open-editor/[id].post.ts +28 -0
  24. package/server/api/presentations/import.post.ts +139 -0
  25. package/server/api/presentations/index.get.ts +18 -0
  26. package/server/api/presentations/index.post.ts +175 -0
  27. package/server/api/presentations/upload.post.ts +174 -0
  28. package/server/api/presentations/validate.post.ts +14 -0
  29. package/server/api/servers/[id].delete.ts +15 -0
  30. package/server/api/servers/[id].post.ts +17 -0
  31. package/server/api/servers/index.delete.ts +5 -0
  32. package/server/api/servers/index.get.ts +5 -0
  33. package/server/api/servers/stop-all.post.ts +5 -0
  34. package/server/plugins/generate.ts +12 -0
  35. package/server/plugins/shutdown.ts +16 -0
  36. package/server/routes/exports/[...path].get.ts +25 -0
  37. package/server/utils/config.ts +13 -0
  38. package/server/utils/process-manager.ts +119 -0
  39. package/src/cli/commands/create.ts +125 -0
  40. package/src/cli/commands/deploy.ts +90 -0
  41. package/src/cli/commands/dev.ts +116 -0
  42. package/src/cli/commands/export.ts +63 -0
  43. package/src/cli/commands/import.ts +178 -0
  44. package/src/cli/commands/present.ts +111 -0
  45. package/src/cli/index.ts +87 -0
  46. package/src/cli/utils.ts +94 -0
  47. package/src/config.ts +21 -0
  48. package/src/index.ts +2 -0
  49. package/src/module.ts +12 -0
  50. package/src/shared/catalog.ts +94 -0
  51. package/src/shared/copy.ts +28 -0
  52. package/src/shared/index.ts +29 -0
  53. package/{scripts/generate-presentations.mjs → src/shared/presentations.ts} +23 -46
  54. package/src/shared/types.ts +29 -0
  55. package/src/shared/validation.ts +111 -0
  56. package/dist/assets/index-BerY9FcI.js +0 -49
  57. package/dist/assets/index-CVzsY-on.css +0 -1
  58. package/dist/index.html +0 -24
  59. package/server/api.js +0 -1225
  60. /package/{dist → public}/apple-touch-icon.png +0 -0
  61. /package/{dist → public}/favicon-96x96.png +0 -0
  62. /package/{dist → public}/favicon.ico +0 -0
  63. /package/{dist → public}/favicon.svg +0 -0
  64. /package/{dist → public}/site.webmanifest +0 -0
  65. /package/{dist → public}/ssl-logo.png +0 -0
  66. /package/{dist → public}/web-app-manifest-192x192.png +0 -0
  67. /package/{dist → public}/web-app-manifest-512x512.png +0 -0
package/nuxt.config.ts ADDED
@@ -0,0 +1,53 @@
1
+ import { fileURLToPath } from 'node:url';
2
+ import { join } from 'node:path';
3
+ import { readFileSync } from 'node:fs';
4
+
5
+ const dir = fileURLToPath(new URL('.', import.meta.url));
6
+ const pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf-8'));
7
+
8
+ // When running via `supaslidev dev`, the Nuxt CWD is the package root inside
9
+ // node_modules. Place the build cache in the user's project root instead so
10
+ // it persists correctly across restarts and doesn't pollute node_modules.
11
+ const projectRoot = process.env.SUPASLIDEV_PROJECT_ROOT || process.cwd();
12
+
13
+ export default defineNuxtConfig({
14
+ buildDir: join(projectRoot, '.nuxt'),
15
+
16
+ // SPA mode: the dashboard is a local dev tool that doesn't need SSR.
17
+ // With SSR, Vue hydration timing causes e2e tests to interact with
18
+ // server-rendered elements before they become interactive.
19
+ ssr: false,
20
+
21
+ modules: ['@nuxt/ui', join(dir, 'src/module.ts')],
22
+
23
+ css: [join(dir, 'app/assets/css/main.css')],
24
+
25
+ devtools: { enabled: false },
26
+
27
+ // Disable external font providers — the dashboard is a local dev tool
28
+ // and should work offline without fetching from remote font services
29
+ fonts: {
30
+ defaults: {
31
+ weights: [400, 500, 600, 700],
32
+ },
33
+ providers: {
34
+ bunny: false,
35
+ fontshare: false,
36
+ fontsource: false,
37
+ google: false,
38
+ googleicons: false,
39
+ },
40
+ },
41
+
42
+ runtimeConfig: {
43
+ supaslidev: {
44
+ projectRoot: '',
45
+ presentationsDir: '',
46
+ },
47
+ public: {
48
+ supaslidevVersion: pkg.version,
49
+ },
50
+ },
51
+
52
+ compatibilityDate: '2025-05-01',
53
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supaslidev",
3
- "version": "0.1.4",
3
+ "version": "0.2.0",
4
4
  "description": "CLI toolkit for managing Supaslidev presentations",
5
5
  "keywords": [
6
6
  "slidev",
@@ -21,15 +21,26 @@
21
21
  "supaslidev": "./dist/cli/index.js"
22
22
  },
23
23
  "files": [
24
+ "app",
24
25
  "dist",
25
- "scripts",
26
- "server"
26
+ "nuxt.config.ts",
27
+ "public",
28
+ "server",
29
+ "src"
27
30
  ],
28
31
  "type": "module",
29
- "main": "./dist/cli/index.js",
32
+ "main": "./nuxt.config.ts",
30
33
  "exports": {
31
34
  ".": {
32
- "import": "./dist/cli/index.js"
35
+ "types": "./dist/index.d.ts",
36
+ "import": "./dist/index.js",
37
+ "default": "./src/index.ts"
38
+ },
39
+ "./layer": "./nuxt.config.ts",
40
+ "./nuxt": {
41
+ "types": "./dist/module.d.ts",
42
+ "import": "./dist/module.js",
43
+ "default": "./src/module.ts"
33
44
  }
34
45
  },
35
46
  "publishConfig": {
@@ -38,31 +49,27 @@
38
49
  "dependencies": {
39
50
  "@nuxt/ui": "^4.4.0",
40
51
  "commander": "^13.1.0",
41
- "create-supaslidev": "^0.1.0",
42
52
  "js-yaml": "^4.1.1",
53
+ "nuxt": "^4.4.2",
43
54
  "tailwindcss": "^4.1.18",
44
- "vue": "^3.5.0",
45
- "vue-router": "^4.6.4"
55
+ "vue": "^3.5.31"
46
56
  },
47
57
  "devDependencies": {
58
+ "@nuxt/kit": "^4.4.2",
59
+ "@nuxt/schema": "^4.4.2",
48
60
  "@types/js-yaml": "^4.0.9",
49
61
  "@types/node": "^22.10.0",
50
- "@vitejs/plugin-vue": "^5.2.1",
51
62
  "tsdown": "^0.12.5",
52
63
  "tsx": "^4.19.0",
53
64
  "typescript": "^5.3.3",
54
- "vite": "^6.0.7",
55
- "vitest": "^3.0.0",
56
- "vue-tsc": "^2.0.0"
65
+ "vitest": "^4.0.0",
66
+ "vue-tsc": "^2.0.0",
67
+ "create-supaslidev": "^0.2.0"
57
68
  },
58
69
  "scripts": {
59
- "dev": "tsx src/cli/index.ts",
60
- "dev:api": "node server/api.js",
61
- "build": "vite build && tsdown",
62
- "build:cli": "tsdown",
63
- "preview": "vite preview",
64
- "generate": "node scripts/generate-presentations.mjs",
65
- "typecheck": "vue-tsc --noEmit && tsc --noEmit -p tsconfig.cli.json",
70
+ "dev": "nuxt dev",
71
+ "build": "tsdown",
72
+ "typecheck": "nuxt typecheck",
66
73
  "lint": "oxlint --config ../../oxlint.json .",
67
74
  "test": "vitest run",
68
75
  "test:watch": "vitest"
@@ -0,0 +1,67 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { join } from 'node:path';
3
+ import { existsSync, mkdirSync } from 'node:fs';
4
+ import { isValidPresentationId } from '../../../src/shared/validation.js';
5
+ import { getProjectRoot } from '../../utils/config';
6
+
7
+ export default defineEventHandler(async (event) => {
8
+ const presentationId = getRouterParam(event, 'id')!;
9
+ const projectRoot = getProjectRoot();
10
+
11
+ if (!isValidPresentationId(presentationId)) {
12
+ throw createError({
13
+ statusCode: 400,
14
+ data: { success: false, error: 'Invalid presentation id' },
15
+ });
16
+ }
17
+
18
+ const presentationPath = join(projectRoot, 'presentations', presentationId);
19
+ const exportsDir = join(projectRoot, 'exports');
20
+ const outputPath = join(exportsDir, `${presentationId}.pdf`);
21
+
22
+ if (!existsSync(presentationPath)) {
23
+ throw createError({
24
+ statusCode: 404,
25
+ data: { success: false, error: 'Presentation not found' },
26
+ });
27
+ }
28
+
29
+ if (!existsSync(exportsDir)) {
30
+ mkdirSync(exportsDir, { recursive: true });
31
+ }
32
+
33
+ return new Promise((resolve) => {
34
+ const child = spawn('npx', ['slidev', 'export', '--output', outputPath], {
35
+ cwd: presentationPath,
36
+ stdio: ['pipe', 'pipe', 'pipe'],
37
+ shell: true,
38
+ });
39
+
40
+ let stderr = '';
41
+
42
+ child.stdout?.on('data', (data: Buffer) => {
43
+ console.log(`[export] ${data.toString().trim()}`);
44
+ });
45
+
46
+ child.stderr?.on('data', (data: Buffer) => {
47
+ stderr += data.toString();
48
+ console.error(`[export] ${data.toString().trim()}`);
49
+ });
50
+
51
+ child.on('error', (err: Error) => {
52
+ resolve({ success: false, error: `Export failed: ${err.message}` });
53
+ });
54
+
55
+ child.on('close', (code: number | null) => {
56
+ if (code === 0) {
57
+ resolve({
58
+ success: true,
59
+ pdfPath: `/exports/${presentationId}.pdf`,
60
+ filename: `${presentationId}.pdf`,
61
+ });
62
+ } else {
63
+ resolve({ success: false, error: `Export failed with exit code ${code}. ${stderr}` });
64
+ }
65
+ });
66
+ });
67
+ });
@@ -0,0 +1,28 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { join } from 'node:path';
3
+ import { existsSync } from 'node:fs';
4
+ import { isValidPresentationId } from '../../../src/shared/validation.js';
5
+ import { getProjectRoot } from '../../utils/config';
6
+
7
+ export default defineEventHandler((event) => {
8
+ const presentationId = getRouterParam(event, 'id')!;
9
+ const projectRoot = getProjectRoot();
10
+
11
+ if (!isValidPresentationId(presentationId)) {
12
+ throw createError({
13
+ statusCode: 400,
14
+ data: { success: false, error: 'Invalid presentation id' },
15
+ });
16
+ }
17
+
18
+ const slidesPath = join(projectRoot, 'presentations', presentationId, 'slides.md');
19
+ if (!existsSync(slidesPath)) {
20
+ throw createError({
21
+ statusCode: 404,
22
+ data: { success: false, error: 'Presentation not found' },
23
+ });
24
+ }
25
+
26
+ spawn('code', [slidesPath], { detached: true, stdio: 'ignore' }).unref();
27
+ return { success: true };
28
+ });
@@ -0,0 +1,139 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { join, basename, resolve } from 'node:path';
3
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
4
+ import {
5
+ validateSourceDirectoryResult,
6
+ SLUG_REGEX,
7
+ copyDirectorySelective,
8
+ convertToCatalogDependencies,
9
+ hasSharedPackage,
10
+ addSharedDependencyToPackageJson,
11
+ addSharedAddonToSlides,
12
+ regeneratePresentationsJson,
13
+ } from '../../../src/shared/index.js';
14
+ import { getProjectRoot, getPresentationsDir, getPresentationsJsonPath } from '../../utils/config';
15
+
16
+ export default defineEventHandler(async (event) => {
17
+ const body = await readBody(event);
18
+ const { source, name } = body;
19
+
20
+ const projectRoot = getProjectRoot();
21
+ const presentationsDir = getPresentationsDir();
22
+ const sourcePath = resolve(source);
23
+
24
+ // This endpoint is dev-only; sourcePath is validated by validateSourceDirectoryResult
25
+ const validation = validateSourceDirectoryResult(sourcePath);
26
+
27
+ if (!validation.isValid) {
28
+ throw createError({
29
+ statusCode: 400,
30
+ data: { field: 'source', message: validation.error },
31
+ });
32
+ }
33
+
34
+ const presentationName =
35
+ name ||
36
+ basename(sourcePath)
37
+ .toLowerCase()
38
+ .replace(/[^a-z0-9-]/g, '-')
39
+ .replace(/-{2,}/g, '-')
40
+ .replace(/^-+|-+$/g, '')
41
+ || 'untitled';
42
+
43
+ if (!SLUG_REGEX.test(presentationName)) {
44
+ throw createError({
45
+ statusCode: 400,
46
+ data: {
47
+ field: 'name',
48
+ message: 'Name must be a valid slug (lowercase letters, numbers, hyphens only)',
49
+ },
50
+ });
51
+ }
52
+
53
+ const destinationPath = join(presentationsDir, presentationName);
54
+
55
+ if (existsSync(destinationPath)) {
56
+ throw createError({
57
+ statusCode: 400,
58
+ data: { field: 'name', message: 'A presentation with this name already exists' },
59
+ });
60
+ }
61
+
62
+ copyDirectorySelective(sourcePath, destinationPath);
63
+
64
+ const sourcePackageJsonPath = join(sourcePath, 'package.json');
65
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
66
+ let packageJson: any;
67
+ try {
68
+ const packageJsonContent = readFileSync(sourcePackageJsonPath, 'utf-8');
69
+ packageJson = JSON.parse(packageJsonContent);
70
+ } catch {
71
+ throw createError({
72
+ statusCode: 400,
73
+ data: { field: 'source', message: `Invalid package.json in ${sourcePackageJsonPath}` },
74
+ });
75
+ }
76
+
77
+ packageJson.name = `@supaslidev/${presentationName}`;
78
+ packageJson.private = true;
79
+ packageJson.scripts = {
80
+ dev: 'slidev --open',
81
+ build: 'slidev build',
82
+ export: 'slidev export',
83
+ };
84
+
85
+ if (packageJson.dependencies) {
86
+ packageJson.dependencies = convertToCatalogDependencies(packageJson.dependencies);
87
+ }
88
+ if (packageJson.devDependencies) {
89
+ packageJson.devDependencies = convertToCatalogDependencies(packageJson.devDependencies);
90
+ }
91
+
92
+ const sharedExists = hasSharedPackage(projectRoot);
93
+ if (sharedExists) {
94
+ addSharedDependencyToPackageJson(packageJson);
95
+ }
96
+
97
+ writeFileSync(
98
+ join(destinationPath, 'package.json'),
99
+ JSON.stringify(packageJson, null, 2) + '\n',
100
+ );
101
+
102
+ if (sharedExists) {
103
+ const slidesPath = join(destinationPath, 'slides.md');
104
+ if (existsSync(slidesPath)) {
105
+ addSharedAddonToSlides(slidesPath);
106
+ }
107
+ }
108
+
109
+ regeneratePresentationsJson(presentationsDir, getPresentationsJsonPath());
110
+
111
+ const presentation = {
112
+ id: presentationName,
113
+ title: presentationName,
114
+ description: '',
115
+ theme: 'default',
116
+ background: 'https://cover.sli.dev',
117
+ duration: '',
118
+ };
119
+
120
+ // Run pnpm install in background
121
+ const install = spawn('pnpm', ['install'], {
122
+ cwd: projectRoot,
123
+ stdio: 'inherit',
124
+ shell: true,
125
+ });
126
+
127
+ install.on('close', (code: number | null) => {
128
+ if (code !== 0) {
129
+ console.error(`[import] pnpm install failed with code ${code}`);
130
+ }
131
+ });
132
+
133
+ install.on('error', (err) => {
134
+ console.error(`[import] pnpm install spawn error: ${err.message}`);
135
+ });
136
+
137
+ setResponseStatus(event, 201);
138
+ return presentation;
139
+ });
@@ -0,0 +1,18 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { getPresentationsJsonPath } from '../../utils/config';
3
+
4
+ export default defineEventHandler(() => {
5
+ const presentationsJsonPath = getPresentationsJsonPath();
6
+
7
+ if (existsSync(presentationsJsonPath)) {
8
+ const data = readFileSync(presentationsJsonPath, 'utf-8');
9
+ try {
10
+ return JSON.parse(data);
11
+ } catch (err) {
12
+ console.error(`Failed to parse ${presentationsJsonPath}: ${(err as Error).message}`);
13
+ return [];
14
+ }
15
+ }
16
+
17
+ return [];
18
+ });
@@ -0,0 +1,175 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { join } from 'node:path';
3
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
4
+ import {
5
+ parseFrontmatter,
6
+ extractDescription,
7
+ regeneratePresentationsJson,
8
+ hasSharedPackage,
9
+ addSharedAddonToSlides,
10
+ SLUG_REGEX,
11
+ } from '../../../src/shared/index.js';
12
+ import { getProjectRoot, getPresentationsDir, getPresentationsJsonPath } from '../../utils/config';
13
+
14
+ export default defineEventHandler(async (event) => {
15
+ const body = await readBody(event);
16
+ const { name, title, description, template = 'default' } = body;
17
+
18
+ const projectRoot = getProjectRoot();
19
+ const presentationsDir = getPresentationsDir();
20
+ const presentationPath = join(presentationsDir, name);
21
+
22
+ if (existsSync(presentationPath)) {
23
+ throw createError({
24
+ statusCode: 400,
25
+ data: { field: 'name', message: 'A presentation with this name already exists' },
26
+ });
27
+ }
28
+
29
+ if (!SLUG_REGEX.test(name)) {
30
+ throw createError({
31
+ statusCode: 400,
32
+ data: {
33
+ field: 'name',
34
+ message: 'Name must be a valid slug (lowercase letters, numbers, hyphens only)',
35
+ },
36
+ });
37
+ }
38
+
39
+ return new Promise((resolve) => {
40
+ const child = spawn('pnpm', ['create', 'slidev', name], {
41
+ cwd: presentationsDir,
42
+ stdio: ['pipe', 'pipe', 'pipe'],
43
+ shell: true,
44
+ });
45
+
46
+ let stderr = '';
47
+ let scaffoldingDone = false;
48
+ let pollInterval: ReturnType<typeof setInterval> | null = null;
49
+ const slidesPath = join(presentationPath, 'slides.md');
50
+
51
+ const checkScaffoldingComplete = () => {
52
+ if (existsSync(slidesPath) && !scaffoldingDone) {
53
+ scaffoldingDone = true;
54
+ if (pollInterval) clearInterval(pollInterval);
55
+ child.kill('SIGTERM');
56
+ }
57
+ };
58
+
59
+ pollInterval = setInterval(checkScaffoldingComplete, 200);
60
+
61
+ const scaffoldTimeout = setTimeout(() => {
62
+ if (!scaffoldingDone) {
63
+ if (pollInterval) clearInterval(pollInterval);
64
+ child.kill('SIGTERM');
65
+ }
66
+ }, 60000);
67
+
68
+ child.stderr.on('data', (data: Buffer) => {
69
+ stderr += data.toString();
70
+ });
71
+
72
+ child.on('error', (err: Error) => {
73
+ if (pollInterval) clearInterval(pollInterval);
74
+ clearTimeout(scaffoldTimeout);
75
+ resolve({ success: false, message: `Failed to create presentation: ${err.message}` });
76
+ });
77
+
78
+ child.on('close', () => {
79
+ if (pollInterval) clearInterval(pollInterval);
80
+ clearTimeout(scaffoldTimeout);
81
+
82
+ if (!existsSync(slidesPath)) {
83
+ resolve({ success: false, message: `Slidev CLI failed. ${stderr}` });
84
+ return;
85
+ }
86
+
87
+ let slidesContent = readFileSync(slidesPath, 'utf-8');
88
+
89
+ slidesContent = slidesContent.replace(
90
+ /^(---\n[\s\S]*?)theme:\s*\S+/m,
91
+ `$1theme: ${template}`,
92
+ );
93
+
94
+ if (title) {
95
+ slidesContent = slidesContent.replace(
96
+ /^(---\n[\s\S]*?)title:\s*.+$/m,
97
+ `$1title: ${title}`,
98
+ );
99
+ }
100
+
101
+ if (description) {
102
+ slidesContent = slidesContent.replace(
103
+ /^(---\n[\s\S]*?)info:\s*\|[\s\S]*?(?=\n[a-zA-Z]|\n---)/m,
104
+ `$1info: |\n ${description}\n`,
105
+ );
106
+ }
107
+
108
+ writeFileSync(slidesPath, slidesContent);
109
+
110
+ const sharedExists = hasSharedPackage(projectRoot);
111
+ if (sharedExists) {
112
+ addSharedAddonToSlides(slidesPath);
113
+ }
114
+
115
+ const frontmatter = parseFrontmatter(readFileSync(slidesPath, 'utf-8'));
116
+
117
+ const packageJsonPath = join(presentationPath, 'package.json');
118
+ const catalogPackageJson = {
119
+ name: `@supaslidev/${name}`,
120
+ private: true,
121
+ type: 'module',
122
+ scripts: {
123
+ build: 'slidev build',
124
+ dev: 'slidev --open',
125
+ export: 'slidev export',
126
+ },
127
+ dependencies: {
128
+ '@slidev/cli': 'catalog:',
129
+ '@slidev/theme-default': 'catalog:',
130
+ '@slidev/theme-seriph': 'catalog:',
131
+ '@slidev/theme-apple-basic': 'catalog:',
132
+ vue: 'catalog:',
133
+ } as Record<string, string>,
134
+ devDependencies: {},
135
+ };
136
+
137
+ if (sharedExists) {
138
+ catalogPackageJson.dependencies['@supaslidev/shared'] = 'workspace:*';
139
+ }
140
+
141
+ writeFileSync(packageJsonPath, JSON.stringify(catalogPackageJson, null, 2) + '\n');
142
+
143
+ regeneratePresentationsJson(presentationsDir, getPresentationsJsonPath());
144
+
145
+ const presentation = {
146
+ id: name,
147
+ title: frontmatter.title || name,
148
+ description: extractDescription(frontmatter.info) || '',
149
+ theme: template || 'default',
150
+ background: frontmatter.background || 'https://cover.sli.dev',
151
+ duration: frontmatter.duration || '',
152
+ };
153
+
154
+ // Respond immediately so the UI updates, then run pnpm install in the
155
+ // background to register the new workspace package.
156
+ resolve({ success: true, presentation });
157
+
158
+ const install = spawn('pnpm', ['install'], {
159
+ cwd: projectRoot,
160
+ stdio: 'inherit',
161
+ shell: true,
162
+ });
163
+
164
+ install.on('close', (installCode: number | null) => {
165
+ if (installCode !== 0) {
166
+ console.error(`[create] pnpm install failed with code ${installCode}`);
167
+ }
168
+ });
169
+
170
+ install.on('error', (err: Error) => {
171
+ console.error(`[create] pnpm install error:`, err);
172
+ });
173
+ });
174
+ });
175
+ });