spine-framework 0.3.62 → 0.3.65

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.
@@ -0,0 +1,231 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander'
4
+ import { readFileSync, existsSync, readdirSync, statSync } from 'fs'
5
+ import { join, dirname } from 'path'
6
+ import { fileURLToPath } from 'url'
7
+
8
+ const __filename = fileURLToPath(import.meta.url)
9
+ const __dirname = dirname(__filename)
10
+
11
+ interface Manifest {
12
+ name: string
13
+ slug: string
14
+ version: string
15
+ app_type: 'full' | 'ui' | 'backend' | 'data'
16
+ registration?: {
17
+ enabled?: boolean
18
+ default_role?: string
19
+ redirect_path?: string
20
+ account_strategy?: 'existing' | 'new' | 'choice'
21
+ target_account?: string
22
+ }
23
+ }
24
+
25
+ interface PackageJson {
26
+ name: string
27
+ version: string
28
+ files?: string[]
29
+ spine?: {
30
+ type?: string
31
+ slug?: string
32
+ manifestPath?: string
33
+ app_slug?: string
34
+ entry_point?: string
35
+ manifest?: string
36
+ }
37
+ }
38
+
39
+ class ValidationError extends Error {
40
+ constructor(message: string) {
41
+ super(message)
42
+ this.name = 'ValidationError'
43
+ }
44
+ }
45
+
46
+ function validateManifest(appPath: string): Manifest {
47
+ const manifestPath = join(appPath, 'manifest.json')
48
+
49
+ if (!existsSync(manifestPath)) {
50
+ throw new ValidationError('❌ manifest.json is required')
51
+ }
52
+
53
+ let manifest: Manifest
54
+ try {
55
+ manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
56
+ } catch (err) {
57
+ throw new ValidationError('❌ manifest.json is not valid JSON')
58
+ }
59
+
60
+ // Required fields
61
+ const requiredFields = ['name', 'slug', 'version', 'app_type']
62
+ for (const field of requiredFields) {
63
+ if (!manifest[field]) {
64
+ throw new ValidationError(`❌ manifest.json missing required field: ${field}`)
65
+ }
66
+ }
67
+
68
+ // Validate app_type
69
+ const validAppTypes = ['full', 'ui', 'backend', 'data']
70
+ if (!validAppTypes.includes(manifest.app_type)) {
71
+ throw new ValidationError(`❌ manifest.json app_type must be one of: ${validAppTypes.join(', ')}`)
72
+ }
73
+
74
+ return manifest
75
+ }
76
+
77
+ function validatePackageJson(appPath: string, manifest: Manifest): PackageJson {
78
+ const packagePath = join(appPath, 'package.json')
79
+
80
+ if (!existsSync(packagePath)) {
81
+ throw new ValidationError('❌ package.json is required')
82
+ }
83
+
84
+ let packageJson: PackageJson
85
+ try {
86
+ packageJson = JSON.parse(readFileSync(packagePath, 'utf8'))
87
+ } catch (err) {
88
+ throw new ValidationError('❌ package.json is not valid JSON')
89
+ }
90
+
91
+ // Check version sync
92
+ if (packageJson.version !== manifest.version) {
93
+ throw new ValidationError(`❌ Version mismatch: package.json has ${packageJson.version} but manifest.json has ${manifest.version}`)
94
+ }
95
+
96
+ return packageJson
97
+ }
98
+
99
+ function validateRequiredFiles(appPath: string, manifest: Manifest): void {
100
+ const { app_type } = manifest
101
+
102
+ // index.tsx is required for full and ui apps
103
+ if (['full', 'ui'].includes(app_type)) {
104
+ if (!existsSync(join(appPath, 'index.tsx'))) {
105
+ throw new ValidationError('❌ index.tsx is required for full and ui apps')
106
+ }
107
+ }
108
+
109
+ // pages/ is required for full and ui apps
110
+ if (['full', 'ui'].includes(app_type)) {
111
+ if (!existsSync(join(appPath, 'pages'))) {
112
+ throw new ValidationError('❌ pages/ directory is required for full and ui apps')
113
+ }
114
+ }
115
+
116
+ // functions/ is required for full and backend apps
117
+ if (['full', 'backend'].includes(app_type)) {
118
+ if (!existsSync(join(appPath, 'functions'))) {
119
+ throw new ValidationError('❌ functions/ directory is required for full and backend apps')
120
+ }
121
+ }
122
+ }
123
+
124
+ function validateAllowedDirectories(appPath: string): void {
125
+ const allowedDirs = [
126
+ 'pages', 'components', 'hooks', 'utils', 'functions',
127
+ 'seed', 'public', '.devin'
128
+ ]
129
+
130
+ const entries = readdirSync(appPath, { withFileTypes: true })
131
+ .filter(dirent => dirent.isDirectory())
132
+ .map(dirent => dirent.name)
133
+
134
+ const unknownDirs = entries.filter(dir => !allowedDirs.includes(dir))
135
+
136
+ if (unknownDirs.length > 0) {
137
+ throw new ValidationError(`❌ Unknown directories found: ${unknownDirs.join(', ')}. Allowed directories: ${allowedDirs.join(', ')}`)
138
+ }
139
+ }
140
+
141
+ function validateDevinDirectory(appPath: string): void {
142
+ const devinPath = join(appPath, '.devin')
143
+
144
+ if (existsSync(devinPath)) {
145
+ const agentsPath = join(devinPath, 'AGENTS.md')
146
+ const agentsAltPath = join(devinPath, 'AGENT.md')
147
+
148
+ if (!existsSync(agentsPath) && !existsSync(agentsAltPath)) {
149
+ throw new ValidationError('❌ .devin/ directory exists but AGENTS.md is missing. If .devin/ exists, it must contain AGENTS.md with agentic guidance.')
150
+ }
151
+
152
+ // Validate skills if they exist
153
+ const skillsPath = join(devinPath, 'skills')
154
+ if (existsSync(skillsPath)) {
155
+ const skillDirs = readdirSync(skillsPath, { withFileTypes: true })
156
+ .filter(dirent => dirent.isDirectory())
157
+
158
+ for (const skillDir of skillDirs) {
159
+ const skillFile = join(skillsPath, skillDir.name, 'SKILL.md')
160
+ if (!existsSync(skillFile)) {
161
+ throw new ValidationError(`❌ Skill directory ${skillDir.name} exists but SKILL.md is missing`)
162
+ }
163
+ }
164
+ }
165
+ }
166
+ }
167
+
168
+ function validatePackageFiles(appPath: string, packageJson: PackageJson): void {
169
+ const { files = [] } = packageJson
170
+
171
+ // Check that important directories are included in files array
172
+ const importantFiles = ['index.tsx', 'manifest.json']
173
+ const importantDirs = ['pages', 'components', 'hooks', 'utils', 'functions', 'seed', 'public']
174
+
175
+ for (const file of importantFiles) {
176
+ if (existsSync(join(appPath, file)) && !files.includes(file)) {
177
+ console.warn(`⚠️ ${file} exists but is not in package.json files array`)
178
+ }
179
+ }
180
+
181
+ for (const dir of importantDirs) {
182
+ if (existsSync(join(appPath, dir)) && !files.includes(`${dir}/`)) {
183
+ console.warn(`⚠️ ${dir}/ exists but is not in package.json files array`)
184
+ }
185
+ }
186
+ }
187
+
188
+ function validateApp(appPath: string): void {
189
+ try {
190
+ console.log(`🔍 Validating app at: ${appPath}`)
191
+
192
+ const manifest = validateManifest(appPath)
193
+ console.log(`✅ manifest.json valid for ${manifest.name} v${manifest.version}`)
194
+
195
+ const packageJson = validatePackageJson(appPath, manifest)
196
+ console.log(`✅ package.json valid and versions match`)
197
+
198
+ validateRequiredFiles(appPath, manifest)
199
+ console.log(`✅ Required files present for app_type: ${manifest.app_type}`)
200
+
201
+ validateAllowedDirectories(appPath)
202
+ console.log(`✅ No unknown directories found`)
203
+
204
+ validateDevinDirectory(appPath)
205
+ console.log(`✅ .devin/ directory valid (if present)`)
206
+
207
+ validatePackageFiles(appPath, packageJson)
208
+ console.log(`✅ package.json files array checked`)
209
+
210
+ console.log(`\n🎉 App validation passed!`)
211
+
212
+ } catch (err) {
213
+ if (err instanceof ValidationError) {
214
+ console.error(err.message)
215
+ process.exit(1)
216
+ } else {
217
+ console.error(`❌ Unexpected error: ${err.message}`)
218
+ process.exit(1)
219
+ }
220
+ }
221
+ }
222
+
223
+ const program = new Command()
224
+
225
+ program
226
+ .name('validate-app')
227
+ .description('Validate a Spine app structure and configuration')
228
+ .argument('[path]', 'Path to the app directory', '.')
229
+ .action(validateApp)
230
+
231
+ program.parse()
@@ -42,12 +42,32 @@ fi
42
42
 
43
43
  # 3. Overlay custom functions (overrides + additions), excluding test files
44
44
  CUSTOM_COUNT=0
45
+
46
+ # 3a. Copy functions from custom/functions/
45
47
  if [ -d "$CUSTOM_DIR/functions" ]; then
46
48
  # Copy all files except test files (*-test.ts, *.test.ts)
47
49
  find "$CUSTOM_DIR/functions" -maxdepth 1 -name '*.ts' ! -name '*-test.ts' ! -name '*.test.ts' -exec cp {} "$TMP_DIR"/ \; 2>/dev/null || true
48
50
  # Copy subdirectories if any
49
51
  find "$CUSTOM_DIR/functions" -mindepth 1 -type d -exec sh -c 'dir="$1"; base=$(basename "$dir"); mkdir -p "'"$TMP_DIR"'/$base" && cp -r "$dir"/* "'"$TMP_DIR"'/$base/"' _ {} \; 2>/dev/null || true
50
- CUSTOM_COUNT=$(find "$CUSTOM_DIR/functions" -name '*.ts' ! -name '*-test.ts' ! -name '*.test.ts' 2>/dev/null | wc -l | tr -d ' ')
52
+ CUSTOM_COUNT=$((CUSTOM_COUNT + $(find "$CUSTOM_DIR/functions" -name '*.ts' ! -name '*-test.ts' ! -name '*.test.ts' 2>/dev/null | wc -l | tr -d ' ')))
53
+ fi
54
+
55
+ # 3b. Copy functions from custom/apps/*/functions/
56
+ if [ -d "$CUSTOM_DIR/apps" ]; then
57
+ for app_dir in "$CUSTOM_DIR/apps"/*; do
58
+ if [ -d "$app_dir/functions" ]; then
59
+ # Copy all files except test files (*-test.ts, *.test.ts)
60
+ find "$app_dir/functions" -maxdepth 1 -name '*.ts' ! -name '*-test.ts' ! -name '*.test.ts' -exec cp {} "$TMP_DIR"/ \; 2>/dev/null || true
61
+ # Copy subdirectories if any
62
+ find "$app_dir/functions" -mindepth 1 -type d -exec sh -c 'dir="$1"; base=$(basename "$dir"); mkdir -p "'"$TMP_DIR"'/$base" && cp -r "$dir"/* "'"$TMP_DIR"'/$base/"' _ {} \; 2>/dev/null || true
63
+ APP_COUNT=$(find "$app_dir/functions" -name '*.ts' ! -name '*-test.ts' ! -name '*.test.ts' 2>/dev/null | wc -l | tr -d ' ')
64
+ CUSTOM_COUNT=$((CUSTOM_COUNT + APP_COUNT))
65
+ echo " ✓ $(basename "$app_dir"): $APP_COUNT files"
66
+ fi
67
+ done
68
+ fi
69
+
70
+ if [ $CUSTOM_COUNT -gt 0 ]; then
51
71
  echo " ✓ Custom: $CUSTOM_COUNT files"
52
72
  else
53
73
  echo " ○ Custom: (empty)"
@@ -252,6 +252,8 @@ export function RegisterPage() {
252
252
  if (urlApp && regConfig?.apps[urlApp]) {
253
253
  const appConfig = regConfig.apps[urlApp]
254
254
 
255
+ console.log('App config found:', appConfig)
256
+
255
257
  // Determine role
256
258
  if (urlRole && appConfig.roles.includes(urlRole)) {
257
259
  effectiveRole = urlRole
@@ -262,6 +264,8 @@ export function RegisterPage() {
262
264
  // Determine account strategy
263
265
  effectiveAccountStrategy = appConfig.account_strategy
264
266
  targetAccount = appConfig.target_account
267
+
268
+ console.log('Account strategy resolved:', { effectiveAccountStrategy, targetAccount })
265
269
  }
266
270
  // If only URL role is specified (no app), use it directly with default strategy
267
271
  else if (urlRole) {
@@ -294,8 +298,11 @@ export function RegisterPage() {
294
298
 
295
299
  // Handle account creation vs existing account assignment
296
300
  let registrationResult
301
+ console.log('Registration decision:', { accountStrategy, targetAccount, effectiveRole })
302
+
297
303
  if (accountStrategy === 'existing' && targetAccount) {
298
304
  // Assign to existing account
305
+ console.log('Using existing_account path')
299
306
  registrationResult = await callInvitesApi('complete-registration', {
300
307
  path: 'existing_account',
301
308
  email: resolvedEmail,
@@ -304,6 +311,7 @@ export function RegisterPage() {
304
311
  }, 'POST')
305
312
  } else {
306
313
  // Create new account (personal or company)
314
+ console.log('Using new_account path')
307
315
  const accountName = useType === 'personal' ? fullName : companyName
308
316
  registrationResult = await callInvitesApi('complete-registration', {
309
317
  path: 'new_account',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spine-framework",
3
- "version": "0.3.62",
3
+ "version": "0.3.65",
4
4
  "description": "Multi-tenant, modular application platform for modern SaaS systems",
5
5
  "type": "module",
6
6
  "bin": {