@keepgoingdev/cli 0.1.0 → 0.2.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.
Files changed (2) hide show
  1. package/dist/index.js +316 -88
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,10 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // src/storage.ts
4
- import fs from "fs";
5
- import path from "path";
6
- import { randomUUID as randomUUID2 } from "crypto";
7
-
8
3
  // ../../packages/shared/src/session.ts
9
4
  import { randomUUID } from "crypto";
10
5
  function generateCheckpointId() {
@@ -57,8 +52,49 @@ function formatRelativeTime(timestamp) {
57
52
 
58
53
  // ../../packages/shared/src/gitUtils.ts
59
54
  import { execFileSync, execFile } from "child_process";
55
+ import path from "path";
60
56
  import { promisify } from "util";
61
57
  var execFileAsync = promisify(execFile);
58
+ function findGitRoot(startPath) {
59
+ try {
60
+ const gitCommonDir = execFileSync("git", ["rev-parse", "--git-common-dir"], {
61
+ cwd: startPath,
62
+ encoding: "utf-8",
63
+ timeout: 5e3
64
+ }).trim();
65
+ if (!gitCommonDir) return startPath;
66
+ const absoluteGitDir = path.isAbsolute(gitCommonDir) ? gitCommonDir : path.resolve(startPath, gitCommonDir);
67
+ return path.dirname(absoluteGitDir);
68
+ } catch {
69
+ return startPath;
70
+ }
71
+ }
72
+ var storageRootCache = /* @__PURE__ */ new Map();
73
+ function resolveStorageRoot(startPath) {
74
+ const cached = storageRootCache.get(startPath);
75
+ if (cached !== void 0) {
76
+ return cached;
77
+ }
78
+ try {
79
+ const toplevel = execFileSync("git", ["rev-parse", "--show-toplevel"], {
80
+ cwd: startPath,
81
+ encoding: "utf-8",
82
+ timeout: 5e3
83
+ }).trim();
84
+ const commonDir = execFileSync("git", ["rev-parse", "--git-common-dir"], {
85
+ cwd: startPath,
86
+ encoding: "utf-8",
87
+ timeout: 5e3
88
+ }).trim();
89
+ const absoluteCommonDir = path.resolve(toplevel, commonDir);
90
+ const mainRoot = path.dirname(absoluteCommonDir);
91
+ storageRootCache.set(startPath, mainRoot);
92
+ return mainRoot;
93
+ } catch {
94
+ storageRootCache.set(startPath, startPath);
95
+ return startPath;
96
+ }
97
+ }
62
98
  function getCurrentBranch(workspacePath) {
63
99
  try {
64
100
  const result = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
@@ -93,79 +129,25 @@ function getRecentSessions(allSessions, count = RECENT_SESSION_COUNT) {
93
129
  return allSessions.slice(-count).reverse();
94
130
  }
95
131
 
96
- // src/storage.ts
132
+ // ../../packages/shared/src/storage.ts
133
+ import fs from "fs";
134
+ import path2 from "path";
135
+ import { randomUUID as randomUUID2 } from "crypto";
97
136
  var STORAGE_DIR = ".keepgoing";
98
137
  var META_FILE = "meta.json";
99
138
  var SESSIONS_FILE = "sessions.json";
100
139
  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
140
  var KeepGoingWriter = class {
160
141
  storagePath;
161
142
  sessionsFilePath;
162
143
  stateFilePath;
163
144
  metaFilePath;
164
145
  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);
146
+ const mainRoot = resolveStorageRoot(workspacePath);
147
+ this.storagePath = path2.join(mainRoot, STORAGE_DIR);
148
+ this.sessionsFilePath = path2.join(this.storagePath, SESSIONS_FILE);
149
+ this.stateFilePath = path2.join(this.storagePath, STATE_FILE);
150
+ this.metaFilePath = path2.join(this.storagePath, META_FILE);
169
151
  }
170
152
  ensureDir() {
171
153
  if (!fs.existsSync(this.storagePath)) {
@@ -191,6 +173,10 @@ var KeepGoingWriter = class {
191
173
  }
192
174
  sessionsData.sessions.push(checkpoint);
193
175
  sessionsData.lastSessionId = checkpoint.id;
176
+ const MAX_SESSIONS = 200;
177
+ if (sessionsData.sessions.length > MAX_SESSIONS) {
178
+ sessionsData.sessions = sessionsData.sessions.slice(-MAX_SESSIONS);
179
+ }
194
180
  fs.writeFileSync(this.sessionsFilePath, JSON.stringify(sessionsData, null, 2), "utf-8");
195
181
  const state = {
196
182
  lastSessionId: checkpoint.id,
@@ -198,29 +184,213 @@ var KeepGoingWriter = class {
198
184
  lastActivityAt: checkpoint.timestamp
199
185
  };
200
186
  fs.writeFileSync(this.stateFilePath, JSON.stringify(state, null, 2), "utf-8");
187
+ this.updateMeta(checkpoint.timestamp);
188
+ }
189
+ updateMeta(timestamp) {
201
190
  let meta;
202
191
  try {
203
192
  if (fs.existsSync(this.metaFilePath)) {
204
193
  meta = JSON.parse(fs.readFileSync(this.metaFilePath, "utf-8"));
205
- meta.lastUpdated = checkpoint.timestamp;
194
+ meta.lastUpdated = timestamp;
206
195
  } else {
207
196
  meta = {
208
197
  projectId: randomUUID2(),
209
- createdAt: checkpoint.timestamp,
210
- lastUpdated: checkpoint.timestamp
198
+ createdAt: timestamp,
199
+ lastUpdated: timestamp
211
200
  };
212
201
  }
213
202
  } catch {
214
203
  meta = {
215
204
  projectId: randomUUID2(),
216
- createdAt: checkpoint.timestamp,
217
- lastUpdated: checkpoint.timestamp
205
+ createdAt: timestamp,
206
+ lastUpdated: timestamp
218
207
  };
219
208
  }
220
209
  fs.writeFileSync(this.metaFilePath, JSON.stringify(meta, null, 2), "utf-8");
221
210
  }
222
211
  };
223
212
 
213
+ // ../../packages/shared/src/decisionStorage.ts
214
+ import fs2 from "fs";
215
+ import path3 from "path";
216
+
217
+ // ../../packages/shared/src/featureGate.ts
218
+ var DefaultFeatureGate = class {
219
+ isEnabled(_feature) {
220
+ return true;
221
+ }
222
+ };
223
+ var currentGate = new DefaultFeatureGate();
224
+
225
+ // ../../packages/shared/src/license.ts
226
+ import crypto from "crypto";
227
+ import fs3 from "fs";
228
+ import os from "os";
229
+ import path4 from "path";
230
+ var LICENSE_FILE = "license.json";
231
+ var DEVICE_ID_FILE = "device-id";
232
+ function getGlobalLicenseDir() {
233
+ return path4.join(os.homedir(), ".keepgoing");
234
+ }
235
+ function getGlobalLicensePath() {
236
+ return path4.join(getGlobalLicenseDir(), LICENSE_FILE);
237
+ }
238
+ function getDeviceId() {
239
+ const dir = getGlobalLicenseDir();
240
+ const filePath = path4.join(dir, DEVICE_ID_FILE);
241
+ try {
242
+ const existing = fs3.readFileSync(filePath, "utf-8").trim();
243
+ if (existing) return existing;
244
+ } catch {
245
+ }
246
+ const id = crypto.randomUUID();
247
+ if (!fs3.existsSync(dir)) {
248
+ fs3.mkdirSync(dir, { recursive: true });
249
+ }
250
+ fs3.writeFileSync(filePath, id, "utf-8");
251
+ return id;
252
+ }
253
+ function readLicenseCache() {
254
+ const licensePath = getGlobalLicensePath();
255
+ try {
256
+ if (!fs3.existsSync(licensePath)) {
257
+ return void 0;
258
+ }
259
+ const raw = fs3.readFileSync(licensePath, "utf-8");
260
+ return JSON.parse(raw);
261
+ } catch {
262
+ return void 0;
263
+ }
264
+ }
265
+ function writeLicenseCache(cache) {
266
+ const dirPath = getGlobalLicenseDir();
267
+ if (!fs3.existsSync(dirPath)) {
268
+ fs3.mkdirSync(dirPath, { recursive: true });
269
+ }
270
+ const licensePath = path4.join(dirPath, LICENSE_FILE);
271
+ fs3.writeFileSync(licensePath, JSON.stringify(cache, null, 2), "utf-8");
272
+ }
273
+ function deleteLicenseCache() {
274
+ const licensePath = getGlobalLicensePath();
275
+ try {
276
+ if (fs3.existsSync(licensePath)) {
277
+ fs3.unlinkSync(licensePath);
278
+ }
279
+ } catch {
280
+ }
281
+ }
282
+ function isCachedLicenseValid(cache) {
283
+ return cache?.status === "active";
284
+ }
285
+ var REVALIDATION_THRESHOLD_MS = 24 * 60 * 60 * 1e3;
286
+
287
+ // ../../packages/shared/src/licenseClient.ts
288
+ var BASE_URL = "https://api.lemonsqueezy.com/v1/licenses";
289
+ async function activateLicense(licenseKey, instanceName) {
290
+ try {
291
+ const res = await fetch(`${BASE_URL}/activate`, {
292
+ method: "POST",
293
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
294
+ body: new URLSearchParams({ license_key: licenseKey, instance_name: instanceName })
295
+ });
296
+ const data = await res.json();
297
+ if (!res.ok || !data.activated) {
298
+ return { valid: false, error: data.error || `Activation failed (${res.status})` };
299
+ }
300
+ return {
301
+ valid: true,
302
+ licenseKey: data.license_key?.key,
303
+ instanceId: data.instance?.id,
304
+ customerName: data.meta?.customer_name,
305
+ productName: data.meta?.product_name
306
+ };
307
+ } catch (err) {
308
+ return { valid: false, error: err instanceof Error ? err.message : "Network error" };
309
+ }
310
+ }
311
+ async function deactivateLicense(licenseKey, instanceId) {
312
+ try {
313
+ const res = await fetch(`${BASE_URL}/deactivate`, {
314
+ method: "POST",
315
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
316
+ body: new URLSearchParams({ license_key: licenseKey, instance_id: instanceId })
317
+ });
318
+ const data = await res.json();
319
+ if (!res.ok || !data.deactivated) {
320
+ return { deactivated: false, error: data.error || `Deactivation failed (${res.status})` };
321
+ }
322
+ return { deactivated: true };
323
+ } catch (err) {
324
+ return { deactivated: false, error: err instanceof Error ? err.message : "Network error" };
325
+ }
326
+ }
327
+
328
+ // src/storage.ts
329
+ import fs4 from "fs";
330
+ import path5 from "path";
331
+ var STORAGE_DIR2 = ".keepgoing";
332
+ var META_FILE2 = "meta.json";
333
+ var SESSIONS_FILE2 = "sessions.json";
334
+ var STATE_FILE2 = "state.json";
335
+ var KeepGoingReader = class {
336
+ storagePath;
337
+ metaFilePath;
338
+ sessionsFilePath;
339
+ stateFilePath;
340
+ constructor(workspacePath) {
341
+ this.storagePath = path5.join(workspacePath, STORAGE_DIR2);
342
+ this.metaFilePath = path5.join(this.storagePath, META_FILE2);
343
+ this.sessionsFilePath = path5.join(this.storagePath, SESSIONS_FILE2);
344
+ this.stateFilePath = path5.join(this.storagePath, STATE_FILE2);
345
+ }
346
+ exists() {
347
+ return fs4.existsSync(this.storagePath);
348
+ }
349
+ getState() {
350
+ return this.readJsonFile(this.stateFilePath);
351
+ }
352
+ getMeta() {
353
+ return this.readJsonFile(this.metaFilePath);
354
+ }
355
+ getSessions() {
356
+ return this.parseSessions().sessions;
357
+ }
358
+ getLastSession() {
359
+ const { sessions, wrapperLastSessionId } = this.parseSessions();
360
+ if (sessions.length === 0) {
361
+ return void 0;
362
+ }
363
+ const state = this.getState();
364
+ if (state?.lastSessionId) {
365
+ const found = sessions.find((s) => s.id === state.lastSessionId);
366
+ if (found) return found;
367
+ }
368
+ if (wrapperLastSessionId) {
369
+ const found = sessions.find((s) => s.id === wrapperLastSessionId);
370
+ if (found) return found;
371
+ }
372
+ return sessions[sessions.length - 1];
373
+ }
374
+ getRecentSessions(count) {
375
+ return getRecentSessions(this.getSessions(), count);
376
+ }
377
+ parseSessions() {
378
+ const raw = this.readJsonFile(this.sessionsFilePath);
379
+ if (!raw) return { sessions: [] };
380
+ if (Array.isArray(raw)) return { sessions: raw };
381
+ return { sessions: raw.sessions ?? [], wrapperLastSessionId: raw.lastSessionId };
382
+ }
383
+ readJsonFile(filePath) {
384
+ try {
385
+ if (!fs4.existsSync(filePath)) return void 0;
386
+ const raw = fs4.readFileSync(filePath, "utf-8");
387
+ return JSON.parse(raw);
388
+ } catch {
389
+ return void 0;
390
+ }
391
+ }
392
+ };
393
+
224
394
  // src/render.ts
225
395
  var RESET = "\x1B[0m";
226
396
  var BOLD = "\x1B[1m";
@@ -300,7 +470,7 @@ async function statusCommand(opts) {
300
470
 
301
471
  // src/commands/save.ts
302
472
  import readline from "readline";
303
- import path2 from "path";
473
+ import path6 from "path";
304
474
  function prompt(rl, question) {
305
475
  return new Promise((resolve) => {
306
476
  rl.question(question, (answer) => {
@@ -344,16 +514,16 @@ async function saveCommand(opts) {
344
514
  workspaceRoot: opts.cwd,
345
515
  source: "manual"
346
516
  });
347
- const projectName = path2.basename(opts.cwd);
517
+ const projectName = path6.basename(opts.cwd);
348
518
  const writer = new KeepGoingWriter(opts.cwd);
349
519
  writer.saveCheckpoint(checkpoint, projectName);
350
520
  console.log("Checkpoint saved.");
351
521
  }
352
522
 
353
523
  // src/commands/hook.ts
354
- import fs2 from "fs";
355
- import path3 from "path";
356
- import os from "os";
524
+ import fs5 from "fs";
525
+ import path7 from "path";
526
+ import os2 from "os";
357
527
  var HOOK_MARKER_START = "# keepgoing-hook-start";
358
528
  var HOOK_MARKER_END = "# keepgoing-hook-end";
359
529
  var ZSH_HOOK = `${HOOK_MARKER_START}
@@ -379,12 +549,12 @@ fi
379
549
  ${HOOK_MARKER_END}`;
380
550
  function detectShellRcFile() {
381
551
  const shellEnv = process.env["SHELL"] ?? "";
382
- const home = os.homedir();
552
+ const home = os2.homedir();
383
553
  if (shellEnv.endsWith("zsh")) {
384
- return { shell: "zsh", rcFile: path3.join(home, ".zshrc") };
554
+ return { shell: "zsh", rcFile: path7.join(home, ".zshrc") };
385
555
  }
386
556
  if (shellEnv.endsWith("bash")) {
387
- return { shell: "bash", rcFile: path3.join(home, ".bashrc") };
557
+ return { shell: "bash", rcFile: path7.join(home, ".bashrc") };
388
558
  }
389
559
  return void 0;
390
560
  }
@@ -400,14 +570,14 @@ function hookInstallCommand() {
400
570
  const hookBlock = shell === "zsh" ? ZSH_HOOK : BASH_HOOK;
401
571
  let existing = "";
402
572
  try {
403
- existing = fs2.readFileSync(rcFile, "utf-8");
573
+ existing = fs5.readFileSync(rcFile, "utf-8");
404
574
  } catch {
405
575
  }
406
576
  if (existing.includes(HOOK_MARKER_START)) {
407
577
  console.log(`KeepGoing hook is already installed in ${rcFile}.`);
408
578
  return;
409
579
  }
410
- fs2.appendFileSync(rcFile, `
580
+ fs5.appendFileSync(rcFile, `
411
581
  ${hookBlock}
412
582
  `, "utf-8");
413
583
  console.log(`KeepGoing hook installed in ${rcFile}.`);
@@ -426,7 +596,7 @@ function hookUninstallCommand() {
426
596
  const { rcFile } = detected;
427
597
  let existing = "";
428
598
  try {
429
- existing = fs2.readFileSync(rcFile, "utf-8");
599
+ existing = fs5.readFileSync(rcFile, "utf-8");
430
600
  } catch {
431
601
  console.log(`${rcFile} not found \u2014 nothing to remove.`);
432
602
  return;
@@ -442,21 +612,72 @@ function hookUninstallCommand() {
442
612
  "g"
443
613
  );
444
614
  const updated = existing.replace(pattern, "").replace(/\n{3,}/g, "\n\n");
445
- fs2.writeFileSync(rcFile, updated, "utf-8");
615
+ fs5.writeFileSync(rcFile, updated, "utf-8");
446
616
  console.log(`KeepGoing hook removed from ${rcFile}.`);
447
617
  console.log(`Reload your shell config to deactivate it:
448
618
  `);
449
619
  console.log(` source ${rcFile}`);
450
620
  }
451
621
 
622
+ // src/commands/activate.ts
623
+ async function activateCommand({ licenseKey }) {
624
+ if (!licenseKey) {
625
+ console.error("Usage: keepgoing activate <license-key>");
626
+ process.exit(1);
627
+ }
628
+ const existing = readLicenseCache();
629
+ if (isCachedLicenseValid(existing)) {
630
+ const who2 = existing.customerName ? ` (${existing.customerName})` : "";
631
+ console.log(`Pro license is already active${who2}.`);
632
+ return;
633
+ }
634
+ console.log("Activating license...");
635
+ const result = await activateLicense(licenseKey, getDeviceId());
636
+ if (!result.valid) {
637
+ console.error(`Activation failed: ${result.error ?? "unknown error"}`);
638
+ process.exit(1);
639
+ }
640
+ const now = (/* @__PURE__ */ new Date()).toISOString();
641
+ writeLicenseCache({
642
+ licenseKey: result.licenseKey || licenseKey,
643
+ instanceId: result.instanceId || getDeviceId(),
644
+ status: "active",
645
+ lastValidatedAt: now,
646
+ activatedAt: now,
647
+ customerName: result.customerName,
648
+ productName: result.productName
649
+ });
650
+ const who = result.customerName ? ` Welcome, ${result.customerName}!` : "";
651
+ console.log(`Pro license activated successfully.${who}`);
652
+ }
653
+
654
+ // src/commands/deactivate.ts
655
+ async function deactivateCommand() {
656
+ const cache = readLicenseCache();
657
+ if (!cache) {
658
+ console.log("No active license found on this device.");
659
+ return;
660
+ }
661
+ console.log("Deactivating license...");
662
+ const result = await deactivateLicense(cache.licenseKey, cache.instanceId);
663
+ deleteLicenseCache();
664
+ if (!result.deactivated) {
665
+ console.error(`License cleared locally, but remote deactivation failed: ${result.error ?? "unknown error"}`);
666
+ process.exit(1);
667
+ }
668
+ console.log("Pro license deactivated successfully. The activation slot has been freed.");
669
+ }
670
+
452
671
  // src/index.ts
453
672
  var HELP_TEXT = `
454
673
  keepgoing: resume side projects without the mental friction
455
674
 
456
675
  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
676
+ keepgoing status Show the last checkpoint for this project
677
+ keepgoing save Save a new checkpoint interactively
678
+ keepgoing hook Manage the shell hook
679
+ keepgoing activate <key> Activate a Pro license on this device
680
+ keepgoing deactivate Deactivate the Pro license from this device
460
681
 
461
682
  Options:
462
683
  --cwd <path> Override the working directory (default: current directory)
@@ -491,6 +712,7 @@ function parseArgs(argv) {
491
712
  subcommand = arg;
492
713
  }
493
714
  }
715
+ cwd = findGitRoot(cwd);
494
716
  return { command, subcommand, cwd, json, quiet };
495
717
  }
496
718
  async function main() {
@@ -514,6 +736,12 @@ async function main() {
514
736
  process.exit(1);
515
737
  }
516
738
  break;
739
+ case "activate":
740
+ await activateCommand({ licenseKey: subcommand });
741
+ break;
742
+ case "deactivate":
743
+ await deactivateCommand();
744
+ break;
517
745
  case "help":
518
746
  case "":
519
747
  console.log(HELP_TEXT);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keepgoingdev/cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Terminal CLI for KeepGoing. Resume side projects without the mental friction.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",