@keepgoingdev/cli 0.1.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/README.md +93 -0
- package/dist/index.js +529 -0
- package/package.json +24 -0
package/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# @keepgoing/cli
|
|
2
|
+
|
|
3
|
+
Terminal CLI for [KeepGoing](https://keepgoing.dev) — resume side projects without the mental friction.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Run without installing
|
|
9
|
+
npx @keepgoing/cli status
|
|
10
|
+
|
|
11
|
+
# Install globally
|
|
12
|
+
npm install -g @keepgoing/cli
|
|
13
|
+
|
|
14
|
+
# Or add to a project
|
|
15
|
+
npm install --save-dev @keepgoing/cli
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Commands
|
|
19
|
+
|
|
20
|
+
### `keepgoing status`
|
|
21
|
+
|
|
22
|
+
Show the last checkpoint for the current project.
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
KeepGoing · 12 days ago
|
|
26
|
+
|
|
27
|
+
Summary: Refactored auth middleware to support JWT rotation
|
|
28
|
+
Next step: Implement verifyRefreshToken helper in auth.ts
|
|
29
|
+
Branch: feature/auth-refactor
|
|
30
|
+
Files: auth.ts, middleware.ts, routes/token.ts (+2 more)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Flags:
|
|
34
|
+
|
|
35
|
+
- `--cwd <path>` — override the working directory
|
|
36
|
+
- `--json` — output raw JSON of the last checkpoint (for scripting)
|
|
37
|
+
- `--quiet` — output a single summary line (used by the shell hook)
|
|
38
|
+
|
|
39
|
+
### `keepgoing save`
|
|
40
|
+
|
|
41
|
+
Save a new checkpoint interactively. Prompts for:
|
|
42
|
+
|
|
43
|
+
1. What did you work on? (required)
|
|
44
|
+
2. What's your next step? (required)
|
|
45
|
+
3. Any blockers? (optional)
|
|
46
|
+
|
|
47
|
+
Git branch and touched files are auto-detected from the workspace.
|
|
48
|
+
|
|
49
|
+
### `keepgoing hook install`
|
|
50
|
+
|
|
51
|
+
Install a shell hook that runs `keepgoing status --quiet` automatically whenever you `cd` into a directory that contains `.keepgoing/`.
|
|
52
|
+
|
|
53
|
+
Supports **zsh** and **bash**. Detected from `$SHELL`.
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
keepgoing hook install
|
|
57
|
+
# → Reload your shell:
|
|
58
|
+
source ~/.zshrc # or ~/.bashrc
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### `keepgoing hook uninstall`
|
|
62
|
+
|
|
63
|
+
Remove the shell hook from your `~/.zshrc` or `~/.bashrc`.
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
keepgoing hook uninstall
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Shell Hook Setup
|
|
70
|
+
|
|
71
|
+
The shell hook gives you zero-friction project context when you enter a project directory.
|
|
72
|
+
|
|
73
|
+
After running `keepgoing hook install` and reloading your shell, entering any directory with a `.keepgoing/` folder will automatically print:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
KeepGoing · 3 days ago · Refactored auth middleware to support JWT rotation
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Data Format
|
|
80
|
+
|
|
81
|
+
Reads and writes the same `.keepgoing/` schema used by the VS Code extension:
|
|
82
|
+
|
|
83
|
+
- `.keepgoing/sessions.json` — all checkpoints
|
|
84
|
+
- `.keepgoing/state.json` — last session state
|
|
85
|
+
- `.keepgoing/meta.json` — project metadata
|
|
86
|
+
|
|
87
|
+
## Development
|
|
88
|
+
|
|
89
|
+
From the monorepo root:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
npm run build --workspace=apps/cli
|
|
93
|
+
```
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/storage.ts
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
7
|
+
|
|
8
|
+
// ../../packages/shared/src/session.ts
|
|
9
|
+
import { randomUUID } from "crypto";
|
|
10
|
+
function generateCheckpointId() {
|
|
11
|
+
return randomUUID();
|
|
12
|
+
}
|
|
13
|
+
function createCheckpoint(fields) {
|
|
14
|
+
return {
|
|
15
|
+
id: generateCheckpointId(),
|
|
16
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
17
|
+
...fields
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ../../packages/shared/src/timeUtils.ts
|
|
22
|
+
function formatRelativeTime(timestamp) {
|
|
23
|
+
const now = Date.now();
|
|
24
|
+
const then = new Date(timestamp).getTime();
|
|
25
|
+
const diffMs = now - then;
|
|
26
|
+
if (isNaN(diffMs)) {
|
|
27
|
+
return "unknown time";
|
|
28
|
+
}
|
|
29
|
+
if (diffMs < 0) {
|
|
30
|
+
return "in the future";
|
|
31
|
+
}
|
|
32
|
+
const seconds = Math.floor(diffMs / 1e3);
|
|
33
|
+
const minutes = Math.floor(seconds / 60);
|
|
34
|
+
const hours = Math.floor(minutes / 60);
|
|
35
|
+
const days = Math.floor(hours / 24);
|
|
36
|
+
const weeks = Math.floor(days / 7);
|
|
37
|
+
const months = Math.floor(days / 30);
|
|
38
|
+
const years = Math.floor(days / 365);
|
|
39
|
+
if (seconds < 10) {
|
|
40
|
+
return "just now";
|
|
41
|
+
} else if (seconds < 60) {
|
|
42
|
+
return `${seconds} seconds ago`;
|
|
43
|
+
} else if (minutes < 60) {
|
|
44
|
+
return minutes === 1 ? "1 minute ago" : `${minutes} minutes ago`;
|
|
45
|
+
} else if (hours < 24) {
|
|
46
|
+
return hours === 1 ? "1 hour ago" : `${hours} hours ago`;
|
|
47
|
+
} else if (days < 7) {
|
|
48
|
+
return days === 1 ? "1 day ago" : `${days} days ago`;
|
|
49
|
+
} else if (weeks < 4) {
|
|
50
|
+
return weeks === 1 ? "1 week ago" : `${weeks} weeks ago`;
|
|
51
|
+
} else if (months < 12) {
|
|
52
|
+
return months === 1 ? "1 month ago" : `${months} months ago`;
|
|
53
|
+
} else {
|
|
54
|
+
return years === 1 ? "1 year ago" : `${years} years ago`;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ../../packages/shared/src/gitUtils.ts
|
|
59
|
+
import { execFileSync, execFile } from "child_process";
|
|
60
|
+
import { promisify } from "util";
|
|
61
|
+
var execFileAsync = promisify(execFile);
|
|
62
|
+
function getCurrentBranch(workspacePath) {
|
|
63
|
+
try {
|
|
64
|
+
const result = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
65
|
+
cwd: workspacePath,
|
|
66
|
+
encoding: "utf-8",
|
|
67
|
+
timeout: 5e3
|
|
68
|
+
});
|
|
69
|
+
return result.trim() || void 0;
|
|
70
|
+
} catch {
|
|
71
|
+
return void 0;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function getTouchedFiles(workspacePath) {
|
|
75
|
+
try {
|
|
76
|
+
const result = execFileSync("git", ["status", "--porcelain"], {
|
|
77
|
+
cwd: workspacePath,
|
|
78
|
+
encoding: "utf-8",
|
|
79
|
+
timeout: 5e3
|
|
80
|
+
});
|
|
81
|
+
if (!result.trim()) {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
return result.trim().split("\n").map((line) => line.substring(3).trim()).filter((file) => file.length > 0 && !file.endsWith("/"));
|
|
85
|
+
} catch {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ../../packages/shared/src/reentry.ts
|
|
91
|
+
var RECENT_SESSION_COUNT = 5;
|
|
92
|
+
function getRecentSessions(allSessions, count = RECENT_SESSION_COUNT) {
|
|
93
|
+
return allSessions.slice(-count).reverse();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/storage.ts
|
|
97
|
+
var STORAGE_DIR = ".keepgoing";
|
|
98
|
+
var META_FILE = "meta.json";
|
|
99
|
+
var SESSIONS_FILE = "sessions.json";
|
|
100
|
+
var STATE_FILE = "state.json";
|
|
101
|
+
var KeepGoingReader = class {
|
|
102
|
+
storagePath;
|
|
103
|
+
metaFilePath;
|
|
104
|
+
sessionsFilePath;
|
|
105
|
+
stateFilePath;
|
|
106
|
+
constructor(workspacePath) {
|
|
107
|
+
this.storagePath = path.join(workspacePath, STORAGE_DIR);
|
|
108
|
+
this.metaFilePath = path.join(this.storagePath, META_FILE);
|
|
109
|
+
this.sessionsFilePath = path.join(this.storagePath, SESSIONS_FILE);
|
|
110
|
+
this.stateFilePath = path.join(this.storagePath, STATE_FILE);
|
|
111
|
+
}
|
|
112
|
+
exists() {
|
|
113
|
+
return fs.existsSync(this.storagePath);
|
|
114
|
+
}
|
|
115
|
+
getState() {
|
|
116
|
+
return this.readJsonFile(this.stateFilePath);
|
|
117
|
+
}
|
|
118
|
+
getMeta() {
|
|
119
|
+
return this.readJsonFile(this.metaFilePath);
|
|
120
|
+
}
|
|
121
|
+
getSessions() {
|
|
122
|
+
return this.parseSessions().sessions;
|
|
123
|
+
}
|
|
124
|
+
getLastSession() {
|
|
125
|
+
const { sessions, wrapperLastSessionId } = this.parseSessions();
|
|
126
|
+
if (sessions.length === 0) {
|
|
127
|
+
return void 0;
|
|
128
|
+
}
|
|
129
|
+
const state = this.getState();
|
|
130
|
+
if (state?.lastSessionId) {
|
|
131
|
+
const found = sessions.find((s) => s.id === state.lastSessionId);
|
|
132
|
+
if (found) return found;
|
|
133
|
+
}
|
|
134
|
+
if (wrapperLastSessionId) {
|
|
135
|
+
const found = sessions.find((s) => s.id === wrapperLastSessionId);
|
|
136
|
+
if (found) return found;
|
|
137
|
+
}
|
|
138
|
+
return sessions[sessions.length - 1];
|
|
139
|
+
}
|
|
140
|
+
getRecentSessions(count) {
|
|
141
|
+
return getRecentSessions(this.getSessions(), count);
|
|
142
|
+
}
|
|
143
|
+
parseSessions() {
|
|
144
|
+
const raw = this.readJsonFile(this.sessionsFilePath);
|
|
145
|
+
if (!raw) return { sessions: [] };
|
|
146
|
+
if (Array.isArray(raw)) return { sessions: raw };
|
|
147
|
+
return { sessions: raw.sessions ?? [], wrapperLastSessionId: raw.lastSessionId };
|
|
148
|
+
}
|
|
149
|
+
readJsonFile(filePath) {
|
|
150
|
+
try {
|
|
151
|
+
if (!fs.existsSync(filePath)) return void 0;
|
|
152
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
153
|
+
return JSON.parse(raw);
|
|
154
|
+
} catch {
|
|
155
|
+
return void 0;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
var KeepGoingWriter = class {
|
|
160
|
+
storagePath;
|
|
161
|
+
sessionsFilePath;
|
|
162
|
+
stateFilePath;
|
|
163
|
+
metaFilePath;
|
|
164
|
+
constructor(workspacePath) {
|
|
165
|
+
this.storagePath = path.join(workspacePath, STORAGE_DIR);
|
|
166
|
+
this.sessionsFilePath = path.join(this.storagePath, SESSIONS_FILE);
|
|
167
|
+
this.stateFilePath = path.join(this.storagePath, STATE_FILE);
|
|
168
|
+
this.metaFilePath = path.join(this.storagePath, META_FILE);
|
|
169
|
+
}
|
|
170
|
+
ensureDir() {
|
|
171
|
+
if (!fs.existsSync(this.storagePath)) {
|
|
172
|
+
fs.mkdirSync(this.storagePath, { recursive: true });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
saveCheckpoint(checkpoint, projectName) {
|
|
176
|
+
this.ensureDir();
|
|
177
|
+
let sessionsData;
|
|
178
|
+
try {
|
|
179
|
+
if (fs.existsSync(this.sessionsFilePath)) {
|
|
180
|
+
const raw = JSON.parse(fs.readFileSync(this.sessionsFilePath, "utf-8"));
|
|
181
|
+
if (Array.isArray(raw)) {
|
|
182
|
+
sessionsData = { version: 1, project: projectName, sessions: raw };
|
|
183
|
+
} else {
|
|
184
|
+
sessionsData = raw;
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
sessionsData = { version: 1, project: projectName, sessions: [] };
|
|
188
|
+
}
|
|
189
|
+
} catch {
|
|
190
|
+
sessionsData = { version: 1, project: projectName, sessions: [] };
|
|
191
|
+
}
|
|
192
|
+
sessionsData.sessions.push(checkpoint);
|
|
193
|
+
sessionsData.lastSessionId = checkpoint.id;
|
|
194
|
+
fs.writeFileSync(this.sessionsFilePath, JSON.stringify(sessionsData, null, 2), "utf-8");
|
|
195
|
+
const state = {
|
|
196
|
+
lastSessionId: checkpoint.id,
|
|
197
|
+
lastKnownBranch: checkpoint.gitBranch,
|
|
198
|
+
lastActivityAt: checkpoint.timestamp
|
|
199
|
+
};
|
|
200
|
+
fs.writeFileSync(this.stateFilePath, JSON.stringify(state, null, 2), "utf-8");
|
|
201
|
+
let meta;
|
|
202
|
+
try {
|
|
203
|
+
if (fs.existsSync(this.metaFilePath)) {
|
|
204
|
+
meta = JSON.parse(fs.readFileSync(this.metaFilePath, "utf-8"));
|
|
205
|
+
meta.lastUpdated = checkpoint.timestamp;
|
|
206
|
+
} else {
|
|
207
|
+
meta = {
|
|
208
|
+
projectId: randomUUID2(),
|
|
209
|
+
createdAt: checkpoint.timestamp,
|
|
210
|
+
lastUpdated: checkpoint.timestamp
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
} catch {
|
|
214
|
+
meta = {
|
|
215
|
+
projectId: randomUUID2(),
|
|
216
|
+
createdAt: checkpoint.timestamp,
|
|
217
|
+
lastUpdated: checkpoint.timestamp
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
fs.writeFileSync(this.metaFilePath, JSON.stringify(meta, null, 2), "utf-8");
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// src/render.ts
|
|
225
|
+
var RESET = "\x1B[0m";
|
|
226
|
+
var BOLD = "\x1B[1m";
|
|
227
|
+
var DIM = "\x1B[2m";
|
|
228
|
+
var YELLOW = "\x1B[33m";
|
|
229
|
+
var CYAN = "\x1B[36m";
|
|
230
|
+
function renderCheckpoint(checkpoint, daysSince) {
|
|
231
|
+
const relTime = formatRelativeTime(checkpoint.timestamp);
|
|
232
|
+
if (daysSince !== void 0 && daysSince >= 7) {
|
|
233
|
+
console.log(`${YELLOW}\u26A0 This project has been idle for ${daysSince} days.${RESET}`);
|
|
234
|
+
}
|
|
235
|
+
console.log(`
|
|
236
|
+
${BOLD}KeepGoing${RESET} \xB7 ${DIM}${relTime}${RESET}
|
|
237
|
+
`);
|
|
238
|
+
const label = (s) => `${CYAN}${s}${RESET}`;
|
|
239
|
+
if (checkpoint.summary) {
|
|
240
|
+
console.log(` ${label("Summary:")} ${checkpoint.summary}`);
|
|
241
|
+
}
|
|
242
|
+
if (checkpoint.nextStep) {
|
|
243
|
+
console.log(` ${label("Next step:")} ${checkpoint.nextStep}`);
|
|
244
|
+
}
|
|
245
|
+
if (checkpoint.blocker) {
|
|
246
|
+
console.log(` ${label("Blocker:")} ${checkpoint.blocker}`);
|
|
247
|
+
}
|
|
248
|
+
if (checkpoint.gitBranch) {
|
|
249
|
+
console.log(` ${label("Branch:")} ${checkpoint.gitBranch}`);
|
|
250
|
+
}
|
|
251
|
+
if (checkpoint.touchedFiles && checkpoint.touchedFiles.length > 0) {
|
|
252
|
+
const MAX_FILES = 3;
|
|
253
|
+
const shown = checkpoint.touchedFiles.slice(0, MAX_FILES).join(", ");
|
|
254
|
+
const extra = checkpoint.touchedFiles.length - MAX_FILES;
|
|
255
|
+
const filesStr = extra > 0 ? `${shown} (+${extra} more)` : shown;
|
|
256
|
+
console.log(` ${label("Files:")} ${filesStr}`);
|
|
257
|
+
}
|
|
258
|
+
console.log("");
|
|
259
|
+
}
|
|
260
|
+
function renderQuiet(checkpoint) {
|
|
261
|
+
const relTime = formatRelativeTime(checkpoint.timestamp);
|
|
262
|
+
const summary = checkpoint.summary || checkpoint.nextStep || "checkpoint saved";
|
|
263
|
+
console.log(`KeepGoing \xB7 ${relTime} \xB7 ${summary}`);
|
|
264
|
+
}
|
|
265
|
+
function renderNoData() {
|
|
266
|
+
console.log(
|
|
267
|
+
`No KeepGoing data found. Run ${BOLD}keepgoing save${RESET} to save your first checkpoint.`
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// src/commands/status.ts
|
|
272
|
+
async function statusCommand(opts) {
|
|
273
|
+
const reader = new KeepGoingReader(opts.cwd);
|
|
274
|
+
if (!reader.exists()) {
|
|
275
|
+
if (!opts.quiet) {
|
|
276
|
+
renderNoData();
|
|
277
|
+
}
|
|
278
|
+
process.exit(0);
|
|
279
|
+
}
|
|
280
|
+
const lastSession = reader.getLastSession();
|
|
281
|
+
if (!lastSession) {
|
|
282
|
+
if (!opts.quiet) {
|
|
283
|
+
renderNoData();
|
|
284
|
+
}
|
|
285
|
+
process.exit(0);
|
|
286
|
+
}
|
|
287
|
+
if (opts.json) {
|
|
288
|
+
console.log(JSON.stringify(lastSession, null, 2));
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (opts.quiet) {
|
|
292
|
+
renderQuiet(lastSession);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const daysSince = Math.floor(
|
|
296
|
+
(Date.now() - new Date(lastSession.timestamp).getTime()) / (1e3 * 60 * 60 * 24)
|
|
297
|
+
);
|
|
298
|
+
renderCheckpoint(lastSession, daysSince);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// src/commands/save.ts
|
|
302
|
+
import readline from "readline";
|
|
303
|
+
import path2 from "path";
|
|
304
|
+
function prompt(rl, question) {
|
|
305
|
+
return new Promise((resolve) => {
|
|
306
|
+
rl.question(question, (answer) => {
|
|
307
|
+
resolve(answer.trim());
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
async function saveCommand(opts) {
|
|
312
|
+
const rl = readline.createInterface({
|
|
313
|
+
input: process.stdin,
|
|
314
|
+
output: process.stdout
|
|
315
|
+
});
|
|
316
|
+
let summary = "";
|
|
317
|
+
let nextStep = "";
|
|
318
|
+
let blocker = "";
|
|
319
|
+
try {
|
|
320
|
+
while (!summary) {
|
|
321
|
+
summary = await prompt(rl, "What did you work on? ");
|
|
322
|
+
if (!summary) {
|
|
323
|
+
console.log(" (This field is required)");
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
while (!nextStep) {
|
|
327
|
+
nextStep = await prompt(rl, "What's your next step? ");
|
|
328
|
+
if (!nextStep) {
|
|
329
|
+
console.log(" (This field is required)");
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
blocker = await prompt(rl, "Any blockers? (leave empty to skip) ");
|
|
333
|
+
} finally {
|
|
334
|
+
rl.close();
|
|
335
|
+
}
|
|
336
|
+
const gitBranch = getCurrentBranch(opts.cwd);
|
|
337
|
+
const touchedFiles = getTouchedFiles(opts.cwd);
|
|
338
|
+
const checkpoint = createCheckpoint({
|
|
339
|
+
summary,
|
|
340
|
+
nextStep,
|
|
341
|
+
blocker: blocker || void 0,
|
|
342
|
+
gitBranch,
|
|
343
|
+
touchedFiles,
|
|
344
|
+
workspaceRoot: opts.cwd,
|
|
345
|
+
source: "manual"
|
|
346
|
+
});
|
|
347
|
+
const projectName = path2.basename(opts.cwd);
|
|
348
|
+
const writer = new KeepGoingWriter(opts.cwd);
|
|
349
|
+
writer.saveCheckpoint(checkpoint, projectName);
|
|
350
|
+
console.log("Checkpoint saved.");
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// src/commands/hook.ts
|
|
354
|
+
import fs2 from "fs";
|
|
355
|
+
import path3 from "path";
|
|
356
|
+
import os from "os";
|
|
357
|
+
var HOOK_MARKER_START = "# keepgoing-hook-start";
|
|
358
|
+
var HOOK_MARKER_END = "# keepgoing-hook-end";
|
|
359
|
+
var ZSH_HOOK = `${HOOK_MARKER_START}
|
|
360
|
+
# KeepGoing shell hook \u2014 auto-injected by 'keepgoing hook install'
|
|
361
|
+
if command -v keepgoing >/dev/null 2>&1; then
|
|
362
|
+
function chpwd() {
|
|
363
|
+
if [ -d ".keepgoing" ]; then
|
|
364
|
+
keepgoing status --quiet
|
|
365
|
+
fi
|
|
366
|
+
}
|
|
367
|
+
fi
|
|
368
|
+
${HOOK_MARKER_END}`;
|
|
369
|
+
var BASH_HOOK = `${HOOK_MARKER_START}
|
|
370
|
+
# KeepGoing shell hook \u2014 auto-injected by 'keepgoing hook install'
|
|
371
|
+
if command -v keepgoing >/dev/null 2>&1; then
|
|
372
|
+
function cd() {
|
|
373
|
+
builtin cd "$@" || return
|
|
374
|
+
if [ -d ".keepgoing" ]; then
|
|
375
|
+
keepgoing status --quiet
|
|
376
|
+
fi
|
|
377
|
+
}
|
|
378
|
+
fi
|
|
379
|
+
${HOOK_MARKER_END}`;
|
|
380
|
+
function detectShellRcFile() {
|
|
381
|
+
const shellEnv = process.env["SHELL"] ?? "";
|
|
382
|
+
const home = os.homedir();
|
|
383
|
+
if (shellEnv.endsWith("zsh")) {
|
|
384
|
+
return { shell: "zsh", rcFile: path3.join(home, ".zshrc") };
|
|
385
|
+
}
|
|
386
|
+
if (shellEnv.endsWith("bash")) {
|
|
387
|
+
return { shell: "bash", rcFile: path3.join(home, ".bashrc") };
|
|
388
|
+
}
|
|
389
|
+
return void 0;
|
|
390
|
+
}
|
|
391
|
+
function hookInstallCommand() {
|
|
392
|
+
const detected = detectShellRcFile();
|
|
393
|
+
if (!detected) {
|
|
394
|
+
console.error(
|
|
395
|
+
'Could not detect your shell. Set $SHELL to "zsh" or "bash" and try again.'
|
|
396
|
+
);
|
|
397
|
+
process.exit(1);
|
|
398
|
+
}
|
|
399
|
+
const { shell, rcFile } = detected;
|
|
400
|
+
const hookBlock = shell === "zsh" ? ZSH_HOOK : BASH_HOOK;
|
|
401
|
+
let existing = "";
|
|
402
|
+
try {
|
|
403
|
+
existing = fs2.readFileSync(rcFile, "utf-8");
|
|
404
|
+
} catch {
|
|
405
|
+
}
|
|
406
|
+
if (existing.includes(HOOK_MARKER_START)) {
|
|
407
|
+
console.log(`KeepGoing hook is already installed in ${rcFile}.`);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
fs2.appendFileSync(rcFile, `
|
|
411
|
+
${hookBlock}
|
|
412
|
+
`, "utf-8");
|
|
413
|
+
console.log(`KeepGoing hook installed in ${rcFile}.`);
|
|
414
|
+
console.log(`Reload your shell config to activate it:
|
|
415
|
+
`);
|
|
416
|
+
console.log(` source ${rcFile}`);
|
|
417
|
+
}
|
|
418
|
+
function hookUninstallCommand() {
|
|
419
|
+
const detected = detectShellRcFile();
|
|
420
|
+
if (!detected) {
|
|
421
|
+
console.error(
|
|
422
|
+
'Could not detect your shell. Set $SHELL to "zsh" or "bash" and try again.'
|
|
423
|
+
);
|
|
424
|
+
process.exit(1);
|
|
425
|
+
}
|
|
426
|
+
const { rcFile } = detected;
|
|
427
|
+
let existing = "";
|
|
428
|
+
try {
|
|
429
|
+
existing = fs2.readFileSync(rcFile, "utf-8");
|
|
430
|
+
} catch {
|
|
431
|
+
console.log(`${rcFile} not found \u2014 nothing to remove.`);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
if (!existing.includes(HOOK_MARKER_START)) {
|
|
435
|
+
console.log("KeepGoing hook is not installed \u2014 nothing to remove.");
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
const pattern = new RegExp(
|
|
439
|
+
`
|
|
440
|
+
?${HOOK_MARKER_START}[\\s\\S]*?${HOOK_MARKER_END}
|
|
441
|
+
?`,
|
|
442
|
+
"g"
|
|
443
|
+
);
|
|
444
|
+
const updated = existing.replace(pattern, "").replace(/\n{3,}/g, "\n\n");
|
|
445
|
+
fs2.writeFileSync(rcFile, updated, "utf-8");
|
|
446
|
+
console.log(`KeepGoing hook removed from ${rcFile}.`);
|
|
447
|
+
console.log(`Reload your shell config to deactivate it:
|
|
448
|
+
`);
|
|
449
|
+
console.log(` source ${rcFile}`);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// src/index.ts
|
|
453
|
+
var HELP_TEXT = `
|
|
454
|
+
keepgoing: resume side projects without the mental friction
|
|
455
|
+
|
|
456
|
+
Usage:
|
|
457
|
+
keepgoing status Show the last checkpoint for this project
|
|
458
|
+
keepgoing save Save a new checkpoint interactively
|
|
459
|
+
keepgoing hook Manage the shell hook
|
|
460
|
+
|
|
461
|
+
Options:
|
|
462
|
+
--cwd <path> Override the working directory (default: current directory)
|
|
463
|
+
--json Output raw JSON (status only)
|
|
464
|
+
--quiet Output a single summary line (status only)
|
|
465
|
+
-h, --help Show this help text
|
|
466
|
+
|
|
467
|
+
Hook subcommands:
|
|
468
|
+
keepgoing hook install Install the shell hook into ~/.zshrc or ~/.bashrc
|
|
469
|
+
keepgoing hook uninstall Remove the shell hook
|
|
470
|
+
`;
|
|
471
|
+
function parseArgs(argv) {
|
|
472
|
+
const args = argv.slice(2);
|
|
473
|
+
let command = "";
|
|
474
|
+
let subcommand = "";
|
|
475
|
+
let cwd = process.cwd();
|
|
476
|
+
let json = false;
|
|
477
|
+
let quiet = false;
|
|
478
|
+
for (let i = 0; i < args.length; i++) {
|
|
479
|
+
const arg = args[i];
|
|
480
|
+
if (arg === "--cwd" && i + 1 < args.length) {
|
|
481
|
+
cwd = args[++i];
|
|
482
|
+
} else if (arg === "--json") {
|
|
483
|
+
json = true;
|
|
484
|
+
} else if (arg === "--quiet") {
|
|
485
|
+
quiet = true;
|
|
486
|
+
} else if (arg === "-h" || arg === "--help") {
|
|
487
|
+
command = "help";
|
|
488
|
+
} else if (!command) {
|
|
489
|
+
command = arg;
|
|
490
|
+
} else if (!subcommand) {
|
|
491
|
+
subcommand = arg;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return { command, subcommand, cwd, json, quiet };
|
|
495
|
+
}
|
|
496
|
+
async function main() {
|
|
497
|
+
const { command, subcommand, cwd, json, quiet } = parseArgs(process.argv);
|
|
498
|
+
switch (command) {
|
|
499
|
+
case "status":
|
|
500
|
+
await statusCommand({ cwd, json, quiet });
|
|
501
|
+
break;
|
|
502
|
+
case "save":
|
|
503
|
+
await saveCommand({ cwd });
|
|
504
|
+
break;
|
|
505
|
+
case "hook":
|
|
506
|
+
if (subcommand === "install") {
|
|
507
|
+
hookInstallCommand();
|
|
508
|
+
} else if (subcommand === "uninstall") {
|
|
509
|
+
hookUninstallCommand();
|
|
510
|
+
} else {
|
|
511
|
+
console.error(
|
|
512
|
+
`Unknown hook subcommand: "${subcommand}". Use "install" or "uninstall".`
|
|
513
|
+
);
|
|
514
|
+
process.exit(1);
|
|
515
|
+
}
|
|
516
|
+
break;
|
|
517
|
+
case "help":
|
|
518
|
+
case "":
|
|
519
|
+
console.log(HELP_TEXT);
|
|
520
|
+
break;
|
|
521
|
+
default:
|
|
522
|
+
console.error(`Unknown command: "${command}". Run "keepgoing --help" for usage.`);
|
|
523
|
+
process.exit(1);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
main().catch((err) => {
|
|
527
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
528
|
+
process.exit(1);
|
|
529
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@keepgoingdev/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Terminal CLI for KeepGoing. Resume side projects without the mental friction.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"keepgoing": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist/"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsup",
|
|
15
|
+
"watch": "tsup --watch",
|
|
16
|
+
"prepublishOnly": "tsup"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@keepgoingdev/shared": "*",
|
|
20
|
+
"@types/node": "^20.11.0",
|
|
21
|
+
"tsup": "^8.0.0",
|
|
22
|
+
"typescript": "^5.3.0"
|
|
23
|
+
}
|
|
24
|
+
}
|