deepspace 0.5.5 → 0.5.7

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.
@@ -24,6 +24,8 @@
24
24
  "protected": true
25
25
  },
26
26
  "requires": [],
27
- "instructions": [],
27
+ "instructions": [
28
+ "This layers an admin-only /admin panel on top of the /settings page that every scaffold already ships (that shared src/schemas/admin-schema.ts is why it reports \"Exists (skipped)\" here). Add a nav entry for /admin; keep /settings for per-user settings."
29
+ ],
28
30
  "patterns": []
29
31
  }
@@ -27,11 +27,13 @@
27
27
  "@codemirror/lang-javascript": "^6.2.2",
28
28
  "@codemirror/theme-one-dark": "^6.1.2",
29
29
  "papaparse": "^5.4.1",
30
- "@types/papaparse": "^5.3.0",
31
30
  "docx-preview": "^0.3.5",
32
31
  "exceljs": "^4.4.0",
33
32
  "pdfjs-dist": "^4.7.76"
34
33
  },
34
+ "devDependencies": {
35
+ "@types/papaparse": "^5.3.0"
36
+ },
35
37
  "instructions": [],
36
38
  "patterns": [
37
39
  "useMimeTypeDetection() → { detectMimeType, getFileCategory, getExtension, canPreview }",
@@ -105,7 +105,7 @@
105
105
  },
106
106
  "requires": [],
107
107
  "instructions": [
108
- "Optionally add a second route for /chat-multi -> ChatMultiPage"
108
+ "Optionally add a nav entry for /chat-multi (the multi-channel view). Its route is created automatically from the bundled page file — only the nav link is manual."
109
109
  ],
110
110
  "patterns": []
111
111
  }
@@ -12,11 +12,12 @@
12
12
  ],
13
13
  "requires": [],
14
14
  "instructions": [
15
- "Run npx deepspace add search-bar before building any search UI that filters records or displays results; it only installs src/components/search/SearchOverlay.tsx and does not add app routes.",
16
- "Prefer InlineSearch + useSearchIndex for simple page, feed, table, list, picker, or RecordRoom filtering instead of composing SearchInput manually.",
17
- "Use the raw SearchInput primitive only when you need a bare input with no result list, ranking, keyboard navigation, empty state, or selection handling.",
18
- "Use InlineSearch when search belongs to a specific page section and results should appear beneath that input.",
19
- "Use SearchOverlay when search is temporary, global, command-like, includes recent searches, submits partial text to a search-results page, or should interrupt the current workflow.",
15
+ "Installs src/components/search/SearchOverlay.tsx (exports InlineSearch, SearchOverlay, and useSearchIndex). It adds no route — import what you need into your own page.",
16
+ "Use InlineSearch + useSearchIndex for in-page filtering (lists, feeds, tables, pickers, RecordRoom records); use SearchOverlay for global/command-style search.",
17
+ "Use the raw SearchInput primitive only for a bare input with no result list, ranking, keyboard navigation, or selection handling."
18
+ ],
19
+ "agentNotes": [
20
+ "Use InlineSearch when search belongs to a specific page section and results should appear beneath that input; use SearchOverlay when search is temporary, global, command-like, includes recent searches, submits partial text to a search-results page, or should interrupt the current workflow.",
20
21
  "Pass onSearchSubmit when pressing Enter should navigate to or render a full search-results page for the typed query; result clicks still call onSelect.",
21
22
  "Ask the user which placement they prefer only when both overlay and inline search are plausible and the page context does not make the placement obvious.",
22
23
  "Use SearchOverlay's recentStorageKey, maxRecentItems, and showRecentSearches props when recent selections or submitted text queries should appear before the user types; keep recent searches off inline controls unless the surrounding UI has room for them.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepspace",
3
- "version": "0.5.5",
3
+ "version": "0.5.7",
4
4
  "engines": {
5
5
  "node": ">=22.15.0"
6
6
  },
@@ -33,7 +33,8 @@
33
33
  "files": [
34
34
  "dist",
35
35
  "features",
36
- "scripts"
36
+ "scripts",
37
+ "CHANGELOG.md"
37
38
  ],
38
39
  "scripts": {
39
40
  "build": "tsup --clean",
@@ -126,5 +127,14 @@
126
127
  "collaborative",
127
128
  "sdk"
128
129
  ],
129
- "license": "MIT"
130
+ "license": "Apache-2.0",
131
+ "homepage": "https://deep.space",
132
+ "repository": {
133
+ "type": "git",
134
+ "url": "git+https://github.com/deepdotspace/deepspace.git",
135
+ "directory": "packages/deepspace"
136
+ },
137
+ "bugs": {
138
+ "url": "https://github.com/deepdotspace/deepspace/issues"
139
+ }
130
140
  }
@@ -12,6 +12,8 @@
12
12
  const fs = require('fs')
13
13
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- CommonJS (.cjs) module: require is required
14
14
  const path = require('path')
15
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- CommonJS (.cjs) module: require is required
16
+ const { spawnSync } = require('child_process')
15
17
 
16
18
  // ---------------------------------------------------------------------------
17
19
  // Environment-aware features path resolution
@@ -90,6 +92,107 @@ function groupByCategory(features) {
90
92
  return groups
91
93
  }
92
94
 
95
+ // First feature id in display order — used for a never-stale help example.
96
+ function firstFeatureId(groups) {
97
+ for (const category of CATEGORY_ORDER) {
98
+ const list = groups.get(category)
99
+ if (list && list.length > 0) return list[0].id
100
+ }
101
+ return null
102
+ }
103
+
104
+ // Small edit distance for "did you mean" suggestions on a typo'd feature id.
105
+ function levenshtein(a, b) {
106
+ const m = a.length
107
+ const n = b.length
108
+ const row = Array.from({ length: n + 1 }, (_, i) => i)
109
+ for (let i = 1; i <= m; i++) {
110
+ let prev = row[0]
111
+ row[0] = i
112
+ for (let j = 1; j <= n; j++) {
113
+ const tmp = row[j]
114
+ row[j] = Math.min(
115
+ row[j] + 1,
116
+ row[j - 1] + 1,
117
+ prev + (a[i - 1] === b[j - 1] ? 0 : 1),
118
+ )
119
+ prev = tmp
120
+ }
121
+ }
122
+ return row[n]
123
+ }
124
+
125
+ // Closest visible feature id to a typo, or null when nothing is close enough.
126
+ function suggestFeature(featureId) {
127
+ let best = null
128
+ let bestD = Infinity
129
+ for (const f of listFeatures()) {
130
+ const d = levenshtein(featureId, f.id)
131
+ if (d < bestD) {
132
+ bestD = d
133
+ best = f.id
134
+ }
135
+ }
136
+ const threshold = Math.max(2, Math.floor(featureId.length / 3))
137
+ return best && bestD <= threshold ? best : null
138
+ }
139
+
140
+ // Print a friendly unknown-feature error (with a suggestion) and exit.
141
+ function unknownFeature(featureId) {
142
+ console.error(`\nError: Unknown feature: ${featureId}`)
143
+ const guess = suggestFeature(featureId)
144
+ if (guess) console.error(`Did you mean "${guess}"?`)
145
+ console.error('Run `deepspace add --list` to see available features')
146
+ process.exit(1)
147
+ }
148
+
149
+ // Detect the target app's package manager from its lockfile (defaults to npm).
150
+ function detectPackageManager(targetDir) {
151
+ const has = (f) => fs.existsSync(path.join(targetDir, f))
152
+ if (has('pnpm-lock.yaml')) return 'pnpm'
153
+ if (has('yarn.lock')) return 'yarn'
154
+ if (has('bun.lockb') || has('bun.lock')) return 'bun'
155
+ return 'npm'
156
+ }
157
+
158
+ // The nav features are mutually exclusive (competing navigation). Derive which
159
+ // are installed from the catalog itself — each nav feature's first installed
160
+ // file is its signature — so this never drifts from what a feature ships.
161
+ function installedNavFeatures(targetDir) {
162
+ return listFeatures()
163
+ .filter((f) => f.category === 'nav' && Array.isArray(f.files) && f.files.length > 0)
164
+ .filter((f) =>
165
+ fs.existsSync(path.join(targetDir, resolveDestPath(f.files[0].dest, f.route))),
166
+ )
167
+ .map((f) => f.id)
168
+ }
169
+
170
+ function warnNavConflicts(config, targetDir) {
171
+ if (config.category !== 'nav') return
172
+ const present = installedNavFeatures(targetDir)
173
+ if (present.length > 1) {
174
+ console.log(`\n⚠ Multiple navigation features present (${present.join(', ')}).`)
175
+ console.log(
176
+ ' sidebar, topbar, and tree are mutually exclusive — keep one and remove the',
177
+ )
178
+ console.log(' others to avoid competing navigation in your layout.')
179
+ }
180
+ }
181
+
182
+ // Always-printed footer so every install ends with a clear "what now".
183
+ function printNextSteps(config, deps) {
184
+ console.log('\nNext steps:')
185
+ let n = 1
186
+ if (deps.added.length > 0 && !deps.installRan) {
187
+ console.log(` ${n++}. Install new dependencies: ${deps.pm} install`)
188
+ console.log(' (or re-run this command with --install to do it for you)')
189
+ }
190
+ console.log(` ${n++}. Start the dev server: deepspace dev`)
191
+ if (config.route && config.route.path) {
192
+ console.log(` ${n++}. Open ${config.route.path} once the dev server is up`)
193
+ }
194
+ }
195
+
93
196
  function copyFile(src, dest) {
94
197
  const destDir = path.dirname(dest)
95
198
  if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true })
@@ -437,7 +540,9 @@ function printRouteInstructions(route, config) {
437
540
  * but easy to get wrong; deferring to the user keeps the contract
438
541
  * narrow and respects whatever lockfile setup they have.
439
542
  */
440
- function integrateDependencies(config, targetDir) {
543
+ function integrateDependencies(config, targetDir, options = {}) {
544
+ const result = { added: [], installRan: false, pm: detectPackageManager(targetDir) }
545
+
441
546
  const sections = [
442
547
  { key: 'dependencies', label: 'dependencies' },
443
548
  { key: 'devDependencies', label: 'devDependencies' },
@@ -445,12 +550,12 @@ function integrateDependencies(config, targetDir) {
445
550
 
446
551
  // Skip the work if nothing is declared.
447
552
  const hasAny = sections.some((s) => config[s.key] && Object.keys(config[s.key]).length > 0)
448
- if (!hasAny) return
553
+ if (!hasAny) return result
449
554
 
450
555
  const pkgPath = path.join(targetDir, 'package.json')
451
556
  if (!fs.existsSync(pkgPath)) {
452
557
  console.log(' Could not patch package.json (file not found)')
453
- return
558
+ return result
454
559
  }
455
560
 
456
561
  let pkg
@@ -458,7 +563,7 @@ function integrateDependencies(config, targetDir) {
458
563
  pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
459
564
  } catch (err) {
460
565
  console.log(` Could not patch package.json (parse error: ${err.message})`)
461
- return
566
+ return result
462
567
  }
463
568
 
464
569
  const added = []
@@ -469,8 +574,13 @@ function integrateDependencies(config, targetDir) {
469
574
  if (!declared) continue
470
575
  pkg[key] = pkg[key] || {}
471
576
  for (const [name, version] of Object.entries(declared)) {
472
- if (pkg[key][name]) {
473
- skipped.push(name)
577
+ // Skip if already declared in this OR the sibling section — never let a
578
+ // package land in both dependencies and devDependencies (e.g. when a
579
+ // feature moves a dep between sections across versions). Don't report a
580
+ // name we added moments ago (from the sibling section of this same
581
+ // config) as "already present" — that would list it as both added+skipped.
582
+ if (pkg.dependencies?.[name] || pkg.devDependencies?.[name]) {
583
+ if (!added.includes(name)) skipped.push(name)
474
584
  continue
475
585
  }
476
586
  pkg[key][name] = version
@@ -480,7 +590,7 @@ function integrateDependencies(config, targetDir) {
480
590
 
481
591
  if (added.length === 0) {
482
592
  console.log(' Dependencies already present')
483
- return
593
+ return result
484
594
  }
485
595
 
486
596
  // Re-write with two-space indent (Node's de-facto default; matches
@@ -490,7 +600,31 @@ function integrateDependencies(config, targetDir) {
490
600
  if (skipped.length > 0) {
491
601
  console.log(` Already present (kept your version): ${skipped.join(', ')}`)
492
602
  }
493
- console.log(' ➜ Run `npm install` (or your package manager) before starting the dev server')
603
+ result.added = added
604
+
605
+ // Opt-in (--install): run the detected package manager for the user. By
606
+ // default we only patch package.json and let the "Next steps" footer tell
607
+ // them to install, so we never guess wrong about their PM/lockfile.
608
+ if (options.runInstall) {
609
+ console.log(`\n Installing dependencies with ${result.pm}...`)
610
+ // shell:true on Windows, where npm/pnpm/yarn/bun are .cmd shims that
611
+ // spawnSync can't exec directly.
612
+ const res = spawnSync(result.pm, ['install'], {
613
+ cwd: targetDir,
614
+ stdio: 'inherit',
615
+ shell: process.platform === 'win32',
616
+ })
617
+ if (res.error) {
618
+ const why = res.error.code === 'ENOENT' ? `${result.pm} not found on PATH` : res.error.message
619
+ console.log(` Could not launch ${result.pm} (${why}) — run "${result.pm} install" manually.`)
620
+ } else if (res.status === 0) {
621
+ result.installRan = true
622
+ } else {
623
+ console.log(` ${result.pm} install did not complete — run "${result.pm} install" manually.`)
624
+ }
625
+ }
626
+
627
+ return result
494
628
  }
495
629
 
496
630
  // ---------------------------------------------------------------------------
@@ -513,21 +647,25 @@ function printPostInstallInstructions(config) {
513
647
  // ---------------------------------------------------------------------------
514
648
 
515
649
  function main() {
516
- const args = process.argv.slice(2)
650
+ // Pull the `--install` flag out first so it can appear anywhere; the rest of
651
+ // main() treats args[0]/args[1] as feature + dir positionally.
652
+ const runInstall = process.argv.slice(2).includes('--install')
653
+ const args = process.argv.slice(2).filter((a) => a !== '--install')
517
654
 
518
655
  // --help
519
656
  if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
520
- console.log('\nFeature Installation Script\n')
521
- console.log('Usage: node add-feature.js <feature-id> <app-dir>\n')
522
- console.log('Commands:')
523
- console.log(' <feature-id> <dir> Install a feature into app dir')
524
- console.log(' --list, -l List all available features')
525
- console.log(' --info <id> Show detailed info about a feature')
526
- console.log(' --help, -h Show this help\n')
527
-
528
657
  const features = listFeatures()
529
658
  const groups = groupByCategory(features)
530
659
 
660
+ console.log('\nAdd a feature to your DeepSpace app\n')
661
+ console.log('Usage: deepspace add <feature> [dir]\n')
662
+ console.log('Commands:')
663
+ console.log(' <feature> [dir] Install a feature (dir defaults to .)')
664
+ console.log(' --list, -l List all available features')
665
+ console.log(' --info <feature> Show detailed info about a feature')
666
+ console.log(' --install Run your package manager after adding deps')
667
+ console.log(' --help, -h Show this help')
668
+
531
669
  for (const category of CATEGORY_ORDER) {
532
670
  const categoryFeatures = groups.get(category)
533
671
  if (categoryFeatures && categoryFeatures.length > 0) {
@@ -538,9 +676,11 @@ function main() {
538
676
  }
539
677
  }
540
678
 
679
+ const example = firstFeatureId(groups) || '<feature>'
541
680
  console.log('\nExamples:')
542
- console.log(' add feature items-crud .')
543
- console.log(' node add-feature.js items-crud ../my-app')
681
+ console.log(' deepspace add --list')
682
+ console.log(` deepspace add ${example}`)
683
+ console.log(` deepspace add ${example} ./my-app`)
544
684
  process.exit(0)
545
685
  }
546
686
 
@@ -562,7 +702,7 @@ function main() {
562
702
  }
563
703
  }
564
704
 
565
- console.log('Use: npx deepspace add <feature-id>')
705
+ console.log('Use: deepspace add <feature>')
566
706
  process.exit(0)
567
707
  }
568
708
 
@@ -574,11 +714,7 @@ function main() {
574
714
  process.exit(1)
575
715
  }
576
716
  const config = loadFeatureConfig(featureId)
577
- if (!config) {
578
- console.error(`\nError: Unknown feature: ${featureId}`)
579
- console.error('Use --list to see available features')
580
- process.exit(1)
581
- }
717
+ if (!config) unknownFeature(featureId)
582
718
 
583
719
  console.log(`\n${config.name} (${config.id})`)
584
720
  if (config.category) {
@@ -617,16 +753,12 @@ function main() {
617
753
 
618
754
  if (!targetDir) {
619
755
  console.error('\nError: Please specify an app directory')
620
- console.error('Usage: node add-feature.js <feature-id> <app-dir>')
756
+ console.error('Usage: deepspace add <feature> [dir]')
621
757
  process.exit(1)
622
758
  }
623
759
 
624
760
  const config = loadFeatureConfig(featureId)
625
- if (!config) {
626
- console.error(`\nError: Unknown feature: ${featureId}`)
627
- console.error('Use --list to see available features')
628
- process.exit(1)
629
- }
761
+ if (!config) unknownFeature(featureId)
630
762
 
631
763
  if (!fs.existsSync(targetDir)) {
632
764
  console.error(`\nError: Target directory not found: ${targetDir}`)
@@ -650,11 +782,11 @@ function main() {
650
782
  // Add any new npm dependencies the feature declares to the target's
651
783
  // package.json. Without this, copied source files import packages
652
784
  // that don't exist in node_modules and the next `vite dev` blows up
653
- // with a hard-to-diagnose "Failed to resolve import" error. We patch
654
- // package.json (rather than running `npm install` ourselves) so the
655
- // user keeps control over their package manager + lockfile, and we
656
- // print a single clear instruction when changes were made.
657
- integrateDependencies(config, targetDir)
785
+ // with a hard-to-diagnose "Failed to resolve import" error. By default we
786
+ // only patch package.json (the "Next steps" footer tells the user to
787
+ // install) so we never guess wrong about their package manager + lockfile;
788
+ // `--install` opts into running it for them.
789
+ const deps = integrateDependencies(config, targetDir, { runInstall })
658
790
 
659
791
  // Print any remaining manual wiring instructions
660
792
  printPostInstallInstructions(config)
@@ -664,6 +796,12 @@ function main() {
664
796
  config.patterns.forEach((p) => console.log(`- ${p}`))
665
797
  console.log('')
666
798
  }
799
+
800
+ // Warn when installing a nav feature alongside another (they're exclusive).
801
+ warnNavConflicts(config, targetDir)
802
+
803
+ // Always close with concrete next steps.
804
+ printNextSteps(config, deps)
667
805
  }
668
806
 
669
807
  main()
package/dist/testing.d.ts DELETED
@@ -1,115 +0,0 @@
1
- import * as _playwright_test from '@playwright/test';
2
- import { BrowserContext, Page, Browser } from '@playwright/test';
3
- export { expect } from '@playwright/test';
4
-
5
- interface MultiplayerUser {
6
- context: BrowserContext;
7
- page: Page;
8
- email: string;
9
- name: string;
10
- /** Test account user ID, if known from the accounts registry. */
11
- userId?: string;
12
- }
13
- /**
14
- * Fixture function signature. Pass either a count (`users(2)`) or an
15
- * array of account names (`users(['Alice', 'Bob'])`). Returns one entry
16
- * per requested user.
17
- */
18
- type UsersFixture = (selector: number | string[], options?: {
19
- label?: string;
20
- }) => Promise<MultiplayerUser[]>;
21
- interface MultiplayerFixtures {
22
- users: UsersFixture;
23
- }
24
- declare const test: _playwright_test.TestType<_playwright_test.PlaywrightTestArgs & _playwright_test.PlaywrightTestOptions & MultiplayerFixtures, _playwright_test.PlaywrightWorkerArgs & _playwright_test.PlaywrightWorkerOptions>;
25
-
26
- /**
27
- * Account-credential plumbing for the testing module.
28
- *
29
- * Reads test accounts from `~/.deepspace/test-accounts.json` (the same
30
- * file `deepspace test-accounts create` writes). Each account has at
31
- * minimum `{email, password}`; `name` and `label` are optional.
32
- *
33
- * Used by the multi-user Playwright fixture in `./fixtures.ts` and is
34
- * also exported standalone in case suites want to do their own
35
- * orchestration.
36
- */
37
- interface TestAccount {
38
- email: string;
39
- password: string;
40
- name?: string;
41
- label?: string | null;
42
- id?: string;
43
- userId?: string;
44
- createdAt?: number;
45
- }
46
- /**
47
- * Load all test accounts the developer has created. Returns an empty
48
- * array if the file doesn't exist yet.
49
- */
50
- declare function loadAllTestAccounts(): TestAccount[];
51
- /**
52
- * Pick `count` test accounts from the local registry, optionally
53
- * filtered by `label`. Order-stable: sorted by `createdAt` ascending so
54
- * the first test account you created is always the first one returned.
55
- *
56
- * Throws with a helpful message if not enough accounts exist.
57
- */
58
- declare function pickTestAccounts(count: number, options?: {
59
- label?: string;
60
- }): TestAccount[];
61
- /**
62
- * Find a single account by name. Used by `users(['Alice', 'Bob'])`.
63
- * Throws if not found.
64
- */
65
- declare function findTestAccountByName(name: string): TestAccount;
66
-
67
- /**
68
- * Per-account `storageState` cache for Playwright.
69
- *
70
- * Better Auth applies a per-IP rate limit on `/api/auth/sign-in/email`
71
- * (~5 sign-ins / 60s / endpoint). A multiplayer suite that spins up
72
- * 5+ users per spec would hit the limit and start failing in non-obvious
73
- * ways. This module signs each account in *once*, persists the
74
- * resulting browser cookies + storage to disk, and reuses the file on
75
- * subsequent runs.
76
- *
77
- * Cache layout:
78
- * ~/.deepspace/playwright-states/<sha256(email)>.json
79
- *
80
- * Validity: a cached file is reused if Playwright successfully loads it
81
- * AND the resulting context produces a non-anonymous session on the
82
- * target app. If validation fails, we fall back to a fresh sign-in and
83
- * overwrite the cache.
84
- */
85
-
86
- interface EnsureStorageStateOptions {
87
- /** Max age of a cached state file before we re-sign-in. Default 7 days. */
88
- maxAgeMs?: number;
89
- /** Force a fresh sign-in even if the cache is fresh. */
90
- force?: boolean;
91
- }
92
- /**
93
- * Ensure a Playwright `storageState` file exists for `account` and
94
- * return its path. Signs in once if the cache is missing, stale, or
95
- * `force: true`.
96
- */
97
- declare function ensureStorageState(browser: Browser, account: {
98
- email: string;
99
- password: string;
100
- }, baseURL: string, options?: EnsureStorageStateOptions): Promise<string>;
101
- /**
102
- * Convenience: open a fresh `BrowserContext` for `account` using the
103
- * cached storage state (signing in if needed). The caller is responsible
104
- * for closing the context.
105
- */
106
- declare function newSignedInContext(browser: Browser, account: {
107
- email: string;
108
- password: string;
109
- }, baseURL: string, options?: EnsureStorageStateOptions): Promise<BrowserContext>;
110
- /** Read a cached storageState file (mostly for debugging). */
111
- declare function readCachedState(email: string): unknown | null;
112
- /** Where the cache for `email` lives. Useful for diagnostics. */
113
- declare function getStatePathForEmail(email: string): string;
114
-
115
- export { type EnsureStorageStateOptions, type MultiplayerUser, type TestAccount, type UsersFixture, ensureStorageState, findTestAccountByName, getStatePathForEmail, loadAllTestAccounts, newSignedInContext, pickTestAccounts, readCachedState, test };