@keepgoingdev/cli 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +316 -88
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// src/storage.ts
|
|
4
|
-
import fs from "fs";
|
|
5
|
-
import path from "path";
|
|
6
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
7
|
-
|
|
8
3
|
// ../../packages/shared/src/session.ts
|
|
9
4
|
import { randomUUID } from "crypto";
|
|
10
5
|
function generateCheckpointId() {
|
|
@@ -57,8 +52,49 @@ function formatRelativeTime(timestamp) {
|
|
|
57
52
|
|
|
58
53
|
// ../../packages/shared/src/gitUtils.ts
|
|
59
54
|
import { execFileSync, execFile } from "child_process";
|
|
55
|
+
import path from "path";
|
|
60
56
|
import { promisify } from "util";
|
|
61
57
|
var execFileAsync = promisify(execFile);
|
|
58
|
+
function findGitRoot(startPath) {
|
|
59
|
+
try {
|
|
60
|
+
const gitCommonDir = execFileSync("git", ["rev-parse", "--git-common-dir"], {
|
|
61
|
+
cwd: startPath,
|
|
62
|
+
encoding: "utf-8",
|
|
63
|
+
timeout: 5e3
|
|
64
|
+
}).trim();
|
|
65
|
+
if (!gitCommonDir) return startPath;
|
|
66
|
+
const absoluteGitDir = path.isAbsolute(gitCommonDir) ? gitCommonDir : path.resolve(startPath, gitCommonDir);
|
|
67
|
+
return path.dirname(absoluteGitDir);
|
|
68
|
+
} catch {
|
|
69
|
+
return startPath;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
var storageRootCache = /* @__PURE__ */ new Map();
|
|
73
|
+
function resolveStorageRoot(startPath) {
|
|
74
|
+
const cached = storageRootCache.get(startPath);
|
|
75
|
+
if (cached !== void 0) {
|
|
76
|
+
return cached;
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const toplevel = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
80
|
+
cwd: startPath,
|
|
81
|
+
encoding: "utf-8",
|
|
82
|
+
timeout: 5e3
|
|
83
|
+
}).trim();
|
|
84
|
+
const commonDir = execFileSync("git", ["rev-parse", "--git-common-dir"], {
|
|
85
|
+
cwd: startPath,
|
|
86
|
+
encoding: "utf-8",
|
|
87
|
+
timeout: 5e3
|
|
88
|
+
}).trim();
|
|
89
|
+
const absoluteCommonDir = path.resolve(toplevel, commonDir);
|
|
90
|
+
const mainRoot = path.dirname(absoluteCommonDir);
|
|
91
|
+
storageRootCache.set(startPath, mainRoot);
|
|
92
|
+
return mainRoot;
|
|
93
|
+
} catch {
|
|
94
|
+
storageRootCache.set(startPath, startPath);
|
|
95
|
+
return startPath;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
62
98
|
function getCurrentBranch(workspacePath) {
|
|
63
99
|
try {
|
|
64
100
|
const result = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
@@ -93,79 +129,25 @@ function getRecentSessions(allSessions, count = RECENT_SESSION_COUNT) {
|
|
|
93
129
|
return allSessions.slice(-count).reverse();
|
|
94
130
|
}
|
|
95
131
|
|
|
96
|
-
// src/storage.ts
|
|
132
|
+
// ../../packages/shared/src/storage.ts
|
|
133
|
+
import fs from "fs";
|
|
134
|
+
import path2 from "path";
|
|
135
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
97
136
|
var STORAGE_DIR = ".keepgoing";
|
|
98
137
|
var META_FILE = "meta.json";
|
|
99
138
|
var SESSIONS_FILE = "sessions.json";
|
|
100
139
|
var STATE_FILE = "state.json";
|
|
101
|
-
var KeepGoingReader = class {
|
|
102
|
-
storagePath;
|
|
103
|
-
metaFilePath;
|
|
104
|
-
sessionsFilePath;
|
|
105
|
-
stateFilePath;
|
|
106
|
-
constructor(workspacePath) {
|
|
107
|
-
this.storagePath = path.join(workspacePath, STORAGE_DIR);
|
|
108
|
-
this.metaFilePath = path.join(this.storagePath, META_FILE);
|
|
109
|
-
this.sessionsFilePath = path.join(this.storagePath, SESSIONS_FILE);
|
|
110
|
-
this.stateFilePath = path.join(this.storagePath, STATE_FILE);
|
|
111
|
-
}
|
|
112
|
-
exists() {
|
|
113
|
-
return fs.existsSync(this.storagePath);
|
|
114
|
-
}
|
|
115
|
-
getState() {
|
|
116
|
-
return this.readJsonFile(this.stateFilePath);
|
|
117
|
-
}
|
|
118
|
-
getMeta() {
|
|
119
|
-
return this.readJsonFile(this.metaFilePath);
|
|
120
|
-
}
|
|
121
|
-
getSessions() {
|
|
122
|
-
return this.parseSessions().sessions;
|
|
123
|
-
}
|
|
124
|
-
getLastSession() {
|
|
125
|
-
const { sessions, wrapperLastSessionId } = this.parseSessions();
|
|
126
|
-
if (sessions.length === 0) {
|
|
127
|
-
return void 0;
|
|
128
|
-
}
|
|
129
|
-
const state = this.getState();
|
|
130
|
-
if (state?.lastSessionId) {
|
|
131
|
-
const found = sessions.find((s) => s.id === state.lastSessionId);
|
|
132
|
-
if (found) return found;
|
|
133
|
-
}
|
|
134
|
-
if (wrapperLastSessionId) {
|
|
135
|
-
const found = sessions.find((s) => s.id === wrapperLastSessionId);
|
|
136
|
-
if (found) return found;
|
|
137
|
-
}
|
|
138
|
-
return sessions[sessions.length - 1];
|
|
139
|
-
}
|
|
140
|
-
getRecentSessions(count) {
|
|
141
|
-
return getRecentSessions(this.getSessions(), count);
|
|
142
|
-
}
|
|
143
|
-
parseSessions() {
|
|
144
|
-
const raw = this.readJsonFile(this.sessionsFilePath);
|
|
145
|
-
if (!raw) return { sessions: [] };
|
|
146
|
-
if (Array.isArray(raw)) return { sessions: raw };
|
|
147
|
-
return { sessions: raw.sessions ?? [], wrapperLastSessionId: raw.lastSessionId };
|
|
148
|
-
}
|
|
149
|
-
readJsonFile(filePath) {
|
|
150
|
-
try {
|
|
151
|
-
if (!fs.existsSync(filePath)) return void 0;
|
|
152
|
-
const raw = fs.readFileSync(filePath, "utf-8");
|
|
153
|
-
return JSON.parse(raw);
|
|
154
|
-
} catch {
|
|
155
|
-
return void 0;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
};
|
|
159
140
|
var KeepGoingWriter = class {
|
|
160
141
|
storagePath;
|
|
161
142
|
sessionsFilePath;
|
|
162
143
|
stateFilePath;
|
|
163
144
|
metaFilePath;
|
|
164
145
|
constructor(workspacePath) {
|
|
165
|
-
|
|
166
|
-
this.
|
|
167
|
-
this.
|
|
168
|
-
this.
|
|
146
|
+
const mainRoot = resolveStorageRoot(workspacePath);
|
|
147
|
+
this.storagePath = path2.join(mainRoot, STORAGE_DIR);
|
|
148
|
+
this.sessionsFilePath = path2.join(this.storagePath, SESSIONS_FILE);
|
|
149
|
+
this.stateFilePath = path2.join(this.storagePath, STATE_FILE);
|
|
150
|
+
this.metaFilePath = path2.join(this.storagePath, META_FILE);
|
|
169
151
|
}
|
|
170
152
|
ensureDir() {
|
|
171
153
|
if (!fs.existsSync(this.storagePath)) {
|
|
@@ -191,6 +173,10 @@ var KeepGoingWriter = class {
|
|
|
191
173
|
}
|
|
192
174
|
sessionsData.sessions.push(checkpoint);
|
|
193
175
|
sessionsData.lastSessionId = checkpoint.id;
|
|
176
|
+
const MAX_SESSIONS = 200;
|
|
177
|
+
if (sessionsData.sessions.length > MAX_SESSIONS) {
|
|
178
|
+
sessionsData.sessions = sessionsData.sessions.slice(-MAX_SESSIONS);
|
|
179
|
+
}
|
|
194
180
|
fs.writeFileSync(this.sessionsFilePath, JSON.stringify(sessionsData, null, 2), "utf-8");
|
|
195
181
|
const state = {
|
|
196
182
|
lastSessionId: checkpoint.id,
|
|
@@ -198,29 +184,213 @@ var KeepGoingWriter = class {
|
|
|
198
184
|
lastActivityAt: checkpoint.timestamp
|
|
199
185
|
};
|
|
200
186
|
fs.writeFileSync(this.stateFilePath, JSON.stringify(state, null, 2), "utf-8");
|
|
187
|
+
this.updateMeta(checkpoint.timestamp);
|
|
188
|
+
}
|
|
189
|
+
updateMeta(timestamp) {
|
|
201
190
|
let meta;
|
|
202
191
|
try {
|
|
203
192
|
if (fs.existsSync(this.metaFilePath)) {
|
|
204
193
|
meta = JSON.parse(fs.readFileSync(this.metaFilePath, "utf-8"));
|
|
205
|
-
meta.lastUpdated =
|
|
194
|
+
meta.lastUpdated = timestamp;
|
|
206
195
|
} else {
|
|
207
196
|
meta = {
|
|
208
197
|
projectId: randomUUID2(),
|
|
209
|
-
createdAt:
|
|
210
|
-
lastUpdated:
|
|
198
|
+
createdAt: timestamp,
|
|
199
|
+
lastUpdated: timestamp
|
|
211
200
|
};
|
|
212
201
|
}
|
|
213
202
|
} catch {
|
|
214
203
|
meta = {
|
|
215
204
|
projectId: randomUUID2(),
|
|
216
|
-
createdAt:
|
|
217
|
-
lastUpdated:
|
|
205
|
+
createdAt: timestamp,
|
|
206
|
+
lastUpdated: timestamp
|
|
218
207
|
};
|
|
219
208
|
}
|
|
220
209
|
fs.writeFileSync(this.metaFilePath, JSON.stringify(meta, null, 2), "utf-8");
|
|
221
210
|
}
|
|
222
211
|
};
|
|
223
212
|
|
|
213
|
+
// ../../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();
|
|
224
|
+
|
|
225
|
+
// ../../packages/shared/src/license.ts
|
|
226
|
+
import crypto from "crypto";
|
|
227
|
+
import fs3 from "fs";
|
|
228
|
+
import os from "os";
|
|
229
|
+
import path4 from "path";
|
|
230
|
+
var LICENSE_FILE = "license.json";
|
|
231
|
+
var DEVICE_ID_FILE = "device-id";
|
|
232
|
+
function getGlobalLicenseDir() {
|
|
233
|
+
return path4.join(os.homedir(), ".keepgoing");
|
|
234
|
+
}
|
|
235
|
+
function getGlobalLicensePath() {
|
|
236
|
+
return path4.join(getGlobalLicenseDir(), LICENSE_FILE);
|
|
237
|
+
}
|
|
238
|
+
function getDeviceId() {
|
|
239
|
+
const dir = getGlobalLicenseDir();
|
|
240
|
+
const filePath = path4.join(dir, DEVICE_ID_FILE);
|
|
241
|
+
try {
|
|
242
|
+
const existing = fs3.readFileSync(filePath, "utf-8").trim();
|
|
243
|
+
if (existing) return existing;
|
|
244
|
+
} catch {
|
|
245
|
+
}
|
|
246
|
+
const id = crypto.randomUUID();
|
|
247
|
+
if (!fs3.existsSync(dir)) {
|
|
248
|
+
fs3.mkdirSync(dir, { recursive: true });
|
|
249
|
+
}
|
|
250
|
+
fs3.writeFileSync(filePath, id, "utf-8");
|
|
251
|
+
return id;
|
|
252
|
+
}
|
|
253
|
+
function readLicenseCache() {
|
|
254
|
+
const licensePath = getGlobalLicensePath();
|
|
255
|
+
try {
|
|
256
|
+
if (!fs3.existsSync(licensePath)) {
|
|
257
|
+
return void 0;
|
|
258
|
+
}
|
|
259
|
+
const raw = fs3.readFileSync(licensePath, "utf-8");
|
|
260
|
+
return JSON.parse(raw);
|
|
261
|
+
} catch {
|
|
262
|
+
return void 0;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function writeLicenseCache(cache) {
|
|
266
|
+
const dirPath = getGlobalLicenseDir();
|
|
267
|
+
if (!fs3.existsSync(dirPath)) {
|
|
268
|
+
fs3.mkdirSync(dirPath, { recursive: true });
|
|
269
|
+
}
|
|
270
|
+
const licensePath = path4.join(dirPath, LICENSE_FILE);
|
|
271
|
+
fs3.writeFileSync(licensePath, JSON.stringify(cache, null, 2), "utf-8");
|
|
272
|
+
}
|
|
273
|
+
function deleteLicenseCache() {
|
|
274
|
+
const licensePath = getGlobalLicensePath();
|
|
275
|
+
try {
|
|
276
|
+
if (fs3.existsSync(licensePath)) {
|
|
277
|
+
fs3.unlinkSync(licensePath);
|
|
278
|
+
}
|
|
279
|
+
} catch {
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
function isCachedLicenseValid(cache) {
|
|
283
|
+
return cache?.status === "active";
|
|
284
|
+
}
|
|
285
|
+
var REVALIDATION_THRESHOLD_MS = 24 * 60 * 60 * 1e3;
|
|
286
|
+
|
|
287
|
+
// ../../packages/shared/src/licenseClient.ts
|
|
288
|
+
var BASE_URL = "https://api.lemonsqueezy.com/v1/licenses";
|
|
289
|
+
async function activateLicense(licenseKey, instanceName) {
|
|
290
|
+
try {
|
|
291
|
+
const res = await fetch(`${BASE_URL}/activate`, {
|
|
292
|
+
method: "POST",
|
|
293
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
294
|
+
body: new URLSearchParams({ license_key: licenseKey, instance_name: instanceName })
|
|
295
|
+
});
|
|
296
|
+
const data = await res.json();
|
|
297
|
+
if (!res.ok || !data.activated) {
|
|
298
|
+
return { valid: false, error: data.error || `Activation failed (${res.status})` };
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
valid: true,
|
|
302
|
+
licenseKey: data.license_key?.key,
|
|
303
|
+
instanceId: data.instance?.id,
|
|
304
|
+
customerName: data.meta?.customer_name,
|
|
305
|
+
productName: data.meta?.product_name
|
|
306
|
+
};
|
|
307
|
+
} catch (err) {
|
|
308
|
+
return { valid: false, error: err instanceof Error ? err.message : "Network error" };
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
async function deactivateLicense(licenseKey, instanceId) {
|
|
312
|
+
try {
|
|
313
|
+
const res = await fetch(`${BASE_URL}/deactivate`, {
|
|
314
|
+
method: "POST",
|
|
315
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
316
|
+
body: new URLSearchParams({ license_key: licenseKey, instance_id: instanceId })
|
|
317
|
+
});
|
|
318
|
+
const data = await res.json();
|
|
319
|
+
if (!res.ok || !data.deactivated) {
|
|
320
|
+
return { deactivated: false, error: data.error || `Deactivation failed (${res.status})` };
|
|
321
|
+
}
|
|
322
|
+
return { deactivated: true };
|
|
323
|
+
} catch (err) {
|
|
324
|
+
return { deactivated: false, error: err instanceof Error ? err.message : "Network error" };
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// src/storage.ts
|
|
329
|
+
import fs4 from "fs";
|
|
330
|
+
import path5 from "path";
|
|
331
|
+
var STORAGE_DIR2 = ".keepgoing";
|
|
332
|
+
var META_FILE2 = "meta.json";
|
|
333
|
+
var SESSIONS_FILE2 = "sessions.json";
|
|
334
|
+
var STATE_FILE2 = "state.json";
|
|
335
|
+
var KeepGoingReader = class {
|
|
336
|
+
storagePath;
|
|
337
|
+
metaFilePath;
|
|
338
|
+
sessionsFilePath;
|
|
339
|
+
stateFilePath;
|
|
340
|
+
constructor(workspacePath) {
|
|
341
|
+
this.storagePath = path5.join(workspacePath, STORAGE_DIR2);
|
|
342
|
+
this.metaFilePath = path5.join(this.storagePath, META_FILE2);
|
|
343
|
+
this.sessionsFilePath = path5.join(this.storagePath, SESSIONS_FILE2);
|
|
344
|
+
this.stateFilePath = path5.join(this.storagePath, STATE_FILE2);
|
|
345
|
+
}
|
|
346
|
+
exists() {
|
|
347
|
+
return fs4.existsSync(this.storagePath);
|
|
348
|
+
}
|
|
349
|
+
getState() {
|
|
350
|
+
return this.readJsonFile(this.stateFilePath);
|
|
351
|
+
}
|
|
352
|
+
getMeta() {
|
|
353
|
+
return this.readJsonFile(this.metaFilePath);
|
|
354
|
+
}
|
|
355
|
+
getSessions() {
|
|
356
|
+
return this.parseSessions().sessions;
|
|
357
|
+
}
|
|
358
|
+
getLastSession() {
|
|
359
|
+
const { sessions, wrapperLastSessionId } = this.parseSessions();
|
|
360
|
+
if (sessions.length === 0) {
|
|
361
|
+
return void 0;
|
|
362
|
+
}
|
|
363
|
+
const state = this.getState();
|
|
364
|
+
if (state?.lastSessionId) {
|
|
365
|
+
const found = sessions.find((s) => s.id === state.lastSessionId);
|
|
366
|
+
if (found) return found;
|
|
367
|
+
}
|
|
368
|
+
if (wrapperLastSessionId) {
|
|
369
|
+
const found = sessions.find((s) => s.id === wrapperLastSessionId);
|
|
370
|
+
if (found) return found;
|
|
371
|
+
}
|
|
372
|
+
return sessions[sessions.length - 1];
|
|
373
|
+
}
|
|
374
|
+
getRecentSessions(count) {
|
|
375
|
+
return getRecentSessions(this.getSessions(), count);
|
|
376
|
+
}
|
|
377
|
+
parseSessions() {
|
|
378
|
+
const raw = this.readJsonFile(this.sessionsFilePath);
|
|
379
|
+
if (!raw) return { sessions: [] };
|
|
380
|
+
if (Array.isArray(raw)) return { sessions: raw };
|
|
381
|
+
return { sessions: raw.sessions ?? [], wrapperLastSessionId: raw.lastSessionId };
|
|
382
|
+
}
|
|
383
|
+
readJsonFile(filePath) {
|
|
384
|
+
try {
|
|
385
|
+
if (!fs4.existsSync(filePath)) return void 0;
|
|
386
|
+
const raw = fs4.readFileSync(filePath, "utf-8");
|
|
387
|
+
return JSON.parse(raw);
|
|
388
|
+
} catch {
|
|
389
|
+
return void 0;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
|
|
224
394
|
// src/render.ts
|
|
225
395
|
var RESET = "\x1B[0m";
|
|
226
396
|
var BOLD = "\x1B[1m";
|
|
@@ -300,7 +470,7 @@ async function statusCommand(opts) {
|
|
|
300
470
|
|
|
301
471
|
// src/commands/save.ts
|
|
302
472
|
import readline from "readline";
|
|
303
|
-
import
|
|
473
|
+
import path6 from "path";
|
|
304
474
|
function prompt(rl, question) {
|
|
305
475
|
return new Promise((resolve) => {
|
|
306
476
|
rl.question(question, (answer) => {
|
|
@@ -344,16 +514,16 @@ async function saveCommand(opts) {
|
|
|
344
514
|
workspaceRoot: opts.cwd,
|
|
345
515
|
source: "manual"
|
|
346
516
|
});
|
|
347
|
-
const projectName =
|
|
517
|
+
const projectName = path6.basename(opts.cwd);
|
|
348
518
|
const writer = new KeepGoingWriter(opts.cwd);
|
|
349
519
|
writer.saveCheckpoint(checkpoint, projectName);
|
|
350
520
|
console.log("Checkpoint saved.");
|
|
351
521
|
}
|
|
352
522
|
|
|
353
523
|
// src/commands/hook.ts
|
|
354
|
-
import
|
|
355
|
-
import
|
|
356
|
-
import
|
|
524
|
+
import fs5 from "fs";
|
|
525
|
+
import path7 from "path";
|
|
526
|
+
import os2 from "os";
|
|
357
527
|
var HOOK_MARKER_START = "# keepgoing-hook-start";
|
|
358
528
|
var HOOK_MARKER_END = "# keepgoing-hook-end";
|
|
359
529
|
var ZSH_HOOK = `${HOOK_MARKER_START}
|
|
@@ -379,12 +549,12 @@ fi
|
|
|
379
549
|
${HOOK_MARKER_END}`;
|
|
380
550
|
function detectShellRcFile() {
|
|
381
551
|
const shellEnv = process.env["SHELL"] ?? "";
|
|
382
|
-
const home =
|
|
552
|
+
const home = os2.homedir();
|
|
383
553
|
if (shellEnv.endsWith("zsh")) {
|
|
384
|
-
return { shell: "zsh", rcFile:
|
|
554
|
+
return { shell: "zsh", rcFile: path7.join(home, ".zshrc") };
|
|
385
555
|
}
|
|
386
556
|
if (shellEnv.endsWith("bash")) {
|
|
387
|
-
return { shell: "bash", rcFile:
|
|
557
|
+
return { shell: "bash", rcFile: path7.join(home, ".bashrc") };
|
|
388
558
|
}
|
|
389
559
|
return void 0;
|
|
390
560
|
}
|
|
@@ -400,14 +570,14 @@ function hookInstallCommand() {
|
|
|
400
570
|
const hookBlock = shell === "zsh" ? ZSH_HOOK : BASH_HOOK;
|
|
401
571
|
let existing = "";
|
|
402
572
|
try {
|
|
403
|
-
existing =
|
|
573
|
+
existing = fs5.readFileSync(rcFile, "utf-8");
|
|
404
574
|
} catch {
|
|
405
575
|
}
|
|
406
576
|
if (existing.includes(HOOK_MARKER_START)) {
|
|
407
577
|
console.log(`KeepGoing hook is already installed in ${rcFile}.`);
|
|
408
578
|
return;
|
|
409
579
|
}
|
|
410
|
-
|
|
580
|
+
fs5.appendFileSync(rcFile, `
|
|
411
581
|
${hookBlock}
|
|
412
582
|
`, "utf-8");
|
|
413
583
|
console.log(`KeepGoing hook installed in ${rcFile}.`);
|
|
@@ -426,7 +596,7 @@ function hookUninstallCommand() {
|
|
|
426
596
|
const { rcFile } = detected;
|
|
427
597
|
let existing = "";
|
|
428
598
|
try {
|
|
429
|
-
existing =
|
|
599
|
+
existing = fs5.readFileSync(rcFile, "utf-8");
|
|
430
600
|
} catch {
|
|
431
601
|
console.log(`${rcFile} not found \u2014 nothing to remove.`);
|
|
432
602
|
return;
|
|
@@ -442,21 +612,72 @@ function hookUninstallCommand() {
|
|
|
442
612
|
"g"
|
|
443
613
|
);
|
|
444
614
|
const updated = existing.replace(pattern, "").replace(/\n{3,}/g, "\n\n");
|
|
445
|
-
|
|
615
|
+
fs5.writeFileSync(rcFile, updated, "utf-8");
|
|
446
616
|
console.log(`KeepGoing hook removed from ${rcFile}.`);
|
|
447
617
|
console.log(`Reload your shell config to deactivate it:
|
|
448
618
|
`);
|
|
449
619
|
console.log(` source ${rcFile}`);
|
|
450
620
|
}
|
|
451
621
|
|
|
622
|
+
// src/commands/activate.ts
|
|
623
|
+
async function activateCommand({ licenseKey }) {
|
|
624
|
+
if (!licenseKey) {
|
|
625
|
+
console.error("Usage: keepgoing activate <license-key>");
|
|
626
|
+
process.exit(1);
|
|
627
|
+
}
|
|
628
|
+
const existing = readLicenseCache();
|
|
629
|
+
if (isCachedLicenseValid(existing)) {
|
|
630
|
+
const who2 = existing.customerName ? ` (${existing.customerName})` : "";
|
|
631
|
+
console.log(`Pro license is already active${who2}.`);
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
console.log("Activating license...");
|
|
635
|
+
const result = await activateLicense(licenseKey, getDeviceId());
|
|
636
|
+
if (!result.valid) {
|
|
637
|
+
console.error(`Activation failed: ${result.error ?? "unknown error"}`);
|
|
638
|
+
process.exit(1);
|
|
639
|
+
}
|
|
640
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
641
|
+
writeLicenseCache({
|
|
642
|
+
licenseKey: result.licenseKey || licenseKey,
|
|
643
|
+
instanceId: result.instanceId || getDeviceId(),
|
|
644
|
+
status: "active",
|
|
645
|
+
lastValidatedAt: now,
|
|
646
|
+
activatedAt: now,
|
|
647
|
+
customerName: result.customerName,
|
|
648
|
+
productName: result.productName
|
|
649
|
+
});
|
|
650
|
+
const who = result.customerName ? ` Welcome, ${result.customerName}!` : "";
|
|
651
|
+
console.log(`Pro license activated successfully.${who}`);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// src/commands/deactivate.ts
|
|
655
|
+
async function deactivateCommand() {
|
|
656
|
+
const cache = readLicenseCache();
|
|
657
|
+
if (!cache) {
|
|
658
|
+
console.log("No active license found on this device.");
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
console.log("Deactivating license...");
|
|
662
|
+
const result = await deactivateLicense(cache.licenseKey, cache.instanceId);
|
|
663
|
+
deleteLicenseCache();
|
|
664
|
+
if (!result.deactivated) {
|
|
665
|
+
console.error(`License cleared locally, but remote deactivation failed: ${result.error ?? "unknown error"}`);
|
|
666
|
+
process.exit(1);
|
|
667
|
+
}
|
|
668
|
+
console.log("Pro license deactivated successfully. The activation slot has been freed.");
|
|
669
|
+
}
|
|
670
|
+
|
|
452
671
|
// src/index.ts
|
|
453
672
|
var HELP_TEXT = `
|
|
454
673
|
keepgoing: resume side projects without the mental friction
|
|
455
674
|
|
|
456
675
|
Usage:
|
|
457
|
-
keepgoing status
|
|
458
|
-
keepgoing save
|
|
459
|
-
keepgoing hook
|
|
676
|
+
keepgoing status Show the last checkpoint for this project
|
|
677
|
+
keepgoing save Save a new checkpoint interactively
|
|
678
|
+
keepgoing hook Manage the shell hook
|
|
679
|
+
keepgoing activate <key> Activate a Pro license on this device
|
|
680
|
+
keepgoing deactivate Deactivate the Pro license from this device
|
|
460
681
|
|
|
461
682
|
Options:
|
|
462
683
|
--cwd <path> Override the working directory (default: current directory)
|
|
@@ -491,6 +712,7 @@ function parseArgs(argv) {
|
|
|
491
712
|
subcommand = arg;
|
|
492
713
|
}
|
|
493
714
|
}
|
|
715
|
+
cwd = findGitRoot(cwd);
|
|
494
716
|
return { command, subcommand, cwd, json, quiet };
|
|
495
717
|
}
|
|
496
718
|
async function main() {
|
|
@@ -514,6 +736,12 @@ async function main() {
|
|
|
514
736
|
process.exit(1);
|
|
515
737
|
}
|
|
516
738
|
break;
|
|
739
|
+
case "activate":
|
|
740
|
+
await activateCommand({ licenseKey: subcommand });
|
|
741
|
+
break;
|
|
742
|
+
case "deactivate":
|
|
743
|
+
await deactivateCommand();
|
|
744
|
+
break;
|
|
517
745
|
case "help":
|
|
518
746
|
case "":
|
|
519
747
|
console.log(HELP_TEXT);
|