@keepgoingdev/cli 0.2.1 → 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 +255 -64
  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,82 +218,214 @@ 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
431
  var REQUEST_TIMEOUT_MS = 15e3;
@@ -334,13 +476,21 @@ async function activateLicense(licenseKey, instanceName, options) {
334
476
  }
335
477
  return { valid: false, error: productError };
336
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
+ }
337
485
  }
338
486
  return {
339
487
  valid: true,
340
488
  licenseKey: data.license_key?.key,
341
489
  instanceId: data.instance?.id,
342
490
  customerName: data.meta?.customer_name,
343
- productName: data.meta?.product_name
491
+ productName: data.meta?.product_name,
492
+ variantId: data.meta?.variant_id,
493
+ variantName: data.meta?.variant_name
344
494
  };
345
495
  } catch (err) {
346
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";
@@ -483,7 +633,7 @@ import { spawn } from "child_process";
483
633
  import { readFileSync, existsSync } from "fs";
484
634
  import path6 from "path";
485
635
  import os2 from "os";
486
- var CLI_VERSION = "0.2.1";
636
+ var CLI_VERSION = "0.3.0";
487
637
  var NPM_REGISTRY_URL = "https://registry.npmjs.org/@keepgoingdev/cli/latest";
488
638
  var FETCH_TIMEOUT_MS = 5e3;
489
639
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
@@ -759,10 +909,14 @@ async function activateCommand({ licenseKey }) {
759
909
  console.error("Usage: keepgoing activate <license-key>");
760
910
  process.exit(1);
761
911
  }
762
- const existing = readLicenseCache();
763
- if (isCachedLicenseValid(existing)) {
764
- const who2 = existing.customerName ? ` (${existing.customerName})` : "";
765
- 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}.`);
766
920
  return;
767
921
  }
768
922
  console.log("Activating license...");
@@ -771,35 +925,72 @@ async function activateCommand({ licenseKey }) {
771
925
  console.error(`Activation failed: ${result.error ?? "unknown error"}`);
772
926
  process.exit(1);
773
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
+ }
774
938
  const now = (/* @__PURE__ */ new Date()).toISOString();
775
- writeLicenseCache({
939
+ addLicenseEntry({
776
940
  licenseKey: result.licenseKey || licenseKey,
777
941
  instanceId: result.instanceId || getDeviceId(),
778
942
  status: "active",
779
943
  lastValidatedAt: now,
780
944
  activatedAt: now,
945
+ variantId,
781
946
  customerName: result.customerName,
782
- productName: result.productName
947
+ productName: result.productName,
948
+ variantName: result.variantName
783
949
  });
950
+ const label = getVariantLabel(variantId);
784
951
  const who = result.customerName ? ` Welcome, ${result.customerName}!` : "";
785
- console.log(`Pro license activated successfully.${who}`);
952
+ console.log(`${label} activated successfully.${who}`);
786
953
  }
787
954
 
788
955
  // src/commands/deactivate.ts
789
- async function deactivateCommand() {
790
- const cache = readLicenseCache();
791
- if (!cache) {
956
+ async function deactivateCommand(opts) {
957
+ const active = getActiveLicenses();
958
+ if (active.length === 0) {
792
959
  console.log("No active license found on this device.");
793
960
  return;
794
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
+ }
795
983
  console.log("Deactivating license...");
796
- const result = await deactivateLicense(cache.licenseKey, cache.instanceId);
797
- deleteLicenseCache();
798
- if (!result.deactivated) {
799
- console.error(`License cleared locally, but remote deactivation failed: ${result.error ?? "unknown error"}`);
800
- 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
+ }
801
993
  }
802
- console.log("Pro license deactivated successfully. The activation slot has been freed.");
803
994
  }
804
995
 
805
996
  // src/index.ts
@@ -874,13 +1065,13 @@ async function main() {
874
1065
  }
875
1066
  break;
876
1067
  case "version":
877
- console.log(`keepgoing v${"0.2.1"}`);
1068
+ console.log(`keepgoing v${"0.3.0"}`);
878
1069
  break;
879
1070
  case "activate":
880
1071
  await activateCommand({ licenseKey: subcommand });
881
1072
  break;
882
1073
  case "deactivate":
883
- await deactivateCommand();
1074
+ await deactivateCommand({ licenseKey: subcommand || void 0 });
884
1075
  break;
885
1076
  case "help":
886
1077
  case "":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keepgoingdev/cli",
3
- "version": "0.2.1",
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",