spine-framework 0.2.1 → 0.2.2

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,397 @@
1
+ /// <reference types="node" />
2
+ /**
3
+ * @module cli/commands/update-app
4
+ * @audience installer
5
+ * @layer cli
6
+ * @stability stable
7
+ *
8
+ * `spine-framework update-app <slug>` — Check for and assist with app updates.
9
+ *
10
+ * Design principle: the agentic IDE is the merge tool. This command never
11
+ * writes to custom/apps/. It only:
12
+ * 1. Gets the new version into node_modules/ (read-only upstream reference)
13
+ * 2. Gives the agent a complete picture: versions, changelog, file diff summary
14
+ * 3. Records the new version in DB once the agent signals it is done (--accept)
15
+ *
16
+ * **Usage:**
17
+ * ```bash
18
+ * spine-framework update-app cortex # check for updates
19
+ * spine-framework update-app cortex --dry-run # full plan: changelog + file diff
20
+ * spine-framework update-app cortex --accept # record new version in DB only
21
+ * ```
22
+ */
23
+
24
+ import type { Command } from 'commander'
25
+ import { existsSync, readFileSync, readdirSync, createReadStream } from 'fs'
26
+ import { resolve, dirname } from 'path'
27
+ import { fileURLToPath } from 'url'
28
+ import { execSync } from 'child_process'
29
+ import { adminDb } from '../../functions/_shared/index.ts'
30
+
31
+ const __filename = fileURLToPath(import.meta.url)
32
+ const __dirname = dirname(__filename)
33
+ const PROJECT_ROOT = process.cwd()
34
+
35
+ interface UpdateOptions {
36
+ dryRun: boolean
37
+ accept: boolean
38
+ }
39
+
40
+ interface InstallationRecord {
41
+ app_slug: string
42
+ is_enabled: boolean
43
+ installed_version: string | null
44
+ package_name: string | null
45
+ }
46
+
47
+ // ─── NPM REGISTRY ─────────────────────────────────────────────────────────────
48
+
49
+ async function fetchLatestVersion(packageName: string): Promise<string | null> {
50
+ try {
51
+ const url = `https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`
52
+ const res = await fetch(url)
53
+ if (!res.ok) return null
54
+ const data = await res.json() as { version?: string }
55
+ return data.version || null
56
+ } catch {
57
+ return null
58
+ }
59
+ }
60
+
61
+ // ─── SEMVER COMPARISON ────────────────────────────────────────────────────────
62
+
63
+ function parseSemver(v: string): [number, number, number] {
64
+ const parts = v.replace(/^v/, '').split('.').map(Number)
65
+ return [parts[0] || 0, parts[1] || 0, parts[2] || 0]
66
+ }
67
+
68
+ function isNewer(candidate: string, current: string): boolean {
69
+ const [cMaj, cMin, cPatch] = parseSemver(candidate)
70
+ const [iMaj, iMin, iPatch] = parseSemver(current)
71
+ if (cMaj !== iMaj) return cMaj > iMaj
72
+ if (cMin !== iMin) return cMin > iMin
73
+ return cPatch > iPatch
74
+ }
75
+
76
+ function changelogBetween(
77
+ changelog: Array<{ version: string; notes: string[] }>,
78
+ fromVersion: string,
79
+ toVersion: string,
80
+ ): Array<{ version: string; notes: string[] }> {
81
+ return changelog.filter(entry => isNewer(entry.version, fromVersion) && !isNewer(entry.version, toVersion))
82
+ }
83
+
84
+ // ─── FILE DIFF SUMMARY ────────────────────────────────────────────────────────
85
+
86
+ function readFileSafe(filePath: string): string | null {
87
+ try { return readFileSync(filePath, 'utf8') } catch { return null }
88
+ }
89
+
90
+ function diffAppFiles(
91
+ pkgDir: string,
92
+ localDir: string,
93
+ dirs: string[],
94
+ topFiles: string[],
95
+ ): { changed: string[]; added: string[]; localOnly: string[] } {
96
+ const changed: string[] = []
97
+ const added: string[] = []
98
+ const localOnly: string[] = []
99
+
100
+ function diffFile(relPath: string) {
101
+ const upstreamContent = readFileSafe(resolve(pkgDir, relPath))
102
+ const localContent = readFileSafe(resolve(localDir, relPath))
103
+ if (upstreamContent === null && localContent !== null) {
104
+ localOnly.push(relPath)
105
+ } else if (upstreamContent !== null && localContent === null) {
106
+ added.push(relPath)
107
+ } else if (upstreamContent !== null && localContent !== null && upstreamContent !== localContent) {
108
+ changed.push(relPath)
109
+ }
110
+ }
111
+
112
+ for (const file of topFiles) {
113
+ diffFile(file)
114
+ }
115
+
116
+ for (const dir of dirs) {
117
+ const upstreamDirPath = resolve(pkgDir, dir)
118
+ if (!existsSync(upstreamDirPath)) continue
119
+ const walkDir = (base: string, rel: string) => {
120
+ const entries = readdirSync(resolve(base, rel), { withFileTypes: true })
121
+ for (const entry of entries) {
122
+ const entryRel = rel ? `${rel}/${entry.name}` : entry.name
123
+ if (entry.isDirectory()) {
124
+ walkDir(base, entryRel)
125
+ } else {
126
+ diffFile(`${dir}/${entryRel}`)
127
+ }
128
+ }
129
+ }
130
+ walkDir(upstreamDirPath, '')
131
+ }
132
+
133
+ return { changed, added, localOnly }
134
+ }
135
+
136
+ // ─── SEED HELPERS (reused from install-app logic) ─────────────────────────────
137
+
138
+ async function resolveAppId(slug: string): Promise<string | null> {
139
+ const { data } = await adminDb.from('apps').select('id').eq('slug', slug).single()
140
+ return data?.id || null
141
+ }
142
+
143
+ async function upsertSeedFile(
144
+ appId: string,
145
+ seedFile: string,
146
+ records: any[],
147
+ ): Promise<void> {
148
+ const tableMap: Record<string, string> = {
149
+ 'types.json': 'types',
150
+ 'link-types.json': 'link_types',
151
+ 'roles.json': 'roles',
152
+ 'pipelines.json': 'pipelines',
153
+ }
154
+ const table = tableMap[seedFile]
155
+ if (!table) return
156
+
157
+ const conflictMap: Record<string, string> = {
158
+ types: 'app_id,kind,slug',
159
+ link_types: 'app_id,slug',
160
+ roles: 'app_id,slug',
161
+ pipelines: 'app_id,name',
162
+ }
163
+
164
+ for (const record of records) {
165
+ await adminDb.from(table).upsert(
166
+ { ...record, app_id: appId },
167
+ { onConflict: conflictMap[table], ignoreDuplicates: false },
168
+ )
169
+ }
170
+ }
171
+
172
+ async function reapplySeed(slug: string, pkgDir: string): Promise<void> {
173
+ const appId = await resolveAppId(slug)
174
+ if (!appId) {
175
+ console.log(` ⚠️ App '${slug}' not found in DB — skipping seed re-apply`)
176
+ return
177
+ }
178
+
179
+ const seedDir = resolve(pkgDir, 'seed')
180
+ if (!existsSync(seedDir)) return
181
+
182
+ const SEED_ORDER = ['accounts.json', 'types.json', 'link-types.json', 'roles.json', 'pipelines.json', 'triggers.json']
183
+ const allFiles = readdirSync(seedDir).filter(f => f.endsWith('.json'))
184
+ const seedFiles = [
185
+ ...SEED_ORDER.filter(f => allFiles.includes(f)),
186
+ ...allFiles.filter(f => !SEED_ORDER.includes(f)),
187
+ ]
188
+
189
+ for (const file of seedFiles) {
190
+ try {
191
+ const records = JSON.parse(readFileSync(resolve(seedDir, file), 'utf8'))
192
+ if (Array.isArray(records)) {
193
+ await upsertSeedFile(appId, file, records)
194
+ console.log(` ✓ Re-seeded ${file}`)
195
+ }
196
+ } catch { /* ignore parse errors */ }
197
+ }
198
+ }
199
+
200
+ // ─── MAIN ─────────────────────────────────────────────────────────────────────
201
+
202
+ async function updateApp(slug: string, options: UpdateOptions): Promise<void> {
203
+ console.log(`\n🔄 Update check for '${slug}'...\n`)
204
+
205
+ // 1. Read installation record from DB
206
+ const { data: installation, error: dbErr } = await adminDb
207
+ .from('app_installations')
208
+ .select('app_slug, is_enabled, installed_version, package_name')
209
+ .eq('app_slug', slug)
210
+ .maybeSingle() as { data: InstallationRecord | null; error: any }
211
+
212
+ if (dbErr) {
213
+ console.error(` ❌ DB error: ${dbErr.message}`)
214
+ console.error(` Has migration 004_app_version_tracking.sql been applied?`)
215
+ process.exit(1)
216
+ }
217
+
218
+ if (!installation) {
219
+ console.error(` ❌ App '${slug}' is not installed (not found in app_installations).`)
220
+ console.error(` Run: spine-framework install-app spine-framework-${slug}`)
221
+ process.exit(1)
222
+ }
223
+
224
+ const packageName = installation.package_name || `spine-framework-${slug}`
225
+ const installedVersion = installation.installed_version
226
+
227
+ if (!installedVersion) {
228
+ console.log(` ⚠️ No installed_version recorded. Re-install to track version:`)
229
+ console.log(` spine-framework install-app ${packageName} --force`)
230
+ process.exit(1)
231
+ }
232
+
233
+ // 2. Fetch latest from npm registry
234
+ process.stdout.write(` Checking npm registry for ${packageName}... `)
235
+ const latestVersion = await fetchLatestVersion(packageName)
236
+ if (!latestVersion) {
237
+ console.log(`❌`)
238
+ console.error(` Could not reach npm registry. Check your internet connection.`)
239
+ process.exit(1)
240
+ }
241
+ console.log(`done`)
242
+
243
+ console.log(` Installed : v${installedVersion}`)
244
+ console.log(` Latest : v${latestVersion}`)
245
+
246
+ if (!isNewer(latestVersion, installedVersion)) {
247
+ console.log(`\n✅ Already up to date.\n`)
248
+ return
249
+ }
250
+
251
+ console.log(` Status : update available\n`)
252
+
253
+ if (!options.dryRun && !options.accept) {
254
+ console.log(` Run with --dry-run to see changelog and file diff.`)
255
+ console.log(` Run with --accept after merging to record the new version.\n`)
256
+ return
257
+ }
258
+
259
+ // ── --dry-run ──────────────────────────────────────────────────────────────
260
+ if (options.dryRun) {
261
+ console.log(`📥 Fetching ${packageName}@latest into node_modules (read-only reference)...`)
262
+ try {
263
+ execSync(`npm install ${packageName}@latest`, { cwd: PROJECT_ROOT, stdio: 'inherit' })
264
+ console.log(` ✓ Done\n`)
265
+ } catch (err: any) {
266
+ console.error(` ❌ npm install failed: ${err.message}`)
267
+ process.exit(1)
268
+ }
269
+
270
+ const pkgDir = resolve(PROJECT_ROOT, `node_modules/${packageName}`)
271
+ const localDir = resolve(PROJECT_ROOT, `custom/apps/${slug}`)
272
+
273
+ // Read changelog from new upstream version
274
+ const upstreamManifestPath = resolve(pkgDir, 'manifest.json')
275
+ let changelog: Array<{ version: string; notes: string[] }> = []
276
+ let newVersion = latestVersion
277
+ if (existsSync(upstreamManifestPath)) {
278
+ try {
279
+ const m = JSON.parse(readFileSync(upstreamManifestPath, 'utf8'))
280
+ changelog = m.changelog || []
281
+ newVersion = m.version || latestVersion
282
+ } catch { /* ignore */ }
283
+ }
284
+
285
+ // Print changelog entries between installed and latest
286
+ const relevantChangelog = changelogBetween(changelog, installedVersion, newVersion)
287
+ console.log(`📋 Changelog (v${installedVersion} → v${newVersion}):`)
288
+ if (relevantChangelog.length === 0) {
289
+ console.log(` (no changelog entries found between these versions)`)
290
+ } else {
291
+ for (const entry of relevantChangelog) {
292
+ console.log(`\n v${entry.version}:`)
293
+ for (const note of entry.notes) {
294
+ console.log(` • ${note}`)
295
+ }
296
+ }
297
+ }
298
+
299
+ // Read directories list from upstream manifest
300
+ let appDirs = ['pages', 'components', 'hooks', 'config', 'seed', 'api', 'functions', 'lib']
301
+ try {
302
+ const m = JSON.parse(readFileSync(upstreamManifestPath, 'utf8'))
303
+ if (Array.isArray(m.directories)) appDirs = m.directories.filter((d: string) => d !== 'migrations' && d !== 'tests')
304
+ } catch { /* use defaults */ }
305
+
306
+ const topFiles = ['index.tsx', 'manifest.json']
307
+ const diff = diffAppFiles(pkgDir, localDir, appDirs, topFiles)
308
+
309
+ console.log(`\n📁 File diff summary (upstream vs your version):`)
310
+ console.log(` Upstream (new) : node_modules/${packageName}/`)
311
+ console.log(` Your version : custom/apps/${slug}/\n`)
312
+
313
+ if (diff.changed.length === 0 && diff.added.length === 0 && diff.localOnly.length === 0) {
314
+ console.log(` (no file differences detected)`)
315
+ } else {
316
+ if (diff.changed.length > 0) {
317
+ console.log(` Modified (${diff.changed.length}) — review and merge:`)
318
+ diff.changed.forEach(f => console.log(` ~ ${f}`))
319
+ }
320
+ if (diff.added.length > 0) {
321
+ console.log(`\n New in upstream (${diff.added.length}) — copy to your version:`)
322
+ diff.added.forEach(f => console.log(` + ${f}`))
323
+ }
324
+ if (diff.localOnly.length > 0) {
325
+ console.log(`\n Only in your version (${diff.localOnly.length}) — your additions, keep them:`)
326
+ diff.localOnly.forEach(f => console.log(` → ${f}`))
327
+ }
328
+ }
329
+
330
+ console.log(`\n──────────────────────────────────────────────────────────────`)
331
+ console.log(` Next steps:`)
332
+ console.log(` 1. Review and merge changed files listed above`)
333
+ console.log(` 2. Run: spine-framework update-app ${slug} --accept`)
334
+ console.log(`──────────────────────────────────────────────────────────────\n`)
335
+ return
336
+ }
337
+
338
+ // ── --accept ──────────────────────────────────────────────────────────────
339
+ if (options.accept) {
340
+ console.log(`📝 Recording update...`)
341
+
342
+ const pkgDir = resolve(PROJECT_ROOT, `node_modules/${packageName}`)
343
+ if (!existsSync(pkgDir)) {
344
+ console.error(` ❌ ${packageName} not found in node_modules.`)
345
+ console.error(` Run --dry-run first to fetch the new version.`)
346
+ process.exit(1)
347
+ }
348
+
349
+ // Read new version from upstream manifest
350
+ let newVersion = latestVersion
351
+ const upstreamManifestPath = resolve(pkgDir, 'manifest.json')
352
+ if (existsSync(upstreamManifestPath)) {
353
+ try {
354
+ newVersion = JSON.parse(readFileSync(upstreamManifestPath, 'utf8')).version || latestVersion
355
+ } catch { /* use latestVersion */ }
356
+ }
357
+
358
+ const { error: updateErr } = await adminDb
359
+ .from('app_installations')
360
+ .update({
361
+ installed_version: newVersion,
362
+ updated_at: new Date().toISOString(),
363
+ })
364
+ .eq('app_slug', slug)
365
+
366
+ if (updateErr) {
367
+ console.error(` ❌ Failed to update installed_version: ${updateErr.message}`)
368
+ process.exit(1)
369
+ }
370
+
371
+ console.log(` ✓ installed_version updated to v${newVersion}`)
372
+
373
+ // Re-apply seed (idempotent)
374
+ console.log(`\n🌱 Re-applying seed data (idempotent)...`)
375
+ await reapplySeed(slug, pkgDir)
376
+
377
+ console.log(`\n✅ Update recorded for '${slug}' → v${newVersion}`)
378
+ console.log(` Run 'spine-framework status' to verify.\n`)
379
+ }
380
+ }
381
+
382
+ export function registerUpdateAppCommands(program: Command) {
383
+ program
384
+ .command('update-app <slug>')
385
+ .description('Check for app updates and assist with agentic-IDE-driven merge')
386
+ .option('--dry-run', 'Fetch latest, show changelog + file diff (no writes to custom/apps/)', false)
387
+ .option('--accept', 'Record new version in DB after agent has merged files', false)
388
+ .action(async (slug, opts) => {
389
+ try {
390
+ await updateApp(slug, opts)
391
+ } catch (err: any) {
392
+ console.error('Error:', err.message)
393
+ if (process.env.SPINE_CLI_DEBUG) console.error(err.stack)
394
+ process.exit(1)
395
+ }
396
+ })
397
+ }
@@ -21,7 +21,10 @@
21
21
  * | migrations | commands/migrations.ts |
22
22
  * | init | commands/init.ts |
23
23
  * | install-app | commands/install-app.ts |
24
+ * | update-app | commands/update-app.ts |
25
+ * | publish | commands/publish.ts |
24
26
  * | status | commands/status.ts |
27
+ * | claim | commands/claim.ts |
25
28
  * | uninstall | commands/uninstall-app.ts |
26
29
  *
27
30
  * **Usage:**
@@ -75,7 +78,10 @@ if (!noDbCommands.includes(subcommand)) {
75
78
  { registerGenerateCommands },
76
79
  { registerCreateAppCommands },
77
80
  { registerInstallAppCommands },
81
+ { registerUpdateAppCommands },
82
+ { registerPublishCommands },
78
83
  { registerStatusCommands },
84
+ { registerClaimCommands },
79
85
  { registerUninstallAppCommands },
80
86
  ] = await Promise.all([
81
87
  import('./commands/auth.ts'),
@@ -90,7 +96,10 @@ if (!noDbCommands.includes(subcommand)) {
90
96
  import('./commands/generate.ts'),
91
97
  import('./commands/create-app.ts'),
92
98
  import('./commands/install-app.ts'),
99
+ import('./commands/update-app.ts'),
100
+ import('./commands/publish.ts'),
93
101
  import('./commands/status.ts'),
102
+ import('./commands/claim.ts'),
94
103
  import('./commands/uninstall-app.ts'),
95
104
  ])
96
105
 
@@ -106,7 +115,10 @@ if (!noDbCommands.includes(subcommand)) {
106
115
  registerGenerateCommands(program)
107
116
  registerCreateAppCommands(program)
108
117
  registerInstallAppCommands(program)
118
+ registerUpdateAppCommands(program)
119
+ registerPublishCommands(program)
109
120
  registerStatusCommands(program)
121
+ registerClaimCommands(program)
110
122
  registerUninstallAppCommands(program)
111
123
  }
112
124
 
@@ -139,8 +139,7 @@ CREATE TABLE IF NOT EXISTS public.roles (
139
139
  app_id uuid,
140
140
  account_id uuid,
141
141
  created_at timestamptz DEFAULT now(),
142
- updated_at timestamptz DEFAULT now(),
143
- UNIQUE (app_id, slug)
142
+ updated_at timestamptz DEFAULT now()
144
143
  );
145
144
 
146
145
  -- apps (no dependencies on other custom tables)
@@ -764,8 +763,7 @@ CREATE TABLE IF NOT EXISTS public.app_installations (
764
763
  app_slug varchar NOT NULL,
765
764
  is_enabled boolean DEFAULT true,
766
765
  installed_at timestamptz DEFAULT now(),
767
- updated_at timestamptz DEFAULT now(),
768
- UNIQUE (account_id, app_slug)
766
+ updated_at timestamptz DEFAULT now()
769
767
  );
770
768
 
771
769
  -- ---------------------------------------------------------------------------
@@ -795,6 +793,14 @@ CREATE INDEX IF NOT EXISTS idx_api_keys_key_value ON public.api_keys (key_valu
795
793
 
796
794
  CREATE INDEX IF NOT EXISTS idx_app_installations_account ON public.app_installations (account_id) WHERE (is_enabled = true);
797
795
  CREATE INDEX IF NOT EXISTS idx_app_installations_slug ON public.app_installations (app_slug) WHERE (is_enabled = true);
796
+ -- Dedup: system installs (account_id IS NULL) — one row per app slug
797
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_app_installations_slug_no_account
798
+ ON public.app_installations (app_slug)
799
+ WHERE account_id IS NULL;
800
+ -- Dedup: tenant installs (account_id IS NOT NULL) — one row per (slug, account)
801
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_app_installations_slug_with_account
802
+ ON public.app_installations (app_slug, account_id)
803
+ WHERE account_id IS NOT NULL;
798
804
 
799
805
  CREATE INDEX IF NOT EXISTS idx_apps_owner ON public.apps (owner_account_id);
800
806
  CREATE INDEX IF NOT EXISTS idx_apps_slug ON public.apps (slug);
@@ -848,6 +854,14 @@ CREATE INDEX IF NOT EXISTS idx_prompt_configs_app ON public.prompt_configs (
848
854
  CREATE INDEX IF NOT EXISTS idx_prompt_configs_slug ON public.prompt_configs (slug);
849
855
 
850
856
  CREATE INDEX IF NOT EXISTS idx_roles_slug ON public.roles (slug);
857
+ -- Dedup: system roles (app_id IS NULL) — one row per slug globally
858
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_roles_slug_no_app
859
+ ON public.roles (slug)
860
+ WHERE app_id IS NULL;
861
+ -- Dedup: app-scoped roles (app_id IS NOT NULL) — one row per (app_id, slug)
862
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_roles_slug_with_app
863
+ ON public.roles (app_id, slug)
864
+ WHERE app_id IS NOT NULL;
851
865
 
852
866
  CREATE INDEX IF NOT EXISTS idx_se_account ON public.schedule_executions (account_id);
853
867
  CREATE INDEX IF NOT EXISTS idx_se_schedule ON public.schedule_executions (schedule_id);
@@ -0,0 +1,8 @@
1
+ -- 004_app_version_tracking.sql
2
+ -- Adds version tracking columns to app_installations so the CLI and
3
+ -- agentic IDE can detect drift between the installed version and the
4
+ -- latest published npm package version.
5
+
6
+ ALTER TABLE public.app_installations
7
+ ADD COLUMN IF NOT EXISTS installed_version varchar,
8
+ ADD COLUMN IF NOT EXISTS package_name varchar;
@@ -8,7 +8,7 @@ import { NotFoundPage } from './pages/NotFoundPage'
8
8
  import { DashboardPage } from './pages/DashboardPage'
9
9
  import { AppWrapper } from './components/AppWrapper'
10
10
  import { AppsRegistryProvider, useAppsRegistry } from './contexts/AppContext'
11
- import { CustomAppLoader } from './components/CustomAppLoader'
11
+ import { CustomAppLoader, getInstalledAppSlugs } from './components/CustomAppLoader'
12
12
  import { GenericAppShell } from './components/app-shell/GenericAppShell'
13
13
  import { APIPage } from './pages/spine-framework/APIPage'
14
14
  import { CLIPage } from './pages/spine-framework/CLIPage'
@@ -58,9 +58,14 @@ function AuthenticatedRouter() {
58
58
  )
59
59
  }
60
60
 
61
+ // Filter to apps that are physically present in this build (custom renderer only —
62
+ // generic apps have no module to load so they're always allowed through).
63
+ const installedSlugs = getInstalledAppSlugs()
64
+
61
65
  // Sort: explicit prefixes first, root (/) last
62
66
  const sorted = [...apps]
63
67
  .filter(app => app.slug !== 'spine-framework' && !app.route_prefix?.startsWith('/spine-framework'))
68
+ .filter(app => app.renderer !== 'custom' || installedSlugs.has(app.slug))
64
69
  .sort((a, b) => {
65
70
  if (a.route_prefix === '/') return 1
66
71
  if (b.route_prefix === '/') return -1
@@ -60,6 +60,23 @@ class AppErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState>
60
60
  const customAppModules = import.meta.glob('../../custom/apps/*/index.tsx')
61
61
  const coreAppModules = import.meta.glob('../apps/*/index.tsx')
62
62
 
63
+ // Extract app slug from glob key path (e.g., '.../apps/xyz/index.tsx' → 'xyz')
64
+ function slugFromKey(key: string): string {
65
+ return key.split('/').slice(-2)[0]
66
+ }
67
+
68
+ /**
69
+ * Returns the set of app slugs that are physically present in this build.
70
+ * Used by App.tsx to filter DB app records down to only what's actually installed,
71
+ * preventing route registration for apps that exist in the DB but not this deployment.
72
+ */
73
+ export function getInstalledAppSlugs(): Set<string> {
74
+ const slugs = new Set<string>()
75
+ Object.keys(customAppModules).forEach(k => slugs.add(slugFromKey(k)))
76
+ Object.keys(coreAppModules).forEach(k => slugs.add(slugFromKey(k)))
77
+ return slugs
78
+ }
79
+
63
80
  // Module-level cache for lazy components to avoid re-creating on each render
64
81
  const appModuleCache = new Map<string, React.LazyExoticComponent<React.ComponentType>>()
65
82
 
@@ -10,7 +10,7 @@ const projectRoot = resolve(pkgRoot, '../..')
10
10
 
11
11
  // Handle no-args case here — before spawning tsx or loading db.ts
12
12
  if (process.argv.length === 2) {
13
- require(resolve(__dirname, 'welcome.cjs'))
13
+ require(resolve(__dirname, 'welcome.js'))
14
14
  process.exit(0)
15
15
  }
16
16
 
@@ -12,7 +12,7 @@ const lines = [
12
12
  '│ ███████╗██████╔╝██║██╔██╗ ██║█████╗ │',
13
13
  '│ ╚════██║██╔═══╝ ██║██║╚██╗██║██╔══╝ │',
14
14
  '│ ███████║██║ ██║██║ ╚████║███████╗ │',
15
- '│ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝ Framework v0.1 │',
15
+ '│ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝ Framework v0.2 │',
16
16
  '│ │',
17
17
  '╰──────────────────────────────────────────────────────────────╯',
18
18
  '',
@@ -38,7 +38,7 @@ const lines = [
38
38
  ' spine-framework install-app <app-slug> # optional apps',
39
39
  ' npm run assemble && netlify dev # start dev server',
40
40
  '',
41
- ' Docs: https://github.com/art-mojo-admin/spine',
41
+ ' Docs: https://github.com/spine-framework/spine-framework',
42
42
  '',
43
43
  ]
44
44
 
@@ -0,0 +1,56 @@
1
+ /**
2
+ * @module cli/commands/claim
3
+ * @audience installer
4
+ * @layer cli
5
+ * @stability stable
6
+ *
7
+ * `spine-framework claim` — Claim framework installation via Spine Portal.
8
+ *
9
+ * Engagement-based claim system that offers contextual prompts based on
10
+ * user activity. Tracks engagement signals and triggers claim prompts
11
+ * at value-realization moments.
12
+ *
13
+ * **Usage:**
14
+ * ```bash
15
+ * spine-framework claim # Interactive claim flow
16
+ * spine-framework claim --status # Show claim status only
17
+ * spine-framework claim --token <t> # Headless claim with token
18
+ * ```
19
+ */
20
+ import type { Command } from 'commander';
21
+ import { type EngagementData, type ClaimPrompt, type EngagementSignalType } from '../../config/signals.ts';
22
+ export interface SerialData {
23
+ serial_number: string;
24
+ salt: string;
25
+ created_at: string;
26
+ framework_version: string;
27
+ account_id: string | null;
28
+ machine_fingerprint: string;
29
+ }
30
+ export declare function ensureSerialData(): SerialData;
31
+ export declare function readEngagementData(): EngagementData | null;
32
+ export declare function writeEngagementData(data: EngagementData): void;
33
+ export declare function recordEngagementSignal(signalType: EngagementSignalType): void;
34
+ export declare function shouldOfferClaim(): ClaimPrompt | null;
35
+ export interface FrameworkInstallSignal {
36
+ signal_type: 'framework_install';
37
+ install_serial: string;
38
+ package: string;
39
+ version: string;
40
+ anonymous_id: string;
41
+ machine_fingerprint: string;
42
+ platform: string;
43
+ timestamp: string;
44
+ }
45
+ export interface AppInstallSignal {
46
+ signal_type: 'app_install';
47
+ package: string;
48
+ version: string;
49
+ framework_install_serial: string;
50
+ app_serial: string;
51
+ timestamp: string;
52
+ }
53
+ export declare function sendFrameworkInstallSignal(): Promise<void>;
54
+ export declare function sendAppInstallSignal(packageName: string, version: string): Promise<void>;
55
+ export declare function registerClaimCommands(program: Command): void;
56
+ //# sourceMappingURL=claim.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claim.d.ts","sourceRoot":"","sources":["../../../.framework/cli/commands/claim.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAKxC,OAAO,EAAkD,KAAK,cAAc,EAAE,KAAK,WAAW,EAAE,KAAK,oBAAoB,EAAE,MAAM,yBAAyB,CAAA;AAS1J,MAAM,WAAW,UAAU;IACzB,aAAa,EAAE,MAAM,CAAA;IACrB,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,MAAM,CAAA;IAClB,iBAAiB,EAAE,MAAM,CAAA;IACzB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,mBAAmB,EAAE,MAAM,CAAA;CAC5B;AAiBD,wBAAgB,gBAAgB,IAAI,UAAU,CA6B7C;AAeD,wBAAgB,kBAAkB,IAAI,cAAc,GAAG,IAAI,CAO1D;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,cAAc,GAAG,IAAI,CAK9D;AAED,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,oBAAoB,GAAG,IAAI,CAgC7E;AAED,wBAAgB,gBAAgB,IAAI,WAAW,GAAG,IAAI,CA8BrD;AAID,MAAM,WAAW,sBAAsB;IACrC,WAAW,EAAE,mBAAmB,CAAA;IAChC,cAAc,EAAE,MAAM,CAAA;IACtB,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,YAAY,EAAE,MAAM,CAAA;IACpB,mBAAmB,EAAE,MAAM,CAAA;IAC3B,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,aAAa,CAAA;IAC1B,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,wBAAwB,EAAE,MAAM,CAAA;IAChC,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,wBAAsB,0BAA0B,IAAI,OAAO,CAAC,IAAI,CAAC,CAuBhE;AAED,wBAAsB,oBAAoB,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAqB9F;AAkLD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAe5D"}
@@ -1 +1 @@
1
- {"version":3,"file":"install-app.d.ts","sourceRoot":"","sources":["../../../.framework/cli/commands/install-app.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAugBxC,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,OAAO,QAgB1D"}
1
+ {"version":3,"file":"install-app.d.ts","sourceRoot":"","sources":["../../../.framework/cli/commands/install-app.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAmkBxC,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,OAAO,QAgB1D"}
@@ -1 +1 @@
1
- {"version":3,"file":"migrate.d.ts","sourceRoot":"","sources":["../../../.framework/cli/commands/migrate.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAuGxC,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,QAevD"}
1
+ {"version":3,"file":"migrate.d.ts","sourceRoot":"","sources":["../../../.framework/cli/commands/migrate.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAmHxC,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,QAevD"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @module cli/commands/publish
3
+ * @audience developer
4
+ * @layer cli
5
+ * @stability stable
6
+ *
7
+ * `spine-framework publish` — Bump version, write changelog, publish to npm, commit.
8
+ *
9
+ * Run from the spine-ia coding repo. Handles both the framework package and
10
+ * individual app packages (cortex, portal, etc.).
11
+ *
12
+ * **Usage:**
13
+ * ```bash
14
+ * spine-framework publish # publish framework (patch bump)
15
+ * spine-framework publish --app cortex # publish cortex app (patch bump)
16
+ * spine-framework publish --app cortex --bump minor
17
+ * spine-framework publish --app cortex --bump patch --notes "Fix sidebar nav"
18
+ * spine-framework publish --app cortex --ci --notes "Fix sidebar nav" --bump patch
19
+ * ```
20
+ */
21
+ import type { Command } from 'commander';
22
+ export declare function registerPublishCommands(program: Command): void;
23
+ //# sourceMappingURL=publish.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"publish.d.ts","sourceRoot":"","sources":["../../../.framework/cli/commands/publish.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAsJxC,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,QAkBvD"}