@runchr/gstack-antigravity 0.1.0 → 0.1.2
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.
Potentially problematic release.
This version of @runchr/gstack-antigravity might be problematic. Click here for more details.
- package/.agents/skills/gstack/.agents/skills/gstack/SKILL.md +651 -0
- package/.agents/skills/gstack/.agents/skills/gstack-autoplan/SKILL.md +678 -0
- package/.agents/skills/gstack/.agents/skills/gstack-benchmark/SKILL.md +482 -0
- package/.agents/skills/gstack/.agents/skills/gstack-browse/SKILL.md +511 -0
- package/.agents/skills/gstack/.agents/skills/gstack-canary/SKILL.md +486 -0
- package/.agents/skills/gstack/.agents/skills/gstack-careful/SKILL.md +50 -0
- package/.agents/skills/gstack/.agents/skills/gstack-cso/SKILL.md +607 -0
- package/.agents/skills/gstack/.agents/skills/gstack-design-consultation/SKILL.md +615 -0
- package/.agents/skills/gstack/.agents/skills/gstack-design-review/SKILL.md +988 -0
- package/.agents/skills/gstack/.agents/skills/gstack-document-release/SKILL.md +604 -0
- package/.agents/skills/gstack/.agents/skills/gstack-freeze/SKILL.md +67 -0
- package/.agents/skills/gstack/.agents/skills/gstack-guard/SKILL.md +62 -0
- package/.agents/skills/gstack/.agents/skills/gstack-investigate/SKILL.md +415 -0
- package/.agents/skills/gstack/.agents/skills/gstack-land-and-deploy/SKILL.md +873 -0
- package/.agents/skills/gstack/.agents/skills/gstack-office-hours/SKILL.md +986 -0
- package/.agents/skills/gstack/.agents/skills/gstack-plan-ceo-review/SKILL.md +1268 -0
- package/.agents/skills/gstack/.agents/skills/gstack-plan-design-review/SKILL.md +668 -0
- package/.agents/skills/gstack/.agents/skills/gstack-plan-eng-review/SKILL.md +826 -0
- package/.agents/skills/gstack/.agents/skills/gstack-qa/SKILL.md +1006 -0
- package/.agents/skills/gstack/.agents/skills/gstack-qa-only/SKILL.md +626 -0
- package/.agents/skills/gstack/.agents/skills/gstack-retro/SKILL.md +1065 -0
- package/.agents/skills/gstack/.agents/skills/gstack-review/SKILL.md +704 -0
- package/.agents/skills/gstack/.agents/skills/gstack-setup-browser-cookies/SKILL.md +325 -0
- package/.agents/skills/gstack/.agents/skills/gstack-setup-deploy/SKILL.md +450 -0
- package/.agents/skills/gstack/.agents/skills/gstack-ship/SKILL.md +1312 -0
- package/.agents/skills/gstack/.agents/skills/gstack-unfreeze/SKILL.md +36 -0
- package/.agents/skills/gstack/.agents/skills/gstack-upgrade/SKILL.md +220 -0
- package/.agents/skills/gstack/.env.example +5 -0
- package/.agents/skills/gstack/.github/workflows/skill-docs.yml +17 -0
- package/.agents/skills/gstack/AGENTS.md +49 -0
- package/.agents/skills/gstack/ARCHITECTURE.md +359 -0
- package/.agents/skills/gstack/BROWSER.md +271 -0
- package/.agents/skills/gstack/CHANGELOG.md +800 -0
- package/.agents/skills/gstack/CLAUDE.md +284 -0
- package/.agents/skills/gstack/CONTRIBUTING.md +370 -0
- package/.agents/skills/gstack/ETHOS.md +129 -0
- package/.agents/skills/gstack/LICENSE +21 -0
- package/.agents/skills/gstack/README.md +228 -0
- package/.agents/skills/gstack/SKILL.md +657 -0
- package/.agents/skills/gstack/SKILL.md.tmpl +281 -0
- package/.agents/skills/gstack/TODOS.md +564 -0
- package/.agents/skills/gstack/VERSION +1 -0
- package/.agents/skills/gstack/autoplan/SKILL.md +689 -0
- package/.agents/skills/gstack/autoplan/SKILL.md.tmpl +416 -0
- package/.agents/skills/gstack/benchmark/SKILL.md +489 -0
- package/.agents/skills/gstack/benchmark/SKILL.md.tmpl +233 -0
- package/.agents/skills/gstack/bin/dev-setup +68 -0
- package/.agents/skills/gstack/bin/dev-teardown +56 -0
- package/.agents/skills/gstack/bin/gstack-analytics +191 -0
- package/.agents/skills/gstack/bin/gstack-community-dashboard +113 -0
- package/.agents/skills/gstack/bin/gstack-config +38 -0
- package/.agents/skills/gstack/bin/gstack-diff-scope +71 -0
- package/.agents/skills/gstack/bin/gstack-global-discover.ts +591 -0
- package/.agents/skills/gstack/bin/gstack-repo-mode +93 -0
- package/.agents/skills/gstack/bin/gstack-review-log +9 -0
- package/.agents/skills/gstack/bin/gstack-review-read +12 -0
- package/.agents/skills/gstack/bin/gstack-slug +15 -0
- package/.agents/skills/gstack/bin/gstack-telemetry-log +158 -0
- package/.agents/skills/gstack/bin/gstack-telemetry-sync +127 -0
- package/.agents/skills/gstack/bin/gstack-update-check +196 -0
- package/.agents/skills/gstack/browse/SKILL.md +517 -0
- package/.agents/skills/gstack/browse/SKILL.md.tmpl +141 -0
- package/.agents/skills/gstack/browse/bin/find-browse +21 -0
- package/.agents/skills/gstack/browse/bin/remote-slug +14 -0
- package/.agents/skills/gstack/browse/scripts/build-node-server.sh +48 -0
- package/.agents/skills/gstack/browse/src/browser-manager.ts +634 -0
- package/.agents/skills/gstack/browse/src/buffers.ts +137 -0
- package/.agents/skills/gstack/browse/src/bun-polyfill.cjs +109 -0
- package/.agents/skills/gstack/browse/src/cli.ts +420 -0
- package/.agents/skills/gstack/browse/src/commands.ts +111 -0
- package/.agents/skills/gstack/browse/src/config.ts +150 -0
- package/.agents/skills/gstack/browse/src/cookie-import-browser.ts +417 -0
- package/.agents/skills/gstack/browse/src/cookie-picker-routes.ts +207 -0
- package/.agents/skills/gstack/browse/src/cookie-picker-ui.ts +541 -0
- package/.agents/skills/gstack/browse/src/find-browse.ts +61 -0
- package/.agents/skills/gstack/browse/src/meta-commands.ts +269 -0
- package/.agents/skills/gstack/browse/src/platform.ts +17 -0
- package/.agents/skills/gstack/browse/src/read-commands.ts +335 -0
- package/.agents/skills/gstack/browse/src/server.ts +369 -0
- package/.agents/skills/gstack/browse/src/snapshot.ts +398 -0
- package/.agents/skills/gstack/browse/src/url-validation.ts +91 -0
- package/.agents/skills/gstack/browse/src/write-commands.ts +352 -0
- package/.agents/skills/gstack/browse/test/bun-polyfill.test.ts +72 -0
- package/.agents/skills/gstack/browse/test/commands.test.ts +1836 -0
- package/.agents/skills/gstack/browse/test/config.test.ts +250 -0
- package/.agents/skills/gstack/browse/test/cookie-import-browser.test.ts +397 -0
- package/.agents/skills/gstack/browse/test/cookie-picker-routes.test.ts +205 -0
- package/.agents/skills/gstack/browse/test/find-browse.test.ts +50 -0
- package/.agents/skills/gstack/browse/test/fixtures/basic.html +33 -0
- package/.agents/skills/gstack/browse/test/fixtures/cursor-interactive.html +22 -0
- package/.agents/skills/gstack/browse/test/fixtures/dialog.html +15 -0
- package/.agents/skills/gstack/browse/test/fixtures/empty.html +2 -0
- package/.agents/skills/gstack/browse/test/fixtures/forms.html +55 -0
- package/.agents/skills/gstack/browse/test/fixtures/qa-eval-checkout.html +108 -0
- package/.agents/skills/gstack/browse/test/fixtures/qa-eval-spa.html +98 -0
- package/.agents/skills/gstack/browse/test/fixtures/qa-eval.html +51 -0
- package/.agents/skills/gstack/browse/test/fixtures/responsive.html +49 -0
- package/.agents/skills/gstack/browse/test/fixtures/snapshot.html +55 -0
- package/.agents/skills/gstack/browse/test/fixtures/spa.html +24 -0
- package/.agents/skills/gstack/browse/test/fixtures/states.html +17 -0
- package/.agents/skills/gstack/browse/test/fixtures/upload.html +25 -0
- package/.agents/skills/gstack/browse/test/gstack-config.test.ts +125 -0
- package/.agents/skills/gstack/browse/test/gstack-update-check.test.ts +467 -0
- package/.agents/skills/gstack/browse/test/handoff.test.ts +235 -0
- package/.agents/skills/gstack/browse/test/path-validation.test.ts +63 -0
- package/.agents/skills/gstack/browse/test/platform.test.ts +37 -0
- package/.agents/skills/gstack/browse/test/snapshot.test.ts +467 -0
- package/.agents/skills/gstack/browse/test/test-server.ts +57 -0
- package/.agents/skills/gstack/browse/test/url-validation.test.ts +72 -0
- package/.agents/skills/gstack/canary/SKILL.md +493 -0
- package/.agents/skills/gstack/canary/SKILL.md.tmpl +220 -0
- package/.agents/skills/gstack/careful/SKILL.md +59 -0
- package/.agents/skills/gstack/careful/SKILL.md.tmpl +57 -0
- package/.agents/skills/gstack/careful/bin/check-careful.sh +112 -0
- package/.agents/skills/gstack/codex/SKILL.md +677 -0
- package/.agents/skills/gstack/codex/SKILL.md.tmpl +356 -0
- package/.agents/skills/gstack/conductor.json +6 -0
- package/.agents/skills/gstack/cso/SKILL.md +615 -0
- package/.agents/skills/gstack/cso/SKILL.md.tmpl +376 -0
- package/.agents/skills/gstack/design-consultation/SKILL.md +625 -0
- package/.agents/skills/gstack/design-consultation/SKILL.md.tmpl +369 -0
- package/.agents/skills/gstack/design-review/SKILL.md +998 -0
- package/.agents/skills/gstack/design-review/SKILL.md.tmpl +262 -0
- package/.agents/skills/gstack/docs/images/github-2013.png +0 -0
- package/.agents/skills/gstack/docs/images/github-2026.png +0 -0
- package/.agents/skills/gstack/docs/skills.md +877 -0
- package/.agents/skills/gstack/document-release/SKILL.md +613 -0
- package/.agents/skills/gstack/document-release/SKILL.md.tmpl +357 -0
- package/.agents/skills/gstack/freeze/SKILL.md +82 -0
- package/.agents/skills/gstack/freeze/SKILL.md.tmpl +80 -0
- package/.agents/skills/gstack/freeze/bin/check-freeze.sh +68 -0
- package/.agents/skills/gstack/gstack-upgrade/SKILL.md +226 -0
- package/.agents/skills/gstack/gstack-upgrade/SKILL.md.tmpl +224 -0
- package/.agents/skills/gstack/guard/SKILL.md +82 -0
- package/.agents/skills/gstack/guard/SKILL.md.tmpl +80 -0
- package/.agents/skills/gstack/investigate/SKILL.md +435 -0
- package/.agents/skills/gstack/investigate/SKILL.md.tmpl +196 -0
- package/.agents/skills/gstack/land-and-deploy/SKILL.md +880 -0
- package/.agents/skills/gstack/land-and-deploy/SKILL.md.tmpl +575 -0
- package/.agents/skills/gstack/office-hours/SKILL.md +996 -0
- package/.agents/skills/gstack/office-hours/SKILL.md.tmpl +624 -0
- package/.agents/skills/gstack/package.json +55 -0
- package/.agents/skills/gstack/plan-ceo-review/SKILL.md +1277 -0
- package/.agents/skills/gstack/plan-ceo-review/SKILL.md.tmpl +838 -0
- package/.agents/skills/gstack/plan-design-review/SKILL.md +676 -0
- package/.agents/skills/gstack/plan-design-review/SKILL.md.tmpl +314 -0
- package/.agents/skills/gstack/plan-eng-review/SKILL.md +836 -0
- package/.agents/skills/gstack/plan-eng-review/SKILL.md.tmpl +279 -0
- package/.agents/skills/gstack/qa/SKILL.md +1016 -0
- package/.agents/skills/gstack/qa/SKILL.md.tmpl +316 -0
- package/.agents/skills/gstack/qa/references/issue-taxonomy.md +85 -0
- package/.agents/skills/gstack/qa/templates/qa-report-template.md +126 -0
- package/.agents/skills/gstack/qa-only/SKILL.md +633 -0
- package/.agents/skills/gstack/qa-only/SKILL.md.tmpl +101 -0
- package/.agents/skills/gstack/retro/SKILL.md +1072 -0
- package/.agents/skills/gstack/retro/SKILL.md.tmpl +833 -0
- package/.agents/skills/gstack/review/SKILL.md +849 -0
- package/.agents/skills/gstack/review/SKILL.md.tmpl +259 -0
- package/.agents/skills/gstack/review/TODOS-format.md +62 -0
- package/.agents/skills/gstack/review/checklist.md +190 -0
- package/.agents/skills/gstack/review/design-checklist.md +132 -0
- package/.agents/skills/gstack/review/greptile-triage.md +220 -0
- package/.agents/skills/gstack/scripts/analytics.ts +190 -0
- package/.agents/skills/gstack/scripts/dev-skill.ts +82 -0
- package/.agents/skills/gstack/scripts/eval-compare.ts +96 -0
- package/.agents/skills/gstack/scripts/eval-list.ts +116 -0
- package/.agents/skills/gstack/scripts/eval-select.ts +86 -0
- package/.agents/skills/gstack/scripts/eval-summary.ts +187 -0
- package/.agents/skills/gstack/scripts/eval-watch.ts +172 -0
- package/.agents/skills/gstack/scripts/gen-skill-docs.ts +2414 -0
- package/.agents/skills/gstack/scripts/skill-check.ts +167 -0
- package/.agents/skills/gstack/setup +269 -0
- package/.agents/skills/gstack/setup-browser-cookies/SKILL.md +330 -0
- package/.agents/skills/gstack/setup-browser-cookies/SKILL.md.tmpl +74 -0
- package/.agents/skills/gstack/setup-deploy/SKILL.md +459 -0
- package/.agents/skills/gstack/setup-deploy/SKILL.md.tmpl +220 -0
- package/.agents/skills/gstack/ship/SKILL.md +1457 -0
- package/.agents/skills/gstack/ship/SKILL.md.tmpl +528 -0
- package/.agents/skills/gstack/supabase/config.sh +10 -0
- package/.agents/skills/gstack/supabase/functions/community-pulse/index.ts +59 -0
- package/.agents/skills/gstack/supabase/functions/telemetry-ingest/index.ts +135 -0
- package/.agents/skills/gstack/supabase/functions/update-check/index.ts +37 -0
- package/.agents/skills/gstack/supabase/migrations/001_telemetry.sql +89 -0
- package/.agents/skills/gstack/test/analytics.test.ts +277 -0
- package/.agents/skills/gstack/test/codex-e2e.test.ts +197 -0
- package/.agents/skills/gstack/test/fixtures/coverage-audit-fixture.ts +76 -0
- package/.agents/skills/gstack/test/fixtures/eval-baselines.json +7 -0
- package/.agents/skills/gstack/test/fixtures/qa-eval-checkout-ground-truth.json +43 -0
- package/.agents/skills/gstack/test/fixtures/qa-eval-ground-truth.json +43 -0
- package/.agents/skills/gstack/test/fixtures/qa-eval-spa-ground-truth.json +43 -0
- package/.agents/skills/gstack/test/fixtures/review-eval-design-slop.css +86 -0
- package/.agents/skills/gstack/test/fixtures/review-eval-design-slop.html +41 -0
- package/.agents/skills/gstack/test/fixtures/review-eval-enum-diff.rb +30 -0
- package/.agents/skills/gstack/test/fixtures/review-eval-enum.rb +27 -0
- package/.agents/skills/gstack/test/fixtures/review-eval-vuln.rb +14 -0
- package/.agents/skills/gstack/test/gemini-e2e.test.ts +173 -0
- package/.agents/skills/gstack/test/gen-skill-docs.test.ts +1049 -0
- package/.agents/skills/gstack/test/global-discover.test.ts +187 -0
- package/.agents/skills/gstack/test/helpers/codex-session-runner.ts +282 -0
- package/.agents/skills/gstack/test/helpers/e2e-helpers.ts +239 -0
- package/.agents/skills/gstack/test/helpers/eval-store.test.ts +548 -0
- package/.agents/skills/gstack/test/helpers/eval-store.ts +689 -0
- package/.agents/skills/gstack/test/helpers/gemini-session-runner.test.ts +104 -0
- package/.agents/skills/gstack/test/helpers/gemini-session-runner.ts +201 -0
- package/.agents/skills/gstack/test/helpers/llm-judge.ts +130 -0
- package/.agents/skills/gstack/test/helpers/observability.test.ts +283 -0
- package/.agents/skills/gstack/test/helpers/session-runner.test.ts +96 -0
- package/.agents/skills/gstack/test/helpers/session-runner.ts +357 -0
- package/.agents/skills/gstack/test/helpers/skill-parser.ts +206 -0
- package/.agents/skills/gstack/test/helpers/touchfiles.ts +260 -0
- package/.agents/skills/gstack/test/hook-scripts.test.ts +373 -0
- package/.agents/skills/gstack/test/skill-e2e-browse.test.ts +293 -0
- package/.agents/skills/gstack/test/skill-e2e-deploy.test.ts +279 -0
- package/.agents/skills/gstack/test/skill-e2e-design.test.ts +614 -0
- package/.agents/skills/gstack/test/skill-e2e-plan.test.ts +538 -0
- package/.agents/skills/gstack/test/skill-e2e-qa-bugs.test.ts +194 -0
- package/.agents/skills/gstack/test/skill-e2e-qa-workflow.test.ts +412 -0
- package/.agents/skills/gstack/test/skill-e2e-review.test.ts +535 -0
- package/.agents/skills/gstack/test/skill-e2e-workflow.test.ts +586 -0
- package/.agents/skills/gstack/test/skill-e2e.test.ts +3325 -0
- package/.agents/skills/gstack/test/skill-llm-eval.test.ts +787 -0
- package/.agents/skills/gstack/test/skill-parser.test.ts +179 -0
- package/.agents/skills/gstack/test/skill-routing-e2e.test.ts +605 -0
- package/.agents/skills/gstack/test/skill-validation.test.ts +1520 -0
- package/.agents/skills/gstack/test/telemetry.test.ts +278 -0
- package/.agents/skills/gstack/test/touchfiles.test.ts +262 -0
- package/.agents/skills/gstack/unfreeze/SKILL.md +40 -0
- package/.agents/skills/gstack/unfreeze/SKILL.md.tmpl +38 -0
- package/README.md +12 -7
- package/README_KO.md +12 -6
- package/package.json +3 -2
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# gstack-config — read/write ~/.gstack/config.yaml
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# gstack-config get <key> — read a config value
|
|
6
|
+
# gstack-config set <key> <value> — write a config value
|
|
7
|
+
# gstack-config list — show all config
|
|
8
|
+
#
|
|
9
|
+
# Env overrides (for testing):
|
|
10
|
+
# GSTACK_STATE_DIR — override ~/.gstack state directory
|
|
11
|
+
set -euo pipefail
|
|
12
|
+
|
|
13
|
+
STATE_DIR="${GSTACK_STATE_DIR:-$HOME/.gstack}"
|
|
14
|
+
CONFIG_FILE="$STATE_DIR/config.yaml"
|
|
15
|
+
|
|
16
|
+
case "${1:-}" in
|
|
17
|
+
get)
|
|
18
|
+
KEY="${2:?Usage: gstack-config get <key>}"
|
|
19
|
+
grep -E "^${KEY}:" "$CONFIG_FILE" 2>/dev/null | tail -1 | awk '{print $2}' | tr -d '[:space:]' || true
|
|
20
|
+
;;
|
|
21
|
+
set)
|
|
22
|
+
KEY="${2:?Usage: gstack-config set <key> <value>}"
|
|
23
|
+
VALUE="${3:?Usage: gstack-config set <key> <value>}"
|
|
24
|
+
mkdir -p "$STATE_DIR"
|
|
25
|
+
if grep -qE "^${KEY}:" "$CONFIG_FILE" 2>/dev/null; then
|
|
26
|
+
sed -i '' "s/^${KEY}:.*/${KEY}: ${VALUE}/" "$CONFIG_FILE"
|
|
27
|
+
else
|
|
28
|
+
echo "${KEY}: ${VALUE}" >> "$CONFIG_FILE"
|
|
29
|
+
fi
|
|
30
|
+
;;
|
|
31
|
+
list)
|
|
32
|
+
cat "$CONFIG_FILE" 2>/dev/null || true
|
|
33
|
+
;;
|
|
34
|
+
*)
|
|
35
|
+
echo "Usage: gstack-config {get|set|list} [key] [value]"
|
|
36
|
+
exit 1
|
|
37
|
+
;;
|
|
38
|
+
esac
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# gstack-diff-scope — categorize what changed in the diff against a base branch
|
|
3
|
+
# Usage: source <(gstack-diff-scope main) → sets SCOPE_FRONTEND=true SCOPE_BACKEND=false ...
|
|
4
|
+
# Or: gstack-diff-scope main → prints SCOPE_*=... lines
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
BASE="${1:-main}"
|
|
8
|
+
|
|
9
|
+
# Get changed file list
|
|
10
|
+
FILES=$(git diff "${BASE}...HEAD" --name-only 2>/dev/null || git diff "${BASE}" --name-only 2>/dev/null || echo "")
|
|
11
|
+
|
|
12
|
+
if [ -z "$FILES" ]; then
|
|
13
|
+
echo "SCOPE_FRONTEND=false"
|
|
14
|
+
echo "SCOPE_BACKEND=false"
|
|
15
|
+
echo "SCOPE_PROMPTS=false"
|
|
16
|
+
echo "SCOPE_TESTS=false"
|
|
17
|
+
echo "SCOPE_DOCS=false"
|
|
18
|
+
echo "SCOPE_CONFIG=false"
|
|
19
|
+
exit 0
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
FRONTEND=false
|
|
23
|
+
BACKEND=false
|
|
24
|
+
PROMPTS=false
|
|
25
|
+
TESTS=false
|
|
26
|
+
DOCS=false
|
|
27
|
+
CONFIG=false
|
|
28
|
+
|
|
29
|
+
while IFS= read -r f; do
|
|
30
|
+
case "$f" in
|
|
31
|
+
# Frontend: CSS, views, components, templates
|
|
32
|
+
*.css|*.scss|*.less|*.sass|*.pcss|*.module.css|*.module.scss) FRONTEND=true ;;
|
|
33
|
+
*.tsx|*.jsx|*.vue|*.svelte|*.astro) FRONTEND=true ;;
|
|
34
|
+
*.erb|*.haml|*.slim|*.hbs|*.ejs) FRONTEND=true ;;
|
|
35
|
+
*.html) FRONTEND=true ;;
|
|
36
|
+
tailwind.config.*|postcss.config.*) FRONTEND=true ;;
|
|
37
|
+
app/views/*|*/components/*|styles/*|css/*|app/assets/stylesheets/*) FRONTEND=true ;;
|
|
38
|
+
|
|
39
|
+
# Prompts: prompt builders, system prompts, generation services
|
|
40
|
+
*prompt_builder*|*generation_service*|*writer_service*|*designer_service*) PROMPTS=true ;;
|
|
41
|
+
*evaluator*|*scorer*|*classifier_service*|*analyzer*) PROMPTS=true ;;
|
|
42
|
+
*voice*.rb|*writing*.rb|*prompt*.rb|*token*.rb) PROMPTS=true ;;
|
|
43
|
+
app/services/chat_tools/*|app/services/x_thread_tools/*) PROMPTS=true ;;
|
|
44
|
+
config/system_prompts/*) PROMPTS=true ;;
|
|
45
|
+
|
|
46
|
+
# Tests
|
|
47
|
+
*.test.*|*.spec.*|*_test.*|*_spec.*) TESTS=true ;;
|
|
48
|
+
test/*|tests/*|spec/*|__tests__/*|cypress/*|e2e/*) TESTS=true ;;
|
|
49
|
+
|
|
50
|
+
# Docs
|
|
51
|
+
*.md) DOCS=true ;;
|
|
52
|
+
|
|
53
|
+
# Config
|
|
54
|
+
package.json|package-lock.json|yarn.lock|bun.lockb) CONFIG=true ;;
|
|
55
|
+
Gemfile|Gemfile.lock) CONFIG=true ;;
|
|
56
|
+
*.yml|*.yaml) CONFIG=true ;;
|
|
57
|
+
.github/*) CONFIG=true ;;
|
|
58
|
+
requirements.txt|pyproject.toml|go.mod|Cargo.toml|composer.json) CONFIG=true ;;
|
|
59
|
+
|
|
60
|
+
# Backend: everything else that's code (excluding views/components already matched)
|
|
61
|
+
*.rb|*.py|*.go|*.rs|*.java|*.php|*.ex|*.exs) BACKEND=true ;;
|
|
62
|
+
*.ts|*.js) BACKEND=true ;; # Non-component TS/JS is backend
|
|
63
|
+
esac
|
|
64
|
+
done <<< "$FILES"
|
|
65
|
+
|
|
66
|
+
echo "SCOPE_FRONTEND=$FRONTEND"
|
|
67
|
+
echo "SCOPE_BACKEND=$BACKEND"
|
|
68
|
+
echo "SCOPE_PROMPTS=$PROMPTS"
|
|
69
|
+
echo "SCOPE_TESTS=$TESTS"
|
|
70
|
+
echo "SCOPE_DOCS=$DOCS"
|
|
71
|
+
echo "SCOPE_CONFIG=$CONFIG"
|
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* gstack-global-discover — Discover AI coding sessions across Claude Code, Codex CLI, and Gemini CLI.
|
|
4
|
+
* Resolves each session's working directory to a git repo, deduplicates by normalized remote URL,
|
|
5
|
+
* and outputs structured JSON to stdout.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* gstack-global-discover --since 7d [--format json|summary]
|
|
9
|
+
* gstack-global-discover --help
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { existsSync, readdirSync, statSync, readFileSync, openSync, readSync, closeSync } from "fs";
|
|
13
|
+
import { join, basename } from "path";
|
|
14
|
+
import { execSync } from "child_process";
|
|
15
|
+
import { homedir } from "os";
|
|
16
|
+
|
|
17
|
+
// ── Types ──────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
interface Session {
|
|
20
|
+
tool: "claude_code" | "codex" | "gemini";
|
|
21
|
+
cwd: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface Repo {
|
|
25
|
+
name: string;
|
|
26
|
+
remote: string;
|
|
27
|
+
paths: string[];
|
|
28
|
+
sessions: { claude_code: number; codex: number; gemini: number };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface DiscoveryResult {
|
|
32
|
+
window: string;
|
|
33
|
+
start_date: string;
|
|
34
|
+
repos: Repo[];
|
|
35
|
+
tools: {
|
|
36
|
+
claude_code: { total_sessions: number; repos: number };
|
|
37
|
+
codex: { total_sessions: number; repos: number };
|
|
38
|
+
gemini: { total_sessions: number; repos: number };
|
|
39
|
+
};
|
|
40
|
+
total_sessions: number;
|
|
41
|
+
total_repos: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── CLI parsing ────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
function printUsage(): void {
|
|
47
|
+
console.error(`Usage: gstack-global-discover --since <window> [--format json|summary]
|
|
48
|
+
|
|
49
|
+
--since <window> Time window: e.g. 7d, 14d, 30d, 24h
|
|
50
|
+
--format <fmt> Output format: json (default) or summary
|
|
51
|
+
--help Show this help
|
|
52
|
+
|
|
53
|
+
Examples:
|
|
54
|
+
gstack-global-discover --since 7d
|
|
55
|
+
gstack-global-discover --since 14d --format summary`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function parseArgs(): { since: string; format: "json" | "summary" } {
|
|
59
|
+
const args = process.argv.slice(2);
|
|
60
|
+
let since = "";
|
|
61
|
+
let format: "json" | "summary" = "json";
|
|
62
|
+
|
|
63
|
+
for (let i = 0; i < args.length; i++) {
|
|
64
|
+
if (args[i] === "--help" || args[i] === "-h") {
|
|
65
|
+
printUsage();
|
|
66
|
+
process.exit(0);
|
|
67
|
+
} else if (args[i] === "--since" && args[i + 1]) {
|
|
68
|
+
since = args[++i];
|
|
69
|
+
} else if (args[i] === "--format" && args[i + 1]) {
|
|
70
|
+
const f = args[++i];
|
|
71
|
+
if (f !== "json" && f !== "summary") {
|
|
72
|
+
console.error(`Invalid format: ${f}. Use 'json' or 'summary'.`);
|
|
73
|
+
printUsage();
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
format = f;
|
|
77
|
+
} else {
|
|
78
|
+
console.error(`Unknown argument: ${args[i]}`);
|
|
79
|
+
printUsage();
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!since) {
|
|
85
|
+
console.error("Error: --since is required.");
|
|
86
|
+
printUsage();
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!/^\d+(d|h|w)$/.test(since)) {
|
|
91
|
+
console.error(`Invalid window format: ${since}. Use e.g. 7d, 24h, 2w.`);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { since, format };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function windowToDate(window: string): Date {
|
|
99
|
+
const match = window.match(/^(\d+)(d|h|w)$/);
|
|
100
|
+
if (!match) throw new Error(`Invalid window: ${window}`);
|
|
101
|
+
const [, numStr, unit] = match;
|
|
102
|
+
const num = parseInt(numStr, 10);
|
|
103
|
+
const now = new Date();
|
|
104
|
+
|
|
105
|
+
if (unit === "h") {
|
|
106
|
+
return new Date(now.getTime() - num * 60 * 60 * 1000);
|
|
107
|
+
} else if (unit === "w") {
|
|
108
|
+
// weeks — midnight-aligned like days
|
|
109
|
+
const d = new Date(now);
|
|
110
|
+
d.setDate(d.getDate() - num * 7);
|
|
111
|
+
d.setHours(0, 0, 0, 0);
|
|
112
|
+
return d;
|
|
113
|
+
} else {
|
|
114
|
+
// days — midnight-aligned
|
|
115
|
+
const d = new Date(now);
|
|
116
|
+
d.setDate(d.getDate() - num);
|
|
117
|
+
d.setHours(0, 0, 0, 0);
|
|
118
|
+
return d;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ── URL normalization ──────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
export function normalizeRemoteUrl(url: string): string {
|
|
125
|
+
let normalized = url.trim();
|
|
126
|
+
|
|
127
|
+
// SSH → HTTPS: git@github.com:user/repo → https://github.com/user/repo
|
|
128
|
+
const sshMatch = normalized.match(/^(?:ssh:\/\/)?git@([^:]+):(.+)$/);
|
|
129
|
+
if (sshMatch) {
|
|
130
|
+
normalized = `https://${sshMatch[1]}/${sshMatch[2]}`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Strip .git suffix
|
|
134
|
+
if (normalized.endsWith(".git")) {
|
|
135
|
+
normalized = normalized.slice(0, -4);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Lowercase the host portion
|
|
139
|
+
try {
|
|
140
|
+
const parsed = new URL(normalized);
|
|
141
|
+
parsed.hostname = parsed.hostname.toLowerCase();
|
|
142
|
+
normalized = parsed.toString();
|
|
143
|
+
// Remove trailing slash
|
|
144
|
+
if (normalized.endsWith("/")) {
|
|
145
|
+
normalized = normalized.slice(0, -1);
|
|
146
|
+
}
|
|
147
|
+
} catch {
|
|
148
|
+
// Not a valid URL (e.g., local:<path>), return as-is
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return normalized;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ── Git helpers ────────────────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
function isGitRepo(dir: string): boolean {
|
|
157
|
+
return existsSync(join(dir, ".git"));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function getGitRemote(cwd: string): string | null {
|
|
161
|
+
if (!existsSync(cwd) || !isGitRepo(cwd)) return null;
|
|
162
|
+
try {
|
|
163
|
+
const remote = execSync("git remote get-url origin", {
|
|
164
|
+
cwd,
|
|
165
|
+
encoding: "utf-8",
|
|
166
|
+
timeout: 5000,
|
|
167
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
168
|
+
}).trim();
|
|
169
|
+
return remote || null;
|
|
170
|
+
} catch {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ── Scanners ───────────────────────────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
function scanClaudeCode(since: Date): Session[] {
|
|
178
|
+
const projectsDir = join(homedir(), ".claude", "projects");
|
|
179
|
+
if (!existsSync(projectsDir)) return [];
|
|
180
|
+
|
|
181
|
+
const sessions: Session[] = [];
|
|
182
|
+
|
|
183
|
+
let dirs: string[];
|
|
184
|
+
try {
|
|
185
|
+
dirs = readdirSync(projectsDir);
|
|
186
|
+
} catch {
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
for (const dirName of dirs) {
|
|
191
|
+
const dirPath = join(projectsDir, dirName);
|
|
192
|
+
try {
|
|
193
|
+
const stat = statSync(dirPath);
|
|
194
|
+
if (!stat.isDirectory()) continue;
|
|
195
|
+
} catch {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Find JSONL files
|
|
200
|
+
let jsonlFiles: string[];
|
|
201
|
+
try {
|
|
202
|
+
jsonlFiles = readdirSync(dirPath).filter((f) => f.endsWith(".jsonl"));
|
|
203
|
+
} catch {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (jsonlFiles.length === 0) continue;
|
|
207
|
+
|
|
208
|
+
// Coarse mtime pre-filter: check if any JSONL file is recent
|
|
209
|
+
const hasRecentFile = jsonlFiles.some((f) => {
|
|
210
|
+
try {
|
|
211
|
+
return statSync(join(dirPath, f)).mtime >= since;
|
|
212
|
+
} catch {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
if (!hasRecentFile) continue;
|
|
217
|
+
|
|
218
|
+
// Resolve cwd
|
|
219
|
+
let cwd = resolveClaudeCodeCwd(dirPath, dirName, jsonlFiles);
|
|
220
|
+
if (!cwd) continue;
|
|
221
|
+
|
|
222
|
+
// Count only JSONL files modified within the window as sessions
|
|
223
|
+
const recentFiles = jsonlFiles.filter((f) => {
|
|
224
|
+
try {
|
|
225
|
+
return statSync(join(dirPath, f)).mtime >= since;
|
|
226
|
+
} catch {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
for (let i = 0; i < recentFiles.length; i++) {
|
|
231
|
+
sessions.push({ tool: "claude_code", cwd });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return sessions;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function resolveClaudeCodeCwd(
|
|
239
|
+
dirPath: string,
|
|
240
|
+
dirName: string,
|
|
241
|
+
jsonlFiles: string[]
|
|
242
|
+
): string | null {
|
|
243
|
+
// Fast-path: decode directory name
|
|
244
|
+
// e.g., -Users-garrytan-git-repo → /Users/garrytan/git/repo
|
|
245
|
+
const decoded = dirName.replace(/^-/, "/").replace(/-/g, "/");
|
|
246
|
+
if (existsSync(decoded)) return decoded;
|
|
247
|
+
|
|
248
|
+
// Fallback: read cwd from first JSONL file
|
|
249
|
+
// Sort by mtime descending, pick most recent
|
|
250
|
+
const sorted = jsonlFiles
|
|
251
|
+
.map((f) => {
|
|
252
|
+
try {
|
|
253
|
+
return { name: f, mtime: statSync(join(dirPath, f)).mtime.getTime() };
|
|
254
|
+
} catch {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
})
|
|
258
|
+
.filter(Boolean)
|
|
259
|
+
.sort((a, b) => b!.mtime - a!.mtime) as { name: string; mtime: number }[];
|
|
260
|
+
|
|
261
|
+
for (const file of sorted.slice(0, 3)) {
|
|
262
|
+
const cwd = extractCwdFromJsonl(join(dirPath, file.name));
|
|
263
|
+
if (cwd && existsSync(cwd)) return cwd;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function extractCwdFromJsonl(filePath: string): string | null {
|
|
270
|
+
try {
|
|
271
|
+
// Read only the first 8KB to avoid loading huge JSONL files into memory
|
|
272
|
+
const fd = openSync(filePath, "r");
|
|
273
|
+
const buf = Buffer.alloc(8192);
|
|
274
|
+
const bytesRead = readSync(fd, buf, 0, 8192, 0);
|
|
275
|
+
closeSync(fd);
|
|
276
|
+
const text = buf.toString("utf-8", 0, bytesRead);
|
|
277
|
+
const lines = text.split("\n").slice(0, 15);
|
|
278
|
+
for (const line of lines) {
|
|
279
|
+
if (!line.trim()) continue;
|
|
280
|
+
try {
|
|
281
|
+
const obj = JSON.parse(line);
|
|
282
|
+
if (obj.cwd) return obj.cwd;
|
|
283
|
+
} catch {
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
} catch {
|
|
288
|
+
// File read error
|
|
289
|
+
}
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function scanCodex(since: Date): Session[] {
|
|
294
|
+
const sessionsDir = join(homedir(), ".codex", "sessions");
|
|
295
|
+
if (!existsSync(sessionsDir)) return [];
|
|
296
|
+
|
|
297
|
+
const sessions: Session[] = [];
|
|
298
|
+
|
|
299
|
+
// Walk YYYY/MM/DD directory structure
|
|
300
|
+
try {
|
|
301
|
+
const years = readdirSync(sessionsDir);
|
|
302
|
+
for (const year of years) {
|
|
303
|
+
const yearPath = join(sessionsDir, year);
|
|
304
|
+
if (!statSync(yearPath).isDirectory()) continue;
|
|
305
|
+
|
|
306
|
+
const months = readdirSync(yearPath);
|
|
307
|
+
for (const month of months) {
|
|
308
|
+
const monthPath = join(yearPath, month);
|
|
309
|
+
if (!statSync(monthPath).isDirectory()) continue;
|
|
310
|
+
|
|
311
|
+
const days = readdirSync(monthPath);
|
|
312
|
+
for (const day of days) {
|
|
313
|
+
const dayPath = join(monthPath, day);
|
|
314
|
+
if (!statSync(dayPath).isDirectory()) continue;
|
|
315
|
+
|
|
316
|
+
const files = readdirSync(dayPath).filter((f) =>
|
|
317
|
+
f.startsWith("rollout-") && f.endsWith(".jsonl")
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
for (const file of files) {
|
|
321
|
+
const filePath = join(dayPath, file);
|
|
322
|
+
try {
|
|
323
|
+
const stat = statSync(filePath);
|
|
324
|
+
if (stat.mtime < since) continue;
|
|
325
|
+
} catch {
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Read first line for session_meta (only first 4KB)
|
|
330
|
+
try {
|
|
331
|
+
const fd = openSync(filePath, "r");
|
|
332
|
+
const buf = Buffer.alloc(4096);
|
|
333
|
+
const bytesRead = readSync(fd, buf, 0, 4096, 0);
|
|
334
|
+
closeSync(fd);
|
|
335
|
+
const firstLine = buf.toString("utf-8", 0, bytesRead).split("\n")[0];
|
|
336
|
+
if (!firstLine) continue;
|
|
337
|
+
const meta = JSON.parse(firstLine);
|
|
338
|
+
if (meta.type === "session_meta" && meta.payload?.cwd) {
|
|
339
|
+
sessions.push({ tool: "codex", cwd: meta.payload.cwd });
|
|
340
|
+
}
|
|
341
|
+
} catch {
|
|
342
|
+
console.error(`Warning: could not parse Codex session ${filePath}`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
} catch {
|
|
349
|
+
// Directory read error
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return sessions;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function scanGemini(since: Date): Session[] {
|
|
356
|
+
const tmpDir = join(homedir(), ".gemini", "tmp");
|
|
357
|
+
if (!existsSync(tmpDir)) return [];
|
|
358
|
+
|
|
359
|
+
// Load projects.json for path mapping
|
|
360
|
+
const projectsPath = join(homedir(), ".gemini", "projects.json");
|
|
361
|
+
let projectsMap: Record<string, string> = {}; // name → path
|
|
362
|
+
if (existsSync(projectsPath)) {
|
|
363
|
+
try {
|
|
364
|
+
const data = JSON.parse(readFileSync(projectsPath, { encoding: "utf-8" }));
|
|
365
|
+
// Format: { projects: { "/path": "name" } } — we want name → path
|
|
366
|
+
const projects = data.projects || {};
|
|
367
|
+
for (const [path, name] of Object.entries(projects)) {
|
|
368
|
+
projectsMap[name as string] = path;
|
|
369
|
+
}
|
|
370
|
+
} catch {
|
|
371
|
+
console.error("Warning: could not parse ~/.gemini/projects.json");
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const sessions: Session[] = [];
|
|
376
|
+
const seenTimestamps = new Map<string, Set<string>>(); // projectName → Set<startTime>
|
|
377
|
+
|
|
378
|
+
let projectDirs: string[];
|
|
379
|
+
try {
|
|
380
|
+
projectDirs = readdirSync(tmpDir);
|
|
381
|
+
} catch {
|
|
382
|
+
return [];
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
for (const projectName of projectDirs) {
|
|
386
|
+
const chatsDir = join(tmpDir, projectName, "chats");
|
|
387
|
+
if (!existsSync(chatsDir)) continue;
|
|
388
|
+
|
|
389
|
+
// Resolve cwd from projects.json
|
|
390
|
+
let cwd = projectsMap[projectName] || null;
|
|
391
|
+
|
|
392
|
+
// Fallback: check .project_root
|
|
393
|
+
if (!cwd) {
|
|
394
|
+
const projectRootFile = join(tmpDir, projectName, ".project_root");
|
|
395
|
+
if (existsSync(projectRootFile)) {
|
|
396
|
+
try {
|
|
397
|
+
cwd = readFileSync(projectRootFile, { encoding: "utf-8" }).trim();
|
|
398
|
+
} catch {}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (!cwd || !existsSync(cwd)) continue;
|
|
403
|
+
|
|
404
|
+
const seen = seenTimestamps.get(projectName) || new Set<string>();
|
|
405
|
+
seenTimestamps.set(projectName, seen);
|
|
406
|
+
|
|
407
|
+
let files: string[];
|
|
408
|
+
try {
|
|
409
|
+
files = readdirSync(chatsDir).filter((f) =>
|
|
410
|
+
f.startsWith("session-") && f.endsWith(".json")
|
|
411
|
+
);
|
|
412
|
+
} catch {
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
for (const file of files) {
|
|
417
|
+
const filePath = join(chatsDir, file);
|
|
418
|
+
try {
|
|
419
|
+
const stat = statSync(filePath);
|
|
420
|
+
if (stat.mtime < since) continue;
|
|
421
|
+
} catch {
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
try {
|
|
426
|
+
const data = JSON.parse(readFileSync(filePath, { encoding: "utf-8" }));
|
|
427
|
+
const startTime = data.startTime || "";
|
|
428
|
+
|
|
429
|
+
// Deduplicate by startTime within project
|
|
430
|
+
if (startTime && seen.has(startTime)) continue;
|
|
431
|
+
if (startTime) seen.add(startTime);
|
|
432
|
+
|
|
433
|
+
sessions.push({ tool: "gemini", cwd });
|
|
434
|
+
} catch {
|
|
435
|
+
console.error(`Warning: could not parse Gemini session ${filePath}`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return sessions;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// ── Deduplication ──────────────────────────────────────────────────────────
|
|
444
|
+
|
|
445
|
+
async function resolveAndDeduplicate(sessions: Session[]): Promise<Repo[]> {
|
|
446
|
+
// Group sessions by cwd
|
|
447
|
+
const byCwd = new Map<string, Session[]>();
|
|
448
|
+
for (const s of sessions) {
|
|
449
|
+
const existing = byCwd.get(s.cwd) || [];
|
|
450
|
+
existing.push(s);
|
|
451
|
+
byCwd.set(s.cwd, existing);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Resolve git remotes for each cwd
|
|
455
|
+
const cwds = Array.from(byCwd.keys());
|
|
456
|
+
const remoteMap = new Map<string, string>(); // cwd → normalized remote
|
|
457
|
+
|
|
458
|
+
for (const cwd of cwds) {
|
|
459
|
+
const raw = getGitRemote(cwd);
|
|
460
|
+
if (raw) {
|
|
461
|
+
remoteMap.set(cwd, normalizeRemoteUrl(raw));
|
|
462
|
+
} else if (existsSync(cwd) && isGitRepo(cwd)) {
|
|
463
|
+
remoteMap.set(cwd, `local:${cwd}`);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Group by normalized remote
|
|
468
|
+
const byRemote = new Map<string, { paths: string[]; sessions: Session[] }>();
|
|
469
|
+
for (const [cwd, cwdSessions] of byCwd) {
|
|
470
|
+
const remote = remoteMap.get(cwd);
|
|
471
|
+
if (!remote) continue;
|
|
472
|
+
|
|
473
|
+
const existing = byRemote.get(remote) || { paths: [], sessions: [] };
|
|
474
|
+
if (!existing.paths.includes(cwd)) existing.paths.push(cwd);
|
|
475
|
+
existing.sessions.push(...cwdSessions);
|
|
476
|
+
byRemote.set(remote, existing);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Build Repo objects
|
|
480
|
+
const repos: Repo[] = [];
|
|
481
|
+
for (const [remote, data] of byRemote) {
|
|
482
|
+
// Find first valid path
|
|
483
|
+
const validPath = data.paths.find((p) => existsSync(p) && isGitRepo(p));
|
|
484
|
+
if (!validPath) continue;
|
|
485
|
+
|
|
486
|
+
// Derive name from remote URL
|
|
487
|
+
let name: string;
|
|
488
|
+
if (remote.startsWith("local:")) {
|
|
489
|
+
name = basename(remote.replace("local:", ""));
|
|
490
|
+
} else {
|
|
491
|
+
try {
|
|
492
|
+
const url = new URL(remote);
|
|
493
|
+
name = basename(url.pathname);
|
|
494
|
+
} catch {
|
|
495
|
+
name = basename(remote);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const sessionCounts = { claude_code: 0, codex: 0, gemini: 0 };
|
|
500
|
+
for (const s of data.sessions) {
|
|
501
|
+
sessionCounts[s.tool]++;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
repos.push({
|
|
505
|
+
name,
|
|
506
|
+
remote,
|
|
507
|
+
paths: data.paths,
|
|
508
|
+
sessions: sessionCounts,
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Sort by total sessions descending
|
|
513
|
+
repos.sort(
|
|
514
|
+
(a, b) =>
|
|
515
|
+
b.sessions.claude_code + b.sessions.codex + b.sessions.gemini -
|
|
516
|
+
(a.sessions.claude_code + a.sessions.codex + a.sessions.gemini)
|
|
517
|
+
);
|
|
518
|
+
|
|
519
|
+
return repos;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// ── Main ───────────────────────────────────────────────────────────────────
|
|
523
|
+
|
|
524
|
+
async function main() {
|
|
525
|
+
const { since, format } = parseArgs();
|
|
526
|
+
const sinceDate = windowToDate(since);
|
|
527
|
+
const startDate = sinceDate.toISOString().split("T")[0];
|
|
528
|
+
|
|
529
|
+
// Run all scanners
|
|
530
|
+
const ccSessions = scanClaudeCode(sinceDate);
|
|
531
|
+
const codexSessions = scanCodex(sinceDate);
|
|
532
|
+
const geminiSessions = scanGemini(sinceDate);
|
|
533
|
+
|
|
534
|
+
const allSessions = [...ccSessions, ...codexSessions, ...geminiSessions];
|
|
535
|
+
|
|
536
|
+
// Summary to stderr
|
|
537
|
+
console.error(
|
|
538
|
+
`Discovered: ${ccSessions.length} CC sessions, ${codexSessions.length} Codex sessions, ${geminiSessions.length} Gemini sessions`
|
|
539
|
+
);
|
|
540
|
+
|
|
541
|
+
// Deduplicate
|
|
542
|
+
const repos = await resolveAndDeduplicate(allSessions);
|
|
543
|
+
|
|
544
|
+
console.error(`→ ${repos.length} unique repos`);
|
|
545
|
+
|
|
546
|
+
// Count per-tool repo counts
|
|
547
|
+
const ccRepos = new Set(repos.filter((r) => r.sessions.claude_code > 0).map((r) => r.remote)).size;
|
|
548
|
+
const codexRepos = new Set(repos.filter((r) => r.sessions.codex > 0).map((r) => r.remote)).size;
|
|
549
|
+
const geminiRepos = new Set(repos.filter((r) => r.sessions.gemini > 0).map((r) => r.remote)).size;
|
|
550
|
+
|
|
551
|
+
const result: DiscoveryResult = {
|
|
552
|
+
window: since,
|
|
553
|
+
start_date: startDate,
|
|
554
|
+
repos,
|
|
555
|
+
tools: {
|
|
556
|
+
claude_code: { total_sessions: ccSessions.length, repos: ccRepos },
|
|
557
|
+
codex: { total_sessions: codexSessions.length, repos: codexRepos },
|
|
558
|
+
gemini: { total_sessions: geminiSessions.length, repos: geminiRepos },
|
|
559
|
+
},
|
|
560
|
+
total_sessions: allSessions.length,
|
|
561
|
+
total_repos: repos.length,
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
if (format === "json") {
|
|
565
|
+
console.log(JSON.stringify(result, null, 2));
|
|
566
|
+
} else {
|
|
567
|
+
// Summary format
|
|
568
|
+
console.log(`Window: ${since} (since ${startDate})`);
|
|
569
|
+
console.log(`Sessions: ${allSessions.length} total (CC: ${ccSessions.length}, Codex: ${codexSessions.length}, Gemini: ${geminiSessions.length})`);
|
|
570
|
+
console.log(`Repos: ${repos.length} unique`);
|
|
571
|
+
console.log("");
|
|
572
|
+
for (const repo of repos) {
|
|
573
|
+
const total = repo.sessions.claude_code + repo.sessions.codex + repo.sessions.gemini;
|
|
574
|
+
const tools = [];
|
|
575
|
+
if (repo.sessions.claude_code > 0) tools.push(`CC:${repo.sessions.claude_code}`);
|
|
576
|
+
if (repo.sessions.codex > 0) tools.push(`Codex:${repo.sessions.codex}`);
|
|
577
|
+
if (repo.sessions.gemini > 0) tools.push(`Gemini:${repo.sessions.gemini}`);
|
|
578
|
+
console.log(` ${repo.name} (${total} sessions) — ${tools.join(", ")}`);
|
|
579
|
+
console.log(` Remote: ${repo.remote}`);
|
|
580
|
+
console.log(` Paths: ${repo.paths.join(", ")}`);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Only run main when executed directly (not when imported for testing)
|
|
586
|
+
if (import.meta.main) {
|
|
587
|
+
main().catch((err) => {
|
|
588
|
+
console.error(`Fatal error: ${err.message}`);
|
|
589
|
+
process.exit(1);
|
|
590
|
+
});
|
|
591
|
+
}
|