@appspacer/cli 1.0.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.
- package/LICENSE +21 -0
- package/README.md +271 -0
- package/dist/__tests__/api.test.d.ts +1 -0
- package/dist/__tests__/api.test.js +142 -0
- package/dist/__tests__/config.test.d.ts +1 -0
- package/dist/__tests__/config.test.js +109 -0
- package/dist/__tests__/hash.test.d.ts +1 -0
- package/dist/__tests__/hash.test.js +47 -0
- package/dist/__tests__/setup-injections.test.d.ts +1 -0
- package/dist/__tests__/setup-injections.test.js +238 -0
- package/dist/__tests__/zip.test.d.ts +1 -0
- package/dist/__tests__/zip.test.js +62 -0
- package/dist/api.d.ts +6 -0
- package/dist/api.js +52 -0
- package/dist/commands/deployments.d.ts +2 -0
- package/dist/commands/deployments.js +39 -0
- package/dist/commands/envsync.d.ts +2 -0
- package/dist/commands/envsync.js +230 -0
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.js +41 -0
- package/dist/commands/release-flutter.d.ts +2 -0
- package/dist/commands/release-flutter.js +176 -0
- package/dist/commands/release-react-native.d.ts +2 -0
- package/dist/commands/release-react-native.js +143 -0
- package/dist/commands/release.d.ts +2 -0
- package/dist/commands/release.js +106 -0
- package/dist/commands/rollback.d.ts +2 -0
- package/dist/commands/rollback.js +43 -0
- package/dist/commands/setup.d.ts +22 -0
- package/dist/commands/setup.js +575 -0
- package/dist/commands/vault.d.ts +2 -0
- package/dist/commands/vault.js +292 -0
- package/dist/commands/whoami.d.ts +2 -0
- package/dist/commands/whoami.js +16 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.js +45 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +25 -0
- package/dist/utils/bundle.d.ts +8 -0
- package/dist/utils/bundle.js +59 -0
- package/dist/utils/hash.d.ts +4 -0
- package/dist/utils/hash.js +9 -0
- package/dist/utils/ui.d.ts +19 -0
- package/dist/utils/ui.js +43 -0
- package/dist/utils/validators.d.ts +25 -0
- package/dist/utils/validators.js +65 -0
- package/dist/utils/zip.d.ts +5 -0
- package/dist/utils/zip.js +17 -0
- package/package.json +66 -0
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import ora from "ora";
|
|
6
|
+
// ─── Marker Constants ────────────────────────────────────────────────────────
|
|
7
|
+
const MARKER_START = "// APPSPACER_START";
|
|
8
|
+
const MARKER_END = "// APPSPACER_END";
|
|
9
|
+
export function detectArchitecture(content) {
|
|
10
|
+
if (content.includes("ReactHostDelegate") ||
|
|
11
|
+
content.includes("ReactHostImpl") ||
|
|
12
|
+
content.includes("getReactHost") ||
|
|
13
|
+
content.includes("DefaultReactHost")) {
|
|
14
|
+
return "new";
|
|
15
|
+
}
|
|
16
|
+
if (content.includes("ReactNativeHost") ||
|
|
17
|
+
content.includes("getReactNativeHost") ||
|
|
18
|
+
content.includes("getJSBundleFile")) {
|
|
19
|
+
return "traditional";
|
|
20
|
+
}
|
|
21
|
+
if (content.includes("DefaultNewArchitectureEntryPoint")) {
|
|
22
|
+
return "new";
|
|
23
|
+
}
|
|
24
|
+
return "traditional";
|
|
25
|
+
}
|
|
26
|
+
// ─── Shared Helpers ──────────────────────────────────────────────────────────
|
|
27
|
+
function searchForFile(dir, fileNames, depth = 8) {
|
|
28
|
+
if (depth < 0)
|
|
29
|
+
return null;
|
|
30
|
+
let entries;
|
|
31
|
+
try {
|
|
32
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
for (const entry of entries) {
|
|
38
|
+
if (entry.name === "node_modules" || entry.name.startsWith("."))
|
|
39
|
+
continue;
|
|
40
|
+
const fullPath = path.join(dir, entry.name);
|
|
41
|
+
if (entry.isSymbolicLink())
|
|
42
|
+
continue;
|
|
43
|
+
if (entry.isDirectory()) {
|
|
44
|
+
const result = searchForFile(fullPath, fileNames, depth - 1);
|
|
45
|
+
if (result)
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
else if (fileNames.includes(entry.name)) {
|
|
49
|
+
return fullPath;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
export function hasExistingInjection(content) {
|
|
55
|
+
return content.includes(MARKER_START) && content.includes(MARKER_END);
|
|
56
|
+
}
|
|
57
|
+
export function removeExistingInjection(content) {
|
|
58
|
+
const startIdx = content.indexOf(MARKER_START);
|
|
59
|
+
const endIdx = content.indexOf(MARKER_END);
|
|
60
|
+
if (startIdx === -1 || endIdx === -1)
|
|
61
|
+
return content;
|
|
62
|
+
// Go back to the start of the line containing MARKER_START (capture leading whitespace)
|
|
63
|
+
const lineStart = content.lastIndexOf("\n", startIdx - 1);
|
|
64
|
+
const endLineIdx = content.indexOf("\n", endIdx + MARKER_END.length);
|
|
65
|
+
const before = content.substring(0, lineStart === -1 ? 0 : lineStart);
|
|
66
|
+
const after = endLineIdx !== -1 ? content.substring(endLineIdx + 1) : "";
|
|
67
|
+
// Remove one leading blank line to avoid double blank lines after removal
|
|
68
|
+
return (before + "\n" + after).replace(/\n{3,}/g, "\n\n");
|
|
69
|
+
}
|
|
70
|
+
function writeWithBackup(filePath, modified) {
|
|
71
|
+
const backupPath = filePath + ".appspacer.bak";
|
|
72
|
+
// Only create backup if one doesn't already exist (preserve original)
|
|
73
|
+
if (!fs.existsSync(backupPath)) {
|
|
74
|
+
fs.copyFileSync(filePath, backupPath);
|
|
75
|
+
}
|
|
76
|
+
fs.writeFileSync(filePath, modified, "utf-8");
|
|
77
|
+
}
|
|
78
|
+
function isPackageInstalled(projectDir) {
|
|
79
|
+
const nodeModulesPath = path.join(projectDir, "node_modules", "@appspacer/react-native");
|
|
80
|
+
return fs.existsSync(nodeModulesPath);
|
|
81
|
+
}
|
|
82
|
+
// ─── Android Setup ──────────────────────────────────────────────────────────
|
|
83
|
+
function findMainApplication(androidDir) {
|
|
84
|
+
const srcDirs = [
|
|
85
|
+
path.join(androidDir, "app", "src", "main", "java"),
|
|
86
|
+
path.join(androidDir, "app", "src", "main", "kotlin"),
|
|
87
|
+
];
|
|
88
|
+
for (const srcDir of srcDirs) {
|
|
89
|
+
if (!fs.existsSync(srcDir))
|
|
90
|
+
continue;
|
|
91
|
+
// Search for both Kotlin and Java variants
|
|
92
|
+
const result = searchForFile(srcDir, ["MainApplication.kt", "MainApplication.java"]);
|
|
93
|
+
if (result)
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
function getTraditionalAndroidInjection() {
|
|
99
|
+
return `
|
|
100
|
+
${MARKER_START}
|
|
101
|
+
// AppSpacer OTA — Dynamic bundle resolution (Traditional Architecture)
|
|
102
|
+
override fun getJSBundleFile(): String? =
|
|
103
|
+
com.appspacer.AppSpacerModule.getCustomBundlePath(applicationContext)
|
|
104
|
+
${MARKER_END}`;
|
|
105
|
+
}
|
|
106
|
+
function getNewArchAndroidInjection() {
|
|
107
|
+
return `
|
|
108
|
+
${MARKER_START}
|
|
109
|
+
// AppSpacer OTA — Dynamic bundle resolution (New Architecture)
|
|
110
|
+
override fun getJsBundleFilePath(): String? =
|
|
111
|
+
com.appspacer.AppSpacerModule.getCustomBundlePath(applicationContext)
|
|
112
|
+
${MARKER_END}`;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Safely injects code INSIDE a matched block's opening brace,
|
|
116
|
+
* rather than using lastIndexOf("}") which can target the wrong brace.
|
|
117
|
+
*/
|
|
118
|
+
function injectAfterOpenBrace(content, pattern, injection) {
|
|
119
|
+
const match = content.match(pattern);
|
|
120
|
+
if (!match || match.index === undefined)
|
|
121
|
+
return null;
|
|
122
|
+
const insertPos = match.index + match[0].length;
|
|
123
|
+
return content.substring(0, insertPos) + injection + content.substring(insertPos);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Injects before the OUTER class closing brace by tracking brace depth,
|
|
127
|
+
* rather than blindly using lastIndexOf("}").
|
|
128
|
+
*/
|
|
129
|
+
function injectBeforeClassEnd(content, injection, classPattern) {
|
|
130
|
+
const classMatch = content.match(classPattern);
|
|
131
|
+
if (!classMatch || classMatch.index === undefined) {
|
|
132
|
+
throw new Error(`Could not locate the class declaration matching ${classPattern}. ` +
|
|
133
|
+
"Please add the AppSpacer bundle resolver manually.");
|
|
134
|
+
}
|
|
135
|
+
// Walk from class opening brace, track depth to find the closing brace
|
|
136
|
+
let depth = 0;
|
|
137
|
+
let classBodyStart = -1;
|
|
138
|
+
for (let i = classMatch.index; i < content.length; i++) {
|
|
139
|
+
if (content[i] === "{") {
|
|
140
|
+
depth++;
|
|
141
|
+
if (depth === 1)
|
|
142
|
+
classBodyStart = i;
|
|
143
|
+
}
|
|
144
|
+
else if (content[i] === "}") {
|
|
145
|
+
depth--;
|
|
146
|
+
if (depth === 0 && classBodyStart !== -1) {
|
|
147
|
+
return (content.substring(0, i) +
|
|
148
|
+
"\n" + injection + "\n" +
|
|
149
|
+
content.substring(i));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
throw new Error("Could not find the closing brace of the class. " +
|
|
154
|
+
"Please add the AppSpacer bundle resolver manually.");
|
|
155
|
+
}
|
|
156
|
+
export function injectTraditionalAndroid(content) {
|
|
157
|
+
const injection = getTraditionalAndroidInjection();
|
|
158
|
+
// Pattern 1: Inline ReactNativeHost object
|
|
159
|
+
const hostPattern = /object\s*:\s*(?:Default)?ReactNativeHost\s*\(.*?\)\s*\{/s;
|
|
160
|
+
const result = injectAfterOpenBrace(content, hostPattern, injection);
|
|
161
|
+
if (result)
|
|
162
|
+
return { result, method: "ReactNativeHost block" };
|
|
163
|
+
// Pattern 2: class-level fallback using brace tracking
|
|
164
|
+
const classPattern = /class\s+MainApplication\b[^{]*\{/;
|
|
165
|
+
return {
|
|
166
|
+
result: injectBeforeClassEnd(content, injection, classPattern),
|
|
167
|
+
method: "class body (fallback)",
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
export function injectNewArchAndroid(content) {
|
|
171
|
+
const injection = getNewArchAndroidInjection();
|
|
172
|
+
// Pattern 1: Inline ReactHostDelegate object expression
|
|
173
|
+
const delegatePattern = /object\s*:\s*ReactHostDelegate\s*\{/;
|
|
174
|
+
const result1 = injectAfterOpenBrace(content, delegatePattern, injection);
|
|
175
|
+
if (result1)
|
|
176
|
+
return { result: result1, method: "ReactHostDelegate block" };
|
|
177
|
+
// Pattern 2: reactHost lazy block containing DefaultReactHostDelegate
|
|
178
|
+
const reactHostPattern = /override\s+val\s+reactHost\s*[^{]*\{/;
|
|
179
|
+
const result2 = injectAfterOpenBrace(content, reactHostPattern, injection);
|
|
180
|
+
if (result2)
|
|
181
|
+
return { result: result2, method: "reactHost lazy block" };
|
|
182
|
+
// Pattern 3: DefaultReactHost.getDefaultReactHost — inject real commented
|
|
183
|
+
// instructions with a warning (can't auto-patch this pattern)
|
|
184
|
+
const defaultHostPattern = /DefaultReactHost\s*\.\s*getDefaultReactHost/;
|
|
185
|
+
if (defaultHostPattern.test(content)) {
|
|
186
|
+
const classPattern = /class\s+MainApplication\b[^{]*\{/;
|
|
187
|
+
const manualInjection = `
|
|
188
|
+
${MARKER_START}
|
|
189
|
+
// AppSpacer OTA — ACTION REQUIRED (DefaultReactHost pattern detected)
|
|
190
|
+
//
|
|
191
|
+
// Automatic injection is not possible for this pattern.
|
|
192
|
+
// Please manually replace your reactHost initialisation with:
|
|
193
|
+
//
|
|
194
|
+
// override val reactHost: ReactHost by lazy {
|
|
195
|
+
// val delegate = object : ReactHostDelegate {
|
|
196
|
+
// override fun getJsBundleFilePath(): String? =
|
|
197
|
+
// com.appspacer.AppSpacerModule.getCustomBundlePath(applicationContext)
|
|
198
|
+
// override fun getJsMainModulePath(): String = "index"
|
|
199
|
+
// override fun getReactPackages(): List<ReactPackage> =
|
|
200
|
+
// PackageList(this@MainApplication).packages
|
|
201
|
+
// }
|
|
202
|
+
// ReactHostImpl(applicationContext, delegate, SurfaceDelegateFactory { null }, true, true)
|
|
203
|
+
// }
|
|
204
|
+
${MARKER_END}`;
|
|
205
|
+
return {
|
|
206
|
+
result: injectBeforeClassEnd(content, manualInjection, classPattern),
|
|
207
|
+
method: "DefaultReactHost pattern",
|
|
208
|
+
warn: "DefaultReactHost pattern detected — manual action required. A comment block with instructions has been injected.",
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
// Fallback: inject at class body level
|
|
212
|
+
const classPattern = /class\s+MainApplication\b[^{]*\{/;
|
|
213
|
+
return {
|
|
214
|
+
result: injectBeforeClassEnd(content, injection, classPattern),
|
|
215
|
+
method: "class body (fallback)",
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
// ─── iOS Setup ──────────────────────────────────────────────────────────────
|
|
219
|
+
function findAppDelegate(iosDir) {
|
|
220
|
+
// Check flat root first
|
|
221
|
+
for (const name of ["AppDelegate.mm", "AppDelegate.swift", "AppDelegate.m"]) {
|
|
222
|
+
const p = path.join(iosDir, name);
|
|
223
|
+
if (fs.existsSync(p))
|
|
224
|
+
return p;
|
|
225
|
+
}
|
|
226
|
+
// Search one level deep in subdirectories
|
|
227
|
+
const entries = fs.readdirSync(iosDir, { withFileTypes: true });
|
|
228
|
+
for (const entry of entries) {
|
|
229
|
+
if (!entry.isDirectory())
|
|
230
|
+
continue;
|
|
231
|
+
const subDir = path.join(iosDir, entry.name);
|
|
232
|
+
for (const name of ["AppDelegate.mm", "AppDelegate.swift", "AppDelegate.m"]) {
|
|
233
|
+
const p = path.join(subDir, name);
|
|
234
|
+
if (fs.existsSync(p))
|
|
235
|
+
return p;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
export function injectIosSetup(content, filePath) {
|
|
241
|
+
if (hasExistingInjection(content)) {
|
|
242
|
+
content = removeExistingInjection(content);
|
|
243
|
+
}
|
|
244
|
+
return filePath.endsWith(".swift")
|
|
245
|
+
? injectIosSwift(content)
|
|
246
|
+
: injectIosObjC(content);
|
|
247
|
+
}
|
|
248
|
+
function injectIosObjC(content) {
|
|
249
|
+
// Ensure #import <AppSpacerModule.h> is present
|
|
250
|
+
if (!content.includes("AppSpacerModule.h")) {
|
|
251
|
+
const lastImportIdx = content.lastIndexOf("#import");
|
|
252
|
+
if (lastImportIdx !== -1) {
|
|
253
|
+
const lineEnd = content.indexOf("\n", lastImportIdx);
|
|
254
|
+
content =
|
|
255
|
+
content.substring(0, lineEnd + 1) +
|
|
256
|
+
`#import <AppSpacerModule.h>\n` +
|
|
257
|
+
content.substring(lineEnd + 1);
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
content = `#import <AppSpacerModule.h>\n${content}`;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const replacement = `
|
|
264
|
+
${MARKER_START}
|
|
265
|
+
// AppSpacer OTA — Dynamic bundle URL resolution
|
|
266
|
+
return [AppSpacerModule bundleURL];
|
|
267
|
+
${MARKER_END}
|
|
268
|
+
}`;
|
|
269
|
+
// Pattern 1: RN 0.72 and earlier — bundleURL method
|
|
270
|
+
const bundleUrlPattern = /(-\s*\(NSURL\s*\*\s*\)\s*bundleURL\s*\{)([\s\S]*?)(\n\})/;
|
|
271
|
+
const bundleMatch = content.match(bundleUrlPattern);
|
|
272
|
+
if (bundleMatch && bundleMatch.index !== undefined) {
|
|
273
|
+
return {
|
|
274
|
+
result: content.substring(0, bundleMatch.index) +
|
|
275
|
+
`${bundleMatch[1]}\n${replacement}` +
|
|
276
|
+
content.substring(bundleMatch.index + bundleMatch[0].length),
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
// Pattern 2: RN 0.73+ — sourceURLForBridge method
|
|
280
|
+
const sourceUrlPattern = /(-\s*\(NSURL\s*\*\s*\)\s*sourceURLForBridge\s*:\s*\(RCTBridge\s*\*\s*\)\s*\w+\s*\{)([\s\S]*?)(\n\})/;
|
|
281
|
+
const sourceMatch = content.match(sourceUrlPattern);
|
|
282
|
+
if (sourceMatch && sourceMatch.index !== undefined) {
|
|
283
|
+
return {
|
|
284
|
+
result: content.substring(0, sourceMatch.index) +
|
|
285
|
+
`${sourceMatch[1]}\n${replacement}` +
|
|
286
|
+
content.substring(sourceMatch.index + sourceMatch[0].length),
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
// Fallback: inject a new bundleURL method before @end
|
|
290
|
+
const endIdx = content.lastIndexOf("@end");
|
|
291
|
+
if (endIdx !== -1) {
|
|
292
|
+
const bundleMethod = `
|
|
293
|
+
${MARKER_START}
|
|
294
|
+
// AppSpacer OTA — Dynamic bundle URL resolution
|
|
295
|
+
- (NSURL *)bundleURL {
|
|
296
|
+
return [AppSpacerModule bundleURL];
|
|
297
|
+
}
|
|
298
|
+
${MARKER_END}
|
|
299
|
+
|
|
300
|
+
`;
|
|
301
|
+
return {
|
|
302
|
+
result: content.substring(0, endIdx) + bundleMethod + content.substring(endIdx),
|
|
303
|
+
warn: "bundleURL / sourceURLForBridge method not found — injected a new bundleURL method before @end. Verify it doesn't conflict with an existing implementation.",
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
return {
|
|
307
|
+
result: content,
|
|
308
|
+
warn: "Could not find a suitable injection point in AppDelegate.mm — please add the bundleURL method manually.",
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
function injectIosSwift(content) {
|
|
312
|
+
// Swift AppDelegate: ObjC native modules are exposed via the bridging header,
|
|
313
|
+
// no explicit `import AppSpacerModule` is needed or valid.
|
|
314
|
+
const replacement = `
|
|
315
|
+
${MARKER_START}
|
|
316
|
+
// AppSpacer OTA — Dynamic bundle URL resolution
|
|
317
|
+
return AppSpacerModule.bundleURL()
|
|
318
|
+
${MARKER_END}
|
|
319
|
+
}`;
|
|
320
|
+
// Pattern 1: bundleURL override
|
|
321
|
+
const bundleUrlPattern = /(override\s+func\s+bundleURL\s*\(\s*\)\s*->\s*URL\??\s*\{)([\s\S]*?)(\n\s*\})/;
|
|
322
|
+
const bundleMatch = content.match(bundleUrlPattern);
|
|
323
|
+
if (bundleMatch && bundleMatch.index !== undefined) {
|
|
324
|
+
return {
|
|
325
|
+
result: content.substring(0, bundleMatch.index) +
|
|
326
|
+
`${bundleMatch[1]}\n${replacement}` +
|
|
327
|
+
content.substring(bundleMatch.index + bundleMatch[0].length),
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
// Pattern 2: sourceURL(for:) override (RN 0.73+)
|
|
331
|
+
const sourceUrlPattern = /(override\s+func\s+sourceURL\s*\(for\s+bridge\s*:[^)]*\)\s*->\s*URL\??\s*\{)([\s\S]*?)(\n\s*\})/;
|
|
332
|
+
const sourceMatch = content.match(sourceUrlPattern);
|
|
333
|
+
if (sourceMatch && sourceMatch.index !== undefined) {
|
|
334
|
+
return {
|
|
335
|
+
result: content.substring(0, sourceMatch.index) +
|
|
336
|
+
`${sourceMatch[1]}\n${replacement}` +
|
|
337
|
+
content.substring(sourceMatch.index + sourceMatch[0].length),
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
// Fallback: inject before the last closing brace of the class
|
|
341
|
+
const classPattern = /class\s+AppDelegate\b[^{]*\{/;
|
|
342
|
+
const classMatch = content.match(classPattern);
|
|
343
|
+
if (classMatch) {
|
|
344
|
+
const bundleMethod = `
|
|
345
|
+
${MARKER_START}
|
|
346
|
+
// AppSpacer OTA — Dynamic bundle URL resolution
|
|
347
|
+
override func bundleURL() -> URL? {
|
|
348
|
+
return AppSpacerModule.bundleURL()
|
|
349
|
+
}
|
|
350
|
+
${MARKER_END}`;
|
|
351
|
+
return {
|
|
352
|
+
result: injectBeforeClassEnd(content, bundleMethod, classPattern),
|
|
353
|
+
warn: "bundleURL / sourceURL method not found in Swift AppDelegate — injected a new override. Verify it is correct for your RN version.",
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
return {
|
|
357
|
+
result: content,
|
|
358
|
+
warn: "Could not find AppDelegate class in Swift file — please add the bundleURL override manually.",
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
// ─── Undo Command ────────────────────────────────────────────────────────────
|
|
362
|
+
export const undoCommand = new Command("setup:undo")
|
|
363
|
+
.description("Remove AppSpacer injections from native files")
|
|
364
|
+
.option("--project-dir <dir>", "Root directory of the React Native project", ".")
|
|
365
|
+
.action((opts) => {
|
|
366
|
+
const projectDir = path.resolve(opts.projectDir);
|
|
367
|
+
console.log("");
|
|
368
|
+
console.log(chalk.bold.hex("#7C3AED")(" ╔══════════════════════════════════════╗"));
|
|
369
|
+
console.log(chalk.bold.hex("#7C3AED")(" ║ 🔄 AppSpacer Setup Undo ║"));
|
|
370
|
+
console.log(chalk.bold.hex("#7C3AED")(" ╚══════════════════════════════════════╝"));
|
|
371
|
+
console.log("");
|
|
372
|
+
const filesToCheck = [];
|
|
373
|
+
const androidDir = path.join(projectDir, "android");
|
|
374
|
+
if (fs.existsSync(androidDir)) {
|
|
375
|
+
const mainApp = findMainApplication(androidDir);
|
|
376
|
+
if (mainApp)
|
|
377
|
+
filesToCheck.push(mainApp);
|
|
378
|
+
}
|
|
379
|
+
const iosDir = path.join(projectDir, "ios");
|
|
380
|
+
if (fs.existsSync(iosDir)) {
|
|
381
|
+
const appDelegate = findAppDelegate(iosDir);
|
|
382
|
+
if (appDelegate)
|
|
383
|
+
filesToCheck.push(appDelegate);
|
|
384
|
+
}
|
|
385
|
+
let cleaned = 0;
|
|
386
|
+
for (const filePath of filesToCheck) {
|
|
387
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
388
|
+
if (!hasExistingInjection(content)) {
|
|
389
|
+
console.log(chalk.dim(` — No injection found in ${path.relative(projectDir, filePath)}`));
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
const cleaned_content = removeExistingInjection(content);
|
|
393
|
+
fs.writeFileSync(filePath, cleaned_content, "utf-8");
|
|
394
|
+
console.log(chalk.green(` ✔ Removed injection from ${path.relative(projectDir, filePath)}`));
|
|
395
|
+
cleaned++;
|
|
396
|
+
}
|
|
397
|
+
if (cleaned === 0) {
|
|
398
|
+
console.log(chalk.yellow("\n No AppSpacer injections found in any native files.\n"));
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
console.log(chalk.green.bold(`\n ✔ Removed ${cleaned} injection(s). Rebuild your app to apply.\n`));
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
// ─── Main Setup Command ─────────────────────────────────────────────────────
|
|
405
|
+
export const setupCommand = new Command("setup")
|
|
406
|
+
.description("Auto-configure native files for AppSpacer OTA updates")
|
|
407
|
+
.option("--android-only", "Only configure Android")
|
|
408
|
+
.option("--ios-only", "Only configure iOS")
|
|
409
|
+
.option("--project-dir <dir>", "Root directory of the React Native project", ".")
|
|
410
|
+
.option("--dry-run", "Preview changes without writing files")
|
|
411
|
+
.action(async (opts) => {
|
|
412
|
+
const projectDir = path.resolve(opts.projectDir);
|
|
413
|
+
console.log("");
|
|
414
|
+
console.log(chalk.bold.hex("#7C3AED")(" ╔══════════════════════════════════════╗"));
|
|
415
|
+
console.log(chalk.bold.hex("#7C3AED")(" ║ 🚀 AppSpacer Auto Setup ║"));
|
|
416
|
+
console.log(chalk.bold.hex("#7C3AED")(" ╚══════════════════════════════════════╝"));
|
|
417
|
+
console.log("");
|
|
418
|
+
// ── SDK check ────────────────────────────────────────────
|
|
419
|
+
if (!isPackageInstalled(projectDir)) {
|
|
420
|
+
console.log(chalk.yellow.bold(" ⚠ react-native-appspacer is not installed in node_modules."));
|
|
421
|
+
console.log(chalk.dim(" Run: npm install react-native-appspacer (or yarn add)"));
|
|
422
|
+
console.log(chalk.dim(" Then re-run: appspacer setup\n"));
|
|
423
|
+
process.exit(1);
|
|
424
|
+
}
|
|
425
|
+
let androidSuccess = false;
|
|
426
|
+
let iosSuccess = false;
|
|
427
|
+
// ── Android ──────────────────────────────────────────────
|
|
428
|
+
if (!opts.iosOnly) {
|
|
429
|
+
const spinner = ora({
|
|
430
|
+
text: "Detecting Android project structure...",
|
|
431
|
+
color: "cyan",
|
|
432
|
+
}).start();
|
|
433
|
+
const androidDir = path.join(projectDir, "android");
|
|
434
|
+
if (!fs.existsSync(androidDir)) {
|
|
435
|
+
spinner.warn(chalk.yellow("Android directory not found — skipping Android setup"));
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
const mainAppPath = findMainApplication(androidDir);
|
|
439
|
+
if (!mainAppPath) {
|
|
440
|
+
spinner.fail(chalk.red("Could not find MainApplication.kt or MainApplication.java"));
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
const fileName = path.basename(mainAppPath);
|
|
444
|
+
spinner.succeed(chalk.green(`Found ${fileName}`) +
|
|
445
|
+
chalk.dim(` (${path.relative(projectDir, mainAppPath)})`));
|
|
446
|
+
let content = fs.readFileSync(mainAppPath, "utf-8");
|
|
447
|
+
if (hasExistingInjection(content)) {
|
|
448
|
+
const reapplySpinner = ora({
|
|
449
|
+
text: "Removing previous AppSpacer injection...",
|
|
450
|
+
color: "yellow",
|
|
451
|
+
}).start();
|
|
452
|
+
content = removeExistingInjection(content);
|
|
453
|
+
reapplySpinner.succeed(chalk.green("Cleaned previous injection"));
|
|
454
|
+
}
|
|
455
|
+
const archSpinner = ora({
|
|
456
|
+
text: "Detecting React Native architecture...",
|
|
457
|
+
color: "cyan",
|
|
458
|
+
}).start();
|
|
459
|
+
const arch = detectArchitecture(content);
|
|
460
|
+
archSpinner.succeed(chalk.green("Detected architecture: ") +
|
|
461
|
+
chalk.bold(arch === "new"
|
|
462
|
+
? "New Architecture (ReactHostDelegate)"
|
|
463
|
+
: "Traditional (ReactNativeHost)"));
|
|
464
|
+
const injectSpinner = ora({
|
|
465
|
+
text: "Injecting AppSpacer bundle resolver...",
|
|
466
|
+
color: "cyan",
|
|
467
|
+
}).start();
|
|
468
|
+
const injectResult = arch === "new"
|
|
469
|
+
? injectNewArchAndroid(content)
|
|
470
|
+
: injectTraditionalAndroid(content);
|
|
471
|
+
if (injectResult.warn) {
|
|
472
|
+
injectSpinner.warn(chalk.yellow(injectResult.warn));
|
|
473
|
+
}
|
|
474
|
+
if (opts.dryRun) {
|
|
475
|
+
injectSpinner.info(chalk.blue("Dry run — no files modified"));
|
|
476
|
+
console.log(chalk.dim("\n--- Preview (Android) ---\n"));
|
|
477
|
+
const startIdx = injectResult.result.indexOf(MARKER_START);
|
|
478
|
+
const endIdx = injectResult.result.indexOf(MARKER_END) + MARKER_END.length;
|
|
479
|
+
if (startIdx !== -1 && endIdx > MARKER_END.length) {
|
|
480
|
+
console.log(chalk.cyan(injectResult.result.substring(startIdx, endIdx)));
|
|
481
|
+
}
|
|
482
|
+
console.log("");
|
|
483
|
+
}
|
|
484
|
+
else {
|
|
485
|
+
// Bail on manual-only patterns — don't write a file that "looks" patched but isn't
|
|
486
|
+
if (injectResult.warn && injectResult.warn.includes("ACTION REQUIRED")) {
|
|
487
|
+
injectSpinner.warn(chalk.yellow("Android file written with instruction comment — manual steps required above."));
|
|
488
|
+
writeWithBackup(mainAppPath, injectResult.result);
|
|
489
|
+
// Still mark as "handled" so summary is shown, but flag it
|
|
490
|
+
androidSuccess = true;
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
writeWithBackup(mainAppPath, injectResult.result);
|
|
494
|
+
injectSpinner.succeed(chalk.green(`Android setup complete`) +
|
|
495
|
+
chalk.dim(` via ${injectResult.method}`));
|
|
496
|
+
androidSuccess = true;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
// ── iOS ──────────────────────────────────────────────────
|
|
503
|
+
if (!opts.androidOnly) {
|
|
504
|
+
const spinner = ora({
|
|
505
|
+
text: "Detecting iOS project structure...",
|
|
506
|
+
color: "cyan",
|
|
507
|
+
}).start();
|
|
508
|
+
const iosDir = path.join(projectDir, "ios");
|
|
509
|
+
if (!fs.existsSync(iosDir)) {
|
|
510
|
+
spinner.warn(chalk.yellow("iOS directory not found — skipping iOS setup"));
|
|
511
|
+
}
|
|
512
|
+
else {
|
|
513
|
+
const appDelegatePath = findAppDelegate(iosDir);
|
|
514
|
+
if (!appDelegatePath) {
|
|
515
|
+
spinner.fail(chalk.red("Could not find AppDelegate.mm, AppDelegate.m, or AppDelegate.swift"));
|
|
516
|
+
}
|
|
517
|
+
else {
|
|
518
|
+
const ext = path.extname(appDelegatePath);
|
|
519
|
+
const lang = ext === ".swift" ? "Swift" : "Obj-C";
|
|
520
|
+
spinner.succeed(chalk.green(`Found AppDelegate${ext}`) +
|
|
521
|
+
chalk.dim(` (${lang}) (${path.relative(projectDir, appDelegatePath)})`));
|
|
522
|
+
const content = fs.readFileSync(appDelegatePath, "utf-8");
|
|
523
|
+
const injectSpinner = ora({
|
|
524
|
+
text: "Injecting AppSpacer bundle URL resolver...",
|
|
525
|
+
color: "cyan",
|
|
526
|
+
}).start();
|
|
527
|
+
const { result: modified, warn } = injectIosSetup(content, appDelegatePath);
|
|
528
|
+
if (warn) {
|
|
529
|
+
injectSpinner.warn(chalk.yellow(warn));
|
|
530
|
+
}
|
|
531
|
+
if (opts.dryRun) {
|
|
532
|
+
injectSpinner.info(chalk.blue("Dry run — no files modified"));
|
|
533
|
+
console.log(chalk.dim("\n--- Preview (iOS) ---\n"));
|
|
534
|
+
const startIdx = modified.indexOf(MARKER_START);
|
|
535
|
+
const endIdx = modified.indexOf(MARKER_END) + MARKER_END.length;
|
|
536
|
+
if (startIdx !== -1 && endIdx > MARKER_END.length) {
|
|
537
|
+
console.log(chalk.cyan(modified.substring(startIdx, endIdx)));
|
|
538
|
+
}
|
|
539
|
+
console.log("");
|
|
540
|
+
}
|
|
541
|
+
else {
|
|
542
|
+
writeWithBackup(appDelegatePath, modified);
|
|
543
|
+
if (!warn) {
|
|
544
|
+
injectSpinner.succeed(chalk.green("iOS setup complete"));
|
|
545
|
+
}
|
|
546
|
+
iosSuccess = true;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
// ── Summary ──────────────────────────────────────────────
|
|
552
|
+
console.log("");
|
|
553
|
+
if (opts.dryRun) {
|
|
554
|
+
console.log(chalk.blue.bold(" ℹ Dry run complete — no files were modified."));
|
|
555
|
+
console.log(chalk.dim(" Run again without --dry-run to apply changes.\n"));
|
|
556
|
+
}
|
|
557
|
+
else if (androidSuccess || iosSuccess) {
|
|
558
|
+
console.log(chalk.green.bold(" ✔ AppSpacer setup complete!"));
|
|
559
|
+
if (androidSuccess)
|
|
560
|
+
console.log(chalk.dim(" ✔ Android — bundle resolver injected (backup saved as .appspacer.bak)"));
|
|
561
|
+
if (iosSuccess)
|
|
562
|
+
console.log(chalk.dim(" ✔ iOS — bundle URL resolver injected (backup saved as .appspacer.bak)"));
|
|
563
|
+
console.log("");
|
|
564
|
+
console.log(chalk.dim(" Next steps:"));
|
|
565
|
+
console.log(chalk.dim(" 1. Rebuild your app") + chalk.dim(" (npx react-native run-android / run-ios)"));
|
|
566
|
+
console.log(chalk.dim(" 2. Wrap your root component with ") + chalk.cyan("withAppSpacer()"));
|
|
567
|
+
console.log(chalk.dim(" 3. Push an update with ") + chalk.cyan("appspacer release-react-native"));
|
|
568
|
+
console.log(chalk.dim(" 4. To undo all changes run ") + chalk.cyan("appspacer setup:undo"));
|
|
569
|
+
console.log("");
|
|
570
|
+
}
|
|
571
|
+
else {
|
|
572
|
+
console.log(chalk.red.bold(" ✗ Setup failed. See errors above.\n"));
|
|
573
|
+
process.exit(1);
|
|
574
|
+
}
|
|
575
|
+
});
|