Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion cmd/internal/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ type Config struct {
}

type ConfigParser struct {
EnvVars map[string]string
EnvVars map[string]string
OptionalEnvVars []string
requiredEnvVars []string
}

// parseEnv replaces environment variables ${ENV_NAME} with their values.
Expand All @@ -59,6 +61,25 @@ func (p *ConfigParser) parseEnv(input string) (string, error) {

// extract the variable name
variableName := parts[1]

isOptional := len(parts) >= 4 && parts[2] != ""
if isOptional {
// Add to optional list only if it hasn't been explicitly required
if !slices.Contains(p.requiredEnvVars, variableName) && !slices.Contains(p.OptionalEnvVars, variableName) {
p.OptionalEnvVars = append(p.OptionalEnvVars, variableName)
}
} else {
// Mark as required
if !slices.Contains(p.requiredEnvVars, variableName) {
p.requiredEnvVars = append(p.requiredEnvVars, variableName)
}

// Remove from optional list if it's there
if i := slices.Index(p.OptionalEnvVars, variableName); i != -1 {
p.OptionalEnvVars = slices.Delete(p.OptionalEnvVars, i, i+1)
}
}

if value, found := os.LookupEnv(variableName); found {
p.EnvVars[variableName] = value
return value
Expand Down
58 changes: 44 additions & 14 deletions cmd/internal/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,13 @@ import (

func TestParseEnv(t *testing.T) {
tcs := []struct {
desc string
env map[string]string
in string
want string
err bool
errString string
desc string
env map[string]string
in string
want string
err bool
errString string
wantOptional []string
}{
{
desc: "without default without env",
Expand All @@ -61,22 +62,43 @@ func TestParseEnv(t *testing.T) {
want: "bar",
},
{
desc: "with empty default",
in: "${FOO:}",
want: "",
desc: "with empty default",
in: "${FOO:}",
want: "",
wantOptional: []string{"FOO"},
},
{
desc: "with default",
in: "${FOO:bar}",
want: "bar",
desc: "with default",
in: "${FOO:bar}",
want: "bar",
wantOptional: []string{"FOO"},
},
{
desc: "with default with env",
env: map[string]string{
"FOO": "hello",
},
in: "${FOO:bar}",
want: "hello",
in: "${FOO:bar}",
want: "hello",
wantOptional: []string{"FOO"},
},
{
desc: "multiple variables",
in: "user: ${USER_NAME:}, password: ${PASSWORD:}, ip: ${IP:public}, region: ${REGION}",
env: map[string]string{
"REGION": "us-central1",
},
want: "user: , password: , ip: public, region: us-central1",
wantOptional: []string{"USER_NAME", "PASSWORD", "IP"},
},
{
desc: "variable required in one place and optional in another",
in: "project_req: ${PROJECT_ID}, project_opt: ${PROJECT_ID:default}",
env: map[string]string{
"PROJECT_ID": "my_project",
},
want: "project_req: my_project, project_opt: my_project",
wantOptional: []string{}, // Because it was marked required at least once
},
}
for _, tc := range tcs {
Expand All @@ -99,6 +121,14 @@ func TestParseEnv(t *testing.T) {
if tc.want != got {
t.Fatalf("unexpected want: got %s, want %s", got, tc.want)
}
if len(parser.OptionalEnvVars) != len(tc.wantOptional) {
t.Fatalf("OptionalEnvVars length mismatch: got %d, want %d. Got: %v, Want: %v", len(parser.OptionalEnvVars), len(tc.wantOptional), parser.OptionalEnvVars, tc.wantOptional)
}
for i, v := range parser.OptionalEnvVars {
if v != tc.wantOptional[i] {
t.Errorf("OptionalEnvVars element %d mismatch: got %q, want %q", i, v, tc.wantOptional[i])
}
}
})
}
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/internal/skills/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ func run(cmd *skillsCmd, opts *internal.ToolboxOptions) error {

for _, toolName := range toolNames {
// Generate wrapper script in scripts directory
scriptContent, err := generateScriptContent(toolName, configArgsStr, cmd.licenseHeader, cmd.invocationMode, cmd.toolboxVersion)
scriptContent, err := generateScriptContent(toolName, configArgsStr, cmd.licenseHeader, cmd.invocationMode, cmd.toolboxVersion, parser.OptionalEnvVars)
if err != nil {
errMsg := fmt.Errorf("error generating script content for %s: %w", toolName, err)
opts.Logger.ErrorContext(ctx, errMsg.Error())
Expand Down
176 changes: 102 additions & 74 deletions cmd/internal/skills/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,89 +128,115 @@ const os = require('os');

const toolName = "{{.Name}}";
const configArgs = [{{.ConfigArgs}}];
{{if .OptionalVars}}
const OPTIONAL_VARS_TO_OMIT_IF_EMPTY = [
{{range .OptionalVars}} '{{.}}',
{{end}}];
{{end}}

function getEnv() {
const envPath = path.resolve(__dirname, '../../../.env');
const env = { ...process.env };
if (fs.existsSync(envPath)) {
const envContent = fs.readFileSync(envPath, 'utf-8');
envContent.split('\n').forEach(line => {
const trimmed = line.trim();
if (trimmed && !trimmed.startsWith('#')) {
const splitIdx = trimmed.indexOf('=');
if (splitIdx !== -1) {
const key = trimmed.slice(0, splitIdx).trim();
let value = trimmed.slice(splitIdx + 1).trim();
value = value.replace(/(^['"]|['"]$)/g, '');
if (env[key] === undefined) {
env[key] = value;
}
}
}
});
}
return env;
function mergeEnvVars(env) {
if (process.env.GEMINI_CLI === '1') {
const envPath = path.resolve(__dirname, '../../../.env');
if (fs.existsSync(envPath)) {
const envContent = fs.readFileSync(envPath, 'utf-8');
envContent.split('\n').forEach(line => {
const trimmed = line.trim();
if (trimmed && !trimmed.startsWith('#')) {
const splitIdx = trimmed.indexOf('=');
if (splitIdx !== -1) {
const key = trimmed.slice(0, splitIdx).trim();
let value = trimmed.slice(splitIdx + 1).trim();
value = value.replace(/(^['"]|['"]$)/g, '');
if (env[key] === undefined) {
env[key] = value;
}
}
}
});
}
} else if (process.env.CLAUDECODE === '1') {
const prefix = 'CLAUDE_PLUGIN_OPTION_';
for (const key in process.env) {
if (key.startsWith(prefix)) {
env[key.substring(prefix.length)] = process.env[key];
}
}
}
}

let env = process.env;
let userAgent = "skills";
if (process.env.GEMINI_CLI === '1') {
env = getEnv();
userAgent = "skills-geminicli";
}
function prepareEnvironment() {
let env = { ...process.env };
let userAgent = "skills";
if (process.env.GEMINI_CLI === '1') {
userAgent = "skills-geminicli";
} else if (process.env.CLAUDECODE === '1') {
userAgent = "skills-claudecode";
}
mergeEnvVars(env);
{{if .OptionalVars}}
OPTIONAL_VARS_TO_OMIT_IF_EMPTY.forEach(varName => {
if (env[varName] === '') {
delete env[varName];
}
});
{{end}}

const args = process.argv.slice(2);

{{if eq .InvocationMode "npx"}}
const command = os.platform() === 'win32' ? 'npx.cmd' : 'npx';

const processedArgs = os.platform() === 'win32' ? args.map(arg => arg.includes('"') ? '"' + arg.replace(/"/g, '""') + '"' : arg) : args;

const npxArgs = ["--yes", "@toolbox-sdk/server@{{.ToolboxVersion}}", "--log-level", "error", ...configArgs, "invoke", toolName, "--user-agent-metadata", userAgent, ...processedArgs];

const child = spawn(command, npxArgs, { shell: os.platform() === 'win32', stdio: 'inherit', env });
{{else}}
function getToolboxPath() {
if (process.env.GEMINI_CLI === '1') {
const ext = process.platform === 'win32' ? '.exe' : '';
const localPath = path.resolve(__dirname, '../../../toolbox' + ext);
if (fs.existsSync(localPath)) {
return localPath;
}
}
try {
const checkCommand = process.platform === 'win32' ? 'where toolbox' : 'which toolbox';
const globalPath = execSync(checkCommand, { stdio: 'pipe', encoding: 'utf-8' }).trim();
if (globalPath) {
return globalPath.split('\n')[0].trim();
}
throw new Error("Toolbox binary not found");
} catch (e) {
throw new Error("Toolbox binary not found");
}
return { env, userAgent };
}

let toolboxBinary;
try {
toolboxBinary = getToolboxPath();
} catch (err) {
console.error("Error:", err.message);
process.exit(1);
}
function main() {
const { env, userAgent } = prepareEnvironment();
const args = process.argv.slice(2);
{{if eq .InvocationMode "npx"}}
const command = os.platform() === 'win32' ? 'npx.cmd' : 'npx';
const processedArgs = os.platform() === 'win32' ? args.map(arg => arg.includes('"') ? '"' + arg.replace(/"/g, '""') + '"' : arg) : args;
const npxArgs = ["--yes", "@toolbox-sdk/server@{{.ToolboxVersion}}", "--log-level", "error", ...configArgs, "invoke", toolName, "--user-agent-metadata", userAgent, ...processedArgs];

const child = spawn(command, npxArgs, { shell: os.platform() === 'win32', stdio: 'inherit', env });
{{else}}
function getToolboxPath() {
if (process.env.GEMINI_CLI === '1') {
const ext = process.platform === 'win32' ? '.exe' : '';
const localPath = path.resolve(__dirname, '../../../toolbox' + ext);
if (fs.existsSync(localPath)) {
return localPath;
}
}
try {
const checkCommand = process.platform === 'win32' ? 'where toolbox' : 'which toolbox';
const globalPath = execSync(checkCommand, { stdio: 'pipe', encoding: 'utf-8' }).trim();
if (globalPath) {
return globalPath.split('\n')[0].trim();
}
throw new Error("Toolbox binary not found");
} catch (e) {
throw new Error("Toolbox binary not found");
}
}

const toolboxArgs = ["--log-level", "error", ...configArgs, "invoke", toolName, "--user-agent-metadata", userAgent, ...args];
let toolboxBinary;
try {
toolboxBinary = getToolboxPath();
} catch (err) {
console.error("Error:", err.message);
process.exit(1);
}

const child = spawn(toolboxBinary, toolboxArgs, { stdio: 'inherit', env });
{{end}}
const toolboxArgs = ["--log-level", "error", ...configArgs, "invoke", toolName, "--user-agent-metadata", userAgent, ...args];
const child = spawn(toolboxBinary, toolboxArgs, { stdio: 'inherit', env });
{{end}}

child.on('close', (code) => {
process.exit(code);
});
child.on('close', (code) => {
process.exit(code);
});

child.on('error', (err) => {
console.error("Error executing toolbox:", err);
process.exit(1);
});
}

child.on('error', (err) => {
console.error("Error executing toolbox:", err);
process.exit(1);
});
main();
`

type scriptData struct {
Expand All @@ -219,18 +245,20 @@ type scriptData struct {
LicenseHeader string
InvocationMode string
ToolboxVersion string
OptionalVars []string
}

// generateScriptContent creates the content for a Node.js wrapper script.
// This script invokes the toolbox CLI with the appropriate configuration
// (using a generated config) and arguments to execute the specific tool.
func generateScriptContent(name string, configArgs string, licenseHeader string, mode string, version string) (string, error) {
func generateScriptContent(name string, configArgs string, licenseHeader string, mode string, version string, optionalVars []string) (string, error) {
data := scriptData{
Name: name,
ConfigArgs: configArgs,
LicenseHeader: licenseHeader,
InvocationMode: mode,
ToolboxVersion: version,
OptionalVars: optionalVars,
}

tmpl, err := template.New("script").Parse(nodeScriptTemplate)
Expand Down
Loading
Loading