@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.
- package/dist/index.js +223 -33
- 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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
118
|
-
this.
|
|
119
|
-
this.
|
|
120
|
-
this.
|
|
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
|
|
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/
|
|
225
|
+
// ../../packages/shared/src/license.ts
|
|
226
|
+
import crypto from "crypto";
|
|
196
227
|
import fs3 from "fs";
|
|
197
|
-
import
|
|
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 =
|
|
209
|
-
this.metaFilePath =
|
|
210
|
-
this.sessionsFilePath =
|
|
211
|
-
this.stateFilePath =
|
|
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
|
|
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 (!
|
|
253
|
-
const raw =
|
|
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
|
|
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 =
|
|
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
|
|
392
|
-
import
|
|
393
|
-
import
|
|
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 =
|
|
552
|
+
const home = os2.homedir();
|
|
420
553
|
if (shellEnv.endsWith("zsh")) {
|
|
421
|
-
return { shell: "zsh", rcFile:
|
|
554
|
+
return { shell: "zsh", rcFile: path7.join(home, ".zshrc") };
|
|
422
555
|
}
|
|
423
556
|
if (shellEnv.endsWith("bash")) {
|
|
424
|
-
return { shell: "bash", rcFile:
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
495
|
-
keepgoing save
|
|
496
|
-
keepgoing 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);
|