deepspace 0.19.0 → 0.19.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.
- package/CHANGELOG.md +32 -0
- package/dist/cli.js +558 -261
- package/dist/cli.js.map +1 -1
- package/dist/feature-installer.js +14 -5
- package/dist/feature-installer.js.map +1 -1
- package/dist/index.d.ts +9 -5
- package/dist/index.js +42 -11
- package/dist/index.js.map +1 -1
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js.map +1 -1
- package/dist/server.d.ts +12 -7
- package/dist/server.js +63 -19
- package/dist/server.js.map +1 -1
- package/dist/worker.d.ts +9 -2
- package/dist/worker.js +63 -19
- package/dist/worker.js.map +1 -1
- package/features/admin-page/feature.json +3 -4
- package/features/admin-page/src/AdminPage.tsx +4 -4
- package/features/cron/feature.json +8 -5
- package/features/documents/feature.json +2 -1
- package/features/documents/src/DocumentsEditorPage.tsx +31 -10
- package/features/documents/src/DocumentsListPage.tsx +59 -26
- package/features/documents/src/InviteDialog.tsx +49 -14
- package/features/documents/src/LibrarySidebar.tsx +92 -84
- package/features/documents/src/__tests__/documents-presence-access.test.ts +15 -2
- package/features/documents/src/documents-library-types.ts +6 -0
- package/features/tasks/src/TasksPage.tsx +1 -1
- package/features/tasks/src/tasks-schema.ts +1 -1
- package/package.json +5 -5
- package/features/cron/src/cron.ts +0 -25
|
@@ -182,6 +182,7 @@ export interface LibrarySidebarProps {
|
|
|
182
182
|
selection: LibraryNavSelection
|
|
183
183
|
onSelect: (s: LibraryNavSelection) => void
|
|
184
184
|
folders: RecordData<DocFolderFields>[]
|
|
185
|
+
canManageFolders: boolean
|
|
185
186
|
collapsed: boolean
|
|
186
187
|
onToggleCollapsed: () => void
|
|
187
188
|
onCreateFolder: (name: string) => void | Promise<void>
|
|
@@ -198,6 +199,7 @@ export function LibrarySidebar({
|
|
|
198
199
|
selection,
|
|
199
200
|
onSelect,
|
|
200
201
|
folders,
|
|
202
|
+
canManageFolders,
|
|
201
203
|
collapsed,
|
|
202
204
|
onToggleCollapsed,
|
|
203
205
|
onCreateFolder,
|
|
@@ -337,7 +339,7 @@ export function LibrarySidebar({
|
|
|
337
339
|
</span>
|
|
338
340
|
<div className="space-y-0.5">
|
|
339
341
|
{sortedFolders.map((f) =>
|
|
340
|
-
renamingFolderId === f.recordId ? (
|
|
342
|
+
canManageFolders && renamingFolderId === f.recordId ? (
|
|
341
343
|
<div
|
|
342
344
|
key={f.recordId}
|
|
343
345
|
className="group flex w-full min-w-0 items-center gap-0.5 rounded-md bg-[color:var(--documents-el-accent)]/10 py-0.5 pl-0.5 dark:bg-[color:var(--documents-el-accent)]/15"
|
|
@@ -369,96 +371,102 @@ export function LibrarySidebar({
|
|
|
369
371
|
onClick={() => onSelect({ kind: 'folder', folderId: f.recordId })}
|
|
370
372
|
collapsed={collapsed}
|
|
371
373
|
trailing={
|
|
372
|
-
|
|
373
|
-
<
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
e
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
e
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
e
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
374
|
+
canManageFolders ? (
|
|
375
|
+
<span className="relative z-[1] flex shrink-0 items-center gap-0.5">
|
|
376
|
+
<button
|
|
377
|
+
type="button"
|
|
378
|
+
data-testid={`rename-folder-${f.recordId}`}
|
|
379
|
+
onMouseDown={(e) => {
|
|
380
|
+
e.preventDefault()
|
|
381
|
+
e.stopPropagation()
|
|
382
|
+
}}
|
|
383
|
+
onClick={(e) => {
|
|
384
|
+
e.stopPropagation()
|
|
385
|
+
onStartRenameFolder(f)
|
|
386
|
+
}}
|
|
387
|
+
className="rounded p-1 text-[color:var(--documents-el-muted)] opacity-0 transition-all hover:bg-black/[0.04] hover:text-[color:var(--documents-el-accent)] group-hover:opacity-100 dark:hover:bg-white/[0.06]"
|
|
388
|
+
title="Rename folder"
|
|
389
|
+
aria-label={`Rename folder ${f.data.name}`}
|
|
390
|
+
>
|
|
391
|
+
<Pencil className="h-3.5 w-3.5" />
|
|
392
|
+
</button>
|
|
393
|
+
<button
|
|
394
|
+
type="button"
|
|
395
|
+
data-testid={`delete-folder-${f.recordId}`}
|
|
396
|
+
onClick={(e) => {
|
|
397
|
+
e.stopPropagation()
|
|
398
|
+
if (
|
|
399
|
+
confirm(
|
|
400
|
+
`Delete folder “${f.data.name}”? Documents inside will move to uncategorized.`,
|
|
401
|
+
)
|
|
402
|
+
) {
|
|
403
|
+
void onDeleteFolder(f.recordId)
|
|
404
|
+
}
|
|
405
|
+
}}
|
|
406
|
+
className="rounded p-1 text-[color:var(--documents-el-muted)] opacity-0 transition-all hover:bg-red-500/10 hover:text-red-600 group-hover:opacity-100"
|
|
407
|
+
title="Delete folder"
|
|
408
|
+
aria-label={`Delete folder ${f.data.name}`}
|
|
409
|
+
>
|
|
410
|
+
<Trash2 className="h-3.5 w-3.5" />
|
|
411
|
+
</button>
|
|
412
|
+
</span>
|
|
413
|
+
) : undefined
|
|
410
414
|
}
|
|
411
415
|
/>
|
|
412
416
|
),
|
|
413
417
|
)}
|
|
414
418
|
|
|
415
|
-
{
|
|
416
|
-
|
|
417
|
-
<
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
419
|
+
{canManageFolders &&
|
|
420
|
+
(addingFolder ? (
|
|
421
|
+
<div className="py-1">
|
|
422
|
+
<input
|
|
423
|
+
autoFocus
|
|
424
|
+
value={newFolderName}
|
|
425
|
+
onChange={(e) => setNewFolderName(e.target.value)}
|
|
426
|
+
onBlur={() => void submitNewFolder()}
|
|
427
|
+
onKeyDown={(e) => {
|
|
428
|
+
if (e.key === 'Enter') void submitNewFolder()
|
|
429
|
+
if (e.key === 'Escape') {
|
|
430
|
+
setAddingFolder(false)
|
|
431
|
+
setNewFolderName('')
|
|
432
|
+
}
|
|
433
|
+
}}
|
|
434
|
+
placeholder="Folder name"
|
|
435
|
+
data-testid="new-folder-input"
|
|
436
|
+
className="w-full rounded-md border px-2 py-1.5 text-[13px] outline-none focus-visible:ring-2 focus-visible:ring-ring/30"
|
|
437
|
+
style={{
|
|
438
|
+
borderColor: 'var(--documents-el-line)',
|
|
439
|
+
backgroundColor: 'var(--documents-el-bg)',
|
|
440
|
+
color: 'var(--documents-el-text)',
|
|
441
|
+
}}
|
|
442
|
+
/>
|
|
443
|
+
</div>
|
|
444
|
+
) : (
|
|
445
|
+
<button
|
|
446
|
+
type="button"
|
|
447
|
+
data-testid="new-folder-btn"
|
|
448
|
+
onClick={() => {
|
|
449
|
+
if (!collapsed) {
|
|
450
|
+
setAddingFolder(true)
|
|
451
|
+
return
|
|
427
452
|
}
|
|
453
|
+
const name = window.prompt('Folder name')?.trim()
|
|
454
|
+
if (name) void onCreateFolder(name)
|
|
428
455
|
}}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
onClick={() => {
|
|
444
|
-
if (!collapsed) {
|
|
445
|
-
setAddingFolder(true)
|
|
446
|
-
return
|
|
447
|
-
}
|
|
448
|
-
const name = window.prompt('Folder name')?.trim()
|
|
449
|
-
if (name) void onCreateFolder(name)
|
|
450
|
-
}}
|
|
451
|
-
title={collapsed ? 'New folder' : undefined}
|
|
452
|
-
className="flex h-9 w-full min-w-0 items-center gap-0 rounded-md pl-0.5 pr-0 text-left text-[13px] text-[color:var(--documents-el-muted)] transition-colors hover:bg-black/[0.04] hover:text-[color:var(--documents-el-accent)] dark:hover:bg-white/[0.06]"
|
|
453
|
-
>
|
|
454
|
-
<span className="flex h-9 w-10 shrink-0 items-center justify-center" aria-hidden>
|
|
455
|
-
<FolderPlus className="h-3.5 w-3.5 shrink-0" />
|
|
456
|
-
</span>
|
|
457
|
-
<span className={collapsed ? 'sr-only' : 'min-w-0 flex-1 truncate pr-2'}>
|
|
458
|
-
New folder
|
|
459
|
-
</span>
|
|
460
|
-
</button>
|
|
461
|
-
)}
|
|
456
|
+
title={collapsed ? 'New folder' : undefined}
|
|
457
|
+
className="flex h-9 w-full min-w-0 items-center gap-0 rounded-md pl-0.5 pr-0 text-left text-[13px] text-[color:var(--documents-el-muted)] transition-colors hover:bg-black/[0.04] hover:text-[color:var(--documents-el-accent)] dark:hover:bg-white/[0.06]"
|
|
458
|
+
>
|
|
459
|
+
<span
|
|
460
|
+
className="flex h-9 w-10 shrink-0 items-center justify-center"
|
|
461
|
+
aria-hidden
|
|
462
|
+
>
|
|
463
|
+
<FolderPlus className="h-3.5 w-3.5 shrink-0" />
|
|
464
|
+
</span>
|
|
465
|
+
<span className={collapsed ? 'sr-only' : 'min-w-0 flex-1 truncate pr-2'}>
|
|
466
|
+
New folder
|
|
467
|
+
</span>
|
|
468
|
+
</button>
|
|
469
|
+
))}
|
|
462
470
|
</div>
|
|
463
471
|
</div>
|
|
464
472
|
</nav>
|
|
@@ -5,6 +5,15 @@ import {
|
|
|
5
5
|
isDocumentsOwner,
|
|
6
6
|
resolveDocumentsAccessRole,
|
|
7
7
|
} from '../use-documents-presence-access'
|
|
8
|
+
import { recordsReadyForMutation } from '../documents-library-types'
|
|
9
|
+
|
|
10
|
+
describe('document mutation readiness', () => {
|
|
11
|
+
it('requires both a ready query and a ready mutation transport', () => {
|
|
12
|
+
expect(recordsReadyForMutation('loading', true)).toBe(false)
|
|
13
|
+
expect(recordsReadyForMutation('ready', false)).toBe(false)
|
|
14
|
+
expect(recordsReadyForMutation('ready', true)).toBe(true)
|
|
15
|
+
})
|
|
16
|
+
})
|
|
8
17
|
|
|
9
18
|
describe('document access policy', () => {
|
|
10
19
|
const document = {
|
|
@@ -42,8 +51,12 @@ describe('document access policy', () => {
|
|
|
42
51
|
expect(deriveDocumentsAccessChange('viewer', 'editor', null)).toBe('upgrade')
|
|
43
52
|
expect(deriveDocumentsAccessChange('viewer', 'none', null)).toBe('revoked')
|
|
44
53
|
expect(deriveDocumentsAccessChange('owner', 'owner', null)).toBeNull()
|
|
45
|
-
expect(deriveDocumentsAccessChange(null, 'viewer', { kind: 'downgrade', at: 20 })).toBe(
|
|
46
|
-
|
|
54
|
+
expect(deriveDocumentsAccessChange(null, 'viewer', { kind: 'downgrade', at: 20 })).toBe(
|
|
55
|
+
'downgrade',
|
|
56
|
+
)
|
|
57
|
+
expect(deriveDocumentsAccessChange('editor', 'viewer', { kind: 'revoked', at: 20 })).toBe(
|
|
58
|
+
'revoked',
|
|
59
|
+
)
|
|
47
60
|
})
|
|
48
61
|
|
|
49
62
|
it('accepts only a fresh ACL signal addressed to the current user', () => {
|
|
@@ -29,6 +29,12 @@ export type LibraryNavSelection =
|
|
|
29
29
|
| { kind: 'uncategorized' }
|
|
30
30
|
| { kind: 'folder'; folderId: string }
|
|
31
31
|
|
|
32
|
+
/** Record controls are usable only after both the initial query and mutation
|
|
33
|
+
* transport are ready. Keep that boundary identical across feature pages. */
|
|
34
|
+
export function recordsReadyForMutation(queryStatus: string, mutationsReady: boolean): boolean {
|
|
35
|
+
return queryStatus === 'ready' && mutationsReady
|
|
36
|
+
}
|
|
37
|
+
|
|
32
38
|
export function parseDocumentsIdList(raw: string | undefined): string[] {
|
|
33
39
|
if (!raw) return []
|
|
34
40
|
try {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Demonstrates:
|
|
5
5
|
* - 'unclaimed-or-own' permission for claimable tasks
|
|
6
|
-
* - writableFields for restricting which fields can be updated
|
|
6
|
+
* - writableFields for restricting which fields can be created or updated
|
|
7
7
|
* - timestampTrigger for automatic timestamps
|
|
8
8
|
* - Admin grading workflow
|
|
9
9
|
*/
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* A claimable task system demonstrating:
|
|
5
5
|
* - 'unclaimed-or-own' permission for claiming unclaimed items
|
|
6
|
-
* - writableFields for restricting which fields users can update
|
|
6
|
+
* - writableFields for restricting which fields users can create or update
|
|
7
7
|
* - timestampTrigger for automatic timestamp fields
|
|
8
8
|
* - Admin-only grading workflow
|
|
9
9
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "deepspace",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.2",
|
|
4
4
|
"engines": {
|
|
5
5
|
"node": ">=22.15.0 <23 || >=24 <25 || >=26 <27"
|
|
6
6
|
},
|
|
@@ -56,13 +56,13 @@
|
|
|
56
56
|
"prepublishOnly": "pnpm build"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@ai-sdk/anthropic": "
|
|
60
|
-
"@ai-sdk/openai": "
|
|
61
|
-
"@ai-sdk/openai-compatible": "
|
|
59
|
+
"@ai-sdk/anthropic": "2.0.91",
|
|
60
|
+
"@ai-sdk/openai": "2.0.116",
|
|
61
|
+
"@ai-sdk/openai-compatible": "1.0.46",
|
|
62
62
|
"@clack/prompts": "^1.2.0",
|
|
63
63
|
"@mdx-js/mdx": "^3.1.1",
|
|
64
64
|
"acorn": "^8.16.0",
|
|
65
|
-
"ai": "
|
|
65
|
+
"ai": "5.0.222",
|
|
66
66
|
"better-auth": "1.6.23",
|
|
67
67
|
"citty": "^0.2.0",
|
|
68
68
|
"cross-spawn": "^7.0.6",
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cron task definitions for the heartbeat reference feature.
|
|
3
|
-
*
|
|
4
|
-
* AppCronRoom (declared in worker.ts) reads `tasks` at construction time
|
|
5
|
-
* and validates them. The DO alarm fires `runTask(name, env)` on the
|
|
6
|
-
* configured schedule; each fire is recorded in the DO's `cron_history`
|
|
7
|
-
* table and pushed to subscribers over `/ws/cron/:roomId`.
|
|
8
|
-
*
|
|
9
|
-
* The heartbeat task here is the low-cost liveness probe used by both
|
|
10
|
-
* the cron e2e spec and as a quick sanity check after a deploy. To add
|
|
11
|
-
* your own task, append a {name, intervalMinutes} or {name, schedule,
|
|
12
|
-
* timezone} entry to `tasks` and dispatch on `name` inside `runTask`.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import type { CronTask } from 'deepspace/worker'
|
|
16
|
-
|
|
17
|
-
export const tasks: CronTask[] = [
|
|
18
|
-
{ name: 'heartbeat', intervalMinutes: 1 },
|
|
19
|
-
]
|
|
20
|
-
|
|
21
|
-
export async function runTask(_name: string, _env: unknown): Promise<void> {
|
|
22
|
-
// No-op work — the DO records every invocation in cron_history regardless,
|
|
23
|
-
// which is what the e2e spec asserts against. Real apps would dispatch on
|
|
24
|
-
// `_name` here and call into integrations / records via buildCronContext.
|
|
25
|
-
}
|