@keepgoingdev/cli 0.1.1 → 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 +223 -33
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -52,20 +52,49 @@ function formatRelativeTime(timestamp) {
52
52
 
53
53
  // ../../packages/shared/src/gitUtils.ts
54
54
  import { execFileSync, execFile } from "child_process";
55
+ import path from "path";
55
56
  import { promisify } from "util";
56
57
  var execFileAsync = promisify(execFile);
57
58
  function findGitRoot(startPath) {
58
59
  try {
59
- const result = execFileSync("git", ["rev-parse", "--show-toplevel"], {
60
+ const gitCommonDir = execFileSync("git", ["rev-parse", "--git-common-dir"], {
60
61
  cwd: startPath,
61
62
  encoding: "utf-8",
62
63
  timeout: 5e3
63
- });
64
- return result.trim() || startPath;
64
+ }).trim();
65
+ if (!gitCommonDir) return startPath;
66
+ const absoluteGitDir = path.isAbsolute(gitCommonDir) ? gitCommonDir : path.resolve(startPath, gitCommonDir);
67
+ return path.dirname(absoluteGitDir);
65
68
  } catch {
66
69
  return startPath;
67
70
  }
68
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
+ }
69
98
  function getCurrentBranch(workspacePath) {
70
99
  try {
71
100
  const result = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
@@ -102,7 +131,7 @@ function getRecentSessions(allSessions, count = RECENT_SESSION_COUNT) {
102
131
 
103
132
  // ../../packages/shared/src/storage.ts
104
133
  import fs from "fs";
105
- import path from "path";
134
+ import path2 from "path";
106
135
  import { randomUUID as randomUUID2 } from "crypto";
107
136
  var STORAGE_DIR = ".keepgoing";
108
137
  var META_FILE = "meta.json";
@@ -114,10 +143,11 @@ var KeepGoingWriter = class {
114
143
  stateFilePath;
115
144
  metaFilePath;
116
145
  constructor(workspacePath) {
117
- this.storagePath = path.join(workspacePath, STORAGE_DIR);
118
- this.sessionsFilePath = path.join(this.storagePath, SESSIONS_FILE);
119
- this.stateFilePath = path.join(this.storagePath, STATE_FILE);
120
- 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);
121
151
  }
122
152
  ensureDir() {
123
153
  if (!fs.existsSync(this.storagePath)) {
@@ -182,7 +212,7 @@ var KeepGoingWriter = class {
182
212
 
183
213
  // ../../packages/shared/src/decisionStorage.ts
184
214
  import fs2 from "fs";
185
- import path2 from "path";
215
+ import path3 from "path";
186
216
 
187
217
  // ../../packages/shared/src/featureGate.ts
188
218
  var DefaultFeatureGate = class {
@@ -192,9 +222,112 @@ var DefaultFeatureGate = class {
192
222
  };
193
223
  var currentGate = new DefaultFeatureGate();
194
224
 
195
- // src/storage.ts
225
+ // ../../packages/shared/src/license.ts
226
+ import crypto from "crypto";
196
227
  import fs3 from "fs";
197
- import path3 from "path";
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";
198
331
  var STORAGE_DIR2 = ".keepgoing";
199
332
  var META_FILE2 = "meta.json";
200
333
  var SESSIONS_FILE2 = "sessions.json";
@@ -205,13 +338,13 @@ var KeepGoingReader = class {
205
338
  sessionsFilePath;
206
339
  stateFilePath;
207
340
  constructor(workspacePath) {
208
- this.storagePath = path3.join(workspacePath, STORAGE_DIR2);
209
- this.metaFilePath = path3.join(this.storagePath, META_FILE2);
210
- this.sessionsFilePath = path3.join(this.storagePath, SESSIONS_FILE2);
211
- this.stateFilePath = path3.join(this.storagePath, STATE_FILE2);
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);
212
345
  }
213
346
  exists() {
214
- return fs3.existsSync(this.storagePath);
347
+ return fs4.existsSync(this.storagePath);
215
348
  }
216
349
  getState() {
217
350
  return this.readJsonFile(this.stateFilePath);
@@ -249,8 +382,8 @@ var KeepGoingReader = class {
249
382
  }
250
383
  readJsonFile(filePath) {
251
384
  try {
252
- if (!fs3.existsSync(filePath)) return void 0;
253
- const raw = fs3.readFileSync(filePath, "utf-8");
385
+ if (!fs4.existsSync(filePath)) return void 0;
386
+ const raw = fs4.readFileSync(filePath, "utf-8");
254
387
  return JSON.parse(raw);
255
388
  } catch {
256
389
  return void 0;
@@ -337,7 +470,7 @@ async function statusCommand(opts) {
337
470
 
338
471
  // src/commands/save.ts
339
472
  import readline from "readline";
340
- import path4 from "path";
473
+ import path6 from "path";
341
474
  function prompt(rl, question) {
342
475
  return new Promise((resolve) => {
343
476
  rl.question(question, (answer) => {
@@ -381,16 +514,16 @@ async function saveCommand(opts) {
381
514
  workspaceRoot: opts.cwd,
382
515
  source: "manual"
383
516
  });
384
- const projectName = path4.basename(opts.cwd);
517
+ const projectName = path6.basename(opts.cwd);
385
518
  const writer = new KeepGoingWriter(opts.cwd);
386
519
  writer.saveCheckpoint(checkpoint, projectName);
387
520
  console.log("Checkpoint saved.");
388
521
  }
389
522
 
390
523
  // src/commands/hook.ts
391
- import fs4 from "fs";
392
- import path5 from "path";
393
- import os from "os";
524
+ import fs5 from "fs";
525
+ import path7 from "path";
526
+ import os2 from "os";
394
527
  var HOOK_MARKER_START = "# keepgoing-hook-start";
395
528
  var HOOK_MARKER_END = "# keepgoing-hook-end";
396
529
  var ZSH_HOOK = `${HOOK_MARKER_START}
@@ -416,12 +549,12 @@ fi
416
549
  ${HOOK_MARKER_END}`;
417
550
  function detectShellRcFile() {
418
551
  const shellEnv = process.env["SHELL"] ?? "";
419
- const home = os.homedir();
552
+ const home = os2.homedir();
420
553
  if (shellEnv.endsWith("zsh")) {
421
- return { shell: "zsh", rcFile: path5.join(home, ".zshrc") };
554
+ return { shell: "zsh", rcFile: path7.join(home, ".zshrc") };
422
555
  }
423
556
  if (shellEnv.endsWith("bash")) {
424
- return { shell: "bash", rcFile: path5.join(home, ".bashrc") };
557
+ return { shell: "bash", rcFile: path7.join(home, ".bashrc") };
425
558
  }
426
559
  return void 0;
427
560
  }
@@ -437,14 +570,14 @@ function hookInstallCommand() {
437
570
  const hookBlock = shell === "zsh" ? ZSH_HOOK : BASH_HOOK;
438
571
  let existing = "";
439
572
  try {
440
- existing = fs4.readFileSync(rcFile, "utf-8");
573
+ existing = fs5.readFileSync(rcFile, "utf-8");
441
574
  } catch {
442
575
  }
443
576
  if (existing.includes(HOOK_MARKER_START)) {
444
577
  console.log(`KeepGoing hook is already installed in ${rcFile}.`);
445
578
  return;
446
579
  }
447
- fs4.appendFileSync(rcFile, `
580
+ fs5.appendFileSync(rcFile, `
448
581
  ${hookBlock}
449
582
  `, "utf-8");
450
583
  console.log(`KeepGoing hook installed in ${rcFile}.`);
@@ -463,7 +596,7 @@ function hookUninstallCommand() {
463
596
  const { rcFile } = detected;
464
597
  let existing = "";
465
598
  try {
466
- existing = fs4.readFileSync(rcFile, "utf-8");
599
+ existing = fs5.readFileSync(rcFile, "utf-8");
467
600
  } catch {
468
601
  console.log(`${rcFile} not found \u2014 nothing to remove.`);
469
602
  return;
@@ -479,21 +612,72 @@ function hookUninstallCommand() {
479
612
  "g"
480
613
  );
481
614
  const updated = existing.replace(pattern, "").replace(/\n{3,}/g, "\n\n");
482
- fs4.writeFileSync(rcFile, updated, "utf-8");
615
+ fs5.writeFileSync(rcFile, updated, "utf-8");
483
616
  console.log(`KeepGoing hook removed from ${rcFile}.`);
484
617
  console.log(`Reload your shell config to deactivate it:
485
618
  `);
486
619
  console.log(` source ${rcFile}`);
487
620
  }
488
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
+
489
671
  // src/index.ts
490
672
  var HELP_TEXT = `
491
673
  keepgoing: resume side projects without the mental friction
492
674
 
493
675
  Usage:
494
- keepgoing status Show the last checkpoint for this project
495
- keepgoing save Save a new checkpoint interactively
496
- 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
497
681
 
498
682
  Options:
499
683
  --cwd <path> Override the working directory (default: current directory)
@@ -552,6 +736,12 @@ async function main() {
552
736
  process.exit(1);
553
737
  }
554
738
  break;
739
+ case "activate":
740
+ await activateCommand({ licenseKey: subcommand });
741
+ break;
742
+ case "deactivate":
743
+ await deactivateCommand();
744
+ break;
555
745
  case "help":
556
746
  case "":
557
747
  console.log(HELP_TEXT);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keepgoingdev/cli",
3
- "version": "0.1.1",
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",