@basestream/cli 0.1.0 → 0.1.3

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 @basestream/cli might be problematic. Click here for more details.

package/package.json CHANGED
@@ -1,64 +1,22 @@
1
1
  {
2
- "name": "@basestream/cli",
3
- "version": "0.1.0",
4
- "description": "AI work intelligence for teams — automatic work tracking for Claude Code",
5
- "type": "module",
6
2
  "bin": {
7
- "basestream": "./bin/basestream.js"
3
+ "basestream": "bin/basestream.js"
8
4
  },
5
+ "description": "AI work intelligence for teams — automatic work tracking for Claude Code",
9
6
  "files": [
10
7
  "bin/",
11
- "dist/cli/",
8
+ "dist/cli.mjs",
12
9
  "README.md"
13
10
  ],
11
+ "name": "@basestream/cli",
14
12
  "scripts": {
15
- "dev": "NODE_OPTIONS='--max-old-space-size=4096' next dev",
16
- "build": "next build",
17
- "build:cli": "tsc -p tsconfig.cli.json",
13
+ "build:cli": "node scripts/build-cli.mjs",
18
14
  "dev:cli": "tsc -p tsconfig.cli.json --watch",
19
- "test:cli": "tsc -p tsconfig.cli.json && node bin/basestream.js",
20
15
  "link:cli": "pnpm run build:cli && npm link",
21
- "unlink:cli": "npm unlink -g @basestream/cli",
22
16
  "prebuild:publish": "npm run build:cli",
23
- "start": "next start",
24
- "lint": "eslint .",
25
- "db:generate": "drizzle-kit generate",
26
- "db:migrate": "drizzle-kit migrate",
27
- "db:push": "drizzle-kit push",
28
- "db:seed": "tsx src/db/seed.ts"
29
- },
30
- "dependencies": {
31
- "@neondatabase/serverless": "^1.0.2",
32
- "@octokit/auth-app": "^8.2.0",
33
- "@octokit/rest": "^22.0.1",
34
- "@paralleldrive/cuid2": "^2.2.2",
35
- "@stripe/stripe-js": "^3.0.0",
36
- "better-auth": "^1.6.0",
37
- "dotenv": "^17.4.1",
38
- "drizzle-orm": "^0.45.2",
39
- "next": "^16.2.2",
40
- "pg": "^8.20.0",
41
- "react": "^19.2.4",
42
- "react-dom": "^19.2.4",
43
- "react-markdown": "^10.1.0",
44
- "recharts": "^2.12.7",
45
- "stripe": "^15.0.0",
46
- "swr": "^2.4.1",
47
- "zod": "^4.3.6"
17
+ "test:cli": "tsc -p tsconfig.cli.json && node bin/basestream.js",
18
+ "unlink:cli": "npm unlink -g @basestream/cli"
48
19
  },
49
- "devDependencies": {
50
- "@tailwindcss/postcss": "^4.2.2",
51
- "@types/node": "^25.5.2",
52
- "@types/pg": "^8.20.0",
53
- "@types/react": "^19.2.14",
54
- "@types/react-dom": "^19.2.3",
55
- "@eslint/eslintrc": "^3",
56
- "drizzle-kit": "^0.31.10",
57
- "eslint": "^9",
58
- "eslint-config-next": "16.2.2",
59
- "postcss": "^8.4.38",
60
- "tailwindcss": "^4.2.2",
61
- "tsx": "^4.19.0",
62
- "typescript": "^5.4.5"
63
- }
20
+ "type": "module",
21
+ "version": "0.1.3"
64
22
  }
@@ -1 +0,0 @@
1
- export declare function hookStop(): Promise<void>;
@@ -1,247 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { execSync } from "node:child_process";
4
- import { BUFFER_DIR, SESSION_DIR, ensureDirs, readConfig } from "../config.js";
5
- function parseTranscriptLine(line) {
6
- try {
7
- return JSON.parse(line);
8
- }
9
- catch {
10
- return null;
11
- }
12
- }
13
- function extractGitInfo(cwd) {
14
- const result = {};
15
- try {
16
- result.branch = execSync("git rev-parse --abbrev-ref HEAD", {
17
- cwd,
18
- encoding: "utf-8",
19
- stdio: ["pipe", "pipe", "pipe"],
20
- }).trim();
21
- }
22
- catch { }
23
- try {
24
- result.repo = execSync("git remote get-url origin", {
25
- cwd,
26
- encoding: "utf-8",
27
- stdio: ["pipe", "pipe", "pipe"],
28
- }).trim();
29
- }
30
- catch { }
31
- result.projectName = path.basename(cwd);
32
- return result;
33
- }
34
- function readSessionAccumulator(sessionId) {
35
- const sessionFile = path.join(SESSION_DIR, `${sessionId}.json`);
36
- if (!fs.existsSync(sessionFile))
37
- return null;
38
- try {
39
- const raw = JSON.parse(fs.readFileSync(sessionFile, "utf-8"));
40
- raw.filesWritten = new Set(raw.filesWritten || []);
41
- raw.commitShas = new Set(raw.commitShas || []);
42
- return raw;
43
- }
44
- catch {
45
- return null;
46
- }
47
- }
48
- function writeSessionAccumulator(acc) {
49
- const sessionFile = path.join(SESSION_DIR, `${acc.sessionId}.json`);
50
- const serializable = {
51
- ...acc,
52
- filesWritten: Array.from(acc.filesWritten),
53
- commitShas: Array.from(acc.commitShas),
54
- };
55
- fs.writeFileSync(sessionFile, JSON.stringify(serializable, null, 2));
56
- }
57
- function analyzeTranscript(transcriptPath, acc) {
58
- if (!fs.existsSync(transcriptPath))
59
- return acc;
60
- const content = fs.readFileSync(transcriptPath, "utf-8");
61
- const lines = content.split("\n").filter(Boolean);
62
- for (const line of lines) {
63
- const msg = parseTranscriptLine(line);
64
- if (!msg)
65
- continue;
66
- // Count assistant turns
67
- if (msg.role === "assistant") {
68
- acc.turns++;
69
- }
70
- // Track tool usage
71
- if (msg.type === "tool_use" || msg.type === "tool_call") {
72
- const toolName = (msg).name ||
73
- msg.tool_name ||
74
- "unknown";
75
- acc.toolCalls.push({ tool: toolName, timestamp: msg.timestamp });
76
- // Extract file paths from Write/Edit tool calls
77
- if (toolName === "Write" ||
78
- toolName === "Edit" ||
79
- toolName === "NotebookEdit") {
80
- const input = (msg).input;
81
- const filePath = input?.file_path || input?.path;
82
- if (filePath)
83
- acc.filesWritten.add(filePath);
84
- }
85
- // Extract commit SHAs from Bash tool calls
86
- if (toolName === "Bash") {
87
- const input = (msg).input;
88
- const cmd = input?.command || "";
89
- if (cmd.includes("git commit")) {
90
- // Look ahead for the result containing the SHA
91
- // We'll catch it in the tool_result handler
92
- }
93
- }
94
- }
95
- // Extract commit SHAs from tool results
96
- if (msg.type === "tool_result") {
97
- const resultContent = String((msg).content || "");
98
- const shaMatch = resultContent.match(/\[([a-f0-9]{7,12})\]\s/);
99
- if (shaMatch) {
100
- acc.commitShas.add(shaMatch[1]);
101
- }
102
- }
103
- }
104
- return acc;
105
- }
106
- function categorizeWork(acc) {
107
- const files = Array.from(acc.filesWritten);
108
- const fileCount = files.length;
109
- // Categorize based on file patterns
110
- let category = "OTHER";
111
- const testFiles = files.filter((f) => f.includes(".test.") || f.includes(".spec.") || f.includes("__tests__"));
112
- const docFiles = files.filter((f) => f.endsWith(".md") || f.includes("/docs/"));
113
- const configFiles = files.filter((f) => f.includes("Dockerfile") ||
114
- f.includes(".yml") ||
115
- f.includes(".yaml") ||
116
- f.includes("ci"));
117
- if (testFiles.length > fileCount / 2)
118
- category = "TESTING";
119
- else if (docFiles.length > fileCount / 2)
120
- category = "DOCS";
121
- else if (configFiles.length > fileCount / 2)
122
- category = "DEVOPS";
123
- else if (fileCount > 0)
124
- category = "FEATURE";
125
- // Complexity
126
- let complexity = "LOW";
127
- if (fileCount === 0)
128
- complexity = "LOW";
129
- else if (fileCount <= 2)
130
- complexity = "LOW";
131
- else if (fileCount <= 6)
132
- complexity = "MEDIUM";
133
- else
134
- complexity = "HIGH";
135
- return { category, complexity };
136
- }
137
- function flushToBuffer(acc) {
138
- const { category, complexity } = categorizeWork(acc);
139
- const now = new Date();
140
- const startTime = new Date(acc.startedAt);
141
- const durationMin = Math.round((now.getTime() - startTime.getTime()) / 60_000);
142
- const entry = {
143
- sessionId: acc.sessionId,
144
- tool: "claude-code",
145
- projectName: acc.projectName || path.basename(acc.cwd),
146
- repo: acc.gitRepo || null,
147
- branch: acc.gitBranch || null,
148
- summary: buildSummary(acc, category),
149
- category,
150
- why: null, // Hook-based: no "why" available (CLAUDE.md rules would provide this)
151
- whatChanged: Array.from(acc.filesWritten).map((f) => path.relative(acc.cwd, f) || f),
152
- outcome: "IN_PROGRESS",
153
- filesTouched: acc.filesWritten.size,
154
- complexity,
155
- toolVersion: null,
156
- model: null,
157
- sessionDurationMin: durationMin > 0 ? durationMin : 1,
158
- turns: acc.turns,
159
- prUrl: null,
160
- ticketUrl: null,
161
- commitShas: Array.from(acc.commitShas),
162
- visibility: "TEAM",
163
- bufferedAt: now.toISOString(),
164
- };
165
- ensureDirs();
166
- const filename = `${Date.now()}-${acc.sessionId.slice(0, 8)}.json`;
167
- fs.writeFileSync(path.join(BUFFER_DIR, filename), JSON.stringify(entry, null, 2));
168
- }
169
- function buildSummary(acc, category) {
170
- const fileCount = acc.filesWritten.size;
171
- const commitCount = acc.commitShas.size;
172
- const parts = [];
173
- if (category !== "OTHER") {
174
- parts.push(category.toLowerCase());
175
- }
176
- if (fileCount > 0) {
177
- parts.push(`${fileCount} file${fileCount !== 1 ? "s" : ""} modified`);
178
- }
179
- if (commitCount > 0) {
180
- parts.push(`${commitCount} commit${commitCount !== 1 ? "s" : ""}`);
181
- }
182
- if (acc.projectName) {
183
- parts.push(`in ${acc.projectName}`);
184
- }
185
- return parts.length > 0
186
- ? parts.join(", ")
187
- : `Claude Code session in ${acc.projectName || "unknown project"}`;
188
- }
189
- export async function hookStop() {
190
- // Read hook payload from stdin
191
- let payload;
192
- try {
193
- const chunks = [];
194
- for await (const chunk of process.stdin) {
195
- chunks.push(chunk);
196
- }
197
- payload = JSON.parse(Buffer.concat(chunks).toString("utf-8"));
198
- }
199
- catch {
200
- // No valid payload — nothing to do
201
- process.exit(0);
202
- }
203
- const { session_id, transcript_path, cwd } = payload;
204
- if (!session_id)
205
- process.exit(0);
206
- ensureDirs();
207
- // Load or create session accumulator
208
- let acc = readSessionAccumulator(session_id);
209
- if (!acc) {
210
- const gitInfo = extractGitInfo(cwd);
211
- acc = {
212
- sessionId: session_id,
213
- cwd,
214
- startedAt: new Date().toISOString(),
215
- lastUpdatedAt: new Date().toISOString(),
216
- turns: 0,
217
- toolCalls: [],
218
- filesWritten: new Set(),
219
- commitShas: new Set(),
220
- gitBranch: gitInfo.branch,
221
- gitRepo: gitInfo.repo,
222
- projectName: gitInfo.projectName,
223
- };
224
- }
225
- // Analyze transcript to accumulate data
226
- if (transcript_path) {
227
- acc = analyzeTranscript(transcript_path, acc);
228
- }
229
- acc.lastUpdatedAt = new Date().toISOString();
230
- // Save accumulator for future Stop events in this session
231
- writeSessionAccumulator(acc);
232
- // Flush to buffer if we have meaningful work
233
- if (acc.filesWritten.size > 0 || acc.commitShas.size > 0 || acc.turns >= 3) {
234
- flushToBuffer(acc);
235
- // Auto-sync if config exists
236
- const config = readConfig();
237
- if (config?.apiKey) {
238
- try {
239
- const { syncEntries } = await import("./sync.js");
240
- await syncEntries(config);
241
- }
242
- catch {
243
- // Sync failure is non-fatal — entries stay in buffer
244
- }
245
- }
246
- }
247
- }
@@ -1 +0,0 @@
1
- export declare function init(): Promise<void>;
@@ -1,92 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import os from "node:os";
4
- import { execSync } from "node:child_process";
5
- import { ensureDirs, readConfig } from "../config.js";
6
- import { c, check, warn } from "../util.js";
7
- import { login } from "./login.js";
8
- const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), ".claude", "settings.json");
9
- const HOOK_COMMAND = "npx @basestream/cli _hook-stop";
10
- function detectClaudeCode() {
11
- try {
12
- const version = execSync("claude --version 2>/dev/null", {
13
- encoding: "utf-8",
14
- }).trim();
15
- return version || null;
16
- }
17
- catch {
18
- return null;
19
- }
20
- }
21
- function injectClaudeCodeHook() {
22
- const settingsDir = path.dirname(CLAUDE_SETTINGS_PATH);
23
- fs.mkdirSync(settingsDir, { recursive: true });
24
- let settings = {};
25
- if (fs.existsSync(CLAUDE_SETTINGS_PATH)) {
26
- try {
27
- settings = JSON.parse(fs.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8"));
28
- }
29
- catch {
30
- // Corrupted settings — start fresh
31
- }
32
- }
33
- // Initialize hooks structure
34
- if (!settings.hooks || typeof settings.hooks !== "object") {
35
- settings.hooks = {};
36
- }
37
- const hooks = settings.hooks;
38
- // Check if our hook is already installed
39
- if (Array.isArray(hooks.Stop)) {
40
- const existing = hooks.Stop;
41
- const alreadyInstalled = existing.some((entry) => entry.hooks?.some((h) => h.command?.includes("@basestream/cli")));
42
- if (alreadyInstalled) {
43
- check("Claude Code hook already installed");
44
- return;
45
- }
46
- }
47
- // Add our Stop hook
48
- if (!Array.isArray(hooks.Stop)) {
49
- hooks.Stop = [];
50
- }
51
- hooks.Stop.push({
52
- matcher: "*",
53
- hooks: [
54
- {
55
- type: "command",
56
- command: HOOK_COMMAND,
57
- timeout: 30,
58
- },
59
- ],
60
- });
61
- fs.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2));
62
- check("Injected tracking hook into ~/.claude/settings.json");
63
- }
64
- export async function init() {
65
- console.log();
66
- // 1. Detect Claude Code
67
- const ccVersion = detectClaudeCode();
68
- if (ccVersion) {
69
- console.log(` ${c.dim(`Detected: Claude Code ${ccVersion}`)}`);
70
- }
71
- else {
72
- warn("Claude Code not detected — hook will activate when installed");
73
- }
74
- console.log();
75
- // 2. Inject hook
76
- injectClaudeCodeHook();
77
- // 3. Create buffer directory
78
- ensureDirs();
79
- check(`Created ~/.basestream/buffer/`);
80
- // 4. Authenticate
81
- const existing = readConfig();
82
- if (existing?.apiKey) {
83
- check("Already authenticated");
84
- }
85
- else {
86
- console.log();
87
- await login();
88
- }
89
- console.log();
90
- console.log(` ${c.dim("That's it. Work normally — every session is now tracked.")}`);
91
- console.log();
92
- }
@@ -1 +0,0 @@
1
- export declare function login(): Promise<string>;
@@ -1,75 +0,0 @@
1
- import http from "node:http";
2
- import { execSync } from "node:child_process";
3
- import { writeConfig, readConfig } from "../config.js";
4
- import { c, check } from "../util.js";
5
- const DEFAULT_BASE_URL = "https://basestream.ai";
6
- function openBrowser(url) {
7
- try {
8
- const platform = process.platform;
9
- if (platform === "darwin")
10
- execSync(`open "${url}"`);
11
- else if (platform === "win32")
12
- execSync(`start "${url}"`);
13
- else
14
- execSync(`xdg-open "${url}"`);
15
- }
16
- catch {
17
- console.log(` ${c.dim(`Open this URL in your browser: ${url}`)}`);
18
- }
19
- }
20
- export async function login() {
21
- const existing = readConfig();
22
- const baseUrl = existing?.baseUrl ||
23
- process.env.BASESTREAM_URL ||
24
- DEFAULT_BASE_URL;
25
- console.log(` ${c.dim("Opening browser for authentication...")}`);
26
- // Start a temporary local server to receive the API key callback
27
- const apiKey = await new Promise((resolve, reject) => {
28
- const server = http.createServer((req, res) => {
29
- const url = new URL(req.url || "/", `http://localhost`);
30
- const key = url.searchParams.get("key");
31
- const orgId = url.searchParams.get("orgId");
32
- if (key) {
33
- res.writeHead(200, { "Content-Type": "text/html" });
34
- res.end(`
35
- <html>
36
- <body style="font-family: system-ui; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; background: #0a0a0a; color: #fafafa;">
37
- <div style="text-align: center;">
38
- <h1 style="color: #22c55e;">&#10003; Authenticated</h1>
39
- <p style="color: #a1a1aa;">You can close this tab and return to your terminal.</p>
40
- </div>
41
- </body>
42
- </html>
43
- `);
44
- server.close();
45
- writeConfig({
46
- apiKey: key,
47
- baseUrl,
48
- orgId: orgId || undefined,
49
- });
50
- resolve(key);
51
- }
52
- else {
53
- res.writeHead(400);
54
- res.end("Missing key parameter");
55
- }
56
- });
57
- server.listen(0, "127.0.0.1", () => {
58
- const addr = server.address();
59
- if (!addr || typeof addr === "string") {
60
- reject(new Error("Failed to start callback server"));
61
- return;
62
- }
63
- const callbackPort = addr.port;
64
- const authUrl = `${baseUrl}/api/auth/cli?callback=http://127.0.0.1:${callbackPort}`;
65
- openBrowser(authUrl);
66
- });
67
- // Timeout after 2 minutes
68
- setTimeout(() => {
69
- server.close();
70
- reject(new Error("Authentication timed out"));
71
- }, 120_000);
72
- });
73
- check("Authenticated with Basestream");
74
- return apiKey;
75
- }
@@ -1 +0,0 @@
1
- export declare function status(): Promise<void>;
@@ -1,97 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { BUFFER_DIR, ensureDirs, readConfig } from "../config.js";
4
- import { c } from "../util.js";
5
- function groupBy(arr, fn) {
6
- const result = {};
7
- for (const item of arr) {
8
- const key = fn(item);
9
- (result[key] ??= []).push(item);
10
- }
11
- return result;
12
- }
13
- function formatOutcome(outcome) {
14
- switch (outcome) {
15
- case "COMPLETED":
16
- return c.green("completed");
17
- case "IN_PROGRESS":
18
- return c.yellow("in progress");
19
- case "BLOCKED":
20
- return c.red("blocked");
21
- default:
22
- return c.dim(outcome.toLowerCase());
23
- }
24
- }
25
- async function fetchRemoteEntries(config) {
26
- try {
27
- const res = await fetch(`${config.baseUrl}/api/entries?period=week&limit=100`, {
28
- headers: { Authorization: `Bearer ${config.apiKey}` },
29
- });
30
- if (!res.ok)
31
- return [];
32
- const data = (await res.json());
33
- return data.entries || [];
34
- }
35
- catch {
36
- return [];
37
- }
38
- }
39
- function readLocalEntries() {
40
- ensureDirs();
41
- const files = fs
42
- .readdirSync(BUFFER_DIR)
43
- .filter((f) => f.endsWith(".json"));
44
- const entries = [];
45
- for (const file of files) {
46
- try {
47
- entries.push(JSON.parse(fs.readFileSync(path.join(BUFFER_DIR, file), "utf-8")));
48
- }
49
- catch { }
50
- }
51
- return entries;
52
- }
53
- export async function status() {
54
- const config = readConfig();
55
- // Combine local + remote entries
56
- const entries = readLocalEntries();
57
- if (config?.apiKey) {
58
- const remote = await fetchRemoteEntries(config);
59
- // Merge, dedup by sessionId
60
- const seen = new Set(entries.map((e) => e.sessionId).filter(Boolean));
61
- for (const re of remote) {
62
- if (!re.sessionId || !seen.has(re.sessionId)) {
63
- entries.push(re);
64
- }
65
- }
66
- }
67
- if (entries.length === 0) {
68
- console.log();
69
- console.log(` ${c.dim("No sessions logged this week.")}`);
70
- console.log(` ${c.dim("Start using Claude Code — entries will appear here.")}`);
71
- console.log();
72
- return;
73
- }
74
- console.log();
75
- console.log(` ${c.yellow(`This week — ${entries.length} sessions logged`)}`);
76
- console.log();
77
- // Group by project
78
- const byProject = groupBy(entries, (e) => e.projectName || "unknown");
79
- for (const [project, projectEntries] of Object.entries(byProject)) {
80
- const commits = projectEntries.reduce((sum, e) => sum + (e.commitShas?.length || 0), 0);
81
- const latestOutcome = projectEntries[0]?.outcome || "IN_PROGRESS";
82
- const outcomeStr = formatOutcome(latestOutcome);
83
- const parts = [`${projectEntries.length} sessions`];
84
- if (commits > 0) {
85
- parts.push(`${commits} commit${commits !== 1 ? "s" : ""}`);
86
- }
87
- parts.push(outcomeStr);
88
- console.log(` ${c.bold(project)} ${c.dim(parts.join(" · "))}`);
89
- }
90
- // Show pending sync count
91
- const localOnly = readLocalEntries();
92
- if (localOnly.length > 0) {
93
- console.log();
94
- console.log(` ${c.dim(`${localOnly.length} entries pending sync`)}`);
95
- }
96
- console.log();
97
- }
@@ -1,7 +0,0 @@
1
- import { type BasestreamConfig } from "../config.js";
2
- export declare function syncEntries(config?: BasestreamConfig): Promise<{
3
- synced: number;
4
- created: number;
5
- updated: number;
6
- } | undefined>;
7
- export declare function sync(): Promise<void>;
@@ -1,77 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { BUFFER_DIR, ensureDirs, readConfig } from "../config.js";
4
- import { c, check, fail } from "../util.js";
5
- export async function syncEntries(config) {
6
- const cfg = config || readConfig();
7
- if (!cfg?.apiKey) {
8
- fail("Not authenticated. Run: basestream login");
9
- process.exit(1);
10
- }
11
- ensureDirs();
12
- const files = fs
13
- .readdirSync(BUFFER_DIR)
14
- .filter((f) => f.endsWith(".json"))
15
- .sort();
16
- if (files.length === 0)
17
- return;
18
- const entries = [];
19
- const successFiles = [];
20
- for (const file of files) {
21
- try {
22
- const raw = fs.readFileSync(path.join(BUFFER_DIR, file), "utf-8");
23
- const entry = JSON.parse(raw);
24
- entries.push(entry);
25
- successFiles.push(file);
26
- }
27
- catch {
28
- // Skip corrupt files
29
- }
30
- }
31
- if (entries.length === 0)
32
- return;
33
- const res = await fetch(`${cfg.baseUrl}/api/sync`, {
34
- method: "POST",
35
- headers: {
36
- "Content-Type": "application/json",
37
- Authorization: `Bearer ${cfg.apiKey}`,
38
- },
39
- body: JSON.stringify({ entries }),
40
- });
41
- if (!res.ok) {
42
- const body = await res.text().catch(() => "");
43
- throw new Error(`Sync failed (${res.status}): ${body}`);
44
- }
45
- const result = (await res.json());
46
- // Clean up synced files
47
- for (const file of successFiles) {
48
- fs.unlinkSync(path.join(BUFFER_DIR, file));
49
- }
50
- return result;
51
- }
52
- export async function sync() {
53
- const cfg = readConfig();
54
- if (!cfg?.apiKey) {
55
- fail("Not authenticated. Run: basestream login");
56
- process.exit(1);
57
- }
58
- ensureDirs();
59
- const files = fs
60
- .readdirSync(BUFFER_DIR)
61
- .filter((f) => f.endsWith(".json"));
62
- if (files.length === 0) {
63
- console.log(` ${c.dim("No entries to sync.")}`);
64
- return;
65
- }
66
- console.log(` ${c.dim(`Syncing ${files.length} entries...`)}`);
67
- try {
68
- const result = await syncEntries(cfg);
69
- if (result) {
70
- check(`Synced ${result.synced} entries (${result.created} new, ${result.updated} updated)`);
71
- }
72
- }
73
- catch (err) {
74
- fail(`Sync failed: ${err instanceof Error ? err.message : String(err)}`);
75
- process.exit(1);
76
- }
77
- }