@teardown/cli 1.2.38 → 2.0.41

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 (182) hide show
  1. package/bin/teardown.js +11 -1
  2. package/package.json +77 -57
  3. package/src/cli/commands/init.ts +254 -0
  4. package/src/cli/commands/plugins.ts +93 -0
  5. package/src/cli/commands/prebuild.ts +168 -0
  6. package/src/cli/commands/run.ts +727 -0
  7. package/src/cli/commands/start.ts +87 -0
  8. package/src/cli/commands/validate.ts +62 -0
  9. package/src/cli/index.ts +59 -0
  10. package/src/config/index.ts +45 -0
  11. package/src/config/loader.ts +366 -0
  12. package/src/config/schema.ts +235 -0
  13. package/src/config/types.ts +322 -0
  14. package/src/index.ts +177 -0
  15. package/src/pipeline/cache.ts +179 -0
  16. package/src/pipeline/index.ts +10 -0
  17. package/src/pipeline/stages.ts +692 -0
  18. package/src/plugins/base.ts +370 -0
  19. package/src/plugins/capabilities/biometrics.ts +64 -0
  20. package/src/plugins/capabilities/bluetooth.ts +86 -0
  21. package/src/plugins/capabilities/calendar.ts +57 -0
  22. package/src/plugins/capabilities/camera.ts +77 -0
  23. package/src/plugins/capabilities/contacts.ts +57 -0
  24. package/src/plugins/capabilities/deep-linking.ts +124 -0
  25. package/src/plugins/capabilities/firebase.ts +138 -0
  26. package/src/plugins/capabilities/index.ts +96 -0
  27. package/src/plugins/capabilities/location.ts +87 -0
  28. package/src/plugins/capabilities/photo-library.ts +80 -0
  29. package/src/plugins/capabilities/push-notifications.ts +98 -0
  30. package/src/plugins/capabilities/sign-in-with-apple.ts +53 -0
  31. package/src/plugins/context.ts +220 -0
  32. package/src/plugins/index.ts +26 -0
  33. package/src/plugins/resolver.ts +321 -0
  34. package/src/templates/generator.ts +507 -0
  35. package/src/templates/index.ts +9 -0
  36. package/src/templates/paths.ts +25 -0
  37. package/src/transformers/android/gradle.ts +400 -0
  38. package/src/transformers/android/index.ts +19 -0
  39. package/src/transformers/android/manifest.ts +506 -0
  40. package/src/transformers/index.ts +39 -0
  41. package/src/transformers/ios/entitlements.ts +283 -0
  42. package/src/transformers/ios/index.ts +10 -0
  43. package/src/transformers/ios/pbxproj.ts +267 -0
  44. package/src/transformers/ios/plist.ts +198 -0
  45. package/src/utils/fs.ts +429 -0
  46. package/src/utils/index.ts +21 -0
  47. package/src/utils/logger.ts +203 -0
  48. package/templates/.gitignore +63 -0
  49. package/templates/Gemfile +3 -0
  50. package/templates/android/app/build.gradle.kts +97 -0
  51. package/templates/android/app/proguard-rules.pro +10 -0
  52. package/templates/android/app/src/main/AndroidManifest.xml +26 -0
  53. package/templates/android/app/src/main/java/com/appname/MainActivity.kt +22 -0
  54. package/templates/android/app/src/main/java/com/appname/MainApplication.kt +44 -0
  55. package/templates/android/app/src/main/res/values/strings.xml +3 -0
  56. package/templates/android/app/src/main/res/values/styles.xml +7 -0
  57. package/templates/android/build.gradle.kts +44 -0
  58. package/templates/android/gradle.properties +39 -0
  59. package/templates/android/settings.gradle.kts +12 -0
  60. package/templates/babel.config.js +15 -0
  61. package/templates/index.js +7 -0
  62. package/templates/ios/.xcode.env +11 -0
  63. package/templates/ios/AppName/AppDelegate.swift +25 -0
  64. package/templates/ios/AppName/AppName-Bridging-Header.h +4 -0
  65. package/templates/ios/AppName/AppName.entitlements +6 -0
  66. package/templates/ios/AppName/Images.xcassets/AppIcon.appiconset/Contents.json +35 -0
  67. package/templates/ios/AppName/Images.xcassets/Contents.json +6 -0
  68. package/templates/ios/AppName/Info.plist +49 -0
  69. package/templates/ios/AppName/LaunchScreen.storyboard +38 -0
  70. package/templates/ios/AppName.xcodeproj/project.pbxproj +402 -0
  71. package/templates/ios/AppName.xcodeproj/xcshareddata/xcschemes/AppName.xcscheme +78 -0
  72. package/templates/ios/Podfile +35 -0
  73. package/templates/metro.config.js +41 -0
  74. package/templates/package.json +57 -0
  75. package/templates/react-native.config.js +8 -0
  76. package/templates/src/app/index.tsx +34 -0
  77. package/templates/src/assets/fonts/.gitkeep +1 -0
  78. package/templates/src/assets/images/.gitkeep +1 -0
  79. package/templates/src/components/ui/accordion.tsx +114 -0
  80. package/templates/src/components/ui/avatar.tsx +75 -0
  81. package/templates/src/components/ui/button.tsx +93 -0
  82. package/templates/src/components/ui/card.tsx +120 -0
  83. package/templates/src/components/ui/checkbox.tsx +133 -0
  84. package/templates/src/components/ui/chip.tsx +95 -0
  85. package/templates/src/components/ui/dialog.tsx +134 -0
  86. package/templates/src/components/ui/divider.tsx +67 -0
  87. package/templates/src/components/ui/error-view.tsx +82 -0
  88. package/templates/src/components/ui/form-field.tsx +101 -0
  89. package/templates/src/components/ui/index.ts +100 -0
  90. package/templates/src/components/ui/popover.tsx +92 -0
  91. package/templates/src/components/ui/pressable-feedback.tsx +88 -0
  92. package/templates/src/components/ui/radio-group.tsx +153 -0
  93. package/templates/src/components/ui/scroll-shadow.tsx +108 -0
  94. package/templates/src/components/ui/select.tsx +165 -0
  95. package/templates/src/components/ui/skeleton-group.tsx +97 -0
  96. package/templates/src/components/ui/skeleton.tsx +87 -0
  97. package/templates/src/components/ui/spinner.tsx +87 -0
  98. package/templates/src/components/ui/surface.tsx +95 -0
  99. package/templates/src/components/ui/switch.tsx +124 -0
  100. package/templates/src/components/ui/tabs.tsx +154 -0
  101. package/templates/src/components/ui/text-field.tsx +106 -0
  102. package/templates/src/components/ui/toast.tsx +129 -0
  103. package/templates/src/contexts/.gitkeep +2 -0
  104. package/templates/src/core/clients/api/api.client.ts +113 -0
  105. package/templates/src/core/clients/api/index.ts +1 -0
  106. package/templates/src/core/clients/storage/index.ts +1 -0
  107. package/templates/src/core/clients/storage/storage.client.ts +121 -0
  108. package/templates/src/core/constants/index.ts +19 -0
  109. package/templates/src/core/core.ts +40 -0
  110. package/templates/src/core/index.ts +10 -0
  111. package/templates/src/global.css +87 -0
  112. package/templates/src/hooks/index.ts +6 -0
  113. package/templates/src/hooks/use-debounce.ts +23 -0
  114. package/templates/src/hooks/use-mounted.ts +21 -0
  115. package/templates/src/index.ts +28 -0
  116. package/templates/src/lib/index.ts +5 -0
  117. package/templates/src/lib/utils.ts +115 -0
  118. package/templates/src/modules/.gitkeep +6 -0
  119. package/templates/src/navigation/index.ts +8 -0
  120. package/templates/src/navigation/navigation-provider.tsx +36 -0
  121. package/templates/src/navigation/router.tsx +137 -0
  122. package/templates/src/providers/app.provider.tsx +29 -0
  123. package/templates/src/providers/index.ts +5 -0
  124. package/templates/src/routes/(tabs)/_layout.tsx +42 -0
  125. package/templates/src/routes/(tabs)/explore.tsx +161 -0
  126. package/templates/src/routes/(tabs)/home.tsx +138 -0
  127. package/templates/src/routes/(tabs)/profile.tsx +151 -0
  128. package/templates/src/routes/_layout.tsx +18 -0
  129. package/templates/src/routes/settings.tsx +194 -0
  130. package/templates/src/screens/auth/index.ts +6 -0
  131. package/templates/src/screens/auth/login.tsx +165 -0
  132. package/templates/src/screens/auth/register.tsx +203 -0
  133. package/templates/src/screens/home.tsx +204 -0
  134. package/templates/src/screens/index.ts +17 -0
  135. package/templates/src/screens/profile.tsx +210 -0
  136. package/templates/src/screens/settings.tsx +216 -0
  137. package/templates/src/screens/welcome.tsx +101 -0
  138. package/templates/src/styles/index.ts +103 -0
  139. package/templates/src/types/common.ts +71 -0
  140. package/templates/src/types/index.ts +5 -0
  141. package/templates/tsconfig.json +14 -0
  142. package/README.md +0 -15
  143. package/assets/favicon.ico +0 -0
  144. package/dist/commands/dev/dev.js +0 -55
  145. package/dist/commands/init/init-teardown.js +0 -26
  146. package/dist/index.js +0 -20
  147. package/dist/modules/dev/dev-menu/keyboard-handler.js +0 -138
  148. package/dist/modules/dev/dev-menu/open-debugger-keyboard-handler.js +0 -105
  149. package/dist/modules/dev/dev-server/cdp/cdp.adapter.js +0 -12
  150. package/dist/modules/dev/dev-server/cdp/index.js +0 -18
  151. package/dist/modules/dev/dev-server/cdp/types.js +0 -2
  152. package/dist/modules/dev/dev-server/dev-server-checker.js +0 -72
  153. package/dist/modules/dev/dev-server/dev-server.js +0 -269
  154. package/dist/modules/dev/dev-server/inspector/device.event-reporter.js +0 -165
  155. package/dist/modules/dev/dev-server/inspector/device.js +0 -577
  156. package/dist/modules/dev/dev-server/inspector/inspector.js +0 -204
  157. package/dist/modules/dev/dev-server/inspector/types.js +0 -2
  158. package/dist/modules/dev/dev-server/inspector/wss/servers/debugger-connection.server.js +0 -61
  159. package/dist/modules/dev/dev-server/inspector/wss/servers/device-connection.server.js +0 -64
  160. package/dist/modules/dev/dev-server/plugins/devtools.plugin.js +0 -50
  161. package/dist/modules/dev/dev-server/plugins/favicon.plugin.js +0 -19
  162. package/dist/modules/dev/dev-server/plugins/multipart.plugin.js +0 -62
  163. package/dist/modules/dev/dev-server/plugins/systrace.plugin.js +0 -28
  164. package/dist/modules/dev/dev-server/plugins/types.js +0 -2
  165. package/dist/modules/dev/dev-server/plugins/wss/index.js +0 -19
  166. package/dist/modules/dev/dev-server/plugins/wss/servers/web-socket-api.server.js +0 -66
  167. package/dist/modules/dev/dev-server/plugins/wss/servers/web-socket-debugger.server.js +0 -128
  168. package/dist/modules/dev/dev-server/plugins/wss/servers/web-socket-dev-client.server.js +0 -75
  169. package/dist/modules/dev/dev-server/plugins/wss/servers/web-socket-events.server.js +0 -198
  170. package/dist/modules/dev/dev-server/plugins/wss/servers/web-socket-hmr.server.js +0 -120
  171. package/dist/modules/dev/dev-server/plugins/wss/servers/web-socket-message.server.js +0 -357
  172. package/dist/modules/dev/dev-server/plugins/wss/types.js +0 -2
  173. package/dist/modules/dev/dev-server/plugins/wss/web-socket-router.js +0 -57
  174. package/dist/modules/dev/dev-server/plugins/wss/web-socket-server-adapter.js +0 -26
  175. package/dist/modules/dev/dev-server/plugins/wss/web-socket-server.js +0 -46
  176. package/dist/modules/dev/dev-server/plugins/wss/wss.plugin.js +0 -55
  177. package/dist/modules/dev/dev-server/sybmolicate/sybmolicate.plugin.js +0 -36
  178. package/dist/modules/dev/dev-server/sybmolicate/types.js +0 -2
  179. package/dist/modules/dev/terminal/base.terminal.reporter.js +0 -78
  180. package/dist/modules/dev/terminal/terminal.reporter.js +0 -76
  181. package/dist/modules/dev/types.js +0 -2
  182. package/dist/modules/dev/utils/log.js +0 -73
@@ -0,0 +1,198 @@
1
+ import * as plist from "simple-plist";
2
+ import type { ProcessedAppConfig } from "../../config/types";
3
+ import type { PluginContext } from "../../plugins/context";
4
+
5
+ /**
6
+ * Info.plist structure
7
+ */
8
+ export interface InfoPlistContent {
9
+ CFBundleName: string;
10
+ CFBundleDisplayName: string;
11
+ CFBundleIdentifier: string;
12
+ CFBundleVersion: string;
13
+ CFBundleShortVersionString: string;
14
+ CFBundlePackageType: string;
15
+ CFBundleExecutable: string;
16
+ UILaunchStoryboardName: string;
17
+ UIRequiredDeviceCapabilities: string[];
18
+ UISupportedInterfaceOrientations: string[];
19
+ UIStatusBarStyle?: string;
20
+ UIViewControllerBasedStatusBarAppearance?: boolean;
21
+ UIBackgroundModes?: string[];
22
+ LSApplicationQueriesSchemes?: string[];
23
+ CFBundleURLTypes?: CFBundleURLType[];
24
+ NSAppTransportSecurity?: {
25
+ NSAllowsArbitraryLoads?: boolean;
26
+ NSAllowsLocalNetworking?: boolean;
27
+ NSExceptionDomains?: Record<string, unknown>;
28
+ };
29
+ [key: string]: unknown;
30
+ }
31
+
32
+ export interface CFBundleURLType {
33
+ CFBundleURLName?: string;
34
+ CFBundleURLSchemes: string[];
35
+ CFBundleTypeRole?: string;
36
+ }
37
+
38
+ /**
39
+ * Plist transformer for iOS Info.plist files
40
+ */
41
+ export class PlistTransformer {
42
+ /**
43
+ * Transform the Info.plist file with configuration
44
+ */
45
+ async transform(context: PluginContext, config: ProcessedAppConfig): Promise<void> {
46
+ const { fs, log, iosPaths } = context;
47
+
48
+ log.debug("Transforming Info.plist...");
49
+
50
+ let existingPlist: InfoPlistContent;
51
+
52
+ try {
53
+ const content = await fs.readFile(iosPaths.infoPlist);
54
+ existingPlist = plist.parse(content) as InfoPlistContent;
55
+ } catch {
56
+ // Create default plist if doesn't exist
57
+ existingPlist = this.createDefaultPlist(config);
58
+ }
59
+
60
+ // Apply configuration
61
+ const updatedPlist = this.applyConfig(existingPlist, config);
62
+
63
+ // Write back
64
+ const plistContent = plist.stringify(updatedPlist);
65
+ await fs.writeFile(iosPaths.infoPlist, plistContent);
66
+
67
+ log.debug("Info.plist transformation complete");
68
+ }
69
+
70
+ /**
71
+ * Create default Info.plist content
72
+ */
73
+ private createDefaultPlist(config: ProcessedAppConfig): InfoPlistContent {
74
+ const appName = config.name.replace(/[^a-zA-Z0-9]/g, "");
75
+
76
+ return {
77
+ CFBundleName: config.name,
78
+ CFBundleDisplayName: config.name,
79
+ CFBundleIdentifier: config.ios?.bundleIdentifier ?? "com.example.app",
80
+ CFBundleVersion: String(config.ios?.buildNumber ?? 1),
81
+ CFBundleShortVersionString: config.version,
82
+ CFBundlePackageType: "APPL",
83
+ CFBundleExecutable: appName,
84
+ UILaunchStoryboardName: "LaunchScreen",
85
+ UIRequiredDeviceCapabilities: ["arm64"],
86
+ UISupportedInterfaceOrientations: this.getOrientations(config),
87
+ };
88
+ }
89
+
90
+ /**
91
+ * Apply configuration to existing plist
92
+ */
93
+ private applyConfig(plist: InfoPlistContent, config: ProcessedAppConfig): InfoPlistContent {
94
+ // Update basic info
95
+ plist.CFBundleName = config.name;
96
+ plist.CFBundleDisplayName = config.name;
97
+ plist.CFBundleIdentifier = config.ios?.bundleIdentifier ?? plist.CFBundleIdentifier;
98
+ plist.CFBundleVersion = String(config.ios?.buildNumber ?? 1);
99
+ plist.CFBundleShortVersionString = config.version;
100
+
101
+ // Apply orientation
102
+ plist.UISupportedInterfaceOrientations = this.getOrientations(config);
103
+
104
+ // Apply user interface style
105
+ if (config.userInterfaceStyle === "light") {
106
+ plist.UIUserInterfaceStyle = "Light";
107
+ } else if (config.userInterfaceStyle === "dark") {
108
+ plist.UIUserInterfaceStyle = "Dark";
109
+ }
110
+
111
+ // Apply background modes
112
+ if (config.resolvedBackgroundModes.length > 0) {
113
+ plist.UIBackgroundModes = config.resolvedBackgroundModes;
114
+ }
115
+
116
+ // Apply URL schemes
117
+ if (config.resolvedUrlSchemes.length > 0 || config.scheme) {
118
+ const schemes = [...config.resolvedUrlSchemes];
119
+ if (config.scheme && !schemes.includes(config.scheme)) {
120
+ schemes.push(config.scheme);
121
+ }
122
+
123
+ plist.CFBundleURLTypes = plist.CFBundleURLTypes ?? [];
124
+
125
+ for (const scheme of schemes) {
126
+ // Check if scheme already exists
127
+ const existing = plist.CFBundleURLTypes.find((t) => t.CFBundleURLSchemes.includes(scheme));
128
+
129
+ if (!existing) {
130
+ plist.CFBundleURLTypes.push({
131
+ CFBundleURLName: config.ios?.bundleIdentifier ?? "",
132
+ CFBundleURLSchemes: [scheme],
133
+ CFBundleTypeRole: "Editor",
134
+ });
135
+ }
136
+ }
137
+ }
138
+
139
+ // Apply plugin-resolved Info.plist entries (permissions, etc.)
140
+ for (const [key, value] of Object.entries(config.resolvedInfoPlist)) {
141
+ plist[key] = value;
142
+ }
143
+
144
+ // Apply custom Info.plist entries from config
145
+ if (config.ios?.infoPlist) {
146
+ for (const [key, value] of Object.entries(config.ios.infoPlist)) {
147
+ plist[key] = value;
148
+ }
149
+ }
150
+
151
+ return plist;
152
+ }
153
+
154
+ /**
155
+ * Get supported interface orientations based on config
156
+ */
157
+ private getOrientations(config: ProcessedAppConfig): string[] {
158
+ const orientations: string[] = [];
159
+
160
+ switch (config.orientation) {
161
+ case "portrait":
162
+ orientations.push("UIInterfaceOrientationPortrait");
163
+ orientations.push("UIInterfaceOrientationPortraitUpsideDown");
164
+ break;
165
+ case "landscape":
166
+ orientations.push("UIInterfaceOrientationLandscapeLeft");
167
+ orientations.push("UIInterfaceOrientationLandscapeRight");
168
+ break;
169
+ default:
170
+ orientations.push(
171
+ "UIInterfaceOrientationPortrait",
172
+ "UIInterfaceOrientationPortraitUpsideDown",
173
+ "UIInterfaceOrientationLandscapeLeft",
174
+ "UIInterfaceOrientationLandscapeRight"
175
+ );
176
+ }
177
+
178
+ return orientations;
179
+ }
180
+
181
+ /**
182
+ * Read and parse an existing plist file
183
+ */
184
+ static async readPlist(filePath: string): Promise<InfoPlistContent> {
185
+ const fs = await import("node:fs/promises");
186
+ const content = await fs.readFile(filePath, "utf-8");
187
+ return plist.parse(content) as InfoPlistContent;
188
+ }
189
+
190
+ /**
191
+ * Write plist content to file
192
+ */
193
+ static async writePlist(filePath: string, content: InfoPlistContent): Promise<void> {
194
+ const fs = await import("node:fs/promises");
195
+ const plistContent = plist.stringify(content);
196
+ await fs.writeFile(filePath, plistContent, "utf-8");
197
+ }
198
+ }
@@ -0,0 +1,429 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import type { FileChange } from "../config/types";
4
+
5
+ /**
6
+ * Virtual file system that can operate in dry-run mode
7
+ *
8
+ * In normal mode, operations are performed on the real filesystem.
9
+ * In dry-run mode, operations are recorded but not applied.
10
+ */
11
+ export interface VirtualFileSystem {
12
+ /**
13
+ * Read a file
14
+ */
15
+ readFile(filePath: string): Promise<string>;
16
+
17
+ /**
18
+ * Read a file synchronously
19
+ */
20
+ readFileSync(filePath: string): string;
21
+
22
+ /**
23
+ * Write a file
24
+ */
25
+ writeFile(filePath: string, content: string): Promise<void>;
26
+
27
+ /**
28
+ * Write a file synchronously
29
+ */
30
+ writeFileSync(filePath: string, content: string): void;
31
+
32
+ /**
33
+ * Check if a file exists
34
+ */
35
+ exists(filePath: string): Promise<boolean>;
36
+
37
+ /**
38
+ * Check if a file exists synchronously
39
+ */
40
+ existsSync(filePath: string): boolean;
41
+
42
+ /**
43
+ * Create a directory recursively
44
+ */
45
+ mkdir(dirPath: string): Promise<void>;
46
+
47
+ /**
48
+ * Create a directory recursively synchronously
49
+ */
50
+ mkdirSync(dirPath: string): void;
51
+
52
+ /**
53
+ * Delete a file
54
+ */
55
+ unlink(filePath: string): Promise<void>;
56
+
57
+ /**
58
+ * Delete a file synchronously
59
+ */
60
+ unlinkSync(filePath: string): void;
61
+
62
+ /**
63
+ * Delete a directory recursively
64
+ */
65
+ rmdir(dirPath: string): Promise<void>;
66
+
67
+ /**
68
+ * Copy a file
69
+ */
70
+ copyFile(src: string, dest: string): Promise<void>;
71
+
72
+ /**
73
+ * Copy a file synchronously
74
+ */
75
+ copyFileSync(src: string, dest: string): void;
76
+
77
+ /**
78
+ * Get all recorded changes (for dry-run mode)
79
+ */
80
+ getChanges(): FileChange[];
81
+
82
+ /**
83
+ * Commit all pending changes to the real filesystem
84
+ */
85
+ commit(): Promise<void>;
86
+
87
+ /**
88
+ * Check if operating in dry-run mode
89
+ */
90
+ isDryRun(): boolean;
91
+ }
92
+
93
+ /**
94
+ * Create a virtual file system
95
+ */
96
+ export function createVirtualFileSystem(options: { dryRun?: boolean; projectRoot: string }): VirtualFileSystem {
97
+ const { dryRun = false, projectRoot } = options;
98
+ const changes: FileChange[] = [];
99
+ const virtualFiles: Map<string, string> = new Map();
100
+ const deletedFiles: Set<string> = new Set();
101
+
102
+ /**
103
+ * Normalize path to absolute
104
+ */
105
+ const normalizePath = (filePath: string): string => {
106
+ if (path.isAbsolute(filePath)) {
107
+ return filePath;
108
+ }
109
+ return path.join(projectRoot, filePath);
110
+ };
111
+
112
+ /**
113
+ * Read from virtual or real filesystem
114
+ */
115
+ const readFromVirtualOrReal = (filePath: string): string | null => {
116
+ const normalizedPath = normalizePath(filePath);
117
+
118
+ // Check if deleted
119
+ if (deletedFiles.has(normalizedPath)) {
120
+ return null;
121
+ }
122
+
123
+ // Check virtual files first
124
+ if (virtualFiles.has(normalizedPath)) {
125
+ return virtualFiles.get(normalizedPath)!;
126
+ }
127
+
128
+ // Fall back to real filesystem
129
+ try {
130
+ return fs.readFileSync(normalizedPath, "utf-8");
131
+ } catch {
132
+ return null;
133
+ }
134
+ };
135
+
136
+ /**
137
+ * Record a file change
138
+ */
139
+ const recordChange = (
140
+ filePath: string,
141
+ operation: "create" | "modify" | "delete",
142
+ before: string | undefined,
143
+ after: string
144
+ ): void => {
145
+ const normalizedPath = normalizePath(filePath);
146
+
147
+ // Find existing change for this file
148
+ const existingIndex = changes.findIndex((c) => c.path === normalizedPath);
149
+
150
+ if (existingIndex >= 0) {
151
+ // Update existing change
152
+ changes[existingIndex] = {
153
+ ...changes[existingIndex],
154
+ after,
155
+ operation: operation === "delete" ? "delete" : changes[existingIndex].operation,
156
+ };
157
+ } else {
158
+ changes.push({
159
+ path: normalizedPath,
160
+ operation,
161
+ before,
162
+ after,
163
+ diff: generateDiff(before, after),
164
+ });
165
+ }
166
+ };
167
+
168
+ return {
169
+ async readFile(filePath: string): Promise<string> {
170
+ const content = readFromVirtualOrReal(filePath);
171
+ if (content === null) {
172
+ throw new Error(`File not found: ${filePath}`);
173
+ }
174
+ return content;
175
+ },
176
+
177
+ readFileSync(filePath: string): string {
178
+ const content = readFromVirtualOrReal(filePath);
179
+ if (content === null) {
180
+ throw new Error(`File not found: ${filePath}`);
181
+ }
182
+ return content;
183
+ },
184
+
185
+ async writeFile(filePath: string, content: string): Promise<void> {
186
+ const normalizedPath = normalizePath(filePath);
187
+ const before = readFromVirtualOrReal(filePath) ?? undefined;
188
+ const isNew = before === undefined;
189
+
190
+ if (dryRun) {
191
+ virtualFiles.set(normalizedPath, content);
192
+ deletedFiles.delete(normalizedPath);
193
+ recordChange(filePath, isNew ? "create" : "modify", before, content);
194
+ } else {
195
+ // Ensure directory exists
196
+ const dir = path.dirname(normalizedPath);
197
+ await fs.promises.mkdir(dir, { recursive: true });
198
+ await fs.promises.writeFile(normalizedPath, content, "utf-8");
199
+ recordChange(filePath, isNew ? "create" : "modify", before, content);
200
+ }
201
+ },
202
+
203
+ writeFileSync(filePath: string, content: string): void {
204
+ const normalizedPath = normalizePath(filePath);
205
+ const before = readFromVirtualOrReal(filePath) ?? undefined;
206
+ const isNew = before === undefined;
207
+
208
+ if (dryRun) {
209
+ virtualFiles.set(normalizedPath, content);
210
+ deletedFiles.delete(normalizedPath);
211
+ recordChange(filePath, isNew ? "create" : "modify", before, content);
212
+ } else {
213
+ // Ensure directory exists
214
+ const dir = path.dirname(normalizedPath);
215
+ fs.mkdirSync(dir, { recursive: true });
216
+ fs.writeFileSync(normalizedPath, content, "utf-8");
217
+ recordChange(filePath, isNew ? "create" : "modify", before, content);
218
+ }
219
+ },
220
+
221
+ async exists(filePath: string): Promise<boolean> {
222
+ const normalizedPath = normalizePath(filePath);
223
+
224
+ if (deletedFiles.has(normalizedPath)) {
225
+ return false;
226
+ }
227
+
228
+ if (virtualFiles.has(normalizedPath)) {
229
+ return true;
230
+ }
231
+
232
+ try {
233
+ await fs.promises.access(normalizedPath);
234
+ return true;
235
+ } catch {
236
+ return false;
237
+ }
238
+ },
239
+
240
+ existsSync(filePath: string): boolean {
241
+ const normalizedPath = normalizePath(filePath);
242
+
243
+ if (deletedFiles.has(normalizedPath)) {
244
+ return false;
245
+ }
246
+
247
+ if (virtualFiles.has(normalizedPath)) {
248
+ return true;
249
+ }
250
+
251
+ return fs.existsSync(normalizedPath);
252
+ },
253
+
254
+ async mkdir(dirPath: string): Promise<void> {
255
+ if (!dryRun) {
256
+ await fs.promises.mkdir(normalizePath(dirPath), { recursive: true });
257
+ }
258
+ },
259
+
260
+ mkdirSync(dirPath: string): void {
261
+ if (!dryRun) {
262
+ fs.mkdirSync(normalizePath(dirPath), { recursive: true });
263
+ }
264
+ },
265
+
266
+ async unlink(filePath: string): Promise<void> {
267
+ const normalizedPath = normalizePath(filePath);
268
+ const before = readFromVirtualOrReal(filePath) ?? "";
269
+
270
+ if (dryRun) {
271
+ virtualFiles.delete(normalizedPath);
272
+ deletedFiles.add(normalizedPath);
273
+ recordChange(filePath, "delete", before, "");
274
+ } else {
275
+ await fs.promises.unlink(normalizedPath);
276
+ recordChange(filePath, "delete", before, "");
277
+ }
278
+ },
279
+
280
+ unlinkSync(filePath: string): void {
281
+ const normalizedPath = normalizePath(filePath);
282
+ const before = readFromVirtualOrReal(filePath) ?? "";
283
+
284
+ if (dryRun) {
285
+ virtualFiles.delete(normalizedPath);
286
+ deletedFiles.add(normalizedPath);
287
+ recordChange(filePath, "delete", before, "");
288
+ } else {
289
+ fs.unlinkSync(normalizedPath);
290
+ recordChange(filePath, "delete", before, "");
291
+ }
292
+ },
293
+
294
+ async rmdir(dirPath: string): Promise<void> {
295
+ if (!dryRun) {
296
+ await fs.promises.rm(normalizePath(dirPath), { recursive: true, force: true });
297
+ }
298
+ },
299
+
300
+ async copyFile(src: string, dest: string): Promise<void> {
301
+ const content = await this.readFile(src);
302
+ await this.writeFile(dest, content);
303
+ },
304
+
305
+ copyFileSync(src: string, dest: string): void {
306
+ const content = this.readFileSync(src);
307
+ this.writeFileSync(dest, content);
308
+ },
309
+
310
+ getChanges(): FileChange[] {
311
+ return [...changes];
312
+ },
313
+
314
+ async commit(): Promise<void> {
315
+ if (!dryRun) {
316
+ // Changes are already committed in non-dry-run mode
317
+ return;
318
+ }
319
+
320
+ // Apply all virtual changes to real filesystem
321
+ for (const change of changes) {
322
+ switch (change.operation) {
323
+ case "create":
324
+ case "modify": {
325
+ const dir = path.dirname(change.path);
326
+ await fs.promises.mkdir(dir, { recursive: true });
327
+ await fs.promises.writeFile(change.path, change.after, "utf-8");
328
+ break;
329
+ }
330
+ case "delete": {
331
+ try {
332
+ await fs.promises.unlink(change.path);
333
+ } catch {
334
+ // File might not exist
335
+ }
336
+ break;
337
+ }
338
+ }
339
+ }
340
+
341
+ // Clear virtual state
342
+ virtualFiles.clear();
343
+ deletedFiles.clear();
344
+ },
345
+
346
+ isDryRun(): boolean {
347
+ return dryRun;
348
+ },
349
+ };
350
+ }
351
+
352
+ /**
353
+ * Generate a simple diff between two strings
354
+ */
355
+ function generateDiff(before: string | undefined, after: string): string {
356
+ if (before === undefined) {
357
+ return `+ ${after.split("\n").length} lines added`;
358
+ }
359
+
360
+ const beforeLines = before.split("\n");
361
+ const afterLines = after.split("\n");
362
+
363
+ const added = afterLines.length - beforeLines.length;
364
+ const removed = beforeLines.filter((line, i) => afterLines[i] !== line).length;
365
+
366
+ const parts: string[] = [];
367
+ if (added > 0) parts.push(`+${added}`);
368
+ if (added < 0) parts.push(`${added}`);
369
+ if (removed > 0) parts.push(`~${removed} changed`);
370
+
371
+ return parts.join(", ") || "no changes";
372
+ }
373
+
374
+ /**
375
+ * Recursively copy a directory
376
+ */
377
+ export async function copyDirectory(src: string, dest: string): Promise<void> {
378
+ await fs.promises.mkdir(dest, { recursive: true });
379
+
380
+ const entries = await fs.promises.readdir(src, { withFileTypes: true });
381
+
382
+ for (const entry of entries) {
383
+ const srcPath = path.join(src, entry.name);
384
+ const destPath = path.join(dest, entry.name);
385
+
386
+ if (entry.isDirectory()) {
387
+ await copyDirectory(srcPath, destPath);
388
+ } else {
389
+ await fs.promises.copyFile(srcPath, destPath);
390
+ }
391
+ }
392
+ }
393
+
394
+ /**
395
+ * Remove directory recursively
396
+ */
397
+ export async function removeDirectory(dirPath: string): Promise<void> {
398
+ try {
399
+ await fs.promises.rm(dirPath, { recursive: true, force: true });
400
+ } catch {
401
+ // Directory might not exist
402
+ }
403
+ }
404
+
405
+ /**
406
+ * Ensure directory exists
407
+ */
408
+ export async function ensureDirectory(dirPath: string): Promise<void> {
409
+ await fs.promises.mkdir(dirPath, { recursive: true });
410
+ }
411
+
412
+ /**
413
+ * Find files matching a pattern
414
+ */
415
+ export async function findFiles(dir: string, pattern: RegExp, results: string[] = []): Promise<string[]> {
416
+ const entries = await fs.promises.readdir(dir, { withFileTypes: true });
417
+
418
+ for (const entry of entries) {
419
+ const fullPath = path.join(dir, entry.name);
420
+
421
+ if (entry.isDirectory()) {
422
+ await findFiles(fullPath, pattern, results);
423
+ } else if (pattern.test(entry.name)) {
424
+ results.push(fullPath);
425
+ }
426
+ }
427
+
428
+ return results;
429
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Utility functions for Teardown Launchpad
3
+ */
4
+
5
+ export type { VirtualFileSystem } from "./fs";
6
+ export {
7
+ copyDirectory,
8
+ createVirtualFileSystem,
9
+ ensureDirectory,
10
+ findFiles,
11
+ removeDirectory,
12
+ } from "./fs";
13
+ export type { Logger, LoggerConfig, LogLevel } from "./logger";
14
+ export {
15
+ createLogger,
16
+ defaultLogger,
17
+ formatBytes,
18
+ formatDuration,
19
+ formatKeyValue,
20
+ formatList,
21
+ } from "./logger";