@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.
- package/dist/index.js +411 -80
- 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
|
|
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
|
-
|
|
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
|
|
215
|
-
import
|
|
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
|
|
316
|
+
import fs2 from "fs";
|
|
228
317
|
import os from "os";
|
|
229
|
-
import
|
|
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
|
|
322
|
+
return path3.join(os.homedir(), ".keepgoing");
|
|
234
323
|
}
|
|
235
324
|
function getGlobalLicensePath() {
|
|
236
|
-
return
|
|
325
|
+
return path3.join(getGlobalLicenseDir(), LICENSE_FILE);
|
|
237
326
|
}
|
|
238
327
|
function getDeviceId() {
|
|
239
328
|
const dir = getGlobalLicenseDir();
|
|
240
|
-
const filePath =
|
|
329
|
+
const filePath = path3.join(dir, DEVICE_ID_FILE);
|
|
241
330
|
try {
|
|
242
|
-
const existing =
|
|
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 (!
|
|
248
|
-
|
|
336
|
+
if (!fs2.existsSync(dir)) {
|
|
337
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
249
338
|
}
|
|
250
|
-
|
|
339
|
+
fs2.writeFileSync(filePath, id, "utf-8");
|
|
251
340
|
return id;
|
|
252
341
|
}
|
|
253
|
-
|
|
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 (!
|
|
257
|
-
|
|
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
|
-
|
|
385
|
+
store = { version: 2, licenses: [] };
|
|
263
386
|
}
|
|
387
|
+
_cachedStore = store;
|
|
388
|
+
_cacheTimestamp = now;
|
|
389
|
+
return store;
|
|
264
390
|
}
|
|
265
|
-
function
|
|
391
|
+
function writeLicenseStore(store) {
|
|
266
392
|
const dirPath = getGlobalLicenseDir();
|
|
267
|
-
if (!
|
|
268
|
-
|
|
393
|
+
if (!fs2.existsSync(dirPath)) {
|
|
394
|
+
fs2.mkdirSync(dirPath, { recursive: true });
|
|
269
395
|
}
|
|
270
|
-
const licensePath =
|
|
271
|
-
|
|
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
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
|
283
|
-
|
|
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;
|
|
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
|
|
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
|
|
297
|
-
if (!res.ok || !data
|
|
298
|
-
return { valid: false, error: data
|
|
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
|
-
|
|
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
|
|
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
|
|
319
|
-
if (!res.ok || !data
|
|
320
|
-
return { deactivated: false, error: data
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
526
|
-
import
|
|
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 =
|
|
836
|
+
const home = os3.homedir();
|
|
553
837
|
if (shellEnv.endsWith("zsh")) {
|
|
554
|
-
return { shell: "zsh", rcFile:
|
|
838
|
+
return { shell: "zsh", rcFile: path8.join(home, ".zshrc") };
|
|
555
839
|
}
|
|
556
840
|
if (shellEnv.endsWith("bash")) {
|
|
557
|
-
return { shell: "bash", rcFile:
|
|
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
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
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
|
-
|
|
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(
|
|
952
|
+
console.log(`${label} activated successfully.${who}`);
|
|
652
953
|
}
|
|
653
954
|
|
|
654
955
|
// src/commands/deactivate.ts
|
|
655
|
-
async function deactivateCommand() {
|
|
656
|
-
const
|
|
657
|
-
if (
|
|
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
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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 "":
|