@carter-mcalister/pi-mise-toolchain 0.6.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 +182 -0
- package/package.json +76 -0
- package/src/blockers/index.ts +23 -0
- package/src/blockers/package-manager.ts +62 -0
- package/src/blockers/python.ts +61 -0
- package/src/commands/settings-command.ts +72 -0
- package/src/config.ts +325 -0
- package/src/hooks/bash-integration.ts +35 -0
- package/src/hooks/rewrite-notifications.ts +42 -0
- package/src/hooks/session-start.ts +10 -0
- package/src/index.ts +47 -0
- package/src/project-config.ts +98 -0
- package/src/rewriters/git-rebase.ts +75 -0
- package/src/rewriters/index.ts +49 -0
- package/src/rewriters/package-manager.ts +152 -0
- package/src/rewriters/python.ts +130 -0
- package/src/rewriters/types.ts +12 -0
- package/src/toolchain.test.ts +322 -0
- package/src/utils/bash-composition.ts +28 -0
- package/src/utils/bash-source-mode.ts +5 -0
- package/src/utils/migration.ts +100 -0
- package/src/utils/shell-utils.ts +245 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config migration for the toolchain extension.
|
|
3
|
+
*
|
|
4
|
+
* Uses a version field to gate migrations, following the same pattern as
|
|
5
|
+
* pi-guardrails. Configs without a version field are considered v0 (legacy).
|
|
6
|
+
*
|
|
7
|
+
* v0 -> current:
|
|
8
|
+
* - features.enforcePackageManager: boolean -> FeatureMode
|
|
9
|
+
* - features.rewritePython: boolean -> FeatureMode
|
|
10
|
+
* - features.gitRebaseEditor: boolean -> FeatureMode
|
|
11
|
+
* - features.preventBrew: removed (warn to use pi-guardrails)
|
|
12
|
+
* - features.preventDockerSecrets: removed (warn to use pi-guardrails)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { FeatureMode, ToolchainConfig } from '../config'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Config schema version. Bump only when a migration is added.
|
|
19
|
+
* Keep independent from package.json version.
|
|
20
|
+
*/
|
|
21
|
+
export const CURRENT_VERSION = '0.5.2-20260331'
|
|
22
|
+
|
|
23
|
+
/** Warnings queued during migration, flushed at session_start. */
|
|
24
|
+
export const pendingWarnings: string[] = []
|
|
25
|
+
|
|
26
|
+
const LEGACY_BOOLEAN_FEATURES = [
|
|
27
|
+
'enforcePackageManager',
|
|
28
|
+
'rewritePython',
|
|
29
|
+
'gitRebaseEditor',
|
|
30
|
+
] as const
|
|
31
|
+
|
|
32
|
+
const REMOVED_FEATURES = ['preventBrew', 'preventDockerSecrets'] as const
|
|
33
|
+
|
|
34
|
+
/** v0 = any config without a version field. */
|
|
35
|
+
export function isV0(config: ToolchainConfig): boolean {
|
|
36
|
+
return (config as Record<string, unknown>).version === undefined
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function migrateV0(config: ToolchainConfig): ToolchainConfig {
|
|
40
|
+
const migrated = structuredClone(config) as Record<string, unknown> & {
|
|
41
|
+
features: Record<string, unknown>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!migrated.features) migrated.features = {}
|
|
45
|
+
|
|
46
|
+
// Migrate boolean features to FeatureMode strings.
|
|
47
|
+
for (const key of LEGACY_BOOLEAN_FEATURES) {
|
|
48
|
+
const val = migrated.features[key]
|
|
49
|
+
if (typeof val === 'boolean') {
|
|
50
|
+
const mode: FeatureMode = val ? 'rewrite' : 'disabled'
|
|
51
|
+
migrated.features[key] = mode
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Strip removed features and warn.
|
|
56
|
+
const removedFound: string[] = []
|
|
57
|
+
for (const key of REMOVED_FEATURES) {
|
|
58
|
+
if (key in migrated.features) {
|
|
59
|
+
delete migrated.features[key]
|
|
60
|
+
removedFound.push(key)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Always show an update notice on first run after upgrading.
|
|
65
|
+
// The migration runs exactly once per config file (version is stamped below),
|
|
66
|
+
// so this message will never appear more than once per scope.
|
|
67
|
+
pendingWarnings.push(
|
|
68
|
+
'[toolchain] Updated: feature options now support three modes: ' +
|
|
69
|
+
'"disabled", "rewrite" (transparent command rewriting), or "block" ' +
|
|
70
|
+
'(block the command via tool_call without overriding the bash tool). ' +
|
|
71
|
+
'Use /toolchain:settings to configure. ' +
|
|
72
|
+
(removedFound.length > 0
|
|
73
|
+
? `The following features were removed and stripped from your config: ${removedFound.join(', ')}. ` +
|
|
74
|
+
'They are now available in @aliou/pi-guardrails ' +
|
|
75
|
+
'(/guardrails:settings > Examples > Dangerous command presets).'
|
|
76
|
+
: 'Note: preventBrew and preventDockerSecrets have been removed from pi-toolchain. ' +
|
|
77
|
+
'They are now available in @aliou/pi-guardrails ' +
|
|
78
|
+
'(/guardrails:settings > Examples > Dangerous command presets).'),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
migrated.version = CURRENT_VERSION
|
|
82
|
+
return migrated as ToolchainConfig
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function isMissingBashSourceMode(config: ToolchainConfig): boolean {
|
|
86
|
+
return config.bash?.sourceMode === undefined
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function migrateMissingBashSourceMode(
|
|
90
|
+
config: ToolchainConfig,
|
|
91
|
+
): ToolchainConfig {
|
|
92
|
+
return {
|
|
93
|
+
...config,
|
|
94
|
+
bash: {
|
|
95
|
+
...config.bash,
|
|
96
|
+
sourceMode: 'override-bash',
|
|
97
|
+
},
|
|
98
|
+
version: CURRENT_VERSION,
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared shell AST helpers used by toolchain hooks and rewriters.
|
|
3
|
+
*
|
|
4
|
+
* Duplicated from guardrails (not shared) because extensions are
|
|
5
|
+
* independent packages -- cross-extension imports are not allowed.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
Assignment,
|
|
10
|
+
Command,
|
|
11
|
+
Program,
|
|
12
|
+
SimpleCommand,
|
|
13
|
+
Statement,
|
|
14
|
+
Word,
|
|
15
|
+
WordPart,
|
|
16
|
+
} from '@aliou/sh'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Resolve a Word node to its literal string value.
|
|
20
|
+
* Concatenates Literal, SglQuoted, and simple DblQuoted parts.
|
|
21
|
+
* For parts containing parameter expansions, command substitutions, etc.,
|
|
22
|
+
* includes the raw text representation (e.g. `$VAR`).
|
|
23
|
+
*/
|
|
24
|
+
export function wordToString(word: Word): string {
|
|
25
|
+
return word.parts.map(partToString).join('')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function partToString(part: WordPart): string {
|
|
29
|
+
switch (part.type) {
|
|
30
|
+
case 'Literal':
|
|
31
|
+
return part.value
|
|
32
|
+
case 'SglQuoted':
|
|
33
|
+
return part.value
|
|
34
|
+
case 'DblQuoted':
|
|
35
|
+
return part.parts.map(partToString).join('')
|
|
36
|
+
case 'ParamExp':
|
|
37
|
+
return part.short
|
|
38
|
+
? `$${part.param.value}`
|
|
39
|
+
: `\${${part.param.value}${part.op ?? ''}${part.value ? wordToString(part.value) : ''}}`
|
|
40
|
+
case 'CmdSubst':
|
|
41
|
+
return '$(...)'
|
|
42
|
+
case 'ArithExp':
|
|
43
|
+
return `$((${part.expr}))`
|
|
44
|
+
case 'ProcSubst':
|
|
45
|
+
return `${part.op}(...)`
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Walk the AST and call `callback` for every SimpleCommand found at any
|
|
51
|
+
* nesting depth. Returns early if callback returns `true`.
|
|
52
|
+
*/
|
|
53
|
+
export function walkCommands(
|
|
54
|
+
node: Program,
|
|
55
|
+
callback: (cmd: SimpleCommand) => boolean | undefined,
|
|
56
|
+
): void {
|
|
57
|
+
for (const stmt of node.body) {
|
|
58
|
+
if (walkStatement(stmt, callback)) return
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Walk the AST and call `callback` for every SimpleCommand, passing
|
|
64
|
+
* both the command and its assignments. Needed for checking env var
|
|
65
|
+
* assignments like `GIT_EDITOR=x git rebase`.
|
|
66
|
+
*/
|
|
67
|
+
export function walkCommandsWithAssignments(
|
|
68
|
+
node: Program,
|
|
69
|
+
callback: (
|
|
70
|
+
cmd: SimpleCommand,
|
|
71
|
+
assignments: Assignment[],
|
|
72
|
+
) => boolean | undefined,
|
|
73
|
+
): void {
|
|
74
|
+
for (const stmt of node.body) {
|
|
75
|
+
if (walkStatementWithAssignments(stmt, callback)) return
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function walkStatement(
|
|
80
|
+
stmt: Statement,
|
|
81
|
+
callback: (cmd: SimpleCommand) => boolean | undefined,
|
|
82
|
+
): boolean {
|
|
83
|
+
return walkCommand(stmt.command, callback)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function walkStatementWithAssignments(
|
|
87
|
+
stmt: Statement,
|
|
88
|
+
callback: (
|
|
89
|
+
cmd: SimpleCommand,
|
|
90
|
+
assignments: Assignment[],
|
|
91
|
+
) => boolean | undefined,
|
|
92
|
+
): boolean {
|
|
93
|
+
return walkCommandWithAssignments(stmt.command, callback)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function walkStatements(
|
|
97
|
+
stmts: Statement[],
|
|
98
|
+
callback: (cmd: SimpleCommand) => boolean | undefined,
|
|
99
|
+
): boolean {
|
|
100
|
+
for (const stmt of stmts) {
|
|
101
|
+
if (walkStatement(stmt, callback)) return true
|
|
102
|
+
}
|
|
103
|
+
return false
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function walkStatementsWithAssignments(
|
|
107
|
+
stmts: Statement[],
|
|
108
|
+
callback: (
|
|
109
|
+
cmd: SimpleCommand,
|
|
110
|
+
assignments: Assignment[],
|
|
111
|
+
) => boolean | undefined,
|
|
112
|
+
): boolean {
|
|
113
|
+
for (const stmt of stmts) {
|
|
114
|
+
if (walkStatementWithAssignments(stmt, callback)) return true
|
|
115
|
+
}
|
|
116
|
+
return false
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function walkCommand(
|
|
120
|
+
cmd: Command,
|
|
121
|
+
callback: (cmd: SimpleCommand) => boolean | undefined,
|
|
122
|
+
): boolean {
|
|
123
|
+
switch (cmd.type) {
|
|
124
|
+
case 'SimpleCommand':
|
|
125
|
+
return callback(cmd) === true
|
|
126
|
+
|
|
127
|
+
case 'Pipeline':
|
|
128
|
+
return walkStatements(cmd.commands, callback)
|
|
129
|
+
|
|
130
|
+
case 'Logical':
|
|
131
|
+
return (
|
|
132
|
+
walkStatement(cmd.left, callback) || walkStatement(cmd.right, callback)
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
case 'Subshell':
|
|
136
|
+
case 'Block':
|
|
137
|
+
return walkStatements(cmd.body, callback)
|
|
138
|
+
|
|
139
|
+
case 'IfClause':
|
|
140
|
+
return (
|
|
141
|
+
walkStatements(cmd.cond, callback) ||
|
|
142
|
+
walkStatements(cmd.then, callback) ||
|
|
143
|
+
(cmd.else ? walkStatements(cmd.else, callback) : false)
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
case 'ForClause':
|
|
147
|
+
case 'SelectClause':
|
|
148
|
+
case 'WhileClause':
|
|
149
|
+
return (
|
|
150
|
+
('cond' in cmd && cmd.cond
|
|
151
|
+
? walkStatements(cmd.cond, callback)
|
|
152
|
+
: false) || walkStatements(cmd.body, callback)
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
case 'CaseClause':
|
|
156
|
+
for (const item of cmd.items) {
|
|
157
|
+
if (walkStatements(item.body, callback)) return true
|
|
158
|
+
}
|
|
159
|
+
return false
|
|
160
|
+
|
|
161
|
+
case 'FunctionDecl':
|
|
162
|
+
return walkStatements(cmd.body, callback)
|
|
163
|
+
|
|
164
|
+
case 'TimeClause':
|
|
165
|
+
return walkStatement(cmd.command, callback)
|
|
166
|
+
|
|
167
|
+
case 'CoprocClause':
|
|
168
|
+
return walkStatement(cmd.body, callback)
|
|
169
|
+
|
|
170
|
+
case 'CStyleLoop':
|
|
171
|
+
return walkStatements(cmd.body, callback)
|
|
172
|
+
|
|
173
|
+
case 'TestClause':
|
|
174
|
+
case 'ArithCmd':
|
|
175
|
+
case 'DeclClause':
|
|
176
|
+
case 'LetClause':
|
|
177
|
+
return false
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function walkCommandWithAssignments(
|
|
182
|
+
cmd: Command,
|
|
183
|
+
callback: (
|
|
184
|
+
cmd: SimpleCommand,
|
|
185
|
+
assignments: Assignment[],
|
|
186
|
+
) => boolean | undefined,
|
|
187
|
+
): boolean {
|
|
188
|
+
switch (cmd.type) {
|
|
189
|
+
case 'SimpleCommand':
|
|
190
|
+
return callback(cmd, cmd.assignments ?? []) === true
|
|
191
|
+
|
|
192
|
+
case 'Pipeline':
|
|
193
|
+
return walkStatementsWithAssignments(cmd.commands, callback)
|
|
194
|
+
|
|
195
|
+
case 'Logical':
|
|
196
|
+
return (
|
|
197
|
+
walkStatementWithAssignments(cmd.left, callback) ||
|
|
198
|
+
walkStatementWithAssignments(cmd.right, callback)
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
case 'Subshell':
|
|
202
|
+
case 'Block':
|
|
203
|
+
return walkStatementsWithAssignments(cmd.body, callback)
|
|
204
|
+
|
|
205
|
+
case 'IfClause':
|
|
206
|
+
return (
|
|
207
|
+
walkStatementsWithAssignments(cmd.cond, callback) ||
|
|
208
|
+
walkStatementsWithAssignments(cmd.then, callback) ||
|
|
209
|
+
(cmd.else ? walkStatementsWithAssignments(cmd.else, callback) : false)
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
case 'ForClause':
|
|
213
|
+
case 'SelectClause':
|
|
214
|
+
case 'WhileClause':
|
|
215
|
+
return (
|
|
216
|
+
('cond' in cmd && cmd.cond
|
|
217
|
+
? walkStatementsWithAssignments(cmd.cond, callback)
|
|
218
|
+
: false) || walkStatementsWithAssignments(cmd.body, callback)
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
case 'CaseClause':
|
|
222
|
+
for (const item of cmd.items) {
|
|
223
|
+
if (walkStatementsWithAssignments(item.body, callback)) return true
|
|
224
|
+
}
|
|
225
|
+
return false
|
|
226
|
+
|
|
227
|
+
case 'FunctionDecl':
|
|
228
|
+
return walkStatementsWithAssignments(cmd.body, callback)
|
|
229
|
+
|
|
230
|
+
case 'TimeClause':
|
|
231
|
+
return walkStatementWithAssignments(cmd.command, callback)
|
|
232
|
+
|
|
233
|
+
case 'CoprocClause':
|
|
234
|
+
return walkStatementWithAssignments(cmd.body, callback)
|
|
235
|
+
|
|
236
|
+
case 'CStyleLoop':
|
|
237
|
+
return walkStatementsWithAssignments(cmd.body, callback)
|
|
238
|
+
|
|
239
|
+
case 'TestClause':
|
|
240
|
+
case 'ArithCmd':
|
|
241
|
+
case 'DeclClause':
|
|
242
|
+
case 'LetClause':
|
|
243
|
+
return false
|
|
244
|
+
}
|
|
245
|
+
}
|