appship-cli 0.1.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 +93 -0
- package/data/rules/app-store.yml +47 -0
- package/data/rules/google-play.yml +51 -0
- package/data/sdk-signatures/signatures.yml +376 -0
- package/dist/chunk-7GCQ6I27.js +177 -0
- package/dist/chunk-CVYD5FVI.js +8 -0
- package/dist/chunk-ICHGACSP.js +98 -0
- package/dist/chunk-TLOV7VBN.js +105 -0
- package/dist/flutter-ITOL6J4H.js +12 -0
- package/dist/index.js +3697 -0
- package/dist/native-RCWBM6UF.js +14 -0
- package/dist/upload-FMEKKZSD.js +21 -0
- package/package.json +69 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
APPSHIP_VERSION
|
|
4
|
+
} from "./chunk-CVYD5FVI.js";
|
|
5
|
+
|
|
6
|
+
// src/core/project/flutter.ts
|
|
7
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
8
|
+
import { join as join4 } from "path";
|
|
9
|
+
import { parse } from "yaml";
|
|
10
|
+
|
|
11
|
+
// src/core/project/react-native.ts
|
|
12
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
13
|
+
import { join as join3 } from "path";
|
|
14
|
+
import glob from "fast-glob";
|
|
15
|
+
|
|
16
|
+
// src/core/project/detector.ts
|
|
17
|
+
import { readFile } from "fs/promises";
|
|
18
|
+
import { join } from "path";
|
|
19
|
+
var UnsupportedProjectError = class extends Error {
|
|
20
|
+
};
|
|
21
|
+
async function readPackageJson(projectRoot) {
|
|
22
|
+
try {
|
|
23
|
+
const raw = await readFile(join(projectRoot, "package.json"), "utf8");
|
|
24
|
+
return JSON.parse(raw);
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function detectProjectType(projectRoot) {
|
|
30
|
+
const pkg = await readPackageJson(projectRoot);
|
|
31
|
+
const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
|
|
32
|
+
if (pkg && "react-native" in deps) {
|
|
33
|
+
return "react-native";
|
|
34
|
+
}
|
|
35
|
+
const { readPubspec: readPubspec2, isFlutterPubspec: isFlutterPubspec2 } = await import("./flutter-ITOL6J4H.js");
|
|
36
|
+
if (isFlutterPubspec2(await readPubspec2(projectRoot))) {
|
|
37
|
+
return "flutter";
|
|
38
|
+
}
|
|
39
|
+
const { findRootPbxproj, hasNativeAndroidLayout } = await import("./native-RCWBM6UF.js");
|
|
40
|
+
const { existsSync } = await import("fs");
|
|
41
|
+
if (hasNativeAndroidLayout(projectRoot, existsSync)) {
|
|
42
|
+
return "native-android";
|
|
43
|
+
}
|
|
44
|
+
if ((await findRootPbxproj(projectRoot)).length > 0) {
|
|
45
|
+
return "native-ios";
|
|
46
|
+
}
|
|
47
|
+
throw new UnsupportedProjectError(
|
|
48
|
+
"No supported project detected. AppShip supports React Native (package.json with a react-native dependency), Flutter (pubspec.yaml), native iOS (.xcodeproj at the root), and native Android (settings.gradle + app/ module)."
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/core/project/expo.ts
|
|
53
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
54
|
+
import { join as join2 } from "path";
|
|
55
|
+
var EXPO_APP_JSON_EVIDENCE = "app.json (expo)";
|
|
56
|
+
async function readExpoConfig(projectRoot) {
|
|
57
|
+
try {
|
|
58
|
+
const raw = await readFile2(join2(projectRoot, "app.json"), "utf8");
|
|
59
|
+
const parsed = JSON.parse(raw);
|
|
60
|
+
return parsed.expo ?? null;
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function normalizeAndroidPermission(permission) {
|
|
66
|
+
return permission.includes(".") ? permission : `android.permission.${permission}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/core/project/react-native.ts
|
|
70
|
+
var IGNORE_DIRS = ["**/node_modules/**", "**/Pods/**", "**/build/**", "**/.git/**"];
|
|
71
|
+
async function readAppName(projectRoot) {
|
|
72
|
+
try {
|
|
73
|
+
const raw = await readFile3(join3(projectRoot, "app.json"), "utf8");
|
|
74
|
+
const appJson = JSON.parse(raw);
|
|
75
|
+
return appJson.displayName ?? appJson.expo?.name ?? appJson.name ?? null;
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async function readIosBundleId(projectRoot) {
|
|
81
|
+
const pbxprojFiles = await glob("ios/*.xcodeproj/project.pbxproj", {
|
|
82
|
+
cwd: projectRoot,
|
|
83
|
+
ignore: IGNORE_DIRS,
|
|
84
|
+
absolute: false
|
|
85
|
+
});
|
|
86
|
+
for (const file of pbxprojFiles) {
|
|
87
|
+
const content = await readFile3(join3(projectRoot, file), "utf8");
|
|
88
|
+
const matches = [
|
|
89
|
+
...content.matchAll(/PRODUCT_BUNDLE_IDENTIFIER\s*=\s*"?([A-Za-z0-9.\-$()]+)"?\s*;/g)
|
|
90
|
+
].map((m) => m[1]).filter((id) => !id.endsWith("Tests") && !id.includes("$"));
|
|
91
|
+
if (matches[0]) return matches[0];
|
|
92
|
+
}
|
|
93
|
+
const expo = await readExpoConfig(projectRoot);
|
|
94
|
+
return expo?.ios?.bundleIdentifier ?? null;
|
|
95
|
+
}
|
|
96
|
+
async function readAndroidPackageName(projectRoot) {
|
|
97
|
+
for (const gradleFile of ["android/app/build.gradle", "android/app/build.gradle.kts"]) {
|
|
98
|
+
try {
|
|
99
|
+
const content = await readFile3(join3(projectRoot, gradleFile), "utf8");
|
|
100
|
+
const match = content.match(/applicationId\s*[=(]?\s*["']([A-Za-z0-9._]+)["']/) ?? content.match(/namespace\s*[=(]?\s*["']([A-Za-z0-9._]+)["']/);
|
|
101
|
+
if (match?.[1]) return match[1];
|
|
102
|
+
} catch {
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
const manifest = await readFile3(
|
|
107
|
+
join3(projectRoot, "android/app/src/main/AndroidManifest.xml"),
|
|
108
|
+
"utf8"
|
|
109
|
+
);
|
|
110
|
+
const match = manifest.match(/package\s*=\s*["']([A-Za-z0-9._]+)["']/);
|
|
111
|
+
if (match?.[1]) return match[1];
|
|
112
|
+
} catch {
|
|
113
|
+
}
|
|
114
|
+
const expo = await readExpoConfig(projectRoot);
|
|
115
|
+
return expo?.android?.package ?? null;
|
|
116
|
+
}
|
|
117
|
+
async function analyzeReactNativeProject(projectRoot) {
|
|
118
|
+
const pkg = await readPackageJson(projectRoot);
|
|
119
|
+
const [appName, bundleId, packageName, expo] = await Promise.all([
|
|
120
|
+
readAppName(projectRoot),
|
|
121
|
+
readIosBundleId(projectRoot),
|
|
122
|
+
readAndroidPackageName(projectRoot),
|
|
123
|
+
readExpoConfig(projectRoot)
|
|
124
|
+
]);
|
|
125
|
+
return {
|
|
126
|
+
projectType: "react-native",
|
|
127
|
+
appName: appName ?? pkg?.name ?? null,
|
|
128
|
+
version: pkg?.version ?? expo?.version ?? null,
|
|
129
|
+
ios: { bundleId },
|
|
130
|
+
android: { packageName },
|
|
131
|
+
scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
132
|
+
appshipVersion: APPSHIP_VERSION
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// src/core/project/flutter.ts
|
|
137
|
+
async function readPubspec(projectRoot) {
|
|
138
|
+
try {
|
|
139
|
+
const parsed = parse(await readFile4(join4(projectRoot, "pubspec.yaml"), "utf8"));
|
|
140
|
+
return typeof parsed === "object" && parsed !== null ? parsed : null;
|
|
141
|
+
} catch {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function isFlutterPubspec(pubspec) {
|
|
146
|
+
return pubspec?.dependencies !== void 0 && "flutter" in pubspec.dependencies;
|
|
147
|
+
}
|
|
148
|
+
async function analyzeFlutterProject(projectRoot) {
|
|
149
|
+
const [pubspec, bundleId, packageName] = await Promise.all([
|
|
150
|
+
readPubspec(projectRoot),
|
|
151
|
+
readIosBundleId(projectRoot),
|
|
152
|
+
readAndroidPackageName(projectRoot)
|
|
153
|
+
]);
|
|
154
|
+
const version = typeof pubspec?.version === "string" ? pubspec.version.split("+")[0] : null;
|
|
155
|
+
return {
|
|
156
|
+
projectType: "flutter",
|
|
157
|
+
appName: pubspec?.name ?? null,
|
|
158
|
+
version,
|
|
159
|
+
ios: { bundleId },
|
|
160
|
+
android: { packageName },
|
|
161
|
+
scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
162
|
+
appshipVersion: APPSHIP_VERSION
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export {
|
|
167
|
+
EXPO_APP_JSON_EVIDENCE,
|
|
168
|
+
readExpoConfig,
|
|
169
|
+
normalizeAndroidPermission,
|
|
170
|
+
analyzeReactNativeProject,
|
|
171
|
+
readPubspec,
|
|
172
|
+
isFlutterPubspec,
|
|
173
|
+
analyzeFlutterProject,
|
|
174
|
+
UnsupportedProjectError,
|
|
175
|
+
readPackageJson,
|
|
176
|
+
detectProjectType
|
|
177
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/core/upload/index.ts
|
|
4
|
+
import { spawn } from "child_process";
|
|
5
|
+
import { existsSync } from "fs";
|
|
6
|
+
import { readFile, stat } from "fs/promises";
|
|
7
|
+
import { join } from "path";
|
|
8
|
+
import glob from "fast-glob";
|
|
9
|
+
var UploadError = class extends Error {
|
|
10
|
+
};
|
|
11
|
+
var ARTIFACT_GLOBS = {
|
|
12
|
+
ios: ["*.ipa", "build/**/*.ipa", "ios/build/**/*.ipa"],
|
|
13
|
+
android: [
|
|
14
|
+
"*.aab",
|
|
15
|
+
"android/app/build/outputs/bundle/**/*.aab",
|
|
16
|
+
"app/build/outputs/bundle/**/*.aab",
|
|
17
|
+
"build/**/*.aab"
|
|
18
|
+
]
|
|
19
|
+
};
|
|
20
|
+
var ARTIFACT_IGNORE = ["**/node_modules/**", "**/Pods/**", "**/.git/**"];
|
|
21
|
+
var UPLOAD_LANES = {
|
|
22
|
+
ios: "upload_testflight",
|
|
23
|
+
android: "upload_build"
|
|
24
|
+
};
|
|
25
|
+
async function findArtifact(projectRoot, platform) {
|
|
26
|
+
const matches = await glob(ARTIFACT_GLOBS[platform], {
|
|
27
|
+
cwd: projectRoot,
|
|
28
|
+
ignore: ARTIFACT_IGNORE
|
|
29
|
+
});
|
|
30
|
+
if (matches.length === 0) return null;
|
|
31
|
+
const withTimes = await Promise.all(
|
|
32
|
+
matches.map(async (path) => ({
|
|
33
|
+
path,
|
|
34
|
+
mtime: (await stat(join(projectRoot, path))).mtimeMs
|
|
35
|
+
}))
|
|
36
|
+
);
|
|
37
|
+
withTimes.sort((a, b) => b.mtime - a.mtime);
|
|
38
|
+
return withTimes[0].path;
|
|
39
|
+
}
|
|
40
|
+
function resolveFastlaneCommand(projectRoot, hasGlobalFastlane) {
|
|
41
|
+
if (existsSync(join(projectRoot, "Gemfile"))) {
|
|
42
|
+
return { command: "bundle", prefixArgs: ["exec", "fastlane"] };
|
|
43
|
+
}
|
|
44
|
+
if (hasGlobalFastlane) {
|
|
45
|
+
return { command: "fastlane", prefixArgs: [] };
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
async function assertUploadLane(projectRoot, platform) {
|
|
50
|
+
const fastfilePath = join(projectRoot, "fastlane", "Fastfile");
|
|
51
|
+
if (!existsSync(fastfilePath)) {
|
|
52
|
+
throw new UploadError(
|
|
53
|
+
"fastlane/Fastfile not found. Run `appship export fastlane` first."
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
const fastfile = await readFile(fastfilePath, "utf8");
|
|
57
|
+
const lane = UPLOAD_LANES[platform];
|
|
58
|
+
if (!fastfile.includes(`lane :${lane}`)) {
|
|
59
|
+
throw new UploadError(
|
|
60
|
+
`fastlane/Fastfile has no :${lane} lane. Re-export with \`appship export fastlane --force\` (or add the lane yourself).`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function buildUploadPlan(platform, artifact, track) {
|
|
65
|
+
if (platform === "ios") {
|
|
66
|
+
return {
|
|
67
|
+
platform,
|
|
68
|
+
artifact,
|
|
69
|
+
destination: "TestFlight",
|
|
70
|
+
fastlaneArgs: ["ios", UPLOAD_LANES.ios, `ipa:${artifact}`]
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
platform,
|
|
75
|
+
artifact,
|
|
76
|
+
destination: `Play track "${track}"`,
|
|
77
|
+
fastlaneArgs: ["android", UPLOAD_LANES.android, `aab:${artifact}`, `track:${track}`]
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
var spawnRunner = (command, args, cwd) => new Promise((resolve, reject) => {
|
|
81
|
+
const child = spawn(command, args, { cwd, stdio: "inherit" });
|
|
82
|
+
child.on("error", reject);
|
|
83
|
+
child.on("close", (code) => resolve(code ?? 1));
|
|
84
|
+
});
|
|
85
|
+
async function runUpload(projectRoot, fastlane, plan, runner = spawnRunner) {
|
|
86
|
+
return runner(fastlane.command, [...fastlane.prefixArgs, ...plan.fastlaneArgs], projectRoot);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export {
|
|
90
|
+
UploadError,
|
|
91
|
+
UPLOAD_LANES,
|
|
92
|
+
findArtifact,
|
|
93
|
+
resolveFastlaneCommand,
|
|
94
|
+
assertUploadLane,
|
|
95
|
+
buildUploadPlan,
|
|
96
|
+
spawnRunner,
|
|
97
|
+
runUpload
|
|
98
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
APPSHIP_VERSION
|
|
4
|
+
} from "./chunk-CVYD5FVI.js";
|
|
5
|
+
|
|
6
|
+
// src/core/project/native.ts
|
|
7
|
+
import { readFile } from "fs/promises";
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
import glob from "fast-glob";
|
|
10
|
+
import * as plist from "plist";
|
|
11
|
+
var IGNORE_DIRS = ["**/node_modules/**", "**/Pods/**", "**/build/**", "**/.git/**"];
|
|
12
|
+
async function findRootPbxproj(projectRoot) {
|
|
13
|
+
return glob("*.xcodeproj/project.pbxproj", { cwd: projectRoot, ignore: IGNORE_DIRS });
|
|
14
|
+
}
|
|
15
|
+
function hasNativeAndroidLayout(projectRoot, exists) {
|
|
16
|
+
const hasSettings = exists(join(projectRoot, "settings.gradle")) || exists(join(projectRoot, "settings.gradle.kts"));
|
|
17
|
+
const hasAppModule = exists(join(projectRoot, "app", "build.gradle")) || exists(join(projectRoot, "app", "build.gradle.kts"));
|
|
18
|
+
return hasSettings && hasAppModule;
|
|
19
|
+
}
|
|
20
|
+
function literal(value) {
|
|
21
|
+
return typeof value === "string" && value.length > 0 && !value.includes("$(") ? value : null;
|
|
22
|
+
}
|
|
23
|
+
async function analyzeNativeIosProject(projectRoot) {
|
|
24
|
+
const pbxprojFiles = await findRootPbxproj(projectRoot);
|
|
25
|
+
let bundleId = null;
|
|
26
|
+
let version = null;
|
|
27
|
+
let projectName = null;
|
|
28
|
+
for (const file of pbxprojFiles) {
|
|
29
|
+
const content = await readFile(join(projectRoot, file), "utf8");
|
|
30
|
+
if (!bundleId) {
|
|
31
|
+
const ids = [
|
|
32
|
+
...content.matchAll(/PRODUCT_BUNDLE_IDENTIFIER\s*=\s*"?([A-Za-z0-9.\-$()]+)"?\s*;/g)
|
|
33
|
+
].map((m) => m[1]).filter((id) => !id.endsWith("Tests") && !id.includes("$"));
|
|
34
|
+
bundleId = ids[0] ?? null;
|
|
35
|
+
}
|
|
36
|
+
if (!version) {
|
|
37
|
+
version = content.match(/MARKETING_VERSION\s*=\s*"?([0-9.]+)"?\s*;/)?.[1] ?? null;
|
|
38
|
+
}
|
|
39
|
+
projectName ??= file.split(".xcodeproj")[0] ?? null;
|
|
40
|
+
}
|
|
41
|
+
let appName = null;
|
|
42
|
+
const plistFiles = await glob(["*/Info.plist", "Info.plist"], {
|
|
43
|
+
cwd: projectRoot,
|
|
44
|
+
ignore: IGNORE_DIRS
|
|
45
|
+
});
|
|
46
|
+
for (const file of plistFiles) {
|
|
47
|
+
try {
|
|
48
|
+
const parsed = plist.parse(await readFile(join(projectRoot, file), "utf8"));
|
|
49
|
+
appName = literal(parsed["CFBundleDisplayName"]) ?? literal(parsed["CFBundleName"]);
|
|
50
|
+
if (appName) break;
|
|
51
|
+
} catch {
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
projectType: "native-ios",
|
|
56
|
+
appName: appName ?? projectName,
|
|
57
|
+
version,
|
|
58
|
+
ios: { bundleId },
|
|
59
|
+
android: null,
|
|
60
|
+
scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
61
|
+
appshipVersion: APPSHIP_VERSION
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
async function tryRead(path) {
|
|
65
|
+
try {
|
|
66
|
+
return await readFile(path, "utf8");
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async function analyzeNativeAndroidProject(projectRoot) {
|
|
72
|
+
let packageName = null;
|
|
73
|
+
let version = null;
|
|
74
|
+
for (const gradleFile of ["app/build.gradle", "app/build.gradle.kts"]) {
|
|
75
|
+
const content = await tryRead(join(projectRoot, gradleFile));
|
|
76
|
+
if (!content) continue;
|
|
77
|
+
packageName ??= content.match(/applicationId\s*[=(]?\s*["']([A-Za-z0-9._]+)["']/)?.[1] ?? content.match(/namespace\s*[=(]?\s*["']([A-Za-z0-9._]+)["']/)?.[1] ?? null;
|
|
78
|
+
version ??= content.match(/versionName\s*[=(]?\s*["']([^"']+)["']/)?.[1] ?? null;
|
|
79
|
+
}
|
|
80
|
+
let appName = null;
|
|
81
|
+
for (const settingsFile of ["settings.gradle", "settings.gradle.kts"]) {
|
|
82
|
+
const content = await tryRead(join(projectRoot, settingsFile));
|
|
83
|
+
appName ??= content?.match(/rootProject\.name\s*=\s*["']([^"']+)["']/)?.[1] ?? null;
|
|
84
|
+
}
|
|
85
|
+
if (!appName) {
|
|
86
|
+
const strings = await tryRead(join(projectRoot, "app/src/main/res/values/strings.xml"));
|
|
87
|
+
appName = strings?.match(/<string name="app_name">([^<]+)<\/string>/)?.[1] ?? null;
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
projectType: "native-android",
|
|
91
|
+
appName,
|
|
92
|
+
version,
|
|
93
|
+
ios: null,
|
|
94
|
+
android: { packageName },
|
|
95
|
+
scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
96
|
+
appshipVersion: APPSHIP_VERSION
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export {
|
|
101
|
+
findRootPbxproj,
|
|
102
|
+
hasNativeAndroidLayout,
|
|
103
|
+
analyzeNativeIosProject,
|
|
104
|
+
analyzeNativeAndroidProject
|
|
105
|
+
};
|