voltra 2.0.0 → 2.1.1

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 (84) hide show
  1. package/build/cjs/config/normalize.js +93 -0
  2. package/build/cjs/config/normalize.js.map +1 -1
  3. package/build/cjs/index.js +5 -2
  4. package/build/cjs/index.js.map +1 -1
  5. package/build/cjs/platforms/android/apply.js +11 -2
  6. package/build/cjs/platforms/android/apply.js.map +1 -1
  7. package/build/cjs/platforms/android/generated.js +152 -130
  8. package/build/cjs/platforms/android/generated.js.map +1 -1
  9. package/build/cjs/platforms/android/gradle.js +120 -0
  10. package/build/cjs/platforms/android/gradle.js.map +1 -0
  11. package/build/cjs/platforms/ios/generated.js +246 -142
  12. package/build/cjs/platforms/ios/generated.js.map +1 -1
  13. package/build/cjs/platforms/ios/xcodeTarget.js +68 -0
  14. package/build/cjs/platforms/ios/xcodeTarget.js.map +1 -1
  15. package/build/cjs/platforms/shared/widgetModule.js +151 -0
  16. package/build/cjs/platforms/shared/widgetModule.js.map +1 -0
  17. package/build/esm/config/normalize.js +93 -0
  18. package/build/esm/config/normalize.js.map +1 -1
  19. package/build/esm/index.js +1 -0
  20. package/build/esm/index.js.map +1 -1
  21. package/build/esm/platforms/android/apply.js +11 -2
  22. package/build/esm/platforms/android/apply.js.map +1 -1
  23. package/build/esm/platforms/android/generated.js +152 -97
  24. package/build/esm/platforms/android/generated.js.map +1 -1
  25. package/build/esm/platforms/android/gradle.js +112 -0
  26. package/build/esm/platforms/android/gradle.js.map +1 -0
  27. package/build/esm/platforms/ios/generated.js +246 -109
  28. package/build/esm/platforms/ios/generated.js.map +1 -1
  29. package/build/esm/platforms/ios/xcodeTarget.js +69 -1
  30. package/build/esm/platforms/ios/xcodeTarget.js.map +1 -1
  31. package/build/esm/platforms/shared/widgetModule.js +111 -0
  32. package/build/esm/platforms/shared/widgetModule.js.map +1 -0
  33. package/build/types/config/normalize.d.ts.map +1 -1
  34. package/build/types/config/types.d.ts +32 -0
  35. package/build/types/config/types.d.ts.map +1 -1
  36. package/build/types/index.d.ts +3 -1
  37. package/build/types/index.d.ts.map +1 -1
  38. package/build/types/platforms/android/apply.d.ts.map +1 -1
  39. package/build/types/platforms/android/generated.d.ts.map +1 -1
  40. package/build/types/platforms/android/gradle.d.ts +18 -0
  41. package/build/types/platforms/android/gradle.d.ts.map +1 -0
  42. package/build/types/platforms/ios/generated.d.ts.map +1 -1
  43. package/build/types/platforms/ios/xcodeTarget.d.ts.map +1 -1
  44. package/build/types/platforms/shared/widgetModule.d.ts +9 -0
  45. package/build/types/platforms/shared/widgetModule.d.ts.map +1 -0
  46. package/package.json +2 -1
  47. package/src/apply/index.ts +309 -0
  48. package/src/apply/preflight.ts +172 -0
  49. package/src/bin.ts +14 -0
  50. package/src/commands/apply.ts +114 -0
  51. package/src/config/defaults.ts +34 -0
  52. package/src/config/load.ts +100 -0
  53. package/src/config/normalize.ts +562 -0
  54. package/src/config/types.ts +317 -0
  55. package/src/cosmiconfig.d.ts +21 -0
  56. package/src/dependencies/platformPackages.ts +75 -0
  57. package/src/discovery/android.ts +233 -0
  58. package/src/discovery/ios.ts +579 -0
  59. package/src/fs/path.ts +25 -0
  60. package/src/fs/readWrite.ts +102 -0
  61. package/src/git/status.ts +205 -0
  62. package/src/index.ts +204 -0
  63. package/src/platforms/android/apply.ts +106 -0
  64. package/src/platforms/android/generated.ts +1284 -0
  65. package/src/platforms/android/gradle.ts +155 -0
  66. package/src/platforms/android/manifest.ts +379 -0
  67. package/src/platforms/ios/apply.ts +139 -0
  68. package/src/platforms/ios/deploymentTarget.ts +47 -0
  69. package/src/platforms/ios/entitlements.ts +146 -0
  70. package/src/platforms/ios/generated.ts +1233 -0
  71. package/src/platforms/ios/mainAppEntitlements.ts +11 -0
  72. package/src/platforms/ios/plist.ts +321 -0
  73. package/src/platforms/ios/podfile.ts +255 -0
  74. package/src/platforms/ios/targetName.ts +11 -0
  75. package/src/platforms/ios/xcode.ts +181 -0
  76. package/src/platforms/ios/xcodeTarget.ts +899 -0
  77. package/src/platforms/shared/widgetModule.ts +153 -0
  78. package/src/reporting/clack.ts +67 -0
  79. package/src/reporting/summary.ts +116 -0
  80. package/src/state/diff.ts +21 -0
  81. package/src/state/files.ts +52 -0
  82. package/src/state/load.ts +66 -0
  83. package/src/state/save.ts +31 -0
  84. package/src/xml2js.d.ts +23 -0
@@ -0,0 +1,309 @@
1
+ import path from 'node:path'
2
+
3
+ import { normalizeRelativePath } from '../fs/path'
4
+ import { loadVoltraConfig } from '../config/load'
5
+ import { normalizeVoltraConfig } from '../config/normalize'
6
+ import { removePathIfExists } from '../fs/readWrite'
7
+ import { ensureGitWorktreeIsReady } from '../git/status'
8
+ import { applyAndroidPlatform, createAndroidPreflightRunner } from '../platforms/android/apply'
9
+ import { applyIOSPlatform, createIOSPreflightRunner } from '../platforms/ios/apply'
10
+ import { renderApplySummary, renderIntro } from '../reporting/clack'
11
+ import { VoltraCliError } from '../reporting/summary'
12
+ import { diffVoltraState } from '../state/diff'
13
+ import { loadVoltraState } from '../state/load'
14
+ import { saveVoltraState } from '../state/save'
15
+
16
+ import { runApplyPreflight } from './preflight'
17
+
18
+ import type { NormalizedVoltraConfig, VoltraPlatform } from '../config/types'
19
+ import type { ApplyPreflightResult, ApplyPreflightRunners } from './preflight'
20
+ import type { ReportedChange } from '../reporting/summary'
21
+ import type { VoltraState } from '../state/load'
22
+
23
+ export interface ApplyOptions {
24
+ configPath?: string
25
+ platform?: VoltraPlatform
26
+ allowDirty?: boolean
27
+ }
28
+
29
+ export interface ApplyResult {
30
+ exitCode: number
31
+ errorMessage?: string
32
+ }
33
+
34
+ export interface PlatformApplyContext {
35
+ config: NormalizedVoltraConfig
36
+ platform: VoltraPlatform
37
+ preflight: unknown
38
+ previousState?: VoltraState
39
+ requestedPlatforms: VoltraPlatform[]
40
+ }
41
+
42
+ export interface PlatformApplyResult {
43
+ platform: VoltraPlatform
44
+ changes: ReportedChange[]
45
+ generatedFiles: string[]
46
+ warnings?: string[]
47
+ }
48
+
49
+ export type PlatformApplyRunner = (context: PlatformApplyContext) => Promise<PlatformApplyResult>
50
+
51
+ export interface ApplyDependencies {
52
+ applyRunners: Partial<Record<VoltraPlatform, PlatformApplyRunner>>
53
+ preflightRunners: ApplyPreflightRunners
54
+ writeIntro(): Promise<void>
55
+ writeSummary(summary: { changes: ReportedChange[]; warnings?: string[] }): Promise<void>
56
+ }
57
+
58
+ const DEFAULT_DEPENDENCIES: ApplyDependencies = {
59
+ applyRunners: {},
60
+ preflightRunners: {},
61
+ async writeIntro() {
62
+ await renderIntro()
63
+ },
64
+ async writeSummary(summary) {
65
+ await renderApplySummary(summary)
66
+ },
67
+ }
68
+
69
+ export async function applyVoltra(options: ApplyOptions): Promise<ApplyResult> {
70
+ try {
71
+ await runApplyPipeline(options, DEFAULT_DEPENDENCIES)
72
+
73
+ return {
74
+ exitCode: 0,
75
+ }
76
+ } catch (error: unknown) {
77
+ return {
78
+ exitCode: 1,
79
+ errorMessage: error instanceof Error ? error.message : String(error),
80
+ }
81
+ }
82
+ }
83
+
84
+ export async function runApplyPipeline(options: ApplyOptions, dependencies: ApplyDependencies): Promise<void> {
85
+ const loadedConfig = await loadVoltraConfig({ configPath: options.configPath })
86
+ const normalizedConfig = normalizeVoltraConfig(loadedConfig)
87
+ const resolvedDependencies = resolveApplyDependencies(normalizedConfig, dependencies)
88
+ await resolvedDependencies.writeIntro()
89
+ await ensureGitWorktreeIsReady({
90
+ cwd: normalizedConfig.projectRoot,
91
+ allowDirty: options.allowDirty,
92
+ })
93
+ const preflight = await runApplyPreflight(normalizedConfig, resolvedDependencies.preflightRunners, options.platform)
94
+ const previousState = await loadVoltraState(normalizedConfig.projectRoot)
95
+ const platformResults = await runPlatformApply(
96
+ normalizedConfig,
97
+ preflight,
98
+ previousState,
99
+ resolvedDependencies.applyRunners
100
+ )
101
+ const nextGeneratedFiles = mergeGeneratedFiles(
102
+ normalizedConfig,
103
+ previousState,
104
+ preflight.requestedPlatforms,
105
+ platformResults
106
+ )
107
+ const stateDiff = diffVoltraState(previousState, nextGeneratedFiles)
108
+ const deletedChanges = await removeStaleGeneratedFiles(normalizedConfig.projectRoot, stateDiff.staleFiles)
109
+ await saveVoltraState(normalizedConfig.projectRoot, { files: stateDiff.nextFiles })
110
+
111
+ const summaryWarnings = platformResults.flatMap((result) => result.warnings ?? []).filter(isDefined)
112
+ const summaryChanges = [...platformResults.flatMap((result) => result.changes), ...deletedChanges]
113
+
114
+ await resolvedDependencies.writeSummary({ changes: summaryChanges, warnings: summaryWarnings })
115
+ }
116
+
117
+ function resolveApplyDependencies(config: NormalizedVoltraConfig, dependencies: ApplyDependencies): ApplyDependencies {
118
+ return {
119
+ applyRunners: {
120
+ android: dependencies.applyRunners.android ?? applyAndroidPlatform,
121
+ ios: dependencies.applyRunners.ios ?? applyIOSPlatform,
122
+ },
123
+ preflightRunners: {
124
+ android:
125
+ dependencies.preflightRunners.android ?? (config.android ? createAndroidPreflightRunner(config) : undefined),
126
+ ios: dependencies.preflightRunners.ios ?? (config.ios ? createIOSPreflightRunner(config) : undefined),
127
+ },
128
+ writeIntro: dependencies.writeIntro,
129
+ writeSummary: dependencies.writeSummary,
130
+ }
131
+ }
132
+
133
+ function mergeGeneratedFiles(
134
+ config: NormalizedVoltraConfig,
135
+ previousState: VoltraState | undefined,
136
+ requestedPlatforms: VoltraPlatform[],
137
+ platformResults: PlatformApplyResult[]
138
+ ): string[] {
139
+ const nextGeneratedFilesByPlatform = new Map(
140
+ platformResults.map((result) => [result.platform, result.generatedFiles] as const)
141
+ )
142
+ const mergedFiles = new Set<string>()
143
+ const platformRoots = getTrackedPlatformRoots(config)
144
+ const configuredPlatforms = getConfiguredPlatforms(config)
145
+ const isPartialApply = requestedPlatforms.length < configuredPlatforms.length
146
+
147
+ for (const previousFile of previousState?.files ?? []) {
148
+ const owningPlatform = getTrackedFilePlatform(previousFile, platformRoots)
149
+
150
+ if (!owningPlatform) {
151
+ if (isPartialApply) {
152
+ mergedFiles.add(previousFile)
153
+ }
154
+
155
+ continue
156
+ }
157
+
158
+ if (requestedPlatforms.includes(owningPlatform)) {
159
+ continue
160
+ }
161
+
162
+ mergedFiles.add(previousFile)
163
+ }
164
+
165
+ for (const platform of requestedPlatforms) {
166
+ for (const filePath of nextGeneratedFilesByPlatform.get(platform) ?? []) {
167
+ mergedFiles.add(filePath)
168
+ }
169
+ }
170
+
171
+ return [...mergedFiles]
172
+ }
173
+
174
+ function getTrackedPlatformRoots(config: NormalizedVoltraConfig): Partial<Record<VoltraPlatform, string>> {
175
+ const projectRoot = config.projectRoot
176
+ const androidRoot = config.android
177
+ ? normalizeRelativePath(
178
+ path.relative(projectRoot, config.android.project.rootDir ?? path.join(projectRoot, 'android'))
179
+ )
180
+ : undefined
181
+ const iosRoot = config.ios
182
+ ? normalizeRelativePath(path.relative(projectRoot, config.ios.project.rootDir ?? path.join(projectRoot, 'ios')))
183
+ : undefined
184
+
185
+ return {
186
+ ...(androidRoot ? { android: androidRoot } : {}),
187
+ ...(iosRoot ? { ios: iosRoot } : {}),
188
+ }
189
+ }
190
+
191
+ function getConfiguredPlatforms(config: NormalizedVoltraConfig): VoltraPlatform[] {
192
+ const platforms: VoltraPlatform[] = []
193
+
194
+ if (config.android) {
195
+ platforms.push('android')
196
+ }
197
+
198
+ if (config.ios) {
199
+ platforms.push('ios')
200
+ }
201
+
202
+ return platforms
203
+ }
204
+
205
+ function getTrackedFilePlatform(
206
+ filePath: string,
207
+ platformRoots: Partial<Record<VoltraPlatform, string>>
208
+ ): VoltraPlatform | undefined {
209
+ const normalizedFilePath = normalizeRelativePath(filePath)
210
+
211
+ for (const platform of ['android', 'ios'] as const) {
212
+ const root = platformRoots[platform]
213
+
214
+ if (!root) {
215
+ continue
216
+ }
217
+
218
+ if (normalizedFilePath === root || normalizedFilePath.startsWith(`${root}/`)) {
219
+ return platform
220
+ }
221
+ }
222
+
223
+ return undefined
224
+ }
225
+
226
+ async function runPlatformApply(
227
+ config: NormalizedVoltraConfig,
228
+ preflight: ApplyPreflightResult,
229
+ previousState: VoltraState | undefined,
230
+ applyRunners: Partial<Record<VoltraPlatform, PlatformApplyRunner>>
231
+ ): Promise<PlatformApplyResult[]> {
232
+ const missingRunners = preflight.requestedPlatforms.filter((platform) => !applyRunners[platform])
233
+
234
+ if (missingRunners.length > 0) {
235
+ throw new VoltraCliError(`No apply runner is registered for ${missingRunners.join(', ')}.`)
236
+ }
237
+
238
+ const results: PlatformApplyResult[] = []
239
+
240
+ for (const platform of preflight.requestedPlatforms) {
241
+ const applyRunner = applyRunners[platform]
242
+
243
+ if (!applyRunner) {
244
+ throw new VoltraCliError(`No apply runner is registered for ${platform}.`)
245
+ }
246
+
247
+ let result: PlatformApplyResult
248
+
249
+ try {
250
+ result = await applyRunner({
251
+ config,
252
+ platform,
253
+ preflight: preflight.platformResults[platform],
254
+ previousState,
255
+ requestedPlatforms: preflight.requestedPlatforms,
256
+ })
257
+ } catch (error: unknown) {
258
+ throw new VoltraCliError(`Apply failed for ${platform}: ${getApplyRunnerErrorMessage(error)}`)
259
+ }
260
+
261
+ if (result.platform !== platform) {
262
+ throw new VoltraCliError(
263
+ `Apply runner returned a mismatched platform result: expected ${platform}, received ${result.platform}.`
264
+ )
265
+ }
266
+
267
+ results.push(result)
268
+ }
269
+
270
+ return results
271
+ }
272
+
273
+ async function removeStaleGeneratedFiles(projectRoot: string, staleFiles: string[]): Promise<ReportedChange[]> {
274
+ const deletedChanges: ReportedChange[] = []
275
+
276
+ for (const staleFile of staleFiles) {
277
+ const staleFilePath = path.join(projectRoot, staleFile)
278
+ let deleted: boolean
279
+
280
+ try {
281
+ deleted = await removePathIfExists(staleFilePath)
282
+ } catch (error: unknown) {
283
+ throw new VoltraCliError(
284
+ `Failed to remove stale generated file ${staleFile}: ${getApplyRunnerErrorMessage(error)}`
285
+ )
286
+ }
287
+
288
+ if (deleted) {
289
+ deletedChanges.push({
290
+ kind: 'deleted',
291
+ path: staleFile,
292
+ })
293
+ }
294
+ }
295
+
296
+ return deletedChanges
297
+ }
298
+
299
+ function isDefined<TValue>(value: TValue | undefined): value is TValue {
300
+ return value !== undefined
301
+ }
302
+
303
+ function getApplyRunnerErrorMessage(error: unknown): string {
304
+ if (error instanceof Error && error.message) {
305
+ return error.message
306
+ }
307
+
308
+ return String(error)
309
+ }
@@ -0,0 +1,172 @@
1
+ import { PreflightError } from '../reporting/summary'
2
+
3
+ import type { NormalizedVoltraConfig, VoltraPlatform } from '../config/types'
4
+ import type { PreflightFailureReport, PreflightIssue } from '../reporting/summary'
5
+
6
+ export interface PlatformPreflightSuccess<TContext> {
7
+ platform: VoltraPlatform
8
+ context: TContext
9
+ }
10
+
11
+ export interface PlatformPreflightFailure {
12
+ platform: VoltraPlatform
13
+ issues: PreflightIssue[]
14
+ }
15
+
16
+ export type PlatformPreflightResult<TContext> = PlatformPreflightSuccess<TContext> | PlatformPreflightFailure
17
+
18
+ export interface ApplyPreflightContext {
19
+ requestedPlatforms: VoltraPlatform[]
20
+ }
21
+
22
+ export type PlatformPreflightRunner<TContext> = (
23
+ context: ApplyPreflightContext
24
+ ) => Promise<PlatformPreflightResult<TContext>>
25
+
26
+ export interface ApplyPreflightRunners {
27
+ android?: PlatformPreflightRunner<unknown>
28
+ ios?: PlatformPreflightRunner<unknown>
29
+ }
30
+
31
+ export interface ApplyPreflightResult {
32
+ requestedPlatforms: VoltraPlatform[]
33
+ platformResults: Partial<Record<VoltraPlatform, unknown>>
34
+ }
35
+
36
+ export function getRequestedPlatforms(config: NormalizedVoltraConfig, platform?: VoltraPlatform): VoltraPlatform[] {
37
+ if (platform) {
38
+ validateConfiguredPlatform(config, platform)
39
+ return [platform]
40
+ }
41
+
42
+ const configuredPlatforms = getConfiguredPlatforms(config)
43
+
44
+ if (configuredPlatforms.length === 0) {
45
+ throw new PreflightError({
46
+ summary: 'No platforms are configured for Voltra apply.',
47
+ issues: [{ message: 'Add an android or ios config block before running apply.' }],
48
+ })
49
+ }
50
+
51
+ return configuredPlatforms
52
+ }
53
+
54
+ export async function runApplyPreflight(
55
+ config: NormalizedVoltraConfig,
56
+ runners: ApplyPreflightRunners,
57
+ platform?: VoltraPlatform
58
+ ): Promise<ApplyPreflightResult> {
59
+ const requestedPlatforms = getRequestedPlatforms(config, platform)
60
+ const preflightContext: ApplyPreflightContext = { requestedPlatforms }
61
+ const failures: PlatformPreflightFailure[] = []
62
+ const platformResults: Partial<Record<VoltraPlatform, unknown>> = {}
63
+
64
+ await Promise.all(
65
+ requestedPlatforms.map(async (requestedPlatform) => {
66
+ const runner = runners[requestedPlatform]
67
+
68
+ if (!runner) {
69
+ failures.push({
70
+ platform: requestedPlatform,
71
+ issues: [{ message: `No preflight runner is registered for ${requestedPlatform}.` }],
72
+ })
73
+ return
74
+ }
75
+
76
+ let result: PlatformPreflightResult<unknown>
77
+
78
+ try {
79
+ result = await runner(preflightContext)
80
+ } catch (error: unknown) {
81
+ failures.push({
82
+ platform: requestedPlatform,
83
+ issues: [{ message: getPreflightRunnerErrorMessage(error) }],
84
+ })
85
+ return
86
+ }
87
+
88
+ if ('issues' in result) {
89
+ failures.push(result)
90
+ return
91
+ }
92
+
93
+ if (result.platform !== requestedPlatform) {
94
+ failures.push({
95
+ platform: requestedPlatform,
96
+ issues: [{ message: `Preflight runner returned a mismatched platform result: ${result.platform}.` }],
97
+ })
98
+ return
99
+ }
100
+
101
+ platformResults[requestedPlatform] = result.context
102
+ })
103
+ )
104
+
105
+ if (failures.length > 0) {
106
+ throw new PreflightError(buildPreflightFailureReport(failures))
107
+ }
108
+
109
+ return {
110
+ requestedPlatforms,
111
+ platformResults,
112
+ }
113
+ }
114
+
115
+ function getConfiguredPlatforms(config: NormalizedVoltraConfig): VoltraPlatform[] {
116
+ const platforms: VoltraPlatform[] = []
117
+
118
+ if (config.android) {
119
+ platforms.push('android')
120
+ }
121
+
122
+ if (config.ios) {
123
+ platforms.push('ios')
124
+ }
125
+
126
+ return platforms
127
+ }
128
+
129
+ function validateConfiguredPlatform(config: NormalizedVoltraConfig, platform: VoltraPlatform): void {
130
+ if ((platform === 'android' && config.android) || (platform === 'ios' && config.ios)) {
131
+ return
132
+ }
133
+
134
+ throw new PreflightError({
135
+ summary: `Requested platform '${platform}' is not configured.`,
136
+ issues: [{ message: `Add a ${platform} config block or remove --platform ${platform}.` }],
137
+ })
138
+ }
139
+
140
+ function buildPreflightFailureReport(failures: PlatformPreflightFailure[]): PreflightFailureReport {
141
+ const orderedFailures = [...failures].sort((left, right) => comparePlatforms(left.platform, right.platform))
142
+
143
+ return {
144
+ summary: 'Preflight failed before any files were written.',
145
+ issues: orderedFailures.flatMap((failure) =>
146
+ failure.issues.map((issue) => ({
147
+ ...issue,
148
+ message: `${failure.platform}: ${issue.message}`,
149
+ }))
150
+ ),
151
+ }
152
+ }
153
+
154
+ function getPreflightRunnerErrorMessage(error: unknown): string {
155
+ if (error instanceof Error && error.message) {
156
+ return error.message
157
+ }
158
+
159
+ return String(error)
160
+ }
161
+
162
+ function comparePlatforms(left: VoltraPlatform, right: VoltraPlatform): number {
163
+ return getPlatformSortOrder(left) - getPlatformSortOrder(right)
164
+ }
165
+
166
+ function getPlatformSortOrder(platform: VoltraPlatform): number {
167
+ if (platform === 'android') {
168
+ return 0
169
+ }
170
+
171
+ return 1
172
+ }
package/src/bin.ts ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runCli } from './index'
4
+
5
+ void runCli(process.argv.slice(2))
6
+ .then((exitCode) => {
7
+ process.exitCode = exitCode
8
+ })
9
+ .catch((error: unknown) => {
10
+ const message = error instanceof Error ? error.message : String(error)
11
+
12
+ process.stderr.write(`${message}\n`)
13
+ process.exitCode = 1
14
+ })
@@ -0,0 +1,114 @@
1
+ import { Command } from 'commander'
2
+
3
+ import { applyVoltra } from '../apply'
4
+ import { normalizeClackMessage, renderError } from '../reporting/clack'
5
+
6
+ import type { ApplyOptions } from '../apply'
7
+ import type { VoltraPlatform } from '../config/types'
8
+
9
+ export const CLI_EXIT_CODE_SUCCESS = 0
10
+ export const CLI_EXIT_CODE_FAILURE = 1
11
+
12
+ interface ApplyCommandCliOptions {
13
+ platform?: VoltraPlatform
14
+ config?: string
15
+ yes?: boolean
16
+ }
17
+
18
+ export async function runApplyCommand(argv: string[]): Promise<number> {
19
+ let exitCode = CLI_EXIT_CODE_SUCCESS
20
+ const command = createApplyCommand((nextExitCode) => {
21
+ exitCode = nextExitCode
22
+ })
23
+
24
+ try {
25
+ await command.parseAsync(argv, { from: 'user' })
26
+ return exitCode
27
+ } catch (error) {
28
+ return await handleCommanderError(error)
29
+ }
30
+ }
31
+
32
+ export function createApplyCommand(onComplete?: (exitCode: number) => void): Command {
33
+ const command = new Command('apply')
34
+
35
+ command
36
+ .description('Apply Voltra changes to the native project')
37
+ .usage('[options]')
38
+ .exitOverride()
39
+ .showHelpAfterError()
40
+ .showSuggestionAfterError()
41
+ .option('-p, --platform <platform>', 'limit apply to a single platform', parsePlatform)
42
+ .option('-c, --config <path>', 'load config from an explicit file path')
43
+ .option('-y, --yes', 'skip the dirty git worktree confirmation prompt')
44
+ .action(async () => {
45
+ const cliOptions = command.opts<ApplyCommandCliOptions>()
46
+ const options = toApplyOptions(cliOptions)
47
+ const result = await applyVoltra(options)
48
+
49
+ onComplete?.(result.exitCode)
50
+
51
+ if (result.exitCode !== CLI_EXIT_CODE_SUCCESS && result.errorMessage) {
52
+ await renderError(result.errorMessage, { output: process.stderr })
53
+ }
54
+ })
55
+
56
+ return command
57
+ }
58
+
59
+ function toApplyOptions(cliOptions: ApplyCommandCliOptions): ApplyOptions {
60
+ if (cliOptions.config !== undefined && !cliOptions.config.trim()) {
61
+ throw new Error('--config must be a non-empty path')
62
+ }
63
+
64
+ return {
65
+ platform: cliOptions.platform,
66
+ configPath: cliOptions.config,
67
+ allowDirty: cliOptions.yes,
68
+ }
69
+ }
70
+
71
+ function parsePlatform(value: string): VoltraPlatform {
72
+ if (value === 'android' || value === 'ios') {
73
+ return value
74
+ }
75
+
76
+ throw new Error(`Invalid platform '${value}'. Expected 'ios' or 'android'.`)
77
+ }
78
+
79
+ async function handleCommanderError(error: unknown): Promise<number> {
80
+ if (isCommanderError(error)) {
81
+ return isCommanderDisplayError(error) ? CLI_EXIT_CODE_SUCCESS : CLI_EXIT_CODE_FAILURE
82
+ }
83
+
84
+ await renderError(formatCommandError(error), { output: process.stderr })
85
+ return CLI_EXIT_CODE_FAILURE
86
+ }
87
+
88
+ function isCommanderDisplayError(error: unknown): error is { code: string } {
89
+ return Boolean(
90
+ error &&
91
+ typeof error === 'object' &&
92
+ 'code' in error &&
93
+ (error.code === 'commander.helpDisplayed' || error.code === 'commander.version')
94
+ )
95
+ }
96
+
97
+ function isCommanderError(error: unknown): error is { code: string } {
98
+ return Boolean(
99
+ error &&
100
+ typeof error === 'object' &&
101
+ 'code' in error &&
102
+ typeof error.code === 'string' &&
103
+ error.code.startsWith('commander.')
104
+ )
105
+ }
106
+
107
+ export function formatCommandError(error: unknown): string {
108
+ const message = error instanceof Error ? error.message : String(error)
109
+ return normalizeClackMessage(message)
110
+ }
111
+
112
+ export function getApplyHelpText(): string {
113
+ return createApplyCommand().helpInformation()
114
+ }
@@ -0,0 +1,34 @@
1
+ import type { IOSWidgetFamily } from './types'
2
+
3
+ export const DEFAULT_ANDROID_ENABLE_NOTIFICATIONS = false
4
+ export const DEFAULT_ANDROID_SERVER_UPDATE_INTERVAL_MINUTES = 60
5
+ export const DEFAULT_ANDROID_SERVER_UPDATE_REFRESH = false
6
+ export const DEFAULT_ANDROID_USER_IMAGES_PATH = './assets/voltra-android'
7
+
8
+ export const DEFAULT_IOS_ENABLE_PUSH_NOTIFICATIONS = false
9
+ export const DEFAULT_IOS_DEPLOYMENT_TARGET = '17.0'
10
+ export const DEFAULT_IOS_SERVER_UPDATE_INTERVAL_MINUTES = 15
11
+ export const DEFAULT_IOS_SERVER_UPDATE_REFRESH = false
12
+ export const DEFAULT_IOS_USER_IMAGES_PATH = './assets/voltra'
13
+ export const DEFAULT_IOS_WIDGET_FAMILIES: IOSWidgetFamily[] = ['systemSmall', 'systemMedium', 'systemLarge']
14
+
15
+ /**
16
+ * These defaults intentionally mirror the existing Expo plugin behavior where that behavior is already explicit.
17
+ * Some values, such as the default iOS widget target name, still depend on native project discovery and app naming.
18
+ */
19
+ export const CLI_DEFAULTS = {
20
+ android: {
21
+ enableNotifications: DEFAULT_ANDROID_ENABLE_NOTIFICATIONS,
22
+ serverUpdateIntervalMinutes: DEFAULT_ANDROID_SERVER_UPDATE_INTERVAL_MINUTES,
23
+ serverUpdateRefresh: DEFAULT_ANDROID_SERVER_UPDATE_REFRESH,
24
+ userImagesPath: DEFAULT_ANDROID_USER_IMAGES_PATH,
25
+ },
26
+ ios: {
27
+ deploymentTarget: DEFAULT_IOS_DEPLOYMENT_TARGET,
28
+ enablePushNotifications: DEFAULT_IOS_ENABLE_PUSH_NOTIFICATIONS,
29
+ serverUpdateIntervalMinutes: DEFAULT_IOS_SERVER_UPDATE_INTERVAL_MINUTES,
30
+ serverUpdateRefresh: DEFAULT_IOS_SERVER_UPDATE_REFRESH,
31
+ userImagesPath: DEFAULT_IOS_USER_IMAGES_PATH,
32
+ widgetFamilies: DEFAULT_IOS_WIDGET_FAMILIES,
33
+ },
34
+ } as const