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,153 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import vm from 'node:vm'
4
+ import { createRequire } from 'node:module'
5
+
6
+ import * as babel from '@babel/core'
7
+
8
+ const MODULE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '']
9
+
10
+ export interface WidgetModuleBuildInfo {
11
+ isDev: false
12
+ metroUrl: null
13
+ appVersion: 'unknown'
14
+ voltraVersion: '1.4.1'
15
+ }
16
+
17
+ export const DYNAMIC_WIDGET_BUILD_INFO: WidgetModuleBuildInfo = {
18
+ isDev: false,
19
+ metroUrl: null,
20
+ appVersion: 'unknown',
21
+ voltraVersion: '1.4.1',
22
+ }
23
+
24
+ export function evaluateWidgetModuleExports(
25
+ projectRoot: string,
26
+ filePath: string,
27
+ createError: (message: string) => Error
28
+ ): unknown {
29
+ const projectRequire = createProjectRequire(projectRoot)
30
+ const moduleCache = new Map<string, unknown>()
31
+
32
+ const customRequire = (moduleSpecifier: string, currentDir: string): unknown => {
33
+ if (!isLocalModule(moduleSpecifier)) {
34
+ return projectRequire(moduleSpecifier)
35
+ }
36
+
37
+ const resolvedModulePath = resolveModulePath(moduleSpecifier, currentDir)
38
+
39
+ if (!resolvedModulePath) {
40
+ throw createError(`Cannot resolve module '${moduleSpecifier}' from '${currentDir}'`)
41
+ }
42
+
43
+ const cachedModule = moduleCache.get(resolvedModulePath)
44
+ if (cachedModule !== undefined) {
45
+ return cachedModule
46
+ }
47
+
48
+ const transpiledCode = transpileWidgetModule(projectRoot, resolvedModulePath, projectRequire, createError)
49
+ const moduleDir = path.dirname(resolvedModulePath)
50
+ const moduleRecord = { exports: {} as Record<string, unknown> }
51
+ moduleCache.set(resolvedModulePath, moduleRecord.exports)
52
+
53
+ const context = vm.createContext({
54
+ __dirname: moduleDir,
55
+ __filename: resolvedModulePath,
56
+ console,
57
+ exports: moduleRecord.exports,
58
+ module: moduleRecord,
59
+ process,
60
+ require: (specifier: string) => customRequire(specifier, moduleDir),
61
+ })
62
+
63
+ const script = new vm.Script(transpiledCode, { filename: resolvedModulePath })
64
+ script.runInContext(context)
65
+
66
+ moduleCache.set(resolvedModulePath, moduleRecord.exports)
67
+ return moduleRecord.exports
68
+ }
69
+
70
+ return customRequire(filePath, path.dirname(filePath))
71
+ }
72
+
73
+ function transpileWidgetModule(
74
+ projectRoot: string,
75
+ filePath: string,
76
+ projectRequire: NodeRequire,
77
+ createError: (message: string) => Error
78
+ ): string {
79
+ const source = fs.readFileSync(filePath, 'utf8')
80
+ const projectBabelConfigPath = resolveProjectBabelConfig(projectRoot)
81
+ const result = babel.transformSync(source, {
82
+ babelrc: false,
83
+ configFile: projectBabelConfigPath,
84
+ cwd: projectRoot,
85
+ filename: filePath,
86
+ presets: projectBabelConfigPath ? undefined : [resolveFallbackBabelPreset(projectRequire, createError)],
87
+ })
88
+
89
+ if (!result?.code) {
90
+ throw createError(`Babel transpilation failed for ${filePath}`)
91
+ }
92
+
93
+ return result.code
94
+ }
95
+
96
+ function resolveProjectBabelConfig(projectRoot: string): string | undefined {
97
+ const candidates = ['babel.config.js', 'babel.config.cjs', 'babel.config.mjs']
98
+
99
+ for (const candidate of candidates) {
100
+ const candidatePath = path.join(projectRoot, candidate)
101
+ if (fs.existsSync(candidatePath)) {
102
+ return candidatePath
103
+ }
104
+ }
105
+
106
+ return undefined
107
+ }
108
+
109
+ function resolveFallbackBabelPreset(projectRequire: NodeRequire, createError: (message: string) => Error): string {
110
+ try {
111
+ return projectRequire.resolve('@react-native/babel-preset')
112
+ } catch {
113
+ try {
114
+ return projectRequire.resolve('babel-preset-expo')
115
+ } catch {
116
+ throw createError(
117
+ 'Could not resolve a Babel preset for widget evaluation. Add a project babel.config.js or install @react-native/babel-preset.'
118
+ )
119
+ }
120
+ }
121
+ }
122
+
123
+ function createProjectRequire(projectRoot: string): NodeRequire {
124
+ return createRequire(path.join(projectRoot, 'package.json'))
125
+ }
126
+
127
+ function isLocalModule(moduleSpecifier: string): boolean {
128
+ return moduleSpecifier.startsWith('.') || moduleSpecifier.startsWith('/')
129
+ }
130
+
131
+ function resolveModulePath(moduleSpecifier: string, fromDir: string): string | null {
132
+ const basePath = path.resolve(fromDir, moduleSpecifier)
133
+
134
+ for (const extension of MODULE_EXTENSIONS) {
135
+ const candidate = `${basePath}${extension}`
136
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
137
+ return candidate
138
+ }
139
+ }
140
+
141
+ if (!fs.existsSync(basePath) || !fs.statSync(basePath).isDirectory()) {
142
+ return null
143
+ }
144
+
145
+ for (const extension of MODULE_EXTENSIONS) {
146
+ const indexCandidate = path.join(basePath, `index${extension}`)
147
+ if (fs.existsSync(indexCandidate) && fs.statSync(indexCandidate).isFile()) {
148
+ return indexCandidate
149
+ }
150
+ }
151
+
152
+ return null
153
+ }
@@ -0,0 +1,67 @@
1
+ import type { Readable, Writable } from 'node:stream'
2
+
3
+ import type * as ClackPrompts from '@clack/prompts'
4
+
5
+ import type { ApplySummary } from './summary'
6
+
7
+ interface CommonClackOptions {
8
+ input?: Readable
9
+ output?: Writable
10
+ }
11
+
12
+ export async function renderApplySummary(summary: ApplySummary, options?: CommonClackOptions): Promise<void> {
13
+ const { outro, log } = await loadClackPrompts()
14
+
15
+ for (const kind of ['created', 'updated', 'deleted'] as const) {
16
+ const paths = summary.changes.filter((change) => change.kind === kind).map((change) => change.path)
17
+
18
+ for (const filePath of paths) {
19
+ log.step(`${capitalize(kind)} ${filePath}`, options)
20
+ }
21
+ }
22
+
23
+ for (const warning of summary.warnings ?? []) {
24
+ log.warn(normalizeClackMessage(warning), options)
25
+ }
26
+
27
+ outro('Done', options)
28
+ }
29
+
30
+ export async function renderError(message: string, options?: CommonClackOptions): Promise<void> {
31
+ const { log } = await loadClackPrompts()
32
+ log.error(normalizeClackMessage(message), options)
33
+ }
34
+
35
+ export async function renderWarning(message: string, options?: CommonClackOptions): Promise<void> {
36
+ const { log } = await loadClackPrompts()
37
+ log.warn(normalizeClackMessage(message), options)
38
+ }
39
+
40
+ export async function renderCancelled(options?: CommonClackOptions): Promise<void> {
41
+ const { cancel } = await loadClackPrompts()
42
+ cancel('Cancelled.', options)
43
+ }
44
+
45
+ export async function renderIntro(options?: CommonClackOptions): Promise<void> {
46
+ const { intro } = await loadClackPrompts()
47
+ intro('Voltra', options)
48
+ }
49
+
50
+ export function normalizeClackMessage(message: string): string {
51
+ return message
52
+ .split('\n')
53
+ .map((line) => line.replace(/^\[voltra\]\s*(Warning:\s*|Error:\s*|Preflight:\s*)?/, ''))
54
+ .join('\n')
55
+ }
56
+
57
+ function capitalize(value: string): string {
58
+ return value.charAt(0).toUpperCase() + value.slice(1)
59
+ }
60
+
61
+ function loadClackPrompts() {
62
+ const dynamicImport = new Function('specifier', 'return import(specifier)') as (
63
+ specifier: string
64
+ ) => Promise<typeof ClackPrompts>
65
+
66
+ return dynamicImport('@clack/prompts')
67
+ }
@@ -0,0 +1,116 @@
1
+ const PREFIX = '[voltra]'
2
+
3
+ export type ReportedChangeKind = 'created' | 'updated' | 'deleted'
4
+
5
+ export interface ReportedChange {
6
+ kind: ReportedChangeKind
7
+ path: string
8
+ }
9
+
10
+ export interface ApplySummary {
11
+ changes: ReportedChange[]
12
+ warnings?: string[]
13
+ }
14
+
15
+ export interface PreflightIssue {
16
+ message: string
17
+ path?: string
18
+ }
19
+
20
+ export interface PreflightFailureReport {
21
+ summary?: string
22
+ issues: PreflightIssue[]
23
+ }
24
+
25
+ export class VoltraCliError extends Error {
26
+ readonly code: string
27
+
28
+ constructor(message: string, code = 'VOLTRA_CLI_ERROR') {
29
+ super(message)
30
+ this.name = 'VoltraCliError'
31
+ this.code = code
32
+ }
33
+ }
34
+
35
+ export class PreflightError extends VoltraCliError {
36
+ readonly report: PreflightFailureReport
37
+
38
+ constructor(report: PreflightFailureReport) {
39
+ super(formatPreflightFailure(report), 'VOLTRA_PREFLIGHT_FAILED')
40
+ this.name = 'PreflightError'
41
+ this.report = report
42
+ }
43
+ }
44
+
45
+ export function formatApplySummary(summary: ApplySummary): string {
46
+ const lines = [
47
+ `${PREFIX} Apply summary`,
48
+ `${PREFIX} Created: ${countChanges(summary.changes, 'created')}`,
49
+ `${PREFIX} Updated: ${countChanges(summary.changes, 'updated')}`,
50
+ `${PREFIX} Deleted: ${countChanges(summary.changes, 'deleted')}`,
51
+ ]
52
+
53
+ for (const kind of ['created', 'updated', 'deleted'] as const) {
54
+ const paths = summary.changes.filter((change) => change.kind === kind).map((change) => change.path)
55
+
56
+ for (const filePath of paths) {
57
+ lines.push(`${PREFIX} ${capitalize(kind)} ${filePath}`)
58
+ }
59
+ }
60
+
61
+ for (const warning of summary.warnings ?? []) {
62
+ lines.push(formatWarning(warning))
63
+ }
64
+
65
+ return lines.join('\n')
66
+ }
67
+
68
+ export function formatDirtyWorktreeWarning(changeCount?: number): string {
69
+ if (!changeCount || changeCount <= 0) {
70
+ return `${PREFIX} Warning: git worktree has uncommitted changes.`
71
+ }
72
+
73
+ const noun = changeCount === 1 ? 'change' : 'changes'
74
+ return `${PREFIX} Warning: git worktree has ${changeCount} uncommitted ${noun}.`
75
+ }
76
+
77
+ export function formatAmbiguousDiscoveryWarning(subject: string, candidates: string[]): string {
78
+ if (candidates.length === 0) {
79
+ return `${PREFIX} Warning: ${subject} is ambiguous.`
80
+ }
81
+
82
+ return `${PREFIX} Warning: ${subject} is ambiguous. Candidates: ${candidates.join(', ')}`
83
+ }
84
+
85
+ export function formatWarning(message: string): string {
86
+ return `${PREFIX} Warning: ${message}`
87
+ }
88
+
89
+ export function formatError(message: string): string {
90
+ return `${PREFIX} Error: ${message}`
91
+ }
92
+
93
+ export function formatPreflightFailure(report: PreflightFailureReport): string {
94
+ const lines = [
95
+ report.summary ? formatError(report.summary) : formatError('Preflight failed.'),
96
+ ...report.issues.map((issue) => formatPreflightIssue(issue)),
97
+ ]
98
+
99
+ return lines.join('\n')
100
+ }
101
+
102
+ function formatPreflightIssue(issue: PreflightIssue): string {
103
+ if (!issue.path) {
104
+ return `${PREFIX} Preflight: ${issue.message}`
105
+ }
106
+
107
+ return `${PREFIX} Preflight: ${issue.path}: ${issue.message}`
108
+ }
109
+
110
+ function countChanges(changes: ReportedChange[], kind: ReportedChangeKind): number {
111
+ return changes.filter((change) => change.kind === kind).length
112
+ }
113
+
114
+ function capitalize(value: string): string {
115
+ return value.charAt(0).toUpperCase() + value.slice(1)
116
+ }
@@ -0,0 +1,21 @@
1
+ import { normalizeTrackedStateFiles, normalizeTrackedStateFilesForDiff } from './files'
2
+
3
+ import type { VoltraState } from './load'
4
+
5
+ export interface VoltraStateDiff {
6
+ previousFiles: string[]
7
+ nextFiles: string[]
8
+ staleFiles: string[]
9
+ }
10
+
11
+ export function diffVoltraState(previousState: VoltraState | undefined, nextFiles: string[]): VoltraStateDiff {
12
+ const previousFiles = normalizeTrackedStateFilesForDiff(previousState?.files)
13
+ const normalizedNextFiles = normalizeTrackedStateFiles(nextFiles, 'Next Voltra state files')
14
+ const nextFileSet = new Set(normalizedNextFiles)
15
+
16
+ return {
17
+ previousFiles,
18
+ nextFiles: normalizedNextFiles,
19
+ staleFiles: previousFiles.filter((filePath) => !nextFileSet.has(filePath)),
20
+ }
21
+ }
@@ -0,0 +1,52 @@
1
+ import path from 'node:path'
2
+
3
+ import { normalizeRelativePath } from '../fs/path'
4
+ import { VoltraCliError } from '../reporting/summary'
5
+
6
+ export function normalizeTrackedStateFiles(files: string[] | unknown[], errorContext: string): string[] {
7
+ if (!Array.isArray(files)) {
8
+ throw new VoltraCliError(`${errorContext} must be an array.`)
9
+ }
10
+
11
+ const seen = new Set<string>()
12
+ const normalizedFiles: string[] = []
13
+
14
+ for (const filePath of files) {
15
+ if (typeof filePath !== 'string' || !filePath.trim()) {
16
+ throw new VoltraCliError(`${errorContext} must contain only non-empty relative paths.`)
17
+ }
18
+
19
+ const normalizedFilePath = normalizeTrackedStateFilePath(filePath)
20
+
21
+ if (path.isAbsolute(normalizedFilePath)) {
22
+ throw new VoltraCliError(`${errorContext} must contain only project-relative paths.`)
23
+ }
24
+
25
+ if (seen.has(normalizedFilePath)) {
26
+ continue
27
+ }
28
+
29
+ seen.add(normalizedFilePath)
30
+ normalizedFiles.push(normalizedFilePath)
31
+ }
32
+
33
+ return normalizedFiles.sort((left, right) => left.localeCompare(right))
34
+ }
35
+
36
+ function normalizeTrackedStateFilePath(filePath: string): string {
37
+ const normalizedFilePath = normalizeRelativePath(path.posix.normalize(filePath))
38
+
39
+ if (normalizedFilePath === '..' || normalizedFilePath.startsWith('../')) {
40
+ throw new VoltraCliError('Voltra state files must contain only project-relative paths.')
41
+ }
42
+
43
+ return normalizedFilePath
44
+ }
45
+
46
+ export function normalizeTrackedStateFilesForDiff(files: string[] | undefined): string[] {
47
+ if (!files || files.length === 0) {
48
+ return []
49
+ }
50
+
51
+ return normalizeTrackedStateFiles(files, 'Voltra state files')
52
+ }
@@ -0,0 +1,66 @@
1
+ import path from 'node:path'
2
+
3
+ import { pathExists, readJsonFile } from '../fs/readWrite'
4
+ import { VoltraCliError } from '../reporting/summary'
5
+
6
+ import { normalizeTrackedStateFiles } from './files'
7
+
8
+ const STATE_SCHEMA_VERSION = 1
9
+ const STATE_DIRECTORY_NAME = '.voltra'
10
+ const STATE_FILE_NAME = 'state.json'
11
+
12
+ export interface VoltraState {
13
+ schemaVersion: 1
14
+ files: string[]
15
+ }
16
+
17
+ interface RawVoltraState {
18
+ schemaVersion?: unknown
19
+ files?: unknown
20
+ }
21
+
22
+ export async function loadVoltraState(projectRoot: string): Promise<VoltraState | undefined> {
23
+ const statePath = getVoltraStatePath(projectRoot)
24
+
25
+ if (!(await pathExists(statePath))) {
26
+ return undefined
27
+ }
28
+
29
+ let rawState: RawVoltraState
30
+
31
+ try {
32
+ rawState = await readJsonFile<RawVoltraState>(statePath)
33
+ } catch (error: unknown) {
34
+ const message = error instanceof Error ? error.message : String(error)
35
+ throw new VoltraCliError(`Failed to read Voltra state at ${statePath}: ${message}`)
36
+ }
37
+
38
+ return validateVoltraState(rawState, statePath)
39
+ }
40
+
41
+ export function getVoltraStatePath(projectRoot: string): string {
42
+ return path.join(projectRoot, STATE_DIRECTORY_NAME, STATE_FILE_NAME)
43
+ }
44
+
45
+ function validateVoltraState(rawState: RawVoltraState, statePath: string): VoltraState {
46
+ if (!rawState || typeof rawState !== 'object' || Array.isArray(rawState)) {
47
+ throw new VoltraCliError(`Voltra state at ${statePath} must be a JSON object.`)
48
+ }
49
+
50
+ if (rawState.schemaVersion !== STATE_SCHEMA_VERSION) {
51
+ throw new VoltraCliError(
52
+ `Unsupported Voltra state schema at ${statePath}: expected ${STATE_SCHEMA_VERSION}, received ${String(
53
+ rawState.schemaVersion
54
+ )}.`
55
+ )
56
+ }
57
+
58
+ if (!Array.isArray(rawState.files)) {
59
+ throw new VoltraCliError(`Voltra state at ${statePath} must contain a files array.`)
60
+ }
61
+
62
+ return {
63
+ schemaVersion: STATE_SCHEMA_VERSION,
64
+ files: normalizeTrackedStateFiles(rawState.files, `Voltra state at ${statePath} files`),
65
+ }
66
+ }
@@ -0,0 +1,31 @@
1
+ import { writeJsonFile } from '../fs/readWrite'
2
+ import { VoltraCliError } from '../reporting/summary'
3
+
4
+ import { normalizeTrackedStateFiles } from './files'
5
+ import { getVoltraStatePath } from './load'
6
+
7
+ import type { VoltraState } from './load'
8
+
9
+ const STATE_SCHEMA_VERSION = 1
10
+
11
+ export interface SaveVoltraStateInput {
12
+ files: string[]
13
+ }
14
+
15
+ export async function saveVoltraState(projectRoot: string, input: SaveVoltraStateInput): Promise<VoltraState> {
16
+ const state: VoltraState = {
17
+ schemaVersion: STATE_SCHEMA_VERSION,
18
+ files: normalizeTrackedStateFiles(input.files, 'Voltra state files'),
19
+ }
20
+
21
+ const statePath = getVoltraStatePath(projectRoot)
22
+
23
+ try {
24
+ await writeJsonFile(statePath, state)
25
+ } catch (error: unknown) {
26
+ const message = error instanceof Error ? error.message : String(error)
27
+ throw new VoltraCliError(`Failed to write Voltra state at ${statePath}: ${message}`)
28
+ }
29
+
30
+ return state
31
+ }
@@ -0,0 +1,23 @@
1
+ declare module 'xml2js' {
2
+ export interface ParserOptions {
3
+ explicitArray?: boolean
4
+ explicitChildren?: boolean
5
+ preserveChildrenOrder?: boolean
6
+ }
7
+
8
+ export interface BuilderOptions {
9
+ headless?: boolean
10
+ renderOpts?: {
11
+ pretty?: boolean
12
+ indent?: string
13
+ newline?: string
14
+ }
15
+ }
16
+
17
+ export class Builder {
18
+ constructor(options?: BuilderOptions)
19
+ buildObject(rootObj: unknown): string
20
+ }
21
+
22
+ export function parseStringPromise(value: string, options?: ParserOptions): Promise<unknown>
23
+ }