spine-framework 0.3.19 → 0.3.21
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,202 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
/**
|
|
3
|
+
* @module cli/commands/init
|
|
4
|
+
* @audience installer
|
|
5
|
+
* @layer cli
|
|
6
|
+
* @stability stable
|
|
7
|
+
*
|
|
8
|
+
* `spine-framework init` — scaffolds a new Spine Framework project in the
|
|
9
|
+
* current directory. Copies framework files, creates the custom/ workspace,
|
|
10
|
+
* writes package.json and .env template.
|
|
11
|
+
*
|
|
12
|
+
* **Usage:**
|
|
13
|
+
* ```bash
|
|
14
|
+
* npx spine-framework init
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { Command } from 'commander'
|
|
19
|
+
import { existsSync, mkdirSync, writeFileSync, copyFileSync, readdirSync, statSync, cpSync } from 'fs'
|
|
20
|
+
import { resolve, dirname, join } from 'path'
|
|
21
|
+
import { fileURLToPath } from 'url'
|
|
22
|
+
import { execSync } from 'child_process'
|
|
23
|
+
|
|
24
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
25
|
+
const __dirname = dirname(__filename)
|
|
26
|
+
|
|
27
|
+
// Root of the spine-framework package (node_modules/spine-framework/)
|
|
28
|
+
const PACKAGE_ROOT = resolve(__dirname, '../../../')
|
|
29
|
+
// Root of the user's project (cwd)
|
|
30
|
+
const PROJECT_ROOT = process.cwd()
|
|
31
|
+
|
|
32
|
+
export function registerInitCommands(program: Command) {
|
|
33
|
+
program
|
|
34
|
+
.command('init')
|
|
35
|
+
.description('Scaffold a new Spine Framework project in the current directory')
|
|
36
|
+
.option('--force', 'Overwrite existing files', false)
|
|
37
|
+
.action(async (opts) => {
|
|
38
|
+
console.log('\nInitializing Spine Framework project...\n')
|
|
39
|
+
|
|
40
|
+
// Guard: don't overwrite an existing project unless --force
|
|
41
|
+
if (existsSync(join(PROJECT_ROOT, 'package.json')) && !opts.force) {
|
|
42
|
+
console.error('Error: package.json already exists. Use --force to overwrite.')
|
|
43
|
+
process.exit(1)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
// 1. Copy .framework/ into the project
|
|
48
|
+
console.log(' Copying .framework/...')
|
|
49
|
+
const fwSrc = join(PACKAGE_ROOT, '.framework')
|
|
50
|
+
const fwDest = join(PROJECT_ROOT, '.framework')
|
|
51
|
+
if (!existsSync(fwDest)) {
|
|
52
|
+
cpSync(fwSrc, fwDest, { recursive: true })
|
|
53
|
+
console.log(' ✓ .framework/ copied')
|
|
54
|
+
} else {
|
|
55
|
+
console.log(' ○ .framework/ already exists, skipping')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 2. Create custom/ workspace directories
|
|
59
|
+
console.log(' Creating custom/ workspace...')
|
|
60
|
+
const customDirs = [
|
|
61
|
+
'custom/apps',
|
|
62
|
+
'custom/functions',
|
|
63
|
+
'custom/components',
|
|
64
|
+
'custom/migrations',
|
|
65
|
+
'custom/tests',
|
|
66
|
+
]
|
|
67
|
+
for (const dir of customDirs) {
|
|
68
|
+
const full = join(PROJECT_ROOT, dir)
|
|
69
|
+
if (!existsSync(full)) {
|
|
70
|
+
mkdirSync(full, { recursive: true })
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Copy custom/.devin if present in package
|
|
74
|
+
const devinSrc = join(PACKAGE_ROOT, 'custom/.devin')
|
|
75
|
+
const devinDest = join(PROJECT_ROOT, 'custom/.devin')
|
|
76
|
+
if (existsSync(devinSrc) && !existsSync(devinDest)) {
|
|
77
|
+
cpSync(devinSrc, devinDest, { recursive: true })
|
|
78
|
+
}
|
|
79
|
+
console.log(' ✓ custom/ workspace created')
|
|
80
|
+
|
|
81
|
+
// 3. Copy config files (vite, tailwind, tsconfig, postcss)
|
|
82
|
+
console.log(' Copying config files...')
|
|
83
|
+
const configSrc = join(PACKAGE_ROOT, 'config')
|
|
84
|
+
const configDest = join(PROJECT_ROOT, 'config')
|
|
85
|
+
if (existsSync(configSrc) && !existsSync(configDest)) {
|
|
86
|
+
cpSync(configSrc, configDest, { recursive: true })
|
|
87
|
+
console.log(' ✓ config/ copied')
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 4. Copy scripts/
|
|
91
|
+
console.log(' Copying scripts/...')
|
|
92
|
+
const scriptsSrc = join(PACKAGE_ROOT, 'scripts')
|
|
93
|
+
const scriptsDest = join(PROJECT_ROOT, 'scripts')
|
|
94
|
+
if (existsSync(scriptsSrc) && !existsSync(scriptsDest)) {
|
|
95
|
+
cpSync(scriptsSrc, scriptsDest, { recursive: true })
|
|
96
|
+
console.log(' ✓ scripts/ copied')
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 5. Copy netlify.toml
|
|
100
|
+
const netlifyTomlSrc = join(PACKAGE_ROOT, 'netlify.toml')
|
|
101
|
+
if (existsSync(netlifyTomlSrc) && !existsSync(join(PROJECT_ROOT, 'netlify.toml'))) {
|
|
102
|
+
copyFileSync(netlifyTomlSrc, join(PROJECT_ROOT, 'netlify.toml'))
|
|
103
|
+
console.log(' ✓ netlify.toml copied')
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 6. Write package.json
|
|
107
|
+
console.log(' Writing package.json...')
|
|
108
|
+
const projectName = PROJECT_ROOT.split('/').pop() || 'my-spine-project'
|
|
109
|
+
const pkgJson = {
|
|
110
|
+
name: projectName,
|
|
111
|
+
version: '0.1.0',
|
|
112
|
+
private: true,
|
|
113
|
+
type: 'module',
|
|
114
|
+
scripts: {
|
|
115
|
+
dev: 'netlify dev',
|
|
116
|
+
assemble: 'bash scripts/assemble.sh',
|
|
117
|
+
'assemble:frontend': 'bash scripts/assemble-frontend.sh',
|
|
118
|
+
'assemble:functions': 'bash scripts/assemble-functions.sh',
|
|
119
|
+
build: 'npm run assemble && vite build --config config/vite.config.ts',
|
|
120
|
+
verify: 'bash scripts/verify-integrity.sh',
|
|
121
|
+
test: 'vitest run',
|
|
122
|
+
},
|
|
123
|
+
dependencies: {
|
|
124
|
+
'spine-framework': `^${getPackageVersion()}`,
|
|
125
|
+
},
|
|
126
|
+
}
|
|
127
|
+
if (!existsSync(join(PROJECT_ROOT, 'package.json')) || opts.force) {
|
|
128
|
+
writeFileSync(
|
|
129
|
+
join(PROJECT_ROOT, 'package.json'),
|
|
130
|
+
JSON.stringify(pkgJson, null, 2) + '\n'
|
|
131
|
+
)
|
|
132
|
+
console.log(' ✓ package.json written')
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 7. Write .env template
|
|
136
|
+
console.log(' Writing .env template...')
|
|
137
|
+
if (!existsSync(join(PROJECT_ROOT, '.env'))) {
|
|
138
|
+
const envTemplate = [
|
|
139
|
+
'# Supabase credentials',
|
|
140
|
+
'# Get these from: Supabase Dashboard → Your Project → Settings → API',
|
|
141
|
+
'SUPABASE_URL=https://<project-ref>.supabase.co',
|
|
142
|
+
'SUPABASE_ANON_KEY=',
|
|
143
|
+
'SUPABASE_SERVICE_ROLE_KEY=',
|
|
144
|
+
'SUPABASE_DB_PASSWORD=',
|
|
145
|
+
'',
|
|
146
|
+
'# Vite (browser-safe)',
|
|
147
|
+
'VITE_SUPABASE_URL=https://<project-ref>.supabase.co',
|
|
148
|
+
'VITE_SUPABASE_ANON_KEY=',
|
|
149
|
+
'VITE_DB_SCHEMA=public',
|
|
150
|
+
'VITE_APP_NAME=Spine',
|
|
151
|
+
'',
|
|
152
|
+
].join('\n')
|
|
153
|
+
writeFileSync(join(PROJECT_ROOT, '.env'), envTemplate)
|
|
154
|
+
console.log(' ✓ .env template written')
|
|
155
|
+
} else {
|
|
156
|
+
console.log(' ○ .env already exists, skipping')
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 8. Write .gitignore
|
|
160
|
+
if (!existsSync(join(PROJECT_ROOT, '.gitignore'))) {
|
|
161
|
+
const gitignore = [
|
|
162
|
+
'node_modules',
|
|
163
|
+
'dist',
|
|
164
|
+
'.assembled/',
|
|
165
|
+
'.netlify',
|
|
166
|
+
'.env',
|
|
167
|
+
'.env.local',
|
|
168
|
+
'.env.production',
|
|
169
|
+
'*.log',
|
|
170
|
+
'.DS_Store',
|
|
171
|
+
'*.tsbuildinfo',
|
|
172
|
+
'.vite',
|
|
173
|
+
].join('\n') + '\n'
|
|
174
|
+
writeFileSync(join(PROJECT_ROOT, '.gitignore'), gitignore)
|
|
175
|
+
console.log(' ✓ .gitignore written')
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
console.log('\n✅ Project initialized!\n')
|
|
179
|
+
console.log('Next steps:')
|
|
180
|
+
console.log(' 1. Edit .env with your Supabase credentials')
|
|
181
|
+
console.log(' 2. Run: npx spine-framework migrate')
|
|
182
|
+
console.log(' 3. Run: npm install && npm run assemble && netlify dev')
|
|
183
|
+
console.log()
|
|
184
|
+
|
|
185
|
+
} catch (err: any) {
|
|
186
|
+
console.error('\n❌ Init failed:', err.message)
|
|
187
|
+
if (process.env.SPINE_CLI_DEBUG) console.error(err.stack)
|
|
188
|
+
process.exit(1)
|
|
189
|
+
}
|
|
190
|
+
})
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function getPackageVersion(): string {
|
|
194
|
+
try {
|
|
195
|
+
const pkg = JSON.parse(
|
|
196
|
+
require('fs').readFileSync(join(PACKAGE_ROOT, 'package.json'), 'utf8')
|
|
197
|
+
)
|
|
198
|
+
return pkg.version || '0.3.0'
|
|
199
|
+
} catch {
|
|
200
|
+
return '0.3.0'
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
/**
|
|
3
|
+
* @module cli/commands/install-app
|
|
4
|
+
* @audience installer
|
|
5
|
+
* @layer cli
|
|
6
|
+
* @stability stable
|
|
7
|
+
*
|
|
8
|
+
* `spine-framework install-app` — installs a Spine Framework app from npm.
|
|
9
|
+
* Copies app files into custom/apps/, seeds types/roles/link-types into the DB.
|
|
10
|
+
*
|
|
11
|
+
* **Usage:**
|
|
12
|
+
* ```bash
|
|
13
|
+
* npx spine-framework install-app spine-framework-portal@latest
|
|
14
|
+
* npx spine-framework install-app spine-framework-cortex
|
|
15
|
+
* npx spine-framework uninstall-app spine-framework-portal
|
|
16
|
+
* npx spine-framework update-app spine-framework-portal
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { Command } from 'commander'
|
|
21
|
+
import { existsSync, readFileSync, mkdirSync, cpSync } from 'fs'
|
|
22
|
+
import { resolve, dirname, join, basename } from 'path'
|
|
23
|
+
import { fileURLToPath } from 'url'
|
|
24
|
+
import { execSync } from 'child_process'
|
|
25
|
+
import { createClient } from '@supabase/supabase-js'
|
|
26
|
+
|
|
27
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
28
|
+
const __dirname = dirname(__filename)
|
|
29
|
+
const PROJECT_ROOT = process.cwd()
|
|
30
|
+
|
|
31
|
+
export function registerInstallAppCommands(program: Command) {
|
|
32
|
+
// install-app
|
|
33
|
+
program
|
|
34
|
+
.command('install-app <package>')
|
|
35
|
+
.description('Install a Spine Framework app from npm')
|
|
36
|
+
.option('--no-seed', 'Skip seeding types and roles into the database', false)
|
|
37
|
+
.action(async (packageName: string, opts) => {
|
|
38
|
+
console.log(`\nInstalling app: ${packageName}\n`)
|
|
39
|
+
|
|
40
|
+
loadEnv()
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
// 1. npm install the package
|
|
44
|
+
console.log(` Installing ${packageName} via npm...`)
|
|
45
|
+
execSync(`npm install ${packageName} --save`, {
|
|
46
|
+
cwd: PROJECT_ROOT,
|
|
47
|
+
stdio: 'inherit',
|
|
48
|
+
})
|
|
49
|
+
console.log(' ✓ npm install complete')
|
|
50
|
+
|
|
51
|
+
// 2. Resolve package location in node_modules
|
|
52
|
+
const pkgSlug = packageName.split('@')[0] // strip version
|
|
53
|
+
const pkgPath = join(PROJECT_ROOT, 'node_modules', pkgSlug)
|
|
54
|
+
|
|
55
|
+
if (!existsSync(pkgPath)) {
|
|
56
|
+
throw new Error(`Package not found at ${pkgPath}`)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 3. Determine app slug from package manifest or name
|
|
60
|
+
const appSlug = resolveAppSlug(pkgPath, pkgSlug)
|
|
61
|
+
console.log(` App slug: ${appSlug}`)
|
|
62
|
+
|
|
63
|
+
// 4. Copy app files to custom/apps/<slug>/
|
|
64
|
+
const appDest = join(PROJECT_ROOT, 'custom/apps', appSlug)
|
|
65
|
+
if (!existsSync(appDest)) {
|
|
66
|
+
mkdirSync(appDest, { recursive: true })
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Copy everything except node_modules and package.json internals
|
|
70
|
+
const skipDirs = new Set(['node_modules', '.git'])
|
|
71
|
+
for (const entry of ['index.tsx', 'manifest.json', 'pages', 'components', 'hooks', 'api', 'seed']) {
|
|
72
|
+
const src = join(pkgPath, entry)
|
|
73
|
+
if (existsSync(src)) {
|
|
74
|
+
cpSync(src, join(appDest, entry), { recursive: true })
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
console.log(` ✓ App files copied to custom/apps/${appSlug}/`)
|
|
78
|
+
|
|
79
|
+
// 5. Seed types, roles, link-types into database
|
|
80
|
+
if (!opts.seed === false) {
|
|
81
|
+
await seedApp(appDest, appSlug)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
console.log(`\n✅ ${appSlug} installed successfully!\n`)
|
|
85
|
+
console.log('Next: npm run assemble && netlify dev\n')
|
|
86
|
+
|
|
87
|
+
} catch (err: any) {
|
|
88
|
+
console.error(`\n❌ Install failed: ${err.message}`)
|
|
89
|
+
if (process.env.SPINE_CLI_DEBUG) console.error(err.stack)
|
|
90
|
+
process.exit(1)
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
// uninstall-app
|
|
95
|
+
program
|
|
96
|
+
.command('uninstall-app <package>')
|
|
97
|
+
.description('Remove an installed Spine Framework app')
|
|
98
|
+
.option('--keep-data', 'Keep seeded types and roles in the database', false)
|
|
99
|
+
.action(async (packageName: string, opts) => {
|
|
100
|
+
console.log(`\nUninstalling app: ${packageName}\n`)
|
|
101
|
+
loadEnv()
|
|
102
|
+
|
|
103
|
+
const pkgSlug = packageName.split('@')[0]
|
|
104
|
+
const appSlug = pkgSlug.replace('spine-framework-', '')
|
|
105
|
+
const appDir = join(PROJECT_ROOT, 'custom/apps', appSlug)
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
if (existsSync(appDir)) {
|
|
109
|
+
const { rmSync } = await import('fs')
|
|
110
|
+
rmSync(appDir, { recursive: true, force: true })
|
|
111
|
+
console.log(` ✓ Removed custom/apps/${appSlug}/`)
|
|
112
|
+
} else {
|
|
113
|
+
console.log(` ○ custom/apps/${appSlug}/ not found, nothing to remove`)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
execSync(`npm uninstall ${pkgSlug}`, { cwd: PROJECT_ROOT, stdio: 'inherit' })
|
|
117
|
+
console.log(` ✓ npm uninstall complete`)
|
|
118
|
+
|
|
119
|
+
console.log(`\n✅ ${appSlug} uninstalled.\n`)
|
|
120
|
+
} catch (err: any) {
|
|
121
|
+
console.error(`\n❌ Uninstall failed: ${err.message}`)
|
|
122
|
+
process.exit(1)
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
// update-app
|
|
127
|
+
program
|
|
128
|
+
.command('update-app <package>')
|
|
129
|
+
.description('Update an installed Spine Framework app to a newer version')
|
|
130
|
+
.action(async (packageName: string) => {
|
|
131
|
+
console.log(`\nUpdating app: ${packageName}\n`)
|
|
132
|
+
loadEnv()
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
execSync(`npm install ${packageName}@latest --save`, {
|
|
136
|
+
cwd: PROJECT_ROOT,
|
|
137
|
+
stdio: 'inherit',
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
const pkgSlug = packageName.split('@')[0]
|
|
141
|
+
const pkgPath = join(PROJECT_ROOT, 'node_modules', pkgSlug)
|
|
142
|
+
const appSlug = resolveAppSlug(pkgPath, pkgSlug)
|
|
143
|
+
const appDest = join(PROJECT_ROOT, 'custom/apps', appSlug)
|
|
144
|
+
|
|
145
|
+
if (!existsSync(appDest)) {
|
|
146
|
+
mkdirSync(appDest, { recursive: true })
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
for (const entry of ['index.tsx', 'manifest.json', 'pages', 'components', 'hooks', 'api', 'seed']) {
|
|
150
|
+
const src = join(pkgPath, entry)
|
|
151
|
+
if (existsSync(src)) {
|
|
152
|
+
cpSync(src, join(appDest, entry), { recursive: true })
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
console.log(`\n✅ ${appSlug} updated.\n`)
|
|
157
|
+
} catch (err: any) {
|
|
158
|
+
console.error(`\n❌ Update failed: ${err.message}`)
|
|
159
|
+
process.exit(1)
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ─── HELPERS ────────────────────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
function resolveAppSlug(pkgPath: string, pkgName: string): string {
|
|
167
|
+
try {
|
|
168
|
+
const manifest = JSON.parse(readFileSync(join(pkgPath, 'manifest.json'), 'utf8'))
|
|
169
|
+
if (manifest.slug) return manifest.slug
|
|
170
|
+
} catch {}
|
|
171
|
+
// Derive from package name: spine-framework-portal → portal
|
|
172
|
+
return pkgName.replace(/^spine-framework-/, '')
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function loadEnv() {
|
|
176
|
+
const envPath = join(PROJECT_ROOT, '.env')
|
|
177
|
+
if (!existsSync(envPath)) return
|
|
178
|
+
const lines = readFileSync(envPath, 'utf8').split('\n')
|
|
179
|
+
for (const line of lines) {
|
|
180
|
+
const trimmed = line.trim()
|
|
181
|
+
if (!trimmed || trimmed.startsWith('#')) continue
|
|
182
|
+
const eqIdx = trimmed.indexOf('=')
|
|
183
|
+
if (eqIdx === -1) continue
|
|
184
|
+
const key = trimmed.slice(0, eqIdx).trim()
|
|
185
|
+
const val = trimmed.slice(eqIdx + 1).trim()
|
|
186
|
+
if (key && !(key in process.env)) process.env[key] = val
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function seedApp(appDir: string, appSlug: string) {
|
|
191
|
+
const supabaseUrl = process.env.SUPABASE_URL
|
|
192
|
+
const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY
|
|
193
|
+
|
|
194
|
+
if (!supabaseUrl || !serviceRoleKey) {
|
|
195
|
+
console.log(' ○ Skipping DB seed (SUPABASE_URL / SUPABASE_SERVICE_ROLE_KEY not set)')
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const db = createClient(supabaseUrl, serviceRoleKey, {
|
|
200
|
+
auth: { persistSession: false },
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
// Get spine-core app id for foreign key
|
|
204
|
+
const { data: coreApp } = await db
|
|
205
|
+
.from('apps')
|
|
206
|
+
.select('id')
|
|
207
|
+
.eq('slug', appSlug)
|
|
208
|
+
.single()
|
|
209
|
+
|
|
210
|
+
const appId = coreApp?.id
|
|
211
|
+
|
|
212
|
+
// Seed types
|
|
213
|
+
const typesPath = join(appDir, 'seed/types.json')
|
|
214
|
+
if (existsSync(typesPath)) {
|
|
215
|
+
const types = JSON.parse(readFileSync(typesPath, 'utf8'))
|
|
216
|
+
console.log(` Seeding ${types.length} type(s) for ${appSlug}...`)
|
|
217
|
+
for (const type of types) {
|
|
218
|
+
const { error } = await db
|
|
219
|
+
.from('types')
|
|
220
|
+
.upsert({ ...type, app_id: appId }, { onConflict: 'slug' })
|
|
221
|
+
if (error) console.warn(` ⚠ type ${type.slug}: ${error.message}`)
|
|
222
|
+
}
|
|
223
|
+
console.log(` ✓ Types seeded`)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Seed roles
|
|
227
|
+
const rolesPath = join(appDir, 'seed/roles.json')
|
|
228
|
+
if (existsSync(rolesPath)) {
|
|
229
|
+
const roles = JSON.parse(readFileSync(rolesPath, 'utf8'))
|
|
230
|
+
console.log(` Seeding ${roles.length} role(s) for ${appSlug}...`)
|
|
231
|
+
for (const role of roles) {
|
|
232
|
+
const { error } = await db
|
|
233
|
+
.from('roles')
|
|
234
|
+
.upsert({ ...role, app_id: appId }, { onConflict: 'slug' })
|
|
235
|
+
if (error) console.warn(` ⚠ role ${role.slug}: ${error.message}`)
|
|
236
|
+
}
|
|
237
|
+
console.log(` ✓ Roles seeded`)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Seed link-types
|
|
241
|
+
const linkTypesPath = join(appDir, 'seed/link-types.json')
|
|
242
|
+
if (existsSync(linkTypesPath)) {
|
|
243
|
+
const linkTypes = JSON.parse(readFileSync(linkTypesPath, 'utf8'))
|
|
244
|
+
if (linkTypes.length > 0) {
|
|
245
|
+
console.log(` Seeding ${linkTypes.length} link type(s) for ${appSlug}...`)
|
|
246
|
+
for (const lt of linkTypes) {
|
|
247
|
+
const { error } = await db
|
|
248
|
+
.from('link_types')
|
|
249
|
+
.upsert({ ...lt, app_id: appId }, { onConflict: 'slug' })
|
|
250
|
+
if (error) console.warn(` ⚠ link_type ${lt.slug}: ${error.message}`)
|
|
251
|
+
}
|
|
252
|
+
console.log(` ✓ Link types seeded`)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
/**
|
|
3
|
+
* @module cli/commands/migrate
|
|
4
|
+
* @audience installer
|
|
5
|
+
* @layer cli
|
|
6
|
+
* @stability stable
|
|
7
|
+
*
|
|
8
|
+
* `spine-framework migrate` — applies pending SQL migrations to the configured
|
|
9
|
+
* Supabase project using the Supabase Management API and direct DB connection.
|
|
10
|
+
*
|
|
11
|
+
* Reads migration files from `.framework/migrations/` and `custom/migrations/`
|
|
12
|
+
* in order, skipping any already tracked in `spine_migrations` table.
|
|
13
|
+
*
|
|
14
|
+
* **Usage:**
|
|
15
|
+
* ```bash
|
|
16
|
+
* npx spine-framework migrate
|
|
17
|
+
* npx spine-framework migrate --dry-run
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { Command } from 'commander'
|
|
22
|
+
import { readFileSync, readdirSync, existsSync } from 'fs'
|
|
23
|
+
import { resolve, dirname, join, basename } from 'path'
|
|
24
|
+
import { fileURLToPath } from 'url'
|
|
25
|
+
import { createClient } from '@supabase/supabase-js'
|
|
26
|
+
|
|
27
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
28
|
+
const __dirname = dirname(__filename)
|
|
29
|
+
const PACKAGE_ROOT = resolve(__dirname, '../../../')
|
|
30
|
+
const PROJECT_ROOT = process.cwd()
|
|
31
|
+
|
|
32
|
+
export function registerMigrateCommands(program: Command) {
|
|
33
|
+
program
|
|
34
|
+
.command('migrate')
|
|
35
|
+
.description('Apply pending database migrations to your Supabase project')
|
|
36
|
+
.option('--dry-run', 'Show pending migrations without applying them', false)
|
|
37
|
+
.option('--framework-only', 'Only apply .framework/migrations/, skip custom/', false)
|
|
38
|
+
.action(async (opts) => {
|
|
39
|
+
console.log('\nRunning Spine Framework migrations...\n')
|
|
40
|
+
|
|
41
|
+
// Load env
|
|
42
|
+
loadEnv()
|
|
43
|
+
|
|
44
|
+
const supabaseUrl = process.env.SUPABASE_URL
|
|
45
|
+
const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY
|
|
46
|
+
|
|
47
|
+
if (!supabaseUrl || !serviceRoleKey) {
|
|
48
|
+
console.error('Error: SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY must be set in .env')
|
|
49
|
+
console.error('Run: npx spine-framework init to create a .env template')
|
|
50
|
+
process.exit(1)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const db = createClient(supabaseUrl, serviceRoleKey, {
|
|
54
|
+
auth: { persistSession: false },
|
|
55
|
+
db: { schema: 'public' },
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
// Ensure tracking table exists
|
|
60
|
+
await ensureMigrationsTable(db)
|
|
61
|
+
|
|
62
|
+
// Collect migration files in order
|
|
63
|
+
const migrationFiles = collectMigrations(opts.frameworkOnly)
|
|
64
|
+
|
|
65
|
+
if (migrationFiles.length === 0) {
|
|
66
|
+
console.log('No migration files found.')
|
|
67
|
+
process.exit(0)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Get already-applied migrations
|
|
71
|
+
const { data: applied, error: fetchErr } = await db
|
|
72
|
+
.from('spine_migrations')
|
|
73
|
+
.select('filename')
|
|
74
|
+
.order('applied_at', { ascending: true })
|
|
75
|
+
|
|
76
|
+
if (fetchErr) throw new Error(`Could not read migration state: ${fetchErr.message}`)
|
|
77
|
+
|
|
78
|
+
const appliedSet = new Set((applied || []).map((r: any) => r.filename))
|
|
79
|
+
|
|
80
|
+
const pending = migrationFiles.filter(f => !appliedSet.has(basename(f)))
|
|
81
|
+
|
|
82
|
+
if (pending.length === 0) {
|
|
83
|
+
console.log('✓ All migrations already applied. Nothing to do.\n')
|
|
84
|
+
process.exit(0)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
console.log(`Found ${pending.length} pending migration(s):\n`)
|
|
88
|
+
for (const f of pending) {
|
|
89
|
+
console.log(` ○ ${basename(f)}`)
|
|
90
|
+
}
|
|
91
|
+
console.log()
|
|
92
|
+
|
|
93
|
+
if (opts.dryRun) {
|
|
94
|
+
console.log('Dry run — no changes made.\n')
|
|
95
|
+
process.exit(0)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Apply each pending migration
|
|
99
|
+
let applied_count = 0
|
|
100
|
+
for (const filepath of pending) {
|
|
101
|
+
const filename = basename(filepath)
|
|
102
|
+
process.stdout.write(` Applying ${filename}...`)
|
|
103
|
+
|
|
104
|
+
const sql = readFileSync(filepath, 'utf8')
|
|
105
|
+
|
|
106
|
+
// Execute via rpc exec_sql or direct query
|
|
107
|
+
const { error } = await db.rpc('exec_sql' as any, { sql }).single()
|
|
108
|
+
|
|
109
|
+
if (error) {
|
|
110
|
+
// exec_sql may not exist — fall back to running via REST
|
|
111
|
+
const fallbackResult = await applyViaMigrationEndpoint(supabaseUrl, serviceRoleKey, sql)
|
|
112
|
+
if (!fallbackResult.ok) {
|
|
113
|
+
console.log(' ✗')
|
|
114
|
+
console.error(`\nFailed to apply ${filename}: ${fallbackResult.error}`)
|
|
115
|
+
console.error('\nFallback: Apply this migration manually in the Supabase SQL editor:')
|
|
116
|
+
console.error(` ${filepath}`)
|
|
117
|
+
process.exit(1)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Track as applied
|
|
122
|
+
await db.from('spine_migrations').insert({
|
|
123
|
+
filename,
|
|
124
|
+
applied_at: new Date().toISOString(),
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
console.log(' ✓')
|
|
128
|
+
applied_count++
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
console.log(`\n✅ Applied ${applied_count} migration(s) successfully.\n`)
|
|
132
|
+
|
|
133
|
+
if (applied_count > 0) {
|
|
134
|
+
console.log('Next: npx spine-framework status — to verify the installation\n')
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
} catch (err: any) {
|
|
138
|
+
console.error('\n❌ Migration failed:', err.message)
|
|
139
|
+
if (process.env.SPINE_CLI_DEBUG) console.error(err.stack)
|
|
140
|
+
process.exit(1)
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ─── HELPERS ────────────────────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
function loadEnv() {
|
|
148
|
+
const envPath = join(PROJECT_ROOT, '.env')
|
|
149
|
+
if (!existsSync(envPath)) return
|
|
150
|
+
const lines = readFileSync(envPath, 'utf8').split('\n')
|
|
151
|
+
for (const line of lines) {
|
|
152
|
+
const trimmed = line.trim()
|
|
153
|
+
if (!trimmed || trimmed.startsWith('#')) continue
|
|
154
|
+
const eqIdx = trimmed.indexOf('=')
|
|
155
|
+
if (eqIdx === -1) continue
|
|
156
|
+
const key = trimmed.slice(0, eqIdx).trim()
|
|
157
|
+
const val = trimmed.slice(eqIdx + 1).trim()
|
|
158
|
+
if (key && !(key in process.env)) {
|
|
159
|
+
process.env[key] = val
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function ensureMigrationsTable(db: any) {
|
|
165
|
+
// Create tracking table if it doesn't exist
|
|
166
|
+
await db.rpc('exec_sql' as any, {
|
|
167
|
+
sql: `
|
|
168
|
+
CREATE TABLE IF NOT EXISTS public.spine_migrations (
|
|
169
|
+
id SERIAL PRIMARY KEY,
|
|
170
|
+
filename TEXT NOT NULL UNIQUE,
|
|
171
|
+
applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
172
|
+
);
|
|
173
|
+
`
|
|
174
|
+
})
|
|
175
|
+
// Ignore errors — table may already exist or exec_sql may not exist yet
|
|
176
|
+
// We'll handle this via the fallback path
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function collectMigrations(frameworkOnly: boolean): string[] {
|
|
180
|
+
const files: string[] = []
|
|
181
|
+
|
|
182
|
+
// Framework migrations (from installed package or local .framework/)
|
|
183
|
+
const fwMigrationsDir = existsSync(join(PROJECT_ROOT, '.framework/migrations'))
|
|
184
|
+
? join(PROJECT_ROOT, '.framework/migrations')
|
|
185
|
+
: join(PACKAGE_ROOT, '.framework/migrations')
|
|
186
|
+
|
|
187
|
+
if (existsSync(fwMigrationsDir)) {
|
|
188
|
+
const fwFiles = readdirSync(fwMigrationsDir)
|
|
189
|
+
.filter(f => f.endsWith('.sql') && !f.startsWith('.'))
|
|
190
|
+
.sort()
|
|
191
|
+
.map(f => join(fwMigrationsDir, f))
|
|
192
|
+
files.push(...fwFiles)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Custom migrations (from project's custom/migrations/)
|
|
196
|
+
if (!frameworkOnly) {
|
|
197
|
+
const customMigrationsDir = join(PROJECT_ROOT, 'custom/migrations')
|
|
198
|
+
if (existsSync(customMigrationsDir)) {
|
|
199
|
+
const customFiles = readdirSync(customMigrationsDir)
|
|
200
|
+
.filter(f => f.endsWith('.sql') && !f.startsWith('.'))
|
|
201
|
+
.sort()
|
|
202
|
+
.map(f => join(customMigrationsDir, f))
|
|
203
|
+
files.push(...customFiles)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return files
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function applyViaMigrationEndpoint(
|
|
211
|
+
supabaseUrl: string,
|
|
212
|
+
serviceRoleKey: string,
|
|
213
|
+
sql: string
|
|
214
|
+
): Promise<{ ok: boolean; error?: string }> {
|
|
215
|
+
try {
|
|
216
|
+
// Use Supabase's REST SQL endpoint
|
|
217
|
+
const projectRef = supabaseUrl.match(/https:\/\/([^.]+)\.supabase\.co/)?.[1]
|
|
218
|
+
if (!projectRef) return { ok: false, error: 'Could not parse project ref from SUPABASE_URL' }
|
|
219
|
+
|
|
220
|
+
const res = await fetch(
|
|
221
|
+
`https://api.supabase.com/v1/projects/${projectRef}/database/query`,
|
|
222
|
+
{
|
|
223
|
+
method: 'POST',
|
|
224
|
+
headers: {
|
|
225
|
+
'Content-Type': 'application/json',
|
|
226
|
+
'Authorization': `Bearer ${serviceRoleKey}`,
|
|
227
|
+
},
|
|
228
|
+
body: JSON.stringify({ query: sql }),
|
|
229
|
+
}
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
if (!res.ok) {
|
|
233
|
+
const body = await res.text()
|
|
234
|
+
return { ok: false, error: `API error ${res.status}: ${body}` }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return { ok: true }
|
|
238
|
+
} catch (err: any) {
|
|
239
|
+
return { ok: false, error: err.message }
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
/**
|
|
3
|
+
* @module cli/commands/status
|
|
4
|
+
* @audience installer
|
|
5
|
+
* @layer cli
|
|
6
|
+
* @stability stable
|
|
7
|
+
*
|
|
8
|
+
* `spine-framework status` — shows installed apps, migration state, and
|
|
9
|
+
* framework version.
|
|
10
|
+
*
|
|
11
|
+
* **Usage:**
|
|
12
|
+
* ```bash
|
|
13
|
+
* npx spine-framework status
|
|
14
|
+
* npx spine-framework status --json
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { Command } from 'commander'
|
|
19
|
+
import { existsSync, readFileSync, readdirSync } from 'fs'
|
|
20
|
+
import { resolve, dirname, join, basename } from 'path'
|
|
21
|
+
import { fileURLToPath } from 'url'
|
|
22
|
+
import { createClient } from '@supabase/supabase-js'
|
|
23
|
+
|
|
24
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
25
|
+
const __dirname = dirname(__filename)
|
|
26
|
+
const PACKAGE_ROOT = resolve(__dirname, '../../../')
|
|
27
|
+
const PROJECT_ROOT = process.cwd()
|
|
28
|
+
|
|
29
|
+
export function registerStatusCommands(program: Command) {
|
|
30
|
+
program
|
|
31
|
+
.command('status')
|
|
32
|
+
.description('Show installed apps and framework version')
|
|
33
|
+
.option('--json', 'Output as JSON')
|
|
34
|
+
.action(async (opts) => {
|
|
35
|
+
loadEnv()
|
|
36
|
+
|
|
37
|
+
const supabaseUrl = process.env.SUPABASE_URL
|
|
38
|
+
const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY
|
|
39
|
+
|
|
40
|
+
// Framework version
|
|
41
|
+
let frameworkVersion = 'unknown'
|
|
42
|
+
try {
|
|
43
|
+
const pkg = JSON.parse(readFileSync(join(PACKAGE_ROOT, 'package.json'), 'utf8'))
|
|
44
|
+
frameworkVersion = pkg.version
|
|
45
|
+
} catch {}
|
|
46
|
+
|
|
47
|
+
// Installed apps (from custom/apps/)
|
|
48
|
+
const appsDir = join(PROJECT_ROOT, 'custom/apps')
|
|
49
|
+
const installedApps: { slug: string; version?: string; hasManifest: boolean }[] = []
|
|
50
|
+
|
|
51
|
+
if (existsSync(appsDir)) {
|
|
52
|
+
for (const slug of readdirSync(appsDir)) {
|
|
53
|
+
const appDir = join(appsDir, slug)
|
|
54
|
+
const manifestPath = join(appDir, 'manifest.json')
|
|
55
|
+
let version: string | undefined
|
|
56
|
+
let hasManifest = false
|
|
57
|
+
if (existsSync(manifestPath)) {
|
|
58
|
+
hasManifest = true
|
|
59
|
+
try {
|
|
60
|
+
const m = JSON.parse(readFileSync(manifestPath, 'utf8'))
|
|
61
|
+
version = m.version
|
|
62
|
+
} catch {}
|
|
63
|
+
}
|
|
64
|
+
installedApps.push({ slug, version, hasManifest })
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Migration state
|
|
69
|
+
let migrations: { applied: number; pending: number; files: string[] } = {
|
|
70
|
+
applied: 0,
|
|
71
|
+
pending: 0,
|
|
72
|
+
files: [],
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const fwMigrationsDir = existsSync(join(PROJECT_ROOT, '.framework/migrations'))
|
|
76
|
+
? join(PROJECT_ROOT, '.framework/migrations')
|
|
77
|
+
: join(PACKAGE_ROOT, '.framework/migrations')
|
|
78
|
+
|
|
79
|
+
if (existsSync(fwMigrationsDir)) {
|
|
80
|
+
migrations.files = readdirSync(fwMigrationsDir)
|
|
81
|
+
.filter(f => f.endsWith('.sql'))
|
|
82
|
+
.sort()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (supabaseUrl && serviceRoleKey) {
|
|
86
|
+
try {
|
|
87
|
+
const db = createClient(supabaseUrl, serviceRoleKey, {
|
|
88
|
+
auth: { persistSession: false },
|
|
89
|
+
})
|
|
90
|
+
const { data } = await db
|
|
91
|
+
.from('spine_migrations')
|
|
92
|
+
.select('filename')
|
|
93
|
+
const appliedSet = new Set((data || []).map((r: any) => r.filename))
|
|
94
|
+
migrations.applied = migrations.files.filter(f => appliedSet.has(f)).length
|
|
95
|
+
migrations.pending = migrations.files.length - migrations.applied
|
|
96
|
+
} catch {
|
|
97
|
+
// DB not accessible — just show file count
|
|
98
|
+
migrations.pending = migrations.files.length
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (opts.json) {
|
|
103
|
+
console.log(JSON.stringify({ frameworkVersion, installedApps, migrations }, null, 2))
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Human-readable output
|
|
108
|
+
console.log('\n── Spine Framework Status ─────────────────────────────')
|
|
109
|
+
console.log(` Version: spine-framework@${frameworkVersion}`)
|
|
110
|
+
console.log(` Project: ${PROJECT_ROOT}`)
|
|
111
|
+
console.log()
|
|
112
|
+
|
|
113
|
+
console.log('── Installed Apps ─────────────────────────────────────')
|
|
114
|
+
if (installedApps.length === 0) {
|
|
115
|
+
console.log(' No apps installed.')
|
|
116
|
+
console.log(' Run: npx spine-framework install-app spine-framework-portal@latest')
|
|
117
|
+
} else {
|
|
118
|
+
for (const app of installedApps) {
|
|
119
|
+
const ver = app.version ? `@${app.version}` : ''
|
|
120
|
+
const manifest = app.hasManifest ? '' : ' (no manifest)'
|
|
121
|
+
console.log(` ✓ ${app.slug}${ver}${manifest}`)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
console.log()
|
|
125
|
+
|
|
126
|
+
console.log('── Migrations ─────────────────────────────────────────')
|
|
127
|
+
if (!supabaseUrl) {
|
|
128
|
+
console.log(' (Set SUPABASE_URL in .env to check migration state)')
|
|
129
|
+
} else {
|
|
130
|
+
console.log(` Applied: ${migrations.applied}`)
|
|
131
|
+
console.log(` Pending: ${migrations.pending}`)
|
|
132
|
+
if (migrations.pending > 0) {
|
|
133
|
+
console.log(' Run: npx spine-framework migrate')
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
console.log()
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function loadEnv() {
|
|
141
|
+
const envPath = join(PROJECT_ROOT, '.env')
|
|
142
|
+
if (!existsSync(envPath)) return
|
|
143
|
+
const lines = readFileSync(envPath, 'utf8').split('\n')
|
|
144
|
+
for (const line of lines) {
|
|
145
|
+
const trimmed = line.trim()
|
|
146
|
+
if (!trimmed || trimmed.startsWith('#')) continue
|
|
147
|
+
const eqIdx = trimmed.indexOf('=')
|
|
148
|
+
if (eqIdx === -1) continue
|
|
149
|
+
const key = trimmed.slice(0, eqIdx).trim()
|
|
150
|
+
const val = trimmed.slice(eqIdx + 1).trim()
|
|
151
|
+
if (key && !(key in process.env)) process.env[key] = val
|
|
152
|
+
}
|
|
153
|
+
}
|
package/.framework/cli/index.ts
CHANGED
|
@@ -42,11 +42,15 @@ const program = new Command()
|
|
|
42
42
|
|
|
43
43
|
program
|
|
44
44
|
.name('spine-framework')
|
|
45
|
-
.description('Spine
|
|
45
|
+
.description('Spine Framework CLI — install, migrate, and manage your Spine instance')
|
|
46
46
|
.version('2.0.0')
|
|
47
47
|
.option('--account <id>', 'Override the account ID for this command')
|
|
48
48
|
|
|
49
49
|
const [
|
|
50
|
+
{ registerInitCommands },
|
|
51
|
+
{ registerMigrateCommands },
|
|
52
|
+
{ registerInstallAppCommands },
|
|
53
|
+
{ registerStatusCommands },
|
|
50
54
|
{ registerAuthCommands },
|
|
51
55
|
{ registerPipelineCommands },
|
|
52
56
|
{ registerItemCommands },
|
|
@@ -59,6 +63,10 @@ const [
|
|
|
59
63
|
{ registerGenerateCommands },
|
|
60
64
|
{ registerCreateAppCommands },
|
|
61
65
|
] = await Promise.all([
|
|
66
|
+
import('./commands/init.ts'),
|
|
67
|
+
import('./commands/migrate.ts'),
|
|
68
|
+
import('./commands/install-app.ts'),
|
|
69
|
+
import('./commands/status.ts'),
|
|
62
70
|
import('./commands/auth.ts'),
|
|
63
71
|
import('./commands/pipelines.ts'),
|
|
64
72
|
import('./commands/items.ts'),
|
|
@@ -72,6 +80,10 @@ const [
|
|
|
72
80
|
import('./commands/create-app.ts'),
|
|
73
81
|
])
|
|
74
82
|
|
|
83
|
+
registerInitCommands(program)
|
|
84
|
+
registerMigrateCommands(program)
|
|
85
|
+
registerInstallAppCommands(program)
|
|
86
|
+
registerStatusCommands(program)
|
|
75
87
|
registerAuthCommands(program)
|
|
76
88
|
registerPipelineCommands(program)
|
|
77
89
|
registerItemCommands(program)
|
package/package.json
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spine-framework",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.21",
|
|
4
4
|
"description": "Multi-tenant, modular application platform for modern SaaS systems",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"bin":
|
|
7
|
-
"spine-framework": "./.framework/cli/bin.mjs"
|
|
8
|
-
},
|
|
6
|
+
"bin": "./.framework/cli/bin.mjs",
|
|
9
7
|
"files": [
|
|
10
8
|
".framework/cli/bin.mjs",
|
|
11
9
|
".framework/cli/",
|
|
@@ -46,7 +44,7 @@
|
|
|
46
44
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
47
45
|
"repository": {
|
|
48
46
|
"type": "git",
|
|
49
|
-
"url": "https://github.com/spine-framework/spine-framework.git"
|
|
47
|
+
"url": "git+https://github.com/spine-framework/spine-framework.git"
|
|
50
48
|
},
|
|
51
49
|
"homepage": "https://spine-framework.com",
|
|
52
50
|
"bugs": {
|