@keepgoingdev/cli 0.2.0 → 0.3.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 +411 -80
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -57,14 +57,12 @@ import { promisify } from "util";
57
57
  var execFileAsync = promisify(execFile);
58
58
  function findGitRoot(startPath) {
59
59
  try {
60
- const gitCommonDir = execFileSync("git", ["rev-parse", "--git-common-dir"], {
60
+ const toplevel = execFileSync("git", ["rev-parse", "--show-toplevel"], {
61
61
  cwd: startPath,
62
62
  encoding: "utf-8",
63
63
  timeout: 5e3
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
+ return toplevel || startPath;
68
66
  } catch {
69
67
  return startPath;
70
68
  }
@@ -132,22 +130,34 @@ function getRecentSessions(allSessions, count = RECENT_SESSION_COUNT) {
132
130
  // ../../packages/shared/src/storage.ts
133
131
  import fs from "fs";
134
132
  import path2 from "path";
135
- import { randomUUID as randomUUID2 } from "crypto";
133
+ import { randomUUID as randomUUID2, createHash } from "crypto";
136
134
  var STORAGE_DIR = ".keepgoing";
137
135
  var META_FILE = "meta.json";
138
136
  var SESSIONS_FILE = "sessions.json";
139
137
  var STATE_FILE = "state.json";
138
+ var CURRENT_TASKS_FILE = "current-tasks.json";
139
+ var STALE_SESSION_MS = 2 * 60 * 60 * 1e3;
140
+ function pruneStaleTasks(tasks) {
141
+ const now = Date.now();
142
+ return tasks.filter((t) => {
143
+ if (t.sessionActive) return true;
144
+ const updatedAt = new Date(t.updatedAt).getTime();
145
+ return !isNaN(updatedAt) && now - updatedAt < STALE_SESSION_MS;
146
+ });
147
+ }
140
148
  var KeepGoingWriter = class {
141
149
  storagePath;
142
150
  sessionsFilePath;
143
151
  stateFilePath;
144
152
  metaFilePath;
153
+ currentTasksFilePath;
145
154
  constructor(workspacePath) {
146
155
  const mainRoot = resolveStorageRoot(workspacePath);
147
156
  this.storagePath = path2.join(mainRoot, STORAGE_DIR);
148
157
  this.sessionsFilePath = path2.join(this.storagePath, SESSIONS_FILE);
149
158
  this.stateFilePath = path2.join(this.storagePath, STATE_FILE);
150
159
  this.metaFilePath = path2.join(this.storagePath, META_FILE);
160
+ this.currentTasksFilePath = path2.join(this.storagePath, CURRENT_TASKS_FILE);
151
161
  }
152
162
  ensureDir() {
153
163
  if (!fs.existsSync(this.storagePath)) {
@@ -208,120 +218,300 @@ var KeepGoingWriter = class {
208
218
  }
209
219
  fs.writeFileSync(this.metaFilePath, JSON.stringify(meta, null, 2), "utf-8");
210
220
  }
221
+ // ---------------------------------------------------------------------------
222
+ // Multi-session API
223
+ // ---------------------------------------------------------------------------
224
+ /** Read all current tasks from current-tasks.json. Auto-prunes stale sessions. */
225
+ readCurrentTasks() {
226
+ try {
227
+ if (fs.existsSync(this.currentTasksFilePath)) {
228
+ const raw = JSON.parse(fs.readFileSync(this.currentTasksFilePath, "utf-8"));
229
+ const tasks = Array.isArray(raw) ? raw : raw.tasks ?? [];
230
+ return this.pruneStale(tasks);
231
+ }
232
+ } catch {
233
+ }
234
+ return [];
235
+ }
236
+ /**
237
+ * Upsert a session task by sessionId into current-tasks.json.
238
+ * If no sessionId is present on the task, generates one.
239
+ */
240
+ upsertSession(update) {
241
+ this.ensureDir();
242
+ this.upsertSessionCore(update);
243
+ }
244
+ /** Core upsert logic: merges the update into current-tasks.json and returns the pruned task list. */
245
+ upsertSessionCore(update) {
246
+ this.ensureDir();
247
+ const sessionId = update.sessionId || generateSessionId(update);
248
+ const tasks = this.readAllTasksRaw();
249
+ const existingIdx = tasks.findIndex((t) => t.sessionId === sessionId);
250
+ let merged;
251
+ if (existingIdx >= 0) {
252
+ const existing = tasks[existingIdx];
253
+ merged = { ...existing, ...update, sessionId };
254
+ tasks[existingIdx] = merged;
255
+ } else {
256
+ merged = { ...update, sessionId };
257
+ tasks.push(merged);
258
+ }
259
+ const pruned = this.pruneStale(tasks);
260
+ this.writeTasksFile(pruned);
261
+ return pruned;
262
+ }
263
+ /** Remove a specific session by ID. */
264
+ removeSession(sessionId) {
265
+ const tasks = this.readAllTasksRaw().filter((t) => t.sessionId !== sessionId);
266
+ this.writeTasksFile(tasks);
267
+ }
268
+ /** Get all active sessions (sessionActive=true and within stale threshold). */
269
+ getActiveSessions() {
270
+ return this.readCurrentTasks().filter((t) => t.sessionActive);
271
+ }
272
+ /** Get a specific session by ID. */
273
+ getSession(sessionId) {
274
+ return this.readCurrentTasks().find((t) => t.sessionId === sessionId);
275
+ }
276
+ // ---------------------------------------------------------------------------
277
+ // Private helpers
278
+ // ---------------------------------------------------------------------------
279
+ readAllTasksRaw() {
280
+ try {
281
+ if (fs.existsSync(this.currentTasksFilePath)) {
282
+ const raw = JSON.parse(fs.readFileSync(this.currentTasksFilePath, "utf-8"));
283
+ return Array.isArray(raw) ? [...raw] : [...raw.tasks ?? []];
284
+ }
285
+ } catch {
286
+ }
287
+ return [];
288
+ }
289
+ pruneStale(tasks) {
290
+ return pruneStaleTasks(tasks);
291
+ }
292
+ writeTasksFile(tasks) {
293
+ const data = { version: 1, tasks };
294
+ fs.writeFileSync(this.currentTasksFilePath, JSON.stringify(data, null, 2), "utf-8");
295
+ }
211
296
  };
297
+ function generateSessionId(context) {
298
+ const parts = [
299
+ context.worktreePath || context.workspaceRoot || "",
300
+ context.agentLabel || "",
301
+ context.branch || ""
302
+ ].filter(Boolean);
303
+ if (parts.length === 0) {
304
+ return randomUUID2();
305
+ }
306
+ const hash = createHash("sha256").update(parts.join("|")).digest("hex").slice(0, 12);
307
+ return `ses_${hash}`;
308
+ }
212
309
 
213
310
  // ../../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();
311
+ import fs3 from "fs";
312
+ import path4 from "path";
224
313
 
225
314
  // ../../packages/shared/src/license.ts
226
315
  import crypto from "crypto";
227
- import fs3 from "fs";
316
+ import fs2 from "fs";
228
317
  import os from "os";
229
- import path4 from "path";
318
+ import path3 from "path";
230
319
  var LICENSE_FILE = "license.json";
231
320
  var DEVICE_ID_FILE = "device-id";
232
321
  function getGlobalLicenseDir() {
233
- return path4.join(os.homedir(), ".keepgoing");
322
+ return path3.join(os.homedir(), ".keepgoing");
234
323
  }
235
324
  function getGlobalLicensePath() {
236
- return path4.join(getGlobalLicenseDir(), LICENSE_FILE);
325
+ return path3.join(getGlobalLicenseDir(), LICENSE_FILE);
237
326
  }
238
327
  function getDeviceId() {
239
328
  const dir = getGlobalLicenseDir();
240
- const filePath = path4.join(dir, DEVICE_ID_FILE);
329
+ const filePath = path3.join(dir, DEVICE_ID_FILE);
241
330
  try {
242
- const existing = fs3.readFileSync(filePath, "utf-8").trim();
331
+ const existing = fs2.readFileSync(filePath, "utf-8").trim();
243
332
  if (existing) return existing;
244
333
  } catch {
245
334
  }
246
335
  const id = crypto.randomUUID();
247
- if (!fs3.existsSync(dir)) {
248
- fs3.mkdirSync(dir, { recursive: true });
336
+ if (!fs2.existsSync(dir)) {
337
+ fs2.mkdirSync(dir, { recursive: true });
249
338
  }
250
- fs3.writeFileSync(filePath, id, "utf-8");
339
+ fs2.writeFileSync(filePath, id, "utf-8");
251
340
  return id;
252
341
  }
253
- function readLicenseCache() {
342
+ var DECISION_DETECTION_VARIANT_ID = 1361527;
343
+ var SESSION_AWARENESS_VARIANT_ID = 1366510;
344
+ var TEST_DECISION_DETECTION_VARIANT_ID = 1345647;
345
+ var TEST_SESSION_AWARENESS_VARIANT_ID = 1365992;
346
+ var VARIANT_FEATURE_MAP = {
347
+ [DECISION_DETECTION_VARIANT_ID]: ["decisions"],
348
+ [SESSION_AWARENESS_VARIANT_ID]: ["session-awareness"],
349
+ [TEST_DECISION_DETECTION_VARIANT_ID]: ["decisions"],
350
+ [TEST_SESSION_AWARENESS_VARIANT_ID]: ["session-awareness"]
351
+ // Future bundle: [BUNDLE_VARIANT_ID]: ['decisions', 'session-awareness'],
352
+ };
353
+ var KNOWN_VARIANT_IDS = new Set(Object.keys(VARIANT_FEATURE_MAP).map(Number));
354
+ function getVariantLabel(variantId) {
355
+ const features = VARIANT_FEATURE_MAP[variantId];
356
+ if (!features) return "Unknown Add-on";
357
+ if (features.includes("decisions") && features.includes("session-awareness")) return "Pro Bundle";
358
+ if (features.includes("decisions")) return "Decision Detection";
359
+ if (features.includes("session-awareness")) return "Session Awareness";
360
+ return "Pro Add-on";
361
+ }
362
+ var _cachedStore;
363
+ var _cacheTimestamp = 0;
364
+ var LICENSE_CACHE_TTL_MS = 2e3;
365
+ function readLicenseStore() {
366
+ const now = Date.now();
367
+ if (_cachedStore && now - _cacheTimestamp < LICENSE_CACHE_TTL_MS) {
368
+ return _cachedStore;
369
+ }
254
370
  const licensePath = getGlobalLicensePath();
371
+ let store;
255
372
  try {
256
- if (!fs3.existsSync(licensePath)) {
257
- return void 0;
373
+ if (!fs2.existsSync(licensePath)) {
374
+ store = { version: 2, licenses: [] };
375
+ } else {
376
+ const raw = fs2.readFileSync(licensePath, "utf-8");
377
+ const data = JSON.parse(raw);
378
+ if (data?.version === 2 && Array.isArray(data.licenses)) {
379
+ store = data;
380
+ } else {
381
+ store = { version: 2, licenses: [] };
382
+ }
258
383
  }
259
- const raw = fs3.readFileSync(licensePath, "utf-8");
260
- return JSON.parse(raw);
261
384
  } catch {
262
- return void 0;
385
+ store = { version: 2, licenses: [] };
263
386
  }
387
+ _cachedStore = store;
388
+ _cacheTimestamp = now;
389
+ return store;
264
390
  }
265
- function writeLicenseCache(cache) {
391
+ function writeLicenseStore(store) {
266
392
  const dirPath = getGlobalLicenseDir();
267
- if (!fs3.existsSync(dirPath)) {
268
- fs3.mkdirSync(dirPath, { recursive: true });
393
+ if (!fs2.existsSync(dirPath)) {
394
+ fs2.mkdirSync(dirPath, { recursive: true });
269
395
  }
270
- const licensePath = path4.join(dirPath, LICENSE_FILE);
271
- fs3.writeFileSync(licensePath, JSON.stringify(cache, null, 2), "utf-8");
396
+ const licensePath = path3.join(dirPath, LICENSE_FILE);
397
+ fs2.writeFileSync(licensePath, JSON.stringify(store, null, 2), "utf-8");
398
+ _cachedStore = store;
399
+ _cacheTimestamp = Date.now();
272
400
  }
273
- function deleteLicenseCache() {
274
- const licensePath = getGlobalLicensePath();
275
- try {
276
- if (fs3.existsSync(licensePath)) {
277
- fs3.unlinkSync(licensePath);
278
- }
279
- } catch {
401
+ function addLicenseEntry(entry) {
402
+ const store = readLicenseStore();
403
+ const idx = store.licenses.findIndex((l) => l.licenseKey === entry.licenseKey);
404
+ if (idx >= 0) {
405
+ store.licenses[idx] = entry;
406
+ } else {
407
+ store.licenses.push(entry);
280
408
  }
409
+ writeLicenseStore(store);
281
410
  }
282
- function isCachedLicenseValid(cache) {
283
- return cache?.status === "active";
411
+ function removeLicenseEntry(licenseKey) {
412
+ const store = readLicenseStore();
413
+ store.licenses = store.licenses.filter((l) => l.licenseKey !== licenseKey);
414
+ writeLicenseStore(store);
415
+ }
416
+ function getActiveLicenses() {
417
+ return readLicenseStore().licenses.filter((l) => l.status === "active");
284
418
  }
285
419
  var REVALIDATION_THRESHOLD_MS = 24 * 60 * 60 * 1e3;
286
420
 
421
+ // ../../packages/shared/src/featureGate.ts
422
+ var DefaultFeatureGate = class {
423
+ isEnabled(_feature) {
424
+ return true;
425
+ }
426
+ };
427
+ var currentGate = new DefaultFeatureGate();
428
+
287
429
  // ../../packages/shared/src/licenseClient.ts
288
430
  var BASE_URL = "https://api.lemonsqueezy.com/v1/licenses";
289
- async function activateLicense(licenseKey, instanceName) {
431
+ var REQUEST_TIMEOUT_MS = 15e3;
432
+ var EXPECTED_STORE_ID = 301555;
433
+ var EXPECTED_PRODUCT_ID = 864311;
434
+ function fetchWithTimeout(url, init) {
435
+ const controller = new AbortController();
436
+ const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
437
+ return fetch(url, { ...init, signal: controller.signal }).finally(() => clearTimeout(timer));
438
+ }
439
+ function validateProductIdentity(meta) {
440
+ if (!meta) return "License response missing product metadata.";
441
+ if (meta.store_id !== EXPECTED_STORE_ID || meta.product_id !== EXPECTED_PRODUCT_ID) {
442
+ return "This license key does not belong to KeepGoing.";
443
+ }
444
+ return void 0;
445
+ }
446
+ async function safeJson(res) {
290
447
  try {
291
- const res = await fetch(`${BASE_URL}/activate`, {
448
+ const text = await res.text();
449
+ return JSON.parse(text);
450
+ } catch {
451
+ return null;
452
+ }
453
+ }
454
+ async function activateLicense(licenseKey, instanceName, options) {
455
+ try {
456
+ const res = await fetchWithTimeout(`${BASE_URL}/activate`, {
292
457
  method: "POST",
293
458
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
294
459
  body: new URLSearchParams({ license_key: licenseKey, instance_name: instanceName })
295
460
  });
296
- const data = await res.json();
297
- if (!res.ok || !data.activated) {
298
- return { valid: false, error: data.error || `Activation failed (${res.status})` };
461
+ const data = await safeJson(res);
462
+ if (!res.ok || !data?.activated) {
463
+ return { valid: false, error: data?.error || `Activation failed (${res.status})` };
464
+ }
465
+ if (!options?.allowTestMode && data.license_key?.test_mode) {
466
+ if (data.license_key?.key && data.instance?.id) {
467
+ await deactivateLicense(data.license_key.key, data.instance.id);
468
+ }
469
+ return { valid: false, error: "This is a test license key. Please use a production license key from your purchase confirmation." };
470
+ }
471
+ if (!options?.allowTestMode) {
472
+ const productError = validateProductIdentity(data.meta);
473
+ if (productError) {
474
+ if (data.license_key?.key && data.instance?.id) {
475
+ await deactivateLicense(data.license_key.key, data.instance.id);
476
+ }
477
+ return { valid: false, error: productError };
478
+ }
479
+ if (data.meta?.variant_id && !KNOWN_VARIANT_IDS.has(data.meta.variant_id)) {
480
+ if (data.license_key?.key && data.instance?.id) {
481
+ await deactivateLicense(data.license_key.key, data.instance.id);
482
+ }
483
+ return { valid: false, error: "This license key is for an unrecognized add-on variant. Please update KeepGoing or contact support." };
484
+ }
299
485
  }
300
486
  return {
301
487
  valid: true,
302
488
  licenseKey: data.license_key?.key,
303
489
  instanceId: data.instance?.id,
304
490
  customerName: data.meta?.customer_name,
305
- productName: data.meta?.product_name
491
+ productName: data.meta?.product_name,
492
+ variantId: data.meta?.variant_id,
493
+ variantName: data.meta?.variant_name
306
494
  };
307
495
  } catch (err) {
308
- return { valid: false, error: err instanceof Error ? err.message : "Network error" };
496
+ const message = err instanceof Error && err.name === "AbortError" ? "Request timed out. Please check your network connection and try again." : err instanceof Error ? err.message : "Network error";
497
+ return { valid: false, error: message };
309
498
  }
310
499
  }
311
500
  async function deactivateLicense(licenseKey, instanceId) {
312
501
  try {
313
- const res = await fetch(`${BASE_URL}/deactivate`, {
502
+ const res = await fetchWithTimeout(`${BASE_URL}/deactivate`, {
314
503
  method: "POST",
315
504
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
316
505
  body: new URLSearchParams({ license_key: licenseKey, instance_id: instanceId })
317
506
  });
318
- const data = await res.json();
319
- if (!res.ok || !data.deactivated) {
320
- return { deactivated: false, error: data.error || `Deactivation failed (${res.status})` };
507
+ const data = await safeJson(res);
508
+ if (!res.ok || !data?.deactivated) {
509
+ return { deactivated: false, error: data?.error || `Deactivation failed (${res.status})` };
321
510
  }
322
511
  return { deactivated: true };
323
512
  } catch (err) {
324
- return { deactivated: false, error: err instanceof Error ? err.message : "Network error" };
513
+ const message = err instanceof Error && err.name === "AbortError" ? "Request timed out. Please check your network connection and try again." : err instanceof Error ? err.message : "Network error";
514
+ return { deactivated: false, error: message };
325
515
  }
326
516
  }
327
517
 
@@ -438,7 +628,96 @@ function renderNoData() {
438
628
  );
439
629
  }
440
630
 
631
+ // src/updateCheck.ts
632
+ import { spawn } from "child_process";
633
+ import { readFileSync, existsSync } from "fs";
634
+ import path6 from "path";
635
+ import os2 from "os";
636
+ var CLI_VERSION = "0.3.0";
637
+ var NPM_REGISTRY_URL = "https://registry.npmjs.org/@keepgoingdev/cli/latest";
638
+ var FETCH_TIMEOUT_MS = 5e3;
639
+ var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
640
+ var CACHE_DIR = path6.join(os2.homedir(), ".keepgoing");
641
+ var CACHE_PATH = path6.join(CACHE_DIR, "update-check.json");
642
+ function isNewerVersion(current, latest) {
643
+ const cur = current.split(".").map(Number);
644
+ const lat = latest.split(".").map(Number);
645
+ for (let i = 0; i < 3; i++) {
646
+ if ((lat[i] ?? 0) > (cur[i] ?? 0)) return true;
647
+ if ((lat[i] ?? 0) < (cur[i] ?? 0)) return false;
648
+ }
649
+ return false;
650
+ }
651
+ function getCachedUpdateInfo() {
652
+ try {
653
+ if (!existsSync(CACHE_PATH)) return null;
654
+ const raw = readFileSync(CACHE_PATH, "utf-8");
655
+ const cache = JSON.parse(raw);
656
+ if (!cache.latest || !cache.checkedAt) return null;
657
+ const age = Date.now() - new Date(cache.checkedAt).getTime();
658
+ if (age > CHECK_INTERVAL_MS) return null;
659
+ return {
660
+ current: CLI_VERSION,
661
+ latest: cache.latest,
662
+ updateAvailable: isNewerVersion(CLI_VERSION, cache.latest)
663
+ };
664
+ } catch {
665
+ return null;
666
+ }
667
+ }
668
+ function spawnBackgroundCheck() {
669
+ try {
670
+ if (existsSync(CACHE_PATH)) {
671
+ const raw = readFileSync(CACHE_PATH, "utf-8");
672
+ const cache = JSON.parse(raw);
673
+ const age = Date.now() - new Date(cache.checkedAt).getTime();
674
+ if (age < CHECK_INTERVAL_MS) return;
675
+ }
676
+ } catch {
677
+ }
678
+ const script = `
679
+ const https = require('https');
680
+ const fs = require('fs');
681
+ const path = require('path');
682
+ const os = require('os');
683
+
684
+ const url = ${JSON.stringify(NPM_REGISTRY_URL)};
685
+ const cacheDir = path.join(os.homedir(), '.keepgoing');
686
+ const cachePath = path.join(cacheDir, 'update-check.json');
687
+ const currentVersion = ${JSON.stringify(CLI_VERSION)};
688
+
689
+ const req = https.get(url, { timeout: ${FETCH_TIMEOUT_MS} }, (res) => {
690
+ let data = '';
691
+ res.on('data', (chunk) => { data += chunk; });
692
+ res.on('end', () => {
693
+ try {
694
+ const latest = JSON.parse(data).version;
695
+ if (!latest) process.exit(0);
696
+ if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir, { recursive: true });
697
+ fs.writeFileSync(cachePath, JSON.stringify({
698
+ latest,
699
+ current: currentVersion,
700
+ checkedAt: new Date().toISOString(),
701
+ }));
702
+ } catch {}
703
+ process.exit(0);
704
+ });
705
+ });
706
+ req.on('error', () => process.exit(0));
707
+ req.on('timeout', () => { req.destroy(); process.exit(0); });
708
+ `;
709
+ const child = spawn(process.execPath, ["-e", script], {
710
+ detached: true,
711
+ stdio: "ignore",
712
+ env: { ...process.env }
713
+ });
714
+ child.unref();
715
+ }
716
+
441
717
  // src/commands/status.ts
718
+ var RESET2 = "\x1B[0m";
719
+ var BOLD2 = "\x1B[1m";
720
+ var DIM2 = "\x1B[2m";
442
721
  async function statusCommand(opts) {
443
722
  const reader = new KeepGoingReader(opts.cwd);
444
723
  if (!reader.exists()) {
@@ -466,11 +745,16 @@ async function statusCommand(opts) {
466
745
  (Date.now() - new Date(lastSession.timestamp).getTime()) / (1e3 * 60 * 60 * 24)
467
746
  );
468
747
  renderCheckpoint(lastSession, daysSince);
748
+ const cached = getCachedUpdateInfo();
749
+ if (cached?.updateAvailable) {
750
+ console.log(`${DIM2}Update available: ${cached.current} \u2192 ${cached.latest}. Run: ${RESET2}${BOLD2}npm install -g @keepgoingdev/cli@latest${RESET2}`);
751
+ }
752
+ spawnBackgroundCheck();
469
753
  }
470
754
 
471
755
  // src/commands/save.ts
472
756
  import readline from "readline";
473
- import path6 from "path";
757
+ import path7 from "path";
474
758
  function prompt(rl, question) {
475
759
  return new Promise((resolve) => {
476
760
  rl.question(question, (answer) => {
@@ -514,7 +798,7 @@ async function saveCommand(opts) {
514
798
  workspaceRoot: opts.cwd,
515
799
  source: "manual"
516
800
  });
517
- const projectName = path6.basename(opts.cwd);
801
+ const projectName = path7.basename(opts.cwd);
518
802
  const writer = new KeepGoingWriter(opts.cwd);
519
803
  writer.saveCheckpoint(checkpoint, projectName);
520
804
  console.log("Checkpoint saved.");
@@ -522,8 +806,8 @@ async function saveCommand(opts) {
522
806
 
523
807
  // src/commands/hook.ts
524
808
  import fs5 from "fs";
525
- import path7 from "path";
526
- import os2 from "os";
809
+ import path8 from "path";
810
+ import os3 from "os";
527
811
  var HOOK_MARKER_START = "# keepgoing-hook-start";
528
812
  var HOOK_MARKER_END = "# keepgoing-hook-end";
529
813
  var ZSH_HOOK = `${HOOK_MARKER_START}
@@ -549,12 +833,12 @@ fi
549
833
  ${HOOK_MARKER_END}`;
550
834
  function detectShellRcFile() {
551
835
  const shellEnv = process.env["SHELL"] ?? "";
552
- const home = os2.homedir();
836
+ const home = os3.homedir();
553
837
  if (shellEnv.endsWith("zsh")) {
554
- return { shell: "zsh", rcFile: path7.join(home, ".zshrc") };
838
+ return { shell: "zsh", rcFile: path8.join(home, ".zshrc") };
555
839
  }
556
840
  if (shellEnv.endsWith("bash")) {
557
- return { shell: "bash", rcFile: path7.join(home, ".bashrc") };
841
+ return { shell: "bash", rcFile: path8.join(home, ".bashrc") };
558
842
  }
559
843
  return void 0;
560
844
  }
@@ -625,10 +909,14 @@ async function activateCommand({ licenseKey }) {
625
909
  console.error("Usage: keepgoing activate <license-key>");
626
910
  process.exit(1);
627
911
  }
628
- const existing = readLicenseCache();
629
- if (isCachedLicenseValid(existing)) {
630
- const who2 = existing.customerName ? ` (${existing.customerName})` : "";
631
- console.log(`Pro license is already active${who2}.`);
912
+ const store = readLicenseStore();
913
+ const existingForKey = store.licenses.find(
914
+ (l) => l.status === "active" && l.licenseKey === licenseKey
915
+ );
916
+ if (existingForKey) {
917
+ const label2 = getVariantLabel(existingForKey.variantId);
918
+ const who2 = existingForKey.customerName ? ` (${existingForKey.customerName})` : "";
919
+ console.log(`${label2} is already active${who2}.`);
632
920
  return;
633
921
  }
634
922
  console.log("Activating license...");
@@ -637,35 +925,72 @@ async function activateCommand({ licenseKey }) {
637
925
  console.error(`Activation failed: ${result.error ?? "unknown error"}`);
638
926
  process.exit(1);
639
927
  }
928
+ const variantId = result.variantId;
929
+ const existingForVariant = store.licenses.find(
930
+ (l) => l.status === "active" && l.variantId === variantId
931
+ );
932
+ if (existingForVariant) {
933
+ const label2 = getVariantLabel(variantId);
934
+ const who2 = existingForVariant.customerName ? ` (${existingForVariant.customerName})` : "";
935
+ console.log(`${label2} is already active${who2}.`);
936
+ return;
937
+ }
640
938
  const now = (/* @__PURE__ */ new Date()).toISOString();
641
- writeLicenseCache({
939
+ addLicenseEntry({
642
940
  licenseKey: result.licenseKey || licenseKey,
643
941
  instanceId: result.instanceId || getDeviceId(),
644
942
  status: "active",
645
943
  lastValidatedAt: now,
646
944
  activatedAt: now,
945
+ variantId,
647
946
  customerName: result.customerName,
648
- productName: result.productName
947
+ productName: result.productName,
948
+ variantName: result.variantName
649
949
  });
950
+ const label = getVariantLabel(variantId);
650
951
  const who = result.customerName ? ` Welcome, ${result.customerName}!` : "";
651
- console.log(`Pro license activated successfully.${who}`);
952
+ console.log(`${label} activated successfully.${who}`);
652
953
  }
653
954
 
654
955
  // src/commands/deactivate.ts
655
- async function deactivateCommand() {
656
- const cache = readLicenseCache();
657
- if (!cache) {
956
+ async function deactivateCommand(opts) {
957
+ const active = getActiveLicenses();
958
+ if (active.length === 0) {
658
959
  console.log("No active license found on this device.");
659
960
  return;
660
961
  }
962
+ let targets;
963
+ if (opts?.licenseKey) {
964
+ const match = active.find((l) => l.licenseKey === opts.licenseKey);
965
+ if (!match) {
966
+ console.error(`No active license found with key "${opts.licenseKey}".`);
967
+ process.exit(1);
968
+ }
969
+ targets = [match];
970
+ } else if (active.length === 1) {
971
+ targets = active;
972
+ } else {
973
+ console.log("Multiple active licenses found. Specify which to deactivate:");
974
+ console.log(" keepgoing deactivate <license-key>");
975
+ console.log("");
976
+ for (const l of active) {
977
+ const label = getVariantLabel(l.variantId);
978
+ const who = l.customerName ? ` (${l.customerName})` : "";
979
+ console.log(` ${label}${who}: ${l.licenseKey}`);
980
+ }
981
+ return;
982
+ }
661
983
  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);
984
+ for (const entry of targets) {
985
+ const result = await deactivateLicense(entry.licenseKey, entry.instanceId);
986
+ removeLicenseEntry(entry.licenseKey);
987
+ const label = getVariantLabel(entry.variantId);
988
+ if (!result.deactivated) {
989
+ console.error(`${label} license cleared locally, but remote deactivation failed: ${result.error ?? "unknown error"}`);
990
+ } else {
991
+ console.log(`${label} license deactivated successfully. The activation slot has been freed.`);
992
+ }
667
993
  }
668
- console.log("Pro license deactivated successfully. The activation slot has been freed.");
669
994
  }
670
995
 
671
996
  // src/index.ts
@@ -683,6 +1008,7 @@ Options:
683
1008
  --cwd <path> Override the working directory (default: current directory)
684
1009
  --json Output raw JSON (status only)
685
1010
  --quiet Output a single summary line (status only)
1011
+ -v, --version Show the CLI version
686
1012
  -h, --help Show this help text
687
1013
 
688
1014
  Hook subcommands:
@@ -704,6 +1030,8 @@ function parseArgs(argv) {
704
1030
  json = true;
705
1031
  } else if (arg === "--quiet") {
706
1032
  quiet = true;
1033
+ } else if (arg === "-v" || arg === "--version") {
1034
+ command = "version";
707
1035
  } else if (arg === "-h" || arg === "--help") {
708
1036
  command = "help";
709
1037
  } else if (!command) {
@@ -736,11 +1064,14 @@ async function main() {
736
1064
  process.exit(1);
737
1065
  }
738
1066
  break;
1067
+ case "version":
1068
+ console.log(`keepgoing v${"0.3.0"}`);
1069
+ break;
739
1070
  case "activate":
740
1071
  await activateCommand({ licenseKey: subcommand });
741
1072
  break;
742
1073
  case "deactivate":
743
- await deactivateCommand();
1074
+ await deactivateCommand({ licenseKey: subcommand || void 0 });
744
1075
  break;
745
1076
  case "help":
746
1077
  case "":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keepgoingdev/cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.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",