@bagelink/workspace 1.7.3 → 1.7.8

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bagelink/workspace",
3
3
  "type": "module",
4
- "version": "1.7.3",
4
+ "version": "1.7.8",
5
5
  "description": "Monorepo workspace tooling for Bagel projects with proxy and config management",
6
6
  "author": {
7
7
  "name": "Bagel Studio",
@@ -52,16 +52,27 @@
52
52
  "dependencies": {
53
53
  "prompts": "^2.4.2"
54
54
  },
55
+ "peerDependencies": {
56
+ "@bagelink/lint-config": ">=1.0.0",
57
+ "@bagelink/sdk": ">=1.0.0",
58
+ "vite": ">=5.0.0"
59
+ },
60
+ "peerDependenciesMeta": {
61
+ "@bagelink/lint-config": {
62
+ "optional": true
63
+ },
64
+ "@bagelink/sdk": {
65
+ "optional": true
66
+ }
67
+ },
55
68
  "devDependencies": {
69
+ "@types/bun": "^1.1.16",
56
70
  "@types/node": "^24.0.0",
57
71
  "@types/prompts": "^2.4.9",
58
72
  "rimraf": "^6.0.1",
59
73
  "typescript": "^5.8.3",
60
74
  "unbuild": "^3.5.0"
61
75
  },
62
- "peerDependencies": {
63
- "vite": ">=5.0.0"
64
- },
65
76
  "scripts": {
66
77
  "dev": "unbuild --stub",
67
78
  "build": "unbuild",
package/src/detect.ts ADDED
@@ -0,0 +1,90 @@
1
+ import { existsSync, readdirSync, readFileSync } from 'node:fs'
2
+ import { resolve } from 'node:path'
3
+ import process from 'node:process'
4
+
5
+ /**
6
+ * Detect if current directory is a workspace root
7
+ */
8
+ export function isWorkspace(root: string = process.cwd()): boolean {
9
+ // Check if package.json has workspaces field
10
+ const packageJsonPath = resolve(root, 'package.json')
11
+ if (existsSync(packageJsonPath)) {
12
+ try {
13
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'))
14
+ if (packageJson.workspaces !== undefined) {
15
+ return true
16
+ }
17
+ }
18
+ catch {
19
+ // Ignore parse errors
20
+ }
21
+ }
22
+
23
+ // Check if there are multiple project directories
24
+ // (directories with their own package.json, excluding shared, node_modules, etc.)
25
+ try {
26
+ const items = readdirSync(root, { withFileTypes: true })
27
+ const projectDirs = items.filter(
28
+ item => item.isDirectory()
29
+ && item.name !== 'node_modules'
30
+ && item.name !== 'shared'
31
+ && item.name !== '.git'
32
+ && !item.name.startsWith('.')
33
+ && existsSync(resolve(root, item.name, 'package.json')),
34
+ )
35
+
36
+ // If we have 2+ project directories, it's likely a workspace
37
+ return projectDirs.length >= 2
38
+ }
39
+ catch {
40
+ return false
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Get workspace info
46
+ */
47
+ export function getWorkspaceInfo(root: string = process.cwd()): {
48
+ isWorkspace: boolean
49
+ projects: string[]
50
+ hasShared: boolean
51
+ } {
52
+ const workspace = isWorkspace(root)
53
+
54
+ if (!workspace) {
55
+ return {
56
+ isWorkspace: false,
57
+ projects: [],
58
+ hasShared: false,
59
+ }
60
+ }
61
+
62
+ try {
63
+ const items = readdirSync(root, { withFileTypes: true })
64
+ const projects = items
65
+ .filter(
66
+ item => item.isDirectory()
67
+ && item.name !== 'node_modules'
68
+ && item.name !== 'shared'
69
+ && item.name !== '.git'
70
+ && !item.name.startsWith('.')
71
+ && existsSync(resolve(root, item.name, 'package.json')),
72
+ )
73
+ .map(item => item.name)
74
+
75
+ const hasShared = existsSync(resolve(root, 'shared'))
76
+
77
+ return {
78
+ isWorkspace: true,
79
+ projects,
80
+ hasShared,
81
+ }
82
+ }
83
+ catch {
84
+ return {
85
+ isWorkspace: false,
86
+ projects: [],
87
+ hasShared: false,
88
+ }
89
+ }
90
+ }
package/src/index.ts CHANGED
@@ -34,6 +34,11 @@ export {
34
34
  writeNetlifyConfig,
35
35
  }
36
36
 
37
+ export { setupLint } from './lint'
38
+ export { generateSDK, generateSDKForWorkspace } from './sdk'
39
+ export { addProject, initWorkspace, listProjects } from './workspace'
40
+ export { getWorkspaceInfo, isWorkspace } from './detect'
41
+
37
42
  /**
38
43
  * Define workspace configuration
39
44
  * Simple helper to get config from a config map
package/src/init.ts CHANGED
@@ -1,7 +1,9 @@
1
+ import type { WorkspaceConfig } from './types'
1
2
  import { existsSync, readFileSync, writeFileSync } from 'node:fs'
2
3
  import { resolve } from 'node:path'
3
4
  import process from 'node:process'
4
5
  import prompts from 'prompts'
6
+ import { writeNetlifyConfig } from './netlify'
5
7
 
6
8
  /**
7
9
  * Generate bgl.config.ts file interactively
@@ -75,12 +77,12 @@ export default defineWorkspace(configs)
75
77
  console.log(` Production host: ${productionHost}`)
76
78
  console.log(` Local dev host: http://localhost:8000\n`)
77
79
 
78
- // Ask if they want to update package.json and vite.config
80
+ // Ask if they want to update package.json, vite.config, and netlify.toml
79
81
  const setupResponse = await prompts([
80
82
  {
81
83
  type: 'confirm',
82
84
  name: 'updatePackageJson',
83
- message: 'Add dev scripts to package.json?',
85
+ message: 'Add/update dev scripts in package.json?',
84
86
  initial: true,
85
87
  },
86
88
  {
@@ -89,6 +91,12 @@ export default defineWorkspace(configs)
89
91
  message: 'Create/update vite.config.ts?',
90
92
  initial: true,
91
93
  },
94
+ {
95
+ type: 'confirm',
96
+ name: 'generateNetlify',
97
+ message: 'Generate netlify.toml for deployment?',
98
+ initial: true,
99
+ },
92
100
  ])
93
101
 
94
102
  if (setupResponse.updatePackageJson) {
@@ -99,6 +107,14 @@ export default defineWorkspace(configs)
99
107
  updateViteConfig(root)
100
108
  }
101
109
 
110
+ if (setupResponse.generateNetlify) {
111
+ const prodConfig: WorkspaceConfig = {
112
+ host: productionHost,
113
+ proxy: '/api',
114
+ }
115
+ writeNetlifyConfig(prodConfig, resolve(root, 'netlify.toml'))
116
+ }
117
+
102
118
  console.log('\nšŸ’” You can edit these files to customize your configuration.\n')
103
119
  }
104
120
 
@@ -160,7 +176,7 @@ function updatePackageJsonScripts(root: string): void {
160
176
  packageJson.scripts = {}
161
177
  }
162
178
 
163
- // Add scripts if they don't exist
179
+ // Add/overwrite dev scripts
164
180
  const scriptsToAdd = {
165
181
  'dev': 'vite',
166
182
  'dev:local': 'vite --mode localhost',
@@ -168,20 +184,31 @@ function updatePackageJsonScripts(root: string): void {
168
184
  'preview': 'vite preview',
169
185
  }
170
186
 
171
- let added = false
187
+ // Always overwrite dev and dev:local, preserve others if they exist
188
+ let modified = false
172
189
  for (const [key, value] of Object.entries(scriptsToAdd)) {
173
- if (!packageJson.scripts[key]) {
174
- packageJson.scripts[key] = value
175
- added = true
190
+ if (key === 'dev' || key === 'dev:local') {
191
+ // Always overwrite these
192
+ if (packageJson.scripts[key] !== value) {
193
+ packageJson.scripts[key] = value
194
+ modified = true
195
+ }
196
+ }
197
+ else {
198
+ // Only add if doesn't exist
199
+ if (!packageJson.scripts[key]) {
200
+ packageJson.scripts[key] = value
201
+ modified = true
202
+ }
176
203
  }
177
204
  }
178
205
 
179
- if (added) {
206
+ if (modified) {
180
207
  writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, 'utf-8')
181
208
  console.log('āœ… Updated package.json with dev scripts')
182
209
  }
183
210
  else {
184
- console.log('ā„¹ļø Scripts already exist in package.json')
211
+ console.log('ā„¹ļø Scripts already up to date in package.json')
185
212
  }
186
213
  }
187
214
  catch (error) {
package/src/lint.ts ADDED
@@ -0,0 +1,235 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs'
2
+ import { resolve } from 'node:path'
3
+ import process from 'node:process'
4
+ import prompts from 'prompts'
5
+
6
+ /**
7
+ * Set up linting in a project
8
+ */
9
+ export async function setupLint(
10
+ root: string = process.cwd(),
11
+ isWorkspace: boolean = false,
12
+ ): Promise<void> {
13
+ console.log('\nšŸ” Setting up linting...\n')
14
+
15
+ const response = await prompts([
16
+ {
17
+ type: 'multiselect',
18
+ name: 'configs',
19
+ message: 'Select configurations to set up:',
20
+ choices: [
21
+ { title: 'ESLint', value: 'eslint', selected: true },
22
+ { title: 'Prettier', value: 'prettier', selected: true },
23
+ { title: 'EditorConfig', value: 'editorconfig', selected: true },
24
+ { title: 'Git Hooks', value: 'githooks', selected: false },
25
+ ],
26
+ },
27
+ {
28
+ type: 'confirm',
29
+ name: 'installDeps',
30
+ message: 'Install dependencies?',
31
+ initial: true,
32
+ },
33
+ ])
34
+
35
+ if (!response || !response.configs) {
36
+ console.log('\nāŒ Setup cancelled.\n')
37
+ process.exit(1)
38
+ }
39
+
40
+ const { configs, installDeps } = response
41
+
42
+ // Create config files
43
+ if (configs.includes('eslint')) {
44
+ createEslintConfig(root, isWorkspace)
45
+ }
46
+
47
+ if (configs.includes('prettier')) {
48
+ createPrettierConfig(root)
49
+ }
50
+
51
+ if (configs.includes('editorconfig')) {
52
+ createEditorConfig(root)
53
+ }
54
+
55
+ if (configs.includes('githooks')) {
56
+ createGitHooks(root)
57
+ }
58
+
59
+ // Update package.json
60
+ updatePackageJsonLint(root, configs)
61
+
62
+ if (installDeps) {
63
+ console.log('\nšŸ“¦ Installing dependencies...')
64
+ console.log('Run: bun add -D @bagelink/lint-config eslint prettier typescript')
65
+ }
66
+
67
+ console.log('\nāœ… Linting setup complete!')
68
+ console.log('\nAvailable commands:')
69
+ console.log(' bun run lint - Run linter')
70
+ console.log(' bun run lint:fix - Fix linting issues')
71
+ console.log(' bun run format - Format code with Prettier')
72
+ console.log('')
73
+ }
74
+
75
+ /**
76
+ * Create ESLint config
77
+ */
78
+ function createEslintConfig(root: string, isWorkspace: boolean): void {
79
+ const configPath = resolve(root, 'eslint.config.js')
80
+
81
+ const config = isWorkspace
82
+ ? `import { defineConfig } from '@bagelink/lint-config/eslint'
83
+
84
+ export default defineConfig({
85
+ // Workspace-level ESLint config
86
+ ignores: ['**/dist/**', '**/node_modules/**', '**/.bun-cache/**'],
87
+ })
88
+ `
89
+ : `import vue3Config from '@bagelink/lint-config/eslint/vue3'
90
+
91
+ export default vue3Config
92
+ `
93
+
94
+ writeFileSync(configPath, config)
95
+ console.log('āœ… Created eslint.config.js')
96
+ }
97
+
98
+ /**
99
+ * Create Prettier config
100
+ */
101
+ function createPrettierConfig(root: string): void {
102
+ const configPath = resolve(root, '.prettierrc')
103
+
104
+ const config = {
105
+ semi: false,
106
+ singleQuote: true,
107
+ tabWidth: 2,
108
+ useTabs: true,
109
+ trailingComma: 'all',
110
+ printWidth: 100,
111
+ arrowParens: 'avoid',
112
+ }
113
+
114
+ writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`)
115
+ console.log('āœ… Created .prettierrc')
116
+
117
+ // .prettierignore
118
+ const ignorePath = resolve(root, '.prettierignore')
119
+ const ignore = `dist
120
+ node_modules
121
+ .bun-cache
122
+ *.min.js
123
+ *.min.css
124
+ `
125
+
126
+ writeFileSync(ignorePath, ignore)
127
+ console.log('āœ… Created .prettierignore')
128
+ }
129
+
130
+ /**
131
+ * Create EditorConfig
132
+ */
133
+ function createEditorConfig(root: string): void {
134
+ const configPath = resolve(root, '.editorconfig')
135
+
136
+ const config = `root = true
137
+
138
+ [*]
139
+ charset = utf-8
140
+ indent_style = tab
141
+ indent_size = 2
142
+ end_of_line = lf
143
+ insert_final_newline = true
144
+ trim_trailing_whitespace = true
145
+
146
+ [*.md]
147
+ trim_trailing_whitespace = false
148
+
149
+ [*.{json,yml,yaml}]
150
+ indent_style = space
151
+ indent_size = 2
152
+ `
153
+
154
+ writeFileSync(configPath, config)
155
+ console.log('āœ… Created .editorconfig')
156
+ }
157
+
158
+ /**
159
+ * Create Git Hooks
160
+ */
161
+ function createGitHooks(root: string): void {
162
+ const packageJsonPath = resolve(root, 'package.json')
163
+
164
+ if (!existsSync(packageJsonPath)) {
165
+ console.warn('āš ļø No package.json found, skipping git hooks')
166
+ return
167
+ }
168
+
169
+ // .lintstagedrc
170
+ const lintStagedConfig = {
171
+ '*.{js,jsx,ts,tsx,vue}': ['eslint --fix'],
172
+ '*.{json,md,yml,yaml}': ['prettier --write'],
173
+ }
174
+
175
+ writeFileSync(
176
+ resolve(root, '.lintstagedrc'),
177
+ `${JSON.stringify(lintStagedConfig, null, 2)}\n`,
178
+ )
179
+
180
+ console.log('āœ… Created .lintstagedrc')
181
+ console.log('ā„¹ļø Add simple-git-hooks and lint-staged to devDependencies')
182
+ console.log(' Then run: npx simple-git-hooks')
183
+ }
184
+
185
+ /**
186
+ * Update package.json with lint scripts
187
+ */
188
+ function updatePackageJsonLint(root: string, configs: string[]): void {
189
+ const packageJsonPath = resolve(root, 'package.json')
190
+
191
+ if (!existsSync(packageJsonPath)) {
192
+ console.warn('āš ļø No package.json found')
193
+ return
194
+ }
195
+
196
+ try {
197
+ const packageJson = JSON.parse(
198
+ readFileSync(packageJsonPath, 'utf-8'),
199
+ )
200
+
201
+ if (!packageJson.scripts) {
202
+ packageJson.scripts = {}
203
+ }
204
+
205
+ // Add lint scripts
206
+ if (configs.includes('eslint')) {
207
+ if (!packageJson.scripts.lint) {
208
+ packageJson.scripts.lint = 'eslint .'
209
+ }
210
+ if (!packageJson.scripts['lint:fix']) {
211
+ packageJson.scripts['lint:fix'] = 'eslint . --fix'
212
+ }
213
+ }
214
+
215
+ // Add format scripts
216
+ if (configs.includes('prettier')) {
217
+ if (!packageJson.scripts.format) {
218
+ packageJson.scripts.format = 'prettier --write .'
219
+ }
220
+ if (!packageJson.scripts['format:check']) {
221
+ packageJson.scripts['format:check'] = 'prettier --check .'
222
+ }
223
+ }
224
+
225
+ writeFileSync(
226
+ packageJsonPath,
227
+ `${JSON.stringify(packageJson, null, 2)}\n`,
228
+ )
229
+
230
+ console.log('āœ… Updated package.json with lint scripts')
231
+ }
232
+ catch (error) {
233
+ console.error('āŒ Failed to update package.json:', error)
234
+ }
235
+ }
package/src/sdk.ts ADDED
@@ -0,0 +1,175 @@
1
+ import type { WorkspaceConfig } from './types'
2
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
3
+ import { resolve } from 'node:path'
4
+ import process from 'node:process'
5
+ import prompts from 'prompts'
6
+
7
+ /**
8
+ * Generate SDK from OpenAPI spec
9
+ */
10
+ export async function generateSDK(
11
+ root: string = process.cwd(),
12
+ ): Promise<void> {
13
+ console.log('\nšŸ”§ Generating SDK from OpenAPI...\n')
14
+
15
+ // Try to load config
16
+ let config: WorkspaceConfig | null = null
17
+ let openApiUrl: string | undefined
18
+
19
+ try {
20
+ const configPath = resolve(root, 'bgl.config.ts')
21
+ if (existsSync(configPath)) {
22
+ const module = await import(`file://${configPath}`)
23
+ const workspace = module.default
24
+ if (typeof workspace === 'function') {
25
+ config = workspace('development')
26
+ if (config?.openapi_url) {
27
+ openApiUrl = config.openapi_url
28
+ }
29
+ }
30
+ }
31
+ }
32
+ catch {
33
+ // Ignore config load errors
34
+ }
35
+
36
+ // Prompt for missing info
37
+ const response = await prompts([
38
+ {
39
+ type: openApiUrl !== undefined ? null : 'text',
40
+ name: 'openApiUrl',
41
+ message: 'OpenAPI spec URL:',
42
+ initial: openApiUrl ?? 'http://localhost:8000/openapi.json',
43
+ },
44
+ {
45
+ type: 'text',
46
+ name: 'outputDir',
47
+ message: 'Output directory:',
48
+ initial: './src/api',
49
+ },
50
+ {
51
+ type: 'confirm',
52
+ name: 'splitFiles',
53
+ message: 'Split into organized files?',
54
+ initial: true,
55
+ },
56
+ ])
57
+
58
+ if (!response) {
59
+ console.log('\nāŒ SDK generation cancelled.\n')
60
+ process.exit(1)
61
+ }
62
+
63
+ const finalUrl = openApiUrl ?? response.openApiUrl
64
+ const { outputDir, splitFiles } = response
65
+
66
+ console.log(`\nšŸ“” Fetching OpenAPI spec from: ${finalUrl}`)
67
+ console.log(`šŸ“ Output directory: ${outputDir}\n`)
68
+
69
+ try {
70
+ // Dynamic import of @bagelink/sdk
71
+ const { openAPI } = await import('@bagelink/sdk')
72
+
73
+ const { types, code } = await openAPI(finalUrl, '/api')
74
+
75
+ const outputPath = resolve(root, outputDir)
76
+ if (!existsSync(outputPath)) {
77
+ mkdirSync(outputPath, { recursive: true })
78
+ }
79
+
80
+ // Write types
81
+ const typesPath = resolve(outputPath, 'types.d.ts')
82
+ writeFileSync(typesPath, types)
83
+ console.log('āœ… Generated types.d.ts')
84
+
85
+ // Write API client
86
+ const apiPath = resolve(outputPath, 'api.ts')
87
+ writeFileSync(apiPath, code)
88
+ console.log('āœ… Generated api.ts')
89
+
90
+ // Write index
91
+ const indexPath = resolve(outputPath, 'index.ts')
92
+ writeFileSync(
93
+ indexPath,
94
+ 'export * from \'./api\'\nexport * from \'./types.d\'\n',
95
+ )
96
+ console.log('āœ… Generated index.ts')
97
+
98
+ if (splitFiles) {
99
+ console.log('\nšŸ”€ Splitting into organized files...')
100
+ console.log('ā„¹ļø File splitting requires @bagelink/sdk bin scripts')
101
+ console.log(' Keeping monolithic structure for now')
102
+ // Note: File splitting requires unpublished bin scripts
103
+ // Users can manually run: bunx bagelink generate
104
+ }
105
+
106
+ console.log('\nāœ… SDK generated successfully!')
107
+ console.log(`\nImport it in your code:`)
108
+ console.log(` import { api } from '${outputDir.replace('./src/', './')}'`)
109
+ console.log('')
110
+ }
111
+ catch (error: unknown) {
112
+ console.error('\nāŒ Failed to generate SDK:')
113
+ if (error instanceof Error) {
114
+ console.error(error.message)
115
+ }
116
+ else {
117
+ console.error(error)
118
+ }
119
+ console.log('\nMake sure:')
120
+ console.log(' 1. @bagelink/sdk is installed: bun add -D @bagelink/sdk')
121
+ console.log(' 2. OpenAPI URL is accessible')
122
+ console.log(' 3. API server is running (if using localhost)')
123
+ process.exit(1)
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Generate SDK for all projects in workspace
129
+ */
130
+ export async function generateSDKForWorkspace(root: string = process.cwd()): Promise<void> {
131
+ console.log('\nšŸ¢ Generating SDK for workspace projects...\n')
132
+
133
+ // Find all projects
134
+ const fs = await import('node:fs')
135
+ const items = fs.readdirSync(root, { withFileTypes: true })
136
+ const projects = items
137
+ .filter(
138
+ item => item.isDirectory()
139
+ && item.name !== 'node_modules'
140
+ && item.name !== 'shared'
141
+ && item.name !== '.git'
142
+ && !item.name.startsWith('.'),
143
+ )
144
+ .map(item => item.name)
145
+
146
+ if (projects.length === 0) {
147
+ console.log('No projects found in workspace')
148
+ return
149
+ }
150
+
151
+ const response = await prompts({
152
+ type: 'multiselect',
153
+ name: 'selectedProjects',
154
+ message: 'Select projects to generate SDK for:',
155
+ choices: projects.map(p => ({ title: p, value: p, selected: true })),
156
+ })
157
+
158
+ if (!response || !response.selectedProjects || response.selectedProjects.length === 0) {
159
+ console.log('\nāŒ No projects selected.\n')
160
+ return
161
+ }
162
+
163
+ for (const project of response.selectedProjects) {
164
+ console.log(`\nšŸ“¦ Generating SDK for: ${project}`)
165
+ const projectPath = resolve(root, project)
166
+ try {
167
+ await generateSDK(projectPath)
168
+ }
169
+ catch {
170
+ console.error(`Failed to generate SDK for ${project}`)
171
+ }
172
+ }
173
+
174
+ console.log('\nāœ… All SDKs generated!')
175
+ }