Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
22 changes: 16 additions & 6 deletions bin/gstack-config
Original file line number Diff line number Diff line change
Expand Up @@ -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 <key>}"
Expand All @@ -257,8 +267,7 @@ case "${1:-}" in
echo "Error: key must contain only alphanumeric characters, underscores, and an optional @<hex-hash> 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
Expand Down Expand Up @@ -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
Expand All @@ -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"
Expand Down
17 changes: 17 additions & 0 deletions test/explain-level-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
Loading