@symbo.ls/cli 2.33.11 → 2.33.13
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/bin/collab.js +334 -0
- package/bin/fetch.js +77 -54
- package/bin/fs.js +150 -61
- package/bin/index.js +1 -0
- package/bin/install.js +6 -19
- package/bin/login.js +82 -62
- package/bin/push.js +157 -117
- package/bin/require.js +140 -6
- package/bin/socket-server.js +5 -10
- package/bin/sync.js +313 -112
- package/helpers/apiUtils.js +60 -58
- package/helpers/buildMessages.js +76 -0
- package/helpers/changesUtils.js +474 -0
- package/helpers/compareUtils.js +136 -2
- package/helpers/config.js +131 -18
- package/helpers/credentialManager.js +66 -0
- package/helpers/fileUtils.js +8 -2
- package/helpers/symbolsConfig.js +35 -0
- package/helpers/transportUtils.js +58 -0
- package/package.json +7 -5
package/bin/sync.js
CHANGED
|
@@ -1,128 +1,329 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import fs from 'fs'
|
|
4
|
+
import path from 'path'
|
|
3
5
|
import chalk from 'chalk'
|
|
4
|
-
import
|
|
6
|
+
import inquirer from 'inquirer'
|
|
5
7
|
import { loadModule } from './require.js'
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
8
|
+
import { program } from './program.js'
|
|
9
|
+
import { CredentialManager } from '../helpers/credentialManager.js'
|
|
10
|
+
import { buildDirectory } from '../helpers/fileUtils.js'
|
|
11
|
+
import { generateDiffDisplay, showDiffPager } from '../helpers/diffUtils.js'
|
|
12
|
+
import { normalizeKeys } from '../helpers/compareUtils.js'
|
|
13
|
+
import { getCurrentProjectData, postProjectChanges } from '../helpers/apiUtils.js'
|
|
14
|
+
import { computeCoarseChanges, threeWayRebase, computeOrdersForTuples, preprocessChanges } from '../helpers/changesUtils.js'
|
|
15
|
+
import { createFs } from './fs.js'
|
|
16
|
+
import { showAuthRequiredMessages, showBuildErrorMessages } from '../helpers/buildMessages.js'
|
|
17
|
+
import { loadSymbolsConfig } from '../helpers/symbolsConfig.js'
|
|
18
|
+
import { loadCliConfig, readLock, writeLock, getConfigPaths, updateLegacySymbolsJson } from '../helpers/config.js'
|
|
19
|
+
const RC_PATH = process.cwd() + '/symbols.json'
|
|
20
|
+
const distDir = path.join(process.cwd(), 'smbls')
|
|
21
|
+
|
|
22
|
+
async function buildLocalProject() {
|
|
23
|
+
try {
|
|
24
|
+
const outputDirectory = path.join(distDir, 'dist')
|
|
25
|
+
await buildDirectory(distDir, outputDirectory)
|
|
26
|
+
const outputFile = path.join(outputDirectory, 'index.js')
|
|
27
|
+
return normalizeKeys(await loadModule(outputFile, { silent: false }))
|
|
28
|
+
} catch (error) {
|
|
29
|
+
// Enhance error with build context
|
|
30
|
+
error.buildContext = {
|
|
31
|
+
command: 'sync',
|
|
32
|
+
workspace: process.cwd()
|
|
33
|
+
}
|
|
34
|
+
throw error
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function resolveTopLevelConflicts(conflictsKeys, ours, theirs) {
|
|
39
|
+
const choices = conflictsKeys.map((key) => {
|
|
40
|
+
const ourChange = ours.find((c) => c[1][0] === key)
|
|
41
|
+
const theirChange = theirs.find((c) => c[1][0] === key)
|
|
42
|
+
return {
|
|
43
|
+
name: `${chalk.cyan(key)} ${chalk.green('Local')} vs ${chalk.red('Remote')}`,
|
|
44
|
+
value: key,
|
|
45
|
+
short: key,
|
|
46
|
+
description: `${chalk.green('+ Local:')} ${JSON.stringify(ourChange?.[2])}
|
|
47
|
+
${chalk.red('- Remote:')} ${JSON.stringify(theirChange?.[2])}`
|
|
48
|
+
}
|
|
49
|
+
})
|
|
8
50
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
51
|
+
const { selected } = await inquirer.prompt([{
|
|
52
|
+
type: 'checkbox',
|
|
53
|
+
name: 'selected',
|
|
54
|
+
message: 'Conflicts detected. Select the keys to keep from Local (unchecked will keep Remote):',
|
|
55
|
+
choices,
|
|
56
|
+
pageSize: 10
|
|
57
|
+
}])
|
|
12
58
|
|
|
13
|
-
const
|
|
59
|
+
const final = conflictsKeys.map((key) => {
|
|
60
|
+
if (selected.includes(key)) {
|
|
61
|
+
return ours.find((c) => c[1][0] === key)
|
|
62
|
+
}
|
|
63
|
+
return theirs.find((c) => c[1][0] === key)
|
|
64
|
+
}).filter(Boolean)
|
|
14
65
|
|
|
15
|
-
|
|
16
|
-
|
|
66
|
+
return final
|
|
67
|
+
}
|
|
17
68
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
)
|
|
69
|
+
function getAt(obj, pathArr = []) {
|
|
70
|
+
try {
|
|
71
|
+
return pathArr.reduce((acc, k) => (acc == null ? undefined : acc[k]), obj)
|
|
72
|
+
} catch (_) {
|
|
73
|
+
return undefined
|
|
74
|
+
}
|
|
75
|
+
}
|
|
21
76
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
77
|
+
function buildDiffsFromChanges(changes, base, target) {
|
|
78
|
+
const diffs = []
|
|
79
|
+
for (const [op, path, value] of changes) {
|
|
80
|
+
const oldVal = getAt(base, path)
|
|
81
|
+
if (op === 'delete') {
|
|
82
|
+
diffs.push(generateDiffDisplay('delete', path, oldVal))
|
|
83
|
+
} else {
|
|
84
|
+
const newVal = value !== undefined ? value : getAt(target, path)
|
|
85
|
+
diffs.push(generateDiffDisplay('update', path, oldVal, newVal))
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return diffs
|
|
28
89
|
}
|
|
29
90
|
|
|
30
|
-
|
|
31
|
-
.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
91
|
+
async function confirmChanges(localChanges, remoteChanges, base, local, remote) {
|
|
92
|
+
if (localChanges.length === 0 && remoteChanges.length === 0) {
|
|
93
|
+
console.log(chalk.bold.yellow('No changes detected'))
|
|
94
|
+
return false
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
console.log(chalk.bold.white('\nDetected changes:'))
|
|
98
|
+
|
|
99
|
+
if (localChanges.length) {
|
|
100
|
+
const localChangesByType = localChanges.reduce((acc, [type]) => {
|
|
101
|
+
acc[type] = (acc[type] || 0) + 1
|
|
102
|
+
return acc
|
|
103
|
+
}, {})
|
|
104
|
+
console.log(chalk.cyan('\nLocal changes:'))
|
|
105
|
+
Object.entries(localChangesByType).forEach(([type, count]) => {
|
|
106
|
+
console.log(chalk.gray(`- ${type}: ${chalk.cyan(count)} changes`))
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (remoteChanges.length) {
|
|
111
|
+
const remoteChangesByType = remoteChanges.reduce((acc, [type]) => {
|
|
112
|
+
acc[type] = (acc[type] || 0) + 1
|
|
113
|
+
return acc
|
|
114
|
+
}, {})
|
|
115
|
+
console.log(chalk.cyan('\nRemote changes:'))
|
|
116
|
+
Object.entries(remoteChangesByType).forEach(([type, count]) => {
|
|
117
|
+
console.log(chalk.gray(`- ${type}: ${chalk.cyan(count)} changes`))
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const { view } = await inquirer.prompt([{
|
|
122
|
+
type: 'confirm',
|
|
123
|
+
name: 'view',
|
|
124
|
+
message: 'View full list of changes?',
|
|
125
|
+
default: false
|
|
126
|
+
}])
|
|
127
|
+
if (view) {
|
|
128
|
+
const diffs = []
|
|
129
|
+
if (localChanges.length) {
|
|
130
|
+
diffs.push(chalk.bold('\nLocal changes:\n'))
|
|
131
|
+
diffs.push(...buildDiffsFromChanges(localChanges, base, local))
|
|
132
|
+
}
|
|
133
|
+
if (remoteChanges.length) {
|
|
134
|
+
diffs.push(chalk.bold('\nRemote changes:\n'))
|
|
135
|
+
diffs.push(...buildDiffsFromChanges(remoteChanges, base, remote))
|
|
136
|
+
}
|
|
137
|
+
await showDiffPager(diffs)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const { proceed } = await inquirer.prompt([{
|
|
141
|
+
type: 'confirm',
|
|
142
|
+
name: 'proceed',
|
|
143
|
+
message: 'Proceed with sync?',
|
|
144
|
+
default: false
|
|
145
|
+
}])
|
|
146
|
+
|
|
147
|
+
return proceed
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function syncProjectChanges(options) {
|
|
151
|
+
const credManager = new CredentialManager()
|
|
152
|
+
const authToken = credManager.ensureAuthToken()
|
|
153
|
+
|
|
154
|
+
if (!authToken) {
|
|
155
|
+
showAuthRequiredMessages()
|
|
156
|
+
process.exit(1)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
// Load configuration
|
|
161
|
+
const symbolsConfig = await loadSymbolsConfig()
|
|
162
|
+
const cliConfig = loadCliConfig()
|
|
163
|
+
const lock = readLock()
|
|
164
|
+
const { projectPath } = getConfigPaths()
|
|
165
|
+
const { key: legacyKey } = symbolsConfig
|
|
166
|
+
const appKey = cliConfig.projectKey || legacyKey
|
|
167
|
+
const localBranch = cliConfig.branch || symbolsConfig.branch || 'main'
|
|
168
|
+
|
|
169
|
+
if (options.verbose) {
|
|
170
|
+
console.log(chalk.dim('\nSync configuration:'))
|
|
171
|
+
console.log(chalk.gray(`App Key: ${chalk.cyan(appKey)}`))
|
|
172
|
+
console.log(chalk.gray(`Current version: ${chalk.cyan(lock.version || 'unknown')}`))
|
|
173
|
+
console.log(chalk.gray(`Branch: ${chalk.cyan(localBranch)}`))
|
|
174
|
+
console.log(chalk.gray(`Environment: ${chalk.cyan(options.dev ? 'Development' : 'Production')}\n`))
|
|
175
|
+
} else {
|
|
176
|
+
console.log(chalk.dim('\nSyncing project...'))
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Build and load local project
|
|
180
|
+
console.log(chalk.dim('Building local project...'))
|
|
181
|
+
let localProject
|
|
182
|
+
try {
|
|
183
|
+
localProject = await buildLocalProject()
|
|
184
|
+
console.log(chalk.gray('Local project built successfully'))
|
|
185
|
+
} catch (buildError) {
|
|
186
|
+
showBuildErrorMessages(buildError)
|
|
187
|
+
process.exit(1)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Load base snapshot (last pulled)
|
|
191
|
+
const baseSnapshot = (() => {
|
|
192
|
+
try {
|
|
193
|
+
const raw = fs.readFileSync(projectPath, 'utf8')
|
|
194
|
+
return JSON.parse(raw)
|
|
195
|
+
} catch (_) {
|
|
196
|
+
return {}
|
|
197
|
+
}
|
|
198
|
+
})()
|
|
199
|
+
|
|
200
|
+
// Get latest server data (ignore ETag to ensure latest)
|
|
201
|
+
console.log(chalk.dim('Fetching server data...'))
|
|
202
|
+
const serverResp = await getCurrentProjectData(
|
|
203
|
+
{ projectKey: appKey, projectId: lock.projectId },
|
|
204
|
+
authToken,
|
|
205
|
+
{ branch: localBranch, includePending: true }
|
|
206
|
+
)
|
|
207
|
+
const serverProject = serverResp.data || {}
|
|
208
|
+
console.log(chalk.gray('Server data fetched successfully'))
|
|
209
|
+
|
|
210
|
+
// Generate coarse local and remote changes via simple three-way rebase
|
|
211
|
+
const base = normalizeKeys(baseSnapshot || {})
|
|
212
|
+
const local = normalizeKeys(localProject || {})
|
|
213
|
+
const remote = normalizeKeys(serverProject || {})
|
|
214
|
+
const { ours, theirs, conflicts, finalChanges } = threeWayRebase(base, local, remote)
|
|
215
|
+
|
|
216
|
+
const localChanges = ours
|
|
217
|
+
const remoteChanges = theirs
|
|
218
|
+
|
|
219
|
+
if (!localChanges.length && !remoteChanges.length) {
|
|
220
|
+
console.log(chalk.bold.green('\nProject is already in sync'))
|
|
53
221
|
return
|
|
54
222
|
}
|
|
55
223
|
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
socketUrl,
|
|
70
|
-
onConnect: (id, socket) => {
|
|
71
|
-
console.log(
|
|
72
|
-
'Connected to',
|
|
73
|
-
chalk.green(key),
|
|
74
|
-
'from',
|
|
75
|
-
chalk.bold('Symbols'),
|
|
76
|
-
'socket server'
|
|
77
|
-
)
|
|
78
|
-
console.log('Socket id:', id)
|
|
79
|
-
console.log(chalk.dim('\nListening to updates...\n'))
|
|
80
|
-
},
|
|
81
|
-
onChange: debounce(async (event, data) => {
|
|
82
|
-
if (event === 'clients') {
|
|
83
|
-
console.log(
|
|
84
|
-
'Active clients:',
|
|
85
|
-
chalk.green.bold(Object.keys(data).join(', '))
|
|
86
|
-
)
|
|
87
|
-
return
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const parseData = JSON.parse(data)
|
|
91
|
-
const d = parseData && parseData.DATA
|
|
92
|
-
|
|
93
|
-
if (!d) return
|
|
94
|
-
|
|
95
|
-
if (Object.keys(d).length) {
|
|
96
|
-
console.log(chalk.dim('\n----------------\n'))
|
|
97
|
-
console.log(chalk.dim('Received update:'))
|
|
98
|
-
console.log(Object.keys(d).join(', '))
|
|
99
|
-
if (verboseCode)
|
|
100
|
-
console.log(chalk.dim(JSON.stringify(d, null, prettify ?? 2)))
|
|
101
|
-
|
|
102
|
-
if (distDir) {
|
|
103
|
-
if (fetchOpt) {
|
|
104
|
-
await fetchFromCli(options)
|
|
105
|
-
console.log(chalk.dim('\n----------------\n'))
|
|
106
|
-
return
|
|
107
|
-
}
|
|
108
|
-
} else {
|
|
109
|
-
updateDynamycFile(d, { framework, ...options })
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (d.components && convertOpt && framework) {
|
|
114
|
-
convertFromCli(d.components, {
|
|
115
|
-
...options,
|
|
116
|
-
framework
|
|
117
|
-
})
|
|
118
|
-
}
|
|
119
|
-
}, 1500),
|
|
120
|
-
onError: (err, socket) => {
|
|
121
|
-
console.log(chalk.bold.green('Error during connection'))
|
|
122
|
-
if (verbose) console.error(err)
|
|
123
|
-
else console.log(debugMsg)
|
|
124
|
-
},
|
|
125
|
-
...options
|
|
224
|
+
// Show change summaries
|
|
225
|
+
if (localChanges.length) {
|
|
226
|
+
const byType = localChanges.reduce((acc, [t]) => ((acc[t] = (acc[t] || 0) + 1), acc), {})
|
|
227
|
+
console.log(chalk.cyan('\nLocal changes:'))
|
|
228
|
+
Object.entries(byType).forEach(([type, count]) => {
|
|
229
|
+
console.log(chalk.gray(`- ${type}: ${chalk.cyan(count)} changes`))
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
if (remoteChanges.length) {
|
|
233
|
+
const byType = remoteChanges.reduce((acc, [t]) => ((acc[t] = (acc[t] || 0) + 1), acc), {})
|
|
234
|
+
console.log(chalk.cyan('\nRemote changes:'))
|
|
235
|
+
Object.entries(byType).forEach(([type, count]) => {
|
|
236
|
+
console.log(chalk.gray(`- ${type}: ${chalk.cyan(count)} changes`))
|
|
126
237
|
})
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Handle conflicts if any
|
|
241
|
+
let toApply = finalChanges && finalChanges.length ? finalChanges : [...localChanges]
|
|
242
|
+
if (conflicts.length) {
|
|
243
|
+
console.log(chalk.yellow(`\nFound ${conflicts.length} conflicts`))
|
|
244
|
+
const chosen = await resolveTopLevelConflicts(conflicts, ours, theirs)
|
|
245
|
+
// Combine non-conflicting ours with chosen resolutions
|
|
246
|
+
const nonConflictOurs = ours.filter(([_, [k]]) => !conflicts.includes(k))
|
|
247
|
+
toApply = [...nonConflictOurs, ...chosen]
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Confirm sync
|
|
251
|
+
const shouldProceed = await confirmChanges(localChanges, remoteChanges, base, local, remote)
|
|
252
|
+
if (!shouldProceed) {
|
|
253
|
+
console.log(chalk.yellow('Sync cancelled'))
|
|
254
|
+
return
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Update server
|
|
258
|
+
console.log(chalk.dim('\nUpdating server...'))
|
|
259
|
+
const projectId = lock.projectId || serverProject?.projectInfo?.id
|
|
260
|
+
if (!projectId) {
|
|
261
|
+
console.log(chalk.red('Unable to resolve projectId. Please fetch first to initialize lock.'))
|
|
262
|
+
process.exit(1)
|
|
263
|
+
}
|
|
264
|
+
const operationId = `cli-${Date.now()}`
|
|
265
|
+
// Expand into granular changes against remote/server state, compute orders from local
|
|
266
|
+
const { granularChanges } = preprocessChanges(remote, toApply)
|
|
267
|
+
const orders = computeOrdersForTuples(local, granularChanges)
|
|
268
|
+
const result = await postProjectChanges(projectId, authToken, {
|
|
269
|
+
branch: localBranch,
|
|
270
|
+
type: options.type || 'patch',
|
|
271
|
+
operationId,
|
|
272
|
+
changes: toApply,
|
|
273
|
+
granularChanges,
|
|
274
|
+
orders
|
|
127
275
|
})
|
|
128
|
-
|
|
276
|
+
const { id: versionId, value: version } = result.noOp ? {} : (result.data || {})
|
|
277
|
+
|
|
278
|
+
// Update symbols.json with new version
|
|
279
|
+
if (version) {
|
|
280
|
+
updateLegacySymbolsJson({ ...(symbolsConfig || {}), version, branch: localBranch, versionId })
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Get latest project data after sync
|
|
284
|
+
console.log(chalk.dim('Fetching latest project data...'))
|
|
285
|
+
const updated = await getCurrentProjectData(
|
|
286
|
+
{ projectKey: appKey, projectId },
|
|
287
|
+
authToken,
|
|
288
|
+
{ branch: localBranch, includePending: true }
|
|
289
|
+
)
|
|
290
|
+
const updatedServerData = updated?.data || {}
|
|
291
|
+
|
|
292
|
+
// Apply changes to local files
|
|
293
|
+
console.log(chalk.dim('Updating local files...'))
|
|
294
|
+
await createFs(updatedServerData, distDir, { update: true, metadata: false })
|
|
295
|
+
console.log(chalk.gray('Local files updated successfully'))
|
|
296
|
+
|
|
297
|
+
console.log(chalk.bold.green('\nProject synced successfully!'))
|
|
298
|
+
console.log(chalk.gray(`New version: ${chalk.cyan(version)}`))
|
|
299
|
+
|
|
300
|
+
// Update lock and base snapshot
|
|
301
|
+
writeLock({
|
|
302
|
+
etag: updated.etag || null,
|
|
303
|
+
version: version || (lock.version || null),
|
|
304
|
+
branch: localBranch,
|
|
305
|
+
projectId,
|
|
306
|
+
pulledAt: new Date().toISOString()
|
|
307
|
+
})
|
|
308
|
+
try {
|
|
309
|
+
const { projectPath } = getConfigPaths()
|
|
310
|
+
await fs.promises.writeFile(projectPath, JSON.stringify(updatedServerData, null, 2))
|
|
311
|
+
} catch (_) {}
|
|
312
|
+
|
|
313
|
+
} catch (error) {
|
|
314
|
+
console.error(chalk.bold.red('\nSync failed:'), chalk.white(error.message))
|
|
315
|
+
if (options.verbose && error.stack) {
|
|
316
|
+
console.error(chalk.dim('\nStack trace:'))
|
|
317
|
+
console.error(chalk.gray(error.stack))
|
|
318
|
+
}
|
|
319
|
+
process.exit(1)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
program
|
|
324
|
+
.command('sync')
|
|
325
|
+
.description('Sync local changes with remote server')
|
|
326
|
+
.option('-m, --message <message>', 'Specify a commit message')
|
|
327
|
+
.option('-d, --dev', 'Run against local server')
|
|
328
|
+
.option('-v, --verbose', 'Show verbose output')
|
|
329
|
+
.action(syncProjectChanges)
|
package/helpers/apiUtils.js
CHANGED
|
@@ -1,73 +1,75 @@
|
|
|
1
1
|
import { getApiUrl } from './config.js'
|
|
2
|
-
import { ALLOWED_FIELDS } from './compareUtils.js'
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const data = await response.json()
|
|
15
|
-
|
|
16
|
-
if (!response.ok) {
|
|
17
|
-
const error = new Error(`Failed to fetch server data: ${response.statusText}`)
|
|
18
|
-
error.response = {
|
|
19
|
-
status: response.status,
|
|
20
|
-
data
|
|
21
|
-
}
|
|
22
|
-
throw error
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Check if response is empty object
|
|
26
|
-
if (data && Object.keys(data).length === 0) {
|
|
27
|
-
const error = new Error('Project not found')
|
|
28
|
-
error.response = {
|
|
29
|
-
status: 404,
|
|
30
|
-
data
|
|
31
|
-
}
|
|
32
|
-
throw error
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return data
|
|
36
|
-
} catch (error) {
|
|
37
|
-
if (!error.response) {
|
|
38
|
-
error.response = {
|
|
39
|
-
status: 500,
|
|
40
|
-
data: {}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
throw error
|
|
3
|
+
// New API (adds ETag and new endpoints)
|
|
4
|
+
function buildProjectDataUrl({ projectKey, projectId, branch, includePending = true, version }) {
|
|
5
|
+
const base = getApiUrl()
|
|
6
|
+
let url
|
|
7
|
+
if (projectId) {
|
|
8
|
+
url = new URL(`${base}/core/projects/${encodeURIComponent(projectId)}/data`)
|
|
9
|
+
} else if (projectKey) {
|
|
10
|
+
url = new URL(`${base}/core/projects/key/${encodeURIComponent(projectKey)}/data`)
|
|
11
|
+
} else {
|
|
12
|
+
throw new Error('Missing project identifier (projectId or projectKey)')
|
|
44
13
|
}
|
|
14
|
+
if (branch) url.searchParams.set('branch', branch)
|
|
15
|
+
if (includePending !== undefined) url.searchParams.set('includePending', includePending ? 'true' : 'false')
|
|
16
|
+
if (version) url.searchParams.set('version', version)
|
|
17
|
+
return url.toString()
|
|
45
18
|
}
|
|
46
19
|
|
|
47
|
-
export async function
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
20
|
+
export async function getCurrentProjectData(project, authToken, opts = {}) {
|
|
21
|
+
const { branch, includePending = true, etag, version } = opts
|
|
22
|
+
const url = buildProjectDataUrl({ projectKey: project.projectKey, projectId: project.projectId, branch, includePending, version })
|
|
23
|
+
const headers = {
|
|
24
|
+
'Authorization': `Bearer ${authToken}`
|
|
25
|
+
}
|
|
26
|
+
if (etag) headers['If-None-Match'] = etag
|
|
27
|
+
const response = await fetch(url, { method: 'GET', headers })
|
|
28
|
+
if (response.status === 304) {
|
|
29
|
+
return { notModified: true, etag: etag }
|
|
30
|
+
}
|
|
31
|
+
if (!response.ok) {
|
|
32
|
+
const data = await safeJson(response)
|
|
33
|
+
const err = new Error(data?.message || `Failed to fetch project data (${response.status})`)
|
|
34
|
+
err.response = { status: response.status, data }
|
|
35
|
+
throw err
|
|
36
|
+
}
|
|
37
|
+
const json = await response.json()
|
|
38
|
+
return {
|
|
39
|
+
notModified: false,
|
|
40
|
+
etag: response.headers.get('ETag') || response.headers.get('Etag') || null,
|
|
41
|
+
data: json?.data || json
|
|
42
|
+
}
|
|
43
|
+
}
|
|
53
44
|
|
|
54
|
-
|
|
45
|
+
export async function postProjectChanges(projectId, authToken, body) {
|
|
46
|
+
const base = getApiUrl()
|
|
47
|
+
const url = `${base}/core/projects/${encodeURIComponent(projectId)}/changes`
|
|
48
|
+
const response = await fetch(url, {
|
|
55
49
|
method: 'POST',
|
|
56
50
|
headers: {
|
|
57
51
|
'Content-Type': 'application/json',
|
|
58
|
-
Authorization: `Bearer ${authToken}`
|
|
52
|
+
'Authorization': `Bearer ${authToken}`
|
|
59
53
|
},
|
|
60
|
-
body: JSON.stringify(
|
|
61
|
-
appKey,
|
|
62
|
-
changes: validChanges,
|
|
63
|
-
projectUpdates: projectData
|
|
64
|
-
})
|
|
54
|
+
body: JSON.stringify(body)
|
|
65
55
|
})
|
|
66
|
-
|
|
56
|
+
if (response.status === 204) {
|
|
57
|
+
return { noOp: true }
|
|
58
|
+
}
|
|
67
59
|
if (!response.ok) {
|
|
68
|
-
const
|
|
69
|
-
|
|
60
|
+
const data = await safeJson(response)
|
|
61
|
+
const err = new Error(data?.message || `Failed to post changes (${response.status})`)
|
|
62
|
+
err.response = { status: response.status, data }
|
|
63
|
+
throw err
|
|
70
64
|
}
|
|
65
|
+
const json = await response.json()
|
|
66
|
+
return { noOp: false, data: json?.data || json }
|
|
67
|
+
}
|
|
71
68
|
|
|
72
|
-
|
|
69
|
+
async function safeJson(res) {
|
|
70
|
+
try {
|
|
71
|
+
return await res.json()
|
|
72
|
+
} catch (_) {
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
73
75
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
|
|
3
|
+
const CREATE_PROJECT_URL = 'https://symbols.app/create'
|
|
4
|
+
|
|
5
|
+
export const showAuthRequiredMessages = () => {
|
|
6
|
+
console.log('\n' + chalk.yellow('⚠️ Authentication Required'))
|
|
7
|
+
console.log(chalk.white('\nTo access Symbols resources, please log in first:'))
|
|
8
|
+
console.log(chalk.cyan('\n smbls login'))
|
|
9
|
+
|
|
10
|
+
console.log('\nNeed help?')
|
|
11
|
+
console.log(chalk.dim('• Make sure you have a Symbols account'))
|
|
12
|
+
console.log(chalk.dim('• Visit https://symbols.app/docs/cli for more information'))
|
|
13
|
+
console.log(chalk.dim('• Contact support@symbols.app if you continue having issues\n'))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const showProjectNotFoundMessages = (appKey) => {
|
|
17
|
+
console.error(chalk.bold.red('\nProject not found or access denied.'))
|
|
18
|
+
console.error(chalk.bold.yellow('\nPossible reasons:'))
|
|
19
|
+
console.error(chalk.gray('1. The project does not exist'))
|
|
20
|
+
console.error(chalk.gray("2. You don't have access to this project"))
|
|
21
|
+
console.error(chalk.gray('3. The app key in symbols.json might be incorrect'))
|
|
22
|
+
|
|
23
|
+
console.error(chalk.bold.yellow('\nTo resolve this:'))
|
|
24
|
+
console.error(chalk.white(
|
|
25
|
+
`1. Visit ${chalk.cyan.underline(
|
|
26
|
+
CREATE_PROJECT_URL
|
|
27
|
+
)} to create a new project`
|
|
28
|
+
))
|
|
29
|
+
console.error(chalk.white(
|
|
30
|
+
'2. After creating the project, update your symbols.json with the correct information:'
|
|
31
|
+
))
|
|
32
|
+
console.error(chalk.gray(` - Verify the app key: ${chalk.cyan(appKey)}`))
|
|
33
|
+
console.error(chalk.gray(' - Make sure you have the correct permissions'))
|
|
34
|
+
|
|
35
|
+
console.error(chalk.bold.yellow('\nThen try again:'))
|
|
36
|
+
console.error(chalk.cyan('$ smbls push'))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function showBuildErrorMessages(error) {
|
|
40
|
+
console.log(chalk.bold.red('\nBuild Failed ❌'))
|
|
41
|
+
console.log(chalk.white('\nError details:'))
|
|
42
|
+
console.log(chalk.yellow(error.message))
|
|
43
|
+
|
|
44
|
+
// Extract useful information from the error
|
|
45
|
+
const errorLocation = error.errors?.[0]?.location || {}
|
|
46
|
+
if (errorLocation.file) {
|
|
47
|
+
console.log(chalk.white('\nError location:'))
|
|
48
|
+
console.log(chalk.gray(`File: ${chalk.cyan(errorLocation.file)}`))
|
|
49
|
+
if (errorLocation.line) {
|
|
50
|
+
console.log(chalk.gray(`Line: ${chalk.cyan(errorLocation.line)}`))
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log(chalk.white('\nPossible solutions:'))
|
|
55
|
+
|
|
56
|
+
// Common build issues and their solutions
|
|
57
|
+
if (error.message.includes('instanceof')) {
|
|
58
|
+
console.log(chalk.gray('• Check if you are using browser-specific APIs in Node.js environment'))
|
|
59
|
+
console.log(chalk.gray(' Consider using conditional checks: typeof window !== "undefined"'))
|
|
60
|
+
}
|
|
61
|
+
if (error.message.includes('Could not resolve')) {
|
|
62
|
+
console.log(chalk.gray('• Verify all dependencies are installed: npm install'))
|
|
63
|
+
console.log(chalk.gray('• Check import/require paths are correct'))
|
|
64
|
+
}
|
|
65
|
+
if (error.message.includes('Unexpected token')) {
|
|
66
|
+
console.log(chalk.gray('• Ensure the code syntax is compatible with the target environment'))
|
|
67
|
+
console.log(chalk.gray('• Check for syntax errors in the indicated file'))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.log(chalk.white('\nNext steps:'))
|
|
71
|
+
console.log(chalk.gray('1. Fix the build errors in your local project'))
|
|
72
|
+
console.log(chalk.gray('2. Ensure all dependencies are properly installed'))
|
|
73
|
+
console.log(chalk.gray('3. Run the command again'))
|
|
74
|
+
|
|
75
|
+
console.log(chalk.gray('\nFor more help, please check the documentation or report this issue on GitHub'))
|
|
76
|
+
}
|