@better-update/cli 0.3.0 → 0.3.2
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/dist/index.js +5319 -0
- package/dist/index.js.map +1 -0
- package/package.json +12 -9
- package/CHANGELOG.md +0 -58
- package/oxlint.config.ts +0 -6
- package/src/app-layer.ts +0 -29
- package/src/application/build-workflow.ts +0 -222
- package/src/application/command-exit.ts +0 -13
- package/src/application/login.ts +0 -87
- package/src/application/update-promote.ts +0 -88
- package/src/application/update-publish.ts +0 -402
- package/src/application/update-rollback.ts +0 -275
- package/src/commands/analytics/adoption.ts +0 -40
- package/src/commands/analytics/channels.ts +0 -35
- package/src/commands/analytics/helpers.ts +0 -3
- package/src/commands/analytics/index.ts +0 -13
- package/src/commands/analytics/platforms.ts +0 -39
- package/src/commands/analytics/updates.ts +0 -35
- package/src/commands/audit-logs/helpers.ts +0 -3
- package/src/commands/audit-logs/index.ts +0 -8
- package/src/commands/audit-logs/list.ts +0 -66
- package/src/commands/branches.ts +0 -70
- package/src/commands/build/android.ts +0 -129
- package/src/commands/build/index.ts +0 -63
- package/src/commands/build/ios.ts +0 -199
- package/src/commands/build/reserve-and-upload.test.ts +0 -263
- package/src/commands/build/reserve-and-upload.ts +0 -160
- package/src/commands/build/run-step.ts +0 -131
- package/src/commands/builds/compatibility-matrix.ts +0 -48
- package/src/commands/builds/delete.ts +0 -15
- package/src/commands/builds/get.ts +0 -34
- package/src/commands/builds/helpers.ts +0 -3
- package/src/commands/builds/index.ts +0 -20
- package/src/commands/builds/install-link.ts +0 -20
- package/src/commands/builds/list.ts +0 -38
- package/src/commands/channels/create.ts +0 -37
- package/src/commands/channels/delete.ts +0 -15
- package/src/commands/channels/helpers.ts +0 -18
- package/src/commands/channels/index.ts +0 -24
- package/src/commands/channels/list.ts +0 -38
- package/src/commands/channels/pause.ts +0 -15
- package/src/commands/channels/resume.ts +0 -15
- package/src/commands/channels/rollout/complete.ts +0 -17
- package/src/commands/channels/rollout/create.ts +0 -36
- package/src/commands/channels/rollout/index.ts +0 -11
- package/src/commands/channels/rollout/revert.ts +0 -17
- package/src/commands/channels/rollout/update.ts +0 -23
- package/src/commands/channels/update.ts +0 -32
- package/src/commands/credentials/delete.ts +0 -24
- package/src/commands/credentials/index.ts +0 -10
- package/src/commands/credentials/list.ts +0 -33
- package/src/commands/credentials/upload.ts +0 -91
- package/src/commands/env/delete.ts +0 -35
- package/src/commands/env/export.ts +0 -27
- package/src/commands/env/get.ts +0 -25
- package/src/commands/env/helpers.ts +0 -13
- package/src/commands/env/import.ts +0 -31
- package/src/commands/env/index.ts +0 -24
- package/src/commands/env/list.ts +0 -44
- package/src/commands/env/pull.ts +0 -27
- package/src/commands/env/set.ts +0 -42
- package/src/commands/fingerprint/compare.ts +0 -25
- package/src/commands/fingerprint/generate.ts +0 -18
- package/src/commands/fingerprint/index.ts +0 -9
- package/src/commands/init.ts +0 -35
- package/src/commands/login.ts +0 -13
- package/src/commands/logout.ts +0 -12
- package/src/commands/projects.ts +0 -84
- package/src/commands/status.ts +0 -48
- package/src/commands/update/delete.ts +0 -15
- package/src/commands/update/helpers.ts +0 -22
- package/src/commands/update/index.ts +0 -22
- package/src/commands/update/list.ts +0 -60
- package/src/commands/update/promote.ts +0 -30
- package/src/commands/update/publish.ts +0 -94
- package/src/commands/update/rollback.ts +0 -42
- package/src/commands/update/rollout/complete.ts +0 -17
- package/src/commands/update/rollout/index.ts +0 -10
- package/src/commands/update/rollout/revert.ts +0 -17
- package/src/commands/update/rollout/set.ts +0 -23
- package/src/index.ts +0 -53
- package/src/lib/android-keystore.test.ts +0 -114
- package/src/lib/android-keystore.ts +0 -76
- package/src/lib/android-signing-gradle.test.ts +0 -95
- package/src/lib/android-signing-gradle.ts +0 -52
- package/src/lib/app-json.ts +0 -81
- package/src/lib/apple-auth.test.ts +0 -402
- package/src/lib/apple-auth.ts +0 -132
- package/src/lib/artifact-finder.test.ts +0 -195
- package/src/lib/artifact-finder.ts +0 -122
- package/src/lib/browser-login.test.ts +0 -88
- package/src/lib/browser-login.ts +0 -193
- package/src/lib/build-profile.test.ts +0 -290
- package/src/lib/build-profile.ts +0 -234
- package/src/lib/cli-schemas.ts +0 -39
- package/src/lib/command-errors.ts +0 -60
- package/src/lib/credentials-downloader.ts +0 -181
- package/src/lib/credentials-manager.ts +0 -354
- package/src/lib/env-exporter.test.ts +0 -96
- package/src/lib/env-exporter.ts +0 -28
- package/src/lib/exit-codes.ts +0 -82
- package/src/lib/expo-config.ts +0 -130
- package/src/lib/expo-export.test.ts +0 -94
- package/src/lib/expo-export.ts +0 -281
- package/src/lib/fingerprint.ts +0 -67
- package/src/lib/format-error.ts +0 -22
- package/src/lib/git-context.ts +0 -56
- package/src/lib/gradle-config.ts +0 -126
- package/src/lib/ios-export-options.test.ts +0 -98
- package/src/lib/ios-export-options.ts +0 -62
- package/src/lib/ios-keychain.ts +0 -181
- package/src/lib/ios-provisioning.test.ts +0 -115
- package/src/lib/ios-provisioning.ts +0 -179
- package/src/lib/output.ts +0 -32
- package/src/lib/pkcs12.ts +0 -73
- package/src/lib/plist.ts +0 -39
- package/src/lib/post-build-validation.ts +0 -146
- package/src/lib/presigned-upload.test.ts +0 -140
- package/src/lib/presigned-upload.ts +0 -35
- package/src/lib/record.ts +0 -5
- package/src/lib/resolve-named-resource.ts +0 -24
- package/src/lib/runtime-version.test.ts +0 -119
- package/src/lib/runtime-version.ts +0 -62
- package/src/lib/sha256.test.ts +0 -108
- package/src/lib/sha256.ts +0 -80
- package/src/lib/signed-payloads.test.ts +0 -181
- package/src/lib/signed-payloads.ts +0 -164
- package/src/lib/string-utils.ts +0 -4
- package/src/lib/temp-dir.ts +0 -14
- package/src/lib/test-utils.ts +0 -13
- package/src/lib/update-platforms.test.ts +0 -45
- package/src/lib/update-platforms.ts +0 -19
- package/src/lib/xcpretty-formatter.ts +0 -21
- package/src/services/api-client.ts +0 -42
- package/src/services/apple-session-store.ts +0 -100
- package/src/services/auth-store.ts +0 -85
- package/src/services/cli-runtime.ts +0 -46
- package/src/services/config-store.ts +0 -108
- package/src/services/presigned-upload.ts +0 -84
- package/src/services/update-asset-uploader.ts +0 -72
- package/src/types/keychain.d.ts +0 -22
- package/tests/e2e/build.test.ts +0 -270
- package/tests/e2e/commands.test.ts +0 -694
- package/tests/e2e/ota-lifecycle.test.ts +0 -275
- package/tests/e2e/publish.test.ts +0 -150
- package/tests/helpers/cli-e2e.ts +0 -426
- package/tests/helpers/pty-driver.ts +0 -142
- package/tests/interactive/harness/provider-prompt.ts +0 -54
- package/tests/interactive/login.test.ts +0 -47
- package/tests/interactive/provider-select.test.ts +0 -59
- package/tsconfig.json +0 -7
- package/vitest.config.ts +0 -38
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import { renderExportOptionsPlist } from "./ios-export-options";
|
|
2
|
-
|
|
3
|
-
// ── snapshot-style tests for each method ──────────────────────────
|
|
4
|
-
|
|
5
|
-
describe(renderExportOptionsPlist, () => {
|
|
6
|
-
it("app-store method includes uploadSymbols=true", () => {
|
|
7
|
-
const plist = renderExportOptionsPlist({
|
|
8
|
-
method: "app-store",
|
|
9
|
-
teamId: "ABCD1234EF",
|
|
10
|
-
bundleId: "com.example.app",
|
|
11
|
-
provisioningProfileName: "My AppStore Profile",
|
|
12
|
-
});
|
|
13
|
-
expect(plist).toMatchInlineSnapshot(`
|
|
14
|
-
"<?xml version="1.0" encoding="UTF-8"?>
|
|
15
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
16
|
-
<plist version="1.0">
|
|
17
|
-
<dict>
|
|
18
|
-
<key>method</key>
|
|
19
|
-
<string>app-store</string>
|
|
20
|
-
<key>teamID</key>
|
|
21
|
-
<string>ABCD1234EF</string>
|
|
22
|
-
<key>signingStyle</key>
|
|
23
|
-
<string>manual</string>
|
|
24
|
-
<key>compileBitcode</key>
|
|
25
|
-
<false/>
|
|
26
|
-
<key>provisioningProfiles</key>
|
|
27
|
-
<dict>
|
|
28
|
-
<key>com.example.app</key>
|
|
29
|
-
<string>My AppStore Profile</string>
|
|
30
|
-
</dict>
|
|
31
|
-
<key>uploadSymbols</key>
|
|
32
|
-
<true/>
|
|
33
|
-
</dict>
|
|
34
|
-
</plist>
|
|
35
|
-
"
|
|
36
|
-
`);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("ad-hoc method omits uploadSymbols", () => {
|
|
40
|
-
const plist = renderExportOptionsPlist({
|
|
41
|
-
method: "ad-hoc",
|
|
42
|
-
teamId: "ABCD1234EF",
|
|
43
|
-
bundleId: "com.example.app",
|
|
44
|
-
provisioningProfileName: "My AdHoc Profile",
|
|
45
|
-
});
|
|
46
|
-
expect(plist).toContain("<string>ad-hoc</string>");
|
|
47
|
-
expect(plist).not.toContain("uploadSymbols");
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("enterprise method omits uploadSymbols", () => {
|
|
51
|
-
const plist = renderExportOptionsPlist({
|
|
52
|
-
method: "enterprise",
|
|
53
|
-
teamId: "XYZ9876543",
|
|
54
|
-
bundleId: "com.enterprise.app",
|
|
55
|
-
provisioningProfileName: "Enterprise Profile",
|
|
56
|
-
});
|
|
57
|
-
expect(plist).toContain("<string>enterprise</string>");
|
|
58
|
-
expect(plist).not.toContain("uploadSymbols");
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("development method omits uploadSymbols", () => {
|
|
62
|
-
const plist = renderExportOptionsPlist({
|
|
63
|
-
method: "development",
|
|
64
|
-
teamId: "DEV1234567",
|
|
65
|
-
bundleId: "com.dev.app",
|
|
66
|
-
provisioningProfileName: "Dev Profile",
|
|
67
|
-
});
|
|
68
|
-
expect(plist).toContain("<string>development</string>");
|
|
69
|
-
expect(plist).not.toContain("uploadSymbols");
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it("compileBitcode=true renders <true/>", () => {
|
|
73
|
-
const plist = renderExportOptionsPlist({
|
|
74
|
-
method: "ad-hoc",
|
|
75
|
-
teamId: "ABCD1234EF",
|
|
76
|
-
bundleId: "com.example.app",
|
|
77
|
-
provisioningProfileName: "My Profile",
|
|
78
|
-
compileBitcode: true,
|
|
79
|
-
});
|
|
80
|
-
expect(plist).toContain("<key>compileBitcode</key>\n\t<true/>");
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it("xML-escapes &, <, >, ', \" in bundleId and profile name", () => {
|
|
84
|
-
const plist = renderExportOptionsPlist({
|
|
85
|
-
method: "ad-hoc",
|
|
86
|
-
teamId: "T1",
|
|
87
|
-
bundleId: "com.a&b.app",
|
|
88
|
-
provisioningProfileName: '<test & "name">',
|
|
89
|
-
});
|
|
90
|
-
// BundleId escaped
|
|
91
|
-
expect(plist).toContain("com.a&b.app");
|
|
92
|
-
// Profile name escaped
|
|
93
|
-
expect(plist).toContain("<test & "name">");
|
|
94
|
-
// No raw special chars present inside the escaped segments
|
|
95
|
-
expect(plist).not.toContain("com.a&b.app");
|
|
96
|
-
expect(plist).not.toContain('<test & "name">');
|
|
97
|
-
});
|
|
98
|
-
});
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
export type ExportMethod = "app-store" | "ad-hoc" | "enterprise" | "development";
|
|
2
|
-
|
|
3
|
-
export interface RenderExportOptionsPlistInput {
|
|
4
|
-
readonly method: ExportMethod;
|
|
5
|
-
readonly teamId: string;
|
|
6
|
-
readonly bundleId: string;
|
|
7
|
-
readonly provisioningProfileName: string;
|
|
8
|
-
readonly compileBitcode?: boolean;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const escapeXml = (value: string): string =>
|
|
12
|
-
value
|
|
13
|
-
.replaceAll("&", "&")
|
|
14
|
-
.replaceAll("<", "<")
|
|
15
|
-
.replaceAll(">", ">")
|
|
16
|
-
.replaceAll('"', """)
|
|
17
|
-
.replaceAll("'", "'");
|
|
18
|
-
|
|
19
|
-
const boolTag = (value: boolean): string => (value ? "<true/>" : "<false/>");
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Render an Xcode `ExportOptions.plist` for `xcodebuild -exportArchive`.
|
|
23
|
-
*
|
|
24
|
-
* - `signingStyle` is always `manual` (ephemeral keychain + downloaded profile)
|
|
25
|
-
* - `uploadSymbols` is emitted only for `app-store` exports
|
|
26
|
-
* - `provisioningProfiles` dict maps bundleId → profile name
|
|
27
|
-
* - `compileBitcode` defaults to `false`
|
|
28
|
-
*/
|
|
29
|
-
export const renderExportOptionsPlist = ({
|
|
30
|
-
method,
|
|
31
|
-
teamId,
|
|
32
|
-
bundleId,
|
|
33
|
-
provisioningProfileName,
|
|
34
|
-
compileBitcode = false,
|
|
35
|
-
}: RenderExportOptionsPlistInput): string => {
|
|
36
|
-
const lines: string[] = [
|
|
37
|
-
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
38
|
-
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
39
|
-
'<plist version="1.0">',
|
|
40
|
-
"<dict>",
|
|
41
|
-
"\t<key>method</key>",
|
|
42
|
-
`\t<string>${escapeXml(method)}</string>`,
|
|
43
|
-
"\t<key>teamID</key>",
|
|
44
|
-
`\t<string>${escapeXml(teamId)}</string>`,
|
|
45
|
-
"\t<key>signingStyle</key>",
|
|
46
|
-
"\t<string>manual</string>",
|
|
47
|
-
"\t<key>compileBitcode</key>",
|
|
48
|
-
`\t${boolTag(compileBitcode)}`,
|
|
49
|
-
"\t<key>provisioningProfiles</key>",
|
|
50
|
-
"\t<dict>",
|
|
51
|
-
`\t\t<key>${escapeXml(bundleId)}</key>`,
|
|
52
|
-
`\t\t<string>${escapeXml(provisioningProfileName)}</string>`,
|
|
53
|
-
"\t</dict>",
|
|
54
|
-
];
|
|
55
|
-
|
|
56
|
-
if (method === "app-store") {
|
|
57
|
-
lines.push("\t<key>uploadSymbols</key>", "\t<true/>");
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
lines.push("</dict>", "</plist>", "");
|
|
61
|
-
return lines.join("\n");
|
|
62
|
-
};
|
package/src/lib/ios-keychain.ts
DELETED
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
import { randomBytes, randomUUID } from "node:crypto";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
|
|
4
|
-
import { Command } from "@effect/platform";
|
|
5
|
-
import { Effect } from "effect";
|
|
6
|
-
|
|
7
|
-
import type { CommandExecutor } from "@effect/platform";
|
|
8
|
-
import type { Scope } from "effect";
|
|
9
|
-
|
|
10
|
-
import { KeychainError } from "./exit-codes";
|
|
11
|
-
|
|
12
|
-
export interface AcquireKeychainOptions {
|
|
13
|
-
readonly tempDir: string;
|
|
14
|
-
readonly p12Path: string;
|
|
15
|
-
readonly p12Password: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface KeychainHandle {
|
|
19
|
-
readonly keychainName: string;
|
|
20
|
-
readonly keychainPath: string;
|
|
21
|
-
readonly signingIdentity: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// ── shell helpers ─────────────────────────────────────────────────
|
|
25
|
-
|
|
26
|
-
const runOrFail = (
|
|
27
|
-
cmd: Command.Command,
|
|
28
|
-
step: string,
|
|
29
|
-
): Effect.Effect<string, KeychainError, CommandExecutor.CommandExecutor> =>
|
|
30
|
-
Command.string(cmd).pipe(
|
|
31
|
-
Effect.mapError(
|
|
32
|
-
(cause) =>
|
|
33
|
-
new KeychainError({
|
|
34
|
-
message: `keychain ${step} failed: ${String(cause)}`,
|
|
35
|
-
}),
|
|
36
|
-
),
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
const listCurrentKeychains = Effect.gen(function* () {
|
|
40
|
-
// `security list-keychains -d user` returns each keychain path on its own line,
|
|
41
|
-
// Surrounded by double quotes and optionally preceded by whitespace.
|
|
42
|
-
const output = yield* runOrFail(
|
|
43
|
-
Command.make("security", "list-keychains", "-d", "user"),
|
|
44
|
-
"list-keychains",
|
|
45
|
-
);
|
|
46
|
-
return output
|
|
47
|
-
.split("\n")
|
|
48
|
-
.map((line) => line.trim().replace(/^"/, "").replace(/"$/, ""))
|
|
49
|
-
.filter((line) => line.length > 0);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
// Parse `security find-identity -v <keychain>` output to extract the first
|
|
53
|
-
// Signing identity. Lines look like:
|
|
54
|
-
// 1) 1A2B3C4D... "Apple Distribution: Your Name (TEAMID)"
|
|
55
|
-
const parseSigningIdentity = (output: string): string | undefined => {
|
|
56
|
-
const lines = output.split("\n");
|
|
57
|
-
for (const line of lines) {
|
|
58
|
-
const match = /"([^"]+)"/.exec(line);
|
|
59
|
-
if (match?.[1]) {
|
|
60
|
-
return match[1];
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
return undefined;
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
// ── acquireRelease ────────────────────────────────────────────────
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Acquire an ephemeral macOS keychain, import a `.p12` into it, add it to the
|
|
70
|
-
* user search list, and tear it all down on scope close. The keychain name is
|
|
71
|
-
* namespaced as `better-update-<uuid>` and lives in `$tempDir`, so cleanup is
|
|
72
|
-
* guaranteed under all termination paths.
|
|
73
|
-
*/
|
|
74
|
-
export const acquireKeychain = ({
|
|
75
|
-
tempDir,
|
|
76
|
-
p12Path,
|
|
77
|
-
p12Password,
|
|
78
|
-
}: AcquireKeychainOptions): Effect.Effect<
|
|
79
|
-
KeychainHandle,
|
|
80
|
-
KeychainError,
|
|
81
|
-
CommandExecutor.CommandExecutor | Scope.Scope
|
|
82
|
-
> => {
|
|
83
|
-
const keychainName = `better-update-${randomUUID()}.keychain-db`;
|
|
84
|
-
const keychainPath = path.join(tempDir, keychainName);
|
|
85
|
-
const keychainPassword = randomBytes(32).toString("hex");
|
|
86
|
-
|
|
87
|
-
return Effect.acquireRelease(
|
|
88
|
-
// ── acquire ─────────────────────────────────────────────────
|
|
89
|
-
Effect.gen(function* () {
|
|
90
|
-
const priorKeychains = yield* listCurrentKeychains;
|
|
91
|
-
|
|
92
|
-
yield* runOrFail(
|
|
93
|
-
Command.make("security", "create-keychain", "-p", keychainPassword, keychainPath),
|
|
94
|
-
"create-keychain",
|
|
95
|
-
);
|
|
96
|
-
|
|
97
|
-
yield* runOrFail(
|
|
98
|
-
Command.make("security", "unlock-keychain", "-p", keychainPassword, keychainPath),
|
|
99
|
-
"unlock-keychain",
|
|
100
|
-
);
|
|
101
|
-
|
|
102
|
-
yield* runOrFail(
|
|
103
|
-
Command.make("security", "set-keychain-settings", "-t", "3600", "-l", keychainPath),
|
|
104
|
-
"set-keychain-settings",
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
yield* runOrFail(
|
|
108
|
-
Command.make(
|
|
109
|
-
"security",
|
|
110
|
-
"import",
|
|
111
|
-
p12Path,
|
|
112
|
-
"-k",
|
|
113
|
-
keychainPath,
|
|
114
|
-
"-P",
|
|
115
|
-
p12Password,
|
|
116
|
-
"-T",
|
|
117
|
-
"/usr/bin/codesign",
|
|
118
|
-
),
|
|
119
|
-
"import",
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
yield* runOrFail(
|
|
123
|
-
Command.make(
|
|
124
|
-
"security",
|
|
125
|
-
"set-key-partition-list",
|
|
126
|
-
"-S",
|
|
127
|
-
"apple-tool:,apple:,codesign:",
|
|
128
|
-
"-s",
|
|
129
|
-
"-k",
|
|
130
|
-
keychainPassword,
|
|
131
|
-
keychainPath,
|
|
132
|
-
),
|
|
133
|
-
"set-key-partition-list",
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
// Prepend our keychain to the search list while preserving the user's
|
|
137
|
-
// Existing ones.
|
|
138
|
-
yield* runOrFail(
|
|
139
|
-
Command.make(
|
|
140
|
-
"security",
|
|
141
|
-
"list-keychains",
|
|
142
|
-
"-d",
|
|
143
|
-
"user",
|
|
144
|
-
"-s",
|
|
145
|
-
keychainPath,
|
|
146
|
-
...priorKeychains,
|
|
147
|
-
),
|
|
148
|
-
"list-keychains -s (add)",
|
|
149
|
-
);
|
|
150
|
-
|
|
151
|
-
const identitiesOutput = yield* runOrFail(
|
|
152
|
-
Command.make("security", "find-identity", "-v", "-p", "codesigning", keychainPath),
|
|
153
|
-
"find-identity",
|
|
154
|
-
);
|
|
155
|
-
const signingIdentity = parseSigningIdentity(identitiesOutput);
|
|
156
|
-
if (!signingIdentity) {
|
|
157
|
-
return yield* new KeychainError({
|
|
158
|
-
message: "No code signing identity found after importing .p12 into ephemeral keychain.",
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return {
|
|
163
|
-
handle: { keychainName, keychainPath, signingIdentity },
|
|
164
|
-
priorKeychains,
|
|
165
|
-
};
|
|
166
|
-
}),
|
|
167
|
-
|
|
168
|
-
// ── release ─────────────────────────────────────────────────
|
|
169
|
-
({ priorKeychains }) =>
|
|
170
|
-
Effect.gen(function* () {
|
|
171
|
-
// Restore the original search list first, then delete our keychain.
|
|
172
|
-
yield* Command.string(
|
|
173
|
-
Command.make("security", "list-keychains", "-d", "user", "-s", ...priorKeychains),
|
|
174
|
-
).pipe(Effect.catchAll(() => Effect.void));
|
|
175
|
-
|
|
176
|
-
yield* Command.string(Command.make("security", "delete-keychain", keychainPath)).pipe(
|
|
177
|
-
Effect.catchAll(() => Effect.void),
|
|
178
|
-
);
|
|
179
|
-
}),
|
|
180
|
-
).pipe(Effect.map(({ handle }) => handle));
|
|
181
|
-
};
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import { it } from "@effect/vitest";
|
|
2
|
-
import { Effect, Exit } from "effect";
|
|
3
|
-
|
|
4
|
-
import { ProvisioningError } from "./exit-codes";
|
|
5
|
-
import { extractProvisioningInfo } from "./ios-provisioning";
|
|
6
|
-
import { failureError } from "./test-utils";
|
|
7
|
-
|
|
8
|
-
// ── fixtures ──────────────────────────────────────────────────────
|
|
9
|
-
|
|
10
|
-
const VALID_PLIST = `<?xml version="1.0" encoding="UTF-8"?>
|
|
11
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
12
|
-
<plist version="1.0">
|
|
13
|
-
<dict>
|
|
14
|
-
\t<key>AppIDName</key>
|
|
15
|
-
\t<string>My App</string>
|
|
16
|
-
\t<key>ApplicationIdentifierPrefix</key>
|
|
17
|
-
\t<array>
|
|
18
|
-
\t\t<string>ABCD1234EF</string>
|
|
19
|
-
\t</array>
|
|
20
|
-
\t<key>CreationDate</key>
|
|
21
|
-
\t<date>2026-01-15T12:00:00Z</date>
|
|
22
|
-
\t<key>Platform</key>
|
|
23
|
-
\t<array>
|
|
24
|
-
\t\t<string>iOS</string>
|
|
25
|
-
\t</array>
|
|
26
|
-
\t<key>DeveloperCertificates</key>
|
|
27
|
-
\t<array>
|
|
28
|
-
\t\t<data>SOMEBASE64DATA==</data>
|
|
29
|
-
\t</array>
|
|
30
|
-
\t<key>Entitlements</key>
|
|
31
|
-
\t<dict>
|
|
32
|
-
\t\t<key>application-identifier</key>
|
|
33
|
-
\t\t<string>ABCD1234EF.com.example.app</string>
|
|
34
|
-
\t\t<key>get-task-allow</key>
|
|
35
|
-
\t\t<false/>
|
|
36
|
-
\t</dict>
|
|
37
|
-
\t<key>Name</key>
|
|
38
|
-
\t<string>MyApp AppStore</string>
|
|
39
|
-
\t<key>ProvisionedDevices</key>
|
|
40
|
-
\t<array>
|
|
41
|
-
\t\t<string>00008030-000000000000000E</string>
|
|
42
|
-
\t</array>
|
|
43
|
-
\t<key>TeamIdentifier</key>
|
|
44
|
-
\t<array>
|
|
45
|
-
\t\t<string>ABCD1234EF</string>
|
|
46
|
-
\t\t<string>XYZ0000000</string>
|
|
47
|
-
\t</array>
|
|
48
|
-
\t<key>TeamName</key>
|
|
49
|
-
\t<string>Example Inc.</string>
|
|
50
|
-
\t<key>TimeToLive</key>
|
|
51
|
-
\t<integer>365</integer>
|
|
52
|
-
\t<key>UUID</key>
|
|
53
|
-
\t<string>11111111-2222-3333-4444-555555555555</string>
|
|
54
|
-
\t<key>Version</key>
|
|
55
|
-
\t<integer>1</integer>
|
|
56
|
-
</dict>
|
|
57
|
-
</plist>`;
|
|
58
|
-
|
|
59
|
-
// ── tests ─────────────────────────────────────────────────────────
|
|
60
|
-
|
|
61
|
-
describe(extractProvisioningInfo, () => {
|
|
62
|
-
it.effect("parses UUID, Name, and first TeamIdentifier from a realistic plist", () =>
|
|
63
|
-
Effect.gen(function* () {
|
|
64
|
-
const info = yield* extractProvisioningInfo(VALID_PLIST);
|
|
65
|
-
expect(info.uuid).toBe("11111111-2222-3333-4444-555555555555");
|
|
66
|
-
expect(info.name).toBe("MyApp AppStore");
|
|
67
|
-
expect(info.teamId).toBe("ABCD1234EF");
|
|
68
|
-
}),
|
|
69
|
-
);
|
|
70
|
-
|
|
71
|
-
it.effect("fails with ProvisioningError when UUID is missing", () =>
|
|
72
|
-
Effect.gen(function* () {
|
|
73
|
-
const withoutUuid = VALID_PLIST.replace(/<key>UUID<\/key>\s*<string>[^<]+<\/string>/, "");
|
|
74
|
-
const exit = yield* extractProvisioningInfo(withoutUuid).pipe(Effect.exit);
|
|
75
|
-
expect(Exit.isFailure(exit)).toBe(true);
|
|
76
|
-
if (Exit.isFailure(exit)) {
|
|
77
|
-
const error = failureError(exit);
|
|
78
|
-
expect(error).toBeInstanceOf(ProvisioningError);
|
|
79
|
-
expect(error!.message).toContain("UUID");
|
|
80
|
-
}
|
|
81
|
-
}),
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
it.effect("fails when TeamIdentifier array is missing", () =>
|
|
85
|
-
Effect.gen(function* () {
|
|
86
|
-
const withoutTeam = VALID_PLIST.replace(
|
|
87
|
-
/<key>TeamIdentifier<\/key>\s*<array>[\s\S]*?<\/array>/,
|
|
88
|
-
"",
|
|
89
|
-
);
|
|
90
|
-
const exit = yield* extractProvisioningInfo(withoutTeam).pipe(Effect.exit);
|
|
91
|
-
expect(Exit.isFailure(exit)).toBe(true);
|
|
92
|
-
if (Exit.isFailure(exit)) {
|
|
93
|
-
const error = failureError(exit);
|
|
94
|
-
expect(error!.message).toContain("TeamIdentifier");
|
|
95
|
-
}
|
|
96
|
-
}),
|
|
97
|
-
);
|
|
98
|
-
|
|
99
|
-
it.effect("fails when Name is missing", () =>
|
|
100
|
-
Effect.gen(function* () {
|
|
101
|
-
const withoutName = VALID_PLIST.replace(/<key>Name<\/key>\s*<string>[^<]+<\/string>/, "");
|
|
102
|
-
const exit = yield* extractProvisioningInfo(withoutName).pipe(Effect.exit);
|
|
103
|
-
expect(Exit.isFailure(exit)).toBe(true);
|
|
104
|
-
}),
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
it.effect("picks the first <string> inside TeamIdentifier array even if multiple", () =>
|
|
108
|
-
Effect.gen(function* () {
|
|
109
|
-
const info = yield* extractProvisioningInfo(VALID_PLIST);
|
|
110
|
-
expect(info.teamId).toBe("ABCD1234EF");
|
|
111
|
-
// Second string should NOT be chosen
|
|
112
|
-
expect(info.teamId).not.toBe("XYZ0000000");
|
|
113
|
-
}),
|
|
114
|
-
);
|
|
115
|
-
});
|
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
import os from "node:os";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
|
|
4
|
-
import { Command, FileSystem } from "@effect/platform";
|
|
5
|
-
import { Effect } from "effect";
|
|
6
|
-
|
|
7
|
-
import type { CommandExecutor } from "@effect/platform";
|
|
8
|
-
import type { Scope } from "effect";
|
|
9
|
-
|
|
10
|
-
import { ProvisioningError } from "./exit-codes";
|
|
11
|
-
import { parsePlistXml } from "./plist";
|
|
12
|
-
|
|
13
|
-
import type { PlistObject } from "./plist";
|
|
14
|
-
|
|
15
|
-
// ── pure extraction ───────────────────────────────────────────────
|
|
16
|
-
|
|
17
|
-
export interface ProvisioningInfo {
|
|
18
|
-
readonly uuid: string;
|
|
19
|
-
readonly name: string;
|
|
20
|
-
readonly teamId: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const getString = (obj: PlistObject, key: string): string | undefined => {
|
|
24
|
-
const value = obj[key];
|
|
25
|
-
return typeof value === "string" ? value : undefined;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
const getFirstArrayString = (obj: PlistObject, key: string): string | undefined => {
|
|
29
|
-
const value = obj[key];
|
|
30
|
-
if (Array.isArray(value) && typeof value[0] === "string") {
|
|
31
|
-
return value[0];
|
|
32
|
-
}
|
|
33
|
-
return undefined;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Extract `UUID`, `Name`, and the first `TeamIdentifier` from the XML plist
|
|
38
|
-
* output of `security cms -D -i <path>`. Returns `ProvisioningError` when any
|
|
39
|
-
* of the three fields are missing.
|
|
40
|
-
*/
|
|
41
|
-
export const extractProvisioningInfo = (
|
|
42
|
-
plistXml: string,
|
|
43
|
-
): Effect.Effect<ProvisioningInfo, ProvisioningError> =>
|
|
44
|
-
Effect.gen(function* () {
|
|
45
|
-
const parsed: PlistObject = yield* Effect.try({
|
|
46
|
-
try: () => parsePlistXml(plistXml),
|
|
47
|
-
catch: (error) =>
|
|
48
|
-
new ProvisioningError({
|
|
49
|
-
message: `Failed to parse provisioning profile plist: ${error instanceof Error ? error.message : String(error)}`,
|
|
50
|
-
}),
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
const uuid = getString(parsed, "UUID");
|
|
54
|
-
const name = getString(parsed, "Name");
|
|
55
|
-
const teamId = getFirstArrayString(parsed, "TeamIdentifier");
|
|
56
|
-
|
|
57
|
-
if (!uuid || !name || !teamId) {
|
|
58
|
-
return yield* new ProvisioningError({
|
|
59
|
-
message:
|
|
60
|
-
`Failed to parse provisioning profile: missing ${uuid ? "" : "UUID "}${name ? "" : "Name "}${teamId ? "" : "TeamIdentifier "}`.trim(),
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return { uuid, name, teamId };
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
// ── scoped installation ───────────────────────────────────────────
|
|
68
|
-
|
|
69
|
-
export interface InstallProvisioningProfileOptions {
|
|
70
|
-
readonly profilePath: string;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export interface InstalledProvisioning {
|
|
74
|
-
readonly uuid: string;
|
|
75
|
-
readonly name: string;
|
|
76
|
-
readonly teamId: string;
|
|
77
|
-
readonly installedPath: string;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
interface AcquiredProvisioning extends InstalledProvisioning {
|
|
81
|
-
/**
|
|
82
|
-
* True if we installed the profile (so release should delete it).
|
|
83
|
-
* False if the profile was already present (e.g., installed by Xcode) —
|
|
84
|
-
* in that case we leave it alone on release to avoid breaking the user's
|
|
85
|
-
* other signing operations.
|
|
86
|
-
*/
|
|
87
|
-
readonly ownsInstallation: boolean;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const userProvisioningProfilesDir = (): string =>
|
|
91
|
-
path.join(os.homedir(), "Library", "MobileDevice", "Provisioning Profiles");
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Scoped installation of a provisioning profile: parses its metadata via
|
|
95
|
-
* `security cms -D -i`, copies it into `~/Library/MobileDevice/Provisioning Profiles`
|
|
96
|
-
* under `<uuid>.mobileprovision`, and removes the copy on scope close — but
|
|
97
|
-
* only if we installed it. If the target file already existed when we arrived
|
|
98
|
-
* (e.g., Xcode had it), we leave both the file and the contents untouched.
|
|
99
|
-
*/
|
|
100
|
-
export const installProvisioningProfile = ({
|
|
101
|
-
profilePath,
|
|
102
|
-
}: InstallProvisioningProfileOptions): Effect.Effect<
|
|
103
|
-
InstalledProvisioning,
|
|
104
|
-
ProvisioningError,
|
|
105
|
-
CommandExecutor.CommandExecutor | FileSystem.FileSystem | Scope.Scope
|
|
106
|
-
> =>
|
|
107
|
-
Effect.acquireRelease(
|
|
108
|
-
Effect.gen(function* () {
|
|
109
|
-
const fs = yield* FileSystem.FileSystem;
|
|
110
|
-
|
|
111
|
-
const plistXml = yield* Command.string(
|
|
112
|
-
Command.make("security", "cms", "-D", "-i", profilePath),
|
|
113
|
-
).pipe(
|
|
114
|
-
Effect.mapError(
|
|
115
|
-
(cause) =>
|
|
116
|
-
new ProvisioningError({
|
|
117
|
-
message: `security cms -D failed for ${profilePath}: ${String(cause)}`,
|
|
118
|
-
}),
|
|
119
|
-
),
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
const info = yield* extractProvisioningInfo(plistXml);
|
|
123
|
-
const targetDir = userProvisioningProfilesDir();
|
|
124
|
-
const installedPath = path.join(targetDir, `${info.uuid}.mobileprovision`);
|
|
125
|
-
|
|
126
|
-
yield* fs.makeDirectory(targetDir, { recursive: true }).pipe(
|
|
127
|
-
Effect.catchAll(
|
|
128
|
-
(cause) =>
|
|
129
|
-
new ProvisioningError({
|
|
130
|
-
message: `Failed to create provisioning profiles dir: ${String(cause)}`,
|
|
131
|
-
}),
|
|
132
|
-
),
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
const alreadyInstalled = yield* fs
|
|
136
|
-
.exists(installedPath)
|
|
137
|
-
.pipe(Effect.orElseSucceed(() => false));
|
|
138
|
-
|
|
139
|
-
if (alreadyInstalled) {
|
|
140
|
-
return {
|
|
141
|
-
...info,
|
|
142
|
-
installedPath,
|
|
143
|
-
ownsInstallation: false,
|
|
144
|
-
} satisfies AcquiredProvisioning;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
yield* fs.copyFile(profilePath, installedPath).pipe(
|
|
148
|
-
Effect.catchAll(
|
|
149
|
-
(cause) =>
|
|
150
|
-
new ProvisioningError({
|
|
151
|
-
message: `Failed to copy provisioning profile into ${installedPath}: ${String(cause)}`,
|
|
152
|
-
}),
|
|
153
|
-
),
|
|
154
|
-
);
|
|
155
|
-
|
|
156
|
-
return {
|
|
157
|
-
...info,
|
|
158
|
-
installedPath,
|
|
159
|
-
ownsInstallation: true,
|
|
160
|
-
} satisfies AcquiredProvisioning;
|
|
161
|
-
}),
|
|
162
|
-
(acquired) =>
|
|
163
|
-
Effect.gen(function* () {
|
|
164
|
-
if (!acquired.ownsInstallation) {
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
const fs = yield* FileSystem.FileSystem;
|
|
168
|
-
yield* fs.remove(acquired.installedPath).pipe(Effect.catchAll(() => Effect.void));
|
|
169
|
-
}),
|
|
170
|
-
).pipe(
|
|
171
|
-
Effect.map<AcquiredProvisioning, InstalledProvisioning>(
|
|
172
|
-
({ uuid, name, teamId, installedPath }) => ({
|
|
173
|
-
uuid,
|
|
174
|
-
name,
|
|
175
|
-
teamId,
|
|
176
|
-
installedPath,
|
|
177
|
-
}),
|
|
178
|
-
),
|
|
179
|
-
);
|
package/src/lib/output.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { Console, Effect } from "effect";
|
|
2
|
-
|
|
3
|
-
export const printTable = (
|
|
4
|
-
headers: readonly string[],
|
|
5
|
-
rows: readonly (readonly string[])[],
|
|
6
|
-
): Effect.Effect<void> =>
|
|
7
|
-
Effect.gen(function* () {
|
|
8
|
-
const allRows = [headers, ...rows];
|
|
9
|
-
const colWidths = headers.map((_, colIndex) =>
|
|
10
|
-
// eslint-disable-next-line eslint-js/no-restricted-syntax -- table padding for ragged rows; missing cell treated as empty-width
|
|
11
|
-
Math.max(...allRows.map((row) => (row[colIndex] ?? "").length)),
|
|
12
|
-
);
|
|
13
|
-
|
|
14
|
-
const formatRow = (row: readonly string[]): string =>
|
|
15
|
-
row.map((cell, idx) => cell.padEnd(colWidths[idx] ?? 0)).join(" ");
|
|
16
|
-
|
|
17
|
-
yield* Console.log(formatRow(headers));
|
|
18
|
-
yield* Console.log(colWidths.map((width) => "-".repeat(width)).join(" "));
|
|
19
|
-
|
|
20
|
-
for (const row of rows) {
|
|
21
|
-
yield* Console.log(formatRow(row));
|
|
22
|
-
}
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
export const printKeyValue = (pairs: readonly (readonly [string, string])[]): Effect.Effect<void> =>
|
|
26
|
-
Effect.gen(function* () {
|
|
27
|
-
const maxKeyLen = Math.max(...pairs.map(([key]) => key.length));
|
|
28
|
-
|
|
29
|
-
for (const [key, value] of pairs) {
|
|
30
|
-
yield* Console.log(`${key.padEnd(maxKeyLen)} ${value}`);
|
|
31
|
-
}
|
|
32
|
-
});
|