whydid 1.0.1 → 1.0.2
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.
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
-
import { createDefaultLicenseManager
|
|
2
|
+
import { createDefaultLicenseManager } from "../../licensing/config.js";
|
|
3
|
+
import { LicenseClientError } from "../../licensing/license-client.js";
|
|
4
|
+
const SUPPORT_EMAIL = "hello@whydid.dev";
|
|
3
5
|
export async function runLogin(manager, licenseKey) {
|
|
4
6
|
await manager.activate(licenseKey);
|
|
5
7
|
}
|
|
6
8
|
export async function runLogout(manager) {
|
|
7
|
-
|
|
9
|
+
return manager.deactivate();
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Only the device-limit case gets device-management guidance — every other
|
|
13
|
+
* failure (bad key, network down, server down) showing the same hint was
|
|
14
|
+
* noise at best and, since there's no self-serve devices page yet, a dead
|
|
15
|
+
* link at worst. Points to support directly until that page exists.
|
|
16
|
+
*/
|
|
17
|
+
export function formatLoginFailureMessage(err) {
|
|
18
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
19
|
+
if (err instanceof LicenseClientError && err.code === "MACHINE_LIMIT_REACHED") {
|
|
20
|
+
return [message, `Already using this license on 2 devices? Email ${SUPPORT_EMAIL} to free up a slot.`];
|
|
21
|
+
}
|
|
22
|
+
return [message];
|
|
8
23
|
}
|
|
9
24
|
/** Reads only the locally-stored, already-verified license — no network call, matching whydid's local-first design even for its own status check. */
|
|
10
25
|
export async function runLicenseStatus(manager) {
|
|
@@ -23,8 +38,8 @@ export function createLoginCommand() {
|
|
|
23
38
|
console.log("✓ Activated. Run `whydid license` any time to check status.");
|
|
24
39
|
}
|
|
25
40
|
catch (err) {
|
|
26
|
-
|
|
27
|
-
|
|
41
|
+
for (const line of formatLoginFailureMessage(err))
|
|
42
|
+
console.error(line);
|
|
28
43
|
process.exitCode = 1;
|
|
29
44
|
}
|
|
30
45
|
});
|
|
@@ -34,8 +49,8 @@ export function createLogoutCommand() {
|
|
|
34
49
|
.description("Deactivate this machine")
|
|
35
50
|
.action(async () => {
|
|
36
51
|
const manager = await createDefaultLicenseManager();
|
|
37
|
-
await runLogout(manager);
|
|
38
|
-
console.log("✓ Deactivated this machine.");
|
|
52
|
+
const wasActivated = await runLogout(manager);
|
|
53
|
+
console.log(wasActivated ? "✓ Deactivated this machine." : "Nothing was activated on this machine.");
|
|
39
54
|
});
|
|
40
55
|
}
|
|
41
56
|
export function createLicenseCommand() {
|
package/dist/cli/index.js
CHANGED
|
@@ -49,8 +49,9 @@ async function main() {
|
|
|
49
49
|
const program = new Command("whydid").description("A local time machine for AI-assisted coding").version(PACKAGE_VERSION, "-V, --version");
|
|
50
50
|
const rawArg = process.argv[2];
|
|
51
51
|
const commandArg = rawArg === "doctor" ? "recap" : rawArg; // doctor is an alias registered on the recap Command itself
|
|
52
|
+
const subCommandArg = process.argv[3];
|
|
52
53
|
const { commandNeedsLicense, printLicenseRequiredMessage } = await import("./license-gate.js");
|
|
53
|
-
if (commandNeedsLicense(commandArg)) {
|
|
54
|
+
if (commandNeedsLicense(commandArg, subCommandArg)) {
|
|
54
55
|
const { createDefaultLicenseManager } = await import("../licensing/config.js");
|
|
55
56
|
const manager = await createDefaultLicenseManager();
|
|
56
57
|
await manager.refreshIfNeeded().catch(() => undefined); // best-effort, offline-tolerant
|
package/dist/cli/license-gate.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { PRICING_URL } from "../licensing/config.js";
|
|
2
2
|
/** Never gated — a customer must be able to authenticate/check status/see this message without already having a valid license. */
|
|
3
3
|
const EXEMPT_COMMANDS = new Set(["login", "logout", "license"]);
|
|
4
|
-
|
|
4
|
+
/** Stopping or checking an already-running daemon shouldn't require a valid license — only starting new work (or any other command) does. */
|
|
5
|
+
const EXEMPT_DAEMON_SUBCOMMANDS = new Set(["stop", "status"]);
|
|
6
|
+
export function commandNeedsLicense(commandArg, subCommandArg) {
|
|
5
7
|
if (!commandArg)
|
|
6
8
|
return false; // no args -> shows help
|
|
7
9
|
if (commandArg === "--help" || commandArg === "-h")
|
|
8
10
|
return false;
|
|
9
11
|
if (commandArg === "--version" || commandArg === "-V")
|
|
10
12
|
return false;
|
|
13
|
+
if (commandArg === "daemon" && subCommandArg && EXEMPT_DAEMON_SUBCOMMANDS.has(subCommandArg))
|
|
14
|
+
return false;
|
|
11
15
|
return !EXEMPT_COMMANDS.has(commandArg);
|
|
12
16
|
}
|
|
13
17
|
export function printLicenseRequiredMessage() {
|
|
@@ -58,12 +58,15 @@ export class LicenseManager {
|
|
|
58
58
|
const signed = await this.apiClient.activate(licenseKey, machineId, this.cached?.dodoInstanceId);
|
|
59
59
|
await this.persist(signed);
|
|
60
60
|
}
|
|
61
|
+
/** Returns whether anything was actually activated before this call, so the CLI can give an accurate message rather than always reporting success. */
|
|
61
62
|
async deactivate() {
|
|
63
|
+
const wasActivated = this.cached !== null;
|
|
62
64
|
if (this.cached) {
|
|
63
65
|
await this.apiClient.deactivate(this.cached.licenseId, this.cached.machineId, this.cached.dodoInstanceId).catch(() => undefined);
|
|
64
66
|
}
|
|
65
67
|
await this.store.clear();
|
|
66
68
|
this.cached = null;
|
|
69
|
+
return wasActivated;
|
|
67
70
|
}
|
|
68
71
|
/** Offline-tolerant by design: a failed refresh (no network) leaves the last-known signed license in place, still valid until the grace period actually runs out. */
|
|
69
72
|
async refreshIfNeeded() {
|
package/dist/watcher/watcher.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import chokidar from "chokidar";
|
|
2
|
+
import { relative as nodeRelative } from "node:path";
|
|
2
3
|
import { createCheckpoint } from "../core/engine.js";
|
|
3
4
|
import { annotateCheckpoint } from "../llm/annotate-checkpoint.js";
|
|
4
5
|
import { updateContextTree } from "../core/context-tree.js";
|
|
@@ -6,6 +7,19 @@ import { generateAndSaveSocialPost } from "../llm/social-post.js";
|
|
|
6
7
|
import { NoopProvider } from "../llm/noop-provider.js";
|
|
7
8
|
import { syncClaudeCodeTurns } from "../adapters/sync-claude-code.js";
|
|
8
9
|
const TURN_POLL_MS = 250;
|
|
10
|
+
/**
|
|
11
|
+
* Chokidar normalizes the paths it hands to `ignored` to forward slashes on
|
|
12
|
+
* every platform, but a project root resolved via Node's own path APIs is
|
|
13
|
+
* OS-native (backslashes on Windows) — a naive string-prefix strip silently
|
|
14
|
+
* fails to match there, passing the full absolute path straight through to
|
|
15
|
+
* IgnoreRules, which then throws (the `ignore` package requires a relative
|
|
16
|
+
* path). `path.relative()` correctly handles drive letters, casing, and
|
|
17
|
+
* slash style regardless of platform; `relativeImpl` is overridable only so
|
|
18
|
+
* tests can exercise Windows semantics (`path.win32.relative`) on any host OS.
|
|
19
|
+
*/
|
|
20
|
+
export function toProjectRelativePath(projectRoot, absolutePath, relativeImpl = nodeRelative) {
|
|
21
|
+
return relativeImpl(projectRoot, absolutePath);
|
|
22
|
+
}
|
|
9
23
|
export class ProjectWatcher {
|
|
10
24
|
ctx;
|
|
11
25
|
projectId;
|
|
@@ -90,7 +104,7 @@ export class ProjectWatcher {
|
|
|
90
104
|
this.pollingMode = usePolling;
|
|
91
105
|
const watcher = chokidar.watch(this.ctx.projectRoot, {
|
|
92
106
|
ignored: (path) => {
|
|
93
|
-
const rel =
|
|
107
|
+
const rel = toProjectRelativePath(this.ctx.projectRoot, path);
|
|
94
108
|
return rel !== "" && this.ctx.ignoreRules.isIgnored(rel);
|
|
95
109
|
},
|
|
96
110
|
ignoreInitial: true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "whydid",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"description": "A local-first AI coding time machine — captures meaningful changes, reasoning, and session context so you can see what happened across your coding sessions, even days later.",
|