@symbo.ls/cli 2.33.12 → 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/fs.js CHANGED
@@ -18,24 +18,90 @@ const {
18
18
  } = utils.default || utils
19
19
 
20
20
  let singleFileKeys = ['designSystem', 'state', 'files', 'dependencies']
21
- const directoryKeys = [
22
- 'components',
23
- 'snippets',
24
- 'pages',
25
- 'functions',
26
- 'methods'
27
- ]
28
-
29
- const defaultExports = [
30
- 'pages',
31
- 'designSystem',
32
- 'state',
33
- 'files',
34
- 'dependencies',
35
- 'schema'
36
- ]
37
-
38
- export async function createFs(
21
+ const directoryKeys = ['components', 'snippets', 'pages', 'functions', 'methods']
22
+
23
+ const defaultExports = ['pages', 'designSystem', 'state', 'files', 'dependencies', 'schema']
24
+
25
+ // Minimal reserved identifier set to avoid invalid named exports like "export const default"
26
+ const RESERVED_IDENTIFIERS = new Set(['default'])
27
+ function isReservedIdentifier (name) {
28
+ return RESERVED_IDENTIFIERS.has(name)
29
+ }
30
+
31
+ // Keys that should never be materialized as files inside collection directories
32
+ const SKIP_ENTRY_KEYS = new Set(['__order', 'schema'])
33
+ function shouldSkipEntryKey (name) {
34
+ return SKIP_ENTRY_KEYS.has(name) || isReservedIdentifier(name)
35
+ }
36
+
37
+ function reorderWithOrderKeys (input) {
38
+ if (Array.isArray(input)) {
39
+ return input.map(reorderWithOrderKeys)
40
+ }
41
+ if (!input || typeof input !== 'object') return input
42
+ const hasOrder = Array.isArray(input.__order)
43
+ const originalKeys = Object.keys(input)
44
+ const orderedKeys = []
45
+ if (hasOrder) {
46
+ for (let i = 0; i < input.__order.length; i++) {
47
+ const k = input.__order[i]
48
+ if (k === '__order') continue
49
+ if (originalKeys.includes(k) && !orderedKeys.includes(k)) {
50
+ orderedKeys.push(k)
51
+ }
52
+ }
53
+ }
54
+ for (let i = 0; i < originalKeys.length; i++) {
55
+ const k = originalKeys[i]
56
+ if (k === '__order') continue
57
+ if (!orderedKeys.includes(k)) orderedKeys.push(k)
58
+ }
59
+ const out = {}
60
+ for (let i = 0; i < orderedKeys.length; i++) {
61
+ const k = orderedKeys[i]
62
+ out[k] = reorderWithOrderKeys(input[k])
63
+ }
64
+ if (hasOrder) {
65
+ out.__order = input.__order.slice()
66
+ } else if (Object.prototype.hasOwnProperty.call(input, '__order')) {
67
+ // Preserve explicit empty/non-array __order semantics at the end
68
+ out.__order = input.__order
69
+ }
70
+ return out
71
+ }
72
+
73
+ async function removeStaleFiles(body, targetDir) {
74
+ for (const key of directoryKeys) {
75
+ const dirPath = path.join(targetDir, key)
76
+ if (!fs.existsSync(dirPath)) continue
77
+
78
+ const existingFiles = await fs.promises.readdir(dirPath)
79
+ const currentEntries = body[key] ? Object.keys(body[key])
80
+ // Drop meta/reserved identifiers like "__order" and "default"
81
+ .filter(entry => !shouldSkipEntryKey(entry))
82
+ .map(entry => {
83
+ // Apply the same transformations as in createKeyDirectoryAndFiles
84
+ let fileName = entry
85
+ if (fileName.startsWith('/')) fileName = fileName.slice(1)
86
+ if (fileName === '') fileName = 'main'
87
+ if (fileName.includes('*')) fileName = 'fallback'
88
+ return `${fileName.replace('/', '-')}.js`
89
+ }) : []
90
+
91
+ // Don't remove index.js
92
+ const filesToCheck = existingFiles.filter(file => file !== 'index.js')
93
+
94
+ for (const file of filesToCheck) {
95
+ if (!currentEntries.includes(file)) {
96
+ const filePath = path.join(dirPath, file)
97
+ console.log(chalk.yellow(`Removing stale file: ${path.join(key, file)}`))
98
+ await fs.promises.unlink(filePath)
99
+ }
100
+ }
101
+ }
102
+ }
103
+
104
+ export async function createFs (
39
105
  body,
40
106
  distDir = path.join(process.cwd(), 'smbls'),
41
107
  opts = {}
@@ -84,54 +150,72 @@ export async function createFs(
84
150
 
85
151
  if (filesExist) {
86
152
  const cacheDir = path.join(distDir, '.cache')
87
- await fs.promises.mkdir(cacheDir, { recursive: true })
88
153
 
89
- const cachePromises = [
90
- ...directoryKeys.map((key) =>
91
- createKeyDirectoryAndFiles(key, body, cacheDir, true)
92
- ),
93
- ...singleFileKeys.map((key) => {
94
- if (body[key] && typeof body[key] === 'object') {
95
- return createSingleFileFolderAndFile(key, body[key], cacheDir, true)
96
- }
97
- return undefined
98
- })
99
- ]
154
+ try {
155
+ await fs.promises.mkdir(cacheDir, { recursive: true })
100
156
 
101
- await Promise.all(cachePromises)
102
- await generateIndexjsFile(
103
- joinArrays(directoryKeys, singleFileKeys),
104
- cacheDir,
105
- 'root'
106
- )
157
+ if (update) {
158
+ await removeStaleFiles(body, targetDir)
159
+ }
107
160
 
108
- const diffs = await findDiff(cacheDir, targetDir)
109
- if (diffs.length > 0) {
110
- console.log('Differences found:')
111
- diffs.forEach((diff) => {
112
- console.log(chalk.green(`File: ${diff.file}`))
113
- console.log(chalk.yellow('Diff:'))
114
- console.log(chalk.yellow(diff.diff))
115
- console.log('---')
116
- })
117
- if (!update) {
118
- const { consent } = await askForConsent()
119
- if (consent) {
161
+ const cachePromises = [
162
+ ...directoryKeys.map((key) =>
163
+ createKeyDirectoryAndFiles(key, body, cacheDir, true)
164
+ ),
165
+ ...singleFileKeys.map((key) => {
166
+ if (body[key] && typeof body[key] === 'object') {
167
+ return createSingleFileFolderAndFile(key, body[key], cacheDir, true)
168
+ }
169
+ return undefined
170
+ })
171
+ ]
172
+
173
+ await Promise.all(cachePromises)
174
+ await generateIndexjsFile(
175
+ joinArrays(directoryKeys, singleFileKeys),
176
+ cacheDir,
177
+ 'root'
178
+ )
179
+
180
+ const diffs = await findDiff(cacheDir, targetDir)
181
+ if (diffs.length > 0) {
182
+ console.log('Differences found:')
183
+ diffs.forEach((diff) => {
184
+ console.log(chalk.green(`File: ${diff.file}`))
185
+ console.log(chalk.yellow('Diff:'))
186
+ console.log(chalk.yellow(diff.diff))
187
+ console.log('---')
188
+ })
189
+ if (!update) {
190
+ const { consent } = await askForConsent()
191
+ if (consent) {
192
+ await overrideFiles(cacheDir, targetDir)
193
+ console.log('Files overridden successfully.')
194
+ } else {
195
+ console.log('Files not overridden.')
196
+ }
197
+ } else {
120
198
  await overrideFiles(cacheDir, targetDir)
121
199
  console.log('Files overridden successfully.')
122
- } else {
123
- console.log('Files not overridden.')
200
+ console.log()
201
+ console.log(chalk.dim('\n----------------\n'))
124
202
  }
125
203
  } else {
126
- await overrideFiles(cacheDir, targetDir)
127
- console.log('Files overridden successfully.')
204
+ console.log('No differences found.')
128
205
  console.log()
129
206
  console.log(chalk.dim('\n----------------\n'))
130
207
  }
131
- } else {
132
- console.log('No differences found.')
133
- console.log()
134
- console.log(chalk.dim('\n----------------\n'))
208
+
209
+ // Clean up cache directory
210
+ await fs.promises.rm(cacheDir, { recursive: true, force: true })
211
+ } catch (error) {
212
+ // Make sure we clean up even if there's an error
213
+ try {
214
+ await fs.promises.rm(cacheDir, { recursive: true, force: true })
215
+ } catch (cleanupError) {
216
+ // Ignore cleanup errors
217
+ }
218
+ throw error // Re-throw the original error
135
219
  }
136
220
  }
137
221
 
@@ -142,7 +226,10 @@ export async function createFs(
142
226
  const dirs = []
143
227
 
144
228
  if (body[key] && isObject(body[key])) {
145
- const promises = Object.entries(body[key]).map(
229
+ const promises = Object.entries(body[key])
230
+ // Skip meta/reserved identifier entries (e.g. "__order", "default")
231
+ .filter(([entryKey]) => !shouldSkipEntryKey(entryKey))
232
+ .map(
146
233
  async ([entryKey, value]) => {
147
234
  // if pages
148
235
  if (entryKey.startsWith('/')) entryKey = entryKey.slice(1)
@@ -165,7 +252,9 @@ export async function createFs(
165
252
  childKey.includes('-') || childKey.includes('/')
166
253
  ? removeChars(toCamelCase(childKey))
167
254
  : childKey
168
- const filePath = path.join(dirPath, `${childKey.replaceAll('/', '-')}.js`)
255
+ // Avoid reserved identifiers that break ESM syntax, e.g. "export const default"
256
+ const safeItemKey = isReservedIdentifier(itemKey) ? `_${itemKey}` : itemKey
257
+ const filePath = path.join(dirPath, `${childKey.replace('/', '-')}.js`)
169
258
 
170
259
  if (!update && fs.existsSync(filePath)) {
171
260
  return
@@ -174,13 +263,13 @@ export async function createFs(
174
263
  const itemKeyInvalid = itemKey.includes('.')
175
264
  const validKey = itemKeyInvalid
176
265
  ? `const ${removeChars(toTitleCase(itemKey))}`
177
- : `export const ${itemKey}`
266
+ : `export const ${safeItemKey}`
178
267
 
179
268
  let stringifiedContent
180
269
  if (isString(value)) {
181
270
  stringifiedContent = `${validKey} = ${value}`
182
271
  } else {
183
- const content = deepDestringifyFunctions(value)
272
+ const content = reorderWithOrderKeys(deepDestringifyFunctions(value))
184
273
  // console.log('ON DEEPDESTR:')
185
274
  // console.log(content.components.Configuration)
186
275
  stringifiedContent = `${validKey} = ${objectToString(content)};`
@@ -203,7 +292,7 @@ export { ${removeChars(toTitleCase(itemKey))} as '${itemKey}' }`
203
292
  }
204
293
 
205
294
  if (isString(data)) data = { default: data }
206
- const content = deepDestringifyFunctions(data)
295
+ const content = reorderWithOrderKeys(deepDestringifyFunctions(data))
207
296
  const stringifiedContent = `export default ${objectToString(content)};`
208
297
 
209
298
  await fs.promises.writeFile(filePath, stringifiedContent, 'utf8')
package/bin/index.js CHANGED
@@ -13,6 +13,7 @@ import './create.js'
13
13
  import './login.js'
14
14
  import './push.js'
15
15
  import './link-packages.js'
16
+ import './collab.js'
16
17
 
17
18
  const args = process.argv
18
19
  program.parse(args)
package/bin/install.js CHANGED
@@ -4,20 +4,10 @@ import chalk from 'chalk'
4
4
  import { loadModule } from './require.js'
5
5
  import { exec } from 'child_process'
6
6
  import { program } from './program.js'
7
-
7
+ import { loadSymbolsConfig } from '../helpers/symbolsConfig.js'
8
8
  const PACKAGE_PATH = process.cwd() + '/package.json'
9
- const RC_PATH = process.cwd() + '/symbols.json'
10
- const LOCAL_CONFIG_PATH = process.cwd() + '/node_modules/@symbo.ls/init/dynamic.json'
11
- const DEFAULT_REMOTE_CONFIG_PATH = 'https://api.symbols.app/' // eslint-disable-line
12
-
13
- const pkg = loadModule(PACKAGE_PATH)
14
- const rcFile = loadModule(RC_PATH) // eslint-disable-line
15
- const localConfig = loadModule(LOCAL_CONFIG_PATH) // eslint-disable-line
16
9
 
17
- let rc = {}
18
- try {
19
- rc = loadModule(RC_PATH) // eslint-disable-line
20
- } catch (e) { console.error('Please include symbols.json to your root of respository') }
10
+ const pkg = await loadModule(PACKAGE_PATH, { json: true, silent: true })
21
11
 
22
12
  const makeCommand = (packageManager, packageName) => {
23
13
  return packageManager === 'yarn'
@@ -28,13 +18,10 @@ const makeCommand = (packageManager, packageName) => {
28
18
  }
29
19
 
30
20
  export const installFromCli = async (options) => {
31
- if (!rcFile || !localConfig) {
32
- console.error('symbols.json not found in the root of the repository')
33
- return
34
- }
21
+ const symbolsConfig = await loadSymbolsConfig()
35
22
 
36
- const framework = rcFile.framework || options.framework
37
- const packageManager = rcFile.packageManager || options.packageManager
23
+ const framework = symbolsConfig.framework || options.framework
24
+ const packageManager = symbolsConfig.packageManager || options.packageManager
38
25
 
39
26
  // const packageName = `@symbo.ls/${mode || 'uikit'}`
40
27
  const packageName = framework === 'react' ? '@symbo.ls/react' : 'smbls'
@@ -63,7 +50,7 @@ export const installFromCli = async (options) => {
63
50
  }
64
51
 
65
52
  program
66
- .version(pkg.version ?? 'unknown')
53
+ .version(pkg && pkg.version ? pkg.version : 'unknown')
67
54
 
68
55
  program
69
56
  .command('install')
package/bin/login.js CHANGED
@@ -1,56 +1,25 @@
1
1
  'use strict'
2
2
 
3
3
  import inquirer from 'inquirer'
4
- import fs from 'fs'
5
- import path from 'path'
6
- import os from 'os'
7
4
  import chalk from 'chalk'
8
5
  import { program } from './program.js'
9
- import { getApiUrl } from '../helpers/config.js'
6
+ import { getApiUrl, saveCliConfig, loadCliConfig } from '../helpers/config.js'
7
+ import { CredentialManager } from '../helpers/credentialManager.js'
10
8
 
11
- const RC_FILE = '.smblsrc'
12
-
13
- // Helper to manage credentials
14
- export class CredentialManager {
15
- constructor () {
16
- this.rcPath = path.join(os.homedir(), RC_FILE)
17
- }
18
-
19
- // Load credentials from rc file
20
- loadCredentials () {
21
- try {
22
- const data = fs.readFileSync(this.rcPath, 'utf8')
23
- return JSON.parse(data)
24
- } catch (err) {
25
- return {}
26
- }
27
- }
28
-
29
- // Save credentials to rc file
30
- saveCredentials (credentials) {
31
- try {
32
- fs.writeFileSync(this.rcPath, JSON.stringify(credentials, null, 2))
33
- return true
34
- } catch (err) {
35
- console.error('Failed to save credentials:', err)
36
- return false
37
- }
38
- }
39
-
40
- // Get stored auth token
41
- getAuthToken () {
42
- const creds = this.loadCredentials()
43
- return creds.authToken
44
- }
45
-
46
- // Clear stored credentials
47
- clearCredentials () {
48
- try {
49
- fs.unlinkSync(this.rcPath)
50
- return true
51
- } catch (err) {
52
- return false
53
- }
9
+ function websiteFromApi(apiBaseUrl) {
10
+ try {
11
+ const u = new URL(apiBaseUrl)
12
+ const host = u.host
13
+ if (apiBaseUrl.startsWith('http://localhost')) return 'http://localhost:1024'
14
+ if (host === 'api.dev.symbols.app') return 'https://dev.symbols.app'
15
+ if (host === 'api.staging.symbols.app') return 'https://staging.symbols.app'
16
+ if (host === 'api.test.symbols.app') return 'https://test.symbols.app'
17
+ if (host === 'api.symbols.app') return 'https://symbols.app'
18
+ // Fallback: strip leading api.
19
+ if (host.startsWith('api.')) return `https://${host.replace(/^api\./, '')}`
20
+ return `${u.protocol}//${host}`
21
+ } catch (_) {
22
+ return 'https://symbols.app'
54
23
  }
55
24
  }
56
25
 
@@ -58,29 +27,38 @@ program
58
27
  .command('login')
59
28
  .description('Sign in to Symbols')
60
29
  .action(async () => {
61
- console.log('yo login')
62
-
63
- console.log(getApiUrl())
30
+ console.log(chalk.cyan('\nšŸ”‘ Welcome to Symbols CLI'))
31
+ console.log(chalk.white('\nPlease sign in with your Symbols account:'))
32
+ console.log(chalk.dim('Don\'t have an account? Visit https://symbols.app/signup\n'))
64
33
 
65
34
  // Prompt for credentials
35
+ const currentConfig = loadCliConfig()
66
36
  const answers = await inquirer.prompt([
37
+ {
38
+ type: 'input',
39
+ name: 'apiBaseUrl',
40
+ message: 'API Base URL:',
41
+ default: currentConfig.apiBaseUrl || getApiUrl(),
42
+ validate: input => /^https?:\/\//.test(input) || 'āŒ Please enter a valid URL'
43
+ },
67
44
  {
68
45
  type: 'input',
69
46
  name: 'email',
70
- message: 'Enter your email:',
71
- validate: input => input.includes('@') || 'Please enter a valid email'
47
+ message: 'Email:',
48
+ validate: input => input.includes('@') || 'āŒ Please enter a valid email address'
72
49
  },
73
50
  {
74
51
  type: 'password',
75
52
  name: 'password',
76
- message: 'Enter your password:',
77
- validate: input => input.length >= 6 || 'Password must be at least 6 characters'
53
+ message: 'Password:',
54
+ validate: input => input.length >= 6 || 'āŒ Password must be at least 6 characters'
78
55
  }
79
56
  ])
80
57
 
81
58
  try {
82
59
  // Make login request
83
- const response = await fetch(`${getApiUrl()}/auth/login`, {
60
+ console.log(chalk.dim('\nAuthenticating...'))
61
+ const response = await fetch(`${answers.apiBaseUrl}/core/auth/login`, {
84
62
  method: 'POST',
85
63
  headers: {
86
64
  'Content-Type': 'application/json'
@@ -94,21 +72,63 @@ program
94
72
  const data = await response.json()
95
73
 
96
74
  if (!response.ok) {
97
- throw new Error(data.error || 'Authentication failed')
75
+ const msg = data?.message || data?.error || `Authentication failed (${response.status})`
76
+ const err = new Error(msg)
77
+ err.response = { status: response.status, data }
78
+ throw err
98
79
  }
99
80
 
81
+ // Extract token from various possible shapes
82
+ const user = data?.data?.user || data?.user
83
+ const tokens = data?.data?.tokens || data?.tokens || {}
84
+ const token =
85
+ tokens?.accessToken ||
86
+ data?.token ||
87
+ data?.accessToken ||
88
+ data?.jwt ||
89
+ data?.data?.token ||
90
+ data?.data?.accessToken ||
91
+ data?.data?.jwt
92
+ if (!token) {
93
+ throw new Error('Login succeeded but no token was returned by the server')
94
+ }
95
+ const refreshToken = tokens?.refreshToken || null
96
+ const accessTokenExp = tokens?.accessTokenExp?.expiresAt || null
97
+
100
98
  // Save credentials
101
99
  const credManager = new CredentialManager()
102
100
  credManager.saveCredentials({
103
- authToken: data.token,
104
- userId: data.userId,
105
- email: answers.email
101
+ authToken: token,
102
+ refreshToken,
103
+ authTokenExpiresAt: accessTokenExp,
104
+ userId: user?.id || data?.userId,
105
+ email: user?.email || answers.email
106
106
  })
107
107
 
108
- console.log(chalk.green('\nSuccessfully logged in!'))
109
- console.log(chalk.dim(`Credentials saved to ${credManager.rcPath}`))
108
+ // Persist API base URL to local config
109
+ saveCliConfig({ apiBaseUrl: answers.apiBaseUrl })
110
+
111
+ console.log(chalk.green('\n✨ Successfully logged in!'))
112
+ console.log(chalk.white('\nYou can now use Symbols CLI commands:'))
113
+ console.log(chalk.cyan(' smbls fetch ') + chalk.dim('Fetch your design system'))
114
+ console.log(chalk.cyan(' smbls sync ') + chalk.dim('Sync local changes'))
115
+ console.log(chalk.cyan(' smbls push ') + chalk.dim('Push updates to Symbols'))
116
+ console.log(chalk.cyan(' smbls collab ') + chalk.dim('Connect to team on platform in realtime'))
117
+
118
+ console.log(chalk.dim(`\nCredentials saved to ${credManager.rcPath}`))
119
+ console.log(chalk.dim('For more commands, run: smbls --help\n'))
120
+
110
121
  } catch (error) {
111
- console.error(chalk.red('\nLogin failed:'), error.message)
122
+ const website = websiteFromApi(answers.apiBaseUrl)
123
+ console.log(chalk.red('\nāŒ Login failed'))
124
+ console.log(chalk.white('\nError:'))
125
+ console.log(chalk.yellow(error.message || 'Unknown error'))
126
+
127
+ console.log(chalk.white('\nNeed help?'))
128
+ console.log(chalk.dim(`• Reset password: ${website}/reset-password`))
129
+ console.log(chalk.dim('• Contact support: support@symbols.app'))
130
+ console.log(chalk.dim(`• Documentation: ${website}/docs/cli\n`))
131
+
112
132
  process.exit(1)
113
133
  }
114
134
  })