@wingsbutterfly/dsh-rtk 0.1.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.
Files changed (94) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/LICENSE +21 -0
  3. package/README.md +214 -0
  4. package/README.zh.md +211 -0
  5. package/THIRD_PARTY_NOTICES.md +51 -0
  6. package/docs/assets/how-it-works.svg +47 -0
  7. package/docs/verification.md +279 -0
  8. package/lib/command.d.ts +41 -0
  9. package/lib/command.d.ts.map +1 -0
  10. package/lib/command.js +137 -0
  11. package/lib/command.js.map +1 -0
  12. package/lib/compact/build.d.ts +15 -0
  13. package/lib/compact/build.d.ts.map +1 -0
  14. package/lib/compact/build.js +140 -0
  15. package/lib/compact/build.js.map +1 -0
  16. package/lib/compact/detect.d.ts +22 -0
  17. package/lib/compact/detect.d.ts.map +1 -0
  18. package/lib/compact/detect.js +54 -0
  19. package/lib/compact/detect.js.map +1 -0
  20. package/lib/compact/dsh-result.d.ts +49 -0
  21. package/lib/compact/dsh-result.d.ts.map +1 -0
  22. package/lib/compact/dsh-result.js +84 -0
  23. package/lib/compact/dsh-result.js.map +1 -0
  24. package/lib/compact/git.d.ts +21 -0
  25. package/lib/compact/git.d.ts.map +1 -0
  26. package/lib/compact/git.js +197 -0
  27. package/lib/compact/git.js.map +1 -0
  28. package/lib/compact/index.d.ts +39 -0
  29. package/lib/compact/index.d.ts.map +1 -0
  30. package/lib/compact/index.js +236 -0
  31. package/lib/compact/index.js.map +1 -0
  32. package/lib/compact/linter.d.ts +12 -0
  33. package/lib/compact/linter.d.ts.map +1 -0
  34. package/lib/compact/linter.js +118 -0
  35. package/lib/compact/linter.js.map +1 -0
  36. package/lib/compact/search.d.ts +16 -0
  37. package/lib/compact/search.d.ts.map +1 -0
  38. package/lib/compact/search.js +67 -0
  39. package/lib/compact/search.js.map +1 -0
  40. package/lib/compact/source.d.ts +22 -0
  41. package/lib/compact/source.d.ts.map +1 -0
  42. package/lib/compact/source.js +224 -0
  43. package/lib/compact/source.js.map +1 -0
  44. package/lib/compact/test-output.d.ts +12 -0
  45. package/lib/compact/test-output.d.ts.map +1 -0
  46. package/lib/compact/test-output.js +168 -0
  47. package/lib/compact/test-output.js.map +1 -0
  48. package/lib/compact/text.d.ts +22 -0
  49. package/lib/compact/text.d.ts.map +1 -0
  50. package/lib/compact/text.js +87 -0
  51. package/lib/compact/text.js.map +1 -0
  52. package/lib/config.d.ts +243 -0
  53. package/lib/config.d.ts.map +1 -0
  54. package/lib/config.js +183 -0
  55. package/lib/config.js.map +1 -0
  56. package/lib/index.d.ts +46 -0
  57. package/lib/index.d.ts.map +1 -0
  58. package/lib/index.js +405 -0
  59. package/lib/index.js.map +1 -0
  60. package/lib/metrics.d.ts +35 -0
  61. package/lib/metrics.d.ts.map +1 -0
  62. package/lib/metrics.js +51 -0
  63. package/lib/metrics.js.map +1 -0
  64. package/lib/rtk-executable.d.ts +51 -0
  65. package/lib/rtk-executable.d.ts.map +1 -0
  66. package/lib/rtk-executable.js +75 -0
  67. package/lib/rtk-executable.js.map +1 -0
  68. package/lib/rtk-rewrite.d.ts +88 -0
  69. package/lib/rtk-rewrite.d.ts.map +1 -0
  70. package/lib/rtk-rewrite.js +150 -0
  71. package/lib/rtk-rewrite.js.map +1 -0
  72. package/lib/runtime-guard.d.ts +31 -0
  73. package/lib/runtime-guard.d.ts.map +1 -0
  74. package/lib/runtime-guard.js +32 -0
  75. package/lib/runtime-guard.js.map +1 -0
  76. package/package.json +82 -0
  77. package/scripts/link-dsh.mjs +135 -0
  78. package/src/command.ts +174 -0
  79. package/src/compact/build.ts +154 -0
  80. package/src/compact/detect.ts +54 -0
  81. package/src/compact/dsh-result.ts +99 -0
  82. package/src/compact/git.ts +209 -0
  83. package/src/compact/index.ts +284 -0
  84. package/src/compact/linter.ts +126 -0
  85. package/src/compact/search.ts +73 -0
  86. package/src/compact/source.ts +244 -0
  87. package/src/compact/test-output.ts +184 -0
  88. package/src/compact/text.ts +86 -0
  89. package/src/config.ts +263 -0
  90. package/src/index.ts +431 -0
  91. package/src/metrics.ts +84 -0
  92. package/src/rtk-executable.ts +117 -0
  93. package/src/rtk-rewrite.ts +179 -0
  94. package/src/runtime-guard.ts +44 -0
@@ -0,0 +1,179 @@
1
+ /** One `rtk rewrite` outcome. */
2
+ export interface RtkRewriteResult {
3
+ changed: boolean
4
+ originalCommand: string
5
+ rewrittenCommand: string
6
+ exitCode: number
7
+ error?: string
8
+ }
9
+
10
+ /** Injected runner so tests never spawn a process. */
11
+ export type RtkRunner = (
12
+ command: string,
13
+ args: readonly string[],
14
+ options?: { timeoutMs?: number; signal?: AbortSignal },
15
+ ) => Promise<{ code: number; stdout: string; stderr: string }>
16
+
17
+ /** Options for {@link resolveRtkRewrite}. */
18
+ export interface RtkRewriteOptions {
19
+ runner: RtkRunner
20
+ executable: string
21
+ timeoutMs?: number
22
+ signal?: AbortSignal
23
+ }
24
+
25
+ const ENV_ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/
26
+
27
+ /**
28
+ * Split leading `KEY=VALUE` assignments off a command line.
29
+ *
30
+ * Shell treats a run of assignments before the command word as that command's
31
+ * environment, so `FOO=1 ls` invokes `ls`, not `FOO=1`. Detection must look at
32
+ * the command word; passing the whole line would classify `FOO=1 git status`
33
+ * as unsupported.
34
+ *
35
+ * @param command - raw command line.
36
+ * @returns the assignment prefix (verbatim, including trailing space) and the command word onward.
37
+ */
38
+ export function splitLeadingEnvAssignments(command: string): { envPrefix: string; command: string } {
39
+ let index = 0
40
+ let prefixEnd = 0
41
+
42
+ while (index < command.length) {
43
+ const rest = command.slice(index)
44
+ const match = ENV_ASSIGNMENT.exec(rest)
45
+ if (match === null) break
46
+
47
+ let cursor = index + match[0].length
48
+ if (cursor >= command.length) return { envPrefix: '', command }
49
+
50
+ const first = command[cursor]
51
+ if (first === '"' || first === "'") {
52
+ cursor += 1
53
+ while (cursor < command.length && command[cursor] !== first) cursor += 1
54
+ if (cursor >= command.length) return { envPrefix: '', command }
55
+ cursor += 1
56
+ } else {
57
+ while (cursor < command.length && !/\s/.test(command[cursor] as string)) cursor += 1
58
+ }
59
+
60
+ if (cursor < command.length && !/\s/.test(command[cursor] as string)) return { envPrefix: '', command }
61
+ while (cursor < command.length && /\s/.test(command[cursor] as string)) cursor += 1
62
+ prefixEnd = cursor
63
+ index = cursor
64
+ }
65
+
66
+ return { envPrefix: command.slice(0, prefixEnd), command: command.slice(prefixEnd) }
67
+ }
68
+
69
+ /**
70
+ * Whether a command (ignoring a leading assignment run) already invokes rtk.
71
+ *
72
+ * Rewriting an rtk command would produce `rtk rtk …`, and the guard also saves
73
+ * a process spawn on every already-optimized call.
74
+ */
75
+ export function isAlreadyRtkCommand(command: string): boolean {
76
+ const effective = splitLeadingEnvAssignments(command.trimStart()).command.trimStart()
77
+ return effective === 'rtk' || effective.startsWith('rtk ')
78
+ }
79
+
80
+ const RTK_HISTORY_ENV = 'RTK_DB_PATH'
81
+
82
+ /** Which shell dialect an assignment prefix must be written in. */
83
+ export type ShellKind = 'posix' | 'powershell'
84
+
85
+ /// Matches a PowerShell environment assignment already present at the head.
86
+ const PWSH_ASSIGNMENT = /^\$env:RTK_DB_PATH\s*=/
87
+
88
+ /**
89
+ * Prefix a command with an isolated `RTK_DB_PATH` so rtk's usage history lands
90
+ * in a scratch directory instead of the working tree.
91
+ *
92
+ * The two shells need different syntax: `export NAME='value'` is a syntax error
93
+ * in PowerShell, which spells the same thing `$env:NAME = 'value'`. Both the
94
+ * harness's `pwsh` tool and its `bash` tool are rewrite targets, so the caller
95
+ * passes the dialect rather than assuming one.
96
+ *
97
+ * The assignment is skipped when the command already sets it or the ambient
98
+ * environment provides one, so an explicit choice is never overridden.
99
+ *
100
+ * @param command - the rewritten command.
101
+ * @param historyDbPath - absolute path for this deployment's rtk history database.
102
+ * @param ambientValue - value currently in the environment, if any.
103
+ * @param shell - the dialect of the command being prefixed.
104
+ * @returns the command to run, prefixed only when isolation applies.
105
+ */
106
+ export function applyRtkHistoryScope(
107
+ command: string,
108
+ historyDbPath: string,
109
+ ambientValue: string | undefined,
110
+ shell: ShellKind = 'posix',
111
+ ): string {
112
+ if (!command.trim() || !historyDbPath.trim()) return command
113
+ if (ambientValue !== undefined && ambientValue.trim()) return command
114
+
115
+ const trimmed = command.trimStart()
116
+
117
+ if (shell === 'powershell') {
118
+ if (PWSH_ASSIGNMENT.test(trimmed)) return command
119
+ // PowerShell escapes a literal quote by doubling it.
120
+ return `$env:${RTK_HISTORY_ENV} = '${historyDbPath.replace(/'/g, "''")}'; ${command}`
121
+ }
122
+
123
+ if (splitLeadingEnvAssignments(trimmed).envPrefix.includes(`${RTK_HISTORY_ENV}=`)) return command
124
+ return `export ${RTK_HISTORY_ENV}='${historyDbPath.replace(/'/g, "'\\''")}'; ${command}`
125
+ }
126
+
127
+ /**
128
+ * Ask the installed rtk binary how it would rewrite a command.
129
+ *
130
+ * rtk owns the rewrite rules; this plugin deliberately carries no duplicate
131
+ * table. The exit-code contract is rtk's:
132
+ *
133
+ * | code | meaning |
134
+ * |------|---------|
135
+ * | 0, 3 | stdout is the rewritten command |
136
+ * | 1 | no rtk equivalent — run the command unchanged |
137
+ * | 2 | rtk refused the rewrite; stderr explains why |
138
+ *
139
+ * A non-zero code is an ordinary outcome, never a rejection: the caller always
140
+ * has the original command to fall back on.
141
+ *
142
+ * @param command - the raw command the model asked to run.
143
+ * @param options - runner, executable, deadline, and caller cancellation.
144
+ * @returns the decision, including the fallback command when nothing changed.
145
+ */
146
+ export async function resolveRtkRewrite(command: string, options: RtkRewriteOptions): Promise<RtkRewriteResult> {
147
+ const unchanged = (exitCode: number, error?: string): RtkRewriteResult => ({
148
+ changed: false,
149
+ originalCommand: command,
150
+ rewrittenCommand: command,
151
+ exitCode,
152
+ ...(error === undefined ? {} : { error }),
153
+ })
154
+
155
+ if (!command || !command.trim()) return unchanged(1)
156
+ if (isAlreadyRtkCommand(command)) return unchanged(1)
157
+
158
+ let result: { code: number; stdout: string; stderr: string }
159
+ try {
160
+ const runOptions: { timeoutMs?: number; signal?: AbortSignal } = {}
161
+ if (options.timeoutMs !== undefined) runOptions.timeoutMs = options.timeoutMs
162
+ if (options.signal !== undefined) runOptions.signal = options.signal
163
+ result = await options.runner(options.executable, ['rewrite', command], runOptions)
164
+ } catch (error) {
165
+ return unchanged(-1, error instanceof Error ? error.message : String(error))
166
+ }
167
+
168
+ if (result.code === 1) return unchanged(1)
169
+ if (result.code === 2) return unchanged(2, result.stderr.trim() || 'rtk refused the rewrite')
170
+
171
+ if (result.code === 0 || result.code === 3) {
172
+ const rewritten = result.stdout.trim()
173
+ if (!rewritten) return unchanged(result.code, 'rtk returned empty output')
174
+ if (rewritten === command) return unchanged(result.code)
175
+ return { changed: true, originalCommand: command, rewrittenCommand: rewritten, exitCode: result.code }
176
+ }
177
+
178
+ return unchanged(result.code, `unexpected rtk exit code ${result.code}`)
179
+ }
@@ -0,0 +1,44 @@
1
+ import type { RtkConfig } from './config.js'
2
+
3
+ /** What the last probe found. */
4
+ export interface RtkRuntimeStatus {
5
+ rtkAvailable: boolean
6
+ lastCheckedAt?: number
7
+ rtkExecutablePath?: string
8
+ rtkExecutableCommand?: string
9
+ rtkExecutableResolver?: string
10
+ rtkExecutableResolutionWarning?: string
11
+ lastError?: string
12
+ }
13
+
14
+ /** How long a probe result stays authoritative before it is refreshed. */
15
+ export const RUNTIME_STATUS_TTL_MS = 30_000
16
+
17
+ /**
18
+ * Whether a missing rtk binary must be established before handling a command.
19
+ *
20
+ * Only rewrite mode consults availability: `suggest` mode reports what rtk
21
+ * *would* do and never gates execution on it, and compaction is a pure text
22
+ * transform that needs no binary at all.
23
+ */
24
+ export function shouldRequireRtkAvailability(config: RtkConfig): boolean {
25
+ return config.enabled && config.mode === 'rewrite'
26
+ }
27
+
28
+ /**
29
+ * Whether command rewriting must stand down for this call.
30
+ *
31
+ * With `guardWhenRtkMissing` on, a call that cannot prove rtk is available
32
+ * runs unchanged — the user's command is never blocked by an absent optimizer.
33
+ */
34
+ export function shouldSkipRewrite(config: RtkConfig, status: RtkRuntimeStatus): boolean {
35
+ if (!config.enabled) return true
36
+ if (!config.guardWhenRtkMissing) return false
37
+ return !status.rtkAvailable
38
+ }
39
+
40
+ /** Whether the cached probe is stale and must be refreshed before use. */
41
+ export function isStatusStale(status: RtkRuntimeStatus, now: number, ttlMs = RUNTIME_STATUS_TTL_MS): boolean {
42
+ if (status.lastCheckedAt === undefined) return true
43
+ return now - status.lastCheckedAt > ttlMs
44
+ }