@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.
@@ -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
+ }