spine-framework 0.2.1 → 0.2.3
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/.framework/cli/commands/claim.ts +447 -0
- package/.framework/cli/commands/install-app.ts +67 -7
- package/.framework/cli/commands/migrate.ts +13 -1
- package/.framework/cli/commands/publish.ts +191 -0
- package/.framework/cli/commands/status.ts +74 -3
- package/.framework/cli/commands/update-app.ts +397 -0
- package/.framework/cli/index.ts +12 -0
- package/.framework/migrations/000_foundation.sql +18 -4
- package/.framework/migrations/004_app_version_tracking.sql +8 -0
- package/.framework/src/App.tsx +6 -1
- package/.framework/src/components/CustomAppLoader.tsx +17 -0
- package/bin/spine-framework.cjs +0 -0
- package/bin/welcome.cjs +2 -2
- package/dist/cli/commands/claim.d.ts +56 -0
- package/dist/cli/commands/claim.d.ts.map +1 -0
- package/dist/cli/commands/install-app.d.ts.map +1 -1
- package/dist/cli/commands/migrate.d.ts.map +1 -1
- package/dist/cli/commands/publish.d.ts +23 -0
- package/dist/cli/commands/publish.d.ts.map +1 -0
- package/dist/cli/commands/status.d.ts.map +1 -1
- package/dist/cli/commands/update-app.d.ts +24 -0
- package/dist/cli/commands/update-app.d.ts.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/config/signals.d.ts +82 -0
- package/dist/config/signals.d.ts.map +1 -0
- package/package.json +3 -2
- package/bin/ws-shim.cjs +0 -8
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
/**
|
|
3
|
+
* @module cli/commands/publish
|
|
4
|
+
* @audience developer
|
|
5
|
+
* @layer cli
|
|
6
|
+
* @stability stable
|
|
7
|
+
*
|
|
8
|
+
* `spine-framework publish` — Bump version, write changelog, publish to npm, commit.
|
|
9
|
+
*
|
|
10
|
+
* Run from the spine-ia coding repo. Handles both the framework package and
|
|
11
|
+
* individual app packages (cortex, portal, etc.).
|
|
12
|
+
*
|
|
13
|
+
* **Usage:**
|
|
14
|
+
* ```bash
|
|
15
|
+
* spine-framework publish # publish framework (patch bump)
|
|
16
|
+
* spine-framework publish --app cortex # publish cortex app (patch bump)
|
|
17
|
+
* spine-framework publish --app cortex --bump minor
|
|
18
|
+
* spine-framework publish --app cortex --bump patch --notes "Fix sidebar nav"
|
|
19
|
+
* spine-framework publish --app cortex --ci --notes "Fix sidebar nav" --bump patch
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { Command } from 'commander'
|
|
24
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs'
|
|
25
|
+
import { resolve, dirname } from 'path'
|
|
26
|
+
import { fileURLToPath } from 'url'
|
|
27
|
+
import { execSync } from 'child_process'
|
|
28
|
+
import * as readline from 'readline'
|
|
29
|
+
|
|
30
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
31
|
+
const __dirname = dirname(__filename)
|
|
32
|
+
const PROJECT_ROOT = process.cwd()
|
|
33
|
+
|
|
34
|
+
interface PublishOptions {
|
|
35
|
+
app?: string
|
|
36
|
+
bump: 'patch' | 'minor' | 'major'
|
|
37
|
+
notes?: string
|
|
38
|
+
ci: boolean
|
|
39
|
+
dryRun: boolean
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ─── SEMVER BUMP ──────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
function bumpVersion(current: string, bump: 'patch' | 'minor' | 'major'): string {
|
|
45
|
+
const [major, minor, patch] = current.replace(/^v/, '').split('.').map(Number)
|
|
46
|
+
if (bump === 'major') return `${major + 1}.0.0`
|
|
47
|
+
if (bump === 'minor') return `${major}.${minor + 1}.0`
|
|
48
|
+
return `${major}.${minor}.${patch + 1}`
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─── INTERACTIVE PROMPT ───────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
function promptNotes(): Promise<string> {
|
|
54
|
+
return new Promise(resolve => {
|
|
55
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
|
56
|
+
rl.question(' Changelog notes (comma-separated): ', answer => {
|
|
57
|
+
rl.close()
|
|
58
|
+
resolve(answer.trim())
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ─── PUBLISH LOGIC ────────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
async function publishTarget(options: PublishOptions): Promise<void> {
|
|
66
|
+
const isApp = !!options.app
|
|
67
|
+
const slug = options.app
|
|
68
|
+
|
|
69
|
+
let pkgJsonPath: string
|
|
70
|
+
let manifestPath: string | null
|
|
71
|
+
let publishDir: string
|
|
72
|
+
let displayName: string
|
|
73
|
+
|
|
74
|
+
if (isApp && slug) {
|
|
75
|
+
pkgJsonPath = resolve(PROJECT_ROOT, `custom/apps/${slug}/package.json`)
|
|
76
|
+
manifestPath = resolve(PROJECT_ROOT, `custom/apps/${slug}/manifest.json`)
|
|
77
|
+
publishDir = resolve(PROJECT_ROOT, `custom/apps/${slug}`)
|
|
78
|
+
displayName = `spine-framework-${slug}`
|
|
79
|
+
} else {
|
|
80
|
+
pkgJsonPath = resolve(PROJECT_ROOT, 'package.json')
|
|
81
|
+
manifestPath = null
|
|
82
|
+
publishDir = PROJECT_ROOT
|
|
83
|
+
displayName = 'spine-framework'
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!existsSync(pkgJsonPath)) {
|
|
87
|
+
console.error(` ❌ package.json not found: ${pkgJsonPath}`)
|
|
88
|
+
process.exit(1)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 1. Read current version
|
|
92
|
+
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf8'))
|
|
93
|
+
const currentVersion = pkgJson.version as string
|
|
94
|
+
const newVersion = bumpVersion(currentVersion, options.bump)
|
|
95
|
+
|
|
96
|
+
console.log(`\n📦 Publishing ${displayName}`)
|
|
97
|
+
console.log(` ${currentVersion} → ${newVersion} (${options.bump} bump)\n`)
|
|
98
|
+
|
|
99
|
+
// 2. Collect changelog notes
|
|
100
|
+
let notesInput = options.notes || ''
|
|
101
|
+
if (!notesInput) {
|
|
102
|
+
if (options.ci) {
|
|
103
|
+
console.error(` ❌ --ci mode requires --notes "..."`)
|
|
104
|
+
process.exit(1)
|
|
105
|
+
}
|
|
106
|
+
notesInput = await promptNotes()
|
|
107
|
+
}
|
|
108
|
+
const notes = notesInput
|
|
109
|
+
.split(',')
|
|
110
|
+
.map(s => s.trim())
|
|
111
|
+
.filter(Boolean)
|
|
112
|
+
|
|
113
|
+
if (notes.length === 0) {
|
|
114
|
+
console.error(` ❌ At least one changelog note is required.`)
|
|
115
|
+
process.exit(1)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (options.dryRun) {
|
|
119
|
+
console.log(` [dry-run] Would bump ${displayName} ${currentVersion} → ${newVersion}`)
|
|
120
|
+
console.log(` [dry-run] Changelog entry:`)
|
|
121
|
+
console.log(` { "version": "${newVersion}", "notes": ${JSON.stringify(notes)} }`)
|
|
122
|
+
console.log(` [dry-run] Would run: npm publish (from ${publishDir})`)
|
|
123
|
+
console.log(` [dry-run] Would commit: chore: publish ${displayName}@${newVersion}`)
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 3. Bump version in package.json
|
|
128
|
+
pkgJson.version = newVersion
|
|
129
|
+
writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2) + '\n', 'utf8')
|
|
130
|
+
console.log(` ✓ package.json version → ${newVersion}`)
|
|
131
|
+
|
|
132
|
+
// 4. Prepend changelog entry to manifest.json (apps only)
|
|
133
|
+
if (manifestPath && existsSync(manifestPath)) {
|
|
134
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
|
|
135
|
+
const changelog: Array<{ version: string; notes: string[] }> = manifest.changelog || []
|
|
136
|
+
changelog.unshift({ version: newVersion, notes })
|
|
137
|
+
manifest.version = newVersion
|
|
138
|
+
manifest.changelog = changelog
|
|
139
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf8')
|
|
140
|
+
console.log(` ✓ manifest.json changelog prepended, version → ${newVersion}`)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 5. npm publish
|
|
144
|
+
console.log(`\n🚀 Running npm publish...`)
|
|
145
|
+
try {
|
|
146
|
+
execSync('npm publish --access public', { cwd: publishDir, stdio: 'inherit' })
|
|
147
|
+
console.log(` ✓ Published ${displayName}@${newVersion}`)
|
|
148
|
+
} catch (err: any) {
|
|
149
|
+
console.error(` ❌ npm publish failed: ${err.message}`)
|
|
150
|
+
console.error(` Version files have already been updated — commit them manually.`)
|
|
151
|
+
process.exit(1)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 6. Git commit
|
|
155
|
+
console.log(`\n📝 Committing version bump...`)
|
|
156
|
+
try {
|
|
157
|
+
const filesToStage = [pkgJsonPath]
|
|
158
|
+
if (manifestPath && existsSync(manifestPath)) filesToStage.push(manifestPath)
|
|
159
|
+
execSync(`git add ${filesToStage.map(f => `"${f}"`).join(' ')}`, { cwd: PROJECT_ROOT })
|
|
160
|
+
execSync(
|
|
161
|
+
`git commit -m "chore: publish ${displayName}@${newVersion}"`,
|
|
162
|
+
{ cwd: PROJECT_ROOT, stdio: 'inherit' },
|
|
163
|
+
)
|
|
164
|
+
console.log(` ✓ Committed`)
|
|
165
|
+
} catch (err: any) {
|
|
166
|
+
console.error(` ⚠️ Git commit failed: ${err.message}`)
|
|
167
|
+
console.error(` Published successfully — commit the version files manually.`)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
console.log(`\n✅ ${displayName}@${newVersion} published successfully!\n`)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function registerPublishCommands(program: Command) {
|
|
174
|
+
program
|
|
175
|
+
.command('publish')
|
|
176
|
+
.description('Bump version, write changelog, publish to npm, commit (run from spine-ia)')
|
|
177
|
+
.option('--app <slug>', 'App slug to publish (e.g. cortex). Omit to publish the framework itself.')
|
|
178
|
+
.option('--bump <level>', 'Version bump level: patch | minor | major', 'patch')
|
|
179
|
+
.option('--notes <text>', 'Comma-separated changelog notes (required in --ci mode)')
|
|
180
|
+
.option('--ci', 'Non-interactive mode: skip prompts, requires --notes', false)
|
|
181
|
+
.option('--dry-run', 'Show what would happen without making changes', false)
|
|
182
|
+
.action(async (opts) => {
|
|
183
|
+
try {
|
|
184
|
+
await publishTarget(opts as PublishOptions)
|
|
185
|
+
} catch (err: any) {
|
|
186
|
+
console.error('Error:', err.message)
|
|
187
|
+
if (process.env.SPINE_CLI_DEBUG) console.error(err.stack)
|
|
188
|
+
process.exit(1)
|
|
189
|
+
}
|
|
190
|
+
})
|
|
191
|
+
}
|
|
@@ -26,6 +26,7 @@ import { resolve, dirname } from 'path'
|
|
|
26
26
|
import { fileURLToPath } from 'url'
|
|
27
27
|
import { adminDb } from '../../functions/_shared/index.ts'
|
|
28
28
|
import { discoverManifests } from '../../functions/_shared/app-manifest.ts'
|
|
29
|
+
import { readEngagementData, ensureSerialData, shouldOfferClaim } from './claim.ts'
|
|
29
30
|
|
|
30
31
|
const __filename = fileURLToPath(import.meta.url)
|
|
31
32
|
const __dirname = dirname(__filename)
|
|
@@ -37,14 +38,58 @@ interface StatusOptions {
|
|
|
37
38
|
|
|
38
39
|
interface StatusReport {
|
|
39
40
|
database: { connected: boolean; error?: string }
|
|
40
|
-
|
|
41
|
+
installation: {
|
|
42
|
+
frameworkInstallSerial: string | null
|
|
43
|
+
engagementScore: number
|
|
44
|
+
claimStatus: 'anonymous' | 'claimed' | 'pending' | null
|
|
45
|
+
claimPrompt: { timing: string; message: string } | null
|
|
46
|
+
}
|
|
47
|
+
apps: Array<{
|
|
48
|
+
slug: string
|
|
49
|
+
name: string
|
|
50
|
+
hasManifest: boolean
|
|
51
|
+
hasSeed: boolean
|
|
52
|
+
installed: boolean
|
|
53
|
+
installedVersion: string | null
|
|
54
|
+
packageName: string | null
|
|
55
|
+
appInstallSerial: string | null
|
|
56
|
+
installTimestamp: string | null
|
|
57
|
+
}>
|
|
41
58
|
webhookHandlers: Array<{ name: string; functionName: string; isActive: boolean }>
|
|
42
59
|
typeCounts: { total: number; byApp: Record<string, number> }
|
|
43
60
|
}
|
|
44
61
|
|
|
45
62
|
async function gatherStatus(): Promise<StatusReport> {
|
|
63
|
+
// Get installation/engagement data
|
|
64
|
+
let frameworkSerial: string | null = null
|
|
65
|
+
let engagementScore = 0
|
|
66
|
+
let claimStatus: 'anonymous' | 'claimed' | 'pending' | null = null
|
|
67
|
+
let claimPrompt: { timing: string; message: string } | null = null
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const serialData = ensureSerialData()
|
|
71
|
+
frameworkSerial = serialData.serial_number
|
|
72
|
+
const engagement = readEngagementData()
|
|
73
|
+
if (engagement) {
|
|
74
|
+
engagementScore = engagement.engagement_score
|
|
75
|
+
claimStatus = engagement.claim_status
|
|
76
|
+
const prompt = shouldOfferClaim()
|
|
77
|
+
if (prompt) {
|
|
78
|
+
claimPrompt = { timing: prompt.timing, message: prompt.message }
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
// Silent fail - engagement tracking is optional
|
|
83
|
+
}
|
|
84
|
+
|
|
46
85
|
const report: StatusReport = {
|
|
47
86
|
database: { connected: false },
|
|
87
|
+
installation: {
|
|
88
|
+
frameworkInstallSerial: frameworkSerial,
|
|
89
|
+
engagementScore,
|
|
90
|
+
claimStatus,
|
|
91
|
+
claimPrompt,
|
|
92
|
+
},
|
|
48
93
|
apps: [],
|
|
49
94
|
webhookHandlers: [],
|
|
50
95
|
typeCounts: { total: 0, byApp: {} },
|
|
@@ -77,12 +122,16 @@ async function gatherStatus(): Promise<StatusReport> {
|
|
|
77
122
|
|
|
78
123
|
const { data: installations } = await adminDb
|
|
79
124
|
.from('app_installations')
|
|
80
|
-
.select('app_slug, is_enabled')
|
|
125
|
+
.select('app_slug, is_enabled, installed_version, package_name')
|
|
81
126
|
|
|
82
127
|
const installedSlugs = new Set(
|
|
83
128
|
(installations || []).filter(i => i.is_enabled).map(i => i.app_slug)
|
|
84
129
|
)
|
|
85
130
|
|
|
131
|
+
const installationMap = new Map(
|
|
132
|
+
(installations || []).map(i => [i.app_slug, i])
|
|
133
|
+
)
|
|
134
|
+
|
|
86
135
|
const allSlugs = new Set([
|
|
87
136
|
...manifests.map(m => m.slug),
|
|
88
137
|
...(dbApps || []).map(a => a.slug).filter(s => s !== 'spine-core'),
|
|
@@ -93,12 +142,17 @@ async function gatherStatus(): Promise<StatusReport> {
|
|
|
93
142
|
const dbApp = dbApps?.find(a => a.slug === slug)
|
|
94
143
|
const seedDir = resolve(PROJECT_ROOT, `custom/apps/${slug}/seed`)
|
|
95
144
|
|
|
145
|
+
const installation = installationMap.get(slug)
|
|
96
146
|
report.apps.push({
|
|
97
147
|
slug,
|
|
98
148
|
name: dbApp?.name || slug,
|
|
99
149
|
hasManifest: !!manifest,
|
|
100
150
|
hasSeed: existsSync(seedDir) && readdirSync(seedDir).some(f => f.endsWith('.json')),
|
|
101
151
|
installed: installedSlugs.has(slug),
|
|
152
|
+
installedVersion: installation?.installed_version || null,
|
|
153
|
+
packageName: installation?.package_name || null,
|
|
154
|
+
appInstallSerial: null, // Populated via signal system
|
|
155
|
+
installTimestamp: null, // Populated via signal system
|
|
102
156
|
})
|
|
103
157
|
}
|
|
104
158
|
|
|
@@ -132,6 +186,22 @@ async function gatherStatus(): Promise<StatusReport> {
|
|
|
132
186
|
function printStatus(report: StatusReport): void {
|
|
133
187
|
console.log('\n🔍 Spine Framework Status\n')
|
|
134
188
|
|
|
189
|
+
// Installation / Claim Status
|
|
190
|
+
if (report.installation.frameworkInstallSerial) {
|
|
191
|
+
console.log(` 🔐 Installation: ${report.installation.frameworkInstallSerial}`)
|
|
192
|
+
if (report.installation.claimStatus === 'claimed') {
|
|
193
|
+
console.log(' ✅ Claimed: Yes')
|
|
194
|
+
} else {
|
|
195
|
+
console.log(` ⏳ Claim Status: ${report.installation.claimStatus || 'anonymous'}`)
|
|
196
|
+
console.log(` 📊 Engagement Score: ${report.installation.engagementScore}`)
|
|
197
|
+
if (report.installation.claimPrompt) {
|
|
198
|
+
console.log(` 💡 ${report.installation.claimPrompt.message}`)
|
|
199
|
+
console.log(` Run: spine-framework claim`)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
console.log('')
|
|
203
|
+
}
|
|
204
|
+
|
|
135
205
|
// Database
|
|
136
206
|
if (report.database.connected) {
|
|
137
207
|
console.log(' 📡 Database: ✅ Connected')
|
|
@@ -146,10 +216,11 @@ function printStatus(report: StatusReport): void {
|
|
|
146
216
|
console.log(' (none)')
|
|
147
217
|
} else {
|
|
148
218
|
for (const app of report.apps) {
|
|
219
|
+
const versionStr = app.installedVersion ? `v${app.installedVersion}` : ''
|
|
149
220
|
const status = [
|
|
150
221
|
app.hasManifest ? '📋 manifest' : '',
|
|
151
222
|
app.hasSeed ? '🌱 seed' : '',
|
|
152
|
-
app.installed ?
|
|
223
|
+
app.installed ? `✅ installed${versionStr ? ' ' + versionStr : ''}` : '⬜ not installed',
|
|
153
224
|
].filter(Boolean).join(' ')
|
|
154
225
|
console.log(` ${app.slug.padEnd(24)} ${status}`)
|
|
155
226
|
}
|