@kya-os/create-mcpi-app 1.7.38-canary.2 → 1.7.38

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/.turbo/turbo-build.log +4 -0
  2. package/.turbo/turbo-test$colon$coverage.log +755 -0
  3. package/.turbo/turbo-test.log +200 -0
  4. package/dist/helpers/fetch-cloudflare-mcpi-template.d.ts.map +1 -1
  5. package/dist/helpers/fetch-cloudflare-mcpi-template.js +35 -914
  6. package/dist/helpers/fetch-cloudflare-mcpi-template.js.map +1 -1
  7. package/dist/utils/fetch-remote-config.d.ts.map +1 -1
  8. package/dist/utils/fetch-remote-config.js +2 -2
  9. package/dist/utils/fetch-remote-config.js.map +1 -1
  10. package/package/package.json +77 -0
  11. package/package.json +1 -1
  12. package/ARCHITECTURE_ANALYSIS.md +0 -392
  13. package/CHANGELOG.md +0 -372
  14. package/DEPRECATION_WARNINGS_ANALYSIS.md +0 -192
  15. package/IMPLEMENTATION_SUMMARY.md +0 -108
  16. package/REMEDIATION_PLAN.md +0 -99
  17. package/dist/.tsbuildinfo +0 -1
  18. package/scripts/prepare-pack.js +0 -47
  19. package/scripts/validate-no-workspace.js +0 -79
  20. package/src/__tests__/cloudflare-template.test.ts +0 -490
  21. package/src/__tests__/helpers/fetch-cloudflare-mcpi-template.test.ts +0 -337
  22. package/src/__tests__/helpers/generate-config.test.ts +0 -312
  23. package/src/__tests__/helpers/generate-identity.test.ts +0 -271
  24. package/src/__tests__/helpers/install.test.ts +0 -370
  25. package/src/__tests__/helpers/validate-project-structure.test.ts +0 -467
  26. package/src/__tests__.bak/regression.test.ts +0 -434
  27. package/src/effects/index.ts +0 -80
  28. package/src/helpers/__tests__/config-builder.spec.ts +0 -231
  29. package/src/helpers/apply-identity-preset.ts +0 -209
  30. package/src/helpers/config-builder.ts +0 -165
  31. package/src/helpers/copy-template.ts +0 -11
  32. package/src/helpers/create.ts +0 -239
  33. package/src/helpers/fetch-cloudflare-mcpi-template.ts +0 -2404
  34. package/src/helpers/fetch-cloudflare-template.ts +0 -361
  35. package/src/helpers/fetch-mcpi-template.ts +0 -236
  36. package/src/helpers/fetch-xmcp-template.ts +0 -153
  37. package/src/helpers/generate-config.ts +0 -118
  38. package/src/helpers/generate-identity.ts +0 -163
  39. package/src/helpers/identity-manager.ts +0 -186
  40. package/src/helpers/install.ts +0 -79
  41. package/src/helpers/rename.ts +0 -17
  42. package/src/helpers/validate-project-structure.ts +0 -127
  43. package/src/index.ts +0 -520
  44. package/src/utils/__tests__/fetch-remote-config.test.ts +0 -271
  45. package/src/utils/check-node.ts +0 -17
  46. package/src/utils/fetch-remote-config.ts +0 -179
  47. package/src/utils/is-folder-empty.ts +0 -60
  48. package/src/utils/validate-project-name.ts +0 -132
  49. package/test-cloudflare/README.md +0 -164
  50. package/test-cloudflare/package.json +0 -28
  51. package/test-cloudflare/src/index.ts +0 -341
  52. package/test-cloudflare/src/tools/greet.ts +0 -19
  53. package/test-cloudflare/tests/cache-invalidation.test.ts +0 -410
  54. package/test-cloudflare/tests/cors-security.test.ts +0 -349
  55. package/test-cloudflare/tests/delegation.test.ts +0 -335
  56. package/test-cloudflare/tests/do-routing.test.ts +0 -314
  57. package/test-cloudflare/tests/integration.test.ts +0 -205
  58. package/test-cloudflare/tests/session-management.test.ts +0 -359
  59. package/test-cloudflare/tsconfig.json +0 -16
  60. package/test-cloudflare/vitest.config.ts +0 -9
  61. package/test-cloudflare/wrangler.toml +0 -37
  62. package/test-node/README.md +0 -44
  63. package/test-node/package.json +0 -23
  64. package/test-node/src/tools/greet.ts +0 -25
  65. package/test-node/xmcp.config.ts +0 -20
  66. package/tsconfig.json +0 -26
  67. package/vitest.config.ts +0 -14
@@ -1,271 +0,0 @@
1
- /**
2
- * Tests for Remote Configuration Fetching
3
- *
4
- * Validates that remote config fetching works correctly with caching
5
- * and fallback behavior.
6
- *
7
- * These tests mirror the tests in @kya-os/mcp-i-core to ensure
8
- * behavior matches the original implementation.
9
- */
10
-
11
- import { describe, it, expect, beforeEach, vi } from 'vitest';
12
- import {
13
- fetchRemoteConfig,
14
- type RemoteConfigOptions,
15
- type RemoteConfigCache
16
- } from '../fetch-remote-config.js';
17
- import type { MCPIConfig } from '@kya-os/contracts/config';
18
-
19
- describe('fetchRemoteConfig', () => {
20
- let mockFetch: ReturnType<typeof vi.fn>;
21
- let mockCache: RemoteConfigCache;
22
-
23
- beforeEach(() => {
24
- mockFetch = vi.fn();
25
- mockCache = {
26
- get: vi.fn(),
27
- set: vi.fn()
28
- };
29
- });
30
-
31
- describe('Cache hit scenario', () => {
32
- it('should return cached config if available and not expired', async () => {
33
- const cachedConfig: MCPIConfig = {
34
- environment: 'production',
35
- identity: { enabled: true, environment: 'production' }
36
- };
37
-
38
- vi.mocked(mockCache.get).mockResolvedValue(
39
- JSON.stringify({
40
- config: cachedConfig,
41
- expiresAt: Date.now() + 60000 // 1 minute in future
42
- })
43
- );
44
-
45
- const options: RemoteConfigOptions = {
46
- apiUrl: 'https://kya.vouched.id',
47
- apiKey: 'test-key',
48
- projectId: 'test-project',
49
- fetchProvider: mockFetch
50
- };
51
-
52
- const result = await fetchRemoteConfig(options, mockCache);
53
-
54
- expect(result).toEqual(cachedConfig);
55
- expect(mockCache.get).toHaveBeenCalledWith('config:project:test-project');
56
- expect(mockFetch).not.toHaveBeenCalled();
57
- });
58
-
59
- it('should fetch fresh config if cache expired', async () => {
60
- const cachedConfig: MCPIConfig = {
61
- environment: 'production'
62
- };
63
-
64
- vi.mocked(mockCache.get).mockResolvedValue(
65
- JSON.stringify({
66
- config: cachedConfig,
67
- expiresAt: Date.now() - 1000 // Expired
68
- })
69
- );
70
-
71
- const freshConfig: MCPIConfig = {
72
- environment: 'production',
73
- identity: { enabled: true, environment: 'production' }
74
- };
75
-
76
- mockFetch.mockResolvedValue({
77
- ok: true,
78
- json: async () => ({ success: true, data: freshConfig })
79
- } as Response);
80
-
81
- const options: RemoteConfigOptions = {
82
- apiUrl: 'https://kya.vouched.id',
83
- apiKey: 'test-key',
84
- projectId: 'test-project',
85
- fetchProvider: mockFetch
86
- };
87
-
88
- const result = await fetchRemoteConfig(options, mockCache);
89
-
90
- expect(result).toEqual(freshConfig);
91
- expect(mockFetch).toHaveBeenCalled();
92
- });
93
- });
94
-
95
- describe('Cache miss scenario', () => {
96
- it('should fetch from API when cache is empty', async () => {
97
- vi.mocked(mockCache.get).mockResolvedValue(null);
98
-
99
- const config: MCPIConfig = {
100
- environment: 'production',
101
- identity: { enabled: true, environment: 'production' }
102
- };
103
-
104
- mockFetch.mockResolvedValue({
105
- ok: true,
106
- json: async () => ({ success: true, data: config })
107
- } as Response);
108
-
109
- const options: RemoteConfigOptions = {
110
- apiUrl: 'https://kya.vouched.id',
111
- apiKey: 'test-key',
112
- projectId: 'test-project',
113
- fetchProvider: mockFetch
114
- };
115
-
116
- const result = await fetchRemoteConfig(options, mockCache);
117
-
118
- expect(result).toEqual(config);
119
- expect(mockCache.set).toHaveBeenCalled();
120
- });
121
-
122
- it('should use agentDid when projectId not available', async () => {
123
- vi.mocked(mockCache.get).mockResolvedValue(null);
124
-
125
- const config: MCPIConfig = {
126
- environment: 'production'
127
- };
128
-
129
- mockFetch.mockResolvedValue({
130
- ok: true,
131
- json: async () => ({ success: true, data: config })
132
- } as Response);
133
-
134
- const options: RemoteConfigOptions = {
135
- apiUrl: 'https://kya.vouched.id',
136
- apiKey: 'test-key',
137
- agentDid: 'did:key:test',
138
- fetchProvider: mockFetch
139
- };
140
-
141
- const result = await fetchRemoteConfig(options, mockCache);
142
-
143
- expect(result).toEqual(config);
144
- expect(mockCache.get).toHaveBeenCalledWith('config:agent:did:key:test');
145
- });
146
- });
147
-
148
- describe('Error handling', () => {
149
- it('should return null if API request fails', async () => {
150
- vi.mocked(mockCache.get).mockResolvedValue(null);
151
-
152
- mockFetch.mockResolvedValue({
153
- ok: false,
154
- status: 404,
155
- statusText: 'Not Found'
156
- } as Response);
157
-
158
- const options: RemoteConfigOptions = {
159
- apiUrl: 'https://kya.vouched.id',
160
- apiKey: 'test-key',
161
- projectId: 'test-project',
162
- fetchProvider: mockFetch
163
- };
164
-
165
- const result = await fetchRemoteConfig(options, mockCache);
166
-
167
- expect(result).toBeNull();
168
- });
169
-
170
- it('should return null if API throws error', async () => {
171
- vi.mocked(mockCache.get).mockResolvedValue(null);
172
-
173
- mockFetch.mockRejectedValue(new Error('Network error'));
174
-
175
- const options: RemoteConfigOptions = {
176
- apiUrl: 'https://kya.vouched.id',
177
- apiKey: 'test-key',
178
- projectId: 'test-project',
179
- fetchProvider: mockFetch
180
- };
181
-
182
- const result = await fetchRemoteConfig(options, mockCache);
183
-
184
- expect(result).toBeNull();
185
- });
186
-
187
- it('should return null if neither projectId nor agentDid provided', async () => {
188
- const options: RemoteConfigOptions = {
189
- apiUrl: 'https://kya.vouched.id',
190
- apiKey: 'test-key',
191
- fetchProvider: mockFetch
192
- };
193
-
194
- const result = await fetchRemoteConfig(options, mockCache);
195
-
196
- expect(result).toBeNull();
197
- expect(mockFetch).not.toHaveBeenCalled();
198
- });
199
-
200
- it('should handle cache read errors gracefully', async () => {
201
- vi.mocked(mockCache.get).mockRejectedValue(new Error('Cache error'));
202
-
203
- const config: MCPIConfig = {
204
- environment: 'production'
205
- };
206
-
207
- mockFetch.mockResolvedValue({
208
- ok: true,
209
- json: async () => ({ success: true, data: config })
210
- } as Response);
211
-
212
- const options: RemoteConfigOptions = {
213
- apiUrl: 'https://kya.vouched.id',
214
- apiKey: 'test-key',
215
- projectId: 'test-project',
216
- fetchProvider: mockFetch
217
- };
218
-
219
- const result = await fetchRemoteConfig(options, mockCache);
220
-
221
- expect(result).toEqual(config);
222
- });
223
- });
224
-
225
- describe('Response format handling', () => {
226
- it('should handle different API response formats', async () => {
227
- vi.mocked(mockCache.get).mockResolvedValue(null);
228
-
229
- const config: MCPIConfig = {
230
- environment: 'production'
231
- };
232
-
233
- // Format 1: { config: {...} }
234
- mockFetch.mockResolvedValueOnce({
235
- ok: true,
236
- json: async () => ({ config })
237
- } as Response);
238
-
239
- const options: RemoteConfigOptions = {
240
- apiUrl: 'https://kya.vouched.id',
241
- apiKey: 'test-key',
242
- projectId: 'test-project',
243
- fetchProvider: mockFetch
244
- };
245
-
246
- const result1 = await fetchRemoteConfig(options, mockCache);
247
- expect(result1).toEqual(config);
248
-
249
- // Format 2: { data: { config: {...} } }
250
- vi.mocked(mockCache.get).mockResolvedValue(null);
251
- mockFetch.mockResolvedValueOnce({
252
- ok: true,
253
- json: async () => ({ data: { config } })
254
- } as Response);
255
-
256
- const result2 = await fetchRemoteConfig(options, mockCache);
257
- expect(result2).toEqual(config);
258
-
259
- // Format 3: { success: true, data: {...} }
260
- vi.mocked(mockCache.get).mockResolvedValue(null);
261
- mockFetch.mockResolvedValueOnce({
262
- ok: true,
263
- json: async () => ({ success: true, data: config })
264
- } as Response);
265
-
266
- const result3 = await fetchRemoteConfig(options, mockCache);
267
- expect(result3).toEqual(config);
268
- });
269
- });
270
- });
271
-
@@ -1,17 +0,0 @@
1
- import chalk from "chalk";
2
-
3
- export function checkNodeVersion(): void {
4
- const currentNodeVersion = process.versions.node;
5
- const [major] = currentNodeVersion.split(".").map(Number);
6
-
7
- if (major < 20) {
8
- console.error(
9
- chalk.red(
10
- `Error: Node.js version ${currentNodeVersion} is not supported.\n` +
11
- `create-mcpi-app requires Node.js 20.0.0 or higher.\n` +
12
- `Please update your Node.js version.`
13
- )
14
- );
15
- process.exit(1);
16
- }
17
- }
@@ -1,179 +0,0 @@
1
- /**
2
- * Remote Configuration Fetching
3
- *
4
- * Service for fetching configuration from remote APIs (AgentShield dashboard)
5
- * with caching support for performance optimization.
6
- *
7
- * NOTE: This implementation is extracted from @kya-os/mcp-i-core/src/config/remote-config.ts
8
- * to avoid runtime dependency on the entire mcp-i-core package.
9
- *
10
- * Source: packages/mcp-i-core/src/config/remote-config.ts
11
- *
12
- * @module @kya-os/create-mcpi-app/utils/fetch-remote-config
13
- */
14
-
15
- import type { MCPIConfig } from '@kya-os/contracts/config';
16
- import { AGENTSHIELD_ENDPOINTS } from '@kya-os/contracts/agentshield-api';
17
-
18
- /**
19
- * Options for fetching remote configuration
20
- */
21
- export interface RemoteConfigOptions {
22
- /**
23
- * API base URL
24
- * @example 'https://kya.vouched.id'
25
- */
26
- apiUrl: string;
27
-
28
- /**
29
- * API key for authentication
30
- */
31
- apiKey: string;
32
-
33
- /**
34
- * Project ID (optional, preferred over agentDid)
35
- * Used for project-scoped configuration
36
- */
37
- projectId?: string;
38
-
39
- /**
40
- * Agent DID (optional, used when projectId not available)
41
- * Used for agent-scoped configuration
42
- */
43
- agentDid?: string;
44
-
45
- /**
46
- * Cache TTL in milliseconds
47
- * @default 300000 (5 minutes)
48
- */
49
- cacheTtl?: number;
50
-
51
- /**
52
- * Fetch provider function
53
- * Platform-agnostic fetch implementation
54
- */
55
- fetchProvider: (url: string, options: RequestInit) => Promise<Response>;
56
- }
57
-
58
- /**
59
- * Cache interface for remote configuration
60
- * Abstracts platform-specific caching (KV, Redis, Memory, etc.)
61
- */
62
- export interface RemoteConfigCache {
63
- /**
64
- * Get a cached value
65
- */
66
- get(key: string): Promise<string | null>;
67
-
68
- /**
69
- * Set a cached value with TTL
70
- */
71
- set(key: string, value: string, ttl: number): Promise<void>;
72
- }
73
-
74
- /**
75
- * Fetch configuration from remote API (AgentShield dashboard)
76
- *
77
- * Attempts to fetch configuration from the AgentShield API with caching support.
78
- * Falls back gracefully if remote fetch fails.
79
- *
80
- * @param options - Remote config options
81
- * @param cache - Optional cache implementation
82
- * @returns Configuration object or null if fetch fails
83
- */
84
- export async function fetchRemoteConfig(
85
- options: RemoteConfigOptions,
86
- cache?: RemoteConfigCache
87
- ): Promise<MCPIConfig | null> {
88
- const { apiUrl, apiKey, projectId, agentDid, cacheTtl = 300000, fetchProvider } = options;
89
-
90
- // Generate cache key
91
- const cacheKey = projectId
92
- ? `config:project:${projectId}`
93
- : agentDid
94
- ? `config:agent:${agentDid}`
95
- : null;
96
-
97
- // Try cache first
98
- if (cache && cacheKey) {
99
- try {
100
- const cached = await cache.get(cacheKey);
101
- if (cached) {
102
- try {
103
- const parsed = JSON.parse(cached) as { config: MCPIConfig; expiresAt: number };
104
- if (parsed.expiresAt > Date.now()) {
105
- return parsed.config;
106
- }
107
- } catch {
108
- // Invalid cache entry, continue to fetch
109
- }
110
- }
111
- } catch (error) {
112
- // Cache read failed, continue to fetch
113
- console.warn('[RemoteConfig] Cache read failed:', error);
114
- }
115
- }
116
-
117
- // Fetch from API
118
- try {
119
- // Build API URL
120
- let url: string;
121
- if (projectId) {
122
- // Use project-scoped endpoint (preferred)
123
- url = `${apiUrl}${AGENTSHIELD_ENDPOINTS.CONFIG(projectId)}`;
124
- } else if (agentDid) {
125
- // Use agent-scoped endpoint
126
- url = `${apiUrl}/api/v1/bouncer/config?agent_did=${encodeURIComponent(agentDid)}`;
127
- } else {
128
- console.warn('[RemoteConfig] Neither projectId nor agentDid provided');
129
- return null;
130
- }
131
-
132
- const response = await fetchProvider(url, {
133
- headers: {
134
- 'Authorization': `Bearer ${apiKey}`,
135
- 'Content-Type': 'application/json'
136
- }
137
- });
138
-
139
- if (!response.ok) {
140
- console.warn(`[RemoteConfig] API returned ${response.status}: ${response.statusText}`);
141
- return null;
142
- }
143
-
144
- const data = await response.json();
145
-
146
- // Extract config from API response
147
- // API response format: { success: boolean, data: { config: MCPIConfig } }
148
- const responseData = data as { config?: MCPIConfig; data?: { config?: MCPIConfig }; success?: boolean };
149
- const config = responseData.config || responseData.data?.config || (responseData.success ? responseData.data as MCPIConfig | null : null) as MCPIConfig | null;
150
-
151
- if (!config) {
152
- console.warn('[RemoteConfig] No config found in API response');
153
- return null;
154
- }
155
-
156
- // Cache the result
157
- if (cache && cacheKey) {
158
- try {
159
- await cache.set(
160
- cacheKey,
161
- JSON.stringify({
162
- config,
163
- expiresAt: Date.now() + cacheTtl
164
- }),
165
- cacheTtl
166
- );
167
- } catch (error) {
168
- // Cache write failed, but we got the config so continue
169
- console.warn('[RemoteConfig] Cache write failed:', error);
170
- }
171
- }
172
-
173
- return config as MCPIConfig;
174
- } catch (error) {
175
- console.warn('[RemoteConfig] Failed to fetch config:', error);
176
- return null;
177
- }
178
- }
179
-
@@ -1,60 +0,0 @@
1
- import fs from "fs";
2
- import path from "path";
3
-
4
- export function isFolderEmpty(root: string, name: string): boolean {
5
- const validFiles = [
6
- ".DS_Store",
7
- ".git",
8
- ".gitattributes",
9
- ".gitignore",
10
- ".gitlab-ci.yml",
11
- ".hg",
12
- ".hgcheck",
13
- ".hgignore",
14
- ".idea",
15
- ".npmignore",
16
- ".travis.yml",
17
- "LICENSE",
18
- "Makefile",
19
- "README.md",
20
- "Thumbs.db",
21
- "docs",
22
- "mkdocs.yml",
23
- "npm-debug.log",
24
- "yarn-debug.log",
25
- "yarn-error.log",
26
- ];
27
-
28
- const conflicts = fs
29
- .readdirSync(root)
30
- .filter((file) => !validFiles.includes(file))
31
- // Support IntelliJ IDEA-based editors
32
- .filter((file) => !/\.iml$/.test(file));
33
-
34
- if (conflicts.length > 0) {
35
- console.log(
36
- `The directory ${name} contains conflicting files:\n`
37
- );
38
- console.log();
39
- for (const file of conflicts) {
40
- try {
41
- const stats = fs.lstatSync(path.join(root, file));
42
- if (stats.isDirectory()) {
43
- console.log(` ${file}/`);
44
- } else {
45
- console.log(` ${file}`);
46
- }
47
- } catch {
48
- console.log(` ${file}`);
49
- }
50
- }
51
- console.log();
52
- console.log(
53
- "Either try using a new directory name, or remove the conflicting files."
54
- );
55
- console.log();
56
- return false;
57
- }
58
-
59
- return true;
60
- }
@@ -1,132 +0,0 @@
1
- import fs from "fs-extra";
2
- import path from "path";
3
-
4
- /**
5
- * Validate project name meets requirements
6
- */
7
- export function validateProjectName(name: string): {
8
- valid: boolean;
9
- issues: string[];
10
- } {
11
- const issues: string[] = [];
12
-
13
- // Check if name is empty
14
- if (!name || name.trim().length === 0) {
15
- issues.push("Project name cannot be empty");
16
- return { valid: false, issues };
17
- }
18
-
19
- const trimmedName = name.trim();
20
-
21
- // Check length
22
- if (trimmedName.length < 1) {
23
- issues.push("Project name must be at least 1 character long");
24
- }
25
-
26
- if (trimmedName.length > 214) {
27
- issues.push("Project name must be less than 214 characters");
28
- }
29
-
30
- // Check for invalid characters (npm package name rules)
31
- if (!/^[a-z0-9@._/-]+$/.test(trimmedName)) {
32
- issues.push(
33
- "Project name can only contain lowercase letters, numbers, and the characters @._/-"
34
- );
35
- }
36
-
37
- // Check if it starts with . or _
38
- if (trimmedName.startsWith(".") || trimmedName.startsWith("_")) {
39
- issues.push("Project name cannot start with . or _");
40
- }
41
-
42
- // Check for reserved names
43
- const reservedNames = [
44
- "node_modules",
45
- "favicon.ico",
46
- "package.json",
47
- "package-lock.json",
48
- "yarn.lock",
49
- "pnpm-lock.yaml",
50
- ".git",
51
- ".gitignore",
52
- ".env",
53
- "readme",
54
- "readme.md",
55
- "readme.txt",
56
- "license",
57
- "license.md",
58
- "license.txt",
59
- "changelog",
60
- "changelog.md",
61
- "changelog.txt",
62
- ];
63
-
64
- if (reservedNames.includes(trimmedName.toLowerCase())) {
65
- issues.push(
66
- `"${trimmedName}" is a reserved name and cannot be used as a project name`
67
- );
68
- }
69
-
70
- // Check for scoped package format
71
- if (trimmedName.startsWith("@")) {
72
- const parts = trimmedName.split("/");
73
- if (parts.length !== 2) {
74
- issues.push("Scoped package names must be in the format @scope/name");
75
- } else if (parts[1].length === 0) {
76
- issues.push("Scoped package name cannot have empty package name");
77
- }
78
- }
79
-
80
- return {
81
- valid: issues.length === 0,
82
- issues,
83
- };
84
- }
85
-
86
- /**
87
- * Check if directory is available for project creation
88
- */
89
- export function validateDirectoryAvailability(projectPath: string): {
90
- valid: boolean;
91
- issues: string[];
92
- } {
93
- const issues: string[] = [];
94
-
95
- try {
96
- // Check if path exists
97
- if (fs.existsSync(projectPath)) {
98
- const stats = fs.statSync(projectPath);
99
-
100
- if (!stats.isDirectory()) {
101
- issues.push(`Path exists but is not a directory: ${projectPath}`);
102
- } else {
103
- // Check if directory is empty
104
- const files = fs.readdirSync(projectPath);
105
- const nonHiddenFiles = files.filter(
106
- (file: string) => !file.startsWith(".")
107
- );
108
-
109
- if (nonHiddenFiles.length > 0) {
110
- issues.push(`Directory is not empty: ${projectPath}`);
111
- }
112
- }
113
- }
114
-
115
- // Check if parent directory is writable
116
- const parentDir = path.dirname(projectPath);
117
- try {
118
- fs.accessSync(parentDir, fs.constants.W_OK);
119
- } catch {
120
- issues.push(`Parent directory is not writable: ${parentDir}`);
121
- }
122
- } catch (error) {
123
- issues.push(
124
- `Error checking directory: ${error instanceof Error ? error.message : String(error)}`
125
- );
126
- }
127
-
128
- return {
129
- valid: issues.length === 0,
130
- issues,
131
- };
132
- }