whydid 1.0.0 → 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.
- package/dist/cli/commands/license.js +21 -6
- package/dist/cli/index.js +2 -1
- package/dist/cli/license-gate.js +5 -1
- package/dist/licensing/config.js +6 -6
- package/dist/licensing/license-manager.js +3 -0
- package/dist/licensing/license-store.js +38 -5
- package/dist/watcher/watcher.js +15 -1
- package/package.json +1 -1
|
@@ -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() {
|
package/dist/licensing/config.js
CHANGED
|
@@ -5,13 +5,13 @@ import { HttpLicenseApiClient } from "./license-client.js";
|
|
|
5
5
|
import { createLicenseStore } from "./license-store.js";
|
|
6
6
|
import { getMachineId } from "./machine-id.js";
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
8
|
+
* The real production Ed25519 public key, matching the private key held
|
|
9
|
+
* only in license-server's deployed .env (LICENSE_ED25519_PRIVATE_KEY) —
|
|
10
|
+
* generated once via license-server/scripts/generate-keypair.ts. Every
|
|
11
|
+
* signed license this public key verifies was signed by that exact
|
|
12
|
+
* deployment; swapping it invalidates every previously-issued license.
|
|
13
13
|
*/
|
|
14
|
-
export const PUBLIC_KEY_DER_BASE64 = process.env.WHYDID_LICENSE_PUBLIC_KEY ?? "
|
|
14
|
+
export const PUBLIC_KEY_DER_BASE64 = process.env.WHYDID_LICENSE_PUBLIC_KEY ?? "MCowBQYDK2VwAyEA2CpLqf2+q26q6q1k77bbJZYvQ1otG4E6iY19e+KNogc=";
|
|
15
15
|
export const LICENSE_API_URL = process.env.WHYDID_LICENSE_API_URL ?? "https://license.whydid.dev";
|
|
16
16
|
export const PRICING_URL = "https://whydid.dev/pricing";
|
|
17
17
|
function globalLicenseDir() {
|
|
@@ -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() {
|
|
@@ -3,6 +3,24 @@ import { createCipheriv, createDecipheriv, createHash, randomBytes } from "node:
|
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
const SERVICE_NAME = "whydid-license";
|
|
5
5
|
const ACCOUNT_NAME = "license";
|
|
6
|
+
async function loadRealKeytar() {
|
|
7
|
+
return import("@napi-rs/keyring/keytar.js");
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The OS keychain prompt (e.g. macOS's "wants to use your confidential
|
|
11
|
+
* information") asks for the user's own Mac login password, not anything
|
|
12
|
+
* whydid-specific — but denying/canceling it throws a raw native error
|
|
13
|
+
* ("User canceled the operation.") straight out of @napi-rs/keyring. Left
|
|
14
|
+
* unwrapped, that string reaches the user verbatim with no context. This
|
|
15
|
+
* wraps ANY keychain-access failure (denied, canceled, or an unexpected
|
|
16
|
+
* platform error) in one clear, actionable message.
|
|
17
|
+
*/
|
|
18
|
+
function wrapKeychainError(err) {
|
|
19
|
+
const raw = err instanceof Error ? err.message : String(err);
|
|
20
|
+
return new Error(`whydid couldn't access your saved license in the system keychain (${raw}). ` +
|
|
21
|
+
`This is asking for your Mac's own login password, not a whydid password — ` +
|
|
22
|
+
`run the command again and click "Allow" (or "Always Allow") when prompted.`);
|
|
23
|
+
}
|
|
6
24
|
/**
|
|
7
25
|
* OS keychain (macOS Keychain / Windows Credential Manager / Linux Secret
|
|
8
26
|
* Service), via @napi-rs/keyring's keytar-compatible API — the maintained
|
|
@@ -12,13 +30,28 @@ const ACCOUNT_NAME = "license";
|
|
|
12
30
|
* this platform/architecture.
|
|
13
31
|
*/
|
|
14
32
|
export class KeychainLicenseStore {
|
|
33
|
+
loadKeytar;
|
|
34
|
+
constructor(loadKeytar = loadRealKeytar) {
|
|
35
|
+
this.loadKeytar = loadKeytar;
|
|
36
|
+
}
|
|
15
37
|
async save(signed) {
|
|
16
|
-
const { setPassword } = await
|
|
17
|
-
|
|
38
|
+
const { setPassword } = await this.loadKeytar();
|
|
39
|
+
try {
|
|
40
|
+
await setPassword(SERVICE_NAME, ACCOUNT_NAME, JSON.stringify(signed));
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
throw wrapKeychainError(err);
|
|
44
|
+
}
|
|
18
45
|
}
|
|
19
46
|
async load() {
|
|
20
|
-
const { getPassword } = await
|
|
21
|
-
|
|
47
|
+
const { getPassword } = await this.loadKeytar();
|
|
48
|
+
let raw;
|
|
49
|
+
try {
|
|
50
|
+
raw = await getPassword(SERVICE_NAME, ACCOUNT_NAME);
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
throw wrapKeychainError(err);
|
|
54
|
+
}
|
|
22
55
|
if (!raw)
|
|
23
56
|
return null;
|
|
24
57
|
try {
|
|
@@ -29,7 +62,7 @@ export class KeychainLicenseStore {
|
|
|
29
62
|
}
|
|
30
63
|
}
|
|
31
64
|
async clear() {
|
|
32
|
-
const { deletePassword } = await
|
|
65
|
+
const { deletePassword } = await this.loadKeytar();
|
|
33
66
|
await deletePassword(SERVICE_NAME, ACCOUNT_NAME).catch(() => false);
|
|
34
67
|
}
|
|
35
68
|
}
|
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.",
|