spine-framework 0.3.63 → 0.3.66

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,236 @@
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'
128
+ ]
129
+
130
+ const entries = readdirSync(appPath, { withFileTypes: true })
131
+ .filter(dirent => dirent.isDirectory())
132
+ .map(dirent => dirent.name)
133
+
134
+ // Filter out .devin/ since it's optional and handled separately
135
+ const unknownDirs = entries.filter(dir => !allowedDirs.includes(dir) && dir !== '.devin')
136
+
137
+ if (unknownDirs.length > 0) {
138
+ throw new ValidationError(`❌ Unknown directories found: ${unknownDirs.join(', ')}. Allowed directories: ${allowedDirs.join(', ')} (plus optional .devin/)`)
139
+ }
140
+ }
141
+
142
+ function validateDevinDirectory(appPath: string): void {
143
+ const devinPath = join(appPath, '.devin')
144
+
145
+ if (existsSync(devinPath)) {
146
+ const agentsPath = join(devinPath, 'AGENTS.md')
147
+ const agentsAltPath = join(devinPath, 'AGENT.md')
148
+
149
+ if (!existsSync(agentsPath) && !existsSync(agentsAltPath)) {
150
+ throw new ValidationError('❌ .devin/ directory exists but AGENTS.md is missing. If .devin/ exists, it must contain AGENTS.md with agentic guidance.')
151
+ }
152
+
153
+ // Validate skills if they exist
154
+ const skillsPath = join(devinPath, 'skills')
155
+ if (existsSync(skillsPath)) {
156
+ const skillDirs = readdirSync(skillsPath, { withFileTypes: true })
157
+ .filter(dirent => dirent.isDirectory())
158
+
159
+ for (const skillDir of skillDirs) {
160
+ const skillFile = join(skillsPath, skillDir.name, 'SKILL.md')
161
+ if (!existsSync(skillFile)) {
162
+ throw new ValidationError(`❌ Skill directory ${skillDir.name} exists but SKILL.md is missing`)
163
+ }
164
+ }
165
+ }
166
+
167
+ console.log(`✅ .devin/ directory valid (if present)`)
168
+ } else {
169
+ console.log(`✅ .devin/ directory not present (optional)`)
170
+ }
171
+ }
172
+
173
+ function validatePackageFiles(appPath: string, packageJson: PackageJson): void {
174
+ const { files = [] } = packageJson
175
+
176
+ // Check that important directories are included in files array
177
+ const importantFiles = ['index.tsx', 'manifest.json']
178
+ const importantDirs = ['pages', 'components', 'hooks', 'utils', 'functions', 'seed', 'public']
179
+
180
+ for (const file of importantFiles) {
181
+ if (existsSync(join(appPath, file)) && !files.includes(file)) {
182
+ console.warn(`⚠️ ${file} exists but is not in package.json files array`)
183
+ }
184
+ }
185
+
186
+ for (const dir of importantDirs) {
187
+ if (existsSync(join(appPath, dir)) && !files.includes(`${dir}/`)) {
188
+ console.warn(`⚠️ ${dir}/ exists but is not in package.json files array`)
189
+ }
190
+ }
191
+ }
192
+
193
+ function validateApp(appPath: string): void {
194
+ try {
195
+ console.log(`🔍 Validating app at: ${appPath}`)
196
+
197
+ const manifest = validateManifest(appPath)
198
+ console.log(`✅ manifest.json valid for ${manifest.name} v${manifest.version}`)
199
+
200
+ const packageJson = validatePackageJson(appPath, manifest)
201
+ console.log(`✅ package.json valid and versions match`)
202
+
203
+ validateRequiredFiles(appPath, manifest)
204
+ console.log(`✅ Required files present for app_type: ${manifest.app_type}`)
205
+
206
+ validateAllowedDirectories(appPath)
207
+ console.log(`✅ No unknown directories found`)
208
+
209
+ validateDevinDirectory(appPath)
210
+ console.log(`✅ .devin/ directory valid (if present)`)
211
+
212
+ validatePackageFiles(appPath, packageJson)
213
+ console.log(`✅ package.json files array checked`)
214
+
215
+ console.log(`\n🎉 App validation passed!`)
216
+
217
+ } catch (err) {
218
+ if (err instanceof ValidationError) {
219
+ console.error(err.message)
220
+ process.exit(1)
221
+ } else {
222
+ console.error(`❌ Unexpected error: ${err.message}`)
223
+ process.exit(1)
224
+ }
225
+ }
226
+ }
227
+
228
+ const program = new Command()
229
+
230
+ program
231
+ .name('validate-app')
232
+ .description('Validate a Spine app structure and configuration')
233
+ .argument('[path]', 'Path to the app directory', '.')
234
+ .action(validateApp)
235
+
236
+ 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)"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spine-framework",
3
- "version": "0.3.63",
3
+ "version": "0.3.66",
4
4
  "description": "Multi-tenant, modular application platform for modern SaaS systems",
5
5
  "type": "module",
6
6
  "bin": {