@keepgoingdev/cli 0.2.1 → 0.3.1
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/README.md +5 -3
- package/dist/index.js +272 -68
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -50,17 +50,19 @@ Git branch and touched files are auto-detected from the workspace.
|
|
|
50
50
|
|
|
51
51
|
Install a shell hook that runs `keepgoing status --quiet` automatically whenever you `cd` into a directory that contains `.keepgoing/`.
|
|
52
52
|
|
|
53
|
-
Supports **zsh** and **
|
|
53
|
+
Supports **zsh**, **bash**, and **fish**. Detected from `$SHELL`.
|
|
54
54
|
|
|
55
55
|
```bash
|
|
56
56
|
keepgoing hook install
|
|
57
57
|
# → Reload your shell:
|
|
58
|
-
source ~/.zshrc
|
|
58
|
+
source ~/.zshrc # zsh
|
|
59
|
+
source ~/.bashrc # bash
|
|
60
|
+
source ~/.config/fish/config.fish # fish
|
|
59
61
|
```
|
|
60
62
|
|
|
61
63
|
### `keepgoing hook uninstall`
|
|
62
64
|
|
|
63
|
-
Remove the shell hook from your
|
|
65
|
+
Remove the shell hook from your shell config file.
|
|
64
66
|
|
|
65
67
|
```bash
|
|
66
68
|
keepgoing hook uninstall
|
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,33 @@ 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
|
+
const updatedAt = new Date(t.updatedAt).getTime();
|
|
144
|
+
return !isNaN(updatedAt) && now - updatedAt < STALE_SESSION_MS;
|
|
145
|
+
});
|
|
146
|
+
}
|
|
140
147
|
var KeepGoingWriter = class {
|
|
141
148
|
storagePath;
|
|
142
149
|
sessionsFilePath;
|
|
143
150
|
stateFilePath;
|
|
144
151
|
metaFilePath;
|
|
152
|
+
currentTasksFilePath;
|
|
145
153
|
constructor(workspacePath) {
|
|
146
154
|
const mainRoot = resolveStorageRoot(workspacePath);
|
|
147
155
|
this.storagePath = path2.join(mainRoot, STORAGE_DIR);
|
|
148
156
|
this.sessionsFilePath = path2.join(this.storagePath, SESSIONS_FILE);
|
|
149
157
|
this.stateFilePath = path2.join(this.storagePath, STATE_FILE);
|
|
150
158
|
this.metaFilePath = path2.join(this.storagePath, META_FILE);
|
|
159
|
+
this.currentTasksFilePath = path2.join(this.storagePath, CURRENT_TASKS_FILE);
|
|
151
160
|
}
|
|
152
161
|
ensureDir() {
|
|
153
162
|
if (!fs.existsSync(this.storagePath)) {
|
|
@@ -208,82 +217,214 @@ var KeepGoingWriter = class {
|
|
|
208
217
|
}
|
|
209
218
|
fs.writeFileSync(this.metaFilePath, JSON.stringify(meta, null, 2), "utf-8");
|
|
210
219
|
}
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
// Multi-session API
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
/** Read all current tasks from current-tasks.json. Auto-prunes stale sessions. */
|
|
224
|
+
readCurrentTasks() {
|
|
225
|
+
try {
|
|
226
|
+
if (fs.existsSync(this.currentTasksFilePath)) {
|
|
227
|
+
const raw = JSON.parse(fs.readFileSync(this.currentTasksFilePath, "utf-8"));
|
|
228
|
+
const tasks = Array.isArray(raw) ? raw : raw.tasks ?? [];
|
|
229
|
+
return this.pruneStale(tasks);
|
|
230
|
+
}
|
|
231
|
+
} catch {
|
|
232
|
+
}
|
|
233
|
+
return [];
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Upsert a session task by sessionId into current-tasks.json.
|
|
237
|
+
* If no sessionId is present on the task, generates one.
|
|
238
|
+
*/
|
|
239
|
+
upsertSession(update) {
|
|
240
|
+
this.ensureDir();
|
|
241
|
+
this.upsertSessionCore(update);
|
|
242
|
+
}
|
|
243
|
+
/** Core upsert logic: merges the update into current-tasks.json and returns the pruned task list. */
|
|
244
|
+
upsertSessionCore(update) {
|
|
245
|
+
this.ensureDir();
|
|
246
|
+
const sessionId = update.sessionId || generateSessionId(update);
|
|
247
|
+
const tasks = this.readAllTasksRaw();
|
|
248
|
+
const existingIdx = tasks.findIndex((t) => t.sessionId === sessionId);
|
|
249
|
+
let merged;
|
|
250
|
+
if (existingIdx >= 0) {
|
|
251
|
+
const existing = tasks[existingIdx];
|
|
252
|
+
merged = { ...existing, ...update, sessionId };
|
|
253
|
+
tasks[existingIdx] = merged;
|
|
254
|
+
} else {
|
|
255
|
+
merged = { ...update, sessionId };
|
|
256
|
+
tasks.push(merged);
|
|
257
|
+
}
|
|
258
|
+
const pruned = this.pruneStale(tasks);
|
|
259
|
+
this.writeTasksFile(pruned);
|
|
260
|
+
return pruned;
|
|
261
|
+
}
|
|
262
|
+
/** Remove a specific session by ID. */
|
|
263
|
+
removeSession(sessionId) {
|
|
264
|
+
const tasks = this.readAllTasksRaw().filter((t) => t.sessionId !== sessionId);
|
|
265
|
+
this.writeTasksFile(tasks);
|
|
266
|
+
}
|
|
267
|
+
/** Get all active sessions (sessionActive=true and within stale threshold). */
|
|
268
|
+
getActiveSessions() {
|
|
269
|
+
return this.readCurrentTasks().filter((t) => t.sessionActive);
|
|
270
|
+
}
|
|
271
|
+
/** Get a specific session by ID. */
|
|
272
|
+
getSession(sessionId) {
|
|
273
|
+
return this.readCurrentTasks().find((t) => t.sessionId === sessionId);
|
|
274
|
+
}
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
// Private helpers
|
|
277
|
+
// ---------------------------------------------------------------------------
|
|
278
|
+
readAllTasksRaw() {
|
|
279
|
+
try {
|
|
280
|
+
if (fs.existsSync(this.currentTasksFilePath)) {
|
|
281
|
+
const raw = JSON.parse(fs.readFileSync(this.currentTasksFilePath, "utf-8"));
|
|
282
|
+
return Array.isArray(raw) ? [...raw] : [...raw.tasks ?? []];
|
|
283
|
+
}
|
|
284
|
+
} catch {
|
|
285
|
+
}
|
|
286
|
+
return [];
|
|
287
|
+
}
|
|
288
|
+
pruneStale(tasks) {
|
|
289
|
+
return pruneStaleTasks(tasks);
|
|
290
|
+
}
|
|
291
|
+
writeTasksFile(tasks) {
|
|
292
|
+
const data = { version: 1, tasks };
|
|
293
|
+
fs.writeFileSync(this.currentTasksFilePath, JSON.stringify(data, null, 2), "utf-8");
|
|
294
|
+
}
|
|
211
295
|
};
|
|
296
|
+
function generateSessionId(context) {
|
|
297
|
+
const parts = [
|
|
298
|
+
context.worktreePath || context.workspaceRoot || "",
|
|
299
|
+
context.agentLabel || "",
|
|
300
|
+
context.branch || ""
|
|
301
|
+
].filter(Boolean);
|
|
302
|
+
if (parts.length === 0) {
|
|
303
|
+
return randomUUID2();
|
|
304
|
+
}
|
|
305
|
+
const hash = createHash("sha256").update(parts.join("|")).digest("hex").slice(0, 12);
|
|
306
|
+
return `ses_${hash}`;
|
|
307
|
+
}
|
|
212
308
|
|
|
213
309
|
// ../../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();
|
|
310
|
+
import fs3 from "fs";
|
|
311
|
+
import path4 from "path";
|
|
224
312
|
|
|
225
313
|
// ../../packages/shared/src/license.ts
|
|
226
314
|
import crypto from "crypto";
|
|
227
|
-
import
|
|
315
|
+
import fs2 from "fs";
|
|
228
316
|
import os from "os";
|
|
229
|
-
import
|
|
317
|
+
import path3 from "path";
|
|
230
318
|
var LICENSE_FILE = "license.json";
|
|
231
319
|
var DEVICE_ID_FILE = "device-id";
|
|
232
320
|
function getGlobalLicenseDir() {
|
|
233
|
-
return
|
|
321
|
+
return path3.join(os.homedir(), ".keepgoing");
|
|
234
322
|
}
|
|
235
323
|
function getGlobalLicensePath() {
|
|
236
|
-
return
|
|
324
|
+
return path3.join(getGlobalLicenseDir(), LICENSE_FILE);
|
|
237
325
|
}
|
|
238
326
|
function getDeviceId() {
|
|
239
327
|
const dir = getGlobalLicenseDir();
|
|
240
|
-
const filePath =
|
|
328
|
+
const filePath = path3.join(dir, DEVICE_ID_FILE);
|
|
241
329
|
try {
|
|
242
|
-
const existing =
|
|
330
|
+
const existing = fs2.readFileSync(filePath, "utf-8").trim();
|
|
243
331
|
if (existing) return existing;
|
|
244
332
|
} catch {
|
|
245
333
|
}
|
|
246
334
|
const id = crypto.randomUUID();
|
|
247
|
-
if (!
|
|
248
|
-
|
|
335
|
+
if (!fs2.existsSync(dir)) {
|
|
336
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
249
337
|
}
|
|
250
|
-
|
|
338
|
+
fs2.writeFileSync(filePath, id, "utf-8");
|
|
251
339
|
return id;
|
|
252
340
|
}
|
|
253
|
-
|
|
341
|
+
var DECISION_DETECTION_VARIANT_ID = 1361527;
|
|
342
|
+
var SESSION_AWARENESS_VARIANT_ID = 1366510;
|
|
343
|
+
var TEST_DECISION_DETECTION_VARIANT_ID = 1345647;
|
|
344
|
+
var TEST_SESSION_AWARENESS_VARIANT_ID = 1365992;
|
|
345
|
+
var VARIANT_FEATURE_MAP = {
|
|
346
|
+
[DECISION_DETECTION_VARIANT_ID]: ["decisions"],
|
|
347
|
+
[SESSION_AWARENESS_VARIANT_ID]: ["session-awareness"],
|
|
348
|
+
[TEST_DECISION_DETECTION_VARIANT_ID]: ["decisions"],
|
|
349
|
+
[TEST_SESSION_AWARENESS_VARIANT_ID]: ["session-awareness"]
|
|
350
|
+
// Future bundle: [BUNDLE_VARIANT_ID]: ['decisions', 'session-awareness'],
|
|
351
|
+
};
|
|
352
|
+
var KNOWN_VARIANT_IDS = new Set(Object.keys(VARIANT_FEATURE_MAP).map(Number));
|
|
353
|
+
function getVariantLabel(variantId) {
|
|
354
|
+
const features = VARIANT_FEATURE_MAP[variantId];
|
|
355
|
+
if (!features) return "Unknown Add-on";
|
|
356
|
+
if (features.includes("decisions") && features.includes("session-awareness")) return "Pro Bundle";
|
|
357
|
+
if (features.includes("decisions")) return "Decision Detection";
|
|
358
|
+
if (features.includes("session-awareness")) return "Session Awareness";
|
|
359
|
+
return "Pro Add-on";
|
|
360
|
+
}
|
|
361
|
+
var _cachedStore;
|
|
362
|
+
var _cacheTimestamp = 0;
|
|
363
|
+
var LICENSE_CACHE_TTL_MS = 2e3;
|
|
364
|
+
function readLicenseStore() {
|
|
365
|
+
const now = Date.now();
|
|
366
|
+
if (_cachedStore && now - _cacheTimestamp < LICENSE_CACHE_TTL_MS) {
|
|
367
|
+
return _cachedStore;
|
|
368
|
+
}
|
|
254
369
|
const licensePath = getGlobalLicensePath();
|
|
370
|
+
let store;
|
|
255
371
|
try {
|
|
256
|
-
if (!
|
|
257
|
-
|
|
372
|
+
if (!fs2.existsSync(licensePath)) {
|
|
373
|
+
store = { version: 2, licenses: [] };
|
|
374
|
+
} else {
|
|
375
|
+
const raw = fs2.readFileSync(licensePath, "utf-8");
|
|
376
|
+
const data = JSON.parse(raw);
|
|
377
|
+
if (data?.version === 2 && Array.isArray(data.licenses)) {
|
|
378
|
+
store = data;
|
|
379
|
+
} else {
|
|
380
|
+
store = { version: 2, licenses: [] };
|
|
381
|
+
}
|
|
258
382
|
}
|
|
259
|
-
const raw = fs3.readFileSync(licensePath, "utf-8");
|
|
260
|
-
return JSON.parse(raw);
|
|
261
383
|
} catch {
|
|
262
|
-
|
|
384
|
+
store = { version: 2, licenses: [] };
|
|
263
385
|
}
|
|
386
|
+
_cachedStore = store;
|
|
387
|
+
_cacheTimestamp = now;
|
|
388
|
+
return store;
|
|
264
389
|
}
|
|
265
|
-
function
|
|
390
|
+
function writeLicenseStore(store) {
|
|
266
391
|
const dirPath = getGlobalLicenseDir();
|
|
267
|
-
if (!
|
|
268
|
-
|
|
392
|
+
if (!fs2.existsSync(dirPath)) {
|
|
393
|
+
fs2.mkdirSync(dirPath, { recursive: true });
|
|
269
394
|
}
|
|
270
|
-
const licensePath =
|
|
271
|
-
|
|
395
|
+
const licensePath = path3.join(dirPath, LICENSE_FILE);
|
|
396
|
+
fs2.writeFileSync(licensePath, JSON.stringify(store, null, 2), "utf-8");
|
|
397
|
+
_cachedStore = store;
|
|
398
|
+
_cacheTimestamp = Date.now();
|
|
272
399
|
}
|
|
273
|
-
function
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
400
|
+
function addLicenseEntry(entry) {
|
|
401
|
+
const store = readLicenseStore();
|
|
402
|
+
const idx = store.licenses.findIndex((l) => l.licenseKey === entry.licenseKey);
|
|
403
|
+
if (idx >= 0) {
|
|
404
|
+
store.licenses[idx] = entry;
|
|
405
|
+
} else {
|
|
406
|
+
store.licenses.push(entry);
|
|
280
407
|
}
|
|
408
|
+
writeLicenseStore(store);
|
|
281
409
|
}
|
|
282
|
-
function
|
|
283
|
-
|
|
410
|
+
function removeLicenseEntry(licenseKey) {
|
|
411
|
+
const store = readLicenseStore();
|
|
412
|
+
store.licenses = store.licenses.filter((l) => l.licenseKey !== licenseKey);
|
|
413
|
+
writeLicenseStore(store);
|
|
414
|
+
}
|
|
415
|
+
function getActiveLicenses() {
|
|
416
|
+
return readLicenseStore().licenses.filter((l) => l.status === "active");
|
|
284
417
|
}
|
|
285
418
|
var REVALIDATION_THRESHOLD_MS = 24 * 60 * 60 * 1e3;
|
|
286
419
|
|
|
420
|
+
// ../../packages/shared/src/featureGate.ts
|
|
421
|
+
var DefaultFeatureGate = class {
|
|
422
|
+
isEnabled(_feature) {
|
|
423
|
+
return true;
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
var currentGate = new DefaultFeatureGate();
|
|
427
|
+
|
|
287
428
|
// ../../packages/shared/src/licenseClient.ts
|
|
288
429
|
var BASE_URL = "https://api.lemonsqueezy.com/v1/licenses";
|
|
289
430
|
var REQUEST_TIMEOUT_MS = 15e3;
|
|
@@ -334,13 +475,21 @@ async function activateLicense(licenseKey, instanceName, options) {
|
|
|
334
475
|
}
|
|
335
476
|
return { valid: false, error: productError };
|
|
336
477
|
}
|
|
478
|
+
if (data.meta?.variant_id && !KNOWN_VARIANT_IDS.has(data.meta.variant_id)) {
|
|
479
|
+
if (data.license_key?.key && data.instance?.id) {
|
|
480
|
+
await deactivateLicense(data.license_key.key, data.instance.id);
|
|
481
|
+
}
|
|
482
|
+
return { valid: false, error: "This license key is for an unrecognized add-on variant. Please update KeepGoing or contact support." };
|
|
483
|
+
}
|
|
337
484
|
}
|
|
338
485
|
return {
|
|
339
486
|
valid: true,
|
|
340
487
|
licenseKey: data.license_key?.key,
|
|
341
488
|
instanceId: data.instance?.id,
|
|
342
489
|
customerName: data.meta?.customer_name,
|
|
343
|
-
productName: data.meta?.product_name
|
|
490
|
+
productName: data.meta?.product_name,
|
|
491
|
+
variantId: data.meta?.variant_id,
|
|
492
|
+
variantName: data.meta?.variant_name
|
|
344
493
|
};
|
|
345
494
|
} catch (err) {
|
|
346
495
|
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 +632,7 @@ import { spawn } from "child_process";
|
|
|
483
632
|
import { readFileSync, existsSync } from "fs";
|
|
484
633
|
import path6 from "path";
|
|
485
634
|
import os2 from "os";
|
|
486
|
-
var CLI_VERSION = "0.
|
|
635
|
+
var CLI_VERSION = "0.3.1";
|
|
487
636
|
var NPM_REGISTRY_URL = "https://registry.npmjs.org/@keepgoingdev/cli/latest";
|
|
488
637
|
var FETCH_TIMEOUT_MS = 5e3;
|
|
489
638
|
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -681,6 +830,16 @@ if command -v keepgoing >/dev/null 2>&1; then
|
|
|
681
830
|
}
|
|
682
831
|
fi
|
|
683
832
|
${HOOK_MARKER_END}`;
|
|
833
|
+
var FISH_HOOK = `${HOOK_MARKER_START}
|
|
834
|
+
# KeepGoing shell hook, auto-injected by 'keepgoing hook install'
|
|
835
|
+
if command -v keepgoing >/dev/null 2>&1
|
|
836
|
+
function __keepgoing_on_pwd_change --on-variable PWD
|
|
837
|
+
if test -d .keepgoing
|
|
838
|
+
keepgoing status --quiet
|
|
839
|
+
end
|
|
840
|
+
end
|
|
841
|
+
end
|
|
842
|
+
${HOOK_MARKER_END}`;
|
|
684
843
|
function detectShellRcFile() {
|
|
685
844
|
const shellEnv = process.env["SHELL"] ?? "";
|
|
686
845
|
const home = os3.homedir();
|
|
@@ -690,18 +849,22 @@ function detectShellRcFile() {
|
|
|
690
849
|
if (shellEnv.endsWith("bash")) {
|
|
691
850
|
return { shell: "bash", rcFile: path8.join(home, ".bashrc") };
|
|
692
851
|
}
|
|
852
|
+
if (shellEnv.endsWith("fish")) {
|
|
853
|
+
const xdgConfig = process.env["XDG_CONFIG_HOME"] || path8.join(home, ".config");
|
|
854
|
+
return { shell: "fish", rcFile: path8.join(xdgConfig, "fish", "config.fish") };
|
|
855
|
+
}
|
|
693
856
|
return void 0;
|
|
694
857
|
}
|
|
695
858
|
function hookInstallCommand() {
|
|
696
859
|
const detected = detectShellRcFile();
|
|
697
860
|
if (!detected) {
|
|
698
861
|
console.error(
|
|
699
|
-
'Could not detect your shell. Set $SHELL to "zsh" or "
|
|
862
|
+
'Could not detect your shell. Set $SHELL to "zsh", "bash", or "fish" and try again.'
|
|
700
863
|
);
|
|
701
864
|
process.exit(1);
|
|
702
865
|
}
|
|
703
866
|
const { shell, rcFile } = detected;
|
|
704
|
-
const hookBlock = shell === "zsh" ? ZSH_HOOK : BASH_HOOK;
|
|
867
|
+
const hookBlock = shell === "zsh" ? ZSH_HOOK : shell === "fish" ? FISH_HOOK : BASH_HOOK;
|
|
705
868
|
let existing = "";
|
|
706
869
|
try {
|
|
707
870
|
existing = fs5.readFileSync(rcFile, "utf-8");
|
|
@@ -723,7 +886,7 @@ function hookUninstallCommand() {
|
|
|
723
886
|
const detected = detectShellRcFile();
|
|
724
887
|
if (!detected) {
|
|
725
888
|
console.error(
|
|
726
|
-
'Could not detect your shell. Set $SHELL to "zsh" or "
|
|
889
|
+
'Could not detect your shell. Set $SHELL to "zsh", "bash", or "fish" and try again.'
|
|
727
890
|
);
|
|
728
891
|
process.exit(1);
|
|
729
892
|
}
|
|
@@ -759,10 +922,14 @@ async function activateCommand({ licenseKey }) {
|
|
|
759
922
|
console.error("Usage: keepgoing activate <license-key>");
|
|
760
923
|
process.exit(1);
|
|
761
924
|
}
|
|
762
|
-
const
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
925
|
+
const store = readLicenseStore();
|
|
926
|
+
const existingForKey = store.licenses.find(
|
|
927
|
+
(l) => l.status === "active" && l.licenseKey === licenseKey
|
|
928
|
+
);
|
|
929
|
+
if (existingForKey) {
|
|
930
|
+
const label2 = getVariantLabel(existingForKey.variantId);
|
|
931
|
+
const who2 = existingForKey.customerName ? ` (${existingForKey.customerName})` : "";
|
|
932
|
+
console.log(`${label2} is already active${who2}.`);
|
|
766
933
|
return;
|
|
767
934
|
}
|
|
768
935
|
console.log("Activating license...");
|
|
@@ -771,35 +938,72 @@ async function activateCommand({ licenseKey }) {
|
|
|
771
938
|
console.error(`Activation failed: ${result.error ?? "unknown error"}`);
|
|
772
939
|
process.exit(1);
|
|
773
940
|
}
|
|
941
|
+
const variantId = result.variantId;
|
|
942
|
+
const existingForVariant = store.licenses.find(
|
|
943
|
+
(l) => l.status === "active" && l.variantId === variantId
|
|
944
|
+
);
|
|
945
|
+
if (existingForVariant) {
|
|
946
|
+
const label2 = getVariantLabel(variantId);
|
|
947
|
+
const who2 = existingForVariant.customerName ? ` (${existingForVariant.customerName})` : "";
|
|
948
|
+
console.log(`${label2} is already active${who2}.`);
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
774
951
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
775
|
-
|
|
952
|
+
addLicenseEntry({
|
|
776
953
|
licenseKey: result.licenseKey || licenseKey,
|
|
777
954
|
instanceId: result.instanceId || getDeviceId(),
|
|
778
955
|
status: "active",
|
|
779
956
|
lastValidatedAt: now,
|
|
780
957
|
activatedAt: now,
|
|
958
|
+
variantId,
|
|
781
959
|
customerName: result.customerName,
|
|
782
|
-
productName: result.productName
|
|
960
|
+
productName: result.productName,
|
|
961
|
+
variantName: result.variantName
|
|
783
962
|
});
|
|
963
|
+
const label = getVariantLabel(variantId);
|
|
784
964
|
const who = result.customerName ? ` Welcome, ${result.customerName}!` : "";
|
|
785
|
-
console.log(
|
|
965
|
+
console.log(`${label} activated successfully.${who}`);
|
|
786
966
|
}
|
|
787
967
|
|
|
788
968
|
// src/commands/deactivate.ts
|
|
789
|
-
async function deactivateCommand() {
|
|
790
|
-
const
|
|
791
|
-
if (
|
|
969
|
+
async function deactivateCommand(opts) {
|
|
970
|
+
const active = getActiveLicenses();
|
|
971
|
+
if (active.length === 0) {
|
|
792
972
|
console.log("No active license found on this device.");
|
|
793
973
|
return;
|
|
794
974
|
}
|
|
975
|
+
let targets;
|
|
976
|
+
if (opts?.licenseKey) {
|
|
977
|
+
const match = active.find((l) => l.licenseKey === opts.licenseKey);
|
|
978
|
+
if (!match) {
|
|
979
|
+
console.error(`No active license found with key "${opts.licenseKey}".`);
|
|
980
|
+
process.exit(1);
|
|
981
|
+
}
|
|
982
|
+
targets = [match];
|
|
983
|
+
} else if (active.length === 1) {
|
|
984
|
+
targets = active;
|
|
985
|
+
} else {
|
|
986
|
+
console.log("Multiple active licenses found. Specify which to deactivate:");
|
|
987
|
+
console.log(" keepgoing deactivate <license-key>");
|
|
988
|
+
console.log("");
|
|
989
|
+
for (const l of active) {
|
|
990
|
+
const label = getVariantLabel(l.variantId);
|
|
991
|
+
const who = l.customerName ? ` (${l.customerName})` : "";
|
|
992
|
+
console.log(` ${label}${who}: ${l.licenseKey}`);
|
|
993
|
+
}
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
795
996
|
console.log("Deactivating license...");
|
|
796
|
-
const
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
997
|
+
for (const entry of targets) {
|
|
998
|
+
const result = await deactivateLicense(entry.licenseKey, entry.instanceId);
|
|
999
|
+
removeLicenseEntry(entry.licenseKey);
|
|
1000
|
+
const label = getVariantLabel(entry.variantId);
|
|
1001
|
+
if (!result.deactivated) {
|
|
1002
|
+
console.error(`${label} license cleared locally, but remote deactivation failed: ${result.error ?? "unknown error"}`);
|
|
1003
|
+
} else {
|
|
1004
|
+
console.log(`${label} license deactivated successfully. The activation slot has been freed.`);
|
|
1005
|
+
}
|
|
801
1006
|
}
|
|
802
|
-
console.log("Pro license deactivated successfully. The activation slot has been freed.");
|
|
803
1007
|
}
|
|
804
1008
|
|
|
805
1009
|
// src/index.ts
|
|
@@ -821,7 +1025,7 @@ Options:
|
|
|
821
1025
|
-h, --help Show this help text
|
|
822
1026
|
|
|
823
1027
|
Hook subcommands:
|
|
824
|
-
keepgoing hook install Install the shell hook
|
|
1028
|
+
keepgoing hook install Install the shell hook (zsh, bash, fish)
|
|
825
1029
|
keepgoing hook uninstall Remove the shell hook
|
|
826
1030
|
`;
|
|
827
1031
|
function parseArgs(argv) {
|
|
@@ -874,13 +1078,13 @@ async function main() {
|
|
|
874
1078
|
}
|
|
875
1079
|
break;
|
|
876
1080
|
case "version":
|
|
877
|
-
console.log(`keepgoing v${"0.
|
|
1081
|
+
console.log(`keepgoing v${"0.3.1"}`);
|
|
878
1082
|
break;
|
|
879
1083
|
case "activate":
|
|
880
1084
|
await activateCommand({ licenseKey: subcommand });
|
|
881
1085
|
break;
|
|
882
1086
|
case "deactivate":
|
|
883
|
-
await deactivateCommand();
|
|
1087
|
+
await deactivateCommand({ licenseKey: subcommand || void 0 });
|
|
884
1088
|
break;
|
|
885
1089
|
case "help":
|
|
886
1090
|
case "":
|