cue-ai 0.3.0 → 0.4.0

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.
@@ -0,0 +1,302 @@
1
+ /**
2
+ * Agent adapters — materialize skills + MCPs for any AI coding agent.
3
+ *
4
+ * Each adapter knows how to write skills and MCP configs in the format
5
+ * that a specific agent expects.
6
+ */
7
+
8
+ import { writeFileSync, mkdirSync, existsSync, readFileSync, symlinkSync } from "node:fs";
9
+ import { join, resolve } from "node:path";
10
+ import { homedir } from "node:os";
11
+ import { spawnSync } from "node:child_process";
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Interface
15
+ // ---------------------------------------------------------------------------
16
+
17
+ export interface AgentAdapter {
18
+ /** Agent identifier (used in profile.yaml agents field) */
19
+ id: string;
20
+ /** Human-readable name */
21
+ name: string;
22
+ /** Where this agent reads its config from */
23
+ configDir(): string;
24
+ /** Write skills as the agent's rules/instructions file */
25
+ writeSkills(skills: { id: string; content: string }[], targetDir: string): void;
26
+ /** Write MCP server configs in the agent's format */
27
+ writeMcps(mcps: Record<string, unknown>, targetDir: string): void;
28
+ /** Try to find the agent binary on PATH */
29
+ detectBinary(): string | null;
30
+ }
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Helpers
34
+ // ---------------------------------------------------------------------------
35
+
36
+ function findBinary(name: string): string | null {
37
+ const res = spawnSync("which", [name], { encoding: "utf8" });
38
+ return res.status === 0 ? res.stdout.trim() : null;
39
+ }
40
+
41
+ function concatSkills(skills: { id: string; content: string }[]): string {
42
+ return skills.map(s => `<!-- skill: ${s.id} -->\n${s.content}`).join("\n\n---\n\n");
43
+ }
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Claude Code adapter (existing behavior)
47
+ // ---------------------------------------------------------------------------
48
+
49
+ export const claudeCode: AgentAdapter = {
50
+ id: "claude-code",
51
+ name: "Claude Code",
52
+ configDir: () => process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), ".claude"),
53
+ writeSkills(skills, targetDir) {
54
+ const skillsDir = join(targetDir, "skills");
55
+ mkdirSync(skillsDir, { recursive: true });
56
+ // Skills are symlinked individually (handled by materializer)
57
+ },
58
+ writeMcps(mcps, targetDir) {
59
+ const settingsPath = join(targetDir, "settings.json");
60
+ let settings: Record<string, unknown> = {};
61
+ if (existsSync(settingsPath)) {
62
+ try { settings = JSON.parse(readFileSync(settingsPath, "utf8")); } catch {}
63
+ }
64
+ settings.mcpServers = { ...(settings.mcpServers as Record<string, unknown> ?? {}), ...mcps };
65
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
66
+ },
67
+ detectBinary: () => findBinary("claude"),
68
+ };
69
+
70
+ // ---------------------------------------------------------------------------
71
+ // Codex adapter (existing behavior)
72
+ // ---------------------------------------------------------------------------
73
+
74
+ export const codex: AgentAdapter = {
75
+ id: "codex",
76
+ name: "Codex",
77
+ configDir: () => process.env.CODEX_HOME ?? join(homedir(), ".codex"),
78
+ writeSkills(skills, targetDir) {
79
+ const skillsDir = join(targetDir, "skills");
80
+ mkdirSync(skillsDir, { recursive: true });
81
+ },
82
+ writeMcps(mcps, targetDir) {
83
+ // Codex uses config.toml
84
+ const lines: string[] = [];
85
+ for (const [id, val] of Object.entries(mcps)) {
86
+ lines.push(`[mcp_servers.${id}]`);
87
+ for (const [k, v] of Object.entries(val as Record<string, unknown>)) {
88
+ lines.push(`${k} = ${JSON.stringify(v)}`);
89
+ }
90
+ lines.push("");
91
+ }
92
+ writeFileSync(join(targetDir, "config.toml"), lines.join("\n"));
93
+ },
94
+ detectBinary: () => findBinary("codex"),
95
+ };
96
+
97
+ // ---------------------------------------------------------------------------
98
+ // Cursor adapter
99
+ // ---------------------------------------------------------------------------
100
+
101
+ export const cursor: AgentAdapter = {
102
+ id: "cursor",
103
+ name: "Cursor",
104
+ configDir: () => process.cwd(), // project-local
105
+ writeSkills(skills, targetDir) {
106
+ // Cursor reads .cursorrules in project root
107
+ const content = concatSkills(skills);
108
+ writeFileSync(join(targetDir, ".cursorrules"), content);
109
+ },
110
+ writeMcps(mcps, targetDir) {
111
+ // Cursor reads .cursor/mcp.json
112
+ const cursorDir = join(targetDir, ".cursor");
113
+ mkdirSync(cursorDir, { recursive: true });
114
+ writeFileSync(join(cursorDir, "mcp.json"), JSON.stringify({ mcpServers: mcps }, null, 2));
115
+ },
116
+ detectBinary: () => findBinary("cursor"),
117
+ };
118
+
119
+ // ---------------------------------------------------------------------------
120
+ // Cline adapter
121
+ // ---------------------------------------------------------------------------
122
+
123
+ export const cline: AgentAdapter = {
124
+ id: "cline",
125
+ name: "Cline",
126
+ configDir: () => process.cwd(),
127
+ writeSkills(skills, targetDir) {
128
+ // Cline reads .clinerules in project root
129
+ const content = concatSkills(skills);
130
+ writeFileSync(join(targetDir, ".clinerules"), content);
131
+ },
132
+ writeMcps(mcps, targetDir) {
133
+ // Cline reads cline_mcp_settings.json in project root
134
+ writeFileSync(join(targetDir, "cline_mcp_settings.json"), JSON.stringify({ mcpServers: mcps }, null, 2));
135
+ },
136
+ detectBinary: () => null, // VS Code extension, no binary
137
+ };
138
+
139
+ // ---------------------------------------------------------------------------
140
+ // Windsurf adapter
141
+ // ---------------------------------------------------------------------------
142
+
143
+ export const windsurf: AgentAdapter = {
144
+ id: "windsurf",
145
+ name: "Windsurf",
146
+ configDir: () => process.cwd(),
147
+ writeSkills(skills, targetDir) {
148
+ const content = concatSkills(skills);
149
+ writeFileSync(join(targetDir, ".windsurfrules"), content);
150
+ },
151
+ writeMcps(mcps, targetDir) {
152
+ // Windsurf uses same format as Cursor
153
+ const dir = join(targetDir, ".windsurf");
154
+ mkdirSync(dir, { recursive: true });
155
+ writeFileSync(join(dir, "mcp.json"), JSON.stringify({ mcpServers: mcps }, null, 2));
156
+ },
157
+ detectBinary: () => findBinary("windsurf"),
158
+ };
159
+
160
+ // ---------------------------------------------------------------------------
161
+ // Gemini CLI adapter
162
+ // ---------------------------------------------------------------------------
163
+
164
+ export const gemini: AgentAdapter = {
165
+ id: "gemini",
166
+ name: "Gemini CLI",
167
+ configDir: () => join(homedir(), ".gemini"),
168
+ writeSkills(skills, targetDir) {
169
+ // Gemini reads skills from ~/.gemini/skills/ as individual files
170
+ const skillsDir = join(targetDir, "skills");
171
+ mkdirSync(skillsDir, { recursive: true });
172
+ for (const s of skills) {
173
+ const slug = s.id.split("/").pop() ?? s.id;
174
+ writeFileSync(join(skillsDir, `${slug}.md`), s.content);
175
+ }
176
+ },
177
+ writeMcps(mcps, targetDir) {
178
+ // Gemini uses settings.json with mcpServers
179
+ const settingsPath = join(targetDir, "settings.json");
180
+ let settings: Record<string, unknown> = {};
181
+ if (existsSync(settingsPath)) {
182
+ try { settings = JSON.parse(readFileSync(settingsPath, "utf8")); } catch {}
183
+ }
184
+ settings.mcpServers = mcps;
185
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
186
+ },
187
+ detectBinary: () => findBinary("gemini"),
188
+ };
189
+
190
+ // ---------------------------------------------------------------------------
191
+ // GitHub Copilot adapter
192
+ // ---------------------------------------------------------------------------
193
+
194
+ export const copilot: AgentAdapter = {
195
+ id: "copilot",
196
+ name: "GitHub Copilot",
197
+ configDir: () => process.cwd(),
198
+ writeSkills(skills, targetDir) {
199
+ // Copilot reads .github/copilot-instructions.md
200
+ const ghDir = join(targetDir, ".github");
201
+ mkdirSync(ghDir, { recursive: true });
202
+ const content = concatSkills(skills);
203
+ writeFileSync(join(ghDir, "copilot-instructions.md"), content);
204
+ },
205
+ writeMcps(mcps, targetDir) {
206
+ // Copilot uses .vscode/mcp.json or .github/mcp.json
207
+ const ghDir = join(targetDir, ".github");
208
+ mkdirSync(ghDir, { recursive: true });
209
+ writeFileSync(join(ghDir, "mcp.json"), JSON.stringify({ servers: mcps }, null, 2));
210
+ },
211
+ detectBinary: () => null, // VS Code extension
212
+ };
213
+
214
+ // ---------------------------------------------------------------------------
215
+ // Roo Code adapter
216
+ // ---------------------------------------------------------------------------
217
+
218
+ export const roo: AgentAdapter = {
219
+ id: "roo",
220
+ name: "Roo Code",
221
+ configDir: () => process.cwd(),
222
+ writeSkills(skills, targetDir) {
223
+ // Roo reads .roo/rules/
224
+ const rulesDir = join(targetDir, ".roo", "rules");
225
+ mkdirSync(rulesDir, { recursive: true });
226
+ for (const s of skills) {
227
+ const slug = s.id.split("/").pop() ?? s.id;
228
+ writeFileSync(join(rulesDir, `${slug}.md`), s.content);
229
+ }
230
+ },
231
+ writeMcps(mcps, targetDir) {
232
+ // Roo uses .roo/mcp.json
233
+ const rooDir = join(targetDir, ".roo");
234
+ mkdirSync(rooDir, { recursive: true });
235
+ writeFileSync(join(rooDir, "mcp.json"), JSON.stringify({ mcpServers: mcps }, null, 2));
236
+ },
237
+ detectBinary: () => null, // VS Code extension
238
+ };
239
+
240
+ // ---------------------------------------------------------------------------
241
+ // Amp adapter
242
+ // ---------------------------------------------------------------------------
243
+
244
+ export const amp: AgentAdapter = {
245
+ id: "amp",
246
+ name: "Amp",
247
+ configDir: () => process.cwd(),
248
+ writeSkills(skills, targetDir) {
249
+ // Amp reads AGENTS.md in project root
250
+ const content = concatSkills(skills);
251
+ writeFileSync(join(targetDir, "AGENTS.md"), content);
252
+ },
253
+ writeMcps(mcps, targetDir) {
254
+ // Amp uses .amp/mcp.json
255
+ const ampDir = join(targetDir, ".amp");
256
+ mkdirSync(ampDir, { recursive: true });
257
+ writeFileSync(join(ampDir, "mcp.json"), JSON.stringify({ mcpServers: mcps }, null, 2));
258
+ },
259
+ detectBinary: () => findBinary("amp"),
260
+ };
261
+
262
+ // ---------------------------------------------------------------------------
263
+ // Aider adapter
264
+ // ---------------------------------------------------------------------------
265
+
266
+ export const aider: AgentAdapter = {
267
+ id: "aider",
268
+ name: "Aider",
269
+ configDir: () => process.cwd(),
270
+ writeSkills(skills, targetDir) {
271
+ // Aider reads .aider.conf.yml conventions
272
+ const content = concatSkills(skills);
273
+ writeFileSync(join(targetDir, ".aider.conventions.md"), content);
274
+ },
275
+ writeMcps(_mcps, _targetDir) {
276
+ // Aider doesn't support MCP
277
+ },
278
+ detectBinary: () => findBinary("aider"),
279
+ };
280
+
281
+ // ---------------------------------------------------------------------------
282
+ // Registry
283
+ // ---------------------------------------------------------------------------
284
+
285
+ export const ADAPTERS: Record<string, AgentAdapter> = {
286
+ "claude-code": claudeCode,
287
+ "codex": codex,
288
+ "cursor": cursor,
289
+ "cline": cline,
290
+ "windsurf": windsurf,
291
+ "gemini": gemini,
292
+ "copilot": copilot,
293
+ "roo": roo,
294
+ "amp": amp,
295
+ "aider": aider,
296
+ };
297
+
298
+ export const AGENT_IDS = Object.keys(ADAPTERS);
299
+
300
+ export function getAdapter(id: string): AgentAdapter | null {
301
+ return ADAPTERS[id] ?? null;
302
+ }
@@ -339,22 +339,25 @@ export function probeKittyTerminal(timeoutMs = 100): Promise<boolean> {
339
339
  * Use this from the launch hot path; `isKittyTerminal()` is the env-only
340
340
  * sync version kept around for callers that can't await.
341
341
  *
342
- * The probe is the only reliable signal env vars like KITTY_WINDOW_ID can
343
- * leak into non-Kitty terminals (e.g. GNOME Terminal launched from a Kitty
344
- * session). When the probe is available (TTY), we always use it.
342
+ * Trust strong env signals (KITTY_WINDOW_ID, TERM=xterm-kitty) directly
343
+ * these are set by Kitty itself and reliable. Only probe when signals are
344
+ * ambiguous (e.g. inside tmux with no Kitty env vars).
345
345
  */
346
346
  export async function detectKittyTerminal(timeoutMs = 100): Promise<boolean> {
347
347
  if (process.env.CUE_DISABLE_KITTY_IMAGES === "1") return false;
348
+ if (process.env.CUE_KITTY === "1") return true;
349
+
350
+ // Strong signals — trust directly (Kitty sets these for child processes)
351
+ if (process.env.TERM === "xterm-kitty") return true;
352
+ if (process.env.KITTY_WINDOW_ID) return true;
353
+ if (process.env.KITTY_PID) return true;
348
354
 
349
- // If we can probe (TTY), always probe — it's the only reliable signal.
350
- // Env vars like KITTY_WINDOW_ID leak through tmux/child terminals.
355
+ // Weak/ambiguous probe the terminal
351
356
  if (process.stdout.isTTY && process.stdin.isTTY) {
352
357
  return probeKittyTerminal(timeoutMs);
353
358
  }
354
359
 
355
- // Non-TTY fallback: trust env hints
356
- if (process.env.CUE_KITTY === "1") return true;
357
- if (process.env.TERM === "xterm-kitty") return true;
358
- if (process.env.KITTY_WINDOW_ID) return true;
360
+ // Non-TTY fallback
361
+ if (process.env.TERM_PROGRAM === "kitty") return true;
359
362
  return false;
360
363
  }
@@ -195,6 +195,19 @@ export async function materializeRuntime(input: MaterializeInput): Promise<Mater
195
195
  }
196
196
 
197
197
  // 6. Atomic swap: rm -rf old, rename tmp.
198
+ // Preserve .claude.json and backups/ — Claude Code writes session state here
199
+ // and resume depends on it surviving across rematerializations.
200
+ const preserveFiles = [".claude.json", "backups"];
201
+ for (const name of preserveFiles) {
202
+ const oldPath = join(runtimeDir, name);
203
+ const newPath = join(tmpDir, name);
204
+ try {
205
+ const st = await lstat(oldPath);
206
+ if (st.isFile() || st.isDirectory()) {
207
+ await rename(oldPath, newPath);
208
+ }
209
+ } catch { /* doesn't exist — skip */ }
210
+ }
198
211
  await rm(runtimeDir, { recursive: true, force: true });
199
212
  await rename(tmpDir, runtimeDir);
200
213
 
@@ -255,9 +268,20 @@ async function overlaySourceState(targetDir: string, sourceDir: string): Promise
255
268
  await rm(targetPath, { force: true });
256
269
  } catch { continue; }
257
270
  }
258
- try {
259
- await symlink(sourcePath, targetPath);
260
- } catch { /* race or permission skip silently */ }
271
+
272
+ // .credentials.json must be COPIED (not symlinked) because Claude Code
273
+ // refreshes tokens via atomic write (write tmp rename), which replaces
274
+ // symlinks with regular files, leaving the source stale.
275
+ if (name === ".credentials.json") {
276
+ const { copyFile } = await import("node:fs/promises");
277
+ try {
278
+ await copyFile(sourcePath, targetPath);
279
+ } catch { /* skip */ }
280
+ } else {
281
+ try {
282
+ await symlink(sourcePath, targetPath);
283
+ } catch { /* race or permission — skip silently */ }
284
+ }
261
285
  }
262
286
  }
263
287
 
package/bin/medusa-dev DELETED
@@ -1,240 +0,0 @@
1
- #!/usr/bin/env bash
2
- # medusa-dev — run multiple Medusa shops side-by-side without port collisions.
3
- #
4
- # Reads ~/Documents/medusa-shops/.dev-ports.yaml as the source of truth.
5
- # Each shop gets a deterministic backend port + storefront port from that file.
6
- #
7
- # Usage:
8
- # medusa-dev list # show registry + which ports are live now
9
- # medusa-dev start <shop> [back|front|both] # default: both
10
- # medusa-dev stop <shop> [back|front|both]
11
- # medusa-dev tail <shop> [back|front] # tail the log
12
- # medusa-dev status # what's running across all shops
13
- # medusa-dev cors-fix <shop> # set STORE_CORS/AUTH_CORS to a regex
14
- # # that allows any http://localhost:PORT
15
- #
16
- # Logs go to ~/.cache/medusa-dev/<shop>.<back|front>.log
17
- # Pidfiles to ~/.cache/medusa-dev/<shop>.<back|front>.pid
18
-
19
- set -euo pipefail
20
-
21
- REGISTRY="${MEDUSA_DEV_REGISTRY:-$HOME/Documents/medusa-shops/.dev-ports.yaml}"
22
- WORKSPACE="${MEDUSA_DEV_WORKSPACE:-$HOME/Documents}"
23
- CACHE="$HOME/.cache/medusa-dev"
24
- mkdir -p "$CACHE"
25
-
26
- err() { printf '\033[31m%s\033[0m\n' "$*" >&2; }
27
- info() { printf '\033[36m%s\033[0m\n' "$*" >&2; }
28
- ok() { printf '\033[32m%s\033[0m\n' "$*" >&2; }
29
-
30
- # Parse the YAML using inline python3 (always available on the user's machine).
31
- # Returns a TSV: shop<TAB>path<TAB>backend_port<TAB>storefront_port<TAB>framework<TAB>notes
32
- registry_tsv() {
33
- python3 - "$REGISTRY" <<'PY'
34
- import sys, re
35
- path = sys.argv[1]
36
- with open(path) as f: txt = f.read()
37
-
38
- # Tiny YAML walker — no PyYAML dependency. Handles the flat 2-level shape
39
- # only: `shops:` then `<name>:` with indented `key: value` children.
40
- out, cur = [], None
41
- def flush():
42
- if cur is not None:
43
- out.append((cur["name"], cur.get("path",""), cur.get("backend",""),
44
- cur.get("storefront",""), cur.get("framework",""),
45
- cur.get("notes","").strip('"')))
46
- for raw in txt.splitlines():
47
- line = raw.rstrip()
48
- if not line.strip() or line.lstrip().startswith("#"): continue
49
- if line == "shops:": continue
50
- if re.match(r"^ [A-Za-z0-9_.-]+:\s*$", line):
51
- flush()
52
- cur = {"name": line.strip().rstrip(":")}
53
- elif cur is not None and re.match(r"^ [a-z_]+:", line):
54
- k,_,v = line.strip().partition(":")
55
- cur[k.strip()] = v.strip()
56
- flush()
57
- for row in out:
58
- print("\t".join(str(c) for c in row))
59
- PY
60
- }
61
-
62
- # Look up a single shop. Echoes a TSV line, or returns nonzero if not found.
63
- shop_row() {
64
- local name="$1"
65
- registry_tsv | awk -F'\t' -v n="$name" '$1==n {print; found=1} END{exit !found}'
66
- }
67
-
68
- # Process status — is the pidfile alive?
69
- is_alive() {
70
- local pidfile="$1"
71
- [[ -f "$pidfile" ]] || return 1
72
- local pid; pid="$(cat "$pidfile" 2>/dev/null || true)"
73
- [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null
74
- }
75
-
76
- # Is a port currently held by *any* process?
77
- port_held() {
78
- ss -lntH "sport = :$1" 2>/dev/null | grep -q LISTEN
79
- }
80
-
81
- cmd_list() {
82
- printf "%-16s %-7s %-7s %-7s %-7s %s\n" SHOP BACKEND STATE FRONT STATE PATH
83
- while IFS=$'\t' read -r name path back front fw notes; do
84
- [[ -z "${name:-}" ]] && continue
85
- local bstate fstate
86
- if is_alive "$CACHE/$name.back.pid"; then bstate="up"
87
- elif port_held "$back"; then bstate="busy"
88
- else bstate="-"; fi
89
- if is_alive "$CACHE/$name.front.pid"; then fstate="up"
90
- elif port_held "$front"; then fstate="busy"
91
- else fstate="-"; fi
92
- printf "%-16s %-7s %-7s %-7s %-7s %s\n" "$name" "$back" "$bstate" "$front" "$fstate" "$path"
93
- done < <(registry_tsv)
94
- }
95
-
96
- cmd_status() { cmd_list; }
97
-
98
- # Start one role of one shop.
99
- start_one() {
100
- local shop="$1" role="$2" path="$3" port="$4"
101
- local cwd="$WORKSPACE/$path"
102
- local pidfile="$CACHE/$shop.$role.pid"
103
- local logfile="$CACHE/$shop.$role.log"
104
-
105
- if is_alive "$pidfile"; then
106
- info "$shop.$role already running (pid $(cat "$pidfile"))."
107
- return 0
108
- fi
109
- if port_held "$port"; then
110
- local owner
111
- owner=$(ss -lntpH "sport = :$port" 2>/dev/null | awk -F'"' '{print $2}' | head -1)
112
- err "Port $port already in use by '$owner' — won't collide. Stop it or change the registry."
113
- return 1
114
- fi
115
- if [[ ! -d "$cwd" ]]; then
116
- err "Path missing: $cwd"
117
- return 1
118
- fi
119
-
120
- local script
121
- case "$role" in
122
- back) script="--filter backend dev" ;;
123
- front) script="--filter storefront dev" ;;
124
- *) err "unknown role: $role"; return 1 ;;
125
- esac
126
-
127
- info "Starting $shop.$role on port $port → $cwd"
128
- ( cd "$cwd" && PORT="$port" nohup pnpm $script > "$logfile" 2>&1 & echo $! > "$pidfile" )
129
- ok "$shop.$role started (pid $(cat "$pidfile"), log: $logfile)"
130
- }
131
-
132
- cmd_start() {
133
- local shop="${1:?shop name required}" which="${2:-both}"
134
- local row; row="$(shop_row "$shop")" || { err "unknown shop: $shop"; exit 1; }
135
- IFS=$'\t' read -r name path back front _fw _notes <<<"$row"
136
- case "$which" in
137
- back) start_one "$shop" back "$path" "$back" ;;
138
- front) start_one "$shop" front "$path" "$front" ;;
139
- both) start_one "$shop" back "$path" "$back" || true
140
- start_one "$shop" front "$path" "$front" || true ;;
141
- *) err "expected back|front|both, got: $which"; exit 1 ;;
142
- esac
143
- }
144
-
145
- stop_one() {
146
- local shop="$1" role="$2"
147
- local pidfile="$CACHE/$shop.$role.pid"
148
- if is_alive "$pidfile"; then
149
- local pid; pid="$(cat "$pidfile")"
150
- info "Stopping $shop.$role (pid $pid)"
151
- kill "$pid" 2>/dev/null || true
152
- # Give it 5s for graceful, then SIGKILL the whole process group.
153
- for _ in 1 2 3 4 5; do kill -0 "$pid" 2>/dev/null || break; sleep 1; done
154
- if kill -0 "$pid" 2>/dev/null; then
155
- kill -9 -- "-$pid" 2>/dev/null || kill -9 "$pid" 2>/dev/null || true
156
- fi
157
- rm -f "$pidfile"
158
- ok "$shop.$role stopped"
159
- else
160
- info "$shop.$role not running"
161
- rm -f "$pidfile" 2>/dev/null || true
162
- fi
163
- }
164
-
165
- cmd_stop() {
166
- local shop="${1:?shop name required}" which="${2:-both}"
167
- shop_row "$shop" >/dev/null || { err "unknown shop: $shop"; exit 1; }
168
- case "$which" in
169
- back) stop_one "$shop" back ;;
170
- front) stop_one "$shop" front ;;
171
- both) stop_one "$shop" back; stop_one "$shop" front ;;
172
- *) err "expected back|front|both, got: $which"; exit 1 ;;
173
- esac
174
- }
175
-
176
- cmd_tail() {
177
- local shop="${1:?shop name required}" role="${2:-back}"
178
- local logfile="$CACHE/$shop.$role.log"
179
- [[ -f "$logfile" ]] || { err "no log at $logfile"; exit 1; }
180
- exec tail -f "$logfile"
181
- }
182
-
183
- # Loosen CORS in a shop's backend .env so any localhost port is accepted.
184
- cmd_cors_fix() {
185
- local shop="${1:?shop name required}"
186
- local row; row="$(shop_row "$shop")" || { err "unknown shop: $shop"; exit 1; }
187
- IFS=$'\t' read -r name path _back _front _fw _notes <<<"$row"
188
- local envfile="$WORKSPACE/$path/apps/backend/.env"
189
- [[ -f "$envfile" ]] || { err "no backend .env at $envfile"; exit 1; }
190
- cp "$envfile" "$envfile.bak.$(date +%s)"
191
- python3 - "$envfile" <<'PY'
192
- import sys, re
193
- p = sys.argv[1]
194
- regex = "^http://localhost:\\d+$"
195
- with open(p) as f: lines = f.readlines()
196
- keys = ("STORE_CORS","AUTH_CORS","ADMIN_CORS")
197
- seen = {k: False for k in keys}
198
- out = []
199
- for ln in lines:
200
- m = re.match(r"^(STORE_CORS|AUTH_CORS|ADMIN_CORS)=", ln)
201
- if m:
202
- out.append(f"{m.group(1)}={regex}\n")
203
- seen[m.group(1)] = True
204
- else:
205
- out.append(ln)
206
- for k, ok in seen.items():
207
- if not ok:
208
- out.append(f"{k}={regex}\n")
209
- with open(p, "w") as f: f.writelines(out)
210
- print("Updated:", ", ".join(seen.keys()))
211
- PY
212
- ok "Backup saved alongside $envfile.bak.<ts>"
213
- }
214
-
215
- usage() {
216
- cat <<EOF
217
- medusa-dev — run multiple Medusa shops side-by-side on stable per-shop ports.
218
-
219
- medusa-dev list # show registry + live state
220
- medusa-dev status # alias for list
221
- medusa-dev start <shop> [back|front|both]
222
- medusa-dev stop <shop> [back|front|both]
223
- medusa-dev tail <shop> [back|front]
224
- medusa-dev cors-fix <shop> # set CORS keys to localhost regex
225
-
226
- Registry: $REGISTRY
227
- Cache: $CACHE
228
- EOF
229
- }
230
-
231
- case "${1:-list}" in
232
- list|ls) shift; cmd_list "$@" ;;
233
- status) shift; cmd_status "$@" ;;
234
- start|up) shift; cmd_start "$@" ;;
235
- stop|down|kill) shift; cmd_stop "$@" ;;
236
- tail|log|logs) shift; cmd_tail "$@" ;;
237
- cors-fix) shift; cmd_cors_fix "$@" ;;
238
- -h|--help|help) usage ;;
239
- *) err "unknown command: $1"; usage; exit 1 ;;
240
- esac
package/bin/soul DELETED
@@ -1,4 +0,0 @@
1
- #!/usr/bin/env bash
2
- # DEPRECATED — renamed to "cue". This shim forwards all invocations.
3
- echo "soul: renamed to 'cue'. Update your PATH/alias." >&2
4
- exec "$(dirname "$0")/cue" "$@"