@unpolarize/code-sessions 0.3.0 → 0.5.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.
- package/dist/{chunk-ON3CPW4C.js → chunk-Y7DRROK7.js} +78 -3
- package/dist/cli.js +5 -1
- package/dist/index.js +3 -1
- package/package.json +2 -2
- package/src/cli.ts +4 -0
- package/src/cliargs.ts +1 -0
- package/src/commands.ts +26 -1
- package/src/index_store/db.test.ts +19 -0
- package/src/index_store/db.ts +57 -0
- package/src/store/git.ts +20 -2
|
@@ -141,6 +141,7 @@ Commands:
|
|
|
141
141
|
index (Re)build the internal SQLite index from the git store
|
|
142
142
|
query List recent sessions from the index [--limit N] [--agent X]
|
|
143
143
|
usage Aggregated token/cost usage (totals/by-agent/by-day) [--json]
|
|
144
|
+
graph Sessions \xD7 topics graph (nodes + edges) [--json]
|
|
144
145
|
search Full-text search session turns <text> [--limit N]
|
|
145
146
|
fork Fork a session at a turn ("git for sessions") <session-id> --at N [--id X]
|
|
146
147
|
analytics Compute MVP-2 rollups + digest into analytics/
|
|
@@ -633,17 +634,32 @@ var GitStore = class {
|
|
|
633
634
|
isRepo() {
|
|
634
635
|
return existsSync5(join3(this.dir, ".git"));
|
|
635
636
|
}
|
|
636
|
-
/** Initialize the store repo (idempotent): git init,
|
|
637
|
+
/** Initialize the store repo (idempotent): git init, connect remote (adopting
|
|
638
|
+
* its existing history on a fresh clone so a second machine continues the same
|
|
639
|
+
* store instead of forking), then write scaffolding files if still absent. */
|
|
637
640
|
init() {
|
|
638
641
|
if (!this.isRepo()) {
|
|
639
642
|
const r = spawnSync("git", ["init", "-b", "main", this.dir], { encoding: "utf8" });
|
|
640
643
|
if (r.status !== 0) throw new Error(`git init failed: ${r.stderr}`);
|
|
641
644
|
}
|
|
645
|
+
if (this.opts.remote) {
|
|
646
|
+
this.ensureRemote(this.opts.remote);
|
|
647
|
+
this.adoptRemoteIfEmpty();
|
|
648
|
+
}
|
|
642
649
|
const giPath = join3(this.dir, ".gitignore");
|
|
643
650
|
if (!existsSync5(giPath)) writeFileSync3(giPath, GITIGNORE);
|
|
644
651
|
const gaPath = join3(this.dir, ".gitattributes");
|
|
645
652
|
if (!existsSync5(gaPath)) writeFileSync3(gaPath, GITATTRIBUTES);
|
|
646
|
-
|
|
653
|
+
}
|
|
654
|
+
/** When this clone has no commits yet, pull the remote's existing store so we
|
|
655
|
+
* continue its history (multi-machine) rather than starting a divergent one.
|
|
656
|
+
* No-op for an empty/unreachable remote. */
|
|
657
|
+
adoptRemoteIfEmpty() {
|
|
658
|
+
if (this.run(["rev-parse", "--verify", "HEAD"]).ok) return;
|
|
659
|
+
if (!this.run(["fetch", "origin"]).ok) return;
|
|
660
|
+
if (!this.run(["rev-parse", "--verify", "origin/main"]).ok) return;
|
|
661
|
+
this.run(["reset", "--hard", "origin/main"]);
|
|
662
|
+
this.run(["branch", "--set-upstream-to=origin/main", "main"]);
|
|
647
663
|
}
|
|
648
664
|
ensureRemote(remote) {
|
|
649
665
|
const existing = this.run(["remote", "get-url", "origin"]);
|
|
@@ -2157,6 +2173,41 @@ var SessionIndex = class {
|
|
|
2157
2173
|
).all(like, like, limit);
|
|
2158
2174
|
return rows.map((r) => this.rowToIndex(r));
|
|
2159
2175
|
}
|
|
2176
|
+
/** Graph of sessions clustered by topic (+ fork lineage where known). */
|
|
2177
|
+
graphData() {
|
|
2178
|
+
const rows = this.db.prepare("SELECT session_id, agent, topic, intent, cost_usd, title FROM session ORDER BY started_at DESC").all();
|
|
2179
|
+
const nodes = [];
|
|
2180
|
+
const edges = [];
|
|
2181
|
+
const topicNodes = /* @__PURE__ */ new Map();
|
|
2182
|
+
const topicId = (t) => {
|
|
2183
|
+
let id = topicNodes.get(t);
|
|
2184
|
+
if (!id) {
|
|
2185
|
+
id = `topic:${topicNodes.size}`;
|
|
2186
|
+
topicNodes.set(t, id);
|
|
2187
|
+
nodes.push({ id, kind: "topic", label: t, sessions: 0, cost_usd: 0 });
|
|
2188
|
+
}
|
|
2189
|
+
return id;
|
|
2190
|
+
};
|
|
2191
|
+
for (const r of rows) {
|
|
2192
|
+
const sid = `s:${r.session_id}`;
|
|
2193
|
+
nodes.push({
|
|
2194
|
+
id: sid,
|
|
2195
|
+
kind: "session",
|
|
2196
|
+
label: (r.topic || r.title || r.session_id).slice(0, 48),
|
|
2197
|
+
agent: r.agent,
|
|
2198
|
+
intent: r.intent ?? null,
|
|
2199
|
+
cost_usd: r.cost_usd,
|
|
2200
|
+
sessions: 1
|
|
2201
|
+
});
|
|
2202
|
+
const topic = (r.topic || r.intent || "misc").toString();
|
|
2203
|
+
const tid = topicId(topic);
|
|
2204
|
+
edges.push({ from: sid, to: tid, kind: "has-topic" });
|
|
2205
|
+
const tn = nodes.find((n) => n.id === tid);
|
|
2206
|
+
tn.sessions += 1;
|
|
2207
|
+
tn.cost_usd += r.cost_usd;
|
|
2208
|
+
}
|
|
2209
|
+
return { nodes, edges };
|
|
2210
|
+
}
|
|
2160
2211
|
/** Aggregated usage for a Usage panel: totals, by agent, by day, by project, top cost. */
|
|
2161
2212
|
usageSummary(opts = {}) {
|
|
2162
2213
|
const rows = this.db.prepare(
|
|
@@ -2462,7 +2513,17 @@ function cmdInit(cfg) {
|
|
|
2462
2513
|
if (!existsSync16(configPath)) {
|
|
2463
2514
|
writeFileSync9(
|
|
2464
2515
|
configPath,
|
|
2465
|
-
`${JSON.stringify(
|
|
2516
|
+
`${JSON.stringify(
|
|
2517
|
+
{
|
|
2518
|
+
insights: cfg.insights,
|
|
2519
|
+
batch: cfg.batch,
|
|
2520
|
+
hygiene: cfg.hygiene,
|
|
2521
|
+
// persist the remote so the daemon/CLI keep pushing here without flags
|
|
2522
|
+
git: { ...cfg.git.remote ? { remote: cfg.git.remote } : {}, autoPush: cfg.git.autoPush }
|
|
2523
|
+
},
|
|
2524
|
+
null,
|
|
2525
|
+
2
|
|
2526
|
+
)}
|
|
2466
2527
|
`
|
|
2467
2528
|
);
|
|
2468
2529
|
}
|
|
@@ -2605,6 +2666,19 @@ function cmdFork(cfg, opts) {
|
|
|
2605
2666
|
return { code: 1, output: `fork failed: ${e instanceof Error ? e.message : String(e)}` };
|
|
2606
2667
|
}
|
|
2607
2668
|
}
|
|
2669
|
+
function cmdGraph(cfg, opts = {}) {
|
|
2670
|
+
syncIndex(cfg);
|
|
2671
|
+
const index = new SessionIndex(cfg.indexPath);
|
|
2672
|
+
try {
|
|
2673
|
+
const g = index.graphData();
|
|
2674
|
+
if (opts.json) return { code: 0, output: JSON.stringify(g) };
|
|
2675
|
+
const topics = g.nodes.filter((n) => n.kind === "topic").length;
|
|
2676
|
+
const sessions = g.nodes.filter((n) => n.kind === "session").length;
|
|
2677
|
+
return { code: 0, output: `graph: ${sessions} sessions across ${topics} topics, ${g.edges.length} edges` };
|
|
2678
|
+
} finally {
|
|
2679
|
+
index.close();
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2608
2682
|
function cmdUsage(cfg, opts = {}) {
|
|
2609
2683
|
syncIndex(cfg);
|
|
2610
2684
|
const index = new SessionIndex(cfg.indexPath);
|
|
@@ -2794,6 +2868,7 @@ export {
|
|
|
2794
2868
|
cmdDoctor,
|
|
2795
2869
|
cmdExport,
|
|
2796
2870
|
cmdFork,
|
|
2871
|
+
cmdGraph,
|
|
2797
2872
|
cmdUsage,
|
|
2798
2873
|
cmdInstallSkills,
|
|
2799
2874
|
cmdIndex,
|
package/dist/cli.js
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
cmdDoctor,
|
|
6
6
|
cmdExport,
|
|
7
7
|
cmdFork,
|
|
8
|
+
cmdGraph,
|
|
8
9
|
cmdIndex,
|
|
9
10
|
cmdInit,
|
|
10
11
|
cmdInstallHooks,
|
|
@@ -23,7 +24,7 @@ import {
|
|
|
23
24
|
parseFlags,
|
|
24
25
|
readStdin,
|
|
25
26
|
startDaemon
|
|
26
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-Y7DRROK7.js";
|
|
27
28
|
|
|
28
29
|
// src/analytics/command.ts
|
|
29
30
|
import { mkdirSync, writeFileSync } from "fs";
|
|
@@ -260,6 +261,9 @@ async function main(argv) {
|
|
|
260
261
|
case "usage":
|
|
261
262
|
emit(cmdUsage(cfg, { json: flags.json === true }));
|
|
262
263
|
break;
|
|
264
|
+
case "graph":
|
|
265
|
+
emit(cmdGraph(cfg, { json: flags.json === true }));
|
|
266
|
+
break;
|
|
263
267
|
case "query":
|
|
264
268
|
emit(
|
|
265
269
|
cmdQuery(cfg, {
|
package/dist/index.js
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
cmdDoctor,
|
|
21
21
|
cmdExport,
|
|
22
22
|
cmdFork,
|
|
23
|
+
cmdGraph,
|
|
23
24
|
cmdIndex,
|
|
24
25
|
cmdInit,
|
|
25
26
|
cmdInstallHooks,
|
|
@@ -92,7 +93,7 @@ import {
|
|
|
92
93
|
writeBlobFile,
|
|
93
94
|
writeImportedSession,
|
|
94
95
|
writeTurnFile
|
|
95
|
-
} from "./chunk-
|
|
96
|
+
} from "./chunk-Y7DRROK7.js";
|
|
96
97
|
export {
|
|
97
98
|
CaptureEngine,
|
|
98
99
|
DEFAULT_HOOK_EVENTS,
|
|
@@ -115,6 +116,7 @@ export {
|
|
|
115
116
|
cmdDoctor,
|
|
116
117
|
cmdExport,
|
|
117
118
|
cmdFork,
|
|
119
|
+
cmdGraph,
|
|
118
120
|
cmdIndex,
|
|
119
121
|
cmdInit,
|
|
120
122
|
cmdInstallHooks,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unpolarize/code-sessions",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Headless, event-driven cross-agent session capture agent (daemon + CLI)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"build": "tsup src/index.ts src/cli.ts --format esm --clean --out-dir dist"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@unpolarize/code-sessions-schema": "^0.
|
|
28
|
+
"@unpolarize/code-sessions-schema": "^0.5.0",
|
|
29
29
|
"zod": "^3.23.8"
|
|
30
30
|
}
|
|
31
31
|
}
|
package/src/cli.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
cmdDoctor,
|
|
6
6
|
cmdExport,
|
|
7
7
|
cmdFork,
|
|
8
|
+
cmdGraph,
|
|
8
9
|
cmdIndex,
|
|
9
10
|
cmdInit,
|
|
10
11
|
cmdInstallHooks,
|
|
@@ -74,6 +75,9 @@ export async function main(argv: string[]): Promise<void> {
|
|
|
74
75
|
case 'usage':
|
|
75
76
|
emit(cmdUsage(cfg, { json: flags.json === true }));
|
|
76
77
|
break;
|
|
78
|
+
case 'graph':
|
|
79
|
+
emit(cmdGraph(cfg, { json: flags.json === true }));
|
|
80
|
+
break;
|
|
77
81
|
case 'query':
|
|
78
82
|
emit(
|
|
79
83
|
cmdQuery(cfg, {
|
package/src/cliargs.ts
CHANGED
|
@@ -59,6 +59,7 @@ Commands:
|
|
|
59
59
|
index (Re)build the internal SQLite index from the git store
|
|
60
60
|
query List recent sessions from the index [--limit N] [--agent X]
|
|
61
61
|
usage Aggregated token/cost usage (totals/by-agent/by-day) [--json]
|
|
62
|
+
graph Sessions × topics graph (nodes + edges) [--json]
|
|
62
63
|
search Full-text search session turns <text> [--limit N]
|
|
63
64
|
fork Fork a session at a turn ("git for sessions") <session-id> --at N [--id X]
|
|
64
65
|
analytics Compute MVP-2 rollups + digest into analytics/
|
package/src/commands.ts
CHANGED
|
@@ -59,7 +59,17 @@ export function cmdInit(cfg: CodeSessionsConfig): CommandResult {
|
|
|
59
59
|
if (!existsSync(configPath)) {
|
|
60
60
|
writeFileSync(
|
|
61
61
|
configPath,
|
|
62
|
-
`${JSON.stringify(
|
|
62
|
+
`${JSON.stringify(
|
|
63
|
+
{
|
|
64
|
+
insights: cfg.insights,
|
|
65
|
+
batch: cfg.batch,
|
|
66
|
+
hygiene: cfg.hygiene,
|
|
67
|
+
// persist the remote so the daemon/CLI keep pushing here without flags
|
|
68
|
+
git: { ...(cfg.git.remote ? { remote: cfg.git.remote } : {}), autoPush: cfg.git.autoPush },
|
|
69
|
+
},
|
|
70
|
+
null,
|
|
71
|
+
2,
|
|
72
|
+
)}\n`,
|
|
63
73
|
);
|
|
64
74
|
}
|
|
65
75
|
git.commit('init store');
|
|
@@ -234,6 +244,21 @@ export function cmdFork(
|
|
|
234
244
|
}
|
|
235
245
|
}
|
|
236
246
|
|
|
247
|
+
/** Sessions × topics graph (nodes + edges) from the CS index. JSON for the graph view. */
|
|
248
|
+
export function cmdGraph(cfg: CodeSessionsConfig, opts: { json?: boolean } = {}): CommandResult {
|
|
249
|
+
syncIndex(cfg);
|
|
250
|
+
const index = new SessionIndex(cfg.indexPath);
|
|
251
|
+
try {
|
|
252
|
+
const g = index.graphData();
|
|
253
|
+
if (opts.json) return { code: 0, output: JSON.stringify(g) };
|
|
254
|
+
const topics = g.nodes.filter((n) => n.kind === 'topic').length;
|
|
255
|
+
const sessions = g.nodes.filter((n) => n.kind === 'session').length;
|
|
256
|
+
return { code: 0, output: `graph: ${sessions} sessions across ${topics} topics, ${g.edges.length} edges` };
|
|
257
|
+
} finally {
|
|
258
|
+
index.close();
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
237
262
|
/** Aggregated usage from the CS index (totals/byAgent/byDay/byProject/topByCost). */
|
|
238
263
|
export function cmdUsage(cfg: CodeSessionsConfig, opts: { json?: boolean } = {}): CommandResult {
|
|
239
264
|
syncIndex(cfg); // ensure the index reflects the current store
|
|
@@ -116,6 +116,25 @@ describe('SessionIndex', () => {
|
|
|
116
116
|
}
|
|
117
117
|
});
|
|
118
118
|
|
|
119
|
+
it('builds a sessions×topics graph', () => {
|
|
120
|
+
const idx = new SessionIndex(':memory:');
|
|
121
|
+
try {
|
|
122
|
+
idx.upsertSession(env('a', 'claude-code'), { ...src, topic: 'fix parser' });
|
|
123
|
+
idx.upsertSession(env('b', 'grok'), { ...src, source_path: '/s/b.json', topic: 'fix parser' });
|
|
124
|
+
idx.upsertSession(env('c', 'codex'), { ...src, source_path: '/s/c.json', topic: 'add feature' });
|
|
125
|
+
const g = idx.graphData();
|
|
126
|
+
const topics = g.nodes.filter((n) => n.kind === 'topic');
|
|
127
|
+
const sessions = g.nodes.filter((n) => n.kind === 'session');
|
|
128
|
+
expect(sessions).toHaveLength(3);
|
|
129
|
+
expect(topics).toHaveLength(2); // "fix parser" + "add feature"
|
|
130
|
+
expect(g.edges).toHaveLength(3); // each session -> its topic
|
|
131
|
+
const fixParser = topics.find((t) => t.label === 'fix parser')!;
|
|
132
|
+
expect(fixParser.sessions).toBe(2);
|
|
133
|
+
} finally {
|
|
134
|
+
idx.close();
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
119
138
|
it('filters by agent and deletes sessions (cascade turns)', () => {
|
|
120
139
|
const idx = new SessionIndex(':memory:');
|
|
121
140
|
try {
|
package/src/index_store/db.ts
CHANGED
|
@@ -51,6 +51,25 @@ export interface UsageBucket {
|
|
|
51
51
|
cost_usd: number;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
export interface GraphNode {
|
|
55
|
+
id: string;
|
|
56
|
+
kind: 'session' | 'topic';
|
|
57
|
+
label: string;
|
|
58
|
+
agent?: string;
|
|
59
|
+
intent?: string | null;
|
|
60
|
+
cost_usd: number;
|
|
61
|
+
sessions: number;
|
|
62
|
+
}
|
|
63
|
+
export interface GraphEdge {
|
|
64
|
+
from: string;
|
|
65
|
+
to: string;
|
|
66
|
+
kind: 'has-topic' | 'forked-from';
|
|
67
|
+
}
|
|
68
|
+
export interface GraphData {
|
|
69
|
+
nodes: GraphNode[];
|
|
70
|
+
edges: GraphEdge[];
|
|
71
|
+
}
|
|
72
|
+
|
|
54
73
|
export interface UsageSummary {
|
|
55
74
|
totals: { sessions: number; input_tokens: number; output_tokens: number; cost_usd: number };
|
|
56
75
|
byAgent: Record<string, UsageBucket>;
|
|
@@ -308,6 +327,44 @@ export class SessionIndex {
|
|
|
308
327
|
return (rows as any[]).map((r) => this.rowToIndex(r));
|
|
309
328
|
}
|
|
310
329
|
|
|
330
|
+
/** Graph of sessions clustered by topic (+ fork lineage where known). */
|
|
331
|
+
graphData(): GraphData {
|
|
332
|
+
const rows = this.db
|
|
333
|
+
.prepare('SELECT session_id, agent, topic, intent, cost_usd, title FROM session ORDER BY started_at DESC')
|
|
334
|
+
.all() as any[];
|
|
335
|
+
const nodes: GraphNode[] = [];
|
|
336
|
+
const edges: GraphEdge[] = [];
|
|
337
|
+
const topicNodes = new Map<string, string>(); // topic -> node id
|
|
338
|
+
const topicId = (t: string) => {
|
|
339
|
+
let id = topicNodes.get(t);
|
|
340
|
+
if (!id) {
|
|
341
|
+
id = `topic:${topicNodes.size}`;
|
|
342
|
+
topicNodes.set(t, id);
|
|
343
|
+
nodes.push({ id, kind: 'topic', label: t, sessions: 0, cost_usd: 0 });
|
|
344
|
+
}
|
|
345
|
+
return id;
|
|
346
|
+
};
|
|
347
|
+
for (const r of rows) {
|
|
348
|
+
const sid = `s:${r.session_id}`;
|
|
349
|
+
nodes.push({
|
|
350
|
+
id: sid,
|
|
351
|
+
kind: 'session',
|
|
352
|
+
label: (r.topic || r.title || r.session_id).slice(0, 48),
|
|
353
|
+
agent: r.agent,
|
|
354
|
+
intent: r.intent ?? null,
|
|
355
|
+
cost_usd: r.cost_usd,
|
|
356
|
+
sessions: 1,
|
|
357
|
+
});
|
|
358
|
+
const topic = (r.topic || r.intent || 'misc').toString();
|
|
359
|
+
const tid = topicId(topic);
|
|
360
|
+
edges.push({ from: sid, to: tid, kind: 'has-topic' });
|
|
361
|
+
const tn = nodes.find((n) => n.id === tid)!;
|
|
362
|
+
tn.sessions += 1;
|
|
363
|
+
tn.cost_usd += r.cost_usd;
|
|
364
|
+
}
|
|
365
|
+
return { nodes, edges };
|
|
366
|
+
}
|
|
367
|
+
|
|
311
368
|
/** Aggregated usage for a Usage panel: totals, by agent, by day, by project, top cost. */
|
|
312
369
|
usageSummary(opts: { days?: number; topN?: number } = {}): UsageSummary {
|
|
313
370
|
const rows = this.db
|
package/src/store/git.ts
CHANGED
|
@@ -58,17 +58,35 @@ export class GitStore {
|
|
|
58
58
|
return existsSync(join(this.dir, '.git'));
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
/** Initialize the store repo (idempotent): git init,
|
|
61
|
+
/** Initialize the store repo (idempotent): git init, connect remote (adopting
|
|
62
|
+
* its existing history on a fresh clone so a second machine continues the same
|
|
63
|
+
* store instead of forking), then write scaffolding files if still absent. */
|
|
62
64
|
init(): void {
|
|
63
65
|
if (!this.isRepo()) {
|
|
64
66
|
const r = spawnSync('git', ['init', '-b', 'main', this.dir], { encoding: 'utf8' });
|
|
65
67
|
if (r.status !== 0) throw new Error(`git init failed: ${r.stderr}`);
|
|
66
68
|
}
|
|
69
|
+
// Connect + adopt remote BEFORE writing scaffolding, so an adopted checkout
|
|
70
|
+
// doesn't collide with untracked .gitignore/.gitattributes we'd write.
|
|
71
|
+
if (this.opts.remote) {
|
|
72
|
+
this.ensureRemote(this.opts.remote);
|
|
73
|
+
this.adoptRemoteIfEmpty();
|
|
74
|
+
}
|
|
67
75
|
const giPath = join(this.dir, '.gitignore');
|
|
68
76
|
if (!existsSync(giPath)) writeFileSync(giPath, GITIGNORE);
|
|
69
77
|
const gaPath = join(this.dir, '.gitattributes');
|
|
70
78
|
if (!existsSync(gaPath)) writeFileSync(gaPath, GITATTRIBUTES);
|
|
71
|
-
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** When this clone has no commits yet, pull the remote's existing store so we
|
|
82
|
+
* continue its history (multi-machine) rather than starting a divergent one.
|
|
83
|
+
* No-op for an empty/unreachable remote. */
|
|
84
|
+
private adoptRemoteIfEmpty(): void {
|
|
85
|
+
if (this.run(['rev-parse', '--verify', 'HEAD']).ok) return; // already has local history
|
|
86
|
+
if (!this.run(['fetch', 'origin']).ok) return; // empty or unreachable remote
|
|
87
|
+
if (!this.run(['rev-parse', '--verify', 'origin/main']).ok) return; // remote has no main
|
|
88
|
+
this.run(['reset', '--hard', 'origin/main']);
|
|
89
|
+
this.run(['branch', '--set-upstream-to=origin/main', 'main']);
|
|
72
90
|
}
|
|
73
91
|
|
|
74
92
|
ensureRemote(remote: string): void {
|