@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/serve.ts
CHANGED
|
@@ -1,38 +1,54 @@
|
|
|
1
|
+
import { createProjectAuthoring } from './projects.ts'
|
|
2
|
+
import { createDocumentHistory } from './document-history.ts'
|
|
3
|
+
import { createAuthoringClients } from './authoring-clients.ts'
|
|
1
4
|
import { createHash } from 'node:crypto'
|
|
2
5
|
import { mkdirSync, mkdtempSync, rmSync } from 'node:fs'
|
|
3
6
|
import { homedir, tmpdir } from 'node:os'
|
|
4
|
-
import { join } from 'node:path'
|
|
5
|
-
import {
|
|
7
|
+
import { join, relative } from 'node:path'
|
|
8
|
+
import { Compiler, operatorDir as resolveOperatorDir, pluginLogger, runBlock, workspacePlugins } from '@lincyaw/compiler'
|
|
6
9
|
import type { Compilation, CompilerOptions } from '@lincyaw/compiler'
|
|
7
|
-
import { CHANGE_REVIEW, DOCUMENT_FILES, DOCUMENT_LOCKS, errorMessage, REVISIONS } from '@lincyaw/kernel'
|
|
8
|
-
import
|
|
9
|
-
import {
|
|
10
|
-
import type {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import
|
|
15
|
-
import {
|
|
16
|
-
import type { DocumentsChange } from './
|
|
17
|
-
import {
|
|
18
|
-
import type { DocumentWriter } from './files.ts'
|
|
10
|
+
import { CHANGE_REVIEW, DOCUMENT_FILES, DOCUMENT_LOCKS, DOCUMENT_VOICE_TASKS, errorMessage, PRODUCTION_AUDIO_AUTHORING, REVISIONS, SPEECH, VOICEOVER, VOICEOVER_EXECUTION } from '@lincyaw/kernel'
|
|
11
|
+
import { irRevision, operatorSpeechSecrets } from '@lincyaw/kernel/node'
|
|
12
|
+
import { speechSettingsRoute } from './speech-settings.ts'
|
|
13
|
+
import type { CanonicalSource, ChangeReviewService, Finding, IRDocument, Logger, ProductionAudioAuthoring, ServiceToken, SiteSpec, SpeechService } from '@lincyaw/kernel'
|
|
14
|
+
import { serverConfigKey, servedDocument, startServer } from '@lincyaw/server'
|
|
15
|
+
import type { DiagnosticStore, PresmiumServer, ServedDocument, ServedPlugin, ServerHost } from '@lincyaw/server'
|
|
16
|
+
import { CompileSession } from './compile-session.ts'
|
|
17
|
+
import { documentSummaries, listDocuments, readWorkspaceConfig } from './config.ts'
|
|
18
|
+
import { DocumentOperations, documentsDiff } from './document-operations.ts'
|
|
19
|
+
import type { DocumentsChange } from './document-operations.ts'
|
|
20
|
+
import { RequestError } from './errors.ts'
|
|
19
21
|
import { Git } from './git.ts'
|
|
22
|
+
import { createJobs, OUTPUTS_PATH } from './jobs.ts'
|
|
20
23
|
import { DocumentLocks } from './locks.ts'
|
|
21
|
-
import {
|
|
24
|
+
import { AgentSessionReviews } from './review/agent-session-reviews.ts'
|
|
25
|
+
import { createTaskReviewService } from './review/task-reviews.ts'
|
|
22
26
|
import { Revisions } from './revisions.ts'
|
|
23
|
-
import {
|
|
24
|
-
import
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
+
import { WORKSPACE_ID, authorizeWorkspaceRoutes, workspaceRoutes, workspaceRoutesPlugin } from './routes.ts'
|
|
28
|
+
import { refreshDocumentVitePlugins, siteContext, siteDocument, siteDocumentIR, siteSpec, untranslatedFindings, webRenderer } from './site.ts'
|
|
29
|
+
import { SourceEditor } from './source-editor.ts'
|
|
30
|
+
import { createDocumentEditor } from './document-editor.ts'
|
|
31
|
+
import type { DocumentEditor } from './document-editor.ts'
|
|
32
|
+
import { createDocumentTranscriptions } from './document-transcription.ts'
|
|
33
|
+
import { createTranscriptionAdoption } from './document-transcription-adoption.ts'
|
|
34
|
+
import { createDocumentAudioAuthoring } from './document-audio.ts'
|
|
35
|
+
import { createDocumentImageAuthoring } from './document-images.ts'
|
|
36
|
+
import { createDocumentVoiceAuthoring } from './document-voice.ts'
|
|
37
|
+
import type { DocumentVoiceServices } from './document-voice.ts'
|
|
38
|
+
import { createWorkbenchBackend } from './workbench-api.ts'
|
|
39
|
+
import type { WorkbenchBackend } from './workbench-api.ts'
|
|
40
|
+
import { localWriter } from './writer.ts'
|
|
27
41
|
|
|
28
42
|
export interface ServeOptions {
|
|
29
43
|
dir: string
|
|
30
|
-
|
|
44
|
+
listenAddress?: string
|
|
31
45
|
port?: number
|
|
32
46
|
operatorDir?: string
|
|
33
47
|
bundled?: string
|
|
34
48
|
check?(compilation: Compilation): Promise<Finding[]>
|
|
35
49
|
previewOrigins?: string[]
|
|
50
|
+
/** The address browsers use when a reverse proxy fronts the service (see `ServerOptions.publicUrl`). */
|
|
51
|
+
publicUrl?: string
|
|
36
52
|
env?: NodeJS.ProcessEnv
|
|
37
53
|
approvalTimeoutMs?: number
|
|
38
54
|
log?: Logger
|
|
@@ -45,9 +61,21 @@ export interface WorkspaceServer {
|
|
|
45
61
|
close(): Promise<void>
|
|
46
62
|
}
|
|
47
63
|
|
|
64
|
+
/** What the workspace keeps when it is a git repository: its revisions, the document operations and the reviews. */
|
|
65
|
+
interface WorkspaceHistory {
|
|
66
|
+
git: Git
|
|
67
|
+
revisions: Revisions
|
|
68
|
+
operations: DocumentOperations
|
|
69
|
+
agentSessionReviews: AgentSessionReviews
|
|
70
|
+
taskReviews: ChangeReviewService
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface AuthoringServices extends DocumentVoiceServices { audio?: ProductionAudioAuthoring }
|
|
74
|
+
interface NodeServiceLoad { key: string, promise: Promise<AuthoringServices>, controller: AbortController }
|
|
75
|
+
|
|
48
76
|
const DISK_LOOK_MS = 1000
|
|
49
77
|
|
|
50
|
-
function
|
|
78
|
+
function makeStateDir(root: string, env: NodeJS.ProcessEnv): string {
|
|
51
79
|
const base = env.XDG_CACHE_HOME || join(homedir(), '.cache')
|
|
52
80
|
const dir = join(base, 'presmium', 'serve', createHash('sha256').update(root).digest('hex').slice(0, 16))
|
|
53
81
|
mkdirSync(dir, { recursive: true })
|
|
@@ -57,194 +85,355 @@ function stateDir(root: string, env: NodeJS.ProcessEnv): string {
|
|
|
57
85
|
export async function serveWorkspace(options: ServeOptions): Promise<WorkspaceServer> {
|
|
58
86
|
const env = options.env ?? process.env
|
|
59
87
|
const log = options.log ?? pluginLogger('presmium serve')
|
|
60
|
-
const config =
|
|
88
|
+
const config = readWorkspaceConfig(options.dir)
|
|
61
89
|
const { root } = config
|
|
90
|
+
const operatorDirectory = resolveOperatorDir(root, options.operatorDir ?? env.PRESMIUM_OPERATOR_DIR)
|
|
91
|
+
const stateDir = makeStateDir(root, env)
|
|
62
92
|
const docs = () => listDocuments(config)
|
|
63
|
-
const
|
|
93
|
+
const compileSessions = new Map<string, CompileSession>()
|
|
94
|
+
const agentToolListeners = new Set<(document: string) => void>()
|
|
64
95
|
const locks = new DocumentLocks()
|
|
96
|
+
const writer = localWriter(config)
|
|
97
|
+
const editor = new SourceEditor({ config, writer, locks })
|
|
65
98
|
const check = options.check ?? (compilation => compilation.check())
|
|
66
|
-
const
|
|
99
|
+
const launcherToken = env.PRESMIUM_SERVICE_TOKEN || undefined
|
|
67
100
|
const preview = { origins: options.previewOrigins ?? [] }
|
|
101
|
+
// Behind a proxy that forwards a path (`--public-url https://host/user/a/preview/`), the site, the jobs' files and the
|
|
102
|
+
// previews' links are all below that path; the server finds its own routes below it too.
|
|
103
|
+
const basePath = options.publicUrl ? `${new URL(options.publicUrl).pathname.replace(/\/+$/, '')}/` : '/'
|
|
104
|
+
const outputsPath = `${basePath}${OUTPUTS_PATH.slice(1)}`
|
|
105
|
+
const publicOrigin = () => options.publicUrl ? new URL(options.publicUrl).origin : server?.url ?? ''
|
|
68
106
|
|
|
69
|
-
let
|
|
70
|
-
let
|
|
71
|
-
let site:
|
|
107
|
+
let serverServices: [ServiceToken<unknown>, unknown][] = []
|
|
108
|
+
let workspaceServices: [ServiceToken<unknown>, unknown][] = []
|
|
109
|
+
let site: WorkspaceSite | undefined
|
|
72
110
|
let server: PresmiumServer | undefined
|
|
73
|
-
let
|
|
111
|
+
let workbench: WorkbenchBackend | undefined
|
|
112
|
+
let history: WorkspaceHistory | undefined
|
|
113
|
+
let documentEditor: DocumentEditor | undefined
|
|
114
|
+
let documentHistory: ReturnType<typeof createDocumentHistory> | undefined
|
|
115
|
+
let projectAuthoring: ReturnType<typeof createProjectAuthoring> | undefined
|
|
116
|
+
let authoringClients: ReturnType<typeof createAuthoringClients> | undefined
|
|
117
|
+
let transcriptions: ReturnType<typeof createDocumentTranscriptions> | undefined
|
|
118
|
+
let transcriptionAdoption: ReturnType<typeof createTranscriptionAdoption> | undefined
|
|
119
|
+
let audioAuthoring: ReturnType<typeof createDocumentAudioAuthoring> | undefined
|
|
120
|
+
let imageAuthoring: ReturnType<typeof createDocumentImageAuthoring> | undefined
|
|
121
|
+
let voiceAuthoring: ReturnType<typeof createDocumentVoiceAuthoring> | undefined
|
|
122
|
+
let workspaceRoutesOwner: ReturnType<typeof workspaceRoutes> | undefined
|
|
123
|
+
let routesPlugin: ServedPlugin | undefined
|
|
124
|
+
// The builds and exports, as jobs the workbench and the AI start and follow.
|
|
125
|
+
const jobs = createJobs({
|
|
126
|
+
stateDir,
|
|
127
|
+
url: publicOrigin,
|
|
128
|
+
outputsPath,
|
|
129
|
+
compilation: (dir, fresh) => fresh ? compileSessionOf(dir).recompile() : compileSessionOf(dir).ready(),
|
|
130
|
+
revision: compilation => irRevision(compilation.doc),
|
|
131
|
+
origins: preview.origins,
|
|
132
|
+
log,
|
|
133
|
+
})
|
|
74
134
|
|
|
75
|
-
const
|
|
76
|
-
const known =
|
|
135
|
+
const compileSessionOf = (dir: string): CompileSession => {
|
|
136
|
+
const known = compileSessions.get(dir)
|
|
77
137
|
if (known) return known
|
|
78
138
|
const document = docs().find(doc => doc.dir === dir)
|
|
79
139
|
if (!document) throw new RequestError(404, `no document ${JSON.stringify(dir)}`)
|
|
80
|
-
const session = new
|
|
140
|
+
const session = new CompileSession(dir, new Compiler(join(root, document.entry), { ...compilerOptions(), priority: 'interactive', execution: { env: 'serve' } }), log)
|
|
141
|
+
let configKey: string | undefined
|
|
81
142
|
session.onCompile(async (compilation) => {
|
|
82
|
-
|
|
143
|
+
const next = serverConfigKey(compilation)
|
|
144
|
+
if (configKey !== undefined && configKey !== next) void server?.unmount(dir, { preserveFocus: true }).catch((error: unknown) => {
|
|
145
|
+
log.error(`the previous server contributions of ${dir || '.'} did not close: ${errorMessage(error)}`)
|
|
146
|
+
})
|
|
147
|
+
configKey = next
|
|
148
|
+
for (const finding of untranslatedFindings(compilation, root)) {
|
|
83
149
|
if (!compilation.doc.diagnostics.some(f => f.message === finding.message)) compilation.doc.diagnostics.push(finding)
|
|
84
150
|
}
|
|
85
151
|
})
|
|
86
152
|
session.onCompile((compilation, changed) => {
|
|
87
|
-
|
|
88
|
-
|
|
153
|
+
server?.diagnostics.compiled(dir, compilation.doc, root, irRevision(compilation.doc))
|
|
154
|
+
workbench?.compiled(dir, compilation, changed)
|
|
155
|
+
for (const listener of agentToolListeners) listener(dir)
|
|
156
|
+
void site?.update(dir, compilation).catch((error: unknown) => log.warn(`the site did not take ${dir}: ${errorMessage(error)}`))
|
|
89
157
|
})
|
|
90
|
-
|
|
158
|
+
session.onFailure(error => server?.diagnostics.failed(dir, errorMessage(error), { file: relative(root, session.compiler.entry).replace(/\\/g, '/') }))
|
|
159
|
+
compileSessions.set(dir, session)
|
|
91
160
|
return session
|
|
92
161
|
}
|
|
93
162
|
|
|
94
|
-
const
|
|
163
|
+
const compilerOptions = (): CompilerOptions => ({
|
|
95
164
|
workspaceRoot: root,
|
|
96
165
|
settings: config.settings,
|
|
97
|
-
services: [...
|
|
166
|
+
services: [...serverServices, ...workspaceServices],
|
|
98
167
|
bundled: options.bundled,
|
|
99
|
-
|
|
100
|
-
secret: name => env[name],
|
|
168
|
+
operatorDir: operatorDirectory,
|
|
169
|
+
secret: operatorSpeechSecrets(operatorDirectory, name => env[name]),
|
|
101
170
|
})
|
|
102
171
|
|
|
172
|
+
// Coalesce only in-flight discovery. A later read reassembles actual services and problems;
|
|
173
|
+
// source edits or a repaired plugin cannot be hidden behind a retained capability cache.
|
|
174
|
+
const nodeServiceLoads = new Map<string, NodeServiceLoad>()
|
|
175
|
+
const nodeServicePending = new Set<NodeServiceLoad>()
|
|
176
|
+
let authoringServicesClosed = false
|
|
177
|
+
const authoringServices = (document: string, source: CanonicalSource): Promise<AuthoringServices> => {
|
|
178
|
+
if (authoringServicesClosed) throw new RequestError(503, 'document-services-unavailable')
|
|
179
|
+
const key = JSON.stringify(source), known = nodeServiceLoads.get(document)
|
|
180
|
+
if (known?.key === key) return known.promise
|
|
181
|
+
const selected = docs().find(doc => doc.dir === document)
|
|
182
|
+
if (!selected) throw new RequestError(404, 'authoring-document-missing')
|
|
183
|
+
const controller = new AbortController()
|
|
184
|
+
const promise: Promise<AuthoringServices> = new Compiler(join(root, selected.entry), compilerOptions()).nodeServices({ signal: controller.signal }).then(loaded => {
|
|
185
|
+
if (loaded.source.documentId !== source.documentId || loaded.source.version !== source.version || loaded.source.fingerprint !== source.fingerprint) throw new RequestError(409, 'document-version-changed')
|
|
186
|
+
return { audio: loaded.service(PRODUCTION_AUDIO_AUTHORING), speech: loaded.service(SPEECH), voiceover: loaded.service(VOICEOVER), voiceoverExecution: loaded.service(VOICEOVER_EXECUTION), findings: loaded.findings }
|
|
187
|
+
}).finally(() => { if (nodeServiceLoads.get(document) === load) nodeServiceLoads.delete(document); nodeServicePending.delete(load) })
|
|
188
|
+
const load: NodeServiceLoad = { key, promise, controller }
|
|
189
|
+
nodeServiceLoads.set(document, load)
|
|
190
|
+
nodeServicePending.add(load)
|
|
191
|
+
return promise
|
|
192
|
+
}
|
|
193
|
+
const closeAuthoring = async () => {
|
|
194
|
+
authoringServicesClosed = true
|
|
195
|
+
transcriptionAdoption?.close()
|
|
196
|
+
for (const load of nodeServicePending) load.controller.abort()
|
|
197
|
+
await Promise.all([projectAuthoring?.close(), transcriptions?.close(), audioAuthoring?.close(), voiceAuthoring?.close(), documentHistory?.close(), authoringClients?.close(), workspaceRoutesOwner?.close?.()])
|
|
198
|
+
await Promise.allSettled([...nodeServicePending].map(load => load.promise))
|
|
199
|
+
nodeServiceLoads.clear()
|
|
200
|
+
}
|
|
201
|
+
|
|
103
202
|
const spec: SiteSpec = siteSpec(config, docs().map(doc => siteDocument(doc)))
|
|
104
203
|
const refreshSpec = (): DocumentsChange => {
|
|
204
|
+
const retained = new Map(spec.documents.map(doc => [doc.dir, doc]))
|
|
205
|
+
const before = [...retained.keys()]
|
|
105
206
|
const now = docs()
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
207
|
+
const listed = now.map(doc => {
|
|
208
|
+
const summary = siteDocument(doc)
|
|
209
|
+
const known = retained.get(doc.dir)
|
|
210
|
+
if (!known) return summary
|
|
211
|
+
// Undoing prose also refreshes the list: retain compiled document modules and translations.
|
|
212
|
+
delete known.lang
|
|
213
|
+
delete known.tags
|
|
214
|
+
return Object.assign(known, summary)
|
|
215
|
+
})
|
|
216
|
+
spec.documents.splice(0, spec.documents.length, ...listed)
|
|
217
|
+
return documentsDiff(before, now.map(doc => doc.dir))
|
|
109
218
|
}
|
|
110
219
|
|
|
111
220
|
const documentsChanged = async (change: DocumentsChange): Promise<void> => {
|
|
112
221
|
for (const dir of change.removed) {
|
|
113
|
-
|
|
114
|
-
|
|
222
|
+
server?.diagnostics.forget(dir)
|
|
223
|
+
compileSessions.get(dir)?.close()
|
|
224
|
+
compileSessions.delete(dir)
|
|
225
|
+
for (const listener of agentToolListeners) listener(dir)
|
|
115
226
|
}
|
|
116
227
|
const listed = refreshSpec()
|
|
117
228
|
const removed = [...new Set([...change.removed, ...listed.removed])]
|
|
118
229
|
for (const dir of new Set([...removed, ...change.added, ...listed.added])) await server?.unmount(dir)
|
|
119
|
-
await
|
|
120
|
-
await service?.removed(removed)
|
|
230
|
+
await workbench?.documentsRemoved(removed)
|
|
121
231
|
}
|
|
122
232
|
|
|
123
233
|
let lookedAt = 0
|
|
124
234
|
const lookAtDisk = (): void => {
|
|
125
235
|
if (Date.now() - lookedAt < DISK_LOOK_MS) return
|
|
126
236
|
lookedAt = Date.now()
|
|
127
|
-
const
|
|
128
|
-
const known = spec.documents.map(doc => doc.dir)
|
|
129
|
-
const change = { removed: known.filter(dir => !now.includes(dir)), added: now.filter(dir => !known.includes(dir)) }
|
|
237
|
+
const change = documentsDiff(spec.documents.map(doc => doc.dir), docs().map(doc => doc.dir))
|
|
130
238
|
if (change.removed.length || change.added.length) void documentsChanged(change).catch((error: unknown) => log.warn(`the documents on disk changed, but the site did not follow: ${errorMessage(error)}`))
|
|
131
239
|
}
|
|
132
240
|
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
241
|
+
const openHistory = (): WorkspaceHistory | undefined => {
|
|
242
|
+
if (!Git.isRepository(root)) {
|
|
243
|
+
log.warn(`${root} is not a git repository: editing, the document operations and the history are not available (run git init)`)
|
|
244
|
+
return undefined
|
|
245
|
+
}
|
|
246
|
+
const git = new Git(root)
|
|
247
|
+
const revisions = new Revisions(git, { config, writer })
|
|
248
|
+
return {
|
|
249
|
+
revisions,
|
|
250
|
+
operations: new DocumentOperations({ config, git, revisions, writer, changed: documentsChanged, locks }),
|
|
251
|
+
git,
|
|
252
|
+
agentSessionReviews: new AgentSessionReviews({ config, revisions, locks, files: writer, log }),
|
|
253
|
+
taskReviews: createTaskReviewService({ config, locks, files: writer, log }),
|
|
254
|
+
}
|
|
142
255
|
}
|
|
143
|
-
let workspace: ReturnType<typeof workspaceOf> | undefined
|
|
144
|
-
const plugin = (): ServedPlugin => workspace!.plugin
|
|
145
256
|
|
|
146
257
|
let started!: () => void
|
|
147
258
|
const ready = new Promise<void>(resolve => started = resolve)
|
|
148
259
|
|
|
149
260
|
const serverHost: ServerHost = {
|
|
261
|
+
workspaceRoot: root,
|
|
262
|
+
get authoring() { return documentEditor },
|
|
263
|
+
get documentHistory() { return documentHistory?.service },
|
|
264
|
+
get projectAuthoring() { return projectAuthoring?.service },
|
|
265
|
+
get authoringClients() { return authoringClients?.service },
|
|
266
|
+
get audioAuthoring() { return audioAuthoring?.service },
|
|
267
|
+
get imageAuthoring() { return imageAuthoring?.service },
|
|
268
|
+
get voiceAuthoring() { return voiceAuthoring?.service },
|
|
269
|
+
get transcriptions() { return transcriptions },
|
|
270
|
+
get transcriptionAdoption() { return transcriptionAdoption?.service },
|
|
271
|
+
get routes() { return workspaceRoutesOwner?.routes ? { [WORKSPACE_ID]: workspaceRoutesOwner.routes, ...voiceAuthoring ? { 'presmium.lecture': authorizeWorkspaceRoutes(voiceAuthoring.routes, launcherToken) } : {} } : undefined },
|
|
272
|
+
agentTools(document) {
|
|
273
|
+
const compiled = (document === undefined ? [...compileSessions.values()] : [compileSessions.get(document)]).flatMap(session => session?.latest ? [session.latest] : [])
|
|
274
|
+
const tools = new Map(compiled.flatMap(compilation => compilation.plugins.flatMap(loaded => loaded.plugin?.tools ?? [])).map(tool => [tool.name, tool]))
|
|
275
|
+
return { tools: [...tools.values()], service: <T>(token: ServiceToken<T>) => compiled[0]?.service(token) }
|
|
276
|
+
},
|
|
277
|
+
watchAgentTools(listener) { agentToolListeners.add(listener); return () => { agentToolListeners.delete(listener) } },
|
|
278
|
+
async readSource(path, member) {
|
|
279
|
+
await ready
|
|
280
|
+
return editor.read(editor.scope(''), path, member)
|
|
281
|
+
},
|
|
150
282
|
async documents() {
|
|
151
|
-
return
|
|
283
|
+
return documentSummaries(config)
|
|
152
284
|
},
|
|
153
285
|
async compile(document) {
|
|
154
286
|
await ready
|
|
155
|
-
const { doc } = await
|
|
287
|
+
const { doc } = await compileSessionOf(document).ready()
|
|
156
288
|
return { doc, diagnostics: doc.diagnostics }
|
|
157
289
|
},
|
|
158
290
|
async check(document) {
|
|
159
291
|
await ready
|
|
160
|
-
const
|
|
161
|
-
|
|
292
|
+
const session = compileSessionOf(document)
|
|
293
|
+
const compilation = await session.ready()
|
|
294
|
+
const findings = await check(compilation)
|
|
295
|
+
if (session.latest === compilation) server?.diagnostics.replace({ document, origin: 'check' }, findings, { root, irRevision: irRevision(compilation.doc) })
|
|
296
|
+
return [...compilation.doc.diagnostics, ...findings]
|
|
162
297
|
},
|
|
163
298
|
async build(document, buildOptions = {}) {
|
|
164
299
|
await ready
|
|
165
|
-
|
|
166
|
-
const
|
|
167
|
-
const
|
|
168
|
-
|
|
300
|
+
// As jobs, which the workbench shows: one a renderer, waited for.
|
|
301
|
+
const targets = buildOptions.to ?? (await jobs.targets(document)).filter(target => target.kind === 'render').map(target => target.id)
|
|
302
|
+
const ended = await Promise.all((await Promise.all(targets.map(target => jobs.start(document, target)))).map(job => jobs.wait(job.id)))
|
|
303
|
+
const outputs = ended.filter(job => job.state === 'done').map(job => ({ renderer: job.target, outDir: jobs.directory(job) }))
|
|
304
|
+
const findings: Finding[] = ended.flatMap(job => [
|
|
305
|
+
...job.state === 'failed' ? [{ checkId: 'presmium.render', severity: 'error' as const, message: `${job.target}: ${job.error ?? job.state}` }] : [],
|
|
306
|
+
...job.warnings.map(message => ({ checkId: 'presmium.render', severity: 'warning' as const, message })),
|
|
307
|
+
])
|
|
308
|
+
return { outputs, findings }
|
|
169
309
|
},
|
|
310
|
+
jobs,
|
|
170
311
|
async run(document, target, signal) {
|
|
171
312
|
await ready
|
|
172
|
-
return runBlock(await
|
|
313
|
+
return runBlock(await compileSessionOf(document).ready(), target, signal)
|
|
173
314
|
},
|
|
174
315
|
}
|
|
175
316
|
|
|
176
317
|
let operators: ReturnType<typeof workspacePlugins> | undefined
|
|
177
318
|
let home: string | undefined
|
|
178
|
-
const
|
|
319
|
+
const loadOperatorPlugins = async () => {
|
|
179
320
|
await ready
|
|
180
321
|
home ??= mkdtempSync(join(tmpdir(), 'presmium-home-'))
|
|
181
|
-
return operators ??= workspacePlugins(root,
|
|
322
|
+
return operators ??= workspacePlugins(root, compilerOptions())
|
|
182
323
|
}
|
|
183
324
|
const homeDocument = async (): Promise<ServedDocument> => {
|
|
184
|
-
const loaded = await
|
|
325
|
+
const loaded = await loadOperatorPlugins()
|
|
185
326
|
for (const problem of loaded.problems) log.warn(`the home page: ${problem.message}`)
|
|
186
|
-
const own = (token: ServiceToken<unknown>) => [...
|
|
327
|
+
const own = (token: ServiceToken<unknown>) => [...serverServices, ...workspaceServices].find(([known]) => known.id === token.id)?.[1]
|
|
187
328
|
return {
|
|
188
329
|
deckDir: home!,
|
|
189
330
|
workspaceRoot: home!,
|
|
190
|
-
plugins: [
|
|
331
|
+
plugins: [routesPlugin!, ...loaded.plugins.flatMap(({ plugin: node }) => (node ? [{ id: node.id, plugin: node, config: {} }] : []))],
|
|
191
332
|
service: <T>(token: ServiceToken<T>) => (own(token) ?? loaded.service(token)) as T | undefined,
|
|
192
333
|
settings: config.settings,
|
|
193
|
-
|
|
334
|
+
operatorDir: operatorDirectory,
|
|
194
335
|
home: true,
|
|
195
336
|
}
|
|
196
337
|
}
|
|
197
338
|
|
|
198
|
-
const stateFile = join(stateDir
|
|
339
|
+
const stateFile = join(stateDir, 'server.json')
|
|
199
340
|
server = await startServer({
|
|
200
341
|
mode: 'serve',
|
|
201
|
-
|
|
342
|
+
listenAddress: options.listenAddress,
|
|
202
343
|
port: options.port,
|
|
344
|
+
...options.publicUrl ? { publicUrl: options.publicUrl } : {},
|
|
203
345
|
stateFile,
|
|
204
|
-
forward: () => {
|
|
346
|
+
forward: async (path) => {
|
|
205
347
|
lookAtDisk()
|
|
206
|
-
return
|
|
348
|
+
if (path.startsWith(outputsPath)) return await jobs.origin()
|
|
349
|
+
if (!site) return undefined
|
|
350
|
+
// Straight to the dev server of the path's document, not through the site's router: one hop less per module.
|
|
351
|
+
try {
|
|
352
|
+
return await site.target(path)
|
|
353
|
+
} catch {
|
|
354
|
+
return site.forward()
|
|
355
|
+
}
|
|
207
356
|
},
|
|
208
357
|
settings: config.settings,
|
|
209
358
|
approvals: config.approvals,
|
|
210
|
-
|
|
359
|
+
operatorDir: operatorDirectory,
|
|
211
360
|
serverHost,
|
|
212
361
|
env,
|
|
213
362
|
...options.approvalTimeoutMs ? { approvalTimeoutMs: options.approvalTimeoutMs } : {},
|
|
214
363
|
log,
|
|
215
364
|
async document(dir) {
|
|
216
365
|
await ready
|
|
217
|
-
if (dir === '' && !config.
|
|
366
|
+
if (dir === '' && !config.singleDeck) return await homeDocument()
|
|
218
367
|
if (!docs().some(doc => doc.dir === dir)) return undefined
|
|
219
|
-
const compilation = await
|
|
368
|
+
const compilation = await compileSessionOf(dir).ready()
|
|
220
369
|
const served = servedDocument(compilation)
|
|
221
|
-
const
|
|
222
|
-
const service = <T>(token: ServiceToken<T>) => served.service(token) ?? (
|
|
223
|
-
return { ...served, service, plugins: [
|
|
370
|
+
const taskReviews = history?.taskReviews
|
|
371
|
+
const service = <T>(token: ServiceToken<T>) => served.service(token) ?? (taskReviews && token.id === CHANGE_REVIEW.id ? taskReviews as T : undefined)
|
|
372
|
+
return { ...served, service, plugins: [routesPlugin!, ...served.plugins] }
|
|
224
373
|
},
|
|
225
374
|
})
|
|
226
375
|
const running = server
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
376
|
+
serverServices = running.services
|
|
377
|
+
voiceAuthoring = createDocumentVoiceAuthoring({ editor, services: authoringServices, revision: document => { const latest = compileSessions.get(document)?.latest; return latest ? irRevision(latest.doc) : undefined } })
|
|
378
|
+
workbench = createWorkbenchBackend({ root, compileSessionOf, serverHost, voices: voiceAuthoring.service, diagnostics: running.diagnostics, url: () => options.publicUrl ? new URL(options.publicUrl).origin : running.url, basePath, preview, log, warmSite: dir => site?.warm(dir) ?? Promise.resolve(), viewerPage: (dir, wait) => site?.app(dir, wait) ?? Promise.resolve(undefined) })
|
|
379
|
+
history = openHistory()
|
|
380
|
+
documentEditor = createDocumentEditor({ productionPlan: async (document, source) => { const audio = (await authoringServices(document, source)).audio; if (!audio) throw new RequestError(503, 'caption-production-unavailable'); return audio.readPlan({ source }) }, editor, revisions: history?.revisions, reviewRequired: config.settings['presmium.assistant']?.review === 'required' })
|
|
381
|
+
projectAuthoring = createProjectAuthoring({ config, documentEditor, operations: history?.operations, revisions: history?.revisions, git: history?.git })
|
|
382
|
+
authoringClients = createAuthoringClients(editor)
|
|
383
|
+
documentHistory = createDocumentHistory({ config, editor, locks, revisions: history?.revisions, reviewRequired: config.settings['presmium.assistant']?.review === 'required', changed: (before, after) => documentsChanged(documentsDiff(before, after)), reverted: (target, revision) => history?.agentSessionReviews.reverted(target, revision) })
|
|
384
|
+
transcriptions = createDocumentTranscriptions({ root, editor, services: authoringServices })
|
|
385
|
+
transcriptionAdoption = createTranscriptionAdoption({ documentEditor, transcriptions })
|
|
386
|
+
audioAuthoring = createDocumentAudioAuthoring({ root, editor, revisions: history?.revisions, services: authoringServices, reviewRequired: config.settings['presmium.assistant']?.review === 'required' })
|
|
387
|
+
imageAuthoring = createDocumentImageAuthoring({ root, editor, revisions: history?.revisions, reviewRequired: config.settings['presmium.assistant']?.review === 'required' })
|
|
388
|
+
const routeOptions = {
|
|
389
|
+
speechSettings: speechSettingsRoute(operatorDirectory, name => env[name]),
|
|
390
|
+
config,
|
|
391
|
+
editor,
|
|
392
|
+
documentEditor,
|
|
393
|
+
transcriptions,
|
|
394
|
+
transcriptionAdoptionRoutes: transcriptionAdoption.routes,
|
|
395
|
+
audioAuthoringRoutes: audioAuthoring.routes,
|
|
396
|
+
imageAuthoringRoutes: imageAuthoring.routes,
|
|
397
|
+
historyRoutes: documentHistory.routes,
|
|
398
|
+
authoringClientRoutes: authoringClients.routes,
|
|
399
|
+
projectRoutes: projectAuthoring.routes,
|
|
400
|
+
locks,
|
|
401
|
+
...history ? { revisions: history.revisions, operations: history.operations, reviews: history.agentSessionReviews } : {},
|
|
402
|
+
documentsChanged,
|
|
403
|
+
workbench,
|
|
404
|
+
jobs,
|
|
405
|
+
...launcherToken ? { launcherToken } : {},
|
|
406
|
+
}
|
|
407
|
+
workspaceRoutesOwner = workspaceRoutes(routeOptions)
|
|
408
|
+
routesPlugin = { id: WORKSPACE_ID, plugin: workspaceRoutesPlugin(routeOptions, workspaceRoutesOwner.routes), config: {} }
|
|
409
|
+
workspaceServices = [
|
|
410
|
+
...history ? [[REVISIONS, history.revisions] as [ServiceToken<unknown>, unknown]] : [],
|
|
411
|
+
[DOCUMENT_FILES, writer],
|
|
233
412
|
[DOCUMENT_LOCKS, locks],
|
|
413
|
+
[DOCUMENT_VOICE_TASKS, voiceAuthoring.service],
|
|
234
414
|
]
|
|
235
|
-
|
|
415
|
+
try {
|
|
416
|
+
await history?.agentSessionReviews.recover(docs().map(doc => doc.dir))
|
|
417
|
+
} catch (error) {
|
|
418
|
+
history?.agentSessionReviews.close()
|
|
419
|
+
await closeAuthoring()
|
|
420
|
+
await workbench.close()
|
|
421
|
+
await running.close()
|
|
422
|
+
throw error
|
|
423
|
+
}
|
|
236
424
|
|
|
237
425
|
started()
|
|
238
|
-
if (config.single) for (const { dir } of docs()) void sessionOf(dir).ready().catch(() => undefined)
|
|
239
426
|
|
|
240
|
-
const
|
|
241
|
-
const compilation = await
|
|
242
|
-
|
|
427
|
+
const compileForSite = async (dir: string): Promise<IRDocument> => {
|
|
428
|
+
const compilation = await compileSessionOf(dir).ready()
|
|
429
|
+
refreshDocumentVitePlugins(spec, dir, compilation)
|
|
430
|
+
return workbench!.stampPreviewRevision(compilation, siteDocumentIR(compilation))
|
|
243
431
|
}
|
|
244
432
|
try {
|
|
245
|
-
site = await
|
|
433
|
+
site = await startWorkspaceSite({ root, spec, basePath, bundled: options.bundled, stateDir, compile: compileForSite, stampPreviewRevision: (compilation, doc) => workbench!.stampPreviewRevision(compilation, doc), diagnostics: running.diagnostics, preview, log })
|
|
246
434
|
} catch (error) {
|
|
247
|
-
await
|
|
435
|
+
await closeAuthoring()
|
|
436
|
+
await workbench.close()
|
|
248
437
|
await running.close()
|
|
249
438
|
if (home) rmSync(home, { recursive: true, force: true })
|
|
250
439
|
throw error
|
|
@@ -255,9 +444,11 @@ export async function serveWorkspace(options: ServeOptions): Promise<WorkspaceSe
|
|
|
255
444
|
root,
|
|
256
445
|
mcp: { url: `${running.url}/mcp`, stateFile },
|
|
257
446
|
async close() {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
447
|
+
history?.agentSessionReviews.close()
|
|
448
|
+
await closeAuthoring()
|
|
449
|
+
for (const session of compileSessions.values()) session.close()
|
|
450
|
+
await jobs.close()
|
|
451
|
+
await workbench?.close()
|
|
261
452
|
await site?.close()
|
|
262
453
|
await running.close()
|
|
263
454
|
if (home) rmSync(home, { recursive: true, force: true })
|
|
@@ -265,38 +456,48 @@ export async function serveWorkspace(options: ServeOptions): Promise<WorkspaceSe
|
|
|
265
456
|
}
|
|
266
457
|
}
|
|
267
458
|
|
|
268
|
-
interface
|
|
269
|
-
forward(): string | undefined
|
|
270
|
-
update(dir: string, compilation: Compilation): Promise<void> | void
|
|
271
|
-
documentsChanged(removed: string[]): Promise<void>
|
|
272
|
-
close(): Promise<void>
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
interface LocalSiteOptions {
|
|
459
|
+
interface WorkspaceSiteOptions {
|
|
276
460
|
root: string
|
|
461
|
+
basePath: string
|
|
277
462
|
spec: SiteSpec
|
|
278
463
|
bundled?: string
|
|
279
|
-
|
|
464
|
+
stateDir: string
|
|
280
465
|
compile(dir: string): Promise<IRDocument>
|
|
281
|
-
|
|
466
|
+
stampPreviewRevision(compilation: Compilation, doc: IRDocument): IRDocument
|
|
282
467
|
preview: { origins: string[] }
|
|
283
468
|
log: Logger
|
|
469
|
+
diagnostics: DiagnosticStore
|
|
284
470
|
}
|
|
285
471
|
|
|
286
|
-
|
|
472
|
+
type WorkspaceSite = Awaited<ReturnType<typeof startWorkspaceSite>>
|
|
473
|
+
|
|
474
|
+
/** The workspace site's dev router in this process, which serve forwards the site's paths to. */
|
|
475
|
+
async function startWorkspaceSite(options: WorkspaceSiteOptions) {
|
|
287
476
|
const { root, spec, log } = options
|
|
288
477
|
const renderer = await webRenderer(root, options.bundled)
|
|
289
478
|
const findings: Finding[] = []
|
|
290
|
-
const work = join(options.
|
|
291
|
-
const ctx = siteContext({ root, outDir: work, workDir: work, findings })
|
|
479
|
+
const work = join(options.stateDir, 'site', 'full')
|
|
480
|
+
const ctx = siteContext({ root, outDir: work, workDir: work, base: options.basePath, findings })
|
|
292
481
|
ctx.preview = options.preview
|
|
293
482
|
const dev = await renderer.devSite!(spec, ctx, options.compile)
|
|
294
483
|
const origin = new URL(dev.url).origin
|
|
295
484
|
log.info(`the workspace site's dev router is at ${origin} (this machine only; pages reach it through serve)`)
|
|
296
485
|
return {
|
|
297
|
-
forward: () => origin,
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
486
|
+
forward: (): string => origin,
|
|
487
|
+
target: (path: string): Promise<string> => dev.target?.(path) ?? Promise.resolve(origin),
|
|
488
|
+
update: (dir: string, compilation: Compilation): Promise<void> => {
|
|
489
|
+
refreshDocumentVitePlugins(spec, dir, compilation)
|
|
490
|
+
const revision = irRevision(compilation.doc)
|
|
491
|
+
return dev.update(dir, options.stampPreviewRevision(compilation, siteDocumentIR(compilation))).then(() => {
|
|
492
|
+
if (options.diagnostics.irRevision(dir) === revision) options.diagnostics.clear(dir, 'preview', 'web')
|
|
493
|
+
}, (error: unknown) => {
|
|
494
|
+
const message = `the site did not take ${dir}: ${errorMessage(error)}`
|
|
495
|
+
if (options.diagnostics.irRevision(dir) === revision) options.diagnostics.replace({ document: dir, origin: 'preview', renderer: 'web' }, [{ checkId: 'presmium.preview', severity: 'error', message }], { irRevision: revision })
|
|
496
|
+
log.warn(message)
|
|
497
|
+
})
|
|
498
|
+
},
|
|
499
|
+
warm: (dir: string): Promise<void> => dev.warm?.(dir) ?? Promise.resolve(),
|
|
500
|
+
app: (dir: string, wait: boolean): Promise<string | undefined> => dev.app?.(dir, wait) ?? Promise.resolve(undefined),
|
|
501
|
+
close: (): Promise<void> => dev.close(),
|
|
301
502
|
}
|
|
302
503
|
}
|