@lincyaw/cli 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 +6 -6
- package/package.json +6 -5
- package/src/check.ts +9 -3
- package/src/cli.ts +98 -125
- package/src/{host.ts → dev-host.ts} +3 -3
- package/src/index.ts +1 -2
- package/src/messages.ts +219 -0
- package/src/testing.ts +0 -2
- package/src/fake-model.ts +0 -101
package/README.md
CHANGED
|
@@ -2,20 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
The `presmium` command: `new`, `check`, `ir`, `build`, `dev` and `serve` for Presmium documents and workspaces.
|
|
4
4
|
|
|
5
|
-
Part of [Presmium](https://github.com/Lincyaw/presmium), which turns
|
|
5
|
+
Part of [Presmium](https://github.com/Lincyaw/presmium), which turns structured teaching documents into web pages, slides, Jupyter notebooks and narrated lectures.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
9
|
Most people install the `presmium` package, which brings this command with the renderers and the official plugins:
|
|
10
10
|
|
|
11
11
|
```sh
|
|
12
|
-
pnpm add -g presmium
|
|
12
|
+
pnpm add -g @lincyaw/presmium
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
In a project that picks its own renderers and plugins, install the CLI with them (Node.js 22.19 or later):
|
|
16
16
|
|
|
17
17
|
```sh
|
|
18
|
-
pnpm add -D @lincyaw/cli @lincyaw/web @lincyaw/render-slides @lincyaw/render-notebook
|
|
18
|
+
pnpm add -D @lincyaw/cli @lincyaw/web @lincyaw/render-slides @lincyaw/render-notebook
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
## Commands
|
|
@@ -30,7 +30,7 @@ pnpm add -D @lincyaw/cli @lincyaw/web @lincyaw/render-slides @lincyaw/render-not
|
|
|
30
30
|
| `presmium serve [dir]` | The workspace service, on this machine: the documents and their sites, compiled on demand (`--port`, `--host`, `--operator-dir`, `--preview-origin`) |
|
|
31
31
|
| `presmium studio [dir]` | The workbench (experimental), from `@lincyaw/studio` installed in the project (`--port`, `--host`); installed globally, it is `presmium-studio` |
|
|
32
32
|
|
|
33
|
-
`[deck]` is a directory with `lecture.
|
|
33
|
+
`[deck]` is a directory with `lecture.json` or an entry file, by default the current directory. Exit codes: 0 no errors, 1 error findings, 2 usage or internal error. `presmium --help` lists every option.
|
|
34
34
|
|
|
35
35
|
## Testing plugins
|
|
36
36
|
|
|
@@ -44,8 +44,8 @@ const { code, stdout } = await presmium(dir, ['build', '--to', 'web'])
|
|
|
44
44
|
|
|
45
45
|
## Documentation
|
|
46
46
|
|
|
47
|
-
- [Author guide](https://github.com/Lincyaw/presmium/blob/main/site/guide/lecture.
|
|
48
|
-
- [Plugin guide](https://github.com/Lincyaw/presmium/blob/main/site/plugins/lecture.
|
|
47
|
+
- [Author guide](https://github.com/Lincyaw/presmium/blob/main/site/guide/locales/en/lecture.json)
|
|
48
|
+
- [Plugin guide](https://github.com/Lincyaw/presmium/blob/main/site/plugins/locales/en/lecture.json)
|
|
49
49
|
|
|
50
50
|
## License
|
|
51
51
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lincyaw/cli",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.2",
|
|
4
4
|
"description": "The presmium command: build, dev, ir, check and serve for Presmium documents",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -36,9 +36,10 @@
|
|
|
36
36
|
],
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"jiti": "^2.7.0",
|
|
39
|
-
"@lincyaw/kernel": "1.0.0-rc.
|
|
40
|
-
"@lincyaw/workspace": "1.0.0-rc.
|
|
41
|
-
"@lincyaw/
|
|
42
|
-
"@lincyaw/
|
|
39
|
+
"@lincyaw/kernel": "1.0.0-rc.2",
|
|
40
|
+
"@lincyaw/workspace": "1.0.0-rc.2",
|
|
41
|
+
"@lincyaw/compiler": "1.0.0-rc.2",
|
|
42
|
+
"@lincyaw/server": "1.0.0-rc.2",
|
|
43
|
+
"@lincyaw/skills": "1.0.0-rc.2"
|
|
43
44
|
}
|
|
44
45
|
}
|
package/src/check.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
|
2
2
|
import { dirname, join } from 'node:path'
|
|
3
3
|
import { ANCHOR_LOCK_PATH, errorMessage } from '@lincyaw/kernel'
|
|
4
4
|
import type { AnchorString, Finding } from '@lincyaw/kernel'
|
|
5
|
-
import { checkContext, Compiler,
|
|
5
|
+
import { checkContext, Compiler, sortFindings } from '@lincyaw/compiler'
|
|
6
6
|
import type { Compilation, CompilerOptions } from '@lincyaw/compiler'
|
|
7
7
|
|
|
8
8
|
export interface CheckOptions {
|
|
@@ -35,7 +35,7 @@ export async function runCheck(options: CheckOptions): Promise<CheckResult> {
|
|
|
35
35
|
|
|
36
36
|
let lock: LockUpdate | undefined
|
|
37
37
|
if (options.updateLock) {
|
|
38
|
-
lock = await updateLock(compilation, (name, error) => fail(`plugin "${name}" failed to list anchors: ${errorMessage(error)}`,
|
|
38
|
+
lock = await updateLock(compilation, (name, error) => fail(`plugin "${name}" failed to list anchors: ${errorMessage(error)}`, jsonLine(entryText, name)))
|
|
39
39
|
compilation = await compiler.compile()
|
|
40
40
|
}
|
|
41
41
|
|
|
@@ -54,7 +54,7 @@ export async function probeEnvironments(compilation: Compilation, text: string):
|
|
|
54
54
|
} catch (error) {
|
|
55
55
|
reason = errorMessage(error)
|
|
56
56
|
}
|
|
57
|
-
if (reason) findings.push({ checkId: 'presmium.execute', severity: 'error', message: `environment "${name}" (${spec.kind}) is not reachable: ${reason}`, source: { file: compilation.entry, line:
|
|
57
|
+
if (reason) findings.push({ checkId: 'presmium.execute', severity: 'error', message: `environment "${name}" (${spec.kind}) is not reachable: ${reason}`, source: { file: compilation.entry, line: jsonLine(text, name) } })
|
|
58
58
|
}
|
|
59
59
|
return findings
|
|
60
60
|
}
|
|
@@ -83,3 +83,9 @@ async function updateLock(compilation: Compilation, onError: (plugin: string, er
|
|
|
83
83
|
}
|
|
84
84
|
return { file, anchors: lock.size, changed: text !== before }
|
|
85
85
|
}
|
|
86
|
+
|
|
87
|
+
/** JSON property locations are diagnostic hints; entity paths are supplied by document validation. */
|
|
88
|
+
function jsonLine(text: string, property: string): number {
|
|
89
|
+
const index = text.indexOf(JSON.stringify(property))
|
|
90
|
+
return index < 0 ? 1 : text.slice(0, index).split("\n").length
|
|
91
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -5,92 +5,42 @@ import { dirname, join, relative, resolve } from 'node:path'
|
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
6
6
|
import { parseArgs } from 'node:util'
|
|
7
7
|
import { createJiti } from 'jiti'
|
|
8
|
-
import { errorMessage } from '@lincyaw/kernel'
|
|
8
|
+
import { APPROVAL_KINDS, errorMessage } from '@lincyaw/kernel'
|
|
9
|
+
import { bracketIPv6, ffmpegInstallCommand, findFfmpeg, irRevision, lanAddresses } from '@lincyaw/kernel/node'
|
|
9
10
|
import type { ApprovalKind, Finding, Severity } from '@lincyaw/kernel'
|
|
10
11
|
import { build, Compiler, dev, resolveEntry } from '@lincyaw/compiler'
|
|
11
12
|
import type { BuildReport, Compilation, RunStats } from '@lincyaw/compiler'
|
|
12
|
-
import {
|
|
13
|
+
import { isAnyAddress, isLoopback, serverConfigKey, servedDocument, startServer } from '@lincyaw/server'
|
|
13
14
|
import type { ApprovalPolicy } from '@lincyaw/server'
|
|
14
15
|
import { buildWorkspace, isWorkspaceRoot, serveWorkspace } from '@lincyaw/workspace'
|
|
15
16
|
import type { WorkspaceBuild } from '@lincyaw/workspace'
|
|
16
17
|
import { runCheck } from './check.ts'
|
|
17
18
|
import type { CheckResult } from './check.ts'
|
|
18
|
-
import { devHost } from './host.ts'
|
|
19
|
+
import { devHost } from './dev-host.ts'
|
|
20
|
+
import { CLI_MESSAGES, systemLocale } from './messages.ts'
|
|
19
21
|
|
|
20
|
-
|
|
22
|
+
const BUNDLED = fileURLToPath(new URL('..', import.meta.url))
|
|
23
|
+
const PRESMIUM_BIN = fileURLToPath(new URL('../bin/presmium.mjs', import.meta.url))
|
|
21
24
|
|
|
22
25
|
export interface MainOptions {
|
|
26
|
+
/** The package whose dependencies hold the renderers and plugins a document does not install itself. */
|
|
23
27
|
bundled?: string
|
|
28
|
+
/** The presmium command that presmium studio starts. */
|
|
24
29
|
bin?: string
|
|
25
30
|
}
|
|
26
31
|
|
|
27
|
-
|
|
28
|
-
let presmiumBin = fileURLToPath(new URL('../bin/presmium.mjs', import.meta.url))
|
|
29
|
-
|
|
30
|
-
const USAGE = `Usage: presmium <command> [deck] [options]
|
|
31
|
-
|
|
32
|
-
The deck is a directory holding lecture.md, or an entry file (default: .).
|
|
33
|
-
|
|
34
|
-
Commands:
|
|
35
|
-
new a new project in the directory (default: asks), from create-presmium
|
|
36
|
-
--type <kind> lecture, workspace or plugin (default: asks)
|
|
37
|
-
--title <text> the lecture's or the workspace's title
|
|
38
|
-
--name <name> the plugin's package name
|
|
39
|
-
--no-git do not run git init
|
|
40
|
-
build compile and render; writes <out>/ir.json and <out>/<renderer>/; at a workspace root
|
|
41
|
-
(presmium.workspace.yaml) builds the workspace site into <out>/web/
|
|
42
|
-
--to <ids> renderers to run, comma-separated (default: the deck's renderers)
|
|
43
|
-
--out <dir> output directory (default: <deck>/dist)
|
|
44
|
-
--audience <name> only the content for this audience (e.g. student); then every file
|
|
45
|
-
written is scanned for the sentences of the content left out
|
|
46
|
-
dev compile, start the Presmium server and the renderers' dev servers, recompile on save
|
|
47
|
-
--to <ids> renderers to run, comma-separated
|
|
48
|
-
--out <dir> working directory (default: <deck>/.presmium/dev)
|
|
49
|
-
--server-port <n> port of the Presmium server (default: a free port)
|
|
50
|
-
--allow <kinds> approvals given without asking: operate, spend, write, agent
|
|
51
|
-
ir print the IR as JSON
|
|
52
|
-
--out <file> write it to a file instead
|
|
53
|
-
check compile and run the plugins' checks
|
|
54
|
-
--json print the result as JSON
|
|
55
|
-
--update-lock re-resolve anchors and rewrite .presmium/anchors.lock.json
|
|
56
|
-
--env also connect to the environments of execute.environments
|
|
57
|
-
--audience <name> check the deck as compiled for this audience
|
|
58
|
-
--external-links also check the external links, with lychee when it is installed
|
|
59
|
-
serve the workspace service for you on this machine (the argument is the workspace root, or
|
|
60
|
-
a deck): the workspace site, the documents and their history, the reviews, the plugins'
|
|
61
|
-
routes, the classroom sync; without --host it listens on this machine only
|
|
62
|
-
--port <n> port (default: 4310)
|
|
63
|
-
--host <addr> address to listen on (default: 127.0.0.1); 0.0.0.0 or an address on the
|
|
64
|
-
local network opens it to your other devices there, with no login:
|
|
65
|
-
whoever reaches the port works as you
|
|
66
|
-
--operator-dir <dir> the operator directory (default: the user's state directory)
|
|
67
|
-
--preview-origin <origin> (repeatable) the origin of a workbench whose pages embed the
|
|
68
|
-
previews, e.g. http://127.0.0.1:4320; with PRESMIUM_SERVICE_TOKEN set, the
|
|
69
|
-
workspace's routes ask for it (Authorization: Bearer)
|
|
70
|
-
studio the workbench (experimental): DeepSeek Harness with Presmium's bundle and the workspace
|
|
71
|
-
service of the directory (the workspace root, or a deck); prints the one address to open.
|
|
72
|
-
It is the package @lincyaw/studio (with DeepSeek Harness, about 865 MB), installed beside
|
|
73
|
-
this one
|
|
74
|
-
--port <n> the workbench's port (default: 4320, or a free one when that is in use)
|
|
75
|
-
--host <addr> listen on this address of the machine on the local network instead of
|
|
76
|
-
127.0.0.1, with no login: whoever reaches the port works as you
|
|
77
|
-
|
|
78
|
-
-h, --help show this help
|
|
79
|
-
|
|
80
|
-
Exit codes: 0 no errors, 1 error findings, 2 usage or internal error.`
|
|
32
|
+
const t = CLI_MESSAGES[systemLocale()]
|
|
81
33
|
|
|
82
34
|
const OPTIONS = {
|
|
83
35
|
'build': ['to', 'out', 'audience'],
|
|
84
36
|
'dev': ['to', 'out', 'server-port', 'allow'],
|
|
85
37
|
'ir': ['out'],
|
|
86
38
|
'check': ['json', 'update-lock', 'env', 'audience', 'external-links'],
|
|
87
|
-
'serve': ['port', 'host', 'operator-dir', 'preview-origin'],
|
|
39
|
+
'serve': ['port', 'host', 'operator-dir', 'preview-origin', 'public-url'],
|
|
88
40
|
} as const
|
|
89
41
|
|
|
90
42
|
type Name = keyof typeof OPTIONS
|
|
91
43
|
|
|
92
|
-
const KINDS: readonly ApprovalKind[] = ['operate', 'spend', 'write', 'agent']
|
|
93
|
-
|
|
94
44
|
interface Command {
|
|
95
45
|
name: Name
|
|
96
46
|
entry: string
|
|
@@ -106,33 +56,34 @@ interface Command {
|
|
|
106
56
|
allow: ApprovalKind[]
|
|
107
57
|
operatorDir?: string
|
|
108
58
|
port?: number
|
|
109
|
-
|
|
59
|
+
listenAddress?: string
|
|
110
60
|
previewOrigins: string[]
|
|
61
|
+
publicUrl?: string
|
|
111
62
|
}
|
|
112
63
|
|
|
113
64
|
export async function main(argv: string[], options: MainOptions = {}): Promise<number> {
|
|
114
|
-
bundled = options.bundled ?? BUNDLED
|
|
115
|
-
if (
|
|
116
|
-
if (argv[0] === '
|
|
117
|
-
if (argv[0] === 'studio') return
|
|
65
|
+
const bundled = options.bundled ?? BUNDLED
|
|
66
|
+
if (argv[0] === 'new') return runNewCommand(argv.slice(1), bundled)
|
|
67
|
+
if (argv[0] === 'skills') return (await import('@lincyaw/skills')).main(argv.slice(1))
|
|
68
|
+
if (argv[0] === 'studio') return runStudioCommand(argv.slice(1), bundled, options.bin ?? PRESMIUM_BIN)
|
|
118
69
|
let command: Command | 'help'
|
|
119
70
|
try {
|
|
120
71
|
command = parseCommand(argv)
|
|
121
72
|
} catch (error) {
|
|
122
|
-
process.stderr.write(`presmium: ${errorMessage(error)}\n\n${
|
|
73
|
+
process.stderr.write(`presmium: ${errorMessage(error)}\n\n${t.usage}\n`)
|
|
123
74
|
return 2
|
|
124
75
|
}
|
|
125
76
|
if (command === 'help') {
|
|
126
|
-
process.stdout.write(`${
|
|
77
|
+
process.stdout.write(`${t.usage}\n`)
|
|
127
78
|
return 0
|
|
128
79
|
}
|
|
129
80
|
try {
|
|
130
81
|
switch (command.name) {
|
|
131
|
-
case 'check': return await
|
|
132
|
-
case 'ir': return await
|
|
133
|
-
case 'build': return await
|
|
134
|
-
case 'dev': return await
|
|
135
|
-
case 'serve': return await
|
|
82
|
+
case 'check': return await runCheckCommand(command, bundled)
|
|
83
|
+
case 'ir': return await runIrCommand(command, bundled)
|
|
84
|
+
case 'build': return await runBuildCommand(command, bundled)
|
|
85
|
+
case 'dev': return await runDevCommand(command, bundled)
|
|
86
|
+
case 'serve': return await runServeCommand(command, bundled)
|
|
136
87
|
}
|
|
137
88
|
} catch (error) {
|
|
138
89
|
process.stderr.write(`presmium: ${errorMessage(error)}\n`)
|
|
@@ -158,34 +109,37 @@ function parseCommand(argv: string[]): Command | 'help' {
|
|
|
158
109
|
'port': { type: 'string' },
|
|
159
110
|
'host': { type: 'string' },
|
|
160
111
|
'preview-origin': { type: 'string', multiple: true },
|
|
112
|
+
'public-url': { type: 'string' },
|
|
161
113
|
'help': { type: 'boolean', short: 'h', default: false },
|
|
162
114
|
},
|
|
163
115
|
})
|
|
164
116
|
if (values.help) return 'help'
|
|
165
117
|
const [name, deck = '.', ...extra] = positionals
|
|
166
|
-
if (!name) throw new Error(
|
|
167
|
-
if (!(name in OPTIONS)) throw new Error(
|
|
118
|
+
if (!name) throw new Error(t.missingCommand)
|
|
119
|
+
if (!(name in OPTIONS)) throw new Error(t.unknownCommand(name))
|
|
168
120
|
const allowed: readonly string[] = OPTIONS[name as Name]
|
|
169
121
|
const given = Object.entries(values).filter(([key, value]) => key !== 'help' && value !== undefined && value !== false)
|
|
170
122
|
const wrong = given.find(([key]) => !allowed.includes(key))
|
|
171
|
-
if (wrong) throw new Error(
|
|
172
|
-
if (extra.length > 0) throw new Error(
|
|
123
|
+
if (wrong) throw new Error(t.optionNotFor(wrong[0], name))
|
|
124
|
+
if (extra.length > 0) throw new Error(t.unexpectedArgument(extra[0]!))
|
|
173
125
|
const workspace = name === 'build' && existsSync(deck) && statSync(deck).isDirectory() && isWorkspaceRoot(resolve(deck)) ? resolve(deck) : undefined
|
|
174
|
-
if (workspace && values.to) throw new Error(
|
|
175
|
-
if (workspace && values.audience !== undefined) throw new Error(
|
|
126
|
+
if (workspace && values.to) throw new Error(t.toNotForWorkspace)
|
|
127
|
+
if (workspace && values.audience !== undefined) throw new Error(t.audienceNotForWorkspace)
|
|
176
128
|
const audience = values.audience?.trim()
|
|
177
|
-
if (values.audience !== undefined && !/^[\w.-]+$/.test(audience ?? '')) throw new Error(
|
|
129
|
+
if (values.audience !== undefined && !/^[\w.-]+$/.test(audience ?? '')) throw new Error(t.badAudience(values.audience))
|
|
178
130
|
const directory = name === 'serve'
|
|
179
131
|
const entry = directory || workspace ? resolve(deck) : resolveEntry(deck)
|
|
180
|
-
if (!existsSync(entry) || (directory && !statSync(entry).isDirectory())) throw new Error(
|
|
132
|
+
if (!existsSync(entry) || (directory && !statSync(entry).isDirectory())) throw new Error(t.notFound(entry))
|
|
181
133
|
const to = values.to?.split(',').map(id => id.trim()).filter(Boolean)
|
|
182
|
-
if (to?.length === 0) throw new Error(
|
|
134
|
+
if (to?.length === 0) throw new Error(t.emptyTo)
|
|
183
135
|
const allow = values.allow?.split(',').map(kind => kind.trim()).filter(Boolean) ?? []
|
|
184
|
-
const unknown = allow.find(kind => !(
|
|
185
|
-
if (unknown) throw new Error(
|
|
136
|
+
const unknown = allow.find(kind => !(APPROVAL_KINDS as readonly string[]).includes(kind))
|
|
137
|
+
if (unknown) throw new Error(t.badAllow(APPROVAL_KINDS, unknown))
|
|
186
138
|
const previewOrigins = values['preview-origin'] ?? []
|
|
187
139
|
const notOrigin = previewOrigins.find(origin => !isOrigin(origin))
|
|
188
|
-
if (notOrigin !== undefined) throw new Error(
|
|
140
|
+
if (notOrigin !== undefined) throw new Error(t.badPreviewOrigin(notOrigin))
|
|
141
|
+
const publicUrl = values['public-url']
|
|
142
|
+
if (publicUrl !== undefined && !isPublicUrl(publicUrl)) throw new Error(t.badPublicUrl(publicUrl))
|
|
189
143
|
return {
|
|
190
144
|
name: name as Name,
|
|
191
145
|
entry,
|
|
@@ -197,12 +151,23 @@ function parseCommand(argv: string[]): Command | 'help' {
|
|
|
197
151
|
env: values.env,
|
|
198
152
|
...audience ? { audience } : {},
|
|
199
153
|
externalLinks: values['external-links'],
|
|
200
|
-
serverPort:
|
|
154
|
+
serverPort: parsePort(values['server-port'], '--server-port'),
|
|
201
155
|
allow: allow as ApprovalKind[],
|
|
202
156
|
operatorDir: values['operator-dir'],
|
|
203
|
-
port:
|
|
204
|
-
|
|
157
|
+
port: parsePort(values.port, '--port'),
|
|
158
|
+
listenAddress: values.host,
|
|
205
159
|
previewOrigins,
|
|
160
|
+
...publicUrl ? { publicUrl } : {},
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** An http(s) address, optionally with the path a proxy forwards; no credentials, query or fragment. */
|
|
165
|
+
function isPublicUrl(value: string): boolean {
|
|
166
|
+
try {
|
|
167
|
+
const url = new URL(value)
|
|
168
|
+
return /^https?:$/.test(url.protocol) && !url.username && !url.password && !url.search && !url.hash
|
|
169
|
+
} catch {
|
|
170
|
+
return false
|
|
206
171
|
}
|
|
207
172
|
}
|
|
208
173
|
|
|
@@ -215,14 +180,14 @@ function isOrigin(value: string): boolean {
|
|
|
215
180
|
}
|
|
216
181
|
}
|
|
217
182
|
|
|
218
|
-
function
|
|
183
|
+
function parsePort(value: string | undefined, option: string): number | undefined {
|
|
219
184
|
if (value === undefined) return undefined
|
|
220
185
|
const n = Number(value)
|
|
221
|
-
if (!Number.isInteger(n) || n < 0 || n > 65535) throw new Error(
|
|
186
|
+
if (!Number.isInteger(n) || n < 0 || n > 65535) throw new Error(t.badPort(option, value))
|
|
222
187
|
return n
|
|
223
188
|
}
|
|
224
189
|
|
|
225
|
-
async function
|
|
190
|
+
async function runCheckCommand(command: Command, bundled: string): Promise<number> {
|
|
226
191
|
const result = await runCheck({
|
|
227
192
|
deck: command.entry,
|
|
228
193
|
updateLock: command.updateLock,
|
|
@@ -234,7 +199,7 @@ async function check(command: Command): Promise<number> {
|
|
|
234
199
|
return exitCode(result.findings)
|
|
235
200
|
}
|
|
236
201
|
|
|
237
|
-
async function
|
|
202
|
+
async function runIrCommand(command: Command, bundled: string): Promise<number> {
|
|
238
203
|
const { doc } = await new Compiler(command.entry, { bundled }).compile()
|
|
239
204
|
const json = `${JSON.stringify(doc, null, 2)}\n`
|
|
240
205
|
if (command.out) {
|
|
@@ -248,7 +213,7 @@ async function ir(command: Command): Promise<number> {
|
|
|
248
213
|
return exitCode(doc.diagnostics)
|
|
249
214
|
}
|
|
250
215
|
|
|
251
|
-
async function
|
|
216
|
+
async function runBuildCommand(command: Command, bundled: string): Promise<number> {
|
|
252
217
|
if (command.workspace) {
|
|
253
218
|
const report = await buildWorkspace(command.workspace, { outDir: command.out, bundled })
|
|
254
219
|
process.stdout.write(workspaceText(report))
|
|
@@ -257,11 +222,11 @@ async function buildDeck(command: Command): Promise<number> {
|
|
|
257
222
|
const compilation = await new Compiler(command.entry, { bundled, ...command.audience ? { audience: command.audience } : {} }).compile({ pruneCache: true })
|
|
258
223
|
const outDir = resolve(command.out ?? join(compilation.deckDir, 'dist'))
|
|
259
224
|
const report = await build(compilation, { outDir, to: command.to })
|
|
260
|
-
process.stdout.write(buildText(report, compilation.
|
|
225
|
+
process.stdout.write(buildText(report, compilation.runStats, compilation.audience?.name))
|
|
261
226
|
return exitCode(report.findings)
|
|
262
227
|
}
|
|
263
228
|
|
|
264
|
-
async function
|
|
229
|
+
async function runDevCommand(command: Command, bundled: string): Promise<number> {
|
|
265
230
|
let latest: Compilation | undefined
|
|
266
231
|
const deckDir = dirname(command.entry)
|
|
267
232
|
const approvals: Partial<Record<ApprovalKind, ApprovalPolicy>> = Object.fromEntries(command.allow.map(kind => [kind, 'allow']))
|
|
@@ -274,20 +239,27 @@ async function devDeck(command: Command): Promise<number> {
|
|
|
274
239
|
document: async dir => (dir === '' && latest ? servedDocument(latest) : undefined),
|
|
275
240
|
stateFile: join(deckDir, '.presmium', 'server.json'),
|
|
276
241
|
})
|
|
277
|
-
const compiler = new Compiler(command.entry, { bundled, services: server.services })
|
|
242
|
+
const compiler = new Compiler(command.entry, { bundled, priority: 'interactive', services: server.services })
|
|
278
243
|
const outDir = resolve(command.out ?? join(compiler.deckDir, '.presmium', 'dev'))
|
|
279
244
|
await mkdir(outDir, { recursive: true })
|
|
280
245
|
await writeFile(join(outDir, '.gitignore'), '*\n')
|
|
281
|
-
let
|
|
246
|
+
let devServers
|
|
282
247
|
try {
|
|
283
|
-
|
|
248
|
+
devServers = await dev(compiler, {
|
|
284
249
|
outDir,
|
|
285
250
|
to: command.to,
|
|
286
251
|
server: { url: server.url },
|
|
287
252
|
onCompile(compilation, findings) {
|
|
253
|
+
const changed = latest && serverConfigKey(latest) !== serverConfigKey(compilation)
|
|
288
254
|
latest = compilation
|
|
255
|
+
const revision = irRevision(compilation.doc)
|
|
256
|
+
server.diagnostics.compiled('', compilation.doc, compilation.workspaceRoot, revision)
|
|
257
|
+
server.diagnostics.replace({ document: '', origin: 'preview' }, findings.filter(finding => finding.checkId === 'presmium.render'), { root: compilation.workspaceRoot, irRevision: revision })
|
|
258
|
+
if (changed) void server.unmount('', { preserveFocus: true }).catch((error: unknown) => {
|
|
259
|
+
process.stderr.write(`${t.unmountFailed(errorMessage(error))}\n`)
|
|
260
|
+
})
|
|
289
261
|
const lines = findingLines(findings)
|
|
290
|
-
lines.push(
|
|
262
|
+
lines.push(t.compiled(compilation.doc.sections.length, summary(findings)))
|
|
291
263
|
process.stdout.write(`${lines.join('\n')}\n`)
|
|
292
264
|
},
|
|
293
265
|
})
|
|
@@ -295,32 +267,34 @@ async function devDeck(command: Command): Promise<number> {
|
|
|
295
267
|
await server.close()
|
|
296
268
|
throw error
|
|
297
269
|
}
|
|
298
|
-
for (const { renderer, url } of
|
|
299
|
-
process.stdout.write(`server ${server.url}\nmcp ${server.url}/mcp
|
|
300
|
-
process.stdout.write(
|
|
270
|
+
for (const { renderer, url } of devServers.servers) process.stdout.write(`${renderer} ${url}\n`)
|
|
271
|
+
process.stdout.write(`server ${server.url}\nmcp ${server.url}/mcp${t.token(relative(process.cwd(), join(deckDir, '.presmium', 'server.json')))}\n`)
|
|
272
|
+
process.stdout.write(`${t.watching(relative(process.cwd(), devServers.ir))}\n`)
|
|
301
273
|
await new Promise<void>((done) => {
|
|
302
274
|
process.once('SIGINT', done)
|
|
303
275
|
process.once('SIGTERM', done)
|
|
304
276
|
})
|
|
305
|
-
await
|
|
277
|
+
await devServers.close()
|
|
306
278
|
await server.close()
|
|
307
279
|
return 0
|
|
308
280
|
}
|
|
309
281
|
|
|
310
|
-
async function
|
|
282
|
+
async function runServeCommand(command: Command, bundled: string): Promise<number> {
|
|
311
283
|
const started = await serveWorkspace({
|
|
312
284
|
dir: command.entry,
|
|
313
|
-
|
|
285
|
+
listenAddress: command.listenAddress,
|
|
314
286
|
port: command.port,
|
|
315
287
|
...command.operatorDir ? { operatorDir: resolve(command.operatorDir) } : {},
|
|
316
288
|
bundled,
|
|
317
289
|
check: compilation => compilation.check(),
|
|
318
290
|
previewOrigins: command.previewOrigins,
|
|
291
|
+
...command.publicUrl ? { publicUrl: command.publicUrl } : {},
|
|
319
292
|
})
|
|
320
|
-
process.stdout.write(`serve ${started.url}\n${networkLines(command.
|
|
321
|
-
if (command.
|
|
322
|
-
process.stderr.write(
|
|
293
|
+
process.stdout.write(`serve ${started.url}\n${networkLines(command.listenAddress, started.url)}workspace ${started.root}\nmcp ${started.mcp.url}${t.token(started.mcp.stateFile)}\n`)
|
|
294
|
+
if (command.listenAddress && !isLoopback(command.listenAddress)) {
|
|
295
|
+
process.stderr.write(`${t.serveOpen(command.listenAddress, new URL(started.url).port)}\n`)
|
|
323
296
|
}
|
|
297
|
+
if (!findFfmpeg()) process.stderr.write(`${t.noFfmpeg(ffmpegInstallCommand())}\n`)
|
|
324
298
|
await new Promise<void>((done) => {
|
|
325
299
|
process.once('SIGINT', done)
|
|
326
300
|
process.once('SIGTERM', done)
|
|
@@ -329,7 +303,7 @@ async function serve(command: Command): Promise<number> {
|
|
|
329
303
|
return 0
|
|
330
304
|
}
|
|
331
305
|
|
|
332
|
-
function
|
|
306
|
+
function resolveInstalled(name: string, bundled: string): string | undefined {
|
|
333
307
|
for (const from of [pathToFileURL(join(bundled, 'package.json')).href, import.meta.url, pathToFileURL(join(process.cwd(), 'package.json')).href]) {
|
|
334
308
|
try {
|
|
335
309
|
return createRequire(from).resolve(name)
|
|
@@ -339,30 +313,30 @@ function installed(name: string): string | undefined {
|
|
|
339
313
|
return undefined
|
|
340
314
|
}
|
|
341
315
|
|
|
342
|
-
async function
|
|
343
|
-
const entry =
|
|
316
|
+
async function runNewCommand(argv: string[], bundled: string): Promise<number> {
|
|
317
|
+
const entry = resolveInstalled('create-presmium', bundled)
|
|
344
318
|
if (!entry) {
|
|
345
|
-
process.stderr.write(
|
|
319
|
+
process.stderr.write(`${t.newMissing}\n`)
|
|
346
320
|
return 2
|
|
347
321
|
}
|
|
348
322
|
const { main: createMain } = await createJiti(import.meta.url).import<{ main(argv: string[]): Promise<number> }>(entry)
|
|
349
323
|
return createMain(argv)
|
|
350
324
|
}
|
|
351
325
|
|
|
352
|
-
async function
|
|
353
|
-
const entry =
|
|
326
|
+
async function runStudioCommand(argv: string[], bundled: string, presmiumBin: string): Promise<number> {
|
|
327
|
+
const entry = resolveInstalled('@lincyaw/studio', bundled)
|
|
354
328
|
if (!entry) {
|
|
355
|
-
process.stderr.write(
|
|
329
|
+
process.stderr.write(`${t.studioMissing}\n`)
|
|
356
330
|
return 2
|
|
357
331
|
}
|
|
358
332
|
const { main: studioMain } = await createJiti(import.meta.url).import<{ main(argv: string[], options: { presmium: string }): Promise<number> }>(entry)
|
|
359
333
|
return studioMain(argv, { presmium: presmiumBin })
|
|
360
334
|
}
|
|
361
335
|
|
|
362
|
-
function networkLines(
|
|
363
|
-
if (!
|
|
336
|
+
function networkLines(listenAddress: string | undefined, url: string): string {
|
|
337
|
+
if (!listenAddress || isLoopback(listenAddress)) return ''
|
|
364
338
|
const { port } = new URL(url)
|
|
365
|
-
const addresses =
|
|
339
|
+
const addresses = isAnyAddress(listenAddress) ? lanAddresses() : [bracketIPv6(listenAddress)]
|
|
366
340
|
return addresses.map(address => `network http://${address}:${port}/\n`).join('')
|
|
367
341
|
}
|
|
368
342
|
|
|
@@ -378,13 +352,13 @@ function counts(findings: Finding[]): Record<Severity, number> {
|
|
|
378
352
|
|
|
379
353
|
function summary(findings: Finding[]): string {
|
|
380
354
|
const n = counts(findings)
|
|
381
|
-
return findings.length === 0 ?
|
|
355
|
+
return findings.length === 0 ? t.noProblems : t.counts(n.error, n.warning, n.info)
|
|
382
356
|
}
|
|
383
357
|
|
|
384
358
|
function findingLines(findings: Finding[], cwd = process.cwd()): string[] {
|
|
385
359
|
return findings.flatMap((f) => {
|
|
386
360
|
const where = f.source ? `${relative(cwd, f.source.file)}${f.source.line ? `:${f.source.line}` : ''}` : ''
|
|
387
|
-
return [`${where} ${f.severity} ${f.message} (${f.checkId})`, ...f.hint ? [
|
|
361
|
+
return [`${where} ${t.severity[f.severity]} ${f.message} (${f.checkId})`, ...f.hint ? [t.hint(f.hint)] : []]
|
|
388
362
|
})
|
|
389
363
|
}
|
|
390
364
|
|
|
@@ -395,8 +369,7 @@ function checkJson(result: CheckResult) {
|
|
|
395
369
|
function checkText(result: CheckResult, cwd = process.cwd()): string {
|
|
396
370
|
const out = findingLines(result.findings, cwd)
|
|
397
371
|
if (result.lock) {
|
|
398
|
-
|
|
399
|
-
out.push(`${relative(cwd, result.lock.file)} ${state} (${result.lock.anchors} anchors)`)
|
|
372
|
+
out.push(t.lock(relative(cwd, result.lock.file), result.lock.changed, result.lock.anchors))
|
|
400
373
|
}
|
|
401
374
|
out.push(summary(result.findings))
|
|
402
375
|
return `${out.join('\n')}\n`
|
|
@@ -405,7 +378,7 @@ function checkText(result: CheckResult, cwd = process.cwd()): string {
|
|
|
405
378
|
function workspaceText(report: WorkspaceBuild, cwd = process.cwd()): string {
|
|
406
379
|
const out = findingLines(report.findings, cwd)
|
|
407
380
|
for (const { dir, title, runs } of report.documents) {
|
|
408
|
-
out.push(`document ${dir || '.'} ${title}${runs.executed + runs.cached > 0 ?
|
|
381
|
+
out.push(`document ${dir || '.'} ${title}${runs.executed + runs.cached > 0 ? t.documentRuns(runs.executed, runs.cached) : ''}`)
|
|
409
382
|
}
|
|
410
383
|
out.push(`web ${relative(cwd, report.outDir)}`)
|
|
411
384
|
out.push(summary(report.findings))
|
|
@@ -417,10 +390,10 @@ function buildText(report: BuildReport, runs: RunStats, audience?: string, cwd =
|
|
|
417
390
|
if (audience) out.push(`audience ${audience}`)
|
|
418
391
|
out.push(`ir ${relative(cwd, report.ir)}`)
|
|
419
392
|
for (const { renderer, outDir, skipped } of report.outputs) {
|
|
420
|
-
out.push(`${renderer} ${relative(cwd, outDir)}${skipped.length ?
|
|
393
|
+
out.push(`${renderer} ${relative(cwd, outDir)}${skipped.length ? t.notRendered(skipped.length) : ''}`)
|
|
421
394
|
for (const s of skipped) out.push(` ${s.section}#${s.block}: ${s.reason}`)
|
|
422
395
|
}
|
|
423
|
-
if (runs.executed + runs.cached > 0) out.push(
|
|
396
|
+
if (runs.executed + runs.cached > 0) out.push(t.codeRuns(runs.executed, runs.cached, runs.sandbox && runs.executed > 0 ? runs.sandbox : undefined))
|
|
424
397
|
out.push(summary(report.findings))
|
|
425
398
|
return `${out.join('\n')}\n`
|
|
426
399
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { buildSummary, buildToDist, runBlock } from '@lincyaw/compiler'
|
|
2
2
|
import type { Compilation } from '@lincyaw/compiler'
|
|
3
3
|
import type { ServerHost } from '@lincyaw/server'
|
|
4
4
|
|
|
@@ -9,6 +9,7 @@ export function devHost(latest: () => Compilation | undefined): ServerHost {
|
|
|
9
9
|
return compilation
|
|
10
10
|
}
|
|
11
11
|
return {
|
|
12
|
+
get workspaceRoot() { return latest()?.workspaceRoot },
|
|
12
13
|
async documents() {
|
|
13
14
|
const compilation = latest()
|
|
14
15
|
return compilation ? [{ dir: '', title: compilation.doc.meta.title, lang: compilation.doc.meta.lang }] : []
|
|
@@ -22,8 +23,7 @@ export function devHost(latest: () => Compilation | undefined): ServerHost {
|
|
|
22
23
|
return [...compilation.doc.diagnostics, ...await compilation.check()]
|
|
23
24
|
},
|
|
24
25
|
async build(document, options = {}) {
|
|
25
|
-
|
|
26
|
-
return { outputs: report.outputs.map(({ renderer, outDir }) => ({ renderer, outDir })), findings: report.findings }
|
|
26
|
+
return buildSummary(await buildToDist(current(document), options))
|
|
27
27
|
},
|
|
28
28
|
run: async (document, target, signal) => runBlock(current(document), target, signal),
|
|
29
29
|
}
|
package/src/index.ts
CHANGED
package/src/messages.ts
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
export { systemLocale } from '@lincyaw/kernel/node'
|
|
2
|
+
import { DOCUMENT_ENTRY } from '@lincyaw/kernel'
|
|
3
|
+
import type { Locale } from '@lincyaw/kernel'
|
|
4
|
+
|
|
5
|
+
// The lines that start with a key and two spaces (`serve <url>`, `workspace <root>`, `mcp <url>`, `<renderer> <url>`)
|
|
6
|
+
// are read by programs (`@lincyaw/dsh/launch`, the tests): their keys, URLs and paths stay the same in both languages.
|
|
7
|
+
export interface CliMessages {
|
|
8
|
+
usage: string
|
|
9
|
+
missingCommand: string
|
|
10
|
+
unknownCommand(name: string): string
|
|
11
|
+
optionNotFor(option: string, command: string): string
|
|
12
|
+
unexpectedArgument(argument: string): string
|
|
13
|
+
toNotForWorkspace: string
|
|
14
|
+
audienceNotForWorkspace: string
|
|
15
|
+
badAudience(value: string): string
|
|
16
|
+
notFound(entry: string): string
|
|
17
|
+
emptyTo: string
|
|
18
|
+
badAllow(kinds: readonly string[], value: string): string
|
|
19
|
+
badPreviewOrigin(value: string): string
|
|
20
|
+
badPublicUrl(value: string): string
|
|
21
|
+
badPort(option: string, value: string): string
|
|
22
|
+
unmountFailed(error: string): string
|
|
23
|
+
compiled(sections: number, summary: string): string
|
|
24
|
+
token(file: string): string
|
|
25
|
+
watching(ir: string): string
|
|
26
|
+
serveOpen(address: string, port: string): string
|
|
27
|
+
noFfmpeg(command: string): string
|
|
28
|
+
newMissing: string
|
|
29
|
+
studioMissing: string
|
|
30
|
+
noProblems: string
|
|
31
|
+
counts(errors: number, warnings: number, infos: number): string
|
|
32
|
+
severity: Record<'error' | 'warning' | 'info', string>
|
|
33
|
+
hint(text: string): string
|
|
34
|
+
lock(file: string, changed: boolean, anchors: number): string
|
|
35
|
+
documentRuns(executed: number, cached: number): string
|
|
36
|
+
notRendered(blocks: number): string
|
|
37
|
+
codeRuns(executed: number, cached: number, sandbox?: string): string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const CLI_MESSAGES: Record<Locale, CliMessages> = {
|
|
41
|
+
'zh-CN': {
|
|
42
|
+
usage: `用法:presmium <命令> [<文稿>] [<选项>]
|
|
43
|
+
|
|
44
|
+
<文稿> 是含有 ${DOCUMENT_ENTRY} 的目录,或这个文件本身(默认为 .)。
|
|
45
|
+
|
|
46
|
+
命令:
|
|
47
|
+
new 在目录中新建项目(没有给出时逐项询问),由 create-presmium 完成
|
|
48
|
+
--type <种类> lecture、workspace、site 或 plugin(默认询问)
|
|
49
|
+
--theme <主题> site 的主题:course、docs 或 showcase(默认 course)
|
|
50
|
+
--title <标题> 讲义、工作区或网站的标题
|
|
51
|
+
--name <包名> 插件的包名
|
|
52
|
+
--no-git 不运行 git init
|
|
53
|
+
skills 安装内置技能(与 MCP 和可视化编辑器共用)
|
|
54
|
+
install <目录> [--json] 安装缺少的 .agents/skills,保留已有技能
|
|
55
|
+
--name <技能> 只安装指定技能,可重复
|
|
56
|
+
--json 以 JSON 打印安装结果
|
|
57
|
+
build 导出文稿:写出 <输出>/ir.json 与 <输出>/<形态>/;在工作区的根目录
|
|
58
|
+
(有 presmium.workspace.yaml)把整个工作区导出为网站,写到 <输出>/web/
|
|
59
|
+
--to <形态> 要导出的形态,以逗号分隔(默认为文稿声明的形态)
|
|
60
|
+
--out <目录> 输出目录(默认为 <文稿>/dist)
|
|
61
|
+
--audience <名称> 只导出面向这类读者的内容(例如 student),并在写出的每个
|
|
62
|
+
文件中查找被略去内容的句子
|
|
63
|
+
dev 实时预览:启动 Presmium 服务与各形态的预览服务器,保存后自动更新
|
|
64
|
+
--to <形态> 要预览的形态,以逗号分隔
|
|
65
|
+
--out <目录> 工作目录(默认为 <文稿>/.presmium/dev)
|
|
66
|
+
--server-port <端口> Presmium 服务的端口(默认为空闲端口)
|
|
67
|
+
--allow <种类> 无需询问即允许的操作:operate、spend、write、agent
|
|
68
|
+
ir 以 JSON 打印文稿的运行快照
|
|
69
|
+
--out <文件> 写入文件,而不是打印
|
|
70
|
+
check 检查文稿并运行插件的检查
|
|
71
|
+
--json 以 JSON 打印结果
|
|
72
|
+
--update-lock 重新定位代码锚点,并重写 .presmium/anchors.lock.json
|
|
73
|
+
--env 同时连接 execute.environments 中的运行环境
|
|
74
|
+
--audience <名称> 按面向这类读者的内容检查
|
|
75
|
+
--external-links 同时检查外部链接(装有 lychee 时用它)
|
|
76
|
+
serve 在本机提供工作区服务(参数是工作区的根目录或一份文稿):工作区网站、
|
|
77
|
+
各文稿与其版本历史、审阅、插件的服务端路由与课堂同步;不加 --host 时
|
|
78
|
+
只在本机监听
|
|
79
|
+
--port <端口> 端口(默认为 4310)
|
|
80
|
+
--host <地址> 监听的地址(默认为 127.0.0.1);0.0.0.0 或局域网中的地址
|
|
81
|
+
让同一网络中的其他设备也能使用,且没有登录:任何能访问
|
|
82
|
+
这个端口的人都以你的身份操作
|
|
83
|
+
--operator-dir <目录> 运营者目录(默认为用户的状态目录)
|
|
84
|
+
--preview-origin <来源> (可重复)嵌入预览的 Presmium Studio 页面的来源,
|
|
85
|
+
例如 http://127.0.0.1:4320;设置 PRESMIUM_SERVICE_TOKEN 时,
|
|
86
|
+
工作区的路由要求它(Authorization: Bearer)
|
|
87
|
+
--public-url <网址> 反向代理在前面时浏览器使用的地址:信任它的主机,预览与产物
|
|
88
|
+
的地址用它给出;带路径时代理原样转发该路径,服务的全部内容都在它下面
|
|
89
|
+
studio 启动 Presmium Studio(实验性):浏览器中的编辑应用与这个目录(工作区的
|
|
90
|
+
根目录或一份文稿)的工作区服务,只打印一个要打开的网址。需要另外安装
|
|
91
|
+
@lincyaw/studio(约 865 MB)
|
|
92
|
+
--port <端口> Presmium Studio 的端口(默认为 4320,被占用时取空闲端口)
|
|
93
|
+
--host <地址> 改为监听本机在局域网中的这个地址,而不是 127.0.0.1,且没有
|
|
94
|
+
登录:任何能访问这个端口的人都以你的身份操作
|
|
95
|
+
|
|
96
|
+
-h, --help 显示本说明
|
|
97
|
+
|
|
98
|
+
退出码:0 没有错误,1 有错误级别的问题,2 用法错误或内部错误。`,
|
|
99
|
+
missingCommand: '缺少命令。请在 presmium 之后给出命令,例如 presmium build。',
|
|
100
|
+
unknownCommand: name => `没有命令“${name}”。可用的命令见 presmium --help。`,
|
|
101
|
+
optionNotFor: (option, command) => `选项 --${option} 不能用于 presmium ${command}。可用的选项见 presmium --help。`,
|
|
102
|
+
unexpectedArgument: argument => `多余的参数“${argument}”。每个命令只接受一个文稿或目录。`,
|
|
103
|
+
toNotForWorkspace: '--to 不能用于工作区:工作区总是导出为网站。',
|
|
104
|
+
audienceNotForWorkspace: '--audience 暂不能用于工作区。请分别为每份文稿加 --audience 导出。',
|
|
105
|
+
badAudience: value => `“${value}”不是读者类别的名称。请给出字母、数字、.、_ 或 - 组成的名称,例如 student。`,
|
|
106
|
+
notFound: entry => `找不到 ${entry}。请检查路径,或用 presmium new 新建文稿。`,
|
|
107
|
+
emptyTo: '--to 至少需要一个形态,例如 --to web。',
|
|
108
|
+
badAllow: (kinds, value) => `--allow 不接受“${value}”。可用的种类:${kinds.join('、')}。`,
|
|
109
|
+
badPreviewOrigin: value => `“${value}”不是来源。请给出协议、主机与端口,例如 http://127.0.0.1:4320。`,
|
|
110
|
+
badPublicUrl: value => `“${value}”不是可用的网址。请给出浏览器访问的地址,例如 https://preview.example.com 或 https://example.com/user/a/preview/。`,
|
|
111
|
+
badPort: (option, value) => `${option} 需要 0 到 65535 之间的端口号,而不是“${value}”。`,
|
|
112
|
+
unmountFailed: error => `presmium: 无法关闭上一次加载的服务端扩展:${error}`,
|
|
113
|
+
compiled: (sections, summary) => `已更新 ${sections} 个页面;${summary}`,
|
|
114
|
+
token: file => ` (访问令牌在 ${file} 中)`,
|
|
115
|
+
watching: ir => `正在监视修改;运行快照在 ${ir}`,
|
|
116
|
+
noFfmpeg: command => `presmium serve: 没有找到 FFmpeg,讲授视频只能由浏览器导出为 WebM,画质较低。安装后可导出清晰的 MP4:${command}`,
|
|
117
|
+
serveOpen: (address, port) => `presmium serve: 正在 ${address} 上监听,局域网中的设备可以访问,且没有登录:任何能访问端口 ${port} 的人都以你的身份使用这个工作区(编辑、运行代码、允许操作)。请只在可信的网络中使用;不加 --host 时只在本机监听。`,
|
|
118
|
+
newMissing: 'presmium new: 没有安装 create-presmium。它随 presmium 一起安装,请运行 pnpm add -g @lincyaw/presmium。',
|
|
119
|
+
studioMissing: 'presmium studio: 这里没有安装 Presmium Studio(实验性,约 865 MB)。请运行 pnpm add -g @lincyaw/studio 后使用 presmium-studio,或在本项目中运行 pnpm add -D @lincyaw/studio。',
|
|
120
|
+
noProblems: '没有问题',
|
|
121
|
+
counts: (errors, warnings, infos) => `${errors} 个错误,${warnings} 个警告,${infos} 条信息`,
|
|
122
|
+
severity: { error: '错误', warning: '警告', info: '信息' },
|
|
123
|
+
hint: text => ` 建议:${text}`,
|
|
124
|
+
lock: (file, changed, anchors) => `${file} ${changed ? '已更新' : '没有变化'}(${anchors} 个锚点)`,
|
|
125
|
+
documentRuns: (executed, cached) => ` (代码运行:执行 ${executed} 个,取自缓存 ${cached} 个)`,
|
|
126
|
+
notRendered: blocks => ` (${blocks} 个内容块未显示)`,
|
|
127
|
+
codeRuns: (executed, cached, sandbox) => `代码运行:执行 ${executed} 个,取自缓存 ${cached} 个${sandbox ? `,隔离方式为 ${sandbox}` : ''}`,
|
|
128
|
+
},
|
|
129
|
+
'en': {
|
|
130
|
+
usage: `Usage: presmium <command> [deck] [options]
|
|
131
|
+
|
|
132
|
+
The deck is a directory holding ${DOCUMENT_ENTRY}, or that entry file (default: .).
|
|
133
|
+
|
|
134
|
+
Commands:
|
|
135
|
+
new a new project in the directory (default: asks), from create-presmium
|
|
136
|
+
--type <kind> lecture, workspace, site, or plugin (default: asks)
|
|
137
|
+
--theme <name> site theme: course, docs, or showcase (default: course)
|
|
138
|
+
--title <text> the lecture, workspace, or site title
|
|
139
|
+
--name <name> the plugin’s package name
|
|
140
|
+
--no-git do not run git init
|
|
141
|
+
skills install bundled skills, shared with MCP and the visual editor
|
|
142
|
+
install <dir> [--json] install missing .agents/skills; keep existing skills
|
|
143
|
+
--name <skill> install only the named skill; repeat for more
|
|
144
|
+
--json print the installation result as JSON
|
|
145
|
+
build export the deck: writes <out>/ir.json and <out>/<renderer>/; at a workspace root
|
|
146
|
+
(presmium.workspace.yaml) exports the workspace as a website into <out>/web/
|
|
147
|
+
--to <ids> forms to export, comma-separated (default: the deck’s renderers)
|
|
148
|
+
--out <dir> output directory (default: <deck>/dist)
|
|
149
|
+
--audience <name> only the content for this audience (e.g. student); then every file
|
|
150
|
+
written is scanned for the sentences of the content left out
|
|
151
|
+
dev live preview: starts the Presmium server and the renderers’ dev servers, updates on save
|
|
152
|
+
--to <ids> forms to preview, comma-separated
|
|
153
|
+
--out <dir> working directory (default: <deck>/.presmium/dev)
|
|
154
|
+
--server-port <n> port of the Presmium server (default: a free port)
|
|
155
|
+
--allow <kinds> approvals given without asking: operate, spend, write, agent
|
|
156
|
+
ir print the runtime snapshot as JSON
|
|
157
|
+
--out <file> write it to a file instead
|
|
158
|
+
check check the deck and run the plugins’ checks
|
|
159
|
+
--json print the result as JSON
|
|
160
|
+
--update-lock re-resolve anchors and rewrite .presmium/anchors.lock.json
|
|
161
|
+
--env also connect to the environments of execute.environments
|
|
162
|
+
--audience <name> check the content for this audience
|
|
163
|
+
--external-links also check the external links, with lychee when it is installed
|
|
164
|
+
serve the workspace service for you on this machine (the argument is the workspace root, or
|
|
165
|
+
a deck): the workspace site, the documents and their history, the reviews, the plugins’
|
|
166
|
+
routes, and the classroom sync; without --host it listens on this machine only
|
|
167
|
+
--port <n> port (default: 4310)
|
|
168
|
+
--host <addr> address to listen on (default: 127.0.0.1); 0.0.0.0 or an address on the
|
|
169
|
+
local network opens it to your other devices there, with no login:
|
|
170
|
+
whoever reaches the port works as you
|
|
171
|
+
--operator-dir <dir> the operator directory (default: the user’s state directory)
|
|
172
|
+
--preview-origin <origin> (repeatable) the origin of a Presmium Studio whose pages embed
|
|
173
|
+
the previews, e.g. http://127.0.0.1:4320; with PRESMIUM_SERVICE_TOKEN set,
|
|
174
|
+
the workspace’s routes ask for it (Authorization: Bearer)
|
|
175
|
+
--public-url <url> the address browsers use when a reverse proxy fronts the service: its
|
|
176
|
+
host is trusted, and previews and outputs are linked under it; with a path, the
|
|
177
|
+
proxy forwards that path unchanged and everything the service serves is below it
|
|
178
|
+
studio Presmium Studio (experimental): the editing app in the browser and the workspace service
|
|
179
|
+
of the directory (the workspace root, or a deck); prints the one address to open. It is
|
|
180
|
+
the package @lincyaw/studio (about 865 MB), installed beside this one
|
|
181
|
+
--port <n> Presmium Studio’s port (default: 4320, or a free one when that is in use)
|
|
182
|
+
--host <addr> listen on this address of the machine on the local network instead of
|
|
183
|
+
127.0.0.1, with no login: whoever reaches the port works as you
|
|
184
|
+
|
|
185
|
+
-h, --help show this help
|
|
186
|
+
|
|
187
|
+
Exit Codes: 0 no errors, 1 error findings, 2 usage or internal error.`,
|
|
188
|
+
missingCommand: 'No command given. Put a command after presmium, for example presmium build.',
|
|
189
|
+
unknownCommand: name => `There is no command “${name}”. See presmium --help for the commands.`,
|
|
190
|
+
optionNotFor: (option, command) => `The option --${option} does not apply to presmium ${command}. See presmium --help for its options.`,
|
|
191
|
+
unexpectedArgument: argument => `Unexpected argument “${argument}”. Each command takes one deck or directory.`,
|
|
192
|
+
toNotForWorkspace: '--to does not apply to a workspace: its site is the web form.',
|
|
193
|
+
audienceNotForWorkspace: '--audience does not apply to a workspace yet. Export each document with --audience instead.',
|
|
194
|
+
badAudience: value => `“${value}” is not the name of an audience. Give a name of letters, digits, ., _, or -, such as student.`,
|
|
195
|
+
notFound: entry => `Couldn’t find ${entry}. Check the path, or make a document with presmium new.`,
|
|
196
|
+
emptyTo: '--to needs at least one renderer, such as --to web.',
|
|
197
|
+
badAllow: (kinds, value) => `--allow does not take “${value}”. The kinds are ${kinds.join(', ')}.`,
|
|
198
|
+
badPreviewOrigin: value => `“${value}” is not an origin. Give a scheme, host, and port, such as http://127.0.0.1:4320.`,
|
|
199
|
+
badPublicUrl: value => `“${value}” is not a usable address. Give the address browsers use, such as https://preview.example.com or https://example.com/user/a/preview/.`,
|
|
200
|
+
badPort: (option, value) => `${option} takes a port number from 0 to 65535, not “${value}”.`,
|
|
201
|
+
unmountFailed: error => `presmium: couldn’t close the previous server contributions: ${error}`,
|
|
202
|
+
compiled: (sections, summary) => `compiled ${sections} sections; ${summary}`,
|
|
203
|
+
token: file => ` (its token is in ${file})`,
|
|
204
|
+
watching: ir => `watching; runtime snapshot in ${ir}`,
|
|
205
|
+
noFfmpeg: command => `presmium serve: FFmpeg was not found, so lecture videos export from the browser as WebM, at a lower quality. Install it for sharp MP4 videos: ${command}`,
|
|
206
|
+
serveOpen: (address, port) => `presmium serve: listening on ${address}, on the local network, with no login: anyone who reaches port ${port} uses this workspace as you (edits, runs code, approves). Use it on a network you trust; without --host it listens on this machine only.`,
|
|
207
|
+
newMissing: 'presmium new: create-presmium is not installed. It comes with the presmium package: run pnpm add -g @lincyaw/presmium.',
|
|
208
|
+
studioMissing: 'presmium studio: Presmium Studio is not installed here (experimental, about 865 MB). Run pnpm add -g @lincyaw/studio, then presmium-studio; or run pnpm add -D @lincyaw/studio in this project.',
|
|
209
|
+
noProblems: 'no problems',
|
|
210
|
+
counts: (errors, warnings, infos) => `${errors} errors, ${warnings} warnings, ${infos} infos`,
|
|
211
|
+
severity: { error: 'error', warning: 'warning', info: 'info' },
|
|
212
|
+
hint: text => ` hint: ${text}`,
|
|
213
|
+
lock: (file, changed, anchors) => `${file} ${changed ? 'updated' : 'unchanged'} (${anchors} anchors)`,
|
|
214
|
+
documentRuns: (executed, cached) => ` (code runs: ${executed} executed, ${cached} from cache)`,
|
|
215
|
+
notRendered: blocks => ` (${blocks} blocks not rendered)`,
|
|
216
|
+
codeRuns: (executed, cached, sandbox) => `code runs: ${executed} executed, ${cached} from cache${sandbox ? `, isolated by ${sandbox}` : ''}`,
|
|
217
|
+
},
|
|
218
|
+
}
|
|
219
|
+
|
package/src/testing.ts
CHANGED
|
@@ -22,5 +22,3 @@ export function presmium(cwd: string, args: string[], env: Record<string, string
|
|
|
22
22
|
return run(process.execPath, [PRESMIUM_BIN, ...args], { cwd, env: { ...inherited, ...env } })
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
export { fakeModel, recordingProxy } from './fake-model.ts'
|
|
26
|
-
export type { FakeModel, Received, Step } from './fake-model.ts'
|
package/src/fake-model.ts
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import { createServer } from 'node:http'
|
|
2
|
-
import type { IncomingHttpHeaders } from 'node:http'
|
|
3
|
-
import type { AddressInfo } from 'node:net'
|
|
4
|
-
|
|
5
|
-
export type Step = { text: string } | { tool: string, input: Record<string, unknown> }
|
|
6
|
-
|
|
7
|
-
export interface Received {
|
|
8
|
-
path: string
|
|
9
|
-
headers: IncomingHttpHeaders
|
|
10
|
-
body: Record<string, any>
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface FakeModel {
|
|
14
|
-
url: string
|
|
15
|
-
received: Received[]
|
|
16
|
-
hold(): void
|
|
17
|
-
release(): void
|
|
18
|
-
close(): Promise<void>
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const event = (name: string, data: unknown) => `event: ${name}\ndata: ${JSON.stringify(data)}\n\n`
|
|
22
|
-
|
|
23
|
-
function stream(step: Step, n: number): string {
|
|
24
|
-
const start = { type: 'message_start', message: { id: `msg_${n}`, type: 'message', role: 'assistant', model: 'fake-model', content: [], stop_reason: null, stop_sequence: null, usage: { input_tokens: 10, output_tokens: 0 } } }
|
|
25
|
-
const block = 'text' in step
|
|
26
|
-
? [event('content_block_start', { type: 'content_block_start', index: 0, content_block: { type: 'text', text: '' } }), event('content_block_delta', { type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: step.text } })]
|
|
27
|
-
: [event('content_block_start', { type: 'content_block_start', index: 0, content_block: { type: 'tool_use', id: `toolu_${n}`, name: step.tool, input: {} } }), event('content_block_delta', { type: 'content_block_delta', index: 0, delta: { type: 'input_json_delta', partial_json: JSON.stringify(step.input) } })]
|
|
28
|
-
return [
|
|
29
|
-
event('message_start', start),
|
|
30
|
-
...block,
|
|
31
|
-
event('content_block_stop', { type: 'content_block_stop', index: 0 }),
|
|
32
|
-
event('message_delta', { type: 'message_delta', delta: { stop_reason: 'text' in step ? 'end_turn' : 'tool_use', stop_sequence: null }, usage: { output_tokens: 5 } }),
|
|
33
|
-
event('message_stop', { type: 'message_stop' }),
|
|
34
|
-
].join('')
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export async function fakeModel(script: Step[]): Promise<FakeModel> {
|
|
38
|
-
const received: Received[] = []
|
|
39
|
-
let held: Promise<void> | undefined
|
|
40
|
-
let release = () => {}
|
|
41
|
-
const server = createServer((req, res) => {
|
|
42
|
-
let body = ''
|
|
43
|
-
req.setEncoding('utf8').on('data', (chunk: string) => body += chunk)
|
|
44
|
-
req.on('end', async () => {
|
|
45
|
-
let parsed: Record<string, any> = {}
|
|
46
|
-
try {
|
|
47
|
-
parsed = JSON.parse(body) as Record<string, any>
|
|
48
|
-
} catch {
|
|
49
|
-
}
|
|
50
|
-
const record: Received = { path: req.url ?? '', headers: req.headers, body: parsed }
|
|
51
|
-
received.push(record)
|
|
52
|
-
if (req.method !== 'POST' || !(req.url ?? '').endsWith('/v1/messages')) {
|
|
53
|
-
res.writeHead(404).end()
|
|
54
|
-
return
|
|
55
|
-
}
|
|
56
|
-
await held
|
|
57
|
-
const turn = (r: Received) => r.path.endsWith('/v1/messages') && Array.isArray(r.body.tools) && r.body.tools.length > 0
|
|
58
|
-
const n = received.filter(r => r.path.endsWith('/v1/messages')).length
|
|
59
|
-
const step = turn(record) ? script[received.filter(turn).indexOf(record)] ?? { text: '完毕。' } : { text: 'Presmium' }
|
|
60
|
-
res.writeHead(200, { 'Content-Type': 'text/event-stream' }).end(stream(step, n))
|
|
61
|
-
})
|
|
62
|
-
})
|
|
63
|
-
const port = await new Promise<number>(done => server.listen(0, '127.0.0.1', () => done((server.address() as AddressInfo).port)))
|
|
64
|
-
return {
|
|
65
|
-
url: `http://127.0.0.1:${port}`,
|
|
66
|
-
received,
|
|
67
|
-
hold() {
|
|
68
|
-
held = new Promise(done => release = done)
|
|
69
|
-
},
|
|
70
|
-
release() {
|
|
71
|
-
release()
|
|
72
|
-
held = undefined
|
|
73
|
-
},
|
|
74
|
-
close: () => new Promise<void>((done) => {
|
|
75
|
-
release()
|
|
76
|
-
server.close(() => done())
|
|
77
|
-
server.closeAllConnections()
|
|
78
|
-
}),
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export async function recordingProxy(): Promise<{ url: string, attempts: string[], close(): Promise<void> }> {
|
|
83
|
-
const attempts: string[] = []
|
|
84
|
-
const server = createServer((req, res) => {
|
|
85
|
-
attempts.push(`${req.method} ${req.url}`)
|
|
86
|
-
res.writeHead(502).end()
|
|
87
|
-
})
|
|
88
|
-
server.on('connect', (req, socket) => {
|
|
89
|
-
attempts.push(`CONNECT ${req.url}`)
|
|
90
|
-
socket.end('HTTP/1.1 502 Bad Gateway\r\n\r\n')
|
|
91
|
-
})
|
|
92
|
-
const port = await new Promise<number>(done => server.listen(0, '127.0.0.1', () => done((server.address() as AddressInfo).port)))
|
|
93
|
-
return {
|
|
94
|
-
url: `http://127.0.0.1:${port}`,
|
|
95
|
-
attempts,
|
|
96
|
-
close: () => new Promise<void>((done) => {
|
|
97
|
-
server.close(() => done())
|
|
98
|
-
server.closeAllConnections()
|
|
99
|
-
}),
|
|
100
|
-
}
|
|
101
|
-
}
|