@lincyaw/kernel 1.0.0-rc.0 → 1.0.0-rc.1
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 +7 -7
- package/package.json +6 -2
- package/src/access.ts +18 -0
- package/src/anchors.ts +5 -1
- package/src/authoring.ts +56 -0
- package/src/blocks.ts +0 -8
- package/src/component-code.ts +6 -0
- package/src/contracts/agent.ts +5 -82
- package/src/contracts/anchors.ts +46 -0
- package/src/contracts/authoring-actions.ts +60 -0
- package/src/contracts/authoring-clients.ts +213 -0
- package/src/contracts/authoring.ts +180 -0
- package/src/contracts/checks.ts +29 -0
- package/src/contracts/classroom.ts +49 -0
- package/src/contracts/diagnostics.ts +37 -0
- package/src/contracts/document-audio.ts +150 -0
- package/src/contracts/document-comments.ts +31 -0
- package/src/contracts/document-history.ts +63 -0
- package/src/contracts/document-image.ts +32 -0
- package/src/contracts/document-reads.ts +57 -0
- package/src/contracts/document-voice.ts +44 -0
- package/src/contracts/document.ts +240 -0
- package/src/contracts/execution.ts +4 -5
- package/src/contracts/locale.ts +3 -0
- package/src/contracts/offline.ts +2 -0
- package/src/contracts/plugin.ts +183 -0
- package/src/contracts/presenter-panels.ts +17 -0
- package/src/contracts/preview.ts +305 -310
- package/src/contracts/production-assets.ts +61 -0
- package/src/contracts/production.ts +109 -0
- package/src/contracts/projects.ts +59 -0
- package/src/contracts/server.ts +1 -51
- package/src/contracts/speech-settings.ts +16 -0
- package/src/contracts/speech.ts +82 -70
- package/src/contracts/steps.ts +15 -0
- package/src/contracts/transcription-adoption.ts +79 -0
- package/src/contracts/transcription.ts +112 -0
- package/src/contracts/voice.ts +97 -0
- package/src/contracts/voiceover-execution.ts +68 -0
- package/src/contracts/workspace.ts +458 -0
- package/src/contracts.ts +28 -278
- package/src/diagnostics.ts +7 -0
- package/src/document-authoring-actions.ts +47 -0
- package/src/document-caption-timings.ts +64 -0
- package/src/document-colors.ts +25 -0
- package/src/document-comments.ts +118 -0
- package/src/document-components.ts +251 -0
- package/src/document-operations.ts +132 -0
- package/src/document-pages.ts +215 -0
- package/src/document-reads.ts +81 -0
- package/src/document-schema.ts +85 -0
- package/src/document-voices.ts +153 -0
- package/src/document.ts +252 -0
- package/src/durations.ts +7 -0
- package/src/errors.ts +8 -0
- package/src/execution.ts +5 -1
- package/src/fingerprint.ts +1 -1
- package/src/follow.ts +26 -30
- package/src/git.ts +1 -1
- package/src/index.ts +66 -13
- package/src/ir.ts +54 -5
- package/src/jupyter-kernel.ts +9 -0
- package/src/kernel.ts +38 -25
- package/src/keyboard.ts +43 -0
- package/src/locale.ts +13 -0
- package/src/lock.ts +4 -8
- package/src/logging.ts +1 -1
- package/src/{markdown.ts → lower-myst.ts} +2 -1
- package/src/node/assets.ts +23 -0
- package/src/node/authoring-actions.ts +50 -0
- package/src/node/authoring-clients.ts +14 -0
- package/src/node/document-audio.ts +22 -0
- package/src/node/document-history.ts +11 -0
- package/src/node/document-image.ts +8 -0
- package/src/node/document-reads.ts +10 -0
- package/src/node/document-voice.ts +7 -0
- package/src/node/ffmpeg.ts +29 -0
- package/src/node/file-version.ts +6 -0
- package/src/node/fonts.ts +22 -0
- package/src/node/index.ts +29 -133
- package/src/node/installed-packages.ts +11 -0
- package/src/node/lines.ts +24 -0
- package/src/node/locale.ts +7 -0
- package/src/node/network.ts +11 -0
- package/src/node/operator-speech.ts +91 -0
- package/src/node/packages.ts +74 -0
- package/src/node/paths.ts +7 -0
- package/src/node/projects.ts +7 -0
- package/src/node/resources.ts +86 -0
- package/src/node/revision.ts +6 -0
- package/src/node/run-directory.ts +70 -0
- package/src/node/transcription-adoption.ts +9 -0
- package/src/node/transcription.ts +10 -0
- package/src/node/voiceover-routes.ts +73 -0
- package/src/node/voiceover.ts +199 -0
- package/src/page-source.ts +359 -0
- package/src/parameters.ts +81 -0
- package/src/preview-edit.ts +18 -0
- package/src/preview-events.ts +57 -0
- package/src/preview-frame.ts +1220 -0
- package/src/props.ts +17 -0
- package/src/push-stream.ts +67 -0
- package/src/reading.ts +12 -7
- package/src/recording.ts +38 -0
- package/src/renderer-components.ts +30 -0
- package/src/responses.ts +49 -0
- package/src/run-outputs.ts +15 -0
- package/src/runner.py +3 -3
- package/src/services.ts +6 -6
- package/src/slides.ts +36 -0
- package/src/speech-words.ts +76 -0
- package/src/speech.ts +13 -0
- package/src/step-keys.ts +38 -0
- package/src/steps.ts +7 -0
- package/src/transcription-adoption.ts +105 -0
- package/src/uri-decoding.ts +8 -0
- package/src/urls.ts +13 -0
- package/src/views.ts +47 -0
- package/src/voice.ts +86 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Validator } from '@cfworker/json-schema'
|
|
2
|
+
import type { Schema } from '@cfworker/json-schema'
|
|
3
|
+
import { DOCUMENT_JSON_SCHEMA } from '../document-schema.ts'
|
|
4
|
+
import { DOCUMENT_PAGE_DUPLICATION_SCHEMA } from '../contracts/authoring.ts'
|
|
5
|
+
import { DOCUMENT_COMPONENT_IMPACT_COLLECTIONS, DOCUMENT_PARAMETER_MAX_CHANGES, DOCUMENT_PARAMETER_MAX_PATH_DEPTH } from '../contracts/authoring-actions.ts'
|
|
6
|
+
import type { DocumentAuthoringActionRequest, DocumentAuthoringActionReference, DocumentComponentImpactQuery } from '../contracts/authoring-actions.ts'
|
|
7
|
+
|
|
8
|
+
const id: Schema = { type: 'string', minLength: 1, maxLength: 200 }
|
|
9
|
+
const object = (properties: Record<string, Schema>, required = Object.keys(properties)): Schema => ({ type: 'object', additionalProperties: false, properties, required })
|
|
10
|
+
const action = (type: string, properties: Record<string, Schema>, required = Object.keys(properties)) => object({ type: { const: type }, ...properties }, ['type', ...required])
|
|
11
|
+
const content: Schema = { type: 'array', minItems: 1, maxItems: 100, items: { $ref: '#/$defs/RichTextNode' } }
|
|
12
|
+
const metadata: Schema = { $ref: '#/$defs/JsonObject' }
|
|
13
|
+
const scope: Schema = { oneOf: [object({ kind: { const: 'document' } }), object({ kind: { const: 'page' }, sectionId: id }), object({ kind: { const: 'step' }, stepId: id }), object({ kind: { const: 'steps' }, stepIds: { type: 'array', minItems: 1, maxItems: 1000, items: id } })] }
|
|
14
|
+
const voice: Schema = { oneOf: [{ type: 'null' }, { allOf: [{ $ref: '#/$defs/VoiceSource' }, { properties: { type: { enum: ['none', 'tts'] } } }] }] }
|
|
15
|
+
const parameterPath: Schema = { type: 'array', minItems: 1, maxItems: DOCUMENT_PARAMETER_MAX_PATH_DEPTH, items: { type: 'string', maxLength: 200 } }
|
|
16
|
+
const changes: Schema = { type: 'array', minItems: 1, maxItems: DOCUMENT_PARAMETER_MAX_CHANGES, items: { oneOf: [
|
|
17
|
+
object({ op: { const: 'set' }, path: parameterPath, value: { $ref: '#/$defs/JsonValue' } }),
|
|
18
|
+
object({ op: { const: 'remove' }, path: parameterPath }),
|
|
19
|
+
] } }
|
|
20
|
+
|
|
21
|
+
/** Node-only schema reuses the actual canonical rich-text definitions, not BlockNote data. */
|
|
22
|
+
export const DOCUMENT_AUTHORING_ACTION_SCHEMA: Schema = {
|
|
23
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
24
|
+
...object({ id, source: DOCUMENT_PAGE_DUPLICATION_SCHEMA.properties.source as unknown as Schema, action: { $ref: '#/$defs/AuthoringAction' } }),
|
|
25
|
+
$defs: { ...(structuredClone(DOCUMENT_JSON_SCHEMA) as Exclude<Schema, boolean>).$defs,
|
|
26
|
+
AuthoringAction: { oneOf: [
|
|
27
|
+
action('comment.create', { content, anchor: { oneOf: [{ type: 'null' }, object({ from: { $ref: '#/$defs/TextPoint' }, to: { $ref: '#/$defs/TextPoint' } })] }, metadata, commentMetadata: metadata }, ['content', 'anchor']),
|
|
28
|
+
action('comment.reply', { threadId: id, content, metadata }, ['threadId', 'content']),
|
|
29
|
+
action('comment.update', { threadId: id, commentId: id, content, metadata }, ['threadId', 'commentId', 'content']),
|
|
30
|
+
action('comment.react', { threadId: id, commentId: id, emoji: { type: 'string', minLength: 1, maxLength: 32 }, active: { type: 'boolean' } }),
|
|
31
|
+
action('comment.resolve', { threadId: id }), action('comment.reopen', { threadId: id }),
|
|
32
|
+
action('comment.delete', { threadId: id, commentId: id }, ['threadId']),
|
|
33
|
+
action('voice.set', { scope, source: voice, replaceOverrides: { type: 'boolean' } }, ['scope', 'source']),
|
|
34
|
+
action('caption.timing.set', { stepId: id, value: { $ref: '#/$defs/CaptionTiming' } }),
|
|
35
|
+
action('caption.timing.reset', { stepId: id }),
|
|
36
|
+
action('component.parameters.patch', { blockId: id, changes }),
|
|
37
|
+
action('component.step-parameters.patch', { blockId: id, stepId: id, changes }),
|
|
38
|
+
action('component.step-parameters.reset', { blockId: id, stepId: id }),
|
|
39
|
+
action('component.definition.use', { blockId: id, definitionId: id, changes: { ...changes, minItems: 0 } }, ['blockId', 'definitionId']),
|
|
40
|
+
] },
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
export const DOCUMENT_AUTHORING_ACTION_APPLY_SCHEMA: Schema = object({ planId: id, source: DOCUMENT_PAGE_DUPLICATION_SCHEMA.properties.source as unknown as Schema, digest: { type: 'string', pattern: '^[a-f0-9]{64}$' }, confirmDeletion: { const: true } }, ['planId', 'source', 'digest'])
|
|
44
|
+
export const DOCUMENT_AUTHORING_ACTION_IMPACT_SCHEMA: Schema = object({ planId: id, digest: { type: 'string', pattern: '^[a-f0-9]{64}$' }, collection: { enum: [...DOCUMENT_COMPONENT_IMPACT_COLLECTIONS] }, offset: { type: 'integer', minimum: 0, maximum: Number.MAX_SAFE_INTEGER } }, ['planId', 'digest', 'collection'])
|
|
45
|
+
const request = new Validator(structuredClone(DOCUMENT_AUTHORING_ACTION_SCHEMA), '2020-12', false)
|
|
46
|
+
const reference = new Validator(structuredClone(DOCUMENT_AUTHORING_ACTION_APPLY_SCHEMA), '2020-12', false)
|
|
47
|
+
const impact = new Validator(structuredClone(DOCUMENT_AUTHORING_ACTION_IMPACT_SCHEMA), '2020-12', false)
|
|
48
|
+
export function validateDocumentAuthoringAction(value: unknown): value is DocumentAuthoringActionRequest { return request.validate(value).valid }
|
|
49
|
+
export function validateDocumentAuthoringActionReference(value: unknown): value is DocumentAuthoringActionReference { return reference.validate(value).valid }
|
|
50
|
+
export function validateDocumentAuthoringActionImpact(value: unknown): value is DocumentComponentImpactQuery { return impact.validate(value).valid }
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Validator } from '@cfworker/json-schema'
|
|
2
|
+
import { AUTHORING_CLIENT_ACK_SCHEMA, AUTHORING_CLIENT_CONNECTION_SCHEMA, AUTHORING_CLIENT_OPEN_SCHEMA, AUTHORING_CLIENT_QUERY_SCHEMA, AUTHORING_CLIENT_REPORT_SCHEMA, AUTHORING_CLIENT_REQUEST_QUERY_SCHEMA, AUTHORING_CLIENT_REQUEST_SCHEMA } from '../contracts/authoring-clients.ts'
|
|
3
|
+
import type { AuthoringClientAcknowledgement, AuthoringClientLease, AuthoringClientOpen, AuthoringClientReport, AuthoringClientRequest } from '../contracts/authoring-clients.ts'
|
|
4
|
+
|
|
5
|
+
const validator = (schema: typeof AUTHORING_CLIENT_REQUEST_SCHEMA) => new Validator(structuredClone(schema), '2020-12', false)
|
|
6
|
+
const request = validator(AUTHORING_CLIENT_REQUEST_SCHEMA), query = validator(AUTHORING_CLIENT_QUERY_SCHEMA), get = validator(AUTHORING_CLIENT_REQUEST_QUERY_SCHEMA)
|
|
7
|
+
const open = validator(AUTHORING_CLIENT_OPEN_SCHEMA), report = validator(AUTHORING_CLIENT_REPORT_SCHEMA), ack = validator(AUTHORING_CLIENT_ACK_SCHEMA), connection = validator(AUTHORING_CLIENT_CONNECTION_SCHEMA)
|
|
8
|
+
export const validateAuthoringClientRequest = (value: unknown): value is AuthoringClientRequest => request.validate(value).valid
|
|
9
|
+
export const validateAuthoringClientQuery = (value: unknown): value is { clientId?: string, after?: string } => query.validate(value).valid
|
|
10
|
+
export const validateAuthoringClientRequestQuery = (value: unknown): value is { clientId: string, epoch: string, requestId: string } => get.validate(value).valid
|
|
11
|
+
export const validateAuthoringClientOpen = (value: unknown): value is AuthoringClientOpen => open.validate(value).valid
|
|
12
|
+
export const validateAuthoringClientReport = (value: unknown): value is AuthoringClientReport => report.validate(value).valid
|
|
13
|
+
export const validateAuthoringClientAcknowledgement = (value: unknown): value is AuthoringClientAcknowledgement => ack.validate(value).valid
|
|
14
|
+
export const validateAuthoringClientLease = (value: unknown): value is AuthoringClientLease => connection.validate(value).valid
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Validator } from '@cfworker/json-schema'
|
|
2
|
+
import type { Schema } from '@cfworker/json-schema'
|
|
3
|
+
import { DOCUMENT_RECORDING_ADOPTION_SCHEMA, DOCUMENT_AUDIO_INSPECTION_SCHEMA, DOCUMENT_AUDIO_WORKSPACE_IMPORT_SCHEMA, DOCUMENT_WORKSPACE_AUDIO_INSPECTION_SCHEMA, DOCUMENT_MEDIA_CLIP_VALIDATION_SCHEMA, DOCUMENT_MEDIA_CLIP_CHANGE_SCHEMA, DOCUMENT_PRODUCTION_PLAN_SCHEMA } from '../contracts/document-audio.ts'
|
|
4
|
+
import type { DocumentRecordingAdoption, DocumentAudioInspect, DocumentWorkspaceAudioImport, DocumentWorkspaceAudioInspect, DocumentMediaClipValidation, DocumentMediaClipEdit, DocProductionPlanQuery } from '../contracts/document-audio.ts'
|
|
5
|
+
|
|
6
|
+
const inspectValidator = new Validator(structuredClone(DOCUMENT_AUDIO_INSPECTION_SCHEMA) as unknown as Schema, '2020-12', false)
|
|
7
|
+
const importValidator = new Validator(structuredClone(DOCUMENT_AUDIO_WORKSPACE_IMPORT_SCHEMA) as unknown as Schema, '2020-12', false)
|
|
8
|
+
export const validateDocumentAudioInspect = (value: unknown): value is DocumentAudioInspect => inspectValidator.validate(value).valid
|
|
9
|
+
export const validateDocumentWorkspaceAudioImport = (value: unknown): value is DocumentWorkspaceAudioImport => importValidator.validate(value).valid
|
|
10
|
+
|
|
11
|
+
const workspaceInspectValidator = new Validator(structuredClone(DOCUMENT_WORKSPACE_AUDIO_INSPECTION_SCHEMA) as unknown as Schema, '2020-12', false)
|
|
12
|
+
export const validateDocumentWorkspaceAudioInspect = (value: unknown): value is DocumentWorkspaceAudioInspect => workspaceInspectValidator.validate(value).valid
|
|
13
|
+
|
|
14
|
+
const clipValidationValidator = new Validator(structuredClone(DOCUMENT_MEDIA_CLIP_VALIDATION_SCHEMA) as unknown as Schema, '2020-12', false)
|
|
15
|
+
const clipChangeValidator = new Validator(structuredClone(DOCUMENT_MEDIA_CLIP_CHANGE_SCHEMA) as unknown as Schema, '2020-12', false)
|
|
16
|
+
export const validateDocumentMediaClipValidation = (value: unknown): value is DocumentMediaClipValidation => clipValidationValidator.validate(value).valid
|
|
17
|
+
export const validateDocumentMediaClipEdit = (value: unknown): value is DocumentMediaClipEdit => clipChangeValidator.validate(value).valid
|
|
18
|
+
const productionPlanValidator = new Validator(structuredClone(DOCUMENT_PRODUCTION_PLAN_SCHEMA) as unknown as Schema, '2020-12', false)
|
|
19
|
+
export const validateDocumentProductionPlanQuery = (value: unknown): value is DocProductionPlanQuery => productionPlanValidator.validate(value).valid
|
|
20
|
+
|
|
21
|
+
const recordingValidator = new Validator(structuredClone(DOCUMENT_RECORDING_ADOPTION_SCHEMA) as unknown as Schema, '2020-12', false)
|
|
22
|
+
export const validateDocumentRecordingAdoption = (value: unknown): value is DocumentRecordingAdoption => recordingValidator.validate(value).valid
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Validator } from '@cfworker/json-schema'
|
|
2
|
+
import type { Schema } from '@cfworker/json-schema'
|
|
3
|
+
import { DOCUMENT_HISTORY_CHANGE_SCHEMA, DOCUMENT_HISTORY_QUERY_SCHEMA, DOCUMENT_HISTORY_READ_SCHEMA } from '../contracts/document-history.ts'
|
|
4
|
+
import type { DocumentHistoryChange, DocumentHistoryQuery, DocumentHistoryRead } from '../contracts/document-history.ts'
|
|
5
|
+
|
|
6
|
+
const query = new Validator(structuredClone(DOCUMENT_HISTORY_QUERY_SCHEMA) as unknown as Schema, '2020-12', false)
|
|
7
|
+
const read = new Validator(structuredClone(DOCUMENT_HISTORY_READ_SCHEMA) as unknown as Schema, '2020-12', false)
|
|
8
|
+
const change = new Validator(structuredClone(DOCUMENT_HISTORY_CHANGE_SCHEMA) as unknown as Schema, '2020-12', false)
|
|
9
|
+
export const validateDocumentHistoryQuery = (value: unknown): value is DocumentHistoryQuery => query.validate(value).valid
|
|
10
|
+
export const validateDocumentHistoryRead = (value: unknown): value is DocumentHistoryRead => read.validate(value).valid
|
|
11
|
+
export const validateDocumentHistoryChange = (value: unknown): value is DocumentHistoryChange => change.validate(value).valid
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Validator } from '@cfworker/json-schema'
|
|
2
|
+
import type { Schema } from '@cfworker/json-schema'
|
|
3
|
+
import { DOCUMENT_WORKSPACE_IMAGE_INSPECTION_SCHEMA, DOCUMENT_WORKSPACE_IMAGE_IMPORT_SCHEMA } from '../contracts/document-image.ts'
|
|
4
|
+
import type { DocumentWorkspaceImageInspect, DocumentWorkspaceImageImport } from '../contracts/document-image.ts'
|
|
5
|
+
const inspect = new Validator(structuredClone(DOCUMENT_WORKSPACE_IMAGE_INSPECTION_SCHEMA) as unknown as Schema, '2020-12', false)
|
|
6
|
+
const importImage = new Validator(structuredClone(DOCUMENT_WORKSPACE_IMAGE_IMPORT_SCHEMA) as unknown as Schema, '2020-12', false)
|
|
7
|
+
export const validateDocumentWorkspaceImageInspect = (value: unknown): value is DocumentWorkspaceImageInspect => inspect.validate(value).valid
|
|
8
|
+
export const validateDocumentWorkspaceImageImport = (value: unknown): value is DocumentWorkspaceImageImport => importImage.validate(value).valid
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Validator } from '@cfworker/json-schema'
|
|
2
|
+
import type { Schema } from '@cfworker/json-schema'
|
|
3
|
+
import { DOCUMENT_OUTLINE_QUERY_SCHEMA, DOCUMENT_PART_QUERY_SCHEMA, DOCUMENT_READ_MAX_REQUEST_CHARS } from '../contracts/document-reads.ts'
|
|
4
|
+
import type { DocumentOutlineQuery, DocumentPartQuery } from '../contracts/document-reads.ts'
|
|
5
|
+
|
|
6
|
+
const outline = new Validator(structuredClone(DOCUMENT_OUTLINE_QUERY_SCHEMA) as unknown as Schema, '2020-12', false)
|
|
7
|
+
const part = new Validator(structuredClone(DOCUMENT_PART_QUERY_SCHEMA) as unknown as Schema, '2020-12', false)
|
|
8
|
+
const bounded = (value: unknown) => { try { return JSON.stringify(value).length <= DOCUMENT_READ_MAX_REQUEST_CHARS } catch { return false } }
|
|
9
|
+
export function validateDocumentOutlineQuery(value: unknown): value is DocumentOutlineQuery { return bounded(value) && outline.validate(value).valid }
|
|
10
|
+
export function validateDocumentPartQuery(value: unknown): value is DocumentPartQuery { return bounded(value) && part.validate(value).valid }
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Validator } from '@cfworker/json-schema'
|
|
2
|
+
import type { Schema } from '@cfworker/json-schema'
|
|
3
|
+
import { DOCUMENT_VOICE_CATALOG_SCHEMA } from '../contracts/document-voice.ts'
|
|
4
|
+
import type { DocumentVoiceCatalogQuery } from '../contracts/document-voice.ts'
|
|
5
|
+
|
|
6
|
+
const validator = new Validator(structuredClone(DOCUMENT_VOICE_CATALOG_SCHEMA) as unknown as Schema, '2020-12', false)
|
|
7
|
+
export const validateDocumentVoiceCatalogQuery = (value: unknown): value is DocumentVoiceCatalogQuery => validator.validate(value).valid
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
|
+
|
|
3
|
+
export interface Ffmpeg {
|
|
4
|
+
/** The program, as it is run. */
|
|
5
|
+
file: string
|
|
6
|
+
/** The names of its encoders (`libx264`, `libvpx-vp9`, `aac`, `libopus`…). */
|
|
7
|
+
encoders: Set<string>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let found: Ffmpeg | null | undefined
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* FFmpeg as the author installed it: `PRESMIUM_FFMPEG`, else `ffmpeg` on the path; undefined when it does not run.
|
|
14
|
+
* Presmium does not ship it; it encodes the lecture video with it when it is there (`ffmpegInstallHint` says how).
|
|
15
|
+
*/
|
|
16
|
+
export function findFfmpeg(): Ffmpeg | undefined {
|
|
17
|
+
if (found !== undefined) return found ?? undefined
|
|
18
|
+
const file = process.env.PRESMIUM_FFMPEG || 'ffmpeg'
|
|
19
|
+
const run = spawnSync(file, ['-hide_banner', '-encoders'], { encoding: 'utf8', timeout: 10_000 })
|
|
20
|
+
found = run.status === 0 ? { file, encoders: new Set([...run.stdout.matchAll(/^ [A-Z.]{6} (\S+)/gm)].map(m => m[1]!)) } : null
|
|
21
|
+
return found ?? undefined
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The one command that installs FFmpeg on this system. */
|
|
25
|
+
export function ffmpegInstallCommand(platform: NodeJS.Platform = process.platform): string {
|
|
26
|
+
if (platform === 'darwin') return 'brew install ffmpeg'
|
|
27
|
+
if (platform === 'win32') return 'winget install --id Gyan.FFmpeg'
|
|
28
|
+
return 'sudo apt install ffmpeg'
|
|
29
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { BigIntStats } from 'node:fs'
|
|
2
|
+
|
|
3
|
+
/** A local file's identity and observed contents, including replacements that preserve its modification time. */
|
|
4
|
+
export function fileVersion(stat: Pick<BigIntStats, 'dev' | 'ino' | 'size' | 'mtimeNs' | 'ctimeNs'>): string {
|
|
5
|
+
return [stat.dev, stat.ino, stat.size, stat.mtimeNs, stat.ctimeNs].join(':')
|
|
6
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { createRequire, findPackageJSON } from 'node:module'
|
|
3
|
+
|
|
4
|
+
export const FONT_LICENSE = 'presmium/fonts/LICENSE'
|
|
5
|
+
|
|
6
|
+
const FONT_COPYRIGHTS: Record<string, string> = {
|
|
7
|
+
'@fontsource/ibm-plex-mono': 'IBM Plex Mono: Copyright 2017 IBM Corp. All rights reserved.',
|
|
8
|
+
'@fontsource/ibm-plex-sans': 'IBM Plex Sans: Copyright 2019 IBM Corp. All rights reserved.',
|
|
9
|
+
'@fontsource/source-serif-4': 'Source Serif 4: Copyright 2014 - 2021 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name \'Source\'.',
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function fontLicense(from: string | URL): string {
|
|
13
|
+
const manifest = findPackageJSON('@lincyaw/host', from)
|
|
14
|
+
if (!manifest) throw new Error('@lincyaw/host is not installed')
|
|
15
|
+
const { dependencies = {} } = JSON.parse(readFileSync(manifest, 'utf8')) as { dependencies?: Record<string, string> }
|
|
16
|
+
const require = createRequire(manifest)
|
|
17
|
+
const files = Object.keys(dependencies).filter(name => name.startsWith('@fontsource/')).sort().map((name) => {
|
|
18
|
+
const copyright = FONT_COPYRIGHTS[name] ? `${FONT_COPYRIGHTS[name]}\n\n` : ''
|
|
19
|
+
return `${name}\n\n${copyright}${readFileSync(require.resolve(`${name}/LICENSE`), 'utf8').trim()}\n`
|
|
20
|
+
})
|
|
21
|
+
return [`The fonts of this site, from Fontsource, with their copyrights and licenses (SIL Open Font License 1.1).\n`, ...files].join(`\n${'='.repeat(78)}\n\n`)
|
|
22
|
+
}
|
package/src/node/index.ts
CHANGED
|
@@ -1,133 +1,29 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const copyright = FONT_COPYRIGHTS[name] ? `${FONT_COPYRIGHTS[name]}\n\n` : ''
|
|
31
|
-
return `${name}\n\n${copyright}${readFileSync(require.resolve(`${name}/LICENSE`), 'utf8').trim()}\n`
|
|
32
|
-
})
|
|
33
|
-
return [`The fonts of this site, from Fontsource, with their copyrights and licenses (SIL Open Font License 1.1).\n`, ...files].join(`\n${'='.repeat(78)}\n\n`)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export const TYPESCRIPT_IMPORT: readonly string[] = ['--import', new URL('./strip-types.mjs', import.meta.url).href]
|
|
37
|
-
|
|
38
|
-
const PROVIDED = /^(?:vue|@vue\/.*|vitepress|@slidev\/.*)$/
|
|
39
|
-
const PRESMIUM = /^@lincyaw\//
|
|
40
|
-
const IMPORT = /(?:^|\n)\s*(?:import|export)(?!\s+type\b)[^'"`;]*?\bfrom\s*['"]([^'"./][^'"]*)['"]|(?:^|\n)\s*import\s*['"]([^'"./][^'"]*)['"]|\bimport\(\s*['"]([^'"./][^'"]*)['"]\s*\)/g
|
|
41
|
-
const SOURCE = /\.(?:[cm]?[jt]s|vue)$/
|
|
42
|
-
const DECLARATION = /\.d\.[cm]?ts$/
|
|
43
|
-
const STYLE = /\.(?:css|scss|sass|less|styl)$/
|
|
44
|
-
const NOT_BROWSER = new Set(['node', 'bin', 'test', 'fixtures', 'node_modules'])
|
|
45
|
-
|
|
46
|
-
export function preBundle(root: string, packages: Iterable<string>): string[] {
|
|
47
|
-
return optimizeDeps(root, packages).include
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function optimizeDeps(root: string, packages: Iterable<string>): { include: string[], exclude: string[] } {
|
|
51
|
-
const include = new Set<string>()
|
|
52
|
-
const exclude = new Set<string>()
|
|
53
|
-
const seen = new Set<string>()
|
|
54
|
-
const visit = (dir: string): void => {
|
|
55
|
-
const real = realpathSync(dir)
|
|
56
|
-
if (seen.has(real) || !real.split(sep).includes('node_modules')) return
|
|
57
|
-
seen.add(real)
|
|
58
|
-
const { name, imports, presmium } = browserDependencies(real)
|
|
59
|
-
exclude.add(name)
|
|
60
|
-
if (imports.length > 0) linkPackage(root, name, real)
|
|
61
|
-
for (const specifier of imports) include.add(`${name} > ${specifier}`)
|
|
62
|
-
for (const dependency of presmium) {
|
|
63
|
-
const manifest = installedManifest(dependency, real)
|
|
64
|
-
if (manifest) visit(dirname(manifest))
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
for (const dir of packages) if (existsSync(join(dir, 'package.json'))) visit(dir)
|
|
68
|
-
return { include: [...include].sort(), exclude: [...exclude].sort() }
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function installedManifest(name: string, dir: string): string | undefined {
|
|
72
|
-
try {
|
|
73
|
-
return findPackageJSON(name, pathToFileURL(join(dir, 'package.json')))
|
|
74
|
-
} catch {
|
|
75
|
-
return undefined
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export function packageOf(file: string): string | undefined {
|
|
80
|
-
const manifest = findPackageJSON(pathToFileURL(file))
|
|
81
|
-
return manifest && dirname(manifest)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function linkPackage(root: string, name: string, dir: string): void {
|
|
85
|
-
const link = join(root, 'node_modules', name)
|
|
86
|
-
const stat = lstatSync(link, { throwIfNoEntry: false })
|
|
87
|
-
if (stat?.isSymbolicLink() && readlinkSync(link) === dir) return
|
|
88
|
-
if (stat && !stat.isSymbolicLink()) return
|
|
89
|
-
rmSync(link, { force: true })
|
|
90
|
-
mkdirSync(dirname(link), { recursive: true })
|
|
91
|
-
symlinkSync(dir, link, 'junction')
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
interface BrowserDependencies {
|
|
95
|
-
name: string
|
|
96
|
-
imports: string[]
|
|
97
|
-
presmium: string[]
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const dependencies = new Map<string, BrowserDependencies>()
|
|
101
|
-
function browserDependencies(dir: string): BrowserDependencies {
|
|
102
|
-
let found = dependencies.get(dir)
|
|
103
|
-
if (!found) {
|
|
104
|
-
const pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')) as { name: string, dependencies?: Record<string, string>, peerDependencies?: Record<string, string> }
|
|
105
|
-
const own = (specifier: string) => Object.hasOwn(pkg.dependencies ?? {}, packageName(specifier))
|
|
106
|
-
const specifiers = [...browserImports(dir)]
|
|
107
|
-
found = {
|
|
108
|
-
name: pkg.name,
|
|
109
|
-
imports: specifiers.filter(specifier => own(specifier) && !PROVIDED.test(packageName(specifier)) && !PRESMIUM.test(specifier) && !STYLE.test(specifier)),
|
|
110
|
-
presmium: [...new Set(specifiers.filter(specifier => PRESMIUM.test(specifier)).map(packageName))]
|
|
111
|
-
.filter(name => Object.hasOwn({ ...pkg.dependencies, ...pkg.peerDependencies }, name)),
|
|
112
|
-
}
|
|
113
|
-
dependencies.set(dir, found)
|
|
114
|
-
}
|
|
115
|
-
return found
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function browserImports(dir: string, found = new Set<string>(), top = true): Set<string> {
|
|
119
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
120
|
-
const path = join(dir, entry.name)
|
|
121
|
-
if (entry.isDirectory()) {
|
|
122
|
-
if (!NOT_BROWSER.has(entry.name)) browserImports(path, found, false)
|
|
123
|
-
} else if (SOURCE.test(entry.name) && !DECLARATION.test(entry.name) && !(top && entry.name === 'vite.config.ts')) {
|
|
124
|
-
for (const match of readFileSync(path, 'utf8').matchAll(IMPORT)) found.add((match[1] ?? match[2] ?? match[3])!)
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
return found
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function packageName(specifier: string): string {
|
|
131
|
-
const parts = specifier.split('/')
|
|
132
|
-
return specifier.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0]!
|
|
133
|
-
}
|
|
1
|
+
export { copyPublished, productionAssetFile } from './assets.ts'
|
|
2
|
+
export { fileVersion } from './file-version.ts'
|
|
3
|
+
export { DOCUMENT_CHANGE_JSON_SCHEMA, validateDocumentChange, validateDocumentPageChange, validateDocumentPageChangeApply, validateDocumentPageDuplication, validateDocumentPageDuplicationApply } from '../authoring.ts'
|
|
4
|
+
export { validateDocumentOutlineQuery, validateDocumentPartQuery } from './document-reads.ts'
|
|
5
|
+
export { DOCUMENT_AUTHORING_ACTION_SCHEMA, DOCUMENT_AUTHORING_ACTION_APPLY_SCHEMA, DOCUMENT_AUTHORING_ACTION_IMPACT_SCHEMA, validateDocumentAuthoringAction, validateDocumentAuthoringActionReference, validateDocumentAuthoringActionImpact } from './authoring-actions.ts'
|
|
6
|
+
export { validateDocumentTranscriptionRequest } from './transcription.ts'
|
|
7
|
+
export { validateDocumentRecordingAdoption, validateDocumentAudioInspect, validateDocumentWorkspaceAudioImport, validateDocumentWorkspaceAudioInspect, validateDocumentMediaClipValidation, validateDocumentMediaClipEdit, validateDocumentProductionPlanQuery } from './document-audio.ts'
|
|
8
|
+
export { validateDocumentVoiceCatalogQuery } from './document-voice.ts'
|
|
9
|
+
export { createVoiceoverOwner, VoiceoverOperationError, validateVoiceoverStart, validateVoiceoverStatus, validateVoiceoverJobQuery, validateVoicePreviewCheck, validateVoicePreviewStart } from './voiceover.ts'
|
|
10
|
+
export { createVoiceoverRoutes } from './voiceover-routes.ts'
|
|
11
|
+
export { validateTranscriptionAdoptionDraft, validateTranscriptionAdoptionApply } from './transcription-adoption.ts'
|
|
12
|
+
export { FONT_LICENSE, fontLicense } from './fonts.ts'
|
|
13
|
+
export { findPackageBin, importSpecifier, literalGlob, packageOf, playwrightChromium, TYPESCRIPT_IMPORT } from './packages.ts'
|
|
14
|
+
export { ffmpegInstallCommand, findFfmpeg } from './ffmpeg.ts'
|
|
15
|
+
export type { Ffmpeg } from './ffmpeg.ts'
|
|
16
|
+
export { bracketIPv6, lanAddresses } from './network.ts'
|
|
17
|
+
export { forEachLine } from './lines.ts'
|
|
18
|
+
export { isInside } from './paths.ts'
|
|
19
|
+
export { backgroundPriority, machineState, NODE_RESOURCE_ARGS, nodeResourceArgs, resourceEnvironment, spawnBackgroundNode, spawnInteractiveNode, threadBudget } from './resources.ts'
|
|
20
|
+
export type { MachineState, ResourceClass } from './resources.ts'
|
|
21
|
+
export { irRevision } from './revision.ts'
|
|
22
|
+
export { findInstalledPackage } from './installed-packages.ts'
|
|
23
|
+
export { directSandbox, minimalRunEnv, RUN_REQUEST_FILE, RUN_RESULT_PREFIX, runResultFile } from './run-directory.ts'
|
|
24
|
+
export { validateDocumentHistoryQuery, validateDocumentHistoryRead, validateDocumentHistoryChange } from './document-history.ts'
|
|
25
|
+
export { validateAuthoringClientRequest, validateAuthoringClientQuery, validateAuthoringClientRequestQuery, validateAuthoringClientOpen, validateAuthoringClientReport, validateAuthoringClientAcknowledgement, validateAuthoringClientLease } from './authoring-clients.ts'
|
|
26
|
+
export { validateProjectPlan, validateProjectApply } from './projects.ts'
|
|
27
|
+
export { validateDocumentWorkspaceImageInspect, validateDocumentWorkspaceImageImport } from './document-image.ts'
|
|
28
|
+
export { systemLocale } from './locale.ts'
|
|
29
|
+
export { OperatorSpeechSettings, operatorSpeechSecrets, SpeechSettingsError } from './operator-speech.ts'
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { existsSync, realpathSync } from 'node:fs'
|
|
2
|
+
import { dirname, join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
/** The real directory of the package `name` installed in a `node_modules` at or above `from`, as Node resolves it. */
|
|
5
|
+
export function findInstalledPackage(name: string, from: string): string | undefined {
|
|
6
|
+
for (let dir = from; ; dir = dirname(dir)) {
|
|
7
|
+
const candidate = join(dir, 'node_modules', name)
|
|
8
|
+
if (existsSync(join(candidate, 'package.json'))) return realpathSync(candidate)
|
|
9
|
+
if (dirname(dir) === dir) return undefined
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Readable } from 'node:stream'
|
|
2
|
+
|
|
3
|
+
// Terminal colours and cursor moves, which a log shown on a page does not draw.
|
|
4
|
+
const ESCAPES = /\x1B\[[0-?]*[ -/]*[@-~]|\x1B\][^\x07]*\x07/g
|
|
5
|
+
|
|
6
|
+
/** Calls `each` with every line `stream` writes (carriage returns end a line too), without terminal escapes. */
|
|
7
|
+
export function forEachLine(stream: Readable | null | undefined, each: (line: string) => void): void {
|
|
8
|
+
if (!stream) return
|
|
9
|
+
let rest = ''
|
|
10
|
+
stream.setEncoding('utf8')
|
|
11
|
+
stream.on('data', (chunk: string) => {
|
|
12
|
+
const parts = (rest + chunk).split(/\r\n|\r|\n/)
|
|
13
|
+
rest = parts.pop() ?? ''
|
|
14
|
+
for (const part of parts) {
|
|
15
|
+
const line = part.replace(ESCAPES, '')
|
|
16
|
+
if (line.trim()) each(line)
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
stream.on('end', () => {
|
|
20
|
+
const line = rest.replace(ESCAPES, '')
|
|
21
|
+
if (line.trim()) each(line)
|
|
22
|
+
rest = ''
|
|
23
|
+
})
|
|
24
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Locale } from '../contracts/locale.ts'
|
|
2
|
+
|
|
3
|
+
/** The language of command-line output: the system's (`LC_ALL`, `LC_MESSAGES`, `LANG`, else `Intl`'s); `zh*` is zh-CN. */
|
|
4
|
+
export function systemLocale(env: NodeJS.ProcessEnv = process.env): Locale {
|
|
5
|
+
const tag = env.LC_ALL || env.LC_MESSAGES || env.LANG || Intl.DateTimeFormat().resolvedOptions().locale
|
|
6
|
+
return /^zh/i.test(tag) ? 'zh-CN' : 'en'
|
|
7
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { networkInterfaces } from 'node:os'
|
|
2
|
+
|
|
3
|
+
/** This machine's IPv4 addresses on its local networks, for printing the addresses other devices can use. */
|
|
4
|
+
export function lanAddresses(): string[] {
|
|
5
|
+
return Object.values(networkInterfaces()).flatMap(entries => entries ?? []).filter(entry => entry.family === 'IPv4' && !entry.internal).map(entry => entry.address)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** The host as a URL writes it: an IPv6 address in brackets. */
|
|
9
|
+
export function bracketIPv6(host: string): string {
|
|
10
|
+
return host.includes(':') && !host.startsWith('[') ? `[${host}]` : host
|
|
11
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { createRequire } from 'node:module'
|
|
2
|
+
import { randomUUID } from 'node:crypto'
|
|
3
|
+
import { constants, closeSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'
|
|
4
|
+
import { dirname, join } from 'node:path'
|
|
5
|
+
import type { SpeechSettingsAnswer, SpeechSettingsChange } from '../contracts/speech-settings.ts'
|
|
6
|
+
|
|
7
|
+
const KEY = 'PRESMIUM_SPEECH_VOLCENGINE_KEY', APPID = 'PRESMIUM_SPEECH_VOLCENGINE_APPID'
|
|
8
|
+
const MAX_BYTES = 16384
|
|
9
|
+
const lockfile = createRequire(import.meta.url)('proper-lockfile') as { lock(file: string, options: { realpath: boolean, retries: number }): Promise<() => Promise<void>> }
|
|
10
|
+
interface Stored { version: 1, revision: string, key?: string, appId?: string }
|
|
11
|
+
const empty = (): Stored => ({ version: 1, revision: 'none' })
|
|
12
|
+
|
|
13
|
+
export class SpeechSettingsError extends Error {
|
|
14
|
+
readonly code: 'speech-settings-conflict' | 'speech-settings-invalid' | 'speech-settings-unavailable' | 'speech-settings-busy'
|
|
15
|
+
constructor(code: SpeechSettingsError['code']) { super(code); this.code = code }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** One operator-owned file; no values leave this module except through the Node secret resolver. */
|
|
19
|
+
export class OperatorSpeechSettings {
|
|
20
|
+
readonly #file: string | undefined
|
|
21
|
+
readonly #fallback: (name: string) => string | undefined
|
|
22
|
+
constructor(directory: string | undefined, fallback: (name: string) => string | undefined) {
|
|
23
|
+
this.#file = directory ? join(directory, 'speech', 'credentials.json') : undefined
|
|
24
|
+
this.#fallback = fallback
|
|
25
|
+
}
|
|
26
|
+
#read(): Stored {
|
|
27
|
+
if (!this.#file) return empty()
|
|
28
|
+
let fd: number | undefined
|
|
29
|
+
try {
|
|
30
|
+
fd = openSync(this.#file, constants.O_RDONLY | constants.O_NOFOLLOW)
|
|
31
|
+
const stat = fstatSync(fd)
|
|
32
|
+
if (!stat.isFile() || stat.size > MAX_BYTES) throw new SpeechSettingsError('speech-settings-unavailable')
|
|
33
|
+
const value = JSON.parse(readFileSync(fd, 'utf8')) as Stored
|
|
34
|
+
if (value?.version !== 1 || typeof value.revision !== 'string' || value.key !== undefined && typeof value.key !== 'string' || value.appId !== undefined && (typeof value.appId !== 'string' || !/^\d{1,32}$/.test(value.appId))) throw new SpeechSettingsError('speech-settings-unavailable')
|
|
35
|
+
return value
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return empty()
|
|
38
|
+
throw new SpeechSettingsError('speech-settings-unavailable')
|
|
39
|
+
} finally { if (fd !== undefined) closeSync(fd) }
|
|
40
|
+
}
|
|
41
|
+
secret = (name: string): string | undefined => {
|
|
42
|
+
if (name !== KEY && name !== APPID) return this.#fallback(name)
|
|
43
|
+
const saved = this.#read()
|
|
44
|
+
// API-key authentication must not inherit a different environment credential's App ID.
|
|
45
|
+
if (name === APPID && saved.key) return saved.appId
|
|
46
|
+
return (name === KEY ? saved.key : undefined) ?? this.#fallback(name)
|
|
47
|
+
}
|
|
48
|
+
read(): SpeechSettingsAnswer {
|
|
49
|
+
const value = this.#read(), key = value.key ?? this.#fallback(KEY)?.trim()
|
|
50
|
+
return { provider: 'volcengine', model: 'seed-tts-2.0', revision: value.revision, configured: Boolean(key), appIdConfigured: Boolean(value.key ? value.appId : this.#fallback(APPID)), source: value.key ? 'saved' : key ? 'environment' : 'none', stored: Boolean(value.key), writable: Boolean(this.#file), verification: 'configuration-only' }
|
|
51
|
+
}
|
|
52
|
+
async change(change: SpeechSettingsChange): Promise<SpeechSettingsAnswer> {
|
|
53
|
+
if (!this.#file) throw new SpeechSettingsError('speech-settings-unavailable')
|
|
54
|
+
const directory = dirname(this.#file)
|
|
55
|
+
try {
|
|
56
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 })
|
|
57
|
+
if (lstatSync(directory).isSymbolicLink()) throw new Error('unsafe-directory')
|
|
58
|
+
} catch { throw new SpeechSettingsError('speech-settings-unavailable') }
|
|
59
|
+
let release: () => Promise<void>
|
|
60
|
+
try { release = await lockfile.lock(this.#file, { realpath: false, retries: 0 }) }
|
|
61
|
+
catch { throw new SpeechSettingsError('speech-settings-busy') }
|
|
62
|
+
let temporary: string | undefined
|
|
63
|
+
try {
|
|
64
|
+
const previous = this.#read()
|
|
65
|
+
if (change.revision !== previous.revision) throw new SpeechSettingsError('speech-settings-conflict')
|
|
66
|
+
let next: Stored = { version: 1, revision: randomUUID() }
|
|
67
|
+
if (change.action === 'save') {
|
|
68
|
+
const key = change.apiKey?.trim() || previous.key || this.#fallback(KEY)?.trim()
|
|
69
|
+
if (!key || !/^[\x21-\x7e]{8,8192}$/.test(key)) throw new SpeechSettingsError('speech-settings-invalid')
|
|
70
|
+
const appId = change.appId?.trim() ?? (change.apiKey?.trim() ? undefined : previous.key ? previous.appId : this.#fallback(APPID)?.trim())
|
|
71
|
+
if (appId && !/^\d{1,32}$/.test(appId)) throw new SpeechSettingsError('speech-settings-invalid')
|
|
72
|
+
next = { ...next, key, ...appId ? { appId } : {} }
|
|
73
|
+
}
|
|
74
|
+
temporary = `${this.#file}.${randomUUID()}.tmp`
|
|
75
|
+
writeFileSync(temporary, `${JSON.stringify(next)}\n`, { mode: 0o600, flag: 'wx' })
|
|
76
|
+
renameSync(temporary, this.#file)
|
|
77
|
+
temporary = undefined
|
|
78
|
+
return this.read()
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (error instanceof SpeechSettingsError) throw error
|
|
81
|
+
throw new SpeechSettingsError('speech-settings-unavailable')
|
|
82
|
+
} finally {
|
|
83
|
+
if (temporary) rmSync(temporary, { force: true })
|
|
84
|
+
await release()
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function operatorSpeechSecrets(directory: string | undefined, fallback: (name: string) => string | undefined): (name: string) => string | undefined {
|
|
90
|
+
return new OperatorSpeechSettings(directory, fallback).secret
|
|
91
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { createRequire, findPackageJSON } from 'node:module'
|
|
3
|
+
import { dirname, isAbsolute, join, relative, sep } from 'node:path'
|
|
4
|
+
import { pathToFileURL } from 'node:url'
|
|
5
|
+
|
|
6
|
+
export const TYPESCRIPT_IMPORT: readonly string[] = ['--import', new URL('./strip-types.mjs', import.meta.url).href]
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The import specifier of `file` in a module generated in `dir`: relative where it can be (Vite's dependency scan does
|
|
10
|
+
* not follow an import written as an absolute path), else absolute (another drive on Windows).
|
|
11
|
+
*/
|
|
12
|
+
export function importSpecifier(dir: string, file: string): string {
|
|
13
|
+
const path = relative(dir, file)
|
|
14
|
+
if (isAbsolute(path)) return file
|
|
15
|
+
const posix = path.split(sep).join('/')
|
|
16
|
+
return posix.startsWith('.') ? posix : `./${posix}`
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** `file` as a glob that matches only it: Vite's `optimizeDeps.entries` are globs. */
|
|
20
|
+
export function literalGlob(file: string): string {
|
|
21
|
+
return file.split(sep).join('/').replace(/[\\*?[\]{}()!@+|]/g, '\\$&')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function packageOf(file: string): string | undefined {
|
|
25
|
+
const manifest = findPackageJSON(pathToFileURL(file))
|
|
26
|
+
return manifest && dirname(manifest)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function manifestOf(name: string, from: string): string | undefined {
|
|
30
|
+
const require = createRequire(from)
|
|
31
|
+
try {
|
|
32
|
+
return require.resolve(`${name}/package.json`)
|
|
33
|
+
} catch {}
|
|
34
|
+
let dir: string
|
|
35
|
+
try {
|
|
36
|
+
dir = dirname(require.resolve(name))
|
|
37
|
+
} catch {
|
|
38
|
+
return undefined
|
|
39
|
+
}
|
|
40
|
+
for (; dirname(dir) !== dir; dir = dirname(dir)) {
|
|
41
|
+
const file = join(dir, 'package.json')
|
|
42
|
+
if (existsSync(file) && (JSON.parse(readFileSync(file, 'utf8')) as { name?: string }).name === name) return file
|
|
43
|
+
}
|
|
44
|
+
return undefined
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The file of the package's command `command` (its `bin`), the package as Node resolves it from `from`; undefined when either is missing. */
|
|
48
|
+
export function findPackageBin(name: string, command: string, from: string): string | undefined {
|
|
49
|
+
const manifest = manifestOf(name, from)
|
|
50
|
+
if (!manifest) return undefined
|
|
51
|
+
const { bin } = JSON.parse(readFileSync(manifest, 'utf8')) as { bin?: string | Record<string, string> }
|
|
52
|
+
const file = typeof bin === 'string' ? bin : bin?.[command]
|
|
53
|
+
return file === undefined ? undefined : join(dirname(manifest), file)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Playwright's Chromium (`playwright-chromium`), as the deck installs it, else as `fallbackFrom` (a module's URL or a
|
|
58
|
+
* `package.json`) finds it.
|
|
59
|
+
*/
|
|
60
|
+
export async function playwrightChromium<T = unknown>(deckDir: string, fallbackFrom?: string): Promise<T> {
|
|
61
|
+
for (const from of [join(deckDir, 'package.json'), ...fallbackFrom ? [fallbackFrom] : []]) {
|
|
62
|
+
let entry: string
|
|
63
|
+
try {
|
|
64
|
+
entry = createRequire(from).resolve('playwright-chromium')
|
|
65
|
+
} catch {
|
|
66
|
+
continue
|
|
67
|
+
}
|
|
68
|
+
const loaded = await import(pathToFileURL(entry).href) as { chromium?: T, default?: { chromium?: T } }
|
|
69
|
+
const chromium = loaded.chromium ?? loaded.default?.chromium
|
|
70
|
+
if (chromium) return chromium
|
|
71
|
+
throw new Error('playwright-chromium does not export its Chromium browser')
|
|
72
|
+
}
|
|
73
|
+
throw new Error('this needs playwright-chromium: install it in the deck (and its browser with pnpm exec playwright install chromium)')
|
|
74
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { isAbsolute, relative, sep } from 'node:path'
|
|
2
|
+
|
|
3
|
+
/** Whether path is root itself or lies under it; symbolic links are not resolved. */
|
|
4
|
+
export function isInside(path: string, root: string): boolean {
|
|
5
|
+
const rel = relative(root, path)
|
|
6
|
+
return rel === '' || (rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel))
|
|
7
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Schema } from '@cfworker/json-schema'
|
|
2
|
+
import { Validator } from '@cfworker/json-schema'
|
|
3
|
+
import { PROJECT_APPLY_SCHEMA, PROJECT_PLAN_SCHEMA } from '../contracts/projects.ts'
|
|
4
|
+
import type { ProjectApply, ProjectPlanRequest } from '../contracts/projects.ts'
|
|
5
|
+
const plan = new Validator(structuredClone(PROJECT_PLAN_SCHEMA) as unknown as Schema, '2020-12', false), apply = new Validator(structuredClone(PROJECT_APPLY_SCHEMA) as unknown as Schema, '2020-12', false)
|
|
6
|
+
export const validateProjectPlan = (value: unknown): value is ProjectPlanRequest => plan.validate(value).valid
|
|
7
|
+
export const validateProjectApply = (value: unknown): value is ProjectApply => apply.validate(value).valid
|