claude-code-station 0.2.1
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.
- package/CHANGELOG.md +176 -0
- package/LICENSE +21 -0
- package/README.md +311 -0
- package/bin/ccs +376 -0
- package/bin/ccs-config.ts +528 -0
- package/bin/ccs-db.ts +404 -0
- package/bin/ccs-delete-session.ts +48 -0
- package/bin/ccs-delete.sh +100 -0
- package/bin/ccs-init.ts +287 -0
- package/bin/ccs-list.ts +363 -0
- package/bin/ccs-preview-session.ts +147 -0
- package/bin/ccs-preview.ts +368 -0
- package/bin/ccs-sanitize.ts +57 -0
- package/bin/ccs-scan-sessions.ts +402 -0
- package/bin/ccs-scan.ts +734 -0
- package/bin/ccs-secrets.ts +104 -0
- package/bin/ccs-time.ts +27 -0
- package/bin/ccs-utils.ts +161 -0
- package/docs/design/repos-yml-schema.md +217 -0
- package/docs/design/sqlite-schema.md +253 -0
- package/docs/v0.2.0-regression-checklist.md +40 -0
- package/docs/v0.2.0-review-notes.md +151 -0
- package/docs/v0.2.1-backlog.md +225 -0
- package/package.json +44 -0
package/bin/ccs
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ccs - Claude Code Station: fzf-powered launcher (NEW repos + RESUME sessions)
|
|
3
|
+
# https://github.com/indigo-gr/claude-code-station
|
|
4
|
+
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
VERSION="0.2.0"
|
|
8
|
+
|
|
9
|
+
# Resolve $0 through symlinks before taking dirname: `npm install -g
|
|
10
|
+
# github:indigo-gr/claude-code-station` links this script into the global
|
|
11
|
+
# bin dir, and the SYMLINK's dirname has none of the sibling .ts modules.
|
|
12
|
+
SOURCE="$0"
|
|
13
|
+
while [[ -L "$SOURCE" ]]; do
|
|
14
|
+
DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)"
|
|
15
|
+
SOURCE="$(readlink "$SOURCE")"
|
|
16
|
+
if [[ "$SOURCE" != /* ]]; then SOURCE="${DIR}/${SOURCE}"; fi
|
|
17
|
+
done
|
|
18
|
+
SCRIPT_DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)"
|
|
19
|
+
|
|
20
|
+
# Prefer a package-local tsx over the PATH one: the npm install brings tsx
|
|
21
|
+
# as a dependency (../node_modules from the package's bin/), and the copy
|
|
22
|
+
# install (install.sh) symlinks node_modules next to the scripts. Fall back
|
|
23
|
+
# to a globally installed tsx for the bare-checkout workflow.
|
|
24
|
+
if [[ -x "${SCRIPT_DIR}/../node_modules/.bin/tsx" ]]; then
|
|
25
|
+
TSX="${SCRIPT_DIR}/../node_modules/.bin/tsx"
|
|
26
|
+
elif [[ -x "${SCRIPT_DIR}/node_modules/.bin/tsx" ]]; then
|
|
27
|
+
TSX="${SCRIPT_DIR}/node_modules/.bin/tsx"
|
|
28
|
+
else
|
|
29
|
+
TSX="tsx"
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
CCS_LIST="${SCRIPT_DIR}/ccs-list.ts"
|
|
33
|
+
CCS_SCAN="${SCRIPT_DIR}/ccs-scan.ts"
|
|
34
|
+
CCS_PREVIEW="${SCRIPT_DIR}/ccs-preview.ts"
|
|
35
|
+
CCS_DELETE="${SCRIPT_DIR}/ccs-delete.sh"
|
|
36
|
+
CCS_INIT="${SCRIPT_DIR}/ccs-init.ts"
|
|
37
|
+
|
|
38
|
+
# ── CCR_CMD -> CCS_CMD migration shim ────────────────────────────────────────
|
|
39
|
+
if [[ -n "${CCR_CMD:-}" && -z "${CCS_CMD:-}" ]]; then
|
|
40
|
+
echo "[ccs] CCR_CMD is deprecated, please rename to CCS_CMD (using CCR_CMD value for now)" >&2
|
|
41
|
+
export CCS_CMD="${CCR_CMD}"
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# ── Clipboard helper (cross-platform) ────────────────────────────────────────
|
|
45
|
+
clipboard_cmd() {
|
|
46
|
+
if command -v pbcopy &>/dev/null; then
|
|
47
|
+
echo "pbcopy"
|
|
48
|
+
elif command -v xclip &>/dev/null; then
|
|
49
|
+
echo "xclip -selection clipboard"
|
|
50
|
+
elif command -v xsel &>/dev/null; then
|
|
51
|
+
echo "xsel --clipboard --input"
|
|
52
|
+
elif command -v wl-copy &>/dev/null; then
|
|
53
|
+
echo "wl-copy"
|
|
54
|
+
else
|
|
55
|
+
echo ""
|
|
56
|
+
fi
|
|
57
|
+
}
|
|
58
|
+
CLIP_CMD=$(clipboard_cmd)
|
|
59
|
+
|
|
60
|
+
# ── Dependency check ─────────────────────────────────────────────────────────
|
|
61
|
+
check_deps() {
|
|
62
|
+
local missing=()
|
|
63
|
+
command -v fzf &>/dev/null || missing+=("fzf")
|
|
64
|
+
# tsx: a resolved package-local binary counts; only require a PATH tsx
|
|
65
|
+
# when no local one was found.
|
|
66
|
+
if [[ "$TSX" == "tsx" ]]; then
|
|
67
|
+
command -v tsx &>/dev/null || missing+=("tsx (npm install -g tsx)")
|
|
68
|
+
fi
|
|
69
|
+
command -v node &>/dev/null || missing+=("node")
|
|
70
|
+
command -v claude &>/dev/null || missing+=("claude (Claude Code CLI)")
|
|
71
|
+
command -v tput &>/dev/null || missing+=("tput")
|
|
72
|
+
|
|
73
|
+
if [[ ${#missing[@]} -gt 0 ]]; then
|
|
74
|
+
echo "❌ Missing dependencies:" >&2
|
|
75
|
+
for dep in "${missing[@]}"; do
|
|
76
|
+
echo " - $dep" >&2
|
|
77
|
+
done
|
|
78
|
+
echo "" >&2
|
|
79
|
+
echo "Install with:" >&2
|
|
80
|
+
echo " brew install fzf # or: apt install fzf" >&2
|
|
81
|
+
echo " npm install -g tsx" >&2
|
|
82
|
+
exit 1
|
|
83
|
+
fi
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# ── Help / version ───────────────────────────────────────────────────────────
|
|
87
|
+
print_help() {
|
|
88
|
+
cat <<HELP
|
|
89
|
+
ccs v${VERSION} - Claude Code Station (fzf-powered launcher)
|
|
90
|
+
|
|
91
|
+
Usage:
|
|
92
|
+
ccs Mixed list: NEW repos + RESUME sessions
|
|
93
|
+
ccs . Sessions whose cwd is (under) current dir
|
|
94
|
+
ccs --new Only NEW repo entries
|
|
95
|
+
ccs --resume Only past sessions
|
|
96
|
+
ccs --refresh Force pre-scan before showing list
|
|
97
|
+
ccs --no-scan Skip implicit pre-scan (use stale DB)
|
|
98
|
+
ccs init Create ~/.config/ccs/repos.yml from the template
|
|
99
|
+
ccs init --auto-discover [--depth N]
|
|
100
|
+
Scan \$HOME for git repos (default depth 5) and pick
|
|
101
|
+
which to add to repos.yml via fzf multi-select
|
|
102
|
+
ccs [options] Unknown options pass through to launched command
|
|
103
|
+
|
|
104
|
+
Controls:
|
|
105
|
+
Enter Launch (or resume) selected entry
|
|
106
|
+
Ctrl-Y Copy the effective shell command to clipboard
|
|
107
|
+
Ctrl-I Copy session UUID (resume) or repo path (new)
|
|
108
|
+
Ctrl-R Refresh DB and reload list
|
|
109
|
+
Ctrl-D Delete session (resume rows only)
|
|
110
|
+
Esc / Ctrl-C Cancel
|
|
111
|
+
|
|
112
|
+
Environment:
|
|
113
|
+
CCS_CMD Fallback launch command for repos with no explicit
|
|
114
|
+
command. Per-repo command in repos.yml always wins.
|
|
115
|
+
Example: export CCS_CMD="opr claude"
|
|
116
|
+
CCR_CMD Deprecated — still honored with a warning if CCS_CMD unset
|
|
117
|
+
|
|
118
|
+
Examples:
|
|
119
|
+
ccs # Full launcher
|
|
120
|
+
ccs . # Resume current-dir session
|
|
121
|
+
ccs --new # Pick a repo to launch fresh
|
|
122
|
+
ccs --refresh # Force DB rebuild
|
|
123
|
+
ccs --dangerously-skip-permissions # Pass-through flag to claude
|
|
124
|
+
|
|
125
|
+
Config: ~/.config/ccs/repos.yml
|
|
126
|
+
Cache: ~/.cache/ccs/state.db
|
|
127
|
+
HELP
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
# ── init subcommand ──────────────────────────────────────────────────────────
|
|
131
|
+
# `ccs init` scaffolds the config; `ccs init --auto-discover [--depth N]`
|
|
132
|
+
# scans $HOME for git repos and appends fzf-selected ones to repos.yml.
|
|
133
|
+
# Handled before the launcher arg loop — init never opens the main list.
|
|
134
|
+
if [[ "${1:-}" == "init" ]]; then
|
|
135
|
+
shift
|
|
136
|
+
AUTO=0
|
|
137
|
+
INIT_ARGS=()
|
|
138
|
+
for a in "$@"; do
|
|
139
|
+
case "$a" in
|
|
140
|
+
--auto-discover) AUTO=1 ;;
|
|
141
|
+
*) INIT_ARGS+=("$a") ;;
|
|
142
|
+
esac
|
|
143
|
+
done
|
|
144
|
+
|
|
145
|
+
if [[ "$TSX" == "tsx" ]] && ! command -v tsx &>/dev/null; then
|
|
146
|
+
echo "❌ tsx not found (npm install -g tsx)" >&2
|
|
147
|
+
exit 1
|
|
148
|
+
fi
|
|
149
|
+
|
|
150
|
+
if (( ! AUTO )); then
|
|
151
|
+
"${TSX}" "${CCS_INIT}" scaffold
|
|
152
|
+
exit $?
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
check_deps
|
|
156
|
+
echo "🔍 Scanning \$HOME for git repositories..." >&2
|
|
157
|
+
CANDIDATES="$("${TSX}" "${CCS_INIT}" discover ${INIT_ARGS[@]+"${INIT_ARGS[@]}"})" || exit 1
|
|
158
|
+
if [[ -z "$CANDIDATES" ]]; then
|
|
159
|
+
echo "No new git repositories found (everything within depth is already registered)." >&2
|
|
160
|
+
exit 0
|
|
161
|
+
fi
|
|
162
|
+
SELECTED=$(printf '%s\n' "$CANDIDATES" | fzf \
|
|
163
|
+
--multi \
|
|
164
|
+
--delimiter=$'\t' \
|
|
165
|
+
--header=$'TAB: toggle | Enter: add selected to repos.yml | Esc: cancel' \
|
|
166
|
+
--prompt='ccs init> ' \
|
|
167
|
+
--height='80%' \
|
|
168
|
+
--layout=reverse \
|
|
169
|
+
--border \
|
|
170
|
+
--tabstop=4 \
|
|
171
|
+
) || exit 0
|
|
172
|
+
[[ -z "$SELECTED" ]] && exit 0
|
|
173
|
+
printf '%s\n' "$SELECTED" | "${TSX}" "${CCS_INIT}" append || exit 1
|
|
174
|
+
# Index the new repos right away so the next `ccs` shows them with stats.
|
|
175
|
+
"${TSX}" "${CCS_SCAN}" --force --quiet || {
|
|
176
|
+
echo "[ccs] scan failed — run \`ccs --refresh\` to index the new repos" >&2
|
|
177
|
+
}
|
|
178
|
+
exit 0
|
|
179
|
+
fi
|
|
180
|
+
|
|
181
|
+
# ── Argument parsing ─────────────────────────────────────────────────────────
|
|
182
|
+
LIST_FLAGS=()
|
|
183
|
+
PASS_ARGS=()
|
|
184
|
+
DO_REFRESH=0
|
|
185
|
+
NO_SCAN=0
|
|
186
|
+
|
|
187
|
+
for arg in "$@"; do
|
|
188
|
+
case "$arg" in
|
|
189
|
+
.) LIST_FLAGS+=("--current-only") ;;
|
|
190
|
+
--new) LIST_FLAGS+=("--repos-only") ;;
|
|
191
|
+
--resume) LIST_FLAGS+=("--sessions-only") ;;
|
|
192
|
+
--refresh) DO_REFRESH=1 ;;
|
|
193
|
+
--no-scan) NO_SCAN=1 ;;
|
|
194
|
+
--help|-h) print_help; exit 0 ;;
|
|
195
|
+
--version|-v) echo "ccs v${VERSION}"; exit 0 ;;
|
|
196
|
+
*) PASS_ARGS+=("$arg") ;;
|
|
197
|
+
esac
|
|
198
|
+
done
|
|
199
|
+
|
|
200
|
+
check_deps
|
|
201
|
+
|
|
202
|
+
# ── Pre-scan ─────────────────────────────────────────────────────────────────
|
|
203
|
+
if (( DO_REFRESH )); then
|
|
204
|
+
"${TSX}" "${CCS_SCAN}" --force --quiet || {
|
|
205
|
+
echo "[ccs] refresh failed, continuing with stale DB" >&2
|
|
206
|
+
}
|
|
207
|
+
elif (( ! NO_SCAN )); then
|
|
208
|
+
"${TSX}" "${CCS_SCAN}" --quiet || {
|
|
209
|
+
echo "[ccs] scan failed, continuing with stale DB" >&2
|
|
210
|
+
}
|
|
211
|
+
fi
|
|
212
|
+
|
|
213
|
+
# ── fzf bindings ─────────────────────────────────────────────────────────────
|
|
214
|
+
# Row layout (tab-separated columns):
|
|
215
|
+
# 1: LABEL 2: DESCRIPTION 3: BADGES 4: KIND:KEY 5: CWD 6: COMMAND
|
|
216
|
+
|
|
217
|
+
LIST_ARGS_STR=""
|
|
218
|
+
for f in "${LIST_FLAGS[@]+"${LIST_FLAGS[@]}"}"; do
|
|
219
|
+
LIST_ARGS_STR+=" ${f}"
|
|
220
|
+
done
|
|
221
|
+
|
|
222
|
+
CLIP_BINDS=()
|
|
223
|
+
if [[ -n "$CLIP_CMD" ]]; then
|
|
224
|
+
# ctrl-y: build "cd CWD && COMMAND [--resume UUID]" depending on kind ({4})
|
|
225
|
+
# Keep fzf open (no +abort) and briefly flash a header confirming the copy.
|
|
226
|
+
# cwd/uuid are %q-quoted: the clipboard line gets pasted into a live shell,
|
|
227
|
+
# so a tainted cwd must arrive as an inert literal, never as syntax (audit
|
|
228
|
+
# H-1 defense-in-depth — intake sanitization in ccs-scan is the first gate).
|
|
229
|
+
CLIP_BINDS+=(--bind "ctrl-y:execute-silent(
|
|
230
|
+
kk={4}; cwd={5}; cmd={6};
|
|
231
|
+
case \"\$kk\" in
|
|
232
|
+
resume:*) uuid=\${kk#resume:}; printf 'cd %q && %s --resume %q' \"\$cwd\" \"\$cmd\" \"\$uuid\" | ${CLIP_CMD} ;;
|
|
233
|
+
new:*) printf 'cd %q && %s' \"\$cwd\" \"\$cmd\" | ${CLIP_CMD} ;;
|
|
234
|
+
esac
|
|
235
|
+
)+change-header(📋 Copied command to clipboard — ⏎: launch | ^Y: copy again)")
|
|
236
|
+
# ctrl-i: copy UUID (resume) or repo path (new)
|
|
237
|
+
CLIP_BINDS+=(--bind "ctrl-i:execute-silent(
|
|
238
|
+
kk={4};
|
|
239
|
+
case \"\$kk\" in
|
|
240
|
+
resume:*) printf '%s' \"\${kk#resume:}\" | ${CLIP_CMD} ;;
|
|
241
|
+
new:*) printf '%s' {5} | ${CLIP_CMD} ;;
|
|
242
|
+
esac
|
|
243
|
+
)+change-header(📋 Copied ID/path to clipboard — ⏎: launch | ^I: copy again)")
|
|
244
|
+
fi
|
|
245
|
+
|
|
246
|
+
# ctrl-d: delete session (resume only). Repo (new) rows cannot be deleted
|
|
247
|
+
# from ccs — repos.yml is the source of truth; edit it directly to remove.
|
|
248
|
+
DELETE_BIND=(--bind "ctrl-d:execute(
|
|
249
|
+
kk={4};
|
|
250
|
+
case \"\$kk\" in
|
|
251
|
+
resume:*) bash '${CCS_DELETE}' \"\${kk#resume:}\" ;;
|
|
252
|
+
new:*) echo '[ccs] Repos can only be removed by editing ~/.config/ccs/repos.yml (press any key)'; read -r -n1 _ ;;
|
|
253
|
+
esac
|
|
254
|
+
)+reload('${TSX}' '${CCS_LIST}'${LIST_ARGS_STR})")
|
|
255
|
+
|
|
256
|
+
# ctrl-r: force refresh
|
|
257
|
+
REFRESH_BIND=(--bind "ctrl-r:execute('${TSX}' '${CCS_SCAN}' --force --quiet)+reload('${TSX}' '${CCS_LIST}'${LIST_ARGS_STR})")
|
|
258
|
+
|
|
259
|
+
# ── Header (adaptive to terminal width) ──────────────────────────────────────
|
|
260
|
+
# Note: ^D deletes SESSIONS only; repos are managed via ~/.config/ccs/repos.yml
|
|
261
|
+
COLS=$(tput cols 2>/dev/null || echo 80)
|
|
262
|
+
if (( COLS >= 80 )); then
|
|
263
|
+
HEADER=$'Enter: launch | ^Y: copy cmd | ^I: copy ID/path | ^R: refresh\n^D: del session (edit repos.yml to remove a repo) | Esc: cancel'
|
|
264
|
+
else
|
|
265
|
+
HEADER=$'⏎launch ^Y:cmd ^I:ID ^R:refresh\n^D:del[resume] Esc:quit'
|
|
266
|
+
fi
|
|
267
|
+
|
|
268
|
+
# ── Launch fzf ───────────────────────────────────────────────────────────────
|
|
269
|
+
SELECTED=$("${TSX}" "${CCS_LIST}" "${LIST_FLAGS[@]+"${LIST_FLAGS[@]}"}" | fzf \
|
|
270
|
+
--ansi \
|
|
271
|
+
--delimiter=$'\t' \
|
|
272
|
+
--with-nth=1..3 \
|
|
273
|
+
--header="${HEADER}" \
|
|
274
|
+
--preview="'${TSX}' '${CCS_PREVIEW}' {4} {5}" \
|
|
275
|
+
--preview-window='right:55%:wrap' \
|
|
276
|
+
"${CLIP_BINDS[@]+"${CLIP_BINDS[@]}"}" \
|
|
277
|
+
"${DELETE_BIND[@]}" \
|
|
278
|
+
"${REFRESH_BIND[@]}" \
|
|
279
|
+
--height='80%' \
|
|
280
|
+
--layout=reverse \
|
|
281
|
+
--border \
|
|
282
|
+
--prompt='ccs> ' \
|
|
283
|
+
--tabstop=4 \
|
|
284
|
+
) || exit 0
|
|
285
|
+
|
|
286
|
+
[[ -z "$SELECTED" ]] && exit 0
|
|
287
|
+
|
|
288
|
+
# ── Parse selected row ───────────────────────────────────────────────────────
|
|
289
|
+
# 6-col TSV from ccs-list.ts: LABEL, DESCRIPTION, BADGES, KIND:KEY, CWD, COMMAND.
|
|
290
|
+
# Use IFS-based read so values are never resplit by shell word-splitting
|
|
291
|
+
# (H6 fix). The three `_` placeholders skip columns 1-3.
|
|
292
|
+
IFS=$'\t' read -r _ _ _ KIND_KEY ROW_CWD ROW_CMD <<< "$SELECTED"
|
|
293
|
+
|
|
294
|
+
if [[ -z "$KIND_KEY" ]]; then
|
|
295
|
+
echo "❌ Could not parse selection" >&2
|
|
296
|
+
exit 1
|
|
297
|
+
fi
|
|
298
|
+
|
|
299
|
+
KIND="${KIND_KEY%%:*}"
|
|
300
|
+
KEY="${KIND_KEY#*:}"
|
|
301
|
+
|
|
302
|
+
# Separator row is a non-actionable divider (empty ROW_CWD/ROW_CMD by design).
|
|
303
|
+
# Short-circuit here so the downstream ROW_CMD empty-check doesn't misfire.
|
|
304
|
+
[[ "$KIND" == "separator" ]] && exit 0
|
|
305
|
+
|
|
306
|
+
if [[ -z "$ROW_CMD" ]]; then
|
|
307
|
+
echo "❌ Could not parse selection" >&2
|
|
308
|
+
exit 1
|
|
309
|
+
fi
|
|
310
|
+
|
|
311
|
+
# ── Per-row command is authoritative ─────────────────────────────────────────
|
|
312
|
+
# CCS_CMD env is resolved by ccs-config.ts as a fallback for repos without
|
|
313
|
+
# explicit `command:` (priority: repos[].command > defaults.command > CCS_CMD > "claude").
|
|
314
|
+
# Do NOT override here — that would break special-command repos like Strapi.
|
|
315
|
+
|
|
316
|
+
# ── Validation ───────────────────────────────────────────────────────────────
|
|
317
|
+
case "$KIND" in
|
|
318
|
+
resume)
|
|
319
|
+
if [[ ! "$KEY" =~ ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ ]]; then
|
|
320
|
+
echo "❌ Invalid session ID format: ${KEY}" >&2
|
|
321
|
+
exit 1
|
|
322
|
+
fi
|
|
323
|
+
;;
|
|
324
|
+
new)
|
|
325
|
+
# Reject shell metacharacters, quotes/backslash AND control chars
|
|
326
|
+
# (incl. ESC — audit NEW-2). Mirrors the SHELL_METACHARS policy that
|
|
327
|
+
# ccs-config.ts enforces on repo names at load time; this is the
|
|
328
|
+
# defense-in-depth copy for rows coming back out of fzf.
|
|
329
|
+
if [[ "$KEY" =~ [\;\&\|\<\>\$\`\"\'\\] || "$KEY" =~ [[:cntrl:]] ]]; then
|
|
330
|
+
echo "❌ Invalid repo name (shell metacharacters/control chars): ${KEY}" >&2
|
|
331
|
+
exit 1
|
|
332
|
+
fi
|
|
333
|
+
;;
|
|
334
|
+
separator)
|
|
335
|
+
# Defensive: separator should already be short-circuited above. Kept as
|
|
336
|
+
# a safety net in case row-parse order ever changes.
|
|
337
|
+
exit 0
|
|
338
|
+
;;
|
|
339
|
+
*)
|
|
340
|
+
echo "❌ Unknown row kind: ${KIND}" >&2
|
|
341
|
+
exit 1
|
|
342
|
+
;;
|
|
343
|
+
esac
|
|
344
|
+
|
|
345
|
+
# Path validation (traversal prevention). Must be $HOME itself or a child
|
|
346
|
+
# path ("$HOME"/*): a bare prefix match would also accept sibling dirs like
|
|
347
|
+
# /Users/daiskeEVIL (audit L-1).
|
|
348
|
+
if [[ "$ROW_CWD" != "$HOME" && "$ROW_CWD" != "$HOME"/* ]]; then
|
|
349
|
+
echo "⚠️ cwd is outside \$HOME: ${ROW_CWD}" >&2
|
|
350
|
+
echo "Launching in current directory instead." >&2
|
|
351
|
+
ROW_CWD="."
|
|
352
|
+
fi
|
|
353
|
+
|
|
354
|
+
# Note: shell metachars in ROW_CMD are hard-rejected at config-load time by
|
|
355
|
+
# bin/ccs-config.ts (SHELL_METACHARS regex covers `;&|<>$`"'\` + control chars).
|
|
356
|
+
# The informational rm/sudo/; warning that used to live here was removed in
|
|
357
|
+
# Phase 6 because the metachar rejection is now the authoritative guard.
|
|
358
|
+
|
|
359
|
+
# ── cd ───────────────────────────────────────────────────────────────────────
|
|
360
|
+
if ! cd "$ROW_CWD" 2>/dev/null; then
|
|
361
|
+
echo "⚠️ Directory ${ROW_CWD} not found. Launching in $(pwd) instead." >&2
|
|
362
|
+
fi
|
|
363
|
+
|
|
364
|
+
# ── Execute ──────────────────────────────────────────────────────────────────
|
|
365
|
+
# Direct invocation (NOT exec) so shell functions / aliases like `opr` work.
|
|
366
|
+
# shellcheck disable=SC2086
|
|
367
|
+
case "$KIND" in
|
|
368
|
+
resume)
|
|
369
|
+
echo "🔄 Resuming session ${KEY} in $(pwd)..."
|
|
370
|
+
${ROW_CMD} --resume "${KEY}" "${PASS_ARGS[@]+"${PASS_ARGS[@]}"}"
|
|
371
|
+
;;
|
|
372
|
+
new)
|
|
373
|
+
echo "🚀 Launching ${KEY} in $(pwd)..."
|
|
374
|
+
${ROW_CMD} "${PASS_ARGS[@]+"${PASS_ARGS[@]}"}"
|
|
375
|
+
;;
|
|
376
|
+
esac
|