@lincyaw/workspace 1.0.0-rc.0 → 1.0.0-rc.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/README.md +5 -5
- package/package.json +10 -5
- package/src/domain/{revisions.ts → commit-message.ts} +7 -7
- package/src/domain/locks.ts +5 -0
- package/src/domain/paths.ts +2 -2
- package/src/index.ts +7 -2
- package/src/node/authoring-clients.ts +206 -0
- package/src/node/build.ts +4 -4
- package/src/node/compile-session.ts +187 -0
- package/src/node/config.ts +36 -21
- package/src/node/document-assets.ts +30 -0
- package/src/node/document-audio.ts +473 -0
- package/src/node/document-authoring.ts +436 -0
- package/src/node/document-editor.ts +157 -0
- package/src/node/document-history.ts +190 -0
- package/src/node/document-images.ts +241 -0
- package/src/node/document-operations.ts +191 -0
- package/src/node/document-transcription-adoption.ts +96 -0
- package/src/node/document-transcription.ts +471 -0
- package/src/node/document-voice.ts +115 -0
- package/src/node/errors.ts +23 -0
- package/src/node/git.ts +15 -15
- package/src/node/jobs.ts +361 -0
- package/src/node/locks.ts +10 -5
- package/src/node/page-delete.ts +106 -0
- package/src/node/project-files.ts +163 -0
- package/src/node/projects.ts +192 -0
- package/src/node/review/agent-session-reviews.ts +374 -0
- package/src/node/review/git-guard.ts +9 -8
- package/src/node/review/git-review.ts +102 -90
- package/src/node/review/record.ts +77 -0
- package/src/node/review/routes.ts +36 -35
- package/src/node/review/task-reviews.ts +50 -0
- package/src/node/revisions.ts +321 -68
- package/src/node/routes.ts +236 -146
- package/src/node/serve.ts +319 -118
- package/src/node/site.ts +34 -23
- package/src/node/source-editor.ts +332 -0
- package/src/node/speech-settings.ts +21 -0
- package/src/node/templates.ts +27 -0
- package/src/node/workbench-api.ts +176 -0
- package/src/node/writer.ts +389 -0
- package/templates/document/default/lecture.json +75 -0
- package/src/domain/api.ts +0 -102
- package/src/domain/index.ts +0 -6
- package/src/node/documents.ts +0 -185
- package/src/node/files.ts +0 -252
- package/src/node/review/review-record.ts +0 -73
- package/src/node/review/reviews.ts +0 -422
- package/src/node/service.ts +0 -237
- package/src/node/session.ts +0 -123
- package/src/node/source.ts +0 -148
- package/templates/document/default/lecture.md +0 -18
package/src/node/config.ts
CHANGED
|
@@ -1,26 +1,24 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from 'node:fs'
|
|
2
2
|
import { join, resolve } from 'node:path'
|
|
3
|
-
import {
|
|
3
|
+
import { WORKSPACE_CONFIG } from '@lincyaw/compiler'
|
|
4
4
|
import type { OperatorSettings } from '@lincyaw/compiler'
|
|
5
|
-
import { errorMessage, isRecord } from '@lincyaw/kernel'
|
|
6
|
-
import type { ApprovalKind } from '@lincyaw/kernel'
|
|
5
|
+
import { APPROVAL_KINDS, DOCUMENT_ENTRY, errorMessage, isRecord } from '@lincyaw/kernel'
|
|
6
|
+
import type { ApprovalKind, DocumentSummary } from '@lincyaw/kernel'
|
|
7
7
|
import type { ApprovalPolicy } from '@lincyaw/server'
|
|
8
8
|
import { parse as parseYaml } from 'yaml'
|
|
9
|
-
import type { WorkspaceDocument } from '../domain/api.ts'
|
|
10
9
|
import { matchesPattern, patternSegments } from '../domain/paths.ts'
|
|
11
10
|
|
|
12
|
-
export const DEFAULT_DOCUMENTS = [`*/${
|
|
11
|
+
export const DEFAULT_DOCUMENTS = [`*/${DOCUMENT_ENTRY}`]
|
|
13
12
|
|
|
14
13
|
const SKIPPED = new Set(['node_modules', '.git', '.presmium', 'dist'])
|
|
15
14
|
|
|
16
|
-
const KINDS: ApprovalKind[] = ['operate', 'spend', 'write', 'agent']
|
|
17
15
|
const POLICIES = new Set<ApprovalPolicy>(['off', 'ask', 'allow'])
|
|
18
16
|
|
|
19
17
|
export interface WorkspaceConfig {
|
|
20
18
|
root: string
|
|
21
19
|
title: string
|
|
22
20
|
documents: string[]
|
|
23
|
-
|
|
21
|
+
singleDeck: boolean
|
|
24
22
|
site: Record<string, unknown>
|
|
25
23
|
approvals: Partial<Record<ApprovalKind, ApprovalPolicy>>
|
|
26
24
|
templates?: string
|
|
@@ -31,7 +29,7 @@ function strings(value: unknown): string[] {
|
|
|
31
29
|
return Array.isArray(value) ? value.filter((v): v is string => typeof v === 'string' && v.trim() !== '').map(v => v.trim()) : typeof value === 'string' && value.trim() ? [value.trim()] : []
|
|
32
30
|
}
|
|
33
31
|
|
|
34
|
-
export function
|
|
32
|
+
export function readWorkspaceConfig(root: string): WorkspaceConfig {
|
|
35
33
|
const dir = realpathSync(resolve(root))
|
|
36
34
|
const file = join(dir, WORKSPACE_CONFIG)
|
|
37
35
|
let data: Record<string, unknown> = {}
|
|
@@ -44,16 +42,20 @@ export function readWorkspace(root: string): WorkspaceConfig {
|
|
|
44
42
|
}
|
|
45
43
|
}
|
|
46
44
|
const declared = strings(data.documents)
|
|
47
|
-
const
|
|
45
|
+
const singleDeck = declared.length === 0 && existsSync(join(dir, DOCUMENT_ENTRY))
|
|
48
46
|
const approvals = isRecord(data.approvals) ? data.approvals : {}
|
|
49
47
|
const settings = isRecord(data.settings) ? data.settings : {}
|
|
48
|
+
const renderers = isRecord(data.renderers) ? data.renderers : {}
|
|
49
|
+
for (const [path, value] of [['renderers.lecture', renderers.lecture], ['settings.presmium.lecture', settings['presmium.lecture']]] as const) {
|
|
50
|
+
if (isRecord(value) && Object.hasOwn(value, 'script')) throw new Error(`${file}: ${path}.script: External lecture scripts are not supported. Author narration in each document's lecture.json narrations.`)
|
|
51
|
+
}
|
|
50
52
|
return {
|
|
51
53
|
root: dir,
|
|
52
54
|
title: typeof data.title === 'string' && data.title.trim() ? data.title.trim() : dir.split('/').at(-1) ?? 'Presmium',
|
|
53
|
-
documents:
|
|
54
|
-
|
|
55
|
+
documents: singleDeck ? [DOCUMENT_ENTRY] : declared.length > 0 ? declared : DEFAULT_DOCUMENTS,
|
|
56
|
+
singleDeck,
|
|
55
57
|
site: isRecord(data.site) ? data.site : {},
|
|
56
|
-
approvals: Object.fromEntries(
|
|
58
|
+
approvals: Object.fromEntries(APPROVAL_KINDS.flatMap(kind => (POLICIES.has(approvals[kind] as ApprovalPolicy) ? [[kind, approvals[kind] as ApprovalPolicy]] : []))),
|
|
57
59
|
...typeof data.templates === 'string' && data.templates.trim() ? { templates: resolve(dir, data.templates.trim()) } : {},
|
|
58
60
|
settings: Object.fromEntries(Object.entries(settings).map(([id, value]) => [id, isRecord(value) ? value : {}])),
|
|
59
61
|
}
|
|
@@ -62,7 +64,7 @@ export function readWorkspace(root: string): WorkspaceConfig {
|
|
|
62
64
|
export function isWorkspaceRoot(dir: string): boolean {
|
|
63
65
|
if (!existsSync(join(dir, WORKSPACE_CONFIG))) return false
|
|
64
66
|
try {
|
|
65
|
-
return !
|
|
67
|
+
return !readWorkspaceConfig(dir).singleDeck
|
|
66
68
|
} catch {
|
|
67
69
|
return true
|
|
68
70
|
}
|
|
@@ -87,7 +89,7 @@ function entries(config: WorkspaceConfig): string[] {
|
|
|
87
89
|
return names.flatMap((name) => {
|
|
88
90
|
const path = join(dir, name)
|
|
89
91
|
const next = rel ? `${rel}/${name}` : name
|
|
90
|
-
if (last) return existsSync(path) && statSync(path).isFile() ? [next] : []
|
|
92
|
+
if (last) return name === DOCUMENT_ENTRY && existsSync(path) && statSync(path).isFile() ? [next] : []
|
|
91
93
|
return existsSync(path) && statSync(path).isDirectory() ? walk(path, next, i + 1) : []
|
|
92
94
|
})
|
|
93
95
|
}
|
|
@@ -96,15 +98,23 @@ function entries(config: WorkspaceConfig): string[] {
|
|
|
96
98
|
return found
|
|
97
99
|
}
|
|
98
100
|
|
|
99
|
-
export interface
|
|
101
|
+
export interface DocumentEntry {
|
|
102
|
+
dir: string
|
|
100
103
|
entry: string
|
|
101
104
|
}
|
|
102
105
|
|
|
103
|
-
|
|
104
|
-
|
|
106
|
+
export interface FoundDocument extends DocumentSummary, DocumentEntry {}
|
|
107
|
+
|
|
108
|
+
/** The configured documents that exist on disk, without reading their content. */
|
|
109
|
+
export function listDocumentEntries(config: WorkspaceConfig): DocumentEntry[] {
|
|
110
|
+
return entries(config).map(entry => ({ dir: entry.split('/').slice(0, -1).join('/'), entry }))
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function readDocumentSummary(config: WorkspaceConfig, { dir, entry }: DocumentEntry): FoundDocument {
|
|
105
114
|
let headmatter: Record<string, unknown> = {}
|
|
106
115
|
try {
|
|
107
|
-
|
|
116
|
+
const parsed: unknown = JSON.parse(readFileSync(join(config.root, entry), 'utf8'))
|
|
117
|
+
headmatter = isRecord(parsed) && isRecord(parsed.metadata) ? parsed.metadata : {}
|
|
108
118
|
} catch {
|
|
109
119
|
}
|
|
110
120
|
const tags = strings(headmatter.tags)
|
|
@@ -112,15 +122,20 @@ function documentOf(config: WorkspaceConfig, entry: string): FoundDocument {
|
|
|
112
122
|
dir,
|
|
113
123
|
entry,
|
|
114
124
|
title: typeof headmatter.title === 'string' && headmatter.title.trim() ? headmatter.title.trim() : dir || config.title,
|
|
125
|
+
...typeof headmatter.description === 'string' && headmatter.description.trim() ? { description: headmatter.description.trim() } : {},
|
|
115
126
|
...typeof headmatter.lang === 'string' && headmatter.lang.trim() ? { lang: headmatter.lang.trim() } : {},
|
|
116
127
|
...tags.length > 0 ? { tags } : {},
|
|
117
128
|
}
|
|
118
129
|
}
|
|
119
130
|
|
|
120
131
|
export function listDocuments(config: WorkspaceConfig): FoundDocument[] {
|
|
121
|
-
return
|
|
132
|
+
return listDocumentEntries(config).map(entry => readDocumentSummary(config, entry))
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function documentSummaries(config: WorkspaceConfig): DocumentSummary[] {
|
|
136
|
+
return listDocuments(config).map(({ entry: _entry, ...document }) => document)
|
|
122
137
|
}
|
|
123
138
|
|
|
124
|
-
export function
|
|
125
|
-
return config.documents.some(pattern => matchesPattern(pattern, dir ? `${dir}/${
|
|
139
|
+
export function isDocumentDir(config: WorkspaceConfig, dir: string): boolean {
|
|
140
|
+
return config.documents.some(pattern => matchesPattern(pattern, dir ? `${dir}/${DOCUMENT_ENTRY}` : DOCUMENT_ENTRY))
|
|
126
141
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createReadStream, realpathSync } from 'node:fs'
|
|
2
|
+
import { basename, isAbsolute, relative, resolve, sep } from 'node:path'
|
|
3
|
+
import { Readable } from 'node:stream'
|
|
4
|
+
import { lookup } from 'mime-types'
|
|
5
|
+
import type { TeachingDocument } from '@lincyaw/kernel'
|
|
6
|
+
import { RequestError } from './errors.ts'
|
|
7
|
+
|
|
8
|
+
/** Read only a file registered by the current canonical document, without compiling it. */
|
|
9
|
+
export function documentAssetResponse(root: string, directory: string, document: TeachingDocument, id: string, readableFile: (path: string) => string, signal?: AbortSignal): Response {
|
|
10
|
+
const asset = document.assets[id]
|
|
11
|
+
if (!asset) throw new RequestError(404, 'document-asset-missing')
|
|
12
|
+
if (!asset.location || /^(?:[a-z][a-z\d+.-]*:|\/)/i.test(asset.location)) throw new RequestError(400, 'document-asset-not-local')
|
|
13
|
+
let location = asset.location.replace(/[?#].*$/s, '')
|
|
14
|
+
try { location = decodeURIComponent(location) } catch { throw new RequestError(400, 'document-asset-path') }
|
|
15
|
+
if (!location || isAbsolute(location) || location.includes('\\') || location.includes('\0')) throw new RequestError(400, 'document-asset-path')
|
|
16
|
+
const workspace = realpathSync(root), target = resolve(workspace, directory, location)
|
|
17
|
+
const path = relative(workspace, target)
|
|
18
|
+
if (!path || path === '..' || path.startsWith(`..${sep}`) || isAbsolute(path)) throw new RequestError(403, 'document-asset-outside-workspace')
|
|
19
|
+
const file = readableFile(path.split(sep).join('/'))
|
|
20
|
+
const type = lookup(file) || 'application/octet-stream'
|
|
21
|
+
const inline = /^(?:image\/(?:png|jpeg|gif|webp|avif|svg\+xml|bmp|x-icon)|audio\/|video\/)/i.test(type)
|
|
22
|
+
return new Response(Readable.toWeb(createReadStream(file, { signal })), { headers: {
|
|
23
|
+
'content-type': type,
|
|
24
|
+
'cache-control': 'private, no-store',
|
|
25
|
+
'content-disposition': `${inline ? 'inline' : 'attachment'}; filename*=UTF-8''${encodeURIComponent(basename(file))}`,
|
|
26
|
+
'x-content-type-options': 'nosniff',
|
|
27
|
+
'cross-origin-resource-policy': 'same-origin',
|
|
28
|
+
'content-security-policy': "sandbox; default-src 'none'; style-src 'unsafe-inline'",
|
|
29
|
+
} })
|
|
30
|
+
}
|