kimaki 1.2.0 → 1.4.0
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/dist/bot.d.ts +4 -2
- package/dist/bot.js.map +1 -1
- package/dist/builtin-skills.d.ts +3 -0
- package/dist/builtin-skills.js +29 -0
- package/dist/builtin-skills.js.map +1 -1
- package/dist/builtin-skills.test.js +73 -1
- package/dist/builtin-skills.test.js.map +1 -1
- package/dist/cli/bot.d.ts +1 -1
- package/dist/cli/bot.js +15 -6
- package/dist/cli/bot.js.map +1 -1
- package/dist/cli-lock.e2e.test.js +3 -2
- package/dist/cli-lock.e2e.test.js.map +1 -1
- package/dist/commands/login-commands.d.ts +1 -1
- package/dist/commands/preference-commands.d.ts +1 -1
- package/dist/commands/worktree-commands.d.ts +1 -1
- package/dist/hello-thread.e2e.test.js +31 -0
- package/dist/hello-thread.e2e.test.js.map +1 -1
- package/dist/ingress.js +11 -6
- package/dist/ingress.js.map +1 -1
- package/dist/lock-routes.d.ts +14 -14
- package/dist/main.d.ts +2 -0
- package/dist/main.js +1 -0
- package/dist/main.js.map +1 -1
- package/dist/markdown/markdown.test.js +10 -0
- package/dist/markdown/markdown.test.js.map +1 -1
- package/dist/markdown/render-markdown.js +3 -1
- package/dist/markdown/render-markdown.js.map +1 -1
- package/dist/plugin/index.js +8 -2
- package/dist/plugin/index.js.map +1 -1
- package/dist/prompt.d.ts +6 -6
- package/dist/scheduler.d.ts +1 -1
- package/dist/session-events.js +9 -5
- package/dist/session-events.js.map +1 -1
- package/dist/sessions.d.ts +10 -10
- package/dist/sessions.js +1 -0
- package/dist/sessions.js.map +1 -1
- package/dist/system-prompt.d.ts +3 -1
- package/dist/system-prompt.js +52 -43
- package/dist/system-prompt.js.map +1 -1
- package/dist/tunnel.d.ts +21 -0
- package/dist/tunnel.js +296 -0
- package/dist/tunnel.js.map +1 -0
- package/dist/tunnel.test.d.ts +1 -0
- package/dist/tunnel.test.js +114 -0
- package/dist/tunnel.test.js.map +1 -0
- package/package.json +3 -3
- package/skills/kimaki-tunnel/SKILL.md +16 -11
- package/src/bot.ts +3 -0
- package/src/builtin-skills.test.ts +74 -1
- package/src/builtin-skills.ts +33 -0
- package/src/cli/bot.ts +13 -7
- package/src/cli-lock.e2e.test.ts +3 -2
- package/src/hello-thread.e2e.test.ts +35 -0
- package/src/ingress.ts +9 -5
- package/src/main.ts +4 -0
- package/src/markdown/markdown.test.ts +11 -0
- package/src/markdown/render-markdown.ts +3 -1
- package/src/plugin/index.ts +9 -2
- package/src/session-events.ts +8 -4
- package/src/sessions.ts +1 -0
- package/src/system-prompt.ts +56 -42
- package/src/tunnel.test.ts +115 -0
- package/src/tunnel.ts +308 -0
package/src/system-prompt.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
import dedent from 'string-dedent'
|
|
6
6
|
import { z } from 'zod'
|
|
7
7
|
|
|
8
|
+
import type { PromptSection } from './builtin-skills.ts'
|
|
9
|
+
|
|
8
10
|
export const INSTRUCTION_KEY = 'kimaki'
|
|
9
11
|
|
|
10
12
|
export type InstructionAgent = { name: string; description: string }
|
|
@@ -15,8 +17,8 @@ export type ScheduledRun = { id: number; cronExpr: string | null; timezone: stri
|
|
|
15
17
|
|
|
16
18
|
const PARENT_SESSION_LINE = 'Your parent OpenCode session ID is: '
|
|
17
19
|
|
|
18
|
-
// Every system-level instruction of Kimaki is built in this file.
|
|
19
|
-
//
|
|
20
|
+
// Every system-level instruction of Kimaki is built in this file. Parts are
|
|
21
|
+
// joined in order; a tagged part is static and `--disable-prompt <tag>` drops it.
|
|
20
22
|
export function baseInstructions({
|
|
21
23
|
sessionId,
|
|
22
24
|
threadId,
|
|
@@ -28,6 +30,7 @@ export function baseInstructions({
|
|
|
28
30
|
agents,
|
|
29
31
|
parentSessionId,
|
|
30
32
|
scheduledTask = null,
|
|
33
|
+
disabled = [],
|
|
31
34
|
}: {
|
|
32
35
|
sessionId: string
|
|
33
36
|
threadId: string
|
|
@@ -41,13 +44,15 @@ export function baseInstructions({
|
|
|
41
44
|
// Only from an explicit `kimaki send --parent-session`.
|
|
42
45
|
parentSessionId: string | null
|
|
43
46
|
scheduledTask?: ScheduledRun | null
|
|
47
|
+
// `kimaki --disable-prompt`: tagged static parts to leave out.
|
|
48
|
+
disabled?: readonly PromptSection[]
|
|
44
49
|
}): string {
|
|
45
50
|
const userArg = ` --user '${userId}'`
|
|
46
51
|
const parentArg = ` --parent-session ${sessionId}`
|
|
47
52
|
const topic = channelTopic?.trim()
|
|
48
53
|
const agentList = agents.map((agent) => `- \`${agent.name}\`${agent.description ? `: ${agent.description}` : ''}`).join('\n')
|
|
49
|
-
const
|
|
50
|
-
dedent`
|
|
54
|
+
const parts: Array<[PromptSection | null, string]> = [
|
|
55
|
+
[null, dedent`
|
|
51
56
|
The user is reading your messages from inside Discord, via kimaki.dev.
|
|
52
57
|
|
|
53
58
|
## Discord output
|
|
@@ -76,8 +81,8 @@ export function baseInstructions({
|
|
|
76
81
|
\`description\` is shown in Discord when the shell command is longer than 50 characters.
|
|
77
82
|
\`hasSideEffect\` only decides if Discord shows the shell call. Set it to \`true\` for every command the user should be aware of, for example commands that change state or that the user asked you to run (the user says "run sleep 100", so \`sleep 100\` gets \`true\`). Set \`false\` only for routine commands the user does not need to see.
|
|
78
83
|
The shell environment provides the \`kimaki\` command and OPENCODE_SESSION_ID. Do not pass stale IDs copied from another session.
|
|
79
|
-
|
|
80
|
-
dedent`
|
|
84
|
+
`],
|
|
85
|
+
[null, dedent`
|
|
81
86
|
Discord channel ID: ${channelId}
|
|
82
87
|
Discord thread ID: ${threadId}
|
|
83
88
|
Discord guild ID: ${guildId}
|
|
@@ -94,8 +99,8 @@ export function baseInstructions({
|
|
|
94
99
|
\`\`\`
|
|
95
100
|
|
|
96
101
|
Links have the form \`https://discord.com/channels/<guild_id>/<thread_id>\`. For changes made by another session, use that session's link (see "who edited a file").
|
|
97
|
-
|
|
98
|
-
dedent`
|
|
102
|
+
`],
|
|
103
|
+
['discord-permissions', dedent`
|
|
99
104
|
## permissions
|
|
100
105
|
|
|
101
106
|
Only users with these Discord permissions can send messages to the bot:
|
|
@@ -105,11 +110,13 @@ export function baseInstructions({
|
|
|
105
110
|
- "Kimaki" role (case-insensitive)
|
|
106
111
|
|
|
107
112
|
Other Discord bots are ignored by default. To allow another bot to trigger sessions (for multi-agent orchestration), assign it the "Kimaki" role.
|
|
108
|
-
|
|
113
|
+
`],
|
|
114
|
+
['kimaki-debugging', dedent`
|
|
109
115
|
## debugging kimaki issues
|
|
110
116
|
|
|
111
117
|
When sessions do not respond, the bot shows errors, or you need the Kimaki or OpenCode logs, load the \`kimaki-debug\` skill (logs, profiling, \`kimaki login\` for provider credentials) and read the logs before guessing. Do not restart the bot unless the user explicitly asks. ALWAYS read https://kimaki.dev/docs/guides/report-bugs before submitting any issue to Kimaki. Never open a pull request on remorses/kimaki unless remorses asked for one in a comment on the issue.
|
|
112
|
-
|
|
118
|
+
`],
|
|
119
|
+
[null, dedent`
|
|
113
120
|
## uploading files to discord
|
|
114
121
|
|
|
115
122
|
To upload files to the Discord thread (images, screenshots, long files that would clutter the chat), run:
|
|
@@ -137,20 +144,23 @@ export function baseInstructions({
|
|
|
137
144
|
\`\`\`
|
|
138
145
|
|
|
139
146
|
See \`kimaki tts --help\` for options like voice, speed, and style instructions.
|
|
140
|
-
|
|
147
|
+
`],
|
|
148
|
+
['file-upload-requests', dedent`
|
|
141
149
|
## requesting files from the user
|
|
142
150
|
|
|
143
151
|
To ask the user to upload files from their device, write all visible text first, then run \`kimaki upload-request --prompt 'Send the files' --max-files 5\`. This shows a native file picker in Discord. Set the shell timeout to 600000 ms.
|
|
144
152
|
The request waits at most 6 minutes and returns the local paths (in the session folder under \`uploads/\`) or cancelled. A new user prompt cancels the request. Do not keep waiting after cancellation.
|
|
145
153
|
You MUST call it LAST, after ALL text.
|
|
146
|
-
|
|
154
|
+
`],
|
|
155
|
+
['action-buttons', dedent`
|
|
147
156
|
## action buttons
|
|
148
157
|
|
|
149
158
|
To show 1 to 3 buttons, write all visible text first, then run \`kimaki buttons --button 'Label'\`. Repeat \`--button\` for more. Prefer a single button whenever possible. Colors are white, blue, green, red: \`--button 'Label:green'\`.
|
|
150
159
|
A click sends "User clicked: Label" to this session as a new prompt. Labels have at most 80 chars.
|
|
151
160
|
You MUST call \`kimaki buttons\` LAST, after ALL text. Never call it in a turn that has no text before it. The text must explain the choice. Labels alone are not an explanation.
|
|
152
161
|
The user does not see tool calls, command outputs, or subagent and task results. Before the buttons, concisely explain what was done in the session and any findings the choice depends on, then what each button does.
|
|
153
|
-
|
|
162
|
+
`],
|
|
163
|
+
['session-sleep', dedent`
|
|
154
164
|
## sleeping the session
|
|
155
165
|
|
|
156
166
|
Use \`kimaki sleep\` to pause this session for hours or days, then continue when the time is reached. The wake is stored in SQLite and survives bot restarts.
|
|
@@ -161,8 +171,9 @@ export function baseInstructions({
|
|
|
161
171
|
Wake is a later message that starts with \`Woke after sleeping until\`. Only then continue the wait reason.
|
|
162
172
|
To monitor something slow (email replies, PR reviews, long CI), use long waits: 2h or more, up to 1d. Never poll every few minutes. Prefer \`--until\` at a natural check time, for example the next morning.
|
|
163
173
|
On a wake where nothing changed, do not post a status report. Write one short line, then sleep again with a longer wait (double it, up to 1d).
|
|
164
|
-
|
|
165
|
-
|
|
174
|
+
`],
|
|
175
|
+
[null, scheduledTask ? scheduledTaskSection(scheduledTask) : ''],
|
|
176
|
+
[null, dedent`
|
|
166
177
|
## archiving the current thread
|
|
167
178
|
|
|
168
179
|
To archive the current Discord thread (hide it from sidebar) without stopping the session, run:
|
|
@@ -170,7 +181,8 @@ export function baseInstructions({
|
|
|
170
181
|
kimaki session archive ${threadId}
|
|
171
182
|
|
|
172
183
|
Only do this when the user explicitly asks to close or archive the thread, and only after your final message.
|
|
173
|
-
|
|
184
|
+
`],
|
|
185
|
+
['session-abort', dedent`
|
|
174
186
|
## aborting a session
|
|
175
187
|
|
|
176
188
|
If you made a mistake with \`kimaki send\` (wrong prompt, wrong channel, mangled heredoc), abort the session immediately using the session ID printed in the output:
|
|
@@ -178,7 +190,8 @@ export function baseInstructions({
|
|
|
178
190
|
kimaki session abort <session_id>
|
|
179
191
|
|
|
180
192
|
This stops the AI from processing but keeps the thread visible in Discord. Different from \`kimaki session archive\` which hides the thread.
|
|
181
|
-
|
|
193
|
+
`],
|
|
194
|
+
[null, dedent`
|
|
182
195
|
## updating the session title
|
|
183
196
|
|
|
184
197
|
Skip the first turn. OpenCode already auto-generates the title from the first message.
|
|
@@ -201,8 +214,8 @@ export function baseInstructions({
|
|
|
201
214
|
kimaki user list --guild ${guildId} --query "username"
|
|
202
215
|
|
|
203
216
|
This returns user IDs you can use for Discord mentions. It can fail when Server Members Intent is disabled, so prefer IDs from existing Discord metadata or raw mentions when possible.
|
|
204
|
-
|
|
205
|
-
dedent`
|
|
217
|
+
`],
|
|
218
|
+
[null, dedent`
|
|
206
219
|
## starting new sessions from CLI
|
|
207
220
|
|
|
208
221
|
Start a new thread/session in this channel with:
|
|
@@ -356,7 +369,8 @@ export function baseInstructions({
|
|
|
356
369
|
Thread reminders: when the user says "remind me about this in 2 hours", schedule a send to this thread. \`--notify-only\` does not work with \`--thread\`; the scheduled prompt always runs in that thread's session:
|
|
357
370
|
|
|
358
371
|
kimaki send --thread ${threadId} --prompt 'Reminder: you asked to be reminded about this thread.' --send-at '<future_UTC_time>' --agent <current_agent>${userArg}
|
|
359
|
-
|
|
372
|
+
`],
|
|
373
|
+
[null, dedent`
|
|
360
374
|
## reading other sessions
|
|
361
375
|
|
|
362
376
|
\`\`\`bash
|
|
@@ -369,7 +383,7 @@ export function baseInstructions({
|
|
|
369
383
|
|
|
370
384
|
Each row shows the session ID, status (\`busy\`, \`waiting\` for input, or \`idle\`), the title, and \`tokens: N\`. Titles prefixed with \`btw:\` are side sessions that answer a related user question in parallel. They are not duplicate sessions of the main task.
|
|
371
385
|
|
|
372
|
-
To search past sessions, search the transcripts. Kimaki writes one markdown file per session to \`${dataDir}/transcripts/<session id>.md\` when a run ends (user messages, assistant text
|
|
386
|
+
To search past sessions, search the transcripts. Kimaki writes one markdown file per session to \`${dataDir}/transcripts/<session id>.md\` when a run ends (user messages, assistant text, thinking, tool inputs up to 5000 chars and compaction summaries; no tool outputs). The first lines are \`# <title> (<session id>)\` and \`directory: <folder>\`; messages start with \`## user <time>\` or \`## assistant <time> (<agent>, <model>)\`. Use rg (or \`grep -r\` when rg is missing):
|
|
373
387
|
|
|
374
388
|
\`\`\`bash
|
|
375
389
|
rg -il 'auth timeout' ${dataDir}/transcripts # sessions that mention it; the file name is the session ID
|
|
@@ -418,7 +432,8 @@ export function baseInstructions({
|
|
|
418
432
|
Output is newest first. Each row has the **session ID** (\`ses_xxx\`), the **title** (Discord thread name, so you can tell what that session was doing), and **time ago** (when it last edited the file).
|
|
419
433
|
|
|
420
434
|
Use this before committing a file this session did not edit. Get the original session's link with \`kimaki session url <session_id>\` and put it as the last line of the commit message: \`Discord: https://discord.com/channels/<guild_id>/<thread_id>\`. If that session has no Kimaki thread, use \`Session: ses_xxx\` instead. If this session edited the file, use this thread's link. If files come from different sessions, split the commit by session. Do not attribute another session's edits to this one.
|
|
421
|
-
|
|
435
|
+
`],
|
|
436
|
+
['cross-project-commands', dedent`
|
|
422
437
|
## cross-project commands
|
|
423
438
|
|
|
424
439
|
When the user references another project by name, run \`kimaki project list\` to find its directory and channel ID, then read files, search code, or run commands directly in that directory. If the project is not listed, register its root with \`kimaki project add /path/to/repo\` (this creates a Discord channel). Never add subfolders of an existing project.
|
|
@@ -441,7 +456,8 @@ export function baseInstructions({
|
|
|
441
456
|
kimaki send --channel <channel_id> --prompt 'Plan how to update the API client to v2' --agent <current_agent>
|
|
442
457
|
kimaki send --project /path/to/other-repo --prompt 'Plan how to bump version to 1.2.0' --agent <current_agent>
|
|
443
458
|
\`\`\`
|
|
444
|
-
|
|
459
|
+
`],
|
|
460
|
+
['waiting-sessions', dedent`
|
|
445
461
|
## waiting for a session to finish
|
|
446
462
|
|
|
447
463
|
\`--wait\` blocks until a session completes and prints its full conversation to stdout. Use it when you need another session's result before continuing: fixing a bug in another project first, or chaining sessions where the next depends on the previous output. When the user asks you to wait for an existing session, run \`kimaki session wait <session_id>\` yourself via the shell tool and continue from the printed markdown. Do not tell the user to run it.
|
|
@@ -461,12 +477,8 @@ export function baseInstructions({
|
|
|
461
477
|
\`\`\`
|
|
462
478
|
|
|
463
479
|
\`session list --active\` exits 0 while it finds active sessions, 1 when none remain, and 64 on errors. The loop stops only on 1, so an error never looks like "no active sessions". \`--active\` never lists the calling session (\`OPENCODE_SESSION_ID\`), so the loop does not wait for itself. Sessions can start again after the loop ends, so run the check again right before each commit. \`session wait\` returns once the model finishes responding, or when the session pauses to show the user a question (it does not finish on its own until answered).
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
When pulling submodules and they jump to a new commit, commit that submodule pointer update right away before doing other work. Otherwise critique diffs later will include the noisy submodule jump along with the real changes.
|
|
468
|
-
`,
|
|
469
|
-
dedent`
|
|
480
|
+
`],
|
|
481
|
+
['critique-diffs', dedent`
|
|
470
482
|
## showing diffs
|
|
471
483
|
|
|
472
484
|
The user cannot see tool output. Share diffs as critique web URLs, never raw \`git diff\` output:
|
|
@@ -495,14 +507,13 @@ export function baseInstructions({
|
|
|
495
507
|
|
|
496
508
|
If the changes are already committed (only commit when the user asks), show one URL per commit with \`bunx critique --commit <hash> --web\`, running the critique calls in parallel.
|
|
497
509
|
|
|
510
|
+
When pulling submodules and they jump to a new commit, commit that submodule pointer update right away before doing other work. Otherwise critique diffs later will include the noisy submodule jump along with the real changes.
|
|
511
|
+
|
|
498
512
|
Users can leave line comments on a diff page (Agentation widget, bottom right). When they say they did, read them with \`curl https://critique.work/v/<id>/annotations\` (or WebFetch). It returns markdown with file, line, and comment text.
|
|
499
513
|
|
|
500
514
|
critique is open source (MIT, https://github.com/remorses/critique). Diff URLs are unique, unguessable, not indexed, and ephemeral. If the user is worried about uploading code, tell them this.
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
Localhost URLs are useless from Discord. When the user should open a local dev server in a browser, load the \`kimaki-tunnel\` skill: it wraps the server in \`kimaki tunnel\` to get a public URL.
|
|
505
|
-
|
|
515
|
+
`],
|
|
516
|
+
['markdown-formatting', dedent`
|
|
506
517
|
## markdown formatting
|
|
507
518
|
|
|
508
519
|
Format responses in **Claude-style markdown** - structured, scannable, never walls of text. Use:
|
|
@@ -519,7 +530,8 @@ export function baseInstructions({
|
|
|
519
530
|
Discord supports: headings, bold, italic, strikethrough, code blocks, inline code, quotes, lists, and links.
|
|
520
531
|
|
|
521
532
|
NEVER wrap URLs in inline code or code blocks - this breaks clickability in Discord. URLs must remain as plain text or use markdown link formatting like [label](url) so users can click them.
|
|
522
|
-
|
|
533
|
+
`],
|
|
534
|
+
['discord-callouts', dedent`
|
|
523
535
|
## Callouts in Kimaki Discord
|
|
524
536
|
|
|
525
537
|
Use \`<callout>\` HTML blocks for important notices in Discord. Do **not** use GitHub callout syntax like \`> [!WARNING]\`, because Kimaki renders \`<callout>\` natively.
|
|
@@ -553,15 +565,18 @@ export function baseInstructions({
|
|
|
553
565
|
- action-required notes, breaking caveats, or important limitations, use **purple** like \`#8b5cf6\`
|
|
554
566
|
|
|
555
567
|
Do not wrap the whole response in callouts. Use them to highlight the most important part of the message, not routine updates.
|
|
556
|
-
|
|
568
|
+
`],
|
|
569
|
+
['search-result-urls', dedent`
|
|
557
570
|
## URLs in search results
|
|
558
571
|
|
|
559
572
|
When performing web searches, code searches, or any lookup that returns URLs (GitHub repos, docs, Stack Overflow, npm packages, etc.), ALWAYS include the URLs in your response so the user can click them. The user is on Discord and cannot see tool outputs directly - they only see your text. If you found a relevant link, show it. Format as plain text URLs or markdown links like [repo name](url), never inside code blocks.
|
|
560
|
-
|
|
573
|
+
`],
|
|
574
|
+
['ascii-diagrams', dedent`
|
|
561
575
|
## diagrams
|
|
562
576
|
|
|
563
577
|
Make heavy use of diagrams to explain architecture, flows, and relationships. Create diagrams using ASCII art inside code blocks. Prefer diagrams over lengthy text explanations whenever possible. Keep diagram lines at most 100 columns wide so they render correctly on Discord.
|
|
564
|
-
|
|
578
|
+
`],
|
|
579
|
+
['action-buttons', dedent`
|
|
565
580
|
## ending conversations with options
|
|
566
581
|
|
|
567
582
|
The \`question\` tool is disabled in Kimaki sessions. To offer choices, use \`kimaki buttons\` instead.
|
|
@@ -577,10 +592,10 @@ export function baseInstructions({
|
|
|
577
592
|
- After completing edits: offer "Commit changes?"
|
|
578
593
|
- If a plan has multiple strategy of implementation show these as options
|
|
579
594
|
- After a genuinely ambiguous request where you cannot infer intent: offer the different approaches
|
|
580
|
-
|
|
581
|
-
|
|
595
|
+
`],
|
|
596
|
+
[null, topic ? `<channel-topic>\n${topic}\n</channel-topic>` : ''],
|
|
582
597
|
]
|
|
583
|
-
return
|
|
598
|
+
return parts.filter(([tag, text]) => text !== '' && !(tag && disabled.includes(tag))).map(([, text]) => text).join('\n\n')
|
|
584
599
|
}
|
|
585
600
|
|
|
586
601
|
function scheduledTaskSection(task: ScheduledRun): string {
|
|
@@ -588,7 +603,6 @@ function scheduledTaskSection(task: ScheduledRun): string {
|
|
|
588
603
|
? `Schedule: cron \`${task.cronExpr}\` in ${task.timezone || 'UTC'}. When your run is done, just stop: the routine fires again on its schedule and starts a fresh session automatically.`
|
|
589
604
|
: 'This routine runs once and does not repeat. When your run is done, just stop.'
|
|
590
605
|
return dedent`
|
|
591
|
-
|
|
592
606
|
## routine session
|
|
593
607
|
|
|
594
608
|
This session was started automatically by kimaki routine #${task.id}.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import http from 'node:http'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { createServer } from 'vite'
|
|
6
|
+
import { expect, test } from 'vitest'
|
|
7
|
+
import { detectPortFromText, parseNetstatListeningPids, routeName, startHostProxy } from './tunnel.ts'
|
|
8
|
+
|
|
9
|
+
test('detects the dev server port and skips noise lines', () => {
|
|
10
|
+
const vite = '\u001b[33mPort 5173 is in use, trying another one...\u001b[39m\n ➜ Local: \u001b[36mhttp://localhost:\u001b[1m5174\u001b[22m/\u001b[39m\n'
|
|
11
|
+
expect({
|
|
12
|
+
vite: detectPortFromText(vite),
|
|
13
|
+
next: detectPortFromText(' - Local: http://127.0.0.1:3000'),
|
|
14
|
+
bare: detectPortFromText('Server running at 0.0.0.0:4321'),
|
|
15
|
+
inspector: detectPortFromText('Debugger listening on ws://127.0.0.1:9229/abc\nready on http://localhost:4173'),
|
|
16
|
+
outOfRange: detectPortFromText('listening on localhost:70000'),
|
|
17
|
+
portOnly: detectPortFromText('Port 5173 is in use'),
|
|
18
|
+
}).toMatchInlineSnapshot(`
|
|
19
|
+
{
|
|
20
|
+
"bare": 4321,
|
|
21
|
+
"inspector": 4173,
|
|
22
|
+
"next": 3000,
|
|
23
|
+
"outOfRange": null,
|
|
24
|
+
"portOnly": null,
|
|
25
|
+
"vite": 5174,
|
|
26
|
+
}
|
|
27
|
+
`)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('route name: explicit, then tuistory session, then random', () => {
|
|
31
|
+
expect({
|
|
32
|
+
explicit: routeName({ name: 'docs', tuistorySessionId: 'abc123' }),
|
|
33
|
+
normalized: routeName({ name: '_My_App.dev_' }),
|
|
34
|
+
invalid: String(routeName({ name: '___' })),
|
|
35
|
+
tuistory: routeName({ tuistorySessionId: 'Holocron_Dev' }),
|
|
36
|
+
tuistoryInvalid: String(routeName({ tuistorySessionId: '___' })).length,
|
|
37
|
+
random: String(routeName({})).length,
|
|
38
|
+
}).toMatchInlineSnapshot(`
|
|
39
|
+
{
|
|
40
|
+
"explicit": "docs",
|
|
41
|
+
"invalid": "Error: Invalid tunnel ID "___": use letters, digits and dashes",
|
|
42
|
+
"normalized": "my-app-dev",
|
|
43
|
+
"random": 16,
|
|
44
|
+
"tuistory": "holocron-dev",
|
|
45
|
+
"tuistoryInvalid": 16,
|
|
46
|
+
}
|
|
47
|
+
`)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('netstat listening pids on Windows', () => {
|
|
51
|
+
const stdout = [
|
|
52
|
+
'Active Connections',
|
|
53
|
+
' Proto Local Address Foreign Address State PID',
|
|
54
|
+
' TCP 0.0.0.0:5173 0.0.0.0:0 LISTENING 1234',
|
|
55
|
+
' TCP [::]:5173 [::]:0 LISTENING 1234',
|
|
56
|
+
' TCP 127.0.0.1:51730 0.0.0.0:0 LISTENING 999',
|
|
57
|
+
' TCP 127.0.0.1:5173 127.0.0.1:60000 ESTABLISHED 777',
|
|
58
|
+
' UDP 0.0.0.0:5173 *:* 555',
|
|
59
|
+
].join('\r\n')
|
|
60
|
+
expect(parseNetstatListeningPids({ stdout, port: 5173 })).toMatchInlineSnapshot(`
|
|
61
|
+
[
|
|
62
|
+
1234,
|
|
63
|
+
]
|
|
64
|
+
`)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
function request({ port, headers }: { port: number; headers: http.OutgoingHttpHeaders }): Promise<string> {
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
const req = http.request({ host: '127.0.0.1', port, path: '/', headers })
|
|
70
|
+
req.on('response', (res) => {
|
|
71
|
+
res.resume()
|
|
72
|
+
resolve(String(res.statusCode))
|
|
73
|
+
req.destroy()
|
|
74
|
+
})
|
|
75
|
+
req.on('upgrade', (res, socket) => {
|
|
76
|
+
resolve(`${res.statusCode} ${res.headers['sec-websocket-protocol']}`)
|
|
77
|
+
socket.destroy()
|
|
78
|
+
})
|
|
79
|
+
req.on('error', reject)
|
|
80
|
+
req.end()
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
test('host proxy lets the public tunnel host through Vite host check, HMR included', async () => {
|
|
85
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'kimaki-tunnel-'))
|
|
86
|
+
fs.writeFileSync(path.join(root, 'index.html'), '<p>hi</p>')
|
|
87
|
+
const vite = await createServer({ root, configFile: false, logLevel: 'silent', server: { host: '127.0.0.1', port: 0 } })
|
|
88
|
+
await vite.listen()
|
|
89
|
+
const address = vite.httpServer!.address()
|
|
90
|
+
if (!address || typeof address === 'string') throw new Error('no vite address')
|
|
91
|
+
const proxy = await startHostProxy({ host: '127.0.0.1', port: address.port })
|
|
92
|
+
if (proxy instanceof Error) throw proxy
|
|
93
|
+
const proxyAddress = proxy.address()
|
|
94
|
+
if (!proxyAddress || typeof proxyAddress === 'string') throw new Error('no proxy address')
|
|
95
|
+
|
|
96
|
+
const publicHost = { host: 'docs.abc.opentunnel.xyz' }
|
|
97
|
+
const hmr = { ...publicHost, connection: 'Upgrade', upgrade: 'websocket', 'sec-websocket-version': '13', 'sec-websocket-key': 'dGhlIHNhbXBsZSBub25jZQ==', 'sec-websocket-protocol': 'vite-hmr' }
|
|
98
|
+
try {
|
|
99
|
+
expect({
|
|
100
|
+
direct: await request({ port: address.port, headers: publicHost }),
|
|
101
|
+
proxied: await request({ port: proxyAddress.port, headers: publicHost }),
|
|
102
|
+
proxiedHmr: await request({ port: proxyAddress.port, headers: hmr }),
|
|
103
|
+
}).toMatchInlineSnapshot(`
|
|
104
|
+
{
|
|
105
|
+
"direct": "403",
|
|
106
|
+
"proxied": "200",
|
|
107
|
+
"proxiedHmr": "101 vite-hmr",
|
|
108
|
+
}
|
|
109
|
+
`)
|
|
110
|
+
} finally {
|
|
111
|
+
proxy.close()
|
|
112
|
+
await vite.close()
|
|
113
|
+
fs.rmSync(root, { recursive: true, force: true })
|
|
114
|
+
}
|
|
115
|
+
})
|