@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.
Files changed (152) hide show
  1. package/dist/index.js +5319 -0
  2. package/dist/index.js.map +1 -0
  3. package/package.json +12 -9
  4. package/CHANGELOG.md +0 -58
  5. package/oxlint.config.ts +0 -6
  6. package/src/app-layer.ts +0 -29
  7. package/src/application/build-workflow.ts +0 -222
  8. package/src/application/command-exit.ts +0 -13
  9. package/src/application/login.ts +0 -87
  10. package/src/application/update-promote.ts +0 -88
  11. package/src/application/update-publish.ts +0 -402
  12. package/src/application/update-rollback.ts +0 -275
  13. package/src/commands/analytics/adoption.ts +0 -40
  14. package/src/commands/analytics/channels.ts +0 -35
  15. package/src/commands/analytics/helpers.ts +0 -3
  16. package/src/commands/analytics/index.ts +0 -13
  17. package/src/commands/analytics/platforms.ts +0 -39
  18. package/src/commands/analytics/updates.ts +0 -35
  19. package/src/commands/audit-logs/helpers.ts +0 -3
  20. package/src/commands/audit-logs/index.ts +0 -8
  21. package/src/commands/audit-logs/list.ts +0 -66
  22. package/src/commands/branches.ts +0 -70
  23. package/src/commands/build/android.ts +0 -129
  24. package/src/commands/build/index.ts +0 -63
  25. package/src/commands/build/ios.ts +0 -199
  26. package/src/commands/build/reserve-and-upload.test.ts +0 -263
  27. package/src/commands/build/reserve-and-upload.ts +0 -160
  28. package/src/commands/build/run-step.ts +0 -131
  29. package/src/commands/builds/compatibility-matrix.ts +0 -48
  30. package/src/commands/builds/delete.ts +0 -15
  31. package/src/commands/builds/get.ts +0 -34
  32. package/src/commands/builds/helpers.ts +0 -3
  33. package/src/commands/builds/index.ts +0 -20
  34. package/src/commands/builds/install-link.ts +0 -20
  35. package/src/commands/builds/list.ts +0 -38
  36. package/src/commands/channels/create.ts +0 -37
  37. package/src/commands/channels/delete.ts +0 -15
  38. package/src/commands/channels/helpers.ts +0 -18
  39. package/src/commands/channels/index.ts +0 -24
  40. package/src/commands/channels/list.ts +0 -38
  41. package/src/commands/channels/pause.ts +0 -15
  42. package/src/commands/channels/resume.ts +0 -15
  43. package/src/commands/channels/rollout/complete.ts +0 -17
  44. package/src/commands/channels/rollout/create.ts +0 -36
  45. package/src/commands/channels/rollout/index.ts +0 -11
  46. package/src/commands/channels/rollout/revert.ts +0 -17
  47. package/src/commands/channels/rollout/update.ts +0 -23
  48. package/src/commands/channels/update.ts +0 -32
  49. package/src/commands/credentials/delete.ts +0 -24
  50. package/src/commands/credentials/index.ts +0 -10
  51. package/src/commands/credentials/list.ts +0 -33
  52. package/src/commands/credentials/upload.ts +0 -91
  53. package/src/commands/env/delete.ts +0 -35
  54. package/src/commands/env/export.ts +0 -27
  55. package/src/commands/env/get.ts +0 -25
  56. package/src/commands/env/helpers.ts +0 -13
  57. package/src/commands/env/import.ts +0 -31
  58. package/src/commands/env/index.ts +0 -24
  59. package/src/commands/env/list.ts +0 -44
  60. package/src/commands/env/pull.ts +0 -27
  61. package/src/commands/env/set.ts +0 -42
  62. package/src/commands/fingerprint/compare.ts +0 -25
  63. package/src/commands/fingerprint/generate.ts +0 -18
  64. package/src/commands/fingerprint/index.ts +0 -9
  65. package/src/commands/init.ts +0 -35
  66. package/src/commands/login.ts +0 -13
  67. package/src/commands/logout.ts +0 -12
  68. package/src/commands/projects.ts +0 -84
  69. package/src/commands/status.ts +0 -48
  70. package/src/commands/update/delete.ts +0 -15
  71. package/src/commands/update/helpers.ts +0 -22
  72. package/src/commands/update/index.ts +0 -22
  73. package/src/commands/update/list.ts +0 -60
  74. package/src/commands/update/promote.ts +0 -30
  75. package/src/commands/update/publish.ts +0 -94
  76. package/src/commands/update/rollback.ts +0 -42
  77. package/src/commands/update/rollout/complete.ts +0 -17
  78. package/src/commands/update/rollout/index.ts +0 -10
  79. package/src/commands/update/rollout/revert.ts +0 -17
  80. package/src/commands/update/rollout/set.ts +0 -23
  81. package/src/index.ts +0 -53
  82. package/src/lib/android-keystore.test.ts +0 -114
  83. package/src/lib/android-keystore.ts +0 -76
  84. package/src/lib/android-signing-gradle.test.ts +0 -95
  85. package/src/lib/android-signing-gradle.ts +0 -52
  86. package/src/lib/app-json.ts +0 -81
  87. package/src/lib/apple-auth.test.ts +0 -402
  88. package/src/lib/apple-auth.ts +0 -132
  89. package/src/lib/artifact-finder.test.ts +0 -195
  90. package/src/lib/artifact-finder.ts +0 -122
  91. package/src/lib/browser-login.test.ts +0 -88
  92. package/src/lib/browser-login.ts +0 -193
  93. package/src/lib/build-profile.test.ts +0 -290
  94. package/src/lib/build-profile.ts +0 -234
  95. package/src/lib/cli-schemas.ts +0 -39
  96. package/src/lib/command-errors.ts +0 -60
  97. package/src/lib/credentials-downloader.ts +0 -181
  98. package/src/lib/credentials-manager.ts +0 -354
  99. package/src/lib/env-exporter.test.ts +0 -96
  100. package/src/lib/env-exporter.ts +0 -28
  101. package/src/lib/exit-codes.ts +0 -82
  102. package/src/lib/expo-config.ts +0 -130
  103. package/src/lib/expo-export.test.ts +0 -94
  104. package/src/lib/expo-export.ts +0 -281
  105. package/src/lib/fingerprint.ts +0 -67
  106. package/src/lib/format-error.ts +0 -22
  107. package/src/lib/git-context.ts +0 -56
  108. package/src/lib/gradle-config.ts +0 -126
  109. package/src/lib/ios-export-options.test.ts +0 -98
  110. package/src/lib/ios-export-options.ts +0 -62
  111. package/src/lib/ios-keychain.ts +0 -181
  112. package/src/lib/ios-provisioning.test.ts +0 -115
  113. package/src/lib/ios-provisioning.ts +0 -179
  114. package/src/lib/output.ts +0 -32
  115. package/src/lib/pkcs12.ts +0 -73
  116. package/src/lib/plist.ts +0 -39
  117. package/src/lib/post-build-validation.ts +0 -146
  118. package/src/lib/presigned-upload.test.ts +0 -140
  119. package/src/lib/presigned-upload.ts +0 -35
  120. package/src/lib/record.ts +0 -5
  121. package/src/lib/resolve-named-resource.ts +0 -24
  122. package/src/lib/runtime-version.test.ts +0 -119
  123. package/src/lib/runtime-version.ts +0 -62
  124. package/src/lib/sha256.test.ts +0 -108
  125. package/src/lib/sha256.ts +0 -80
  126. package/src/lib/signed-payloads.test.ts +0 -181
  127. package/src/lib/signed-payloads.ts +0 -164
  128. package/src/lib/string-utils.ts +0 -4
  129. package/src/lib/temp-dir.ts +0 -14
  130. package/src/lib/test-utils.ts +0 -13
  131. package/src/lib/update-platforms.test.ts +0 -45
  132. package/src/lib/update-platforms.ts +0 -19
  133. package/src/lib/xcpretty-formatter.ts +0 -21
  134. package/src/services/api-client.ts +0 -42
  135. package/src/services/apple-session-store.ts +0 -100
  136. package/src/services/auth-store.ts +0 -85
  137. package/src/services/cli-runtime.ts +0 -46
  138. package/src/services/config-store.ts +0 -108
  139. package/src/services/presigned-upload.ts +0 -84
  140. package/src/services/update-asset-uploader.ts +0 -72
  141. package/src/types/keychain.d.ts +0 -22
  142. package/tests/e2e/build.test.ts +0 -270
  143. package/tests/e2e/commands.test.ts +0 -694
  144. package/tests/e2e/ota-lifecycle.test.ts +0 -275
  145. package/tests/e2e/publish.test.ts +0 -150
  146. package/tests/helpers/cli-e2e.ts +0 -426
  147. package/tests/helpers/pty-driver.ts +0 -142
  148. package/tests/interactive/harness/provider-prompt.ts +0 -54
  149. package/tests/interactive/login.test.ts +0 -47
  150. package/tests/interactive/provider-select.test.ts +0 -59
  151. package/tsconfig.json +0 -7
  152. 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&amp;b.app");
92
- // Profile name escaped
93
- expect(plist).toContain("&lt;test &amp; &quot;name&quot;&gt;");
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("&", "&amp;")
14
- .replaceAll("<", "&lt;")
15
- .replaceAll(">", "&gt;")
16
- .replaceAll('"', "&quot;")
17
- .replaceAll("'", "&apos;");
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
- };
@@ -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
- });