deepspace 0.5.5 → 0.5.6
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/dist/cli.js +719 -318
- package/dist/cli.js.map +1 -1
- package/features/admin-page/feature.json +3 -1
- package/features/file-attachments/feature.json +3 -1
- package/features/messaging/feature.json +1 -1
- package/features/search-bar/feature.json +6 -5
- package/package.json +1 -1
- package/scripts/add-feature.cjs +174 -36
- package/dist/testing.d.ts +0 -115
|
@@ -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
|
|
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
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"Use the raw SearchInput primitive only
|
|
18
|
-
|
|
19
|
-
|
|
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
package/scripts/add-feature.cjs
CHANGED
|
@@ -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
|
|
473
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
543
|
-
console.log(
|
|
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:
|
|
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:
|
|
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.
|
|
654
|
-
// package.json (
|
|
655
|
-
//
|
|
656
|
-
//
|
|
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 };
|