From 551d0b13bef8c01cc5b8feb861ce822e448bc91b Mon Sep 17 00:00:00 2001 From: Jayesh Betala Date: Fri, 29 May 2026 12:24:25 +0530 Subject: [PATCH] fix(config): preserve spaces in values --- bin/gstack-config | 22 ++++++++++++++++------ test/explain-level-config.test.ts | 17 +++++++++++++++++ 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/bin/gstack-config b/bin/gstack-config index 01defcef86..aaaa6777bb 100755 --- a/bin/gstack-config +++ b/bin/gstack-config @@ -248,6 +248,16 @@ resolve_user_slug() { printf '%s' "$_slug" } +read_config_value() { + local key="$1" + if [ ! -f "$CONFIG_FILE" ]; then + return 0 + fi + grep -E "^${key}:" "$CONFIG_FILE" 2>/dev/null \ + | tail -1 \ + | sed -E "s/^${key}:[[:space:]]*//; s/[[:space:]]+$//" +} + case "${1:-}" in get) KEY="${2:?Usage: gstack-config get }" @@ -257,8 +267,7 @@ case "${1:-}" in echo "Error: key must contain only alphanumeric characters, underscores, and an optional @ suffix" >&2 exit 1 fi - # Use literal match for keys containing @ (sha hashes), regex otherwise - VALUE=$(grep -F "${KEY}:" "$CONFIG_FILE" 2>/dev/null | grep -E "^${KEY%@*}(@[a-f0-9]+)?:" | grep -F "${KEY}:" | tail -1 | awk '{print $2}' | tr -d '[:space:]' || true) + VALUE=$(read_config_value "$KEY" || true) if [ -z "$VALUE" ]; then VALUE=$(lookup_default "$KEY") fi @@ -307,14 +316,15 @@ case "${1:-}" in if [ ! -f "$CONFIG_FILE" ]; then printf '%s' "$CONFIG_HEADER" > "$CONFIG_FILE" fi - # Escape sed special chars in value and drop embedded newlines - ESC_VALUE="$(printf '%s' "$VALUE" | head -1 | sed 's/[&/\]/\\&/g')" + # Drop embedded newlines, then escape sed replacement metacharacters. + SAFE_VALUE="$(printf '%s' "$VALUE" | head -1)" + ESC_VALUE="$(printf '%s' "$SAFE_VALUE" | sed 's/[&/\]/\\&/g')" if grep -qE "^${KEY}:" "$CONFIG_FILE" 2>/dev/null; then # Portable in-place edit (BSD sed uses -i '', GNU sed uses -i without arg) _tmpfile="$(mktemp "${CONFIG_FILE}.XXXXXX")" sed "/^${KEY}:/s/.*/${KEY}: ${ESC_VALUE}/" "$CONFIG_FILE" > "$_tmpfile" && mv "$_tmpfile" "$CONFIG_FILE" else - echo "${KEY}: ${VALUE}" >> "$CONFIG_FILE" + echo "${KEY}: ${SAFE_VALUE}" >> "$CONFIG_FILE" fi # Auto-relink skills when prefix setting changes (skip during setup to avoid recursive call) if [ "$KEY" = "skill_prefix" ] && [ -z "${GSTACK_SETUP_RUNNING:-}" ]; then @@ -332,7 +342,7 @@ case "${1:-}" in skill_prefix checkpoint_mode checkpoint_push explain_level \ codex_reviews gstack_contributor skip_eng_review workspace_root \ artifacts_sync_mode artifacts_sync_mode_prompted plan_tune_hooks; do - VALUE=$(grep -E "^${KEY}:" "$CONFIG_FILE" 2>/dev/null | tail -1 | awk '{print $2}' | tr -d '[:space:]' || true) + VALUE=$(read_config_value "$KEY" || true) SOURCE="default" if [ -n "$VALUE" ]; then SOURCE="set" diff --git a/test/explain-level-config.test.ts b/test/explain-level-config.test.ts index 9a48a9f6b2..cdb61296cc 100644 --- a/test/explain-level-config.test.ts +++ b/test/explain-level-config.test.ts @@ -88,3 +88,20 @@ describe('gstack-config explain_level', () => { expect(run('get', 'explain_level').stdout).toBe('default'); }); }); + +describe('gstack-config values with spaces', () => { + test('workspace_root preserves internal spaces on set/get/list', () => { + const value = path.join(os.tmpdir(), 'Conductor Workspaces'); + expect(run('set', 'workspace_root', value).status).toBe(0); + + expect(run('get', 'workspace_root').stdout).toBe(value); + + const listed = run('list'); + expect(listed.status).toBe(0); + expect( + listed.stdout + .split('\n') + .some((line) => line.includes('workspace_root:') && line.includes(value) && line.includes('(set)')), + ).toBe(true); + }); +});