@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/push.js CHANGED
@@ -5,79 +5,58 @@ import chalk from 'chalk'
5
5
  import inquirer from 'inquirer'
6
6
  import { loadModule } from './require.js'
7
7
  import { program } from './program.js'
8
- import { CredentialManager } from './login.js'
8
+ import { CredentialManager } from '../helpers/credentialManager.js'
9
9
  import { buildDirectory } from '../helpers/fileUtils.js'
10
- import { showDiffPager } from '../helpers/diffUtils.js'
11
- import { normalizeKeys, generateChanges } from '../helpers/compareUtils.js'
12
- import {
13
- getServerProjectData,
14
- updateProjectOnServer
15
- } from '../helpers/apiUtils.js'
16
-
17
- const RC_PATH = process.cwd() + '/symbols.json'
18
- const CREATE_PROJECT_URL = 'https://symbols.app/create'
19
-
20
- function printProjectNotFoundGuidance (appKey) {
21
- console.error(chalk.bold.red('\nProject not found or access denied.'))
22
- console.error(chalk.bold.yellow('\nPossible reasons:'))
23
- console.error(chalk.gray('1. The project does not exist'))
24
- console.error(chalk.gray("2. You don't have access to this project"))
25
- console.error(chalk.gray('3. The app key in symbols.json might be incorrect'))
26
-
27
- console.error(chalk.bold.yellow('\nTo resolve this:'))
28
- console.error(
29
- chalk.white(
30
- `1. Visit ${chalk.cyan.underline(
31
- CREATE_PROJECT_URL
32
- )} to create a new project`
33
- )
34
- )
35
- console.error(
36
- chalk.white(
37
- '2. After creating the project, update your symbols.json with the correct information:'
38
- )
39
- )
40
- console.error(chalk.gray(` - Verify the app key: ${chalk.cyan(appKey)}`))
41
- console.error(chalk.gray(' - Make sure you have the correct permissions'))
10
+ import { normalizeKeys } from '../helpers/compareUtils.js'
11
+ import { generateDiffDisplay, showDiffPager } from '../helpers/diffUtils.js'
12
+ import { getCurrentProjectData, postProjectChanges } from '../helpers/apiUtils.js'
13
+ import { computeCoarseChanges, computeOrdersForTuples, preprocessChanges } from '../helpers/changesUtils.js'
14
+ import { showAuthRequiredMessages, showProjectNotFoundMessages, showBuildErrorMessages } from '../helpers/buildMessages.js'
15
+ import { loadSymbolsConfig } from '../helpers/symbolsConfig.js'
16
+ import { loadCliConfig, readLock, writeLock, updateLegacySymbolsJson } from '../helpers/config.js'
42
17
 
43
- console.error(chalk.bold.yellow('\nThen try again:'))
44
- console.error(chalk.cyan('$ smbls push'))
45
- }
46
18
 
47
- async function loadProjectConfiguration () {
19
+ async function buildLocalProject () {
48
20
  try {
49
- const config = await loadModule(RC_PATH)
50
- if (!config.key) {
51
- throw new Error('Missing app key in symbols.json')
52
- }
53
- return config
54
- } catch (e) {
55
- if (e.message.includes('Missing app key')) {
56
- console.error(chalk.bold.red('\nInvalid symbols.json configuration:'))
57
- console.error(chalk.white('The file must contain a valid app key.'))
58
- console.error(chalk.bold.yellow('\nExample symbols.json:'))
59
- console.error(
60
- chalk.cyan(JSON.stringify({ key: 'your.app.key' }, null, 2))
61
- )
62
- } else {
63
- console.error(
64
- chalk.bold.red('Please include symbols.json in your repository root')
65
- )
21
+ const distDir = path.join(process.cwd(), 'smbls')
22
+ const outputDirectory = path.join(distDir, 'dist')
23
+
24
+ await buildDirectory(distDir, outputDirectory)
25
+ const outputFile = path.join(outputDirectory, 'index.js')
26
+ return normalizeKeys(await loadModule(outputFile, { silent: false }))
27
+ } catch (error) {
28
+ // Enhance error with build context
29
+ error.buildContext = {
30
+ command: 'push',
31
+ workspace: process.cwd()
66
32
  }
67
- process.exit(1)
33
+ throw error
68
34
  }
69
35
  }
70
36
 
71
- async function buildLocalProject () {
72
- const distDir = path.join(process.cwd(), 'smbls')
73
- const outputDirectory = path.join(distDir, 'dist')
37
+ function getAt(obj, pathArr = []) {
38
+ try {
39
+ return pathArr.reduce((acc, k) => (acc == null ? undefined : acc[k]), obj)
40
+ } catch (_) {
41
+ return undefined
42
+ }
43
+ }
74
44
 
75
- await buildDirectory(distDir, outputDirectory)
76
- const outputFile = path.join(outputDirectory, 'index.js')
77
- return normalizeKeys(await loadModule(outputFile))
45
+ function buildDiffsFromChanges(changes, base, local) {
46
+ const diffs = []
47
+ for (const [op, path, value] of changes) {
48
+ const oldVal = getAt(base, path)
49
+ if (op === 'delete') {
50
+ diffs.push(generateDiffDisplay('delete', path, oldVal))
51
+ } else {
52
+ const newVal = value !== undefined ? value : getAt(local, path)
53
+ diffs.push(generateDiffDisplay('update', path, oldVal, newVal))
54
+ }
55
+ }
56
+ return diffs
78
57
  }
79
58
 
80
- async function confirmChanges (changes) {
59
+ async function confirmChanges (changes, base, local) {
81
60
  if (changes.length === 0) {
82
61
  console.log(chalk.bold.yellow('No changes detected'))
83
62
  return false
@@ -93,6 +72,19 @@ async function confirmChanges (changes) {
93
72
  console.log(chalk.gray(`- ${type}: ${chalk.cyan(count)} changes`))
94
73
  })
95
74
 
75
+ const { view } = await inquirer.prompt([
76
+ {
77
+ type: 'confirm',
78
+ name: 'view',
79
+ message: 'View full list of changes?',
80
+ default: false
81
+ }
82
+ ])
83
+ if (view) {
84
+ const diffs = buildDiffsFromChanges(changes, base, local)
85
+ await showDiffPager(diffs)
86
+ }
87
+
96
88
  const { proceed } = await inquirer.prompt([
97
89
  {
98
90
  type: 'confirm',
@@ -105,76 +97,123 @@ async function confirmChanges (changes) {
105
97
  return proceed
106
98
  }
107
99
 
108
- export async function pushProjectChanges () {
100
+ export async function pushProjectChanges(options) {
101
+ const { verbose, message, type = 'patch' } = options
109
102
  const credManager = new CredentialManager()
110
- const authToken = credManager.getAuthToken()
103
+ const authToken = credManager.ensureAuthToken()
111
104
 
112
105
  if (!authToken) {
113
- console.error(chalk.red('Please login first using: smbls login'))
106
+ showAuthRequiredMessages()
114
107
  process.exit(1)
115
108
  }
116
109
 
117
110
  try {
118
- const config = await loadProjectConfiguration()
119
- const { key: appKey } = config
111
+ const symbolsConfig = await loadSymbolsConfig()
112
+ const cliConfig = loadCliConfig()
113
+ const lock = readLock()
114
+ const appKey = cliConfig.projectKey || symbolsConfig.key
115
+ const branch = cliConfig.branch || symbolsConfig.branch || 'main'
120
116
 
121
117
  // Build and load local project
122
118
  console.log(chalk.dim('Building local project...'))
123
- const projectData = await buildLocalProject()
124
- console.log(chalk.gray('Local project built and loaded successfully'))
125
-
126
- // Get server data
119
+ let localProject
127
120
  try {
128
- console.log(chalk.dim('Fetching server data...'))
129
- const serverProjectData = await getServerProjectData(appKey, authToken)
130
-
131
- if (!serverProjectData) {
132
- throw new Error('Failed to fetch server data: Empty response')
133
- }
134
-
135
- const normalizedServerData = normalizeKeys(serverProjectData)
136
- console.log(chalk.gray('Server data fetched successfully'))
137
-
138
- // Compare and get changes
139
- const { changes, diffs } = generateChanges(
140
- normalizedServerData,
141
- projectData
142
- )
143
-
144
- // Show changes and confirm
145
- if (changes.length > 0) {
146
- console.log('\nDetailed changes:')
147
- await showDiffPager(diffs)
148
- }
149
-
150
- const shouldProceed = await confirmChanges(changes)
151
- if (!shouldProceed) {
152
- console.log(chalk.yellow('Operation cancelled'))
153
- return
154
- }
155
-
156
- // Update server
157
- console.log(chalk.dim('Updating server...'))
158
- await updateProjectOnServer(appKey, authToken, changes, projectData)
159
-
160
- console.log(chalk.bold.green('\nProject updated successfully!'))
161
- console.log(
162
- chalk.gray(`Total changes applied: ${chalk.cyan(changes.length)}`)
163
- )
164
- } catch (error) {
165
- if (
166
- error.message.includes('Failed to fetch server data') ||
167
- (error.response && error.response.status === 404) ||
168
- Object.keys(error.response?.data || {}).length === 0
169
- ) {
170
- printProjectNotFoundGuidance(appKey)
171
- } else {
172
- throw error // Re-throw other errors to be caught by outer catch block
173
- }
121
+ localProject = await buildLocalProject()
122
+ console.log(chalk.gray('Local project built successfully'))
123
+ } catch (buildError) {
124
+ showBuildErrorMessages(buildError)
125
+ process.exit(1)
126
+ }
127
+
128
+ // Get current server state (ETag aware)
129
+ console.log(chalk.dim('Fetching current server state...'))
130
+ const serverResp = await getCurrentProjectData(
131
+ { projectKey: appKey, projectId: lock.projectId },
132
+ authToken,
133
+ { branch, includePending: true, etag: lock.etag }
134
+ )
135
+ const serverProject = serverResp.notModified ? null : serverResp.data
136
+
137
+ // Check if server project is empty (not found or no access)
138
+ if (serverProject && Object.keys(serverProject).length === 0) {
139
+ showProjectNotFoundMessages(appKey)
140
+ process.exit(1)
141
+ }
142
+
143
+ console.log(chalk.gray('Server state fetched successfully'))
144
+
145
+ // Calculate coarse local changes vs server snapshot (or base)
146
+ const base = normalizeKeys(serverProject || {})
147
+ const changes = computeCoarseChanges(base, localProject)
148
+
149
+ if (!changes.length) {
150
+ console.log(chalk.bold.yellow('\nNo changes to push'))
151
+ return
152
+ }
153
+
154
+ // Show change summary
155
+ console.log('\nLocal changes to push:')
156
+ const byType = changes.reduce((acc, [t]) => ((acc[t] = (acc[t] || 0) + 1), acc), {})
157
+ Object.entries(byType).forEach(([t, c]) => {
158
+ console.log(chalk.gray(`- ${t}: ${chalk.cyan(c)} changes`))
159
+ })
160
+
161
+ // Confirm push
162
+ const shouldProceed = await confirmChanges(changes, base, localProject)
163
+ if (!shouldProceed) {
164
+ console.log(chalk.yellow('Push cancelled'))
165
+ return
166
+ }
167
+
168
+ // Push changes
169
+ console.log(chalk.dim('\nPushing changes...'))
170
+ const projectId = lock.projectId || serverProject?.projectInfo?.id
171
+ if (!projectId) {
172
+ console.log(chalk.red('Unable to resolve projectId. Please fetch first to initialize lock.'))
174
173
  process.exit(1)
175
174
  }
175
+ const operationId = `cli-${Date.now()}`
176
+ // Derive granular changes against server base and compute orders using local for pending children
177
+ const { granularChanges } = preprocessChanges(base, changes)
178
+ const orders = computeOrdersForTuples(localProject, granularChanges)
179
+ const result = await postProjectChanges(projectId, authToken, {
180
+ branch,
181
+ type,
182
+ operationId,
183
+ // Send both forms for compatibility with preprocessors
184
+ changes,
185
+ granularChanges,
186
+ orders
187
+ })
188
+ if (result.noOp) {
189
+ console.log(chalk.bold.yellow('\nNo-op on server'))
190
+ return
191
+ }
192
+ const { id: versionId, value: version } = result.data || {}
193
+
194
+ // Update symbols.json
195
+ updateLegacySymbolsJson({ ...(symbolsConfig || {}), version, versionId })
196
+
197
+ console.log(chalk.bold.green('\nChanges pushed successfully!'))
198
+ console.log(chalk.gray(`New version: ${chalk.cyan(version)}`))
199
+
200
+ // Refresh lock with latest ETag by fetching head
201
+ const latest = await getCurrentProjectData(
202
+ { projectKey: appKey, projectId },
203
+ authToken,
204
+ { branch, includePending: true }
205
+ )
206
+ writeLock({
207
+ etag: latest.etag || null,
208
+ version,
209
+ branch,
210
+ projectId,
211
+ pulledAt: new Date().toISOString()
212
+ })
213
+
176
214
  } catch (error) {
177
215
  console.error(chalk.bold.red('\nPush failed:'), chalk.white(error.message))
216
+ if (verbose) console.error(error.stack)
178
217
  process.exit(1)
179
218
  }
180
219
  }
@@ -182,4 +221,5 @@ export async function pushProjectChanges () {
182
221
  program
183
222
  .command('push')
184
223
  .description('Push changes to platform')
224
+ .option('-m, --message <message>', 'Specify a commit message')
185
225
  .action(pushProjectChanges)
package/bin/require.js CHANGED
@@ -2,12 +2,146 @@
2
2
 
3
3
  import fs from 'fs'
4
4
  import { createRequire } from 'module'
5
+ import path from 'path'
5
6
 
6
- class ImportError extends Error {} /* Bring in the ability to create the 'require' method */ // eslint-disable-line
7
- const require = createRequire(import.meta.url) // construct the require method
7
+ class ImportError extends Error {
8
+ constructor(message, path, error) {
9
+ super(message)
10
+ this.name = 'ImportError'
11
+ this.path = path
12
+ this.originalError = error
13
+ }
14
+ }
15
+
16
+ const require = createRequire(import.meta.url)
17
+
18
+ /**
19
+ * Loads a module or file content based on file type and options
20
+ * @param {string} modulePath - Path to the module/file to load
21
+ * @param {Object} options - Configuration options
22
+ * @param {boolean} options.json - Force JSON parsing
23
+ * @param {boolean} options.raw - Return raw file contents
24
+ * @param {string} options.encoding - File encoding (default: 'utf8')
25
+ * @param {boolean} options.silent - Don't throw errors, return null instead
26
+ * @param {boolean} options.noCache - Bust Node's require cache before loading
27
+ * @returns {Promise<any>} - Loaded module/file contents
28
+ */
29
+ export const loadModule = async (modulePath, options = {}) => {
30
+ const {
31
+ json = false,
32
+ raw = false,
33
+ encoding = 'utf8',
34
+ silent = false,
35
+ noCache = false
36
+ } = options
37
+
38
+ try {
39
+ if (!fs.existsSync(modulePath)) {
40
+ throw new ImportError(`File not found: ${modulePath}`, modulePath)
41
+ }
42
+
43
+ // Get file extension
44
+ const ext = path.extname(modulePath)
45
+
46
+ // Return raw file contents if requested
47
+ if (raw) {
48
+ return fs.readFileSync(modulePath, encoding)
49
+ }
50
+
51
+ // Handle JSON files or forced JSON parsing
52
+ if (ext === '.json' || json) {
53
+ const content = fs.readFileSync(modulePath, encoding)
54
+ try {
55
+ return JSON.parse(content)
56
+ } catch (e) {
57
+ throw new ImportError(
58
+ `Invalid JSON in ${modulePath}: ${e.message}`,
59
+ modulePath,
60
+ e
61
+ )
62
+ }
63
+ }
64
+
65
+ // Handle JavaScript/Node modules
66
+ try {
67
+ if (noCache) {
68
+ try {
69
+ const resolved = require.resolve(modulePath)
70
+ delete require.cache[resolved]
71
+ } catch (_) {}
72
+ }
73
+ const module = require(modulePath)
74
+ return module?.default || module
75
+ } catch (e) {
76
+ throw new ImportError(
77
+ `Error loading module ${modulePath}: ${e.message}`,
78
+ modulePath,
79
+ e
80
+ )
81
+ }
82
+ } catch (error) {
83
+ if (silent) return null
84
+ throw error
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Synchronously loads a module or file content
90
+ * @param {string} modulePath - Path to the module/file to load
91
+ * @param {Object} options - Configuration options (same as loadModule)
92
+ * @returns {any} - Loaded module/file contents
93
+ */
94
+ export const loadModuleSync = (modulePath, options = {}) => {
95
+ const {
96
+ json = false,
97
+ raw = false,
98
+ encoding = 'utf8',
99
+ silent = false,
100
+ noCache = false
101
+ } = options
102
+
103
+ try {
104
+ if (!fs.existsSync(modulePath)) {
105
+ throw new ImportError(`File not found: ${modulePath}`, modulePath)
106
+ }
107
+
108
+ const ext = path.extname(modulePath)
109
+
110
+ if (raw) {
111
+ return fs.readFileSync(modulePath, encoding)
112
+ }
113
+
114
+ if (ext === '.json' || json) {
115
+ const content = fs.readFileSync(modulePath, encoding)
116
+ try {
117
+ return JSON.parse(content)
118
+ } catch (e) {
119
+ throw new ImportError(
120
+ `Invalid JSON in ${modulePath}: ${e.message}`,
121
+ modulePath,
122
+ e
123
+ )
124
+ }
125
+ }
8
126
 
9
- export const loadModule = async (modulePath) => {
10
- if (fs.existsSync(modulePath)) {
11
- return require(modulePath)
12
- } else { return null }
127
+ try {
128
+ if (noCache) {
129
+ try {
130
+ const resolved = require.resolve(modulePath)
131
+ delete require.cache[resolved]
132
+ } catch (_) {}
133
+ }
134
+ const module = require(modulePath)
135
+ return module?.default || module
136
+ } catch (e) {
137
+ throw new ImportError(
138
+ `Error loading module ${modulePath}: ${e.message}`,
139
+ modulePath,
140
+ e
141
+ )
142
+ }
143
+ } catch (error) {
144
+ if (silent) return null
145
+ throw error
146
+ }
13
147
  }
@@ -2,21 +2,16 @@
2
2
 
3
3
  import { sync } from '@symbo.ls/socket'
4
4
  import { program } from './program.js'
5
- import { loadModule } from './require.js'
6
-
7
- const RC_PATH = process.cwd() + '/symbols.json'
8
- let rc = {}
9
- try {
10
- rc = loadModule(RC_PATH) // eslint-disable-line
11
- } catch (e) { console.error('Please include symbols.json to your root of respository') }
5
+ import { loadSymbolsConfig } from '../helpers/symbolsConfig.js'
12
6
 
13
7
  program
14
8
  .command('socket-server')
15
9
  .description('Realtime sync with Symbols')
16
10
  .option('-l, --live', 'Bypass the local build')
17
11
  .action(async (options) => {
18
- rc.then(data => {
19
- const opts = { ...data, ...options }
12
+ const symbolsConfig = await loadSymbolsConfig()
13
+ if (symbolsConfig) {
14
+ const opts = { ...symbolsConfig, ...options }
20
15
  sync(null, opts)
21
- })
16
+ }
22
17
  })