mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-04 15:57:33 +01:00
Bins, careful/freeze hooks, setup, upgrade migrations, browse/src, design, ios-qa daemon, lib and scripts resolve the state root through bin/gstack-state-root.sh (bash) or lib/state-root.ts (TS). Bins source the twin and stop with a reinstall message when it is missing; hooks source it and never spawn gstack-paths. browse/src/config.ts and lib/cso/state.ts delegate to resolveStateRoot. Analytics writers and readers move together so the usage log stays one file. gstack-uninstall deletes state only at ~/.gstack, refuses (exit 2) when it resolves to /, $HOME or an ancestor, the checkout or the git root, and leaves any other resolved root in place with the removal command. Fixtures that copy single bins now copy the twin.
99 lines
3.2 KiB
Bash
Executable File
99 lines
3.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# gstack-timeline-read — read and format project timeline
|
|
# Usage: gstack-timeline-read [--since "7 days ago"] [--limit N] [--branch NAME]
|
|
#
|
|
# Session timeline: local-only, never sent anywhere.
|
|
# Reads ~/.gstack/projects/$SLUG/timeline.jsonl, filters, formats.
|
|
# Exit 0 silently if no timeline file exists.
|
|
set -euo pipefail
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
eval "$("$SCRIPT_DIR/gstack-slug" 2>/dev/null)"
|
|
. "$(dirname "$0")/gstack-state-root.sh" 2>/dev/null || { echo "$0: cannot resolve the gstack state root: $(dirname "$0")/gstack-state-root.sh is missing. fix: reinstall with ./setup or /gstack-upgrade (docs/state-root.md)" >&2; exit 1; }
|
|
gstack_state_root_select
|
|
GSTACK_HOME="$_gstack_sr_root"
|
|
|
|
SINCE=""
|
|
LIMIT=20
|
|
BRANCH=""
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--since) SINCE="$2"; shift 2 ;;
|
|
--limit) LIMIT="$2"; shift 2 ;;
|
|
--branch) BRANCH="$2"; shift 2 ;;
|
|
*) shift ;;
|
|
esac
|
|
done
|
|
|
|
TIMELINE_FILE="$GSTACK_HOME/projects/$SLUG/timeline.jsonl"
|
|
|
|
if [ ! -f "$TIMELINE_FILE" ]; then
|
|
exit 0
|
|
fi
|
|
|
|
cat "$TIMELINE_FILE" 2>/dev/null | GSTACK_TIMELINE_SINCE="$SINCE" GSTACK_TIMELINE_BRANCH="$BRANCH" GSTACK_TIMELINE_LIMIT="$LIMIT" bun -e "
|
|
const lines = (await Bun.stdin.text()).trim().split('\n').filter(Boolean);
|
|
const since = process.env.GSTACK_TIMELINE_SINCE || '';
|
|
const branch = process.env.GSTACK_TIMELINE_BRANCH || '';
|
|
const limitRaw = process.env.GSTACK_TIMELINE_LIMIT || '20';
|
|
const parsedLimit = Number.parseInt(limitRaw, 10);
|
|
const limit = Number.isSafeInteger(parsedLimit) && parsedLimit > 0 ? parsedLimit : 20;
|
|
|
|
let sinceMs = 0;
|
|
if (since) {
|
|
// Parse relative time like '7 days ago'
|
|
const match = since.match(/(\d+)\s*(day|hour|minute|week|month)s?\s*ago/i);
|
|
if (match) {
|
|
const n = parseInt(match[1]);
|
|
const unit = match[2].toLowerCase();
|
|
const ms = { minute: 60000, hour: 3600000, day: 86400000, week: 604800000, month: 2592000000 };
|
|
sinceMs = Date.now() - n * (ms[unit] || 86400000);
|
|
}
|
|
}
|
|
|
|
const entries = [];
|
|
for (const line of lines) {
|
|
try {
|
|
const e = JSON.parse(line);
|
|
if (sinceMs && new Date(e.ts).getTime() < sinceMs) continue;
|
|
if (branch && e.branch !== branch) continue;
|
|
entries.push(e);
|
|
} catch {}
|
|
}
|
|
|
|
if (entries.length === 0) process.exit(0);
|
|
|
|
// Take last N entries
|
|
const recent = entries.slice(-limit);
|
|
|
|
// Skill counts (completed events only)
|
|
const counts = {};
|
|
const branches = new Set();
|
|
for (const e of entries) {
|
|
if (e.event === 'completed') {
|
|
counts[e.skill] = (counts[e.skill] || 0) + 1;
|
|
}
|
|
if (e.branch) branches.add(e.branch);
|
|
}
|
|
|
|
// Output summary
|
|
const countStr = Object.entries(counts)
|
|
.sort((a, b) => b[1] - a[1])
|
|
.map(([s, n]) => n + ' /' + s)
|
|
.join(', ');
|
|
|
|
if (countStr) {
|
|
console.log('TIMELINE: ' + countStr + ' across ' + branches.size + ' branch' + (branches.size !== 1 ? 'es' : ''));
|
|
}
|
|
|
|
// Output recent events
|
|
console.log('');
|
|
console.log('## Recent Events');
|
|
for (const e of recent) {
|
|
const ts = (e.ts || '').replace('T', ' ').replace(/\.\d+Z$/, 'Z');
|
|
const dur = e.duration_s ? ' (' + e.duration_s + 's)' : '';
|
|
const outcome = e.outcome ? ' [' + e.outcome + ']' : '';
|
|
console.log('- ' + ts + ' /' + e.skill + ' ' + e.event + outcome + dur + (e.branch ? ' on ' + e.branch : ''));
|
|
}
|
|
" 2>/dev/null || exit 0
|