Skip to content

Commit 1913cc5

Browse files
authored
initscript -- support for local files, and overrides in connections.json (#1818)
1 parent 8bf90c0 commit 1913cc5

File tree

8 files changed

+423
-48
lines changed

8 files changed

+423
-48
lines changed

cmd/wsh/cmd/setmeta_test.go

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
// Copyright 2025, Command Line Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package cmd
5+
6+
import (
7+
"reflect"
8+
"testing"
9+
)
10+
11+
func TestParseMetaSets(t *testing.T) {
12+
tests := []struct {
13+
name string
14+
input []string
15+
want map[string]any
16+
wantErr bool
17+
}{
18+
{
19+
name: "basic types",
20+
input: []string{"str=hello", "num=42", "float=3.14", "bool=true", "null=null"},
21+
want: map[string]any{
22+
"str": "hello",
23+
"num": int64(42),
24+
"float": float64(3.14),
25+
"bool": true,
26+
"null": nil,
27+
},
28+
},
29+
{
30+
name: "json values",
31+
input: []string{
32+
`arr=[1,2,3]`,
33+
`obj={"foo":"bar"}`,
34+
`str="quoted"`,
35+
},
36+
want: map[string]any{
37+
"arr": []any{float64(1), float64(2), float64(3)},
38+
"obj": map[string]any{"foo": "bar"},
39+
"str": "quoted",
40+
},
41+
},
42+
{
43+
name: "nested paths",
44+
input: []string{
45+
"a/b=55",
46+
"a/c=2",
47+
},
48+
want: map[string]any{
49+
"a": map[string]any{
50+
"b": int64(55),
51+
"c": int64(2),
52+
},
53+
},
54+
},
55+
{
56+
name: "deep nesting",
57+
input: []string{
58+
"a/b/c/d=hello",
59+
},
60+
want: map[string]any{
61+
"a": map[string]any{
62+
"b": map[string]any{
63+
"c": map[string]any{
64+
"d": "hello",
65+
},
66+
},
67+
},
68+
},
69+
},
70+
{
71+
name: "override nested value",
72+
input: []string{
73+
"a/b/c=1",
74+
"a/b=2",
75+
},
76+
want: map[string]any{
77+
"a": map[string]any{
78+
"b": int64(2),
79+
},
80+
},
81+
},
82+
{
83+
name: "override with null",
84+
input: []string{
85+
"a/b=1",
86+
"a/c=2",
87+
"a=null",
88+
},
89+
want: map[string]any{
90+
"a": nil,
91+
},
92+
},
93+
{
94+
name: "mixed types in path",
95+
input: []string{
96+
"a/b=1",
97+
"a/c=[1,2,3]",
98+
"a/d/e=true",
99+
},
100+
want: map[string]any{
101+
"a": map[string]any{
102+
"b": int64(1),
103+
"c": []any{float64(1), float64(2), float64(3)},
104+
"d": map[string]any{
105+
"e": true,
106+
},
107+
},
108+
},
109+
},
110+
{
111+
name: "invalid format",
112+
input: []string{"invalid"},
113+
wantErr: true,
114+
},
115+
{
116+
name: "invalid json",
117+
input: []string{`a={"invalid`},
118+
wantErr: true,
119+
},
120+
}
121+
122+
for _, tt := range tests {
123+
t.Run(tt.name, func(t *testing.T) {
124+
got, err := parseMetaSets(tt.input)
125+
if (err != nil) != tt.wantErr {
126+
t.Errorf("parseMetaSets() error = %v, wantErr %v", err, tt.wantErr)
127+
return
128+
}
129+
if !tt.wantErr && !reflect.DeepEqual(got, tt.want) {
130+
t.Errorf("parseMetaSets() = %v, want %v", got, tt.want)
131+
}
132+
})
133+
}
134+
}
135+
136+
func TestParseMetaValue(t *testing.T) {
137+
tests := []struct {
138+
name string
139+
input string
140+
want any
141+
wantErr bool
142+
}{
143+
{"empty string", "", nil, false},
144+
{"null", "null", nil, false},
145+
{"true", "true", true, false},
146+
{"false", "false", false, false},
147+
{"integer", "42", int64(42), false},
148+
{"negative integer", "-42", int64(-42), false},
149+
{"hex integer", "0xff", int64(255), false},
150+
{"float", "3.14", float64(3.14), false},
151+
{"string", "hello", "hello", false},
152+
{"json array", "[1,2,3]", []any{float64(1), float64(2), float64(3)}, false},
153+
{"json object", `{"foo":"bar"}`, map[string]any{"foo": "bar"}, false},
154+
{"quoted string", `"quoted"`, "quoted", false},
155+
{"invalid json", `{"invalid`, nil, true},
156+
}
157+
158+
for _, tt := range tests {
159+
t.Run(tt.name, func(t *testing.T) {
160+
got, err := parseMetaValue(tt.input)
161+
if (err != nil) != tt.wantErr {
162+
t.Errorf("parseMetaValue() error = %v, wantErr %v", err, tt.wantErr)
163+
return
164+
}
165+
if !tt.wantErr && !reflect.DeepEqual(got, tt.want) {
166+
t.Errorf("parseMetaValue() = %v, want %v", got, tt.want)
167+
}
168+
})
169+
}
170+
}

cmd/wsh/cmd/wshcmd-setmeta.go

Lines changed: 76 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -58,40 +58,88 @@ func loadJSONFile(filepath string) (map[string]interface{}, error) {
5858
return result, nil
5959
}
6060

61-
func parseMetaSets(metaSets []string) (map[string]interface{}, error) {
62-
meta := make(map[string]interface{})
61+
func parseMetaValue(setVal string) (any, error) {
62+
if setVal == "" || setVal == "null" {
63+
return nil, nil
64+
}
65+
if setVal == "true" {
66+
return true, nil
67+
}
68+
if setVal == "false" {
69+
return false, nil
70+
}
71+
if setVal[0] == '[' || setVal[0] == '{' || setVal[0] == '"' {
72+
var val any
73+
err := json.Unmarshal([]byte(setVal), &val)
74+
if err != nil {
75+
return nil, fmt.Errorf("invalid json value: %v", err)
76+
}
77+
return val, nil
78+
}
79+
80+
// Try parsing as integer
81+
ival, err := strconv.ParseInt(setVal, 0, 64)
82+
if err == nil {
83+
return ival, nil
84+
}
85+
86+
// Try parsing as float
87+
fval, err := strconv.ParseFloat(setVal, 64)
88+
if err == nil {
89+
return fval, nil
90+
}
91+
92+
// Fallback to string
93+
return setVal, nil
94+
}
95+
96+
func setNestedValue(meta map[string]any, path []string, value any) {
97+
// For single key, just set directly
98+
if len(path) == 1 {
99+
meta[path[0]] = value
100+
return
101+
}
102+
103+
// For nested path, traverse or create maps as needed
104+
current := meta
105+
for i := 0; i < len(path)-1; i++ {
106+
key := path[i]
107+
// If next level doesn't exist or isn't a map, create new map
108+
next, exists := current[key]
109+
if !exists {
110+
nextMap := make(map[string]any)
111+
current[key] = nextMap
112+
current = nextMap
113+
} else if nextMap, ok := next.(map[string]any); ok {
114+
current = nextMap
115+
} else {
116+
// If existing value isn't a map, replace with new map
117+
nextMap = make(map[string]any)
118+
current[key] = nextMap
119+
current = nextMap
120+
}
121+
}
122+
123+
// Set the final value
124+
current[path[len(path)-1]] = value
125+
}
126+
127+
func parseMetaSets(metaSets []string) (map[string]any, error) {
128+
meta := make(map[string]any)
63129
for _, metaSet := range metaSets {
64130
fields := strings.SplitN(metaSet, "=", 2)
65131
if len(fields) != 2 {
66132
return nil, fmt.Errorf("invalid meta set: %q", metaSet)
67133
}
68-
setVal := fields[1]
69-
if setVal == "" || setVal == "null" {
70-
meta[fields[0]] = nil
71-
} else if setVal == "true" {
72-
meta[fields[0]] = true
73-
} else if setVal == "false" {
74-
meta[fields[0]] = false
75-
} else if setVal[0] == '[' || setVal[0] == '{' || setVal[0] == '"' {
76-
var val interface{}
77-
err := json.Unmarshal([]byte(setVal), &val)
78-
if err != nil {
79-
return nil, fmt.Errorf("invalid json value: %v", err)
80-
}
81-
meta[fields[0]] = val
82-
} else {
83-
ival, err := strconv.ParseInt(setVal, 0, 64)
84-
if err == nil {
85-
meta[fields[0]] = ival
86-
} else {
87-
fval, err := strconv.ParseFloat(setVal, 64)
88-
if err == nil {
89-
meta[fields[0]] = fval
90-
} else {
91-
meta[fields[0]] = setVal
92-
}
93-
}
134+
135+
val, err := parseMetaValue(fields[1])
136+
if err != nil {
137+
return nil, err
94138
}
139+
140+
// Split the key path and set nested value
141+
path := strings.Split(fields[0], "/")
142+
setNestedValue(meta, path, val)
95143
}
96144
return meta, nil
97145
}

frontend/types/gotypes.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,13 @@ declare global {
306306
"term:fontsize"?: number;
307307
"term:fontfamily"?: string;
308308
"term:theme"?: string;
309+
"cmd:env"?: {[key: string]: string};
310+
"cmd:initscript"?: string;
311+
"cmd:initscript.sh"?: string;
312+
"cmd:initscript.bash"?: string;
313+
"cmd:initscript.zsh"?: string;
314+
"cmd:initscript.pwsh"?: string;
315+
"cmd:initscript.fish"?: string;
309316
"ssh:user"?: string;
310317
"ssh:hostname"?: string;
311318
"ssh:port"?: string;

0 commit comments

Comments
 (0)