rird 1.0.200
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/AGENTS.md +27 -0
- package/Dockerfile +18 -0
- package/README.md +15 -0
- package/bin/opencode +336 -0
- package/bin/pty-wrapper.js +285 -0
- package/bunfig.toml +4 -0
- package/facebook_ads_library.png +0 -0
- package/nul`nif +0 -0
- package/package.json +111 -0
- package/parsers-config.ts +239 -0
- package/rird-1.0.199.tgz +0 -0
- package/script/build-windows.ts +54 -0
- package/script/build.ts +167 -0
- package/script/postinstall.mjs +544 -0
- package/script/publish-registries.ts +187 -0
- package/script/publish.ts +72 -0
- package/script/schema.ts +47 -0
- package/src/acp/README.md +164 -0
- package/src/acp/agent.ts +1063 -0
- package/src/acp/session.ts +101 -0
- package/src/acp/types.ts +22 -0
- package/src/agent/agent.ts +367 -0
- package/src/agent/generate.txt +75 -0
- package/src/agent/prompt/compaction.txt +12 -0
- package/src/agent/prompt/explore.txt +18 -0
- package/src/agent/prompt/summary.txt +10 -0
- package/src/agent/prompt/title.txt +36 -0
- package/src/auth/index.ts +70 -0
- package/src/bun/index.ts +114 -0
- package/src/bus/bus-event.ts +43 -0
- package/src/bus/global.ts +10 -0
- package/src/bus/index.ts +105 -0
- package/src/cli/bootstrap.ts +17 -0
- package/src/cli/cmd/acp.ts +88 -0
- package/src/cli/cmd/agent.ts +256 -0
- package/src/cli/cmd/auth.ts +391 -0
- package/src/cli/cmd/cmd.ts +7 -0
- package/src/cli/cmd/debug/config.ts +15 -0
- package/src/cli/cmd/debug/file.ts +91 -0
- package/src/cli/cmd/debug/index.ts +43 -0
- package/src/cli/cmd/debug/lsp.ts +48 -0
- package/src/cli/cmd/debug/ripgrep.ts +83 -0
- package/src/cli/cmd/debug/scrap.ts +15 -0
- package/src/cli/cmd/debug/skill.ts +15 -0
- package/src/cli/cmd/debug/snapshot.ts +48 -0
- package/src/cli/cmd/export.ts +88 -0
- package/src/cli/cmd/generate.ts +38 -0
- package/src/cli/cmd/github.ts +1400 -0
- package/src/cli/cmd/import.ts +98 -0
- package/src/cli/cmd/mcp.ts +654 -0
- package/src/cli/cmd/models.ts +77 -0
- package/src/cli/cmd/pr.ts +112 -0
- package/src/cli/cmd/run.ts +368 -0
- package/src/cli/cmd/serve.ts +31 -0
- package/src/cli/cmd/session.ts +106 -0
- package/src/cli/cmd/stats.ts +298 -0
- package/src/cli/cmd/tui/app.tsx +696 -0
- package/src/cli/cmd/tui/attach.ts +30 -0
- package/src/cli/cmd/tui/component/border.tsx +21 -0
- package/src/cli/cmd/tui/component/dialog-agent.tsx +31 -0
- package/src/cli/cmd/tui/component/dialog-command.tsx +124 -0
- package/src/cli/cmd/tui/component/dialog-mcp.tsx +86 -0
- package/src/cli/cmd/tui/component/dialog-model.tsx +245 -0
- package/src/cli/cmd/tui/component/dialog-provider.tsx +224 -0
- package/src/cli/cmd/tui/component/dialog-session-list.tsx +102 -0
- package/src/cli/cmd/tui/component/dialog-session-rename.tsx +31 -0
- package/src/cli/cmd/tui/component/dialog-stash.tsx +86 -0
- package/src/cli/cmd/tui/component/dialog-status.tsx +162 -0
- package/src/cli/cmd/tui/component/dialog-tag.tsx +44 -0
- package/src/cli/cmd/tui/component/dialog-theme-list.tsx +50 -0
- package/src/cli/cmd/tui/component/did-you-know.tsx +85 -0
- package/src/cli/cmd/tui/component/logo.tsx +35 -0
- package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +574 -0
- package/src/cli/cmd/tui/component/prompt/history.tsx +108 -0
- package/src/cli/cmd/tui/component/prompt/index.tsx +1090 -0
- package/src/cli/cmd/tui/component/prompt/stash.tsx +101 -0
- package/src/cli/cmd/tui/component/tips.ts +27 -0
- package/src/cli/cmd/tui/component/todo-item.tsx +32 -0
- package/src/cli/cmd/tui/context/args.tsx +14 -0
- package/src/cli/cmd/tui/context/directory.ts +13 -0
- package/src/cli/cmd/tui/context/exit.tsx +23 -0
- package/src/cli/cmd/tui/context/helper.tsx +25 -0
- package/src/cli/cmd/tui/context/keybind.tsx +101 -0
- package/src/cli/cmd/tui/context/kv.tsx +49 -0
- package/src/cli/cmd/tui/context/local.tsx +354 -0
- package/src/cli/cmd/tui/context/prompt.tsx +18 -0
- package/src/cli/cmd/tui/context/route.tsx +46 -0
- package/src/cli/cmd/tui/context/sdk.tsx +74 -0
- package/src/cli/cmd/tui/context/sync.tsx +372 -0
- package/src/cli/cmd/tui/context/theme/aura.json +69 -0
- package/src/cli/cmd/tui/context/theme/ayu.json +80 -0
- package/src/cli/cmd/tui/context/theme/catppuccin-frappe.json +233 -0
- package/src/cli/cmd/tui/context/theme/catppuccin-macchiato.json +233 -0
- package/src/cli/cmd/tui/context/theme/catppuccin.json +112 -0
- package/src/cli/cmd/tui/context/theme/cobalt2.json +228 -0
- package/src/cli/cmd/tui/context/theme/cursor.json +249 -0
- package/src/cli/cmd/tui/context/theme/dracula.json +219 -0
- package/src/cli/cmd/tui/context/theme/everforest.json +241 -0
- package/src/cli/cmd/tui/context/theme/flexoki.json +237 -0
- package/src/cli/cmd/tui/context/theme/github.json +233 -0
- package/src/cli/cmd/tui/context/theme/gruvbox.json +95 -0
- package/src/cli/cmd/tui/context/theme/kanagawa.json +77 -0
- package/src/cli/cmd/tui/context/theme/lucent-orng.json +227 -0
- package/src/cli/cmd/tui/context/theme/material.json +235 -0
- package/src/cli/cmd/tui/context/theme/matrix.json +77 -0
- package/src/cli/cmd/tui/context/theme/mercury.json +252 -0
- package/src/cli/cmd/tui/context/theme/monokai.json +221 -0
- package/src/cli/cmd/tui/context/theme/nightowl.json +221 -0
- package/src/cli/cmd/tui/context/theme/nord.json +223 -0
- package/src/cli/cmd/tui/context/theme/one-dark.json +84 -0
- package/src/cli/cmd/tui/context/theme/orng.json +245 -0
- package/src/cli/cmd/tui/context/theme/palenight.json +222 -0
- package/src/cli/cmd/tui/context/theme/rird.json +245 -0
- package/src/cli/cmd/tui/context/theme/rosepine.json +234 -0
- package/src/cli/cmd/tui/context/theme/solarized.json +223 -0
- package/src/cli/cmd/tui/context/theme/synthwave84.json +226 -0
- package/src/cli/cmd/tui/context/theme/tokyonight.json +243 -0
- package/src/cli/cmd/tui/context/theme/vercel.json +245 -0
- package/src/cli/cmd/tui/context/theme/vesper.json +218 -0
- package/src/cli/cmd/tui/context/theme/zenburn.json +223 -0
- package/src/cli/cmd/tui/context/theme.tsx +1109 -0
- package/src/cli/cmd/tui/event.ts +40 -0
- package/src/cli/cmd/tui/routes/home.tsx +138 -0
- package/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx +64 -0
- package/src/cli/cmd/tui/routes/session/dialog-message.tsx +109 -0
- package/src/cli/cmd/tui/routes/session/dialog-subagent.tsx +26 -0
- package/src/cli/cmd/tui/routes/session/dialog-timeline.tsx +47 -0
- package/src/cli/cmd/tui/routes/session/footer.tsx +88 -0
- package/src/cli/cmd/tui/routes/session/header.tsx +125 -0
- package/src/cli/cmd/tui/routes/session/index.tsx +1864 -0
- package/src/cli/cmd/tui/routes/session/sidebar.tsx +318 -0
- package/src/cli/cmd/tui/spawn.ts +60 -0
- package/src/cli/cmd/tui/thread.ts +142 -0
- package/src/cli/cmd/tui/ui/dialog-alert.tsx +57 -0
- package/src/cli/cmd/tui/ui/dialog-confirm.tsx +83 -0
- package/src/cli/cmd/tui/ui/dialog-help.tsx +38 -0
- package/src/cli/cmd/tui/ui/dialog-prompt.tsx +77 -0
- package/src/cli/cmd/tui/ui/dialog-select.tsx +332 -0
- package/src/cli/cmd/tui/ui/dialog.tsx +170 -0
- package/src/cli/cmd/tui/ui/spinner.ts +368 -0
- package/src/cli/cmd/tui/ui/toast.tsx +100 -0
- package/src/cli/cmd/tui/util/clipboard.ts +127 -0
- package/src/cli/cmd/tui/util/editor.ts +32 -0
- package/src/cli/cmd/tui/util/terminal.ts +114 -0
- package/src/cli/cmd/tui/worker.ts +63 -0
- package/src/cli/cmd/uninstall.ts +344 -0
- package/src/cli/cmd/upgrade.ts +100 -0
- package/src/cli/cmd/web.ts +84 -0
- package/src/cli/error.ts +56 -0
- package/src/cli/ui.ts +84 -0
- package/src/cli/upgrade.ts +25 -0
- package/src/command/index.ts +80 -0
- package/src/command/template/initialize.txt +10 -0
- package/src/command/template/review.txt +97 -0
- package/src/config/config.ts +995 -0
- package/src/config/markdown.ts +41 -0
- package/src/env/index.ts +26 -0
- package/src/file/ignore.ts +83 -0
- package/src/file/index.ts +328 -0
- package/src/file/ripgrep.ts +393 -0
- package/src/file/time.ts +64 -0
- package/src/file/watcher.ts +103 -0
- package/src/flag/flag.ts +46 -0
- package/src/format/formatter.ts +315 -0
- package/src/format/index.ts +137 -0
- package/src/global/index.ts +52 -0
- package/src/id/id.ts +73 -0
- package/src/ide/index.ts +76 -0
- package/src/index.ts +240 -0
- package/src/installation/index.ts +239 -0
- package/src/lsp/client.ts +229 -0
- package/src/lsp/index.ts +485 -0
- package/src/lsp/language.ts +116 -0
- package/src/lsp/server.ts +1895 -0
- package/src/mcp/auth.ts +135 -0
- package/src/mcp/index.ts +690 -0
- package/src/mcp/oauth-callback.ts +200 -0
- package/src/mcp/oauth-provider.ts +154 -0
- package/src/patch/index.ts +622 -0
- package/src/permission/index.ts +199 -0
- package/src/plugin/index.ts +91 -0
- package/src/project/bootstrap.ts +31 -0
- package/src/project/instance.ts +78 -0
- package/src/project/project.ts +221 -0
- package/src/project/state.ts +65 -0
- package/src/project/vcs.ts +76 -0
- package/src/provider/auth.ts +143 -0
- package/src/provider/models-macro.ts +11 -0
- package/src/provider/models.ts +106 -0
- package/src/provider/provider.ts +1071 -0
- package/src/provider/sdk/openai-compatible/src/README.md +5 -0
- package/src/provider/sdk/openai-compatible/src/index.ts +2 -0
- package/src/provider/sdk/openai-compatible/src/openai-compatible-provider.ts +100 -0
- package/src/provider/sdk/openai-compatible/src/responses/convert-to-openai-responses-input.ts +303 -0
- package/src/provider/sdk/openai-compatible/src/responses/map-openai-responses-finish-reason.ts +22 -0
- package/src/provider/sdk/openai-compatible/src/responses/openai-config.ts +18 -0
- package/src/provider/sdk/openai-compatible/src/responses/openai-error.ts +22 -0
- package/src/provider/sdk/openai-compatible/src/responses/openai-responses-api-types.ts +207 -0
- package/src/provider/sdk/openai-compatible/src/responses/openai-responses-language-model.ts +1713 -0
- package/src/provider/sdk/openai-compatible/src/responses/openai-responses-prepare-tools.ts +177 -0
- package/src/provider/sdk/openai-compatible/src/responses/openai-responses-settings.ts +1 -0
- package/src/provider/sdk/openai-compatible/src/responses/tool/code-interpreter.ts +88 -0
- package/src/provider/sdk/openai-compatible/src/responses/tool/file-search.ts +128 -0
- package/src/provider/sdk/openai-compatible/src/responses/tool/image-generation.ts +115 -0
- package/src/provider/sdk/openai-compatible/src/responses/tool/local-shell.ts +65 -0
- package/src/provider/sdk/openai-compatible/src/responses/tool/web-search-preview.ts +104 -0
- package/src/provider/sdk/openai-compatible/src/responses/tool/web-search.ts +103 -0
- package/src/provider/transform.ts +455 -0
- package/src/pty/index.ts +231 -0
- package/src/security/guardrails.test.ts +341 -0
- package/src/security/guardrails.ts +558 -0
- package/src/security/index.ts +19 -0
- package/src/server/error.ts +36 -0
- package/src/server/project.ts +79 -0
- package/src/server/server.ts +2642 -0
- package/src/server/tui.ts +71 -0
- package/src/session/compaction.ts +223 -0
- package/src/session/index.ts +461 -0
- package/src/session/llm.ts +201 -0
- package/src/session/message-v2.ts +690 -0
- package/src/session/message.ts +189 -0
- package/src/session/processor.ts +409 -0
- package/src/session/prompt/act-switch.txt +5 -0
- package/src/session/prompt/anthropic-20250930.txt +166 -0
- package/src/session/prompt/anthropic.txt +85 -0
- package/src/session/prompt/anthropic_spoof.txt +1 -0
- package/src/session/prompt/beast.txt +103 -0
- package/src/session/prompt/codex.txt +304 -0
- package/src/session/prompt/copilot-gpt-5.txt +138 -0
- package/src/session/prompt/gemini.txt +85 -0
- package/src/session/prompt/max-steps.txt +16 -0
- package/src/session/prompt/plan-reminder-anthropic.txt +35 -0
- package/src/session/prompt/plan.txt +24 -0
- package/src/session/prompt/polaris.txt +84 -0
- package/src/session/prompt/qwen.txt +106 -0
- package/src/session/prompt.ts +1509 -0
- package/src/session/retry.ts +86 -0
- package/src/session/revert.ts +108 -0
- package/src/session/sensitive-filter.test.ts +327 -0
- package/src/session/sensitive-filter.ts +466 -0
- package/src/session/status.ts +76 -0
- package/src/session/summary.ts +194 -0
- package/src/session/system.ts +120 -0
- package/src/session/todo.ts +37 -0
- package/src/share/share-next.ts +194 -0
- package/src/share/share.ts +87 -0
- package/src/shell/shell.ts +67 -0
- package/src/skill/index.ts +1 -0
- package/src/skill/skill.ts +83 -0
- package/src/snapshot/index.ts +197 -0
- package/src/storage/storage.ts +226 -0
- package/src/tests/agent.test.ts +308 -0
- package/src/tests/build-guards.test.ts +267 -0
- package/src/tests/config.test.ts +664 -0
- package/src/tests/tool-registry.test.ts +589 -0
- package/src/tool/bash.ts +317 -0
- package/src/tool/bash.txt +158 -0
- package/src/tool/batch.ts +175 -0
- package/src/tool/batch.txt +24 -0
- package/src/tool/codesearch.ts +168 -0
- package/src/tool/codesearch.txt +12 -0
- package/src/tool/edit.ts +675 -0
- package/src/tool/edit.txt +10 -0
- package/src/tool/glob.ts +65 -0
- package/src/tool/glob.txt +6 -0
- package/src/tool/grep.ts +121 -0
- package/src/tool/grep.txt +8 -0
- package/src/tool/invalid.ts +17 -0
- package/src/tool/ls.ts +110 -0
- package/src/tool/ls.txt +1 -0
- package/src/tool/lsp-diagnostics.ts +26 -0
- package/src/tool/lsp-diagnostics.txt +1 -0
- package/src/tool/lsp-hover.ts +31 -0
- package/src/tool/lsp-hover.txt +1 -0
- package/src/tool/lsp.ts +87 -0
- package/src/tool/lsp.txt +19 -0
- package/src/tool/multiedit.ts +46 -0
- package/src/tool/multiedit.txt +41 -0
- package/src/tool/patch.ts +233 -0
- package/src/tool/patch.txt +1 -0
- package/src/tool/read.ts +219 -0
- package/src/tool/read.txt +12 -0
- package/src/tool/registry.ts +162 -0
- package/src/tool/skill.ts +100 -0
- package/src/tool/task.ts +136 -0
- package/src/tool/task.txt +51 -0
- package/src/tool/todo.ts +39 -0
- package/src/tool/todoread.txt +14 -0
- package/src/tool/todowrite.txt +167 -0
- package/src/tool/tool.ts +71 -0
- package/src/tool/webfetch.ts +198 -0
- package/src/tool/webfetch.txt +13 -0
- package/src/tool/websearch.ts +180 -0
- package/src/tool/websearch.txt +11 -0
- package/src/tool/write.ts +110 -0
- package/src/tool/write.txt +8 -0
- package/src/util/archive.ts +16 -0
- package/src/util/color.ts +19 -0
- package/src/util/context.ts +25 -0
- package/src/util/defer.ts +12 -0
- package/src/util/eventloop.ts +20 -0
- package/src/util/filesystem.ts +83 -0
- package/src/util/fn.ts +11 -0
- package/src/util/iife.ts +3 -0
- package/src/util/keybind.ts +102 -0
- package/src/util/lazy.ts +11 -0
- package/src/util/license.ts +325 -0
- package/src/util/locale.ts +81 -0
- package/src/util/lock.ts +98 -0
- package/src/util/log.ts +180 -0
- package/src/util/queue.ts +32 -0
- package/src/util/rpc.ts +42 -0
- package/src/util/scrap.ts +10 -0
- package/src/util/signal.ts +12 -0
- package/src/util/timeout.ts +14 -0
- package/src/util/token.ts +7 -0
- package/src/util/wildcard.ts +54 -0
- package/sst-env.d.ts +9 -0
- package/test/agent/agent.test.ts +146 -0
- package/test/bun.test.ts +53 -0
- package/test/cli/github-remote.test.ts +80 -0
- package/test/config/agent-color.test.ts +66 -0
- package/test/config/config.test.ts +535 -0
- package/test/config/markdown.test.ts +89 -0
- package/test/file/ignore.test.ts +10 -0
- package/test/fixture/fixture.ts +36 -0
- package/test/fixture/lsp/fake-lsp-server.js +77 -0
- package/test/ide/ide.test.ts +82 -0
- package/test/keybind.test.ts +421 -0
- package/test/lsp/client.test.ts +95 -0
- package/test/mcp/headers.test.ts +153 -0
- package/test/patch/patch.test.ts +348 -0
- package/test/preload.ts +57 -0
- package/test/project/project.test.ts +72 -0
- package/test/provider/provider.test.ts +1809 -0
- package/test/provider/transform.test.ts +411 -0
- package/test/session/retry.test.ts +111 -0
- package/test/session/session.test.ts +71 -0
- package/test/skill/skill.test.ts +131 -0
- package/test/snapshot/snapshot.test.ts +939 -0
- package/test/tool/__snapshots__/tool.test.ts.snap +9 -0
- package/test/tool/bash.test.ts +434 -0
- package/test/tool/grep.test.ts +108 -0
- package/test/tool/patch.test.ts +259 -0
- package/test/tool/read.test.ts +42 -0
- package/test/util/iife.test.ts +36 -0
- package/test/util/lazy.test.ts +50 -0
- package/test/util/timeout.test.ts +21 -0
- package/test/util/wildcard.test.ts +55 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { realpathSync } from "fs"
|
|
2
|
+
import { exists } from "fs/promises"
|
|
3
|
+
import { dirname, join, relative } from "path"
|
|
4
|
+
|
|
5
|
+
export namespace Filesystem {
|
|
6
|
+
/**
|
|
7
|
+
* On Windows, normalize a path to its canonical casing using the filesystem.
|
|
8
|
+
* This is needed because Windows paths are case-insensitive but LSP servers
|
|
9
|
+
* may return paths with different casing than what we send them.
|
|
10
|
+
*/
|
|
11
|
+
export function normalizePath(p: string): string {
|
|
12
|
+
if (process.platform !== "win32") return p
|
|
13
|
+
try {
|
|
14
|
+
return realpathSync.native(p)
|
|
15
|
+
} catch {
|
|
16
|
+
return p
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function overlaps(a: string, b: string) {
|
|
20
|
+
const relA = relative(a, b)
|
|
21
|
+
const relB = relative(b, a)
|
|
22
|
+
return !relA || !relA.startsWith("..") || !relB || !relB.startsWith("..")
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function contains(parent: string, child: string) {
|
|
26
|
+
return !relative(parent, child).startsWith("..")
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function findUp(target: string, start: string, stop?: string) {
|
|
30
|
+
let current = start
|
|
31
|
+
const result = []
|
|
32
|
+
while (true) {
|
|
33
|
+
const search = join(current, target)
|
|
34
|
+
if (await exists(search)) result.push(search)
|
|
35
|
+
if (stop === current) break
|
|
36
|
+
const parent = dirname(current)
|
|
37
|
+
if (parent === current) break
|
|
38
|
+
current = parent
|
|
39
|
+
}
|
|
40
|
+
return result
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function* up(options: { targets: string[]; start: string; stop?: string }) {
|
|
44
|
+
const { targets, start, stop } = options
|
|
45
|
+
let current = start
|
|
46
|
+
while (true) {
|
|
47
|
+
for (const target of targets) {
|
|
48
|
+
const search = join(current, target)
|
|
49
|
+
if (await exists(search)) yield search
|
|
50
|
+
}
|
|
51
|
+
if (stop === current) break
|
|
52
|
+
const parent = dirname(current)
|
|
53
|
+
if (parent === current) break
|
|
54
|
+
current = parent
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function globUp(pattern: string, start: string, stop?: string) {
|
|
59
|
+
let current = start
|
|
60
|
+
const result = []
|
|
61
|
+
while (true) {
|
|
62
|
+
try {
|
|
63
|
+
const glob = new Bun.Glob(pattern)
|
|
64
|
+
for await (const match of glob.scan({
|
|
65
|
+
cwd: current,
|
|
66
|
+
absolute: true,
|
|
67
|
+
onlyFiles: true,
|
|
68
|
+
followSymlinks: true,
|
|
69
|
+
dot: true,
|
|
70
|
+
})) {
|
|
71
|
+
result.push(match)
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
// Skip invalid glob patterns
|
|
75
|
+
}
|
|
76
|
+
if (stop === current) break
|
|
77
|
+
const parent = dirname(current)
|
|
78
|
+
if (parent === current) break
|
|
79
|
+
current = parent
|
|
80
|
+
}
|
|
81
|
+
return result
|
|
82
|
+
}
|
|
83
|
+
}
|
package/src/util/fn.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { z } from "zod"
|
|
2
|
+
|
|
3
|
+
export function fn<T extends z.ZodType, Result>(schema: T, cb: (input: z.infer<T>) => Result) {
|
|
4
|
+
const result = (input: z.infer<T>) => {
|
|
5
|
+
const parsed = schema.parse(input)
|
|
6
|
+
return cb(parsed)
|
|
7
|
+
}
|
|
8
|
+
result.force = (input: z.infer<T>) => cb(input)
|
|
9
|
+
result.schema = schema
|
|
10
|
+
return result
|
|
11
|
+
}
|
package/src/util/iife.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { isDeepEqual } from "remeda"
|
|
2
|
+
import type { ParsedKey } from "@opentui/core"
|
|
3
|
+
|
|
4
|
+
export namespace Keybind {
|
|
5
|
+
/**
|
|
6
|
+
* Keybind info derived from OpenTUI's ParsedKey with our custom `leader` field.
|
|
7
|
+
* This ensures type compatibility and catches missing fields at compile time.
|
|
8
|
+
*/
|
|
9
|
+
export type Info = Pick<ParsedKey, "name" | "ctrl" | "meta" | "shift" | "super"> & {
|
|
10
|
+
leader: boolean // our custom field
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function match(a: Info, b: Info): boolean {
|
|
14
|
+
// Normalize super field (undefined and false are equivalent)
|
|
15
|
+
const normalizedA = { ...a, super: a.super ?? false }
|
|
16
|
+
const normalizedB = { ...b, super: b.super ?? false }
|
|
17
|
+
return isDeepEqual(normalizedA, normalizedB)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Convert OpenTUI's ParsedKey to our Keybind.Info format.
|
|
22
|
+
* This helper ensures all required fields are present and avoids manual object creation.
|
|
23
|
+
*/
|
|
24
|
+
export function fromParsedKey(key: ParsedKey, leader = false): Info {
|
|
25
|
+
return {
|
|
26
|
+
name: key.name,
|
|
27
|
+
ctrl: key.ctrl,
|
|
28
|
+
meta: key.meta,
|
|
29
|
+
shift: key.shift,
|
|
30
|
+
super: key.super ?? false,
|
|
31
|
+
leader,
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function toString(info: Info): string {
|
|
36
|
+
const parts: string[] = []
|
|
37
|
+
|
|
38
|
+
if (info.ctrl) parts.push("ctrl")
|
|
39
|
+
if (info.meta) parts.push("alt")
|
|
40
|
+
if (info.super) parts.push("super")
|
|
41
|
+
if (info.shift) parts.push("shift")
|
|
42
|
+
if (info.name) {
|
|
43
|
+
if (info.name === "delete") parts.push("del")
|
|
44
|
+
else parts.push(info.name)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let result = parts.join("+")
|
|
48
|
+
|
|
49
|
+
if (info.leader) {
|
|
50
|
+
result = result ? `<leader> ${result}` : `<leader>`
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return result
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function parse(key: string): Info[] {
|
|
57
|
+
if (key === "none") return []
|
|
58
|
+
|
|
59
|
+
return key.split(",").map((combo) => {
|
|
60
|
+
// Handle <leader> syntax by replacing with leader+
|
|
61
|
+
const normalized = combo.replace(/<leader>/g, "leader+")
|
|
62
|
+
const parts = normalized.toLowerCase().split("+")
|
|
63
|
+
const info: Info = {
|
|
64
|
+
ctrl: false,
|
|
65
|
+
meta: false,
|
|
66
|
+
shift: false,
|
|
67
|
+
leader: false,
|
|
68
|
+
name: "",
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
for (const part of parts) {
|
|
72
|
+
switch (part) {
|
|
73
|
+
case "ctrl":
|
|
74
|
+
info.ctrl = true
|
|
75
|
+
break
|
|
76
|
+
case "alt":
|
|
77
|
+
case "meta":
|
|
78
|
+
case "option":
|
|
79
|
+
info.meta = true
|
|
80
|
+
break
|
|
81
|
+
case "super":
|
|
82
|
+
info.super = true
|
|
83
|
+
break
|
|
84
|
+
case "shift":
|
|
85
|
+
info.shift = true
|
|
86
|
+
break
|
|
87
|
+
case "leader":
|
|
88
|
+
info.leader = true
|
|
89
|
+
break
|
|
90
|
+
case "esc":
|
|
91
|
+
info.name = "escape"
|
|
92
|
+
break
|
|
93
|
+
default:
|
|
94
|
+
info.name = part
|
|
95
|
+
break
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return info
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
}
|
package/src/util/lazy.ts
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import path from "path"
|
|
2
|
+
import os from "os"
|
|
3
|
+
import fs from "fs"
|
|
4
|
+
import crypto from "crypto"
|
|
5
|
+
import { execSync } from "child_process"
|
|
6
|
+
import { Log } from "./log"
|
|
7
|
+
|
|
8
|
+
const LICENSE_CACHE_PATH = path.join(os.homedir(), ".rird", "license_cache.json")
|
|
9
|
+
const LICENSE_KEY_PATH = path.join(os.homedir(), ".rird", "license.key")
|
|
10
|
+
const RIRD_API_URL = "https://rird.ai/api/desktop/validate-license"
|
|
11
|
+
const CACHE_VALIDITY_HOURS = 24
|
|
12
|
+
|
|
13
|
+
export interface LicenseCheckResult {
|
|
14
|
+
valid: boolean
|
|
15
|
+
message: string
|
|
16
|
+
user?: any
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get primary MAC address from non-internal network interfaces.
|
|
21
|
+
* Returns the first non-loopback, non-internal MAC address found.
|
|
22
|
+
*/
|
|
23
|
+
function getPrimaryMacAddress(): string {
|
|
24
|
+
try {
|
|
25
|
+
const interfaces = os.networkInterfaces()
|
|
26
|
+
for (const [name, addrs] of Object.entries(interfaces)) {
|
|
27
|
+
if (!addrs) continue
|
|
28
|
+
// Skip virtual interfaces that are commonly spoofed
|
|
29
|
+
if (name.toLowerCase().includes("virtual") || name.toLowerCase().includes("veth")) continue
|
|
30
|
+
for (const addr of addrs) {
|
|
31
|
+
// Get non-internal interface with valid MAC
|
|
32
|
+
if (!addr.internal && addr.mac && addr.mac !== "00:00:00:00:00:00") {
|
|
33
|
+
return addr.mac
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
// Fallback silently
|
|
39
|
+
}
|
|
40
|
+
return ""
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get machine ID from system-specific locations.
|
|
45
|
+
* This is a persistent identifier that survives reboots but not OS reinstalls.
|
|
46
|
+
*/
|
|
47
|
+
function getMachineId(): string {
|
|
48
|
+
const platform = os.platform()
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
if (platform === "linux") {
|
|
52
|
+
// Primary: /etc/machine-id (systemd standard)
|
|
53
|
+
if (fs.existsSync("/etc/machine-id")) {
|
|
54
|
+
return fs.readFileSync("/etc/machine-id", "utf-8").trim()
|
|
55
|
+
}
|
|
56
|
+
// Fallback: /var/lib/dbus/machine-id
|
|
57
|
+
if (fs.existsSync("/var/lib/dbus/machine-id")) {
|
|
58
|
+
return fs.readFileSync("/var/lib/dbus/machine-id", "utf-8").trim()
|
|
59
|
+
}
|
|
60
|
+
} else if (platform === "darwin") {
|
|
61
|
+
// macOS: IOPlatformUUID from ioreg
|
|
62
|
+
const output = execSync("ioreg -rd1 -c IOPlatformExpertDevice | grep IOPlatformUUID", {
|
|
63
|
+
encoding: "utf-8",
|
|
64
|
+
timeout: 5000,
|
|
65
|
+
})
|
|
66
|
+
const match = output.match(/"IOPlatformUUID"\s*=\s*"([^"]+)"/)
|
|
67
|
+
if (match) {
|
|
68
|
+
return match[1]
|
|
69
|
+
}
|
|
70
|
+
} else if (platform === "win32") {
|
|
71
|
+
// Windows: MachineGuid from registry
|
|
72
|
+
const output = execSync(
|
|
73
|
+
'reg query "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography" /v MachineGuid',
|
|
74
|
+
{ encoding: "utf-8", timeout: 5000 }
|
|
75
|
+
)
|
|
76
|
+
const match = output.match(/MachineGuid\s+REG_SZ\s+([^\s\r\n]+)/)
|
|
77
|
+
if (match) {
|
|
78
|
+
return match[1]
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
// Fallback silently - machine ID is optional but strengthens fingerprint
|
|
83
|
+
}
|
|
84
|
+
return ""
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get CPU model string for hardware binding.
|
|
89
|
+
*/
|
|
90
|
+
function getCpuModel(): string {
|
|
91
|
+
try {
|
|
92
|
+
const cpus = os.cpus()
|
|
93
|
+
if (cpus.length > 0) {
|
|
94
|
+
return cpus[0].model
|
|
95
|
+
}
|
|
96
|
+
} catch {
|
|
97
|
+
// Fallback silently
|
|
98
|
+
}
|
|
99
|
+
return ""
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get disk serial number (primary disk) for hardware binding.
|
|
104
|
+
* This is one of the hardest identifiers to spoof.
|
|
105
|
+
*/
|
|
106
|
+
function getDiskSerial(): string {
|
|
107
|
+
const platform = os.platform()
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
if (platform === "linux") {
|
|
111
|
+
// Try to get the serial of the root disk
|
|
112
|
+
// First, find the root device
|
|
113
|
+
const rootDev = execSync("df / | tail -1 | awk '{print $1}'", {
|
|
114
|
+
encoding: "utf-8",
|
|
115
|
+
timeout: 5000,
|
|
116
|
+
}).trim()
|
|
117
|
+
// Extract base device name (e.g., /dev/sda1 -> sda)
|
|
118
|
+
const devName = rootDev.replace(/\/dev\//, "").replace(/[0-9]+$/, "")
|
|
119
|
+
// Get serial from udevadm
|
|
120
|
+
if (devName) {
|
|
121
|
+
const serial = execSync(`udevadm info --query=property --name=/dev/${devName} | grep ID_SERIAL_SHORT`, {
|
|
122
|
+
encoding: "utf-8",
|
|
123
|
+
timeout: 5000,
|
|
124
|
+
})
|
|
125
|
+
const match = serial.match(/ID_SERIAL_SHORT=(.+)/)
|
|
126
|
+
if (match) {
|
|
127
|
+
return match[1].trim()
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
} else if (platform === "darwin") {
|
|
131
|
+
// macOS: Use diskutil to get disk serial
|
|
132
|
+
const output = execSync("diskutil info disk0 | grep 'Device Identifier\\|Serial'", {
|
|
133
|
+
encoding: "utf-8",
|
|
134
|
+
timeout: 5000,
|
|
135
|
+
})
|
|
136
|
+
const match = output.match(/Serial Number:\s+(\S+)/i)
|
|
137
|
+
if (match) {
|
|
138
|
+
return match[1]
|
|
139
|
+
}
|
|
140
|
+
} else if (platform === "win32") {
|
|
141
|
+
// Windows: Use WMIC to get disk serial
|
|
142
|
+
const output = execSync("wmic diskdrive get SerialNumber", {
|
|
143
|
+
encoding: "utf-8",
|
|
144
|
+
timeout: 5000,
|
|
145
|
+
})
|
|
146
|
+
const lines = output.split("\n").filter((l) => l.trim() && !l.includes("SerialNumber"))
|
|
147
|
+
if (lines.length > 0) {
|
|
148
|
+
return lines[0].trim()
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
} catch {
|
|
152
|
+
// Disk serial is optional - many VMs don't have it
|
|
153
|
+
}
|
|
154
|
+
return ""
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Get a hardware-bound device fingerprint that is difficult to spoof.
|
|
159
|
+
* Combines multiple hardware identifiers for robust device identification.
|
|
160
|
+
*
|
|
161
|
+
* Components (in order of uniqueness/difficulty to spoof):
|
|
162
|
+
* 1. Machine ID - System-generated UUID, persistent across reboots
|
|
163
|
+
* 2. Primary MAC address - Network interface hardware address
|
|
164
|
+
* 3. Disk serial - Hard drive serial number (hardest to spoof)
|
|
165
|
+
* 4. CPU model - Processor identification string
|
|
166
|
+
* 5. Total memory - System RAM in bytes
|
|
167
|
+
* 6. CPU core count - Number of logical processors
|
|
168
|
+
* 7. Platform + arch - OS and architecture (least unique, but stable)
|
|
169
|
+
*
|
|
170
|
+
* The fingerprint is a SHA-256 hash of all components, making it:
|
|
171
|
+
* - Deterministic: Same hardware always produces same fingerprint
|
|
172
|
+
* - Secure: Cannot reverse-engineer the original values
|
|
173
|
+
* - Tamper-resistant: Changing any component changes the fingerprint
|
|
174
|
+
*/
|
|
175
|
+
function getDeviceFingerprint(): string {
|
|
176
|
+
const parts: string[] = []
|
|
177
|
+
|
|
178
|
+
// Hardware identifiers (hard to spoof)
|
|
179
|
+
const machineId = getMachineId()
|
|
180
|
+
const macAddress = getPrimaryMacAddress()
|
|
181
|
+
const diskSerial = getDiskSerial()
|
|
182
|
+
const cpuModel = getCpuModel()
|
|
183
|
+
|
|
184
|
+
// System characteristics
|
|
185
|
+
const totalMemory = os.totalmem().toString()
|
|
186
|
+
const cpuCount = os.cpus().length.toString()
|
|
187
|
+
const platform = os.platform()
|
|
188
|
+
const arch = os.arch()
|
|
189
|
+
|
|
190
|
+
// Build fingerprint from all available components
|
|
191
|
+
// Order matters - keep it consistent
|
|
192
|
+
parts.push(`machine:${machineId}`)
|
|
193
|
+
parts.push(`mac:${macAddress}`)
|
|
194
|
+
parts.push(`disk:${diskSerial}`)
|
|
195
|
+
parts.push(`cpu:${cpuModel}`)
|
|
196
|
+
parts.push(`mem:${totalMemory}`)
|
|
197
|
+
parts.push(`cores:${cpuCount}`)
|
|
198
|
+
parts.push(`platform:${platform}`)
|
|
199
|
+
parts.push(`arch:${arch}`)
|
|
200
|
+
|
|
201
|
+
// Create SHA-256 hash of combined components
|
|
202
|
+
const combined = parts.join("|")
|
|
203
|
+
const hash = crypto.createHash("sha256").update(combined).digest("hex")
|
|
204
|
+
|
|
205
|
+
// Return first 32 chars for reasonable length while maintaining security
|
|
206
|
+
return hash.substring(0, 32)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function getLicenseKey(): string | null {
|
|
210
|
+
if (process.env.RIRD_LICENSE_KEY) {
|
|
211
|
+
return process.env.RIRD_LICENSE_KEY.trim()
|
|
212
|
+
}
|
|
213
|
+
if (fs.existsSync(LICENSE_KEY_PATH)) {
|
|
214
|
+
try {
|
|
215
|
+
return fs.readFileSync(LICENSE_KEY_PATH, "utf-8").trim()
|
|
216
|
+
} catch {
|
|
217
|
+
return null
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return null
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function loadLicenseCache(): any | null {
|
|
224
|
+
try {
|
|
225
|
+
if (fs.existsSync(LICENSE_CACHE_PATH)) {
|
|
226
|
+
const data = JSON.parse(fs.readFileSync(LICENSE_CACHE_PATH, "utf-8"))
|
|
227
|
+
const cachedAt = new Date(data.cached_at)
|
|
228
|
+
const now = new Date()
|
|
229
|
+
const hoursDiff = (now.getTime() - cachedAt.getTime()) / (1000 * 60 * 60)
|
|
230
|
+
if (hoursDiff < CACHE_VALIDITY_HOURS) {
|
|
231
|
+
return data
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
} catch (e) {
|
|
235
|
+
// Ignore cache errors
|
|
236
|
+
}
|
|
237
|
+
return null
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function saveLicenseCache(data: any) {
|
|
241
|
+
try {
|
|
242
|
+
const dir = path.dirname(LICENSE_CACHE_PATH)
|
|
243
|
+
if (!fs.existsSync(dir)) {
|
|
244
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 })
|
|
245
|
+
}
|
|
246
|
+
data.cached_at = new Date().toISOString()
|
|
247
|
+
fs.writeFileSync(LICENSE_CACHE_PATH, JSON.stringify(data), { mode: 0o600 })
|
|
248
|
+
} catch (e) {
|
|
249
|
+
Log.Default.error("license", { error: "Failed to save license cache", details: e })
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export async function validateLicense(): Promise<LicenseCheckResult> {
|
|
254
|
+
const licenseKey = getLicenseKey()
|
|
255
|
+
if (!licenseKey) {
|
|
256
|
+
return {
|
|
257
|
+
valid: false,
|
|
258
|
+
message: "No license key found.\nGet your key at: https://rird.ai\nThen run: rird activate YOUR_LICENSE_KEY",
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Check cache first
|
|
263
|
+
const cached = loadLicenseCache()
|
|
264
|
+
if (cached && cached.license_key === licenseKey && cached.valid) {
|
|
265
|
+
Log.Default.debug("Using cached license validation")
|
|
266
|
+
return {
|
|
267
|
+
valid: true,
|
|
268
|
+
message: `License valid (cached) - ${cached.email || "unknown"}`,
|
|
269
|
+
user: { email: cached.email, plan: cached.plan },
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Validate online
|
|
274
|
+
try {
|
|
275
|
+
const response = await fetch(RIRD_API_URL, {
|
|
276
|
+
method: "POST",
|
|
277
|
+
headers: { "Content-Type": "application/json" },
|
|
278
|
+
body: JSON.stringify({
|
|
279
|
+
license_key: licenseKey,
|
|
280
|
+
device_fingerprint: getDeviceFingerprint(),
|
|
281
|
+
version: "1.0.0", // TODO: Get from package.json
|
|
282
|
+
platform: os.platform(),
|
|
283
|
+
}),
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
if (response.status === 200) {
|
|
287
|
+
const data = await response.json()
|
|
288
|
+
if (data.valid) {
|
|
289
|
+
saveLicenseCache({
|
|
290
|
+
license_key: licenseKey,
|
|
291
|
+
valid: true,
|
|
292
|
+
email: data.user?.email,
|
|
293
|
+
plan: data.user?.plan,
|
|
294
|
+
expires_at: data.user?.expires_at,
|
|
295
|
+
})
|
|
296
|
+
return {
|
|
297
|
+
valid: true,
|
|
298
|
+
message: `License valid - ${data.user?.email || "unknown"}`,
|
|
299
|
+
user: data.user,
|
|
300
|
+
}
|
|
301
|
+
} else {
|
|
302
|
+
// Clear bad cache
|
|
303
|
+
if (fs.existsSync(LICENSE_CACHE_PATH)) fs.unlinkSync(LICENSE_CACHE_PATH)
|
|
304
|
+
return { valid: false, message: data.error || "Invalid license" }
|
|
305
|
+
}
|
|
306
|
+
} else if (response.status === 401) {
|
|
307
|
+
return { valid: false, message: "Invalid or expired license key" }
|
|
308
|
+
} else if (response.status === 403) {
|
|
309
|
+
return { valid: false, message: "Subscription inactive - please renew at rird.ai" }
|
|
310
|
+
} else if (response.status === 429) {
|
|
311
|
+
return { valid: false, message: "Too many validation attempts - please wait" }
|
|
312
|
+
} else {
|
|
313
|
+
// Offline/Server error - allow if previously cached (but here we don't have valid cache)
|
|
314
|
+
// If we are here, cache was invalid or missing.
|
|
315
|
+
// Strict mode: fail. Lenient mode: warn.
|
|
316
|
+
// User requested "rate limits connected to auth", implying enforcement.
|
|
317
|
+
return { valid: false, message: `License server error: ${response.status}` }
|
|
318
|
+
}
|
|
319
|
+
} catch (e) {
|
|
320
|
+
// Network error
|
|
321
|
+
Log.Default.warn("License check offline")
|
|
322
|
+
return { valid: false, message: "Could not reach license server" } // Fail safe or fail secure?
|
|
323
|
+
// Given the requirement, probably fail secure unless cached.
|
|
324
|
+
}
|
|
325
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export namespace Locale {
|
|
2
|
+
export function titlecase(str: string) {
|
|
3
|
+
return str.replace(/\b\w/g, (c) => c.toUpperCase())
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function time(input: number): string {
|
|
7
|
+
const date = new Date(input)
|
|
8
|
+
return date.toLocaleTimeString(undefined, { timeStyle: "short" })
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function datetime(input: number): string {
|
|
12
|
+
const date = new Date(input)
|
|
13
|
+
const localTime = time(input)
|
|
14
|
+
const localDate = date.toLocaleDateString()
|
|
15
|
+
return `${localTime} · ${localDate}`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function todayTimeOrDateTime(input: number): string {
|
|
19
|
+
const date = new Date(input)
|
|
20
|
+
const now = new Date()
|
|
21
|
+
const isToday =
|
|
22
|
+
date.getFullYear() === now.getFullYear() && date.getMonth() === now.getMonth() && date.getDate() === now.getDate()
|
|
23
|
+
|
|
24
|
+
if (isToday) {
|
|
25
|
+
return time(input)
|
|
26
|
+
} else {
|
|
27
|
+
return datetime(input)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function number(num: number): string {
|
|
32
|
+
if (num >= 1000000) {
|
|
33
|
+
return (num / 1000000).toFixed(1) + "M"
|
|
34
|
+
} else if (num >= 1000) {
|
|
35
|
+
return (num / 1000).toFixed(1) + "K"
|
|
36
|
+
}
|
|
37
|
+
return num.toString()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function duration(input: number) {
|
|
41
|
+
if (input < 1000) {
|
|
42
|
+
return `${input}ms`
|
|
43
|
+
}
|
|
44
|
+
if (input < 60000) {
|
|
45
|
+
return `${(input / 1000).toFixed(1)}s`
|
|
46
|
+
}
|
|
47
|
+
if (input < 3600000) {
|
|
48
|
+
const minutes = Math.floor(input / 60000)
|
|
49
|
+
const seconds = Math.floor((input % 60000) / 1000)
|
|
50
|
+
return `${minutes}m ${seconds}s`
|
|
51
|
+
}
|
|
52
|
+
if (input < 86400000) {
|
|
53
|
+
const hours = Math.floor(input / 3600000)
|
|
54
|
+
const minutes = Math.floor((input % 3600000) / 60000)
|
|
55
|
+
return `${hours}h ${minutes}m`
|
|
56
|
+
}
|
|
57
|
+
const hours = Math.floor(input / 3600000)
|
|
58
|
+
const days = Math.floor((input % 3600000) / 86400000)
|
|
59
|
+
return `${days}d ${hours}h`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function truncate(str: string, len: number): string {
|
|
63
|
+
if (str.length <= len) return str
|
|
64
|
+
return str.slice(0, len - 1) + "…"
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function truncateMiddle(str: string, maxLength: number = 35): string {
|
|
68
|
+
if (str.length <= maxLength) return str
|
|
69
|
+
|
|
70
|
+
const ellipsis = "…"
|
|
71
|
+
const keepStart = Math.ceil((maxLength - ellipsis.length) / 2)
|
|
72
|
+
const keepEnd = Math.floor((maxLength - ellipsis.length) / 2)
|
|
73
|
+
|
|
74
|
+
return str.slice(0, keepStart) + ellipsis + str.slice(-keepEnd)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function pluralize(count: number, singular: string, plural: string): string {
|
|
78
|
+
const template = count === 1 ? singular : plural
|
|
79
|
+
return template.replace("{}", count.toString())
|
|
80
|
+
}
|
|
81
|
+
}
|